initial commit

This commit is contained in:
2026-09-21 13:41:40 +09:00
commit 855c7328df
411 changed files with 85352 additions and 0 deletions
+403
View File
@@ -0,0 +1,403 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import DocView 1.0
ApplicationWindow {
id: window
width: 1100; height: 800; minimumWidth: 480; minimumHeight: 360
visible: true
title: reader.title + (reader.title === "DocView" ? "" : " — DocView")
property bool dark: reader.settings.ui.theme === "system"
? Qt.styleHints.colorScheme !== Qt.ColorScheme.Light : reader.settings.ui.theme !== "light"
property color backgroundColor: dark ? "#181c20" : "#f1f3f4"
property color surfaceColor: dark ? "#22282e" : "#ffffff"
property color textColor: dark ? "#d8e0e8" : "#24303b"
property color mutedColor: dark ? "#97a4b1" : "#586775"
property color accentColor: dark ? "#abc8de" : "#215f8e"
property bool tocVisible: reader.settings.ui.toc_visible
property bool pagesVisible: reader.settings.ui.pages_visible
property bool statusVisible: reader.settings.ui.status_bar
property var collapsed: ({})
property var tocRows: []
readonly property int visibleCurrentOutline: {
var current = reader.currentOutline, visible = -1
for (var i = 0; i < tocRows.length; ++i) {
if (tocRows[i].originalIndex === current) return i
// A collapsed ancestor represents the current section below it.
if (tocRows[i].originalIndex < current && collapsed[tocRows[i].originalIndex]) {
var depth = tocRows[i].depth || 0, within = true
for (var j = tocRows[i].originalIndex + 1; j <= current; ++j)
if ((reader.outline[j].depth || 0) <= depth) { within = false; break }
if (within) visible = i
}
}
return visible
}
property bool searchInput: false
property string previousContext: ""
property var commandNames: ({
"file.open": qsTr("文書を開く"), "app.quit": qsTr("終了"),
"operation.cancel": qsTr("処理を取り消す"), "document.root.choose": qsTr("文書フォルダーを指定"),
"document.info": qsTr("文書情報"), "history.clear": qsTr("履歴を消去"),
"scroll.down": qsTr("下へスクロール"), "scroll.up": qsTr("上へスクロール"),
"scroll.left": qsTr("左へスクロール"), "scroll.right": qsTr("右へスクロール"),
"scroll.half_down": qsTr("半画面進む"), "scroll.half_up": qsTr("半画面戻る"),
"scroll.page_down": qsTr("一画面進む"), "scroll.page_up": qsTr("一画面戻る"),
"nav.document_start": qsTr("文書の先頭"), "nav.document_end": qsTr("文書の末尾"),
"nav.page": qsTr("指定ページへ移動"), "nav.page_next": qsTr("次のPDFページ"),
"nav.page_previous": qsTr("前のPDFページ"), "nav.heading_next": qsTr("次の見出し"),
"nav.heading_previous": qsTr("前の見出し"), "nav.chapter_next": qsTr("次の章"),
"nav.chapter_previous": qsTr("前の章"), "panel.toc.toggle": qsTr("目次の表示切替"),
"panel.pages.toggle": qsTr("ページ一覧の表示切替"), "panel.status.toggle": qsTr("ステータスの表示切替"),
"focus.next": qsTr("操作する領域を切替"), "focus.content": qsTr("本文に戻る"),
"zoom.in": qsTr("拡大"), "zoom.out": qsTr("縮小"), "zoom.fit_width": qsTr("幅に合わせる"),
"view.rotate": qsTr("PDFを回転"), "command.open": qsTr("コマンドを入力"),
"config.reload": qsTr("設定を再読込"), "help.toggle": qsTr("操作一覧"),
"nav.back": qsTr("移動履歴を戻る"), "nav.forward": qsTr("移動履歴を進む"),
"search.open": qsTr("本文を検索"), "search.next": qsTr("次の検索結果"),
"search.previous": qsTr("前の検索結果"), "panel.next": qsTr("次の項目を選択"),
"panel.previous": qsTr("前の項目を選択"), "panel.collapse": qsTr("階層を折りたたむ"),
"panel.expand": qsTr("階層を展開"), "panel.first": qsTr("先頭項目を選択"),
"panel.last": qsTr("末尾項目を選択"), "panel.half_down": qsTr("項目を半画面進む"),
"panel.half_up": qsTr("項目を半画面戻る"), "panel.activate": qsTr("選択項目へ移動"),
"link.next": qsTr("次のリンク・PDF注釈を選択"), "link.previous": qsTr("前のリンク・PDF注釈を選択"),
"link.activate": qsTr("選択リンク・PDF注釈を開く"), "unbound": qsTr("割り当てなし")
})
function contextName(value) {
return value === "content" ? qsTr("本文") : value === "toc" ? qsTr("目次")
: value === "pages" ? qsTr("ページ一覧") : qsTr("共通")
}
color: backgroundColor
font.family: "monospace"
font.pixelSize: reader.settings.ui.font_size
function focusContent() {
previousContext = "" // Explicit navigation takes precedence over temporary UI restoration.
reader.context = "content"
if (reader.format === "pdf") pdf.forceActiveFocus()
else if (reader.format !== "") web.focusContent()
else if (recentList.count > 0) recentList.forceActiveFocus()
else content.forceActiveFocus()
}
function beginTemporary() {
if (!previousContext) previousContext = reader.context
}
function endTemporary() {
reader.mode = "normal"
var target = previousContext
previousContext = ""
if (!target) return // A successful action already selected its own focus.
if (target === "toc" && tocVisible && tocList.count > 0) {
reader.context = "toc"; tocList.forceActiveFocus()
} else if (target === "pages" && pagesVisible && pageList.count > 0) {
reader.context = "pages"; pageList.forceActiveFocus()
} else focusContent()
}
function setInputMode(mode) { reader.mode = mode }
function updateToc(resetSelection) {
var previous = resetSelection ? null : tocRows[tocList.currentIndex]
let list = [], skipDepth = -1
for (let i = 0; i < reader.outline.length; ++i) {
let row = reader.outline[i], depth = row.depth || 0
if (skipDepth >= 0 && depth > skipDepth) continue
skipDepth = -1
let copy = Object.assign({}, row)
copy.originalIndex = i
copy.hasChildren = i + 1 < reader.outline.length && (reader.outline[i+1].depth || 0) > depth
list.push(copy)
if (collapsed[i]) skipDepth = depth
}
tocRows = list
var selected = 0
if (previous) {
for (var row = 0; row < list.length; ++row)
if (list[row].originalIndex === previous.originalIndex && list[row].title === previous.title &&
list[row].href === previous.href && list[row].page === previous.page) { selected = row; break }
}
tocList.currentIndex = list.length ? selected : -1
}
function panelCommand(id) {
let list = reader.context === "toc" ? tocList : pageList
if (list.count === 0) return
let next = list.currentIndex
if (id === "panel.next") next++
if (id === "panel.previous") next--
if (id === "panel.first") next = 0
if (id === "panel.last") next = list.count - 1
if (id === "panel.half_down") next += Math.max(1, Math.floor(list.height / 56))
if (id === "panel.half_up") next -= Math.max(1, Math.floor(list.height / 56))
if (id === "panel.activate") { reader.activateItem(list.model[list.currentIndex]); return }
if (reader.context === "toc" && (id === "panel.collapse" || id === "panel.expand")) {
let row = tocRows[list.currentIndex]
let c = Object.assign({}, collapsed)
if (id === "panel.collapse") {
if (row.hasChildren && !c[row.originalIndex]) c[row.originalIndex] = true
else for(let i = list.currentIndex-1; i >= 0; --i) if ((tocRows[i].depth || 0) < (row.depth || 0)) { next = i; break }
} else {
if (c[row.originalIndex]) delete c[row.originalIndex]
else if (row.hasChildren) next++
}
collapsed = c; updateToc()
}
list.currentIndex = Math.max(0, Math.min(list.count-1,next))
list.positionViewAtIndex(list.currentIndex,ListView.Contain)
}
function beginInput(search) {
beginTemporary(); searchInput = search
reader.mode = search ? "search" : "command"
commandField.text = ""; commandField.forceActiveFocus()
}
function recentCommand(id) {
if (!recentList.count || reader.state !== "Empty") return
if (id === "link.next" || id === "link.previous") {
if (clearRecent.activeFocus) recentList.forceActiveFocus()
else clearRecent.forceActiveFocus()
return
}
if (id === "link.activate") {
if (clearRecent.activeFocus) reader.execute("history.clear")
else if (recentList.currentIndex >= 0) reader.openRecent(recentList.model[recentList.currentIndex].documentId)
return
}
let next = recentList.currentIndex
if (id === "scroll.down") next++
if (id === "scroll.up") next--
if (id === "nav.document_start") next = 0
if (id === "nav.document_end") next = recentList.count - 1
if (id === "scroll.half_down" || id === "scroll.page_down") next += Math.max(1, Math.floor(recentList.height / 52))
if (id === "scroll.half_up" || id === "scroll.page_up") next -= Math.max(1, Math.floor(recentList.height / 52))
recentList.currentIndex = Math.max(0, Math.min(recentList.count - 1, next))
recentList.positionViewAtIndex(recentList.currentIndex, ListView.Contain)
recentList.forceActiveFocus()
}
function endInput() { endTemporary() }
Connections {
target: reader
function onNavigationChanged() { window.updateToc() }
function onDocumentChanged() { window.collapsed = ({}); window.updateToc(true) }
function onRecentDocumentsChanged() {
Qt.callLater(function() { if (reader.format === "" && reader.mode === "normal") window.focusContent() })
}
function onUiCommand(id,args) {
if(id === "file.open") { beginTemporary(); reader.mode = "dialog"; fileDialog.open() }
else if(id === "panel.toc.toggle") { tocVisible = !tocVisible; if(!tocVisible && reader.context === "toc") focusContent() }
else if(id === "panel.pages.toggle") { pagesVisible = !pagesVisible; if(!pagesVisible && reader.context === "pages") focusContent() }
else if(id === "panel.status.toggle") statusVisible = !statusVisible
else if(id.startsWith("panel.")) panelCommand(id)
else if(id === "focus.next") {
previousContext = ""; let areas=["content"]; if(tocVisible && reader.outline.length)areas.push("toc");if(pagesVisible && reader.pages.length)areas.push("pages")
reader.context=areas[(areas.indexOf(reader.context)+1)%areas.length]
if(reader.context === "toc")tocList.forceActiveFocus();else if(reader.context === "pages")pageList.forceActiveFocus();else focusContent()
}
else if(id === "focus.content") focusContent()
else if(id === "command.open") beginInput(false)
else if(id === "search.open") beginInput(true)
else if(id === "help.toggle") { beginTemporary(); reader.mode="dialog"; helpDialog.open() }
else if(id === "document.info") { beginTemporary(); infoText.text=args.text; infoDialog.title=args.title || qsTr("文書情報"); reader.mode="dialog"; infoDialog.open() }
else if(id === "document.root.choose") { beginTemporary(); reader.mode="dialog"; folderDialog.open() }
else if(id === "history.clear") { beginTemporary(); reader.mode="dialog"; historyDialog.open() }
else if(id === "recent.navigate") recentCommand(args.command)
}
function onPasswordNeeded(name) { beginTemporary(); reader.mode="dialog"; passwordDialog.title=name; passwordField.text="";passwordDialog.open();passwordField.forceActiveFocus() }
function onEntryNeeded(candidates) { beginTemporary(); candidateList.model=candidates; reader.mode="dialog"; entryDialog.open();candidateList.forceActiveFocus() }
function onExternalLinkRequested(url) { beginTemporary(); externalDialog.destination=url;reader.mode="dialog";externalDialog.open() }
function onSettingsChanged() {
tocVisible=reader.settings.ui.toc_visible;pagesVisible=reader.settings.ui.pages_visible;statusVisible=reader.settings.ui.status_bar
if ((!tocVisible && reader.context === "toc") || (!pagesVisible && reader.context === "pages")) focusContent()
}
}
ColumnLayout {
anchors.fill: parent; spacing: 0
RowLayout {
Layout.fillWidth: true; Layout.fillHeight: true; spacing: 1
ColumnLayout {
objectName: "navigationSidebar"
visible: (tocVisible && (reader.outline.length > 0 || reader.outlineLoading)) || (pagesVisible && reader.pages.length > 0)
readonly property real panelWidth: Math.min(reader.settings.ui.panel_width,window.width*0.4)
Layout.preferredWidth: panelWidth
Layout.minimumWidth: panelWidth
Layout.maximumWidth: panelWidth
Layout.fillWidth: false
Layout.fillHeight: true; spacing: 1
Rectangle {
visible: tocVisible && (reader.outline.length > 0 || reader.outlineLoading)
Layout.fillWidth:true;Layout.fillHeight:true;color:surfaceColor
border.color:reader.context === "toc" ? accentColor : surfaceColor
ColumnLayout {
anchors.fill:parent;anchors.margins:8;spacing:6
Label { objectName:"outlineTitle";text:reader.outlineTitle;color:mutedColor;font.pixelSize:12 }
Label { objectName:"outlineLoading";visible:reader.outlineLoading;text:qsTr("目次を読み込んでいます…");color:mutedColor;wrapMode:Text.Wrap;Layout.fillWidth:true;textFormat:Text.PlainText }
ListView {
id:tocList;objectName:"tocList";Layout.fillWidth:true;Layout.fillHeight:true;clip:true;model:window.tocRows;currentIndex:0
delegate: Rectangle {
required property var modelData;required property int index
objectName:"tocRow";property bool readingCurrent:window.visibleCurrentOutline === index
property bool keyboardSelected:tocList.currentIndex === index
width:ListView.view.width;height:32;color:tocList.currentIndex === index ? (dark ? "#35414b" : "#dce6ee") : "transparent"
border.width:keyboardSelected ? 1 : 0;border.color:mutedColor
Label { anchors.fill:parent;anchors.leftMargin:4+(modelData.depth||0)*14;verticalAlignment:Text.AlignVCenter;color:textColor;elide:Text.ElideRight;text:(readingCurrent ? "> " : " ")+(modelData.hasChildren ? (window.collapsed[modelData.originalIndex] ? "+ " : "− ") : " ")+(modelData.title||qsTr("見出し"));textFormat:Text.PlainText }
MouseArea { anchors.fill:parent;onClicked:{tocList.currentIndex=index;reader.context="toc";reader.activateItem(modelData)} }
}
ScrollBar.vertical:ScrollBar{}
Accessible.name:qsTr("目次一覧")
}
}
}
Rectangle {
visible:pagesVisible && reader.pages.length > 0
Layout.fillWidth:true;Layout.fillHeight:true;color:surfaceColor
border.color:reader.context === "pages" ? accentColor : surfaceColor
ColumnLayout {
anchors.fill:parent;anchors.margins:8;spacing:6
Label{text:reader.listTitle;color:mutedColor;font.pixelSize:12}
ListView {
id:pageList;Layout.fillWidth:true;Layout.fillHeight:true;clip:true;model:reader.pages;currentIndex:0
delegate:Rectangle {
required property var modelData;required property int index
width:ListView.view.width;height:32;color:pageList.currentIndex === index ? (dark ? "#35414b" : "#dce6ee") : "transparent"
Label{anchors.fill:parent;anchors.leftMargin:4;verticalAlignment:Text.AlignVCenter;color:textColor;elide:Text.ElideRight;text:(reader.format === "pdf" && pdf.currentPage === index ? "> " : " ")+(modelData.title||modelData.href||String(index+1));textFormat:Text.PlainText}
MouseArea{anchors.fill:parent;onClicked:{pageList.currentIndex=index;reader.context="pages";reader.activateItem(modelData)}}
}
ScrollBar.vertical:ScrollBar{}
Accessible.name:reader.listTitle
}
}
}
}
Item {
id:content;objectName:"documentContent";Layout.fillWidth:true;Layout.fillHeight:true;focus:true
PdfCanvas { id:pdf;anchors.fill:parent;visible:reader.format === "pdf";backgroundColor:window.backgroundColor;Component.onCompleted:reader.attachCanvas(pdf);Accessible.name:qsTr("PDF本文") }
WebPane { id:web;anchors.fill:parent;visible:reader.format !== "pdf" && reader.format !== "" }
ColumnLayout {
id:initialScreen;objectName:"initialScreen"
readonly property real availableHeight:content.height-(noticeBanner.visible ? noticeBanner.height : 0)
anchors.centerIn:parent;width:Math.min(580,parent.width-48);spacing:8;visible:reader.format === ""
anchors.verticalCenterOffset:noticeBanner.visible ? -noticeBanner.height/2 : 0
Label { text:qsTr("Ctrl+O: 文書を開く");color:textColor;Layout.fillWidth:true }
Label { text:qsTr("?: 操作一覧");color:mutedColor;Layout.fillWidth:true }
Label { visible:recentList.count>0;text:qsTr("最近使った文書");color:textColor;Layout.topMargin:12;Layout.fillWidth:true }
ListView {
id:recentList;objectName:"recentDocumentsList"
readonly property real rowHeight: Math.max(52, window.font.pixelSize+34)
Layout.fillWidth:true;Layout.preferredHeight:Math.min(260,Math.max(rowHeight,initialScreen.availableHeight-4*window.font.pixelSize-154),count*rowHeight)
visible:count>0;enabled:reader.state === "Empty";clip:true
model:reader.recentDocuments;currentIndex:0;keyNavigationEnabled:true
onCountChanged:currentIndex=count>0 ? Math.max(0,Math.min(currentIndex,count-1)) : -1
Keys.onPressed:event=>{
if (event.key === Qt.Key_Home) currentIndex=0
else if (event.key === Qt.Key_End) currentIndex=count-1
else return
positionViewAtIndex(currentIndex,ListView.Contain);event.accepted=true
}
delegate:ItemDelegate {
required property var modelData;required property int index
width:ListView.view.width;height:recentList.rowHeight;padding:6;focusPolicy:Qt.NoFocus
highlighted:recentList.currentIndex===index
background:Rectangle {
color:recentList.currentIndex===index ? (dark ? "#35414b" : "#dce6ee") : surfaceColor
border.color:recentList.activeFocus && recentList.currentIndex===index ? accentColor : "transparent"
}
contentItem:Column {
Label { width:parent.width;text:modelData.title;textFormat:Text.PlainText;color:textColor;elide:Text.ElideRight }
Label { width:parent.width;text:modelData.path;textFormat:Text.PlainText;color:mutedColor;font.pixelSize:12;elide:Text.ElideMiddle }
}
Accessible.name:modelData.title+" — "+modelData.path
onClicked: { recentList.currentIndex=index;recentList.forceActiveFocus();reader.openRecent(modelData.documentId) }
}
ScrollBar.vertical:ScrollBar{}
Accessible.name:qsTr("最近使った文書。上下キーで選択し、Enterで開きます")
}
RowLayout {
visible:recentList.count>0;Layout.fillWidth:true
Label { text:qsTr("↑↓: 選択 Enter: 開く");color:mutedColor;font.pixelSize:12;Layout.fillWidth:true }
Button { id:clearRecent;objectName:"clearRecentDocuments";text:qsTr("履歴を消去…");enabled:reader.state === "Empty";onClicked:reader.execute("history.clear") }
}
}
Rectangle {
id:noticeBanner;objectName:"noticeBanner"
anchors.left:parent.left;anchors.right:parent.right;anchors.bottom:parent.bottom
visible:reader.notice.length > 0;height:noticeLabel.implicitHeight+20;color:surfaceColor
Label { id:noticeLabel;anchors.left:parent.left;anchors.right:parent.right;anchors.margins:12;anchors.verticalCenter:parent.verticalCenter;text:reader.notice;textFormat:Text.PlainText;wrapMode:Text.Wrap;color:textColor;Accessible.role:Accessible.AlertMessage }
}
DropArea { anchors.fill:parent;onDropped:drop=>{if(drop.hasUrls){reader.open(drop.urls[0]);drop.acceptProposedAction()}} }
}
}
Rectangle {
visible:statusVisible;Layout.fillWidth:true;Layout.preferredHeight:28;color:surfaceColor
RowLayout {
anchors.fill:parent;anchors.leftMargin:10;anchors.rightMargin:10
Label { Layout.fillWidth:true;text:reader.title;elide:Text.ElideMiddle;color:mutedColor;textFormat:Text.PlainText }
Label { text:reader.state === "Opening" ? qsTr("読込中") : reader.position;color:textColor }
Label { text:reader.pending;color:accentColor }
Label { text:reader.context === "content" ? qsTr("内容") : reader.context === "toc" ? qsTr("目次") : reader.listTitle;color:mutedColor;leftPadding:18 }
}
}
RowLayout {
visible:reader.mode === "command" || reader.mode === "search"
Layout.fillWidth:true;spacing:0
Label { text:window.searchInput ? "/" : ":";color:textColor;leftPadding:10 }
TextField {
id:commandField;Layout.fillWidth:true;selectByMouse:true;color:textColor;background:Rectangle{color:surfaceColor}Accessible.name:window.searchInput ? qsTr("本文検索") : qsTr("コマンド")
placeholderText:window.searchInput ? (reader.format === "epub" ? qsTr("表示中の章を検索") : reader.format === "pdf" ? qsTr("本文を検索") : qsTr("表示中のHTMLを検索")) : ""
onAccepted:{if(inputMethodComposing)return;if(window.searchInput){reader.search(text);endInput()}else if(reader.command(text)){if(reader.mode === "command")endInput();else text=""}}
Keys.onEscapePressed:event=>{if(!inputMethodComposing){endInput();event.accepted=true}}
}
}
}
FileDialog { id:fileDialog;title:qsTr("文書を開く");nameFilters:[qsTr("文書 (*.pdf *.html *.htm *.xhtml *.zip *.epub)"),qsTr("すべて (*)")];onAccepted:{reader.mode="normal";reader.open(selectedFile);endTemporary()}onRejected:endTemporary() }
FolderDialog { id:folderDialog;title:qsTr("文書フォルダーを指定");onAccepted:{reader.mode="normal";reader.setHtmlRoot(selectedFolder);endTemporary()}onRejected:endTemporary() }
Dialog {
id:passwordDialog;anchors.centerIn:parent;modal:true;standardButtons:Dialog.Ok|Dialog.Cancel
TextField{id:passwordField;width:300;echoMode:TextInput.Password;Accessible.name:qsTr("PDFパスワード");onAccepted:if(!inputMethodComposing)passwordDialog.accept()}
onAccepted:{reader.mode="normal";let value=passwordField.text;passwordField.text="";reader.password(value);endTemporary()}
onRejected:{passwordField.text="";reader.mode="normal";reader.cancel();endTemporary()}
}
Dialog {
id:entryDialog;objectName:"entryDialog";anchors.centerIn:parent;modal:true;title:qsTr("入口HTMLを選択");width:Math.min(600,window.width-48);height:Math.min(440,window.height-60);standardButtons:Dialog.Ok|Dialog.Cancel
ListView{id:candidateList;objectName:"entryCandidates";anchors.fill:parent;clip:true;currentIndex:0;delegate:ItemDelegate{required property string modelData;required property int index;width:ListView.view.width;text:modelData;highlighted:candidateList.currentIndex===index;onClicked:{candidateList.currentIndex=index;entryDialog.accept()}}Keys.onReturnPressed:entryDialog.accept()}
onAccepted:{reader.mode="normal";if(candidateList.currentIndex>=0)reader.chooseEntry(candidateList.model[candidateList.currentIndex]);endTemporary()}
onRejected:{reader.cancel();endTemporary()}
}
Dialog {
id:externalDialog;property url destination;anchors.centerIn:parent;modal:true;title:qsTr("外部ブラウザーで開く");width:Math.min(640,window.width-48);standardButtons:Dialog.Open|Dialog.Cancel
Label{width:parent.width;text:externalDialog.destination.toString();textFormat:Text.PlainText;wrapMode:Text.WrapAnywhere}
onAccepted:{reader.openExternal(destination);endTemporary()}
onRejected:endTemporary()
}
Dialog {
id:infoDialog;objectName:"infoDialog";anchors.centerIn:parent;modal:true;title:qsTr("文書情報")
width:Math.min(640,window.width-48);height:Math.min(implicitHeight,window.height-60)
standardButtons:Dialog.Close
contentItem:ScrollView {
id:infoScroll;objectName:"infoScroll";clip:true
contentWidth:availableWidth;implicitHeight:infoText.implicitHeight
Label{id:infoText;objectName:"infoText";width:infoScroll.availableWidth;wrapMode:Text.WrapAnywhere;textFormat:Text.PlainText}
}
onClosed:endTemporary()
}
Dialog {
id:historyDialog;objectName:"historyDialog";anchors.centerIn:parent;modal:true
title:qsTr("履歴と保存位置を消去");width:Math.min(580,window.width-48);standardButtons:Dialog.Ok|Dialog.Cancel
Label { id:historyConfirmation;objectName:"historyConfirmation";width:parent.width;wrapMode:Text.Wrap }
onAboutToShow:historyConfirmation.text=qsTr("%1 件の履歴と読書位置を消去します。原本は変更されません。").arg(reader.historyCount())
onAccepted:{reader.clearHistory();endTemporary()}
onRejected:endTemporary()
}
Dialog {
id:helpDialog;anchors.centerIn:parent;modal:true;title:qsTr("操作一覧");width:Math.min(760,window.width-48);height:Math.min(620,window.height-60);standardButtons:Dialog.Close
ColumnLayout {
anchors.fill:parent
Label{text:qsTr("Vim風の閲覧操作です。q単押しでは終了しません。:q で終了します。");wrapMode:Text.Wrap;Layout.fillWidth:true}
ListView {
Layout.fillWidth:true;Layout.fillHeight:true;clip:true;model:reader.settings.keybindings
delegate:Label{required property var modelData;width:ListView.view.width;text:modelData.keys.join(" ")+" "+(window.commandNames[modelData.command] || modelData.command)+" ["+window.contextName(modelData.context)+"]";textFormat:Text.PlainText;font.family:"monospace";height:26;elide:Text.ElideRight}
ScrollBar.vertical:ScrollBar{}
}
}
onClosed:endTemporary()
}
Shortcut { sequence:"?";enabled:helpDialog.visible;onActivated:helpDialog.close() }
Component.onCompleted:{updateToc();focusContent()}
}
+594
View File
@@ -0,0 +1,594 @@
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()
}
}
}