initial commit
This commit is contained in:
+403
@@ -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
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user