Once again, I welcome guest blogger Matt Whitaker, who continues his series of articles.
In my last post I explored some tips on tooltips. One of these tips involved displaying tooltips on disabled uicontrols. I explained that displaying tooltips on inactive controls is problematic since Matlab appears to intercept mouse events to these inactive controls, so even setting the tooltip on the underlying Java object will not work: The java object appears not to receive the mouse-hover event and therefore does not “know” that it’s time to display the tooltip.
When Yair and I deliberated this issue, he pointed me to his comment on a previous article showing an undocumented Java technique (Java also has some…) for forcing a tooltip to appear using the ActionMap of the uicontrol’s underlying Java object to get at a postTip action. We discussed using a WindowButtonMotionFcn callback to see if the mouse was above the inactive control, then triggering the forced tooltip display. Yair then went on to remind me and I quote: “you’ll need to chain existing WindowButtonMotionFcn callbacks and take into account ModeManagers that override them.”
Frankly, having written code previously that handles callback chaining, I would rather poke myself in the eye with a fork!
The Image Processing Toolbox has the nice pair of iptaddcallback and iptremovecallback functions that largely handle these issues. But for general Matlab, there seemed to be no alternative until I remembered that events trigger callbacks. I decided to use a listener for the WindowButtonMotion event to detect the mouse motion. Event listeners were briefly explained two weeks ago and deserve a dedicated future article. The advantage of using an event listener is that we don’t disturb any existing WindowButtonMotionFcn callback. We still need to be somewhat careful that our listeners don’t do conflicting things, but it’s a lot easier than trying to manage everything through the single WindowButtonMotionFcn.
A demonstration of this appears below with some comments following (note that this code uses the FindJObj utility):
function inactiveBtnToolTip %Illustrates how to make a tooltip appear on an inactive control h = figure('WindowButtonMotionFcn',@windowMotion,'Pos',[400,400,200,200]); col = get(h,'color'); lbl = uicontrol('Style','text', 'Pos',[10,160,120,20], ... 'Background',col, 'HorizontalAlignment','left'); btn = uicontrol('Parent',h, 'String','Button', ... 'Enable','inactive', 'Pos',[10,40,60,20]); uicontrol('Style','check', 'Parent',h, 'String','Enable button tooltip', ... 'Callback',@chkTooltipEnable, 'Value',1, ... 'Pos',[10,80,180,20], 'Background',col); drawnow; %create the tooltip and postTip action jBtn = findjobj(btn); import java.awt.event.ActionEvent; javaMethodEDT('setToolTipText',jBtn,'This button is inactive'); actionMap = javaMethodEDT('getActionMap',jBtn); action = javaMethodEDT('get',actionMap,'postTip'); actionEvent = ActionEvent(jBtn, ActionEvent.ACTION_PERFORMED, 'postTip'); %get the extents plus 2 pixels of the control to compare to the mouse position btnPos = getpixelposition(btn)+[-2,-2,4,4]; %give a little band around the control left = btnPos(1); right = sum(btnPos([1,3])); btm = btnPos(2); top = sum(btnPos([2,4])); % add a listener on mouse movement events tm = javax.swing.ToolTipManager.sharedInstance; %tooltip manager pointListener = handle.listener(h,'WindowButtonMotionEvent',@figMouseMove); %inControl is a flag to prevent multiple triggers of the postTip action %while mouse remains in the button inControl = false; function figMouseMove(src,evtData) %#ok %get the current point cPoint = evtData.CurrentPoint; if cPoint(1) >= left && cPoint(1) <= right &&... cPoint(2) >= btm && cPoint(2) <= top if ~inControl %we just entered inControl = true; action.actionPerformed(actionEvent); %show the tooltip end %if else if inControl %we just existed inControl = false; %toggle to make it disappear when leaving button javaMethodEDT('setEnabled',tm,false); javaMethodEDT('setEnabled',tm,true); end %if end %if end %gpMouseMove function windowMotion(varargin) %illustrate that we can still do a regular window button motion callback set(lbl,'String',sprintf('Mouse position: %d, %d',get(h,'CurrentPoint'))); drawnow; end %windowMotion function chkTooltipEnable(src,varargin) if get(src,'Value') set(pointListener,'Enable','on'); else set(pointListener,'Enable','off'); end %if end %chkTooltipEnable end %inactiveBtnToolTip |
Comments on the code:
- The code illustrates that we can successfully add an additional listener to listen for mouse motion events while still carrying out the original WindowButtonMotionFcn callback. This makes chaining callbacks much easier.
- The handle.listener object has an Enable property that we can use to temporarily turn the listener on and off. This can be seen in the chkTooltipEnable() callback for the check box in the code above. If we wanted to permanently remove the listener we would simply use delete(pointListener). Note that addlistener adds a hidden property to the object being listened to, so that the listener is tied to the object’s lifecycle. If you create a listener directly using handle.listener you are responsible for it’s disposition. Unfortunately, addlistener fails for HG handles on pre-R2009 Matlab releases, so we use handle.listener directly.
- The code illustrates a good practice when tracking rapidly firing events like mouse movement of handling reentry into the callback while it is still processing a previous callback. Here we use a flag called inControl to prevent the postTip action being continuously fired while the mouse remains in the control.
- I was unable to determine if there is any corresponding action for the postTip to dismiss tips so I resorted to using the ToolTipManager to toggle its own Enable property to cleanly hide the tooltip as the mouse leaves the control.
Each Matlab callback has an associated event with it. Some of the ones that might be immediately useful at the figure-level are WindowButtonDown, WindowButtonUp, WindowKeyPress, and WindowKeyRelease. They can all be accessed through handle.listener or addlistener as in the code above.
Unfortunately, events do not always have names that directly correspond to the callback names. In order to see the list of available events for a particular Matlab object, use the following code, which relies on another undocumented function – classhandle. Here we list the events for gcf:
>> get(get(classhandle(handle(gcf)),'Events'),'Name') ans = 'SerializeEvent' 'FigureUpdateEvent' 'ResizeEvent' 'WindowKeyReleaseEvent' 'WindowKeyPressEvent' 'WindowButtonUpEvent' 'WindowButtonDownEvent' 'WindowButtonMotionEvent' 'WindowPostChangeEvent' 'WindowPreChangeEvent' |
Note that I have made extensive use of the javaMethodEDT function to execute Java methods that affect swing components on Swing’s Event Dispatch Thread. I plan to write about this and related functions in my next article.
I realize this isn’t an undocumented feature, but I note you’ve had a couple event/listener features explored here. I’ve implemented this construct as a means to have handle class objects interact with one another such that I can have one object contain many others. I can then pass the container into a GUI which establishes listeners to the various contained object’s events so that it will update the GUI as necessary. The problem I’ve run into is that the calls are computationally expensive (on the order of tenths of seconds or more per call), and according to the profile viewer, the time is entirely “overhead” in calling the listener – not the function the listener actually performs when the event is triggered as I was expecting. I’m not sure what is causing the latency, but it makes the whole interface crawl if the objects are highly active.
It’s a real shame too since the event/listener construct is a really elegant way to make one’s objects interface-agnostic in a model-viewer relationship. To give you an idea of scale, I have 50 objects with 3 events each being listened to by a single viewer.
Does anyone know of a way to speed up the event/listener overhead or make the profile cough up what ‘overhead’ is actually taking so long?
@Thomas – I suspect a case of endless recursion due to the contained object’s listener triggering its parent which triggers back the child and so on until you reach your recursion limit (or something similar to that effect).
To test this hypothesis, try triggering some entirely external Matlab function (i.e., a function outside any class) and see whether the overhead remains. If my hunch is correct, then you’ll see the overhead disappear. If not, then you’ll have a very simple example to submit with your bug report to isupport@mathworks.com.
There are several ways to fix such recursive triggering. Here is a simple bare-bones implementation that you can use:
Weird – you might be on to something, but for the life of me I am not seeing where the recursion is happening. Actually, I’m getting the feeling “overhead” is taking into consideration the time the GUI is usingto draw/refresh objects.
If I replace all callbacks the GUI does with a function that draws/updates nothing to the screen (writes to the command window), the overhead of the events drops to nothing. Yet if I tie it to the GUI updating callbacks, the only thing listed in the event and callback function is overhead.
Maybe I need to look at a different way to draw the objects to the screen? I’m already re-using object handles, so I’m not sure how much more optimized it can be – haha.
I guess I’ll keep digging. Thank you for the insight.
Of some relevance to this, I have just posted a mouse motion handle class to the FEX. MouseMotionHandler is a MATLAB class that uses a MATLAB figure’s WindowButtonMotionFcn callback as a hook to extend button motion functionality to all handle graphics child objects of a figure including axes, lines, surfaces uipanels, uicontrols etc.
http://www.mathworks.com/matlabcentral/fileexchange/29913-mouse-motion-handler
MouseMotionHandler provides an alternative to chaining callbacks or having a lengthy switch block in a WindowButtonMotionFcn callback to manage mouse motion effects. MouseMotionHandler puts its own callback into a figure’s WindowButtonMotionFcn property. This callback manages the servicing of callbacks for other objects in the figure. It actively determines what is beneath the mouse then invokes a user-specified callback for that object, if one is set. Specify these objects and their mouse motion callbacks using the MouseMotionHandler add and put methods. MouseMotionHandler also extends the functionality of the standard WindowButtonMotionFcn callback: its internal logic discriminates between mouse entered, moved and exited calls.
@Malcolm – Thanks for the pointer. It appears to be a very useful function for heavily-laden GUIs.
You might wish to modify the sigtools link at the bottom of your demo to a blue-underlined hyperlink. I posted an article that discussed this in 2009, as well as some code and discussion on CSSM.
[…] I have already posted several articles about how to tweak tooltip contents (here, here, and here). Today I would like to expand on one of the aspects that were already covered […]
Can you use this method for a multi-line tooltip on an inactive control? The solution below, which works for Matlab’s tooltipstring, does not work here.
@Steven – you can use HTML formatting to achieve multi-line tooltips, as explained here.