mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-04 04:42:05 -04:00
1715 lines
76 KiB
QML
1715 lines
76 KiB
QML
import QtQuick
|
|
import QtQuick.Effects
|
|
import Quickshell
|
|
import Quickshell.Hyprland
|
|
import Quickshell.Services.SystemTray
|
|
import Quickshell.Wayland
|
|
import Quickshell.Widgets
|
|
import qs.Common
|
|
import qs.Modules.Plugins
|
|
import qs.Services
|
|
import qs.Widgets
|
|
|
|
BasePill {
|
|
id: root
|
|
|
|
enableBackgroundHover: false
|
|
enableCursor: false
|
|
|
|
property var parentWindow: null
|
|
property bool isAtBottom: false
|
|
property bool isAutoHideBar: false
|
|
readonly property var hiddenTrayIds: {
|
|
const envValue = Quickshell.env("DMS_HIDE_TRAYIDS") || "";
|
|
return envValue ? envValue.split(",").map(id => id.trim().toLowerCase()) : [];
|
|
}
|
|
readonly property var allTrayItems: {
|
|
if (!hiddenTrayIds.length) {
|
|
return SystemTray.items.values;
|
|
}
|
|
return SystemTray.items.values.filter(item => {
|
|
const itemId = item?.id || "";
|
|
return !hiddenTrayIds.includes(itemId.toLowerCase());
|
|
});
|
|
}
|
|
function getTrayItemKey(item) {
|
|
const id = item?.id || "";
|
|
const tooltipTitle = item?.tooltipTitle || "";
|
|
if (!tooltipTitle || tooltipTitle === id) {
|
|
return id;
|
|
}
|
|
return `${id}::${tooltipTitle}`;
|
|
}
|
|
|
|
// ! TODO - replace with either native dbus client (like plugins use) or just a DMS cli or something
|
|
function callContextMenuFallback(trayItemId, globalX, globalY) {
|
|
const script = ['ITEMS=$(dbus-send --session --print-reply --dest=org.kde.StatusNotifierWatcher /StatusNotifierWatcher org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierWatcher string:RegisteredStatusNotifierItems 2>/dev/null)', 'while IFS= read -r line; do', ' line="${line#*\\\"}"', ' line="${line%\\\"*}"', ' [ -z "$line" ] && continue', ' BUS="${line%%/*}"', ' OBJ="/${line#*/}"', ' ID=$(dbus-send --session --print-reply --dest="$BUS" "$OBJ" org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierItem string:Id 2>/dev/null | grep -oP "(?<=\\\")(.*?)(?=\\\")" | tail -1)', ' if [ "$ID" = "$1" ]; then', ' dbus-send --session --type=method_call --dest="$BUS" "$OBJ" org.kde.StatusNotifierItem.ContextMenu int32:"$2" int32:"$3"', ' exit 0', ' fi', 'done <<< "$ITEMS"',].join("\n");
|
|
Quickshell.execDetached(["bash", "-c", script, "_", trayItemId, String(globalX), String(globalY)]);
|
|
}
|
|
|
|
property int _trayOrderTrigger: 0
|
|
|
|
Connections {
|
|
target: SessionData
|
|
function onTrayItemOrderChanged() {
|
|
root._trayOrderTrigger++;
|
|
}
|
|
}
|
|
|
|
function sortByPreferredOrder(items, trigger) {
|
|
void trigger;
|
|
const savedOrder = SessionData.trayItemOrder || [];
|
|
const orderMap = new Map();
|
|
savedOrder.forEach((key, idx) => orderMap.set(key, idx));
|
|
|
|
return [...items].sort((a, b) => {
|
|
const keyA = getTrayItemKey(a);
|
|
const keyB = getTrayItemKey(b);
|
|
const orderA = orderMap.has(keyA) ? orderMap.get(keyA) : 10000 + items.indexOf(a);
|
|
const orderB = orderMap.has(keyB) ? orderMap.get(keyB) : 10000 + items.indexOf(b);
|
|
return orderA - orderB;
|
|
});
|
|
}
|
|
|
|
readonly property var allSortedTrayItems: sortByPreferredOrder(allTrayItems, _trayOrderTrigger)
|
|
readonly property var allSortedTrayItemKeys: allSortedTrayItems.map(item => getTrayItemKey(item))
|
|
readonly property var mainBarItemsRaw: allSortedTrayItems.filter(item => !SessionData.isHiddenTrayId(root.getTrayItemKey(item)))
|
|
readonly property var mainBarItems: mainBarItemsRaw.map((item, idx) => ({
|
|
key: getTrayItemKey(item),
|
|
item: item
|
|
}))
|
|
readonly property var hiddenBarItems: allSortedTrayItems.filter(item => SessionData.isHiddenTrayId(root.getTrayItemKey(item)))
|
|
|
|
function moveTrayItemInFullOrder(visibleFromIndex, visibleToIndex) {
|
|
if (visibleFromIndex === visibleToIndex || visibleFromIndex < 0 || visibleToIndex < 0)
|
|
return;
|
|
|
|
const fromKey = mainBarItems[visibleFromIndex]?.key ?? null;
|
|
const toKey = mainBarItems[visibleToIndex]?.key ?? null;
|
|
if (!fromKey || !toKey)
|
|
return;
|
|
|
|
const fullOrder = [...allSortedTrayItemKeys];
|
|
const fullFromIndex = fullOrder.indexOf(fromKey);
|
|
const fullToIndex = fullOrder.indexOf(toKey);
|
|
if (fullFromIndex < 0 || fullToIndex < 0)
|
|
return;
|
|
|
|
const movedKey = fullOrder.splice(fullFromIndex, 1)[0];
|
|
fullOrder.splice(fullToIndex, 0, movedKey);
|
|
SessionData.setTrayItemOrder(fullOrder);
|
|
}
|
|
|
|
property int draggedIndex: -1
|
|
property int dropTargetIndex: -1
|
|
property bool suppressShiftAnimation: false
|
|
readonly property bool hasHiddenItems: allTrayItems.length > mainBarItems.length
|
|
visible: allTrayItems.length > 0
|
|
opacity: allTrayItems.length > 0 ? 1 : 0
|
|
|
|
states: [
|
|
State {
|
|
name: "hidden_horizontal"
|
|
when: allTrayItems.length === 0 && !isVerticalOrientation
|
|
PropertyChanges {
|
|
target: root
|
|
width: 0
|
|
}
|
|
},
|
|
State {
|
|
name: "hidden_vertical"
|
|
when: allTrayItems.length === 0 && isVerticalOrientation
|
|
PropertyChanges {
|
|
target: root
|
|
height: 0
|
|
}
|
|
}
|
|
]
|
|
|
|
transitions: [
|
|
Transition {
|
|
NumberAnimation {
|
|
properties: "width,height"
|
|
duration: Theme.shortDuration
|
|
easing.type: Theme.standardEasing
|
|
}
|
|
}
|
|
]
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation {
|
|
duration: Theme.shortDuration
|
|
easing.type: Theme.standardEasing
|
|
}
|
|
}
|
|
|
|
readonly property real trayItemSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + 6
|
|
|
|
readonly property real minTooltipY: {
|
|
if (!parentScreen || !isVerticalOrientation) {
|
|
return 0;
|
|
}
|
|
|
|
if (isAutoHideBar) {
|
|
return 0;
|
|
}
|
|
|
|
if (parentScreen.y > 0) {
|
|
const estimatedTopBarHeight = barThickness + barSpacing;
|
|
return estimatedTopBarHeight;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
property bool menuOpen: false
|
|
property var currentTrayMenu: null
|
|
|
|
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 ? columnComp : rowComp
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: rowComp
|
|
Row {
|
|
spacing: 0
|
|
|
|
Repeater {
|
|
model: ScriptModel {
|
|
values: root.mainBarItems
|
|
objectProp: "key"
|
|
}
|
|
|
|
delegate: Item {
|
|
id: delegateRoot
|
|
property var trayItem: modelData.item
|
|
property string itemKey: modelData.key
|
|
property string iconSource: {
|
|
let icon = trayItem && trayItem.icon;
|
|
if (typeof icon === 'string' || icon instanceof String) {
|
|
if (icon === "")
|
|
return "";
|
|
if (icon.includes("?path=")) {
|
|
const split = icon.split("?path=");
|
|
if (split.length !== 2)
|
|
return icon;
|
|
const name = split[0];
|
|
const path = split[1];
|
|
let fileName = name.substring(name.lastIndexOf("/") + 1);
|
|
if (fileName.startsWith("dropboxstatus")) {
|
|
fileName = `hicolor/16x16/status/${fileName}`;
|
|
}
|
|
return `file://${path}/${fileName}`;
|
|
}
|
|
if (icon.startsWith("/") && !icon.startsWith("file://"))
|
|
return `file://${icon}`;
|
|
return icon;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
width: root.trayItemSize
|
|
height: root.barThickness
|
|
z: dragHandler.dragging ? 100 : 0
|
|
|
|
property real shiftOffset: {
|
|
if (root.draggedIndex < 0)
|
|
return 0;
|
|
if (index === root.draggedIndex)
|
|
return 0;
|
|
const dragIdx = root.draggedIndex;
|
|
const dropIdx = root.dropTargetIndex;
|
|
const shiftAmount = root.trayItemSize;
|
|
if (dropIdx < 0)
|
|
return 0;
|
|
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
|
|
return -shiftAmount;
|
|
if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx)
|
|
return shiftAmount;
|
|
return 0;
|
|
}
|
|
|
|
transform: Translate {
|
|
x: delegateRoot.shiftOffset
|
|
Behavior on x {
|
|
enabled: !root.suppressShiftAnimation
|
|
NumberAnimation {
|
|
duration: 150
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: dragHandler
|
|
anchors.fill: parent
|
|
property bool dragging: false
|
|
property point dragStartPos: Qt.point(0, 0)
|
|
property real dragAxisOffset: 0
|
|
property bool longPressing: false
|
|
|
|
Timer {
|
|
id: longPressTimer
|
|
interval: 400
|
|
repeat: false
|
|
onTriggered: dragHandler.longPressing = true
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: visualContent
|
|
width: root.trayItemSize
|
|
height: root.trayItemSize
|
|
anchors.centerIn: parent
|
|
radius: Theme.cornerRadius
|
|
color: trayItemArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
|
border.width: dragHandler.dragging ? 2 : 0
|
|
border.color: Theme.primary
|
|
opacity: dragHandler.dragging ? 0.8 : 1.0
|
|
|
|
transform: Translate {
|
|
x: dragHandler.dragging ? dragHandler.dragAxisOffset : 0
|
|
}
|
|
|
|
IconImage {
|
|
id: iconImg
|
|
anchors.centerIn: parent
|
|
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
|
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
|
source: delegateRoot.iconSource
|
|
asynchronous: true
|
|
smooth: true
|
|
mipmap: true
|
|
visible: status === Image.Ready
|
|
}
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
visible: !iconImg.visible
|
|
text: {
|
|
const itemId = trayItem?.id || "";
|
|
if (!itemId)
|
|
return "?";
|
|
return itemId.charAt(0).toUpperCase();
|
|
}
|
|
font.pixelSize: 10
|
|
color: Theme.widgetTextColor
|
|
}
|
|
|
|
DankRipple {
|
|
id: itemRipple
|
|
cornerRadius: Theme.cornerRadius
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: trayItemArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
cursorShape: dragHandler.longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
|
|
|
|
onPressed: mouse => {
|
|
const pos = mapToItem(visualContent, mouse.x, mouse.y);
|
|
itemRipple.trigger(pos.x, pos.y);
|
|
if (mouse.button === Qt.LeftButton) {
|
|
dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
|
|
longPressTimer.start();
|
|
}
|
|
}
|
|
|
|
onReleased: mouse => {
|
|
longPressTimer.stop();
|
|
const wasDragging = dragHandler.dragging;
|
|
const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex;
|
|
|
|
if (didReorder) {
|
|
root.suppressShiftAnimation = true;
|
|
root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex);
|
|
Qt.callLater(() => root.suppressShiftAnimation = false);
|
|
}
|
|
|
|
dragHandler.longPressing = false;
|
|
dragHandler.dragging = false;
|
|
dragHandler.dragAxisOffset = 0;
|
|
root.draggedIndex = -1;
|
|
root.dropTargetIndex = -1;
|
|
|
|
if (wasDragging || mouse.button !== Qt.LeftButton)
|
|
return;
|
|
|
|
if (!delegateRoot.trayItem)
|
|
return;
|
|
if (!delegateRoot.trayItem.onlyMenu) {
|
|
delegateRoot.trayItem.activate();
|
|
return;
|
|
}
|
|
if (!delegateRoot.trayItem.hasMenu)
|
|
return;
|
|
root.menuOpen = false;
|
|
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
|
}
|
|
|
|
onPositionChanged: mouse => {
|
|
if (dragHandler.longPressing && !dragHandler.dragging) {
|
|
const distance = Math.abs(mouse.x - dragHandler.dragStartPos.x);
|
|
if (distance > 5) {
|
|
dragHandler.dragging = true;
|
|
root.draggedIndex = index;
|
|
root.dropTargetIndex = index;
|
|
}
|
|
}
|
|
if (!dragHandler.dragging)
|
|
return;
|
|
|
|
const axisOffset = mouse.x - dragHandler.dragStartPos.x;
|
|
dragHandler.dragAxisOffset = axisOffset;
|
|
const itemSize = root.trayItemSize;
|
|
const slotOffset = Math.round(axisOffset / itemSize);
|
|
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
|
|
if (newTargetIndex !== root.dropTargetIndex) {
|
|
root.dropTargetIndex = newTargetIndex;
|
|
}
|
|
}
|
|
|
|
onClicked: mouse => {
|
|
if (dragHandler.dragging)
|
|
return;
|
|
if (mouse.button !== Qt.RightButton)
|
|
return;
|
|
if (!delegateRoot.trayItem?.hasMenu) {
|
|
const gp = trayItemArea.mapToGlobal(mouse.x, mouse.y);
|
|
root.callContextMenuFallback(delegateRoot.trayItem.id, Math.round(gp.x), Math.round(gp.y));
|
|
return;
|
|
}
|
|
root.menuOpen = false;
|
|
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
width: root.trayItemSize
|
|
height: root.barThickness
|
|
visible: root.hasHiddenItems
|
|
|
|
Rectangle {
|
|
id: caretButton
|
|
width: root.trayItemSize
|
|
height: root.trayItemSize
|
|
anchors.centerIn: parent
|
|
radius: Theme.cornerRadius
|
|
color: caretArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
|
|
|
DankIcon {
|
|
anchors.centerIn: parent
|
|
name: root.menuOpen ? "expand_less" : "expand_more"
|
|
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
|
color: Theme.widgetTextColor
|
|
}
|
|
|
|
DankRipple {
|
|
id: caretRipple
|
|
cornerRadius: Theme.cornerRadius
|
|
}
|
|
|
|
MouseArea {
|
|
id: caretArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onPressed: mouse => {
|
|
caretRipple.trigger(mouse.x, mouse.y);
|
|
}
|
|
onClicked: root.menuOpen = !root.menuOpen
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: columnComp
|
|
Column {
|
|
spacing: 0
|
|
|
|
Repeater {
|
|
model: ScriptModel {
|
|
values: root.mainBarItems
|
|
objectProp: "key"
|
|
}
|
|
|
|
delegate: Item {
|
|
id: delegateRoot
|
|
property var trayItem: modelData.item
|
|
property string itemKey: modelData.key
|
|
property string iconSource: {
|
|
let icon = trayItem && trayItem.icon;
|
|
if (typeof icon === 'string' || icon instanceof String) {
|
|
if (icon === "")
|
|
return "";
|
|
if (icon.includes("?path=")) {
|
|
const split = icon.split("?path=");
|
|
if (split.length !== 2)
|
|
return icon;
|
|
const name = split[0];
|
|
const path = split[1];
|
|
let fileName = name.substring(name.lastIndexOf("/") + 1);
|
|
if (fileName.startsWith("dropboxstatus")) {
|
|
fileName = `hicolor/16x16/status/${fileName}`;
|
|
}
|
|
return `file://${path}/${fileName}`;
|
|
}
|
|
if (icon.startsWith("/") && !icon.startsWith("file://"))
|
|
return `file://${icon}`;
|
|
return icon;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
width: root.barThickness
|
|
height: root.trayItemSize
|
|
z: dragHandler.dragging ? 100 : 0
|
|
|
|
property real shiftOffset: {
|
|
if (root.draggedIndex < 0)
|
|
return 0;
|
|
if (index === root.draggedIndex)
|
|
return 0;
|
|
const dragIdx = root.draggedIndex;
|
|
const dropIdx = root.dropTargetIndex;
|
|
const shiftAmount = root.trayItemSize;
|
|
if (dropIdx < 0)
|
|
return 0;
|
|
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
|
|
return -shiftAmount;
|
|
if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx)
|
|
return shiftAmount;
|
|
return 0;
|
|
}
|
|
|
|
transform: Translate {
|
|
y: delegateRoot.shiftOffset
|
|
Behavior on y {
|
|
enabled: !root.suppressShiftAnimation
|
|
NumberAnimation {
|
|
duration: 150
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: dragHandler
|
|
anchors.fill: parent
|
|
property bool dragging: false
|
|
property point dragStartPos: Qt.point(0, 0)
|
|
property real dragAxisOffset: 0
|
|
property bool longPressing: false
|
|
|
|
Timer {
|
|
id: longPressTimer
|
|
interval: 400
|
|
repeat: false
|
|
onTriggered: dragHandler.longPressing = true
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: visualContent
|
|
width: root.trayItemSize
|
|
height: root.trayItemSize
|
|
anchors.centerIn: parent
|
|
radius: Theme.cornerRadius
|
|
color: trayItemArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
|
border.width: dragHandler.dragging ? 2 : 0
|
|
border.color: Theme.primary
|
|
opacity: dragHandler.dragging ? 0.8 : 1.0
|
|
|
|
transform: Translate {
|
|
y: dragHandler.dragging ? dragHandler.dragAxisOffset : 0
|
|
}
|
|
|
|
IconImage {
|
|
id: iconImg
|
|
anchors.centerIn: parent
|
|
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
|
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
|
source: delegateRoot.iconSource
|
|
asynchronous: true
|
|
smooth: true
|
|
mipmap: true
|
|
visible: status === Image.Ready
|
|
}
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
visible: !iconImg.visible
|
|
text: {
|
|
const itemId = trayItem?.id || "";
|
|
if (!itemId)
|
|
return "?";
|
|
return itemId.charAt(0).toUpperCase();
|
|
}
|
|
font.pixelSize: 10
|
|
color: Theme.widgetTextColor
|
|
}
|
|
|
|
DankRipple {
|
|
id: itemRipple
|
|
cornerRadius: Theme.cornerRadius
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: trayItemArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
cursorShape: dragHandler.longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
|
|
|
|
onPressed: mouse => {
|
|
const pos = mapToItem(visualContent, mouse.x, mouse.y);
|
|
itemRipple.trigger(pos.x, pos.y);
|
|
if (mouse.button === Qt.LeftButton) {
|
|
dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
|
|
longPressTimer.start();
|
|
}
|
|
}
|
|
|
|
onReleased: mouse => {
|
|
longPressTimer.stop();
|
|
const wasDragging = dragHandler.dragging;
|
|
const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex;
|
|
|
|
if (didReorder) {
|
|
root.suppressShiftAnimation = true;
|
|
root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex);
|
|
Qt.callLater(() => root.suppressShiftAnimation = false);
|
|
}
|
|
|
|
dragHandler.longPressing = false;
|
|
dragHandler.dragging = false;
|
|
dragHandler.dragAxisOffset = 0;
|
|
root.draggedIndex = -1;
|
|
root.dropTargetIndex = -1;
|
|
|
|
if (wasDragging || mouse.button !== Qt.LeftButton)
|
|
return;
|
|
|
|
if (!delegateRoot.trayItem)
|
|
return;
|
|
if (!delegateRoot.trayItem.onlyMenu) {
|
|
delegateRoot.trayItem.activate();
|
|
return;
|
|
}
|
|
if (!delegateRoot.trayItem.hasMenu)
|
|
return;
|
|
root.menuOpen = false;
|
|
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
|
}
|
|
|
|
onPositionChanged: mouse => {
|
|
if (dragHandler.longPressing && !dragHandler.dragging) {
|
|
const distance = Math.abs(mouse.y - dragHandler.dragStartPos.y);
|
|
if (distance > 5) {
|
|
dragHandler.dragging = true;
|
|
root.draggedIndex = index;
|
|
root.dropTargetIndex = index;
|
|
}
|
|
}
|
|
if (!dragHandler.dragging)
|
|
return;
|
|
|
|
const axisOffset = mouse.y - dragHandler.dragStartPos.y;
|
|
dragHandler.dragAxisOffset = axisOffset;
|
|
const itemSize = root.trayItemSize;
|
|
const slotOffset = Math.round(axisOffset / itemSize);
|
|
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
|
|
if (newTargetIndex !== root.dropTargetIndex) {
|
|
root.dropTargetIndex = newTargetIndex;
|
|
}
|
|
}
|
|
|
|
onClicked: mouse => {
|
|
if (dragHandler.dragging)
|
|
return;
|
|
if (mouse.button !== Qt.RightButton)
|
|
return;
|
|
if (!delegateRoot.trayItem?.hasMenu) {
|
|
const gp = trayItemArea.mapToGlobal(mouse.x, mouse.y);
|
|
root.callContextMenuFallback(delegateRoot.trayItem.id, Math.round(gp.x), Math.round(gp.y));
|
|
return;
|
|
}
|
|
root.menuOpen = false;
|
|
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
width: root.barThickness
|
|
height: root.trayItemSize
|
|
visible: root.hasHiddenItems
|
|
|
|
Rectangle {
|
|
id: caretButtonVert
|
|
width: root.trayItemSize
|
|
height: root.trayItemSize
|
|
anchors.centerIn: parent
|
|
radius: Theme.cornerRadius
|
|
color: caretAreaVert.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
|
|
|
DankIcon {
|
|
anchors.centerIn: parent
|
|
name: {
|
|
const edge = root.axis?.edge;
|
|
if (edge === "left") {
|
|
return root.menuOpen ? "chevron_left" : "chevron_right";
|
|
} else {
|
|
return root.menuOpen ? "chevron_right" : "chevron_left";
|
|
}
|
|
}
|
|
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
|
color: Theme.widgetTextColor
|
|
}
|
|
|
|
DankRipple {
|
|
id: caretRippleVert
|
|
cornerRadius: Theme.cornerRadius
|
|
}
|
|
|
|
MouseArea {
|
|
id: caretAreaVert
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onPressed: mouse => {
|
|
caretRippleVert.trigger(mouse.x, mouse.y);
|
|
}
|
|
onClicked: root.menuOpen = !root.menuOpen
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
PanelWindow {
|
|
id: overflowMenu
|
|
visible: root.menuOpen
|
|
screen: root.parentScreen
|
|
WlrLayershell.layer: WlrLayershell.Top
|
|
WlrLayershell.exclusiveZone: -1
|
|
WlrLayershell.keyboardFocus: {
|
|
if (!root.menuOpen)
|
|
return WlrKeyboardFocus.None;
|
|
if (CompositorService.useHyprlandFocusGrab)
|
|
return WlrKeyboardFocus.OnDemand;
|
|
return WlrKeyboardFocus.Exclusive;
|
|
}
|
|
WlrLayershell.namespace: "dms:tray-overflow-menu"
|
|
color: "transparent"
|
|
|
|
HyprlandFocusGrab {
|
|
windows: [overflowMenu]
|
|
active: CompositorService.useHyprlandFocusGrab && root.menuOpen
|
|
}
|
|
|
|
Connections {
|
|
target: PopoutManager
|
|
function onPopoutOpening() {
|
|
root.menuOpen = false;
|
|
}
|
|
}
|
|
|
|
Component.onDestruction: {
|
|
if (root.parentScreen) {
|
|
TrayMenuManager.unregisterMenu(root.parentScreen.name);
|
|
}
|
|
}
|
|
|
|
function close() {
|
|
root.menuOpen = false;
|
|
}
|
|
|
|
anchors {
|
|
top: true
|
|
left: true
|
|
right: true
|
|
bottom: true
|
|
}
|
|
|
|
readonly property real dpr: (typeof CompositorService !== "undefined" && CompositorService.getScreenScale) ? CompositorService.getScreenScale(overflowMenu.screen) : (screen?.devicePixelRatio || 1)
|
|
property point anchorPos: Qt.point(screen.width / 2, screen.height / 2)
|
|
|
|
property var barBounds: {
|
|
if (!overflowMenu.screen || !root.barConfig) {
|
|
return {
|
|
"x": 0,
|
|
"y": 0,
|
|
"width": 0,
|
|
"height": 0,
|
|
"wingSize": 0
|
|
};
|
|
}
|
|
const barPosition = root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1));
|
|
return SettingsData.getBarBounds(overflowMenu.screen, root.barThickness + root.barSpacing, barPosition, root.barConfig);
|
|
}
|
|
|
|
property real barX: barBounds.x
|
|
property real barY: barBounds.y
|
|
property real barWidth: barBounds.width
|
|
property real barHeight: barBounds.height
|
|
|
|
readonly property int barPosition: root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1))
|
|
readonly property var adjacentBarInfo: parentScreen ? SettingsData.getAdjacentBarInfo(parentScreen, barPosition, root.barConfig) : ({
|
|
"topBar": 0,
|
|
"bottomBar": 0,
|
|
"leftBar": 0,
|
|
"rightBar": 0
|
|
})
|
|
readonly property real effectiveBarSize: root.barThickness + root.barSpacing
|
|
|
|
readonly property real maskX: {
|
|
const triggeringBarX = (barPosition === 2) ? effectiveBarSize : 0;
|
|
const adjacentLeftBar = adjacentBarInfo?.leftBar ?? 0;
|
|
return Math.max(triggeringBarX, adjacentLeftBar);
|
|
}
|
|
|
|
readonly property real maskY: {
|
|
const triggeringBarY = (barPosition === 0) ? effectiveBarSize : 0;
|
|
const adjacentTopBar = adjacentBarInfo?.topBar ?? 0;
|
|
return Math.max(triggeringBarY, adjacentTopBar);
|
|
}
|
|
|
|
readonly property real maskWidth: {
|
|
const triggeringBarRight = (barPosition === 3) ? effectiveBarSize : 0;
|
|
const adjacentRightBar = adjacentBarInfo?.rightBar ?? 0;
|
|
const rightExclusion = Math.max(triggeringBarRight, adjacentRightBar);
|
|
return Math.max(100, width - maskX - rightExclusion);
|
|
}
|
|
|
|
readonly property real maskHeight: {
|
|
const triggeringBarBottom = (barPosition === 1) ? effectiveBarSize : 0;
|
|
const adjacentBottomBar = adjacentBarInfo?.bottomBar ?? 0;
|
|
const bottomExclusion = Math.max(triggeringBarBottom, adjacentBottomBar);
|
|
return Math.max(100, height - maskY - bottomExclusion);
|
|
}
|
|
|
|
mask: Region {
|
|
item: Rectangle {
|
|
x: overflowMenu.maskX
|
|
y: overflowMenu.maskY
|
|
width: overflowMenu.maskWidth
|
|
height: overflowMenu.maskHeight
|
|
}
|
|
}
|
|
|
|
onVisibleChanged: {
|
|
if (visible) {
|
|
if (currentTrayMenu) {
|
|
currentTrayMenu.showMenu = false;
|
|
}
|
|
if (root.parentScreen) {
|
|
TrayMenuManager.registerMenu(root.parentScreen.name, overflowMenu);
|
|
}
|
|
PopoutManager.closeAllPopouts();
|
|
ModalManager.closeAllModalsExcept(null);
|
|
updatePosition();
|
|
} else if (!visible && root.parentScreen) {
|
|
TrayMenuManager.unregisterMenu(root.parentScreen.name);
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
x: overflowMenu.maskX
|
|
y: overflowMenu.maskY
|
|
width: overflowMenu.maskWidth
|
|
height: overflowMenu.maskHeight
|
|
z: -1
|
|
enabled: root.menuOpen
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
|
onClicked: mouse => {
|
|
const clickX = mouse.x + overflowMenu.maskX;
|
|
const clickY = mouse.y + overflowMenu.maskY;
|
|
const outsideContent = clickX < menuContainer.x || clickX > menuContainer.x + menuContainer.width || clickY < menuContainer.y || clickY > menuContainer.y + menuContainer.height;
|
|
|
|
if (!outsideContent)
|
|
return;
|
|
root.menuOpen = false;
|
|
}
|
|
}
|
|
|
|
FocusScope {
|
|
id: overflowFocusScope
|
|
anchors.fill: parent
|
|
focus: true
|
|
|
|
Keys.onEscapePressed: {
|
|
root.menuOpen = false;
|
|
}
|
|
}
|
|
|
|
function updatePosition() {
|
|
const globalPos = root.mapToGlobal(0, 0);
|
|
const screenX = screen.x || 0;
|
|
const screenY = screen.y || 0;
|
|
const relativeX = globalPos.x - screenX;
|
|
const relativeY = globalPos.y - screenY;
|
|
|
|
if (root.isVerticalOrientation) {
|
|
const edge = root.axis?.edge;
|
|
let targetX = edge === "left" ? root.barThickness + root.barSpacing + Theme.popupDistance : screen.width - (root.barThickness + root.barSpacing + Theme.popupDistance);
|
|
const adjustedY = relativeY + root.height / 2 + root.minTooltipY;
|
|
anchorPos = Qt.point(targetX, adjustedY);
|
|
} else {
|
|
let targetY = root.isAtBottom ? screen.height - (root.barThickness + root.barSpacing + (barConfig?.bottomGap ?? 0) + Theme.popupDistance) : root.barThickness + root.barSpacing + (barConfig?.bottomGap ?? 0) + Theme.popupDistance;
|
|
anchorPos = Qt.point(relativeX + root.width / 2, targetY);
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: menuContainer
|
|
objectName: "overflowMenuContainer"
|
|
|
|
readonly property real rawWidth: {
|
|
const itemCount = root.hiddenBarItems.length;
|
|
const cols = Math.min(5, itemCount);
|
|
const itemSize = root.trayItemSize + 4;
|
|
const spacing = 2;
|
|
return cols * itemSize + (cols - 1) * spacing + Theme.spacingS * 2;
|
|
}
|
|
readonly property real rawHeight: {
|
|
const itemCount = root.hiddenBarItems.length;
|
|
const cols = Math.min(5, itemCount);
|
|
const rows = Math.ceil(itemCount / cols);
|
|
const itemSize = root.trayItemSize + 4;
|
|
const spacing = 2;
|
|
return rows * itemSize + (rows - 1) * spacing + Theme.spacingS * 2;
|
|
}
|
|
|
|
readonly property real alignedWidth: Theme.px(rawWidth, overflowMenu.dpr)
|
|
readonly property real alignedHeight: Theme.px(rawHeight, overflowMenu.dpr)
|
|
|
|
width: alignedWidth
|
|
height: alignedHeight
|
|
|
|
x: Theme.snap((() => {
|
|
if (root.isVerticalOrientation) {
|
|
const edge = root.axis?.edge;
|
|
if (edge === "left") {
|
|
const targetX = overflowMenu.anchorPos.x;
|
|
return Math.min(overflowMenu.screen.width - alignedWidth - 10, targetX);
|
|
} else {
|
|
const targetX = overflowMenu.anchorPos.x - alignedWidth;
|
|
return Math.max(10, targetX);
|
|
}
|
|
} else {
|
|
const left = 10;
|
|
const right = overflowMenu.width - alignedWidth - 10;
|
|
const want = overflowMenu.anchorPos.x - alignedWidth / 2;
|
|
return Math.max(left, Math.min(right, want));
|
|
}
|
|
})(), overflowMenu.dpr)
|
|
|
|
y: Theme.snap((() => {
|
|
if (root.isVerticalOrientation) {
|
|
const top = Math.max(overflowMenu.barY, 10);
|
|
const bottom = overflowMenu.height - alignedHeight - 10;
|
|
const want = overflowMenu.anchorPos.y - alignedHeight / 2;
|
|
return Math.max(top, Math.min(bottom, want));
|
|
} else {
|
|
if (root.isAtBottom) {
|
|
const targetY = overflowMenu.anchorPos.y - alignedHeight;
|
|
return Math.max(10, targetY);
|
|
} else {
|
|
const targetY = overflowMenu.anchorPos.y;
|
|
return Math.min(overflowMenu.screen.height - alignedHeight - 10, targetY);
|
|
}
|
|
}
|
|
})(), overflowMenu.dpr)
|
|
|
|
readonly property var elev: Theme.elevationLevel2
|
|
property real shadowBlurPx: elev && elev.blurPx !== undefined ? elev.blurPx : 8
|
|
property real shadowSpreadPx: elev && elev.spreadPx !== undefined ? elev.spreadPx : 0
|
|
property real shadowBaseAlpha: elev && elev.alpha !== undefined ? elev.alpha : 0.25
|
|
readonly property real popupSurfaceAlpha: Theme.popupTransparency
|
|
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha))
|
|
|
|
opacity: root.menuOpen ? 1 : 0
|
|
scale: root.menuOpen ? 1 : 0.85
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation {
|
|
duration: Theme.mediumDuration
|
|
easing.type: Theme.emphasizedEasing
|
|
}
|
|
}
|
|
|
|
Behavior on scale {
|
|
NumberAnimation {
|
|
duration: Theme.mediumDuration
|
|
easing.type: Theme.emphasizedEasing
|
|
}
|
|
}
|
|
|
|
ElevationShadow {
|
|
id: bgShadowLayer
|
|
anchors.fill: parent
|
|
level: menuContainer.elev
|
|
fallbackOffset: 4
|
|
shadowBlurPx: menuContainer.shadowBlurPx
|
|
shadowSpreadPx: menuContainer.shadowSpreadPx
|
|
shadowColor: {
|
|
const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest;
|
|
return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha);
|
|
}
|
|
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
|
targetRadius: Theme.cornerRadius
|
|
sourceRect.antialiasing: true
|
|
sourceRect.smooth: true
|
|
shadowEnabled: Theme.elevationEnabled
|
|
layer.smooth: true
|
|
layer.textureSize: Qt.size(Math.round(width * overflowMenu.dpr * 2), Math.round(height * overflowMenu.dpr * 2))
|
|
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
|
layer.samples: 4
|
|
}
|
|
|
|
Grid {
|
|
id: menuGrid
|
|
anchors.centerIn: parent
|
|
columns: Math.min(5, root.hiddenBarItems.length)
|
|
spacing: 2
|
|
rowSpacing: 2
|
|
|
|
Repeater {
|
|
model: root.hiddenBarItems
|
|
|
|
delegate: Rectangle {
|
|
property var trayItem: modelData
|
|
property string iconSource: {
|
|
let icon = trayItem?.icon;
|
|
if (typeof icon === 'string' || icon instanceof String) {
|
|
if (icon === "")
|
|
return "";
|
|
if (icon.includes("?path=")) {
|
|
const split = icon.split("?path=");
|
|
if (split.length !== 2)
|
|
return icon;
|
|
const name = split[0];
|
|
const path = split[1];
|
|
let fileName = name.substring(name.lastIndexOf("/") + 1);
|
|
if (fileName.startsWith("dropboxstatus")) {
|
|
fileName = `hicolor/16x16/status/${fileName}`;
|
|
}
|
|
return `file://${path}/${fileName}`;
|
|
}
|
|
if (icon.startsWith("/") && !icon.startsWith("file://")) {
|
|
return `file://${icon}`;
|
|
}
|
|
return icon;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
width: root.trayItemSize + 4
|
|
height: root.trayItemSize + 4
|
|
radius: Theme.cornerRadius
|
|
color: itemArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.withAlpha(Theme.surfaceContainer, 0)
|
|
|
|
IconImage {
|
|
id: menuIconImg
|
|
anchors.centerIn: parent
|
|
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
|
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
|
source: parent.iconSource
|
|
asynchronous: true
|
|
smooth: true
|
|
mipmap: true
|
|
visible: status === Image.Ready
|
|
}
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
visible: !menuIconImg.visible
|
|
text: {
|
|
const itemId = trayItem?.id || "";
|
|
if (!itemId)
|
|
return "?";
|
|
return itemId.charAt(0).toUpperCase();
|
|
}
|
|
font.pixelSize: 10
|
|
color: Theme.widgetTextColor
|
|
}
|
|
|
|
MouseArea {
|
|
id: itemArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: mouse => {
|
|
if (!trayItem)
|
|
return;
|
|
if (mouse.button === Qt.LeftButton && !trayItem.onlyMenu) {
|
|
trayItem.activate();
|
|
root.menuOpen = false;
|
|
return;
|
|
}
|
|
if (!trayItem.hasMenu) {
|
|
const gp = itemArea.mapToGlobal(mouse.x, mouse.y);
|
|
root.callContextMenuFallback(trayItem.id, Math.round(gp.x), Math.round(gp.y));
|
|
return;
|
|
}
|
|
root.showForTrayItem(trayItem, menuContainer, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: trayMenuComponent
|
|
|
|
Rectangle {
|
|
id: menuRoot
|
|
|
|
property var trayItem: null
|
|
property var anchorItem: null
|
|
property var parentScreen: null
|
|
property bool isAtBottom: false
|
|
property bool isVertical: false
|
|
property var axis: null
|
|
property bool showMenu: false
|
|
property var menuHandle: null
|
|
|
|
ListModel {
|
|
id: entryStack
|
|
}
|
|
function topEntry() {
|
|
return entryStack.count ? entryStack.get(entryStack.count - 1).handle : null;
|
|
}
|
|
|
|
function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) {
|
|
trayItem = item;
|
|
anchorItem = anchor;
|
|
parentScreen = screen;
|
|
isAtBottom = atBottom;
|
|
isVertical = vertical;
|
|
axis = axisObj;
|
|
menuHandle = item?.menu;
|
|
|
|
if (parentScreen) {
|
|
for (var i = 0; i < Quickshell.screens.length; i++) {
|
|
const s = Quickshell.screens[i];
|
|
if (s === parentScreen) {
|
|
menuWindow.screen = s;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
showMenu = true;
|
|
}
|
|
|
|
function close() {
|
|
showMenu = false;
|
|
}
|
|
|
|
Connections {
|
|
target: menuWindow
|
|
function onVisibleChanged() {
|
|
if (menuWindow.visible && parentScreen) {
|
|
TrayMenuManager.registerMenu(parentScreen.name, menuRoot);
|
|
} else if (!menuWindow.visible && parentScreen) {
|
|
TrayMenuManager.unregisterMenu(parentScreen.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
Component.onDestruction: {
|
|
if (parentScreen) {
|
|
TrayMenuManager.unregisterMenu(parentScreen.name);
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: PopoutManager
|
|
function onPopoutOpening() {
|
|
menuRoot.close();
|
|
}
|
|
}
|
|
|
|
function closeWithAction() {
|
|
close();
|
|
}
|
|
|
|
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();
|
|
|
|
submenuHydrator.menu = h;
|
|
submenuHydrator.open();
|
|
Qt.callLater(() => submenuHydrator.close());
|
|
}
|
|
|
|
function goBack() {
|
|
if (!entryStack.count)
|
|
return;
|
|
entryStack.remove(entryStack.count - 1);
|
|
}
|
|
|
|
width: 0
|
|
height: 0
|
|
color: "transparent"
|
|
|
|
PanelWindow {
|
|
id: menuWindow
|
|
|
|
WlrLayershell.namespace: "dms:tray-menu-window"
|
|
visible: menuRoot.showMenu && (menuRoot.trayItem?.hasMenu ?? false)
|
|
WlrLayershell.layer: WlrLayershell.Top
|
|
WlrLayershell.exclusiveZone: -1
|
|
WlrLayershell.keyboardFocus: {
|
|
if (!menuRoot.showMenu)
|
|
return WlrKeyboardFocus.None;
|
|
if (CompositorService.useHyprlandFocusGrab)
|
|
return WlrKeyboardFocus.OnDemand;
|
|
return WlrKeyboardFocus.Exclusive;
|
|
}
|
|
color: "transparent"
|
|
|
|
HyprlandFocusGrab {
|
|
windows: [menuWindow]
|
|
active: CompositorService.useHyprlandFocusGrab && menuRoot.showMenu
|
|
}
|
|
|
|
anchors {
|
|
top: true
|
|
left: true
|
|
right: true
|
|
bottom: true
|
|
}
|
|
|
|
readonly property real dpr: (typeof CompositorService !== "undefined" && CompositorService.getScreenScale) ? CompositorService.getScreenScale(menuWindow.screen) : (screen?.devicePixelRatio || 1)
|
|
property point anchorPos: Qt.point(screen.width / 2, screen.height / 2)
|
|
|
|
property var barBounds: {
|
|
if (!menuWindow.screen || !root.barConfig) {
|
|
return {
|
|
"x": 0,
|
|
"y": 0,
|
|
"width": 0,
|
|
"height": 0,
|
|
"wingSize": 0
|
|
};
|
|
}
|
|
const barPosition = root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1));
|
|
return SettingsData.getBarBounds(menuWindow.screen, root.barThickness + root.barSpacing, barPosition, root.barConfig);
|
|
}
|
|
|
|
property real barX: barBounds.x
|
|
property real barY: barBounds.y
|
|
property real barWidth: barBounds.width
|
|
property real barHeight: barBounds.height
|
|
|
|
readonly property int barPosition: root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1))
|
|
readonly property var adjacentBarInfo: menuRoot.parentScreen ? SettingsData.getAdjacentBarInfo(menuRoot.parentScreen, barPosition, root.barConfig) : ({
|
|
"topBar": 0,
|
|
"bottomBar": 0,
|
|
"leftBar": 0,
|
|
"rightBar": 0
|
|
})
|
|
readonly property real effectiveBarSize: root.barThickness + root.barSpacing
|
|
|
|
readonly property real maskX: {
|
|
const triggeringBarX = (barPosition === 2) ? effectiveBarSize : 0;
|
|
const adjacentLeftBar = adjacentBarInfo?.leftBar ?? 0;
|
|
return Math.max(triggeringBarX, adjacentLeftBar);
|
|
}
|
|
|
|
readonly property real maskY: {
|
|
const triggeringBarY = (barPosition === 0) ? effectiveBarSize : 0;
|
|
const adjacentTopBar = adjacentBarInfo?.topBar ?? 0;
|
|
return Math.max(triggeringBarY, adjacentTopBar);
|
|
}
|
|
|
|
readonly property real maskWidth: {
|
|
const triggeringBarRight = (barPosition === 3) ? effectiveBarSize : 0;
|
|
const adjacentRightBar = adjacentBarInfo?.rightBar ?? 0;
|
|
const rightExclusion = Math.max(triggeringBarRight, adjacentRightBar);
|
|
return Math.max(100, width - maskX - rightExclusion);
|
|
}
|
|
|
|
readonly property real maskHeight: {
|
|
const triggeringBarBottom = (barPosition === 1) ? effectiveBarSize : 0;
|
|
const adjacentBottomBar = adjacentBarInfo?.bottomBar ?? 0;
|
|
const bottomExclusion = Math.max(triggeringBarBottom, adjacentBottomBar);
|
|
return Math.max(100, height - maskY - bottomExclusion);
|
|
}
|
|
|
|
mask: Region {
|
|
item: Rectangle {
|
|
x: menuWindow.maskX
|
|
y: menuWindow.maskY
|
|
width: menuWindow.maskWidth
|
|
height: menuWindow.maskHeight
|
|
}
|
|
}
|
|
|
|
onVisibleChanged: {
|
|
if (visible) {
|
|
updatePosition();
|
|
root.menuOpen = false;
|
|
PopoutManager.closeAllPopouts();
|
|
ModalManager.closeAllModalsExcept(null);
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
x: menuWindow.maskX
|
|
y: menuWindow.maskY
|
|
width: menuWindow.maskWidth
|
|
height: menuWindow.maskHeight
|
|
z: -1
|
|
enabled: menuRoot.showMenu
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
|
onClicked: mouse => {
|
|
const clickX = mouse.x + menuWindow.maskX;
|
|
const clickY = mouse.y + menuWindow.maskY;
|
|
const outsideContent = clickX < menuContainer.x || clickX > menuContainer.x + menuContainer.width || clickY < menuContainer.y || clickY > menuContainer.y + menuContainer.height;
|
|
|
|
if (!outsideContent)
|
|
return;
|
|
menuRoot.close();
|
|
}
|
|
}
|
|
|
|
FocusScope {
|
|
id: menuFocusScope
|
|
anchors.fill: parent
|
|
focus: true
|
|
|
|
Keys.onEscapePressed: {
|
|
if (entryStack.count > 0) {
|
|
menuRoot.goBack();
|
|
} else {
|
|
menuRoot.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
function updatePosition() {
|
|
const targetItem = (typeof menuRoot !== "undefined" && menuRoot.anchorItem) ? menuRoot.anchorItem : root;
|
|
|
|
const isFromOverflowMenu = targetItem.objectName === "overflowMenuContainer";
|
|
|
|
if (isFromOverflowMenu) {
|
|
if (menuRoot.isVertical) {
|
|
const edge = menuRoot.axis?.edge;
|
|
let targetX = edge === "left" ? root.barThickness + root.barSpacing + Theme.popupDistance : screen.width - (root.barThickness + root.barSpacing + Theme.popupDistance);
|
|
const targetY = targetItem.y + targetItem.height / 2;
|
|
anchorPos = Qt.point(targetX, targetY);
|
|
} else {
|
|
const targetX = targetItem.x + targetItem.width / 2;
|
|
let targetY = menuRoot.isAtBottom ? screen.height - (root.barThickness + root.barSpacing + (barConfig?.bottomGap ?? 0) + Theme.popupDistance) : root.barThickness + root.barSpacing + (barConfig?.bottomGap ?? 0) + Theme.popupDistance;
|
|
anchorPos = Qt.point(targetX, targetY);
|
|
}
|
|
} else {
|
|
const globalPos = targetItem.mapToGlobal(0, 0);
|
|
const screenX = screen.x || 0;
|
|
const screenY = screen.y || 0;
|
|
const relativeX = globalPos.x - screenX;
|
|
const relativeY = globalPos.y - screenY;
|
|
|
|
if (menuRoot.isVertical) {
|
|
const edge = menuRoot.axis?.edge;
|
|
let targetX = edge === "left" ? root.barThickness + root.barSpacing + Theme.popupDistance : screen.width - (root.barThickness + root.barSpacing + Theme.popupDistance);
|
|
const adjustedY = relativeY + targetItem.height / 2 + root.minTooltipY;
|
|
anchorPos = Qt.point(targetX, adjustedY);
|
|
} else {
|
|
let targetY = menuRoot.isAtBottom ? screen.height - (root.barThickness + root.barSpacing + (barConfig?.bottomGap ?? 0) + Theme.popupDistance) : root.barThickness + root.barSpacing + (barConfig?.bottomGap ?? 0) + Theme.popupDistance;
|
|
anchorPos = Qt.point(relativeX + targetItem.width / 2, targetY);
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: menuContainer
|
|
|
|
readonly property real rawWidth: Math.min(500, Math.max(250, menuColumn.implicitWidth + Theme.spacingS * 2))
|
|
readonly property real rawHeight: Math.max(40, menuColumn.implicitHeight + Theme.spacingS * 2)
|
|
|
|
readonly property real alignedWidth: Theme.px(rawWidth, menuWindow.dpr)
|
|
readonly property real alignedHeight: Theme.px(rawHeight, menuWindow.dpr)
|
|
|
|
width: alignedWidth
|
|
height: alignedHeight
|
|
|
|
x: Theme.snap((() => {
|
|
if (menuRoot.isVertical) {
|
|
const edge = menuRoot.axis?.edge;
|
|
if (edge === "left") {
|
|
const targetX = menuWindow.anchorPos.x;
|
|
return Math.min(menuWindow.screen.width - alignedWidth - 10, targetX);
|
|
} else {
|
|
const targetX = menuWindow.anchorPos.x - alignedWidth;
|
|
return Math.max(10, targetX);
|
|
}
|
|
} else {
|
|
const left = 10;
|
|
const right = menuWindow.width - alignedWidth - 10;
|
|
const want = menuWindow.anchorPos.x - alignedWidth / 2;
|
|
return Math.max(left, Math.min(right, want));
|
|
}
|
|
})(), menuWindow.dpr)
|
|
|
|
y: Theme.snap((() => {
|
|
if (menuRoot.isVertical) {
|
|
const top = Math.max(menuWindow.barY, 10);
|
|
const bottom = menuWindow.height - alignedHeight - 10;
|
|
const want = menuWindow.anchorPos.y - alignedHeight / 2;
|
|
return Math.max(top, Math.min(bottom, want));
|
|
} else {
|
|
if (menuRoot.isAtBottom) {
|
|
const targetY = menuWindow.anchorPos.y - alignedHeight;
|
|
return Math.max(10, targetY);
|
|
} else {
|
|
const targetY = menuWindow.anchorPos.y;
|
|
return Math.min(menuWindow.screen.height - alignedHeight - 10, targetY);
|
|
}
|
|
}
|
|
})(), menuWindow.dpr)
|
|
|
|
readonly property var elev: Theme.elevationLevel2
|
|
property real shadowBlurPx: elev && elev.blurPx !== undefined ? elev.blurPx : 8
|
|
property real shadowSpreadPx: elev && elev.spreadPx !== undefined ? elev.spreadPx : 0
|
|
property real shadowBaseAlpha: elev && elev.alpha !== undefined ? elev.alpha : 0.25
|
|
readonly property real popupSurfaceAlpha: Theme.popupTransparency
|
|
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha))
|
|
|
|
opacity: menuRoot.showMenu ? 1 : 0
|
|
scale: menuRoot.showMenu ? 1 : 0.85
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation {
|
|
duration: Theme.mediumDuration
|
|
easing.type: Theme.emphasizedEasing
|
|
}
|
|
}
|
|
|
|
Behavior on scale {
|
|
NumberAnimation {
|
|
duration: Theme.mediumDuration
|
|
easing.type: Theme.emphasizedEasing
|
|
}
|
|
}
|
|
|
|
ElevationShadow {
|
|
id: menuBgShadowLayer
|
|
anchors.fill: parent
|
|
level: menuContainer.elev
|
|
fallbackOffset: 4
|
|
shadowBlurPx: menuContainer.shadowBlurPx
|
|
shadowSpreadPx: menuContainer.shadowSpreadPx
|
|
shadowColor: {
|
|
const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest;
|
|
return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha);
|
|
}
|
|
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
|
targetRadius: Theme.cornerRadius
|
|
sourceRect.antialiasing: true
|
|
shadowEnabled: Theme.elevationEnabled
|
|
layer.smooth: true
|
|
layer.textureSize: Qt.size(Math.round(width * menuWindow.dpr), Math.round(height * menuWindow.dpr))
|
|
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
|
}
|
|
|
|
QsMenuAnchor {
|
|
id: submenuHydrator
|
|
anchor.window: menuWindow
|
|
}
|
|
|
|
QsMenuOpener {
|
|
id: rootOpener
|
|
menu: menuRoot.menuHandle
|
|
}
|
|
|
|
QsMenuOpener {
|
|
id: subOpener
|
|
menu: {
|
|
const e = menuRoot.topEntry();
|
|
return e ? (e.menu || e) : null;
|
|
}
|
|
}
|
|
|
|
Column {
|
|
id: menuColumn
|
|
|
|
width: parent.width - Theme.spacingS * 2
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
anchors.top: parent.top
|
|
anchors.topMargin: Theme.spacingS
|
|
spacing: 1
|
|
|
|
Rectangle {
|
|
visible: entryStack.count === 0
|
|
width: parent.width
|
|
height: 28
|
|
radius: Theme.cornerRadius
|
|
color: visibilityToggleArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.withAlpha(Theme.surfaceContainer, 0)
|
|
|
|
StyledText {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: menuRoot.trayItem?.id || "Unknown"
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceTextMedium
|
|
elide: Text.ElideMiddle
|
|
width: parent.width - Theme.spacingS * 2 - 24
|
|
}
|
|
|
|
DankIcon {
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
name: SessionData.isHiddenTrayId(root.getTrayItemKey(menuRoot.trayItem)) ? "visibility" : "visibility_off"
|
|
size: 16
|
|
color: Theme.widgetTextColor
|
|
}
|
|
|
|
MouseArea {
|
|
id: visibilityToggleArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
const itemKey = root.getTrayItemKey(menuRoot.trayItem);
|
|
if (!itemKey)
|
|
return;
|
|
if (SessionData.isHiddenTrayId(itemKey)) {
|
|
SessionData.showTrayId(itemKey);
|
|
} else {
|
|
SessionData.hideTrayId(itemKey);
|
|
}
|
|
menuRoot.closeWithAction();
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
visible: entryStack.count === 0
|
|
width: parent.width
|
|
height: 1
|
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
|
}
|
|
|
|
Rectangle {
|
|
visible: entryStack.count > 0
|
|
width: parent.width
|
|
height: 28
|
|
radius: Theme.cornerRadius
|
|
color: backArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.withAlpha(Theme.surfaceContainer, 0)
|
|
|
|
Row {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: Theme.spacingXS
|
|
|
|
DankIcon {
|
|
name: "arrow_back"
|
|
size: 16
|
|
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: menuRoot.goBack()
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
visible: entryStack.count > 0
|
|
width: parent.width
|
|
height: 1
|
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
|
}
|
|
|
|
Repeater {
|
|
model: entryStack.count ? (subOpener.children ? subOpener.children : (menuRoot.topEntry()?.children || [])) : rootOpener.children
|
|
|
|
Rectangle {
|
|
property var menuEntry: modelData
|
|
|
|
width: menuColumn.width
|
|
height: menuEntry?.isSeparator ? 1 : 28
|
|
radius: menuEntry?.isSeparator ? 0 : Theme.cornerRadius
|
|
color: {
|
|
if (menuEntry?.isSeparator)
|
|
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2);
|
|
return itemArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.withAlpha(Theme.surfaceContainer, 0);
|
|
}
|
|
|
|
MouseArea {
|
|
id: itemArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
enabled: !menuEntry?.isSeparator && (menuEntry?.enabled !== false)
|
|
cursorShape: Qt.PointingHandCursor
|
|
|
|
onClicked: {
|
|
if (!menuEntry || menuEntry.isSeparator)
|
|
return;
|
|
if (menuEntry.hasChildren) {
|
|
menuRoot.showSubMenu(menuEntry);
|
|
return;
|
|
}
|
|
|
|
if (typeof menuEntry.activate === "function") {
|
|
menuEntry.activate();
|
|
} else if (typeof menuEntry.triggered === "function") {
|
|
menuEntry.triggered();
|
|
}
|
|
Qt.createQmlObject('import QtQuick; Timer { interval: 80; running: true; repeat: false; onTriggered: menuRoot.closeWithAction() }', menuRoot);
|
|
}
|
|
}
|
|
|
|
Row {
|
|
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 {
|
|
width: 16
|
|
height: 16
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
visible: menuEntry?.buttonType !== undefined && menuEntry.buttonType !== 0
|
|
radius: menuEntry?.buttonType === 2 ? 8 : 2
|
|
border.width: 1
|
|
border.color: Theme.outline
|
|
color: "transparent"
|
|
|
|
Rectangle {
|
|
anchors.centerIn: parent
|
|
width: parent.width - 6
|
|
height: parent.height - 6
|
|
radius: parent.radius - 3
|
|
color: Theme.primary
|
|
visible: menuEntry?.checkState === 2
|
|
}
|
|
|
|
DankIcon {
|
|
anchors.centerIn: parent
|
|
name: "check"
|
|
size: 10
|
|
color: Theme.primaryText
|
|
visible: menuEntry?.buttonType === 1 && menuEntry?.checkState === 2
|
|
}
|
|
}
|
|
|
|
Item {
|
|
width: 16
|
|
height: 16
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
visible: (menuEntry?.icon ?? "") !== ""
|
|
|
|
Image {
|
|
anchors.fill: parent
|
|
source: menuEntry?.icon || ""
|
|
sourceSize.width: 16
|
|
sourceSize.height: 16
|
|
fillMode: Image.PreserveAspectFit
|
|
smooth: true
|
|
}
|
|
}
|
|
|
|
StyledText {
|
|
text: menuEntry?.text || ""
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: (menuEntry?.enabled !== false) ? Theme.surfaceText : Theme.surfaceTextMedium
|
|
elide: Text.ElideRight
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: Math.max(150, parent.width - 64)
|
|
wrapMode: Text.NoWrap
|
|
}
|
|
|
|
Item {
|
|
width: 16
|
|
height: 16
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
DankIcon {
|
|
anchors.centerIn: parent
|
|
name: "chevron_right"
|
|
size: 14
|
|
color: Theme.widgetTextColor
|
|
visible: menuEntry?.hasChildren ?? false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) {
|
|
if (!screen)
|
|
return;
|
|
if (currentTrayMenu) {
|
|
currentTrayMenu.showMenu = false;
|
|
currentTrayMenu.destroy();
|
|
currentTrayMenu = null;
|
|
}
|
|
|
|
PopoutManager.closeAllPopouts();
|
|
ModalManager.closeAllModalsExcept(null);
|
|
|
|
currentTrayMenu = trayMenuComponent.createObject(null);
|
|
if (!currentTrayMenu)
|
|
return;
|
|
currentTrayMenu.showForTrayItem(item, anchor, screen, atBottom, vertical ?? false, axisObj);
|
|
}
|
|
}
|