diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9391855..82865eae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -97,18 +97,25 @@ jobs: # The build system tests register themselves only where the configure found the tool they # drive, which is the one way this job could go green while covering less than it says. A # missing qmake or nmake is meant to be a skip on a developer's machine and a failure here. - - name: Check that the consumer tests registered + # The fast unit suites are also required, so accidental conditional exclusions fail CI. + - name: Check that the required tests registered shell: bash run: | - expected="buildsystems.cmake buildsystems.qmake" + expected="testpolicy.timeouts buildsystems.cmake buildsystems.qmake buildsystems.qtmajor core.eventdispatch.unit core.dispatchlifetime.unit core.objecteventfilters.unit core.windowcontext.unit core.qtwindowcontext.unit core.windowmovegeometry.unit core.windowmove.component core.styleagent.unit core.portalstyle.component agents.unit" + for scenario in present default core missing-required unknown-required optional quiet-missing repeat optional-qt-missing; do + expected="$expected buildsystems.components.$scenario" + done if [ "$RUNNER_OS" = "Windows" ]; then - expected="$expected buildsystems.msbuild" + expected="$expected buildsystems.msbuild quickgeometry.unit quicksystembuttonarea.component systembuttons.qml widgetborder.lifetime windows.windowproc windows.windowlifetime windows.borderattributes windows.acrylic windows.effects windows.titlebar windows.styleagent core.windowsregistry.unit fallbackcursor.offscreen fallbackcursor.windows" fi + for stage in prepare build_core build_widgets build_quick all core widgets quick restored; do + expected="$expected buildsystems.qmake.reconfigure.$stage" + done for name in $expected; do - if ! ctest --test-dir build -N -R "$name" | grep -q "$name"; then - echo "$name did not register, so the tool it drives was not found here" + if ! ctest --test-dir build -N -R "^${name}$" | grep -Fq "$name"; then + echo "Required test $name did not register" exit 1 fi done @@ -119,7 +126,76 @@ jobs: # --no-tests=error is not optional. ctest exits 0 when it finds nothing to run, and every # test in this suite registers itself conditionally. - name: Test - run: ctest --test-dir build --output-on-failure --no-tests=error + timeout-minutes: 3 + run: ctest --test-dir build -LE reconfigure --output-on-failure --no-tests=error + + - name: Configure static package consumer regressions + if: matrix.platform == 'windows-msvc' + shell: bash + env: + CC: ${{ matrix.cc }} + CXX: ${{ matrix.cxx }} + run: | + cmake -G Ninja -S . -B build-static \ + -DCMAKE_BUILD_TYPE=Release \ + -DQWINDOWKIT_BUILD_STATIC=ON \ + -DQWINDOWKIT_BUILD_WIDGETS=ON \ + -DQWINDOWKIT_BUILD_QUICK=ON \ + -DQWINDOWKIT_BUILD_TESTS=ON + expected="buildsystems.msbuild buildsystems.qmake core.qtwindowcontext.unit core.windowmovegeometry.unit core.windowmove.component core.portalstyle.component quicksystembuttonarea.component systembuttons.qml windows.borderattributes windows.acrylic windows.effects windows.titlebar windows.styleagent core.windowsregistry.unit fallbackcursor.offscreen fallbackcursor.windows" + for stage in prepare build_core build_widgets build_quick all core widgets quick restored; do + expected="$expected buildsystems.qmake.reconfigure.$stage" + done + for name in $expected; do + if ! ctest --test-dir build-static -N -R "^${name}$" | grep -Fq "$name"; then + echo "Required static test $name did not register" + exit 1 + fi + done + + - name: Build static configuration + if: matrix.platform == 'windows-msvc' + run: cmake --build build-static + + - name: Test static units and package consumers + if: matrix.platform == 'windows-msvc' + timeout-minutes: 2 + run: ctest --test-dir build-static -R "(\.unit$|^core.windowmove.component$|^core.portalstyle.component$|^quicksystembuttonarea.component$|^systembuttons.qml$|^windows.borderattributes$|^windows.acrylic$|^windows.effects$|^windows.titlebar$|^windows.styleagent$|^fallbackcursor\.|^testpolicy.timeouts$|^buildsystems.msbuild$|^buildsystems.qmake$|^buildsystems.qtmajor$|^buildsystems.components\.)" --output-on-failure --no-tests=error + + - name: Test shared qmake reconfiguration + timeout-minutes: 4 + run: ctest --test-dir build -L reconfigure --output-on-failure --no-tests=error + + - name: Test static qmake reconfiguration + if: matrix.platform == 'windows-msvc' + timeout-minutes: 4 + run: ctest --test-dir build-static -L reconfigure --output-on-failure --no-tests=error + + # Single-axis caption hit testing has a separate compile-time path without + # system borders. Exercise it with real Widgets and Quick HWNDs as well. + - name: Configure without system borders + if: matrix.platform == 'windows-msvc' + shell: bash + env: + CC: ${{ matrix.cc }} + CXX: ${{ matrix.cxx }} + run: | + cmake -G Ninja -S . -B build-no-system-borders \ + -DCMAKE_BUILD_TYPE=Debug \ + -DQWINDOWKIT_BUILD_WIDGETS=ON \ + -DQWINDOWKIT_BUILD_QUICK=ON \ + -DQWINDOWKIT_BUILD_TESTS=ON \ + -DQWINDOWKIT_INSTALL=OFF \ + -DQWINDOWKIT_ENABLE_WINDOWS_SYSTEM_BORDERS=OFF + + - name: Build native hit-test regression without system borders + if: matrix.platform == 'windows-msvc' + run: cmake --build build-no-system-borders --target tst_windowhittest + + - name: Test native hit testing without system borders + if: matrix.platform == 'windows-msvc' + timeout-minutes: 1 + run: ctest --test-dir build-no-system-borders -R "^windows.hittest$" --output-on-failure --no-tests=error # MinGW is a job of its own because it wants msys2 rather than the toolchain the matrix above # sets up with an environment variable, and because Qt comes from the same place as the compiler @@ -162,12 +238,19 @@ jobs: # The same question the matrix jobs ask. MSBuild is not part of this toolchain, so only the # qmake half is expected here. - - name: Check that the consumer tests registered + - name: Check that the required tests registered shell: msys2 {0} run: | - for name in buildsystems.cmake buildsystems.qmake; do - if ! ctest --test-dir build -N -R "$name" | grep -q "$name"; then - echo "$name did not register, so the tool it drives was not found here" + expected="testpolicy.timeouts buildsystems.cmake buildsystems.qmake buildsystems.qtmajor core.eventdispatch.unit core.dispatchlifetime.unit core.objecteventfilters.unit core.windowcontext.unit core.qtwindowcontext.unit core.windowmovegeometry.unit core.windowmove.component core.styleagent.unit core.portalstyle.component agents.unit quickgeometry.unit quicksystembuttonarea.component systembuttons.qml widgetborder.lifetime windows.windowproc windows.windowlifetime windows.borderattributes windows.acrylic windows.effects windows.titlebar windows.styleagent core.windowsregistry.unit fallbackcursor.offscreen fallbackcursor.windows" + for scenario in present default core missing-required unknown-required optional quiet-missing repeat optional-qt-missing; do + expected="$expected buildsystems.components.$scenario" + done + for stage in prepare build_core build_widgets build_quick all core widgets quick restored; do + expected="$expected buildsystems.qmake.reconfigure.$stage" + done + for name in $expected; do + if ! ctest --test-dir build -N -R "^${name}$" | grep -Fq "$name"; then + echo "Required test $name did not register" exit 1 fi done @@ -177,5 +260,11 @@ jobs: run: cmake --build build - name: Test + timeout-minutes: 3 + shell: msys2 {0} + run: ctest --test-dir build -LE reconfigure --output-on-failure --no-tests=error + + - name: Test qmake reconfiguration + timeout-minutes: 4 shell: msys2 {0} - run: ctest --test-dir build --output-on-failure --no-tests=error + run: ctest --test-dir build -L reconfigure --output-on-failure --no-tests=error diff --git a/README.md b/README.md index e052316b..4bd90b03 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,25 @@ target_link_libraries(my_quick_app PRIVATE QWindowKit can also be added as a CMake subdirectory when it is vendored into your project. +With Qt 6.3 or later, building Quick generates `qml/QWindowKit/qmldir` and +`QWKQuick.qmltypes` in the build tree and installs them under `/qml/QWindowKit`. +`QWindowKit_QML_IMPORT_PATH` points to the import root for both CMake integration methods. +Add it to Qt Creator's code model and to the application's QML module: + +```cmake +list(APPEND QML_IMPORT_PATH "${QWindowKit_QML_IMPORT_PATH}") +list(REMOVE_DUPLICATES QML_IMPORT_PATH) +set(QML_IMPORT_PATH "${QML_IMPORT_PATH}" CACHE STRING "QML import paths" FORCE) +# When using qt_add_qml_module(my_quick_app ...), also pass: +# IMPORT_PATH "${QWindowKit_QML_IMPORT_PATH}" +``` + +For other editors, add this same directory to the QML language server's import paths. +For a direct check, run `qmllint -I /qml `. +Continue linking `QWindowKit::Quick` and calling `QWK::registerTypes(&engine)` before loading +QML, including in static builds; this module does not install a runtime plugin. +Qt 5 and Qt 6.0–6.2 retain procedural registration without generated tooling metadata. + ### qmake After installing QWindowKit with CMake, include the generated `.pri` file: diff --git a/README.zh-CN.md b/README.zh-CN.md index 36b9e6e7..75ce45f0 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -147,6 +147,25 @@ target_link_libraries(my_quick_app PRIVATE 如果你将 QWindowKit 作为源码依赖放入项目,也可以通过 CMake `add_subdirectory()` 集成。 +使用 Qt 6.3 或更高版本构建 Quick 时,会在构建目录的 `qml/QWindowKit` 中生成 +`qmldir` 和 `QWKQuick.qmltypes`,并安装到 `/qml/QWindowKit`。 +上述两种 CMake 集成方式均提供 `QWindowKit_QML_IMPORT_PATH`,指向 QML 导入根目录。 +将其加入 Qt Creator 的代码模型以及应用的 QML 模块: + +```cmake +list(APPEND QML_IMPORT_PATH "${QWindowKit_QML_IMPORT_PATH}") +list(REMOVE_DUPLICATES QML_IMPORT_PATH) +set(QML_IMPORT_PATH "${QML_IMPORT_PATH}" CACHE STRING "QML import paths" FORCE) +# 使用 qt_add_qml_module(my_quick_app ...) 时,还需传入: +# IMPORT_PATH "${QWindowKit_QML_IMPORT_PATH}" +``` + +其他编辑器需要将同一目录加入 QML 语言服务器的导入路径。 +也可运行 `qmllint -I /qml ` 检查。 +仍须链接 `QWindowKit::Quick`,并在加载 QML 前调用 `QWK::registerTypes(&engine)`, +静态构建也采用这一方式;本模块不安装运行时插件。 +Qt 5 和 Qt 6.0–6.2 保留原来的手动注册方式,暂不生成工具元数据。 + ### qmake 先使用 CMake 安装 QWindowKit,然后包含生成的 `.pri` 文件: diff --git a/docs/TODO.md b/docs/TODO.md index ba4b5d63..bdc67008 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,53 +1,30 @@ # TODO -## Wayland: `showSystemMenu()` may be passing the wrong coordinate space +## Wayland: validate system menu placement on a compositor -**File:** `src/core/contexts/linuxwaylandcontext.cpp` -**Introduced by:** the Wayland/X11 backend contribution (Wing-summer), needs the original author's eyes. -**Status:** unverified — reported from code reading only, no Wayland machine was available to test. +**File:** `src/core/contexts/waylandcontext.cpp` +**Status:** coordinate conversion fixed and statically reviewed; Wayland runtime validation pending. -`LinuxWaylandContext::virtual_hook()` forwards the incoming point straight to the -xdg-shell request: +`showSystemMenu()` accepts global Qt device-independent coordinates. The Wayland backend maps +them to window-local coordinates and uses Qt's native local conversion before issuing +`xdg_toplevel.show_window_menu`, which requires surface-local coordinates. QWindowKit sets +`Qt::FramelessWindowHint`, so there is no Qt client-side decoration offset to add. Wayland's +buffer scale must not be applied to the request coordinates. -```cpp -auto pos = static_cast(data); -xdg_toplevel_show_window_menu(toplevel, seat, serial, pos->x(), pos->y()); -``` +References: -The point that arrives here is a **global** position. `AbstractWindowContext::showSystemMenu()` -is fed from `QtWindowEventFilter::sharedEventFilter()`, which passes -`getMouseEventGlobalPos(me)`, and the public `WindowAgentBase::showSystemMenu()` is documented -in terms of global coordinates too. +- [xdg-shell protocol](https://raw.githubusercontent.com/wayland-mirror/wayland-protocols/main/stable/xdg-shell/xdg-shell.xml), `show_window_menu`. +- [Qt Wayland window implementation](https://github.com/qt/qtwayland/blob/6.8/src/client/qwaylandwindow.cpp), `mapFromWlSurface()` and `createDecoration()`. +- [Qt window coordinate mapping](https://github.com/qt/qtbase/blob/6.8/src/gui/kernel/qwindow.cpp), `mapFromGlobal()`. -`xdg_toplevel.show_window_menu` is believed to expect **surface-local** coordinates (relative to -the top-left of the window geometry). If that is right, the menu pops up displaced by the -window's own position on screen, and the error disappears only when the window happens to sit at -the origin. +On a Wayland compositor that supports this menu, check both Widgets and Quick windows: -Circumstantial evidence that the two Linux backends disagree about what they are handed: the X11 -path in `linuxx11context.cpp` treats the same argument as global and converts it to root -coordinates, +1. Right-click different title-bar positions, then move the window and repeat. The menu anchor + should follow the click, subject to compositor placement constraints. +2. Repeat with compositor scaling at 100%, 150%, and 200%, and with `QT_SCALE_FACTOR=2` to + exercise Qt's extra coordinate scaling separately from the Wayland buffer scale. +3. Trigger the public `showSystemMenu()` API during a valid input event using a point obtained + from `QWindow::mapToGlobal()`, and confirm the corresponding local anchor is used. -```cpp -qreal dpr = m_windowHandle->devicePixelRatio(); -int root_x = qRound(pos->x() * dpr); -int root_y = qRound(pos->y() * dpr); -``` - -which is the correct thing to do for `_GTK_SHOW_WINDOW_MENU`. Both backends implement the same -hook and receive the same input, so at most one of them can be right. - -### To check - -1. Confirm against the xdg-shell protocol XML which coordinate space `show_window_menu` wants. -2. If it is surface-local, subtract the window position before sending, e.g. - `*pos - m_windowHandle->position()`, and decide whether a device pixel ratio conversion is - needed at all (Wayland surface-local coordinates are logical, so probably not). -3. Test with a window that is **not** at the top-left of the screen — the bug is invisible at the - origin. - -### Related, same file - -`m_windowHandle` is passed to `nativeResourceForWindow()` without a null check. It happens to be -safe today because a null window yields a null `xdg_toplevel` and the function returns early, but -the X11 path had the same shape and did crash; it has since been given an explicit guard. +Record the compositor and Qt versions. Windows builds and tests do not execute this backend; +no Wayland compile or runtime result is claimed for the fix. diff --git a/docs/tutorial.en-US.md b/docs/tutorial.en-US.md index 372c910a..bfca3aff 100644 --- a/docs/tutorial.en-US.md +++ b/docs/tutorial.en-US.md @@ -305,6 +305,18 @@ Disable maximization: setWindowFlag(Qt::WindowMaximizeButtonHint, false); ``` +For QWindowKit's custom title bar on macOS and in the Qt fallback context, a left +double-click toggles maximized/normal state only when `Qt::WindowMaximizeButtonHint` +is set, the window is not fixed-size or fullscreen, and the pointer is in a +draggable title bar area. Clearing the flag also disables double-click restoration +of an already maximized window. The same rule applies to Quick window `flags`. +It does not prevent application code or a custom button handler from changing +the window state directly. + +The macOS custom title bar uses this QWindowKit policy; it does not read the +Desktop & Dock preference for native title bar double-click actions. Native +traffic-light buttons remain separate from this custom title bar behavior. + Create a fixed-size window: ```cpp @@ -523,6 +535,35 @@ windowAgent->setWindowAttribute(QStringLiteral("glass-tint-color"), QColor(255, If the platform does not support an attribute, `setWindowAttribute()` returns `false`. +## System appearance with StyleAgent + +Enable `QWINDOWKIT_ENABLE_STYLE_AGENT` to use `QWK::StyleAgent`. Create and use it +on the GUI thread with a running Qt event loop. Both getters are updated before +change signals are emitted; repeated equal settings do not emit signals. + +On Linux, both X11 and Wayland use only the [XDG Desktop Portal Settings interface](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Settings.html) +through Qt DBus. This adds a Qt DBus build/runtime dependency when StyleAgent is +enabled; static CMake/qmake consumers receive it automatically. Disabling +StyleAgent removes this dependency from the library. No direct XCB/Wayland calls +or desktop-specific fallbacks are used. The desktop's portal backend must provide +the corresponding appearance settings. + +The initial read is asynchronous: immediately after construction, `systemTheme()` +is `Unknown` and `systemAccentColor()` is invalid. Connect the change signals and +read the getters to apply subsequent snapshots. The portal's `color-scheme` selects +Dark/Light; high `contrast` selects HighContrast. No preference or a missing scheme +means Unknown; missing/invalid `accent-color` means an invalid QColor. Application +palette overrides never replace unavailable system settings. + +StyleAgent subscribes before reading and rereads the full snapshot on changes. +Each asynchronous read has a 1-second timeout. A change during a read discards that +snapshot and triggers another read; after changes settle, allow the outstanding +read plus one new read (up to 2 seconds of read time), plus event-loop delivery. +Service/connection loss or failed reads invalidate old values. Service-owner changes +trigger immediate recovery; otherwise failures retry every 5 seconds (then up to +1 second for the read). These timings require a responsive event loop and working +session bus. There is no periodic polling while the portal is healthy. + ## Linux Notes On Linux, QWindowKit can use the Qt fallback window context and provides partial system menu support on Qt 6 Wayland/X11. Behavior varies by desktop environment and window manager. diff --git a/docs/tutorial.zh-CN.md b/docs/tutorial.zh-CN.md index 0ff41694..599778a5 100644 --- a/docs/tutorial.zh-CN.md +++ b/docs/tutorial.zh-CN.md @@ -311,6 +311,14 @@ connect(closeButton, &QPushButton::clicked, window, &QWidget::close); setWindowFlag(Qt::WindowMaximizeButtonHint, false); ``` +在 macOS 和 Qt fallback 上,QWindowKit 自定义标题栏仅在设置了 +`Qt::WindowMaximizeButtonHint`、窗口不是固定尺寸或全屏、且指针位于标题栏可拖拽区域时, +通过左键双击切换最大化/普通状态。清除该标志也会禁止通过双击还原已最大化的窗口。 +Quick 窗口的 `flags` 遵循相同规则。应用代码或自定义按钮处理函数仍可直接修改窗口状态。 + +macOS 自定义标题栏使用上述 QWindowKit 策略,不读取“桌面与程序坞”中的原生标题栏双击 +操作偏好。原生红绿灯按钮与此自定义标题栏行为相互独立。 + 固定大小窗口: ```cpp @@ -529,6 +537,29 @@ windowAgent->setWindowAttribute(QStringLiteral("glass-tint-color"), QColor(255, 如果平台不支持,`setWindowAttribute()` 会返回 `false`。 +## 使用 StyleAgent 获取系统外观 + +启用 `QWINDOWKIT_ENABLE_STYLE_AGENT` 后可使用 `QWK::StyleAgent`。在 GUI 线程中 +创建和使用对象,并保持 Qt 事件循环运行。发出变化信号前会先更新两个 getter, +重复的相同设置不会再次通知。 + +Linux 的 X11 和 Wayland 均仅使用 [XDG Desktop Portal Settings 接口](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Settings.html), +通过 Qt DBus 访问。启用 StyleAgent 会增加 Qt DBus 编译和运行依赖,静态 CMake/qmake +消费端自动获得该依赖;关闭 StyleAgent 后库不需要 Qt DBus。不直接调用 XCB/Wayland, +也不增加桌面或发行版专用回退。桌面的 Portal 后端需要提供对应的外观设置。 + +初始读取是异步的:刚构造时 `systemTheme()` 为 `Unknown`,`systemAccentColor()` +为无效颜色。连接变化信号后读取 getter 来应用后续快照。Portal 的 `color-scheme` +决定深色/浅色,高 `contrast` 优先映射为 `HighContrast`。未指定偏好或缺少主题设置时 +为 `Unknown`,缺少或无效的 `accent-color` 对应无效 QColor;不使用应用自己覆盖的 +palette 冒充系统设置。 + +先订阅再读取,收到变化后重读完整外观快照,每次异步读取超时为 1 秒。读取期间再次 +变化会丢弃该次快照并重读;变化停止后,需预留尚未完成的读取和一次重读(最多 2 秒 +读取时间),另加事件循环投递时间。服务/连接断开或读取失败时清空旧值;服务所有者 +变化会立即触发恢复,其他失败每 5 秒重试一次(读取再最多 1 秒)。这些时限依赖正常 +运行的事件循环和会话总线;Portal 正常时没有周期轮询。 + ## Linux 说明 Linux 下 QWindowKit 优先使用 Qt 窗口上下文,并在 Qt 6 的 Wayland/X11 环境中提供部分系统菜单支持。不同桌面环境和窗口管理器行为差异较大,建议: diff --git a/examples/qml/FramelessWindow.qml b/examples/qml/FramelessWindow.qml index 3404820c..1a419ce5 100644 --- a/examples/qml/FramelessWindow.qml +++ b/examples/qml/FramelessWindow.qml @@ -1,8 +1,7 @@ -import QtQuick 2.15 -import QtQuick.Window 2.15 -import QtQuick.Controls 2.15 -import Qt.labs.platform 1.1 -import QWindowKit 1.0 +import QtQuick +import QtQuick.Controls +import Qt.labs.platform +import QWindowKit Window { property bool showWhenReady: true @@ -15,7 +14,18 @@ Window { color: darkStyle.windowBackgroundColor title: qsTr("QWindowKit QtQuick Demo") Component.onCompleted: { - windowAgent.setup(window) + if (!windowAgent.setup(window)) { + console.error("Failed to set up the window agent.") + return + } + // Keep registration here: Component.onCompleted ordering between objects is undefined. + windowAgent.setTitleBar(titleBar) + if (!window.isMacOS) { + windowAgent.setSystemButton(WindowAgent.WindowIcon, iconButton) + windowAgent.setSystemButton(WindowAgent.Minimize, minButton) + windowAgent.setSystemButton(WindowAgent.Maximize, maxButton) + windowAgent.setSystemButton(WindowAgent.Close, closeButton) + } windowAgent.setWindowAttribute("dark-mode", true) if (window.showWhenReady) { window.visible = true @@ -71,7 +81,6 @@ Window { height: 32 //color: window.active ? "#3C3C3C" : "#505050" color: "transparent" - Component.onCompleted: windowAgent.setTitleBar(titleBar) Image { id: iconButton @@ -86,11 +95,6 @@ Window { mipmap: true source: "qrc:///app/example.png" fillMode: Image.PreserveAspectFit - Component.onCompleted: { - if (!window.isMacOS) { - windowAgent.setSystemButton(WindowAgent.WindowIcon, iconButton) - } - } } Text { @@ -124,11 +128,6 @@ Window { height: parent.height source: "qrc:///window-bar/minimize.svg" onClicked: window.showMinimized() - Component.onCompleted: { - if (!window.isMacOS) { - windowAgent.setSystemButton(WindowAgent.Minimize, minButton) - } - } } QWKButton { @@ -142,11 +141,6 @@ Window { window.showMaximized() } } - Component.onCompleted: { - if (!window.isMacOS) { - windowAgent.setSystemButton(WindowAgent.Maximize, maxButton) - } - } } QWKButton { @@ -168,11 +162,6 @@ Window { } } onClicked: window.close() - Component.onCompleted: { - if (!window.isMacOS) { - windowAgent.setSystemButton(WindowAgent.Close, closeButton) - } - } } } } diff --git a/examples/qml/main.qml b/examples/qml/main.qml index 67331684..d3f4897b 100644 --- a/examples/qml/main.qml +++ b/examples/qml/main.qml @@ -1,6 +1,5 @@ -import QtQuick 2.15 -import QtQuick.Window 2.15 -import QtQuick.Controls 2.15 +import QtQuick +import QtQuick.Controls FramelessWindow { property FramelessWindow childWindow: FramelessWindow { diff --git a/share/install.cmake b/share/install.cmake index 55279934..5df9ac6e 100644 --- a/share/install.cmake +++ b/share/install.cmake @@ -23,10 +23,19 @@ if(TRUE) set(QMAKE_QWK_WIDGETS_NAME_DEBUG QWKWidgets${CMAKE_DEBUG_POSTFIX}) set(QMAKE_QWK_QUICK_NAME_DEBUG QWKQuick${CMAKE_DEBUG_POSTFIX}) + set(QMAKE_QWK_CORE_STATIC_LIBS "") if(QWINDOWKIT_BUILD_STATIC) set(QMAKE_QWK_CORE_STATIC_MACRO "DEFINES += QWK_CORE_STATIC") set(QMAKE_QWK_WIDGETS_STATIC_MACRO "DEFINES += QWK_WIDGETS_STATIC") set(QMAKE_QWK_QUICK_STATIC_MACRO "DEFINES += QWK_QUICK_STATIC") + if(QWINDOWKIT_STYLE_USES_DBUS) + set(QMAKE_QWK_CORE_STATIC_LIBS "QT += dbus") + endif() + if(WIN32) + # Static consumers must resolve QWKCore's native calls themselves. + # Qt's shared import libraries do not propagate these system libraries. + set(QMAKE_QWK_CORE_STATIC_LIBS "LIBS += -luser32 -lgdi32 -lshell32 -luxtheme -ladvapi32") + endif() endif() # Only the modules that were built. A `.pri` naming a library that was never installed sends @@ -37,12 +46,17 @@ if(TRUE) list(APPEND _qmake_components "${CMAKE_CURRENT_LIST_DIR}/qmake/${_target}.pri.in") endforeach() + set(_qmake_files) foreach(_item IN LISTS _qmake_components) get_filename_component(_name ${_item} NAME_WLE) - configure_file(${_item} ${_build_data_dir}/qmake/${_name} @ONLY) + set(_file "${_build_data_dir}/qmake/${_name}") + configure_file("${_item}" "${_file}" @ONLY) + list(APPEND _qmake_files "${_file}") endforeach() - install(DIRECTORY ${_build_data_dir}/qmake/ + # A reconfigured build may still contain files for disabled modules. + # Install exactly this configure's outputs, leaving historical outputs alone. + install(FILES ${_qmake_files} DESTINATION ${_qmake_install_dir} ) endif() @@ -79,9 +93,10 @@ if(MSVC) to_dos_separator(MSBUILD_QWK_INSTALL_LIBDIR) to_dos_separator(MSBUILD_QWK_INSTALL_INCDIR) + set(MSBUILD_QWK_STATIC_MACRO "") if(QWINDOWKIT_BUILD_STATIC) - set(MSBUILD_QWK_STATIC_MACRO - "QWK_CORE_STATIC;QWK_WIDGETS_STATIC;QWK_QUICK_STATIC" + set(MSBUILD_QWK_STATIC_MACRO + "QWK_CORE_STATIC;QWK_WIDGETS_STATIC;QWK_QUICK_STATIC;" ) endif() @@ -93,4 +108,4 @@ if(MSVC) install(DIRECTORY ${_build_data_dir}/msbuild/ DESTINATION ${_msbuild_install_dir} ) -endif() \ No newline at end of file +endif() diff --git a/share/msbuild/QWindowKit.props.in b/share/msbuild/QWindowKit.props.in index 53282a03..f5dd24b6 100644 --- a/share/msbuild/QWindowKit.props.in +++ b/share/msbuild/QWindowKit.props.in @@ -8,14 +8,11 @@ - %(PreprocessorDefinitions) + @MSBUILD_QWK_STATIC_MACRO@%(PreprocessorDefinitions) @MSBUILD_QWK_LIBRARY_LIST_DEBUG@;%(AdditionalDependencies); @MSBUILD_QWK_LIBRARY_LIST_RELEASE@;%(AdditionalDependencies); - - @MSBUILD_QWK_STATIC_MACRO@ - diff --git a/share/qmake/QWKCore.pri.in b/share/qmake/QWKCore.pri.in index 4ef27734..46596c39 100644 --- a/share/qmake/QWKCore.pri.in +++ b/share/qmake/QWKCore.pri.in @@ -21,4 +21,5 @@ } @QMAKE_QWK_CORE_STATIC_MACRO@ -} \ No newline at end of file + @QMAKE_QWK_CORE_STATIC_LIBS@ +} diff --git a/share/qmake/QWKQuick.pri.in b/share/qmake/QWKQuick.pri.in index 24399997..3981e393 100644 --- a/share/qmake/QWKQuick.pri.in +++ b/share/qmake/QWKQuick.pri.in @@ -10,4 +10,9 @@ } @QMAKE_QWK_QUICK_STATIC_MACRO@ -} \ No newline at end of file + + # Qt Creator's QML code model also needs the installed module metadata. + exists($$QMAKE_QWK_INSTALL_PREFIX/qml/QWindowKit/qmldir) { + QML_IMPORT_PATH += $$QMAKE_QWK_INSTALL_PREFIX/qml + } +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 37eb9908..129669c6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -73,7 +73,19 @@ endif() # ---------------------------------- # Install # ---------------------------------- -qwk_install() +# Keep the helper's header installation, but generate our component-aware package +# without its NO_CHECK_REQUIRED_COMPONENTS_MACRO option. +qwk_install(NO_EXPORT) +if(QWINDOWKIT_INSTALL AND QWINDOWKIT_DEVEL) + qm_basic_install( + NAME ${QWINDOWKIT_INSTALL_NAME} + VERSION ${QWINDOWKIT_INSTALL_VERSION} + INSTALL_DIR ${QWINDOWKIT_INSTALL_CMAKE_DIR} + CONFIG_TEMPLATE ${QWINDOWKIT_INSTALL_CONFIG_TEMPLATE} + NAMESPACE ${QWINDOWKIT_INSTALL_NAMESPACE}:: + EXPORT ${QWINDOWKIT_EXPORT} + ) +endif() qwk_finish_buildsystem() diff --git a/src/QWindowKitConfig.cmake.in b/src/QWindowKitConfig.cmake.in index 51d8a21d..1220df9a 100644 --- a/src/QWindowKitConfig.cmake.in +++ b/src/QWindowKitConfig.cmake.in @@ -1,18 +1,100 @@ @PACKAGE_INIT@ +set(QWindowKit_FOUND TRUE) +unset(QWindowKit_NOT_FOUND_MESSAGE) +set(QWindowKit_AVAILABLE_COMPONENTS Core) +set(QWindowKit_Core_FOUND TRUE) +set(QWindowKit_Widgets_FOUND "@QWINDOWKIT_BUILD_WIDGETS@") +set(QWindowKit_Quick_FOUND "@QWINDOWKIT_BUILD_QUICK@") +foreach(_qwk_component Widgets Quick) + if(QWindowKit_${_qwk_component}_FOUND) + list(APPEND QWindowKit_AVAILABLE_COMPONENTS "${_qwk_component}") + endif() +endforeach() + +# Reject missing QWindowKit components before looking for their Qt dependencies. +# Optional components that were not installed must not make the package fail. +set(_qwk_missing_components) +foreach(_qwk_component IN LISTS QWindowKit_FIND_COMPONENTS) + if(NOT _qwk_component IN_LIST QWindowKit_AVAILABLE_COMPONENTS) + set(QWindowKit_${_qwk_component}_FOUND FALSE) + if(QWindowKit_FIND_REQUIRED_${_qwk_component}) + list(APPEND _qwk_missing_components "${_qwk_component}") + endif() + endif() +endforeach() +check_required_components(QWindowKit) +if(NOT QWindowKit_FOUND) + set(QWindowKit_NOT_FOUND_MESSAGE + "Unavailable QWindowKit components: ${_qwk_missing_components}. Installed components: ${QWindowKit_AVAILABLE_COMPONENTS}") + unset(_qwk_missing_components) + unset(_qwk_component) + return() +endif() +unset(_qwk_missing_components) + +# Resolve before find_dependency(), which can change PACKAGE_PREFIX_DIR on older CMake. +if("@QWINDOWKIT_HAS_QML_MODULE@") + set_and_check(QWindowKit_QML_IMPORT_PATH "${PACKAGE_PREFIX_DIR}/qml") +endif() + include(CMakeFindDependencyMacro) -find_dependency(QT NAMES Qt6 Qt5 COMPONENTS Core Gui REQUIRED) -find_dependency(Qt${QT_VERSION_MAJOR} COMPONENTS Core Gui REQUIRED) +# The exported targets name the Qt major used to build this package. Do not +# reselect it from the consumer's search order or ambient QT_VERSION_MAJOR. +set(QWindowKit_QT_VERSION_MAJOR @QT_VERSION_MAJOR@) +find_dependency(Qt@QT_VERSION_MAJOR@ COMPONENTS Core Gui) +if("@QWINDOWKIT_BUILD_STATIC@" AND "@QWINDOWKIT_STYLE_USES_DBUS@") + find_dependency(Qt@QT_VERSION_MAJOR@ COMPONENTS DBus) +endif() -if ("Widgets" IN_LIST QWindowKit_FIND_COMPONENTS) - find_dependency(QT NAMES Qt6 Qt5 COMPONENTS Widgets REQUIRED) - find_dependency(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets REQUIRED) +# Static archives expose their private Qt link dependencies to consumers. Recent +# Qt 6 releases put these targets in separate packages instead of loading them +# with the public components. +if("@QWINDOWKIT_BUILD_STATIC@" AND @QT_VERSION_MAJOR@ EQUAL 6) + foreach(_module Core Gui) + if(NOT TARGET Qt6::${_module}Private) + find_dependency(Qt6 COMPONENTS ${_module}Private) + endif() + endforeach() + unset(_module) endif() -if ("Quick" IN_LIST QWindowKit_FIND_COMPONENTS) - find_dependency(QT NAMES Qt6 Qt5 COMPONENTS Quick REQUIRED) - find_dependency(Qt${QT_VERSION_MAJOR} COMPONENTS Quick REQUIRED) +# With no explicit selection, make all installed exports usable. Otherwise resolve +# only requested components; the monolithic export still declares all installed targets. +set(_qwk_components "${QWindowKit_FIND_COMPONENTS}") +if(NOT _qwk_components) + set(_qwk_components "${QWindowKit_AVAILABLE_COMPONENTS}") endif() +foreach(_qwk_component Widgets Quick) + if(NOT _qwk_component IN_LIST _qwk_components OR NOT QWindowKit_${_qwk_component}_FOUND) + continue() + endif() + set(QWindowKit_${_qwk_component}_FOUND FALSE) + if(NOT QWindowKit_FIND_COMPONENTS OR QWindowKit_FIND_REQUIRED_${_qwk_component}) + find_dependency(Qt@QT_VERSION_MAJOR@ COMPONENTS ${_qwk_component}) + if("@QWINDOWKIT_BUILD_STATIC@" AND @QT_VERSION_MAJOR@ EQUAL 6 AND + NOT TARGET Qt6::${_qwk_component}Private) + find_dependency(Qt6 COMPONENTS ${_qwk_component}Private) + endif() + else() + # find_dependency would mark the whole package unavailable on an optional miss. + find_package(Qt@QT_VERSION_MAJOR@ QUIET COMPONENTS ${_qwk_component}) + if(NOT Qt@QT_VERSION_MAJOR@_FOUND) + continue() + endif() + if("@QWINDOWKIT_BUILD_STATIC@" AND @QT_VERSION_MAJOR@ EQUAL 6 AND + NOT TARGET Qt6::${_qwk_component}Private) + find_package(Qt6 QUIET COMPONENTS ${_qwk_component}Private) + if(NOT Qt6_FOUND) + continue() + endif() + endif() + endif() + set(QWindowKit_${_qwk_component}_FOUND TRUE) +endforeach() +unset(_qwk_components) +unset(_qwk_component) -include("${CMAKE_CURRENT_LIST_DIR}/QWindowKitTargets.cmake") \ No newline at end of file +include("${CMAKE_CURRENT_LIST_DIR}/QWindowKitTargets.cmake") +check_required_components(QWindowKit) diff --git a/src/conf.cmake b/src/conf.cmake index baedbae3..f71d1540 100644 --- a/src/conf.cmake +++ b/src/conf.cmake @@ -82,6 +82,13 @@ qm_setup_build_repo_helpers(qwk) # `Qt5Gui_PRIVATE_INCLUDE_DIRS` only on the call that creates the targets. qm_find_qt(Core Gui) +# The optional Linux StyleAgent uses the standard XDG Settings portal only. +set(QWINDOWKIT_STYLE_USES_DBUS OFF) +if(UNIX AND NOT APPLE AND QWINDOWKIT_ENABLE_STYLE_AGENT) + qm_find_qt(DBus) + set(QWINDOWKIT_STYLE_USES_DBUS ON) +endif() + if(QWINDOWKIT_BUILD_WIDGETS) qm_find_qt(Widgets) endif() diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 4bf684d8..a2b9f552 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -12,6 +12,7 @@ set(_src windowagentbase.cpp windowitemdelegate_p.h windowitemdelegate.cpp + kernel/eventdispatch_p.h kernel/nativeeventfilter_p.h kernel/nativeeventfilter.cpp kernel/sharedeventfilter_p.h @@ -26,6 +27,7 @@ set(_src ) set(_links_private) +set(_qt_links_private) if(WIN32) list(APPEND _src @@ -37,8 +39,10 @@ if(WIN32) elseif(APPLE) else() list(APPEND _src - qwindowkit_linux.h - qwindowkit_linux.cpp + qwindowkit_x11.h + qwindowkit_x11.cpp + qwindowkit_wayland.h + qwindowkit_wayland.cpp ) endif() @@ -47,7 +51,7 @@ if(WIN32) contexts/win32windowcontext_p.h contexts/win32windowcontext.cpp ) - list(APPEND _links_private uxtheme) + list(APPEND _links_private uxtheme advapi32) elseif(APPLE) list(APPEND _src contexts/cocoawindowcontext_p.h @@ -60,10 +64,10 @@ elseif(APPLE) ) else() list(APPEND _src - contexts/linuxx11context_p.h - contexts/linuxx11context.cpp - contexts/linuxwaylandcontext_p.h - contexts/linuxwaylandcontext.cpp + contexts/x11context_p.h + contexts/x11context.cpp + contexts/waylandcontext_p.h + contexts/waylandcontext.cpp ) endif() @@ -73,6 +77,7 @@ if(QWINDOWKIT_ENABLE_STYLE_AGENT) list(APPEND _src style/styleagent.h style/styleagent_p.h + style/styleagentregistry_p.h style/styleagent.cpp ) @@ -81,7 +86,9 @@ if(QWINDOWKIT_ENABLE_STYLE_AGENT) elseif(APPLE) list(APPEND _src style/styleagent_mac.mm) else() - list(APPEND _src style/styleagent_linux.cpp) + list(APPEND _src style/styleagent_linux.cpp + style/portalsettings_p.h style/portalsettings.cpp style/portalsettings_dbus.cpp) + list(APPEND _qt_links_private DBus) endif() else() list(APPEND _sync_include_options EXCLUDE "src/core/style/.+") @@ -92,6 +99,7 @@ qwk_add_library(${PROJECT_NAME} QT_AUTOGEN FEATURES cxx_std_17 LINKS_PRIVATE ${_links_private} QT_LINKS Core Gui + QT_LINKS_PRIVATE ${_qt_links_private} QT_INCLUDE_PRIVATE Core Gui INCLUDE_PRIVATE * MACRO_PREFIX QWK_CORE @@ -99,5 +107,10 @@ qwk_add_library(${PROJECT_NAME} QT_AUTOGEN qwk_sync_include(${PROJECT_NAME} FORCE ${_sync_include_options}) +# QWKQuick's type registrar needs the inherited methods and SystemButton enum. +if(QWINDOWKIT_BUILD_QUICK AND QT_VERSION VERSION_GREATER_EQUAL 6.3) + qt6_extract_metatypes(${PROJECT_NAME}) +endif() + set(QWINDOWKIT_ENABLED_TARGETS ${QWINDOWKIT_ENABLED_TARGETS} ${PROJECT_NAME} PARENT_SCOPE) set(QWINDOWKIT_ENABLED_SUBDIRECTORIES ${QWINDOWKIT_ENABLED_SUBDIRECTORIES} core PARENT_SCOPE) diff --git a/src/core/contexts/abstractwindowcontext.cpp b/src/core/contexts/abstractwindowcontext.cpp index ded9614b..a7ec8171 100644 --- a/src/core/contexts/abstractwindowcontext.cpp +++ b/src/core/contexts/abstractwindowcontext.cpp @@ -4,18 +4,33 @@ #include "abstractwindowcontext_p.h" -#include -#include #include -#include "qwkglobal_p.h" - namespace QWK { AbstractWindowContext::AbstractWindowContext() = default; AbstractWindowContext::~AbstractWindowContext() = default; + AbstractWindowContext::AttributeChange::AttributeChange(AbstractWindowContext *ctx, + const QString &attributeKey) + : context(ctx), key(attributeKey), windowRevision(ctx->m_windowRevision), + previous(ctx->m_attributeChange) { + ctx->m_attributeChange = this; + } + + AbstractWindowContext::AttributeChange::~AttributeChange() { + if (context) + context->m_attributeChange = previous; + } + + void AbstractWindowContext::AttributeChange::supersedePrevious() { + for (auto frame = previous; frame; frame = frame->previous) { + if (frame->key == key) + frame->superseded = true; + } + } + void AbstractWindowContext::setup(QObject *host, WindowItemDelegate *delegate) { if (m_host || !host || !delegate) { return; @@ -48,8 +63,7 @@ namespace QWK { bool AbstractWindowContext::setSystemButton(WindowAgentBase::SystemButton button, QObject *obj) { - Q_ASSERT(button != WindowAgentBase::Unknown); - if (button == WindowAgentBase::Unknown) { + if (!isValidSystemButton(button)) { return false; } @@ -68,18 +82,20 @@ namespace QWK { return false; } - if (org) { - // Since the title bar is changed, all items inside it should be dereferenced right away + if (m_titleBarAssigned) { + // QPointer is already null after the old title is destroyed, but its + // registered buttons/exclusions may still live elsewhere in the window. removeSystemButtonsAndHitTestItems(); } m_titleBar = item; + m_titleBarAssigned = true; return true; } #ifdef Q_OS_MAC void AbstractWindowContext::setSystemButtonAreaCallback(const ScreenRectCallback &callback) { m_systemButtonAreaCallback = callback; - virtual_hook(SystemButtonAreaChangedHook, nullptr); + updateSystemButtonArea(); } #endif @@ -91,7 +107,7 @@ namespace QWK { if (!currentButton || !m_delegate->isVisible(currentButton) || !m_delegate->isEnabled(currentButton)) { continue; } - if (m_delegate->mapGeometryToScene(currentButton).contains(pos)) { + if (m_delegate->containsScenePoint(currentButton, pos)) { *button = static_cast(i); return true; } @@ -117,7 +133,7 @@ namespace QWK { return false; } - if (!titleBarRect.contains(pos)) { + if (!m_delegate->containsScenePoint(m_titleBar, pos)) { return false; } @@ -128,7 +144,7 @@ namespace QWK { for (auto &&item : std::as_const(m_hitTestVisibleItems)) { if (item && m_delegate->isVisible(item) && - m_delegate->mapGeometryToScene(item).contains(pos)) { + m_delegate->containsScenePoint(item, pos)) { return false; } } @@ -139,62 +155,51 @@ namespace QWK { return {}; } - QWK_USED static constexpr const struct { - const quint32 activeLight = MAKE_RGBA_COLOR(210, 233, 189, 226); - const quint32 activeDark = MAKE_RGBA_COLOR(177, 205, 190, 240); - const quint32 inactiveLight = MAKE_RGBA_COLOR(193, 195, 211, 203); - const quint32 inactiveDark = MAKE_RGBA_COLOR(240, 240, 250, 255); - } kSampleColorSet; - - void AbstractWindowContext::virtual_hook(int id, void *data) { - switch (id) { - case CentralizeHook: { - if (!m_windowId) - return; - - QRect windowGeometry = m_delegate->getGeometry(m_host); - QRect screenGeometry = m_windowHandle->screen()->geometry(); - int x = (screenGeometry.width() - windowGeometry.width()) / 2; - int y = (screenGeometry.height() - windowGeometry.height()) / 2; - QPoint pos(x, y); - pos += screenGeometry.topLeft(); - m_delegate->setGeometry(m_host, QRect(pos, windowGeometry.size())); - return; - } - - case RaiseWindowHook: { - if (!m_windowId) - return; + void AbstractWindowContext::centralizeWindow() { + if (!m_windowId) + return; - m_delegate->setWindowVisible(m_host, true); - Qt::WindowStates state = m_delegate->getWindowState(m_host); - if (state & Qt::WindowMinimized) { - m_delegate->setWindowState(m_host, state & ~Qt::WindowMinimized); - } - m_delegate->bringWindowToTop(m_host); - return; - } + QRect windowGeometry = m_delegate->getGeometry(m_host); + QRect screenGeometry = m_windowHandle->screen()->geometry(); + int x = (screenGeometry.width() - windowGeometry.width()) / 2; + int y = (screenGeometry.height() - windowGeometry.height()) / 2; + QPoint pos(x, y); + pos += screenGeometry.topLeft(); + m_delegate->setGeometry(m_host, QRect(pos, windowGeometry.size())); + } - case DefaultColorsHook: { - auto &map = *static_cast *>(data); - map.clear(); - map.insert(QStringLiteral("activeLight"), kSampleColorSet.activeLight); - map.insert(QStringLiteral("activeDark"), kSampleColorSet.activeDark); - map.insert(QStringLiteral("inactiveLight"), kSampleColorSet.inactiveLight); - map.insert(QStringLiteral("inactiveDark"), kSampleColorSet.inactiveDark); - return; - } + void AbstractWindowContext::raiseWindow() { + if (!m_windowId) + return; - default: - break; + m_delegate->setWindowVisible(m_host, true); + Qt::WindowStates state = m_delegate->getWindowState(m_host); + if (state & Qt::WindowMinimized) { + m_delegate->setWindowState(m_host, state & ~Qt::WindowMinimized); } + m_delegate->bringWindowToTop(m_host); } void AbstractWindowContext::showSystemMenu(const QPoint &pos) { - virtual_hook(ShowSystemMenuHook, &const_cast(pos)); + Q_UNUSED(pos) + } + +#ifdef Q_OS_MAC + void AbstractWindowContext::updateSystemButtonArea() {} +#endif + +#ifdef Q_OS_WINDOWS + QColor AbstractWindowContext::windows10BorderColor() const { return {}; } + + void AbstractWindowContext::setWindows10BorderActive(bool active) { + Q_UNUSED(active) } + void AbstractWindowContext::drawWindows10Border() {} +#endif + void AbstractWindowContext::notifyWinIdChange() { + const QPointer self(this); auto oldWinId = m_windowId; m_windowId = m_winIdChangeEventFilter->winId(); @@ -202,6 +207,7 @@ namespace QWK { // platform window will be removed, and the WinId will be set to 0. After that, when the // QWidget is shown again, the whole things will be recreated again. // As a result, we must update our WindowContext each time the WinId changes. + const auto oldWindow = m_windowHandle; if (m_windowHandle) { m_windowHandle->removeEventFilter(this); } @@ -210,19 +216,37 @@ namespace QWK { m_windowHandle->installEventFilter(this); } + if (oldWinId != m_windowId || oldWindow != m_windowHandle) + ++m_windowRevision; + if (oldWinId != m_windowId) { + const auto revision = m_windowRevision; winIdChanged(m_windowId, oldWinId); + if (!self || m_windowRevision != revision) + return; if (m_windowId) { - // Refresh window attributes - for (auto it = m_windowAttributesOrder.begin(); - it != m_windowAttributesOrder.end();) { - if (!windowAttributeChanged(it->first, it->second, {})) { - m_windowAttributes.remove(it->first); - it = m_windowAttributesOrder.erase(it); + // Each original revision is replayed at most once. Updates made by + // callbacks apply themselves; never replay a superseded snapshot. + const auto attributes = m_windowAttributesOrder; + for (const auto &attribute : attributes) { + auto it = m_windowAttributes.constFind(attribute.key); + if (it == m_windowAttributes.cend() || + it.value()->revision != attribute.revision) continue; + AttributeChange change(this, attribute.key); + const bool accepted = windowAttributeChanged(attribute.key, attribute.value); + if (!self || m_windowRevision != revision) + return; + if (!accepted && change.isCurrent()) { + // Re-find after the callback: it may rehash, erase or splice. + it = m_windowAttributes.constFind(attribute.key); + if (it != m_windowAttributes.cend() && + it.value()->revision == attribute.revision) { + m_windowAttributesOrder.erase(it.value()); + m_windowAttributes.remove(attribute.key); + } } - ++it; } } @@ -237,38 +261,44 @@ namespace QWK { if (it == m_windowAttributes.end()) { return {}; } - return it.value()->second; + return it.value()->value; } bool AbstractWindowContext::setWindowAttribute(const QString &key, const QVariant &attribute) { - auto it = m_windowAttributes.find(key); - if (it == m_windowAttributes.end()) { - if (!attribute.isValid()) { - return true; - } - if (m_windowId && !windowAttributeChanged(key, attribute, {})) { - return false; - } - m_windowAttributes.insert( - key, m_windowAttributesOrder.insert(m_windowAttributesOrder.end(), - std::make_pair(key, attribute))); + // Copy inputs: callers may pass references into + // storage that a synchronous platform callback changes or destroys. + const QString name = key; + const QVariant value = attribute; + auto it = m_windowAttributes.constFind(name); + const bool existed = it != m_windowAttributes.cend(); + AttributeChange change(this, name); + if (!existed && !value.isValid()) { + change.supersedePrevious(); return true; } - - auto &listIter = it.value(); - auto &oldAttr = listIter->second; - if (m_windowId && !windowAttributeChanged(key, attribute, oldAttr)) { + if (m_windowId && !windowAttributeChanged(name, value)) + return false; + if (!change.isCurrent()) return false; - } - if (attribute.isValid()) { - oldAttr = attribute; - m_windowAttributesOrder.splice(m_windowAttributesOrder.end(), m_windowAttributesOrder, - listIter); - } else { - m_windowAttributesOrder.erase(listIter); - m_windowAttributes.erase(it); + it = m_windowAttributes.constFind(name); + if (value.isValid()) { + const auto revision = ++m_attributeRevision; + if (it == m_windowAttributes.cend()) { + m_windowAttributes.insert(name, m_windowAttributesOrder.insert( + m_windowAttributesOrder.end(), {name, value, revision})); + } else { + const auto listIter = it.value(); + listIter->value = value; + listIter->revision = revision; + m_windowAttributesOrder.splice(m_windowAttributesOrder.end(), m_windowAttributesOrder, + listIter); + } + } else if (it != m_windowAttributes.cend()) { + m_windowAttributesOrder.erase(it.value()); + m_windowAttributes.remove(name); } + change.supersedePrevious(); return true; } @@ -280,8 +310,7 @@ namespace QWK { } bool AbstractWindowContext::windowAttributeChanged(const QString &key, - const QVariant &attribute, - const QVariant &oldAttribute) { + const QVariant &attribute) { return false; } diff --git a/src/core/contexts/abstractwindowcontext_p.h b/src/core/contexts/abstractwindowcontext_p.h index 88db7f16..c20d8bd9 100644 --- a/src/core/contexts/abstractwindowcontext_p.h +++ b/src/core/contexts/abstractwindowcontext_p.h @@ -21,7 +21,7 @@ #include #include -#include +#include #include #include @@ -52,6 +52,9 @@ namespace QWK { bool setHitTestVisible(QObject *obj, bool visible); inline QObject *systemButton(WindowAgentBase::SystemButton button) const; + static constexpr bool isValidSystemButton(WindowAgentBase::SystemButton button) { + return button >= WindowAgentBase::WindowIcon && button <= WindowAgentBase::Close; + } bool setSystemButton(WindowAgentBase::SystemButton button, QObject *obj); inline QObject *titleBar() const; @@ -71,18 +74,18 @@ namespace QWK { virtual QString key() const; - enum WindowContextHook { - CentralizeHook = 1, - RaiseWindowHook, - ShowSystemMenuHook, - DefaultColorsHook, - DrawWindows10BorderHook_Emulated, // Only works on Windows 10, emulated workaround - DrawWindows10BorderHook_Native, // Only works on Windows 10, native workaround - SystemButtonAreaChangedHook, // Only works on Mac - }; - virtual void virtual_hook(int id, void *data); - - void showSystemMenu(const QPoint &pos); + virtual void centralizeWindow(); + virtual void raiseWindow(); + virtual void showSystemMenu(const QPoint &pos); +#ifdef Q_OS_MAC + virtual void updateSystemButtonArea(); +#endif +#ifdef Q_OS_WINDOWS + virtual QColor windows10BorderColor() const; + // Temporary DWM activation state; does not update application attributes. + virtual void setWindows10BorderActive(bool active); + virtual void drawWindows10Border(); +#endif void notifyWinIdChange(); virtual QVariant windowAttribute(const QString &key) const; @@ -93,8 +96,7 @@ namespace QWK { protected: virtual void winIdChanged(WId winId, WId oldWinId) = 0; - virtual bool windowAttributeChanged(const QString &key, const QVariant &attribute, - const QVariant &oldAttribute); + virtual bool windowAttributeChanged(const QString &key, const QVariant &attribute); protected: QObject *m_host{}; @@ -108,11 +110,39 @@ namespace QWK { #endif QPointer m_titleBar{}; + bool m_titleBarAssigned = false; std::array, WindowAgentBase::Close + 1> m_systemButtons{}; - std::list> m_windowAttributesOrder; + struct WindowAttribute { + QString key; + QVariant value; + quint64 revision; + }; + std::list m_windowAttributesOrder; QHash m_windowAttributes; + // Frames live on the caller's stack, including when a callback deletes us. + struct AttributeChange { + AttributeChange(AbstractWindowContext *context, const QString &key); + ~AttributeChange(); + AttributeChange(const AttributeChange &) = delete; + AttributeChange &operator=(const AttributeChange &) = delete; + bool isWindowCurrent() const { + return context && context->m_windowRevision == windowRevision; + } + bool isCurrent() const { return !superseded && isWindowCurrent(); } + void supersedePrevious(); + + QPointer context; + QString key; + quint64 windowRevision; + AttributeChange *previous; + bool superseded = false; + }; + AttributeChange *m_attributeChange = nullptr; + quint64 m_attributeRevision = 0; + quint64 m_windowRevision = 0; + std::unique_ptr m_winIdChangeEventFilter; void removeSystemButtonsAndHitTestItems(); @@ -140,7 +170,7 @@ namespace QWK { inline QObject * AbstractWindowContext::systemButton(WindowAgentBase::SystemButton button) const { - return m_systemButtons[button]; + return isValidSystemButton(button) ? m_systemButtons[button].data() : nullptr; } inline QObject *AbstractWindowContext::titleBar() const { diff --git a/src/core/contexts/cocoawindowcontext.mm b/src/core/contexts/cocoawindowcontext.mm index 2d470340..69350fd7 100644 --- a/src/core/contexts/cocoawindowcontext.mm +++ b/src/core/contexts/cocoawindowcontext.mm @@ -299,7 +299,12 @@ void updateSystemButtonRect() { auto height = refButton.frame.size.height; auto viewSize = nsview.frame.size; - QPoint center = screenRectCallback(QSize(viewSize.width, titlebarHeight)).center(); + const QRect area = screenRectCallback(QSize(viewSize.width, titlebarHeight)); + // A Quick area can be detached, destroyed or moved to another window. + // Keep the existing native layout until the callback has a usable host area. + if (area.isEmpty()) + return; + QPoint center = area.center(); // The origin of the NSWindow coordinate system is in the lower left corner, we // do the necessary transformations @@ -660,18 +665,6 @@ static void replaceImplementations(Class nativeViewClass) { reinterpret_cast(method_setImplementation( method, reinterpret_cast(setTitlebarAppearsTransparent))); -#if 0 - method = class_getInstanceMethod(windowClass, @selector(canBecomeKeyWindow)); - oldCanBecomeKeyWindow = reinterpret_cast(method_setImplementation(method, reinterpret_cast(canBecomeKeyWindow))); - - method = class_getInstanceMethod(windowClass, @selector(canBecomeMainWindow)); - oldCanBecomeMainWindow = reinterpret_cast(method_setImplementation(method, reinterpret_cast(canBecomeMainWindow))); -#endif - - method = class_getInstanceMethod(windowClass, @selector(sendEvent:)); - oldSendEvent = reinterpret_cast( - method_setImplementation(method, reinterpret_cast(sendEvent))); - viewClass = nativeViewClass; method = class_getInstanceMethod(viewClass, @selector(mouseDownCanMoveWindow)); oldMouseDownCanMoveWindow = reinterpret_cast(method_getImplementation(method)); @@ -693,20 +686,6 @@ static void restoreImplementations() { reinterpret_cast(oldSetTitlebarAppearsTransparent)); oldSetTitlebarAppearsTransparent = nil; -#if 0 - method = class_getInstanceMethod(windowClass, @selector(canBecomeKeyWindow)); - method_setImplementation(method, reinterpret_cast(oldCanBecomeKeyWindow)); - oldCanBecomeKeyWindow = nil; - - method = class_getInstanceMethod(windowClass, @selector(canBecomeMainWindow)); - method_setImplementation(method, reinterpret_cast(oldCanBecomeMainWindow)); - oldCanBecomeMainWindow = nil; -#endif - - method = class_getInstanceMethod(windowClass, @selector(sendEvent:)); - method_setImplementation(method, reinterpret_cast(oldSendEvent)); - oldSendEvent = nil; - method = class_getInstanceMethod(viewClass, @selector(mouseDownCanMoveWindow)); class_replaceMethod(viewClass, @selector(mouseDownCanMoveWindow), reinterpret_cast(oldMouseDownCanMoveWindow), @@ -731,34 +710,6 @@ static BOOL mouseDownCanMoveWindow(id obj, SEL sel) { return oldMouseDownCanMoveWindow(obj, sel); } - static BOOL canBecomeKeyWindow(id obj, SEL sel) { - auto nswindow = reinterpret_cast(obj); - auto nsview = [nswindow contentView]; - if (g_proxyList->contains(reinterpret_cast(nsview))) { - return YES; - } - - if (oldCanBecomeKeyWindow) { - return oldCanBecomeKeyWindow(obj, sel); - } - - return YES; - } - - static BOOL canBecomeMainWindow(id obj, SEL sel) { - auto nswindow = reinterpret_cast(obj); - auto nsview = [nswindow contentView]; - if (g_proxyList->contains(reinterpret_cast(nsview))) { - return YES; - } - - if (oldCanBecomeMainWindow) { - return oldCanBecomeMainWindow(obj, sel); - } - - return YES; - } - static void setStyleMask(id obj, SEL sel, NSWindowStyleMask styleMask) { auto nswindow = reinterpret_cast(obj); auto nsview = [nswindow contentView]; @@ -783,27 +734,6 @@ static void setTitlebarAppearsTransparent(id obj, SEL sel, BOOL transparent) { } } - static void sendEvent(id obj, SEL sel, NSEvent *event) { - if (oldSendEvent) { - oldSendEvent(obj, sel, event); - } - -#if 0 - const auto nswindow = reinterpret_cast(obj); - const auto it = instances.find(nswindow); - if (it == instances.end()) { - return; - } - - NSWindowProxy *proxy = it.value(); - if (event.type == NSEventTypeLeftMouseDown) { - proxy->lastMouseDownEvent = event; - QCoreApplication::processEvents(); - proxy->lastMouseDownEvent = nil; - } -#endif - } - private: Q_DISABLE_COPY(NSWindowProxy) @@ -827,32 +757,16 @@ static void sendEvent(id obj, SEL sel, NSEvent *event) { static inline QWK_NSWindowObserver *windowObserver = nil; - // NSEvent *lastMouseDownEvent = nil; - using setStyleMaskPtr = void (*)(id, SEL, NSWindowStyleMask); static inline setStyleMaskPtr oldSetStyleMask = nil; using setTitlebarAppearsTransparentPtr = void (*)(id, SEL, BOOL); static inline setTitlebarAppearsTransparentPtr oldSetTitlebarAppearsTransparent = nil; - using canBecomeKeyWindowPtr = BOOL (*)(id, SEL); - static inline canBecomeKeyWindowPtr oldCanBecomeKeyWindow = nil; - - using canBecomeMainWindowPtr = BOOL (*)(id, SEL); - static inline canBecomeMainWindowPtr oldCanBecomeMainWindow = nil; - - using sendEventPtr = void (*)(id, SEL, NSEvent *); - static inline sendEventPtr oldSendEvent = nil; - using mouseDownCanMoveWindowPtr = BOOL (*)(id, SEL); static inline mouseDownCanMoveWindowPtr oldMouseDownCanMoveWindow = nil; }; - static inline NSWindow *mac_getNSWindow(const WId windowId) { - const auto nsview = reinterpret_cast(windowId); - return [nsview window]; - } - static inline NSWindowProxy *ensureWindowProxy(const WId windowId) { NSView *nsview = reinterpret_cast(windowId); if (g_proxyList->isEmpty()) { @@ -900,12 +814,11 @@ static inline void releaseWindowProxy(const WId windowId) { private: AbstractWindowContext *m_context; - bool m_cursorShapeChanged; WindowStatus m_windowStatus; }; CocoaWindowEventFilter::CocoaWindowEventFilter(AbstractWindowContext *context) - : m_context(context), m_cursorShapeChanged(false), m_windowStatus(Idle) { + : m_context(context), m_windowStatus(Idle) { m_context->installSharedEventFilter(this); } @@ -996,7 +909,9 @@ static inline void releaseWindowProxy(const WId windowId) { if (me->button() == Qt::LeftButton && inTitleBar && !m_context->isHostSizeFixed()) { Qt::WindowFlags windowFlags = delegate->getWindowFlags(host); Qt::WindowStates windowState = delegate->getWindowState(host); - if (!(windowState & Qt::WindowFullScreen)) { + // Match the documented maximize opt-out and the Qt fallback policy. + if ((windowFlags & Qt::WindowMaximizeButtonHint) && + !(windowState & Qt::WindowFullScreen)) { if (windowState & Qt::WindowMaximized) { delegate->setWindowState(host, windowState & ~Qt::WindowMaximized); } else { @@ -1027,26 +942,12 @@ static inline void releaseWindowProxy(const WId windowId) { return QStringLiteral("cocoa"); } - void CocoaWindowContext::virtual_hook(int id, void *data) { - switch (id) { - case SystemButtonAreaChangedHook: { - // This hook fires whenever the callback is set or the system button area item - // moves or resizes, and both happen freely before the window has a native - // handle. Calling ensureWindowProxy() with a null WId would swizzle against a - // nil view class and park a bogus proxy in g_proxyList, which then never gets - // empty again and prevents the real window from ever being set up. The callback - // is kept in m_systemButtonAreaCallback and applied by winIdChanged() as soon as - // the window exists, so there is nothing to do here yet. - if (!m_windowId) - return; - ensureWindowProxy(m_windowId)->setScreenRectCallback(m_systemButtonAreaCallback); - return; - } - - default: - break; - } - AbstractWindowContext::virtual_hook(id, data); + void CocoaWindowContext::updateSystemButtonArea() { + // The callback or area can change before the window has a native handle. + // Keep the callback for winIdChanged() without creating a proxy for a nil view. + if (!m_windowId) + return; + ensureWindowProxy(m_windowId)->setScreenRectCallback(m_systemButtonAreaCallback); } QVariant CocoaWindowContext::windowAttribute(const QString &key) const { @@ -1077,10 +978,7 @@ static inline void releaseWindowProxy(const WId windowId) { } } - bool CocoaWindowContext::windowAttributeChanged(const QString &key, const QVariant &attribute, - const QVariant &oldAttribute) { - Q_UNUSED(oldAttribute) - + bool CocoaWindowContext::windowAttributeChanged(const QString &key, const QVariant &attribute) { Q_ASSERT(m_windowId); if (key == QStringLiteral("no-system-buttons")) { diff --git a/src/core/contexts/cocoawindowcontext_p.h b/src/core/contexts/cocoawindowcontext_p.h index 8c18f098..0297858e 100644 --- a/src/core/contexts/cocoawindowcontext_p.h +++ b/src/core/contexts/cocoawindowcontext_p.h @@ -25,14 +25,13 @@ namespace QWK { ~CocoaWindowContext() override; QString key() const override; - void virtual_hook(int id, void *data) override; + void updateSystemButtonArea() override; QVariant windowAttribute(const QString &key) const override; protected: void winIdChanged(WId winId, WId oldWinId) override; - bool windowAttributeChanged(const QString &key, const QVariant &attribute, - const QVariant &oldAttribute) override; + bool windowAttributeChanged(const QString &key, const QVariant &attribute) override; protected: std::unique_ptr cocoaWindowEventFilter; diff --git a/src/core/contexts/linuxwaylandcontext.cpp b/src/core/contexts/linuxwaylandcontext.cpp deleted file mode 100644 index 50211e74..00000000 --- a/src/core/contexts/linuxwaylandcontext.cpp +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (C) 2021-2023 wangwenx190 (Yuhang Zhao) -// Copyright (C) 2023-present Stdware Collections (https://www.github.com/stdware) -// Copyright (C) 2025-2027 Wing-summer (wingsummer) -// SPDX-License-Identifier: Apache-2.0 - -#include "linuxwaylandcontext_p.h" - -#include "qwindowkit_linux.h" - -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) -#include - -namespace QWK { - static inline void xdg_toplevel_show_window_menu(struct xdg_toplevel *xdg_toplevel, - struct wl_seat *seat, uint32_t serial, - int32_t x, int32_t y) { - constexpr auto XDG_TOPLEVEL_SHOW_WINDOW_MENU = 4; - const auto &api = QWK::Private::waylandAPI(); - Q_ASSERT(api.isValid()); - api.wl_proxy_marshal_flags( - reinterpret_cast(xdg_toplevel), XDG_TOPLEVEL_SHOW_WINDOW_MENU, - nullptr, api.wl_proxy_get_version(reinterpret_cast(xdg_toplevel)), 0, - seat, serial, x, y); - } - - LinuxWaylandContext::LinuxWaylandContext() = default; - - LinuxWaylandContext::~LinuxWaylandContext() = default; - - QString LinuxWaylandContext::key() const { - return QStringLiteral("wayland"); - } - - void LinuxWaylandContext::virtual_hook(int id, void *data) { - if (id == ShowSystemMenuHook) { - auto *waylandApp = qApp->nativeInterface(); - if (!waylandApp) { - return; - } - uint serial = waylandApp->lastInputSerial(); - wl_seat *seat = waylandApp->lastInputSeat(); - if (serial == 0 || !seat) { - return; - } - - auto toplevel = static_cast( - QGuiApplication::platformNativeInterface()->nativeResourceForWindow( - "xdg_toplevel", m_windowHandle)); - if (!toplevel) { - return; - } - auto pos = static_cast(data); - xdg_toplevel_show_window_menu(toplevel, seat, serial, pos->x(), pos->y()); - - wl_display *d = waylandApp->display(); - if (d) { - const auto &api = QWK::Private::waylandAPI(); - Q_ASSERT(api.isValid()); - api.wl_display_flush(d); - } - } else { - QtWindowContext::virtual_hook(id, data); - } - } -} -#endif // QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) diff --git a/src/core/contexts/linuxwaylandcontext_p.h b/src/core/contexts/linuxwaylandcontext_p.h deleted file mode 100644 index 4157e4d2..00000000 --- a/src/core/contexts/linuxwaylandcontext_p.h +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (C) 2021-2023 wangwenx190 (Yuhang Zhao) -// Copyright (C) 2023-present Stdware Collections (https://www.github.com/stdware) -// Copyright (C) 2025-2027 Wing-summer (wingsummer) -// SPDX-License-Identifier: Apache-2.0 - - -#ifndef LINUXWAYLANDCONTEXT_P_H -#define LINUXWAYLANDCONTEXT_P_H - -// -// W A R N I N G !!! -// ----------------- -// -// This file is not part of the QWindowKit API. It is used purely as an -// implementation detail. This header file may change from version to -// version without notice, or may even be removed. -// - - -#include "qtwindowcontext_p.h" - -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) -namespace QWK { - - class LinuxWaylandContext : public QtWindowContext { - Q_OBJECT - public: - LinuxWaylandContext(); - ~LinuxWaylandContext() override; - - QString key() const override; - void virtual_hook(int id, void *data) override; - }; - -} -#endif // QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) -#endif // LINUXWAYLANDCONTEXT_P_H diff --git a/src/core/contexts/linuxx11context.cpp b/src/core/contexts/linuxx11context.cpp deleted file mode 100644 index bd1a7b28..00000000 --- a/src/core/contexts/linuxx11context.cpp +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright (C) 2021-2023 wangwenx190 (Yuhang Zhao) -// Copyright (C) 2023-present Stdware Collections (https://www.github.com/stdware) -// Copyright (C) 2025-2027 Wing-summer (wingsummer) -// SPDX-License-Identifier: Apache-2.0 - - -#include "linuxx11context_p.h" - -#include "qwindowkit_linux.h" - -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) -// copy from X11 library and simplify it -typedef struct _XExtData XExtData; -struct ScreenFormat; -struct Depth; -struct Visual; -typedef XID Colormap; -typedef struct { - XExtData *ext_data; /* hook for extension to hang data */ - struct _XDisplay *display; /* back pointer to display structure */ - Window root; /* Root window id. */ - int width, height; /* width and height of screen */ - int mwidth, mheight; /* width and height of in millimeters */ - int ndepths; /* number of depths possible */ - Depth *depths; /* list of allowable depths on the screen */ - int root_depth; /* bits per pixel */ - Visual *root_visual; /* root visual */ - - // typedef struct _XGC - // #ifdef XLIB_ILLEGAL_ACCESS - // { - // XExtData *ext_data; /* hook for extension to hang data */ - // GContext gid; /* protocol ID for graphics context */ - // /* there is more to this structure, but it is private to Xlib */ - // } - // #endif - // *GC; - void * /*GC*/ default_gc; /* GC for the root root visual */ - - Colormap cmap; /* default color map */ - unsigned long white_pixel; - unsigned long black_pixel; /* White and Black pixel values */ - int max_maps, min_maps; /* max and min color maps */ - int backing_store; /* Never, WhenMapped, Always */ - Bool save_unders; - long root_input_mask; /* initial root input mask */ -} Screen; -typedef char *XPointer; -typedef struct { - XExtData *ext_data; /* hook for extension to hang data */ - struct _XPrivate *private1; - int fd; /* Network socket. */ - int private2; - int proto_major_version; /* major version of server's X protocol */ - int proto_minor_version; /* minor version of servers X protocol */ - char *vendor; /* vendor of the server hardware */ - XID private3; - XID private4; - XID private5; - int private6; - XID (*resource_alloc)(/* allocator function */ - struct _XDisplay *); - int byte_order; /* screen byte order, LSBFirst, MSBFirst */ - int bitmap_unit; /* padding and data requirements */ - int bitmap_pad; /* padding requirements on bitmaps */ - int bitmap_bit_order; /* LeastSignificant or MostSignificant */ - int nformats; /* number of pixmap formats in list */ - ScreenFormat *pixmap_format; /* pixmap format list */ - int private8; - int release; /* release of the server */ - struct _XPrivate *private9, *private10; - int qlen; /* Length of input event queue */ - unsigned long last_request_read; /* seq number of last event read */ - unsigned long request; /* sequence number of last request. */ - XPointer private11; - XPointer private12; - XPointer private13; - XPointer private14; - unsigned max_request_size; /* maximum number 32 bit words in request*/ - struct _XrmHashBucketRec *db; - int (*private15)(struct _XDisplay *); - char *display_name; /* "host:display" string used on this connect*/ - int default_screen; /* default screen for operations */ - int nscreens; /* number of screens on this server*/ - Screen *screens; /* pointer to list of screens */ - unsigned long motion_buffer; /* size of motion buffer */ - unsigned long private16; - int min_keycode; /* minimum defined keycode */ - int max_keycode; /* maximum defined keycode */ - XPointer private17; - XPointer private18; - int private19; - char *xdefaults; /* contents of defaults from server */ - /* there is more to this structure, but it is private to Xlib */ -} *_XPrivDisplay; - -typedef struct { - int type; - unsigned long serial; /* # of last request processed by server */ - Bool send_event; /* true if this came from a SendEvent request */ - Display *display; /* Display the event was read from */ - Window window; - Atom message_type; - int format; - union { - char b[20]; - short s[10]; - long l[5]; - } data; -} XClientMessageEvent; - -/* - * this union is defined so Xlib can always use the same sized - * event structure internally, to avoid memory fragmentation. - */ -union _XEvent { - // delete so many members but size unchanged because of pad - XClientMessageEvent xclient; - long pad[24]; -}; - -#define ScreenOfDisplay(dpy, scr) (&((_XPrivDisplay) (dpy))->screens[scr]) -#define DefaultScreen(dpy) (((_XPrivDisplay) (dpy))->default_screen) -#define DefaultRootWindow(dpy) (ScreenOfDisplay(dpy, DefaultScreen(dpy))->root) - -namespace QWK { - - LinuxX11Context::LinuxX11Context() = default; - - LinuxX11Context::~LinuxX11Context() = default; - - QString LinuxX11Context::key() const { - return QStringLiteral("xcb"); - } - - void LinuxX11Context::virtual_hook(int id, void *data) { - if (id == ShowSystemMenuHook) { - // showSystemMenu() is public API and may be called before the window is created or - // after it has been destroyed. - if (!m_windowId || !m_windowHandle) { - return; - } - - auto *x11app = qApp->nativeInterface(); - if (!x11app) { - return; - } - - auto display = x11app->display(); - if (!display) { - return; - } - - const auto &api = QWK::Private::x11API(); - Q_ASSERT(api.isValid()); - - // some marcos to constexpr in X11 - constexpr auto None = 0L; - constexpr auto ClientMessage = 33; - constexpr auto False = 0; - constexpr auto Button3 = 3; - constexpr auto SubstructureNotifyMask = 1L << 19; - constexpr auto SubstructureRedirectMask = 1L << 20; - - // use window id (XID) - auto xwin = static_cast(m_windowId); - Atom atom = api.XInternAtom(display, "_GTK_SHOW_WINDOW_MENU", False); - if (atom == None) - return; // WM might not support this atom - auto pos = static_cast(data); - XEvent ev{}; - ev.xclient.type = ClientMessage; - ev.xclient.window = xwin; - ev.xclient.message_type = atom; - - // The format member is set to 8, 16, or 32 - // and specifies whether the data should be viewed as - // a list of bytes, shorts, or longs - typeof(xclient.data). - ev.xclient.format = 32; - - qreal dpr = m_windowHandle->devicePixelRatio(); - int root_x = qRound(pos->x() * dpr); - int root_y = qRound(pos->y() * dpr); - - ev.xclient.data.l[0] = Button3; // right button - ev.xclient.data.l[1] = root_x; - ev.xclient.data.l[2] = root_y; - - Window root = DefaultRootWindow(display); - api.XUngrabPointer(display, 0L); - api.XSendEvent(display, root, False, SubstructureRedirectMask | SubstructureNotifyMask, - &ev); - api.XFlush(display); - } else { - QtWindowContext::virtual_hook(id, data); - } - } -} -#endif // QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) diff --git a/src/core/contexts/qtwindowcontext.cpp b/src/core/contexts/qtwindowcontext.cpp index 9f41816b..87d91a01 100644 --- a/src/core/contexts/qtwindowcontext.cpp +++ b/src/core/contexts/qtwindowcontext.cpp @@ -13,46 +13,28 @@ namespace QWK { static constexpr const quint8 kDefaultResizeBorderThickness = 8; - static Qt::CursorShape calculateCursorShape(const QWindow *window, const QPoint &pos) { -#ifdef Q_OS_MACOS - Q_UNUSED(window); - Q_UNUSED(pos); - return Qt::ArrowCursor; -#else - Q_ASSERT(window); - if (!window) { - return Qt::ArrowCursor; - } - if (window->visibility() != QWindow::Windowed) { - return Qt::ArrowCursor; - } - const int x = pos.x(); - const int y = pos.y(); - const int w = window->width(); - const int h = window->height(); - if (((x < kDefaultResizeBorderThickness) && (y < kDefaultResizeBorderThickness)) || - ((x >= (w - kDefaultResizeBorderThickness)) && - (y >= (h - kDefaultResizeBorderThickness)))) { - return Qt::SizeFDiagCursor; + static Qt::CursorShape calculateCursorShape(Qt::Edges edges) { + const bool horizontal = edges & (Qt::LeftEdge | Qt::RightEdge); + const bool vertical = edges & (Qt::TopEdge | Qt::BottomEdge); + if (horizontal && vertical) { + return edges == (Qt::LeftEdge | Qt::TopEdge) || + edges == (Qt::RightEdge | Qt::BottomEdge) + ? Qt::SizeFDiagCursor : Qt::SizeBDiagCursor; } - if (((x >= (w - kDefaultResizeBorderThickness)) && (y < kDefaultResizeBorderThickness)) || - ((x < kDefaultResizeBorderThickness) && (y >= (h - kDefaultResizeBorderThickness)))) { - return Qt::SizeBDiagCursor; - } - if ((x < kDefaultResizeBorderThickness) || (x >= (w - kDefaultResizeBorderThickness))) { + if (horizontal) return Qt::SizeHorCursor; - } - if ((y < kDefaultResizeBorderThickness) || (y >= (h - kDefaultResizeBorderThickness))) { + if (vertical) return Qt::SizeVerCursor; - } return Qt::ArrowCursor; -#endif } - static inline Qt::Edges calculateWindowEdges(const QWindow *window, const QPoint &pos) { + static inline Qt::Edges calculateWindowEdges(const QWindow *window, const QPoint &pos, + bool widthFixed, bool heightFixed) { #ifdef Q_OS_MACOS Q_UNUSED(window); Q_UNUSED(pos); + Q_UNUSED(widthFixed); + Q_UNUSED(heightFixed); return {}; #else Q_ASSERT(window); @@ -77,13 +59,17 @@ namespace QWK { if (y >= (window->height() - kDefaultResizeBorderThickness)) { edges |= Qt::BottomEdge; } + if (widthFixed) + edges &= ~(Qt::LeftEdge | Qt::RightEdge); + if (heightFixed) + edges &= ~(Qt::TopEdge | Qt::BottomEdge); return edges; #endif } - class QtWindowEventFilter : public SharedEventFilter { + class QtWindowEventFilter : public QObject, public SharedEventFilter { public: - explicit QtWindowEventFilter(AbstractWindowContext *context); + explicit QtWindowEventFilter(QtWindowContext *context); ~QtWindowEventFilter() override; enum WindowStatus { @@ -96,51 +82,121 @@ namespace QWK { protected: bool sharedEventFilter(QObject *object, QEvent *event) override; + bool eventFilter(QObject *object, QEvent *event) override; private: - AbstractWindowContext *m_context; + void observeCursorHost(QObject *host, QWindow *window); + void restoreCursor(); + QtWindowContext *m_context; + QPointer m_cursorHost; + QPointer m_cursorWindow; bool m_cursorShapeChanged; WindowStatus m_windowStatus; }; - QtWindowEventFilter::QtWindowEventFilter(AbstractWindowContext *context) + QtWindowEventFilter::QtWindowEventFilter(QtWindowContext *context) : m_context(context), m_cursorShapeChanged(false), m_windowStatus(Idle) { m_context->installSharedEventFilter(this); } - QtWindowEventFilter::~QtWindowEventFilter() = default; + QtWindowEventFilter::~QtWindowEventFilter() { + restoreCursor(); + } - bool QtWindowEventFilter::sharedEventFilter(QObject *obj, QEvent *event) { - Q_UNUSED(obj) + void QtWindowEventFilter::restoreCursor() { + if (!m_cursorShapeChanged) + return; + m_cursorShapeChanged = false; + if (m_cursorHost) + m_context->delegate()->restoreCursorShape(m_cursorHost); + } + + void QtWindowEventFilter::observeCursorHost(QObject *host, QWindow *window) { + if (m_cursorHost != host) { + m_cursorHost = host; + host->installEventFilter(this); + } + if (m_cursorWindow == window) + return; + if (m_cursorWindow) + disconnect(m_cursorWindow, nullptr, this, nullptr); + m_cursorWindow = window; + // QWindow constraints can change without a mouse or resize event. QWidget + // updates them without signals; that path is rechecked on the next mouse event. + connect(window, &QWindow::minimumWidthChanged, this, [this] { restoreCursor(); }); + connect(window, &QWindow::minimumHeightChanged, this, [this] { restoreCursor(); }); + connect(window, &QWindow::maximumWidthChanged, this, [this] { restoreCursor(); }); + connect(window, &QWindow::maximumHeightChanged, this, [this] { restoreCursor(); }); + connect(window, &QWindow::windowStateChanged, this, [this] { + m_windowStatus = Idle; + restoreCursor(); + }); + } + bool QtWindowEventFilter::eventFilter(QObject *object, QEvent *event) { + if (object == m_cursorHost) { + if (event->type() == QEvent::Destroy) { + // QWidget delivers Destroy before tearing down its cursor storage. + // Do not try to restore into a host whose destruction has begun. + m_cursorHost.clear(); + m_cursorShapeChanged = false; + return false; + } + switch (event->type()) { + case QEvent::Leave: + case QEvent::Hide: + case QEvent::WindowStateChange: { + const QPointer receiver(object); + restoreCursor(); + return !receiver; + } + default: + break; + } + } + return false; + } + + bool QtWindowEventFilter::sharedEventFilter(QObject *obj, QEvent *event) { auto type = event->type(); + if (type == QEvent::Leave || type == QEvent::Hide || + type == QEvent::WindowStateChange || type == QEvent::WinIdChange) { + const QPointer self(this); + const QPointer receiver(obj); + if (type != QEvent::Leave) + m_windowStatus = Idle; + restoreCursor(); + return !self || !receiver; + } if (type < QEvent::MouseButtonPress || type > QEvent::MouseMove) { return false; } auto host = m_context->host(); auto window = m_context->window(); auto delegate = m_context->delegate(); + if (!host || !window) + return false; + const QPointer self(this); + const QPointer receiver(host); auto me = static_cast(event); - bool fixedSize = m_context->isHostSizeFixed(); + const bool widthFixed = m_context->isHostWidthFixed(); + const bool heightFixed = m_context->isHostHeightFixed(); + const bool fixedSize = widthFixed && heightFixed; QPoint scenePos = getMouseEventScenePos(me); QPoint globalPos = getMouseEventGlobalPos(me); bool inTitleBar = m_context->isInTitleBarDraggableArea(scenePos); + const Qt::Edges edges = calculateWindowEdges(window, scenePos, widthFixed, heightFixed); const auto& updateCursorShape{ [&](){ - if (fixedSize) { - return; - } - const Qt::CursorShape shape = calculateCursorShape(window, scenePos); + const Qt::CursorShape shape = calculateCursorShape(edges); if (shape == Qt::ArrowCursor) { - if (m_cursorShapeChanged) { - delegate->restoreCursorShape(host); - m_cursorShapeChanged = false; - } + restoreCursor(); } else { - delegate->setCursorShape(host, shape); + observeCursorHost(host, window); m_cursorShapeChanged = true; + delegate->setCursorShape(host, shape); } } }; @@ -151,14 +207,14 @@ namespace QWK { m_windowStatus = WaitingRelease; switch (me->button()) { case Qt::LeftButton: { - if (!fixedSize) { - Qt::Edges edges = calculateWindowEdges(window, scenePos); - if (edges != Qt::Edges()) { - startSystemResize(window, edges); - m_windowStatus = Resizing; - handled = true; - break; - } + updateCursorShape(); + if (!self || !receiver) + return true; + if (edges != Qt::Edges()) { + m_context->systemResize(edges); + m_windowStatus = Resizing; + handled = true; + break; } if (inTitleBar) { // If we call startSystemMove() now but release the mouse without actual @@ -201,6 +257,9 @@ namespace QWK { } } m_windowStatus = Idle; + updateCursorShape(); + if (!self || !receiver) + return true; break; } @@ -209,10 +268,12 @@ namespace QWK { case Idle: case WaitingRelease: { updateCursorShape(); + if (!self || !receiver) + return true; break; } case PreparingMove: { - startSystemMove(window); + m_context->systemMove(); m_windowStatus = Moving; handled = true; break; @@ -261,12 +322,16 @@ namespace QWK { QtWindowContext::~QtWindowContext() = default; - QString QtWindowContext::key() const { - return QStringLiteral("qt"); + void QtWindowContext::systemMove() { + startSystemMove(window()); + } + + void QtWindowContext::systemResize(Qt::Edges edges) { + startSystemResize(window(), edges); } - void QtWindowContext::virtual_hook(int id, void *data) { - AbstractWindowContext::virtual_hook(id, data); + QString QtWindowContext::key() const { + return QStringLiteral("qt"); } void QtWindowContext::winIdChanged(WId winId, WId oldWinId) { diff --git a/src/core/contexts/qtwindowcontext_p.h b/src/core/contexts/qtwindowcontext_p.h index 03a8f0df..75f1b32f 100644 --- a/src/core/contexts/qtwindowcontext_p.h +++ b/src/core/contexts/qtwindowcontext_p.h @@ -25,7 +25,10 @@ namespace QWK { ~QtWindowContext() override; QString key() const override; - void virtual_hook(int id, void *data) override; + + // Internal platform boundary; event decisions can be tested without native input. + virtual void systemMove(); + virtual void systemResize(Qt::Edges edges); protected: void winIdChanged(WId winId, WId oldWinId) override; diff --git a/src/core/contexts/waylandcontext.cpp b/src/core/contexts/waylandcontext.cpp new file mode 100644 index 00000000..e819cbca --- /dev/null +++ b/src/core/contexts/waylandcontext.cpp @@ -0,0 +1,69 @@ +// Copyright (C) 2021-2023 wangwenx190 (Yuhang Zhao) +// Copyright (C) 2023-present Stdware Collections (https://www.github.com/stdware) +// Copyright (C) 2025-2027 Wing-summer (wingsummer) +// SPDX-License-Identifier: Apache-2.0 + +#include "waylandcontext_p.h" + +#ifdef QWK_HAS_WAYLAND_CONTEXT +#include "qwindowkit_wayland.h" +#include +#include + +namespace QWK { + static inline void xdg_toplevel_show_window_menu(struct xdg_toplevel *xdg_toplevel, + struct wl_seat *seat, uint32_t serial, + int32_t x, int32_t y) { + constexpr auto XDG_TOPLEVEL_SHOW_WINDOW_MENU = 4; + const auto &api = QWK::Private::waylandAPI(); + Q_ASSERT(api.isValid()); + api.wl_proxy_marshal_flags( + reinterpret_cast(xdg_toplevel), XDG_TOPLEVEL_SHOW_WINDOW_MENU, + nullptr, api.wl_proxy_get_version(reinterpret_cast(xdg_toplevel)), 0, + seat, serial, x, y); + } + + WaylandContext::WaylandContext() = default; + + WaylandContext::~WaylandContext() = default; + + QString WaylandContext::key() const { + return QStringLiteral("wayland"); + } + + void WaylandContext::showSystemMenu(const QPoint &globalPos) { + if (!m_windowId || !m_windowHandle) { + return; + } + auto *waylandApp = qApp->nativeInterface(); + if (!waylandApp) { + return; + } + uint serial = waylandApp->lastInputSerial(); + wl_seat *seat = waylandApp->lastInputSeat(); + if (serial == 0 || !seat) { + return; + } + + auto toplevel = static_cast( + QGuiApplication::platformNativeInterface()->nativeResourceForWindow( + "xdg_toplevel", m_windowHandle)); + if (!toplevel) { + return; + } + const QPointF localPos = m_windowHandle->mapFromGlobal(QPointF(globalPos)); + // QtWindowContext uses FramelessWindowHint, so the content and surface origins + // coincide. Undo Qt's coordinate scaling, without applying Wayland's buffer scale. + const QPoint surfacePos = + QHighDpi::toNativeLocalPosition(localPos, m_windowHandle.data()).toPoint(); + xdg_toplevel_show_window_menu(toplevel, seat, serial, surfacePos.x(), surfacePos.y()); + + wl_display *d = waylandApp->display(); + if (d) { + const auto &api = QWK::Private::waylandAPI(); + Q_ASSERT(api.isValid()); + api.wl_display_flush(d); + } + } +} +#endif // QWK_HAS_WAYLAND_CONTEXT diff --git a/src/core/contexts/waylandcontext_p.h b/src/core/contexts/waylandcontext_p.h new file mode 100644 index 00000000..6b2e971c --- /dev/null +++ b/src/core/contexts/waylandcontext_p.h @@ -0,0 +1,47 @@ +// Copyright (C) 2021-2023 wangwenx190 (Yuhang Zhao) +// Copyright (C) 2023-present Stdware Collections (https://www.github.com/stdware) +// Copyright (C) 2025-2027 Wing-summer (wingsummer) +// SPDX-License-Identifier: Apache-2.0 + + +#ifndef WAYLANDCONTEXT_P_H +#define WAYLANDCONTEXT_P_H + +// +// W A R N I N G !!! +// ----------------- +// +// This file is not part of the QWindowKit API. It is used purely as an +// implementation detail. This header file may change from version to +// version without notice, or may even be removed. +// + + +#include "qtwindowcontext_p.h" + +// QWaylandApplication was introduced in Qt 6.5. Since Qt 6.7 its declaration +// also depends on the wayland feature, which older Qt versions do not define. +#if defined(Q_OS_LINUX) && QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) +# if QT_VERSION < QT_VERSION_CHECK(6, 7, 0) +# define QWK_HAS_WAYLAND_CONTEXT +# elif QT_CONFIG(wayland) +# define QWK_HAS_WAYLAND_CONTEXT +# endif +#endif + +#ifdef QWK_HAS_WAYLAND_CONTEXT +namespace QWK { + + class WaylandContext : public QtWindowContext { + Q_OBJECT + public: + WaylandContext(); + ~WaylandContext() override; + + QString key() const override; + void showSystemMenu(const QPoint &globalPos) override; + }; + +} +#endif // QWK_HAS_WAYLAND_CONTEXT +#endif // WAYLANDCONTEXT_P_H diff --git a/src/core/contexts/win32windowcontext.cpp b/src/core/contexts/win32windowcontext.cpp index ad4e97bb..d95b9a55 100644 --- a/src/core/contexts/win32windowcontext.cpp +++ b/src/core/contexts/win32windowcontext.cpp @@ -5,6 +5,7 @@ #include "win32windowcontext_p.h" #include +#include #include #include @@ -13,7 +14,6 @@ #include #include #include -#include #include #include @@ -66,12 +66,36 @@ namespace QWK { const uint32_t inactiveDark = MAKE_RGBA_COLOR(61, 61, 62, 255); // #3D3D3E } kWindowsColorSet; - // hWnd -> context - using WndProcHash = QHash; + struct ManagedWindow { + QPointer context; + WNDPROC previousProc = nullptr; + bool destroying = false; + }; + // Retain inactive entries when a later subclass still calls our procedure. + // Active dispatch frames also retain the entry across removal/reentrant messages. + using WndProcHash = QHash>; Q_GLOBAL_STATIC(WndProcHash, g_wndProcHash) - // Original Qt window proc function - static WNDPROC g_qtWindowProc = nullptr; + // Native calls can run a nested event loop. A live QObject alone is not enough: + // its HWND may have been destroyed/recreated, or its registration replaced. + class ManagedWindowGuard { + public: + ManagedWindowGuard(Win32WindowContext *ctx, HWND hwnd) + : context(ctx), window(ctx->window()), hwnd(hwnd), entry(g_wndProcHash->value(hwnd)) {} + + bool isCurrent() const { + return context && window && entry && !entry->destroying && + entry->context == context && g_wndProcHash->value(hwnd) == entry && + context->window() == window && + context->windowId() == reinterpret_cast(hwnd) && ::IsWindow(hwnd); + } + + private: + QPointer context; + QPointer window; + HWND hwnd; + std::shared_ptr entry; + }; static bool isLikelyFrameDriftAfterWinIdChange(HWND hwnd, const RECT &expectedFrameRect, const RECT &candidateFrameRect) { @@ -126,7 +150,10 @@ namespace QWK { // the title bar by pretending the whole window is filled by client area, // this however confuses Qt's internal logic. We need to do the following // hack to let Qt consider the extra margin when changing window geometry. + const QPointer windowGuard(window); window->setProperty("_q_windowsCustomMargins", marginsVar); + if (!windowGuard) + return; # if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) if (QPlatformWindow *platformWindow = window->handle()) { if (const auto ni = QGuiApplication::platformNativeInterface()) { @@ -256,9 +283,10 @@ namespace QWK { } // Returns false if the menu is canceled - static bool showSystemMenu_sys(HWND hWnd, const POINT &pos, const bool selectFirstEntry, - const bool fixedSize) { + static bool showSystemMenu_sys(Win32WindowContext *context, HWND hWnd, const POINT &pos, + const bool selectFirstEntry, const bool fixedSize) { Q_ASSERT(hWnd); + const ManagedWindowGuard guard(context, hWnd); HMENU hMenu = ::GetSystemMenu(hWnd, FALSE); if (!hMenu) { // The corresponding window doesn't have a system menu, most likely due to the @@ -321,6 +349,9 @@ namespace QWK { TPM_RIGHTBUTTON), pos.x, pos.y, 0, hWnd, nullptr); + if (!guard.isCurrent()) + return false; + // Unhighlight the first menu item after the popup menu is closed, otherwise it will keep // highlighting until we unhighlight it manually. ::HiliteMenuItem(hWnd, hMenu, SC_RESTORE, (MF_BYCOMMAND | MFS_UNHILITE)); @@ -520,6 +551,14 @@ namespace QWK { // handles Windows window messages in the main thread, it is safe to do so. class WindowsNativeEventFilter : public AppNativeEventFilter { public: + struct MessageContext { + QPointer context; + HWND hwnd; + UINT message; + WPARAM wParam; + LPARAM lParam; + }; + bool nativeEventFilter(const QByteArray &eventType, void *message, QT_NATIVE_EVENT_RESULT_TYPE *result) override { Q_UNUSED(eventType) @@ -536,9 +575,15 @@ namespace QWK { // https://github.com/qt/qtbase/blob/e26a87f1ecc40bc8c6aa5b889fce67410a57a702/src/plugins/platforms/windows/qwindowscontext.cpp#L1546 // Qt needs to refer to the WM_NCCALCSIZE message data that hasn't been // processed, so we have to process it after Qt acquires the initial data. - if (lastMessageContext) { + const auto frame = currentMessage; + const auto ctx = frame ? frame->context.data() : nullptr; + const auto entry = g_wndProcHash->value(msg->hwnd); + if (ctx && frame->hwnd == msg->hwnd && frame->message == msg->message && + frame->wParam == msg->wParam && frame->lParam == msg->lParam && + entry && !entry->destroying && entry->context == ctx && ctx->window() && + ctx->windowId() == reinterpret_cast(msg->hwnd)) { LRESULT res; - if (lastMessageContext->nonClientCalcSizeHandler( + if (ctx->nonClientCalcSizeHandler( msg->hwnd, msg->message, msg->wParam, msg->lParam, &res)) { *result = decltype(*result)(res); return true; @@ -550,7 +595,7 @@ namespace QWK { // case WM_NCHITTEST: { // // The child window must return HTTRANSPARENT when processing WM_NCHITTEST for // // the parent window to receive WM_NCHITTEST. - // if (!lastMessageContext) { + // if (!currentMessage || !currentMessage->context) { // auto rootHWnd = ::GetAncestor(msg->hwnd, GA_ROOT); // if (rootHWnd != msg->hwnd) { // if (auto ctx = g_wndProcHash->value(rootHWnd)) { @@ -566,7 +611,9 @@ namespace QWK { } static inline WindowsNativeEventFilter *instance = nullptr; - static inline Win32WindowContext *lastMessageContext = nullptr; + // Points into the synchronous WndProc stack. Each frame has its own guarded + // context, so nested messages cannot overwrite or resurrect an outer owner. + static inline const MessageContext *currentMessage = nullptr; static inline void install() { if (instance) { @@ -642,26 +689,53 @@ namespace QWK { return FALSE; } - // QWindow may have been destroyed before WinIdChange event comes - auto ctx = g_wndProcHash->value(hWnd); - if (!ctx || !ctx->window()) { - return ::CallWindowProcW(g_qtWindowProc, hWnd, message, wParam, lParam); + const auto entry = g_wndProcHash->value(hWnd); + const WindowsNativeEventFilter::MessageContext frame{ + entry && !entry->destroying && message != WM_NCDESTROY + ? entry->context : QPointer{}, + hWnd, message, wParam, lParam}; + const auto previousMessage = WindowsNativeEventFilter::currentMessage; + WindowsNativeEventFilter::currentMessage = &frame; + const auto contextCleaner = qScopeGuard([previousMessage] { + WindowsNativeEventFilter::currentMessage = previousMessage; + }); + if (!entry) { + return ::DefWindowProcW(hWnd, message, wParam, lParam); + } + const auto previousProc = entry->previousProc; + // Always deliver native teardown to the original chain, even after the agent is gone. + if (message == WM_NCDESTROY) { + entry->destroying = true; + const auto cleanup = qScopeGuard([hWnd, entry] { + if (g_wndProcHash->value(hWnd) == entry) { + g_wndProcHash->remove(hWnd); + } + if (g_wndProcHash->isEmpty()) { + WindowsNativeEventFilter::uninstall(); + } + }); + return ::CallWindowProcW(previousProc, hWnd, message, wParam, lParam); } - WindowsNativeEventFilter::lastMessageContext = ctx; - const auto &contextCleaner = qScopeGuard([]() { - WindowsNativeEventFilter::lastMessageContext = nullptr; // - }); + // QWindow may have been destroyed before WinIdChange event comes. + auto ctx = entry->context.data(); + if (!ctx || !ctx->window()) { + return ::CallWindowProcW(previousProc, hWnd, message, wParam, lParam); + } // Since Qt does the necessary processing of the WM_NCCALCSIZE message, we need to // forward it right away and process it in our native event filter. if (message == WM_NCCALCSIZE) { - return ::CallWindowProcW(g_qtWindowProc, hWnd, message, wParam, lParam); + return ::CallWindowProcW(previousProc, hWnd, message, wParam, lParam); } // Try hooked procedure and save result + const ManagedWindowGuard guard(ctx, hWnd); LRESULT result; - if (ctx->windowProc(hWnd, message, wParam, lParam, &result)) { + const bool handled = ctx->windowProc(hWnd, message, wParam, lParam, &result); + if (!guard.isCurrent()) + return result; + if (handled) { // https://github.com/stdware/qwindowkit/issues/45 // Forward the event to user-defined native event filters, there may be some messages // that need to be processed by the user. @@ -671,10 +745,12 @@ namespace QWK { } // Continue dispatching. - return ::CallWindowProcW(g_qtWindowProc, hWnd, message, wParam, lParam); + return ::CallWindowProcW(previousProc, hWnd, message, wParam, lParam); } - static inline void addManagedWindow(QWindow *window, HWND hWnd, Win32WindowContext *ctx) { + template + static inline void addManagedWindow(QWindow *window, HWND hWnd, Win32WindowContext *ctx, + const IsCurrent &isCurrent) { Q_ASSERT(window); Q_ASSERT(hWnd); Q_ASSERT(ctx); @@ -682,44 +758,64 @@ namespace QWK { if (isSystemBorderEnabled()) { // Inform Qt we want and have set custom margins setInternalWindowFrameMargins(window, QMargins(0, -getTitleBarHeight(hWnd), 0, 0)); + if (!isCurrent()) + return; } - // Store original window proc - if (!g_qtWindowProc) { - g_qtWindowProc = reinterpret_cast(::GetWindowLongPtrW(hWnd, GWLP_WNDPROC)); + auto entry = g_wndProcHash->value(hWnd); + if (entry) { + // Our procedure is already somewhere in this HWND's chain. Installing it + // again over a later subclass would introduce a cycle in that chain. + if (entry->destroying || (entry->context && entry->context != ctx)) { + qWarning("QWindowKit: window already managed or being destroyed"); + return; + } + } else { + entry = std::make_shared(); + ::SetLastError(ERROR_SUCCESS); + entry->previousProc = reinterpret_cast(::SetWindowLongPtrW( + hWnd, GWLP_WNDPROC, reinterpret_cast(QWKHookedWndProc))); + if (!entry->previousProc) { + qWarning("QWindowKit: failed to subclass window (error %lu)", ::GetLastError()); + return; + } + g_wndProcHash->insert(hWnd, entry); } - - // Hook window proc - ::SetWindowLongPtrW(hWnd, GWLP_WNDPROC, reinterpret_cast(QWKHookedWndProc)); + entry->context = ctx; // Install global native event filter WindowsNativeEventFilter::install(); - // Save window handle mapping - g_wndProcHash->insert(hWnd, ctx); - // Force a WM_NCCALCSIZE message manually to avoid the title bar become visible // while Qt is re-creating the window (such as setWindowFlag(s) calls). It has // been observed by our users. triggerFrameChange(hWnd); } - static inline void removeManagedWindow(HWND hWnd) { + static inline void removeManagedWindow(HWND hWnd, Win32WindowContext *ctx) { Q_ASSERT(hWnd); - // Remove window handle mapping - if (!g_wndProcHash->remove(hWnd)) + const auto entry = g_wndProcHash->value(hWnd); + // An old context must not remove a new registration for a reused HWND. + if (!entry || entry->context != ctx) return; + entry->context = nullptr; - // Unhook the window procedure, but only when ours is still the one installed. Anybody - // is free to subclass the window after we did, and writing g_qtWindowProc back - // unconditionally would silently drop them out of the chain. When that happens we - // simply leave QWKHookedWndProc in place: with no context left in g_wndProcHash it - // forwards everything straight to g_qtWindowProc anyway. - if (g_qtWindowProc && - reinterpret_cast(::GetWindowLongPtrW(hWnd, GWLP_WNDPROC)) == - QWKHookedWndProc) { - ::SetWindowLongPtrW(hWnd, GWLP_WNDPROC, reinterpret_cast(g_qtWindowProc)); + // Never unlink a later subclass. Keep the original procedure available until + // we can remove our hook, or until WM_NCDESTROY retires this HWND's entry. + if (!::IsWindow(hWnd)) { + g_wndProcHash->remove(hWnd); + } else if (!entry->destroying && + reinterpret_cast(::GetWindowLongPtrW(hWnd, GWLP_WNDPROC)) == + QWKHookedWndProc) { + ::SetLastError(ERROR_SUCCESS); + if (::SetWindowLongPtrW(hWnd, GWLP_WNDPROC, + reinterpret_cast(entry->previousProc))) { + g_wndProcHash->remove(hWnd); + } else { + qWarning("QWindowKit: failed to restore window procedure (error %lu)", + ::GetLastError()); + } } // Remove event filter if the all windows has been destroyed @@ -732,7 +828,7 @@ namespace QWK { Win32WindowContext::~Win32WindowContext() { if (m_windowId) { - removeManagedWindow(reinterpret_cast(m_windowId)); + removeManagedWindow(reinterpret_cast(m_windowId), this); } } @@ -740,122 +836,60 @@ namespace QWK { return QStringLiteral("win32"); } - void Win32WindowContext::virtual_hook(int id, void *data) { - switch (id) { - case RaiseWindowHook: { - if (!m_windowId) - return; - m_delegate->setWindowVisible(m_host, true); - const auto hwnd = reinterpret_cast(m_windowId); - bringWindowToFront(hwnd); - return; - } + void Win32WindowContext::raiseWindow() { + if (!m_windowId) + return; + m_delegate->setWindowVisible(m_host, true); + const auto hwnd = reinterpret_cast(m_windowId); + bringWindowToFront(hwnd); + } - case ShowSystemMenuHook: { - if (!m_windowId) - return; - const auto &pos = *static_cast(data); - auto hWnd = reinterpret_cast(m_windowId); + void Win32WindowContext::showSystemMenu(const QPoint &pos) { + if (!m_windowId) + return; + auto hWnd = reinterpret_cast(m_windowId); #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - const QPoint nativeGlobalPos = - QHighDpi::toNativeGlobalPosition(pos, m_windowHandle.data()); + const QPoint nativeGlobalPos = + QHighDpi::toNativeGlobalPosition(pos, m_windowHandle.data()); #else - const QPoint nativeGlobalPos = QHighDpi::toNativePixels(pos, m_windowHandle.data()); + const QPoint nativeGlobalPos = QHighDpi::toNativePixels(pos, m_windowHandle.data()); #endif - std::ignore = showSystemMenu_sys(hWnd, qpoint2point(nativeGlobalPos), false, - isHostSizeFixed()); - return; - } - - case DefaultColorsHook: { - auto &map = *static_cast *>(data); - map.clear(); - map.insert(QStringLiteral("activeLight"), kWindowsColorSet.activeLight); - map.insert(QStringLiteral("activeDark"), kWindowsColorSet.activeDark); - map.insert(QStringLiteral("inactiveLight"), kWindowsColorSet.inactiveLight); - map.insert(QStringLiteral("inactiveDark"), kWindowsColorSet.inactiveDark); - return; - } + std::ignore = showSystemMenu_sys(this, hWnd, qpoint2point(nativeGlobalPos), false, + isHostSizeFixed()); + } #if QWINDOWKIT_CONFIG(ENABLE_WINDOWS_SYSTEM_BORDERS) - // ### FIXME: May be deprecated - case DrawWindows10BorderHook_Emulated: { - if (!m_windowId) - return; - - auto args = static_cast(data); - auto &painter = *static_cast(args[0]); - const auto &rect = *static_cast(args[1]); - const auto ®ion = *static_cast(args[2]); - const auto hwnd = reinterpret_cast(m_windowId); - - QPen pen; -# if QT_VERSION_MAJOR < 6 - pen.setWidth(1); -# else - pen.setWidthF(1 / m_windowHandle->devicePixelRatio()); // why 0.25? -# endif - - const bool dark = isDarkThemeActive() && isDarkWindowFrameEnabled(hwnd); - if (m_delegate->isWindowActive(m_host)) { - if (isWindowFrameBorderColorized()) { - pen.setColor(getAccentColor()); - } else { - static QColor frameBorderActiveColorLight(kWindowsColorSet.activeLight); - static QColor frameBorderActiveColorDark(kWindowsColorSet.activeDark); - pen.setColor(dark ? frameBorderActiveColorDark - : frameBorderActiveColorLight); - } - } else { - static QColor frameBorderInactiveColorLight(kWindowsColorSet.inactiveLight); - static QColor frameBorderInactiveColorDark(kWindowsColorSet.inactiveDark); - pen.setColor(dark ? frameBorderInactiveColorDark - : frameBorderInactiveColorLight); - } - painter.save(); - - // We need antialiasing to give us better result. - painter.setRenderHint(QPainter::Antialiasing); - - painter.setPen(pen); - painter.drawLine(QLine{ - QPoint{0, 0}, - QPoint{m_windowHandle->width(), 0} - }); - painter.restore(); - return; - } - - case DrawWindows10BorderHook_Native: { - if (!m_windowId) - return; + void Win32WindowContext::setWindows10BorderActive(bool active) { + if (!m_windowId) + return; + windows10BorderInactive = !active; + applyFrameMargins(effectiveExtraMargins( + windowAttribute(QStringLiteral("extra-margins")).value())); + } - // https://github.com/microsoft/terminal/blob/71a6f26e6ece656084e87de1a528c4a8072eeabd/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp#L1025 - // https://docs.microsoft.com/en-us/windows/win32/dwm/customframe#extending-the-client-frame - // Draw a black rectangle to make Windows native top border show + QColor Win32WindowContext::windows10BorderColor() const { + if (!m_windowId || !m_windowHandle) + return {}; + const auto hwnd = reinterpret_cast(m_windowId); + const bool dark = isDarkThemeActive() && isDarkWindowFrameEnabled(hwnd); + if (m_delegate->isWindowActive(m_host)) { + return isWindowFrameBorderColorized() + ? getAccentColor() + : QColor(dark ? kWindowsColorSet.activeDark : kWindowsColorSet.activeLight); + } + return QColor(dark ? kWindowsColorSet.inactiveDark : kWindowsColorSet.inactiveLight); + } - auto hWnd = reinterpret_cast(m_windowId); - HDC hdc = ::GetDC(hWnd); - RECT windowRect{}; - ::GetClientRect(hWnd, &windowRect); - RECT rcTopBorder = { - 0, - 0, - RECT_WIDTH(windowRect), - 1, - }; - ::FillRect(hdc, &rcTopBorder, - reinterpret_cast(::GetStockObject(BLACK_BRUSH))); - ::ReleaseDC(hWnd, hdc); - return; - } -#endif + void Win32WindowContext::drawWindows10Border() { + if (!m_windowId) + return; - default: - break; - } - AbstractWindowContext::virtual_hook(id, data); + // https://github.com/microsoft/terminal/blob/71a6f26e6ece656084e87de1a528c4a8072eeabd/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp#L1025 + // https://docs.microsoft.com/en-us/windows/win32/dwm/customframe#extending-the-client-frame + // Draw a black rectangle to make Windows native top border show. + drawWindows10BorderNative(reinterpret_cast(m_windowId)); } +#endif QVariant Win32WindowContext::windowAttribute(const QString &key) const { if (key == QStringLiteral("window-rect")) { @@ -898,10 +932,20 @@ namespace QWK { } void Win32WindowContext::winIdChanged(WId winId, WId oldWinId) { + appliedFrameMargins = {}; + ++frameMarginsRevision; + const QPointer self(this); + const auto revision = m_windowRevision; + const auto isCurrent = [self, revision] { + return self && self->m_windowRevision == revision; + }; // Reset the context data mouseLeaveBlocked = false; lastHitTestResult = WindowPart::Outside; lastHitTestResultRaw = HTNOWHERE; + iconButtonClickTime = 0; + iconButtonClickLevel = 0; + windows10BorderInactive = false; // If the original window id is valid, remove all resources related if (oldWinId) { @@ -911,7 +955,7 @@ namespace QWK { hasFrameRectBeforeWinIdChange = ::GetWindowRect(oldHWnd, &frameRectBeforeWinIdChange) != FALSE; } - removeManagedWindow(oldHWnd); + removeManagedWindow(oldHWnd, this); } if (!winId) { QTimer::singleShot(0, this, [this]() { @@ -925,13 +969,18 @@ namespace QWK { // Install window hook auto hWnd = reinterpret_cast(winId); - if (!isSystemBorderEnabled()) { + if (!isSystemBorderEnabled() && + !windowAttribute(QStringLiteral("extra-margins")).isValid()) { static auto margins = QVariant::fromValue(QMargins(1, 1, 1, 1)); // If we remove the system border, the window will lose its shadow. If dwm is enabled, // then we need to set at least 1px margins, otherwise the following operation will // fail with no effect. + // Install the default only once. Cached user margins and material + // ordering must survive native-window recreation and be replayed below. setWindowAttribute(QStringLiteral("extra-margins"), margins); + if (!isCurrent()) + return; } // We should disable WS_SYSMENU, otherwise the system button icons will be visible if mica @@ -946,8 +995,12 @@ namespace QWK { } } + if (!isCurrent()) + return; // Add managed window - addManagedWindow(m_windowHandle, hWnd, this); + addManagedWindow(m_windowHandle, hWnd, this, isCurrent); + if (!isCurrent()) + return; if (hasFrameRectBeforeWinIdChange && !restoringFrameRectAfterWinIdChange) { pendingFrameRectAfterWinIdChange = frameRectBeforeWinIdChange; @@ -1058,7 +1111,7 @@ namespace QWK { } // Forward to native event filter subscribers - if (!m_nativeEventFilters.isEmpty()) { + if (!m_nativeDispatch.isEmpty()) { MSG msg = createMessageBlock(hWnd, message, wParam, lParam); QT_NATIVE_EVENT_RESULT_TYPE res = 0; if (nativeDispatch(nativeEventType(), &msg, &res)) { @@ -1069,64 +1122,168 @@ namespace QWK { return false; // Not handled } - bool Win32WindowContext::windowAttributeChanged(const QString &key, const QVariant &attribute, - const QVariant &oldAttribute) { - Q_UNUSED(oldAttribute) + bool Win32WindowContext::extendFrameMargins(const QMargins &margins) { + const auto nativeMargins = qmargins2margins(margins); + return SUCCEEDED(DynamicApis::instance().pDwmExtendFrameIntoClientArea( + reinterpret_cast(m_windowId), &nativeMargins)); + } + + bool Win32WindowContext::applyFrameMargins(const QMargins &margins) { + const QPointer self(this); + const auto windowRevision = m_windowRevision; + const auto revision = frameMarginsRevision; + const bool applied = extendFrameMargins(margins); + if (!self || m_windowRevision != windowRevision || frameMarginsRevision != revision) + return false; + if (applied) { + appliedFrameMargins = margins; + ++frameMarginsRevision; + } + return applied; + } + + bool Win32WindowContext::supportsSystemBackdrop() const { + // Use the official API on its documented baseline; older builds retain + // the project's private ACCENT_POLICY path instead of being rejected. + return isWin1122H2OrGreater(); + } + + HRESULT Win32WindowContext::querySystemBackdrop(int *type) const { + return DynamicApis::instance().pDwmGetWindowAttribute( + reinterpret_cast(m_windowId), _DWMWA_SYSTEMBACKDROP_TYPE, type, sizeof(*type)); + } + HRESULT Win32WindowContext::setSystemBackdrop(int type) { + return setWindowDwmAttribute(_DWMWA_SYSTEMBACKDROP_TYPE, &type, sizeof(type)); + } + + bool Win32WindowContext::supportsLegacyMica() const { + return isWin11OrGreater(); + } + + HRESULT Win32WindowContext::setWindowDwmAttribute(DWORD attribute, const void *value, DWORD size) { + return DynamicApis::instance().pDwmSetWindowAttribute( + reinterpret_cast(m_windowId), attribute, value, size); + } + + bool Win32WindowContext::setBlurBehind(bool enable) { + if (isWin8OrGreater()) { + if (!DynamicApis::instance().pSetWindowCompositionAttribute) + return false; + ACCENT_POLICY policy{}; + policy.dwAccentState = enable ? ACCENT_ENABLE_BLURBEHIND : ACCENT_DISABLED; + return setAccentPolicy(policy); + } + DWM_BLURBEHIND blur{}; + blur.fEnable = enable; + blur.dwFlags = DWM_BB_ENABLE; + return SUCCEEDED(DynamicApis::instance().pDwmEnableBlurBehindWindow( + reinterpret_cast(m_windowId), &blur)); + } + + bool Win32WindowContext::supportsLegacyAcrylic() const { + const auto &apis = DynamicApis::instance(); + return isWin11OrGreater() && apis.pSetWindowCompositionAttribute; + } + + bool Win32WindowContext::setAccentPolicy(const ACCENT_POLICY &policy) { + auto value = policy; + WINDOWCOMPOSITIONATTRIBDATA data{WCA_ACCENT_POLICY, &value, sizeof(value)}; + return DynamicApis::instance().pSetWindowCompositionAttribute( + reinterpret_cast(m_windowId), &data); + } + + QMargins Win32WindowContext::effectiveExtraMargins(QMargins margins) const { + // A negative margin requests full-client glass; do not narrow that request. + if (windows10BorderInactive && margins.left() >= 0 && margins.top() >= 0 && + margins.right() >= 0 && margins.bottom() >= 0) { + const auto frame = windowAttribute(QStringLiteral("window-rect")).toRect(); + // The inactive Windows 10 border needs the whole title bar extended to + // avoid a transparent seam. Preserve the application's other edges and + // any larger top extension, without storing this temporary correction. + margins.setTop(qMax(margins.top(), -frame.top())); + } + return margins; + } + + bool Win32WindowContext::windowAttributeChanged(const QString &key, const QVariant &attribute) { const auto hwnd = reinterpret_cast(m_windowId); Q_ASSERT(hwnd); + // Owned by the base setter/replay stack, so this remains usable even if + // a synchronous Windows callback deletes the context. + const auto *change = m_attributeChange; const DynamicApis &apis = DynamicApis::instance(); - const auto &extendMargins = [this, &apis, hwnd]() { - // For some unknown reason, the window background is totally black and extending - // the window frame into the client area seems to fix it magically. - // After many times of trying, we found that the Acrylic/Mica/Mica Alt background - // only appears on the native Win32 window's background, so naturally we want to - // extend the window frame into the whole client area to be able to let the special - // material fill the whole window. Previously we are using negative margins because - // it's widely known that using negative margins will let the window frame fill - // the whole window and that's indeed what we wanted to do, however, later we found - // that doing so is causing issues. When the user enabled the "show accent color on - // window title bar and borders" option on system personalize settings, a 30px bar - // would appear on window top. It has the same color with the system accent color. - // Actually it's the original title bar we've already hidden, and it magically - // appears again when we use negative margins to extend the window frame. And again - // after some experiments, I found that the title bar won't appear if we don't extend - // from the top side. In the end I found that we only need to extend from the left - // side if we extend long enough. In this way we can see the special material even - // when the host object is a QWidget and the title bar still remain hidden. But even - // though this solution seems perfect, I really don't know why it works. The following - // hack is totally based on experiments. - static constexpr const MARGINS margins = {65536, 0, 0, 0}; - apis.pDwmExtendFrameIntoClientArea(hwnd, &margins); + const bool material = key == QStringLiteral("mica") || key == QStringLiteral("mica-alt") || + key == QStringLiteral("acrylic-material") || key == QStringLiteral("dwm-blur"); + auto revision = materialRevision; + const auto isCurrent = [this, change, material, &revision]() { + // These keys share native effects and margins. Keep successful nested + // effects, and partial state left by a failed nested rollback, intact. + return change->isCurrent() && (!material || materialRevision == revision); }; - const auto &restoreMargins = [this, &apis, hwnd]() { - auto margins = qmargins2margins( + const auto ownMaterial = [this, &revision]() { revision = ++materialRevision; }; + const auto restoredMargins = [this]() { + return effectiveExtraMargins( windowAttribute(QStringLiteral("extra-margins")).value()); - apis.pDwmExtendFrameIntoClientArea(hwnd, &margins); }; - - const auto &effectBugWorkaround = [this, hwnd]() { + // Keep the historical left-only extension: full negative margins can expose + // the original accent-colored title bar. 65536 covers the client area without + // extending from the top, for both Widgets and Quick. + const QMargins materialMargins(65536, 0, 0, 0); + const auto applyEffect = [this, &isCurrent, &ownMaterial]( + const QMargins &margins, const auto &setEffect, const char *effectName = "Effect") { + // Neither private Mica nor ACCENT_POLICY has a reliable readback contract. + // Change margins first, then leave the old effect untouched if they fail. + const auto previousMargins = appliedFrameMargins; + if (!applyFrameMargins(margins) || !isCurrent()) + return false; + const auto marginRevision = frameMarginsRevision; + const bool accepted = setEffect(); + if (!isCurrent()) + return false; + if (!accepted) { + // Preserve any successful newer extra-margins write in the callback. + if (frameMarginsRevision == marginRevision) { + const bool restored = applyFrameMargins(previousMargins); + if (isCurrent() && !restored) { + ownMaterial(); + qWarning("QWindowKit: %s margins rollback failed; retry the effect update.", effectName); + } + } + return false; + } + ownMaterial(); + return true; + }; + const auto &effectBugWorkaround = [this, hwnd, change, &isCurrent]() { // We don't need the following *HACK* for QWidget windows. // Completely based on actual experiments, root reason is totally unknown. // TODO: add more descriptions if (m_host->isWidgetType()) { - return; + return true; } static const char *kPropKey = "_qwk_effectBugWorkaround1"; if (property(kPropKey).toBool()) { - return; + return true; } setProperty(kPropKey, true); + if (!change->isWindowCurrent()) + return false; RECT rect{}; ::GetWindowRect(hwnd, &rect); ::MoveWindow(hwnd, rect.left, rect.top, 1, 1, FALSE); + if (!change->isWindowCurrent()) + return false; ::MoveWindow(hwnd, rect.right - 1, rect.bottom - 1, 1, 1, FALSE); + if (!change->isWindowCurrent()) + return false; ::MoveWindow(hwnd, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, FALSE); + return isCurrent(); }; if (key == QStringLiteral("no-system-menu")) { @@ -1135,168 +1292,106 @@ namespace QWK { } if (key == QStringLiteral("extra-margins")) { - auto margins = qmargins2margins(attribute.value()); - return SUCCEEDED(apis.pDwmExtendFrameIntoClientArea(hwnd, &margins)); + return applyFrameMargins(effectiveExtraMargins(attribute.value())); } if (key == QStringLiteral("dark-mode")) { - if (!isWin101809OrGreater()) { + if (!isWin101809OrGreater()) return false; - } - - BOOL enable = attribute.toBool(); - if (isWin101903OrGreater()) { - apis.pSetPreferredAppMode(enable ? PAM_AUTO : PAM_DEFAULT); - } else { - apis.pAllowDarkModeForApp(enable); - } + const BOOL enable = attribute.toBool(); const auto attr = isWin1020H1OrGreater() ? _DWMWA_USE_IMMERSIVE_DARK_MODE : _DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1; - apis.pDwmSetWindowAttribute(hwnd, attr, &enable, sizeof(enable)); - + // Reject a failed per-window write before changing process-wide menu policy. + if (FAILED(setWindowDwmAttribute(attr, &enable, sizeof(enable))) || !isCurrent()) + return false; + if (isWin101903OrGreater()) + apis.pSetPreferredAppMode(enable ? PAM_AUTO : PAM_DEFAULT); + else + apis.pAllowDarkModeForApp(enable); + if (!isCurrent()) + return false; apis.pFlushMenuThemes(); - return true; + return isCurrent(); } - // For Win11 or later - if (key == QStringLiteral("mica")) { - if (!isWin11OrGreater()) { + if (key == QStringLiteral("mica") || key == QStringLiteral("mica-alt")) { + const bool modern = supportsSystemBackdrop(); + if (!isCurrent()) return false; - } - if (attribute.toBool()) { - extendMargins(); - if (isWin1122H2OrGreater()) { - // Use official DWM API to enable Mica, available since Windows 11 22H2 - // (10.0.22621). - const _DWM_SYSTEMBACKDROP_TYPE backdropType = _DWMSBT_MAINWINDOW; - apis.pDwmSetWindowAttribute(hwnd, _DWMWA_SYSTEMBACKDROP_TYPE, &backdropType, - sizeof(backdropType)); - } else { - // Use undocumented DWM API to enable Mica, available since Windows 11 - // (10.0.22000). - const BOOL enable = TRUE; - apis.pDwmSetWindowAttribute(hwnd, _DWMWA_MICA_EFFECT, &enable, sizeof(enable)); - } - } else { - if (isWin1122H2OrGreater()) { - const _DWM_SYSTEMBACKDROP_TYPE backdropType = _DWMSBT_AUTO; - apis.pDwmSetWindowAttribute(hwnd, _DWMWA_SYSTEMBACKDROP_TYPE, &backdropType, - sizeof(backdropType)); - } else { - const BOOL enable = FALSE; - apis.pDwmSetWindowAttribute(hwnd, _DWMWA_MICA_EFFECT, &enable, sizeof(enable)); - } - restoreMargins(); - } - effectBugWorkaround(); - return true; + if (!modern && (key == QStringLiteral("mica-alt") || !supportsLegacyMica() || !isCurrent())) + return false; + const BOOL enable = attribute.toBool(); + const int backdrop = !enable ? _DWMSBT_AUTO + : key == QStringLiteral("mica") ? _DWMSBT_MAINWINDOW : _DWMSBT_TABBEDWINDOW; + if (!applyEffect(enable ? materialMargins : restoredMargins(), [&]() { + // Retain the undocumented 22000 Mica attribute 1029 for both + // enable and disable. Do not gate it on the official API baseline. + return SUCCEEDED(modern ? setSystemBackdrop(backdrop) + : setWindowDwmAttribute(_DWMWA_MICA_EFFECT, &enable, sizeof(enable))); + })) + return false; + return isCurrent() && effectBugWorkaround(); } - if (key == QStringLiteral("mica-alt")) { - if (!isWin1122H2OrGreater()) { + if (key == QStringLiteral("acrylic-material")) { + const bool modern = supportsSystemBackdrop(); + if (!isCurrent()) + return false; + if (!modern && (!supportsLegacyAcrylic() || !isCurrent())) { return false; } - if (attribute.toBool()) { - extendMargins(); - // Use official DWM API to enable Mica Alt, available since Windows 11 22H2 - // (10.0.22621). - const _DWM_SYSTEMBACKDROP_TYPE backdropType = _DWMSBT_TABBEDWINDOW; - apis.pDwmSetWindowAttribute(hwnd, _DWMWA_SYSTEMBACKDROP_TYPE, &backdropType, - sizeof(backdropType)); - } else { - const _DWM_SYSTEMBACKDROP_TYPE backdropType = _DWMSBT_AUTO; - apis.pDwmSetWindowAttribute(hwnd, _DWMWA_SYSTEMBACKDROP_TYPE, &backdropType, - sizeof(backdropType)); - restoreMargins(); + + // Preserve the project's original private-API hack (0e9c2e4) on 21H2. + // Unlike the official backdrop attribute, ACCENT_POLICY works before 22621. + // The zero gradient uses the historical #AABBGGRR value; luminosity flags + // are deliberately retained rather than replacing this with ordinary blur. + ACCENT_POLICY requestedAccent{}; + requestedAccent.dwAccentState = attribute.toBool() + ? ACCENT_ENABLE_ACRYLICBLURBEHIND : ACCENT_DISABLED; + requestedAccent.dwAccentFlags = attribute.toBool() + ? ACCENT_ENABLE_ACRYLIC_WITH_LUMINOSITY : ACCENT_NONE; + const auto margins = attribute.toBool() ? materialMargins : restoredMargins(); + if (!modern) { + if (!applyEffect(margins, [&]() { return setAccentPolicy(requestedAccent); }, "Acrylic")) + return false; + return effectBugWorkaround(); } - effectBugWorkaround(); - return true; - } - if (key == QStringLiteral("acrylic-material")) { - if (!isWin11OrGreater()) { + // Snapshot the actual backdrop, which may have been set by Mica/Mica Alt + // or by the application. The cached acrylic boolean cannot describe it. + int previous = _DWMSBT_AUTO; + if (FAILED(querySystemBackdrop(&previous)) || !isCurrent()) + return false; + const int requested = attribute.toBool() ? _DWMSBT_TRANSIENTWINDOW : _DWMSBT_AUTO; + // A failed backdrop write must not leave newly extended margins behind. + if (FAILED(setSystemBackdrop(requested)) || !isCurrent()) + return false; + + // Use the same left-only extension as the other materials (see above). + const bool extended = applyFrameMargins(margins); + // Never roll back onto a replacement HWND or over a successful inner write. + if (!isCurrent()) + return false; + if (!extended) { + const bool restored = SUCCEEDED(setSystemBackdrop(previous)); + if (isCurrent() && !restored) { + ownMaterial(); + // DWM has no atomic backdrop+margins operation. Keep the cache at + // its last successful value, report failure and make drift visible. + // A subsequent explicit write re-queries DWM and can repair it. + qWarning("QWindowKit: Acrylic backdrop rollback failed; retry the effect update."); + } return false; } - if (attribute.toBool()) { - extendMargins(); - - const _DWM_SYSTEMBACKDROP_TYPE backdropType = _DWMSBT_TRANSIENTWINDOW; - apis.pDwmSetWindowAttribute(hwnd, _DWMWA_SYSTEMBACKDROP_TYPE, &backdropType, - sizeof(backdropType)); - - // PRIVATE API REFERENCE: - // QColor gradientColor = {}; - // ACCENT_POLICY policy{}; - // policy.dwAccentState = ACCENT_ENABLE_ACRYLICBLURBEHIND; - // policy.dwAccentFlags = ACCENT_ENABLE_ACRYLIC_WITH_LUMINOSITY; - // // This API expects the #AABBGGRR format. - // policy.dwGradientColor = - // DWORD(qRgba(gradientColor.blue(), gradientColor.green(), - // gradientColor.red(), gradientColor.alpha())); - // WINDOWCOMPOSITIONATTRIBDATA wcad{}; - // wcad.Attrib = WCA_ACCENT_POLICY; - // wcad.pvData = &policy; - // wcad.cbData = sizeof(policy); - // apis.pSetWindowCompositionAttribute(hwnd, &wcad); - } else { - const _DWM_SYSTEMBACKDROP_TYPE backdropType = _DWMSBT_AUTO; - apis.pDwmSetWindowAttribute(hwnd, _DWMWA_SYSTEMBACKDROP_TYPE, &backdropType, - sizeof(backdropType)); - - // PRIVATE API REFERENCE: - // ACCENT_POLICY policy{}; - // policy.dwAccentState = ACCENT_DISABLED; - // policy.dwAccentFlags = ACCENT_NONE; - // WINDOWCOMPOSITIONATTRIBDATA wcad{}; - // wcad.Attrib = WCA_ACCENT_POLICY; - // wcad.pvData = &policy; - // wcad.cbData = sizeof(policy); - // apis.pSetWindowCompositionAttribute(hwnd, &wcad); - - restoreMargins(); - } - effectBugWorkaround(); - return true; + ownMaterial(); + return isCurrent() && effectBugWorkaround(); } if (key == QStringLiteral("dwm-blur")) { // Extending window frame would break this effect for some unknown reason. - restoreMargins(); - if (attribute.toBool()) { - if (isWin8OrGreater()) { - ACCENT_POLICY policy{}; - policy.dwAccentState = ACCENT_ENABLE_BLURBEHIND; - policy.dwAccentFlags = ACCENT_NONE; - WINDOWCOMPOSITIONATTRIBDATA wcad{}; - wcad.Attrib = WCA_ACCENT_POLICY; - wcad.pvData = &policy; - wcad.cbData = sizeof(policy); - apis.pSetWindowCompositionAttribute(hwnd, &wcad); - } else { - DWM_BLURBEHIND bb{}; - bb.fEnable = TRUE; - bb.dwFlags = DWM_BB_ENABLE; - apis.pDwmEnableBlurBehindWindow(hwnd, &bb); - } - } else { - if (isWin8OrGreater()) { - ACCENT_POLICY policy{}; - policy.dwAccentState = ACCENT_DISABLED; - policy.dwAccentFlags = ACCENT_NONE; - WINDOWCOMPOSITIONATTRIBDATA wcad{}; - wcad.Attrib = WCA_ACCENT_POLICY; - wcad.pvData = &policy; - wcad.cbData = sizeof(policy); - apis.pSetWindowCompositionAttribute(hwnd, &wcad); - } else { - DWM_BLURBEHIND bb{}; - bb.fEnable = FALSE; - bb.dwFlags = DWM_BB_ENABLE; - apis.pDwmEnableBlurBehindWindow(hwnd, &bb); - } - } - effectBugWorkaround(); - return true; + if (!applyEffect(restoredMargins(), [&]() { return setBlurBehind(attribute.toBool()); })) + return false; + return isCurrent() && effectBugWorkaround(); } if (key == QStringLiteral("dwm-border-color")) { @@ -1309,8 +1404,8 @@ namespace QWK { QColor color = attribute.value(); COLORREF colorRef = RGB(color.red(), color.green(), color.blue()); - apis.pDwmSetWindowAttribute(hwnd, _DWMWA_BORDER_COLOR, &colorRef, sizeof(colorRef)); - return true; + return SUCCEEDED(setWindowDwmAttribute(_DWMWA_BORDER_COLOR, &colorRef, sizeof(colorRef))) && + isCurrent(); } return false; } @@ -1586,12 +1681,15 @@ namespace QWK { } if (lastHitTestResult == WindowPart::ChromeButton) { + const ManagedWindowGuard guard(this, hWnd); if (message == WM_NCMOUSEMOVE) { // ### FIXME FIXME FIXME // ### FIXME: Calling DefWindowProc() here is really dangerous, investigate // how to avoid doing this. // ### FIXME FIXME FIXME *result = ::DefWindowProcW(hWnd, WM_NCMOUSEMOVE, wParam, lParam); + if (!guard.isCurrent()) + return true; emulateClientAreaMessage(hWnd, message, wParam, lParam); return true; } @@ -1606,6 +1704,8 @@ namespace QWK { // until the menu returns iconButtonClickTime = ::GetTickCount64(); *result = ::DefWindowProcW(hWnd, message, wParam, lParam); + if (!guard.isCurrent()) + return true; iconButtonClickTime = 0; if (iconButtonClickLevel & IconButtonTriggersClose) { ::PostMessageW(hWnd, WM_SYSCOMMAND, SC_CLOSE, 0); @@ -2029,7 +2129,7 @@ namespace QWK { *result = isInTopBorder ? HTTOP : HTBOTTOM; } } else { - *result = HTCLIENT; + *result = isInTitleBar ? HTCAPTION : HTCLIENT; } return true; } else { @@ -2550,10 +2650,19 @@ namespace QWK { } if (shouldShowSystemMenu) { + const ManagedWindowGuard guard(this, hWnd); static HHOOK mouseHook = nullptr; static std::optional mouseClickPos; static bool mouseDoubleClicked = false; bool mouseHookedLocal = false; + const auto cleanup = qScopeGuard([&mouseHookedLocal] { + if (!mouseHookedLocal) + return; + ::UnhookWindowsHookEx(mouseHook); + mouseHook = nullptr; + mouseClickPos.reset(); + mouseDoubleClicked = false; + }); // The menu is triggered by a click on icon button if (iconButtonClickTime > 0) { @@ -2599,19 +2708,22 @@ namespace QWK { return ::CallNextHookEx(nullptr, nCode, wParam, lParam); }, nullptr, ::GetCurrentThreadId()); - mouseHookedLocal = true; + mouseHookedLocal = mouseHook != nullptr; } } #undef MOUSE_HOOK bool res = - showSystemMenu_sys(hWnd, nativeGlobalPos, broughtByKeyboard, isHostSizeFixed()); + showSystemMenu_sys(this, hWnd, nativeGlobalPos, broughtByKeyboard, isHostSizeFixed()); - // Uninstall mouse hook and check if it's a double-click - if (mouseHookedLocal) { - ::UnhookWindowsHookEx(mouseHook); + // cleanup owns the hook independently of this context's lifetime. + *result = FALSE; + if (!guard.isCurrent()) + return true; + // Check the recorded click before the scope guard uninstalls the hook. + if (mouseHookedLocal) { // Emulate the Windows icon button's behavior if (!res && mouseClickPos.has_value()) { POINT nativeLocalPos = mouseClickPos.value(); @@ -2631,10 +2743,6 @@ namespace QWK { if (mouseDoubleClicked) { iconButtonClickLevel |= IconButtonDoubleClicked; } - - mouseHook = nullptr; - mouseClickPos.reset(); - mouseDoubleClicked = false; } // QPA's internal code will handle system menu events separately, and its diff --git a/src/core/contexts/win32windowcontext_p.h b/src/core/contexts/win32windowcontext_p.h index a409b65a..e2d5fbaf 100644 --- a/src/core/contexts/win32windowcontext_p.h +++ b/src/core/contexts/win32windowcontext_p.h @@ -14,11 +14,14 @@ // version without notice, or may even be removed. // +#include #include #include namespace QWK { + struct ACCENT_POLICY; + class Win32WindowContext : public AbstractWindowContext { Q_OBJECT public: @@ -36,14 +39,31 @@ namespace QWK { Q_ENUM(WindowPart) QString key() const override; - void virtual_hook(int id, void *data) override; + void raiseWindow() override; + void showSystemMenu(const QPoint &pos) override; +#if QWINDOWKIT_CONFIG(ENABLE_WINDOWS_SYSTEM_BORDERS) + QColor windows10BorderColor() const override; + void setWindows10BorderActive(bool active) override; + void drawWindows10Border() override; +#endif QVariant windowAttribute(const QString &key) const override; protected: void winIdChanged(WId winId, WId oldWinId) override; - bool windowAttributeChanged(const QString &key, const QVariant &attribute, - const QVariant &oldAttribute) override; + bool windowAttributeChanged(const QString &key, const QVariant &attribute) override; + QMargins effectiveExtraMargins(QMargins margins) const; + virtual bool extendFrameMargins(const QMargins &margins); + bool applyFrameMargins(const QMargins &margins); + // Narrow DWM boundary for backdrop capability and failure-path regression tests. + virtual bool supportsSystemBackdrop() const; + virtual HRESULT querySystemBackdrop(int *type) const; + virtual HRESULT setSystemBackdrop(int type); + virtual bool supportsLegacyMica() const; + virtual HRESULT setWindowDwmAttribute(DWORD attribute, const void *value, DWORD size); + virtual bool setBlurBehind(bool enable); + virtual bool supportsLegacyAcrylic() const; + virtual bool setAccentPolicy(const ACCENT_POLICY &policy); public: bool windowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, LRESULT *result); @@ -79,6 +99,10 @@ namespace QWK { // Attributes bool noSystemMenu = false; + bool windows10BorderInactive = false; + QMargins appliedFrameMargins; + quint64 frameMarginsRevision = 0; + quint64 materialRevision = 0; // Native HWNDs can be recreated while the logical QWindow stays alive. Keep the last // stable native frame rect so we can prevent Qt's recreate path from applying a stale diff --git a/src/core/contexts/x11context.cpp b/src/core/contexts/x11context.cpp new file mode 100644 index 00000000..4f25124b --- /dev/null +++ b/src/core/contexts/x11context.cpp @@ -0,0 +1,106 @@ +// Copyright (C) 2021-2023 wangwenx190 (Yuhang Zhao) +// Copyright (C) 2023-present Stdware Collections (https://www.github.com/stdware) +// Copyright (C) 2025-2027 Wing-summer (wingsummer) +// SPDX-License-Identifier: Apache-2.0 + + +#include "x11context_p.h" + +#include "qwindowkit_x11.h" + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +// Xlib client-message layout; Display remains opaque. +typedef struct { + int type; + unsigned long serial; /* # of last request processed by server */ + Bool send_event; /* true if this came from a SendEvent request */ + Display *display; /* Display the event was read from */ + Window window; + Atom message_type; + int format; + union { + char b[20]; + short s[10]; + long l[5]; + } data; +} XClientMessageEvent; + +/* + * this union is defined so Xlib can always use the same sized + * event structure internally, to avoid memory fragmentation. + */ +union _XEvent { + // delete so many members but size unchanged because of pad + XClientMessageEvent xclient; + long pad[24]; +}; + +namespace QWK { + + X11Context::X11Context() = default; + + X11Context::~X11Context() = default; + + QString X11Context::key() const { + return QStringLiteral("xcb"); + } + + void X11Context::showSystemMenu(const QPoint &pos) { + // showSystemMenu() is public API and may be called before the window is created or + // after it has been destroyed. + if (!m_windowId || !m_windowHandle) { + return; + } + + auto *x11app = qApp->nativeInterface(); + if (!x11app) { + return; + } + + auto display = x11app->display(); + if (!display) { + return; + } + + const auto &api = QWK::Private::x11API(); + Q_ASSERT(api.isValid()); + + // some marcos to constexpr in X11 + constexpr auto None = 0L; + constexpr auto ClientMessage = 33; + constexpr auto False = 0; + constexpr auto Button3 = 3; + constexpr auto SubstructureNotifyMask = 1L << 19; + constexpr auto SubstructureRedirectMask = 1L << 20; + + // use window id (XID) + auto xwin = static_cast(m_windowId); + Atom atom = api.XInternAtom(display, "_GTK_SHOW_WINDOW_MENU", False); + if (atom == None) + return; // WM might not support this atom + XEvent ev{}; + ev.xclient.type = ClientMessage; + ev.xclient.window = xwin; + ev.xclient.message_type = atom; + + // The format member is set to 8, 16, or 32 + // and specifies whether the data should be viewed as + // a list of bytes, shorts, or longs - typeof(xclient.data). + ev.xclient.format = 32; + + qreal dpr = m_windowHandle->devicePixelRatio(); + int root_x = qRound(pos.x() * dpr); + int root_y = qRound(pos.y() * dpr); + + ev.xclient.data.l[0] = Button3; // right button + ev.xclient.data.l[1] = root_x; + ev.xclient.data.l[2] = root_y; + + Window root = api.XDefaultRootWindow(display); + api.XUngrabPointer(display, 0L); + api.XSendEvent(display, root, False, SubstructureRedirectMask | SubstructureNotifyMask, + &ev); + api.XFlush(display); + } +} +#endif // QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) diff --git a/src/core/contexts/linuxx11context_p.h b/src/core/contexts/x11context_p.h similarity index 72% rename from src/core/contexts/linuxx11context_p.h rename to src/core/contexts/x11context_p.h index 741a29f3..b7c47bb1 100644 --- a/src/core/contexts/linuxx11context_p.h +++ b/src/core/contexts/x11context_p.h @@ -3,8 +3,8 @@ // Copyright (C) 2025-2027 Wing-summer (wingsummer) // SPDX-License-Identifier: Apache-2.0 -#ifndef LINUXX11CONTEXT_P_H -#define LINUXX11CONTEXT_P_H +#ifndef X11CONTEXT_P_H +#define X11CONTEXT_P_H // // W A R N I N G !!! @@ -20,16 +20,16 @@ #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) namespace QWK { - class LinuxX11Context : public QtWindowContext { + class X11Context : public QtWindowContext { Q_OBJECT public: - LinuxX11Context(); - ~LinuxX11Context() override; + X11Context(); + ~X11Context() override; QString key() const override; - void virtual_hook(int id, void *data) override; + void showSystemMenu(const QPoint &pos) override; }; } #endif // QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) -#endif // LINUXX11CONTEXT_P_H +#endif // X11CONTEXT_P_H diff --git a/src/core/kernel/eventdispatch_p.h b/src/core/kernel/eventdispatch_p.h new file mode 100644 index 00000000..4fcb0ca4 --- /dev/null +++ b/src/core/kernel/eventdispatch_p.h @@ -0,0 +1,93 @@ +// Copyright (C) 2023-present Stdware Collections (https://www.github.com/stdware) +// SPDX-License-Identifier: Apache-2.0 + +#ifndef EVENTDISPATCH_P_H +#define EVENTDISPATCH_P_H + +// Implementation detail shared by the native and QObject event dispatchers. +// This file is not part of the QWindowKit API. + +#include +#include +#include + +namespace QWK::Private { + + template + class EventDispatchState { + public: + EventDispatchState() = default; + + template + void install(Filter *filter, Dispatcher *dispatcher, Dispatcher *Filter::*owner) { + if (!filter || filter->*owner) + return; + m_filters.append(filter); + filter->*owner = dispatcher; + } + + template + void remove(Filter *filter, Dispatcher *Filter::*owner) { + const qsizetype index = m_filters.indexOf(filter); + if (index < 0) + return; + if (m_depth > 0) + m_filters[index] = nullptr; + else + m_filters.removeAt(index); + filter->*owner = nullptr; + } + + // Called by the owning dispatcher before its state is destroyed. + template + void detach(Dispatcher *Filter::*owner) { + *m_alive = false; + for (Filter *filter : std::as_const(m_filters)) { + if (filter) + filter->*owner = nullptr; + } + } + + template + bool dispatch(Callback callback) { + // Keep a token for this specific dispatcher lifetime, not its address. A callback + // may destroy the dispatcher and immediately construct another at the same address. + const auto alive = m_alive; + ++m_depth; + bool filtered = false; + // New filters participate in this dispatch. Removals leave tombstones until the + // outermost frame returns, keeping every nested frame's index valid. Never retain + // a list iterator or reference across callbacks, which may reallocate the list. + for (qsizetype i = 0; i < m_filters.size(); ++i) { + Filter *filter = m_filters.at(i); + if (!filter) + continue; + const bool consumed = callback(filter); + // Do not touch this state (including depth/cleanup) after owner destruction. + // Consume the event so the caller cannot forward through its deleted context. + if (!*alive) + return true; + if (consumed) { + filtered = true; + break; + } + } + if (--m_depth == 0) + m_filters.removeAll(nullptr); + return filtered; + } + + bool isEmpty() const { return m_filters.isEmpty(); } + bool isIdleAndEmpty() const { return m_depth == 0 && isEmpty(); } + + private: + QList m_filters; + int m_depth = 0; + std::shared_ptr m_alive = std::make_shared(true); + + Q_DISABLE_COPY(EventDispatchState) + }; + +} + +#endif // EVENTDISPATCH_P_H diff --git a/src/core/kernel/nativeeventfilter.cpp b/src/core/kernel/nativeeventfilter.cpp index ed5f37ac..84f01a83 100644 --- a/src/core/kernel/nativeeventfilter.cpp +++ b/src/core/kernel/nativeeventfilter.cpp @@ -19,59 +19,24 @@ namespace QWK { NativeEventDispatcher::NativeEventDispatcher() = default; NativeEventDispatcher::~NativeEventDispatcher() { - for (const auto &observer : std::as_const(m_nativeEventFilters)) { - if (!observer) - continue; - observer->m_nativeDispatcher = nullptr; - } + m_nativeDispatch.detach(&NativeEventFilter::m_nativeDispatcher); } bool NativeEventDispatcher::nativeDispatch(const QByteArray &eventType, void *message, QT_NATIVE_EVENT_RESULT_TYPE *result) { - // A callback is free to install or remove filters, including itself, and it may even - // re-enter this function because handling a native event can pump more native events. - // Iterate by index and re-read the size on every step, and rely on removals leaving a - // null tombstone behind so that the indexes of the outer dispatches stay valid. This - // mirrors how QObject's event filter list is walked. - ++m_nativeDispatchDepth; - bool filtered = false; - for (qsizetype i = 0; i < m_nativeEventFilters.size(); ++i) { - NativeEventFilter *ef = m_nativeEventFilters.at(i); - if (!ef) - continue; - if (ef->nativeEventFilter(eventType, message, result)) { - filtered = true; - break; - } - } - if (--m_nativeDispatchDepth == 0) { - m_nativeEventFilters.removeAll(nullptr); - } - return filtered; + return m_nativeDispatch.dispatch([&eventType, message, result](NativeEventFilter *filter) { + return filter->nativeEventFilter(eventType, message, result); + }); } void NativeEventDispatcher::installNativeEventFilter(NativeEventFilter *filter) { - if (!filter || filter->m_nativeDispatcher) - return; - - m_nativeEventFilters.append(filter); - filter->m_nativeDispatcher = this; + m_nativeDispatch.install(filter, this, &NativeEventFilter::m_nativeDispatcher); } void NativeEventDispatcher::removeNativeEventFilter(NativeEventFilter *filter) { - const qsizetype index = m_nativeEventFilters.indexOf(filter); - if (index < 0) { - return; - } - if (m_nativeDispatchDepth > 0) { - m_nativeEventFilters[index] = nullptr; - } else { - m_nativeEventFilters.removeAt(index); - } - filter->m_nativeDispatcher = nullptr; + m_nativeDispatch.remove(filter, &NativeEventFilter::m_nativeDispatcher); } - // Avoid adding multiple global native event filters to QGuiApplication // in this library. class AppMasterNativeEventFilter : public QAbstractNativeEventFilter, @@ -107,7 +72,7 @@ namespace QWK { // Never destroy the master from inside its own dispatch, its stack frame is still // alive. It stays registered with an empty filter list instead, and the next // AppNativeEventFilter simply picks it up again. - if (master->m_nativeDispatchDepth == 0 && master->m_nativeEventFilters.isEmpty()) { + if (master->m_nativeDispatch.isIdleAndEmpty()) { delete std::exchange(AppMasterNativeEventFilter::instance, nullptr); } } diff --git a/src/core/kernel/nativeeventfilter_p.h b/src/core/kernel/nativeeventfilter_p.h index bc7047a8..c70b9534 100644 --- a/src/core/kernel/nativeeventfilter_p.h +++ b/src/core/kernel/nativeeventfilter_p.h @@ -15,6 +15,7 @@ // #include +#include namespace QWK { @@ -34,9 +35,7 @@ namespace QWK { void removeNativeEventFilter(NativeEventFilter *filter); protected: - QList m_nativeEventFilters; - - int m_nativeDispatchDepth = 0; + Private::EventDispatchState m_nativeDispatch; friend class NativeEventFilter; diff --git a/src/core/kernel/sharedeventfilter.cpp b/src/core/kernel/sharedeventfilter.cpp index 520ac99d..876d9d67 100644 --- a/src/core/kernel/sharedeventfilter.cpp +++ b/src/core/kernel/sharedeventfilter.cpp @@ -16,55 +16,21 @@ namespace QWK { SharedEventDispatcher::SharedEventDispatcher() = default; SharedEventDispatcher::~SharedEventDispatcher() { - for (const auto &observer : std::as_const(m_sharedEventFilters)) { - if (!observer) - continue; - observer->m_sharedDispatcher = nullptr; - } + m_sharedDispatch.detach(&SharedEventFilter::m_sharedDispatcher); } bool SharedEventDispatcher::sharedDispatch(QObject *obj, QEvent *event) { - // A callback is free to install or remove filters, including itself, and it may even - // re-enter this function because handling an event can deliver more events. Iterate by - // index and re-read the size on every step, and rely on removals leaving a null - // tombstone behind so that the indexes of the outer dispatches stay valid. This mirrors - // how QObject's event filter list is walked. - ++m_sharedDispatchDepth; - bool filtered = false; - for (qsizetype i = 0; i < m_sharedEventFilters.size(); ++i) { - SharedEventFilter *ef = m_sharedEventFilters.at(i); - if (!ef) - continue; - if (ef->sharedEventFilter(obj, event)) { - filtered = true; - break; - } - } - if (--m_sharedDispatchDepth == 0) { - m_sharedEventFilters.removeAll(nullptr); - } - return filtered; + return m_sharedDispatch.dispatch([obj, event](SharedEventFilter *filter) { + return filter->sharedEventFilter(obj, event); + }); } void SharedEventDispatcher::installSharedEventFilter(SharedEventFilter *filter) { - if (!filter || filter->m_sharedDispatcher) - return; - - m_sharedEventFilters.append(filter); - filter->m_sharedDispatcher = this; + m_sharedDispatch.install(filter, this, &SharedEventFilter::m_sharedDispatcher); } void SharedEventDispatcher::removeSharedEventFilter(SharedEventFilter *filter) { - const qsizetype index = m_sharedEventFilters.indexOf(filter); - if (index < 0) { - return; - } - if (m_sharedDispatchDepth > 0) { - m_sharedEventFilters[index] = nullptr; - } else { - m_sharedEventFilters.removeAt(index); - } - filter->m_sharedDispatcher = nullptr; + m_sharedDispatch.remove(filter, &SharedEventFilter::m_sharedDispatcher); } } diff --git a/src/core/kernel/sharedeventfilter_p.h b/src/core/kernel/sharedeventfilter_p.h index da35b0ef..7d4fcdee 100644 --- a/src/core/kernel/sharedeventfilter_p.h +++ b/src/core/kernel/sharedeventfilter_p.h @@ -15,6 +15,7 @@ // #include +#include namespace QWK { @@ -33,9 +34,7 @@ namespace QWK { void removeSharedEventFilter(SharedEventFilter *filter); protected: - QList m_sharedEventFilters; - - int m_sharedDispatchDepth = 0; + Private::EventDispatchState m_sharedDispatch; friend class SharedEventFilter; diff --git a/src/core/kernel/winidchangeeventfilter.cpp b/src/core/kernel/winidchangeeventfilter.cpp index 337e4581..5d5957d5 100644 --- a/src/core/kernel/winidchangeeventfilter.cpp +++ b/src/core/kernel/winidchangeeventfilter.cpp @@ -26,18 +26,22 @@ namespace QWK { } bool WindowWinIdChangeEventFilter::eventFilter(QObject *obj, QEvent *event) { - Q_UNUSED(obj) - if (event->type() == QEvent::PlatformSurface) { - auto e = static_cast(event); - if (e->surfaceEventType() == QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed) { - isAboutToBeDestroyed = true; - context->notifyWinIdChange(); - isAboutToBeDestroyed = false; - } else { - context->notifyWinIdChange(); - } + if (event->type() != QEvent::PlatformSurface) + return false; + const QPointer receiver(obj); + const QPointer self(this); + auto e = static_cast(event); + if (e->surfaceEventType() == QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed) { + const bool previous = isAboutToBeDestroyed; + isAboutToBeDestroyed = true; + context->notifyWinIdChange(); + if (self) + self->isAboutToBeDestroyed = previous; + } else { + context->notifyWinIdChange(); } - return false; + // Qt must not deliver the event to a receiver deleted by a shared callback. + return !receiver; } } diff --git a/src/core/qwindowkit_linux.cpp b/src/core/qwindowkit_linux.cpp deleted file mode 100644 index f8e2fbf6..00000000 --- a/src/core/qwindowkit_linux.cpp +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (C) 2021-2023 wangwenx190 (Yuhang Zhao) -// Copyright (C) 2023-present Stdware Collections (https://www.github.com/stdware) -// Copyright (C) 2025-2027 Wing-summer (wingsummer) -// SPDX-License-Identifier: Apache-2.0 - -#include "qwindowkit_linux.h" - -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) -#include -#include - -namespace QWK { - namespace Private { - - bool isX11Platform() { - static const bool isX11 = QGuiApplication::platformName().startsWith( - QStringLiteral("xcb"), Qt::CaseInsensitive); - return isX11; - } - - bool isWaylandPlatform() { - static const bool isWayland = QGuiApplication::platformName().startsWith( - QStringLiteral("wayland"), Qt::CaseInsensitive); - return isWayland; - } - - const LinuxWaylandAPI &waylandAPI() { - static LinuxWaylandAPI api; - static bool guard = true; - if (guard && isWaylandPlatform()) { - QLibrary waylib(QStringLiteral("libwayland-client.so")); - bool loaded = false; - if (waylib.load()) { - loaded = true; - } else { - waylib.setFileName(QStringLiteral("libwayland-client.so.0")); - if (waylib.load()) { - loaded = true; - } - } - - if (loaded) { - api.wl_display_flush = reinterpret_cast( - waylib.resolve("wl_display_flush")); - api.wl_proxy_marshal_flags = - reinterpret_cast( - waylib.resolve("wl_proxy_marshal_flags")); - api.wl_proxy_get_version = - reinterpret_cast( - waylib.resolve("wl_proxy_get_version")); - } - } - guard = false; - return api; - } - - const LinuxX11API &x11API() { - static LinuxX11API api; - static bool guard = true; - if (guard && isX11Platform()) { - QString libName = QStringLiteral( -#if defined(__CYGWIN__) - "libX11-6.so" -#elif defined(__OpenBSD__) || defined(__NetBSD__) - "libX11.so" -#else - "libX11.so.6" -#endif - ); - QLibrary x11lib(libName); - if (x11lib.load()) { - api.XInternAtom = - reinterpret_cast(x11lib.resolve("XInternAtom")); - api.XSendEvent = - reinterpret_cast(x11lib.resolve("XSendEvent")); - api.XFlush = reinterpret_cast(x11lib.resolve("XFlush")); - api.XUngrabPointer = reinterpret_cast( - x11lib.resolve("XUngrabPointer")); - } - } - guard = false; - return api; - } - - } -} -#endif // QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) diff --git a/src/core/qwindowkit_wayland.cpp b/src/core/qwindowkit_wayland.cpp new file mode 100644 index 00000000..4de73564 --- /dev/null +++ b/src/core/qwindowkit_wayland.cpp @@ -0,0 +1,53 @@ +// Copyright (C) 2021-2023 wangwenx190 (Yuhang Zhao) +// Copyright (C) 2023-present Stdware Collections (https://www.github.com/stdware) +// Copyright (C) 2025-2027 Wing-summer (wingsummer) +// SPDX-License-Identifier: Apache-2.0 + +#include "qwindowkit_wayland.h" + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +#include +#include + +namespace QWK { + namespace Private { + + bool isWaylandPlatform() { + static const bool isWayland = QGuiApplication::platformName().startsWith( + QStringLiteral("wayland"), Qt::CaseInsensitive); + return isWayland; + } + + const WaylandAPI &waylandAPI() { + static WaylandAPI api; + static bool guard = true; + if (guard && isWaylandPlatform()) { + QLibrary waylib(QStringLiteral("libwayland-client.so")); + bool loaded = false; + if (waylib.load()) { + loaded = true; + } else { + waylib.setFileName(QStringLiteral("libwayland-client.so.0")); + if (waylib.load()) { + loaded = true; + } + } + + if (loaded) { + api.wl_display_flush = reinterpret_cast( + waylib.resolve("wl_display_flush")); + api.wl_proxy_marshal_flags = + reinterpret_cast( + waylib.resolve("wl_proxy_marshal_flags")); + api.wl_proxy_get_version = + reinterpret_cast( + waylib.resolve("wl_proxy_get_version")); + } + } + guard = false; + return api; + } + + } +} +#endif // QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) diff --git a/src/core/qwindowkit_linux.h b/src/core/qwindowkit_wayland.h similarity index 55% rename from src/core/qwindowkit_linux.h rename to src/core/qwindowkit_wayland.h index 62d51d84..ec6f7a1f 100644 --- a/src/core/qwindowkit_linux.h +++ b/src/core/qwindowkit_wayland.h @@ -3,8 +3,8 @@ // Copyright (C) 2025-2027 Wing-summer (wingsummer) // SPDX-License-Identifier: Apache-2.0 -#ifndef QWINDOWKIT_LINUX_H -#define QWINDOWKIT_LINUX_H +#ifndef QWINDOWKIT_WAYLAND_H +#define QWINDOWKIT_WAYLAND_H // // W A R N I N G !!! @@ -20,42 +20,14 @@ #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #include -// some declarations about x11 -using Atom = unsigned long; -using Bool = int; -using XID = unsigned long; -using Window = XID; - -union _XEvent; -using XEvent = union _XEvent; - // for wayland struct wl_proxy; namespace QWK { namespace Private { - struct LinuxX11API { - LinuxX11API() = default; - Q_DISABLE_COPY(LinuxX11API) - - using XInternAtomFn = Atom (*)(Display *, const char *, Bool); - using XSendEventFn = int (*)(Display *, Window, Bool, long, XEvent *); - using XFlushFn = int (*)(Display *); - using XUngrabPointerFn = int (*)(Display *, unsigned long); - - XInternAtomFn XInternAtom = nullptr; - XSendEventFn XSendEvent = nullptr; - XFlushFn XFlush = nullptr; - XUngrabPointerFn XUngrabPointer = nullptr; - - inline bool isValid() const { - return XInternAtom && XSendEvent && XFlush && XUngrabPointer; - } - }; - - struct LinuxWaylandAPI { - LinuxWaylandAPI() = default; - Q_DISABLE_COPY(LinuxWaylandAPI) + struct WaylandAPI { + WaylandAPI() = default; + Q_DISABLE_COPY(WaylandAPI) using wl_display_flush_fn = int (*)(struct wl_display *); using wl_proxy_marshal_flags_fn = void (*)(struct wl_proxy *, uint32_t, @@ -75,12 +47,8 @@ namespace QWK { bool isWaylandPlatform(); - bool isX11Platform(); - - const LinuxX11API &x11API(); - - const LinuxWaylandAPI &waylandAPI(); + const WaylandAPI &waylandAPI(); } } #endif // QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) -#endif // QWINDOWKIT_LINUX_H +#endif // QWINDOWKIT_WAYLAND_H diff --git a/src/core/qwindowkit_windows.cpp b/src/core/qwindowkit_windows.cpp index bd068373..b10e0a11 100644 --- a/src/core/qwindowkit_windows.cpp +++ b/src/core/qwindowkit_windows.cpp @@ -23,70 +23,28 @@ namespace QWK { namespace Private { - QWK_OSVERSIONINFOW GetRealOSVersion() { - static const auto result = GetRealOSVersionImpl(); - return result; - } - - } - -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) - WindowsRegistryKey::WindowsRegistryKey(HKEY parentHandle, QStringView subKey, - REGSAM permissions, REGSAM access) { - if (::RegOpenKeyExW(parentHandle, reinterpret_cast(subKey.utf16()), 0, - permissions | access, &m_key) != ERROR_SUCCESS) { - m_key = nullptr; - } - } - - WindowsRegistryKey::~WindowsRegistryKey() { - close(); - } - - void WindowsRegistryKey::close() { - if (isValid()) { - ::RegCloseKey(std::exchange(m_key, nullptr)); + std::optional readRegistryDword(HKEY parent, const wchar_t *subKey, + const wchar_t *valueName) { + HKEY key = nullptr; + if (::RegOpenKeyExW(parent, subKey, 0, KEY_READ, &key) != ERROR_SUCCESS) + return std::nullopt; + + DWORD value = 0; + DWORD type = 0; + DWORD size = sizeof(value); + // Read type and data together: a value may change between separate queries. + const auto status = ::RegQueryValueExW(key, valueName, nullptr, &type, + reinterpret_cast(&value), &size); + ::RegCloseKey(key); + if (status != ERROR_SUCCESS || type != REG_DWORD || size != sizeof(value)) + return std::nullopt; + return value; } - } - QString WindowsRegistryKey::stringValue(QStringView subKey) const { - QString result; - if (!isValid()) - return result; - DWORD type; - DWORD size; - auto subKeyC = reinterpret_cast(subKey.utf16()); - if (::RegQueryValueExW(m_key, subKeyC, nullptr, &type, nullptr, &size) != ERROR_SUCCESS || - (type != REG_SZ && type != REG_EXPAND_SZ) || size <= 2) { + QWK_OSVERSIONINFOW GetRealOSVersion() { + static const auto result = GetRealOSVersionImpl(); return result; } - // Reserve more for rare cases where trailing '\0' are missing in registry. - // Rely on 0-termination since strings of size 256 padded with 0 have been - // observed (QTBUG-84455). - size += 2; - QVarLengthArray buffer(static_cast(size)); - std::fill(buffer.data(), buffer.data() + size, 0u); - if (::RegQueryValueExW(m_key, subKeyC, nullptr, &type, buffer.data(), &size) == - ERROR_SUCCESS) - result = QString::fromWCharArray(reinterpret_cast(buffer.constData())); - return result; - } - std::pair WindowsRegistryKey::dwordValue(QStringView subKey) const { - if (!isValid()) - return std::make_pair(0, false); - DWORD type; - auto subKeyC = reinterpret_cast(subKey.utf16()); - if (::RegQueryValueExW(m_key, subKeyC, nullptr, &type, nullptr, nullptr) != ERROR_SUCCESS || - type != REG_DWORD) { - return std::make_pair(0, false); - } - DWORD value = 0; - DWORD size = sizeof(value); - const bool ok = - ::RegQueryValueExW(m_key, subKeyC, nullptr, nullptr, - reinterpret_cast(&value), &size) == ERROR_SUCCESS; - return std::make_pair(value, ok); } -#endif -} \ No newline at end of file +} diff --git a/src/core/qwindowkit_windows.h b/src/core/qwindowkit_windows.h index 55e739d0..4301d71f 100644 --- a/src/core/qwindowkit_windows.h +++ b/src/core/qwindowkit_windows.h @@ -16,9 +16,8 @@ #include #include -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) -# include -#endif +#include +#include #include @@ -76,6 +75,10 @@ namespace QWK { QWK_CORE_EXPORT QWK_OSVERSIONINFOW GetRealOSVersion(); + // A missing, unreadable or non-DWORD value is distinct from a valid zero. + QWK_CORE_EXPORT std::optional readRegistryDword(HKEY parent, const wchar_t *subKey, + const wchar_t *valueName); + inline bool IsWindows1122H2OrGreater_Real() { QWK_OSVERSIONINFOW rovi = GetRealOSVersion(); return (rovi.dwMajorVersion > 10) || @@ -97,10 +100,6 @@ namespace QWK { (rovi.dwMinorVersion > 0 || rovi.dwBuildNumber >= 19041)); } - inline bool IsWindows102004OrGreater_Real() { - return IsWindows1020H1OrGreater_Real(); - } - inline bool IsWindows101903OrGreater_Real() { QWK_OSVERSIONINFOW rovi = GetRealOSVersion(); return (rovi.dwMajorVersion > 10) || @@ -108,10 +107,6 @@ namespace QWK { (rovi.dwMinorVersion > 0 || rovi.dwBuildNumber >= 18362)); } - inline bool IsWindows1019H1OrGreater_Real() { - return IsWindows101903OrGreater_Real(); - } - inline bool IsWindows101809OrGreater_Real() { QWK_OSVERSIONINFOW rovi = GetRealOSVersion(); return (rovi.dwMajorVersion > 10) || @@ -119,10 +114,6 @@ namespace QWK { (rovi.dwMinorVersion > 0 || rovi.dwBuildNumber >= 17763)); } - inline bool IsWindows10RS5OrGreater_Real() { - return IsWindows101809OrGreater_Real(); - } - inline bool IsWindows10OrGreater_Real() { QWK_OSVERSIONINFOW rovi = GetRealOSVersion(); return rovi.dwMajorVersion >= 10; @@ -140,65 +131,7 @@ namespace QWK { (rovi.dwMajorVersion == 6 && rovi.dwMinorVersion >= 2); } - inline bool IsWindows10Only_Real() { - return IsWindows10OrGreater_Real() && !IsWindows11OrGreater_Real(); - } - - } - - // - // Registry Helpers - // - -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) - class QWK_CORE_EXPORT WindowsRegistryKey { - public: - WindowsRegistryKey(HKEY parentHandle, QStringView subKey, REGSAM permissions = KEY_READ, - REGSAM access = 0); - - ~WindowsRegistryKey(); - - inline bool isValid() const; - - void close(); - QString stringValue(QStringView subKey) const; - std::pair dwordValue(QStringView subKey) const; - - private: - HKEY m_key; - - Q_DISABLE_COPY(WindowsRegistryKey) - }; - - inline bool WindowsRegistryKey::isValid() const { - return m_key != nullptr; - } -#elif QT_VERSION < QT_VERSION_CHECK(6, 8, 1) - using WindowsRegistryKey = QWinRegistryKey; -#else - class WindowsRegistryKey : public QWinRegistryKey { -#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 1) - using SubKeyType = const wchar_t*; -#else - using SubKeyType = QStringView; -#endif - public: - WindowsRegistryKey(HKEY parentHandle, SubKeyType subKey, - REGSAM permissions = KEY_READ, REGSAM access = 0) - : QWinRegistryKey(parentHandle, subKey, permissions, access) { - } - - inline std::pair dwordValue(SubKeyType subKey) const; - }; - - inline std::pair WindowsRegistryKey::dwordValue(SubKeyType subKey) const { - const auto val = value(subKey); - if (!val) { - return {0, false}; - } - return {val.value(), true}; } -#endif // // Version Helpers @@ -224,28 +157,16 @@ namespace QWK { return result; } - inline bool isWin10RS5OrGreater() { - return isWin101809OrGreater(); - } - inline bool isWin101903OrGreater() { static const bool result = Private::IsWindows101903OrGreater_Real(); return result; } - inline bool isWin1019H1OrGreater() { - return isWin101903OrGreater(); - } - inline bool isWin1020H1OrGreater() { static const bool result = Private::IsWindows1020H1OrGreater_Real(); return result; } - inline bool isWin102004OrGreater() { - return isWin1020H1OrGreater(); - } - inline bool isWin11OrGreater() { static const bool result = Private::IsWindows11OrGreater_Real(); return result; @@ -256,11 +177,6 @@ namespace QWK { return result; } - inline bool isWin10Only() { - static const bool result = Private::IsWindows10Only_Real(); - return result; - }; - // // Native Event Helpers // diff --git a/src/core/qwindowkit_x11.cpp b/src/core/qwindowkit_x11.cpp new file mode 100644 index 00000000..d01fdf31 --- /dev/null +++ b/src/core/qwindowkit_x11.cpp @@ -0,0 +1,53 @@ +// Copyright (C) 2021-2023 wangwenx190 (Yuhang Zhao) +// Copyright (C) 2023-present Stdware Collections (https://www.github.com/stdware) +// Copyright (C) 2025-2027 Wing-summer (wingsummer) +// SPDX-License-Identifier: Apache-2.0 + +#include "qwindowkit_x11.h" + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +#include +#include + +namespace QWK { + namespace Private { + + bool isX11Platform() { + static const bool isX11 = QGuiApplication::platformName().startsWith( + QStringLiteral("xcb"), Qt::CaseInsensitive); + return isX11; + } + + const X11API &x11API() { + static X11API api; + static bool guard = true; + if (guard && isX11Platform()) { + QString libName = QStringLiteral( +#if defined(__CYGWIN__) + "libX11-6.so" +#elif defined(__OpenBSD__) || defined(__NetBSD__) + "libX11.so" +#else + "libX11.so.6" +#endif + ); + QLibrary x11lib(libName); + if (x11lib.load()) { + api.XInternAtom = + reinterpret_cast(x11lib.resolve("XInternAtom")); + api.XSendEvent = + reinterpret_cast(x11lib.resolve("XSendEvent")); + api.XFlush = reinterpret_cast(x11lib.resolve("XFlush")); + api.XUngrabPointer = reinterpret_cast( + x11lib.resolve("XUngrabPointer")); + api.XDefaultRootWindow = reinterpret_cast( + x11lib.resolve("XDefaultRootWindow")); + } + } + guard = false; + return api; + } + + } +} +#endif // QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) diff --git a/src/core/qwindowkit_x11.h b/src/core/qwindowkit_x11.h new file mode 100644 index 00000000..29c11dcf --- /dev/null +++ b/src/core/qwindowkit_x11.h @@ -0,0 +1,61 @@ +// Copyright (C) 2021-2023 wangwenx190 (Yuhang Zhao) +// Copyright (C) 2023-present Stdware Collections (https://www.github.com/stdware) +// Copyright (C) 2025-2027 Wing-summer (wingsummer) +// SPDX-License-Identifier: Apache-2.0 + +#ifndef QWINDOWKIT_X11_H +#define QWINDOWKIT_X11_H + +// +// W A R N I N G !!! +// ----------------- +// +// This file is not part of the QWindowKit API. It is used purely as an +// implementation detail. This header file may change from version to +// version without notice, or may even be removed. +// + +#include + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +#include + +// some declarations about x11 +using Atom = unsigned long; +using Bool = int; +using XID = unsigned long; +using Window = XID; + +union _XEvent; +using XEvent = union _XEvent; + +namespace QWK { + namespace Private { + struct X11API { + X11API() = default; + Q_DISABLE_COPY(X11API) + + using XInternAtomFn = Atom (*)(Display *, const char *, Bool); + using XSendEventFn = int (*)(Display *, Window, Bool, long, XEvent *); + using XFlushFn = int (*)(Display *); + using XUngrabPointerFn = int (*)(Display *, unsigned long); + using XDefaultRootWindowFn = Window (*)(Display *); + + XInternAtomFn XInternAtom = nullptr; + XSendEventFn XSendEvent = nullptr; + XFlushFn XFlush = nullptr; + XUngrabPointerFn XUngrabPointer = nullptr; + XDefaultRootWindowFn XDefaultRootWindow = nullptr; + + inline bool isValid() const { + return XInternAtom && XSendEvent && XFlush && XUngrabPointer && XDefaultRootWindow; + } + }; + + bool isX11Platform(); + + const X11API &x11API(); + } +} +#endif // QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +#endif // QWINDOWKIT_X11_H diff --git a/src/core/qwkglobal_p.h b/src/core/qwkglobal_p.h index 90b88cac..60bcfc12 100644 --- a/src/core/qwkglobal_p.h +++ b/src/core/qwkglobal_p.h @@ -30,18 +30,12 @@ QWK_CORE_EXPORT Q_DECLARE_LOGGING_CATEGORY(qWindowKitLog) # define QWK_FATAL qCFatal(qWindowKitLog) #endif -#define MAKE_RGB_COLOR(r, g, b) ((quint32) (((r) &0xFF) << 16) | (((g) &0xFF) << 8) | ((b) &0xFF)) - #define MAKE_RGBA_COLOR(r, g, b, a) \ ((quint32) (((a) &0xFF) << 24) | (((r) &0xFF) << 16) | (((g) &0xFF) << 8) | ((b) &0xFF)) #if defined(Q_CC_MSVC) -# define QWK_NOINLINE __declspec(noinline) -# define QWK_INLINE __forceinline # define QWK_USED #else -# define QWK_NOINLINE __attribute__((noinline)) -# define QWK_INLINE __attribute__((always_inline)) # define QWK_USED __attribute__((used)) #endif diff --git a/src/core/shared/qwkwindowsextra_p.h b/src/core/shared/qwkwindowsextra_p.h index 68dc9367..9b271706 100644 --- a/src/core/shared/qwkwindowsextra_p.h +++ b/src/core/shared/qwkwindowsextra_p.h @@ -65,6 +65,22 @@ extern "C" { namespace QWK { + // Takes only a native handle so the Quick render thread never needs a window context. + inline void drawWindows10BorderNative(HWND hwnd) { + if (!hwnd) + return; + HDC hdc = ::GetDC(hwnd); + if (!hdc) + return; + RECT clientRect{}; + if (::GetClientRect(hwnd, &clientRect)) { + RECT borderRect{0, 0, RECT_WIDTH(clientRect), 1}; + ::FillRect(hdc, &borderRect, + reinterpret_cast(::GetStockObject(BLACK_BRUSH))); + } + ::ReleaseDC(hwnd, hdc); + } + enum _DWMWINDOWATTRIBUTE { // [set] BOOL, Allows the use of host backdrop brushes for the window. _DWMWA_USE_HOSTBACKDROPBRUSH = 17, @@ -187,8 +203,6 @@ namespace QWK { using SetWindowCompositionAttributePtr = BOOL(WINAPI *)(HWND, PWINDOWCOMPOSITIONATTRIBDATA); // Win10 1809 (10.0.17763) - using RefreshImmersiveColorPolicyStatePtr = VOID(WINAPI *)(VOID); // Ordinal 104 - using AllowDarkModeForWindowPtr = BOOL(WINAPI *)(HWND, BOOL); // Ordinal 133 using AllowDarkModeForAppPtr = BOOL(WINAPI *)(BOOL); // Ordinal 135 using FlushMenuThemesPtr = VOID(WINAPI *)(VOID); // Ordinal 136 // Win10 1903 (10.0.18362) @@ -219,8 +233,6 @@ namespace QWK { #undef DYNAMIC_API_DECLARE SetWindowCompositionAttributePtr pSetWindowCompositionAttribute = nullptr; - RefreshImmersiveColorPolicyStatePtr pRefreshImmersiveColorPolicyState = nullptr; - AllowDarkModeForWindowPtr pAllowDarkModeForWindow = nullptr; AllowDarkModeForAppPtr pAllowDarkModeForApp = nullptr; FlushMenuThemesPtr pFlushMenuThemes = nullptr; SetPreferredAppModePtr pSetPreferredAppMode = nullptr; @@ -254,8 +266,6 @@ namespace QWK { p##NAME = reinterpret_cast(DLL.resolve(MAKEINTRESOURCEA(ORDINAL))) QSystemLibrary uxtheme(QStringLiteral("uxtheme")); - UNDOC_API_RESOLVE(uxtheme, RefreshImmersiveColorPolicyState, 104); - UNDOC_API_RESOLVE(uxtheme, AllowDarkModeForWindow, 133); UNDOC_API_RESOLVE(uxtheme, AllowDarkModeForApp, 135); UNDOC_API_RESOLVE(uxtheme, FlushMenuThemes, 136); UNDOC_API_RESOLVE(uxtheme, SetPreferredAppMode, 135); @@ -303,14 +313,6 @@ namespace QWK { return POINT{LONG(point.x()), LONG(point.y())}; } - inline constexpr QSize size2qsize(const SIZE &size) { - return QSize{int(size.cx), int(size.cy)}; - } - - inline constexpr SIZE qsize2size(const QSize &size) { - return SIZE{LONG(size.width()), LONG(size.height())}; - } - inline constexpr QRect rect2qrect(const RECT &rect) { return QRect{ QPoint{int(rect.left), int(rect.top) }, @@ -318,33 +320,10 @@ namespace QWK { }; } - inline constexpr RECT qrect2rect(const QRect &qrect) { - return RECT{LONG(qrect.left()), LONG(qrect.top()), LONG(qrect.right()), - LONG(qrect.bottom())}; - } - - inline constexpr QMargins margins2qmargins(const MARGINS &margins) { - return {margins.cxLeftWidth, margins.cyTopHeight, margins.cxRightWidth, - margins.cyBottomHeight}; - } - inline constexpr MARGINS qmargins2margins(const QMargins &qmargins) { return {qmargins.left(), qmargins.right(), qmargins.top(), qmargins.bottom()}; } - inline /*constexpr*/ QString hwnd2str(const WId windowId) { - if (!windowId) { - return QStringLiteral("0x00000000"); - } - return QStringLiteral("0x") + - QString::number(windowId, 16).toUpper().rightJustified(8, u'0'); - } - - inline /*constexpr*/ QString hwnd2str(HWND hwnd) { - // NULL handle is allowed here. - return hwnd2str(reinterpret_cast(hwnd)); - } - inline bool isDwmCompositionEnabled() { if (isWin8OrGreater()) { return true; @@ -358,15 +337,8 @@ namespace QWK { } inline bool isWindowFrameBorderColorized() { - WindowsRegistryKey registry(HKEY_CURRENT_USER, LR"(Software\Microsoft\Windows\DWM)"); - if (!registry.isValid()) { - return false; - } - auto value = registry.dwordValue(L"ColorPrevalence"); - if (!value.second) { - return false; - } - return value.first; + return Private::readRegistryDword(HKEY_CURRENT_USER, LR"(Software\Microsoft\Windows\DWM)", + L"ColorPrevalence").value_or(0) != 0; } inline bool isHighContrastModeEnabled() { @@ -383,16 +355,10 @@ namespace QWK { #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) return QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark; #else - WindowsRegistryKey registry( - HKEY_CURRENT_USER, LR"(Software\Microsoft\Windows\CurrentVersion\Themes\Personalize)"); - if (!registry.isValid()) { - return false; - } - auto value = registry.dwordValue(L"AppsUseLightTheme"); - if (!value.second) { - return false; - } - return !value.first; + const auto value = Private::readRegistryDword( + HKEY_CURRENT_USER, LR"(Software\Microsoft\Windows\CurrentVersion\Themes\Personalize)", + L"AppsUseLightTheme"); + return value && *value == 0; #endif } @@ -413,17 +379,14 @@ namespace QWK { #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) return QGuiApplication::palette().color(QPalette::Accent); #else - WindowsRegistryKey registry(HKEY_CURRENT_USER, LR"(Software\Microsoft\Windows\DWM)"); - if (!registry.isValid()) { - return {}; - } - auto value = registry.dwordValue(L"AccentColor"); - if (!value.second) { + const auto value = Private::readRegistryDword( + HKEY_CURRENT_USER, LR"(Software\Microsoft\Windows\DWM)", L"AccentColor"); + if (!value) { return {}; } // The retrieved value is in the #AABBGGRR format, we need to // convert it to the #AARRGGBB format which Qt expects. - QColor color = QColor::fromRgba(value.first); + QColor color = QColor::fromRgba(*value); if (!color.isValid()) { return {}; } diff --git a/src/core/shared/systemwindow_p.h b/src/core/shared/systemwindow_p.h index cc006ea5..c2c4fb50 100644 --- a/src/core/shared/systemwindow_p.h +++ b/src/core/shared/systemwindow_p.h @@ -16,21 +16,72 @@ #include #include +#include +#include #include namespace QWK { + // Keep the point used to grab the title area reachable, including custom title + // bars away from the top edge. Use each screen separately: a virtual desktop's + // bounding rectangle may contain gaps. The 16-DIP inset leaves room to grab + // again; tiny screens use their center. Do not try to fit oversized windows. + inline QPoint reachableWindowPosition(const QPoint &position, const QPoint &grabOffset, + const QList &availableAreas) { + const QPoint grabPosition = position + grabOffset; + QPoint bestPosition = position; + double bestDistance = -1; + for (const auto &area : availableAreas) { + if (area.isEmpty()) + continue; + const int insetX = qMin(16, (area.width() - 1) / 2); + const int insetY = qMin(16, (area.height() - 1) / 2); + const QPoint reachableGrab(qBound(area.left() + insetX, grabPosition.x(), + area.right() - insetX), + qBound(area.top() + insetY, grabPosition.y(), + area.bottom() - insetY)); + const double dx = double(reachableGrab.x()) - grabPosition.x(); + const double dy = double(reachableGrab.y()) - grabPosition.y(); + const double distance = dx * dx + dy * dy; + if (bestDistance < 0 || distance < bestDistance) { + bestDistance = distance; + bestPosition = position + (reachableGrab - grabPosition); + } + } + // No valid screen data: preserve the position rather than invent an origin. + return bestPosition; + } + class WindowMoveManipulator : public QObject { public: - explicit WindowMoveManipulator(QWindow *targetWindow) + explicit WindowMoveManipulator(QWindow *targetWindow, + const QPoint &mousePosition = QCursor::pos()) : QObject(targetWindow), target(targetWindow), operationComplete(false), - initialMousePosition(QCursor::pos()), + initialMousePosition(mousePosition), initialWindowPosition(targetWindow->position()) { + const QPoint offset = initialMousePosition - initialWindowPosition; + grabOffset = QPoint(qBound(0, offset.x(), qMax(0, target->width() - 1)), + qBound(0, offset.y(), qMax(0, target->height() - 1))); target->installEventFilter(this); } protected: + virtual QList availableScreenGeometries() const { + QList areas; + const auto screen = target->screen(); + // Prefer the current screen on an exact distance tie. Query at release + // so screen removal and work-area changes during the drag are reflected. + if (screen) + areas.append(screen->availableGeometry()); + const auto screens = screen ? screen->virtualSiblings() : QGuiApplication::screens(); + for (const auto sibling : screens) { + if (sibling != screen) + areas.append(sibling->availableGeometry()); + } + return areas; + } + bool eventFilter(QObject *obj, QEvent *event) override { if (operationComplete) { return false; @@ -44,11 +95,13 @@ namespace QWK { } case QEvent::MouseButtonRelease: { - if (target->y() < 0) { - target->setPosition(target->x(), 0); - } + const QPoint position = reachableWindowPosition( + target->position(), grabOffset, availableScreenGeometries()); operationComplete = true; deleteLater(); + // Finish before moving: setPosition can deliver synchronous events. + if (position != target->position()) + target->setPosition(position); break; } @@ -63,6 +116,7 @@ namespace QWK { bool operationComplete; QPoint initialMousePosition; QPoint initialWindowPosition; + QPoint grabOffset; }; class WindowResizeManipulator : public QObject { diff --git a/src/core/shared/windows10borderhandler_p.h b/src/core/shared/windows10borderhandler_p.h index efeb12bb..ed7cdcd4 100644 --- a/src/core/shared/windows10borderhandler_p.h +++ b/src/core/shared/windows10borderhandler_p.h @@ -15,7 +15,7 @@ // #include -#include +#include #include #include @@ -25,21 +25,30 @@ namespace QWK { class Windows10BorderHandler : public NativeEventFilter, public SharedEventFilter { public: - inline Windows10BorderHandler(AbstractWindowContext *ctx) : ctx(ctx) { + inline Windows10BorderHandler(AbstractWindowContext *ctx, QObject *owner) + : ctx(ctx), owner(owner) { ctx->installNativeEventFilter(this); ctx->installSharedEventFilter(this); } inline void setupNecessaryAttributes() { - if (!isWin11OrGreater()) { + const auto guard = owner; + const QPointer context(ctx); + const auto windowId = ctx->windowId(); + if (!ctx->windowAttribute(QStringLiteral("extra-margins")).isValid()) { // https://github.com/microsoft/terminal/blob/71a6f26e6ece656084e87de1a528c4a8072eeabd/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp#L940 // Must extend top frame to client area static QVariant defaultMargins = QVariant::fromValue(QMargins(0, 1, 0, 0)); ctx->setWindowAttribute(QStringLiteral("extra-margins"), defaultMargins); } + if (!guard || !context || context->windowId() != windowId) + return; // Enable dark mode by default, otherwise the system borders are white - ctx->setWindowAttribute(QStringLiteral("dark-mode"), true); + if (!ctx->windowAttribute(QStringLiteral("dark-mode")).isValid()) + ctx->setWindowAttribute(QStringLiteral("dark-mode"), true); + if (guard && context && context->windowId() == windowId) + updateExtraMargins(isWindowActive()); } inline bool isNormalWindow() const { @@ -47,44 +56,10 @@ namespace QWK { (Qt::WindowMinimized | Qt::WindowMaximized | Qt::WindowFullScreen)); } - inline void drawBorderEmulated(QPainter *painter, const QRect &rect) { - QRegion region(rect); - void *args[] = { - painter, - const_cast(&rect), - ®ion, - }; - ctx->virtual_hook(AbstractWindowContext::DrawWindows10BorderHook_Emulated, args); - } - - inline void drawBorderNative() { - ctx->virtual_hook(AbstractWindowContext::DrawWindows10BorderHook_Native, nullptr); - } - - inline int borderThickness() const { - return ctx->windowAttribute(QStringLiteral("border-thickness")).toInt(); - } - inline void updateExtraMargins(bool windowActive) { - if (isWin11OrGreater()) { - return; - } - - // ### FIXME: transparent seam - if (windowActive) { - // Restore margins when the window is active - static QVariant defaultMargins = QVariant::fromValue(QMargins(0, 1, 0, 0)); - ctx->setWindowAttribute(QStringLiteral("extra-margins"), defaultMargins); - return; - } - - // https://github.com/microsoft/terminal/blob/71a6f26e6ece656084e87de1a528c4a8072eeabd/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp#L904 - // When the window is inactive, there is a transparency bug in the top - // border, and we need to extend the non-client area to the whole title - // bar. - QRect frame = ctx->windowAttribute(QStringLiteral("window-rect")).toRect(); - QMargins margins{0, -frame.top(), 0, 0}; - ctx->setWindowAttribute(QStringLiteral("extra-margins"), QVariant::fromValue(margins)); + // This handler is installed only when win10-border-needed is true. + // Activation is platform state, not a new application attribute value. + ctx->setWindows10BorderActive(windowActive); } virtual void updateGeometry() = 0; @@ -101,8 +76,11 @@ namespace QWK { const auto msg = static_cast(message); switch (msg->message) { case WM_DPICHANGED: { + const auto guard = owner; + const QPointer context(ctx); updateGeometry(); - updateExtraMargins(isWindowActive()); + if (guard && context) + updateExtraMargins(isWindowActive()); break; } @@ -136,8 +114,11 @@ namespace QWK { if (event->type() == QEvent::WinIdChange) { if (ctx->windowId()) { + const auto guard = owner; + const QPointer context(ctx); setupNecessaryAttributes(); - updateGeometry(); + if (guard && context) + updateGeometry(); } } return false; @@ -145,6 +126,7 @@ namespace QWK { protected: AbstractWindowContext *ctx; + QPointer owner; }; } diff --git a/src/core/style/portalsettings.cpp b/src/core/style/portalsettings.cpp new file mode 100644 index 00000000..84b07332 --- /dev/null +++ b/src/core/style/portalsettings.cpp @@ -0,0 +1,74 @@ +// Copyright (C) 2023-present Stdware Collections (https://www.github.com/stdware) +// SPDX-License-Identifier: Apache-2.0 + +#include "portalsettings_p.h" +#include + +namespace QWK { + PortalStyleObserver::PortalStyleObserver(StyleAgentPrivate *agent, PortalSettingsSource *source) + : QObject(agent->q_ptr), m_agent(agent), m_source(source), m_refresh(this) { + source->setParent(this); + m_refresh.setSingleShot(true); + connect(&m_refresh, &QTimer::timeout, this, &PortalStyleObserver::refresh); + connect(source, &PortalSettingsSource::changed, this, [this] { + m_dirty = true; + m_refresh.start(0); + }); + connect(source, &PortalSettingsSource::unavailable, this, &PortalStyleObserver::invalidate); + connect(source, &PortalSettingsSource::snapshot, this, &PortalStyleObserver::receive); + // Subscribe before requesting the first snapshot; never block a getter or constructor. + m_refresh.start(0); + } + + void PortalStyleObserver::refresh() { + if (m_pending) + return; // receive() will discard an obsolete reply and read again. + m_pending = true; + m_dirty = false; + m_source->read(); + } + + void PortalStyleObserver::receive(const QVariantMap &settings, bool valid) { + m_pending = false; + if (m_dirty) { + m_refresh.start(0); + return; + } + // Retry only when unavailable, not when a valid portal simply omits a key. + if (!valid) + m_refresh.start(5000); + publish(valid ? settings : QVariantMap()); // May delete this observer. + } + + void PortalStyleObserver::invalidate() { + m_pending = false; + m_dirty = false; + m_refresh.start(0); + publish({}); // Disconnection must not expose an indefinitely stale value. + } + + void PortalStyleObserver::publish(const QVariantMap &settings) { + auto theme = StyleAgent::Unknown; + const auto scheme = settings.value(QStringLiteral("color-scheme")); + const auto contrast = settings.value(QStringLiteral("contrast")); + if (contrast.userType() == QMetaType::UInt && contrast.toUInt() == 1) { + theme = StyleAgent::HighContrast; + } else if (scheme.userType() == QMetaType::UInt) { + if (scheme.toUInt() == 1) theme = StyleAgent::Dark; + if (scheme.toUInt() == 2) theme = StyleAgent::Light; + } + QColor color; + const auto rgb = settings.value(QStringLiteral("accent-color")).toList(); + if (rgb.size() == 3) { + bool valid = true; + for (const auto &channel : rgb) { + valid &= channel.userType() == QMetaType::Double && + std::isfinite(channel.toDouble()) && channel.toDouble() >= 0 && + channel.toDouble() <= 1; + } + if (valid) + color = QColor::fromRgbF(rgb[0].toDouble(), rgb[1].toDouble(), rgb[2].toDouble()); + } + m_agent->notifyAppearanceChanged(theme, color); + } +} diff --git a/src/core/style/portalsettings_dbus.cpp b/src/core/style/portalsettings_dbus.cpp new file mode 100644 index 00000000..54f5e404 --- /dev/null +++ b/src/core/style/portalsettings_dbus.cpp @@ -0,0 +1,117 @@ +// Copyright (C) 2023-present Stdware Collections (https://www.github.com/stdware) +// SPDX-License-Identifier: Apache-2.0 + +#include "portalsettings_p.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace QWK { + static const QString service = QStringLiteral("org.freedesktop.portal.Desktop"); + static const QString path = QStringLiteral("/org/freedesktop/portal/desktop"); + static const QString interface = QStringLiteral("org.freedesktop.portal.Settings"); + static const QString appearance = QStringLiteral("org.freedesktop.appearance"); + using SettingsMap = QMap; + + class DBusPortalSettingsSource : public PortalSettingsSource { + Q_OBJECT + public: + DBusPortalSettingsSource() + : m_name(QStringLiteral("qwk-style-") + QUuid::createUuid().toString()), + m_serviceWatcher(this) { + qDBusRegisterMetaType(); + m_serviceWatcher.setWatchMode(QDBusServiceWatcher::WatchForOwnerChange); + m_serviceWatcher.addWatchedService(service); + connect(&m_serviceWatcher, &QDBusServiceWatcher::serviceOwnerChanged, + this, [this](const QString &, const QString &, const QString &) { + ++m_generation; // Invalidate outstanding replies from the previous owner. + Q_EMIT unavailable(); + }); + } + ~DBusPortalSettingsSource() override { QDBusConnection::disconnectFromBus(m_name); } + + void read() override { + auto bus = QDBusConnection(m_name); + if (!bus.isConnected()) { + QDBusConnection::disconnectFromBus(m_name); + bus = QDBusConnection::connectToBus(QDBusConnection::SessionBus, m_name); + ++m_generation; + if (bus.isConnected()) { + m_serviceWatcher.setConnection(bus); + const bool subscribed = bus.connect( + service, path, interface, QStringLiteral("SettingChanged"), + this, SLOT(settingChanged(QString,QString,QDBusVariant))); + const bool watchingConnection = bus.connect( + QString(), QStringLiteral("/org/freedesktop/DBus/Local"), + QStringLiteral("org.freedesktop.DBus.Local"), + QStringLiteral("Disconnected"), this, SLOT(disconnected())); + if (!subscribed || !watchingConnection) { + QDBusConnection::disconnectFromBus(m_name); + Q_EMIT snapshot({}, false); + return; + } + } + } + if (!bus.isConnected()) { + Q_EMIT snapshot({}, false); + return; + } + auto message = QDBusMessage::createMethodCall(service, path, interface, + QStringLiteral("ReadAll")); + message << QStringList{appearance}; + const auto generation = m_generation; + auto watcher = new QDBusPendingCallWatcher(bus.asyncCall(message, 1000), this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, + [this, generation](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply reply = *watcher; + watcher->deleteLater(); + if (generation != m_generation) + return; + QVariantMap settings; + if (!reply.isError()) { + settings = reply.value().value(appearance); + const auto value = settings.value(QStringLiteral("accent-color")); + // The portal's accent is a D-Bus (ddd) structure, not a palette color. + if (value.userType() == qMetaTypeId()) { + const auto argument = qvariant_cast(value); + if (argument.currentSignature() == QStringLiteral("(ddd)")) { + double red, green, blue; + argument.beginStructure(); + argument >> red >> green >> blue; + argument.endStructure(); + settings.insert(QStringLiteral("accent-color"), + QVariantList{red, green, blue}); + } + } + } + Q_EMIT snapshot(settings, !reply.isError()); + }); + } + private Q_SLOTS: + void settingChanged(const QString &group, const QString &key, const QDBusVariant &) { + if (group == appearance && (key == QStringLiteral("color-scheme") || + key == QStringLiteral("accent-color") || + key == QStringLiteral("contrast"))) + Q_EMIT changed(); // Read the whole snapshot; do not expose mixed values. + } + void disconnected() { + ++m_generation; + Q_EMIT unavailable(); + } + private: + QString m_name; + QDBusServiceWatcher m_serviceWatcher; + quint64 m_generation = 0; + }; + + PortalSettingsSource *createDBusPortalSettingsSource() { + return new DBusPortalSettingsSource; + } +} +#include "portalsettings_dbus.moc" diff --git a/src/core/style/portalsettings_p.h b/src/core/style/portalsettings_p.h new file mode 100644 index 00000000..3b064be0 --- /dev/null +++ b/src/core/style/portalsettings_p.h @@ -0,0 +1,43 @@ +// Copyright (C) 2023-present Stdware Collections (https://www.github.com/stdware) +// SPDX-License-Identifier: Apache-2.0 +#ifndef PORTALSETTINGS_P_H +#define PORTALSETTINGS_P_H + +// Private implementation detail, not part of the QWindowKit API. +#include +#include +#include +#include "styleagent_p.h" + +namespace QWK { + // Only the D-Bus boundary is substituted in deterministic tests. + class PortalSettingsSource : public QObject { + Q_OBJECT + public: + using QObject::QObject; + virtual void read() = 0; + Q_SIGNALS: + void changed(); + void unavailable(); + void snapshot(const QVariantMap &settings, bool valid); + }; + + PortalSettingsSource *createDBusPortalSettingsSource(); + + class PortalStyleObserver : public QObject { + Q_OBJECT + public: + PortalStyleObserver(StyleAgentPrivate *agent, PortalSettingsSource *source); + private: + void refresh(); + void receive(const QVariantMap &settings, bool valid); + void invalidate(); + void publish(const QVariantMap &settings); + StyleAgentPrivate *m_agent; + PortalSettingsSource *m_source; + QTimer m_refresh; + bool m_pending = false; + bool m_dirty = false; + }; +} +#endif diff --git a/src/core/style/styleagent.cpp b/src/core/style/styleagent.cpp index a245ed53..e0cc6c5c 100644 --- a/src/core/style/styleagent.cpp +++ b/src/core/style/styleagent.cpp @@ -5,6 +5,8 @@ #include "styleagent.h" #include "styleagent_p.h" +#include + namespace QWK { /*! @@ -14,6 +16,15 @@ namespace QWK { Qt6.6 started to support system theme detection, this class is intended as an auxiliary support for lower versions of Qt. If your Qt already supports it, it is recommended that you don't include this class in your build system. + + Create and use this object on the GUI thread. On Linux, Qt DBus reads the + XDG Desktop Portal Settings interface asynchronously and subscribes to + appearance changes. Until the initial reply, the theme is Unknown and + the accent color is invalid. Unsupported or unavailable settings have + the same values; application palette overrides are not system settings. + ReadAll calls have a 1000 ms timeout; failed reads retry after 5000 ms. + Service/connection loss invalidates the cache and starts recovery. + An active event loop and a working portal backend are required. */ StyleAgentPrivate::StyleAgentPrivate() = default; @@ -22,28 +33,28 @@ namespace QWK { removeSystemThemeHook(); } - void StyleAgentPrivate::init() { - setupSystemThemeHook(); - } - - void StyleAgentPrivate::notifyThemeChanged(StyleAgent::SystemTheme theme) { - if (theme == systemTheme) + void StyleAgentPrivate::notifyAppearanceChanged(StyleAgent::SystemTheme theme, + const QColor &color) { + const bool themeChanged = theme != systemTheme; + const bool colorChanged = color != systemAccentColor; + if (!themeChanged && !colorChanged) return; systemTheme = theme; - - Q_Q(StyleAgent); - Q_EMIT q->systemThemeChanged(); - } - - void StyleAgentPrivate::notifyAccentColorChanged(const QColor &color) { - // An invalid color is not a programming error: the platform code legitimately reports - // one when it cannot determine the accent color (a failed registry read, for example). - if (color == systemAccentColor) - return; systemAccentColor = color; - - Q_Q(StyleAgent); - Q_EMIT q->systemAccentColorChanged(); + accentNotificationPending |= colorChanged; + + // Both getters must expose the same snapshot in either notification. A slot + // can delete this agent or publish a newer snapshot. A nested theme-only + // change must still deliver the pending accent notification exactly once. + QPointer owner(q_ptr); + if (themeChanged) + Q_EMIT q_ptr->systemThemeChanged(); + if (!owner) + return; + if (accentNotificationPending) { + accentNotificationPending = false; + Q_EMIT q_ptr->systemAccentColorChanged(); + } } /*! @@ -59,7 +70,9 @@ namespace QWK { StyleAgent::~StyleAgent() = default; /*! - Returns the system theme. + Returns the last observed system theme, or Unknown when unavailable or + when the Linux portal reports no preference. A high-contrast preference + takes precedence over the Linux color-scheme preference. */ StyleAgent::SystemTheme StyleAgent::systemTheme() const { Q_D(const StyleAgent); @@ -67,7 +80,8 @@ namespace QWK { } /*! - Returns the system accent color. + Returns the last observed system accent color, or an invalid QColor when + unavailable. Linux reads the portal accent-color key, not the app palette. */ QColor StyleAgent::systemAccentColor() const { Q_D(const StyleAgent); @@ -80,19 +94,23 @@ namespace QWK { StyleAgent::StyleAgent(StyleAgentPrivate &d, QObject *parent) : QObject(parent), d_ptr(&d) { d.q_ptr = this; - d.init(); + d.setupSystemThemeHook(); } /*! \fn void StyleAgent::systemThemeChanged() - This signal is emitted when the system theme changes. + This signal is emitted when the observed system theme changes, including + transitions to Unknown. Both appearance getters are updated before either + change signal is emitted. Equal values do not emit another signal. */ /*! \fn void StyleAgent::systemAccentColorChanged() - This signal is emitted when the system accent color changes. + This signal is emitted when the observed system accent color changes, + including transitions to an invalid color. Both appearance getters are + updated before either change signal is emitted. */ } diff --git a/src/core/style/styleagent_linux.cpp b/src/core/style/styleagent_linux.cpp index 7bb0815d..9b0ef68b 100644 --- a/src/core/style/styleagent_linux.cpp +++ b/src/core/style/styleagent_linux.cpp @@ -4,20 +4,14 @@ #include "styleagent_p.h" -#include -#include +#include "portalsettings_p.h" namespace QWK { - void StyleAgentPrivate::setupSystemThemeHook() { -#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) - systemAccentColor = QGuiApplication::palette().color(QPalette::Accent); -#else - systemAccentColor = QGuiApplication::palette().color(QPalette::Highlight); -#endif + systemThemeHook.reset(new PortalStyleObserver(this, createDBusPortalSettingsSource())); } void StyleAgentPrivate::removeSystemThemeHook() { + systemThemeHook.reset(); } - -} \ No newline at end of file +} diff --git a/src/core/style/styleagent_mac.mm b/src/core/style/styleagent_mac.mm index 50c35505..f1bc2bf0 100644 --- a/src/core/style/styleagent_mac.mm +++ b/src/core/style/styleagent_mac.mm @@ -2,7 +2,7 @@ // Copyright (C) 2021-2023 wangwenx190 (Yuhang Zhao) // SPDX-License-Identifier: Apache-2.0 -#include "styleagent_p.h" +#include "styleagentregistry_p.h" #include @@ -74,18 +74,14 @@ - (void)interfaceModeChanged:(NSNotification *)notification { namespace QWK { - using StyleAgentSet = QSet; - Q_GLOBAL_STATIC(StyleAgentSet, g_styleAgentSet) + Q_GLOBAL_STATIC(StyleAgentRegistry, g_styleAgents) static QWK_SystemThemeObserver *g_systemThemeObserver = nil; void notifyAllStyleAgents() { - auto theme = getSystemTheme(); - auto color = getAccentColor(); - for (auto &&ap : std::as_const(*g_styleAgentSet())) { - ap->notifyThemeChanged(theme); - ap->notifyAccentColorChanged(color); - } + g_styleAgents->notify([] { + return StyleAgentRegistry::Appearance{getSystemTheme(), getAccentColor()}; + }); } void StyleAgentPrivate::setupSystemThemeHook() { @@ -93,22 +89,22 @@ void notifyAllStyleAgents() { systemAccentColor = getAccentColor(); // Alloc - if (g_styleAgentSet->isEmpty()) { + if (g_styleAgents->isEmpty()) { g_systemThemeObserver = [[QWK_SystemThemeObserver alloc] init]; } - g_styleAgentSet->insert(this); + g_styleAgents->insert(this); } void StyleAgentPrivate::removeSystemThemeHook() { - if (!g_styleAgentSet->remove(this)) + if (!g_styleAgents->remove(this)) return; - if (g_styleAgentSet->isEmpty()) { + if (g_styleAgents->isEmpty()) { // Delete [g_systemThemeObserver release]; g_systemThemeObserver = nil; } } -} \ No newline at end of file +} diff --git a/src/core/style/styleagent_p.h b/src/core/style/styleagent_p.h index 51af0d15..76f90bb9 100644 --- a/src/core/style/styleagent_p.h +++ b/src/core/style/styleagent_p.h @@ -24,18 +24,17 @@ namespace QWK { StyleAgentPrivate(); ~StyleAgentPrivate(); - void init(); - StyleAgent *q_ptr = nullptr; StyleAgent::SystemTheme systemTheme = StyleAgent::Unknown; QColor systemAccentColor; + bool accentNotificationPending = false; + std::unique_ptr systemThemeHook; void setupSystemThemeHook(); void removeSystemThemeHook(); - void notifyThemeChanged(StyleAgent::SystemTheme theme); - void notifyAccentColorChanged(const QColor &color); + void notifyAppearanceChanged(StyleAgent::SystemTheme theme, const QColor &color); }; } diff --git a/src/core/style/styleagent_win.cpp b/src/core/style/styleagent_win.cpp index 3a0667c3..682b0a56 100644 --- a/src/core/style/styleagent_win.cpp +++ b/src/core/style/styleagent_win.cpp @@ -2,17 +2,14 @@ // Copyright (C) 2021-2023 wangwenx190 (Yuhang Zhao) // SPDX-License-Identifier: Apache-2.0 -#include "styleagent_p.h" - -#include +#include "styleagentregistry_p.h" #include #include namespace QWK { - using StyleAgentSet = QSet; - Q_GLOBAL_STATIC(StyleAgentSet, g_styleAgentSet) + Q_GLOBAL_STATIC(StyleAgentRegistry, g_styleAgents) static StyleAgent::SystemTheme getSystemTheme() { if (isHighContrastModeEnabled()) { @@ -25,24 +22,9 @@ namespace QWK { } static void notifyAllStyleAgents() { - auto theme = getSystemTheme(); - auto color = getAccentColor(); - - // Each notification emits a signal, so it runs user code that is free to create or - // destroy StyleAgent instances, which mutates the very set we are walking. Iterate over - // a copy (QSet is implicitly shared, so this costs nothing until someone mutates it) and - // re-check membership before every call so that an agent destroyed by an earlier - // notification is never touched again. - const auto agents = *g_styleAgentSet(); - for (const auto &ap : agents) { - if (!g_styleAgentSet->contains(ap)) - continue; - ap->notifyThemeChanged(theme); - - if (!g_styleAgentSet->contains(ap)) - continue; - ap->notifyAccentColorChanged(color); - } + g_styleAgents->notify([] { + return StyleAgentRegistry::Appearance{getSystemTheme(), getAccentColor()}; + }); } class SystemSettingEventFilter : public AppNativeEventFilter { @@ -88,7 +70,7 @@ namespace QWK { uninstallPending = false; // A new StyleAgent may have been created in the meantime, only leave if // there is still nobody left to notify. - if (g_styleAgentSet->isEmpty()) { + if (g_styleAgents->isEmpty()) { uninstall(); // 'this' is deleted here, touch no members afterwards } } @@ -125,17 +107,17 @@ namespace QWK { systemTheme = getSystemTheme(); systemAccentColor = getAccentColor(); - g_styleAgentSet->insert(this); + g_styleAgents->insert(this); SystemSettingEventFilter::install(); } void StyleAgentPrivate::removeSystemThemeHook() { - if (!g_styleAgentSet->remove(this)) + if (!g_styleAgents->remove(this)) return; - if (g_styleAgentSet->isEmpty()) { + if (g_styleAgents->isEmpty()) { SystemSettingEventFilter::uninstall(); } } -} \ No newline at end of file +} diff --git a/src/core/style/styleagentregistry_p.h b/src/core/style/styleagentregistry_p.h new file mode 100644 index 00000000..70d4523d --- /dev/null +++ b/src/core/style/styleagentregistry_p.h @@ -0,0 +1,62 @@ +// Copyright (C) 2023-present Stdware Collections (https://www.github.com/stdware) +// SPDX-License-Identifier: Apache-2.0 + +#ifndef STYLEAGENTREGISTRY_P_H +#define STYLEAGENTREGISTRY_P_H + +// Private synchronous notification support for Windows and macOS. +// Platform subscriptions and the asynchronous Linux portal remain separate. +#include "styleagent_p.h" + +#include +#include +#include +#include + +namespace QWK { + + class StyleAgentRegistry { + public: + struct Appearance { + StyleAgent::SystemTheme theme; + QColor accentColor; + }; + + void insert(StyleAgentPrivate *agent) { m_agents.insert(agent); } + bool remove(StyleAgentPrivate *agent) { return m_agents.remove(agent); } + bool isEmpty() const { return m_agents.isEmpty(); } + + template + void notify(ReadAppearance readAppearance) { + // Increment before platform reads: even reentry while sampling supersedes this run. + const auto revision = ++m_notificationRevision; + const auto appearance = readAppearance(); + + struct Entry { + StyleAgentPrivate *agent; + QPointer owner; + }; + QVector agents; + agents.reserve(m_agents.size()); + for (auto agent : std::as_const(m_agents)) + agents.append({agent, QPointer(agent->q_ptr)}); + + // Signals may add/remove agents. New agents wait for a subsequent snapshot. + // QPointer rejects a destroyed owner even if its address is reused; membership + // rejects a private object unregistered before QObject clears its guards. + for (const auto &entry : std::as_const(agents)) { + if (revision != m_notificationRevision) + return; + if (entry.owner && m_agents.contains(entry.agent)) + entry.agent->notifyAppearanceChanged(appearance.theme, appearance.accentColor); + } + } + + private: + QSet m_agents; + quint64 m_notificationRevision = 0; + }; + +} + +#endif // STYLEAGENTREGISTRY_P_H diff --git a/src/core/windowagentbase.cpp b/src/core/windowagentbase.cpp index 45b30bc1..d353d271 100644 --- a/src/core/windowagentbase.cpp +++ b/src/core/windowagentbase.cpp @@ -14,9 +14,10 @@ #elif defined(Q_OS_MAC) # include "cocoawindowcontext_p.h" #elif defined(Q_OS_LINUX) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) -# include "qwindowkit_linux.h" -# include "linuxwaylandcontext_p.h" -# include "linuxx11context_p.h" +# include "qwindowkit_x11.h" +# include "qwindowkit_wayland.h" +# include "waylandcontext_p.h" +# include "x11context_p.h" #endif #include "qtwindowcontext_p.h" @@ -45,9 +46,6 @@ namespace QWK { WindowAgentBasePrivate::~WindowAgentBasePrivate() = default; - void WindowAgentBasePrivate::init() { - } - AbstractWindowContext *WindowAgentBasePrivate::createContext() const { if (windowContextFactoryMethod) { return windowContextFactoryMethod(); @@ -60,11 +58,13 @@ namespace QWK { # elif defined(Q_OS_MAC) return new CocoaWindowContext(); # elif defined(Q_OS_LINUX) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +# ifdef QWK_HAS_WAYLAND_CONTEXT if (Private::isWaylandPlatform() && Private::waylandAPI().isValid()) { - return new LinuxWaylandContext(); + return new WaylandContext(); } +# endif if (Private::isX11Platform() && Private::x11API().isValid()) { - return new LinuxX11Context(); + return new X11Context(); } # endif // Final fallback, no native features. @@ -94,7 +94,32 @@ namespace QWK { } /*! - Sets the platform-related attribute for the window. Available attributes: + Sets the platform-related attribute for the window. + + With a native window, changes are cached only after the platform accepts + them. Without one, values are cached for replay when the window is created. A successful + reentrant write or removal of the same key takes precedence over an outer + call; that outer call returns false. A rejected inner call leaves the outer + call eligible to commit. Changes to other keys are independent except + Windows material attributes sharing native effects and frame margins. + + If the context is destroyed or the native window changes during the call, + the interrupted call returns false without committing its cached value. + This does not roll back native side effects already performed. On window + recreation, cached attributes are replayed in successful-write order; + entries changed by a callback are not replayed again from the old snapshot. + Unchanged entries rejected during replay are removed from the cache. + + Available attributes: + + Windows effect updates return false when a required platform call fails. + Mica and blur restore the last successfully applied QWK frame margins if + their effect setter fails. A failed restoration emits a warning; the cache + retains its last successful value, while native state may differ. Retry an + explicit value to repair it. Successful nested material updates, or partial + state left by a failed nested rollback, take precedence over older material + updates. A rejected inner call with restored state leaves the outer eligible. + Removing an absent cached value is a no-op, not a native-state reset. On Windows, \li \c no-system-menu: Specify a boolean value to disable the system menu. @@ -104,7 +129,11 @@ namespace QWK { enabled by default on Windows 10 if the system borders config is enabled. This attribute is available on Windows 10 or later. \li \c acrylic-material: Specify a boolean value to enable or disable acrylic material, - this attribute is only available on Windows 11. + this attribute is only available on Windows 11. Builds before 22621 use + the private ACCENT_POLICY compatibility path; newer builds use DWM's + system backdrop API. Failed platform operations return false. If rollback + also fails, a warning is emitted and the last successful cached value + is retained; retry an explicit boolean to repair the platform state. \li \c mica: Specify a boolean value to enable or disable mica material, this attribute is only available on Windows 11. \li \c mica-alt: Specify a boolean value to enable or disable mica-alt material, @@ -143,7 +172,8 @@ namespace QWK { } /*! - Shows the system menu, it's only implemented on Windows. + Shows the system menu at the global position \a pos, in Qt device-independent + coordinates, on supported platforms. */ void WindowAgentBase::showSystemMenu(const QPoint &pos) { Q_D(WindowAgentBase); @@ -155,7 +185,7 @@ namespace QWK { */ void WindowAgentBase::centralize() { Q_D(WindowAgentBase); - d->context->virtual_hook(AbstractWindowContext::CentralizeHook, nullptr); + d->context->centralizeWindow(); } /*! @@ -163,7 +193,7 @@ namespace QWK { */ void WindowAgentBase::raise() { Q_D(WindowAgentBase); - d->context->virtual_hook(AbstractWindowContext::RaiseWindowHook, nullptr); + d->context->raiseWindow(); } /*! @@ -172,8 +202,6 @@ namespace QWK { WindowAgentBase::WindowAgentBase(WindowAgentBasePrivate &d, QObject *parent) : QObject(parent), d_ptr(&d) { d.q_ptr = this; - - d.init(); } } diff --git a/src/core/windowagentbase.h b/src/core/windowagentbase.h index 80101923..f2e47639 100644 --- a/src/core/windowagentbase.h +++ b/src/core/windowagentbase.h @@ -21,7 +21,9 @@ namespace QWK { public: ~WindowAgentBase() override; - enum SystemButton { + // A fixed underlying type also makes numeric input from Qt's meta-object + // and QML APIs well-defined before the context validates the role. + enum SystemButton : int { Unknown, WindowIcon, Help, @@ -47,4 +49,4 @@ namespace QWK { } -#endif // WINDOWAGENTBASE_H \ No newline at end of file +#endif // WINDOWAGENTBASE_H diff --git a/src/core/windowagentbase_p.h b/src/core/windowagentbase_p.h index e1e9ab6f..ec273547 100644 --- a/src/core/windowagentbase_p.h +++ b/src/core/windowagentbase_p.h @@ -25,11 +25,9 @@ namespace QWK { WindowAgentBasePrivate(); virtual ~WindowAgentBasePrivate(); - void init(); - WindowAgentBase *q_ptr = nullptr; - virtual AbstractWindowContext *createContext() const; + AbstractWindowContext *createContext() const; void setup(QObject *host, WindowItemDelegate *delegate); diff --git a/src/core/windowitemdelegate.cpp b/src/core/windowitemdelegate.cpp index 5017db36..43229f75 100644 --- a/src/core/windowitemdelegate.cpp +++ b/src/core/windowitemdelegate.cpp @@ -10,6 +10,10 @@ namespace QWK { WindowItemDelegate::~WindowItemDelegate() = default; + bool WindowItemDelegate::containsScenePoint(const QObject *obj, const QPoint &pos) const { + return mapGeometryToScene(obj).contains(pos); + } + void WindowItemDelegate::resetQtGrabbedControl(QObject *host) const { Q_UNUSED(host); } diff --git a/src/core/windowitemdelegate_p.h b/src/core/windowitemdelegate_p.h index a79cf604..88c4da34 100644 --- a/src/core/windowitemdelegate_p.h +++ b/src/core/windowitemdelegate_p.h @@ -30,10 +30,11 @@ namespace QWK { public: // Item property query - virtual QWindow *window(const QObject *obj) const = 0; virtual bool isEnabled(const QObject *obj) const = 0; virtual bool isVisible(const QObject *obj) const = 0; + // Bounding rectangle for geometry queries; hit testing can be more precise. virtual QRect mapGeometryToScene(const QObject *obj) const = 0; + virtual bool containsScenePoint(const QObject *obj, const QPoint &pos) const; // Host property query virtual QWindow *hostWindow(const QObject *host) const = 0; @@ -45,6 +46,10 @@ namespace QWK { // Callbacks virtual void resetQtGrabbedControl(QObject *host) const; virtual void setWindowState(QObject *host, Qt::WindowStates state) const = 0; + // Temporarily own the cursor until restoreCursorShape: save the complete initial + // cursor (and widget inheritance) once, even across consecutive edge changes. + // Application writes during ownership are temporary; restoring reinstates the + // initial snapshot. Implementations must clear it before calling back into Qt. virtual void setCursorShape(QObject *host, Qt::CursorShape shape) const = 0; virtual void restoreCursorShape(QObject *host) const = 0; virtual void setWindowFlags(QObject *host, Qt::WindowFlags flags) const = 0; diff --git a/src/quick/CMakeLists.txt b/src/quick/CMakeLists.txt index a320d740..6976d49c 100644 --- a/src/quick/CMakeLists.txt +++ b/src/quick/CMakeLists.txt @@ -11,6 +11,8 @@ set(_src quickwindowagent.h quickwindowagent_p.h quickwindowagent.cpp + quicksystembuttonarea_p.h + quicksystembuttonarea.cpp ) if(WIN32) @@ -31,5 +33,43 @@ qwk_add_library(${PROJECT_NAME} QT_AUTOGEN qwk_sync_include(${PROJECT_NAME} FORCE) +# Qt's registrar produces both the runtime registration and the tooling metadata. +# Older Qt releases keep the existing procedural registration path. +if(QT_VERSION VERSION_GREATER_EQUAL 6.3) + set(QWindowKit_QML_IMPORT_PATH "${CMAKE_BINARY_DIR}/qml" CACHE PATH + "QWindowKit QML tooling import directory" FORCE) + set(QWINDOWKIT_HAS_QML_MODULE ON PARENT_SCOPE) + qt6_add_qml_module(${PROJECT_NAME} + URI QWindowKit + VERSION 1.0 + NO_PLUGIN + OUTPUT_DIRECTORY "${QWindowKit_QML_IMPORT_PATH}/QWindowKit" + RESOURCE_PREFIX /qt/qml + DEPENDENCIES QtQuick/2.0 + SOURCES qmltypes_p.h + OUTPUT_TARGETS _qml_output_targets + ) + target_compile_definitions(${PROJECT_NAME} PRIVATE QWK_QUICK_HAS_QML_MODULE) + + if(QWINDOWKIT_INSTALL) + # Qml is also the module name in older qmsetup releases. + if(EXISTS "${QMSETUP_MODULES_DIR}/modules/QtQml.cmake") + qm_import(QtQml) + else() + qm_import(Qml) + endif() + qm_install_qml_modules(${PROJECT_NAME} PREFIX qml) + if(_qml_output_targets) + install(TARGETS ${_qml_output_targets} + EXPORT ${QWINDOWKIT_EXPORT} + RUNTIME DESTINATION ${QWINDOWKIT_INSTALL_RUNTIME_DIR} + LIBRARY DESTINATION ${QWINDOWKIT_INSTALL_LIBRARY_DIR} + ARCHIVE DESTINATION ${QWINDOWKIT_INSTALL_LIBRARY_DIR} + OBJECTS DESTINATION ${QWINDOWKIT_INSTALL_LIBRARY_DIR} + ) + endif() + endif() +endif() + set(QWINDOWKIT_ENABLED_TARGETS ${QWINDOWKIT_ENABLED_TARGETS} ${PROJECT_NAME} PARENT_SCOPE) set(QWINDOWKIT_ENABLED_SUBDIRECTORIES ${QWINDOWKIT_ENABLED_SUBDIRECTORIES} quick PARENT_SCOPE) diff --git a/src/quick/qmltypes_p.h b/src/quick/qmltypes_p.h new file mode 100644 index 00000000..154782bc --- /dev/null +++ b/src/quick/qmltypes_p.h @@ -0,0 +1,17 @@ +// Copyright (C) 2023-present Stdware Collections (https://www.github.com/stdware) +// SPDX-License-Identifier: Apache-2.0 + +#ifndef QWK_QMLTYPES_P_H +#define QWK_QMLTYPES_P_H + +#include +#include + +// Describe the inherited API without making Core depend on Qt Qml. +struct WindowAgentBaseForeign { + Q_GADGET + QML_FOREIGN(QWK::WindowAgentBase) + QML_ANONYMOUS +}; + +#endif // QWK_QMLTYPES_P_H diff --git a/src/quick/quickitemdelegate.cpp b/src/quick/quickitemdelegate.cpp index baf8244a..2962b438 100644 --- a/src/quick/quickitemdelegate.cpp +++ b/src/quick/quickitemdelegate.cpp @@ -13,10 +13,6 @@ namespace QWK { QuickItemDelegate::~QuickItemDelegate() = default; - QWindow *QuickItemDelegate::window(const QObject *obj) const { - return static_cast(obj)->window(); - } - bool QuickItemDelegate::isEnabled(const QObject *obj) const { return static_cast(obj)->isEnabled(); } @@ -27,9 +23,22 @@ namespace QWK { QRect QuickItemDelegate::mapGeometryToScene(const QObject *obj) const { auto item = static_cast(obj); - const QPointF originPoint = item->mapToScene(QPointF(0.0, 0.0)); - const QSizeF size = item->size(); - return QRectF(originPoint, size).toRect(); + return item->mapRectToScene(QRectF(QPointF(), item->size())).toAlignedRect(); + } + + bool QuickItemDelegate::containsScenePoint(const QObject *obj, const QPoint &pos) const { + auto item = static_cast(obj); + // Invert the full item-to-scene transform, including ancestors. An axis-aligned + // scene bounding box includes empty corners when the item is rotated. + bool invertible = false; + const auto sceneToItem = item->itemTransform(nullptr, nullptr).inverted(&invertible); + if (!invertible) { + // A collapsed item has no hit area; QTransform otherwise returns identity. + return false; + } + const auto local = sceneToItem.map(QPointF(pos)); + return local.x() >= 0 && local.x() < item->width() && + local.y() >= 0 && local.y() < item->height(); } QWindow *QuickItemDelegate::hostWindow(const QObject *host) const { @@ -49,11 +58,18 @@ namespace QWK { } void QuickItemDelegate::setCursorShape(QObject *host, const Qt::CursorShape shape) const { - static_cast(host)->setCursor(QCursor(shape)); + auto window = static_cast(host); + if (!m_savedCursor) + m_savedCursor = window->cursor(); + window->setCursor(QCursor(shape)); } void QuickItemDelegate::restoreCursorShape(QObject *host) const { - static_cast(host)->unsetCursor(); + if (!m_savedCursor) + return; + const auto cursor = *m_savedCursor; + m_savedCursor.reset(); + static_cast(host)->setCursor(cursor); } Qt::WindowFlags QuickItemDelegate::getWindowFlags(const QObject *host) const { @@ -80,4 +96,4 @@ namespace QWK { static_cast(host)->raise(); } -} \ No newline at end of file +} diff --git a/src/quick/quickitemdelegate_p.h b/src/quick/quickitemdelegate_p.h index 418aa250..f8e2ee2d 100644 --- a/src/quick/quickitemdelegate_p.h +++ b/src/quick/quickitemdelegate_p.h @@ -16,6 +16,8 @@ #include #include +#include +#include #include #include @@ -28,10 +30,10 @@ namespace QWK { ~QuickItemDelegate() override; public: - QWindow *window(const QObject *obj) const override; bool isEnabled(const QObject *obj) const override; bool isVisible(const QObject *obj) const override; QRect mapGeometryToScene(const QObject *obj) const override; + bool containsScenePoint(const QObject *obj, const QPoint &pos) const override; QWindow *hostWindow(const QObject *host) const override; bool isWindowActive(const QObject *host) const override; @@ -46,6 +48,9 @@ namespace QWK { void setWindowVisible(QObject *host, bool visible) const override; void setGeometry(QObject *host, const QRect &rect) override; void bringWindowToTop(QObject *host) const override; + + private: + mutable std::optional m_savedCursor; }; } diff --git a/src/quick/quicksystembuttonarea.cpp b/src/quick/quicksystembuttonarea.cpp new file mode 100644 index 00000000..18c82c7b --- /dev/null +++ b/src/quick/quicksystembuttonarea.cpp @@ -0,0 +1,96 @@ +// Copyright (C) 2023-present Stdware Collections (https://www.github.com/stdware) +// SPDX-License-Identifier: Apache-2.0 + +#include "quicksystembuttonarea_p.h" + +#include + +namespace QWK { + + QuickSystemButtonArea::QuickSystemButtonArea(QQuickItem *item, QObject *parent) + : QObject(parent), m_item(item) { + rebuildConnections(); + m_lastRect = mappedRect(); + m_lastWindow = item ? item->window() : nullptr; + m_hadItem = item != nullptr; + } + + QuickSystemButtonArea::~QuickSystemButtonArea() { + clearConnections(); + } + + QRect QuickSystemButtonArea::mappedRect() const { + return m_item ? m_item->mapRectToScene(QRectF(QPointF(), m_item->size())).toRect() + : QRect(); + } + + QRect QuickSystemButtonArea::sceneRect(const QWindow *hostWindow) const { + // Never interpret coordinates from an unrelated window as host coordinates. + // The callback stays installed while detached and becomes usable on reattachment. + if (!m_item || !hostWindow || m_item->window() != hostWindow) + return {}; + return mappedRect(); + } + + void QuickSystemButtonArea::clearConnections() { + for (const auto &connection : m_connections) + disconnect(connection); + m_connections.clear(); + } + + void QuickSystemButtonArea::rebuildConnections() { + clearConnections(); + m_rebuild = false; + for (auto item = m_item.data(); item; item = item->parentItem()) { + const auto geometryChanged = [this] { scheduleRefresh(); }; + m_connections.append(connect(item, &QQuickItem::xChanged, this, geometryChanged)); + m_connections.append(connect(item, &QQuickItem::yChanged, this, geometryChanged)); + m_connections.append(connect(item, &QQuickItem::widthChanged, this, geometryChanged)); + m_connections.append(connect(item, &QQuickItem::heightChanged, this, geometryChanged)); + m_connections.append(connect(item, &QQuickItem::scaleChanged, this, geometryChanged)); + m_connections.append(connect(item, &QQuickItem::rotationChanged, this, geometryChanged)); + m_connections.append(connect(item, &QQuickItem::transformOriginChanged, this, + geometryChanged)); + const auto hierarchyChanged = [this] { scheduleRefresh(true); }; + m_connections.append(connect(item, &QQuickItem::parentChanged, this, hierarchyChanged)); + m_connections.append(connect(item, &QQuickItem::windowChanged, this, hierarchyChanged)); + // Defer traversal until the destructor/reparent operation has finished. + m_connections.append(connect(item, &QObject::destroyed, this, hierarchyChanged)); + } + if (m_item && m_item->window()) { + // Qt 5 does not expose a general transform-list change signal. This GUI-thread + // frame boundary also catches arbitrary QQuickTransform updates, additions and + // removals, without polling an idle window or reading items on the render thread. + m_connections.append(connect(m_item->window(), &QQuickWindow::afterAnimating, + this, &QuickSystemButtonArea::refresh)); + } + } + + void QuickSystemButtonArea::scheduleRefresh(bool rebuild) { + m_rebuild |= rebuild; + if (m_pending) + return; + m_pending = true; + QTimer::singleShot(0, this, [this] { + m_pending = false; + refresh(); + }); + } + + void QuickSystemButtonArea::refresh() { + if (m_rebuild) + rebuildConnections(); + const QRect rect = mappedRect(); + auto window = m_item ? m_item->window() : nullptr; + const bool hasItem = !m_item.isNull(); + if (rect == m_lastRect && window == m_lastWindow && hasItem == m_hadItem) + return; + m_lastRect = rect; + m_lastWindow = window; + m_hadItem = hasItem; + // State is committed before notification. Receivers may reparent the item or + // destroy this registration; do not touch members after emitting the signal. + Q_EMIT changed(); + } + +} diff --git a/src/quick/quicksystembuttonarea_p.h b/src/quick/quicksystembuttonarea_p.h new file mode 100644 index 00000000..0c41cac2 --- /dev/null +++ b/src/quick/quicksystembuttonarea_p.h @@ -0,0 +1,45 @@ +// Copyright (C) 2023-present Stdware Collections (https://www.github.com/stdware) +// SPDX-License-Identifier: Apache-2.0 + +#ifndef QUICKSYSTEMBUTTONAREA_P_H +#define QUICKSYSTEMBUTTONAREA_P_H + +// Private implementation detail, not part of the QWindowKit API. +#include +#include +#include + +namespace QWK { + + // Observe the visual parent chain, which can differ from QObject ownership. + // This is platform independent so the macOS geometry contract is testable. + class QuickSystemButtonArea : public QObject { + Q_OBJECT + public: + explicit QuickSystemButtonArea(QQuickItem *item, QObject *parent = nullptr); + ~QuickSystemButtonArea() override; + + QQuickItem *item() const { return m_item; } + QRect sceneRect(const QWindow *hostWindow) const; + + Q_SIGNALS: + void changed(); + + private: + void scheduleRefresh(bool rebuild = false); + void rebuildConnections(); + void clearConnections(); + void refresh(); + QRect mappedRect() const; + + QPointer m_item; + QList m_connections; + QRect m_lastRect; + QQuickWindow *m_lastWindow = nullptr; // Identity only; never dereferenced. + bool m_hadItem = false; + bool m_pending = false; + bool m_rebuild = true; + }; + +} +#endif diff --git a/src/quick/quickwindowagent.cpp b/src/quick/quickwindowagent.cpp index 481663e8..746d767c 100644 --- a/src/quick/quickwindowagent.cpp +++ b/src/quick/quickwindowagent.cpp @@ -9,6 +9,9 @@ #include #include "quickitemdelegate_p.h" +#ifdef Q_OS_MAC +# include "quicksystembuttonarea_p.h" +#endif namespace QWK { @@ -22,9 +25,12 @@ namespace QWK { QuickWindowAgentPrivate::QuickWindowAgentPrivate() = default; - QuickWindowAgentPrivate::~QuickWindowAgentPrivate() = default; - - void QuickWindowAgentPrivate::init() { + QuickWindowAgentPrivate::~QuickWindowAgentPrivate() { +#if defined(Q_OS_WINDOWS) && QWINDOWKIT_CONFIG(ENABLE_WINDOWS_SYSTEM_BORDERS) + // The window may have deleted the item already. Otherwise retire its filters and + // render callbacks before the base class destroys the context they depend on. + delete borderItem.data(); +#endif } QuickWindowAgent::QuickWindowAgent(QObject *parent) @@ -40,12 +46,11 @@ namespace QWK { } Q_D(QuickWindowAgent); - if (d->hostWindow) { + if (d->context) { return false; } d->setup(window, new QuickItemDelegate()); - d->hostWindow = window; #if defined(Q_OS_WINDOWS) && QWINDOWKIT_CONFIG(ENABLE_WINDOWS_SYSTEM_BORDERS) d->setupWindows10BorderWorkaround(); @@ -101,7 +106,6 @@ namespace QWK { */ QuickWindowAgent::QuickWindowAgent(QuickWindowAgentPrivate &d, QObject *parent) : WindowAgentBase(d, parent) { - d.init(); } } diff --git a/src/quick/quickwindowagent.h b/src/quick/quickwindowagent.h index efb0d606..70d99755 100644 --- a/src/quick/quickwindowagent.h +++ b/src/quick/quickwindowagent.h @@ -8,6 +8,10 @@ #include #include +#if QT_VERSION >= QT_VERSION_CHECK(6, 3, 0) +# include +#endif + #include #include @@ -17,6 +21,9 @@ namespace QWK { class QWK_QUICK_EXPORT QuickWindowAgent : public WindowAgentBase { Q_OBJECT +#if QT_VERSION >= QT_VERSION_CHECK(6, 3, 0) + QML_NAMED_ELEMENT(WindowAgent) +#endif Q_DECLARE_PRIVATE(QuickWindowAgent) public: explicit QuickWindowAgent(QObject *parent = nullptr); diff --git a/src/quick/quickwindowagent_mac.cpp b/src/quick/quickwindowagent_mac.cpp index 3d5c86ea..2c0dc826 100644 --- a/src/quick/quickwindowagent_mac.cpp +++ b/src/quick/quickwindowagent_mac.cpp @@ -3,67 +3,50 @@ // SPDX-License-Identifier: Apache-2.0 #include "quickwindowagent_p.h" +#include "quicksystembuttonarea_p.h" namespace QWK { - class SystemButtonAreaItemHandler : public QObject { - public: - SystemButtonAreaItemHandler(QQuickItem *item, AbstractWindowContext *ctx, - QObject *parent = nullptr); - ~SystemButtonAreaItemHandler() override = default; - - void updateSystemButtonArea(); - - protected: - QQuickItem *item; - AbstractWindowContext *ctx; - }; - - SystemButtonAreaItemHandler::SystemButtonAreaItemHandler(QQuickItem *item, - AbstractWindowContext *ctx, - QObject *parent) - : QObject(parent), item(item), ctx(ctx) { - connect(item, &QQuickItem::xChanged, this, - &SystemButtonAreaItemHandler::updateSystemButtonArea); - connect(item, &QQuickItem::yChanged, this, - &SystemButtonAreaItemHandler::updateSystemButtonArea); - connect(item, &QQuickItem::widthChanged, this, - &SystemButtonAreaItemHandler::updateSystemButtonArea); - connect(item, &QQuickItem::heightChanged, this, - &SystemButtonAreaItemHandler::updateSystemButtonArea); - - ctx->setSystemButtonAreaCallback([item](const QSize &) { - return QRectF(item->mapToScene(QPointF(0, 0)), item->size()).toRect(); // - }); - } - - void SystemButtonAreaItemHandler::updateSystemButtonArea() { - ctx->virtual_hook(AbstractWindowContext::SystemButtonAreaChangedHook, nullptr); - } - QQuickItem *QuickWindowAgent::systemButtonArea() const { Q_D(const QuickWindowAgent); - return d->systemButtonAreaItem; + return d->systemButtonAreaItemHandler ? d->systemButtonAreaItemHandler->item() : nullptr; } void QuickWindowAgent::setSystemButtonArea(QQuickItem *item) { Q_D(QuickWindowAgent); - if (d->systemButtonAreaItem == item) + if (systemButtonArea() == item && (item || !d->systemButtonAreaItemHandler)) return; auto ctx = d->context.get(); - d->systemButtonAreaItem = item; + // QPointer is already null during destroyed(); still retire the old registration + // before installing another area or a custom callback. + d->systemButtonAreaItemHandler.reset(); if (!item) { - d->systemButtonAreaItemHandler.reset(); ctx->setSystemButtonAreaCallback({}); return; } - d->systemButtonAreaItemHandler = std::make_unique(item, ctx); + auto handler = new QuickSystemButtonArea(item); + d->systemButtonAreaItemHandler.reset(handler); + const QPointer context(ctx); + connect(handler, &QuickSystemButtonArea::changed, ctx, + [context, area = QPointer(handler)] { + if (!context || !area) + return; + if (!area->item()) { + context->setSystemButtonAreaCallback({}); + return; + } + context->updateSystemButtonArea(); + }); + ctx->setSystemButtonAreaCallback( + [context, area = QPointer(handler)](const QSize &) { + return context && area ? area->sceneRect(context->window()) : QRect(); + }); } ScreenRectCallback QuickWindowAgent::systemButtonAreaCallback() const { Q_D(const QuickWindowAgent); - return d->systemButtonAreaItem ? nullptr : d->context->systemButtonAreaCallback(); + return systemButtonArea() ? nullptr : d->context->systemButtonAreaCallback(); } void QuickWindowAgent::setSystemButtonAreaCallback(const ScreenRectCallback &callback) { diff --git a/src/quick/quickwindowagent_p.h b/src/quick/quickwindowagent_p.h index 25c30dad..46ec4702 100644 --- a/src/quick/quickwindowagent_p.h +++ b/src/quick/quickwindowagent_p.h @@ -14,30 +14,31 @@ // version without notice, or may even be removed. // +#include + #include #include #include namespace QWK { +#ifdef Q_OS_MAC + class QuickSystemButtonArea; +#endif + class QuickWindowAgentPrivate : public WindowAgentBasePrivate { Q_DECLARE_PUBLIC(QuickWindowAgent) public: QuickWindowAgentPrivate(); ~QuickWindowAgentPrivate() override; - void init(); - - // Host - QQuickWindow *hostWindow{}; - #ifdef Q_OS_MAC - QQuickItem *systemButtonAreaItem{}; - std::unique_ptr systemButtonAreaItemHandler; + std::unique_ptr systemButtonAreaItemHandler; #endif #if defined(Q_OS_WINDOWS) && QWINDOWKIT_CONFIG(ENABLE_WINDOWS_SYSTEM_BORDERS) void setupWindows10BorderWorkaround(); + QPointer borderItem; #endif }; diff --git a/src/quick/quickwindowagent_win.cpp b/src/quick/quickwindowagent_win.cpp index cb667960..5824f886 100644 --- a/src/quick/quickwindowagent_win.cpp +++ b/src/quick/quickwindowagent_win.cpp @@ -5,202 +5,220 @@ #include "quickwindowagent_p.h" #include -#include +#include +#include -#include +#include #include namespace QWK { - static inline bool isWindows1022H2OrGreater() { - QWK_OSVERSIONINFOW rovi = Private::GetRealOSVersion(); - return (rovi.dwMajorVersion > 10) || - (rovi.dwMajorVersion == 10 && - (rovi.dwMinorVersion > 0 || rovi.dwBuildNumber >= 19045)); - } - #if QWINDOWKIT_CONFIG(ENABLE_WINDOWS_SYSTEM_BORDERS) - class BorderItem : public QQuickPaintedItem, public Windows10BorderHandler { + // Retain the native path's transparent painted surface and subpixel offset until its + // DWM seam workaround can be validated on Windows 10. Emulated borders do not use it. + class NativeBorderSurface : public QQuickPaintedItem { + public: + explicit NativeBorderSurface(QQuickItem *parent) : QQuickPaintedItem(parent) { + setAntialiasing(true); + setFillColor(Qt::transparent); + setOpaquePainting(true); + setAcceptedMouseButtons(Qt::NoButton); + setAcceptTouchEvents(false); + setAcceptHoverEvents(false); + } + + void paint(QPainter *) override {} + }; + + class BorderItem : public QQuickItem, public Windows10BorderHandler { public: explicit BorderItem(QQuickItem *parent, AbstractWindowContext *context); ~BorderItem() override; - bool shouldEnableEmulatedPainter() const; void updateGeometry() override; - public: - void paint(QPainter *painter) override; - void itemChange(ItemChange change, const ItemChangeData &data) override; - protected: + QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) override; + void itemChange(ItemChange change, const ItemChangeData &data) override; bool sharedEventFilter(QObject *obj, QEvent *event) override; bool nativeEventFilter(const QByteArray &eventType, void *message, QT_NATIVE_EVENT_RESULT_TYPE *result) override; private: - bool needNativePaint = false; - - void _q_afterSynchronizing(); - void _q_windowActivityChanged(); + bool shouldEnableNativePainter() const; + void afterSynchronizing(); + + // Written on the GUI thread; read only during synchronization while it is blocked. + QColor borderColor; + WId nativeWindowId = 0; + bool nativePainting = false; + std::unique_ptr nativeSurface; }; - bool BorderItem::shouldEnableEmulatedPainter() const { + bool BorderItem::shouldEnableNativePainter() const { # if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - const QQuickWindow *win = window(); - if (!win) { - return true; - } - auto api = win->rendererInterface()->graphicsApi(); - switch (api) { - case QSGRendererInterface::OpenGL: - // FIXME: may be wrong in earlier Windows 10. - return false; - case QSGRendererInterface::Direct3D11: + if (auto win = window()) { + switch (win->rendererInterface()->graphicsApi()) { + case QSGRendererInterface::OpenGL: + case QSGRendererInterface::Direct3D11: # if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) - case QSGRendererInterface::Direct3D12: + case QSGRendererInterface::Direct3D12: # endif - return false; - default: - break; + return true; + default: + break; + } } # endif - return true; + return false; } BorderItem::BorderItem(QQuickItem *parent, AbstractWindowContext *context) - : QQuickPaintedItem(parent), Windows10BorderHandler(context) { - setAntialiasing(true); // We need anti-aliasing to give us better result. - setFillColor(Qt::transparent); // Will improve the performance a little bit. - setOpaquePainting(true); // Will also improve the performance, we don't draw - // semi-transparent borders of course. - - auto parentPri = QQuickItemPrivate::get(parent); - auto anchors = QQuickItemPrivate::get(this)->anchors(); - - // Workaround for top border - // anchors->setTop(parentPri->top()); - - anchors->setLeft(parentPri->left()); - anchors->setRight(parentPri->right()); - - setZ(std::numeric_limits::max()); // Make sure our fake border always above - // everything in the window. - - // The item is parented to the window's content item, so there normally is a window - // here, but window() is nullable and the rest of this class already treats it as such. + : QQuickItem(parent), Windows10BorderHandler(context, this) { + setFlag(ItemHasContents); + setAntialiasing(false); + setAcceptedMouseButtons(Qt::NoButton); + setAcceptTouchEvents(false); + setAcceptHoverEvents(false); + setActiveFocusOnTab(false); + setEnabled(false); + // A decoration above ordinary sibling content, not above other native windows. + setZ(std::numeric_limits::max()); + + connect(parent, &QQuickItem::widthChanged, this, &BorderItem::updateGeometry); if (auto win = window()) { -# if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + connect(win, &QQuickWindow::activeChanged, this, &BorderItem::updateGeometry); + connect(win, &QQuickWindow::screenChanged, this, &BorderItem::updateGeometry); + // Backend selection may not be final before the first scene graph is initialized. + connect(win, &QQuickWindow::sceneGraphInitialized, this, + &BorderItem::updateGeometry, Qt::QueuedConnection); connect(win, &QQuickWindow::afterSynchronizing, this, - &BorderItem::_q_afterSynchronizing, Qt::DirectConnection); -# endif - connect(win, &QQuickWindow::activeChanged, this, - &BorderItem::_q_windowActivityChanged); + &BorderItem::afterSynchronizing, Qt::DirectConnection); } - // First update if (context->windowId()) { setupNecessaryAttributes(); } - BorderItem::updateGeometry(); + updateGeometry(); } - BorderItem::~BorderItem() = default; + BorderItem::~BorderItem() { + // Stop render callbacks before destroying members and unregistering context filters. + if (auto win = window()) { + disconnect(win, nullptr, this, nullptr); + } + // Qt owns the node returned by updatePaintNode and releases it on the render thread. + } void BorderItem::updateGeometry() { - const QQuickWindow *win = window(); + auto win = window(); if (!win) { + nativeWindowId = 0; + setVisible(false); return; } -# if QT_VERSION_MAJOR < 6 - setHeight(1); -# else - // Workaround for top border - // When the height is less than 0.5, it will be regarded as invisible, we apply this - // workaround to make it slightly exposed. When the height is too big, a transparent gap - // will appear on the upper frame. - setHeight(0.5); - setY(-0.49); -# endif - setVisible(isNormalWindow()); - } - void BorderItem::paint(QPainter *painter) { - Q_UNUSED(painter) - if (shouldEnableEmulatedPainter()) { - drawBorderEmulated(painter, QRect({0, 0}, size().toSize())); + nativePainting = shouldEnableNativePainter(); + nativeWindowId = ctx->windowId(); + borderColor = {}; + if (!nativePainting) { + borderColor = ctx->windows10BorderColor(); + } + + setX(0); + setWidth(parentItem() ? parentItem()->width() : 0); + if (nativePainting) { + // Preserve the original native workaround's surface geometry and opacity setup. + setY(-0.49); + setHeight(0.5); + if (!nativeSurface) { + nativeSurface = std::make_unique(this); + } + nativeSurface->setSize(size()); + nativeSurface->update(); } else { - needNativePaint = true; + nativeSurface.reset(); + setY(0); + const qreal dpr = win->effectiveDevicePixelRatio(); + setHeight(dpr > 0 ? 1 / dpr : 1); + } + setVisible(!(win->windowStates() & + (Qt::WindowMinimized | Qt::WindowMaximized | Qt::WindowFullScreen))); + update(); + } + + QSGNode *BorderItem::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) { + if (nativePainting || !borderColor.isValid() || width() <= 0 || height() <= 0) { + delete oldNode; + return nullptr; + } + auto node = static_cast(oldNode); + if (!node) { + node = window()->createRectangleNode(); + if (!node) + return nullptr; } + node->setRect(QRectF(0, 0, width(), height())); + node->setColor(borderColor); + return node; } void BorderItem::itemChange(ItemChange change, const ItemChangeData &data) { - QQuickPaintedItem::itemChange(change, data); - switch (change) { - case ItemVisibleHasChanged: - case ItemDevicePixelRatioHasChanged: { - updateGeometry(); - break; - } - default: - break; + QQuickItem::itemChange(change, data); + if (change == ItemDevicePixelRatioHasChanged || change == ItemSceneChange) { + updateGeometry(); } } bool BorderItem::sharedEventFilter(QObject *obj, QEvent *event) { - Q_UNUSED(obj) - - switch (event->type()) { - case QEvent::WindowStateChange: { - updateGeometry(); - break; - } - default: - break; + const QPointer guard(this); + const bool filtered = Windows10BorderHandler::sharedEventFilter(obj, event); + if (guard && (event->type() == QEvent::WindowStateChange || event->type() == QEvent::WinIdChange)) { + updateGeometry(); } - return Windows10BorderHandler::sharedEventFilter(obj, event); + return filtered; } bool BorderItem::nativeEventFilter(const QByteArray &eventType, void *message, QT_NATIVE_EVENT_RESULT_TYPE *result) { + if (!message) + return false; + const QPointer guard(this); + const bool filtered = Windows10BorderHandler::nativeEventFilter(eventType, message, result); + if (!guard) + return filtered; const auto msg = static_cast(message); switch (msg->message) { case WM_THEMECHANGED: case WM_SYSCOLORCHANGE: - case WM_DWMCOLORIZATIONCOLORCHANGED: { - update(); + case WM_DWMCOLORIZATIONCOLORCHANGED: + updateGeometry(); break; - } - - case WM_SETTINGCHANGE: { + case WM_SETTINGCHANGE: if (isImmersiveColorSetChange(msg->wParam, msg->lParam)) { - update(); + updateGeometry(); } break; - } - default: break; } - return Windows10BorderHandler::nativeEventFilter(eventType, message, result); + return filtered; } - void BorderItem::_q_afterSynchronizing() { - if (needNativePaint) { - needNativePaint = false; - drawBorderNative(); + void BorderItem::afterSynchronizing() { + // The GUI thread is still blocked here. Paint independently of item dirtiness, + // retaining the native timing without querying ctx or its host from the render thread. + if (nativePainting && isVisible()) { + drawWindows10BorderNative(reinterpret_cast(nativeWindowId)); } } - void BorderItem::_q_windowActivityChanged() { - update(); - } - void QuickWindowAgentPrivate::setupWindows10BorderWorkaround() { - // Install painting hook auto ctx = context.get(); if (ctx->windowAttribute(QStringLiteral("win10-border-needed")).toBool()) { - std::ignore = new BorderItem(hostWindow->contentItem(), ctx); + auto window = static_cast(ctx->host()); + borderItem = new BorderItem(window->contentItem(), ctx); } } #endif diff --git a/src/quick/qwkquickglobal.cpp b/src/quick/qwkquickglobal.cpp index 9eff69ae..d6387644 100644 --- a/src/quick/qwkquickglobal.cpp +++ b/src/quick/qwkquickglobal.cpp @@ -8,9 +8,11 @@ #include "quickwindowagent.h" -namespace QWK { +#ifdef QWK_QUICK_HAS_QML_MODULE +void qml_register_types_QWindowKit(); +#endif - static constexpr const char kModuleUri[] = "QWindowKit"; +namespace QWK { void registerTypes(QQmlEngine *engine) { Q_UNUSED(engine); @@ -21,9 +23,15 @@ namespace QWK { } once = true; +#ifdef QWK_QUICK_HAS_QML_MODULE + // Explicitly reference the generated registration for static library consumers too. + qml_register_types_QWindowKit(); +#else + static constexpr const char kModuleUri[] = "QWindowKit"; // @uri QWindowKit qmlRegisterType(kModuleUri, 1, 0, "WindowAgent"); qmlRegisterModule(kModuleUri, 1, 0); +#endif } } diff --git a/src/widgets/widgetitemdelegate.cpp b/src/widgets/widgetitemdelegate.cpp index 1c97b41c..721b16ce 100644 --- a/src/widgets/widgetitemdelegate.cpp +++ b/src/widgets/widgetitemdelegate.cpp @@ -27,11 +27,11 @@ namespace QWK { protected: bool eventFilter(QObject *obj, QEvent *event) override { - Q_UNUSED(obj) - if (event->type() == QEvent::WinIdChange) { - context->notifyWinIdChange(); - } - return false; + if (event->type() != QEvent::WinIdChange) + return false; + const QPointer receiver(obj); + context->notifyWinIdChange(); + return !receiver; } QWidget *widget; @@ -41,10 +41,6 @@ namespace QWK { WidgetItemDelegate::~WidgetItemDelegate() = default; - QWindow *WidgetItemDelegate::window(const QObject *obj) const { - return static_cast(obj)->windowHandle(); - } - bool WidgetItemDelegate::isEnabled(const QObject *obj) const { return static_cast(obj)->isEnabled(); } @@ -91,11 +87,29 @@ namespace QWK { } void WidgetItemDelegate::setCursorShape(QObject *host, Qt::CursorShape shape) const { - static_cast(host)->setCursor(QCursor(shape)); + auto widget = static_cast(host); + if (!m_savedCursor) { + m_savedCursor = widget->cursor(); + m_hadExplicitCursor = widget->testAttribute(Qt::WA_SetCursor); + } + widget->setCursor(QCursor(shape)); } void WidgetItemDelegate::restoreCursorShape(QObject *host) const { - static_cast(host)->unsetCursor(); + if (!m_savedCursor) + return; + const auto cursor = *m_savedCursor; + const bool explicitCursor = m_hadExplicitCursor; + // Clear before QWidget sends CursorChange, which can reenter or delete the agent. + m_savedCursor.reset(); + auto widget = static_cast(host); + if (explicitCursor) + widget->setCursor(cursor); + else { + // unsetCursor() only clears this attribute on non-window widgets. + widget->setAttribute(Qt::WA_SetCursor, false); + widget->unsetCursor(); + } } Qt::WindowFlags WidgetItemDelegate::getWindowFlags(const QObject *host) const { diff --git a/src/widgets/widgetitemdelegate_p.h b/src/widgets/widgetitemdelegate_p.h index 1fce59e8..8c7b3cd6 100644 --- a/src/widgets/widgetitemdelegate_p.h +++ b/src/widgets/widgetitemdelegate_p.h @@ -16,6 +16,8 @@ #include #include +#include +#include #include #include @@ -28,7 +30,6 @@ namespace QWK { ~WidgetItemDelegate() override; public: - QWindow *window(const QObject *obj) const override; bool isEnabled(const QObject *obj) const override; bool isVisible(const QObject *obj) const override; QRect mapGeometryToScene(const QObject *obj) const override; @@ -50,6 +51,10 @@ namespace QWK { WinIdChangeEventFilter * createWinIdEventFilter(QObject *host, AbstractWindowContext *context) const override; + + private: + mutable std::optional m_savedCursor; + mutable bool m_hadExplicitCursor = false; }; } diff --git a/src/widgets/widgetwindowagent.cpp b/src/widgets/widgetwindowagent.cpp index 93059ef5..da634a3f 100644 --- a/src/widgets/widgetwindowagent.cpp +++ b/src/widgets/widgetwindowagent.cpp @@ -6,7 +6,6 @@ #include "widgetwindowagent_p.h" #include -#include #include #include "widgetitemdelegate_p.h" @@ -25,9 +24,6 @@ namespace QWK { WidgetWindowAgentPrivate::~WidgetWindowAgentPrivate() = default; - void WidgetWindowAgentPrivate::init() { - } - /*! Constructs a widget agent, it's better to set the widget to setup as \a parent. */ @@ -58,7 +54,7 @@ namespace QWK { #endif Q_D(WidgetWindowAgent); - if (d->hostWidget) { + if (d->context) { return false; } @@ -71,7 +67,6 @@ namespace QWK { // w->setAttribute(Qt::WA_NativeWindow); // ### FIXME: Check d->setup(w, new WidgetItemDelegate()); - d->hostWidget = w; #if defined(Q_OS_WINDOWS) && QWINDOWKIT_CONFIG(ENABLE_WINDOWS_SYSTEM_BORDERS) d->setupWindows10BorderWorkaround(); @@ -88,8 +83,11 @@ namespace QWK { } /*! - Sets the title bar widget, all system button and hit-test visible widget references that - have been set will be removed. + Sets the title bar widget. Replacing a previously assigned title bar clears + system button and hit-test visible registrations even if the old title bar + has already been destroyed. The registered objects are not deleted. + Assigning the same title bar again does not clear registrations or emit a + change signal. Registrations made before the first title bar are preserved. */ void WidgetWindowAgent::setTitleBar(QWidget *w) { Q_D(WidgetWindowAgent); @@ -103,7 +101,8 @@ namespace QWK { } /*! - Returns the system button of the given type. + Returns the system button of the given type, or nullptr for Unknown or an + out-of-range type. QuickWindowAgent uses the same rule. */ QWidget *WidgetWindowAgent::systemButton(SystemButton button) const { Q_D(const WidgetWindowAgent); @@ -113,6 +112,10 @@ namespace QWK { /*! Sets the system button of the given type, the system buttons always receive mouse events so you don't need to call \c setHitTestVisible for them. + Unknown and out-of-range types are ignored without emitting + systemButtonChanged. Passing nullptr unregisters a valid type. Setting an + unchanged registration does not emit a signal. QuickWindowAgent follows + the same rules, after Qt converts a QML argument to the enum's int type. */ void WidgetWindowAgent::setSystemButton(SystemButton button, QWidget *w) { Q_D(WidgetWindowAgent); @@ -145,7 +148,6 @@ namespace QWK { */ WidgetWindowAgent::WidgetWindowAgent(WidgetWindowAgentPrivate &d, QObject *parent) : WindowAgentBase(d, parent) { - d.init(); } /*! diff --git a/src/widgets/widgetwindowagent_mac.cpp b/src/widgets/widgetwindowagent_mac.cpp index 34464af3..fde17efb 100644 --- a/src/widgets/widgetwindowagent_mac.cpp +++ b/src/widgets/widgetwindowagent_mac.cpp @@ -16,10 +16,14 @@ namespace QWK { public: SystemButtonAreaWidgetEventFilter(QWidget *widget, AbstractWindowContext *ctx, QObject *parent = nullptr) - : QObject(parent), widget(widget), ctx(ctx) { + : QObject(parent), ctx(ctx) { widget->installEventFilter(this); - ctx->setSystemButtonAreaCallback([widget](const QSize &) { - return getWidgetSceneRect(widget); // + // Tie cleanup to this registration so replacing it disconnects the old area. + connect(widget, &QObject::destroyed, this, [ctx] { + ctx->setSystemButtonAreaCallback({}); + }); + ctx->setSystemButtonAreaCallback([widget = QPointer(widget)](const QSize &) { + return widget ? getWidgetSceneRect(widget.data()) : QRect(); }); } ~SystemButtonAreaWidgetEventFilter() override = default; @@ -30,7 +34,7 @@ namespace QWK { switch (event->type()) { case QEvent::Move: case QEvent::Resize: { - ctx->virtual_hook(AbstractWindowContext::SystemButtonAreaChangedHook, nullptr); + ctx->updateSystemButtonArea(); break; } @@ -41,7 +45,6 @@ namespace QWK { } protected: - QWidget *widget; AbstractWindowContext *ctx; }; @@ -61,14 +64,17 @@ namespace QWK { */ void WidgetWindowAgent::setSystemButtonArea(QWidget *widget) { Q_D(WidgetWindowAgent); - if (d->systemButtonAreaWidget == widget) + if (d->systemButtonAreaWidget == widget && + (widget || !d->systemButtonAreaWidgetEventFilter)) return; auto ctx = d->context.get(); + // QPointer is already null during destroyed(); still retire the old registration + // before installing another area or a custom callback. + d->systemButtonAreaWidgetEventFilter.reset(); d->systemButtonAreaWidget = widget; if (!widget) { - d->context->setSystemButtonAreaCallback({}); - d->systemButtonAreaWidgetEventFilter.reset(); + ctx->setSystemButtonAreaCallback({}); return; } d->systemButtonAreaWidgetEventFilter = @@ -95,4 +101,4 @@ namespace QWK { d->context->setSystemButtonAreaCallback(callback); } -} \ No newline at end of file +} diff --git a/src/widgets/widgetwindowagent_p.h b/src/widgets/widgetwindowagent_p.h index b8d50d94..5642cc87 100644 --- a/src/widgets/widgetwindowagent_p.h +++ b/src/widgets/widgetwindowagent_p.h @@ -14,6 +14,8 @@ // version without notice, or may even be removed. // +#include + #include #include #include @@ -26,13 +28,8 @@ namespace QWK { WidgetWindowAgentPrivate(); ~WidgetWindowAgentPrivate(); - void init(); - - // Host - QWidget *hostWidget{}; - #ifdef Q_OS_MAC - QWidget *systemButtonAreaWidget{}; + QPointer systemButtonAreaWidget; std::unique_ptr systemButtonAreaWidgetEventFilter; #endif diff --git a/src/widgets/widgetwindowagent_win.cpp b/src/widgets/widgetwindowagent_win.cpp index d4051520..223b0a96 100644 --- a/src/widgets/widgetwindowagent_win.cpp +++ b/src/widgets/widgetwindowagent_win.cpp @@ -6,7 +6,7 @@ #include #include -#include +#include #include @@ -44,14 +44,13 @@ namespace QWK { public: explicit WidgetBorderHandler(QWidget *widget, AbstractWindowContext *ctx, QObject *parent = nullptr) - : QObject(parent), Windows10BorderHandler(ctx), widget(widget) { + : QObject(parent), Windows10BorderHandler(ctx, this), widget(widget) { widget->installEventFilter(this); // First update if (ctx->windowId()) { setupNecessaryAttributes(); } - WidgetBorderHandler::updateGeometry(); } void updateGeometry() override { @@ -62,42 +61,30 @@ namespace QWK { // integer. So far we haven't found a perfect solution, so just don't // set any margins. In theory the window content will only be covered // by 1px or so, it should not be a serious issue in the real world. - // - // widget->setContentsMargins(isNormalWindow() ? QMargins(0, borderThickness(), 0, 0) - // : QMargins()); } bool isWindowActive() const override { return widget->isActiveWindow(); } - inline void forwardEventToWidgetAndDraw(QWidget *w, QEvent *event) { + void forwardEventAndDraw(QObject *receiver, QObject *currentFilter, QEvent *event) { // https://github.com/qt/qtbase/blob/e26a87f1ecc40bc8c6aa5b889fce67410a57a702/src/widgets/kernel/qapplication.cpp#L3286 - // Deliver the event - if (!forwardObjectEventFilters(this, w, event)) { - // Let the widget paint first - std::ignore = static_cast(w)->event(event); + const QPointer handlerGuard(this); + const QPointer receiverGuard(receiver); + const QPointer contextGuard(ctx); + // Filters may destroy the window or just its agent (and this handler). + // A surviving receiver still needs its event even if the agent is gone. + if (!forwardObjectEventFilters(currentFilter, receiver, event) && receiverGuard) { + // Let Qt paint and flush first. + std::ignore = receiver->event(event); QCoreApplicationPrivate::setEventSpontaneous(event, false); } - // Due to the timer or user action, Qt will repaint some regions spontaneously, - // even if there is no WM_PAINT message, we must wait for it to finish painting - // and then update the top border area. - drawBorderNative(); - } - - inline void forwardEventToWindowAndDraw(QWindow *window, QEvent *event) { - // https://github.com/qt/qtbase/blob/e26a87f1ecc40bc8c6aa5b889fce67410a57a702/src/widgets/kernel/qapplication.cpp#L3286 - // Deliver the event - if (!forwardObjectEventFilters(ctx, window, event)) { - // Let Qt paint first - std::ignore = static_cast(window)->event(event); - QCoreApplicationPrivate::setEventSpontaneous(event, false); + // Both native Expose and spontaneous UpdateRequest must finish painting + // before the top border is drawn. + if (handlerGuard && receiverGuard && contextGuard) { + contextGuard->drawWindows10Border(); } - - // Upon receiving the WM_PAINT message, Qt will repaint the entire view, and we - // must wait for it to finish painting before drawing this top border area. - drawBorderNative(); } protected: @@ -125,7 +112,7 @@ namespace QWK { #endif auto window = widget->windowHandle(); if (window && window->isExposed() && isNormalWindow() && exposeRegionValid) { - forwardEventToWindowAndDraw(window, event); + forwardEventAndDraw(window, ctx, event); return true; } break; @@ -143,15 +130,10 @@ namespace QWK { case QEvent::UpdateRequest: { if (!isNormalWindow()) break; - forwardEventToWidgetAndDraw(widget, event); + forwardEventAndDraw(widget, this, event); return true; } - case QEvent::WindowStateChange: { - updateGeometry(); - break; - } - case QEvent::WindowActivate: case QEvent::WindowDeactivate: { widget->update(); @@ -171,7 +153,7 @@ namespace QWK { // Install painting hook auto ctx = context.get(); if (ctx->windowAttribute(QStringLiteral("win10-border-needed")).toBool()) { - borderHandler = std::make_unique(hostWidget, ctx); + borderHandler = std::make_unique(static_cast(ctx->host()), ctx); } } #endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f6486c96..f7778824 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1 +1,42 @@ +add_test(NAME testpolicy.timeouts + COMMAND ${CMAKE_COMMAND} + "-DBUILD_DIR=${PROJECT_BINARY_DIR}" + "-DBUILD_CONFIG=$" + -P "${CMAKE_CURRENT_SOURCE_DIR}/CheckTimeouts.cmake") +set_tests_properties(testpolicy.timeouts PROPERTIES TIMEOUT 10 LABELS infrastructure) + +add_subdirectory(unit) add_subdirectory(buildsystems) + +if(WIN32 AND QWINDOWKIT_ENABLE_STYLE_AGENT) + add_subdirectory(windows/styleagent) +endif() + +if(WIN32 AND (QWINDOWKIT_BUILD_WIDGETS OR QWINDOWKIT_BUILD_QUICK)) + add_subdirectory(fallbackcursor) +endif() + +if(WIN32 AND QWINDOWKIT_BUILD_WIDGETS AND NOT QWINDOWKIT_FORCE_QT_WINDOW_CONTEXT) + add_subdirectory(windows/windowproc) +endif() + +if(WIN32 AND QWINDOWKIT_BUILD_WIDGETS AND QWINDOWKIT_ENABLE_WINDOWS_SYSTEM_BORDERS) + add_subdirectory(widgetborder) +endif() + +if(WIN32 AND QWINDOWKIT_BUILD_QUICK) + add_subdirectory(quickgeometry) +endif() + +if(WIN32 AND NOT QWINDOWKIT_FORCE_QT_WINDOW_CONTEXT + AND (QWINDOWKIT_BUILD_WIDGETS OR QWINDOWKIT_BUILD_QUICK)) + add_subdirectory(windows/hittest) +endif() + +if(WIN32 AND QWINDOWKIT_BUILD_QUICK AND QWINDOWKIT_INSTALL) + add_subdirectory(qmltypes) +endif() + +if(WIN32 AND QWINDOWKIT_BUILD_QUICK AND QWINDOWKIT_ENABLE_WINDOWS_SYSTEM_BORDERS) + add_subdirectory(quickborder) +endif() diff --git a/tests/CheckTimeouts.cmake b/tests/CheckTimeouts.cmake new file mode 100644 index 00000000..ea53bba2 --- /dev/null +++ b/tests/CheckTimeouts.cmake @@ -0,0 +1,36 @@ +# Inspect the generated CTest registrations, including every enabled subdirectory. +# CTest JSON and string(JSON) are available with the project's CMake 3.19 minimum. +if(NOT DEFINED BUILD_DIR OR "${BUILD_DIR}" STREQUAL "") + message(FATAL_ERROR "BUILD_DIR is required") +endif() +execute_process(COMMAND "${CMAKE_CTEST_COMMAND}" --test-dir "${BUILD_DIR}" + -C "${BUILD_CONFIG}" --show-only=json-v1 + RESULT_VARIABLE result OUTPUT_VARIABLE manifest ERROR_VARIABLE error TIMEOUT 5) +if(NOT "${result}" STREQUAL "0") + message(FATAL_ERROR "Could not inspect CTest timeouts (${result}): ${error}") +endif() +string(JSON count LENGTH "${manifest}" tests) +if(count EQUAL 0) + message(FATAL_ERROR "No tests registered; timeout policy cannot be verified") +endif() +math(EXPR last "${count} - 1") +foreach(index RANGE 0 ${last}) + string(JSON name GET "${manifest}" tests ${index} name) + string(JSON property_count LENGTH "${manifest}" tests ${index} properties) + set(timeout "") + if(property_count GREATER 0) + math(EXPR property_last "${property_count} - 1") + foreach(property_index RANGE 0 ${property_last}) + string(JSON property_name GET "${manifest}" tests ${index} properties ${property_index} name) + if(property_name STREQUAL "TIMEOUT") + string(JSON timeout GET "${manifest}" tests ${index} properties ${property_index} value) + endif() + endforeach() + endif() + # CTest treats TIMEOUT=0 as unlimited. Require a concrete, positive bound. + if(NOT timeout MATCHES "^[0-9]+([.][0-9]+)?$" OR timeout LESS_EQUAL 0 OR timeout GREATER 60) + message(FATAL_ERROR "${name}: TIMEOUT must be explicit and greater than 0, at most 60 seconds; got '${timeout}'") + endif() + message(STATUS "${name}: ${timeout}s") +endforeach() +message(STATUS "All ${count} registered tests have bounded timeouts") diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..d7012c10 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,64 @@ +# Test time limits + +Every QWindowKit CTest entry must have an explicit, positive `TIMEOUT` of at +most 60 seconds. `testpolicy.timeouts` inspects CTest's generated JSON manifest +for all enabled test directories and fails on missing, unlimited (`0`), or +excessive timeouts. It does not execute the tests it inspects. The check itself +has a five-second subprocess deadline and a ten-second CTest limit. + +| Tests | CTest limit | Inner process limits | +| --- | ---: | --- | +| Fast suites, including Quick geometry and `systembuttons.qml` | 10 s each | 8 s | +| `widgetborder.lifetime` | 10 s | 8 s overall; each child: 2 s startup, 5 s execution, 1 s kill wait | +| `buildsystems.install` | 15 s | Direct install process bounded by CTest | +| `buildsystems.cmake` | 60 s | Configure 25 s, build 25 s | +| `buildsystems.qtmajor` | 45 s | Each isolated configuration: 5 s | +| `buildsystems.components.*` | 35 s each | Configure 15 s, build 15 s | +| `buildsystems.qmake` | 60 s | qmake 15 s, make 25 s | +| `buildsystems.qmake.reconfigure.*` | 60 s each | Prepare 15 s; each cold module build 50 s; transitions: configure/build 15 s each, install 10 s, qmake 15 s, make 25 s | +| `buildsystems.msbuild` | 60 s | MSBuild 25 s | +| `qmltypes.installed` | 60 s total | Install 10 s, configure/build 20 s each, runtime 8 s, each lint invocation 5 s | +| `quickborder.software.*`, `quickborder.native.d3d11` | 20 s each | 15 s | +| `windows.hittest` | 45 s | Driver 40 s; existing fixture/message/cleanup deadlines also apply | +| `fallbackcursor.offscreen`, `fallbackcursor.windows` | 45 s each | 40 s; exact pass counts, no skips/expected failures | +| `windows.windowproc`, `windows.windowlifetime` | 20 s each | 15 s overall; each child: 2 s startup, 5 s execution, 1 s kill wait | +| `windows.acrylic` | 20 s | 15 s; controlled API failures and native Widgets/Quick calls | +| `windows.effects` | 20 s | 15 s; Mica, blur, dark mode and border color failures and native calls | +| `windows.titlebar` | 20 s | 15 s; real Widgets/Quick registration lifecycle and native hit testing | +| `windows.styleagent` | 20 s | 15 s; real native subscriptions and HWND messages, 9 required passes | + +Inner deadlines apply to individual operations. The outer CTest deadline caps +the entire test even when several operations are slow. Timeout is a failure; +it is never converted to a skip, success, or automatic retry. Test configuration's +qmake prefix query is also bounded to five seconds and fails if it cannot finish. + +Full CI test steps have a separate three-minute aggregate deadline. The static +unit/MSBuild consumer CI step is limited to two minutes and the no-system-borders +hit-test step to one minute. These CI deadlines exclude dependency setup and +building the main project. qmake reconfiguration has a separate four-minute CI +step for each shared/static run. Local CTest runs enforce the per-test limits above; +they do not inherit the CI step's aggregate deadline. + +Run all enabled tests from a configured, built tree, using the compiler's developer +environment for build consumers: + +```sh +ctest --test-dir build -C Release --output-on-failure --no-tests=error +``` + +For the short, offline unit-only loop, see [unit/README.md](unit/README.md) and +select `-L fast`. To inspect the timeout policy alone, select +`-R '^testpolicy.timeouts$'`. All helpers use the existing CMake/CTest tools; +no external testing framework or network access is added. + +Before the lifetime regression additions, local validation on Windows/MSVC and Qt 6.12.0: all 19 registered Release tests, +including qmake and the expanded unit suites, passed in approximately 20.11 seconds +under these limits. +Negative probes confirmed rejection of absent/zero/61-second timeouts and +acceptance of a ten-second timeout. A deliberately sleeping consumer process +failed after approximately 1.03 seconds with a one-second step budget. Hosted +CI and other platforms have not been executed with the tighter limits. + +The independently built `qmsetup` and nested `stdcorelib` submodule suites are +not part of QWindowKit's CTest invocation. Their registered tests already have +their own 60/120-second limits; this change does not modify those repositories. diff --git a/tests/buildsystems/CMakeLists.txt b/tests/buildsystems/CMakeLists.txt index a41aa6da..c7696a5f 100644 --- a/tests/buildsystems/CMakeLists.txt +++ b/tests/buildsystems/CMakeLists.txt @@ -20,7 +20,17 @@ add_test(NAME buildsystems.install COMMAND ${CMAKE_COMMAND} --install ${PROJECT_BINARY_DIR} --prefix ${_prefix} --config $ ) -set_tests_properties(buildsystems.install PROPERTIES FIXTURES_SETUP qwk_install_tree) +set_tests_properties(buildsystems.install PROPERTIES FIXTURES_SETUP qwk_install_tree TIMEOUT 15) + +# Configuration-only Qt-major matrix: isolated Qt package substitutes, no compiler or network. +add_test(NAME buildsystems.qtmajor + COMMAND ${CMAKE_COMMAND} + "-DTEMPLATE=${PROJECT_SOURCE_DIR}/src/QWindowKitConfig.cmake.in" + "-DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/qtmajor-$" + "-DGENERATOR=${CMAKE_GENERATOR}" + "-DMAKE_PROGRAM=${CMAKE_MAKE_PROGRAM}" + -P "${CMAKE_CURRENT_SOURCE_DIR}/package/CheckQtMajor.cmake") +set_tests_properties(buildsystems.qtmajor PROPERTIES TIMEOUT 45 LABELS "cmake;configuration") set(_modules core) @@ -30,6 +40,9 @@ if(QWINDOWKIT_BUILD_WIDGETS) else() set(_use_widgets false) endif() +if(QWINDOWKIT_BUILD_QUICK) + list(APPEND _modules quick) +endif() # Only the debug build of QWindowKit installs libraries with the debug postfix, and only a debug # consumer would look for them. @@ -56,8 +69,13 @@ if(_qmake_executable) COMMAND ${_qmake_executable} -query QT_INSTALL_PREFIX OUTPUT_VARIABLE _qt_prefix OUTPUT_STRIP_TRAILING_WHITESPACE - ERROR_QUIET + ERROR_VARIABLE _qt_query_error + RESULT_VARIABLE _qt_query_result + TIMEOUT 5 ) + if(NOT "${_qt_query_result}" STREQUAL "0") + message(FATAL_ERROR "qmake prefix query failed (${_qt_query_result}): ${_qt_query_error}") + endif() elseif(Qt${QT_VERSION_MAJOR}_DIR) # /lib/cmake/Qt get_filename_component(_qt_prefix "${Qt${QT_VERSION_MAJOR}_DIR}/../../.." ABSOLUTE) @@ -88,7 +106,46 @@ if(_qmake_executable AND QWINDOWKIT_TEST_MAKE_PROGRAM) -D "QWK_MODULES=${_modules}" -P "${CMAKE_CURRENT_SOURCE_DIR}/qmake/RunBuild.cmake" ) - set_tests_properties(buildsystems.qmake PROPERTIES FIXTURES_REQUIRED qwk_install_tree) + set_tests_properties(buildsystems.qmake PROPERTIES FIXTURES_REQUIRED qwk_install_tree TIMEOUT 60) + + # Separate bounded stages retain one real build tree while switching modules. + # Fixtures order them even under ctest -j, and failed prerequisites stop the chain. + if(QWINDOWKIT_BUILD_WIDGETS AND QWINDOWKIT_BUILD_QUICK) + set(_previous_stage) + foreach(_stage prepare build_core build_widgets build_quick all core widgets quick restored) + set(_test "buildsystems.qmake.reconfigure.${_stage}") + add_test(NAME ${_test} + COMMAND ${CMAKE_COMMAND} + "-DSOURCE_DIR=${PROJECT_SOURCE_DIR}" + "-DTEST_ROOT=${CMAKE_CURRENT_BINARY_DIR}" + "-DBUILD_CONFIG=$" "-DSTAGE=${_stage}" + "-DGENERATOR=${CMAKE_GENERATOR}" + "-DGENERATOR_PLATFORM=${CMAKE_GENERATOR_PLATFORM}" + "-DGENERATOR_TOOLSET=${CMAKE_GENERATOR_TOOLSET}" + "-DCMAKE_MAKE_TOOL=${CMAKE_MAKE_PROGRAM}" + "-DCXX_COMPILER=${CMAKE_CXX_COMPILER}" + "-DENABLE_STYLE_AGENT=${QWINDOWKIT_ENABLE_STYLE_AGENT}" + "-DQT_PREFIX=${_qt_prefix}" "-DQT_MAJOR=${QT_VERSION_MAJOR}" + "-DQMSETUP_DIR=${qmsetup_DIR}" + "-DBUILD_STATIC=${QWINDOWKIT_BUILD_STATIC}" + "-DDEBUG_POSTFIX=${CMAKE_DEBUG_POSTFIX}" + "-DQMAKE_EXECUTABLE=${_qmake_executable}" + "-DMAKE_PROGRAM=${QWINDOWKIT_TEST_MAKE_PROGRAM}" + -P "${CMAKE_CURRENT_SOURCE_DIR}/qmake/Reconfigure.cmake") + set_tests_properties(${_test} PROPERTIES TIMEOUT 60 + LABELS "qmake;install;reconfigure" + FIXTURES_SETUP "qmake_reconfigure_${_stage}" + RESOURCE_LOCK qmake_reconfigure) + if(_previous_stage) + set_tests_properties(${_test} PROPERTIES + FIXTURES_REQUIRED "qmake_reconfigure_${_previous_stage}") + endif() + set(_previous_stage ${_stage}) + endforeach() + unset(_previous_stage) + unset(_stage) + unset(_test) + endif() else() message(STATUS "qmake or a make tool was not found, skipping the qmake build system test") endif() @@ -129,9 +186,11 @@ if(QWINDOWKIT_TEST_MSBUILD_EXECUTABLE AND _platform AND _qt_prefix) -D "QT_PREFIX=${_qt_prefix}" -D "QT_MAJOR_VERSION=${QT_VERSION_MAJOR}" -D "USE_WIDGETS=${_use_widgets}" + -D "USE_QUICK=${QWINDOWKIT_BUILD_QUICK}" + -D "EXPECT_STATIC=${QWINDOWKIT_BUILD_STATIC}" -P "${CMAKE_CURRENT_SOURCE_DIR}/msbuild/RunBuild.cmake" ) - set_tests_properties(buildsystems.msbuild PROPERTIES FIXTURES_REQUIRED qwk_install_tree) + set_tests_properties(buildsystems.msbuild PROPERTIES FIXTURES_REQUIRED qwk_install_tree TIMEOUT 60) elseif(MSVC) message(STATUS "MSBuild or the Qt prefix was not found, skipping the MSBuild build system test") endif() @@ -142,6 +201,21 @@ endif() # The package most callers use. It needs no tool of its own, only somewhere to find Qt, since that # is what `QWindowKitConfig.cmake` looks for before it hands out a target. if(_qt_prefix) + foreach(_case present default core missing-required unknown-required optional quiet-missing repeat optional-qt-missing) + add_test(NAME buildsystems.components.${_case} + COMMAND ${CMAKE_COMMAND} + "-DSOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/package/component-consumer" + "-DINSTALL_PREFIX=${_prefix}" + "-DQT_PREFIX=${_qt_prefix}" + "-DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/components-${_case}-$" + "-DBUILD_CONFIG=$" "-DGENERATOR=${CMAKE_GENERATOR}" + "-DMAKE_PROGRAM=${CMAKE_MAKE_PROGRAM}" "-DCXX_COMPILER=${CMAKE_CXX_COMPILER}" + "-DUSE_WIDGETS=${QWINDOWKIT_BUILD_WIDGETS}" "-DUSE_QUICK=${QWINDOWKIT_BUILD_QUICK}" + "-DQT_MAJOR=${QT_VERSION_MAJOR}" "-DCASE=${_case}" + -P "${CMAKE_CURRENT_SOURCE_DIR}/package/RunComponentCase.cmake") + set_tests_properties(buildsystems.components.${_case} PROPERTIES + FIXTURES_REQUIRED qwk_install_tree TIMEOUT 35 LABELS "cmake;install;components") + endforeach() add_test(NAME buildsystems.cmake COMMAND ${CMAKE_COMMAND} -D "SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/cmake/consumer" @@ -155,7 +229,7 @@ if(_qt_prefix) -D "USE_WIDGETS=${_use_widgets}" -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/RunBuild.cmake" ) - set_tests_properties(buildsystems.cmake PROPERTIES FIXTURES_REQUIRED qwk_install_tree) + set_tests_properties(buildsystems.cmake PROPERTIES FIXTURES_REQUIRED qwk_install_tree TIMEOUT 60) else() message(STATUS "The Qt prefix was not found, skipping the CMake build system test") endif() diff --git a/tests/buildsystems/cmake/RunBuild.cmake b/tests/buildsystems/cmake/RunBuild.cmake index a53eba6b..1265016e 100644 --- a/tests/buildsystems/cmake/RunBuild.cmake +++ b/tests/buildsystems/cmake/RunBuild.cmake @@ -23,17 +23,21 @@ if(MAKE_PROGRAM) list(APPEND _toolchain_args "-DCMAKE_MAKE_PROGRAM=${MAKE_PROGRAM}") endif() -qwk_run_step("cmake configure" "${CMAKE_COMMAND}" +# CMake does not search custom library directories such as lib64 on every host. +# Point at this fixture's config explicitly rather than finding another installation. +qwk_find_installed_config(_package_dir "${INSTALL_PREFIX}") +qwk_run_step("cmake configure" 25 "${CMAKE_COMMAND}" -S "${SOURCE_DIR}" -B "${WORK_DIR}/build" ${_toolchain_args} "-DCMAKE_BUILD_TYPE=${BUILD_CONFIG}" "-DQWK_PREFIX=${INSTALL_PREFIX}" + "-DQWindowKit_DIR=${_package_dir}" "-DQT_PREFIX=${QT_PREFIX}" "-DQWK_USE_WIDGETS=${USE_WIDGETS}" ) -qwk_run_step("cmake build" "${CMAKE_COMMAND}" +qwk_run_step("cmake build" 25 "${CMAKE_COMMAND}" --build "${WORK_DIR}/build" --config "${BUILD_CONFIG}" ) diff --git a/tests/buildsystems/consumer.cpp b/tests/buildsystems/consumer.cpp index 0fd7a881..e15dd338 100644 --- a/tests/buildsystems/consumer.cpp +++ b/tests/buildsystems/consumer.cpp @@ -14,15 +14,24 @@ #include #include +#include +#include #ifdef CONSUMER_USE_WIDGETS # include #endif +#ifdef CONSUMER_USE_QUICK +# include +#endif int main(int argc, char *argv[]) { Q_UNUSED(argc) Q_UNUSED(argv) + // Private dispatcher headers depend on the shared state header being installed too. + QWK::SharedEventDispatcher sharedDispatcher; + QWK::NativeEventDispatcher nativeDispatcher; + // A reference to an exported symbol of every module linked, so that a library that was not // found is a link error rather than a program that builds and does nothing. const QMetaObject *core = &QWK::WindowAgentBase::staticMetaObject; @@ -33,5 +42,11 @@ int main(int argc, char *argv[]) { const QMetaObject *widgets = nullptr; #endif - return (core != nullptr && widgets != core) ? 0 : 1; +#ifdef CONSUMER_USE_QUICK + const QMetaObject *quick = &QWK::QuickWindowAgent::staticMetaObject; +#else + const QMetaObject *quick = nullptr; +#endif + + return (core != nullptr && widgets != core && quick != core) ? 0 : 1; } diff --git a/tests/buildsystems/msbuild/RunBuild.cmake b/tests/buildsystems/msbuild/RunBuild.cmake index bdf141f4..6e9e5fa5 100644 --- a/tests/buildsystems/msbuild/RunBuild.cmake +++ b/tests/buildsystems/msbuild/RunBuild.cmake @@ -2,12 +2,13 @@ # # cmake -D MSBUILD_EXECUTABLE=... -D PROJECT_FILE=... -D INSTALL_PREFIX=... -D WORK_DIR=... # -D BUILD_CONFIG=Debug|Release -D PLATFORM=x64 -D QT_PREFIX=... -D QT_MAJOR_VERSION=6 -# -D USE_WIDGETS=true|false -P msbuild/RunBuild.cmake +# -D USE_WIDGETS=true|false -D USE_QUICK=ON|OFF -D EXPECT_STATIC=ON|OFF +# -P msbuild/RunBuild.cmake include("${CMAKE_CURRENT_LIST_DIR}/../testing/ConsumerBuildCommon.cmake") qwk_require_variables(MSBUILD_EXECUTABLE PROJECT_FILE INSTALL_PREFIX WORK_DIR BUILD_CONFIG - PLATFORM QT_PREFIX QT_MAJOR_VERSION USE_WIDGETS + PLATFORM QT_PREFIX QT_MAJOR_VERSION USE_WIDGETS USE_QUICK EXPECT_STATIC ) file(REMOVE_RECURSE "${WORK_DIR}") @@ -20,7 +21,15 @@ file(MAKE_DIRECTORY "${WORK_DIR}") file(TO_NATIVE_PATH "${INSTALL_PREFIX}" _native_prefix) file(TO_NATIVE_PATH "${QT_PREFIX}" _native_qt_prefix) -qwk_run_step("msbuild" "${MSBUILD_EXECUTABLE}" "${PROJECT_FILE}" +foreach(_option USE_QUICK EXPECT_STATIC) + if(${_option}) + set(_${_option} true) + else() + set(_${_option} false) + endif() +endforeach() + +qwk_run_step("msbuild" 25 "${MSBUILD_EXECUTABLE}" "${PROJECT_FILE}" /nologo /verbosity:minimal "/p:Configuration=${BUILD_CONFIG}" @@ -29,6 +38,8 @@ qwk_run_step("msbuild" "${MSBUILD_EXECUTABLE}" "${PROJECT_FILE}" "/p:QTDIR=${_native_qt_prefix}" "/p:QtVersionMajor=${QT_MAJOR_VERSION}" "/p:QwkUseWidgets=${USE_WIDGETS}" + "/p:QwkUseQuick=${_USE_QUICK}" + "/p:QwkExpectStatic=${_EXPECT_STATIC}" "/p:OutDir=${WORK_DIR}/bin/" "/p:IntDir=${WORK_DIR}/obj/" ) diff --git a/tests/buildsystems/msbuild/consumer.cpp b/tests/buildsystems/msbuild/consumer.cpp new file mode 100644 index 00000000..6ac27fe5 --- /dev/null +++ b/tests/buildsystems/msbuild/consumer.cpp @@ -0,0 +1,49 @@ +// Copyright (C) 2023-present Stdware Collections (https://www.github.com/stdware) +// SPDX-License-Identifier: Apache-2.0 + +// Compile and link only: exercise the installed property sheet, including static imports. +#include + +#ifdef CONSUMER_USE_WIDGETS +# include +#endif +#ifdef CONSUMER_USE_QUICK +# include +#endif + +#ifndef CONSUMER_INHERITED_DEFINITION +# error The property sheet discarded inherited compiler definitions. +#endif + +#ifdef CONSUMER_EXPECT_STATIC +# ifndef QWK_CORE_STATIC +# error The static property sheet did not define QWK_CORE_STATIC. +# endif +# if defined(CONSUMER_USE_WIDGETS) && !defined(QWK_WIDGETS_STATIC) +# error The static property sheet did not define QWK_WIDGETS_STATIC. +# endif +# if defined(CONSUMER_USE_QUICK) && !defined(QWK_QUICK_STATIC) +# error The static property sheet did not define QWK_QUICK_STATIC. +# endif +#else +# if defined(QWK_CORE_STATIC) || defined(QWK_WIDGETS_STATIC) || defined(QWK_QUICK_STATIC) +# error The shared property sheet must not define static import macros. +# endif +#endif + +int main() { + // Reading metaobjects and calling exported methods keeps the linker references alive + // even in optimized builds; merely comparing their addresses can be optimized away. + int result = QWK::WindowAgentBase::staticMetaObject.className()[0]; +#ifdef CONSUMER_USE_WIDGETS + QWK::WidgetWindowAgent widgets; + result += QWK::WidgetWindowAgent::staticMetaObject.className()[0]; + result += widgets.titleBar() != nullptr; +#endif +#ifdef CONSUMER_USE_QUICK + QWK::QuickWindowAgent quick; + result += QWK::QuickWindowAgent::staticMetaObject.className()[0]; + result += quick.titleBar() != nullptr; +#endif + return result; +} diff --git a/tests/buildsystems/msbuild/consumer.vcxproj b/tests/buildsystems/msbuild/consumer.vcxproj index ad1c622f..e6e37642 100644 --- a/tests/buildsystems/msbuild/consumer.vcxproj +++ b/tests/buildsystems/msbuild/consumer.vcxproj @@ -1,7 +1,7 @@ @@ -66,15 +71,21 @@ MultiThreadedDLL $(QtHeaderSearchPath);$(QTDIR)\include;$(QTDIR)\include\QtCore;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtWidgets;%(AdditionalIncludeDirectories) CONSUMER_USE_WIDGETS;%(PreprocessorDefinitions) + CONSUMER_USE_QUICK;%(PreprocessorDefinitions) + CONSUMER_EXPECT_STATIC;%(PreprocessorDefinitions) + $(QTDIR)\include\QtQuick;$(QTDIR)\include\QtQml;%(AdditionalIncludeDirectories) Console $(QTDIR)\lib;%(AdditionalLibraryDirectories) Qt$(QtVersionMajor)Core$(QtLibSuffix).lib;Qt$(QtVersionMajor)Gui$(QtLibSuffix).lib;Qt$(QtVersionMajor)Widgets$(QtLibSuffix).lib;%(AdditionalDependencies) + Qt$(QtVersionMajor)Quick$(QtLibSuffix).lib;Qt$(QtVersionMajor)Qml$(QtLibSuffix).lib;%(AdditionalDependencies) + + uxtheme.lib;%(AdditionalDependencies) - + diff --git a/tests/buildsystems/package/CheckQtMajor.cmake b/tests/buildsystems/package/CheckQtMajor.cmake new file mode 100644 index 00000000..552361b0 --- /dev/null +++ b/tests/buildsystems/package/CheckQtMajor.cmake @@ -0,0 +1,73 @@ +cmake_minimum_required(VERSION 3.19) +include(CMakePackageConfigHelpers) +include("${CMAKE_CURRENT_LIST_DIR}/../testing/ConsumerBuildCommon.cmake") +qwk_require_variables(TEMPLATE WORK_DIR GENERATOR) +file(MAKE_DIRECTORY "${WORK_DIR}") +set(QWINDOWKIT_HAS_QML_MODULE OFF) +set(QWINDOWKIT_BUILD_WIDGETS ON) +set(QWINDOWKIT_BUILD_QUICK ON) +set(CMAKE_INSTALL_PREFIX "${WORK_DIR}") +foreach(FAKE_MAJOR 5 6) + file(MAKE_DIRECTORY "${WORK_DIR}/qt${FAKE_MAJOR}/lib/cmake/Qt${FAKE_MAJOR}") + configure_file("${CMAKE_CURRENT_LIST_DIR}/QtConfig.cmake.in" + "${WORK_DIR}/qt${FAKE_MAJOR}/lib/cmake/Qt${FAKE_MAJOR}/Qt${FAKE_MAJOR}Config.cmake" @ONLY) +endforeach() +set(cases 0) +foreach(QT_VERSION_MAJOR 5 6) + math(EXPR wrong_major "11 - ${QT_VERSION_MAJOR}") + foreach(QWINDOWKIT_BUILD_STATIC OFF ON) + foreach(QWINDOWKIT_STYLE_USES_DBUS OFF ON) + set(package "${WORK_DIR}/package-${QT_VERSION_MAJOR}-${QWINDOWKIT_BUILD_STATIC}-${QWINDOWKIT_STYLE_USES_DBUS}") + file(MAKE_DIRECTORY "${package}/lib/cmake/QWindowKit") + configure_package_config_file("${TEMPLATE}" + "${package}/lib/cmake/QWindowKit/QWindowKitConfig.cmake" + INSTALL_DESTINATION lib/cmake/QWindowKit INSTALL_PREFIX "${package}") + set(exports "") + foreach(component Core Widgets Quick) + string(APPEND exports "add_library(QWindowKit::${component} INTERFACE IMPORTED)\n") + endforeach() + file(WRITE "${package}/lib/cmake/QWindowKit/QWindowKitTargets.cmake" "${exports}") + foreach(mode both matching wrong-only missing-dbus) + set(qt_roots "${WORK_DIR}/qt${QT_VERSION_MAJOR}") + if(mode STREQUAL "both") + list(PREPEND qt_roots "${WORK_DIR}/qt${wrong_major}") + elseif(mode STREQUAL "wrong-only") + set(qt_roots "${WORK_DIR}/qt${wrong_major}") + endif() + set(disable_dbus OFF) + if(mode STREQUAL "missing-dbus") + set(disable_dbus ON) + endif() + # Independent caches prevent package discovery from a preceding case leaking in. + set(case_dir "${WORK_DIR}/consumer-${QT_VERSION_MAJOR}-${QWINDOWKIT_BUILD_STATIC}-${QWINDOWKIT_STYLE_USES_DBUS}-${mode}") + file(REMOVE "${case_dir}/CMakeCache.txt") + execute_process(COMMAND "${CMAKE_COMMAND}" + -S "${CMAKE_CURRENT_LIST_DIR}/major-consumer" -B "${case_dir}" + -G "${GENERATOR}" "-DCMAKE_MAKE_PROGRAM=${MAKE_PROGRAM}" + "-DQWindowKit_DIR=${package}/lib/cmake/QWindowKit" + "-DQT_ROOTS=${qt_roots}" "-DEXPECTED_MAJOR=${QT_VERSION_MAJOR}" + "-DWRONG_MAJOR=${wrong_major}" "-DSTATIC_PACKAGE=${QWINDOWKIT_BUILD_STATIC}" + "-DPORTAL_PACKAGE=${QWINDOWKIT_STYLE_USES_DBUS}" "-DDISABLE_DBUS=${disable_dbus}" + RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error TIMEOUT 5) + if(mode STREQUAL "wrong-only") + if(NOT "${result}" MATCHES "^[1-9][0-9]*$" OR + NOT "${output}${error}" MATCHES "Qt${QT_VERSION_MAJOR}Config.cmake") + message(FATAL_ERROR "Wrong-only case did not reject missing Qt ${QT_VERSION_MAJOR}: ${result}\n${output}\n${error}") + endif() + elseif(mode STREQUAL "missing-dbus" AND QWINDOWKIT_BUILD_STATIC AND QWINDOWKIT_STYLE_USES_DBUS) + if(NOT "${result}" MATCHES "^[1-9][0-9]*$" OR + NOT "${output}${error}" MATCHES "DBus") + message(FATAL_ERROR "Static portal package accepted missing DBus: ${result}\n${output}\n${error}") + endif() + elseif(NOT "${result}" STREQUAL "0") + message(FATAL_ERROR "Qt ${QT_VERSION_MAJOR}/${QWINDOWKIT_BUILD_STATIC}/${mode}: ${result}\n${output}\n${error}") + endif() + math(EXPR cases "${cases} + 1") + endforeach() + endforeach() + endforeach() +endforeach() +if(NOT cases EQUAL 32) + message(FATAL_ERROR "Expected 32 Qt-major discovery cases, got ${cases}") +endif() +message(STATUS "All 32 Qt-major/portal dependency discovery cases passed (configuration substitutes, not ABI tests)") diff --git a/tests/buildsystems/package/QtConfig.cmake.in b/tests/buildsystems/package/QtConfig.cmake.in new file mode 100644 index 00000000..80d24761 --- /dev/null +++ b/tests/buildsystems/package/QtConfig.cmake.in @@ -0,0 +1,16 @@ +# Deliberately small discovery substitute, not a Qt ABI or compile/link fixture. +set(QT_VERSION_MAJOR @FAKE_MAJOR@) +set(Qt@FAKE_MAJOR@_FOUND TRUE) +set(QT_FOUND TRUE) +set_property(GLOBAL APPEND PROPERTY QWK_TEST_QT_MAJORS @FAKE_MAJOR@) +foreach(module IN LISTS ${CMAKE_FIND_PACKAGE_NAME}_FIND_COMPONENTS) + if(module STREQUAL "DBus" AND DISABLE_DBUS) + set(Qt@FAKE_MAJOR@_FOUND FALSE) + set(Qt@FAKE_MAJOR@_NOT_FOUND_MESSAGE "DBus is unavailable in this test Qt package") + return() + endif() + if(NOT TARGET Qt@FAKE_MAJOR@::${module}) + add_library(Qt@FAKE_MAJOR@::${module} INTERFACE IMPORTED) + endif() + set(Qt@FAKE_MAJOR@${module}_FOUND TRUE) +endforeach() diff --git a/tests/buildsystems/package/README.md b/tests/buildsystems/package/README.md new file mode 100644 index 00000000..350ff5c6 --- /dev/null +++ b/tests/buildsystems/package/README.md @@ -0,0 +1,56 @@ +# Installed CMake package contracts + +`buildsystems.qtmajor` generates the production config template with Qt 5 and Qt 6 +build metadata, for both shared and static packages. Twelve fresh consumer caches +test the matching major alone, both majors with the wrong one first, and only the +wrong major. Consumers start with a conflicting `QT_VERSION_MAJOR`. Assertions check +the selected major, public targets, and Qt 6 private targets needed for static links. +Wrong-major-only configurations must fail while looking for the matching Qt package. + +The Qt packages and export targets in this matrix are discovery substitutes. It +does not compile or link Qt 5 binaries, check ABI compatibility, or require two Qt +installations. The ordinary `buildsystems.cmake` test compiles and links a separate +consumer against the real staged installation and local Qt toolchain. Test execution +is offline; each configuration has a five-second timeout and CTest caps the matrix +at 45 seconds. + +```sh +ctest --test-dir build -C Release -R '^buildsystems\.(qtmajor|cmake)$' --output-on-failure --no-tests=error +``` + +The package records its build-time major as `QWindowKit_QT_VERSION_MAJOR` and requests +that version's Qt targets directly. It does not force consumers to use the same Qt +patch version or make a fresh Qt5/Qt6 selection from their search paths. + +The installed component names are `Core`, `Widgets`, and `Quick` (case-sensitive). +`QWindowKit_AVAILABLE_COMPONENTS` lists the modules built into the installation. +Missing required or unknown components reject the package before Qt discovery, +with a diagnostic naming the missing and installed components. Missing optional +components, including installed modules whose Qt dependencies cannot be found, +have a false `QWindowKit__FOUND` without rejecting usable required modules. +With no component selection, the config resolves dependencies for every installed +module. With an explicit selection, consumers should only link the requested, +found components: the common export file still declares all installed targets. + +`buildsystems.components.*` configures independent C++ consumers against the real +staged installation. Its nine cases cover all present modules, default selection, +Core alone, missing and unknown required components, missing optional components, +a quiet failure, a successful lookup after failure, and a missing optional Qt +dependency. Successful consumers link the selected libraries; they are not executed. +Required-component failures must be normal configure errors with the package's +diagnostic, not timeouts. Each configure and build has a 15-second timeout; each +CTest case is capped at 35 seconds. The fixture locates the installed config directly +so custom library directories such as `lib64` work on Windows too. + +Run these tests in separate Core-only, Core+Widgets, and Core+Quick build trees to +exercise real missing modules, as well as with all modules enabled. Both shared and +static builds use the same cases. Build all installable targets before testing; +static Quick installations also need their generated QML resource object target. + +```sh +ctest --test-dir build -C Release -R '^buildsystems\.(qtmajor|components\.|cmake)' --output-on-failure --no-tests=error +``` + +These are package discovery and compile/link checks. They do not test window +behavior, QML runtime registration, binary compatibility across Qt majors, or +platforms/toolchains absent from the machine executing them. diff --git a/tests/buildsystems/package/RunComponentCase.cmake b/tests/buildsystems/package/RunComponentCase.cmake new file mode 100644 index 00000000..fc8bec95 --- /dev/null +++ b/tests/buildsystems/package/RunComponentCase.cmake @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.19) +include("${CMAKE_CURRENT_LIST_DIR}/../testing/ConsumerBuildCommon.cmake") +qwk_require_variables(SOURCE_DIR INSTALL_PREFIX QT_PREFIX WORK_DIR BUILD_CONFIG GENERATOR + CXX_COMPILER USE_WIDGETS USE_QUICK QT_MAJOR CASE) +file(MAKE_DIRECTORY "${WORK_DIR}") +qwk_find_installed_config(package_dir "${INSTALL_PREFIX}") +file(REMOVE "${WORK_DIR}/build/CMakeCache.txt") +set(toolchain_args -G "${GENERATOR}" "-DCMAKE_CXX_COMPILER=${CXX_COMPILER}") +if(MAKE_PROGRAM) + list(APPEND toolchain_args "-DCMAKE_MAKE_PROGRAM=${MAKE_PROGRAM}") +endif() +execute_process(COMMAND "${CMAKE_COMMAND}" + -S "${SOURCE_DIR}" -B "${WORK_DIR}/build" ${toolchain_args} + "-DCMAKE_BUILD_TYPE=${BUILD_CONFIG}" "-DPACKAGE_DIR=${package_dir}" + "-DQT_PREFIX=${QT_PREFIX}" "-DUSE_WIDGETS=${USE_WIDGETS}" "-DUSE_QUICK=${USE_QUICK}" + "-DQT_MAJOR=${QT_MAJOR}" "-DCASE=${CASE}" + RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error TIMEOUT 15) +if(CASE MATCHES "^(missing-required|unknown-required)$") + if(NOT "${result}" MATCHES "^[1-9][0-9]*$" OR + NOT "${output}${error}" MATCHES "Unavailable QWindowKit components:") + message(FATAL_ERROR "Expected package-level component failure, got ${result}\n${output}\n${error}") + endif() + message(STATUS "${CASE}: rejected during package discovery with a component diagnostic") +elseif(NOT "${result}" STREQUAL "0") + message(FATAL_ERROR "${CASE}: configuration failed (${result})\n${output}\n${error}") +elseif(NOT CASE STREQUAL "quiet-missing") + qwk_run_step("component consumer build" 15 "${CMAKE_COMMAND}" + --build "${WORK_DIR}/build" --config "${BUILD_CONFIG}") + message(STATUS "${CASE}: component states, exports and consumer link passed") +endif() diff --git a/tests/buildsystems/package/component-consumer/CMakeLists.txt b/tests/buildsystems/package/component-consumer/CMakeLists.txt new file mode 100644 index 00000000..6793d07d --- /dev/null +++ b/tests/buildsystems/package/component-consumer/CMakeLists.txt @@ -0,0 +1,101 @@ +cmake_minimum_required(VERSION 3.19) +project(qwk_component_consumer LANGUAGES CXX) + +list(APPEND CMAKE_PREFIX_PATH "${QT_PREFIX}") +set(package_path CONFIG PATHS "${PACKAGE_DIR}" NO_DEFAULT_PATH) +set(expected_components Core) +if(USE_WIDGETS) + list(APPEND expected_components Widgets) +endif() +if(USE_QUICK) + list(APPEND expected_components Quick) +endif() +set(missing MissingComponent) +if(NOT USE_WIDGETS) + set(missing Widgets) +elseif(NOT USE_QUICK) + set(missing Quick) +endif() +set(link_components Core) +if(CASE STREQUAL "present") + find_package(QWindowKit REQUIRED COMPONENTS ${expected_components} ${package_path}) + set(link_components "${expected_components}") +elseif(CASE STREQUAL "default") + find_package(QWindowKit REQUIRED ${package_path}) + set(link_components "${expected_components}") +elseif(CASE STREQUAL "core") + find_package(QWindowKit REQUIRED COMPONENTS Core ${package_path}) +elseif(CASE MATCHES "^(missing-required|unknown-required|quiet-missing|repeat)$") + if(CASE STREQUAL "unknown-required") + set(missing MisspelledComponent) + endif() + # A missing QWK component must be reported before any Qt dependency lookup. + set(CMAKE_DISABLE_FIND_PACKAGE_Qt${QT_MAJOR} TRUE) + if(CASE MATCHES "required$") + find_package(QWindowKit REQUIRED COMPONENTS ${missing} ${package_path}) + message(FATAL_ERROR "Missing required component was accepted") + endif() + find_package(QWindowKit QUIET COMPONENTS ${missing} ${package_path}) + if(QWindowKit_FOUND OR QWindowKit_${missing}_FOUND OR TARGET QWindowKit::Core OR + NOT QWindowKit_NOT_FOUND_MESSAGE MATCHES "Unavailable QWindowKit components:") + message(FATAL_ERROR "Incorrect missing-component state/diagnostic") + endif() + if(CASE STREQUAL "quiet-missing") + return() + endif() + unset(CMAKE_DISABLE_FIND_PACKAGE_Qt${QT_MAJOR}) + find_package(QWindowKit REQUIRED COMPONENTS Core ${package_path}) + if(QWindowKit_NOT_FOUND_MESSAGE) + message(FATAL_ERROR "A preceding failed lookup leaked its diagnostic") + endif() +elseif(CASE STREQUAL "optional") + find_package(QWindowKit REQUIRED COMPONENTS Core + OPTIONAL_COMPONENTS ${missing} AnotherUnknown ${package_path}) + if(QWindowKit_${missing}_FOUND OR QWindowKit_AnotherUnknown_FOUND) + message(FATAL_ERROR "Absent optional components were reported as found") + endif() +elseif(CASE STREQUAL "optional-qt-missing") + if(USE_WIDGETS) + set(optional Widgets) + elseif(USE_QUICK) + set(optional Quick) + else() + set(optional Widgets) + endif() + set(CMAKE_DISABLE_FIND_PACKAGE_Qt${QT_MAJOR}${optional} TRUE) + find_package(QWindowKit REQUIRED COMPONENTS Core OPTIONAL_COMPONENTS ${optional} ${package_path}) + if(QWindowKit_${optional}_FOUND) + message(FATAL_ERROR "Optional component without its Qt dependency was reported as usable") + endif() +else() + message(FATAL_ERROR "Unknown test case: ${CASE}") +endif() + +if(NOT QWindowKit_FOUND OR NOT QWindowKit_Core_FOUND) + message(FATAL_ERROR "Required Core/package not found") +endif() +if(NOT QWindowKit_AVAILABLE_COMPONENTS STREQUAL expected_components) + message(FATAL_ERROR "Installed manifest differs: ${QWindowKit_AVAILABLE_COMPONENTS} vs ${expected_components}") +endif() +if(NOT QWindowKit_QT_VERSION_MAJOR EQUAL QT_MAJOR) + message(FATAL_ERROR "Installed package lost its build-time Qt major") +endif() +foreach(component Core Widgets Quick) + if(component IN_LIST expected_components) + if(NOT TARGET QWindowKit::${component}) + message(FATAL_ERROR "Installed target ${component} is absent") + endif() + elseif(TARGET QWindowKit::${component} OR QWindowKit_${component}_FOUND) + message(FATAL_ERROR "Uninstalled component ${component} leaked into this consumer") + endif() +endforeach() +add_executable(component_consumer ../../consumer.cpp) +target_compile_features(component_consumer PRIVATE cxx_std_17) +foreach(component IN LISTS link_components) + if(NOT QWindowKit_${component}_FOUND) + message(FATAL_ERROR "Requested ${component} is not usable") + endif() + target_link_libraries(component_consumer PRIVATE QWindowKit::${component}) + string(TOUPPER "${component}" upper) + target_compile_definitions(component_consumer PRIVATE CONSUMER_USE_${upper}) +endforeach() diff --git a/tests/buildsystems/package/major-consumer/CMakeLists.txt b/tests/buildsystems/package/major-consumer/CMakeLists.txt new file mode 100644 index 00000000..cc476628 --- /dev/null +++ b/tests/buildsystems/package/major-consumer/CMakeLists.txt @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 3.19) +project(qwk_major_contract LANGUAGES NONE) + +# Search only the explicitly supplied fake package roots, never a local Qt installation. +set(CMAKE_FIND_USE_CMAKE_ENVIRONMENT_PATH FALSE) +set(CMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH FALSE) +set(CMAKE_FIND_USE_CMAKE_SYSTEM_PATH FALSE) +set(CMAKE_FIND_USE_PACKAGE_REGISTRY FALSE) +set(CMAKE_FIND_USE_SYSTEM_PACKAGE_REGISTRY FALSE) +set(CMAKE_PREFIX_PATH "${QT_ROOTS}") +set(QT_VERSION_MAJOR "${WRONG_MAJOR}") +find_package(QWindowKit REQUIRED COMPONENTS Core Widgets Quick) +if(STATIC_PACKAGE AND PORTAL_PACKAGE) + if(NOT TARGET Qt${EXPECTED_MAJOR}::DBus) + message(FATAL_ERROR "Static portal package omitted the DBus dependency") + endif() +elseif(TARGET Qt${EXPECTED_MAJOR}::DBus) + message(FATAL_ERROR "Package requires DBus without a static portal backend") +endif() +get_property(found_majors GLOBAL PROPERTY QWK_TEST_QT_MAJORS) +foreach(major IN LISTS found_majors) + if(NOT major EQUAL EXPECTED_MAJOR) + message(FATAL_ERROR "Selected Qt ${major}, expected Qt ${EXPECTED_MAJOR}") + endif() +endforeach() +if(NOT QWindowKit_QT_VERSION_MAJOR EQUAL EXPECTED_MAJOR) + message(FATAL_ERROR "The package did not report its build-time Qt major") +endif() +foreach(module Core Gui Widgets Quick) + if(NOT TARGET Qt${EXPECTED_MAJOR}::${module} OR TARGET Qt${WRONG_MAJOR}::${module}) + message(FATAL_ERROR "Wrong public Qt targets for ${module}") + endif() + if(STATIC_PACKAGE AND EXPECTED_MAJOR EQUAL 6 AND NOT TARGET Qt6::${module}Private) + message(FATAL_ERROR "Missing static Qt dependency: ${module}Private") + endif() +endforeach() +foreach(component Core Widgets Quick) + if(NOT TARGET QWindowKit::${component}) + message(FATAL_ERROR "Missing exported ${component} target") + endif() +endforeach() diff --git a/tests/buildsystems/qmake/README.md b/tests/buildsystems/qmake/README.md new file mode 100644 index 00000000..d6caadea --- /dev/null +++ b/tests/buildsystems/qmake/README.md @@ -0,0 +1,51 @@ +# qmake installation and reconfiguration + +`buildsystems.qmake` builds and links a consumer against the installed enabled +modules, including Quick. The consumer references each selected module's exported +meta-object; it is not a GUI/runtime test. + +When both Widgets and Quick are available, the nine +`buildsystems.qmake.reconfigure.*` tests use one isolated, real QWindowKit build +tree under the current test binary directory: + +1. `prepare`: configure all modules with tests/examples disabled. +2. `build_core`, `build_widgets`, `build_quick`: build each actual library in + dependency order, giving each cold build its own deadline. +3. `all`: install/consume all modules. +4. `core`: turn both optional modules off, install/consume Core. +5. `widgets`: enable Widgets alone, install/consume Core and Widgets. +6. `quick`: switch to Quick alone, install/consume Core and Quick. +7. `restored`: enable both again, install/consume all modules. + +Each transition reconfigures and builds in the same directory. Its install prefix +and consumer build directory are new. Stale Widgets/Quick pri files must still +exist in the library build output after disabling both modules, so deleting the +output directory cannot make the regression pass. The installed pri filenames +must exactly match the enabled targets, and library artifacts must be present +only for enabled modules. All transitions build/link a real qmake consumer. + +The fixture uses nested custom data/library/binary/include installation paths. +It inherits the parent Qt, generator/compiler, StyleAgent setting, shared/static +setting and Debug/Release configuration. Installed libraries, headers and install +rules come from the production project; no fake libraries or install-script copies +are used. +The prebuilt qmsetup dependency is reused, and test execution downloads nothing. +Tests do not claim independently enforced network isolation. + +CTest fixtures order the stages and prevent later execution after a prerequisite +fails; a resource lock prevents parallel mutation of this test's tree. Every test +has a 60-second limit. Configuration, build, install and consumer operations also +have explicit subprocess deadlines. No retries or skip-on-failure behavior is +used. Only `prepare` cleans the dedicated test run directory, after resolving its +path; transitions preserve all historical library build outputs. Existing user +installation prefixes are never cleaned. Removing disabled-module files from an +already populated installation prefix is outside the install contract. + +Run from the compiler's developer environment: + +```sh +ctest --test-dir build -C Release -L reconfigure --output-on-failure --no-tests=error +``` + +Repeat with Debug or a parent configured with `QWINDOWKIT_BUILD_STATIC=ON` to +exercise the matching library names, definitions and static dependencies. diff --git a/tests/buildsystems/qmake/Reconfigure.cmake b/tests/buildsystems/qmake/Reconfigure.cmake new file mode 100644 index 00000000..0ce396ab --- /dev/null +++ b/tests/buildsystems/qmake/Reconfigure.cmake @@ -0,0 +1,138 @@ +cmake_minimum_required(VERSION 3.19) + +include("${CMAKE_CURRENT_LIST_DIR}/../testing/ConsumerBuildCommon.cmake") +qwk_require_variables(SOURCE_DIR TEST_ROOT BUILD_CONFIG STAGE GENERATOR QT_PREFIX QMSETUP_DIR + QMAKE_EXECUTABLE MAKE_PROGRAM) +if(NOT BUILD_CONFIG MATCHES "^(Debug|Release|RelWithDebInfo|MinSizeRel)$" OR + NOT STAGE MATCHES "^(prepare|build_core|build_widgets|build_quick|all|core|widgets|quick|restored)$") + message(FATAL_ERROR "Unexpected reconfiguration stage/configuration") +endif() + +# Resolve the generated work directory before any cleanup. Never clean the caller's +# project/build root, or follow a work-directory junction outside that root. +get_filename_component(_test_root "${TEST_ROOT}" REALPATH) +set(_root "${_test_root}/qmake-reconfigure-${BUILD_CONFIG}") +get_filename_component(_resolved "${_root}" REALPATH) +if(NOT _resolved STREQUAL _root) + message(FATAL_ERROR "Reconfiguration work directory resolves elsewhere: ${_resolved}") +endif() +if(STAGE STREQUAL "prepare") + file(REMOVE_RECURSE "${_root}") +endif() +file(MAKE_DIRECTORY "${_root}") +set(WORK_DIR "${_root}") +set(_build "${_root}/build") +if(STAGE MATCHES "^build_") + # A real cold library build gets its own deadline, separate from configuration + # and consumption. No substitutes for the libraries or their install rules. + set(_build_core QWKCore) + set(_build_widgets QWKWidgets) + set(_build_quick QWKQuick) + execute_process(COMMAND "${CMAKE_COMMAND}" --build "${_build}" + --config "${BUILD_CONFIG}" --parallel 4 --target ${_${STAGE}} + RESULT_VARIABLE _result OUTPUT_VARIABLE _out ERROR_VARIABLE _err TIMEOUT 50) + if(NOT "${_result}" STREQUAL "0") + message(FATAL_ERROR "Initial library build failed (${_result}):\n${_out}\n${_err}") + endif() + return() +endif() +set(_prefix "${_root}/install-${STAGE}") +set(_widgets OFF) +set(_quick OFF) +set(_modules core) +set(_targets QWKCore) +if(STAGE MATCHES "^(prepare|all|widgets|restored)$") + set(_widgets ON) + list(APPEND _modules widgets) + list(APPEND _targets QWKWidgets) +endif() +if(STAGE MATCHES "^(prepare|all|quick|restored)$") + set(_quick ON) + list(APPEND _modules quick) + list(APPEND _targets QWKQuick) +endif() + +set(_generator_args -G "${GENERATOR}") +if(GENERATOR_PLATFORM) + list(APPEND _generator_args -A "${GENERATOR_PLATFORM}") +endif() +if(GENERATOR_TOOLSET) + list(APPEND _generator_args -T "${GENERATOR_TOOLSET}") +endif() +if(CMAKE_MAKE_TOOL) + list(APPEND _generator_args "-DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_TOOL}") +endif() +if(CXX_COMPILER) + list(APPEND _generator_args "-DCMAKE_CXX_COMPILER=${CXX_COMPILER}") +endif() +if(DEFINED ENABLE_STYLE_AGENT) + list(APPEND _generator_args "-DQWINDOWKIT_ENABLE_STYLE_AGENT=${ENABLE_STYLE_AGENT}") +endif() +qwk_run_step("configure ${STAGE}" 15 "${CMAKE_COMMAND}" -S "${SOURCE_DIR}" -B "${_build}" + ${_generator_args} "-DCMAKE_PREFIX_PATH=${QT_PREFIX}" "-Dqmsetup_DIR=${QMSETUP_DIR}" + "-DQT_VERSION_MAJOR=${QT_MAJOR}" "-DCMAKE_BUILD_TYPE=${BUILD_CONFIG}" + "-DCMAKE_DEBUG_POSTFIX=${DEBUG_POSTFIX}" + "-DQWINDOWKIT_BUILD_STATIC=${BUILD_STATIC}" + "-DQWINDOWKIT_BUILD_WIDGETS=${_widgets}" "-DQWINDOWKIT_BUILD_QUICK=${_quick}" + -DQWINDOWKIT_BUILD_TESTS=OFF -DQWINDOWKIT_BUILD_EXAMPLES=OFF + -DQWINDOWKIT_BUILD_DOCUMENTATIONS=OFF -DQWINDOWKIT_INSTALL=ON + "-DCMAKE_INSTALL_PREFIX=${_prefix}" + -DCMAKE_INSTALL_DATADIR=custom/data -DCMAKE_INSTALL_LIBDIR=custom/lib + -DCMAKE_INSTALL_BINDIR=custom/bin -DCMAKE_INSTALL_INCLUDEDIR=custom/include) + +if(STAGE STREQUAL "prepare") + return() +endif() + +qwk_run_step("build ${STAGE}" 15 "${CMAKE_COMMAND}" --build "${_build}" + --config "${BUILD_CONFIG}" --parallel 4 --target ${_targets}) +# Every stage installs into a fresh directory. Preserve stale files in the shared +# build output: they are the regression trigger, and must not be removed by a fix. +if(EXISTS "${_prefix}") + message(FATAL_ERROR "Expected a new install prefix: ${_prefix}") +endif() +qwk_run_step("install ${STAGE}" 10 "${CMAKE_COMMAND}" --install "${_build}" + --config "${BUILD_CONFIG}" --prefix "${_prefix}") +set(_pri_dir "${_prefix}/custom/data/QWindowKit/qmake") +file(GLOB _actual RELATIVE "${_pri_dir}" "${_pri_dir}/*.pri") +set(_expected) +foreach(_target IN LISTS _targets) + list(APPEND _expected "${_target}.pri") +endforeach() +list(SORT _actual) +list(SORT _expected) +if(NOT _actual STREQUAL _expected) + message(FATAL_ERROR "${STAGE}: expected pri files '${_expected}', got '${_actual}'") +endif() +foreach(_target QWKCore QWKWidgets QWKQuick) + # Include versioned Unix shared libraries, archives, import libraries and DLLs. + file(GLOB _libraries "${_prefix}/custom/lib/*${_target}*" + "${_prefix}/custom/bin/*${_target}*") + if(_target IN_LIST _targets) + if(NOT _libraries) + message(FATAL_ERROR "${STAGE}: missing installed library for ${_target}") + endif() + elseif(_libraries) + message(FATAL_ERROR "${STAGE}: disabled module libraries installed: ${_libraries}") + endif() +endforeach() +if(STAGE STREQUAL "core") + foreach(_target QWKWidgets QWKQuick) + if(NOT EXISTS "${_build}/out/share/QWindowKit/qmake/${_target}.pri") + message(FATAL_ERROR "Missing stale build artifact for regression: ${_target}.pri") + endif() + endforeach() +endif() + +set(INSTALL_PREFIX "${_prefix}") +set(PROJECT_FILE "${CMAKE_CURRENT_LIST_DIR}/consumer.pro") +set(WORK_DIR "${_root}/consumer-${STAGE}") +set(QWK_QMAKE_DIR "${_pri_dir}") +set(QWK_MODULES "${_modules}") +if(BUILD_CONFIG STREQUAL "Debug") + set(BUILD_CONFIG debug) +else() + set(BUILD_CONFIG release) +endif() +include("${CMAKE_CURRENT_LIST_DIR}/RunBuild.cmake") +message(STATUS "${STAGE}: installed exactly ${_expected}; qmake consumer linked successfully") diff --git a/tests/buildsystems/qmake/RunBuild.cmake b/tests/buildsystems/qmake/RunBuild.cmake index bda71710..676c6981 100644 --- a/tests/buildsystems/qmake/RunBuild.cmake +++ b/tests/buildsystems/qmake/RunBuild.cmake @@ -16,11 +16,16 @@ file(MAKE_DIRECTORY "${WORK_DIR}") # qmake takes a list as a whitespace separated value. string(REPLACE ";" " " _modules "${QWK_MODULES}") +set(_qmake_args) +if(DEFINED QWK_QMAKE_DIR) + list(APPEND _qmake_args "QWK_QMAKE_DIR=${QWK_QMAKE_DIR}") +endif() -qwk_run_step("qmake" "${QMAKE_EXECUTABLE}" "${PROJECT_FILE}" +qwk_run_step("qmake" 15 "${QMAKE_EXECUTABLE}" "${PROJECT_FILE}" "QWK_PREFIX=${INSTALL_PREFIX}" "QWK_MODULES=${_modules}" "CONFIG+=${BUILD_CONFIG}" + ${_qmake_args} ) -qwk_run_step("make" "${MAKE_PROGRAM}") +qwk_run_step("make" 25 "${MAKE_PROGRAM}") diff --git a/tests/buildsystems/qmake/consumer.pro b/tests/buildsystems/qmake/consumer.pro index 7db9e275..28131ee2 100644 --- a/tests/buildsystems/qmake/consumer.pro +++ b/tests/buildsystems/qmake/consumer.pro @@ -17,7 +17,7 @@ QT += core gui isEmpty(QWK_PREFIX): error("QWK_PREFIX must be given on the qmake command line.") -QWK_QMAKE_DIR = $$QWK_PREFIX/share/QWindowKit/qmake +isEmpty(QWK_QMAKE_DIR): QWK_QMAKE_DIR = $$QWK_PREFIX/share/QWindowKit/qmake contains(QWK_MODULES, widgets) { QT += widgets @@ -30,3 +30,9 @@ contains(QWK_MODULES, widgets) { } SOURCES += $$PWD/../consumer.cpp + +contains(QWK_MODULES, quick) { + QT += quick + DEFINES += CONSUMER_USE_QUICK + !include($$QWK_QMAKE_DIR/QWKQuick.pri): error("cannot include $$QWK_QMAKE_DIR/QWKQuick.pri") +} diff --git a/tests/buildsystems/testing/ConsumerBuildCommon.cmake b/tests/buildsystems/testing/ConsumerBuildCommon.cmake index 24434a71..be3b0628 100644 --- a/tests/buildsystems/testing/ConsumerBuildCommon.cmake +++ b/tests/buildsystems/testing/ConsumerBuildCommon.cmake @@ -15,23 +15,38 @@ function(qwk_require_variables) endforeach() endfunction() +function(qwk_find_installed_config _out _prefix) + file(GLOB_RECURSE _configs "${_prefix}/*QWindowKitConfig.cmake") + list(LENGTH _configs _count) + if(NOT _count EQUAL 1) + message(FATAL_ERROR "Expected one installed QWindowKit config in ${_prefix}, got ${_configs}") + endif() + list(GET _configs 0 _config) + get_filename_component(_directory "${_config}" DIRECTORY) + set(${_out} "${_directory}" PARENT_SCOPE) +endfunction() + #[[ Run one step of a consumer build in WORK_DIR, and turn a non-zero exit into a test failure carrying the command and its output. The output is held back until then, since a passing test that prints a whole build log buries the ones that did not pass. - qwk_run_step(