/* SPDX-FileCopyrightText: 2012-2016 Eike Hein SPDX-FileCopyrightText: 2016 Kai Uwe Broulik SPDX-License-Identifier: GPL-2.0-or-later */ import QtQuick 2.15 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.4 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 import org.kde.plasma.extras 2.0 as PlasmaExtras /* * This is the custom context menu control for SevenTasks. * It is designed to look and feel like the context menu from Windows Vista and onwards, * while making sure that it behaves just like a normal context menu under KDE. This means: * * 1. The context menu grabs *all* mouse and key inputs. * 2. The context menu must disappear if an event outside of it causes it to 'lose focus'. * 3. The context menu must disappear if a menu item has been activated either with the mouse or the keyboard. * 4. The context menu must disappear if the user clicks away from it or the Escape key is pressed on the keyboard. * * As PlasmaCore.Dialog inherits QWindow, we can use QWindow::setMouseGrabEnabled(bool) and QWindow::setKeyboardGrabEnabled(bool) * to steal all mouse and keyboard events from the system and direct it towards the context menu. This is done through C++, more * info in the C++ source files. * */ PlasmaCore.Dialog { id: tasksMenu // Properties passed by the task when the context menu is created dynamically. // Context menu specific stuff. property QtObject backend property QtObject mpris2Source property var modelIndex readonly property var atm: TaskManager.AbstractTasksModel property var menuDecoration: "exec" property QtObject currentItem: null property int currentItemIndex: -1 readonly property int menuItemHeight: PlasmaCore.Units.smallSpacing*5 readonly property int menuWidth: 263 property bool showAllPlaces: false property bool alsoCloseTask: false property bool secondaryColumn: false property color backgroundColorStatic: "#f1f6fb" property color backgroundColorGradient: "white" property color borderColor: "#ccd9ea" // Functions inherited from the original ContextMenu function get(modelProp) { return tasksModel.data(modelIndex, modelProp) } function showContextMenuWithAllPlaces() { visualParent.showContextMenu({showAllPlaces: true}); } function newPlasmaMenuItem(parent) { return Qt.createQmlObject(` import org.kde.plasma.components 2.0 as PlasmaComponents PlasmaComponents.MenuItem {} `, parent); } function newPlasmaSeparator(parent) { return Qt.createQmlObject(` import org.kde.plasma.components 2.0 as PlasmaComponents PlasmaComponents.MenuItem { separator: true } `, parent); } function newMenuItem(parent) { return Qt.createQmlObject(` TasksMenuItemWrapper {} `, parent); } function newSeparator(parent) { return Qt.createQmlObject(` TasksMenuItemSeparator {} `, parent); } function addItemToMenu(obj) { obj.Layout.fillWidth = true; obj.Layout.preferredHeight = menuItemHeight; menuitems.height += obj.Layout.preferredHeight + menuitems.spacing; } function clearIndices() { if(currentItem !== null) { currentItem.selected = false; currentItem = null; } currentItemIndex = -1; } function setCurrentItem(obj) { clearIndices(); var i = Array.prototype.indexOf.call(menuitems.children, obj); if(i === -1) { i = menuitems.children.length + Array.prototype.indexOf.call(staticMenuItems.children, obj); } currentItemIndex = i; currentItem = obj; currentItem.selected = true; } // Tasksmenu specific stuff property alias tMenu: tasksMenu property int xpos: -1 // Variable is used to keep track of the original x position which sometimes gets changed for no reason. visible: false opacity: 0 objectName: "tasksMenuDialog" hideOnWindowDeactivate: true // Makes it so that the context menu disappears if it gets forcibly out of focus by an external event. flags: Qt.WindowStaysOnTopHint | Qt.Dialog // Used to animate the context menu appearing and disappearing. Behavior on opacity { NumberAnimation { duration: 125; } } Behavior on y { NumberAnimation {duration: 125; } } // Tries to detect when the x position resets to 0. onXChanged: { if(tasksMenu.x !== xpos) { tasksMenu.x = xpos; } } // If the context menu is no longer visible (most often when it loses focus), close the menu. onVisibleChanged: { if(visible) { var diff = parent.mapToGlobal(tasksMenu.x, tasksMenu.y).x - tasksMenu.x; xpos = visualParent.x + diff + visualParent.width/2 - PlasmaCore.Units.smallSpacing; xpos -= menuWidth / 2; if(xpos < 0) xpos = 0; tasksMenu.x = xpos; plasmoid.nativeInterface.setMouseGrab(true, tasksMenu); } else if(!visible) { tasksMenu.closeMenu(); } } onActiveChanged: { if(!active) tasksMenu.close(); } // Set to Floating so that the borders are visible all the time, even when it is right next to another object. location: { return PlasmaCore.Types.Floating; } // Used publicly by other objects to show the dynamically created context menu. function show() { loadDynamicLauncherActions(get(atm.LauncherUrlWithoutIcon)); visible = true; opacity = 1; tasksMenu.y -= PlasmaCore.Units.smallSpacing*2; openTimer.start(); } // Closes the menu gracefully, by first showing a fade out animation before freeing the object from memory. function closeMenu() { plasmoid.nativeInterface.disableBlurBehind(tasksMenu); tasksMenu.y += PlasmaCore.Units.smallSpacing*2; opacity = 0; closeTimer.start(); } function loadDynamicLauncherActions(launcherUrl) { var sections = [ { title: i18n("Frequent"), group: "places", actions: backend.placesActions(launcherUrl, showAllPlaces, tasksMenu) }, { title: i18n("Recent"), group: "recents", actions: backend.recentDocumentActions(launcherUrl, tasksMenu) }, { title: i18n("Tasks"), group: "actions", actions: backend.jumpListActions(launcherUrl, tasksMenu) } ] // C++ can override section heading by returning a QString as first action sections.forEach((section) => { if (typeof section.actions[0] === "string") { section.title = section.actions.shift(); // take first } }); // 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 {}", menuitems); var maximumWidth = LayoutManager.maximumContextMenuTextWidth() + PlasmaCore.Units.smallSpacing*2; for(var i = 0; i < sections.length; i++) { var section = sections[i]; if(section["actions"].length == 0) continue; // Make a separator header var sepHeader = tasksMenu.newSeparator(menuitems); sepHeader.menuText = section["title"]; addItemToMenu(sepHeader); for(var j = 0; j < section["actions"].length; j++) { if(section["group"] == "recents" && j == section["actions"].length-2) continue; var mAction = section["actions"][j]; var mItem = tasksMenu.newMenuItem(menuitems); // Crude way of manually eliding... var elided = false; textMetrics.text = Qt.binding(function() { return mAction.text; }); while (textMetrics.width > maximumWidth) { mAction.text = mAction.text.slice(0, -1); elided = true; } if (elided) { mAction.text += "…"; } mItem.text = mAction.text; mItem.icon = mAction.icon; mItem.clicked.connect(mAction.trigger); addItemToMenu(mItem); secondaryColumn = true; } } // 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] var sepHeader = tasksMenu.newSeparator(menuitems); sepHeader.menuText = "Media controls"; addItemToMenu(sepHeader); secondaryColumn = true; if (playerData.CanControl) { var playing = (playerData.PlaybackStatus === "Playing"); var menuItem = tasksMenu.newMenuItem(menuitems); 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); tasksMenu.closeMenu(); }); tasksMenu.addItemToMenu(menuItem); menuItem = tasksMenu.newMenuItem(menuitems); // 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); } tasksMenu.closeMenu(); }); tasksMenu.addItemToMenu(menuItem); menuItem = tasksMenu.newMenuItem(menuitems); 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); tasksMenu.closeMenu(); }); tasksMenu.addItemToMenu(menuItem); menuItem = tasksMenu.newMenuItem(menuitems); 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); tasksMenu.closeMenu(); }); tasksMenu.addItemToMenu(menuItem); // 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 = tasksMenu.newMenuItem(menuitems); 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); tasksMenu.closeMenu(); }); tasksMenu.addItemToMenu(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 (get(atm.IsLauncher) === true && !startNewInstanceItem.visible && playerData.CanRaise) { menuItem = tasksMenu.newMenuItem(menuitems); 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); tasksMenu.closeMenu(); }); tasksMenu.addItemToMenu(menuItem); } } } // 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 (tasksMenu.visualParent.hasAudioStream) { var muteItem = tasksMenu.newMenuItem(menuitems); muteItem.checkable = true; muteItem.checked = Qt.binding(function() { return tasksMenu.visualParent && tasksMenu.visualParent.muted; }); muteItem.clicked.connect(function() { tasksMenu.visualParent.toggleMuted(); muteItem.text = !muteItem.checked ? "Unmute" : "Mute"; muteItem.icon = !muteItem.checked ? "audio-volume-muted" : "audio-volume-high"; }); muteItem.text = muteItem.checked ? "Unmute" : "Mute"; muteItem.icon = muteItem.checked ? "audio-volume-muted" : "audio-volume-high"; tasksMenu.addItemToMenu(muteItem); secondaryColumn = true; } } function delayedMenu(delay, func) { plasmoid.nativeInterface.disableBlurBehind(tasksMenu); tasksMenu.y += PlasmaCore.Units.smallSpacing*2; opacity = 0; delayTimer.interval = delay; delayTimer.repeat = false; delayTimer.triggered.connect(func); delayTimer.start(); } FocusScope { id: fscope focus: true Layout.minimumWidth: menuWidth Layout.maximumWidth: menuWidth Layout.minimumHeight: staticMenuItems.height + menuitems.height + PlasmaCore.Units.smallSpacing*3 - (secondaryColumn ? 0 : PlasmaCore.Units.smallSpacing*2) Layout.maximumHeight: staticMenuItems.height + menuitems.height + PlasmaCore.Units.smallSpacing*3 - (secondaryColumn ? 0 : PlasmaCore.Units.smallSpacing*2) // This is the last resort to avoiding the dialog displacement bug. It's set to correct the x position at a delay of 18ms. // This may result in a brief but noticeable jump in position when the context menu is shown. Timer { id: delayTimer } Timer { id: openTimer interval: 20 repeat: false onTriggered: { tasksMenu.x = xpos; plasmoid.nativeInterface.setMouseGrab(true, tasksMenu); } } // Timer used to free the object from memory after the fade out animation has finished. Timer { id: closeTimer interval: 150 onTriggered: { tasksMenu.destroy(); } } ColumnLayout { id: menuitems z: 1 spacing: PlasmaCore.Units.smallSpacing/2 anchors { left: parent.left right: parent.right bottom: staticMenuItems.top leftMargin: PlasmaCore.Units.smallSpacing*2 rightMargin: PlasmaCore.Units.smallSpacing*2 bottomMargin: PlasmaCore.Units.smallSpacing } Item { Layout.fillHeight: true } Item { height: PlasmaCore.Units.smallSpacing } } ColumnLayout { id: staticMenuItems z: 1 spacing: PlasmaCore.Units.smallSpacing/2 anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: PlasmaCore.Units.smallSpacing*2 Item { Layout.fillHeight: true } Item { height: PlasmaCore.Units.smallSpacing } TasksMenuItemWrapper { id: startNewInstanceItem visible: get(atm.CanLaunchNewInstance) text: get(atm.AppName) icon: menuDecoration onClicked: tasksModel.requestNewInstance(modelIndex) Layout.fillWidth: true Layout.preferredHeight: menuItemHeight } TasksMenuItemWrapper { id: virtualDesktopsMenuItem Layout.fillWidth: true Layout.preferredHeight: menuItemHeight text: i18n("Move to Desktop...") icon: "virtual-desktops" visible: virtualDesktopInfo.numberOfDesktops > 1 && (visualParent && get(atm.IsLauncher) !== true && get(atm.IsStartup) !== true && get(atm.IsVirtualDesktopsChangeable) === true) onClicked: virtualDesktopsMenu.openRelative() Connections { target: virtualDesktopInfo function onNumberOfDesktopsChanged() {Qt.callLater(virtualDesktopsMenu.refresh)} function onDesktopIdsChanged() {Qt.callLater(virtualDesktopsMenu.refresh)} function onDesktopNamesChanged() {Qt.callLater(virtualDesktopsMenu.refresh)} } PlasmaComponents.ContextMenu { id: virtualDesktopsMenu visualParent: virtualDesktopsMenuItem onTriggered: { tasksMenu.closeMenu(); } function refresh() { clearMenuItems(); if (virtualDesktopInfo.numberOfDesktops <= 1) { return; } var menuItem = tasksMenu.newPlasmaMenuItem(virtualDesktopsMenu); menuItem.text = i18n("Move &To Current Desktop"); menuItem.enabled = Qt.binding(function() { return tasksMenu.visualParent && tasksMenu.get(atm.VirtualDesktops).indexOf(virtualDesktopInfo.currentDesktop) === -1; }); menuItem.clicked.connect(function() { tasksModel.requestVirtualDesktops(tasksMenu.modelIndex, [virtualDesktopInfo.currentDesktop]); }); menuItem = tasksMenu.newPlasmaMenuItem(virtualDesktopsMenu); menuItem.text = i18n("&All Desktops"); menuItem.checkable = true; menuItem.checked = Qt.binding(function() { return tasksMenu.visualParent && tasksMenu.get(atm.IsOnAllVirtualDesktops) === true; }); menuItem.clicked.connect(function() { tasksModel.requestVirtualDesktops(tasksMenu.modelIndex, []); }); backend.setActionGroup(menuItem.action); tasksMenu.newPlasmaSeparator(virtualDesktopsMenu); for (var i = 0; i < virtualDesktopInfo.desktopNames.length; ++i) { menuItem = tasksMenu.newPlasmaMenuItem(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 tasksMenu.visualParent && tasksMenu.get(atm.VirtualDesktops).indexOf(virtualDesktopInfo.desktopIds[i]) > -1 }; })(i)); menuItem.clicked.connect((function(i) { return function() { return tasksModel.requestVirtualDesktops(tasksMenu.modelIndex, [virtualDesktopInfo.desktopIds[i]]); }; })(i)); backend.setActionGroup(menuItem.action); } tasksMenu.newPlasmaSeparator(virtualDesktopsMenu); menuItem = tasksMenu.newPlasmaMenuItem(virtualDesktopsMenu); menuItem.text = i18n("&New Desktop"); menuItem.clicked.connect(function() { tasksModel.requestNewVirtualDesktop(tasksMenu.modelIndex); }); } // Return mouse grabbing to the original context menu when the context menu closes onStatusChanged: { if(status == 3) { plasmoid.nativeInterface.setMouseGrab(true, tasksMenu); } } Component.onCompleted: { if(virtualDesktopsMenuItem.visible) refresh() } } } TasksMenuItemWrapper { id: activitiesDesktopsMenuItem Layout.fillWidth: true Layout.preferredHeight: menuItemHeight visible: activityInfo.numberOfRunningActivities > 1 && (visualParent && !get(atm.IsLauncher) && !get(atm.IsStartup)) enabled: visible text: i18n("Show in Activities...") icon: "activities" onClicked: activitiesDesktopsMenu.openRelative() Connections { target: activityInfo function onNumberOfRunningActivitiesChanged() { activitiesDesktopsMenu.refresh() } } PlasmaComponents.ContextMenu { id: activitiesDesktopsMenu visualParent: activitiesDesktopsMenuItem onTriggered: { tasksMenu.closeMenu(); } function refresh() { clearMenuItems(); if (activityInfo.numberOfRunningActivities <= 1) { return; } var menuItem = tasksMenu.newPlasmaMenuItem(activitiesDesktopsMenu); menuItem.text = i18n("Add To Current Activity"); menuItem.enabled = Qt.binding(function() { return tasksMenu.visualParent && tasksMenu.get(atm.Activities).length > 0 && tasksMenu.get(atm.Activities).indexOf(activityInfo.currentActivity) < 0; }); menuItem.clicked.connect(function() { tasksModel.requestActivities(tasksMenu.modelIndex, tasksMenu.get(atm.Activities).concat(activityInfo.currentActivity)); }); menuItem = tasksMenu.newPlasmaMenuItem(activitiesDesktopsMenu); menuItem.text = i18n("All Activities"); menuItem.checkable = true; menuItem.checked = Qt.binding(function() { return tasksMenu.visualParent && tasksMenu.get(atm.Activities).length === 0; }); menuItem.toggled.connect(function(checked) { let newActivities = []; // will cast to an empty QStringList i.e all activities if (!checked) { newActivities = new Array(activityInfo.currentActivity); } tasksModel.requestActivities(tasksMenu.modelIndex, newActivities); }); tasksMenu.newPlasmaSeparator(activitiesDesktopsMenu); var runningActivities = activityInfo.runningActivities(); for (var i = 0; i < runningActivities.length; ++i) { var activityId = runningActivities[i]; menuItem = tasksMenu.newPlasmaMenuItem(activitiesDesktopsMenu); menuItem.text = activityInfo.activityName(runningActivities[i]); menuItem.checkable = true; menuItem.checked = Qt.binding( (function(activityId) { return function() { return tasksMenu.visualParent && tasksMenu.get(atm.Activities).indexOf(activityId) >= 0; }; })(activityId)); menuItem.toggled.connect((function(activityId) { return function (checked) { var newActivities = tasksMenu.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(tasksMenu.modelIndex, newActivities); }; })(activityId)); } tasksMenu.newPlasmaSeparator(activitiesDesktopsMenu); for (var i = 0; i < runningActivities.length; ++i) { var activityId = runningActivities[i]; var onActivities = tasksMenu.get(atm.Activities); // if the task is on a single activity, don't insert a "move to" item for that activity if(onActivities.length == 1 && onActivities[0] == activityId) { continue; } menuItem = tasksMenu.newPlasmaMenuItem(activitiesDesktopsMenu); menuItem.text = i18n("Move to %1", activityInfo.activityName(activityId)) menuItem.clicked.connect((function(activityId) { return function () { return tasksModel.requestActivities(tasksMenu.modelIndex, [activityId]); }; })(activityId)); } tasksMenu.newPlasmaSeparator(activitiesDesktopsMenu); } onStatusChanged: { if(status == 3) { plasmoid.nativeInterface.setMouseGrab(true, tasksMenu); } } Component.onCompleted: { if(activitiesDesktopsMenuItem.visible) refresh() } } } TasksMenuItemWrapper { id: launcherToggleAction Layout.fillWidth: true Layout.preferredHeight: menuItemHeight text: i18n("Pin this program to taskbar") icon: "window-pin" visible: visualParent && get(atm.IsLauncher) !== true && get(atm.IsStartup) !== true && plasmoid.immutability !== PlasmaCore.Types.SystemImmutable && (activityInfo.numberOfRunningActivities < 2) && !doesBelongToCurrentActivity() function doesBelongToCurrentActivity() { return tasksModel.launcherActivities(get(atm.LauncherUrlWithoutIcon)).some(function(activity) { return activity === activityInfo.currentActivity || activity === activityInfo.nullUuid; }); } onClicked: { tasksModel.requestAddLauncher(get(atm.LauncherUrl)); tasksMenu.closeMenu(); } } TasksMenuItemWrapper { id: showLauncherInActivitiesItem Layout.fillWidth: true Layout.preferredHeight: menuItemHeight text: i18n("Pin this program to taskbar") + "..." icon: "window-pin" visible: visualParent && get(atm.IsStartup) !== true && plasmoid.immutability !== PlasmaCore.Types.SystemImmutable && (activityInfo.numberOfRunningActivities >= 2) onClicked: activitiesLaunchersMenu.openRelative() Connections { target: activityInfo function onNumberOfRunningActivitiesChanged() { activitiesDesktopsMenu.refresh() } } PlasmaComponents.ContextMenu { id: activitiesLaunchersMenu visualParent: showLauncherInActivitiesItem function refresh() { clearMenuItems(); if (tasksMenu.visualParent === null) return; var createNewItem = function(id, title, url, activities) { var result = tasksMenu.newPlasmaMenuItem(activitiesLaunchersMenu); result.text = title; result.visible = true; result.checkable = true; result.checked = activities.some(function(activity) { return activity === id }); result.clicked.connect( function() { delayedMenu(150, function() { if (result.checked) { tasksModel.requestAddLauncherToActivity(url, id); } else { tasksModel.requestRemoveLauncherFromActivity(url, id); } tasksMenu.destroy(); }); } ); return result; } if (tasksMenu.visualParent === null) return; var url = tasksMenu.get(atm.LauncherUrlWithoutIcon); var activities = tasksModel.launcherActivities(url); createNewItem(activityInfo.nullUuid, i18n("On All Activities"), url, activities); if (activityInfo.numberOfRunningActivities <= 1) { return; } createNewItem(activityInfo.currentActivity, i18n("On The Current Activity"), url, activities); tasksMenu.newPlasmaSeparator(activitiesLaunchersMenu); var runningActivities = activityInfo.runningActivities(); runningActivities.forEach(function(id) { createNewItem(id, activityInfo.activityName(id), url, activities); }); } onStatusChanged: { if(status == 3) { plasmoid.nativeInterface.setMouseGrab(true, tasksMenu); } } Component.onCompleted: { tasksMenu.onVisualParentChanged.connect(refresh); refresh(); } } } TasksMenuItemWrapper { id: unpinFromTaskMan Layout.fillWidth: true Layout.preferredHeight: menuItemHeight visible: (visualParent && plasmoid.immutability !== PlasmaCore.Types.SystemImmutable && !launcherToggleAction.visible && activityInfo.numberOfRunningActivities < 2) text: i18n("Unpin this program from taskbar") icon: "window-unpin" onClicked: { delayedMenu(150, function() { tasksModel.requestRemoveLauncher(get(atm.LauncherUrlWithoutIcon)); tasksMenu.destroy(); }); } } TasksMenuItemWrapper { id: moreActionsMenuItem Layout.fillWidth: true Layout.preferredHeight: menuItemHeight visible: (visualParent && get(atm.IsLauncher) !== true && get(atm.IsStartup) !== true) enabled: visible text: i18n("More") + "..." icon: "view-more-symbolic" onClicked: moreActionsMenu.openRelative(); PlasmaComponents.ContextMenu { id: moreActionsMenu visualParent: moreActionsMenuItem onTriggered: { plasmoid.nativeInterface.setMouseGrab(false, tasksMenu); tasksMenu.closeMenu(); } onStatusChanged: { if(status == 3) { plasmoid.nativeInterface.setMouseGrab(true, tasksMenu); } } PlasmaComponents.MenuItem { enabled: tasksMenu.visualParent && tasksMenu.get(atm.IsMovable) === true text: i18n("&Move") icon: "transform-move" onClicked: tasksModel.requestMove(tasksMenu.modelIndex) } PlasmaComponents.MenuItem { enabled: tasksMenu.visualParent && tasksMenu.get(atm.IsResizable) === true text: i18n("Re&size") icon: "transform-scale" onClicked: tasksModel.requestResize(tasksMenu.modelIndex) } PlasmaComponents.MenuItem { visible: (tasksMenu.visualParent && get(atm.IsLauncher) !== true && get(atm.IsStartup) !== true) enabled: tasksMenu.visualParent && get(atm.IsMaximizable) === true checkable: true checked: tasksMenu.visualParent && get(atm.IsMaximized) === true text: i18n("Ma&ximize") icon: "window-maximize" onClicked: tasksModel.requestToggleMaximized(modelIndex) } PlasmaComponents.MenuItem { visible: (tasksMenu.visualParent && get(atm.IsLauncher) !== true && get(atm.IsStartup) !== true) enabled: tasksMenu.visualParent && get(atm.IsMinimizable) === true checkable: true checked: tasksMenu.visualParent && get(atm.IsMinimized) === true text: i18n("Mi&nimize") icon: "window-minimize" onClicked: tasksModel.requestToggleMinimized(modelIndex) } PlasmaComponents.MenuItem { checkable: true checked: tasksMenu.visualParent && tasksMenu.get(atm.IsKeepAbove) === true text: i18n("Keep &Above Others") icon: "window-keep-above" onClicked: tasksModel.requestToggleKeepAbove(tasksMenu.modelIndex) } PlasmaComponents.MenuItem { checkable: true checked: tasksMenu.visualParent && tasksMenu.get(atm.IsKeepBelow) === true text: i18n("Keep &Below Others") icon: "window-keep-below" onClicked: tasksModel.requestToggleKeepBelow(tasksMenu.modelIndex) } PlasmaComponents.MenuItem { enabled: tasksMenu.visualParent && tasksMenu.get(atm.IsFullScreenable) === true checkable: true checked: tasksMenu.visualParent && tasksMenu.get(atm.IsFullScreen) === true text: i18n("&Fullscreen") icon: "view-fullscreen" onClicked: tasksModel.requestToggleFullScreen(tasksMenu.modelIndex) } PlasmaComponents.MenuItem { enabled: tasksMenu.visualParent && tasksMenu.get(atm.IsShadeable) === true checkable: true checked: tasksMenu.visualParent && tasksMenu.get(atm.IsShaded) === true text: i18n("&Shade") icon: "window-shade" onClicked: tasksModel.requestToggleShaded(tasksMenu.modelIndex) } PlasmaComponents.MenuItem { separator: true } PlasmaComponents.MenuItem { visible: (plasmoid.configuration.groupingStrategy !== 0) && tasksMenu.get(atm.IsWindow) === true checkable: true checked: tasksMenu.visualParent && tasksMenu.get(atm.IsGroupable) === true text: i18n("Allow this program to be grouped") icon: "view-group" 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") } } } TasksMenuItemWrapper { id: closeWindowItem Layout.fillWidth: true Layout.preferredHeight: menuItemHeight visible: (visualParent && get(atm.IsLauncher) !== true && get(atm.IsStartup) !== true) enabled: visualParent && get(atm.IsClosable) === true text: get(atm.IsGroupParent) ? "Close all windows" : i18n("Close window") icon: "window-close" onClicked: { alsoCloseTask = true; closeMenu(); } } /*TasksMenuItemWrapper { id: testItem Layout.fillWidth: true Layout.preferredHeight: menuItemHeight text: "Test" icon: "window-close" onClicked: { } }*/ } Rectangle { id: bgRect visible: secondaryColumn anchors { top: parent.top bottom: bgStatic.top left: parent.left right: parent.right leftMargin: 2 rightMargin: 2 topMargin: 2 } gradient: Gradient { GradientStop { position: 0; color: backgroundColorStatic } GradientStop { position: 0.5; color: backgroundColorGradient } GradientStop { position: 1; color: backgroundColorStatic } } z: -2 } Rectangle { id: bgStatic anchors { top: staticMenuItems.top bottom: parent.bottom left: parent.left right: parent.right leftMargin: 2 rightMargin: 2 bottomMargin: 2 } Rectangle { id: bgStaticBorderLine anchors { top: parent.top left: parent.left right: parent.right } height: PlasmaCore.Units.smallSpacing gradient: Gradient { GradientStop { position: 0; color: borderColor } GradientStop { position: 1; color: "transparent"} } } z: -1 color: backgroundColorStatic } function decreaseItemIndex() { currentItemIndex--; if(currentItemIndex < 0) { currentItemIndex = menuitems.children.length + staticMenuItems.children.length - 1; } var temp = currentItemIndex; var container = menuitems.children; if(currentItemIndex >= menuitems.children.length) { temp -= menuitems.children.length; container = staticMenuItems.children; } if(container[temp].objectName !== "menuitemwrapper" || (container[temp].objectName === "menuitemwrapper" && (!container[temp].enabled || !container[temp].visible))) { decreaseItemIndex(); } else { if(currentItem !== null) currentItem.selected = false; container[temp].selected = true; currentItem = container[temp]; } } function increaseItemIndex() { currentItemIndex++; if(currentItemIndex == menuitems.children.length + staticMenuItems.children.length) { currentItemIndex = 0; } var temp = currentItemIndex; var container = menuitems.children; if(currentItemIndex >= menuitems.children.length) { temp -= menuitems.children.length; container = staticMenuItems.children; } if(container[temp].objectName !== "menuitemwrapper" || (container[temp].objectName === "menuitemwrapper" && (!container[temp].enabled || !container[temp].visible))) { increaseItemIndex(); } else { if(currentItem !== null) currentItem.selected = false; container[temp].selected = true; currentItem = container[temp]; } } Keys.onPressed: { if(event.key == Qt.Key_Up) { decreaseItemIndex(); } else if(event.key == Qt.Key_Down || event.key == Qt.Key_Tab) { increaseItemIndex(); } else if(event.key == Qt.Key_Escape) { tasksMenu.closeMenu(); } else if(event.key == Qt.Key_Enter || event.key == Qt.Key_Return) { if(currentItem !== null) { currentItem.clicked(); } } } /* * Connects the context menu with the C++ part of the plasmoid. * The native interface installs itself onto this dialog as an event filter, upon which * all mouse click events are captured. By checking if the mouse has been clicked outside of * the context menu, we can then safely close it. * * This works because right after creating the context menu, we have set this dialog window to * grab all mouse events, which mimicks the way context menus work under Linux. * */ Connections { target: plasmoid.nativeInterface; function onMouseEventDetected(mouse) { if(!fscope.contains(plasmoid.nativeInterface.getPosition(tasksMenu))) { tasksMenu.closeMenu(); } } /*onMouseEventDetected: { }*/ } } Component.onCompleted: { backend.showAllPlaces.connect(showContextMenuWithAllPlaces) tasksMenu.backgroundHints = 2; // Sets the dialog background to the solid SVG variant. } Component.onDestruction: { backend.showAllPlaces.disconnect(showContextMenuWithAllPlaces) if(alsoCloseTask) tasksModel.requestClose(modelIndex); } }