CHOP-763: Implement widget-pathname-based routing for mouse events.

Send mouseDown(), mouseUp(), mouseMove() through static mouseEvent() helper
function. Process new optional ["path"] param, validating corresponding LLView
and capturing certain information about it for caller. Synthesize (x, y) pos
if need be. Use LLView::TemporaryDrilldownFunc and llview::TargetEvent to
temporarily hijack normal LLView mouse-event propagation.
Define Response helper class to capture LLSD blob about the current request
and ensure it gets sent on return.
master
Nat Goodspeed 2011-08-29 15:57:32 -04:00
parent 34a620523a
commit f08d7fba9f
1 changed files with 183 additions and 16 deletions

View File

@ -34,10 +34,19 @@
#include "llwindowcallbacks.h"
#include "llui.h"
#include "llview.h"
#include "llviewinject.h"
#include "llviewerwindow.h"
#include "llviewerkeyboard.h"
#include "llrootview.h"
#include "llsdutil.h"
#include "stringize.h"
#include <typeinfo>
#include <map>
#include <boost/scoped_ptr.hpp>
#include <boost/lambda/core.hpp>
#include <boost/lambda/bind.hpp>
namespace bll = boost::lambda;
LLWindowListener::LLWindowListener(LLViewerWindow *window, const KeyboardGetter& kbgetter)
: LLEventAPI("LLWindow", "Inject input events into the LLWindow instance"),
@ -233,12 +242,12 @@ void LLWindowListener::keyUp(LLSD const & evt)
}
// for WhichButton
typedef BOOL (LLWindowCallbacks::*MouseFunc)(LLWindow *, LLCoordGL, MASK);
typedef BOOL (LLWindowCallbacks::*MouseMethod)(LLWindow *, LLCoordGL, MASK);
struct Actions
{
Actions(const MouseFunc& d, const MouseFunc& u): down(d), up(u), valid(true) {}
Actions(const MouseMethod& d, const MouseMethod& u): down(d), up(u), valid(true) {}
Actions(): valid(false) {}
MouseFunc down, up;
MouseMethod down, up;
bool valid;
};
@ -256,38 +265,196 @@ struct WhichButton: public StringLookup<Actions>
};
static WhichButton buttons;
static LLCoordGL getPos(const LLSD& event)
struct Response
{
return LLCoordGL(event["x"].asInteger(), event["y"].asInteger());
Response(const LLSD& seed, const LLSD& request, const LLSD::String& replyKey="reply"):
mResp(seed),
mReq(request),
mKey(replyKey)
{}
~Response()
{
// When you instantiate a stack Response object, if the original
// request requested a reply, send it when we leave this block, no
// matter how.
sendReply(mResp, mReq, mKey);
}
void warn(const std::string& warning)
{
LL_WARNS("LLWindowListener") << warning << LL_ENDL;
mResp["warnings"].append(warning);
}
void error(const std::string& error)
{
// Use LL_WARNS rather than LL_ERROR: we don't want the viewer to shut
// down altogether.
LL_WARNS("LLWindowListener") << error << LL_ENDL;
mResp["error"] = error;
}
// set other keys...
LLSD& operator[](const LLSD::String& key) { return mResp[key]; }
LLSD mResp, mReq;
LLSD::String mKey;
};
typedef boost::function<bool(LLCoordGL, MASK)> MouseFunc;
static void mouseEvent(const MouseFunc& func, const LLSD& request)
{
// Ensure we send response
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"));
boost::scoped_ptr<LLView::TemporaryDrilldownFunc> 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::getRootView();
LLView* target = LLUI::resolvePath(root, path);
if (! target)
{
return response.error(STRINGIZE(request["op"].asString() << " request "
"specified invalid \"path\": '" << path << "'"));
return;
}
// Get info about this LLView* for when we send response.
response["path"] = target->getPathname();
response["class"] = typeid(*target).name();
bool visible_chain(target->isInVisibleChain());
bool enabled_chain(target->isInEnabledChain());
response["visible"] = target->getVisible();
response["visible_chain"] = visible_chain;
response["enabled"] = target->getEnabled();
response["enabled_chain"] = enabled_chain;
response["available"] = target->isAvailable();
// Don't show caller the LLView's own relative rectangle; that only
// tells its dimensions. Provide actual location on screen.
LLRect rect(target->calcScreenRect());
response["rect"] = LLSDMap("left", rect.mLeft)("top", rect.mTop)("right", rect.mRight)("bottom", rect.mBottom);
// 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 (! visible_chain)
{
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 (! enabled_chain)
{
response.warn(STRINGIZE(request["op"].asString() << " request "
"specified \"path\" not currently enabled: '"
<< path << "'"));
}
if (! has_pos)
{
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 << ")"));
}
// 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 '" << frontmost->getPathname() << "'"));
}
// 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 & evt)
void LLWindowListener::mouseDown(LLSD const & request)
{
Actions actions(buttons.lookup(evt["button"]));
Actions actions(buttons.lookup(request["button"]));
if (actions.valid)
{
(mWindow->*(actions.down))(NULL, getPos(evt), getMask(evt));
// Normally you can pass NULL to an LLWindow* without compiler
// complaint, but going through boost::lambda::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(bll::bind(actions.down, mWindow,
static_cast<LLWindow*>(NULL), bll::_1, bll::_2),
request);
}
}
void LLWindowListener::mouseUp(LLSD const & evt)
void LLWindowListener::mouseUp(LLSD const & request)
{
Actions actions(buttons.lookup(evt["button"]));
Actions actions(buttons.lookup(request["button"]));
if (actions.valid)
{
(mWindow->*(actions.up))(NULL, getPos(evt), getMask(evt));
mouseEvent(bll::bind(actions.up, mWindow,
static_cast<LLWindow*>(NULL), bll::_1, bll::_2),
request);
}
}
void LLWindowListener::mouseMove(LLSD const & evt)
void LLWindowListener::mouseMove(LLSD const & request)
{
mWindow->handleMouseMove(NULL, getPos(evt), getMask(evt));
// 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 (void-lambda-expression, true) to construct
// a callable that returns bool anyway. Pass 'true' because we expect that
// our caller will usually treat 'false' as a problem.
mouseEvent((bll::bind(&LLWindowCallbacks::handleMouseMove, mWindow,
static_cast<LLWindow*>(NULL), bll::_1, bll::_2),
true),
request);
}
void LLWindowListener::mouseScroll(LLSD const & evt)
void LLWindowListener::mouseScroll(LLSD const & request)
{
S32 clicks = evt["clicks"].asInteger();
S32 clicks = request["clicks"].asInteger();
mWindow->handleScrollWheel(NULL, clicks);
}