Return correct hit-test values for title bar buttons on Windows (#4994)

Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
nerix 2023-12-03 14:41:33 +01:00 committed by GitHub
parent 584a7c86fc
commit 812186dc4c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 381 additions and 38 deletions

View file

@ -43,6 +43,7 @@
- Bugfix: Fixed lookahead/-behind not working in _Ignores_. (#4965) - Bugfix: Fixed lookahead/-behind not working in _Ignores_. (#4965)
- Bugfix: Fixed Image Uploader accidentally deleting images with some hosts when link resolver was enabled. (#4971) - Bugfix: Fixed Image Uploader accidentally deleting images with some hosts when link resolver was enabled. (#4971)
- Bugfix: Fixed rare crash with Image Uploader when closing a split right after starting an upload. (#4971) - Bugfix: Fixed rare crash with Image Uploader when closing a split right after starting an upload. (#4971)
- Bugfix: Fixed support for Windows 11 Snap layouts. (#4994)
- Bugfix: Fixed some windows appearing between screens. (#4797) - Bugfix: Fixed some windows appearing between screens. (#4797)
- Dev: Run miniaudio in a separate thread, and simplify it to not manage the device ourselves. There's a chance the simplification is a bad idea. (#4978) - Dev: Run miniaudio in a separate thread, and simplify it to not manage the device ourselves. There's a chance the simplification is a bad idea. (#4978)
- Dev: Change clang-format from v14 to v16. (#4929) - Dev: Change clang-format from v14 to v16. (#4929)

View file

@ -626,6 +626,8 @@ set(SOURCE_FILES
widgets/helper/SignalLabel.hpp widgets/helper/SignalLabel.hpp
widgets/helper/TitlebarButton.cpp widgets/helper/TitlebarButton.cpp
widgets/helper/TitlebarButton.hpp widgets/helper/TitlebarButton.hpp
widgets/helper/TitlebarButtons.cpp
widgets/helper/TitlebarButtons.hpp
widgets/listview/GenericItemDelegate.cpp widgets/listview/GenericItemDelegate.cpp
widgets/listview/GenericItemDelegate.hpp widgets/listview/GenericItemDelegate.hpp

View file

@ -8,6 +8,7 @@
#include "util/PostToThread.hpp" #include "util/PostToThread.hpp"
#include "util/WindowsHelper.hpp" #include "util/WindowsHelper.hpp"
#include "widgets/helper/EffectLabel.hpp" #include "widgets/helper/EffectLabel.hpp"
#include "widgets/helper/TitlebarButtons.hpp"
#include "widgets/Label.hpp" #include "widgets/Label.hpp"
#include "widgets/TooltipWidget.hpp" #include "widgets/TooltipWidget.hpp"
#include "widgets/Window.hpp" #include "widgets/Window.hpp"
@ -180,9 +181,8 @@ void BaseWindow::init()
this->close(); this->close();
}); });
this->ui_.minButton = _minButton; this->ui_.titlebarButtons = new TitleBarButtons(
this->ui_.maxButton = _maxButton; this, _minButton, _maxButton, _exitButton);
this->ui_.exitButton = _exitButton;
this->ui_.buttons.push_back(_minButton); this->ui_.buttons.push_back(_minButton);
this->ui_.buttons.push_back(_maxButton); this->ui_.buttons.push_back(_maxButton);
@ -474,12 +474,9 @@ void BaseWindow::changeEvent(QEvent *)
} }
#ifdef USEWINSDK #ifdef USEWINSDK
if (this->ui_.maxButton) if (this->ui_.titlebarButtons)
{ {
this->ui_.maxButton->setButtonStyle( this->ui_.titlebarButtons->updateMaxButton();
this->windowState() & Qt::WindowMaximized
? TitleBarButtonStyle::Unmaximize
: TitleBarButtonStyle::Maximize);
} }
if (this->isVisible() && this->hasCustomWindowFrame()) if (this->isVisible() && this->hasCustomWindowFrame())
@ -585,6 +582,11 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message,
bool returnValue = false; bool returnValue = false;
auto isHoveringTitlebarButton = [&]() {
auto ht = msg->wParam;
return ht == HTMAXBUTTON || ht == HTMINBUTTON || ht == HTCLOSE;
};
switch (msg->message) switch (msg->message)
{ {
case WM_DPICHANGED: case WM_DPICHANGED:
@ -612,6 +614,91 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message,
returnValue = this->handleNCHITTEST(msg, result); returnValue = this->handleNCHITTEST(msg, result);
break; break;
case WM_NCMOUSEHOVER:
case WM_NCMOUSEMOVE: {
// WM_NCMOUSEMOVE/WM_NCMOUSEHOVER gets sent when the mouse is
// moving/hovering in the non-client area
// - (mostly) the edges and the titlebar.
// We only need to handle the event for the titlebar buttons,
// as Qt doesn't create mouse events for these events.
if (!this->ui_.titlebarButtons)
{
// we don't consume the event if we don't have custom buttons
break;
}
if (isHoveringTitlebarButton())
{
*result = 0;
returnValue = true;
long x = GET_X_LPARAM(msg->lParam);
long y = GET_Y_LPARAM(msg->lParam);
RECT winrect;
GetWindowRect(HWND(winId()), &winrect);
QPoint globalPos(x, y);
this->ui_.titlebarButtons->hover(msg->wParam, globalPos);
this->lastEventWasNcMouseMove_ = true;
}
else
{
this->ui_.titlebarButtons->leave();
}
}
break;
case WM_MOUSEMOVE: {
if (!this->lastEventWasNcMouseMove_)
{
break;
}
this->lastEventWasNcMouseMove_ = false;
// Windows doesn't send WM_NCMOUSELEAVE in some cases,
// so the buttons show as hovered even though they're not hovered.
[[fallthrough]];
}
case WM_NCMOUSELEAVE: {
// WM_NCMOUSELEAVE gets sent when the mouse leaves any
// non-client area. In case we have titlebar buttons,
// we want to ensure they're deselected.
if (this->ui_.titlebarButtons)
{
this->ui_.titlebarButtons->leave();
}
}
break;
case WM_NCLBUTTONDOWN:
case WM_NCLBUTTONUP: {
// WM_NCLBUTTON{DOWN, UP} gets called when the left mouse button
// was pressed in a non-client area.
// We simulate a mouse down/up event for the titlebar buttons
// as Qt doesn't create an event in that case.
if (!this->ui_.titlebarButtons || !isHoveringTitlebarButton())
{
break;
}
returnValue = true;
*result = 0;
auto ht = msg->wParam;
long x = GET_X_LPARAM(msg->lParam);
long y = GET_Y_LPARAM(msg->lParam);
RECT winrect;
GetWindowRect(HWND(winId()), &winrect);
QPoint globalPos(x, y);
if (msg->message == WM_NCLBUTTONDOWN)
{
this->ui_.titlebarButtons->mousePress(ht, globalPos);
}
else
{
this->ui_.titlebarButtons->mouseRelease(ht, globalPos);
}
}
break;
default: default:
return QWidget::nativeEvent(eventType, message, result); return QWidget::nativeEvent(eventType, message, result);
} }
@ -668,29 +755,21 @@ void BaseWindow::calcButtonsSizes()
return; return;
} }
if (this->frameless_) if (this->frameless_ || !this->ui_.titlebarButtons)
{ {
return; return;
} }
if ((this->width() / this->scale()) < 300) #ifdef USEWINSDK
if ((static_cast<float>(this->width()) / this->scale()) < 300)
{ {
if (this->ui_.minButton) this->ui_.titlebarButtons->setSmallSize();
this->ui_.minButton->setScaleIndependantSize(30, 30);
if (this->ui_.maxButton)
this->ui_.maxButton->setScaleIndependantSize(30, 30);
if (this->ui_.exitButton)
this->ui_.exitButton->setScaleIndependantSize(30, 30);
} }
else else
{ {
if (this->ui_.minButton) this->ui_.titlebarButtons->setRegularSize();
this->ui_.minButton->setScaleIndependantSize(46, 30);
if (this->ui_.maxButton)
this->ui_.maxButton->setScaleIndependantSize(46, 30);
if (this->ui_.exitButton)
this->ui_.exitButton->setScaleIndependantSize(46, 30);
} }
#endif
} }
void BaseWindow::drawCustomWindowFrame(QPainter &painter) void BaseWindow::drawCustomWindowFrame(QPainter &painter)
@ -943,32 +1022,55 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result)
if (*result == 0) if (*result == 0)
{ {
bool client = false;
// Check the main layout first, as it's the largest area // Check the main layout first, as it's the largest area
if (this->ui_.layoutBase->geometry().contains(point)) if (this->ui_.layoutBase->geometry().contains(point))
{ {
client = true; *result = HTCLIENT;
} }
// Check the titlebar buttons // Check the titlebar buttons
if (!client && this->ui_.titlebarBox->geometry().contains(point)) if (*result == 0 &&
this->ui_.titlebarBox->geometry().contains(point))
{ {
for (QWidget *widget : this->ui_.buttons) for (const auto *widget : this->ui_.buttons)
{ {
if (widget->isVisible() && if (!widget->isVisible() ||
widget->geometry().contains(point)) !widget->geometry().contains(point))
{ {
client = true; continue;
} }
if (const auto *btn =
dynamic_cast<const TitleBarButton *>(widget))
{
switch (btn->getButtonStyle())
{
case TitleBarButtonStyle::Minimize: {
*result = HTMINBUTTON;
break;
}
case TitleBarButtonStyle::Unmaximize:
case TitleBarButtonStyle::Maximize: {
*result = HTMAXBUTTON;
break;
}
case TitleBarButtonStyle::Close: {
*result = HTCLOSE;
break;
}
default: {
*result = HTCLIENT;
break;
}
}
break;
}
*result = HTCLIENT;
break;
} }
} }
if (client) if (*result == 0)
{
*result = HTCLIENT;
}
else
{ {
*result = HTCAPTION; *result = HTCAPTION;
} }

View file

@ -18,6 +18,7 @@ namespace chatterino {
class Button; class Button;
class EffectLabel; class EffectLabel;
class TitleBarButton; class TitleBarButton;
class TitleBarButtons;
enum class TitleBarButtonStyle; enum class TitleBarButtonStyle;
class BaseWindow : public BaseWidget class BaseWindow : public BaseWidget
@ -135,9 +136,7 @@ private:
QLayout *windowLayout = nullptr; QLayout *windowLayout = nullptr;
QHBoxLayout *titlebarBox = nullptr; QHBoxLayout *titlebarBox = nullptr;
QWidget *titleLabel = nullptr; QWidget *titleLabel = nullptr;
TitleBarButton *minButton = nullptr; TitleBarButtons *titlebarButtons = nullptr;
TitleBarButton *maxButton = nullptr;
TitleBarButton *exitButton = nullptr;
QWidget *layoutBase = nullptr; QWidget *layoutBase = nullptr;
std::vector<Button *> buttons; std::vector<Button *> buttons;
} ui_; } ui_;
@ -148,6 +147,7 @@ private:
QRect nextBounds_; QRect nextBounds_;
QTimer useNextBounds_; QTimer useNextBounds_;
bool isNotMinimizedOrMaximized_{}; bool isNotMinimizedOrMaximized_{};
bool lastEventWasNcMouseMove_ = false;
#endif #endif
pajlada::Signals::SignalHolder connections_; pajlada::Signals::SignalHolder connections_;

View file

@ -124,4 +124,39 @@ void TitleBarButton::paintEvent(QPaintEvent *event)
this->paintButton(painter); this->paintButton(painter);
} }
void TitleBarButton::ncEnter()
{
this->enterEvent(nullptr);
this->update();
}
void TitleBarButton::ncLeave()
{
this->leaveEvent(nullptr);
this->update();
}
void TitleBarButton::ncMove(QPoint at)
{
QMouseEvent evt(QMouseEvent::MouseMove, at, Qt::NoButton, Qt::NoButton,
Qt::NoModifier);
this->mouseMoveEvent(&evt);
}
void TitleBarButton::ncMousePress(QPoint at)
{
QMouseEvent evt(QMouseEvent::MouseButtonPress, at, Qt::LeftButton,
Qt::NoButton, Qt::NoModifier);
this->mousePressEvent(&evt);
this->update();
}
void TitleBarButton::ncMouseRelease(QPoint at)
{
QMouseEvent evt(QMouseEvent::MouseButtonRelease, at, Qt::LeftButton,
Qt::NoButton, Qt::NoModifier);
this->mouseReleaseEvent(&evt);
this->update();
}
} // namespace chatterino } // namespace chatterino

View file

@ -23,6 +23,24 @@ public:
TitleBarButtonStyle getButtonStyle() const; TitleBarButtonStyle getButtonStyle() const;
void setButtonStyle(TitleBarButtonStyle style_); void setButtonStyle(TitleBarButtonStyle style_);
/// Simulate a `mouseEnter` event.
void ncEnter();
/// Simulate a `mouseLeave` event.
void ncLeave();
/// Simulate a `mouseMove` event.
/// @param at a local position relative to this widget
void ncMove(QPoint at);
/// Simulate a `mousePress` event with the left mouse button.
/// @param at a local position relative to this widget
void ncMousePress(QPoint at);
/// Simulate a `mouseRelease` event with the left mouse button.
/// @param at a local position relative to this widget
void ncMouseRelease(QPoint at);
protected: protected:
void paintEvent(QPaintEvent *) override; void paintEvent(QPaintEvent *) override;

View file

@ -0,0 +1,116 @@
#include "widgets/helper/TitlebarButtons.hpp"
#ifdef USEWINSDK
# include "widgets/helper/TitlebarButton.hpp"
# include <Windows.h>
# include <cassert>
namespace chatterino {
TitleBarButtons::TitleBarButtons(QWidget *window, TitleBarButton *minButton,
TitleBarButton *maxButton,
TitleBarButton *closeButton)
: QObject(window)
, window_(window)
, minButton_(minButton)
, maxButton_(maxButton)
, closeButton_(closeButton)
{
}
void TitleBarButtons::hover(size_t ht, QPoint at)
{
TitleBarButton *hovered{};
TitleBarButton *other1{};
TitleBarButton *other2{};
switch (ht)
{
case HTMAXBUTTON:
hovered = this->maxButton_;
other1 = this->minButton_;
other2 = this->closeButton_;
break;
case HTMINBUTTON:
hovered = this->minButton_;
other1 = this->maxButton_;
other2 = this->closeButton_;
break;
case HTCLOSE:
hovered = this->closeButton_;
other1 = this->minButton_;
other2 = this->maxButton_;
break;
default:
assert(false && "TitleBarButtons::hover precondition violated");
return;
}
hovered->ncEnter();
hovered->ncMove(hovered->mapFromGlobal(at));
other1->ncLeave();
other2->ncLeave();
}
void TitleBarButtons::leave()
{
this->minButton_->ncLeave();
this->maxButton_->ncLeave();
this->closeButton_->ncLeave();
}
void TitleBarButtons::mousePress(size_t ht, QPoint at)
{
auto *button = this->buttonForHt(ht);
button->ncMousePress(button->mapFromGlobal(at));
}
void TitleBarButtons::mouseRelease(size_t ht, QPoint at)
{
auto *button = this->buttonForHt(ht);
button->ncMouseRelease(button->mapFromGlobal(at));
}
void TitleBarButtons::updateMaxButton()
{
this->maxButton_->setButtonStyle(
this->window_->windowState().testFlag(Qt::WindowMaximized)
? TitleBarButtonStyle::Unmaximize
: TitleBarButtonStyle::Maximize);
}
void TitleBarButtons::setSmallSize()
{
this->minButton_->setScaleIndependantSize(30, 30);
this->maxButton_->setScaleIndependantSize(30, 30);
this->closeButton_->setScaleIndependantSize(30, 30);
}
void TitleBarButtons::setRegularSize()
{
this->minButton_->setScaleIndependantSize(46, 30);
this->maxButton_->setScaleIndependantSize(46, 30);
this->closeButton_->setScaleIndependantSize(46, 30);
}
TitleBarButton *TitleBarButtons::buttonForHt(size_t ht) const
{
switch (ht)
{
case HTMAXBUTTON:
return this->maxButton_;
case HTMINBUTTON:
return this->minButton_;
case HTCLOSE:
return this->closeButton_;
default:
assert(false &&
"TitleBarButtons::buttonForHt precondition violated");
return nullptr;
}
}
} // namespace chatterino
#endif

View file

@ -0,0 +1,69 @@
#pragma once
class QPoint;
class QWidget;
#include <QtGlobal>
namespace chatterino {
#ifdef USEWINSDK
class TitleBarButton;
class TitleBarButtons : QObject
{
public:
/// The parent of this object is set to `window`.
///
/// All parameters must have a parent;
/// they're not deleted in the destructor.
TitleBarButtons(QWidget *window, TitleBarButton *minButton,
TitleBarButton *maxButton, TitleBarButton *closeButton);
/// Hover over the button `ht` at the global position `at`.
///
/// @pre `ht` must be one of { HTMAXBUTTON, HTMINBUTTON, HTCLOSE }.
/// @param ht The hovered button
/// @param at The global position of the event
void hover(size_t ht, QPoint at);
/// Leave all buttons - simulate `leaveEvent` for all buttons.
void leave();
/// Press the left mouse over the button `ht` at the global position `at`.
///
/// @pre `ht` must be one of { HTMAXBUTTON, HTMINBUTTON, HTCLOSE }.
/// @param ht The clicked button
/// @param at The global position of the event
void mousePress(size_t ht, QPoint at);
/// Release the left mouse button over the button `ht` at the
/// global position `at`.
///
/// @pre `ht` must be one of { HTMAXBUTTON, HTMINBUTTON, HTCLOSE }.
/// @param ht The clicked button
/// @param at The global position of the event
void mouseRelease(size_t ht, QPoint at);
/// Update the maximize/restore button to show the correct image
/// according to the current window state.
void updateMaxButton();
/// Set buttons to be narrow.
void setSmallSize();
/// Set buttons to be regular size.
void setRegularSize();
private:
/// @pre ht must be one of { HTMAXBUTTON, HTMINBUTTON, HTCLOSE }.
TitleBarButton *buttonForHt(size_t ht) const;
QWidget *window_ = nullptr;
TitleBarButton *minButton_ = nullptr;
TitleBarButton *maxButton_ = nullptr;
TitleBarButton *closeButton_ = nullptr;
};
#endif
} // namespace chatterino