From e840d1f71c34df16d9f97067c65b4f6b66e14c72 Mon Sep 17 00:00:00 2001 From: Adrian Wielgosik Date: Wed, 14 Sep 2022 22:29:25 +0200 Subject: [PATCH] core: Support AVM2 context menu (sans callbacks) --- core/src/avm1/globals/context_menu.rs | 97 ++++-------- core/src/avm2.rs | 1 + .../flash/ui/ContextMenuBuiltInItems.as | 6 - .../src/avm2/globals/flash/ui/context_menu.rs | 145 +++++++++++++++++- core/src/context_menu.rs | 97 ++++++++++++ core/src/player.rs | 69 +++++---- 6 files changed, 306 insertions(+), 109 deletions(-) diff --git a/core/src/avm1/globals/context_menu.rs b/core/src/avm1/globals/context_menu.rs index 12b79430c..a8b6229c4 100644 --- a/core/src/avm1/globals/context_menu.rs +++ b/core/src/avm1/globals/context_menu.rs @@ -5,7 +5,6 @@ use crate::avm1::property_decl::{define_properties_on, Declaration}; use crate::avm1::Object; use crate::avm1::{ScriptObject, Value}; use crate::context_menu; -use crate::display_object::TDisplayObject; use gc_arena::MutationContext; const PROTO_DECLS: &[Declaration] = declare_properties! { @@ -157,77 +156,37 @@ pub fn make_context_menu_state<'gc>( ) -> context_menu::ContextMenuState<'gc> { let mut result = context_menu::ContextMenuState::new(); - let root_mc = activation.context.stage.root_clip().as_movie_clip(); - let builtin_items = { - let is_multiframe_movie = root_mc.map(|mc| mc.total_frames() > 1).unwrap_or(false); - let mut names = if is_multiframe_movie { - vec![ - "zoom", - "quality", - "play", - "loop", - "rewind", - "forward_back", - "print", - ] - } else { - vec!["zoom", "quality", "print"] - }; - if let Some(menu) = menu { - if let Ok(Value::Object(builtins)) = menu.get("builtInItems", activation) { - names.retain(|name| { - !matches!(builtins.get(*name, activation), Ok(Value::Bool(false))) - }); + let mut builtin_items = context_menu::BuiltInItemFlags::for_stage(activation.context.stage); + if let Some(menu) = menu { + if let Ok(Value::Object(builtins)) = menu.get("builtInItems", activation) { + if matches!(builtins.get("zoom", activation), Ok(Value::Bool(false))) { + builtin_items.zoom = false; + } + if matches!(builtins.get("quality", activation), Ok(Value::Bool(false))) { + builtin_items.quality = false; + } + if matches!(builtins.get("play", activation), Ok(Value::Bool(false))) { + builtin_items.play = false; + } + if matches!(builtins.get("loop", activation), Ok(Value::Bool(false))) { + builtin_items.loop_ = false; + } + if matches!(builtins.get("rewind", activation), Ok(Value::Bool(false))) { + builtin_items.rewind = false; + } + if matches!( + builtins.get("forward_back", activation), + Ok(Value::Bool(false)) + ) { + builtin_items.forward_and_back = false; + } + if matches!(builtins.get("print", activation), Ok(Value::Bool(false))) { + builtin_items.print = false; } } - names - }; + } - if builtin_items.contains(&"play") { - let is_playing_root_movie = root_mc.unwrap().playing(); - result.push( - context_menu::ContextMenuItem { - enabled: true, - separator_before: true, - caption: "Play".to_string(), - checked: is_playing_root_movie, - }, - context_menu::ContextMenuCallback::Play, - ); - } - if builtin_items.contains(&"rewind") { - let is_first_frame = root_mc.unwrap().current_frame() <= 1; - result.push( - context_menu::ContextMenuItem { - enabled: !is_first_frame, - separator_before: true, - caption: "Rewind".to_string(), - checked: false, - }, - context_menu::ContextMenuCallback::Rewind, - ); - } - if builtin_items.contains(&"forward_back") { - let is_first_frame = root_mc.unwrap().current_frame() <= 1; - result.push( - context_menu::ContextMenuItem { - enabled: true, - separator_before: false, - caption: "Forward".to_string(), - checked: false, - }, - context_menu::ContextMenuCallback::Forward, - ); - result.push( - context_menu::ContextMenuItem { - enabled: !is_first_frame, - separator_before: false, - caption: "Back".to_string(), - checked: false, - }, - context_menu::ContextMenuCallback::Back, - ); - } + result.build_builtin_items(builtin_items, activation.context.stage); if let Some(menu) = menu { if let Ok(Value::Object(custom_items)) = menu.get("customItems", activation) { diff --git a/core/src/avm2.rs b/core/src/avm2.rs index a86682593..88791c2d9 100644 --- a/core/src/avm2.rs +++ b/core/src/avm2.rs @@ -53,6 +53,7 @@ pub use crate::avm2::array::ArrayStorage; pub use crate::avm2::call_stack::{CallNode, CallStack}; pub use crate::avm2::domain::Domain; pub use crate::avm2::error::Error; +pub use crate::avm2::globals::flash::ui::context_menu::make_context_menu_state; pub use crate::avm2::multiname::Multiname; pub use crate::avm2::namespace::Namespace; pub use crate::avm2::object::{ diff --git a/core/src/avm2/globals/flash/ui/ContextMenuBuiltInItems.as b/core/src/avm2/globals/flash/ui/ContextMenuBuiltInItems.as index 0906e5953..28005df7c 100644 --- a/core/src/avm2/globals/flash/ui/ContextMenuBuiltInItems.as +++ b/core/src/avm2/globals/flash/ui/ContextMenuBuiltInItems.as @@ -1,10 +1,6 @@ package flash.ui { public final class ContextMenuBuiltInItems { - - // FIXME - implement setters for all of these, - // and actually hide the corresponding menu item. - public var forwardAndBack:Boolean = true; public var loop:Boolean = true; public var play:Boolean = true; @@ -13,7 +9,5 @@ package flash.ui public var rewind:Boolean = true; public var save:Boolean = true; public var zoom:Boolean = true; - - public function ContextMenuBuiltInItems() {} } } diff --git a/core/src/avm2/globals/flash/ui/context_menu.rs b/core/src/avm2/globals/flash/ui/context_menu.rs index be7f4585c..d1154e693 100644 --- a/core/src/avm2/globals/flash/ui/context_menu.rs +++ b/core/src/avm2/globals/flash/ui/context_menu.rs @@ -1,19 +1,148 @@ use crate::avm2::activation::Activation; -use crate::avm2::object::Object; +use crate::avm2::multiname::Multiname; +use crate::avm2::object::{Object, TObject}; use crate::avm2::value::Value; use crate::avm2::Error; +use crate::context_menu; pub fn hide_built_in_items<'gc>( activation: &mut Activation<'_, 'gc, '_>, - _this: Option>, + this: Option>, _args: &[Value<'gc>], ) -> Result, Error<'gc>> { - // TODO: replace this by a proper implementation. - log::warn!("flash.ui.ContextMenu is a stub"); - activation - .context - .stage - .set_show_menu(&mut activation.context, false); + if let Some(this) = this { + if let Value::Object(mut items) = + this.get_property(&Multiname::public("builtInItems"), activation)? + { + // items is a ContextMenuBuiltInItems + items.set_property( + &Multiname::public("forwardAndBack"), + Value::Bool(false), + activation, + )?; + items.set_property(&Multiname::public("loop"), Value::Bool(false), activation)?; + items.set_property(&Multiname::public("play"), Value::Bool(false), activation)?; + items.set_property(&Multiname::public("print"), Value::Bool(false), activation)?; + items.set_property( + &Multiname::public("quality"), + Value::Bool(false), + activation, + )?; + items.set_property(&Multiname::public("rewind"), Value::Bool(false), activation)?; + items.set_property(&Multiname::public("save"), Value::Bool(false), activation)?; + items.set_property(&Multiname::public("zoom"), Value::Bool(false), activation)?; + } + } Ok(Value::Undefined) } + +pub fn make_context_menu_state<'gc>( + menu: Option>, + activation: &mut Activation<'_, 'gc, '_>, +) -> context_menu::ContextMenuState<'gc> { + let mut result = context_menu::ContextMenuState::new(); + + let mut builtin_items = context_menu::BuiltInItemFlags::for_stage(activation.context.stage); + if let Some(menu) = menu { + if let Ok(Value::Object(builtins)) = + menu.get_property(&Multiname::public("builtInItems"), activation) + { + if matches!( + builtins.get_property(&Multiname::public("zoom"), activation), + Ok(Value::Bool(false)) + ) { + builtin_items.zoom = false; + } + if matches!( + builtins.get_property(&Multiname::public("quality"), activation), + Ok(Value::Bool(false)) + ) { + builtin_items.quality = false; + } + if matches!( + builtins.get_property(&Multiname::public("play"), activation), + Ok(Value::Bool(false)) + ) { + builtin_items.play = false; + } + if matches!( + builtins.get_property(&Multiname::public("loop"), activation), + Ok(Value::Bool(false)) + ) { + builtin_items.loop_ = false; + } + if matches!( + builtins.get_property(&Multiname::public("rewind"), activation), + Ok(Value::Bool(false)) + ) { + builtin_items.rewind = false; + } + if matches!( + builtins.get_property(&Multiname::public("forwardAndBack"), activation), + Ok(Value::Bool(false)) + ) { + builtin_items.forward_and_back = false; + } + if matches!( + builtins.get_property(&Multiname::public("print"), activation), + Ok(Value::Bool(false)) + ) { + builtin_items.print = false; + } + } + } + + result.build_builtin_items(builtin_items, activation.context.stage); + + if let Some(menu) = menu { + if let Ok(Value::Object(custom_items)) = + menu.get_property(&Multiname::public("customItems"), activation) + { + // note: this borrows the array, but it shouldn't be possible for + // AS to get invoked here and cause BorrowMutError + if let Some(array) = custom_items.as_array_storage() { + for (i, item) in array.iter().enumerate() { + // this is a CustomMenuItem + if let Some(Value::Object(item)) = item { + let caption = if let Ok(Value::String(s)) = + item.get_property(&Multiname::public("caption"), activation) + { + s + } else { + // It's a CustomMenuItem, so this shouldn't happen + continue; + }; + let enabled = matches!( + item.get_property(&Multiname::public("enabled"), activation), + Ok(Value::Bool(true)) + ); + let visible = matches!( + item.get_property(&Multiname::public("visible"), activation), + Ok(Value::Bool(true)) + ); + let separator_before = matches!( + item.get_property(&Multiname::public("separatorBefore"), activation), + Ok(Value::Bool(true)) + ); + + if !visible { + continue; + } + + result.push( + context_menu::ContextMenuItem { + enabled, + separator_before: separator_before || i == 0, + caption: caption.to_string(), + checked: false, + }, + context_menu::ContextMenuCallback::Avm2 { item }, + ); + } + } + } + } + } + result +} diff --git a/core/src/context_menu.rs b/core/src/context_menu.rs index 64d3763b8..bd8bc8e4c 100644 --- a/core/src/context_menu.rs +++ b/core/src/context_menu.rs @@ -5,6 +5,9 @@ //! items work even if the movie changed `object.menu` in the meantime. use crate::avm1; +use crate::avm2; +use crate::display_object::Stage; +use crate::display_object::TDisplayObject; use gc_arena::Collect; use serde::Serialize; @@ -29,6 +32,54 @@ impl<'gc> ContextMenuState<'gc> { pub fn callback(&self, index: usize) -> &ContextMenuCallback<'gc> { &self.callbacks[index] } + pub fn build_builtin_items(&mut self, item_flags: BuiltInItemFlags, stage: Stage<'gc>) { + let root_mc = stage.root_clip().as_movie_clip(); + if item_flags.play { + let is_playing_root_movie = root_mc.unwrap().playing(); + self.push( + ContextMenuItem { + enabled: true, + separator_before: true, + caption: "Play".to_string(), + checked: is_playing_root_movie, + }, + ContextMenuCallback::Play, + ); + } + if item_flags.rewind { + let is_first_frame = root_mc.unwrap().current_frame() <= 1; + self.push( + ContextMenuItem { + enabled: !is_first_frame, + separator_before: true, + caption: "Rewind".to_string(), + checked: false, + }, + ContextMenuCallback::Rewind, + ); + } + if item_flags.forward_and_back { + let is_first_frame = root_mc.unwrap().current_frame() <= 1; + self.push( + ContextMenuItem { + enabled: true, + separator_before: false, + caption: "Forward".to_string(), + checked: false, + }, + ContextMenuCallback::Forward, + ); + self.push( + ContextMenuItem { + enabled: !is_first_frame, + separator_before: false, + caption: "Back".to_string(), + checked: false, + }, + ContextMenuCallback::Back, + ); + } + } } #[derive(Collect, Clone, Serialize)] @@ -56,4 +107,50 @@ pub enum ContextMenuCallback<'gc> { item: avm1::Object<'gc>, callback: avm1::Object<'gc>, }, + Avm2 { + item: avm2::Object<'gc>, + }, +} + +pub struct BuiltInItemFlags { + pub forward_and_back: bool, + pub loop_: bool, + pub play: bool, + pub print: bool, + pub quality: bool, + pub rewind: bool, + pub save: bool, + pub zoom: bool, +} + +impl BuiltInItemFlags { + pub fn for_stage(stage: Stage<'_>) -> Self { + let root_mc = stage.root_clip().as_movie_clip(); + let is_multiframe_movie = root_mc.map(|mc| mc.total_frames() > 1).unwrap_or(false); + if is_multiframe_movie { + Self { + forward_and_back: true, + loop_: true, + play: true, + print: true, + quality: true, + rewind: true, + zoom: true, + + save: false, + } + } else { + Self { + print: true, + quality: true, + zoom: true, + + forward_and_back: false, + rewind: false, + loop_: false, + play: false, + save: false, + } + } + } } diff --git a/core/src/player.rs b/core/src/player.rs index 5632aa91a..4588b4e33 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -18,7 +18,9 @@ use crate::backend::{ }; use crate::config::Letterbox; use crate::context::{ActionQueue, ActionType, RenderContext, UpdateContext}; -use crate::context_menu::{ContextMenuCallback, ContextMenuItem, ContextMenuState}; +use crate::context_menu::{ + BuiltInItemFlags, ContextMenuCallback, ContextMenuItem, ContextMenuState, +}; use crate::display_object::{ EditText, InteractiveObject, MovieClip, Stage, StageAlign, StageDisplayState, StageQuality, StageScaleMode, TInteractiveObject, WindowMode, @@ -556,38 +558,50 @@ impl Player { return vec![]; } - let mut activation = Activation::from_stub( - context.reborrow(), - ActivationIdentifier::root("[ContextMenu]"), - ); - // TODO: This should use a pointed display object with `.menu` - let menu_object = { - let dobj = activation.context.stage.root_clip(); - if let Value::Object(obj) = dobj.object() { - if let Ok(Value::Object(menu)) = obj.get("menu", &mut activation) { - Some(menu) - } else { - None + let root_dobj = context.stage.root_clip(); + + let menu = if let Value::Object(obj) = root_dobj.object() { + let mut activation = Activation::from_stub( + context.reborrow(), + ActivationIdentifier::root("[ContextMenu]"), + ); + let menu_object = if let Ok(Value::Object(menu)) = obj.get("menu", &mut activation) + { + if let Ok(Value::Object(on_select)) = menu.get("onSelect", &mut activation) { + Self::run_context_menu_custom_callback( + menu, + on_select, + &mut activation.context, + ); } + Some(menu) } else { None - } + }; + crate::avm1::make_context_menu_state(menu_object, &mut activation) + } else if let Avm2Value::Object(_obj) = root_dobj.object2() { + // TODO: send "menuSelect" event + log::warn!("AVM2 Context menu callbacks are not implemented"); + + let mut activation = Avm2Activation::from_nothing(context.reborrow()); + + let menu_object = root_dobj + .as_interactive() + .map(|iobj| iobj.context_menu()) + .and_then(|v| v.as_object()); + + crate::avm2::make_context_menu_state(menu_object, &mut activation) + } else { + // no AVM1 or AVM2 object - so just prepare the builtin items + let mut menu = ContextMenuState::new(); + let builtin_items = BuiltInItemFlags::for_stage(context.stage); + menu.build_builtin_items(builtin_items, context.stage); + menu }; - if let Some(menu) = menu_object { - if let Ok(Value::Object(on_select)) = menu.get("onSelect", &mut activation) { - Self::run_context_menu_custom_callback( - menu, - on_select, - &mut activation.context, - ); - } - } - - let menu = crate::avm1::make_context_menu_state(menu_object, &mut activation); let ret = menu.info().clone(); - *activation.context.current_context_menu = Some(menu); + *context.current_context_menu = Some(menu); ret }) } @@ -611,6 +625,9 @@ impl Player { ContextMenuCallback::Forward => Self::forward_root_movie(context), ContextMenuCallback::Back => Self::back_root_movie(context), ContextMenuCallback::Rewind => Self::rewind_root_movie(context), + ContextMenuCallback::Avm2 { .. } => { + // TODO: Send menuItemSelect event + } _ => {} } Self::run_actions(context);