diff --git a/autostart/CMakeLists.txt b/autostart/CMakeLists.txt index 098103168..f56d282ff 100644 --- a/autostart/CMakeLists.txt +++ b/autostart/CMakeLists.txt @@ -1,9 +1,9 @@ -file(GLOB DESKTOP_FILES_IN *.desktop.in) +set(AUTOSTART_DESKTOP_FILES_IN lxqt-panel.desktop.in) # Translations ********************************** lxqt_translate_desktop(DESKTOP_FILES SOURCES - ${DESKTOP_FILES_IN} + ${AUTOSTART_DESKTOP_FILES_IN} USE_YAML ) add_custom_target(lxqt_panel_autostart_desktop_files ALL DEPENDS ${DESKTOP_FILES}) @@ -14,3 +14,12 @@ install(FILES DESTINATION "${LXQT_ETC_XDG_DIR}/autostart" COMPONENT Runtime ) + +configure_file(lxqt-panel_wayland.desktop.in lxqt-panel_wayland.desktop @ONLY) + +install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/lxqt-panel_wayland.desktop" + DESTINATION "/usr/share/applications" + RENAME "lxqt-panel.desktop" + COMPONENT Runtime +) diff --git a/autostart/lxqt-panel_wayland.desktop b/autostart/lxqt-panel_wayland.desktop new file mode 100644 index 000000000..5b03e4120 --- /dev/null +++ b/autostart/lxqt-panel_wayland.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Type=Application +TryExec=lxqt-panel + +# NOTE: KWin wants absolute path here, make sure it's correct +Exec=/usr/local/bin/lxqt-panel + +# NOTE: adding KDE to make it work under Plasma Wayland session +OnlyShowIn=LXQt;KDE +X-LXQt-Module=true + +# Make KWin recognize us as priviledged client +X-KDE-Wayland-Interfaces=org_kde_plasma_window_management diff --git a/autostart/lxqt-panel_wayland.desktop.in b/autostart/lxqt-panel_wayland.desktop.in new file mode 100644 index 000000000..540955e18 --- /dev/null +++ b/autostart/lxqt-panel_wayland.desktop.in @@ -0,0 +1,14 @@ +[Desktop Entry] +Type=Application +TryExec=lxqt-panel +NoDisplay=true + +# NOTE: KWin wants absolute path here, get it from CMake install path +Exec=@CMAKE_INSTALL_PREFIX@/@CMAKE_INSTALL_BINDIR@/lxqt-panel + +# NOTE: adding KDE to make it work under Plasma Wayland session +OnlyShowIn=LXQt;KDE +X-LXQt-Module=true + +# Make KWin recognize us as priviledged client +X-KDE-Wayland-Interfaces=org_kde_plasma_window_management diff --git a/panel/CMakeLists.txt b/panel/CMakeLists.txt index 074c62af3..c08b1c726 100644 --- a/panel/CMakeLists.txt +++ b/panel/CMakeLists.txt @@ -1,8 +1,16 @@ set(PROJECT lxqt-panel) -# TODO +# Window Manager abstraction backend add_subdirectory(backends) +# TODO: allow compile time selection via CMake variables +set(PANEL_BACKENDS + lxqt-panel-backend-wlroots + lxqt-panel-backend-wayland + lxqt-panel-backend-xcb +) + + set(PRIV_HEADERS panelpluginsmodel.h windownotifier.h @@ -21,12 +29,6 @@ set(PRIV_HEADERS config/configstyling.h config/configpluginswidget.h config/addplugindialog.h - - backends/ilxqttaskbarabstractbackend.h - backends/lxqttaskbartypes.h - - backends/lxqttaskbardummybackend.h - backends/xcb/lxqttaskbarbackend_x11.h ) # using LXQt namespace in the public headers. @@ -35,9 +37,6 @@ set(PUB_HEADERS pluginsettings.h ilxqtpanelplugin.h ilxqtpanel.h - - backends/ilxqttaskbarabstractbackend.h - backends/lxqttaskbartypes.h ) set(SOURCES @@ -57,11 +56,6 @@ set(SOURCES config/configstyling.cpp config/configpluginswidget.cpp config/addplugindialog.cpp - - backends/ilxqttaskbarabstractbackend.cpp - - backends/lxqttaskbardummybackend.cpp - backends/xcb/lxqttaskbarbackend_x11.cpp ) set(UI @@ -120,6 +114,7 @@ target_link_libraries(${PROJECT} ${LIBRARIES} ${QTX_LIBRARIES} KF6::WindowSystem + ${PANEL_BACKENDS} LayerShellQt::Interface ${STATIC_PLUGINS} ) diff --git a/panel/backends/CMakeLists.txt b/panel/backends/CMakeLists.txt index 8f34a3c67..c369eb81f 100644 --- a/panel/backends/CMakeLists.txt +++ b/panel/backends/CMakeLists.txt @@ -1 +1,19 @@ +# Common interface for Window Manager abstraction backend +# This also contains dummy backend + +add_library(lxqt-panel-backend-common STATIC + + lxqttaskbartypes.h + ilxqttaskbarabstractbackend.h + ilxqttaskbarabstractbackend.cpp + + lxqttaskbardummybackend.h + lxqttaskbardummybackend.cpp +) + +target_link_libraries(lxqt-panel-backend-common + Qt6::Gui +) + +add_subdirectory(wayland) add_subdirectory(xcb) diff --git a/panel/backends/ilxqttaskbarabstractbackend.cpp b/panel/backends/ilxqttaskbarabstractbackend.cpp index 137728263..696266e3b 100644 --- a/panel/backends/ilxqttaskbarabstractbackend.cpp +++ b/panel/backends/ilxqttaskbarabstractbackend.cpp @@ -1,4 +1,31 @@ -#include "../panel/backends/ilxqttaskbarabstractbackend.h" +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2023 LXQt team + * Authors: + * Filippo Gentile + * + * This program or library is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * END_COMMON_COPYRIGHT_HEADER */ + +#include "ilxqttaskbarabstractbackend.h" ILXQtTaskbarAbstractBackend::ILXQtTaskbarAbstractBackend(QObject *parent) diff --git a/panel/backends/ilxqttaskbarabstractbackend.h b/panel/backends/ilxqttaskbarabstractbackend.h index 44840671a..dbceaff71 100644 --- a/panel/backends/ilxqttaskbarabstractbackend.h +++ b/panel/backends/ilxqttaskbarabstractbackend.h @@ -1,3 +1,31 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2023 LXQt team + * Authors: + * Filippo Gentile + * + * This program or library is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * END_COMMON_COPYRIGHT_HEADER */ + + #ifndef ILXQTTASKBARABSTRACTBACKEND_H #define ILXQTTASKBARABSTRACTBACKEND_H diff --git a/panel/backends/lxqttaskbardummybackend.cpp b/panel/backends/lxqttaskbardummybackend.cpp index 15e7e1149..05bbfb0ff 100644 --- a/panel/backends/lxqttaskbardummybackend.cpp +++ b/panel/backends/lxqttaskbardummybackend.cpp @@ -1,3 +1,31 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2023 LXQt team + * Authors: + * Filippo Gentile + * + * This program or library is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * END_COMMON_COPYRIGHT_HEADER */ + + #include "lxqttaskbardummybackend.h" #include diff --git a/panel/backends/lxqttaskbardummybackend.h b/panel/backends/lxqttaskbardummybackend.h index 15506838f..64dce2b11 100644 --- a/panel/backends/lxqttaskbardummybackend.h +++ b/panel/backends/lxqttaskbardummybackend.h @@ -1,3 +1,31 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2023 LXQt team + * Authors: + * Filippo Gentile + * + * This program or library is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * END_COMMON_COPYRIGHT_HEADER */ + + #ifndef LXQTTASKBARDUMMYBACKEND_H #define LXQTTASKBARDUMMYBACKEND_H diff --git a/panel/backends/lxqttaskbartypes.h b/panel/backends/lxqttaskbartypes.h index 656591fbc..e821b410d 100644 --- a/panel/backends/lxqttaskbartypes.h +++ b/panel/backends/lxqttaskbartypes.h @@ -1,3 +1,31 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2023 LXQt team + * Authors: + * Filippo Gentile + * + * This program or library is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * END_COMMON_COPYRIGHT_HEADER */ + + #ifndef LXQTTASKBARTYPES_H #define LXQTTASKBARTYPES_H @@ -50,7 +78,7 @@ enum class LXQtTaskBarWindowLayer enum class LXQtTaskBarWorkspace { - ShowOnAll = -1 + ShowOnAll = 0 // Virtual destops have 1-based indexes }; #endif // LXQTTASKBARTYPES_H diff --git a/panel/backends/wayland/CMakeLists.txt b/panel/backends/wayland/CMakeLists.txt new file mode 100644 index 000000000..34432725a --- /dev/null +++ b/panel/backends/wayland/CMakeLists.txt @@ -0,0 +1,30 @@ +project(lxqt-panel-backend-wayland) + +find_package(Qt6 ${REQUIRED_QT_VERSION} REQUIRED COMPONENTS WaylandClient Concurrent) + +add_library(lxqt-panel-backend-wayland STATIC + lxqttaskbarbackendwayland.h + lxqttaskbarplasmawindowmanagment.h + lxqtplasmavirtualdesktop.h + + lxqttaskbarbackendwayland.cpp + lxqttaskbarplasmawindowmanagment.cpp + lxqtplasmavirtualdesktop.cpp +) + +qt6_generate_wayland_protocol_client_sources(lxqt-panel-backend-wayland +FILES + ${CMAKE_CURRENT_SOURCE_DIR}/protocols/plasma-window-management.xml +) + +qt6_generate_wayland_protocol_client_sources(lxqt-panel-backend-wayland +FILES + ${CMAKE_CURRENT_SOURCE_DIR}/protocols/org-kde-plasma-virtual-desktop.xml +) + +target_link_libraries(lxqt-panel-backend-wayland + Qt6::GuiPrivate + Qt6::WaylandClient + Qt6::Concurrent + lxqt-panel-backend-common +) diff --git a/panel/backends/wayland/lxqtplasmavirtualdesktop.cpp b/panel/backends/wayland/lxqtplasmavirtualdesktop.cpp new file mode 100644 index 000000000..d26574e62 --- /dev/null +++ b/panel/backends/wayland/lxqtplasmavirtualdesktop.cpp @@ -0,0 +1,258 @@ +/* + SPDX-FileCopyrightText: 2016 Eike Hein + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + + Adapted from KDE Plasma Workspace: plasma-workspace/libtaskmanager/waylandtasksmodel.cpp +*/ + + +#include "lxqtplasmavirtualdesktop.h" + +#include + +LXQtPlasmaVirtualDesktop::LXQtPlasmaVirtualDesktop(::org_kde_plasma_virtual_desktop *object, const QString &id) + : org_kde_plasma_virtual_desktop(object) + , id(id) +{ +} + +LXQtPlasmaVirtualDesktop::~LXQtPlasmaVirtualDesktop() +{ + wl_proxy_destroy(reinterpret_cast(object())); +} + +void LXQtPlasmaVirtualDesktop::org_kde_plasma_virtual_desktop_name(const QString &name) +{ + this->name = name; + Q_EMIT nameChanged(); +} + +void LXQtPlasmaVirtualDesktop::org_kde_plasma_virtual_desktop_done() +{ + Q_EMIT done(); +} + +void LXQtPlasmaVirtualDesktop::org_kde_plasma_virtual_desktop_activated() +{ + Q_EMIT activated(); +} + +LXQtPlasmaVirtualDesktopManagment::LXQtPlasmaVirtualDesktopManagment() + : QWaylandClientExtensionTemplate(version) +{ + connect(this, &QWaylandClientExtension::activeChanged, this, [this] { + if (!isActive()) { + org_kde_plasma_virtual_desktop_management_destroy(object()); + } + }); +} + +LXQtPlasmaVirtualDesktopManagment::~LXQtPlasmaVirtualDesktopManagment() +{ + if (isActive()) { + org_kde_plasma_virtual_desktop_management_destroy(object()); + } +} + +void LXQtPlasmaVirtualDesktopManagment::org_kde_plasma_virtual_desktop_management_desktop_created(const QString &desktop_id, uint32_t position) +{ + emit desktopCreated(desktop_id, position); +} + +void LXQtPlasmaVirtualDesktopManagment::org_kde_plasma_virtual_desktop_management_desktop_removed(const QString &desktop_id) +{ + emit desktopRemoved(desktop_id); +} + +void LXQtPlasmaVirtualDesktopManagment::org_kde_plasma_virtual_desktop_management_rows(uint32_t rows) +{ + emit rowsChanged(rows); +} + +LXQtPlasmaWaylandWorkspaceInfo::LXQtPlasmaWaylandWorkspaceInfo() +{ + init(); +} + +LXQtPlasmaWaylandWorkspaceInfo::VirtualDesktopsIterator LXQtPlasmaWaylandWorkspaceInfo::findDesktop(const QString &id) const +{ + return std::find_if(virtualDesktops.begin(), virtualDesktops.end(), + [&id](const std::unique_ptr &desktop) { + return desktop->id == id; + }); +} + +QString LXQtPlasmaWaylandWorkspaceInfo::getDesktopName(int pos) const +{ + if(pos < 0 || size_t(pos) >= virtualDesktops.size()) + return QString(); + return virtualDesktops[pos]->name; +} + +QString LXQtPlasmaWaylandWorkspaceInfo::getDesktopId(int pos) const +{ + if(pos < 0 || size_t(pos) >= virtualDesktops.size()) + return QString(); + return virtualDesktops[pos]->id; +} + +void LXQtPlasmaWaylandWorkspaceInfo::init() +{ + virtualDesktopManagement = std::make_unique(); + + connect(virtualDesktopManagement.get(), &LXQtPlasmaVirtualDesktopManagment::activeChanged, this, [this] { + if (!virtualDesktopManagement->isActive()) { + rows = 0; + virtualDesktops.clear(); + currentVirtualDesktop.clear(); + Q_EMIT currentDesktopChanged(); + Q_EMIT numberOfDesktopsChanged(); + Q_EMIT navigationWrappingAroundChanged(); + Q_EMIT desktopIdsChanged(); + Q_EMIT desktopLayoutRowsChanged(); + } + }); + + connect(virtualDesktopManagement.get(), &LXQtPlasmaVirtualDesktopManagment::desktopCreated, + this, &LXQtPlasmaWaylandWorkspaceInfo::addDesktop); + + connect(virtualDesktopManagement.get(), &LXQtPlasmaVirtualDesktopManagment::desktopRemoved, this, [this](const QString &id) { + + + virtualDesktops.erase(std::remove_if(virtualDesktops.begin(), virtualDesktops.end(), + [id](const std::unique_ptr &desktop) + { + return desktop->id == id; + }), + virtualDesktops.end()); + + Q_EMIT numberOfDesktopsChanged(); + Q_EMIT desktopIdsChanged(); + + if (currentVirtualDesktop == id) { + currentVirtualDesktop.clear(); + Q_EMIT currentDesktopChanged(); + } + }); + + connect(virtualDesktopManagement.get(), &LXQtPlasmaVirtualDesktopManagment::rowsChanged, this, [this](quint32 rows) { + this->rows = rows; + Q_EMIT desktopLayoutRowsChanged(); + }); +} + +void LXQtPlasmaWaylandWorkspaceInfo::addDesktop(const QString &id, quint32 pos) +{ + if (findDesktop(id) != virtualDesktops.end()) { + return; + } + + auto desktop = std::make_unique(virtualDesktopManagement->get_virtual_desktop(id), id); + + connect(desktop.get(), &LXQtPlasmaVirtualDesktop::activated, this, [id, this]() { + currentVirtualDesktop = id; + Q_EMIT currentDesktopChanged(); + }); + + connect(desktop.get(), &LXQtPlasmaVirtualDesktop::nameChanged, this, [id, this]() { + Q_EMIT desktopNameChanged(position(id)); + }); + + connect(desktop.get(), &LXQtPlasmaVirtualDesktop::done, this, [id, this]() { + Q_EMIT desktopNameChanged(position(id)); + }); + + virtualDesktops.insert(std::next(virtualDesktops.begin(), pos), std::move(desktop)); + + Q_EMIT numberOfDesktopsChanged(); + Q_EMIT desktopIdsChanged(); + Q_EMIT desktopNameChanged(position(id)); +} + +QVariant LXQtPlasmaWaylandWorkspaceInfo::currentDesktop() const +{ + return currentVirtualDesktop; +} + +int LXQtPlasmaWaylandWorkspaceInfo::numberOfDesktops() const +{ + return virtualDesktops.size(); +} + +quint32 LXQtPlasmaWaylandWorkspaceInfo::position(const QVariant &desktop) const +{ + return std::distance(virtualDesktops.begin(), findDesktop(desktop.toString())); +} + +QVariantList LXQtPlasmaWaylandWorkspaceInfo::desktopIds() const +{ + QVariantList ids; + ids.reserve(virtualDesktops.size()); + + std::transform(virtualDesktops.cbegin(), virtualDesktops.cend(), std::back_inserter(ids), [](const std::unique_ptr &desktop) { + return desktop->id; + }); + return ids; +} + +QStringList LXQtPlasmaWaylandWorkspaceInfo::desktopNames() const +{ + if (!virtualDesktopManagement->isActive()) { + return QStringList(); + } + QStringList names; + names.reserve(virtualDesktops.size()); + + std::transform(virtualDesktops.cbegin(), virtualDesktops.cend(), std::back_inserter(names), [](const std::unique_ptr &desktop) { + return desktop->name; + }); + return names; +} + +int LXQtPlasmaWaylandWorkspaceInfo::desktopLayoutRows() const +{ + if (!virtualDesktopManagement->isActive()) { + return 0; + } + + return rows; +} + +void LXQtPlasmaWaylandWorkspaceInfo::requestActivate(const QVariant &desktop) +{ + if (!virtualDesktopManagement->isActive()) { + return; + } + + if (auto it = findDesktop(desktop.toString()); it != virtualDesktops.end()) { + (*it)->request_activate(); + } +} + +void LXQtPlasmaWaylandWorkspaceInfo::requestCreateDesktop(quint32 position) +{ + if (!virtualDesktopManagement->isActive()) { + return; + } + + //TODO: translatestd + virtualDesktopManagement->request_create_virtual_desktop(QLatin1String("New Desktop"), position); +} + +void LXQtPlasmaWaylandWorkspaceInfo::requestRemoveDesktop(quint32 position) +{ + if (!virtualDesktopManagement->isActive()) { + return; + } + if (virtualDesktops.size() == 1) { + return; + } + + if (position > (virtualDesktops.size() - 1)) { + return; + } + + virtualDesktopManagement->request_remove_virtual_desktop(virtualDesktops.at(position)->id); +} + diff --git a/panel/backends/wayland/lxqtplasmavirtualdesktop.h b/panel/backends/wayland/lxqtplasmavirtualdesktop.h new file mode 100644 index 000000000..16935be1a --- /dev/null +++ b/panel/backends/wayland/lxqtplasmavirtualdesktop.h @@ -0,0 +1,99 @@ +/* + SPDX-FileCopyrightText: 2016 Eike Hein + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + + Adapted from KDE Plasma Workspace: plasma-workspace/libtaskmanager/waylandtasksmodel.cpp +*/ + +#ifndef LXQTPLASMAVIRTUALDESKTOP_H +#define LXQTPLASMAVIRTUALDESKTOP_H + +#include +#include + +#include + +#include "qwayland-org-kde-plasma-virtual-desktop.h" + +class LXQtPlasmaVirtualDesktop : public QObject, public QtWayland::org_kde_plasma_virtual_desktop +{ + Q_OBJECT +public: + LXQtPlasmaVirtualDesktop(::org_kde_plasma_virtual_desktop *object, const QString &id); + ~LXQtPlasmaVirtualDesktop(); + const QString id; + QString name; +Q_SIGNALS: + void done(); + void activated(); + void nameChanged(); + +protected: + void org_kde_plasma_virtual_desktop_name(const QString &name) override; + void org_kde_plasma_virtual_desktop_done() override; + void org_kde_plasma_virtual_desktop_activated() override; +}; + + +class LXQtPlasmaVirtualDesktopManagment : public QWaylandClientExtensionTemplate, + public QtWayland::org_kde_plasma_virtual_desktop_management +{ + Q_OBJECT +public: + static constexpr int version = 2; + + LXQtPlasmaVirtualDesktopManagment(); + ~LXQtPlasmaVirtualDesktopManagment(); + +signals: + void desktopCreated(const QString &id, quint32 position); + void desktopRemoved(const QString &id); + void rowsChanged(const quint32 rows); + +protected: + virtual void org_kde_plasma_virtual_desktop_management_desktop_created(const QString &desktop_id, uint32_t position) override; + virtual void org_kde_plasma_virtual_desktop_management_desktop_removed(const QString &desktop_id) override; + virtual void org_kde_plasma_virtual_desktop_management_rows(uint32_t rows) override; +}; + +class Q_DECL_HIDDEN LXQtPlasmaWaylandWorkspaceInfo : public QObject +{ + Q_OBJECT +public: + LXQtPlasmaWaylandWorkspaceInfo(); + + QVariant currentVirtualDesktop; + std::vector> virtualDesktops; + std::unique_ptr virtualDesktopManagement; + quint32 rows; + + typedef std::vector>::const_iterator VirtualDesktopsIterator; + + VirtualDesktopsIterator findDesktop(const QString &id) const; + + QString getDesktopName(int pos) const; + QString getDesktopId(int pos) const; + + void init(); + void addDesktop(const QString &id, quint32 pos); + QVariant currentDesktop() const; + int numberOfDesktops() const; + QVariantList desktopIds() const; + QStringList desktopNames() const; + quint32 position(const QVariant &desktop) const; + int desktopLayoutRows() const; + void requestActivate(const QVariant &desktop); + void requestCreateDesktop(quint32 position); + void requestRemoveDesktop(quint32 position); + +signals: + void currentDesktopChanged(); + void numberOfDesktopsChanged(); + void navigationWrappingAroundChanged(); + void desktopIdsChanged(); + void desktopNameChanged(quint32 position); + void desktopLayoutRowsChanged(); +}; + +#endif // LXQTPLASMAVIRTUALDESKTOP_H diff --git a/panel/backends/wayland/lxqttaskbarbackendwayland.cpp b/panel/backends/wayland/lxqttaskbarbackendwayland.cpp new file mode 100644 index 000000000..1d0644284 --- /dev/null +++ b/panel/backends/wayland/lxqttaskbarbackendwayland.cpp @@ -0,0 +1,758 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2023 LXQt team + * Authors: + * Filippo Gentile + * + * This program or library is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * END_COMMON_COPYRIGHT_HEADER */ + +#include "lxqttaskbarbackendwayland.h" + +#include "lxqttaskbarplasmawindowmanagment.h" +#include "lxqtplasmavirtualdesktop.h" + +#include +#include +#include + +auto findWindow(const std::vector>& windows, LXQtTaskBarPlasmaWindow *window) +{ + //TODO: use algorithms + auto end = windows.end(); + for(auto it = windows.begin(); it != end; it++) + { + if((*it).get() == window) + { + return it; + } + } + + return windows.end(); +} + +LXQtTaskbarWaylandBackend::LXQtTaskbarWaylandBackend(QObject *parent) : + ILXQtTaskbarAbstractBackend(parent) +{ + m_managment.reset(new LXQtTaskBarPlasmaWindowManagment); + m_workspaceInfo.reset(new LXQtPlasmaWaylandWorkspaceInfo); + + connect(m_managment.get(), &LXQtTaskBarPlasmaWindowManagment::windowCreated, this, [this](LXQtTaskBarPlasmaWindow *window) { + connect(window, &LXQtTaskBarPlasmaWindow::initialStateDone, this, [this, window] { + addWindow(window); + }); + }); + + // connect(m_managment.get(), &LXQtTaskBarPlasmaWindowManagment::stackingOrderChanged, + // this, [this](const QString &order) { + // // stackingOrder = order.split(QLatin1Char(';')); + // // for (const auto &window : std::as_const(windows)) { + // // this->dataChanged(window.get(), StackingOrder); + // // } + // }); + + connect(m_workspaceInfo.get(), &LXQtPlasmaWaylandWorkspaceInfo::currentDesktopChanged, this, + [this](){ + int idx = m_workspaceInfo->position(m_workspaceInfo->currentDesktop()); + idx += 1; // Make 1-based + emit currentWorkspaceChanged(idx); + }); + + connect(m_workspaceInfo.get(), &LXQtPlasmaWaylandWorkspaceInfo::numberOfDesktopsChanged, + this, &ILXQtTaskbarAbstractBackend::workspacesCountChanged); + + connect(m_workspaceInfo.get(), &LXQtPlasmaWaylandWorkspaceInfo::desktopNameChanged, + this, [this](int idx) { + emit workspaceNameChanged(idx + 1); // Make 1-based + }); +} + +bool LXQtTaskbarWaylandBackend::supportsAction(WId windowId, LXQtTaskBarBackendAction action) const +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return false; + + LXQtTaskBarPlasmaWindow::state state; + + switch (action) + { + case LXQtTaskBarBackendAction::Move: + state = LXQtTaskBarPlasmaWindow::state::state_movable; + break; + + case LXQtTaskBarBackendAction::Resize: + state = LXQtTaskBarPlasmaWindow::state::state_resizable; + break; + + case LXQtTaskBarBackendAction::Maximize: + state = LXQtTaskBarPlasmaWindow::state::state_maximizable; + break; + + case LXQtTaskBarBackendAction::Minimize: + state = LXQtTaskBarPlasmaWindow::state::state_minimizable; + break; + + case LXQtTaskBarBackendAction::RollUp: + state = LXQtTaskBarPlasmaWindow::state::state_shadeable; + break; + + case LXQtTaskBarBackendAction::FullScreen: + state = LXQtTaskBarPlasmaWindow::state::state_fullscreenable; + break; + + default: + return false; + } + + return window->windowState.testFlag(state); +} + +bool LXQtTaskbarWaylandBackend::reloadWindows() +{ + const QVector wids = getCurrentWindows(); + + // Force removal and re-adding + for(WId windowId : wids) + { + emit windowRemoved(windowId); + } + for(WId windowId : wids) + { + emit windowAdded(windowId); + } + + return true; +} + +QVector LXQtTaskbarWaylandBackend::getCurrentWindows() const +{ + QVector wids; + wids.reserve(wids.size()); + + for(const std::unique_ptr& window : std::as_const(windows)) + { + if(window->acceptedInTaskBar) + wids << window->getWindowId(); + } + return wids; +} + +QString LXQtTaskbarWaylandBackend::getWindowTitle(WId windowId) const +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return QString(); + + return window->title; +} + +bool LXQtTaskbarWaylandBackend::applicationDemandsAttention(WId windowId) const +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return false; + + return window->windowState.testFlag(LXQtTaskBarPlasmaWindow::state::state_demands_attention) || transientsDemandingAttention.contains(window); +} + +QIcon LXQtTaskbarWaylandBackend::getApplicationIcon(WId windowId, int devicePixels) const +{ + Q_UNUSED(devicePixels) + + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return QIcon(); + + return window->icon; +} + +QString LXQtTaskbarWaylandBackend::getWindowClass(WId windowId) const +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return QString(); + return window->appId; +} + +LXQtTaskBarWindowLayer LXQtTaskbarWaylandBackend::getWindowLayer(WId windowId) const +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return LXQtTaskBarWindowLayer::Normal; + + if(window->windowState.testFlag(LXQtTaskBarPlasmaWindow::state::state_keep_above)) + return LXQtTaskBarWindowLayer::KeepAbove; + + if(window->windowState.testFlag(LXQtTaskBarPlasmaWindow::state::state_keep_below)) + return LXQtTaskBarWindowLayer::KeepBelow; + + return LXQtTaskBarWindowLayer::Normal; +} + +bool LXQtTaskbarWaylandBackend::setWindowLayer(WId windowId, LXQtTaskBarWindowLayer layer) +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return false; + + if(getWindowLayer(windowId) == layer) + return true; //TODO: make more efficient + + LXQtTaskBarPlasmaWindow::state plasmaState = LXQtTaskBarPlasmaWindow::state::state_keep_above; + switch (layer) + { + case LXQtTaskBarWindowLayer::Normal: + case LXQtTaskBarWindowLayer::KeepAbove: + break; + case LXQtTaskBarWindowLayer::KeepBelow: + plasmaState = LXQtTaskBarPlasmaWindow::state::state_keep_below; + break; + default: + return false; + } + + window->set_state(plasmaState, layer == LXQtTaskBarWindowLayer::Normal ? 0 : plasmaState); + return false; +} + +LXQtTaskBarWindowState LXQtTaskbarWaylandBackend::getWindowState(WId windowId) const +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return LXQtTaskBarWindowState::Normal; + + if(window->windowState.testFlag(LXQtTaskBarPlasmaWindow::state::state_minimized)) + return LXQtTaskBarWindowState::Hidden; + if(window->windowState.testFlag(LXQtTaskBarPlasmaWindow::state::state_maximizable)) + return LXQtTaskBarWindowState::Maximized; + if(window->windowState.testFlag(LXQtTaskBarPlasmaWindow::state::state_shaded)) + return LXQtTaskBarWindowState::RolledUp; + if(window->windowState.testFlag(LXQtTaskBarPlasmaWindow::state::state_fullscreen)) + return LXQtTaskBarWindowState::FullScreen; + + return LXQtTaskBarWindowState::Normal; +} + +bool LXQtTaskbarWaylandBackend::setWindowState(WId windowId, LXQtTaskBarWindowState state, bool set) +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return false; + + LXQtTaskBarPlasmaWindow::state plasmaState; + switch (state) + { + case LXQtTaskBarWindowState::Minimized: + { + plasmaState = LXQtTaskBarPlasmaWindow::state::state_minimized; + break; + } + case LXQtTaskBarWindowState::Maximized: + case LXQtTaskBarWindowState::MaximizedVertically: + case LXQtTaskBarWindowState::MaximizedHorizontally: + { + plasmaState = LXQtTaskBarPlasmaWindow::state::state_maximized; + break; + } + case LXQtTaskBarWindowState::Normal: + { + plasmaState = LXQtTaskBarPlasmaWindow::state::state_maximized; + set = !set; //TODO: correct + break; + } + case LXQtTaskBarWindowState::RolledUp: + { + plasmaState = LXQtTaskBarPlasmaWindow::state::state_shaded; + break; + } + default: + return false; + } + + window->set_state(plasmaState, set ? plasmaState : 0); + return true; +} + +bool LXQtTaskbarWaylandBackend::isWindowActive(WId windowId) const +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return false; + + return activeWindow == window || window->windowState.testFlag(LXQtTaskBarPlasmaWindow::state::state_active); +} + +bool LXQtTaskbarWaylandBackend::raiseWindow(WId windowId, bool onCurrentWorkSpace) +{ + Q_UNUSED(onCurrentWorkSpace) //TODO + + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return false; + + // Pull forward any transient demanding attention. + if (auto *transientDemandingAttention = transientsDemandingAttention.value(window)) + { + window = transientDemandingAttention; + } + else + { + // TODO Shouldn't KWin take care of that? + // Bringing a transient to the front usually brings its parent with it + // but focus is not handled properly. + // TODO take into account d->lastActivation instead + // of just taking the first one. + while (transients.key(window)) + { + window = transients.key(window); + } + } + + window->set_state(LXQtTaskBarPlasmaWindow::state::state_active, LXQtTaskBarPlasmaWindow::state::state_active); + return true; +} + +bool LXQtTaskbarWaylandBackend::closeWindow(WId windowId) +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return false; + + window->close(); + return true; +} + +WId LXQtTaskbarWaylandBackend::getActiveWindow() const +{ + if(activeWindow) + return activeWindow->getWindowId(); + return 0; +} + +int LXQtTaskbarWaylandBackend::getWorkspacesCount() const +{ + return m_workspaceInfo->numberOfDesktops(); +} + +QString LXQtTaskbarWaylandBackend::getWorkspaceName(int idx) const +{ + return m_workspaceInfo->getDesktopName(idx - 1); //Return to 0-based +} + +int LXQtTaskbarWaylandBackend::getCurrentWorkspace() const +{ + if(!m_workspaceInfo->currentDesktop().isValid()) + return 0; + return m_workspaceInfo->position(m_workspaceInfo->currentDesktop()) + 1; // 1-based +} + +bool LXQtTaskbarWaylandBackend::setCurrentWorkspace(int idx) +{ + QString id = m_workspaceInfo->getDesktopId(idx - 1); //Return to 0-based + if(id.isEmpty()) + return false; + m_workspaceInfo->requestActivate(id); + return true; +} + +int LXQtTaskbarWaylandBackend::getWindowWorkspace(WId windowId) const +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return 0; + + // TODO: this protocol seems to allow multiple desktop for each window + // We do not support that yet + // Also from KDE Plasma task switch it's not clear how to actually put + // a window on multiple desktops (which is different from "All desktops") + QString id = window->virtualDesktops.value(0, QString()); + if(id.isEmpty()) + return 0; + + return m_workspaceInfo->position(id) + 1; //Make 1-based +} + +bool LXQtTaskbarWaylandBackend::setWindowOnWorkspace(WId windowId, int idx) +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return false; + + // Prepare for future multiple virtual desktops per window + QList newDesktops; + + // Fill the list + newDesktops.append(m_workspaceInfo->getDesktopId(idx - 1)); //Return to 0-based + + // Keep only valid IDs + newDesktops.erase(std::remove_if(newDesktops.begin(), newDesktops.end(), + [](const QString& id) { return id.isEmpty(); }), + newDesktops.end()); + + // Add to new requested desktops + for(const QString& id : std::as_const(newDesktops)) + { + if(!window->virtualDesktops.contains(id)) + window->request_enter_virtual_desktop(id); + } + + // Remove from non-requested destops + const QList currentDesktops = window->virtualDesktops; + for(const QString& id : std::as_const(currentDesktops)) + { + if(!newDesktops.contains(id)) + window->request_leave_virtual_desktop(id); + } + + return true; +} + +void LXQtTaskbarWaylandBackend::moveApplicationToPrevNextMonitor(WId windowId, bool next, bool raiseOnCurrentDesktop) +{ + +} + +bool LXQtTaskbarWaylandBackend::isWindowOnScreen(QScreen *screen, WId windowId) const +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return false; + + return screen->geometry().intersects(window->geometry); +} + +bool LXQtTaskbarWaylandBackend::setDesktopLayout(Qt::Orientation, int, int, bool) +{ + //TODO: implement + return false; +} + +void LXQtTaskbarWaylandBackend::moveApplication(WId windowId) +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return; + + window->set_state(LXQtTaskBarPlasmaWindow::state::state_active, LXQtTaskBarPlasmaWindow::state::state_active); + window->request_move(); +} + +void LXQtTaskbarWaylandBackend::resizeApplication(WId windowId) +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return; + + window->set_state(LXQtTaskBarPlasmaWindow::state::state_active, LXQtTaskBarPlasmaWindow::state::state_active); + window->request_resize(); +} + +void LXQtTaskbarWaylandBackend::refreshIconGeometry(WId windowId, const QRect &geom) +{ + +} + +bool LXQtTaskbarWaylandBackend::isAreaOverlapped(const QRect &area) const +{ + for(auto &window : std::as_const(windows)) + { + if(window->geometry.intersects(area)) + return true; + } + return false; +} + +bool LXQtTaskbarWaylandBackend::isShowingDesktop() const +{ + return m_managment->isActive() ? m_managment->isShowingDesktop() : false; +} + +bool LXQtTaskbarWaylandBackend::showDesktop(bool value) +{ + if(!m_managment->isActive()) + return false; + + enum LXQtTaskBarPlasmaWindowManagment::show_desktop flag_; + if(value) + flag_ = LXQtTaskBarPlasmaWindowManagment::show_desktop::show_desktop_enabled; + else + flag_ = LXQtTaskBarPlasmaWindowManagment::show_desktop::show_desktop_disabled; + + m_managment->show_desktop(flag_); + return true; +} + +void LXQtTaskbarWaylandBackend::addWindow(LXQtTaskBarPlasmaWindow *window) +{ + if (findWindow(windows, window) != windows.end() || transients.contains(window)) + { + return; + } + + auto removeWindow = [window, this] + { + auto it = findWindow(windows, window); + if (it != windows.end()) + { + if(window->acceptedInTaskBar) + emit windowRemoved(window->getWindowId()); + windows.erase(it); + transientsDemandingAttention.remove(window); + lastActivated.remove(window); + } + else + { + // Could be a transient. + // Removing a transient might change the demands attention state of the leader. + if (transients.remove(window)) + { + if (LXQtTaskBarPlasmaWindow *leader = transientsDemandingAttention.key(window)) { + transientsDemandingAttention.remove(leader, window); + emit windowPropertyChanged(leader->getWindowId(), int(LXQtTaskBarWindowProperty::Urgency)); + } + } + } + + if (activeWindow == window) + { + activeWindow = nullptr; + emit activeWindowChanged(0); + } + }; + + connect(window, &LXQtTaskBarPlasmaWindow::unmapped, this, removeWindow); + + connect(window, &LXQtTaskBarPlasmaWindow::titleChanged, this, [window, this] { + updateWindowAcceptance(window); + if(window->acceptedInTaskBar) + emit windowPropertyChanged(window->getWindowId(), int(LXQtTaskBarWindowProperty::Title)); + }); + + connect(window, &LXQtTaskBarPlasmaWindow::iconChanged, this, [window, this] { + updateWindowAcceptance(window); + if(window->acceptedInTaskBar) + emit windowPropertyChanged(window->getWindowId(), int(LXQtTaskBarWindowProperty::Icon)); + }); + + connect(window, &LXQtTaskBarPlasmaWindow::geometryChanged, this, [window, this] { + updateWindowAcceptance(window); + if(window->acceptedInTaskBar) + emit windowPropertyChanged(window->getWindowId(), int(LXQtTaskBarWindowProperty::Geometry)); + }); + + connect(window, &LXQtTaskBarPlasmaWindow::appIdChanged, this, [window, this] { + emit windowPropertyChanged(window->getWindowId(), int(LXQtTaskBarWindowProperty::WindowClass)); + }); + + if (window->windowState & LXQtTaskBarPlasmaWindow::state::state_active) { + LXQtTaskBarPlasmaWindow *effectiveActive = window; + while (effectiveActive->parentWindow) { + effectiveActive = effectiveActive->parentWindow; + } + + lastActivated[effectiveActive] = QTime::currentTime(); + activeWindow = effectiveActive; + } + + connect(window, &LXQtTaskBarPlasmaWindow::activeChanged, this, [window, this] { + const bool active = window->windowState & LXQtTaskBarPlasmaWindow::state::state_active; + + LXQtTaskBarPlasmaWindow *effectiveWindow = window; + + while (effectiveWindow->parentWindow) + { + effectiveWindow = effectiveWindow->parentWindow; + } + + if (active) + { + lastActivated[effectiveWindow] = QTime::currentTime(); + + if (activeWindow != effectiveWindow) + { + activeWindow = effectiveWindow; + emit activeWindowChanged(activeWindow->getWindowId()); + } + } + else + { + if (activeWindow == effectiveWindow) + { + activeWindow = nullptr; + emit activeWindowChanged(0); + } + } + }); + + connect(window, &LXQtTaskBarPlasmaWindow::parentWindowChanged, this, [window, this] { + LXQtTaskBarPlasmaWindow *leader = window->parentWindow.data(); + + // Migrate demanding attention to new leader. + if (window->windowState.testFlag(LXQtTaskBarPlasmaWindow::state::state_demands_attention)) + { + if (auto *oldLeader = transientsDemandingAttention.key(window)) + { + if (window->parentWindow != oldLeader) + { + transientsDemandingAttention.remove(oldLeader, window); + transientsDemandingAttention.insert(leader, window); + emit windowPropertyChanged(oldLeader->getWindowId(), int(LXQtTaskBarWindowProperty::Urgency)); + emit windowPropertyChanged(leader->getWindowId(), int(LXQtTaskBarWindowProperty::Urgency)); + } + } + } + + if (transients.remove(window)) + { + if (leader) + { + // leader change. + transients.insert(window, leader); + } + else + { + // lost a leader, add to regular windows list. + Q_ASSERT(findWindow(windows, window) == windows.end()); + + windows.emplace_back(window); + } + } + else if (leader) + { + // gained a leader, remove from regular windows list. + auto it = findWindow(windows, window); + Q_ASSERT(it != windows.end()); + + windows.erase(it); + lastActivated.remove(window); + } + }); + + auto stateChanged = [window, this] { + updateWindowAcceptance(window); + if(window->acceptedInTaskBar) + emit windowPropertyChanged(window->getWindowId(), int(LXQtTaskBarWindowProperty::State)); + }; + + connect(window, &LXQtTaskBarPlasmaWindow::fullscreenChanged, this, stateChanged); + + connect(window, &LXQtTaskBarPlasmaWindow::maximizedChanged, this, stateChanged); + + connect(window, &LXQtTaskBarPlasmaWindow::minimizedChanged, this, stateChanged); + + connect(window, &LXQtTaskBarPlasmaWindow::shadedChanged, this, stateChanged); + + auto workspaceChanged = [window, this] { + updateWindowAcceptance(window); + if(window->acceptedInTaskBar) + emit windowPropertyChanged(window->getWindowId(), int(LXQtTaskBarWindowProperty::Workspace)); + }; + + connect(window, &LXQtTaskBarPlasmaWindow::virtualDesktopEntered, this, workspaceChanged); + connect(window, &LXQtTaskBarPlasmaWindow::virtualDesktopLeft, this, workspaceChanged); + + connect(window, &LXQtTaskBarPlasmaWindow::demandsAttentionChanged, this, [window, this] { + // Changes to a transient's state might change demands attention state for leader. + if (auto *leader = transients.value(window)) + { + if (window->windowState.testFlag(LXQtTaskBarPlasmaWindow::state::state_demands_attention)) + { + if (!transientsDemandingAttention.values(leader).contains(window)) + { + transientsDemandingAttention.insert(leader, window); + emit windowPropertyChanged(leader->getWindowId(), int(LXQtTaskBarWindowProperty::Urgency)); + } + } + else if (transientsDemandingAttention.remove(window)) + { + emit windowPropertyChanged(leader->getWindowId(), int(LXQtTaskBarWindowProperty::Urgency)); + } + } + else + { + emit windowPropertyChanged(window->getWindowId(), int(LXQtTaskBarWindowProperty::Urgency)); + } + }); + + connect(window, &LXQtTaskBarPlasmaWindow::skipTaskbarChanged, this, [window, this] { + updateWindowAcceptance(window); + }); + + // QObject::connect(window, &PlasmaWindow::applicationMenuChanged, q, [window, this] { + // this->dataChanged(window, QList{ApplicationMenuServiceName, ApplicationMenuObjectPath}); + // }); + + // Handle transient. + if (LXQtTaskBarPlasmaWindow *leader = window->parentWindow.data()) + { + transients.insert(window, leader); + + // Update demands attention state for leader. + if (window->windowState.testFlag(LXQtTaskBarPlasmaWindow::state::state_demands_attention)) + { + transientsDemandingAttention.insert(leader, window); + if(leader->acceptedInTaskBar) + emit windowPropertyChanged(leader->getWindowId(), int(LXQtTaskBarWindowProperty::Urgency)); + } + } + else + { + windows.emplace_back(window); + updateWindowAcceptance(window); + } +} + +bool LXQtTaskbarWaylandBackend::acceptWindow(LXQtTaskBarPlasmaWindow *window) const +{ + if(window->windowState.testFlag(LXQtTaskBarPlasmaWindow::state::state_skiptaskbar)) + return false; + + if(transients.contains(window)) + return false; + + return true; +} + +void LXQtTaskbarWaylandBackend::updateWindowAcceptance(LXQtTaskBarPlasmaWindow *window) +{ + if(!window->acceptedInTaskBar && acceptWindow(window)) + { + window->acceptedInTaskBar = true; + emit windowAdded(window->getWindowId()); + } + else if(window->acceptedInTaskBar && !acceptWindow(window)) + { + window->acceptedInTaskBar = false; + emit windowRemoved(window->getWindowId()); + } +} + +LXQtTaskBarPlasmaWindow *LXQtTaskbarWaylandBackend::getWindow(WId windowId) const +{ + for(auto &window : std::as_const(windows)) + { + if(window->getWindowId() == windowId) + return window.get(); + } + + return nullptr; +} diff --git a/panel/backends/wayland/lxqttaskbarbackendwayland.h b/panel/backends/wayland/lxqttaskbarbackendwayland.h new file mode 100644 index 000000000..22a699fa8 --- /dev/null +++ b/panel/backends/wayland/lxqttaskbarbackendwayland.h @@ -0,0 +1,124 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2023 LXQt team + * Authors: + * Filippo Gentile + * + * This program or library is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * END_COMMON_COPYRIGHT_HEADER */ + +#ifndef LXQTTASKBARBACKENDWAYLAND_H +#define LXQTTASKBARBACKENDWAYLAND_H + +#include "../ilxqttaskbarabstractbackend.h" + +#include +#include +#include + +class LXQtTaskBarPlasmaWindow; +class LXQtTaskBarPlasmaWindowManagment; +class LXQtPlasmaWaylandWorkspaceInfo; + + +class LXQtTaskbarWaylandBackend : public ILXQtTaskbarAbstractBackend +{ + Q_OBJECT + +public: + explicit LXQtTaskbarWaylandBackend(QObject *parent = nullptr); + + // Backend + virtual bool supportsAction(WId windowId, LXQtTaskBarBackendAction action) const override; + + // Windows + virtual bool reloadWindows() override; + + virtual QVector getCurrentWindows() const override; + virtual QString getWindowTitle(WId windowId) const override; + virtual bool applicationDemandsAttention(WId windowId) const override; + virtual QIcon getApplicationIcon(WId windowId, int devicePixels) const override; + virtual QString getWindowClass(WId windowId) const override; + + virtual LXQtTaskBarWindowLayer getWindowLayer(WId windowId) const override; + virtual bool setWindowLayer(WId windowId, LXQtTaskBarWindowLayer layer) override; + + virtual LXQtTaskBarWindowState getWindowState(WId windowId) const override; + virtual bool setWindowState(WId windowId, LXQtTaskBarWindowState state, bool set) override; + + virtual bool isWindowActive(WId windowId) const override; + virtual bool raiseWindow(WId windowId, bool onCurrentWorkSpace) override; + + virtual bool closeWindow(WId windowId) override; + + virtual WId getActiveWindow() const override; + + // Workspaces + virtual int getWorkspacesCount() const override; + virtual QString getWorkspaceName(int idx) const override; + + virtual int getCurrentWorkspace() const override; + virtual bool setCurrentWorkspace(int idx) override; + + virtual int getWindowWorkspace(WId windowId) const override; + virtual bool setWindowOnWorkspace(WId windowId, int idx) override; + + virtual void moveApplicationToPrevNextMonitor(WId windowId, bool next, bool raiseOnCurrentDesktop) override; + + virtual bool isWindowOnScreen(QScreen *screen, WId windowId) const override; + + virtual bool setDesktopLayout(Qt::Orientation orientation, int rows, int columns, bool rightToLeft) override; + + // X11 Specific + virtual void moveApplication(WId windowId) override; + virtual void resizeApplication(WId windowId) override; + + virtual void refreshIconGeometry(WId windowId, const QRect &geom) override; + + // Panel internal + virtual bool isAreaOverlapped(const QRect& area) const override; + + // Show Destop + virtual bool isShowingDesktop() const override; + virtual bool showDesktop(bool value) override; + +private: + void addWindow(LXQtTaskBarPlasmaWindow *window); + bool acceptWindow(LXQtTaskBarPlasmaWindow *window) const; + void updateWindowAcceptance(LXQtTaskBarPlasmaWindow *window); + +private: + LXQtTaskBarPlasmaWindow *getWindow(WId windowId) const; + + std::unique_ptr m_workspaceInfo; + + std::unique_ptr m_managment; + + QHash lastActivated; + LXQtTaskBarPlasmaWindow *activeWindow = nullptr; + std::vector> windows; + // key=transient child, value=leader + QHash transients; + // key=leader, values=transient children + QMultiHash transientsDemandingAttention; +}; + +#endif // LXQTTASKBARBACKENDWAYLAND_H diff --git a/panel/backends/wayland/lxqttaskbarplasmawindowmanagment.cpp b/panel/backends/wayland/lxqttaskbarplasmawindowmanagment.cpp new file mode 100644 index 000000000..d64ee9b0d --- /dev/null +++ b/panel/backends/wayland/lxqttaskbarplasmawindowmanagment.cpp @@ -0,0 +1,311 @@ +/* + SPDX-FileCopyrightText: 2016 Eike Hein + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + + Adapted from KDE Plasma Workspace: plasma-workspace/libtaskmanager/waylandtasksmodel.cpp +*/ + +#include "lxqttaskbarplasmawindowmanagment.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +/* + * LXQtTaskBarPlasmaWindow + */ + +LXQtTaskBarPlasmaWindow::LXQtTaskBarPlasmaWindow(const QString &uuid, ::org_kde_plasma_window *id) + : org_kde_plasma_window(id) + , uuid(uuid) +{ +} + +LXQtTaskBarPlasmaWindow::~LXQtTaskBarPlasmaWindow() +{ + destroy(); +} + +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_unmapped() +{ + wasUnmapped = true; + Q_EMIT unmapped(); +} + +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_title_changed(const QString &title) +{ + if(this->title == title) + return; + this->title = title; + Q_EMIT titleChanged(); +} + +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_app_id_changed(const QString &app_id) +{ + if(appId == app_id) + return; + appId = app_id; + Q_EMIT appIdChanged(); +} + +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_icon_changed() +{ + int pipeFds[2]; + if (pipe2(pipeFds, O_CLOEXEC) != 0) { + qWarning() << "TaskManager: failed creating pipe"; + return; + } + get_icon(pipeFds[1]); + ::close(pipeFds[1]); + auto readIcon = [uuid = uuid](int fd) { + auto closeGuard = qScopeGuard([fd]() { + ::close(fd); + }); + pollfd pollFd; + pollFd.fd = fd; + pollFd.events = POLLIN; + QByteArray data; + while (true) { + int ready = poll(&pollFd, 1, 1000); + if (ready < 0 && errno != EINTR) { + qWarning() << "TaskManager: polling for icon of window" << uuid << "failed"; + return QIcon(); + } else if (ready == 0) { + qWarning() << "TaskManager: time out polling for icon of window" << uuid; + return QIcon(); + } else { + char buffer[4096]; + int n = read(fd, buffer, sizeof(buffer)); + if (n < 0) { + qWarning() << "TaskManager: error reading icon of window" << uuid; + return QIcon(); + } else if (n > 0) { + data.append(buffer, n); + } else { + QIcon icon; + QDataStream ds(data); + ds >> icon; + return icon; + } + } + } + }; + QFuture future = QtConcurrent::run(readIcon, pipeFds[0]); + auto watcher = new QFutureWatcher(); + watcher->setFuture(future); + connect(watcher, &QFutureWatcher::finished, this, [this, watcher] { + icon = watcher->future().result(); + Q_EMIT iconChanged(); + }); + connect(watcher, &QFutureWatcher::finished, watcher, &QObject::deleteLater); +} + +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_themed_icon_name_changed(const QString &name) +{ + icon = QIcon::fromTheme(name); + Q_EMIT iconChanged(); +} + +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_state_changed(uint32_t flags) +{ + auto diff = windowState ^ flags; + if (diff & state::state_active) { + windowState.setFlag(state::state_active, flags & state::state_active); + Q_EMIT activeChanged(); + } + if (diff & state::state_minimized) { + windowState.setFlag(state::state_minimized, flags & state::state_minimized); + Q_EMIT minimizedChanged(); + } + if (diff & state::state_maximized) { + windowState.setFlag(state::state_maximized, flags & state::state_maximized); + Q_EMIT maximizedChanged(); + } + if (diff & state::state_fullscreen) { + windowState.setFlag(state::state_fullscreen, flags & state::state_fullscreen); + Q_EMIT fullscreenChanged(); + } + if (diff & state::state_keep_above) { + windowState.setFlag(state::state_keep_above, flags & state::state_keep_above); + Q_EMIT keepAboveChanged(); + } + if (diff & state::state_keep_below) { + windowState.setFlag(state::state_keep_below, flags & state::state_keep_below); + Q_EMIT keepBelowChanged(); + } + if (diff & state::state_on_all_desktops) { + windowState.setFlag(state::state_on_all_desktops, flags & state::state_on_all_desktops); + Q_EMIT onAllDesktopsChanged(); + } + if (diff & state::state_demands_attention) { + windowState.setFlag(state::state_demands_attention, flags & state::state_demands_attention); + Q_EMIT demandsAttentionChanged(); + } + if (diff & state::state_closeable) { + windowState.setFlag(state::state_closeable, flags & state::state_closeable); + Q_EMIT closeableChanged(); + } + if (diff & state::state_minimizable) { + windowState.setFlag(state::state_minimizable, flags & state::state_minimizable); + Q_EMIT minimizeableChanged(); + } + if (diff & state::state_maximizable) { + windowState.setFlag(state::state_maximizable, flags & state::state_maximizable); + Q_EMIT maximizeableChanged(); + } + if (diff & state::state_fullscreenable) { + windowState.setFlag(state::state_fullscreenable, flags & state::state_fullscreenable); + Q_EMIT fullscreenableChanged(); + } + if (diff & state::state_skiptaskbar) { + windowState.setFlag(state::state_skiptaskbar, flags & state::state_skiptaskbar); + Q_EMIT skipTaskbarChanged(); + } + if (diff & state::state_shadeable) { + windowState.setFlag(state::state_shadeable, flags & state::state_shadeable); + Q_EMIT shadeableChanged(); + } + if (diff & state::state_shaded) { + windowState.setFlag(state::state_shaded, flags & state::state_shaded); + Q_EMIT shadedChanged(); + } + if (diff & state::state_movable) { + windowState.setFlag(state::state_movable, flags & state::state_movable); + Q_EMIT movableChanged(); + } + if (diff & state::state_resizable) { + windowState.setFlag(state::state_resizable, flags & state::state_resizable); + Q_EMIT resizableChanged(); + } + if (diff & state::state_virtual_desktop_changeable) { + windowState.setFlag(state::state_virtual_desktop_changeable, flags & state::state_virtual_desktop_changeable); + Q_EMIT virtualDesktopChangeableChanged(); + } + if (diff & state::state_skipswitcher) { + windowState.setFlag(state::state_skipswitcher, flags & state::state_skipswitcher); + Q_EMIT skipSwitcherChanged(); + } +} + +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_virtual_desktop_entered(const QString &id) +{ + virtualDesktops.push_back(id); + Q_EMIT virtualDesktopEntered(); +} + +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_virtual_desktop_left(const QString &id) +{ + virtualDesktops.removeAll(id); + Q_EMIT virtualDesktopLeft(); +} + +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_geometry(int32_t x, int32_t y, uint32_t width, uint32_t height) +{ + geometry = QRect(x, y, width, height); + Q_EMIT geometryChanged(); +} + +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_application_menu(const QString &service_name, const QString &object_path) +{ + applicationMenuService = service_name; + applicationMenuObjectPath = object_path; + Q_EMIT applicationMenuChanged(); +} + +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_activity_entered(const QString &id) +{ + activities.push_back(id); + Q_EMIT activitiesChanged(); +} +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_activity_left(const QString &id) +{ + activities.removeAll(id); + Q_EMIT activitiesChanged(); +} +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_pid_changed(uint32_t pid) +{ + this->pid = pid; +} +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_resource_name_changed(const QString &resource_name) +{ + resourceName = resource_name; +} +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_parent_window(::org_kde_plasma_window *parent) +{ + LXQtTaskBarPlasmaWindow *parentWindow = nullptr; + if (parent) { + parentWindow = dynamic_cast(LXQtTaskBarPlasmaWindow::fromObject(parent)); + } + setParentWindow(parentWindow); +} +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_initial_state() +{ + Q_EMIT initialStateDone(); +} + +void LXQtTaskBarPlasmaWindow::setParentWindow(LXQtTaskBarPlasmaWindow *parent) +{ + const auto old = parentWindow; + QObject::disconnect(parentWindowUnmappedConnection); + + if (parent && !parent->wasUnmapped) { + parentWindow = QPointer(parent); + parentWindowUnmappedConnection = QObject::connect(parent, &LXQtTaskBarPlasmaWindow::unmapped, this, [this] { + setParentWindow(nullptr); + }); + } else { + parentWindow = QPointer(); + parentWindowUnmappedConnection = QMetaObject::Connection(); + } + + if (parentWindow.data() != old.data()) { + Q_EMIT parentWindowChanged(); + } +} + +/* + * LXQtTaskBarPlasmaWindowManagment + */ + +LXQtTaskBarPlasmaWindowManagment::LXQtTaskBarPlasmaWindowManagment() + : QWaylandClientExtensionTemplate(version) +{ + connect(this, &QWaylandClientExtension::activeChanged, this, [this] { + if (!isActive()) { + org_kde_plasma_window_management_destroy(object()); + } + }); +} + +LXQtTaskBarPlasmaWindowManagment::~LXQtTaskBarPlasmaWindowManagment() +{ + if (isActive()) { + org_kde_plasma_window_management_destroy(object()); + } +} + +void LXQtTaskBarPlasmaWindowManagment::org_kde_plasma_window_management_show_desktop_changed(uint32_t state) +{ + m_isShowingDesktop = (state == show_desktop::show_desktop_enabled); +} + +void LXQtTaskBarPlasmaWindowManagment::org_kde_plasma_window_management_window_with_uuid(uint32_t id, const QString &uuid) +{ + Q_UNUSED(id) + Q_EMIT windowCreated(new LXQtTaskBarPlasmaWindow(uuid, get_window_by_uuid(uuid))); +} +void LXQtTaskBarPlasmaWindowManagment::org_kde_plasma_window_management_stacking_order_uuid_changed(const QString &uuids) +{ + Q_EMIT stackingOrderChanged(uuids); +} diff --git a/panel/backends/wayland/lxqttaskbarplasmawindowmanagment.h b/panel/backends/wayland/lxqttaskbarplasmawindowmanagment.h new file mode 100644 index 000000000..b7c6d1f00 --- /dev/null +++ b/panel/backends/wayland/lxqttaskbarplasmawindowmanagment.h @@ -0,0 +1,170 @@ +/* + SPDX-FileCopyrightText: 2016 Eike Hein + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + + Adapted from KDE Plasma Workspace: plasma-workspace/libtaskmanager/waylandtasksmodel.cpp +*/ + +#ifndef LXQTTASKBARPLASMAWINDOWMANAGMENT_H +#define LXQTTASKBARPLASMAWINDOWMANAGMENT_H + +#include +#include +#include + +#include "qwayland-plasma-window-management.h" + +typedef quintptr WId; + +class LXQtTaskBarPlasmaWindowManagment; + +class LXQtTaskBarPlasmaWindow : public QObject, + public QtWayland::org_kde_plasma_window +{ + Q_OBJECT +public: + LXQtTaskBarPlasmaWindow(const QString &uuid, ::org_kde_plasma_window *id); + ~LXQtTaskBarPlasmaWindow(); + + inline WId getWindowId() const { return reinterpret_cast(this); } + + using state = QtWayland::org_kde_plasma_window_management::state; + const QString uuid; + QString title; + QString appId; + QIcon icon; + QFlags windowState; + QList virtualDesktops; + QRect geometry; + QString applicationMenuService; + QString applicationMenuObjectPath; + QList activities; + quint32 pid; + QString resourceName; + QPointer parentWindow; + bool wasUnmapped = false; + bool acceptedInTaskBar = false; + +Q_SIGNALS: + void unmapped(); + void titleChanged(); + void appIdChanged(); + void iconChanged(); + void activeChanged(); + void minimizedChanged(); + void maximizedChanged(); + void fullscreenChanged(); + void keepAboveChanged(); + void keepBelowChanged(); + void onAllDesktopsChanged(); + void demandsAttentionChanged(); + void closeableChanged(); + void minimizeableChanged(); + void maximizeableChanged(); + void fullscreenableChanged(); + void skiptaskbarChanged(); + void shadeableChanged(); + void shadedChanged(); + void movableChanged(); + void resizableChanged(); + void virtualDesktopChangeableChanged(); + void skipSwitcherChanged(); + void virtualDesktopEntered(); + void virtualDesktopLeft(); + void geometryChanged(); + void skipTaskbarChanged(); + void applicationMenuChanged(); + void activitiesChanged(); + void parentWindowChanged(); + void initialStateDone(); + +protected: + void org_kde_plasma_window_unmapped() override; + void org_kde_plasma_window_title_changed(const QString &title) override; + void org_kde_plasma_window_app_id_changed(const QString &app_id) override; + void org_kde_plasma_window_icon_changed() override; + void org_kde_plasma_window_themed_icon_name_changed(const QString &name) override; + void org_kde_plasma_window_state_changed(uint32_t flags) override; + void org_kde_plasma_window_virtual_desktop_entered(const QString &id) override; + + void org_kde_plasma_window_virtual_desktop_left(const QString &id) override; + void org_kde_plasma_window_geometry(int32_t x, int32_t y, uint32_t width, uint32_t height) override; + void org_kde_plasma_window_application_menu(const QString &service_name, const QString &object_path) override; + void org_kde_plasma_window_activity_entered(const QString &id) override; + void org_kde_plasma_window_activity_left(const QString &id) override; + void org_kde_plasma_window_pid_changed(uint32_t pid) override; + void org_kde_plasma_window_resource_name_changed(const QString &resource_name) override; + void org_kde_plasma_window_parent_window(::org_kde_plasma_window *parent) override; + void org_kde_plasma_window_initial_state() override; + +private: + void setParentWindow(LXQtTaskBarPlasmaWindow *parent); + + QMetaObject::Connection parentWindowUnmappedConnection; +}; + +class LXQtTaskBarPlasmaWindowManagment : public QWaylandClientExtensionTemplate, + public QtWayland::org_kde_plasma_window_management +{ + Q_OBJECT +public: + static constexpr int version = 16; + + LXQtTaskBarPlasmaWindowManagment(); + ~LXQtTaskBarPlasmaWindowManagment(); + + inline bool isShowingDesktop() const { return m_isShowingDesktop; } + +protected: + void org_kde_plasma_window_management_show_desktop_changed(uint32_t state) override; + void org_kde_plasma_window_management_window_with_uuid(uint32_t id, const QString &uuid) override; + void org_kde_plasma_window_management_stacking_order_uuid_changed(const QString &uuids) override; + +Q_SIGNALS: + void windowCreated(LXQtTaskBarPlasmaWindow *window); + void stackingOrderChanged(const QString &uuids); + +private: + bool m_isShowingDesktop = false; +}; + +// class Q_DECL_HIDDEN WaylandTasksModel::Private +// { +// public: +// Private(WaylandTasksModel *q); +// QHash appDataCache; +// QHash lastActivated; +// PlasmaWindow *activeWindow = nullptr; +// std::vector> windows; +// // key=transient child, value=leader +// QHash transients; +// // key=leader, values=transient children +// QMultiHash transientsDemandingAttention; +// std::unique_ptr windowManagement; +// KSharedConfig::Ptr rulesConfig; +// KDirWatch *configWatcher = nullptr; +// VirtualDesktopInfo *virtualDesktopInfo = nullptr; +// static QUuid uuid; +// QList stackingOrder; + +// void init(); +// void initWayland(); +// auto findWindow(PlasmaWindow *window) const; +// void addWindow(PlasmaWindow *window); + +// const AppData &appData(PlasmaWindow *window); + +// QIcon icon(PlasmaWindow *window); + +// static QString mimeType(); +// static QString groupMimeType(); + +// void dataChanged(PlasmaWindow *window, int role); +// void dataChanged(PlasmaWindow *window, const QList &roles); + +// private: +// WaylandTasksModel *q; +// }; + +#endif // LXQTTASKBARPLASMAWINDOWMANAGMENT_H diff --git a/panel/backends/wayland/plasma/CMakeLists.txt b/panel/backends/wayland/plasma/CMakeLists.txt new file mode 100644 index 000000000..71b2b273b --- /dev/null +++ b/panel/backends/wayland/plasma/CMakeLists.txt @@ -0,0 +1,32 @@ +# Wayland WM Backend + +project(lxqt-panel-backend-plasma) + +find_package(Qt6 ${REQUIRED_QT_VERSION} REQUIRED COMPONENTS WaylandClient Concurrent) + +add_library(lxqt-panel-backend-plasma STATIC + lxqttaskbarbackendplasma.h + lxqttaskbarplasmawindowmanagment.h + lxqtplasmavirtualdesktop.h + + lxqttaskbarbackendplasma.cpp + lxqttaskbarplasmawindowmanagment.cpp + lxqtplasmavirtualdesktop.cpp +) + +qt6_generate_wayland_protocol_client_sources(lxqt-panel-backend-plasma +FILES + ${CMAKE_CURRENT_SOURCE_DIR}/protocols/plasma-window-management.xml +) + +qt6_generate_wayland_protocol_client_sources(lxqt-panel-backend-plasma +FILES + ${CMAKE_CURRENT_SOURCE_DIR}/protocols/org-kde-plasma-virtual-desktop.xml +) + +target_link_libraries(lxqt-panel-backend-plasma + Qt6::GuiPrivate + Qt6::WaylandClient + Qt6::Concurrent + lxqt-panel-backend-common +) diff --git a/panel/backends/wayland/plasma/lxqtplasmavirtualdesktop.cpp b/panel/backends/wayland/plasma/lxqtplasmavirtualdesktop.cpp new file mode 100644 index 000000000..01e28fe75 --- /dev/null +++ b/panel/backends/wayland/plasma/lxqtplasmavirtualdesktop.cpp @@ -0,0 +1,249 @@ +#include "lxqtplasmavirtualdesktop.h" + +#include + +LXQtPlasmaVirtualDesktop::LXQtPlasmaVirtualDesktop(::org_kde_plasma_virtual_desktop *object, const QString &id) + : org_kde_plasma_virtual_desktop(object) + , id(id) +{ +} + +LXQtPlasmaVirtualDesktop::~LXQtPlasmaVirtualDesktop() +{ + wl_proxy_destroy(reinterpret_cast(object())); +} + +void LXQtPlasmaVirtualDesktop::org_kde_plasma_virtual_desktop_name(const QString &name) +{ + this->name = name; + Q_EMIT nameChanged(); +} + +void LXQtPlasmaVirtualDesktop::org_kde_plasma_virtual_desktop_done() +{ + Q_EMIT done(); +} + +void LXQtPlasmaVirtualDesktop::org_kde_plasma_virtual_desktop_activated() +{ + Q_EMIT activated(); +} + +LXQtPlasmaVirtualDesktopManagment::LXQtPlasmaVirtualDesktopManagment() + : QWaylandClientExtensionTemplate(version) +{ + connect(this, &QWaylandClientExtension::activeChanged, this, [this] { + if (!isActive()) { + org_kde_plasma_virtual_desktop_management_destroy(object()); + } + }); +} + +LXQtPlasmaVirtualDesktopManagment::~LXQtPlasmaVirtualDesktopManagment() +{ + if (isActive()) { + org_kde_plasma_virtual_desktop_management_destroy(object()); + } +} + +void LXQtPlasmaVirtualDesktopManagment::org_kde_plasma_virtual_desktop_management_desktop_created(const QString &desktop_id, uint32_t position) +{ + emit desktopCreated(desktop_id, position); +} + +void LXQtPlasmaVirtualDesktopManagment::org_kde_plasma_virtual_desktop_management_desktop_removed(const QString &desktop_id) +{ + emit desktopRemoved(desktop_id); +} + +void LXQtPlasmaVirtualDesktopManagment::org_kde_plasma_virtual_desktop_management_rows(uint32_t rows) +{ + emit rowsChanged(rows); +} + +LXQtPlasmaWaylandWorkspaceInfo::LXQtPlasmaWaylandWorkspaceInfo() +{ + init(); +} + +LXQtPlasmaWaylandWorkspaceInfo::VirtualDesktopsIterator LXQtPlasmaWaylandWorkspaceInfo::findDesktop(const QString &id) const +{ + return std::find_if(virtualDesktops.begin(), virtualDesktops.end(), + [&id](const std::unique_ptr &desktop) { + return desktop->id == id; + }); +} + +QString LXQtPlasmaWaylandWorkspaceInfo::getDesktopName(int pos) const +{ + if(pos < 0 || size_t(pos) >= virtualDesktops.size()) + return QString(); + return virtualDesktops[pos]->name; +} + +QString LXQtPlasmaWaylandWorkspaceInfo::getDesktopId(int pos) const +{ + if(pos < 0 || size_t(pos) >= virtualDesktops.size()) + return QString(); + return virtualDesktops[pos]->id; +} + +void LXQtPlasmaWaylandWorkspaceInfo::init() +{ + virtualDesktopManagement = std::make_unique(); + + connect(virtualDesktopManagement.get(), &LXQtPlasmaVirtualDesktopManagment::activeChanged, this, [this] { + if (!virtualDesktopManagement->isActive()) { + rows = 0; + virtualDesktops.clear(); + currentVirtualDesktop.clear(); + Q_EMIT currentDesktopChanged(); + Q_EMIT numberOfDesktopsChanged(); + Q_EMIT navigationWrappingAroundChanged(); + Q_EMIT desktopIdsChanged(); + Q_EMIT desktopLayoutRowsChanged(); + } + }); + + connect(virtualDesktopManagement.get(), &LXQtPlasmaVirtualDesktopManagment::desktopCreated, + this, &LXQtPlasmaWaylandWorkspaceInfo::addDesktop); + + connect(virtualDesktopManagement.get(), &LXQtPlasmaVirtualDesktopManagment::desktopRemoved, this, [this](const QString &id) { + + + virtualDesktops.erase(std::remove_if(virtualDesktops.begin(), virtualDesktops.end(), + [id](const std::unique_ptr &desktop) + { + return desktop->id == id; + }), + virtualDesktops.end()); + + Q_EMIT numberOfDesktopsChanged(); + Q_EMIT desktopIdsChanged(); + + if (currentVirtualDesktop == id) { + currentVirtualDesktop.clear(); + Q_EMIT currentDesktopChanged(); + } + }); + + connect(virtualDesktopManagement.get(), &LXQtPlasmaVirtualDesktopManagment::rowsChanged, this, [this](quint32 rows) { + this->rows = rows; + Q_EMIT desktopLayoutRowsChanged(); + }); +} + +void LXQtPlasmaWaylandWorkspaceInfo::addDesktop(const QString &id, quint32 pos) +{ + if (findDesktop(id) != virtualDesktops.end()) { + return; + } + + auto desktop = std::make_unique(virtualDesktopManagement->get_virtual_desktop(id), id); + + connect(desktop.get(), &LXQtPlasmaVirtualDesktop::activated, this, [id, this]() { + currentVirtualDesktop = id; + Q_EMIT currentDesktopChanged(); + }); + + connect(desktop.get(), &LXQtPlasmaVirtualDesktop::nameChanged, this, [id, this]() { + Q_EMIT desktopNameChanged(position(id)); + }); + + connect(desktop.get(), &LXQtPlasmaVirtualDesktop::done, this, [id, this]() { + Q_EMIT desktopNameChanged(position(id)); + }); + + virtualDesktops.insert(std::next(virtualDesktops.begin(), pos), std::move(desktop)); + + Q_EMIT numberOfDesktopsChanged(); + Q_EMIT desktopIdsChanged(); + Q_EMIT desktopNameChanged(position(id)); +} + +QVariant LXQtPlasmaWaylandWorkspaceInfo::currentDesktop() const +{ + return currentVirtualDesktop; +} + +int LXQtPlasmaWaylandWorkspaceInfo::numberOfDesktops() const +{ + return virtualDesktops.size(); +} + +quint32 LXQtPlasmaWaylandWorkspaceInfo::position(const QVariant &desktop) const +{ + return std::distance(virtualDesktops.begin(), findDesktop(desktop.toString())); +} + +QVariantList LXQtPlasmaWaylandWorkspaceInfo::desktopIds() const +{ + QVariantList ids; + ids.reserve(virtualDesktops.size()); + + std::transform(virtualDesktops.cbegin(), virtualDesktops.cend(), std::back_inserter(ids), [](const std::unique_ptr &desktop) { + return desktop->id; + }); + return ids; +} + +QStringList LXQtPlasmaWaylandWorkspaceInfo::desktopNames() const +{ + if (!virtualDesktopManagement->isActive()) { + return QStringList(); + } + QStringList names; + names.reserve(virtualDesktops.size()); + + std::transform(virtualDesktops.cbegin(), virtualDesktops.cend(), std::back_inserter(names), [](const std::unique_ptr &desktop) { + return desktop->name; + }); + return names; +} + +int LXQtPlasmaWaylandWorkspaceInfo::desktopLayoutRows() const +{ + if (!virtualDesktopManagement->isActive()) { + return 0; + } + + return rows; +} + +void LXQtPlasmaWaylandWorkspaceInfo::requestActivate(const QVariant &desktop) +{ + if (!virtualDesktopManagement->isActive()) { + return; + } + + if (auto it = findDesktop(desktop.toString()); it != virtualDesktops.end()) { + (*it)->request_activate(); + } +} + +void LXQtPlasmaWaylandWorkspaceInfo::requestCreateDesktop(quint32 position) +{ + if (!virtualDesktopManagement->isActive()) { + return; + } + + //TODO: translatestd + virtualDesktopManagement->request_create_virtual_desktop(QLatin1String("New Desktop"), position); +} + +void LXQtPlasmaWaylandWorkspaceInfo::requestRemoveDesktop(quint32 position) +{ + if (!virtualDesktopManagement->isActive()) { + return; + } + if (virtualDesktops.size() == 1) { + return; + } + + if (position > (virtualDesktops.size() - 1)) { + return; + } + + virtualDesktopManagement->request_remove_virtual_desktop(virtualDesktops.at(position)->id); +} + diff --git a/panel/backends/wayland/plasma/lxqtplasmavirtualdesktop.h b/panel/backends/wayland/plasma/lxqtplasmavirtualdesktop.h new file mode 100644 index 000000000..825f0de83 --- /dev/null +++ b/panel/backends/wayland/plasma/lxqtplasmavirtualdesktop.h @@ -0,0 +1,88 @@ +#pragma once + +#include +#include + +#include + +#include "qwayland-org-kde-plasma-virtual-desktop.h" + +class LXQtPlasmaVirtualDesktop : public QObject, public QtWayland::org_kde_plasma_virtual_desktop +{ + Q_OBJECT +public: + LXQtPlasmaVirtualDesktop(::org_kde_plasma_virtual_desktop *object, const QString &id); + ~LXQtPlasmaVirtualDesktop(); + const QString id; + QString name; +Q_SIGNALS: + void done(); + void activated(); + void nameChanged(); + +protected: + void org_kde_plasma_virtual_desktop_name(const QString &name) override; + void org_kde_plasma_virtual_desktop_done() override; + void org_kde_plasma_virtual_desktop_activated() override; +}; + + +class LXQtPlasmaVirtualDesktopManagment : public QWaylandClientExtensionTemplate, + public QtWayland::org_kde_plasma_virtual_desktop_management +{ + Q_OBJECT +public: + static constexpr int version = 2; + + LXQtPlasmaVirtualDesktopManagment(); + ~LXQtPlasmaVirtualDesktopManagment(); + +signals: + void desktopCreated(const QString &id, quint32 position); + void desktopRemoved(const QString &id); + void rowsChanged(const quint32 rows); + +protected: + virtual void org_kde_plasma_virtual_desktop_management_desktop_created(const QString &desktop_id, uint32_t position) override; + virtual void org_kde_plasma_virtual_desktop_management_desktop_removed(const QString &desktop_id) override; + virtual void org_kde_plasma_virtual_desktop_management_rows(uint32_t rows) override; +}; + +class Q_DECL_HIDDEN LXQtPlasmaWaylandWorkspaceInfo : public QObject +{ + Q_OBJECT +public: + LXQtPlasmaWaylandWorkspaceInfo(); + + QVariant currentVirtualDesktop; + std::vector> virtualDesktops; + std::unique_ptr virtualDesktopManagement; + quint32 rows; + + typedef std::vector>::const_iterator VirtualDesktopsIterator; + + VirtualDesktopsIterator findDesktop(const QString &id) const; + + QString getDesktopName(int pos) const; + QString getDesktopId(int pos) const; + + void init(); + void addDesktop(const QString &id, quint32 pos); + QVariant currentDesktop() const; + int numberOfDesktops() const; + QVariantList desktopIds() const; + QStringList desktopNames() const; + quint32 position(const QVariant &desktop) const; + int desktopLayoutRows() const; + void requestActivate(const QVariant &desktop); + void requestCreateDesktop(quint32 position); + void requestRemoveDesktop(quint32 position); + +signals: + void currentDesktopChanged(); + void numberOfDesktopsChanged(); + void navigationWrappingAroundChanged(); + void desktopIdsChanged(); + void desktopNameChanged(quint32 position); + void desktopLayoutRowsChanged(); +}; diff --git a/panel/backends/wayland/plasma/lxqttaskbarbackendplasma.cpp b/panel/backends/wayland/plasma/lxqttaskbarbackendplasma.cpp new file mode 100644 index 000000000..4631c03d3 --- /dev/null +++ b/panel/backends/wayland/plasma/lxqttaskbarbackendplasma.cpp @@ -0,0 +1,725 @@ +#include "lxqttaskbarbackendplasma.h" + +#include "lxqttaskbarplasmawindowmanagment.h" +#include "lxqtplasmavirtualdesktop.h" + +#include +#include +#include + +auto findWindow(const std::vector>& windows, LXQtTaskBarPlasmaWindow *window) +{ + //TODO: use algorithms + auto end = windows.end(); + for(auto it = windows.begin(); it != end; it++) + { + if((*it).get() == window) + { + return it; + } + } + + return windows.end(); +} + +LXQtTaskbarPlasmaBackend::LXQtTaskbarPlasmaBackend(QObject *parent) : + ILXQtTaskbarAbstractBackend(parent) +{ + m_managment.reset(new LXQtTaskBarPlasmaWindowManagment); + m_workspaceInfo.reset(new LXQtPlasmaWaylandWorkspaceInfo); + + connect(m_managment.get(), &LXQtTaskBarPlasmaWindowManagment::windowCreated, this, [this](LXQtTaskBarPlasmaWindow *window) { + connect(window, &LXQtTaskBarPlasmaWindow::initialStateDone, this, [this, window] { + addWindow(window); + }); + }); + + // connect(m_managment.get(), &LXQtTaskBarPlasmaWindowManagment::stackingOrderChanged, + // this, [this](const QString &order) { + // // stackingOrder = order.split(QLatin1Char(';')); + // // for (const auto &window : std::as_const(windows)) { + // // this->dataChanged(window.get(), StackingOrder); + // // } + // }); + + connect(m_workspaceInfo.get(), &LXQtPlasmaWaylandWorkspaceInfo::currentDesktopChanged, this, + [this](){ + int idx = m_workspaceInfo->position(m_workspaceInfo->currentDesktop()); + idx += 1; // Make 1-based + emit currentWorkspaceChanged(idx); + }); + + connect(m_workspaceInfo.get(), &LXQtPlasmaWaylandWorkspaceInfo::numberOfDesktopsChanged, + this, &ILXQtTaskbarAbstractBackend::workspacesCountChanged); + + connect(m_workspaceInfo.get(), &LXQtPlasmaWaylandWorkspaceInfo::desktopNameChanged, + this, [this](int idx) { + emit workspaceNameChanged(idx + 1); // Make 1-based + }); +} + +bool LXQtTaskbarPlasmaBackend::supportsAction(WId windowId, LXQtTaskBarBackendAction action) const +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return false; + + LXQtTaskBarPlasmaWindow::state state; + + switch (action) + { + case LXQtTaskBarBackendAction::Move: + state = LXQtTaskBarPlasmaWindow::state::state_movable; + break; + + case LXQtTaskBarBackendAction::Resize: + state = LXQtTaskBarPlasmaWindow::state::state_resizable; + break; + + case LXQtTaskBarBackendAction::Maximize: + state = LXQtTaskBarPlasmaWindow::state::state_maximizable; + break; + + case LXQtTaskBarBackendAction::Minimize: + state = LXQtTaskBarPlasmaWindow::state::state_minimizable; + break; + + case LXQtTaskBarBackendAction::RollUp: + state = LXQtTaskBarPlasmaWindow::state::state_shadeable; + break; + + case LXQtTaskBarBackendAction::FullScreen: + state = LXQtTaskBarPlasmaWindow::state::state_fullscreenable; + break; + + default: + return false; + } + + return window->windowState.testFlag(state); +} + +bool LXQtTaskbarPlasmaBackend::reloadWindows() +{ + const QVector wids = getCurrentWindows(); + + // Force removal and re-adding + for(WId windowId : wids) + { + emit windowRemoved(windowId); + } + for(WId windowId : wids) + { + emit windowAdded(windowId); + } + + return true; +} + +QVector LXQtTaskbarPlasmaBackend::getCurrentWindows() const +{ + QVector wids; + wids.reserve(wids.size()); + + for(const std::unique_ptr& window : std::as_const(windows)) + { + if(window->acceptedInTaskBar) + wids << window->getWindowId(); + } + return wids; +} + +QString LXQtTaskbarPlasmaBackend::getWindowTitle(WId windowId) const +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return QString(); + + return window->title; +} + +bool LXQtTaskbarPlasmaBackend::applicationDemandsAttention(WId windowId) const +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return false; + + return window->windowState.testFlag(LXQtTaskBarPlasmaWindow::state::state_demands_attention) || transientsDemandingAttention.contains(window); +} + +QIcon LXQtTaskbarPlasmaBackend::getApplicationIcon(WId windowId, int devicePixels) const +{ + Q_UNUSED(devicePixels) + + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return QIcon(); + + return window->icon; +} + +QString LXQtTaskbarPlasmaBackend::getWindowClass(WId windowId) const +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return QString(); + return window->appId; +} + +LXQtTaskBarWindowLayer LXQtTaskbarPlasmaBackend::getWindowLayer(WId windowId) const +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return LXQtTaskBarWindowLayer::Normal; + + if(window->windowState.testFlag(LXQtTaskBarPlasmaWindow::state::state_keep_above)) + return LXQtTaskBarWindowLayer::KeepAbove; + + if(window->windowState.testFlag(LXQtTaskBarPlasmaWindow::state::state_keep_below)) + return LXQtTaskBarWindowLayer::KeepBelow; + + return LXQtTaskBarWindowLayer::Normal; +} + +bool LXQtTaskbarPlasmaBackend::setWindowLayer(WId windowId, LXQtTaskBarWindowLayer layer) +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return false; + + if(getWindowLayer(windowId) == layer) + return true; //TODO: make more efficient + + LXQtTaskBarPlasmaWindow::state plasmaState = LXQtTaskBarPlasmaWindow::state::state_keep_above; + switch (layer) + { + case LXQtTaskBarWindowLayer::Normal: + case LXQtTaskBarWindowLayer::KeepAbove: + break; + case LXQtTaskBarWindowLayer::KeepBelow: + plasmaState = LXQtTaskBarPlasmaWindow::state::state_keep_below; + break; + default: + return false; + } + + window->set_state(plasmaState, layer == LXQtTaskBarWindowLayer::Normal ? 0 : plasmaState); + return false; +} + +LXQtTaskBarWindowState LXQtTaskbarPlasmaBackend::getWindowState(WId windowId) const +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return LXQtTaskBarWindowState::Normal; + + if(window->windowState.testFlag(LXQtTaskBarPlasmaWindow::state::state_minimized)) + return LXQtTaskBarWindowState::Hidden; + if(window->windowState.testFlag(LXQtTaskBarPlasmaWindow::state::state_maximizable)) + return LXQtTaskBarWindowState::Maximized; + if(window->windowState.testFlag(LXQtTaskBarPlasmaWindow::state::state_shaded)) + return LXQtTaskBarWindowState::RolledUp; + if(window->windowState.testFlag(LXQtTaskBarPlasmaWindow::state::state_fullscreen)) + return LXQtTaskBarWindowState::FullScreen; + + return LXQtTaskBarWindowState::Normal; +} + +bool LXQtTaskbarPlasmaBackend::setWindowState(WId windowId, LXQtTaskBarWindowState state, bool set) +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return false; + + LXQtTaskBarPlasmaWindow::state plasmaState; + switch (state) + { + case LXQtTaskBarWindowState::Minimized: + { + plasmaState = LXQtTaskBarPlasmaWindow::state::state_minimized; + break; + } + case LXQtTaskBarWindowState::Maximized: + case LXQtTaskBarWindowState::MaximizedVertically: + case LXQtTaskBarWindowState::MaximizedHorizontally: + { + plasmaState = LXQtTaskBarPlasmaWindow::state::state_maximized; + break; + } + case LXQtTaskBarWindowState::Normal: + { + plasmaState = LXQtTaskBarPlasmaWindow::state::state_maximized; + set = !set; //TODO: correct + break; + } + case LXQtTaskBarWindowState::RolledUp: + { + plasmaState = LXQtTaskBarPlasmaWindow::state::state_shaded; + break; + } + default: + return false; + } + + window->set_state(plasmaState, set ? plasmaState : 0); + return true; +} + +bool LXQtTaskbarPlasmaBackend::isWindowActive(WId windowId) const +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return false; + + return activeWindow == window || window->windowState.testFlag(LXQtTaskBarPlasmaWindow::state::state_active); +} + +bool LXQtTaskbarPlasmaBackend::raiseWindow(WId windowId, bool onCurrentWorkSpace) +{ + Q_UNUSED(onCurrentWorkSpace) //TODO + + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return false; + + // Pull forward any transient demanding attention. + if (auto *transientDemandingAttention = transientsDemandingAttention.value(window)) + { + window = transientDemandingAttention; + } + else + { + // TODO Shouldn't KWin take care of that? + // Bringing a transient to the front usually brings its parent with it + // but focus is not handled properly. + // TODO take into account d->lastActivation instead + // of just taking the first one. + while (transients.key(window)) + { + window = transients.key(window); + } + } + + window->set_state(LXQtTaskBarPlasmaWindow::state::state_active, LXQtTaskBarPlasmaWindow::state::state_active); + return true; +} + +bool LXQtTaskbarPlasmaBackend::closeWindow(WId windowId) +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return false; + + window->close(); + return true; +} + +WId LXQtTaskbarPlasmaBackend::getActiveWindow() const +{ + if(activeWindow) + return activeWindow->getWindowId(); + return 0; +} + +int LXQtTaskbarPlasmaBackend::getWorkspacesCount() const +{ + return m_workspaceInfo->numberOfDesktops(); +} + +QString LXQtTaskbarPlasmaBackend::getWorkspaceName(int idx) const +{ + return m_workspaceInfo->getDesktopName(idx - 1); //Return to 0-based +} + +int LXQtTaskbarPlasmaBackend::getCurrentWorkspace() const +{ + if(!m_workspaceInfo->currentDesktop().isValid()) + return 0; + return m_workspaceInfo->position(m_workspaceInfo->currentDesktop()) + 1; // 1-based +} + +bool LXQtTaskbarPlasmaBackend::setCurrentWorkspace(int idx) +{ + QString id = m_workspaceInfo->getDesktopId(idx - 1); //Return to 0-based + if(id.isEmpty()) + return false; + m_workspaceInfo->requestActivate(id); + return true; +} + +int LXQtTaskbarPlasmaBackend::getWindowWorkspace(WId windowId) const +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return 0; + + // TODO: this protocol seems to allow multiple desktop for each window + // We do not support that yet + // Also from KDE Plasma task switch it's not clear how to actually put + // a window on multiple desktops (which is different from "All desktops") + QString id = window->virtualDesktops.value(0, QString()); + if(id.isEmpty()) + return 0; + + return m_workspaceInfo->position(id) + 1; //Make 1-based +} + +bool LXQtTaskbarPlasmaBackend::setWindowOnWorkspace(WId windowId, int idx) +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return false; + + // Prepare for future multiple virtual desktops per window + QList newDesktops; + + // Fill the list + newDesktops.append(m_workspaceInfo->getDesktopId(idx - 1)); //Return to 0-based + + // Keep only valid IDs + newDesktops.erase(std::remove_if(newDesktops.begin(), newDesktops.end(), + [](const QString& id) { return id.isEmpty(); }), + newDesktops.end()); + + // Add to new requested desktops + for(const QString& id : std::as_const(newDesktops)) + { + if(!window->virtualDesktops.contains(id)) + window->request_enter_virtual_desktop(id); + } + + // Remove from non-requested destops + const QList currentDesktops = window->virtualDesktops; + for(const QString& id : std::as_const(currentDesktops)) + { + if(!newDesktops.contains(id)) + window->request_leave_virtual_desktop(id); + } + + return true; +} + +void LXQtTaskbarPlasmaBackend::moveApplicationToPrevNextMonitor(WId windowId, bool next, bool raiseOnCurrentDesktop) +{ + +} + +bool LXQtTaskbarPlasmaBackend::isWindowOnScreen(QScreen *screen, WId windowId) const +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return false; + + return screen->geometry().intersects(window->geometry); +} + +void LXQtTaskbarPlasmaBackend::moveApplication(WId windowId) +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return; + + window->set_state(LXQtTaskBarPlasmaWindow::state::state_active, LXQtTaskBarPlasmaWindow::state::state_active); + window->request_move(); +} + +void LXQtTaskbarPlasmaBackend::resizeApplication(WId windowId) +{ + LXQtTaskBarPlasmaWindow *window = getWindow(windowId); + if(!window) + return; + + window->set_state(LXQtTaskBarPlasmaWindow::state::state_active, LXQtTaskBarPlasmaWindow::state::state_active); + window->request_resize(); +} + +void LXQtTaskbarPlasmaBackend::refreshIconGeometry(WId windowId, const QRect &geom) +{ + +} + +bool LXQtTaskbarPlasmaBackend::isAreaOverlapped(const QRect &area) const +{ + for(auto &window : std::as_const(windows)) + { + if(window->geometry.intersects(area)) + return true; + } + return false; +} + +bool LXQtTaskbarPlasmaBackend::isShowingDesktop() const +{ + return m_managment->isActive() ? m_managment->isShowingDesktop() : false; +} + +bool LXQtTaskbarPlasmaBackend::showDesktop(bool value) +{ + if(!m_managment->isActive()) + return false; + + enum LXQtTaskBarPlasmaWindowManagment::show_desktop flag_; + if(value) + flag_ = LXQtTaskBarPlasmaWindowManagment::show_desktop::show_desktop_enabled; + else + flag_ = LXQtTaskBarPlasmaWindowManagment::show_desktop::show_desktop_disabled; + + m_managment->show_desktop(flag_); + return true; +} + +void LXQtTaskbarPlasmaBackend::addWindow(LXQtTaskBarPlasmaWindow *window) +{ + if (findWindow(windows, window) != windows.end() || transients.contains(window)) + { + return; + } + + auto removeWindow = [window, this] + { + auto it = findWindow(windows, window); + if (it != windows.end()) + { + if(window->acceptedInTaskBar) + emit windowRemoved(window->getWindowId()); + windows.erase(it); + transientsDemandingAttention.remove(window); + lastActivated.remove(window); + } + else + { + // Could be a transient. + // Removing a transient might change the demands attention state of the leader. + if (transients.remove(window)) + { + if (LXQtTaskBarPlasmaWindow *leader = transientsDemandingAttention.key(window)) { + transientsDemandingAttention.remove(leader, window); + emit windowPropertyChanged(leader->getWindowId(), int(LXQtTaskBarWindowProperty::Urgency)); + } + } + } + + if (activeWindow == window) + { + activeWindow = nullptr; + emit activeWindowChanged(0); + } + }; + + connect(window, &LXQtTaskBarPlasmaWindow::unmapped, this, removeWindow); + + connect(window, &LXQtTaskBarPlasmaWindow::titleChanged, this, [window, this] { + updateWindowAcceptance(window); + if(window->acceptedInTaskBar) + emit windowPropertyChanged(window->getWindowId(), int(LXQtTaskBarWindowProperty::Title)); + }); + + connect(window, &LXQtTaskBarPlasmaWindow::iconChanged, this, [window, this] { + updateWindowAcceptance(window); + if(window->acceptedInTaskBar) + emit windowPropertyChanged(window->getWindowId(), int(LXQtTaskBarWindowProperty::Icon)); + }); + + connect(window, &LXQtTaskBarPlasmaWindow::geometryChanged, this, [window, this] { + updateWindowAcceptance(window); + if(window->acceptedInTaskBar) + emit windowPropertyChanged(window->getWindowId(), int(LXQtTaskBarWindowProperty::Geometry)); + }); + + connect(window, &LXQtTaskBarPlasmaWindow::appIdChanged, this, [window, this] { + emit windowPropertyChanged(window->getWindowId(), int(LXQtTaskBarWindowProperty::WindowClass)); + }); + + if (window->windowState & LXQtTaskBarPlasmaWindow::state::state_active) { + LXQtTaskBarPlasmaWindow *effectiveActive = window; + while (effectiveActive->parentWindow) { + effectiveActive = effectiveActive->parentWindow; + } + + lastActivated[effectiveActive] = QTime::currentTime(); + activeWindow = effectiveActive; + } + + connect(window, &LXQtTaskBarPlasmaWindow::activeChanged, this, [window, this] { + const bool active = window->windowState & LXQtTaskBarPlasmaWindow::state::state_active; + + LXQtTaskBarPlasmaWindow *effectiveWindow = window; + + while (effectiveWindow->parentWindow) + { + effectiveWindow = effectiveWindow->parentWindow; + } + + if (active) + { + lastActivated[effectiveWindow] = QTime::currentTime(); + + if (activeWindow != effectiveWindow) + { + activeWindow = effectiveWindow; + emit activeWindowChanged(activeWindow->getWindowId()); + } + } + else + { + if (activeWindow == effectiveWindow) + { + activeWindow = nullptr; + emit activeWindowChanged(0); + } + } + }); + + connect(window, &LXQtTaskBarPlasmaWindow::parentWindowChanged, this, [window, this] { + LXQtTaskBarPlasmaWindow *leader = window->parentWindow.data(); + + // Migrate demanding attention to new leader. + if (window->windowState.testFlag(LXQtTaskBarPlasmaWindow::state::state_demands_attention)) + { + if (auto *oldLeader = transientsDemandingAttention.key(window)) + { + if (window->parentWindow != oldLeader) + { + transientsDemandingAttention.remove(oldLeader, window); + transientsDemandingAttention.insert(leader, window); + emit windowPropertyChanged(oldLeader->getWindowId(), int(LXQtTaskBarWindowProperty::Urgency)); + emit windowPropertyChanged(leader->getWindowId(), int(LXQtTaskBarWindowProperty::Urgency)); + } + } + } + + if (transients.remove(window)) + { + if (leader) + { + // leader change. + transients.insert(window, leader); + } + else + { + // lost a leader, add to regular windows list. + Q_ASSERT(findWindow(windows, window) == windows.end()); + + windows.emplace_back(window); + } + } + else if (leader) + { + // gained a leader, remove from regular windows list. + auto it = findWindow(windows, window); + Q_ASSERT(it != windows.end()); + + windows.erase(it); + lastActivated.remove(window); + } + }); + + auto stateChanged = [window, this] { + updateWindowAcceptance(window); + if(window->acceptedInTaskBar) + emit windowPropertyChanged(window->getWindowId(), int(LXQtTaskBarWindowProperty::State)); + }; + + connect(window, &LXQtTaskBarPlasmaWindow::fullscreenChanged, this, stateChanged); + + connect(window, &LXQtTaskBarPlasmaWindow::maximizedChanged, this, stateChanged); + + connect(window, &LXQtTaskBarPlasmaWindow::minimizedChanged, this, stateChanged); + + connect(window, &LXQtTaskBarPlasmaWindow::shadedChanged, this, stateChanged); + + auto workspaceChanged = [window, this] { + updateWindowAcceptance(window); + if(window->acceptedInTaskBar) + emit windowPropertyChanged(window->getWindowId(), int(LXQtTaskBarWindowProperty::Workspace)); + }; + + connect(window, &LXQtTaskBarPlasmaWindow::virtualDesktopEntered, this, workspaceChanged); + connect(window, &LXQtTaskBarPlasmaWindow::virtualDesktopLeft, this, workspaceChanged); + + connect(window, &LXQtTaskBarPlasmaWindow::demandsAttentionChanged, this, [window, this] { + // Changes to a transient's state might change demands attention state for leader. + if (auto *leader = transients.value(window)) + { + if (window->windowState.testFlag(LXQtTaskBarPlasmaWindow::state::state_demands_attention)) + { + if (!transientsDemandingAttention.values(leader).contains(window)) + { + transientsDemandingAttention.insert(leader, window); + emit windowPropertyChanged(leader->getWindowId(), int(LXQtTaskBarWindowProperty::Urgency)); + } + } + else if (transientsDemandingAttention.remove(window)) + { + emit windowPropertyChanged(leader->getWindowId(), int(LXQtTaskBarWindowProperty::Urgency)); + } + } + else + { + emit windowPropertyChanged(window->getWindowId(), int(LXQtTaskBarWindowProperty::Urgency)); + } + }); + + connect(window, &LXQtTaskBarPlasmaWindow::skipTaskbarChanged, this, [window, this] { + updateWindowAcceptance(window); + }); + + // QObject::connect(window, &PlasmaWindow::applicationMenuChanged, q, [window, this] { + // this->dataChanged(window, QList{ApplicationMenuServiceName, ApplicationMenuObjectPath}); + // }); + + // Handle transient. + if (LXQtTaskBarPlasmaWindow *leader = window->parentWindow.data()) + { + transients.insert(window, leader); + + // Update demands attention state for leader. + if (window->windowState.testFlag(LXQtTaskBarPlasmaWindow::state::state_demands_attention)) + { + transientsDemandingAttention.insert(leader, window); + if(leader->acceptedInTaskBar) + emit windowPropertyChanged(leader->getWindowId(), int(LXQtTaskBarWindowProperty::Urgency)); + } + } + else + { + windows.emplace_back(window); + updateWindowAcceptance(window); + } +} + +bool LXQtTaskbarPlasmaBackend::acceptWindow(LXQtTaskBarPlasmaWindow *window) const +{ + if(window->windowState.testFlag(LXQtTaskBarPlasmaWindow::state::state_skiptaskbar)) + return false; + + if(transients.contains(window)) + return false; + + return true; +} + +void LXQtTaskbarPlasmaBackend::updateWindowAcceptance(LXQtTaskBarPlasmaWindow *window) +{ + if(!window->acceptedInTaskBar && acceptWindow(window)) + { + window->acceptedInTaskBar = true; + emit windowAdded(window->getWindowId()); + } + else if(window->acceptedInTaskBar && !acceptWindow(window)) + { + window->acceptedInTaskBar = false; + emit windowRemoved(window->getWindowId()); + } +} + +LXQtTaskBarPlasmaWindow *LXQtTaskbarPlasmaBackend::getWindow(WId windowId) const +{ + for(auto &window : std::as_const(windows)) + { + if(window->getWindowId() == windowId) + return window.get(); + } + + return nullptr; +} diff --git a/panel/backends/wayland/plasma/lxqttaskbarbackendplasma.h b/panel/backends/wayland/plasma/lxqttaskbarbackendplasma.h new file mode 100644 index 000000000..f47d724c8 --- /dev/null +++ b/panel/backends/wayland/plasma/lxqttaskbarbackendplasma.h @@ -0,0 +1,92 @@ +#pragma once + +#include "../../ilxqttaskbarabstractbackend.h" + +#include +#include +#include + +class LXQtTaskBarPlasmaWindow; +class LXQtTaskBarPlasmaWindowManagment; +class LXQtPlasmaWaylandWorkspaceInfo; + + +class LXQtTaskbarPlasmaBackend : public ILXQtTaskbarAbstractBackend +{ + Q_OBJECT + +public: + explicit LXQtTaskbarPlasmaBackend(QObject *parent = nullptr); + + // Backend + virtual bool supportsAction(WId windowId, LXQtTaskBarBackendAction action) const override; + + // Windows + virtual bool reloadWindows() override; + + virtual QVector getCurrentWindows() const override; + virtual QString getWindowTitle(WId windowId) const override; + virtual bool applicationDemandsAttention(WId windowId) const override; + virtual QIcon getApplicationIcon(WId windowId, int devicePixels) const override; + virtual QString getWindowClass(WId windowId) const override; + + virtual LXQtTaskBarWindowLayer getWindowLayer(WId windowId) const override; + virtual bool setWindowLayer(WId windowId, LXQtTaskBarWindowLayer layer) override; + + virtual LXQtTaskBarWindowState getWindowState(WId windowId) const override; + virtual bool setWindowState(WId windowId, LXQtTaskBarWindowState state, bool set) override; + + virtual bool isWindowActive(WId windowId) const override; + virtual bool raiseWindow(WId windowId, bool onCurrentWorkSpace) override; + + virtual bool closeWindow(WId windowId) override; + + virtual WId getActiveWindow() const override; + + // Workspaces + virtual int getWorkspacesCount() const override; + virtual QString getWorkspaceName(int idx) const override; + + virtual int getCurrentWorkspace() const override; + virtual bool setCurrentWorkspace(int idx) override; + + virtual int getWindowWorkspace(WId windowId) const override; + virtual bool setWindowOnWorkspace(WId windowId, int idx) override; + + virtual void moveApplicationToPrevNextMonitor(WId windowId, bool next, bool raiseOnCurrentDesktop) override; + + virtual bool isWindowOnScreen(QScreen *screen, WId windowId) const override; + + // X11 Specific + virtual void moveApplication(WId windowId) override; + virtual void resizeApplication(WId windowId) override; + + virtual void refreshIconGeometry(WId windowId, const QRect &geom) override; + + // Panel internal + virtual bool isAreaOverlapped(const QRect& area) const override; + + // Show Destop + virtual bool isShowingDesktop() const override; + virtual bool showDesktop(bool value) override; + +private: + void addWindow(LXQtTaskBarPlasmaWindow *window); + bool acceptWindow(LXQtTaskBarPlasmaWindow *window) const; + void updateWindowAcceptance(LXQtTaskBarPlasmaWindow *window); + +private: + LXQtTaskBarPlasmaWindow *getWindow(WId windowId) const; + + std::unique_ptr m_workspaceInfo; + + std::unique_ptr m_managment; + + QHash lastActivated; + LXQtTaskBarPlasmaWindow *activeWindow = nullptr; + std::vector> windows; + // key=transient child, value=leader + QHash transients; + // key=leader, values=transient children + QMultiHash transientsDemandingAttention; +}; diff --git a/panel/backends/wayland/plasma/lxqttaskbarplasmawindowmanagment.cpp b/panel/backends/wayland/plasma/lxqttaskbarplasmawindowmanagment.cpp new file mode 100644 index 000000000..b4339dbb0 --- /dev/null +++ b/panel/backends/wayland/plasma/lxqttaskbarplasmawindowmanagment.cpp @@ -0,0 +1,1149 @@ +#include "lxqttaskbarplasmawindowmanagment.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + + +// QUuid WaylandTasksModel::Private::uuid = QUuid::createUuid(); + +// WaylandTasksModel::Private::Private(WaylandTasksModel *q) +// : q(q) +// { +// } + +// void WaylandTasksModel::Private::init() +// { +// auto clearCacheAndRefresh = [this] { +// if (windows.empty()) { +// return; +// } + +// appDataCache.clear(); + +// // Emit changes of all roles satisfied from app data cache. +// Q_EMIT q->dataChanged(q->index(0, 0), +// q->index(windows.size() - 1, 0), +// QList{Qt::DecorationRole, +// AbstractTasksModel::AppId, +// AbstractTasksModel::AppName, +// AbstractTasksModel::GenericName, +// AbstractTasksModel::LauncherUrl, +// AbstractTasksModel::LauncherUrlWithoutIcon, +// AbstractTasksModel::CanLaunchNewInstance, +// AbstractTasksModel::SkipTaskbar}); +// }; + +// rulesConfig = KSharedConfig::openConfig(QStringLiteral("taskmanagerrulesrc")); +// configWatcher = new KDirWatch(q); + +// for (const QString &location : QStandardPaths::standardLocations(QStandardPaths::ConfigLocation)) { +// configWatcher->addFile(location + QLatin1String("/taskmanagerrulesrc")); +// } + +// auto rulesConfigChange = [this, clearCacheAndRefresh] { +// rulesConfig->reparseConfiguration(); +// clearCacheAndRefresh(); +// }; + +// QObject::connect(configWatcher, &KDirWatch::dirty, rulesConfigChange); +// QObject::connect(configWatcher, &KDirWatch::created, rulesConfigChange); +// QObject::connect(configWatcher, &KDirWatch::deleted, rulesConfigChange); + +// virtualDesktopInfo = new VirtualDesktopInfo(q); + +// initWayland(); +// } + +// void WaylandTasksModel::Private::initWayland() +// { +// if (!KWindowSystem::isPlatformWayland()) { +// return; +// } + +// windowManagement = std::make_unique(); + +// QObject::connect(windowManagement.get(), &PlasmaWindowManagement::activeChanged, q, [this] { +// q->beginResetModel(); +// windows.clear(); +// q->endResetModel(); +// }); + +// QObject::connect(windowManagement.get(), &PlasmaWindowManagement::windowCreated, q, [this](PlasmaWindow *window) { +// connect(window, &PlasmaWindow::initialStateDone, q, [this, window] { +// addWindow(window); +// }); +// }); + +// QObject::connect(windowManagement.get(), &PlasmaWindowManagement::stackingOrderChanged, q, [this](const QString &order) { +// stackingOrder = order.split(QLatin1Char(';')); +// for (const auto &window : std::as_const(windows)) { +// this->dataChanged(window.get(), StackingOrder); +// } +// }); +// } + +// auto WaylandTasksModel::Private::findWindow(PlasmaWindow *window) const +// { +// return std::find_if(windows.begin(), windows.end(), [window](const std::unique_ptr &candidate) { +// return candidate.get() == window; +// }); +// } + +// void WaylandTasksModel::Private::addWindow(PlasmaWindow *window) +// { +// if (findWindow(window) != windows.end() || transients.contains(window)) { +// return; +// } + +// auto removeWindow = [window, this] { +// auto it = findWindow(window); +// if (it != windows.end()) { +// const int row = it - windows.begin(); +// q->beginRemoveRows(QModelIndex(), row, row); +// windows.erase(it); +// transientsDemandingAttention.remove(window); +// appDataCache.remove(window); +// lastActivated.remove(window); +// q->endRemoveRows(); +// } else { // Could be a transient. +// // Removing a transient might change the demands attention state of the leader. +// if (transients.remove(window)) { +// if (PlasmaWindow *leader = transientsDemandingAttention.key(window)) { +// transientsDemandingAttention.remove(leader, window); +// dataChanged(leader, QVector{IsDemandingAttention}); +// } +// } +// } + +// if (activeWindow == window) { +// activeWindow = nullptr; +// } +// }; + +// QObject::connect(window, &PlasmaWindow::unmapped, q, removeWindow); + +// QObject::connect(window, &PlasmaWindow::titleChanged, q, [window, this] { +// this->dataChanged(window, Qt::DisplayRole); +// }); + +// QObject::connect(window, &PlasmaWindow::iconChanged, q, [window, this] { +// // The icon in the AppData struct might come from PlasmaWindow if it wasn't +// // filled in by windowUrlFromMetadata+appDataFromUrl. +// // TODO: Don't evict the cache unnecessarily if this isn't the case. As icons +// // are currently very static on Wayland, this eviction is unlikely to happen +// // frequently as of now. +// appDataCache.remove(window); +// this->dataChanged(window, Qt::DecorationRole); +// }); + +// QObject::connect(window, &PlasmaWindow::appIdChanged, q, [window, this] { +// // The AppData struct in the cache is derived from this and needs +// // to be evicted in favor of a fresh struct based on the changed +// // window metadata. +// appDataCache.remove(window); + +// // Refresh roles satisfied from the app data cache. +// this->dataChanged(window, +// QList{Qt::DecorationRole, AppId, AppName, GenericName, LauncherUrl, LauncherUrlWithoutIcon, SkipTaskbar, CanLaunchNewInstance}); +// }); + +// if (window->windowState & PlasmaWindow::state::state_active) { +// PlasmaWindow *effectiveActive = window; +// while (effectiveActive->parentWindow) { +// effectiveActive = effectiveActive->parentWindow; +// } + +// lastActivated[effectiveActive] = QTime::currentTime(); +// activeWindow = effectiveActive; +// } + +// QObject::connect(window, &PlasmaWindow::activeChanged, q, [window, this] { +// const bool active = window->windowState & PlasmaWindow::state::state_active; + +// PlasmaWindow *effectiveWindow = window; + +// while (effectiveWindow->parentWindow) { +// effectiveWindow = effectiveWindow->parentWindow; +// } + +// if (active) { +// lastActivated[effectiveWindow] = QTime::currentTime(); + +// if (activeWindow != effectiveWindow) { +// activeWindow = effectiveWindow; +// this->dataChanged(effectiveWindow, IsActive); +// } +// } else { +// if (activeWindow == effectiveWindow) { +// activeWindow = nullptr; +// this->dataChanged(effectiveWindow, IsActive); +// } +// } +// }); + +// QObject::connect(window, &PlasmaWindow::parentWindowChanged, q, [window, this] { +// PlasmaWindow *leader = window->parentWindow.data(); + +// // Migrate demanding attention to new leader. +// if (window->windowState.testFlag(PlasmaWindow::state::state_demands_attention)) { +// if (auto *oldLeader = transientsDemandingAttention.key(window)) { +// if (window->parentWindow != oldLeader) { +// transientsDemandingAttention.remove(oldLeader, window); +// transientsDemandingAttention.insert(leader, window); +// dataChanged(oldLeader, QVector{IsDemandingAttention}); +// dataChanged(leader, QVector{IsDemandingAttention}); +// } +// } +// } + +// if (transients.remove(window)) { +// if (leader) { // leader change. +// transients.insert(window, leader); +// } else { // lost a leader, add to regular windows list. +// Q_ASSERT(findWindow(window) == windows.end()); + +// const int count = windows.size(); +// q->beginInsertRows(QModelIndex(), count, count); +// windows.emplace_back(window); +// q->endInsertRows(); +// } +// } else if (leader) { // gained a leader, remove from regular windows list. +// auto it = findWindow(window); +// Q_ASSERT(it != windows.end()); + +// const int row = it - windows.begin(); +// q->beginRemoveRows(QModelIndex(), row, row); +// windows.erase(it); +// appDataCache.remove(window); +// lastActivated.remove(window); +// q->endRemoveRows(); +// } +// }); + +// QObject::connect(window, &PlasmaWindow::closeableChanged, q, [window, this] { +// this->dataChanged(window, IsClosable); +// }); + +// QObject::connect(window, &PlasmaWindow::movableChanged, q, [window, this] { +// this->dataChanged(window, IsMovable); +// }); + +// QObject::connect(window, &PlasmaWindow::resizableChanged, q, [window, this] { +// this->dataChanged(window, IsResizable); +// }); + +// QObject::connect(window, &PlasmaWindow::fullscreenableChanged, q, [window, this] { +// this->dataChanged(window, IsFullScreenable); +// }); + +// QObject::connect(window, &PlasmaWindow::fullscreenChanged, q, [window, this] { +// this->dataChanged(window, IsFullScreen); +// }); + +// QObject::connect(window, &PlasmaWindow::maximizeableChanged, q, [window, this] { +// this->dataChanged(window, IsMaximizable); +// }); + +// QObject::connect(window, &PlasmaWindow::maximizedChanged, q, [window, this] { +// this->dataChanged(window, IsMaximized); +// }); + +// QObject::connect(window, &PlasmaWindow::minimizeableChanged, q, [window, this] { +// this->dataChanged(window, IsMinimizable); +// }); + +// QObject::connect(window, &PlasmaWindow::minimizedChanged, q, [window, this] { +// this->dataChanged(window, IsMinimized); +// }); + +// QObject::connect(window, &PlasmaWindow::keepAboveChanged, q, [window, this] { +// this->dataChanged(window, IsKeepAbove); +// }); + +// QObject::connect(window, &PlasmaWindow::keepBelowChanged, q, [window, this] { +// this->dataChanged(window, IsKeepBelow); +// }); + +// QObject::connect(window, &PlasmaWindow::shadeableChanged, q, [window, this] { +// this->dataChanged(window, IsShadeable); +// }); + +// QObject::connect(window, &PlasmaWindow::virtualDesktopChangeableChanged, q, [window, this] { +// this->dataChanged(window, IsVirtualDesktopsChangeable); +// }); + +// QObject::connect(window, &PlasmaWindow::virtualDesktopEntered, q, [window, this] { +// this->dataChanged(window, VirtualDesktops); + +// // If the count has changed from 0, the window may no longer be on all virtual +// // desktops. +// if (window->virtualDesktops.count() > 0) { +// this->dataChanged(window, IsOnAllVirtualDesktops); +// } +// }); + +// QObject::connect(window, &PlasmaWindow::virtualDesktopLeft, q, [window, this] { +// this->dataChanged(window, VirtualDesktops); + +// // If the count has changed to 0, the window is now on all virtual desktops. +// if (window->virtualDesktops.count() == 0) { +// this->dataChanged(window, IsOnAllVirtualDesktops); +// } +// }); + +// QObject::connect(window, &PlasmaWindow::geometryChanged, q, [window, this] { +// this->dataChanged(window, QList{Geometry, ScreenGeometry}); +// }); + +// QObject::connect(window, &PlasmaWindow::demandsAttentionChanged, q, [window, this] { +// // Changes to a transient's state might change demands attention state for leader. +// if (auto *leader = transients.value(window)) { +// if (window->windowState.testFlag(PlasmaWindow::state::state_demands_attention)) { +// if (!transientsDemandingAttention.values(leader).contains(window)) { +// transientsDemandingAttention.insert(leader, window); +// this->dataChanged(leader, QVector{IsDemandingAttention}); +// } +// } else if (transientsDemandingAttention.remove(window)) { +// this->dataChanged(leader, QVector{IsDemandingAttention}); +// } +// } else { +// this->dataChanged(window, QVector{IsDemandingAttention}); +// } +// }); + +// QObject::connect(window, &PlasmaWindow::skipTaskbarChanged, q, [window, this] { +// this->dataChanged(window, SkipTaskbar); +// }); + +// QObject::connect(window, &PlasmaWindow::applicationMenuChanged, q, [window, this] { +// this->dataChanged(window, QList{ApplicationMenuServiceName, ApplicationMenuObjectPath}); +// }); + +// QObject::connect(window, &PlasmaWindow::activitiesChanged, q, [window, this] { +// this->dataChanged(window, Activities); +// }); + +// // Handle transient. +// if (PlasmaWindow *leader = window->parentWindow.data()) { +// transients.insert(window, leader); + +// // Update demands attention state for leader. +// if (window->windowState.testFlag(PlasmaWindow::state::state_demands_attention)) { +// transientsDemandingAttention.insert(leader, window); +// dataChanged(leader, QVector{IsDemandingAttention}); +// } +// } else { +// const int count = windows.size(); + +// q->beginInsertRows(QModelIndex(), count, count); + +// windows.emplace_back(window); + +// q->endInsertRows(); +// } +// } + +// const AppData &WaylandTasksModel::Private::appData(PlasmaWindow *window) +// { +// static_assert(!std::is_trivially_copy_assignable_v); +// if (auto it = appDataCache.constFind(window); it != appDataCache.constEnd()) { +// return *it; +// } + +// return *appDataCache.emplace(window, appDataFromUrl(windowUrlFromMetadata(window->appId, window->pid, rulesConfig, window->resourceName))); +// } + +// QIcon WaylandTasksModel::Private::icon(PlasmaWindow *window) +// { +// const AppData &app = appData(window); + +// if (!app.icon.isNull()) { +// return app.icon; +// } + +// appDataCache[window].icon = window->icon; + +// return window->icon; +// } + +// QString WaylandTasksModel::Private::mimeType() +// { +// // Use a unique format id to make this intentionally useless for +// // cross-process DND. +// return QStringLiteral("windowsystem/winid+") + uuid.toString(); +// } + +// QString WaylandTasksModel::Private::groupMimeType() +// { +// // Use a unique format id to make this intentionally useless for +// // cross-process DND. +// return QStringLiteral("windowsystem/multiple-winids+") + uuid.toString(); +// } + +// void WaylandTasksModel::Private::dataChanged(PlasmaWindow *window, int role) +// { +// auto it = findWindow(window); +// if (it == windows.end()) { +// return; +// } +// QModelIndex idx = q->index(it - windows.begin()); +// Q_EMIT q->dataChanged(idx, idx, QList{role}); +// } + +// void WaylandTasksModel::Private::dataChanged(PlasmaWindow *window, const QList &roles) +// { +// auto it = findWindow(window); +// if (it == windows.end()) { +// return; +// } +// QModelIndex idx = q->index(it - windows.begin()); +// Q_EMIT q->dataChanged(idx, idx, roles); +// } + +// WaylandTasksModel::WaylandTasksModel(QObject *parent) +// : AbstractWindowTasksModel(parent) +// , d(new Private(this)) +// { +// d->init(); +// } + +// WaylandTasksModel::~WaylandTasksModel() = default; + +// QVariant WaylandTasksModel::data(const QModelIndex &index, int role) const +// { +// // Note: when index is valid, its row >= 0, so casting to unsigned is safe +// if (!index.isValid() || static_cast(index.row()) >= d->windows.size()) { +// return QVariant(); +// } + +// PlasmaWindow *window = d->windows.at(index.row()).get(); + +// if (role == Qt::DisplayRole) { +// return window->title; +// } else if (role == Qt::DecorationRole) { +// return d->icon(window); +// } else if (role == AppId) { +// const QString &id = d->appData(window).id; + +// if (id.isEmpty()) { +// return window->appId; +// } else { +// return id; +// } +// } else if (role == AppName) { +// return d->appData(window).name; +// } else if (role == GenericName) { +// return d->appData(window).genericName; +// } else if (role == LauncherUrl || role == LauncherUrlWithoutIcon) { +// return d->appData(window).url; +// } else if (role == WinIdList) { +// return QVariantList{window->uuid}; +// } else if (role == MimeType) { +// return d->mimeType(); +// } else if (role == MimeData) { +// return window->uuid; +// } else if (role == IsWindow) { +// return true; +// } else if (role == IsActive) { +// return (window == d->activeWindow); +// } else if (role == IsClosable) { +// return window->windowState.testFlag(PlasmaWindow::state::state_closeable); +// } else if (role == IsMovable) { +// return window->windowState.testFlag(PlasmaWindow::state::state_movable); +// } else if (role == IsResizable) { +// return window->windowState.testFlag(PlasmaWindow::state::state_resizable); +// } else if (role == IsMaximizable) { +// return window->windowState.testFlag(PlasmaWindow::state::state_maximizable); +// } else if (role == IsMaximized) { +// return window->windowState.testFlag(PlasmaWindow::state::state_maximized); +// } else if (role == IsMinimizable) { +// return window->windowState.testFlag(PlasmaWindow::state::state_minimizable); +// } else if (role == IsMinimized || role == IsHidden) { +// return window->windowState.testFlag(PlasmaWindow::state::state_minimized); +// } else if (role == IsKeepAbove) { +// return window->windowState.testFlag(PlasmaWindow::state::state_keep_above); +// } else if (role == IsKeepBelow) { +// return window->windowState.testFlag(PlasmaWindow::state::state_keep_below); +// } else if (role == IsFullScreenable) { +// return window->windowState.testFlag(PlasmaWindow::state::state_fullscreenable); +// } else if (role == IsFullScreen) { +// return window->windowState.testFlag(PlasmaWindow::state::state_fullscreen); +// } else if (role == IsShadeable) { +// return window->windowState.testFlag(PlasmaWindow::state::state_shadeable); +// } else if (role == IsShaded) { +// return window->windowState.testFlag(PlasmaWindow::state::state_shaded); +// } else if (role == IsVirtualDesktopsChangeable) { +// return window->windowState.testFlag(PlasmaWindow::state::state_virtual_desktop_changeable); +// } else if (role == VirtualDesktops) { +// return window->virtualDesktops; +// } else if (role == IsOnAllVirtualDesktops) { +// return window->virtualDesktops.isEmpty(); +// } else if (role == Geometry) { +// return window->geometry; +// } else if (role == ScreenGeometry) { +// return screenGeometry(window->geometry.center()); +// } else if (role == Activities) { +// return window->activities; +// } else if (role == IsDemandingAttention) { +// return window->windowState.testFlag(PlasmaWindow::state::state_demands_attention) || d->transientsDemandingAttention.contains(window); +// } else if (role == SkipTaskbar) { +// return window->windowState.testFlag(PlasmaWindow::state::state_skiptaskbar) || d->appData(window).skipTaskbar; +// } else if (role == SkipPager) { +// // FIXME Implement. +// } else if (role == AppPid) { +// return window->pid; +// } else if (role == StackingOrder) { +// return d->stackingOrder.indexOf(window->uuid); +// } else if (role == LastActivated) { +// if (d->lastActivated.contains(window)) { +// return d->lastActivated.value(window); +// } +// } else if (role == ApplicationMenuObjectPath) { +// return window->applicationMenuObjectPath; +// } else if (role == ApplicationMenuServiceName) { +// return window->applicationMenuService; +// } else if (role == CanLaunchNewInstance) { +// return canLauchNewInstance(d->appData(window)); +// } + +// return AbstractTasksModel::data(index, role); +// } + +// int WaylandTasksModel::rowCount(const QModelIndex &parent) const +// { +// return parent.isValid() ? 0 : d->windows.size(); +// } + +// QModelIndex WaylandTasksModel::index(int row, int column, const QModelIndex &parent) const +// { +// return hasIndex(row, column, parent) ? createIndex(row, column, d->windows.at(row).get()) : QModelIndex(); +// } + +// void WaylandTasksModel::requestActivate(const QModelIndex &index) +// { +// if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::DoNotUseParent)) { +// return; +// } + +// PlasmaWindow *window = d->windows.at(index.row()).get(); + +// // Pull forward any transient demanding attention. +// if (auto *transientDemandingAttention = d->transientsDemandingAttention.value(window)) { +// window = transientDemandingAttention; +// } else { +// // TODO Shouldn't KWin take care of that? +// // Bringing a transient to the front usually brings its parent with it +// // but focus is not handled properly. +// // TODO take into account d->lastActivation instead +// // of just taking the first one. +// while (d->transients.key(window)) { +// window = d->transients.key(window); +// } +// } + +// window->set_state(PlasmaWindow::state::state_active, PlasmaWindow::state::state_active); +// } + +// void WaylandTasksModel::requestNewInstance(const QModelIndex &index) +// { +// if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::DoNotUseParent)) { +// return; +// } + +// runApp(d->appData(d->windows.at(index.row()).get())); +// } + +// void WaylandTasksModel::requestOpenUrls(const QModelIndex &index, const QList &urls) +// { +// if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::DoNotUseParent) || urls.isEmpty()) { +// return; +// } + +// runApp(d->appData(d->windows.at(index.row()).get()), urls); +// } + +// void WaylandTasksModel::requestClose(const QModelIndex &index) +// { +// if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::DoNotUseParent)) { +// return; +// } + +// d->windows.at(index.row())->close(); +// } + +// void WaylandTasksModel::requestMove(const QModelIndex &index) +// { +// if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::DoNotUseParent)) { +// return; +// } + +// auto &window = d->windows.at(index.row()); + +// window->set_state(PlasmaWindow::state::state_active, PlasmaWindow::state::state_active); +// window->request_move(); +// } + +// void WaylandTasksModel::requestResize(const QModelIndex &index) +// { +// if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::DoNotUseParent)) { +// return; +// } + +// auto &window = d->windows.at(index.row()); + +// window->set_state(PlasmaWindow::state::state_active, PlasmaWindow::state::state_active); +// window->request_resize(); +// } + +// void WaylandTasksModel::requestToggleMinimized(const QModelIndex &index) +// { +// if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::DoNotUseParent)) { +// return; +// } + +// auto &window = d->windows.at(index.row()); + +// if (window->windowState & PlasmaWindow::state::state_minimized) { +// window->set_state(PlasmaWindow::state::state_minimized, 0); +// } else { +// window->set_state(PlasmaWindow::state::state_minimized, PlasmaWindow::state::state_minimized); +// } +// } + +// void WaylandTasksModel::requestToggleMaximized(const QModelIndex &index) +// { +// if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::DoNotUseParent)) { +// return; +// } + +// auto &window = d->windows.at(index.row()); + +// if (window->windowState & PlasmaWindow::state::state_maximized) { +// window->set_state(PlasmaWindow::state::state_maximized | PlasmaWindow::state::state_active, PlasmaWindow::state::state_active); +// } else { +// window->set_state(PlasmaWindow::state::state_maximized | PlasmaWindow::state::state_active, +// PlasmaWindow::state::state_maximized | PlasmaWindow::state::state_active); +// } +// } + +// void WaylandTasksModel::requestToggleKeepAbove(const QModelIndex &index) +// { +// if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::DoNotUseParent)) { +// return; +// } + +// auto &window = d->windows.at(index.row()); + +// if (window->windowState & PlasmaWindow::state::state_keep_above) { +// window->set_state(PlasmaWindow::state::state_keep_above, 0); +// } else { +// window->set_state(PlasmaWindow::state::state_keep_above, PlasmaWindow::state::state_keep_above); +// } +// } + +// void WaylandTasksModel::requestToggleKeepBelow(const QModelIndex &index) +// { +// if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::DoNotUseParent)) { +// return; +// } +// auto &window = d->windows.at(index.row()); + +// if (window->windowState & PlasmaWindow::state::state_keep_below) { +// window->set_state(PlasmaWindow::state::state_keep_below, 0); +// } else { +// window->set_state(PlasmaWindow::state::state_keep_below, PlasmaWindow::state::state_keep_below); +// } +// } + +// void WaylandTasksModel::requestToggleFullScreen(const QModelIndex &index) +// { +// if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::DoNotUseParent)) { +// return; +// } + +// auto &window = d->windows.at(index.row()); + +// if (window->windowState & PlasmaWindow::state::state_fullscreen) { +// window->set_state(PlasmaWindow::state::state_fullscreen, 0); +// } else { +// window->set_state(PlasmaWindow::state::state_fullscreen, PlasmaWindow::state::state_fullscreen); +// } +// } + +// void WaylandTasksModel::requestToggleShaded(const QModelIndex &index) +// { +// if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::DoNotUseParent)) { +// return; +// } + +// auto &window = d->windows.at(index.row()); + +// if (window->windowState & PlasmaWindow::state::state_shaded) { +// window->set_state(PlasmaWindow::state::state_shaded, 0); +// } else { +// window->set_state(PlasmaWindow::state::state_shaded, PlasmaWindow::state::state_shaded); +// }; +// } + +// void WaylandTasksModel::requestVirtualDesktops(const QModelIndex &index, const QVariantList &desktops) +// { +// // FIXME TODO: Lacks the "if we've requested the current desktop, force-activate +// // the window" logic from X11 version. This behavior should be in KWin rather than +// // libtm however. + +// if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::DoNotUseParent)) { +// return; +// } + +// auto &window = d->windows.at(index.row()); + +// if (desktops.isEmpty()) { +// const QStringList virtualDesktops = window->virtualDesktops; +// for (const QString &desktop : virtualDesktops) { +// window->request_leave_virtual_desktop(desktop); +// } +// } else { +// const QStringList &now = window->virtualDesktops; +// QStringList next; + +// for (const QVariant &desktop : desktops) { +// const QString &desktopId = desktop.toString(); + +// if (!desktopId.isEmpty()) { +// next << desktopId; + +// if (!now.contains(desktopId)) { +// window->request_enter_virtual_desktop(desktopId); +// } +// } +// } + +// for (const QString &desktop : now) { +// if (!next.contains(desktop)) { +// window->request_leave_virtual_desktop(desktop); +// } +// } +// } +// } + +// void WaylandTasksModel::requestNewVirtualDesktop(const QModelIndex &index) +// { +// if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::DoNotUseParent)) { +// return; +// } + +// d->windows.at(index.row())->request_enter_new_virtual_desktop(); +// } + +// void WaylandTasksModel::requestActivities(const QModelIndex &index, const QStringList &activities) +// { +// if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::DoNotUseParent)) { +// return; +// } + +// auto &window = d->windows.at(index.row()); +// const auto newActivities = QSet(activities.begin(), activities.end()); +// const auto plasmaActivities = window->activities; +// const auto oldActivities = QSet(plasmaActivities.begin(), plasmaActivities.end()); + +// const auto activitiesToAdd = newActivities - oldActivities; +// for (const auto &activity : activitiesToAdd) { +// window->request_enter_activity(activity); +// } + +// const auto activitiesToRemove = oldActivities - newActivities; +// for (const auto &activity : activitiesToRemove) { +// window->request_leave_activity(activity); +// } +// } + +// void WaylandTasksModel::requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate) +// { +// /* +// FIXME: This introduces the dependency on Qt::Quick. I might prefer +// reversing this and publishing the window pointer through the model, +// then calling PlasmaWindow::setMinimizeGeometry in the applet backend, +// rather than hand delegate items into the lib, keeping the lib more UI- +// agnostic. +// */ + +// Q_UNUSED(geometry) + +// if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::DoNotUseParent)) { +// return; +// } + +// const QQuickItem *item = qobject_cast(delegate); + +// if (!item || !item->parentItem()) { +// return; +// } + +// QWindow *itemWindow = item->window(); + +// if (!itemWindow) { +// return; +// } + +// auto waylandWindow = itemWindow->nativeInterface(); + +// if (!waylandWindow || !waylandWindow->surface()) { +// return; +// } + +// QRect rect(item->x(), item->y(), item->width(), item->height()); +// rect.moveTopLeft(item->parentItem()->mapToScene(rect.topLeft()).toPoint()); + +// auto &window = d->windows.at(index.row()); + +// window->set_minimized_geometry(waylandWindow->surface(), rect.x(), rect.y(), rect.width(), rect.height()); +// } + +// QUuid WaylandTasksModel::winIdFromMimeData(const QMimeData *mimeData, bool *ok) +// { +// Q_ASSERT(mimeData); + +// if (ok) { +// *ok = false; +// } + +// if (!mimeData->hasFormat(Private::mimeType())) { +// return {}; +// } + +// QUuid id(mimeData->data(Private::mimeType())); +// *ok = !id.isNull(); + +// return id; +// } + +// QList WaylandTasksModel::winIdsFromMimeData(const QMimeData *mimeData, bool *ok) +// { +// Q_ASSERT(mimeData); +// QList ids; + +// if (ok) { +// *ok = false; +// } + +// if (!mimeData->hasFormat(Private::groupMimeType())) { +// // Try to extract single window id. +// bool singularOk; +// QUuid id = winIdFromMimeData(mimeData, &singularOk); + +// if (ok) { +// *ok = singularOk; +// } + +// if (singularOk) { +// ids << id; +// } + +// return ids; +// } + +// // FIXME: Extracting multiple winids is still unimplemented; +// // TaskGroupingProxy::data(..., ::MimeData) can't produce +// // a payload with them anyways. + +// return ids; +// } + +LXQtTaskBarPlasmaWindow::LXQtTaskBarPlasmaWindow(const QString &uuid, ::org_kde_plasma_window *id) + : org_kde_plasma_window(id) + , uuid(uuid) +{ +} + +/* + * LXQtTaskBarPlasmaWindow + */ + +LXQtTaskBarPlasmaWindow::~LXQtTaskBarPlasmaWindow() +{ + destroy(); +} + +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_unmapped() +{ + wasUnmapped = true; + Q_EMIT unmapped(); +} + +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_title_changed(const QString &title) +{ + if(this->title == title) + return; + this->title = title; + Q_EMIT titleChanged(); +} + +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_app_id_changed(const QString &app_id) +{ + if(appId == app_id) + return; + appId = app_id; + Q_EMIT appIdChanged(); +} + +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_icon_changed() +{ + int pipeFds[2]; + if (pipe2(pipeFds, O_CLOEXEC) != 0) { + qWarning() << "TaskManager: failed creating pipe"; + return; + } + get_icon(pipeFds[1]); + ::close(pipeFds[1]); + auto readIcon = [uuid = uuid](int fd) { + auto closeGuard = qScopeGuard([fd]() { + ::close(fd); + }); + pollfd pollFd; + pollFd.fd = fd; + pollFd.events = POLLIN; + QByteArray data; + while (true) { + int ready = poll(&pollFd, 1, 1000); + if (ready < 0 && errno != EINTR) { + qWarning() << "TaskManager: polling for icon of window" << uuid << "failed"; + return QIcon(); + } else if (ready == 0) { + qWarning() << "TaskManager: time out polling for icon of window" << uuid; + return QIcon(); + } else { + char buffer[4096]; + int n = read(fd, buffer, sizeof(buffer)); + if (n < 0) { + qWarning() << "TaskManager: error reading icon of window" << uuid; + return QIcon(); + } else if (n > 0) { + data.append(buffer, n); + } else { + QIcon icon; + QDataStream ds(data); + ds >> icon; + return icon; + } + } + } + }; + QFuture future = QtConcurrent::run(readIcon, pipeFds[0]); + auto watcher = new QFutureWatcher(); + watcher->setFuture(future); + connect(watcher, &QFutureWatcher::finished, this, [this, watcher] { + icon = watcher->future().result(); + Q_EMIT iconChanged(); + }); + connect(watcher, &QFutureWatcher::finished, watcher, &QObject::deleteLater); +} + +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_themed_icon_name_changed(const QString &name) +{ + icon = QIcon::fromTheme(name); + Q_EMIT iconChanged(); +} + +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_state_changed(uint32_t flags) +{ + auto diff = windowState ^ flags; + if (diff & state::state_active) { + windowState.setFlag(state::state_active, flags & state::state_active); + Q_EMIT activeChanged(); + } + if (diff & state::state_minimized) { + windowState.setFlag(state::state_minimized, flags & state::state_minimized); + Q_EMIT minimizedChanged(); + } + if (diff & state::state_maximized) { + windowState.setFlag(state::state_maximized, flags & state::state_maximized); + Q_EMIT maximizedChanged(); + } + if (diff & state::state_fullscreen) { + windowState.setFlag(state::state_fullscreen, flags & state::state_fullscreen); + Q_EMIT fullscreenChanged(); + } + if (diff & state::state_keep_above) { + windowState.setFlag(state::state_keep_above, flags & state::state_keep_above); + Q_EMIT keepAboveChanged(); + } + if (diff & state::state_keep_below) { + windowState.setFlag(state::state_keep_below, flags & state::state_keep_below); + Q_EMIT keepBelowChanged(); + } + if (diff & state::state_on_all_desktops) { + windowState.setFlag(state::state_on_all_desktops, flags & state::state_on_all_desktops); + Q_EMIT onAllDesktopsChanged(); + } + if (diff & state::state_demands_attention) { + windowState.setFlag(state::state_demands_attention, flags & state::state_demands_attention); + Q_EMIT demandsAttentionChanged(); + } + if (diff & state::state_closeable) { + windowState.setFlag(state::state_closeable, flags & state::state_closeable); + Q_EMIT closeableChanged(); + } + if (diff & state::state_minimizable) { + windowState.setFlag(state::state_minimizable, flags & state::state_minimizable); + Q_EMIT minimizeableChanged(); + } + if (diff & state::state_maximizable) { + windowState.setFlag(state::state_maximizable, flags & state::state_maximizable); + Q_EMIT maximizeableChanged(); + } + if (diff & state::state_fullscreenable) { + windowState.setFlag(state::state_fullscreenable, flags & state::state_fullscreenable); + Q_EMIT fullscreenableChanged(); + } + if (diff & state::state_skiptaskbar) { + windowState.setFlag(state::state_skiptaskbar, flags & state::state_skiptaskbar); + Q_EMIT skipTaskbarChanged(); + } + if (diff & state::state_shadeable) { + windowState.setFlag(state::state_shadeable, flags & state::state_shadeable); + Q_EMIT shadeableChanged(); + } + if (diff & state::state_shaded) { + windowState.setFlag(state::state_shaded, flags & state::state_shaded); + Q_EMIT shadedChanged(); + } + if (diff & state::state_movable) { + windowState.setFlag(state::state_movable, flags & state::state_movable); + Q_EMIT movableChanged(); + } + if (diff & state::state_resizable) { + windowState.setFlag(state::state_resizable, flags & state::state_resizable); + Q_EMIT resizableChanged(); + } + if (diff & state::state_virtual_desktop_changeable) { + windowState.setFlag(state::state_virtual_desktop_changeable, flags & state::state_virtual_desktop_changeable); + Q_EMIT virtualDesktopChangeableChanged(); + } + if (diff & state::state_skipswitcher) { + windowState.setFlag(state::state_skipswitcher, flags & state::state_skipswitcher); + Q_EMIT skipSwitcherChanged(); + } +} + +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_virtual_desktop_entered(const QString &id) +{ + virtualDesktops.push_back(id); + Q_EMIT virtualDesktopEntered(); +} + +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_virtual_desktop_left(const QString &id) +{ + virtualDesktops.removeAll(id); + Q_EMIT virtualDesktopLeft(); +} + +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_geometry(int32_t x, int32_t y, uint32_t width, uint32_t height) +{ + geometry = QRect(x, y, width, height); + Q_EMIT geometryChanged(); +} + +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_application_menu(const QString &service_name, const QString &object_path) +{ + applicationMenuService = service_name; + applicationMenuObjectPath = object_path; + Q_EMIT applicationMenuChanged(); +} + +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_activity_entered(const QString &id) +{ + activities.push_back(id); + Q_EMIT activitiesChanged(); +} +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_activity_left(const QString &id) +{ + activities.removeAll(id); + Q_EMIT activitiesChanged(); +} +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_pid_changed(uint32_t pid) +{ + this->pid = pid; +} +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_resource_name_changed(const QString &resource_name) +{ + resourceName = resource_name; +} +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_parent_window(::org_kde_plasma_window *parent) +{ + LXQtTaskBarPlasmaWindow *parentWindow = nullptr; + if (parent) { + parentWindow = dynamic_cast(LXQtTaskBarPlasmaWindow::fromObject(parent)); + } + setParentWindow(parentWindow); +} +void LXQtTaskBarPlasmaWindow::org_kde_plasma_window_initial_state() +{ + Q_EMIT initialStateDone(); +} + +void LXQtTaskBarPlasmaWindow::setParentWindow(LXQtTaskBarPlasmaWindow *parent) +{ + const auto old = parentWindow; + QObject::disconnect(parentWindowUnmappedConnection); + + if (parent && !parent->wasUnmapped) { + parentWindow = QPointer(parent); + parentWindowUnmappedConnection = QObject::connect(parent, &LXQtTaskBarPlasmaWindow::unmapped, this, [this] { + setParentWindow(nullptr); + }); + } else { + parentWindow = QPointer(); + parentWindowUnmappedConnection = QMetaObject::Connection(); + } + + if (parentWindow.data() != old.data()) { + Q_EMIT parentWindowChanged(); + } +} + +/* + * LXQtTaskBarPlasmaWindowManagment + */ + +LXQtTaskBarPlasmaWindowManagment::LXQtTaskBarPlasmaWindowManagment() + : QWaylandClientExtensionTemplate(version) +{ + connect(this, &QWaylandClientExtension::activeChanged, this, [this] { + if (!isActive()) { + org_kde_plasma_window_management_destroy(object()); + } + }); +} + +LXQtTaskBarPlasmaWindowManagment::~LXQtTaskBarPlasmaWindowManagment() +{ + if (isActive()) { + org_kde_plasma_window_management_destroy(object()); + } +} + +void LXQtTaskBarPlasmaWindowManagment::org_kde_plasma_window_management_show_desktop_changed(uint32_t state) +{ + m_isShowingDesktop = (state == show_desktop::show_desktop_enabled); +} + +void LXQtTaskBarPlasmaWindowManagment::org_kde_plasma_window_management_window_with_uuid(uint32_t id, const QString &uuid) +{ + Q_UNUSED(id) + Q_EMIT windowCreated(new LXQtTaskBarPlasmaWindow(uuid, get_window_by_uuid(uuid))); +} +void LXQtTaskBarPlasmaWindowManagment::org_kde_plasma_window_management_stacking_order_uuid_changed(const QString &uuids) +{ + Q_EMIT stackingOrderChanged(uuids); +} diff --git a/panel/backends/wayland/plasma/lxqttaskbarplasmawindowmanagment.h b/panel/backends/wayland/plasma/lxqttaskbarplasmawindowmanagment.h new file mode 100644 index 000000000..c05dbc5d5 --- /dev/null +++ b/panel/backends/wayland/plasma/lxqttaskbarplasmawindowmanagment.h @@ -0,0 +1,159 @@ +#pragma once + +#include +#include +#include + +#include "qwayland-plasma-window-management.h" + +typedef quintptr WId; + +class LXQtTaskBarPlasmaWindowManagment; + +class LXQtTaskBarPlasmaWindow : public QObject, + public QtWayland::org_kde_plasma_window +{ + Q_OBJECT +public: + LXQtTaskBarPlasmaWindow(const QString &uuid, ::org_kde_plasma_window *id); + ~LXQtTaskBarPlasmaWindow(); + + inline WId getWindowId() const { return reinterpret_cast(this); } + + using state = QtWayland::org_kde_plasma_window_management::state; + const QString uuid; + QString title; + QString appId; + QIcon icon; + QFlags windowState; + QList virtualDesktops; + QRect geometry; + QString applicationMenuService; + QString applicationMenuObjectPath; + QList activities; + quint32 pid; + QString resourceName; + QPointer parentWindow; + bool wasUnmapped = false; + bool acceptedInTaskBar = false; + +Q_SIGNALS: + void unmapped(); + void titleChanged(); + void appIdChanged(); + void iconChanged(); + void activeChanged(); + void minimizedChanged(); + void maximizedChanged(); + void fullscreenChanged(); + void keepAboveChanged(); + void keepBelowChanged(); + void onAllDesktopsChanged(); + void demandsAttentionChanged(); + void closeableChanged(); + void minimizeableChanged(); + void maximizeableChanged(); + void fullscreenableChanged(); + void skiptaskbarChanged(); + void shadeableChanged(); + void shadedChanged(); + void movableChanged(); + void resizableChanged(); + void virtualDesktopChangeableChanged(); + void skipSwitcherChanged(); + void virtualDesktopEntered(); + void virtualDesktopLeft(); + void geometryChanged(); + void skipTaskbarChanged(); + void applicationMenuChanged(); + void activitiesChanged(); + void parentWindowChanged(); + void initialStateDone(); + +protected: + void org_kde_plasma_window_unmapped() override; + void org_kde_plasma_window_title_changed(const QString &title) override; + void org_kde_plasma_window_app_id_changed(const QString &app_id) override; + void org_kde_plasma_window_icon_changed() override; + void org_kde_plasma_window_themed_icon_name_changed(const QString &name) override; + void org_kde_plasma_window_state_changed(uint32_t flags) override; + void org_kde_plasma_window_virtual_desktop_entered(const QString &id) override; + + void org_kde_plasma_window_virtual_desktop_left(const QString &id) override; + void org_kde_plasma_window_geometry(int32_t x, int32_t y, uint32_t width, uint32_t height) override; + void org_kde_plasma_window_application_menu(const QString &service_name, const QString &object_path) override; + void org_kde_plasma_window_activity_entered(const QString &id) override; + void org_kde_plasma_window_activity_left(const QString &id) override; + void org_kde_plasma_window_pid_changed(uint32_t pid) override; + void org_kde_plasma_window_resource_name_changed(const QString &resource_name) override; + void org_kde_plasma_window_parent_window(::org_kde_plasma_window *parent) override; + void org_kde_plasma_window_initial_state() override; + +private: + void setParentWindow(LXQtTaskBarPlasmaWindow *parent); + + QMetaObject::Connection parentWindowUnmappedConnection; +}; + +class LXQtTaskBarPlasmaWindowManagment : public QWaylandClientExtensionTemplate, + public QtWayland::org_kde_plasma_window_management +{ + Q_OBJECT +public: + static constexpr int version = 16; + + LXQtTaskBarPlasmaWindowManagment(); + ~LXQtTaskBarPlasmaWindowManagment(); + + inline bool isShowingDesktop() const { return m_isShowingDesktop; } + +protected: + void org_kde_plasma_window_management_show_desktop_changed(uint32_t state) override; + void org_kde_plasma_window_management_window_with_uuid(uint32_t id, const QString &uuid) override; + void org_kde_plasma_window_management_stacking_order_uuid_changed(const QString &uuids) override; + +Q_SIGNALS: + void windowCreated(LXQtTaskBarPlasmaWindow *window); + void stackingOrderChanged(const QString &uuids); + +private: + bool m_isShowingDesktop = false; +}; + +// class Q_DECL_HIDDEN WaylandTasksModel::Private +// { +// public: +// Private(WaylandTasksModel *q); +// QHash appDataCache; +// QHash lastActivated; +// PlasmaWindow *activeWindow = nullptr; +// std::vector> windows; +// // key=transient child, value=leader +// QHash transients; +// // key=leader, values=transient children +// QMultiHash transientsDemandingAttention; +// std::unique_ptr windowManagement; +// KSharedConfig::Ptr rulesConfig; +// KDirWatch *configWatcher = nullptr; +// VirtualDesktopInfo *virtualDesktopInfo = nullptr; +// static QUuid uuid; +// QList stackingOrder; + +// void init(); +// void initWayland(); +// auto findWindow(PlasmaWindow *window) const; +// void addWindow(PlasmaWindow *window); + +// const AppData &appData(PlasmaWindow *window); + +// QIcon icon(PlasmaWindow *window); + +// static QString mimeType(); +// static QString groupMimeType(); + +// void dataChanged(PlasmaWindow *window, int role); +// void dataChanged(PlasmaWindow *window, const QList &roles); + +// private: +// WaylandTasksModel *q; +// }; diff --git a/panel/backends/wayland/plasma/protocols/org-kde-plasma-virtual-desktop.xml b/panel/backends/wayland/plasma/protocols/org-kde-plasma-virtual-desktop.xml new file mode 100644 index 000000000..0e0551b0a --- /dev/null +++ b/panel/backends/wayland/plasma/protocols/org-kde-plasma-virtual-desktop.xml @@ -0,0 +1,110 @@ + + + + + + + + Given the id of a particular virtual desktop, get the corresponding org_kde_plasma_virtual_desktop which represents only the desktop with that id. + + + + + + + + Ask the server to create a new virtual desktop, and position it at a specified position. If the position is zero or less, it will be positioned at the beginning, if the position is the count or more, it will be positioned at the end. + + + + + + + + Ask the server to get rid of a virtual desktop, the server may or may not acconsent to the request. + + + + + + + + + + + + + + + + + + + This event is sent after all other properties has been + sent after binding to the desktop manager object and after any + other property changes done after that. This allows + changes to the org_kde_plasma_virtual_desktop_management properties to be seen as + atomic, even if they happen via multiple events. + + + + + + + + + + + + + Request the server to set the status of this desktop to active: The server is free to consent or deny the request. This will be the new "current" virtual desktop of the system. + + + + + + The format of the id is decided by the compositor implementation. A desktop id univocally identifies a virtual desktop and must be guaranteed to never exist two desktops with the same id. The format of the string id is up to the server implementation. + + + + + + + + + + + The desktop will be the new "current" desktop of the system. The server may support either one virtual desktop active at a time, or other combinations such as one virtual desktop active per screen. + Windows associated to this virtual desktop will be shown. + + + + + + Windows that were associated only to this desktop will be hidden. + + + + + + This event is sent after all other properties has been + sent after binding to the desktop object and after any + other property changes done after that. This allows + changes to the org_kde_plasma_virtual_desktop properties to be seen as + atomic, even if they happen via multiple events. + + + + + + This virtual desktop has just been removed by the server: + All windows will lose the association to this desktop. + + + + + diff --git a/panel/backends/wayland/plasma/protocols/plasma-window-management.xml b/panel/backends/wayland/plasma/protocols/plasma-window-management.xml new file mode 100644 index 000000000..5990ebfda --- /dev/null +++ b/panel/backends/wayland/plasma/protocols/plasma-window-management.xml @@ -0,0 +1,425 @@ + + + + + + + This interface manages application windows. + It provides requests to show and hide the desktop and emits + an event every time a window is created so that the client can + use it to manage the window. + + Only one client can bind this interface at a time. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tell the compositor to show/hide the desktop. + + + + + + Deprecated: use get_window_by_uuid + + + + + + + + + + + + This event will be sent whenever the show desktop mode changes. E.g. when it is entered + or left. + + On binding the interface the current state is sent. + + + + + + + This event will be sent immediately after a window is mapped. + + + + + + + This event will be sent when stacking order changed and on bind + + + + + + + This event will be sent when stacking order changed and on bind + + + + + + + This event will be sent immediately after a window is mapped. + + + + + + + + + Manages and control an application window. + + Only one client can bind this interface at a time. + + + + + Set window state. + + Values for state argument are described by org_kde_plasma_window_management.state + and can be used together in a bitfield. The flags bitfield describes which flags are + supposed to be set, the state bitfield the value for the set flags + + + + + + + + Deprecated: use enter_virtual_desktop + Maps the window to a different virtual desktop. + + To show the window on all virtual desktops, call the + org_kde_plasma_window.set_state request and specify a on_all_desktops + state in the bitfield. + + + + + + + Sets the geometry of the taskbar entry for this window. + The geometry is relative to a panel in particular. + + + + + + + + + + + Remove the task geometry information for a particular panel. + + + + + + + + + Close this window. + + + + + + Request an interactive move for this window. + + + + + + Request an interactive resize for this window. + + + + + + Removes the resource bound for this org_kde_plasma_window. + + + + + + The compositor will write the window icon into the provided file descriptor. + The data is a serialized QIcon with QDataStream. + + + + + + + This event will be sent as soon as the window title is changed. + + + + + + + This event will be sent as soon as the application + identifier is changed. + + + + + + + This event will be sent as soon as the window state changes. + + Values for state argument are described by org_kde_plasma_window_management.state. + + + + + + + DEPRECATED: use virtual_desktop_entered and virtual_desktop_left instead + This event will be sent when a window is moved to another + virtual desktop. + + It is not sent if it becomes visible on all virtual desktops though. + + + + + + + This event will be sent whenever the themed icon name changes. May be null. + + + + + + + This event will be sent immediately after the window is closed + and its surface is unmapped. + + + + + + This event will be sent immediately after all initial state been sent to the client. + If the Plasma window is already unmapped, the unmapped event will be sent before the + initial_state event. + + + + + + This event will be sent whenever the parent window of this org_kde_plasma_window changes. + The passed parent is another org_kde_plasma_window and this org_kde_plasma_window is a + transient window to the parent window. If the parent argument is null, this + org_kde_plasma_window does not have a parent window. + + + + + + + This event will be sent whenever the window geometry of this org_kde_plasma_window changes. + The coordinates are in absolute coordinates of the windowing system. + + + + + + + + + + This event will be sent whenever the icon of the window changes, but there is no themed + icon name. Common examples are Xwayland windows which have a pixmap based icon. + + The client can request the icon using get_icon. + + + + + + This event will be sent when the compositor has set the process id this window belongs to. + This should be set once before the initial_state is sent. + + + + + + + + + + Make the window enter a virtual desktop. A window can enter more + than one virtual desktop. if the id is empty or invalid, no action will be performed. + + + + + + RFC: do this with an empty id to request_enter_virtual_desktop? + Make the window enter a new virtual desktop. If the server consents the request, + it will create a new virtual desktop and assign the window to it. + + + + + + Make the window exit a virtual desktop. If it exits all desktops it will be considered on all of them. + + + + + + + This event will be sent when the window has entered a new virtual desktop. The window can be on more than one desktop, or none: then is considered on all of them. + + + + + + + This event will be sent when the window left a virtual desktop. If the window leaves all desktops, it can be considered on all. + If the window gets manually added on all desktops, the server has to send virtual_desktop_left for every previous desktop it was in for the window to be really considered on all desktops. + + + + + + + + + This event will be sent after the application menu + for the window has changed. + + + + + + + + Make the window enter an activity. A window can enter more activity. If the id is empty or invalid, no action will be performed. + + + + + + + Make the window exit a an activity. If it exits all activities it will be considered on all of them. + + + + + + + This event will be sent when the window has entered an activity. The window can be on more than one activity, or none: then is considered on all of them. + + + + + + + This event will be sent when the window left an activity. If the window leaves all activities, it will be considered on all. + If the window gets manually added on all activities, the server has to send activity_left for every previous activity it was in for the window to be really considered on all activities. + + + + + + + Requests this window to be displayed in a specific output. + + + + + + + This event will be sent when the X11 resource name of the window has changed. + This is only set for XWayland windows. + + + + + + + + The activation manager interface provides a way to get notified + when an application is about to be activated. + + + + + Destroy the activation manager object. The activation objects introduced + by this manager object will be unaffected. + + + + + + Will be issued when an app is set to be activated. It offers + an instance of org_kde_plasma_activation that will tell us the app_id + and the extent of the activation. + + + + + + + + + Notify the compositor that the org_kde_plasma_activation object will no + longer be used. + + + + + + + + + + + + + diff --git a/panel/backends/wayland/protocols/org-kde-plasma-virtual-desktop.xml b/panel/backends/wayland/protocols/org-kde-plasma-virtual-desktop.xml new file mode 100644 index 000000000..0e0551b0a --- /dev/null +++ b/panel/backends/wayland/protocols/org-kde-plasma-virtual-desktop.xml @@ -0,0 +1,110 @@ + + + + + + + + Given the id of a particular virtual desktop, get the corresponding org_kde_plasma_virtual_desktop which represents only the desktop with that id. + + + + + + + + Ask the server to create a new virtual desktop, and position it at a specified position. If the position is zero or less, it will be positioned at the beginning, if the position is the count or more, it will be positioned at the end. + + + + + + + + Ask the server to get rid of a virtual desktop, the server may or may not acconsent to the request. + + + + + + + + + + + + + + + + + + + This event is sent after all other properties has been + sent after binding to the desktop manager object and after any + other property changes done after that. This allows + changes to the org_kde_plasma_virtual_desktop_management properties to be seen as + atomic, even if they happen via multiple events. + + + + + + + + + + + + + Request the server to set the status of this desktop to active: The server is free to consent or deny the request. This will be the new "current" virtual desktop of the system. + + + + + + The format of the id is decided by the compositor implementation. A desktop id univocally identifies a virtual desktop and must be guaranteed to never exist two desktops with the same id. The format of the string id is up to the server implementation. + + + + + + + + + + + The desktop will be the new "current" desktop of the system. The server may support either one virtual desktop active at a time, or other combinations such as one virtual desktop active per screen. + Windows associated to this virtual desktop will be shown. + + + + + + Windows that were associated only to this desktop will be hidden. + + + + + + This event is sent after all other properties has been + sent after binding to the desktop object and after any + other property changes done after that. This allows + changes to the org_kde_plasma_virtual_desktop properties to be seen as + atomic, even if they happen via multiple events. + + + + + + This virtual desktop has just been removed by the server: + All windows will lose the association to this desktop. + + + + + diff --git a/panel/backends/wayland/protocols/plasma-window-management.xml b/panel/backends/wayland/protocols/plasma-window-management.xml new file mode 100644 index 000000000..5990ebfda --- /dev/null +++ b/panel/backends/wayland/protocols/plasma-window-management.xml @@ -0,0 +1,425 @@ + + + + + + + This interface manages application windows. + It provides requests to show and hide the desktop and emits + an event every time a window is created so that the client can + use it to manage the window. + + Only one client can bind this interface at a time. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tell the compositor to show/hide the desktop. + + + + + + Deprecated: use get_window_by_uuid + + + + + + + + + + + + This event will be sent whenever the show desktop mode changes. E.g. when it is entered + or left. + + On binding the interface the current state is sent. + + + + + + + This event will be sent immediately after a window is mapped. + + + + + + + This event will be sent when stacking order changed and on bind + + + + + + + This event will be sent when stacking order changed and on bind + + + + + + + This event will be sent immediately after a window is mapped. + + + + + + + + + Manages and control an application window. + + Only one client can bind this interface at a time. + + + + + Set window state. + + Values for state argument are described by org_kde_plasma_window_management.state + and can be used together in a bitfield. The flags bitfield describes which flags are + supposed to be set, the state bitfield the value for the set flags + + + + + + + + Deprecated: use enter_virtual_desktop + Maps the window to a different virtual desktop. + + To show the window on all virtual desktops, call the + org_kde_plasma_window.set_state request and specify a on_all_desktops + state in the bitfield. + + + + + + + Sets the geometry of the taskbar entry for this window. + The geometry is relative to a panel in particular. + + + + + + + + + + + Remove the task geometry information for a particular panel. + + + + + + + + + Close this window. + + + + + + Request an interactive move for this window. + + + + + + Request an interactive resize for this window. + + + + + + Removes the resource bound for this org_kde_plasma_window. + + + + + + The compositor will write the window icon into the provided file descriptor. + The data is a serialized QIcon with QDataStream. + + + + + + + This event will be sent as soon as the window title is changed. + + + + + + + This event will be sent as soon as the application + identifier is changed. + + + + + + + This event will be sent as soon as the window state changes. + + Values for state argument are described by org_kde_plasma_window_management.state. + + + + + + + DEPRECATED: use virtual_desktop_entered and virtual_desktop_left instead + This event will be sent when a window is moved to another + virtual desktop. + + It is not sent if it becomes visible on all virtual desktops though. + + + + + + + This event will be sent whenever the themed icon name changes. May be null. + + + + + + + This event will be sent immediately after the window is closed + and its surface is unmapped. + + + + + + This event will be sent immediately after all initial state been sent to the client. + If the Plasma window is already unmapped, the unmapped event will be sent before the + initial_state event. + + + + + + This event will be sent whenever the parent window of this org_kde_plasma_window changes. + The passed parent is another org_kde_plasma_window and this org_kde_plasma_window is a + transient window to the parent window. If the parent argument is null, this + org_kde_plasma_window does not have a parent window. + + + + + + + This event will be sent whenever the window geometry of this org_kde_plasma_window changes. + The coordinates are in absolute coordinates of the windowing system. + + + + + + + + + + This event will be sent whenever the icon of the window changes, but there is no themed + icon name. Common examples are Xwayland windows which have a pixmap based icon. + + The client can request the icon using get_icon. + + + + + + This event will be sent when the compositor has set the process id this window belongs to. + This should be set once before the initial_state is sent. + + + + + + + + + + Make the window enter a virtual desktop. A window can enter more + than one virtual desktop. if the id is empty or invalid, no action will be performed. + + + + + + RFC: do this with an empty id to request_enter_virtual_desktop? + Make the window enter a new virtual desktop. If the server consents the request, + it will create a new virtual desktop and assign the window to it. + + + + + + Make the window exit a virtual desktop. If it exits all desktops it will be considered on all of them. + + + + + + + This event will be sent when the window has entered a new virtual desktop. The window can be on more than one desktop, or none: then is considered on all of them. + + + + + + + This event will be sent when the window left a virtual desktop. If the window leaves all desktops, it can be considered on all. + If the window gets manually added on all desktops, the server has to send virtual_desktop_left for every previous desktop it was in for the window to be really considered on all desktops. + + + + + + + + + This event will be sent after the application menu + for the window has changed. + + + + + + + + Make the window enter an activity. A window can enter more activity. If the id is empty or invalid, no action will be performed. + + + + + + + Make the window exit a an activity. If it exits all activities it will be considered on all of them. + + + + + + + This event will be sent when the window has entered an activity. The window can be on more than one activity, or none: then is considered on all of them. + + + + + + + This event will be sent when the window left an activity. If the window leaves all activities, it will be considered on all. + If the window gets manually added on all activities, the server has to send activity_left for every previous activity it was in for the window to be really considered on all activities. + + + + + + + Requests this window to be displayed in a specific output. + + + + + + + This event will be sent when the X11 resource name of the window has changed. + This is only set for XWayland windows. + + + + + + + + The activation manager interface provides a way to get notified + when an application is about to be activated. + + + + + Destroy the activation manager object. The activation objects introduced + by this manager object will be unaffected. + + + + + + Will be issued when an app is set to be activated. It offers + an instance of org_kde_plasma_activation that will tell us the app_id + and the extent of the activation. + + + + + + + + + Notify the compositor that the org_kde_plasma_activation object will no + longer be used. + + + + + + + + + + + + + diff --git a/panel/backends/wayland/wlroots/CMakeLists.txt b/panel/backends/wayland/wlroots/CMakeLists.txt new file mode 100644 index 000000000..9fe10d647 --- /dev/null +++ b/panel/backends/wayland/wlroots/CMakeLists.txt @@ -0,0 +1,28 @@ +# Wayland WM Backend + +project(lxqt-panel-backend-wlroots) + +find_package(Qt6 ${REQUIRED_QT_VERSION} REQUIRED COMPONENTS WaylandClient Concurrent) + +add_library(lxqt-panel-backend-wlroots STATIC + lxqttaskbarbackendwlr.h + lxqttaskbarwlrwindowmanagment.h + lxqtwlrvirtualdesktop.h + + lxqttaskbarbackendwlr.cpp + lxqttaskbarwlrwindowmanagment.cpp + lxqtwlrvirtualdesktop.cpp +) + +qt6_generate_wayland_protocol_client_sources(lxqt-panel-backend-wlroots +FILES + ${CMAKE_CURRENT_SOURCE_DIR}/protocols/wlr-foreign-toplevel-management-unstable-v1.xml +) + +target_link_libraries(lxqt-panel-backend-wlroots + Qt6::GuiPrivate + Qt6::WaylandClient + Qt6::Concurrent + Qt6Xdg + lxqt-panel-backend-common +) diff --git a/panel/backends/wayland/wlroots/lxqttaskbarbackendwlr.cpp b/panel/backends/wayland/wlroots/lxqttaskbarbackendwlr.cpp new file mode 100644 index 000000000..f9af04fa3 --- /dev/null +++ b/panel/backends/wayland/wlroots/lxqttaskbarbackendwlr.cpp @@ -0,0 +1,471 @@ +#include "lxqttaskbarbackendwlr.h" + +#include "lxqttaskbarwlrwindowmanagment.h" +#include "lxqtwlrvirtualdesktop.h" + +#include +#include +#include +#include + +// Function to search for a window in the vector +WId findWindow(const std::vector& windows, WId tgt) { + // Use std::find to locate the target window + auto it = std::find(windows.begin(), windows.end(), tgt); + + // Check if the window was found (iterator points to windows.end() if not found) + if (it != windows.end()) { + // If found, return the window ID by dereferencing the iterator + return *it; + } + + return 0; +} + +// Function to erase a window from the vector +void eraseWindow(std::vector& windows, WId tgt) { + // Use std::vector::iterator to find the window + auto it = std::find(windows.begin(), windows.end(), tgt); + + // Check if the window was found + if (it != windows.end()) { + // If found, erase the element pointed to by the iterator + windows.erase(it); + } +} + +LXQtTaskbarWlrootsBackend::LXQtTaskbarWlrootsBackend(QObject *parent) : + ILXQtTaskbarAbstractBackend(parent) +{ + m_managment.reset(new LXQtTaskbarWlrootsWindowManagment); + m_workspaceInfo.reset(new LXQtWlrootsWaylandWorkspaceInfo); + + connect(m_managment.get(), &LXQtTaskbarWlrootsWindowManagment::windowCreated, this, &LXQtTaskbarWlrootsBackend::addWindow); +} + +bool LXQtTaskbarWlrootsBackend::supportsAction(WId, LXQtTaskBarBackendAction action) const +{ + switch (action) + { + case LXQtTaskBarBackendAction::Maximize: + return true; + + case LXQtTaskBarBackendAction::Minimize: + return true; + + case LXQtTaskBarBackendAction::FullScreen: + return true; + + default: + return false; + } + + return false; +} + +bool LXQtTaskbarWlrootsBackend::reloadWindows() +{ + const QVector wids = getCurrentWindows(); + + // Force removal and re-adding + for(WId windowId : wids) + { + emit windowRemoved(windowId); + } + for(WId windowId : wids) + { + emit windowAdded(windowId); + } + + return true; +} + +QVector LXQtTaskbarWlrootsBackend::getCurrentWindows() const +{ + QVector wids; + + for( WId wid: windows ){ + wids << wid; + } + + return wids; +} + +QString LXQtTaskbarWlrootsBackend::getWindowTitle(WId windowId) const +{ + LXQtTaskbarWlrootsWindow *window = getWindow(windowId); + if(!window) + return QString(); + + return window->windowState.title; +} + +bool LXQtTaskbarWlrootsBackend::applicationDemandsAttention(WId) const +{ + return false; +} + +QIcon LXQtTaskbarWlrootsBackend::getApplicationIcon(WId windowId, int devicePixels) const +{ + Q_UNUSED(devicePixels) + + LXQtTaskbarWlrootsWindow *window = getWindow(windowId); + if(!window) + return QIcon(); + + return window->icon; +} + +QString LXQtTaskbarWlrootsBackend::getWindowClass(WId windowId) const +{ + LXQtTaskbarWlrootsWindow *window = getWindow(windowId); + if(!window) + return QString(); + return window->windowState.appId; +} + +LXQtTaskBarWindowLayer LXQtTaskbarWlrootsBackend::getWindowLayer(WId) const +{ + return LXQtTaskBarWindowLayer::Normal; +} + +bool LXQtTaskbarWlrootsBackend::setWindowLayer(WId, LXQtTaskBarWindowLayer) +{ + return false; +} + +LXQtTaskBarWindowState LXQtTaskbarWlrootsBackend::getWindowState(WId windowId) const +{ + LXQtTaskbarWlrootsWindow *window = getWindow(windowId); + if(!window) + return LXQtTaskBarWindowState::Normal; + + if(window->windowState.minimized) + return LXQtTaskBarWindowState::Minimized; + + if(window->windowState.maximized) + return LXQtTaskBarWindowState::Maximized; + + if(window->windowState.fullscreen) + return LXQtTaskBarWindowState::FullScreen; + + return LXQtTaskBarWindowState::Normal; +} + +bool LXQtTaskbarWlrootsBackend::setWindowState(WId windowId, LXQtTaskBarWindowState state, bool set) +{ + LXQtTaskbarWlrootsWindow *window = getWindow(windowId); + if(!window) + return false; + + switch (state) + { + case LXQtTaskBarWindowState::Minimized: + { + if ( set ) { + window->set_minimized(); + } + + else { + window->unset_minimized(); + } + + break; + } + case LXQtTaskBarWindowState::Maximized: + case LXQtTaskBarWindowState::MaximizedVertically: + case LXQtTaskBarWindowState::MaximizedHorizontally: + { + if ( set ) { + window->set_maximized(); + } + + else { + window->unset_maximized(); + } + + break; + } + case LXQtTaskBarWindowState::Normal: + { + if (set) + { + if ( window->windowState.maximized) { + window->unset_maximized(); + } + } + + break; + } + + case LXQtTaskBarWindowState::FullScreen: + { + if ( set ) { + window->set_fullscreen(nullptr); + } + + else { + window->unset_fullscreen(); + } + break; + } + + default: + return false; + } + + return true; +} + +bool LXQtTaskbarWlrootsBackend::isWindowActive(WId windowId) const +{ + LXQtTaskbarWlrootsWindow *window = getWindow(windowId); + if(!window) + return false; + + return activeWindow == windowId || window->windowState.activated; +} + +bool LXQtTaskbarWlrootsBackend::raiseWindow(WId windowId, bool onCurrentWorkSpace) +{ + Q_UNUSED(onCurrentWorkSpace) // Cannot be done on a generic wlroots-based compositor! + + LXQtTaskbarWlrootsWindow *window = getWindow(windowId); + if(!window) + return false; + + window->activate(); + return true; +} + +bool LXQtTaskbarWlrootsBackend::closeWindow(WId windowId) +{ + LXQtTaskbarWlrootsWindow *window = getWindow(windowId); + if(!window) + return false; + + window->close(); + return true; +} + +WId LXQtTaskbarWlrootsBackend::getActiveWindow() const +{ + return activeWindow; +} + +int LXQtTaskbarWlrootsBackend::getWorkspacesCount() const +{ + return m_workspaceInfo->numberOfDesktops(); +} + +QString LXQtTaskbarWlrootsBackend::getWorkspaceName(int idx) const +{ + return m_workspaceInfo->getDesktopName(idx - 1); //Return to 0-based +} + +int LXQtTaskbarWlrootsBackend::getCurrentWorkspace() const +{ + return 1; +} + +bool LXQtTaskbarWlrootsBackend::setCurrentWorkspace(int) +{ + return false; +} + +int LXQtTaskbarWlrootsBackend::getWindowWorkspace(WId) const +{ + return 1; +} + +bool LXQtTaskbarWlrootsBackend::setWindowOnWorkspace(WId, int) +{ + return false; +} + +void LXQtTaskbarWlrootsBackend::moveApplicationToPrevNextMonitor(WId, bool, bool) +{ +} + +bool LXQtTaskbarWlrootsBackend::isWindowOnScreen(QScreen *, WId) const +{ + // TODO: Manage based on output-enter/output-leave + return true; +} + +void LXQtTaskbarWlrootsBackend::moveApplication(WId) +{ +} + +void LXQtTaskbarWlrootsBackend::resizeApplication(WId) +{ +} + +void LXQtTaskbarWlrootsBackend::refreshIconGeometry(WId, const QRect &) +{ + +} + +bool LXQtTaskbarWlrootsBackend::isAreaOverlapped(const QRect &) const +{ + return false; +} + +bool LXQtTaskbarWlrootsBackend::isShowingDesktop() const +{ + return m_managment->isShowingDesktop(); +} + +bool LXQtTaskbarWlrootsBackend::showDesktop(bool) +{ + return false; +} + +void LXQtTaskbarWlrootsBackend::addWindow(WId winId) +{ + if (findWindow(windows, winId) != 0 || transients.contains(winId)) + { + return; + } + + auto removeWindow = [winId, this] + { + eraseWindow(windows, winId); + lastActivated.remove(winId); + + if (activeWindow == winId) + { + activeWindow = 0; + emit activeWindowChanged(0); + } + + emit windowRemoved(winId); + }; + + LXQtTaskbarWlrootsWindow *window = getWindow( winId ); + if ( window == nullptr ) { + return; + } + + /** The window was closed. Remove from our lists */ + connect(window, &LXQtTaskbarWlrootsWindow::closed, this, removeWindow); + + /** */ + connect(window, &LXQtTaskbarWlrootsWindow::titleChanged, this, [winId, this] { + emit windowPropertyChanged(winId, int(LXQtTaskBarWindowProperty::Title)); + }); + + connect(window, &LXQtTaskbarWlrootsWindow::appIdChanged, this, [winId, this] { + emit windowPropertyChanged(winId, int(LXQtTaskBarWindowProperty::WindowClass)); + }); + + if (window->windowState.activated) { + LXQtTaskbarWlrootsWindow *effectiveActive = window; + while (effectiveActive->parentWindow) { + effectiveActive = getWindow(effectiveActive->parentWindow); + } + + lastActivated[effectiveActive->getWindowId()] = QTime::currentTime(); + activeWindow = effectiveActive->getWindowId(); + } + + connect(window, &LXQtTaskbarWlrootsWindow::activatedChanged, this, [window, this] { + WId effectiveWindow = window->getWindowId(); + + while (getWindow(effectiveWindow)->parentWindow) + { + effectiveWindow = getWindow(effectiveWindow)->parentWindow; + } + + if (window->windowState.activated) + { + lastActivated[effectiveWindow] = QTime::currentTime(); + + if (activeWindow != effectiveWindow) + { + activeWindow = effectiveWindow; + emit activeWindowChanged(activeWindow); + } + } + else + { + if (activeWindow == effectiveWindow) + { + activeWindow = 0; + emit activeWindowChanged(0); + } + } + }); + + connect(window, &LXQtTaskbarWlrootsWindow::parentChanged, this, [window, this] { + WId leader = window->parentWindow; + + /** Basically, check if this window is a transient */ + if (transients.remove(leader)) + { + if (leader) + { + // leader change. + transients.insert(window->getWindowId(), leader); + } + else + { + // lost a leader, add to regular windows list. + Q_ASSERT(findWindow(windows, leader) == 0); + + windows.push_back(leader); + } + } + + else if (leader) + { + eraseWindow(windows, window->getWindowId()); + lastActivated.remove(window->getWindowId()); + } + }); + + auto stateChanged = [window, this] { + emit windowPropertyChanged(window->getWindowId(), int(LXQtTaskBarWindowProperty::State)); + }; + + connect(window, &LXQtTaskbarWlrootsWindow::fullscreenChanged, this, stateChanged); + + connect(window, &LXQtTaskbarWlrootsWindow::maximizedChanged, this, stateChanged); + + connect(window, &LXQtTaskbarWlrootsWindow::minimizedChanged, this, stateChanged); + + // Handle transient. + if (WId leader = window->parentWindow) + { + transients.insert(winId, leader); + } + else + { + windows.push_back(winId); + } + + emit windowAdded( winId ); + emit windowPropertyChanged(winId, int(LXQtTaskBarWindowProperty::WindowClass)); + emit windowPropertyChanged(winId, int(LXQtTaskBarWindowProperty::Title)); + emit windowPropertyChanged(winId, int(LXQtTaskBarWindowProperty::Icon)); + emit windowPropertyChanged(winId, int(LXQtTaskBarWindowProperty::State)); +} + +bool LXQtTaskbarWlrootsBackend::acceptWindow(WId window) const +{ + if(transients.contains(window)) + return false; + + return true; +} + +LXQtTaskbarWlrootsWindow *LXQtTaskbarWlrootsBackend::getWindow(WId windowId) const +{ + /** Easiest way is to convert the quintptr to the actual pointer */ + LXQtTaskbarWlrootsWindow *win = reinterpret_cast( windowId ); + if ( win ) { + return win; + } + + return nullptr; +} diff --git a/panel/backends/wayland/wlroots/lxqttaskbarbackendwlr.h b/panel/backends/wayland/wlroots/lxqttaskbarbackendwlr.h new file mode 100644 index 000000000..b2ae9a925 --- /dev/null +++ b/panel/backends/wayland/wlroots/lxqttaskbarbackendwlr.h @@ -0,0 +1,91 @@ +#pragma once + +#include "../../ilxqttaskbarabstractbackend.h" + +#include +#include +#include + +class LXQtTaskbarWlrootsWindow; +class LXQtTaskbarWlrootsWindowManagment; +class LXQtWlrootsWaylandWorkspaceInfo; + + +class LXQtTaskbarWlrootsBackend : public ILXQtTaskbarAbstractBackend +{ + Q_OBJECT + +public: + explicit LXQtTaskbarWlrootsBackend(QObject *parent = nullptr); + + // Backend + virtual bool supportsAction(WId windowId, LXQtTaskBarBackendAction action) const override; + + // Windows + virtual bool reloadWindows() override; + + virtual QVector getCurrentWindows() const override; + virtual QString getWindowTitle(WId windowId) const override; + virtual bool applicationDemandsAttention(WId windowId) const override; + virtual QIcon getApplicationIcon(WId windowId, int devicePixels) const override; + virtual QString getWindowClass(WId windowId) const override; + + virtual LXQtTaskBarWindowLayer getWindowLayer(WId windowId) const override; + virtual bool setWindowLayer(WId windowId, LXQtTaskBarWindowLayer layer) override; + + virtual LXQtTaskBarWindowState getWindowState(WId windowId) const override; + virtual bool setWindowState(WId windowId, LXQtTaskBarWindowState state, bool set) override; + + virtual bool isWindowActive(WId windowId) const override; + virtual bool raiseWindow(WId windowId, bool onCurrentWorkSpace) override; + + virtual bool closeWindow(WId windowId) override; + + virtual WId getActiveWindow() const override; + + // Workspaces + virtual int getWorkspacesCount() const override; + virtual QString getWorkspaceName(int idx) const override; + + virtual int getCurrentWorkspace() const override; + virtual bool setCurrentWorkspace(int idx) override; + + virtual int getWindowWorkspace(WId windowId) const override; + virtual bool setWindowOnWorkspace(WId windowId, int idx) override; + + virtual void moveApplicationToPrevNextMonitor(WId windowId, bool next, bool raiseOnCurrentDesktop) override; + + virtual bool isWindowOnScreen(QScreen *screen, WId windowId) const override; + + // X11 Specific + virtual void moveApplication(WId windowId) override; + virtual void resizeApplication(WId windowId) override; + + virtual void refreshIconGeometry(WId windowId, const QRect &geom) override; + + // Panel internal + virtual bool isAreaOverlapped(const QRect& area) const override; + + // Show Destop + virtual bool isShowingDesktop() const override; + virtual bool showDesktop(bool value) override; + +private: + void addWindow(WId wid); + bool acceptWindow(WId wid) const; + +private: + /** Convert WId (i.e. quintptr into LXQtTaskbarWlrootsWindow*) */ + LXQtTaskbarWlrootsWindow *getWindow(WId windowId) const; + + std::unique_ptr m_workspaceInfo; + + std::unique_ptr m_managment; + + QHash lastActivated; + WId activeWindow = 0; + std::vector windows; + + // key=transient child, value=leader + QHash transients; +}; diff --git a/panel/backends/wayland/wlroots/lxqttaskbarwlrwindowmanagment.cpp b/panel/backends/wayland/wlroots/lxqttaskbarwlrwindowmanagment.cpp new file mode 100644 index 000000000..f3b5f9710 --- /dev/null +++ b/panel/backends/wayland/wlroots/lxqttaskbarwlrwindowmanagment.cpp @@ -0,0 +1,545 @@ +#include "lxqttaskbarwlrwindowmanagment.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include + +#include + +QString U8Str( const char *str ) { + return QString::fromUtf8( str ); +} + +static inline QString getPixmapIcon(QString name) +{ + QStringList paths{ + U8Str("/usr/local/share/pixmaps/"), + U8Str("/usr/share/pixmaps/"), + }; + + QStringList sfxs{ + U8Str( ".svg" ), U8Str( ".png" ), U8Str( ".xpm" ) + }; + + for (QString path: paths) + { + for (QString sfx: sfxs) + { + if (QFile::exists(path + name + sfx)) + { + return path + name + sfx; + } + } + } + + return QString(); +} + + +QIcon getIconForAppId(QString mAppId) +{ + if (mAppId.isEmpty() or (mAppId == U8Str("Unknown"))) + { + return QIcon(); + } + + /** Wine apps */ + if (mAppId.endsWith(U8Str(".exe"))) + { + return QIcon::fromTheme(U8Str("wine")); + } + + /** Check if a theme icon exists called @mAppId */ + if (QIcon::hasThemeIcon(mAppId)) + { + return QIcon::fromTheme(mAppId); + } + + /** Check if the theme icon is @mAppId, but all lower-case letters */ + else if (QIcon::hasThemeIcon(mAppId.toLower())) + { + return QIcon::fromTheme(mAppId.toLower()); + } + + QStringList appDirs = { + QDir::home().filePath(U8Str(".local/share/applications/")), + U8Str("/usr/local/share/applications/"), + U8Str("/usr/share/applications/"), + }; + + /** + * Assume mAppId == desktop-file-name (ideal situation) + * or mAppId.toLower() == desktop-file-name (cheap fallback) + */ + QString iconName; + + for (QString path: appDirs) + { + /** Get the icon name from desktop (mAppId: as it is) */ + if (QFile::exists(path + mAppId + U8Str(".desktop"))) + { + QSettings desktop(path + mAppId + U8Str(".desktop"), QSettings::IniFormat); + iconName = desktop.value(U8Str("Desktop Entry/Icon")).toString(); + } + + /** Get the icon name from desktop (mAppId: all lower-case letters) */ + else if (QFile::exists(path + mAppId.toLower() + U8Str(".desktop"))) + { + QSettings desktop(path + mAppId.toLower() + U8Str(".desktop"), QSettings::IniFormat); + iconName = desktop.value(U8Str("Desktop Entry/Icon")).toString(); + } + + /** No icon specified: try else-where */ + if (iconName.isEmpty()) + { + continue; + } + + /** We got an iconName, and it's in the current theme */ + if (QIcon::hasThemeIcon(iconName)) + { + return QIcon::fromTheme(iconName); + } + + /** Not a theme icon, but an absolute path */ + else if (QFile::exists(iconName)) + { + return QIcon(iconName); + } + + /** Not theme icon or absolute path. So check /usr/share/pixmaps/ */ + else + { + iconName = getPixmapIcon(iconName); + + if (not iconName.isEmpty()) + { + return QIcon(iconName); + } + } + } + + /* Check all desktop files for @mAppId */ + for (QString path: appDirs) + { + QStringList desktops = QDir(path).entryList({ U8Str("*.desktop") }); + for (QString dskf: desktops) + { + QSettings desktop(path + dskf, QSettings::IniFormat); + + QString exec = desktop.value(U8Str("Desktop Entry/Exec"), U8Str("abcd1234/-")).toString(); + QString name = desktop.value(U8Str("Desktop Entry/Name"), U8Str("abcd1234/-")).toString(); + QString cls = desktop.value(U8Str("Desktop Entry/StartupWMClass"), U8Str("abcd1234/-")).toString(); + + QString execPath = U8Str(std::filesystem::path(exec.toStdString()).filename().c_str()); + + if (mAppId.compare(execPath, Qt::CaseInsensitive) == 0) + { + iconName = desktop.value(U8Str("Desktop Entry/Icon")).toString(); + } + + else if (mAppId.compare(name, Qt::CaseInsensitive) == 0) + { + iconName = desktop.value(U8Str("Desktop Entry/Icon")).toString(); + } + + else if (mAppId.compare(cls, Qt::CaseInsensitive) == 0) + { + iconName = desktop.value(U8Str("Desktop Entry/Icon")).toString(); + } + + if (not iconName.isEmpty()) + { + if (QIcon::hasThemeIcon(iconName)) + { + return QIcon::fromTheme(iconName); + } + + else if (QFile::exists(iconName)) + { + return QIcon(iconName); + } + + else + { + iconName = getPixmapIcon(iconName); + + if (not iconName.isEmpty()) + { + return QIcon(iconName); + } + } + } + } + } + + iconName = getPixmapIcon(iconName); + + if (not iconName.isEmpty()) + { + return QIcon(iconName); + } + + return QIcon(); +} + + +static inline wl_seat *get_seat() +{ + QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface(); + + if (!native) + { + return nullptr; + } + + struct wl_seat *seat = reinterpret_cast(native->nativeResourceForIntegration("wl_seat")); + + return seat; +} + + +/* + * LXQtTaskbarWlrootsWindowManagment + */ + +LXQtTaskbarWlrootsWindowManagment::LXQtTaskbarWlrootsWindowManagment() : QWaylandClientExtensionTemplate(version) +{ + /** Automatically destroy thie object */ + connect( + this, &QWaylandClientExtension::activeChanged, this, [ this ] { + if (!isActive()) + { + zwlr_foreign_toplevel_manager_v1_destroy(object()); + } + }); +} + + +LXQtTaskbarWlrootsWindowManagment::~LXQtTaskbarWlrootsWindowManagment() +{ + if (isActive()) + { + zwlr_foreign_toplevel_manager_v1_destroy(object()); + } +} + + +void LXQtTaskbarWlrootsWindowManagment::zwlr_foreign_toplevel_manager_v1_toplevel(struct ::zwlr_foreign_toplevel_handle_v1 *toplevel) +{ + /** + * A window was created. + * Wait for the window to become ready, i.e. wait for done() event to be sent by the compositor. + * Once we recieve done(), emit the windowReady() signal. + */ + + auto w = new LXQtTaskbarWlrootsWindow(toplevel); + + connect(w, &LXQtTaskbarWlrootsWindow::windowReady, [w, this] () { + emit windowCreated(w->getWindowId()); + }); +} + + +/* + * LXQtTaskbarWlrootsWindow + */ + +LXQtTaskbarWlrootsWindow::LXQtTaskbarWlrootsWindow(::zwlr_foreign_toplevel_handle_v1 *id) : zwlr_foreign_toplevel_handle_v1(id) +{ +} + + +LXQtTaskbarWlrootsWindow::~LXQtTaskbarWlrootsWindow() +{ + destroy(); +} + + +void LXQtTaskbarWlrootsWindow::activate() +{ + /** + * Activate on default seat. + * TODO: Worry about multi-seat setups, when we have no other worries :P + */ + zwlr_foreign_toplevel_handle_v1::activate(get_seat()); +} + + +void LXQtTaskbarWlrootsWindow::zwlr_foreign_toplevel_handle_v1_title(const QString& title) +{ + /** Store the incoming title in pending */ + m_pendingState.title = title; + m_pendingState.titleChanged = true; +} + + +void LXQtTaskbarWlrootsWindow::zwlr_foreign_toplevel_handle_v1_app_id(const QString& app_id) +{ + /** Store the incoming appId in pending */ + m_pendingState.appId = app_id; + m_pendingState.appIdChanged = true; + + /** Update the icon */ + this->icon = getIconForAppId(app_id); + + /** We did not get any icon from app-id. Let's use application-x-executable */ + if (this->icon.pixmap(64).width() == 0) + { + this->icon = XdgIcon::fromTheme(QString::fromUtf8("application-x-executable")); + } +} + + +void LXQtTaskbarWlrootsWindow::zwlr_foreign_toplevel_handle_v1_output_enter(struct ::wl_output *output) +{ + /** This view was added to an output */ + m_pendingState.outputs << output; + m_pendingState.outputsChanged = true; +} + + +void LXQtTaskbarWlrootsWindow::zwlr_foreign_toplevel_handle_v1_output_leave(struct ::wl_output *output) +{ + /** This view was removed from an output; store it in pending. */ + m_pendingState.outputsLeft << output; + + if (m_pendingState.outputs.contains(output)) + { + m_pendingState.outputs.removeAll(output); + } + + m_pendingState.outputsChanged = true; +} + + +void LXQtTaskbarWlrootsWindow::zwlr_foreign_toplevel_handle_v1_state(wl_array *state) +{ + /** State of this window was changed; store it in pending. */ + auto *states = static_cast(state->data); + int numStates = static_cast(state->size / sizeof(uint32_t)); + + for (int i = 0; i < numStates; i++) + { + switch ((uint32_t)states[ i ]) + { + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MAXIMIZED: { + m_pendingState.maximized = true; + m_pendingState.maximizedChanged = true; + break; + } + + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MINIMIZED: { + m_pendingState.minimized = true; + m_pendingState.minimizedChanged = true; + break; + } + + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ACTIVATED: { + m_pendingState.activated = true; + m_pendingState.activatedChanged = true; + break; + } + + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_FULLSCREEN: { + m_pendingState.fullscreen = true; + m_pendingState.fullscreenChanged = true; + break; + } + } + } +} + + +void LXQtTaskbarWlrootsWindow::zwlr_foreign_toplevel_handle_v1_done() +{ + /** + * All the states/properties have been sent. + * We can now emit the signals and clear the pending state: + * 1. Update all the variables first. + * 2. Then clear the m_pendingState. + * 3. Emit the changed signals. + * 4. Finally, cleanr the m_pendingState.Changed flags. + */ + + // (1) title, if it changed + if (m_pendingState.titleChanged) + { + windowState.title = m_pendingState.title; + } + + // (2) appId, if it changed + if (m_pendingState.appIdChanged) + { + windowState.appId = m_pendingState.appId; + } + + // (3) outputs, if they changed + if (m_pendingState.outputsChanged) + { + for (::wl_output *op: m_pendingState.outputsLeft) + { + if (windowState.outputs.contains(op)) + { + windowState.outputs.removeAll(op); + } + } + + for (::wl_output *op: m_pendingState.outputs) + { + if (!windowState.outputs.contains(op)) + { + windowState.outputs << op; + } + } + } + + // (4) states, if they changed. Don't trust the changed flag. + if (m_pendingState.maximized != windowState.maximized) + { + windowState.maximized = m_pendingState.maximized; + m_pendingState.maximizedChanged = true; + } + + if (m_pendingState.minimized != windowState.minimized) + { + windowState.minimized = m_pendingState.minimized; + m_pendingState.minimizedChanged = true; + } + + if (m_pendingState.activated != windowState.activated) + { + windowState.activated = m_pendingState.activated; + m_pendingState.activatedChanged = true; + } + + if (m_pendingState.fullscreen != windowState.fullscreen) + { + windowState.fullscreen = m_pendingState.fullscreen; + m_pendingState.fullscreenChanged = true; + } + + // (5) parent, if it changed. + if (m_pendingState.parentChanged) + { + if (m_pendingState.parent) + { + setParentWindow(new LXQtTaskbarWlrootsWindow(m_pendingState.parent)); + } + + else + { + setParentWindow(nullptr); + } + } + + /** 2. Clear all m_pendingState. for next run */ + m_pendingState.title = QString(); + m_pendingState.appId = QString(); + m_pendingState.outputs.clear(); + m_pendingState.maximized = false; + m_pendingState.minimized = false; + m_pendingState.activated = false; + m_pendingState.fullscreen = false; + m_pendingState.parent = nullptr; + + /** + * 3. Emit signals + * (a) First time done was emitted after the window was created. + * (b) Other times. + */ + + /** (a) First time done was emitted */ + if (initDone == false) + { + /** + * All the states/properties are already set. + * Any query will give valid results. + */ + initDone = true; + emit windowReady(); + } + + /** (b) All the subsequent times */ + else + { + if (m_pendingState.titleChanged) + emit titleChanged(); + if (m_pendingState.appIdChanged) + emit appIdChanged(); + if (m_pendingState.outputsChanged) + emit outputsChanged(); + if (m_pendingState.maximizedChanged) + emit maximizedChanged(); + if (m_pendingState.minimizedChanged) + emit minimizedChanged(); + if (m_pendingState.activatedChanged) + emit activatedChanged(); + if (m_pendingState.fullscreenChanged) + emit fullscreenChanged(); + if (m_pendingState.parentChanged) + emit parentChanged(); + + emit stateChanged(); + } + + /** 4. Clear m+m_pendingState.Changed flags */ + m_pendingState.titleChanged = false; + m_pendingState.appIdChanged = false; + m_pendingState.outputsChanged = false; + m_pendingState.maximizedChanged = false; + m_pendingState.minimizedChanged = false; + m_pendingState.activatedChanged = false; + m_pendingState.fullscreenChanged = false; + m_pendingState.parentChanged = false; +} + + +void LXQtTaskbarWlrootsWindow::zwlr_foreign_toplevel_handle_v1_closed() +{ + /** This window was closed */ + emit closed(); +} + + +void LXQtTaskbarWlrootsWindow::zwlr_foreign_toplevel_handle_v1_parent(struct ::zwlr_foreign_toplevel_handle_v1 *parent) +{ + /** Parent of this window changed; store it in pending. */ + m_pendingState.parent = parent; + m_pendingState.parentChanged = true; +} + + +void LXQtTaskbarWlrootsWindow::setParentWindow(LXQtTaskbarWlrootsWindow *parent) +{ + QObject::disconnect(parentWindowUnmappedConnection); + + if (parent) + { + parentWindow = parent->getWindowId(); + parentWindowUnmappedConnection = QObject::connect( + parent, &LXQtTaskbarWlrootsWindow::closed, this, [ this ] { + setParentWindow(nullptr); + }); + } + else + { + parentWindow = 0; + parentWindowUnmappedConnection = QMetaObject::Connection(); + } +} diff --git a/panel/backends/wayland/wlroots/lxqttaskbarwlrwindowmanagment.h b/panel/backends/wayland/wlroots/lxqttaskbarwlrwindowmanagment.h new file mode 100644 index 000000000..c79f174da --- /dev/null +++ b/panel/backends/wayland/wlroots/lxqttaskbarwlrwindowmanagment.h @@ -0,0 +1,136 @@ +#pragma once + +#include +#include +#include + +#include "qwayland-wlr-foreign-toplevel-management-unstable-v1.h" +#include "wayland-wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" + +typedef quintptr WId; + +class LXQtTaskbarWlrootsWindow; + +class LXQtTaskbarWlrootsWindowManagment : public QWaylandClientExtensionTemplate, + public QtWayland::zwlr_foreign_toplevel_manager_v1 +{ + Q_OBJECT +public: + static constexpr int version = 16; + + LXQtTaskbarWlrootsWindowManagment(); + ~LXQtTaskbarWlrootsWindowManagment(); + + inline bool isShowingDesktop() const { return m_isShowingDesktop; } + +protected: + void zwlr_foreign_toplevel_manager_v1_toplevel(struct ::zwlr_foreign_toplevel_handle_v1 *toplevel); + void zwlr_foreign_toplevel_manager_v1_finished() {}; + +Q_SIGNALS: + void windowCreated(WId wid); + +private: + bool m_isShowingDesktop = false; +}; + +using WindowState = QtWayland::zwlr_foreign_toplevel_handle_v1::state; + +class WindowProperties { + public: + /** Title of the window */ + QString title = QString::fromUtf8( "untitled" ); + bool titleChanged = false; + + /** appId of the window */ + QString appId = QString::fromUtf8( "unidentified" ); + bool appIdChanged = false; + + /** List of outputs which the window is currently on */ + QList<::wl_output *> outputs; + bool outputsChanged = false; + + /** Is maximized */ + bool maximized = false; + bool maximizedChanged = false; + + /** Is minimized */ + bool minimized = false; + bool minimizedChanged = false; + + /** Is active */ + bool activated = false; + bool activatedChanged = false; + + /** Is fullscreen */ + bool fullscreen = false; + bool fullscreenChanged = false; + + /** Parent of this view, can be null */ + ::zwlr_foreign_toplevel_handle_v1 * parent = nullptr; + bool parentChanged = false; + + /** List of outputs from which window has left */ + QList<::wl_output *> outputsLeft; +}; + +class LXQtTaskbarWlrootsWindow : public QObject, + public QtWayland::zwlr_foreign_toplevel_handle_v1 +{ + Q_OBJECT +public: + LXQtTaskbarWlrootsWindow(::zwlr_foreign_toplevel_handle_v1 *id); + ~LXQtTaskbarWlrootsWindow(); + + inline WId getWindowId() const { return reinterpret_cast(this); } + + void activate(); + + QIcon icon; + WindowProperties windowState; + WId parentWindow = 0; + +Q_SIGNALS: + void titleChanged(); + void appIdChanged(); + void outputsChanged(); + + /** Individual state change signals */ + void maximizedChanged(); + void minimizedChanged(); + void activatedChanged(); + void fullscreenChanged(); + + void parentChanged(); + + /** Bulk state change signal */ + void stateChanged(); + + /** First state change signal: Before this, the window did not have a valid state */ + void windowReady(); + + /** All state changes have been sent. */ + void done(); + + /** Window closed signal */ + void closed(); + +protected: + void zwlr_foreign_toplevel_handle_v1_title(const QString &title); + void zwlr_foreign_toplevel_handle_v1_app_id(const QString &app_id); + void zwlr_foreign_toplevel_handle_v1_output_enter(struct ::wl_output *output); + void zwlr_foreign_toplevel_handle_v1_output_leave(struct ::wl_output *output); + void zwlr_foreign_toplevel_handle_v1_state(wl_array *state); + void zwlr_foreign_toplevel_handle_v1_done(); + void zwlr_foreign_toplevel_handle_v1_closed(); + void zwlr_foreign_toplevel_handle_v1_parent(struct ::zwlr_foreign_toplevel_handle_v1 *parent); + +private: + void setParentWindow(LXQtTaskbarWlrootsWindow *parent); + + QMetaObject::Connection parentWindowUnmappedConnection; + + WindowProperties m_pendingState; + + bool initDone = false; +}; diff --git a/panel/backends/wayland/wlroots/lxqtwlrvirtualdesktop.cpp b/panel/backends/wayland/wlroots/lxqtwlrvirtualdesktop.cpp new file mode 100644 index 000000000..51e2974ac --- /dev/null +++ b/panel/backends/wayland/wlroots/lxqtwlrvirtualdesktop.cpp @@ -0,0 +1,104 @@ +#include "lxqtwlrvirtualdesktop.h" + +#include + +LXQtWlrootsVirtualDesktop::LXQtWlrootsVirtualDesktop(const QString &id) + : id(id) + , name(QString::fromUtf8("Desktop 1")) +{ +} + +LXQtWlrootsVirtualDesktop::~LXQtWlrootsVirtualDesktop() +{ +} + +LXQtWlrootsVirtualDesktopManagment::LXQtWlrootsVirtualDesktopManagment() : QObject() +{ +} + +LXQtWlrootsVirtualDesktopManagment::~LXQtWlrootsVirtualDesktopManagment() +{ +} + +LXQtWlrootsWaylandWorkspaceInfo::LXQtWlrootsWaylandWorkspaceInfo() +{ + init(); +} + +LXQtWlrootsWaylandWorkspaceInfo::VirtualDesktopsIterator LXQtWlrootsWaylandWorkspaceInfo::findDesktop(const QString &id) const +{ + return std::find_if(virtualDesktops.begin(), virtualDesktops.end(), + [&id](const std::unique_ptr &desktop) { + return desktop->id == id; + }); +} + +QString LXQtWlrootsWaylandWorkspaceInfo::getDesktopName(int pos) const +{ + if(pos < 0 || size_t(pos) >= virtualDesktops.size()) + return QString(); + return virtualDesktops[pos]->name; +} + +QString LXQtWlrootsWaylandWorkspaceInfo::getDesktopId(int pos) const +{ + if(pos < 0 || size_t(pos) >= virtualDesktops.size()) + return QString(); + return virtualDesktops[pos]->id; +} + +void LXQtWlrootsWaylandWorkspaceInfo::init() +{ + virtualDesktopManagement = std::make_unique(); + auto desktopOne = std::make_unique(QString::fromUtf8("desktop-1")); + virtualDesktops.insert(std::next(virtualDesktops.begin(), 0), std::move(desktopOne)); + + currentVirtualDesktop = QString::fromUtf8( "desktop-1" ); +} + +void LXQtWlrootsWaylandWorkspaceInfo::addDesktop(const QString &, quint32) +{ +} + +QVariant LXQtWlrootsWaylandWorkspaceInfo::currentDesktop() const +{ + return currentVirtualDesktop; +} + +int LXQtWlrootsWaylandWorkspaceInfo::numberOfDesktops() const +{ + // will always be 1 (at least until ext-workspace becomes available) + return 1; +} + +quint32 LXQtWlrootsWaylandWorkspaceInfo::position(const QVariant &desktop) const +{ + return 0; +} + +QVariantList LXQtWlrootsWaylandWorkspaceInfo::desktopIds() const +{ + return { virtualDesktops.at( 0 ).get()->id }; +} + +QStringList LXQtWlrootsWaylandWorkspaceInfo::desktopNames() const +{ + return { virtualDesktops.at( 0 ).get()->name }; +} + +int LXQtWlrootsWaylandWorkspaceInfo::desktopLayoutRows() const +{ + return 1; +} + +void LXQtWlrootsWaylandWorkspaceInfo::requestActivate(const QVariant &) +{ +} + +void LXQtWlrootsWaylandWorkspaceInfo::requestCreateDesktop(quint32 position) +{ +} + +void LXQtWlrootsWaylandWorkspaceInfo::requestRemoveDesktop(quint32 position) +{ +} diff --git a/panel/backends/wayland/wlroots/lxqtwlrvirtualdesktop.h b/panel/backends/wayland/wlroots/lxqtwlrvirtualdesktop.h new file mode 100644 index 000000000..4853c43a6 --- /dev/null +++ b/panel/backends/wayland/wlroots/lxqtwlrvirtualdesktop.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include + +class LXQtWlrootsVirtualDesktop : public QObject +{ + Q_OBJECT +public: + LXQtWlrootsVirtualDesktop(const QString &id); + ~LXQtWlrootsVirtualDesktop(); + const QString id; + QString name; +Q_SIGNALS: + void done(); + void activated(); + void nameChanged(); +}; + +class LXQtWlrootsVirtualDesktopManagment : public QObject +{ + Q_OBJECT +public: + LXQtWlrootsVirtualDesktopManagment(); + ~LXQtWlrootsVirtualDesktopManagment(); + +signals: + void desktopCreated(const QString &id, quint32 position); + void desktopRemoved(const QString &id); + void rowsChanged(const quint32 rows); +}; + +class Q_DECL_HIDDEN LXQtWlrootsWaylandWorkspaceInfo : public QObject +{ + Q_OBJECT +public: + LXQtWlrootsWaylandWorkspaceInfo(); + + QVariant currentVirtualDesktop; + std::vector> virtualDesktops; + std::unique_ptr virtualDesktopManagement; + const quint32 rows = 1; + + typedef std::vector>::const_iterator VirtualDesktopsIterator; + + VirtualDesktopsIterator findDesktop(const QString &id) const; + + QString getDesktopName(int pos) const; + QString getDesktopId(int pos) const; + + void init(); + void addDesktop(const QString &id, quint32 pos); + QVariant currentDesktop() const; + int numberOfDesktops() const; + QVariantList desktopIds() const; + QStringList desktopNames() const; + quint32 position(const QVariant &desktop) const; + int desktopLayoutRows() const; + void requestActivate(const QVariant &desktop); + void requestCreateDesktop(quint32 position); + void requestRemoveDesktop(quint32 position); + +signals: + void currentDesktopChanged(); + void numberOfDesktopsChanged(); + void navigationWrappingAroundChanged(); + void desktopIdsChanged(); + void desktopNameChanged(quint32 position); + void desktopLayoutRowsChanged(); +}; diff --git a/panel/backends/wayland/wlroots/protocols/wlr-foreign-toplevel-management-unstable-v1.xml b/panel/backends/wayland/wlroots/protocols/wlr-foreign-toplevel-management-unstable-v1.xml new file mode 100644 index 000000000..108133715 --- /dev/null +++ b/panel/backends/wayland/wlroots/protocols/wlr-foreign-toplevel-management-unstable-v1.xml @@ -0,0 +1,270 @@ + + + + Copyright © 2018 Ilia Bozhinov + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + The purpose of this protocol is to enable the creation of taskbars + and docks by providing them with a list of opened applications and + letting them request certain actions on them, like maximizing, etc. + + After a client binds the zwlr_foreign_toplevel_manager_v1, each opened + toplevel window will be sent via the toplevel event + + + + + This event is emitted whenever a new toplevel window is created. It + is emitted for all toplevels, regardless of the app that has created + them. + + All initial details of the toplevel(title, app_id, states, etc.) will + be sent immediately after this event via the corresponding events in + zwlr_foreign_toplevel_handle_v1. + + + + + + + Indicates the client no longer wishes to receive events for new toplevels. + However the compositor may emit further toplevel_created events, until + the finished event is emitted. + + The client must not send any more requests after this one. + + + + + + This event indicates that the compositor is done sending events to the + zwlr_foreign_toplevel_manager_v1. The server will destroy the object + immediately after sending this request, so it will become invalid and + the client should free any resources associated with it. + + + + + + + A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel + window. Each app may have multiple opened toplevels. + + Each toplevel has a list of outputs it is visible on, conveyed to the + client with the output_enter and output_leave events. + + + + + This event is emitted whenever the title of the toplevel changes. + + + + + + + This event is emitted whenever the app-id of the toplevel changes. + + + + + + + This event is emitted whenever the toplevel becomes visible on + the given output. A toplevel may be visible on multiple outputs. + + + + + + + This event is emitted whenever the toplevel stops being visible on + the given output. It is guaranteed that an entered-output event + with the same output has been emitted before this event. + + + + + + + Requests that the toplevel be maximized. If the maximized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be unmaximized. If the maximized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be minimized. If the minimized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be unminimized. If the minimized state actually + changes, this will be indicated by the state event. + + + + + + Request that this toplevel be activated on the given seat. + There is no guarantee the toplevel will be actually activated. + + + + + + + The different states that a toplevel can have. These have the same meaning + as the states with the same names defined in xdg-toplevel + + + + + + + + + + + This event is emitted immediately after the zlw_foreign_toplevel_handle_v1 + is created and each time the toplevel state changes, either because of a + compositor action or because of a request in this protocol. + + + + + + + + This event is sent after all changes in the toplevel state have been + sent. + + This allows changes to the zwlr_foreign_toplevel_handle_v1 properties + to be seen as atomic, even if they happen via multiple events. + + + + + + Send a request to the toplevel to close itself. The compositor would + typically use a shell-specific method to carry out this request, for + example by sending the xdg_toplevel.close event. However, this gives + no guarantees the toplevel will actually be destroyed. If and when + this happens, the zwlr_foreign_toplevel_handle_v1.closed event will + be emitted. + + + + + + The rectangle of the surface specified in this request corresponds to + the place where the app using this protocol represents the given toplevel. + It can be used by the compositor as a hint for some operations, e.g + minimizing. The client is however not required to set this, in which + case the compositor is free to decide some default value. + + If the client specifies more than one rectangle, only the last one is + considered. + + The dimensions are given in surface-local coordinates. + Setting width=height=0 removes the already-set rectangle. + + + + + + + + + + + + + + + + This event means the toplevel has been destroyed. It is guaranteed there + won't be any more events for this zwlr_foreign_toplevel_handle_v1. The + toplevel itself becomes inert so any requests will be ignored except the + destroy request. + + + + + + Destroys the zwlr_foreign_toplevel_handle_v1 object. + + This request should be called either when the client does not want to + use the toplevel anymore or after the closed event to finalize the + destruction of the object. + + + + + + + + Requests that the toplevel be fullscreened on the given output. If the + fullscreen state and/or the outputs the toplevel is visible on actually + change, this will be indicated by the state and output_enter/leave + events. + + The output parameter is only a hint to the compositor. Also, if output + is NULL, the compositor should decide which output the toplevel will be + fullscreened on, if at all. + + + + + + + Requests that the toplevel be unfullscreened. If the fullscreen state + actually changes, this will be indicated by the state event. + + + + + + + + This event is emitted whenever the parent of the toplevel changes. + + No event is emitted when the parent handle is destroyed by the client. + + + + + diff --git a/panel/backends/xcb/CMakeLists.txt b/panel/backends/xcb/CMakeLists.txt index 8b1378917..ad36ccaa3 100644 --- a/panel/backends/xcb/CMakeLists.txt +++ b/panel/backends/xcb/CMakeLists.txt @@ -1 +1,14 @@ +# XCB WM Backend + +project(lxqt-panel-backend-xcb) + +add_library(lxqt-panel-backend-xcb STATIC + lxqttaskbarbackend_x11.h + lxqttaskbarbackend_x11.cpp +) + +target_link_libraries(lxqt-panel-backend-xcb + KF6::WindowSystem + lxqt-panel-backend-common +) diff --git a/panel/backends/xcb/lxqttaskbarbackend_x11.cpp b/panel/backends/xcb/lxqttaskbarbackend_x11.cpp index fe0452776..8156a729e 100644 --- a/panel/backends/xcb/lxqttaskbarbackend_x11.cpp +++ b/panel/backends/xcb/lxqttaskbarbackend_x11.cpp @@ -1,3 +1,31 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2023 LXQt team + * Authors: + * Filippo Gentile + * + * This program or library is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * END_COMMON_COPYRIGHT_HEADER */ + + #include "lxqttaskbarbackend_x11.h" #include diff --git a/panel/backends/xcb/lxqttaskbarbackend_x11.h b/panel/backends/xcb/lxqttaskbarbackend_x11.h index 2478b3fff..fe2e30783 100644 --- a/panel/backends/xcb/lxqttaskbarbackend_x11.h +++ b/panel/backends/xcb/lxqttaskbarbackend_x11.h @@ -1,3 +1,31 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2023 LXQt team + * Authors: + * Filippo Gentile + * + * This program or library is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * END_COMMON_COPYRIGHT_HEADER */ + + #ifndef LXQTTASKBARBACKEND_X11_H #define LXQTTASKBARBACKEND_X11_H diff --git a/panel/lxqtpanel.cpp b/panel/lxqtpanel.cpp index f4144ff41..0c676eb4b 100644 --- a/panel/lxqtpanel.cpp +++ b/panel/lxqtpanel.cpp @@ -54,7 +54,6 @@ #include "backends/ilxqttaskbarabstractbackend.h" - #include // Turn on this to show the time required to load each plugin during startup @@ -1416,48 +1415,60 @@ Plugin* LXQtPanel::findPlugin(const ILXQtPanelPlugin* iPlugin) const ************************************************/ QRect LXQtPanel::calculatePopupWindowPos(QPoint const & absolutePos, QSize const & windowSize) const { - int x = absolutePos.x(), y = absolutePos.y(); + QPoint localPos = mapFromGlobal(absolutePos); + int x = localPos.x(), y = localPos.y(); switch (position()) { case ILXQtPanel::PositionTop: - y = mGeometry.bottom(); + y = geometry().height(); break; case ILXQtPanel::PositionBottom: - y = mGeometry.top() - windowSize.height(); + y = 0 - windowSize.height(); break; case ILXQtPanel::PositionLeft: - x = mGeometry.right(); + x = geometry().right(); break; case ILXQtPanel::PositionRight: - x = mGeometry.left() - windowSize.width(); + x = geometry().left() - windowSize.width(); break; } QRect res(QPoint(x, y), windowSize); - QRect panelScreen; - const auto screens = QApplication::screens(); - if (mActualScreenNum < screens.size()) - panelScreen = screens.at(mActualScreenNum)->geometry(); - // NOTE: We cannot use AvailableGeometry() which returns the work area here because when in a - // multihead setup with different resolutions. In this case, the size of the work area is limited - // by the smallest monitor and may be much smaller than the current screen and we will place the - // menu at the wrong place. This is very bad for UX. So let's use the full size of the screen. - if (res.right() > panelScreen.right()) - res.moveRight(panelScreen.right()); + // Map to global coordinates + res = QRect(mapToGlobal(res.topLeft()), mapToGlobal(res.bottomRight())); + + if(qGuiApp->nativeInterface()) + { + //On X11 we clamp rects inside screen area. + //NOTE: On Wayland it's done by compositor - if (res.bottom() > panelScreen.bottom()) - res.moveBottom(panelScreen.bottom()); + QRect panelScreen; + const auto screens = QApplication::screens(); + if (mActualScreenNum < screens.size()) + panelScreen = screens.at(mActualScreenNum)->geometry(); - if (res.left() < panelScreen.left()) - res.moveLeft(panelScreen.left()); + // NOTE: We cannot use AvailableGeometry() which returns the work area here because when in a + // multihead setup with different resolutions. In this case, the size of the work area is limited + // by the smallest monitor and may be much smaller than the current screen and we will place the + // menu at the wrong place. This is very bad for UX. So let's use the full size of the screen. - if (res.top() < panelScreen.top()) - res.moveTop(panelScreen.top()); + if (res.right() > panelScreen.right()) + res.moveRight(panelScreen.right()); + + if (res.bottom() > panelScreen.bottom()) + res.moveBottom(panelScreen.bottom() - mGeometry.top()); + + if (res.left() < panelScreen.left()) + res.moveLeft(panelScreen.left()); + + if (res.top() < panelScreen.top()) + res.moveTop(panelScreen.top() - mGeometry.top()); + } return res; } @@ -1479,7 +1490,7 @@ QRect LXQtPanel::calculatePopupWindowPos(const ILXQtPanelPlugin *plugin, const Q } // Note: assuming there are not contentMargins around the "BackgroundWidget" (LXQtPanelWidget) - return calculatePopupWindowPos(mGeometry.topLeft() + panel_plugin->geometry().topLeft(), windowSize); + return calculatePopupWindowPos(mapToGlobal(panel_plugin->geometry().topLeft()), windowSize); } diff --git a/panel/lxqtpanelapplication.cpp b/panel/lxqtpanelapplication.cpp index c3b666620..a3d2132f1 100644 --- a/panel/lxqtpanelapplication.cpp +++ b/panel/lxqtpanelapplication.cpp @@ -40,11 +40,14 @@ #include "backends/lxqttaskbardummybackend.h" #include "backends/xcb/lxqttaskbarbackend_x11.h" +#include "backends/wayland/lxqttaskbarbackendwayland.h" ILXQtTaskbarAbstractBackend *createWMBackend() { if(qGuiApp->nativeInterface()) return new LXQtTaskbarX11Backend; + else if(qGuiApp->nativeInterface()) + return new LXQtTaskbarWaylandBackend; qWarning() << "\n" << "ERROR: Could not create a backend for window managment operations.\n" @@ -104,6 +107,8 @@ LXQtPanelApplication::LXQtPanelApplication(int& argc, char** argv) QCoreApplication::setApplicationVersion(VERINFO); + QGuiApplication::setDesktopFileName(QLatin1String("lxqt-panel")); + QCommandLineParser parser; parser.setApplicationDescription(QLatin1String("LXQt Panel")); parser.addHelpOption(); diff --git a/plugin-colorpicker/colorpicker.cpp b/plugin-colorpicker/colorpicker.cpp index 004314557..0827686d5 100644 --- a/plugin-colorpicker/colorpicker.cpp +++ b/plugin-colorpicker/colorpicker.cpp @@ -36,6 +36,9 @@ #include #include +#include +#include + //NOTE: Xlib.h defines Bool which conflicts with QJsonValue::Type enum #include #undef Bool @@ -77,6 +80,33 @@ void ColorPicker::realign() mWidget.update(panel()->lineCount() <= 1 ? panel()->isHorizontal() : !panel()->isHorizontal()); } +void ColorPicker::queryXDGSupport() +{ + if (qEnvironmentVariableIntValue("QT_NO_XDG_DESKTOP_PORTAL") > 0) { + return; + } + QDBusMessage message = QDBusMessage::createMethodCall( + QLatin1String("org.freedesktop.portal.Desktop"), + QLatin1String("/org/freedesktop/portal/desktop"), + QLatin1String("org.freedesktop.DBus.Properties"), + QLatin1String("Get")); + message << QLatin1String("org.freedesktop.portal.Screenshot") + << QLatin1String("version"); + + QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message); + auto watcher = new QDBusPendingCallWatcher(pendingCall); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, watcher, + [this](QDBusPendingCallWatcher *watcher) { + watcher->deleteLater(); + QDBusPendingReply reply = *watcher; + if (!reply.isError() && reply.value().toUInt() >= 2) + m_hasScreenshotPortalWithColorPicking = true; + }); + + //TODO: show error tooltip if not supported + //NOTE: on Wayland we cannot pick color without it +} + ColorPickerWidget::ColorPickerWidget(QWidget *parent) : QWidget(parent) { @@ -108,7 +138,8 @@ ColorPickerWidget::ColorPickerWidget(QWidget *parent) : QWidget(parent) layout->addWidget(mColorButton); setLayout(layout); - connect(mPickerButton, &QToolButton::clicked, this, &ColorPickerWidget::captureMouse); + connect(mPickerButton, &QToolButton::clicked, this, &ColorPickerWidget::startCapturingColor); + connect(mColorButton, &QToolButton::clicked, this, [&]() { buildMenu(); @@ -162,29 +193,86 @@ void ColorPickerWidget::mouseReleaseEvent(QMouseEvent *event) qWarning() << "WAYLAND does not support grabbing windows"; } - mColorButton->setColor(col); - paste(col.name()); + setCapturedColor(col); - if (mColorsList.contains(col)) + mCapturing = false; + releaseMouse(); + + if (!mPickerButton->contentsRect().contains(mapFromGlobal(QCursor::pos()))) { - mColorsList.move(mColorsList.indexOf(col), 0); + QApplication::sendEvent(mPickerButton, new QEvent(QEvent::Leave)); } - else +} + +void ColorPickerWidget::startCapturingColor() +{ + //NOTE: see qt6 `src/gui/platform/unix/qgenericunixservices.cpp` + + // Make double sure that we are in a wayland environment. In particular check + // WAYLAND_DISPLAY so also XWayland apps benefit from portal-based color picking. + // Outside wayland we'll rather rely on other means than the XDG desktop portal. + if (!qEnvironmentVariableIsEmpty("WAYLAND_DISPLAY") + || QGuiApplication::platformName().startsWith(QLatin1String("wayland"))) { - mColorsList.prepend(col); + // On Wayland use XDG Desktop Portal + + QString m_parentWindowId; //TODO + + QDBusMessage message = QDBusMessage::createMethodCall( + QLatin1String("org.freedesktop.portal.Desktop"), + QLatin1String("/org/freedesktop/portal/desktop"), + QLatin1String("org.freedesktop.portal.Screenshot"), + QLatin1String("PickColor")); + message << m_parentWindowId << QVariantMap(); + + QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message); + auto watcher = new QDBusPendingCallWatcher(pendingCall, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, + [this](QDBusPendingCallWatcher *watcher) { + watcher->deleteLater(); + QDBusPendingReply reply = *watcher; + if (reply.isError()) { + qWarning("DBus call to pick color failed: %s", + qPrintable(reply.error().message())); + setCapturedColor({}); + } else { + QDBusConnection::sessionBus().connect( + QLatin1String("org.freedesktop.portal.Desktop"), + reply.value().path(), + QLatin1String("org.freedesktop.portal.Request"), + QLatin1String("Response"), + this, + // clang-format off + SLOT(gotColorResponse(uint,QVariantMap)) + // clang-format on + ); + } + }); } - - if (mColorsList.size() > 10) + else if (qGuiApp->nativeInterface()) { - mColorsList.removeLast(); + // On X11 grab mouse and let `mouseReleaseEvent()` retrieve color + captureMouse(); } +} - mCapturing = false; - releaseMouse(); +void ColorPickerWidget::setCapturedColor(const QColor &color) +{ + mColorButton->setColor(color); + paste(color.name()); - if (!mPickerButton->contentsRect().contains(mapFromGlobal(QCursor::pos()))) + if (mColorsList.contains(color)) { - QApplication::sendEvent(mPickerButton, new QEvent(QEvent::Leave)); + mColorsList.move(mColorsList.indexOf(color), 0); + } + else + { + mColorsList.prepend(color); + } + + if (mColorsList.size() > 10) + { + mColorsList.removeLast(); } } @@ -195,6 +283,46 @@ void ColorPickerWidget::captureMouse() mCapturing = true; } +struct XDGDesktopColor +{ + double r = 0; + double g = 0; + double b = 0; + + QColor toQColor() const + { + constexpr auto rgbMax = 255; + return { static_cast(r * rgbMax), static_cast(g * rgbMax), + static_cast(b * rgbMax) }; + } +}; + +const QDBusArgument &operator>>(const QDBusArgument &argument, XDGDesktopColor &myStruct) +{ + argument.beginStructure(); + argument >> myStruct.r >> myStruct.g >> myStruct.b; + argument.endStructure(); + return argument; +} + +void ColorPickerWidget::gotColorResponse(uint result, const QVariantMap &map) +{ + auto colorProp = QStringLiteral("color"); + + if (result != 0) + return; + if (map.contains(colorProp)) + { + XDGDesktopColor color{}; + map.value(colorProp).value() >> color; + setCapturedColor(color.toQColor()); + } + else + { + setCapturedColor({}); + } +} + QIcon ColorPickerWidget::colorIcon(QColor color) { diff --git a/plugin-colorpicker/colorpicker.h b/plugin-colorpicker/colorpicker.h index 919f42490..ecf2a3b41 100644 --- a/plugin-colorpicker/colorpicker.h +++ b/plugin-colorpicker/colorpicker.h @@ -58,7 +58,10 @@ class ColorPickerWidget : public QWidget void mouseReleaseEvent(QMouseEvent *event); private slots: + void startCapturingColor(); + void setCapturedColor(const QColor& color); void captureMouse(); + void gotColorResponse(uint result, const QVariantMap& map); private: static const QString svgIcon; @@ -91,8 +94,13 @@ class ColorPicker : public QObject, public ILXQtPanelPlugin virtual void realign() override; +private: + void queryXDGSupport(); + private: ColorPickerWidget mWidget; + + bool m_hasScreenshotPortalWithColorPicking = false; }; class ColorPickerLibrary: public QObject, public ILXQtPanelPluginLibrary diff --git a/plugin-taskbar/lxqttaskbutton.cpp b/plugin-taskbar/lxqttaskbutton.cpp index c86e6af5f..6e49d0a85 100644 --- a/plugin-taskbar/lxqttaskbutton.cpp +++ b/plugin-taskbar/lxqttaskbutton.cpp @@ -128,7 +128,7 @@ LXQtTaskButton::~LXQtTaskButton() = default; void LXQtTaskButton::updateText() { QString title = mBackend->getWindowTitle(mWindow); - setText(title.replace(QStringLiteral("&"), QStringLiteral("&&"))); + setTextExplicitly(title.replace(QStringLiteral("&"), QStringLiteral("&&"))); setToolTip(title); } @@ -314,6 +314,30 @@ QMimeData * LXQtTaskButton::mimeData() return mimedata; } +/*! + * \brief LXQtTaskButton::setTextExplicitly + * \param str + * + * This is needed to workaround flickering caused by KAcceleratorManager + * This class is hooked by KDE Integration and adds accelerators to button text + * (Adds some '&' characters) + * This triggers widget update but soon after text is reset to original value + * This triggers a KAcceleratorManager update which again adds accelerator + * This happens in loop + * + * TODO: investigate proper solution + */ +void LXQtTaskButton::setTextExplicitly(const QString &str) +{ + if(str == mExplicitlySetText) + { + return; + } + + mExplicitlySetText = str; + setText(mExplicitlySetText); +} + /************************************************ ************************************************/ @@ -666,6 +690,9 @@ void LXQtTaskButton::contextMenuEvent(QContextMenuEvent* event) menu->addSeparator(); QMenu* layerMenu = menu->addMenu(tr("&Layer")); + layerMenu->setEnabled(mBackend->supportsAction(mWindow, LXQtTaskBarBackendAction::SetLayer)); + + LXQtTaskBarWindowLayer currentLayer = mBackend->getWindowLayer(mWindow); LXQtTaskBarWindowLayer currentLayer = mBackend->getWindowLayer(mWindow); @@ -688,6 +715,7 @@ void LXQtTaskButton::contextMenuEvent(QContextMenuEvent* event) menu->addSeparator(); a = menu->addAction(XdgIcon::fromTheme(QStringLiteral("process-stop")), tr("&Close")); connect(a, &QAction::triggered, this, &LXQtTaskButton::closeApplication); + menu->setGeometry(mParentTaskBar->panel()->calculatePopupWindowPos(mapToGlobal(event->pos()), menu->sizeHint())); mPlugin->willShowWindow(menu); menu->show(); diff --git a/plugin-taskbar/lxqttaskbutton.h b/plugin-taskbar/lxqttaskbutton.h index 9ccca36fa..6d5841df4 100644 --- a/plugin-taskbar/lxqttaskbutton.h +++ b/plugin-taskbar/lxqttaskbutton.h @@ -122,6 +122,8 @@ public slots: inline ILXQtPanelPlugin * plugin() const { return mPlugin; } + void setTextExplicitly(const QString& str); + protected: //TODO: public getter instead? ILXQtTaskbarAbstractBackend *mBackend; @@ -138,6 +140,8 @@ public slots: int mIconSize; int mWheelDelta; + QString mExplicitlySetText; + // Timer for when draggind something into a button (the button's window // must be activated so that the use can continue dragging to the window QTimer * mDNDTimer; diff --git a/plugin-taskbar/lxqttaskgroup.cpp b/plugin-taskbar/lxqttaskgroup.cpp index 8317c034f..b86b1657c 100644 --- a/plugin-taskbar/lxqttaskgroup.cpp +++ b/plugin-taskbar/lxqttaskgroup.cpp @@ -57,7 +57,7 @@ LXQtTaskGroup::LXQtTaskGroup(const QString &groupName, WId window, LXQtTaskBar * Q_ASSERT(parent); setObjectName(groupName); - setText(groupName); + setTextExplicitly(groupName); connect(this, &LXQtTaskGroup::clicked, this, &LXQtTaskGroup::onClicked); connect(parent, &LXQtTaskBar::buttonRotationRefreshed, this, &LXQtTaskGroup::setAutoRotation); @@ -336,7 +336,7 @@ void LXQtTaskGroup::regroup() if (button) { - setText(button->text()); + setTextExplicitly(button->text()); setToolTip(button->toolTip()); setWindowId(button->windowId()); } @@ -347,7 +347,7 @@ void LXQtTaskGroup::regroup() { mSingleButton = false; QString t = QString(QStringLiteral("%1 - %2 windows")).arg(mGroupName).arg(cont); - setText(t); + setTextExplicitly(t); setToolTip(parentTaskBar()->isShowGroupOnHover() ? QString() : t); } } diff --git a/plugin-tray/CMakeLists.txt b/plugin-tray/CMakeLists.txt index a0541e226..fc51b1b50 100644 --- a/plugin-tray/CMakeLists.txt +++ b/plugin-tray/CMakeLists.txt @@ -38,6 +38,7 @@ qt_add_dbus_adaptor(SOURCES org.kde.StatusNotifierItem.xml sniproxy.h SNIProxy) qt_add_dbus_interface(SOURCES org.kde.StatusNotifierWatcher.xml statusnotifierwatcher_interface) set(LIBRARIES + Qt6::GuiPrivate ${XCB_LIBRARIES} ${xtst_LDFLAGS} ) diff --git a/plugin-tray/sniproxy.cpp b/plugin-tray/sniproxy.cpp index 9fbe924b4..4cb05ce97 100644 --- a/plugin-tray/sniproxy.cpp +++ b/plugin-tray/sniproxy.cpp @@ -38,6 +38,7 @@ #include #include +#include //For "gettimestamp" Xcb integration resource #include "kwindowinfo.h" #include "statusnotifieritemadaptor.h" @@ -590,13 +591,17 @@ void SNIProxy::sendClick(uint8_t mouseButton, int x, int y) auto *x11Application = qGuiApp->nativeInterface(); WId appRootWindow = XDefaultRootWindow(x11Application->display()); + // Qt private access + void *ptr = qGuiApp->platformNativeInterface()->nativeResourceForScreen("gettimestamp", qGuiApp->primaryScreen()); + xcb_timestamp_t timeStamp = reinterpret_cast(ptr); + // mouse down if (m_injectMode == Direct) { xcb_button_press_event_t *event = new xcb_button_press_event_t; memset(event, 0x00, sizeof(xcb_button_press_event_t)); event->response_type = XCB_BUTTON_PRESS; event->event = m_windowId; - event->time = XCB_CURRENT_TIME; //NOTE: to get proper timestamp we would need Qt Private APIs + event->time = timeStamp; event->same_screen = 1; event->root = appRootWindow; event->root_x = x; @@ -619,7 +624,7 @@ void SNIProxy::sendClick(uint8_t mouseButton, int x, int y) memset(event, 0x00, sizeof(xcb_button_release_event_t)); event->response_type = XCB_BUTTON_RELEASE; event->event = m_windowId; - event->time = XCB_CURRENT_TIME; //NOTE: to get proper timestamp we would need Qt Private APIs + event->time = timeStamp; event->same_screen = 1; event->root = appRootWindow; event->root_x = x;