mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-17 00:34:01 -04:00
* feat(island): add system tray slot and overflow menu to compact home
- Add system tray slot and overflow expander menu in Dynamic Island
- Share tray icon path resolution and DBus context menu fallback in TrayMenuManager
- Support edge-anchored context menu positioning across top, bottom, left, and right
- Lock bottom and edge anchoring in DContextMenu to prevent animation jitter
* fix(island): only instantiate tray menus when tray slot is enabled
* feat(island): expand tray item and caret click area to slot height
* fix(island): use Theme.surfaceTextHover and restore DContextMenu height behavior
* refactor(tray): extract shared TrayMenuContent and TrayOverflowGrid, support tray order and IPC in island
* Revert "refactor(tray): extract shared TrayMenuContent and TrayOverflowGrid, support tray order and IPC in island"
This reverts commit 29888f89cd.
* refactor(tray): use shared TrayMenuManager helpers and TrayItemIcon, support tray order and IPC in island
* refactor(tray): extract shared TrayMenuContent and TrayOverflowGrid
* feat(island): support tray item drag and drop reordering
* fix(island): prevent re-entrant tray drag reordering on model destruction
* fix(tray): remove native submenu anchor hydration to prevent popup stutter
* fix(tray): decouple submenu hydrator from anchor window to avoid stutter
* fix(tray): restore anchor window on submenu hydrator and disable on island
* tray: fix bar overflow surface & reuse max visible items on the island
329 lines
12 KiB
QML
329 lines
12 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import Quickshell
|
|
import qs.Common
|
|
import qs.DCommon.Widgets
|
|
import qs.Widgets
|
|
|
|
Item {
|
|
id: root
|
|
|
|
property var trayItem: null
|
|
property var menuHandle: trayItem?.menu ?? null
|
|
property var anchorWindow: null
|
|
property var bar: null
|
|
|
|
signal closeRequested()
|
|
signal actionTriggered()
|
|
|
|
implicitWidth: Math.max(Theme.fieldDefaultWidth, menuColumn.implicitWidth)
|
|
implicitHeight: menuColumn.implicitHeight
|
|
width: implicitWidth
|
|
height: implicitHeight
|
|
|
|
focus: true
|
|
property alias entryStack: entryStack
|
|
|
|
ListModel {
|
|
id: entryStack
|
|
}
|
|
|
|
function clearState() {
|
|
entryStack.clear();
|
|
}
|
|
|
|
function topEntry() {
|
|
return entryStack.count ? entryStack.get(entryStack.count - 1).handle : null;
|
|
}
|
|
|
|
function showSubMenu(entry) {
|
|
if (!entry || !entry.hasChildren)
|
|
return;
|
|
entryStack.append({
|
|
handle: entry
|
|
});
|
|
const h = entry.menu || entry;
|
|
if (h && typeof h.updateLayout === "function")
|
|
h.updateLayout();
|
|
if (root.anchorWindow) {
|
|
submenuHydrator.menu = h;
|
|
submenuHydrator.open();
|
|
Qt.callLater(() => submenuHydrator.close());
|
|
}
|
|
}
|
|
|
|
function goBack() {
|
|
if (!entryStack.count)
|
|
return;
|
|
entryStack.remove(entryStack.count - 1);
|
|
}
|
|
|
|
QsMenuAnchor {
|
|
id: submenuHydrator
|
|
anchor.window: root.anchorWindow
|
|
}
|
|
|
|
QsMenuOpener {
|
|
id: rootOpener
|
|
menu: root.menuHandle
|
|
}
|
|
|
|
QsMenuOpener {
|
|
id: subOpener
|
|
menu: {
|
|
const e = root.topEntry();
|
|
return e ? (e.menu || e) : null;
|
|
}
|
|
}
|
|
|
|
Keys.onEscapePressed: {
|
|
if (entryStack.count > 0)
|
|
root.goBack();
|
|
else
|
|
root.closeRequested();
|
|
}
|
|
|
|
Column {
|
|
id: menuColumn
|
|
width: root.width > 0 ? root.width : Theme.fieldDefaultWidth
|
|
spacing: 0
|
|
|
|
Rectangle {
|
|
visible: entryStack.count === 0
|
|
width: parent.width
|
|
height: Theme.menuItemHeight
|
|
radius: Theme.cornerRadiusS
|
|
color: Theme.withAlpha(Theme.onSurface, visibilityToggleArea.pressed ? Theme.stateLayerPressed : visibilityToggleArea.containsMouse ? Theme.stateLayerHover : 0)
|
|
|
|
StyledText {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: {
|
|
const itemTitle = root.trayItem?.tooltipTitle || root.trayItem?.id || I18n.tr("Unknown");
|
|
if (root.bar && root.bar.isAutoOverflowTrayItem(root.trayItem))
|
|
return itemTitle + " · " + I18n.tr("Keep in Bar");
|
|
return itemTitle;
|
|
}
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceTextMedium
|
|
elide: Text.ElideMiddle
|
|
wrapMode: Text.NoWrap
|
|
maximumLineCount: 1
|
|
width: parent.width - Theme.spacingS * 2 - (Theme.iconSizeSmall + Theme.spacingS)
|
|
}
|
|
|
|
DIcon {
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
name: {
|
|
if (root.bar && root.bar.isAutoOverflowTrayItem(root.trayItem))
|
|
return "push_pin";
|
|
const itemKey = TrayMenuManager.getTrayItemKey(root.trayItem);
|
|
const isHidden = root.bar ? root.bar.isManualHiddenTrayItem(root.trayItem) : SessionData.isHiddenTrayId(itemKey);
|
|
return isHidden ? "visibility" : "visibility_off";
|
|
}
|
|
size: Theme.iconSizeSmall
|
|
color: Theme.widgetTextColor
|
|
}
|
|
|
|
MouseArea {
|
|
id: visibilityToggleArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
const itemKey = TrayMenuManager.getTrayItemKey(root.trayItem);
|
|
if (!itemKey)
|
|
return;
|
|
if (root.bar && root.bar.isAutoOverflowTrayItem(root.trayItem)) {
|
|
root.bar.promoteTrayItemToBar(root.trayItem);
|
|
} else {
|
|
const isHidden = root.bar ? root.bar.isManualHiddenTrayItem(root.trayItem) : SessionData.isHiddenTrayId(itemKey);
|
|
if (isHidden)
|
|
SessionData.showTrayId(itemKey);
|
|
else
|
|
SessionData.hideTrayId(itemKey);
|
|
}
|
|
root.closeRequested();
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
visible: entryStack.count === 0
|
|
width: parent.width
|
|
height: Theme.dividerWidth
|
|
color: Theme.outlineHeavy
|
|
}
|
|
|
|
Rectangle {
|
|
visible: entryStack.count > 0
|
|
width: parent.width
|
|
height: Theme.menuItemHeight
|
|
radius: Theme.cornerRadiusS
|
|
color: Theme.withAlpha(Theme.onSurface, backArea.pressed ? Theme.stateLayerPressed : backArea.containsMouse ? Theme.stateLayerHover : 0)
|
|
|
|
Row {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: Theme.spacingXS
|
|
|
|
DIcon {
|
|
name: "arrow_back"
|
|
size: Theme.iconSizeSmall
|
|
color: Theme.widgetTextColor
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Back")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.widgetTextColor
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: backArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: root.goBack()
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
visible: entryStack.count > 0
|
|
width: parent.width
|
|
height: Theme.dividerWidth
|
|
color: Theme.outlineHeavy
|
|
}
|
|
|
|
Repeater {
|
|
model: entryStack.count ? (subOpener.children ? subOpener.children : (topEntry()?.children || [])) : rootOpener.children
|
|
|
|
Rectangle {
|
|
id: menuEntryDelegate
|
|
required property var modelData
|
|
readonly property var menuEntry: modelData
|
|
|
|
width: menuColumn.width
|
|
height: menuEntry?.isSeparator ? Theme.dividerWidth : Theme.menuItemHeight
|
|
radius: menuEntry?.isSeparator ? 0 : Theme.cornerRadiusS
|
|
color: {
|
|
if (menuEntry?.isSeparator)
|
|
return Theme.outlineHeavy;
|
|
return Theme.withAlpha(Theme.onSurface, entryItemArea.pressed ? Theme.stateLayerPressed : entryItemArea.containsMouse ? Theme.stateLayerHover : 0);
|
|
}
|
|
|
|
MouseArea {
|
|
id: entryItemArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
enabled: !menuEntry?.isSeparator && (menuEntry?.enabled !== false)
|
|
cursorShape: Qt.PointingHandCursor
|
|
|
|
onClicked: {
|
|
if (!menuEntry || menuEntry.isSeparator)
|
|
return;
|
|
if (menuEntry.hasChildren) {
|
|
root.showSubMenu(menuEntry);
|
|
return;
|
|
}
|
|
if (typeof menuEntry.activate === "function") {
|
|
menuEntry.activate();
|
|
} else if (typeof menuEntry.triggered === "function") {
|
|
menuEntry.triggered();
|
|
}
|
|
root.actionTriggered();
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Theme.spacingS
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: Theme.spacingXS
|
|
visible: !menuEntry?.isSeparator
|
|
|
|
Rectangle {
|
|
Layout.preferredWidth: Theme.iconSizeSmall
|
|
Layout.preferredHeight: Theme.iconSizeSmall
|
|
Layout.alignment: Qt.AlignVCenter
|
|
visible: menuEntry?.buttonType !== undefined && menuEntry.buttonType !== 0
|
|
radius: menuEntry?.buttonType === 2 ? Theme.cornerRadiusFull : Theme.cornerRadiusXXS
|
|
border.width: Theme.outlineWidth
|
|
border.color: Theme.outline
|
|
color: "transparent"
|
|
|
|
Rectangle {
|
|
anchors.centerIn: parent
|
|
width: parent.width - Theme.spacingXS - Theme.spacingXXS
|
|
height: parent.height - Theme.spacingXS - Theme.spacingXXS
|
|
radius: menuEntry?.buttonType === 2 ? Theme.cornerRadiusFull : Theme.cornerRadiusXXS
|
|
color: Theme.primary
|
|
visible: menuEntry?.checkState === 2
|
|
}
|
|
|
|
DIcon {
|
|
anchors.centerIn: parent
|
|
name: "check"
|
|
size: Theme.iconSizeSmall - Theme.spacingXS - Theme.spacingXXS
|
|
color: Theme.primaryText
|
|
visible: menuEntry?.buttonType === 1 && menuEntry?.checkState === 2
|
|
}
|
|
}
|
|
|
|
Item {
|
|
Layout.preferredWidth: Theme.iconSizeSmall
|
|
Layout.preferredHeight: Theme.iconSizeSmall
|
|
Layout.alignment: Qt.AlignVCenter
|
|
visible: (menuEntry?.icon ?? "") !== ""
|
|
|
|
Image {
|
|
anchors.fill: parent
|
|
source: menuEntry?.icon || ""
|
|
sourceSize.width: Theme.iconSizeSmall
|
|
sourceSize.height: Theme.iconSizeSmall
|
|
fillMode: Image.PreserveAspectFit
|
|
smooth: true
|
|
}
|
|
}
|
|
|
|
StyledText {
|
|
text: menuEntry?.text || ""
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: (menuEntry?.enabled !== false) ? Theme.surfaceText : Theme.surfaceTextMedium
|
|
elide: Text.ElideRight
|
|
Layout.alignment: Qt.AlignVCenter
|
|
Layout.fillWidth: true
|
|
Layout.minimumWidth: 0
|
|
wrapMode: Text.NoWrap
|
|
}
|
|
|
|
Item {
|
|
Layout.preferredWidth: Theme.iconSizeSmall
|
|
Layout.preferredHeight: Theme.iconSizeSmall
|
|
Layout.alignment: Qt.AlignVCenter
|
|
visible: menuEntry?.hasChildren ?? false
|
|
|
|
DIcon {
|
|
anchors.centerIn: parent
|
|
name: "chevron_right"
|
|
size: Theme.iconSizeSmall - 2
|
|
color: Theme.widgetTextColor
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|