Something went wrong. Try again.
The agentic engineering control plane for the posthuman future
Something went wrong. Try again.
12 kB · 256 lines
C
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257#pragma once
#include "EditorModel.h"#include "ScrollPhysics.h"#include "XenoSettings.h"
#include <QFont>#include <QObject>#include <QPointer>#include <QPoint>#include <QString>#include <QTimer>#include <QVariantList>#include <QVariantMap>#include <QtQml/qqmlregistration.h>
// The editor widget's controller: orchestration only. It carries the// view-relevant properties, forwards the model's signals one to one, and owns// the two steps the model cannot take: the QQuickTextDocument cast, and the// editor font spec derived from settings. It holds no other policy, no window// state, and no document state of its own.class EditorController : public QObject{ Q_DISABLE_COPY_MOVE(EditorController) Q_OBJECT QML_ELEMENT // The app settings. The editor reuses the terminal font (family and weight) // with its own size and line height, so the controller needs the settings to // build the font and set the factor. Q_PROPERTY(XenoSettings* settings READ settings WRITE setSettings NOTIFY settingsChanged) Q_PROPERTY(QFont editorFont READ editorFont WRITE setEditorFont NOTIFY editorFontChanged) Q_PROPERTY(bool wrap READ wrap WRITE setWrap NOTIFY wrapChanged) Q_PROPERTY(bool highlightEnabled READ highlightEnabled WRITE setHighlightEnabled NOTIFY highlightEnabledChanged) Q_PROPERTY(QString statusLine READ statusLine NOTIFY statusLineChanged) Q_PROPERTY(QString filePath READ filePath NOTIFY documentChanged) Q_PROPERTY(int lineCount READ lineCount NOTIFY documentChanged) Q_PROPERTY(int maxLineLength READ maxLineLength NOTIFY storeChanged) Q_PROPERTY(int windowStartLine READ windowStartLine NOTIFY windowChanged) Q_PROPERTY(int windowLineCount READ windowLineCount NOTIFY windowChanged) Q_PROPERTY(int totalLines READ totalLines NOTIFY windowChanged) Q_PROPERTY(int totalRows READ totalRows NOTIFY windowChanged) Q_PROPERTY(int windowStartRow READ windowStartRow NOTIFY windowChanged) Q_PROPERTY(int windowRowCount READ windowRowCount NOTIFY windowChanged) Q_PROPERTY(QVariantList windowRowOffsets READ windowRowOffsets NOTIFY windowChanged) Q_PROPERTY(qreal lineHeightPx READ lineHeightPx NOTIFY windowChanged) Q_PROPERTY(qreal viewportTopPx READ viewportTopPx NOTIFY viewportTopPxChanged) Q_PROPERTY(bool caretOutsideWindow READ caretOutsideWindow NOTIFY caretOutsideWindowChanged) Q_PROPERTY(bool canUndo READ canUndo NOTIFY undoAvailableChanged) Q_PROPERTY(bool canRedo READ canRedo NOTIFY undoAvailableChanged) Q_PROPERTY(bool dirty READ dirty NOTIFY dirtyChanged) Q_PROPERTY(int conflictCount READ conflictCount NOTIFY conflictsChanged) Q_PROPERTY(int conflictIndex READ conflictIndex WRITE setConflictIndex NOTIFY conflictIndexChanged) Q_PROPERTY(QString searchQuery READ searchQuery NOTIFY searchStateChanged) Q_PROPERTY(int matchCount READ matchCount NOTIFY searchStateChanged) Q_PROPERTY(int currentMatch READ currentMatch NOTIFY searchStateChanged) Q_PROPERTY(bool searchBarOpen READ searchBarOpen NOTIFY searchStateChanged)
public: explicit EditorController(QObject* parent = nullptr); ~EditorController() override;
EditorModel* model() const { return m_model; } [[nodiscard]] XenoSettings* settings() const { return m_settings; } void setSettings(XenoSettings* settings);
// QML passes editor.textDocument here. The argument is a plain QObject // because the QML type TextDocument is not constructible. Q_INVOKABLE void attach(QObject* textDocument); Q_INVOKABLE bool openFile(const QString& path); // Write the open file back to disk and clear the dirty flag. Q_INVOKABLE bool save(); // Drop the view's document. Called as the view is destroyed, so the pooled // controller never touches a document that died with its view. Q_INVOKABLE void detach(); Q_INVOKABLE bool loadSpanTable(const QString& path); Q_INVOKABLE void setViewport(qreal topPx, qreal viewportHeightPx); // The drawn width the view's text may occupy. The model measures its wrap // rows against it. Q_INVOKABLE void setWrapWidth(qreal px); // Drive the editor's scroll from one wheel event. The physics come from // xeno::scroll (app/support/ScrollPhysics.h), the same header the terminal's // wheel path reads, so both surfaces share one set of constants: a classic // notch steps exactly wheelScrollLines lines and stops, a trackpad streams // pixel deltas and glides with the shared momentum, and ctrl flips the // direction. QML forwards the event's angleDelta/pixelDelta/modifiers // verbatim and marks the event accepted. Q_INVOKABLE void wheelScroll(const QPoint& angleDelta, const QPoint& pixelDelta, Qt::KeyboardModifiers modifiers); Q_INVOKABLE void requestCursor(int line, int column); Q_INVOKABLE void requestSelection(int anchorLine, int caretLine); Q_INVOKABLE void extendSelectionTo(int fileLine, int column); Q_INVOKABLE void selectAll(); Q_INVOKABLE void replaceSelection(const QString& text); Q_INVOKABLE void undo(); Q_INVOKABLE void redo(); Q_INVOKABLE QVariantMap filePositionForDocument(int position) const; Q_INVOKABLE void noteCursorPosition(int position); Q_INVOKABLE void noteSelection(int start, int end); Q_INVOKABLE bool selectionClamped() const; Q_INVOKABLE bool windowCoversSelection() const; Q_INVOKABLE bool caretInWindow() const; Q_INVOKABLE bool hasSelection() const; Q_INVOKABLE int caretLine() const; Q_INVOKABLE int caretColumn() const; Q_INVOKABLE int anchorLine() const; Q_INVOKABLE int anchorColumn() const;
// One conflict's store character range and its two texts, as "start", "end", // "local" and "disk". Empty for an out-of-range index. Q_INVOKABLE QVariantMap conflictRange(int index) const; // The document position of a store offset, or -1 when the window does not hold // that offset's line. The card geometry starts here. Q_INVOKABLE int documentPositionForOffset(int offset) const; // Settle one conflict, or every one at once. Accepting takes the disk side; // rejecting keeps the buffer's own text. Q_INVOKABLE void acceptConflict(int index); Q_INVOKABLE void rejectConflict(int index); Q_INVOKABLE void acceptAllConflicts(); Q_INVOKABLE void rejectAllConflicts(); // Put the caret on the conflict, which brings its line into the window and the // viewport, so its card is drawn. Q_INVOKABLE void revealConflict(int index); Q_INVOKABLE void nextConflict(); Q_INVOKABLE void previousConflict();
// Store search, mirroring the terminal's scrollback-search API: the needle, // the match set, the selected index, and the search-bar overlay flag. The // matches are character offsets over the whole store. Q_INVOKABLE void startSearch(const QString& query); Q_INVOKABLE void searchNext(); Q_INVOKABLE void searchPrevious(); Q_INVOKABLE void clearSearch(); Q_INVOKABLE void setSearchBarOpen(bool open);
int conflictCount() const; int conflictIndex() const { return m_conflictIndex; } void setConflictIndex(int index);
QFont editorFont() const { return m_model->font(); } void setEditorFont(const QFont& font) { m_model->setFont(font); } bool wrap() const { return m_model->wrap(); } void setWrap(bool wrap) { m_model->setWrap(wrap); } bool highlightEnabled() const { return m_model->highlightEnabled(); } void setHighlightEnabled(bool enabled) { m_model->setHighlightEnabled(enabled); } QString statusLine() const { return m_model->statusLine(); } QString filePath() const { return m_model->filePath(); } int lineCount() const { return m_model->totalLines(); } int maxLineLength() const { return m_model->maxLineLength(); } int windowStartLine() const { return m_model->windowStartLine(); } int windowLineCount() const { return m_model->windowLineCount(); } int totalLines() const { return m_model->totalLines(); } int totalRows() const { return m_model->totalRows(); } int windowStartRow() const { return m_model->windowStartRow(); } int windowRowCount() const { return m_model->windowRowCount(); } QVariantList windowRowOffsets() const { return m_model->windowRowOffsets(); } qreal lineHeightPx() const { return m_model->lineHeightPx(); } qreal viewportTopPx() const { return m_model->viewportTopPx(); } bool caretOutsideWindow() const { return m_model->caretOutsideWindow(); } bool canUndo() const { return m_model->canUndo(); } bool canRedo() const { return m_model->canRedo(); } bool dirty() const { return m_model->isDirty(); } QString searchQuery() const { return m_searchQuery; } int matchCount() const { return int(m_searchMatches.size()); } int currentMatch() const { return m_currentMatch; } bool searchBarOpen() const { return m_searchBarOpen; }
signals: void settingsChanged(); // The model's signals that QML consumes, forwarded one to one. biasChanged // and highlightOnChanged stay on the model: no view binds to them. void editorFontChanged(); void wrapChanged(); void highlightEnabledChanged(); void statusLineChanged(); void documentChanged(); void windowChanged(); void storeChanged(); void viewportTopPxChanged(); void caretOutsideWindowChanged(); void cursorRequested(int position); void selectionRequested(int anchor, int position); void loaded(bool ok, const QString& error); void undoAvailableChanged(); void dirtyChanged(); void conflictsChanged(); void conflictIndexChanged(); // Search state moved: the query, the match count, the selected index, or the // bar's open flag. void searchStateChanged(); // A one-line report for the pane's corner note. void notePosted(const QString& text); // A save was refused because conflicts exist: the pane brings the conflict // into view and the bar carries the decisions. void conflictSaveRequested();
private: // Apply a line delta (positive = scroll up) to the viewport through the // model's public viewport API. void applyWheelLines(float lines); // One glide tick. Stops the timer when the shared glide has ended. void stepWheelGlide(); // Build the editor font from settings and hand it to the model, and apply // the configured line-height factor. Called on setSettings and on each // relevant settings change. void applyFontSettings(); void applyLineHeight(); // Hand the configured wrap flag to the model. Called on setSettings and on // each wrap setting change. void applyWrap();
// The caret's character offset into the store. The search rules measure the // caret against match offsets; zero is the answer for an unloaded store. [[nodiscard]] int caretStoreOffset() const; // The first match at or after the caret, wrapping to the first match; -1 when // the match set is empty. [[nodiscard]] int matchAtOrAfterCaret() const; // The last match strictly before the caret, wrapping to the last match; -1 // when the match set is empty. [[nodiscard]] int matchBeforeCaret() const; // Select the match at `index` and scroll it into view. An out-of-range index // does nothing. void selectMatch(int index); // Rebuild the match set for a live query without moving the caret or the // selection. Runs on every store change and on a document load. void rerunSearch();
EditorModel* m_model = nullptr; // QPointer so a settings object destroyed elsewhere leaves the controller // with a null, not a dangling, pointer. QPointer<XenoSettings> m_settings; // Which conflict the pane's bar is looking at. It clamps to the list on every // change, so an edit that drops the last conflict cannot leave it dangling. int m_conflictIndex = 0; // Search state, mirroring the terminal's scrollback search: the live needle, // the match offsets over the whole store, the selected index (0-based; -1 = // none), and whether the QML bar overlays the pane. QString m_searchQuery; QList<int> m_searchMatches; int m_currentMatch = -1; bool m_searchBarOpen = false; // Scroll momentum, shared with the terminal pane: the glide owns the // arithmetic (see app/support/ScrollPhysics.h), this timer ticks it at the // shared period. Only the trackpad path starts it; a wheel notch cancels // the glide. QTimer* m_wheelGlideTimer = nullptr; xeno::scroll::MomentumGlide m_wheelGlide;};