Copy link to clipboard
Copied
When an object with tabEnabled = false is clicked, the default behavior is to remove focus from the currently focused object and set it to null, after raising a MOUSE_FOCUS_CHANGE event.
That event and behavior is supposed to be cancelable, but it does not fire at all and the behavior occurs anyway, when the clicked object already has the focus.
The problem is that when you click an object in flash, tabEnabled is false by default, so a MOUSE_FOCUS_CHANGE event occurs. If you don't cancel the default behavior, the focus is set to null, and a FOCUS_OUT event is raised on the object that used to have the focus, and that event's relatedObject property is null (since the focus is becoming null).
I was trying to override that default behavior in order to prevent FOCUS_OUT events from having null relatedObjects, because for certain objects such as popup menu, I'd like to check whether the object receiving the focus is a child of the popup menu, in which case I would not want to close the menu. Because the default mechanism assigns focus to null when an object with tabEnabled is clicked, I either have to set tabEnabled to true for every conceivable object that might appear as a child in the menu (sorry, not going to happen), or I have to prevent and override the default behavior of MOUSE_FOCUS_CHANGE, and give focus to the object in spite of its tabEnabled property, in order to ensure the relatedObject of the FOCUS_OUT event is not null, so the handler can check whether focus is actually leaving highest level container for the popup menu.
So that's exactly what I did.
But it's not working when the clicked object already has the focus.
In that case, the focus is forcibly removed from the object without any MOUSE_FOCUS_CHANGE event ever firing. As it's easy to see how this was overlooked, because technically the object already having the focus is clicked, so no event is generated. However, it's still following through with the "tabEnabled=false therefore null the focus" behavior, without having raised a mouse focus change event. This is obviously some simple programming error in the Flash Player. If the focus is going to be changed as a result of a mouse click, then the mouse focus change event should fire, giving me the opportunity to cancel it. Alternatively, if the clicked object already has the focus and the player opts not to dispatch a focus change event, then it should not subsequently set the focus to null, as it is currently and illogically doing.
You can see this behavior exhibited by running the following code on a main timeline script; it's fully self-contained:
import flash.events.MouseEvent;
import flash.events.Event;
import flash.events.FocusEvent;
import flash.display.InteractiveObject;
import flash.display.Sprite;
var box:Sprite = new Sprite();
box.graphics.beginFill( 0x000000, 1 );
box.graphics.drawRect( 100, 100, 100, 100 ); //100x100 rectangle offset 100px on each axis
box.graphics.endFill();
box.tabEnabled = false; //focus not assigned by mouse click; we will override this behavior
box.mouseChildren = false; //treat as single region for sake of clicking, do not distinguish clicks on child objects from that of the parent
box.addEventListener( flash.events.MouseEvent.MOUSE_DOWN, mouseDown, false, 0, true );
box.addEventListener( flash.events.FocusEvent.FOCUS_IN, focusIn, false, 0, true );
box.addEventListener( flash.events.FocusEvent.FOCUS_OUT, focusOut, false, 0, true );
addChild( box );
stage.addEventListener( flash.events.FocusEvent.MOUSE_FOCUS_CHANGE, mouseFocusChange, false, 0, true );
function mouseDown( e:MouseEvent ):void {trace( "mouseDown on " + e.target );}
function focusIn( e:FocusEvent ):void {trace( "focus gained on box, focus used to be " + e.relatedObject );}
function focusOut( e:FocusEvent ):void {trace( "focus lost on box, focus becoming " + e.relatedObject );}
function mouseFocusChange( e:FocusEvent ):void
{
if (e.relatedObject != null) //object receiving the focus
{
trace("mouseFocusChange to " + e.relatedObject + " from " + e.target );
if (!e.relatedObject.tabEnabled)
{
//override default behavior that would set the focus to null, and go ahead and give focus to the clicked object in spite of its tabEnabled value
trace("overriding tabEnabled=false behavior, and assigning focus to clicked object anyway");
e.stopImmediatePropagation();
e.preventDefault();
stage.focus = e.relatedObject;
}
//else proceed with default mouseFocusChange behavior
}
else
trace("mouseFocusChange to nothing from " + e.target );
}
When you run this, click the box one and observe the output. The default behavior is overridden, such that the mouse focus change event is cancelled and focus is assigned to the box in spite of its false tabEnabled value.
Debug output after 1st click:
mouseFocusChange to [object Sprite] from [object Stage]
overriding tabEnabled=false behavior, and assigning focus to clicked object anyway
focus gained on box, focus used to be null
mouseDown on [object Sprite]
When you click the box a second time, I would expect either nothing to occur, since the object already has the focus, OR I would expect the focus change event to fire again, since the player (as you'll see) still plans to set the focus to null because tabEnabled is false. Instead, you'll see that the focus is set to null, but no MOUSE_FOCUS_CHANGE is ever dispatched beforehand to give you the opportunity to cancel the default behavior.
Debug output after 2nd click:
focus lost on box, focus becoming null
mouseDown on [object Sprite]
If you click the box a second time, you'll see that it reverts to the first output since the clicked object does not have the focus anymore. It toggles back and forth each time you click it. Unless I'm missing something, this is a critical flaw in the event system that makes the interception of MOUSE_FOCUS_CHANGE pointless, since the default behavior cannot be intercepted or prevented when the clicked object already has the focus and tabEnabled is false.
I hope someone looks into that after I submit a bug report now that we've talked about this a little bit, but anyway.... I have produced a workaround that is a sort of middle ground.
This will work with all existing complex controls, most of which don't bother with setting tabEnabled on any of its children, and basically alters the default focus change behavior from:
"if tabEnabled is false on clicked object, set focus to null"
to
"if tabEnabled is false on clicked object, walk the parent chain unti
...Copy link to clipboard
Copied
(i'm not sure if anyone else will reply, but if so, just ignore this.)
i had difficultly following your post. can you succinctly state what you want to do?
Copy link to clipboard
Copied
MOUSE_FOCUS_CHANGE is supposed to fire when the mouse is clicked, regardless of whether the clicked object's tabEnabled property is true or false.
In general, that does occur.
There is an edge case where it does not occur when the object already has the focus.
There is a logical conflict or dilemma in that the clicked object already has the focus, so it seems logical that no MOUSE_FOCUS_CHANGE event should be dispatched, but at the same time, the rule that says clicked objects with tabEnabled = false should set the focus to null is occuring anyway, in spite of having not dispatched any MOUSE_FOCUS_CHANGE event, which means you have no opportunity to prevent that default behavior.
Detailed restatement of problem follows, perhaps clearer:
I discovered this issue, because I noticed that FOCUS_OUT events sometimes have a null relatedObject, which occurs when any object is clicked that has tabEnabled = false. That's the default behavior. Focus leaves the current object and becomes null when a non-tab-ordered object is clicked (the default for all DisplayObjects, except SimpleButton and TextField). That makes it hard to handle FOCUS_OUT events, because you don't have any information about what object was clicked. The clicked object may very well be a child of the focused object (e.g. a menu item in a popup menu), but you cannot determine that when the event occurs. I then discovered a workaround. MOUSE_FOCUS_CHANGE precedes the FOCUS_OUT event, is cancelable unlike FOCUS_OUT, and always has a non-null relatedObject (the clicked object that *might* receive the focus, depending on whether its tabEnabled property is true). By intercepting the event, you can force focus on the clicked object in spite of its tabEnabled value, thereby preventing subsequent FOCUS_OUT events having null related object, hence the handler has the information it needs to take appropriate action (close the menu if focus is actually leaving the container altogether). The problem occurs when the clicked object (with tabEnabled = false) already has the focus. In that case, when you click it, no MOUSE_FOCUS_CHANGE event is fired, so there is no opportunity to cancel the default behavior, and the player proceeds to null out the focus anyway.
Copy link to clipboard
Copied
To restate it another way (just left this comment on another forum, as a disclaimer to the behavior overriding approach):
There is a problem with the approach. When the clicked object already has the focus, the player fails to dispatch a MOUSE_FOCUS_CHANGE event (right, because the clicked object already has the focus), but it then proceeds to set the focus to null anyway (right, because non-tabEnabled objects, when clicked, cause the focus to become null), so it doesn't tell you the focus is about to change because why should it, but then it remembers its supposed to null out the focus when that particular type of object (having tabEnabled = false) is clicked. So if such an object has the focus, you have no way to prevent the default focus change behavior. Typically, non-tabEnabled objects would never receive the focus, but they are perfectly valid targets to be assigned to stage.focus.
Ideally, if the object already has the focus and is clicked, nothing should happen. No focus change event should be dispatched, and the focus should not be altered, regardless of whether tabEnabled is false. The object already has the focus, there's no reason to honor tabEnabled = false in such a way that focus is removed from the object without warning and without any opportunity to cancel it.
Update: I also forgot to mention that in the case where a child object of the focused object is clicked, you cannot even intercept the mouse down event, as the MOUSE_FOCUS_CHANGE event (whether it occurs or not) and subsequent nulling of the focus always occurs BEFORE any mouse down events, regardless of the event priority. The are two different events, and MOUSE_FOCUS_CHANGE and corresponding nulling-of-focus behavior when non-tab-enabled objects are clicked always occurs first. So there is no apparent workaround, other than, perhaps, to listen for focus_in events on the menu and immediately force focus onto the parent object, so the edge case is avoided.
Copy link to clipboard
Copied
It may not be a programming error, but a design decision. It's possible that in that specific situation, dispatching the event and allowing it to be canceled would just take way more programming effort than Adobe thinks it's worth. Or it's possible that the nature of tabEnabled = false means that to dispatch the event you'd need to know something that at that point the best design means you shouldn't know.
Regardless, if your menu assets are constructed in such a way as to make it practical, you could consider drawing a transparent shape over all those objects that could be clicked but you don't want to make tabEnabled. Shapes can't receive clicks, but they can prevent objects underneath them from getting them. This would greatly reduce the number of objects you'd need to set tabEnabled to true on and then subsequently manage to not allow focus to go there.
Copy link to clipboard
Copied
Sorry, I disagree. And suggesting that I overlay a shape is speaking at a pre-school level compared to what I'm doing with Flash; I'm not being ungrateful, and you don't know me, but I just had to clarify.
I think its actually a simple programming issue. In fact, the code probably looks exactly like this, where the decision to dispatch the event is handled separately from the decision to alter the focus. In the case where the clicked object already has the focus, it is confounding the notion of "user has not cancelled the behavior" with "we have not asked the user
var cancelled:Boolean = false; //assume it's ok to do default behavior
if (clickedObject != stage.focus) //don't bother notifying/confirming focus change since one is probably not occuring
cancelled = !dispatchEvent( MOUSE_FOCUS_CHANGE);
if (!cancelled) //nevermind, even if we didn't ask the user based on the notion that no focus change was going to occur, we're going to change the focus anyway, HAHA
{
if (clickedObject.tabEnabled)
stage.focus = clickedObject;
else
stage.focus = null;
}
Because that is precisely the behavior I'm seeing, and the sequence of events is very clear.
When it should simply look like this, rolled into one integrated two-level check that's logically consistent:
if (clickedObject != stage.focus) //do not alter focus if clicked object has the focus
{
if (!dispatchEvent( MOUSE_FOCUS_CHANGE)) //focus might change, confirm behavior by notifying user
{
//proceed with default focus change behavior if user allows it
if (clickedObject.tabEnabled)
stage.focus = clickedObject;
else
stage.focus = null;
}
}
And here's the problem with the idea that "the nature of tabEnabled = false means that to dispatch the event you'd need to know something that at that point the best design means you shouldn't know". That contradicts the fact that ANY InteractiveObject instance, regardless of its tabEnabled property value, can be assigned the focus and can receive events. tabEnabled deals strictly with whether user interaction via mouse or tab key will, by default, alter the focus. There is nothing saying an object with tabEnabled = false, can't or shouldn't gain the focus, and rightly so. Furthermore, Adobe exposes two events KEYBOARD_FOCUS_CHANGE and MOUSE_FOCUS_CHANGE, which are both cancelable, suggesting you should be able to intercept and override this default behavior, but it's just not implemented correctly. I am fairly certain that this is a simple case of a novice mistake in the Flash Player code.
Copy link to clipboard
Copied
I hope someone looks into that after I submit a bug report now that we've talked about this a little bit, but anyway.... I have produced a workaround that is a sort of middle ground.
This will work with all existing complex controls, most of which don't bother with setting tabEnabled on any of its children, and basically alters the default focus change behavior from:
"if tabEnabled is false on clicked object, set focus to null"
to
"if tabEnabled is false on clicked object, walk the parent chain until you find one with tabEnabled = true, and set focus to that"
The new logic actually works a LOT better than the default, it avoids the edge case of leaving the focus on a non-tab-enabled object, and it has the side effect of allowing the focus to bubble up to a parent object that's supposed to have the focus. All you have to do is make sure the main control has tabEnabled set to true, and you can leave all the child objects alone. You can even keep mouseChildren true to allow events to occur on the children, such as when your control is so complex it has subcontrols on it.
Here is the handler:
private function mouse_focus_change( e:FocusEvent ):void
{
if (e.relatedObject != null)
{
var target:InteractiveObject = e.relatedObject;
if (!target.tabEnabled)
{
//try to find parent object with tabEnabled and assign focus to that
var p:DisplayObjectContainer = target.parent;
while (p != null)
{
if (p.tabEnabled)
{
//override default behavior that would set the focus to null,
//and go ahead and give focus to the clicked object in spite of its tabEnabled value
//or rather give focus to the first parent having tabEnabled = true (due to a bug that occurs if we leave focus on a non-tabEnabled object)
trace("rerouting mouseFocusChange to " + p + " instead of " + e.relatedObject );
e.stopImmediatePropagation();
e.preventDefault();
stage.focus = p; //assignFocus( p, GUIFocusManagerEvent.DIRECTION_NONE, GUIFocusManagerEvent.CAUSE_MOUSE );
return; //<<UPDATE: FORGOT TO ADD RETURN
}
p = p.parent;
}
}
}
}
Copy link to clipboard
Copied
James22s22 wrote:
Sorry, I disagree. And suggesting that I overlay a shape is speaking at a pre-school level compared to what I'm doing with Flash; I'm not being ungrateful, and you don't know me, but I just had to clarify.
The Scots have a saying: A gentleman is a man who can play the bagpipes, but doesn't. Choosing a simple, non-code solution isn't "preschool." I actually get quite sophisticated results by blending the best of the IDE with really great code in the way no one else does. Sorry, you don't know me, but I just had to clarify.
Copy link to clipboard
Copied
Lol, sorry, I sometimes come across the wrong way. I didn't mean your solution was unsophisticated, I just meant it was too simple for the particular situation I was addressing. The reason I can't draw a single translucent surface over the entire menu, is simply because the menu is composed of multiple objects, each of which must be clickable independently.
Individual objects of course use the technique you suggested. For example, my SkinnedButton class has two properties of type DisplayObject: "surface" and "skin". When any display object is assigned to the surface property, it becomes the hit area for the button, is automatically raised to the top, has its alpha set to zero, and mouseChildren property set to false. The "skin" property houses the MovieClip with the various button states, but the class actually supports named instances on the main object as well, in case you just want to lay all the states out next to each other, and they're automatically moved to the origin and have their visibile set according to the current button state; it's a very flexible class. Anyway, all I meant by "you don't know me" was that I don't fault you for offering a solution that's not a good match for my particular situation and was a technique I'm already familiar with. I really do appreciate the help.
Btw, I left out a "return" statement in the code in my last post, so I updated it.
Copy link to clipboard
Copied
Right, I didn't mean draw a transparent object over the entire menu, but inside each "button" or "option" or what have you. Glad you found a solution that works .
Copy link to clipboard
Copied
Just to be clear, the transparent object over the individual buttons was not the solution, since I was using that technique all along. The behavior of the player is still incorrect, as described in reponse #5 of this thread, and the solution is to override the logic of the mouse_focus_change event as described in post #6.
The reason I am posting now, is that I found another issue with the mouse_focus_change event, requiring an additional modification.
The new problem is that Flash does not update the focus change during or immediately after the mouse_focus_change event is dispatched. This is evidenced by tracing the relatedObject (the object that will be receiving the focus), its tabEnabled value, the current stage focus in the mouse_focus_change handler, and then tracing the current stage focus in the subsequent mouse down event. In the mouse down event, the current stage focus has not been updated to be the relatedObject that was traced when the mouse_focus_change event was dispatched. In other words, the player dispatches a mouse_focus_change event, but does not alter the focus at that time. It lets other events run such as mouse_down, and only alters the focus at some point later.
That's a problem, because the mouse_down event could alter a button's composition (adding/removing children to change its visual state for example) that can result in Flash Player failing to follow through with the focus change to the clicked object. Therefore, to ensure the focus change occurs by the time the mouse_focus_change event completes, I must add an "else" condition to the previously posted code (in response #6), such that the focus assignment is always explicitly made, whether tabEnabled is true or not.
if (!target.tabEnabled)
{
//existing code for this logic block (see response #6)
}
else
{ //new additional code
e.stopImmediatePropagation();
e.preventDefault();
stage.focus = target; //assignFocus( target, GUIFocusManagerEvent.DIRECTION_NONE, GUIFocusManagerEvent.CAUSE_MOUSE );
}
In summary, the solution is to intercept and override the default behavior of the mouse_focus_change event. If the relatedObject (the one that will potentially gain the focus) has it's tabEnabled property set to true, then explicitly assign the stage.focus to it at this time, remembering to call preventDefault. Meanwhile, if tabEnabled is false, instead of allowing the focus to become null (i.e. jump directly up to the stage), just walk up the parent list until a parent with tabEnabled = true is found, and assign focus to that. This ensures that when children of a tabEnabled object (i.e. one which is allowed to gain focus via mouse) are clicked, the object does not lose the focus to the stage.
Find more inspiration, events, and resources on the new Adobe Community
Explore Now