diff --git a/core/src/display_object.rs b/core/src/display_object.rs index 09c0000af176..9c13fc6f55b9 100644 --- a/core/src/display_object.rs +++ b/core/src/display_object.rs @@ -1587,10 +1587,25 @@ pub trait TDisplayObject<'gc>: /// Set the parent of this display object. fn set_parent(&self, context: &mut UpdateContext<'_, 'gc>, parent: Option>) { + let had_parent = self.parent().is_some(); self.base_mut(context.gc_context) .set_parent_ignoring_orphan_list(parent); + let has_parent = self.parent().is_some(); + let parent_removed = had_parent && !has_parent; + + if parent_removed { + if let Some(int) = self.as_interactive() { + int.drop_focus(context); + } + + self.on_parent_removed(context); + } } + /// This method is called when the parent is removed. + /// It may be overwritten to inject some implementation-specific behavior. + fn on_parent_removed(&self, _context: &mut UpdateContext<'_, 'gc>) {} + /// Retrieve the parent of this display object. /// /// This version of the function implements the concept of parenthood as diff --git a/core/src/display_object/avm1_button.rs b/core/src/display_object/avm1_button.rs index 858e41ff5748..831fb4aaeee7 100644 --- a/core/src/display_object/avm1_button.rs +++ b/core/src/display_object/avm1_button.rs @@ -45,7 +45,6 @@ pub struct Avm1ButtonData<'gc> { tracking: Cell, object: Lock>>, initialized: Cell, - has_focus: Cell, } #[derive(Clone, Collect)] @@ -98,7 +97,6 @@ impl<'gc> Avm1Button<'gc> { } else { ButtonTracking::Push }), - has_focus: Cell::new(false), }, )) } @@ -393,11 +391,7 @@ impl<'gc> TDisplayObject<'gc> for Avm1Button<'gc> { } fn avm1_unload(&self, context: &mut UpdateContext<'_, 'gc>) { - let had_focus = self.0.has_focus.get(); - if had_focus { - let tracker = context.focus_tracker; - tracker.set(None, context); - } + self.drop_focus(context); if let Some(node) = self.maskee() { node.set_masker(context.gc(), None, true); } else if let Some(node) = self.masker() { @@ -530,7 +524,7 @@ impl<'gc> TInteractiveObject<'gc> for Avm1Button<'gc> { if let Some(name) = event.method_name() { // Keyboard events don't fire their methods // unless the Button has focus (like for MovieClips). - if !event.is_key_event() || self.0.has_focus.get() { + if !event.is_key_event() || self.has_focus() { context.action_queue.queue_action( self_display_object, ActionType::Method { @@ -608,15 +602,6 @@ impl<'gc> TInteractiveObject<'gc> for Avm1Button<'gc> { } } - fn on_focus_changed( - &self, - _context: &mut UpdateContext<'_, 'gc>, - focused: bool, - _other: Option>, - ) { - self.0.has_focus.set(focused); - } - fn tab_enabled_avm1(&self, context: &mut UpdateContext<'_, 'gc>) -> bool { self.get_avm1_boolean_property(context, "tabEnabled", |_| true) } diff --git a/core/src/display_object/avm2_button.rs b/core/src/display_object/avm2_button.rs index 5b5def4ef734..f4feedab7d1b 100644 --- a/core/src/display_object/avm2_button.rs +++ b/core/src/display_object/avm2_button.rs @@ -74,7 +74,6 @@ pub struct Avm2ButtonData<'gc> { /// The AVM2 representation of this button. object: Lock>>, - has_focus: Cell, enabled: Cell, use_hand_cursor: Cell, @@ -135,7 +134,6 @@ impl<'gc> Avm2Button<'gc> { } else { ButtonTracking::Push }), - has_focus: Cell::new(false), enabled: Cell::new(true), use_hand_cursor: Cell::new(true), skip_current_frame: Cell::new(false), @@ -823,15 +821,6 @@ impl<'gc> TInteractiveObject<'gc> for Avm2Button<'gc> { } } - fn on_focus_changed( - &self, - _context: &mut UpdateContext<'_, 'gc>, - focused: bool, - _other: Option>, - ) { - self.0.has_focus.set(focused); - } - fn tab_enabled_avm2_default(&self, _context: &mut UpdateContext<'_, 'gc>) -> bool { true } diff --git a/core/src/display_object/edit_text.rs b/core/src/display_object/edit_text.rs index 357b5b74b423..2d24fb014e9c 100644 --- a/core/src/display_object/edit_text.rs +++ b/core/src/display_object/edit_text.rs @@ -987,7 +987,7 @@ impl<'gc> EditText<'gc> { ..Default::default() }); - let visible_selection = if edit_text.flags.contains(EditTextFlag::HAS_FOCUS) { + let visible_selection = if self.has_focus() { edit_text.selection } else { None @@ -2114,21 +2114,6 @@ impl<'gc> TDisplayObject<'gc> for EditText<'gc> { self.0.write(context.gc_context).object = Some(to.into()); } - fn set_parent(&self, context: &mut UpdateContext<'_, 'gc>, parent: Option>) { - let had_parent = self.parent().is_some(); - self.base_mut(context.gc_context) - .set_parent_ignoring_orphan_list(parent); - let has_parent = self.parent().is_some(); - - if self.movie().is_action_script_3() && had_parent && !has_parent { - let had_focus = self.0.read().flags.contains(EditTextFlag::HAS_FOCUS); - if had_focus { - let tracker = context.focus_tracker; - tracker.set(None, context); - } - } - } - fn self_bounds(&self) -> Rectangle { self.0.read().bounds.clone() } @@ -2237,7 +2222,7 @@ impl<'gc> TDisplayObject<'gc> for EditText<'gc> { }); if edit_text.layout.is_empty() && !edit_text.flags.contains(EditTextFlag::READ_ONLY) { - let visible_selection = if edit_text.flags.contains(EditTextFlag::HAS_FOCUS) { + let visible_selection = if self.has_focus() { edit_text.selection } else { None @@ -2275,11 +2260,7 @@ impl<'gc> TDisplayObject<'gc> for EditText<'gc> { } fn avm1_unload(&self, context: &mut UpdateContext<'_, 'gc>) { - let had_focus = self.0.read().flags.contains(EditTextFlag::HAS_FOCUS); - if had_focus { - let tracker = context.focus_tracker; - tracker.set(None, context); - } + self.drop_focus(context); if let Some(node) = self.maskee() { node.set_masker(context.gc_context, None, true); @@ -2459,11 +2440,9 @@ impl<'gc> TInteractiveObject<'gc> for EditText<'gc> { focused: bool, _other: Option>, ) { - let is_action_script_3 = self.movie().is_action_script_3(); - let mut text = self.0.write(context.gc_context); - text.flags.set(EditTextFlag::HAS_FOCUS, focused); - if !focused && !is_action_script_3 { - text.selection = None; + let is_avm1 = !self.movie().is_action_script_3(); + if !focused && is_avm1 { + self.0.write(context.gc_context).selection = None; } } @@ -2494,7 +2473,6 @@ bitflags::bitflags! { struct EditTextFlag: u16 { const FIRING_VARIABLE_BINDING = 1 << 0; const HAS_BACKGROUND = 1 << 1; - const HAS_FOCUS = 1 << 2; // The following bits need to match `swf::EditTextFlag`. const READ_ONLY = 1 << 3; diff --git a/core/src/display_object/interactive.rs b/core/src/display_object/interactive.rs index e1b1708917eb..de2f38f7c6c9 100644 --- a/core/src/display_object/interactive.rs +++ b/core/src/display_object/interactive.rs @@ -76,6 +76,9 @@ bitflags! { /// Whether this `InteractiveObject` accepts double-clicks. const DOUBLE_CLICK_ENABLED = 1 << 1; + + /// Whether this `InteractiveObject` is currently focused. + const HAS_FOCUS = 1 << 2; } } @@ -167,6 +170,18 @@ pub trait TInteractiveObject<'gc>: .set(InteractiveObjectFlags::DOUBLE_CLICK_ENABLED, value) } + fn has_focus(self) -> bool { + self.raw_interactive() + .flags + .contains(InteractiveObjectFlags::HAS_FOCUS) + } + + fn set_has_focus(self, mc: &Mutation<'gc>, value: bool) { + self.raw_interactive_mut(mc) + .flags + .set(InteractiveObjectFlags::HAS_FOCUS, value) + } + fn context_menu(self) -> Avm2Value<'gc> { self.raw_interactive().context_menu } @@ -536,6 +551,14 @@ pub trait TInteractiveObject<'gc>: ) { } + /// If this object has focus, this method drops it. + fn drop_focus(&self, context: &mut UpdateContext<'_, 'gc>) { + if self.has_focus() { + let tracker = context.focus_tracker; + tracker.set(None, context); + } + } + fn call_focus_handler( &self, context: &mut UpdateContext<'_, 'gc>, diff --git a/core/src/display_object/loader_display.rs b/core/src/display_object/loader_display.rs index 3874f18e8a1c..a2e1beda24a6 100644 --- a/core/src/display_object/loader_display.rs +++ b/core/src/display_object/loader_display.rs @@ -136,13 +136,8 @@ impl<'gc> TDisplayObject<'gc> for LoaderDisplay<'gc> { self.0.read().movie.clone() } - fn set_parent(&self, context: &mut UpdateContext<'_, 'gc>, parent: Option>) { - let had_parent = self.parent().is_some(); - self.base_mut(context.gc_context) - .set_parent_ignoring_orphan_list(parent); - let has_parent = self.parent().is_some(); - - if self.movie().is_action_script_3() && had_parent && !has_parent { + fn on_parent_removed(&self, context: &mut UpdateContext<'_, 'gc>) { + if self.movie().is_action_script_3() { context.avm2.add_orphan_obj((*self).into()) } } diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index f1a09815ba48..9b79dbfc2124 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -162,7 +162,6 @@ pub struct MovieClipData<'gc> { avm2_class: Option>, #[collect(require_static)] drawing: Drawing, - has_focus: bool, avm2_enabled: bool, /// Show a hand cursor when the clip is in button mode. @@ -212,7 +211,6 @@ impl<'gc> MovieClip<'gc> { flags: MovieClipFlags::empty(), avm2_class: None, drawing: Drawing::new(), - has_focus: false, avm2_enabled: true, avm2_use_hand_cursor: true, button_mode: false, @@ -255,7 +253,6 @@ impl<'gc> MovieClip<'gc> { flags: MovieClipFlags::empty(), avm2_class: Some(class), drawing: Drawing::new(), - has_focus: false, avm2_enabled: true, avm2_use_hand_cursor: true, button_mode: false, @@ -299,7 +296,6 @@ impl<'gc> MovieClip<'gc> { flags: MovieClipFlags::PLAYING, avm2_class: None, drawing: Drawing::new(), - has_focus: false, avm2_enabled: true, avm2_use_hand_cursor: true, button_mode: false, @@ -368,7 +364,6 @@ impl<'gc> MovieClip<'gc> { flags: MovieClipFlags::PLAYING, avm2_class: None, drawing: Drawing::new(), - has_focus: false, avm2_enabled: true, avm2_use_hand_cursor: true, button_mode: false, @@ -2933,13 +2928,8 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { } } - fn set_parent(&self, context: &mut UpdateContext<'_, 'gc>, parent: Option>) { - let had_parent = self.parent().is_some(); - self.base_mut(context.gc_context) - .set_parent_ignoring_orphan_list(parent); - let has_parent = self.parent().is_some(); - - if self.movie().is_action_script_3() && had_parent && !has_parent { + fn on_parent_removed(&self, context: &mut UpdateContext<'_, 'gc>) { + if self.movie().is_action_script_3() { context.avm2.add_orphan_obj((*self).into()) } } @@ -2962,11 +2952,7 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { } } - let had_focus = self.0.read().has_focus; - if had_focus { - let tracker = context.focus_tracker; - tracker.set(None, context); - } + self.drop_focus(context); { let mut mc = self.0.write(context.gc_context); @@ -3101,7 +3087,7 @@ impl<'gc> TInteractiveObject<'gc> for MovieClip<'gc> { if swf_version >= 6 { if let Some(name) = event.method_name() { // Keyboard events don't fire their methods unless the MovieClip has focus (#2120). - if !event.is_key_event() || read.has_focus { + if !event.is_key_event() || self.has_focus() { context.action_queue.queue_action( self.into(), ActionType::Method { @@ -3386,15 +3372,6 @@ impl<'gc> TInteractiveObject<'gc> for MovieClip<'gc> { } } - fn on_focus_changed( - &self, - context: &mut UpdateContext<'_, 'gc>, - focused: bool, - _other: Option>, - ) { - self.0.write(context.gc_context).has_focus = focused; - } - fn tab_enabled_avm1(&self, context: &mut UpdateContext<'_, 'gc>) -> bool { self.get_avm1_boolean_property(context, "tabEnabled", |context| { self.tab_index().is_some() || self.is_button_mode(context) diff --git a/core/src/focus_tracker.rs b/core/src/focus_tracker.rs index f492d941b746..71cf9dc6255f 100644 --- a/core/src/focus_tracker.rs +++ b/core/src/focus_tracker.rs @@ -98,10 +98,12 @@ impl<'gc> FocusTracker<'gc> { self.update_highlight(context); if let Some(old) = old { + old.set_has_focus(context.gc(), false); old.on_focus_changed(context, false, new); old.call_focus_handler(context, false, new); } if let Some(new) = new { + new.set_has_focus(context.gc(), true); new.on_focus_changed(context, true, old); new.call_focus_handler(context, true, old); } diff --git a/tests/tests/swfs/avm1/focus_remove/output.txt b/tests/tests/swfs/avm1/focus_remove/output.txt new file mode 100644 index 000000000000..9dd377cadd30 --- /dev/null +++ b/tests/tests/swfs/avm1/focus_remove/output.txt @@ -0,0 +1,24 @@ +===== clip +Object: _level0.clip +Focus changed + old: null + new: _level0.clip +Focus: _level0.clip +Focus: null +Focus: null +===== text +Object: _level0.clip.text +Focus changed + old: null + new: _level0.clip.text +Focus: _level0.clip.text +Focus: null +Focus: null +===== button +Object: _level0.clip.button +Focus changed + old: null + new: _level0.clip.button +Focus: _level0.clip.button +Focus: null +Focus: null diff --git a/tests/tests/swfs/avm1/focus_remove/test.as b/tests/tests/swfs/avm1/focus_remove/test.as new file mode 100644 index 000000000000..31a2e1ae5865 --- /dev/null +++ b/tests/tests/swfs/avm1/focus_remove/test.as @@ -0,0 +1,48 @@ +var listener = new Object(); +listener.onSetFocus = function(oldFocus, newFocus) { + if (newFocus) { + trace("Focus changed"); + trace(" old: " + oldFocus); + trace(" new: " + newFocus); + } +}; +Selection.addListener(listener); + +function testObject(create, remove) { + var obj = create(); + trace("Object: " + obj); + obj.focusEnabled = true; + Selection.setFocus(obj); + trace("Focus: " + Selection.getFocus()); + remove(); + trace("Focus: " + Selection.getFocus()); + + create(); + trace("Focus: " + Selection.getFocus()); + remove(); +} + +trace("===== clip"); +testObject(function() { + return _root.createEmptyMovieClip("clip", 2); +}, function() { + _root.clip.removeMovieClip(); +}); + +trace("===== text"); +testObject(function() { + var mc = _root.createEmptyMovieClip("clip", 3); + var tf = mc.createTextField("text", 1, 0, 0, 150, 20); + tf.type = "input"; + return tf; +}, function() { + _root.clip.removeMovieClip(); +}); + +trace("===== button"); +testObject(function() { + var mc = _root.attachMovie("CustomButton", "clip", 3); + return mc.button; +}, function() { + _root.clip.removeMovieClip(); +}); diff --git a/tests/tests/swfs/avm1/focus_remove/test.swf b/tests/tests/swfs/avm1/focus_remove/test.swf new file mode 100644 index 000000000000..74e11ef9d52e Binary files /dev/null and b/tests/tests/swfs/avm1/focus_remove/test.swf differ diff --git a/tests/tests/swfs/avm1/focus_remove/test.toml b/tests/tests/swfs/avm1/focus_remove/test.toml new file mode 100644 index 000000000000..cf6123969a1d --- /dev/null +++ b/tests/tests/swfs/avm1/focus_remove/test.toml @@ -0,0 +1 @@ +num_ticks = 1 diff --git a/tests/tests/swfs/avm2/focus_remove/Test.as b/tests/tests/swfs/avm2/focus_remove/Test.as new file mode 100644 index 000000000000..e8046cd56173 --- /dev/null +++ b/tests/tests/swfs/avm2/focus_remove/Test.as @@ -0,0 +1,44 @@ +package { + +import flash.display.DisplayObject; +import flash.display.InteractiveObject; +import flash.display.InteractiveObject; +import flash.display.InteractiveObject; +import flash.display.SimpleButton; +import flash.display.Sprite; +import flash.events.KeyboardEvent; +import flash.text.TextField; +import flash.text.TextField; +import flash.display.Sprite; +import flash.display.SimpleButton; +import flash.display.MovieClip; +import flash.events.Event; +import flash.events.FocusEvent; + +[SWF(width="50", height="50", backgroundColor="#000000")] +public class Test extends MovieClip { + public function Test() { + super(); + + trace("===== sprite"); + testObject(new Sprite()); + trace("===== clip"); + testObject(new MovieClip()); + trace("===== text"); + testObject(new TextField()); + trace("===== button"); + testObject(new SimpleButton()); + } + + function testObject(obj:InteractiveObject) { + stage.focus = obj; + trace("Focus: " + stage.focus); + stage.addChild(obj); + trace("Focus: " + stage.focus); + stage.removeChild(obj); + trace("Focus: " + stage.focus); + stage.focus = obj; + trace("Focus: " + stage.focus); + } +} +} diff --git a/tests/tests/swfs/avm2/focus_remove/output.txt b/tests/tests/swfs/avm2/focus_remove/output.txt new file mode 100644 index 000000000000..5bc24d110215 --- /dev/null +++ b/tests/tests/swfs/avm2/focus_remove/output.txt @@ -0,0 +1,20 @@ +===== sprite +Focus: [object Sprite] +Focus: [object Sprite] +Focus: null +Focus: [object Sprite] +===== clip +Focus: [object MovieClip] +Focus: [object MovieClip] +Focus: null +Focus: [object MovieClip] +===== text +Focus: [object TextField] +Focus: [object TextField] +Focus: null +Focus: [object TextField] +===== button +Focus: [object SimpleButton] +Focus: [object SimpleButton] +Focus: null +Focus: [object SimpleButton] diff --git a/tests/tests/swfs/avm2/focus_remove/test.swf b/tests/tests/swfs/avm2/focus_remove/test.swf new file mode 100644 index 000000000000..ab9a7c7c9f19 Binary files /dev/null and b/tests/tests/swfs/avm2/focus_remove/test.swf differ diff --git a/tests/tests/swfs/avm2/focus_remove/test.toml b/tests/tests/swfs/avm2/focus_remove/test.toml new file mode 100644 index 000000000000..cf6123969a1d --- /dev/null +++ b/tests/tests/swfs/avm2/focus_remove/test.toml @@ -0,0 +1 @@ +num_ticks = 1