fedora
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
|
||||
/**
|
||||
* BarExclusionZone - Invisible PanelWindow that reserves exclusive space for the bar
|
||||
*
|
||||
* This is a minimal window that works with the compositor to reserve space,
|
||||
* while the actual bar UI is rendered in NFullScreenWindow.
|
||||
*/
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name)
|
||||
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property bool barFloating: Settings.data.bar.barType === "floating"
|
||||
readonly property real barMarginH: barFloating ? Settings.data.bar.marginHorizontal : 0
|
||||
readonly property real barMarginV: barFloating ? Settings.data.bar.marginVertical : 0
|
||||
readonly property real barHeight: Style.getBarHeightForScreen(screen?.name)
|
||||
|
||||
// Invisible - just reserves space
|
||||
color: "transparent"
|
||||
|
||||
mask: Region {}
|
||||
|
||||
// Wayland layer shell configuration
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.namespace: "noctalia-bar-exclusion-" + (screen?.name || "unknown")
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Auto
|
||||
|
||||
// Anchor based on bar position
|
||||
anchors {
|
||||
top: barPosition === "top"
|
||||
bottom: barPosition === "bottom"
|
||||
left: barPosition === "left" || barPosition === "top" || barPosition === "bottom"
|
||||
right: barPosition === "right" || barPosition === "top" || barPosition === "bottom"
|
||||
}
|
||||
|
||||
// Size based on bar orientation
|
||||
// When floating, only reserve space for the bar + margin on the anchored edge
|
||||
implicitWidth: {
|
||||
if (barIsVertical) {
|
||||
// Vertical bar: reserve bar height + margin on the anchored edge only
|
||||
if (barFloating) {
|
||||
// For left bar, reserve left margin; for right bar, reserve right margin
|
||||
return barHeight + barMarginH;
|
||||
}
|
||||
return barHeight;
|
||||
}
|
||||
return 0; // Auto-width when left/right anchors are true
|
||||
}
|
||||
|
||||
implicitHeight: {
|
||||
if (!barIsVertical) {
|
||||
// Horizontal bar: reserve bar height + margin on the anchored edge only
|
||||
if (barFloating) {
|
||||
// For top bar, reserve top margin; for bottom bar, reserve bottom margin
|
||||
return barHeight + barMarginV;
|
||||
}
|
||||
return barHeight;
|
||||
}
|
||||
return 0; // Auto-height when top/bottom anchors are true
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.d("BarExclusionZone", "Created for screen:", screen?.name);
|
||||
Logger.d("BarExclusionZone", " Position:", barPosition, "Floating:", barFloating);
|
||||
Logger.d("BarExclusionZone", " Anchors - top:", anchors.top, "bottom:", anchors.bottom, "left:", anchors.left, "right:", anchors.right);
|
||||
Logger.d("BarExclusionZone", " Size:", width, "x", height, "implicitWidth:", implicitWidth, "implicitHeight:", implicitHeight);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
|
||||
property string icon: ""
|
||||
property string text: ""
|
||||
property string suffix: ""
|
||||
property var tooltipText
|
||||
property bool autoHide: false
|
||||
property bool forceOpen: false
|
||||
property bool forceClose: false
|
||||
property bool oppositeDirection: false
|
||||
property bool hovered: false
|
||||
property bool rotateText: false
|
||||
property color customBackgroundColor: "transparent"
|
||||
property color customTextIconColor: "transparent"
|
||||
property color customIconColor: "transparent"
|
||||
property color customTextColor: "transparent"
|
||||
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name)
|
||||
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
|
||||
|
||||
signal shown
|
||||
signal hidden
|
||||
signal entered
|
||||
signal exited
|
||||
signal clicked
|
||||
signal rightClicked
|
||||
signal middleClicked
|
||||
signal wheel(int delta)
|
||||
|
||||
// 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 fills BarPill so child components can extend to full bar dimension
|
||||
Loader {
|
||||
id: pillLoader
|
||||
anchors.fill: parent
|
||||
sourceComponent: isVerticalBar ? verticalPillComponent : horizontalPillComponent
|
||||
|
||||
Component {
|
||||
id: verticalPillComponent
|
||||
BarPillVertical {
|
||||
screen: root.screen
|
||||
icon: root.icon
|
||||
text: root.text
|
||||
suffix: root.suffix
|
||||
tooltipText: root.tooltipText
|
||||
autoHide: root.autoHide
|
||||
forceOpen: root.forceOpen
|
||||
forceClose: root.forceClose
|
||||
oppositeDirection: root.oppositeDirection
|
||||
hovered: root.hovered
|
||||
rotateText: root.rotateText
|
||||
customBackgroundColor: root.customBackgroundColor
|
||||
customTextIconColor: root.customTextIconColor
|
||||
customIconColor: root.customIconColor
|
||||
customTextColor: root.customTextColor
|
||||
onShown: root.shown()
|
||||
onHidden: root.hidden()
|
||||
onEntered: root.entered()
|
||||
onExited: root.exited()
|
||||
onClicked: root.clicked()
|
||||
onRightClicked: root.rightClicked()
|
||||
onMiddleClicked: root.middleClicked()
|
||||
onWheel: delta => root.wheel(delta)
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: horizontalPillComponent
|
||||
BarPillHorizontal {
|
||||
screen: root.screen
|
||||
icon: root.icon
|
||||
text: root.text
|
||||
suffix: root.suffix
|
||||
tooltipText: root.tooltipText
|
||||
autoHide: root.autoHide
|
||||
forceOpen: root.forceOpen
|
||||
forceClose: root.forceClose
|
||||
oppositeDirection: root.oppositeDirection
|
||||
hovered: root.hovered
|
||||
customBackgroundColor: root.customBackgroundColor
|
||||
customTextIconColor: root.customTextIconColor
|
||||
customIconColor: root.customIconColor
|
||||
customTextColor: root.customTextColor
|
||||
onShown: root.shown()
|
||||
onHidden: root.hidden()
|
||||
onEntered: root.entered()
|
||||
onExited: root.exited()
|
||||
onClicked: root.clicked()
|
||||
onRightClicked: root.rightClicked()
|
||||
onMiddleClicked: root.middleClicked()
|
||||
onWheel: delta => root.wheel(delta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (pillLoader.item && pillLoader.item.show) {
|
||||
pillLoader.item.show();
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (pillLoader.item && pillLoader.item.hide) {
|
||||
pillLoader.item.hide();
|
||||
}
|
||||
}
|
||||
|
||||
function showDelayed() {
|
||||
if (pillLoader.item && pillLoader.item.showDelayed) {
|
||||
pillLoader.item.showDelayed();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
|
||||
property string icon: ""
|
||||
property string text: ""
|
||||
property string suffix: ""
|
||||
property var tooltipText
|
||||
property bool autoHide: false
|
||||
property bool forceOpen: false
|
||||
property bool forceClose: false
|
||||
property bool oppositeDirection: false
|
||||
property bool hovered: false
|
||||
property color customBackgroundColor: "transparent"
|
||||
property color customTextIconColor: "transparent"
|
||||
property color customIconColor: "transparent"
|
||||
property color customTextColor: "transparent"
|
||||
|
||||
readonly property bool collapseToIcon: forceClose && !forceOpen
|
||||
|
||||
// Effective shown state (true if hovered/animated open or forced)
|
||||
readonly property bool revealed: !forceClose && (forceOpen || showPill)
|
||||
readonly property bool hasIcon: root.icon !== ""
|
||||
|
||||
signal shown
|
||||
signal hidden
|
||||
signal entered
|
||||
signal exited
|
||||
signal clicked
|
||||
signal rightClicked
|
||||
signal middleClicked
|
||||
signal wheel(int delta)
|
||||
|
||||
// Internal state
|
||||
property bool showPill: false
|
||||
property bool shouldAnimateHide: false
|
||||
|
||||
readonly property int pillHeight: Style.getCapsuleHeightForScreen(screen?.name)
|
||||
readonly property real barFontSize: Style.getBarFontSizeForScreen(screen?.name)
|
||||
readonly property int pillPaddingHorizontal: Math.round(pillHeight * 0.2)
|
||||
readonly property int pillOverlap: Math.round(pillHeight * 0.5)
|
||||
readonly property int pillMaxWidth: Math.max(1, Math.round(textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap))
|
||||
|
||||
// Always prioritize hover color, then the custom one and finally the fallback color
|
||||
readonly property color bgColor: hovered ? Color.mHover : (customBackgroundColor.a > 0) ? customBackgroundColor : Style.capsuleColor
|
||||
readonly property color fgColor: hovered ? Color.mOnHover : (customTextIconColor.a > 0) ? customTextIconColor : Color.mOnSurface
|
||||
readonly property color iconFgColor: hovered ? Color.mOnHover : (customIconColor.a > 0) ? customIconColor : (customTextIconColor.a > 0) ? customTextIconColor : Color.mOnSurface
|
||||
readonly property color textFgColor: hovered ? Color.mOnHover : (customTextColor.a > 0) ? customTextColor : (customTextIconColor.a > 0) ? customTextIconColor : Color.mOnSurface
|
||||
|
||||
readonly property real iconSize: Style.toOdd(pillHeight * 0.48)
|
||||
|
||||
// Content width calculation (for implicit sizing)
|
||||
readonly property real contentWidth: {
|
||||
if (collapseToIcon) {
|
||||
return hasIcon ? pillHeight : 0;
|
||||
}
|
||||
var overlap = hasIcon ? pillOverlap : 0;
|
||||
var baseWidth = hasIcon ? pillHeight : 0;
|
||||
return baseWidth + Math.max(0, pill.width - overlap);
|
||||
}
|
||||
|
||||
// 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
|
||||
function onTooltipTextChanged() {
|
||||
if (hovered) {
|
||||
TooltipService.updateText(root.tooltipText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unified background for the entire pill area to avoid overlapping opacity
|
||||
Rectangle {
|
||||
id: pillBackground
|
||||
width: collapseToIcon ? pillHeight : root.width
|
||||
height: pillHeight
|
||||
radius: Style.radiusM
|
||||
color: root.bgColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
border.color: Style.capsuleBorderColor
|
||||
border.width: Style.capsuleBorderWidth
|
||||
|
||||
Behavior on color {
|
||||
enabled: !Color.isTransitioning
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: pill
|
||||
|
||||
width: revealed ? pillMaxWidth : 1
|
||||
height: pillHeight
|
||||
|
||||
x: {
|
||||
if (!hasIcon)
|
||||
return 0;
|
||||
return oppositeDirection ? (iconCircle.x + iconCircle.width / 2) : (iconCircle.x + iconCircle.width / 2) - width;
|
||||
}
|
||||
|
||||
opacity: revealed ? Style.opacityFull : Style.opacityNone
|
||||
color: "transparent" // Make pill background transparent to avoid double opacity
|
||||
|
||||
topLeftRadius: oppositeDirection ? 0 : Style.radiusM
|
||||
bottomLeftRadius: oppositeDirection ? 0 : Style.radiusM
|
||||
topRightRadius: oppositeDirection ? Style.radiusM : 0
|
||||
bottomRightRadius: oppositeDirection ? Style.radiusM : 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
NText {
|
||||
id: textItem
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: {
|
||||
if (!hasIcon)
|
||||
return (parent.width - width) / 2;
|
||||
|
||||
// Better text horizontal centering
|
||||
var centerX = (parent.width - width) / 2;
|
||||
var offset = oppositeDirection ? Style.marginXS : -Style.marginXS;
|
||||
if (forceOpen) {
|
||||
// If its force open, the icon disc background is the same color as the bg pill move text slightly
|
||||
offset += oppositeDirection ? -Style.marginXXS : Style.marginXXS;
|
||||
}
|
||||
return centerX + offset;
|
||||
}
|
||||
text: root.text + root.suffix
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: root.barFontSize
|
||||
applyUiScale: false
|
||||
color: root.textFgColor
|
||||
visible: revealed
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
enabled: showAnim.running || hideAnim.running
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
enabled: showAnim.running || hideAnim.running
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: iconCircle
|
||||
width: hasIcon ? pillHeight : 0
|
||||
height: pillHeight
|
||||
radius: Math.min(Style.radiusL, width / 2)
|
||||
color: "transparent" // Make icon background transparent to avoid double opacity
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
x: oppositeDirection ? 0 : (parent.width - width)
|
||||
|
||||
NIcon {
|
||||
icon: root.icon
|
||||
pointSize: iconSize
|
||||
applyUiScale: false
|
||||
color: root.iconFgColor
|
||||
// Center horizontally
|
||||
x: (iconCircle.width - width) / 2
|
||||
// Center vertically accounting for font metrics
|
||||
y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: showAnim
|
||||
running: false
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "width"
|
||||
from: 1
|
||||
to: pillMaxWidth
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
onStarted: {
|
||||
showPill = true;
|
||||
}
|
||||
onStopped: {
|
||||
delayedHideAnim.start();
|
||||
root.shown();
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: delayedHideAnim
|
||||
running: false
|
||||
PauseAnimation {
|
||||
duration: 2500
|
||||
}
|
||||
ScriptAction {
|
||||
script: if (shouldAnimateHide) {
|
||||
hideAnim.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: hideAnim
|
||||
running: false
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "width"
|
||||
from: pillMaxWidth
|
||||
to: 1
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
onStopped: {
|
||||
showPill = false;
|
||||
shouldAnimateHide = false;
|
||||
root.hidden();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: showTimer
|
||||
interval: Style.pillDelay
|
||||
onTriggered: {
|
||||
if (!showPill) {
|
||||
showAnim.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
cursorShape: root.clicked ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onEntered: {
|
||||
hovered = true;
|
||||
root.entered();
|
||||
TooltipService.show(root, root.tooltipText, BarService.getTooltipDirection(root.screen?.name), (forceOpen || forceClose) ? Style.tooltipDelay : Style.tooltipDelayLong);
|
||||
if (forceClose) {
|
||||
return;
|
||||
}
|
||||
if (!forceOpen) {
|
||||
showDelayed();
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
hovered = false;
|
||||
root.exited();
|
||||
if (!forceOpen && !forceClose) {
|
||||
hide();
|
||||
}
|
||||
TooltipService.hide();
|
||||
}
|
||||
onClicked: mouse => {
|
||||
TooltipService.hide();
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
root.clicked();
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
root.rightClicked();
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
root.middleClicked();
|
||||
}
|
||||
}
|
||||
onWheel: wheel => root.wheel(wheel.angleDelta.y)
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (collapseToIcon || root.text.trim().length === 0)
|
||||
return;
|
||||
if (!showPill) {
|
||||
shouldAnimateHide = autoHide;
|
||||
showAnim.start();
|
||||
} else {
|
||||
hideAnim.stop();
|
||||
delayedHideAnim.restart();
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (collapseToIcon)
|
||||
return;
|
||||
if (forceOpen) {
|
||||
return;
|
||||
}
|
||||
if (showPill) {
|
||||
hideAnim.start();
|
||||
}
|
||||
showTimer.stop();
|
||||
}
|
||||
|
||||
function showDelayed() {
|
||||
if (collapseToIcon || root.text.trim().length === 0)
|
||||
return;
|
||||
if (!showPill) {
|
||||
shouldAnimateHide = autoHide;
|
||||
showTimer.start();
|
||||
} else {
|
||||
hideAnim.stop();
|
||||
delayedHideAnim.restart();
|
||||
}
|
||||
}
|
||||
|
||||
onForceOpenChanged: {
|
||||
if (forceOpen) {
|
||||
// Immediately lock open without animations
|
||||
showAnim.stop();
|
||||
hideAnim.stop();
|
||||
delayedHideAnim.stop();
|
||||
showPill = true;
|
||||
} else {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
|
||||
property string icon: ""
|
||||
property string text: ""
|
||||
property string suffix: ""
|
||||
property var tooltipText
|
||||
property bool autoHide: false
|
||||
property bool forceOpen: false
|
||||
property bool forceClose: false
|
||||
property bool oppositeDirection: false
|
||||
property bool hovered: false
|
||||
property bool rotateText: false
|
||||
property color customBackgroundColor: "transparent"
|
||||
property color customTextIconColor: "transparent"
|
||||
property color customIconColor: "transparent"
|
||||
property color customTextColor: "transparent"
|
||||
|
||||
readonly property bool collapseToIcon: forceClose && !forceOpen
|
||||
|
||||
signal shown
|
||||
signal hidden
|
||||
signal entered
|
||||
signal exited
|
||||
signal clicked
|
||||
signal rightClicked
|
||||
signal middleClicked
|
||||
signal wheel(int delta)
|
||||
|
||||
// Internal state
|
||||
property bool showPill: false
|
||||
property bool shouldAnimateHide: false
|
||||
|
||||
// Sizing logic for vertical bars
|
||||
readonly property int buttonSize: Style.getCapsuleHeightForScreen(screen?.name)
|
||||
readonly property real barFontSize: Style.getBarFontSizeForScreen(screen?.name)
|
||||
readonly property int pillHeight: buttonSize
|
||||
readonly property int pillOverlap: Math.round(buttonSize * 0.5)
|
||||
readonly property int maxPillWidth: rotateText ? Math.max(buttonSize, Math.round(textItem.implicitHeight + Style.margin2M)) : buttonSize
|
||||
readonly property int maxPillHeight: rotateText ? Math.max(1, Math.round(textItem.implicitWidth + Style.margin2M + Math.round(iconCircle.height / 4))) : Math.max(1, Math.round(textItem.implicitHeight + Style.margin2M))
|
||||
|
||||
// Determine pill direction based on section position
|
||||
readonly property bool openDownward: oppositeDirection
|
||||
readonly property bool openUpward: !oppositeDirection
|
||||
|
||||
// Effective shown state (true if animated open or forced, but not if force closed)
|
||||
readonly property bool revealed: !forceClose && (forceOpen || showPill)
|
||||
readonly property bool hasIcon: root.icon !== ""
|
||||
|
||||
// Always prioritize hover color, then the custom one and finally the fallback color
|
||||
readonly property color bgColor: hovered ? Color.mHover : (customBackgroundColor.a > 0) ? customBackgroundColor : Style.capsuleColor
|
||||
readonly property color fgColor: hovered ? Color.mOnHover : (customTextIconColor.a > 0) ? customTextIconColor : Color.mOnSurface
|
||||
readonly property color iconFgColor: hovered ? Color.mOnHover : (customIconColor.a > 0) ? customIconColor : (customTextIconColor.a > 0) ? customTextIconColor : Color.mOnSurface
|
||||
readonly property color textFgColor: hovered ? Color.mOnHover : (customTextColor.a > 0) ? customTextColor : (customTextIconColor.a > 0) ? customTextIconColor : Color.mOnSurface
|
||||
|
||||
readonly property real iconSize: Style.toOdd(pillHeight * 0.48)
|
||||
|
||||
// Content height calculation (for implicit sizing)
|
||||
readonly property real contentHeight: {
|
||||
if (collapseToIcon) {
|
||||
return hasIcon ? buttonSize : 0;
|
||||
}
|
||||
var overlap = hasIcon ? pillOverlap : 0;
|
||||
var baseHeight = hasIcon ? buttonSize : 0;
|
||||
return baseHeight + Math.max(0, pill.height - overlap);
|
||||
}
|
||||
|
||||
// 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() {
|
||||
if (hovered) {
|
||||
TooltipService.updateText(root.tooltipText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unified background for the entire pill area to avoid overlapping opacity
|
||||
Rectangle {
|
||||
id: pillBackground
|
||||
width: buttonSize
|
||||
height: root.contentHeight
|
||||
radius: Style.radiusM
|
||||
color: root.bgColor
|
||||
border.color: Style.capsuleBorderColor
|
||||
border.width: Style.capsuleBorderWidth
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Behavior on color {
|
||||
enabled: !Color.isTransitioning
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: pill
|
||||
|
||||
width: revealed ? maxPillWidth : 1
|
||||
height: revealed ? maxPillHeight : 1
|
||||
|
||||
// Position based on direction - center the pill relative to the icon
|
||||
x: 0
|
||||
y: {
|
||||
if (!hasIcon)
|
||||
return 0;
|
||||
return openUpward ? (iconCircle.y + iconCircle.height / 2 - height) : (iconCircle.y + iconCircle.height / 2);
|
||||
}
|
||||
|
||||
opacity: revealed ? Style.opacityFull : Style.opacityNone
|
||||
color: "transparent" // Make pill background transparent to avoid double opacity
|
||||
|
||||
// Radius logic for vertical expansion - rounded on the side that connects to icon
|
||||
topLeftRadius: openUpward ? Style.radiusM : 0
|
||||
bottomLeftRadius: openDownward ? Style.radiusM : 0
|
||||
topRightRadius: openUpward ? Style.radiusM : 0
|
||||
bottomRightRadius: openDownward ? Style.radiusM : 0
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
NText {
|
||||
id: textItem
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: hasIcon ? (openDownward ? Style.marginXXS : -Style.marginXXS) : 0
|
||||
rotation: rotateText ? -90 : 0
|
||||
text: root.text + root.suffix
|
||||
family: Settings.data.ui.fontFixed
|
||||
pointSize: root.barFontSize
|
||||
applyUiScale: false
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: root.textFgColor
|
||||
visible: revealed
|
||||
|
||||
function getVerticalCenterOffset() {
|
||||
// A small, symmetrical offset to push the text slightly away from the icon's edge.
|
||||
return openDownward ? Style.marginXS : -Style.marginXS;
|
||||
}
|
||||
}
|
||||
Behavior on width {
|
||||
enabled: showAnim.running || hideAnim.running
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
enabled: showAnim.running || hideAnim.running
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
enabled: showAnim.running || hideAnim.running
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: iconCircle
|
||||
width: buttonSize
|
||||
height: buttonSize
|
||||
radius: Math.min(Style.radiusL, width / 2)
|
||||
color: "transparent" // Make icon background transparent to avoid double opacity
|
||||
|
||||
// Icon positioning based on direction
|
||||
x: 0
|
||||
y: openUpward ? (root.contentHeight - height) : 0
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
NIcon {
|
||||
icon: root.icon
|
||||
pointSize: iconSize
|
||||
applyUiScale: false
|
||||
color: root.iconFgColor
|
||||
// Center horizontally
|
||||
x: (iconCircle.width - width) / 2
|
||||
// Center vertically accounting for font metrics
|
||||
y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: showAnim
|
||||
running: false
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "width"
|
||||
from: 1
|
||||
to: maxPillWidth
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "height"
|
||||
from: 1
|
||||
to: maxPillHeight
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
onStarted: {
|
||||
showPill = true;
|
||||
}
|
||||
onStopped: {
|
||||
delayedHideAnim.start();
|
||||
root.shown();
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: delayedHideAnim
|
||||
running: false
|
||||
PauseAnimation {
|
||||
duration: 2500
|
||||
}
|
||||
ScriptAction {
|
||||
script: if (shouldAnimateHide) {
|
||||
hideAnim.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: hideAnim
|
||||
running: false
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "width"
|
||||
from: maxPillWidth
|
||||
to: 1
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "height"
|
||||
from: maxPillHeight
|
||||
to: 1
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
onStopped: {
|
||||
showPill = false;
|
||||
shouldAnimateHide = false;
|
||||
root.hidden();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: showTimer
|
||||
interval: Style.pillDelay
|
||||
onTriggered: {
|
||||
if (!showPill) {
|
||||
showAnim.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
cursorShape: root.clicked ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onEntered: {
|
||||
hovered = true;
|
||||
root.entered();
|
||||
TooltipService.show(root, root.tooltipText, BarService.getTooltipDirection(root.screen?.name), (forceOpen || forceClose) ? Style.tooltipDelay : Style.tooltipDelayLong);
|
||||
if (forceClose) {
|
||||
return;
|
||||
}
|
||||
if (!forceOpen) {
|
||||
showDelayed();
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
hovered = false;
|
||||
root.exited();
|
||||
if (!forceOpen && !forceClose) {
|
||||
hide();
|
||||
}
|
||||
TooltipService.hide();
|
||||
}
|
||||
onClicked: mouse => {
|
||||
TooltipService.hide();
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
root.clicked();
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
root.rightClicked();
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
root.middleClicked();
|
||||
}
|
||||
}
|
||||
onWheel: wheel => root.wheel(wheel.angleDelta.y)
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (collapseToIcon || root.text.trim().length === 0)
|
||||
return;
|
||||
if (!showPill) {
|
||||
shouldAnimateHide = autoHide;
|
||||
showAnim.start();
|
||||
} else {
|
||||
hideAnim.stop();
|
||||
delayedHideAnim.restart();
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (collapseToIcon)
|
||||
return;
|
||||
if (forceOpen) {
|
||||
return;
|
||||
}
|
||||
if (showPill) {
|
||||
hideAnim.start();
|
||||
}
|
||||
showTimer.stop();
|
||||
}
|
||||
|
||||
function showDelayed() {
|
||||
if (collapseToIcon || root.text.trim().length === 0)
|
||||
return;
|
||||
if (!showPill) {
|
||||
shouldAnimateHide = autoHide;
|
||||
showTimer.start();
|
||||
} else {
|
||||
hideAnim.stop();
|
||||
delayedHideAnim.restart();
|
||||
}
|
||||
}
|
||||
|
||||
onForceOpenChanged: {
|
||||
if (forceOpen) {
|
||||
// Immediately lock open without animations
|
||||
showAnim.stop();
|
||||
hideAnim.stop();
|
||||
delayedHideAnim.stop();
|
||||
showPill = true;
|
||||
} else {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Noctalia
|
||||
import qs.Services.UI
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string widgetId
|
||||
required property var widgetScreen
|
||||
required property var widgetProps
|
||||
|
||||
// Extract section info from widgetProps
|
||||
readonly property string section: widgetProps ? (widgetProps.section || "") : ""
|
||||
readonly property int sectionIndex: widgetProps ? (widgetProps.sectionWidgetIndex || 0) : 0
|
||||
|
||||
// Store registration key at registration time so unregistration always uses the correct key,
|
||||
// even if binding properties (section, sectionIndex) have changed by destruction time
|
||||
property string _regScreen: ""
|
||||
property string _regSection: ""
|
||||
property string _regWidgetId: ""
|
||||
property int _regIndex: -1
|
||||
|
||||
function _unregister() {
|
||||
if (_regScreen !== "") {
|
||||
BarService.unregisterWidget(_regScreen, _regSection, _regWidgetId, _regIndex);
|
||||
_regScreen = "";
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
function getImplicitSize(item, prop) {
|
||||
return (item && item.visible) ? Math.round(item[prop]) : 0;
|
||||
}
|
||||
|
||||
// Only load if widget exists in registry
|
||||
function checkWidgetExists(): bool {
|
||||
return root.widgetId !== "" && BarWidgetRegistry.hasWidget(root.widgetId);
|
||||
}
|
||||
|
||||
// Force reload counter - incremented when plugin widget registry changes
|
||||
property int reloadCounter: 0
|
||||
|
||||
// Listen for plugin widget registry changes to force reload
|
||||
Connections {
|
||||
target: BarWidgetRegistry
|
||||
enabled: BarWidgetRegistry.isPluginWidget(root.widgetId)
|
||||
|
||||
function onPluginWidgetRegistryUpdated() {
|
||||
if (BarWidgetRegistry.hasWidget(root.widgetId)) {
|
||||
root.reloadCounter++;
|
||||
// Plugin widgets use setSource, so also trigger reload directly
|
||||
if (root._isPlugin && loader.active)
|
||||
root._loadWidget();
|
||||
Logger.d("BarWidgetLoader", "Plugin widget registry updated, reloading:", root.widgetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property bool _isPlugin: BarWidgetRegistry.isPluginWidget(widgetId)
|
||||
|
||||
// Build initial properties that must be available during Component.onCompleted.
|
||||
// This prevents registration-key mismatches in widgets that build IDs from
|
||||
// screen.name, section, or sectionWidgetIndex.
|
||||
// All standard bar widget props are passed for both core and plugin widgets.
|
||||
// Plugins that don't define some of these properties will get harmless warnings
|
||||
// but still load correctly — and plugins that DO define them (e.g. for unique
|
||||
// SpectrumService keys with multiple instances) get correct values from the start.
|
||||
function _initialProps() {
|
||||
return {
|
||||
"screen": widgetScreen,
|
||||
"widgetId": widgetProps.widgetId || "",
|
||||
"section": widgetProps.section || "",
|
||||
"sectionWidgetIndex": widgetProps.sectionWidgetIndex || 0,
|
||||
"sectionWidgetsCount": widgetProps.sectionWidgetsCount || 0
|
||||
};
|
||||
}
|
||||
|
||||
// Core widget URLs: file names match widget IDs exactly
|
||||
readonly property string _barWidgetsDir: Quickshell.shellDir + "/Modules/Bar/Widgets/"
|
||||
|
||||
function _loadWidget() {
|
||||
if (!BarWidgetRegistry.hasWidget(root.widgetId))
|
||||
return;
|
||||
|
||||
var props = _initialProps();
|
||||
|
||||
if (_isPlugin) {
|
||||
var comp = BarWidgetRegistry.getWidget(root.widgetId);
|
||||
if (!comp)
|
||||
return;
|
||||
var pluginId = root.widgetId.replace("plugin:", "");
|
||||
var api = PluginService.getPluginAPI(pluginId);
|
||||
if (api)
|
||||
props.pluginApi = api;
|
||||
loader.setSource(comp.url, props);
|
||||
} else {
|
||||
loader.setSource(_barWidgetsDir + root.widgetId + ".qml", props);
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
active: root.checkWidgetExists() && (root.reloadCounter >= 0)
|
||||
|
||||
// All widgets use setSource() so that screen and widget properties
|
||||
// are set as initial properties, available during Component.onCompleted.
|
||||
Component.onCompleted: root._loadWidget()
|
||||
|
||||
onActiveChanged: {
|
||||
if (active)
|
||||
root._loadWidget();
|
||||
}
|
||||
|
||||
// Unregister when the loaded item is destroyed (Loader deactivated or sourceComponent changed)
|
||||
onItemChanged: {
|
||||
if (!item) {
|
||||
root._unregister();
|
||||
}
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
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 remaining widget properties (screen is already set as initial prop)
|
||||
for (var prop in widgetProps) {
|
||||
if (item.hasOwnProperty(prop)) {
|
||||
item[prop] = widgetProps[prop];
|
||||
}
|
||||
}
|
||||
|
||||
// Unregister any previous registration before registering the new instance
|
||||
root._unregister();
|
||||
|
||||
// Register and store the key for reliable unregistration
|
||||
BarService.registerWidget(widgetScreen.name, section, widgetId, sectionIndex, item);
|
||||
root._regScreen = widgetScreen.name;
|
||||
root._regSection = section;
|
||||
root._regWidgetId = widgetId;
|
||||
root._regIndex = sectionIndex;
|
||||
|
||||
// Call custom onLoaded if it exists
|
||||
if (item.hasOwnProperty("onLoaded")) {
|
||||
item.onLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
root._unregister();
|
||||
}
|
||||
}
|
||||
|
||||
// Error handling
|
||||
Component.onCompleted: {
|
||||
if (!BarWidgetRegistry.hasWidget(widgetId)) {
|
||||
Logger.w("BarWidgetLoader", "Widget not found in registry:", widgetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,638 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
PopupWindow {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
property var trayItem: null
|
||||
property var anchorItem: null
|
||||
property real anchorX
|
||||
property real anchorY
|
||||
property bool isSubMenu: false
|
||||
property string widgetSection: ""
|
||||
property int widgetIndex: -1
|
||||
|
||||
// Derive menu from trayItem (only used for non-submenus)
|
||||
readonly property QsMenuHandle menu: isSubMenu ? null : (trayItem ? trayItem.menu : null)
|
||||
|
||||
// Compute if current tray item is pinned
|
||||
readonly property bool isPinned: {
|
||||
if (!trayItem || widgetSection === "" || widgetIndex < 0)
|
||||
return false;
|
||||
var widgets = Settings.getBarWidgetsForScreen(root.screen?.name)[widgetSection];
|
||||
if (!widgets || widgetIndex >= widgets.length)
|
||||
return false;
|
||||
var widgetSettings = widgets[widgetIndex];
|
||||
if (!widgetSettings || widgetSettings.id !== "Tray")
|
||||
return false;
|
||||
var pinnedList = widgetSettings.pinned || [];
|
||||
const itemName = trayItem.tooltipTitle || trayItem.name || trayItem.id || "";
|
||||
for (var i = 0; i < pinnedList.length; i++) {
|
||||
if (pinnedList[i] === itemName)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
readonly property int menuWidth: 220
|
||||
|
||||
implicitWidth: menuWidth
|
||||
|
||||
// Use the content height of the Flickable for implicit height
|
||||
implicitHeight: Math.min(screen?.height * 0.9, flickable.contentHeight + Style.margin2S)
|
||||
|
||||
// When implicitHeight changes (menu content loads), force anchor recalculation
|
||||
onImplicitHeightChanged: {
|
||||
if (visible && anchorItem) {
|
||||
Qt.callLater(() => {
|
||||
anchor.updateAnchor();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
visible: false
|
||||
color: "transparent"
|
||||
anchor.item: anchorItem
|
||||
anchor.rect.x: {
|
||||
if (anchorItem && screen) {
|
||||
let baseX = anchorX;
|
||||
|
||||
// Calculate position relative to current screen
|
||||
let menuScreenX;
|
||||
if (isSubMenu && anchorItem.Window && anchorItem.Window.window) {
|
||||
const posInPopup = anchorItem.mapToItem(null, 0, 0);
|
||||
const parentWindow = anchorItem.Window.window;
|
||||
const windowXOnScreen = parentWindow.x - screen.x;
|
||||
menuScreenX = windowXOnScreen + posInPopup.x + baseX;
|
||||
} else {
|
||||
const anchorGlobalPos = anchorItem.mapToItem(null, 0, 0);
|
||||
const anchorScreenX = anchorGlobalPos.x;
|
||||
menuScreenX = anchorScreenX + baseX;
|
||||
}
|
||||
|
||||
const menuRight = menuScreenX + implicitWidth;
|
||||
const screenRight = screen.width;
|
||||
const menuLeft = menuScreenX;
|
||||
|
||||
// Only adjust if menu would clip off screen boundaries
|
||||
// Don't adjust if the positioning is intentional (e.g., negative offset for right bar)
|
||||
if (menuRight > screenRight && menuLeft < screenRight) {
|
||||
// Clipping on right edge - shift left
|
||||
const overflow = menuRight - screenRight;
|
||||
return baseX - overflow - Style.marginS;
|
||||
} else if (menuLeft < 0 && menuRight > 0) {
|
||||
// Clipping on left edge - shift right
|
||||
return baseX - menuLeft + Style.marginS;
|
||||
}
|
||||
|
||||
return baseX;
|
||||
}
|
||||
return anchorX;
|
||||
}
|
||||
anchor.rect.y: {
|
||||
if (anchorItem && screen) {
|
||||
const barPosition = Settings.getBarPositionForScreen(root.screen?.name);
|
||||
|
||||
let baseY = anchorY;
|
||||
|
||||
// Only apply bottom bar special positioning if:
|
||||
// 1. Not a submenu
|
||||
// 2. Bar is at bottom
|
||||
// 3. anchorY is not already negative (if negative, it's pre-calculated from drawer)
|
||||
const shouldApplyBottomBarLogic = !isSubMenu && barPosition === "bottom" && anchorY >= 0;
|
||||
|
||||
if (shouldApplyBottomBarLogic) {
|
||||
// For bottom bar from the bar itself, position menu above the anchor with margin
|
||||
baseY = -(implicitHeight + Style.marginS);
|
||||
} else if (barPosition === "top" && !isSubMenu && anchorY >= 0) {
|
||||
// For top bar: position menu below bar with margin
|
||||
const barHeight = Style.getBarHeightForScreen(root.screen?.name);
|
||||
baseY = barHeight + Style.marginS;
|
||||
}
|
||||
|
||||
// Use a robust way to get screen coordinates
|
||||
const posInWindow = anchorItem.mapToItem(null, 0, 0);
|
||||
const parentWindow = anchorItem.Window.window;
|
||||
|
||||
// Calculate screen-relative Y of the window
|
||||
let windowYOnScreen = (parentWindow && screen) ? (parentWindow.y - screen.y) : 0;
|
||||
|
||||
// If window reported 0 but bar is at bottom, assume it's at screen bottom
|
||||
if (windowYOnScreen === 0 && barPosition === "bottom" && screen) {
|
||||
windowYOnScreen = screen.height - (parentWindow ? parentWindow.height : Style.getBarHeightForScreen(screen.name));
|
||||
}
|
||||
|
||||
// Calculate the screen Y of the menu top
|
||||
// Use a small guess for height if implicitHeight is 0 to avoid covering the bar on the first frame
|
||||
const effectiveHeight = implicitHeight > 0 ? implicitHeight : 200;
|
||||
const effectiveBaseY = shouldApplyBottomBarLogic ? -(effectiveHeight + Style.marginS) : baseY;
|
||||
|
||||
const menuScreenY = windowYOnScreen + posInWindow.y + effectiveBaseY;
|
||||
const menuBottom = menuScreenY + (implicitHeight > 0 ? implicitHeight : effectiveHeight);
|
||||
const screenHeight = screen ? screen.height : 1080;
|
||||
|
||||
// Adjust the final baseY (the actual value returned to anchor.rect.y)
|
||||
let finalBaseY = shouldApplyBottomBarLogic ? -(implicitHeight + Style.marginS) : baseY;
|
||||
|
||||
// Adjust if menu would clip off the bottom
|
||||
if (menuBottom > screenHeight) {
|
||||
const overflow = menuBottom - screenHeight;
|
||||
finalBaseY -= (overflow + Style.marginS);
|
||||
}
|
||||
|
||||
// Adjust if menu would clip off the top
|
||||
// menuScreenY < 0 means it's above the screen edge
|
||||
if (menuScreenY < 0) {
|
||||
finalBaseY -= (menuScreenY - Style.marginS);
|
||||
}
|
||||
|
||||
return finalBaseY;
|
||||
}
|
||||
|
||||
// Fallback if no anchor/screen
|
||||
if (isSubMenu) {
|
||||
return anchorY;
|
||||
}
|
||||
return anchorY + (Settings.getBarPositionForScreen(root.screen?.name) === "bottom" ? -implicitHeight : Style.getBarHeightForScreen(root.screen?.name));
|
||||
}
|
||||
|
||||
function showAt(item, x, y) {
|
||||
if (!item) {
|
||||
Logger.w("TrayMenu", "anchorItem is undefined, won't show menu.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!opener.children || opener.children.values.length === 0) {
|
||||
//Logger.w("TrayMenu", "Menu not ready, delaying show")
|
||||
Qt.callLater(() => showAt(item, x, y));
|
||||
return;
|
||||
}
|
||||
|
||||
anchorItem = item;
|
||||
anchorX = x;
|
||||
anchorY = y;
|
||||
|
||||
visible = true;
|
||||
forceActiveFocus();
|
||||
|
||||
// Force update after showing.
|
||||
Qt.callLater(() => {
|
||||
root.anchor.updateAnchor();
|
||||
});
|
||||
}
|
||||
|
||||
function hideMenu() {
|
||||
visible = false;
|
||||
|
||||
// Clean up all submenus recursively
|
||||
for (var i = 0; i < columnLayout.children.length; i++) {
|
||||
const child = columnLayout.children[i];
|
||||
if (child?.subMenu) {
|
||||
child.subMenu.hideMenu();
|
||||
child.subMenu.destroy();
|
||||
child.subMenu = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
Keys.onEscapePressed: root.hideMenu()
|
||||
}
|
||||
|
||||
QsMenuOpener {
|
||||
id: opener
|
||||
menu: root.menu
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Color.mSurface
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS)
|
||||
radius: Style.radiusM
|
||||
|
||||
// Fade-in animation
|
||||
opacity: root.visible ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: flickable
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS
|
||||
contentHeight: columnLayout.implicitHeight
|
||||
interactive: true
|
||||
|
||||
// Fade-in animation
|
||||
opacity: root.visible ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
|
||||
// Use a ColumnLayout to handle menu item arrangement
|
||||
ColumnLayout {
|
||||
id: columnLayout
|
||||
width: flickable.width
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: opener.children ? [...opener.children.values] : []
|
||||
|
||||
delegate: Rectangle {
|
||||
id: entry
|
||||
required property var modelData
|
||||
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: {
|
||||
if (modelData?.isSeparator) {
|
||||
return 8;
|
||||
} else {
|
||||
// Calculate based on text content
|
||||
const textHeight = text.contentHeight || (Style.fontSizeS * 1.2);
|
||||
return Math.max(28, textHeight + Style.margin2S);
|
||||
}
|
||||
}
|
||||
|
||||
color: "transparent"
|
||||
property var subMenu: null
|
||||
|
||||
NDivider {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Style.margin2M
|
||||
visible: modelData?.isSeparator ?? false
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: innerRect
|
||||
anchors.fill: parent
|
||||
color: mouseArea.containsMouse ? Color.mHover : "transparent"
|
||||
radius: Style.radiusS
|
||||
visible: !(modelData?.isSeparator ?? false)
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.marginM
|
||||
anchors.rightMargin: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
// Indicator Container
|
||||
Item {
|
||||
visible: (modelData?.buttonType ?? QsMenuButtonType.None) !== QsMenuButtonType.None
|
||||
|
||||
implicitWidth: Math.round(Style.baseWidgetSize * 0.5)
|
||||
implicitHeight: Math.round(Style.baseWidgetSize * 0.5)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
// Helper properties
|
||||
readonly property int type: modelData?.buttonType ?? QsMenuButtonType.None
|
||||
readonly property bool isRadio: type === QsMenuButtonType.RadioButton
|
||||
readonly property bool isChecked: modelData?.checkState === Qt.Checked || (modelData?.checked ?? false)
|
||||
|
||||
// Color Logic
|
||||
readonly property color activeColor: mouseArea.containsMouse ? Color.mOnHover : Color.mPrimary
|
||||
readonly property color checkMarkColor: mouseArea.containsMouse ? Color.mHover : Color.mOnPrimary
|
||||
readonly property color borderColor: isChecked ? activeColor : (mouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface)
|
||||
|
||||
// Checkbox Visuals
|
||||
Rectangle {
|
||||
visible: !parent.isRadio
|
||||
anchors.centerIn: parent
|
||||
width: Math.round(Style.baseWidgetSize * 0.5)
|
||||
height: Math.round(Style.baseWidgetSize * 0.5)
|
||||
radius: Style.iRadiusXS
|
||||
color: "transparent" // Transparent to match RadioButton style
|
||||
border.color: parent.borderColor
|
||||
border.width: Style.borderM
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
NIcon {
|
||||
visible: parent.parent.isChecked
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: -1
|
||||
icon: "check"
|
||||
color: parent.parent.activeColor
|
||||
pointSize: Math.max(Style.fontSizeXXS, parent.width * 0.6)
|
||||
}
|
||||
}
|
||||
|
||||
// RadioButton Visuals
|
||||
Rectangle {
|
||||
visible: parent.isRadio
|
||||
anchors.centerIn: parent
|
||||
width: Style.toOdd(Style.baseWidgetSize * 0.5)
|
||||
height: Style.toOdd(Style.baseWidgetSize * 0.5)
|
||||
radius: width / 2
|
||||
color: "transparent"
|
||||
border.color: parent.borderColor
|
||||
border.width: Style.borderM // Slightly thicker for radio look
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: parent.parent.isChecked
|
||||
anchors.centerIn: parent
|
||||
width: Style.toOdd(parent.width * 0.5)
|
||||
height: Style.toOdd(parent.height * 0.5)
|
||||
radius: width / 2
|
||||
color: parent.parent.activeColor
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
id: text
|
||||
Layout.fillWidth: true
|
||||
color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface) : Color.mOnSurfaceVariant
|
||||
text: modelData?.text !== "" ? modelData?.text.replace(/[\n\r]+/g, ' ') : "..."
|
||||
pointSize: Style.fontSizeS
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Image {
|
||||
Layout.preferredWidth: Style.marginL
|
||||
Layout.preferredHeight: Style.marginL
|
||||
source: modelData?.icon ?? ""
|
||||
visible: (modelData?.icon ?? "") !== ""
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: modelData?.hasChildren ? "menu" : ""
|
||||
pointSize: Style.fontSizeS
|
||||
applyUiScale: false
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: modelData?.hasChildren ?? false
|
||||
color: (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface)
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && root.visible
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
|
||||
onClicked: mouse => {
|
||||
if (modelData && !modelData.isSeparator) {
|
||||
if (modelData.hasChildren) {
|
||||
// Click on items with children toggles submenu
|
||||
if (entry.subMenu) {
|
||||
// Close existing submenu
|
||||
entry.subMenu.hideMenu();
|
||||
entry.subMenu.destroy();
|
||||
entry.subMenu = null;
|
||||
} else {
|
||||
// Close any other open submenus first
|
||||
for (var i = 0; i < columnLayout.children.length; i++) {
|
||||
const sibling = columnLayout.children[i];
|
||||
if (sibling !== entry && sibling.subMenu) {
|
||||
sibling.subMenu.hideMenu();
|
||||
sibling.subMenu.destroy();
|
||||
sibling.subMenu = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine submenu opening direction
|
||||
let openLeft = false;
|
||||
const barPosition = Settings.getBarPositionForScreen(root.screen?.name);
|
||||
const globalPos = entry.mapToItem(null, 0, 0);
|
||||
|
||||
if (barPosition === "right") {
|
||||
openLeft = true;
|
||||
} else if (barPosition === "left") {
|
||||
openLeft = false;
|
||||
} else {
|
||||
openLeft = (root.widgetSection === "right");
|
||||
}
|
||||
|
||||
// Open new submenu
|
||||
entry.subMenu = Qt.createComponent("TrayMenu.qml").createObject(root, {
|
||||
"menu": modelData,
|
||||
"isSubMenu": true,
|
||||
"screen": root.screen
|
||||
});
|
||||
|
||||
if (entry.subMenu) {
|
||||
const overlap = 60;
|
||||
entry.subMenu.anchorItem = entry;
|
||||
entry.subMenu.anchorX = openLeft ? -overlap : overlap;
|
||||
entry.subMenu.anchorY = 0;
|
||||
entry.subMenu.visible = true;
|
||||
// Force anchor update with new position
|
||||
Qt.callLater(() => {
|
||||
entry.subMenu.anchor.updateAnchor();
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Click on regular items triggers them
|
||||
modelData.triggered();
|
||||
root.hideMenu();
|
||||
|
||||
// Close the drawer if it's open
|
||||
if (root.screen) {
|
||||
const panel = PanelService.getPanel("trayDrawerPanel", root.screen);
|
||||
if (panel && panel.visible) {
|
||||
panel.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
if (subMenu) {
|
||||
subMenu.destroy();
|
||||
subMenu = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PIN / UNPIN
|
||||
Rectangle {
|
||||
visible: {
|
||||
if (widgetSection === "" || widgetIndex < 0)
|
||||
return false;
|
||||
var widgets = Settings.getBarWidgetsForScreen(root.screen?.name)[widgetSection];
|
||||
if (!widgets || widgetIndex >= widgets.length)
|
||||
return false;
|
||||
var widgetSettings = widgets[widgetIndex];
|
||||
if (!widgetSettings)
|
||||
return false;
|
||||
return widgetSettings.drawerEnabled ?? false;
|
||||
}
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: 28
|
||||
color: pinUnpinMouseArea.containsMouse ? Qt.alpha(Color.mPrimary, 0.2) : Qt.alpha(Color.mPrimary, 0.08)
|
||||
radius: Style.radiusS
|
||||
border.color: Qt.alpha(Color.mPrimary, pinUnpinMouseArea.containsMouse ? 0.4 : 0.2)
|
||||
border.width: Style.borderS
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.marginM
|
||||
anchors.rightMargin: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
NIcon {
|
||||
icon: root.isPinned ? "unpin" : "pin"
|
||||
pointSize: Style.fontSizeS
|
||||
applyUiScale: false
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NText {
|
||||
Layout.fillWidth: true
|
||||
color: Color.mPrimary
|
||||
text: root.isPinned ? I18n.tr("panels.bar.tray-unpin-application") : I18n.tr("panels.bar.tray-pin-application")
|
||||
pointSize: Style.fontSizeS
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: pinUnpinMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onClicked: {
|
||||
if (root.isPinned) {
|
||||
root.removeFromPinned();
|
||||
} else {
|
||||
root.addToPinned();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addToPinned() {
|
||||
if (!trayItem || widgetSection === "" || widgetIndex < 0) {
|
||||
Logger.w("TrayMenu", "Cannot pin: missing tray item or widget info");
|
||||
return;
|
||||
}
|
||||
const itemName = trayItem.tooltipTitle || trayItem.name || trayItem.id || "";
|
||||
if (!itemName) {
|
||||
Logger.w("TrayMenu", "Cannot pin: tray item has no name");
|
||||
return;
|
||||
}
|
||||
var screenName = root.screen?.name || "";
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[widgetSection];
|
||||
if (!widgets || widgetIndex >= widgets.length) {
|
||||
Logger.w("TrayMenu", "Cannot pin: invalid widget index");
|
||||
return;
|
||||
}
|
||||
var widgetSettings = widgets[widgetIndex];
|
||||
if (!widgetSettings || widgetSettings.id !== "Tray") {
|
||||
Logger.w("TrayMenu", "Cannot pin: widget is not a Tray widget");
|
||||
return;
|
||||
}
|
||||
var pinnedList = widgetSettings.pinned || [];
|
||||
var newPinned = pinnedList.slice();
|
||||
newPinned.push(itemName);
|
||||
var newSettings = Object.assign({}, widgetSettings);
|
||||
newSettings.pinned = newPinned;
|
||||
widgets[widgetIndex] = newSettings;
|
||||
|
||||
// Write to the correct location: screen override or global
|
||||
if (Settings.hasScreenOverride(screenName, "widgets")) {
|
||||
var overrideWidgets = Settings.getBarWidgetsForScreen(screenName);
|
||||
overrideWidgets[widgetSection] = widgets;
|
||||
Settings.setScreenOverride(screenName, "widgets", overrideWidgets);
|
||||
} else {
|
||||
Settings.data.bar.widgets[widgetSection] = widgets;
|
||||
}
|
||||
Settings.saveImmediate();
|
||||
|
||||
// Close drawer when pinning (drawer needs to resize)
|
||||
if (screen) {
|
||||
const panel = PanelService.getPanel("trayDrawerPanel", screen);
|
||||
if (panel)
|
||||
panel.close();
|
||||
}
|
||||
}
|
||||
|
||||
function removeFromPinned() {
|
||||
if (!trayItem || widgetSection === "" || widgetIndex < 0) {
|
||||
Logger.w("TrayMenu", "Cannot unpin: missing tray item or widget info");
|
||||
return;
|
||||
}
|
||||
const itemName = trayItem.tooltipTitle || trayItem.name || trayItem.id || "";
|
||||
if (!itemName) {
|
||||
Logger.w("TrayMenu", "Cannot unpin: tray item has no name");
|
||||
return;
|
||||
}
|
||||
var screenName = root.screen?.name || "";
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[widgetSection];
|
||||
if (!widgets || widgetIndex >= widgets.length) {
|
||||
Logger.w("TrayMenu", "Cannot unpin: invalid widget index");
|
||||
return;
|
||||
}
|
||||
var widgetSettings = widgets[widgetIndex];
|
||||
if (!widgetSettings || widgetSettings.id !== "Tray") {
|
||||
Logger.w("TrayMenu", "Cannot unpin: widget is not a Tray widget");
|
||||
return;
|
||||
}
|
||||
var pinnedList = widgetSettings.pinned || [];
|
||||
var newPinned = [];
|
||||
for (var i = 0; i < pinnedList.length; i++) {
|
||||
if (pinnedList[i] !== itemName) {
|
||||
newPinned.push(pinnedList[i]);
|
||||
}
|
||||
}
|
||||
var newSettings = Object.assign({}, widgetSettings);
|
||||
newSettings.pinned = newPinned;
|
||||
widgets[widgetIndex] = newSettings;
|
||||
|
||||
// Write to the correct location: screen override or global
|
||||
if (Settings.hasScreenOverride(screenName, "widgets")) {
|
||||
var overrideWidgets = Settings.getBarWidgetsForScreen(screenName);
|
||||
overrideWidgets[widgetSection] = widgets;
|
||||
Settings.setScreenOverride(screenName, "widgets", overrideWidgets);
|
||||
} else {
|
||||
Settings.data.bar.widgets[widgetSection] = widgets;
|
||||
}
|
||||
Settings.saveImmediate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import QtQuick
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: pillContainer
|
||||
|
||||
required property var workspace
|
||||
required property bool isVertical
|
||||
|
||||
// These must be provided by the parent Workspace widget
|
||||
required property real baseDimensionRatio
|
||||
required property real capsuleHeight
|
||||
required property real barHeight
|
||||
required property string labelMode
|
||||
required property int fontWeight
|
||||
required property int characterCount
|
||||
required property real textRatio
|
||||
required property bool showLabelsOnlyWhenOccupied
|
||||
required property string focusedColor
|
||||
required property string occupiedColor
|
||||
required property string emptyColor
|
||||
required property real masterProgress
|
||||
required property bool effectsActive
|
||||
required property color effectColor
|
||||
required property var getWorkspaceWidth
|
||||
required property var getWorkspaceHeight
|
||||
|
||||
// Fixed dimension (cross-axis) for visual pill
|
||||
readonly property real fixedDimension: Style.toOdd(capsuleHeight * baseDimensionRatio)
|
||||
|
||||
// Animated pill dimensions (for visual pill, not container)
|
||||
property real pillWidth: isVertical ? fixedDimension : getWorkspaceWidth(workspace, false)
|
||||
property real pillHeight: isVertical ? getWorkspaceHeight(workspace, false) : fixedDimension
|
||||
|
||||
// Container uses full barHeight on cross-axis for larger click area
|
||||
width: isVertical ? barHeight : getWorkspaceWidth(workspace, false)
|
||||
height: isVertical ? getWorkspaceHeight(workspace, false) : barHeight
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "active"
|
||||
when: workspace.isActive
|
||||
PropertyChanges {
|
||||
target: pillContainer
|
||||
width: isVertical ? barHeight : getWorkspaceWidth(workspace, true)
|
||||
height: isVertical ? getWorkspaceHeight(workspace, true) : barHeight
|
||||
pillWidth: isVertical ? fixedDimension : getWorkspaceWidth(workspace, true)
|
||||
pillHeight: isVertical ? getWorkspaceHeight(workspace, true) : fixedDimension
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
from: "inactive"
|
||||
to: "active"
|
||||
NumberAnimation {
|
||||
properties: isVertical ? "height,pillHeight" : "width,pillWidth"
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: "active"
|
||||
to: "inactive"
|
||||
NumberAnimation {
|
||||
properties: isVertical ? "height,pillHeight" : "width,pillWidth"
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Rectangle {
|
||||
id: pill
|
||||
width: pillContainer.pillWidth
|
||||
height: pillContainer.pillHeight
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, height)
|
||||
radius: Style.radiusM
|
||||
z: 0
|
||||
|
||||
color: {
|
||||
if (pillMouseArea.containsMouse)
|
||||
return Color.mHover;
|
||||
if (workspace.isFocused)
|
||||
return Color.resolveColorKey(focusedColor);
|
||||
if (workspace.isUrgent)
|
||||
return Color.mError;
|
||||
if (workspace.isOccupied)
|
||||
return Color.resolveColorKey(occupiedColor);
|
||||
return Qt.alpha(Color.resolveColorKey(emptyColor), 0.3);
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: (labelMode !== "none") && (!showLabelsOnlyWhenOccupied || workspace.isOccupied || workspace.isFocused)
|
||||
anchors.fill: parent
|
||||
sourceComponent: Component {
|
||||
NText {
|
||||
text: {
|
||||
if (workspace.name && workspace.name.length > 0) {
|
||||
if (labelMode === "name") {
|
||||
return workspace.name.substring(0, characterCount);
|
||||
}
|
||||
if (labelMode === "index+name") {
|
||||
// Vertical mode: compact format (no space, first char only)
|
||||
// Horizontal mode: full format (space, more chars)
|
||||
if (isVertical) {
|
||||
return workspace.idx.toString() + workspace.name.substring(0, 1);
|
||||
}
|
||||
return workspace.idx.toString() + " " + workspace.name.substring(0, characterCount);
|
||||
}
|
||||
}
|
||||
return workspace.idx.toString();
|
||||
}
|
||||
family: Settings.data.ui.fontFixed
|
||||
// Size based on the fixed dimension (cross-axis) of the visual pill
|
||||
pointSize: (isVertical ? pillContainer.pillWidth : pillContainer.pillHeight) * textRatio
|
||||
applyUiScale: false
|
||||
font.capitalization: Font.AllUppercase
|
||||
font.weight: fontWeight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.Wrap
|
||||
color: {
|
||||
if (pillMouseArea.containsMouse)
|
||||
return Color.mOnHover;
|
||||
if (workspace.isFocused)
|
||||
return Color.resolveOnColorKey(focusedColor);
|
||||
if (workspace.isUrgent)
|
||||
return Color.mOnError;
|
||||
if (workspace.isOccupied)
|
||||
return Color.resolveOnColorKey(occupiedColor);
|
||||
return Color.resolveOnColorKey(emptyColor);
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
enabled: !Color.isTransitioning
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Material 3-inspired smooth animations
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
Behavior on color {
|
||||
enabled: !Color.isTransitioning
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.InOutCubic
|
||||
}
|
||||
}
|
||||
Behavior on radius {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
Behavior on pillWidth {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
Behavior on pillHeight {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
|
||||
// Full-height click area
|
||||
MouseArea {
|
||||
id: pillMouseArea
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
CompositorService.switchToWorkspace(workspace);
|
||||
}
|
||||
}
|
||||
|
||||
// Burst effect overlay for focused pill
|
||||
Rectangle {
|
||||
id: pillBurst
|
||||
anchors.centerIn: pill
|
||||
width: pillContainer.pillWidth + 18 * masterProgress * scale
|
||||
height: pillContainer.pillHeight + 18 * masterProgress * scale
|
||||
radius: width / 2
|
||||
color: "transparent"
|
||||
border.color: effectColor
|
||||
border.width: Math.max(1, Math.round((2 + 6 * (1.0 - masterProgress))))
|
||||
opacity: effectsActive && workspace.isFocused ? (1.0 - masterProgress) * 0.7 : 0
|
||||
visible: effectsActive && workspace.isFocused
|
||||
z: 1
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user