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?