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.
449 lines
13 KiB
449 lines
13 KiB
/* |
|
SPDX-FileCopyrightText: 2014, 2015 Ivan Cukic <ivan.cukic(at)kde.org> |
|
SPDX-FileCopyrightText: 2009 Martin Gräßlin <[email protected]> |
|
SPDX-FileCopyrightText: 2003 Lubos Lunak <[email protected]> |
|
SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich <[email protected]> |
|
|
|
SPDX-License-Identifier: GPL-2.0-or-later |
|
*/ |
|
|
|
// Self |
|
#include "switcherbackend.h" |
|
|
|
// Qt |
|
#include <QAction> |
|
#include <QDBusConnection> |
|
#include <QDBusMessage> |
|
#include <QDateTime> |
|
#include <QGuiApplication> |
|
#include <QRasterWindow> |
|
|
|
// Qml and QtQuick |
|
#include <QQmlEngine> |
|
#include <QQuickImageProvider> |
|
|
|
// KDE |
|
#include <KConfig> |
|
#include <KConfigGroup> |
|
#include <KGlobalAccel> |
|
#include <KIO/PreviewJob> |
|
#include <KLocalizedString> |
|
#include <KWindowSystem> |
|
#include <windowtasksmodel.h> |
|
#include <xwindowtasksmodel.h> |
|
|
|
static const char *s_action_name_next_activity = "next activity"; |
|
static const char *s_action_name_previous_activity = "previous activity"; |
|
|
|
namespace |
|
{ |
|
bool areModifiersPressed(const QKeySequence &seq) |
|
{ |
|
if (seq.isEmpty()) { |
|
return false; |
|
} |
|
int mod = seq[seq.count() - 1] & Qt::KeyboardModifierMask; |
|
auto activeMods = qGuiApp->queryKeyboardModifiers(); |
|
return activeMods & mod; |
|
} |
|
|
|
bool isReverseTab(const QKeySequence &prevAction) |
|
{ |
|
if (prevAction == QKeySequence(Qt::ShiftModifier | Qt::Key_Tab)) { |
|
return areModifiersPressed(Qt::SHIFT); |
|
} else { |
|
return false; |
|
} |
|
} |
|
|
|
class ThumbnailImageResponse : public QQuickImageResponse |
|
{ |
|
public: |
|
ThumbnailImageResponse(const QString &id, const QSize &requestedSize); |
|
|
|
QQuickTextureFactory *textureFactory() const override; |
|
|
|
void run(); |
|
|
|
private: |
|
QString m_id; |
|
QSize m_requestedSize; |
|
QQuickTextureFactory *m_texture = nullptr; |
|
}; |
|
|
|
ThumbnailImageResponse::ThumbnailImageResponse(const QString &id, const QSize &requestedSize) |
|
: m_id(id) |
|
, m_requestedSize(requestedSize) |
|
, m_texture(nullptr) |
|
{ |
|
int width = m_requestedSize.width(); |
|
int height = m_requestedSize.height(); |
|
|
|
if (width <= 0) { |
|
width = 320; |
|
} |
|
|
|
if (height <= 0) { |
|
height = 240; |
|
} |
|
|
|
if (m_id.isEmpty()) { |
|
Q_EMIT finished(); |
|
return; |
|
} |
|
|
|
const auto file = QUrl::fromUserInput(m_id); |
|
|
|
KFileItemList list; |
|
list.append(KFileItem(file, QString(), 0)); |
|
|
|
auto job = KIO::filePreview(list, QSize(width, height)); |
|
job->setScaleType(KIO::PreviewJob::Scaled); |
|
job->setIgnoreMaximumSize(true); |
|
|
|
connect( |
|
job, |
|
&KIO::PreviewJob::gotPreview, |
|
this, |
|
[this, file](const KFileItem &item, const QPixmap &pixmap) { |
|
Q_UNUSED(item); |
|
|
|
auto image = pixmap.toImage(); |
|
|
|
m_texture = QQuickTextureFactory::textureFactoryForImage(image); |
|
Q_EMIT finished(); |
|
}, |
|
Qt::QueuedConnection); |
|
|
|
connect(job, &KIO::PreviewJob::failed, this, [this, job](const KFileItem &item) { |
|
Q_UNUSED(item); |
|
qWarning() << "SwitcherBackend: FAILED to get the thumbnail" << job->errorString() << job->detailedErrorStrings(); |
|
Q_EMIT finished(); |
|
}); |
|
} |
|
|
|
QQuickTextureFactory *ThumbnailImageResponse::textureFactory() const |
|
{ |
|
return m_texture; |
|
} |
|
|
|
class ThumbnailImageProvider : public QQuickAsyncImageProvider |
|
{ |
|
public: |
|
QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override |
|
{ |
|
return new ThumbnailImageResponse(id, requestedSize); |
|
} |
|
}; |
|
|
|
} // local namespace |
|
|
|
template<typename Handler> |
|
inline void SwitcherBackend::registerShortcut(const QString &actionName, const QString &text, const QKeySequence &shortcut, Handler &&handler) |
|
{ |
|
auto action = new QAction(this); |
|
|
|
m_actionShortcut[actionName] = shortcut; |
|
|
|
action->setObjectName(actionName); |
|
action->setText(text); |
|
|
|
KGlobalAccel::self()->setShortcut(action, {shortcut}); |
|
|
|
using KActivities::Controller; |
|
|
|
connect(action, &QAction::triggered, this, std::forward<Handler>(handler)); |
|
} |
|
|
|
SwitcherBackend::SwitcherBackend(QObject *parent) |
|
: QObject(parent) |
|
, m_shouldShowSwitcher(false) |
|
, m_dropModeActive(false) |
|
, m_runningActivitiesModel(new SortedActivitiesModel({KActivities::Info::Running, KActivities::Info::Stopping}, this)) |
|
, m_stoppedActivitiesModel(new SortedActivitiesModel({KActivities::Info::Stopped, KActivities::Info::Starting}, this)) |
|
{ |
|
registerShortcut(QString::fromLatin1(s_action_name_next_activity), |
|
i18n("Walk through activities"), |
|
Qt::META | Qt::Key_Tab, |
|
&SwitcherBackend::keybdSwitchToNextActivity); |
|
|
|
registerShortcut(QString::fromLatin1(s_action_name_previous_activity), |
|
i18n("Walk through activities (Reverse)"), |
|
Qt::META | Qt::SHIFT | Qt::Key_Tab, |
|
&SwitcherBackend::keybdSwitchToPreviousActivity); |
|
|
|
connect(this, &SwitcherBackend::shouldShowSwitcherChanged, m_runningActivitiesModel, &SortedActivitiesModel::setInhibitUpdates); |
|
|
|
m_modKeyPollingTimer.setInterval(100); |
|
connect(&m_modKeyPollingTimer, &QTimer::timeout, this, &SwitcherBackend::showActivitySwitcherIfNeeded); |
|
|
|
m_dropModeHider.setInterval(500); |
|
m_dropModeHider.setSingleShot(true); |
|
connect(&m_dropModeHider, &QTimer::timeout, this, [this] { |
|
setShouldShowSwitcher(false); |
|
}); |
|
|
|
connect(&m_activities, &KActivities::Controller::currentActivityChanged, this, &SwitcherBackend::onCurrentActivityChanged); |
|
m_previousActivity = m_activities.currentActivity(); |
|
} |
|
|
|
SwitcherBackend::~SwitcherBackend() |
|
{ |
|
} |
|
|
|
QObject *SwitcherBackend::instance(QQmlEngine *engine, QJSEngine *scriptEngine) |
|
{ |
|
Q_UNUSED(scriptEngine) |
|
engine->addImageProvider(QStringLiteral("wallpaperthumbnail"), new ThumbnailImageProvider()); |
|
return new SwitcherBackend(); |
|
} |
|
|
|
void SwitcherBackend::keybdSwitchToNextActivity() |
|
{ |
|
if (isReverseTab(m_actionShortcut[QString::fromLatin1(s_action_name_previous_activity)])) { |
|
switchToActivity(Previous); |
|
} else { |
|
switchToActivity(Next); |
|
} |
|
} |
|
|
|
void SwitcherBackend::keybdSwitchToPreviousActivity() |
|
{ |
|
switchToActivity(Previous); |
|
} |
|
|
|
void SwitcherBackend::switchToActivity(Direction direction) |
|
{ |
|
const auto activityToSet = m_runningActivitiesModel->relativeActivity(direction == Next ? 1 : -1); |
|
|
|
if (activityToSet.isEmpty()) |
|
return; |
|
|
|
QTimer::singleShot(0, this, [this, activityToSet]() { |
|
setCurrentActivity(activityToSet); |
|
}); |
|
|
|
keybdSwitchedToAnotherActivity(); |
|
} |
|
|
|
void SwitcherBackend::keybdSwitchedToAnotherActivity() |
|
{ |
|
m_lastInvokedAction = dynamic_cast<QAction *>(sender()); |
|
if (KWindowSystem::isPlatformWayland() && !qGuiApp->focusWindow() && !m_inputWindow) { |
|
// create a new Window so the compositor sends us modifier info |
|
m_inputWindow = new QRasterWindow(); |
|
m_inputWindow->setGeometry(0, 0, 1, 1); |
|
// Only show once the initial switch has been completed, not cause a switch back |
|
connect(&m_activities, &KActivities::Consumer::currentActivityChanged, m_inputWindow, [this] { |
|
m_inputWindow->show(); |
|
m_inputWindow->update(); |
|
}); |
|
connect(m_inputWindow, &QWindow::activeChanged, this, [this] { |
|
showActivitySwitcherIfNeeded(); |
|
}); |
|
} else { |
|
QTimer::singleShot(100, this, &SwitcherBackend::showActivitySwitcherIfNeeded); |
|
} |
|
} |
|
|
|
void SwitcherBackend::showActivitySwitcherIfNeeded() |
|
{ |
|
if (!m_lastInvokedAction || m_dropModeActive) { |
|
return; |
|
} |
|
|
|
auto actionName = m_lastInvokedAction->objectName(); |
|
|
|
if (!m_actionShortcut.contains(actionName)) { |
|
return; |
|
} |
|
|
|
if (!areModifiersPressed(m_actionShortcut[actionName])) { |
|
m_lastInvokedAction = nullptr; |
|
setShouldShowSwitcher(false); |
|
return; |
|
} |
|
|
|
setShouldShowSwitcher(true); |
|
} |
|
|
|
void SwitcherBackend::init() |
|
{ |
|
// nothing |
|
} |
|
|
|
void SwitcherBackend::onCurrentActivityChanged(const QString &id) |
|
{ |
|
if (m_shouldShowSwitcher) { |
|
// If we are showing the switcher because the user is |
|
// pressing Meta+Tab, we are not ready to commit the |
|
// activity change to memory |
|
return; |
|
} |
|
|
|
if (m_previousActivity == id) |
|
return; |
|
|
|
// Safe, we have a long-lived Consumer object |
|
KActivities::Info activity(id); |
|
Q_EMIT showSwitchNotification(id, activity.name(), activity.icon()); |
|
|
|
KConfig config(QStringLiteral("kactivitymanagerd-switcher")); |
|
KConfigGroup times(&config, "LastUsed"); |
|
|
|
const auto now = QDateTime::currentDateTime().toTime_t(); |
|
|
|
// Updating the time for the activity we just switched to |
|
// in the case we do not power off properly, and on the next |
|
// start, kamd switches to another activity for some reason |
|
times.writeEntry(id, now); |
|
|
|
if (!m_previousActivity.isEmpty()) { |
|
// When leaving an activity, say goodbye and fondly remember |
|
// the last time we saw it |
|
times.writeEntry(m_previousActivity, now); |
|
} |
|
|
|
times.sync(); |
|
|
|
m_previousActivity = id; |
|
} |
|
|
|
bool SwitcherBackend::shouldShowSwitcher() const |
|
{ |
|
return m_shouldShowSwitcher; |
|
} |
|
|
|
void SwitcherBackend::setShouldShowSwitcher(bool shouldShowSwitcher) |
|
{ |
|
if (m_inputWindow) { |
|
delete m_inputWindow; |
|
m_inputWindow = nullptr; |
|
} |
|
|
|
if (m_shouldShowSwitcher == shouldShowSwitcher) |
|
return; |
|
|
|
m_shouldShowSwitcher = shouldShowSwitcher; |
|
|
|
if (m_shouldShowSwitcher) { |
|
// TODO: We really should NOT do this by polling |
|
m_modKeyPollingTimer.start(); |
|
} else { |
|
m_modKeyPollingTimer.stop(); |
|
|
|
// We might have an unprocessed onCurrentActivityChanged |
|
onCurrentActivityChanged(m_activities.currentActivity()); |
|
} |
|
|
|
Q_EMIT shouldShowSwitcherChanged(m_shouldShowSwitcher); |
|
} |
|
|
|
QAbstractItemModel *SwitcherBackend::runningActivitiesModel() const |
|
{ |
|
return m_runningActivitiesModel; |
|
} |
|
|
|
QAbstractItemModel *SwitcherBackend::stoppedActivitiesModel() const |
|
{ |
|
return m_stoppedActivitiesModel; |
|
} |
|
|
|
void SwitcherBackend::setCurrentActivity(const QString &activity) |
|
{ |
|
m_activities.setCurrentActivity(activity); |
|
} |
|
|
|
void SwitcherBackend::stopActivity(const QString &activity) |
|
{ |
|
m_activities.stopActivity(activity); |
|
} |
|
|
|
bool SwitcherBackend::dropEnabled() const |
|
{ |
|
#if HAVE_X11 |
|
return true; |
|
#else |
|
return false; |
|
#endif |
|
} |
|
|
|
void SwitcherBackend::dropCopy(QMimeData *mimeData, const QVariant &activityId) |
|
{ |
|
drop(mimeData, Qt::ControlModifier, activityId); |
|
} |
|
|
|
void SwitcherBackend::dropMove(QMimeData *mimeData, const QVariant &activityId) |
|
{ |
|
drop(mimeData, 0, activityId); |
|
} |
|
|
|
void SwitcherBackend::drop(QMimeData *mimeData, int modifiers, const QVariant &activityId) |
|
{ |
|
setDropMode(false); |
|
|
|
#if HAVE_X11 |
|
if (KWindowSystem::isPlatformX11()) { |
|
bool ok = false; |
|
const QList<WId> &ids = TaskManager::XWindowTasksModel::winIdsFromMimeData(mimeData, &ok); |
|
|
|
if (!ok) { |
|
return; |
|
} |
|
|
|
const QString newActivity = activityId.toString(); |
|
const QStringList runningActivities = m_activities.runningActivities(); |
|
|
|
if (!runningActivities.contains(newActivity)) { |
|
return; |
|
} |
|
|
|
for (const auto &id : ids) { |
|
QStringList activities = KWindowInfo(id, NET::Properties(), NET::WM2Activities).activities(); |
|
|
|
if (modifiers & Qt::ControlModifier) { |
|
// Add to the activity instead of moving. |
|
// This is a hack because the task manager reports that |
|
// is supports only the 'Move' DND action. |
|
if (!activities.contains(newActivity)) { |
|
activities << newActivity; |
|
} |
|
|
|
} else { |
|
// Move to this activity |
|
// if on only one activity, set it to only the new activity |
|
// if on >1 activity, remove it from the current activity and add it to the new activity |
|
|
|
const QString currentActivity = m_activities.currentActivity(); |
|
activities.removeAll(currentActivity); |
|
activities << newActivity; |
|
} |
|
|
|
KWindowSystem::setOnActivities(id, activities); |
|
} |
|
} |
|
#endif |
|
} |
|
|
|
void SwitcherBackend::setDropMode(bool value) |
|
{ |
|
if (m_dropModeActive == value) |
|
return; |
|
|
|
m_dropModeActive = value; |
|
if (value) { |
|
setShouldShowSwitcher(true); |
|
m_dropModeHider.stop(); |
|
} else { |
|
m_dropModeHider.start(); |
|
} |
|
} |
|
|
|
void SwitcherBackend::toggleActivityManager() |
|
{ |
|
auto message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.plasmashell"), |
|
QStringLiteral("/PlasmaShell"), |
|
QStringLiteral("org.kde.PlasmaShell"), |
|
QStringLiteral("toggleActivityManager")); |
|
QDBusConnection::sessionBus().call(message, QDBus::NoBlock); |
|
}
|
|
|