/** * That stuff is released under the terms of the LGPL license (somewhere here: http://www.gnu.org/copyleft/lesser.html). * You should comply with it, and otherwise do whatever please you with the code. * * Author: dmp */ package org.dmp.bugs.mousesnafu { import flash.display.DisplayObject; import flash.display.Stage; import flash.events.Event; import flash.events.IEventDispatcher; import flash.events.MouseEvent; import flash.events.TimerEvent; import flash.system.Capabilities; import flash.utils.Timer; /** * Firefox 3.6 plugin mouse position snafu */ public class Bugzilla537269 { // Ref to the stage, bomb timer, affected nodes, phase, counter private var m_Stage : Stage; private var m_Timer : Timer; private var m_SnafuNode : IEventDispatcher; private var m_SnafuTwin : IEventDispatcher; private var m_SnafuPhase : int; private var m_SnafuCounter: int; // Dbl click emulation private var m_ClickyNode : IEventDispatcher; private var m_ClickyBomb : Timer; // GC private static var m_KeepAlive: Bugzilla537269; public function Bugzilla537269( yourApp: DisplayObject, dblClickDelay: int = 0 ) { // Only one instance. May be turned into a real singleton. Or not. if( m_KeepAlive ) throw "Only one instance of this."; if( !Capabilities.os.match(/^mac os/i) ) return; // Store a ref to the stage. m_Stage = yourApp.stage; // Hook it up registerEventSnafuHack(); // Keep a ref so we are not garbage collected m_KeepAlive = this; // Remove cleanly when the app goes away yourApp.addEventListener( Event.REMOVED_FROM_STAGE, unregisterEventSnafuHack, false, 0, true ); // Possibly add support for dblclick if( dblClickDelay ) emulateDoubleClick( dblClickDelay ); } /** * Call this as well if you really need double-click handling */ private function emulateDoubleClick( delay: int ): void { trace( " + Double click emulation is enabled " ); m_Stage.addEventListener( MouseEvent.CLICK , dblClickSnafoo, false, 900, true ); m_ClickyBomb = new Timer( delay, 1 ); m_ClickyBomb.addEventListener( TimerEvent.TIMER_COMPLETE, resetClickSnafoo, false, 0, true ); } private function resetClickSnafoo( e: Event ): void { trace( " - Forgetting dbl click mechanic"); m_Stage.removeEventListener( MouseEvent.MOUSE_MOVE , resetClickSnafoo, false ); m_ClickyNode = null; m_ClickyBomb.stop(); } private function dblClickSnafoo( e: MouseEvent ): void { // Did we already have a stored node? And this was the same node? Then it is a doubleClick if( m_ClickyNode && (m_ClickyNode == e.target) ){ trace( " ++++ Dispatching a dblClick"); // Ok, dispatch the event var dblClick: MouseEvent = new MouseEvent( MouseEvent.DOUBLE_CLICK, true, false, e.localX, e.localY, null, e.ctrlKey, e.altKey, e.shiftKey, e.buttonDown, 0 ); m_ClickyNode.dispatchEvent( dblClick ); // Reset the stuff resetClickSnafoo( e ); // Prevent the click from getting through e.stopImmediatePropagation(); }else if( e.target.doubleClickEnabled ){ trace( " + Got a reason to think we might get a dbl click"); m_Stage.addEventListener( MouseEvent.MOUSE_MOVE , resetClickSnafoo, false, 900, true ); // First click. Store the node m_ClickyNode = e.target as DisplayObject; // Start the time bomb m_ClickyBomb.reset(); m_ClickyBomb.start(); } } /** * Called to get the hack in motion */ private function registerEventSnafuHack(): void { trace(" + Attaching snafu hack mechanics"); // Declare a timer to help clean-up the mess m_Timer = new Timer( 1, 2 ); m_Timer.addEventListener( TimerEvent.TIMER, timerListener, false, 1000, true ); // Declare the actual hook on click events m_Stage.addEventListener( MouseEvent.CLICK, onClickHackListener, true, 1000, true ); m_Stage.addEventListener( MouseEvent.CLICK, onClickHackListener, false, 1000, true ); m_SnafuPhase = 0; } /** * Called if we obtain the guarantee you don't need it */ private function unregisterEventSnafuHack( e: Event ): void { trace(" - Removing snafu hack mechanics"); // Remove the clicks hooks m_Stage.removeEventListener( MouseEvent.CLICK, onClickHackListener, true ); m_Stage.removeEventListener( MouseEvent.CLICK, onClickHackListener, false ); // Get rid of the timer m_Timer.stop(); m_Timer.removeEventListener( TimerEvent.TIMER_COMPLETE, timerListener, false ); m_Timer = null; m_KeepAlive = null; } /** * This gets triggered when we catch a click. Declares stuff for interception. */ private function onClickHackListener( e: MouseEvent ): void { // Don't re-attach if we already declared on capture (and are bubbling) if( m_SnafuPhase ) return; trace(" + Snafu mechanics triggering up"); // Setup initial m_SnafuPhase = 1; m_SnafuCounter = 0; m_SnafuNode = e.target as IEventDispatcher; // We will have to eat that stuff m_Stage.addEventListener( MouseEvent.MOUSE_OVER , snafEventSwallow, true, 1000, true ); m_Stage.addEventListener( MouseEvent.MOUSE_OUT , snafEventSwallow, true, 1000, true ); m_Stage.addEventListener( MouseEvent.MOUSE_MOVE , snafEventSwallow, true, 1000, true ); m_Stage.addEventListener( MouseEvent.MOUSE_OVER , snafEventSwallow, false, 1000, true ); m_Stage.addEventListener( MouseEvent.MOUSE_OUT , snafEventSwallow, false, 1000, true ); m_Stage.addEventListener( MouseEvent.MOUSE_MOVE , snafEventSwallow, false, 1000, true ); m_Stage.addEventListener( Event.MOUSE_LEAVE , snafEventSwallow, false, 1000, true ); // And this might get triggered on the node itself (and probably on a related node as well) if( m_SnafuNode != m_Stage ){ m_SnafuNode.addEventListener( MouseEvent.ROLL_OUT , snafEventSwallow, false, 1000, true ); m_SnafuNode.addEventListener( MouseEvent.ROLL_OVER , snafEventSwallow, false, 1000, true ); } // Set the time bomb m_Timer.reset(); m_Timer.start(); } private function timerListener( e: TimerEvent ): void { trace( " / Snafu phase controller triggering on end of phase", m_SnafuPhase ); m_SnafuPhase++; if( m_SnafuPhase > 2){ // Didn't get *anything*? Then there is no bug. Get out of the way. if( !m_SnafuCounter ) unregisterEventSnafuHack( e ); // Either way, end the current mechanic onEndSequence( e ); } } // Call this whenever the spurious flow is done getting through private function onEndSequence( e: Event ): void { trace(" - Snafu mechanics triggering down"); m_Stage.removeEventListener( MouseEvent.MOUSE_OVER , snafEventSwallow, true ); m_Stage.removeEventListener( MouseEvent.MOUSE_OUT , snafEventSwallow, true ); m_Stage.removeEventListener( MouseEvent.MOUSE_MOVE , snafEventSwallow, true ); m_Stage.removeEventListener( MouseEvent.MOUSE_OVER , snafEventSwallow, false ); m_Stage.removeEventListener( MouseEvent.MOUSE_OUT , snafEventSwallow, false ); m_Stage.removeEventListener( MouseEvent.MOUSE_MOVE , snafEventSwallow, false ); m_Stage.removeEventListener( Event.MOUSE_LEAVE , snafEventSwallow, false ); if( m_SnafuNode != m_Stage ){ m_SnafuNode.removeEventListener( MouseEvent.ROLL_OUT , snafEventSwallow, false ); m_SnafuNode.removeEventListener( MouseEvent.ROLL_OVER , snafEventSwallow, false ); } if( m_SnafuTwin ){ m_SnafuTwin.removeEventListener( MouseEvent.ROLL_OUT , snafEventSwallow, false ); m_SnafuTwin.removeEventListener( MouseEvent.ROLL_OVER , snafEventSwallow, false ); } m_SnafuNode = null; m_SnafuTwin = null m_SnafuPhase = 0; m_SnafuCounter = 0; } /** * This the function that actually swallows the stuff. * There is two phases: events that get dispatched due to the wrong mouse position being sent, and these due to the correct mouse repositioning. * Both phases are sandwiched between timerEvents triggers. * During the first phase, we need to get a grasp on the node that gets suddenly hovered if there is one, and prevent stuff to happen on him as well. * We *also* need to register long shot listeners if the position is out of the stage, that will get triggered out of the snafu time window */ private function snafEventSwallow( e: Event ): void { trace(" * Worker snafu phase ", m_SnafuPhase, " is swallowing", e); // Phase 1 handling if( m_SnafuPhase == 1 ){ // MouseLeave and not the stage? We need a defered swallower then if( (e.type == Event.MOUSE_LEAVE) && (m_SnafuNode != m_Stage )){ // When returning, (way) later, we will have to swallow back a mouseOver and a rollOver m_Stage.addEventListener( MouseEvent.MOUSE_OVER , deferedSwallower, true, 1000, true ); m_SnafuNode.addEventListener( MouseEvent.ROLL_OVER , deferedSwallower, false, 1000, true ); }else if( e.type == MouseEvent.MOUSE_MOVE ){ // So, we moved. Did we move far enough to hit another node? If yes, have it swallowed as well. if( e.target != m_SnafuNode ){ m_SnafuTwin = e.target as IEventDispatcher; m_SnafuTwin.addEventListener( MouseEvent.ROLL_OUT , snafEventSwallow, false, 1000, true ); m_SnafuTwin.addEventListener( MouseEvent.ROLL_OVER , snafEventSwallow, false, 1000, true ); } } } // Either way, swallow the event m_SnafuCounter++; e.stopImmediatePropagation(); } // This listener *may* get attached for long-shoot after-match snafus :-) private function deferedSwallower( e: Event ): void { trace(" ------ Defered swallower saved the day on event ", e); e.target.removeEventListener( e.type , deferedSwallower, true ); e.stopImmediatePropagation(); } } }