Skip to content

visualdynamics.gui.report_editor

report_editor

The report editor: the exported page as the preview, the acts on the bar.

The page shown is the very HTML the export writes — a browser view of exactly what the reader will get, figures drawn by the same JavaScript their browser will run — and it does two things the export's page does not: it frames the selected block, and it says which block was clicked. Everything else lives in Qt (Brandon, 2026-09-08: "the toolbar should be the same as the task bar at the top of the report screen how we have for every other GUI object"): a bar of acts above the page — Insert, Reference, Move Up, Move Down, Delete, Export — and a settings pane beside it carrying what the selected block has to say: a figure block's sources and caption, a text block's Markdown in a Qt editor, the report's title and marking when nothing is selected. Every act is one operation on the Report model, journalled by the window, followed by a re-render of the page; the exported file never carries any chrome.

Classes:

Name Description
ReportEditor

Owns the web view, the bar and the pane; mutates the report the

Classes

ReportEditor

ReportEditor(parent: QWidget | None = None)

Bases: QWidget

Owns the web view, the bar and the pane; mutates the report the operations describe.

Methods:

Name Description
insert

Insert a block of kind after the selected block, or at the

insert_reference

{{figure:caption}} or {{table:caption}} at the text

flush_text

Land the text editor's Markdown on the block now — what the

Source code in src/visualdynamics/gui/report_editor.py
def __init__(self, parent: QWidget | None = None) -> None:
    super().__init__(parent)
    self.report: Any = None
    #: callables the window sets, so the editor reads the project
    #: as it stands rather than a copy taken when it opened
    self.objects: Callable[[], dict[str, Any]] | None = None
    self.links: Callable[[], list[Any]] | None = None
    self.unit_system: Any = None
    #: the block the bar and the pane act on, or None for the report
    self.selected: int | None = None
    #: (label, caption) for every numbered figure and table, from
    #: the last render — the Reference menu's offer
    self._labels: list[tuple[str, str]] = []
    layout = QVBoxLayout(self)
    layout.setContentsMargins(0, 0, 0, 0)
    layout.setSpacing(0)
    self.toolbar: QToolBar = self._build_toolbar()
    layout.addWidget(self.toolbar)
    self.split: QSplitter = QSplitter(Qt.Orientation.Horizontal)
    self.view: QWebEngineView = QWebEngineView()
    self.split.addWidget(self.view)
    self.pane: QScrollArea = QScrollArea()
    self.pane.setWidgetResizable(True)
    self.pane.setMinimumWidth(240)
    self.split.addWidget(self.pane)
    self.split.setStretchFactor(0, 3)
    self.split.setStretchFactor(1, 1)
    self.split.setSizes([900, 300])
    layout.addWidget(self.split, 1)
    self.bridge: _Bridge = _Bridge(self)
    self.bridge.operated.connect(self._operate)
    self.channel: QWebChannel = QWebChannel(self)
    self.channel.registerObject('bridge', self.bridge)
    self.view.page().setWebChannel(self.channel)
    self._scroll = 0
    self._page_path = None
    self._channel_js = None
    #: the project changed while this page was not on screen; the
    #: window rebuilds before showing it again rather than paying
    #: 254 ms per change for a document nobody is looking at
    self.stale: bool = False
    # the pane's widgets, rebuilt when the selection changes; None
    # while the selection has no such field
    self.text_editor: QPlainTextEdit | None = None
    self.caption_edit: QLineEdit | None = None
    self.title_edit: QLineEdit | None = None
    self.marking_edit: QLineEdit | None = None
    self.color_box: QComboBox | None = None
    self.field_boxes: dict[str, QComboBox] = {}
    self._loading_pane = False
    self._text_timer: QTimer = QTimer(self)
    self._text_timer.setSingleShot(True)
    self._text_timer.setInterval(TEXT_DEBOUNCE_MS)
    self._text_timer.timeout.connect(self.flush_text)
    self._show_selection()
Methods:
insert
insert(kind: str) -> None

Insert a block of kind after the selected block, or at the end, and select it.

Source code in src/visualdynamics/gui/report_editor.py
def insert(self, kind: str) -> None:
    """Insert a block of `kind` after the selected block, or at the
    end, and select it."""
    if self.report is None:
        return
    at = (self.selected + 1 if self.selected is not None
          else self.report.num_blocks)
    self._operate({'op': 'insert', 'at': at, 'kind': kind})
insert_reference
insert_reference(kind: str, caption: str) -> None

{{figure:caption}} or {{table:caption}} at the text editor's cursor — the token the model stores, renumbered by the same code that numbers the page.

Source code in src/visualdynamics/gui/report_editor.py
def insert_reference(self, kind: str, caption: str) -> None:
    """`{{figure:caption}}` or `{{table:caption}}` at the text
    editor's cursor — the token the model stores, renumbered by
    the same code that numbers the page."""
    if self.text_editor is None:
        return
    self.text_editor.textCursor().insertText(f'{{{{{kind}:{caption}}}}}')
    self.text_editor.setFocus()
flush_text
flush_text() -> None

Land the text editor's Markdown on the block now — what the debounce does after typing rests, and what a test calls.

Source code in src/visualdynamics/gui/report_editor.py
def flush_text(self) -> None:
    """Land the text editor's Markdown on the block now — what the
    debounce does after typing rests, and what a test calls."""
    self._text_timer.stop()
    if (self.text_editor is None or self.report is None
            or self.selected is None
            or not 0 <= self.selected < self.report.num_blocks):
        return
    text = self.text_editor.toPlainText()
    if text == self.report.blocks[self.selected].get('text', ''):
        return
    self._operate({'op': 'field', 'at': self.selected, 'field': 'text',
                   'value': text})

Functions: