diff --git a/data/pref.xml b/data/pref.xml
index ce21897b0..77faa3ca6 100644
--- a/data/pref.xml
+++ b/data/pref.xml
@@ -154,6 +154,7 @@
diff --git a/data/strings/en.ini b/data/strings/en.ini
index b6465a486..8c61e94e2 100644
--- a/data/strings/en.ini
+++ b/data/strings/en.ini
@@ -1014,6 +1014,15 @@ open_extension_folder = Open &Folder
user_interface = User Interface
native_clipboard = Use native clipboard
native_file_dialog = Use native file dialog
+one_finger_as_mouse_movement = Interpret one finger as mouse movement
+one_finger_as_mouse_movement_tooltip = <<
+
-
+
diff --git a/src/app/app.cpp b/src/app/app.cpp
index 6c4f4b508..3b7bcf1d8 100644
--- a/src/app/app.cpp
+++ b/src/app/app.cpp
@@ -278,6 +278,13 @@ void App::run()
#ifdef ENABLE_UI
// Run the GUI
if (isGui()) {
+#ifdef _WIN32
+ // How to interpret one finger on Windows tablets.
+ ui::Manager::getDefault()->getDisplay()
+ ->setInterpretOneFingerGestureAsMouseMovement(
+ Preferences::instance().experimental.oneFingerAsMouseMovement());
+#endif
+
#if !defined(_WIN32) && !defined(__APPLE__)
// Setup app icon for Linux window managers
try {
diff --git a/src/app/commands/cmd_options.cpp b/src/app/commands/cmd_options.cpp
index 397021760..eebc06cbf 100644
--- a/src/app/commands/cmd_options.cpp
+++ b/src/app/commands/cmd_options.cpp
@@ -244,6 +244,10 @@ public:
if (m_pref.experimental.useNativeFileDialog())
nativeFileDialog()->setSelected(true);
+#ifndef _WIN32
+ oneFingerAsMouseMovement()->setVisible(false);
+#endif
+
if (m_pref.experimental.flashLayer())
flashLayer()->setSelected(true);
@@ -479,6 +483,12 @@ public:
m_pref.experimental.flashLayer(flashLayer()->isSelected());
m_pref.experimental.nonactiveLayersOpacity(nonactiveLayersOpacity()->getValue());
+#ifdef _WIN32
+ manager()->getDisplay()
+ ->setInterpretOneFingerGestureAsMouseMovement(
+ oneFingerAsMouseMovement()->isSelected());
+#endif
+
ui::set_use_native_cursors(m_pref.cursor.useNativeCursor());
ui::set_mouse_cursor_scale(m_pref.cursor.cursorScale());
diff --git a/src/she/alleg4/alleg_display.cpp b/src/she/alleg4/alleg_display.cpp
index 687b83b5d..d0ffdcfeb 100644
--- a/src/she/alleg4/alleg_display.cpp
+++ b/src/she/alleg4/alleg_display.cpp
@@ -730,6 +730,11 @@ void Alleg4Display::setLayout(const std::string& layout)
#endif
}
+void Alleg4Display::setInterpretOneFingerGestureAsMouseMovement(bool state)
+{
+ // Do nothing
+}
+
void* Alleg4Display::nativeHandle()
{
#ifdef _WIN32
diff --git a/src/she/alleg4/alleg_display.h b/src/she/alleg4/alleg_display.h
index e063441f6..51b7fa07f 100644
--- a/src/she/alleg4/alleg_display.h
+++ b/src/she/alleg4/alleg_display.h
@@ -1,5 +1,5 @@
// SHE library
-// Copyright (C) 2012-2017 David Capello
+// Copyright (C) 2012-2018 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
@@ -44,6 +44,7 @@ namespace she {
void releaseMouse() override;
std::string getLayout() override;
void setLayout(const std::string& layout) override;
+ void setInterpretOneFingerGestureAsMouseMovement(bool state) override;
void* nativeHandle() override;
private:
diff --git a/src/she/display.h b/src/she/display.h
index 7fdd77cd8..ae765ff0f 100644
--- a/src/she/display.h
+++ b/src/she/display.h
@@ -1,5 +1,5 @@
// SHE library
-// Copyright (C) 2012-2017 David Capello
+// Copyright (C) 2012-2018 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
@@ -71,6 +71,12 @@ namespace she {
virtual std::string getLayout() = 0;
virtual void setLayout(const std::string& layout) = 0;
+ // For Windows 8/10 only in tablet devices: Set to true if you
+ // want to interpret one finger as the mouse movement and two
+ // fingers as pan/scroll (true by default). If you want to pan
+ // with one finger, call this function with false.
+ virtual void setInterpretOneFingerGestureAsMouseMovement(bool state) = 0;
+
// Returns the HWND on Windows.
virtual DisplayHandle nativeHandle() = 0;
};
diff --git a/src/she/skia/skia_display.cpp b/src/she/skia/skia_display.cpp
index bd82cb916..dfa89a4c5 100644
--- a/src/she/skia/skia_display.cpp
+++ b/src/she/skia/skia_display.cpp
@@ -1,5 +1,5 @@
// SHE library
-// Copyright (C) 2012-2017 David Capello
+// Copyright (C) 2012-2018 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
@@ -185,6 +185,11 @@ void SkiaDisplay::setLayout(const std::string& layout)
m_window.setLayout(layout);
}
+void SkiaDisplay::setInterpretOneFingerGestureAsMouseMovement(bool state)
+{
+ m_window.setInterpretOneFingerGestureAsMouseMovement(state);
+}
+
void SkiaDisplay::setTranslateDeadKeys(bool state)
{
m_window.setTranslateDeadKeys(state);
diff --git a/src/she/skia/skia_display.h b/src/she/skia/skia_display.h
index be9ca2337..7b18dcf6e 100644
--- a/src/she/skia/skia_display.h
+++ b/src/she/skia/skia_display.h
@@ -1,5 +1,5 @@
// SHE library
-// Copyright (C) 2012-2017 David Capello
+// Copyright (C) 2012-2018 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
@@ -61,6 +61,8 @@ public:
std::string getLayout() override;
void setLayout(const std::string& layout) override;
+ void setInterpretOneFingerGestureAsMouseMovement(bool state) override;
+
void setTranslateDeadKeys(bool state);
// Returns the HWND on Windows.
diff --git a/src/she/win/window.cpp b/src/she/win/window.cpp
index 0b2bd6236..4788669b6 100644
--- a/src/she/win/window.cpp
+++ b/src/she/win/window.cpp
@@ -29,6 +29,9 @@
#define KEY_TRACE(...)
#define MOUSE_TRACE(...)
+#define TOUCH_TRACE(...)
+
+#define kFingerAsMouseTimeout 50
// Gets the window client are in absolute/screen coordinates
#define ABS_CLIENT_RC(rc) \
@@ -59,6 +62,14 @@ static PointerType wt_packet_pkcursor_to_pointer_type(int pkCursor)
return PointerType::Unknown;
}
+WinWindow::Touch::Touch()
+ : fingers(0)
+ , canBeMouse(false)
+ , asMouse(false)
+ , timerID(0)
+{
+}
+
WinWindow::WinWindow(int width, int height, int scale)
: m_hwnd(nullptr)
, m_hcursor(nullptr)
@@ -74,6 +85,8 @@ WinWindow::WinWindow(int width, int height, int scale)
, m_lastPointerId(0)
, m_ictx(nullptr)
, m_ignoreRandomMouseEvents(0)
+ // True by default, we prefer to interpret one finger as mouse movement
+ , m_touch(new Touch)
#if SHE_USE_POINTER_API_FOR_MOUSE
, m_emulateDoubleClick(false)
, m_doubleClickMsecs(GetDoubleClickTime())
@@ -472,6 +485,19 @@ void WinWindow::setTranslateDeadKeys(bool state)
}
}
+void WinWindow::setInterpretOneFingerGestureAsMouseMovement(bool state)
+{
+ if (state) {
+ if (!m_touch)
+ m_touch = new Touch;
+ }
+ else if (m_touch) {
+ killTouchTimer();
+ delete m_touch;
+ m_touch = nullptr;
+ }
+}
+
bool WinWindow::setCursor(HCURSOR hcursor, bool custom)
{
SetCursor(hcursor);
@@ -795,8 +821,21 @@ LRESULT WinWindow::wndProc(UINT msg, WPARAM wparam, LPARAM lparam)
if (pi.pointerType == PT_TOUCH || pi.pointerType == PT_PEN) {
auto& winApi = system()->winApi();
- if (m_ictx && winApi.AddPointerInteractionContext)
+ if (m_ictx && winApi.AddPointerInteractionContext) {
winApi.AddPointerInteractionContext(m_ictx, pi.pointerId);
+
+ if (m_touch && pi.pointerType == PT_TOUCH &&
+ !m_touch->asMouse) {
+ ++m_touch->fingers;
+ TOUCH_TRACE("POINTERENTER fingers=%d\n", m_touch->fingers);
+ if (m_touch->fingers == 1) {
+ waitTimerToConvertFingerAsMouseMovement();
+ }
+ else if (m_touch->canBeMouse && m_touch->fingers >= 2) {
+ delegateFingerToInteractionContext();
+ }
+ }
+ }
}
if (!m_hasMouse) {
@@ -817,12 +856,33 @@ LRESULT WinWindow::wndProc(UINT msg, WPARAM wparam, LPARAM lparam)
break;
MOUSE_TRACE("POINTERLEAVE id=%d\n", pi.pointerId);
- m_ignoreRandomMouseEvents = 0;
+
+ // After releasing a finger a WM_MOUSEMOVE event in the trackpad
+ // position is generated, we'll ignore that message.
+ if (m_touch)
+ m_ignoreRandomMouseEvents = 1;
+ else
+ m_ignoreRandomMouseEvents = 0;
if (pi.pointerType == PT_TOUCH || pi.pointerType == PT_PEN) {
auto& winApi = system()->winApi();
- if (m_ictx && winApi.RemovePointerInteractionContext)
+ if (m_ictx && winApi.RemovePointerInteractionContext) {
winApi.RemovePointerInteractionContext(m_ictx, pi.pointerId);
+
+ if (m_touch && pi.pointerType == PT_TOUCH) {
+ if (m_touch->fingers > 0)
+ --m_touch->fingers;
+ TOUCH_TRACE("POINTERLEAVE fingers=%d\n", m_touch->fingers);
+ if (m_touch->fingers == 0) {
+ if (m_touch->canBeMouse)
+ sendDelayedTouchEvents();
+ else
+ clearDelayedTouchEvents();
+ killTouchTimer();
+ m_touch->asMouse = false;
+ }
+ }
+ }
}
#if 0 // Don't generate MouseLeave from pen/touch messages
@@ -851,7 +911,7 @@ LRESULT WinWindow::wndProc(UINT msg, WPARAM wparam, LPARAM lparam)
auto& winApi = system()->winApi();
if (m_ictx && winApi.ProcessPointerFramesInteractionContext) {
winApi.ProcessPointerFramesInteractionContext(m_ictx, 1, 1, &pi);
- if (pi.pointerType == PT_TOUCH)
+ if (!m_touch && pi.pointerType == PT_TOUCH)
return 0;
}
}
@@ -874,7 +934,7 @@ LRESULT WinWindow::wndProc(UINT msg, WPARAM wparam, LPARAM lparam)
auto& winApi = system()->winApi();
if (m_ictx && winApi.ProcessPointerFramesInteractionContext) {
winApi.ProcessPointerFramesInteractionContext(m_ictx, 1, 1, &pi);
- if (pi.pointerType == PT_TOUCH)
+ if (!m_touch && pi.pointerType == PT_TOUCH)
return 0;
}
}
@@ -901,7 +961,7 @@ LRESULT WinWindow::wndProc(UINT msg, WPARAM wparam, LPARAM lparam)
auto& winApi = system()->winApi();
if (m_ictx && winApi.ProcessPointerFramesInteractionContext) {
winApi.ProcessPointerFramesInteractionContext(m_ictx, 1, 1, &pi);
- if (pi.pointerType == PT_TOUCH)
+ if (!m_touch && pi.pointerType == PT_TOUCH)
return 0;
}
}
@@ -916,7 +976,22 @@ LRESULT WinWindow::wndProc(UINT msg, WPARAM wparam, LPARAM lparam)
}
ev.setType(Event::MouseMove);
- queueEvent(ev);
+
+ if (m_touch && pi.pointerType == PT_TOUCH) {
+ TOUCH_TRACE("POINTERUPDATE canBeMouse=%d asMouse=%d\n",
+ m_touch->canBeMouse,
+ m_touch->asMouse);
+ if (!m_touch->asMouse) {
+ if (m_touch->canBeMouse)
+ m_touch->delayedEvents.push_back(ev);
+ else
+ return 0;
+ }
+ else
+ queueEvent(ev);
+ }
+ else
+ queueEvent(ev);
handlePointerButtonChange(ev, pi);
@@ -956,6 +1031,25 @@ LRESULT WinWindow::wndProc(UINT msg, WPARAM wparam, LPARAM lparam)
return 0;
}
+ case WM_TIMER:
+ TOUCH_TRACE("TIMER %d\n", wparam);
+ if (m_touch && m_touch->timerID == wparam) {
+ killTouchTimer();
+
+ if (!m_touch->asMouse &&
+ m_touch->canBeMouse &&
+ m_touch->fingers == 1) {
+ TOUCH_TRACE("-> finger as mouse, sent %d events\n",
+ m_touch->delayedEvents.size());
+
+ convertFingerAsMouseMovement();
+ }
+ else {
+ delegateFingerToInteractionContext();
+ }
+ }
+ break;
+
// Keyboard Messages
case WM_SYSKEYDOWN:
@@ -1272,6 +1366,20 @@ void WinWindow::handlePointerButtonChange(Event& ev, POINTER_INFO& pi)
}
#endif
+ if (m_touch && pi.pointerType == PT_TOUCH) {
+ if (!m_touch->asMouse) {
+ if (m_touch->canBeMouse) {
+ // TODO Review why the ui layer needs a Event::MouseMove event
+ // before ButtonDown/Up events.
+ Event evMouseMove = ev;
+ evMouseMove.setType(Event::MouseMove);
+ m_touch->delayedEvents.push_back(evMouseMove);
+ m_touch->delayedEvents.push_back(ev);
+ }
+ return;
+ }
+ }
+
queueEvent(ev);
}
@@ -1288,11 +1396,15 @@ void WinWindow::handleInteractionContextOutput(
output->interactionFlags,
output->inputType);
- // We use the InteractionContext to interpret touch gestures only and double tap with pen.
- if ((output->inputType == PT_TOUCH) ||
- (output->inputType == PT_PEN &&
- output->interactionId == INTERACTION_ID_TAP &&
- output->arguments.tap.count == 2)) {
+ // We use the InteractionContext to interpret touch gestures only
+ // and double tap with pen.
+ if ((output->inputType == PT_TOUCH
+ && (!m_touch
+ || (!m_touch->asMouse && !m_touch->canBeMouse)
+ || (output->arguments.tap.count == 2)))
+ || (output->inputType == PT_PEN &&
+ output->interactionId == INTERACTION_ID_TAP &&
+ output->arguments.tap.count == 2)) {
ABS_CLIENT_RC(rc);
gfx::Point pos(int((output->x - rc.left) / m_scale),
@@ -1362,6 +1474,57 @@ void WinWindow::handleInteractionContextOutput(
}
}
+void WinWindow::waitTimerToConvertFingerAsMouseMovement()
+{
+ ASSERT(m_touch);
+ m_touch->canBeMouse = true;
+ clearDelayedTouchEvents();
+ SetTimer(m_hwnd, m_touch->timerID = 1,
+ kFingerAsMouseTimeout, nullptr);
+ TOUCH_TRACE(" - Set timer\n");
+}
+
+void WinWindow::convertFingerAsMouseMovement()
+{
+ ASSERT(m_touch);
+ m_touch->asMouse = true;
+ sendDelayedTouchEvents();
+}
+
+void WinWindow::delegateFingerToInteractionContext()
+{
+ ASSERT(m_touch);
+ m_touch->canBeMouse = false;
+ m_touch->asMouse = false;
+ clearDelayedTouchEvents();
+ if (m_touch->timerID > 0)
+ killTouchTimer();
+}
+
+void WinWindow::sendDelayedTouchEvents()
+{
+ ASSERT(m_touch);
+ for (auto& ev : m_touch->delayedEvents)
+ queueEvent(ev);
+ clearDelayedTouchEvents();
+}
+
+void WinWindow::clearDelayedTouchEvents()
+{
+ ASSERT(m_touch);
+ m_touch->delayedEvents.clear();
+}
+
+void WinWindow::killTouchTimer()
+{
+ ASSERT(m_touch);
+ if (m_touch->timerID > 0) {
+ KillTimer(m_hwnd, m_touch->timerID);
+ m_touch->timerID = 0;
+ TOUCH_TRACE(" - Kill timer\n");
+ }
+}
+
//static
void WinWindow::registerClass()
{
diff --git a/src/she/win/window.h b/src/she/win/window.h
index be125f1d4..380b1313d 100644
--- a/src/she/win/window.h
+++ b/src/she/win/window.h
@@ -52,6 +52,7 @@ namespace she {
void setLayout(const std::string& layout);
void setTranslateDeadKeys(bool state);
+ void setInterpretOneFingerGestureAsMouseMovement(bool state);
HWND handle() { return m_hwnd; }
@@ -64,6 +65,13 @@ namespace she {
void handleInteractionContextOutput(
const INTERACTION_CONTEXT_OUTPUT* output);
+ void waitTimerToConvertFingerAsMouseMovement();
+ void convertFingerAsMouseMovement();
+ void delegateFingerToInteractionContext();
+ void sendDelayedTouchEvents();
+ void clearDelayedTouchEvents();
+ void killTouchTimer();
+
virtual void onQueueEvent(Event& ev) { }
virtual void onResize(const gfx::Size& sz) { }
virtual void onPaint(HDC hdc) { }
@@ -127,6 +135,36 @@ namespace she {
// messages again.
int m_ignoreRandomMouseEvents;
+ // Variables used to convert one finger in mouse-like movement,
+ // and two/more fingers in scroll movement/pan/zoom. The idea
+ // is as follows:
+ // 1) When a PT_TOUCH is received, we count this event in
+ // m_fingers and setup a timer (m_fingerTimerID) to wait for
+ // another touch event.
+ // 2) If another touch event is received, we process the messages
+ // with the interaction context (m_ictx) to handle special
+ // gestures (pan, magnify, etc.).
+ // 3) If the timeout is reached, and we've received only one
+ // finger on the windows, we can send all awaiting events
+ // (m_fingerEvents) like mouse movement messages/button
+ // presses/releases.
+ struct Touch {
+ int fingers; // Number of fingers in the window
+ // True when the timeout wasn't reached yet and the finger can be
+ // converted to mouse events yet.
+ bool canBeMouse;
+ // True if we're already processing finger/touch events as mouse
+ // movement events.
+ bool asMouse;
+ // Timeout (WM_TIMER) when the finger is converted to mouse events.
+ UINT_PTR timerID;
+ // Queued events to be sent when the finger is converted to mouse
+ // events (these events are discarded if another finger is
+ // introduced in the gesture e.g. to pan)
+ std::vector delayedEvents;
+ Touch();
+ } *m_touch;
+
#if SHE_USE_POINTER_API_FOR_MOUSE
// Emulate double-click with pointer API. I guess that this should
// be done by the Interaction Context API but it looks like