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
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: