diff --git a/src/app/qml/viewers/ImageBrowser.qml b/src/app/qml/viewers/ImageBrowser.qml index 61fbf71..c50a203 100644 --- a/src/app/qml/viewers/ImageBrowser.qml +++ b/src/app/qml/viewers/ImageBrowser.qml @@ -1,358 +1,348 @@ /* * Copyright (C) 2015 Vishesh Handa * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ import QtQuick 2.3 // import QtQuick.Layouts 1.1 // import QtQuick.Controls 1.0 as QtControls import org.kde.kirigami 2.1 as Kirigami import "helpers" as Helpers /** * @brief The image viewer used by the CBR and Folder Viewer Base classes. * * It handles drawing the image and the different zoom modes. */ ListView { id: root function goNextFrame() { root.currentItem.goNextFrame(); } function goPreviousFrame() { root.currentItem.goPreviousFrame(); } signal goNextPage(); signal goPreviousPage(); property int imageWidth property int imageHeight orientation: ListView.Horizontal snapMode: ListView.SnapOneItem cacheBuffer: 3000 // This ensures that the current index is always up to date, which we need to ensure we can track the current page // as required by the thumbnail navigator, and the resume-reading-from functionality onMovementEnded: { var indexHere = indexAt(contentX + width / 2, contentY + height / 2); if(currentIndex !== indexHere) { currentIndex = indexHere; } } /** * An interactive area with an image. * * Clicking once on the image will hide all other controls from view. * Clicking twice will instead zoom in. * * Pinch will zoom in as well. */ delegate: Flickable { id: flick width: imageWidth height: imageHeight contentWidth: imageWidth contentHeight: imageHeight interactive: contentWidth > width || contentHeight > height onInteractiveChanged: root.interactive = !interactive; z: interactive ? 1000 : 0 function goNextFrame() { image.nextFrame(); } function goPreviousFrame() { image.previousFrame(); } function setColouredHole(holeRect,holeColor) { holyRect.setHole(holeRect); holyRect.color = holeColor; } function resetHole() { if(image.status == Image.Ready) { var holeColor = "blue"; if (image.currentPageObject !== null) { holeColor = image.currentPageObject.bgcolor; } setColouredHole(image.paintedRect, holeColor); } } ListView.onIsCurrentItemChanged: resetHole(); Connections { target: image onStatusChanged: resetHole(); } property alias totalFrames: image.totalFrames; property alias currentFrame: image.currentFrame; pixelAligned: true Behavior on contentX { NumberAnimation { duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } } Behavior on contentY { NumberAnimation { duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } } Behavior on contentWidth { NumberAnimation { duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } } Behavior on contentHeight { NumberAnimation { duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } } PinchArea { width: Math.max(flick.contentWidth, flick.width) height: Math.max(flick.contentHeight, flick.height) property real initialWidth property real initialHeight onPinchStarted: { initialWidth = flick.contentWidth initialHeight = flick.contentHeight } onPinchUpdated: { // adjust content pos due to drag flick.contentX += pinch.previousCenter.x - pinch.center.x flick.contentY += pinch.previousCenter.y - pinch.center.y // resize content flick.resizeContent(Math.max(imageWidth, initialWidth * pinch.scale), Math.max(imageHeight, initialHeight * pinch.scale), pinch.center) } onPinchFinished: { // Move its content within bounds. flick.returnToBounds(); } Image { id: image width: flick.contentWidth height: flick.contentHeight Helpers.HolyRectangle { id: holyRect anchors.fill: parent - topBorder: 0 - Behavior on topBorder { NumberAnimation { duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } } - leftBorder: 0 - Behavior on leftBorder { NumberAnimation { duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } } - rightBorder: 0 - Behavior on rightBorder { NumberAnimation { duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } } - bottomBorder: 0 - Behavior on bottomBorder { NumberAnimation { duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } } color: image.currentPageObject.bgcolor - Behavior on color { ColorAnimation { duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } } opacity: flick.ListView.isCurrentItem ? 1 : 0 - Behavior on opacity { NumberAnimation { duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } } visible: opacity > 0 } source: model.url fillMode: Image.PreserveAspectFit asynchronous: true property bool shouldCheat: imageWidth * 2 > maxTextureSize || imageHeight * 2 > maxTextureSize; property bool isTall: imageHeight < imageWidth; property int fixedWidth: isTall ? maxTextureSize * (imageWidth / imageHeight) : maxTextureSize; property int fixedHeight: isTall ? maxTextureSize : maxTextureSize * (imageHeight / imageWidth); sourceSize.width: shouldCheat ? fixedWidth : imageWidth * 2; sourceSize.height: shouldCheat ? fixedHeight : imageHeight * 2; MouseArea { anchors.fill: parent onClicked: startToggleControls(); onDoubleClicked: { abortToggleControls(); if (flick.interactive) { flick.resizeContent(imageWidth, imageHeight, Qt.point(imageWidth/2, imageHeight/2)); } else { flick.resizeContent(imageWidth * 2, imageHeight * 2, Qt.point(mouse.x, mouse.y)); } } } // Setup for all the entries. property QtObject currentPageObject: { if (root.model.acbfData) { if (model.index===0) { currentPageObject = root.model.acbfData.metaData.bookInfo.coverpage(); } else if (model.index > 0) { currentPageObject = root.model.acbfData.body.page(model.index-1); } } else { null; } } property real muliplier: isTall? (paintedHeight / implicitHeight): (paintedWidth / implicitWidth); property int offsetX: (width-paintedWidth)/2; property int offsetY: (height-paintedHeight)/2; property rect paintedRect: Qt.rect(offsetX, offsetY, paintedWidth, paintedHeight); function focusOnFrame(index) { if (index>-1) { var frameObj = image.currentPageObject.frame(index); var frameBounds = frameObj.bounds; var frameMultiplier = image.implicitWidth/frameBounds.width; //Check if the height of the final frame is higher than the contentHeight //When we're using a *fit to with scheme. if ((frameBounds.height/frameBounds.width)*contentWidth > contentHeight) { frameMultiplier = image.implicitHeight/frameBounds.height; frameMultiplier = frameMultiplier * (contentHeight/image.paintedHeight); } else { frameMultiplier = frameMultiplier * (contentWidth/image.paintedWidth); } flick.resizeContent(imageWidth * frameMultiplier, imageHeight * frameMultiplier, Qt.point(flick.contentX,flick.contentY)); var frameRect = Qt.rect(image.muliplier * frameBounds.x + image.offsetX, image.muliplier * frameBounds.y+ image.offsetY, image.muliplier * frameBounds.width, image.muliplier * frameBounds.height); flick.contentX = frameRect.x - (flick.width-frameRect.width)/2; flick.contentY = frameRect.y - (flick.height-frameRect.height)/2; holyRect.color = frameObj.bgcolor; holyRect.setHole(frameRect); } else { flick.resetHole(); } } function nextFrame() { if (image.totalFrames > 0 && image.currentFrame+1 < image.totalFrames) { image.currentFrame++; } else { flick.resizeContent(imageWidth, imageHeight, Qt.point(imageWidth/2, imageHeight/2)); image.currentFrame = -1; flick.returnToBounds(); root.goNextPage(); if(root.currentItem.totalFrames > 0) { root.currentItem.currentFrame = 0; } } } function previousFrame() { if (image.totalFrames > 0 && image.currentFrame-1 > -1) { image.currentFrame--; } else { flick.resizeContent(imageWidth, imageHeight, Qt.point(imageWidth/2, imageHeight/2)); image.currentFrame = -1; flick.returnToBounds(); root.goPreviousPage(); if(root.currentItem.totalFrames > 0) { root.currentItem.currentFrame = root.currentItem.totalFrames - 1; } } } property int totalFrames: image.currentPageObject? image.currentPageObject.framePointStrings.length: 0; property int currentFrame: -1; onCurrentFrameChanged: focusOnFrame(currentFrame); Repeater { model: image.currentPageObject? image.currentPageObject.framePointStrings: 0; Rectangle { id: frame; x: image.muliplier * image.currentPageObject.frame(index).bounds.x + image.offsetX; y: image.muliplier * image.currentPageObject.frame(index).bounds.y + image.offsetY; width: { image.muliplier * image.currentPageObject.frame(index).bounds.width; } height: image.muliplier * image.currentPageObject.frame(index).bounds.height; color: "blue"; opacity: 0; MouseArea { anchors.fill: parent; onClicked: startToggleControls(); preventStealing: true; onDoubleClicked: { abortToggleControls(); if (flick.interactive && image.currentFrame == index) { flick.resizeContent(imageWidth, imageHeight, Qt.point(imageWidth/2, imageHeight/2)); image.currentFrame = -1; } else { image.currentFrame = index; mouse.accepted; } } } } } } } } MouseArea { anchors { top: parent.top; left: parent.left; bottom: parent.bottom; } width: parent.width / 6; preventStealing: true; onClicked: root.layoutDirection === Qt.RightToLeft? root.goNextFrame(): root.goPreviousFrame(); hoverEnabled: true; onPositionChanged: { var hWidth = width/2; var hHeight = height/2; var opacityX = mouse.x>hWidth? hWidth-(mouse.x-hWidth) : mouse.x; opacityX = opacityX/(hWidth - (Kirigami.Units.iconSizes.huge/2)); var opacityY = mouse.y>hHeight? hHeight-(mouse.y-hHeight) : mouse.y; opacityY = opacityY/(hHeight - (Kirigami.Units.iconSizes.huge/2)); leftPageIcon.opacity = opacityX*opacityY; } onExited: { leftPageIcon.opacity = 0; } Rectangle { id: leftPageIcon; anchors.centerIn: parent; width: Kirigami.Units.iconSizes.huge; height: width; radius:width/2; color: Kirigami.Theme.highlightColor; opacity: 0; Kirigami.Icon { anchors.centerIn: parent; source: "go-previous" width: parent.width*(2/3); height: width; } } } MouseArea { anchors { top: parent.top; right: parent.right; bottom: parent.bottom; } width: parent.width / 6; preventStealing: true; onClicked: root.layoutDirection === Qt.RightToLeft? root.goPreviousFrame(): root.goNextFrame(); hoverEnabled: true; onPositionChanged: { var hWidth = width/2; var hHeight = height/2; var opacityX = mouse.x>hWidth? hWidth-(mouse.x-hWidth) : mouse.x; opacityX = opacityX/(hWidth - (Kirigami.Units.iconSizes.huge/2)); var opacityY = mouse.y>hHeight? hHeight-(mouse.y-hHeight) : mouse.y; opacityY = opacityY/(hHeight - (Kirigami.Units.iconSizes.huge/2)); rightPageIcon.opacity = opacityX*opacityY; } onExited: { rightPageIcon.opacity = 0; } Rectangle { id: rightPageIcon; anchors.centerIn: parent; width: Kirigami.Units.iconSizes.huge; height: width; radius:width/2; color: Kirigami.Theme.highlightColor; opacity: 0; Kirigami.Icon { anchors.centerIn: parent; source: "go-next" width: parent.width*(2/3); height: width; } } } } diff --git a/src/app/qml/viewers/helpers/HolyRectangle.qml b/src/app/qml/viewers/helpers/HolyRectangle.qml index d3057fa..631a492 100644 --- a/src/app/qml/viewers/helpers/HolyRectangle.qml +++ b/src/app/qml/viewers/helpers/HolyRectangle.qml @@ -1,110 +1,123 @@ /* * Copyright (C) 2015 Vishesh Handa * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ import QtQuick 2.3 /** * @brief Renders a rectangle of a solid color, with a hole somewhere inside it * * The rectangle will extend past the item's borders, yielding the result of obscuring everything * behind it, except for the hole. The extent of the obscured area is the width and height of the * component instance (thus ensuring we definitely obscure the image it's put over the top of, * even when the area is moved into the viewport quite a way, such as is done when moving - * through the frames of a page) + * through the frames of a page). To stop this behavior, set the instance's clip property to true. */ Item { id: component /** * The height of the top border (distance from the top edge to the beginning of the hole) */ property int topBorder: 0 /** * The width of the left hand side border (distance from the left edge to the beginning of the hole) */ property int leftBorder: 0 /** * The width of the right hand side border (distance from the right edge to the beginning of the hole) */ property int rightBorder: 0 /** * The height of the bottom border (distance from the bottom edge to the beginning of the hole) */ property int bottomBorder: 0 /** * The color of the rectangle, around the hole */ property alias color: topRect.color + /** + * Set all the values of the hole in one go, by using an inscribed rectangle. + * It will conceptually punch a hole in HolyRect in the location and of the + * size described by the rectangle passed to the function. + * @param holeRect A rectangle which must fit inside HolyRect instance + */ function setHole(holeRect) { component.topBorder = holeRect.y; component.leftBorder = holeRect.x; component.rightBorder = component.width - (holeRect.x + holeRect.width); component.bottomBorder = component.height - (holeRect.y + holeRect.height); } + Behavior on topBorder { NumberAnimation { duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } } + Behavior on leftBorder { NumberAnimation { duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } } + Behavior on rightBorder { NumberAnimation { duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } } + Behavior on bottomBorder { NumberAnimation { duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } } + Behavior on color { ColorAnimation { duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } } + Behavior on opacity { NumberAnimation { duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } } + Rectangle { id: topRect anchors { top: parent.top topMargin: -component.height left: parent.left leftMargin: -component.leftBorder right: parent.right rightMargin: -component.rightBorder } height: component.height + component.topBorder } Rectangle { id: leftRect anchors { top: parent.top left: parent.left leftMargin: -component.width bottom: parent.bottom } width: component.width + component.leftBorder color: topRect.color } Rectangle { id: rightRect anchors { top: parent.top right: parent.right rightMargin: -component.width bottom: parent.bottom } width: component.width + component.rightBorder color: topRect.color } Rectangle { id: bottomRect anchors { left: parent.left leftMargin: -component.leftBorder right: parent.right rightMargin: -component.rightBorder bottom: parent.bottom bottomMargin: -component.height } height: component.height + component.bottomBorder color: topRect.color } }