From 053a47bdaf40d80d92457a02ae9aae1285316731 Mon Sep 17 00:00:00 2001 From: yeshanshan Date: Fri, 24 Apr 2026 15:19:48 +0800 Subject: [PATCH] feat: rewrite PopupHandle for Qt6 with Window popupType support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Rewrite DPopupWindowHandle to work natively with Qt6 2. Support Popup.Window popupType with window decorations (radius, border, shadow, blur) 3. Implement screen edge avoidance for popup windows 4. Add close-on-focus-loss behavior through event filters 5. Simplify architecture: remove DPopupWindowHandleImpl and DVtableHook usage 6. Remove global PopupMode enum and forceWindowMode property 7. Update DQuickWindowAttached to support QQuickPopup objects Log: Rewrite PopupHandle to support popupType Window mode in Qt6 Influence: 1. Test Popup with popupType: Window – verify window decorations (radius, border, shadow, blur) 2. Test screen edge avoidance: open popups near screen edges and verify positioning 3. Test close-on-click-outside: click outside popup and verify it closes 4. Test close-on-focus-loss: switch to other window and verify popup closes 5. Test nested popups (submenus) – verify correct screen assignment and flipping behavior 6. Test window properties: windowRadius, borderWidth, borderColor, shadowRadius, shadowOffset, shadowColor 7. Test translucentBackground and enableBlurWindow properties 8. Test menu popup behavior – ensure no regressions in Menu popup 9. Test with different screen configurations (multiple monitors, different DPI) 10. Verify no crash when popup is destroyed feat: 重写PopupHandle以支持Qt6的Window模式 1. 重写DPopupWindowHandle以原生支持Qt6 2. 支持Popup.Window弹窗类型,添加窗口装饰(圆角、边框、阴影、模糊) 3. 实现弹窗窗口的屏幕边缘避让 4. 通过事件过滤器实现失去焦点自动关闭 5. 简化架构:移除DPopupWindowHandleImpl和DVtableHook的使用 6. 移除全局PopupMode枚举和forceWindowMode属性 7. 更新DQuickWindowAttached以支持QQuickPopup对象 Log: 重写PopupHandle支持popupType为Window模式 Influence: 1. 测试Popup的popupType: Window模式 – 验证窗口装饰(圆角、边框、阴影、 模糊) 2. 测试屏幕边缘避让:在屏幕边缘打开弹窗,验证位置调整 3. 测试点击外部关闭:点击弹窗外区域,验证弹窗关闭 4. 测试失去焦点关闭:切换到其他窗口,验证弹窗关闭 5. 测试嵌套弹窗(子菜单)– 验证正确的屏幕分配和翻转行为 6. 测试窗口属性:windowRadius、borderWidth、borderColor、shadowRadius、 shadowOffset、shadowColor 7. 测试translucentBackground和enableBlurWindow属性 8. 测试菜单弹窗行为 – 确保菜单弹窗没有回归问题 9. 测试不同屏幕配置(多显示器、不同DPI) 10. 验证弹窗销毁时不会崩溃 --- examples/qml-inspect/Example_Popup.qml | 33 ++- qmlplugin/qmlplugin_plugin.cpp | 2 - qt6/src/qml/Menu.qml | 7 +- qt6/src/qml/Popup.qml | 4 +- src/dquickwindow.cpp | 46 ++- src/dquickwindow.h | 4 +- src/private/dpopupwindowhandle.cpp | 386 +++++++++++++------------ src/private/dpopupwindowhandle_p.h | 87 +++--- src/private/dqmlglobalobject.cpp | 7 +- src/private/dqmlglobalobject_p.h | 10 +- src/private/dquickwindow_p.h | 1 + src/qml/Menu.qml | 7 +- src/qml/Popup.qml | 3 +- src/src.cmake | 2 + 14 files changed, 318 insertions(+), 281 deletions(-) diff --git a/examples/qml-inspect/Example_Popup.qml b/examples/qml-inspect/Example_Popup.qml index 9328b3f5f..e32c6996e 100644 --- a/examples/qml-inspect/Example_Popup.qml +++ b/examples/qml-inspect/Example_Popup.qml @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2022 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -68,12 +68,6 @@ Column { } } } - Button { - text: "handle forceWindowMode" - onClicked: { - popupWindow.PopupHandle.forceWindowMode = !popupWindow.PopupHandle.forceWindowMode - } - } Popup { id: popupWindow; objectName: "pupup window" @@ -85,10 +79,25 @@ Column { // width: 300 // height: 300 // margins: 100 - PopupHandle.forceWindowMode: true - PopupHandle.delegate: PopupWindow { - blurControl: popupWindow - } + + // Test window properties in Qt6 + // popupType: Popup.Window + // PopupHandle.windowRadius: 18 + // PopupHandle.borderWidth: 2 + // PopupHandle.borderColor: "red" + // PopupHandle.shadowRadius: 30 + // PopupHandle.shadowOffset: Qt.point(0, 4) + // PopupHandle.shadowColor: Qt.rgba(0, 0, 0, 0.5) + // PopupHandle.translucentBackground: true + // PopupHandle.enableBlurWindow: true + + // Component.onCompleted: { + // console.log("=== Popup Properties ===") + // console.log("windowRadius:", PopupHandle.windowRadius) + // console.log("borderWidth:", PopupHandle.borderWidth) + // console.log("borderColor:", PopupHandle.borderColor) + // console.log("translucentBackground:", PopupHandle.translucentBackground) + // } contentItem: Column { spacing: 10 Text { @@ -125,8 +134,6 @@ Column { Menu { id: menuPopup MenuItem { text: "Text" } - - PopupHandle.forceWindowMode: true } ArrowShapePopup { id: arrow diff --git a/qmlplugin/qmlplugin_plugin.cpp b/qmlplugin/qmlplugin_plugin.cpp index f9a44e6b7..057261d70 100644 --- a/qmlplugin/qmlplugin_plugin.cpp +++ b/qmlplugin/qmlplugin_plugin.cpp @@ -22,7 +22,6 @@ #include "private/dquickiconlabel_p.h" #include "private/dsettingscontainer_p.h" #include "private/dmessagemanager_p.h" -#include "private/dpopupwindowhandle_p.h" #include "private/dobjectmodelproxy_p.h" #include "private/dquickwaterprogressattribute_p.h" #include "private/dquickarrowboxpath_p.h" @@ -208,7 +207,6 @@ void QmlpluginPlugin::registerTypes(const char *uri) QStringLiteral("ColorSelector is only available as an attached property.")); dtkRegisterUncreatableType(uri, implUri, 1, 0, "Color", QStringLiteral("Color is only available as enums.")); - dtkRegisterUncreatableType(uri, implUri, 1, 0, "PopupHandle", "PopupWindow Attached"); dtkRegisterUncreatableType(uri, implUri, 1, 0, "PlatformHandle", "PlatformHandle"); qRegisterMetaType(); diff --git a/qt6/src/qml/Menu.qml b/qt6/src/qml/Menu.qml index 92155b3f0..b3a07729b 100644 --- a/qt6/src/qml/Menu.qml +++ b/qt6/src/qml/Menu.qml @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2022 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -39,9 +39,6 @@ T.Menu { delegate: MenuItem { } - D.PopupHandle.delegate: PopupWindow { - blurControl: control - } contentItem: FocusScope { // QTBUG-99897 focus doesn't be clear. @@ -100,7 +97,7 @@ T.Menu { } background: Loader { - active: !control.D.PopupHandle.window + active: control.popupType !== Popup.Window sourceComponent: FloatingPanel { implicitWidth: DS.Style.menu.item.width implicitHeight: DS.Style.menu.item.height diff --git a/qt6/src/qml/Popup.qml b/qt6/src/qml/Popup.qml index 9cb01484c..5394912ce 100644 --- a/qt6/src/qml/Popup.qml +++ b/qt6/src/qml/Popup.qml @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2022 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -19,7 +19,7 @@ T.Popup { padding: DS.Style.popup.padding background: Loader { - active: !control.D.PopupHandle.window + active: control.popupType !== Popup.Window sourceComponent: FloatingPanel { implicitHeight: DS.Style.popup.height implicitWidth: DS.Style.popup.width diff --git a/src/dquickwindow.cpp b/src/dquickwindow.cpp index b1c948529..64210d35b 100644 --- a/src/dquickwindow.cpp +++ b/src/dquickwindow.cpp @@ -56,6 +56,11 @@ DQuickWindowAttached *DQuickWindow::qmlAttachedProperties(QObject *object) if (window) { return new DQuickWindowAttached(window); } + + // Support QQuickPopup with popupType == Window + if (object && object->inherits("QQuickPopup")) { + return new DQuickWindowAttached(object); + } return nullptr; } @@ -131,6 +136,10 @@ bool DQuickWindowAttachedPrivate::ensurePlatformHandle() if (handle) return true; + if (!window) { + return false; + } + if (!DPlatformHandle::setEnabledNoTitlebarForWindow(window, true)) { qWarning() << "Failed to enable NoTitlebar for the window:" << window; return false; @@ -179,10 +188,32 @@ void DQuickWindowAttachedPrivate::destoryPlatformHandle() handle = nullptr; } +void DQuickWindowAttachedPrivate::setWindow(QWindow *newWindow) +{ + Q_Q(DQuickWindowAttached); + + if (window == newWindow) + return; + + window = newWindow; + + if (newWindow) { + newWindow->installEventFilter(q); + QObject::connect(DWindowManagerHelper::instance(), SIGNAL(windowMotifWMHintsChanged(quint32)), + q, SLOT(_q_onWindowMotifHintsChanged(quint32)), Qt::UniqueConnection); + + if (explicitEnable == True) { + ensurePlatformHandle(); + } + } +} + void DQuickWindowAttachedPrivate::_q_onWindowMotifHintsChanged(quint32 winId) { D_Q(DQuickWindowAttached); + if (!q->window()) + return; if (q->window()->winId() != winId) return; @@ -339,9 +370,22 @@ DQuickWindowAttached::DQuickWindowAttached(QWindow *window) this, SLOT(_q_onWindowMotifHintsChanged(quint32))); } +DQuickWindowAttached::DQuickWindowAttached(QObject *popupObject) + : QObject(popupObject) + , DObject(*new DQuickWindowAttachedPrivate(nullptr, this)) +{ +} + QQuickWindow *DQuickWindowAttached::window() const { - return qobject_cast(parent()); + D_DC(DQuickWindowAttached); + return qobject_cast(d->window); +} + +void DQuickWindowAttached::setWindow(QQuickWindow *window) +{ + D_D(DQuickWindowAttached); + d->setWindow(window); } /*! diff --git a/src/dquickwindow.h b/src/dquickwindow.h index a312a72c5..db67525b4 100644 --- a/src/dquickwindow.h +++ b/src/dquickwindow.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 - 2022 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2020 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -80,8 +80,10 @@ class DQuickWindowAttached : public QObject, public DTK_CORE_NAMESPACE::DObject public: explicit DQuickWindowAttached(QWindow *window); + explicit DQuickWindowAttached(QObject *popupObject); QQuickWindow *window() const; + void setWindow(QQuickWindow *window); bool isEnabled() const; int windowRadius() const; diff --git a/src/private/dpopupwindowhandle.cpp b/src/private/dpopupwindowhandle.cpp index 8472663af..647269a44 100644 --- a/src/private/dpopupwindowhandle.cpp +++ b/src/private/dpopupwindowhandle.cpp @@ -1,265 +1,285 @@ -// SPDX-FileCopyrightText: 2022 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2022 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later -#define protected public #include "dpopupwindowhandle_p.h" +#include "dquickwindow.h" #include #include +#include +#include +#include +#include +#include +#include -#include +#include -DCORE_USE_NAMESPACE DQUICK_BEGIN_NAMESPACE -// className prepend string of QT_NAMESPACE if existed. -bool inheritsTheClassType(QObject *object, QString className) { -#if defined(QT_NAMESPACE) -#define D_GET_NAMESPACE_STR_IMPL(M) #M "::" -#define D_GET_NAMESPACE_STR(M) D_GET_NAMESPACE_STR_IMPL(M) - className.prepend(D_GET_NAMESPACE_STR(QT_NAMESPACE)); -#undef D_GET_NAMESPACE_STR -#undef D_GET_NAMESPACE_STR_IMPL -#endif - return object && object->inherits(qPrintable(className)); -} - -static inline bool shouldCreatePopupWindowForMode(const DQMLGlobalObject::PopupMode mode) +static bool isPopupWindow(QWindow *window) { - switch (mode) { - case DQMLGlobalObject::WindowMode: - return true; - case DQMLGlobalObject::EmbedMode: - return false; - case DQMLGlobalObject::AutoMode: - // TODO https://github.com/linuxdeepin/dtk/issues/70 - if (qEnvironmentVariableIsEmpty("D_POPUP_MODE")) - return false; - return qEnvironmentVariable("D_POPUP_MODE") != "embed"; - } - return false; + return window && window->inherits("QQuickPopupWindow"); } -DQMLGlobalObject::PopupMode DPopupWindowHandle::m_popupMode = DQMLGlobalObject::AutoMode; -DPopupWindowHandle::DPopupWindowHandle(QObject *parent) - : QObject (parent) + +static bool isPopupItem(QQuickItem *item) { - // after `autoWindowMode` property initialized to createHandle. - connect(popup(), SIGNAL(windowChanged(QQuickWindow *)), this, SLOT(createHandle())); + return item && item->inherits("QQuickPopupItem"); } DPopupWindowHandle::~DPopupWindowHandle() { + if (m_trackedItem) + QQuickItemPrivate::get(m_trackedItem)->removeItemChangeListener(this, QQuickItemPrivate::Geometry); + if (m_parentWindow) + m_parentWindow->removeEventFilter(this); + if (m_popupWin) + m_popupWin->removeEventFilter(this); } -DPopupWindowHandle *DPopupWindowHandle::qmlAttachedProperties(QObject *object) +DPopupWindowHandle::DPopupWindowHandle(QObject *popup) + : QObject(popup) + , m_popup(popup) { - if (!inheritsTheClassType(object, "QQuickPopup")) - return nullptr; - - return new DPopupWindowHandle(object); -} + m_attached = DQuickWindow::qmlAttachedProperties(popup); + if (!m_attached) + return; + + // Initial update + updateEnabled(); + + popupItemReparented(); -void DPopupWindowHandle::setPopupMode(const DQMLGlobalObject::PopupMode mode) -{ - m_popupMode = mode; + connect(popup, SIGNAL(popupTypeChanged()), this, SLOT(updateEnabled())); } -QQuickWindow *DPopupWindowHandle::window() const +DQuickWindowAttached *DPopupWindowHandle::qmlAttachedProperties(QObject *object) { - return m_handle ? m_handle->window() : nullptr; + auto handle = new DPopupWindowHandle(object); + return handle->m_attached; } -QQmlComponent *DPopupWindowHandle::delegate() const +DQuickWindowAttached *DPopupWindowHandle::windowAttached() const { - return m_delegate; + return m_attached; } -void DPopupWindowHandle::setDelegate(QQmlComponent *delegate) +QQuickItem *DPopupWindowHandle::popupItem() const { - m_delegate = delegate; -} + if (!m_popup) + return nullptr; -bool DPopupWindowHandle::forceWindowMode() const -{ - return m_forceWindowMode; + const auto children = m_popup->findChildren(Qt::FindDirectChildrenOnly); + auto it = std::find_if(children.begin(), children.end(), isPopupItem); + if (it != children.end()) + return *it; + return nullptr; } -void DPopupWindowHandle::setForceWindowMode(bool forceWindowMode) +void DPopupWindowHandle::popupItemReparented() { - if (m_forceWindowMode == forceWindowMode) + QQuickItem *item = popupItem(); + if (m_trackedItem == item) return; - m_forceWindowMode = forceWindowMode; - if (!m_forceWindowMode && m_handle) { - m_handle.reset(); - Q_EMIT windowChanged(); + if (m_trackedItem) { + QQuickItemPrivate::get(m_trackedItem)->removeItemChangeListener(this, QQuickItemPrivate::Geometry | QQuickItemPrivate::Visibility | QQuickItemPrivate::Parent); + disconnect(m_trackedItem, &QQuickItem::windowChanged, this, &DPopupWindowHandle::onWindowChanged); } - if (m_forceWindowMode) { - // try to create handle. - createHandle(); - } -} -void DPopupWindowHandle::createHandle() -{ - if (!needCreateHandle()) - return; + m_trackedItem = item; - auto window = qobject_cast(m_delegate->create(m_delegate->creationContext())); - Q_ASSERT(window); + // QQuickItemPrivate::get(item)->addItemChangeListener(this, QQuickItemPrivate::Geometry | QQuickItemPrivate::Visibility | QQuickItemPrivate::Parent); + connect(item, &QQuickItem::windowChanged, this, &DPopupWindowHandle::onWindowChanged); - m_handle.reset(new DPopupWindowHandleImpl(window, popup())); - Q_EMIT windowChanged(); + + if (QQuickWindow *window = popupWindow()) + m_attached->setWindow(window); } -bool DPopupWindowHandle::needCreateHandle() const +QQuickWindow *DPopupWindowHandle::popupWindow() const { - // has created. - if (m_handle) - return false; - - // no delegate. - if (!m_delegate) { - if (m_forceWindowMode) - qWarning() << "delegate don't set but forceWindowMode has been set."; - - return false; - } - // forceWindowMode > `D_POPUP_MODE` > popupMode - return m_forceWindowMode || shouldCreatePopupWindowForMode(m_popupMode); + QQuickItem *item = popupItem(); + return item ? item->window() : nullptr; } -QObject *DPopupWindowHandle::popup() const +void DPopupWindowHandle::updateEnabled() { - return parent(); + if (!m_popup || !m_attached) + return; + + QVariant popupTypeVar = m_popup->property("popupType"); + bool shouldEnable = popupTypeVar.isValid() && popupTypeVar.toInt() == 1; + if (shouldEnable == m_enabled) + return; + m_enabled = shouldEnable; + m_attached->setEnabled(shouldEnable); } -// it's not to call QQuickPopupItem's reposition when handle is positioning. -static constexpr char const *PopupWindowHandlePointer = "_d_popup_window_handle"; -static inline void popupGeometryChanged(QQuickItem *obj, const QRectF &newGeometry, const QRectF &oldGeometry) +bool DPopupWindowHandle::isEnabled() const { - DPopupWindowHandleImpl *handle = obj->property(PopupWindowHandlePointer).value(); - Q_ASSERT(handle); - if (!handle->isPositioning()) { - // only in `reposition` to override virtual function. -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - DVtableHook::callOriginalFun(obj, &QQuickItem::geometryChanged, newGeometry, oldGeometry); -#else - DVtableHook::callOriginalFun(obj, &QQuickItem::geometryChange, newGeometry, oldGeometry); -#endif - } + return m_enabled; } -static inline void popupUpdatePolish(QQuickItem *obj) + +void DPopupWindowHandle::onWindowChanged(QQuickWindow *window) { - DPopupWindowHandleImpl *handle = obj->property(PopupWindowHandlePointer).value(); - Q_ASSERT(handle); - if (handle->isPositioning()) { - // avoid to call original function in `reposition`. - handle->setPositioning(false); - } else { - // only sepcial scene to override virtual function. - DVtableHook::callOriginalFun(obj, &QQuickItem::updatePolish); + // Cleanup old filters + if (m_popupWin) { + m_popupWin->removeEventFilter(this); + m_popupWin = nullptr; + } + if (m_parentWindow) { + m_parentWindow->removeEventFilter(this); + m_parentWindow = nullptr; } -} -DPopupWindowHandleImpl::DPopupWindowHandleImpl(QQuickWindow *window, QObject *parent) - : QObject(parent) - , m_window(window) - , m_popup(parent) -{ - Q_ASSERT(popupItem()); - - connect(popup(), SIGNAL(opened()), this, SLOT(reposition())); - popupItem()->setProperty(PopupWindowHandlePointer, QVariant::fromValue(this)); - // geometryChanged would call reposition of `PopupItem`. -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - DVtableHook::overrideVfptrFun(popupItem(), &QQuickItem::geometryChanged, &popupGeometryChanged); -#else - DVtableHook::overrideVfptrFun(popupItem(), &QQuickItem::geometryChange, &popupGeometryChanged); -#endif - // updatePolish would call reposition of `PopupItem`. - DVtableHook::overrideVfptrFun(popupItem(), &QQuickItem::updatePolish, &popupUpdatePolish); - - // TODO QML Window with Qt::Popup flag not behaving correctly (QTBUG-69777) - connect(m_window, &QWindow::activeChanged, this, &DPopupWindowHandleImpl::close); - connect(popup(), SIGNAL(closed()), this, SLOT(close())); + // Apply attached properties (blur, radius, etc.) to popup windows. + if (m_attached) { + if (!window || isPopupWindow(window)) + m_attached->setWindow(window); + } + + m_popupWin = window; + if (window && isEnabled() && isPopupWindow(window)) { + + window->installEventFilter(this); + + // Install event filter on parent window for close-on-click. + // Done here so it is registered once per popup window instance. + if (QQuickWindow *main = qobject_cast(window->transientParent())) { + m_parentWindow = main; + m_parentWindow->installEventFilter(this); + } + } } -DPopupWindowHandleImpl::~DPopupWindowHandleImpl() +bool DPopupWindowHandle::eventFilter(QObject *watched, QEvent *event) { - QQuickItem *item = popupItem(); - if (item) { - // reset original virtual function. -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - DVtableHook::resetVfptrFun(item, &QQuickItem::geometryChanged); -#else - DVtableHook::resetVfptrFun(item, &QQuickItem::geometryChange); -#endif - DVtableHook::resetVfptrFun(item, &QQuickItem::updatePolish); - disconnect(item, nullptr, this, nullptr); + if (watched == m_popupWin && event->type() == QEvent::Move) { + adjustPopupPosition(); } - disconnect(popup(), nullptr, this, nullptr); - disconnect(m_window, nullptr, this, nullptr); - m_window->deleteLater(); - m_window = nullptr; -} + // Close popup on parent window click (only while popup is visible) + if (watched == m_parentWindow && event->type() == QEvent::MouseButtonPress + && m_popup->property("visible").toBool()) { + int closePolicy = m_popup->property("closePolicy").toInt(); + bool closeOnPressOutside = closePolicy & 0x01; + bool closeOnPressOutsideParent = closePolicy & 0x02; -QQuickWindow *DPopupWindowHandleImpl::window() const -{ - return m_window; + if (closeOnPressOutside || closeOnPressOutsideParent) { + QMetaObject::invokeMethod(m_popup, "close", Qt::QueuedConnection); + } + } + + return QObject::eventFilter(watched, event); } -QObject *DPopupWindowHandleImpl::popup() const +void DPopupWindowHandle::itemGeometryChanged(QQuickItem *, + QQuickGeometryChange change, + const QRectF &) { - return m_popup; -} + if (!change.positionChange() && !change.sizeChange()) + return; -QQuickItem *DPopupWindowHandleImpl::popupItem() const { - for (auto item : popup()->children()) { - if (inheritsTheClassType(item, "QQuickPopupItem")) - return qobject_cast(item); - } - return nullptr; + adjustPopupPosition(); } -bool DPopupWindowHandleImpl::isPositioning() const +void DPopupWindowHandle::itemVisibilityChanged(QQuickItem *item) { - return m_positioning; + if (!item->isVisible()) + return; + + adjustPopupPosition(); } -void DPopupWindowHandleImpl::setPositioning(bool positioning) +void DPopupWindowHandle::itemParentChanged(QQuickItem *item, QQuickItem *) { - m_positioning = positioning; + if (!item) + return; + + adjustPopupPosition(); } -void DPopupWindowHandleImpl::reposition() +void DPopupWindowHandle::adjustPopupPosition() { - if (isPositioning()) + if (!isEnabled() ||!m_popupWin) return; - setPositioning(true); - - m_window->resize(popupItem()->size().toSize()); - // set window's position to origin popupItem' leftTop position. - m_window->setPosition(popupItem()->mapToGlobal(QPoint(0, 0)).toPoint()); - // reset popupItem's position to window's contentItem leftTop position. - popupItem()->setPosition(m_window->contentItem()->position()); - popupItem()->setParentItem(m_window->contentItem()); + const QSize size = m_popupWin->size(); + if (size.width() <= 0 || size.height() <= 0) + return; - m_window->show(); - m_window->requestActivate(); -} + // Nested popup (submenu) detection: + // A submenu's popupWindow uses the parent menu's popupWindow as its transientParent. + QWindow *transient = m_popupWin->transientParent(); + const bool isNested = isPopupWindow(transient); + QRectF parentWindowRect; + if (isNested) + parentWindowRect = QRectF(QPointF(transient->position()), QSizeF(transient->size())); + + // Screen selection: + // - Nested popups (submenus) must use the parent menu's screen to avoid + // screenAt() returning an adjacent screen or null when the submenu's + // initial position is already off-screen. + // - Flat popups use their own position to determine the screen. + QScreen *screen = nullptr; + if (isNested) { + screen = transient->screen(); + } else { + const QPoint winCenter = m_popupWin->position() + QPoint(size.width() / 2, size.height() / 2); + screen = QGuiApplication::screenAt(winCenter); + } + if (!screen) + screen = m_popupWin->screen(); + if (!screen) + return; -void DPopupWindowHandleImpl::close() -{ - if (!m_window->isActive() || !popup()->property("visible").toBool()) { - m_window->hide(); - // window hide but popup's visible is true, and it effects popup next open. - popup()->setProperty("visible", false); + const QRectF bounds(screen->availableGeometry()); + QRectF rect(QPointF(m_popupWin->position()), QSizeF(size)); + + // Horizontal flip for submenus: + // Qt places submenus to the right of the parent menu by default; + // flip to the left if there is not enough space on the right, and vice versa. + if (isNested && !parentWindowRect.isNull()) { + const bool overflowsRight = rect.right() > bounds.right(); + const bool overflowsLeft = rect.left() < bounds.left(); + + if (overflowsRight && !overflowsLeft) { + // Not enough space on the right — try flipping to the left of the parent. + const qreal leftCandidate = parentWindowRect.left() - size.width(); + if (leftCandidate >= bounds.left()) { + rect.moveLeft(leftCandidate); + } else { + // Not enough space on either side — push against the right edge. + rect.moveRight(bounds.right()); + } + } else if (overflowsLeft && !overflowsRight) { + // Not enough space on the left — flip to the right of the parent. + const qreal rightCandidate = parentWindowRect.right(); + if (rightCandidate + size.width() <= bounds.right()) { + rect.moveLeft(rightCandidate); + } else { + rect.moveLeft(bounds.left()); + } + } } + + // Vertical clamping and final bounds enforcement. + if (rect.right() > bounds.right()) + rect.moveRight(bounds.right()); + if (rect.left() < bounds.left()) + rect.moveLeft(bounds.left()); + if (rect.bottom() > bounds.bottom()) + rect.moveBottom(bounds.bottom()); + if (rect.top() < bounds.top()) + rect.moveTop(bounds.top()); + + const QPoint newPos = rect.topLeft().toPoint(); + if (newPos != m_popupWin->position()) + m_popupWin->setPosition(newPos); } + DQUICK_END_NAMESPACE #include "moc_dpopupwindowhandle_p.cpp" diff --git a/src/private/dpopupwindowhandle_p.h b/src/private/dpopupwindowhandle_p.h index d99e85bfc..430189202 100644 --- a/src/private/dpopupwindowhandle_p.h +++ b/src/private/dpopupwindowhandle_p.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2022 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -10,79 +10,64 @@ #include "dqmlglobalobject_p.h" +#include + QT_BEGIN_NAMESPACE class QQuickWindow; class QQuickItem; QT_END_NAMESPACE - DQUICK_BEGIN_NAMESPACE -class DPopupWindowHandleImpl; -class Q_DECL_EXPORT DPopupWindowHandle : public QObject +class DQuickWindowAttached; + +class DPopupWindowHandle : public QObject, public QQuickItemChangeListener { Q_OBJECT - Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate) - Q_PROPERTY(QQuickWindow *window READ window NOTIFY windowChanged) - Q_PROPERTY(bool forceWindowMode READ forceWindowMode WRITE setForceWindowMode) -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - QML_UNCREATABLE("PopupWindow Attached.") + + QML_UNCREATABLE("PopupHandle Attached.") QML_NAMED_ELEMENT(PopupHandle) - QML_ATTACHED(DPopupWindowHandle) -#endif - + QML_ATTACHED(DQuickWindowAttached) + public: - explicit DPopupWindowHandle(QObject *parent = nullptr); + explicit DPopupWindowHandle(QObject *popup); ~DPopupWindowHandle() override; - static DPopupWindowHandle *qmlAttachedProperties(QObject *object); + static DQuickWindowAttached *qmlAttachedProperties(QObject *object); - static void setPopupMode(const DQMLGlobalObject::PopupMode mode); + DQuickWindowAttached *windowAttached() const; - QQuickWindow *window() const; - QQmlComponent *delegate() const; - void setDelegate(QQmlComponent *delegate); - bool forceWindowMode() const; - void setForceWindowMode(bool forceWindowMode); +protected: + bool eventFilter(QObject *watched, QEvent *event) override; -Q_SIGNALS: - void windowChanged(); + // QQuickItemChangeListener + void itemGeometryChanged(QQuickItem *item, + QQuickGeometryChange change, + const QRectF &oldGeometry) override; -private Q_SLOTS: - void createHandle(); - -private: - QObject *popup() const; - bool needCreateHandle() const; + void itemVisibilityChanged(QQuickItem *item) override; -private: - bool m_forceWindowMode = false; - bool m_isWindowMode = false; - QQmlComponent *m_delegate = nullptr; - QScopedPointer m_handle; - static DQMLGlobalObject::PopupMode m_popupMode; -}; + void itemParentChanged(QQuickItem *item, QQuickItem *oldParent) override; -class DPopupWindowHandleImpl : public QObject -{ - Q_OBJECT -public: - explicit DPopupWindowHandleImpl(QQuickWindow *window, QObject *parent); - ~DPopupWindowHandleImpl() override; +private Q_SLOTS: + void updateEnabled(); + void onWindowChanged(QQuickWindow *window); - QQuickWindow *window() const; - QObject *popup() const; +private: + QQuickWindow *popupWindow() const; QQuickItem *popupItem() const; - void updatePosition(); - bool isPositioning() const; - void setPositioning(bool positioning); + void popupItemReparented(); -private Q_SLOTS: - void reposition(); - void close(); + bool isEnabled() const; + void adjustPopupPosition(); + private: - QQuickWindow *m_window = nullptr; QObject *m_popup = nullptr; - bool m_positioning = false; + + bool m_enabled = false; + DQuickWindowAttached *m_attached = nullptr; + QPointer m_parentWindow = nullptr; + QPointer m_popupWin = nullptr; + QPointer m_trackedItem = nullptr; }; DQUICK_END_NAMESPACE diff --git a/src/private/dqmlglobalobject.cpp b/src/private/dqmlglobalobject.cpp index 05695cf10..8ea840fac 100644 --- a/src/private/dqmlglobalobject.cpp +++ b/src/private/dqmlglobalobject.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 - 2022 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2020 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -517,11 +517,6 @@ void DQMLGlobalObject::sendSystemMessage(const QString &summary, const QString & }); } -void DQMLGlobalObject::setPopupMode(const PopupMode mode) -{ - DPopupWindowHandle::setPopupMode(mode); -} - bool DQMLGlobalObject::loadTranslator() { DCORE_USE_NAMESPACE; diff --git a/src/private/dqmlglobalobject_p.h b/src/private/dqmlglobalobject_p.h index f73724065..b40f92e88 100644 --- a/src/private/dqmlglobalobject_p.h +++ b/src/private/dqmlglobalobject_p.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 - 2022 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2020 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -170,13 +170,6 @@ class DQMLGlobalObject : public QObject, public DTK_CORE_NAMESPACE::DObject }; Q_ENUM(ZOrder) - enum PopupMode { - AutoMode, - WindowMode, - EmbedMode - }; - Q_ENUM(PopupMode) - #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) enum class CompositionMode { Source = QPainter::CompositionMode_Source, @@ -243,7 +236,6 @@ class DQMLGlobalObject : public QObject, public DTK_CORE_NAMESPACE::DObject const QString &appIcon = QString(), const QStringList &actions = QStringList(), const QVariantMap hints = QVariantMap(), const int timeout = 3000, const uint replaceId = 0); - static void setPopupMode(const PopupMode mode); static bool loadTranslator(); #if QT_VERSION_MAJOR > 5 diff --git a/src/private/dquickwindow_p.h b/src/private/dquickwindow_p.h index a9f8633a1..801279e78 100644 --- a/src/private/dquickwindow_p.h +++ b/src/private/dquickwindow_p.h @@ -41,6 +41,7 @@ class DQuickWindowAttachedPrivate : public DTK_CORE_NAMESPACE::DObjectPrivate bool ensurePlatformHandle(); void destoryPlatformHandle(); + void setWindow(QWindow *newWindow); void _q_onWindowMotifHintsChanged(quint32 winId); void addBlur(DQuickBehindWindowBlur *blur); void removeBlur(DQuickBehindWindowBlur *blur); diff --git a/src/qml/Menu.qml b/src/qml/Menu.qml index 559f6b1d3..226c2e1bd 100644 --- a/src/qml/Menu.qml +++ b/src/qml/Menu.qml @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2022 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -36,10 +36,6 @@ T.Menu { delegate: MenuItem { } - D.PopupHandle.delegate: PopupWindow { - blurControl: control - } - contentItem: Control { contentItem: ColumnLayout { id: viewLayout @@ -82,7 +78,6 @@ T.Menu { } background: Loader { - active: !control.D.PopupHandle.window sourceComponent: FloatingPanel { implicitWidth: DS.Style.menu.item.width implicitHeight: DS.Style.menu.item.height diff --git a/src/qml/Popup.qml b/src/qml/Popup.qml index 5da5b8d13..7c80dfb77 100644 --- a/src/qml/Popup.qml +++ b/src/qml/Popup.qml @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2022 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -19,7 +19,6 @@ T.Popup { padding: DS.Style.popup.padding background: Loader { - active: !control.D.PopupHandle.window sourceComponent: FloatingPanel { implicitHeight: DS.Style.popup.height implicitWidth: DS.Style.popup.width diff --git a/src/src.cmake b/src/src.cmake index 9d7b05ea0..e693c8576 100644 --- a/src/src.cmake +++ b/src/src.cmake @@ -35,11 +35,13 @@ if (DTK5) ${PROJECT_SOURCE_DIR}/src/private/dbackdropnode_p.h ${PROJECT_SOURCE_DIR}/src/private/dquickbackdropblitter_p.h ${PROJECT_SOURCE_DIR}/src/private/dquickborderimage_p.h + ${PROJECT_SOURCE_DIR}/src/private/dpopupwindowhandle_p.h ) list(REMOVE_ITEM SRCS ${PROJECT_SOURCE_DIR}/src/private/dbackdropnode.cpp ${PROJECT_SOURCE_DIR}/src/private/dquickbackdropblitter.cpp ${PROJECT_SOURCE_DIR}/src/private/dquickborderimage.cpp + ${PROJECT_SOURCE_DIR}/src/private/dpopupwindowhandle.cpp ) endif()