Vertical Bar!
This commit is contained in:
commit
0c6aea7154
49 changed files with 3140 additions and 1154 deletions
|
|
@ -106,6 +106,7 @@ Singleton {
|
|||
"settings-wallpaper-selector": "library-photo",
|
||||
"settings-screen-recorder": "video",
|
||||
"settings-hooks": "link",
|
||||
"settings-notification": "bell",
|
||||
"settings-about": "info-square-rounded",
|
||||
"bluetooth": "bluetooth",
|
||||
"bt-device-generic": "bluetooth",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -61,15 +61,6 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: externalHideTimer
|
||||
running: false
|
||||
interval: 1500
|
||||
onTriggered: {
|
||||
pill.hide()
|
||||
}
|
||||
}
|
||||
|
||||
NPill {
|
||||
id: pill
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ NPanel {
|
|||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: "Close"
|
||||
tooltipText: "Close."
|
||||
sizeRatio: 0.8
|
||||
onClicked: {
|
||||
root.close()
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ NPanel {
|
|||
|
||||
preferredWidth: 340
|
||||
preferredHeight: 320
|
||||
panelAnchorRight: true
|
||||
panelAnchorRight: Settings.data.bar.position === "right"
|
||||
|
||||
// Main Column
|
||||
panelContent: ColumnLayout {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,340 +0,0 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
// Time dropdown options (00:00 .. 23:30)
|
||||
ListModel {
|
||||
id: timeOptions
|
||||
}
|
||||
Component.onCompleted: {
|
||||
for (var h = 0; h < 24; h++) {
|
||||
for (var m = 0; m < 60; m += 30) {
|
||||
var hh = ("0" + h).slice(-2)
|
||||
var mm = ("0" + m).slice(-2)
|
||||
var key = hh + ":" + mm
|
||||
timeOptions.append({
|
||||
"key": key,
|
||||
"name": key
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for wlsunset availability when enabling Night Light
|
||||
Process {
|
||||
id: wlsunsetCheck
|
||||
command: ["which", "wlsunset"]
|
||||
running: false
|
||||
|
||||
onExited: function (exitCode) {
|
||||
if (exitCode === 0) {
|
||||
Settings.data.nightLight.enabled = true
|
||||
NightLightService.apply()
|
||||
ToastService.showNotice("Night Light", "Enabled")
|
||||
} else {
|
||||
Settings.data.nightLight.enabled = false
|
||||
ToastService.showWarning("Night Light", "wlsunset not installed")
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {}
|
||||
}
|
||||
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
// Brightness Step Section
|
||||
ColumnLayout {
|
||||
spacing: Style.marginS * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
NSpinBox {
|
||||
Layout.fillWidth: true
|
||||
label: "Brightness Step Size"
|
||||
description: "Adjust the step size for brightness changes (scroll wheel, keyboard shortcuts)."
|
||||
minimum: 1
|
||||
maximum: 50
|
||||
value: Settings.data.brightness.brightnessStep
|
||||
stepSize: 1
|
||||
suffix: "%"
|
||||
onValueChanged: {
|
||||
Settings.data.brightness.brightnessStep = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Monitor Overview Section
|
||||
ColumnLayout {
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
NLabel {
|
||||
label: "Monitors Brightness Control"
|
||||
description: "Current brightness levels for all detected monitors."
|
||||
}
|
||||
|
||||
// Single monitor display using the same data source as the bar icon
|
||||
Repeater {
|
||||
model: BrightnessService.monitors
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
radius: Style.radiusM * scaling
|
||||
color: Color.mSurface
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
implicitHeight: contentCol.implicitHeight + Style.marginXL * 2 * scaling
|
||||
|
||||
ColumnLayout {
|
||||
id: contentCol
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NText {
|
||||
text: `${model.modelData.name} [${model.modelData.model}]`
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mSecondary
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NText {
|
||||
text: model.method
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NText {
|
||||
text: "Brightness:"
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
NSlider {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 1
|
||||
value: model.brightness
|
||||
stepSize: 0.05
|
||||
onPressedChanged: {
|
||||
if (!pressed) {
|
||||
var monitor = BrightnessService.getMonitorForScreen(model.modelData)
|
||||
monitor.setBrightness(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: Math.round(model.brightness * 100) + "%"
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mPrimary
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginXL * scaling
|
||||
Layout.bottomMargin: Style.marginXL * scaling
|
||||
}
|
||||
|
||||
// Night Light Section
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXS * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
NText {
|
||||
text: "Night Light"
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mSecondary
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Reduce blue light emission to help you sleep better and reduce eye strain."
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Enable Night Light"
|
||||
description: "Apply a warm color filter to reduce blue light emission."
|
||||
checked: Settings.data.nightLight.enabled
|
||||
onToggled: checked => {
|
||||
if (checked) {
|
||||
// Verify wlsunset exists before enabling
|
||||
wlsunsetCheck.running = true
|
||||
} else {
|
||||
Settings.data.nightLight.enabled = false
|
||||
Settings.data.nightLight.forced = false
|
||||
NightLightService.apply()
|
||||
ToastService.showNotice("Night Light", "Disabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Temperature
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXS * scaling
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
NLabel {
|
||||
label: "Color temperature"
|
||||
description: "Choose two temperatures in Kelvin."
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: Settings.data.nightLight.enabled
|
||||
spacing: Style.marginM * scaling
|
||||
Layout.fillWidth: false
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
NText {
|
||||
text: "Night"
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
NTextInput {
|
||||
text: Settings.data.nightLight.nightTemp
|
||||
inputMethodHints: Qt.ImhDigitsOnly
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
onEditingFinished: {
|
||||
var nightTemp = parseInt(text)
|
||||
var dayTemp = parseInt(Settings.data.nightLight.dayTemp)
|
||||
if (!isNaN(nightTemp) && !isNaN(dayTemp)) {
|
||||
// Clamp value between [1000 .. (dayTemp-500)]
|
||||
var clampedValue = Math.min(dayTemp - 500, Math.max(1000, nightTemp))
|
||||
text = Settings.data.nightLight.nightTemp = clampedValue.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {}
|
||||
|
||||
NText {
|
||||
text: "Day"
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
NTextInput {
|
||||
text: Settings.data.nightLight.dayTemp
|
||||
inputMethodHints: Qt.ImhDigitsOnly
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
onEditingFinished: {
|
||||
var dayTemp = parseInt(text)
|
||||
var nightTemp = parseInt(Settings.data.nightLight.nightTemp)
|
||||
if (!isNaN(nightTemp) && !isNaN(dayTemp)) {
|
||||
// Clamp value between [(nightTemp+500) .. 6500]
|
||||
var clampedValue = Math.max(nightTemp + 500, Math.min(6500, dayTemp))
|
||||
text = Settings.data.nightLight.dayTemp = clampedValue.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Automatic Scheduling"
|
||||
description: `Based on the sunset and sunrise time in <i>${LocationService.stableName}</i> - recommended.`
|
||||
checked: Settings.data.nightLight.autoSchedule
|
||||
onToggled: checked => Settings.data.nightLight.autoSchedule = checked
|
||||
visible: Settings.data.nightLight.enabled
|
||||
}
|
||||
|
||||
// Schedule settings
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXS * scaling
|
||||
visible: Settings.data.nightLight.enabled && !Settings.data.nightLight.autoSchedule && !Settings.data.nightLight.forced
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: false
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NLabel {
|
||||
label: "Manual Scheduling"
|
||||
}
|
||||
|
||||
Item {// add a little more spacing
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Sunrise Time"
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
model: timeOptions
|
||||
currentKey: Settings.data.nightLight.manualSunrise
|
||||
placeholder: "Select start time"
|
||||
onSelected: key => Settings.data.nightLight.manualSunrise = key
|
||||
minimumWidth: 120 * scaling
|
||||
}
|
||||
|
||||
Item {// add a little more spacing
|
||||
}
|
||||
|
||||
NText {
|
||||
text: "Sunset Time"
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
NComboBox {
|
||||
model: timeOptions
|
||||
currentKey: Settings.data.nightLight.manualSunset
|
||||
placeholder: "Select stop time"
|
||||
onSelected: key => Settings.data.nightLight.manualSunset = key
|
||||
minimumWidth: 120 * scaling
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Force activation toggle
|
||||
NToggle {
|
||||
label: "Force activation"
|
||||
description: "Immediately apply night temperature without scheduling or fade."
|
||||
checked: Settings.data.nightLight.forced
|
||||
onToggled: checked => {
|
||||
Settings.data.nightLight.forced = checked
|
||||
if (checked && !Settings.data.nightLight.enabled) {
|
||||
// Ensure enabled when forcing
|
||||
wlsunsetCheck.running = true
|
||||
} else {
|
||||
NightLightService.apply()
|
||||
}
|
||||
}
|
||||
visible: Settings.data.nightLight.enabled
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,9 @@ import qs.Widgets
|
|||
|
||||
ColumnLayout {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
183
Modules/SettingsPanel/Tabs/NotificationTab.qml
Normal file
183
Modules/SettingsPanel/Tabs/NotificationTab.qml
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
// Helper functions to update arrays immutably
|
||||
function addMonitor(list, name) {
|
||||
const arr = (list || []).slice()
|
||||
if (!arr.includes(name))
|
||||
arr.push(name)
|
||||
return arr
|
||||
}
|
||||
function removeMonitor(list, name) {
|
||||
return (list || []).filter(function (n) {
|
||||
return n !== name
|
||||
})
|
||||
}
|
||||
|
||||
// General Notification Settings
|
||||
ColumnLayout {
|
||||
spacing: Style.marginL * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
NHeader {
|
||||
label: "Appearance"
|
||||
description: "Configure notifications appearance and behavior."
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: "Do Not Disturb"
|
||||
description: "Disable all notification popups when enabled."
|
||||
checked: Settings.data.notifications.doNotDisturb
|
||||
onToggled: checked => Settings.data.notifications.doNotDisturb = checked
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginXL * scaling
|
||||
Layout.bottomMargin: Style.marginXL * scaling
|
||||
}
|
||||
|
||||
// Monitor Configuration
|
||||
ColumnLayout {
|
||||
spacing: Style.marginM * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
NHeader {
|
||||
label: "Monitors Configuration"
|
||||
description: "Choose which monitors should display notifications."
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Quickshell.screens || []
|
||||
delegate: NCheckbox {
|
||||
Layout.fillWidth: true
|
||||
label: `${modelData.name || "Unknown"}${modelData.model ? `: ${modelData.model}` : ""}`
|
||||
description: `${modelData.width}x${modelData.height} at (${modelData.x}, ${modelData.y})`
|
||||
checked: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1
|
||||
onToggled: checked => {
|
||||
if (checked) {
|
||||
Settings.data.notifications.monitors = addMonitor(Settings.data.notifications.monitors, modelData.name)
|
||||
} else {
|
||||
Settings.data.notifications.monitors = removeMonitor(Settings.data.notifications.monitors, modelData.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginXL * scaling
|
||||
Layout.bottomMargin: Style.marginXL * scaling
|
||||
}
|
||||
|
||||
// Notification Duration Settings
|
||||
ColumnLayout {
|
||||
spacing: Style.marginL * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
NHeader {
|
||||
label: "Notification Duration"
|
||||
description: "Configure how long notifications stay visible based on their urgency level."
|
||||
}
|
||||
|
||||
// Low Urgency Duration
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: "Low Urgency Duration"
|
||||
description: "How long low priority notifications stay visible."
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
NSlider {
|
||||
Layout.fillWidth: true
|
||||
from: 1
|
||||
to: 30
|
||||
stepSize: 1
|
||||
value: Settings.data.notifications.lowUrgencyDuration
|
||||
onMoved: Settings.data.notifications.lowUrgencyDuration = value
|
||||
cutoutColor: Color.mSurface
|
||||
}
|
||||
|
||||
NText {
|
||||
text: Settings.data.notifications.lowUrgencyDuration + "s"
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.leftMargin: Style.marginS * scaling
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normal Urgency Duration
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: "Normal Urgency Duration"
|
||||
description: "How long normal priority notifications stay visible."
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
NSlider {
|
||||
Layout.fillWidth: true
|
||||
from: 1
|
||||
to: 30
|
||||
stepSize: 1
|
||||
value: Settings.data.notifications.normalUrgencyDuration
|
||||
onMoved: Settings.data.notifications.normalUrgencyDuration = value
|
||||
cutoutColor: Color.mSurface
|
||||
}
|
||||
|
||||
NText {
|
||||
text: Settings.data.notifications.normalUrgencyDuration + "s"
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.leftMargin: Style.marginS * scaling
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Critical Urgency Duration
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
label: "Critical Urgency Duration"
|
||||
description: "How long critical priority notifications stay visible."
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
NSlider {
|
||||
Layout.fillWidth: true
|
||||
from: 1
|
||||
to: 30
|
||||
stepSize: 1
|
||||
value: Settings.data.notifications.criticalUrgencyDuration
|
||||
onMoved: Settings.data.notifications.criticalUrgencyDuration = value
|
||||
cutoutColor: Color.mSurface
|
||||
}
|
||||
|
||||
NText {
|
||||
text: Settings.data.notifications.criticalUrgencyDuration + "s"
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.leftMargin: Style.marginS * scaling
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,11 @@ ColumnLayout {
|
|||
|
||||
spacing: Style.marginL * scaling
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ NPanel {
|
|||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: "Close"
|
||||
tooltipText: "Close."
|
||||
sizeRatio: 0.8
|
||||
onClicked: root.close()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
32
Widgets/NHeader.qml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
property string description: ""
|
||||
|
||||
spacing: Style.marginXXS * scaling
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginM * scaling
|
||||
|
||||
NText {
|
||||
text: root.label
|
||||
font.pointSize: Style.fontSizeXL * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
font.capitalization: Font.Capitalize
|
||||
color: Color.mSecondary
|
||||
visible: root.title !== ""
|
||||
}
|
||||
|
||||
NText {
|
||||
text: root.description
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: Color.mOnSurfaceVariant
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
visible: root.description !== ""
|
||||
}
|
||||
}
|
||||
|
|
@ -40,6 +40,7 @@ Loader {
|
|||
property real opacityValue: originalOpacity
|
||||
|
||||
property 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)
|
||||
|
|
|
|||
|
|
@ -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
326
Widgets/NPillHorizontal.qml
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string icon: ""
|
||||
property string text: ""
|
||||
property string tooltipText: ""
|
||||
property real sizeRatio: 0.8
|
||||
property bool autoHide: false
|
||||
property bool forceOpen: false
|
||||
property bool disableOpen: false
|
||||
property bool rightOpen: false
|
||||
property bool hovered: false
|
||||
property real fontSize: Style.fontSizeXS
|
||||
|
||||
// Bar position detection for pill direction
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
|
||||
|
||||
// Determine pill direction based on section position
|
||||
readonly property bool openRightward: rightOpen
|
||||
readonly property bool openLeftward: !rightOpen
|
||||
|
||||
// Effective shown state (true if animated open or forced)
|
||||
readonly property bool revealed: forceOpen || showPill
|
||||
|
||||
signal shown
|
||||
signal hidden
|
||||
signal entered
|
||||
signal exited
|
||||
signal clicked
|
||||
signal rightClicked
|
||||
signal middleClicked
|
||||
signal wheel(int delta)
|
||||
|
||||
// Internal state
|
||||
property bool showPill: false
|
||||
property bool shouldAnimateHide: false
|
||||
|
||||
// Sizing logic for horizontal bars
|
||||
readonly property int iconSize: Math.round(Style.baseWidgetSize * sizeRatio * scaling)
|
||||
readonly property int pillWidth: iconSize
|
||||
readonly property int pillPaddingHorizontal: Style.marginS * scaling
|
||||
readonly property int pillPaddingVertical: Style.marginS * scaling
|
||||
readonly property int pillOverlap: iconSize * 0.5
|
||||
readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 4)
|
||||
readonly property int maxPillHeight: iconSize
|
||||
|
||||
// For horizontal bars: height is just icon size, width includes pill space
|
||||
width: revealed ? (openRightward ? (iconSize + maxPillWidth - pillOverlap) : (iconSize + maxPillWidth - pillOverlap)) : iconSize
|
||||
height: iconSize
|
||||
|
||||
Rectangle {
|
||||
id: pill
|
||||
width: revealed ? maxPillWidth : 1
|
||||
height: revealed ? maxPillHeight : 1
|
||||
|
||||
// Position based on direction - center the pill relative to the icon
|
||||
x: openLeftward ? (iconCircle.x + iconCircle.width / 2 - width) : (iconCircle.x + iconCircle.width / 2 - pillOverlap)
|
||||
y: 0
|
||||
|
||||
opacity: revealed ? Style.opacityFull : Style.opacityNone
|
||||
color: Color.mSurfaceVariant
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
|
||||
// Radius logic for horizontal expansion - rounded on the side that connects to icon
|
||||
topLeftRadius: openLeftward ? iconSize * 0.5 : 0
|
||||
bottomLeftRadius: openLeftward ? iconSize * 0.5 : 0
|
||||
topRightRadius: openRightward ? iconSize * 0.5 : 0
|
||||
bottomRightRadius: openRightward ? iconSize * 0.5 : 0
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
NText {
|
||||
id: textItem
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenterOffset: openLeftward ? -6 * scaling : 6 * scaling // Adjust based on opening direction
|
||||
text: root.text
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeXXS * scaling
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
visible: revealed
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
enabled: showAnim.running || hideAnim.running
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
enabled: showAnim.running || hideAnim.running
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
enabled: showAnim.running || hideAnim.running
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: iconCircle
|
||||
width: iconSize
|
||||
height: iconSize
|
||||
radius: width * 0.5
|
||||
color: hovered && !forceOpen ? Color.mTertiary : Color.mSurfaceVariant
|
||||
|
||||
// Icon positioning based on direction
|
||||
x: openLeftward ? (parent.width - width) : 0
|
||||
y: 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: root.icon
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: hovered && !forceOpen ? Color.mOnTertiary : Color.mOnSurfaceVariant
|
||||
// Center horizontally
|
||||
x: (iconCircle.width - width) / 2
|
||||
// Center vertically accounting for font metrics
|
||||
y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: showAnim
|
||||
running: false
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "width"
|
||||
from: 1
|
||||
to: maxPillWidth
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "height"
|
||||
from: 1
|
||||
to: maxPillHeight
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
onStarted: {
|
||||
showPill = true
|
||||
}
|
||||
onStopped: {
|
||||
delayedHideAnim.start()
|
||||
root.shown()
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: delayedHideAnim
|
||||
running: false
|
||||
PauseAnimation {
|
||||
duration: 2500
|
||||
}
|
||||
ScriptAction {
|
||||
script: if (shouldAnimateHide) {
|
||||
hideAnim.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: hideAnim
|
||||
running: false
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "width"
|
||||
from: maxPillWidth
|
||||
to: 1
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "height"
|
||||
from: maxPillHeight
|
||||
to: 1
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
onStopped: {
|
||||
showPill = false
|
||||
shouldAnimateHide = false
|
||||
root.hidden()
|
||||
}
|
||||
}
|
||||
|
||||
NTooltip {
|
||||
id: tooltip
|
||||
target: pill
|
||||
text: root.tooltipText
|
||||
positionLeft: barPosition === "right"
|
||||
positionRight: barPosition === "left"
|
||||
positionAbove: Settings.data.bar.position === "bottom"
|
||||
delay: Style.tooltipDelayLong
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: showTimer
|
||||
interval: Style.pillDelay
|
||||
onTriggered: {
|
||||
if (!showPill) {
|
||||
showAnim.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onEntered: {
|
||||
hovered = true
|
||||
root.entered()
|
||||
tooltip.show()
|
||||
if (disableOpen) {
|
||||
return
|
||||
}
|
||||
if (!forceOpen) {
|
||||
showDelayed()
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
hovered = false
|
||||
root.exited()
|
||||
if (!forceOpen) {
|
||||
hide()
|
||||
}
|
||||
tooltip.hide()
|
||||
}
|
||||
onClicked: function (mouse) {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
root.clicked()
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
root.rightClicked()
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
root.middleClicked()
|
||||
}
|
||||
}
|
||||
onWheel: wheel => {
|
||||
root.wheel(wheel.angleDelta.y)
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (!showPill) {
|
||||
shouldAnimateHide = autoHide
|
||||
showAnim.start()
|
||||
} else {
|
||||
hideAnim.stop()
|
||||
delayedHideAnim.restart()
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (forceOpen) {
|
||||
return
|
||||
}
|
||||
if (showPill) {
|
||||
hideAnim.start()
|
||||
}
|
||||
showTimer.stop()
|
||||
}
|
||||
|
||||
function showDelayed() {
|
||||
if (!showPill) {
|
||||
shouldAnimateHide = autoHide
|
||||
showTimer.start()
|
||||
} else {
|
||||
hideAnim.stop()
|
||||
delayedHideAnim.restart()
|
||||
}
|
||||
}
|
||||
|
||||
onForceOpenChanged: {
|
||||
if (forceOpen) {
|
||||
// Immediately lock open without animations
|
||||
showAnim.stop()
|
||||
hideAnim.stop()
|
||||
delayedHideAnim.stop()
|
||||
showPill = true
|
||||
} else {
|
||||
hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
325
Widgets/NPillVertical.qml
Normal file
325
Widgets/NPillVertical.qml
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string icon: ""
|
||||
property string text: ""
|
||||
property string tooltipText: ""
|
||||
property real sizeRatio: 0.8
|
||||
property bool autoHide: false
|
||||
property bool forceOpen: false
|
||||
property bool disableOpen: false
|
||||
property bool rightOpen: false
|
||||
property bool hovered: false
|
||||
property real fontSize: Style.fontSizeXS
|
||||
|
||||
// Bar position detection for pill direction
|
||||
readonly property string barPosition: Settings.data.bar.position
|
||||
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
|
||||
|
||||
// Determine pill direction based on section position
|
||||
readonly property bool openDownward: rightOpen
|
||||
readonly property bool openUpward: !rightOpen
|
||||
|
||||
// Effective shown state (true if animated open or forced)
|
||||
readonly property bool revealed: forceOpen || showPill
|
||||
|
||||
signal shown
|
||||
signal hidden
|
||||
signal entered
|
||||
signal exited
|
||||
signal clicked
|
||||
signal rightClicked
|
||||
signal middleClicked
|
||||
signal wheel(int delta)
|
||||
|
||||
// Internal state
|
||||
property bool showPill: false
|
||||
property bool shouldAnimateHide: false
|
||||
|
||||
// Sizing logic for vertical bars
|
||||
readonly property int iconSize: Math.round(Style.baseWidgetSize * sizeRatio * scaling)
|
||||
readonly property int pillHeight: iconSize
|
||||
readonly property int pillPaddingHorizontal: Style.marginS * scaling
|
||||
readonly property int pillPaddingVertical: Style.marginS * scaling
|
||||
readonly property int pillOverlap: iconSize * 0.5
|
||||
readonly property int maxPillWidth: iconSize
|
||||
readonly property int maxPillHeight: Math.max(1, textItem.implicitHeight + pillPaddingVertical * 3)
|
||||
|
||||
// For vertical bars: width is just icon size, height includes pill space
|
||||
width: iconSize
|
||||
height: revealed ? (iconSize + maxPillHeight - pillOverlap) : iconSize
|
||||
|
||||
Rectangle {
|
||||
id: pill
|
||||
width: revealed ? maxPillWidth : 1
|
||||
height: revealed ? maxPillHeight : 1
|
||||
|
||||
// Position based on direction - center the pill relative to the icon
|
||||
x: 0
|
||||
y: openUpward ? (iconCircle.y + iconCircle.height / 2 - height) : (iconCircle.y + iconCircle.height / 2)
|
||||
|
||||
opacity: revealed ? Style.opacityFull : Style.opacityNone
|
||||
color: Color.mSurfaceVariant
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
|
||||
// Radius logic for vertical expansion - rounded on the side that connects to icon
|
||||
topLeftRadius: openUpward ? iconSize * 0.5 : 0
|
||||
bottomLeftRadius: openDownward ? iconSize * 0.5 : 0
|
||||
topRightRadius: openUpward ? iconSize * 0.5 : 0
|
||||
bottomRightRadius: openDownward ? iconSize * 0.5 : 0
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
NTextVertical {
|
||||
id: textItem
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: openUpward ? -6 * scaling : 6 * scaling // Adjust based on opening direction
|
||||
text: root.text
|
||||
fontSize: Style.fontSizeXXS * scaling
|
||||
fontWeight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
visible: revealed
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
enabled: showAnim.running || hideAnim.running
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
enabled: showAnim.running || hideAnim.running
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
enabled: showAnim.running || hideAnim.running
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: iconCircle
|
||||
width: iconSize
|
||||
height: iconSize
|
||||
radius: width * 0.5
|
||||
color: hovered && !forceOpen ? Color.mTertiary : Color.mSurfaceVariant
|
||||
|
||||
// Icon positioning based on direction
|
||||
x: 0
|
||||
y: openUpward ? (parent.height - height) : 0
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: root.icon
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: hovered && !forceOpen ? Color.mOnTertiary : Color.mOnSurfaceVariant
|
||||
// Center horizontally
|
||||
x: (iconCircle.width - width) / 2
|
||||
// Center vertically accounting for font metrics
|
||||
y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: showAnim
|
||||
running: false
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "width"
|
||||
from: 1
|
||||
to: maxPillWidth
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "height"
|
||||
from: 1
|
||||
to: maxPillHeight
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
onStarted: {
|
||||
showPill = true
|
||||
}
|
||||
onStopped: {
|
||||
delayedHideAnim.start()
|
||||
root.shown()
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: delayedHideAnim
|
||||
running: false
|
||||
PauseAnimation {
|
||||
duration: 2500
|
||||
}
|
||||
ScriptAction {
|
||||
script: if (shouldAnimateHide) {
|
||||
hideAnim.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: hideAnim
|
||||
running: false
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "width"
|
||||
from: maxPillWidth
|
||||
to: 1
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "height"
|
||||
from: maxPillHeight
|
||||
to: 1
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pill
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
onStopped: {
|
||||
showPill = false
|
||||
shouldAnimateHide = false
|
||||
root.hidden()
|
||||
}
|
||||
}
|
||||
|
||||
NTooltip {
|
||||
id: tooltip
|
||||
target: pill
|
||||
text: root.tooltipText
|
||||
positionLeft: barPosition === "right"
|
||||
positionRight: barPosition === "left"
|
||||
positionAbove: Settings.data.bar.position === "bottom"
|
||||
delay: Style.tooltipDelayLong
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: showTimer
|
||||
interval: Style.pillDelay
|
||||
onTriggered: {
|
||||
if (!showPill) {
|
||||
showAnim.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onEntered: {
|
||||
hovered = true
|
||||
root.entered()
|
||||
tooltip.show()
|
||||
if (disableOpen) {
|
||||
return
|
||||
}
|
||||
if (!forceOpen) {
|
||||
showDelayed()
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
hovered = false
|
||||
root.exited()
|
||||
if (!forceOpen) {
|
||||
hide()
|
||||
}
|
||||
tooltip.hide()
|
||||
}
|
||||
onClicked: function (mouse) {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
root.clicked()
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
root.rightClicked()
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
root.middleClicked()
|
||||
}
|
||||
}
|
||||
onWheel: wheel => {
|
||||
root.wheel(wheel.angleDelta.y)
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (!showPill) {
|
||||
shouldAnimateHide = autoHide
|
||||
showAnim.start()
|
||||
} else {
|
||||
hideAnim.stop()
|
||||
delayedHideAnim.restart()
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (forceOpen) {
|
||||
return
|
||||
}
|
||||
if (showPill) {
|
||||
hideAnim.start()
|
||||
}
|
||||
showTimer.stop()
|
||||
}
|
||||
|
||||
function showDelayed() {
|
||||
if (!showPill) {
|
||||
shouldAnimateHide = autoHide
|
||||
showTimer.start()
|
||||
} else {
|
||||
hideAnim.stop()
|
||||
delayedHideAnim.restart()
|
||||
}
|
||||
}
|
||||
|
||||
onForceOpenChanged: {
|
||||
if (forceOpen) {
|
||||
// Immediately lock open without animations
|
||||
showAnim.stop()
|
||||
hideAnim.stop()
|
||||
delayedHideAnim.stop()
|
||||
showPill = true
|
||||
} else {
|
||||
hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
253
Widgets/NSearchableComboBox.qml
Normal file
253
Widgets/NSearchableComboBox.qml
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import "../Helpers/FuzzySort.js" as Fuzzysort
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
property real minimumWidth: 280 * scaling
|
||||
property real popupHeight: 180 * scaling
|
||||
|
||||
property string label: ""
|
||||
property string description: ""
|
||||
property ListModel model: {
|
||||
|
||||
}
|
||||
property string currentKey: ""
|
||||
property string placeholder: ""
|
||||
property string searchPlaceholder: "Search..."
|
||||
|
||||
readonly property real preferredHeight: Style.baseWidgetSize * 1.1 * scaling
|
||||
|
||||
signal selected(string key)
|
||||
|
||||
spacing: Style.marginL * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Filtered model for search results
|
||||
property ListModel filteredModel: ListModel {}
|
||||
property string searchText: ""
|
||||
|
||||
function findIndexByKey(key) {
|
||||
for (var i = 0; i < root.model.count; i++) {
|
||||
if (root.model.get(i).key === key) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
function findIndexByKeyInFiltered(key) {
|
||||
for (var i = 0; i < root.filteredModel.count; i++) {
|
||||
if (root.filteredModel.get(i).key === key) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
function filterModel() {
|
||||
filteredModel.clear()
|
||||
|
||||
if (searchText.trim() === "") {
|
||||
// If no search text, show all items
|
||||
for (var i = 0; i < root.model.count; i++) {
|
||||
filteredModel.append(root.model.get(i))
|
||||
}
|
||||
} else {
|
||||
// Convert ListModel to array for fuzzy search
|
||||
var items = []
|
||||
for (var i = 0; i < root.model.count; i++) {
|
||||
items.push(root.model.get(i))
|
||||
}
|
||||
|
||||
// Use fuzzy search if available, fallback to simple search
|
||||
if (typeof Fuzzysort !== 'undefined') {
|
||||
var fuzzyResults = Fuzzysort.go(searchText, items, {
|
||||
"key": "name",
|
||||
"threshold": -1000,
|
||||
"limit": 50
|
||||
})
|
||||
|
||||
// Add results in order of relevance
|
||||
for (var j = 0; j < fuzzyResults.length; j++) {
|
||||
filteredModel.append(fuzzyResults[j].obj)
|
||||
}
|
||||
} else {
|
||||
// Fallback to simple search
|
||||
var searchLower = searchText.toLowerCase()
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var item = items[i]
|
||||
if (item.name.toLowerCase().includes(searchLower)) {
|
||||
filteredModel.append(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSearchTextChanged: filterModel()
|
||||
onModelChanged: filterModel()
|
||||
|
||||
NLabel {
|
||||
label: root.label
|
||||
description: root.description
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
id: combo
|
||||
|
||||
Layout.minimumWidth: root.minimumWidth
|
||||
Layout.preferredHeight: root.preferredHeight
|
||||
model: filteredModel
|
||||
currentIndex: findIndexByKeyInFiltered(currentKey)
|
||||
onActivated: {
|
||||
if (combo.currentIndex >= 0 && combo.currentIndex < filteredModel.count) {
|
||||
root.selected(filteredModel.get(combo.currentIndex).key)
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
implicitWidth: Style.baseWidgetSize * 3.75 * scaling
|
||||
implicitHeight: preferredHeight
|
||||
color: Color.mSurface
|
||||
border.color: combo.activeFocus ? Color.mSecondary : Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
radius: Style.radiusM * scaling
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: NText {
|
||||
leftPadding: Style.marginL * scaling
|
||||
rightPadding: combo.indicator.width + Style.marginL * scaling
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
color: (combo.currentIndex >= 0 && combo.currentIndex < filteredModel.count) ? Color.mOnSurface : Color.mOnSurfaceVariant
|
||||
text: (combo.currentIndex >= 0 && combo.currentIndex < filteredModel.count) ? filteredModel.get(combo.currentIndex).name : root.placeholder
|
||||
}
|
||||
|
||||
indicator: NIcon {
|
||||
x: combo.width - width - Style.marginM * scaling
|
||||
y: combo.topPadding + (combo.availableHeight - height) / 2
|
||||
icon: "caret-down"
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
}
|
||||
|
||||
popup: Popup {
|
||||
y: combo.height
|
||||
width: combo.width
|
||||
height: root.popupHeight + 60 * scaling
|
||||
padding: Style.marginM * scaling
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
// Search input
|
||||
NTextInput {
|
||||
id: searchInput
|
||||
Layout.fillWidth: true
|
||||
placeholderText: root.searchPlaceholder
|
||||
text: root.searchText
|
||||
onTextChanged: root.searchText = text
|
||||
fontSize: Style.fontSizeS * scaling
|
||||
}
|
||||
|
||||
// Font list
|
||||
ListView {
|
||||
id: listView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
model: combo.popup.visible ? filteredModel : null
|
||||
ScrollIndicator.vertical: ScrollIndicator {}
|
||||
|
||||
delegate: ItemDelegate {
|
||||
width: listView.width
|
||||
hoverEnabled: true
|
||||
highlighted: ListView.view.currentIndex === index
|
||||
|
||||
onHoveredChanged: {
|
||||
if (hovered) {
|
||||
ListView.view.currentIndex = index
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
root.selected(filteredModel.get(index).key)
|
||||
combo.currentIndex = root.findIndexByKeyInFiltered(filteredModel.get(index).key)
|
||||
combo.popup.close()
|
||||
}
|
||||
|
||||
contentItem: NText {
|
||||
text: name
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
color: highlighted ? Color.mSurface : Color.mOnSurface
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
width: listView.width * scaling
|
||||
color: highlighted ? Color.mTertiary : Color.transparent
|
||||
radius: Style.radiusS * scaling
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Color.mSurfaceVariant
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
radius: Style.radiusM * scaling
|
||||
}
|
||||
}
|
||||
|
||||
// Update the currentIndex if the currentKey is changed externally
|
||||
Connections {
|
||||
target: root
|
||||
function onCurrentKeyChanged() {
|
||||
combo.currentIndex = root.findIndexByKeyInFiltered(currentKey)
|
||||
}
|
||||
}
|
||||
|
||||
// Focus search input when popup opens
|
||||
Connections {
|
||||
target: combo.popup
|
||||
function onVisibleChanged() {
|
||||
if (combo.popup.visible) {
|
||||
// Small delay to ensure the popup is fully rendered
|
||||
Qt.callLater(function () {
|
||||
if (searchInput && searchInput.inputItem) {
|
||||
searchInput.inputItem.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
Widgets/NTextVertical.qml
Normal file
26
Widgets/NTextVertical.qml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import QtQuick
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
property string text: ""
|
||||
property real fontSize: Style.fontSizeXS
|
||||
property color color: Color.mOnSurface
|
||||
property int fontWeight: Style.fontWeightBold
|
||||
|
||||
spacing: -2 * scaling
|
||||
|
||||
Repeater {
|
||||
model: root.text.split("")
|
||||
NText {
|
||||
text: modelData
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: root.fontSize
|
||||
font.weight: root.fontWeight
|
||||
color: root.color
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ RowLayout {
|
|||
property string description: ""
|
||||
property 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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
60
flake.nix
60
flake.nix
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue