mirror of
https://invent.kde.org/plasma/plasma-desktop.git
synced 2026-11-03 22:59:45 -09:00
This skips the conversion of the thumbnail to a QPixmap. It consumes the QImage directly.
410 lines
11 KiB
C++
410 lines
11 KiB
C++
/*
|
|
SPDX-FileCopyrightText: 2014, 2015 Ivan Cukic <ivan.cukic(at)kde.org>
|
|
SPDX-FileCopyrightText: 2009 Martin Gräßlin <mgraesslin@kde.org>
|
|
SPDX-FileCopyrightText: 2003 Lubos Lunak <l.lunak@kde.org>
|
|
SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich <ettrich@kde.org>
|
|
|
|
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 <KWindowInfo>
|
|
#include <KWindowSystem>
|
|
#include <waylandtasksmodel.h>
|
|
#include <windowtasksmodel.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, const KModifierKeyInfo &modifierInfo)
|
|
{
|
|
if (seq.isEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
auto mod = seq[seq.count() - 1].keyboardModifiers();
|
|
|
|
if (mod & Qt::ShiftModifier && modifierInfo.isKeyPressed(Qt::Key_Shift)) {
|
|
return true;
|
|
}
|
|
if (mod & Qt::ControlModifier && modifierInfo.isKeyPressed(Qt::Key_Control)) {
|
|
return true;
|
|
}
|
|
if (mod & Qt::AltModifier && modifierInfo.isKeyPressed(Qt::Key_Alt)) {
|
|
return true;
|
|
}
|
|
if (mod & Qt::MetaModifier && (modifierInfo.isKeyPressed(Qt::Key_Meta))) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool isReverseTab(const QKeySequence &prevAction, const KModifierKeyInfo &modifierInfo)
|
|
{
|
|
if (prevAction == QKeySequence(Qt::ShiftModifier | Qt::Key_Tab)) {
|
|
return areModifiersPressed(Qt::SHIFT, modifierInfo);
|
|
} 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::generated,
|
|
this,
|
|
[this, file](const KFileItem &item, const QImage &image) {
|
|
Q_UNUSED(item);
|
|
|
|
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(this))
|
|
{
|
|
registerShortcut(QString::fromLatin1(s_action_name_next_activity),
|
|
i18n("Walk through activities"),
|
|
Qt::META | Qt::Key_A,
|
|
&SwitcherBackend::keybdSwitchToNextActivity);
|
|
|
|
registerShortcut(QString::fromLatin1(s_action_name_previous_activity),
|
|
i18n("Walk through activities (Reverse)"),
|
|
Qt::META | Qt::SHIFT | Qt::Key_A,
|
|
&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();
|
|
|
|
// migrate values to state location
|
|
KConfig oldConfig(QStringLiteral("kactivitymanagerd-switcher"));
|
|
KConfigGroup oldTimes(&oldConfig, QStringLiteral("LastUsed"));
|
|
|
|
KConfig stateConfig(QStringLiteral("plasma/activitiesstaterc"), KConfig::SimpleConfig, QStandardPaths::GenericStateLocation);
|
|
KConfigGroup times = stateConfig.group(QStringLiteral("LastUsed"));
|
|
|
|
oldTimes.moveValuesTo(times);
|
|
}
|
|
|
|
SwitcherBackend::~SwitcherBackend()
|
|
{
|
|
}
|
|
|
|
SwitcherBackend *SwitcherBackend::create(QQmlEngine *engine, QJSEngine *scriptEngine)
|
|
{
|
|
Q_UNUSED(scriptEngine)
|
|
engine->addImageProvider(QStringLiteral("wallpaperthumbnail"), new ThumbnailImageProvider());
|
|
return new SwitcherBackend(nullptr);
|
|
}
|
|
|
|
void SwitcherBackend::keybdSwitchToNextActivity()
|
|
{
|
|
if (isReverseTab(m_actionShortcut[QString::fromLatin1(s_action_name_previous_activity)], m_modifierInfo)) {
|
|
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());
|
|
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_modifierInfo)) {
|
|
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("plasma/activitiesstaterc"), KConfig::SimpleConfig, QStandardPaths::GenericStateLocation);
|
|
KConfigGroup times(&config, QStringLiteral("LastUsed"));
|
|
|
|
const auto now = QDateTime::currentSecsSinceEpoch();
|
|
|
|
// 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_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;
|
|
}
|
|
|
|
void SwitcherBackend::setCurrentActivity(const QString &activity)
|
|
{
|
|
m_activities.setCurrentActivity(activity);
|
|
}
|
|
|
|
void SwitcherBackend::removeActivity(const QString &activity)
|
|
{
|
|
m_activities.removeActivity(activity);
|
|
}
|
|
|
|
bool SwitcherBackend::dropEnabled() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
void SwitcherBackend::setDropMode(bool value)
|
|
{
|
|
if (m_dropModeActive == value)
|
|
return;
|
|
|
|
m_dropModeActive = value;
|
|
if (value) {
|
|
setShouldShowSwitcher(true);
|
|
m_dropModeHider.stop();
|
|
} else {
|
|
m_dropModeHider.start();
|
|
}
|
|
}
|
|
|
|
bool SwitcherBackend::dragContainsWindows(QMimeData *mimeData) const
|
|
{
|
|
if (KWindowSystem::isPlatformWayland()) {
|
|
return TaskManager::WaylandTasksModel::winIdsFromMimeData(mimeData).count();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
#include "moc_switcherbackend.cpp"
|