/** * @file llwindowlistener.cpp * @brief EventAPI interface for injecting input into LLWindow * * $LicenseInfo:firstyear=2001&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2010, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License only. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ */ #include "llviewerprecompiledheaders.h" #include "linden_common.h" #include "llwindowlistener.h" #include "llcoord.h" #include "llfocusmgr.h" #include "llkeyboard.h" #include "llwindowcallbacks.h" #include "llui.h" #include "llview.h" #include "llviewinject.h" #include "llviewerwindow.h" #include "llviewerinput.h" #include "llrootview.h" #include "llsdutil.h" #include "stringize.h" #include #include #include #include LLWindowListener::LLWindowListener(LLViewerWindow *window, const KeyboardGetter& kbgetter) : LLEventAPI("LLWindow", "Inject input events into the LLWindow instance"), mWindow(window), mKbGetter(kbgetter) { std::string keySomething = "Given [\"keysym\"], [\"keycode\"] or [\"char\"], inject the specified "; std::string keyExplain = "(integer keycode values, or keysym string from any addKeyName() call in\n" "https://github.com/secondlife/viewer/blob/main/indra/llwindow/llkeyboard.cpp#L68-L124)\n"; std::string mask = "Specify optional [\"mask\"] as an array containing any of \"CTL\", \"ALT\",\n" "\"SHIFT\" or \"MAC_CONTROL\"; the corresponding modifier bits will be combined\n" "to form the mask used with the event."; std::string given = "Given "; std::string mouseParams = "optional [\"path\"], optional [\"x\"] and [\"y\"], inject the requested mouse "; std::string buttonParams = std::string("[\"button\"], ") + mouseParams; std::string buttonExplain = "(button values \"LEFT\", \"MIDDLE\", \"RIGHT\")\n"; std::string paramsExplain = "[\"path\"] is as for LLUI::getInstance()->resolvePath(), described in\n" "http://bitbucket.org/lindenlab/viewer-release/src/tip/indra/llui/llui.h\n" "If you omit [\"path\"], you must specify both [\"x\"] and [\"y\"].\n" "If you specify [\"path\"] without both [\"x\"] and [\"y\"], will synthesize (x, y)\n" "in the center of the LLView selected by [\"path\"].\n" "You may specify [\"path\"] with both [\"x\"] and [\"y\"], will use your (x, y).\n" "This may cause the LLView selected by [\"path\"] to reject the event.\n" "Optional [\"reply\"] requests a reply event on the named LLEventPump.\n" "reply[\"error\"] isUndefined (None) on success, else an explanatory message.\n"; add("getInfo", "Get information about the ui element specified by [\"path\"]", &LLWindowListener::getInfo, LLSDMap("reply", LLSD())); add("getPaths", "Send on [\"reply\"] an event in which [\"paths\"] is an array of valid LLView\n" "pathnames. Optional [\"under\"] pathname specifies the base node under which\n" "to list; all nodes from root if no [\"under\"].", &LLWindowListener::getPaths, LLSDMap("reply", LLSD())); add("keyDown", keySomething + "keypress event.\n" + keyExplain + mask, &LLWindowListener::keyDown); add("keyUp", keySomething + "key release event.\n" + keyExplain + mask, &LLWindowListener::keyUp); add("mouseDown", given + buttonParams + "click event.\n" + buttonExplain + paramsExplain + mask, &LLWindowListener::mouseDown); add("mouseUp", given + buttonParams + "release event.\n" + buttonExplain + paramsExplain + mask, &LLWindowListener::mouseUp); add("mouseMove", given + mouseParams + "movement event.\n" + paramsExplain + mask, &LLWindowListener::mouseMove); add("mouseScroll", "Given an integer number of [\"clicks\"], inject the requested mouse scroll event.\n" "(positive clicks moves downward through typical content)", &LLWindowListener::mouseScroll); } template class StringLookup { private: std::string mDesc; typedef std::map Map; Map mMap; public: StringLookup(const std::string& desc): mDesc(desc) {} MAPPED lookup(const typename Map::key_type& key) const { typename Map::const_iterator found = mMap.find(key); if (found == mMap.end()) { LL_WARNS("LLWindowListener") << "Unknown " << mDesc << " '" << key << "'" << LL_ENDL; return MAPPED(); } return found->second; } protected: void add(const typename Map::key_type& key, const typename Map::mapped_type& value) { mMap.insert(typename Map::value_type(key, value)); } }; namespace { // helper for getMask() MASK lookupMask_(const std::string& maskname) { // It's unclear to me whether MASK_MAC_CONTROL is important, but it's not // supported by maskFromString(). Handle that specially. if (maskname == "MAC_CONTROL") { return MASK_MAC_CONTROL; } else { // In case of lookup failure, return MASK_NONE, which won't affect our // caller's OR. MASK mask(MASK_NONE); LLKeyboard::maskFromString(maskname, &mask); return mask; } } MASK getMask(const LLSD& event) { LLSD masknames(event["mask"]); if (! masknames.isArray()) { // If event["mask"] is a single string, perform normal lookup on it. return lookupMask_(masknames); } // Here event["mask"] is an array of mask-name strings. OR together their // corresponding bits. MASK mask(MASK_NONE); for (LLSD::array_const_iterator ai(masknames.beginArray()), aend(masknames.endArray()); ai != aend; ++ai) { mask |= lookupMask_(*ai); } return mask; } KEY getKEY(const LLSD& event) { if (event.has("keysym")) { // Initialize to KEY_NONE; that way we can ignore the bool return from // keyFromString() and, in the lookup-fail case, simply return KEY_NONE. KEY key(KEY_NONE); LLKeyboard::keyFromString(event["keysym"], &key); return key; } else if (event.has("keycode")) { return KEY(event["keycode"].asInteger()); } else { return KEY(event["char"].asString()[0]); } } } // namespace void LLWindowListener::getInfo(LLSD const & evt) { Response response(LLSD(), evt); if (evt.has("path")) { std::string path(evt["path"]); LLView * target_view = LLUI::getInstance()->resolvePath(LLUI::getInstance()->getRootView(), path); if (target_view != 0) { response.setResponse(target_view->getInfo()); } else { response.error(STRINGIZE(evt["op"].asString() << " request " "specified invalid \"path\": '" << path << "'")); } } else { response.error( STRINGIZE(evt["op"].asString() << "request did not provide a path" )); } } void LLWindowListener::getPaths(LLSD const & request) { Response response(LLSD(), request); LLView *root(LLUI::getInstance()->getRootView()), *base(NULL); // Capturing request["under"] as string means we conflate the case in // which there is no ["under"] key with the case in which its value is the // empty string. That seems to make sense to me. std::string under(request["under"]); // Deal with optional "under" parameter if (under.empty()) { base = root; } else { base = LLUI::getInstance()->resolvePath(root, under); if (! base) { return response.error(STRINGIZE(request["op"].asString() << " request " "specified invalid \"under\" path: '" << under << "'")); } } // Traverse the entire subtree under 'base', collecting pathnames for (LLView::tree_iterator_t ti(base->beginTreeDFS()), tend(base->endTreeDFS()); ti != tend; ++ti) { response["paths"].append((*ti)->getPathname()); } } void LLWindowListener::keyDown(LLSD const & evt) { Response response(LLSD(), evt); KEY key = getKEY(evt); MASK mask = getMask(evt); if (evt.has("path")) { std::string path(evt["path"]); LLView * target_view = LLUI::getInstance()->resolvePath(LLUI::getInstance()->getRootView(), path); if (target_view == 0) { response.error(STRINGIZE(evt["op"].asString() << " request " "specified invalid \"path\": '" << path << "'")); } else if(target_view->isAvailable()) { response.setResponse(target_view->getInfo()); gFocusMgr.setKeyboardFocus(target_view); gViewerInput.handleKey(key, mask, false); if(key < 0x80) mWindow->handleUnicodeChar(key, mask); } else { response.error(STRINGIZE(evt["op"].asString() << " request " "element specified by \"path\": '" << path << "'" << " is not visible")); } } else { gViewerInput.handleKey(key, mask, false); if(key < 0x80) mWindow->handleUnicodeChar(key, mask); } } void LLWindowListener::keyUp(LLSD const & evt) { Response response(LLSD(), evt); if (evt.has("path")) { std::string path(evt["path"]); LLView * target_view = LLUI::getInstance()->resolvePath(LLUI::getInstance()->getRootView(), path); if (target_view == 0 ) { response.error(STRINGIZE(evt["op"].asString() << " request " "specified invalid \"path\": '" << path << "'")); } else if (target_view->isAvailable()) { response.setResponse(target_view->getInfo()); gFocusMgr.setKeyboardFocus(target_view); mKbGetter()->handleTranslatedKeyUp(getKEY(evt), getMask(evt)); } else { response.error(STRINGIZE(evt["op"].asString() << " request " "element specified byt \"path\": '" << path << "'" << " is not visible")); } } else { mKbGetter()->handleTranslatedKeyUp(getKEY(evt), getMask(evt)); } } // for WhichButton typedef BOOL (LLWindowCallbacks::*MouseMethod)(LLWindow *, LLCoordGL, MASK); struct Actions { Actions(const MouseMethod& d, const MouseMethod& u): down(d), up(u), valid(true) {} Actions(): valid(false) {} MouseMethod down, up; bool valid; }; struct WhichButton: public StringLookup { WhichButton(): StringLookup("mouse button") { add("LEFT", Actions(&LLWindowCallbacks::handleMouseDown, &LLWindowCallbacks::handleMouseUp)); add("RIGHT", Actions(&LLWindowCallbacks::handleRightMouseDown, &LLWindowCallbacks::handleRightMouseUp)); add("MIDDLE", Actions(&LLWindowCallbacks::handleMiddleMouseDown, &LLWindowCallbacks::handleMiddleMouseUp)); } }; static WhichButton buttons; typedef boost::function MouseFunc; // Wrap a function returning 'void' to return 'true' instead. I'm sure there's // a more generic way to accomplish this, but generically handling the // arguments seems to require variadic templates and perfect forwarding. (We // used to be able to write (boost::lambda::bind(...), true), counting on // boost::lambda's comma operator overload, until // https://svn.boost.org/trac/boost/ticket/10864. And boost::phoenix doesn't // seem to overload comma the same way; or at least not with bind().) class MouseFuncTrue { typedef boost::function MouseFuncVoid; MouseFuncVoid mFunc; public: MouseFuncTrue(const MouseFuncVoid& func): mFunc(func) {} bool operator()(LLCoordGL coords, MASK mask) const { mFunc(coords, mask); return true; } }; static void mouseEvent(const MouseFunc& func, const LLSD& request) { // Ensure we send response LLEventAPI::Response response(LLSD(), request); // We haven't yet established whether the incoming request has "x" and "y", // but capture this anyway, with 0 for omitted values. LLCoordGL pos(request["x"].asInteger(), request["y"].asInteger()); bool has_pos(request.has("x") && request.has("y")); std::unique_ptr tempfunc; // Documentation for mouseDown(), mouseUp() and mouseMove() claims you // must either specify ["path"], or both of ["x"] and ["y"]. You MAY // specify all. Let's say that passing "path" as an empty string is // equivalent to not passing it at all. std::string path(request["path"]); if (path.empty()) { // Without "path", you must specify both "x" and "y". if (! has_pos) { return response.error(STRINGIZE(request["op"].asString() << " request " "without \"path\" must specify both \"x\" and \"y\": " << request)); } } else // ! path.empty() { LLView* root = LLUI::getInstance()->getRootView(); LLView* target = LLUI::getInstance()->resolvePath(root, path); if (! target) { return response.error(STRINGIZE(request["op"].asString() << " request " "specified invalid \"path\": '" << path << "'")); } response.setResponse(target->getInfo()); // The intent of this test is to prevent trying to drill down to a // widget in a hidden floater, or on a tab that's not current, etc. if (! target->isInVisibleChain()) { return response.error(STRINGIZE(request["op"].asString() << " request " "specified \"path\" not currently visible: '" << path << "'")); } // This test isn't folded in with the above error case since you can // (e.g.) pop up a tooltip even for a disabled widget. if (! target->isInEnabledChain()) { response.warn(STRINGIZE(request["op"].asString() << " request " "specified \"path\" not currently enabled: '" << path << "'")); } if (! has_pos) { LLRect rect(target->calcScreenRect()); pos.set(rect.getCenterX(), rect.getCenterY()); // nonstandard warning tactic: probably usual case; we want event // sender to know synthesized (x, y), but maybe don't need to log? response["warnings"].append(STRINGIZE("using center point (" << pos.mX << ", " << pos.mY << ")")); } /*==========================================================================*| // NEVER MIND: the LLView tree defines priority handler layers in // front of the normal widget set, so this has never yet produced // anything but spam warnings. (sigh) // recursive childFromPoint() should give us the frontmost, leafmost // widget at the specified (x, y). LLView* frontmost = root->childFromPoint(pos.mX, pos.mY, true); if (frontmost != target) { response.warn(STRINGIZE(request["op"].asString() << " request " "specified \"path\" = '" << path << "', but frontmost LLView at (" << pos.mX << ", " << pos.mY << ") is '" << LLView::getPathname(frontmost) << "'")); } |*==========================================================================*/ // Instantiate a TemporaryDrilldownFunc to route incoming mouse events // to the target LLView*. But put it on the heap since "path" is // optional. Nonetheless, manage it with a boost::scoped_ptr so it // will be destroyed when we leave. tempfunc.reset(new LLView::TemporaryDrilldownFunc(llview::TargetEvent(target))); } // The question of whether the requested LLView actually handled the // specified event is important enough, and its handling unclear enough, // to warrant a separate response attribute. Instead of deciding here to // make it a warning, or an error, let caller decide. response["handled"] = func(pos, getMask(request)); // On exiting this scope, response will send, tempfunc will restore the // normal pointInView(x, y) containment logic, etc. } void LLWindowListener::mouseDown(LLSD const & request) { Actions actions(buttons.lookup(request["button"])); if (actions.valid) { // Normally you can pass NULL to an LLWindow* without compiler // complaint, but going through boost::bind() evidently // bypasses that special case: it only knows you're trying to pass an // int to a pointer. Explicitly cast NULL to the desired pointer type. mouseEvent(boost::bind(actions.down, mWindow, static_cast(NULL), _1, _2), request); } } void LLWindowListener::mouseUp(LLSD const & request) { Actions actions(buttons.lookup(request["button"])); if (actions.valid) { mouseEvent(boost::bind(actions.up, mWindow, static_cast(NULL), _1, _2), request); } } void LLWindowListener::mouseMove(LLSD const & request) { // We want to call the same central mouseEvent() routine for // handleMouseMove() as for button clicks. But handleMouseMove() returns // void, whereas mouseEvent() accepts a function returning bool -- and // uses that bool return. Use MouseFuncTrue to construct a callable that // returns bool anyway. mouseEvent(MouseFuncTrue(boost::bind(&LLWindowCallbacks::handleMouseMove, mWindow, static_cast(NULL), _1, _2)), request); } void LLWindowListener::mouseScroll(LLSD const & request) { S32 clicks = request["clicks"].asInteger(); mWindow->handleScrollWheel(NULL, clicks); }