NPill: act as loder for NVerticalPill and NHorizontalPill
NHorizontalPill: should be used for anything that expands horizontal NVerticalPill: should be used for anything that expands vertical
This commit is contained in:
parent
9dfac69e9e
commit
0035fbcc4e
5 changed files with 731 additions and 243 deletions
|
|
@ -61,15 +61,6 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: externalHideTimer
|
|
||||||
running: false
|
|
||||||
interval: 1500
|
|
||||||
onTriggered: {
|
|
||||||
pill.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NPill {
|
NPill {
|
||||||
id: pill
|
id: pill
|
||||||
|
|
||||||
|
|
|
||||||
324
Widgets/NHorizontalPill.qml
Normal file
324
Widgets/NHorizontalPill.qml
Normal file
|
|
@ -0,0 +1,324 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string icon: ""
|
||||||
|
property string text: ""
|
||||||
|
property string tooltipText: ""
|
||||||
|
property real sizeRatio: 0.8
|
||||||
|
property bool autoHide: false
|
||||||
|
property bool forceOpen: false
|
||||||
|
property bool disableOpen: false
|
||||||
|
property bool rightOpen: false
|
||||||
|
property bool hovered: false
|
||||||
|
property real fontSize: Style.fontSizeXS
|
||||||
|
|
||||||
|
// Bar position detection for pill direction
|
||||||
|
readonly property string barPosition: Settings.data.bar.position
|
||||||
|
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
|
||||||
|
|
||||||
|
// Determine pill direction based on section position
|
||||||
|
readonly property bool openRightward: rightOpen
|
||||||
|
readonly property bool openLeftward: !rightOpen
|
||||||
|
|
||||||
|
// Effective shown state (true if animated open or forced)
|
||||||
|
readonly property bool revealed: 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
|
||||||
|
|
||||||
|
// Sizing logic for horizontal bars
|
||||||
|
readonly property int iconSize: Math.round(Style.baseWidgetSize * sizeRatio * scaling)
|
||||||
|
readonly property int pillWidth: iconSize
|
||||||
|
readonly property int pillPaddingHorizontal: Style.marginS * scaling
|
||||||
|
readonly property int pillPaddingVertical: Style.marginS * scaling
|
||||||
|
readonly property int pillOverlap: iconSize * 0.5
|
||||||
|
readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 4)
|
||||||
|
readonly property int maxPillHeight: iconSize
|
||||||
|
|
||||||
|
// For horizontal bars: height is just icon size, width includes pill space
|
||||||
|
width: revealed ? (iconSize + maxPillWidth - pillOverlap) : iconSize
|
||||||
|
height: iconSize
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: pill
|
||||||
|
width: revealed ? maxPillWidth : 1
|
||||||
|
height: revealed ? maxPillHeight : 1
|
||||||
|
|
||||||
|
// Position based on direction - center the pill relative to the icon
|
||||||
|
x: openLeftward ? (iconCircle.x + iconCircle.width / 2 - width) : (iconCircle.x + iconCircle.width / 2)
|
||||||
|
y: 0
|
||||||
|
|
||||||
|
opacity: revealed ? Style.opacityFull : Style.opacityNone
|
||||||
|
color: Color.mSurfaceVariant
|
||||||
|
border.color: Color.mOutline
|
||||||
|
border.width: Math.max(1, Style.borderS * scaling)
|
||||||
|
|
||||||
|
// Radius logic for horizontal expansion - rounded on the side that connects to icon
|
||||||
|
topLeftRadius: openLeftward ? iconSize * 0.5 : 0
|
||||||
|
bottomLeftRadius: openLeftward ? iconSize * 0.5 : 0
|
||||||
|
topRightRadius: openRightward ? iconSize * 0.5 : 0
|
||||||
|
bottomRightRadius: openRightward ? iconSize * 0.5 : 0
|
||||||
|
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
NText {
|
||||||
|
id: textItem
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: root.text
|
||||||
|
font.pointSize: Style.fontSizeXXS * scaling
|
||||||
|
font.weight: Style.fontWeightBold
|
||||||
|
color: Color.mOnSurface
|
||||||
|
visible: revealed
|
||||||
|
}
|
||||||
|
|
||||||
|
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.animationNormal
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: iconCircle
|
||||||
|
width: iconSize
|
||||||
|
height: iconSize
|
||||||
|
radius: width * 0.5
|
||||||
|
color: hovered && !forceOpen ? Color.mTertiary : Color.mSurfaceVariant
|
||||||
|
|
||||||
|
// Icon positioning based on direction
|
||||||
|
x: openLeftward ? (parent.width - width) : 0
|
||||||
|
y: 0
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NIcon {
|
||||||
|
icon: root.icon
|
||||||
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
|
color: hovered && !forceOpen ? Color.mOnTertiary : Color.mOnSurfaceVariant
|
||||||
|
// 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.animationNormal
|
||||||
|
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.animationNormal
|
||||||
|
easing.type: Easing.InCubic
|
||||||
|
}
|
||||||
|
onStopped: {
|
||||||
|
showPill = false
|
||||||
|
shouldAnimateHide = false
|
||||||
|
root.hidden()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NTooltip {
|
||||||
|
id: tooltip
|
||||||
|
target: pill
|
||||||
|
text: root.tooltipText
|
||||||
|
positionLeft: barPosition === "right"
|
||||||
|
positionRight: barPosition === "left"
|
||||||
|
positionAbove: Settings.data.bar.position === "bottom"
|
||||||
|
delay: Style.tooltipDelayLong
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
tooltip.show()
|
||||||
|
if (disableOpen) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!forceOpen) {
|
||||||
|
showDelayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onExited: {
|
||||||
|
hovered = false
|
||||||
|
root.exited()
|
||||||
|
if (!forceOpen) {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
tooltip.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 (!showPill) {
|
||||||
|
shouldAnimateHide = autoHide
|
||||||
|
showAnim.start()
|
||||||
|
} else {
|
||||||
|
hideAnim.stop()
|
||||||
|
delayedHideAnim.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
if (forceOpen) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (showPill) {
|
||||||
|
hideAnim.start()
|
||||||
|
}
|
||||||
|
showTimer.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDelayed() {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,8 +17,8 @@ Item {
|
||||||
property bool hovered: false
|
property bool hovered: false
|
||||||
property real fontSize: Style.fontSizeXS
|
property real fontSize: Style.fontSizeXS
|
||||||
|
|
||||||
// Effective shown state (true if hovered/animated open or forced)
|
readonly property string barPosition: Settings.data.bar.position
|
||||||
readonly property bool revealed: forceOpen || showPill
|
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
|
||||||
|
|
||||||
signal shown
|
signal shown
|
||||||
signal hidden
|
signal hidden
|
||||||
|
|
@ -29,258 +29,81 @@ Item {
|
||||||
signal middleClicked
|
signal middleClicked
|
||||||
signal wheel(int delta)
|
signal wheel(int delta)
|
||||||
|
|
||||||
// Internal state
|
// Dynamic sizing based on loaded component
|
||||||
property bool showPill: false
|
width: pillLoader.item ? pillLoader.item.width : 0
|
||||||
property bool shouldAnimateHide: false
|
height: pillLoader.item ? pillLoader.item.height : 0
|
||||||
|
|
||||||
// Exposed width logic
|
// Loader to switch between vertical and horizontal pill implementations
|
||||||
readonly property int iconSize: Math.round(Style.baseWidgetSize * sizeRatio * scaling)
|
Loader {
|
||||||
readonly property int pillHeight: iconSize
|
id: pillLoader
|
||||||
readonly property int pillPaddingHorizontal: Style.marginS * scaling
|
sourceComponent: isVerticalBar ? verticalPillComponent : horizontalPillComponent
|
||||||
readonly property int pillOverlap: iconSize * 0.5
|
|
||||||
readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap)
|
|
||||||
|
|
||||||
width: iconSize + Math.max(0, pill.width - pillOverlap)
|
Component {
|
||||||
height: pillHeight
|
id: verticalPillComponent
|
||||||
|
NVerticalPill {
|
||||||
|
icon: root.icon
|
||||||
|
text: root.text
|
||||||
|
tooltipText: root.tooltipText
|
||||||
|
sizeRatio: root.sizeRatio
|
||||||
|
autoHide: root.autoHide
|
||||||
|
forceOpen: root.forceOpen
|
||||||
|
disableOpen: root.disableOpen
|
||||||
|
rightOpen: root.rightOpen
|
||||||
|
hovered: root.hovered
|
||||||
|
fontSize: root.fontSize
|
||||||
|
|
||||||
Rectangle {
|
onShown: root.shown()
|
||||||
id: pill
|
onHidden: root.hidden()
|
||||||
width: revealed ? maxPillWidth : 1
|
onEntered: root.entered()
|
||||||
height: pillHeight
|
onExited: root.exited()
|
||||||
|
onClicked: root.clicked()
|
||||||
x: rightOpen ? (iconCircle.x + iconCircle.width / 2) : // Opens right
|
onRightClicked: root.rightClicked()
|
||||||
(iconCircle.x + iconCircle.width / 2) - width // Opens left
|
onMiddleClicked: root.middleClicked()
|
||||||
|
onWheel: root.wheel
|
||||||
opacity: revealed ? Style.opacityFull : Style.opacityNone
|
|
||||||
color: Color.mSurfaceVariant
|
|
||||||
|
|
||||||
topLeftRadius: rightOpen ? 0 : pillHeight * 0.5
|
|
||||||
bottomLeftRadius: rightOpen ? 0 : pillHeight * 0.5
|
|
||||||
topRightRadius: rightOpen ? pillHeight * 0.5 : 0
|
|
||||||
bottomRightRadius: rightOpen ? pillHeight * 0.5 : 0
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
NText {
|
|
||||||
id: textItem
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
x: {
|
|
||||||
// Little tweak to have a better text horizontal centering
|
|
||||||
var centerX = (parent.width - width) / 2
|
|
||||||
var offset = rightOpen ? Style.marginXS * scaling : -Style.marginXS * scaling
|
|
||||||
return centerX + offset
|
|
||||||
}
|
|
||||||
text: root.text
|
|
||||||
font.pointSize: root.fontSize * scaling
|
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mPrimary
|
|
||||||
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.animationNormal
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: iconCircle
|
|
||||||
width: iconSize
|
|
||||||
height: iconSize
|
|
||||||
radius: width * 0.5
|
|
||||||
color: hovered && !forceOpen ? Color.mTertiary : Color.mSurfaceVariant
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
x: rightOpen ? 0 : (parent.width - width)
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Style.animationNormal
|
|
||||||
easing.type: Easing.InOutQuad
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NIcon {
|
Component {
|
||||||
icon: root.icon
|
id: horizontalPillComponent
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
NHorizontalPill {
|
||||||
color: hovered && !forceOpen ? Color.mOnTertiary : Color.mOnSurface
|
icon: root.icon
|
||||||
// Center horizontally
|
text: root.text
|
||||||
x: (iconCircle.width - width) / 2
|
tooltipText: root.tooltipText
|
||||||
// Center vertically accounting for font metrics
|
sizeRatio: root.sizeRatio
|
||||||
y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2
|
autoHide: root.autoHide
|
||||||
}
|
forceOpen: root.forceOpen
|
||||||
}
|
disableOpen: root.disableOpen
|
||||||
|
rightOpen: root.rightOpen
|
||||||
|
hovered: root.hovered
|
||||||
|
fontSize: root.fontSize
|
||||||
|
|
||||||
ParallelAnimation {
|
onShown: root.shown()
|
||||||
id: showAnim
|
onHidden: root.hidden()
|
||||||
running: false
|
onEntered: root.entered()
|
||||||
NumberAnimation {
|
onExited: root.exited()
|
||||||
target: pill
|
onClicked: root.clicked()
|
||||||
property: "width"
|
onRightClicked: root.rightClicked()
|
||||||
from: 1
|
onMiddleClicked: root.middleClicked()
|
||||||
to: maxPillWidth
|
onWheel: root.wheel
|
||||||
duration: Style.animationNormal
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
NumberAnimation {
|
|
||||||
target: pill
|
|
||||||
property: "opacity"
|
|
||||||
from: 0
|
|
||||||
to: 1
|
|
||||||
duration: Style.animationNormal
|
|
||||||
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: "opacity"
|
|
||||||
from: 1
|
|
||||||
to: 0
|
|
||||||
duration: Style.animationNormal
|
|
||||||
easing.type: Easing.InCubic
|
|
||||||
}
|
|
||||||
onStopped: {
|
|
||||||
showPill = false
|
|
||||||
shouldAnimateHide = false
|
|
||||||
root.hidden()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NTooltip {
|
|
||||||
id: tooltip
|
|
||||||
positionAbove: Settings.data.bar.position === "bottom"
|
|
||||||
target: pill
|
|
||||||
delay: Style.tooltipDelayLong
|
|
||||||
text: root.tooltipText
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
tooltip.show()
|
|
||||||
if (disableOpen) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!forceOpen) {
|
|
||||||
showDelayed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onExited: {
|
|
||||||
hovered = false
|
|
||||||
root.exited()
|
|
||||||
if (!forceOpen) {
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
tooltip.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() {
|
function show() {
|
||||||
if (!showPill) {
|
if (pillLoader.item && pillLoader.item.show) {
|
||||||
shouldAnimateHide = autoHide
|
pillLoader.item.show()
|
||||||
showAnim.start()
|
|
||||||
} else {
|
|
||||||
hideAnim.stop()
|
|
||||||
delayedHideAnim.restart()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
if (forceOpen) {
|
if (pillLoader.item && pillLoader.item.hide) {
|
||||||
return
|
pillLoader.item.hide()
|
||||||
}
|
}
|
||||||
if (showPill) {
|
|
||||||
hideAnim.start()
|
|
||||||
}
|
|
||||||
showTimer.stop()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showDelayed() {
|
function showDelayed() {
|
||||||
if (!showPill) {
|
if (pillLoader.item && pillLoader.item.showDelayed) {
|
||||||
shouldAnimateHide = autoHide
|
pillLoader.item.showDelayed()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
324
Widgets/NVerticalPill.qml
Normal file
324
Widgets/NVerticalPill.qml
Normal file
|
|
@ -0,0 +1,324 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string icon: ""
|
||||||
|
property string text: ""
|
||||||
|
property string tooltipText: ""
|
||||||
|
property real sizeRatio: 0.8
|
||||||
|
property bool autoHide: false
|
||||||
|
property bool forceOpen: false
|
||||||
|
property bool disableOpen: false
|
||||||
|
property bool rightOpen: false
|
||||||
|
property bool hovered: false
|
||||||
|
property real fontSize: Style.fontSizeXS
|
||||||
|
|
||||||
|
// Bar position detection for pill direction
|
||||||
|
readonly property string barPosition: Settings.data.bar.position
|
||||||
|
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
|
||||||
|
|
||||||
|
// Determine pill direction based on section position
|
||||||
|
readonly property bool openDownward: rightOpen
|
||||||
|
readonly property bool openUpward: !rightOpen
|
||||||
|
|
||||||
|
// Effective shown state (true if animated open or forced)
|
||||||
|
readonly property bool revealed: 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
|
||||||
|
|
||||||
|
// Sizing logic for vertical bars
|
||||||
|
readonly property int iconSize: Math.round(Style.baseWidgetSize * sizeRatio * scaling)
|
||||||
|
readonly property int pillHeight: iconSize
|
||||||
|
readonly property int pillPaddingHorizontal: Style.marginS * scaling
|
||||||
|
readonly property int pillPaddingVertical: Style.marginS * scaling
|
||||||
|
readonly property int pillOverlap: iconSize * 0.5
|
||||||
|
readonly property int maxPillWidth: iconSize
|
||||||
|
readonly property int maxPillHeight: Math.max(1, textItem.implicitHeight + pillPaddingVertical * 3)
|
||||||
|
|
||||||
|
// For vertical bars: width is just icon size, height includes pill space
|
||||||
|
width: iconSize
|
||||||
|
height: revealed ? (iconSize + maxPillHeight - pillOverlap) : iconSize
|
||||||
|
|
||||||
|
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: openUpward ? (iconCircle.y + iconCircle.height / 2 - height) : (iconCircle.y + iconCircle.height / 2)
|
||||||
|
|
||||||
|
opacity: revealed ? Style.opacityFull : Style.opacityNone
|
||||||
|
color: Color.mSurfaceVariant
|
||||||
|
border.color: Color.mOutline
|
||||||
|
border.width: Math.max(1, Style.borderS * scaling)
|
||||||
|
|
||||||
|
// Radius logic for vertical expansion - rounded on the side that connects to icon
|
||||||
|
topLeftRadius: openUpward ? iconSize * 0.5 : 0
|
||||||
|
bottomLeftRadius: openDownward ? iconSize * 0.5 : 0
|
||||||
|
topRightRadius: openUpward ? iconSize * 0.5 : 0
|
||||||
|
bottomRightRadius: openDownward ? iconSize * 0.5 : 0
|
||||||
|
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
NVerticalText {
|
||||||
|
id: textItem
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: root.text
|
||||||
|
fontSize: Style.fontSizeXXS * scaling
|
||||||
|
fontWeight: Style.fontWeightBold
|
||||||
|
color: Color.mOnSurface
|
||||||
|
visible: revealed
|
||||||
|
}
|
||||||
|
|
||||||
|
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.animationNormal
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: iconCircle
|
||||||
|
width: iconSize
|
||||||
|
height: iconSize
|
||||||
|
radius: width * 0.5
|
||||||
|
color: hovered && !forceOpen ? Color.mTertiary : Color.mSurfaceVariant
|
||||||
|
|
||||||
|
// Icon positioning based on direction
|
||||||
|
x: 0
|
||||||
|
y: openUpward ? (parent.height - height) : 0
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NIcon {
|
||||||
|
icon: root.icon
|
||||||
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
|
color: hovered && !forceOpen ? Color.mOnTertiary : Color.mOnSurfaceVariant
|
||||||
|
// 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.animationNormal
|
||||||
|
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.animationNormal
|
||||||
|
easing.type: Easing.InCubic
|
||||||
|
}
|
||||||
|
onStopped: {
|
||||||
|
showPill = false
|
||||||
|
shouldAnimateHide = false
|
||||||
|
root.hidden()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NTooltip {
|
||||||
|
id: tooltip
|
||||||
|
target: pill
|
||||||
|
text: root.tooltipText
|
||||||
|
positionLeft: barPosition === "right"
|
||||||
|
positionRight: barPosition === "left"
|
||||||
|
positionAbove: Settings.data.bar.position === "bottom"
|
||||||
|
delay: Style.tooltipDelayLong
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
tooltip.show()
|
||||||
|
if (disableOpen) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!forceOpen) {
|
||||||
|
showDelayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onExited: {
|
||||||
|
hovered = false
|
||||||
|
root.exited()
|
||||||
|
if (!forceOpen) {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
tooltip.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 (!showPill) {
|
||||||
|
shouldAnimateHide = autoHide
|
||||||
|
showAnim.start()
|
||||||
|
} else {
|
||||||
|
hideAnim.stop()
|
||||||
|
delayedHideAnim.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
if (forceOpen) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (showPill) {
|
||||||
|
hideAnim.start()
|
||||||
|
}
|
||||||
|
showTimer.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDelayed() {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Widgets/NVerticalText.qml
Normal file
26
Widgets/NVerticalText.qml
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string text: ""
|
||||||
|
property real fontSize: Style.fontSizeXS
|
||||||
|
property color color: Color.mOnSurface
|
||||||
|
property int fontWeight: Style.fontWeightBold
|
||||||
|
|
||||||
|
spacing: -2 * scaling
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.text.split("")
|
||||||
|
NText {
|
||||||
|
text: modelData
|
||||||
|
font.pointSize: root.fontSize
|
||||||
|
font.weight: root.fontWeight
|
||||||
|
font.family: Settings.data.ui.fontDefault
|
||||||
|
color: root.color
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue