From 47dcb8f032b5042b5fe621ad83d6a8e4d9137221 Mon Sep 17 00:00:00 2001 From: Yuhang Zhao <2546789017@qq.com> Date: Wed, 14 Sep 2022 17:07:06 +0800 Subject: [PATCH] quick: emulate Windows behavior more, just like widget Signed-off-by: Yuhang Zhao <2546789017@qq.com> --- .../Quick/framelessquickhelper.h | 1 + .../Quick/private/framelessquickhelper_p.h | 1 + .../Quick/private/quickstandardtitlebar_p.h | 5 + src/quick/framelessquickhelper.cpp | 38 ++++++ src/quick/quickstandardtitlebar.cpp | 114 +++++++++++++++++- src/widgets/standardtitlebar.cpp | 7 ++ 6 files changed, 164 insertions(+), 2 deletions(-) diff --git a/include/FramelessHelper/Quick/framelessquickhelper.h b/include/FramelessHelper/Quick/framelessquickhelper.h index 87ad9a5..f3c562c 100644 --- a/include/FramelessHelper/Quick/framelessquickhelper.h +++ b/include/FramelessHelper/Quick/framelessquickhelper.h @@ -66,6 +66,7 @@ public Q_SLOTS: void setTitleBarItem(QQuickItem *value); void setSystemButton(QQuickItem *item, const QuickGlobal::SystemButtonType buttonType); void setHitTestVisible(QQuickItem *item, const bool visible = true); + void setHitTestVisible(const QRect &rect, const bool visible = true); void showSystemMenu(const QPoint &pos); void windowStartSystemMove2(const QPoint &pos); diff --git a/include/FramelessHelper/Quick/private/framelessquickhelper_p.h b/include/FramelessHelper/Quick/private/framelessquickhelper_p.h index 117588a..566bfa7 100644 --- a/include/FramelessHelper/Quick/private/framelessquickhelper_p.h +++ b/include/FramelessHelper/Quick/private/framelessquickhelper_p.h @@ -56,6 +56,7 @@ public: void attachToWindow(); void setSystemButton(QQuickItem *item, const QuickGlobal::SystemButtonType buttonType); void setHitTestVisible(QQuickItem *item, const bool visible = true); + void setHitTestVisible(const QRect &rect, const bool visible = true); void showSystemMenu(const QPoint &pos); void windowStartSystemMove2(const QPoint &pos); void windowStartSystemResize2(const Qt::Edges edges, const QPoint &pos); diff --git a/include/FramelessHelper/Quick/private/quickstandardtitlebar_p.h b/include/FramelessHelper/Quick/private/quickstandardtitlebar_p.h index 4845761..98147a6 100644 --- a/include/FramelessHelper/Quick/private/quickstandardtitlebar_p.h +++ b/include/FramelessHelper/Quick/private/quickstandardtitlebar_p.h @@ -113,6 +113,10 @@ Q_SIGNALS: private: void initialize(); void updateAll(); + void mouseEventHandler(const QMouseEvent *event); + Q_NODISCARD QRect windowIconRect() const; + Q_NODISCARD bool isInTitleBarIconArea(const QPoint &pos) const; + Q_NODISCARD bool windowIconVisible_real() const; private: Qt::Alignment m_labelAlignment = {}; @@ -128,6 +132,7 @@ private: bool m_extended = false; bool m_hideWhenClose = false; QScopedPointer m_chromePalette; + bool m_closeTriggered = false; }; FRAMELESSHELPER_END_NAMESPACE diff --git a/src/quick/framelessquickhelper.cpp b/src/quick/framelessquickhelper.cpp index 6271825..1dc8991 100644 --- a/src/quick/framelessquickhelper.cpp +++ b/src/quick/framelessquickhelper.cpp @@ -61,6 +61,7 @@ struct QuickHelperData QPointer minimizeButton = nullptr; QPointer maximizeButton = nullptr; QPointer closeButton = nullptr; + QList hitTestVisibleRects = {}; }; struct QuickHelper @@ -279,6 +280,26 @@ void FramelessQuickHelperPrivate::setHitTestVisible(QQuickItem *item, const bool } } +void FramelessQuickHelperPrivate::setHitTestVisible(const QRect &rect, const bool visible) +{ + Q_ASSERT(rect.isValid()); + if (!rect.isValid()) { + return; + } + const QMutexLocker locker(&g_quickHelper()->mutex); + QuickHelperData *data = getWindowDataMutable(); + if (!data) { + return; + } + const bool exists = data->hitTestVisibleRects.contains(rect); + if (visible && !exists) { + data->hitTestVisibleRects.append(rect); + } + if (!visible && exists) { + data->hitTestVisibleRects.removeAll(rect); + } +} + void FramelessQuickHelperPrivate::showSystemMenu(const QPoint &pos) { #ifdef Q_OS_WINDOWS @@ -620,6 +641,13 @@ bool FramelessQuickHelperPrivate::isInTitleBarDraggableArea(const QPoint &pos) c } } } + if (!data.hitTestVisibleRects.isEmpty()) { + for (auto &&rect : qAsConst(data.hitTestVisibleRects)) { + if (rect.isValid()) { + region -= rect; + } + } + } return region.contains(pos); } @@ -856,6 +884,16 @@ void FramelessQuickHelper::setHitTestVisible(QQuickItem *item, const bool visibl d->setHitTestVisible(item, visible); } +void FramelessQuickHelper::setHitTestVisible(const QRect &rect, const bool visible) +{ + Q_ASSERT(rect.isValid()); + if (!rect.isValid()) { + return; + } + Q_D(FramelessQuickHelper); + d->setHitTestVisible(rect, visible); +} + void FramelessQuickHelper::showSystemMenu(const QPoint &pos) { Q_D(FramelessQuickHelper); diff --git a/src/quick/quickstandardtitlebar.cpp b/src/quick/quickstandardtitlebar.cpp index 1ecad5a..71c0ae0 100644 --- a/src/quick/quickstandardtitlebar.cpp +++ b/src/quick/quickstandardtitlebar.cpp @@ -25,8 +25,11 @@ #include "quickstandardtitlebar_p.h" #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) #include "quickimageitem.h" +#include "framelessquickhelper.h" #include "quickstandardsystembutton_p.h" #include "framelessquickwindow_p.h" +#include +#include #include #include #include @@ -181,7 +184,6 @@ void QuickStandardTitleBar::setWindowIconSize(const QSizeF &value) m_windowIcon->setWidth(value.width()); m_windowIcon->setHeight(value.height()); #endif - Q_EMIT windowIconSizeChanged(); } bool QuickStandardTitleBar::windowIconVisible() const @@ -201,6 +203,7 @@ void QuickStandardTitleBar::setWindowIconVisible(const bool value) } else { labelAnchors->setLeft(QQuickItemPrivate::get(this)->left()); } + FramelessQuickHelper::get(this)->setHitTestVisible(windowIconRect(), windowIconVisible_real()); } QVariant QuickStandardTitleBar::windowIcon() const @@ -354,6 +357,93 @@ void QuickStandardTitleBar::updateWindowIcon() m_windowIcon->setSource(icon); } +void QuickStandardTitleBar::mouseEventHandler(const QMouseEvent *event) +{ + Q_ASSERT(event); + if (!event) { + return; + } + const Qt::MouseButton button = event->button(); +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + const QPoint scenePos = event->scenePosition().toPoint(); +#else + const QPoint scenePos = event->windowPos().toPoint(); +#endif + const bool interestArea = isInTitleBarIconArea(scenePos); + switch (event->type()) { + case QEvent::MouseButtonRelease: + if (interestArea) { + // Sadly the mouse release events are always triggered before the + // mouse double click events, and if we intercept the mouse release + // events here, we'll never get the double click events afterwards, + // so we have to wait for a little bit to make sure the double + // click events are handled first, before we actually handle the + // mouse release events here. + // We need to wait long enough because the time interval between these + // events is really really short, if the delay time is not long enough, + // we still can't trigger the double click event due to we have handled + // the mouse release events here already. But we also can't wait too + // long, otherwise the system menu will show up too late, which is not + // a good user experience. In my experiments, I found that 150ms is + // the minimum value we can use here. + // We need a copy of the "scenePos" variable here, otherwise it will + // soon fall out of scope when the lambda function actually runs. + QTimer::singleShot(150, this, [this, button, scenePos](){ + // The close event is already triggered, don't try to show the + // system menu anymore, otherwise it will prevent our window + // from closing. + if (m_closeTriggered) { + return; + } + FramelessQuickHelper::get(this)->showSystemMenu([this, button, &scenePos]() -> QPoint { + if (button == Qt::LeftButton) { + return {0, qRound(height())}; + } + return scenePos; + }()); + }); + } + break; + case QEvent::MouseButtonDblClick: + if (QQuickWindow * const w = window()) { + if ((button == Qt::LeftButton) && interestArea) { + m_closeTriggered = true; + w->close(); + } + } + break; + default: + break; + } +} + +QRect QuickStandardTitleBar::windowIconRect() const +{ +#if 0 + const QSizeF size = windowIconSize(); + const qreal y = ((height() - size.height()) / qreal(2)); + return QRectF(QPointF(kDefaultTitleBarContentsMargin, y), size).toRect(); +#else + return QRectF(QPointF(m_windowIcon->x(), m_windowIcon->y()), windowIconSize()).toRect(); +#endif +} + +bool QuickStandardTitleBar::windowIconVisible_real() const +{ + if (m_windowIcon.isNull() || !m_windowIcon->isVisible() || !m_windowIcon->source().isValid()) { + return false; + } + return true; +} + +bool QuickStandardTitleBar::isInTitleBarIconArea(const QPoint &pos) const +{ + if (!windowIconVisible_real()) { + return false; + } + return windowIconRect().contains(pos); +} + void QuickStandardTitleBar::initialize() { setSmooth(true); @@ -380,6 +470,9 @@ void QuickStandardTitleBar::initialize() iconAnchors->setVerticalCenter(thisPriv->verticalCenter()); connect(m_windowIcon.data(), &QuickImageItem::visibleChanged, this, &QuickStandardTitleBar::windowIconVisibleChanged); connect(m_windowIcon.data(), &QuickImageItem::sourceChanged, this, &QuickStandardTitleBar::windowIconChanged); + // ### TODO: QuickImageItem::sizeChanged() + connect(m_windowIcon.data(), &QuickImageItem::widthChanged, this, &QuickStandardTitleBar::windowIconSizeChanged); + connect(m_windowIcon.data(), &QuickImageItem::heightChanged, this, &QuickStandardTitleBar::windowIconSizeChanged); m_windowTitleLabel.reset(new QQuickLabel(this)); QFont f = m_windowTitleLabel->font(); @@ -428,13 +521,30 @@ void QuickStandardTitleBar::itemChange(const ItemChange change, const ItemChange m_windowTitleChangeConnection = connect(value.window, &QQuickWindow::windowTitleChanged, this, &QuickStandardTitleBar::updateTitleLabelText); updateAll(); value.window->installEventFilter(this); + FramelessQuickHelper::get(this)->setHitTestVisible(windowIconRect(), windowIconVisible_real()); } } bool QuickStandardTitleBar::eventFilter(QObject *object, QEvent *event) { - if (event && (event->type() == QEvent::LanguageChange)) { + Q_ASSERT(object); + Q_ASSERT(event); + if (!object || !event) { + return false; + } + if (!object->isWindowType()) { + return QQuickRectangle::eventFilter(object, event); + } + const auto w = qobject_cast(object); + if (w != window()) { + return QQuickRectangle::eventFilter(object, event); + } + const QEvent::Type type = event->type(); + if (type == QEvent::LanguageChange) { retranslateUi(); + } else if ((type >= QEvent::MouseButtonPress) && (type <= QEvent::MouseMove)) { + const auto mouseEvent = static_cast(event); + mouseEventHandler(mouseEvent); } return QQuickRectangle::eventFilter(object, event); } diff --git a/src/widgets/standardtitlebar.cpp b/src/widgets/standardtitlebar.cpp index 70fe903..c4216b1 100644 --- a/src/widgets/standardtitlebar.cpp +++ b/src/widgets/standardtitlebar.cpp @@ -278,6 +278,13 @@ void StandardTitleBarPrivate::mouseEventHandler(const QMouseEvent *event) // so we have to wait for a little bit to make sure the double // click events are handled first, before we actually handle the // mouse release events here. + // We need to wait long enough because the time interval between these + // events is really really short, if the delay time is not long enough, + // we still can't trigger the double click event due to we have handled + // the mouse release events here already. But we also can't wait too + // long, otherwise the system menu will show up too late, which is not + // a good user experience. In my experiments, I found that 150ms is + // the minimum value we can use here. // We need a copy of the "scenePos" variable here, otherwise it will // soon fall out of scope when the lambda function actually runs. QTimer::singleShot(150, this, [this, button, q, scenePos](){