/* SPDX-FileCopyrightText: 2012-2016 Eike Hein SPDX-FileCopyrightText: 2016 Kai Uwe Broulik SPDX-License-Identifier: GPL-2.0-or-later */ import QtQuick import org.kde.plasma.plasmoid import org.kde.plasma.core as PlasmaCore import org.kde.plasma.extras as PlasmaExtras import org.kde.taskmanager as TaskManager import org.kde.plasma.private.mpris as Mpris import org.kde.plasma.private.taskmanager as TaskManagerApplet import "code/layoutmetrics.js" as LayoutMetrics PlasmaExtras.Menu { id: menu required property TaskManagerApplet.Backend backend required property Mpris.Mpris2Model mpris2Source required property /*QModelIndex*/var modelIndex readonly property var atm: TaskManager.AbstractTasksModel property bool showAllPlaces: false placement: { if (Plasmoid.location === PlasmaCore.Types.LeftEdge) { return PlasmaExtras.Menu.RightPosedTopAlignedPopup; } else if (Plasmoid.location === PlasmaCore.Types.TopEdge) { return PlasmaExtras.Menu.BottomPosedLeftAlignedPopup; } else if (Plasmoid.location === PlasmaCore.Types.RightEdge) { return PlasmaExtras.Menu.LeftPosedTopAlignedPopup; } else { return PlasmaExtras.Menu.TopPosedLeftAlignedPopup; } } minimumWidth: visualParent.width onStatusChanged: { if (visualParent && get(atm.LauncherUrlWithoutIcon).toString() !== "" && status === PlasmaExtras.Menu.Open) { activitiesDesktopsMenu.refresh(); } else if (status === PlasmaExtras.Menu.Closed) { menu.destroy(); } } Component.onCompleted: { // Cannot have "Connections" as child of PlasmaExtras.Menu. backend.showAllPlaces.connect(showContextMenuWithAllPlaces); } Component.onDestruction: { backend.showAllPlaces.disconnect(showContextMenuWithAllPlaces); } function showContextMenuWithAllPlaces(): void { visualParent.showContextMenu({showAllPlaces: true}); } function get(modelProp: int): var { return tasksModel.data(modelIndex, modelProp) } function show(): void { Plasmoid.contextualActionsAboutToShow(); loadDynamicLaunchActions(get(atm.LauncherUrlWithoutIcon)); openRelative(); } function newMenuItem(parent: QtObject): PlasmaExtras.MenuItem { return Qt.createQmlObject(` import org.kde.plasma.extras as PlasmaExtras PlasmaExtras.MenuItem {} `, parent); } function newSeparator(parent: QtObject): PlasmaExtras.MenuItem { return Qt.createQmlObject(` import org.kde.plasma.extras as PlasmaExtras PlasmaExtras.MenuItem { separator: true } `, parent); } function loadDynamicLaunchActions(launcherUrl: url): void { const sections = []; const placesActions = backend.placesActions(launcherUrl, showAllPlaces, menu); if (placesActions.length > 0) { sections.push({ title: i18n("Places"), group: "places", actions: placesActions }); } else { sections.push({ title: i18n("Recent Files"), group: "recents", actions: backend.recentDocumentActions(launcherUrl, menu) }); } sections.push({ title: i18n("Actions"), group: "actions", actions: backend.jumpListActions(launcherUrl, menu) }); // 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. const textMetrics = Qt.createQmlObject("import QtQuick; TextMetrics {}", menu); textMetrics.elide = Qt.ElideRight; textMetrics.elideWidth = LayoutMetrics.maximumContextMenuTextWidth(); sections.forEach(section => { if (section["actions"].length > 0 || section["group"] === "actions") { // Don't add the "Actions" header if the menu has nothing but actions // in it, because then it's redundant (all menus have actions) if ( (section["group"] !== "actions") || (section["group"] === "actions" && (sections[0]["actions"].length > 0 || sections[1]["actions"].length > 0)) ) { 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 = section["actions"][i]; textMetrics.text = item.action.text; item.action.text = textMetrics.elidedText; menu.addMenuItem(item, startNewInstanceItem); } }); // Add Media Player control actions const playerData = mpris2Source.playerForLauncherUrl(launcherUrl, get(atm.AppPid)); if (playerData && playerData.canControl && !(get(atm.WinIdList) !== undefined && get(atm.WinIdList).length > 1)) { const playing = playerData.playbackStatus === Mpris.PlaybackStatus.Playing; let menuItem = menu.newMenuItem(menu); menuItem.text = i18nc("Play previous track", "Previous Track"); menuItem.icon = "media-skip-backward"; menuItem.enabled = Qt.binding(() => { return playerData.canGoPrevious; }); menuItem.clicked.connect(() => { playerData.Previous(); }); menu.addMenuItem(menuItem, startNewInstanceItem); menuItem = menu.newMenuItem(menu); // PlasmaCore Menu doesn't actually handle icons or labels changing at runtime... menuItem.text = Qt.binding(() => { // 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(() => { return playing && playerData.canPause ? "media-playback-pause" : "media-playback-start"; }); menuItem.enabled = Qt.binding(() => { return playing ? playerData.canPause : playerData.canPlay; }); menuItem.clicked.connect(() => { if (playing) { playerData.Pause(); } else { playerData.Play(); } }); 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(() => { return playerData.canGoNext; }); menuItem.clicked.connect(() => { playerData.Next(); }); menu.addMenuItem(menuItem, startNewInstanceItem); menuItem = menu.newMenuItem(menu); menuItem.text = i18nc("Stop playback", "Stop"); menuItem.icon = "media-playback-stop"; menuItem.enabled = Qt.binding(() => { return playerData.canStop; }); menuItem.clicked.connect(() => { playerData.Stop(); }); 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), 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(() => { return !closeWindowItem.visible; }); menuItem.clicked.connect(() => { playerData.Quit(); }); 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 (get(atm.IsLauncher) && !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.iconName; menuItem.visible = Qt.binding(() => { return !startNewInstanceItem.visible; }); menuItem.clicked.connect(() => { playerData.Raise(); }); 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) { const muteItem = menu.newMenuItem(menu); muteItem.checkable = true; muteItem.checked = Qt.binding(() => { return menu.visualParent && menu.visualParent.muted; }); muteItem.clicked.connect(() => { menu.visualParent.toggleMuted(); }); muteItem.text = i18n("Mute"); muteItem.icon = "audio-volume-muted"; menu.addMenuItem(muteItem, startNewInstanceItem); menu.addMenuItem(newSeparator(menu), startNewInstanceItem); } } PlasmaExtras.MenuItem { id: startNewInstanceItem visible: get(atm.CanLaunchNewInstance) text: i18n("Open New Window") icon: "window-new" onClicked: tasksModel.requestNewInstance(modelIndex) } PlasmaExtras.MenuItem { id: virtualDesktopsMenuItem visible: virtualDesktopInfo.numberOfDesktops > 1 && (visualParent && !get(atm.IsLauncher) && !get(atm.IsStartup) && get(atm.IsVirtualDesktopsChangeable)) enabled: visible text: i18n("Move to &Desktop") icon: "virtual-desktops" readonly property Connections virtualDesktopsMenuConnections: Connections { target: virtualDesktopInfo function onNumberOfDesktopsChanged(): void { Qt.callLater(virtualDesktopsMenu.refresh); } function onDesktopIdsChanged(): void { Qt.callLater(virtualDesktopsMenu.refresh); } function onDesktopNamesChanged(): void { Qt.callLater(virtualDesktopsMenu.refresh); } } readonly property PlasmaExtras.Menu _virtualDesktopsMenu: PlasmaExtras.Menu { id: virtualDesktopsMenu visualParent: virtualDesktopsMenuItem.action function refresh(): void { clearMenuItems(); if (virtualDesktopInfo.numberOfDesktops <= 1 || !virtualDesktopsMenuItem.enabled) { return; } let menuItem = menu.newMenuItem(virtualDesktopsMenu); menuItem.text = i18n("Move &To Current Desktop"); menuItem.enabled = Qt.binding(() => { return menu.visualParent && menu.get(atm.VirtualDesktops).indexOf(virtualDesktopInfo.currentDesktop) === -1; }); menuItem.clicked.connect(() => { tasksModel.requestVirtualDesktops(menu.modelIndex, [virtualDesktopInfo.currentDesktop]); }); menuItem = menu.newMenuItem(virtualDesktopsMenu); menuItem.text = i18n("&All Desktops"); menuItem.checkable = true; menuItem.checked = Qt.binding(() => { return menu.visualParent && menu.get(atm.IsOnAllVirtualDesktops); }); menuItem.clicked.connect(() => { tasksModel.requestVirtualDesktops(menu.modelIndex, []); }); backend.setActionGroup(menuItem.action); menu.newSeparator(virtualDesktopsMenu); for (let i = 0; i < virtualDesktopInfo.desktopNames.length; ++i) { menuItem = menu.newMenuItem(virtualDesktopsMenu); menuItem.text = virtualDesktopInfo.desktopNames[i]; menuItem.checkable = true; menuItem.checked = Qt.binding((i => { return () => menu.visualParent && menu.get(atm.VirtualDesktops).indexOf(virtualDesktopInfo.desktopIds[i]) > -1; })(i)); menuItem.clicked.connect((i => { 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.icon = "list-add"; menuItem.clicked.connect(() => { tasksModel.requestNewVirtualDesktop(menu.modelIndex); }); } Component.onCompleted: refresh() } } PlasmaExtras.MenuItem { id: activitiesDesktopsMenuItem visible: activityInfo.numberOfRunningActivities > 1 && (visualParent && !get(atm.IsLauncher) && !get(atm.IsStartup)) enabled: visible text: i18n("Show in &Activities") icon: "activities" readonly property Connections activityInfoConnections: Connections { target: activityInfo function onNumberOfRunningActivitiesChanged(): void { activitiesDesktopsMenu.refresh() } } readonly property PlasmaExtras.Menu _activitiesDesktopsMenu: PlasmaExtras.Menu { id: activitiesDesktopsMenu visualParent: activitiesDesktopsMenuItem.action function refresh(): void { clearMenuItems(); if (activityInfo.numberOfRunningActivities <= 1) { return; } let menuItem = menu.newMenuItem(activitiesDesktopsMenu); menuItem.text = i18n("Add To Current Activity"); menuItem.enabled = Qt.binding(() => { return menu.visualParent && menu.get(atm.Activities).length > 0 && menu.get(atm.Activities).indexOf(activityInfo.currentActivity) < 0; }); menuItem.clicked.connect(() => { 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(() => { return menu.visualParent && menu.get(atm.Activities).length === 0; }); menuItem.toggled.connect(checked => { let newActivities = []; // will cast to an empty QStringList i.e all activities if (!checked) { newActivities = [activityInfo.currentActivity]; } tasksModel.requestActivities(menu.modelIndex, newActivities); }); menu.newSeparator(activitiesDesktopsMenu); const runningActivities = activityInfo.runningActivities(); for (let i = 0; i < runningActivities.length; ++i) { const activityId = runningActivities[i]; menuItem = menu.newMenuItem(activitiesDesktopsMenu); menuItem.text = activityInfo.activityName(runningActivities[i]); menuItem.icon = activityInfo.activityIcon(runningActivities[i]); menuItem.checkable = true; menuItem.checked = Qt.binding((activityId => { return () => menu.visualParent && menu.get(atm.Activities).indexOf(activityId) >= 0; })(activityId)); menuItem.toggled.connect((activityId => { return checked => { let newActivities = menu.get(atm.Activities); if (checked) { newActivities = newActivities.concat(activityId); } else { const index = newActivities.indexOf(activityId); if (index < 0) { return; } newActivities.splice(index, 1); } return tasksModel.requestActivities(menu.modelIndex, newActivities); }; })(activityId)); } menu.newSeparator(activitiesDesktopsMenu); for (let i = 0; i < runningActivities.length; ++i) { const activityId = runningActivities[i]; const onActivities = menu.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 = menu.newMenuItem(activitiesDesktopsMenu); menuItem.text = i18n("Move to %1", activityInfo.activityName(activityId)) menuItem.icon = activityInfo.activityIcon(activityId) menuItem.clicked.connect((activityId => { return () => tasksModel.requestActivities(menu.modelIndex, [activityId]); })(activityId)); } menu.newSeparator(activitiesDesktopsMenu); } Component.onCompleted: refresh() } } PlasmaExtras.MenuItem { id: launcherToggleAction visible: visualParent && !get(atm.IsLauncher) && !get(atm.IsStartup) && Plasmoid.immutability !== PlasmaCore.Types.SystemImmutable && (activityInfo.numberOfRunningActivities < 2) && !doesBelongToCurrentActivity() enabled: visualParent && get(atm.LauncherUrlWithoutIcon).toString() !== "" text: i18n("&Pin to Task Manager") icon: "window-pin" function doesBelongToCurrentActivity(): bool { return tasksModel.launcherActivities(get(atm.LauncherUrlWithoutIcon)) .some(activity => activity === activityInfo.currentActivity || activity === activityInfo.nullUuid); } onClicked: { tasksModel.requestAddLauncher(get(atm.LauncherUrl)); } } PlasmaExtras.MenuItem { id: showLauncherInActivitiesItem text: i18n("&Pin to Task Manager") icon: "window-pin" visible: visualParent && !get(atm.IsStartup) && Plasmoid.immutability !== PlasmaCore.Types.SystemImmutable && (activityInfo.numberOfRunningActivities >= 2) readonly property Connections activitiesLaunchersMenuConnections: Connections { target: activityInfo function onNumberOfRunningActivitiesChanged(): void { activitiesDesktopsMenu.refresh() } } readonly property PlasmaExtras.Menu _activitiesLaunchersMenu: PlasmaExtras.Menu { id: activitiesLaunchersMenu visualParent: showLauncherInActivitiesItem.action function refresh(): void { clearMenuItems(); if (menu.visualParent === null) return; const createNewItem = (id, title, iconName, url, activities) => { var result = menu.newMenuItem(activitiesLaunchersMenu); result.text = title; result.icon = iconName; result.visible = true; result.checkable = true; result.checked = activities.some(activity => activity === id); result.clicked.connect(() => { if (result.checked) { tasksModel.requestAddLauncherToActivity(url, id); } else { tasksModel.requestRemoveLauncherFromActivity(url, id); } }); return result; }; if (menu.visualParent === null) return; const url = menu.get(atm.LauncherUrlWithoutIcon); const 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"), activityInfo.activityIcon(activityInfo.currentActivity), url, activities); menu.newSeparator(activitiesLaunchersMenu); activityInfo.runningActivities() .forEach(id => { createNewItem(id, activityInfo.activityName(id), activityInfo.activityIcon(id), url, activities); }); } Component.onCompleted: { menu.visualParentChanged.connect(refresh); refresh(); } } } PlasmaExtras.MenuItem { visible: (visualParent && get(atm.IsStartup) !== true && Plasmoid.immutability !== PlasmaCore.Types.SystemImmutable && !launcherToggleAction.visible && activityInfo.numberOfRunningActivities < 2) text: i18n("Unpin from Task Manager") icon: "window-unpin" onClicked: { tasksModel.requestRemoveLauncher(get(atm.LauncherUrlWithoutIcon)); } } PlasmaExtras.MenuItem { id: moreActionsMenuItem visible: (visualParent && !get(atm.IsLauncher) && !get(atm.IsStartup)) enabled: visible text: i18n("More") icon: "view-more-symbolic" readonly property PlasmaExtras.Menu moreMenu: PlasmaExtras.Menu { visualParent: moreActionsMenuItem.action PlasmaExtras.MenuItem { enabled: menu.visualParent && menu.get(atm.IsMovable) text: i18n("&Move") icon: "transform-move" onClicked: tasksModel.requestMove(menu.modelIndex) } PlasmaExtras.MenuItem { enabled: menu.visualParent && menu.get(atm.IsResizable) text: i18n("Re&size") icon: "transform-scale" onClicked: tasksModel.requestResize(menu.modelIndex) } PlasmaExtras.MenuItem { visible: (menu.visualParent && !get(atm.IsLauncher) && !get(atm.IsStartup)) enabled: menu.visualParent && get(atm.IsMaximizable) checkable: true checked: menu.visualParent && get(atm.IsMaximized) text: i18n("Ma&ximize") icon: "window-maximize" onClicked: tasksModel.requestToggleMaximized(modelIndex) } PlasmaExtras.MenuItem { visible: (menu.visualParent && !get(atm.IsLauncher) && !get(atm.IsStartup)) enabled: menu.visualParent && get(atm.IsMinimizable) checkable: true checked: menu.visualParent && get(atm.IsMinimized) text: i18n("Mi&nimize") icon: "window-minimize" onClicked: tasksModel.requestToggleMinimized(modelIndex) } PlasmaExtras.MenuItem { checkable: true checked: menu.visualParent && menu.get(atm.IsKeepAbove) text: i18n("Keep &Above Others") icon: "window-keep-above" onClicked: tasksModel.requestToggleKeepAbove(menu.modelIndex) } PlasmaExtras.MenuItem { checkable: true checked: menu.visualParent && menu.get(atm.IsKeepBelow) text: i18n("Keep &Below Others") icon: "window-keep-below" onClicked: tasksModel.requestToggleKeepBelow(menu.modelIndex) } PlasmaExtras.MenuItem { enabled: menu.visualParent && menu.get(atm.IsFullScreenable) checkable: true checked: menu.visualParent && menu.get(atm.IsFullScreen) text: i18n("&Fullscreen") icon: "view-fullscreen" onClicked: tasksModel.requestToggleFullScreen(menu.modelIndex) } PlasmaExtras.MenuItem { enabled: menu.visualParent && menu.get(atm.IsShadeable) checkable: true checked: menu.visualParent && menu.get(atm.IsShaded) text: i18n("&Shade") icon: "window-shade" onClicked: tasksModel.requestToggleShaded(menu.modelIndex) } PlasmaExtras.MenuItem { separator: true } PlasmaExtras.MenuItem { visible: (Plasmoid.configuration.groupingStrategy !== 0) && menu.get(atm.IsWindow) checkable: true checked: menu.visualParent && menu.get(atm.IsGroupable) text: i18n("Allow this program to be grouped") icon: "view-group" onClicked: tasksModel.requestToggleGrouping(menu.modelIndex) } } } PlasmaExtras.MenuItem { separator: true } PlasmaExtras.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.internalAction("configure") } PlasmaExtras.MenuItem { property QtObject editModeAction: null enabled: editModeAction && editModeAction.enabled visible: editModeAction && editModeAction.visible text: editModeAction ? editModeAction.text : "" icon: editModeAction ? editModeAction.icon : "" onClicked: editModeAction.trigger() Component.onCompleted: editModeAction = Plasmoid.containment.internalAction("configure") } PlasmaExtras.MenuItem { separator: true } PlasmaExtras.MenuItem { id: closeWindowItem visible: (visualParent && !get(atm.IsLauncher) && !get(atm.IsStartup)) enabled: visualParent && get(atm.IsClosable) text: get(atm.IsGroupParent) ? i18nc("@item:inmenu", "&Close All") : i18n("&Close") icon: "window-close" onClicked: { if (tasks.groupDialog !== null && tasks.groupDialog.visualParent === visualParent) { tasks.groupDialog.visible = false; } tasksModel.requestClose(modelIndex); } } }