1 Commits

Author SHA1 Message Date
zach 33746fca04 Initial commit for submenu popouts, unfinished 2026-05-20 14:07:38 +02:00
7 changed files with 300 additions and 242 deletions
+75
View File
@@ -13,6 +13,7 @@ import qs.Modules.Resources as Resources
import qs.Modules.Settings as Settings import qs.Modules.Settings as Settings
import qs.Modules.Drawing as Drawing import qs.Modules.Drawing as Drawing
import qs.Modules.Dock as Dock import qs.Modules.Dock as Dock
import qs.Modules.SysTray.Popouts as SysPopouts
import qs.Config import qs.Config
Item { Item {
@@ -37,6 +38,7 @@ Item {
readonly property alias settingsWrapper: settingsWrapper readonly property alias settingsWrapper: settingsWrapper
readonly property alias sidebar: sidebar readonly property alias sidebar: sidebar
readonly property alias toasts: toasts readonly property alias toasts: toasts
readonly property alias traySubmenus: traySubmenus
readonly property alias utilities: utilities readonly property alias utilities: utilities
required property PersistentProperties visibilities required property PersistentProperties visibilities
@@ -93,6 +95,79 @@ Item {
visibilities: root.visibilities 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 { Modules.ClipWrapper {
id: popouts id: popouts
+29 -1
View File
@@ -64,7 +64,7 @@ Variants {
height: win.height - bar.implicitHeight - Config.barConfig.border height: win.height - bar.implicitHeight - Config.barConfig.border
intersection: Intersection.Xor intersection: Intersection.Xor
regions: popoutRegions.instances regions: [...popoutRegions.instances, ...subMenuRegions.instances]
width: win.width - Config.barConfig.border * 2 width: win.width - Config.barConfig.border * 2
x: Config.barConfig.border x: Config.barConfig.border
y: bar.implicitHeight 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 { HyprlandFocusGrab {
id: focusGrab id: focusGrab
@@ -302,6 +318,18 @@ Variants {
panel: panels.drawing panel: panels.drawing
radius: Appearance.rounding.normal 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 { Drawing {
+2
View File
@@ -16,6 +16,7 @@ Item {
readonly property Item current: currentPopout?.item ?? null readonly property Item current: currentPopout?.item ?? null
readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null
required property PopoutState popouts required property PopoutState popouts
required property ShellScreen screen
implicitHeight: (currentPopout?.implicitHeight ?? 0) + 5 * 2 implicitHeight: (currentPopout?.implicitHeight ?? 0) + 5 * 2
implicitWidth: (currentPopout?.implicitWidth ?? 0) + 5 * 2 implicitWidth: (currentPopout?.implicitWidth ?? 0) + 5 * 2
@@ -63,6 +64,7 @@ Item {
TrayMenuPopout { TrayMenuPopout {
popouts: root.popouts popouts: root.popouts
screen: root.screen
trayItem: trayMenu.modelData.menu trayItem: trayMenu.modelData.menu
} }
} }
+28
View File
@@ -1,8 +1,36 @@
import QtQuick import QtQuick
QtObject { QtObject {
id: root
property string currentName property string currentName
property bool hasCurrent property bool hasCurrent
property var submenus: []
signal detachRequested(mode: string) 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();
}
} }
+158
View File
@@ -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"
}
}
}
}
}
}
}
+5 -240
View File
@@ -1,251 +1,16 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import Quickshell.Widgets
import QtQuick import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import qs.Components import qs.Components
import qs.Modules import qs.Modules
import qs.Config import qs.Config
StackView { SubMenu {
id: root id: root
property int biggestWidth: 0 handle: trayItem
required property PopoutState popouts level: 0
property int rootWidth: 0
required property QsMenuHandle trayItem 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")
}
}
}
}
}
}
}
+3 -1
View File
@@ -15,13 +15,14 @@ Item {
property real currentCenter property real currentCenter
property alias currentName: popoutState.currentName property alias currentName: popoutState.currentName
property string detachedMode property string detachedMode
readonly property bool isDetached: detachedMode.length > 0
property alias hasCurrent: popoutState.hasCurrent 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 nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight
readonly property real nonAnimWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth readonly property real nonAnimWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth
required property real offsetScale required property real offsetScale
property string queuedMode property string queuedMode
required property ShellScreen screen required property ShellScreen screen
property alias state: popoutState
function close(): void { function close(): void {
hasCurrent = false; hasCurrent = false;
@@ -79,6 +80,7 @@ Item {
sourceComponent: Content { sourceComponent: Content {
popouts: popoutState popouts: popoutState
screen: root.screen
} }
} }