Skip to content

Commit

Permalink
win: use Qt events to set system button state
Browse files Browse the repository at this point in the history
  • Loading branch information
wangwenx190 committed Aug 17, 2023
1 parent c85c6d6 commit 9265735
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 81 deletions.
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,11 +383,9 @@ Please refer to the demo projects to see more detailed usages: [examples](./exam
- Due to there are many sub-versions of Windows 10, it's highly recommended to use the latest version of Windows 10, at least **no older than Windows 10 1809**. If you try to use this framework on some very old Windows 10 versions such as 1507 or 1607, there may be some compatibility issues. Using this framework on Windows 7 is also supported but not recommended. To get the most stable behavior and the best appearance, you should use it on the latest version of Windows 10 or Windows 11.
- To make the snap layout work as expected, there are some additional rules for your homemade system buttons to follow:
- **Add a manifest file to your application. In the manifest file, you need to claim your application supports Windows 11 explicitly. This step is VERY VERY IMPORTANT. Without this step, the snap layout feature can't be enabled.**
- Make sure there are two public invokable functions (slot functions are always invokable): `void setHovered(bool)` and `void setPressed(bool)`. These two functions will be invoked by FramelessHelper when the button is being hovered or pressed. You should change the button's visual state inside these functions. If you need to show tooltips, you'll have to do it manually in these functions.
- Make sure there's a public signal: `void clicked()`. When the button is being clicked, that signal will be triggered by FramelessHelper. You should connect your event handler to that signal.
- For Qt Quick applications, for the C++ side, you need to inherit your button from the `QQuickAbstractButton` class, for the QML side, you need to inherit your button from the `Button` type (from the `QtQuick.Controls.Basic` module). They have all the invokable functions and signals we need, so no more extra work is needed.
- Don't forget to call `setSystemButton()` for each button to let FramelessHelper know which is the minimize/maximize/close button.
- System buttons will not be able to receive any actual mouse and keyboard events so there's no need to handle these events inside these buttons. That's also why we need to set the button's visual state manually.
- Call `setSystemButton()` for each button to let FramelessHelper know which is the minimize/maximize/close button.
- System buttons will not be able to receive any keyboard events so there's no need to handle these events inside these buttons.
- The mouse events of the system buttons are all emulated by FramelessHelper, they are not sent by the OS, so don't trust them too much.
- I know this is making everything complicated but unfortunately we can't avoid this mess if we need to support the snap layout feature. Snap layout is really only designed for the original standard window frame, so if we want to forcely support it without a standard window frame, many black magic will be needed.

### Linux
Expand Down
3 changes: 3 additions & 0 deletions include/FramelessHelper/Core/utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ FRAMELESSHELPER_CORE_API void registerThemeChangeNotification();
[[nodiscard]] FRAMELESSHELPER_CORE_API quint32 defaultScreenDpi();
[[nodiscard]] FRAMELESSHELPER_CORE_API bool isWindowAccelerated(const QWindow *window);
[[nodiscard]] FRAMELESSHELPER_CORE_API bool isWindowTransparent(const QWindow *window);
FRAMELESSHELPER_CORE_API void emulateQtMouseEvent(
const QObject *target, const QWindow *window, const Global::ButtonState buttonState,
const QPoint &globalPos, const QPoint &scenePos, const QPoint &localPos, const bool underMouse);

#ifdef Q_OS_WINDOWS
[[nodiscard]] FRAMELESSHELPER_CORE_API bool isWindowsVersionOrGreater(const Global::WindowsVersion version);
Expand Down
51 changes: 51 additions & 0 deletions src/core/utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
#include <QtGui/qpalette.h>
#include <QtGui/qsurface.h>
#include <QtGui/qsurfaceformat.h>
#include <QtGui/qevent.h>
#ifndef FRAMELESSHELPER_CORE_NO_PRIVATE
# include <QtGui/private/qhighdpiscaling_p.h>
# include <QtGui/private/qwindow_p.h>
Expand Down Expand Up @@ -630,4 +631,54 @@ bool Utils::isWindowTransparent(const QWindow *window)
return window->format().hasAlpha();
}

void Utils::emulateQtMouseEvent(const QObject *target, const QWindow *window, const ButtonState buttonState,
const QPoint &globalPos, const QPoint &scenePos, const QPoint &localPos, const bool underMouse)
{
Q_ASSERT(target);
Q_ASSERT(window);
if (!target || !window) {
return;
}
const auto object = const_cast<QObject *>(target);
const auto candidateObject = const_cast<QWindow *>(window);
static constexpr const QPoint oldPos = {}; // Not needed.
static constexpr const Qt::MouseButton button = Qt::LeftButton;
const Qt::MouseButtons buttons = QGuiApplication::mouseButtons();
const Qt::KeyboardModifiers modifiers = QGuiApplication::keyboardModifiers();
switch (buttonState) {
case ButtonState::Normal: {
QEvent leaveEvent(QEvent::Leave);
QHoverEvent hoverLeaveEvent(QEvent::HoverLeave, scenePos, globalPos, oldPos, modifiers);
QCoreApplication::sendEvent(object, &leaveEvent);
QCoreApplication::sendEvent(object, &hoverLeaveEvent);
} break;
case ButtonState::Hovered: {
const auto receiver = (target->isWidgetType() ? object : static_cast<QObject *>(candidateObject));
if (underMouse) {
QMouseEvent mouseMoveEvent(QEvent::MouseMove, localPos, scenePos, globalPos, Qt::NoButton, buttons, modifiers);
QHoverEvent hoverMoveEvent(QEvent::HoverMove, scenePos, globalPos, oldPos, modifiers);
QCoreApplication::sendEvent(receiver, &mouseMoveEvent);
QCoreApplication::sendEvent(receiver, &hoverMoveEvent);
} else {
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
QEnterEvent enterEvent(localPos, scenePos, globalPos);
#else
QEvent enterEvent(QEvent::Enter);
#endif
QHoverEvent hoverEnterEvent(QEvent::HoverEnter, scenePos, globalPos, oldPos, modifiers);
QCoreApplication::sendEvent(receiver, &enterEvent);
QCoreApplication::sendEvent(receiver, &hoverEnterEvent);
}
} break;
case ButtonState::Pressed: {
QMouseEvent event(QEvent::MouseButtonPress, localPos, scenePos, globalPos, button, buttons, modifiers);
QCoreApplication::sendEvent(object, &event);
} break;
case ButtonState::Released: {
QMouseEvent event(QEvent::MouseButtonRelease, localPos, scenePos, globalPos, button, buttons, modifiers);
QCoreApplication::sendEvent(object, &event);
} break;
}
}

FRAMELESSHELPER_END_NAMESPACE
75 changes: 27 additions & 48 deletions src/quick/framelessquickhelper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@
#include <QtCore/qtimer.h>
#include <QtCore/qeventloop.h>
#include <QtCore/qloggingcategory.h>
#include <QtGui/qcursor.h>
#include <QtGui/qguiapplication.h>
#ifndef FRAMELESSHELPER_QUICK_NO_PRIVATE
# if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
# include <QtGui/qpa/qplatformwindow.h> // For QWINDOWSIZE_MAX
# else // (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
# include <QtGui/private/qwindow_p.h> // For QWINDOWSIZE_MAX
# endif // (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
# include <QtQuick/private/qquickitem_p.h>
# include <QtQuickTemplates2/private/qquickabstractbutton_p.h>
# include <QtQuickTemplates2/private/qquickabstractbutton_p_p.h>
#endif // FRAMELESSHELPER_QUICK_NO_PRIVATE

#ifndef QWINDOWSIZE_MAX
Expand Down Expand Up @@ -865,75 +865,54 @@ void FramelessQuickHelperPrivate::setSystemButtonState(const QuickGlobal::System
if (!data) {
return;
}
QQuickAbstractButton *quickButton = nullptr;
QQuickItem *quickButton = nullptr;
switch (button) {
case QuickGlobal::SystemButtonType::WindowIcon:
if (data->windowIconButton) {
if (const auto btn = qobject_cast<QQuickAbstractButton *>(data->windowIconButton)) {
quickButton = btn;
}
}
quickButton = data->windowIconButton;
break;
case QuickGlobal::SystemButtonType::Help:
if (data->contextHelpButton) {
if (const auto btn = qobject_cast<QQuickAbstractButton *>(data->contextHelpButton)) {
quickButton = btn;
}
}
quickButton = data->contextHelpButton;
break;
case QuickGlobal::SystemButtonType::Minimize:
if (data->minimizeButton) {
if (const auto btn = qobject_cast<QQuickAbstractButton *>(data->minimizeButton)) {
quickButton = btn;
}
}
quickButton = data->minimizeButton;
break;
case QuickGlobal::SystemButtonType::Maximize:
case QuickGlobal::SystemButtonType::Restore:
if (data->maximizeButton) {
if (const auto btn = qobject_cast<QQuickAbstractButton *>(data->maximizeButton)) {
quickButton = btn;
}
}
quickButton = data->maximizeButton;
break;
case QuickGlobal::SystemButtonType::Close:
if (data->closeButton) {
if (const auto btn = qobject_cast<QQuickAbstractButton *>(data->closeButton)) {
quickButton = btn;
}
}
quickButton = data->closeButton;
break;
case QuickGlobal::SystemButtonType::Unknown:
Q_UNREACHABLE_RETURN(void(0));
}
if (!quickButton) {
return;
}
const auto updateButtonState = [state](QQuickAbstractButton *btn) -> void {
const auto updateButtonState = [state](QQuickItem *btn) -> void {
Q_ASSERT(btn);
if (!btn) {
return;
}
switch (state) {
case QuickGlobal::ButtonState::Normal: {
btn->setPressed(false);
btn->setHovered(false);
} break;
case QuickGlobal::ButtonState::Hovered: {
btn->setPressed(false);
btn->setHovered(true);
} break;
case QuickGlobal::ButtonState::Pressed: {
btn->setHovered(true);
btn->setPressed(true);
} break;
case QuickGlobal::ButtonState::Released: {
// Clicked: pressed --> released, so behave like hovered.
btn->setPressed(false);
btn->setHovered(true);
QQuickAbstractButtonPrivate::get(btn)->click();
} break;
const QQuickWindow *window = btn->window();
Q_ASSERT(window);
if (!window) {
return;
}
const QScreen *screen = (window->screen() ? window->screen() : QGuiApplication::primaryScreen());
const QPoint globalPos = (screen ? QCursor::pos(screen) : QCursor::pos());
const QPoint localPos = btn->mapFromGlobal(globalPos).toPoint();
const QPoint scenePos = window->mapFromGlobal(globalPos);
const auto underMouse = [btn, &globalPos]() -> bool {
const QPointF originPoint = btn->mapToGlobal(QPointF{ 0, 0 });
#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
const QSizeF size = btn->size();
#else
const auto size = QSizeF{ btn->width(), btn->height() };
#endif
return QRectF{ originPoint, size }.contains(globalPos);
}();
Utils::emulateQtMouseEvent(btn, window, FRAMELESSHELPER_ENUM_QUICK_TO_CORE(ButtonState, state), globalPos, scenePos, localPos, underMouse);
};
updateButtonState(quickButton);
#endif // FRAMELESSHELPER_QUICK_NO_PRIVATE
Expand Down
43 changes: 15 additions & 28 deletions src/widgets/framelesswidgetshelper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
#include <QtCore/qloggingcategory.h>
#include <QtGui/qwindow.h>
#include <QtGui/qpalette.h>
#include <QtGui/qcursor.h>
#include <QtWidgets/qwidget.h>

#ifndef QWIDGETSIZE_MAX
Expand Down Expand Up @@ -849,36 +850,22 @@ void FramelessWidgetsHelperPrivate::setSystemButtonState(const SystemButtonType
if (!btn) {
return;
}
switch (state) {
case ButtonState::Normal: {
QMetaObject::invokeMethod(btn, "setPressed", Q_ARG(bool, false));
QMetaObject::invokeMethod(btn, "setHovered", Q_ARG(bool, false));
} break;
case ButtonState::Hovered: {
QMetaObject::invokeMethod(btn, "setPressed", Q_ARG(bool, false));
QMetaObject::invokeMethod(btn, "setHovered", Q_ARG(bool, true));
} break;
case ButtonState::Pressed: {
QMetaObject::invokeMethod(btn, "setHovered", Q_ARG(bool, true));
QMetaObject::invokeMethod(btn, "setPressed", Q_ARG(bool, true));
} break;
case ButtonState::Released: {
// Clicked: pressed --> released, so behave like hovered.
QMetaObject::invokeMethod(btn, "setPressed", Q_ARG(bool, false));
QMetaObject::invokeMethod(btn, "setHovered", Q_ARG(bool, true));
// Trigger the clicked signal.
QMetaObject::invokeMethod(btn, "clicked");
} break;
const QWidget *window = btn->window();
Q_ASSERT(window);
if (!window) {
return;
}
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
const QScreen *screen = window->screen();
#else
const QScreen *screen = QGuiApplication::primaryScreen();
#endif
const QPoint globalPos = (screen ? QCursor::pos(screen) : QCursor::pos());
const QPoint localPos = btn->mapFromGlobal(globalPos);
const QPoint scenePos = window->mapFromGlobal(globalPos);
Utils::emulateQtMouseEvent(btn, window->windowHandle(), state, globalPos, scenePos, localPos, btn->underMouse());
};
if (const auto mo = widgetButton->metaObject()) {
const int pressedIndex = mo->indexOfSlot(QMetaObject::normalizedSignature("setPressed(bool)").constData());
const int hoveredIndex = mo->indexOfSlot(QMetaObject::normalizedSignature("setHovered(bool)").constData());
const int clickedIndex = mo->indexOfSignal(QMetaObject::normalizedSignature("clicked()").constData());
if ((pressedIndex >= 0) && (hoveredIndex >= 0) && (clickedIndex >= 0)) {
updateButtonState(widgetButton);
}
}
updateButtonState(widgetButton);
}

void FramelessWidgetsHelperPrivate::moveWindowToDesktopCenter()
Expand Down

0 comments on commit 9265735

Please sign in to comment.