diff --git a/applets/taskmanager/package/contents/ui/ContextMenu.qml b/applets/taskmanager/package/contents/ui/ContextMenu.qml index bc88d9266..5677f65a2 100644 --- a/applets/taskmanager/package/contents/ui/ContextMenu.qml +++ b/applets/taskmanager/package/contents/ui/ContextMenu.qml @@ -1,719 +1,738 @@ /*************************************************************************** * Copyright (C) 2012-2016 by Eike Hein * * Copyright (C) 2016 by Kai Uwe Broulik * * * * 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 . * ***************************************************************************/ import QtQuick 2.0 import org.kde.plasma.plasmoid 2.0 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.taskmanager 0.1 as TaskManager import "code/layout.js" as LayoutManager PlasmaComponents.ContextMenu { id: menu property QtObject backend property QtObject mpris2Source property var modelIndex readonly property var atm: TaskManager.AbstractTasksModel property bool showAllPlaces: false placement: { if (plasmoid.location === PlasmaCore.Types.LeftEdge) { return PlasmaCore.Types.RightPosedTopAlignedPopup; } else if (plasmoid.location === PlasmaCore.Types.TopEdge) { return PlasmaCore.Types.BottomPosedLeftAlignedPopup; } else if (plasmoid.location === PlasmaCore.Types.RightEdge) { return PlasmaCore.Types.LeftPosedTopAlignedPopup; } else { return PlasmaCore.Types.TopPosedLeftAlignedPopup; } } minimumWidth: visualParent.width onStatusChanged: { if (visualParent && get(atm.LauncherUrlWithoutIcon) !== "" && status == PlasmaComponents.DialogStatus.Open) { launcherToggleAction.checked = (tasksModel.launcherPosition(get(atm.LauncherUrlWithoutIcon)) !== -1); activitiesDesktopsMenu.refresh(); } else if (status == PlasmaComponents.DialogStatus.Closed) { menu.destroy(); backend.ungrabMouse(visualParent); } } Component.onCompleted: { // Cannot have "Connections" as child of PlasmaCoponents.ContextMenu. backend.showAllPlaces.connect(function() { visualParent.showContextMenu({showAllPlaces: true}); }); } function get(modelProp) { return tasksModel.data(modelIndex, modelProp) } function show() { loadDynamicLaunchActions(get(atm.LauncherUrlWithoutIcon)); openRelative(); } function newMenuItem(parent) { return Qt.createQmlObject( "import org.kde.plasma.components 2.0 as PlasmaComponents;" + "PlasmaComponents.MenuItem {}", parent); } function newSeparator(parent) { return Qt.createQmlObject( "import org.kde.plasma.components 2.0 as PlasmaComponents;" + "PlasmaComponents.MenuItem { separator: true }", parent); } function loadDynamicLaunchActions(launcherUrl) { - var lists = [ - backend.jumpListActions(launcherUrl, menu), - backend.placesActions(launcherUrl, showAllPlaces, menu), - backend.recentDocumentActions(launcherUrl, menu) + var sections = [ + { + title: i18n("Places"), + group: "places", + actions: backend.placesActions(launcherUrl, showAllPlaces, menu) + }, + { + title: i18n("Recent Documents"), + group: "recents", + actions: backend.recentDocumentActions(launcherUrl, menu) + }, + { + title: i18n("Actions"), + group: "actions", + actions: backend.jumpListActions(launcherUrl, menu) + } ] // QMenu does not limit its width automatically. Even if we set a maximumWidth // it would just cut off text rather than eliding. So we do this manually. var textMetrics = Qt.createQmlObject("import QtQuick 2.4; TextMetrics {}", menu); var maximumWidth = LayoutManager.maximumContextMenuTextWidth(); - lists.forEach(function (list) { - for (var i = 0; i < list.length; ++i) { + sections.forEach(function (section) { + // Always show the "Actions:" header, since we visually merge + // This section with the one beneath it that shows universal actions + if (section["actions"].length > 0 || section["group"] == "actions") { + var sectionHeader = newMenuItem(menu); + sectionHeader.text = section["title"]; + sectionHeader.section = true; + menu.addMenuItem(sectionHeader, startNewInstanceItem); + } + + for (var i = 0; i < section["actions"].length; ++i) { var item = newMenuItem(menu); - item.action = list[i]; + item.action = section["actions"][i]; // Crude way of manually eliding... var elided = false; textMetrics.text = Qt.binding(function() { return item.action.text; }); while (textMetrics.width > maximumWidth) { item.action.text = item.action.text.slice(0, -1); elided = true; } if (elided) { item.action.text += "..."; } - menu.addMenuItem(item, virtualDesktopsMenuItem); - } - - if (list.length > 0) { - menu.addMenuItem(newSeparator(menu), virtualDesktopsMenuItem); + menu.addMenuItem(item, startNewInstanceItem); } }); // Add Media Player control actions var sourceName = mpris2Source.sourceNameForLauncherUrl(launcherUrl, get(atm.AppPid)); if (sourceName && !(get(atm.WinIdList) !== undefined && get(atm.WinIdList).length > 1)) { var playerData = mpris2Source.data[sourceName] if (playerData.CanControl) { var playing = (playerData.PlaybackStatus === "Playing"); var menuItem = menu.newMenuItem(menu); menuItem.text = i18nc("Play previous track", "Previous Track"); menuItem.icon = "media-skip-backward"; menuItem.enabled = Qt.binding(function() { return playerData.CanGoPrevious; }); menuItem.clicked.connect(function() { mpris2Source.goPrevious(sourceName); }); - menu.addMenuItem(menuItem, virtualDesktopsMenuItem); + menu.addMenuItem(menuItem, startNewInstanceItem); menuItem = menu.newMenuItem(menu); // PlasmaCore Menu doesn't actually handle icons or labels changing at runtime... menuItem.text = Qt.binding(function() { // if CanPause, toggle the menu entry between Play & Pause, otherwise always use Play return playing && playerData.CanPause ? i18nc("Pause playback", "Pause") : i18nc("Start playback", "Play"); }); menuItem.icon = Qt.binding(function() { return playing && playerData.CanPause ? "media-playback-pause" : "media-playback-start"; }); menuItem.enabled = Qt.binding(function() { return playing ? playerData.CanPause : playerData.CanPlay; }); menuItem.clicked.connect(function() { if (playing) { mpris2Source.pause(sourceName); } else { mpris2Source.play(sourceName); } }); - menu.addMenuItem(menuItem, virtualDesktopsMenuItem); + menu.addMenuItem(menuItem, startNewInstanceItem); menuItem = menu.newMenuItem(menu); menuItem.text = i18nc("Play next track", "Next Track"); menuItem.icon = "media-skip-forward"; menuItem.enabled = Qt.binding(function() { return playerData.CanGoNext; }); menuItem.clicked.connect(function() { mpris2Source.goNext(sourceName); }); - menu.addMenuItem(menuItem, virtualDesktopsMenuItem); + menu.addMenuItem(menuItem, startNewInstanceItem); menuItem = menu.newMenuItem(menu); menuItem.text = i18nc("Stop playback", "Stop"); menuItem.icon = "media-playback-stop"; menuItem.enabled = Qt.binding(function() { return playerData.PlaybackStatus !== "Stopped"; }); menuItem.clicked.connect(function() { mpris2Source.stop(sourceName); }); - menu.addMenuItem(menuItem, virtualDesktopsMenuItem); + menu.addMenuItem(menuItem, startNewInstanceItem); // Technically media controls and audio streams are separate but for the user they're // semantically related, don't add a separator inbetween. if (!menu.visualParent.hasAudioStream) { - menu.addMenuItem(newSeparator(menu), virtualDesktopsMenuItem); + menu.addMenuItem(newSeparator(menu), startNewInstanceItem); } // If we don't have a window associated with the player but we can quit // it through MPRIS we'll offer a "Quit" option instead of "Close" if (!closeWindowItem.visible && playerData.CanQuit) { menuItem = menu.newMenuItem(menu); menuItem.text = i18nc("Quit media player app", "Quit"); menuItem.icon = "application-exit"; menuItem.visible = Qt.binding(function() { return !closeWindowItem.visible; }); menuItem.clicked.connect(function() { mpris2Source.quit(sourceName); }); menu.addMenuItem(menuItem); } // If we don't have a window associated with the player but we can raise // it through MPRIS we'll offer a "Restore" option if (!startNewInstanceItem.visible && playerData.CanRaise) { menuItem = menu.newMenuItem(menu); menuItem.text = i18nc("Open or bring to the front window of media player app", "Restore"); menuItem.icon = playerData["Desktop Icon Name"]; menuItem.visible = Qt.binding(function() { return !startNewInstanceItem.visible; }); menuItem.clicked.connect(function() { mpris2Source.raise(sourceName); }); menu.addMenuItem(menuItem, startNewInstanceItem); } } } // We allow mute/unmute whenever an application has a stream, regardless of whether it // is actually playing sound. // This way you can unmute, e.g. a telephony app, even after the conversation has ended, // so you still have it ringing later on. if (menu.visualParent.hasAudioStream) { var muteItem = menu.newMenuItem(menu); muteItem.checkable = true; muteItem.checked = Qt.binding(function() { return menu.visualParent && menu.visualParent.muted; }); muteItem.clicked.connect(function() { menu.visualParent.toggleMuted(); }); muteItem.text = i18n("Mute"); muteItem.icon = "audio-volume-muted"; - menu.addMenuItem(muteItem, virtualDesktopsMenuItem); + menu.addMenuItem(muteItem, startNewInstanceItem); - menu.addMenuItem(newSeparator(menu), virtualDesktopsMenuItem); + menu.addMenuItem(newSeparator(menu), startNewInstanceItem); } } PlasmaComponents.MenuItem { + id: startNewInstanceItem + visible: (visualParent && get(atm.IsLauncher) !== true && get(atm.IsStartup) !== true) + + enabled: visualParent && get(atm.LauncherUrlWithoutIcon) !== "" + + text: i18n("Start New Instance") + icon: "list-add-symbolic" + + onClicked: tasksModel.requestNewInstance(modelIndex) + } + + PlasmaComponents.MenuItem { id: virtualDesktopsMenuItem visible: virtualDesktopInfo.numberOfDesktops > 1 && (visualParent && get(atm.IsLauncher) !== true && get(atm.IsStartup) !== true && get(atm.IsVirtualDesktopsChangeable) === true) enabled: visible text: i18n("Move To &Desktop") Connections { target: virtualDesktopInfo onNumberOfDesktopsChanged: Qt.callLater(virtualDesktopsMenu.refresh) onDesktopIdsChanged: Qt.callLater(virtualDesktopsMenu.refresh) onDesktopNamesChanged: Qt.callLater(virtualDesktopsMenu.refresh) } PlasmaComponents.ContextMenu { id: virtualDesktopsMenu visualParent: virtualDesktopsMenuItem.action function refresh() { clearMenuItems(); if (virtualDesktopInfo.numberOfDesktops <= 1) { return; } var menuItem = menu.newMenuItem(virtualDesktopsMenu); menuItem.text = i18n("Move &To Current Desktop"); menuItem.enabled = Qt.binding(function() { return menu.visualParent && menu.get(atm.VirtualDesktops).indexOf(virtualDesktopInfo.currentDesktop) === -1; }); menuItem.clicked.connect(function() { tasksModel.requestVirtualDesktops(menu.modelIndex, [virtualDesktopInfo.currentDesktop]); }); menuItem = menu.newMenuItem(virtualDesktopsMenu); menuItem.text = i18n("&All Desktops"); menuItem.checkable = true; menuItem.checked = Qt.binding(function() { return menu.visualParent && menu.get(atm.IsOnAllVirtualDesktops) === true; }); menuItem.clicked.connect(function() { tasksModel.requestVirtualDesktops(menu.modelIndex, []); }); backend.setActionGroup(menuItem.action); menu.newSeparator(virtualDesktopsMenu); for (var i = 0; i < virtualDesktopInfo.desktopNames.length; ++i) { menuItem = menu.newMenuItem(virtualDesktopsMenu); menuItem.text = i18nc("1 = number of desktop, 2 = desktop name", "&%1 %2", i + 1, virtualDesktopInfo.desktopNames[i]); menuItem.checkable = true; menuItem.checked = Qt.binding((function(i) { return function() { return menu.visualParent && menu.get(atm.VirtualDesktops).indexOf(virtualDesktopInfo.desktopIds[i]) > -1 }; })(i)); menuItem.clicked.connect((function(i) { return function() { return tasksModel.requestVirtualDesktops(menu.modelIndex, [virtualDesktopInfo.desktopIds[i]]); }; })(i)); backend.setActionGroup(menuItem.action); } menu.newSeparator(virtualDesktopsMenu); menuItem = menu.newMenuItem(virtualDesktopsMenu); menuItem.text = i18n("&New Desktop"); menuItem.clicked.connect(function() { tasksModel.requestNewVirtualDesktop(menu.modelIndex); }); } Component.onCompleted: refresh() } } PlasmaComponents.MenuItem { id: activitiesDesktopsMenuItem visible: activityInfo.numberOfRunningActivities > 1 && (visualParent && !get(atm.IsLauncher) && !get(atm.IsStartup)) enabled: visible text: i18n("Move To &Activity") Connections { target: activityInfo onNumberOfRunningActivitiesChanged: activitiesDesktopsMenu.refresh() } PlasmaComponents.ContextMenu { id: activitiesDesktopsMenu visualParent: activitiesDesktopsMenuItem.action function refresh() { clearMenuItems(); if (activityInfo.numberOfRunningActivities <= 1) { return; } var menuItem = menu.newMenuItem(activitiesDesktopsMenu); menuItem.text = i18n("Add To Current Activity"); menuItem.enabled = Qt.binding(function() { return menu.visualParent && menu.get(atm.Activities).length > 0 && menu.get(atm.Activities).indexOf(activityInfo.currentActivity) < 0; }); menuItem.clicked.connect(function() { tasksModel.requestActivities(menu.modelIndex, menu.get(atm.Activities).concat(activityInfo.currentActivity)); }); menuItem = menu.newMenuItem(activitiesDesktopsMenu); menuItem.text = i18n("All Activities"); menuItem.checkable = true; menuItem.checked = Qt.binding(function() { return menu.visualParent && menu.get(atm.Activities).length === 0; }); menuItem.toggled.connect(function(checked) { var newActivities = undefined; // will cast to an empty QStringList i.e all activities if (!checked) { newActivities = new Array(activityInfo.currentActivity); } tasksModel.requestActivities(menu.modelIndex, newActivities); }); menu.newSeparator(activitiesDesktopsMenu); var runningActivities = activityInfo.runningActivities(); for (var i = 0; i < runningActivities.length; ++i) { var activityId = runningActivities[i]; menuItem = menu.newMenuItem(activitiesDesktopsMenu); menuItem.text = activityInfo.activityName(runningActivities[i]); menuItem.checkable = true; menuItem.checked = Qt.binding( (function(activityId) { return function() { return menu.visualParent && menu.get(atm.Activities).indexOf(activityId) >= 0; }; })(activityId)); menuItem.toggled.connect((function(activityId) { return function (checked) { var newActivities = menu.get(atm.Activities); if (checked) { newActivities = newActivities.concat(activityId); } else { var index = newActivities.indexOf(activityId) if (index < 0) { return; } newActivities.splice(index, 1); } return tasksModel.requestActivities(menu.modelIndex, newActivities); }; })(activityId)); } menu.newSeparator(activitiesDesktopsMenu); } Component.onCompleted: refresh() } } - PlasmaComponents.MenuItem { + id: moreActionsMenuItem + visible: (visualParent && get(atm.IsLauncher) !== true && get(atm.IsStartup) !== true) - enabled: visualParent && get(atm.IsMinimizable) === true + enabled: visible - checkable: true - checked: visualParent && get(atm.IsMinimized) === true + text: i18n("More Actions") + icon: "view-more-symbolic" - text: i18n("Mi&nimize") + PlasmaComponents.ContextMenu { + visualParent: moreActionsMenuItem.action - onClicked: tasksModel.requestToggleMinimized(modelIndex) - } + PlasmaComponents.MenuItem { + enabled: menu.visualParent && menu.get(atm.IsMovable) === true - PlasmaComponents.MenuItem { - visible: (visualParent && get(atm.IsLauncher) !== true && get(atm.IsStartup) !== true) + text: i18n("&Move") + icon: "transform-move" - enabled: visualParent && get(atm.IsMaximizable) === true + onClicked: tasksModel.requestMove(menu.modelIndex) + } - checkable: true - checked: visualParent && get(atm.IsMaximized) === true + PlasmaComponents.MenuItem { + enabled: menu.visualParent && menu.get(atm.IsResizable) === true - text: i18n("Ma&ximize") + text: i18n("Re&size") + icon: "transform-scale" - onClicked: tasksModel.requestToggleMaximized(modelIndex) - } + onClicked: tasksModel.requestResize(menu.modelIndex) + } - PlasmaComponents.MenuItem { - id: startNewInstanceItem - visible: (visualParent && get(atm.IsLauncher) !== true && get(atm.IsStartup) !== true) + PlasmaComponents.MenuItem { + visible: (menu.visualParent && get(atm.IsLauncher) !== true && get(atm.IsStartup) !== true) - enabled: visualParent && get(atm.LauncherUrlWithoutIcon) !== "" + enabled: menu.visualParent && get(atm.IsMaximizable) === true - text: i18n("Start New Instance") - icon: "system-run" + checkable: true + checked: menu.visualParent && get(atm.IsMaximized) === true - onClicked: tasksModel.requestNewInstance(modelIndex) + text: i18n("Ma&ximize") + icon: "window-maximize" + + onClicked: tasksModel.requestToggleMaximized(modelIndex) + } + + PlasmaComponents.MenuItem { + visible: (menu.visualParent && get(atm.IsLauncher) !== true && get(atm.IsStartup) !== true) + + enabled: menu.visualParent && get(atm.IsMinimizable) === true + + checkable: true + checked: menu.visualParent && get(atm.IsMinimized) === true + + text: i18n("Mi&nimize") + icon: "window-minimize" + + onClicked: tasksModel.requestToggleMinimized(modelIndex) + } + + PlasmaComponents.MenuItem { + checkable: true + checked: menu.visualParent && menu.get(atm.IsKeepAbove) === true + + text: i18n("Keep &Above Others") + icon: "window-keep-above" + + onClicked: tasksModel.requestToggleKeepAbove(menu.modelIndex) + } + + PlasmaComponents.MenuItem { + checkable: true + checked: menu.visualParent && menu.get(atm.IsKeepBelow) === true + + text: i18n("Keep &Below Others") + icon: "window-keep-below" + + onClicked: tasksModel.requestToggleKeepBelow(menu.modelIndex) + } + + PlasmaComponents.MenuItem { + enabled: menu.visualParent && menu.get(atm.IsFullScreenable) === true + + checkable: true + checked: menu.visualParent && menu.get(atm.IsFullScreen) === true + + text: i18n("&Fullscreen") + icon: "view-fullscreen" + + onClicked: tasksModel.requestToggleFullScreen(menu.modelIndex) + } + + PlasmaComponents.MenuItem { + enabled: menu.visualParent && menu.get(atm.IsShadeable) === true + + checkable: true + checked: menu.visualParent && menu.get(atm.IsShaded) === true + + text: i18n("&Shade") + icon: "window-shade" + + onClicked: tasksModel.requestToggleShaded(menu.modelIndex) + } + + PlasmaComponents.MenuItem { + separator: true + } + + PlasmaComponents.MenuItem { + visible: (plasmoid.configuration.groupingStrategy !== 0) && menu.get(atm.IsWindow) === true + + checkable: true + checked: menu.visualParent && menu.get(atm.IsGroupable) === true + + text: i18n("Allow this program to be grouped") + + onClicked: tasksModel.requestToggleGrouping(menu.modelIndex) + } + + PlasmaComponents.MenuItem { + separator: true + } + + PlasmaComponents.MenuItem { + property QtObject configureAction: null + + enabled: configureAction && configureAction.enabled + visible: configureAction && configureAction.visible + + text: configureAction ? configureAction.text : "" + icon: configureAction ? configureAction.icon : "" + + onClicked: configureAction.trigger() + + Component.onCompleted: configureAction = plasmoid.action("configure") + } + + PlasmaComponents.MenuItem { + property QtObject alternativesAction: null + + enabled: alternativesAction && alternativesAction.enabled + visible: alternativesAction && alternativesAction.visible + + text: alternativesAction ? alternativesAction.text : "" + icon: alternativesAction ? alternativesAction.icon : "" + + onClicked: alternativesAction.trigger() + + Component.onCompleted: alternativesAction = plasmoid.action("alternatives") + } + } } PlasmaComponents.MenuItem { id: launcherToggleAction visible: visualParent && get(atm.IsLauncher) !== true && get(atm.IsStartup) !== true && plasmoid.immutability !== PlasmaCore.Types.SystemImmutable && (activityInfo.numberOfRunningActivities < 2) enabled: visualParent && get(atm.LauncherUrlWithoutIcon) !== "" checkable: true - text: i18nc("Toggle action for showing a launcher button while the application is not running", "&Pin") + text: i18n("&Pin to Task Manager") + icon: "window-pin" onClicked: { if (tasksModel.launcherPosition(get(atm.LauncherUrlWithoutIcon)) !== -1) { tasksModel.requestRemoveLauncher(get(atm.LauncherUrlWithoutIcon)); } else { tasksModel.requestAddLauncher(get(atm.LauncherUrl)); } } } PlasmaComponents.MenuItem { id: showLauncherInActivitiesItem - text: i18n("&Pin") + text: i18n("&Pin to Task Manager") + icon: "window-pin" visible: visualParent && get(atm.IsLauncher) !== true && get(atm.IsStartup) !== true && plasmoid.immutability !== PlasmaCore.Types.SystemImmutable && (activityInfo.numberOfRunningActivities >= 2) Connections { target: activityInfo onNumberOfRunningActivitiesChanged: activitiesDesktopsMenu.refresh() } PlasmaComponents.ContextMenu { id: activitiesLaunchersMenu visualParent: showLauncherInActivitiesItem.action function refresh() { clearMenuItems(); if (menu.visualParent === null) return; var createNewItem = function(id, title, url, activities) { var result = menu.newMenuItem(activitiesLaunchersMenu); result.text = title; result.visible = true; result.checkable = true; result.checked = activities.some(function(activity) { return activity === id }); result.clicked.connect( function() { if (result.checked) { tasksModel.requestAddLauncherToActivity(url, id); } else { tasksModel.requestRemoveLauncherFromActivity(url, id); } } ); return result; } if (menu.visualParent === null) return; var url = menu.get(atm.LauncherUrlWithoutIcon); var activities = tasksModel.launcherActivities(url); var NULL_UUID = "00000000-0000-0000-0000-000000000000"; createNewItem(NULL_UUID, i18n("On All Activities"), url, activities); if (activityInfo.numberOfRunningActivities <= 1) { return; } createNewItem(activityInfo.currentActivity, i18n("On The Current Activity"), url, activities); menu.newSeparator(activitiesLaunchersMenu); var runningActivities = activityInfo.runningActivities(); runningActivities.forEach(function(id) { createNewItem(id, activityInfo.activityName(id), url, activities); }); } Component.onCompleted: { menu.onVisualParentChanged.connect(refresh); refresh(); } } } PlasmaComponents.MenuItem { visible: (visualParent && get(atm.IsLauncher) === true) && plasmoid.immutability !== PlasmaCore.Types.SystemImmutable - text: i18nc("Remove launcher button for application shown while it is not running", "Unpin") + text: i18n("Unpin from Task Manager") + icon: "window-unpin" onClicked: { tasksModel.requestRemoveLauncher(get(atm.LauncherUrlWithoutIcon)); } } - - PlasmaComponents.MenuItem { - id: moreActionsMenuItem - - visible: (visualParent && get(atm.IsLauncher) !== true && get(atm.IsStartup) !== true) - - enabled: visible - - text: i18n("More Actions") - - PlasmaComponents.ContextMenu { - visualParent: moreActionsMenuItem.action - - PlasmaComponents.MenuItem { - enabled: menu.visualParent && menu.get(atm.IsMovable) === true - - text: i18n("&Move") - icon: "transform-move" - - onClicked: tasksModel.requestMove(menu.modelIndex) - } - - PlasmaComponents.MenuItem { - enabled: menu.visualParent && menu.get(atm.IsResizable) === true - - text: i18n("Re&size") - - onClicked: tasksModel.requestResize(menu.modelIndex) - } - - PlasmaComponents.MenuItem { - checkable: true - checked: menu.visualParent && menu.get(atm.IsKeepAbove) === true - - text: i18n("Keep &Above Others") - icon: "go-up" - - onClicked: tasksModel.requestToggleKeepAbove(menu.modelIndex) - } - - PlasmaComponents.MenuItem { - checkable: true - checked: menu.visualParent && menu.get(atm.IsKeepBelow) === true - - text: i18n("Keep &Below Others") - icon: "go-down" - - onClicked: tasksModel.requestToggleKeepBelow(menu.modelIndex) - } - - PlasmaComponents.MenuItem { - enabled: menu.visualParent && menu.get(atm.IsFullScreenable) === true - - checkable: true - checked: menu.visualParent && menu.get(atm.IsFullScreen) === true - - text: i18n("&Fullscreen") - icon: "view-fullscreen" - - onClicked: tasksModel.requestToggleFullScreen(menu.modelIndex) - } - - PlasmaComponents.MenuItem { - enabled: menu.visualParent && menu.get(atm.IsShadeable) === true - - checkable: true - checked: menu.visualParent && menu.get(atm.IsShaded) === true - - text: i18n("&Shade") - - onClicked: tasksModel.requestToggleShaded(menu.modelIndex) - } - - PlasmaComponents.MenuItem { - separator: true - } - - PlasmaComponents.MenuItem { - visible: (plasmoid.configuration.groupingStrategy !== 0) && menu.get(atm.IsWindow) === true - - checkable: true - checked: menu.visualParent && menu.get(atm.IsGroupable) === true - - text: i18n("Allow this program to be grouped") - - onClicked: tasksModel.requestToggleGrouping(menu.modelIndex) - } - } - } - - PlasmaComponents.MenuItem { - separator: true - } - - PlasmaComponents.MenuItem { - property QtObject configureAction: null - - enabled: configureAction && configureAction.enabled - visible: configureAction && configureAction.visible - - text: configureAction ? configureAction.text : "" - icon: configureAction ? configureAction.icon : "" - - onClicked: configureAction.trigger() - - Component.onCompleted: configureAction = plasmoid.action("configure") - } - - PlasmaComponents.MenuItem { - property QtObject alternativesAction: null - - enabled: alternativesAction && alternativesAction.enabled - visible: alternativesAction && alternativesAction.visible - - text: alternativesAction ? alternativesAction.text : "" - icon: alternativesAction ? alternativesAction.icon : "" - - onClicked: alternativesAction.trigger() - - Component.onCompleted: alternativesAction = plasmoid.action("alternatives") - } - - PlasmaComponents.MenuItem { - separator: true - } - PlasmaComponents.MenuItem { id: closeWindowItem visible: (visualParent && get(atm.IsLauncher) !== true && get(atm.IsStartup) !== true) enabled: visualParent && get(atm.IsClosable) === true text: i18n("&Close") icon: "window-close" onClicked: tasksModel.requestClose(modelIndex) } } diff --git a/applets/taskmanager/plugin/backend.cpp b/applets/taskmanager/plugin/backend.cpp index 3bdfbeb41..c134bd086 100644 --- a/applets/taskmanager/plugin/backend.cpp +++ b/applets/taskmanager/plugin/backend.cpp @@ -1,586 +1,587 @@ /*************************************************************************** * Copyright (C) 2012-2016 by Eike Hein * * * * 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 "backend.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace KAStats = KActivities::Stats; using namespace KAStats; using namespace KAStats::Terms; Backend::Backend(QObject* parent) : QObject(parent) , m_panelWinId(0) , m_highlightWindows(false) , m_actionGroup(new QActionGroup(this)) { } Backend::~Backend() { } QQuickItem *Backend::taskManagerItem() const { return m_taskManagerItem; } void Backend::setTaskManagerItem(QQuickItem* item) { if (item != m_taskManagerItem) { m_taskManagerItem = item; emit taskManagerItemChanged(); } } QQuickItem *Backend::toolTipItem() const { return m_toolTipItem; } void Backend::setToolTipItem(QQuickItem *item) { if (item != m_toolTipItem) { m_toolTipItem = item; connect(item, SIGNAL(windowChanged(QQuickWindow*)), this, SLOT(toolTipWindowChanged(QQuickWindow*))); emit toolTipItemChanged(); } } QQuickWindow *Backend::groupDialog() const { return m_groupDialog; } void Backend::setGroupDialog(QQuickWindow *dialog) { if (dialog != m_groupDialog) { m_groupDialog = dialog; emit groupDialogChanged(); } } bool Backend::highlightWindows() const { return m_highlightWindows; } void Backend::setHighlightWindows(bool highlight) { if (highlight != m_highlightWindows) { m_highlightWindows = highlight; updateWindowHighlight(); emit highlightWindowsChanged(); } } QUrl Backend::tryDecodeApplicationsUrl(const QUrl &launcherUrl) { if (launcherUrl.isValid() && launcherUrl.scheme() == QStringLiteral("applications")) { const KService::Ptr service = KService::serviceByMenuId(launcherUrl.path()); if (service) { return QUrl::fromLocalFile(service->entryPath()); } } return launcherUrl; } QVariantList Backend::jumpListActions(const QUrl &launcherUrl, QObject *parent) { if (!parent) { return QVariantList(); } QUrl desktopEntryUrl = tryDecodeApplicationsUrl(launcherUrl); if (!desktopEntryUrl.isValid() || !desktopEntryUrl.isLocalFile() || !KDesktopFile::isDesktopFile(desktopEntryUrl.toLocalFile())) { return QVariantList(); } QVariantList actions; KDesktopFile desktopFile(desktopEntryUrl.toLocalFile()); const QStringList &jumpListActions = desktopFile.readActions(); const QLatin1String kde("KDE"); foreach (const QString &actionName, jumpListActions) { const KConfigGroup &actionGroup = desktopFile.actionGroup(actionName); if (!actionGroup.isValid() || !actionGroup.exists()) { continue; } const QStringList ¬ShowIn = actionGroup.readXdgListEntry(QStringLiteral("NotShowIn")); if (notShowIn.contains(kde)) { continue; } const QStringList &onlyShowIn = actionGroup.readXdgListEntry(QStringLiteral("OnlyShowIn")); if (!onlyShowIn.isEmpty() && !onlyShowIn.contains(kde)) { continue; } const QString &name = actionGroup.readEntry(QStringLiteral("Name")); const QString &exec = actionGroup.readEntry(QStringLiteral("Exec")); if (name.isEmpty() || exec.isEmpty()) { continue; } QAction *action = new QAction(parent); action->setText(name); action->setIcon(QIcon::fromTheme(actionGroup.readEntry("Icon"))); action->setProperty("exec", exec); // so we can show the proper application name and icon when it launches action->setProperty("applicationName", desktopFile.readName()); action->setProperty("applicationIcon", desktopFile.readIcon()); connect(action, &QAction::triggered, this, &Backend::handleJumpListAction); actions << QVariant::fromValue(action); } return actions; } QVariantList Backend::placesActions(const QUrl &launcherUrl, bool showAllPlaces, QObject *parent) { if (!parent) { return QVariantList(); } QUrl desktopEntryUrl = tryDecodeApplicationsUrl(launcherUrl); if (!desktopEntryUrl.isValid() || !desktopEntryUrl.isLocalFile() || !KDesktopFile::isDesktopFile(desktopEntryUrl.toLocalFile())) { return QVariantList(); } QVariantList actions; KDesktopFile desktopFile(desktopEntryUrl.toLocalFile()); // Since we can't have dynamic jump list actions, at least add the user's "Places" for file managers. const QStringList &categories = desktopFile.desktopGroup().readXdgListEntry(QStringLiteral("Categories")); if (!categories.contains(QLatin1String("FileManager"))) { return actions; } QString previousGroup; QMenu *subMenu = nullptr; QScopedPointer placesModel(new KFilePlacesModel()); for (int i = 0; i < placesModel->rowCount(); ++i) { QModelIndex idx = placesModel->index(i, 0); if (placesModel->isHidden(idx)) { continue; } const QString &title = idx.data(Qt::DisplayRole).toString(); const QIcon &icon = idx.data(Qt::DecorationRole).value(); const QUrl &url = idx.data(KFilePlacesModel::UrlRole).toUrl(); QAction *placeAction = new QAction(icon, title, parent); connect(placeAction, &QAction::triggered, this, [url, desktopEntryUrl] { KService::Ptr service = KService::serviceByDesktopPath(desktopEntryUrl.toLocalFile()); if (!service) { return; } KRun::runService(*service, {url}, QApplication::activeWindow()); }); const QString &groupName = idx.data(KFilePlacesModel::GroupRole).toString(); if (previousGroup.isEmpty()) { // Skip first group heading. previousGroup = groupName; } // Put all subsequent categories into a submenu. if (previousGroup != groupName) { QAction *subMenuAction = new QAction(groupName, parent); subMenu = new QMenu(); // Cannot parent a QMenu to a QAction, need to delete it manually. connect(parent, &QObject::destroyed, subMenu, &QObject::deleteLater); subMenuAction->setMenu(subMenu); actions << QVariant::fromValue(subMenuAction); previousGroup = groupName; } if (subMenu) { subMenu->addAction(placeAction); } else { actions << QVariant::fromValue(placeAction); } } // There is nothing more frustrating than having a "More" entry that ends up showing just one or two // additional entries. Therefore we truncate to max. 5 entries only if there are more than 7 in total. if (!showAllPlaces && actions.count() > 7) { const int totalActionCount = actions.count(); while (actions.count() > 5) { actions.removeLast(); } QAction *action = new QAction(parent); action->setText(i18ncp("Show all user Places", "%1 more Place", "%1 more Places", totalActionCount - actions.count())); connect(action, &QAction::triggered, this, &Backend::showAllPlaces); actions << QVariant::fromValue(action); } return actions; } QVariantList Backend::recentDocumentActions(const QUrl &launcherUrl, QObject *parent) { if (!parent) { return QVariantList(); } QUrl desktopEntryUrl = tryDecodeApplicationsUrl(launcherUrl); if (!desktopEntryUrl.isValid() || !desktopEntryUrl.isLocalFile() || !KDesktopFile::isDesktopFile(desktopEntryUrl.toLocalFile())) { return QVariantList(); } QVariantList actions; QString desktopName = desktopEntryUrl.fileName(); QString storageId = desktopName; if (storageId.endsWith(QLatin1String(".desktop"))) { storageId = storageId.left(storageId.length() - 8); } auto query = UsedResources | RecentlyUsedFirst | Agent(storageId) | Type::any() | Activity::current() | Url::file(); ResultSet results(query); ResultSet::const_iterator resultIt = results.begin(); int actionCount = 0; while (actionCount < 5 && resultIt != results.end()) { const QString resource = (*resultIt).resource(); const QUrl url(resource); if (!url.isValid()) { continue; } const KFileItem fileItem(url); if (!fileItem.isFile()) { continue; } QAction *action = new QAction(parent); action->setText(url.fileName()); action->setIcon(QIcon::fromTheme(fileItem.iconName(), QIcon::fromTheme(QStringLiteral("unknown")))); action->setProperty("agent", storageId); action->setProperty("entryPath", desktopEntryUrl); action->setData(resource); connect(action, &QAction::triggered, this, &Backend::handleRecentDocumentAction); actions << QVariant::fromValue(action); ++resultIt; ++actionCount; } if (actionCount > 0) { QAction *action = new QAction(parent); action->setText(i18n("Forget Recent Documents")); + action->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-history"))); action->setProperty("agent", storageId); connect(action, &QAction::triggered, this, &Backend::handleRecentDocumentAction); actions << QVariant::fromValue(action); } return actions; } void Backend::toolTipWindowChanged(QQuickWindow *window) { Q_UNUSED(window) updateWindowHighlight(); } void Backend::handleJumpListAction() const { const QAction *action = qobject_cast(sender()); if (!action) { return; } KRun::run(action->property("exec").toString(), {}, nullptr, action->property("applicationName").toString(), action->property("applicationIcon").toString()); } void Backend::handleRecentDocumentAction() const { const QAction *action = qobject_cast(sender()); if (!action) { return; } const QString agent = action->property("agent").toString(); if (agent.isEmpty()) { return; } const QString desktopPath = action->property("entryPath").toUrl().toLocalFile(); const QString resource = action->data().toString(); if (desktopPath.isEmpty() || resource.isEmpty()) { auto query = UsedResources | Agent(agent) | Type::any() | Activity::current() | Url::file(); KAStats::forgetResources(query); return; } KService::Ptr service = KService::serviceByDesktopPath(desktopPath); qDebug() << service; if (!service) { return; } KRun::runService(*service, QList() << QUrl(resource), QApplication::activeWindow()); } void Backend::setActionGroup(QAction *action) const { if (action) { action->setActionGroup(m_actionGroup); } } QRect Backend::globalRect(QQuickItem *item) const { if (!item || !item->window()) { return QRect(); } QRect iconRect(item->x(), item->y(), item->width(), item->height()); iconRect.moveTopLeft(item->parentItem()->mapToScene(iconRect.topLeft()).toPoint()); iconRect.moveTopLeft(item->window()->mapToGlobal(iconRect.topLeft())); return iconRect; } void Backend::ungrabMouse(QQuickItem *item) const { //this is a workaround where Qt will fail to realize a mouse has been released // this happens if a window which does not accept focus spawns a new window that takes focus and X grab // whilst the mouse is depressed // https://bugreports.qt.io/browse/QTBUG-59044 // this causes the next click to go missing //by releasing manually we avoid that situation auto ungrabMouseHack = [item]() { if (item && item->window() && item->window()->mouseGrabberItem()) { item->window()->mouseGrabberItem()->ungrabMouse(); } }; //pre 5.8.0 QQuickWindow code is "item->grabMouse(); sendEvent(item, mouseEvent)" //post 5.8.0 QQuickWindow code is sendEvent(item, mouseEvent); item->grabMouse() if (QVersionNumber::fromString(QString::fromLatin1(qVersion())) > QVersionNumber(5, 8, 0)) { QTimer::singleShot(0, item, ungrabMouseHack); } else { ungrabMouseHack(); } //end workaround } bool Backend::canPresentWindows() const { return (KWindowSystem::compositingActive() && KWindowEffects::isEffectAvailable(KWindowEffects::PresentWindowsGroup)); } void Backend::presentWindows(const QVariant &_winIds) { if (!m_taskManagerItem || !m_taskManagerItem->window()) { return; } QList winIds; const QVariantList &_winIdsList = _winIds.toList(); foreach(const QVariant &_winId, _winIdsList) { bool ok = false; qlonglong winId = _winId.toLongLong(&ok); if (ok) { winIds.append(winId); } } if (winIds.isEmpty()) { return; } if (m_windowsToHighlight.count()) { m_windowsToHighlight.clear(); updateWindowHighlight(); } KWindowEffects::presentWindows(m_taskManagerItem->window()->winId(), winIds); } bool Backend::isApplication(const QUrl &url) const { if (!url.isValid() || !url.isLocalFile()) { return false; } const QString &localPath = url.toLocalFile(); if (!KDesktopFile::isDesktopFile(localPath)) { return false; } KDesktopFile desktopFile(localPath); return desktopFile.hasApplicationType(); } QList Backend::jsonArrayToUrlList(const QJsonArray &array) const { QList urls; urls.reserve(array.count()); for (auto it = array.constBegin(), end = array.constEnd(); it != end; ++it) { urls << QUrl(it->toString()); } return urls; } void Backend::cancelHighlightWindows() { m_windowsToHighlight.clear(); updateWindowHighlight(); } void Backend::windowsHovered(const QVariant &_winIds, bool hovered) { m_windowsToHighlight.clear(); if (hovered) { const QVariantList &winIds = _winIds.toList(); foreach(const QVariant &_winId, winIds) { bool ok = false; qlonglong winId = _winId.toLongLong(&ok); if (ok) { m_windowsToHighlight.append(winId); } } } updateWindowHighlight(); } void Backend::updateWindowHighlight() { if (!m_highlightWindows) { if (m_panelWinId) { KWindowEffects::highlightWindows(m_panelWinId, QList()); m_panelWinId = 0; } return; } if (m_taskManagerItem && m_taskManagerItem->window()) { m_panelWinId = m_taskManagerItem->window()->winId(); } else { return; } QList windows = m_windowsToHighlight; if (!windows.isEmpty() && m_toolTipItem && m_toolTipItem->window()) { windows.append(m_toolTipItem->window()->winId()); } if (!windows.isEmpty() && m_groupDialog) { windows.append(m_groupDialog->winId()); } KWindowEffects::highlightWindows(m_panelWinId, windows); }