Files
docview/qml/WebPane.qml
T
2026-09-21 13:41:40 +09:00

595 lines
31 KiB
QML

pragma ComponentBehavior: Bound
import QtQuick
import QtWebEngine
Item {
id: pane
clip: true
property var views: ({})
property var activeView: null
property var pendingView: null
property string searchQuery: ""
property var companionView: null
property var spreadSelection: ({})
property string failedCompanionUrl: ""
property bool arrangingPages: false
property real spreadZoom: 1
property bool spreadFitWidth: false
property real pagePanX: 0
property real pagePanY: 0
property real maxPagePanX: 0
property real maxPagePanY: 0
// Navigation requests expose a display string, while signal arguments and
// QML url properties can use percent encoding. Compare the same URL type.
function canonicalUrl(value) { return String(Qt.url(value)) }
// A spread always follows spine order; CSS direction does not reorder pages.
// Unspecified first pages occupy the trailing slot (the conventional cover).
function spreadFor(spine, current, rtl, landscape) {
var leading = rtl ? "right" : "left", trailing = rtl ? "left" : "right"
var groups = [], pending = null, first = true
function flush() { if (pending) { groups.push(pending); pending = null } }
for (var i = 0; i < spine.length; ++i) {
var row = spine[i]
if (row.linear === false) continue
if (row.missing || !row.href) { flush(); continue }
var policy = row.renditionSpread || "auto"
var eligible = row.layout === "fixed" && policy !== "none" && (policy === "both" || landscape)
if (!eligible || row.spread === "center") {
flush(); groups.push({center:i}); first = false; continue
}
var side = row.spread === "left" || row.spread === "right" ? row.spread : (pending ? trailing : (first ? trailing : leading))
first = false
if (side === leading) { flush(); pending = {}; pending[side] = i }
else { var group = pending || {}; group[side] = i; groups.push(group); pending = null }
}
flush()
for (var j = 0; j < groups.length; ++j)
if (groups[j].left === current || groups[j].right === current || groups[j].center === current) return groups[j]
return {center:current} // nonlinear items are independently navigable.
}
function resourcePath(url) {
return decodeURIComponent(String(url).replace(/^doc:\/\/[^/]+\//, "").split("#")[0])
}
function retireView(view) {
if (!view) return
resetHeartbeat(view)
view.alive = false
view.stop()
view.visible = false
if (typeof reader.registerRenderer === "function") reader.registerRenderer(view.documentToken, 0, view.rendererSlot)
view.destroy()
}
function closeCompanion() {
var old = companionView
companionView = null
if (old) retireView(old)
}
function synchronizeSpread() {
var view = activeView
if (!view || !view.alive || !view.documentReady) return
var opts = optionsFor(view), spine = opts.spine || [], current = -1
for (var i = 0; i < spine.length; ++i)
if (resourcePath(view.url) === decodeURIComponent(String(spine[i].href || "").split("#")[0])) { current = i; break }
spreadSelection = current >= 0 ? spreadFor(spine, current, !!opts.rtl, width > height) : ({center:-1})
var other = spreadSelection.left === current ? spreadSelection.right : spreadSelection.left
if (other === undefined || other === current || !view.fixedLayout || pendingView) {
closeCompanion(); arrangePages(); return
}
var url = "doc://" + view.documentToken + "/" + spine[other].href
if (canonicalUrl(url) === canonicalUrl(failedCompanionUrl) || !reader.navigationAllowed(url, WebEngineView.TypedNavigation, view.documentToken)) {
closeCompanion(); arrangePages(); return
}
if (!companionView) {
companionView = webComponent.createObject(pane, {profile:view.profile, documentToken:view.documentToken,
rendererSlot:1, isCompanion:true, permittedUrl:url})
if (!companionView) { arrangePages(); return }
companionView.url = url
} else if (canonicalUrl(companionView.url) !== canonicalUrl(url)) {
companionView.documentReady = false
companionView.pagePlaced = false
companionView.fixedViewport = ({})
companionView.permittedUrl = url
companionView.url = url
}
arrangePages()
}
function arrangePages() {
if (arrangingPages || !activeView) return
arrangingPages = true
var view = activeView, other = companionView
var a = view.fixedViewport, b = other ? other.fixedViewport : ({})
if (!view.fixedLayout || !(a.width > 0) || !(a.height > 0)) {
view.x = 0; view.y = 0; view.width = width; view.height = height; view.scale = 1
} else {
var currentLeft = spreadSelection.left !== undefined && resourcePath(view.url) === decodeURIComponent(String((optionsFor(view).spine || [])[spreadSelection.left].href).split("#")[0])
var paired = other && other.documentReady && b.width > 0 && b.height > 0
var slots = spreadSelection.center === undefined
var leftSize = paired && !currentLeft ? b : a, rightSize = paired && currentLeft ? b : a
var totalWidth = slots ? leftSize.width + rightSize.width : a.width
var totalHeight = paired ? Math.max(a.height, b.height) : a.height
var factor = (spreadFitWidth ? width / totalWidth : Math.min(width / totalWidth, height / totalHeight)) * spreadZoom
maxPagePanX = Math.max(0, totalWidth * factor - width)
maxPagePanY = Math.max(0, totalHeight * factor - height)
pagePanX = Math.max(0, Math.min(maxPagePanX, pagePanX))
pagePanY = Math.max(0, Math.min(maxPagePanY, pagePanY))
var origin = Math.max(0, (width - totalWidth * factor) / 2) - pagePanX
function place(page, size, left) {
page.zoomFactor = Math.max(0.25, Math.min(5, factor))
page.scale = factor / page.zoomFactor
page.width = size.width * page.zoomFactor; page.height = size.height * page.zoomFactor
page.x = origin + left * factor; page.y = Math.max(0, (height - size.height * factor) / 2) - pagePanY
page.pagePlaced = true
}
place(view, a, slots && !currentLeft ? leftSize.width : 0)
if (paired) place(other, b, currentLeft ? leftSize.width : 0)
}
arrangingPages = false
}
function collectResourceIssues(view) {
if (typeof reader.webResourceIssues !== "function") return
run(view, "resource.issues", {}, function(value) { if (value) reader.webResourceIssues(view.documentToken, value) })
}
WheelHandler {
target: null
enabled: !!pane.activeView && pane.activeView.fixedLayout
onWheel: function(event) {
pane.pagePanX -= event.angleDelta.x / 2
pane.pagePanY -= event.angleDelta.y / 2
pane.arrangePages()
}
}
onWidthChanged: Qt.callLater(synchronizeSpread)
onHeightChanged: Qt.callLater(synchronizeSpread)
function focusContent() {
if (activeView) activeView.forceActiveFocus()
}
function optionsFor(view) {
var opts = typeof reader.webOptions === "function" ? reader.webOptions(view.documentToken) : ({})
var reading = opts.reading || reader.settings.reading || ({})
var currentPath = decodeURIComponent(String(view.url).replace(/^doc:\/\/[^/]+\//, "").split("#")[0])
var spine = opts.spine || []
for (var i = 0; i < spine.length; ++i) {
if (decodeURIComponent(String(spine[i].href || "").split("#")[0]) === currentPath) { opts.fixed = spine[i].layout === "fixed"; break }
}
opts.publisherStyle = reading.use_publisher_style !== false
opts.fontSize = reading.font_size || 18
opts.lineHeight = reading.line_height || 1.6
return opts
}
function resetHeartbeat(view) {
if (view.heartbeatPending && typeof reader.finishRendererProbe === "function")
reader.finishRendererProbe(view.documentToken, view.rendererSlot, view.heartbeatPending)
view.heartbeatPending = 0
}
function trackRenderer(view, pid) {
if (!view || !view.alive || view.registeredRendererPid === pid || typeof reader.registerRenderer !== "function") return
resetHeartbeat(view)
view.registeredRendererPid = pid
reader.registerRenderer(view.documentToken, pid, view.rendererSlot)
}
function heartbeat(view) {
if (!view || !view.alive) return
// Qt can expose the initial process through the getter before delivering
// a PID notification. Reconcile once, without resetting healthy probes.
trackRenderer(view, view.renderProcessPid)
if (!view || !view.alive || view.heartbeatPending || view.renderProcessPid <= 0 ||
typeof reader.beginRendererProbe !== "function") return
var probe = reader.beginRendererProbe(view.documentToken, view.rendererSlot)
if (!probe) return
view.heartbeatPending = probe
var serial = view.documentSerial, token = view.documentToken
// The response requires the renderer main thread, including while the
// document is loading. Document JavaScript remains disabled.
view.runJavaScript(String(probe), WebEngineScript.ApplicationWorld, function(result) {
if (!view || !view.alive || view.documentSerial !== serial || view.documentToken !== token ||
view.heartbeatPending !== probe || result !== probe) return
reader.finishRendererProbe(token, view.rendererSlot, probe)
view.heartbeatPending = 0
// Polling work is queued only after the previous heartbeat returns;
// a stalled renderer cannot accumulate periodic DOM operations.
if (view === activeView) updateLocation(view)
collectResourceIssues(view)
})
}
function run(view, id, args, callback) {
if (!view || !view.alive || !view.documentReady) return
var serial = view.documentSerial
var token = view.documentToken
// Only JSON data is interpolated; all executable source is app-owned.
var data = JSON.stringify({id: id, args: args || ({})})
var script = "(function(data){return globalThis.__docviewReader ? globalThis.__docviewReader.command(data.id,data.args) : null;})(" + data + ")"
view.runJavaScript(script, WebEngineScript.ApplicationWorld, function(result) {
if (!view || !view.alive || view.documentSerial !== serial || view.documentToken !== token) return
if (callback) callback(result)
})
}
function updateLocation(view) {
run(view, "location", {}, function(value) {
if (!value || typeof value.url !== "string") return
value.zoom = (view.fixedLayout ? spreadZoom : view.zoomFactor) * 100
if (view.fixedLayout) {
value.fitWidth = spreadFitWidth
value.panX = maxPagePanX > 0 ? pagePanX / maxPagePanX : 0
value.panY = maxPagePanY > 0 ? pagePanY / maxPagePanY : 0
}
view.lastLocation = value
if (view === activeView) reader.webLocation(view.documentToken, value)
})
}
function configureNavigation(view) {
if (!view || view !== activeView || typeof reader.webNavigationTargets !== "function") return
var args = reader.webNavigationTargets(view.documentToken, view.url)
run(view, "navigation.configure", args, function() { updateLocation(view) })
}
function collectIndex(view) {
run(view, "index", {}, function(value) {
if (!value) return
if (view === activeView) reader.webIndex(view.documentToken, value)
if (view === activeView && value.truncated) reader.notify(qsTr("見出し一覧は先頭2,000件を表示しています"))
view.fixedViewport = value.fixedViewport || ({})
if (view.fixedLayout) arrangePages()
if (view === activeView) configureNavigation(view)
})
}
function fitFixed(view, widthOnly) {
var size = view.fixedViewport
if (!view.fixedLayout || !(size.width > 0) || !(size.height > 0)) return
var factor = widthOnly ? view.width / size.width : Math.min(view.width / size.width, view.height / size.height)
view.zoomFactor = Math.max(0.25, Math.min(5, factor))
}
function applyLayout(view, callback) {
if (!view || !view.documentReady) return
var opts = optionsFor(view)
var wasFixed = view.fixedLayout
view.fixedLayout = !!opts.fixed
if (wasFixed && !view.fixedLayout) view.zoomFactor = 1
run(view, "layout", opts, function(result) {
if (!result) return
view.fixedViewport = result.fixedViewport || ({})
if (view === activeView) synchronizeSpread()
else if (view.isCompanion) arrangePages()
if (callback) callback()
})
}
function restoreAfterLayout(view, saved, restoreDisplay, applied) {
if (!saved || !view || !view.alive) return
if (restoreDisplay) {
if (typeof saved.zoom === "number" && isFinite(saved.zoom) && saved.zoom >= 25 && saved.zoom <= 500) {
if (view.fixedLayout) spreadZoom = saved.zoom / 100
else view.zoomFactor = saved.zoom / 100
}
if (view.fixedLayout) {
spreadFitWidth = saved.fitWidth === true
arrangePages()
pagePanX = (saved.panX || 0) * maxPagePanX
pagePanY = (saved.panY || 0) * maxPagePanY
arrangePages()
}
}
run(view, "location.restore", saved, function(result) {
if (result && result.approximate) reader.notify(qsTr("E_LOCATOR_UNRESOLVED: 近い読書位置へ戻りました"))
if (applied) applied()
updateLocation(view)
})
}
function applyBoundary(view) {
if (!view || !view.documentReady || !view.pendingBoundary) return
var boundary = view.pendingBoundary
var request = view.pendingBoundaryRequest
view.pendingBoundary = ""
if (view !== activeView || request !== view.commandSerial) return
run(view, boundary, {}, function() {
if (view !== activeView || request !== view.commandSerial) return
reader.webApplied(view.documentToken, boundary, request)
updateLocation(view)
})
}
function command(id, args) {
var view = activeView
if (!view) return
var request = ++view.commandSerial
var operation = id === "navigate" && args.boundary ? args.boundary : id
reader.webStarted(view.documentToken, operation, request)
view.cancelResizeRestore()
view.restorePending = null // A newer user command supersedes a deferred restoration.
view.pendingBoundary = ""
if (id === "navigate") {
view.pendingBoundary = ["nav.document_start", "nav.document_end", "heading.first", "heading.last"].indexOf(args.boundary) >= 0 ? args.boundary : ""
view.pendingBoundaryRequest = request
view.permittedUrl = String(args.url)
var sameResource = canonicalUrl(view.url).split("#")[0] === canonicalUrl(args.url).split("#")[0]
// A following CFI restoration supplies the exact position. Avoid
// an identical-URL reload or fragment scroll overwriting that jump.
if (sameResource && args.preserveLocation) { view.permittedUrl = ""; return }
// Assigning an identical URL may reload the page. Keep an endpoint
// jump inside this DOM so a new load cannot consume its callback.
if (sameResource && view.documentReady && view.pendingBoundary) {
view.permittedUrl = ""
applyLayout(view, function() { applyBoundary(view) })
return
}
if (!sameResource) { view.documentReady = false; failedCompanionUrl = ""; pagePanX = 0; pagePanY = 0; closeCompanion() }
view.url = args.url
return
}
if (view.fixedLayout && id.indexOf("scroll.") === 0) {
var distance = id.indexOf("half_") >= 0 ? height / 2 : id.indexOf("page_") >= 0 ? height * 0.9 : 48
if (id === "scroll.left") pagePanX -= 48
else if (id === "scroll.right") pagePanX += 48
else if (id.indexOf("up") >= 0) pagePanY -= distance
else if (id.indexOf("down") >= 0) pagePanY += distance
arrangePages()
return
}
if (id === "location.restore") {
if (!view.documentReady) view.restorePending = args
else applyLayout(view, function() {
if (view === activeView && request === view.commandSerial)
restoreAfterLayout(view, args, true, function() {
if (view === activeView && request === view.commandSerial) reader.webApplied(view.documentToken, id, request)
})
})
return
}
if (id === "layout") { applyLayout(view); return }
if (id === "zoom.in" || id === "zoom.out" || id === "zoom.fit_width") {
if (view.fixedLayout) {
spreadFitWidth = id === "zoom.fit_width"
spreadZoom = spreadFitWidth ? 1 : Math.max(0.25, Math.min(5, spreadZoom * (id === "zoom.in" ? 1.15 : 1 / 1.15)))
arrangePages()
updateLocation(view)
return
}
var saved = view.lastLocation
if (id === "zoom.fit_width") {
if (view.fixedLayout) fitFixed(view, true)
else view.zoomFactor = 1
} else view.zoomFactor = Math.max(0.25, Math.min(5, view.zoomFactor * (id === "zoom.in" ? 1.15 : 1 / 1.15)))
Qt.callLater(function() {
if (view !== activeView || !view.alive || request !== view.commandSerial) return
if (saved) restoreAfterLayout(view, saved)
else updateLocation(view)
})
return
}
if (id === "search.cancel") {
++view.searchRevision
view.findText("")
return
}
if (id === "search" || id === "search.next" || id === "search.previous") {
if (id === "search") searchQuery = String(args.query || "").slice(0, 4096)
var serial = view.documentSerial
var revision = ++view.searchRevision
view.findText(searchQuery, id === "search.previous" ? WebEngineView.FindBackward : 0, function(found) {
if (!view || !view.alive || view !== activeView || view.documentSerial !== serial || view.searchRevision !== revision) return
if (!found) reader.notify(qsTr("一致する文字がありません"))
updateLocation(view)
})
return
}
run(view, id, args, function(result) {
if (view !== activeView || request !== view.commandSerial) return
if (typeof reader.webApplied === "function") reader.webApplied(view.documentToken, id, request)
if (id === "link.activate" && result) {
if (result.url && reader.navigationAllowed(result.url, WebEngineView.LinkClickedNavigation, view.documentToken))
pane.command("navigate", {url: result.url})
else if (result.blocked) reader.notify(qsTr("E_RESOURCE_BLOCKED: このリンクは開けません"))
else if (result.empty) reader.notify(qsTr("Tabでリンクを選択してください"))
} else if ((id === "link.next" || id === "link.previous") && result && result.empty)
reader.notify(qsTr("この文書にはリンクがありません"))
if (result && result.boundary) {
if (reader.format === "epub") {
reader.execute(result.boundary > 0 ? "nav.chapter_next" : "nav.chapter_previous",
{headingBoundary: result.boundary > 0 ? "heading.first" : "heading.last"})
} else reader.notify(result.empty ? qsTr("この文書には見出しがありません") : qsTr("見出しの端です"))
}
if (id === "location.restore" && result && result.approximate) reader.notify(qsTr("E_LOCATOR_UNRESOLVED: 近い読書位置へ戻りました"))
updateLocation(view)
})
}
Connections {
target: reader
function onStageWeb(profile, url, token) {
closeCompanion()
var view = webComponent.createObject(pane, {profile: profile, documentToken: token, permittedUrl: String(url)})
if (!view) { reader.webLoaded(token, false); return }
views[token] = view
pendingView = view
view.url = url
}
function onActivateWeb(token) {
if (activeView) { activeView.isCurrent = false; activeView.enabled = false }
activeView = views[token] || null
if (activeView) {
spreadZoom = 1; spreadFitWidth = false; pagePanX = 0; pagePanY = 0
activeView.isCurrent = true
activeView.enabled = true
if (pendingView === activeView) pendingView = null
Qt.callLater(function() { if (activeView && activeView.documentToken === token) { applyLayout(activeView); collectIndex(activeView) } })
}
}
function onDisposeWeb(token) {
var view = views[token]
if (!view) return
if (companionView && companionView.documentToken === token) closeCompanion()
if (activeView === view) activeView = null
if (pendingView === view) pendingView = null
delete views[token]
retireView(view)
if (activeView && !pendingView) Qt.callLater(synchronizeSpread)
}
function onWebCommand(id, args) { pane.command(id, args) }
function onNavigationChanged() { pane.configureNavigation(pane.activeView) }
function onSettingsChanged() {
if (!activeView) return
var view = activeView
run(view, "location", {}, function(saved) {
applyLayout(view, function() { restoreAfterLayout(view, saved) })
})
}
}
Timer {
interval: 400
running: !!pane.activeView || !!pane.pendingView || !!pane.companionView
repeat: true
onTriggered: {
pane.heartbeat(pane.activeView)
pane.heartbeat(pane.pendingView)
pane.heartbeat(pane.companionView)
}
}
Component {
id: webComponent
WebEngineView {
id: web
width: pane.width
height: pane.height
transformOrigin: Item.TopLeft
property int rendererSlot: 0
property bool isCompanion: false
property bool pagePlaced: false
property string documentToken: ""
property string permittedUrl: ""
property bool alive: true
property bool isCurrent: false
property bool documentReady: false
property int documentSerial: 0
property double heartbeatPending: 0
property double registeredRendererPid: 0
property int searchRevision: 0
property double commandSerial: 0
property double pendingBoundaryRequest: 0
property var lastLocation: null
property var resizeLocation: null
property var restorePending: null
property var fixedViewport: ({})
property bool fixedLayout: false
property string pendingBoundary: ""
opacity: isCurrent || (isCompanion && documentReady && pagePlaced) ? 1 : 0
z: isCurrent || isCompanion ? 1 : 0
enabled: isCurrent || (isCompanion && pagePlaced)
audioMuted: true
backgroundColor: "white"
webChannel: null
activeFocusOnPress: true
settings.javascriptEnabled: false
settings.javascriptCanOpenWindows: false
settings.javascriptCanAccessClipboard: false
settings.javascriptCanPaste: false
settings.localStorageEnabled: false
settings.localContentCanAccessRemoteUrls: false
settings.localContentCanAccessFileUrls: false
settings.hyperlinkAuditingEnabled: false
settings.pluginsEnabled: false
settings.fullScreenSupportEnabled: false
settings.screenCaptureEnabled: false
settings.webGLEnabled: false
settings.accelerated2dCanvasEnabled: false
settings.autoLoadIconsForPage: false
settings.touchIconsEnabled: false
settings.allowRunningInsecureContent: false
settings.allowGeolocationOnInsecureOrigins: false
settings.allowWindowActivationFromJavaScript: false
settings.dnsPrefetchEnabled: false
settings.pdfViewerEnabled: false
settings.navigateOnDropEnabled: false
settings.playbackRequiresUserGesture: true
settings.webRTCPublicInterfacesOnly: true
settings.unknownUrlSchemePolicy: WebEngineSettings.DisallowUnknownUrlSchemes
settings.focusOnNavigationEnabled: false
userScripts.collection: [{
name: "DocView Reader",
injectionPoint: WebEngineScript.DocumentCreation,
worldId: WebEngineScript.ApplicationWorld,
runsOnSubFrames: false,
sourceCode: reader.script("reader")
}]
onActiveFocusChanged: if (activeFocus) reader.context = "content"
onNavigationRequested: function(request) {
var type = request.navigationType
var known = type === WebEngineView.LinkClickedNavigation ||
((type === WebEngineView.OtherNavigation || type === WebEngineView.TypedNavigation) && pane.canonicalUrl(request.url) === pane.canonicalUrl(permittedUrl))
if (isCompanion && type === WebEngineView.LinkClickedNavigation) {
request.action = WebEngineView.IgnoreRequest
if (reader.navigationAllowed(request.url, type, documentToken)) pane.command("navigate", {url:request.url})
return
}
request.action = known && reader.navigationAllowed(request.url, type, documentToken)
? WebEngineView.AcceptRequest : WebEngineView.IgnoreRequest
if (known && type !== WebEngineView.LinkClickedNavigation) permittedUrl = ""
}
onLoadingChanged: function(info) {
if (info.status === WebEngineView.LoadStartedStatus) { pane.resetHeartbeat(web); ++documentSerial; documentReady = false }
else if (info.status === WebEngineView.LoadSucceededStatus) {
documentReady = true
if (!isCompanion) {
reader.webLoaded(documentToken, true)
if (typeof reader.webDocumentLoaded === "function") reader.webDocumentLoaded(documentToken, url)
}
pane.applyLayout(web, function() {
pane.applyBoundary(web)
if (restorePending) { var saved = restorePending; restorePending = null; pane.restoreAfterLayout(web, saved, true) }
})
pane.collectIndex(web)
pane.collectResourceIssues(web)
} else if (info.status === WebEngineView.LoadFailedStatus && alive) {
if (isCompanion) { pane.failedCompanionUrl = String(url); pane.closeCompanion(); pane.arrangePages() }
else reader.webLoaded(documentToken, false)
}
}
onNewWindowRequested: function(request) {
if (request.userInitiated && reader.navigationAllowed(request.requestedUrl, WebEngineView.LinkClickedNavigation, documentToken)) {
pane.command("navigate", {url:request.requestedUrl})
}
}
onJavaScriptConsoleMessage: function(level, message, lineNumber, sourceId) { /* Document text is not written to application logs. */ }
onPermissionRequested: function(permissionRequest) { permissionRequest.deny() }
onFeaturePermissionRequested: function(origin, feature) { grantFeaturePermission(origin, feature, false) }
onFileDialogRequested: function(request) { request.accepted = true; request.dialogReject() }
onColorDialogRequested: function(request) { request.accepted = true; request.dialogReject() }
onAuthenticationDialogRequested: function(request) { request.accepted = true; request.dialogReject() }
onJavaScriptDialogRequested: function(request) { request.accepted = true; request.dialogReject() }
onContextMenuRequested: function(request) { request.accepted = true }
onFullScreenRequested: function(request) { request.reject() }
onCertificateError: function(error) { error.rejectCertificate() }
onQuotaRequested: function(request) { request.reject() }
onRegisterProtocolHandlerRequested: function(request) { request.reject() }
onFileSystemAccessRequested: function(request) { request.reject() }
onSelectClientCertificate: function(selection) { selection.selectNone() }
onDesktopMediaRequested: function(request) { request.cancel() }
onWebAuthUxRequested: function(request) { request.cancel() }
onRenderProcessTerminated: function(status, code) {
if (!alive) return
documentReady = false
if (isCurrent || isCompanion) reader.renderStopped()
else reader.webLoaded(documentToken, false)
}
onRenderProcessPidChanged: function(pid) {
pane.trackRenderer(web, pid)
}
Timer {
id: resizeDelay
interval: 150
onTriggered: {
if (web.fixedLayout) pane.arrangePages()
else pane.restoreAfterLayout(web, web.resizeLocation)
web.resizeLocation = null
}
}
function preserveResizeLocation() {
if (!documentReady || pane.arrangingPages) return
if (!resizeDelay.running) resizeLocation = lastLocation
resizeDelay.restart()
}
function cancelResizeRestore() { resizeDelay.stop(); resizeLocation = null }
onWidthChanged: preserveResizeLocation()
onHeightChanged: preserveResizeLocation()
}
}
}