Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

avm2: Properly implement Stage.tabChildren #16608

Merged
merged 6 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions core/src/avm2/globals/flash/display/Stage.as
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,12 @@ package flash.display {
}

override public function get tabChildren():Boolean {
return super.tabChildren;
// stage.tabChildren is always true,
// even if its setter was called with false
return true;
}

override public function set tabChildren(value:Boolean):void {
// Docs say that this operation throws IllegalOperationError,
// but in reality this call is just ignored.
}
override public native function set tabChildren(value:Boolean):void;

override public function set tabEnabled(value:Boolean):void {
throw new IllegalOperationError("Error #2071: The Stage class does not implement this property or method.", 2071);
Expand Down
26 changes: 25 additions & 1 deletion core/src/avm2/globals/flash/display/stage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ use crate::avm2::parameters::ParametersExt;
use crate::avm2::value::Value;
use crate::avm2::vector::VectorStorage;
use crate::avm2::Error;
use crate::display_object::{StageDisplayState, TDisplayObject, TInteractiveObject};
use crate::display_object::{
StageDisplayState, TDisplayObject, TDisplayObjectContainer, TInteractiveObject,
};
use crate::string::{AvmString, WString};
use crate::{avm2_stub_getter, avm2_stub_setter};
use swf::Color;
Expand Down Expand Up @@ -480,3 +482,25 @@ pub fn get_full_screen_width<'gc>(
avm2_stub_getter!(activation, "flash.display.Stage", "fullScreenWidth");
Ok(1024.into())
}

pub fn set_tab_children<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
if let Some(stage) = this.as_display_object().and_then(|this| this.as_stage()) {
// TODO FP actually refers here to the original root,
// even if it has been removed.
// See the test tab_ordering_stage_tab_children_remove_root.
if let Some(root) = stage.root_clip() {
if let Some(root) = root.as_container() {
// Stage's tabChildren setter just propagates the value to the AVM2 root.
// It does not affect the value of tabChildren of the stage, which is always true.
let value = args.get_bool(0);
root.set_tab_children(&mut activation.context, value);
}
}
}

Ok(Value::Undefined)
}
10 changes: 10 additions & 0 deletions core/src/debug_ui/display_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,16 @@ impl DisplayObjectWindow {
}
});
ui.end_row();

ui.label("Tab children enabled");
ui.horizontal(|ui| {
let mut enabled = obj.is_tab_children(context);
Checkbox::new(&mut enabled, "Enabled").ui(ui);
if enabled != obj.is_tab_children(context) {
obj.set_tab_children(context, enabled);
}
});
ui.end_row();
}
});

Expand Down
11 changes: 10 additions & 1 deletion core/src/display_object/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,14 @@ pub trait TDisplayObjectContainer<'gc>:
true
}

/// The property `tabChildren` allows changing the behavior of
/// tab ordering hierarchically.
/// When set to `false`, it excludes the whole subtree represented by
/// the container from tab ordering.
///
/// _NOTE:_
/// According to the AS2 documentation, it should affect only automatic tab ordering.
/// However, that does not seem to be the case, as it also affects custom ordering.
fn is_tab_children(&self, context: &mut UpdateContext<'_, 'gc>) -> bool {
if context.swf.is_action_script_3() {
self.raw_container().tab_children
Expand All @@ -498,7 +506,8 @@ pub trait TDisplayObjectContainer<'gc>:
if context.swf.is_action_script_3() {
self.raw_container_mut(context.gc()).tab_children = value;
} else {
tracing::warn!("Trying to set tab_children on an AVM1 object, this has no effect")
let self_do: DisplayObject<'gc> = (*self).into();
self_do.set_avm1_property(context, "tabChildren", value.into());
}
}

Expand Down
8 changes: 0 additions & 8 deletions core/src/display_object/movie_clip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2985,14 +2985,6 @@ impl<'gc> TDisplayObjectContainer<'gc> for MovieClip<'gc> {
RefMut::map(self.0.write(gc_context), |this| &mut this.container)
}

/// The property `MovieClip.tabChildren` allows changing the behavior of
/// tab ordering hierarchically.
/// When set to `false`, it excludes the whole subtree represented by
/// the movie clip from tab ordering.
///
/// _NOTE:_
/// According to the AS2 documentation, it should affect only automatic tab ordering.
/// However, that does not seem to be the case, as it also affects custom ordering.
fn is_tab_children_avm1(&self, context: &mut UpdateContext<'_, 'gc>) -> bool {
self.get_avm1_boolean_property(context, "tabChildren", |_| true)
}
Expand Down
68 changes: 68 additions & 0 deletions tests/tests/swfs/avm2/tab_ordering_stage_tab_children/Test.as
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package {

import flash.display.MovieClip;
import flash.events.Event;
import flash.events.KeyboardEvent;

public class Test extends MovieClip {
private var testStage: int = 0;
private var clip1: MovieClip;
private var clip2: MovieClip;
private var clip3: MovieClip;

public function Test() {
super();

clip1 = new MovieClip();
clip1.tabEnabled = true;
clip1.tabIndex = 1;
clip2 = new MovieClip();
clip2.tabEnabled = true;
clip2.tabIndex = 2;
clip3 = new MovieClip();
clip3.tabEnabled = true;
clip3.tabIndex = 3;

stage.addChild(clip1);
MovieClip(root).addChild(clip2);
stage.tabChildren = true;
printProps();

clip1.addEventListener("focusIn", function (evt:Event):void {
trace("clip1 focusIn: " + evt.toString());
});
clip2.addEventListener("focusIn", function (evt:Event):void {
trace("clip2 focusIn: " + evt.toString());
});

stage.addEventListener("keyDown", function(evt:KeyboardEvent):void {
if (evt.keyCode == 27) {
trace("Escape pressed");
testStage += 1;
if (testStage == 1) {
trace("Setting tabChildren to false");
stage.focus = null;
stage.tabChildren = false;
printProps();
} else if (testStage == 2) {
trace("Setting tabChildren to true");
stage.tabChildren = true;
printProps();
trace("Adding a child at 0 and setting tabChildren to false");
stage.addChildAt(clip3, 0);
stage.tabChildren = false;
printProps();
}
}
});
}

private function printProps():void {
trace("stage.tabChildren = " + stage.tabChildren);
trace("root.tabChildren = " + MovieClip(root).tabChildren);
trace("clip1.tabChildren = " + clip1.tabChildren);
trace("clip2.tabChildren = " + clip2.tabChildren);
trace("clip3.tabChildren = " + clip3.tabChildren);
}
}
}
16 changes: 16 additions & 0 deletions tests/tests/swfs/avm2/tab_ordering_stage_tab_children/input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[
{ "type": "KeyDown", "key_code": 9 },
{ "type": "KeyDown", "key_code": 9 },
{ "type": "KeyDown", "key_code": 9 },
{ "type": "KeyDown", "key_code": 9 },
{ "type": "KeyDown", "key_code": 27 },
{ "type": "KeyDown", "key_code": 9 },
{ "type": "KeyDown", "key_code": 9 },
{ "type": "KeyDown", "key_code": 9 },
{ "type": "KeyDown", "key_code": 9 },
{ "type": "KeyDown", "key_code": 27 },
{ "type": "KeyDown", "key_code": 9 },
{ "type": "KeyDown", "key_code": 9 },
{ "type": "KeyDown", "key_code": 9 },
{ "type": "KeyDown", "key_code": 9 }
]
32 changes: 32 additions & 0 deletions tests/tests/swfs/avm2/tab_ordering_stage_tab_children/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
stage.tabChildren = true
root.tabChildren = true
clip1.tabChildren = true
clip2.tabChildren = true
clip3.tabChildren = true
clip1 focusIn: [FocusEvent type="focusIn" bubbles=true cancelable=false eventPhase=2 relatedObject=null shiftKey=false keyCode=0]
clip2 focusIn: [FocusEvent type="focusIn" bubbles=true cancelable=false eventPhase=2 relatedObject=[object MovieClip] shiftKey=false keyCode=0]
clip1 focusIn: [FocusEvent type="focusIn" bubbles=true cancelable=false eventPhase=2 relatedObject=[object MovieClip] shiftKey=false keyCode=0]
clip2 focusIn: [FocusEvent type="focusIn" bubbles=true cancelable=false eventPhase=2 relatedObject=[object MovieClip] shiftKey=false keyCode=0]
Escape pressed
Setting tabChildren to false
stage.tabChildren = true
root.tabChildren = false
clip1.tabChildren = true
clip2.tabChildren = true
clip3.tabChildren = true
clip1 focusIn: [FocusEvent type="focusIn" bubbles=true cancelable=false eventPhase=2 relatedObject=null shiftKey=false keyCode=0]
Escape pressed
Setting tabChildren to true
stage.tabChildren = true
root.tabChildren = true
clip1.tabChildren = true
clip2.tabChildren = true
clip3.tabChildren = true
Adding a child at 0 and setting tabChildren to false
stage.tabChildren = true
root.tabChildren = false
clip1.tabChildren = true
clip2.tabChildren = true
clip3.tabChildren = true
clip1 focusIn: [FocusEvent type="focusIn" bubbles=true cancelable=false eventPhase=2 relatedObject=[object MovieClip] shiftKey=false keyCode=0]
clip1 focusIn: [FocusEvent type="focusIn" bubbles=true cancelable=false eventPhase=2 relatedObject=[object MovieClip] shiftKey=false keyCode=0]
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
num_ticks = 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package {

import flash.display.MovieClip;
import flash.display.Stage;

public class Test extends MovieClip {
private var originalStage: Stage;
private var originalRoot: MovieClip;

public function Test() {
super();

originalStage = this.stage;
originalRoot = MovieClip(this.root);

printProps();
trace("Removing the root movie and setting tabChildren to false");
originalStage.removeChild(originalRoot);
originalStage.tabChildren = false;
printProps();
}

private function printProps():void {
trace("stage.tabChildren = " + originalStage.tabChildren);
trace("root.tabChildren = " + originalRoot.tabChildren);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
stage.tabChildren = true
root.tabChildren = true
Removing the root movie and setting tabChildren to false
stage.tabChildren = true
root.tabChildren = false
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
num_ticks = 1

known_failure = true