/* * Copyright (C) 2014. 2015 Ivan Cukic * Copyright (C) 2009 Martin Gräßlin * Copyright (C) 2003 Lubos Lunak * Copyright (C) 1999, 2000 Matthias Ettrich * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, * or (at your option) any later version, as published by the Free * Software Foundation * * 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. */ // Self #include "switcherbackend.h" // Qt #include #include #include #include // KDE #include #include #include // X11 #include #include #include #define ACTION_NAME_NEXT_ACTIVITY "next activity" #define ACTION_NAME_PREVIOUS_ACTIVITY "previous activity" namespace { // Taken from kwin/tabbox/tabbox.cpp Display* display() { static Display *s_display = nullptr; if (!s_display) { s_display = QX11Info::display(); } return s_display; } bool areKeySymXsDepressed(bool bAll, const uint keySyms[], int nKeySyms) { char keymap[32]; XQueryKeymap(display(), keymap); for (int iKeySym = 0; iKeySym < nKeySyms; iKeySym++) { uint keySymX = keySyms[ iKeySym ]; uchar keyCodeX = XKeysymToKeycode(display(), keySymX); int i = keyCodeX / 8; char mask = 1 << (keyCodeX - (i * 8)); // Abort if bad index value, if (i < 0 || i >= 32) return false; // If ALL keys passed need to be depressed, if (bAll) { if ((keymap[i] & mask) == 0) return false; } else { // If we are looking for ANY key press, and this key is depressed, if (keymap[i] & mask) return true; } } // If we were looking for ANY key press, then none was found, return false, // If we were looking for ALL key presses, then all were found, return true. return bAll; } bool areModKeysDepressed(const QKeySequence& seq) { uint rgKeySyms[10]; int nKeySyms = 0; if (seq.isEmpty()) { return false; } int mod = seq[seq.count()-1] & Qt::KeyboardModifierMask; if (mod & Qt::SHIFT) { rgKeySyms[nKeySyms++] = XK_Shift_L; rgKeySyms[nKeySyms++] = XK_Shift_R; } if (mod & Qt::CTRL) { rgKeySyms[nKeySyms++] = XK_Control_L; rgKeySyms[nKeySyms++] = XK_Control_R; } if (mod & Qt::ALT) { rgKeySyms[nKeySyms++] = XK_Alt_L; rgKeySyms[nKeySyms++] = XK_Alt_R; } if (mod & Qt::META) { // It would take some code to determine whether the Win key // is associated with Super or Meta, so check for both. // See bug #140023 for details. rgKeySyms[nKeySyms++] = XK_Super_L; rgKeySyms[nKeySyms++] = XK_Super_R; rgKeySyms[nKeySyms++] = XK_Meta_L; rgKeySyms[nKeySyms++] = XK_Meta_R; } return areKeySymXsDepressed(false, rgKeySyms, nKeySyms); } bool isReverseTab(const QKeySequence &prevAction) { if (Qt::SHIFT & prevAction && Qt::Key_Tab & prevAction) { return areModKeysDepressed(Qt::SHIFT); } else { return false; } } } // local namespace template 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)); connect(&m_activities, &Controller::currentActivityChanged, this, &SwitcherBackend::currentActivityChangedSlot); } SwitcherBackend::SwitcherBackend(QObject *parent) : QObject(parent) , m_lastInvokedAction(Q_NULLPTR) , m_shouldShowSwitcher(false) { m_wallpaperCache = new KImageCache("activityswitcher_wallpaper_preview", 10485760); registerShortcut(ACTION_NAME_NEXT_ACTIVITY, i18n("Walk through activities"), Qt::META + Qt::Key_Tab, &SwitcherBackend::keybdSwitchToNextActivity); registerShortcut(ACTION_NAME_PREVIOUS_ACTIVITY, i18n("Walk through activities (Reverse)"), Qt::META + Qt::SHIFT + Qt::Key_Tab, &SwitcherBackend::keybdSwitchToPreviousActivity); connect(&m_modKeyPollingTimer, &QTimer::timeout, this, &SwitcherBackend::showActivitySwitcherIfNeeded); } SwitcherBackend::~SwitcherBackend() { delete m_wallpaperCache; } QObject *SwitcherBackend::instance(QQmlEngine *engine, QJSEngine *scriptEngine) { Q_UNUSED(engine) Q_UNUSED(scriptEngine) return new SwitcherBackend(); } void SwitcherBackend::keybdSwitchToNextActivity() { if (isReverseTab(m_actionShortcut[ACTION_NAME_PREVIOUS_ACTIVITY])) { switchToActivity(Previous); } else { switchToActivity(Next); } } void SwitcherBackend::keybdSwitchToPreviousActivity() { switchToActivity(Previous); } void SwitcherBackend::switchToActivity(Direction direction) { auto runningActivities = m_activities.activities(KActivities::Info::Running); if (runningActivities.count() == 0) { return; } // Sorting this every time is not really (or at all) efficient, // but at least we do not need to connect to too many Info objects std::sort(runningActivities.begin(), runningActivities.end(), [] (const QString &left, const QString &right) { using KActivities::Info; const QString &leftName = Info(left).name().toLower(); const QString &rightName = Info(right).name().toLower(); return (leftName < rightName) || (leftName == rightName && left < right); }); auto index = std::max( 0, runningActivities.indexOf(m_activities.currentActivity())); index += direction == Next ? 1 : -1; if (index < 0) { index = runningActivities.count() - 1; } else if (index >= runningActivities.count()) { index = 0; } // TODO: This is evil, but plasmashell goes into a dead-lock if // the activity is changed while one tries to open the switcher O.o // m_activities.setCurrentActivity(runningActivities[index]); const auto activityToSet = runningActivities[index]; QTimer::singleShot(150, this, [this,activityToSet] () { m_activities.setCurrentActivity(activityToSet); }); keybdSwitchedToAnotherActivity(); } void SwitcherBackend::keybdSwitchedToAnotherActivity() { m_lastInvokedAction = dynamic_cast(sender()); QTimer::singleShot(0, this, SLOT(showActivitySwitcherIfNeeded())); } void SwitcherBackend::showActivitySwitcherIfNeeded() { if (!m_lastInvokedAction) { return; } auto actionName = m_lastInvokedAction->objectName(); if (!m_actionShortcut.contains(actionName)) { return; } if (!areModKeysDepressed(m_actionShortcut[actionName])) { m_lastInvokedAction = Q_NULLPTR; setShouldShowSwitcher(false); return; } setShouldShowSwitcher(true); } void SwitcherBackend::init() { // nothing } void SwitcherBackend::currentActivityChangedSlot(const QString &id) { // Safe, we have a long-lived Consumer object KActivities::Info activity(id); emit showSwitchNotification(id, activity.name(), activity.icon()); } bool SwitcherBackend::shouldShowSwitcher() const { return m_shouldShowSwitcher; } void SwitcherBackend::setShouldShowSwitcher(const 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(100); } else { m_modKeyPollingTimer.stop(); } emit shouldShowSwitcherChanged(m_shouldShowSwitcher); } QPixmap SwitcherBackend::wallpaperThumbnail(const QString &path, int width, int height, const QJSValue &_callback) { // qDebug() << "SwitcherBackend: Requesting wallpaper: " << path << width << height; QJSValue callback(_callback); if (path.isEmpty()) { callback.call({false}); } QPixmap preview = QPixmap(QSize(0, 0)); if (width == 0) { width = 320; } if (height == 0) { height = 240; } const auto pixmapKey = path + "/" + QString::number(width) + "x" + QString::number(height); // qDebug() << "SwitcherBackend: Wallpaper cache id is: " << pixmapKey; if (m_wallpaperCache->findPixmap(pixmapKey, &preview)) { return preview; } QUrl file(path); // qDebug() << "SwitcherBackend: Cache miss. We need to generate the thumbnail: " << file; if (!m_previewJobs.contains(file) && file.isValid()) { m_previewJobs.insert(file); KFileItemList list; list.append(KFileItem(file, QString(), 0)); KIO::PreviewJob* job = KIO::filePreview(list, QSize(width, height)); job->setScaleType(KIO::PreviewJob::Scaled); job->setIgnoreMaximumSize(true); connect(job, &KIO::PreviewJob::gotPreview, this, [=] (const KFileItem& item, const QPixmap& pixmap) mutable { Q_UNUSED(item); m_wallpaperCache->insertPixmap(pixmapKey, pixmap); m_previewJobs.remove(path); // qDebug() << "SwitcherBackend: Got the thumbnail for " << path << "saving under" << pixmapKey; callback.call({true}); }); connect(job, &KIO::PreviewJob::failed, this, [=] (const KFileItem& item) mutable { Q_UNUSED(item); m_previewJobs.remove(path); qWarning() << "SwitcherBackend: FAILED to get the thumbnail for " << path << job->detailedErrorStrings(&file); callback.call({false}); }); } return preview; } #include "switcherbackend.moc"