diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 40503e480..20f94f0f6 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -23,5 +23,7 @@ add_subdirectory(test_set_wallpaper) add_subdirectory(test_set_xwindow_position) add_subdirectory(test_idle_inhibit_v1) add_subdirectory(test_toplevel_tag) +add_subdirectory(test_im_candidate_panel_tag) +add_subdirectory(test_im_candidate_panel_xprop) add_subdirectory(test_input_manager) add_subdirectory(test_keyboard_state_notify) diff --git a/examples/test_im_candidate_panel_tag/CMakeLists.txt b/examples/test_im_candidate_panel_tag/CMakeLists.txt new file mode 100644 index 000000000..f3211112b --- /dev/null +++ b/examples/test_im_candidate_panel_tag/CMakeLists.txt @@ -0,0 +1,44 @@ +find_package(PkgConfig REQUIRED) +pkg_get_variable(WAYLAND_PROTOCOLS_DATADIR wayland-protocols pkgdatadir) +find_program(WAYLAND_SCANNER wayland-scanner REQUIRED) + +pkg_check_modules(WAYLAND_CLIENT REQUIRED wayland-client) + +set(PROTOCOL_DIR ${CMAKE_CURRENT_BINARY_DIR}) + +# Generate xdg-toplevel-tag-v1 client code +set(TAG_PROTOCOL ${WAYLAND_PROTOCOLS_DATADIR}/staging/xdg-toplevel-tag/xdg-toplevel-tag-v1.xml) +set(TAG_CLIENT_H ${PROTOCOL_DIR}/xdg-toplevel-tag-v1-client-protocol.h) +set(TAG_CLIENT_CODE ${PROTOCOL_DIR}/xdg-toplevel-tag-v1-client-protocol.c) + +add_custom_command( + OUTPUT ${TAG_CLIENT_H} ${TAG_CLIENT_CODE} + COMMAND ${WAYLAND_SCANNER} private-code < ${TAG_PROTOCOL} > ${TAG_CLIENT_CODE} + COMMAND ${WAYLAND_SCANNER} client-header < ${TAG_PROTOCOL} > ${TAG_CLIENT_H} + DEPENDS ${TAG_PROTOCOL} + COMMENT "Generating xdg-toplevel-tag-v1 protocol" + VERBATIM +) + +# Generate xdg-shell client code +set(XDG_SHELL_PROTOCOL ${WAYLAND_PROTOCOLS_DATADIR}/stable/xdg-shell/xdg-shell.xml) +set(XDG_SHELL_CLIENT_H ${PROTOCOL_DIR}/xdg-shell-client-protocol.h) +set(XDG_SHELL_CLIENT_CODE ${PROTOCOL_DIR}/xdg-shell-client-protocol.c) + +add_custom_command( + OUTPUT ${XDG_SHELL_CLIENT_H} ${XDG_SHELL_CLIENT_CODE} + COMMAND ${WAYLAND_SCANNER} private-code < ${XDG_SHELL_PROTOCOL} > ${XDG_SHELL_CLIENT_CODE} + COMMAND ${WAYLAND_SCANNER} client-header < ${XDG_SHELL_PROTOCOL} > ${XDG_SHELL_CLIENT_H} + DEPENDS ${XDG_SHELL_PROTOCOL} + COMMENT "Generating xdg-shell protocol" + VERBATIM +) + +set(BIN_NAME test-im-candidate-panel-tag) + +add_executable(${BIN_NAME} main.c ${TAG_CLIENT_CODE} ${XDG_SHELL_CLIENT_CODE}) + +target_include_directories(${BIN_NAME} PRIVATE ${PROTOCOL_DIR}) +target_link_libraries(${BIN_NAME} PRIVATE ${WAYLAND_CLIENT_LIBRARIES}) + +install(TARGETS ${BIN_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") diff --git a/examples/test_im_candidate_panel_tag/main.c b/examples/test_im_candidate_panel_tag/main.c new file mode 100644 index 000000000..e74a7ed4b --- /dev/null +++ b/examples/test_im_candidate_panel_tag/main.c @@ -0,0 +1,186 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#define _GNU_SOURCE +#include "xdg-shell-client-protocol.h" +#include "xdg-toplevel-tag-v1-client-protocol.h" + +#include + +#include +#include +#include +#include +#include +#include + +static struct wl_display *display = NULL; +static struct wl_surface *surface = NULL; +static struct xdg_surface *xdg_surface = NULL; +static struct xdg_toplevel *xdg_toplevel = NULL; +static struct wl_buffer *buffer = NULL; + +static void cleanup(void) +{ + if (buffer) + wl_buffer_destroy(buffer); + if (xdg_toplevel) + xdg_toplevel_destroy(xdg_toplevel); + if (xdg_surface) + xdg_surface_destroy(xdg_surface); + if (surface) + wl_surface_destroy(surface); + if (display) + wl_display_disconnect(display); +} + +static struct wl_compositor *compositor = NULL; +static struct xdg_wm_base *wm_base = NULL; +static struct xdg_toplevel_tag_manager_v1 *tag_manager = NULL; +static struct wl_shm *shm = NULL; + +static int create_shm_fd(int size) +{ + int fd = memfd_create("wayland-shm", MFD_CLOEXEC); + if (fd < 0) + return -1; + if (ftruncate(fd, size) < 0) { + close(fd); + return -1; + } + return fd; +} + +static struct wl_buffer *create_shm_buffer(int width, int height) +{ + int stride = width * 4; + int size = stride * height; + int fd = create_shm_fd(size); + if (fd < 0) { + fprintf(stderr, "Failed to create shm fd\n"); + return NULL; + } + + uint32_t *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (data == MAP_FAILED) { + close(fd); + return NULL; + } + + for (int i = 0; i < width * height; i++) + data[i] = 0xFF2266AA; + + struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, size); + struct wl_buffer *buffer = + wl_shm_pool_create_buffer(pool, 0, width, height, stride, WL_SHM_FORMAT_XRGB8888); + wl_shm_pool_destroy(pool); + munmap(data, size); + close(fd); + return buffer; +} + +static void xdg_surface_configure([[maybe_unused]] void *data, + struct xdg_surface *xdg_surface, + uint32_t serial) +{ + xdg_surface_ack_configure(xdg_surface, serial); +} + +static const struct xdg_surface_listener xdg_surface_listener = { + .configure = xdg_surface_configure, +}; + +static void xdg_wm_base_ping([[maybe_unused]] void *data, + struct xdg_wm_base *xdg_wm_base, + uint32_t serial) +{ + xdg_wm_base_pong(xdg_wm_base, serial); +} + +static const struct xdg_wm_base_listener xdg_wm_base_listener = { + .ping = xdg_wm_base_ping, +}; + +static void registry_global([[maybe_unused]] void *data, + struct wl_registry *registry, + uint32_t id, + const char *interface, + uint32_t version) +{ + if (strcmp(interface, wl_compositor_interface.name) == 0) { + compositor = wl_registry_bind(registry, id, &wl_compositor_interface, version); + } else if (strcmp(interface, xdg_wm_base_interface.name) == 0) { + wm_base = wl_registry_bind(registry, id, &xdg_wm_base_interface, version); + xdg_wm_base_add_listener(wm_base, &xdg_wm_base_listener, NULL); + } else if (strcmp(interface, xdg_toplevel_tag_manager_v1_interface.name) == 0) { + tag_manager = wl_registry_bind(registry, id, &xdg_toplevel_tag_manager_v1_interface, 1); + } else if (strcmp(interface, wl_shm_interface.name) == 0) { + shm = wl_registry_bind(registry, id, &wl_shm_interface, version); + } +} + +static void registry_global_remove([[maybe_unused]] void *data, + [[maybe_unused]] struct wl_registry *registry, + [[maybe_unused]] uint32_t id) +{ +} + +static const struct wl_registry_listener registry_listener = { + .global = registry_global, + .global_remove = registry_global_remove, +}; + +int main() +{ + display = wl_display_connect(NULL); + if (!display) { + fprintf(stderr, "Failed to connect to wayland display\n"); + return 1; + } + + struct wl_registry *registry = wl_display_get_registry(display); + wl_registry_add_listener(registry, ®istry_listener, NULL); + wl_display_roundtrip(display); + + if (!compositor || !wm_base || !tag_manager || !shm) { + fprintf(stderr, "Missing required interfaces\n"); + wl_display_disconnect(display); + return 1; + } + + int width = 400, height = 100; + surface = wl_compositor_create_surface(compositor); + + xdg_surface = xdg_wm_base_get_xdg_surface(wm_base, surface); + xdg_surface_add_listener(xdg_surface, &xdg_surface_listener, NULL); + + xdg_toplevel = xdg_surface_get_toplevel(xdg_surface); + xdg_toplevel_set_title(xdg_toplevel, "IM Candidate Panel Tag Test"); + xdg_toplevel_set_app_id(xdg_toplevel, "im-candidate-panel-tag"); + + buffer = create_shm_buffer(width, height); + if (!buffer) { + fprintf(stderr, "Failed to create buffer\n"); + cleanup(); + return 1; + } + + // Set tag BEFORE the first commit + xdg_toplevel_tag_manager_v1_set_toplevel_tag(tag_manager, + xdg_toplevel, + "org.deepin.treeland.im-candidate-panel"); + fprintf(stderr, "Tag set: org.deepin.treeland.im-candidate-panel\n"); + + // First commit: initial commit without buffer, wait for configure + wl_surface_commit(surface); + wl_display_roundtrip(display); + + // Attach buffer after configure is acked + wl_surface_attach(surface, buffer, 0, 0); + wl_surface_commit(surface); + + while (wl_display_dispatch(display) != -1) { } + + cleanup(); + return 0; +} diff --git a/examples/test_im_candidate_panel_xprop/CMakeLists.txt b/examples/test_im_candidate_panel_xprop/CMakeLists.txt new file mode 100644 index 000000000..31038bef3 --- /dev/null +++ b/examples/test_im_candidate_panel_xprop/CMakeLists.txt @@ -0,0 +1,14 @@ +find_package(Qt6 REQUIRED COMPONENTS Widgets) +find_package(PkgConfig REQUIRED) +pkg_check_modules(XCB REQUIRED xcb) + +set(BIN_NAME test-im-candidate-panel-xprop) + +qt_add_executable(${BIN_NAME} + main.cpp +) + +target_include_directories(${BIN_NAME} PRIVATE ${XCB_INCLUDE_DIRS}) +target_link_libraries(${BIN_NAME} PRIVATE Qt6::Widgets ${XCB_LIBRARIES}) + +install(TARGETS ${BIN_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") diff --git a/examples/test_im_candidate_panel_xprop/main.cpp b/examples/test_im_candidate_panel_xprop/main.cpp new file mode 100644 index 000000000..f2cbb05fd --- /dev/null +++ b/examples/test_im_candidate_panel_xprop/main.cpp @@ -0,0 +1,86 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include + +#include +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + // Force X11 platform + qputenv("QT_QPA_PLATFORM", "xcb"); + + QApplication app(argc, argv); + + QMainWindow window; + window.setWindowTitle("IM Candidate Panel XProp Test"); + window.resize(400, 100); + + auto *label = new QLabel("example im-candidate-panel/xprop", &window); + label->setAlignment(Qt::AlignCenter); + window.setCentralWidget(label); + + // Force platform window creation before show, so we can set xprop + WId wid = window.winId(); + + // Set _DEEPIN_IM_CANDIDATE_PANEL xprop: type=CARDINAL, value=1 + xcb_connection_t *conn = xcb_connect(nullptr, nullptr); + if (xcb_connection_has_error(conn)) { + qWarning() << "Failed to connect to X server"; + return 1; + } + + xcb_intern_atom_cookie_t atom_cookie = xcb_intern_atom(conn, + 0, + strlen("_DEEPIN_IM_CANDIDATE_PANEL"), + "_DEEPIN_IM_CANDIDATE_PANEL"); + xcb_intern_atom_reply_t *atom_reply = xcb_intern_atom_reply(conn, atom_cookie, nullptr); + if (!atom_reply) { + qWarning() << "Failed to intern atom"; + xcb_disconnect(conn); + return 1; + } + xcb_atom_t atom = atom_reply->atom; + + xcb_intern_atom_cookie_t cardinal_cookie = + xcb_intern_atom(conn, 0, strlen("CARDINAL"), "CARDINAL"); + xcb_intern_atom_reply_t *cardinal_reply = xcb_intern_atom_reply(conn, cardinal_cookie, nullptr); + if (!cardinal_reply) { + qWarning() << "Failed to intern CARDINAL atom"; + free(atom_reply); + xcb_disconnect(conn); + return 1; + } + xcb_atom_t cardinal = cardinal_reply->atom; + + uint32_t value = 1; + xcb_void_cookie_t change_cookie = xcb_change_property_checked(conn, + XCB_PROP_MODE_REPLACE, + wid, + atom, + cardinal, + 32, + 1, + &value); + xcb_flush(conn); + + xcb_generic_error_t *error = xcb_request_check(conn, change_cookie); + if (error) { + qWarning() << "xcb_change_property failed: error_code=" << error->error_code; + free(error); + } else { + qDebug() << "Set _DEEPIN_IM_CANDIDATE_PANEL=1 on window" << wid; + } + + free(cardinal_reply); + free(atom_reply); + xcb_disconnect(conn); + + window.show(); + + return app.exec(); +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fe3d9cfa8..ac1c45c4f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -133,6 +133,8 @@ qt_add_qml_module(libtreeland core/qmlengine.h core/rootsurfacecontainer.cpp core/rootsurfacecontainer.h + core/imcandidatepanelmanager.cpp + core/imcandidatepanelmanager.h core/shellhandler.cpp core/shellhandler.h core/treeland.cpp diff --git a/src/core/imcandidatepanelmanager.cpp b/src/core/imcandidatepanelmanager.cpp new file mode 100644 index 000000000..29542c241 --- /dev/null +++ b/src/core/imcandidatepanelmanager.cpp @@ -0,0 +1,247 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "imcandidatepanelmanager.h" + +#include "output/output.h" +#include "rootsurfacecontainer.h" +#include "shellhandler.h" +#include "surface/surfacewrapper.h" + +#include + +#include +#include +#include +#include +#include + +#include + +const QLatin1String IMCandidatePanelManager::IM_CANDIDATE_PANEL( + "org.deepin.treeland.im-candidate-panel"); + +IMCandidatePanelManager::IMCandidatePanelManager( + ShellHandler *shellHandler, + WAYLIB_SERVER_NAMESPACE::WInputMethodHelper *inputMethodHelper, + QObject *parent) + : QObject(parent) + , m_shellHandler(shellHandler) + , m_inputMethodHelper(inputMethodHelper) +{ + connect(m_shellHandler, + &ShellHandler::surfaceWrapperAboutToRemove, + this, + [this](SurfaceWrapper *wrapper) { + m_imCandidatePanels.removeAll(wrapper); + }); +} + +bool IMCandidatePanelManager::parseIMCandidatePanelProperty( + const QMap &result, + xcb_atom_t atom) +{ + auto it = result.constFind(atom); + if (it != result.constEnd() && it->size() >= static_cast(sizeof(uint32_t))) { + uint32_t v = 0; + memcpy(&v, it->constData(), sizeof(uint32_t)); + return v == 1; + } + return false; +} + +void IMCandidatePanelManager::setupXWayland(WAYLIB_SERVER_NAMESPACE::WXWayland *xwayland) +{ + m_xwayland = xwayland; + m_imCandidatePanelAtom = xwayland->atom("_DEEPIN_IM_CANDIDATE_PANEL"); + xwayland->setAtomSupported(m_imCandidatePanelAtom, true); + + connect(xwayland, + &WAYLIB_SERVER_NAMESPACE::WXWayland::windowPropertyChanged, + this, + &IMCandidatePanelManager::onXwaylandPropertyChanged); +} + +bool IMCandidatePanelManager::isIMCandidatePanel(SurfaceWrapper *wrapper) const +{ + return wrapper->isIMCandidatePanel(); +} + +bool IMCandidatePanelManager::isIMCandidatePanel( + WAYLIB_SERVER_NAMESPACE::WXWaylandSurface *surface) const +{ + if (m_imCandidatePanelAtom == XCB_ATOM_NONE || !m_xwayland) + return false; + + return surface->property("imCandidatePanel").toBool(); +} + +xcb_atom_t IMCandidatePanelManager::imCandidatePanelAtom() const +{ + return m_imCandidatePanelAtom; +} + +bool IMCandidatePanelManager::checkAndApplyIMCandidatePanel( + SurfaceWrapper *wrapper, + WAYLIB_SERVER_NAMESPACE::WXdgToplevelSurface *surface) +{ + if (surface->tag() != IM_CANDIDATE_PANEL) + return false; + if (wrapper->isIMCandidatePanel()) + return false; + applyIMCandidatePanel(wrapper); + return true; +} + +bool IMCandidatePanelManager::checkAndApplyIMCandidatePanel( + SurfaceWrapper *wrapper, + WAYLIB_SERVER_NAMESPACE::WXWaylandSurface *surface) +{ + if (!isIMCandidatePanel(surface)) + return false; + if (wrapper->isIMCandidatePanel()) + return false; + applyIMCandidatePanel(wrapper); + return true; +} + +void IMCandidatePanelManager::applyIMCandidatePanel(SurfaceWrapper *wrapper) +{ + auto *popupContainer = m_shellHandler->popupContainer(); + + auto *currentContainer = wrapper->container(); + if (currentContainer) + currentContainer->removeSurface(wrapper); + + wrapper->setIMCandidatePanel(true); + popupContainer->addSurface(wrapper); + wrapper->setHasInitializeContainer(true); + wrapper->setPositionAutomatic(false); + wrapper->setXwaylandPositionFromSurface(false); + + if (wrapper->surfaceItem()) + wrapper->surfaceItem()->setFocusPolicy(Qt::NoFocus); + + wrapper->setAcceptKeyboardFocus(false); + + if (!wrapper->noDecoration()) + wrapper->setNoDecoration(true); + + if (!wrapper->skipDockPreView()) + m_shellHandler->foreignToplevel()->removeSurface(wrapper); + + wrapper->disableWindowAnimation(); + + m_imCandidatePanels.append(wrapper); + + connect(wrapper, + &SurfaceWrapper::normalGeometryChanged, + this, + &IMCandidatePanelManager::onIMCandidatePanelGeometryChanged); + + applyInitialPositionAtCursor(); +} + +void IMCandidatePanelManager::applyInitialPositionAtCursor() +{ + auto cursorRect = m_inputMethodHelper->textInputCursorRect(); + if (cursorRect.isEmpty() || m_imCandidatePanels.isEmpty()) + return; + + auto *focusSurface = m_inputMethodHelper->textInputFocusSurface(); + if (!focusSurface) + return; + + auto *rootContainer = m_shellHandler->rootSurfaceContainer(); + auto *focusWrapper = rootContainer->getSurface(focusSurface); + if (!focusWrapper) + return; + + auto *output = focusWrapper->ownsOutput(); + if (!output) + return; + + QPointF pos; + auto wrapperPos = focusWrapper->mapToGlobal(QPointF(0, 0)); + pos.setX(wrapperPos.x() + cursorRect.x()); + pos.setY(wrapperPos.y() + cursorRect.y() + cursorRect.height() + + focusWrapper->titlebarGeometry().height()); + + arrangeIMCandidatePanels(output, pos); +} + +void IMCandidatePanelManager::arrangeIMCandidatePanels(Output *output, const QPointF &basePos) +{ + for (auto *wrapper : m_imCandidatePanels) { + auto normalGeo = wrapper->normalGeometry(); + // Wait for geometry to be valid before arranging + if (normalGeo.isEmpty()) + continue; + + auto pos = basePos; + output->adjustToOutputBounds(pos, normalGeo, output->geometry()); + wrapper->moveNormalGeometryInOutput(pos); + } +} + +void IMCandidatePanelManager::onIMCandidatePanelGeometryChanged() +{ + if (m_imCandidatePanels.isEmpty()) + return; + + auto *focusSurface = m_inputMethodHelper->textInputFocusSurface(); + auto *rootContainer = m_shellHandler->rootSurfaceContainer(); + auto *focusWrapper = focusSurface ? rootContainer->getSurface(focusSurface) : nullptr; + if (!focusWrapper) + return; + + auto *output = focusWrapper->ownsOutput(); + if (!output) + return; + + QPointF pos; + auto wrapperPos = focusWrapper->mapToGlobal(QPointF(0, 0)); + auto cursorRect = m_inputMethodHelper->textInputCursorRect(); + pos.setX(wrapperPos.x() + cursorRect.x()); + pos.setY(wrapperPos.y() + cursorRect.y() + cursorRect.height() + + focusWrapper->titlebarGeometry().height()); + + arrangeIMCandidatePanels(output, pos); +} + +void IMCandidatePanelManager::onXwaylandPropertyChanged( + WAYLIB_SERVER_NAMESPACE::WXWaylandSurface *surface, + xcb_atom_t atom) +{ + if (atom != m_imCandidatePanelAtom) + return; + + auto *rootContainer = m_shellHandler->rootSurfaceContainer(); + auto *wrapper = rootContainer->getSurface(surface); + + // Async read the property value via readAsyncProperties (non-blocking). + if (!m_xwayland) + return; + + auto windowId = surface->handle()->handle()->window_id; + QVector requests = { + { m_imCandidatePanelAtom, XCB_ATOM_CARDINAL } + }; + m_xwayland->readAsyncProperties( + windowId, + requests, + 50, + [self = QPointer(this), + surface = QPointer(surface), + wrapper = QPointer(wrapper), + atom = m_imCandidatePanelAtom](xcb_window_t, const QMap &result) { + auto *s = surface.data(); + if (!s || !self) + return; + bool value = IMCandidatePanelManager::parseIMCandidatePanelProperty(result, atom); + s->setProperty("imCandidatePanel", value); + if (wrapper) { + self->checkAndApplyIMCandidatePanel(wrapper, s); + } + }); +} diff --git a/src/core/imcandidatepanelmanager.h b/src/core/imcandidatepanelmanager.h new file mode 100644 index 000000000..445ad6ff2 --- /dev/null +++ b/src/core/imcandidatepanelmanager.h @@ -0,0 +1,63 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include + +#include + +#include +#include + +class SurfaceWrapper; +class ShellHandler; +class RootSurfaceContainer; +class Output; + +namespace WAYLIB_SERVER_NAMESPACE { +class WInputMethodHelper; +class WXdgToplevelSurface; +class WXWayland; +class WXWaylandSurface; +} // namespace WAYLIB_SERVER_NAMESPACE + +class IMCandidatePanelManager : public QObject +{ + Q_OBJECT + +public: + explicit IMCandidatePanelManager(ShellHandler *shellHandler, + WAYLIB_SERVER_NAMESPACE::WInputMethodHelper *inputMethodHelper, + QObject *parent = nullptr); + + void setupXWayland(WAYLIB_SERVER_NAMESPACE::WXWayland *xwayland); + + bool isIMCandidatePanel(SurfaceWrapper *wrapper) const; + bool isIMCandidatePanel(WAYLIB_SERVER_NAMESPACE::WXWaylandSurface *surface) const; + bool checkAndApplyIMCandidatePanel(SurfaceWrapper *wrapper, + WAYLIB_SERVER_NAMESPACE::WXdgToplevelSurface *surface); + bool checkAndApplyIMCandidatePanel(SurfaceWrapper *wrapper, + WAYLIB_SERVER_NAMESPACE::WXWaylandSurface *surface); + xcb_atom_t imCandidatePanelAtom() const; + + // Parse IM candidate panel property from async result map + static bool parseIMCandidatePanelProperty(const QMap &result, + xcb_atom_t atom); + +private: + void applyIMCandidatePanel(SurfaceWrapper *wrapper); + void arrangeIMCandidatePanels(Output *output, const QPointF &basePos); + void onIMCandidatePanelGeometryChanged(); + + static const QLatin1String IM_CANDIDATE_PANEL; + void applyInitialPositionAtCursor(); + void onXwaylandPropertyChanged(WAYLIB_SERVER_NAMESPACE::WXWaylandSurface *surface, + xcb_atom_t atom); + + ShellHandler *m_shellHandler; + WAYLIB_SERVER_NAMESPACE::WInputMethodHelper *m_inputMethodHelper; + QPointer m_xwayland; + xcb_atom_t m_imCandidatePanelAtom = XCB_ATOM_NONE; + QList m_imCandidatePanels; +}; diff --git a/src/core/shellhandler.cpp b/src/core/shellhandler.cpp index b521c8454..72521e125 100644 --- a/src/core/shellhandler.cpp +++ b/src/core/shellhandler.cpp @@ -4,6 +4,7 @@ #include "shellhandler.h" #include "common/treelandlogging.h" +#include "core/imcandidatepanelmanager.h" #include "core/qmlengine.h" #include "core/windowconfigstore.h" #include "layersurfacecontainer.h" @@ -272,6 +273,21 @@ Workspace *ShellHandler::workspace() const return m_workspace; } +SurfaceContainer *ShellHandler::popupContainer() const +{ + return m_popupContainer; +} + +RootSurfaceContainer *ShellHandler::rootSurfaceContainer() const +{ + return m_rootSurfaceContainer; +} + +ForeignToplevelManagerInterfaceV1 *ShellHandler::foreignToplevel() const +{ + return m_treelandForeignToplevel; +} + void ShellHandler::createComponent(QmlEngine *engine, QQuickItem *parentItem) { m_windowMenu = engine->createWindowMenu(Helper::instance()); @@ -335,6 +351,8 @@ void ShellHandler::init(WServer *server, WSeat *seat) m_inputMethodHelper = new WInputMethodHelper(server, seat); m_inputMethodHelper->setParent(this); + m_imCandidatePanelManager = new IMCandidatePanelManager(this, m_inputMethodHelper, this); + connect(m_inputMethodHelper, &WInputMethodHelper::inputPopupSurfaceV2Added, this, @@ -354,11 +372,14 @@ WXWayland *ShellHandler::createXWayland(WServer *server, m_xwaylands.append(xwayland); xwayland->setSeat(seat); connect(xwayland, &WXWayland::surfaceAdded, this, &ShellHandler::onXWaylandSurfaceAdded); - connect(xwayland, &WXWayland::ready, xwayland, [xwayland] { + connect(xwayland, &WXWayland::ready, xwayland, [this, xwayland] { auto atomPid = xwayland->atom("_NET_WM_PID"); xwayland->setAtomSupported(atomPid, true); auto atomNoTitlebar = xwayland->atom("_DEEPIN_NO_TITLEBAR"); xwayland->setAtomSupported(atomNoTitlebar, true); + + if (m_imCandidatePanelManager) + m_imCandidatePanelManager->setupXWayland(xwayland); }); return xwayland; } @@ -470,6 +491,15 @@ void ShellHandler::ensureXdgWrapper(WXdgToplevelSurface *surface, const QString registerSurfaceToForeignToplevel(wrapper); } Q_EMIT surfaceWrapperAdded(wrapper); + + // IM candidate panel detection via xdg-toplevel-tag + if (m_imCandidatePanelManager) { + QPointer wrapperPtr(wrapper); + surface->safeConnect(&WXdgToplevelSurface::tagChanged, this, [this, surface, wrapperPtr]() { + if (wrapperPtr) + m_imCandidatePanelManager->checkAndApplyIMCandidatePanel(wrapperPtr, surface); + }); + } } void ShellHandler::onXdgToplevelSurfaceRemoved(WXdgToplevelSurface *surface) @@ -564,15 +594,17 @@ void ShellHandler::onXWaylandSurfaceAdded(WXWaylandSurface *surface) m_pendingAppIdResolveToplevels.append(raw); bool started = m_appIdResolverManager->resolvePidfd( pidfd, - [this, surface](const QString &appId) { + [self = QPointer(this), + surface](const QString &appId) { auto raw = surface.data(); - if (!raw) + if (!raw || !self) return; // surface destroyed before callback - int idx = m_pendingAppIdResolveToplevels.indexOf(raw); + int idx = + self->m_pendingAppIdResolveToplevels.indexOf(raw); if (idx < 0) return; // removed before callback - ensureXwaylandWrapper(raw, appId); - m_pendingAppIdResolveToplevels.removeAt(idx); + self->fetchInitialProperties(raw, appId); + self->m_pendingAppIdResolveToplevels.removeAt(idx); }); if (started) { qCDebug(lcTlShell) @@ -587,13 +619,20 @@ void ShellHandler::onXWaylandSurfaceAdded(WXWaylandSurface *surface) } } } - // Async path not taken: directly match or create (empty appId triggers - // fallback retrieval) - ensureXwaylandWrapper(raw, QString()); + // Async path not taken: directly fetch properties then match/create + fetchInitialProperties(raw, QString()); }); surface->safeConnect(&WXWaylandSurface::aboutToDissociate, this, [this, surface] { auto wrapper = m_rootSurfaceContainer->getSurface(surface); qCDebug(lcTlShell) << "WXWayland::aboutToDissociate" << surface << wrapper; + + // Cancel pending async property fetch for this surface. + auto *xwayland = surface->xwayland(); + if (xwayland) { + auto windowId = surface->handle()->handle()->window_id; + xwayland->cancelAsyncProperties(windowId); + } + // Cancel pending async resolve if still present. If wrapper never created, return. if (!wrapper) { if (!m_pendingAppIdResolveToplevels.removeOne(surface)) { @@ -618,6 +657,52 @@ void ShellHandler::onXWaylandSurfaceAdded(WXWaylandSurface *surface) }); } +void ShellHandler::fetchInitialProperties(WXWaylandSurface *surface, const QString &appId) +{ + auto *xwayland = surface->xwayland(); + if (!xwayland) { + ensureXwaylandWrapper(surface, appId); + return; + } + + auto windowId = surface->handle()->handle()->window_id; + QVector requests; + if (m_imCandidatePanelManager) { + requests.append({ m_imCandidatePanelManager->imCandidatePanelAtom(), XCB_ATOM_CARDINAL }); + } + + if (requests.isEmpty()) { + ensureXwaylandWrapper(surface, appId); + return; + } + + xwayland->readAsyncProperties( + windowId, + requests, + 50, + [self = QPointer(this), + surface = QPointer(surface), + appId](xcb_window_t, const QMap &result) { + auto *raw = surface.data(); + if (!raw || !self) + return; + self->onInitialPropertiesReady(raw, appId, result); + }); +} + +void ShellHandler::onInitialPropertiesReady(WXWaylandSurface *surface, + const QString &appId, + const QMap &result) +{ + if (m_imCandidatePanelManager) { + bool value = IMCandidatePanelManager::parseIMCandidatePanelProperty( + result, + m_imCandidatePanelManager->imCandidatePanelAtom()); + surface->setProperty("imCandidatePanel", value); + } + ensureXwaylandWrapper(surface, appId); +} + void ShellHandler::ensureXwaylandWrapper(WXWaylandSurface *surface, const QString &targetAppId) { // Check if this matches a closed splash screen @@ -656,6 +741,13 @@ void ShellHandler::ensureXwaylandWrapper(WXWaylandSurface *surface, const QStrin isNewWrapper = true; // newly created } + // IM candidate panel detection via XWayland xprop + if (m_imCandidatePanelManager + && m_imCandidatePanelManager->checkAndApplyIMCandidatePanel(wrapper, surface)) { + Q_EMIT surfaceWrapperAdded(wrapper); + return; + } + // Initialize wrapper auto updateSurfaceWithParentContainer = [this, wrapper, surface] { updateWrapperContainer(wrapper, surface->parentSurface()); diff --git a/src/core/shellhandler.h b/src/core/shellhandler.h index b3dd4a5a8..362cb8772 100644 --- a/src/core/shellhandler.h +++ b/src/core/shellhandler.h @@ -5,12 +5,15 @@ #include "modules/foreign-toplevel/foreigntoplevelmanagerv1.h" +#include + #include #include #include #include +#include #include #include #include @@ -28,6 +31,7 @@ class LayerSurfaceContainer; class Workspace; class SurfaceContainer; class PopupSurfaceContainer; +class IMCandidatePanelManager; class QmlEngine; class ForeignToplevelManagerInterfaceV1; class PrelaunchSplash; @@ -73,6 +77,9 @@ class ShellHandler : public QObject explicit ShellHandler(RootSurfaceContainer *rootContainer, WAYLIB_SERVER_NAMESPACE::WServer *server); [[nodiscard]] Workspace *workspace() const; + [[nodiscard]] SurfaceContainer *popupContainer() const; + [[nodiscard]] RootSurfaceContainer *rootSurfaceContainer() const; + [[nodiscard]] ForeignToplevelManagerInterfaceV1 *foreignToplevel() const; void createComponent(QmlEngine *engine, QQuickItem *parentItem); void init(WAYLIB_SERVER_NAMESPACE::WServer *server, WAYLIB_SERVER_NAMESPACE::WSeat *seat); @@ -155,6 +162,12 @@ private Q_SLOTS: // Unified parent/container update for Xdg & XWayland toplevel wrappers. void updateWrapperContainer(SurfaceWrapper *wrapper, WAYLIB_SERVER_NAMESPACE::WSurface *parentSurface); + // Async X11 property fetch for XWayland surfaces + void fetchInitialProperties(WAYLIB_SERVER_NAMESPACE::WXWaylandSurface *surface, + const QString &appId); + void onInitialPropertiesReady(WAYLIB_SERVER_NAMESPACE::WXWaylandSurface *surface, + const QString &appId, + const QMap &result); WAYLIB_SERVER_NAMESPACE::WXdgShell *m_xdgShell = nullptr; WAYLIB_SERVER_NAMESPACE::WLayerShell *m_layerShell = nullptr; @@ -175,6 +188,7 @@ private Q_SLOTS: // FIXME: https://github.com/linuxdeepin/treeland/pull/428 Caused damage to the tooltip // Need to find a better way to handle popup click events SurfaceContainer *m_popupContainer = nullptr; + IMCandidatePanelManager *m_imCandidatePanelManager = nullptr; QObject *m_windowMenu = nullptr; // Prelaunch wrappers created before binding to a real shell surface QList m_prelaunchWrappers; diff --git a/src/output/output.cpp b/src/output/output.cpp index 1d2ba7166..bf1be4f4b 100644 --- a/src/output/output.cpp +++ b/src/output/output.cpp @@ -660,6 +660,9 @@ void Output::arrangeLayerSurfaces() void Output::arrangeNonLayerSurface(SurfaceWrapper *surface, const QSizeF &sizeDiff) { Q_ASSERT(surface->type() != SurfaceWrapper::Type::Layer); + if (surface->isIMCandidatePanel()) + return; + surface->setFullscreenGeometry(geometry()); const auto validGeo = this->validGeometry(); surface->setMaximizedGeometry(validGeo); @@ -905,8 +908,7 @@ void Output::clearPopupCache(SurfaceWrapper *surface) return; } - if (surface->type() == SurfaceWrapper::Type::XdgPopup || - surface->type() == SurfaceWrapper::Type::InputPopup) { + if (surface->type() == SurfaceWrapper::Type::XdgPopup || surface->isInputPopupLike()) { m_positionCache.remove(surface); } } @@ -932,7 +934,7 @@ void Output::arrangePopupSurface(SurfaceWrapper *surface) if (surface->type() == SurfaceWrapper::Type::XdgPopup) { auto *outputAtCursor = Helper::instance()->getOutputAtCursor(); targetOutput = outputAtCursor ? outputAtCursor->outputItem() : nullptr; - } else if (surface->type() == SurfaceWrapper::Type::InputPopup) { + } else if (surface->isInputPopupLike()) { auto *parentOutput = parentSurfaceWrapper->ownsOutput(); targetOutput = parentOutput ? parentOutput->outputItem() : nullptr; } @@ -962,7 +964,7 @@ void Output::arrangeNonLayerSurfaces() for (SurfaceWrapper *surface : std::as_const(surfaces())) { if (surface->type() == SurfaceWrapper::Type::Layer - || surface->type() == SurfaceWrapper::Type::LockScreen + || surface->type() == SurfaceWrapper::Type::LockScreen || surface->isInputPopupLike() || !surface->hasInitializeContainer()) continue; arrangeNonLayerSurface(surface, sizeDiff); diff --git a/src/output/output.h b/src/output/output.h index 46ae74ca2..b0bfa5fb2 100644 --- a/src/output/output.h +++ b/src/output/output.h @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 UnionTech Software Technology Co., Ltd. +// Copyright (C) 2024-2026 UnionTech Software Technology Co., Ltd. // SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #pragma once @@ -87,6 +87,10 @@ class Output : public SurfaceListModel OutputConfig* config() const; + void adjustToOutputBounds(QPointF &pos, + const QRectF &normalGeo, + const QRectF &outputRect) const; + Q_SIGNALS: void exclusiveZoneChanged(); void moveResizeFinised(); @@ -130,7 +134,6 @@ public Q_SLOTS: qreal preferredScaleFactor() const; QPointF calculateBasePosition(SurfaceWrapper *surface, const QPointF &dPos) const; - void adjustToOutputBounds(QPointF &pos, const QRectF &normalGeo, const QRectF &outputRect) const; void handleLayerShellPopup(SurfaceWrapper *surface, const QRectF &normalGeo); void handleRegularPopup(SurfaceWrapper *surface, const QRectF &normalGeo, bool isSubMenu, WOutputItem *targetOutput); void clearPopupCache(SurfaceWrapper *surface); diff --git a/src/seat/helper.cpp b/src/seat/helper.cpp index e052367aa..369aed990 100644 --- a/src/seat/helper.cpp +++ b/src/seat/helper.cpp @@ -1094,6 +1094,9 @@ void Helper::onRestoreCopyOutput(VirtualOutputInterfaceV1 *interface) void Helper::onSurfaceWrapperAdded(SurfaceWrapper *wrapper) { + if (wrapper->isIMCandidatePanel()) + return; + bool isXdgToplevel = wrapper->type() == SurfaceWrapper::Type::XdgToplevel; bool isXdgPopup = wrapper->type() == SurfaceWrapper::Type::XdgPopup; bool isXwayland = wrapper->type() == SurfaceWrapper::Type::XWayland; @@ -1253,6 +1256,9 @@ void Helper::onSurfaceWrapperAdded(SurfaceWrapper *wrapper) void Helper::onSurfaceWrapperAboutToRemove(SurfaceWrapper *wrapper) { + if (wrapper->isIMCandidatePanel()) + return; + if (!wrapper->skipDockPreView()) { m_foreignToplevel->removeSurface(wrapper->shellSurface()); m_extForeignToplevelListV1->removeSurface(wrapper->shellSurface()); @@ -1716,6 +1722,9 @@ WSeat *Helper::getSeatForEvent(QInputEvent *event) const void Helper::activateSurface(SurfaceWrapper *wrapper, Qt::FocusReason reason) { + if (wrapper && wrapper->isIMCandidatePanel()) + return; + WSeat *interactingSeat = m_currentEventSeat ? m_currentEventSeat : getLastInteractingSeat(wrapper); if (m_blockActivateSurface && wrapper && wrapper->type() != SurfaceWrapper::Type::LockScreen) { auto sh = wrapper->shellSurface(); @@ -2003,6 +2012,11 @@ bool Helper::afterHandleEvent([[maybe_unused]] WSeat *seat, WSeat *eventSeat = getSeatForEvent(event); if (eventSeat && surface) { + // IM candidate panel should not be activated. + if (surface->isIMCandidatePanel()) { + return false; + } + const auto seats = m_seatManager->seats(); for (auto *seat : seats) { if (seat != eventSeat && surface) { diff --git a/src/surface/surfacewrapper.cpp b/src/surface/surfacewrapper.cpp index 4033dcbc0..5b77b72d6 100644 --- a/src/surface/surfacewrapper.cpp +++ b/src/surface/surfacewrapper.cpp @@ -63,6 +63,7 @@ SurfaceWrapper::SurfaceWrapper(QmlEngine *qmlEngine, , m_blur(false) , m_isActivated(false) , m_attention(false) + , m_isIMCandidatePanel(false) , m_resizable(false) , m_maximizable(false) , m_appId(appId) @@ -98,6 +99,7 @@ SurfaceWrapper::SurfaceWrapper(SurfaceWrapper *original, QQuickItem *parent) , m_blur(false) , m_isActivated(false) , m_attention(false) + , m_isIMCandidatePanel(false) , m_resizable(false) , m_maximizable(false) , m_appId(original->m_appId) @@ -166,6 +168,7 @@ SurfaceWrapper::SurfaceWrapper(QmlEngine *qmlEngine, , m_blur(false) , m_isActivated(false) , m_attention(false) + , m_isIMCandidatePanel(false) , m_resizable(false) , m_maximizable(false) , m_appId(appId) @@ -1096,6 +1099,24 @@ bool SurfaceWrapper::setAttention(bool attention) return true; } +bool SurfaceWrapper::isInputPopupLike() const +{ + return m_type == Type::InputPopup || m_isIMCandidatePanel; +} + +bool SurfaceWrapper::isIMCandidatePanel() const +{ + return m_isIMCandidatePanel; +} + +void SurfaceWrapper::setIMCandidatePanel(bool isIMCandidatePanel) +{ + if (m_isIMCandidatePanel == isIMCandidatePanel) + return; + m_isIMCandidatePanel = isIMCandidatePanel; + Q_EMIT isIMCandidatePanelChanged(); +} + void SurfaceWrapper::setNoDecoration(bool newNoDecoration) { if (m_wrapperAboutToRemove) @@ -1594,7 +1615,7 @@ void SurfaceWrapper::startShowDesktopAnimation(bool show) qreal SurfaceWrapper::radius() const { - if (m_type == Type::InputPopup) + if (isInputPopupLike()) return 0; if (m_type == Type::XdgPopup) return 8; @@ -1999,8 +2020,7 @@ void SurfaceWrapper::setAlwaysOnTop(bool alwaysOnTop) bool SurfaceWrapper::showOnAllWorkspace() const { - if (m_type == Type::Layer || m_type == Type::XdgPopup || m_type == Type::InputPopup) - [[unlikely]] + if (m_type == Type::Layer || m_type == Type::XdgPopup || isInputPopupLike()) [[unlikely]] return true; return m_workspaceId == Workspace::ShowOnAllWorkspaceId; } diff --git a/src/surface/surfacewrapper.h b/src/surface/surfacewrapper.h index facf14c16..c536376e6 100644 --- a/src/surface/surfacewrapper.h +++ b/src/surface/surfacewrapper.h @@ -81,6 +81,7 @@ class SurfaceWrapper : public QQuickItem Q_PROPERTY(bool coverEnabled READ coverEnabled NOTIFY coverEnabledChanged FINAL) Q_PROPERTY(bool acceptKeyboardFocus READ acceptKeyboardFocus NOTIFY acceptKeyboardFocusChanged FINAL) Q_PROPERTY(bool isActivated READ isActivated NOTIFY isActivatedChanged FINAL) + Q_PROPERTY(bool isIMCandidatePanel READ isIMCandidatePanel NOTIFY isIMCandidatePanelChanged FINAL) Q_PROPERTY(bool isResizable READ isResizable NOTIFY resizableChanged FINAL) Q_PROPERTY(bool isMaximizable READ isMaximizable NOTIFY maximizableChanged FINAL) @@ -284,6 +285,9 @@ class SurfaceWrapper : public QQuickItem void setAcceptKeyboardFocus(bool accept); bool isActivated() const; + bool isIMCandidatePanel() const; + void setIMCandidatePanel(bool isIMCandidatePanel); + bool isInputPopupLike() const; bool attention() const; bool setAttention(bool attention); @@ -349,6 +353,7 @@ public Q_SLOTS: void aboutToBeInvalidated(); void acceptKeyboardFocusChanged(); void isActivatedChanged(); + void isIMCandidatePanelChanged(); void resizableChanged(); void maximizableChanged(); void attentionChanged(); @@ -473,6 +478,7 @@ public Q_SLOTS: uint m_blur : 1; uint m_isActivated : 1; uint m_attention : 1; + uint m_isIMCandidatePanel : 1; uint m_resizable : 1; uint m_maximizable : 1; SurfaceRole m_surfaceRole = SurfaceRole::Normal; diff --git a/waylib/src/server/protocols/winputmethodhelper.cpp b/waylib/src/server/protocols/winputmethodhelper.cpp index fee0d8646..94b625c53 100644 --- a/waylib/src/server/protocols/winputmethodhelper.cpp +++ b/waylib/src/server/protocols/winputmethodhelper.cpp @@ -224,6 +224,18 @@ WInputMethodV2 *WInputMethodHelper::inputMethod() const return d->activeInputMethod; } +WSurface *WInputMethodHelper::textInputFocusSurface() const +{ + auto ti = enabledTextInput(); + return ti ? ti->focusedSurface() : nullptr; +} + +QRect WInputMethodHelper::textInputCursorRect() const +{ + auto ti = enabledTextInput(); + return ti ? ti->cursorRect() : QRect(); +} + void WInputMethodHelper::setInputMethod(WInputMethodV2 *im) { W_D(WInputMethodHelper); @@ -464,6 +476,7 @@ void WInputMethodHelper::handleFocusedTICommitted() im->sendDone(); } updateAllPopupSurfaces(ti->cursorRect()); + Q_EMIT textInputCursorRectChanged(ti->cursorRect()); } void WInputMethodHelper::handleIMCommitted() diff --git a/waylib/src/server/protocols/winputmethodhelper.h b/waylib/src/server/protocols/winputmethodhelper.h index e6cbdff51..c3c28041c 100644 --- a/waylib/src/server/protocols/winputmethodhelper.h +++ b/waylib/src/server/protocols/winputmethodhelper.h @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Yixue Wang . +// Copyright (C) 2023-2026 Yixue Wang . // SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #pragma once @@ -28,6 +28,7 @@ class WInputMethodV2; class WInputMethodHelperPrivate; class WInputPopupSurface; class WTextInput; +class WSurface; class WAYLIB_SERVER_EXPORT WInputMethodHelper : public QObject, public WObject { Q_OBJECT @@ -37,9 +38,13 @@ class WAYLIB_SERVER_EXPORT WInputMethodHelper : public QObject, public WObject explicit WInputMethodHelper(WServer *server, WSeat *seat); ~WInputMethodHelper() override; + WSurface *textInputFocusSurface() const; + QRect textInputCursorRect() const; + Q_SIGNALS: void inputPopupSurfaceV2Added(WInputPopupSurface *popupSurface); void inputPopupSurfaceV2Removed(WInputPopupSurface *popupSurface); + void textInputCursorRectChanged(QRect cursorRect); private: const QList &virtualKeyboards() const; diff --git a/waylib/src/server/protocols/wxwayland.cpp b/waylib/src/server/protocols/wxwayland.cpp index d0964653b..6922560c2 100644 --- a/waylib/src/server/protocols/wxwayland.cpp +++ b/waylib/src/server/protocols/wxwayland.cpp @@ -1,25 +1,113 @@ -// Copyright (C) 2023 JiDe Zhang . +// Copyright (C) 2023-2026 JiDe Zhang . // SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "wxwayland.h" -#include "wxwaylandsurface.h" + +#include "private/wglobal_p.h" #include "wseat.h" #include "wsocket.h" -#include "private/wglobal_p.h" +#include "wxwaylandsurface.h" + +#include +#include +#include +#include #include #include #include -#include #include -#include -#include +#include -#include +#include +#include + +#include QW_USE_NAMESPACE WAYLIB_SERVER_BEGIN_NAMESPACE +bool xwayland_user_event_handler(wlr_xwayland *xwayland, xcb_generic_event_t *event); + +// --- Xcb::Property --- + +Xcb::Property::Property() = default; + +Xcb::Property::Property(xcb_connection_t *conn, + xcb_window_t win, + xcb_atom_t prop, + xcb_atom_t type, + uint32_t offset, + uint32_t length) + : m_conn(conn) + , m_win(win) + , m_type(type) + , m_offset(offset) + , m_length(length) + , m_cookie(xcb_get_property_unchecked(conn, false, win, prop, type, offset, length)) +{ +} + +Xcb::Property::~Property() +{ + if (!m_retrieved && m_cookie.sequence) + xcb_discard_reply(m_conn, m_cookie.sequence); + else if (m_reply) + free(m_reply); +} + +Xcb::Property::Property(Property &&other) noexcept + : m_conn(other.m_conn) + , m_win(other.m_win) + , m_type(other.m_type) + , m_cookie(other.m_cookie) + , m_reply(other.m_reply) +{ + other.m_retrieved = true; + other.m_reply = nullptr; + other.m_cookie = {}; +} + +Xcb::Property &Xcb::Property::operator=(Property &&other) noexcept +{ + if (this != &other) { + if (!m_retrieved && m_cookie.sequence) + xcb_discard_reply(m_conn, m_cookie.sequence); + else if (m_reply) + free(m_reply); + + m_conn = other.m_conn; + m_win = other.m_win; + m_type = other.m_type; + m_cookie = other.m_cookie; + m_reply = other.m_reply; + + other.m_retrieved = true; + other.m_reply = nullptr; + other.m_cookie = {}; + } + return *this; +} + +void Xcb::Property::getReply() const +{ + if (m_retrieved || !m_cookie.sequence) + return; + m_reply = xcb_get_property_reply(m_conn, m_cookie, nullptr); + m_retrieved = true; +} + +QByteArray Xcb::Property::toByteArray() const +{ + getReply(); + if (!m_reply || m_reply->type != m_type || m_reply->value_len == 0) + return QByteArray(); + return QByteArray(static_cast(xcb_get_property_value(m_reply)), + xcb_get_property_value_length(m_reply)); +} + +// --- WXWayland --- + class Q_DECL_HIDDEN WXWaylandPrivate : public WWrapObjectPrivate { public: @@ -45,6 +133,22 @@ class Q_DECL_HIDDEN WXWaylandPrivate : public WWrapObjectPrivate void on_surface_destroy(qw_xwayland_surface *xwl_surface); // end slot function + // Async property reading + struct PerWindowProps + { + QVector requests; + QMap results; + QVector cookies; + std::function &)> callback; + QTimer *timer = nullptr; + bool propNotifySeen = false; + }; + + QMap asyncProps; + + void xcbPollReplies(); + void xcbAsyncTimeoutForWindow(xcb_window_t windowId); + W_DECLARE_PUBLIC(WXWayland) xcb_screen_t *screen = nullptr; @@ -61,6 +165,61 @@ class Q_DECL_HIDDEN WXWaylandPrivate : public WWrapObjectPrivate void instantRelease() override; }; +bool xwayland_user_event_handler(wlr_xwayland *xwayland, xcb_generic_event_t *event) +{ + if (!event) + return false; + + const uint8_t response_type = event->response_type & ~0x80; + if (response_type != XCB_PROPERTY_NOTIFY) + return false; + + auto *pe = reinterpret_cast(event); + auto *self = WXWayland::fromHandle(xwayland); + + if (!self) + return false; + + auto *d = self->d_func(); + + // Trigger async property reading infrastructure if this window is being tracked. + if (!d->asyncProps.isEmpty()) { + d->xcbPollReplies(); + auto it = d->asyncProps.find(pe->window); + if (it != d->asyncProps.end()) { + auto &props = it.value(); + props.propNotifySeen = true; + // Resend requests on PROPNOTIFY to get the latest value after the change. + xcb_connection_t *conn = self->xcbConnection(); + props.cookies.clear(); + for (const auto &req : std::as_const(props.requests)) { + auto cookie = xcb_get_property_unchecked(conn, + false, + pe->window, + req.atom, + req.type, + 0, + 1024); + props.cookies.append(cookie); + } + xcb_flush(conn); + } + } + + for (auto *surface : self->surfaceList()) { + QPointer sp(surface); + if (!sp) + continue; + if (sp->handle()->handle()->window_id == pe->window) { + Q_EMIT self->windowPropertyChanged(sp, pe->atom); + break; + } + } + + // Do not consume the event; let xcb and wlroots continue normal processing. + return false; +} + void WXWaylandPrivate::init() { W_Q(WXWayland); @@ -249,6 +408,14 @@ xcb_screen_t *WXWayland::xcbScreen() const return d->screen; } +WXWayland *WXWayland::fromHandle(wlr_xwayland *handle) +{ + auto *qw = QW_NAMESPACE::qw_xwayland::from(handle); + if (!qw) + return nullptr; + return qw->get_data(); +} + QVector WXWayland::surfaceList() const { W_DC(WXWayland); @@ -330,6 +497,9 @@ void WXWayland::create(WServer *server) m_handle = handle; d->socket->bind(handle->handle()->server->x_fd[1]); + handle->set_data(this, this); + handle->handle()->user_event_handler = xwayland_user_event_handler; + QObject::connect(handle, &qw_xwayland::notify_new_surface, this, [d] (wlr_xwayland_surface *surface) { d->on_new_surface(surface); }); @@ -349,6 +519,11 @@ void WXWayland::destroy([[maybe_unused]] WServer *server) { W_D(WXWayland); + if (auto handle = this->handle()) { + handle->set_data(nullptr, nullptr); + handle->handle()->user_event_handler = nullptr; + } + auto list = d->surfaceList; d->surfaceList.clear(); d->screen = nullptr; @@ -367,4 +542,169 @@ wl_global *WXWayland::global() const return handle()->handle()->shell_v1->global; } +void WXWayland::readAsyncProperties( + xcb_window_t windowId, + const QVector &requests, + int timeoutMs, + std::function &)> callback) +{ + W_D(WXWayland); + + xcb_connection_t *conn = xcbConnection(); + if (!conn || requests.isEmpty()) { + callback(windowId, QMap{}); + return; + } + + WXWaylandPrivate::PerWindowProps props; + props.requests = requests; + props.callback = std::move(callback); + + props.cookies.reserve(requests.size()); + for (const auto &req : std::as_const(requests)) { + auto cookie = + xcb_get_property_unchecked(conn, false, windowId, req.atom, req.type, 0, 1024); + props.cookies.append(cookie); + } + xcb_flush(conn); + + // Create per-window timer. + props.timer = new QTimer(this); + props.timer->setSingleShot(true); + connect(props.timer, &QTimer::timeout, this, [d, windowId]() { + d->xcbAsyncTimeoutForWindow(windowId); + }); + props.timer->start(timeoutMs); + + d->asyncProps.insert(windowId, std::move(props)); +} + +void WXWaylandPrivate::xcbPollReplies() +{ + W_Q(WXWayland); + + if (asyncProps.isEmpty()) + return; + + xcb_connection_t *conn = q->xcbConnection(); + + QList toRemove; + + for (auto it = asyncProps.begin(); it != asyncProps.end(); ++it) { + xcb_window_t windowId = it.key(); + auto &props = it.value(); + + bool windowDone = true; + for (int i = 0; i < props.cookies.size(); ++i) { + if (props.results.contains(props.requests[i].atom)) + continue; // already got this one + + void *replyPtr = nullptr; + xcb_generic_error_t *err = nullptr; + int ret = xcb_poll_for_reply64(conn, props.cookies[i].sequence, &replyPtr, &err); + + if (ret == 0) { + windowDone = false; + continue; + } + + if (ret == 1 && replyPtr) { + auto *reply = static_cast(replyPtr); + if (reply->type != 0 && reply->value_len > 0) { + QByteArray data(static_cast(xcb_get_property_value(reply)), + xcb_get_property_value_length(reply)); + props.results.insert(props.requests[i].atom, data); + } + free(reply); + } else if (err) { + free(err); + } + } + + // Fire callback when all replies received AND propNotifySeen. + if (windowDone && props.propNotifySeen) { + auto resultCopy = props.results; + props.callback(windowId, resultCopy); + + toRemove.append(windowId); + } + } + + // Cleanup completed windows. + for (auto windowId : std::as_const(toRemove)) { + auto it = asyncProps.find(windowId); + if (it != asyncProps.end()) { + if (it->timer) { + it->timer->stop(); + delete it->timer; + } + asyncProps.erase(it); + } + } +} + +void WXWaylandPrivate::xcbAsyncTimeoutForWindow(xcb_window_t windowId) +{ + W_Q(WXWayland); + + auto it = asyncProps.find(windowId); + if (it == asyncProps.end()) + return; + + auto &props = it.value(); + + xcb_connection_t *conn = q->xcbConnection(); + if (conn) { + // Drain remaining replies. + for (int i = 0; i < props.cookies.size(); ++i) { + if (props.results.contains(props.requests[i].atom)) + continue; + void *replyPtr = nullptr; + xcb_generic_error_t *err = nullptr; + int ret = xcb_poll_for_reply64(conn, props.cookies[i].sequence, &replyPtr, &err); + if (ret == 1 && replyPtr) { + auto *reply = static_cast(replyPtr); + if (reply->type != 0 && reply->value_len > 0) { + QByteArray data(static_cast(xcb_get_property_value(reply)), + xcb_get_property_value_length(reply)); + props.results.insert(props.requests[i].atom, data); + } + free(reply); + } else { + if (replyPtr) + free(replyPtr); + if (err) + free(err); + } + } + } + + auto resultCopy = props.results; + props.callback(windowId, resultCopy); + + // Cleanup. + if (props.timer) { + delete props.timer; + } + asyncProps.erase(it); +} + +void WXWayland::cancelAsyncProperties(xcb_window_t windowId) +{ + W_D(WXWayland); + auto it = d->asyncProps.find(windowId); + if (it == d->asyncProps.end()) + return; + + if (it->timer) { + delete it->timer; + } + d->asyncProps.erase(it); +} + +bool WXWayland::event(QEvent *ev) +{ + return QObject::event(ev); +} + WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/protocols/wxwayland.h b/waylib/src/server/protocols/wxwayland.h index 9db7e509d..a3c8d01e1 100644 --- a/waylib/src/server/protocols/wxwayland.h +++ b/waylib/src/server/protocols/wxwayland.h @@ -1,24 +1,67 @@ -// Copyright (C) 2023 JiDe Zhang . +// Copyright (C) 2023-2026 JiDe Zhang . // SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #pragma once +#include + #include +#include +#include +#include + +#include + QW_BEGIN_NAMESPACE class qw_xwayland; class qw_compositor; QW_END_NAMESPACE -struct xcb_connection_t; +struct wlr_xwayland; struct xcb_screen_t; -typedef uint32_t xcb_atom_t; WAYLIB_SERVER_BEGIN_NAMESPACE class WSeat; class WXWaylandSurface; class WXWaylandPrivate; + +namespace Xcb { + +class Property +{ +public: + Property(xcb_connection_t *conn, + xcb_window_t win, + xcb_atom_t prop, + xcb_atom_t type, + uint32_t offset = 0, + uint32_t length = 1024); + Property(); + ~Property(); + Property(const Property &) = delete; + Property &operator=(const Property &) = delete; + Property(Property &&other) noexcept; + Property &operator=(Property &&other) noexcept; + + QByteArray toByteArray() const; + +private: + void getReply() const; + + xcb_connection_t *m_conn = nullptr; + xcb_window_t m_win = XCB_WINDOW_NONE; + xcb_atom_t m_type = XCB_ATOM_NONE; + uint32_t m_offset = 0; + uint32_t m_length = 0; + mutable bool m_retrieved = false; + mutable xcb_get_property_cookie_t m_cookie = {}; + mutable xcb_get_property_reply_t *m_reply = nullptr; +}; + +} // namespace Xcb + class WAYLIB_SERVER_EXPORT WXWayland : public WWrapObject, public WServerInterface { Q_OBJECT @@ -41,6 +84,8 @@ class WAYLIB_SERVER_EXPORT WXWayland : public WWrapObject, public WServerInterfa }; Q_ENUM(XcbAtom) + static WXWayland *fromHandle(wlr_xwayland *handle); + WXWayland(QW_NAMESPACE::qw_compositor *compositor, bool lazy = true); inline QW_NAMESPACE::qw_xwayland *handle() const { @@ -75,6 +120,7 @@ class WAYLIB_SERVER_EXPORT WXWayland : public WWrapObject, public WServerInterfa void surfaceRemoved(WXWaylandSurface *surface); void toplevelAdded(WXWaylandSurface *surface); void toplevelRemoved(WXWaylandSurface *surface); + void windowPropertyChanged(WXWaylandSurface *surface, xcb_atom_t atom); protected: void addSurface(WXWaylandSurface *surface); @@ -86,6 +132,32 @@ class WAYLIB_SERVER_EXPORT WXWayland : public WWrapObject, public WServerInterfa void create(WServer *server) override; void destroy(WServer *server) override; wl_global *global() const override; + +protected: + bool event(QEvent *ev) override; + +public: + struct AsyncPropRequest + { + xcb_atom_t atom = XCB_ATOM_NONE; + xcb_atom_t type = XCB_ATOM_ANY; + }; + + // Read properties for windowId asynchronously. + // - Fires callback(result) when all replies arrive or timeoutMs elapses. + // - If a specific property was never requested, it won't be in the result map. + void readAsyncProperties( + xcb_window_t windowId, + const QVector &requests, + int timeoutMs, + std::function &)> callback); + + // Cancel pending async property read for windowId. + // Call this when the window/surface is being destroyed. + void cancelAsyncProperties(xcb_window_t windowId); + +private: + friend bool xwayland_user_event_handler(wlr_xwayland *, xcb_generic_event_t *); }; WAYLIB_SERVER_END_NAMESPACE