noctalia-shell/Modules/SettingsPanel/Tabs/BarTab.qml
2025-08-22 17:00:39 +02:00

765 lines
28 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services
import qs.Widgets
ColumnLayout {
id: root
spacing: 0
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
padding: Style.marginM * scaling
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ColumnLayout {
width: scrollView.availableWidth
spacing: 0
Item {
Layout.fillWidth: true
Layout.preferredHeight: 0
}
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Bar & Widgets"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NText {
text: "Bar Position"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
text: "Choose where to place the bar on the screen"
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
NComboBox {
Layout.fillWidth: true
model: ListModel {
ListElement {
key: "top"
name: "Top"
}
ListElement {
key: "bottom"
name: "Bottom"
}
}
currentKey: Settings.data.bar.position
onSelected: key => {
Settings.data.bar.position = key
}
}
}
NToggle {
label: "Show Active Window's Icon"
description: "Display the app icon next to the title of the currently focused window."
checked: Settings.data.bar.showActiveWindowIcon
onToggled: checked => {
Settings.data.bar.showActiveWindowIcon = checked
}
}
NToggle {
label: "Show Battery Percentage"
description: "Show battery percentage at all times."
checked: Settings.data.bar.alwaysShowBatteryPercentage
onToggled: checked => {
Settings.data.bar.alwaysShowBatteryPercentage = checked
}
}
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
}
RowLayout {
NSlider {
Layout.fillWidth: true
from: 0
to: 1
stepSize: 0.01
value: Settings.data.bar.backgroundOpacity
onMoved: Settings.data.bar.backgroundOpacity = value
cutoutColor: Color.mSurface
}
NText {
text: Math.floor(Settings.data.bar.backgroundOpacity * 100) + "%"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
color: Color.mOnSurface
}
}
}
// Widget Management Section
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NText {
text: "Widget Management"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
text: "Configure which widgets appear in each section of the bar. Use the arrow buttons to reorder widgets, or the add/remove buttons to manage them."
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
// Bar Sections
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: Style.marginM * scaling
// Left Section
NCard {
Layout.fillWidth: true
Layout.minimumHeight: {
var widgetCount = Settings.data.bar.widgets.left.length
if (widgetCount === 0) return 120 * scaling
var availableWidth = scrollView.availableWidth - (Style.marginM * scaling * 2) // Card margins
var avgWidgetWidth = 150 * scaling // Estimated widget width including spacing
var widgetsPerRow = Math.max(1, Math.floor(availableWidth / avgWidgetWidth))
var rows = Math.ceil(widgetCount / widgetsPerRow)
// Header (40) + spacing (16) + (rows * widget height) + (rows-1 * spacing) + bottom margin (16)
return (40 + 16 + (rows * 48) + ((rows - 1) * Style.marginS) + 16) * scaling
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginM * scaling
spacing: Style.marginM * scaling
RowLayout {
Layout.fillWidth: true
NText {
text: "Left Section"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
Item { Layout.fillWidth: true }
Rectangle {
width: 32 * scaling
height: 32 * scaling
radius: width * 0.5
color: Color.mPrimary
border.color: Color.mPrimary
border.width: 2 * scaling
NIcon {
anchors.centerIn: parent
text: "add"
color: Color.mOnPrimary
font.pointSize: Style.fontSizeM * scaling
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
addWidgetDialog.show("left")
}
}
}
}
Flow {
id: leftWidgetsFlow
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 52 * scaling
spacing: Style.marginS * scaling
flow: Flow.LeftToRight
Repeater {
model: Settings.data.bar.widgets.left
delegate: Rectangle {
width: widgetContent.implicitWidth + 16 * scaling
height: 48 * scaling
radius: Style.radiusS * scaling
color: Color.mPrimary
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
RowLayout {
id: widgetContent
anchors.centerIn: parent
spacing: Style.marginXS * scaling
NIconButton {
icon: "chevron_left"
size: 20 * scaling
colorBg: Color.applyOpacity(Color.mOnPrimary, "20")
colorFg: Color.mOnPrimary
colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40")
colorFgHover: Color.mOnPrimary
enabled: index > 0
onClicked: {
if (index > 0) {
reorderWidgetInSection("left", index, index - 1)
}
}
}
NText {
text: modelData
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnPrimary
horizontalAlignment: Text.AlignHCenter
}
NIconButton {
icon: "chevron_right"
size: 20 * scaling
colorBg: Color.applyOpacity(Color.mOnPrimary, "20")
colorFg: Color.mOnPrimary
colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40")
colorFgHover: Color.mOnPrimary
enabled: index < Settings.data.bar.widgets.left.length - 1
onClicked: {
if (index < Settings.data.bar.widgets.left.length - 1) {
reorderWidgetInSection("left", index, index + 1)
}
}
}
NIconButton {
icon: "close"
size: 20 * scaling
colorBg: Color.applyOpacity(Color.mOnPrimary, "20")
colorFg: Color.mOnPrimary
colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40")
colorFgHover: Color.mOnPrimary
onClicked: {
removeWidgetFromSection("left", index)
}
}
}
}
}
}
}
}
// Center Section
NCard {
Layout.fillWidth: true
Layout.minimumHeight: {
var widgetCount = Settings.data.bar.widgets.center.length
if (widgetCount === 0) return 120 * scaling
var availableWidth = scrollView.availableWidth - (Style.marginM * scaling * 2) // Card margins
var avgWidgetWidth = 150 * scaling // Estimated widget width including spacing
var widgetsPerRow = Math.max(1, Math.floor(availableWidth / avgWidgetWidth))
var rows = Math.ceil(widgetCount / widgetsPerRow)
// Header (40) + spacing (16) + (rows * widget height) + (rows-1 * spacing) + bottom margin (16)
return (40 + 16 + (rows * 48) + ((rows - 1) * Style.marginS) + 16) * scaling
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginM * scaling
spacing: Style.marginM * scaling
RowLayout {
Layout.fillWidth: true
NText {
text: "Center Section"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
Item { Layout.fillWidth: true }
Rectangle {
width: 32 * scaling
height: 32 * scaling
radius: width * 0.5
color: Color.mPrimary
border.color: Color.mPrimary
border.width: 2 * scaling
NIcon {
anchors.centerIn: parent
text: "add"
color: Color.mOnPrimary
font.pointSize: Style.fontSizeM * scaling
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
addWidgetDialog.show("center")
}
}
}
}
Flow {
id: centerWidgetsFlow
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 52 * scaling
spacing: Style.marginS * scaling
flow: Flow.LeftToRight
Repeater {
model: Settings.data.bar.widgets.center
delegate: Rectangle {
width: widgetContent.implicitWidth + 16 * scaling
height: 48 * scaling
radius: Style.radiusS * scaling
color: Color.mPrimary
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
RowLayout {
id: widgetContent
anchors.centerIn: parent
spacing: Style.marginXS * scaling
NIconButton {
icon: "chevron_left"
size: 20 * scaling
colorBg: Color.applyOpacity(Color.mOnPrimary, "20")
colorFg: Color.mOnPrimary
colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40")
colorFgHover: Color.mOnPrimary
enabled: index > 0
onClicked: {
if (index > 0) {
reorderWidgetInSection("center", index, index - 1)
}
}
}
NText {
text: modelData
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnPrimary
horizontalAlignment: Text.AlignHCenter
}
NIconButton {
icon: "chevron_right"
size: 20 * scaling
colorBg: Color.applyOpacity(Color.mOnPrimary, "20")
colorFg: Color.mOnPrimary
colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40")
colorFgHover: Color.mOnPrimary
enabled: index < Settings.data.bar.widgets.center.length - 1
onClicked: {
if (index < Settings.data.bar.widgets.center.length - 1) {
reorderWidgetInSection("center", index, index + 1)
}
}
}
NIconButton {
icon: "close"
size: 20 * scaling
colorBg: Color.applyOpacity(Color.mOnPrimary, "20")
colorFg: Color.mOnPrimary
colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40")
colorFgHover: Color.mOnPrimary
onClicked: {
removeWidgetFromSection("center", index)
}
}
}
}
}
}
}
}
// Right Section
NCard {
Layout.fillWidth: true
Layout.minimumHeight: {
var widgetCount = Settings.data.bar.widgets.right.length
if (widgetCount === 0) return 120 * scaling
var availableWidth = scrollView.availableWidth - (Style.marginM * scaling * 2) // Card margins
var avgWidgetWidth = 150 * scaling // Estimated widget width including spacing
var widgetsPerRow = Math.max(1, Math.floor(availableWidth / avgWidgetWidth))
var rows = Math.ceil(widgetCount / widgetsPerRow)
// Header (40) + spacing (16) + (rows * widget height) + (rows-1 * spacing) + bottom margin (16)
return (40 + 16 + (rows * 48) + ((rows - 1) * Style.marginS) + 16) * scaling
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginM * scaling
spacing: Style.marginM * scaling
RowLayout {
Layout.fillWidth: true
NText {
text: "Right Section"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
Item { Layout.fillWidth: true }
Rectangle {
width: 32 * scaling
height: 32 * scaling
radius: width * 0.5
color: Color.mPrimary
border.color: Color.mPrimary
border.width: 2 * scaling
NIcon {
anchors.centerIn: parent
text: "add"
color: Color.mOnPrimary
font.pointSize: Style.fontSizeM * scaling
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
addWidgetDialog.show("right")
}
}
}
}
Flow {
id: rightWidgetsFlow
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 52 * scaling
spacing: Style.marginS * scaling
flow: Flow.LeftToRight
Repeater {
model: Settings.data.bar.widgets.right
delegate: Rectangle {
width: widgetContent.implicitWidth + 16 * scaling
height: 48 * scaling
radius: Style.radiusS * scaling
color: Color.mPrimary
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
RowLayout {
id: widgetContent
anchors.centerIn: parent
spacing: Style.marginXS * scaling
NIconButton {
icon: "chevron_left"
size: 20 * scaling
colorBg: Color.applyOpacity(Color.mOnPrimary, "20")
colorFg: Color.mOnPrimary
colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40")
colorFgHover: Color.mOnPrimary
enabled: index > 0
onClicked: {
if (index > 0) {
reorderWidgetInSection("right", index, index - 1)
}
}
}
NText {
text: modelData
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnPrimary
horizontalAlignment: Text.AlignHCenter
}
NIconButton {
icon: "chevron_right"
size: 20 * scaling
colorBg: Color.applyOpacity(Color.mOnPrimary, "20")
colorFg: Color.mOnPrimary
colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40")
colorFgHover: Color.mOnPrimary
enabled: index < Settings.data.bar.widgets.right.length - 1
onClicked: {
if (index < Settings.data.bar.widgets.right.length - 1) {
reorderWidgetInSection("right", index, index + 1)
}
}
}
NIconButton {
icon: "close"
size: 20 * scaling
colorBg: Color.applyOpacity(Color.mOnPrimary, "20")
colorFg: Color.mOnPrimary
colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40")
colorFgHover: Color.mOnPrimary
onClicked: {
removeWidgetFromSection("right", index)
}
}
}
}
}
}
}
}
}
}
}
}
}
// Add Widget Dialog
Rectangle {
id: addWidgetDialog
anchors.fill: parent
color: Color.applyOpacity(Color.mShadow, "80")
visible: false
z: 1000
property string targetSection: ""
function show(section) {
targetSection = section
visible = true
}
function hide() {
visible = false
targetSection = ""
}
MouseArea {
anchors.fill: parent
onClicked: addWidgetDialog.hide()
}
Rectangle {
anchors.centerIn: parent
width: 400 * scaling
height: 500 * scaling
radius: Style.radiusL * scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.margins: Style.marginL * scaling
spacing: Style.marginM * scaling
NText {
text: "Add Widget to " + (addWidgetDialog.targetSection === "left" ? "Left" :
addWidgetDialog.targetSection === "center" ? "Center" : "Right") + " Section"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
spacing: Style.marginXS * scaling
model: ListModel {
ListElement { name: "SystemMonitor"; icon: "memory"; description: "System statistics" }
ListElement { name: "ActiveWindow"; icon: "web_asset"; description: "Active window title" }
ListElement { name: "MediaMini"; icon: "music_note"; description: "Media controls" }
ListElement { name: "Workspace"; icon: "dashboard"; description: "Workspace switcher" }
ListElement { name: "ScreenRecorderIndicator"; icon: "videocam"; description: "Recording indicator" }
ListElement { name: "Tray"; icon: "apps"; description: "System tray" }
ListElement { name: "NotificationHistory"; icon: "notifications"; description: "Notification history" }
ListElement { name: "WiFi"; icon: "wifi"; description: "WiFi status" }
ListElement { name: "Bluetooth"; icon: "bluetooth"; description: "Bluetooth status" }
ListElement { name: "Battery"; icon: "battery_full"; description: "Battery status" }
ListElement { name: "Volume"; icon: "volume_up"; description: "Volume control" }
ListElement { name: "Brightness"; icon: "brightness_6"; description: "Brightness control" }
ListElement { name: "Clock"; icon: "schedule"; description: "Clock" }
ListElement { name: "SidePanelToggle"; icon: "widgets"; description: "Side panel toggle" }
}
delegate: Rectangle {
width: ListView.view.width
height: 48 * scaling
radius: Style.radiusS * scaling
color: mouseArea.containsMouse ? Color.mTertiary : Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginS * scaling
NIcon {
text: model.icon
color: Color.mOnSurface
font.pointSize: Style.fontSizeM * scaling
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
NText {
text: model.name
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
text: model.description
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
addWidgetToSection(model.name, addWidgetDialog.targetSection)
addWidgetDialog.hide()
}
}
}
}
RowLayout {
Layout.fillWidth: true
Item { Layout.fillWidth: true }
NIconButton {
icon: "close"
size: 20 * scaling
color: Color.mOnSurface
onClicked: addWidgetDialog.hide()
}
}
}
}
}
// Helper functions
function addWidgetToSection(widgetName, section) {
console.log("Adding widget", widgetName, "to section", section)
var sectionArray = Settings.data.bar.widgets[section]
if (sectionArray) {
// Create a new array to avoid modifying the original
var newArray = sectionArray.slice()
newArray.push(widgetName)
console.log("Widget added. New array:", JSON.stringify(newArray))
// Assign the new array
Settings.data.bar.widgets[section] = newArray
}
}
function removeWidgetFromSection(section, index) {
console.log("Removing widget from section", section, "at index", index)
var sectionArray = Settings.data.bar.widgets[section]
if (sectionArray && index >= 0 && index < sectionArray.length) {
// Create a new array to avoid modifying the original
var newArray = sectionArray.slice()
newArray.splice(index, 1)
console.log("Widget removed. New array:", JSON.stringify(newArray))
// Assign the new array
Settings.data.bar.widgets[section] = newArray
}
}
function reorderWidgetInSection(section, fromIndex, toIndex) {
console.log("Reordering widget in section", section, "from", fromIndex, "to", toIndex)
var sectionArray = Settings.data.bar.widgets[section]
if (sectionArray && fromIndex >= 0 && fromIndex < sectionArray.length &&
toIndex >= 0 && toIndex < sectionArray.length) {
// Create a new array to avoid modifying the original
var newArray = sectionArray.slice()
var item = newArray[fromIndex]
newArray.splice(fromIndex, 1)
newArray.splice(toIndex, 0, item)
console.log("Widget reordered. New array:", JSON.stringify(newArray))
// Assign the new array
Settings.data.bar.widgets[section] = newArray
}
}
}