mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-16 16:24:00 -04:00
534 lines
21 KiB
QML
534 lines
21 KiB
QML
import QtQuick
|
|
import QtQuick.Effects
|
|
import Quickshell
|
|
import Quickshell.Widgets
|
|
import qs.Common
|
|
import qs.Modules.DBar
|
|
import qs.Modules.Plugins
|
|
import qs.Services
|
|
import qs.DCommon.Widgets
|
|
import qs.Widgets
|
|
|
|
BasePill {
|
|
id: root
|
|
|
|
enableBackgroundHover: false
|
|
enableCursor: false
|
|
section: "left"
|
|
|
|
property var widgetData: null
|
|
property var hoveredItem: null
|
|
|
|
onHoveredItemChanged: {
|
|
if (hoveredItem)
|
|
return;
|
|
hideTooltip();
|
|
}
|
|
|
|
function hideTooltip() {
|
|
if (tooltipLoader.item)
|
|
tooltipLoader.item.hide();
|
|
tooltipLoader.active = false;
|
|
}
|
|
|
|
function edgeAnchor(item, popupHeight) {
|
|
if (isVerticalOrientation) {
|
|
const center = item.mapToItem(null, item.width / 2, item.height / 2);
|
|
const x = axis?.edge === "left" ? (barThickness + barSpacing + Theme.spacingXS) : (parentScreen.width - barThickness - barSpacing - Theme.spacingXS);
|
|
return Qt.point(x, center.y + minTooltipY);
|
|
}
|
|
const top = item.mapToItem(null, item.width / 2, 0);
|
|
const screenHeight = parentScreen ? parentScreen.height : Screen.height;
|
|
const y = axis?.edge === "bottom" ? (screenHeight - barThickness - barSpacing - Theme.spacingXS - popupHeight) : (barThickness + barSpacing + Theme.spacingXS);
|
|
return Qt.point(top.x, y);
|
|
}
|
|
property var topBar: null
|
|
property Item windowRoot: (Window.window ? Window.window.contentItem : null)
|
|
|
|
readonly property real effectiveBarThickness: {
|
|
if (barThickness > 0 && barSpacing > 0) {
|
|
return barThickness + barSpacing;
|
|
}
|
|
return Theme.barThickness(barConfig?.innerPadding ?? 2, CompositorService.getScreenScale(parentScreen)) + (barConfig?.spacing ?? 4);
|
|
}
|
|
|
|
readonly property var barBounds: {
|
|
if (!parentScreen || !barConfig) {
|
|
return {
|
|
"x": 0,
|
|
"y": 0,
|
|
"width": 0,
|
|
"height": 0,
|
|
"wingSize": 0
|
|
};
|
|
}
|
|
const barPosition = axis.edge === "left" ? 2 : (axis.edge === "right" ? 3 : (axis.edge === "top" ? 0 : 1));
|
|
return SettingsData.getBarBounds(parentScreen, effectiveBarThickness, barPosition, barConfig);
|
|
}
|
|
|
|
readonly property real barY: barBounds.y
|
|
|
|
property int _desktopEntriesUpdateTrigger: 0
|
|
property int _toplevelsUpdateTrigger: 0
|
|
property int _appIdSubstitutionsTrigger: 0
|
|
|
|
readonly property bool _currentWorkspace: SettingsData.widgetOption("runningApps", widgetData, "runningAppsCurrentWorkspace")
|
|
readonly property bool _currentMonitor: SettingsData.widgetOption("runningApps", widgetData, "runningAppsCurrentMonitor")
|
|
readonly property bool _groupByApp: SettingsData.widgetOption("runningApps", widgetData, "runningAppsGroupByApp")
|
|
readonly property string windowModelKey: _groupByApp ? "appId" : CompositorService.toplevelKey
|
|
|
|
readonly property var sortedToplevels: {
|
|
_toplevelsUpdateTrigger;
|
|
let toplevels = CompositorService.sortedToplevels;
|
|
if (!toplevels || toplevels.length === 0)
|
|
return [];
|
|
|
|
if (_currentWorkspace)
|
|
toplevels = CompositorService.filterCurrentWorkspace(toplevels, parentScreen?.name) || [];
|
|
if (_currentMonitor)
|
|
toplevels = CompositorService.filterCurrentDisplay(toplevels, parentScreen?.name) || [];
|
|
return toplevels;
|
|
}
|
|
|
|
Connections {
|
|
target: CompositorService
|
|
function onToplevelsChanged() {
|
|
_toplevelsUpdateTrigger++;
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: DesktopEntries
|
|
function onApplicationsChanged() {
|
|
_desktopEntriesUpdateTrigger++;
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: SettingsData
|
|
function onAppIdSubstitutionsChanged() {
|
|
_appIdSubstitutionsTrigger++;
|
|
}
|
|
}
|
|
readonly property var groupedWindows: {
|
|
if (!_groupByApp) {
|
|
return [];
|
|
}
|
|
try {
|
|
if (!sortedToplevels || sortedToplevels.length === 0) {
|
|
return [];
|
|
}
|
|
const appGroups = new Map();
|
|
sortedToplevels.forEach((toplevel, index) => {
|
|
if (!toplevel)
|
|
return;
|
|
const appId = toplevel?.appId || "unknown";
|
|
if (!appGroups.has(appId)) {
|
|
appGroups.set(appId, {
|
|
"appId": appId,
|
|
"windows": []
|
|
});
|
|
}
|
|
appGroups.get(appId).windows.push({
|
|
"toplevel": toplevel,
|
|
"windowId": index,
|
|
"windowTitle": toplevel?.title || "(Unnamed)"
|
|
});
|
|
});
|
|
return Array.from(appGroups.values());
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
}
|
|
readonly property int windowCount: _groupByApp ? (groupedWindows?.length || 0) : (sortedToplevels?.length || 0)
|
|
readonly property bool compactMode: SettingsData.widgetOption("runningApps", widgetData, "runningAppsCompactMode")
|
|
readonly property real appIconSize: Theme.barIconSize(barThickness, undefined, barConfig?.maximizeWidgetIcons, barConfig?.iconScale)
|
|
readonly property real iconCellSize: appIconSize + 6
|
|
|
|
readonly property string focusedAppId: {
|
|
if (!sortedToplevels || sortedToplevels.length === 0)
|
|
return "";
|
|
for (let i = 0; i < sortedToplevels.length; i++) {
|
|
if (sortedToplevels[i].activated)
|
|
return sortedToplevels[i].appId || "";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
visible: windowCount > 0
|
|
|
|
property real scrollAccumulator: 0
|
|
property real touchpadThreshold: 500
|
|
|
|
function activateNeighbor(windows, delta) {
|
|
const currentIndex = windows.findIndex(w => w.activated);
|
|
const nextIndex = delta < 0 ? (currentIndex === -1 ? 0 : Math.min(currentIndex + 1, windows.length - 1)) : (currentIndex === -1 ? windows.length - 1 : Math.max(currentIndex - 1, 0));
|
|
const nextWindow = windows[nextIndex];
|
|
if (nextWindow)
|
|
CompositorService.activateToplevel(nextWindow);
|
|
}
|
|
|
|
onWheel: function (wheelEvent) {
|
|
wheelEvent.accepted = true;
|
|
const deltaY = wheelEvent.angleDelta.y;
|
|
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
|
|
|
|
const windows = root.sortedToplevels.filter(w => !w.skipSwitcher);
|
|
if (windows.length < 2)
|
|
return;
|
|
|
|
if (isMouseWheel) {
|
|
activateNeighbor(windows, deltaY);
|
|
return;
|
|
}
|
|
|
|
scrollAccumulator += deltaY;
|
|
if (Math.abs(scrollAccumulator) < touchpadThreshold)
|
|
return;
|
|
activateNeighbor(windows, scrollAccumulator);
|
|
scrollAccumulator = 0;
|
|
}
|
|
|
|
content: Component {
|
|
Item {
|
|
implicitWidth: layoutLoader.item ? layoutLoader.item.implicitWidth : 0
|
|
implicitHeight: layoutLoader.item ? layoutLoader.item.implicitHeight : 0
|
|
|
|
Loader {
|
|
id: layoutLoader
|
|
anchors.centerIn: parent
|
|
sourceComponent: root.isVerticalOrientation ? columnLayout : rowLayout
|
|
}
|
|
}
|
|
}
|
|
|
|
ScriptModel {
|
|
id: windowModel
|
|
values: root._groupByApp ? root.groupedWindows : root.sortedToplevels
|
|
objectProp: root.windowModelKey
|
|
}
|
|
|
|
Component {
|
|
id: rowLayout
|
|
Row {
|
|
spacing: Theme.spacingXS
|
|
|
|
Repeater {
|
|
model: windowModel
|
|
delegate: windowDelegate
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: columnLayout
|
|
Column {
|
|
spacing: Theme.spacingXS
|
|
|
|
Repeater {
|
|
model: windowModel
|
|
delegate: windowDelegate
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: windowDelegate
|
|
|
|
Item {
|
|
id: delegateItem
|
|
|
|
Component.onDestruction: {
|
|
if (root.hoveredItem === delegateItem)
|
|
root.hoveredItem = null;
|
|
}
|
|
|
|
property bool isGrouped: root._groupByApp
|
|
property var groupData: isGrouped ? modelData : null
|
|
property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData
|
|
property bool isFocused: isGrouped ? (root.focusedAppId === appId) : (toplevelData ? toplevelData.activated : false)
|
|
property string appId: isGrouped ? modelData.appId : (modelData.appId || "")
|
|
readonly property string effectiveAppId: {
|
|
root._appIdSubstitutionsTrigger;
|
|
return Paths.moddedAppId(appId);
|
|
}
|
|
property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)"
|
|
property var toplevelObject: toplevelData
|
|
property int windowCount: isGrouped ? modelData.windows.length : 1
|
|
readonly property bool isMinimized: {
|
|
if (!CompositorService.supportsMinimize)
|
|
return false;
|
|
if (isGrouped)
|
|
return groupData.windows.length > 0 && groupData.windows.every(w => w.toplevel.minimized);
|
|
return toplevelObject?.minimized === true;
|
|
}
|
|
property string tooltipText: {
|
|
root._desktopEntriesUpdateTrigger;
|
|
const desktopEntry = effectiveAppId ? DesktopEntries.heuristicLookup(effectiveAppId) : null;
|
|
const appName = effectiveAppId ? Paths.getAppName(effectiveAppId, desktopEntry) : "Unknown";
|
|
|
|
if (isGrouped && windowCount > 1) {
|
|
return appName + " (" + windowCount + " windows)";
|
|
}
|
|
return appName + (windowTitle ? " • " + windowTitle : "");
|
|
}
|
|
readonly property real visualWidth: root.compactMode ? root.iconCellSize : (root.iconCellSize + Theme.spacingXS + 120)
|
|
|
|
width: root.isVerticalOrientation ? root.barThickness : visualWidth
|
|
height: root.isVerticalOrientation ? root.iconCellSize : root.barThickness
|
|
|
|
BarPillSurface {
|
|
id: visualContent
|
|
width: delegateItem.visualWidth
|
|
height: root.iconCellSize
|
|
anchors.centerIn: parent
|
|
style: BarMetrics.widgetStyle(root.barConfig)
|
|
pressed: mouseArea.pressed
|
|
color: isFocused ? Theme.selectedContainer : "transparent"
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
radius: visualContent.radius
|
|
color: Theme.withAlpha(isFocused ? Theme.onSelectedContainer : Theme.onSurface, mouseArea.pressed ? Theme.stateLayerPressed : mouseArea.containsMouse ? Theme.stateLayerHover : 0)
|
|
}
|
|
|
|
IconImage {
|
|
id: iconImg
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: root.compactMode ? Math.round((parent.width - root.appIconSize) / 2) : Theme.spacingXS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: root.appIconSize
|
|
height: root.appIconSize
|
|
source: {
|
|
root._desktopEntriesUpdateTrigger;
|
|
root._appIdSubstitutionsTrigger;
|
|
if (!effectiveAppId)
|
|
return "";
|
|
const desktopEntry = DesktopEntries.heuristicLookup(effectiveAppId);
|
|
return Paths.getAppIcon(effectiveAppId, desktopEntry);
|
|
}
|
|
smooth: true
|
|
mipmap: true
|
|
asynchronous: true
|
|
visible: status === Image.Ready
|
|
opacity: delegateItem.isMinimized ? 0.4 : 1
|
|
layer.enabled: appId === "org.quickshell" || appId === "com.danklinux.dms"
|
|
layer.smooth: true
|
|
layer.mipmap: true
|
|
layer.effect: MultiEffect {
|
|
saturation: 0
|
|
colorization: 1
|
|
colorizationColor: Theme.primary
|
|
}
|
|
}
|
|
|
|
DIcon {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: root.compactMode ? Math.round((parent.width - root.appIconSize) / 2) : Theme.spacingXS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
size: root.appIconSize
|
|
name: "sports_esports"
|
|
color: isFocused ? Theme.onSelectedContainer : Theme.widgetTextColor
|
|
visible: !iconImg.visible && Paths.isSteamApp(effectiveAppId)
|
|
opacity: delegateItem.isMinimized ? 0.4 : 1
|
|
}
|
|
|
|
StyledText {
|
|
anchors.horizontalCenter: iconImg.horizontalCenter
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
visible: !iconImg.visible && !Paths.isSteamApp(effectiveAppId)
|
|
text: {
|
|
root._desktopEntriesUpdateTrigger;
|
|
if (!effectiveAppId)
|
|
return "?";
|
|
const desktopEntry = DesktopEntries.heuristicLookup(effectiveAppId);
|
|
const appName = Paths.getAppName(effectiveAppId, desktopEntry);
|
|
return appName.charAt(0).toUpperCase();
|
|
}
|
|
font.pixelSize: 10
|
|
color: isFocused ? Theme.onSelectedContainer : Theme.widgetTextColor
|
|
opacity: delegateItem.isMinimized ? 0.4 : 1
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.right: parent.right
|
|
anchors.bottom: parent.bottom
|
|
anchors.rightMargin: root.compactMode ? -2 : 2
|
|
anchors.bottomMargin: -2
|
|
width: 14
|
|
height: 14
|
|
radius: Theme.fullRadius(width, height)
|
|
color: Theme.primary
|
|
visible: isGrouped && windowCount > 1
|
|
z: 10
|
|
|
|
StyledText {
|
|
anchors.centerIn: parent
|
|
text: windowCount > 9 ? "9+" : windowCount
|
|
font.pixelSize: 9
|
|
color: Theme.onPrimary
|
|
}
|
|
}
|
|
|
|
StyledText {
|
|
anchors.left: iconImg.right
|
|
anchors.leftMargin: Theme.spacingXS
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
visible: !root.compactMode
|
|
text: windowTitle
|
|
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText)
|
|
color: isFocused ? Theme.onSelectedContainer : Theme.widgetTextColor
|
|
elide: Text.ElideRight
|
|
maximumLineCount: 1
|
|
}
|
|
|
|
DRipple {
|
|
id: itemRipple
|
|
rippleColor: isFocused ? Theme.onSelectedContainer : Theme.onSurface
|
|
cornerRadius: visualContent.radius
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: mouseArea
|
|
y: root.isVerticalOrientation ? 0 : -root.topMargin
|
|
x: root.isVerticalOrientation ? -root.leftMargin : 0
|
|
width: parent.width + (root.isVerticalOrientation ? root.leftMargin + root.rightMargin : 0)
|
|
height: parent.height + (root.isVerticalOrientation ? 0 : root.topMargin + root.bottomMargin)
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
|
onPressed: mouse => {
|
|
const pos = mapToItem(visualContent, mouse.x, mouse.y);
|
|
itemRipple.trigger(pos.x, pos.y);
|
|
}
|
|
onClicked: mouse => {
|
|
if (mouse.button === Qt.LeftButton) {
|
|
if (isGrouped && windowCount > 1) {
|
|
let currentIndex = -1;
|
|
for (var i = 0; i < groupData.windows.length; i++) {
|
|
if (groupData.windows[i].toplevel.activated) {
|
|
currentIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
const nextIndex = (currentIndex + 1) % groupData.windows.length;
|
|
CompositorService.activateToplevel(groupData.windows[nextIndex].toplevel);
|
|
} else if (toplevelObject) {
|
|
CompositorService.toggleToplevel(toplevelObject);
|
|
}
|
|
} else if (mouse.button === Qt.RightButton) {
|
|
root.openContextMenu(toplevelObject, delegateItem);
|
|
} else if (mouse.button === Qt.MiddleButton) {
|
|
if (typeof toplevelObject?.close === "function")
|
|
toplevelObject.close();
|
|
}
|
|
}
|
|
onEntered: {
|
|
root.hoveredItem = delegateItem;
|
|
tooltipLoader.active = true;
|
|
if (!tooltipLoader.item)
|
|
return;
|
|
const anchor = root.edgeAnchor(delegateItem, 35);
|
|
const alignLeft = root.isVerticalOrientation && root.axis?.edge === "left";
|
|
const alignRight = root.isVerticalOrientation && root.axis?.edge !== "left";
|
|
tooltipLoader.item.show(delegateItem.tooltipText, anchor.x, anchor.y, root.parentScreen, alignLeft, alignRight);
|
|
}
|
|
onExited: {
|
|
if (root.hoveredItem === delegateItem)
|
|
root.hoveredItem = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Loader {
|
|
id: tooltipLoader
|
|
|
|
active: false
|
|
|
|
sourceComponent: DTooltip {}
|
|
}
|
|
|
|
function menuAnchorFor(item) {
|
|
const anchor = root.contextMenuAnchor();
|
|
const center = item.mapToGlobal(item.width / 2, item.height / 2);
|
|
if (anchor.isVertical)
|
|
anchor.y = center.y - (anchor.screen.y || 0) + root.minTooltipY;
|
|
else
|
|
anchor.x = center.x - (anchor.screen.x || 0);
|
|
return anchor;
|
|
}
|
|
|
|
function openContextMenu(toplevelObject, item) {
|
|
root.hideTooltip();
|
|
windowContextMenu.currentWindow = toplevelObject ?? null;
|
|
windowContextMenu.openFromBar(item ? root.menuAnchorFor(item) : root.contextMenuAnchor());
|
|
}
|
|
|
|
DContextMenu {
|
|
id: windowContextMenu
|
|
|
|
property var currentWindow: null
|
|
|
|
layerNamespace: "dms:running-apps-context-menu"
|
|
menuItems: {
|
|
const items = [];
|
|
if (CompositorService.canMinimize(currentWindow))
|
|
items.push({
|
|
type: "item",
|
|
icon: currentWindow?.minimized ? "open_in_full" : "minimize",
|
|
text: currentWindow?.minimized ? I18n.tr("Restore") : I18n.tr("Minimize"),
|
|
action: () => {
|
|
const targetWindow = windowContextMenu.currentWindow;
|
|
if (!targetWindow)
|
|
return;
|
|
if (targetWindow.minimized)
|
|
CompositorService.activateToplevel(targetWindow);
|
|
else
|
|
targetWindow.minimized = true;
|
|
}
|
|
});
|
|
const scratchpad = CompositorService.windowScratchpadName(currentWindow);
|
|
if (scratchpad)
|
|
items.push({
|
|
type: "item",
|
|
icon: "outbox",
|
|
text: I18n.tr("Move out of scratchpad", "window context menu action, brings the window back from the special workspace"),
|
|
action: () => CompositorService.moveWindowOutOfSpecial(windowContextMenu.currentWindow)
|
|
});
|
|
for (const name of scratchpad ? [] : CompositorService.specialWorkspaceNames) {
|
|
items.push({
|
|
type: "item",
|
|
icon: "inbox",
|
|
text: name === "special" ? I18n.tr("Move to scratchpad", "window context menu action, sends the window to the unnamed special workspace") : I18n.tr("Move to scratchpad: %1", "%1 is the named special workspace").arg(name),
|
|
action: () => CompositorService.moveWindowToSpecial(windowContextMenu.currentWindow, name)
|
|
});
|
|
}
|
|
items.push({
|
|
type: "item",
|
|
icon: "close",
|
|
text: I18n.tr("Close"),
|
|
isDestructive: true,
|
|
action: () => windowContextMenu.currentWindow?.close()
|
|
});
|
|
return items;
|
|
}
|
|
onOpenStateChanged: {
|
|
const screenName = root.parentScreen?.name;
|
|
if (!screenName)
|
|
return;
|
|
if (openState)
|
|
TrayMenuManager.registerMenu(screenName, windowContextMenu.contextWindow);
|
|
else
|
|
TrayMenuManager.unregisterMenu(screenName);
|
|
}
|
|
Component.onDestruction: {
|
|
if (openState && root.parentScreen?.name)
|
|
TrayMenuManager.unregisterMenu(root.parentScreen.name);
|
|
}
|
|
}
|
|
}
|