Tab controllers and pages
Every card has exactly one TabController
(engine/tab_controller.py). EngineRegistry creates it when a Tab
is added to BrowserState and disposes it when the tab is removed. The controller owns:
- a
BrowserPage, theQWebEnginePagesubclass, in the space's profile; - a per-page
PageInterceptorthat blocks requests and counts them for this card; - a
QWebChannelwith aPageBridgeobject, registered in JBrowser's isolated script world, for password capture and autofill.
The widget that shows the page (WebCard with its QWebEngineView) belongs to the UI and only attaches to
controller.page. The controller never touches widgets; it writes to its Tab and emits signals.
From engine events to state#
_connect() wires the page's signals:
| Page signal | Result |
|---|---|
urlChanged |
tab.url, secure, back/forward state; per-site zoom; saved-login count; history (after 0.9 s for single-page apps) |
titleChanged, iconChanged |
tab.title (also in history), tab.icon (and the favicon cache, not for incognito) |
loadStarted, loadProgress, loadFinished |
loading, progress; a visit in history; the HTTPS-first fallback on failure |
recentlyAudibleChanged, audioMutedChanged |
audible, muted |
lifecycleStateChanged |
sleeping (discarded) and throttled (frozen) |
renderProcessTerminated |
crashed, and a Reload info bar |
permissionRequested |
an Allow / Block info bar |
certificateError |
a Back to safety / Proceed anyway info bar (or a silent fallback to http, when JBrowser chose https) |
authenticationRequired, proxyAuthenticationRequired |
a sign-in dialog through ctx.hooks |
fullScreenRequested, desktopMediaRequested, printRequested, windowCloseRequested, findTextFinished, linkHovered |
re-emitted for the UI |
All writes go through tab.update(...), so they are coalesced once per frame
(the update pipeline).
Info bars#
Anything that needs the user's answer inside a card is an info bar: the controller emits
infobar(InfoBarSpec) and later infobarClosed(key); the card shows it as an InfoBarWidget.
InfoBarSpec (models/infobar.py)
carries a key (one bar per key), the text, an icon, a kind (info, warning, danger), InfoAction buttons, an
optional timeout, what dismissing means (on_dismiss) and whether it survives navigation. Permission prompts,
certificate errors, Save password?, crashes, dangerous sites and external-protocol links all use it.
Engine requests that are answered later (a permission, a deferred certificate error) are copied and kept in
_pending, because the signal's argument is a temporary. dispose() denies whatever is still pending.
Navigation rules in BrowserPage#
acceptNavigationRequest() runs before every navigation:
- External protocols (
mailto:,tel:,zoommtg:, … or any unknown scheme in the main frame) are not loaded; an info bar offers to open them with the registered Windows app. - Known phishing and malware hosts are refused. For a main-frame navigation the dangerous-site page is shown one event-loop turn later, because starting a navigation from inside this callback aborts Chromium (safe browsing).
- Everything else is left to Chromium.
createWindow() is forwarded to TabController.create_window() (popups).
Loading lazily#
A restored card has a URL and a saved history but no page load. ensure_loaded() restores the history with
QDataStream (which also navigates) or loads the URL, the first time the card is shown or woken. history_bytes()
does the reverse for the session and the Archive.
Lifecycle operations#
The lifecycle manager decides; the controller executes:
| Method | Does |
|---|---|
probe(callback) |
asks the page (main world) whether it plays media, has unsaved input, open WebSockets or WebRTC calls or is in full screen; None after 2 s |
throttle(freeze) |
page.setVisible(False) and, when Chromium agrees, LifecycleState.Frozen |
unthrottle(render_visible) |
back to Active and visible |
discard() |
LifecycleState.Discarded, only when recommendedState() allows it |
wake() |
back to Active, loading the page if it never loaded |
Other responsibilities#
Zoom (steps from 25 % to 500 %, remembered per host in zoom.sites, never for incognito), find in page, mute,
saved-login lookup and autofill (password vault), Clear site data and recording
visits in history (never for incognito spaces).