Merge tag 'v2.9.2'

Release v2.9.2
This commit is contained in:
Never Gude 2025-09-16 13:24:39 +02:00
commit 7b1a5d2eb2
150 changed files with 6907 additions and 5080 deletions

View file

@ -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

View file

@ -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
View 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
}
}

View file

@ -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]
}

View file

@ -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
View 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 !== ""
}
}

View file

@ -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
View 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
}
}
}
}
}
}

View file

@ -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
}

View file

@ -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
View 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
View 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
View 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
}
}
}
}
}

View 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()
}
})
}
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}

View file

@ -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 {

View file

@ -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
View 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
}
}