Keep keyword/priority editable when empty; add chooser menus
Once a keyword cycled to "none" its pill disappeared, leaving no target to cycle or set it again; the priority cookie had the same dead end. - The keyword pill and priority cookie now stay present when empty, rendered as a faint placeholder (+ / [#]) that brightens on hover, so a value can always be re-added. Left-click / wheel still cycle. - Right-click opens a chooser menu. The keyword menu is populated from the document's keyword set via a new DocumentModel::keywordChoices() (reads /<doc>/keywords, drops the | separator) plus a "None" entry; the priority menu offers A/B/C and None. Selecting an item is one 9P write via setKeyword/setPriority. Verified: keywordChoices returns TODO/DONE for a plain document and the full declared sequence for a #+TODO: document; re-adding a keyword after "none" and menu-selecting a specific state both work with the section id stable. GUI builds clean.
This commit is contained in:
parent
9de0c99f25
commit
741ca6d963
|
|
@ -411,27 +411,36 @@ ListView {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO keyword pill. Click cycles the keyword forward
|
||||
// (TODO -> NEXT -> ... -> DONE -> none -> TODO); right-click or
|
||||
// wheel cycles backward. Each change is one 9P write via the
|
||||
// model, matching the "document is the control surface" model.
|
||||
// TODO keyword pill. Left-click / wheel cycles the keyword
|
||||
// (TODO -> NEXT -> ... -> DONE -> none -> TODO); right-click
|
||||
// opens a menu to pick a state directly. The pill stays present
|
||||
// even when there is no keyword (shown as a faint placeholder),
|
||||
// so a keyword can always be re-added. Each change is one 9P
|
||||
// write via the model.
|
||||
Rectangle {
|
||||
visible: (del.model.keyword ?? "") !== ""
|
||||
id: kwPill
|
||||
readonly property string kw: del.model.keyword ?? ""
|
||||
readonly property bool hasKw: kw !== ""
|
||||
radius: 3
|
||||
color: kwMouse.containsMouse ? Qt.rgba(
|
||||
outline.keywordColor(del.model.keyword ?? "").r,
|
||||
outline.keywordColor(del.model.keyword ?? "").g,
|
||||
outline.keywordColor(del.model.keyword ?? "").b, 0.15)
|
||||
outline.keywordColor(kw).r,
|
||||
outline.keywordColor(kw).g,
|
||||
outline.keywordColor(kw).b, 0.15)
|
||||
: "transparent"
|
||||
border.color: outline.keywordColor(del.model.keyword ?? "")
|
||||
border.color: hasKw ? outline.keywordColor(kw)
|
||||
: theme.placeholderTextColor
|
||||
border.width: 1
|
||||
// Faint when empty unless hovered, so it stays discoverable
|
||||
// without cluttering headlines that have no keyword.
|
||||
opacity: hasKw ? 1.0 : (kwMouse.containsMouse ? 0.9 : 0.35)
|
||||
implicitWidth: kwLabel.implicitWidth + 10
|
||||
implicitHeight: kwLabel.implicitHeight + 4
|
||||
Label {
|
||||
id: kwLabel
|
||||
anchors.centerIn: parent
|
||||
text: del.model.keyword ?? ""
|
||||
color: outline.keywordColor(del.model.keyword ?? "")
|
||||
text: kwPill.hasKw ? kwPill.kw : "+"
|
||||
color: kwPill.hasKw ? outline.keywordColor(kwPill.kw)
|
||||
: theme.placeholderTextColor
|
||||
font.pointSize: outline.baseFontSize - 1
|
||||
font.bold: true
|
||||
}
|
||||
|
|
@ -443,38 +452,94 @@ ListView {
|
|||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: (mouse) => {
|
||||
docModel.cycleKeyword(del.index, mouse.button !== Qt.RightButton)
|
||||
if (mouse.button === Qt.RightButton)
|
||||
kwMenu.openFor(del.index)
|
||||
else
|
||||
docModel.cycleKeyword(del.index, true)
|
||||
}
|
||||
onWheel: (wheel) => {
|
||||
docModel.cycleKeyword(del.index, wheel.angleDelta.y > 0)
|
||||
wheel.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
// Keyword chooser: pick a state directly, or clear it.
|
||||
// Populated from the document's keyword set on open.
|
||||
Menu {
|
||||
id: kwMenu
|
||||
property int targetRow: -1
|
||||
property var choices: []
|
||||
function openFor(row) {
|
||||
targetRow = row
|
||||
choices = docModel.keywordChoices()
|
||||
popup()
|
||||
}
|
||||
Repeater {
|
||||
model: kwMenu.choices
|
||||
delegate: MenuItem {
|
||||
required property string modelData
|
||||
text: modelData
|
||||
onTriggered: docModel.setKeyword(kwMenu.targetRow, modelData)
|
||||
}
|
||||
}
|
||||
MenuSeparator {}
|
||||
MenuItem {
|
||||
text: qsTr("None")
|
||||
onTriggered: docModel.setKeyword(kwMenu.targetRow, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority cookie. Click cycles (none) -> A -> B -> C -> (none);
|
||||
// right-click / wheel-down steps back. One 9P write per change.
|
||||
Label {
|
||||
id: prioLabel
|
||||
visible: (del.model.priority ?? "") !== ""
|
||||
text: "[#" + (del.model.priority ?? "") + "]"
|
||||
color: outline.priorityColor(del.model.priority ?? "")
|
||||
font.pointSize: outline.baseFontSize
|
||||
font.bold: true
|
||||
// Priority cookie. Left-click / wheel cycles
|
||||
// (none) -> A -> B -> C -> (none); right-click opens a menu to
|
||||
// pick directly. Stays present (faint placeholder) when empty
|
||||
// so a priority can be re-added. One 9P write per change.
|
||||
Rectangle {
|
||||
id: prioPill
|
||||
readonly property string pri: del.model.priority ?? ""
|
||||
readonly property bool hasPri: pri !== ""
|
||||
radius: 3
|
||||
color: "transparent"
|
||||
opacity: hasPri ? 1.0 : (prioMouse.containsMouse ? 0.9 : 0.3)
|
||||
implicitWidth: prioLabel.implicitWidth + 4
|
||||
implicitHeight: prioLabel.implicitHeight + 2
|
||||
Label {
|
||||
id: prioLabel
|
||||
anchors.centerIn: parent
|
||||
text: prioPill.hasPri ? "[#" + prioPill.pri + "]" : "[#]"
|
||||
color: prioPill.hasPri ? outline.priorityColor(prioPill.pri)
|
||||
: theme.placeholderTextColor
|
||||
font.pointSize: outline.baseFontSize
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: prioMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: (mouse) => {
|
||||
docModel.cyclePriority(del.index, mouse.button !== Qt.RightButton)
|
||||
if (mouse.button === Qt.RightButton)
|
||||
prioMenu.popup()
|
||||
else
|
||||
docModel.cyclePriority(del.index, true)
|
||||
}
|
||||
onWheel: (wheel) => {
|
||||
docModel.cyclePriority(del.index, wheel.angleDelta.y > 0)
|
||||
wheel.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
// Priority chooser: pick A/B/C directly, or clear it.
|
||||
Menu {
|
||||
id: prioMenu
|
||||
MenuItem { text: "A"; onTriggered: docModel.setPriority(del.index, "A") }
|
||||
MenuItem { text: "B"; onTriggered: docModel.setPriority(del.index, "B") }
|
||||
MenuItem { text: "C"; onTriggered: docModel.setPriority(del.index, "C") }
|
||||
MenuSeparator {}
|
||||
MenuItem { text: qsTr("None"); onTriggered: docModel.setPriority(del.index, "") }
|
||||
}
|
||||
}
|
||||
|
||||
// Title. Rendered as StyledText so inline org links in the
|
||||
|
|
|
|||
|
|
@ -346,6 +346,24 @@ bool DocumentModel::cycleKeyword(int row, bool forward)
|
|||
return setKeyword(row, states[next]);
|
||||
}
|
||||
|
||||
QStringList DocumentModel::keywordChoices()
|
||||
{
|
||||
QStringList choices;
|
||||
if (!m_client)
|
||||
return choices;
|
||||
QString doc = m_document;
|
||||
if (doc.startsWith('/'))
|
||||
doc = doc.mid(1);
|
||||
const QString kwLine = m_client->read(QStringLiteral("/%1/keywords").arg(doc));
|
||||
for (const QString &tok : kwLine.split(QRegularExpression(QStringLiteral("\\s+")),
|
||||
Qt::SkipEmptyParts)) {
|
||||
if (tok == QLatin1String("|"))
|
||||
continue;
|
||||
choices.append(tok);
|
||||
}
|
||||
return choices;
|
||||
}
|
||||
|
||||
bool DocumentModel::setPriority(int row, const QString &priority)
|
||||
{
|
||||
if (!m_client || row < 0 || row >= m_visibleRows.size())
|
||||
|
|
|
|||
|
|
@ -93,6 +93,11 @@ public:
|
|||
// `forward` advances; otherwise steps back. Returns true on success.
|
||||
Q_INVOKABLE bool cycleKeyword(int row, bool forward);
|
||||
|
||||
// The current document's TODO keyword choices (its declared #+TODO:
|
||||
// sequence, else the parser default TODO/DONE), in order and without the
|
||||
// '|' separator. Used to populate a keyword chooser menu.
|
||||
Q_INVOKABLE QStringList keywordChoices();
|
||||
|
||||
// Set a section's priority directly (A/B/C, or empty to remove). Writes
|
||||
// /<doc>/<id>/priority and refreshes the row. Returns true on success.
|
||||
Q_INVOKABLE bool setPriority(int row, const QString &priority);
|
||||
|
|
|
|||
Loading…
Reference in New Issue