Vertical Bar!
This commit is contained in:
commit
0c6aea7154
49 changed files with 3140 additions and 1154 deletions
|
|
@ -106,6 +106,7 @@ Singleton {
|
||||||
"settings-wallpaper-selector": "library-photo",
|
"settings-wallpaper-selector": "library-photo",
|
||||||
"settings-screen-recorder": "video",
|
"settings-screen-recorder": "video",
|
||||||
"settings-hooks": "link",
|
"settings-hooks": "link",
|
||||||
|
"settings-notification": "bell",
|
||||||
"settings-about": "info-square-rounded",
|
"settings-about": "info-square-rounded",
|
||||||
"bluetooth": "bluetooth",
|
"bluetooth": "bluetooth",
|
||||||
"bt-device-generic": "bluetooth",
|
"bt-device-generic": "bluetooth",
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,9 @@ QtObject {
|
||||||
"united states": "us",
|
"united states": "us",
|
||||||
"us english": "us",
|
"us english": "us",
|
||||||
"british": "gb",
|
"british": "gb",
|
||||||
"uk": "gb",
|
"uk": "ua",
|
||||||
"united kingdom": "gb",
|
"united kingdom"// FIXED: Ukrainian language code should map to Ukraine
|
||||||
|
: "gb",
|
||||||
"english (uk)": "gb",
|
"english (uk)": "gb",
|
||||||
"canadian": "ca",
|
"canadian": "ca",
|
||||||
"canada": "ca",
|
"canada": "ca",
|
||||||
|
|
@ -91,7 +92,9 @@ QtObject {
|
||||||
"slovak": "sk",
|
"slovak": "sk",
|
||||||
"slovenčina": "sk",
|
"slovenčina": "sk",
|
||||||
"slovakia": "sk",
|
"slovakia": "sk",
|
||||||
"ukrainian": "ua",
|
"uk": "ua",
|
||||||
|
"ukrainian"// Ukrainian language code
|
||||||
|
: "ua",
|
||||||
"українська": "ua",
|
"українська": "ua",
|
||||||
"ukraine": "ua",
|
"ukraine": "ua",
|
||||||
"bulgarian": "bg",
|
"bulgarian": "bg",
|
||||||
|
|
|
||||||
|
|
@ -263,7 +263,7 @@ Singleton {
|
||||||
|
|
||||||
// bar
|
// bar
|
||||||
property JsonObject bar: JsonObject {
|
property JsonObject bar: JsonObject {
|
||||||
property string position: "top" // "top" or "bottom"
|
property string position: "top" // "top", "bottom", "left", or "right"
|
||||||
property real backgroundOpacity: 1.0
|
property real backgroundOpacity: 1.0
|
||||||
property list<string> monitors: []
|
property list<string> monitors: []
|
||||||
|
|
||||||
|
|
@ -399,6 +399,10 @@ Singleton {
|
||||||
property list<string> monitors: []
|
property list<string> monitors: []
|
||||||
// Last time the user opened the notification history (ms since epoch)
|
// Last time the user opened the notification history (ms since epoch)
|
||||||
property real lastSeenTs: 0
|
property real lastSeenTs: 0
|
||||||
|
// Duration settings for different urgency levels (in seconds)
|
||||||
|
property int lowUrgencyDuration: 3
|
||||||
|
property int normalUrgencyDuration: 8
|
||||||
|
property int criticalUrgencyDuration: 15
|
||||||
}
|
}
|
||||||
|
|
||||||
// audio
|
// audio
|
||||||
|
|
|
||||||
|
|
@ -66,9 +66,9 @@ Singleton {
|
||||||
property int animationSlowest: Math.round(750 / Settings.data.general.animationSpeed)
|
property int animationSlowest: Math.round(750 / Settings.data.general.animationSpeed)
|
||||||
|
|
||||||
// Dimensions
|
// Dimensions
|
||||||
property int barHeight: 36
|
property int barHeight: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 40 : 36
|
||||||
property int capsuleHeight: (barHeight * 0.73)
|
property int capsuleHeight: (barHeight * 0.73)
|
||||||
property int baseWidgetSize: 32
|
property int baseWidgetSize: (barHeight * 0.9)
|
||||||
property int sliderWidth: 200
|
property int sliderWidth: 200
|
||||||
|
|
||||||
// Delays
|
// Delays
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,8 @@ Loader {
|
||||||
margins {
|
margins {
|
||||||
top: ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "top" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
|
top: ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "top" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
|
||||||
bottom: ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "bottom" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
|
bottom: ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "bottom" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
|
||||||
|
left: ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "left" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
|
||||||
|
right: ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "right" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
mask: Region {}
|
mask: Region {}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import Qt5Compat.GraphicalEffects
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import Quickshell.Services.UPower
|
import Quickshell.Services.UPower
|
||||||
|
|
@ -34,29 +35,30 @@ Variants {
|
||||||
|
|
||||||
WlrLayershell.namespace: "noctalia-bar"
|
WlrLayershell.namespace: "noctalia-bar"
|
||||||
|
|
||||||
implicitHeight: Math.round(Style.barHeight * scaling)
|
implicitHeight: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? screen.height : Math.round(Style.barHeight * scaling)
|
||||||
|
implicitWidth: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? Math.round(Style.barHeight * scaling) : screen.width
|
||||||
color: Color.transparent
|
color: Color.transparent
|
||||||
|
|
||||||
anchors {
|
anchors {
|
||||||
top: Settings.data.bar.position === "top"
|
top: Settings.data.bar.position === "top" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
|
||||||
bottom: Settings.data.bar.position === "bottom"
|
bottom: Settings.data.bar.position === "bottom" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
|
||||||
left: true
|
left: Settings.data.bar.position === "left" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
|
||||||
right: true
|
right: Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Floating bar margins - only apply when floating is enabled
|
// Floating bar margins - only apply when floating is enabled
|
||||||
margins {
|
margins {
|
||||||
top: Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0
|
top: Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
|
||||||
bottom: Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0
|
bottom: Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
|
||||||
left: Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0
|
left: Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
|
||||||
right: Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0
|
right: Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
// Background fill
|
// Background fill with shadow
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: bar
|
id: bar
|
||||||
|
|
||||||
|
|
@ -67,88 +69,181 @@ Variants {
|
||||||
radius: Settings.data.bar.floating ? Style.radiusL : 0
|
radius: Settings.data.bar.floating ? Style.radiusL : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------
|
// For vertical bars, use a single column layout
|
||||||
// Left Section - Dynamic Widgets
|
Loader {
|
||||||
Row {
|
id: verticalBarLayout
|
||||||
id: leftSection
|
anchors.fill: parent
|
||||||
objectName: "leftSection"
|
visible: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
|
||||||
|
sourceComponent: verticalBarComponent
|
||||||
|
}
|
||||||
|
|
||||||
height: parent.height
|
// For horizontal bars, use the original three-section layout
|
||||||
anchors.left: parent.left
|
Loader {
|
||||||
anchors.leftMargin: Style.marginS * scaling
|
id: horizontalBarLayout
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.fill: parent
|
||||||
spacing: Style.marginS * scaling
|
visible: Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
|
||||||
|
sourceComponent: horizontalBarComponent
|
||||||
|
}
|
||||||
|
|
||||||
Repeater {
|
// Main layout components
|
||||||
model: Settings.data.bar.widgets.left
|
Component {
|
||||||
delegate: NWidgetLoader {
|
id: verticalBarComponent
|
||||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
Item {
|
||||||
widgetProps: {
|
anchors.fill: parent
|
||||||
"screen": root.modelData || null,
|
|
||||||
"scaling": ScalingService.getScreenScale(screen),
|
// Top section (left widgets)
|
||||||
"widgetId": modelData.id,
|
Column {
|
||||||
"section": parent.objectName.replace("Section", "").toLowerCase(),
|
spacing: Style.marginS * root.scaling
|
||||||
"sectionWidgetIndex": index,
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
"sectionWidgetsCount": Settings.data.bar.widgets.left.length
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: Style.marginM * root.scaling
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Settings.data.bar.widgets.left
|
||||||
|
delegate: NWidgetLoader {
|
||||||
|
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||||
|
widgetProps: {
|
||||||
|
"screen": root.modelData || null,
|
||||||
|
"scaling": ScalingService.getScreenScale(screen),
|
||||||
|
"widgetId": modelData.id,
|
||||||
|
"section": "left",
|
||||||
|
"sectionWidgetIndex": index,
|
||||||
|
"sectionWidgetsCount": Settings.data.bar.widgets.left.length
|
||||||
|
}
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center section (center widgets)
|
||||||
|
Column {
|
||||||
|
spacing: Style.marginS * root.scaling
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Settings.data.bar.widgets.center
|
||||||
|
delegate: NWidgetLoader {
|
||||||
|
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||||
|
widgetProps: {
|
||||||
|
"screen": root.modelData || null,
|
||||||
|
"scaling": ScalingService.getScreenScale(screen),
|
||||||
|
"widgetId": modelData.id,
|
||||||
|
"section": "center",
|
||||||
|
"sectionWidgetIndex": index,
|
||||||
|
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
|
||||||
|
}
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom section (right widgets)
|
||||||
|
Column {
|
||||||
|
spacing: Style.marginS * root.scaling
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: Style.marginM * root.scaling
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Settings.data.bar.widgets.right
|
||||||
|
delegate: NWidgetLoader {
|
||||||
|
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||||
|
widgetProps: {
|
||||||
|
"screen": root.modelData || null,
|
||||||
|
"scaling": ScalingService.getScreenScale(screen),
|
||||||
|
"widgetId": modelData.id,
|
||||||
|
"section": "right",
|
||||||
|
"sectionWidgetIndex": index,
|
||||||
|
"sectionWidgetsCount": Settings.data.bar.widgets.right.length
|
||||||
|
}
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------
|
Component {
|
||||||
// Center Section - Dynamic Widgets
|
id: horizontalBarComponent
|
||||||
Row {
|
Item {
|
||||||
id: centerSection
|
anchors.fill: parent
|
||||||
objectName: "centerSection"
|
|
||||||
|
|
||||||
height: parent.height
|
// Left Section
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
RowLayout {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
id: leftSection
|
||||||
spacing: Style.marginS * scaling
|
objectName: "leftSection"
|
||||||
|
anchors.left: parent.left
|
||||||
Repeater {
|
anchors.leftMargin: Style.marginS * root.scaling
|
||||||
model: Settings.data.bar.widgets.center
|
|
||||||
delegate: NWidgetLoader {
|
|
||||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
|
||||||
widgetProps: {
|
|
||||||
"screen": root.modelData || null,
|
|
||||||
"scaling": ScalingService.getScreenScale(screen),
|
|
||||||
"widgetId": modelData.id,
|
|
||||||
"section": parent.objectName.replace("Section", "").toLowerCase(),
|
|
||||||
"sectionWidgetIndex": index,
|
|
||||||
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Style.marginS * root.scaling
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Settings.data.bar.widgets.left
|
||||||
|
delegate: NWidgetLoader {
|
||||||
|
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||||
|
widgetProps: {
|
||||||
|
"screen": root.modelData || null,
|
||||||
|
"scaling": ScalingService.getScreenScale(screen),
|
||||||
|
"widgetId": modelData.id,
|
||||||
|
"section": "left",
|
||||||
|
"sectionWidgetIndex": index,
|
||||||
|
"sectionWidgetsCount": Settings.data.bar.widgets.left.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------
|
// Center Section
|
||||||
// Right Section - Dynamic Widgets
|
RowLayout {
|
||||||
Row {
|
id: centerSection
|
||||||
id: rightSection
|
objectName: "centerSection"
|
||||||
objectName: "rightSection"
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
height: parent.height
|
|
||||||
anchors.right: bar.right
|
|
||||||
anchors.rightMargin: Style.marginS * scaling
|
|
||||||
anchors.verticalCenter: bar.verticalCenter
|
|
||||||
spacing: Style.marginS * scaling
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: Settings.data.bar.widgets.right
|
|
||||||
delegate: NWidgetLoader {
|
|
||||||
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
|
||||||
widgetProps: {
|
|
||||||
"screen": root.modelData || null,
|
|
||||||
"scaling": ScalingService.getScreenScale(screen),
|
|
||||||
"widgetId": modelData.id,
|
|
||||||
"section": parent.objectName.replace("Section", "").toLowerCase(),
|
|
||||||
"sectionWidgetIndex": index,
|
|
||||||
"sectionWidgetsCount": Settings.data.bar.widgets.right.length
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Style.marginS * root.scaling
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Settings.data.bar.widgets.center
|
||||||
|
delegate: NWidgetLoader {
|
||||||
|
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||||
|
widgetProps: {
|
||||||
|
"screen": root.modelData || null,
|
||||||
|
"scaling": ScalingService.getScreenScale(screen),
|
||||||
|
"widgetId": modelData.id,
|
||||||
|
"section": "center",
|
||||||
|
"sectionWidgetIndex": index,
|
||||||
|
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right Section
|
||||||
|
RowLayout {
|
||||||
|
id: rightSection
|
||||||
|
objectName: "rightSection"
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Style.marginS * root.scaling
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Style.marginS * root.scaling
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Settings.data.bar.widgets.right
|
||||||
|
delegate: NWidgetLoader {
|
||||||
|
widgetId: (modelData.id !== undefined ? modelData.id : "")
|
||||||
|
widgetProps: {
|
||||||
|
"screen": root.modelData || null,
|
||||||
|
"scaling": ScalingService.getScreenScale(screen),
|
||||||
|
"widgetId": modelData.id,
|
||||||
|
"section": "right",
|
||||||
|
"sectionWidgetIndex": index,
|
||||||
|
"sectionWidgetsCount": Settings.data.bar.widgets.right.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
RowLayout {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
property ShellScreen screen
|
property ShellScreen screen
|
||||||
property real scaling: 1.0
|
property real scaling: 1.0
|
||||||
|
|
@ -36,6 +36,10 @@ RowLayout {
|
||||||
readonly property real minWidth: Math.max(1, screen.width * 0.06)
|
readonly property real minWidth: Math.max(1, screen.width * 0.06)
|
||||||
readonly property real maxWidth: minWidth * 2
|
readonly property real maxWidth: minWidth * 2
|
||||||
|
|
||||||
|
readonly property string barPosition: Settings.data.bar.position
|
||||||
|
implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)
|
||||||
|
implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling)
|
||||||
|
|
||||||
function getTitle() {
|
function getTitle() {
|
||||||
try {
|
try {
|
||||||
return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : ""
|
return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : ""
|
||||||
|
|
@ -45,10 +49,33 @@ RowLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
spacing: Style.marginS * scaling
|
|
||||||
visible: getTitle() !== ""
|
visible: getTitle() !== ""
|
||||||
|
|
||||||
|
function calculatedVerticalHeight() {
|
||||||
|
// Use standard widget height like other widgets
|
||||||
|
return Math.round(Style.capsuleHeight * scaling)
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculatedHorizontalWidth() {
|
||||||
|
let total = Style.marginM * 2 * scaling // internal padding
|
||||||
|
|
||||||
|
if (showIcon) {
|
||||||
|
total += Style.baseWidgetSize * 0.5 * scaling + 2 * scaling // icon + spacing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate actual text width more accurately
|
||||||
|
const title = getTitle()
|
||||||
|
if (title !== "") {
|
||||||
|
// Estimate text width: average character width * number of characters
|
||||||
|
const avgCharWidth = Style.fontSizeS * scaling * 0.6 // rough estimate
|
||||||
|
const titleWidth = Math.min(title.length * avgCharWidth, 80 * scaling)
|
||||||
|
total += titleWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row layout handles spacing between widgets
|
||||||
|
return Math.max(total, Style.capsuleHeight * scaling) // Minimum width
|
||||||
|
}
|
||||||
|
|
||||||
function getAppIcon() {
|
function getAppIcon() {
|
||||||
try {
|
try {
|
||||||
// Try CompositorService first
|
// Try CompositorService first
|
||||||
|
|
@ -102,27 +129,31 @@ RowLayout {
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: windowTitleRect
|
id: windowTitleRect
|
||||||
visible: root.visible
|
visible: root.visible
|
||||||
Layout.preferredWidth: contentLayout.implicitWidth + Style.marginM * 2 * scaling
|
anchors.left: parent.left
|
||||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling)
|
||||||
|
height: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : Math.round(Style.capsuleHeight * scaling)
|
||||||
radius: Math.round(Style.radiusM * scaling)
|
radius: Math.round(Style.radiusM * scaling)
|
||||||
color: Color.mSurfaceVariant
|
color: Color.mSurfaceVariant
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: mainContainer
|
id: mainContainer
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.leftMargin: Style.marginS * scaling
|
anchors.leftMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
|
||||||
anchors.rightMargin: Style.marginS * scaling
|
anchors.rightMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
|
// Horizontal layout for top/bottom bars
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: contentLayout
|
id: horizontalLayout
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: Style.marginS * scaling
|
spacing: 2 * scaling
|
||||||
|
visible: barPosition === "top" || barPosition === "bottom"
|
||||||
|
|
||||||
// Window icon
|
// Window icon
|
||||||
Item {
|
Item {
|
||||||
Layout.preferredWidth: Style.fontSizeL * scaling * 1.2
|
Layout.preferredWidth: Style.baseWidgetSize * 0.5 * scaling
|
||||||
Layout.preferredHeight: Style.fontSizeL * scaling * 1.2
|
Layout.preferredHeight: Style.baseWidgetSize * 0.5 * scaling
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
visible: getTitle() !== "" && showIcon
|
visible: getTitle() !== "" && showIcon
|
||||||
|
|
||||||
|
|
@ -150,11 +181,11 @@ RowLayout {
|
||||||
if (mouseArea.containsMouse) {
|
if (mouseArea.containsMouse) {
|
||||||
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
|
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
|
||||||
} else {
|
} else {
|
||||||
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling))
|
return Math.round(Math.min(fullTitleMetrics.contentWidth, 80 * scaling)) // Limited width for horizontal bars
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.warn("ActiveWindow", "Error calculating width:", e)
|
Logger.warn("ActiveWindow", "Error calculating width:", e)
|
||||||
return root.minWidth * scaling
|
return 80 * scaling
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
@ -176,12 +207,65 @@ RowLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vertical layout for left/right bars - icon only
|
||||||
|
Item {
|
||||||
|
id: verticalLayout
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Style.marginXS * scaling * 2
|
||||||
|
height: parent.height - Style.marginXS * scaling * 2
|
||||||
|
visible: barPosition === "left" || barPosition === "right"
|
||||||
|
|
||||||
|
// Window icon
|
||||||
|
Item {
|
||||||
|
width: Style.baseWidgetSize * 0.5 * scaling
|
||||||
|
height: Style.baseWidgetSize * 0.5 * scaling
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: getTitle() !== "" && showIcon
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: windowIconVertical
|
||||||
|
anchors.fill: parent
|
||||||
|
source: getAppIcon()
|
||||||
|
asynchronous: true
|
||||||
|
smooth: true
|
||||||
|
visible: source !== ""
|
||||||
|
|
||||||
|
// Handle loading errors gracefully
|
||||||
|
onStatusChanged: {
|
||||||
|
if (status === Image.Error) {
|
||||||
|
Logger.warn("ActiveWindow", "Failed to load icon:", source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Mouse area for hover detection
|
// Mouse area for hover detection
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: mouseArea
|
id: mouseArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onEntered: {
|
||||||
|
if (barPosition === "left" || barPosition === "right") {
|
||||||
|
tooltip.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onExited: {
|
||||||
|
if (barPosition === "left" || barPosition === "right") {
|
||||||
|
tooltip.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hover tooltip with full title (only for vertical bars)
|
||||||
|
NTooltip {
|
||||||
|
id: tooltip
|
||||||
|
target: verticalLayout
|
||||||
|
text: getTitle()
|
||||||
|
positionLeft: barPosition === "right"
|
||||||
|
positionRight: barPosition === "left"
|
||||||
|
delay: 500
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -191,6 +275,7 @@ RowLayout {
|
||||||
function onActiveWindowChanged() {
|
function onActiveWindowChanged() {
|
||||||
try {
|
try {
|
||||||
windowIcon.source = Qt.binding(getAppIcon)
|
windowIcon.source = Qt.binding(getAppIcon)
|
||||||
|
windowIconVertical.source = Qt.binding(getAppIcon)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.warn("ActiveWindow", "Error in onActiveWindowChanged:", e)
|
Logger.warn("ActiveWindow", "Error in onActiveWindowChanged:", e)
|
||||||
}
|
}
|
||||||
|
|
@ -198,6 +283,7 @@ RowLayout {
|
||||||
function onWindowListChanged() {
|
function onWindowListChanged() {
|
||||||
try {
|
try {
|
||||||
windowIcon.source = Qt.binding(getAppIcon)
|
windowIcon.source = Qt.binding(getAppIcon)
|
||||||
|
windowIconVertical.source = Qt.binding(getAppIcon)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.warn("ActiveWindow", "Error in onWindowListChanged:", e)
|
Logger.warn("ActiveWindow", "Error in onWindowListChanged:", e)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,13 +28,19 @@ Rectangle {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property string barPosition: Settings.data.bar.position
|
||||||
|
|
||||||
// Resolve settings: try user settings or defaults from BarWidgetRegistry
|
// Resolve settings: try user settings or defaults from BarWidgetRegistry
|
||||||
readonly property bool use12h: widgetSettings.use12HourClock !== undefined ? widgetSettings.use12HourClock : widgetMetadata.use12HourClock
|
readonly property bool use12h: widgetSettings.use12HourClock !== undefined ? widgetSettings.use12HourClock : widgetMetadata.use12HourClock
|
||||||
readonly property bool reverseDayMonth: widgetSettings.reverseDayMonth !== undefined ? widgetSettings.reverseDayMonth : widgetMetadata.reverseDayMonth
|
readonly property bool reverseDayMonth: widgetSettings.reverseDayMonth !== undefined ? widgetSettings.reverseDayMonth : widgetMetadata.reverseDayMonth
|
||||||
readonly property string displayFormat: widgetSettings.displayFormat !== undefined ? widgetSettings.displayFormat : widgetMetadata.displayFormat
|
readonly property string displayFormat: widgetSettings.displayFormat !== undefined ? widgetSettings.displayFormat : widgetMetadata.displayFormat
|
||||||
|
|
||||||
implicitWidth: Math.round(layout.implicitWidth + Style.marginM * 2 * scaling)
|
// Use compact mode for vertical bars
|
||||||
implicitHeight: Math.round(Style.capsuleHeight * scaling)
|
readonly property bool useCompactMode: barPosition === "left" || barPosition === "right"
|
||||||
|
|
||||||
|
implicitWidth: useCompactMode ? Math.round(Style.capsuleHeight * scaling) : Math.round(layout.implicitWidth + Style.marginM * 2 * scaling)
|
||||||
|
implicitHeight: useCompactMode ? Math.round(Style.capsuleHeight * 2.5 * scaling) : Math.round(Style.capsuleHeight * scaling)
|
||||||
|
|
||||||
radius: Math.round(Style.radiusS * scaling)
|
radius: Math.round(Style.radiusS * scaling)
|
||||||
color: Color.mSurfaceVariant
|
color: Color.mSurfaceVariant
|
||||||
|
|
||||||
|
|
@ -46,66 +52,133 @@ Rectangle {
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: layout
|
id: layout
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: -3 * scaling
|
spacing: useCompactMode ? -2 * scaling : -3 * scaling
|
||||||
|
|
||||||
// First line
|
// Compact mode for vertical bars - Time section (HH, MM)
|
||||||
NText {
|
Repeater {
|
||||||
readonly property bool showSeconds: (displayFormat === "time-seconds")
|
model: useCompactMode ? 2 : 1
|
||||||
readonly property bool inlineDate: (displayFormat === "time-date")
|
NText {
|
||||||
|
readonly property bool showSeconds: (displayFormat === "time-seconds")
|
||||||
|
readonly property bool inlineDate: (displayFormat === "time-date")
|
||||||
|
readonly property var now: Time.date
|
||||||
|
|
||||||
text: {
|
text: {
|
||||||
const now = Time.date
|
if (useCompactMode) {
|
||||||
let timeStr = ""
|
// Compact mode: time section (first 2 lines)
|
||||||
|
switch (index) {
|
||||||
if (use12h) {
|
case 0:
|
||||||
// 12-hour format with proper padding and consistent spacing
|
// Hours
|
||||||
const hours = now.getHours()
|
if (use12h) {
|
||||||
const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours)
|
const hours = now.getHours()
|
||||||
const paddedHours = displayHours.toString().padStart(2, '0')
|
const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours)
|
||||||
const minutes = now.getMinutes().toString().padStart(2, '0')
|
return displayHours.toString().padStart(2, '0')
|
||||||
const ampm = hours < 12 ? 'AM' : 'PM'
|
} else {
|
||||||
|
return now.getHours().toString().padStart(2, '0')
|
||||||
if (showSeconds) {
|
}
|
||||||
const seconds = now.getSeconds().toString().padStart(2, '0')
|
case 1:
|
||||||
timeStr = `${paddedHours}:${minutes}:${seconds} ${ampm}`
|
// Minutes
|
||||||
|
return now.getMinutes().toString().padStart(2, '0')
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
timeStr = `${paddedHours}:${minutes} ${ampm}`
|
// Normal mode: single line with time
|
||||||
}
|
let timeStr = ""
|
||||||
} else {
|
|
||||||
// 24-hour format with padding
|
|
||||||
const hours = now.getHours().toString().padStart(2, '0')
|
|
||||||
const minutes = now.getMinutes().toString().padStart(2, '0')
|
|
||||||
|
|
||||||
if (showSeconds) {
|
if (use12h) {
|
||||||
const seconds = now.getSeconds().toString().padStart(2, '0')
|
// 12-hour format with proper padding and consistent spacing
|
||||||
timeStr = `${hours}:${minutes}:${seconds}`
|
const hours = now.getHours()
|
||||||
} else {
|
const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours)
|
||||||
timeStr = `${hours}:${minutes}`
|
const paddedHours = displayHours.toString().padStart(2, '0')
|
||||||
|
const minutes = now.getMinutes().toString().padStart(2, '0')
|
||||||
|
const ampm = hours < 12 ? 'AM' : 'PM'
|
||||||
|
|
||||||
|
if (showSeconds) {
|
||||||
|
const seconds = now.getSeconds().toString().padStart(2, '0')
|
||||||
|
timeStr = `${paddedHours}:${minutes}:${seconds} ${ampm}`
|
||||||
|
} else {
|
||||||
|
timeStr = `${paddedHours}:${minutes} ${ampm}`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 24-hour format with padding
|
||||||
|
const hours = now.getHours().toString().padStart(2, '0')
|
||||||
|
const minutes = now.getMinutes().toString().padStart(2, '0')
|
||||||
|
|
||||||
|
if (showSeconds) {
|
||||||
|
const seconds = now.getSeconds().toString().padStart(2, '0')
|
||||||
|
timeStr = `${hours}:${minutes}:${seconds}`
|
||||||
|
} else {
|
||||||
|
timeStr = `${hours}:${minutes}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add inline date if needed
|
||||||
|
if (inlineDate) {
|
||||||
|
let dayName = now.toLocaleDateString(Qt.locale(), "ddd")
|
||||||
|
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
|
||||||
|
const day = now.getDate().toString().padStart(2, '0')
|
||||||
|
let month = now.toLocaleDateString(Qt.locale(), "MMM")
|
||||||
|
timeStr += " - " + (reverseDayMonth ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeStr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add inline date if needed
|
//font.family: Settings.data.ui.fontFixed
|
||||||
if (inlineDate) {
|
font.pointSize: useCompactMode ? Style.fontSizeXXS * scaling : Style.fontSizeXS * scaling
|
||||||
let dayName = now.toLocaleDateString(Qt.locale(), "ddd")
|
font.weight: Style.fontWeightBold
|
||||||
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
|
color: Color.mPrimary
|
||||||
const day = now.getDate().toString().padStart(2, '0')
|
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||||
let month = now.toLocaleDateString(Qt.locale(), "MMM")
|
|
||||||
timeStr += " - " + (reverseDayMonth ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return timeStr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//font.family: Settings.data.ui.fontFixed
|
|
||||||
font.pointSize: Style.fontSizeXS * scaling
|
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mPrimary
|
|
||||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second line
|
// Separator line for compact mode (between time and date)
|
||||||
|
Rectangle {
|
||||||
|
visible: useCompactMode
|
||||||
|
Layout.preferredWidth: 20 * scaling
|
||||||
|
Layout.preferredHeight: 2 * scaling
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.topMargin: 3 * scaling
|
||||||
|
Layout.bottomMargin: 3 * scaling
|
||||||
|
color: Color.mPrimary
|
||||||
|
opacity: 0.3
|
||||||
|
radius: 1 * scaling
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compact mode for vertical bars - Date section (DD, MM)
|
||||||
|
Repeater {
|
||||||
|
model: useCompactMode ? 2 : 0
|
||||||
|
NText {
|
||||||
|
readonly property var now: Time.date
|
||||||
|
|
||||||
|
text: {
|
||||||
|
if (useCompactMode) {
|
||||||
|
// Compact mode: date section (last 2 lines)
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
// Day
|
||||||
|
return now.getDate().toString().padStart(2, '0')
|
||||||
|
case 1:
|
||||||
|
// Month
|
||||||
|
return (now.getMonth() + 1).toString().padStart(2, '0')
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
font.pointSize: Style.fontSizeXXS * scaling
|
||||||
|
font.weight: Style.fontWeightBold
|
||||||
|
color: Color.mPrimary
|
||||||
|
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second line for normal mode (date)
|
||||||
NText {
|
NText {
|
||||||
visible: (displayFormat === "time-date-short")
|
visible: !useCompactMode && (displayFormat === "time-date-short")
|
||||||
text: {
|
text: {
|
||||||
const now = Time.date
|
const now = Time.date
|
||||||
const day = now.getDate().toString().padStart(2, '0')
|
const day = now.getDate().toString().padStart(2, '0')
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
RowLayout {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property ShellScreen screen
|
property ShellScreen screen
|
||||||
|
|
@ -30,6 +30,8 @@ RowLayout {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property string barPosition: Settings.data.bar.position
|
||||||
|
|
||||||
readonly property bool showAlbumArt: (widgetSettings.showAlbumArt !== undefined) ? widgetSettings.showAlbumArt : widgetMetadata.showAlbumArt
|
readonly property bool showAlbumArt: (widgetSettings.showAlbumArt !== undefined) ? widgetSettings.showAlbumArt : widgetMetadata.showAlbumArt
|
||||||
readonly property bool showVisualizer: (widgetSettings.showVisualizer !== undefined) ? widgetSettings.showVisualizer : widgetMetadata.showVisualizer
|
readonly property bool showVisualizer: (widgetSettings.showVisualizer !== undefined) ? widgetSettings.showVisualizer : widgetMetadata.showVisualizer
|
||||||
readonly property string visualizerType: (widgetSettings.visualizerType !== undefined && widgetSettings.visualizerType !== "") ? widgetSettings.visualizerType : widgetMetadata.visualizerType
|
readonly property string visualizerType: (widgetSettings.visualizerType !== undefined && widgetSettings.visualizerType !== "") ? widgetSettings.visualizerType : widgetMetadata.visualizerType
|
||||||
|
|
@ -42,10 +44,26 @@ RowLayout {
|
||||||
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
|
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
|
||||||
}
|
}
|
||||||
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
function calculatedVerticalHeight() {
|
||||||
spacing: Style.marginS * scaling
|
return Math.round(Style.baseWidgetSize * 0.8 * scaling)
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculatedHorizontalWidth() {
|
||||||
|
let total = Style.marginM * 2 * scaling // internal padding
|
||||||
|
if (showAlbumArt) {
|
||||||
|
total += 18 * scaling + 2 * scaling // album art + spacing
|
||||||
|
} else {
|
||||||
|
total += Style.fontSizeL * scaling + 2 * scaling // icon + spacing
|
||||||
|
}
|
||||||
|
total += Math.min(fullTitleMetrics.contentWidth, maxWidth * scaling) // title text
|
||||||
|
// Row layout handles spacing between widgets
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitHeight: visible ? ((barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)) : 0
|
||||||
|
implicitWidth: visible ? ((barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (rowLayout.implicitWidth + Style.marginM * 2 * scaling)) : 0
|
||||||
|
|
||||||
visible: MediaService.currentPlayer !== null && MediaService.canPlay
|
visible: MediaService.currentPlayer !== null && MediaService.canPlay
|
||||||
Layout.preferredWidth: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0
|
|
||||||
|
|
||||||
// A hidden text element to safely measure the full title width
|
// A hidden text element to safely measure the full title width
|
||||||
NText {
|
NText {
|
||||||
|
|
@ -57,12 +75,12 @@ RowLayout {
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: mediaMini
|
id: mediaMini
|
||||||
|
visible: root.visible
|
||||||
Layout.preferredWidth: rowLayout.implicitWidth + Style.marginM * 2 * scaling
|
anchors.left: parent.left
|
||||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
Layout.alignment: Qt.AlignVCenter
|
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (rowLayout.implicitWidth + Style.marginM * 2 * scaling)
|
||||||
|
height: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : Math.round(Style.capsuleHeight * scaling)
|
||||||
radius: Math.round(Style.radiusM * scaling)
|
radius: (barPosition === "left" || barPosition === "right") ? width / 2 : Math.round(Style.radiusM * scaling)
|
||||||
color: Color.mSurfaceVariant
|
color: Color.mSurfaceVariant
|
||||||
|
|
||||||
// Used to anchor the tooltip, so the tooltip does not move when the content expands
|
// Used to anchor the tooltip, so the tooltip does not move when the content expands
|
||||||
|
|
@ -75,8 +93,8 @@ RowLayout {
|
||||||
Item {
|
Item {
|
||||||
id: mainContainer
|
id: mainContainer
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.leftMargin: Style.marginS * scaling
|
anchors.leftMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
|
||||||
anchors.rightMargin: Style.marginS * scaling
|
anchors.rightMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
@ -123,10 +141,12 @@ RowLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Horizontal layout for top/bottom bars
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: rowLayout
|
id: rowLayout
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
|
visible: barPosition === "top" || barPosition === "bottom"
|
||||||
z: 1 // Above the visualizer
|
z: 1 // Above the visualizer
|
||||||
|
|
||||||
NIcon {
|
NIcon {
|
||||||
|
|
@ -187,6 +207,33 @@ RowLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vertical layout for left/right bars - icon only
|
||||||
|
Item {
|
||||||
|
id: verticalLayout
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Style.marginM * scaling * 2
|
||||||
|
height: parent.height - Style.marginM * scaling * 2
|
||||||
|
visible: barPosition === "left" || barPosition === "right"
|
||||||
|
z: 1 // Above the visualizer
|
||||||
|
|
||||||
|
// Media icon
|
||||||
|
Item {
|
||||||
|
width: Style.baseWidgetSize * 0.5 * scaling
|
||||||
|
height: Style.baseWidgetSize * 0.5 * scaling
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: getTitle() !== ""
|
||||||
|
|
||||||
|
NIcon {
|
||||||
|
id: mediaIconVertical
|
||||||
|
anchors.fill: parent
|
||||||
|
icon: MediaService.isPlaying ? "media-pause" : "media-play"
|
||||||
|
font.pointSize: Style.fontSizeL * scaling
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Mouse area for hover detection
|
// Mouse area for hover detection
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: mouseArea
|
id: mouseArea
|
||||||
|
|
@ -209,12 +256,18 @@ RowLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
onEntered: {
|
onEntered: {
|
||||||
if (tooltip.text !== "") {
|
if (barPosition === "left" || barPosition === "right") {
|
||||||
|
tooltip.show()
|
||||||
|
} else if (tooltip.text !== "") {
|
||||||
tooltip.show()
|
tooltip.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onExited: {
|
onExited: {
|
||||||
tooltip.hide()
|
if (barPosition === "left" || barPosition === "right") {
|
||||||
|
tooltip.hide()
|
||||||
|
} else {
|
||||||
|
tooltip.hide()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -223,16 +276,23 @@ RowLayout {
|
||||||
NTooltip {
|
NTooltip {
|
||||||
id: tooltip
|
id: tooltip
|
||||||
text: {
|
text: {
|
||||||
var str = ""
|
if (barPosition === "left" || barPosition === "right") {
|
||||||
if (MediaService.canGoNext) {
|
return getTitle()
|
||||||
str += "Right click for next.\n"
|
} else {
|
||||||
|
var str = ""
|
||||||
|
if (MediaService.canGoNext) {
|
||||||
|
str += "Right click for next.\n"
|
||||||
|
}
|
||||||
|
if (MediaService.canGoPrevious) {
|
||||||
|
str += "Middle click for previous."
|
||||||
|
}
|
||||||
|
return str
|
||||||
}
|
}
|
||||||
if (MediaService.canGoPrevious) {
|
|
||||||
str += "Middle click for previous."
|
|
||||||
}
|
|
||||||
return str
|
|
||||||
}
|
}
|
||||||
target: anchor
|
target: (barPosition === "left" || barPosition === "right") ? verticalLayout : anchor
|
||||||
|
positionLeft: barPosition === "right"
|
||||||
|
positionRight: barPosition === "left"
|
||||||
positionAbove: Settings.data.bar.position === "bottom"
|
positionAbove: Settings.data.bar.position === "bottom"
|
||||||
|
delay: 500
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
RowLayout {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property ShellScreen screen
|
property ShellScreen screen
|
||||||
|
|
@ -28,6 +28,8 @@ RowLayout {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property string barPosition: Settings.data.bar.position
|
||||||
|
|
||||||
readonly property bool showCpuUsage: (widgetSettings.showCpuUsage !== undefined) ? widgetSettings.showCpuUsage : widgetMetadata.showCpuUsage
|
readonly property bool showCpuUsage: (widgetSettings.showCpuUsage !== undefined) ? widgetSettings.showCpuUsage : widgetMetadata.showCpuUsage
|
||||||
readonly property bool showCpuTemp: (widgetSettings.showCpuTemp !== undefined) ? widgetSettings.showCpuTemp : widgetMetadata.showCpuTemp
|
readonly property bool showCpuTemp: (widgetSettings.showCpuTemp !== undefined) ? widgetSettings.showCpuTemp : widgetMetadata.showCpuTemp
|
||||||
readonly property bool showMemoryUsage: (widgetSettings.showMemoryUsage !== undefined) ? widgetSettings.showMemoryUsage : widgetMetadata.showMemoryUsage
|
readonly property bool showMemoryUsage: (widgetSettings.showMemoryUsage !== undefined) ? widgetSettings.showMemoryUsage : widgetMetadata.showMemoryUsage
|
||||||
|
|
@ -35,22 +37,109 @@ RowLayout {
|
||||||
readonly property bool showNetworkStats: (widgetSettings.showNetworkStats !== undefined) ? widgetSettings.showNetworkStats : widgetMetadata.showNetworkStats
|
readonly property bool showNetworkStats: (widgetSettings.showNetworkStats !== undefined) ? widgetSettings.showNetworkStats : widgetMetadata.showNetworkStats
|
||||||
readonly property bool showDiskUsage: (widgetSettings.showDiskUsage !== undefined) ? widgetSettings.showDiskUsage : widgetMetadata.showDiskUsage
|
readonly property bool showDiskUsage: (widgetSettings.showDiskUsage !== undefined) ? widgetSettings.showDiskUsage : widgetMetadata.showDiskUsage
|
||||||
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)
|
||||||
spacing: Style.marginS * scaling
|
implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : (horizontalLayout.implicitWidth + Style.marginL * 2 * scaling)
|
||||||
|
|
||||||
|
function calculatedVerticalHeight() {
|
||||||
|
let total = 0
|
||||||
|
let visibleCount = 0
|
||||||
|
|
||||||
|
if (showCpuUsage)
|
||||||
|
visibleCount++
|
||||||
|
if (showCpuTemp)
|
||||||
|
visibleCount++
|
||||||
|
if (showMemoryUsage)
|
||||||
|
visibleCount++
|
||||||
|
if (showNetworkStats)
|
||||||
|
visibleCount += 2 // download + upload
|
||||||
|
if (showDiskUsage)
|
||||||
|
visibleCount++
|
||||||
|
|
||||||
|
total = visibleCount * Math.round(Style.capsuleHeight * scaling)
|
||||||
|
total += Math.max(visibleCount - 1, 0) * Style.marginXS * scaling
|
||||||
|
total += Style.marginXS * scaling * 2 // minimal padding to match other widgets
|
||||||
|
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculatedHorizontalWidth() {
|
||||||
|
let total = Style.marginL * scaling * 2.5 // base padding
|
||||||
|
|
||||||
|
if (showCpuUsage) {
|
||||||
|
// Icon + "99%" text
|
||||||
|
total += Style.fontSizeM * scaling * 1.2 + // icon
|
||||||
|
Style.fontSizeXS * scaling * 2.5 + // text (~3 chars)
|
||||||
|
2 * scaling // spacing
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showCpuTemp) {
|
||||||
|
// Icon + "85°C" text
|
||||||
|
total += Style.fontSizeS * scaling * 1.2 + // smaller fire icon
|
||||||
|
Style.fontSizeXS * scaling * 3.5 + // text (~4 chars)
|
||||||
|
2 * scaling // spacing
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showMemoryUsage) {
|
||||||
|
// Icon + "16G" or "85%" text
|
||||||
|
total += Style.fontSizeM * scaling * 1.2 + // icon
|
||||||
|
Style.fontSizeXS * scaling * 3 + // text (~3-4 chars)
|
||||||
|
2 * scaling // spacing
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showNetworkStats) {
|
||||||
|
// Download: icon + "1.2M" text
|
||||||
|
total += Style.fontSizeM * scaling * 1.2 + // icon
|
||||||
|
Style.fontSizeXS * scaling * 3.5 + // text
|
||||||
|
Style.marginXS * scaling + 2 * scaling // spacing
|
||||||
|
|
||||||
|
// Upload: icon + "256K" text
|
||||||
|
total += Style.fontSizeM * scaling * 1.2 + // icon
|
||||||
|
Style.fontSizeXS * scaling * 3.5 + // text
|
||||||
|
Style.marginXS * scaling + 2 * scaling // spacing
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDiskUsage) {
|
||||||
|
// Icon + "75%" text
|
||||||
|
total += Style.fontSizeM * scaling * 1.2 + // icon
|
||||||
|
Style.fontSizeXS * scaling * 3 + // text (~3 chars)
|
||||||
|
Style.marginXS * scaling + 2 * scaling // spacing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add spacing between visible components
|
||||||
|
let visibleCount = 0
|
||||||
|
if (showCpuUsage)
|
||||||
|
visibleCount++
|
||||||
|
if (showCpuTemp)
|
||||||
|
visibleCount++
|
||||||
|
if (showMemoryUsage)
|
||||||
|
visibleCount++
|
||||||
|
if (showNetworkStats)
|
||||||
|
visibleCount += 2
|
||||||
|
if (showDiskUsage)
|
||||||
|
visibleCount++
|
||||||
|
|
||||||
|
if (visibleCount > 1) {
|
||||||
|
total += (visibleCount - 1) * Style.marginXS * scaling
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row layout handles spacing between widgets
|
||||||
|
return Math.max(total, Style.capsuleHeight * scaling)
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
id: backgroundContainer
|
||||||
Layout.preferredWidth: mainLayout.implicitWidth + Style.marginM * scaling * 2
|
anchors.centerIn: parent
|
||||||
Layout.alignment: Qt.AlignVCenter
|
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : (horizontalLayout.implicitWidth + Style.marginL * 2 * scaling)
|
||||||
|
height: (barPosition === "left" || barPosition === "right") ? parent.height : Math.round(Style.capsuleHeight * scaling)
|
||||||
radius: Math.round(Style.radiusM * scaling)
|
radius: Math.round(Style.radiusM * scaling)
|
||||||
color: Color.mSurfaceVariant
|
color: Color.mSurfaceVariant
|
||||||
|
|
||||||
|
// Horizontal layout for top/bottom bars
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: mainLayout
|
id: horizontalLayout
|
||||||
anchors.centerIn: parent // Better centering than margins
|
anchors.centerIn: parent
|
||||||
width: parent.width - Style.marginM * scaling * 2
|
spacing: Style.marginXS * scaling
|
||||||
spacing: Style.marginS * scaling
|
visible: barPosition === "top" || barPosition === "bottom"
|
||||||
|
|
||||||
// CPU Usage Component
|
// CPU Usage Component
|
||||||
Item {
|
Item {
|
||||||
|
|
@ -62,7 +151,7 @@ RowLayout {
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: cpuUsageRow
|
id: cpuUsageRow
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: Style.marginXS * scaling
|
spacing: 2 * scaling
|
||||||
|
|
||||||
NIcon {
|
NIcon {
|
||||||
icon: "cpu-usage"
|
icon: "cpu-usage"
|
||||||
|
|
@ -92,7 +181,7 @@ RowLayout {
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: cpuTempRow
|
id: cpuTempRow
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: Style.marginXS * scaling
|
spacing: 2 * scaling
|
||||||
|
|
||||||
NIcon {
|
NIcon {
|
||||||
icon: "cpu-temperature"
|
icon: "cpu-temperature"
|
||||||
|
|
@ -123,7 +212,7 @@ RowLayout {
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: memoryUsageRow
|
id: memoryUsageRow
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: Style.marginXS * scaling
|
spacing: 2 * scaling
|
||||||
|
|
||||||
NIcon {
|
NIcon {
|
||||||
icon: "memory"
|
icon: "memory"
|
||||||
|
|
@ -233,5 +322,196 @@ RowLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vertical layout for left/right bars
|
||||||
|
ColumnLayout {
|
||||||
|
id: verticalLayout
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: Math.round(28 * scaling)
|
||||||
|
height: parent.height
|
||||||
|
spacing: Style.marginXXS * scaling
|
||||||
|
visible: barPosition === "left" || barPosition === "right"
|
||||||
|
|
||||||
|
// CPU Usage Component
|
||||||
|
Item {
|
||||||
|
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||||
|
Layout.preferredWidth: Math.round(28 * scaling)
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
visible: showCpuUsage
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: cpuUsageRowVertical
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 1 * scaling
|
||||||
|
|
||||||
|
NIcon {
|
||||||
|
icon: "cpu-usage"
|
||||||
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: `${Math.round(SystemStatService.cpuUsage)}%`
|
||||||
|
font.family: Settings.data.ui.fontFixed
|
||||||
|
font.pointSize: Style.fontSizeXXS * scaling * 0.8
|
||||||
|
font.weight: Style.fontWeightMedium
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
color: Color.mPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPU Temperature Component
|
||||||
|
Item {
|
||||||
|
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||||
|
Layout.preferredWidth: Math.round(28 * scaling)
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
visible: showCpuTemp
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: cpuTempRowVertical
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 1 * scaling
|
||||||
|
|
||||||
|
NIcon {
|
||||||
|
icon: "cpu-temperature"
|
||||||
|
// Fire is so tall, we need to make it smaller
|
||||||
|
font.pointSize: Style.fontSizeXS * scaling
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: `${SystemStatService.cpuTemp}°`
|
||||||
|
font.family: Settings.data.ui.fontFixed
|
||||||
|
font.pointSize: Style.fontSizeXXS * scaling * 0.8
|
||||||
|
font.weight: Style.fontWeightMedium
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
color: Color.mPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory Usage Component
|
||||||
|
Item {
|
||||||
|
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||||
|
Layout.preferredWidth: Math.round(28 * scaling)
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
visible: showMemoryUsage
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: memoryUsageRowVertical
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 1 * scaling
|
||||||
|
|
||||||
|
NIcon {
|
||||||
|
icon: "memory"
|
||||||
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: showMemoryAsPercent ? `${SystemStatService.memPercent}%` : `${Math.round(SystemStatService.memGb)}G`
|
||||||
|
font.family: Settings.data.ui.fontFixed
|
||||||
|
font.pointSize: Style.fontSizeXXS * scaling * 0.8
|
||||||
|
font.weight: Style.fontWeightMedium
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
color: Color.mPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network Download Speed Component
|
||||||
|
Item {
|
||||||
|
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||||
|
Layout.preferredWidth: Math.round(28 * scaling)
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
visible: showNetworkStats
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: networkDownloadRowVertical
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 1 * scaling
|
||||||
|
|
||||||
|
NIcon {
|
||||||
|
icon: "download-speed"
|
||||||
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
|
||||||
|
font.family: Settings.data.ui.fontFixed
|
||||||
|
font.pointSize: Style.fontSizeXXS * scaling * 0.8
|
||||||
|
font.weight: Style.fontWeightMedium
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
color: Color.mPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network Upload Speed Component
|
||||||
|
Item {
|
||||||
|
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||||
|
Layout.preferredWidth: Math.round(28 * scaling)
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
visible: showNetworkStats
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: networkUploadRowVertical
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 1 * scaling
|
||||||
|
|
||||||
|
NIcon {
|
||||||
|
icon: "upload-speed"
|
||||||
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
|
||||||
|
font.family: Settings.data.ui.fontFixed
|
||||||
|
font.pointSize: Style.fontSizeXXS * scaling * 0.8
|
||||||
|
font.weight: Style.fontWeightMedium
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
color: Color.mPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disk Usage Component (primary drive)
|
||||||
|
Item {
|
||||||
|
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
|
||||||
|
Layout.preferredWidth: Math.round(28 * scaling)
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
visible: showDiskUsage
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: diskUsageRowVertical
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 1 * scaling
|
||||||
|
|
||||||
|
NIcon {
|
||||||
|
icon: "storage"
|
||||||
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: `${SystemStatService.diskPercent}%`
|
||||||
|
font.family: Settings.data.ui.fontFixed
|
||||||
|
font.pointSize: Style.fontSizeXXS * scaling * 0.8
|
||||||
|
font.weight: Style.fontWeightMedium
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
color: Color.mPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ Rectangle {
|
||||||
property real scaling: 1.0
|
property real scaling: 1.0
|
||||||
|
|
||||||
readonly property real itemSize: 24 * scaling
|
readonly property real itemSize: 24 * scaling
|
||||||
|
readonly property string barPosition: Settings.data.bar.position
|
||||||
|
|
||||||
function onLoaded() {
|
function onLoaded() {
|
||||||
// When the widget is fully initialized with its props
|
// When the widget is fully initialized with its props
|
||||||
|
|
@ -27,8 +28,8 @@ Rectangle {
|
||||||
}
|
}
|
||||||
|
|
||||||
visible: SystemTray.items.values.length > 0
|
visible: SystemTray.items.values.length > 0
|
||||||
implicitWidth: trayLayout.implicitWidth + Style.marginM * scaling * 2
|
implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : (trayLayout.implicitWidth + Style.marginM * scaling * 2)
|
||||||
implicitHeight: Math.round(Style.capsuleHeight * scaling)
|
implicitHeight: (barPosition === "left" || barPosition === "right") ? Math.round(trayLayout.implicitHeight + Style.marginM * scaling * 2) : Math.round(Style.capsuleHeight * scaling)
|
||||||
radius: Math.round(Style.radiusM * scaling)
|
radius: Math.round(Style.radiusM * scaling)
|
||||||
color: Color.mSurfaceVariant
|
color: Color.mSurfaceVariant
|
||||||
|
|
||||||
|
|
@ -111,9 +112,21 @@ Rectangle {
|
||||||
if (modelData.hasMenu && modelData.menu && trayMenu.item) {
|
if (modelData.hasMenu && modelData.menu && trayMenu.item) {
|
||||||
trayPanel.open()
|
trayPanel.open()
|
||||||
|
|
||||||
// Anchor the menu to the tray icon item (parent) and position it below the icon
|
// Position menu based on bar position
|
||||||
const menuX = (width / 2) - (trayMenu.item.width / 2)
|
let menuX, menuY
|
||||||
const menuY = Math.round(Style.barHeight * scaling)
|
if (barPosition === "left") {
|
||||||
|
// For left bar: position menu to the right of the bar
|
||||||
|
menuX = width + Style.marginM * scaling
|
||||||
|
menuY = 0
|
||||||
|
} else if (barPosition === "right") {
|
||||||
|
// For right bar: position menu to the left of the bar
|
||||||
|
menuX = -trayMenu.item.width - Style.marginM * scaling
|
||||||
|
menuY = 0
|
||||||
|
} else {
|
||||||
|
// For horizontal bars: center horizontally and position below
|
||||||
|
menuX = (width / 2) - (trayMenu.item.width / 2)
|
||||||
|
menuY = Math.round(Style.barHeight * scaling)
|
||||||
|
}
|
||||||
trayMenu.item.menu = modelData.menu
|
trayMenu.item.menu = modelData.menu
|
||||||
trayMenu.item.showAt(parent, menuX, menuY)
|
trayMenu.item.showAt(parent, menuX, menuY)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -61,15 +61,6 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: externalHideTimer
|
|
||||||
running: false
|
|
||||||
interval: 1500
|
|
||||||
onTriggered: {
|
|
||||||
pill.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NPill {
|
NPill {
|
||||||
id: pill
|
id: pill
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ Item {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property string barPosition: Settings.data.bar.position
|
||||||
|
|
||||||
readonly property string labelMode: (widgetSettings.labelMode !== undefined) ? widgetSettings.labelMode : widgetMetadata.labelMode
|
readonly property string labelMode: (widgetSettings.labelMode !== undefined) ? widgetSettings.labelMode : widgetMetadata.labelMode
|
||||||
readonly property bool hideUnoccupied: (widgetSettings.hideUnoccupied !== undefined) ? widgetSettings.hideUnoccupied : widgetMetadata.hideUnoccupied
|
readonly property bool hideUnoccupied: (widgetSettings.hideUnoccupied !== undefined) ? widgetSettings.hideUnoccupied : widgetMetadata.hideUnoccupied
|
||||||
|
|
||||||
|
|
@ -47,17 +49,8 @@ Item {
|
||||||
|
|
||||||
signal workspaceChanged(int workspaceId, color accentColor)
|
signal workspaceChanged(int workspaceId, color accentColor)
|
||||||
|
|
||||||
implicitHeight: Math.round(Style.barHeight * scaling)
|
implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)
|
||||||
implicitWidth: {
|
implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.barHeight * scaling) : calculatedHorizontalWidth()
|
||||||
let total = 0
|
|
||||||
for (var i = 0; i < localWorkspaces.count; i++) {
|
|
||||||
const ws = localWorkspaces.get(i)
|
|
||||||
total += calculatedWsWidth(ws)
|
|
||||||
}
|
|
||||||
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills
|
|
||||||
total += horizontalPadding * 2
|
|
||||||
return total
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculatedWsWidth(ws) {
|
function calculatedWsWidth(ws) {
|
||||||
if (ws.isFocused)
|
if (ws.isFocused)
|
||||||
|
|
@ -68,6 +61,37 @@ Item {
|
||||||
return Math.round(20 * scaling)
|
return Math.round(20 * scaling)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function calculatedWsHeight(ws) {
|
||||||
|
if (ws.isFocused)
|
||||||
|
return Math.round(44 * scaling)
|
||||||
|
else if (ws.isActive)
|
||||||
|
return Math.round(28 * scaling)
|
||||||
|
else
|
||||||
|
return Math.round(20 * scaling)
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculatedVerticalHeight() {
|
||||||
|
let total = 0
|
||||||
|
for (var i = 0; i < localWorkspaces.count; i++) {
|
||||||
|
const ws = localWorkspaces.get(i)
|
||||||
|
total += calculatedWsHeight(ws)
|
||||||
|
}
|
||||||
|
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills
|
||||||
|
total += horizontalPadding * 2
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculatedHorizontalWidth() {
|
||||||
|
let total = 0
|
||||||
|
for (var i = 0; i < localWorkspaces.count; i++) {
|
||||||
|
const ws = localWorkspaces.get(i)
|
||||||
|
total += calculatedWsWidth(ws)
|
||||||
|
}
|
||||||
|
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills
|
||||||
|
total += horizontalPadding * 2
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
refreshWorkspaces()
|
refreshWorkspaces()
|
||||||
}
|
}
|
||||||
|
|
@ -99,7 +123,8 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
workspaceRepeater.model = localWorkspaces
|
workspaceRepeaterHorizontal.model = localWorkspaces
|
||||||
|
workspaceRepeaterVertical.model = localWorkspaces
|
||||||
updateWorkspaceFocus()
|
updateWorkspaceFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,9 +173,8 @@ Item {
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: workspaceBackground
|
id: workspaceBackground
|
||||||
width: parent.width
|
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : parent.width
|
||||||
|
height: (barPosition === "left" || barPosition === "right") ? parent.height : Math.round(Style.capsuleHeight * scaling)
|
||||||
height: Math.round(Style.capsuleHeight * scaling)
|
|
||||||
radius: Math.round(Style.radiusM * scaling)
|
radius: Math.round(Style.radiusM * scaling)
|
||||||
color: Color.mSurfaceVariant
|
color: Color.mSurfaceVariant
|
||||||
|
|
||||||
|
|
@ -158,14 +182,17 @@ Item {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Horizontal layout for top/bottom bars
|
||||||
Row {
|
Row {
|
||||||
id: pillRow
|
id: pillRow
|
||||||
spacing: spacingBetweenPills
|
spacing: spacingBetweenPills
|
||||||
anchors.verticalCenter: workspaceBackground.verticalCenter
|
anchors.verticalCenter: workspaceBackground.verticalCenter
|
||||||
width: root.width - horizontalPadding * 2
|
width: root.width - horizontalPadding * 2
|
||||||
x: horizontalPadding
|
x: horizontalPadding
|
||||||
|
visible: barPosition === "top" || barPosition === "bottom"
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
id: workspaceRepeater
|
id: workspaceRepeaterHorizontal
|
||||||
model: localWorkspaces
|
model: localWorkspaces
|
||||||
Item {
|
Item {
|
||||||
id: workspacePillContainer
|
id: workspacePillContainer
|
||||||
|
|
@ -299,4 +326,149 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vertical layout for left/right bars
|
||||||
|
Column {
|
||||||
|
id: pillColumn
|
||||||
|
spacing: spacingBetweenPills
|
||||||
|
anchors.horizontalCenter: workspaceBackground.horizontalCenter
|
||||||
|
height: root.height - horizontalPadding * 2
|
||||||
|
y: horizontalPadding
|
||||||
|
visible: barPosition === "left" || barPosition === "right"
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: workspaceRepeaterVertical
|
||||||
|
model: localWorkspaces
|
||||||
|
Item {
|
||||||
|
id: workspacePillContainerVertical
|
||||||
|
width: (labelMode !== "none") ? Math.round(18 * scaling) : Math.round(14 * scaling)
|
||||||
|
height: root.calculatedWsHeight(model)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: pillVertical
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
active: (labelMode !== "none")
|
||||||
|
sourceComponent: Component {
|
||||||
|
Text {
|
||||||
|
x: (pillVertical.width - width) / 2
|
||||||
|
y: (pillVertical.height - height) / 2 + (height - contentHeight) / 2
|
||||||
|
text: {
|
||||||
|
if (labelMode === "name" && model.name && model.name.length > 0) {
|
||||||
|
return model.name.substring(0, 2)
|
||||||
|
} else {
|
||||||
|
return model.idx.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font.pointSize: model.isFocused ? Style.fontSizeXS * scaling : Style.fontSizeXXS * scaling
|
||||||
|
font.capitalization: Font.AllUppercase
|
||||||
|
font.family: Settings.data.ui.fontFixed
|
||||||
|
font.weight: Style.fontWeightBold
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
color: {
|
||||||
|
if (model.isFocused)
|
||||||
|
return Color.mOnPrimary
|
||||||
|
if (model.isUrgent)
|
||||||
|
return Color.mOnError
|
||||||
|
if (model.isActive || model.isOccupied)
|
||||||
|
return Color.mOnSecondary
|
||||||
|
|
||||||
|
return Color.mOnSurface
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
radius: width * 0.5
|
||||||
|
color: {
|
||||||
|
if (model.isFocused)
|
||||||
|
return Color.mPrimary
|
||||||
|
if (model.isUrgent)
|
||||||
|
return Color.mError
|
||||||
|
if (model.isActive || model.isOccupied)
|
||||||
|
return Color.mSecondary
|
||||||
|
|
||||||
|
return Color.mOutline
|
||||||
|
}
|
||||||
|
scale: model.isFocused ? 1.0 : 0.9
|
||||||
|
z: 0
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: pillMouseAreaVertical
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
WorkspaceService.switchToWorkspace(model.idx)
|
||||||
|
}
|
||||||
|
hoverEnabled: true
|
||||||
|
}
|
||||||
|
// Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius
|
||||||
|
Behavior on width {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.OutBack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.OutBack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.OutBack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Style.animationFast
|
||||||
|
easing.type: Easing.InOutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationFast
|
||||||
|
easing.type: Easing.InOutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on radius {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.OutBack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on width {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.OutBack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.OutBack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Burst effect overlay for focused pill (smaller outline)
|
||||||
|
Rectangle {
|
||||||
|
id: pillBurstVertical
|
||||||
|
anchors.centerIn: workspacePillContainerVertical
|
||||||
|
width: workspacePillContainerVertical.width + 18 * root.masterProgress * scale
|
||||||
|
height: workspacePillContainerVertical.height + 18 * root.masterProgress * scale
|
||||||
|
radius: width / 2
|
||||||
|
color: Color.transparent
|
||||||
|
border.color: root.effectColor
|
||||||
|
border.width: Math.max(1, Math.round((2 + 6 * (1.0 - root.masterProgress)) * scaling))
|
||||||
|
opacity: root.effectsActive && model.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0
|
||||||
|
visible: root.effectsActive && model.isFocused
|
||||||
|
z: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ NPanel {
|
||||||
|
|
||||||
NIconButton {
|
NIconButton {
|
||||||
icon: "close"
|
icon: "close"
|
||||||
tooltipText: "Close"
|
tooltipText: "Close."
|
||||||
sizeRatio: 0.8
|
sizeRatio: 0.8
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.close()
|
root.close()
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ NPanel {
|
||||||
|
|
||||||
preferredWidth: 340
|
preferredWidth: 340
|
||||||
preferredHeight: 320
|
preferredHeight: 320
|
||||||
panelAnchorRight: true
|
panelAnchorRight: Settings.data.bar.position === "right"
|
||||||
|
|
||||||
// Main Column
|
// Main Column
|
||||||
panelContent: ColumnLayout {
|
panelContent: ColumnLayout {
|
||||||
|
|
|
||||||
|
|
@ -166,7 +166,15 @@ Variants {
|
||||||
|
|
||||||
// Position above the bar if it's at bottom
|
// Position above the bar if it's at bottom
|
||||||
anchors.bottom: true
|
anchors.bottom: true
|
||||||
margins.bottom: barAtBottom ? barHeight + floatingMargin : floatingMargin
|
|
||||||
|
margins.bottom: {
|
||||||
|
switch (Settings.data.bar.position) {
|
||||||
|
case "bottom":
|
||||||
|
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling + floatingMargin : floatingMargin)
|
||||||
|
default:
|
||||||
|
return floatingMargin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Rectangle {
|
// Rectangle {
|
||||||
// anchors.fill: parent
|
// anchors.fill: parent
|
||||||
|
|
|
||||||
|
|
@ -33,13 +33,50 @@ Variants {
|
||||||
screen: modelData
|
screen: modelData
|
||||||
color: Color.transparent
|
color: Color.transparent
|
||||||
|
|
||||||
// Position based on bar location
|
// Position based on bar location - always at top
|
||||||
anchors.top: Settings.data.bar.position === "top"
|
anchors.top: true
|
||||||
anchors.bottom: Settings.data.bar.position === "bottom"
|
anchors.right: Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
|
||||||
anchors.right: true
|
anchors.left: Settings.data.bar.position === "left"
|
||||||
margins.top: Settings.data.bar.position === "top" ? (Style.barHeight + Style.marginM + (Settings.data.bar.floating ? Settings.data.bar.marginVertical : 0)) * scaling : 0
|
|
||||||
margins.bottom: Settings.data.bar.position === "bottom" ? (Style.barHeight + Style.marginM + (Settings.data.bar.floating ? Settings.data.bar.marginVertical : 0)) * scaling : 0
|
margins.top: {
|
||||||
margins.right: Style.marginM * scaling
|
switch (Settings.data.bar.position) {
|
||||||
|
case "top":
|
||||||
|
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
|
||||||
|
default:
|
||||||
|
return Style.marginM * scaling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
margins.bottom: {
|
||||||
|
switch (Settings.data.bar.position) {
|
||||||
|
case "bottom":
|
||||||
|
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
margins.left: {
|
||||||
|
switch (Settings.data.bar.position) {
|
||||||
|
case "left":
|
||||||
|
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0)
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
margins.right: {
|
||||||
|
switch (Settings.data.bar.position) {
|
||||||
|
case "right":
|
||||||
|
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0)
|
||||||
|
case "top":
|
||||||
|
case "bottom":
|
||||||
|
return Style.marginM * scaling
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
implicitWidth: 360 * scaling
|
implicitWidth: 360 * scaling
|
||||||
implicitHeight: Math.min(notificationStack.implicitHeight, (NotificationService.maxVisible * 120) * scaling)
|
implicitHeight: Math.min(notificationStack.implicitHeight, (NotificationService.maxVisible * 120) * scaling)
|
||||||
//WlrLayershell.layer: WlrLayer.Overlay
|
//WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
|
@ -77,10 +114,10 @@ Variants {
|
||||||
// Main notification container
|
// Main notification container
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: notificationStack
|
id: notificationStack
|
||||||
// Position based on bar location
|
// Position based on bar location - always at top
|
||||||
anchors.top: Settings.data.bar.position === "top" ? parent.top : undefined
|
anchors.top: parent.top
|
||||||
anchors.bottom: Settings.data.bar.position === "bottom" ? parent.bottom : undefined
|
anchors.right: (Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom") ? parent.right : undefined
|
||||||
anchors.right: parent.right
|
anchors.left: Settings.data.bar.position === "left" ? parent.left : undefined
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
width: 360 * scaling
|
width: 360 * scaling
|
||||||
visible: true
|
visible: true
|
||||||
|
|
@ -288,7 +325,7 @@ Variants {
|
||||||
// Close button positioned absolutely
|
// Close button positioned absolutely
|
||||||
NIconButton {
|
NIconButton {
|
||||||
icon: "close"
|
icon: "close"
|
||||||
tooltipText: "Close"
|
tooltipText: "Close."
|
||||||
sizeRatio: 0.6
|
sizeRatio: 0.6
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.topMargin: Style.marginM * scaling
|
anchors.topMargin: Style.marginM * scaling
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ NPanel {
|
||||||
|
|
||||||
preferredWidth: 380
|
preferredWidth: 380
|
||||||
preferredHeight: 500
|
preferredHeight: 500
|
||||||
panelAnchorRight: true
|
panelAnchorRight: Settings.data.bar.position === "right"
|
||||||
panelKeyboardFocus: true
|
panelKeyboardFocus: true
|
||||||
|
|
||||||
panelContent: Rectangle {
|
panelContent: Rectangle {
|
||||||
|
|
@ -65,7 +65,7 @@ NPanel {
|
||||||
|
|
||||||
NIconButton {
|
NIconButton {
|
||||||
icon: "close"
|
icon: "close"
|
||||||
tooltipText: "Close"
|
tooltipText: "Close."
|
||||||
sizeRatio: 0.8
|
sizeRatio: 0.8
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.close()
|
root.close()
|
||||||
|
|
|
||||||
|
|
@ -29,11 +29,11 @@ NPanel {
|
||||||
Dock,
|
Dock,
|
||||||
Hooks,
|
Hooks,
|
||||||
Launcher,
|
Launcher,
|
||||||
Brightness,
|
|
||||||
ColorScheme,
|
ColorScheme,
|
||||||
Display,
|
Display,
|
||||||
General,
|
General,
|
||||||
Network,
|
Network,
|
||||||
|
Notification,
|
||||||
ScreenRecorder,
|
ScreenRecorder,
|
||||||
Weather,
|
Weather,
|
||||||
Wallpaper,
|
Wallpaper,
|
||||||
|
|
@ -72,10 +72,6 @@ NPanel {
|
||||||
id: audioTab
|
id: audioTab
|
||||||
Tabs.AudioTab {}
|
Tabs.AudioTab {}
|
||||||
}
|
}
|
||||||
Component {
|
|
||||||
id: brightnessTab
|
|
||||||
Tabs.BrightnessTab {}
|
|
||||||
}
|
|
||||||
Component {
|
Component {
|
||||||
id: displayTab
|
id: displayTab
|
||||||
Tabs.DisplayTab {}
|
Tabs.DisplayTab {}
|
||||||
|
|
@ -116,6 +112,10 @@ NPanel {
|
||||||
id: dockTab
|
id: dockTab
|
||||||
Tabs.DockTab {}
|
Tabs.DockTab {}
|
||||||
}
|
}
|
||||||
|
Component {
|
||||||
|
id: notificationTab
|
||||||
|
Tabs.NotificationTab {}
|
||||||
|
}
|
||||||
|
|
||||||
// Order *DOES* matter
|
// Order *DOES* matter
|
||||||
function updateTabsModel() {
|
function updateTabsModel() {
|
||||||
|
|
@ -149,16 +149,16 @@ NPanel {
|
||||||
"label": "Display",
|
"label": "Display",
|
||||||
"icon": "settings-display",
|
"icon": "settings-display",
|
||||||
"source": displayTab
|
"source": displayTab
|
||||||
|
}, {
|
||||||
|
"id": SettingsPanel.Tab.Notification,
|
||||||
|
"label": "Notification",
|
||||||
|
"icon": "settings-notification",
|
||||||
|
"source": notificationTab
|
||||||
}, {
|
}, {
|
||||||
"id": SettingsPanel.Tab.Network,
|
"id": SettingsPanel.Tab.Network,
|
||||||
"label": "Network",
|
"label": "Network",
|
||||||
"icon": "settings-network",
|
"icon": "settings-network",
|
||||||
"source": networkTab
|
"source": networkTab
|
||||||
}, {
|
|
||||||
"id": SettingsPanel.Tab.Brightness,
|
|
||||||
"label": "Brightness",
|
|
||||||
"icon": "settings-brightness",
|
|
||||||
"source": brightnessTab
|
|
||||||
}, {
|
}, {
|
||||||
"id": SettingsPanel.Tab.Weather,
|
"id": SettingsPanel.Tab.Weather,
|
||||||
"label": "Weather",
|
"label": "Weather",
|
||||||
|
|
@ -468,7 +468,7 @@ NPanel {
|
||||||
NIcon {
|
NIcon {
|
||||||
icon: root.tabsModel[currentTabIndex]?.icon
|
icon: root.tabsModel[currentTabIndex]?.icon
|
||||||
color: Color.mPrimary
|
color: Color.mPrimary
|
||||||
font.pointSize: Style.fontSizeXL * scaling
|
font.pointSize: Style.fontSizeXXL * scaling
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main title
|
// Main title
|
||||||
|
|
@ -484,7 +484,7 @@ NPanel {
|
||||||
// Close button
|
// Close button
|
||||||
NIconButton {
|
NIconButton {
|
||||||
icon: "close"
|
icon: "close"
|
||||||
tooltipText: "Close"
|
tooltipText: "Close."
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
onClicked: root.close()
|
onClicked: root.close()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,23 +10,23 @@ import qs.Widgets
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: root
|
id: root
|
||||||
|
spacing: Style.marginL * scaling
|
||||||
|
|
||||||
property string latestVersion: GitHubService.latestVersion
|
property string latestVersion: GitHubService.latestVersion
|
||||||
property string currentVersion: UpdateService.currentVersion
|
property string currentVersion: UpdateService.currentVersion
|
||||||
property var contributors: GitHubService.contributors
|
property var contributors: GitHubService.contributors
|
||||||
|
|
||||||
NText {
|
NHeader {
|
||||||
text: "Noctalia Shell"
|
label: "Noctalia Shell"
|
||||||
font.pointSize: Style.fontSizeXXXL * scaling
|
description: "A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell."
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mOnSurface
|
|
||||||
Layout.alignment: Qt.AlignCenter
|
|
||||||
Layout.bottomMargin: Style.marginS * scaling
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: Style.marginL * scaling
|
||||||
|
|
||||||
|
|
||||||
// Versions
|
// Versions
|
||||||
GridLayout {
|
GridLayout {
|
||||||
Layout.alignment: Qt.AlignCenter
|
|
||||||
columns: 2
|
columns: 2
|
||||||
rowSpacing: Style.marginXS * scaling
|
rowSpacing: Style.marginXS * scaling
|
||||||
columnSpacing: Style.marginS * scaling
|
columnSpacing: Style.marginS * scaling
|
||||||
|
|
@ -34,7 +34,6 @@ ColumnLayout {
|
||||||
NText {
|
NText {
|
||||||
text: "Latest Version:"
|
text: "Latest Version:"
|
||||||
color: Color.mOnSurface
|
color: Color.mOnSurface
|
||||||
Layout.alignment: Qt.AlignRight
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
|
|
@ -46,7 +45,6 @@ ColumnLayout {
|
||||||
NText {
|
NText {
|
||||||
text: "Installed Version:"
|
text: "Installed Version:"
|
||||||
color: Color.mOnSurface
|
color: Color.mOnSurface
|
||||||
Layout.alignment: Qt.AlignRight
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
|
|
@ -56,10 +54,13 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updater
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update button
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.alignment: Qt.AlignCenter
|
Layout.alignment: Qt.alignmentRight
|
||||||
Layout.topMargin: Style.marginS * scaling
|
|
||||||
Layout.preferredWidth: Math.round(updateRow.implicitWidth + (Style.marginL * scaling * 2))
|
Layout.preferredWidth: Math.round(updateRow.implicitWidth + (Style.marginL * scaling * 2))
|
||||||
Layout.preferredHeight: Math.round(Style.barHeight * scaling)
|
Layout.preferredHeight: Math.round(Style.barHeight * scaling)
|
||||||
radius: Style.radiusL * scaling
|
radius: Style.radiusL * scaling
|
||||||
|
|
@ -115,23 +116,21 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
NDivider {
|
NDivider {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: Style.marginXL * scaling
|
Layout.topMargin: Style.marginXL * scaling
|
||||||
Layout.bottomMargin: Style.marginXL * scaling
|
Layout.bottomMargin: Style.marginXL * scaling
|
||||||
}
|
}
|
||||||
|
|
||||||
NText {
|
NHeader {
|
||||||
text: `Shout-out to our ${root.contributors.length} <b>awesome</b> contributors!`
|
label: "Contributors"
|
||||||
font.pointSize: Style.fontSizeL * scaling
|
description: `Shout-out to our ${root.contributors.length} <b>awesome</b> contributors!`
|
||||||
color: Color.mOnSurface
|
|
||||||
Layout.alignment: Qt.AlignCenter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GridView {
|
GridView {
|
||||||
id: contributorsGrid
|
id: contributorsGrid
|
||||||
|
|
||||||
Layout.topMargin: Style.marginL * scaling
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
Layout.preferredWidth: cellWidth * 3 // Fixed 3 columns
|
Layout.preferredWidth: cellWidth * 3 // Fixed 3 columns
|
||||||
Layout.preferredHeight: {
|
Layout.preferredHeight: {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,12 @@ import qs.Services
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: root
|
id: root
|
||||||
|
spacing: Style.marginL * scaling
|
||||||
|
|
||||||
|
NHeader {
|
||||||
|
label: "Volumes"
|
||||||
|
description: "Configure volume controls and audio levels."
|
||||||
|
}
|
||||||
|
|
||||||
property real localVolume: AudioService.volume
|
property real localVolume: AudioService.volume
|
||||||
|
|
||||||
|
|
@ -20,7 +26,7 @@ ColumnLayout {
|
||||||
|
|
||||||
// Master Volume
|
// Master Volume
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginXXS * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
NLabel {
|
NLabel {
|
||||||
|
|
@ -67,7 +73,6 @@ ColumnLayout {
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: Style.marginM * scaling
|
|
||||||
|
|
||||||
NToggle {
|
NToggle {
|
||||||
label: "Mute Audio Output"
|
label: "Mute Audio Output"
|
||||||
|
|
@ -83,9 +88,8 @@ ColumnLayout {
|
||||||
|
|
||||||
// Input Volume
|
// Input Volume
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginXS * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: Style.marginM * scaling
|
|
||||||
|
|
||||||
NLabel {
|
NLabel {
|
||||||
label: "Input Volume"
|
label: "Input Volume"
|
||||||
|
|
@ -117,7 +121,6 @@ ColumnLayout {
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: Style.marginM * scaling
|
|
||||||
|
|
||||||
NToggle {
|
NToggle {
|
||||||
label: "Mute Audio Input"
|
label: "Mute Audio Input"
|
||||||
|
|
@ -131,7 +134,6 @@ ColumnLayout {
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: Style.marginM * scaling
|
|
||||||
|
|
||||||
NSpinBox {
|
NSpinBox {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
@ -158,12 +160,9 @@ ColumnLayout {
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
|
|
||||||
NText {
|
NHeader {
|
||||||
text: "Audio Devices"
|
label: "Audio Devices"
|
||||||
font.pointSize: Style.fontSizeXXL * scaling
|
description: "Configure audio input and output devices."
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mSecondary
|
|
||||||
Layout.bottomMargin: Style.marginS * scaling
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------
|
// -------------------------------
|
||||||
|
|
@ -203,7 +202,6 @@ ColumnLayout {
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: Style.marginXS * scaling
|
spacing: Style.marginXS * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.bottomMargin: Style.marginL * scaling
|
|
||||||
|
|
||||||
NLabel {
|
NLabel {
|
||||||
label: "Input Device"
|
label: "Input Device"
|
||||||
|
|
@ -234,12 +232,9 @@ ColumnLayout {
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: Style.marginL * scaling
|
spacing: Style.marginL * scaling
|
||||||
|
|
||||||
NText {
|
NHeader {
|
||||||
text: "Media Player"
|
label: "Media Player"
|
||||||
font.pointSize: Style.fontSizeXXL * scaling
|
description: "Configure your favorite media players."
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mSecondary
|
|
||||||
Layout.bottomMargin: Style.marginS * scaling
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preferred player
|
// Preferred player
|
||||||
|
|
@ -360,12 +355,9 @@ ColumnLayout {
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
NText {
|
NHeader {
|
||||||
text: "Audio Visualizer"
|
label: "Audio Visualizer"
|
||||||
font.pointSize: Style.fontSizeXXL * scaling
|
description: "Customize visual effects that respond to audio playback."
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mSecondary
|
|
||||||
Layout.bottomMargin: Style.marginS * scaling
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AudioService Visualizer section
|
// AudioService Visualizer section
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
@ -8,6 +9,21 @@ import qs.Modules.SettingsPanel.Bar
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: root
|
id: root
|
||||||
|
spacing: Style.marginL * scaling
|
||||||
|
|
||||||
|
|
||||||
|
// Helper functions to update arrays immutably
|
||||||
|
function addMonitor(list, name) {
|
||||||
|
const arr = (list || []).slice()
|
||||||
|
if (!arr.includes(name))
|
||||||
|
arr.push(name)
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
function removeMonitor(list, name) {
|
||||||
|
return (list || []).filter(function (n) {
|
||||||
|
return n !== name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Handler for drag start - disables panel background clicks
|
// Handler for drag start - disables panel background clicks
|
||||||
function handleDragStart() {
|
function handleDragStart() {
|
||||||
|
|
@ -24,9 +40,12 @@ ColumnLayout {
|
||||||
panel.enableBackgroundClick()
|
panel.enableBackgroundClick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NHeader {
|
||||||
|
label: "Appearance"
|
||||||
|
description: "Configure bar appearance and positioning."
|
||||||
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
spacing: Style.marginL * scaling
|
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
NComboBox {
|
NComboBox {
|
||||||
|
|
@ -42,6 +61,14 @@ ColumnLayout {
|
||||||
key: "bottom"
|
key: "bottom"
|
||||||
name: "Bottom"
|
name: "Bottom"
|
||||||
}
|
}
|
||||||
|
ListElement {
|
||||||
|
key: "left"
|
||||||
|
name: "Left"
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
key: "right"
|
||||||
|
name: "Right"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
currentKey: Settings.data.bar.position
|
currentKey: Settings.data.bar.position
|
||||||
onSelected: key => Settings.data.bar.position = key
|
onSelected: key => Settings.data.bar.position = key
|
||||||
|
|
@ -52,19 +79,9 @@ ColumnLayout {
|
||||||
spacing: Style.marginXXS * scaling
|
spacing: Style.marginXXS * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
NText {
|
NLabel {
|
||||||
text: "Background Opacity"
|
label: "Background Opacity"
|
||||||
font.pointSize: Style.fontSizeL * scaling
|
description: "Adjust the background opacity of the bar."
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mOnSurface
|
|
||||||
}
|
|
||||||
|
|
||||||
NText {
|
|
||||||
text: "Adjust the background opacity of the bar."
|
|
||||||
font.pointSize: Style.fontSizeXS * scaling
|
|
||||||
color: Color.mOnSurfaceVariant
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
|
|
@ -86,7 +103,6 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NToggle {
|
NToggle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
label: "Floating Bar"
|
label: "Floating Bar"
|
||||||
|
|
@ -173,6 +189,40 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
NDivider {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Style.marginXL * scaling
|
||||||
|
Layout.bottomMargin: Style.marginXL * scaling
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitor Configuration
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: Style.marginM * scaling
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
NHeader {
|
||||||
|
label: "Monitors Configuration"
|
||||||
|
description: "Choose which monitors should display the bar."
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Quickshell.screens || []
|
||||||
|
delegate: NCheckbox {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
label: `${modelData.name || "Unknown"}${modelData.model ? `: ${modelData.model}` : ""}`
|
||||||
|
description: `${modelData.width}x${modelData.height} at (${modelData.x}, ${modelData.y})`
|
||||||
|
checked: (Settings.data.bar.monitors || []).indexOf(modelData.name) !== -1
|
||||||
|
onToggled: checked => {
|
||||||
|
if (checked) {
|
||||||
|
Settings.data.bar.monitors = addMonitor(Settings.data.bar.monitors, modelData.name)
|
||||||
|
} else {
|
||||||
|
Settings.data.bar.monitors = removeMonitor(Settings.data.bar.monitors, modelData.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NDivider {
|
NDivider {
|
||||||
|
|
@ -186,20 +236,9 @@ ColumnLayout {
|
||||||
spacing: Style.marginXXS * scaling
|
spacing: Style.marginXXS * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
NText {
|
NHeader {
|
||||||
text: "Widgets Positioning"
|
label: "Widgets Positioning"
|
||||||
font.pointSize: Style.fontSizeXXL * scaling
|
description: "Drag and drop widgets to reorder them within each section, or use the add/remove buttons to manage widgets."
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mSecondary
|
|
||||||
Layout.bottomMargin: Style.marginS * scaling
|
|
||||||
}
|
|
||||||
|
|
||||||
NText {
|
|
||||||
text: "Drag and drop widgets to reorder them within each section, or use the add/remove buttons to manage widgets."
|
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
|
||||||
color: Color.mOnSurfaceVariant
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bar Sections
|
// Bar Sections
|
||||||
|
|
|
||||||
|
|
@ -1,340 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Commons
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
// Time dropdown options (00:00 .. 23:30)
|
|
||||||
ListModel {
|
|
||||||
id: timeOptions
|
|
||||||
}
|
|
||||||
Component.onCompleted: {
|
|
||||||
for (var h = 0; h < 24; h++) {
|
|
||||||
for (var m = 0; m < 60; m += 30) {
|
|
||||||
var hh = ("0" + h).slice(-2)
|
|
||||||
var mm = ("0" + m).slice(-2)
|
|
||||||
var key = hh + ":" + mm
|
|
||||||
timeOptions.append({
|
|
||||||
"key": key,
|
|
||||||
"name": key
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for wlsunset availability when enabling Night Light
|
|
||||||
Process {
|
|
||||||
id: wlsunsetCheck
|
|
||||||
command: ["which", "wlsunset"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
onExited: function (exitCode) {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
Settings.data.nightLight.enabled = true
|
|
||||||
NightLightService.apply()
|
|
||||||
ToastService.showNotice("Night Light", "Enabled")
|
|
||||||
} else {
|
|
||||||
Settings.data.nightLight.enabled = false
|
|
||||||
ToastService.showWarning("Night Light", "wlsunset not installed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout: StdioCollector {}
|
|
||||||
stderr: StdioCollector {}
|
|
||||||
}
|
|
||||||
|
|
||||||
spacing: Style.marginL * scaling
|
|
||||||
|
|
||||||
// Brightness Step Section
|
|
||||||
ColumnLayout {
|
|
||||||
spacing: Style.marginS * scaling
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
NSpinBox {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
label: "Brightness Step Size"
|
|
||||||
description: "Adjust the step size for brightness changes (scroll wheel, keyboard shortcuts)."
|
|
||||||
minimum: 1
|
|
||||||
maximum: 50
|
|
||||||
value: Settings.data.brightness.brightnessStep
|
|
||||||
stepSize: 1
|
|
||||||
suffix: "%"
|
|
||||||
onValueChanged: {
|
|
||||||
Settings.data.brightness.brightnessStep = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Monitor Overview Section
|
|
||||||
ColumnLayout {
|
|
||||||
spacing: Style.marginL * scaling
|
|
||||||
|
|
||||||
NLabel {
|
|
||||||
label: "Monitors Brightness Control"
|
|
||||||
description: "Current brightness levels for all detected monitors."
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single monitor display using the same data source as the bar icon
|
|
||||||
Repeater {
|
|
||||||
model: BrightnessService.monitors
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
radius: Style.radiusM * scaling
|
|
||||||
color: Color.mSurface
|
|
||||||
border.color: Color.mOutline
|
|
||||||
border.width: Math.max(1, Style.borderS * scaling)
|
|
||||||
implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: contentCol
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Style.marginL * scaling
|
|
||||||
spacing: Style.marginM * scaling
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: Style.marginM * scaling
|
|
||||||
|
|
||||||
NText {
|
|
||||||
text: `${model.modelData.name} [${model.modelData.model}]`
|
|
||||||
font.pointSize: Style.fontSizeL * scaling
|
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mSecondary
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
NText {
|
|
||||||
text: model.method
|
|
||||||
font.pointSize: Style.fontSizeXS * scaling
|
|
||||||
color: Color.mOnSurfaceVariant
|
|
||||||
Layout.alignment: Qt.AlignRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: Style.marginM * scaling
|
|
||||||
|
|
||||||
NText {
|
|
||||||
text: "Brightness:"
|
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
|
||||||
color: Color.mOnSurface
|
|
||||||
}
|
|
||||||
|
|
||||||
NSlider {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
from: 0
|
|
||||||
to: 1
|
|
||||||
value: model.brightness
|
|
||||||
stepSize: 0.05
|
|
||||||
onPressedChanged: {
|
|
||||||
if (!pressed) {
|
|
||||||
var monitor = BrightnessService.getMonitorForScreen(model.modelData)
|
|
||||||
monitor.setBrightness(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NText {
|
|
||||||
text: Math.round(model.brightness * 100) + "%"
|
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mPrimary
|
|
||||||
Layout.alignment: Qt.AlignRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NDivider {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: Style.marginXL * scaling
|
|
||||||
Layout.bottomMargin: Style.marginXL * scaling
|
|
||||||
}
|
|
||||||
|
|
||||||
// Night Light Section
|
|
||||||
ColumnLayout {
|
|
||||||
spacing: Style.marginXS * scaling
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
NText {
|
|
||||||
text: "Night Light"
|
|
||||||
font.pointSize: Style.fontSizeXXL * scaling
|
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mSecondary
|
|
||||||
}
|
|
||||||
|
|
||||||
NText {
|
|
||||||
text: "Reduce blue light emission to help you sleep better and reduce eye strain."
|
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
|
||||||
color: Color.mOnSurfaceVariant
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NToggle {
|
|
||||||
label: "Enable Night Light"
|
|
||||||
description: "Apply a warm color filter to reduce blue light emission."
|
|
||||||
checked: Settings.data.nightLight.enabled
|
|
||||||
onToggled: checked => {
|
|
||||||
if (checked) {
|
|
||||||
// Verify wlsunset exists before enabling
|
|
||||||
wlsunsetCheck.running = true
|
|
||||||
} else {
|
|
||||||
Settings.data.nightLight.enabled = false
|
|
||||||
Settings.data.nightLight.forced = false
|
|
||||||
NightLightService.apply()
|
|
||||||
ToastService.showNotice("Night Light", "Disabled")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Temperature
|
|
||||||
ColumnLayout {
|
|
||||||
spacing: Style.marginXS * scaling
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
|
|
||||||
NLabel {
|
|
||||||
label: "Color temperature"
|
|
||||||
description: "Choose two temperatures in Kelvin."
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
visible: Settings.data.nightLight.enabled
|
|
||||||
spacing: Style.marginM * scaling
|
|
||||||
Layout.fillWidth: false
|
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
|
|
||||||
NText {
|
|
||||||
text: "Night"
|
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
|
||||||
color: Color.mOnSurfaceVariant
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
NTextInput {
|
|
||||||
text: Settings.data.nightLight.nightTemp
|
|
||||||
inputMethodHints: Qt.ImhDigitsOnly
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
onEditingFinished: {
|
|
||||||
var nightTemp = parseInt(text)
|
|
||||||
var dayTemp = parseInt(Settings.data.nightLight.dayTemp)
|
|
||||||
if (!isNaN(nightTemp) && !isNaN(dayTemp)) {
|
|
||||||
// Clamp value between [1000 .. (dayTemp-500)]
|
|
||||||
var clampedValue = Math.min(dayTemp - 500, Math.max(1000, nightTemp))
|
|
||||||
text = Settings.data.nightLight.nightTemp = clampedValue.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {}
|
|
||||||
|
|
||||||
NText {
|
|
||||||
text: "Day"
|
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
|
||||||
color: Color.mOnSurfaceVariant
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
}
|
|
||||||
NTextInput {
|
|
||||||
text: Settings.data.nightLight.dayTemp
|
|
||||||
inputMethodHints: Qt.ImhDigitsOnly
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
onEditingFinished: {
|
|
||||||
var dayTemp = parseInt(text)
|
|
||||||
var nightTemp = parseInt(Settings.data.nightLight.nightTemp)
|
|
||||||
if (!isNaN(nightTemp) && !isNaN(dayTemp)) {
|
|
||||||
// Clamp value between [(nightTemp+500) .. 6500]
|
|
||||||
var clampedValue = Math.max(nightTemp + 500, Math.min(6500, dayTemp))
|
|
||||||
text = Settings.data.nightLight.dayTemp = clampedValue.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NToggle {
|
|
||||||
label: "Automatic Scheduling"
|
|
||||||
description: `Based on the sunset and sunrise time in <i>${LocationService.stableName}</i> - recommended.`
|
|
||||||
checked: Settings.data.nightLight.autoSchedule
|
|
||||||
onToggled: checked => Settings.data.nightLight.autoSchedule = checked
|
|
||||||
visible: Settings.data.nightLight.enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schedule settings
|
|
||||||
ColumnLayout {
|
|
||||||
spacing: Style.marginXS * scaling
|
|
||||||
visible: Settings.data.nightLight.enabled && !Settings.data.nightLight.autoSchedule && !Settings.data.nightLight.forced
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: false
|
|
||||||
spacing: Style.marginM * scaling
|
|
||||||
|
|
||||||
NLabel {
|
|
||||||
label: "Manual Scheduling"
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {// add a little more spacing
|
|
||||||
}
|
|
||||||
|
|
||||||
NText {
|
|
||||||
text: "Sunrise Time"
|
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
|
||||||
color: Color.mOnSurfaceVariant
|
|
||||||
}
|
|
||||||
|
|
||||||
NComboBox {
|
|
||||||
model: timeOptions
|
|
||||||
currentKey: Settings.data.nightLight.manualSunrise
|
|
||||||
placeholder: "Select start time"
|
|
||||||
onSelected: key => Settings.data.nightLight.manualSunrise = key
|
|
||||||
minimumWidth: 120 * scaling
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {// add a little more spacing
|
|
||||||
}
|
|
||||||
|
|
||||||
NText {
|
|
||||||
text: "Sunset Time"
|
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
|
||||||
color: Color.mOnSurfaceVariant
|
|
||||||
}
|
|
||||||
NComboBox {
|
|
||||||
model: timeOptions
|
|
||||||
currentKey: Settings.data.nightLight.manualSunset
|
|
||||||
placeholder: "Select stop time"
|
|
||||||
onSelected: key => Settings.data.nightLight.manualSunset = key
|
|
||||||
minimumWidth: 120 * scaling
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force activation toggle
|
|
||||||
NToggle {
|
|
||||||
label: "Force activation"
|
|
||||||
description: "Immediately apply night temperature without scheduling or fade."
|
|
||||||
checked: Settings.data.nightLight.forced
|
|
||||||
onToggled: checked => {
|
|
||||||
Settings.data.nightLight.forced = checked
|
|
||||||
if (checked && !Settings.data.nightLight.enabled) {
|
|
||||||
// Ensure enabled when forcing
|
|
||||||
wlsunsetCheck.running = true
|
|
||||||
} else {
|
|
||||||
NightLightService.apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
visible: Settings.data.nightLight.enabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -8,7 +8,9 @@ import qs.Widgets
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: root
|
id: root
|
||||||
spacing: 0
|
spacing: Style.marginL * scaling
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Cache for scheme JSON (can be flat or {dark, light})
|
// Cache for scheme JSON (can be flat or {dark, light})
|
||||||
property var schemeColorsCache: ({})
|
property var schemeColorsCache: ({})
|
||||||
|
|
@ -104,10 +106,12 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Main Toggles - Dark Mode / Matugen
|
// Main Toggles - Dark Mode / Matugen
|
||||||
ColumnLayout {
|
NHeader {
|
||||||
spacing: Style.marginL * scaling
|
label: "Behavior"
|
||||||
Layout.fillWidth: true
|
description: "Main settings for Noctalia's colors."
|
||||||
|
}
|
||||||
|
|
||||||
// Dark Mode Toggle (affects both Matugen and predefined schemes that provide variants)
|
// Dark Mode Toggle (affects both Matugen and predefined schemes that provide variants)
|
||||||
NToggle {
|
NToggle {
|
||||||
|
|
@ -138,7 +142,7 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
NDivider {
|
NDivider {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
@ -151,19 +155,9 @@ ColumnLayout {
|
||||||
spacing: Style.marginM * scaling
|
spacing: Style.marginM * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
NText {
|
NHeader {
|
||||||
text: "Predefined Color Schemes"
|
label: "Predefined Color Schemes"
|
||||||
font.pointSize: Style.fontSizeXXL * scaling
|
description: "To use these color schemes, you must turn off Matugen. With Matugen enabled, colors are automatically generated from your wallpaper."
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mSecondary
|
|
||||||
}
|
|
||||||
|
|
||||||
NText {
|
|
||||||
text: "To use these color schemes, you must turn off Matugen. With Matugen enabled, colors are automatically generated from your wallpaper."
|
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
|
||||||
color: Color.mOnSurfaceVariant
|
|
||||||
Layout.fillWidth: true
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Color Schemes Grid
|
// Color Schemes Grid
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
@ -9,36 +10,54 @@ import qs.Widgets
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
// Helper functions to update arrays immutably
|
// Time dropdown options (00:00 .. 23:30)
|
||||||
function addMonitor(list, name) {
|
ListModel {
|
||||||
const arr = (list || []).slice()
|
id: timeOptions
|
||||||
if (!arr.includes(name))
|
|
||||||
arr.push(name)
|
|
||||||
return arr
|
|
||||||
}
|
}
|
||||||
function removeMonitor(list, name) {
|
Component.onCompleted: {
|
||||||
return (list || []).filter(function (n) {
|
for (var h = 0; h < 24; h++) {
|
||||||
return n !== name
|
for (var m = 0; m < 60; m += 30) {
|
||||||
})
|
var hh = ("0" + h).slice(-2)
|
||||||
|
var mm = ("0" + m).slice(-2)
|
||||||
|
var key = hh + ":" + mm
|
||||||
|
timeOptions.append({
|
||||||
|
"key": key,
|
||||||
|
"name": key
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NText {
|
// Check for wlsunset availability when enabling Night Light
|
||||||
text: "Monitor-specific configuration"
|
Process {
|
||||||
font.pointSize: Style.fontSizeL * scaling
|
id: wlsunsetCheck
|
||||||
font.weight: Style.fontWeightBold
|
command: ["which", "wlsunset"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
onExited: function (exitCode) {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
Settings.data.nightLight.enabled = true
|
||||||
|
NightLightService.apply()
|
||||||
|
ToastService.showNotice("Night Light", "Enabled")
|
||||||
|
} else {
|
||||||
|
Settings.data.nightLight.enabled = false
|
||||||
|
ToastService.showWarning("Night Light", "wlsunset not installed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout: StdioCollector {}
|
||||||
|
stderr: StdioCollector {}
|
||||||
}
|
}
|
||||||
|
|
||||||
NText {
|
spacing: Style.marginL * scaling
|
||||||
text: "Bars and notifications appear on all displays by default. Choose specific displays below to limit where they're shown."
|
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
NHeader {
|
||||||
color: Color.mOnSurfaceVariant
|
label: "Monitor-specific configuration"
|
||||||
wrapMode: Text.WordWrap
|
description: "Configure scaling and brightness settings individually for each connected display."
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: Style.marginL * scaling
|
spacing: Style.marginL * scaling
|
||||||
Layout.topMargin: Style.marginL * scaling
|
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: Quickshell.screens || []
|
model: Quickshell.screens || []
|
||||||
|
|
@ -46,11 +65,13 @@ ColumnLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling
|
implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling
|
||||||
radius: Style.radiusM * scaling
|
radius: Style.radiusM * scaling
|
||||||
color: Color.mSurface
|
color: Color.mSurfaceVariant
|
||||||
border.color: Color.mOutline
|
border.color: Color.mOutline
|
||||||
border.width: Math.max(1, Style.borderS * scaling)
|
border.width: Math.max(1, Style.borderS * scaling)
|
||||||
|
|
||||||
property real localScaling: ScalingService.getScreenScale(modelData)
|
property real localScaling: ScalingService.getScreenScale(modelData)
|
||||||
|
property var brightnessMonitor: BrightnessService.getMonitorForScreen(modelData)
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: ScalingService
|
target: ScalingService
|
||||||
function onScaleChanged(screenName, scale) {
|
function onScaleChanged(screenName, scale) {
|
||||||
|
|
@ -67,119 +88,116 @@ ColumnLayout {
|
||||||
spacing: Style.marginXXS * scaling
|
spacing: Style.marginXXS * scaling
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
text: (modelData.name || "Unknown")
|
text: (`${modelData.name}: ${modelData.model}` || "Unknown")
|
||||||
font.pointSize: Style.fontSizeXL * scaling
|
font.pointSize: Style.fontSizeL * scaling
|
||||||
font.weight: Style.fontWeightBold
|
font.weight: Style.fontWeightBold
|
||||||
color: Color.mSecondary
|
color: Color.mPrimary
|
||||||
}
|
}
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
text: `Resolution: ${modelData.width}x${modelData.height} - Position: (${modelData.x}, ${modelData.y})`
|
text: `Resolution: ${modelData.width}x${modelData.height} at (${modelData.x}, ${modelData.y})`
|
||||||
font.pointSize: Style.fontSizeXS * scaling
|
font.pointSize: Style.fontSizeXS * scaling
|
||||||
color: Color.mOnSurfaceVariant
|
color: Color.mOnSurfaceVariant
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scale
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: Style.marginL * scaling
|
spacing: Style.marginS * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
NToggle {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
spacing: Style.marginM * scaling
|
||||||
label: "Bar"
|
|
||||||
description: "Enable the bar on this monitor."
|
|
||||||
checked: (Settings.data.bar.monitors || []).indexOf(modelData.name) !== -1
|
|
||||||
onToggled: checked => {
|
|
||||||
if (checked) {
|
|
||||||
Settings.data.bar.monitors = addMonitor(Settings.data.bar.monitors, modelData.name)
|
|
||||||
} else {
|
|
||||||
Settings.data.bar.monitors = removeMonitor(Settings.data.bar.monitors, modelData.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NToggle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
label: "Notifications"
|
|
||||||
description: "Enable notifications on this monitor."
|
|
||||||
checked: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1
|
|
||||||
onToggled: checked => {
|
|
||||||
if (checked) {
|
|
||||||
Settings.data.notifications.monitors = addMonitor(Settings.data.notifications.monitors, modelData.name)
|
|
||||||
} else {
|
|
||||||
Settings.data.notifications.monitors = removeMonitor(Settings.data.notifications.monitors, modelData.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NToggle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
label: "Dock"
|
|
||||||
description: "Enable the dock on this monitor."
|
|
||||||
checked: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1
|
|
||||||
onToggled: checked => {
|
|
||||||
if (checked) {
|
|
||||||
Settings.data.dock.monitors = addMonitor(Settings.data.dock.monitors, modelData.name)
|
|
||||||
} else {
|
|
||||||
Settings.data.dock.monitors = removeMonitor(Settings.data.dock.monitors, modelData.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
spacing: Style.marginS * scaling
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
RowLayout {
|
NText {
|
||||||
Layout.fillWidth: true
|
text: "Scale"
|
||||||
spacing: Style.marginL * scaling
|
Layout.preferredWidth: 80 * scaling
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
spacing: Style.marginXXS * scaling
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
NText {
|
|
||||||
text: "Scale"
|
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mOnSurface
|
|
||||||
}
|
|
||||||
NText {
|
|
||||||
text: "Scale the user interface on this monitor."
|
|
||||||
font.pointSize: Style.fontSizeS * scaling
|
|
||||||
color: Color.mOnSurfaceVariant
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NText {
|
|
||||||
text: `${Math.round(localScaling * 100)}%`
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
Layout.minimumWidth: 50 * scaling
|
|
||||||
horizontalAlignment: Text.AlignRight
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
NSlider {
|
||||||
spacing: Style.marginS * scaling
|
id: scaleSlider
|
||||||
|
from: 0.7
|
||||||
|
to: 1.8
|
||||||
|
stepSize: 0.01
|
||||||
|
value: localScaling
|
||||||
|
onPressedChanged: ScalingService.setScreenScale(modelData, value)
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
Layout.minimumWidth: 200 * scaling
|
||||||
|
}
|
||||||
|
|
||||||
NSlider {
|
NText {
|
||||||
id: scaleSlider
|
text: `${Math.round(localScaling * 100)}%`
|
||||||
from: 0.7
|
Layout.preferredWidth: 50 * scaling
|
||||||
to: 1.8
|
horizontalAlignment: Text.AlignRight
|
||||||
stepSize: 0.01
|
}
|
||||||
value: localScaling
|
|
||||||
onPressedChanged: ScalingService.setScreenScale(modelData, value)
|
// Reset button container
|
||||||
Layout.fillWidth: true
|
Item {
|
||||||
}
|
Layout.preferredWidth: 40 * scaling
|
||||||
|
Layout.preferredHeight: 30 * scaling
|
||||||
|
|
||||||
NIconButton {
|
NIconButton {
|
||||||
icon: "refresh"
|
icon: "refresh"
|
||||||
|
sizeRatio: 0.8
|
||||||
tooltipText: "Reset scaling"
|
tooltipText: "Reset scaling"
|
||||||
onClicked: ScalingService.setScreenScale(modelData, 1.0)
|
onClicked: ScalingService.setScreenScale(modelData, 1.0)
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Brightness
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: Style.marginL * scaling
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: brightnessMonitor !== undefined && brightnessMonitor !== null
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: "Brightness"
|
||||||
|
Layout.preferredWidth: 80 * scaling
|
||||||
|
}
|
||||||
|
|
||||||
|
NSlider {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.minimumWidth: 200 * scaling
|
||||||
|
from: 0
|
||||||
|
to: 1
|
||||||
|
value: brightnessMonitor ? brightnessMonitor.brightness : 0.5
|
||||||
|
stepSize: 0.05
|
||||||
|
onPressedChanged: {
|
||||||
|
if (!pressed && brightnessMonitor) {
|
||||||
|
brightnessMonitor.setBrightness(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: brightnessMonitor ? Math.round(brightnessMonitor.brightness * 100) + "%" : "N/A"
|
||||||
|
Layout.preferredWidth: 50 * scaling
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty container to match scale row layout
|
||||||
|
Item {
|
||||||
|
Layout.preferredWidth: 40 * scaling
|
||||||
|
Layout.preferredHeight: 30 * scaling
|
||||||
|
|
||||||
|
// Method text positioned in the button area
|
||||||
|
NText {
|
||||||
|
text: brightnessMonitor ? brightnessMonitor.method : ""
|
||||||
|
font.pointSize: Style.fontSizeXS * scaling
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -188,4 +206,212 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NDivider {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Style.marginXL * scaling
|
||||||
|
Layout.bottomMargin: Style.marginXL * scaling
|
||||||
|
}
|
||||||
|
|
||||||
|
// Brightness Section
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: Style.marginS * scaling
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
NHeader {
|
||||||
|
label: "Brightness"
|
||||||
|
description: "Adjust brightness related settings."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Brightness Step Section
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: Style.marginS * scaling
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
NSpinBox {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
label: "Brightness Step Size"
|
||||||
|
description: "Adjust the step size for brightness changes (scroll wheel and keyboard shortcuts)."
|
||||||
|
minimum: 1
|
||||||
|
maximum: 50
|
||||||
|
value: Settings.data.brightness.brightnessStep
|
||||||
|
stepSize: 1
|
||||||
|
suffix: "%"
|
||||||
|
onValueChanged: {
|
||||||
|
Settings.data.brightness.brightnessStep = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NDivider {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Style.marginXL * scaling
|
||||||
|
Layout.bottomMargin: Style.marginXL * scaling
|
||||||
|
}
|
||||||
|
|
||||||
|
// Night Light Section
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: Style.marginXS * scaling
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
NHeader {
|
||||||
|
label: "Night Light"
|
||||||
|
description: "Reduce blue light emission to help you sleep better and reduce eye strain."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NToggle {
|
||||||
|
label: "Enable Night Light"
|
||||||
|
description: "Apply a warm color filter to reduce blue light emission."
|
||||||
|
checked: Settings.data.nightLight.enabled
|
||||||
|
onToggled: checked => {
|
||||||
|
if (checked) {
|
||||||
|
// Verify wlsunset exists before enabling
|
||||||
|
wlsunsetCheck.running = true
|
||||||
|
} else {
|
||||||
|
Settings.data.nightLight.enabled = false
|
||||||
|
Settings.data.nightLight.forced = false
|
||||||
|
NightLightService.apply()
|
||||||
|
ToastService.showNotice("Night Light", "Disabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temperature
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: Style.marginXS * scaling
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
|
NLabel {
|
||||||
|
label: "Color temperature"
|
||||||
|
description: "Choose two temperatures in Kelvin."
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
visible: Settings.data.nightLight.enabled
|
||||||
|
spacing: Style.marginM * scaling
|
||||||
|
Layout.fillWidth: false
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: "Night"
|
||||||
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
NTextInput {
|
||||||
|
text: Settings.data.nightLight.nightTemp
|
||||||
|
inputMethodHints: Qt.ImhDigitsOnly
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
onEditingFinished: {
|
||||||
|
var nightTemp = parseInt(text)
|
||||||
|
var dayTemp = parseInt(Settings.data.nightLight.dayTemp)
|
||||||
|
if (!isNaN(nightTemp) && !isNaN(dayTemp)) {
|
||||||
|
// Clamp value between [1000 .. (dayTemp-500)]
|
||||||
|
var clampedValue = Math.min(dayTemp - 500, Math.max(1000, nightTemp))
|
||||||
|
text = Settings.data.nightLight.nightTemp = clampedValue.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: "Day"
|
||||||
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
NTextInput {
|
||||||
|
text: Settings.data.nightLight.dayTemp
|
||||||
|
inputMethodHints: Qt.ImhDigitsOnly
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
onEditingFinished: {
|
||||||
|
var dayTemp = parseInt(text)
|
||||||
|
var nightTemp = parseInt(Settings.data.nightLight.nightTemp)
|
||||||
|
if (!isNaN(nightTemp) && !isNaN(dayTemp)) {
|
||||||
|
// Clamp value between [(nightTemp+500) .. 6500]
|
||||||
|
var clampedValue = Math.max(nightTemp + 500, Math.min(6500, dayTemp))
|
||||||
|
text = Settings.data.nightLight.dayTemp = clampedValue.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NToggle {
|
||||||
|
label: "Automatic Scheduling"
|
||||||
|
description: `Based on the sunset and sunrise time in <i>${LocationService.stableName}</i> - recommended.`
|
||||||
|
checked: Settings.data.nightLight.autoSchedule
|
||||||
|
onToggled: checked => Settings.data.nightLight.autoSchedule = checked
|
||||||
|
visible: Settings.data.nightLight.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule settings
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: Style.marginXS * scaling
|
||||||
|
visible: Settings.data.nightLight.enabled && !Settings.data.nightLight.autoSchedule && !Settings.data.nightLight.forced
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: false
|
||||||
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
|
NLabel {
|
||||||
|
label: "Manual Scheduling"
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {// add a little more spacing
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: "Sunrise Time"
|
||||||
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
NComboBox {
|
||||||
|
model: timeOptions
|
||||||
|
currentKey: Settings.data.nightLight.manualSunrise
|
||||||
|
placeholder: "Select start time"
|
||||||
|
onSelected: key => Settings.data.nightLight.manualSunrise = key
|
||||||
|
minimumWidth: 120 * scaling
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {// add a little more spacing
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: "Sunset Time"
|
||||||
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
}
|
||||||
|
NComboBox {
|
||||||
|
model: timeOptions
|
||||||
|
currentKey: Settings.data.nightLight.manualSunset
|
||||||
|
placeholder: "Select stop time"
|
||||||
|
onSelected: key => Settings.data.nightLight.manualSunset = key
|
||||||
|
minimumWidth: 120 * scaling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force activation toggle
|
||||||
|
NToggle {
|
||||||
|
label: "Force activation"
|
||||||
|
description: "Immediately apply night temperature without scheduling or fade."
|
||||||
|
checked: Settings.data.nightLight.forced
|
||||||
|
onToggled: checked => {
|
||||||
|
Settings.data.nightLight.forced = checked
|
||||||
|
if (checked && !Settings.data.nightLight.enabled) {
|
||||||
|
// Ensure enabled when forcing
|
||||||
|
wlsunsetCheck.running = true
|
||||||
|
} else {
|
||||||
|
NightLightService.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visible: Settings.data.nightLight.enabled
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,34 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: contentColumn
|
id: root
|
||||||
spacing: Style.marginL * scaling
|
spacing: Style.marginL * scaling
|
||||||
width: root.width
|
|
||||||
|
|
||||||
|
|
||||||
|
// Helper functions to update arrays immutably
|
||||||
|
function addMonitor(list, name) {
|
||||||
|
const arr = (list || []).slice()
|
||||||
|
if (!arr.includes(name))
|
||||||
|
arr.push(name)
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
function removeMonitor(list, name) {
|
||||||
|
return (list || []).filter(function (n) {
|
||||||
|
return n !== name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
NHeader {
|
||||||
|
label: "Appearance"
|
||||||
|
description: "Configure dock behavior and appearance."
|
||||||
|
}
|
||||||
|
|
||||||
NToggle {
|
NToggle {
|
||||||
label: "Auto-hide"
|
label: "Auto-hide"
|
||||||
|
|
@ -27,7 +47,6 @@ ColumnLayout {
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: Style.marginXXS * scaling
|
spacing: Style.marginXXS * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
NLabel {
|
NLabel {
|
||||||
label: "Background Opacity"
|
label: "Background Opacity"
|
||||||
description: "Adjust the background opacity."
|
description: "Adjust the background opacity."
|
||||||
|
|
@ -81,4 +100,44 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NDivider {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Style.marginXL * scaling
|
||||||
|
Layout.bottomMargin: Style.marginXL * scaling
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitor Configuration
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: Style.marginM * scaling
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
NHeader {
|
||||||
|
label: "Monitors Configuration"
|
||||||
|
description: "Choose which monitors should display the dock."
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Quickshell.screens || []
|
||||||
|
delegate: NCheckbox {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
label: `${modelData.name || "Unknown"}${modelData.model ? `: ${modelData.model}` : ""}`
|
||||||
|
description: `${modelData.width}x${modelData.height} at (${modelData.x}, ${modelData.y})`
|
||||||
|
checked: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1
|
||||||
|
onToggled: checked => {
|
||||||
|
if (checked) {
|
||||||
|
Settings.data.dock.monitors = addMonitor(Settings.data.dock.monitors, modelData.name)
|
||||||
|
} else {
|
||||||
|
Settings.data.dock.monitors = removeMonitor(Settings.data.dock.monitors, modelData.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NDivider {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Style.marginXL * scaling
|
||||||
|
Layout.bottomMargin: Style.marginXL * scaling
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,11 @@ import qs.Widgets
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
NHeader {
|
||||||
|
label: "Profile"
|
||||||
|
description: "Configure your user profile and avatar settings."
|
||||||
|
}
|
||||||
|
|
||||||
// Profile section
|
// Profile section
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
@ -48,12 +53,9 @@ ColumnLayout {
|
||||||
spacing: Style.marginL * scaling
|
spacing: Style.marginL * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
NText {
|
NHeader {
|
||||||
text: "User Interface"
|
label: "User Interface"
|
||||||
font.pointSize: Style.fontSizeXXL * scaling
|
description: "Main settings for the user interface."
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mSecondary
|
|
||||||
Layout.bottomMargin: Style.marginS * scaling
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NToggle {
|
NToggle {
|
||||||
|
|
@ -133,12 +135,9 @@ ColumnLayout {
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: Style.marginL * scaling
|
spacing: Style.marginL * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
NText {
|
NHeader {
|
||||||
text: "Screen Corners"
|
label: "Screen Corners"
|
||||||
font.pointSize: Style.fontSizeXXL * scaling
|
description: "Customize screen corner rounding and visual effects."
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mSecondary
|
|
||||||
Layout.bottomMargin: Style.marginS * scaling
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NToggle {
|
NToggle {
|
||||||
|
|
@ -187,12 +186,10 @@ ColumnLayout {
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: Style.marginL * scaling
|
spacing: Style.marginL * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
NText {
|
|
||||||
text: "Fonts"
|
NHeader {
|
||||||
font.pointSize: Style.fontSizeXXL * scaling
|
label: "Fonts"
|
||||||
font.weight: Style.fontWeightBold
|
description: "Configure interface typography."
|
||||||
color: Color.mSecondary
|
|
||||||
Layout.bottomMargin: Style.marginS * scaling
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Font configuration section
|
// Font configuration section
|
||||||
|
|
@ -200,12 +197,13 @@ ColumnLayout {
|
||||||
spacing: Style.marginL * scaling
|
spacing: Style.marginL * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
NComboBox {
|
NSearchableComboBox {
|
||||||
label: "Default Font"
|
label: "Default Font"
|
||||||
description: "Main font used throughout the interface."
|
description: "Main font used throughout the interface."
|
||||||
model: FontService.availableFonts
|
model: FontService.availableFonts
|
||||||
currentKey: Settings.data.ui.fontDefault
|
currentKey: Settings.data.ui.fontDefault
|
||||||
placeholder: "Select default font..."
|
placeholder: "Select default font..."
|
||||||
|
searchPlaceholder: "Search fonts..."
|
||||||
popupHeight: 420 * scaling
|
popupHeight: 420 * scaling
|
||||||
minimumWidth: 300 * scaling
|
minimumWidth: 300 * scaling
|
||||||
onSelected: function (key) {
|
onSelected: function (key) {
|
||||||
|
|
@ -213,12 +211,13 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NComboBox {
|
NSearchableComboBox {
|
||||||
label: "Fixed Width Font"
|
label: "Fixed Width Font"
|
||||||
description: "Monospace font used for terminal and code display."
|
description: "Monospace font used for terminal and code display."
|
||||||
model: FontService.monospaceFonts
|
model: FontService.monospaceFonts
|
||||||
currentKey: Settings.data.ui.fontFixed
|
currentKey: Settings.data.ui.fontFixed
|
||||||
placeholder: "Select monospace font..."
|
placeholder: "Select monospace font..."
|
||||||
|
searchPlaceholder: "Search monospace fonts..."
|
||||||
popupHeight: 320 * scaling
|
popupHeight: 320 * scaling
|
||||||
minimumWidth: 300 * scaling
|
minimumWidth: 300 * scaling
|
||||||
onSelected: function (key) {
|
onSelected: function (key) {
|
||||||
|
|
@ -226,12 +225,13 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NComboBox {
|
NSearchableComboBox {
|
||||||
label: "Billboard Font"
|
label: "Billboard Font"
|
||||||
description: "Large font used for clocks and prominent displays."
|
description: "Large font used for clocks and prominent displays."
|
||||||
model: FontService.displayFonts
|
model: FontService.displayFonts
|
||||||
currentKey: Settings.data.ui.fontBillboard
|
currentKey: Settings.data.ui.fontBillboard
|
||||||
placeholder: "Select display font..."
|
placeholder: "Select display font..."
|
||||||
|
searchPlaceholder: "Search display fonts..."
|
||||||
popupHeight: 320 * scaling
|
popupHeight: 320 * scaling
|
||||||
minimumWidth: 300 * scaling
|
minimumWidth: 300 * scaling
|
||||||
onSelected: function (key) {
|
onSelected: function (key) {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,11 @@ ColumnLayout {
|
||||||
spacing: Style.marginL * scaling
|
spacing: Style.marginL * scaling
|
||||||
width: root.width
|
width: root.width
|
||||||
|
|
||||||
|
NHeader {
|
||||||
|
label: "System Hooks"
|
||||||
|
description: "Configure commands to be executed when system events occur."
|
||||||
|
}
|
||||||
|
|
||||||
// Enable/Disable Toggle
|
// Enable/Disable Toggle
|
||||||
NToggle {
|
NToggle {
|
||||||
label: "Enable Hooks"
|
label: "Enable Hooks"
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,12 @@ import qs.Widgets
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: root
|
id: root
|
||||||
|
spacing: Style.marginL * scaling
|
||||||
|
|
||||||
ColumnLayout {
|
NHeader {
|
||||||
spacing: Style.marginL * scaling
|
label: "Appearance"
|
||||||
|
description: "Configure the launcher behavior and appearance."
|
||||||
|
}
|
||||||
|
|
||||||
NComboBox {
|
NComboBox {
|
||||||
id: launcherPosition
|
id: launcherPosition
|
||||||
|
|
@ -105,7 +108,7 @@ ColumnLayout {
|
||||||
checked: Settings.data.appLauncher.useApp2Unit
|
checked: Settings.data.appLauncher.useApp2Unit
|
||||||
onToggled: checked => Settings.data.appLauncher.useApp2Unit = checked
|
onToggled: checked => Settings.data.appLauncher.useApp2Unit = checked
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
NDivider {
|
NDivider {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,11 @@ ColumnLayout {
|
||||||
id: root
|
id: root
|
||||||
spacing: Style.marginL * scaling
|
spacing: Style.marginL * scaling
|
||||||
|
|
||||||
|
NHeader {
|
||||||
|
label: "Network Settings"
|
||||||
|
description: "Configure Wi-Fi and Bluetooth connectivity options."
|
||||||
|
}
|
||||||
|
|
||||||
NToggle {
|
NToggle {
|
||||||
label: "Enable Wi-Fi"
|
label: "Enable Wi-Fi"
|
||||||
description: "Enable Wi-Fi connectivity."
|
description: "Enable Wi-Fi connectivity."
|
||||||
|
|
|
||||||
183
Modules/SettingsPanel/Tabs/NotificationTab.qml
Normal file
183
Modules/SettingsPanel/Tabs/NotificationTab.qml
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Helper functions to update arrays immutably
|
||||||
|
function addMonitor(list, name) {
|
||||||
|
const arr = (list || []).slice()
|
||||||
|
if (!arr.includes(name))
|
||||||
|
arr.push(name)
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
function removeMonitor(list, name) {
|
||||||
|
return (list || []).filter(function (n) {
|
||||||
|
return n !== name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// General Notification Settings
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: Style.marginL * scaling
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
NHeader {
|
||||||
|
label: "Appearance"
|
||||||
|
description: "Configure notifications appearance and behavior."
|
||||||
|
}
|
||||||
|
|
||||||
|
NToggle {
|
||||||
|
label: "Do Not Disturb"
|
||||||
|
description: "Disable all notification popups when enabled."
|
||||||
|
checked: Settings.data.notifications.doNotDisturb
|
||||||
|
onToggled: checked => Settings.data.notifications.doNotDisturb = checked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NDivider {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Style.marginXL * scaling
|
||||||
|
Layout.bottomMargin: Style.marginXL * scaling
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitor Configuration
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: Style.marginM * scaling
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
NHeader {
|
||||||
|
label: "Monitors Configuration"
|
||||||
|
description: "Choose which monitors should display notifications."
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Quickshell.screens || []
|
||||||
|
delegate: NCheckbox {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
label: `${modelData.name || "Unknown"}${modelData.model ? `: ${modelData.model}` : ""}`
|
||||||
|
description: `${modelData.width}x${modelData.height} at (${modelData.x}, ${modelData.y})`
|
||||||
|
checked: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1
|
||||||
|
onToggled: checked => {
|
||||||
|
if (checked) {
|
||||||
|
Settings.data.notifications.monitors = addMonitor(Settings.data.notifications.monitors, modelData.name)
|
||||||
|
} else {
|
||||||
|
Settings.data.notifications.monitors = removeMonitor(Settings.data.notifications.monitors, modelData.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NDivider {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Style.marginXL * scaling
|
||||||
|
Layout.bottomMargin: Style.marginXL * scaling
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification Duration Settings
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: Style.marginL * scaling
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
NHeader {
|
||||||
|
label: "Notification Duration"
|
||||||
|
description: "Configure how long notifications stay visible based on their urgency level."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Low Urgency Duration
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: Style.marginXXS * scaling
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
NLabel {
|
||||||
|
label: "Low Urgency Duration"
|
||||||
|
description: "How long low priority notifications stay visible."
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
NSlider {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
from: 1
|
||||||
|
to: 30
|
||||||
|
stepSize: 1
|
||||||
|
value: Settings.data.notifications.lowUrgencyDuration
|
||||||
|
onMoved: Settings.data.notifications.lowUrgencyDuration = value
|
||||||
|
cutoutColor: Color.mSurface
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: Settings.data.notifications.lowUrgencyDuration + "s"
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
Layout.leftMargin: Style.marginS * scaling
|
||||||
|
color: Color.mOnSurface
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal Urgency Duration
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: Style.marginXXS * scaling
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
NLabel {
|
||||||
|
label: "Normal Urgency Duration"
|
||||||
|
description: "How long normal priority notifications stay visible."
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
NSlider {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
from: 1
|
||||||
|
to: 30
|
||||||
|
stepSize: 1
|
||||||
|
value: Settings.data.notifications.normalUrgencyDuration
|
||||||
|
onMoved: Settings.data.notifications.normalUrgencyDuration = value
|
||||||
|
cutoutColor: Color.mSurface
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: Settings.data.notifications.normalUrgencyDuration + "s"
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
Layout.leftMargin: Style.marginS * scaling
|
||||||
|
color: Color.mOnSurface
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Critical Urgency Duration
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: Style.marginXXS * scaling
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
NLabel {
|
||||||
|
label: "Critical Urgency Duration"
|
||||||
|
description: "How long critical priority notifications stay visible."
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
NSlider {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
from: 1
|
||||||
|
to: 30
|
||||||
|
stepSize: 1
|
||||||
|
value: Settings.data.notifications.criticalUrgencyDuration
|
||||||
|
onMoved: Settings.data.notifications.criticalUrgencyDuration = value
|
||||||
|
cutoutColor: Color.mSurface
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: Settings.data.notifications.criticalUrgencyDuration + "s"
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
Layout.leftMargin: Style.marginS * scaling
|
||||||
|
color: Color.mOnSurface
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,11 @@ ColumnLayout {
|
||||||
|
|
||||||
spacing: Style.marginL * scaling
|
spacing: Style.marginL * scaling
|
||||||
|
|
||||||
|
NHeader {
|
||||||
|
label: "General Settings"
|
||||||
|
description: "Configure screen recording output and content."
|
||||||
|
}
|
||||||
|
|
||||||
// Output Directory
|
// Output Directory
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
|
|
@ -53,12 +58,8 @@ ColumnLayout {
|
||||||
spacing: Style.marginL * scaling
|
spacing: Style.marginL * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
NText {
|
NHeader {
|
||||||
text: "Video Settings"
|
label: "Video Settings"
|
||||||
font.pointSize: Style.fontSizeXXL * scaling
|
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mSecondary
|
|
||||||
Layout.bottomMargin: Style.marginS * scaling
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Source
|
// Source
|
||||||
|
|
@ -203,12 +204,8 @@ ColumnLayout {
|
||||||
spacing: Style.marginL * scaling
|
spacing: Style.marginL * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
NText {
|
NHeader {
|
||||||
text: "Audio Settings"
|
label: "Audio Settings"
|
||||||
font.pointSize: Style.fontSizeXXL * scaling
|
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mSecondary
|
|
||||||
Layout.bottomMargin: Style.marginS * scaling
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Audio Source
|
// Audio Source
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import qs.Widgets
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: root
|
id: root
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|
||||||
spacing: Style.marginL * scaling
|
spacing: Style.marginL * scaling
|
||||||
|
|
||||||
property list<string> wallpapersList: []
|
property list<string> wallpapersList: []
|
||||||
|
|
@ -42,11 +41,9 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Current wallpaper display
|
// Current wallpaper display
|
||||||
NText {
|
NHeader {
|
||||||
text: "Current Wallpaper"
|
label: "Current Wallpaper"
|
||||||
font.pointSize: Style.fontSizeXXL * scaling
|
description: "Preview and manage your desktop background."
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mSecondary
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
|
@ -80,18 +77,9 @@ ColumnLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
// Wallpaper grid
|
// Wallpaper grid
|
||||||
NText {
|
NHeader {
|
||||||
text: "Wallpaper Selector"
|
label: "Wallpaper Selector"
|
||||||
font.pointSize: Style.fontSizeXXL * scaling
|
description: "Click on a wallpaper to set it as your current wallpaper."
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mSecondary
|
|
||||||
}
|
|
||||||
|
|
||||||
NText {
|
|
||||||
text: "Click on a wallpaper to set it as your current wallpaper."
|
|
||||||
color: Color.mOnSurfaceVariant
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,12 @@ import qs.Widgets
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: root
|
id: root
|
||||||
|
spacing: Style.marginL * scaling
|
||||||
|
|
||||||
|
NHeader {
|
||||||
|
label: "Wallpaper Settings"
|
||||||
|
description: "Control how wallpapers are managed and displayed."
|
||||||
|
}
|
||||||
|
|
||||||
NToggle {
|
NToggle {
|
||||||
label: "Enable Wallpaper Management"
|
label: "Enable Wallpaper Management"
|
||||||
|
|
@ -22,6 +28,7 @@ ColumnLayout {
|
||||||
visible: Settings.data.wallpaper.enabled
|
visible: Settings.data.wallpaper.enabled
|
||||||
spacing: Style.marginL * scaling
|
spacing: Style.marginL * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
NTextInput {
|
NTextInput {
|
||||||
label: "Wallpaper Directory"
|
label: "Wallpaper Directory"
|
||||||
description: "Path to your common wallpaper directory."
|
description: "Path to your common wallpaper directory."
|
||||||
|
|
@ -61,7 +68,7 @@ ColumnLayout {
|
||||||
delegate: RowLayout {
|
delegate: RowLayout {
|
||||||
NText {
|
NText {
|
||||||
text: (modelData.name || "Unknown")
|
text: (modelData.name || "Unknown")
|
||||||
color: Color.mSecondary
|
color: Color.mPrimary
|
||||||
font.weight: Style.fontWeightBold
|
font.weight: Style.fontWeightBold
|
||||||
Layout.preferredWidth: 90 * scaling
|
Layout.preferredWidth: 90 * scaling
|
||||||
}
|
}
|
||||||
|
|
@ -89,11 +96,8 @@ ColumnLayout {
|
||||||
spacing: Style.marginL * scaling
|
spacing: Style.marginL * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
NText {
|
NHeader {
|
||||||
text: "Look & Feel"
|
label: "Look & Feel"
|
||||||
font.pointSize: Style.fontSizeXXL * scaling
|
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mSecondary
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill Mode
|
// Fill Mode
|
||||||
|
|
@ -189,11 +193,8 @@ ColumnLayout {
|
||||||
spacing: Style.marginL * scaling
|
spacing: Style.marginL * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
NText {
|
NHeader {
|
||||||
text: "Automation"
|
label: "Automation"
|
||||||
font.pointSize: Style.fontSizeXXL * scaling
|
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mSecondary
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Random Wallpaper
|
// Random Wallpaper
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,12 @@ import qs.Widgets
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: root
|
id: root
|
||||||
|
spacing: Style.marginL * scaling
|
||||||
|
|
||||||
|
NHeader {
|
||||||
|
label: "Your Location"
|
||||||
|
description: "Set your location for weather, time zones, and scheduling."
|
||||||
|
}
|
||||||
|
|
||||||
// Location section
|
// Location section
|
||||||
RowLayout {
|
RowLayout {
|
||||||
|
|
@ -57,11 +63,9 @@ ColumnLayout {
|
||||||
spacing: Style.marginM * scaling
|
spacing: Style.marginM * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
NText {
|
NHeader {
|
||||||
text: "Weather"
|
label: "Weather"
|
||||||
font.pointSize: Style.fontSizeXXL * scaling
|
description: "Configure weather display preferences and temperature units."
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mSecondary
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NToggle {
|
NToggle {
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,11 @@ Variants {
|
||||||
|
|
||||||
screen: modelData
|
screen: modelData
|
||||||
|
|
||||||
// Position based on bar location, like Notification popup does
|
// Position at top of screen, always allow horizontal centering
|
||||||
anchors {
|
anchors {
|
||||||
top: Settings.data.bar.position === "top"
|
top: true
|
||||||
bottom: Settings.data.bar.position === "bottom"
|
left: true
|
||||||
|
right: true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set a width instead of anchoring left/right so we can click on the side of the toast
|
// Set a width instead of anchoring left/right so we can click on the side of the toast
|
||||||
|
|
@ -43,8 +44,43 @@ Variants {
|
||||||
implicitHeight: Math.round(toast.visible ? toast.height + Style.marginM * scaling : 1)
|
implicitHeight: Math.round(toast.visible ? toast.height + Style.marginM * scaling : 1)
|
||||||
|
|
||||||
// Set margins based on bar position
|
// Set margins based on bar position
|
||||||
margins.top: Settings.data.bar.position === "top" ? (Style.barHeight + Style.marginS + (Settings.data.bar.floating ? Settings.data.bar.marginVertical : 0)) * scaling : 0
|
margins.top: {
|
||||||
margins.bottom: Settings.data.bar.position === "bottom" ? (Style.barHeight + Style.marginS + (Settings.data.bar.floating ? Settings.data.bar.marginVertical : 0)) * scaling : 0
|
switch (Settings.data.bar.position) {
|
||||||
|
case "top":
|
||||||
|
return (Style.barHeight + Style.marginS) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
margins.bottom: {
|
||||||
|
switch (Settings.data.bar.position) {
|
||||||
|
case "bottom":
|
||||||
|
return (Style.barHeight + Style.marginS) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
margins.right: {
|
||||||
|
switch (Settings.data.bar.position) {
|
||||||
|
case "left":
|
||||||
|
case "top":
|
||||||
|
case "bottom":
|
||||||
|
return Style.marginM * scaling
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
margins.left: {
|
||||||
|
switch (Settings.data.bar.position) {
|
||||||
|
case "right":
|
||||||
|
return Style.marginM * scaling
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Transparent background
|
// Transparent background
|
||||||
color: Color.transparent
|
color: Color.transparent
|
||||||
|
|
@ -61,8 +97,8 @@ Variants {
|
||||||
// Simple positioning - margins already account for bar
|
// Simple positioning - margins already account for bar
|
||||||
targetY: Style.marginS * scaling
|
targetY: Style.marginS * scaling
|
||||||
|
|
||||||
// Hidden position based on bar location
|
// Hidden position - always start from above the screen
|
||||||
hiddenY: Settings.data.bar.position === "top" ? -toast.height - 20 : toast.height + 20
|
hiddenY: -toast.height - 20
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
// Register this toast with the service
|
// Register this toast with the service
|
||||||
|
|
@ -74,4 +110,4 @@ Variants {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -64,7 +64,7 @@ NPanel {
|
||||||
|
|
||||||
NIconButton {
|
NIconButton {
|
||||||
icon: "close"
|
icon: "close"
|
||||||
tooltipText: "Close"
|
tooltipText: "Close."
|
||||||
sizeRatio: 0.8
|
sizeRatio: 0.8
|
||||||
onClicked: root.close()
|
onClicked: root.close()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,9 +87,26 @@ Singleton {
|
||||||
// Maximum visible notifications
|
// Maximum visible notifications
|
||||||
property int maxVisible: 5
|
property int maxVisible: 5
|
||||||
|
|
||||||
|
// Function to get duration based on urgency
|
||||||
|
function getDurationForUrgency(urgency) {
|
||||||
|
switch (urgency) {
|
||||||
|
case 0:
|
||||||
|
// Low urgency
|
||||||
|
return (Settings.data.notifications.lowUrgencyDuration || 3) * 1000
|
||||||
|
case 1:
|
||||||
|
// Normal urgency
|
||||||
|
return (Settings.data.notifications.normalUrgencyDuration || 8) * 1000
|
||||||
|
case 2:
|
||||||
|
// Critical urgency
|
||||||
|
return (Settings.data.notifications.criticalUrgencyDuration || 15) * 1000
|
||||||
|
default:
|
||||||
|
return (Settings.data.notifications.normalUrgencyDuration || 8) * 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-hide timer
|
// Auto-hide timer
|
||||||
property Timer hideTimer: Timer {
|
property Timer hideTimer: Timer {
|
||||||
interval: 8000 // 8 seconds - longer display time
|
interval: 1000 // Check every second
|
||||||
repeat: true
|
repeat: true
|
||||||
running: notificationModel.count > 0
|
running: notificationModel.count > 0
|
||||||
|
|
||||||
|
|
@ -98,11 +115,26 @@ Singleton {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the oldest notification (last in the list)
|
// Check each notification for expiration
|
||||||
let oldestNotification = notificationModel.get(notificationModel.count - 1).rawNotification
|
for (var i = notificationModel.count - 1; i >= 0; i--) {
|
||||||
if (oldestNotification) {
|
let notificationData = notificationModel.get(i)
|
||||||
// Trigger animation signal instead of direct dismiss
|
if (notificationData && notificationData.rawNotification) {
|
||||||
animateAndRemove(oldestNotification, notificationModel.count - 1)
|
let notification = notificationData.rawNotification
|
||||||
|
let urgency = notificationData.urgency
|
||||||
|
let timestamp = notificationData.timestamp
|
||||||
|
|
||||||
|
// Calculate if this notification should be removed
|
||||||
|
let duration = getDurationForUrgency(urgency)
|
||||||
|
let now = new Date()
|
||||||
|
let elapsed = now.getTime() - timestamp.getTime()
|
||||||
|
|
||||||
|
if (elapsed >= duration) {
|
||||||
|
// Trigger animation signal instead of direct dismiss
|
||||||
|
animateAndRemove(notification, i)
|
||||||
|
break
|
||||||
|
// Only remove one notification per check to avoid conflicts
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ RowLayout {
|
||||||
property bool hovering: false
|
property bool hovering: false
|
||||||
property color activeColor: Color.mPrimary
|
property color activeColor: Color.mPrimary
|
||||||
property color activeOnColor: Color.mOnPrimary
|
property color activeOnColor: Color.mOnPrimary
|
||||||
property int baseSize: Math.max(Style.baseWidgetSize * 0.8, 14)
|
property int baseSize: Math.max(Style.baseWidgetSize * 0.7, 14)
|
||||||
|
|
||||||
signal toggled(bool checked)
|
signal toggled(bool checked)
|
||||||
signal entered
|
signal entered
|
||||||
|
|
@ -39,7 +39,7 @@ RowLayout {
|
||||||
implicitHeight: root.baseSize * scaling
|
implicitHeight: root.baseSize * scaling
|
||||||
radius: Style.radiusXS * scaling
|
radius: Style.radiusXS * scaling
|
||||||
color: root.checked ? root.activeColor : Color.mSurface
|
color: root.checked ? root.activeColor : Color.mSurface
|
||||||
border.color: root.checked ? root.activeColor : Color.mOutline
|
border.color: Color.mOutline
|
||||||
border.width: Math.max(1, Style.borderM * scaling)
|
border.width: Math.max(1, Style.borderM * scaling)
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
|
|
@ -57,9 +57,10 @@ RowLayout {
|
||||||
NIcon {
|
NIcon {
|
||||||
visible: root.checked
|
visible: root.checked
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
anchors.horizontalCenterOffset: -1 * scaling
|
||||||
icon: "check"
|
icon: "check"
|
||||||
color: root.activeOnColor
|
color: root.activeOnColor
|
||||||
font.pointSize: Math.max(Style.fontSizeS, root.baseSize * 0.7) * scaling
|
font.pointSize: Math.max(Style.fontSizeXS, root.baseSize * 0.6) * scaling
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
|
|
||||||
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 !== ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -40,6 +40,7 @@ Loader {
|
||||||
property real opacityValue: originalOpacity
|
property real opacityValue: originalOpacity
|
||||||
|
|
||||||
property alias isClosing: hideTimer.running
|
property alias isClosing: hideTimer.running
|
||||||
|
readonly property string barPosition: Settings.data.bar.position
|
||||||
|
|
||||||
signal opened
|
signal opened
|
||||||
signal closed
|
signal closed
|
||||||
|
|
@ -141,6 +142,7 @@ Loader {
|
||||||
// PanelWindow has its own screen property inherited of QsWindow
|
// PanelWindow has its own screen property inherited of QsWindow
|
||||||
property real scaling: ScalingService.getScreenScale(screen)
|
property real scaling: ScalingService.getScreenScale(screen)
|
||||||
readonly property real barHeight: Math.round(Style.barHeight * scaling)
|
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 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 bool barIsVisible: (screen !== null) && (Settings.data.bar.monitors.includes(screen.name) || (Settings.data.bar.monitors.length === 0))
|
||||||
|
|
||||||
|
|
@ -148,7 +150,7 @@ Loader {
|
||||||
target: ScalingService
|
target: ScalingService
|
||||||
function onScaleChanged(screenName, scale) {
|
function onScaleChanged(screenName, scale) {
|
||||||
if ((screen !== null) && (screenName === screen.name)) {
|
if ((screen !== null) && (screenName === screen.name)) {
|
||||||
root.scaling = scale
|
root.scaling = scaling = scale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -157,7 +159,7 @@ Loader {
|
||||||
target: panelWindow
|
target: panelWindow
|
||||||
function onScreenChanged() {
|
function onScreenChanged() {
|
||||||
root.screen = screen
|
root.screen = screen
|
||||||
root.scaling = ScalingService.getScreenScale(screen)
|
root.scaling = scaling = ScalingService.getScreenScale(screen)
|
||||||
|
|
||||||
// It's mandatory to force refresh the subloader to ensure the scaling is properly dispatched
|
// It's mandatory to force refresh the subloader to ensure the scaling is properly dispatched
|
||||||
panelContentLoader.active = false
|
panelContentLoader.active = false
|
||||||
|
|
@ -184,8 +186,29 @@ Loader {
|
||||||
anchors.left: true
|
anchors.left: true
|
||||||
anchors.right: true
|
anchors.right: true
|
||||||
anchors.bottom: true
|
anchors.bottom: true
|
||||||
margins.top: (barIsVisible && !barAtBottom) ? (barHeight + ((Settings.data.bar.floating && !panelAnchorVerticalCenter) ? Settings.data.bar.marginVertical : 0)) : 0
|
margins.top: {
|
||||||
margins.bottom: (barIsVisible && barAtBottom) ? (barHeight + ((Settings.data.bar.floating && !panelAnchorVerticalCenter) ? Settings.data.bar.marginVertical : 0)) : 0
|
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
|
// Close any panel with Esc without requiring focus
|
||||||
Shortcut {
|
Shortcut {
|
||||||
|
|
@ -237,31 +260,119 @@ Loader {
|
||||||
y: calculatedY
|
y: calculatedY
|
||||||
|
|
||||||
property int calculatedX: {
|
property int calculatedX: {
|
||||||
if (root.useButtonPosition) {
|
var barPosition = Settings.data.bar.position
|
||||||
// Position panel relative to button
|
|
||||||
var targetX = root.buttonPosition.x + (root.buttonWidth / 2) - (preferredWidth / 2)
|
|
||||||
|
|
||||||
// Keep panel within screen bounds
|
// Check anchor properties first, even when using button positioning
|
||||||
var maxX = panelWindow.width - panelBackground.width - (Style.marginS * scaling)
|
if (!panelAnchorHorizontalCenter && panelAnchorLeft) {
|
||||||
var minX = Style.marginS * scaling
|
|
||||||
|
|
||||||
return Math.round(Math.max(minX, Math.min(targetX, maxX)))
|
|
||||||
} else if (!panelAnchorHorizontalCenter && panelAnchorLeft) {
|
|
||||||
return Math.round(Style.marginS * scaling)
|
return Math.round(Style.marginS * scaling)
|
||||||
} else if (!panelAnchorHorizontalCenter && panelAnchorRight) {
|
} else if (!panelAnchorHorizontalCenter && panelAnchorRight) {
|
||||||
return Math.round(panelWindow.width - panelBackground.width - (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 {
|
} else {
|
||||||
return Math.round((panelWindow.width - panelBackground.width) / 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: {
|
property int calculatedY: {
|
||||||
if (panelAnchorVerticalCenter) {
|
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)
|
return Math.round((panelWindow.height - panelBackground.height) / 2)
|
||||||
} else if (panelAnchorBottom) {
|
} else if (panelAnchorBottom) {
|
||||||
return Math.round(panelWindow.height - panelBackground.height - (Style.marginS * scaling))
|
return Math.round(panelWindow.height - panelBackground.height - (Style.marginS * scaling))
|
||||||
} else if (panelAnchorTop) {
|
} else if (panelAnchorTop) {
|
||||||
return Math.round(Style.marginS * scaling)
|
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) {
|
} else if (!barAtBottom) {
|
||||||
// Below the top bar
|
// Below the top bar
|
||||||
return Math.round(Style.marginS * scaling)
|
return Math.round(Style.marginS * scaling)
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ Item {
|
||||||
property bool hovered: false
|
property bool hovered: false
|
||||||
property real fontSize: Style.fontSizeXS
|
property real fontSize: Style.fontSizeXS
|
||||||
|
|
||||||
// Effective shown state (true if hovered/animated open or forced)
|
readonly property string barPosition: Settings.data.bar.position
|
||||||
readonly property bool revealed: forceOpen || showPill
|
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
|
||||||
|
|
||||||
signal shown
|
signal shown
|
||||||
signal hidden
|
signal hidden
|
||||||
|
|
@ -29,258 +29,81 @@ Item {
|
||||||
signal middleClicked
|
signal middleClicked
|
||||||
signal wheel(int delta)
|
signal wheel(int delta)
|
||||||
|
|
||||||
// Internal state
|
// Dynamic sizing based on loaded component
|
||||||
property bool showPill: false
|
width: pillLoader.item ? pillLoader.item.width : 0
|
||||||
property bool shouldAnimateHide: false
|
height: pillLoader.item ? pillLoader.item.height : 0
|
||||||
|
|
||||||
// Exposed width logic
|
// Loader to switch between vertical and horizontal pill implementations
|
||||||
readonly property int iconSize: Math.round(Style.baseWidgetSize * sizeRatio * scaling)
|
Loader {
|
||||||
readonly property int pillHeight: iconSize
|
id: pillLoader
|
||||||
readonly property int pillPaddingHorizontal: Style.marginS * scaling
|
sourceComponent: isVerticalBar ? verticalPillComponent : horizontalPillComponent
|
||||||
readonly property int pillOverlap: iconSize * 0.5
|
|
||||||
readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap)
|
|
||||||
|
|
||||||
width: iconSize + Math.max(0, pill.width - pillOverlap)
|
Component {
|
||||||
height: pillHeight
|
id: verticalPillComponent
|
||||||
|
NPillVertical {
|
||||||
|
icon: root.icon
|
||||||
|
text: root.text
|
||||||
|
tooltipText: root.tooltipText
|
||||||
|
sizeRatio: root.sizeRatio
|
||||||
|
autoHide: root.autoHide
|
||||||
|
forceOpen: root.forceOpen
|
||||||
|
disableOpen: root.disableOpen
|
||||||
|
rightOpen: root.rightOpen
|
||||||
|
hovered: root.hovered
|
||||||
|
fontSize: root.fontSize
|
||||||
|
|
||||||
Rectangle {
|
onShown: root.shown()
|
||||||
id: pill
|
onHidden: root.hidden()
|
||||||
width: revealed ? maxPillWidth : 1
|
onEntered: root.entered()
|
||||||
height: pillHeight
|
onExited: root.exited()
|
||||||
|
onClicked: root.clicked()
|
||||||
x: rightOpen ? (iconCircle.x + iconCircle.width / 2) : // Opens right
|
onRightClicked: root.rightClicked()
|
||||||
(iconCircle.x + iconCircle.width / 2) - width // Opens left
|
onMiddleClicked: root.middleClicked()
|
||||||
|
onWheel: root.wheel
|
||||||
opacity: revealed ? Style.opacityFull : Style.opacityNone
|
|
||||||
color: Color.mSurfaceVariant
|
|
||||||
|
|
||||||
topLeftRadius: rightOpen ? 0 : pillHeight * 0.5
|
|
||||||
bottomLeftRadius: rightOpen ? 0 : pillHeight * 0.5
|
|
||||||
topRightRadius: rightOpen ? pillHeight * 0.5 : 0
|
|
||||||
bottomRightRadius: rightOpen ? pillHeight * 0.5 : 0
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
NText {
|
|
||||||
id: textItem
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
x: {
|
|
||||||
// Little tweak to have a better text horizontal centering
|
|
||||||
var centerX = (parent.width - width) / 2
|
|
||||||
var offset = rightOpen ? Style.marginXS * scaling : -Style.marginXS * scaling
|
|
||||||
return centerX + offset
|
|
||||||
}
|
|
||||||
text: root.text
|
|
||||||
font.pointSize: root.fontSize * scaling
|
|
||||||
font.weight: Style.fontWeightBold
|
|
||||||
color: Color.mPrimary
|
|
||||||
visible: revealed
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
enabled: showAnim.running || hideAnim.running
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Style.animationNormal
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Behavior on opacity {
|
|
||||||
enabled: showAnim.running || hideAnim.running
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Style.animationNormal
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: iconCircle
|
|
||||||
width: iconSize
|
|
||||||
height: iconSize
|
|
||||||
radius: width * 0.5
|
|
||||||
color: hovered && !forceOpen ? Color.mTertiary : Color.mSurfaceVariant
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
x: rightOpen ? 0 : (parent.width - width)
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Style.animationNormal
|
|
||||||
easing.type: Easing.InOutQuad
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NIcon {
|
Component {
|
||||||
icon: root.icon
|
id: horizontalPillComponent
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
NPillHorizontal {
|
||||||
color: hovered && !forceOpen ? Color.mOnTertiary : Color.mOnSurface
|
icon: root.icon
|
||||||
// Center horizontally
|
text: root.text
|
||||||
x: (iconCircle.width - width) / 2
|
tooltipText: root.tooltipText
|
||||||
// Center vertically accounting for font metrics
|
sizeRatio: root.sizeRatio
|
||||||
y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2
|
autoHide: root.autoHide
|
||||||
}
|
forceOpen: root.forceOpen
|
||||||
}
|
disableOpen: root.disableOpen
|
||||||
|
rightOpen: root.rightOpen
|
||||||
|
hovered: root.hovered
|
||||||
|
fontSize: root.fontSize
|
||||||
|
|
||||||
ParallelAnimation {
|
onShown: root.shown()
|
||||||
id: showAnim
|
onHidden: root.hidden()
|
||||||
running: false
|
onEntered: root.entered()
|
||||||
NumberAnimation {
|
onExited: root.exited()
|
||||||
target: pill
|
onClicked: root.clicked()
|
||||||
property: "width"
|
onRightClicked: root.rightClicked()
|
||||||
from: 1
|
onMiddleClicked: root.middleClicked()
|
||||||
to: maxPillWidth
|
onWheel: root.wheel
|
||||||
duration: Style.animationNormal
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
NumberAnimation {
|
|
||||||
target: pill
|
|
||||||
property: "opacity"
|
|
||||||
from: 0
|
|
||||||
to: 1
|
|
||||||
duration: Style.animationNormal
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
onStarted: {
|
|
||||||
showPill = true
|
|
||||||
}
|
|
||||||
onStopped: {
|
|
||||||
delayedHideAnim.start()
|
|
||||||
root.shown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SequentialAnimation {
|
|
||||||
id: delayedHideAnim
|
|
||||||
running: false
|
|
||||||
PauseAnimation {
|
|
||||||
duration: 2500
|
|
||||||
}
|
|
||||||
ScriptAction {
|
|
||||||
script: if (shouldAnimateHide) {
|
|
||||||
hideAnim.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ParallelAnimation {
|
|
||||||
id: hideAnim
|
|
||||||
running: false
|
|
||||||
NumberAnimation {
|
|
||||||
target: pill
|
|
||||||
property: "width"
|
|
||||||
from: maxPillWidth
|
|
||||||
to: 1
|
|
||||||
duration: Style.animationNormal
|
|
||||||
easing.type: Easing.InCubic
|
|
||||||
}
|
|
||||||
NumberAnimation {
|
|
||||||
target: pill
|
|
||||||
property: "opacity"
|
|
||||||
from: 1
|
|
||||||
to: 0
|
|
||||||
duration: Style.animationNormal
|
|
||||||
easing.type: Easing.InCubic
|
|
||||||
}
|
|
||||||
onStopped: {
|
|
||||||
showPill = false
|
|
||||||
shouldAnimateHide = false
|
|
||||||
root.hidden()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NTooltip {
|
|
||||||
id: tooltip
|
|
||||||
positionAbove: Settings.data.bar.position === "bottom"
|
|
||||||
target: pill
|
|
||||||
delay: Style.tooltipDelayLong
|
|
||||||
text: root.tooltipText
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: showTimer
|
|
||||||
interval: Style.pillDelay
|
|
||||||
onTriggered: {
|
|
||||||
if (!showPill) {
|
|
||||||
showAnim.start()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
|
||||||
onEntered: {
|
|
||||||
hovered = true
|
|
||||||
root.entered()
|
|
||||||
tooltip.show()
|
|
||||||
if (disableOpen) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!forceOpen) {
|
|
||||||
showDelayed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onExited: {
|
|
||||||
hovered = false
|
|
||||||
root.exited()
|
|
||||||
if (!forceOpen) {
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
tooltip.hide()
|
|
||||||
}
|
|
||||||
onClicked: function (mouse) {
|
|
||||||
if (mouse.button === Qt.LeftButton) {
|
|
||||||
root.clicked()
|
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
|
||||||
root.rightClicked()
|
|
||||||
} else if (mouse.button === Qt.MiddleButton) {
|
|
||||||
root.middleClicked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onWheel: wheel => {
|
|
||||||
root.wheel(wheel.angleDelta.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
if (!showPill) {
|
if (pillLoader.item && pillLoader.item.show) {
|
||||||
shouldAnimateHide = autoHide
|
pillLoader.item.show()
|
||||||
showAnim.start()
|
|
||||||
} else {
|
|
||||||
hideAnim.stop()
|
|
||||||
delayedHideAnim.restart()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
if (forceOpen) {
|
if (pillLoader.item && pillLoader.item.hide) {
|
||||||
return
|
pillLoader.item.hide()
|
||||||
}
|
}
|
||||||
if (showPill) {
|
|
||||||
hideAnim.start()
|
|
||||||
}
|
|
||||||
showTimer.stop()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showDelayed() {
|
function showDelayed() {
|
||||||
if (!showPill) {
|
if (pillLoader.item && pillLoader.item.showDelayed) {
|
||||||
shouldAnimateHide = autoHide
|
pillLoader.item.showDelayed()
|
||||||
showTimer.start()
|
|
||||||
} else {
|
|
||||||
hideAnim.stop()
|
|
||||||
delayedHideAnim.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onForceOpenChanged: {
|
|
||||||
if (forceOpen) {
|
|
||||||
// Immediately lock open without animations
|
|
||||||
showAnim.stop()
|
|
||||||
hideAnim.stop()
|
|
||||||
delayedHideAnim.stop()
|
|
||||||
showPill = true
|
|
||||||
} else {
|
|
||||||
hide()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
326
Widgets/NPillHorizontal.qml
Normal file
326
Widgets/NPillHorizontal.qml
Normal file
|
|
@ -0,0 +1,326 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string icon: ""
|
||||||
|
property string text: ""
|
||||||
|
property string tooltipText: ""
|
||||||
|
property real sizeRatio: 0.8
|
||||||
|
property bool autoHide: false
|
||||||
|
property bool forceOpen: false
|
||||||
|
property bool disableOpen: false
|
||||||
|
property bool rightOpen: false
|
||||||
|
property bool hovered: false
|
||||||
|
property real fontSize: Style.fontSizeXS
|
||||||
|
|
||||||
|
// Bar position detection for pill direction
|
||||||
|
readonly property string barPosition: Settings.data.bar.position
|
||||||
|
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
|
||||||
|
|
||||||
|
// Determine pill direction based on section position
|
||||||
|
readonly property bool openRightward: rightOpen
|
||||||
|
readonly property bool openLeftward: !rightOpen
|
||||||
|
|
||||||
|
// Effective shown state (true if animated open or forced)
|
||||||
|
readonly property bool revealed: forceOpen || showPill
|
||||||
|
|
||||||
|
signal shown
|
||||||
|
signal hidden
|
||||||
|
signal entered
|
||||||
|
signal exited
|
||||||
|
signal clicked
|
||||||
|
signal rightClicked
|
||||||
|
signal middleClicked
|
||||||
|
signal wheel(int delta)
|
||||||
|
|
||||||
|
// Internal state
|
||||||
|
property bool showPill: false
|
||||||
|
property bool shouldAnimateHide: false
|
||||||
|
|
||||||
|
// Sizing logic for horizontal bars
|
||||||
|
readonly property int iconSize: Math.round(Style.baseWidgetSize * sizeRatio * scaling)
|
||||||
|
readonly property int pillWidth: iconSize
|
||||||
|
readonly property int pillPaddingHorizontal: Style.marginS * scaling
|
||||||
|
readonly property int pillPaddingVertical: Style.marginS * scaling
|
||||||
|
readonly property int pillOverlap: iconSize * 0.5
|
||||||
|
readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 4)
|
||||||
|
readonly property int maxPillHeight: iconSize
|
||||||
|
|
||||||
|
// For horizontal bars: height is just icon size, width includes pill space
|
||||||
|
width: revealed ? (openRightward ? (iconSize + maxPillWidth - pillOverlap) : (iconSize + maxPillWidth - pillOverlap)) : iconSize
|
||||||
|
height: iconSize
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: pill
|
||||||
|
width: revealed ? maxPillWidth : 1
|
||||||
|
height: revealed ? maxPillHeight : 1
|
||||||
|
|
||||||
|
// Position based on direction - center the pill relative to the icon
|
||||||
|
x: openLeftward ? (iconCircle.x + iconCircle.width / 2 - width) : (iconCircle.x + iconCircle.width / 2 - pillOverlap)
|
||||||
|
y: 0
|
||||||
|
|
||||||
|
opacity: revealed ? Style.opacityFull : Style.opacityNone
|
||||||
|
color: Color.mSurfaceVariant
|
||||||
|
border.color: Color.mOutline
|
||||||
|
border.width: Math.max(1, Style.borderS * scaling)
|
||||||
|
|
||||||
|
// Radius logic for horizontal expansion - rounded on the side that connects to icon
|
||||||
|
topLeftRadius: openLeftward ? iconSize * 0.5 : 0
|
||||||
|
bottomLeftRadius: openLeftward ? iconSize * 0.5 : 0
|
||||||
|
topRightRadius: openRightward ? iconSize * 0.5 : 0
|
||||||
|
bottomRightRadius: openRightward ? iconSize * 0.5 : 0
|
||||||
|
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
NText {
|
||||||
|
id: textItem
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.horizontalCenterOffset: openLeftward ? -6 * scaling : 6 * scaling // Adjust based on opening direction
|
||||||
|
text: root.text
|
||||||
|
font.family: Settings.data.ui.fontFixed
|
||||||
|
font.pointSize: Style.fontSizeXXS * scaling
|
||||||
|
font.weight: Style.fontWeightBold
|
||||||
|
color: Color.mOnSurface
|
||||||
|
visible: revealed
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on width {
|
||||||
|
enabled: showAnim.running || hideAnim.running
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on height {
|
||||||
|
enabled: showAnim.running || hideAnim.running
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on opacity {
|
||||||
|
enabled: showAnim.running || hideAnim.running
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: iconCircle
|
||||||
|
width: iconSize
|
||||||
|
height: iconSize
|
||||||
|
radius: width * 0.5
|
||||||
|
color: hovered && !forceOpen ? Color.mTertiary : Color.mSurfaceVariant
|
||||||
|
|
||||||
|
// Icon positioning based on direction
|
||||||
|
x: openLeftward ? (parent.width - width) : 0
|
||||||
|
y: 0
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NIcon {
|
||||||
|
icon: root.icon
|
||||||
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
|
color: hovered && !forceOpen ? Color.mOnTertiary : Color.mOnSurfaceVariant
|
||||||
|
// Center horizontally
|
||||||
|
x: (iconCircle.width - width) / 2
|
||||||
|
// Center vertically accounting for font metrics
|
||||||
|
y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
id: showAnim
|
||||||
|
running: false
|
||||||
|
NumberAnimation {
|
||||||
|
target: pill
|
||||||
|
property: "width"
|
||||||
|
from: 1
|
||||||
|
to: maxPillWidth
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: pill
|
||||||
|
property: "height"
|
||||||
|
from: 1
|
||||||
|
to: maxPillHeight
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: pill
|
||||||
|
property: "opacity"
|
||||||
|
from: 0
|
||||||
|
to: 1
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
onStarted: {
|
||||||
|
showPill = true
|
||||||
|
}
|
||||||
|
onStopped: {
|
||||||
|
delayedHideAnim.start()
|
||||||
|
root.shown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
id: delayedHideAnim
|
||||||
|
running: false
|
||||||
|
PauseAnimation {
|
||||||
|
duration: 2500
|
||||||
|
}
|
||||||
|
ScriptAction {
|
||||||
|
script: if (shouldAnimateHide) {
|
||||||
|
hideAnim.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
id: hideAnim
|
||||||
|
running: false
|
||||||
|
NumberAnimation {
|
||||||
|
target: pill
|
||||||
|
property: "width"
|
||||||
|
from: maxPillWidth
|
||||||
|
to: 1
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.InCubic
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: pill
|
||||||
|
property: "height"
|
||||||
|
from: maxPillHeight
|
||||||
|
to: 1
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.InCubic
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: pill
|
||||||
|
property: "opacity"
|
||||||
|
from: 1
|
||||||
|
to: 0
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.InCubic
|
||||||
|
}
|
||||||
|
onStopped: {
|
||||||
|
showPill = false
|
||||||
|
shouldAnimateHide = false
|
||||||
|
root.hidden()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NTooltip {
|
||||||
|
id: tooltip
|
||||||
|
target: pill
|
||||||
|
text: root.tooltipText
|
||||||
|
positionLeft: barPosition === "right"
|
||||||
|
positionRight: barPosition === "left"
|
||||||
|
positionAbove: Settings.data.bar.position === "bottom"
|
||||||
|
delay: Style.tooltipDelayLong
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: showTimer
|
||||||
|
interval: Style.pillDelay
|
||||||
|
onTriggered: {
|
||||||
|
if (!showPill) {
|
||||||
|
showAnim.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||||
|
onEntered: {
|
||||||
|
hovered = true
|
||||||
|
root.entered()
|
||||||
|
tooltip.show()
|
||||||
|
if (disableOpen) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!forceOpen) {
|
||||||
|
showDelayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onExited: {
|
||||||
|
hovered = false
|
||||||
|
root.exited()
|
||||||
|
if (!forceOpen) {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
tooltip.hide()
|
||||||
|
}
|
||||||
|
onClicked: function (mouse) {
|
||||||
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
root.clicked()
|
||||||
|
} else if (mouse.button === Qt.RightButton) {
|
||||||
|
root.rightClicked()
|
||||||
|
} else if (mouse.button === Qt.MiddleButton) {
|
||||||
|
root.middleClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onWheel: wheel => {
|
||||||
|
root.wheel(wheel.angleDelta.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
if (!showPill) {
|
||||||
|
shouldAnimateHide = autoHide
|
||||||
|
showAnim.start()
|
||||||
|
} else {
|
||||||
|
hideAnim.stop()
|
||||||
|
delayedHideAnim.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
if (forceOpen) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (showPill) {
|
||||||
|
hideAnim.start()
|
||||||
|
}
|
||||||
|
showTimer.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDelayed() {
|
||||||
|
if (!showPill) {
|
||||||
|
shouldAnimateHide = autoHide
|
||||||
|
showTimer.start()
|
||||||
|
} else {
|
||||||
|
hideAnim.stop()
|
||||||
|
delayedHideAnim.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onForceOpenChanged: {
|
||||||
|
if (forceOpen) {
|
||||||
|
// Immediately lock open without animations
|
||||||
|
showAnim.stop()
|
||||||
|
hideAnim.stop()
|
||||||
|
delayedHideAnim.stop()
|
||||||
|
showPill = true
|
||||||
|
} else {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
325
Widgets/NPillVertical.qml
Normal file
325
Widgets/NPillVertical.qml
Normal file
|
|
@ -0,0 +1,325 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string icon: ""
|
||||||
|
property string text: ""
|
||||||
|
property string tooltipText: ""
|
||||||
|
property real sizeRatio: 0.8
|
||||||
|
property bool autoHide: false
|
||||||
|
property bool forceOpen: false
|
||||||
|
property bool disableOpen: false
|
||||||
|
property bool rightOpen: false
|
||||||
|
property bool hovered: false
|
||||||
|
property real fontSize: Style.fontSizeXS
|
||||||
|
|
||||||
|
// Bar position detection for pill direction
|
||||||
|
readonly property string barPosition: Settings.data.bar.position
|
||||||
|
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
|
||||||
|
|
||||||
|
// Determine pill direction based on section position
|
||||||
|
readonly property bool openDownward: rightOpen
|
||||||
|
readonly property bool openUpward: !rightOpen
|
||||||
|
|
||||||
|
// Effective shown state (true if animated open or forced)
|
||||||
|
readonly property bool revealed: forceOpen || showPill
|
||||||
|
|
||||||
|
signal shown
|
||||||
|
signal hidden
|
||||||
|
signal entered
|
||||||
|
signal exited
|
||||||
|
signal clicked
|
||||||
|
signal rightClicked
|
||||||
|
signal middleClicked
|
||||||
|
signal wheel(int delta)
|
||||||
|
|
||||||
|
// Internal state
|
||||||
|
property bool showPill: false
|
||||||
|
property bool shouldAnimateHide: false
|
||||||
|
|
||||||
|
// Sizing logic for vertical bars
|
||||||
|
readonly property int iconSize: Math.round(Style.baseWidgetSize * sizeRatio * scaling)
|
||||||
|
readonly property int pillHeight: iconSize
|
||||||
|
readonly property int pillPaddingHorizontal: Style.marginS * scaling
|
||||||
|
readonly property int pillPaddingVertical: Style.marginS * scaling
|
||||||
|
readonly property int pillOverlap: iconSize * 0.5
|
||||||
|
readonly property int maxPillWidth: iconSize
|
||||||
|
readonly property int maxPillHeight: Math.max(1, textItem.implicitHeight + pillPaddingVertical * 3)
|
||||||
|
|
||||||
|
// For vertical bars: width is just icon size, height includes pill space
|
||||||
|
width: iconSize
|
||||||
|
height: revealed ? (iconSize + maxPillHeight - pillOverlap) : iconSize
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: pill
|
||||||
|
width: revealed ? maxPillWidth : 1
|
||||||
|
height: revealed ? maxPillHeight : 1
|
||||||
|
|
||||||
|
// Position based on direction - center the pill relative to the icon
|
||||||
|
x: 0
|
||||||
|
y: openUpward ? (iconCircle.y + iconCircle.height / 2 - height) : (iconCircle.y + iconCircle.height / 2)
|
||||||
|
|
||||||
|
opacity: revealed ? Style.opacityFull : Style.opacityNone
|
||||||
|
color: Color.mSurfaceVariant
|
||||||
|
border.color: Color.mOutline
|
||||||
|
border.width: Math.max(1, Style.borderS * scaling)
|
||||||
|
|
||||||
|
// Radius logic for vertical expansion - rounded on the side that connects to icon
|
||||||
|
topLeftRadius: openUpward ? iconSize * 0.5 : 0
|
||||||
|
bottomLeftRadius: openDownward ? iconSize * 0.5 : 0
|
||||||
|
topRightRadius: openUpward ? iconSize * 0.5 : 0
|
||||||
|
bottomRightRadius: openDownward ? iconSize * 0.5 : 0
|
||||||
|
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
NTextVertical {
|
||||||
|
id: textItem
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.verticalCenterOffset: openUpward ? -6 * scaling : 6 * scaling // Adjust based on opening direction
|
||||||
|
text: root.text
|
||||||
|
fontSize: Style.fontSizeXXS * scaling
|
||||||
|
fontWeight: Style.fontWeightBold
|
||||||
|
color: Color.mOnSurface
|
||||||
|
visible: revealed
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on width {
|
||||||
|
enabled: showAnim.running || hideAnim.running
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on height {
|
||||||
|
enabled: showAnim.running || hideAnim.running
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on opacity {
|
||||||
|
enabled: showAnim.running || hideAnim.running
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: iconCircle
|
||||||
|
width: iconSize
|
||||||
|
height: iconSize
|
||||||
|
radius: width * 0.5
|
||||||
|
color: hovered && !forceOpen ? Color.mTertiary : Color.mSurfaceVariant
|
||||||
|
|
||||||
|
// Icon positioning based on direction
|
||||||
|
x: 0
|
||||||
|
y: openUpward ? (parent.height - height) : 0
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NIcon {
|
||||||
|
icon: root.icon
|
||||||
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
|
color: hovered && !forceOpen ? Color.mOnTertiary : Color.mOnSurfaceVariant
|
||||||
|
// Center horizontally
|
||||||
|
x: (iconCircle.width - width) / 2
|
||||||
|
// Center vertically accounting for font metrics
|
||||||
|
y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
id: showAnim
|
||||||
|
running: false
|
||||||
|
NumberAnimation {
|
||||||
|
target: pill
|
||||||
|
property: "width"
|
||||||
|
from: 1
|
||||||
|
to: maxPillWidth
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: pill
|
||||||
|
property: "height"
|
||||||
|
from: 1
|
||||||
|
to: maxPillHeight
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: pill
|
||||||
|
property: "opacity"
|
||||||
|
from: 0
|
||||||
|
to: 1
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
onStarted: {
|
||||||
|
showPill = true
|
||||||
|
}
|
||||||
|
onStopped: {
|
||||||
|
delayedHideAnim.start()
|
||||||
|
root.shown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
id: delayedHideAnim
|
||||||
|
running: false
|
||||||
|
PauseAnimation {
|
||||||
|
duration: 2500
|
||||||
|
}
|
||||||
|
ScriptAction {
|
||||||
|
script: if (shouldAnimateHide) {
|
||||||
|
hideAnim.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
id: hideAnim
|
||||||
|
running: false
|
||||||
|
NumberAnimation {
|
||||||
|
target: pill
|
||||||
|
property: "width"
|
||||||
|
from: maxPillWidth
|
||||||
|
to: 1
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.InCubic
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: pill
|
||||||
|
property: "height"
|
||||||
|
from: maxPillHeight
|
||||||
|
to: 1
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.InCubic
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: pill
|
||||||
|
property: "opacity"
|
||||||
|
from: 1
|
||||||
|
to: 0
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.InCubic
|
||||||
|
}
|
||||||
|
onStopped: {
|
||||||
|
showPill = false
|
||||||
|
shouldAnimateHide = false
|
||||||
|
root.hidden()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NTooltip {
|
||||||
|
id: tooltip
|
||||||
|
target: pill
|
||||||
|
text: root.tooltipText
|
||||||
|
positionLeft: barPosition === "right"
|
||||||
|
positionRight: barPosition === "left"
|
||||||
|
positionAbove: Settings.data.bar.position === "bottom"
|
||||||
|
delay: Style.tooltipDelayLong
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: showTimer
|
||||||
|
interval: Style.pillDelay
|
||||||
|
onTriggered: {
|
||||||
|
if (!showPill) {
|
||||||
|
showAnim.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||||
|
onEntered: {
|
||||||
|
hovered = true
|
||||||
|
root.entered()
|
||||||
|
tooltip.show()
|
||||||
|
if (disableOpen) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!forceOpen) {
|
||||||
|
showDelayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onExited: {
|
||||||
|
hovered = false
|
||||||
|
root.exited()
|
||||||
|
if (!forceOpen) {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
tooltip.hide()
|
||||||
|
}
|
||||||
|
onClicked: function (mouse) {
|
||||||
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
root.clicked()
|
||||||
|
} else if (mouse.button === Qt.RightButton) {
|
||||||
|
root.rightClicked()
|
||||||
|
} else if (mouse.button === Qt.MiddleButton) {
|
||||||
|
root.middleClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onWheel: wheel => {
|
||||||
|
root.wheel(wheel.angleDelta.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
if (!showPill) {
|
||||||
|
shouldAnimateHide = autoHide
|
||||||
|
showAnim.start()
|
||||||
|
} else {
|
||||||
|
hideAnim.stop()
|
||||||
|
delayedHideAnim.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
if (forceOpen) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (showPill) {
|
||||||
|
hideAnim.start()
|
||||||
|
}
|
||||||
|
showTimer.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDelayed() {
|
||||||
|
if (!showPill) {
|
||||||
|
shouldAnimateHide = autoHide
|
||||||
|
showTimer.start()
|
||||||
|
} else {
|
||||||
|
hideAnim.stop()
|
||||||
|
delayedHideAnim.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onForceOpenChanged: {
|
||||||
|
if (forceOpen) {
|
||||||
|
// Immediately lock open without animations
|
||||||
|
showAnim.stop()
|
||||||
|
hideAnim.stop()
|
||||||
|
delayedHideAnim.stop()
|
||||||
|
showPill = true
|
||||||
|
} else {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Widgets/NTextVertical.qml
Normal file
26
Widgets/NTextVertical.qml
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string text: ""
|
||||||
|
property real fontSize: Style.fontSizeXS
|
||||||
|
property color color: Color.mOnSurface
|
||||||
|
property int fontWeight: Style.fontWeightBold
|
||||||
|
|
||||||
|
spacing: -2 * scaling
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.text.split("")
|
||||||
|
NText {
|
||||||
|
text: modelData
|
||||||
|
font.family: Settings.data.ui.fontFixed
|
||||||
|
font.pointSize: root.fontSize
|
||||||
|
font.weight: root.fontWeight
|
||||||
|
color: root.color
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,7 @@ RowLayout {
|
||||||
property string description: ""
|
property string description: ""
|
||||||
property bool checked: false
|
property bool checked: false
|
||||||
property bool hovering: false
|
property bool hovering: false
|
||||||
property int baseSize: Style.baseWidgetSize
|
property int baseSize: Style.baseWidgetSize * 0.8
|
||||||
|
|
||||||
signal toggled(bool checked)
|
signal toggled(bool checked)
|
||||||
signal entered
|
signal entered
|
||||||
|
|
@ -31,7 +31,7 @@ RowLayout {
|
||||||
implicitHeight: root.baseSize * scaling
|
implicitHeight: root.baseSize * scaling
|
||||||
radius: height * 0.5
|
radius: height * 0.5
|
||||||
color: root.checked ? Color.mPrimary : Color.mSurface
|
color: root.checked ? Color.mPrimary : Color.mSurface
|
||||||
border.color: root.checked ? Color.mPrimary : Color.mOutline
|
border.color: Color.mOutline
|
||||||
border.width: Math.max(1, Style.borderM * scaling)
|
border.width: Math.max(1, Style.borderM * scaling)
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
|
|
@ -53,7 +53,7 @@ RowLayout {
|
||||||
color: root.checked ? Color.mOnPrimary : Color.mPrimary
|
color: root.checked ? Color.mOnPrimary : Color.mPrimary
|
||||||
border.color: root.checked ? Color.mSurface : Color.mSurface
|
border.color: root.checked ? Color.mSurface : Color.mSurface
|
||||||
border.width: Math.max(1, Style.borderM * scaling)
|
border.width: Math.max(1, Style.borderM * scaling)
|
||||||
y: 2 * scaling
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
x: root.checked ? switcher.width - width - 2 * scaling : 2 * scaling
|
x: root.checked ? switcher.width - width - 2 * scaling : 2 * scaling
|
||||||
|
|
||||||
Behavior on x {
|
Behavior on x {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ Window {
|
||||||
property bool positionLeft: false
|
property bool positionLeft: false
|
||||||
property bool positionRight: false
|
property bool positionRight: false
|
||||||
|
|
||||||
|
readonly property string barPosition: Settings.data.bar.position
|
||||||
|
|
||||||
flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
|
flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
|
||||||
color: Color.transparent
|
color: Color.transparent
|
||||||
visible: false
|
visible: false
|
||||||
|
|
@ -46,17 +48,34 @@ Window {
|
||||||
return
|
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
|
// Position tooltip to the left of the target
|
||||||
var pos = target.mapToGlobal(0, 0)
|
var pos = target.mapToGlobal(0, 0)
|
||||||
x = pos.x - width - 12 // 12 px margin to the left
|
x = pos.x - width - 12 // 12 px margin to the left
|
||||||
y = pos.y - height / 2 + target.height / 2
|
y = pos.y - height / 2 + target.height / 2
|
||||||
} else if (positionRight) {
|
} else if (shouldPositionRight) {
|
||||||
// Position tooltip to the right of the target
|
// Position tooltip to the right of the target
|
||||||
var pos = target.mapToGlobal(target.width, 0)
|
var pos = target.mapToGlobal(target.width, 0)
|
||||||
x = pos.x + 12 // 12 px margin to the right
|
x = pos.x + 12 // 12 px margin to the right
|
||||||
y = pos.y - height / 2 + target.height / 2
|
y = pos.y - height / 2 + target.height / 2
|
||||||
} else if (positionAbove) {
|
} else if (shouldPositionAbove) {
|
||||||
// Position tooltip above the target
|
// Position tooltip above the target
|
||||||
var pos = target.mapToGlobal(0, 0)
|
var pos = target.mapToGlobal(0, 0)
|
||||||
x = pos.x - width / 2 + target.width / 2
|
x = pos.x - width / 2 + target.width / 2
|
||||||
|
|
|
||||||
60
flake.nix
60
flake.nix
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
description =
|
description = "Noctalia shell - a Wayland desktop shell built with Quickshell";
|
||||||
"Noctalia shell - a Wayland desktop shell built with Quickshell";
|
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
|
|
@ -12,13 +11,22 @@
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, systems, quickshell, ... }:
|
outputs =
|
||||||
let eachSystem = nixpkgs.lib.genAttrs (import systems);
|
{
|
||||||
in {
|
self,
|
||||||
formatter =
|
nixpkgs,
|
||||||
eachSystem (system: nixpkgs.legacyPackages.${system}.alejandra);
|
systems,
|
||||||
|
quickshell,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
eachSystem = nixpkgs.lib.genAttrs (import systems);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
formatter = eachSystem (system: nixpkgs.legacyPackages.${system}.alejandra);
|
||||||
|
|
||||||
packages = eachSystem (system:
|
packages = eachSystem (
|
||||||
|
system:
|
||||||
let
|
let
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
qs = quickshell.packages.${system}.default.override {
|
qs = quickshell.packages.${system}.default.override {
|
||||||
|
|
@ -26,7 +34,8 @@
|
||||||
withI3 = false;
|
withI3 = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
runtimeDeps = with pkgs;
|
runtimeDeps =
|
||||||
|
with pkgs;
|
||||||
[
|
[
|
||||||
bash
|
bash
|
||||||
bluez
|
bluez
|
||||||
|
|
@ -41,21 +50,34 @@
|
||||||
matugen
|
matugen
|
||||||
networkmanager
|
networkmanager
|
||||||
wl-clipboard
|
wl-clipboard
|
||||||
] ++ lib.optionals (pkgs.stdenv.hostPlatform.isx86_64)
|
]
|
||||||
[ gpu-screen-recorder ];
|
++ lib.optionals (pkgs.stdenv.hostPlatform.isx86_64) [
|
||||||
|
gpu-screen-recorder
|
||||||
|
];
|
||||||
|
|
||||||
fontconfig = pkgs.makeFontsConf {
|
fontconfig = pkgs.makeFontsConf {
|
||||||
fontDirectories = [ pkgs.roboto pkgs.inter-nerdfont ];
|
fontDirectories = [
|
||||||
|
pkgs.roboto
|
||||||
|
pkgs.inter-nerdfont
|
||||||
|
];
|
||||||
};
|
};
|
||||||
in {
|
in
|
||||||
|
{
|
||||||
default = pkgs.stdenv.mkDerivation {
|
default = pkgs.stdenv.mkDerivation {
|
||||||
pname = "noctalia-shell";
|
pname = "noctalia-shell";
|
||||||
version = self.rev or self.dirtyRev or "dirty";
|
version = self.rev or self.dirtyRev or "dirty";
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
|
||||||
nativeBuildInputs =
|
nativeBuildInputs = [
|
||||||
[ pkgs.gcc pkgs.makeWrapper pkgs.qt6.wrapQtAppsHook ];
|
pkgs.gcc
|
||||||
buildInputs = [ qs pkgs.xkeyboard_config pkgs.qt6.qtbase ];
|
pkgs.makeWrapper
|
||||||
|
pkgs.qt6.wrapQtAppsHook
|
||||||
|
];
|
||||||
|
buildInputs = [
|
||||||
|
qs
|
||||||
|
pkgs.xkeyboard-config
|
||||||
|
pkgs.qt6.qtbase
|
||||||
|
];
|
||||||
propagatedBuildInputs = runtimeDeps;
|
propagatedBuildInputs = runtimeDeps;
|
||||||
|
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
|
|
@ -69,14 +91,14 @@
|
||||||
'';
|
'';
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
description =
|
description = "A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell.";
|
||||||
"A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell.";
|
|
||||||
homepage = "https://github.com/noctalia-dev/noctalia-shell";
|
homepage = "https://github.com/noctalia-dev/noctalia-shell";
|
||||||
license = pkgs.lib.licenses.mit;
|
license = pkgs.lib.licenses.mit;
|
||||||
mainProgram = "noctalia-shell";
|
mainProgram = "noctalia-shell";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
defaultPackage = eachSystem (system: self.packages.${system}.default);
|
defaultPackage = eachSystem (system: self.packages.${system}.default);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue