From d5738fb492b57da36d06b69a1acce4f921789bef Mon Sep 17 00:00:00 2001 From: David Capello Date: Mon, 10 Feb 2025 22:58:03 -0300 Subject: [PATCH] Avoid capturing the mouse from widgets in background windows This avoids clicking buttons that are not in the foreground/modal window. At the same time the onBroadcastMouseMessage() functionality was expanded in such a way that added widgets to the broadcast are the only ones allowed to re-capture the mouse (even if they are not in the current foreground window). This is required so the Editor can be scrolled when we are visualizing a Filter window with preview. New functions added to simplify some code: * Manager::transferAsMouseDownMessage() * Manager::allowCapture() * base::contains() Related to #4963 and #4973. --- laf | 2 +- src/app/ui/font_entry.cpp | 14 ++---- src/app/ui/toolbar.cpp | 27 +++------- src/ui/combobox.cpp | 19 ++++--- src/ui/int_entry.cpp | 14 ++---- src/ui/manager.cpp | 101 ++++++++++++++++++++++++++++++++++---- src/ui/manager.h | 35 +++++++++++-- src/ui/view.cpp | 4 +- src/ui/widget.cpp | 19 +++---- src/ui/widget.h | 11 +++-- 10 files changed, 162 insertions(+), 84 deletions(-) diff --git a/laf b/laf index 339a0fa13..65829107c 160000 --- a/laf +++ b/laf @@ -1 +1 @@ -Subproject commit 339a0fa13584853bda8559de486e715e743a5763 +Subproject commit 65829107c838817987f3cf6374cc68c583e5d538 diff --git a/src/app/ui/font_entry.cpp b/src/app/ui/font_entry.cpp index f3a342724..9e4d91eb9 100644 --- a/src/app/ui/font_entry.cpp +++ b/src/app/ui/font_entry.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (c) 2024 Igara Studio S.A. +// Copyright (c) 2024-2025 Igara Studio S.A. // // This program is distributed under the terms of // the End-User License Agreement for Aseprite. @@ -51,18 +51,12 @@ bool FontEntry::FontFace::onProcessMessage(Message* msg) MouseMessage* mouseMsg = static_cast(msg); const gfx::Point screenPos = mouseMsg->display()->nativeWindow()->pointToScreen( mouseMsg->position()); - Widget* pick = manager()->pickFromScreenPos(screenPos); + Manager* mgr = manager(); + Widget* pick = mgr->pickFromScreenPos(screenPos); Widget* target = m_popup->getListBox(); if (pick && (pick == target || pick->hasAncestor(target))) { - releaseMouse(); - - MouseMessage mouseMsg2(kMouseDownMessage, - *mouseMsg, - mouseMsg->positionForDisplay(pick->display())); - mouseMsg2.setRecipient(pick); - mouseMsg2.setDisplay(pick->display()); - pick->sendMessage(&mouseMsg2); + mgr->transferAsMouseDownMessage(this, pick, mouseMsg); return true; } } diff --git a/src/app/ui/toolbar.cpp b/src/app/ui/toolbar.cpp index c13ecf13e..7ff3407c3 100644 --- a/src/app/ui/toolbar.cpp +++ b/src/app/ui/toolbar.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2024 Igara Studio S.A. +// Copyright (C) 2018-2025 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -266,17 +266,10 @@ bool ToolBar::onProcessMessage(Message* msg) // mouse over the ToolBar. if (hasCapture()) { MouseMessage* mouseMsg = static_cast(msg); - Widget* pick = manager()->pickFromScreenPos(mouseMsg->screenPosition()); + Manager* mgr = manager(); + Widget* pick = mgr->pickFromScreenPos(mouseMsg->screenPosition()); if (ToolStrip* strip = dynamic_cast(pick)) { - releaseMouse(); - - MouseMessage* mouseMsg2 = new MouseMessage( - kMouseDownMessage, - *mouseMsg, - mouseMsg->positionForDisplay(strip->display())); - mouseMsg2->setRecipient(strip); - mouseMsg2->setDisplay(strip->display()); - manager()->enqueueMessage(mouseMsg2); + mgr->transferAsMouseDownMessage(this, strip, mouseMsg); } } break; @@ -755,16 +748,10 @@ bool ToolBar::ToolStrip::onProcessMessage(Message* msg) if (m_hotTool) m_toolbar->selectTool(m_hotTool); - Widget* pick = manager()->pickFromScreenPos(mouseMsg->screenPosition()); + Manager* mgr = manager(); + Widget* pick = mgr->pickFromScreenPos(mouseMsg->screenPosition()); if (ToolBar* bar = dynamic_cast(pick)) { - releaseMouse(); - - MouseMessage* mouseMsg2 = new MouseMessage(kMouseDownMessage, - *mouseMsg, - mouseMsg->positionForDisplay(pick->display())); - mouseMsg2->setRecipient(bar); - mouseMsg2->setDisplay(pick->display()); - manager()->enqueueMessage(mouseMsg2); + mgr->transferAsMouseDownMessage(this, bar, mouseMsg); } } break; diff --git a/src/ui/combobox.cpp b/src/ui/combobox.cpp index 44d5266a9..e83fc40a2 100644 --- a/src/ui/combobox.cpp +++ b/src/ui/combobox.cpp @@ -1,5 +1,5 @@ // Aseprite UI Library -// Copyright (C) 2018-2023 Igara Studio S.A. +// Copyright (C) 2018-2025 Igara Studio S.A. // Copyright (C) 2001-2017 David Capello // // This file is released under the terms of the MIT license. @@ -503,18 +503,17 @@ bool ComboBoxEntry::onProcessMessage(Message* msg) MouseMessage* mouseMsg = static_cast(msg); gfx::Point screenPos = mouseMsg->display()->nativeWindow()->pointToScreen( mouseMsg->position()); - Widget* pick = manager()->pickFromScreenPos(screenPos); + Manager* mgr = manager(); + Widget* pick = mgr->pickFromScreenPos(screenPos); Widget* listbox = m_comboBox->m_listbox; if (pick != nullptr && (pick == listbox || pick->hasAncestor(listbox))) { - releaseMouse(); - - MouseMessage mouseMsg2(kMouseDownMessage, - *mouseMsg, - mouseMsg->positionForDisplay(pick->display())); - mouseMsg2.setRecipient(pick); - mouseMsg2.setDisplay(pick->display()); - pick->sendMessage(&mouseMsg2); + mgr->transferAsMouseDownMessage(this, + pick, + mouseMsg, + // Send the message right now, if we enqueue + // the message the popup window is closed. + true); return true; } } diff --git a/src/ui/int_entry.cpp b/src/ui/int_entry.cpp index 9dd994ac7..72429d412 100644 --- a/src/ui/int_entry.cpp +++ b/src/ui/int_entry.cpp @@ -1,5 +1,5 @@ // Aseprite UI Library -// Copyright (C) 2019-2022 Igara Studio S.A. +// Copyright (C) 2019-2025 Igara Studio S.A. // Copyright (C) 2001-2017 David Capello // // This file is released under the terms of the MIT license. @@ -89,17 +89,11 @@ bool IntEntry::onProcessMessage(Message* msg) case kMouseMoveMessage: if (hasCapture()) { MouseMessage* mouseMsg = static_cast(msg); - Widget* pick = manager()->pickFromScreenPos( + Manager* mgr = manager(); + Widget* pick = mgr->pickFromScreenPos( display()->nativeWindow()->pointToScreen(mouseMsg->position())); if (pick == m_slider.get()) { - releaseMouse(); - - MouseMessage mouseMsg2(kMouseDownMessage, - *mouseMsg, - mouseMsg->positionForDisplay(pick->display())); - mouseMsg2.setRecipient(pick); - mouseMsg2.setDisplay(pick->display()); - pick->sendMessage(&mouseMsg2); + mgr->transferAsMouseDownMessage(this, pick, mouseMsg); } } break; diff --git a/src/ui/manager.cpp b/src/ui/manager.cpp index 3070fbdc7..a0e9cb6ce 100644 --- a/src/ui/manager.cpp +++ b/src/ui/manager.cpp @@ -1,5 +1,5 @@ // Aseprite UI Library -// Copyright (C) 2018-2024 Igara Studio S.A. +// Copyright (C) 2018-2025 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This file is released under the terms of the MIT license. @@ -13,6 +13,7 @@ // #define DEBUG_PAINT_MESSAGES 1 // #define LIMIT_DISPATCH_TIME 1 #define GARBAGE_TRACE(...) // TRACE(__VA_ARGS__) +#define CAPTURE_TRACE(...) // TRACE(__VA_ARGS__) #ifdef HAVE_CONFIG_H #include "config.h" @@ -21,6 +22,8 @@ #include "ui/manager.h" #include "base/concurrent_queue.h" +#include "base/contains.h" +#include "base/remove_from_container.h" #include "base/scoped_value.h" #include "base/thread.h" #include "base/time.h" @@ -183,8 +186,7 @@ os::Hit handle_native_hittest(os::Window* osWindow, const gfx::Point& pos) bool Manager::widgetAssociatedToManager(Widget* widget) { return (focus_widget == widget || mouse_widget == widget || capture_widget == widget || - std::find(mouse_widgets_list.begin(), mouse_widgets_list.end(), widget) != - mouse_widgets_list.end()); + base::contains(mouse_widgets_list, widget)); } Manager::Manager(const os::WindowRef& nativeWindow) @@ -889,12 +891,12 @@ void Manager::enqueueMessage(Message* msg) concurrent_msg_queue.push(msg); } -Window* Manager::getTopWindow() +Window* Manager::getTopWindow() const { return static_cast(UI_FIRST_WIDGET(children())); } -Window* Manager::getDesktopWindow() +Window* Manager::getDesktopWindow() const { for (auto child : children()) { Window* window = static_cast(child); @@ -904,7 +906,7 @@ Window* Manager::getDesktopWindow() return nullptr; } -Window* Manager::getForegroundWindow() +Window* Manager::getForegroundWindow() const { for (auto child : children()) { Window* window = static_cast(child); @@ -1033,8 +1035,33 @@ void Manager::setMouse(Widget* widget) } } -void Manager::setCapture(Widget* widget) +void Manager::setCapture(Widget* widget, bool force) { + ASSERT(widget); + if (!widget) + return; + + CAPTURE_TRACE("Manager::setCapture %s\n", typeid(*widget).name()); + if (!force && + // The given "widget" cannot capture the mouse if it's not + // "clickable". The definition of "clickable" generally means + // that the widget is in the current foreground/modal window, or + // in the desktop window (or in any floating non-modal window). + // But it can also be in a top window, e.g. a combobox popup. + !isWidgetClickable(widget) && + // In some special cases, a widget transfers a mouse message to + // another widget which doesn't belong to the current foreground + // modal window, this is done using onBroadcastMouseMessage() + // and/or transferAsMouseDownMessage(), so here we allow capturing + // the mouse from widgets inside the "mouse_widgets_list" + // (widgets that are allowed to capture the mouse / added with + // allowCapture() function). + (widget != mouse_widget && !base::contains(mouse_widgets_list, widget))) { + CAPTURE_TRACE("-> FILTERED!\n"); + return; + } + CAPTURE_TRACE("-> OK\n"); + // To set the capture, we set first the mouse_widget (because // mouse_widget shouldn't be != capture_widget) setMouse(widget); @@ -1048,6 +1075,13 @@ void Manager::setCapture(Widget* widget) display->nativeWindow()->captureMouse(); } +void Manager::allowCapture(Widget* widget) +{ + ASSERT(widget); + if (!base::contains(mouse_widgets_list, widget)) + mouse_widgets_list.push_back(widget); +} + // Sets the focus to the "magnetic" widget inside the window void Manager::attractFocus(Widget* widget) { @@ -1082,6 +1116,8 @@ void Manager::freeMouse() void Manager::freeCapture() { if (capture_widget) { + CAPTURE_TRACE("Manager::freeCapture() %s\n", typeid(*capture_widget).name()); + Display* display = capture_widget->display(); capture_widget->disableFlags(HAS_CAPTURE); @@ -1113,9 +1149,7 @@ void Manager::freeWidget(Widget* widget) if (widget->hasMouse() || (widget == mouse_widget)) freeMouse(); - auto it = std::find(mouse_widgets_list.begin(), mouse_widgets_list.end(), widget); - if (it != mouse_widgets_list.end()) - mouse_widgets_list.erase(it); + base::remove_from_container(mouse_widgets_list, widget); ASSERT(!Manager::widgetAssociatedToManager(widget)); } @@ -1279,6 +1313,53 @@ Widget* Manager::pickFromScreenPos(const gfx::Point& screenPos) const return Widget::pickFromScreenPos(screenPos); } +void Manager::transferAsMouseDownMessage(Widget* from, + Widget* to, + const MouseMessage* mouseMsg, + const bool sendNow) +{ + ASSERT(to); + ASSERT(from); + + // Remove the capture from the "from" widget. + if (from->hasCapture()) + from->releaseMouse(); + + // Allow the "to" widget to re-capture the mouse. + allowCapture(to); + + // We enqueue a copy of the mouse message but as a kMouseDownMessage. + auto mouseMsg2 = std::make_unique(kMouseDownMessage, + *mouseMsg, + mouseMsg->positionForDisplay(to->display())); + mouseMsg2->setRecipient(to); + mouseMsg2->setDisplay(to->display()); + + if (sendNow) + to->sendMessage(mouseMsg2.get()); + else + enqueueMessage(mouseMsg2.release()); +} + +bool Manager::isWidgetClickable(const Widget* widget) const +{ + Window* widgetWindow = widget->window(); + if (!widgetWindow) + return false; + + for (auto* child : children()) { + Window* window = static_cast(child); + + if (widgetWindow == window) + return true; + + if (window->isForeground() || window->isDesktop()) + break; + } + + return false; +} + void Manager::_closingAppWithException() { redrawState = RedrawState::ClosingApp; diff --git a/src/ui/manager.h b/src/ui/manager.h index 46273eca1..b99b6fe0f 100644 --- a/src/ui/manager.h +++ b/src/ui/manager.h @@ -1,5 +1,5 @@ // Aseprite UI Library -// Copyright (C) 2018-2024 Igara Studio S.A. +// Copyright (C) 2018-2025 Igara Studio S.A. // Copyright (C) 2001-2017 David Capello // // This file is released under the terms of the MIT license. @@ -72,9 +72,9 @@ public: void addToGarbage(Widget* widget); void collectGarbage(); - Window* getTopWindow(); - Window* getDesktopWindow(); - Window* getForegroundWindow(); + Window* getTopWindow() const; + Window* getDesktopWindow() const; + Window* getForegroundWindow() const; Display* getForegroundDisplay(); Widget* getFocus(); @@ -83,7 +83,7 @@ public: void setFocus(Widget* widget); void setMouse(Widget* widget); - void setCapture(Widget* widget); + void setCapture(Widget* widget, bool force = false); void attractFocus(Widget* widget); void focusFirstChild(Widget* widget); void freeFocus(); @@ -107,6 +107,30 @@ public: Widget* pickFromScreenPos(const gfx::Point& screenPos) const override; + // Transfers the given MouseMessage received "from" (generally a + // kMouseMoveMessage) to the given "to" widget, sending a copy of + // the message but changing its type to kMouseDownMessage. By + // default it enqueues the message. + // + // If "from" has the mouse capture, it will be released as it is + // highly probable that the "to" widget will recapture the mouse + // again. + // + // This is used in cases were the user presses the mouse button on + // one widget, and then drags the mouse to another widget. With this + // we can transfer the mouse capture between widgets (from -> to) + // simulating a kMouseDownMessage for the new widget "to". + void transferAsMouseDownMessage(Widget* from, + Widget* to, + const MouseMessage* mouseMsg, + bool sendNow = false); + + // Returns true if the widget is accessible with the mouse, i.e. the + // widget is in the current foreground window (or a top window above + // the foreground, e.g. a combobox popup), or there is no foreground + // window and the widget is in the desktop window. + bool isWidgetClickable(const Widget* widget) const; + void _openWindow(Window* window, bool center); void _closeWindow(Window* window, bool redraw_background); void _runModalWindow(Window* window); @@ -164,6 +188,7 @@ private: const double magnification); bool handleWindowZOrder(); void updateMouseWidgets(const gfx::Point& mousePos, Display* display); + void allowCapture(Widget* widget); int pumpQueue(); bool sendMessageToWidget(Message* msg, Widget* widget); diff --git a/src/ui/view.cpp b/src/ui/view.cpp index c70e8400f..2b53710b2 100644 --- a/src/ui/view.cpp +++ b/src/ui/view.cpp @@ -1,5 +1,5 @@ // Aseprite UI Library -// Copyright (C) 2018-2024 Igara Studio S.A. +// Copyright (C) 2018-2025 Igara Studio S.A. // Copyright (C) 2001-2017 David Capello // // This file is released under the terms of the MIT license. @@ -186,7 +186,7 @@ void View::updateView(const bool restoreScrollPos) // Restore the mouse capture if it changed, which means that a // scroll bar (when it was temporarily removed) lost the capture. if (man && man->getCapture() != mouseCapture && mouseCapture->isVisible()) - man->setCapture(mouseCapture); + man->setCapture(mouseCapture, true); // Force the capture } Viewport* View::viewport() diff --git a/src/ui/widget.cpp b/src/ui/widget.cpp index 4cc1474f9..6efaedda5 100644 --- a/src/ui/widget.cpp +++ b/src/ui/widget.cpp @@ -1,5 +1,5 @@ // Aseprite UI Library -// Copyright (C) 2018-2024 Igara Studio S.A. +// Copyright (C) 2018-2025 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This file is released under the terms of the MIT license. @@ -1507,22 +1507,15 @@ void Widget::releaseMouse() } } -bool Widget::offerCapture(ui::MouseMessage* mouseMsg, int widget_type) +bool Widget::offerCapture(ui::MouseMessage* mouseMsg, const WidgetType widgetType) { if (hasCapture()) { const gfx::Point screenPos = mouseMsg->display()->nativeWindow()->pointToScreen( mouseMsg->position()); - auto man = manager(); - Widget* pick = (man ? man->pickFromScreenPos(screenPos) : nullptr); - if (pick && pick != this && pick->type() == widget_type) { - releaseMouse(); - - MouseMessage* mouseMsg2 = new MouseMessage(kMouseDownMessage, - *mouseMsg, - mouseMsg->positionForDisplay(pick->display())); - mouseMsg2->setDisplay(pick->display()); - mouseMsg2->setRecipient(pick); - man->enqueueMessage(mouseMsg2); + Manager* mgr = manager(); + Widget* pick = (mgr ? mgr->pickFromScreenPos(screenPos) : nullptr); + if (pick && pick != this && pick->type() == widgetType) { + mgr->transferAsMouseDownMessage(this, pick, mouseMsg); return true; } } diff --git a/src/ui/widget.h b/src/ui/widget.h index c40335bd6..e1104d964 100644 --- a/src/ui/widget.h +++ b/src/ui/widget.h @@ -1,5 +1,5 @@ // Aseprite UI Library -// Copyright (C) 2018-2024 Igara Studio S.A. +// Copyright (C) 2018-2025 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This file is released under the terms of the MIT license. @@ -336,6 +336,11 @@ public: void requestFocus(); void releaseFocus(); + + // Captures the mouse to continue receiving its messages until we + // release the capture. Useful for widgets with painting-like + // capabilities, where we want to keep track of the mouse until the + // user releases the mouse button, or drag-and-drop behaviors. void captureMouse(); void releaseMouse(); @@ -371,9 +376,9 @@ public: // the widget bounds. gfx::Point mousePosInClientBounds() const { return toClient(mousePosInDisplay()); } - // Offer the capture to widgets of the given type. Returns true if + // Offers the capture to widgets of the given type. Returns true if // the capture was passed to other widget. - bool offerCapture(MouseMessage* mouseMsg, int widget_type); + bool offerCapture(MouseMessage* mouseMsg, WidgetType widgetType); // Returns lower-case letter that represet the mnemonic of the widget // (the underscored character, i.e. the letter after & symbol).