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.
This commit is contained in:
David Capello 2025-02-10 22:58:03 -03:00
parent 6c69840184
commit d5738fb492
10 changed files with 162 additions and 84 deletions

2
laf

@ -1 +1 @@
Subproject commit 339a0fa13584853bda8559de486e715e743a5763 Subproject commit 65829107c838817987f3cf6374cc68c583e5d538

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (c) 2024 Igara Studio S.A. // Copyright (c) 2024-2025 Igara Studio S.A.
// //
// This program is distributed under the terms of // This program is distributed under the terms of
// the End-User License Agreement for Aseprite. // the End-User License Agreement for Aseprite.
@ -51,18 +51,12 @@ bool FontEntry::FontFace::onProcessMessage(Message* msg)
MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg); MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
const gfx::Point screenPos = mouseMsg->display()->nativeWindow()->pointToScreen( const gfx::Point screenPos = mouseMsg->display()->nativeWindow()->pointToScreen(
mouseMsg->position()); mouseMsg->position());
Widget* pick = manager()->pickFromScreenPos(screenPos); Manager* mgr = manager();
Widget* pick = mgr->pickFromScreenPos(screenPos);
Widget* target = m_popup->getListBox(); Widget* target = m_popup->getListBox();
if (pick && (pick == target || pick->hasAncestor(target))) { if (pick && (pick == target || pick->hasAncestor(target))) {
releaseMouse(); mgr->transferAsMouseDownMessage(this, pick, mouseMsg);
MouseMessage mouseMsg2(kMouseDownMessage,
*mouseMsg,
mouseMsg->positionForDisplay(pick->display()));
mouseMsg2.setRecipient(pick);
mouseMsg2.setDisplay(pick->display());
pick->sendMessage(&mouseMsg2);
return true; return true;
} }
} }

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A. // Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello // Copyright (C) 2001-2018 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -266,17 +266,10 @@ bool ToolBar::onProcessMessage(Message* msg)
// mouse over the ToolBar. // mouse over the ToolBar.
if (hasCapture()) { if (hasCapture()) {
MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg); MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
Widget* pick = manager()->pickFromScreenPos(mouseMsg->screenPosition()); Manager* mgr = manager();
Widget* pick = mgr->pickFromScreenPos(mouseMsg->screenPosition());
if (ToolStrip* strip = dynamic_cast<ToolStrip*>(pick)) { if (ToolStrip* strip = dynamic_cast<ToolStrip*>(pick)) {
releaseMouse(); mgr->transferAsMouseDownMessage(this, strip, mouseMsg);
MouseMessage* mouseMsg2 = new MouseMessage(
kMouseDownMessage,
*mouseMsg,
mouseMsg->positionForDisplay(strip->display()));
mouseMsg2->setRecipient(strip);
mouseMsg2->setDisplay(strip->display());
manager()->enqueueMessage(mouseMsg2);
} }
} }
break; break;
@ -755,16 +748,10 @@ bool ToolBar::ToolStrip::onProcessMessage(Message* msg)
if (m_hotTool) if (m_hotTool)
m_toolbar->selectTool(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<ToolBar*>(pick)) { if (ToolBar* bar = dynamic_cast<ToolBar*>(pick)) {
releaseMouse(); mgr->transferAsMouseDownMessage(this, bar, mouseMsg);
MouseMessage* mouseMsg2 = new MouseMessage(kMouseDownMessage,
*mouseMsg,
mouseMsg->positionForDisplay(pick->display()));
mouseMsg2->setRecipient(bar);
mouseMsg2->setDisplay(pick->display());
manager()->enqueueMessage(mouseMsg2);
} }
} }
break; break;

View File

@ -1,5 +1,5 @@
// Aseprite UI Library // 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 // Copyright (C) 2001-2017 David Capello
// //
// This file is released under the terms of the MIT license. // This file is released under the terms of the MIT license.
@ -503,18 +503,17 @@ bool ComboBoxEntry::onProcessMessage(Message* msg)
MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg); MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
gfx::Point screenPos = mouseMsg->display()->nativeWindow()->pointToScreen( gfx::Point screenPos = mouseMsg->display()->nativeWindow()->pointToScreen(
mouseMsg->position()); mouseMsg->position());
Widget* pick = manager()->pickFromScreenPos(screenPos); Manager* mgr = manager();
Widget* pick = mgr->pickFromScreenPos(screenPos);
Widget* listbox = m_comboBox->m_listbox; Widget* listbox = m_comboBox->m_listbox;
if (pick != nullptr && (pick == listbox || pick->hasAncestor(listbox))) { if (pick != nullptr && (pick == listbox || pick->hasAncestor(listbox))) {
releaseMouse(); mgr->transferAsMouseDownMessage(this,
pick,
MouseMessage mouseMsg2(kMouseDownMessage, mouseMsg,
*mouseMsg, // Send the message right now, if we enqueue
mouseMsg->positionForDisplay(pick->display())); // the message the popup window is closed.
mouseMsg2.setRecipient(pick); true);
mouseMsg2.setDisplay(pick->display());
pick->sendMessage(&mouseMsg2);
return true; return true;
} }
} }

View File

@ -1,5 +1,5 @@
// Aseprite UI Library // 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 // Copyright (C) 2001-2017 David Capello
// //
// This file is released under the terms of the MIT license. // This file is released under the terms of the MIT license.
@ -89,17 +89,11 @@ bool IntEntry::onProcessMessage(Message* msg)
case kMouseMoveMessage: case kMouseMoveMessage:
if (hasCapture()) { if (hasCapture()) {
MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg); MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
Widget* pick = manager()->pickFromScreenPos( Manager* mgr = manager();
Widget* pick = mgr->pickFromScreenPos(
display()->nativeWindow()->pointToScreen(mouseMsg->position())); display()->nativeWindow()->pointToScreen(mouseMsg->position()));
if (pick == m_slider.get()) { if (pick == m_slider.get()) {
releaseMouse(); mgr->transferAsMouseDownMessage(this, pick, mouseMsg);
MouseMessage mouseMsg2(kMouseDownMessage,
*mouseMsg,
mouseMsg->positionForDisplay(pick->display()));
mouseMsg2.setRecipient(pick);
mouseMsg2.setDisplay(pick->display());
pick->sendMessage(&mouseMsg2);
} }
} }
break; break;

View File

@ -1,5 +1,5 @@
// Aseprite UI Library // 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 // Copyright (C) 2001-2018 David Capello
// //
// This file is released under the terms of the MIT license. // This file is released under the terms of the MIT license.
@ -13,6 +13,7 @@
// #define DEBUG_PAINT_MESSAGES 1 // #define DEBUG_PAINT_MESSAGES 1
// #define LIMIT_DISPATCH_TIME 1 // #define LIMIT_DISPATCH_TIME 1
#define GARBAGE_TRACE(...) // TRACE(__VA_ARGS__) #define GARBAGE_TRACE(...) // TRACE(__VA_ARGS__)
#define CAPTURE_TRACE(...) // TRACE(__VA_ARGS__)
#ifdef HAVE_CONFIG_H #ifdef HAVE_CONFIG_H
#include "config.h" #include "config.h"
@ -21,6 +22,8 @@
#include "ui/manager.h" #include "ui/manager.h"
#include "base/concurrent_queue.h" #include "base/concurrent_queue.h"
#include "base/contains.h"
#include "base/remove_from_container.h"
#include "base/scoped_value.h" #include "base/scoped_value.h"
#include "base/thread.h" #include "base/thread.h"
#include "base/time.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) bool Manager::widgetAssociatedToManager(Widget* widget)
{ {
return (focus_widget == widget || mouse_widget == widget || capture_widget == widget || return (focus_widget == widget || mouse_widget == widget || capture_widget == widget ||
std::find(mouse_widgets_list.begin(), mouse_widgets_list.end(), widget) != base::contains(mouse_widgets_list, widget));
mouse_widgets_list.end());
} }
Manager::Manager(const os::WindowRef& nativeWindow) Manager::Manager(const os::WindowRef& nativeWindow)
@ -889,12 +891,12 @@ void Manager::enqueueMessage(Message* msg)
concurrent_msg_queue.push(msg); concurrent_msg_queue.push(msg);
} }
Window* Manager::getTopWindow() Window* Manager::getTopWindow() const
{ {
return static_cast<Window*>(UI_FIRST_WIDGET(children())); return static_cast<Window*>(UI_FIRST_WIDGET(children()));
} }
Window* Manager::getDesktopWindow() Window* Manager::getDesktopWindow() const
{ {
for (auto child : children()) { for (auto child : children()) {
Window* window = static_cast<Window*>(child); Window* window = static_cast<Window*>(child);
@ -904,7 +906,7 @@ Window* Manager::getDesktopWindow()
return nullptr; return nullptr;
} }
Window* Manager::getForegroundWindow() Window* Manager::getForegroundWindow() const
{ {
for (auto child : children()) { for (auto child : children()) {
Window* window = static_cast<Window*>(child); Window* window = static_cast<Window*>(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 // To set the capture, we set first the mouse_widget (because
// mouse_widget shouldn't be != capture_widget) // mouse_widget shouldn't be != capture_widget)
setMouse(widget); setMouse(widget);
@ -1048,6 +1075,13 @@ void Manager::setCapture(Widget* widget)
display->nativeWindow()->captureMouse(); 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 // Sets the focus to the "magnetic" widget inside the window
void Manager::attractFocus(Widget* widget) void Manager::attractFocus(Widget* widget)
{ {
@ -1082,6 +1116,8 @@ void Manager::freeMouse()
void Manager::freeCapture() void Manager::freeCapture()
{ {
if (capture_widget) { if (capture_widget) {
CAPTURE_TRACE("Manager::freeCapture() %s\n", typeid(*capture_widget).name());
Display* display = capture_widget->display(); Display* display = capture_widget->display();
capture_widget->disableFlags(HAS_CAPTURE); capture_widget->disableFlags(HAS_CAPTURE);
@ -1113,9 +1149,7 @@ void Manager::freeWidget(Widget* widget)
if (widget->hasMouse() || (widget == mouse_widget)) if (widget->hasMouse() || (widget == mouse_widget))
freeMouse(); freeMouse();
auto it = std::find(mouse_widgets_list.begin(), mouse_widgets_list.end(), widget); base::remove_from_container(mouse_widgets_list, widget);
if (it != mouse_widgets_list.end())
mouse_widgets_list.erase(it);
ASSERT(!Manager::widgetAssociatedToManager(widget)); ASSERT(!Manager::widgetAssociatedToManager(widget));
} }
@ -1279,6 +1313,53 @@ Widget* Manager::pickFromScreenPos(const gfx::Point& screenPos) const
return Widget::pickFromScreenPos(screenPos); 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<MouseMessage>(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<Window*>(child);
if (widgetWindow == window)
return true;
if (window->isForeground() || window->isDesktop())
break;
}
return false;
}
void Manager::_closingAppWithException() void Manager::_closingAppWithException()
{ {
redrawState = RedrawState::ClosingApp; redrawState = RedrawState::ClosingApp;

View File

@ -1,5 +1,5 @@
// Aseprite UI Library // 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 // Copyright (C) 2001-2017 David Capello
// //
// This file is released under the terms of the MIT license. // This file is released under the terms of the MIT license.
@ -72,9 +72,9 @@ public:
void addToGarbage(Widget* widget); void addToGarbage(Widget* widget);
void collectGarbage(); void collectGarbage();
Window* getTopWindow(); Window* getTopWindow() const;
Window* getDesktopWindow(); Window* getDesktopWindow() const;
Window* getForegroundWindow(); Window* getForegroundWindow() const;
Display* getForegroundDisplay(); Display* getForegroundDisplay();
Widget* getFocus(); Widget* getFocus();
@ -83,7 +83,7 @@ public:
void setFocus(Widget* widget); void setFocus(Widget* widget);
void setMouse(Widget* widget); void setMouse(Widget* widget);
void setCapture(Widget* widget); void setCapture(Widget* widget, bool force = false);
void attractFocus(Widget* widget); void attractFocus(Widget* widget);
void focusFirstChild(Widget* widget); void focusFirstChild(Widget* widget);
void freeFocus(); void freeFocus();
@ -107,6 +107,30 @@ public:
Widget* pickFromScreenPos(const gfx::Point& screenPos) const override; 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 _openWindow(Window* window, bool center);
void _closeWindow(Window* window, bool redraw_background); void _closeWindow(Window* window, bool redraw_background);
void _runModalWindow(Window* window); void _runModalWindow(Window* window);
@ -164,6 +188,7 @@ private:
const double magnification); const double magnification);
bool handleWindowZOrder(); bool handleWindowZOrder();
void updateMouseWidgets(const gfx::Point& mousePos, Display* display); void updateMouseWidgets(const gfx::Point& mousePos, Display* display);
void allowCapture(Widget* widget);
int pumpQueue(); int pumpQueue();
bool sendMessageToWidget(Message* msg, Widget* widget); bool sendMessageToWidget(Message* msg, Widget* widget);

View File

@ -1,5 +1,5 @@
// Aseprite UI Library // 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 // Copyright (C) 2001-2017 David Capello
// //
// This file is released under the terms of the MIT license. // 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 // Restore the mouse capture if it changed, which means that a
// scroll bar (when it was temporarily removed) lost the capture. // scroll bar (when it was temporarily removed) lost the capture.
if (man && man->getCapture() != mouseCapture && mouseCapture->isVisible()) if (man && man->getCapture() != mouseCapture && mouseCapture->isVisible())
man->setCapture(mouseCapture); man->setCapture(mouseCapture, true); // Force the capture
} }
Viewport* View::viewport() Viewport* View::viewport()

View File

@ -1,5 +1,5 @@
// Aseprite UI Library // 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 // Copyright (C) 2001-2018 David Capello
// //
// This file is released under the terms of the MIT license. // 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()) { if (hasCapture()) {
const gfx::Point screenPos = mouseMsg->display()->nativeWindow()->pointToScreen( const gfx::Point screenPos = mouseMsg->display()->nativeWindow()->pointToScreen(
mouseMsg->position()); mouseMsg->position());
auto man = manager(); Manager* mgr = manager();
Widget* pick = (man ? man->pickFromScreenPos(screenPos) : nullptr); Widget* pick = (mgr ? mgr->pickFromScreenPos(screenPos) : nullptr);
if (pick && pick != this && pick->type() == widget_type) { if (pick && pick != this && pick->type() == widgetType) {
releaseMouse(); mgr->transferAsMouseDownMessage(this, pick, mouseMsg);
MouseMessage* mouseMsg2 = new MouseMessage(kMouseDownMessage,
*mouseMsg,
mouseMsg->positionForDisplay(pick->display()));
mouseMsg2->setDisplay(pick->display());
mouseMsg2->setRecipient(pick);
man->enqueueMessage(mouseMsg2);
return true; return true;
} }
} }

View File

@ -1,5 +1,5 @@
// Aseprite UI Library // 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 // Copyright (C) 2001-2018 David Capello
// //
// This file is released under the terms of the MIT license. // This file is released under the terms of the MIT license.
@ -336,6 +336,11 @@ public:
void requestFocus(); void requestFocus();
void releaseFocus(); 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 captureMouse();
void releaseMouse(); void releaseMouse();
@ -371,9 +376,9 @@ public:
// the widget bounds. // the widget bounds.
gfx::Point mousePosInClientBounds() const { return toClient(mousePosInDisplay()); } 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. // 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 // Returns lower-case letter that represet the mnemonic of the widget
// (the underscored character, i.e. the letter after & symbol). // (the underscored character, i.e. the letter after & symbol).