Warning, /graphics/peruse/src/app/qml/viewers/ImageBrowser.qml is written in an unsupported language. File is not indexed.

0001 /*
0002  * Copyright (C) 2015 Vishesh Handa <vhanda@kde.org>
0003  *
0004  * This library is free software; you can redistribute it and/or
0005  * modify it under the terms of the GNU Lesser General Public
0006  * License as published by the Free Software Foundation; either
0007  * version 2.1 of the License, or (at your option) version 3, or any
0008  * later version accepted by the membership of KDE e.V. (or its
0009  * successor approved by the membership of KDE e.V.), which shall
0010  * act as a proxy defined in Section 6 of version 3 of the license.
0011  *
0012  * This library is distributed in the hope that it will be useful,
0013  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0014  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0015  * Lesser General Public License for more details.
0016  *
0017  * You should have received a copy of the GNU Lesser General Public
0018  * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
0019  *
0020  */
0021 
0022 import QtQuick 2.12
0023 import QtQuick.Layouts 1.12
0024 import QtQuick.Window 2.12
0025 
0026 import org.kde.kirigami 2.13 as Kirigami
0027 
0028 import org.kde.peruse 0.1 as Peruse
0029 import "helpers" as Helpers
0030 
0031 /**
0032  * @brief The image viewer used by the CBR and Folder Viewer Base classes.
0033  * 
0034  * It handles drawing the image and the different zoom modes.
0035  */
0036 ListView {
0037     id: root
0038     function goNextFrame() { root.currentItem.goNextFrame(); }
0039     function goPreviousFrame() { root.currentItem.goPreviousFrame(); }
0040     signal goNextPage();
0041     signal goPreviousPage();
0042     signal goPage(int pageNumber);
0043     signal activateExternalLink(string link);
0044 
0045     onWidthChanged: restorationTimer.start()
0046     onHeightChanged: restorationTimer.start()
0047     Timer {
0048         id: restorationTimer
0049         interval: 300
0050         running: false
0051         repeat: false
0052         onTriggered: {
0053             if (currentItem) {
0054                 imageBrowser.positionViewAtIndex(imageBrowser.currentIndex, ListView.Center);
0055                 currentItem.refocusFrame();
0056             }
0057             else {
0058                 restorationTimer.start();
0059             }
0060         }
0061     }
0062 
0063     function navigateTo(pageNo, frameNo = -1) {
0064         goPage(pageNo);
0065         root.currentItem.currentFrame = frameNo;
0066     }
0067 
0068     property string hoveredLink
0069     function handleLink(link) {
0070         var lowerLink = link.toLowerCase();
0071         if (link.startsWith("#")) {
0072             // Then it's one of our internally identified objects with an explicit name
0073             var linkedObject = root.identifiedObjects.objectById(link.slice(1));
0074             if (linkedObject) {
0075                 if (linkedObject.objectType === "Reference" || linkedObject.objectType === "Binary") {
0076                     // show a popup with the reference or binary in
0077                     // if we've got a sensible way of showing it:
0078                     // pdf
0079                     // various image formats
0080                     infoDisplay.showInfo(linkedObject);
0081                 } else if (linkedObject.objectType === "Page") {
0082                     // navigate to this page
0083                     root.navigateTo(linkedObject.localIndex);
0084                 } else if (linkedObject.objectType === "Frame") {
0085                     // navigate to this frame/page combo
0086                     root.navigateTo(linkedObject.parent.localIndex, linkedObject.localIndex);
0087                 } else {
0088                     // sorry, dunno what to do with these yet, let's tell the user
0089                     applicationWindow().showPassiveNotification(i18n("The link you just activated goes to something we don't really know what to do with. This suggests the book you are reading does not conform to the Advanced Comic Book Format specification, but we would still like to know about it. Please report a bug, and make the book available to us, so we can try and work out how to handle this!"));
0090                 }
0091             } else {
0092                 // we didn't find an object with that name, so let's inform the user about that
0093                 applicationWindow().showPassiveNotification(i18n("The link you activated doesn't seem to go anywhere. We tried to find something with the ID %1 but didn't locate anything. This is likely an error in the book, and the author would probably like to know about it.", link));
0094             }
0095         } else if (lowerLink.startsWith("http://") || lowerLink.startsWith("https://") || lowerLink.startsWith("mailto:") || lowerLink.startsWith("file://")) {
0096             // This is an external link (relative to the archive), let's try and support those in a graceful manner
0097             // Be safe, show the link before launching it
0098             root.activateExternalLink(link);
0099         } else if (lowerLink.startsWith("zip:")) {
0100             // This is a bit of an oddity - link to a file in a zip archive
0101             // link format for e.g.: zip:path/to/file.zip!/path/to/file/page1.jpg
0102             // zip location can be either relative or absolute, so we need to deal with that...
0103         } else {
0104             // If none of the above match, assume we have a link to some internal archive
0105             // file, so let's see if the thing it's trying to link to actually exists...
0106         }
0107     }
0108 
0109     function switchToNextJump() {
0110         root.currentItem.nextJump();
0111     }
0112 
0113     function activateCurrentJump() {
0114         root.currentItem.activateCurrentJump();
0115     }
0116 
0117     interactive: false // No interactive flicky stuff here, we'll handle that with the navigator instance
0118     property int imageWidth
0119     property int imageHeight
0120 
0121     orientation: ListView.Horizontal
0122     snapMode: ListView.SnapOneItem
0123     cacheBuffer: 3000
0124 
0125     readonly property QtObject identifiedObjects: Peruse.IdentifiedObjectModel {
0126         document: root.model.acbfData
0127     }
0128 
0129     // This ensures that the current index is always up to date, which we need to ensure we can track the current page
0130     // as required by the thumbnail navigator, and the resume-reading-from functionality
0131     onMovementEnded: {
0132         var indexHere = indexAt(contentX + width / 2, contentY + height / 2);
0133         if(currentIndex !== indexHere) {
0134             currentIndex = indexHere;
0135         }
0136     }
0137     /**
0138      * An interactive area with an image.
0139      * 
0140      * Clicking once on the image will hide all other controls from view.
0141      * Clicking twice will instead zoom in.
0142      * 
0143      * Pinch will zoom in as well.
0144      */
0145     delegate: Flickable {
0146         id: flick
0147         opacity: ListView.isCurrentItem ? 1 : 0
0148         Behavior on opacity { NumberAnimation { duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } }
0149         width: imageWidth
0150         height: imageHeight
0151         contentWidth: imageWidth
0152         contentHeight: imageHeight
0153         interactive: (contentWidth > width || contentHeight > height) && (totalFrames === 0)
0154         z: interactive ? 1000 : 0
0155         property bool hasInteractiveObjects: image.frameJumps.length > 0 || image.frameLinkRects.length > 0;
0156         function goNextFrame() { image.nextFrame(); }
0157         function goPreviousFrame() { image.previousFrame(); }
0158         function setColouredHole(holeRect,holeColor) {
0159             pageHole.setHole(holeRect);
0160             pageHole.color = holeColor;
0161         }
0162         Timer {
0163             id: refocusTimer
0164             interval: 200
0165             running: false
0166             repeat: false
0167             onTriggered: {
0168                 flick.resetHole();
0169                 if (totalFrames > 0 && currentFrame > -1) {
0170                     image.focusOnFrame();
0171                 }
0172             }
0173         }
0174         function refocusFrame() {
0175             refocusTimer.start();
0176         }
0177         function resetHole() {
0178             if(image.status == Image.Ready) {
0179                 var holeColor = "transparent";
0180                 if (image.currentPageObject !== null) {
0181                     holeColor = image.currentPageObject.bgcolor;
0182                 }
0183                 setColouredHole(image.paintedRect, holeColor);
0184             }
0185         }
0186         function nextJump() {
0187             image.nextJump();
0188         }
0189         function activateCurrentJump() {
0190             image.activateCurrentJump();
0191         }
0192         ListView.onIsCurrentItemChanged: resetHole();
0193         Connections {
0194             target: image
0195             function onStatusChanged() { refocusFrame(); }
0196         }
0197         property alias totalFrames: image.totalFrames;
0198         property alias currentFrame: image.currentFrame;
0199         pixelAligned: true
0200         property bool actuallyMoving: moving || xMover.running || yMover.running || widthMover.running || heightMover.running
0201         Behavior on contentX { NumberAnimation { id: xMover; duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } }
0202         Behavior on contentY { NumberAnimation { id: yMover; duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } }
0203         Behavior on contentWidth { NumberAnimation { id: widthMover; duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } }
0204         Behavior on contentHeight { NumberAnimation { id: heightMover; duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } }
0205         PinchArea {
0206             width: Math.max(flick.contentWidth, flick.width)
0207             height: Math.max(flick.contentHeight, flick.height)
0208 
0209             property real initialWidth
0210             property real initialHeight
0211 
0212             onPinchStarted: {
0213                 initialWidth = flick.contentWidth
0214                 initialHeight = flick.contentHeight
0215             }
0216 
0217             onPinchUpdated: {
0218                 // adjust content pos due to drag
0219                 flick.contentX += pinch.previousCenter.x - pinch.center.x
0220                 flick.contentY += pinch.previousCenter.y - pinch.center.y
0221 
0222                 // resize content
0223                 flick.resizeContent(Math.max(imageWidth, initialWidth * pinch.scale), Math.max(imageHeight, initialHeight * pinch.scale), pinch.center)
0224             }
0225 
0226             onPinchFinished: {
0227                 // Move its content within bounds.
0228                 flick.returnToBounds();
0229             }
0230 
0231 
0232             Image {
0233                 id: image
0234                 width: flick.contentWidth
0235                 height: flick.contentHeight
0236                 Helpers.HolyRectangle {
0237                     id: pageHole
0238                     anchors.fill: parent
0239                     color: image.currentFrameObj.bgcolor
0240                     visible: opacity > 0
0241                     opacity: image.currentFrame === -1 ? 1 : 0
0242                     animatePosition: false
0243                 }
0244                 source: model.url
0245                 fillMode: Image.PreserveAspectFit
0246                 asynchronous: true
0247                 property bool shouldCheat: imageWidth * 2 > maxTextureSize || imageHeight * 2 > maxTextureSize;
0248                 property bool isTall: imageHeight < imageWidth;
0249                 property int fixedWidth: isTall ? maxTextureSize * (imageWidth / imageHeight) : maxTextureSize;
0250                 property int fixedHeight: isTall ? maxTextureSize : maxTextureSize * (imageHeight / imageWidth);
0251                 sourceSize.width: shouldCheat ? fixedWidth : imageWidth * 2;
0252                 sourceSize.height: shouldCheat ? fixedHeight : imageHeight * 2;
0253                 MouseArea {
0254                     anchors.fill: parent
0255                 }
0256 
0257                 // Setup for all the entries.
0258                 property QtObject currentPageObject: {
0259                     if (root.model.acbfData) {
0260                         if (model.index===0) {
0261                             currentPageObject = root.model.acbfData.metaData.bookInfo.coverpage();
0262                         } else if (model.index > 0) {
0263                             currentPageObject = root.model.acbfData.body.page(model.index-1);
0264                         }
0265                     } else {
0266                         null;
0267                     }
0268                 }
0269                 property real muliplier: isTall? (paintedHeight / pixHeight): (paintedWidth / pixWidth);
0270                 property int offsetX: (width-paintedWidth)/2;
0271                 property int offsetY: (height-paintedHeight)/2;
0272                 property rect paintedRect: Qt.rect(offsetX, offsetY, paintedWidth, paintedHeight);
0273 
0274                 // This is some magic that QML Image does for us, to be helpful. It isn't very helpful to us.
0275                 property int pixWidth: image.implicitWidth * Screen.devicePixelRatio;
0276                 property int pixHeight: image.implicitHeight * Screen.devicePixelRatio;
0277 
0278                 function focusOnFrame() {
0279                     flick.resizeContent(imageWidth, imageHeight, Qt.point(flick.contentX, flick.contentY));
0280                     var frameObj = image.currentFrameObj;
0281                     var frameBounds = frameObj.bounds;
0282                     var frameMultiplier = image.pixWidth/frameBounds.width * (root.imageWidth/image.paintedWidth);
0283                     // If it's now too large to fit inside the viewport, scale it by height instead
0284                     if ((frameBounds.height/frameBounds.width)*root.imageWidth > root.imageHeight) {
0285                         frameMultiplier = image.pixHeight/frameBounds.height * (root.imageHeight/image.paintedHeight);
0286                     }
0287 //                     console.debug("Frame bounds for frame " + index + " are " + frameBounds + " with multiplier " + frameMultiplier);
0288 //                     console.debug("Actual pixel size of image with implicit size " + image.implicitWidth + " by " + image.implicitHeight + " is " + pixWidth + " by " + pixHeight);
0289                     flick.resizeContent(imageWidth * frameMultiplier, imageHeight * frameMultiplier, Qt.point(flick.contentX,flick.contentY));
0290                     var frameRect = Qt.rect((image.muliplier * frameBounds.x) + image.offsetX
0291                                         , (image.muliplier * frameBounds.y) + image.offsetY
0292                                         , (image.muliplier * frameBounds.width)
0293                                         , (image.muliplier * frameBounds.height));
0294                     flick.contentX = frameRect.x - (flick.width-frameRect.width)/2;
0295                     flick.contentY = frameRect.y - (flick.height-frameRect.height)/2;
0296                 }
0297 
0298                 function nextFrame() {
0299                     if (image.totalFrames > 0 && image.currentFrame+1 < image.totalFrames) {
0300                         image.currentFrame++;
0301                     } else {
0302                         image.currentFrame = -1;
0303                         flick.returnToBounds();
0304                         root.goNextPage();
0305                         if(root.currentItem.totalFrames > 0) {
0306                             root.currentItem.currentFrame = 0;
0307                         }
0308                     }
0309                 }
0310 
0311                 function previousFrame() {
0312                     if (image.totalFrames > 0 && image.currentFrame-1 > -1) {
0313                         image.currentFrame--;
0314                     } else {
0315                         image.currentFrame = -1;
0316                         flick.returnToBounds();
0317                         root.goPreviousPage();
0318                         if(root.currentItem.totalFrames > 0) {
0319                             root.currentItem.currentFrame = root.currentItem.totalFrames - 1;
0320                         }
0321                     }
0322                 }
0323 
0324                 property int totalFrames: image.currentPageObject? image.currentPageObject.framePointStrings.length: 0;
0325                 property int currentFrame: -1;
0326                 property QtObject currentFrameObj: image.currentPageObject && image.totalFrames > 0 && image.currentFrame > -1 ? image.currentPageObject.frame(currentFrame) : noFrame;
0327                 onCurrentFrameObjChanged: {
0328                     initFrame();
0329                     focusOnFrame(image.currentFrame);
0330                 }
0331                 property QtObject noFrame: QtObject {
0332                     property rect bounds: image.paintedRect
0333                     property color bgcolor: image.currentPageObject? image.currentPageObject.bgcolor: "transparent";
0334                 }
0335 
0336                 // if we're on touch screen, we set the currentJumpIndex to -1 by default
0337                 // otherwise we set it to the first available jump on the frame
0338                 property int currentJumpIndex: Kirigami.Settings.isMobile? -1 : 0;
0339                 property var frameJumps: [];
0340 
0341                 function initFrame() {
0342                     currentJumpIndex = Kirigami.Settings.isMobile? -1 : 0;
0343                     var newFrameJumps = [];
0344 
0345                     if(currentFrameObj === noFrame) {
0346                         newFrameJumps = image.currentPageObject? image.currentPageObject.jumps : [];
0347                     } else {
0348                         for(var i = 0; i < image.currentPageObject.jumps.length; i++) {
0349                             var jumpObj = image.currentPageObject.jump(i);
0350                             if(frameContainsJump(jumpObj)) {
0351                                 newFrameJumps.push(jumpObj);
0352                             }
0353                         }
0354                     }
0355                     frameJumps = newFrameJumps;
0356                     updateFrameLinkRects();
0357                 }
0358 
0359                 property var frameLinkRects: [];
0360                 function updateFrameLinkRects() {
0361                     var newLinkRects = [];
0362                     for(var i = 0; i < textAreaRepeater.model.length; i++) {
0363                         if (frameContainsJump(textAreaRepeater.model[i])) {
0364                             for(var j = 0; j < textAreaRepeater.count; j++) {
0365                                 newLinkRects.push(textAreaRepeater.itemAt(j).linkRects[j]);
0366                             }
0367                         }
0368                     }
0369                     frameLinkRects = newLinkRects;
0370                 }
0371 
0372                 Repeater {
0373                     id: textAreaRepeater
0374                     property QtObject textLayer: root.currentLanguage ? image.currentPageObject.textLayer(root.currentLanguage.language) : null
0375                     onTextLayerChanged: { image.updateFrameLinkRects(); }
0376                     model: textLayer ? textLayer.textareas : 0;
0377                     Helpers.TextAreaHandler {
0378                         id: textAreaHandler
0379                         model: root.model
0380                         identifiedObjects: root.identifiedObjects
0381                         multiplier: image.muliplier
0382                         offsetX: image.offsetX
0383                         offsetY: image.offsetY
0384                         textArea: modelData
0385                         enabled: image.frameContainsJump(modelData) && !flick.actuallyMoving
0386                         onLinkActivated: { root.handleLink(link); }
0387                         onHoveredLinkChanged: { root.hoveredLink = textAreaHandler.hoveredLink; }
0388                         // for mobile, make tooltip a tap instead (or tap-and-hold), and tap-to-dismiss
0389                         // hover on bin link, show name in tooltip, if image show thumbnail in tooltop, on click open in popup if we know how, offer external if we don't (maybe in that popup?)
0390                         // hover on ref link, show small snippet in tooltip, on click open first in popup, if already in popup, open in full display reader
0391                         // hover on page(/frame), tooltip shows destination in some pleasant format (if page has non-numeric title, show the title)
0392                         // hover on external link, show something about opening external link
0393                     }
0394                 }
0395 
0396                 /**
0397                  * \brief returns true if the sent jump bounds are within the image's current frame
0398                  * @param jumpObj - the given jump object
0399                  */
0400                 function frameContainsJump(jumpObj) {
0401                     if(flick.ListView.isCurrentItem) {
0402                         if(image.currentFrameObj === noFrame) {
0403                             return true;
0404                         } 
0405 
0406                         return jumpObj.bounds.x >= image.currentFrameObj.bounds.x && 
0407                                 jumpObj.bounds.y >= image.currentFrameObj.bounds.y &&
0408                                 (jumpObj.bounds.x + jumpObj.bounds.width) <= (image.currentFrameObj.bounds.x + image.currentFrameObj.bounds.width) &&
0409                                 (jumpObj.bounds.y + jumpObj.bounds.height) <= (image.currentFrameObj.bounds.y + image.currentFrameObj.bounds.height);
0410                     }
0411 
0412                     return false;
0413                 }
0414 
0415                 function nextJump() {
0416                     if(image.currentJumpIndex === -1 || !jumpsRepeater.itemAt(image.currentJumpIndex).hovered) {
0417                         image.currentJumpIndex = (image.currentJumpIndex + 1) % image.frameJumps.length;
0418                     }
0419                     //image.currentJumpIndex = image.currentJumpIndex === image.frameJumps.length - 1? 0 : ++image.currentJumpIndex;
0420                 }
0421 
0422                 function activateCurrentJump() {
0423                     if(currentJumpIndex !== -1 && jumpsRepeater.itemAt(currentJumpIndex)) {
0424                         jumpsRepeater.itemAt(currentJumpIndex).activated();
0425                     }
0426                 }
0427 
0428                 Repeater {
0429                     id: jumpsRepeater;
0430                     model: image.frameJumps;
0431 
0432                     Helpers.JumpHandler {
0433                         jumpObject: modelData;
0434 
0435                         offsetX: image.offsetX;
0436                         offsetY: image.offsetY;
0437 
0438                         widthMultiplier: image.muliplier;
0439                         heightMultiplier: image.muliplier;
0440 
0441                         focused: image.currentJumpIndex === index;
0442 
0443                         Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.InOutQuad; } }
0444 
0445                         onActivated: {
0446                             // href for jumps is fairly new, so we need to fall back gracefully
0447                             // (also this allows jumps to link to unnamed pages)
0448                             if (jumpObject.href.length > 0) {
0449                                 root.handleLink(jumpObject.href);
0450                             } else {
0451                                 root.navigateTo(jumpObject.pageIndex);
0452                             }
0453                         }
0454 
0455                         onHoveredChanged: image.currentJumpIndex = (hovered ? index : -1);
0456                     }
0457                 }
0458 
0459                 Repeater {
0460                     model: image.currentPageObject? image.currentPageObject.framePointStrings: 0;
0461 //                     Rectangle {
0462 //                         id: frame;
0463 //                         x: (image.muliplier * image.currentPageObject.frame(index).bounds.x) + image.offsetX;
0464 //                         y: (image.muliplier * image.currentPageObject.frame(index).bounds.y) + image.offsetY;
0465 //                         width: image.muliplier * image.currentPageObject.frame(index).bounds.width;
0466 //                         height: image.muliplier * image.currentPageObject.frame(index).bounds.height;
0467 //                         color: "blue";
0468 //                         opacity: 0;
0469 //                     }
0470                     Helpers.HolyRectangle {
0471                         anchors.fill: parent;
0472                         property QtObject frameObj: image.currentPageObject ? image.currentPageObject.frame(index) : noFrame;
0473                         property rect frameRect: Qt.rect((image.muliplier * frameObj.bounds.x) + image.offsetX,
0474                                             (image.muliplier * frameObj.bounds.y) + image.offsetY,
0475                                             (image.muliplier * frameObj.bounds.width),
0476                                             (image.muliplier * frameObj.bounds.height))
0477                         color: frameObj.bgcolor;
0478                         opacity: image.currentFrame === index ? 1 : 0;
0479                         visible: opacity > 0;
0480                         topBorder: frameRect.y;
0481                         leftBorder: frameRect.x;
0482                         rightBorder: width - (frameRect.x + frameRect.width);
0483                         bottomBorder: height - (frameRect.y + frameRect.height);
0484                     }
0485                 }
0486                 
0487                 MouseArea {
0488                     anchors.fill: parent;
0489                     enabled: flick.interactive && !root.currentItem.hasInteractiveObjects;
0490                     onClicked: startToggleControls();
0491                     onDoubleClicked: {
0492                         abortToggleControls();
0493                         flick.resizeContent(imageWidth, imageHeight, Qt.point(imageWidth/2, imageHeight/2));
0494                         flick.returnToBounds();
0495                     }
0496                 }
0497                 Kirigami.PlaceholderMessage {
0498                     anchors.centerIn: parent
0499                     width: parent.width - (Kirigami.Units.largeSpacing * 4)
0500                     visible: image.status === Image.Error;
0501                     text: i18nc("Message shown on the book reader view when there is an issue loading the image for a specific page", "Could not load the image for this page.\nThis is most commonly due to a missing image decoder (specifically, the Qt Imageformats package, which Peruse depends on for loading images), and likely a packaging error. Contact whoever you got this package from and inform them of this error.\n\nSpecifically, the image we attempted to load is called %1, and the image formats Qt is aware of are %2. If there is a mismatch there, that will be the problem.\n\nIf not, please report this bug to us, and give as much information as you can to assist us in working out what's wrong.", image.source, peruseConfig.supportedImageFormats().join(", "));
0502                 }
0503             }
0504         }
0505     }
0506 
0507     Kirigami.OverlaySheet {
0508         id: infoDisplay
0509         function showInfo(theObject) {
0510             infoDisplay.theObject = theObject;
0511             infoDisplay.open();
0512         }
0513         property QtObject theObject
0514         showCloseButton: true
0515         header: Kirigami.Heading {
0516             text: infoDisplay.theObject ? infoDisplay.theObject.id : ""
0517             Layout.fillWidth: true
0518             elide: Text.ElideRight
0519         }
0520         contentItem: ColumnLayout {
0521             Layout.fillWidth: true
0522             Layout.preferredWidth: root.width * .8
0523             Text {
0524                 opacity: infoDisplay.theObject && infoDisplay.theObject.objectType === "Reference" ? 1 : 0
0525                 Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; } }
0526                 Layout.fillWidth: true
0527                 Layout.preferredWidth: root.width - Kirigami.Units.largeSpacing * 2
0528                 textFormat: Text.StyledText
0529                 wrapMode: Text.Wrap
0530                 // We need some pleasant way to get the paragraphs and turn them into sensibly rich-text styled text (perhaps on Stylesheet? Throw it a qstringlist and it spits out a formatted html string with the styles etc?)
0531                 text: opacity > 0 ? "<p>" + infoDisplay.theObject.paragraphs.join("</p><p>") + "</p>": ""
0532                 onLinkActivated: { root.handleLink(link); }
0533                 onLinkHovered: {
0534                     // Show first line in a popup, or destination, etc, as for Textareas
0535                 }
0536             }
0537             Image {
0538                 opacity: infoDisplay.theObject && infoDisplay.theObject.objectType === "Binary" ? 1 : 0
0539                 Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; } }
0540                 fillMode: Image.PreserveAspectFit
0541                 source: opacity > 0 ? root.model.previewForId("#" + infoDisplay.theObject.id) : ""
0542             }
0543         }
0544     }
0545 
0546     Helpers.Navigator {
0547         enabled: root.currentItem ? !root.currentItem.interactive : false;
0548         acceptTaps: !root.currentItem.hasInteractiveObjects;
0549         anchors.fill: parent;
0550         onLeftRequested: root.layoutDirection === Qt.RightToLeft? root.goNextFrame(): root.goPreviousFrame();
0551         onRightRequested: root.layoutDirection === Qt.RightToLeft? root.goPreviousFrame(): root.goNextFrame();
0552         onTapped: startToggleControls();
0553         onDoubleTapped: {
0554             abortToggleControls();
0555             if (root.currentItem.totalFrames === 0) {
0556                 root.currentItem.resizeContent(imageWidth * 2, imageHeight * 2, Qt.point(eventPoint.x, eventPoint.y));
0557                 root.currentItem.returnToBounds();
0558             }
0559         }
0560     }
0561 }