// Button.cpp: Mouse-sensitive buttons, for Gnash. // // Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012 // Free Software Foundation, Inc // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA // #ifdef HAVE_CONFIG_H #include "gnashconfig.h" // USE_SWF_TREE #endif #include "Button.h" #include #include #include #include "DefineButtonTag.h" #include "as_value.h" #include "Button.h" #include "ActionExec.h" #include "MovieClip.h" #include "movie_root.h" #include "VM.h" #include "NativeFunction.h" #include "fn_call.h" #include "ExecutableCode.h" #include "namedStrings.h" #include "StringPredicates.h" #include "GnashKey.h" #include "SoundInfoRecord.h" #include "Global_as.h" #include "RunResources.h" #include "sound_definition.h" #include "Transform.h" #include "sound_handler.h" /** \page buttons Buttons and mouse behaviour Observations about button & mouse behavior Entities that receive mouse events: only buttons and sprites, AFAIK When the mouse button goes down, it becomes "captured" by whatever element is topmost, directly below the mouse at that moment. While the mouse is captured, no other entity receives mouse events, regardless of how the mouse or other elements move. The mouse remains captured until the mouse button goes up. The mouse remains captured even if the element that captured it is removed from the display list. If the mouse isn't above a button or sprite when the mouse button goes down, then the mouse is captured by the background (i.e. mouse events just don't get sent, until the mouse button goes up again). Mouse events: +------------------+---------------+-------------------------------------+ | Event | Mouse Button | description | ========================================================================= | onRollOver | up | sent to topmost entity when mouse | | | | cursor initially goes over it | +------------------+---------------+-------------------------------------+ | onRollOut | up | when mouse leaves entity, after | | | | onRollOver | +------------------+---------------+-------------------------------------+ | onPress | up -> down | sent to topmost entity when mouse | | | | button goes down. onRollOver | | | | always precedes onPress. Initiates | | | | mouse capture. | +------------------+---------------+-------------------------------------+ | onRelease | down -> up | sent to active entity if mouse goes | | | | up while over the element | +------------------+---------------+-------------------------------------+ | onDragOut | down | sent to active entity if mouse | | | | is no longer over the entity | +------------------+---------------+-------------------------------------+ | onReleaseOutside | down -> up | sent to active entity if mouse goes | | | | up while not over the entity. | | | | onDragOut always precedes | | | | onReleaseOutside | +------------------+---------------+-------------------------------------+ | onDragOver | down | sent to active entity if mouse is | | | | dragged back over it after | | | | onDragOut | +------------------+---------------+-------------------------------------+ There is always one active entity at any given time (considering NULL to be an active entity, representing the background, and other objects that don't receive mouse events). When the mouse button is up, the active entity is the topmost element directly under the mouse pointer. When the mouse button is down, the active entity remains whatever it was when the button last went down. The active entity is the only object that receives mouse events. !!! The "trackAsMenu" property alters this behavior! If trackAsMenu is set on the active entity, then onReleaseOutside is filtered out, and onDragOver from another entity is allowed (from the background, or another trackAsMenu entity). !!! Pseudocode: active_entity = NULL mouse_button_state = UP mouse_inside_entity_state = false frame loop: if mouse_button_state == DOWN // Handle trackAsMenu if (active_entity->trackAsMenu) possible_entity = topmost entity below mouse if (possible_entity != active_entity && possible_entity->trackAsMenu) // Transfer to possible entity active_entity = possible_entity active_entity->onDragOver() mouse_inside_entity_state = true; // Handle onDragOut, onDragOver if (mouse_inside_entity_state == false) if (mouse is actually inside the active_entity) // onDragOver active_entity->onDragOver() mouse_inside_entity_state = true; else // mouse_inside_entity_state == true if (mouse is actually outside the active_entity) // onDragOut active_entity->onDragOut() mouse_inside_entity_state = false; // Handle onRelease, onReleaseOutside if (mouse button is up) if (mouse_inside_entity_state) // onRelease active_entity->onRelease() else // onReleaseOutside if (active_entity->trackAsMenu == false) active_entity->onReleaseOutside() mouse_button_state = UP if mouse_button_state == UP new_active_entity = topmost entity below the mouse if (new_active_entity != active_entity) // onRollOut, onRollOver active_entity->onRollOut() active_entity = new_active_entity active_entity->onRollOver() // Handle press if (mouse button is down) // onPress active_entity->onPress() mouse_inside_entity_state = true mouse_button_state = DOWN */ namespace gnash { namespace { as_value button_blendMode(const fn_call& fn); as_value button_cacheAsBitmap(const fn_call& fn); as_value button_filters(const fn_call& fn); as_value button_scale9Grid(const fn_call& fn); as_value button_setTabIndex(const fn_call& fn); as_value button_getTabIndex(const fn_call& fn); as_value button_getDepth(const fn_call& fn); } namespace { class ButtonActionExecutor { public: ButtonActionExecutor(as_environment& env) : _env(env) {} void operator() (const action_buffer& ab) { ActionExec exec(ab, _env); exec(); } private: as_environment& _env; }; class ButtonActionPusher { public: ButtonActionPusher(movie_root& mr, DisplayObject* this_ptr) : _mr(mr), _tp(this_ptr) {} void operator()(const action_buffer& ab) { _mr.pushAction(ab, _tp); } private: movie_root& _mr; DisplayObject* _tp; }; } namespace { void addInstanceProperty(Button& b, DisplayObject* d) { if (!d) return; const ObjectURI& name = d->get_name(); if (name.empty()) return; getObject(&b)->init_member(name, getObject(d), 0); } void removeInstanceProperty(Button& b, DisplayObject* d) { if (!d) return; const ObjectURI& name = d->get_name(); if (name.empty()) return; getObject(&b)->delProperty(name); } } /// Predicates for standard algorithms. /// Depth comparator for DisplayObjects. static bool charDepthLessThen(const DisplayObject* ch1, const DisplayObject* ch2) { return ch1->get_depth() < ch2->get_depth(); } /// Predicate for finding active DisplayObjects. // /// Returns true if the DisplayObject should be skipped: /// 1) if it is NULL, or /// 2) if we don't want unloaded DisplayObjects and the DisplayObject is unloaded. static bool isCharacterNull(DisplayObject* ch, bool includeUnloaded) { return (!ch || (!includeUnloaded && ch->unloaded())); } static void attachButtonInterface(as_object& o) { const int unprotected = 0; o.init_member(NSV::PROP_ENABLED, true, unprotected); o.init_member("useHandCursor", true, unprotected); const int swf8Flags = PropFlags::onlySWF8Up; VM& vm = getVM(o); o.init_property("tabIndex", *vm.getNative(105, 1), *vm.getNative(105, 2), swf8Flags); o.init_member("getDepth", vm.getNative(105, 3), unprotected); NativeFunction* gs; gs = vm.getNative(105, 4); o.init_property("scale9Grid", *gs, *gs, swf8Flags); gs = vm.getNative(105, 5); o.init_property("filters", *gs, *gs, swf8Flags); gs = vm.getNative(105, 6); o.init_property("cacheAsBitmap", *gs, *gs, swf8Flags); gs = vm.getNative(105, 7); o.init_property("blendMode", *gs, *gs, swf8Flags); } Button::Button(as_object* object, const SWF::DefineButtonTag* def, DisplayObject* parent) : InteractiveObject(object, parent), _mouseState(MOUSESTATE_UP), _def(def) { assert(object); } Button::~Button() { } bool Button::trackAsMenu() { // TODO: check whether the AS or the tag value takes precedence. as_object* obj = getObject(this); assert(obj); VM& vm = getVM(*obj); as_value track; // TODO: use NSV const ObjectURI& propTrackAsMenu = getURI(vm, "trackAsMenu"); if (obj->get_member(propTrackAsMenu, &track)) { return toBool(track, vm); } if (_def) return _def->trackAsMenu(); return false; } bool Button::isEnabled() { as_object* obj = getObject(this); assert(obj); as_value enabled; if (!obj->get_member(NSV::PROP_ENABLED, &enabled)) return false; return toBool(enabled, getVM(*obj)); } void Button::keyPress(key::code c) { if (unloaded()) { // We don't respond to events while unloaded // See bug #22982 return; } ButtonActionPusher xec(stage(), this); _def->forEachTrigger(event_id(event_id::KEY_PRESS, c), xec); } bool Button::handleFocus() { /// Nothing to do, but can receive focus. return false; } void Button::display(Renderer& renderer, const Transform& base) { const DisplayObject::MaskRenderer mr(renderer, *this); const Transform xform = base * transform(); DisplayObjects actChars; getActiveCharacters(actChars); // TODO: by keeping chars sorted by depth we'd avoid the sort on display std::sort(actChars.begin(), actChars.end(), charDepthLessThen); for (auto& actChar : actChars) { actChar->display(renderer, xform); } clear_invalidated(); } // Return the topmost entity that the given point covers. NULL if none. // I.e. check against ourself. InteractiveObject* Button::topmostMouseEntity(std::int32_t x, std::int32_t y) { if (!visible() || !isEnabled()) { return nullptr; } //------------------------------------------------- // Check our active and visible children first //------------------------------------------------- DisplayObjects actChars; getActiveCharacters(actChars); if ( ! actChars.empty() ) { std::sort(actChars.begin(), actChars.end(), charDepthLessThen); SWFMatrix m = getMatrix(*this); point p(x, y); m.invert().transform(p); for (DisplayObjects::reverse_iterator it = actChars.rbegin(), itE=actChars.rend(); it!=itE; ++it) { DisplayObject* ch = *it; if ( ! ch->visible() ) continue; InteractiveObject *hit = ch->topmostMouseEntity(p.x, p.y); if ( hit ) return hit; } } //------------------------------------------------- // If that failed, check our hit area //------------------------------------------------- // Find hit DisplayObjects if ( _hitCharacters.empty() ) return nullptr; // point is in p's space, // we need to convert it in world space point wp(x,y); DisplayObject* p = parent(); if (p) { getWorldMatrix(*p).transform(wp); } for (DisplayObjects::const_iterator i = _hitCharacters.begin(), e = _hitCharacters.end(); i !=e; ++i) { if ((*i)->pointInVisibleShape(wp.x, wp.y)) { // The mouse is inside the shape. return this; } } return nullptr; } void Button::mouseEvent(const event_id& event) { if (unloaded()) { // We don't respond to events while unloaded. See bug #22982. return; } MouseState new_state = _mouseState; // Set our mouse state (so we know how to render). switch (event.id()) { case event_id::ROLL_OUT: case event_id::RELEASE_OUTSIDE: new_state = MOUSESTATE_UP; break; case event_id::RELEASE: case event_id::ROLL_OVER: case event_id::DRAG_OUT: case event_id::MOUSE_UP: new_state = MOUSESTATE_OVER; break; case event_id::PRESS: case event_id::DRAG_OVER: case event_id::MOUSE_DOWN: new_state = MOUSESTATE_DOWN; break; default: //abort(); // missed a case? log_error(_("Unhandled button event %s"), event); break; } set_current_state(new_state); // Button transition sounds. do { if (!_def->hasSound()) break; // Check if there is a sound handler sound::sound_handler* s = getRunResources(*getObject(this)).soundHandler(); if (!s) break; int bi; // button sound array index [0..3] switch (event.id()) { case event_id::ROLL_OUT: bi = 0; break; case event_id::ROLL_OVER: bi = 1; break; case event_id::PRESS: bi = 2; break; case event_id::RELEASE: bi = 3; break; default: bi = -1; break; } // no sound for this transition if (bi < 0) break; #ifdef USE_SOUND const SWF::DefineButtonSoundTag::ButtonSound& bs = _def->buttonSound(bi); // character zero is considered as null character if (!bs.soundID) break; // No actual sound ? if (!bs.sample) break; if (bs.soundInfo.stopPlayback) { s->stopEventSound(bs.sample->m_sound_handler_id); } else { const SWF::SoundInfoRecord& sinfo = bs.soundInfo; const sound::SoundEnvelopes* env = sinfo.envelopes.empty() ? nullptr : &sinfo.envelopes; s->startSound(bs.sample->m_sound_handler_id, bs.soundInfo.loopCount, env, // envelopes !sinfo.noMultiple, // allow multiple instances ? sinfo.inPoint, sinfo.outPoint ); } #endif // USE_SOUND } while(0); // From: "ActionScript - The Definitive Guide" by Colin Moock // (chapter 10: Events and Event Handlers) // // "Event-based code [..] is said to be executed asynchronously // because the triggering of events can occur at arbitrary times." // // We'll push to the global list. The movie_root will process // the action queue on mouse event. // movie_root& mr = stage(); ButtonActionPusher xec(mr, this); _def->forEachTrigger(event, xec); // check for built-in event handler. std::unique_ptr code (get_event_handler(event)); if (code.get()) { mr.pushAction(std::move(code), movie_root::PRIORITY_DOACTION); } sendEvent(*getObject(this), get_environment(), event.functionURI()); } void Button::getActiveCharacters(ConstDisplayObjects& list) const { list.clear(); // Copy all the DisplayObjects to the new list, skipping NULL and unloaded // DisplayObjects. std::remove_copy_if(_stateCharacters.begin(), _stateCharacters.end(), std::back_inserter(list), std::bind(&isCharacterNull, std::placeholders::_1, false)); } void Button::getActiveCharacters(DisplayObjects& list, bool includeUnloaded) { list.clear(); // Copy all the DisplayObjects to the new list, skipping NULL // DisplayObjects, optionally including unloaded DisplayObjects. std::remove_copy_if(_stateCharacters.begin(), _stateCharacters.end(), std::back_inserter(list), std::bind(&isCharacterNull, std::placeholders::_1, includeUnloaded)); } void Button::get_active_records(ActiveRecords& list, MouseState state) { list.clear(); using namespace SWF; const DefineButtonTag::ButtonRecords& br = _def->buttonRecords(); size_t index = 0; for (DefineButtonTag::ButtonRecords::const_iterator i = br.begin(), e = br.end(); i != e; ++i, ++index) { const ButtonRecord& rec =*i; if (rec.hasState(state)) list.insert(index); } } #ifdef GNASH_DEBUG_BUTTON_DISPLAYLIST static void dump(Button::DisplayObjects& chars, std::stringstream& ss) { for (size_t i=0, e=chars.size(); igetTarget() << " (depth:" << ch->get_depth()-DisplayObject::staticDepthOffset-1 << " unloaded:" << ch->unloaded() << " destroyed:" << ch->isDestroyed() << ")"; } ss << std::endl; } } #endif void Button::set_current_state(MouseState new_state) { if (new_state == _mouseState) return; #ifdef GNASH_DEBUG_BUTTON_DISPLAYLIST std::stringstream ss; ss << "at set_current_state enter: " << std::endl; dump(_stateCharacters, ss); log_debug("%s", ss.str()); #endif // Get new state records ActiveRecords newChars; get_active_records(newChars, new_state); // For each possible record, check if it should still be there for (size_t i=0, e=_stateCharacters.size(); iunloaded() ) { removeInstanceProperty(*this, oldch); if ( ! oldch->isDestroyed() ) oldch->destroy(); _stateCharacters[i] = nullptr; oldch = nullptr; } if ( oldch ) // the one we have should not be there... unload! { set_invalidated(); if ( ! oldch->unload() ) { // No onUnload handler: destroy and clear slot removeInstanceProperty(*this, oldch); if (!oldch->isDestroyed()) oldch->destroy(); _stateCharacters[i] = nullptr; } else { // onUnload handler: shift depth and keep slot int oldDepth = oldch->get_depth(); int newDepth = DisplayObject::removedDepthOffset - oldDepth; #ifdef GNASH_DEBUG_BUTTON_DISPLAYLIST log_debug("Removed button record shifted from depth %d to depth %d", oldDepth, newDepth); #endif oldch->set_depth(newDepth); } } } else // should be there { // Is there already, but is unloaded: destroy and consider as gone if ( oldch && oldch->unloaded() ) { removeInstanceProperty(*this, oldch); if ( ! oldch->isDestroyed() ) oldch->destroy(); _stateCharacters[i] = nullptr; oldch = nullptr; } if (!oldch) { // Not there, instantiate const SWF::ButtonRecord& rec = _def->buttonRecords()[i]; DisplayObject* ch = rec.instantiate(this); set_invalidated(); _stateCharacters[i] = ch; addInstanceProperty(*this, ch); ch->construct(); } } } #ifdef GNASH_DEBUG_BUTTON_DISPLAYLIST ss.str(""); ss << "at set_current_state end: " << std::endl; dump(_stateCharacters, ss); log_debug("%s", ss.str()); #endif // Remember current state _mouseState = new_state; } void Button::add_invalidated_bounds(InvalidatedRanges& ranges, bool force) { // Not visible anyway if (!visible()) return; ranges.add(m_old_invalidated_ranges); DisplayObjects actChars; getActiveCharacters(actChars); std::for_each(actChars.begin(), actChars.end(), std::bind(&DisplayObject::add_invalidated_bounds, std::placeholders::_1, std::ref(ranges), force || invalidated()) ); } SWFRect Button::getBounds() const { SWFRect allBounds; typedef std::vector Chars; Chars actChars; getActiveCharacters(actChars); for (Chars::const_iterator i = actChars.begin(), e = actChars.end(); i != e; ++i) { const DisplayObject* ch = *i; // Child bounds need be transformed in our coordinate space SWFRect lclBounds = ch->getBounds(); SWFMatrix m = getMatrix(*ch); allBounds.expand_to_transformed_rect(m, lclBounds); } return allBounds; } bool Button::pointInShape(std::int32_t x, std::int32_t y) const { typedef std::vector Chars; Chars actChars; getActiveCharacters(actChars); for(Chars::const_iterator i=actChars.begin(),e=actChars.end(); i!=e; ++i) { const DisplayObject* ch = *i; if (ch->pointInShape(x,y)) return true; } return false; } void Button::construct(as_object* initObj) { // This can happen if attachMovie is called with an exported Button and // an init object. The attachment happens, but the init object is not used // (see misc-ming.all/attachMovieTest.swf). if (initObj) { IF_VERBOSE_ASCODING_ERRORS( log_aserror(_("Button placed with an init object. This will " "be ignored.")); ); } saveOriginalTarget(); // for soft refs // Don't register this button instance as a live DisplayObject. // Instantiate the hit DisplayObjects ActiveRecords hitChars; get_active_records(hitChars, MOUSESTATE_HIT); for (const auto& hitChar : hitChars) { const SWF::ButtonRecord& rec = _def->buttonRecords()[hitChar]; // These should not be named! DisplayObject* ch = rec.instantiate(this, false); _hitCharacters.push_back(ch); } // Setup the state DisplayObjects container // It will have a slot for each DisplayObject record. // Some slots will probably be never used (consider HIT-only records) // but for now this direct corrispondence between record number // and active DisplayObject will be handy. _stateCharacters.resize(_def->buttonRecords().size()); // Instantiate the default state DisplayObjects ActiveRecords upChars; get_active_records(upChars, MOUSESTATE_UP); for (auto rno : upChars) { const SWF::ButtonRecord& rec = _def->buttonRecords()[rno]; DisplayObject* ch = rec.instantiate(this); _stateCharacters[rno] = ch; addInstanceProperty(*this, ch); ch->construct(); } // There is no INITIALIZE/CONSTRUCT/LOAD/ENTERFRAME/UNLOAD event // for Buttons // Register key events. if (_def->hasKeyPressHandler()) { stage().registerButton(this); } } void Button::markOwnResources() const { // Mark state DisplayObjects as reachable for (DisplayObject* ch : _stateCharacters) { if (ch) ch->setReachable(); } // Mark hit DisplayObjects as reachable std::for_each(_hitCharacters.begin(), _hitCharacters.end(), std::mem_fun(&DisplayObject::setReachable)); } bool Button::unloadChildren() { bool childsHaveUnload = false; // We need to unload all children, or the global instance list // will keep growing forever ! for (DisplayObject* ch : _stateCharacters) { if (!ch || ch->unloaded()) continue; if (ch->unload()) childsHaveUnload = true; } // NOTE: we don't need to ::unload or ::destroy here // as the _hitCharacters are never placed on stage. // As an optimization we might not even instantiate // them, and only use the definition and the // associated transform SWFMatrix... (would take // hit instance off the GC). _hitCharacters.clear(); return childsHaveUnload; } void Button::destroy() { stage().removeButton(this); for (DisplayObject* ch : _stateCharacters) { if (!ch || ch->isDestroyed()) continue; ch->destroy(); } // NOTE: we don't need to ::unload or ::destroy here // as the _hitCharacters are never placed on stage. // As an optimization we might not even instantiate // them, and only use the definition and the // associated transform SWFMatrix... (would take // hit instance off the GC). _hitCharacters.clear(); DisplayObject::destroy(); } int Button::getDefinitionVersion() const { return _def->getSWFVersion(); } void button_class_init(as_object& global, const ObjectURI& uri) { // This is going to be the global Button "class"/"function" Global_as& gl = getGlobal(global); as_object* proto = createObject(gl); as_object* cl = gl.createClass(emptyFunction, proto); attachButtonInterface(*proto); // Register _global.MovieClip global.init_member(uri, cl, as_object::DefaultFlags); } void registerButtonNative(as_object& global) { VM& vm = getVM(global); vm.registerNative(button_setTabIndex, 105, 1); vm.registerNative(button_getTabIndex, 105, 2); vm.registerNative(button_getDepth, 105, 3); vm.registerNative(button_scale9Grid, 105, 4); vm.registerNative(button_filters, 105, 5); vm.registerNative(button_cacheAsBitmap, 105, 6); vm.registerNative(button_blendMode, 105, 7); } #ifdef USE_SWFTREE DisplayObject::InfoTree::iterator Button::getMovieInfo(InfoTree& tr, InfoTree::iterator it) { InfoTree::iterator selfIt = DisplayObject::getMovieInfo(tr, it); std::ostringstream os; DisplayObjects actChars; getActiveCharacters(actChars, true); std::sort(actChars.begin(), actChars.end(), charDepthLessThen); os.str(""); os << std::boolalpha << isEnabled(); InfoTree::iterator localIter = tr.append_child(selfIt, std::make_pair(_("Enabled"), os.str())); os.str(""); os << _mouseState; localIter = tr.append_child(selfIt, std::make_pair(_("Button state"), os.str())); os.str(""); os << actChars.size(); localIter = tr.append_child(selfIt, std::make_pair(_("Action characters"), os.str())); std::for_each(actChars.begin(), actChars.end(), std::bind(&DisplayObject::getMovieInfo, std::placeholders::_1, tr, localIter)); return selfIt; } #endif std::ostream& operator<<(std::ostream& o, const Button::MouseState& st) { switch (st) { case Button::MOUSESTATE_UP: return o << "UP"; case Button::MOUSESTATE_DOWN: return o << "DOWN"; case Button::MOUSESTATE_OVER: return o << "OVER"; case Button::MOUSESTATE_HIT: return o << "HIT"; default: return o << "Unknown state"; } } namespace { as_value button_blendMode(const fn_call& fn) { Button* obj = ensure >(fn); LOG_ONCE(log_unimpl(_("Button.blendMode"))); UNUSED(obj); return as_value(); } as_value button_cacheAsBitmap(const fn_call& fn) { Button* obj = ensure >(fn); LOG_ONCE(log_unimpl(_("Button.cacheAsBitmap"))); UNUSED(obj); return as_value(); } as_value button_filters(const fn_call& fn) { Button* obj = ensure >(fn); LOG_ONCE(log_unimpl(_("Button.filters"))); UNUSED(obj); return as_value(); } as_value button_scale9Grid(const fn_call& fn) { Button* obj = ensure >(fn); LOG_ONCE(log_unimpl(_("Button.scale9Grid"))); UNUSED(obj); return as_value(); } as_value button_getTabIndex(const fn_call& fn) { Button* obj = ensure >(fn); LOG_ONCE(log_unimpl(_("Button.getTabIndex"))); UNUSED(obj); return as_value(); } as_value button_setTabIndex(const fn_call& fn) { Button* obj = ensure >(fn); LOG_ONCE(log_unimpl(_("Button.setTabIndex"))); UNUSED(obj); return as_value(); } as_value button_getDepth(const fn_call& fn) { // This does exactly the same as MovieClip.getDepth, but appears to be // a separate function. DisplayObject* obj = ensure >(fn); return as_value(obj->get_depth()); } } // anonymous namespace } // end of namespace gnash // Local Variables: // mode: C++ // c-basic-offset: 8 // tab-width: 8 // indent-tabs-mode: nil // End: