document_editor.py Chromium-Based WYSIWYG Rich Text Editor for PySide6

📄 4,317 lines  |  📦 PySideAbdhUI Package  |  ⚙ Python 3.10+ / PySide6

document_editor.py implements a fully-featured WYSIWYG (What You See Is What You Get) rich text editor widget for desktop applications built with the PySide6 (Qt for Python) framework. The editor is built on top of QWebEngineView, which embeds a Chromium rendering engine inside a native Qt widget. This hybrid architecture leverages the power of modern web technologies — HTML, CSS, and JavaScript — for the editing surface, while the surrounding Qt infrastructure handles desktop integration concerns such as native file dialogs, context menus, colour pickers, and window management.

The editor presents itself as a multi-page document layout, where each page is a <div class="page"> element with fixed CSS dimensions that correspond to a real paper size (A4, Letter, B5, or a custom "Edu-Item" format). Content overflow is automatically managed by a JavaScript pagination engine that pushes overflowing blocks onto subsequent pages and pulls underflowing content back when space becomes available. This approach gives users a real-time preview of how their document will look when printed or exported to PDF.

Key capabilities of the editor include: KaTeX-based mathematical formula rendering with inline and display modes, image insertion with drag-and-drop repositioning and 8-directional resize handles, table editing with column resizing and row/column insertion, PDF import via Mozilla's pdf.js library, PDF export using Chromium's built-in renderer, and DOCX/ODT import/export via pypandoc. The editor also supports zoom, page navigation, margin control, text formatting (bold, italic, underline, strikethrough, font family, font size, text and background colours), paragraph alignment, and bidirectional text support.

Design Philosophy The editor follows a "web-inside-Qt" design where all visual editing happens in JavaScript inside Chromium, while Qt serves purely as the desktop shell providing native dialogs, menus, and file system access. Communication between the Python and JavaScript layers is achieved through QWebEnginePage.runJavaScript() calls (Python → JS) and the document.title bridge (JS → Python).

Architecture

The system follows a layered architecture where the Python RichTextEditor class acts as the controller, the Chromium QWebEngineView as the rendering engine, and an embedded HTML document as the editing surface. The following diagram illustrates the high-level component relationships and data flow between the layers.

+-------------------------------------------------------------+ | Qt Application Layer | | | | +--------------------+ +-----------------------------+ | | | RichTextEditor | | Desktop Dialogs | | | | (QWebEngineView) | | - QFileDialog | | | | | | - QColorDialog | | | | - Public API | | - QInputDialog | | | | - Signal handling | | - QMessageBox | | | | - File I/O | | - QMenu (context menus) | | | +--------+-----------+ +-----------------------------+ | | | | runJavaScript() document.title bridge | +---+----------------------------+--+--------------+ | | | +--------------v----------------------------v--v--------------+ | Chromium Rendering Engine | | | | +--------------------------------------------------------+ | | | Embedded HTML Document | | | | | | | | .pages-wrapper | | | | +-- .page (contenteditable) <-- Page 1 | | | | +-- .page (contenteditable) <-- Page 2 | | | | +-- ... | | | | | | | | JS Engines: | | | | [1] Drag & Resize Engine (images) | | | | [2] Table Column Resize | | | | [3] Table Tab Navigation | | | | [4] Multi-Page Engine (overflow/underflow) | | | | [5] Keyboard Handler (Backspace, Delete, Arrows...) | | | | [6] KaTeX Rendering Engine | | | | [7] Dirty Flag Bridge (input event listener) | | | +--------------------------------------------------------+ | | | | External Libraries (lazy-loaded): | | - KaTeX (katex.min.js, auto-render.min.js, katex.min.css) | | - pdf.js (pdf.min.js, pdf.worker.min.js) | | - pdf-lib (pdf-lib.min.js) | +--------------------------------------------------------------+

Python → JavaScript Communication

The primary channel for Python-to-JavaScript communication is the QWebEnginePage.runJavaScript() method. This is an asynchronous API — Python submits a JavaScript string for execution, and an optional callback receives the result. The RichTextEditor class uses this mechanism extensively: every formatting command, content manipulation, page size change, and zoom operation is implemented as a JavaScript fragment dispatched through runJavaScript().

For operations that require a synchronous result (such as reading the editor's HTML content before a Pandoc export), the class employs a QEventLoop to block the calling thread until the asynchronous JavaScript callback fires. This pattern is visible in methods like getHtmlContentSync() and getPlainTextSync().

JavaScript → Python Communication

The reverse direction uses a creative "document title" bridge. When the user edits content in the browser, the JavaScript input event listener sets document.title to a string starting with "__edit__" followed by a timestamp. On the Python side, the page().titleChanged signal is connected to _on_title_changed(), which interprets this prefix as a notification that the document has been modified and sets the _is_dirty flag accordingly.

Dependencies

Standard Library

ModulePurpose
base64Encode binary assets (images, JS bundles, PDF bytes) into base64 for HTML data-URI embedding
htmlEscape LaTeX formula text for safe storage in data-latex attributes on math placeholder elements
jsonSafely serialize Python strings into JavaScript string literals for runJavaScript() calls
osEnvironment variable management, filesystem operations during import/export
reRegular expressions for parsing HTML bodies, converting MathJax script tags, extracting style blocks
tempfileCreate short-lived temporary HTML files consumed by pypandoc during DOCX conversion

PySide6 (Qt for Python)

ModuleClassesRole
PySide6.QtCoreQEventLoop, QMarginsF, Qt, QTimerEvent loop blocking, print margins, enums, deferred execution
PySide6.QtGuiQColor, QPageLayout, QPageSizeColour dialogs, PDF page configuration
PySide6.QtWidgetsQDoubleSpinBox, QMenu, QFileDialog, QMessageBox, QSpinBox, QDialog, QColorDialog, QInputDialog, QFormLayout, QDialogButtonBox, QSizePolicyDesktop dialogs, context menus, form layouts, size policies
PySide6.QtWebEngineWidgetsQWebEngineViewChromium web view widget (base class of RichTextEditor)
PySide6.QtWebEngineCoreQWebEnginePageBrowser actions (Cut/Copy/Paste), console message hook

Package-Internal

ImportPurpose
PySideAbdhUI.utils.get_resource_pathResolve paths to bundled resources (KaTeX, pdf.js, pdf-lib) regardless of whether the application runs from source or an installed wheel

Optional Dependency pypandoc is required for DOCX and ODT import/export. If it is not installed, the editor still loads and functions normally, but DOCX-related operations will display a missing-dependency warning dialog instead of performing the conversion. The HAS_PANDOC flag controls this graceful degradation.

Class: DebugPage

DebugPage is a lightweight subclass of QWebEnginePage that overrides the javaScriptConsoleMessage() virtual method. By default, Chromium silently discards JavaScript console output when running inside a QWebEngineView. This class intercepts every message written to the browser console (via console.log(), console.warn(), console.error(), etc.) and mirrors it to the Python standard output, prefixed with the source line number. This makes debugging of the embedded JavaScript significantly easier during development and testing.

def javaScriptConsoleMessage(self, level, message, lineNumber, sourceID) Override

Intercept a JavaScript console message. Prints "JS[lineNumber] message" to Python's stdout, then forwards the message to the base class implementation for normal Qt processing.

level Chromium log severity level (Info, Warning, Error, or Debug)
message Text content written by console.log/warn/error in the editor page
lineNumber 1-based source line number inside the JavaScript file or inline script
sourceID URL or identifier of the script file that produced the message

Class: RichTextEditor

RichTextEditor is the main public widget and the heart of the module. It inherits from QWebEngineView and provides a complete WYSIWYG editing experience inside a Chromium-based web view. The class exposes a rich public API for text formatting, content management, page layout control, file import/export, and more, while delegating all visual editing operations to JavaScript running inside the embedded HTML document.

Constructor & Initialization

def __init__(self, default_size: str = 'A4', parent=None)Public

Constructs the editor widget, generates the initial HTML template, and wires up all Qt/JS communication bridges.

Parameters:

default_size – Initial page size name. Must be one of: "A4", "Letter", "B5", "Edu-Item". Defaults to "A4".
parent – Optional parent widget for Qt ownership semantics.

The constructor performs the following initialization sequence:

  1. Calls super().__init__(parent) to initialise the underlying QWebEngineView.
  2. Replaces the default QWebEnginePage with a DebugPage instance for JS console mirroring.
  3. Sets the size policy to Expanding in both directions so the editor fills available layout space.
  4. Initialises the _page_margins dictionary with 10 mm on all sides.
  5. Disables Chromium's default context menu and connects customContextMenuRequested to the custom menu handler.
  6. Initialises lazy-loading caches for pdf.js, pdf.worker, and pdf-lib base64 data.
  7. Generates and loads the initial HTML document via _build_initial_html().
  8. Connects loadFinished for deferred content injection.
  9. Initialises the _is_dirty flag and connects titleChanged for edit notifications.
  10. Overrides javaScriptConsoleMessage with a lambda that prints detailed JS debug output.

PAGE_SIZES Class Constant

NameWidth (mm)Height (mm)Description
"A4"210297ISO A4 — default paper size
"Letter"215.9279.4US Letter (8.5 in × 11 in)
"B5"176250ISO B5 — compact book/journal format
"Edu-Item"21050Custom short-page format for educational item cards

Dirty Tracking

The editor tracks whether the document contains unsaved modifications through the _is_dirty boolean flag. When the user types, deletes, or otherwise modifies content, the JavaScript input event listener sets document.title to "__edit__<timestamp>", which triggers the _on_title_changed() slot on the Python side and sets _is_dirty = True. The flag can also be set manually via _mark_dirty(), and is cleared when the document is saved via setClean(). The current state is queryable through isModified().

def isModified(self) -> boolPublic

Returns True if the document has unsaved changes, False otherwise.

def setClean(self)Public

Marks the document as saved by clearing the dirty flag.

def _mark_dirty(self)Private

Explicitly marks the document as modified.

def _on_title_changed(self, title: str)Private

Receives edit notifications from JavaScript through the document.title bridge. If the title starts with "__edit__", sets _is_dirty = True.

Before generating a PDF or initiating a print operation, the editor must ensure that its internal page structure is consistent and that no blank trailing pages will appear in the output. The _preparePrintBreaks() method executes JavaScript that removes empty trailing pages and sets a temporary data-print-ready attribute on every remaining page element. After the export completes, _cleanupPrintBreaks() removes these markers and triggers a full page-consistency check.

def _preparePrintBreaks(self)Private

Removes empty trailing pages and marks all pages as print-ready before PDF export.

def _cleanupPrintBreaks(self)Private

Removes data-print-ready attributes from all pages and re-runs checkAllPages().

Image Export (Page-by-Page PNG Capture)

The extractAsImages() method provides a mechanism to capture each logical editor page as a separate PNG image file. The capture process is multi-step and asynchronous, using a callback chain that processes pages one at a time:

extractAsImages()Collect page geometry (JS)_on_page_geometry()_capture_next_page()_grab_current_page()_finish_image_capture()

For each page, the widget is temporarily resized to match the page dimensions exactly at zoom 1.0, the viewport is scrolled to the correct position, and after a 200 ms rendering delay, self.grab() captures the widget contents as a QPixmap. After all pages are captured, the original widget size, zoom, and size constraints are restored, and the user is prompted to select an output directory. Each image is saved as page_1.png, page_2.png, etc.

def extractAsImages(self)Public

Capture each .page element as a separate PNG image using self.grab().

Context Menus

The editor disables Chromium's default right-click context menu and replaces it with custom Qt menus that are context-aware. When the user right-clicks inside a table cell (<td> or <th>), a table-specific menu appears with options to insert rows and columns. For all other locations, the standard editing menu is shown.

Default Menu Actions

CategoryActions
ClipboardCut, Copy, Paste, Select All
AlignmentAlign Center, Align Justified, Align Left, Align Right
DirectionChange Text Direction LTR, Change Text Direction RTL
Text StyleToggle Bold, Toggle Italic, Toggle Underline, Toggle Strikethrough

Table Menu Actions

When right-clicking inside a table cell, the menu adds the following actions above a "Text Format" submenu containing all the default actions:

KaTeX Resource Loader

The _load_katex_resource() method reads KaTeX library files (CSS and JavaScript) from the application's packaged resources and converts them into base64 data-URI strings that can be embedded directly in the <link> and <script> tags of the editor HTML. This approach avoids filesystem path issues and makes the editor self-contained regardless of the installation directory.

If a KaTeX resource file is not found, the method returns an empty string, and the corresponding tag is replaced with an HTML comment in _build_initial_html(). This graceful degradation ensures the editor still loads and functions even when KaTeX is unavailable — math formulas simply won't be rendered.

def _load_katex_resource(self, name: str, ext: str = 'css') -> strPrivate

Read a KaTeX file from the package resources and return a base64 data-URI string. Returns '' if the file is not found.

name – Resource filename without extension (e.g., "katex.min", "auto-render.min")
ext – File extension: "css" or "js". Determines the MIME type in the data-URI.

PDF Library Lazy Loading

The PDF-related JavaScript libraries (pdf.js, pdf.worker, and pdf-lib) are not embedded in the initial HTML document to keep page load times fast and avoid errors when the files are missing. Instead, they are loaded on-demand when the user first opens a PDF file. Once loaded, the base64 data is cached in instance variables to prevent repeated disk reads.

Caching Strategy All three loaders follow the same pattern: check the cache first, attempt to load and encode the file on a cache miss, store the result (or an empty string on failure) in the cache, and return the cached value. This ensures that each file is read at most once per editor instance.

MethodResourceCache Variable
_load_pdfjs_b64()pdf.min.js (Mozilla pdf.js core)_pdfjs_js_b64
_load_pdfjs_worker_b64()pdf.worker.min.js (pdf.js worker)_pdfjs_worker_b64
_load_pdflib_b64()pdf-lib.min.js (PDF manipulation)_pdflib_js_b64

Page Margins

Page margins are stored as a dictionary of four float values (top, right, bottom, left) in millimetres. They are applied as CSS padding on each .page div, determining the printable content area. Margins can be changed programmatically via setPageMargins() or interactively through the showMarginDialog() modal dialog, which provides four QDoubleSpinBox controls (range 0–50 mm, step 1 mm). When margins change, a full pagination recalculation is triggered via checkAllPages().

def setPageMargins(self, top: float, right: float, bottom: float, left: float)Public

Update page margins (CSS padding) in millimetres and recalculate page breaks.

def showMarginDialog(self)Public

Open a modal dialog with spin boxes for editing page margins. Applies changes on OK.

File Open/Save Dialogs

def LoadFileDialog(self, caption='Open a file', dir='', filter='', dialog_type='open')Public

Unified file open/save dialog. Dispatches to the appropriate handler based on the file extension and dialog type. Supports .txt, .html, .pdf, and .docx formats for opening, and .txt, .html, .pdf, and .docx for saving.

def load_file(self, file_path: str = '')Public

Read a file from disk and return its contents as a string suitable for editor insertion. DOCX files are converted to HTML via pypandoc; PDF files are returned as base64; TXT files are read as UTF-8 text; HTML files are returned raw.

def save_file(self, file_path: str = "")Public

Save the current editor content to disk. TXT files use toPlainText(), HTML files use toHtml(), PDF files call savePdf(), and DOCX files call saveWithPandoc().

PDF Import

PDF import is handled by Mozilla's pdf.js library, which is lazily injected into the editor page only when a PDF file is first opened. The import pipeline works as follows:

  1. The raw PDF bytes are read from disk and encoded as base64.
  2. _ensurePdfJsLoaded() checks whether pdf.js has already been injected; if not, it loads the base64-encoded pdf.min.js and pdf.worker.min.js from cached resources and injects them into the Chromium page via runJavaScript().
  3. Once the library is available, _buildPdfRenderJs() generates JavaScript that decodes the base64 PDF data and renders each page sequentially.
  4. Each PDF page is rendered to an off-screen canvas at 2× display scale for sharpness, converted to a PNG data-URL, and inserted into the editor as an <div class="img-wrapper"> element with eight resize handles.
  5. The display width of each page image matches the editor page content width, with proportional height scaling.
def _ensurePdfJsLoaded(self, pdf_data_b64: str)Private

Inject pdf.js into the page if not already done, then render the PDF. Shows an in-editor error when resources are missing.

def _renderPdfPages(self, pdf_data_b64: str)Private

Render PDF pages when pdf.js has already been loaded in a previous open operation.

def _buildPdfRenderJs(self, safe_pdfdata: str) -> strPrivate

Build the JavaScript source that decodes a PDF and inserts each page as an img-wrapper element.

PDF Export

PDF export uses Chromium's built-in QWebEnginePage.printToPdf() method, which renders the HTML document to a PDF file using the same Chromium rendering engine that displays it on screen. This ensures high fidelity between the on-screen preview and the exported document.

A QPageLayout object is constructed with the page size matching the editor's current setting and zero Qt margins — the CSS padding on the .page divs already provides the visual margins, and adding Qt margins on top would double the margin space. The print CSS media query ensures each page fills the printed page width exactly with width:100%; height:auto and uses page-break-after: always for correct page splitting.

def savePdf(self, filepath: str)Public

Export the document as a PDF file. Prepares print breaks, waits 100 ms for layout to settle, then calls printToPdf() with the appropriate page layout and zero margins.

Pandoc Export (DOCX / ODT)

The saveWithPandoc() method enables export to DOCX, ODT, and other formats supported by Pandoc. It first checks the HAS_PANDOC flag to ensure pypandoc is available, then retrieves the current document HTML synchronously via getHtmlContentSync(), writes it to a temporary file, and passes it to pypandoc.convert_file() for conversion. The output format is automatically determined from the file extension. The temporary file is deleted after conversion regardless of success or failure.

def saveWithPandoc(self, filePath: str)Public

Export editor content via pypandoc. Supports DOCX, ODT, and any Pandoc-supported output format. Displays a warning dialog if pypandoc is not installed.

Math Preprocessing & Formula Preparation

The editor provides two complementary methods for handling LaTeX math content in imported HTML:

_preprocess_math_html() — MathJax Script Conversion

When importing HTML documents that were originally authored for MathJax, the <script type="math/tex"> elements would be stripped by the browser's innerHTML parser before KaTeX could see them. This static method converts MathJax script tags to plain-text LaTeX delimiters before the HTML reaches the browser:

prepare_math_formulas() — LaTeX Delimiter to Placeholder Conversion

After MathJax scripts have been converted (or for HTML that already uses standard LaTeX delimiters), this method scans the HTML and replaces all recognised delimiter patterns with lightweight placeholder <span> elements that the editor's KaTeX rendering pipeline understands:

DelimiterModeOutput Element
$$...$$Display (block)<span class="math-formula" data-display="block" data-latex="...">
\[...\]Display (block)<span class="math-formula" data-display="block" data-latex="...">
\(...\)Inline<span class="math-formula" data-display="inline" data-latex="...">
$...$Inline<span class="math-formula" data-display="inline" data-latex="...">

Structure Synchronization

The structure_synchronization() method is a critical import pipeline component that merges an external HTML document into the editor's template without breaking the editor's internal structure. It performs the following steps in order:

  1. Native document check: If the imported HTML already contains the <meta name="abdh-editor"> marker, it is a native editor document and needs no synchronization.
  2. Extract styles: All <style> blocks are extracted from the imported document.
  3. Extract body: The <body> content is extracted, or the entire document is used if no body tag exists.
  4. Remove external scripts: All <script> tags are stripped from the body to prevent external JavaScript from executing inside the editor.
  5. Rename conflicting classes: Any CSS class that matches an editor-reserved name (such as page, katex, img-wrapper, etc.) is renamed by prepending EXT (e.g., .page becomes .EXTpage).
  6. Convert math formulas: LaTeX delimiters in the body are converted to math-formula placeholder spans via prepare_math_formulas().
  7. Inject into template: A fresh editor HTML template is built, the synchronized external styles are injected before </head>, and the body content is inserted into the first .page div.

Reserved Classes The following CSS class names are reserved by the editor and will be renamed with an EXT prefix when found in imported documents: math-formula, math-editor, pages-wrapper, page, readonly-overlay, img-wrapper, resize-handle, handles-visible, handle-nw, handle-n, handle-ne, handle-e, handle-se, handle-s, handle-sw, handle-w, col-resize-handle, katex-error, katex, katex-display, katex-html, katex-mathml.

Page Size & Navigation

def setPageSize(self, size_name: str)Public

Switch all editor pages to a different paper size. Updates the CSS dimensions of every .page element and immediately recalculates page overflow. Raises ValueError for unknown size names.

def getPageCount(self, callback)Public

Asynchronously retrieve the number of .page elements. The result is passed to callback as an integer.

def scrollToPage(self, page_num: int)Public

Smoothly scroll the viewport so that the requested 1-based page number is visible at the top. Page numbers are clamped to the valid range.

def refreshPageBreaks(self)Public

Force an immediate recalculation of page overflow/underflow and reattach MutationObservers.

Editability Toggle

def setEditable(self, b: bool = True)Public

Toggle between editable and read-only mode. When b=False, a transparent overlay (#readonly-overlay) is shown over the editor, blocking all mouse events. When b=True, the overlay is hidden and editing is allowed.

Text Formatting

The editor provides a comprehensive set of text formatting methods that all delegate to the applyTextStyle() helper, which in turn executes the corresponding document.execCommand() call inside Chromium. The following table summarizes the available formatting operations:

MethodexecCommandDescription
setBold()boldToggle bold formatting on selection
setItalic()italicToggle italic formatting on selection
setUnderline()underlineToggle underline formatting on selection
setStrikeOut()strikeThroughToggle strikethrough on selection
setFontFamily(family)fontNameSet CSS font family on selection
setTextColor(color)foreColorSet text colour (QColor input)
setTextBackgroundColor(color)hiliteColorSet background highlight colour (QColor)
setFontSize(size)Custom JSWrap selection in <span style="font-size:Npt">; expands collapsed selections to current word
def chooseTextColor(self)Public

Open a native colour picker dialog and apply the chosen colour as the text foreground.

def chooseBackgroundColor(self)Public

Open a native colour picker dialog and apply the chosen colour as the text background highlight.

Alignment & Text Direction

def setAlignment(self, align: str)Public

Set paragraph alignment on the nearest block-level ancestor of the current selection. Accepts 'left', 'center', 'right', or 'justify'. If the cursor is directly inside a page div, a wrapping <p> element is created first.

def setParagraphDirection(self, rtl: bool)Public

Set the dir attribute of the nearest block ancestor to 'rtl' or 'ltr'. Useful for bidirectional text editing in Arabic, Hebrew, and Persian.

Insert Formula / Table / Image

Image Insertion

insertImage() opens a native file dialog filtered to common raster formats (PNG, JPG, JPEG, BMP, GIF), reads the selected file, encodes it as base64, and inserts a draggable img-wrapper element at the current cursor position. The wrapper includes eight directional resize handles (nw, n, ne, e, se, s, sw, w) that allow the user to resize the image by dragging from any corner or edge.

Math Formula Insertion

insertMath(latex, display=False) inserts a KaTeX-rendered formula span at the cursor. The formula is created via the JavaScript createMathFormula() function, which produces a <span class="math-formula"> element with contentEditable='false'. Double-clicking on a rendered formula replaces it with a text input for live editing; pressing Enter or blurring the input re-renders the formula with the updated LaTeX source.

insertMathDialog() provides a convenience dialog (QInputDialog) for entering LaTeX text, then calls insertMath() with the result.

Table Insertion

insertTable(rows, cols) builds an HTML table with the specified dimensions and inserts it at the cursor. Column resize handles are automatically attached via setupColumnResize(). insertTableDialog() provides a dialog with spin boxes for choosing the dimensions (rows: 1–30, columns: 1–15, both defaulting to 3).

Private helpers _insert_row(where) and _insert_col(where) insert a new row or column relative to the current cursor position, used by the table context menu.

Content Get / Set / Clear

MethodReturn TypeDescription
setText(raw_string, auto_render=True)NoneReplace entire document content. HTML mode (auto_render=True) renders KaTeX and checks pagination; plain-text mode (False) inserts via textContent with no processing.
appendText(html, auto_render=True)NoneAppend HTML to the last page. Optionally pre-processes math and triggers rendering.
clearContent()NoneRemove all pages and reset to a single empty contenteditable page.
getPlainText(callback)None (async)Asynchronously retrieve the concatenated text content of all pages.
getHtmlContent(callback)None (async)Asynchronously retrieve the concatenated inner HTML of all pages.
getPlainTextSync()strBlocking version of getPlainText() using QEventLoop.
getHtmlContentSync()strBlocking version of getHtmlContent() using QEventLoop.
getFullHtmlAsync()strBlocking retrieval of the complete page HTML including all <style> elements.

Zoom Controls

MethodDescription
zoomIn()Increase zoom by 10% (multiplies current factor by 1.1)
zoomOut()Decrease zoom by 10% (divides current factor by 1.1)
setZoomPercent(percent)Set zoom to an exact percentage (e.g., 100 = 1.0×)
fitPage()Automatically zoom so the entire page fits inside the viewport, with 20 px margin. Clamped to 0.1×–5.0×.
on_zoom_changed(text)Slot for a zoom-percentage combo box; parses "100%" and calls setZoomPercent().

HTML Template Builder

The _build_initial_html() method assembles the complete HTML document that serves as the editor's editing surface. This is the largest single method in the file, spanning approximately 1,400 lines and containing the full CSS stylesheet, seven JavaScript engines, and the initial page structure. The template is parameterised by the current page size (width and height in mm) and page margins.

The HTML structure consists of:

CSS Overview

SelectorPurpose
bodyDark background (#0b1120), horizontal scroll for wide pages
html scrollbarThin custom scrollbar styling for WebKit and Firefox
.math-formulaInline-block, clickable, LTR direction
.math-editorMonospace input styling for LaTeX editing
.pages-wrapperFlex column layout, centered, 12 px gap between pages
.pageWhite background, box shadow, fixed mm dimensions, box-sizing border-box, overflow hidden
.readonly-overlayAbsolute-positioned transparent overlay for read-only mode
@media printPrint-specific styles: no shadows, auto height, page-break-after, visible overflow
.img-wrapperFloating, relative-positioned container for draggable images
.resize-handle8-directional handles (nw, n, ne, e, se, s, sw, w), hidden by default, shown on hover via .handles-visible
table, td, thCollapse borders, 1 px solid #aaa, relative positioning for column resize handles
.col-resize-handleAbsolute-positioned 5 px strip at cell right edge for column width adjustment
.katex-errorRed text on pink background for KaTeX rendering errors

Embedded JavaScript Engines

The editor HTML template contains seven self-contained JavaScript modules that handle all interactive editing behaviour inside the Chromium document. Each module is wrapped in an IIFE (Immediately Invoked Function Expression) or attached to the document/window object as appropriate.

Engine 1: Drag & Resize Engine

Manages drag-and-drop repositioning and 8-directional resizing of .img-wrapper elements. Uses margin-based positioning (marginLeft / marginTop) for overlap-free image movement within the content flow. When the user clicks on a resize handle, the engine tracks the mouse delta from the initial click position and applies proportional width/height changes while adjusting margins to keep opposite corners anchored. Resize handles are shown/hidden via CSS class toggling on mouseenter/mouseleave events, since CSS :hover is unreliable inside contenteditable regions.

Engine 2: Table Column Resize

The setupColumnResize(table) function adds invisible .col-resize-handle divs to each cell in the first row of a table. These handles capture mousedown events and track horizontal mouse movement to adjust column widths across all rows simultaneously. Tables are set to table-layout: fixed to ensure consistent column widths. The resize state is tracked via the resizingColumn flag and associated variables.

Engine 3: Table Tab Navigation

A keydown listener intercepts the Tab key when the cursor is inside a table cell and moves focus to the next cell (or previous cell with Shift+Tab). This provides natural spreadsheet-like navigation within tables. When the last cell is reached, focus wraps around to the first cell, and vice versa.

Engine 4: Multi-Page Engine

This is the most complex JavaScript module and the backbone of the editor's multi-page layout. It manages the creation, overflow handling, and deletion of .page divs:

Engine 5: Advanced Keyboard Handler

Overrides several key behaviours inside the contenteditable to handle non-editable elements (math formulas, images), multi-page navigation, and edge cases that Chromium's default contenteditable handling does not address:

Key / ComboBehaviour
Ctrl+ASelect all content across all pages (not just the current page)
Ctrl+Z / Ctrl+YUndo / Redo via execCommand
Backspace / DeleteDelete non-editable nodes (formulas, images) when adjacent; reflow pages after deletion
Backspace at page startMove cursor to previous page or merge empty current page into previous
Delete at page endMove cursor to next page or remove empty next page
Arrow Left/RightSkip over non-editable elements instead of entering them
Arrow Up at page topMove focus to previous page
Arrow Down at page bottomMove focus to next page
EnterInsert a <div><br></div> block; handle empty block exit; special handling for list items
Home / EndSkip over non-editable elements

Engine 6: KaTeX Rendering Engine

Provides three key functions for mathematical formula handling:

Double-click editing is implemented via a dblclick event listener that replaces the rendered span with a <input class="math-editor"> element. Pressing Enter or blurring the input creates a new formula span with the updated LaTeX and re-renders it.

Engine 7: Dirty Flag Bridge

A JavaScript input event listener on the document body detects all user edits (typing, deleting, formatting changes) and sets document.title to "__edit__" + Date.now(). This communicates the modification to Python via the titleChanged signal, which is connected to the _on_title_changed() slot that sets _is_dirty = True. The timestamp ensures that even rapid successive edits trigger distinct title changes.

Shortcut File Loaders

The editor provides four convenience methods that combine a file dialog with the appropriate loading logic for each supported format:

def loadTextFile(self, auto_render=False)Public

Open a file dialog filtered to .txt files, read the content, and insert it as plain text (or optionally rendered HTML).

def loadHtmlFile(self)Public

Open a file dialog filtered to .html / .htm files, synchronise the structure, and load it into the editor with post-load math rendering and pagination.

def loadDocxFile(self)Public

Open a file dialog filtered to .docx files, convert via pypandoc, synchronise the structure, and load it into the editor.

def loadPdfFile(self)Public

Open a file dialog filtered to .pdf files, read and base64-encode the content, and render via pdf.js.

Complete API Reference

The following table lists every public method exposed by the RichTextEditor class, grouped by functional category. Private methods (prefixed with _) are documented in the sections above but are omitted here as they are not part of the stable public interface.

Document State

MethodSignatureReturns
isModifiedisModified() -> boolTrue if unsaved changes exist
setCleansetClean()None
setEditablesetEditable(b: bool = True)None

Page Layout

MethodSignatureReturns
setPageSizesetPageSize(size_name: str)None
setPageMarginssetPageMargins(top, right, bottom, left: float)None
showMarginDialogshowMarginDialog()None
getPageCountgetPageCount(callback)None (async)
scrollToPagescrollToPage(page_num: int)None
refreshPageBreaksrefreshPageBreaks()None

Content

MethodSignatureReturns
setTextsetText(raw_string: str, auto_render: bool = True)None
appendTextappendText(html: str, auto_render: bool = True)None
clearContentclearContent()None
getPlainTextgetPlainText(callback)None (async)
getHtmlContentgetHtmlContent(callback)None (async)
getPlainTextSyncgetPlainTextSync() -> strPlain text content
getHtmlContentSyncgetHtmlContentSync() -> strHTML content
getFullHtmlAsyncgetFullHtmlAsync() -> strComplete page HTML

Text Formatting

MethodSignatureReturns
setBoldsetBold(_: bool = True)None
setItalicsetItalic(_: bool = True)None
setUnderlinesetUnderline(_: bool = True)None
setStrikeOutsetStrikeOut(_: bool = True)None
setFontFamilysetFontFamily(family: str)None
setFontSizesetFontSize(size: int)None
setTextColorsetTextColor(color: QColor)None
setTextBackgroundColorsetTextBackgroundColor(color: QColor)None
chooseTextColorchooseTextColor()None
chooseBackgroundColorchooseBackgroundColor()None

Alignment & Direction

MethodSignatureReturns
setAlignmentsetAlignment(align: str)None
setParagraphDirectionsetParagraphDirection(rtl: bool)None

Insert

MethodSignatureReturns
insertImageinsertImage()None
insertMathinsertMath(latex: str, display: bool = False)None
insertMathDialoginsertMathDialog()None
insertTableinsertTable(rows: int, cols: int)None
insertTableDialoginsertTableDialog()None

Zoom

MethodSignatureReturns
zoomInzoomIn()None
zoomOutzoomOut()None
setZoomPercentsetZoomPercent(percent: int)None
fitPagefitPage()None

File Operations

MethodSignatureReturns
LoadFileDialogLoadFileDialog(caption, dir, filter, dialog_type)None
load_fileload_file(file_path: str = '')File data (str or base64)
save_filesave_file(file_path: str = "")None
savePdfsavePdf(filepath: str)None
saveWithPandocsaveWithPandoc(filePath: str)None
extractAsImagesextractAsImages()None
loadTextFileloadTextFile(auto_render=False)None
loadHtmlFileloadHtmlFile()None
loadDocxFileloadDocxFile()None
loadPdfFileloadPdfFile()None

Import Helpers

MethodSignatureReturns
is_native_documentis_native_document(html_content='') -> boolTrue if HTML is a native editor document
structure_synchronizationstructure_synchronization(imported_html: str) -> strSynchronised HTML
prepare_math_formulasprepare_math_formulas(source_html: str) -> strHTML with math placeholders

Note The editor uses document.execCommand() for basic formatting and custom JS for advanced features. For a complete list of reserved CSS classes (renamed during import), refer to the structure_synchronization() documentation.