// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. import QtQuick import QtQuick.Controls as C import "../.." // [tagcolors] and [keys]: free-form rows, each removable, with an add row. // A row's key is its identity, so changing a tag or a shortcut is a remove // and an add. Everything edits the shared draft; the page's Save writes it. Column { id: page spacing: 6 Note {} // ponytail: copied from qtmaildir's KeyMap::knownActions() (src/keymap.cpp); // a new action there needs adding here. The action field also takes free // text, so a missing name is a typo away, not blocked. readonly property var actions: ["next_thread", "prev_thread", "open_thread", "archive", "delete", "restore", "cleanup_stranded", "cleanup_stranded_spam", "empty_trash", "purge", "empty_spam", "spam", "not_spam", "toggle_unread", "mark_all_read", "edit_tags", "tag_rules", "calendar", "flag", "compose", "reply", "reply_all", "reply_no_quote", "forward", "edit_draft", "save_message", "focus_query", "complete_query", "save_query", "select_all", "clear_pane", "clear_selection", "toggle_html", "load_remote", "message_details", "zoom_in", "zoom_out", "zoom_reset", "undo", "sync", "quit"] property string confirming: "" // The stock ComboBox paints from the palette, light by default; these // are the desktop's colours, so it reads as part of the page. component Combo: C.ComboBox { editable: true model: page.actions font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } palette { base: Theme.surface; window: Theme.base; button: Theme.surface text: Theme.text; buttonText: Theme.text; windowText: Theme.text highlight: Theme.accent; highlightedText: Theme.base mid: Qt.alpha(Theme.text, 0.2); dark: Qt.alpha(Theme.text, 0.2); light: Theme.surface } } component Box: Rectangle { property alias text: ti.text property string placeholder: "" signal accepted signal edited height: 34 radius: 8 color: Theme.surface TextInput { activeFocusOnTab: true id: ti anchors { fill: parent; leftMargin: 10; rightMargin: 10 } verticalAlignment: TextInput.AlignVCenter clip: true selectByMouse: true color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } onAccepted: parent.accepted() onTextEdited: parent.edited() Text { anchors.verticalCenter: parent.verticalCenter visible: parent.text === "" text: parent.parent.placeholder color: Theme.overlay font: parent.font } } } // --- tag colours --- Heading { text: "Tag colours" } Text { width: page.width wrapMode: Text.Wrap text: "By exact tag first, then by top-level prefix: shopping colours shopping/amazon unless it has its own." color: Theme.subtext font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } } Repeater { model: Qtm.tags delegate: Item { id: tagRow required property string modelData readonly property string colour: Qtm.get("tagcolors", modelData) ?? "" objectName: "tag:" + modelData width: page.width height: 34 Text { id: tagName anchors { left: parent.left; verticalCenter: parent.verticalCenter } width: 226 elide: Text.ElideRight text: tagRow.modelData color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } } Box { anchors { left: tagName.right; leftMargin: 8; right: tagPick.left; rightMargin: 8; verticalCenter: parent.verticalCenter } text: tagRow.colour // A half-typed colour stays out of the draft and holds Save. onEdited: { const ok = /^#[0-9a-fA-F]{6}$/.test(text); Qtm.markInvalid("tag:" + tagRow.modelData, !ok); if (ok) Qtm.set("tagcolors", tagRow.modelData, text); } Component.onDestruction: Qtm.markInvalid("tag:" + tagRow.modelData, false) } ColourPick { id: tagPick anchors { right: tagDel.left; rightMargin: 8; verticalCenter: parent.verticalCenter } colour: tagRow.colour onPicked: hex => { Qtm.markInvalid("tag:" + tagRow.modelData, false); Qtm.set("tagcolors", tagRow.modelData, hex); } } Button { id: tagDel anchors { right: parent.right; verticalCenter: parent.verticalCenter } danger: true text: page.confirming === "tag:" + tagRow.modelData ? "Confirm" : "Remove" onClicked: { if (page.confirming === "tag:" + tagRow.modelData) { page.confirming = ""; Qtm.remove("tagcolors", tagRow.modelData); } else page.confirming = "tag:" + tagRow.modelData; } } } } Item { width: page.width height: 34 readonly property bool ok: newTag.text.trim() !== "" && /^#[0-9a-fA-F]{6}$/.test(newColour.text) && Qtm.get("tagcolors", newTag.text.trim()) === undefined function add() { if (!ok) return; Qtm.set("tagcolors", newTag.text.trim(), newColour.text); newTag.text = ""; newColour.text = ""; } Box { id: newTag; anchors { left: parent.left; verticalCenter: parent.verticalCenter } width: 226; placeholder: "tag" } Box { id: newColour; anchors { left: newTag.right; leftMargin: 8; right: newPick.left; rightMargin: 8; verticalCenter: parent.verticalCenter } placeholder: "#rrggbb"; onAccepted: parent.add() } ColourPick { id: newPick anchors { right: tagAdd.left; rightMargin: 8; verticalCenter: parent.verticalCenter } colour: newColour.text onPicked: hex => newColour.text = hex } Button { id: tagAdd; anchors { right: parent.right; verticalCenter: parent.verticalCenter } text: "Add"; enabled: parent.ok; onClicked: parent.add() } } // --- keys --- Heading { text: "Shortcuts" } Text { width: page.width wrapMode: Text.Wrap text: "Overrides of qtmaildir's default bindings: a key sequence such as Ctrl+E, and the action it runs." color: Theme.subtext font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } } Repeater { model: Qtm.shortcuts delegate: Item { id: keyRow required property string modelData readonly property string action: Qtm.get("keys", modelData) ?? "" objectName: "key:" + modelData width: page.width height: 34 Text { id: keySeq anchors { left: parent.left; verticalCenter: parent.verticalCenter } width: 226 elide: Text.ElideRight text: keyRow.modelData color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } } Combo { anchors { left: keySeq.right; leftMargin: 8; right: keyDel.left; rightMargin: 8; verticalCenter: parent.verticalCenter } Component.onCompleted: editText = keyRow.action // Only a real change: rewriting an equal value would still // normalise its line and mark the page changed for nothing. onEditTextChanged: { const a = editText.trim(); if (a !== "" && a !== keyRow.action) Qtm.set("keys", keyRow.modelData, a); } } Button { id: keyDel anchors { right: parent.right; verticalCenter: parent.verticalCenter } danger: true text: page.confirming === "key:" + keyRow.modelData ? "Confirm" : "Remove" onClicked: { if (page.confirming === "key:" + keyRow.modelData) { page.confirming = ""; Qtm.remove("keys", keyRow.modelData); } else page.confirming = "key:" + keyRow.modelData; } } } } Item { width: page.width height: 34 readonly property bool ok: newSeq.text.trim() !== "" && newAction.editText.trim() !== "" && Qtm.get("keys", newSeq.text.trim()) === undefined function add() { if (!ok) return; Qtm.set("keys", newSeq.text.trim(), newAction.editText.trim()); newSeq.text = ""; } Box { id: newSeq; anchors { left: parent.left; verticalCenter: parent.verticalCenter } width: 226; placeholder: "Ctrl+E" } Combo { id: newAction anchors { left: newSeq.right; leftMargin: 8; right: keyAdd.left; rightMargin: 8; verticalCenter: parent.verticalCenter } } Button { id: keyAdd; anchors { right: parent.right; verticalCenter: parent.verticalCenter } text: "Add"; enabled: parent.ok; onClicked: parent.add() } } }