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.