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.
488 lines
21 KiB
488 lines
21 KiB
/*************************************************************************** |
|
* Copyright 2009 by Martin Gräßlin <kde@martin-graesslin.com> * |
|
* * |
|
* This program is free software; you can redistribute it and/or modify * |
|
* it under the terms of the GNU General Public License as published by * |
|
* the Free Software Foundation; either version 2 of the License, or * |
|
* (at your option) any later version. * |
|
* * |
|
* This program 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 General Public License for more details. * |
|
* * |
|
* You should have received a copy of the GNU General Public License * |
|
* along with this program; if not, write to the * |
|
* Free Software Foundation, Inc., * |
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * |
|
***************************************************************************/ |
|
#include "windowsrunner.h" |
|
|
|
#include "config-windowsrunner.h" |
|
|
|
#include <QTimer> |
|
|
|
#include <QMutex> |
|
#include <QMutexLocker> |
|
#include <QDebug> |
|
#include <QIcon> |
|
#include <QMutexLocker> |
|
#include <KWindowSystem> |
|
#include <KLocalizedString> |
|
|
|
#if HAVE_X11 |
|
#include <QX11Info> |
|
#include <netwm.h> |
|
#endif |
|
|
|
K_EXPORT_PLASMA_RUNNER(windows, WindowsRunner) |
|
|
|
WindowsRunner::WindowsRunner(QObject* parent, const QVariantList& args) |
|
: AbstractRunner(parent, args), |
|
m_inSession(false) |
|
{ |
|
Q_UNUSED(args) |
|
setObjectName( QLatin1String("Windows") ); |
|
|
|
addSyntax(Plasma::RunnerSyntax(QStringLiteral(":q:"), i18n("Finds windows whose name, window class or window role match :q:. " |
|
"It is possible to interact with the windows by using one of the following keywords: " |
|
"activate, close, min(imize), max(imize), fullscreen, shade, keep above and keep below."))); |
|
addSyntax(Plasma::RunnerSyntax(QStringLiteral(":q:"), i18n("Finds windows which are on desktop named :q: " |
|
"It is possible to interact with the windows by using one of the following keywords: " |
|
"activate, close, min(imize), max(imize), fullscreen, shade, keep above and keep below."))); |
|
addSyntax(Plasma::RunnerSyntax(QStringLiteral(":q:"), i18n("Switch to desktop named :q:"))); |
|
setDefaultSyntax(Plasma::RunnerSyntax(i18nc("Note this is a KRunner keyword", "window"), |
|
i18n("Lists all windows and allows to activate them. " |
|
"With name=, class=, role= and desktop= the list can be reduced to " |
|
"windows matching these restrictions. " |
|
"It is possible to interact with the windows by using one of the following keywords: " |
|
"activate, close, min(imize), max(imize), fullscreen, shade, keep above and keep below."))); |
|
addSyntax(Plasma::RunnerSyntax(i18nc("Note this is a KRunner keyword", "desktop"), |
|
i18n("Lists all other desktops and allows to switch to them."))); |
|
|
|
connect(this, &Plasma::AbstractRunner::prepare, this, &WindowsRunner::prepareForMatchSession); |
|
connect(this, &Plasma::AbstractRunner::teardown, this, &WindowsRunner::matchSessionComplete); |
|
} |
|
|
|
WindowsRunner::~WindowsRunner() |
|
{ |
|
} |
|
|
|
// Called in the main thread |
|
void WindowsRunner::gatherInfo() |
|
{ |
|
if (!m_inSession) { |
|
return; |
|
} |
|
|
|
const auto windows = KWindowSystem::windows(); |
|
for (const WId &w : windows) { |
|
KWindowInfo info(w, NET::WMWindowType | NET::WMDesktop | |
|
NET::WMState | NET::XAWMState | |
|
NET::WMName, |
|
NET::WM2WindowClass | NET::WM2WindowRole | NET::WM2AllowedActions); |
|
if (info.valid()) { |
|
// ignore NET::Tool and other special window types |
|
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); |
|
|
|
if (wType != NET::Normal && wType != NET::Override && wType != NET::Unknown && |
|
wType != NET::Dialog && wType != NET::Utility) { |
|
continue; |
|
} |
|
m_windows.insert(w, info); |
|
m_icons.insert(w, QIcon(KWindowSystem::icon(w))); |
|
} |
|
} |
|
|
|
for (int i=1; i<=KWindowSystem::numberOfDesktops(); i++) { |
|
m_desktopNames << KWindowSystem::desktopName(i); |
|
} |
|
|
|
// unlock lock locked in prepareForMatchSession |
|
m_mutex.unlock(); |
|
} |
|
|
|
// Called in the main thread |
|
void WindowsRunner::prepareForMatchSession() |
|
{ |
|
// gatherInfo will unlock the lock |
|
m_mutex.lock(); |
|
m_inSession = true; |
|
QTimer::singleShot(0, this, &WindowsRunner::gatherInfo); |
|
} |
|
|
|
// Called in the main thread |
|
void WindowsRunner::matchSessionComplete() |
|
{ |
|
QMutexLocker locker(&m_mutex); |
|
m_inSession = false; |
|
m_desktopNames.clear(); |
|
m_icons.clear(); |
|
m_windows.clear(); |
|
} |
|
|
|
// Called in the secondary thread |
|
void WindowsRunner::match(Plasma::RunnerContext& context) |
|
{ |
|
// will run block as long as gatherInfo has not finished |
|
QMutexLocker locker(&m_mutex); |
|
|
|
QString term = context.query(); |
|
|
|
if (!context.singleRunnerQueryMode() && (term.length() < 3)) { |
|
return; |
|
} |
|
QList<Plasma::QueryMatch> matches; |
|
|
|
// check if the search term ends with an action keyword |
|
WindowAction action = ActivateAction; |
|
if (term.endsWith(i18nc("Note this is a KRunner keyword", "activate") , Qt::CaseInsensitive)) { |
|
action = ActivateAction; |
|
term = term.left(term.lastIndexOf(i18nc("Note this is a KRunner keyword", "activate")) - 1); |
|
} else if (term.endsWith(i18nc("Note this is a KRunner keyword", "close") , Qt::CaseInsensitive)) { |
|
action = CloseAction; |
|
term = term.left(term.lastIndexOf(i18nc("Note this is a KRunner keyword", "close")) - 1); |
|
} else if (term.endsWith(i18nc("Note this is a KRunner keyword", "min") , Qt::CaseInsensitive)) { |
|
action = MinimizeAction; |
|
term = term.left(term.lastIndexOf(i18nc("Note this is a KRunner keyword", "min")) - 1); |
|
} else if (term.endsWith(i18nc("Note this is a KRunner keyword", "minimize") , Qt::CaseInsensitive)) { |
|
action = MinimizeAction; |
|
term = term.left(term.lastIndexOf(i18nc("Note this is a KRunner keyword", "minimize")) - 1); |
|
} else if (term.endsWith(i18nc("Note this is a KRunner keyword", "max") , Qt::CaseInsensitive)) { |
|
action = MaximizeAction; |
|
term = term.left(term.lastIndexOf(i18nc("Note this is a KRunner keyword", "max")) - 1); |
|
} else if (term.endsWith(i18nc("Note this is a KRunner keyword", "maximize") , Qt::CaseInsensitive)) { |
|
action = MaximizeAction; |
|
term = term.left(term.lastIndexOf(i18nc("Note this is a KRunner keyword", "maximize")) - 1); |
|
} else if (term.endsWith(i18nc("Note this is a KRunner keyword", "fullscreen") , Qt::CaseInsensitive)) { |
|
action = FullscreenAction; |
|
term = term.left(term.lastIndexOf(i18nc("Note this is a KRunner keyword", "fullscreen")) - 1); |
|
} else if (term.endsWith(i18nc("Note this is a KRunner keyword", "shade") , Qt::CaseInsensitive)) { |
|
action = ShadeAction; |
|
term = term.left(term.lastIndexOf(i18nc("Note this is a KRunner keyword", "shade")) - 1); |
|
} else if (term.endsWith(i18nc("Note this is a KRunner keyword", "keep above") , Qt::CaseInsensitive)) { |
|
action = KeepAboveAction; |
|
term = term.left(term.lastIndexOf(i18nc("Note this is a KRunner keyword", "keep above")) - 1); |
|
} else if (term.endsWith(i18nc("Note this is a KRunner keyword", "keep below") , Qt::CaseInsensitive)) { |
|
action = KeepBelowAction; |
|
term = term.left(term.lastIndexOf(i18nc("Note this is a KRunner keyword", "keep below")) - 1); |
|
} |
|
|
|
// keyword match: when term starts with "window" we list all windows |
|
// the list can be restricted to windows matching a given name, class, role or desktop |
|
if (term.startsWith(i18nc("Note this is a KRunner keyword", "window") , Qt::CaseInsensitive)) { |
|
const QStringList keywords = term.split(QLatin1Char(' ')); |
|
QString windowName; |
|
QString windowClass; |
|
QString windowRole; |
|
int desktop = -1; |
|
for (const QString& keyword : keywords) { |
|
if (keyword.endsWith(QLatin1Char('='))) { |
|
continue; |
|
} |
|
if (keyword.startsWith(i18nc("Note this is a KRunner keyword", "name") + QStringLiteral("=") , Qt::CaseInsensitive)) { |
|
windowName = keyword.split(QStringLiteral("="))[1]; |
|
} else if (keyword.startsWith(i18nc("Note this is a KRunner keyword", "class") + QStringLiteral("=") , Qt::CaseInsensitive)) { |
|
windowClass = keyword.split(QStringLiteral("="))[1]; |
|
} else if (keyword.startsWith(i18nc("Note this is a KRunner keyword", "role") + QStringLiteral("=") , Qt::CaseInsensitive)) { |
|
windowRole = keyword.split(QStringLiteral("="))[1]; |
|
} else if (keyword.startsWith(i18nc("Note this is a KRunner keyword", "desktop") + QStringLiteral("=") , Qt::CaseInsensitive)) { |
|
bool ok; |
|
desktop = keyword.split(QStringLiteral("="))[1].toInt(&ok); |
|
if (!ok || desktop > KWindowSystem::numberOfDesktops()) { |
|
desktop = -1; // sanity check |
|
} |
|
} else { |
|
// not a keyword - use as name if name is unused, but another option is set |
|
if (windowName.isEmpty() && !keyword.contains(QLatin1Char('=')) && |
|
(!windowRole.isEmpty() || !windowClass.isEmpty() || desktop != -1)) { |
|
windowName = keyword; |
|
} |
|
} |
|
} |
|
QHashIterator<WId, KWindowInfo> it(m_windows); |
|
while(it.hasNext()) { |
|
it.next(); |
|
WId w = it.key(); |
|
KWindowInfo info = it.value(); |
|
QString windowClassCompare = QString::fromUtf8(info.windowClassName()) + QLatin1Char(' ') + |
|
QString::fromUtf8(info.windowClassClass()); |
|
// exclude not matching windows |
|
if (!KWindowSystem::hasWId(w)) { |
|
continue; |
|
} |
|
if (!windowName.isEmpty() && !info.name().startsWith(windowName, Qt::CaseInsensitive)) { |
|
continue; |
|
} |
|
if (!windowClass.isEmpty() && !windowClassCompare.contains(windowClass, Qt::CaseInsensitive)) { |
|
continue; |
|
} |
|
if (!windowRole.isEmpty() && !QString::fromUtf8(info.windowRole()).contains(windowRole, Qt::CaseInsensitive)) { |
|
continue; |
|
} |
|
if (desktop != -1 && !info.isOnDesktop(desktop)) { |
|
continue; |
|
} |
|
// check for windows when no keywords were used |
|
// check the name, class and role for containing the query without the keyword |
|
if (windowName.isEmpty() && windowClass.isEmpty() && windowRole.isEmpty() && desktop == -1) { |
|
const QString& test = term.mid(keywords[0].length() + 1); |
|
if (!info.name().contains(test, Qt::CaseInsensitive) && |
|
!windowClassCompare.contains(test, Qt::CaseInsensitive) && |
|
!QString::fromUtf8(info.windowRole()).contains(test, Qt::CaseInsensitive)) { |
|
continue; |
|
} |
|
} |
|
// blacklisted everything else: we have a match |
|
if (actionSupported(info, action)){ |
|
matches << windowMatch(info, action); |
|
} |
|
} |
|
|
|
if (!matches.isEmpty()) { |
|
// the window keyword found matches - do not process other syntax possibilities |
|
context.addMatches(matches); |
|
return; |
|
} |
|
} |
|
|
|
bool desktopAdded = false; |
|
// check for desktop keyword |
|
if (term.startsWith(i18nc("Note this is a KRunner keyword", "desktop") , Qt::CaseInsensitive)) { |
|
const QStringList parts = term.split(QLatin1Char(' ')); |
|
if (parts.size() == 1) { |
|
// only keyword - list all desktops |
|
for (int i=1; i<=KWindowSystem::numberOfDesktops(); i++) { |
|
if (i == KWindowSystem::currentDesktop()) { |
|
continue; |
|
} |
|
matches << desktopMatch(i); |
|
desktopAdded = true; |
|
} |
|
} else { |
|
// keyword + desktop - restrict matches |
|
bool isInt; |
|
int desktop = term.midRef(parts[0].length() + 1).toInt(&isInt); |
|
if (isInt && desktop != KWindowSystem::currentDesktop()) { |
|
matches << desktopMatch(desktop); |
|
desktopAdded = true; |
|
} |
|
} |
|
} |
|
|
|
// check for matches without keywords |
|
QHashIterator<WId, KWindowInfo> it(m_windows); |
|
while (it.hasNext()) { |
|
it.next(); |
|
WId w = it.key(); |
|
if (!KWindowSystem::hasWId(w)) { |
|
continue; |
|
} |
|
// check if window name, class or role contains the query |
|
KWindowInfo info = it.value(); |
|
QString className = QString::fromUtf8(info.windowClassName()); |
|
if (info.name().startsWith(term, Qt::CaseInsensitive) || |
|
className.startsWith(term, Qt::CaseInsensitive)) { |
|
matches << windowMatch(info, action, 0.8, Plasma::QueryMatch::ExactMatch); |
|
} else if ((info.name().contains(term, Qt::CaseInsensitive) || |
|
className.contains(term, Qt::CaseInsensitive)) && |
|
actionSupported(info, action)) { |
|
matches << windowMatch(info, action, 0.7, Plasma::QueryMatch::PossibleMatch); |
|
} |
|
} |
|
|
|
// check for matching desktops by name |
|
for (const QString& desktopName : qAsConst(m_desktopNames)) { |
|
int desktop = m_desktopNames.indexOf(desktopName) +1; |
|
if (desktopName.contains(term, Qt::CaseInsensitive)) { |
|
// desktop name matches - offer switch to |
|
// only add desktops if it hasn't been added by the keyword which is quite likely |
|
if (!desktopAdded && desktop != KWindowSystem::currentDesktop()) { |
|
matches << desktopMatch(desktop, 0.8); |
|
} |
|
|
|
// search for windows on desktop and list them with less relevance |
|
QHashIterator<WId, KWindowInfo> it(m_windows); |
|
while (it.hasNext()) { |
|
it.next(); |
|
KWindowInfo info = it.value(); |
|
if (info.isOnDesktop(desktop) && actionSupported(info, action)) { |
|
matches << windowMatch(info, action, 0.5, Plasma::QueryMatch::PossibleMatch); |
|
} |
|
} |
|
} |
|
} |
|
|
|
if (!matches.isEmpty()) { |
|
context.addMatches(matches); |
|
} |
|
} |
|
|
|
// Called in the main thread |
|
void WindowsRunner::run(const Plasma::RunnerContext& context, const Plasma::QueryMatch& match) |
|
{ |
|
QMutexLocker locker(&m_mutex); |
|
Q_UNUSED(context) |
|
// check if it's a desktop |
|
if (match.id().startsWith(QLatin1String("windows_desktop"))) { |
|
KWindowSystem::setCurrentDesktop(match.data().toInt()); |
|
return; |
|
} |
|
|
|
const QStringList parts = match.data().toString().split(QLatin1Char('_')); |
|
WindowAction action = WindowAction(parts[0].toInt()); |
|
WId w(parts[1].toULong()); |
|
|
|
KWindowInfo info(w, NET::WMWindowType | NET::WMDesktop | NET::WMState | NET::XAWMState | NET::WMName, |
|
NET::WM2WindowClass | NET::WM2WindowRole | NET::WM2AllowedActions); |
|
if (!info.valid()) { |
|
return; |
|
} |
|
|
|
switch (action) { |
|
case ActivateAction: |
|
KWindowSystem::forceActiveWindow(w); |
|
break; |
|
case CloseAction: |
|
{ |
|
NETRootInfo ri(QX11Info::connection(), NET::CloseWindow); |
|
ri.closeWindowRequest(w); |
|
break; |
|
} |
|
case MinimizeAction: |
|
if (info.isMinimized()) { |
|
KWindowSystem::unminimizeWindow(w); |
|
} else { |
|
KWindowSystem::minimizeWindow(w); |
|
} |
|
break; |
|
case MaximizeAction: |
|
if (info.hasState(NET::Max)) { |
|
KWindowSystem::clearState(w, NET::Max); |
|
} else { |
|
KWindowSystem::setState(w, NET::Max); |
|
} |
|
break; |
|
case FullscreenAction: |
|
if (info.hasState(NET::FullScreen)) { |
|
KWindowSystem::clearState(w, NET::FullScreen); |
|
} else { |
|
KWindowSystem::setState(w, NET::FullScreen); |
|
} |
|
break; |
|
case ShadeAction: |
|
if (info.hasState(NET::Shaded)) { |
|
KWindowSystem::clearState(w, NET::Shaded); |
|
} else { |
|
KWindowSystem::setState(w, NET::Shaded); |
|
} |
|
break; |
|
case KeepAboveAction: |
|
if (info.hasState(NET::KeepAbove)) { |
|
KWindowSystem::clearState(w, NET::KeepAbove); |
|
} else { |
|
KWindowSystem::setState(w, NET::KeepAbove); |
|
} |
|
break; |
|
case KeepBelowAction: |
|
if (info.hasState(NET::KeepBelow)) { |
|
KWindowSystem::clearState(w, NET::KeepBelow); |
|
} else { |
|
KWindowSystem::setState(w, NET::KeepBelow); |
|
} |
|
break; |
|
} |
|
} |
|
|
|
Plasma::QueryMatch WindowsRunner::desktopMatch(int desktop, qreal relevance) |
|
{ |
|
Plasma::QueryMatch match(this); |
|
match.setType(Plasma::QueryMatch::ExactMatch); |
|
match.setData(desktop); |
|
match.setId(QStringLiteral("desktop-") + QString::number(desktop)); |
|
match.setIconName(QStringLiteral("user-desktop")); |
|
QString desktopName; |
|
if (desktop <= m_desktopNames.size()) { |
|
desktopName = m_desktopNames[desktop - 1]; |
|
} else { |
|
desktopName = KWindowSystem::desktopName(desktop); |
|
} |
|
match.setText(desktopName); |
|
match.setSubtext(i18n("Switch to desktop %1", desktop)); |
|
match.setRelevance(relevance); |
|
return match; |
|
} |
|
|
|
Plasma::QueryMatch WindowsRunner::windowMatch(const KWindowInfo& info, WindowAction action, qreal relevance, Plasma::QueryMatch::Type type) |
|
{ |
|
Plasma::QueryMatch match(this); |
|
match.setType(type); |
|
match.setData(QString(QString::number((int)action) + QLatin1Char('_') + QString::number(info.win()))); |
|
match.setIcon(m_icons[info.win()]); |
|
match.setText(info.name()); |
|
QString desktopName; |
|
int desktop = info.desktop(); |
|
if (desktop == NET::OnAllDesktops) { |
|
desktop = KWindowSystem::currentDesktop(); |
|
} |
|
if (desktop <= m_desktopNames.size()) { |
|
desktopName = m_desktopNames[desktop - 1]; |
|
} else { |
|
desktopName = KWindowSystem::desktopName(desktop); |
|
} |
|
switch (action) { |
|
case CloseAction: |
|
match.setSubtext(i18n("Close running window on %1", desktopName)); |
|
break; |
|
case MinimizeAction: |
|
match.setSubtext(i18n("(Un)minimize running window on %1", desktopName)); |
|
break; |
|
case MaximizeAction: |
|
match.setSubtext(i18n("Maximize/restore running window on %1", desktopName)); |
|
break; |
|
case FullscreenAction: |
|
match.setSubtext(i18n("Toggle fullscreen for running window on %1", desktopName)); |
|
break; |
|
case ShadeAction: |
|
match.setSubtext(i18n("(Un)shade running window on %1", desktopName)); |
|
break; |
|
case KeepAboveAction: |
|
match.setSubtext(i18n("Toggle keep above for running window on %1", desktopName)); |
|
break; |
|
case KeepBelowAction: |
|
match.setSubtext(i18n("Toggle keep below running window on %1", desktopName)); |
|
break; |
|
case ActivateAction: |
|
default: |
|
match.setSubtext(i18n("Activate running window on %1", desktopName)); |
|
break; |
|
} |
|
match.setRelevance(relevance); |
|
return match; |
|
} |
|
|
|
bool WindowsRunner::actionSupported(const KWindowInfo& info, WindowAction action) |
|
{ |
|
switch (action) { |
|
case CloseAction: |
|
return info.actionSupported(NET::ActionClose); |
|
case MinimizeAction: |
|
return info.actionSupported(NET::ActionMinimize); |
|
case MaximizeAction: |
|
return info.actionSupported(NET::ActionMax); |
|
case ShadeAction: |
|
return info.actionSupported(NET::ActionShade); |
|
case FullscreenAction: |
|
return info.actionSupported(NET::ActionFullScreen); |
|
case KeepAboveAction: |
|
case KeepBelowAction: |
|
case ActivateAction: |
|
default: |
|
return true; |
|
} |
|
} |
|
|
|
#include "windowsrunner.moc"
|
|
|