// 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 #include #include #include #include #include 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 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 `, // 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 // ///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 // ///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 ///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 // ///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 // ///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 // ///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 ///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 //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 &nextState); OrkClient *m_client = nullptr; QString m_document; // All sections keyed by id mutable QHash m_sections; // Top-level section ids in order QStringList m_topLevelIds; // Currently expanded section ids QSet 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; };