diff --git a/README.md b/README.md index 96b8e2ae..2f83d4a6 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/include/FramelessHelper/Core/utils.h b/include/FramelessHelper/Core/utils.h index d5e041d4..3e0535c5 100644 --- a/include/FramelessHelper/Core/utils.h +++ b/include/FramelessHelper/Core/utils.h @@ -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); diff --git a/src/core/utils.cpp b/src/core/utils.cpp index 7578be1a..2194555c 100644 --- a/src/core/utils.cpp +++ b/src/core/utils.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #ifndef FRAMELESSHELPER_CORE_NO_PRIVATE # include # include @@ -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(target); + const auto candidateObject = const_cast(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(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 diff --git a/src/quick/framelessquickhelper.cpp b/src/quick/framelessquickhelper.cpp index 1d404fd1..b8a2cf9c 100644 --- a/src/quick/framelessquickhelper.cpp +++ b/src/quick/framelessquickhelper.cpp @@ -36,6 +36,8 @@ #include #include #include +#include +#include #ifndef FRAMELESSHELPER_QUICK_NO_PRIVATE # if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) # include // For QWINDOWSIZE_MAX @@ -43,8 +45,6 @@ # include // For QWINDOWSIZE_MAX # endif // (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) # include -# include -# include #endif // FRAMELESSHELPER_QUICK_NO_PRIVATE #ifndef QWINDOWSIZE_MAX @@ -865,43 +865,23 @@ 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(data->windowIconButton)) { - quickButton = btn; - } - } + quickButton = data->windowIconButton; break; case QuickGlobal::SystemButtonType::Help: - if (data->contextHelpButton) { - if (const auto btn = qobject_cast(data->contextHelpButton)) { - quickButton = btn; - } - } + quickButton = data->contextHelpButton; break; case QuickGlobal::SystemButtonType::Minimize: - if (data->minimizeButton) { - if (const auto btn = qobject_cast(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(data->maximizeButton)) { - quickButton = btn; - } - } + quickButton = data->maximizeButton; break; case QuickGlobal::SystemButtonType::Close: - if (data->closeButton) { - if (const auto btn = qobject_cast(data->closeButton)) { - quickButton = btn; - } - } + quickButton = data->closeButton; break; case QuickGlobal::SystemButtonType::Unknown: Q_UNREACHABLE_RETURN(void(0)); @@ -909,31 +889,30 @@ void FramelessQuickHelperPrivate::setSystemButtonState(const QuickGlobal::System 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 diff --git a/src/widgets/framelesswidgetshelper.cpp b/src/widgets/framelesswidgetshelper.cpp index 9846b7df..ebccbddb 100644 --- a/src/widgets/framelesswidgetshelper.cpp +++ b/src/widgets/framelesswidgetshelper.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #ifndef QWIDGETSIZE_MAX @@ -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()