CRITICAL: - Re-inject working entry into the settings form every time the dialog opens. Previously the Loader's onLoaded only fires when the sourceComponent identity changes, so reopening the dialog on the same widget id (or on a second instance of the same id, e.g. two spacers) showed stale field values. - 'Add widget' menu and the centerAnchor dropdown both read draft.layout[section] without depending on the section array identity. Mutating the array in place did not invalidate those bindings, so the menu would show a stale availability list and let non-spacer widgets be added twice. Layout mutations now replace the whole object so any binding that reads it re-evaluates. WARNINGS: - Save now writes only the diff against the merged defaults so future bar-defaults changes are not silently shadowed by a stale snapshot. - FileView onFileChanged handlers simplified to a single reload() — the explicit second loader call was duplicating loadConfig and bumping barConfigSerial twice per change (visible flicker). - userFile onLoaded ignores reloads while the user has an unsaved working draft so external edits during editing do not clobber the in-progress state. - SpinBox forms switched from onValueChanged to onValueModified so the initial-value bind no longer fires fieldChanged. - Hyprland window-rule sizes aligned with the FloatingWindow implicitWidth/Height values (720 main, 380 dialog). - omarchy-launch-bar-settings falls back to initialTitle when focusing, so a dialog that has not yet had its title applied still surfaces the existing instance.
924 lines
27 KiB
QML
924 lines
27 KiB
QML
import QtQuick
|
|
import QtQuick.Controls
|
|
import QtQuick.Layouts
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
|
|
ShellRoot {
|
|
id: root
|
|
|
|
property string home: Quickshell.env("HOME")
|
|
property string omarchyPath: Quickshell.env("OMARCHY_PATH") || (home + "/.local/share/omarchy")
|
|
readonly property string userConfigPath: home + "/.config/omarchy/bar.json"
|
|
readonly property string defaultsPath: omarchyPath + "/default/quickshell/bar/bar-defaults.json"
|
|
|
|
property color foreground: "#cacccc"
|
|
property color background: "#101315"
|
|
property color accent: "#cacccc"
|
|
property color urgent: "#a55555"
|
|
|
|
property string fontFamily: "JetBrainsMono Nerd Font"
|
|
|
|
property var defaultConfig: ({})
|
|
property var draft: ({ position: "top", centerAnchor: "calendar", layout: { left: [], center: [], right: [] }, fontFamily: "JetBrainsMono Nerd Font" })
|
|
property var registry: ({})
|
|
property bool dirty: false
|
|
property int draftRevision: 0
|
|
|
|
function cloneJson(value) {
|
|
return JSON.parse(JSON.stringify(value || null))
|
|
}
|
|
|
|
function isPlainObject(value) {
|
|
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
}
|
|
|
|
function mergeConfig(base, override) {
|
|
var result = cloneJson(base || {})
|
|
if (!isPlainObject(override)) return result
|
|
for (var key in override) {
|
|
if (isPlainObject(result[key]) && isPlainObject(override[key]))
|
|
result[key] = mergeConfig(result[key], override[key])
|
|
else
|
|
result[key] = cloneJson(override[key])
|
|
}
|
|
return result
|
|
}
|
|
|
|
function normalizeLayoutEntry(entry) {
|
|
if (typeof entry === "string") return { id: entry }
|
|
if (isPlainObject(entry) && entry.id) return cloneJson(entry)
|
|
return null
|
|
}
|
|
|
|
function normalizeLayout(layout) {
|
|
var sections = ["left", "center", "right"]
|
|
var result = {}
|
|
for (var i = 0; i < sections.length; i++) {
|
|
var s = sections[i]
|
|
var arr = []
|
|
var src = (layout && layout[s]) || []
|
|
for (var j = 0; j < src.length; j++) {
|
|
var entry = normalizeLayoutEntry(src[j])
|
|
if (entry) arr.push(entry)
|
|
}
|
|
result[s] = arr
|
|
}
|
|
return result
|
|
}
|
|
|
|
function loadConfig() {
|
|
try {
|
|
var defaults = defaultsFile.text() ? JSON.parse(defaultsFile.text()) : {}
|
|
defaultConfig = defaults
|
|
} catch (e) {
|
|
console.warn("Bad defaults JSON:", e)
|
|
defaultConfig = {}
|
|
}
|
|
|
|
var userText = userFile.text() || "{}"
|
|
var user = {}
|
|
try { user = JSON.parse(userText) } catch (e) { user = {} }
|
|
|
|
var merged = mergeConfig(defaultConfig, user)
|
|
draft = {
|
|
position: String(merged.position || "top"),
|
|
centerAnchor: String(merged.centerAnchor || ""),
|
|
fontFamily: String(merged.fontFamily || "JetBrainsMono Nerd Font"),
|
|
layout: normalizeLayout(merged.layout || {})
|
|
}
|
|
dirty = false
|
|
draftRevision++
|
|
}
|
|
|
|
function deepEqual(a, b) {
|
|
return JSON.stringify(a) === JSON.stringify(b)
|
|
}
|
|
|
|
function diffAgainstDefaults() {
|
|
var defaults = mergeConfig(
|
|
{ position: "top", centerAnchor: "", fontFamily: "JetBrainsMono Nerd Font", layout: { left: [], center: [], right: [] } },
|
|
defaultConfig
|
|
)
|
|
var override = {}
|
|
if (!deepEqual(draft.position, defaults.position)) override.position = draft.position
|
|
if (!deepEqual(draft.centerAnchor, defaults.centerAnchor)) override.centerAnchor = draft.centerAnchor
|
|
if (!deepEqual(draft.fontFamily, defaults.fontFamily)) override.fontFamily = draft.fontFamily
|
|
|
|
var defaultLayout = normalizeLayout(defaults.layout || {})
|
|
var layoutDiff = {}
|
|
var hasLayoutDiff = false
|
|
var sections = ["left", "center", "right"]
|
|
for (var i = 0; i < sections.length; i++) {
|
|
var s = sections[i]
|
|
if (!deepEqual(draft.layout[s], defaultLayout[s])) {
|
|
layoutDiff[s] = draft.layout[s]
|
|
hasLayoutDiff = true
|
|
}
|
|
}
|
|
if (hasLayoutDiff) override.layout = layoutDiff
|
|
return override
|
|
}
|
|
|
|
function saveConfig() {
|
|
var override = diffAgainstDefaults()
|
|
userFile.setText(JSON.stringify(override, null, 2) + "\n")
|
|
dirty = false
|
|
}
|
|
|
|
function resetToDefaults() {
|
|
userFile.setText("{}\n")
|
|
loadConfig()
|
|
}
|
|
|
|
function markDirty() {
|
|
dirty = true
|
|
draftRevision++
|
|
}
|
|
|
|
// Replace the whole `layout` object so any binding that reads `draft.layout`
|
|
// is invalidated. Mutating `draft.layout[section]` alone does not notify QML.
|
|
function mutateLayout(section, mutator) {
|
|
var nextLayout = {
|
|
left: draft.layout.left.slice(),
|
|
center: draft.layout.center.slice(),
|
|
right: draft.layout.right.slice()
|
|
}
|
|
mutator(nextLayout[section])
|
|
var nextDraft = {
|
|
position: draft.position,
|
|
centerAnchor: draft.centerAnchor,
|
|
fontFamily: draft.fontFamily,
|
|
layout: nextLayout
|
|
}
|
|
draft = nextDraft
|
|
markDirty()
|
|
}
|
|
|
|
function moveEntry(section, fromIndex, toIndex) {
|
|
if (toIndex < 0 || toIndex >= draft.layout[section].length) return
|
|
mutateLayout(section, function(arr) {
|
|
var item = arr[fromIndex]
|
|
arr.splice(fromIndex, 1)
|
|
arr.splice(toIndex, 0, item)
|
|
})
|
|
}
|
|
|
|
function removeEntry(section, index) {
|
|
mutateLayout(section, function(arr) { arr.splice(index, 1) })
|
|
}
|
|
|
|
function addEntry(section, id) {
|
|
mutateLayout(section, function(arr) { arr.push({ id: id }) })
|
|
}
|
|
|
|
function updateEntry(section, index, newEntry) {
|
|
mutateLayout(section, function(arr) { arr[index] = cloneJson(newEntry) })
|
|
}
|
|
|
|
function loadTheme(raw) {
|
|
var lines = String(raw || "").split("\n")
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var match = lines[i].match(/^\s*([A-Za-z0-9_-]+)\s*=\s*["']?(#[0-9A-Fa-f]{6})/)
|
|
if (!match) continue
|
|
if (match[1] === "foreground") foreground = match[2]
|
|
else if (match[1] === "background") background = match[2]
|
|
else if (match[1] === "color4" || match[1] === "accent") accent = match[2]
|
|
else if (match[1] === "red") urgent = match[2]
|
|
}
|
|
}
|
|
|
|
// Catalog of available widgets — id → display metadata + settings schema path.
|
|
// settingsForm is an inline-defined Component name that opens when editing.
|
|
readonly property var catalog: ({
|
|
"omarchy": { name: "Omarchy menu", description: "Launches the Omarchy menu" },
|
|
"workspaces": { name: "Workspaces (legacy)", description: "Workspace numbers, no animation" },
|
|
"workspacesPro": { name: "Workspaces", description: "Animated workspace switcher" },
|
|
"activeWindow": { name: "Active window", description: "Title of the focused window" },
|
|
"clock": { name: "Clock", description: "Date / time text" },
|
|
"calendar": { name: "Calendar", description: "Clock with month-grid popup", settingsForm: "calendarSettings" },
|
|
"media": { name: "Media", description: "MPRIS now-playing with controls" },
|
|
"weather": { name: "Weather (legacy)", description: "Tiny weather pill" },
|
|
"weatherFlyout": { name: "Weather", description: "Weather pill with detail popup" },
|
|
"update": { name: "Updates", description: "Indicates available system updates" },
|
|
"voxtype": { name: "Voxtype", description: "Voxtype dictation state" },
|
|
"screenRecording": { name: "Screen recording", description: "Active recording indicator" },
|
|
"idle": { name: "Idle (legacy)", description: "Inhibitor indicator" },
|
|
"idleInhibitor": { name: "Keep awake", description: "Idle inhibitor toggle" },
|
|
"notifications": { name: "DND (mako)", description: "Notification silencing indicator" },
|
|
"notificationCenter": { name: "Notification center", description: "Recent notifications + DND (replaces mako)" },
|
|
"tray": { name: "System tray", description: "Status notifier items" },
|
|
"bluetooth": { name: "Bluetooth (legacy)", description: "Bluetooth status icon" },
|
|
"bluetoothPanel": { name: "Bluetooth", description: "Bluetooth devices popup" },
|
|
"network": { name: "Network (legacy)", description: "Wi-Fi/ethernet status" },
|
|
"networkPanel": { name: "Network", description: "Wi-Fi list and connect" },
|
|
"audio": { name: "Volume (legacy)", description: "Speaker icon, scroll for volume" },
|
|
"audioPanel": { name: "Volume", description: "Volume slider, output picker, mixer" },
|
|
"microphone": { name: "Microphone", description: "Mic input state" },
|
|
"nightLight": { name: "Night light", description: "hyprsunset toggle" },
|
|
"brightness": { name: "Brightness", description: "Screen brightness slider", settingsForm: "brightnessSettings" },
|
|
"powerProfile": { name: "Power profile", description: "power-profiles-daemon selector" },
|
|
"battery": { name: "Battery", description: "Battery percent and ETA" },
|
|
"cpu": { name: "CPU (legacy)", description: "btop launcher" },
|
|
"systemStats": { name: "System stats", description: "Inline CPU + RAM graphs" },
|
|
"controlCenter": { name: "Quick settings", description: "Volume/brightness/DND/etc in one popup" },
|
|
"powerMenu": { name: "Power menu", description: "Lock/suspend/reboot/shutdown" },
|
|
"keyboardLayout": { name: "Keyboard layout", description: "Current xkb layout, click cycles" },
|
|
"lockKeys": { name: "Lock keys", description: "Caps/Num/Scroll lock indicators" },
|
|
"spacer": { name: "Spacer", description: "Configurable blank space", settingsForm: "spacerSettings" }
|
|
})
|
|
|
|
function widgetName(id) {
|
|
return catalog[id] ? catalog[id].name : id
|
|
}
|
|
|
|
function widgetDescription(id) {
|
|
return catalog[id] ? (catalog[id].description || "") : ""
|
|
}
|
|
|
|
function widgetHasSettings(id) {
|
|
return !!(catalog[id] && catalog[id].settingsForm)
|
|
}
|
|
|
|
function availableToAdd(section) {
|
|
var existingByOther = {}
|
|
var sections = ["left", "center", "right"]
|
|
for (var s = 0; s < sections.length; s++) {
|
|
if (sections[s] === section) continue
|
|
var list = draft.layout[sections[s]] || []
|
|
for (var i = 0; i < list.length; i++) existingByOther[list[i].id] = true
|
|
}
|
|
var existingHere = {}
|
|
var here = draft.layout[section] || []
|
|
for (var j = 0; j < here.length; j++) existingHere[here[j].id] = true
|
|
|
|
var ids = Object.keys(catalog).sort(function(a, b) {
|
|
return widgetName(a).localeCompare(widgetName(b))
|
|
})
|
|
|
|
var result = []
|
|
for (var k = 0; k < ids.length; k++) {
|
|
var id = ids[k]
|
|
// Allow multiple instances of `spacer` only.
|
|
var allowMultiple = id === "spacer"
|
|
if (!allowMultiple && existingHere[id]) continue
|
|
result.push({ id: id, name: widgetName(id), description: widgetDescription(id), elsewhere: !!existingByOther[id] })
|
|
}
|
|
return result
|
|
}
|
|
|
|
FileView {
|
|
id: defaultsFile
|
|
path: root.defaultsPath
|
|
watchChanges: true
|
|
printErrors: false
|
|
onLoaded: root.loadConfig()
|
|
onFileChanged: reload()
|
|
}
|
|
|
|
FileView {
|
|
id: userFile
|
|
path: root.userConfigPath
|
|
watchChanges: true
|
|
atomicWrites: true
|
|
printErrors: false
|
|
onLoaded: {
|
|
if (root.dirty) return
|
|
root.loadConfig()
|
|
}
|
|
onFileChanged: reload()
|
|
}
|
|
|
|
FileView {
|
|
path: root.home + "/.config/omarchy/current/theme/colors.toml"
|
|
watchChanges: true
|
|
printErrors: false
|
|
onLoaded: root.loadTheme(text())
|
|
onFileChanged: reload()
|
|
}
|
|
|
|
FloatingWindow {
|
|
id: window
|
|
title: "Omarchy bar settings"
|
|
color: root.background
|
|
implicitWidth: 720
|
|
implicitHeight: 720
|
|
minimumSize: Qt.size(560, 500)
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: root.background
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: 20
|
|
spacing: 16
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
implicitHeight: 32
|
|
|
|
Text {
|
|
text: "Bar settings"
|
|
color: root.foreground
|
|
font.family: root.fontFamily
|
|
font.pixelSize: 20
|
|
font.bold: true
|
|
anchors.left: parent.left
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
Row {
|
|
spacing: 8
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
ActionPill {
|
|
text: "Reset to defaults"
|
|
foreground: root.urgent
|
|
onClicked: root.resetToDefaults()
|
|
}
|
|
|
|
ActionPill {
|
|
text: root.dirty ? "Save" : "Saved"
|
|
foreground: root.dirty ? root.accent : Qt.darker(root.foreground, 1.5)
|
|
bordered: root.dirty
|
|
onClicked: if (root.dirty) root.saveConfig()
|
|
}
|
|
}
|
|
}
|
|
|
|
Row {
|
|
Layout.fillWidth: true
|
|
spacing: 10
|
|
|
|
OptionDropdown {
|
|
label: "Position"
|
|
value: root.draft.position
|
|
options: ["top", "right", "bottom", "left"]
|
|
onChanged: function(v) {
|
|
root.draft.position = v
|
|
root.markDirty()
|
|
}
|
|
}
|
|
|
|
OptionDropdown {
|
|
label: "Center anchor"
|
|
value: root.draft.centerAnchor
|
|
options: {
|
|
var list = ["(none)"]
|
|
var entries = root.draft.layout.center || []
|
|
for (var i = 0; i < entries.length; i++) list.push(entries[i].id)
|
|
return list
|
|
}
|
|
onChanged: function(v) {
|
|
root.draft.centerAnchor = v === "(none)" ? "" : v
|
|
root.markDirty()
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
height: 1
|
|
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.12)
|
|
}
|
|
|
|
Flickable {
|
|
id: bodyScroll
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
clip: true
|
|
contentWidth: width
|
|
contentHeight: bodyColumn.implicitHeight
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
flickableDirection: Flickable.VerticalFlick
|
|
|
|
ColumnLayout {
|
|
id: bodyColumn
|
|
width: bodyScroll.width
|
|
spacing: 14
|
|
|
|
SectionEditor { sectionKey: "left"; sectionLabel: "Left" }
|
|
SectionEditor { sectionKey: "center"; sectionLabel: "Center" }
|
|
SectionEditor { sectionKey: "right"; sectionLabel: "Right" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------- Components ---------------------------------------------------
|
|
|
|
component ActionPill: Rectangle {
|
|
id: pill
|
|
property string text: ""
|
|
property color foreground: root.foreground
|
|
property bool bordered: true
|
|
signal clicked()
|
|
|
|
implicitWidth: pillLabel.implicitWidth + 22
|
|
implicitHeight: 26
|
|
radius: 4
|
|
color: pillArea.containsMouse ? Qt.rgba(pill.foreground.r, pill.foreground.g, pill.foreground.b, 0.15) : "transparent"
|
|
border.color: pill.bordered ? pill.foreground : "transparent"
|
|
border.width: 1
|
|
|
|
Behavior on color { ColorAnimation { duration: 100 } }
|
|
|
|
Text {
|
|
id: pillLabel
|
|
anchors.centerIn: parent
|
|
text: pill.text
|
|
color: pill.foreground
|
|
font.family: root.fontFamily
|
|
font.pixelSize: 11
|
|
}
|
|
|
|
MouseArea {
|
|
id: pillArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: pill.clicked()
|
|
}
|
|
}
|
|
|
|
component OptionDropdown: Item {
|
|
id: dropdown
|
|
property string label: ""
|
|
property string value: ""
|
|
property var options: []
|
|
signal changed(string value)
|
|
|
|
implicitWidth: 240
|
|
implicitHeight: 48
|
|
|
|
Column {
|
|
anchors.fill: parent
|
|
spacing: 4
|
|
|
|
Text {
|
|
text: dropdown.label
|
|
color: Qt.darker(root.foreground, 1.4)
|
|
font.family: root.fontFamily
|
|
font.pixelSize: 10
|
|
font.bold: true
|
|
}
|
|
|
|
ComboBox {
|
|
id: combo
|
|
width: parent.width
|
|
height: 28
|
|
font.family: root.fontFamily
|
|
font.pixelSize: 11
|
|
model: dropdown.options
|
|
currentIndex: {
|
|
for (var i = 0; i < model.length; i++) if (model[i] === dropdown.value) return i
|
|
return 0
|
|
}
|
|
|
|
onActivated: function(index) {
|
|
dropdown.changed(model[index])
|
|
}
|
|
|
|
background: Rectangle {
|
|
color: root.background
|
|
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.4)
|
|
border.width: 1
|
|
radius: 4
|
|
}
|
|
|
|
contentItem: Text {
|
|
leftPadding: 8
|
|
rightPadding: 24
|
|
text: combo.displayText
|
|
color: root.foreground
|
|
font: combo.font
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
component SectionEditor: Column {
|
|
id: section
|
|
|
|
property string sectionKey: ""
|
|
property string sectionLabel: ""
|
|
property var entries: (root.draft.layout && root.draft.layout[section.sectionKey]) || []
|
|
Layout.fillWidth: true
|
|
spacing: 8
|
|
|
|
Connections {
|
|
target: root
|
|
function onDraftRevisionChanged() { section.entries = (root.draft.layout && root.draft.layout[section.sectionKey]) || [] }
|
|
}
|
|
|
|
Row {
|
|
width: section.width
|
|
spacing: 8
|
|
|
|
Text {
|
|
text: section.sectionLabel
|
|
color: root.foreground
|
|
font.family: root.fontFamily
|
|
font.pixelSize: 14
|
|
font.bold: true
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
Text {
|
|
text: "· " + section.entries.length + (section.entries.length === 1 ? " widget" : " widgets")
|
|
color: Qt.darker(root.foreground, 1.5)
|
|
font.family: root.fontFamily
|
|
font.pixelSize: 11
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
Item { width: section.width - 200 - parent.children[0].implicitWidth - parent.children[1].implicitWidth; height: 1 }
|
|
|
|
ActionPill {
|
|
text: "+ Add widget"
|
|
onClicked: addMenu.popup()
|
|
}
|
|
|
|
Menu {
|
|
id: addMenu
|
|
Repeater {
|
|
model: root.availableToAdd(section.sectionKey)
|
|
delegate: MenuItem {
|
|
required property var modelData
|
|
text: modelData.name + (modelData.elsewhere ? " (elsewhere)" : "")
|
|
onTriggered: root.addEntry(section.sectionKey, modelData.id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
Layout.fillWidth: true
|
|
width: section.width
|
|
spacing: 4
|
|
|
|
Repeater {
|
|
model: section.entries
|
|
delegate: WidgetCard {
|
|
required property var modelData
|
|
required property int index
|
|
width: section.width
|
|
sectionKey: section.sectionKey
|
|
entryIndex: index
|
|
entry: modelData
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
visible: section.entries.length === 0
|
|
width: parent.width
|
|
height: 32
|
|
radius: 4
|
|
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
|
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.12)
|
|
border.width: 1
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: "Empty — add a widget"
|
|
color: Qt.darker(root.foreground, 1.5)
|
|
font.family: root.fontFamily
|
|
font.pixelSize: 11
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
component WidgetCard: Rectangle {
|
|
id: card
|
|
property string sectionKey: ""
|
|
property int entryIndex: -1
|
|
property var entry: ({})
|
|
readonly property string entryId: entry && entry.id ? String(entry.id) : ""
|
|
readonly property string displayName: root.widgetName(entryId)
|
|
readonly property string description: root.widgetDescription(entryId)
|
|
readonly property bool hasSettings: root.widgetHasSettings(entryId)
|
|
|
|
implicitHeight: 50
|
|
radius: 4
|
|
color: cardArea.containsMouse ? Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.08) : Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.03)
|
|
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.12)
|
|
border.width: 1
|
|
|
|
Behavior on color { ColorAnimation { duration: 100 } }
|
|
|
|
Row {
|
|
id: actionRow
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 8
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 4
|
|
|
|
IconButton {
|
|
glyph: "↑"
|
|
tooltip: "Move up"
|
|
onClicked: root.moveEntry(card.sectionKey, card.entryIndex, card.entryIndex - 1)
|
|
}
|
|
IconButton {
|
|
glyph: "↓"
|
|
tooltip: "Move down"
|
|
onClicked: root.moveEntry(card.sectionKey, card.entryIndex, card.entryIndex + 1)
|
|
}
|
|
IconButton {
|
|
glyph: "⚙"
|
|
tooltip: "Settings"
|
|
visible: card.hasSettings
|
|
onClicked: settingsLoader.open(card.entry)
|
|
}
|
|
IconButton {
|
|
glyph: "✕"
|
|
tooltip: "Remove"
|
|
foreground: root.urgent
|
|
onClicked: root.removeEntry(card.sectionKey, card.entryIndex)
|
|
}
|
|
}
|
|
|
|
Column {
|
|
anchors.left: parent.left
|
|
anchors.right: actionRow.left
|
|
anchors.leftMargin: 12
|
|
anchors.rightMargin: 12
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 2
|
|
|
|
Text {
|
|
text: card.displayName
|
|
color: root.foreground
|
|
font.family: root.fontFamily
|
|
font.pixelSize: 12
|
|
font.bold: true
|
|
elide: Text.ElideRight
|
|
width: parent.width
|
|
}
|
|
Text {
|
|
visible: text !== ""
|
|
text: card.description
|
|
color: Qt.darker(root.foreground, 1.5)
|
|
font.family: root.fontFamily
|
|
font.pixelSize: 10
|
|
elide: Text.ElideRight
|
|
width: parent.width
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: cardArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
acceptedButtons: Qt.NoButton
|
|
}
|
|
|
|
SettingsDialog {
|
|
id: settingsLoader
|
|
anchorWindow: window
|
|
sectionKey: card.sectionKey
|
|
entryIndex: card.entryIndex
|
|
}
|
|
}
|
|
|
|
component IconButton: Rectangle {
|
|
id: iconButton
|
|
property string glyph: ""
|
|
property string tooltip: ""
|
|
property color foreground: root.foreground
|
|
signal clicked()
|
|
|
|
implicitWidth: 26
|
|
implicitHeight: 26
|
|
radius: 3
|
|
color: iconArea.containsMouse ? Qt.rgba(iconButton.foreground.r, iconButton.foreground.g, iconButton.foreground.b, 0.18) : "transparent"
|
|
|
|
Behavior on color { ColorAnimation { duration: 100 } }
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: iconButton.glyph
|
|
color: iconButton.foreground
|
|
font.family: root.fontFamily
|
|
font.pixelSize: 13
|
|
}
|
|
|
|
MouseArea {
|
|
id: iconArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: iconButton.clicked()
|
|
}
|
|
}
|
|
|
|
component SettingsDialog: Item {
|
|
id: dialog
|
|
property var anchorWindow: null
|
|
property string sectionKey: ""
|
|
property int entryIndex: -1
|
|
property var workingEntry: ({})
|
|
|
|
function open(entry) {
|
|
workingEntry = root.cloneJson(entry)
|
|
win.visible = true
|
|
}
|
|
|
|
function commit() {
|
|
root.updateEntry(sectionKey, entryIndex, workingEntry)
|
|
win.visible = false
|
|
}
|
|
|
|
function discard() {
|
|
win.visible = false
|
|
}
|
|
|
|
function fieldChanged(key, value) {
|
|
var copy = root.cloneJson(workingEntry)
|
|
copy[key] = value
|
|
workingEntry = copy
|
|
}
|
|
|
|
FloatingWindow {
|
|
id: win
|
|
title: "Widget settings — " + root.widgetName(dialog.workingEntry.id || "")
|
|
color: root.background
|
|
implicitWidth: 380
|
|
implicitHeight: 320
|
|
visible: false
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: root.background
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: 18
|
|
spacing: 12
|
|
|
|
Text {
|
|
text: root.widgetName(dialog.workingEntry.id || "")
|
|
color: root.foreground
|
|
font.family: root.fontFamily
|
|
font.pixelSize: 14
|
|
font.bold: true
|
|
}
|
|
|
|
Text {
|
|
text: root.widgetDescription(dialog.workingEntry.id || "")
|
|
color: Qt.darker(root.foreground, 1.4)
|
|
font.family: root.fontFamily
|
|
font.pixelSize: 11
|
|
wrapMode: Text.WordWrap
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
Loader {
|
|
id: formLoader
|
|
Layout.fillWidth: true
|
|
sourceComponent: formComponent(dialog.workingEntry.id || "")
|
|
onLoaded: {
|
|
if (item && "entry" in item) item.entry = dialog.workingEntry
|
|
if (item && "fieldChanged" in item) {
|
|
item.fieldChanged.connect(function(key, value) { dialog.fieldChanged(key, value) })
|
|
}
|
|
}
|
|
}
|
|
|
|
Item { Layout.fillHeight: true }
|
|
|
|
Row {
|
|
Layout.alignment: Qt.AlignRight
|
|
spacing: 8
|
|
ActionPill { text: "Cancel"; bordered: false; onClicked: dialog.discard() }
|
|
ActionPill { text: "Apply"; onClicked: dialog.commit() }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function formComponent(id) {
|
|
var cat = catalog[id]
|
|
if (!cat || !cat.settingsForm) return null
|
|
switch (cat.settingsForm) {
|
|
case "spacerSettings": return spacerSettingsComponent
|
|
case "calendarSettings": return calendarSettingsComponent
|
|
case "brightnessSettings": return brightnessSettingsComponent
|
|
default: return null
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: spacerSettingsComponent
|
|
|
|
Column {
|
|
id: spacerForm
|
|
signal fieldChanged(string key, var value)
|
|
property var entry: ({})
|
|
|
|
spacing: 8
|
|
width: parent ? parent.width : 0
|
|
|
|
Text {
|
|
text: "Size (pixels)"
|
|
color: Qt.darker(root.foreground, 1.4)
|
|
font.family: root.fontFamily
|
|
font.pixelSize: 11
|
|
}
|
|
|
|
SpinBox {
|
|
from: 0
|
|
to: 256
|
|
value: spacerForm.entry.size !== undefined ? spacerForm.entry.size : 12
|
|
onValueModified: spacerForm.fieldChanged("size", value)
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: calendarSettingsComponent
|
|
|
|
Column {
|
|
id: calForm
|
|
signal fieldChanged(string key, var value)
|
|
property var entry: ({})
|
|
|
|
spacing: 8
|
|
width: parent ? parent.width : 0
|
|
|
|
Text {
|
|
text: "Horizontal format"
|
|
color: Qt.darker(root.foreground, 1.4)
|
|
font.family: root.fontFamily
|
|
font.pixelSize: 11
|
|
}
|
|
TextField {
|
|
text: calForm.entry.format || "dddd HH:mm"
|
|
font.family: root.fontFamily
|
|
font.pixelSize: 12
|
|
width: parent.width
|
|
onEditingFinished: calForm.fieldChanged("format", text)
|
|
}
|
|
|
|
Text {
|
|
text: "Alternate format (click to swap)"
|
|
color: Qt.darker(root.foreground, 1.4)
|
|
font.family: root.fontFamily
|
|
font.pixelSize: 11
|
|
}
|
|
TextField {
|
|
text: calForm.entry.formatAlt || "dd MMMM 'W'ww yyyy"
|
|
font.family: root.fontFamily
|
|
font.pixelSize: 12
|
|
width: parent.width
|
|
onEditingFinished: calForm.fieldChanged("formatAlt", text)
|
|
}
|
|
|
|
Text {
|
|
text: "Vertical format (left/right bars)"
|
|
color: Qt.darker(root.foreground, 1.4)
|
|
font.family: root.fontFamily
|
|
font.pixelSize: 11
|
|
}
|
|
TextField {
|
|
text: calForm.entry.verticalFormat || "HH\n—\nmm"
|
|
font.family: root.fontFamily
|
|
font.pixelSize: 12
|
|
width: parent.width
|
|
onEditingFinished: calForm.fieldChanged("verticalFormat", text)
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: brightnessSettingsComponent
|
|
|
|
Column {
|
|
id: brightForm
|
|
signal fieldChanged(string key, var value)
|
|
property var entry: ({})
|
|
|
|
spacing: 8
|
|
width: parent ? parent.width : 0
|
|
|
|
Text {
|
|
text: "Scroll step (% per notch)"
|
|
color: Qt.darker(root.foreground, 1.4)
|
|
font.family: root.fontFamily
|
|
font.pixelSize: 11
|
|
}
|
|
SpinBox {
|
|
from: 1
|
|
to: 25
|
|
value: brightForm.entry.step !== undefined ? brightForm.entry.step : 5
|
|
onValueModified: brightForm.fieldChanged("step", value)
|
|
}
|
|
}
|
|
}
|
|
}
|