I have made the following changes intended for : CE:Apps / qmlgallery Please review and accept or decline. BOSS has already run some checks on this request. See the "Messages from BOSS" section below.
https://build.pub.meego.com//request/show/8290 Thank You, Robin Burchell [This message was auto-generated] --- Request # 8290: Messages from BOSS: State: review at 2013-03-03T00:41:26 by bossbot Reviews: accepted by bossbot : Prechecks succeeded. new for CE-maintainers : Please replace this text with a review and approve/reject the review (not the SR). BOSS will take care of the rest Changes: submit: home:w00t:branches:CE:Apps / qmlgallery -> CE:Apps / qmlgallery changes files: -------------- --- qmlgallery.changes +++ qmlgallery.changes @@ -0,0 +1,17 @@ +* Sun Mar 03 2013 Robin Burchell <[email protected]> - 0.1.0 +- Fixes NEMO#498: Slideshow mode is greyed out if there are no elements (from Andrea) +- Fixes NEMO#402: Exiting slideshow mode doesn't change the index of the displayed element (from Andrea) +- Fixes NEMO#397: Fullscreen mode can now be enabled/disabled in all cases (from Andrea) +- Fixes NEMO#196: gallery handles external images/videos (from Andrea) +- Fixes NEMO#404: Added doubletap to zoom (from Andrea) +- Fixes NEMO#218: Toolbar and statusbar now autohide (from Andrea) +- Refactor pinch zoom logic (from Andrea) +- Remove useless Item (from Andrea) +- Restructure mouse handling (from Andrea) +- Allow ImageContainer to work without the gallery model (from Andrea) +- Don't resize the image when toggling fullscreen mode (from Andrea) +- Make toolbar semi-transparent (from Andrea) +- Add packaging to git repository (from Robin) +- Use unified empty state view (from Robin) +- Tidy up packaging a bit (from Robin) + old: ---- qmlgallery-0.0.10.tar.bz2 new: ---- qmlgallery-0.1.0.tar.bz2 spec files: ----------- --- qmlgallery.spec +++ qmlgallery.spec @@ -9,13 +9,14 @@ # << macros Summary: Photo Gallery for Nemo -Version: 0.0.10 +Version: 0.1.0 Release: 1 Group: Applications/System License: BSD URL: https://github.com/nemomobile/qmlgallery Source0: %{name}-%{version}.tar.bz2 Source100: qmlgallery.yaml +Requires: qt-components >= 1.4.8 Requires: libdeclarative-gallery Requires: libdeclarative-multimedia Requires: nemo-qml-plugins-thumbnailer @@ -43,12 +44,7 @@ # >> build pre # << build pre -%qmake \ - MEEGO_VERSION_MAJOR=1 \ - MEEGO_VERSION_MINOR=2 \ - MEEGO_VERSION_PATCH=0 \ - MEEGO_EDITION=harmattan \ - DEFINES+=MEEGO_EDITION_HARMATTAN +%qmake make %{?jobs:-j%jobs} other changes: -------------- ++++++ qmlgallery-0.0.10.tar.bz2 -> qmlgallery-0.1.0.tar.bz2 --- qml/ImageContainer.qml +++ qml/ImageContainer.qml @@ -36,219 +36,111 @@ Item { id: imgContainer property int index: -1 - property variant imgController: imageController - property bool isVideo: galleryModel.isVideo(index) - - //used inside ImagePage's imgFlickable to get the bounding rectangle of the image + property variant pinchingController + property variant pageStack + property string imageSource: "" + property string videoSource: "" + property bool isVideo: false + property alias flickableArea: flickImg + property int doubleClickInterval: 350 property alias image: img - - width: imgController.imgContainerWidth - height: imgController.imgContainerHeight - - function resetZoom() { - //resetting all variables related to pinch-to-zoom - img.scale = 1 - flickImg.contentX = flickImg.contentY = 0 - pinchImg.lastContentX = pinchImg.lastContentY = pinchImg.deltaX = pinchImg.deltaY = 0 - pinchImg.lastScaleX = pinchImg.lastScaleY = 1 - pinchImg.isZoomingOut = false + property int videoThumbnailSize: 480 + //this is to trick the statusBar, so that when it shows the imageContainer isn't moved downwards + y: -(height - parent.height - appWindow.pageStack.toolBar.height) + + //this long ternary conditional expression is to make so that the size is not changed before the screen rotates. + //i.e. if you just use screen.platformHeight/Width the container will resize BEFORE the orientation change + //animation is started, thus causing an unexpected behaviour + width: (parent.width > parent.height) ? + ((screen.platformWidth > screen.platformHeight) ? screen.platformWidth : screen.platformHeight) : + ((screen.platformWidth > screen.platformHeight) ? screen.platformHeight : screen.platformWidth) + height: (parent.width > parent.height) ? + ((screen.platformWidth > screen.platformHeight) ? screen.platformHeight : screen.platformWidth) : + ((screen.platformWidth > screen.platformHeight) ? screen.platformWidth : screen.platformHeight) + + signal clickedWhileZoomed() + signal pressedWhileNotZoomed() + + Timer { + id: doubleClickTimer + interval: doubleClickInterval } - PinchArea { - id: pinchImg - anchors.fill: imgContainer - - //Disable the pincharea if the listview is scrolling, to avoid problems - enabled: (!imgController.moving && !isVideo) - pinch.target: img - pinch.maximumScale: 5 - pinch.dragAxis: Pinch.NoDrag - - property real lastContentX: 0 - property real lastContentY: 0 - property real lastScaleX: 1 - property real lastScaleY: 1 - property real deltaX: 0 - property real deltaY: 0 - property bool initializedX: false - property bool initializedY: false - property bool isZoomingOut: false - - - function updateContentX() { - - //Only calculate the correct ContentX if the image is wider than the screen, otherwise keep it centered (contentX = 0 in the else branch) - if (rect.width == imgController.width) { - - //Anchors the image to the left - if (flickImg.contentX < 0){ - deltaX = 0.0 - lastContentX = 0.0 - flickImg.contentX = 0.0 - } - else { - //if the right end of the image is inside the screen area, lock it to the right and zoom out using right edge as an anchor - if ((flickImg.contentWidth - flickImg.contentX < parent.width) && isZoomingOut) { - - //align to the right - flickImg.contentX -= parent.width - (flickImg.contentWidth - flickImg.contentX) - - //Algo: set variable as if a new pinch starting from right edge were triggered - lastContentX = flickImg.contentX - deltaX = flickImg.contentX + parent.width - lastScaleX = img.scale - } - - flickImg.contentX = (lastContentX + deltaX * ((img.scale / lastScaleX) - 1.0 )) - } - } - else { - flickImg.contentX = 0 - } - } - - function updateContentY() { + Flickable { + id: flickImg - //Only calculate the correct ContentY if the image is taller than the screen, otherwise keep it centered (contentY = 0 in the else branch) - if (rect.height == imgController.height) { + interactive: img.scale > 1 - //Anchors the image to the top when zooming out - if (flickImg.contentY < 0) { - deltaY = 0.0 - lastContentY = 0.0 - flickImg.contentY = 0.0 - } - else { - //if the bottom end of the image is inside the screen area, lock it to the bottom and zoom out using bottom edge as an anchor - if ((flickImg.contentHeight - flickImg.contentY < parent.height) && isZoomingOut) { - //align to the bottom - flickImg.contentY -= parent.height - (flickImg.contentHeight - flickImg.contentY) - - //Algo: set variable as if a new pinch starting from bottom edge were triggered - lastContentY = flickImg.contentY - deltaY = flickImg.contentY + parent.height - lastScaleY = img.scale - } - flickImg.contentY = (lastContentY + deltaY * ((img.scale / lastScaleY) - 1.0 )) - } - } - else { - flickImg.contentY = 0 - } - } - - - onPinchUpdated: { - //Am I zooming in or out? - if (pinch.scale > pinch.previousScale) isZoomingOut = false - else isZoomingOut = true - - //Get updated "zoom center point" values when the image is completely zoomed out - if(img.scale == 1) { - //This is so that everytime you zoom out, the new zoom is started with updated values - initializedX = false - initializedY = false - } + anchors.centerIn: parent + width: Math.min(img.width*img.scale, imgContainer.width) + height: Math.min(img.height*img.scale, imgContainer.height) - //i.e. everytime the image is wider than the screen, it should actually be - // img.width == imgController.width, but this condition is rarely met because of numeric error - if (rect.width == imgController.width) { - if (!initializedX ) { - //If it has not already been set by the "if (height == parent.imgController.height)" branch, set the scale here - lastScaleX = img.scale - - lastContentX = flickImg.contentX - deltaX = flickImg.contentX + pinch.center.x - initializedX = true; - } + transformOrigin: Item.TopLeft - } + contentWidth: img.width * img.scale + contentHeight: img.height * img.scale - if (rect.height == imgController.height) { - if (!initializedY) { - //If it has not already been set by the "if (width == imgController.width)", set the scale here - lastScaleY = img.scale - - lastContentY = flickImg.contentY - deltaY = flickImg.contentY + pinch.center.y - initializedY = true; - } + onContentWidthChanged: { + //this check is because the first time this slot is called, pinchingController isn't set yet + if (pinchingController && pinchingController.pinch.active) { + pinchingController.updateContentX() + pinchingController.updateContentY() } - // updateContentX and updateContentY are called after the scale on the target item updates bindings } - - onPinchFinished: { - lastContentX = flickImg.contentX - lastContentY = flickImg.contentY - - initializedX = false - initializedY = false + onContentHeightChanged: { + if (pinchingController && pinchingController.pinch.active) { + pinchingController.updateContentX() + pinchingController.updateContentY() + } } - } - - Item { - id: rect - anchors.centerIn: parent - - width: Math.min(img.width*img.scale, parent.width) - height: Math.min(img.height*img.scale, parent.height) - - Flickable { - id: flickImg + Image { + id: img + width: (fitsVertically) ? (imgContainer.height * imgRatio) : imgContainer.width + height: (fitsVertically) ? (imgContainer.height) : (imgContainer.width / imgRatio) + + //(isVideo --> imgRatio = 1.0) because we're using a square dummy thumbnail + property real imgRatio: isVideo ? 1.0 : implicitWidth / implicitHeight + property bool fitsVertically: imgRatio < (imgContainer.width / imgContainer.height) - anchors.fill: rect transformOrigin: Item.TopLeft + asynchronous: true + source: isVideo ? "qrc:/images/DefaultVideoThumbnail.jpg" : imageSource + sourceSize.width: 1200 + + MouseArea { + anchors.fill: parent + + //this is only called when img.scale != 1 + //----> WARNING!!!: this causes a problem! double-press will call the zoomIn/Out function, while only + //doubleCLICK will stop the toolbarTimer in ImagePage! So if you double-press (and keep it pressed) + //both the zoomIn/Out function will be called and the toolBar in ImagePage will show/hide!! + //TODO: look for a way to fix this + onClicked: { + imgContainer.clickedWhileZoomed() + } + + onPressed: { + //setting mouse.accepted to false means we'll only receive the onPressed of this event, + //the rest will be propagated to the area underneath + //if img.scale == 1 send events underneath, otherwise emit the signal (to make it + //propagate beyond the Flickable parent) + if (img.scale == 1) { + imgContainer.pressedWhileNotZoomed() + mouse.accepted = false + } - contentWidth: img.width * img.scale - contentHeight: img.height * img.scale - - onContentWidthChanged: { - pinchImg.updateContentX() - pinchImg.updateContentY() - } - onContentHeightChanged: { - pinchImg.updateContentX() - pinchImg.updateContentY() - } - - Image { - id: img - width: (fitsVertically) ? (imgController.height * imgRatio) : imgController.width - height: (fitsVertically) ? (imgController.height) : (imgController.width / imgRatio) - - property int imgWidth: isVideo ? videoThumbnailSize : (info.available ? info.metaData.width : -1) - property int imgHeight: isVideo ? videoThumbnailSize : (info.available ? info.metaData.height : -1) - property real imgRatio: imgWidth / imgHeight - property bool fitsVertically: imgRatio < (imgContainer.width / imgContainer.height) - - //DocumentGalleryItem automatically recognizes the rootType of the file - DocumentGalleryItem { - id: info - item: galleryModel.get(index).itemId - autoUpdate: true - properties: ["width", "height", "url"] - } - - transformOrigin: Item.TopLeft - asynchronous: true - source: isVideo ? "qrc:/images/DefaultVideoThumbnail.jpg" : galleryModel.get(index).url - sourceSize.width: 1200 - - //Disable ListView scrolling if you're zooming - onScaleChanged: { - if (scale != 1 && imgController.flickAreaEnabled == true) imgController.flickAreaEnabled = false - else if (scale == 1 && imgController.flickAreaEnabled == false) imgController.flickAreaEnabled = true - } - - MouseArea { - anchors.fill: parent - - onClicked: { - if (!isVideo) { - if (imgController.doubleClickTimer.running) resetZoom() - else imgController.doubleClickTimer.start() + if (!isVideo) { + if (doubleClickTimer.running) { + if (img.scale != 1) { + pinchingController.resetZoom() + } + else { + var point = mapToItem(imgContainer, mouse.x, mouse.y) + pinchingController.quickZoomIn(point.x, point.y, 2.5) + } } + else doubleClickTimer.start() } } } --- qml/ImagePage.qml +++ qml/ImagePage.qml @@ -37,15 +37,10 @@ id: imageController anchors.fill: parent - clip: true tools: imgTools + clip: true - property int imgContainerWidth: width - property int imgContainerHeight: height property variant galleryModel - // XXX: This is not actually the visible index; it's the index shown when - // loading the page. Use middle.index instead. Should be refactored. - property int visibleIndex: 0 property real firstPressX property real pressX property int flickToX: 0 @@ -56,14 +51,32 @@ property variant middle: three property variant rightMiddle: four property variant rightMost: five + property bool videoPlayerRequested: false + + //this is the index which has to be passed as a parameter when creating this page + //it will only be used for initialization + property int parameterIndex + + property int visibleIndex + + Component.onCompleted: { + //this is to make so that when visibleIndex is changed the image containers have already been + //initialized + visibleIndex = parameterIndex + updateImagesIndexes() + } + + //this will make so that every time visibleIndex is changed, the image containers will all load + //the correct images. + //visibleIndex is the reliable source to know what index is currently being displayed on screen + onVisibleIndexChanged: { + updateImagesIndexes() + } + property real swipeThreshold: 40 property real leftMostOptimalX: -width*2 //number of pixel you have to move before the Pinch Area is disabled property real pinchThreshold: 3 - property alias flickAreaEnabled: imgFlickable.enabled - property variant doubleClickTimer: timer - property int doubleClickInterval: 350 - property int videoThumbnailSize: 480 //This property forces the middle item to be visible on screen by keeping the leftMost item at x = leftMostOptimalX //You have to set it to FALSE when you want to want to modify leftMost's x property, and set it back to true //to be sure that the middle item will be the one centered on screen. @@ -72,13 +85,15 @@ onWidthChanged: { if (!middle.isVideo) - middle.resetZoom() + pinchImg.resetZoom() } - function showVideoPlayer() { - appWindow.pageStack.push(Qt.resolvedUrl("VideoPlayer.qml"), - {videoSource: galleryModel.get(middle.index).url}, - true) + function updateImagesIndexes() { + leftMost.index = modulus(visibleIndex - 2, galleryModel.count); + leftMiddle.index = modulus(visibleIndex - 1, galleryModel.count); + middle.index = modulus(visibleIndex, galleryModel.count); + rightMiddle.index = modulus(visibleIndex + 1, galleryModel.count); + rightMost.index = modulus(visibleIndex + 2, galleryModel.count); } function modulus(a, b) { @@ -86,10 +101,10 @@ else return a % b } - function isInside(x, y, rect) { - var rectAbsolute = rect.mapToItem(imageController, rect.x, rect.y) - return ((x > rectAbsolute.x) && (x < rectAbsolute.x + rect.width) && - (y > rectAbsolute.y) && (y < rectAbsolute.y + rect.height)) + function showVideoPlayer(fileName) { + pageStack.push(Qt.resolvedUrl("VideoPlayer.qml"), + {videoSource: fileName}, + true) } function swapLeftMost() { @@ -104,8 +119,8 @@ middle = rightMiddle rightMiddle = rightMost rightMost = oldLeftMost - //set the index (relative to galleryModel) of the image which has to be loaded by the shifted image container - rightMost.index = modulus(middle.index + 2, galleryModel.count); + + visibleIndex = middle.index; } function swapRightMost() { @@ -119,7 +134,9 @@ middle = leftMiddle leftMiddle = leftMost leftMost = oldRightMost - leftMost.index = modulus(middle.index - 2, galleryModel.count); + + visibleIndex = middle.index; + } NumberAnimation { @@ -154,62 +171,58 @@ when: keepMiddleItemAligned } - ImageContainer { - id: one; - index: modulus(visibleIndex - 2, galleryModel.count) - x: leftMostOptimalX - } - - ImageContainer { - id: two - anchors.left: one.right - index: modulus(visibleIndex - 1, galleryModel.count) - } + ZoomController { + id: pinchImg - ImageContainer { - id: three - anchors.left: two.right - index: visibleIndex - } + //Disable the pincharea if the listview is scrolling, to avoid problems + enabled: (!imageController.moving && !middle.isVideo) - ImageContainer { - id: four - anchors.left: three.right - index: modulus (visibleIndex + 1, galleryModel.count) + pinchTarget: middle.image + connectedFlickable: middle.flickableArea + targetContainer: middle } - ImageContainer { - id: five - anchors.left: four.right - index: modulus (visibleIndex + 2, galleryModel.count) + Connections { + target: middle + onClickedWhileZoomed: listFlickable.handleClick() + onPressedWhileNotZoomed: if (middle.isVideo) videoPlayerRequested = true } Timer { - id: timer - interval: doubleClickInterval + id: toolbarAutohideTimer + interval: 2500 + running: !appWindow.fullscreen && (pageMenu.status === DialogStatus.Closed) + onTriggered: appWindow.fullscreen = true } MouseArea { - id: imgFlickable + id: listFlickable anchors.fill: parent property bool pressedForClick: false - //HACK: this mousarea is disabled when the image is zoomed, and the mousearea inside the imageContainer's img is disabled when zoom factor is 1, - //so we have to get the doubleClick event here when the zoom factor is 1, and inside imageContainer's img when the image is zoomed. - //This is due to the high number of mouse areas (pincharea, flickable, multiple mouseareas) available in the same view - //Adding another MouseArea to handle this only made things worse - //comment added by faenil - onClicked: { - if (!imageController.moving && isInside(mouse.x, mouse.y, middle.image)) { - if (!middle.isVideo) { - if (doubleClickTimer.running) {} //TODO: IMPLEMENT ZOOM-IN VIA DOUBLECLICK INSIDE THE CURLY BRACKETS - else doubleClickTimer.start() + function handleClick() { + if (videoPlayerRequested) { + videoPlayerRequested = false + imageController.showVideoPlayer(middle.videoSource) + } + else { + + if (toolbarTimer.running) { + toolbarTimer.stop() + } else { + toolbarTimer.start() } - else showVideoPlayer() } } + //we use this to be able to not call singleclick handlers when the user is actually doubleclicking + Timer { + id: toolbarTimer + interval: 350 + onTriggered: appWindow.fullscreen = !appWindow.fullscreen + } + onPressed: { firstPressX = mouseX pressX = mouseX @@ -226,7 +239,7 @@ pressedForClick = false } - //Only move the image if we're sure the user isn't trying to pinch + //moving == true means the user isn't trying to pinch if (moving) { leftMost.x = leftMost.x - (pressX - mouseX) pressX = mouseX @@ -234,38 +247,87 @@ } onReleased: { + if (pressedForClick) { + handleClick() + pressedForClick = false + } + if (middle.x >= swipeThreshold) { //move it left - flickToX = leftMostOptimalX + imgContainerWidth + flickToX = leftMostOptimalX + parent.width } else if (middle.x <= -swipeThreshold) { //move it right - flickToX = leftMostOptimalX -imgContainerWidth + flickToX = leftMostOptimalX -parent.width } else { //bring it back flickToX = leftMostOptimalX } - if (pressedForClick) { - appWindow.fullscreen = !appWindow.fullscreen - pressedForClick = false - } - flickFromX = leftMost.x flickTo.start() } } + ImageContainer { + id: one; x: leftMostOptimalX + pinchingController: pinchImg + pageStack: appWindow.pageStack + isVideo: galleryModel.isVideo(index) + imageSource: galleryModel.get(index).url + videoSource: isVideo ? galleryModel.get(index).url : "" + visible: (middle == one || moving) + } + + ImageContainer { + id: two; anchors.left: one.right + pinchingController: pinchImg + pageStack: appWindow.pageStack + isVideo: galleryModel.isVideo(index) + imageSource: galleryModel.get(index).url + videoSource: isVideo ? galleryModel.get(index).url : "" + } + + //this is the item which is in the middle by default + ImageContainer { + id: three; anchors.left: two.right + pinchingController: pinchImg + pageStack: appWindow.pageStack + isVideo: galleryModel.isVideo(index) + imageSource: galleryModel.get(index).url + videoSource: isVideo ? galleryModel.get(index).url : "" + } + + ImageContainer { + id: four; anchors.left: three.right + pinchingController: pinchImg + pageStack: appWindow.pageStack + isVideo: galleryModel.isVideo(index) + imageSource: galleryModel.get(index).url + videoSource: isVideo ? galleryModel.get(index).url : "" + } + + ImageContainer { + id: five; anchors.left: four.right + pinchingController: pinchImg + pageStack: appWindow.pageStack + isVideo: galleryModel.isVideo(index) + imageSource: galleryModel.get(index).url + videoSource: isVideo ? galleryModel.get(index).url : "" + } + Menu { id: pageMenu MenuLayout { MenuItem { text: "Slideshow" onClicked: appWindow.pageStack.push(Qt.resolvedUrl("ImageSlideshowPage.qml"), - {visibleIndex: imageController.middle.index, - galleryModel: imageController.galleryModel}, + { visibleIndex: imageController.visibleIndex, + controller: imageController, + galleryModel: imageController.galleryModel }, true) + enabled: galleryModel.count > 0 } } } @@ -286,4 +348,26 @@ onClicked: (pageMenu.status === DialogStatus.Closed) ? pageMenu.open() : pageMenu.close() } } + + states: State { + name: "active" + when: status === PageStatus.Active || status === PageStatus.Activating + + PropertyChanges { + target: appWindow.pageStack.toolBar + opacity: 0.8 + } + } + + transitions: Transition { + from: "active" + reversible: true + + NumberAnimation { + target: appWindow.pageStack.toolBar + property: "opacity" + duration: 250 + } + } + } --- qml/ImageSlideshowPage.qml +++ qml/ImageSlideshowPage.qml @@ -38,6 +38,12 @@ anchors.fill: parent property int slideVisibleTime: 4000 + + //this is to make so that when the slideshow page is popped, the list view will be showing the + //last element displayed while in slideshow mode + //Related to: NEMO#402 + property variant controller + property int visibleIndex property int phase property variant galleryModel @@ -52,6 +58,7 @@ fillMode: Image.PreserveAspectFit Behavior on opacity { NumberAnimation { duration: 1000 } } } + Image { id: image2 asynchronous: true @@ -62,6 +69,7 @@ fillMode: Image.PreserveAspectFit Behavior on opacity { NumberAnimation { duration: 1000 } } } + SequentialAnimation { id: mainLoop loops: Animation.Infinite @@ -73,11 +81,20 @@ } } PauseAnimation { duration: 1001 } - ScriptAction { script: loadNextImage() } + ScriptAction { + script: { + visibleIndex++ + if (visibleIndex >= galleryModel.count) + visibleIndex = 0 + loadNextImage() + } + } } + MouseArea { anchors.fill: parent onPressed: { + if (controller != undefined) controller.visibleIndex = visibleIndex mainLoop.stop() image1.source = "" image2.source = "" @@ -85,24 +102,20 @@ appWindow.pageStack.pop() } } + Component.onCompleted: { appWindow.fullscreen = true - if (visibleIndex < galleryModel.count) { - phase = 0 - image1.source = galleryModel.get(visibleIndex).url - loadNextImage() - mainLoop.start() - } + phase = 0 + image1.source = galleryModel.get(visibleIndex).url + loadNextImage() + mainLoop.start() } - function loadNextImage() { - visibleIndex++ - if (visibleIndex >= galleryModel.count) - visibleIndex = 0 + function loadNextImage() { if (phase === 0) - image2.source = galleryModel.get(visibleIndex).url + image2.source = galleryModel.get((visibleIndex + 1) % galleryModel.count).url else - image1.source = galleryModel.get(visibleIndex).url + image1.source = galleryModel.get((visibleIndex + 1) % galleryModel.count).url phase = 1-phase } } --- qml/MainPage.qml +++ qml/MainPage.qml @@ -46,7 +46,7 @@ delegate: GalleryDelegate { MouseArea { anchors.fill: parent - onClicked: appWindow.pageStack.push(Qt.resolvedUrl("ImagePage.qml"), {visibleIndex: index, galleryModel: gallery} ) + onClicked: appWindow.pageStack.push(Qt.resolvedUrl("ImagePage.qml"), {parameterIndex: index, galleryModel: gallery} ) } } } @@ -102,6 +102,7 @@ MenuItem { text: "Slideshow" onClicked: appWindow.pageStack.push(Qt.resolvedUrl("ImageSlideshowPage.qml"), { visibleIndex: 0, galleryModel: gallery }) + enabled: gallery.count > 0 } } } --- qml/SingleImagePage.qml +++ qml/SingleImagePage.qml @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2012 Andrea Bernabei <[email protected]> + * + * You may use this file under the terms of the BSD license as follows: + * + * "Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Nemo Mobile nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + */ + +import QtQuick 1.1 +import com.nokia.meego 1.0 +import QtMobility.gallery 1.1 + +Page { + id: singleImagePage + anchors.fill: parent + + tools: imgTools + + //filename of the element we're showing + property alias imageSource: singleImage.imageSource + + property real swipeThreshold: 40 + //number of pixel you have to move before the Pinch Area is disabled + property real pinchThreshold: 3 + + onWidthChanged: { + //we're assuming this is only used for images (videos are auto-played, so they won't use this page) + //if we decide to show this page with the video thumbnail before playing the video, a check + //has to be added here (for instance: if (!video) resetZoom()) + pinchImg.resetZoom() + } + + ZoomController { + id: pinchImg + + //if we decide this page is also used to show video thumbnails + //this area will have to be disabled when showing video thumbnails + + pinchTarget: singleImage.image + connectedFlickable: singleImage.flickableArea + targetContainer: singleImage + } + + Connections { + target: singleImage + onClickedWhileZoomed: fullScreenModeArea.handleClick() + } + + Timer { + id: toolbarAutohideTimer + interval: 2500 + running: !appWindow.fullscreen + onTriggered: appWindow.fullscreen = true + } + + MouseArea { + id: fullScreenModeArea + anchors.fill: parent + + function handleClick() { + if (toolbarTimer.running) { + toolbarTimer.stop() + } else { + toolbarTimer.start() + } + } + + //we use this to be able to not call singleclick handlers when the user is actually doubleclicking + Timer { + id: toolbarTimer + interval: 350 + onTriggered: appWindow.fullscreen = !appWindow.fullscreen + } + + onClicked: handleClick() + } + + ImageContainer { + id: singleImage + pinchingController: pinchImg + } + + ToolBarLayout { + id: imgTools + ToolIcon { + platformIconId: "toolbar-back" + anchors.left: (parent === undefined) ? undefined : parent.left + onClicked: { + appWindow.fullscreen = false + appWindow.pageStack.pop() + } + } + } + + states: State { + name: "active" + when: status === PageStatus.Active || status === PageStatus.Activating + + PropertyChanges { + target: appWindow.pageStack.toolBar + opacity: 0.8 + } + } + + transitions: Transition { + from: "active" + reversible: true + + NumberAnimation { + target: appWindow.pageStack.toolBar + property: "opacity" + duration: 250 + } + } + +} --- qml/ZoomController.qml +++ qml/ZoomController.qml @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2012 Andrea Bernabei <[email protected]> + * + * You may use this file under the terms of the BSD license as follows: + * + * "Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Nemo Mobile nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + */ + +import QtQuick 1.1 + +PinchArea { + anchors.fill: parent + + property variant pinchTarget + property variant connectedFlickable + property variant targetContainer + + pinch.target: pinchTarget + pinch.maximumScale: 5 + pinch.dragAxis: Pinch.NoDrag + + property real lastContentX: 0 + property real lastContentY: 0 + property real lastScaleX: 1 + property real lastScaleY: 1 + property real deltaX: 0 + property real deltaY: 0 + property bool initializedX: false + property bool initializedY: false + property bool isZoomingOut: false + property real middleQuickZoomInContentX: 0 + property real middleQuickZoomInContentY: 0 + property real middleQuickZoomInScale: 1.0 + property real finalQuickZoomInContentX: 0 + property real finalQuickZoomInContentY: 0 + property real finalQuickZoomInScale: 1.0 + property bool needsCenteringAnimation: false + + function resetZoom() { + //resetting all variables related to pinch-to-zoom + pinchTarget.scale = 1 + connectedFlickable.contentX = connectedFlickable.contentY = 0 + lastContentX = lastContentY = 0 + deltaX = deltaY = 0 + lastScaleX = lastScaleY = 1 + isZoomingOut = false + } + + //quickZoomIn works in 2 phases: + //1) zoom image (until it fills screen in case we're going to execute phase 2 too) + //2) (ONLY IF the requested scale factor exceeds the screen-filling scale factor) Zoom again centered on the finger position + function quickZoomIn(centerX, centerY, finalScale) { + //TODO: this needs to be updated when we decide how to get the original size of an image + var imgRatio = pinchTarget.implicitWidth / pinchTarget.implicitHeight + var fitsVertically = imgRatio < (targetContainer.width / targetContainer.height) + + //the scaling factor needed to make the image fill the screen + middleQuickZoomInScale = (fitsVertically ? screen.platformWidth : screen.platformHeight) / (fitsVertically ? pinchTarget.width : pinchTarget.height) + + //if we don't need the image to fill the screen + if (middleQuickZoomInScale > finalScale) middleQuickZoomInScale = finalScale + + if (fitsVertically) { + middleQuickZoomInContentX = 0 + middleQuickZoomInContentY = (centerY * middleQuickZoomInScale) - centerY + needsCenteringAnimation = (pinchTarget.width * finalScale) > screen.platformWidth + } + else { + middleQuickZoomInContentX = (centerX * middleQuickZoomInScale) - centerX + middleQuickZoomInContentY = 0 + needsCenteringAnimation = (pinchTarget.height * finalScale) > screen.platformHeight + } + + if (needsCenteringAnimation) { + finalQuickZoomInScale = finalScale + var remainingScaleFactorX = finalQuickZoomInScale / (screen.platformWidth / pinchTarget.width) + var remainingScaleFactorY = finalQuickZoomInScale / (screen.platformHeight / pinchTarget.height) + finalQuickZoomInContentX = (centerX * remainingScaleFactorX) - centerX + finalQuickZoomInContentY = (centerY * remainingScaleFactorY) - centerY + } + quickZoomInScalingAnimation.start() + } + + //first scale the image so that it fills the screen + ParallelAnimation { + id: quickZoomInScalingAnimation + NumberAnimation {target: pinchTarget; property: "scale"; to: middleQuickZoomInScale; duration: 100} + NumberAnimation {target: connectedFlickable; property: "contentX"; to: middleQuickZoomInContentX; duration: 100} + NumberAnimation {target: connectedFlickable; property: "contentY"; to: middleQuickZoomInContentY; duration: 100} + onCompleted: if (needsCenteringAnimation) quickZoomInCenteringAnimation.start() + } + + //then create zoom effect centered on the doubleclick coordinates (IF NEEDED) + ParallelAnimation { + id: quickZoomInCenteringAnimation + NumberAnimation {target: pinchTarget; property: "scale"; to: finalQuickZoomInScale; duration: 100} + NumberAnimation {target: connectedFlickable; property: "contentX"; to: finalQuickZoomInContentX; duration: 100} + NumberAnimation {target: connectedFlickable; property: "contentY"; to: finalQuickZoomInContentY; duration: 100} + } + + function updateContentX() { + //Only calculate the correct ContentX if the image is wider than the screen, otherwise keep it centered (contentX = 0 in the else branch) + if (connectedFlickable.width == targetContainer.width) { + + //Anchors the image to the left + if (connectedFlickable.contentX < 0){ + deltaX = 0.0 + lastContentX = 0.0 + connectedFlickable.contentX = 0.0 + } + else { + //if the right end of the image is inside the screen area, lock it to the right and zoom out using right edge as an anchor + if ((connectedFlickable.contentWidth - connectedFlickable.contentX < parent.width) && isZoomingOut) { + + //align to the right + connectedFlickable.contentX -= parent.width - (connectedFlickable.contentWidth - connectedFlickable.contentX) + + //Algo: set variable as if a new pinch starting from right edge were triggered + lastContentX = connectedFlickable.contentX + deltaX = connectedFlickable.contentX + parent.width + lastScaleX = pinchTarget.scale + } + connectedFlickable.contentX = (lastContentX + deltaX * ((pinchTarget.scale / lastScaleX) - 1.0 )) + } + } + else { + connectedFlickable.contentX = 0 + } + } + + function updateContentY() { + //Only calculate the correct ContentY if the image is taller than the screen, otherwise keep it centered (contentY = 0 in the else branch) + if (connectedFlickable.height == targetContainer.height) { + + //Anchors the image to the top when zooming out + if (connectedFlickable.contentY < 0) { + deltaY = 0.0 + lastContentY = 0.0 + connectedFlickable.contentY = 0.0 + } + else { + //if the bottom end of the image is inside the screen area, lock it to the bottom and zoom out using bottom edge as an anchor + if ((connectedFlickable.contentHeight - connectedFlickable.contentY < parent.height) && isZoomingOut) { + //align to the bottom + connectedFlickable.contentY -= parent.height - (connectedFlickable.contentHeight - connectedFlickable.contentY) + + //Algo: set variable as if a new pinch starting from bottom edge were triggered + lastContentY = connectedFlickable.contentY + deltaY = connectedFlickable.contentY + parent.height + lastScaleY = pinchTarget.scale + } + connectedFlickable.contentY = (lastContentY + deltaY * ((pinchTarget.scale / lastScaleY) - 1.0 )) + } + } + else { + connectedFlickable.contentY = 0 + } + } + + onPinchUpdated: { + //Am I zooming in or out? + if (pinch.scale > pinch.previousScale) isZoomingOut = false + else isZoomingOut = true + + //Get updated "zoom center point" values when the image is completely zoomed out + if(pinchTarget.scale == 1) { + //This is so that everytime you zoom out, the new zoom is started with updated values + initializedX = false + initializedY = false + } + + //i.e. everytime the image is wider than the screen, it should actually be + // pinchTarget.width == targetContainer.width, but this condition is rarely met because of numeric error + if (connectedFlickable.width == targetContainer.width) { + if (!initializedX ) { + //If it has not already been set by the "if (height == parent.targetContainer.height)" branch, set the scale here + lastScaleX = pinchTarget.scale + + lastContentX = connectedFlickable.contentX + deltaX = connectedFlickable.contentX + pinch.center.x + initializedX = true; + } + + } + + if (connectedFlickable.height == targetContainer.height) { + if (!initializedY) { + //If it has not already been set by the "if (width == targetContainer.width)", set the scale here + lastScaleY = pinchTarget.scale + + lastContentY = connectedFlickable.contentY + deltaY = connectedFlickable.contentY + pinch.center.y + initializedY = true; + } + } + // updateContentX and updateContentY are called after the scale on the target item updates bindings + } + + onPinchFinished: { + lastContentX = connectedFlickable.contentX + lastContentY = connectedFlickable.contentY + + initializedX = false + initializedY = false + } + +} --- qml/api/GalleryView.qml +++ qml/api/GalleryView.qml @@ -30,7 +30,7 @@ */ import QtQuick 1.1 -import com.nokia.meego 1.0 +import com.nokia.meego 1.2 import org.nemomobile.thumbnailer 1.0 GridView { @@ -72,20 +72,8 @@ onCurrentOrientationChanged: updateThumbnailSize() } - Text { - id: noElementsFoundText - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: 20 - anchors.rightMargin: 20 - - visible: parent.model.count == 0 && parent.model.progress == 1.0 - + ViewPlaceholder { text: "No elements found..." - color: "lightgrey" - font.pointSize: 26 - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap + enabled: parent.model.count == 0 && parent.model.progress == 1.0 } } --- qml/main.qml +++ qml/main.qml @@ -35,8 +35,9 @@ PageStackWindow { id: appWindow - initialPage: mainPage property bool fullscreen: false + + initialPage: mainPage showStatusBar: !fullscreen showToolBar: !fullscreen @@ -47,4 +48,21 @@ Component.onCompleted: { theme.inverted = true } + + function displayFile(filename) { + console.log("displayFile:" + filename) + appWindow.pageStack.pop(null) // Unwind to top of the stack, wherever we are + + switch (gallery.isVideo(filename)) { + case 0: + appWindow.pageStack.push(Qt.resolvedUrl("SingleImagePage.qml"), { imageSource: filename }) + break + case 1: + appWindow.pageStack.push(Qt.resolvedUrl("VideoPlayer.qml"), { videoSource: filename }) + break + case -1: + console.log("displayFile: ERROR WHILE LOADING THE FILE, FILE NOT FOUND") + + } + } } --- qmlgallery.desktop +++ qmlgallery.desktop @@ -3,8 +3,9 @@ Type=Application Terminal=false Name=Gallery -Exec=invoker -s --type=d /usr/bin/qmlgallery -fullscreen +Exec=invoker -s --type=d /usr/bin/qmlgallery -fullscreen %u Icon=icons-Applications-photos X-Window-Icon= X-HildonDesk-ShowInToolbar=true X-Osso-Type=application/x-executable +MimeType=image/*;video/*; --- res.qrc +++ res.qrc @@ -12,5 +12,7 @@ <file>qml/SortDialog.qml</file> <file>images/down_arrow.png</file> <file>images/up_arrow.png</file> + <file>qml/ZoomController.qml</file> + <file>qml/SingleImagePage.qml</file> </qresource> </RCC> --- rpm +++ rpm +(directory) --- rpm/qmlgallery.spec +++ rpm/qmlgallery.spec @@ -0,0 +1,73 @@ +# +# Do NOT Edit the Auto-generated Part! +# Generated by: spectacle version 0.25 +# + +Name: qmlgallery + +# >> macros +# << macros + +Summary: Photo Gallery for Nemo +Version: 0.1.0 +Release: 1 +Group: Applications/System +License: BSD +URL: https://github.com/nemomobile/qmlgallery +Source0: %{name}-%{version}.tar.bz2 +Source100: qmlgallery.yaml +Requires: qt-components >= 1.4.8 +Requires: libdeclarative-gallery +Requires: libdeclarative-multimedia +Requires: nemo-qml-plugins-thumbnailer +BuildRequires: pkgconfig(QtCore) >= 4.7.0 +BuildRequires: pkgconfig(QtDeclarative) +BuildRequires: pkgconfig(QtGui) +BuildRequires: pkgconfig(qdeclarative-boostable) +BuildRequires: pkgconfig(QtOpenGL) +BuildRequires: pkgconfig(libresourceqt1) +BuildRequires: desktop-file-utils +Provides: meego-handset-video > 0.2.5 +Obsoletes: meego-handset-video <= 0.2.5 + +%description +Photo Gallery application using Qt Quick for Nemo Mobile. + + +%prep +%setup -q -n %{name}-%{version} + +# >> setup +# << setup + +%build +# >> build pre +# << build pre + +%qmake + +make %{?jobs:-j%jobs} + +# >> build post +# << build post + +%install +rm -rf %{buildroot} +# >> install pre +# << install pre +%qmake_install + +# >> install post +# << install post + +desktop-file-install --delete-original \ + --dir %{buildroot}%{_datadir}/applications \ + %{buildroot}%{_datadir}/applications/*.desktop + +%files +%defattr(-,root,root,-) +%{_bindir}/qmlgallery +%{_datadir}/applications/qmlgallery.desktop +%{_libdir}/qt4/imports/org/nemomobile/qmlgallery/* +# >> files +# << files --- rpm/qmlgallery.yaml +++ rpm/qmlgallery.yaml @@ -0,0 +1,35 @@ +Name: qmlgallery +Summary: Photo Gallery for Nemo +Version: 0.1.0 +Release: 1 +Group: Applications/System +License: BSD +URL: https://github.com/nemomobile/qmlgallery +Sources: + - "%{name}-%{version}.tar.bz2" +Description: Photo Gallery application using Qt Quick for Nemo Mobile. +Configure: none +Builder: qmake +Obsoletes: + - meego-handset-video <= 0.2.5 +Provides: + - meego-handset-video > 0.2.5 + +PkgConfigBR: + - QtCore >= 4.7.0 + - QtDeclarative + - QtGui + - qdeclarative-boostable + - QtOpenGL + - libresourceqt1 +Requires: + - qt-components >= 1.4.8 + - libdeclarative-gallery + - libdeclarative-multimedia + - nemo-qml-plugins-thumbnailer + +Files: + - "%{_bindir}/qmlgallery" + - "%{_datadir}/applications/qmlgallery.desktop" + - "%{_libdir}/qt4/imports/org/nemomobile/qmlgallery/*" + --- src/gallery.cpp +++ src/gallery.cpp @@ -36,10 +36,17 @@ #include <QDir> #include <QGLWidget> #include <QDebug> +#include <QFile> +#include <QFileInfo> +#include <QMetaObject> +#include <QGraphicsObject> +#include <QUrl> +#include <QImageReader> Gallery::Gallery(QDeclarativeView *v, QObject *parent) : QObject(parent), view(v) { + QFile fileToOpen; bool isFullscreen = false; bool glwidget = true; foreach (QString parameter, qApp->arguments()) { @@ -50,8 +57,11 @@ qDebug() << "-fullscreen - show QML fullscreen"; qDebug() << "-no-glwidget - Don't use QGLWidget viewport"; exit(0); - } else if (parameter == "-no-glwidget") + } else if (parameter == "-no-glwidget") { glwidget = false; + } else if (parameter != qApp->arguments().first() && !fileToOpen.exists()) { + fileToOpen.setFileName(parameter); + } } // See NEMO#415 for an explanation of why this may be necessary. @@ -84,6 +94,18 @@ view->showFullScreen(); else view->show(); + + if(!fileToOpen.fileName().isNull()) { + if (fileToOpen.exists()) { + QFileInfo fileInfo(fileToOpen); + QUrl fileUrl = QUrl::fromLocalFile(fileInfo.absoluteFilePath()); + if (view->rootObject()) { + QMetaObject::invokeMethod(view->rootObject(), "displayFile", Q_ARG(QVariant, QVariant(fileUrl.toString()))); + } + } else { + qDebug() << "File " << fileToOpen.fileName() << " does not exist."; + } + } } void Gallery::acquireVideoResources() @@ -123,3 +145,21 @@ qDebug() << Q_FUNC_INFO; } +int Gallery::isVideo(QString fileUrl) +{ + //RETURN VALUES + //-1: ERROR, 0: IMAGE, 1: VIDEO + const QString fileName = QUrl(fileUrl).toLocalFile(); + QFileInfo testFile(fileName); + if (testFile.exists()) + { + QImageReader reader(fileName); + QByteArray format = reader.format(); + if (format.isNull() && reader.error() == QImageReader::UnsupportedFormatError) { + //we assume it's a video + return 1; + } + else return 0; + } + return -1; +} --- src/gallery.h +++ src/gallery.h @@ -35,6 +35,7 @@ #include <policy/resource-set.h> class QDeclarativeView; +class QString; class Gallery : public QObject { @@ -46,6 +47,7 @@ public slots: void acquireVideoResources(); void releaseVideoResources(); + int isVideo(QString fileName); private slots: void resourcesGranted(); ++++++ qmlgallery.yaml --- qmlgallery.yaml +++ qmlgallery.yaml @@ -1,25 +1,16 @@ Name: qmlgallery Summary: Photo Gallery for Nemo -Version: 0.0.10 +Version: 0.1.0 Release: 1 Group: Applications/System License: BSD URL: https://github.com/nemomobile/qmlgallery Sources: - - "%{name}-%{version}.tar.bz2" +- "%{name}-%{version}.tar.bz2" +SetupOptions: -q -n %{name} Description: Photo Gallery application using Qt Quick for Nemo Mobile. Configure: none Builder: qmake - -# These are the flags QtSDK sets for Harmattan -# we want to keep both QtSDK harmattan and Nemo build as close as possible -# so we define the same flags -QMakeOptions: - - "MEEGO_VERSION_MAJOR=1" - - "MEEGO_VERSION_MINOR=2" - - "MEEGO_VERSION_PATCH=0" - - "MEEGO_EDITION=harmattan" - - "DEFINES+=MEEGO_EDITION_HARMATTAN" Obsoletes: - meego-handset-video <= 0.2.5 Provides: @@ -33,6 +24,7 @@ - QtOpenGL - libresourceqt1 Requires: + - qt-components >= 1.4.8 - libdeclarative-gallery - libdeclarative-multimedia - nemo-qml-plugins-thumbnailer @@ -41,3 +33,4 @@ - "%{_bindir}/qmlgallery" - "%{_datadir}/applications/qmlgallery.desktop" - "%{_libdir}/qt4/imports/org/nemomobile/qmlgallery/*" +
