226 lines
8.9 KiB
C++
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 ×tamp);
|
|
|
|
// Set a section's DEADLINE timestamp (see setScheduled). Writes
|
|
// /<doc>/<id>/deadline and refreshes the row.
|
|
Q_INVOKABLE bool setDeadline(int row, const QString ×tamp);
|
|
|
|
// 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 §ionId, 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;
|
|
};
|