/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* * nsWinGesture - Touch input handling for tablet displays. */ #include "nscore.h" #include "nsWinGesture.h" #include "nsUXThemeData.h" #include "mozilla/Logging.h" #include "mozilla/MouseEvents.h" #include "mozilla/Preferences.h" #include "mozilla/TouchEvents.h" #include "mozilla/dom/SimpleGestureEventBinding.h" #include "mozilla/dom/WheelEventBinding.h" #include using namespace mozilla; using namespace mozilla::widget; extern mozilla::LazyLogModule gWindowsLog; static bool gEnableSingleFingerPanEvents = false; nsWinGesture::nsWinGesture() : mPanActive(false), mFeedbackActive(false), mXAxisFeedback(false), mYAxisFeedback(false), mPanInertiaActive(false) { (void)InitLibrary(); mPixelScrollOverflow = 0; } /* Load and shutdown */ bool nsWinGesture::InitLibrary() { // Check to see if we want single finger gesture input. Only do this once // for the app so we don't have to look it up on every window create. gEnableSingleFingerPanEvents = Preferences::GetBool("gestures.enable_single_finger_input", false); return true; } #define GCOUNT 5 bool nsWinGesture::SetWinGestureSupport( HWND hWnd, WidgetGestureNotifyEvent::PanDirection aDirection) { GESTURECONFIG config[GCOUNT]; memset(&config, 0, sizeof(config)); config[0].dwID = GID_ZOOM; config[0].dwWant = GC_ZOOM; config[0].dwBlock = 0; config[1].dwID = GID_ROTATE; config[1].dwWant = GC_ROTATE; config[1].dwBlock = 0; config[2].dwID = GID_PAN; config[2].dwWant = GC_PAN | GC_PAN_WITH_INERTIA | GC_PAN_WITH_GUTTER; config[2].dwBlock = GC_PAN_WITH_SINGLE_FINGER_VERTICALLY | GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY; if (gEnableSingleFingerPanEvents) { if (aDirection == WidgetGestureNotifyEvent::ePanVertical || aDirection == WidgetGestureNotifyEvent::ePanBoth) { config[2].dwWant |= GC_PAN_WITH_SINGLE_FINGER_VERTICALLY; config[2].dwBlock -= GC_PAN_WITH_SINGLE_FINGER_VERTICALLY; } if (aDirection == WidgetGestureNotifyEvent::ePanHorizontal || aDirection == WidgetGestureNotifyEvent::ePanBoth) { config[2].dwWant |= GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY; config[2].dwBlock -= GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY; } } config[3].dwWant = GC_TWOFINGERTAP; config[3].dwID = GID_TWOFINGERTAP; config[3].dwBlock = 0; config[4].dwWant = GC_PRESSANDTAP; config[4].dwID = GID_PRESSANDTAP; config[4].dwBlock = 0; return SetGestureConfig(hWnd, 0, GCOUNT, (PGESTURECONFIG)&config, sizeof(GESTURECONFIG)); } /* Helpers */ bool nsWinGesture::IsPanEvent(LPARAM lParam) { GESTUREINFO gi; ZeroMemory(&gi, sizeof(GESTUREINFO)); gi.cbSize = sizeof(GESTUREINFO); BOOL result = GetGestureInfo((HGESTUREINFO)lParam, &gi); if (!result) return false; if (gi.dwID == GID_PAN) return true; return false; } /* Gesture event processing */ bool nsWinGesture::ProcessGestureMessage(HWND hWnd, WPARAM wParam, LPARAM lParam, WidgetSimpleGestureEvent& evt) { GESTUREINFO gi; ZeroMemory(&gi, sizeof(GESTUREINFO)); gi.cbSize = sizeof(GESTUREINFO); BOOL result = GetGestureInfo((HGESTUREINFO)lParam, &gi); if (!result) return false; // The coordinates of this event nsPointWin coord; coord = gi.ptsLocation; coord.ScreenToClient(hWnd); evt.mRefPoint = LayoutDeviceIntPoint(coord.x, coord.y); // Multiple gesture can occur at the same time so gesture state // info can't be shared. switch (gi.dwID) { case GID_BEGIN: case GID_END: // These should always fall through to DefWndProc return false; break; case GID_ZOOM: { if (gi.dwFlags & GF_BEGIN) { // Send a zoom start event // The low 32 bits are the distance in pixels. mZoomIntermediate = (float)gi.ullArguments; evt.mMessage = eMagnifyGestureStart; evt.mDelta = 0.0; } else if (gi.dwFlags & GF_END) { // Send a zoom end event, the delta is the change // in touch points. evt.mMessage = eMagnifyGesture; // (positive for a "zoom in") evt.mDelta = -1.0 * (mZoomIntermediate - (float)gi.ullArguments); mZoomIntermediate = (float)gi.ullArguments; } else { // Send a zoom intermediate event, the delta is the change // in touch points. evt.mMessage = eMagnifyGestureUpdate; // (positive for a "zoom in") evt.mDelta = -1.0 * (mZoomIntermediate - (float)gi.ullArguments); mZoomIntermediate = (float)gi.ullArguments; } } break; case GID_ROTATE: { // Send a rotate start event double radians = 0.0; // On GF_BEGIN, ullArguments contains the absolute rotation at the // start of the gesture. In later events it contains the offset from // the start angle. if (gi.ullArguments != 0) radians = GID_ROTATE_ANGLE_FROM_ARGUMENT(gi.ullArguments); double degrees = -1 * radians * (180 / M_PI); if (gi.dwFlags & GF_BEGIN) { // At some point we should pass the initial angle in // along with delta. It's useful. degrees = mRotateIntermediate = 0.0; } evt.mDirection = 0; evt.mDelta = degrees - mRotateIntermediate; mRotateIntermediate = degrees; if (evt.mDelta > 0) { evt.mDirection = dom::SimpleGestureEvent_Binding::ROTATION_COUNTERCLOCKWISE; } else if (evt.mDelta < 0) { evt.mDirection = dom::SimpleGestureEvent_Binding::ROTATION_CLOCKWISE; } if (gi.dwFlags & GF_BEGIN) { evt.mMessage = eRotateGestureStart; } else if (gi.dwFlags & GF_END) { evt.mMessage = eRotateGesture; } else { evt.mMessage = eRotateGestureUpdate; } } break; case GID_TWOFINGERTAP: // Normally maps to "restore" from whatever you may have recently changed. // A simple double click. evt.mMessage = eTapGesture; evt.mClickCount = 1; break; case GID_PRESSANDTAP: // Two finger right click. Defaults to right click if it falls through. evt.mMessage = ePressTapGesture; evt.mClickCount = 1; break; } return true; } bool nsWinGesture::ProcessPanMessage(HWND hWnd, WPARAM wParam, LPARAM lParam) { GESTUREINFO gi; ZeroMemory(&gi, sizeof(GESTUREINFO)); gi.cbSize = sizeof(GESTUREINFO); BOOL result = GetGestureInfo((HGESTUREINFO)lParam, &gi); if (!result) return false; // The coordinates of this event nsPointWin coord; coord = mPanRefPoint = gi.ptsLocation; // We want screen coordinates in our local offsets as client coordinates will // change when feedback is taking place. Gui events though require client // coordinates. mPanRefPoint.ScreenToClient(hWnd); switch (gi.dwID) { case GID_BEGIN: case GID_END: // These should always fall through to DefWndProc return false; break; // Setup pixel scroll events for both axis case GID_PAN: { if (gi.dwFlags & GF_BEGIN) { mPanIntermediate = coord; mPixelScrollDelta = 0; mPanActive = true; mPanInertiaActive = false; } else { #ifdef DBG_jimm int32_t deltaX = mPanIntermediate.x - coord.x; int32_t deltaY = mPanIntermediate.y - coord.y; MOZ_LOG(gWindowsLog, LogLevel::Info, ("coordX=%d coordY=%d deltaX=%d deltaY=%d x:%d y:%d\n", coord.x, coord.y, deltaX, deltaY, mXAxisFeedback, mYAxisFeedback)); #endif mPixelScrollDelta.x = mPanIntermediate.x - coord.x; mPixelScrollDelta.y = mPanIntermediate.y - coord.y; mPanIntermediate = coord; if (gi.dwFlags & GF_INERTIA) mPanInertiaActive = true; if (gi.dwFlags & GF_END) { mPanActive = false; mPanInertiaActive = false; PanFeedbackFinalize(hWnd, true); } } } break; } return true; } inline bool TestTransition(int32_t a, int32_t b) { // If a is zero, overflow is zero, implying the cursor has moved back to the // start position. If b is zero, cached overscroll is zero, implying feedback // just begun. if (a == 0 || b == 0) return true; // Test for different signs. return (a < 0) == (b < 0); } void nsWinGesture::UpdatePanFeedbackX(HWND hWnd, int32_t scrollOverflow, bool& endFeedback) { // If scroll overflow was returned indicating we panned past the bounds of // the scrollable view port, start feeback. if (scrollOverflow != 0) { if (!mFeedbackActive) { BeginPanningFeedback(hWnd); mFeedbackActive = true; } endFeedback = false; mXAxisFeedback = true; return; } if (mXAxisFeedback) { int32_t newOverflow = mPixelScrollOverflow.x - mPixelScrollDelta.x; // Detect a reverse transition past the starting drag point. This tells us // the user has panned all the way back so we can stop providing feedback // for this axis. if (!TestTransition(newOverflow, mPixelScrollOverflow.x) || newOverflow == 0) return; // Cache the total over scroll in pixels. mPixelScrollOverflow.x = newOverflow; endFeedback = false; } } void nsWinGesture::UpdatePanFeedbackY(HWND hWnd, int32_t scrollOverflow, bool& endFeedback) { // If scroll overflow was returned indicating we panned past the bounds of // the scrollable view port, start feeback. if (scrollOverflow != 0) { if (!mFeedbackActive) { BeginPanningFeedback(hWnd); mFeedbackActive = true; } endFeedback = false; mYAxisFeedback = true; return; } if (mYAxisFeedback) { int32_t newOverflow = mPixelScrollOverflow.y - mPixelScrollDelta.y; // Detect a reverse transition past the starting drag point. This tells us // the user has panned all the way back so we can stop providing feedback // for this axis. if (!TestTransition(newOverflow, mPixelScrollOverflow.y) || newOverflow == 0) return; // Cache the total over scroll in pixels. mPixelScrollOverflow.y = newOverflow; endFeedback = false; } } void nsWinGesture::PanFeedbackFinalize(HWND hWnd, bool endFeedback) { if (!mFeedbackActive) return; if (endFeedback) { mFeedbackActive = false; mXAxisFeedback = false; mYAxisFeedback = false; mPixelScrollOverflow = 0; EndPanningFeedback(hWnd, TRUE); return; } UpdatePanningFeedback(hWnd, mPixelScrollOverflow.x, mPixelScrollOverflow.y, mPanInertiaActive); } bool nsWinGesture::PanDeltaToPixelScroll(WidgetWheelEvent& aWheelEvent) { aWheelEvent.mDeltaX = aWheelEvent.mDeltaY = aWheelEvent.mDeltaZ = 0.0; aWheelEvent.mLineOrPageDeltaX = aWheelEvent.mLineOrPageDeltaY = 0; aWheelEvent.mRefPoint = LayoutDeviceIntPoint(mPanRefPoint.x, mPanRefPoint.y); aWheelEvent.mDeltaMode = dom::WheelEvent_Binding::DOM_DELTA_PIXEL; aWheelEvent.mScrollType = WidgetWheelEvent::SCROLL_SYNCHRONOUSLY; aWheelEvent.mIsNoLineOrPageDelta = true; aWheelEvent.mOverflowDeltaX = 0.0; aWheelEvent.mOverflowDeltaY = 0.0; // Don't scroll the view if we are currently at a bounds, or, if we are // panning back from a max feedback position. This keeps the original drag // point constant. if (!mXAxisFeedback) { aWheelEvent.mDeltaX = mPixelScrollDelta.x; } if (!mYAxisFeedback) { aWheelEvent.mDeltaY = mPixelScrollDelta.y; } return (aWheelEvent.mDeltaX != 0 || aWheelEvent.mDeltaY != 0); }