Merge tag 'v2.9.2'
Release v2.9.2
This commit is contained in:
commit
7b1a5d2eb2
150 changed files with 6907 additions and 5080 deletions
|
|
@ -33,8 +33,7 @@ Rectangle {
|
|||
|
||||
// Dimensions
|
||||
implicitWidth: customWidth > 0 ? customWidth : contentRow.implicitWidth + (Style.marginL * 2 * scaling)
|
||||
implicitHeight: customHeight > 0 ? customHeight : Math.max(Style.baseWidgetSize * scaling,
|
||||
contentRow.implicitHeight + (Style.marginM * scaling))
|
||||
implicitHeight: customHeight > 0 ? customHeight : Math.max(Style.baseWidgetSize * scaling, contentRow.implicitHeight + (Style.marginM * scaling))
|
||||
|
||||
// Appearance
|
||||
radius: Style.radiusS * scaling
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ RowLayout {
|
|||
property bool hovering: false
|
||||
property color activeColor: Color.mPrimary
|
||||
property color activeOnColor: Color.mOnPrimary
|
||||
property int baseSize: Math.max(Style.baseWidgetSize * 0.8, 14)
|
||||
property int baseSize: Style.baseWidgetSize * 0.7
|
||||
|
||||
signal toggled(bool checked)
|
||||
signal entered
|
||||
|
|
@ -35,12 +35,12 @@ RowLayout {
|
|||
Rectangle {
|
||||
id: box
|
||||
|
||||
implicitWidth: root.baseSize * scaling
|
||||
implicitHeight: root.baseSize * scaling
|
||||
implicitWidth: Math.round(root.baseSize * scaling)
|
||||
implicitHeight: Math.round(root.baseSize * scaling)
|
||||
radius: Style.radiusXS * scaling
|
||||
color: root.checked ? root.activeColor : Color.mSurface
|
||||
border.color: root.checked ? root.activeColor : Color.mOutline
|
||||
border.width: Math.max(1, Style.borderM * scaling)
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
|
|
@ -57,9 +57,11 @@ RowLayout {
|
|||
NIcon {
|
||||
visible: root.checked
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: -1 * scaling
|
||||
icon: "check"
|
||||
color: root.activeOnColor
|
||||
font.pointSize: Math.max(Style.fontSizeS, root.baseSize * 0.7) * scaling
|
||||
font.pointSize: Math.max(Style.fontSizeXS, root.baseSize * 0.5) * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
|
|
|
|||
193
Widgets/NCollapsible.qml
Normal file
193
Widgets/NCollapsible.qml
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
property string label: ""
|
||||
property string description: ""
|
||||
property bool expanded: false
|
||||
property bool defaultExpanded: false
|
||||
property real contentSpacing: Style.marginM * scaling
|
||||
signal toggled(bool expanded)
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
// Default property to accept children
|
||||
default property alias content: contentLayout.children
|
||||
|
||||
// Header with clickable area
|
||||
Rectangle {
|
||||
id: headerContainer
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: headerContent.implicitHeight + (Style.marginL * scaling * 2)
|
||||
|
||||
// Material 3 style background
|
||||
color: root.expanded ? Color.mSecondary : Color.mSurfaceVariant
|
||||
radius: Style.radiusL * scaling
|
||||
|
||||
// Subtle border
|
||||
border.color: root.expanded ? Color.mOnSecondary : Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
|
||||
// Smooth color transitions
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: headerArea
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
|
||||
onClicked: {
|
||||
root.expanded = !root.expanded
|
||||
root.toggled(root.expanded)
|
||||
}
|
||||
|
||||
// Hover effect overlay
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: headerArea.containsMouse ? Color.mOnSurface : Color.transparent
|
||||
opacity: headerArea.containsMouse ? 0.08 : 0
|
||||
radius: headerContainer.radius // Reference the container's radius directly
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: headerContent
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
// Expand/collapse icon with rotation animation
|
||||
NIcon {
|
||||
id: chevronIcon
|
||||
icon: "chevron-right"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: root.expanded ? Color.mOnSecondary : Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
rotation: root.expanded ? 90 : 0
|
||||
Behavior on rotation {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationNormal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Header text content - properly contained
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: Style.marginXXS * scaling
|
||||
|
||||
NText {
|
||||
text: root.label
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightSemiBold
|
||||
color: root.expanded ? Color.mOnSecondary : Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationNormal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: root.description
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
font.weight: Style.fontWeightRegular
|
||||
color: root.expanded ? Color.mOnSecondary : Color.mOnSurfaceVariant
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
visible: root.description !== ""
|
||||
opacity: 0.87
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationNormal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collapsible content with Material 3 styling
|
||||
Rectangle {
|
||||
id: contentContainer
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginS * scaling
|
||||
|
||||
visible: root.expanded
|
||||
color: Color.mSurface
|
||||
radius: Style.radiusL * scaling
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
|
||||
// Dynamic height based on content
|
||||
Layout.preferredHeight: visible ? contentLayout.implicitHeight + (Style.marginL * scaling * 2) : 0
|
||||
|
||||
// Smooth height animation
|
||||
Behavior on Layout.preferredHeight {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
// Content layout
|
||||
ColumnLayout {
|
||||
id: contentLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL * scaling
|
||||
spacing: root.contentSpacing
|
||||
|
||||
// Clip content during animation
|
||||
clip: true
|
||||
}
|
||||
|
||||
// Fade in animation for content
|
||||
opacity: root.expanded ? 1.0 : 0.0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize expanded state
|
||||
Component.onCompleted: {
|
||||
root.expanded = root.defaultExpanded
|
||||
}
|
||||
}
|
||||
|
|
@ -110,12 +110,12 @@ Popup {
|
|||
border.width: Math.max(1, Style.borderM * scaling)
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
NScrollView {
|
||||
id: scrollView
|
||||
anchors.fill: parent
|
||||
|
||||
ScrollBar.vertical.policy: ScrollBar.AlwaysOff
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AlwaysOff
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
clip: true
|
||||
|
||||
ColumnLayout {
|
||||
|
|
@ -180,8 +180,7 @@ Popup {
|
|||
}
|
||||
|
||||
NText {
|
||||
text: "RGB(" + Math.round(root.selectedColor.r * 255) + ", " + Math.round(
|
||||
root.selectedColor.g * 255) + ", " + Math.round(root.selectedColor.b * 255) + ")"
|
||||
text: "RGB(" + Math.round(root.selectedColor.r * 255) + ", " + Math.round(root.selectedColor.g * 255) + ", " + Math.round(root.selectedColor.b * 255) + ")"
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: root.selectedColor.r + root.selectedColor.g + root.selectedColor.b > 1.5 ? "#000000" : "#FFFFFF"
|
||||
|
|
@ -244,25 +243,19 @@ Popup {
|
|||
Layout.preferredWidth: 20 * scaling
|
||||
}
|
||||
|
||||
NSlider {
|
||||
NValueSlider {
|
||||
id: redSlider
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 255
|
||||
value: Math.round(root.selectedColor.r * 255)
|
||||
onMoved: {
|
||||
root.selectedColor = Qt.rgba(value / 255, root.selectedColor.g, root.selectedColor.b, 1)
|
||||
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255,
|
||||
root.selectedColor.b * 255)
|
||||
root.currentHue = hsv[0]
|
||||
root.currentSaturation = hsv[1]
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: Math.round(redSlider.value)
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
Layout.preferredWidth: 30 * scaling
|
||||
onMoved: value => {
|
||||
root.selectedColor = Qt.rgba(value / 255, root.selectedColor.g, root.selectedColor.b, 1)
|
||||
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, root.selectedColor.b * 255)
|
||||
root.currentHue = hsv[0]
|
||||
root.currentSaturation = hsv[1]
|
||||
}
|
||||
text: Math.round(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -276,26 +269,20 @@ Popup {
|
|||
Layout.preferredWidth: 20 * scaling
|
||||
}
|
||||
|
||||
NSlider {
|
||||
NValueSlider {
|
||||
id: greenSlider
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 255
|
||||
value: Math.round(root.selectedColor.g * 255)
|
||||
onMoved: {
|
||||
root.selectedColor = Qt.rgba(root.selectedColor.r, value / 255, root.selectedColor.b, 1)
|
||||
// Update stored hue and saturation when RGB changes
|
||||
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255,
|
||||
root.selectedColor.b * 255)
|
||||
root.currentHue = hsv[0]
|
||||
root.currentSaturation = hsv[1]
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: Math.round(greenSlider.value)
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
Layout.preferredWidth: 30 * scaling
|
||||
onMoved: value => {
|
||||
root.selectedColor = Qt.rgba(root.selectedColor.r, value / 255, root.selectedColor.b, 1)
|
||||
// Update stored hue and saturation when RGB changes
|
||||
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, root.selectedColor.b * 255)
|
||||
root.currentHue = hsv[0]
|
||||
root.currentSaturation = hsv[1]
|
||||
}
|
||||
text: Math.round(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -309,26 +296,20 @@ Popup {
|
|||
Layout.preferredWidth: 20 * scaling
|
||||
}
|
||||
|
||||
NSlider {
|
||||
NValueSlider {
|
||||
id: blueSlider
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 255
|
||||
value: Math.round(root.selectedColor.b * 255)
|
||||
onMoved: {
|
||||
root.selectedColor = Qt.rgba(root.selectedColor.r, root.selectedColor.g, value / 255, 1)
|
||||
// Update stored hue and saturation when RGB changes
|
||||
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255,
|
||||
root.selectedColor.b * 255)
|
||||
root.currentHue = hsv[0]
|
||||
root.currentSaturation = hsv[1]
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: Math.round(blueSlider.value)
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
Layout.preferredWidth: 30 * scaling
|
||||
onMoved: value => {
|
||||
root.selectedColor = Qt.rgba(root.selectedColor.r, root.selectedColor.g, value / 255, 1)
|
||||
// Update stored hue and saturation when RGB changes
|
||||
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, root.selectedColor.b * 255)
|
||||
root.currentHue = hsv[0]
|
||||
root.currentSaturation = hsv[1]
|
||||
}
|
||||
text: Math.round(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -342,38 +323,31 @@ Popup {
|
|||
Layout.preferredWidth: 80 * scaling
|
||||
}
|
||||
|
||||
NSlider {
|
||||
NValueSlider {
|
||||
id: brightnessSlider
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 100
|
||||
value: {
|
||||
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255,
|
||||
root.selectedColor.b * 255)
|
||||
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, root.selectedColor.b * 255)
|
||||
return hsv[2]
|
||||
}
|
||||
onMoved: {
|
||||
var hue = root.currentHue
|
||||
var saturation = root.currentSaturation
|
||||
onMoved: value => {
|
||||
var hue = root.currentHue
|
||||
var saturation = root.currentSaturation
|
||||
|
||||
if (hue === 0 && saturation === 0) {
|
||||
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255,
|
||||
root.selectedColor.b * 255)
|
||||
hue = hsv[0]
|
||||
saturation = hsv[1]
|
||||
root.currentHue = hue
|
||||
root.currentSaturation = saturation
|
||||
}
|
||||
if (hue === 0 && saturation === 0) {
|
||||
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, root.selectedColor.b * 255)
|
||||
hue = hsv[0]
|
||||
saturation = hsv[1]
|
||||
root.currentHue = hue
|
||||
root.currentSaturation = saturation
|
||||
}
|
||||
|
||||
var rgb = root.hsvToRgb(hue, saturation, value)
|
||||
root.selectedColor = Qt.rgba(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255, 1)
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
var rgb = root.hsvToRgb(hue, saturation, value)
|
||||
root.selectedColor = Qt.rgba(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255, 1)
|
||||
}
|
||||
text: Math.round(brightnessSlider.value) + "%"
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
Layout.preferredWidth: 40 * scaling
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -416,8 +390,7 @@ Popup {
|
|||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.selectedColor = modelData
|
||||
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255,
|
||||
root.selectedColor.b * 255)
|
||||
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, root.selectedColor.b * 255)
|
||||
root.currentHue = hsv[0]
|
||||
root.currentSaturation = hsv[1]
|
||||
}
|
||||
|
|
@ -459,16 +432,14 @@ Popup {
|
|||
radius: Style.radiusXXS * scaling
|
||||
color: modelData
|
||||
border.color: root.selectedColor === modelData ? Color.mPrimary : Color.mOutline
|
||||
border.width: Math.max(
|
||||
1, root.selectedColor === modelData ? Style.borderM * scaling : Style.borderS * scaling)
|
||||
border.width: Math.max(1, root.selectedColor === modelData ? Style.borderM * scaling : Style.borderS * scaling)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.selectedColor = modelData
|
||||
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255,
|
||||
root.selectedColor.b * 255)
|
||||
var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, root.selectedColor.b * 255)
|
||||
root.currentHue = hsv[0]
|
||||
root.currentSaturation = hsv[1]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,10 +76,8 @@ RowLayout {
|
|||
font.pointSize: Style.fontSizeM * scaling
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
color: (combo.currentIndex >= 0
|
||||
&& combo.currentIndex < root.model.count) ? Color.mOnSurface : Color.mOnSurfaceVariant
|
||||
text: (combo.currentIndex >= 0
|
||||
&& combo.currentIndex < root.model.count) ? root.model.get(combo.currentIndex).name : root.placeholder
|
||||
color: (combo.currentIndex >= 0 && combo.currentIndex < root.model.count) ? Color.mOnSurface : Color.mOnSurfaceVariant
|
||||
text: (combo.currentIndex >= 0 && combo.currentIndex < root.model.count) ? root.model.get(combo.currentIndex).name : root.placeholder
|
||||
}
|
||||
|
||||
indicator: NIcon {
|
||||
|
|
|
|||
32
Widgets/NHeader.qml
Normal file
32
Widgets/NHeader.qml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
property string description: ""
|
||||
|
||||
spacing: Style.marginXXS * scaling
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginM * scaling
|
||||
|
||||
NText {
|
||||
text: root.label
|
||||
font.pointSize: Style.fontSizeXL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
font.capitalization: Font.Capitalize
|
||||
color: Color.mSecondary
|
||||
visible: root.title !== ""
|
||||
}
|
||||
|
||||
NText {
|
||||
text: root.description
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
visible: root.description !== ""
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ Rectangle {
|
|||
|
||||
// Multiplier to control how large the button container is relative to Style.baseWidgetSize
|
||||
property real sizeRatio: 1.0
|
||||
readonly property real size: Style.baseWidgetSize * sizeRatio * scaling
|
||||
|
||||
property string icon
|
||||
property string tooltipText
|
||||
|
|
@ -19,8 +18,8 @@ Rectangle {
|
|||
|
||||
property color colorBg: Color.mSurfaceVariant
|
||||
property color colorFg: Color.mPrimary
|
||||
property color colorBgHover: Color.mPrimary
|
||||
property color colorFgHover: Color.mOnPrimary
|
||||
property color colorBgHover: Color.mTertiary
|
||||
property color colorFgHover: Color.mOnTertiary
|
||||
property color colorBorder: Color.mOutline
|
||||
property color colorBorderHover: Color.mOutline
|
||||
|
||||
|
|
@ -30,8 +29,8 @@ Rectangle {
|
|||
signal rightClicked
|
||||
signal middleClicked
|
||||
|
||||
implicitWidth: size
|
||||
implicitHeight: size
|
||||
implicitWidth: Math.round(Style.baseWidgetSize * scaling * sizeRatio)
|
||||
implicitHeight: Math.round(Style.baseWidgetSize * scaling * sizeRatio)
|
||||
|
||||
opacity: root.enabled ? Style.opacityFull : Style.opacityMedium
|
||||
color: root.enabled && root.hovering ? colorBgHover : colorBg
|
||||
|
|
|
|||
203
Widgets/NListView.qml
Normal file
203
Widgets/NListView.qml
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Templates as T
|
||||
import qs.Commons
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property color handleColor: Qt.alpha(Color.mTertiary, 0.8)
|
||||
property color handleHoverColor: handleColor
|
||||
property color handlePressedColor: handleColor
|
||||
property color trackColor: Color.transparent
|
||||
property real handleWidth: 6 * scaling
|
||||
property real handleRadius: Style.radiusM * scaling
|
||||
property int verticalPolicy: ScrollBar.AsNeeded
|
||||
property int horizontalPolicy: ScrollBar.AsNeeded
|
||||
|
||||
// Forward ListView properties
|
||||
property alias model: listView.model
|
||||
property alias delegate: listView.delegate
|
||||
property alias spacing: listView.spacing
|
||||
property alias orientation: listView.orientation
|
||||
property alias currentIndex: listView.currentIndex
|
||||
property alias count: listView.count
|
||||
property alias contentHeight: listView.contentHeight
|
||||
property alias contentWidth: listView.contentWidth
|
||||
property alias contentY: listView.contentY
|
||||
property alias contentX: listView.contentX
|
||||
property alias currentItem: listView.currentItem
|
||||
property alias highlightItem: listView.highlightItem
|
||||
property alias headerItem: listView.headerItem
|
||||
property alias footerItem: listView.footerItem
|
||||
property alias section: listView.section
|
||||
property alias highlightFollowsCurrentItem: listView.highlightFollowsCurrentItem
|
||||
property alias highlightMoveDuration: listView.highlightMoveDuration
|
||||
property alias highlightMoveVelocity: listView.highlightMoveVelocity
|
||||
property alias preferredHighlightBegin: listView.preferredHighlightBegin
|
||||
property alias preferredHighlightEnd: listView.preferredHighlightEnd
|
||||
property alias highlightRangeMode: listView.highlightRangeMode
|
||||
property alias snapMode: listView.snapMode
|
||||
property alias keyNavigationWraps: listView.keyNavigationWraps
|
||||
property alias cacheBuffer: listView.cacheBuffer
|
||||
property alias displayMarginBeginning: listView.displayMarginBeginning
|
||||
property alias displayMarginEnd: listView.displayMarginEnd
|
||||
property alias layoutDirection: listView.layoutDirection
|
||||
property alias effectiveLayoutDirection: listView.effectiveLayoutDirection
|
||||
property alias verticalLayoutDirection: listView.verticalLayoutDirection
|
||||
property alias boundsBehavior: listView.boundsBehavior
|
||||
property alias flickableDirection: listView.flickableDirection
|
||||
property alias interactive: listView.interactive
|
||||
property alias moving: listView.moving
|
||||
property alias flicking: listView.flicking
|
||||
property alias dragging: listView.dragging
|
||||
property alias horizontalVelocity: listView.horizontalVelocity
|
||||
property alias verticalVelocity: listView.verticalVelocity
|
||||
|
||||
// Forward ListView methods
|
||||
function positionViewAtIndex(index, mode) {
|
||||
listView.positionViewAtIndex(index, mode)
|
||||
}
|
||||
|
||||
function positionViewAtBeginning() {
|
||||
listView.positionViewAtBeginning()
|
||||
}
|
||||
|
||||
function positionViewAtEnd() {
|
||||
listView.positionViewAtEnd()
|
||||
}
|
||||
|
||||
function forceLayout() {
|
||||
listView.forceLayout()
|
||||
}
|
||||
|
||||
function cancelFlick() {
|
||||
listView.cancelFlick()
|
||||
}
|
||||
|
||||
function flick(xVelocity, yVelocity) {
|
||||
listView.flick(xVelocity, yVelocity)
|
||||
}
|
||||
|
||||
function incrementCurrentIndex() {
|
||||
listView.incrementCurrentIndex()
|
||||
}
|
||||
|
||||
function decrementCurrentIndex() {
|
||||
listView.decrementCurrentIndex()
|
||||
}
|
||||
|
||||
function indexAt(x, y) {
|
||||
return listView.indexAt(x, y)
|
||||
}
|
||||
|
||||
function itemAt(x, y) {
|
||||
return listView.itemAt(x, y)
|
||||
}
|
||||
|
||||
function itemAtIndex(index) {
|
||||
return listView.itemAtIndex(index)
|
||||
}
|
||||
|
||||
// Set reasonable implicit sizes for Layout usage
|
||||
implicitWidth: 200
|
||||
implicitHeight: 200
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
anchors.fill: parent
|
||||
|
||||
// Enable clipping to keep content within bounds
|
||||
clip: true
|
||||
|
||||
// Enable flickable for smooth scrolling
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
parent: listView
|
||||
x: listView.mirrored ? 0 : listView.width - width
|
||||
y: 0
|
||||
height: listView.height
|
||||
active: listView.ScrollBar.horizontal.active
|
||||
policy: root.verticalPolicy
|
||||
|
||||
contentItem: Rectangle {
|
||||
implicitWidth: root.handleWidth
|
||||
implicitHeight: 100
|
||||
radius: root.handleRadius
|
||||
color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor
|
||||
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
implicitWidth: root.handleWidth
|
||||
implicitHeight: 100
|
||||
color: root.trackColor
|
||||
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0.0
|
||||
radius: root.handleRadius / 2
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.horizontal: ScrollBar {
|
||||
id: horizontalScrollBar
|
||||
parent: listView
|
||||
x: 0
|
||||
y: listView.height - height
|
||||
width: listView.width
|
||||
active: listView.ScrollBar.vertical.active
|
||||
policy: root.horizontalPolicy
|
||||
|
||||
contentItem: Rectangle {
|
||||
implicitWidth: 100
|
||||
implicitHeight: root.handleWidth
|
||||
radius: root.handleRadius
|
||||
color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor
|
||||
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
implicitWidth: 100
|
||||
implicitHeight: root.handleWidth
|
||||
color: root.trackColor
|
||||
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0.0
|
||||
radius: root.handleRadius / 2
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,24 +7,14 @@ import qs.Services
|
|||
Loader {
|
||||
id: root
|
||||
|
||||
active: false
|
||||
asynchronous: true
|
||||
|
||||
property ShellScreen screen
|
||||
property real scaling: ScalingService.getScreenScale(screen)
|
||||
|
||||
Connections {
|
||||
target: ScalingService
|
||||
function onScaleChanged(screenName, scale) {
|
||||
if ((screen !== null) && (screenName === screen.name)) {
|
||||
scaling = scale
|
||||
}
|
||||
}
|
||||
}
|
||||
property real scaling: 1.0
|
||||
|
||||
property Component panelContent: null
|
||||
property int panelWidth: 1500
|
||||
property int panelHeight: 400
|
||||
property real preferredWidth: 700
|
||||
property real preferredHeight: 900
|
||||
property real preferredWidthRatio
|
||||
property real preferredHeightRatio
|
||||
property color panelBackgroundColor: Color.mSurface
|
||||
|
||||
property bool panelAnchorHorizontalCenter: false
|
||||
|
|
@ -50,14 +40,14 @@ Loader {
|
|||
property real opacityValue: originalOpacity
|
||||
|
||||
property alias isClosing: hideTimer.running
|
||||
readonly property real barHeight: Math.round(Style.barHeight * scaling)
|
||||
readonly property bool barAtBottom: Settings.data.bar.position === "bottom"
|
||||
readonly property bool barIsVisible: (screen !== null) && (Settings.data.bar.monitors.includes(screen.name)
|
||||
|| (Settings.data.bar.monitors.length === 0))
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
|
||||
signal opened
|
||||
signal closed
|
||||
|
||||
active: false
|
||||
asynchronous: true
|
||||
|
||||
Component.onCompleted: {
|
||||
PanelService.registerPanel(root)
|
||||
}
|
||||
|
|
@ -81,32 +71,16 @@ Loader {
|
|||
}
|
||||
|
||||
// -----------------------------------------
|
||||
function toggle(aScreen, buttonItem) {
|
||||
// Don't toggle if screen is null or invalid
|
||||
if (!aScreen || !aScreen.name) {
|
||||
Logger.warn("NPanel", "Cannot toggle panel: invalid screen object")
|
||||
return
|
||||
}
|
||||
|
||||
function toggle(buttonItem) {
|
||||
if (!active || isClosing) {
|
||||
open(aScreen, buttonItem)
|
||||
open(buttonItem)
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
function open(aScreen, buttonItem) {
|
||||
// Don't open if screen is null or invalid
|
||||
if (!aScreen || !aScreen.name) {
|
||||
Logger.warn("NPanel", "Cannot open panel: invalid screen object")
|
||||
return
|
||||
}
|
||||
|
||||
if (aScreen !== null) {
|
||||
screen = aScreen
|
||||
}
|
||||
|
||||
function open(buttonItem) {
|
||||
// Get the button position if provided
|
||||
if (buttonItem !== undefined && buttonItem !== null) {
|
||||
useButtonPosition = true
|
||||
|
|
@ -165,11 +139,38 @@ Loader {
|
|||
PanelWindow {
|
||||
id: panelWindow
|
||||
|
||||
// PanelWindow has its own screen property inherited of QsWindow
|
||||
property real scaling: ScalingService.getScreenScale(screen)
|
||||
readonly property real barHeight: Math.round(Style.barHeight * scaling)
|
||||
readonly property real barWidth: Math.round(Style.barHeight * scaling)
|
||||
readonly property bool barAtBottom: Settings.data.bar.position === "bottom"
|
||||
readonly property bool barIsVisible: (screen !== null) && (Settings.data.bar.monitors.includes(screen.name) || (Settings.data.bar.monitors.length === 0))
|
||||
|
||||
Connections {
|
||||
target: ScalingService
|
||||
function onScaleChanged(screenName, scale) {
|
||||
if ((screen !== null) && (screenName === screen.name)) {
|
||||
root.scaling = scaling = scale
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: panelWindow
|
||||
function onScreenChanged() {
|
||||
root.screen = screen
|
||||
root.scaling = scaling = ScalingService.getScreenScale(screen)
|
||||
|
||||
// It's mandatory to force refresh the subloader to ensure the scaling is properly dispatched
|
||||
panelContentLoader.active = false
|
||||
panelContentLoader.active = true
|
||||
}
|
||||
}
|
||||
|
||||
visible: true
|
||||
|
||||
// Dim desktop if required
|
||||
color: (root.active && !root.isClosing
|
||||
&& Settings.data.general.dimDesktop) ? Qt.alpha(Color.mShadow, Style.opacityHeavy) : Color.transparent
|
||||
color: (root.active && !root.isClosing && Settings.data.general.dimDesktop) ? Qt.alpha(Color.mShadow, Style.opacityHeavy) : Color.transparent
|
||||
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.namespace: "noctalia-panel"
|
||||
|
|
@ -185,8 +186,29 @@ Loader {
|
|||
anchors.left: true
|
||||
anchors.right: true
|
||||
anchors.bottom: true
|
||||
margins.top: (barIsVisible && !barAtBottom) ? barHeight : 0
|
||||
margins.bottom: (barIsVisible && barAtBottom) ? barHeight : 0
|
||||
margins.top: {
|
||||
if (!barIsVisible || barAtBottom) {
|
||||
return 0
|
||||
}
|
||||
switch (Settings.data.bar.position) {
|
||||
case "top":
|
||||
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating && !panelAnchorVerticalCenter ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
|
||||
default:
|
||||
return Style.marginM * scaling
|
||||
}
|
||||
}
|
||||
|
||||
margins.bottom: {
|
||||
if (!barIsVisible || !barAtBottom) {
|
||||
return 0
|
||||
}
|
||||
switch (Settings.data.bar.position) {
|
||||
case "bottom":
|
||||
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating && !panelAnchorVerticalCenter ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// Close any panel with Esc without requiring focus
|
||||
Shortcut {
|
||||
|
|
@ -209,8 +231,27 @@ Loader {
|
|||
radius: Style.radiusL * scaling
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
width: panelWidth
|
||||
height: panelHeight
|
||||
width: {
|
||||
var w
|
||||
if (preferredWidthRatio !== undefined) {
|
||||
w = Math.round(Math.max(screen?.width * preferredWidthRatio, preferredWidth) * scaling)
|
||||
} else {
|
||||
w = preferredWidth * scaling
|
||||
}
|
||||
// Clamp width so it is never bigger than the screen
|
||||
return Math.min(w, screen?.width - Style.marginL * 2)
|
||||
}
|
||||
height: {
|
||||
var h
|
||||
if (preferredHeightRatio !== undefined) {
|
||||
h = Math.round(Math.max(screen?.height * preferredHeightRatio, preferredHeight) * scaling)
|
||||
} else {
|
||||
h = preferredHeight * scaling
|
||||
}
|
||||
|
||||
// Clamp width so it is never bigger than the screen
|
||||
return Math.min(h, screen?.height - Style.barHeight * scaling - Style.marginL * 2)
|
||||
}
|
||||
|
||||
scale: root.scaleValue
|
||||
opacity: root.opacityValue
|
||||
|
|
@ -219,37 +260,125 @@ Loader {
|
|||
y: calculatedY
|
||||
|
||||
property int calculatedX: {
|
||||
if (root.useButtonPosition) {
|
||||
// Position panel relative to button
|
||||
var targetX = root.buttonPosition.x + (root.buttonWidth / 2) - (panelWidth / 2)
|
||||
var barPosition = Settings.data.bar.position
|
||||
|
||||
// Keep panel within screen bounds
|
||||
var maxX = panelWindow.width - panelWidth - (Style.marginS * scaling)
|
||||
var minX = Style.marginS * scaling
|
||||
|
||||
return Math.round(Math.max(minX, Math.min(targetX, maxX)))
|
||||
} else if (!panelAnchorHorizontalCenter && panelAnchorLeft) {
|
||||
// Check anchor properties first, even when using button positioning
|
||||
if (!panelAnchorHorizontalCenter && panelAnchorLeft) {
|
||||
return Math.round(Style.marginS * scaling)
|
||||
} else if (!panelAnchorHorizontalCenter && panelAnchorRight) {
|
||||
return Math.round(panelWindow.width - panelWidth - (Style.marginS * scaling))
|
||||
// For right anchor, consider bar position
|
||||
if (barPosition === "right") {
|
||||
// If bar is on right, position panel to the left of the bar
|
||||
var maxX = panelWindow.width - barWidth - panelBackground.width - (Style.marginS * scaling)
|
||||
|
||||
// If we have button position, position close to the button like working panels
|
||||
if (root.useButtonPosition) {
|
||||
// Use the same logic as working panels - position at edge of bar with spacing
|
||||
var maxXWithSpacing = panelWindow.width - barWidth - panelBackground.width
|
||||
// Add spacing - more if screen corners are disabled, less if enabled
|
||||
if (!Settings.data.general.showScreenCorners || Settings.data.bar.floating) {
|
||||
maxXWithSpacing -= Style.marginL * scaling
|
||||
} else {
|
||||
maxXWithSpacing -= Style.marginM * scaling
|
||||
}
|
||||
return Math.round(maxXWithSpacing)
|
||||
} else {
|
||||
return Math.round(maxX)
|
||||
}
|
||||
} else {
|
||||
// Default right positioning
|
||||
var rightX = panelWindow.width - panelBackground.width - (Style.marginS * scaling)
|
||||
return Math.round(rightX)
|
||||
}
|
||||
} else if (root.useButtonPosition) {
|
||||
// Position panel relative to button (only if no explicit anchoring)
|
||||
var targetX
|
||||
|
||||
// For vertical bars, position panel close to the button
|
||||
if (barPosition === "left") {
|
||||
// Position panel to the right of the left bar, close to the button
|
||||
var minX = barWidth
|
||||
// Add spacing - more if screen corners are disabled, less if enabled
|
||||
if (!Settings.data.general.showScreenCorners || Settings.data.bar.floating) {
|
||||
minX += Style.marginL * scaling
|
||||
} else {
|
||||
minX += Style.marginM * scaling
|
||||
}
|
||||
targetX = minX
|
||||
} else if (barPosition === "right") {
|
||||
// Position panel to the left of the right bar, close to the button
|
||||
var maxX = panelWindow.width - barWidth - panelBackground.width
|
||||
// Add spacing - more if screen corners are disabled, less if enabled
|
||||
if (!Settings.data.general.showScreenCorners || Settings.data.bar.floating) {
|
||||
maxX -= Style.marginL * scaling
|
||||
} else {
|
||||
maxX -= Style.marginM * scaling
|
||||
}
|
||||
targetX = maxX
|
||||
} else {
|
||||
// For horizontal bars, center panel on button
|
||||
targetX = root.buttonPosition.x + (root.buttonWidth / 2) - (panelBackground.width / 2)
|
||||
}
|
||||
|
||||
// Keep panel within screen bounds
|
||||
var maxScreenX = panelWindow.width - panelBackground.width - (Style.marginS * scaling)
|
||||
var minScreenX = Style.marginS * scaling
|
||||
|
||||
return Math.round(Math.max(minScreenX, Math.min(targetX, maxScreenX)))
|
||||
} else {
|
||||
return Math.round((panelWindow.width - panelWidth) / 2)
|
||||
// For vertical bars, center but avoid bar overlap
|
||||
var centerX = (panelWindow.width - panelBackground.width) / 2
|
||||
if (barPosition === "left") {
|
||||
var minX = barWidth
|
||||
// Add spacing - more if screen corners are disabled, less if enabled
|
||||
if (!Settings.data.general.showScreenCorners || Settings.data.bar.floating) {
|
||||
minX += Style.marginL * scaling
|
||||
} else {
|
||||
minX += Style.marginM * scaling
|
||||
}
|
||||
centerX = Math.max(centerX, minX)
|
||||
} else if (barPosition === "right") {
|
||||
// For right bar, center but ensure it doesn't overlap with the bar
|
||||
var maxX = panelWindow.width - barWidth - panelBackground.width
|
||||
// Add spacing - more if screen corners are disabled, less if enabled
|
||||
if (!Settings.data.general.showScreenCorners || Settings.data.bar.floating) {
|
||||
maxX -= Style.marginL * scaling
|
||||
} else {
|
||||
maxX -= Style.marginM * scaling
|
||||
}
|
||||
centerX = Math.min(centerX, maxX)
|
||||
}
|
||||
return Math.round(centerX)
|
||||
}
|
||||
}
|
||||
|
||||
property int calculatedY: {
|
||||
if (panelAnchorVerticalCenter) {
|
||||
return Math.round((panelWindow.height - panelHeight) / 2)
|
||||
var barPosition = Settings.data.bar.position
|
||||
|
||||
if (root.useButtonPosition) {
|
||||
// Position panel relative to button
|
||||
var targetY = root.buttonPosition.y + (root.buttonHeight / 2) - (panelBackground.height / 2)
|
||||
|
||||
// Keep panel within screen bounds
|
||||
var maxY = panelWindow.height - panelBackground.height - (Style.marginS * scaling)
|
||||
var minY = Style.marginS * scaling
|
||||
|
||||
return Math.round(Math.max(minY, Math.min(targetY, maxY)))
|
||||
} else if (panelAnchorVerticalCenter) {
|
||||
return Math.round((panelWindow.height - panelBackground.height) / 2)
|
||||
} else if (panelAnchorBottom) {
|
||||
return Math.round(panelWindow.height - panelHeight - (Style.marginS * scaling))
|
||||
return Math.round(panelWindow.height - panelBackground.height - (Style.marginS * scaling))
|
||||
} else if (panelAnchorTop) {
|
||||
return Math.round(Style.marginS * scaling)
|
||||
} else if (barPosition === "left" || barPosition === "right") {
|
||||
// For vertical bars, center vertically
|
||||
return Math.round((panelWindow.height - panelBackground.height) / 2)
|
||||
} else if (!barAtBottom) {
|
||||
// Below the top bar
|
||||
return Math.round(Style.marginS * scaling)
|
||||
} else {
|
||||
// Above the bottom bar
|
||||
return Math.round(panelWindow.height - panelHeight - (Style.marginS * scaling))
|
||||
return Math.round(panelWindow.height - panelBackground.height - (Style.marginS * scaling))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -280,6 +409,7 @@ Loader {
|
|||
}
|
||||
|
||||
Loader {
|
||||
id: panelContentLoader
|
||||
anchors.fill: parent
|
||||
sourceComponent: root.panelContent
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,16 +8,18 @@ Item {
|
|||
|
||||
property string icon: ""
|
||||
property string text: ""
|
||||
property string suffix: ""
|
||||
property string tooltipText: ""
|
||||
property real sizeRatio: 0.8
|
||||
property bool autoHide: false
|
||||
property bool forceOpen: false
|
||||
property bool forceClose: false
|
||||
property bool disableOpen: false
|
||||
property bool rightOpen: false
|
||||
property bool hovered: false
|
||||
|
||||
// Effective shown state (true if hovered/animated open or forced)
|
||||
readonly property bool revealed: forceOpen || showPill
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
|
||||
|
||||
signal shown
|
||||
signal hidden
|
||||
|
|
@ -28,258 +30,81 @@ Item {
|
|||
signal middleClicked
|
||||
signal wheel(int delta)
|
||||
|
||||
// Internal state
|
||||
property bool showPill: false
|
||||
property bool shouldAnimateHide: false
|
||||
// Dynamic sizing based on loaded component
|
||||
width: pillLoader.item ? pillLoader.item.width : 0
|
||||
height: pillLoader.item ? pillLoader.item.height : 0
|
||||
|
||||
// Exposed width logic
|
||||
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 pillOverlap: iconSize * 0.5
|
||||
readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap)
|
||||
// Loader to switch between vertical and horizontal pill implementations
|
||||
Loader {
|
||||
id: pillLoader
|
||||
sourceComponent: isVerticalBar ? verticalPillComponent : horizontalPillComponent
|
||||
|
||||
width: iconSize + Math.max(0, pill.width - pillOverlap)
|
||||
height: pillHeight
|
||||
|
||||
Rectangle {
|
||||
id: pill
|
||||
width: revealed ? maxPillWidth : 1
|
||||
height: pillHeight
|
||||
|
||||
x: rightOpen ? (iconCircle.x + iconCircle.width / 2) : // Opens right
|
||||
(iconCircle.x + iconCircle.width / 2) - width // Opens left
|
||||
|
||||
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: Style.fontSizeXS * 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.mPrimary : Color.mSurfaceVariant
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
x: rightOpen ? 0 : (parent.width - width)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutQuad
|
||||
Component {
|
||||
id: verticalPillComponent
|
||||
NPillVertical {
|
||||
icon: root.icon
|
||||
text: root.text
|
||||
suffix: root.suffix
|
||||
tooltipText: root.tooltipText
|
||||
sizeRatio: root.sizeRatio
|
||||
autoHide: root.autoHide
|
||||
forceOpen: root.forceOpen
|
||||
forceClose: root.forceClose
|
||||
disableOpen: root.disableOpen
|
||||
rightOpen: root.rightOpen
|
||||
hovered: root.hovered
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: root.icon
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: hovered && !forceOpen ? Color.mOnPrimary : Color.mOnSurface
|
||||
// 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: "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()
|
||||
Component {
|
||||
id: horizontalPillComponent
|
||||
NPillHorizontal {
|
||||
icon: root.icon
|
||||
text: root.text
|
||||
suffix: root.suffix
|
||||
tooltipText: root.tooltipText
|
||||
sizeRatio: root.sizeRatio
|
||||
autoHide: root.autoHide
|
||||
forceOpen: root.forceOpen
|
||||
forceClose: root.forceClose
|
||||
disableOpen: root.disableOpen
|
||||
rightOpen: root.rightOpen
|
||||
hovered: root.hovered
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
if (pillLoader.item && pillLoader.item.show) {
|
||||
pillLoader.item.show()
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (forceOpen) {
|
||||
return
|
||||
if (pillLoader.item && pillLoader.item.hide) {
|
||||
pillLoader.item.hide()
|
||||
}
|
||||
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()
|
||||
if (pillLoader.item && pillLoader.item.showDelayed) {
|
||||
pillLoader.item.showDelayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
290
Widgets/NPillHorizontal.qml
Normal file
290
Widgets/NPillHorizontal.qml
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string icon: ""
|
||||
property string text: ""
|
||||
property string suffix: ""
|
||||
property string tooltipText: ""
|
||||
property real sizeRatio: 0.8
|
||||
property bool autoHide: false
|
||||
property bool forceOpen: false
|
||||
property bool forceClose: false
|
||||
property bool disableOpen: false
|
||||
property bool rightOpen: false
|
||||
property bool hovered: false
|
||||
|
||||
// Effective shown state (true if hovered/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
|
||||
|
||||
// Exposed width logic
|
||||
readonly property int iconSize: Math.round(Style.baseWidgetSize * sizeRatio * scaling)
|
||||
readonly property int pillHeight: iconSize
|
||||
readonly property int pillPaddingHorizontal: 3 * 2 * scaling // Very precise adjustment don't replace by Style.margin
|
||||
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)
|
||||
height: pillHeight
|
||||
|
||||
Rectangle {
|
||||
id: pill
|
||||
width: revealed ? maxPillWidth : 1
|
||||
height: pillHeight
|
||||
|
||||
x: rightOpen ? (iconCircle.x + iconCircle.width / 2) : // Opens right
|
||||
(iconCircle.x + iconCircle.width / 2) - width // Opens left
|
||||
|
||||
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: {
|
||||
// Better text horizontal centering
|
||||
var centerX = (parent.width - width) / 2
|
||||
var offset = rightOpen ? Style.marginXS * scaling : -Style.marginXS * scaling
|
||||
if (forceOpen) {
|
||||
// If its force open, the icon disc background is the same color as the bg pill move text slightly
|
||||
offset += rightOpen ? -Style.marginXXS * scaling : Style.marginXXS * scaling
|
||||
}
|
||||
return centerX + offset
|
||||
}
|
||||
text: root.text + root.suffix
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: forceOpen ? Color.mOnSurface : 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 ? 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 {
|
||||
icon: root.icon
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: hovered ? Color.mOnTertiary : Color.mOnSurface
|
||||
// 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: "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() {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
331
Widgets/NPillVertical.qml
Normal file
331
Widgets/NPillVertical.qml
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string icon: ""
|
||||
property string text: ""
|
||||
property string suffix: ""
|
||||
property string tooltipText: ""
|
||||
property real sizeRatio: 0.8
|
||||
property bool autoHide: false
|
||||
property bool forceOpen: false
|
||||
property bool forceClose: false
|
||||
property bool disableOpen: false
|
||||
property bool rightOpen: false
|
||||
property bool hovered: false
|
||||
|
||||
// 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, but not if force closed)
|
||||
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
|
||||
|
||||
// Sizing logic for vertical bars
|
||||
readonly property int iconSize: Math.round(Style.baseWidgetSize * sizeRatio * scaling)
|
||||
readonly property int pillHeight: iconSize
|
||||
readonly property int pillPaddingVertical: 3 * 2 * scaling // Very precise adjustment don't replace by Style.margin
|
||||
readonly property int pillOverlap: iconSize * 0.5
|
||||
readonly property int maxPillWidth: iconSize
|
||||
readonly property int maxPillHeight: Math.max(1, textItem.implicitHeight + pillPaddingVertical * 4)
|
||||
|
||||
// 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
|
||||
|
||||
// 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
|
||||
|
||||
NText {
|
||||
id: textItem
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: {
|
||||
var offset = openDownward ? pillPaddingVertical * 0.75 : -pillPaddingVertical * 0.75
|
||||
if (forceOpen) {
|
||||
// If its force open, the icon disc background is the same color as the bg pill move text slightly
|
||||
offset += rightOpen ? -Style.marginXXS * scaling : Style.marginXXS * scaling
|
||||
}
|
||||
return offset
|
||||
}
|
||||
text: root.text + root.suffix
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: forceOpen ? Color.mOnSurface : Color.mPrimary
|
||||
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 ? 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 ? Color.mOnTertiary : Color.mOnSurface
|
||||
// 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 || forceClose) {
|
||||
return
|
||||
}
|
||||
if (!forceOpen) {
|
||||
showDelayed()
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
hovered = false
|
||||
root.exited()
|
||||
if (!forceOpen && !forceClose) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
106
Widgets/NScrollView.qml
Normal file
106
Widgets/NScrollView.qml
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Templates as T
|
||||
import qs.Commons
|
||||
|
||||
T.ScrollView {
|
||||
id: root
|
||||
|
||||
property color handleColor: Qt.alpha(Color.mTertiary, 0.8)
|
||||
property color handleHoverColor: handleColor
|
||||
property color handlePressedColor: handleColor
|
||||
property color trackColor: Color.transparent
|
||||
property real handleWidth: 6 * scaling
|
||||
property real handleRadius: Style.radiusM * scaling
|
||||
property int verticalPolicy: ScrollBar.AsNeeded
|
||||
property int horizontalPolicy: ScrollBar.AsNeeded
|
||||
|
||||
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding)
|
||||
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding)
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
parent: root
|
||||
x: root.mirrored ? 0 : root.width - width
|
||||
y: root.topPadding
|
||||
height: root.availableHeight
|
||||
active: root.ScrollBar.horizontal.active
|
||||
policy: root.verticalPolicy
|
||||
|
||||
contentItem: Rectangle {
|
||||
implicitWidth: root.handleWidth
|
||||
implicitHeight: 100
|
||||
radius: root.handleRadius
|
||||
color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor
|
||||
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
implicitWidth: root.handleWidth
|
||||
implicitHeight: 100
|
||||
color: root.trackColor
|
||||
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0.0
|
||||
radius: root.handleRadius / 2
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.horizontal: ScrollBar {
|
||||
parent: root
|
||||
x: root.leftPadding
|
||||
y: root.height - height
|
||||
width: root.availableWidth
|
||||
active: root.ScrollBar.vertical.active
|
||||
policy: root.horizontalPolicy
|
||||
|
||||
contentItem: Rectangle {
|
||||
implicitWidth: 100
|
||||
implicitHeight: root.handleWidth
|
||||
radius: root.handleRadius
|
||||
color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor
|
||||
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
implicitWidth: 100
|
||||
implicitHeight: root.handleWidth
|
||||
color: root.trackColor
|
||||
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0.0
|
||||
radius: root.handleRadius / 2
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
253
Widgets/NSearchableComboBox.qml
Normal file
253
Widgets/NSearchableComboBox.qml
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import "../Helpers/FuzzySort.js" as Fuzzysort
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
property real minimumWidth: 280 * scaling
|
||||
property real popupHeight: 180 * scaling
|
||||
|
||||
property string label: ""
|
||||
property string description: ""
|
||||
property ListModel model: {
|
||||
|
||||
}
|
||||
property string currentKey: ""
|
||||
property string placeholder: ""
|
||||
property string searchPlaceholder: "Search..."
|
||||
|
||||
readonly property real preferredHeight: Style.baseWidgetSize * 1.1 * scaling
|
||||
|
||||
signal selected(string key)
|
||||
|
||||
spacing: Style.marginL * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Filtered model for search results
|
||||
property ListModel filteredModel: ListModel {}
|
||||
property string searchText: ""
|
||||
|
||||
function findIndexByKey(key) {
|
||||
for (var i = 0; i < root.model.count; i++) {
|
||||
if (root.model.get(i).key === key) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
function findIndexByKeyInFiltered(key) {
|
||||
for (var i = 0; i < root.filteredModel.count; i++) {
|
||||
if (root.filteredModel.get(i).key === key) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
function filterModel() {
|
||||
filteredModel.clear()
|
||||
|
||||
if (searchText.trim() === "") {
|
||||
// If no search text, show all items
|
||||
for (var i = 0; i < root.model.count; i++) {
|
||||
filteredModel.append(root.model.get(i))
|
||||
}
|
||||
} else {
|
||||
// Convert ListModel to array for fuzzy search
|
||||
var items = []
|
||||
for (var i = 0; i < root.model.count; i++) {
|
||||
items.push(root.model.get(i))
|
||||
}
|
||||
|
||||
// Use fuzzy search if available, fallback to simple search
|
||||
if (typeof Fuzzysort !== 'undefined') {
|
||||
var fuzzyResults = Fuzzysort.go(searchText, items, {
|
||||
"key": "name",
|
||||
"threshold": -1000,
|
||||
"limit": 50
|
||||
})
|
||||
|
||||
// Add results in order of relevance
|
||||
for (var j = 0; j < fuzzyResults.length; j++) {
|
||||
filteredModel.append(fuzzyResults[j].obj)
|
||||
}
|
||||
} else {
|
||||
// Fallback to simple search
|
||||
var searchLower = searchText.toLowerCase()
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var item = items[i]
|
||||
if (item.name.toLowerCase().includes(searchLower)) {
|
||||
filteredModel.append(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSearchTextChanged: filterModel()
|
||||
onModelChanged: filterModel()
|
||||
|
||||
NLabel {
|
||||
label: root.label
|
||||
description: root.description
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
id: combo
|
||||
|
||||
Layout.minimumWidth: root.minimumWidth
|
||||
Layout.preferredHeight: root.preferredHeight
|
||||
model: filteredModel
|
||||
currentIndex: findIndexByKeyInFiltered(currentKey)
|
||||
onActivated: {
|
||||
if (combo.currentIndex >= 0 && combo.currentIndex < filteredModel.count) {
|
||||
root.selected(filteredModel.get(combo.currentIndex).key)
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
implicitWidth: Style.baseWidgetSize * 3.75 * scaling
|
||||
implicitHeight: preferredHeight
|
||||
color: Color.mSurface
|
||||
border.color: combo.activeFocus ? Color.mSecondary : Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
radius: Style.radiusM * scaling
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: NText {
|
||||
leftPadding: Style.marginL * scaling
|
||||
rightPadding: combo.indicator.width + Style.marginL * scaling
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
color: (combo.currentIndex >= 0 && combo.currentIndex < filteredModel.count) ? Color.mOnSurface : Color.mOnSurfaceVariant
|
||||
text: (combo.currentIndex >= 0 && combo.currentIndex < filteredModel.count) ? filteredModel.get(combo.currentIndex).name : root.placeholder
|
||||
}
|
||||
|
||||
indicator: NIcon {
|
||||
x: combo.width - width - Style.marginM * scaling
|
||||
y: combo.topPadding + (combo.availableHeight - height) / 2
|
||||
icon: "caret-down"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
}
|
||||
|
||||
popup: Popup {
|
||||
y: combo.height
|
||||
width: combo.width
|
||||
height: root.popupHeight + 60 * scaling
|
||||
padding: Style.marginM * scaling
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
// Search input
|
||||
NTextInput {
|
||||
id: searchInput
|
||||
Layout.fillWidth: true
|
||||
placeholderText: root.searchPlaceholder
|
||||
text: root.searchText
|
||||
onTextChanged: root.searchText = text
|
||||
fontSize: Style.fontSizeS * scaling
|
||||
}
|
||||
|
||||
// Font list
|
||||
ListView {
|
||||
id: listView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
model: combo.popup.visible ? filteredModel : null
|
||||
ScrollIndicator.vertical: ScrollIndicator {}
|
||||
|
||||
delegate: ItemDelegate {
|
||||
width: listView.width
|
||||
hoverEnabled: true
|
||||
highlighted: ListView.view.currentIndex === index
|
||||
|
||||
onHoveredChanged: {
|
||||
if (hovered) {
|
||||
ListView.view.currentIndex = index
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
root.selected(filteredModel.get(index).key)
|
||||
combo.currentIndex = root.findIndexByKeyInFiltered(filteredModel.get(index).key)
|
||||
combo.popup.close()
|
||||
}
|
||||
|
||||
contentItem: NText {
|
||||
text: name
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: highlighted ? Color.mSurface : Color.mOnSurface
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
width: listView.width * scaling
|
||||
color: highlighted ? Color.mTertiary : Color.transparent
|
||||
radius: Style.radiusS * scaling
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Color.mSurfaceVariant
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
radius: Style.radiusM * scaling
|
||||
}
|
||||
}
|
||||
|
||||
// Update the currentIndex if the currentKey is changed externally
|
||||
Connections {
|
||||
target: root
|
||||
function onCurrentKeyChanged() {
|
||||
combo.currentIndex = root.findIndexByKeyInFiltered(currentKey)
|
||||
}
|
||||
}
|
||||
|
||||
// Focus search input when popup opens
|
||||
Connections {
|
||||
target: combo.popup
|
||||
function onVisibleChanged() {
|
||||
if (combo.popup.visible) {
|
||||
// Small delay to ensure the popup is fully rendered
|
||||
Qt.callLater(function () {
|
||||
if (searchInput && searchInput.inputItem) {
|
||||
searchInput.inputItem.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,14 +7,13 @@ import qs.Services
|
|||
Slider {
|
||||
id: root
|
||||
|
||||
// Optional color to cut the track beneath the knob (should match surrounding background)
|
||||
property var cutoutColor
|
||||
property var cutoutColor: Color.mSurface
|
||||
property bool snapAlways: true
|
||||
property real heightRatio: 0.75
|
||||
|
||||
readonly property real knobDiameter: Style.baseWidgetSize * heightRatio * scaling
|
||||
readonly property real trackHeight: knobDiameter * 0.5
|
||||
readonly property real cutoutExtra: Style.baseWidgetSize * 0.1 * scaling
|
||||
readonly property real knobDiameter: Math.round(Style.baseWidgetSize * heightRatio * scaling)
|
||||
readonly property real trackHeight: knobDiameter * 0.4
|
||||
readonly property real cutoutExtra: Math.round(Style.baseWidgetSize * 0.1 * scaling)
|
||||
|
||||
snapMode: snapAlways ? Slider.SnapAlways : Slider.SnapOnRelease
|
||||
implicitHeight: Math.max(trackHeight, knobDiameter)
|
||||
|
|
@ -26,15 +25,54 @@ Slider {
|
|||
implicitHeight: trackHeight
|
||||
width: root.availableWidth
|
||||
height: implicitHeight
|
||||
radius: height / 2
|
||||
color: Color.mSurface
|
||||
radius: 0
|
||||
color: Qt.alpha(Color.mSurface, 0.5)
|
||||
border.color: Qt.alpha(Color.mOutline, 0.5)
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
|
||||
// Animated gradient active track
|
||||
Rectangle {
|
||||
id: activeTrack
|
||||
width: root.visualPosition * parent.width
|
||||
height: parent.height
|
||||
color: Color.mPrimary
|
||||
radius: parent.radius
|
||||
|
||||
// Animated gradient fill
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
GradientStop {
|
||||
position: 0.0
|
||||
color: Qt.darker(Color.mPrimary, 1.2)
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 300
|
||||
}
|
||||
}
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.5
|
||||
color: Color.mPrimary
|
||||
SequentialAnimation on position {
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: 0.3
|
||||
to: 0.7
|
||||
duration: 2000
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
NumberAnimation {
|
||||
from: 0.7
|
||||
to: 0.3
|
||||
duration: 2000
|
||||
easing.type: Easing.InOutSine
|
||||
}
|
||||
}
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0
|
||||
color: Qt.lighter(Color.mPrimary, 1.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Circular cutout
|
||||
|
|
@ -44,54 +82,32 @@ Slider {
|
|||
height: knobDiameter + cutoutExtra
|
||||
radius: width / 2
|
||||
color: root.cutoutColor !== undefined ? root.cutoutColor : Color.mSurface
|
||||
x: Math.max(0, Math.min(parent.width - width,
|
||||
root.visualPosition * (parent.width - root.knobDiameter) - cutoutExtra / 2))
|
||||
y: (parent.height - height) / 2
|
||||
x: root.leftPadding + root.visualPosition * (root.availableWidth - root.knobDiameter) - cutoutExtra / 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
handle: Item {
|
||||
width: knob.implicitWidth
|
||||
height: knob.implicitHeight
|
||||
x: root.leftPadding + root.visualPosition * (root.availableWidth - width)
|
||||
x: root.leftPadding + Math.round(root.visualPosition * (root.availableWidth - width))
|
||||
y: root.topPadding + root.availableHeight / 2 - height / 2
|
||||
|
||||
// Subtle shadow for a more polished look
|
||||
MultiEffect {
|
||||
anchors.fill: knob
|
||||
source: knob
|
||||
shadowEnabled: true
|
||||
shadowColor: Color.mShadow
|
||||
shadowOpacity: 0.25
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 1
|
||||
shadowBlur: 8
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: knob
|
||||
implicitWidth: knobDiameter
|
||||
implicitHeight: knobDiameter
|
||||
radius: width * 0.5
|
||||
color: root.pressed ? Color.mSurfaceVariant : Color.mSurface
|
||||
color: root.pressed ? Color.mTertiary : Color.mSurface
|
||||
border.color: Color.mPrimary
|
||||
border.width: Math.max(1, Style.borderL * scaling)
|
||||
anchors.centerIn: parent
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
// Press feedback halo (using accent color, low opacity)
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width + 8 * scaling
|
||||
height: parent.height + 8 * scaling
|
||||
radius: width / 2
|
||||
color: Color.mPrimary
|
||||
opacity: root.pressed ? 0.16 : 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,193 +0,0 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
property string description: ""
|
||||
property string type: "notice" // "notice", "warning"
|
||||
property int duration: 5000 // Auto-hide after 5 seconds, 0 = no auto-hide
|
||||
property bool persistent: false // If true, requires manual dismiss
|
||||
|
||||
required property ShellScreen screen
|
||||
property real scaling: 1.0
|
||||
|
||||
// Animation properties
|
||||
property real targetY: 0
|
||||
property real hiddenY: -height - 20
|
||||
|
||||
signal dismissed
|
||||
|
||||
width: Math.min(500 * scaling, parent.width * 0.8)
|
||||
height: Math.max(60 * scaling, contentLayout.implicitHeight + Style.marginL * 2 * scaling)
|
||||
|
||||
// Position at top center of parent
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: hiddenY
|
||||
z: 1000 // High z-index to appear above everything
|
||||
|
||||
function show() {
|
||||
// NToast updates its scaling when showing.
|
||||
scaling = ScalingService.getScreenScale(screen)
|
||||
|
||||
// Stop any running animations and reset state
|
||||
showAnimation.stop()
|
||||
hideAnimation.stop()
|
||||
autoHideTimer.stop()
|
||||
|
||||
// Ensure we start from the hidden position
|
||||
y = hiddenY
|
||||
visible = true
|
||||
|
||||
// Start the show animation
|
||||
showAnimation.start()
|
||||
if (duration > 0 && !persistent) {
|
||||
autoHideTimer.start()
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
hideAnimation.start()
|
||||
}
|
||||
|
||||
// Auto-hide timer
|
||||
Timer {
|
||||
id: autoHideTimer
|
||||
interval: root.duration
|
||||
onTriggered: hide()
|
||||
}
|
||||
|
||||
// Show animation
|
||||
PropertyAnimation {
|
||||
id: showAnimation
|
||||
target: root
|
||||
property: "y"
|
||||
to: targetY
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
|
||||
// Hide animation
|
||||
PropertyAnimation {
|
||||
id: hideAnimation
|
||||
target: root
|
||||
property: "y"
|
||||
to: hiddenY
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InCubic
|
||||
onFinished: {
|
||||
root.visible = false
|
||||
root.dismissed()
|
||||
}
|
||||
}
|
||||
|
||||
// Main toast container
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Style.radiusL * scaling
|
||||
|
||||
// Clean surface background
|
||||
color: Color.mSurface
|
||||
|
||||
// Simple colored border all around
|
||||
border.color: {
|
||||
switch (root.type) {
|
||||
case "warning":
|
||||
return Color.mError
|
||||
case "notice":
|
||||
return Color.mPrimary
|
||||
default:
|
||||
return Color.mOutline
|
||||
}
|
||||
}
|
||||
border.width: Math.max(2, Style.borderM * scaling)
|
||||
|
||||
RowLayout {
|
||||
id: contentLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL * scaling
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
// Icon
|
||||
NIcon {
|
||||
id: icon
|
||||
icon: (root.type == "warning") ? "toast-warning" : "toast-notice"
|
||||
color: {
|
||||
switch (root.type) {
|
||||
case "warning":
|
||||
return Color.mError
|
||||
case "notice":
|
||||
return Color.mPrimary
|
||||
default:
|
||||
return Color.mPrimary
|
||||
}
|
||||
}
|
||||
|
||||
font.pointSize: Style.fontSizeXXL * 1.5 * scaling // 150% size to cover two lines
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
// Label and description
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS * scaling
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
NText {
|
||||
Layout.fillWidth: true
|
||||
text: root.label
|
||||
color: Color.mOnSurface
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
wrapMode: Text.WordWrap
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
NText {
|
||||
Layout.fillWidth: true
|
||||
text: root.description
|
||||
color: Color.mOnSurface
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
wrapMode: Text.WordWrap
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
// Close button (only if persistent or manual dismiss needed)
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
visible: root.persistent || root.duration === 0
|
||||
|
||||
colorBg: Color.mSurfaceVariant
|
||||
colorFg: Color.mOnSurface
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.mOutline
|
||||
|
||||
sizeRatio: 0.8
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
onClicked: hide()
|
||||
}
|
||||
}
|
||||
|
||||
// Click to dismiss (if not persistent)
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: !root.persistent
|
||||
onClicked: hide()
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
}
|
||||
|
||||
// Initial state
|
||||
Component.onCompleted: {
|
||||
visible = false
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ RowLayout {
|
|||
property string description: ""
|
||||
property bool checked: false
|
||||
property bool hovering: false
|
||||
property int baseSize: Style.baseWidgetSize
|
||||
property int baseSize: Math.round(Style.baseWidgetSize * 0.8)
|
||||
|
||||
signal toggled(bool checked)
|
||||
signal entered
|
||||
|
|
@ -27,12 +27,12 @@ RowLayout {
|
|||
Rectangle {
|
||||
id: switcher
|
||||
|
||||
implicitWidth: root.baseSize * 1.625 * scaling
|
||||
implicitHeight: root.baseSize * scaling
|
||||
implicitWidth: Math.round(root.baseSize * 1.625 * scaling)
|
||||
implicitHeight: Math.round(root.baseSize * scaling)
|
||||
radius: height * 0.5
|
||||
color: root.checked ? Color.mPrimary : Color.mSurface
|
||||
border.color: root.checked ? Color.mPrimary : Color.mOutline
|
||||
border.width: Math.max(1, Style.borderM * scaling)
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
|
|
@ -47,14 +47,15 @@ RowLayout {
|
|||
}
|
||||
|
||||
Rectangle {
|
||||
implicitWidth: (root.baseSize - 5) * scaling
|
||||
implicitHeight: (root.baseSize - 5) * scaling
|
||||
implicitWidth: Math.round((root.baseSize * 0.8) * scaling)
|
||||
implicitHeight: Math.round((root.baseSize * 0.8) * scaling)
|
||||
radius: height * 0.5
|
||||
color: root.checked ? Color.mOnPrimary : Color.mPrimary
|
||||
border.color: root.checked ? Color.mSurface : Color.mSurface
|
||||
border.width: Math.max(1, Style.borderM * scaling)
|
||||
y: 2 * scaling
|
||||
x: root.checked ? switcher.width - width - 2 * scaling : 2 * scaling
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: 0
|
||||
x: root.checked ? switcher.width - width - 3 * scaling : 3 * scaling
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ Window {
|
|||
property bool positionLeft: false
|
||||
property bool positionRight: false
|
||||
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
|
||||
flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
|
||||
color: Color.transparent
|
||||
visible: false
|
||||
|
|
@ -46,17 +48,34 @@ Window {
|
|||
return
|
||||
}
|
||||
|
||||
if (positionLeft) {
|
||||
// Auto-detect positioning based on bar position if not explicitly set
|
||||
var shouldPositionLeft = positionLeft
|
||||
var shouldPositionRight = positionRight
|
||||
var shouldPositionAbove = positionAbove
|
||||
|
||||
// If no explicit positioning is set, auto-detect based on bar position
|
||||
if (!positionLeft && !positionRight && !positionAbove) {
|
||||
if (barPosition === "left") {
|
||||
shouldPositionRight = true
|
||||
} else if (barPosition === "right") {
|
||||
shouldPositionLeft = true
|
||||
} else if (barPosition === "bottom") {
|
||||
shouldPositionAbove = true
|
||||
}
|
||||
// For "top" bar, default to below (no change needed)
|
||||
}
|
||||
|
||||
if (shouldPositionLeft) {
|
||||
// Position tooltip to the left of the target
|
||||
var pos = target.mapToGlobal(0, 0)
|
||||
x = pos.x - width - 12 // 12 px margin to the left
|
||||
y = pos.y - height / 2 + target.height / 2
|
||||
} else if (positionRight) {
|
||||
} else if (shouldPositionRight) {
|
||||
// Position tooltip to the right of the target
|
||||
var pos = target.mapToGlobal(target.width, 0)
|
||||
x = pos.x + 12 // 12 px margin to the right
|
||||
y = pos.y - height / 2 + target.height / 2
|
||||
} else if (positionAbove) {
|
||||
} else if (shouldPositionAbove) {
|
||||
// Position tooltip above the target
|
||||
var pos = target.mapToGlobal(0, 0)
|
||||
x = pos.x - width / 2 + target.width / 2
|
||||
|
|
|
|||
48
Widgets/NValueSlider.qml
Normal file
48
Widgets/NValueSlider.qml
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
property real from: 0
|
||||
property real to: 1
|
||||
property real value: 0
|
||||
property real stepSize: 0.01
|
||||
property var cutoutColor: Color.mSurface
|
||||
property bool snapAlways: true
|
||||
property real heightRatio: 0.75
|
||||
property string text: ""
|
||||
|
||||
// Signals
|
||||
signal moved(real value)
|
||||
signal pressedChanged(bool pressed, real value)
|
||||
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
NSlider {
|
||||
id: slider
|
||||
Layout.fillWidth: true
|
||||
from: root.from
|
||||
to: root.to
|
||||
value: root.value
|
||||
stepSize: root.stepSize
|
||||
cutoutColor: root.cutoutColor
|
||||
snapAlways: root.snapAlways
|
||||
heightRatio: root.heightRatio
|
||||
onMoved: root.moved(value)
|
||||
onPressedChanged: root.pressedChanged(pressed, value)
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: root.text !== ""
|
||||
text: root.text
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: 40 * scaling
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue