mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2025-12-06 06:36:15 +00:00
342 lines
8.6 KiB
QML
342 lines
8.6 KiB
QML
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 string tooltipText: ""
|
|
property string density: ""
|
|
property bool autoHide: false
|
|
property bool forceOpen: false
|
|
property bool forceClose: false
|
|
property bool oppositeDirection: false
|
|
property bool hovered: false
|
|
property color customBackgroundColor: Color.transparent
|
|
property color customTextIconColor: Color.transparent
|
|
|
|
readonly property bool collapseToIcon: forceClose && !forceOpen
|
|
|
|
// Effective shown state (true if hovered/animated open or forced)
|
|
readonly property bool revealed: !forceClose && (forceOpen || showPill)
|
|
|
|
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.capsuleHeight
|
|
readonly property int pillPaddingHorizontal: Math.round(Style.capsuleHeight * 0.2)
|
|
readonly property int pillOverlap: Math.round(Style.capsuleHeight * 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 real iconSize: {
|
|
switch (root.density) {
|
|
case "compact":
|
|
return Math.max(1, Math.round(pillHeight * 0.65));
|
|
default:
|
|
return Math.max(1, Math.round(pillHeight * 0.48));
|
|
}
|
|
}
|
|
|
|
readonly property real textSize: {
|
|
switch (root.density) {
|
|
case "compact":
|
|
return Math.max(1, Math.round(pillHeight * 0.45));
|
|
default:
|
|
return Math.max(1, Math.round(pillHeight * 0.33));
|
|
}
|
|
}
|
|
|
|
width: collapseToIcon ? pillHeight : pillHeight + Math.max(0, pill.width - pillOverlap)
|
|
height: 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: halfPillHeight
|
|
color: root.bgColor
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
readonly property int halfPillHeight: Math.round(pillHeight * 0.5)
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: Style.animationFast
|
|
easing.type: Easing.InOutQuad
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: pill
|
|
|
|
width: revealed ? pillMaxWidth : 1
|
|
height: pillHeight
|
|
|
|
x: oppositeDirection ? (iconCircle.x + iconCircle.width / 2) : // Opens right
|
|
(iconCircle.x + iconCircle.width / 2) - width // Opens left
|
|
|
|
opacity: revealed ? Style.opacityFull : Style.opacityNone
|
|
color: Color.transparent // Make pill background transparent to avoid double opacity
|
|
|
|
readonly property int halfPillHeight: Math.round(pillHeight * 0.5)
|
|
|
|
topLeftRadius: oppositeDirection ? 0 : halfPillHeight
|
|
bottomLeftRadius: oppositeDirection ? 0 : halfPillHeight
|
|
topRightRadius: oppositeDirection ? halfPillHeight : 0
|
|
bottomRightRadius: oppositeDirection ? halfPillHeight : 0
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
NText {
|
|
id: textItem
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
x: {
|
|
// 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: textSize
|
|
applyUiScale: false
|
|
font.weight: Style.fontWeightBold
|
|
color: root.fgColor
|
|
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: pillHeight
|
|
height: pillHeight
|
|
radius: width * 0.5
|
|
color: 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.fgColor
|
|
// 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
|
|
onEntered: {
|
|
hovered = true;
|
|
root.entered();
|
|
TooltipService.show(pill, root.tooltipText, BarService.getTooltipDirection(), (forceOpen || forceClose) ? Style.tooltipDelay : Style.tooltipDelayLong);
|
|
if (forceClose) {
|
|
return;
|
|
}
|
|
if (!forceOpen) {
|
|
showDelayed();
|
|
}
|
|
}
|
|
onExited: {
|
|
hovered = false;
|
|
root.exited();
|
|
if (!forceOpen && !forceClose) {
|
|
hide();
|
|
}
|
|
TooltipService.hide();
|
|
}
|
|
onClicked: function (mouse) {
|
|
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)
|
|
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)
|
|
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();
|
|
}
|
|
}
|
|
}
|