Vertical Bar!

This commit is contained in:
LemmyCook 2025-09-14 11:26:36 -04:00
commit 0c6aea7154
49 changed files with 3140 additions and 1154 deletions

View file

@ -106,6 +106,7 @@ Singleton {
"settings-wallpaper-selector": "library-photo",
"settings-screen-recorder": "video",
"settings-hooks": "link",
"settings-notification": "bell",
"settings-about": "info-square-rounded",
"bluetooth": "bluetooth",
"bt-device-generic": "bluetooth",

View file

@ -13,8 +13,9 @@ QtObject {
"united states": "us",
"us english": "us",
"british": "gb",
"uk": "gb",
"united kingdom": "gb",
"uk": "ua",
"united kingdom"// FIXED: Ukrainian language code should map to Ukraine
: "gb",
"english (uk)": "gb",
"canadian": "ca",
"canada": "ca",
@ -91,7 +92,9 @@ QtObject {
"slovak": "sk",
"slovenčina": "sk",
"slovakia": "sk",
"ukrainian": "ua",
"uk": "ua",
"ukrainian"// Ukrainian language code
: "ua",
"українська": "ua",
"ukraine": "ua",
"bulgarian": "bg",

View file

@ -263,7 +263,7 @@ Singleton {
// bar
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 list<string> monitors: []
@ -399,6 +399,10 @@ Singleton {
property list<string> monitors: []
// Last time the user opened the notification history (ms since epoch)
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

View file

@ -66,9 +66,9 @@ Singleton {
property int animationSlowest: Math.round(750 / Settings.data.general.animationSpeed)
// 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 baseWidgetSize: 32
property int baseWidgetSize: (barHeight * 0.9)
property int sliderWidth: 200
// Delays

View file

@ -48,6 +48,8 @@ Loader {
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
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 {}

View file

@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.UPower
@ -34,29 +35,30 @@ Variants {
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
anchors {
top: Settings.data.bar.position === "top"
bottom: Settings.data.bar.position === "bottom"
left: true
right: true
top: Settings.data.bar.position === "top" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
bottom: Settings.data.bar.position === "bottom" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
left: Settings.data.bar.position === "left" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
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
margins {
top: Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0
bottom: Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0
left: Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0
right: Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * 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 * scaling : 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 * scaling : 0
}
Item {
anchors.fill: parent
clip: true
// Background fill
// Background fill with shadow
Rectangle {
id: bar
@ -67,88 +69,181 @@ Variants {
radius: Settings.data.bar.floating ? Style.radiusL : 0
}
// ------------------------------
// Left Section - Dynamic Widgets
Row {
id: leftSection
objectName: "leftSection"
// For vertical bars, use a single column layout
Loader {
id: verticalBarLayout
anchors.fill: parent
visible: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
sourceComponent: verticalBarComponent
}
height: parent.height
anchors.left: parent.left
anchors.leftMargin: Style.marginS * scaling
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
// For horizontal bars, use the original three-section layout
Loader {
id: horizontalBarLayout
anchors.fill: parent
visible: Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
sourceComponent: horizontalBarComponent
}
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": parent.objectName.replace("Section", "").toLowerCase(),
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.left.length
// Main layout components
Component {
id: verticalBarComponent
Item {
anchors.fill: parent
// Top section (left widgets)
Column {
spacing: Style.marginS * root.scaling
anchors.horizontalCenter: parent.horizontalCenter
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
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
}
}
}
}
}
// ------------------------------
// Center Section - Dynamic Widgets
Row {
id: centerSection
objectName: "centerSection"
Component {
id: horizontalBarComponent
Item {
anchors.fill: parent
height: parent.height
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * 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": parent.objectName.replace("Section", "").toLowerCase(),
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
}
// Left Section
RowLayout {
id: leftSection
objectName: "leftSection"
anchors.left: parent.left
anchors.leftMargin: Style.marginS * root.scaling
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
}
}
}
}
}
}
// ------------------------------
// Right Section - Dynamic Widgets
Row {
id: rightSection
objectName: "rightSection"
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
}
// Center Section
RowLayout {
id: centerSection
objectName: "centerSection"
anchors.horizontalCenter: parent.horizontalCenter
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
}
}
}
}
}
}

View file

@ -8,7 +8,7 @@ import qs.Commons
import qs.Services
import qs.Widgets
RowLayout {
Item {
id: root
property ShellScreen screen
property real scaling: 1.0
@ -36,6 +36,10 @@ RowLayout {
readonly property real minWidth: Math.max(1, screen.width * 0.06)
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() {
try {
return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : ""
@ -45,10 +49,33 @@ RowLayout {
}
}
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginS * scaling
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() {
try {
// Try CompositorService first
@ -102,27 +129,31 @@ RowLayout {
Rectangle {
id: windowTitleRect
visible: root.visible
Layout.preferredWidth: contentLayout.implicitWidth + Style.marginM * 2 * scaling
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
anchors.left: parent.left
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)
color: Color.mSurfaceVariant
Item {
id: mainContainer
anchors.fill: parent
anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling
anchors.leftMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
anchors.rightMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
clip: true
// Horizontal layout for top/bottom bars
RowLayout {
id: contentLayout
id: horizontalLayout
anchors.centerIn: parent
spacing: Style.marginS * scaling
spacing: 2 * scaling
visible: barPosition === "top" || barPosition === "bottom"
// Window icon
Item {
Layout.preferredWidth: Style.fontSizeL * scaling * 1.2
Layout.preferredHeight: Style.fontSizeL * scaling * 1.2
Layout.preferredWidth: Style.baseWidgetSize * 0.5 * scaling
Layout.preferredHeight: Style.baseWidgetSize * 0.5 * scaling
Layout.alignment: Qt.AlignVCenter
visible: getTitle() !== "" && showIcon
@ -150,11 +181,11 @@ RowLayout {
if (mouseArea.containsMouse) {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
} 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) {
Logger.warn("ActiveWindow", "Error calculating width:", e)
return root.minWidth * scaling
return 80 * scaling
}
}
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
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
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() {
try {
windowIcon.source = Qt.binding(getAppIcon)
windowIconVertical.source = Qt.binding(getAppIcon)
} catch (e) {
Logger.warn("ActiveWindow", "Error in onActiveWindowChanged:", e)
}
@ -198,6 +283,7 @@ RowLayout {
function onWindowListChanged() {
try {
windowIcon.source = Qt.binding(getAppIcon)
windowIconVertical.source = Qt.binding(getAppIcon)
} catch (e) {
Logger.warn("ActiveWindow", "Error in onWindowListChanged:", e)
}

View file

@ -28,13 +28,19 @@ Rectangle {
return {}
}
readonly property string barPosition: Settings.data.bar.position
// Resolve settings: try user settings or defaults from BarWidgetRegistry
readonly property bool use12h: widgetSettings.use12HourClock !== undefined ? widgetSettings.use12HourClock : widgetMetadata.use12HourClock
readonly property bool reverseDayMonth: widgetSettings.reverseDayMonth !== undefined ? widgetSettings.reverseDayMonth : widgetMetadata.reverseDayMonth
readonly property string displayFormat: widgetSettings.displayFormat !== undefined ? widgetSettings.displayFormat : widgetMetadata.displayFormat
implicitWidth: Math.round(layout.implicitWidth + Style.marginM * 2 * scaling)
implicitHeight: Math.round(Style.capsuleHeight * scaling)
// Use compact mode for vertical bars
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)
color: Color.mSurfaceVariant
@ -46,66 +52,133 @@ Rectangle {
ColumnLayout {
id: layout
anchors.centerIn: parent
spacing: -3 * scaling
spacing: useCompactMode ? -2 * scaling : -3 * scaling
// First line
NText {
readonly property bool showSeconds: (displayFormat === "time-seconds")
readonly property bool inlineDate: (displayFormat === "time-date")
// Compact mode for vertical bars - Time section (HH, MM)
Repeater {
model: useCompactMode ? 2 : 1
NText {
readonly property bool showSeconds: (displayFormat === "time-seconds")
readonly property bool inlineDate: (displayFormat === "time-date")
readonly property var now: Time.date
text: {
const now = Time.date
let timeStr = ""
if (use12h) {
// 12-hour format with proper padding and consistent spacing
const hours = now.getHours()
const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours)
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}`
text: {
if (useCompactMode) {
// Compact mode: time section (first 2 lines)
switch (index) {
case 0:
// Hours
if (use12h) {
const hours = now.getHours()
const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours)
return displayHours.toString().padStart(2, '0')
} else {
return now.getHours().toString().padStart(2, '0')
}
case 1:
// Minutes
return now.getMinutes().toString().padStart(2, '0')
default:
return ""
}
} 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')
// Normal mode: single line with time
let timeStr = ""
if (showSeconds) {
const seconds = now.getSeconds().toString().padStart(2, '0')
timeStr = `${hours}:${minutes}:${seconds}`
} else {
timeStr = `${hours}:${minutes}`
if (use12h) {
// 12-hour format with proper padding and consistent spacing
const hours = now.getHours()
const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours)
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
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
//font.family: Settings.data.ui.fontFixed
font.pointSize: useCompactMode ? Style.fontSizeXXS * scaling : Style.fontSizeXS * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
//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 {
visible: (displayFormat === "time-date-short")
visible: !useCompactMode && (displayFormat === "time-date-short")
text: {
const now = Time.date
const day = now.getDate().toString().padStart(2, '0')

View file

@ -7,7 +7,7 @@ import qs.Commons
import qs.Services
import qs.Widgets
RowLayout {
Item {
id: root
property ShellScreen screen
@ -30,6 +30,8 @@ RowLayout {
return {}
}
readonly property string barPosition: Settings.data.bar.position
readonly property bool showAlbumArt: (widgetSettings.showAlbumArt !== undefined) ? widgetSettings.showAlbumArt : widgetMetadata.showAlbumArt
readonly property bool showVisualizer: (widgetSettings.showVisualizer !== undefined) ? widgetSettings.showVisualizer : widgetMetadata.showVisualizer
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}` : "")
}
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginS * scaling
function calculatedVerticalHeight() {
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
Layout.preferredWidth: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0
// A hidden text element to safely measure the full title width
NText {
@ -57,12 +75,12 @@ RowLayout {
Rectangle {
id: mediaMini
Layout.preferredWidth: rowLayout.implicitWidth + Style.marginM * 2 * scaling
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: Qt.AlignVCenter
radius: Math.round(Style.radiusM * scaling)
visible: root.visible
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
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: (barPosition === "left" || barPosition === "right") ? width / 2 : Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
// Used to anchor the tooltip, so the tooltip does not move when the content expands
@ -75,8 +93,8 @@ RowLayout {
Item {
id: mainContainer
anchors.fill: parent
anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling
anchors.leftMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
anchors.rightMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
Loader {
anchors.verticalCenter: parent.verticalCenter
@ -123,10 +141,12 @@ RowLayout {
}
}
// Horizontal layout for top/bottom bars
RowLayout {
id: rowLayout
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
visible: barPosition === "top" || barPosition === "bottom"
z: 1 // Above the visualizer
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
MouseArea {
id: mouseArea
@ -209,12 +256,18 @@ RowLayout {
}
onEntered: {
if (tooltip.text !== "") {
if (barPosition === "left" || barPosition === "right") {
tooltip.show()
} else if (tooltip.text !== "") {
tooltip.show()
}
}
onExited: {
tooltip.hide()
if (barPosition === "left" || barPosition === "right") {
tooltip.hide()
} else {
tooltip.hide()
}
}
}
}
@ -223,16 +276,23 @@ RowLayout {
NTooltip {
id: tooltip
text: {
var str = ""
if (MediaService.canGoNext) {
str += "Right click for next.\n"
if (barPosition === "left" || barPosition === "right") {
return getTitle()
} 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"
delay: 500
}
}

View file

@ -5,7 +5,7 @@ import qs.Commons
import qs.Services
import qs.Widgets
RowLayout {
Item {
id: root
property ShellScreen screen
@ -28,6 +28,8 @@ RowLayout {
return {}
}
readonly property string barPosition: Settings.data.bar.position
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 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 showDiskUsage: (widgetSettings.showDiskUsage !== undefined) ? widgetSettings.showDiskUsage : widgetMetadata.showDiskUsage
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginS * scaling
implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * 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 {
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.preferredWidth: mainLayout.implicitWidth + Style.marginM * scaling * 2
Layout.alignment: Qt.AlignVCenter
id: backgroundContainer
anchors.centerIn: parent
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)
color: Color.mSurfaceVariant
// Horizontal layout for top/bottom bars
RowLayout {
id: mainLayout
anchors.centerIn: parent // Better centering than margins
width: parent.width - Style.marginM * scaling * 2
spacing: Style.marginS * scaling
id: horizontalLayout
anchors.centerIn: parent
spacing: Style.marginXS * scaling
visible: barPosition === "top" || barPosition === "bottom"
// CPU Usage Component
Item {
@ -62,7 +151,7 @@ RowLayout {
RowLayout {
id: cpuUsageRow
anchors.centerIn: parent
spacing: Style.marginXS * scaling
spacing: 2 * scaling
NIcon {
icon: "cpu-usage"
@ -92,7 +181,7 @@ RowLayout {
RowLayout {
id: cpuTempRow
anchors.centerIn: parent
spacing: Style.marginXS * scaling
spacing: 2 * scaling
NIcon {
icon: "cpu-temperature"
@ -123,7 +212,7 @@ RowLayout {
RowLayout {
id: memoryUsageRow
anchors.centerIn: parent
spacing: Style.marginXS * scaling
spacing: 2 * scaling
NIcon {
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
}
}
}
}
}
}

View file

@ -17,6 +17,7 @@ Rectangle {
property real scaling: 1.0
readonly property real itemSize: 24 * scaling
readonly property string barPosition: Settings.data.bar.position
function onLoaded() {
// When the widget is fully initialized with its props
@ -27,8 +28,8 @@ Rectangle {
}
visible: SystemTray.items.values.length > 0
implicitWidth: trayLayout.implicitWidth + Style.marginM * scaling * 2
implicitHeight: Math.round(Style.capsuleHeight * scaling)
implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : (trayLayout.implicitWidth + Style.marginM * scaling * 2)
implicitHeight: (barPosition === "left" || barPosition === "right") ? Math.round(trayLayout.implicitHeight + Style.marginM * scaling * 2) : Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
@ -111,9 +112,21 @@ Rectangle {
if (modelData.hasMenu && modelData.menu && trayMenu.item) {
trayPanel.open()
// Anchor the menu to the tray icon item (parent) and position it below the icon
const menuX = (width / 2) - (trayMenu.item.width / 2)
const menuY = Math.round(Style.barHeight * scaling)
// Position menu based on bar position
let menuX, menuY
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.showAt(parent, menuX, menuY)
} else {

View file

@ -61,15 +61,6 @@ Item {
}
}
Timer {
id: externalHideTimer
running: false
interval: 1500
onTriggered: {
pill.hide()
}
}
NPill {
id: pill

View file

@ -31,6 +31,8 @@ Item {
return {}
}
readonly property string barPosition: Settings.data.bar.position
readonly property string labelMode: (widgetSettings.labelMode !== undefined) ? widgetSettings.labelMode : widgetMetadata.labelMode
readonly property bool hideUnoccupied: (widgetSettings.hideUnoccupied !== undefined) ? widgetSettings.hideUnoccupied : widgetMetadata.hideUnoccupied
@ -47,17 +49,8 @@ Item {
signal workspaceChanged(int workspaceId, color accentColor)
implicitHeight: Math.round(Style.barHeight * scaling)
implicitWidth: {
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
}
implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)
implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.barHeight * scaling) : calculatedHorizontalWidth()
function calculatedWsWidth(ws) {
if (ws.isFocused)
@ -68,6 +61,37 @@ Item {
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: {
refreshWorkspaces()
}
@ -99,7 +123,8 @@ Item {
}
}
}
workspaceRepeater.model = localWorkspaces
workspaceRepeaterHorizontal.model = localWorkspaces
workspaceRepeaterVertical.model = localWorkspaces
updateWorkspaceFocus()
}
@ -148,9 +173,8 @@ Item {
Rectangle {
id: workspaceBackground
width: parent.width
height: Math.round(Style.capsuleHeight * scaling)
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : parent.width
height: (barPosition === "left" || barPosition === "right") ? parent.height : Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
@ -158,14 +182,17 @@ Item {
anchors.verticalCenter: parent.verticalCenter
}
// Horizontal layout for top/bottom bars
Row {
id: pillRow
spacing: spacingBetweenPills
anchors.verticalCenter: workspaceBackground.verticalCenter
width: root.width - horizontalPadding * 2
x: horizontalPadding
visible: barPosition === "top" || barPosition === "bottom"
Repeater {
id: workspaceRepeater
id: workspaceRepeaterHorizontal
model: localWorkspaces
Item {
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
}
}
}
}
}

View file

@ -63,7 +63,7 @@ NPanel {
NIconButton {
icon: "close"
tooltipText: "Close"
tooltipText: "Close."
sizeRatio: 0.8
onClicked: {
root.close()

View file

@ -12,7 +12,7 @@ NPanel {
preferredWidth: 340
preferredHeight: 320
panelAnchorRight: true
panelAnchorRight: Settings.data.bar.position === "right"
// Main Column
panelContent: ColumnLayout {

View file

@ -166,7 +166,15 @@ Variants {
// Position above the bar if it's at bottom
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 {
// anchors.fill: parent

View file

@ -33,13 +33,50 @@ Variants {
screen: modelData
color: Color.transparent
// Position based on bar location
anchors.top: Settings.data.bar.position === "top"
anchors.bottom: Settings.data.bar.position === "bottom"
anchors.right: true
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.right: Style.marginM * scaling
// Position based on bar location - always at top
anchors.top: true
anchors.right: Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
anchors.left: Settings.data.bar.position === "left"
margins.top: {
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
implicitHeight: Math.min(notificationStack.implicitHeight, (NotificationService.maxVisible * 120) * scaling)
//WlrLayershell.layer: WlrLayer.Overlay
@ -77,10 +114,10 @@ Variants {
// Main notification container
ColumnLayout {
id: notificationStack
// Position based on bar location
anchors.top: Settings.data.bar.position === "top" ? parent.top : undefined
anchors.bottom: Settings.data.bar.position === "bottom" ? parent.bottom : undefined
anchors.right: parent.right
// Position based on bar location - always at top
anchors.top: parent.top
anchors.right: (Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom") ? parent.right : undefined
anchors.left: Settings.data.bar.position === "left" ? parent.left : undefined
spacing: Style.marginS * scaling
width: 360 * scaling
visible: true
@ -288,7 +325,7 @@ Variants {
// Close button positioned absolutely
NIconButton {
icon: "close"
tooltipText: "Close"
tooltipText: "Close."
sizeRatio: 0.6
anchors.top: parent.top
anchors.topMargin: Style.marginM * scaling

View file

@ -14,7 +14,7 @@ NPanel {
preferredWidth: 380
preferredHeight: 500
panelAnchorRight: true
panelAnchorRight: Settings.data.bar.position === "right"
panelKeyboardFocus: true
panelContent: Rectangle {
@ -65,7 +65,7 @@ NPanel {
NIconButton {
icon: "close"
tooltipText: "Close"
tooltipText: "Close."
sizeRatio: 0.8
onClicked: {
root.close()

View file

@ -29,11 +29,11 @@ NPanel {
Dock,
Hooks,
Launcher,
Brightness,
ColorScheme,
Display,
General,
Network,
Notification,
ScreenRecorder,
Weather,
Wallpaper,
@ -72,10 +72,6 @@ NPanel {
id: audioTab
Tabs.AudioTab {}
}
Component {
id: brightnessTab
Tabs.BrightnessTab {}
}
Component {
id: displayTab
Tabs.DisplayTab {}
@ -116,6 +112,10 @@ NPanel {
id: dockTab
Tabs.DockTab {}
}
Component {
id: notificationTab
Tabs.NotificationTab {}
}
// Order *DOES* matter
function updateTabsModel() {
@ -149,16 +149,16 @@ NPanel {
"label": "Display",
"icon": "settings-display",
"source": displayTab
}, {
"id": SettingsPanel.Tab.Notification,
"label": "Notification",
"icon": "settings-notification",
"source": notificationTab
}, {
"id": SettingsPanel.Tab.Network,
"label": "Network",
"icon": "settings-network",
"source": networkTab
}, {
"id": SettingsPanel.Tab.Brightness,
"label": "Brightness",
"icon": "settings-brightness",
"source": brightnessTab
}, {
"id": SettingsPanel.Tab.Weather,
"label": "Weather",
@ -468,7 +468,7 @@ NPanel {
NIcon {
icon: root.tabsModel[currentTabIndex]?.icon
color: Color.mPrimary
font.pointSize: Style.fontSizeXL * scaling
font.pointSize: Style.fontSizeXXL * scaling
}
// Main title
@ -484,7 +484,7 @@ NPanel {
// Close button
NIconButton {
icon: "close"
tooltipText: "Close"
tooltipText: "Close."
Layout.alignment: Qt.AlignVCenter
onClicked: root.close()
}

View file

@ -10,23 +10,23 @@ import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL * scaling
property string latestVersion: GitHubService.latestVersion
property string currentVersion: UpdateService.currentVersion
property var contributors: GitHubService.contributors
NText {
text: "Noctalia Shell"
font.pointSize: Style.fontSizeXXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.alignment: Qt.AlignCenter
Layout.bottomMargin: Style.marginS * scaling
NHeader {
label: "Noctalia Shell"
description: "A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell."
}
RowLayout {
spacing: Style.marginL * scaling
// Versions
GridLayout {
Layout.alignment: Qt.AlignCenter
columns: 2
rowSpacing: Style.marginXS * scaling
columnSpacing: Style.marginS * scaling
@ -34,7 +34,6 @@ ColumnLayout {
NText {
text: "Latest Version:"
color: Color.mOnSurface
Layout.alignment: Qt.AlignRight
}
NText {
@ -46,7 +45,6 @@ ColumnLayout {
NText {
text: "Installed Version:"
color: Color.mOnSurface
Layout.alignment: Qt.AlignRight
}
NText {
@ -56,10 +54,13 @@ ColumnLayout {
}
}
// Updater
Item {
Layout.fillWidth: true
}
// Update button
Rectangle {
Layout.alignment: Qt.AlignCenter
Layout.topMargin: Style.marginS * scaling
Layout.alignment: Qt.alignmentRight
Layout.preferredWidth: Math.round(updateRow.implicitWidth + (Style.marginL * scaling * 2))
Layout.preferredHeight: Math.round(Style.barHeight * scaling)
radius: Style.radiusL * scaling
@ -115,23 +116,21 @@ ColumnLayout {
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
NText {
text: `Shout-out to our ${root.contributors.length} <b>awesome</b> contributors!`
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignCenter
NHeader {
label: "Contributors"
description: `Shout-out to our ${root.contributors.length} <b>awesome</b> contributors!`
}
GridView {
id: contributorsGrid
Layout.topMargin: Style.marginL * scaling
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: cellWidth * 3 // Fixed 3 columns
Layout.preferredHeight: {

View file

@ -8,6 +8,12 @@ import qs.Services
ColumnLayout {
id: root
spacing: Style.marginL * scaling
NHeader {
label: "Volumes"
description: "Configure volume controls and audio levels."
}
property real localVolume: AudioService.volume
@ -20,7 +26,7 @@ ColumnLayout {
// Master Volume
ColumnLayout {
spacing: Style.marginS * scaling
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NLabel {
@ -67,7 +73,6 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
NToggle {
label: "Mute Audio Output"
@ -83,9 +88,8 @@ ColumnLayout {
// Input Volume
ColumnLayout {
spacing: Style.marginS * scaling
spacing: Style.marginXS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
NLabel {
label: "Input Volume"
@ -117,7 +121,6 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
NToggle {
label: "Mute Audio Input"
@ -131,7 +134,6 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
NSpinBox {
Layout.fillWidth: true
@ -158,12 +160,9 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginS * scaling
NText {
text: "Audio Devices"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
NHeader {
label: "Audio Devices"
description: "Configure audio input and output devices."
}
// -------------------------------
@ -203,7 +202,6 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.fillWidth: true
Layout.bottomMargin: Style.marginL * scaling
NLabel {
label: "Input Device"
@ -234,12 +232,9 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginL * scaling
NText {
text: "Media Player"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
NHeader {
label: "Media Player"
description: "Configure your favorite media players."
}
// Preferred player
@ -360,12 +355,9 @@ ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NText {
text: "Audio Visualizer"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
NHeader {
label: "Audio Visualizer"
description: "Customize visual effects that respond to audio playback."
}
// AudioService Visualizer section

View file

@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
@ -8,6 +9,21 @@ import qs.Modules.SettingsPanel.Bar
ColumnLayout {
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
function handleDragStart() {
@ -25,8 +41,11 @@ ColumnLayout {
}
}
ColumnLayout {
spacing: Style.marginL * scaling
NHeader {
label: "Appearance"
description: "Configure bar appearance and positioning."
}
RowLayout {
NComboBox {
@ -42,6 +61,14 @@ ColumnLayout {
key: "bottom"
name: "Bottom"
}
ListElement {
key: "left"
name: "Left"
}
ListElement {
key: "right"
name: "Right"
}
}
currentKey: Settings.data.bar.position
onSelected: key => Settings.data.bar.position = key
@ -52,19 +79,9 @@ ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NText {
text: "Background Opacity"
font.pointSize: Style.fontSizeL * scaling
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
NLabel {
label: "Background Opacity"
description: "Adjust the background opacity of the bar."
}
RowLayout {
@ -86,7 +103,6 @@ ColumnLayout {
}
}
}
NToggle {
Layout.fillWidth: true
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 {
@ -186,20 +236,9 @@ ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NText {
text: "Widgets Positioning"
font.pointSize: Style.fontSizeXXL * scaling
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
NHeader {
label: "Widgets Positioning"
description: "Drag and drop widgets to reorder them within each section, or use the add/remove buttons to manage widgets."
}
// Bar Sections

View file

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

View file

@ -8,7 +8,9 @@ import qs.Widgets
ColumnLayout {
id: root
spacing: 0
spacing: Style.marginL * scaling
// Cache for scheme JSON (can be flat or {dark, light})
property var schemeColorsCache: ({})
@ -104,10 +106,12 @@ ColumnLayout {
}
}
// Main Toggles - Dark Mode / Matugen
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NHeader {
label: "Behavior"
description: "Main settings for Noctalia's colors."
}
// Dark Mode Toggle (affects both Matugen and predefined schemes that provide variants)
NToggle {
@ -138,7 +142,7 @@ ColumnLayout {
}
}
}
}
NDivider {
Layout.fillWidth: true
@ -151,19 +155,9 @@ ColumnLayout {
spacing: Style.marginM * scaling
Layout.fillWidth: true
NText {
text: "Predefined Color Schemes"
font.pointSize: Style.fontSizeXXL * scaling
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
NHeader {
label: "Predefined Color Schemes"
description: "To use these color schemes, you must turn off Matugen. With Matugen enabled, colors are automatically generated from your wallpaper."
}
// Color Schemes Grid

View file

@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services
import qs.Widgets
@ -9,36 +10,54 @@ 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
// Time dropdown options (00:00 .. 23:30)
ListModel {
id: timeOptions
}
function removeMonitor(list, name) {
return (list || []).filter(function (n) {
return n !== name
})
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
})
}
}
}
NText {
text: "Monitor-specific configuration"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
// 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 {}
}
NText {
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
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
spacing: Style.marginL * scaling
NHeader {
label: "Monitor-specific configuration"
description: "Configure scaling and brightness settings individually for each connected display."
}
ColumnLayout {
spacing: Style.marginL * scaling
Layout.topMargin: Style.marginL * scaling
Repeater {
model: Quickshell.screens || []
@ -46,11 +65,13 @@ ColumnLayout {
Layout.fillWidth: true
implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling
radius: Style.radiusM * scaling
color: Color.mSurface
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
property real localScaling: ScalingService.getScreenScale(modelData)
property var brightnessMonitor: BrightnessService.getMonitorForScreen(modelData)
Connections {
target: ScalingService
function onScaleChanged(screenName, scale) {
@ -67,119 +88,116 @@ ColumnLayout {
spacing: Style.marginXXS * scaling
NText {
text: (modelData.name || "Unknown")
font.pointSize: Style.fontSizeXL * scaling
text: (`${modelData.name}: ${modelData.model}` || "Unknown")
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
color: Color.mPrimary
}
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
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
// Scale
ColumnLayout {
spacing: Style.marginL * scaling
spacing: Style.marginS * scaling
Layout.fillWidth: true
NToggle {
Layout.fillWidth: true
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
RowLayout {
spacing: Style.marginM * scaling
Layout.fillWidth: true
RowLayout {
Layout.fillWidth: true
spacing: Style.marginL * 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
}
NText {
text: "Scale"
Layout.preferredWidth: 80 * scaling
}
RowLayout {
spacing: Style.marginS * scaling
NSlider {
id: scaleSlider
from: 0.7
to: 1.8
stepSize: 0.01
value: localScaling
onPressedChanged: ScalingService.setScreenScale(modelData, value)
Layout.fillWidth: true
Layout.minimumWidth: 200 * scaling
}
NSlider {
id: scaleSlider
from: 0.7
to: 1.8
stepSize: 0.01
value: localScaling
onPressedChanged: ScalingService.setScreenScale(modelData, value)
Layout.fillWidth: true
}
NText {
text: `${Math.round(localScaling * 100)}%`
Layout.preferredWidth: 50 * scaling
horizontalAlignment: Text.AlignRight
}
// Reset button container
Item {
Layout.preferredWidth: 40 * scaling
Layout.preferredHeight: 30 * scaling
NIconButton {
icon: "refresh"
sizeRatio: 0.8
tooltipText: "Reset scaling"
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
}
}

View file

@ -1,14 +1,34 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
ColumnLayout {
id: contentColumn
id: root
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 {
label: "Auto-hide"
@ -27,7 +47,6 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NLabel {
label: "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
}
}

View file

@ -9,6 +9,11 @@ import qs.Widgets
ColumnLayout {
id: root
NHeader {
label: "Profile"
description: "Configure your user profile and avatar settings."
}
// Profile section
RowLayout {
Layout.fillWidth: true
@ -48,12 +53,9 @@ ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "User Interface"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
NHeader {
label: "User Interface"
description: "Main settings for the user interface."
}
NToggle {
@ -133,12 +135,9 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Screen Corners"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
NHeader {
label: "Screen Corners"
description: "Customize screen corner rounding and visual effects."
}
NToggle {
@ -187,12 +186,10 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Fonts"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
NHeader {
label: "Fonts"
description: "Configure interface typography."
}
// Font configuration section
@ -200,12 +197,13 @@ ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NComboBox {
NSearchableComboBox {
label: "Default Font"
description: "Main font used throughout the interface."
model: FontService.availableFonts
currentKey: Settings.data.ui.fontDefault
placeholder: "Select default font..."
searchPlaceholder: "Search fonts..."
popupHeight: 420 * scaling
minimumWidth: 300 * scaling
onSelected: function (key) {
@ -213,12 +211,13 @@ ColumnLayout {
}
}
NComboBox {
NSearchableComboBox {
label: "Fixed Width Font"
description: "Monospace font used for terminal and code display."
model: FontService.monospaceFonts
currentKey: Settings.data.ui.fontFixed
placeholder: "Select monospace font..."
searchPlaceholder: "Search monospace fonts..."
popupHeight: 320 * scaling
minimumWidth: 300 * scaling
onSelected: function (key) {
@ -226,12 +225,13 @@ ColumnLayout {
}
}
NComboBox {
NSearchableComboBox {
label: "Billboard Font"
description: "Large font used for clocks and prominent displays."
model: FontService.displayFonts
currentKey: Settings.data.ui.fontBillboard
placeholder: "Select display font..."
searchPlaceholder: "Search display fonts..."
popupHeight: 320 * scaling
minimumWidth: 300 * scaling
onSelected: function (key) {

View file

@ -10,6 +10,11 @@ ColumnLayout {
spacing: Style.marginL * scaling
width: root.width
NHeader {
label: "System Hooks"
description: "Configure commands to be executed when system events occur."
}
// Enable/Disable Toggle
NToggle {
label: "Enable Hooks"

View file

@ -7,9 +7,12 @@ import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL * scaling
ColumnLayout {
spacing: Style.marginL * scaling
NHeader {
label: "Appearance"
description: "Configure the launcher behavior and appearance."
}
NComboBox {
id: launcherPosition
@ -105,7 +108,7 @@ ColumnLayout {
checked: Settings.data.appLauncher.useApp2Unit
onToggled: checked => Settings.data.appLauncher.useApp2Unit = checked
}
}
NDivider {
Layout.fillWidth: true

View file

@ -11,6 +11,11 @@ ColumnLayout {
id: root
spacing: Style.marginL * scaling
NHeader {
label: "Network Settings"
description: "Configure Wi-Fi and Bluetooth connectivity options."
}
NToggle {
label: "Enable Wi-Fi"
description: "Enable Wi-Fi connectivity."

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

View file

@ -10,6 +10,11 @@ ColumnLayout {
spacing: Style.marginL * scaling
NHeader {
label: "General Settings"
description: "Configure screen recording output and content."
}
// Output Directory
ColumnLayout {
spacing: Style.marginS * scaling
@ -53,12 +58,8 @@ ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Video Settings"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
NHeader {
label: "Video Settings"
}
// Source
@ -203,12 +204,8 @@ ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Audio Settings"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
NHeader {
label: "Audio Settings"
}
// Audio Source

View file

@ -9,7 +9,6 @@ import qs.Widgets
ColumnLayout {
id: root
width: parent.width
spacing: Style.marginL * scaling
property list<string> wallpapersList: []
@ -42,11 +41,9 @@ ColumnLayout {
}
// Current wallpaper display
NText {
text: "Current Wallpaper"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
NHeader {
label: "Current Wallpaper"
description: "Preview and manage your desktop background."
}
Rectangle {
@ -80,18 +77,9 @@ ColumnLayout {
Layout.fillWidth: true
// Wallpaper grid
NText {
text: "Wallpaper Selector"
font.pointSize: Style.fontSizeXXL * scaling
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
NHeader {
label: "Wallpaper Selector"
description: "Click on a wallpaper to set it as your current wallpaper."
}
}

View file

@ -9,6 +9,12 @@ import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL * scaling
NHeader {
label: "Wallpaper Settings"
description: "Control how wallpapers are managed and displayed."
}
NToggle {
label: "Enable Wallpaper Management"
@ -22,6 +28,7 @@ ColumnLayout {
visible: Settings.data.wallpaper.enabled
spacing: Style.marginL * scaling
Layout.fillWidth: true
NTextInput {
label: "Wallpaper Directory"
description: "Path to your common wallpaper directory."
@ -61,7 +68,7 @@ ColumnLayout {
delegate: RowLayout {
NText {
text: (modelData.name || "Unknown")
color: Color.mSecondary
color: Color.mPrimary
font.weight: Style.fontWeightBold
Layout.preferredWidth: 90 * scaling
}
@ -89,11 +96,8 @@ ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Look & Feel"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
NHeader {
label: "Look & Feel"
}
// Fill Mode
@ -189,11 +193,8 @@ ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Automation"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
NHeader {
label: "Automation"
}
// Random Wallpaper

View file

@ -7,6 +7,12 @@ import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL * scaling
NHeader {
label: "Your Location"
description: "Set your location for weather, time zones, and scheduling."
}
// Location section
RowLayout {
@ -57,11 +63,9 @@ ColumnLayout {
spacing: Style.marginM * scaling
Layout.fillWidth: true
NText {
text: "Weather"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
NHeader {
label: "Weather"
description: "Configure weather display preferences and temperature units."
}
NToggle {

View file

@ -30,10 +30,11 @@ Variants {
screen: modelData
// Position based on bar location, like Notification popup does
// Position at top of screen, always allow horizontal centering
anchors {
top: Settings.data.bar.position === "top"
bottom: Settings.data.bar.position === "bottom"
top: true
left: true
right: true
}
// 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)
// 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.bottom: Settings.data.bar.position === "bottom" ? (Style.barHeight + Style.marginS + (Settings.data.bar.floating ? Settings.data.bar.marginVertical : 0)) * scaling : 0
margins.top: {
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
color: Color.transparent
@ -61,8 +97,8 @@ Variants {
// Simple positioning - margins already account for bar
targetY: Style.marginS * scaling
// Hidden position based on bar location
hiddenY: Settings.data.bar.position === "top" ? -toast.height - 20 : toast.height + 20
// Hidden position - always start from above the screen
hiddenY: -toast.height - 20
Component.onCompleted: {
// Register this toast with the service

View file

@ -64,7 +64,7 @@ NPanel {
NIconButton {
icon: "close"
tooltipText: "Close"
tooltipText: "Close."
sizeRatio: 0.8
onClicked: root.close()
}

View file

@ -87,9 +87,26 @@ Singleton {
// Maximum visible notifications
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
property Timer hideTimer: Timer {
interval: 8000 // 8 seconds - longer display time
interval: 1000 // Check every second
repeat: true
running: notificationModel.count > 0
@ -98,11 +115,26 @@ Singleton {
return
}
// Remove the oldest notification (last in the list)
let oldestNotification = notificationModel.get(notificationModel.count - 1).rawNotification
if (oldestNotification) {
// Trigger animation signal instead of direct dismiss
animateAndRemove(oldestNotification, notificationModel.count - 1)
// Check each notification for expiration
for (var i = notificationModel.count - 1; i >= 0; i--) {
let notificationData = notificationModel.get(i)
if (notificationData && notificationData.rawNotification) {
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
}
}
}
}
}

View file

@ -13,7 +13,7 @@ RowLayout {
property bool hovering: false
property color activeColor: Color.mPrimary
property color activeOnColor: Color.mOnPrimary
property int baseSize: Math.max(Style.baseWidgetSize * 0.8, 14)
property int baseSize: Math.max(Style.baseWidgetSize * 0.7, 14)
signal toggled(bool checked)
signal entered
@ -39,7 +39,7 @@ RowLayout {
implicitHeight: root.baseSize * scaling
radius: Style.radiusXS * scaling
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)
Behavior on color {
@ -57,9 +57,10 @@ RowLayout {
NIcon {
visible: root.checked
anchors.centerIn: parent
anchors.horizontalCenterOffset: -1 * scaling
icon: "check"
color: root.activeOnColor
font.pointSize: Math.max(Style.fontSizeS, root.baseSize * 0.7) * scaling
font.pointSize: Math.max(Style.fontSizeXS, root.baseSize * 0.6) * scaling
}
MouseArea {

32
Widgets/NHeader.qml Normal file
View file

@ -0,0 +1,32 @@
import QtQuick
import QtQuick.Layouts
import qs.Commons
ColumnLayout {
id: root
property string label: ""
property string description: ""
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
Layout.bottomMargin: Style.marginM * scaling
NText {
text: root.label
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
font.capitalization: Font.Capitalize
color: Color.mSecondary
visible: root.title !== ""
}
NText {
text: root.description
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
visible: root.description !== ""
}
}

View file

@ -40,6 +40,7 @@ Loader {
property real opacityValue: originalOpacity
property alias isClosing: hideTimer.running
readonly property string barPosition: Settings.data.bar.position
signal opened
signal closed
@ -141,6 +142,7 @@ Loader {
// PanelWindow has its own screen property inherited of QsWindow
property real scaling: ScalingService.getScreenScale(screen)
readonly property real barHeight: Math.round(Style.barHeight * scaling)
readonly property real barWidth: Math.round(Style.barHeight * scaling)
readonly property bool barAtBottom: Settings.data.bar.position === "bottom"
readonly property bool barIsVisible: (screen !== null) && (Settings.data.bar.monitors.includes(screen.name) || (Settings.data.bar.monitors.length === 0))
@ -148,7 +150,7 @@ Loader {
target: ScalingService
function onScaleChanged(screenName, scale) {
if ((screen !== null) && (screenName === screen.name)) {
root.scaling = scale
root.scaling = scaling = scale
}
}
}
@ -157,7 +159,7 @@ Loader {
target: panelWindow
function onScreenChanged() {
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
panelContentLoader.active = false
@ -184,8 +186,29 @@ Loader {
anchors.left: true
anchors.right: true
anchors.bottom: true
margins.top: (barIsVisible && !barAtBottom) ? (barHeight + ((Settings.data.bar.floating && !panelAnchorVerticalCenter) ? Settings.data.bar.marginVertical : 0)) : 0
margins.bottom: (barIsVisible && barAtBottom) ? (barHeight + ((Settings.data.bar.floating && !panelAnchorVerticalCenter) ? Settings.data.bar.marginVertical : 0)) : 0
margins.top: {
if (!barIsVisible || barAtBottom) {
return 0
}
switch (Settings.data.bar.position) {
case "top":
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating && !panelAnchorVerticalCenter ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
default:
return Style.marginM * scaling
}
}
margins.bottom: {
if (!barIsVisible || !barAtBottom) {
return 0
}
switch (Settings.data.bar.position) {
case "bottom":
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating && !panelAnchorVerticalCenter ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
default:
return 0
}
}
// Close any panel with Esc without requiring focus
Shortcut {
@ -237,31 +260,119 @@ Loader {
y: calculatedY
property int calculatedX: {
if (root.useButtonPosition) {
// Position panel relative to button
var targetX = root.buttonPosition.x + (root.buttonWidth / 2) - (preferredWidth / 2)
var barPosition = Settings.data.bar.position
// Keep panel within screen bounds
var maxX = panelWindow.width - panelBackground.width - (Style.marginS * scaling)
var minX = Style.marginS * scaling
return Math.round(Math.max(minX, Math.min(targetX, maxX)))
} else if (!panelAnchorHorizontalCenter && panelAnchorLeft) {
// Check anchor properties first, even when using button positioning
if (!panelAnchorHorizontalCenter && panelAnchorLeft) {
return Math.round(Style.marginS * scaling)
} else if (!panelAnchorHorizontalCenter && panelAnchorRight) {
return Math.round(panelWindow.width - 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 {
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: {
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)
} else if (panelAnchorBottom) {
return Math.round(panelWindow.height - panelBackground.height - (Style.marginS * scaling))
} else if (panelAnchorTop) {
return Math.round(Style.marginS * scaling)
} else if (barPosition === "left" || barPosition === "right") {
// For vertical bars, center vertically
return Math.round((panelWindow.height - panelBackground.height) / 2)
} else if (!barAtBottom) {
// Below the top bar
return Math.round(Style.marginS * scaling)

View file

@ -17,8 +17,8 @@ Item {
property bool hovered: false
property real fontSize: Style.fontSizeXS
// Effective shown state (true if hovered/animated open or forced)
readonly property bool revealed: forceOpen || showPill
readonly property string barPosition: Settings.data.bar.position
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
signal shown
signal hidden
@ -29,258 +29,81 @@ Item {
signal middleClicked
signal wheel(int delta)
// Internal state
property bool showPill: false
property bool shouldAnimateHide: false
// Dynamic sizing based on loaded component
width: pillLoader.item ? pillLoader.item.width : 0
height: pillLoader.item ? pillLoader.item.height : 0
// Exposed width logic
readonly property int iconSize: Math.round(Style.baseWidgetSize * sizeRatio * scaling)
readonly property int pillHeight: iconSize
readonly property int pillPaddingHorizontal: Style.marginS * scaling
readonly property int pillOverlap: iconSize * 0.5
readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap)
// Loader to switch between vertical and horizontal pill implementations
Loader {
id: pillLoader
sourceComponent: isVerticalBar ? verticalPillComponent : horizontalPillComponent
width: iconSize + Math.max(0, pill.width - pillOverlap)
height: pillHeight
Component {
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 {
id: pill
width: revealed ? maxPillWidth : 1
height: pillHeight
x: rightOpen ? (iconCircle.x + iconCircle.width / 2) : // Opens right
(iconCircle.x + iconCircle.width / 2) - width // Opens left
opacity: revealed ? Style.opacityFull : Style.opacityNone
color: Color.mSurfaceVariant
topLeftRadius: rightOpen ? 0 : pillHeight * 0.5
bottomLeftRadius: rightOpen ? 0 : pillHeight * 0.5
topRightRadius: rightOpen ? pillHeight * 0.5 : 0
bottomRightRadius: rightOpen ? pillHeight * 0.5 : 0
anchors.verticalCenter: parent.verticalCenter
NText {
id: textItem
anchors.verticalCenter: parent.verticalCenter
x: {
// Little tweak to have a better text horizontal centering
var centerX = (parent.width - width) / 2
var offset = rightOpen ? Style.marginXS * scaling : -Style.marginXS * scaling
return centerX + offset
}
text: root.text
font.pointSize: 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
onShown: root.shown()
onHidden: root.hidden()
onEntered: root.entered()
onExited: root.exited()
onClicked: root.clicked()
onRightClicked: root.rightClicked()
onMiddleClicked: root.middleClicked()
onWheel: root.wheel
}
}
NIcon {
icon: root.icon
font.pointSize: Style.fontSizeM * scaling
color: hovered && !forceOpen ? Color.mOnTertiary : Color.mOnSurface
// Center horizontally
x: (iconCircle.width - width) / 2
// Center vertically accounting for font metrics
y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2
}
}
Component {
id: horizontalPillComponent
NPillHorizontal {
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
ParallelAnimation {
id: showAnim
running: false
NumberAnimation {
target: pill
property: "width"
from: 1
to: maxPillWidth
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
NumberAnimation {
target: pill
property: "opacity"
from: 0
to: 1
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
onStarted: {
showPill = true
}
onStopped: {
delayedHideAnim.start()
root.shown()
}
}
SequentialAnimation {
id: delayedHideAnim
running: false
PauseAnimation {
duration: 2500
}
ScriptAction {
script: if (shouldAnimateHide) {
hideAnim.start()
}
}
}
ParallelAnimation {
id: hideAnim
running: false
NumberAnimation {
target: pill
property: "width"
from: maxPillWidth
to: 1
duration: Style.animationNormal
easing.type: Easing.InCubic
}
NumberAnimation {
target: pill
property: "opacity"
from: 1
to: 0
duration: Style.animationNormal
easing.type: Easing.InCubic
}
onStopped: {
showPill = false
shouldAnimateHide = false
root.hidden()
}
}
NTooltip {
id: tooltip
positionAbove: Settings.data.bar.position === "bottom"
target: pill
delay: Style.tooltipDelayLong
text: root.tooltipText
}
Timer {
id: showTimer
interval: Style.pillDelay
onTriggered: {
if (!showPill) {
showAnim.start()
onShown: root.shown()
onHidden: root.hidden()
onEntered: root.entered()
onExited: root.exited()
onClicked: root.clicked()
onRightClicked: root.rightClicked()
onMiddleClicked: root.middleClicked()
onWheel: root.wheel
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onEntered: {
hovered = true
root.entered()
tooltip.show()
if (disableOpen) {
return
}
if (!forceOpen) {
showDelayed()
}
}
onExited: {
hovered = false
root.exited()
if (!forceOpen) {
hide()
}
tooltip.hide()
}
onClicked: function (mouse) {
if (mouse.button === Qt.LeftButton) {
root.clicked()
} else if (mouse.button === Qt.RightButton) {
root.rightClicked()
} else if (mouse.button === Qt.MiddleButton) {
root.middleClicked()
}
}
onWheel: wheel => {
root.wheel(wheel.angleDelta.y)
}
}
function show() {
if (!showPill) {
shouldAnimateHide = autoHide
showAnim.start()
} else {
hideAnim.stop()
delayedHideAnim.restart()
if (pillLoader.item && pillLoader.item.show) {
pillLoader.item.show()
}
}
function hide() {
if (forceOpen) {
return
if (pillLoader.item && pillLoader.item.hide) {
pillLoader.item.hide()
}
if (showPill) {
hideAnim.start()
}
showTimer.stop()
}
function showDelayed() {
if (!showPill) {
shouldAnimateHide = autoHide
showTimer.start()
} else {
hideAnim.stop()
delayedHideAnim.restart()
}
}
onForceOpenChanged: {
if (forceOpen) {
// Immediately lock open without animations
showAnim.stop()
hideAnim.stop()
delayedHideAnim.stop()
showPill = true
} else {
hide()
if (pillLoader.item && pillLoader.item.showDelayed) {
pillLoader.item.showDelayed()
}
}
}

326
Widgets/NPillHorizontal.qml Normal file
View 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
View 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()
}
}
}

View file

@ -0,0 +1,253 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services
import qs.Widgets
import "../Helpers/FuzzySort.js" as Fuzzysort
RowLayout {
id: root
property real minimumWidth: 280 * scaling
property real popupHeight: 180 * scaling
property string label: ""
property string description: ""
property ListModel model: {
}
property string currentKey: ""
property string placeholder: ""
property string searchPlaceholder: "Search..."
readonly property real preferredHeight: Style.baseWidgetSize * 1.1 * scaling
signal selected(string key)
spacing: Style.marginL * scaling
Layout.fillWidth: true
// Filtered model for search results
property ListModel filteredModel: ListModel {}
property string searchText: ""
function findIndexByKey(key) {
for (var i = 0; i < root.model.count; i++) {
if (root.model.get(i).key === key) {
return i
}
}
return -1
}
function findIndexByKeyInFiltered(key) {
for (var i = 0; i < root.filteredModel.count; i++) {
if (root.filteredModel.get(i).key === key) {
return i
}
}
return -1
}
function filterModel() {
filteredModel.clear()
if (searchText.trim() === "") {
// If no search text, show all items
for (var i = 0; i < root.model.count; i++) {
filteredModel.append(root.model.get(i))
}
} else {
// Convert ListModel to array for fuzzy search
var items = []
for (var i = 0; i < root.model.count; i++) {
items.push(root.model.get(i))
}
// Use fuzzy search if available, fallback to simple search
if (typeof Fuzzysort !== 'undefined') {
var fuzzyResults = Fuzzysort.go(searchText, items, {
"key": "name",
"threshold": -1000,
"limit": 50
})
// Add results in order of relevance
for (var j = 0; j < fuzzyResults.length; j++) {
filteredModel.append(fuzzyResults[j].obj)
}
} else {
// Fallback to simple search
var searchLower = searchText.toLowerCase()
for (var i = 0; i < items.length; i++) {
var item = items[i]
if (item.name.toLowerCase().includes(searchLower)) {
filteredModel.append(item)
}
}
}
}
}
onSearchTextChanged: filterModel()
onModelChanged: filterModel()
NLabel {
label: root.label
description: root.description
}
Item {
Layout.fillWidth: true
}
ComboBox {
id: combo
Layout.minimumWidth: root.minimumWidth
Layout.preferredHeight: root.preferredHeight
model: filteredModel
currentIndex: findIndexByKeyInFiltered(currentKey)
onActivated: {
if (combo.currentIndex >= 0 && combo.currentIndex < filteredModel.count) {
root.selected(filteredModel.get(combo.currentIndex).key)
}
}
background: Rectangle {
implicitWidth: Style.baseWidgetSize * 3.75 * scaling
implicitHeight: preferredHeight
color: Color.mSurface
border.color: combo.activeFocus ? Color.mSecondary : Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
radius: Style.radiusM * scaling
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
}
}
}
contentItem: NText {
leftPadding: Style.marginL * scaling
rightPadding: combo.indicator.width + Style.marginL * scaling
font.pointSize: Style.fontSizeM * scaling
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
color: (combo.currentIndex >= 0 && combo.currentIndex < filteredModel.count) ? Color.mOnSurface : Color.mOnSurfaceVariant
text: (combo.currentIndex >= 0 && combo.currentIndex < filteredModel.count) ? filteredModel.get(combo.currentIndex).name : root.placeholder
}
indicator: NIcon {
x: combo.width - width - Style.marginM * scaling
y: combo.topPadding + (combo.availableHeight - height) / 2
icon: "caret-down"
font.pointSize: Style.fontSizeL * scaling
}
popup: Popup {
y: combo.height
width: combo.width
height: root.popupHeight + 60 * scaling
padding: Style.marginM * scaling
contentItem: ColumnLayout {
spacing: Style.marginS * scaling
// Search input
NTextInput {
id: searchInput
Layout.fillWidth: true
placeholderText: root.searchPlaceholder
text: root.searchText
onTextChanged: root.searchText = text
fontSize: Style.fontSizeS * scaling
}
// Font list
ListView {
id: listView
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
model: combo.popup.visible ? filteredModel : null
ScrollIndicator.vertical: ScrollIndicator {}
delegate: ItemDelegate {
width: listView.width
hoverEnabled: true
highlighted: ListView.view.currentIndex === index
onHoveredChanged: {
if (hovered) {
ListView.view.currentIndex = index
}
}
onClicked: {
root.selected(filteredModel.get(index).key)
combo.currentIndex = root.findIndexByKeyInFiltered(filteredModel.get(index).key)
combo.popup.close()
}
contentItem: NText {
text: name
font.pointSize: Style.fontSizeM * scaling
color: highlighted ? Color.mSurface : Color.mOnSurface
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
background: Rectangle {
width: listView.width * scaling
color: highlighted ? Color.mTertiary : Color.transparent
radius: Style.radiusS * scaling
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
}
}
}
background: Rectangle {
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
radius: Style.radiusM * scaling
}
}
// Update the currentIndex if the currentKey is changed externally
Connections {
target: root
function onCurrentKeyChanged() {
combo.currentIndex = root.findIndexByKeyInFiltered(currentKey)
}
}
// Focus search input when popup opens
Connections {
target: combo.popup
function onVisibleChanged() {
if (combo.popup.visible) {
// Small delay to ensure the popup is fully rendered
Qt.callLater(function () {
if (searchInput && searchInput.inputItem) {
searchInput.inputItem.forceActiveFocus()
}
})
}
}
}
}
}

26
Widgets/NTextVertical.qml Normal file
View 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
}
}
}

View file

@ -11,7 +11,7 @@ RowLayout {
property string description: ""
property bool checked: false
property bool hovering: false
property int baseSize: Style.baseWidgetSize
property int baseSize: Style.baseWidgetSize * 0.8
signal toggled(bool checked)
signal entered
@ -31,7 +31,7 @@ RowLayout {
implicitHeight: root.baseSize * scaling
radius: height * 0.5
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)
Behavior on color {
@ -53,7 +53,7 @@ RowLayout {
color: root.checked ? Color.mOnPrimary : Color.mPrimary
border.color: root.checked ? Color.mSurface : Color.mSurface
border.width: Math.max(1, Style.borderM * scaling)
y: 2 * scaling
anchors.verticalCenter: parent.verticalCenter
x: root.checked ? switcher.width - width - 2 * scaling : 2 * scaling
Behavior on x {

View file

@ -13,6 +13,8 @@ Window {
property bool positionLeft: false
property bool positionRight: false
readonly property string barPosition: Settings.data.bar.position
flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
color: Color.transparent
visible: false
@ -46,17 +48,34 @@ Window {
return
}
if (positionLeft) {
// Auto-detect positioning based on bar position if not explicitly set
var shouldPositionLeft = positionLeft
var shouldPositionRight = positionRight
var shouldPositionAbove = positionAbove
// If no explicit positioning is set, auto-detect based on bar position
if (!positionLeft && !positionRight && !positionAbove) {
if (barPosition === "left") {
shouldPositionRight = true
} else if (barPosition === "right") {
shouldPositionLeft = true
} else if (barPosition === "bottom") {
shouldPositionAbove = true
}
// For "top" bar, default to below (no change needed)
}
if (shouldPositionLeft) {
// Position tooltip to the left of the target
var pos = target.mapToGlobal(0, 0)
x = pos.x - width - 12 // 12 px margin to the left
y = pos.y - height / 2 + target.height / 2
} else if (positionRight) {
} else if (shouldPositionRight) {
// Position tooltip to the right of the target
var pos = target.mapToGlobal(target.width, 0)
x = pos.x + 12 // 12 px margin to the right
y = pos.y - height / 2 + target.height / 2
} else if (positionAbove) {
} else if (shouldPositionAbove) {
// Position tooltip above the target
var pos = target.mapToGlobal(0, 0)
x = pos.x - width / 2 + target.width / 2

View file

@ -1,6 +1,5 @@
{
description =
"Noctalia shell - a Wayland desktop shell built with Quickshell";
description = "Noctalia shell - a Wayland desktop shell built with Quickshell";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
@ -12,13 +11,22 @@
};
};
outputs = { self, nixpkgs, systems, quickshell, ... }:
let eachSystem = nixpkgs.lib.genAttrs (import systems);
in {
formatter =
eachSystem (system: nixpkgs.legacyPackages.${system}.alejandra);
outputs =
{
self,
nixpkgs,
systems,
quickshell,
...
}:
let
eachSystem = nixpkgs.lib.genAttrs (import systems);
in
{
formatter = eachSystem (system: nixpkgs.legacyPackages.${system}.alejandra);
packages = eachSystem (system:
packages = eachSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
qs = quickshell.packages.${system}.default.override {
@ -26,7 +34,8 @@
withI3 = false;
};
runtimeDeps = with pkgs;
runtimeDeps =
with pkgs;
[
bash
bluez
@ -41,21 +50,34 @@
matugen
networkmanager
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 {
fontDirectories = [ pkgs.roboto pkgs.inter-nerdfont ];
fontDirectories = [
pkgs.roboto
pkgs.inter-nerdfont
];
};
in {
in
{
default = pkgs.stdenv.mkDerivation {
pname = "noctalia-shell";
version = self.rev or self.dirtyRev or "dirty";
src = ./.;
nativeBuildInputs =
[ pkgs.gcc pkgs.makeWrapper pkgs.qt6.wrapQtAppsHook ];
buildInputs = [ qs pkgs.xkeyboard_config pkgs.qt6.qtbase ];
nativeBuildInputs = [
pkgs.gcc
pkgs.makeWrapper
pkgs.qt6.wrapQtAppsHook
];
buildInputs = [
qs
pkgs.xkeyboard-config
pkgs.qt6.qtbase
];
propagatedBuildInputs = runtimeDeps;
installPhase = ''
@ -69,14 +91,14 @@
'';
meta = {
description =
"A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell.";
description = "A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell.";
homepage = "https://github.com/noctalia-dev/noctalia-shell";
license = pkgs.lib.licenses.mit;
mainProgram = "noctalia-shell";
};
};
});
}
);
defaultPackage = eachSystem (system: self.packages.${system}.default);
};