diff --git a/atom/browser/api/atom_api_window.cc b/atom/browser/api/atom_api_window.cc index 34366e2db5d..ff6e206c47c 100644 --- a/atom/browser/api/atom_api_window.cc +++ b/atom/browser/api/atom_api_window.cc @@ -651,8 +651,11 @@ bool Window::IsDocumentEdited() { return window_->IsDocumentEdited(); } -void Window::SetIgnoreMouseEvents(bool ignore) { - return window_->SetIgnoreMouseEvents(ignore); +void Window::SetIgnoreMouseEvents(bool ignore, mate::Arguments* args) { + mate::Dictionary options; + bool forward = false; + args->GetNext(&options) && options.Get("forward", &forward); + return window_->SetIgnoreMouseEvents(ignore, forward); } void Window::SetContentProtection(bool enable) { diff --git a/atom/browser/api/atom_api_window.h b/atom/browser/api/atom_api_window.h index 22f7e730778..3dac635d36a 100644 --- a/atom/browser/api/atom_api_window.h +++ b/atom/browser/api/atom_api_window.h @@ -166,7 +166,7 @@ class Window : public mate::TrackableObject, std::string GetRepresentedFilename(); void SetDocumentEdited(bool edited); bool IsDocumentEdited(); - void SetIgnoreMouseEvents(bool ignore); + void SetIgnoreMouseEvents(bool ignore, mate::Arguments* args); void SetContentProtection(bool enable); void SetFocusable(bool focusable); void SetProgressBar(double progress, mate::Arguments* args); diff --git a/atom/browser/native_window.h b/atom/browser/native_window.h index dd874057895..84d36714110 100644 --- a/atom/browser/native_window.h +++ b/atom/browser/native_window.h @@ -143,7 +143,7 @@ class NativeWindow : public base::SupportsUserData, virtual std::string GetRepresentedFilename(); virtual void SetDocumentEdited(bool edited); virtual bool IsDocumentEdited(); - virtual void SetIgnoreMouseEvents(bool ignore) = 0; + virtual void SetIgnoreMouseEvents(bool ignore, bool forward) = 0; virtual void SetContentProtection(bool enable) = 0; virtual void SetFocusable(bool focusable); virtual void SetMenu(AtomMenuModel* menu); diff --git a/atom/browser/native_window_views.cc b/atom/browser/native_window_views.cc index 65ad2f57dd4..759f57e13b7 100644 --- a/atom/browser/native_window_views.cc +++ b/atom/browser/native_window_views.cc @@ -334,6 +334,11 @@ NativeWindowViews::NativeWindowViews( NativeWindowViews::~NativeWindowViews() { window_->RemoveObserver(this); + +#if defined(OS_WIN) + // Disable mouse forwarding to relinquish resources, should any be held. + SetForwardMouseMessages(false); +#endif } void NativeWindowViews::Close() { @@ -790,7 +795,7 @@ bool NativeWindowViews::HasShadow() { != wm::ShadowElevation::NONE; } -void NativeWindowViews::SetIgnoreMouseEvents(bool ignore) { +void NativeWindowViews::SetIgnoreMouseEvents(bool ignore, bool forward) { #if defined(OS_WIN) LONG ex_style = ::GetWindowLong(GetAcceleratedWidget(), GWL_EXSTYLE); if (ignore) @@ -798,6 +803,13 @@ void NativeWindowViews::SetIgnoreMouseEvents(bool ignore) { else ex_style &= ~(WS_EX_TRANSPARENT | WS_EX_LAYERED); ::SetWindowLong(GetAcceleratedWidget(), GWL_EXSTYLE, ex_style); + + // Forwarding is always disabled when not ignoring mouse messages. + if (!ignore) { + SetForwardMouseMessages(false); + } else { + SetForwardMouseMessages(forward); + } #elif defined(USE_X11) if (ignore) { XRectangle r = {0, 0, 1, 1}; diff --git a/atom/browser/native_window_views.h b/atom/browser/native_window_views.h index 5d94c2255bb..fe493918117 100644 --- a/atom/browser/native_window_views.h +++ b/atom/browser/native_window_views.h @@ -7,6 +7,7 @@ #include "atom/browser/native_window.h" +#include #include #include @@ -101,7 +102,7 @@ class NativeWindowViews : public NativeWindow, void SetBackgroundColor(const std::string& color_name) override; void SetHasShadow(bool has_shadow) override; bool HasShadow() override; - void SetIgnoreMouseEvents(bool ignore) override; + void SetIgnoreMouseEvents(bool ignore, bool forward) override; void SetContentProtection(bool enable) override; void SetFocusable(bool focusable) override; void SetMenu(AtomMenuModel* menu_model) override; @@ -169,6 +170,12 @@ 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); + void SetForwardMouseMessages(bool forward); + 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: @@ -259,6 +266,12 @@ class NativeWindowViews : public NativeWindow, // The icons of window and taskbar. base::win::ScopedHICON window_icon_; base::win::ScopedHICON app_icon_; + + // The set of windows currently forwarding mouse messages. + static std::set forwarding_windows_; + static HHOOK mouse_hook_; + bool forwarding_mouse_messages_ = false; + HWND legacy_window_ = NULL; #endif // Handles unhandled keyboard messages coming back from the renderer process. diff --git a/atom/browser/native_window_views_win.cc b/atom/browser/native_window_views_win.cc index abda0d0b026..83e7dfaa79a 100644 --- a/atom/browser/native_window_views_win.cc +++ b/atom/browser/native_window_views_win.cc @@ -80,6 +80,9 @@ bool IsScreenReaderActive() { } // namespace +std::set NativeWindowViews::forwarding_windows_; +HHOOK NativeWindowViews::mouse_hook_ = NULL; + bool NativeWindowViews::ExecuteWindowsCommand(int command_id) { std::string command = AppCommandToString(command_id); NotifyWindowExecuteWindowsCommand(command); @@ -151,6 +154,16 @@ 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 is used when forwarding mouse messages. + legacy_window_ = reinterpret_cast(l_param); + } + return false; } default: return false; @@ -207,4 +220,86 @@ void NativeWindowViews::HandleSizeEvent(WPARAM w_param, LPARAM l_param) { } } +void NativeWindowViews::SetForwardMouseMessages(bool forward) { + if (forward && !forwarding_mouse_messages_) { + forwarding_mouse_messages_ = true; + forwarding_windows_.insert(this); + + // Subclassing is used to fix some issues when forwarding mouse messages; + // see comments in |SubclassProc|. + SetWindowSubclass( + legacy_window_, SubclassProc, 1, reinterpret_cast(this)); + + if (!mouse_hook_) { + mouse_hook_ = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProc, NULL, 0); + } + } else if (!forward && forwarding_mouse_messages_) { + forwarding_mouse_messages_ = false; + forwarding_windows_.erase(this); + + RemoveWindowSubclass(legacy_window_, SubclassProc, 1); + + if (forwarding_windows_.size() == 0) { + UnhookWindowsHookEx(mouse_hook_); + mouse_hook_ = NULL; + } + } +} + +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(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; + } + } + + 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 since they are in a state where they would otherwise ignore all + // mouse input. + if (w_param == WM_MOUSEMOVE) { + for (auto window : forwarding_windows_) { + // 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(window->legacy_window_, &client_rect); + POINT p = reinterpret_cast(l_param)->pt; + ScreenToClient(window->legacy_window_, &p); + if (PtInRect(&client_rect, p)) { + WPARAM w = 0; // No virtual keys pressed for our purposes + LPARAM l = MAKELPARAM(p.x, p.y); + PostMessage(window->legacy_window_, WM_MOUSEMOVE, w, l); + } + } + } + + return CallNextHookEx(NULL, n_code, w_param, l_param); +} + } // namespace atom diff --git a/docs/api/browser-window.md b/docs/api/browser-window.md index e08c8de8c91..57d71da59cd 100644 --- a/docs/api/browser-window.md +++ b/docs/api/browser-window.md @@ -1311,9 +1311,14 @@ Returns `Boolean` - Whether the window is visible on all workspaces. **Note:** This API always returns false on Windows. -#### `win.setIgnoreMouseEvents(ignore)` +#### `win.setIgnoreMouseEvents(ignore[, options])` * `ignore` Boolean +* `options` Object (optional) + * `forward` Boolean (optional) _Windows_ - If true, forwards mouse move + messages to Chromium, enabling mouse related events such as `mouseleave`. + Only used when `ignore` is true. If `ignore` is false, forwarding is always + disabled regardless of this value. Makes the window ignore all mouse events. diff --git a/docs/api/frameless-window.md b/docs/api/frameless-window.md index 1ae02935225..b323ae948cd 100644 --- a/docs/api/frameless-window.md +++ b/docs/api/frameless-window.md @@ -103,6 +103,27 @@ let win = new BrowserWindow() win.setIgnoreMouseEvents(true) ``` +### Forwarding + +Ignoring mouse messages makes the web page oblivious to mouse movement, meaning +that mouse movement events will not be emitted. On Windows operating systems an +optional parameter can be used to forward mouse move messages to the web page, +allowing events such as `mouseleave` to be emitted: + +```javascript +let win = require('electron').remote.getCurrentWindow() +let el = document.getElementById('clickThroughElement') +el.addEventListener('mouseenter', () => { + win.setIgnoreMouseEvents(true, {forward: true}) +}) +el.addEventListener('mouseleave', () => { + win.setIgnoreMouseEvents(false) +}) +``` + +This makes the web page click-through when over `el`, and returns to normal +outside it. + ## Draggable region By default, the frameless window is non-draggable. Apps need to specify