Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mouse forward functionality on Windows #10183

Merged
merged 7 commits into from Aug 17, 2017
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions atom/browser/api/atom_api_window.cc
Expand Up @@ -787,6 +787,10 @@ void Window::SetAppDetails(const mate::Dictionary& options) {
relaunch_command, relaunch_display_name,
window_->GetAcceleratedWidget());
}

void Window::SetForwardMouseMessages(bool forward) {
window_->SetForwardMouseMessages(forward);
}
#endif

#if defined(TOOLKIT_VIEWS)
Expand Down Expand Up @@ -1060,6 +1064,7 @@ void Window::BuildPrototype(v8::Isolate* isolate,
.SetMethod("setThumbnailClip", &Window::SetThumbnailClip)
.SetMethod("setThumbnailToolTip", &Window::SetThumbnailToolTip)
.SetMethod("setAppDetails", &Window::SetAppDetails)
.SetMethod("setForwardMouseMessages", &Window::SetForwardMouseMessages)
#endif
#if defined(TOOLKIT_VIEWS)
.SetMethod("setIcon", &Window::SetIcon)
Expand Down
1 change: 1 addition & 0 deletions atom/browser/api/atom_api_window.h
Expand Up @@ -201,6 +201,7 @@ class Window : public mate::TrackableObject<Window>,
bool SetThumbnailClip(const gfx::Rect& region);
bool SetThumbnailToolTip(const std::string& tooltip);
void SetAppDetails(const mate::Dictionary& options);
void SetForwardMouseMessages(bool forward);
#endif

#if defined(TOOLKIT_VIEWS)
Expand Down
3 changes: 3 additions & 0 deletions atom/browser/native_window.h
Expand Up @@ -152,6 +152,9 @@ class NativeWindow : public base::SupportsUserData,
virtual gfx::NativeView GetNativeView() const = 0;
virtual gfx::NativeWindow GetNativeWindow() const = 0;
virtual gfx::AcceleratedWidget GetAcceleratedWidget() const = 0;
#if defined(OS_WIN)
virtual void SetForwardMouseMessages(bool forward) = 0;
#endif

// Taskbar/Dock APIs.
enum ProgressState {
Expand Down
7 changes: 7 additions & 0 deletions atom/browser/native_window_views.cc
Expand Up @@ -1064,6 +1064,13 @@ void NativeWindowViews::SetEnabled(bool enable) {
#endif
}

#if defined(OS_WIN)
void NativeWindowViews::SetForwardMouseMessages(bool forward) {
forwarding_mouse_messages_ = forward;
SetIgnoreMouseEvents(forward);
}
#endif

void NativeWindowViews::OnWidgetActivationChanged(
views::Widget* widget, bool active) {
if (widget != window_.get())
Expand Down
12 changes: 12 additions & 0 deletions atom/browser/native_window_views.h
Expand Up @@ -7,6 +7,7 @@

#include "atom/browser/native_window.h"

#include <map>
#include <string>
#include <vector>

Expand Down Expand Up @@ -133,6 +134,7 @@ class NativeWindowViews : public NativeWindow,

#if defined(OS_WIN)
TaskbarHost& taskbar_host() { return taskbar_host_; }
void SetForwardMouseMessages(bool forward) override;
#endif

private:
Expand Down Expand Up @@ -169,6 +171,11 @@ class NativeWindowViews : public NativeWindow,
bool PreHandleMSG(
UINT message, WPARAM w_param, LPARAM l_param, LRESULT* result) override;
void HandleSizeEvent(WPARAM w_param, LPARAM l_param);
static LRESULT CALLBACK SubclassProc(
HWND hwnd, UINT msg, WPARAM w_param, LPARAM l_param, UINT_PTR subclass_id,
DWORD_PTR ref_data);
static LRESULT CALLBACK MouseHookProc(
int n_code, WPARAM w_param, LPARAM l_param);
#endif

// NativeWindow:
Expand Down Expand Up @@ -259,6 +266,11 @@ class NativeWindowViews : public NativeWindow,
// The icons of window and taskbar.
base::win::ScopedHICON window_icon_;
base::win::ScopedHICON app_icon_;

// Handles to legacy windows iterated by the mouse hook
static std::map<HWND, NativeWindowViews*> legacy_window_map_;
static HHOOK mouse_hook_;
bool forwarding_mouse_messages_ = false;
#endif

// Handles unhandled keyboard messages coming back from the renderer process.
Expand Down
90 changes: 90 additions & 0 deletions atom/browser/native_window_views_win.cc
Expand Up @@ -80,6 +80,9 @@ bool IsScreenReaderActive() {

} // namespace

std::map<HWND, NativeWindowViews*> NativeWindowViews::legacy_window_map_;
HHOOK NativeWindowViews::mouse_hook_ = NULL;

bool NativeWindowViews::ExecuteWindowsCommand(int command_id) {
std::string command = AppCommandToString(command_id);
NotifyWindowExecuteWindowsCommand(command);
Expand Down Expand Up @@ -151,6 +154,25 @@ bool NativeWindowViews::PreHandleMSG(
if (w_param) {
NotifyWindowEndSession();
}
return false;
}
case WM_PARENTNOTIFY: {
if (LOWORD(w_param) == WM_CREATE) {
// Because of reasons regarding legacy drivers and stuff, a window that
// matches the client area is created and used internally by Chromium.
// This window is subclassed in order to fix some issues when forwarding
// mouse messages; see comments in |SubclassProc|. If by any chance
// Chromium removes the legacy window in the future it may be fine to
// move the logic to this very switch statement.
HWND legacy_window = reinterpret_cast<HWND>(l_param);
SetWindowSubclass(
legacy_window, SubclassProc, 1, reinterpret_cast<DWORD_PTR>(this));
if (legacy_window_map_.size() == 0) {
mouse_hook_ = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProc, NULL, 0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would install mouse hook for every Electron apps, we should only do subclass and install the hook when win.setForwardMouseMessages is called.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I completely overlooked that. Fixed.

}
legacy_window_map_.insert({ legacy_window, this });
}
return false;
}
default:
return false;
Expand Down Expand Up @@ -207,4 +229,72 @@ void NativeWindowViews::HandleSizeEvent(WPARAM w_param, LPARAM l_param) {
}
}

LRESULT CALLBACK NativeWindowViews::SubclassProc(
HWND hwnd, UINT msg, WPARAM w_param, LPARAM l_param, UINT_PTR subclass_id,
DWORD_PTR ref_data) {
NativeWindowViews* window = reinterpret_cast<NativeWindowViews*>(ref_data);
switch (msg) {
case WM_MOUSELEAVE: {
// When input is forwarded to underlying windows, this message is posted.
// If not handled, it interferes with Chromium logic, causing for example
// mouseleave events to fire. If those events are used to exit forward
// mode, excessive flickering on for example hover items in underlying
// windows can occur due to rapidly entering and leaving forwarding mode.
// By consuming and ignoring the message, we're essentially telling
// Chromium that we have not left the window despite somebody else getting
// the messages. As to why this is catched for the legacy window and not
// the actual browser window is simply that the legacy window somehow
// makes use of these events; posting to the main window didn't work.
if (window->forwarding_mouse_messages_) {
return 0;
}
break;
}
case WM_DESTROY: {
legacy_window_map_.erase(hwnd);
if (legacy_window_map_.size() == 0) {
UnhookWindowsHookEx(mouse_hook_);
mouse_hook_ = NULL;
}
break;
}
}

return DefSubclassProc(hwnd, msg, w_param, l_param);
}

LRESULT CALLBACK NativeWindowViews::MouseHookProc(
int n_code, WPARAM w_param, LPARAM l_param) {
if (n_code < 0) {
return CallNextHookEx(NULL, n_code, w_param, l_param);
}

// Post a WM_MOUSEMOVE message for those windows whose client area contains
// the cursor and are set to forward messages since they are in a state where
// they would otherwise ignore all mouse input.
if (w_param == WM_MOUSEMOVE) {
for (auto legacy : legacy_window_map_) {
if (!legacy.second->forwarding_mouse_messages_) {
continue;
}

// At first I considered enumerating windows to check whether the cursor
// was directly above the window, but since nothing bad seems to happen
// if we post the message even if some other window occludes it I have
// just left it as is.
RECT client_rect;
GetClientRect(legacy.first, &client_rect);
POINT p = reinterpret_cast<MSLLHOOKSTRUCT*>(l_param)->pt;
ScreenToClient(legacy.first, &p);
if (PtInRect(&client_rect, p)) {
WPARAM w = 0; // No virtual keys pressed for our purposes
LPARAM l = MAKELPARAM(p.x, p.y);
PostMessage(legacy.first, WM_MOUSEMOVE, w, l);
}
}
}

return CallNextHookEx(NULL, n_code, w_param, l_param);
}

} // namespace atom
8 changes: 8 additions & 0 deletions docs/api/browser-window.md
Expand Up @@ -1356,6 +1356,14 @@ removed in future Electron releases.
**Note:** The BrowserView API is currently experimental and may change or be
removed in future Electron releases.

#### `win.setForwardMouseMessages(forward)` _Windows_
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably make this an option of the setIgnoreMouseEvents, I don't think there are other usages of this API.

#### `win.setIgnoreMouseEvents(ignore[, options])`

* `ignore` Boolean
* `options` Object  (optional)
  * `forwardMouseMessages` Boolean (optional) - ...

And I think it is worthwhile to add a simple example for it, otherwise it is still hard to know how to make use of it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally I wasn't sure if I could tamper with existing API, but an optional argument that defaults to false should not break anything and I agree that it's cleaner this way.

Examples are always good. Where would the example belong? My very simple test application is mostly just a matter of ignoring/acknowledging mouse input as I enter/leave a canvas respectively. Cleaned up it should make a good example if there is a good way to put it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Examples are always good. Where would the example belong?

You can make it a sub-chapter of Click-through window in frameless-window.md.


* `forward` Boolean

Forward mouse messages to the window below this one. This is similar to
`setIgnoreMouseEvents`, but additionally allows users to listen to events
related to mouse movement such as `mouseleave`.

[blink-feature-string]: https://cs.chromium.org/chromium/src/third_party/WebKit/Source/platform/RuntimeEnabledFeatures.json5?l=62
[page-visibility-api]: https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
[quick-look]: https://en.wikipedia.org/wiki/Quick_Look
Expand Down