orkmode/ui/src/documentmodel.h

226 lines
8.9 KiB
C++

// DocumentModel — a flat list model over one document's sections.
//
// Maps the ork-server namespace to a flat list where each section is a row.
// Tree structure is represented by the level field (1 = top-level, 2 = child, etc).
// Children appear immediately after their parent in the list.
//
// Expansion state is tracked per-section. Collapsed sections hide their children.
//
// This is a read-only view for v0.3; editing will add setData.
#pragma once
#include <QAbstractListModel>
#include <QHash>
#include <QString>
#include <QVector>
#include <QSet>
#include <functional>
class OrkClient;
class DocumentModel : public QAbstractListModel {
Q_OBJECT
Q_PROPERTY(QString document READ document WRITE setDocument NOTIFY documentChanged)
public:
// Section fields exposed to QML
enum Roles {
TitleRole = Qt::UserRole + 1,
KeywordRole,
PriorityRole,
TagsRole,
LevelRole,
ScheduledRole,
DeadlineRole,
SectionIdRole,
PathRole,
IsTodoRole,
IsDoneRole,
BodyRole,
PropertiesRole,
HasChildrenRole,
ExpandedRole,
};
Q_ENUM(Roles)
explicit DocumentModel(QObject *parent = nullptr);
void setClient(OrkClient *client);
QString document() const { return m_document; }
void setDocument(const QString &doc);
// QAbstractListModel
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
QHash<int, QByteArray> roleNames() const override;
// Toggle expansion for a section by row index
Q_INVOKABLE void toggleExpanded(int row);
// Reload from the server
Q_INVOKABLE void reload();
// Execute the Nth source block (0-based, counting only src blocks) in the
// section at the given visible row. Runs `/ctl execute <doc> <id> <n>`,
// refreshes that section's body so the inserted #+RESULTS: becomes visible,
// and returns the server's execution report.
Q_INVOKABLE QString executeBlock(int row, int blockIndex);
// Whether a source block language can be executed by the server. Backed by
// `/ctl languages` (loaded once and cached). Case-insensitive.
Q_INVOKABLE bool isLanguageSupported(const QString &lang);
// Toggle the Nth checkbox (0-based, in document order, counting only list
// items that carry a `[ ]`/`[X]`/`[-]` cookie) in the section at the given
// visible row. Flips [ ]/[-] -> [X] and [X] -> [ ], writes the section body
// back via /body, and refreshes the row. Returns true on success.
Q_INVOKABLE bool toggleCheckbox(int row, int checkboxOrdinal);
// Cycle the Nth checkbox through unchecked -> checked -> partial and back.
// `forward` steps unchecked->checked->partial; otherwise the reverse. Writes
// the section body back via /body and refreshes the row. Returns success.
Q_INVOKABLE bool cycleCheckbox(int row, int checkboxOrdinal, bool forward);
// Set a section's TODO keyword directly (empty string removes it). Writes
// /<doc>/<id>/keyword and refreshes the row. Returns true on success.
Q_INVOKABLE bool setKeyword(int row, const QString &keyword);
// Cycle a section's TODO keyword through the configured sequence (from
// /keywords) plus a trailing empty state, e.g.
// (none) -> TODO -> NEXT -> WAITING -> DONE -> CANCELLED -> (none).
// `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);
// Cycle a section's priority through (none) -> A -> B -> C -> (none).
// `forward` advances; otherwise steps back. Returns true on success.
Q_INVOKABLE bool cyclePriority(int row, bool forward);
// Set a section's tags from a free-form string. Tags may be separated by
// spaces, commas, or colons; the server normalizes them. An empty string
// removes all tags. Writes /<doc>/<id>/tags and refreshes the row.
Q_INVOKABLE bool setTags(int row, const QString &tags);
// Set a section's SCHEDULED timestamp. `timestamp` is the inner form
// (e.g. "2025-01-15" or "2025-01-15 10:30 +1w"); the server wraps it in
// <...>. An empty string removes the SCHEDULED field. Writes
// /<doc>/<id>/scheduled and refreshes the row. Returns true on success.
Q_INVOKABLE bool setScheduled(int row, const QString &timestamp);
// Set a section's DEADLINE timestamp (see setScheduled). Writes
// /<doc>/<id>/deadline and refreshes the row.
Q_INVOKABLE bool setDeadline(int row, const QString &timestamp);
// Replace a section's body text (the content after the headline, planning
// line, and property drawer). The server reparses on write. Writes
// /<doc>/<id>/body and refreshes the row. Returns true on success.
Q_INVOKABLE bool setBody(int row, const QString &body);
// The current raw body text of a section (as returned by /<doc>/<id>/body),
// for seeding an inline editor. Empty if the row or section is unknown.
Q_INVOKABLE QString bodyText(int row);
// Reveal a section by id: expand all its ancestors so it becomes visible,
// and return its visible row index (-1 if the id is unknown). Emits
// sectionRevealed(row, bodyOffset) so the view can scroll to and highlight
// it; bodyOffset is a byte offset into the section's rendered body (0 when
// the target is the section itself).
Q_INVOKABLE int revealSection(const QString &sectionId, int bodyOffset = 0);
// Resolve a link target via /<doc>/resolve, then act on it: external
// targets open in the OS handler; internal targets are revealed in the
// outline. Resolution spans the whole org path — file:/id:/fuzzy links may
// resolve into another document, in which case the model switches to the
// owning document before revealing. Returns the resolved kind
// ("dedicated"/"fuzzy"/"external"/"notfound"), or "error" if not connected.
Q_INVOKABLE QString followLink(const QString &target);
signals:
void documentChanged();
// Emitted when a section has been revealed (ancestors expanded); `row` is
// its visible index and `bodyOffset` is a byte offset into the section's
// rendered body (0 when the target is the section itself). The view
// scrolls to and highlights it.
void sectionRevealed(int row, int bodyOffset);
private:
struct Section {
QString id; // section UUID/custom-id
QString path; // "/doc/id"
int level = 1; // nesting level (1 = top-level)
QString parentId; // id of parent section (empty for top-level)
QStringList childIds; // ids of direct children
// Cached display fields
QString title;
QString keyword;
QString priority;
QStringList tags;
QString scheduled;
QString deadline;
bool fieldsLoaded = false;
// Section body (lazy)
QString body;
bool bodyLoaded = false;
// Properties drawer (lazy)
QVariantMap properties;
bool propertiesLoaded = false;
};
// Load all sections for the current document
void loadDocument();
// Recursively load sections under a parent path
void loadSections(const QString &parentPath, const QString &parentId, int level);
// Load display fields for a section
void loadFields(Section &sec) const;
// Load body for a section
void loadBody(Section &sec) const;
// Load properties for a section
void loadProperties(Section &sec) const;
// Rebuild the visible row list based on expansion state
void rebuildVisibleRows();
// Rewrite the Nth checkbox cookie (document order) in the given row's
// section body, mapping the current state char (' '/'x'/'X'/'-') to a new
// one via `nextState`. Writes back via /body and refreshes. Returns success.
bool rewriteCheckbox(int row, int checkboxOrdinal,
const std::function<QString(const QString &)> &nextState);
OrkClient *m_client = nullptr;
QString m_document;
// All sections keyed by id
mutable QHash<QString, Section> m_sections;
// Top-level section ids in order
QStringList m_topLevelIds;
// Currently expanded section ids
QSet<QString> m_expanded;
// Visible row order (section ids)
QStringList m_visibleRows;
// Supported src-block languages from `/ctl languages` (loaded once).
mutable QStringList m_supportedLanguages;
mutable bool m_languagesLoaded = false;
};