diff --git a/panel/backends/ilxqtabstractwmiface.h b/panel/backends/ilxqtabstractwmiface.h index 501f80b20..d6f89a488 100644 --- a/panel/backends/ilxqtabstractwmiface.h +++ b/panel/backends/ilxqtabstractwmiface.h @@ -76,7 +76,7 @@ class LXQT_PANEL_API ILXQtAbstractWMInterface : public QObject // Workspaces // NOTE: indexes are 1-based, 0 means "Show on All desktops" virtual int getWorkspacesCount() const = 0; - virtual QString getWorkspaceName(int idx) const = 0; + virtual QString getWorkspaceName(int idx, QString outputName = QString()) const = 0; virtual int getCurrentWorkspace() const = 0; virtual bool setCurrentWorkspace(int idx) = 0; @@ -121,7 +121,7 @@ class LXQT_PANEL_API ILXQtAbstractWMInterface : public QObject // Workspaces void workspacesCountChanged(); void workspaceNameChanged(int idx); - void currentWorkspaceChanged(int idx); + void currentWorkspaceChanged(int idx, QString outputName = QString()); // TODO: needed? void activeWindowChanged(WId windowId); diff --git a/panel/backends/lxqtdummywmbackend.cpp b/panel/backends/lxqtdummywmbackend.cpp index 14d794155..d8f6ba9e4 100644 --- a/panel/backends/lxqtdummywmbackend.cpp +++ b/panel/backends/lxqtdummywmbackend.cpp @@ -122,7 +122,7 @@ int LXQtDummyWMBackend::getWorkspacesCount() const return 1; // Fake 1 workspace } -QString LXQtDummyWMBackend::getWorkspaceName(int) const +QString LXQtDummyWMBackend::getWorkspaceName(int, QString) const { return QString(); } @@ -199,4 +199,3 @@ bool LXQtDummyWMBackend::showDesktop(bool) { return false; } - diff --git a/panel/backends/lxqtdummywmbackend.h b/panel/backends/lxqtdummywmbackend.h index 57778b72d..64f52228b 100644 --- a/panel/backends/lxqtdummywmbackend.h +++ b/panel/backends/lxqtdummywmbackend.h @@ -69,7 +69,7 @@ class LXQtDummyWMBackend : public ILXQtAbstractWMInterface // Workspaces int getWorkspacesCount() const override; - QString getWorkspaceName(int idx) const override; + QString getWorkspaceName(int idx, QString outputName = QString()) const override; int getCurrentWorkspace() const override; bool setCurrentWorkspace(int idx) override; diff --git a/panel/backends/wayland/CMakeLists.txt b/panel/backends/wayland/CMakeLists.txt index 19340698c..4d64a4892 100644 --- a/panel/backends/wayland/CMakeLists.txt +++ b/panel/backends/wayland/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(kwin_wayland) add_subdirectory(wlroots) +add_subdirectory(wayfire) diff --git a/panel/backends/wayland/kwin_wayland/lxqtwmbackend_kwinwayland.cpp b/panel/backends/wayland/kwin_wayland/lxqtwmbackend_kwinwayland.cpp index 32a18b3cb..337b3b856 100644 --- a/panel/backends/wayland/kwin_wayland/lxqtwmbackend_kwinwayland.cpp +++ b/panel/backends/wayland/kwin_wayland/lxqtwmbackend_kwinwayland.cpp @@ -377,7 +377,7 @@ int LXQtWMBackend_KWinWayland::getWorkspacesCount() const return m_workspaceInfo->numberOfDesktops(); } -QString LXQtWMBackend_KWinWayland::getWorkspaceName(int idx) const +QString LXQtWMBackend_KWinWayland::getWorkspaceName(int idx, QString) const { return m_workspaceInfo->getDesktopName(idx - 1); //Return to 0-based } diff --git a/panel/backends/wayland/kwin_wayland/lxqtwmbackend_kwinwayland.h b/panel/backends/wayland/kwin_wayland/lxqtwmbackend_kwinwayland.h index d44ae6ed6..e6804a967 100644 --- a/panel/backends/wayland/kwin_wayland/lxqtwmbackend_kwinwayland.h +++ b/panel/backends/wayland/kwin_wayland/lxqtwmbackend_kwinwayland.h @@ -73,7 +73,7 @@ class LXQtWMBackend_KWinWayland : public ILXQtAbstractWMInterface // Workspaces virtual int getWorkspacesCount() const override; - virtual QString getWorkspaceName(int idx) const override; + virtual QString getWorkspaceName(int idx, QString sceenName = QString()) const override; virtual int getCurrentWorkspace() const override; virtual bool setCurrentWorkspace(int idx) override; diff --git a/panel/backends/wayland/wayfire/CMakeLists.txt b/panel/backends/wayland/wayfire/CMakeLists.txt new file mode 100644 index 000000000..f52690373 --- /dev/null +++ b/panel/backends/wayland/wayfire/CMakeLists.txt @@ -0,0 +1,35 @@ +set(PLATFORM_NAME wayfire) + +set(PREFIX_NAME wmbackend) +set(PROGRAM "lxqt-panel") +set(BACKEND "backend") +set(NAME ${PREFIX_NAME}_${PLATFORM_NAME}) +project(${PROGRAM}_${BACKEND}_${NAME}) + +find_package(Qt6 ${REQUIRED_QT_VERSION} REQUIRED COMPONENTS WaylandClient Concurrent) +find_package(Qt6Xdg) + +set(PROG_SHARE_DIR ${CMAKE_INSTALL_FULL_DATAROOTDIR}/lxqt/${PROGRAM}/${BACKEND}) +set(PLUGIN_SHARE_DIR ${PROG_SHARE_DIR}/${BACKEND}/${NAME}) +#************************************************ + +if (NOT DEFINED PLUGIN_DIR) + set (PLUGIN_DIR ${CMAKE_INSTALL_FULL_LIBDIR}/${PROGRAM}) +endif (NOT DEFINED PLUGIN_DIR) + +set(QTX_LIBRARIES Qt6::Gui Qt6::GuiPrivate) + +set( + SRC + wayfire-common.cpp wayfire-common.h + lxqtwmbackend_wf.cpp lxqtwmbackend_wf.h +) + +add_library(${NAME} MODULE ${SRC}) # build dynamically loadable modules +install(TARGETS ${NAME} DESTINATION ${PLUGIN_DIR}/${BACKEND}) # install the *.so file + +target_link_libraries(${NAME} ${QTX_LIBRARIES} Qt6::Concurrent Qt6::WaylandClient Qt6Xdg) + +# qt6_generate_wayland_protocol_client_sources(${NAME} FILES +# ${CMAKE_CURRENT_SOURCE_DIR}/wlr-foreign-toplevel-management-unstable-v1.xml +# ) diff --git a/panel/backends/wayland/wayfire/lxqtwmbackend_wf.cpp b/panel/backends/wayland/wayfire/lxqtwmbackend_wf.cpp new file mode 100644 index 000000000..36a5a1313 --- /dev/null +++ b/panel/backends/wayland/wayfire/lxqtwmbackend_wf.cpp @@ -0,0 +1,1078 @@ +#include "wayfire-common.h" +#include "lxqtwmbackend_wf.h" + +#include +#include +#include +#include + +// Shortforms +#define QSL QStringLiteral +#define U8Str QString::fromUtf8 + +static inline QJsonObject updateJsonObject(QJsonObject source, QJsonObject other) +{ + /** Overwrite all (key,value) pairs of @osurce from @other */ + for ( QString key : other.keys()) + { + source[key] = other[key]; + } + + return source; +} + +static inline bool isValidToplevel(QJsonObject view) +{ + if (view.isEmpty()) + { + return false; + } + + /** Ghost view: these are unmapped xwayland views */ + if (view[QSL("pid")].toInt() <= 1) + { + return false; + } + + /** We want only the "toplevel" views */ + if (view[QSL("role")].toString() != QSL("toplevel")) + { + return false; + } + + /** We want only the mapped views */ + if (view[QSL("mapped")].toBool() == false) + { + return false; + } + + return true; +} + +static inline QString getPixmapIcon(QString name) +{ + QStringList paths{ + QSL("/usr/local/share/pixmaps/"), + QSL("/usr/share/pixmaps/"), + }; + + QStringList sfxs{ + QSL(".svg"), QSL(".png"), QSL(".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 == QSL("Unknown"))) + { + return QIcon(); + } + + /** Wine apps */ + if (mAppId.endsWith(QSL(".exe"))) + { + return QIcon::fromTheme(QSL("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(QSL(".local/share/applications/")), + QSL("/usr/local/share/applications/"), + QSL("/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 + QSL(".desktop"))) + { + QSettings desktop(path + mAppId + QSL(".desktop"), QSettings::IniFormat); + iconName = desktop.value(QSL("Desktop Entry/Icon")).toString(); + } + /** Get the icon name from desktop (mAppId: all lower-case letters) */ + else if (QFile::exists(path + mAppId.toLower() + QSL(".desktop"))) + { + QSettings desktop(path + mAppId.toLower() + QSL(".desktop"), QSettings::IniFormat); + iconName = desktop.value(QSL("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({QSL("*.desktop")}); + for (QString dskf : desktops) + { + QSettings desktop(path + dskf, QSettings::IniFormat); + + QString exec = desktop.value(QSL("Desktop Entry/Exec"), QSL("abcd1234/-")).toString(); + QString name = desktop.value(QSL("Desktop Entry/Name"), QSL("abcd1234/-")).toString(); + QString cls = desktop.value(QSL("Desktop Entry/StartupWMClass"), + QSL("abcd1234/-")).toString(); + + QString execPath = U8Str(std::filesystem::path(exec.toStdString()).filename().c_str()); + + if (mAppId.compare(execPath, Qt::CaseInsensitive) == 0) + { + iconName = desktop.value(QSL("Desktop Entry/Icon")).toString(); + } else if (mAppId.compare(name, Qt::CaseInsensitive) == 0) + { + iconName = desktop.value(QSL("Desktop Entry/Icon")).toString(); + } else if (mAppId.compare(cls, Qt::CaseInsensitive) == 0) + { + iconName = desktop.value(QSL("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(); +} + +LXQtTaskbarWayfireBackend::LXQtTaskbarWayfireBackend(QObject *parent) : + ILXQtAbstractWMInterface(parent) +{ + // emit workspaceCountChanged() + connect(&mWayfire, &LXQt::Panel::Wayfire::workspaceSetChanged, [this] ( QJsonDocument ) + { + // no-op + }); + + // emit currentWorkspaceChanged(idx) + connect(&mWayfire, &LXQt::Panel::Wayfire::workspaceChanged, [this] ( QJsonDocument respJson ) + { + QJsonObject response = respJson.object(); + + QJsonObject wsInfo = response[QSL("new-workspace")].toObject(); + + QJsonObject output = response[QSL("output-data")].toObject(); + QString outputName = output[QSL("name")].toString(); + QJsonObject workspace = output[QSL("workspace")].toObject(); + + int64_t nRows = workspace[QSL("grid_height")].toInt(); + + int64_t row = wsInfo[QSL("y")].toInt(); + int64_t column = wsInfo[QSL("x")].toInt(); + + qDebug() << "[Slot] Workspace changed"; + + emit currentWorkspaceChanged(row * nRows + column + 1, outputName); + }); + + // emit windowAdded(WId) + connect(&mWayfire, &LXQt::Panel::Wayfire::viewMapped, [this] ( QJsonDocument respJson ) + { + QJsonObject response = respJson.object(); + + QJsonObject view = response[QSL("view")].toObject(); + + /** Filter non-toplevel views */ + if (!isValidToplevel(view)) + { + return; + } + + WaylandId viewId(view[QSL("id")].toInt()); + + if (!mViews.contains(viewId)) + { + emit windowRemoved(viewId); + } + + mViews[viewId] = view; + + qDebug() << "[Slot] View mapped" << viewId; + + emit windowAdded(view[QSL("id")].toInt()); + }); + + connect(&mWayfire, &LXQt::Panel::Wayfire::viewTitleChanged, [this] ( QJsonDocument respJson ) + { + QJsonObject response = respJson.object(); + QJsonObject view = response[QSL("view")].toObject(); + + /** Filter non-toplevel views */ + if (!isValidToplevel(view)) + { + return; + } + + WaylandId viewId(view[QSL("id")].toInt()); + + if (!mViews.contains(viewId)) + { + mViews[viewId] = view; + emit windowAdded(viewId); + } + + mViews[viewId] = updateJsonObject(mViews[viewId], view); + + qDebug() << "[Slot] View title changed" << viewId << mViews[viewId][QSL("title")].toString(); + + emit windowPropertyChanged(viewId, (int)LXQtTaskBarWindowProperty::Title); + }); + + connect(&mWayfire, &LXQt::Panel::Wayfire::viewAppIdChanged, [this] ( QJsonDocument respJson ) + { + QJsonObject response = respJson.object(); + QJsonObject view = response[QSL("view")].toObject(); + + /** Filter non-toplevel views */ + if (!isValidToplevel(view)) + { + return; + } + + WaylandId viewId(view[QSL("id")].toInt()); + + if (!mViews.contains(viewId)) + { + mViews[viewId] = view; + emit windowAdded(viewId); + } + + mViews[viewId] = updateJsonObject(mViews[viewId], view); + + qDebug() << "[Slot] View app-id changed" << viewId << mViews[viewId][QSL("app-id")].toString(); + + emit windowPropertyChanged(viewId, (int)LXQtTaskBarWindowProperty::WindowClass); + emit windowPropertyChanged(viewId, (int)LXQtTaskBarWindowProperty::Icon); + }); + + // emit windowPropertyChanged(WId, State) for all windows + connect(&mWayfire, &LXQt::Panel::Wayfire::viewFocused, [this] ( QJsonDocument respJson ) + { + for ( WaylandId viewId : mViews.keys()) + { + qDebug() << "[Slot] Window property changed" << viewId; + emit windowPropertyChanged(viewId, (int)LXQtTaskBarWindowProperty::State); + } + + QJsonObject response = respJson.object(); + QJsonObject view = response[QSL("view")].toObject(); + + /** Filter non-toplevel views */ + if (!isValidToplevel(view)) + { + return; + } + + WaylandId viewId(view[QSL("id")].toInt()); + + if (!mViews.contains(viewId)) + { + mViews[viewId] = view; + emit windowAdded(viewId); + } + + mViews[viewId] = updateJsonObject(mViews[viewId], view); + + qDebug() << "[Slot] View focused" << viewId; + + emit activeWindowChanged(viewId); + }); + + // emit windowPropertyChanged(WId, State) + connect(&mWayfire, &LXQt::Panel::Wayfire::viewMinimized, [this] ( QJsonDocument respJson ) + { + QJsonObject response = respJson.object(); + QJsonObject view = response[QSL("view")].toObject(); + + /** Filter non-toplevel views */ + if (!isValidToplevel(view)) + { + return; + } + + WaylandId viewId(view[QSL("id")].toInt()); + + if (!mViews.contains(viewId)) + { + mViews[viewId] = view; + emit windowAdded(viewId); + } + + mViews[viewId] = updateJsonObject(mViews[viewId], view); + + qDebug() << "[Slot] View minimized" << viewId; + + emit windowPropertyChanged(viewId, (int)LXQtTaskBarWindowProperty::State); + }); + + // emit windowPropertyChanged(WId, State) + connect(&mWayfire, &LXQt::Panel::Wayfire::viewTiled, [this] ( QJsonDocument respJson ) + { + QJsonObject response = respJson.object(); + QJsonObject view = response[QSL("view")].toObject(); + + /** Filter non-toplevel views */ + if (!isValidToplevel(view)) + { + return; + } + + WaylandId viewId(view[QSL("id")].toInt()); + + if (!mViews.contains(viewId)) + { + mViews[viewId] = view; + emit windowAdded(viewId); + } + + mViews[viewId] = updateJsonObject(mViews[viewId], view); + + qDebug() << "[Slot] View tiled" << viewId; + + emit windowPropertyChanged(viewId, (int)LXQtTaskBarWindowProperty::State); + }); + + // emit windowPropertyChanged(WId, Geometry) + connect(&mWayfire, &LXQt::Panel::Wayfire::viewGeometryChanged, [this] ( QJsonDocument respJson ) + { + QJsonObject response = respJson.object(); + QJsonObject view = response[QSL("view")].toObject(); + + /** Filter non-toplevel views */ + if (!isValidToplevel(view)) + { + return; + } + + WaylandId viewId(view[QSL("id")].toInt()); + + if (!mViews.contains(viewId)) + { + mViews[viewId] = view; + emit windowAdded(viewId); + } + + mViews[viewId] = updateJsonObject(mViews[viewId], view); + + qDebug() << "[Slot] View geometry changed" << viewId; + + emit windowPropertyChanged(viewId, (int)LXQtTaskBarWindowProperty::Geometry); + }); + + connect(&mWayfire, &LXQt::Panel::Wayfire::viewOutputChanged, [this] ( QJsonDocument respJson ) + { + QJsonObject response = respJson.object(); + QJsonObject view = response[QSL("view")].toObject(); + + /** Filter non-toplevel views */ + if (!isValidToplevel(view)) + { + return; + } + + WaylandId viewId(view[QSL("id")].toInt()); + QString oldOp; + + if (!mViews.contains(viewId)) + { + mViews[viewId] = view; + emit windowAdded(viewId); + } else { + oldOp = mViews[viewId][QSL("output-name")].toString(); + } + + QString newOp = view[QSL("output-name")].toString(); + + mViews[viewId] = updateJsonObject(mViews[viewId], view); + + qDebug() << "[Slot] View output changed" << viewId << oldOp << newOp; + + emit windowPropertyChanged(viewId, (int)LXQtTaskBarWindowProperty::Geometry); + }); + + connect(&mWayfire, &LXQt::Panel::Wayfire::viewWorkspaceChanged, [this] ( QJsonDocument respJson ) + { + QJsonObject response = respJson.object(); + QJsonObject view = response[QSL("view")].toObject(); + + /** Filter non-toplevel views */ + if (!isValidToplevel(view)) + { + return; + } + + WaylandId viewId(view[QSL("id")].toInt()); + + if (!mViews.contains(viewId)) + { + mViews[viewId] = view; + emit windowAdded(viewId); + } + + mViews[viewId] = updateJsonObject(mViews[viewId], view); + mViews[viewId][QSL("workspace")] = QJsonObject({ + {QSL("x"), response[QSL("to")][QSL("x")]}, + {QSL("y"), response[QSL("to")][QSL("y")]} + }); + + qDebug() << "[Slot] View workspace changed" << viewId; + + emit windowPropertyChanged(viewId, (int)LXQtTaskBarWindowProperty::Workspace); + }); + + connect(&mWayfire, &LXQt::Panel::Wayfire::viewUnmapped, [this] ( QJsonDocument respJson ) + { + QJsonObject response = respJson.object(); + QJsonObject view = response[QSL("view")].toObject(); + + WaylandId viewId(view[QSL("id")].toInt()); + + if ( mViews.contains(viewId)) + { + mViews.remove(viewId); + qDebug() << "[Slot] View unmapped" << viewId; + emit windowRemoved(viewId); + } + }); + + mWayfire.connectToServer(); +} + +bool LXQtTaskbarWayfireBackend::supportsAction(WId, LXQtTaskBarBackendAction action) const +{ + switch (action) + { + case LXQtTaskBarBackendAction::Move: + return false; + + case LXQtTaskBarBackendAction::Resize: + return false; + + case LXQtTaskBarBackendAction::Maximize: + return true; + + /** To be implemented in wayfire ipc */ + case LXQtTaskBarBackendAction::MaximizeVertically: + return false; + + /** To be implemented in wayfire ipc */ + case LXQtTaskBarBackendAction::MaximizeHorizontally: + return false; + + case LXQtTaskBarBackendAction::Minimize: + return true; + + /** Not implemented */ + case LXQtTaskBarBackendAction::RollUp: + return false; + + case LXQtTaskBarBackendAction::FullScreen: + return true; + + case LXQtTaskBarBackendAction::DesktopSwitch: + return true; + + /** Available via wsets plugin */ + case LXQtTaskBarBackendAction::MoveToDesktop: + return true; + + /** Pin Above and Normal are available */ + case LXQtTaskBarBackendAction::MoveToLayer: + return true; + + /** Available via wsets plugin */ + case LXQtTaskBarBackendAction::MoveToOutput: + return true; + + default: + return false; + } + + return false; +} + +bool LXQtTaskbarWayfireBackend::reloadWindows() +{ + // Force removal and re-adding + for (WaylandId viewId : mViews.keys()) + { + mViews.remove(viewId); + emit windowRemoved(viewId); + } + + QJsonArray views = mWayfire.listViews(); + while (views.count()) + { + QJsonObject view = views.takeAt(0).toObject(); + WaylandId id(view[QSL("id")].toInt()); + + qDebug() << id; + + mViews[id] = view; + emit windowAdded(id); + } + + qDebug() << "====================="; + + return true; +} + +QVector LXQtTaskbarWayfireBackend::getCurrentWindows() const +{ + QVector ids; + for ( WaylandId viewId : mViews.keys()) + { + ids << viewId; + } + + qDebug() << "Current windows" << ids; + + return ids; +} + +QString LXQtTaskbarWayfireBackend::getWindowTitle(WId windowId) const +{ + WaylandId viewId(windowId); + if (!mViews.contains(viewId)) + { + return QString(); + } + + return mViews[viewId][QSL("title")].toString(); +} + +bool LXQtTaskbarWayfireBackend::applicationDemandsAttention(WId) const +{ + return false; +} + +QIcon LXQtTaskbarWayfireBackend::getApplicationIcon(WId windowId, int devicePixels) const +{ + Q_UNUSED(devicePixels) + + WaylandId viewId(windowId); + if (!mViews.contains(viewId)) + { + return QIcon(); + } + + return getIconForAppId(mViews[viewId][QSL("app-id")].toString()); +} + +QString LXQtTaskbarWayfireBackend::getWindowClass(WId windowId) const +{ + WaylandId viewId(windowId); + if (!mViews.contains(viewId)) + { + return QString(); + } + + return mViews[viewId][QSL("app-id")].toString(); +} + +LXQtTaskBarWindowLayer LXQtTaskbarWayfireBackend::getWindowLayer(WId) const +{ + return LXQtTaskBarWindowLayer::Normal; +} + +bool LXQtTaskbarWayfireBackend::setWindowLayer(WId, LXQtTaskBarWindowLayer) +{ + return false; +} + +LXQtTaskBarWindowState LXQtTaskbarWayfireBackend::getWindowState(WId windowId) const +{ + WaylandId viewId(windowId); + if (!mViews.contains(viewId)) + { + return LXQtTaskBarWindowState::Hidden; + } + + if (!mViews[viewId][QSL("mapped")].toBool()) + { + return LXQtTaskBarWindowState::Hidden; + } + + if (mViews[viewId][QSL("minimized")].toBool()) + { + return LXQtTaskBarWindowState::Minimized; + } + + if (mViews[viewId][QSL("fullscreen")].toBool()) + { + return LXQtTaskBarWindowState::FullScreen; + } + + // WLR_EDGE_TOP | WLR_EDGE_BOTTOM | WLR_EDGE_LEFT | WLR_EDGE_RIGHT == 1 | 2 | 4 | 8 == 15 + if (mViews[viewId][QSL("tiled-edges")].toInt() > 0) + { + return LXQtTaskBarWindowState::Maximized; + } + + // // WLR_EDGE_TOP | WLR_EDGE_BOTTOM == 1 | 2 == 3 + // if (mViews[viewId][QSL("tiled-edges")].toInt() == 3) + // { + // return LXQtTaskBarWindowState::MaximizedVertically; + // } + + // // WLR_EDGE_LEFT | WLR_EDGE_RIGHT == 4 | 8 == 12 + // if (mViews[viewId][QSL("tiled-edges")].toInt() == 12) + // { + // return LXQtTaskBarWindowState::MaximizedHorizontally; + // } + + return LXQtTaskBarWindowState::Normal; +} + +bool LXQtTaskbarWayfireBackend::setWindowState(WId windowId, LXQtTaskBarWindowState state, bool set) +{ + WaylandId viewId(windowId); + if (!mViews.contains(viewId)) + { + return false; + } + + switch (state) + { + case LXQtTaskBarWindowState::Minimized: + { + mWayfire.minimizeView(viewId, set); + break; + } + + case LXQtTaskBarWindowState::Maximized: + { + mWayfire.maximizeView(viewId, (set ? 15 : 0)); + break; + } + + case LXQtTaskBarWindowState::MaximizedVertically: + { + mWayfire.maximizeView(viewId, (set ? 3 : 0)); + break; + } + + case LXQtTaskBarWindowState::MaximizedHorizontally: + { + mWayfire.maximizeView(viewId, (set ? 12 : 0)); + break; + } + + case LXQtTaskBarWindowState::Normal: + { + mWayfire.restoreView(viewId); + break; + } + + case LXQtTaskBarWindowState::FullScreen: + { + mWayfire.fullscreenView(viewId, set); + break; + } + + default: + return false; + } + + return true; +} + +bool LXQtTaskbarWayfireBackend::isWindowActive(WId windowId) const +{ + WaylandId viewId(windowId); + return (mWayfire.getActiveView() == viewId); +} + +bool LXQtTaskbarWayfireBackend::raiseWindow(WId windowId, bool onCurrentWorkSpace) +{ + Q_UNUSED(onCurrentWorkSpace) // Cannot be done on a generic wlroots-based compositor! + + WaylandId viewId(windowId); + if (!mViews.contains(viewId)) + { + return false; + } + + if (getWindowState(windowId)==LXQtTaskBarWindowState::Minimized) + { + qDebug() << "Unminimizing view first" << windowId; + mWayfire.minimizeView(WaylandId(windowId), false); + } + + qDebug() << "Focusing view" << windowId; + return mWayfire.focusView(viewId); +} + +bool LXQtTaskbarWayfireBackend::closeWindow(WId windowId) +{ + WaylandId viewId(windowId); + if (!mViews.contains(viewId)) + { + return false; + } + + return mWayfire.closeView(viewId); +} + +WId LXQtTaskbarWayfireBackend::getActiveWindow() const +{ + return mWayfire.getActiveView(); +} + +int LXQtTaskbarWayfireBackend::getWorkspacesCount() const +{ + QJsonObject wsetsInfo = mWayfire.getWorkspaceSetsInfo().at(0).toObject(); + QJsonObject workspace = wsetsInfo[QSL("workspace")].toObject(); + int64_t nRows = workspace[QSL("grid_height")].toInt(); + int64_t nCols = workspace[QSL("grid_width")].toInt(); + + return (nRows * nCols); +} + +QString LXQtTaskbarWayfireBackend::getWorkspaceName(int x, QString outputName) const +{ + return mWayfire.getWorkspaceName(x, outputName); +} + +int LXQtTaskbarWayfireBackend::getCurrentWorkspace() const +{ + QJsonObject outputInfo = mWayfire.getOutputInfo(mWayfire.getActiveOutput()); + QJsonObject outputWS = outputInfo[QSL("workspace")].toObject(); + + int nCols = outputWS[QSL("grid_width")].toInt(); // Total columns in workspace grid + int curRow = outputWS[QSL("y")].toInt(); // Current workspace row (0-based) + int curCol = outputWS[QSL("x")].toInt(); // Current workspace column (0-based) + + return curRow * nCols + curCol + 1; +} + +bool LXQtTaskbarWayfireBackend::setCurrentWorkspace(int x) +{ + return mWayfire.switchToWorkspace(mWayfire.getActiveOutput(), x); +} + +int LXQtTaskbarWayfireBackend::getWindowWorkspace(WId windowId) const +{ + WaylandId viewId(windowId); + QJsonObject viewInfo = mWayfire.getViewInfo(viewId); + + QJsonObject outputInfo = mWayfire.getOutputInfo(WaylandId(viewInfo[QSL("output-id")].toInt())); + QJsonObject outputWS = outputInfo[QSL("workspace")].toObject(); + + int nRows = outputWS[QSL("grid_height")].toInt(); // Total rows in workspace grid + int nCols = outputWS[QSL("grid_width")].toInt(); // Total columns in workspace grid + + if (viewInfo.contains(QSL("workspace"))) + { + int currentRow = viewInfo[QSL("workspace")][QSL("y")].toInt(); // Current workspace row (0-based) + int currentCol = viewInfo[QSL("workspace")][QSL("x")].toInt(); // Current workspace column (0-based) + + return currentRow * nCols + currentCol + 1; + } + + QJsonObject viewGeom = viewInfo[QSL("geometry")].toObject(); + + // Calculate the center of the window + QPoint viewCenter( + viewGeom[QSL("x")].toInt() + viewGeom[QSL("width")].toInt() / 2, + viewGeom[QSL("y")].toInt() + viewGeom[QSL("height")].toInt() / 2 + ); + + QJsonObject outputGeom = outputInfo[QSL("geometry")].toObject(); + + QRect opGeom( + outputGeom[QSL("x")].toInt(), + outputGeom[QSL("y")].toInt(), + outputGeom[QSL("width")].toInt(), + outputGeom[QSL("height")].toInt() + ); + + int currentRow = outputWS[QSL("y")].toInt(); // Current workspace row (0-based) + int currentCol = outputWS[QSL("x")].toInt(); // Current workspace column (0-based) + + // Calculate the geometries of all workspaces relative to the current workspace + QHash wsGeomHash; + for (int row = 0; row < nRows; ++row) + { + for (int col = 0; col < nCols; ++col) + { + // Workspace index (0-based) + int wsIndex = row * nCols + col; + + // Workspace geometry (relative to the current workspace) + QRect wsGeom( + opGeom.x() + (col - currentCol) * opGeom.width(), + opGeom.y() + (row - currentRow) * opGeom.height(), + opGeom.width(), + opGeom.height() + ); + + wsGeomHash[wsIndex] = wsGeom; + } + } + + // Find which workspace contains the view's center + for (auto it = wsGeomHash.constBegin(); it != wsGeomHash.constEnd(); ++it) + { + if (it.value().contains(viewCenter)) + { + return it.key() + 1; + } + } + + // Fallback: If not found, assume current workspace + return currentRow * nCols + currentCol + 1; +} + +bool LXQtTaskbarWayfireBackend::setWindowOnWorkspace(WId windowId, int idx) +{ + WaylandId viewId(windowId); + return mWayfire.sendViewToWorkspace(viewId, idx); +} + +void LXQtTaskbarWayfireBackend::moveApplicationToPrevNextMonitor(WId viewId, bool nextOp, bool raiseWindow) +{ + // 1. Get the current output id and its active wset-id + // Get view info to find which output it's currently on + QJsonObject viewInfo = mWayfire.getViewInfo(WaylandId(viewId)); + if (viewInfo.isEmpty()) + { + qWarning() << "Failed to get view info for view" << viewId; + return; + } + + WaylandId currentOutputId(viewInfo[QSL("output-id")].toInt()); + + // Get all outputs + QJsonArray outputs = mWayfire.listOutputs(); + if (outputs.isEmpty()) + { + qWarning() << "No outputs available"; + return; + } + + // 2. Find the previous/next output + int currentIndex = -1; + for (int i = 0; i < outputs.size(); i++) + { + QJsonObject output = outputs[i].toObject(); + if (output[QSL("id")].toInt() == (int)currentOutputId.id) + { + currentIndex = i; + break; + } + } + + if (currentIndex == -1) + { + qWarning() << "Current output not found in outputs list"; + return; + } + + // Calculate target output index with wrap-around + int targetIndex; + if (nextOp) + { + targetIndex = (currentIndex + 1) % outputs.size(); + } else + { + targetIndex = (currentIndex - 1 + outputs.size()) % outputs.size(); + } + + QJsonObject targetOutput = outputs[targetIndex].toObject(); + WaylandId targetOutputId(targetOutput[QSL("id")].toInt()); + + // 3. Move the view to target output's workspace set + // Get workspace sets info + QJsonArray wsets = mWayfire.getWorkspaceSetsInfo(); + if (wsets.isEmpty()) + { + qWarning() << "No workspace sets available"; + return; + } + + // Find the target output's active workspace set + WaylandId targetWsetId(0); + for (const QJsonValue & wsVal : wsets) + { + QJsonObject ws = wsVal.toObject(); + if (ws[QSL("output-id")].toInt() == (int)targetOutputId.id) + { + targetWsetId = WaylandId(ws[QSL("index")].toInt()); + break; + } + } + + if (targetWsetId == 0) + { + qWarning() << "Failed to find workspace set for target output"; + return; + } + + // Move the view to target workspace set + QJsonObject moveRequest; + moveRequest[QSL("method")] = QSL("wsets/send-view-to-wset"); + moveRequest[QSL("data")] = QJsonObject{ + {QSL("view-id"), QJsonValue::fromVariant((quint64)viewId)}, + {QSL("wset-index"), QJsonValue::fromVariant((quint64)targetWsetId.id)} + }; + + QJsonDocument reply = mWayfire.genericRequest(QJsonDocument(moveRequest)); + if (reply[QSL("result")].toString() != QSL("ok")) + { + qWarning() << "Failed to move view to target workspace set:" << reply.toJson(); + return; + } + + // 4. Focus the window if requested + if (raiseWindow) + { + mWayfire.focusView(WaylandId(viewId)); + } +} + +bool LXQtTaskbarWayfireBackend::isWindowOnScreen(QScreen *scrn, WId windowId) const +{ + WaylandId viewId(windowId); + if (!mViews.contains(viewId)) + { + return false; + } + + return mViews[viewId][QSL("output-name")] == scrn->name(); +} + +bool LXQtTaskbarWayfireBackend::setDesktopLayout(Qt::Orientation, int, int, bool) +{ + // Wayfire does not support dynamic setting of desktops. + return false; +} + +void LXQtTaskbarWayfireBackend::moveApplication(WId) +{ + // no-op +} + +void LXQtTaskbarWayfireBackend::resizeApplication(WId) +{ + // no-op +} + +void LXQtTaskbarWayfireBackend::refreshIconGeometry(WId, const QRect &) +{ + // no-op +} + +bool LXQtTaskbarWayfireBackend::isAreaOverlapped(const QRect &) const +{ + return false; +} + +bool LXQtTaskbarWayfireBackend::isShowingDesktop() const +{ + return mIsDesktopShowing; +} + +bool LXQtTaskbarWayfireBackend::showDesktop(bool yes) +{ + if (mIsDesktopShowing == yes) + { + return true; + } + + mIsDesktopShowing = yes; + + return mWayfire.showDesktop(mWayfire.getActiveOutput()); +} + +int LXQtWMBackendWayfireLibrary::getBackendScore(const QString& key) const +{ + // Only wayfire is supported + if ((key == QSL("wayfire")) || (key == QSL("Wayfire"))) + { + return 100; + } + + // Unsupported + return 0; +} + +ILXQtAbstractWMInterface*LXQtWMBackendWayfireLibrary::instance() const +{ + return new LXQtTaskbarWayfireBackend(nullptr); +} diff --git a/panel/backends/wayland/wayfire/lxqtwmbackend_wf.h b/panel/backends/wayland/wayfire/lxqtwmbackend_wf.h new file mode 100644 index 000000000..a557ee994 --- /dev/null +++ b/panel/backends/wayland/wayfire/lxqtwmbackend_wf.h @@ -0,0 +1,124 @@ +#pragma once + +#include "../../ilxqtabstractwmiface.h" +#include "wayfire-common.h" + +#include +#include +#include + +class LXQtTaskbarWayfireWindow; +class LXQtTaskbarWayfireWindowManagment; +class LXQtWayfireWaylandWorkspaceInfo; + + +class LXQtTaskbarWayfireBackend : public ILXQtAbstractWMInterface +{ + Q_OBJECT + + public: + explicit LXQtTaskbarWayfireBackend(QObject *parent = nullptr); + + // Backend + virtual bool supportsAction(WId windowId, LXQtTaskBarBackendAction action) const override; + + // Windows + virtual bool reloadWindows() override; + + // Get the current windows + virtual QVector getCurrentWindows() const override; + + // Get the window title + virtual QString getWindowTitle(WId windowId) const override; + + // We do not support this + virtual bool applicationDemandsAttention(WId windowId) const override; + + // Support for this is based on app-id (handled by LXQt) + virtual QIcon getApplicationIcon(WId windowId, int devicePixels) const override; + + // Same as app-id + virtual QString getWindowClass(WId windowId) const override; + + // Allways-on-Bottom, Normal or Always-on-top. + // Always-on-bottom is not available on wayfire + virtual LXQtTaskBarWindowLayer getWindowLayer(WId windowId) const override; + virtual bool setWindowLayer(WId windowId, LXQtTaskBarWindowLayer layer) override; + + // Hidden, FullScreen, Minimized, Maximized, MaximizedVertical, MaximizedHorizontally, Normal, RolledUp + virtual LXQtTaskBarWindowState getWindowState(WId windowId) const override; + virtual bool setWindowState(WId windowId, LXQtTaskBarWindowState state, bool set) override; + + // Is window active + virtual bool isWindowActive(WId windowId) const override; + + // Set window as active + virtual bool raiseWindow(WId windowId, bool onCurrentWorkSpace) override; + + // Close window + virtual bool closeWindow(WId windowId) override; + + // Get active window + virtual WId getActiveWindow() const override; + + // Workspaces + virtual int getWorkspacesCount() const override; + virtual QString getWorkspaceName(int idx, QString outputName = QString()) const override; + + // Get/Set the current workspace + virtual int getCurrentWorkspace() const override; + virtual bool setCurrentWorkspace(int idx) override; + + // Get/Set the workspace of a window + virtual int getWindowWorkspace(WId windowId) const override; + virtual bool setWindowOnWorkspace(WId windowId, int idx) override; + + // Move window to previous/next desktop + virtual void moveApplicationToPrevNextMonitor(WId windowId, bool next, + bool raiseOnCurrentDesktop) override; + + virtual bool isWindowOnScreen(QScreen *screen, WId windowId) const override; + + // Not supported on wayfire at the moment + virtual bool setDesktopLayout(Qt::Orientation orientation, int rows, int columns, bool rightToLeft); + + // 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 - not supported + virtual bool isAreaOverlapped(const QRect& area) const override; + + // Show Desktop + virtual bool isShowingDesktop() const override; + virtual bool showDesktop(bool value) override; + + private: + // std::unique_ptr mManagment; + LXQt::Panel::Wayfire mWayfire; + + // Hash-map of view ids, vs their properties + QHash mViews; + + // key=transient child, value=leader + QHash transients; + + // Is Desktop Shown + bool mIsDesktopShowing = false; +}; + + +class LXQtWMBackendWayfireLibrary : public QObject, public ILXQtWMBackendLibrary +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "lxqt.org/Panel/WMInterface/1.0") + Q_INTERFACES(ILXQtWMBackendLibrary) + + public: + int getBackendScore(const QString& key) const override; + + ILXQtAbstractWMInterface * instance() const override; +}; diff --git a/panel/backends/wayland/wayfire/wayfire-common.cpp b/panel/backends/wayland/wayfire/wayfire-common.cpp new file mode 100644 index 000000000..8811bdbc6 --- /dev/null +++ b/panel/backends/wayland/wayfire/wayfire-common.cpp @@ -0,0 +1,784 @@ +/* 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 "wayfire-common.h" + +// Socket related +#include +#include + +// Other headers +#include +#include + + +#define QSL QStringLiteral + +LXQt::Panel::WayfireImpl::WayfireImpl() : QRunnable() +{ + wfSock.fd = -1; +} + +void LXQt::Panel::WayfireImpl::stop() +{ + mTerminate = true; + + if (wfSock.fd != -1) + { + close(wfSock.fd); + wfSock.fd = -1; + } + + qApp->processEvents(); +} + +uint32_t LXQt::Panel::WayfireImpl::request(QJsonDocument req) +{ + QMutexLocker locker(&mutex); + + if (!mConnected) + { + return 0; + } + + writeJson(req); + + // Generate a unique request ID + static std::atomic requestCounter(0); + uint32_t reqId = ++requestCounter; + + if (requestCounter == UINT32_MAX) + { + requestCounter = 1; + } + + mPendingRequests << reqId; + return reqId; +} + +void LXQt::Panel::WayfireImpl::run() +{ + emit started(); + + while (true) + { + int nready = poll(&wfSock, 1, 10); + + /** Something went wrong while polling */ + if (nready < 0) + { + qWarning() << "[Error]:" << strerror(errno); + } + + /** Bye bye..! */ + if (mTerminate) + { + return; + } + + /** Nothing to read */ + if (nready == 0) + { + continue; + } + + /** We have something to read. Let's see what it is. */ + if (wfSock.revents & (POLLRDNORM | POLLERR)) + { + QJsonDocument resp = readJson(); + + /** This is an event */ + if (resp.isObject() && resp.object().contains(QSL("event"))) + { + emit wayfireEvent(resp); + } + /** This is the response to a request */ + else + { + // Lock the mutex + QMutexLocker locker(&mutex); + + uint32_t reqId = mPendingRequests.takeFirst(); + + // Emit signal asynchronously + QMetaObject::invokeMethod(this, [this, reqId, resp] () + { + emit response(reqId, resp); + }, Qt::QueuedConnection); + + emit response(reqId, resp); + } + } + } +} + +bool LXQt::Panel::WayfireImpl::writeJson(QJsonDocument j) +{ + QByteArray str = j.toJson(QJsonDocument::Compact); + uint32_t size = str.size(); + + // Write the size of the JSON data + ssize_t ret = write(wfSock.fd, &size, sizeof(size)); + + if (ret == -1) + { + // Handle write error + qWarning() << "Failed to write size to socket:" << strerror(errno); + // throw std::runtime_error("Failed to write size to socket"); + } else if (ret != sizeof(size)) + { + // Handle partial write + qWarning() << "Partial write of size to socket:" << ret << "bytes written, expected" << sizeof(size); + // throw std::runtime_error("Partial write of size to socket"); + } + + // Write the JSON data + const char *data = str.constData(); + ssize_t bytesWritten = 0; + + while (bytesWritten < str.size()) + { + ret = write(wfSock.fd, data + bytesWritten, str.size() - bytesWritten); + + if (ret == -1) + { + // Handle write error + qWarning() << "Failed to write JSON data to socket:" << strerror(errno); + return false; + } else if (ret == 0) + { + // Handle socket closed by peer + qWarning() << "Socket closed by peer while writing JSON data"; + return false; + } + + bytesWritten += ret; + } + + /** We succeeded in writing the JSON data successfully */ + return true; +} + +QJsonDocument LXQt::Panel::WayfireImpl::readJson() +{ + uint32_t msgSize; + + if (!readExact(reinterpret_cast(&msgSize), sizeof(msgSize))) + { + return QJsonDocument(); + } + + QByteArray buffer(msgSize, Qt::Uninitialized); + if (!readExact(buffer.data(), msgSize)) + { + return QJsonDocument(); + } + + return QJsonDocument::fromJson(buffer); +} + +bool LXQt::Panel::WayfireImpl::readExact(char *buf, uint size) +{ + while (size > 0) + { + int ret = read(wfSock.fd, buf, size); + + if (ret == -1) + { + qCritical() << "Failed to read from socket: " << strerror(errno); + return false; + } + + buf += ret; + size -= ret; + } + + return true; +} + +// Helper function to create a QJsonDocument from an initializer list +QJsonDocument createJsonObject(std::initializer_list> initList) +{ + QJsonObject obj; + + for (const auto& pair : initList) + { + obj.insert(pair.first, pair.second); + } + + return QJsonDocument(obj); +} + +LXQt::Panel::Wayfire::Wayfire(QString wfSock) : QObject() +{ + impl.reset(new WayfireImpl()); + impl->wfSockPath = (wfSock.isEmpty() ? qEnvironmentVariable("WAYFIRE_SOCKET") : wfSock); + + /** Always emit this for any wayfire event */ + connect(impl.data(), &LXQt::Panel::WayfireImpl::wayfireEvent, this, &LXQt::Panel::Wayfire::genericEvent); + + /** Parse the events and emit the correct signal */ + connect(impl.data(), &LXQt::Panel::WayfireImpl::wayfireEvent, this, &LXQt::Panel::Wayfire::parseEvents); +} + +LXQt::Panel::Wayfire::~Wayfire() +{ + impl->stop(); + + if (impl->wfSock.fd != -1) + { + close(impl->wfSock.fd); + impl->wfSock.fd = -1; + } + + impl.reset(nullptr); +} + +bool LXQt::Panel::Wayfire::connectToServer() const +{ + impl->wfSock.fd = socket(AF_UNIX, SOCK_STREAM, 0); + impl->wfSock.events = POLLRDNORM; + + if (impl->wfSock.fd == -1) + { + qCritical() << "Failed to create socket: " << strerror(errno); + return false; + } + + struct sockaddr_un addr; + addr.sun_family = AF_UNIX, + + strncpy(addr.sun_path, impl->wfSockPath.toUtf8().data(), sizeof(addr.sun_path) - 1); + addr.sun_path[sizeof(addr.sun_path) - 1] = '\0'; + + if (::connect(impl->wfSock.fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) + { + qCritical() << "Failed to connect to socket: " << strerror(errno); + qCritical() << "Ensure that ipc and ipc-rules plugins are enabled."; + qCritical() << + "If these plugins were enabled after starting this session, please restart the session."; + close(impl->wfSock.fd); + return false; + } + + impl->mConnected = true; + + /** Run the impl in a separate thread */ + QThreadPool::globalInstance()->start(impl.data()); + + /** Make a request and forget about it. */ + QJsonObject request; + request[QSL("method")] = QSL("window-rules/events/watch"); + genericRequest(QJsonDocument(request)); + + return true; +} + +QJsonArray LXQt::Panel::Wayfire::listOutputs() const +{ + QJsonObject request; + + request[QSL("method")] = QSL("window-rules/list-outputs"); + + QJsonDocument response = genericRequest(QJsonDocument(request)); + + if (response.isArray()) + { + return response.array(); + } + + return QJsonArray(); +} + +QJsonObject LXQt::Panel::Wayfire::getOutputInfo(WaylandId opId) const +{ + QJsonObject request; + + request[QSL("method")] = QSL("window-rules/output-info"); + request[QSL("data")] = QJsonObject({ + {QSL("id"), QJsonValue::fromVariant((quint64)opId)}, + }); + + return genericRequest(QJsonDocument(request)).object(); +} + +WaylandId LXQt::Panel::Wayfire::getActiveOutput() const +{ + QJsonObject request; + + request[QSL("method")] = QSL("window-rules/get-focused-output"); + + QJsonObject reply = genericRequest(QJsonDocument(request)).object(); + + if (reply[QSL("result")].toString() == QSL("ok")) + { + QJsonObject opInfo = reply[QSL("info")].toObject(); + uint32_t opId = opInfo[QSL("id")].toInt(); + + return WaylandId(opId); + } + + return WaylandId(); +} + +bool LXQt::Panel::Wayfire::focusOutput(WaylandId opId) const +{ + QJsonObject request; + request[QSL("method")] = QSL("oswitch/switch-output"); + request[QSL("data")] = QJsonObject({ + {QSL("output-id"), QJsonValue::fromVariant((quint64)opId)}, + }); + + QJsonDocument reply = genericRequest(QJsonDocument(request)); + + if (reply[QSL("result")].toString() == QSL("ok")) + { + return true; + } + + return false; +} + +bool LXQt::Panel::Wayfire::showDesktop(WaylandId opId) const +{ + QJsonObject request; + + request[QSL("method")] = QSL("wm-actions/toggle_showdesktop"); + request[QSL("data")] = QJsonObject({ + {QSL("output_id"), QJsonValue::fromVariant((quint64)opId)}, + }); + + QJsonDocument reply = genericRequest(QJsonDocument(request)); + + return (reply[QSL("result")].toString() == QSL("ok")); +} + +QJsonArray LXQt::Panel::Wayfire::getWorkspaceSetsInfo() const +{ + QJsonObject request; + request[QSL("method")] = QSL("window-rules/list-wsets"); + + return genericRequest(QJsonDocument(request)).array(); +} + +QString LXQt::Panel::Wayfire::getWorkspaceName(int x, QString outputName) const +{ + QString targetKey = QString::fromUtf8("%1_workspace_%2").arg(outputName).arg(x); + + QJsonObject request; + request[QSL("method")] = QSL("wayfire/get-config-option"); + request[QSL("data")] = QJsonObject({ + {QSL("option"), QSL("workspace-names/names")}, + }); + + QJsonObject wsNamesObj = genericRequest(QJsonDocument(request)).object(); + + QJsonArray wsNameList = wsNamesObj[QSL("value")].toArray(); + + for ( int i = 0; i < wsNameList.size(); i++ ) + { + QJsonArray wsNamePair = wsNameList[i].toArray(); + + if (wsNamePair.size() != 2) + { + qDebug() << "Invalid name-pair recieved"; + continue; + } + + QString key = wsNamePair[0].toString(); + if (key == targetKey) + { + QString name = wsNamePair[1].toString(); + return name; + } + } + + return QString::fromUtf8("Workspace %1").arg(x); +} + +bool LXQt::Panel::Wayfire::setWorkspaceName(int, QString) const +{ + return false; +} + +bool LXQt::Panel::Wayfire::switchToWorkspace(WaylandId opId, int64_t nth, WaylandId viewId) const +{ + QJsonObject wsetsInfo = getWorkspaceSetsInfo().at(0).toObject(); + QJsonObject workspace = wsetsInfo[QSL("workspace")].toObject(); + int64_t nCols = workspace[QSL("grid_width")].toInt(); + + int64_t row = floor((nth - 1) / nCols); + int64_t col = (nth - 1) % nCols; + + QJsonObject request; + request[QSL("method")] = QSL("vswitch/set-workspace"); + if (viewId) + { + request[QSL("data")] = QJsonObject({ + {QSL("output-id"), QJsonValue::fromVariant((quint64)opId)}, + {QSL("x"), QJsonValue::fromVariant((quint64)col)}, + {QSL("y"), QJsonValue::fromVariant((quint64)row)}, + {QSL("view-id"), QJsonValue::fromVariant((quint64)viewId)}, + }); + } else + { + request[QSL("data")] = QJsonObject({ + {QSL("output-id"), QJsonValue::fromVariant((quint64)opId)}, + {QSL("x"), QJsonValue::fromVariant((quint64)col)}, + + {QSL("y"), QJsonValue::fromVariant((quint64)row)}, + }); + } + + QJsonDocument reply = genericRequest(QJsonDocument(request)); + + return (reply[QSL("result")].toString() == QSL("ok")); +} + +QJsonArray LXQt::Panel::Wayfire::listViews() const +{ + QJsonObject request; + request[QSL("method")] = QSL("window-rules/list-views"); + + QJsonArray response = genericRequest(QJsonDocument(request)).array(); + + QJsonArray views; + for ( QJsonValue viewVal : response ) + { + if (viewVal.isObject()) + { + QJsonObject view = viewVal.toObject(); + // Ghost windows of Xwayland + if (view[QSL("pid")].toInt() == -1) + { + continue; + } + + // Proper toplevel view + if (view[QSL("role")] == QSL("toplevel")) + { + views << view; + } + } + } + + return views; +} + +WaylandId LXQt::Panel::Wayfire::getActiveView() const +{ + QJsonObject request; + + request[QSL("method")] = QSL("window-rules/get-focused-view"); + + QJsonObject reply = genericRequest(QJsonDocument(request)).object(); + + if (reply[QSL("result")].toString() == QSL("ok")) + { + QJsonObject viewInfo = reply[QSL("info")].toObject(); + uint32_t viewId = viewInfo[QSL("id")].toInt(); + + return WaylandId(viewId); + } + + return WaylandId(); +} + +QJsonObject LXQt::Panel::Wayfire::getViewInfo(WaylandId viewId) const +{ + QJsonObject request; + + request[QSL("method")] = QSL("window-rules/view-info"); + request[QSL("data")] = QJsonObject({ + {QSL("id"), QJsonValue::fromVariant((quint64)viewId)} + }); + + QJsonObject reply = genericRequest(QJsonDocument(request)).object(); + + if (reply[QSL("result")].toString() == QSL("ok")) + { + return reply[QSL("info")].toObject(); + } + + return QJsonObject(); +} + +bool LXQt::Panel::Wayfire::focusView(WaylandId viewId) const +{ + QJsonObject viewInfo = getViewInfo(viewId); + + if ((viewInfo.isEmpty() == false) && (viewInfo[QSL("minimized")].toBool() == true)) + { + minimizeView(viewId, false); + } + + QJsonObject request; + request[QSL("method")] = QSL("window-rules/focus-view"); + request[QSL("data")] = QJsonObject({ + {QSL("id"), QJsonValue::fromVariant((quint64)viewId)} + }); + + QJsonObject reply = genericRequest(QJsonDocument(request)).object(); + + return (reply[QSL("result")].toString() == QSL("ok")); +} + +bool LXQt::Panel::Wayfire::minimizeView(WaylandId viewId, bool yes) const +{ + QJsonObject request; + + request[QSL("method")] = QSL("wm-actions/set-minimized"); + request[QSL("data")] = QJsonObject({ + {QSL("view_id"), QJsonValue::fromVariant((quint64)viewId)}, + {QSL("state"), yes} + }); + + QJsonDocument reply = genericRequest(QJsonDocument(request)); + + if (reply[QSL("result")] != QSL("ok")) + { + qWarning() << QJsonDocument(reply).toJson().data() << "\n"; + } + + return (reply[QSL("result")].toString() == QSL("ok")); +} + +bool LXQt::Panel::Wayfire::maximizeView(WaylandId viewId, int edges) const +{ + QJsonObject request; + + /** + * Support for this does not yet exist in Wayfire. Pending PR from Marcus Britanicus. + */ + + // request[QSL("method")] = QSL("wm-actions/set-tiled"); + // request[QSL("data")] = QJsonObject({ + // {QSL("view_id"), QJsonValue::fromVariant((quint64)viewId)}, + // {QSL("view-id"), QJsonValue::fromVariant((quint64)viewId)}, + // {QSL("id"), QJsonValue::fromVariant((quint64)viewId)}, + // {QSL("edges"), edges} + // }); + + request[QSL("method")] = (edges ? QSL("grid/slot_c") : QSL("grid/restore")); + request[QSL("data")] = QJsonObject({ + {QSL("view_id"), QJsonValue::fromVariant((quint64)viewId)}, + }); + + QJsonDocument reply = genericRequest(QJsonDocument(request)); + + if (reply[QSL("result")] != QSL("ok")) + { + qWarning() << QJsonDocument(reply).toJson().data() << "\n"; + } + + return (reply[QSL("result")].toString() == QSL("ok")); +} + +bool LXQt::Panel::Wayfire::fullscreenView(WaylandId viewId, bool yes) const +{ + QJsonObject request; + + request[QSL("method")] = QSL("wm-actions/set-fullscreen"); + request[QSL("data")] = QJsonObject({ + {QSL("view_id"), QJsonValue::fromVariant((quint64)viewId)}, + {QSL("state"), yes} + }); + + QJsonDocument reply = genericRequest(QJsonDocument(request)); + + return (reply[QSL("result")].toString() == QSL("ok")); +} + +bool LXQt::Panel::Wayfire::restoreView(WaylandId viewId) const +{ + QJsonObject viewInfo = getViewInfo(viewId); + + if (viewInfo.isEmpty()) + { + return false; + } + + qDebug() << "Restoring view" << viewId; + + /** If it's minimized, unminimize it */ + if (viewInfo[QSL("minimized")] == true) + { + return minimizeView(viewId, false); + } + /** The view is fullscreened, un-fullscreen it */ + else if (viewInfo[QSL("fullscreen")] == true) + { + return fullscreenView(viewId, false); + } + /** The view is maximized, unmaximize it */ + else if (viewInfo[QSL("tiled-edges")] != 0) + { + return maximizeView(viewId, false); + } + + return false; +} + +bool LXQt::Panel::Wayfire::sendViewToWorkspace(WaylandId viewId, int nth) const +{ + QJsonObject request; + + /** Set view sticky */ + if ( nth == 0 ) { + QJsonObject viewInfo = getViewInfo( viewId ); + request[QSL("method")] = QSL("wm-actions/set-sticky"); + request[QSL("data")] = QJsonObject({ + {QSL("view-id"), QJsonValue::fromVariant((quint64)viewId)}, + {QSL("view_id"), QJsonValue::fromVariant((quint64)viewId)}, + {QSL("state"), !viewInfo[QSL("sticky")].toBool()}, + }); + } + + /** Send view to a specific desktop */ + else { + QJsonObject wsetsInfo = getWorkspaceSetsInfo().at(0).toObject(); + QJsonObject workspace = wsetsInfo[QSL("workspace")].toObject(); + QJsonObject viewInfo = getViewInfo(viewId); + + int64_t nCols = workspace[QSL("grid_width")].toInt(); + + int64_t row = floor((nth - 1) / nCols); + int64_t col = (nth - 1) % nCols; + + quint64 opId = viewInfo[QSL("output-id")].toInt(); + + request[QSL("method")] = QSL("vswitch/send-view"); + request[QSL("data")] = QJsonObject({ + {QSL("output-id"), QJsonValue::fromVariant((quint64)opId)}, + {QSL("x"), QJsonValue::fromVariant((quint64)col)}, + {QSL("y"), QJsonValue::fromVariant((quint64)row)}, + {QSL("view-id"), QJsonValue::fromVariant((quint64)viewId)}, + {QSL("view_id"), QJsonValue::fromVariant((quint64)viewId)}, + }); + } + + QJsonDocument reply = genericRequest(QJsonDocument(request)); + + return (reply[QSL("result")].toString() == QSL("ok")); +} + +bool LXQt::Panel::Wayfire::closeView(WaylandId viewId) const +{ + QJsonObject request; + + request[QSL("method")] = QSL("window-rules/close-view"); + request[QSL("data")] = QJsonObject({ + {QSL("id"), QJsonValue::fromVariant((quint64)viewId)} + }); + + QJsonDocument reply = genericRequest(QJsonDocument(request)); + + return (reply[QSL("result")].toString() == QSL("ok")); +} + +QJsonDocument LXQt::Panel::Wayfire::genericRequest(QJsonDocument request) const +{ + if (!impl->mConnected) + { + QJsonDocument reply{ + {QSL("result"), QSL("failed")} + }; + return reply; + } + + uint32_t reqId = impl->request(request); + std::shared_ptr reply = std::make_shared(); + + QEventLoop loop; + + auto connection = connect( + impl.data(), &LXQt::Panel::WayfireImpl::response, &loop, + [&reply, reqId, &loop] (uint32_t id, QJsonDocument response) + { + if (id == reqId) + { + *reply = response; // Update the content of the shared_ptr + loop.quit(); + } + }); + + loop.exec(); + + // Disconnect the signal-slot connection to avoid any potential issues + disconnect(connection); + + // Return the QJsonDocument, not the shared_ptr + return *reply; +} + +void LXQt::Panel::Wayfire::parseEvents(QJsonDocument response) +{ + QString event = response[QSL("event")].toString(); + + if (event == QSL("view-mapped")) + { + emit viewMapped(response); + } else if (event == QSL("view-focused")) + { + emit viewFocused(response); + } else if (event == QSL("view-title-changed")) + { + emit viewTitleChanged(response); + } else if (event == QSL("view-app-id-changed")) + { + emit viewAppIdChanged(response); + } else if (event == QSL("view-geometry-changed")) + { + emit viewGeometryChanged(response); + } else if (event == QSL("view-tiled")) + { + emit viewTiled(response); + } else if (event == QSL("view-minimized")) + { + emit viewMinimized(response); + } else if (event == QSL("view-set-output")) + { + emit viewOutputChanged(response); + } else if (event == QSL("view-workspace-changed")) + { + emit viewWorkspaceChanged(response); + } else if (event == QSL("view-unmapped")) + { + emit viewUnmapped(response); + } else if (event == QSL("output-added")) + { + emit outputAdded(response); + } else if (event == QSL("output-removed")) + { + emit outputRemoved(response); + } else if (event == QSL("output-gain-focus")) + { + emit outputFocused(response); + } else if (event == QSL("output-wset-changed")) + { + emit workspaceSetChanged(response); + } else if (event == QSL("wset-workspace-changed")) + { + emit workspaceChanged(response); + } else + { + emit genericEvent(response); + } +} diff --git a/panel/backends/wayland/wayfire/wayfire-common.h b/panel/backends/wayland/wayfire/wayfire-common.h new file mode 100644 index 000000000..b295947bf --- /dev/null +++ b/panel/backends/wayland/wayfire/wayfire-common.h @@ -0,0 +1,280 @@ +/* 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 */ + +#pragma once + +/** For struct pollfd */ +#include + +/* For QString, QThread, QTimer, etc.. */ +#include + +#include "../../lxqttaskbartypes.h" + +namespace LXQt +{ +namespace Panel +{ +class Wayfire; +class WayfireImpl; +} +} + +// Strongly-typed wrapper for Wayland IDs +struct WaylandId +{ + uint32_t id; + explicit WaylandId(uint32_t id_ = 0) : id(id_) + {} + operator WId() const + { + return static_cast(id); + } +}; + + +class LXQt::Panel::WayfireImpl : public QObject, public QRunnable +{ + Q_OBJECT; + + public: + WayfireImpl(); + + /** Stop polling This will terminate the thread and delete this object + */ + void stop(); + + /** Request the compositor something */ + uint32_t request(QJsonDocument req); + + /** One FD for reading, one for writing */ + struct pollfd wfSock; + + /** The socket address */ + QString wfSockPath; + + /** Stop the loop flag */ + volatile bool mTerminate = false; + + /** Flag to check if we're connected or not */ + volatile bool mConnected = false; + + /** Function to write the json to wayfire socket. */ + bool writeJson(QJsonDocument j); + + /** Function to read the data from wayfire socket and parse it into json */ + QJsonDocument readJson(); + + private: + QList mPendingRequests; + QMutex mutex; // Add a mutex for thread safety + + /** Function to read the json from wayfire socket into a buffer. */ + bool readExact(char *buf, uint size); + + protected: + void run() override; + + Q_SIGNALS: + /** We recieved an event from Wayfire */ + void wayfireEvent(QJsonDocument); + + /** Relay the message received from the server */ + void response(uint32_t, QJsonDocument); + + /** Inform the rest that polling has started */ + void started(); +}; + + +class LXQt::Panel::Wayfire : public QObject +{ + Q_OBJECT + + public: + Wayfire(QString wfSock = QString()); + ~Wayfire(); + + /** Connect to Wayfire */ + bool connectToServer() const; + + /* ========== Specific requests ========== */ + + /** Request the compositor to send us the outputs information */ + QJsonArray listOutputs() const; + + /** Request the compositor to send us the outputs information */ + WaylandId getActiveOutput() const; + + /** Request the compositor to send us information of a given output */ + QJsonObject getOutputInfo(WaylandId opId) const; + + /** Request the compositor to focus a given output */ + bool focusOutput(WaylandId opId) const; + + /** Request the compositor to trigger show-desktop a given output */ + bool showDesktop(WaylandId opId) const; + + /** Request the compositor to send us the wsets information */ + QJsonArray getWorkspaceSetsInfo() const; + + /** Request the compositor to send us the wsets information */ + QString getWorkspaceName(int, QString outputName = QString()) const; + bool setWorkspaceName(int, QString) const; + + /** Request the compositor to change the current workspace */ + bool switchToWorkspace(WaylandId opId, int64_t nth, WaylandId viewId = WaylandId()) const; + + /** Request the compositor to send us the list of views */ + QJsonArray listViews() const; + + /** Request the compositor to send us the info of the given view */ + WaylandId getActiveView() const; + + /** Request the compositor to send us the info of the given view */ + QJsonObject getViewInfo(WaylandId viewId) const; + + /** Request the compositor to focus a given view */ + bool focusView(WaylandId viewId) const; + + /** Request the compositor to minimize/restore a given view */ + bool minimizeView(WaylandId viewId, bool) const; + + /** Request the compositor to maximize/restore a given view */ + bool maximizeView(WaylandId viewId, int edges) const; + + /** Request the compositor to fullscreen a given view */ + bool fullscreenView(WaylandId viewId, bool) const; + + /** Request the compositor to focus a given view */ + bool restoreView(WaylandId viewId) const; + + /** Request the compositor to focus a given view */ + bool sendViewToWorkspace(WaylandId viewId, int nth) const; + + /** Request the compositor to focus a given view */ + bool closeView(WaylandId viewId) const; + + /** This is a generic request */ + QJsonDocument genericRequest(QJsonDocument) const; + + /* ========== WAYFIRE EVENTS ========== */ + + /** + * A new output was added. + */ + Q_SIGNAL void outputAdded(QJsonDocument); + + /** + * An existing output was removed. + */ + Q_SIGNAL void outputRemoved(QJsonDocument); + + /** + * A particular output has gained focus. Only one output can have focus at any given time. This outupt can + * be used to calculate the focused view. + */ + Q_SIGNAL void outputFocused(QJsonDocument); + + /** + * Current workspace set of a given output was changed When the wset of an output changes, always query + * the workspace + */ + Q_SIGNAL void workspaceSetChanged(QJsonDocument); + + /** + * The active workspace of a given wset changed. Because wsets of different outputs are independent, + * workspace change on one output will not affect the other. + */ + Q_SIGNAL void workspaceChanged(QJsonDocument); + + /** + * A view was just mapped. This view does not have an output nor a wset. Even the title and app-id will be + * unset. Only the view id is valid. + */ + Q_SIGNAL void viewMapped(QJsonDocument); + + /** + * A view on a particular outupt gained focus Since wayfire supports independent outputs, each output can + * have a view with focus. The focused view of the active outupt will be the one with keyboard focus. + */ + Q_SIGNAL void viewFocused(QJsonDocument); + + /** + * The title of a view changed. The title can be "nil" or null. In such cases, the user can safely ignore + * it. + */ + Q_SIGNAL void viewTitleChanged(QJsonDocument); + + /** + * The app-id of a view changed. The app-id can be "nil" or null. In such cases, the user can safely + * ignore it. + */ + Q_SIGNAL void viewAppIdChanged(QJsonDocument); + + /** + * The geometry of a view changed. + */ + Q_SIGNAL void viewGeometryChanged(QJsonDocument); + + /** + * The tiled status of a view has changed. + */ + Q_SIGNAL void viewTiled(QJsonDocument); + + /** + * A view was minimized. + */ + Q_SIGNAL void viewMinimized(QJsonDocument); + + /** + * The output of a view changed. When the view gets mapped, the output will be null. + */ + Q_SIGNAL void viewOutputChanged(QJsonDocument); + + /** + * The workspace of a view changed. + */ + Q_SIGNAL void viewWorkspaceChanged(QJsonDocument); + + /** + * An existing view was unmapped. The output, wset etc of this view are invalid as of now. + */ + Q_SIGNAL void viewUnmapped(QJsonDocument); + + /** + * A generic wayfire event. This signal is always emitted for all events. Specific signals are emitted by + * parsing this event. + */ + Q_SIGNAL void genericEvent(QJsonDocument); + + /** + * Inform the user tha there was an error. Currently used to indicate failure in watching for wayfire + * events. + */ + Q_SIGNAL void error(); + + private: + /** Implementation class pointer */ + QScopedPointer impl; + + /** Partially parse the events to emit the correct signals */ + void parseEvents(QJsonDocument); +}; diff --git a/panel/backends/wayland/wlroots/lxqtwmbackend_wlr.cpp b/panel/backends/wayland/wlroots/lxqtwmbackend_wlr.cpp index 2e3d04e6c..78a0944dc 100644 --- a/panel/backends/wayland/wlroots/lxqtwmbackend_wlr.cpp +++ b/panel/backends/wayland/wlroots/lxqtwmbackend_wlr.cpp @@ -241,7 +241,7 @@ int LXQtTaskbarWlrootsBackend::getWorkspacesCount() const return 1; } -QString LXQtTaskbarWlrootsBackend::getWorkspaceName(int) const +QString LXQtTaskbarWlrootsBackend::getWorkspaceName(int, QString) const { return QStringLiteral("Desktop 1"); } diff --git a/panel/backends/wayland/wlroots/lxqtwmbackend_wlr.h b/panel/backends/wayland/wlroots/lxqtwmbackend_wlr.h index aebeee04e..c0f4c9e1d 100644 --- a/panel/backends/wayland/wlroots/lxqtwmbackend_wlr.h +++ b/panel/backends/wayland/wlroots/lxqtwmbackend_wlr.h @@ -45,7 +45,7 @@ class LXQtTaskbarWlrootsBackend : public ILXQtAbstractWMInterface // Workspaces virtual int getWorkspacesCount() const override; - virtual QString getWorkspaceName(int idx) const override; + virtual QString getWorkspaceName(int idx, QString sceenName = QString()) const override; virtual int getCurrentWorkspace() const override; virtual bool setCurrentWorkspace(int idx) override; diff --git a/panel/backends/xcb/lxqtwmbackend_x11.cpp b/panel/backends/xcb/lxqtwmbackend_x11.cpp index 471c01385..bbf045daa 100644 --- a/panel/backends/xcb/lxqtwmbackend_x11.cpp +++ b/panel/backends/xcb/lxqtwmbackend_x11.cpp @@ -57,7 +57,9 @@ LXQtWMBackendX11::LXQtWMBackendX11(QObject *parent) connect(KX11Extras::self(), &KX11Extras::windowRemoved, this, &LXQtWMBackendX11::onWindowRemoved); connect(KX11Extras::self(), &KX11Extras::numberOfDesktopsChanged, this, &ILXQtAbstractWMInterface::workspacesCountChanged); - connect(KX11Extras::self(), &KX11Extras::currentDesktopChanged, this, &ILXQtAbstractWMInterface::currentWorkspaceChanged); + connect(KX11Extras::self(), &KX11Extras::currentDesktopChanged, this, [this](int x) { + emit currentWorkspaceChanged(x, QString()); // without specifying an output name + }); connect(KX11Extras::self(), &KX11Extras::desktopNamesChanged, this, [this]() { emit workspaceNameChanged(-1); // without specifying an index }); @@ -477,7 +479,7 @@ int LXQtWMBackendX11::getWorkspacesCount() const return KX11Extras::numberOfDesktops(); } -QString LXQtWMBackendX11::getWorkspaceName(int idx) const +QString LXQtWMBackendX11::getWorkspaceName(int idx, QString) const { return KX11Extras::desktopName(idx); } diff --git a/panel/backends/xcb/lxqtwmbackend_x11.h b/panel/backends/xcb/lxqtwmbackend_x11.h index b1f039c92..222408ccd 100644 --- a/panel/backends/xcb/lxqtwmbackend_x11.h +++ b/panel/backends/xcb/lxqtwmbackend_x11.h @@ -70,7 +70,7 @@ class LXQtWMBackendX11 : public ILXQtAbstractWMInterface // Workspaces virtual int getWorkspacesCount() const override; - virtual QString getWorkspaceName(int idx) const override; + virtual QString getWorkspaceName(int idx, QString screenName = QString()) const override; virtual int getCurrentWorkspace() const override; virtual bool setCurrentWorkspace(int idx) override; diff --git a/plugin-taskbar/lxqttaskbutton.cpp b/plugin-taskbar/lxqttaskbutton.cpp index 67a1b4a39..551a069e5 100644 --- a/plugin-taskbar/lxqttaskbutton.cpp +++ b/plugin-taskbar/lxqttaskbutton.cpp @@ -238,10 +238,16 @@ void LXQtTaskButton::mouseReleaseEvent(QMouseEvent* event) { if (!sDraggging && event->button() == Qt::LeftButton) { - if (isChecked()) + qDebug() << "=================================================="; + qDebug() << "--> button is checked" << isChecked(); + if (isChecked()){ + qDebug() << "minimizeApplication() called"; minimizeApplication(); - else + } + else{ + qDebug() << "raiseApplication() called"; raiseApplication(); + } } QToolButton::mouseReleaseEvent(event); } @@ -405,6 +411,7 @@ bool LXQtTaskButton::isApplicationActive() const ************************************************/ void LXQtTaskButton::raiseApplication() { + qDebug() << "--> raising window" << mWindow; mBackend->raiseWindow(mWindow, parentTaskBar()->raiseOnCurrentDesktop()); } @@ -413,6 +420,7 @@ void LXQtTaskButton::raiseApplication() ************************************************/ void LXQtTaskButton::minimizeApplication() { + qDebug() << "--> minimizing window" << mWindow; mBackend->setWindowState(mWindow, LXQtTaskBarWindowState::Minimized, true); } diff --git a/plugin-taskbar/lxqttaskgroup.cpp b/plugin-taskbar/lxqttaskgroup.cpp index 645c8115a..f73e0c3d9 100644 --- a/plugin-taskbar/lxqttaskgroup.cpp +++ b/plugin-taskbar/lxqttaskgroup.cpp @@ -205,6 +205,14 @@ void LXQtTaskGroup::onActiveWindowChanged(WId window) button->setUrgencyHint(false); } setChecked(nullptr != button); + + qDebug() << "=================================="; + qDebug() << "[Taskbar Group]" << groupName() << "onActiveChanged" << window << (button ? true : false); + if (button) + qDebug() << "Button checked" << button->isChecked() << checkedButton(); + + qDebug() << "Group checked" << isChecked(); + qDebug() << "=================================="; } /************************************************