mirror of https://github.com/Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1127 lines
36 KiB
1127 lines
36 KiB
/* |
|
SPDX-FileCopyrightText: 2016 Eike Hein <[email protected]> |
|
SPDX-FileCopyrightText: 2008 Aaron J. Seigo <[email protected]> |
|
|
|
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL |
|
*/ |
|
|
|
#include "xwindowtasksmodel.h" |
|
#include "tasktools.h" |
|
#include "xwindowsystemeventbatcher.h" |
|
|
|
#include <KDesktopFile> |
|
#include <KDirWatch> |
|
#include <KIconLoader> |
|
#include <KService> |
|
#include <KSharedConfig> |
|
#include <KStartupInfo> |
|
#include <KSycoca> |
|
#include <KWindowInfo> |
|
#include <KWindowSystem> |
|
|
|
#include <QBuffer> |
|
#include <QDir> |
|
#include <QFile> |
|
#include <QIcon> |
|
#include <QSet> |
|
#include <QTimer> |
|
#include <QUrlQuery> |
|
#include <QX11Info> |
|
|
|
namespace TaskManager |
|
{ |
|
static const NET::Properties windowInfoFlags = |
|
NET::WMState | NET::XAWMState | NET::WMDesktop | NET::WMVisibleName | NET::WMGeometry | NET::WMFrameExtents | NET::WMWindowType | NET::WMPid; |
|
static const NET::Properties2 windowInfoFlags2 = NET::WM2DesktopFileName | NET::WM2Activities | NET::WM2WindowClass | NET::WM2AllowedActions |
|
| NET::WM2AppMenuObjectPath | NET::WM2AppMenuServiceName | NET::WM2GTKApplicationId; |
|
|
|
class Q_DECL_HIDDEN XWindowTasksModel::Private |
|
{ |
|
public: |
|
Private(XWindowTasksModel *q); |
|
~Private(); |
|
|
|
QVector<WId> windows; |
|
|
|
// key=transient child, value=leader |
|
QHash<WId, WId> transients; |
|
// key=leader, values=transient children |
|
QMultiHash<WId, WId> transientsDemandingAttention; |
|
|
|
QHash<WId, KWindowInfo *> windowInfoCache; |
|
QHash<WId, AppData> appDataCache; |
|
QHash<WId, QRect> delegateGeometries; |
|
QSet<WId> usingFallbackIcon; |
|
QHash<WId, QTime> lastActivated; |
|
QList<WId> cachedStackingOrder; |
|
WId activeWindow = -1; |
|
KSharedConfig::Ptr rulesConfig; |
|
KDirWatch *configWatcher = nullptr; |
|
QTimer sycocaChangeTimer; |
|
|
|
void init(); |
|
void addWindow(WId window); |
|
void removeWindow(WId window); |
|
void windowChanged(WId window, NET::Properties properties, NET::Properties2 properties2); |
|
void transientChanged(WId window, NET::Properties properties, NET::Properties2 properties2); |
|
void dataChanged(WId window, const QVector<int> &roles); |
|
|
|
KWindowInfo *windowInfo(WId window); |
|
AppData appData(WId window); |
|
QString appMenuServiceName(WId window); |
|
QString appMenuObjectPath(WId window); |
|
|
|
QIcon icon(WId window); |
|
static QString mimeType(); |
|
static QString groupMimeType(); |
|
QUrl windowUrl(WId window); |
|
QUrl launcherUrl(WId window, bool encodeFallbackIcon = true); |
|
bool demandsAttention(WId window); |
|
|
|
private: |
|
XWindowTasksModel *q; |
|
}; |
|
|
|
XWindowTasksModel::Private::Private(XWindowTasksModel *q) |
|
: q(q) |
|
{ |
|
} |
|
|
|
XWindowTasksModel::Private::~Private() |
|
{ |
|
qDeleteAll(windowInfoCache); |
|
windowInfoCache.clear(); |
|
} |
|
|
|
void XWindowTasksModel::Private::init() |
|
{ |
|
auto clearCacheAndRefresh = [this] { |
|
if (!windows.count()) { |
|
return; |
|
} |
|
|
|
appDataCache.clear(); |
|
|
|
// Emit changes of all roles satisfied from app data cache. |
|
Q_EMIT q->dataChanged(q->index(0, 0), |
|
q->index(windows.count() - 1, 0), |
|
QVector<int>{Qt::DecorationRole, |
|
AbstractTasksModel::AppId, |
|
AbstractTasksModel::AppName, |
|
AbstractTasksModel::GenericName, |
|
AbstractTasksModel::LauncherUrl, |
|
AbstractTasksModel::LauncherUrlWithoutIcon, |
|
AbstractTasksModel::CanLaunchNewInstance, |
|
AbstractTasksModel::SkipTaskbar}); |
|
}; |
|
|
|
cachedStackingOrder = KWindowSystem::stackingOrder(); |
|
|
|
sycocaChangeTimer.setSingleShot(true); |
|
sycocaChangeTimer.setInterval(100); |
|
|
|
QObject::connect(&sycocaChangeTimer, &QTimer::timeout, q, clearCacheAndRefresh); |
|
|
|
QObject::connect(KSycoca::self(), &KSycoca::databaseChanged, q, [this]() { |
|
sycocaChangeTimer.start(); |
|
}); |
|
|
|
rulesConfig = KSharedConfig::openConfig(QStringLiteral("taskmanagerrulesrc")); |
|
configWatcher = new KDirWatch(q); |
|
|
|
foreach (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); |
|
|
|
auto windowSystem = new XWindowSystemEventBatcher(q); |
|
|
|
QObject::connect(windowSystem, &XWindowSystemEventBatcher::windowAdded, q, [this](WId window) { |
|
addWindow(window); |
|
}); |
|
|
|
QObject::connect(windowSystem, &XWindowSystemEventBatcher::windowRemoved, q, [this](WId window) { |
|
removeWindow(window); |
|
}); |
|
|
|
QObject::connect(windowSystem, &XWindowSystemEventBatcher::windowChanged, q, [this](WId window, NET::Properties properties, NET::Properties2 properties2) { |
|
windowChanged(window, properties, properties2); |
|
}); |
|
|
|
// Update IsActive for previously- and newly-active windows. |
|
QObject::connect(KWindowSystem::self(), &KWindowSystem::activeWindowChanged, q, [this](WId window) { |
|
const WId oldActiveWindow = activeWindow; |
|
|
|
const auto leader = transients.value(window, XCB_WINDOW_NONE); |
|
if (leader != XCB_WINDOW_NONE) { |
|
window = leader; |
|
} |
|
|
|
activeWindow = window; |
|
lastActivated[activeWindow] = QTime::currentTime(); |
|
|
|
int row = windows.indexOf(oldActiveWindow); |
|
|
|
if (row != -1) { |
|
dataChanged(oldActiveWindow, QVector<int>{IsActive}); |
|
} |
|
|
|
row = windows.indexOf(window); |
|
|
|
if (row != -1) { |
|
dataChanged(window, QVector<int>{IsActive}); |
|
} |
|
}); |
|
|
|
QObject::connect(KWindowSystem::self(), &KWindowSystem::stackingOrderChanged, q, [this]() { |
|
cachedStackingOrder = KWindowSystem::stackingOrder(); |
|
Q_EMIT q->dataChanged(q->index(0, 0), q->index(q->rowCount() - 1, 0), QVector<int>{StackingOrder}); |
|
}); |
|
|
|
activeWindow = KWindowSystem::activeWindow(); |
|
|
|
// Add existing windows. |
|
foreach (const WId window, KWindowSystem::windows()) { |
|
addWindow(window); |
|
} |
|
} |
|
|
|
void XWindowTasksModel::Private::addWindow(WId window) |
|
{ |
|
// Don't add window twice. |
|
if (windows.contains(window)) { |
|
return; |
|
} |
|
|
|
KWindowInfo info(window, NET::WMWindowType | NET::WMState | NET::WMName | NET::WMVisibleName, NET::WM2TransientFor); |
|
|
|
NET::WindowType wType = info.windowType(NET::NormalMask | NET::DesktopMask | NET::DockMask | NET::ToolbarMask | NET::MenuMask | NET::DialogMask |
|
| NET::OverrideMask | NET::TopMenuMask | NET::UtilityMask | NET::SplashMask); |
|
|
|
const WId leader = info.transientFor(); |
|
|
|
// Handle transient. |
|
if (leader > 0 && leader != window && leader != QX11Info::appRootWindow() && !transients.contains(window) && windows.contains(leader)) { |
|
transients.insert(window, leader); |
|
|
|
// Update demands attention state for leader. |
|
if (info.hasState(NET::DemandsAttention) && windows.contains(leader)) { |
|
transientsDemandingAttention.insert(leader, window); |
|
dataChanged(leader, QVector<int>{IsDemandingAttention}); |
|
} |
|
|
|
return; |
|
} |
|
|
|
// Ignore NET::Tool and other special window types; they are not considered tasks. |
|
if (wType != NET::Normal && wType != NET::Override && wType != NET::Unknown && wType != NET::Dialog && wType != NET::Utility) { |
|
return; |
|
} |
|
|
|
const int count = windows.count(); |
|
q->beginInsertRows(QModelIndex(), count, count); |
|
windows.append(window); |
|
q->endInsertRows(); |
|
} |
|
|
|
void XWindowTasksModel::Private::removeWindow(WId window) |
|
{ |
|
const int row = windows.indexOf(window); |
|
|
|
if (row != -1) { |
|
q->beginRemoveRows(QModelIndex(), row, row); |
|
windows.removeAt(row); |
|
transientsDemandingAttention.remove(window); |
|
delete windowInfoCache.take(window); |
|
appDataCache.remove(window); |
|
delegateGeometries.remove(window); |
|
usingFallbackIcon.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)) { |
|
const WId leader = transientsDemandingAttention.key(window, XCB_WINDOW_NONE); |
|
|
|
if (leader != XCB_WINDOW_NONE) { |
|
transientsDemandingAttention.remove(leader, window); |
|
dataChanged(leader, QVector<int>{IsDemandingAttention}); |
|
} |
|
} |
|
} |
|
|
|
if (activeWindow == window) { |
|
activeWindow = -1; |
|
} |
|
} |
|
|
|
void XWindowTasksModel::Private::transientChanged(WId window, NET::Properties properties, NET::Properties2 properties2) |
|
{ |
|
// Changes to a transient's state might change demands attention state for leader. |
|
if (properties & (NET::WMState | NET::XAWMState)) { |
|
const KWindowInfo info(window, NET::WMState | NET::XAWMState, NET::WM2TransientFor); |
|
const WId leader = info.transientFor(); |
|
|
|
if (!windows.contains(leader)) { |
|
return; |
|
} |
|
|
|
if (info.hasState(NET::DemandsAttention)) { |
|
if (!transientsDemandingAttention.values(leader).contains(window)) { |
|
transientsDemandingAttention.insert(leader, window); |
|
dataChanged(leader, QVector<int>{IsDemandingAttention}); |
|
} |
|
} else if (transientsDemandingAttention.remove(window)) { |
|
dataChanged(leader, QVector<int>{IsDemandingAttention}); |
|
} |
|
// Leader might have changed. |
|
} else if (properties2 & NET::WM2TransientFor) { |
|
const KWindowInfo info(window, NET::WMState | NET::XAWMState, NET::WM2TransientFor); |
|
|
|
if (info.hasState(NET::DemandsAttention)) { |
|
const WId oldLeader = transientsDemandingAttention.key(window, XCB_WINDOW_NONE); |
|
|
|
if (oldLeader != XCB_WINDOW_NONE) { |
|
const WId leader = info.transientFor(); |
|
|
|
if (leader != oldLeader) { |
|
transientsDemandingAttention.remove(oldLeader, window); |
|
transientsDemandingAttention.insert(leader, window); |
|
dataChanged(oldLeader, QVector<int>{IsDemandingAttention}); |
|
dataChanged(leader, QVector<int>{IsDemandingAttention}); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
void XWindowTasksModel::Private::windowChanged(WId window, NET::Properties properties, NET::Properties2 properties2) |
|
{ |
|
if (transients.contains(window)) { |
|
transientChanged(window, properties, properties2); |
|
return; |
|
} |
|
|
|
bool wipeInfoCache = false; |
|
bool wipeAppDataCache = false; |
|
QVector<int> changedRoles; |
|
|
|
if (properties & (NET::WMPid) || properties2 & (NET::WM2DesktopFileName | NET::WM2WindowClass)) { |
|
wipeInfoCache = true; |
|
wipeAppDataCache = true; |
|
changedRoles << Qt::DecorationRole << AppId << AppName << GenericName << LauncherUrl << AppPid << SkipTaskbar << CanLaunchNewInstance; |
|
} |
|
|
|
if (properties & (NET::WMName | NET::WMVisibleName)) { |
|
changedRoles << Qt::DisplayRole; |
|
wipeInfoCache = true; |
|
} |
|
|
|
if ((properties & NET::WMIcon) && usingFallbackIcon.contains(window)) { |
|
wipeAppDataCache = true; |
|
|
|
if (!changedRoles.contains(Qt::DecorationRole)) { |
|
changedRoles << Qt::DecorationRole; |
|
} |
|
} |
|
|
|
// FIXME TODO: It might be worth keeping track of which windows were demanding |
|
// attention (or not) to avoid emitting this role on every state change, as |
|
// TaskGroupingProxyModel needs to check for group-ability when a change to it |
|
// is announced and the queried state is false. |
|
if (properties & (NET::WMState | NET::XAWMState)) { |
|
wipeInfoCache = true; |
|
changedRoles << IsFullScreen << IsMaximized << IsMinimized << IsKeepAbove << IsKeepBelow; |
|
changedRoles << IsShaded << IsDemandingAttention << SkipTaskbar << SkipPager; |
|
} |
|
|
|
if (properties & NET::WMWindowType) { |
|
wipeInfoCache = true; |
|
changedRoles << SkipTaskbar; |
|
} |
|
|
|
if (properties2 & NET::WM2AllowedActions) { |
|
wipeInfoCache = true; |
|
changedRoles << IsClosable << IsMovable << IsResizable << IsMaximizable << IsMinimizable; |
|
changedRoles << IsFullScreenable << IsShadeable << IsVirtualDesktopsChangeable; |
|
} |
|
|
|
if (properties & NET::WMDesktop) { |
|
wipeInfoCache = true; |
|
changedRoles << VirtualDesktops << IsOnAllVirtualDesktops; |
|
} |
|
|
|
if (properties & NET::WMGeometry) { |
|
wipeInfoCache = true; |
|
changedRoles << Geometry << ScreenGeometry; |
|
} |
|
|
|
if (properties2 & NET::WM2Activities) { |
|
changedRoles << Activities; |
|
} |
|
|
|
if (properties2 & NET::WM2AppMenuServiceName) { |
|
changedRoles << ApplicationMenuServiceName; |
|
} |
|
|
|
if (properties2 & NET::WM2AppMenuObjectPath) { |
|
changedRoles << ApplicationMenuObjectPath; |
|
} |
|
|
|
if (wipeInfoCache) { |
|
delete windowInfoCache.take(window); |
|
} |
|
|
|
if (wipeAppDataCache) { |
|
appDataCache.remove(window); |
|
usingFallbackIcon.remove(window); |
|
} |
|
|
|
if (!changedRoles.isEmpty()) { |
|
dataChanged(window, changedRoles); |
|
} |
|
} |
|
|
|
void XWindowTasksModel::Private::dataChanged(WId window, const QVector<int> &roles) |
|
{ |
|
const int i = windows.indexOf(window); |
|
|
|
if (i == -1) { |
|
return; |
|
} |
|
|
|
QModelIndex idx = q->index(i); |
|
Q_EMIT q->dataChanged(idx, idx, roles); |
|
} |
|
|
|
KWindowInfo *XWindowTasksModel::Private::windowInfo(WId window) |
|
{ |
|
const auto &it = windowInfoCache.constFind(window); |
|
|
|
if (it != windowInfoCache.constEnd()) { |
|
return *it; |
|
} |
|
|
|
KWindowInfo *info = new KWindowInfo(window, windowInfoFlags, windowInfoFlags2); |
|
windowInfoCache.insert(window, info); |
|
|
|
return info; |
|
} |
|
|
|
AppData XWindowTasksModel::Private::appData(WId window) |
|
{ |
|
const auto &it = appDataCache.constFind(window); |
|
|
|
if (it != appDataCache.constEnd()) { |
|
return *it; |
|
} |
|
|
|
const AppData &data = appDataFromUrl(windowUrl(window)); |
|
|
|
// If we weren't able to derive a launcher URL from the window meta data, |
|
// fall back to WM_CLASS Class string as app id. This helps with apps we |
|
// can't map to an URL due to existing outside the regular system |
|
// environment, e.g. wine clients. |
|
if (data.id.isEmpty() && data.url.isEmpty()) { |
|
AppData dataCopy = data; |
|
|
|
dataCopy.id = windowInfo(window)->windowClassClass(); |
|
|
|
appDataCache.insert(window, dataCopy); |
|
|
|
return dataCopy; |
|
} |
|
|
|
appDataCache.insert(window, data); |
|
|
|
return data; |
|
} |
|
|
|
QString XWindowTasksModel::Private::appMenuServiceName(WId window) |
|
{ |
|
const KWindowInfo *info = windowInfo(window); |
|
return QString::fromUtf8(info->applicationMenuServiceName()); |
|
} |
|
|
|
QString XWindowTasksModel::Private::appMenuObjectPath(WId window) |
|
{ |
|
const KWindowInfo *info = windowInfo(window); |
|
return QString::fromUtf8(info->applicationMenuObjectPath()); |
|
} |
|
|
|
QIcon XWindowTasksModel::Private::icon(WId window) |
|
{ |
|
const AppData &app = appData(window); |
|
|
|
if (!app.icon.isNull()) { |
|
return app.icon; |
|
} |
|
|
|
QIcon icon; |
|
|
|
icon.addPixmap(KWindowSystem::icon(window, KIconLoader::SizeSmall, KIconLoader::SizeSmall, false)); |
|
icon.addPixmap(KWindowSystem::icon(window, KIconLoader::SizeSmallMedium, KIconLoader::SizeSmallMedium, false)); |
|
icon.addPixmap(KWindowSystem::icon(window, KIconLoader::SizeMedium, KIconLoader::SizeMedium, false)); |
|
icon.addPixmap(KWindowSystem::icon(window, KIconLoader::SizeLarge, KIconLoader::SizeLarge, false)); |
|
|
|
appDataCache[window].icon = icon; |
|
usingFallbackIcon.insert(window); |
|
|
|
return icon; |
|
} |
|
|
|
QString XWindowTasksModel::Private::mimeType() |
|
{ |
|
return QStringLiteral("windowsystem/winid"); |
|
} |
|
|
|
QString XWindowTasksModel::Private::groupMimeType() |
|
{ |
|
return QStringLiteral("windowsystem/multiple-winids"); |
|
} |
|
|
|
QUrl XWindowTasksModel::Private::windowUrl(WId window) |
|
{ |
|
const KWindowInfo *info = windowInfo(window); |
|
|
|
QString desktopFile = QString::fromUtf8(info->desktopFileName()); |
|
|
|
if (desktopFile.isEmpty()) { |
|
desktopFile = QString::fromUtf8(info->gtkApplicationId()); |
|
} |
|
|
|
if (!desktopFile.isEmpty()) { |
|
KService::Ptr service = KService::serviceByStorageId(desktopFile); |
|
|
|
if (service) { |
|
const QString &menuId = service->menuId(); |
|
|
|
// applications: URLs are used to refer to applications by their KService::menuId |
|
// (i.e. .desktop file name) rather than the absolute path to a .desktop file. |
|
if (!menuId.isEmpty()) { |
|
return QUrl(QStringLiteral("applications:") + menuId); |
|
} |
|
|
|
return QUrl::fromLocalFile(service->entryPath()); |
|
} |
|
|
|
if (!desktopFile.endsWith(QLatin1String(".desktop"))) { |
|
desktopFile.append(QLatin1String(".desktop")); |
|
} |
|
|
|
if (KDesktopFile::isDesktopFile(desktopFile) && QFile::exists(desktopFile)) { |
|
return QUrl::fromLocalFile(desktopFile); |
|
} |
|
} |
|
|
|
return windowUrlFromMetadata(info->windowClassClass(), info->pid(), rulesConfig, info->windowClassName()); |
|
} |
|
|
|
QUrl XWindowTasksModel::Private::launcherUrl(WId window, bool encodeFallbackIcon) |
|
{ |
|
const AppData &data = appData(window); |
|
|
|
QUrl url = data.url; |
|
if (!encodeFallbackIcon || !data.icon.name().isEmpty()) { |
|
return url; |
|
} |
|
|
|
// Forego adding the window icon pixmap if the URL is otherwise empty. |
|
if (!url.isValid()) { |
|
return QUrl(); |
|
} |
|
|
|
// Only serialize pixmap data if the window pixmap is actually being used. |
|
// QIcon::name() used above only returns a themed icon name but nothing when |
|
// the icon was created using an absolute path, as can be the case with, e.g. |
|
// containerized apps. |
|
if (!usingFallbackIcon.contains(window)) { |
|
return url; |
|
} |
|
|
|
QPixmap pixmap; |
|
|
|
if (!data.icon.isNull()) { |
|
pixmap = data.icon.pixmap(KIconLoader::SizeLarge); |
|
} |
|
|
|
if (pixmap.isNull()) { |
|
pixmap = KWindowSystem::icon(window, KIconLoader::SizeLarge, KIconLoader::SizeLarge, false); |
|
} |
|
|
|
if (pixmap.isNull()) { |
|
return data.url; |
|
} |
|
QUrlQuery uQuery(url); |
|
QByteArray bytes; |
|
QBuffer buffer(&bytes); |
|
buffer.open(QIODevice::WriteOnly); |
|
pixmap.save(&buffer, "PNG"); |
|
uQuery.addQueryItem(QStringLiteral("iconData"), bytes.toBase64(QByteArray::Base64UrlEncoding)); |
|
|
|
url.setQuery(uQuery); |
|
|
|
return url; |
|
} |
|
|
|
bool XWindowTasksModel::Private::demandsAttention(WId window) |
|
{ |
|
if (windows.contains(window)) { |
|
return ((windowInfo(window)->hasState(NET::DemandsAttention)) || transientsDemandingAttention.contains(window)); |
|
} |
|
|
|
return false; |
|
} |
|
|
|
XWindowTasksModel::XWindowTasksModel(QObject *parent) |
|
: AbstractWindowTasksModel(parent) |
|
, d(new Private(this)) |
|
{ |
|
d->init(); |
|
} |
|
|
|
XWindowTasksModel::~XWindowTasksModel() |
|
{ |
|
} |
|
|
|
QVariant XWindowTasksModel::data(const QModelIndex &index, int role) const |
|
{ |
|
if (!index.isValid() || index.row() >= d->windows.count()) { |
|
return QVariant(); |
|
} |
|
|
|
const WId window = d->windows.at(index.row()); |
|
|
|
if (role == Qt::DisplayRole) { |
|
return d->windowInfo(window)->visibleName(); |
|
} else if (role == Qt::DecorationRole) { |
|
return d->icon(window); |
|
} else if (role == AppId) { |
|
return d->appData(window).id; |
|
} else if (role == AppName) { |
|
return d->appData(window).name; |
|
} else if (role == GenericName) { |
|
return d->appData(window).genericName; |
|
} else if (role == LauncherUrl) { |
|
return d->launcherUrl(window); |
|
} else if (role == LauncherUrlWithoutIcon) { |
|
return d->launcherUrl(window, false /* encodeFallbackIcon */); |
|
} else if (role == WinIdList) { |
|
return QVariantList() << window; |
|
} else if (role == MimeType) { |
|
return d->mimeType(); |
|
} else if (role == MimeData) { |
|
return QByteArray((char *)&window, sizeof(window)); |
|
} else if (role == IsWindow) { |
|
return true; |
|
} else if (role == IsActive) { |
|
return (window == d->activeWindow); |
|
} else if (role == IsClosable) { |
|
return d->windowInfo(window)->actionSupported(NET::ActionClose); |
|
} else if (role == IsMovable) { |
|
return d->windowInfo(window)->actionSupported(NET::ActionMove); |
|
} else if (role == IsResizable) { |
|
return d->windowInfo(window)->actionSupported(NET::ActionResize); |
|
} else if (role == IsMaximizable) { |
|
return d->windowInfo(window)->actionSupported(NET::ActionMax); |
|
} else if (role == IsMaximized) { |
|
const KWindowInfo *info = d->windowInfo(window); |
|
return info->hasState(NET::MaxHoriz) && info->hasState(NET::MaxVert); |
|
} else if (role == IsMinimizable) { |
|
return d->windowInfo(window)->actionSupported(NET::ActionMinimize); |
|
} else if (role == IsMinimized) { |
|
return d->windowInfo(window)->isMinimized(); |
|
} else if (role == IsHidden) { |
|
return d->windowInfo(window)->hasState(NET::Hidden); |
|
} else if (role == IsKeepAbove) { |
|
return d->windowInfo(window)->hasState(NET::KeepAbove); |
|
} else if (role == IsKeepBelow) { |
|
return d->windowInfo(window)->hasState(NET::KeepBelow); |
|
} else if (role == IsFullScreenable) { |
|
return d->windowInfo(window)->actionSupported(NET::ActionFullScreen); |
|
} else if (role == IsFullScreen) { |
|
return d->windowInfo(window)->hasState(NET::FullScreen); |
|
} else if (role == IsShadeable) { |
|
return d->windowInfo(window)->actionSupported(NET::ActionShade); |
|
} else if (role == IsShaded) { |
|
return d->windowInfo(window)->hasState(NET::Shaded); |
|
} else if (role == IsVirtualDesktopsChangeable) { |
|
return d->windowInfo(window)->actionSupported(NET::ActionChangeDesktop); |
|
} else if (role == VirtualDesktops) { |
|
return QVariantList() << d->windowInfo(window)->desktop(); |
|
} else if (role == IsOnAllVirtualDesktops) { |
|
return d->windowInfo(window)->onAllDesktops(); |
|
} else if (role == Geometry) { |
|
return d->windowInfo(window)->frameGeometry(); |
|
} else if (role == ScreenGeometry) { |
|
return screenGeometry(d->windowInfo(window)->frameGeometry().center()); |
|
} else if (role == Activities) { |
|
return d->windowInfo(window)->activities(); |
|
} else if (role == IsDemandingAttention) { |
|
return d->demandsAttention(window); |
|
} else if (role == SkipTaskbar) { |
|
const KWindowInfo *info = d->windowInfo(window); |
|
// _NET_WM_WINDOW_TYPE_UTILITY type windows should not be on task bars, |
|
// but they should be shown on pagers. |
|
return (info->hasState(NET::SkipTaskbar) || info->windowType(NET::UtilityMask) == NET::Utility || d->appData(window).skipTaskbar); |
|
} else if (role == SkipPager) { |
|
return d->windowInfo(window)->hasState(NET::SkipPager); |
|
} else if (role == AppPid) { |
|
return d->windowInfo(window)->pid(); |
|
} else if (role == StackingOrder) { |
|
return d->cachedStackingOrder.indexOf(window); |
|
} else if (role == LastActivated) { |
|
if (d->lastActivated.contains(window)) { |
|
return d->lastActivated.value(window); |
|
} |
|
} else if (role == ApplicationMenuObjectPath) { |
|
return d->appMenuObjectPath(window); |
|
} else if (role == ApplicationMenuServiceName) { |
|
return d->appMenuServiceName(window); |
|
} else if (role == CanLaunchNewInstance) { |
|
return canLauchNewInstance(d->appData(window)); |
|
} |
|
|
|
return QVariant(); |
|
} |
|
|
|
int XWindowTasksModel::rowCount(const QModelIndex &parent) const |
|
{ |
|
return parent.isValid() ? 0 : d->windows.count(); |
|
} |
|
|
|
void XWindowTasksModel::requestActivate(const QModelIndex &index) |
|
{ |
|
if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { |
|
return; |
|
} |
|
|
|
if (index.row() >= 0 && index.row() < d->windows.count()) { |
|
WId window = d->windows.at(index.row()); |
|
|
|
// Pull forward any transient demanding attention. |
|
if (d->transientsDemandingAttention.contains(window)) { |
|
window = d->transientsDemandingAttention.value(window); |
|
// Quote from legacy libtaskmanager: |
|
// "this is a work around for (at least?) kwin where a shaded transient will prevent the main |
|
// window from being brought forward unless the transient is actually pulled forward, most |
|
// easily reproduced by opening a modal file open/save dialog on an app then shading the file |
|
// dialog and trying to bring the window forward by clicking on it in a tasks widget |
|
// TODO: do we need to check all the transients for shaded?" |
|
} else if (!d->transients.isEmpty()) { |
|
const auto transients = d->transients.keys(window); |
|
for (const auto transient : qAsConst(transients)) { |
|
KWindowInfo info(transient, NET::WMState, NET::WM2TransientFor); |
|
if (info.valid(true) && info.hasState(NET::Shaded)) { |
|
window = transient; |
|
break; |
|
} |
|
} |
|
} |
|
|
|
KWindowSystem::forceActiveWindow(window); |
|
} |
|
} |
|
|
|
void XWindowTasksModel::requestNewInstance(const QModelIndex &index) |
|
{ |
|
if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { |
|
return; |
|
} |
|
|
|
runApp(d->appData(d->windows.at(index.row()))); |
|
} |
|
|
|
void XWindowTasksModel::requestOpenUrls(const QModelIndex &index, const QList<QUrl> &urls) |
|
{ |
|
if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count() || urls.isEmpty()) { |
|
return; |
|
} |
|
|
|
runApp(d->appData(d->windows.at(index.row())), urls); |
|
} |
|
|
|
void XWindowTasksModel::requestClose(const QModelIndex &index) |
|
{ |
|
if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { |
|
return; |
|
} |
|
|
|
NETRootInfo ri(QX11Info::connection(), NET::CloseWindow); |
|
ri.closeWindowRequest(d->windows.at(index.row())); |
|
} |
|
|
|
void XWindowTasksModel::requestMove(const QModelIndex &index) |
|
{ |
|
if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { |
|
return; |
|
} |
|
|
|
const WId window = d->windows.at(index.row()); |
|
const KWindowInfo *info = d->windowInfo(window); |
|
|
|
bool onCurrent = info->isOnCurrentDesktop(); |
|
|
|
if (!onCurrent) { |
|
KWindowSystem::setCurrentDesktop(info->desktop()); |
|
KWindowSystem::forceActiveWindow(window); |
|
} |
|
|
|
if (info->isMinimized()) { |
|
KWindowSystem::unminimizeWindow(window); |
|
} |
|
|
|
const QRect &geom = info->geometry(); |
|
|
|
NETRootInfo ri(QX11Info::connection(), NET::WMMoveResize); |
|
ri.moveResizeRequest(window, geom.center().x(), geom.center().y(), NET::Move); |
|
} |
|
|
|
void XWindowTasksModel::requestResize(const QModelIndex &index) |
|
{ |
|
if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { |
|
return; |
|
} |
|
|
|
const WId window = d->windows.at(index.row()); |
|
const KWindowInfo *info = d->windowInfo(window); |
|
|
|
bool onCurrent = info->isOnCurrentDesktop(); |
|
|
|
if (!onCurrent) { |
|
KWindowSystem::setCurrentDesktop(info->desktop()); |
|
KWindowSystem::forceActiveWindow(window); |
|
} |
|
|
|
if (info->isMinimized()) { |
|
KWindowSystem::unminimizeWindow(window); |
|
} |
|
|
|
const QRect &geom = info->geometry(); |
|
|
|
NETRootInfo ri(QX11Info::connection(), NET::WMMoveResize); |
|
ri.moveResizeRequest(window, geom.bottomRight().x(), geom.bottomRight().y(), NET::BottomRight); |
|
} |
|
|
|
void XWindowTasksModel::requestToggleMinimized(const QModelIndex &index) |
|
{ |
|
if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { |
|
return; |
|
} |
|
|
|
const WId window = d->windows.at(index.row()); |
|
const KWindowInfo *info = d->windowInfo(window); |
|
|
|
if (index.data(AbstractTasksModel::IsHidden).toBool()) { |
|
bool onCurrent = info->isOnCurrentDesktop(); |
|
|
|
// FIXME: Move logic up into proxy? (See also others.) |
|
if (!onCurrent) { |
|
KWindowSystem::setCurrentDesktop(info->desktop()); |
|
} |
|
|
|
KWindowSystem::unminimizeWindow(window); |
|
|
|
if (onCurrent) { |
|
KWindowSystem::forceActiveWindow(window); |
|
} |
|
} else { |
|
KWindowSystem::minimizeWindow(window); |
|
} |
|
} |
|
|
|
void XWindowTasksModel::requestToggleMaximized(const QModelIndex &index) |
|
{ |
|
if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { |
|
return; |
|
} |
|
|
|
const WId window = d->windows.at(index.row()); |
|
const KWindowInfo *info = d->windowInfo(window); |
|
bool onCurrent = info->isOnCurrentDesktop(); |
|
bool restore = (info->hasState(NET::MaxHoriz) && info->hasState(NET::MaxVert)); |
|
|
|
// FIXME: Move logic up into proxy? (See also others.) |
|
if (!onCurrent) { |
|
KWindowSystem::setCurrentDesktop(info->desktop()); |
|
} |
|
|
|
if (info->isMinimized()) { |
|
KWindowSystem::unminimizeWindow(window); |
|
} |
|
|
|
NETWinInfo ni(QX11Info::connection(), window, QX11Info::appRootWindow(), NET::WMState, NET::Properties2()); |
|
|
|
if (restore) { |
|
ni.setState(NET::States(), NET::Max); |
|
} else { |
|
ni.setState(NET::Max, NET::Max); |
|
} |
|
|
|
if (!onCurrent) { |
|
KWindowSystem::forceActiveWindow(window); |
|
} |
|
} |
|
|
|
void XWindowTasksModel::requestToggleKeepAbove(const QModelIndex &index) |
|
{ |
|
if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { |
|
return; |
|
} |
|
|
|
const WId window = d->windows.at(index.row()); |
|
const KWindowInfo *info = d->windowInfo(window); |
|
|
|
NETWinInfo ni(QX11Info::connection(), window, QX11Info::appRootWindow(), NET::WMState, NET::Properties2()); |
|
|
|
if (info->hasState(NET::KeepAbove)) { |
|
ni.setState(NET::States(), NET::KeepAbove); |
|
} else { |
|
ni.setState(NET::KeepAbove, NET::KeepAbove); |
|
} |
|
} |
|
|
|
void XWindowTasksModel::requestToggleKeepBelow(const QModelIndex &index) |
|
{ |
|
if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { |
|
return; |
|
} |
|
|
|
const WId window = d->windows.at(index.row()); |
|
const KWindowInfo *info = d->windowInfo(window); |
|
|
|
NETWinInfo ni(QX11Info::connection(), window, QX11Info::appRootWindow(), NET::WMState, NET::Properties2()); |
|
|
|
if (info->hasState(NET::KeepBelow)) { |
|
ni.setState(NET::States(), NET::KeepBelow); |
|
} else { |
|
ni.setState(NET::KeepBelow, NET::KeepBelow); |
|
} |
|
} |
|
|
|
void XWindowTasksModel::requestToggleFullScreen(const QModelIndex &index) |
|
{ |
|
if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { |
|
return; |
|
} |
|
|
|
const WId window = d->windows.at(index.row()); |
|
const KWindowInfo *info = d->windowInfo(window); |
|
|
|
NETWinInfo ni(QX11Info::connection(), window, QX11Info::appRootWindow(), NET::WMState, NET::Properties2()); |
|
|
|
if (info->hasState(NET::FullScreen)) { |
|
ni.setState(NET::States(), NET::FullScreen); |
|
} else { |
|
ni.setState(NET::FullScreen, NET::FullScreen); |
|
} |
|
} |
|
|
|
void XWindowTasksModel::requestToggleShaded(const QModelIndex &index) |
|
{ |
|
if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { |
|
return; |
|
} |
|
|
|
const WId window = d->windows.at(index.row()); |
|
const KWindowInfo *info = d->windowInfo(window); |
|
|
|
NETWinInfo ni(QX11Info::connection(), window, QX11Info::appRootWindow(), NET::WMState, NET::Properties2()); |
|
|
|
if (info->hasState(NET::Shaded)) { |
|
ni.setState(NET::States(), NET::Shaded); |
|
} else { |
|
ni.setState(NET::Shaded, NET::Shaded); |
|
} |
|
} |
|
|
|
void XWindowTasksModel::requestVirtualDesktops(const QModelIndex &index, const QVariantList &desktops) |
|
{ |
|
if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { |
|
return; |
|
} |
|
|
|
int desktop = 0; |
|
|
|
if (!desktops.isEmpty()) { |
|
bool ok = false; |
|
|
|
desktop = desktops.first().toUInt(&ok); |
|
|
|
if (!ok) { |
|
return; |
|
} |
|
} |
|
|
|
if (desktop > KWindowSystem::numberOfDesktops()) { |
|
return; |
|
} |
|
|
|
const WId window = d->windows.at(index.row()); |
|
const KWindowInfo *info = d->windowInfo(window); |
|
|
|
if (desktop == 0) { |
|
if (info->onAllDesktops()) { |
|
KWindowSystem::setOnDesktop(window, KWindowSystem::currentDesktop()); |
|
KWindowSystem::forceActiveWindow(window); |
|
} else { |
|
KWindowSystem::setOnAllDesktops(window, true); |
|
} |
|
|
|
return; |
|
} |
|
|
|
KWindowSystem::setOnDesktop(window, desktop); |
|
|
|
if (desktop == KWindowSystem::currentDesktop()) { |
|
KWindowSystem::forceActiveWindow(window); |
|
} |
|
} |
|
|
|
void XWindowTasksModel::requestNewVirtualDesktop(const QModelIndex &index) |
|
{ |
|
if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { |
|
return; |
|
} |
|
|
|
const WId window = d->windows.at(index.row()); |
|
const int desktop = KWindowSystem::numberOfDesktops() + 1; |
|
|
|
// FIXME Arbitrary limit of 20 copied from old code. |
|
if (desktop > 20) { |
|
return; |
|
} |
|
|
|
NETRootInfo ri(QX11Info::connection(), NET::NumberOfDesktops); |
|
ri.setNumberOfDesktops(desktop); |
|
|
|
KWindowSystem::setOnDesktop(window, desktop); |
|
} |
|
|
|
void XWindowTasksModel::requestActivities(const QModelIndex &index, const QStringList &activities) |
|
{ |
|
if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { |
|
return; |
|
} |
|
|
|
const WId window = d->windows.at(index.row()); |
|
|
|
KWindowSystem::setOnActivities(window, activities); |
|
} |
|
|
|
void XWindowTasksModel::requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate) |
|
{ |
|
Q_UNUSED(delegate) |
|
|
|
if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { |
|
return; |
|
} |
|
|
|
const WId window = d->windows.at(index.row()); |
|
|
|
if (d->delegateGeometries.contains(window) && d->delegateGeometries.value(window) == geometry) { |
|
return; |
|
} |
|
|
|
NETWinInfo ni(QX11Info::connection(), window, QX11Info::appRootWindow(), NET::Properties(), NET::Properties2()); |
|
NETRect rect; |
|
|
|
if (geometry.isValid()) { |
|
rect.pos.x = geometry.x(); |
|
rect.pos.y = geometry.y(); |
|
rect.size.width = geometry.width(); |
|
rect.size.height = geometry.height(); |
|
|
|
d->delegateGeometries.insert(window, geometry); |
|
} else { |
|
d->delegateGeometries.remove(window); |
|
} |
|
|
|
ni.setIconGeometry(rect); |
|
} |
|
|
|
WId XWindowTasksModel::winIdFromMimeData(const QMimeData *mimeData, bool *ok) |
|
{ |
|
Q_ASSERT(mimeData); |
|
|
|
if (ok) { |
|
*ok = false; |
|
} |
|
|
|
if (!mimeData->hasFormat(Private::mimeType())) { |
|
return 0; |
|
} |
|
|
|
QByteArray data(mimeData->data(Private::mimeType())); |
|
if (data.size() != sizeof(WId)) { |
|
return 0; |
|
} |
|
|
|
WId id; |
|
memcpy(&id, data.data(), sizeof(WId)); |
|
|
|
if (ok) { |
|
*ok = true; |
|
} |
|
|
|
return id; |
|
} |
|
|
|
QList<WId> XWindowTasksModel::winIdsFromMimeData(const QMimeData *mimeData, bool *ok) |
|
{ |
|
Q_ASSERT(mimeData); |
|
QList<WId> ids; |
|
|
|
if (ok) { |
|
*ok = false; |
|
} |
|
|
|
if (!mimeData->hasFormat(Private::groupMimeType())) { |
|
// Try to extract single window id. |
|
bool singularOk; |
|
WId id = winIdFromMimeData(mimeData, &singularOk); |
|
|
|
if (ok) { |
|
*ok = singularOk; |
|
} |
|
|
|
if (singularOk) { |
|
ids << id; |
|
} |
|
|
|
return ids; |
|
} |
|
|
|
QByteArray data(mimeData->data(Private::groupMimeType())); |
|
if ((unsigned int)data.size() < sizeof(int) + sizeof(WId)) { |
|
return ids; |
|
} |
|
|
|
int count = 0; |
|
memcpy(&count, data.data(), sizeof(int)); |
|
if (count < 1 || (unsigned int)data.size() < sizeof(int) + sizeof(WId) * count) { |
|
return ids; |
|
} |
|
|
|
WId id; |
|
for (int i = 0; i < count; ++i) { |
|
memcpy(&id, data.data() + sizeof(int) + sizeof(WId) * i, sizeof(WId)); |
|
ids << id; |
|
} |
|
|
|
if (ok) { |
|
*ok = true; |
|
} |
|
|
|
return ids; |
|
} |
|
|
|
}
|
|
|