diff --git a/desktoppackage/contents/activitymanager/ActivityItem.qml b/desktoppackage/contents/activitymanager/ActivityItem.qml index bb492b69d..a30a25f4b 100644 --- a/desktoppackage/contents/activitymanager/ActivityItem.qml +++ b/desktoppackage/contents/activitymanager/ActivityItem.qml @@ -1,371 +1,335 @@ import QtQuick 2.0 import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.extras 2.0 as PlasmaExtras import org.kde.kquickcontrolsaddons 2.0 as KQuickControlsAddonsComponents import org.kde.plasma.activityswitcher 1.0 as ActivitySwitcher import org.kde.activities 0.1 as Activities import org.kde.activities.settings 0.1 import "static.js" as S Item { id: root property int innerPadding : units.largeSpacing property bool current : false property bool selected : false property bool stoppable : true property alias title : title.text property alias icon : icon.source property alias hasWindows : hasWindowsIndicator.visible z : current ? 10 : selected ? 5 : 0 property string activityId : "" property string background : "" - function updateBackgroundRetry(valid) { - // TODO: (comment by David) - // - // Registering a a QQuickImageProvider with - // ForceAsynchronousImageLoading (so you can just exec() in the main - // method without blocking) will probably be both simpler code - // and more robust. - - if (valid) { - // Try to get the pixmap, if it is not available, this function - // will be called again when the thumbnailer finishes its job - // console.log("Loading background for " + root.title); - backgroundWallpaper.pixmap = ActivitySwitcher.Backend.wallpaperThumbnail( - background, - backgroundWallpaper.width, - backgroundWallpaper.height, - updateBackgroundRetry - ); - - backgroundColor.visible = false; - - } else { - backgroundColor.color = "#000000"; - backgroundColor.visible = true; - } - } - - function updateBackground() { - if (background == "") return; - - updateBackgroundRetry(true); - } - onBackgroundChanged: if (background[0] != '#') { // We have a proper wallpaper, hurroo! - updateBackground(); + backgroundColor.visible = false; } else { // We have only a color - backgroundColor.color = background - backgroundColor.visible = true + backgroundColor.color = background; + backgroundColor.visible = true; } signal clicked width : 200 // height : width * 1 / units.displayAspectRatio // Marco removed displayAspectRatio height : width * 9.0 / 16.0 - onWidthChanged : updateBackground() - onHeightChanged : updateBackground() - - Item { anchors { fill: parent } // Background until we get something real Rectangle { id: backgroundColor anchors.fill: parent // This is intentional - while waiting for the wallpaper, // we are showing a semi-transparent black background color: "black" opacity: root.selected ? .8 : .5 } - KQuickControlsAddonsComponents.QPixmapItem { + Image { id: backgroundWallpaper anchors.fill: parent visible: !backgroundColor.visible + source: "image://wallpaperthumbnail/" + background + sourceSize: size } - // Title and the icon Rectangle { id: shade width: parent.height height: parent.width anchors.centerIn: parent rotation: 90 gradient: Gradient { GradientStop { position: 1.0; color: "black" } GradientStop { position: 0.0; color: "transparent" } } opacity : root.selected ? 0.5 : 1.0 } Rectangle { id: highlight visible: root.current border.width: root.current ? units.smallSpacing : 0 border.color: theme.highlightColor anchors { fill: parent // Hide the rounding error on the bottom of the rectangle bottomMargin: -1 } color: "transparent" // z: 1 } Item { id: titleBar anchors { top : parent.top left : parent.left right : parent.right leftMargin : 2 * units.smallSpacing + 2 topMargin : 2 * units.smallSpacing } Text { id: title color : "white" elide : Text.ElideRight visible : shade.visible font.bold : true anchors { top : parent.top left : parent.left right : icon.left } } Text { id: description color : "white" elide : Text.ElideRight text : model.description opacity : .6 anchors { top : title.bottom left : parent.left right : icon.left } } PlasmaCore.IconItem { id: icon width : units.iconSizes.medium height : width anchors { right : parent.right rightMargin : 2 * units.smallSpacing } } } Column { id: statsBar height: childrenRect.height + units.smallSpacing anchors { bottom : controlBar.top left : parent.left right : parent.right leftMargin : 2 * units.smallSpacing + 2 rightMargin : 2 * units.smallSpacing bottomMargin : units.smallSpacing } PlasmaCore.IconItem { id : hasWindowsIndicator source : "window-duplicate" width : 16 height : width opacity : .6 visible : false } Text { id: lastUsedDate color : "white" elide : Text.ElideRight opacity : .6 text: root.current ? i18nd("plasma_shell_org.kde.plasma.desktop", "Currently being used") : model.lastTimeUsedString } // Text { // id: stats // // color : "white" // elide : Text.ElideRight // opacity : .6 // // text: "6 documents, 2 applications" // visible: false // // anchors { // top : lastUsedDate.bottom // left : parent.left // right : parent.right // } // } } MouseArea { id: hoverArea anchors.fill : parent onClicked : root.clicked() hoverEnabled : true onEntered : S.showActivityItemActionsBar(root) Accessible.name : root.title Accessible.role : Accessible.Button Accessible.selected : root.selected Accessible.onPressAction : root.clicked() } // Controls Item { id: controlBar height: root.state == "showingControls" ? (configButton.height + 4 * units.smallSpacing) : 0 Behavior on height { NumberAnimation { duration: units.longDuration } } Behavior on opacity { NumberAnimation { duration: units.shortDuration } } clip: true anchors { bottom : parent.bottom left : parent.left right : parent.right } Rectangle { anchors { fill: parent margins: - 2 * units.smallSpacing } opacity: .75 color: theme.backgroundColor } PlasmaComponents.Button { id: configButton iconSource: "configure" tooltip: i18nd("plasma_shell_org.kde.plasma.desktop", "Configure") onClicked: ActivitySettings.configureActivity(root.activityId); anchors { left : parent.left top : parent.top leftMargin : 2 * units.smallSpacing + 2 topMargin : 2 * units.smallSpacing } } PlasmaComponents.Button { id: stopButton iconSource: "process-stop" tooltip: i18nd("plasma_shell_org.kde.plasma.desktop", "Stop activity") onClicked: ActivitySwitcher.Backend.stopActivity(activityId); anchors { right : parent.right top : parent.top rightMargin : 2 * units.smallSpacing + 2 topMargin : 2 * units.smallSpacing } } } } states: [ State { name: "plain" PropertyChanges { target: shade; visible: true } PropertyChanges { target: controlBar; opacity: 0 } }, State { name: "showingControls" PropertyChanges { target: shade; visible: true } PropertyChanges { target: controlBar; opacity: 1 } } ] transitions: [ Transition { NumberAnimation { properties : "opacity" duration : units.shortDuration } } ] } diff --git a/imports/activitymanager/CMakeLists.txt b/imports/activitymanager/CMakeLists.txt index 21aab9dbd..272187e90 100644 --- a/imports/activitymanager/CMakeLists.txt +++ b/imports/activitymanager/CMakeLists.txt @@ -1,57 +1,58 @@ find_package (ECM 0.0.8 REQUIRED NO_MODULE) set (CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH}) -find_package (Qt5 REQUIRED NO_MODULE COMPONENTS Widgets Qml X11Extras) +find_package (Qt5 REQUIRED NO_MODULE COMPONENTS Widgets Qml X11Extras Quick) find_package (KF5I18n ${KF5_DEP_VERSION} CONFIG REQUIRED) find_package (KF5Config ${KF5_DEP_VERSION} CONFIG REQUIRED) find_package (KF5CoreAddons ${KF5_DEP_VERSION} CONFIG REQUIRED) find_package (KF5GlobalAccel ${KF5_DEP_VERSION} CONFIG REQUIRED) find_package (KF5GuiAddons ${KF5_DEP_VERSION} CONFIG REQUIRED) find_package (KF5KIO ${KF5_DEP_VERSION} CONFIG REQUIRED) find_package (KF5Activities ${KF5_DEP_VERSION} CONFIG REQUIRED) find_package (KF5WindowSystem ${KF5_DEP_VERSION} CONFIG REQUIRED) find_package (X11) set_package_properties ( X11 PROPERTIES DESCRIPTION "X11 libraries" URL "http://www.x.org" TYPE REQUIRED ) set ( activityswitcher_imports_LIB_SRCS activityswitcherextensionplugin.cpp switcherbackend.cpp sortedactivitiesmodel.cpp ) add_library (activityswitcherextensionplugin SHARED ${activityswitcher_imports_LIB_SRCS}) add_definitions(-DTRANSLATION_DOMAIN=\"plasmaactivitymanager\") target_link_libraries ( activityswitcherextensionplugin Qt5::Core + Qt5::Quick Qt5::Qml Qt5::Widgets Qt5::X11Extras KF5::I18n KF5::Activities KF5::ConfigCore KF5::CoreAddons KF5::GlobalAccel KF5::GuiAddons KF5::KIOCore KF5::KIOWidgets KF5::WindowSystem ${X11_X11_LIB} ) ## install install (TARGETS activityswitcherextensionplugin DESTINATION ${QML_INSTALL_DIR}/org/kde/plasma/activityswitcher) install (FILES qmldir DESTINATION ${QML_INSTALL_DIR}/org/kde/plasma/activityswitcher) diff --git a/imports/activitymanager/sortedactivitiesmodel.cpp b/imports/activitymanager/sortedactivitiesmodel.cpp index 258093a90..d74f4df73 100644 --- a/imports/activitymanager/sortedactivitiesmodel.cpp +++ b/imports/activitymanager/sortedactivitiesmodel.cpp @@ -1,460 +1,460 @@ /* * Copyright (C) 2016 Ivan Cukic * * 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 "sortedactivitiesmodel.h" // Qt #include #include // KDE #include #include #include #include #include #define KWINDOWSYSTEM_NO_DEPRECATED #include #include namespace { class BackgroundCache { public: BackgroundCache() : initialized(false) , plasmaConfig("plasma-org.kde.plasma.desktop-appletsrc") { using namespace std::placeholders; const auto configFile = QStandardPaths::writableLocation( QStandardPaths::GenericConfigLocation) + QLatin1Char('/') + plasmaConfig.name(); KDirWatch::self()->addFile(configFile); QObject::connect(KDirWatch::self(), &KDirWatch::dirty, [this] (const QString &file) { settingsFileChanged(file); }); QObject::connect(KDirWatch::self(), &KDirWatch::created, [this] (const QString &file) { settingsFileChanged(file); }); } void settingsFileChanged(const QString &file) { if (!file.endsWith(plasmaConfig.name())) return; plasmaConfig.reparseConfiguration(); if (initialized) { reload(false); } } void subscribe(SortedActivitiesModel *model) { if (!initialized) { reload(true); } models << model; } void unsubscribe(SortedActivitiesModel *model) { models.removeAll(model); if (models.isEmpty()) { initialized = false; forActivity.clear(); } } QString backgroundFromConfig(const KConfigGroup &config) const { auto wallpaperPlugin = config.readEntry("wallpaperplugin"); auto wallpaperConfig = config.group("Wallpaper").group(wallpaperPlugin).group("General"); if (wallpaperConfig.hasKey("Image")) { // Trying for the wallpaper auto wallpaper = wallpaperConfig.readEntry("Image", QString()); if (!wallpaper.isEmpty()) { return wallpaper; } } if (wallpaperConfig.hasKey("Color")) { auto backgroundColor = wallpaperConfig.readEntry("Color", QColor(0, 0, 0)); return backgroundColor.name(); } return QString(); } void reload(bool) { auto newForActivity = forActivity; // contains activities for which the wallpaper // has updated QStringList changedActivities; // Contains activities not covered by any containment QStringList ghostActivities = forActivity.keys(); // Traversing through all containments in search for // containments that define activities in plasma for (const auto& containmentId: plasmaConfigContainments().groupList()) { const auto containment = plasmaConfigContainments().group(containmentId); const auto activity = containment.readEntry("activityId", QString()); // Ignore the containment if the activity is not defined if (activity.isEmpty()) continue; // If we have already found the same activity from another // containment, we are using the new one only if // the previous one was a color and not a proper wallpaper const bool processed = !ghostActivities.contains(activity) && newForActivity.contains(activity); if (processed && newForActivity[activity][0] != '#') continue; // Marking the current activity as processed ghostActivities.removeAll(activity); const auto background = backgroundFromConfig(containment); + if (background.isEmpty()) continue; + if (newForActivity[activity] != background) { changedActivities << activity; - if (!background.isEmpty()) { - newForActivity[activity] = background; - } + newForActivity[activity] = background; } } initialized = true; // Removing the activities from the list if we haven't found them // while traversing through the containments for (const auto& activity: ghostActivities) { newForActivity.remove(activity); } // If we have detected the changes, lets notify everyone if (!changedActivities.isEmpty()) { forActivity = newForActivity; for (auto model: models) { model->onBackgroundsUpdated(changedActivities); } } } KConfigGroup plasmaConfigContainments() { return plasmaConfig.group("Containments"); } QHash forActivity; QList models; bool initialized; KConfig plasmaConfig; }; static BackgroundCache &backgrounds() { // If you convert this to a shared pointer, // fix the connections to KDirWatcher static BackgroundCache cache; return cache; } } SortedActivitiesModel::SortedActivitiesModel(QVector states, QObject *parent) : QSortFilterProxyModel(parent) , m_activitiesModel(new KActivities::ActivitiesModel(states, this)) , m_activities(new KActivities::Consumer(this)) { setSourceModel(m_activitiesModel); setDynamicSortFilter(true); setSortRole(LastTimeUsed); sort(0, Qt::DescendingOrder); backgrounds().subscribe(this); const QList windows = KWindowSystem::stackingOrder(); for (const auto& window: windows) { KWindowInfo info(window, NET::WMVisibleName, NET::WM2Activities); const QStringList activities = info.activities(); if (activities.isEmpty() || activities.contains("00000000-0000-0000-0000-000000000000")) continue; for (const auto& activity: activities) { m_activitiesWindows[activity] << window; } } connect(KWindowSystem::self(), &KWindowSystem::windowAdded, this, &SortedActivitiesModel::onWindowAdded); connect(KWindowSystem::self(), &KWindowSystem::windowRemoved, this, &SortedActivitiesModel::onWindowRemoved); connect(KWindowSystem::self(), SIGNAL(windowChanged(WId, NET::Properties, NET::Properties2)), this, SLOT(onWindowChanged(WId, NET::Properties, NET::Properties2))); } SortedActivitiesModel::~SortedActivitiesModel() { backgrounds().unsubscribe(this); } bool SortedActivitiesModel::inhibitUpdates() const { return m_inhibitUpdates; } void SortedActivitiesModel::setInhibitUpdates(bool inhibitUpdates) { if (m_inhibitUpdates != inhibitUpdates) { m_inhibitUpdates = inhibitUpdates; emit inhibitUpdatesChanged(m_inhibitUpdates); setDynamicSortFilter(!inhibitUpdates); } } uint SortedActivitiesModel::lastUsedTime(const QString &activity) const { if (m_activities->currentActivity() == activity) { return ~(uint)0; } else { KConfig config("kactivitymanagerd-switcher"); KConfigGroup times(&config, "LastUsed"); return times.readEntry(activity, (uint)0); } } bool SortedActivitiesModel::lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const { const auto activityLeft = sourceModel()->data(sourceLeft, KActivities::ActivitiesModel::ActivityId); const auto activityRight = sourceModel()->data(sourceRight, KActivities::ActivitiesModel::ActivityId); const auto timeLeft = lastUsedTime(activityLeft.toString()); const auto timeRight = lastUsedTime(activityRight.toString()); return (timeLeft < timeRight) || (timeLeft == timeRight && activityLeft < activityRight); } QHash SortedActivitiesModel::roleNames() const { if (!sourceModel()) return QHash(); auto roleNames = sourceModel()->roleNames(); roleNames[LastTimeUsed] = "lastTimeUsed"; roleNames[LastTimeUsedString] = "lastTimeUsedString"; roleNames[WindowCount] = "windowCount"; roleNames[HasWindows] = "hasWindows"; return roleNames; } QVariant SortedActivitiesModel::data(const QModelIndex &index, int role) const { if (role == KActivities::ActivitiesModel::ActivityBackground) { const auto activity = activityIdForIndex(index); return backgrounds().forActivity[activity]; } else if (role == LastTimeUsed || role == LastTimeUsedString) { const auto activity = activityIdForIndex(index); const auto time = lastUsedTime(activity); if (role == LastTimeUsed) { return QVariant(time); } else { const auto now = QDateTime::currentDateTime().toTime_t(); if (time == 0) return i18n("Used some time ago"); auto diff = now - time; // We do not need to be precise diff /= 60; const auto minutes = diff % 60; diff /= 60; const auto hours = diff % 24; diff /= 24; const auto days = diff % 30; diff /= 30; const auto months = diff % 12; diff /= 12; const auto years = diff; return (years > 0) ? i18n("Used more than a year ago") : (months > 0) ? i18ncp("amount in months", "Used a month ago", "Used %1 months ago", months) : (days > 0) ? i18ncp("amount in days", "Used a day ago", "Used %1 days ago", days) : (hours > 0) ? i18ncp("amount in hours", "Used an hour ago", "Used %1 hours ago", hours) : (minutes > 0) ? i18ncp("amount in minutes", "Used a minute ago", "Used %1 minutes ago", minutes) : i18n("Used a moment ago"); } } else if (role == HasWindows || role == WindowCount) { const auto activity = activityIdForIndex(index); if (role == HasWindows) { return (m_activitiesWindows[activity].size() > 0); } else { return m_activitiesWindows[activity].size(); } } else { return QSortFilterProxyModel::data(index, role); } } QString SortedActivitiesModel::activityIdForIndex(const QModelIndex &index) const { return data(index, KActivities::ActivitiesModel::ActivityId).toString(); } QString SortedActivitiesModel::activityIdForRow(int row) const { return activityIdForIndex(index(row, 0)); } int SortedActivitiesModel::rowForActivityId(const QString &activity) const { int position = -1; for (int row = 0; row < rowCount(); ++row) { if (activity == activityIdForRow(row)) { position = row; } } return position; } QString SortedActivitiesModel::relativeActivity(int relative) const { const auto currentActivity = m_activities->currentActivity(); if (!sourceModel()) return QString(); const auto currentRowCount = sourceModel()->rowCount(); //x % 0 is undefined in c++ if (currentRowCount == 0) { return QString(); } int currentActivityRow = 0; for (; currentActivityRow < currentRowCount; currentActivityRow++) { if (activityIdForRow(currentActivityRow) == currentActivity) break; } currentActivityRow = currentActivityRow + relative; //wrap to within bounds for both positive and negative currentActivityRows currentActivityRow = (currentRowCount + (currentActivityRow % currentRowCount)) % currentRowCount; return activityIdForRow(currentActivityRow); } void SortedActivitiesModel::onCurrentActivityChanged(const QString ¤tActivity) { if (m_previousActivity == currentActivity) return; const int previousActivityRow = rowForActivityId(m_previousActivity); emit rowChanged(previousActivityRow, { LastTimeUsed, LastTimeUsedString }); m_previousActivity = currentActivity; const int currentActivityRow = rowForActivityId(m_previousActivity); emit rowChanged(currentActivityRow, { LastTimeUsed, LastTimeUsedString }); } void SortedActivitiesModel::onBackgroundsUpdated(const QStringList &activities) { for (const auto &activity: activities) { const int row = rowForActivityId(activity); emit rowChanged(row, { KActivities::ActivitiesModel::ActivityBackground }); } } void SortedActivitiesModel::onWindowAdded(WId window) { KWindowInfo info(window, 0, NET::WM2Activities); const QStringList activities = info.activities(); if (activities.isEmpty() || activities.contains("00000000-0000-0000-0000-000000000000")) return; for (const auto& activity: activities) { if (!m_activitiesWindows[activity].contains(window)) { m_activitiesWindows[activity] << window; rowChanged(rowForActivityId(activity), m_activitiesWindows.size() == 1 ? QVector{WindowCount, HasWindows} : QVector{WindowCount}); } } } void SortedActivitiesModel::onWindowRemoved(WId window) { for (const auto& activity: m_activitiesWindows.keys()) { if (m_activitiesWindows[activity].contains(window)) { m_activitiesWindows[activity].removeAll(window); rowChanged(rowForActivityId(activity), m_activitiesWindows.size() == 0 ? QVector{WindowCount, HasWindows} : QVector{WindowCount}); } } } void SortedActivitiesModel::onWindowChanged(WId window, NET::Properties properties, NET::Properties2 properties2) { Q_UNUSED(properties); if (properties2 & NET::WM2Activities) { onWindowRemoved(window); onWindowAdded(window); } } void SortedActivitiesModel::rowChanged(int row, const QVector &roles) { if (row == -1) return; emit dataChanged(index(row, 0), index(row, 0), roles); } diff --git a/imports/activitymanager/switcherbackend.cpp b/imports/activitymanager/switcherbackend.cpp index 8a5ada2d5..f04f0a341 100644 --- a/imports/activitymanager/switcherbackend.cpp +++ b/imports/activitymanager/switcherbackend.cpp @@ -1,424 +1,440 @@ /* * 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 +// Qml and QtQuick +#include +#include + // KDE #include #include #include #include #include // X11 #include #include #include #define ACTION_NAME_NEXT_ACTIVITY "next activity" #define ACTION_NAME_PREVIOUS_ACTIVITY "previous activity" namespace { bool isPlatformX11() { static const bool isX11 = QX11Info::isPlatformX11(); return isX11; } // Taken from kwin/tabbox/tabbox.cpp Display* x11_display() { static Display *s_display = nullptr; if (!s_display) { s_display = QX11Info::display(); } return s_display; } bool x11_areKeySymXsDepressed(bool bAll, const uint keySyms[], int nKeySyms) { char keymap[32]; XQueryKeymap(x11_display(), keymap); for (int iKeySym = 0; iKeySym < nKeySyms; iKeySym++) { uint keySymX = keySyms[ iKeySym ]; uchar keyCodeX = XKeysymToKeycode(x11_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 x11_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 x11_areKeySymXsDepressed(false, rgKeySyms, nKeySyms); } bool x11_isReverseTab(const QKeySequence &prevAction) { if (prevAction == QKeySequence(Qt::ShiftModifier | Qt::Key_Tab)) { return x11_areModKeysDepressed(Qt::SHIFT); } else { return false; } } + class ThumbnailImageResponse: public QQuickImageResponse { + public: + ThumbnailImageResponse(const QString &id, const QSize &requestedSize); + + QQuickTextureFactory *textureFactory() const; + + void run(); + + private: + QString m_id; + QSize m_requestedSize; + QQuickTextureFactory *m_texture; + }; + + ThumbnailImageResponse::ThumbnailImageResponse(const QString &id, + const QSize &requestedSize) + : m_id(id) + , m_requestedSize(requestedSize) + , m_texture(Q_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()) { + 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); + 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(); + emit finished(); + }); + } + + QQuickTextureFactory *ThumbnailImageResponse::textureFactory() const + { + return m_texture; + } + + class ThumbnailImageProvider: public QQuickAsyncImageProvider { + public: + QQuickImageResponse *requestImageResponse(const QString &id, + const QSize &requestedSize) + { + return new ThumbnailImageResponse(id, requestedSize); + } + }; + + } // 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)); } SwitcherBackend::SwitcherBackend(QObject *parent) : QObject(parent) , m_lastInvokedAction(Q_NULLPTR) , m_shouldShowSwitcher(false) , m_runningActivitiesModel(new SortedActivitiesModel({KActivities::Info::Running, KActivities::Info::Stopping}, this)) , m_stoppedActivitiesModel(new SortedActivitiesModel({KActivities::Info::Stopped, KActivities::Info::Starting}, this)) { - 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(this, &SwitcherBackend::shouldShowSwitcherChanged, m_runningActivitiesModel, &SortedActivitiesModel::setInhibitUpdates); connect(&m_modKeyPollingTimer, &QTimer::timeout, this, &SwitcherBackend::showActivitySwitcherIfNeeded); connect(&m_activities, &KActivities::Controller::currentActivityChanged, this, &SwitcherBackend::onCurrentActivityChanged); m_previousActivity = m_activities.currentActivity(); } SwitcherBackend::~SwitcherBackend() { - delete m_wallpaperCache; } QObject *SwitcherBackend::instance(QQmlEngine *engine, QJSEngine *scriptEngine) { - Q_UNUSED(engine) Q_UNUSED(scriptEngine) + engine->addImageProvider("wallpaperthumbnail", new ThumbnailImageProvider()); return new SwitcherBackend(); } void SwitcherBackend::keybdSwitchToNextActivity() { if (isPlatformX11()) { // If we are on X11, we have all needed features for meta+tab // to work properly if (x11_isReverseTab(m_actionShortcut[ACTION_NAME_PREVIOUS_ACTIVITY])) { switchToActivity(Previous); } else { switchToActivity(Next); } } else { // If we are on wayland, just switch to the next activity 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(150, this, [this,activityToSet] () { setCurrentActivity(activityToSet); }); keybdSwitchedToAnotherActivity(); } void SwitcherBackend::keybdSwitchedToAnotherActivity() { m_lastInvokedAction = dynamic_cast(sender()); QTimer::singleShot(0, this, &SwitcherBackend::showActivitySwitcherIfNeeded); } void SwitcherBackend::showActivitySwitcherIfNeeded() { if (!m_lastInvokedAction) { return; } auto actionName = m_lastInvokedAction->objectName(); if (!m_actionShortcut.contains(actionName)) { return; } if (isPlatformX11()) { if (!x11_areModKeysDepressed(m_actionShortcut[actionName])) { m_lastInvokedAction = Q_NULLPTR; setShouldShowSwitcher(false); return; } setShouldShowSwitcher(true); } else { // We are not showing the switcher on wayland // TODO: This is a regression on wayland setShouldShowSwitcher(false); } } 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); emit showSwitchNotification(id, activity.name(), activity.icon()); KConfig config("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(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(); // We might have an unprocessed onCurrentActivityChanged onCurrentActivityChanged(m_activities.currentActivity()); } emit shouldShowSwitcherChanged(m_shouldShowSwitcher); } -QPixmap SwitcherBackend::wallpaperThumbnail(const QString &path, int width, int height, - const QJSValue &_callback) -{ - QPixmap preview = QPixmap(QSize(1, 1)); - - QJSValue callback(_callback); - - if (path.isEmpty()) { - callback.call({false}); - return preview; - } - - if (width == 0) { - width = 320; - } - - if (height == 0) { - height = 240; - } - - - const auto pixmapKey = path + "/" - + QString::number(width) + "x" - + QString::number(height); - - if (m_wallpaperCache->findPixmap(pixmapKey, &preview)) { - return preview; - } - - QUrl file = QUrl::fromLocalFile(path); - - 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(file); - - callback.call({true}); - }); - - connect(job, &KIO::PreviewJob::failed, - this, [=] (const KFileItem& item) mutable { - Q_UNUSED(item); - m_previewJobs.remove(file); - - qWarning() << "SwitcherBackend: FAILED to get the thumbnail for " - << path << job->detailedErrorStrings(&file); - callback.call({false}); - }); - - } - - return preview; -} - 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); } #include "switcherbackend.moc" diff --git a/imports/activitymanager/switcherbackend.h b/imports/activitymanager/switcherbackend.h index 6f9e0d73e..e1d6bdf2d 100644 --- a/imports/activitymanager/switcherbackend.h +++ b/imports/activitymanager/switcherbackend.h @@ -1,113 +1,107 @@ /* * Copyright (C) 2014 Ivan Cukic * * 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. */ #ifndef SWITCHER_BACKEND_H #define SWITCHER_BACKEND_H // Qt #include #include #include #include #include #include // KDE #include #include // Local #include "sortedactivitiesmodel.h" class QAction; class QQmlEngine; class QJSEngine; namespace KIO { class PreviewJob; } class SwitcherBackend : public QObject { Q_OBJECT Q_PROPERTY(bool shouldShowSwitcher READ shouldShowSwitcher WRITE setShouldShowSwitcher NOTIFY shouldShowSwitcherChanged) public: SwitcherBackend(QObject *parent = Q_NULLPTR); ~SwitcherBackend(); static QObject *instance(QQmlEngine *engine, QJSEngine *scriptEngine); Q_SIGNALS: void showSwitchNotification(const QString &id, const QString &name, const QString &icon); void shouldShowSwitcherChanged(bool value); public Q_SLOTS: void init(); bool shouldShowSwitcher() const; void setShouldShowSwitcher(const bool &shouldShowSwitcher); - QPixmap wallpaperThumbnail(const QString &path, int width, int height, - const QJSValue &callback); - QAbstractItemModel *runningActivitiesModel() const; QAbstractItemModel *stoppedActivitiesModel() const; void setCurrentActivity(const QString &activity); void stopActivity(const QString &activity); private: template inline void registerShortcut(const QString &actionName, const QString &name, const QKeySequence &shortcut, Handler &&handler); - enum Direction { Next, Previous }; + void switchToActivity(Direction i); private Q_SLOTS: void keybdSwitchToNextActivity(); void keybdSwitchToPreviousActivity(); void keybdSwitchedToAnotherActivity(); void showActivitySwitcherIfNeeded(); void onCurrentActivityChanged(const QString &id); private: QHash m_actionShortcut; QAction *m_lastInvokedAction; KActivities::Controller m_activities; bool m_shouldShowSwitcher; QTimer m_modKeyPollingTimer; QString m_previousActivity; - KImageCache *m_wallpaperCache; - QSet m_previewJobs; - SortedActivitiesModel *m_runningActivitiesModel; SortedActivitiesModel *m_stoppedActivitiesModel; }; #endif // SWITCHER_BACKEND_H