diff --git a/Drawers/Panels.qml b/Drawers/Panels.qml index 5c84747..443ca12 100644 --- a/Drawers/Panels.qml +++ b/Drawers/Panels.qml @@ -13,6 +13,7 @@ import qs.Modules.Resources as Resources import qs.Modules.Settings as Settings import qs.Modules.Drawing as Drawing import qs.Modules.Dock as Dock +import qs.Modules.SysTray.Popouts as SysPopouts import qs.Config Item { @@ -37,6 +38,7 @@ Item { readonly property alias settingsWrapper: settingsWrapper readonly property alias sidebar: sidebar readonly property alias toasts: toasts + readonly property alias traySubmenus: traySubmenus readonly property alias utilities: utilities required property PersistentProperties visibilities @@ -93,6 +95,79 @@ Item { visibilities: root.visibilities } + Item { + id: traySubmenus + + Repeater { + model: popouts.content.state.submenus + + CustomClippingRect { + id: subMenuWrapper + + required property int index + required property var modelData + property real targetX: 0 + property real targetY: 0 + + function updatePosition() { + let sourceItem = modelData.sourceItem; + if (!sourceItem || !sourceItem.parent) + return; + + let mapped = sourceItem.mapToItem(root, 0, -Appearance.padding.small); + + let rightX = mapped.x + modelData.sourceWidth + Config.barConfig.border; + let leftX = mapped.x - implicitWidth - Config.barConfig.border; + + if (rightX + implicitWidth > root.width) { + targetX = leftX; + } else { + targetX = rightX; + } + + targetY = mapped.y; + + if (targetY + implicitHeight > root.height) { + targetY = root.height - implicitHeight; + } + if (targetY < 0) + targetY = 0; + } + + implicitHeight: subMenuContent.implicitHeight + Appearance.padding.small * 2 + implicitWidth: subMenuContent.implicitWidth + Appearance.padding.small * 2 + radius: Appearance.rounding.normal + x: targetX + y: targetY + + Behavior on implicitHeight { + Anim { + } + } + Behavior on implicitWidth { + Anim { + } + } + + Component.onCompleted: { + updatePosition(); + } + onImplicitHeightChanged: updatePosition() + onImplicitWidthChanged: updatePosition() + + SysPopouts.SubMenu { + id: subMenuContent + + anchors.centerIn: parent + handle: subMenuWrapper.modelData.handle + level: subMenuWrapper.index + 1 + popouts: root.popouts.state + screen: root.screen + } + } + } + } + Modules.ClipWrapper { id: popouts diff --git a/Drawers/Windows.qml b/Drawers/Windows.qml index 37bd73c..04a2e9b 100644 --- a/Drawers/Windows.qml +++ b/Drawers/Windows.qml @@ -64,7 +64,7 @@ Variants { height: win.height - bar.implicitHeight - Config.barConfig.border intersection: Intersection.Xor - regions: popoutRegions.instances + regions: [...popoutRegions.instances, ...subMenuRegions.instances] width: win.width - Config.barConfig.border * 2 x: Config.barConfig.border y: bar.implicitHeight @@ -93,6 +93,22 @@ Variants { } } + Variants { + id: subMenuRegions + + model: panels.traySubmenus.children + + Region { + required property Item modelData + + height: modelData.height + intersection: Intersection.Subtract + width: modelData.width + x: modelData.x + panels.traySubmenus.x + Config.barConfig.border + y: modelData.y + panels.traySubmenus.y + bar.implicitHeight + } + } + HyprlandFocusGrab { id: focusGrab @@ -302,6 +318,18 @@ Variants { panel: panels.drawing radius: Appearance.rounding.normal } + + Repeater { + model: panels.traySubmenus.children + + PanelBg { + required property Item modelData + + deformAmount: 0.1 + panel: modelData + radius: 20 * Appearance.rounding.scale + } + } } Drawing { diff --git a/Modules/Content.qml b/Modules/Content.qml index f3035d3..8d20297 100644 --- a/Modules/Content.qml +++ b/Modules/Content.qml @@ -16,6 +16,7 @@ Item { readonly property Item current: currentPopout?.item ?? null readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null required property PopoutState popouts + required property ShellScreen screen implicitHeight: (currentPopout?.implicitHeight ?? 0) + 5 * 2 implicitWidth: (currentPopout?.implicitWidth ?? 0) + 5 * 2 @@ -63,6 +64,7 @@ Item { TrayMenuPopout { popouts: root.popouts + screen: root.screen trayItem: trayMenu.modelData.menu } } diff --git a/Modules/PopoutState.qml b/Modules/PopoutState.qml index a5df520..3d4989e 100644 --- a/Modules/PopoutState.qml +++ b/Modules/PopoutState.qml @@ -1,8 +1,36 @@ import QtQuick QtObject { + id: root + property string currentName property bool hasCurrent + property var submenus: [] signal detachRequested(mode: string) + + function clearSubmenus(): void { + submenus = []; + } + + function closeSubmenus(level: int): void { + submenus = submenus.slice(0, level); + } + + function pushSubmenu(level: int, handle: var, sourceItem: var, sourceWidth: int): void { + let newSubmenus = submenus.slice(0, level); + newSubmenus.push({ + "handle": handle, + "sourceItem": sourceItem, + "sourceWidth": sourceWidth + }); + submenus = newSubmenus; + } + + onCurrentNameChanged: { + root.clearSubmenus(); + } + onHasCurrentChanged: { + root.clearSubmenus(); + } } diff --git a/Modules/SysTray/Popouts/SubMenu.qml b/Modules/SysTray/Popouts/SubMenu.qml new file mode 100644 index 0000000..0ea1a1d --- /dev/null +++ b/Modules/SysTray/Popouts/SubMenu.qml @@ -0,0 +1,158 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import qs.Components +import qs.Modules +import qs.Config + +Column { + id: menu + + property int biggestWidth: 0 + required property QsMenuHandle handle + required property int level + required property PopoutState popouts + required property ShellScreen screen + property bool shown: true + + height: childrenRect.height + opacity: shown ? 1 : 0 + padding: 0 + scale: shown ? 1 : 0.8 + spacing: 4 + width: biggestWidth + + Behavior on opacity { + Anim { + } + } + Behavior on scale { + Anim { + } + } + + QsMenuOpener { + id: menuOpener + + menu: menu.handle + } + + Repeater { + model: menuOpener.children + + CustomRect { + id: item + + required property int index + required property QsMenuEntry modelData + + color: modelData.isSeparator ? DynamicColors.palette.m3outlineVariant : "transparent" + implicitHeight: modelData.isSeparator ? 1 : children.implicitHeight + implicitWidth: menu.biggestWidth + radius: Appearance.rounding.full + visible: index !== (menuOpener.children.values.length - 1) ? true : (modelData.isSeparator ? false : true) + + Loader { + id: children + + active: !item.modelData.isSeparator + anchors.left: parent.left + anchors.right: parent.right + asynchronous: true + + sourceComponent: Item { + implicitHeight: 30 + + StateLayer { + function onClicked(): void { + const entry = item.modelData; + if (entry.hasChildren) { + menu.popouts.pushSubmenu(menu.level, entry, item, menu.biggestWidth); + } else { + entry.triggered(); + menu.popouts.hasCurrent = false; + } + } + + disabled: !item.modelData.enabled + radius: item.radius + } + + Loader { + id: icon + + active: item.modelData.icon !== "" + anchors.right: parent.right + anchors.rightMargin: 10 + anchors.verticalCenter: parent.verticalCenter + asynchronous: true + + sourceComponent: Item { + implicitHeight: label.implicitHeight + implicitWidth: label.implicitHeight + + IconImage { + id: iconImage + + implicitSize: parent.implicitHeight + source: item.modelData.icon + visible: false + } + + MultiEffect { + anchors.fill: iconImage + colorization: 1.0 + colorizationColor: item.modelData.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3outline + source: iconImage + } + } + } + + CustomText { + id: label + + anchors.left: parent.left + anchors.leftMargin: 10 + anchors.verticalCenter: parent.verticalCenter + color: item.modelData.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3outline + text: labelMetrics.elidedText + } + + TextMetrics { + id: labelMetrics + + font.family: label.font.family + font.pointSize: label.font.pointSize + text: item.modelData.text + + Component.onCompleted: { + var biggestWidth = menu.biggestWidth; + var currentWidth = labelMetrics.width + (item.modelData.icon ?? "" ? 30 : 0) + (item.modelData.hasChildren ? 30 : 0) + 20; + if (currentWidth > biggestWidth) { + menu.biggestWidth = currentWidth; + } + } + } + + Loader { + id: expand + + active: item.modelData.hasChildren + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + asynchronous: true + + sourceComponent: MaterialIcon { + color: item.modelData.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3outline + text: "chevron_right" + } + } + } + } + } + } +} diff --git a/Modules/SysTray/Popouts/TrayMenuPopout.qml b/Modules/SysTray/Popouts/TrayMenuPopout.qml index 908b2f6..6428074 100644 --- a/Modules/SysTray/Popouts/TrayMenuPopout.qml +++ b/Modules/SysTray/Popouts/TrayMenuPopout.qml @@ -1,251 +1,16 @@ pragma ComponentBehavior: Bound import Quickshell -import Quickshell.Widgets import QtQuick -import QtQuick.Controls -import QtQuick.Effects import qs.Components import qs.Modules import qs.Config -StackView { +SubMenu { id: root - property int biggestWidth: 0 - required property PopoutState popouts - property int rootWidth: 0 + handle: trayItem + level: 0 + required property QsMenuHandle trayItem - - implicitHeight: currentItem.implicitHeight - implicitWidth: currentItem.implicitWidth - - initialItem: SubMenu { - handle: root.trayItem - } - popEnter: NoAnim { - } - popExit: NoAnim { - } - pushEnter: NoAnim { - } - pushExit: NoAnim { - } - - Component { - id: subMenuComp - - SubMenu { - } - } - - component NoAnim: Transition { - NumberAnimation { - duration: 0 - } - } - component SubMenu: Column { - id: menu - - required property QsMenuHandle handle - property bool isSubMenu - property bool shown - - opacity: shown ? 1 : 0 - padding: 0 - scale: shown ? 1 : 0.8 - spacing: 4 - - Behavior on opacity { - Anim { - } - } - Behavior on scale { - Anim { - } - } - - Component.onCompleted: shown = true - StackView.onActivating: shown = true - StackView.onDeactivating: shown = false - StackView.onRemoved: destroy() - - QsMenuOpener { - id: menuOpener - - menu: menu.handle - } - - Repeater { - model: menuOpener.children - - CustomRect { - id: item - - required property int index - required property QsMenuEntry modelData - - color: modelData.isSeparator ? DynamicColors.palette.m3outlineVariant : "transparent" - implicitHeight: modelData.isSeparator ? 1 : children.implicitHeight - implicitWidth: root.biggestWidth - radius: Appearance.rounding.full - visible: index !== (menuOpener.children.values.length - 1) ? true : (modelData.isSeparator ? false : true) - - Loader { - id: children - - active: !item.modelData.isSeparator - anchors.left: parent.left - anchors.right: parent.right - asynchronous: true - - sourceComponent: Item { - implicitHeight: 30 - - StateLayer { - function onClicked(): void { - const entry = item.modelData; - if (entry.hasChildren) { - root.rootWidth = root.biggestWidth; - root.biggestWidth = 0; - root.push(subMenuComp.createObject(null, { - handle: entry, - isSubMenu: true - })); - } else { - item.modelData.triggered(); - root.popouts.hasCurrent = false; - } - } - - disabled: !item.modelData.enabled - radius: item.radius - } - - Loader { - id: icon - - active: item.modelData.icon !== "" - anchors.right: parent.right - anchors.rightMargin: 10 - anchors.verticalCenter: parent.verticalCenter - asynchronous: true - - sourceComponent: Item { - implicitHeight: label.implicitHeight - implicitWidth: label.implicitHeight - - IconImage { - id: iconImage - - implicitSize: parent.implicitHeight - source: item.modelData.icon - visible: false - } - - MultiEffect { - anchors.fill: iconImage - colorization: 1.0 - colorizationColor: item.modelData.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3outline - source: iconImage - } - } - } - - CustomText { - id: label - - anchors.left: parent.left - anchors.leftMargin: 10 - anchors.verticalCenter: parent.verticalCenter - color: item.modelData.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3outline - text: labelMetrics.elidedText - } - - TextMetrics { - id: labelMetrics - - font.family: label.font.family - font.pointSize: label.font.pointSize - text: item.modelData.text - - Component.onCompleted: { - var biggestWidth = root.biggestWidth; - var currentWidth = labelMetrics.width + (item.modelData.icon ?? "" ? 30 : 0) + (item.modelData.hasChildren ? 30 : 0) + 20; - if (currentWidth > biggestWidth) { - root.biggestWidth = currentWidth; - } - } - } - - Loader { - id: expand - - active: item.modelData.hasChildren - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - asynchronous: true - - sourceComponent: MaterialIcon { - color: item.modelData.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3outline - text: "chevron_right" - } - } - } - } - } - } - - Loader { - id: loader - - active: menu.isSubMenu - asynchronous: true - - sourceComponent: Item { - implicitHeight: 30 - implicitWidth: back.implicitWidth - - Item { - anchors.bottom: parent.bottom - implicitHeight: 30 - implicitWidth: root.biggestWidth - - CustomRect { - anchors.fill: parent - color: DynamicColors.palette.m3secondaryContainer - radius: Appearance.rounding.full - - StateLayer { - function onClicked(): void { - root.pop(); - root.biggestWidth = root.rootWidth; - } - - color: DynamicColors.palette.m3onSecondaryContainer - radius: parent.radius - } - } - - Row { - id: back - - anchors.verticalCenter: parent.verticalCenter - - MaterialIcon { - anchors.verticalCenter: parent.verticalCenter - color: DynamicColors.palette.m3onSecondaryContainer - text: "chevron_left" - } - - CustomText { - anchors.verticalCenter: parent.verticalCenter - color: DynamicColors.palette.m3onSecondaryContainer - text: qsTr("Back") - } - } - } - } - } - } -} +} \ No newline at end of file diff --git a/Modules/Wrapper.qml b/Modules/Wrapper.qml index 8bf0ab7..75d4bb3 100644 --- a/Modules/Wrapper.qml +++ b/Modules/Wrapper.qml @@ -15,13 +15,14 @@ Item { property real currentCenter property alias currentName: popoutState.currentName property string detachedMode - readonly property bool isDetached: detachedMode.length > 0 property alias hasCurrent: popoutState.hasCurrent + readonly property bool isDetached: detachedMode.length > 0 readonly property real nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight readonly property real nonAnimWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth required property real offsetScale property string queuedMode required property ShellScreen screen + property alias state: popoutState function close(): void { hasCurrent = false; @@ -79,6 +80,7 @@ Item { sourceComponent: Content { popouts: popoutState + screen: root.screen } }