Follow Mouse Cursor Behavior for Flex Tooltip(s)

At work, I have been tasked to implement “Follow Mouse Cursor” feature for a tooltip inside a specified view of the application I’m developing. As you know, in Flex tooltips are handled globally by the ToolTipManager singleton class, and the developer can assign to a UIComponent by setting the toolTip property to a non-null custom string. ToolTipManager allows tooltips customization at the application level – e.g. it affects all tooltips inside the application. My task required to customize a tooltip in a specified view only, leaving other tooltips in the application to default.

There are two strategies:
– use the same ToolTipManager, which also allows using tooltips programmatically
– intercept the current ToolTip and customize it before it is displayed

Since I do not want to be tied into the ToolTipManager, I opted for the second approach. The UIComponent class dispatches several tooltip related events amongst which toolTipShow and toolTipHide are of particular interest. Code will look similar to the one below:

//local tip ref
private var _toolTip:IToolTip;

//add show listener
addEventListener(ToolTipEvent.TOOL_TIP_SHOW, toolTipShowHandler);

//show handler - register mouseMove listener and asjust toolTip position before is shown
protected function toolTipShowHandler(event:ToolTipEvent):void
{
	addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);
	_toolTip = event.toolTip;

	var dObj:DisplayObject = DisplayObject(systemManager);
	_toolTip.move(dObj.mouseX + 10, dObj.mouseY + 22);
}

//mouseMove handler reposition tooltip with the mouse cursor
protected function mouseMoveHandler(event:MouseEvent):void
{
	if (_toolTip)
	{
		_toolTip.move(event.stageX + 11, event.stageY + 22);
		event.updateAfterEvent();
	}
}

//add hide listener
addEventListener(ToolTipEvent.TOOL_TIP_HIDE, toolTipHideHandler);

//clean mouseMode handler, release tip ref
protected function toolTipHideHandler(event:ToolTipEvent):void
{
	removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);
	_toolTip = null;
}

And because this was targeting a custom SkinnableComponent, so I placed the code in the skin and register the listeners on the hostComponent. Task completed.

There are 2 shortcomings with this approach:
– it is not flexible enough to be reused in other views
– the “tooltip” code is mixed with view display logic

The former can be fixed by moving the code into a separate class, while offering the ability to use the class by declaring it inside MXML would take care of the latter. Yet declaring the new class in MXML will prevent me from passing in constructor arguments. Enter IMXMLObject interface:

The IMXMLObject interface defines the APIs that a non-visual component must implement in order to work properly with the MXML compiler. Currently, the only supported method is the initialized() method.

initialized(document:Object, id:String):void

Called after the implementing object has been created and all component properties specified on the MXML tag have been initialized.

Parameters
document:ObjectThe MXML document that created this object.
id:StringThe identifier used by document to refer to this object. If the object is a deep property on document, id is null.

Below is the new class FollowMouseCursor. The code that repositions the tooltip with the mouse has been also extracted into separate defaultTipPositionFunction method. Alternatively clients can supply your own position function by setting the tipPositionFunction to a custom their own custom position function. The custom function has 2 arguments: toolTip:IToolTip – the targeted tooltip, and mousePosition: Point – current mouse position. The default behavior is to enforce the tooltip to stay within screen bounds. This can be turn off by setting the validateInbound property to false.

package claudiu.ursica.tiptracker
{
	import flash.display.DisplayObject;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.geom.Point;

	import mx.core.IMXMLObject;
	import mx.core.IToolTip;
	import mx.core.UIComponent;
	import mx.events.FlexEvent;
	import mx.events.ToolTipEvent;
	import mx.managers.SystemManager;

	import spark.components.supportClasses.Skin;

	public class FollowMouseCursor implements IMXMLObject
	{
		//----------------------------------------------------
		// target
		//----------------------------------------------------
		/**
		 * @private
		 * Storage for <code>target</code> property
		 */
		private var _target:UIComponent;

		//----------------------------------------------------
		// tipOwner
		//----------------------------------------------------
		/**
		 * @private
		 * Storage for tipOwner property
		 */
		private var _tipOwner:UIComponent;

		/**
		 * indicates the tooltip owner, if target is a
		 * chromeless container e.g. group, it will also
		 * the actual tip owner, otherwise the owner will be
		 * the skin's parent
		 */
		protected function get tipOwner():UIComponent
		{
			return _tipOwner;
		}

		/**
		 * @private
		 */
		protected function set tipOwner(value:UIComponent):void
		{
			if (_tipOwner === value)
				return;

			if (_tipOwner)
			{
				_tipOwner.removeEventListener(ToolTipEvent.TOOL_TIP_SHOW, toolTipShowHandler);
				_tipOwner.removeEventListener(ToolTipEvent.TOOL_TIP_HIDE, toolTipHideHandler);
				_tipOwner.removeEventListener("toolTipChanged", toolTipChangedHandler);
			}

			_tipOwner = value;
			if (_tipOwner)
			{
				_tipOwner.addEventListener(ToolTipEvent.TOOL_TIP_SHOW, toolTipShowHandler);
				_tipOwner.addEventListener(ToolTipEvent.TOOL_TIP_HIDE, toolTipHideHandler);
				_tipOwner.addEventListener("toolTipChanged", toolTipChangedHandler);
			}
		}

		//----------------------------------------------------
		// toolTip
		//----------------------------------------------------
		/**
		 * @private
		 * backup variable for tooltip
		 */
		protected var _toolTip:IToolTip;

		//----------------------------------------------------
		// potitionTipFunction
		//----------------------------------------------------
		/**
		 * @private
		 * Storage for positionTip function
		 */
		private var _positionTipFunction:Function = defaultTipPositionFunction;

		/**
		 * Calculates the Point at which the tooltip will be
		 * positioned
		 *
		 * @return Point new tooltip position
		 */
		public function get positionTipFunction():Function
		{
			return _positionTipFunction;
		}

		/**
		 * @private
		 * @param value
		 */
		public function set positionTipFunction(value:Function):void
		{
			if (_positionTipFunction == value || (value == null))
				return;

			_positionTipFunction = value;
		}

		//--------------------------------------------
		// validateTipBounds
		//--------------------------------------------
		/**
		 * indicates whether the tooltip should
		 * be reposition inside screen bounds
		 */
		public var validateInbound:Boolean = true;

		//--------------------------------------------
		// initialized()
		//--------------------------------------------
		/**
		 * @param document Object
		 * @param id String
		 */
		public function initialized(document:Object, id:String):void
		{
			if (!(document is UIComponent))
			{
				throw new ArgumentError("Document must be an UIComponent.");
			}

			_target = document as UIComponent;
			_target.addEventListener(FlexEvent.CREATION_COMPLETE, creationCompleteHandler);
		}

		//--------------------------------------------
		// dispose()
		//--------------------------------------------
		/**
		 * Convenience dispose method
		 */
		public function dispose():void
		{
			tipOwner = null;

			_target = null;
			_toolTip = null;
			_positionTipFunction = null;
		}

		//----------------------------------------------------
		// creationCompleteHandler()
		//----------------------------------------------------
		/**
		 * @private
		 * get the tip owner
		 */
		protected function creationCompleteHandler(event:FlexEvent):void
		{
			_target.removeEventListener(FlexEvent.CREATION_COMPLETE, creationCompleteHandler);
			tipOwner = getTipOwner();
		}

		//----------------------------------------------------
		// getTipOwner()
		//----------------------------------------------------
		/**
		 * @private
		 * @return
		 */
		protected function getTipOwner():UIComponent
		{
			if (_target is Skin)
			{
				return UIComponent(_target.parent);
			}

			return _target;
		}

		//----------------------------------------------------
		// toolTipShowHandler()
		//----------------------------------------------------
		/**
		 * @private
		 * hijacks the tooltip and assign it it's backup variable
		 * alo do intial repositioning to avoid gap between default
		 * position and custom position implemented in the
		 * positionTipFunction
		 */
		protected function toolTipShowHandler(event:ToolTipEvent):void
		{
			_tipOwner.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);

			const obj:DisplayObject = DisplayObject(_tipOwner.systemManager);
			_toolTip = event.toolTip;

			var newPosition:Point = positionTipFunction(_toolTip, new Point(obj.mouseX, obj.mouseY));
			if (validateInbound)
			{
				newPosition = getValidatedTipPosition(_toolTip, newPosition);
			}

			_toolTip.move(newPosition.x, newPosition.y);
		}

		//----------------------------------------------------
		// toolTipHideHandler()
		//----------------------------------------------------
		/**
		 * @private
		 * releases the assigned tooltip
		 */
		protected function toolTipHideHandler(event:ToolTipEvent):void
		{
			_tipOwner.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);
			_toolTip = null;
		}

		//----------------------------------------------------
		// mouseMoveHandler()
		//----------------------------------------------------
		/**
		 * @private
		 * updates tooltip position with the new moust coordinates
		 */
		protected function mouseMoveHandler(event:MouseEvent):void
		{
			if (_toolTip)
			{
				var newPosition:Point = positionTipFunction(_toolTip, new Point(event.stageX, event.stageY));
				if (validateInbound)
				{
					newPosition = getValidatedTipPosition(_toolTip, newPosition);
				}

				_toolTip.move(newPosition.x, newPosition.y);
				event.updateAfterEvent();
			}
		}

		//----------------------------------------------------
		// toolTipChangedHandler
		//----------------------------------------------------
		/**
		 * @private
		 * updates tooltip display text when displayed and
		 * target tooltip value is changed
		 */
		protected function toolTipChangedHandler(event:Event):void
		{
			if (_toolTip)
			{
				_toolTip.text = _tipOwner.toolTip;
			}
		}

		//----------------------------------------------------
		// defaultTipPositionFunction()
		//----------------------------------------------------
		/**
		 * @private
		 * the default tipPositionFunction
		 *
		 * @param mousePosition Point current mouse x,y position
		 * @param toolTip current tooltip
		 */
		protected function defaultTipPositionFunction(toolTip:IToolTip, mousePosition:Point):Point
		{
			return new Point(mousePosition.x - 5, mousePosition.y - toolTip.height - 5);
		}

		//----------------------------------------------------
		// getValidatedTipPosition()
		//----------------------------------------------------
		/**
		 * @private
		 * ensure that the new position is within screen bounds
		 *
		 * @param toolTip current tooltip
		 * @param mousePosition Point current mouse x,y position
		 */
		private function getValidatedTipPosition(toolTip:IToolTip, point:Point):Point
		{
			const screenWidth:Number = toolTip.screen.width;
			const screenHeight:Number = toolTip.screen.height;
			const toolTipWidth:Number = toolTip.width;
			const toolTipHeight:Number = toolTip.height;

			var x:Number = point.x;
			var y:Number = point.y;

			if (x < 0) 
			{ 				
				x = 0; 			
			} 			 			
			
			if (x + toolTipWidth > screenWidth)
			{
				x = screenWidth - toolTipWidth;
			}

			if (y < 0) 			
			{ 				
				y = 0; 			
			} 			 			
			
			if (y + toolTipHeight > screenHeight)
			{
				y = screenHeight - toolTipHeight;
			}

			var newPos:Point = new Point(x, y);
			newPos = DisplayObject(_tipOwner.systemManager).localToGlobal(newPos);
			newPos = DisplayObject(_tipOwner.systemManager.getSandboxRoot()).globalToLocal(newPos);
			return newPos;
		}
	}
}

Usage:

<fx:Declarations>
	<tiptracker:FollowMouseCursor id="followMouseCursor"/>
</fx:Declarations>

See it in action:


Get Adobe Flash player

DOWNLOAD PROJECT!

Leave a Reply

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

*