Files
DankMaterialShell/quickshell/Modules/ControlCenter/Widgets/CcMenu.qml
T

230 lines
7.2 KiB
QML

pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modules.ControlCenter
import qs.Widgets
Item {
id: root
property var items: []
readonly property bool open: _open
property bool _open: false
property var _anchor: null
property int _focusIndex: -1
readonly property var visibleItems: (items || []).filter(item => item.visible !== false)
signal closed
anchors.fill: parent
visible: _open || panel.opacity > 0
z: CcMetrics.overlayZ
function _measure() {
let widest = 0;
let hasIcon = false;
for (const item of visibleItems) {
labelMetrics.text = item.label || "";
widest = Math.max(widest, labelMetrics.advanceWidth);
hasIcon = hasIcon || !!item.iconName;
}
const iconSpan = hasIcon ? Theme.iconSizeMedium + Theme.spacingM : 0;
panel.width = Math.max(CcMetrics.menuMinWidth, widest + iconSpan + Theme.spacingM * 2 + Theme.spacingS * 2);
}
function openAt(anchor) {
_anchor = anchor;
_focusIndex = -1;
_measure();
_reposition();
_open = true;
focusScope.forceActiveFocus();
}
function close() {
if (!_open)
return;
_open = false;
closed();
if (_anchor)
_anchor.forceActiveFocus();
}
function _reposition() {
if (!_anchor)
return;
const pos = _anchor.mapToItem(root, 0, 0);
const maxX = root.width - panel.width - Theme.spacingS;
const preferredX = I18n.isRtl ? pos.x : pos.x + _anchor.width - panel.width;
panel.x = Math.max(Theme.spacingS, Math.min(preferredX, maxX));
let y = pos.y + _anchor.height + Theme.spacingXS;
if (y + panel.height > root.height - Theme.spacingS)
y = pos.y - panel.height - Theme.spacingXS;
panel.y = Math.max(Theme.spacingS, y);
}
function _activate(index) {
const item = visibleItems[index];
if (!item || item.enabled === false)
return;
close();
if (typeof item.action === "function")
item.action();
}
function _moveFocus(delta) {
const count = visibleItems.length;
if (count === 0)
return;
let next = _focusIndex;
for (let i = 0; i < count; i++) {
next = (next + delta + count) % count;
if (visibleItems[next].enabled !== false)
break;
}
_focusIndex = next;
}
MouseArea {
anchors.fill: parent
enabled: root._open
acceptedButtons: Qt.AllButtons
onClicked: root.close()
onWheel: wheel => wheel.accepted = true
}
FocusScope {
id: focusScope
anchors.fill: parent
focus: root._open
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_Escape:
root.close();
break;
case Qt.Key_Down:
root._moveFocus(1);
break;
case Qt.Key_Up:
root._moveFocus(-1);
break;
case Qt.Key_Return:
case Qt.Key_Enter:
case Qt.Key_Space:
root._activate(root._focusIndex);
break;
default:
return;
}
event.accepted = true;
}
}
StyledTextMetrics {
id: labelMetrics
font.pixelSize: Theme.fontSizeMedium
}
Rectangle {
id: panel
width: CcMetrics.menuMinWidth
height: root.visibleItems.length * Theme.menuItemHeight + Theme.spacingS * 2
radius: Theme.windowRadius
color: Theme.foregroundColor(Theme.chipSurface, Theme.isFloatingWindow(root))
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
opacity: root._open ? 1 : 0
scale: root._open ? 1 : CcMetrics.popupEnterScale
transformOrigin: I18n.isRtl ? Item.TopLeft : Item.TopRight
Behavior on opacity {
enabled: CcMetrics.animationsEnabled
NumberAnimation {
duration: Theme.expressiveDurations.expressiveEffects
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
}
}
Behavior on scale {
enabled: CcMetrics.animationsEnabled
NumberAnimation {
duration: Theme.expressiveDurations.expressiveFastSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveFastSpatial
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.AllButtons
onClicked: mouse => mouse.accepted = true
onWheel: wheel => wheel.accepted = true
}
Column {
anchors.left: parent.left
anchors.top: parent.top
anchors.margins: Theme.spacingS
width: parent.width - Theme.spacingS * 2
Repeater {
model: root.visibleItems
Rectangle {
id: row
required property var modelData
required property int index
readonly property bool itemEnabled: modelData.enabled !== false
readonly property color contentColor: {
if (!itemEnabled)
return Theme.onSurface_38;
return modelData.destructive ? Theme.error : Theme.surfaceText;
}
width: parent.width
height: Theme.menuItemHeight
radius: Theme.cornerRadiusS
color: root._focusIndex === index && !rowLayer.containsMouse ? Theme.withAlpha(contentColor, Theme.stateLayerFocus) : "transparent"
Row {
id: rowContent
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: row.modelData.iconName || ""
size: Theme.iconSizeMedium
color: row.contentColor
visible: name !== ""
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: row.modelData.label || ""
font.pixelSize: Theme.fontSizeMedium
color: row.contentColor
anchors.verticalCenter: parent.verticalCenter
}
}
StateLayer {
id: rowLayer
disabled: !row.itemEnabled
stateColor: row.contentColor
cornerRadius: row.radius
onClicked: root._activate(row.index)
}
}
}
}
}
}