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 keyword pill. Left-click / wheel cycles the keyword
|
||||||
// (TODO -> NEXT -> ... -> DONE -> none -> TODO); right-click or
|
// (TODO -> NEXT -> ... -> DONE -> none -> TODO); right-click
|
||||||
// wheel cycles backward. Each change is one 9P write via the
|
// opens a menu to pick a state directly. The pill stays present
|
||||||
// model, matching the "document is the control surface" model.
|
// 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 {
|
Rectangle {
|
||||||
visible: (del.model.keyword ?? "") !== ""
|
id: kwPill
|
||||||
|
readonly property string kw: del.model.keyword ?? ""
|
||||||
|
readonly property bool hasKw: kw !== ""
|
||||||
radius: 3
|
radius: 3
|
||||||
color: kwMouse.containsMouse ? Qt.rgba(
|
color: kwMouse.containsMouse ? Qt.rgba(
|
||||||
outline.keywordColor(del.model.keyword ?? "").r,
|
outline.keywordColor(kw).r,
|
||||||
outline.keywordColor(del.model.keyword ?? "").g,
|
outline.keywordColor(kw).g,
|
||||||
outline.keywordColor(del.model.keyword ?? "").b, 0.15)
|
outline.keywordColor(kw).b, 0.15)
|
||||||
: "transparent"
|
: "transparent"
|
||||||
border.color: outline.keywordColor(del.model.keyword ?? "")
|
border.color: hasKw ? outline.keywordColor(kw)
|
||||||
|
: theme.placeholderTextColor
|
||||||
border.width: 1
|
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
|
implicitWidth: kwLabel.implicitWidth + 10
|
||||||
implicitHeight: kwLabel.implicitHeight + 4
|
implicitHeight: kwLabel.implicitHeight + 4
|
||||||
Label {
|
Label {
|
||||||
id: kwLabel
|
id: kwLabel
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: del.model.keyword ?? ""
|
text: kwPill.hasKw ? kwPill.kw : "+"
|
||||||
color: outline.keywordColor(del.model.keyword ?? "")
|
color: kwPill.hasKw ? outline.keywordColor(kwPill.kw)
|
||||||
|
: theme.placeholderTextColor
|
||||||
font.pointSize: outline.baseFontSize - 1
|
font.pointSize: outline.baseFontSize - 1
|
||||||
font.bold: true
|
font.bold: true
|
||||||
}
|
}
|
||||||
|
|
@ -443,38 +452,94 @@ ListView {
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
onClicked: (mouse) => {
|
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) => {
|
onWheel: (wheel) => {
|
||||||
docModel.cycleKeyword(del.index, wheel.angleDelta.y > 0)
|
docModel.cycleKeyword(del.index, wheel.angleDelta.y > 0)
|
||||||
wheel.accepted = true
|
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);
|
// Priority cookie. Left-click / wheel cycles
|
||||||
// right-click / wheel-down steps back. One 9P write per change.
|
// (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 {
|
Label {
|
||||||
id: prioLabel
|
id: prioLabel
|
||||||
visible: (del.model.priority ?? "") !== ""
|
anchors.centerIn: parent
|
||||||
text: "[#" + (del.model.priority ?? "") + "]"
|
text: prioPill.hasPri ? "[#" + prioPill.pri + "]" : "[#]"
|
||||||
color: outline.priorityColor(del.model.priority ?? "")
|
color: prioPill.hasPri ? outline.priorityColor(prioPill.pri)
|
||||||
|
: theme.placeholderTextColor
|
||||||
font.pointSize: outline.baseFontSize
|
font.pointSize: outline.baseFontSize
|
||||||
font.bold: true
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
id: prioMouse
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
onClicked: (mouse) => {
|
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) => {
|
onWheel: (wheel) => {
|
||||||
docModel.cyclePriority(del.index, wheel.angleDelta.y > 0)
|
docModel.cyclePriority(del.index, wheel.angleDelta.y > 0)
|
||||||
wheel.accepted = true
|
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
|
// 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]);
|
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)
|
bool DocumentModel::setPriority(int row, const QString &priority)
|
||||||
{
|
{
|
||||||
if (!m_client || row < 0 || row >= m_visibleRows.size())
|
if (!m_client || row < 0 || row >= m_visibleRows.size())
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,11 @@ public:
|
||||||
// `forward` advances; otherwise steps back. Returns true on success.
|
// `forward` advances; otherwise steps back. Returns true on success.
|
||||||
Q_INVOKABLE bool cycleKeyword(int row, bool forward);
|
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
|
// 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.
|
// /<doc>/<id>/priority and refreshes the row. Returns true on success.
|
||||||
Q_INVOKABLE bool setPriority(int row, const QString &priority);
|
Q_INVOKABLE bool setPriority(int row, const QString &priority);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue