bar: widgets now use the entire bar space (e.g: full height with a vertical bar) this ease a lot for clicking widgets and avoid dead zones above and below the widget.

Keep an eye on this + plugins bar widget will need updating
This commit is contained in:
Lemmy
2026-01-26 23:47:03 -05:00
parent 6f307dc1fb
commit 382e548d2b
12 changed files with 1415 additions and 1287 deletions
+9 -4
View File
@@ -34,13 +34,18 @@ Item {
signal middleClicked
signal wheel(int delta)
// Dynamic sizing based on loaded component
width: pillLoader.item ? pillLoader.item.width : 0
height: pillLoader.item ? pillLoader.item.height : 0
// Size based on content for the content dimension, fill parent for the extended dimension
// Horizontal bars: width = content, height = fill parent (for extended click area)
// Vertical bars: width = fill parent, height = content
width: isVerticalBar ? parent.width : (pillLoader.item ? pillLoader.item.implicitWidth : 0)
height: isVerticalBar ? (pillLoader.item ? pillLoader.item.implicitHeight : 0) : parent.height
implicitWidth: pillLoader.item ? pillLoader.item.implicitWidth : 0
implicitHeight: pillLoader.item ? pillLoader.item.implicitHeight : 0
// Loader to switch between vertical and horizontal pill implementations
// Loader fills BarPill so child components can extend to full bar dimension
Loader {
id: pillLoader
anchors.fill: parent
sourceComponent: isVerticalBar ? verticalPillComponent : horizontalPillComponent
Component {
+8 -2
View File
@@ -53,7 +53,8 @@ Item {
readonly property real iconSize: Style.toOdd(pillHeight * 0.48)
width: {
// Content width calculation (for implicit sizing)
readonly property real contentWidth: {
if (collapseToIcon) {
return hasIcon ? pillHeight : 0;
}
@@ -61,7 +62,12 @@ Item {
var baseWidth = hasIcon ? pillHeight : 0;
return baseWidth + Math.max(0, pill.width - overlap);
}
height: pillHeight
// Fill parent to extend click area to full bar height
// Visual content is centered vertically within
anchors.fill: parent
implicitWidth: contentWidth
implicitHeight: pillHeight
Connections {
target: root
+13 -5
View File
@@ -60,9 +60,8 @@ Item {
readonly property real iconSize: Style.toOdd(pillHeight * 0.48)
// For vertical bars: width is just icon size, height includes pill space
width: buttonSize
height: {
// Content height calculation (for implicit sizing and visual layout)
readonly property real contentHeight: {
if (collapseToIcon) {
return hasIcon ? buttonSize : 0;
}
@@ -75,6 +74,15 @@ Item {
return buttonSize;
}
// Fill parent width to extend horizontal click area
// Keep content-based height for visual layout
anchors.left: parent ? parent.left : undefined
anchors.right: parent ? parent.right : undefined
anchors.verticalCenter: parent ? parent.verticalCenter : undefined
height: contentHeight
implicitWidth: buttonSize
implicitHeight: contentHeight
Connections {
target: root
function onTooltipTextChanged() {
@@ -88,7 +96,7 @@ Item {
Rectangle {
id: pillBackground
width: buttonSize
height: root.height
height: root.contentHeight
radius: Style.radiusM
color: root.bgColor
border.color: Style.capsuleBorderColor
@@ -183,7 +191,7 @@ Item {
// Icon positioning based on direction
x: 0
y: openUpward ? (parent.height - height) : 0
y: openUpward ? (root.contentHeight - height) : 0
anchors.horizontalCenter: parent.horizontalCenter
NIcon {
+23 -3
View File
@@ -15,9 +15,16 @@ Item {
readonly property string section: widgetProps ? (widgetProps.section || "") : ""
readonly property int sectionIndex: widgetProps ? (widgetProps.sectionWidgetIndex || 0) : 0
// Don't reserve space unless the loaded widget is really visible
implicitWidth: getImplicitSize(loader.item, "implicitWidth")
implicitHeight: getImplicitSize(loader.item, "implicitHeight")
// Bar orientation and height for extended click areas
readonly property string barPosition: Settings.getBarPositionForScreen(widgetScreen?.name)
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
readonly property real barHeight: Style.getBarHeightForScreen(widgetScreen?.name)
// Request full bar dimension from layout to extend click areas above/below widgets
// For horizontal bars: full bar height, widget's content width
// For vertical bars: full bar width, widget's content height
implicitWidth: isVerticalBar ? barHeight : getImplicitSize(loader.item, "implicitWidth")
implicitHeight: isVerticalBar ? getImplicitSize(loader.item, "implicitHeight") : barHeight
// Remove layout space left by hidden widgets
visible: loader.item ? ((loader.item.opacity > 0.0) || (loader.item.hasOwnProperty("hideMode") && loader.item.hideMode === "transparent")) : false
@@ -66,6 +73,19 @@ Item {
Logger.d("BarWidgetLoader", "Loading widget", widgetId, "on screen:", widgetScreen.name);
// Extend widget to fill full bar dimension for extended click areas
// For horizontal bars: widget fills bar height (content width preserved)
// For vertical bars: widget fills bar width (content height preserved)
if (root.isVerticalBar) {
item.width = Qt.binding(function () {
return root.barHeight;
});
} else {
item.height = Qt.binding(function () {
return root.barHeight;
});
}
// Apply properties to loaded widget
for (var prop in widgetProps) {
if (item.hasOwnProperty(prop)) {
+32 -25
View File
@@ -76,8 +76,12 @@ Item {
}
}
implicitWidth: !shouldShow ? 0 : isVerticalBar ? capsuleHeight : visualizerWidth
implicitHeight: !shouldShow ? 0 : isVerticalBar ? visualizerWidth : capsuleHeight
// Content dimensions for implicit sizing
readonly property real contentWidth: !shouldShow ? 0 : isVerticalBar ? capsuleHeight : visualizerWidth
readonly property real contentHeight: !shouldShow ? 0 : isVerticalBar ? visualizerWidth : capsuleHeight
implicitWidth: contentWidth
implicitHeight: contentHeight
visible: shouldShow
opacity: shouldShow ? 1.0 : 0.0
@@ -94,37 +98,40 @@ Item {
}
}
// Store visualizer type to force re-evaluation
readonly property string currentVisualizerType: Settings.data.audio.visualizerType
// Visual capsule centered in parent
Rectangle {
id: background
anchors.fill: parent
width: root.contentWidth
height: root.contentHeight
anchors.centerIn: parent
radius: Style.radiusS
color: Style.capsuleColor
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
}
// Store visualizer type to force re-evaluation
readonly property string currentVisualizerType: Settings.data.audio.visualizerType
// When visualizer type or playback changes, shouldShow updates automatically
// The Loader dynamically loads the appropriate visualizer based on settings
Loader {
id: visualizerLoader
anchors.fill: parent
anchors.margins: Style.marginS
active: shouldShow
asynchronous: true
// When visualizer type or playback changes, shouldShow updates automatically
// The Loader dynamically loads the appropriate visualizer based on settings
Loader {
id: visualizerLoader
anchors.fill: parent
anchors.margins: Style.marginS
active: shouldShow
asynchronous: true
sourceComponent: {
switch (currentVisualizerType) {
case "linear":
return linearComponent;
case "mirrored":
return mirroredComponent;
case "wave":
return waveComponent;
default:
return null;
sourceComponent: {
switch (currentVisualizerType) {
case "linear":
return linearComponent;
case "mirrored":
return mirroredComponent;
case "wave":
return waveComponent;
default:
return null;
}
}
}
}
+71 -58
View File
@@ -7,7 +7,7 @@ import qs.Modules.Bar.Extras
import qs.Services.UI
import qs.Widgets
Rectangle {
Item {
id: root
property ShellScreen screen
@@ -45,76 +45,89 @@ Rectangle {
readonly property string formatVertical: widgetSettings.formatVertical !== undefined ? widgetSettings.formatVertical : widgetMetadata.formatVertical
readonly property string tooltipFormat: widgetSettings.tooltipFormat !== undefined ? widgetSettings.tooltipFormat : widgetMetadata.tooltipFormat
implicitWidth: isBarVertical ? capsuleHeight : Math.round((isBarVertical ? verticalLoader.implicitWidth : horizontalLoader.implicitWidth) + Style.marginXL)
// Content dimensions for implicit sizing
readonly property real contentWidth: isBarVertical ? capsuleHeight : Math.round((isBarVertical ? verticalLoader.implicitWidth : horizontalLoader.implicitWidth) + Style.marginXL)
readonly property real contentHeight: isBarVertical ? Math.round(verticalLoader.implicitHeight + Style.marginS * 2) : capsuleHeight
implicitHeight: isBarVertical ? Math.round(verticalLoader.implicitHeight + Style.marginS * 2) : capsuleHeight
// Size: use implicit width/height
// BarWidgetLoader sets explicit width/height to extend click area
implicitWidth: contentWidth
implicitHeight: contentHeight
radius: Style.radiusS
color: Style.capsuleColor
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
Item {
id: clockContainer
// Visual clock capsule - stays at content size, centered in parent
Rectangle {
id: visualClock
width: root.contentWidth
height: root.contentHeight
anchors.centerIn: parent
// Horizontal
Loader {
id: horizontalLoader
active: !isBarVertical
radius: Style.radiusS
color: Style.capsuleColor
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
Item {
id: clockContainer
anchors.centerIn: parent
sourceComponent: ColumnLayout {
// Horizontal
Loader {
id: horizontalLoader
active: !isBarVertical
anchors.centerIn: parent
spacing: Settings.data.bar.showCapsule ? -5 : -3
Repeater {
id: repeater
model: I18n.locale.toString(now, formatHorizontal.trim()).split("\\n")
NText {
visible: text !== ""
text: modelData
family: useCustomFont && customFont ? customFont : Settings.data.ui.fontDefault
Binding on pointSize {
value: {
if (repeater.model.length == 1) {
// Single line: Full size
return barFontSize;
} else if (repeater.model.length == 2) {
// Two lines: First line is bigger than the second
return (index == 0) ? Math.round(barFontSize * 0.9) : Math.round(barFontSize * 0.75);
} else {
// More than two lines: Make it small!
return Math.round(barFontSize * 0.75);
sourceComponent: ColumnLayout {
anchors.centerIn: parent
spacing: Settings.data.bar.showCapsule ? -5 : -3
Repeater {
id: repeater
model: I18n.locale.toString(now, formatHorizontal.trim()).split("\\n")
NText {
visible: text !== ""
text: modelData
family: useCustomFont && customFont ? customFont : Settings.data.ui.fontDefault
Binding on pointSize {
value: {
if (repeater.model.length == 1) {
// Single line: Full size
return barFontSize;
} else if (repeater.model.length == 2) {
// Two lines: First line is bigger than the second
return (index == 0) ? Math.round(barFontSize * 0.9) : Math.round(barFontSize * 0.75);
} else {
// More than two lines: Make it small!
return Math.round(barFontSize * 0.75);
}
}
}
applyUiScale: false
color: usePrimaryColor ? Color.mPrimary : Color.mOnSurface
wrapMode: Text.WordWrap
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
applyUiScale: false
color: usePrimaryColor ? Color.mPrimary : Color.mOnSurface
wrapMode: Text.WordWrap
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
}
}
}
// Vertical
Loader {
id: verticalLoader
active: isBarVertical
anchors.centerIn: parent // Now this works without layout conflicts
sourceComponent: ColumnLayout {
anchors.centerIn: parent
spacing: -2
Repeater {
model: I18n.locale.toString(now, formatVertical.trim()).split(" ")
delegate: NText {
visible: text !== ""
text: modelData
family: useCustomFont && customFont ? customFont : Settings.data.ui.fontDefault
pointSize: barFontSize
applyUiScale: false
color: usePrimaryColor ? Color.mPrimary : Color.mOnSurface
wrapMode: Text.WordWrap
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
// Vertical
Loader {
id: verticalLoader
active: isBarVertical
anchors.centerIn: parent // Now this works without layout conflicts
sourceComponent: ColumnLayout {
anchors.centerIn: parent
spacing: -2
Repeater {
model: I18n.locale.toString(now, formatVertical.trim()).split(" ")
delegate: NText {
visible: text !== ""
text: modelData
family: useCustomFont && customFont ? customFont : Settings.data.ui.fontDefault
pointSize: barFontSize
applyUiScale: false
color: usePrimaryColor ? Color.mPrimary : Color.mOnSurface
wrapMode: Text.WordWrap
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
}
}
}
+72 -62
View File
@@ -10,7 +10,7 @@ import qs.Services.Keyboard
import qs.Services.UI
import qs.Widgets
Rectangle {
Item {
id: root
property ShellScreen screen
@@ -47,15 +47,12 @@ Rectangle {
readonly property bool hideWhenOff: (widgetSettings.hideWhenOff !== undefined) ? widgetSettings.hideWhenOff : (widgetMetadata.hideWhenOff !== undefined ? widgetMetadata.hideWhenOff : false)
implicitWidth: isVertical ? capsuleHeight : Math.round(layout.implicitWidth + Style.marginXL)
implicitHeight: isVertical ? Math.round(layout.implicitHeight + Style.marginXL) : capsuleHeight
// Content dimensions for implicit sizing
readonly property real contentWidth: isVertical ? capsuleHeight : Math.round(layout.implicitWidth + Style.marginXL)
readonly property real contentHeight: isVertical ? Math.round(layout.implicitHeight + Style.marginXL) : capsuleHeight
Layout.alignment: Qt.AlignVCenter
radius: Style.radiusM
color: Style.capsuleColor
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
implicitWidth: contentWidth
implicitHeight: contentHeight
NPopupContextMenu {
id: contextMenu
@@ -78,6 +75,72 @@ Rectangle {
}
}
// Visual capsule centered in parent
Rectangle {
id: visualCapsule
width: root.contentWidth
height: root.contentHeight
anchors.centerIn: parent
radius: Style.radiusM
color: Style.capsuleColor
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
Item {
id: layout
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: rowLayout.visible ? rowLayout.implicitWidth : colLayout.implicitWidth
implicitHeight: rowLayout.visible ? rowLayout.implicitHeight : colLayout.implicitHeight
RowLayout {
id: rowLayout
visible: !root.isVertical
spacing: 0
NIcon {
visible: root.showCaps && (!root.hideWhenOff || LockKeysService.capsLockOn)
icon: root.capsIcon
color: LockKeysService.capsLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3)
}
NIcon {
visible: root.showNum && (!root.hideWhenOff || LockKeysService.numLockOn)
icon: root.numIcon
color: LockKeysService.numLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3)
}
NIcon {
visible: root.showScroll && (!root.hideWhenOff || LockKeysService.scrollLockOn)
icon: root.scrollIcon
color: LockKeysService.scrollLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3)
}
}
ColumnLayout {
id: colLayout
visible: root.isVertical
spacing: 0
NIcon {
visible: root.showCaps && (!root.hideWhenOff || LockKeysService.capsLockOn)
icon: root.capsIcon
color: LockKeysService.capsLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3)
}
NIcon {
visible: root.showNum && (!root.hideWhenOff || LockKeysService.numLockOn)
icon: root.numIcon
color: LockKeysService.numLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3)
}
NIcon {
visible: root.showScroll && (!root.hideWhenOff || LockKeysService.scrollLockOn)
icon: root.scrollIcon
color: LockKeysService.scrollLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3)
}
}
} // closes layout
} // closes visualCapsule
// MouseArea at root level for extended click area
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
@@ -87,57 +150,4 @@ Rectangle {
}
}
}
Item {
id: layout
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: rowLayout.visible ? rowLayout.implicitWidth : colLayout.implicitWidth
implicitHeight: rowLayout.visible ? rowLayout.implicitHeight : colLayout.implicitHeight
RowLayout {
id: rowLayout
visible: !root.isVertical
spacing: 0
NIcon {
visible: root.showCaps && (!root.hideWhenOff || LockKeysService.capsLockOn)
icon: root.capsIcon
color: LockKeysService.capsLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3)
}
NIcon {
visible: root.showNum && (!root.hideWhenOff || LockKeysService.numLockOn)
icon: root.numIcon
color: LockKeysService.numLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3)
}
NIcon {
visible: root.showScroll && (!root.hideWhenOff || LockKeysService.scrollLockOn)
icon: root.scrollIcon
color: LockKeysService.scrollLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3)
}
}
ColumnLayout {
id: colLayout
visible: root.isVertical
spacing: 0
NIcon {
visible: root.showCaps && (!root.hideWhenOff || LockKeysService.capsLockOn)
icon: root.capsIcon
color: LockKeysService.capsLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3)
}
NIcon {
visible: root.showNum && (!root.hideWhenOff || LockKeysService.numLockOn)
icon: root.numIcon
color: LockKeysService.numLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3)
}
NIcon {
visible: root.showScroll && (!root.hideWhenOff || LockKeysService.scrollLockOn)
icon: root.scrollIcon
color: LockKeysService.scrollLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3)
}
}
}
}
+3 -3
View File
@@ -227,11 +227,11 @@ Item {
}
}
// Main container
// Main container - stays at content size, pixel-perfect centered in parent
Rectangle {
id: container
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, height)
width: isVertical ? (isHidden ? 0 : verticalSize) : (isHidden ? 0 : contentWidth)
height: isVertical ? (isHidden ? 0 : verticalSize) : capsuleHeight
radius: Style.radiusM
File diff suppressed because it is too large Load Diff
+276 -264
View File
@@ -10,7 +10,7 @@ import qs.Services.System
import qs.Services.UI
import qs.Widgets
Rectangle {
Item {
id: root
property ShellScreen screen
@@ -589,7 +589,8 @@ Rectangle {
}
}
implicitWidth: {
// Content dimensions for implicit sizing
readonly property real contentWidth: {
if (!visible)
return 0;
if (isVerticalBar)
@@ -604,315 +605,326 @@ Rectangle {
return Math.round(calculatedWidth);
}
implicitHeight: visible ? (isVerticalBar ? Math.round(taskbarLayout.implicitHeight + Style.marginXL) : capsuleHeight) : 0
radius: Style.radiusM
color: Style.capsuleColor
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
readonly property real contentHeight: visible ? (isVerticalBar ? Math.round(taskbarLayout.implicitHeight + Style.marginXL) : capsuleHeight) : 0
GridLayout {
id: taskbarLayout
implicitWidth: contentWidth
implicitHeight: contentHeight
// Pixel-perfect centering
x: isVerticalBar ? Style.pixelAlignCenter(parent.width, width) : ((root.showTitle) ? Style.pixelAlignCenter(parent.width, width) : Style.marginM)
y: Style.pixelAlignCenter(parent.height, height)
// Visual capsule centered in parent
Rectangle {
id: visualCapsule
width: root.contentWidth
height: root.contentHeight
anchors.centerIn: parent
radius: Style.radiusM
color: Style.capsuleColor
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
// Configure GridLayout to behave like RowLayout or ColumnLayout
rows: isVerticalBar ? -1 : 1 // -1 means unlimited
columns: isVerticalBar ? 1 : -1 // -1 means unlimited
GridLayout {
id: taskbarLayout
rowSpacing: isVerticalBar ? Style.marginXXS : 0
columnSpacing: isVerticalBar ? 0 : Style.marginXXS
// Pixel-perfect centering
x: isVerticalBar ? Style.pixelAlignCenter(parent.width, width) : ((root.showTitle) ? Style.pixelAlignCenter(parent.width, width) : Style.marginM)
y: Style.pixelAlignCenter(parent.height, height)
Repeater {
model: root.combinedModel
delegate: Item {
id: taskbarItem
required property var modelData
required property int index
property ShellScreen screen: root.screen
// Configure GridLayout to behave like RowLayout or ColumnLayout
rows: isVerticalBar ? -1 : 1 // -1 means unlimited
columns: isVerticalBar ? 1 : -1 // -1 means unlimited
readonly property bool isRunning: modelData.window !== null
readonly property bool isPinned: modelData.type === "pinned" || modelData.type === "pinned-running"
readonly property bool isFocused: isRunning && modelData.window && modelData.window.isFocused
readonly property bool isPinnedRunning: isPinned && isRunning && !isFocused
readonly property bool isHovered: root.hoveredWindowId === modelData.id
rowSpacing: isVerticalBar ? Style.marginXXS : 0
columnSpacing: isVerticalBar ? 0 : Style.marginXXS
readonly property bool shouldShowTitle: root.showTitle && modelData.type !== "pinned"
readonly property real itemSpacing: Style.marginS
readonly property real contentWidth: shouldShowTitle ? root.itemSize + itemSpacing + root.titleWidth : root.itemSize
Repeater {
model: root.combinedModel
delegate: Item {
id: taskbarItem
required property var modelData
required property int index
property ShellScreen screen: root.screen
readonly property string title: modelData.title || modelData.appId || "Unknown application"
readonly property color titleBgColor: (isHovered || isFocused) ? Color.mHover : Style.capsuleColor
readonly property color titleFgColor: (isHovered || isFocused) ? Color.mOnHover : Color.mOnSurface
readonly property bool isRunning: modelData.window !== null
readonly property bool isPinned: modelData.type === "pinned" || modelData.type === "pinned-running"
readonly property bool isFocused: isRunning && modelData.window && modelData.window.isFocused
readonly property bool isPinnedRunning: isPinned && isRunning && !isFocused
readonly property bool isHovered: root.hoveredWindowId === modelData.id
Layout.preferredWidth: root.showTitle ? Math.round(contentWidth + Style.marginXL) : Math.round(contentWidth) // Add margins for both pinned and running apps
Layout.preferredHeight: root.itemSize
Layout.alignment: Qt.AlignCenter
readonly property bool shouldShowTitle: root.showTitle && modelData.type !== "pinned"
readonly property real itemSpacing: Style.marginS
readonly property real contentWidth: shouldShowTitle ? root.itemSize + itemSpacing + root.titleWidth : root.itemSize
// Ensure dragged item is on top
z: (root.dragSourceIndex === index) ? 1000 : 1
readonly property string title: modelData.title || modelData.appId || "Unknown application"
readonly property color titleBgColor: (isHovered || isFocused) ? Color.mHover : Style.capsuleColor
readonly property color titleFgColor: (isHovered || isFocused) ? Color.mOnHover : Color.mOnSurface
property int modelIndex: index
objectName: "taskbarAppItem"
Layout.preferredWidth: root.showTitle ? Math.round(contentWidth + Style.marginXL) : Math.round(contentWidth) // Add margins for both pinned and running apps
Layout.preferredHeight: root.itemSize
Layout.alignment: Qt.AlignCenter
DropArea {
anchors.fill: parent
keys: ["taskbar-app"]
onEntered: function (drag) {
if (drag.source && drag.source.objectName === "taskbarAppItem") {
root.dragTargetIndex = taskbarItem.modelIndex;
// Ensure dragged item is on top
z: (root.dragSourceIndex === index) ? 1000 : 1
property int modelIndex: index
objectName: "taskbarAppItem"
DropArea {
anchors.fill: parent
keys: ["taskbar-app"]
onEntered: function (drag) {
if (drag.source && drag.source.objectName === "taskbarAppItem") {
root.dragTargetIndex = taskbarItem.modelIndex;
}
}
}
onExited: function () {
if (root.dragTargetIndex === taskbarItem.modelIndex) {
onExited: function () {
if (root.dragTargetIndex === taskbarItem.modelIndex) {
root.dragTargetIndex = -1;
}
}
onDropped: function (drop) {
root.dragSourceIndex = -1;
root.dragTargetIndex = -1;
}
}
onDropped: function (drop) {
root.dragSourceIndex = -1;
root.dragTargetIndex = -1;
Logger.d("Taskbar", "Dropped! Source: " + (drop.source ? drop.source.objectName : "null") + " Index: " + (drop.source ? drop.source.modelIndex : "?") + " -> Target Index: " + taskbarItem.modelIndex);
if (drop.source && drop.source.objectName === "taskbarAppItem" && drop.source !== taskbarItem) {
root.reorderApps(drop.source.modelIndex, taskbarItem.modelIndex);
} else {
Logger.d("Taskbar", "Drop ignored. Source objectName: " + (drop.source ? drop.source.objectName : "null"));
}
}
}
Item {
id: draggableContent
width: parent.width
height: parent.height
anchors.centerIn: dragging ? undefined : parent
// Visual shifting logic
readonly property bool isDragged: root.dragSourceIndex === index
property real shiftOffset: 0
// Calculate shift based on drag state
// If I am NOT the dragged item, but I am in the path of the drag
Binding on shiftOffset {
value: {
if (root.dragSourceIndex !== -1 && root.dragTargetIndex !== -1 && !draggableContent.isDragged) {
if (root.dragSourceIndex < root.dragTargetIndex) {
// Dragging Right: Items between source and target shift Left
if (index > root.dragSourceIndex && index <= root.dragTargetIndex) {
return -1 * (root.isVerticalBar ? root.itemSize : draggableContent.width); // Simple approximation, could be refined
}
} else if (root.dragSourceIndex > root.dragTargetIndex) {
// Dragging Left: Items between target and source shift Right
if (index >= root.dragTargetIndex && index < root.dragSourceIndex) {
return (root.isVerticalBar ? root.itemSize : draggableContent.width);
}
}
}
return 0;
}
}
transform: Translate {
x: !root.isVerticalBar ? draggableContent.shiftOffset : 0
y: root.isVerticalBar ? draggableContent.shiftOffset : 0
Behavior on x {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutQuad
}
}
Behavior on y {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutQuad
Logger.d("Taskbar", "Dropped! Source: " + (drop.source ? drop.source.objectName : "null") + " Index: " + (drop.source ? drop.source.modelIndex : "?") + " -> Target Index: " + taskbarItem.modelIndex);
if (drop.source && drop.source.objectName === "taskbarAppItem" && drop.source !== taskbarItem) {
root.reorderApps(drop.source.modelIndex, taskbarItem.modelIndex);
} else {
Logger.d("Taskbar", "Drop ignored. Source objectName: " + (drop.source ? drop.source.objectName : "null"));
}
}
}
property bool dragging: taskbarMouseArea.drag.active
onDraggingChanged: {
if (dragging) {
root.dragSourceIndex = index;
} else {
// Don't reset immediately on release to allow drop to handle it,
// or use a timer if needed, but drop handler usually fires.
// However, if dropped outside, we need to reset.
// Let's reset if not handled by drop area quickly?
// Actually, drag.active becomes false on release.
// We might want to clear it if no drop happened.
if (root.dragSourceIndex === index) {
// Slight delay/check? For now, let DropArea handle reset on success.
// If cancelled (dropped nowhere), we should reset.
Qt.callLater(() => {
if (!taskbarMouseArea.drag.active && root.dragSourceIndex === index) {
root.dragSourceIndex = -1;
root.dragTargetIndex = -1;
}
});
}
}
}
Drag.active: dragging
Drag.source: taskbarItem
Drag.hotSpot.x: width / 2
Drag.hotSpot.y: height / 2
Drag.keys: ["taskbar-app"]
z: dragging ? 1000 : 0
scale: dragging ? 1.05 : 1.0
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
}
}
Rectangle {
id: titleBackground
visible: shouldShowTitle
anchors.centerIn: parent
Item {
id: draggableContent
width: parent.width
height: root.height
color: titleBgColor
radius: Style.radiusM
height: parent.height
anchors.centerIn: dragging ? undefined : parent
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
// Visual shifting logic
readonly property bool isDragged: root.dragSourceIndex === index
property real shiftOffset: 0
// Calculate shift based on drag state
// If I am NOT the dragged item, but I am in the path of the drag
Binding on shiftOffset {
value: {
if (root.dragSourceIndex !== -1 && root.dragTargetIndex !== -1 && !draggableContent.isDragged) {
if (root.dragSourceIndex < root.dragTargetIndex) {
// Dragging Right: Items between source and target shift Left
if (index > root.dragSourceIndex && index <= root.dragTargetIndex) {
return -1 * (root.isVerticalBar ? root.itemSize : draggableContent.width); // Simple approximation, could be refined
}
} else if (root.dragSourceIndex > root.dragTargetIndex) {
// Dragging Left: Items between target and source shift Right
if (index >= root.dragTargetIndex && index < root.dragSourceIndex) {
return (root.isVerticalBar ? root.itemSize : draggableContent.width);
}
}
}
return 0;
}
}
}
Rectangle {
anchors.centerIn: parent
width: taskbarItem.contentWidth
height: parent.height
color: "transparent"
transform: Translate {
x: !root.isVerticalBar ? draggableContent.shiftOffset : 0
y: root.isVerticalBar ? draggableContent.shiftOffset : 0
RowLayout {
id: itemLayout
anchors.fill: parent
spacing: taskbarItem.itemSpacing
Behavior on x {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutQuad
}
}
Behavior on y {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutQuad
}
}
}
Item {
Layout.preferredWidth: root.itemSize
Layout.preferredHeight: root.itemSize
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
property bool dragging: taskbarMouseArea.drag.active
onDraggingChanged: {
if (dragging) {
root.dragSourceIndex = index;
} else {
// Don't reset immediately on release to allow drop to handle it,
// or use a timer if needed, but drop handler usually fires.
// However, if dropped outside, we need to reset.
// Let's reset if not handled by drop area quickly?
// Actually, drag.active becomes false on release.
// We might want to clear it if no drop happened.
if (root.dragSourceIndex === index) {
// Slight delay/check? For now, let DropArea handle reset on success.
// If cancelled (dropped nowhere), we should reset.
Qt.callLater(() => {
if (!taskbarMouseArea.drag.active && root.dragSourceIndex === index) {
root.dragSourceIndex = -1;
root.dragTargetIndex = -1;
}
});
}
}
}
IconImage {
id: appIcon
anchors.fill: parent
Drag.active: dragging
Drag.source: taskbarItem
Drag.hotSpot.x: width / 2
Drag.hotSpot.y: height / 2
Drag.keys: ["taskbar-app"]
source: ThemeIcons.iconForAppId(taskbarItem.modelData.appId)
smooth: true
asynchronous: true
z: dragging ? 1000 : 0
scale: dragging ? 1.05 : 1.0
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
}
}
// Apply dock shader to all taskbar icons
layer.enabled: widgetSettings.colorizeIcons !== false
layer.effect: ShaderEffect {
property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant
property real colorizeMode: 0.0 // Dock mode (grayscale)
Rectangle {
id: titleBackground
visible: shouldShowTitle
anchors.centerIn: parent
width: parent.width
height: root.height
color: titleBgColor
radius: Style.radiusM
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
}
Rectangle {
anchors.centerIn: parent
width: taskbarItem.contentWidth
height: parent.height
color: "transparent"
RowLayout {
id: itemLayout
anchors.fill: parent
spacing: taskbarItem.itemSpacing
Item {
Layout.preferredWidth: root.itemSize
Layout.preferredHeight: root.itemSize
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
IconImage {
id: appIcon
anchors.fill: parent
source: ThemeIcons.iconForAppId(taskbarItem.modelData.appId)
smooth: true
asynchronous: true
// Apply dock shader to all taskbar icons
layer.enabled: widgetSettings.colorizeIcons !== false
layer.effect: ShaderEffect {
property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant
property real colorizeMode: 0.0 // Dock mode (grayscale)
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
}
}
Rectangle {
id: iconBackground
visible: !shouldShowTitle
anchors.bottomMargin: -2
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
width: Style.toOdd(root.itemSize * 0.25)
height: 4
color: taskbarItem.isFocused ? Color.mPrimary : "transparent"
radius: Math.min(Style.radiusXXS, width / 2)
}
}
Rectangle {
id: iconBackground
visible: !shouldShowTitle
anchors.bottomMargin: -2
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
width: Style.toOdd(root.itemSize * 0.25)
height: 4
color: taskbarItem.isFocused ? Color.mPrimary : "transparent"
radius: Math.min(Style.radiusXXS, width / 2)
NText {
id: titleText
visible: shouldShowTitle
Layout.preferredWidth: root.titleWidth
Layout.preferredHeight: root.itemSize
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.fillWidth: false
text: taskbarItem.title
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignLeft
pointSize: barFontSize
color: titleFgColor
opacity: Style.opacityFull
}
}
NText {
id: titleText
visible: shouldShowTitle
Layout.preferredWidth: root.titleWidth
Layout.preferredHeight: root.itemSize
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.fillWidth: false
text: taskbarItem.title
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignLeft
pointSize: barFontSize
color: titleFgColor
opacity: Style.opacityFull
}
}
}
}
MouseArea {
id: taskbarMouseArea
objectName: "taskbarMouseArea"
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
MouseArea {
id: taskbarMouseArea
objectName: "taskbarMouseArea"
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
drag.target: draggableContent
drag.axis: root.isVerticalBar ? Drag.YAxis : Drag.XAxis
preventStealing: true
drag.target: draggableContent
drag.axis: root.isVerticalBar ? Drag.YAxis : Drag.XAxis
preventStealing: true
onPressed: {
// Constrain drag to roughly the taskbar area but allow some freedom
// Or just let it be free since we only care about drops
}
onReleased: {
if (draggableContent.Drag.active) {
draggableContent.Drag.drop();
onPressed: {
// Constrain drag to roughly the taskbar area but allow some freedom
// Or just let it be free since we only care about drops
}
}
onClicked: function (mouse) {
if (!modelData)
return;
if (mouse.button === Qt.LeftButton) {
if (isRunning && modelData.window) {
// Running app - focus it
try {
CompositorService.focusWindow(modelData.window);
} catch (error) {
Logger.e("Taskbar", "Failed to activate toplevel: " + error);
}
} else if (isPinned) {
// Pinned app not running - launch it
root.launchPinnedApp(modelData.appId);
onReleased: {
if (draggableContent.Drag.active) {
draggableContent.Drag.drop();
}
} else if (mouse.button === Qt.RightButton) {
}
onClicked: function (mouse) {
if (!modelData)
return;
if (mouse.button === Qt.LeftButton) {
if (isRunning && modelData.window) {
// Running app - focus it
try {
CompositorService.focusWindow(modelData.window);
} catch (error) {
Logger.e("Taskbar", "Failed to activate toplevel: " + error);
}
} else if (isPinned) {
// Pinned app not running - launch it
root.launchPinnedApp(modelData.appId);
}
} else if (mouse.button === Qt.RightButton) {
TooltipService.hide();
// Only show context menu for running apps
if (isRunning && modelData.window) {
root.selectedWindowId = modelData.id;
root.selectedAppId = modelData.appId;
root.openTaskbarContextMenu(taskbarItem);
}
}
}
onEntered: {
root.hoveredWindowId = taskbarItem.modelData.id;
TooltipService.show(taskbarItem, taskbarItem.title, BarService.getTooltipDirection(root.screen?.name));
}
onExited: {
root.hoveredWindowId = "";
TooltipService.hide();
// Only show context menu for running apps
if (isRunning && modelData.window) {
root.selectedWindowId = modelData.id;
root.selectedAppId = modelData.appId;
root.openTaskbarContextMenu(taskbarItem);
}
}
}
onEntered: {
root.hoveredWindowId = taskbarItem.modelData.id;
TooltipService.show(taskbarItem, taskbarItem.title, BarService.getTooltipDirection(root.screen?.name));
}
onExited: {
root.hoveredWindowId = "";
TooltipService.hide();
}
}
}
}
}
} // closes GridLayout
} // closes visualCapsule
function openTaskbarContextMenu(item) {
// Build menu model directly
+185 -172
View File
@@ -10,7 +10,7 @@ import qs.Modules.Bar.Extras
import qs.Services.UI
import qs.Widgets
Rectangle {
Item {
id: root
property ShellScreen screen
@@ -286,204 +286,217 @@ Rectangle {
Component.onCompleted: {
root.updateFilteredItems(); // Initial update
}
implicitWidth: isVertical ? capsuleHeight : Math.round(trayFlow.implicitWidth)
implicitHeight: isVertical ? Math.round(trayFlow.implicitHeight) : capsuleHeight
radius: Style.radiusM
color: Style.capsuleColor
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
// Content dimensions for implicit sizing
readonly property real contentWidth: isVertical ? capsuleHeight : Math.round(trayFlow.implicitWidth)
readonly property real contentHeight: isVertical ? Math.round(trayFlow.implicitHeight) : capsuleHeight
implicitWidth: contentWidth
implicitHeight: contentHeight
visible: filteredItems.length > 0 || dropdownItems.length > 0
opacity: (filteredItems.length > 0 || dropdownItems.length > 0) ? 1.0 : 0.0
Flow {
id: trayFlow
spacing: Style.marginXS
flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight
// Visual capsule centered in parent
Rectangle {
id: visualCapsule
width: root.contentWidth
height: root.contentHeight
anchors.centerIn: parent
radius: Style.radiusM
color: Style.capsuleColor
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
// Pixel-perfect centering
x: isVertical ? Style.pixelAlignCenter(parent.width, width) : 0
y: isVertical ? 0 : Style.pixelAlignCenter(parent.height, height)
Flow {
id: trayFlow
spacing: Style.marginXS
flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight
// Drawer opener (before items if opposite direction)
NIconButton {
id: chevronIconBefore
visible: root.drawerEnabled && dropdownItems.length > 0 && BarService.getPillDirection(root)
tooltipText: I18n.tr("tooltips.open-tray-dropdown")
tooltipDirection: BarService.getTooltipDirection(root.screen?.name)
baseSize: capsuleHeight
applyUiScale: false
customRadius: Style.radiusL
colorBg: "transparent"
colorFg: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mOnPrimary
colorBorder: "transparent"
colorBorderHover: "transparent"
icon: {
switch (barPosition) {
case "bottom":
return "caret-up";
case "left":
return "caret-right";
case "right":
return "caret-left";
case "top":
default:
return "caret-down";
// Pixel-perfect centering
x: isVertical ? Style.pixelAlignCenter(parent.width, width) : 0
y: isVertical ? 0 : Style.pixelAlignCenter(parent.height, height)
// Drawer opener (before items if opposite direction)
NIconButton {
id: chevronIconBefore
visible: root.drawerEnabled && dropdownItems.length > 0 && BarService.getPillDirection(root)
tooltipText: I18n.tr("tooltips.open-tray-dropdown")
tooltipDirection: BarService.getTooltipDirection(root.screen?.name)
baseSize: capsuleHeight
applyUiScale: false
customRadius: Style.radiusL
colorBg: "transparent"
colorFg: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mOnPrimary
colorBorder: "transparent"
colorBorderHover: "transparent"
icon: {
switch (barPosition) {
case "bottom":
return "caret-up";
case "left":
return "caret-right";
case "right":
return "caret-left";
case "top":
default:
return "caret-down";
}
}
onClicked: toggleDrawer(this)
onRightClicked: toggleDrawer(this)
}
onClicked: toggleDrawer(this)
onRightClicked: toggleDrawer(this)
}
// Pinned items
Repeater {
id: repeater
model: root.filteredItems
// Pinned items
Repeater {
id: repeater
model: root.filteredItems
delegate: Item {
width: capsuleHeight
height: capsuleHeight
visible: modelData
delegate: Item {
width: capsuleHeight
height: capsuleHeight
visible: modelData
IconImage {
id: trayIcon
width: iconSize
height: iconSize
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, height)
asynchronous: true
backer.fillMode: Image.PreserveAspectFit
IconImage {
id: trayIcon
width: iconSize
height: iconSize
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, height)
asynchronous: true
backer.fillMode: Image.PreserveAspectFit
property bool menuJustOpened: false
property bool menuJustOpened: false
source: {
let icon = modelData?.icon || "";
if (!icon) {
return "";
source: {
let icon = modelData?.icon || "";
if (!icon) {
return "";
}
// Process icon path
if (icon.includes("?path=")) {
const chunks = icon.split("?path=");
const name = chunks[0];
const path = chunks[1];
const fileName = name.substring(name.lastIndexOf("/") + 1);
return `file://${path}/${fileName}`;
}
return icon;
}
opacity: status === Image.Ready ? 1 : 0
layer.enabled: widgetSettings.colorizeIcons !== false
layer.effect: ShaderEffect {
property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant
property real colorizeMode: 1.0
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
}
// Process icon path
if (icon.includes("?path=")) {
const chunks = icon.split("?path=");
const name = chunks[0];
const path = chunks[1];
const fileName = name.substring(name.lastIndexOf("/") + 1);
return `file://${path}/${fileName}`;
}
return icon;
}
opacity: status === Image.Ready ? 1 : 0
layer.enabled: widgetSettings.colorizeIcons !== false
layer.effect: ShaderEffect {
property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant
property real colorizeMode: 1.0
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: mouse => {
if (!modelData) {
return;
}
if (mouse.button === Qt.LeftButton) {
// Close any open menu first
if (popupMenuWindow) {
popupMenuWindow.close();
}
if (!modelData.onlyMenu) {
modelData.activate();
}
} else if (mouse.button === Qt.MiddleButton) {
// Close the menu if it was visible
if (popupMenuWindow && popupMenuWindow.visible) {
popupMenuWindow.close();
return;
}
modelData.secondaryActivate && modelData.secondaryActivate();
} else if (mouse.button === Qt.RightButton) {
TooltipService.hideImmediately();
// Close the menu if it was visible
if (popupMenuWindow && popupMenuWindow.visible) {
popupMenuWindow.close();
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: mouse => {
if (!modelData) {
return;
}
// Close any opened panel
if ((PanelService.openedPanel !== null) && !PanelService.openedPanel.isClosing) {
PanelService.openedPanel.close();
}
if (modelData.hasMenu && modelData.menu && trayMenu && trayMenu.item) {
// Position menu based on bar position
let menuX, menuY;
if (barPosition === "left") {
// For left bar: position menu to the right of the bar
menuX = width + Style.marginM;
menuY = 0;
} else if (barPosition === "right") {
// For right bar: position menu to the left of the bar
menuX = -trayMenu.item.width - Style.marginM;
menuY = 0;
} else {
// For horizontal bars: center horizontally and position below
menuX = (width / 2) - (trayMenu.item.width / 2);
menuY = (barPosition === "top") ? barHeight + Style.marginS - 2 : barHeight + Style.marginS - 2;
if (mouse.button === Qt.LeftButton) {
// Close any open menu first
if (popupMenuWindow) {
popupMenuWindow.close();
}
PanelService.showTrayMenu(root.screen, modelData, trayMenu.item, parent, menuX, menuY, root.section, root.sectionWidgetIndex);
} else {
Logger.d("Tray", "No menu available for", modelData.id, "or trayMenu not set");
if (!modelData.onlyMenu) {
modelData.activate();
}
} else if (mouse.button === Qt.MiddleButton) {
// Close the menu if it was visible
if (popupMenuWindow && popupMenuWindow.visible) {
popupMenuWindow.close();
return;
}
modelData.secondaryActivate && modelData.secondaryActivate();
} else if (mouse.button === Qt.RightButton) {
TooltipService.hideImmediately();
// Close the menu if it was visible
if (popupMenuWindow && popupMenuWindow.visible) {
popupMenuWindow.close();
return;
}
// Close any opened panel
if ((PanelService.openedPanel !== null) && !PanelService.openedPanel.isClosing) {
PanelService.openedPanel.close();
}
if (modelData.hasMenu && modelData.menu && trayMenu && trayMenu.item) {
// Position menu based on bar position
let menuX, menuY;
if (barPosition === "left") {
// For left bar: position menu to the right of the bar
menuX = width + Style.marginM;
menuY = 0;
} else if (barPosition === "right") {
// For right bar: position menu to the left of the bar
menuX = -trayMenu.item.width - Style.marginM;
menuY = 0;
} else {
// For horizontal bars: center horizontally and position below
menuX = (width / 2) - (trayMenu.item.width / 2);
menuY = (barPosition === "top") ? barHeight + Style.marginS - 2 : barHeight + Style.marginS - 2;
}
PanelService.showTrayMenu(root.screen, modelData, trayMenu.item, parent, menuX, menuY, root.section, root.sectionWidgetIndex);
} else {
Logger.d("Tray", "No menu available for", modelData.id, "or trayMenu not set");
}
}
}
}
onEntered: {
if (popupMenuWindow) {
popupMenuWindow.close();
onEntered: {
if (popupMenuWindow) {
popupMenuWindow.close();
}
TooltipService.show(trayIcon, modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item", BarService.getTooltipDirection(root.screen?.name));
}
TooltipService.show(trayIcon, modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item", BarService.getTooltipDirection(root.screen?.name));
onExited: TooltipService.hide()
}
onExited: TooltipService.hide()
}
}
}
}
// Drawer opener (after items if normal direction)
NIconButton {
id: chevronIconAfter
visible: root.drawerEnabled && dropdownItems.length > 0 && !BarService.getPillDirection(root)
tooltipText: I18n.tr("tooltips.open-tray-dropdown")
tooltipDirection: BarService.getTooltipDirection(root.screen?.name)
baseSize: capsuleHeight
applyUiScale: false
customRadius: Style.radiusL
colorBg: "transparent"
colorFg: Color.mOnSurface
colorBorder: "transparent"
colorBorderHover: "transparent"
icon: {
switch (barPosition) {
case "bottom":
return "caret-up";
case "left":
return "caret-right";
case "right":
return "caret-left";
case "top":
default:
return "caret-down";
// Drawer opener (after items if normal direction)
NIconButton {
id: chevronIconAfter
visible: root.drawerEnabled && dropdownItems.length > 0 && !BarService.getPillDirection(root)
tooltipText: I18n.tr("tooltips.open-tray-dropdown")
tooltipDirection: BarService.getTooltipDirection(root.screen?.name)
baseSize: capsuleHeight
applyUiScale: false
customRadius: Style.radiusL
colorBg: "transparent"
colorFg: Color.mOnSurface
colorBorder: "transparent"
colorBorderHover: "transparent"
icon: {
switch (barPosition) {
case "bottom":
return "caret-up";
case "left":
return "caret-right";
case "right":
return "caret-left";
case "top":
default:
return "caret-down";
}
}
onClicked: toggleDrawer(this)
onRightClicked: toggleDrawer(this)
}
onClicked: toggleDrawer(this)
onRightClicked: toggleDrawer(this)
}
}
} // closes Flow
} // closes visualCapsule
}
+43 -23
View File
@@ -4,7 +4,7 @@ import Quickshell.Widgets
import qs.Commons
import qs.Services.UI
Rectangle {
Item {
id: root
property real baseSize: Style.baseWidgetSize
@@ -24,6 +24,11 @@ Rectangle {
property color colorBorderHover: Color.mOutline
property real customRadius: -1 // -1 means use default (iRadiusL), otherwise use this value
// Expose border properties for backwards compatibility (aliases to visualButton)
property alias border: visualButton.border
property alias radius: visualButton.radius
property alias color: visualButton.color
signal entered
signal exited
signal clicked
@@ -31,31 +36,27 @@ Rectangle {
signal middleClicked
signal wheel(int angleDelta)
implicitWidth: applyUiScale ? Style.toOdd(baseSize * Style.uiScaleRatio) : Style.toOdd(baseSize)
implicitHeight: applyUiScale ? Style.toOdd(baseSize * Style.uiScaleRatio) : Style.toOdd(baseSize)
// Calculate button size based on settings
readonly property real buttonSize: applyUiScale ? Style.toOdd(baseSize * Style.uiScaleRatio) : Style.toOdd(baseSize)
// Size: use implicit width/height which layout can override
// BarWidgetLoader sets explicit width/height to extend click area
implicitWidth: buttonSize
implicitHeight: buttonSize
opacity: root.enabled ? Style.opacityFull : Style.opacityMedium
color: root.enabled && root.hovering ? colorBgHover : colorBg
radius: Math.min((customRadius >= 0 ? customRadius : Style.iRadiusL), width / 2)
border.color: root.enabled && root.hovering ? colorBorderHover : colorBorder
border.width: Style.borderS
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
// Visual button - stays at buttonSize, centered in parent
Rectangle {
id: visualButton
width: root.buttonSize
height: root.buttonSize
anchors.centerIn: parent
NIcon {
icon: root.icon
pointSize: Style.toOdd(root.width * 0.48)
applyUiScale: root.applyUiScale
color: root.enabled && root.hovering ? colorFgHover : colorFg
// Pixel-perfect centering
x: Style.pixelAlignCenter(root.width, width)
y: Style.pixelAlignCenter(root.height, contentHeight)
color: root.enabled && root.hovering ? colorBgHover : colorBg
radius: Math.min((customRadius >= 0 ? customRadius : Style.iRadiusL), width / 2)
border.color: root.enabled && root.hovering ? colorBorderHover : colorBorder
border.width: Style.borderS
Behavior on color {
enabled: !Color.isTransitioning
@@ -64,8 +65,27 @@ Rectangle {
easing.type: Easing.InOutQuad
}
}
NIcon {
icon: root.icon
pointSize: Style.toOdd(visualButton.width * 0.48)
applyUiScale: root.applyUiScale
color: root.enabled && root.hovering ? colorFgHover : colorFg
// Pixel-perfect centering
x: Style.pixelAlignCenter(visualButton.width, width)
y: Style.pixelAlignCenter(visualButton.height, contentHeight)
Behavior on color {
enabled: !Color.isTransitioning
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
}
}
// MouseArea fills root (extends beyond visual button for bar click area)
MouseArea {
// Always enabled to allow hover/tooltip even when the button is disabled
enabled: true
@@ -76,7 +96,7 @@ Rectangle {
onEntered: {
hovering = root.enabled ? true : false;
if (tooltipText) {
TooltipService.show(parent, tooltipText, tooltipDirection);
TooltipService.show(root, tooltipText, tooltipDirection);
}
root.entered();
}