1/**************************************************************************** 2** 3** Copyright (C) 2020 The Qt Company Ltd. 4** Contact: http://www.qt.io/licensing/ 5** 6** This file is part of the QtPDF module of the Qt Toolkit. 7** 8** $QT_BEGIN_LICENSE:LGPL3$ 9** Commercial License Usage 10** Licensees holding valid commercial Qt licenses may use this file in 11** accordance with the commercial license agreement provided with the 12** Software or, alternatively, in accordance with the terms contained in 13** a written agreement between you and The Qt Company. For licensing terms 14** and conditions see http://www.qt.io/terms-conditions. For further 15** information use the contact form at http://www.qt.io/contact-us. 16** 17** GNU Lesser General Public License Usage 18** Alternatively, this file may be used under the terms of the GNU Lesser 19** General Public License version 3 as published by the Free Software 20** Foundation and appearing in the file LICENSE.LGPLv3 included in the 21** packaging of this file. Please review the following information to 22** ensure the GNU Lesser General Public License version 3 requirements 23** will be met: https://www.gnu.org/licenses/lgpl.html. 24** 25** GNU General Public License Usage 26** Alternatively, this file may be used under the terms of the GNU 27** General Public License version 2.0 or later as published by the Free 28** Software Foundation and appearing in the file LICENSE.GPL included in 29** the packaging of this file. Please review the following information to 30** ensure the GNU General Public License version 2.0 requirements will be 31** met: http://www.gnu.org/licenses/gpl-2.0.html. 32** 33** $QT_END_LICENSE$ 34** 35****************************************************************************/ 36import QtQuick 2.14 37import QtQuick.Controls 2.14 38import QtQuick.Layouts 1.14 39import QtQuick.Pdf 5.15 40import QtQuick.Shapes 1.14 41import QtQuick.Window 2.14 42 43Item { 44 // public API 45 // TODO 5.15: required property 46 property var document: undefined 47 property bool debug: false 48 49 property string selectedText 50 function selectAll() { 51 var currentItem = tableHelper.itemAtCell(tableHelper.cellAtPos(root.width / 2, root.height / 2)) 52 if (currentItem) 53 currentItem.selection.selectAll() 54 } 55 function copySelectionToClipboard() { 56 var currentItem = tableHelper.itemAtCell(tableHelper.cellAtPos(root.width / 2, root.height / 2)) 57 if (debug) 58 console.log("currentItem", currentItem, "sel", currentItem.selection.text) 59 if (currentItem) 60 currentItem.selection.copyToClipboard() 61 } 62 63 // page navigation 64 property alias currentPage: navigationStack.currentPage 65 property alias backEnabled: navigationStack.backAvailable 66 property alias forwardEnabled: navigationStack.forwardAvailable 67 function back() { navigationStack.back() } 68 function forward() { navigationStack.forward() } 69 function goToPage(page) { 70 if (page === navigationStack.currentPage) 71 return 72 goToLocation(page, Qt.point(-1, -1), 0) 73 } 74 function goToLocation(page, location, zoom) { 75 if (zoom > 0) { 76 navigationStack.jumping = true // don't call navigationStack.update() because we will push() instead 77 root.renderScale = zoom 78 tableView.forceLayout() // but do ensure that the table layout is correct before we try to jump 79 navigationStack.jumping = false 80 } 81 navigationStack.push(page, location, zoom) // actually jump 82 } 83 property vector2d jumpLocationMargin: Qt.vector2d(10, 10) // px from top-left corner 84 property int currentPageRenderingStatus: Image.Null 85 86 // page scaling 87 property real renderScale: 1 88 property real pageRotation: 0 89 function resetScale() { root.renderScale = 1 } 90 function scaleToWidth(width, height) { 91 root.renderScale = width / (tableView.rot90 ? tableView.firstPagePointSize.height : tableView.firstPagePointSize.width) 92 } 93 function scaleToPage(width, height) { 94 var windowAspect = width / height 95 var pageAspect = tableView.firstPagePointSize.width / tableView.firstPagePointSize.height 96 if (tableView.rot90) { 97 if (windowAspect > pageAspect) { 98 root.renderScale = height / tableView.firstPagePointSize.width 99 } else { 100 root.renderScale = width / tableView.firstPagePointSize.height 101 } 102 } else { 103 if (windowAspect > pageAspect) { 104 root.renderScale = height / tableView.firstPagePointSize.height 105 } else { 106 root.renderScale = width / tableView.firstPagePointSize.width 107 } 108 } 109 } 110 111 // text search 112 property alias searchModel: searchModel 113 property alias searchString: searchModel.searchString 114 function searchBack() { --searchModel.currentResult } 115 function searchForward() { ++searchModel.currentResult } 116 117 id: root 118 PdfStyle { id: style } 119 TableView { 120 id: tableView 121 anchors.fill: parent 122 anchors.leftMargin: 2 123 model: modelInUse && root.document !== undefined ? root.document.pageCount : 0 124 // workaround to make TableView do scheduleRebuildTable(RebuildOption::All) in cases when forceLayout() doesn't 125 property bool modelInUse: true 126 function rebuild() { 127 modelInUse = false 128 modelInUse = true 129 } 130 // end workaround 131 rowSpacing: 6 132 property real rotationNorm: Math.round((360 + (root.pageRotation % 360)) % 360) 133 property bool rot90: rotationNorm == 90 || rotationNorm == 270 134 onRot90Changed: forceLayout() 135 property size firstPagePointSize: document === undefined ? Qt.size(0, 0) : document.pagePointSize(0) 136 property real pageHolderWidth: Math.max(root.width, document === undefined ? 0 : 137 (rot90 ? document.maxPageHeight : document.maxPageWidth) * root.renderScale) 138 contentWidth: document === undefined ? 0 : pageHolderWidth + vscroll.width + 2 139 rowHeightProvider: function(row) { return (rot90 ? document.pagePointSize(row).width : document.pagePointSize(row).height) * root.renderScale } 140 TableViewExtra { 141 id: tableHelper 142 tableView: tableView 143 } 144 delegate: Rectangle { 145 id: pageHolder 146 color: root.debug ? "beige" : "transparent" 147 Text { 148 visible: root.debug 149 anchors { right: parent.right; verticalCenter: parent.verticalCenter } 150 rotation: -90; text: pageHolder.width.toFixed(1) + "x" + pageHolder.height.toFixed(1) + "\n" + 151 image.width.toFixed(1) + "x" + image.height.toFixed(1) 152 } 153 implicitWidth: tableView.pageHolderWidth 154 implicitHeight: tableView.rot90 ? image.width : image.height 155 property alias selection: selection 156 Rectangle { 157 id: paper 158 width: image.width 159 height: image.height 160 rotation: root.pageRotation 161 anchors.centerIn: pinch.active ? undefined : parent 162 property size pagePointSize: document.pagePointSize(index) 163 property real pageScale: image.paintedWidth / pagePointSize.width 164 Image { 165 id: image 166 source: document.source 167 currentFrame: index 168 asynchronous: true 169 fillMode: Image.PreserveAspectFit 170 width: paper.pagePointSize.width * root.renderScale 171 height: paper.pagePointSize.height * root.renderScale 172 property real renderScale: root.renderScale 173 property real oldRenderScale: 1 174 onRenderScaleChanged: { 175 image.sourceSize.width = paper.pagePointSize.width * renderScale 176 image.sourceSize.height = 0 177 paper.scale = 1 178 searchHighlights.update() 179 } 180 onStatusChanged: { 181 if (index === navigationStack.currentPage) 182 root.currentPageRenderingStatus = status 183 } 184 } 185 Shape { 186 anchors.fill: parent 187 visible: image.status === Image.Ready 188 onVisibleChanged: searchHighlights.update() 189 ShapePath { 190 strokeWidth: -1 191 fillColor: style.pageSearchResultsColor 192 scale: Qt.size(paper.pageScale, paper.pageScale) 193 PathMultiline { 194 id: searchHighlights 195 function update() { 196 // paths could be a binding, but we need to be able to "kick" it sometimes 197 paths = searchModel.boundingPolygonsOnPage(index) 198 } 199 } 200 } 201 Connections { 202 target: searchModel 203 // whenever the highlights on the _current_ page change, they actually need to change on _all_ pages 204 // (usually because the search string has changed) 205 function onCurrentPageBoundingPolygonsChanged() { searchHighlights.update() } 206 } 207 ShapePath { 208 strokeWidth: -1 209 fillColor: style.selectionColor 210 scale: Qt.size(paper.pageScale, paper.pageScale) 211 PathMultiline { 212 paths: selection.geometry 213 } 214 } 215 } 216 Shape { 217 anchors.fill: parent 218 visible: image.status === Image.Ready && searchModel.currentPage === index 219 ShapePath { 220 strokeWidth: style.currentSearchResultStrokeWidth 221 strokeColor: style.currentSearchResultStrokeColor 222 fillColor: "transparent" 223 scale: Qt.size(paper.pageScale, paper.pageScale) 224 PathMultiline { 225 paths: searchModel.currentResultBoundingPolygons 226 } 227 } 228 } 229 PinchHandler { 230 id: pinch 231 minimumScale: 0.1 232 maximumScale: root.renderScale < 4 ? 2 : 1 233 minimumRotation: root.pageRotation 234 maximumRotation: root.pageRotation 235 enabled: image.sourceSize.width < 5000 236 onActiveChanged: 237 if (active) { 238 paper.z = 10 239 } else { 240 paper.z = 0 241 var centroidInPoints = Qt.point(pinch.centroid.position.x / root.renderScale, 242 pinch.centroid.position.y / root.renderScale) 243 var centroidInFlickable = tableView.mapFromItem(paper, pinch.centroid.position.x, pinch.centroid.position.y) 244 var newSourceWidth = image.sourceSize.width * paper.scale 245 var ratio = newSourceWidth / image.sourceSize.width 246 if (root.debug) 247 console.log("pinch ended on page", index, "with centroid", pinch.centroid.position, centroidInPoints, "wrt flickable", centroidInFlickable, 248 "page at", pageHolder.x.toFixed(2), pageHolder.y.toFixed(2), 249 "contentX/Y were", tableView.contentX.toFixed(2), tableView.contentY.toFixed(2)) 250 if (ratio > 1.1 || ratio < 0.9) { 251 var centroidOnPage = Qt.point(centroidInPoints.x * root.renderScale * ratio, centroidInPoints.y * root.renderScale * ratio) 252 paper.scale = 1 253 paper.x = 0 254 paper.y = 0 255 root.renderScale *= ratio 256 tableView.forceLayout() 257 if (tableView.rotationNorm == 0) { 258 tableView.contentX = pageHolder.x + tableView.originX + centroidOnPage.x - centroidInFlickable.x 259 tableView.contentY = pageHolder.y + tableView.originY + centroidOnPage.y - centroidInFlickable.y 260 } else if (tableView.rotationNorm == 90) { 261 tableView.contentX = pageHolder.x + tableView.originX + image.height - centroidOnPage.y - centroidInFlickable.x 262 tableView.contentY = pageHolder.y + tableView.originY + centroidOnPage.x - centroidInFlickable.y 263 } else if (tableView.rotationNorm == 180) { 264 tableView.contentX = pageHolder.x + tableView.originX + image.width - centroidOnPage.x - centroidInFlickable.x 265 tableView.contentY = pageHolder.y + tableView.originY + image.height - centroidOnPage.y - centroidInFlickable.y 266 } else if (tableView.rotationNorm == 270) { 267 tableView.contentX = pageHolder.x + tableView.originX + centroidOnPage.y - centroidInFlickable.x 268 tableView.contentY = pageHolder.y + tableView.originY + image.width - centroidOnPage.x - centroidInFlickable.y 269 } 270 if (root.debug) 271 console.log("contentX/Y adjusted to", tableView.contentX.toFixed(2), tableView.contentY.toFixed(2), "y @top", pageHolder.y) 272 tableView.returnToBounds() 273 } 274 } 275 grabPermissions: PointerHandler.CanTakeOverFromAnything 276 } 277 DragHandler { 278 id: textSelectionDrag 279 acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus 280 target: null 281 } 282 TapHandler { 283 id: mouseClickHandler 284 acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus 285 } 286 TapHandler { 287 id: touchTapHandler 288 acceptedDevices: PointerDevice.TouchScreen 289 onTapped: { 290 selection.clear() 291 selection.forceActiveFocus() 292 } 293 } 294 Repeater { 295 model: PdfLinkModel { 296 id: linkModel 297 document: root.document 298 page: image.currentFrame 299 } 300 delegate: Shape { 301 x: rect.x * paper.pageScale 302 y: rect.y * paper.pageScale 303 width: rect.width * paper.pageScale 304 height: rect.height * paper.pageScale 305 visible: image.status === Image.Ready 306 ShapePath { 307 strokeWidth: style.linkUnderscoreStrokeWidth 308 strokeColor: style.linkUnderscoreColor 309 strokeStyle: style.linkUnderscoreStrokeStyle 310 dashPattern: style.linkUnderscoreDashPattern 311 startX: 0; startY: height 312 PathLine { x: width; y: height } 313 } 314 MouseArea { // TODO switch to TapHandler / HoverHandler in 5.15 315 id: linkMA 316 anchors.fill: parent 317 cursorShape: Qt.PointingHandCursor 318 hoverEnabled: true 319 onClicked: { 320 if (page >= 0) 321 root.goToLocation(page, location, zoom) 322 else 323 Qt.openUrlExternally(url) 324 } 325 } 326 ToolTip { 327 visible: linkMA.containsMouse 328 delay: 1000 329 text: page >= 0 ? 330 ("page " + (page + 1) + 331 " location " + location.x.toFixed(1) + ", " + location.y.toFixed(1) + 332 " zoom " + zoom) : url 333 } 334 } 335 } 336 PdfSelection { 337 id: selection 338 anchors.fill: parent 339 document: root.document 340 page: image.currentFrame 341 renderScale: image.renderScale 342 fromPoint: textSelectionDrag.centroid.pressPosition 343 toPoint: textSelectionDrag.centroid.position 344 hold: !textSelectionDrag.active && !mouseClickHandler.pressed 345 onTextChanged: root.selectedText = text 346 focus: true 347 } 348 } 349 } 350 ScrollBar.vertical: ScrollBar { 351 id: vscroll 352 property bool moved: false 353 onPositionChanged: moved = true 354 onActiveChanged: { 355 var cell = tableHelper.cellAtPos(root.width / 2, root.height / 2) 356 var currentItem = tableHelper.itemAtCell(cell) 357 var currentLocation = Qt.point(0, 0) 358 if (currentItem) { // maybe the delegate wasn't loaded yet 359 currentLocation = Qt.point((tableView.contentX - currentItem.x + jumpLocationMargin.x) / root.renderScale, 360 (tableView.contentY - currentItem.y + jumpLocationMargin.y) / root.renderScale) 361 } 362 if (active) { 363 moved = false 364 // emitJumped false to avoid interrupting a pinch if TableView thinks it should scroll at the same time 365 navigationStack.push(cell.y, currentLocation, root.renderScale, false) 366 } else if (moved) { 367 navigationStack.update(cell.y, currentLocation, root.renderScale) 368 } 369 } 370 } 371 ScrollBar.horizontal: ScrollBar { } 372 } 373 onRenderScaleChanged: { 374 // if navigationStack.jumped changes the scale, don't turn around and update the stack again; 375 // and don't force layout either, because positionViewAtCell() will do that 376 if (navigationStack.jumping) 377 return 378 // make TableView rebuild from scratch, because otherwise it doesn't know the delegates are changing size 379 tableView.rebuild() 380 var cell = tableHelper.cellAtPos(root.width / 2, root.height / 2) 381 var currentItem = tableHelper.itemAtCell(cell) 382 if (currentItem) { 383 var currentLocation = Qt.point((tableView.contentX - currentItem.x + jumpLocationMargin.x) / root.renderScale, 384 (tableView.contentY - currentItem.y + jumpLocationMargin.y) / root.renderScale) 385 navigationStack.update(cell.y, currentLocation, renderScale) 386 } 387 } 388 PdfNavigationStack { 389 id: navigationStack 390 property bool jumping: false 391 property int previousPage: 0 392 onJumped: { 393 jumping = true 394 root.renderScale = zoom 395 if (location.y < 0) { 396 // invalid to indicate that a specific location was not needed, 397 // so attempt to position the new page just as the current page is 398 var currentYOffset = 0 399 var previousPageDelegate = tableHelper.itemAtCell(0, previousPage) 400 if (previousPageDelegate) 401 currentYOffset = tableView.contentY - previousPageDelegate.y 402 tableHelper.positionViewAtRow(page, Qt.AlignTop, currentYOffset) 403 if (root.debug) { 404 console.log("going from page", previousPage, "to", page, "offset", currentYOffset, 405 "ended up @", tableView.contentX.toFixed(1) + ", " + tableView.contentY.toFixed(1)) 406 } 407 } else { 408 // jump to a page and position the given location relative to the top-left corner of the viewport 409 var pageSize = root.document.pagePointSize(page) 410 pageSize.width *= root.renderScale 411 pageSize.height *= root.renderScale 412 var xOffsetLimit = Math.max(0, pageSize.width - root.width) / 2 413 var offset = Qt.point(Math.max(-xOffsetLimit, Math.min(xOffsetLimit, 414 location.x * root.renderScale - jumpLocationMargin.x)), 415 Math.max(0, location.y * root.renderScale - jumpLocationMargin.y)) 416 tableHelper.positionViewAtCell(0, page, Qt.AlignLeft | Qt.AlignTop, offset) 417 if (root.debug) { 418 console.log("going to zoom", zoom, "loc", location, "on page", page, 419 "ended up @", tableView.contentX.toFixed(1) + ", " + tableView.contentY.toFixed(1)) 420 } 421 } 422 jumping = false 423 previousPage = page 424 } 425 onCurrentPageChanged: searchModel.currentPage = currentPage 426 } 427 PdfSearchModel { 428 id: searchModel 429 document: root.document === undefined ? null : root.document 430 // TODO maybe avoid jumping if the result is already fully visible in the viewport 431 onCurrentResultBoundingRectChanged: root.goToLocation(currentPage, 432 Qt.point(currentResultBoundingRect.x, currentResultBoundingRect.y), 0) 433 } 434} 435