|
-
stage and static, coding nasties?
Ok, here's an example of using the stage directly for a static (singleton for slackers) TootlTipManager class. The short story is ToolTipManager checks every MOUSE_OVER event target to see if it implements IToolTippable. It also listens for explicit TIP_ITEM_OVER, TIP_ITEM_OUT events which are used when the ITooltippable object does not directly dispatch mouse events, i.e. they are being forwarded by IToolTippable from a composit object.
ToolTipManager does not need to know anything about the document class, or the display hierarchy it just needs a reference to the stage so it can wait for IToolTippable objects. Atleast in my mind, there can only ever be one tooltip and it must aways be on top within the active display area, hence static and direct stage access.
It has been a few years since I wrote this and it has worked well with no modifications but I look at it now and then and think it is closer to procedural then OO. Do you think there is anything wrong with that, both in general and as implemented? Let me have it if you think it's just crap.
Actionscript Code:
package com.company.ui { import flash.text.*; import flash.display.*; import flash.events.*; import flash.utils.*; import fl.transitions.*; import fl.transitions.easing.*; public class ToolTipManager { static public var toolTip:ToolTip = new ToolTip(); static public var fadeTween:Tween = new Tween( toolTip, "alpha", None.easeNone, 0.0, 1.0, 0.25, true ); static public var timer:Timer = new Timer( 500 ); static public var defaultTipOffsetX:Number = 0; static public var defaultTipOffsetY:Number = 21; static public var xml:XMLList; static public const TIP_ITEM_OVER:String = "tipItemOver"; static public const TIP_ITEM_OUT:String = "tipItemOut"; static private var tipItem:IToolTippable; static private var stage:Stage; static private var _enabled:Boolean; static public function init( _stage:Stage, _xml:XMLList, state:Boolean = true ){ stage = _stage; xml = _xml; if( xml.hasOwnProperty( "@delay" ) ) timer.delay = Number( xml.@delay ); enabled = state; } static public function set enabled( state:Boolean ):void { _enabled = state; if( _enabled ){ timer.addEventListener( TimerEvent.TIMER, showToolTip ); stage.addEventListener( MouseEvent.MOUSE_OVER, onItemOver ); stage.addEventListener( MouseEvent.MOUSE_OUT, onItemOut ); stage.addEventListener( TIP_ITEM_OVER, onItemOver ); stage.addEventListener( TIP_ITEM_OUT, onItemOut ); }else{ timer.removeEventListener( TimerEvent.TIMER, showToolTip ); stage.removeEventListener( MouseEvent.MOUSE_OVER, onItemOver ); stage.removeEventListener( MouseEvent.MOUSE_OUT, onItemOut ); stage.removeEventListener( TIP_ITEM_OVER, onItemOver ); stage.removeEventListener( TIP_ITEM_OUT, onItemOut ); onItemOut( null ); } } static public function get enabled():Boolean { return _enabled; } static private function onItemOver( e:MouseEvent ):void { var newTipItem:IToolTippable = e.target as IToolTippable; if( !newTipItem ) return; if( newTipItem != tipItem && e.buttonDown ) return; tipItem = newTipItem; stage.addEventListener( MouseEvent.MOUSE_MOVE, restartTimer ); stage.addEventListener( MouseEvent.MOUSE_DOWN, restartTimer ); stage.addEventListener( MouseEvent.MOUSE_UP, restartTimer ); timer.start(); } static private function onItemOut( e:Event ):void { stage.removeEventListener( MouseEvent.MOUSE_MOVE, restartTimer ); stage.removeEventListener( MouseEvent.MOUSE_DOWN, restartTimer ); stage.removeEventListener( MouseEvent.MOUSE_UP, restartTimer ); hideToolTip(); } static private function restartTimer( e:MouseEvent ):void { hideToolTip(); timer.start(); } static private function showToolTip( e:TimerEvent ):void { var tipDisplay:DisplayObject = tipItem as DisplayObject; if( !tipDisplay.stage || !tipDisplay.hitTestPoint( tipDisplay.stage.mouseX, tipDisplay.stage.mouseY, true ) ){ onItemOut( e ); return; } timer.stop(); var tipName:String = tipItem.tipName ? tipItem.tipName : getQualifiedClassName( tipItem ).split( "::" ).pop(); var tipString:String = tipItem.constructTip( xml.child( tipName ) ); if( !tipString ) return; toolTip.setText( tipString ); var toolTipLeft:Number = isNaN( tipItem.tipOffsetX ) ? defaultTipOffsetX : tipItem.tipOffsetX; toolTipLeft += stage.mouseX; var toolTipTop:Number = isNaN( tipItem.tipOffsetY ) ? defaultTipOffsetY : tipItem.tipOffsetY; toolTipTop += stage.mouseY; var toolTipRight:int = toolTipLeft + toolTip.width; var toolTipBottom:int = toolTipTop + toolTip.height; toolTip.x = Math.max( 0, toolTipRight < stage.stageWidth ? toolTipLeft : stage.stageWidth - toolTip.width ); toolTip.y = Math.max( 0, toolTipBottom < stage.stageHeight ? toolTipTop : stage.mouseY - toolTip.height ); fadeTween.start(); stage.addChild( toolTip ); } static private function hideToolTip():void { timer.reset(); if( toolTip.parent ) toolTip.parent.removeChild( toolTip ); } } }
-
Given your assumptions about the fact that there is only ever one tooltip and that it should be on top of everything else, this is not a bad way to go about it.
I would not have made those assumptions. With multitouch (which I know was not a possibility way back when this was written), multiple tooltips could be a perfectly valid scenario. Also, it seems more natural to me to add a tooltip as a child of whatever IToolTippable has that tool tip. But that's definitely a matter of choice.
I do like the use of event bubbling here to consolidate all the tooltip event handling into a single point. This would be more a little more efficient than having hundreds of individual IToolTippables all listening for those same events. But, that also assumes that the events remain un-messed-with all the way to the stage. Any intermediate parent could listen for the mouse_over event and stop propagation on it. That would prevent the tooltips from working.
It also seems that you are processing an xml resource to get the content of the tips, which is another centralized design consideration that I had not anticipated. Given that requirement, a ToolTipManager class is a good idea. I would have made a real singleton for that, and had each IToolTippable register with it, preferably during construction. Instead of hardcoding the stage, the manager could be passed a DisplayObjectContainer to attach listeners to and/or put the tooltip up on, if that should be centralized.
Even with all the centralization requirements, you could substitue root for stage in the above code. That would prevent this swf from interfering with other swfs, should it be loaded into the same virtual machine.
-
I did consider using a generic DisplayObjectContainer instead of stage, but since the tooltip x/y positioning requires access to stage.stageWidth/stage.stageHeight I wanted to avoid null stage errors. Perhaps it is heavy handed and null stage should be dealt with.
There are situations where IToolTipabble (or its parent) is scaled/rotated so a neutral coordinate space is required to display the tooltip. I think you’re right root would be a better choice, both for the listeners and the tooltip, and agree better still to separate them.
Since switching from AS2, when I stopped using a swf component model, I have not used root. I pretty much forgot it even existed. Mostly I do modular RIAs and microsites (< 1MG), where any externally loaded swfs are for display only. There definitely could be conflicts not using a true singleton, especially if loading something with IToolTippable objects.
I don’t quite follow having IToolTippables registerer with the manager. Would that be for more granular control, i.e. replacing:
PHP Code:
var tipItem:IToolTippable = e.target as IToolTippable;
with:
PHP Code:
var tipItem:IToolTippable = tipItems[ tipItems.indexOf( e.target ) ];
Thanks for the input. All good stuff to consider when refactoring in some multi-touch future.
Posting Permissions
- You may not post new threads
- You may not post replies
- You may not post attachments
- You may not edit your posts
-
Forum Rules
|
Click Here to Expand Forum to Full Width
|