diff --git a/src/imagefoldermodel.cpp b/src/imagefoldermodel.cpp index 49477f6..5b99655 100644 --- a/src/imagefoldermodel.cpp +++ b/src/imagefoldermodel.cpp @@ -1,159 +1,174 @@ /* * Copyright 2017 by Marco Martin * Copyright (C) 2017 Atul Sharma * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program 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 Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "imagefoldermodel.h" #include "types.h" #include "roles.h" #include #include #include #include #include #include #include #include #include ImageFolderModel::ImageFolderModel(QObject *parent) : KDirModel(parent) { QMimeDatabase db; QList mimeList = db.allMimeTypes(); m_mimeTypes << "inode/directory"; foreach (const QMimeType &mime, mimeList) { if (mime.name().startsWith(QStringLiteral("image/"))) { m_mimeTypes << mime.name(); } } dirLister()->setMimeFilter(m_mimeTypes); connect(this, &QAbstractItemModel::rowsInserted, this, &ImageFolderModel::countChanged); connect(this, &QAbstractItemModel::rowsRemoved, this, &ImageFolderModel::countChanged); connect(this, &QAbstractItemModel::modelReset, this, &ImageFolderModel::countChanged); } ImageFolderModel::~ImageFolderModel() { } QHash ImageFolderModel::roleNames() const { return { { Qt::DisplayRole, "display" }, { Qt::DecorationRole, "decoration" }, { Roles::ImageUrlRole, "imageurl" }, { Roles::MimeTypeRole, "mimeType" }, { Roles::ItemTypeRole, "itemType"} }; } QString ImageFolderModel::url() const { return dirLister()->url().toString(); } void ImageFolderModel::setUrl(QString& url) { url = QUrl(url).path(); if (url.isEmpty()) { QStringList locations = QStandardPaths::standardLocations(QStandardPaths::PicturesLocation); Q_ASSERT(locations.size() >= 1); url = locations.first().append("/"); } url = QUrl::fromLocalFile(url).toString(); Q_ASSERT( QUrl(url).isLocalFile()); if (dirLister()->url().path() == QUrl(url).path()) { dirLister()->updateDirectory(QUrl(url)); return; } beginResetModel(); dirLister()->openUrl(QUrl(url)); endResetModel(); emit urlChanged(); } int ImageFolderModel::indexForUrl(const QString &url) const { QModelIndex index = KDirModel::indexForUrl(QUrl(url)); return index.row(); } QVariantMap ImageFolderModel::get(int i) const { QModelIndex modelIndex = index(i, 0); KFileItem item = itemForIndex(modelIndex); QString url = item.url().toString(); QString mimeType = item.mimetype(); QVariantMap ret; ret.insert(QStringLiteral("url"), QVariant(url)); ret.insert(QStringLiteral("mimeType"), QVariant(mimeType)); return ret; } void ImageFolderModel::emptyTrash() { KIO::emptyTrash(); } QVariant ImageFolderModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } switch (role) { case Roles::ImageUrlRole: { KFileItem item = itemForIndex(index); return item.url().toString(); } case Roles::MimeTypeRole: { KFileItem item = itemForIndex(index); return item.mimetype(); } case Roles::ItemTypeRole: { KFileItem item = itemForIndex(index); if( item.isDir()) { return Types::Folder; } else { return Types::Image; } } default: return KDirModel::data(index, role); } } +int ImageFolderModel::countFolder() const +{ + int countFolder = 0; + for (int i = 0; i < rowCount(); i++) { + QModelIndex modelIndex = index(i, 0); + + KFileItem item = itemForIndex(modelIndex); + if(item.isDir()) { + countFolder++; + } + } + qDebug() << "count: " << countFolder; + return countFolder; +} + #include "moc_imagefoldermodel.cpp" diff --git a/src/imagefoldermodel.h b/src/imagefoldermodel.h index bd3cef8..101c9ef 100644 --- a/src/imagefoldermodel.h +++ b/src/imagefoldermodel.h @@ -1,80 +1,86 @@ /* * Copyright 2017 by Marco Martin * Copyright (C) 2017 Atul Sharma * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program 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 Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef IMAGEFOLDERMODEL_H #define IMAGEFOLDERMODEL_H #include #include #include class QTimer; /** * This class provides a QML binding to KDirModel * Provides an easy way to navigate a filesystem from within QML * * @author Marco Martin */ class ImageFolderModel : public KDirModel { Q_OBJECT /** - * @property string The url we want to browse. it may be an absolute path or a correct url of any protocol KIO supports + * @property string The url we want to browse. it may be an absolute path or a correct url of any protocol KIO supports. */ Q_PROPERTY(QString url READ url WRITE setUrl NOTIFY urlChanged) /** - * @property count Total number of rows + * @property count Total number of rows. */ Q_PROPERTY(int count READ count NOTIFY countChanged) + /** + * @property countFolder Total number of subdirectories. + */ + Q_PROPERTY(int countFolder READ countFolder NOTIFY countChanged) + public: ImageFolderModel(QObject* parent=0); virtual ~ImageFolderModel(); QHash roleNames() const override; void setUrl(QString& url); QString url() const; QVariant data(const QModelIndex &index, int role) const override; int count() const {return rowCount();} + int countFolder() const; Q_INVOKABLE int indexForUrl(const QString &url) const; Q_INVOKABLE QVariantMap get(int index) const; /** * Helper method to empty the trash */ Q_INVOKABLE void emptyTrash(); Q_SIGNALS: void countChanged(); void urlChanged(); private: QStringList m_mimeTypes; QString m_imagePath; }; #endif // IMAGEFOLDERMODEL_H diff --git a/src/qml/AlbumView.qml b/src/qml/AlbumView.qml index e7a87c4..a5b9520 100644 --- a/src/qml/AlbumView.qml +++ b/src/qml/AlbumView.qml @@ -1,234 +1,234 @@ /* * Copyright (C) 2017 Atul Sharma * * 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.7 import QtQuick.Controls 2.1 as Controls import org.kde.kirigami 2.12 as Kirigami import org.kde.koko 0.1 as Koko Kirigami.ScrollablePage { id: page property alias model: gridView.model signal collectionSelected(QtObject selectedModel, string cover) signal folderSelected(QtObject selectedModel, string cover) keyboardNavigationEnabled: true focus: true states: [ State { name: "browsing" when: !model.hasSelectedImages }, State { name: "selecting" when: model.hasSelectedImages && Kirigami.Settings.tabletMode } ] actions { main: Kirigami.Action { iconName: "edit-select-none" text: i18n("Deselect All") tooltip: i18n("De-selects all the selected images") enabled: model.hasSelectedImages visible: model.hasSelectedImages && Kirigami.Settings.tabletMode onTriggered: model.clearSelections() } contextualActions: [ Kirigami.Action { iconName: "edit-select-all" text: i18n("Select All") tooltip: i18n("Selects all the images in the current view") enabled: model.containImages onTriggered: model.selectAll() }, Kirigami.Action { iconName: "edit-select-none" text: i18n("Deselect All") tooltip: i18n("De-selects all the selected images") enabled: model.hasSelectedImages onTriggered: model.clearSelections() }, Kirigami.Action { iconName: "emblem-shared-symbolic" text: i18n("Share") tooltip: i18n("Share the selected images") enabled: model.hasSelectedImages onTriggered: { shareMenu.open(); shareMenu.inputData = { "urls": model.selectedImages(), "mimeType": "image/" } } }, Kirigami.Action { iconName: "group-delete" text: i18n("Delete Selection") tooltip: i18n("Move selected items to trash") enabled: model.hasSelectedImages onTriggered: model.deleteSelection() } ] } background: Rectangle { Kirigami.Theme.colorSet: Kirigami.Theme.View color: Kirigami.Theme.backgroundColor } Keys.onPressed: { switch (event.key) { case Qt.Key_Escape: gridView.model.clearSelections() break; default: break; } } ShareDialog { id: shareMenu inputData: { "urls": [], "mimeType": ["image/"] } onFinished: { if (error==0 && output.url !== "") { console.assert(output.url !== undefined); var resultUrl = output.url; console.log("Received", resultUrl) notificationManager.showNotification( true, resultUrl); clipboard.content = resultUrl; } else { notificationManager.showNotification( false); } } } GridView { id: gridView //FIXME: right now if those two objects are out of this, the whole page breaks Koko.SortModel { id: sortedListModel } Koko.ImageFolderModel { id: imageFolderModel } keyNavigationEnabled: true property real widthToApproximate: (applicationWindow().wideScreen ? applicationWindow().pageStack.defaultColumnWidth : page.width) - (1||Kirigami.Settings.tabletMode ? Kirigami.Units.gridUnit : 0) cellWidth: Math.floor(width/Math.floor(width/(kokoConfig.iconSize + Kirigami.Units.largeSpacing * 2))) cellHeight: kokoConfig.iconSize + Kirigami.Units.largeSpacing * 2 topMargin: Kirigami.Units.gridUnit highlightMoveDuration: 0 highlight: Item { Rectangle { anchors.centerIn: parent width: Math.min(parent.width, parent.height) height: width color: Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.3) border.color: Kirigami.Theme.highlightColor radius: 2 } } delegate: AlbumDelegate { id: delegate modelData: model onClicked: { if (page.state == "selecting" || (mouse.modifiers & Qt.ControlModifier ) && (model.itemType == Koko.Types.Image)) { gridView.model.toggleSelected(model.index) } else { activated(); } } onPressAndHold: { gridView.model.toggleSelected(model.index) } onActivated: { gridView.model.clearSelections() gridView.currentIndex = model.index; switch( model.itemType) { case Koko.Types.Album: { imageListModel.query = imageListModel.queryForIndex( model.sourceIndex) sortedListModel.sourceModel = imageListModel collectionSelected( sortedListModel, model.display) break; } case Koko.Types.Folder: { imageFolderModel.url = model.imageurl sortedListModel.sourceModel = imageFolderModel folderSelected( sortedListModel, model.display) break; } case Koko.Types.Image: { - currentImage.model = page.model.sourceModel - currentImage.index = model.index + currentImage.model = gridView.model.sourceModel + currentImage.index = model.index - gridView.model.sourceModel.countFolder applicationWindow().pageStack.layers.push(Qt.resolvedUrl("ImageViewer.qml"), { - startIndex: model.index, - imagesModel: page.model + startIndex: currentImage.index, + imagesModel: currentImage.model }) break; } default: { console.log("Unknown") break; } } } SelectionButton { id: selectionButton opacity: ( delegate.containsMouse || page.state == "selecting") && !(model.itemType == Koko.Types.Folder || model.itemType == Koko.Types.Album) anchors.top: delegate.top anchors.left: delegate.left Behavior on opacity { OpacityAnimator { duration: Kirigami.Units.longDuration easing.type: Easing.InOutQuad } } } } Kirigami.PlaceholderMessage { anchors.centerIn: parent text: i18n("No Images Found") visible: gridView.count == 0 width: parent.width - (Kirigami.Units.largeSpacing * 4) } } onCollectionSelected: pageStack.push( Qt.resolvedUrl("AlbumView.qml"), { "model": selectedModel, "title": i18n(cover)}) onFolderSelected: pageStack.push( Qt.resolvedUrl("AlbumView.qml"), { "model": selectedModel, "title": i18n(cover)}) } diff --git a/src/qml/ImageViewer.qml b/src/qml/ImageViewer.qml index a3a2eb2..22bd64c 100644 --- a/src/qml/ImageViewer.qml +++ b/src/qml/ImageViewer.qml @@ -1,480 +1,480 @@ /* * Copyright (C) 2017 Marco Martin * Copyright (C) 2017 Atul Sharma * 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.12 import QtQuick.Window 2.2 import QtQuick.Controls 2.10 as Controls import QtGraphicalEffects 1.0 as Effects import QtQuick.Layouts 1.3 import org.kde.kirigami 2.13 as Kirigami import org.kde.koko 0.1 as Koko import org.kde.kquickcontrolsaddons 2.0 as KQA Kirigami.Page { id: root title: listView.currentItem.display - property int startIndex: 0 + property alias startIndex: listView.currentIndex property var imagesModel leftPadding: 0 rightPadding: 0 topPadding: 0 Kirigami.Theme.inherit: false Kirigami.Theme.textColor: imgColors.foreground Kirigami.Theme.backgroundColor: imgColors.background Kirigami.Theme.highlightColor: imgColors.highlight Kirigami.Theme.highlightedTextColor: Kirigami.ColorUtils.brightnessForColor(imgColors.highlight) === Kirigami.ColorUtils.Dark ? imgColors.closestToWhite : imgColors.closestToBlack Kirigami.ImageColors { id: imgColors source: listView.currentItem } KQA.MimeDatabase { id: mimeDB } Kirigami.ContextDrawer { id: contextDrawer title: i18n("Edit image") handleVisible: true } actions { main: Kirigami.Action { id: shareAction iconName: "document-share" tooltip: i18n("Share Image") text: i18nc("verb, share an image", "Share") onTriggered: { shareDialog.open(); shareDialog.inputData = { "urls": [ listView.currentItem.currentImageSource.toString() ], "mimeType": mimeDB.mimeTypeForUrl( listView.currentItem.currentImageSource).name } } } right: Kirigami.Action { id: editingAction iconName: "edit-entry" text: i18nc("verb, edit an image", "Edit") onTriggered: { applicationWindow().pageStack.layers.push(editorComponent) } } } Component.onCompleted: { applicationWindow().controlsVisible = false; listView.forceActiveFocus(); applicationWindow().header.visible = false; applicationWindow().footer.visible = false; applicationWindow().globalDrawer.visible = false; applicationWindow().globalDrawer.enabled = false; } function close() { applicationWindow().controlsVisible = true; if (applicationWindow().footer) { applicationWindow().footer.visible = true; } applicationWindow().globalDrawer.enabled = true; applicationWindow().visibility = Window.Windowed; applicationWindow().pageStack.layers.pop(); } background: Rectangle { color: "black" } Keys.onPressed: { switch(event.key) { case Qt.Key_Escape: root.close(); break; case Qt.Key_F: applicationWindow().visibility = applicationWindow().visibility == Window.FullScreen ? Window.Windowed : Window.FullScreen break; default: break; } } ShareDialog { id: shareDialog inputData: { "urls": [], "mimeType": ["image/"] } onFinished: { if (error==0 && output.url !== "") { console.assert(output.url !== undefined); var resultUrl = output.url; console.log("Received", resultUrl) notificationManager.showNotification( true, resultUrl); clipboard.content = resultUrl; } else { notificationManager.showNotification( false); } } } ListView { id: thumbnailView z: 100 anchors { left: parent.left right: parent.right bottom: parent.bottom } state: applicationWindow().controlsVisible ? "show" : "hidden" states: [ State { name: "show" PropertyChanges { target: thumbnailView; opacity: 1.0 } PropertyChanges { target: thumbnailView; anchors.bottomMargin: 0 } }, State { name: "hidden" PropertyChanges { target: thumbnailView; opacity: 0.0 } PropertyChanges { target: thumbnailView; anchors.bottomMargin: -thumbnailView.height } } ] transitions: [ Transition { from: "*" to: "hidden" SequentialAnimation { PropertyAnimation { properties: "opacity,anchors.bottomMargin"; easing.type: Easing.InCubic duration: Kirigami.Units.longDuration } PropertyAction { target: thumbnailView property: "visible" value: false } } }, Transition { from: "*" to: "show" SequentialAnimation { PropertyAction { target: thumbnailView property: "visible" value: true } PropertyAnimation { properties: "opacity,anchors.bottomMargin"; easing.type: Easing.OutCubic duration: Kirigami.Units.longDuration * 0.75 } } } ] height: kokoConfig.iconSize orientation: Qt.Horizontal snapMode: ListView.SnapOneItem currentIndex: listView.currentIndex highlightRangeMode: ListView.ApplyRange highlightFollowsCurrentItem: true preferredHighlightBegin: height preferredHighlightEnd: width - height highlightMoveVelocity: -1 highlightMoveDuration: Kirigami.Units.longDuration // Filter out directories model: Koko.SortModel { sourceModel: imagesModel filterRole: Koko.Roles.MimeTypeRole filterRegExp: /image\// } delegate: AlbumDelegate { width: kokoConfig.iconSize + Kirigami.Units.largeSpacing height: width onClicked: activated() onActivated: listView.currentIndex = index modelData: model Rectangle { z: -1 anchors.centerIn: parent width: Math.min(parent.width, parent.height) height: width color: Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.3) border.color: Kirigami.Theme.highlightColor radius: 2 opacity: thumbnailView.currentIndex === index ? 1 : 0 Behavior on opacity { OpacityAnimator { duration: Kirigami.Units.longDuration easing.type: Easing.InOutQuad } } } } } ListView { id: listView anchors.fill: parent orientation: Qt.Horizontal snapMode: ListView.SnapOneItem highlightMoveDuration: 0 interactive: true // Filter out directories model: Koko.SortModel { sourceModel: imagesModel filterRole: Koko.Roles.MimeTypeRole filterRegExp: /image\// } currentIndex: model.proxyIndex(root.startIndex) delegate: Flickable { id: flick readonly property string currentImageSource: model.imageurl readonly property string display: model.display property alias image: image width: root.width height: root.height contentWidth: root.width contentHeight: root.height boundsBehavior: Flickable.StopAtBounds boundsMovement: Flickable.StopAtBounds interactive: contentWidth > width || contentHeight > height //onInteractiveChanged: listView.interactive = !interactive; clip: true z: index == listView.currentIndex ? 1000 : 0 Controls.ScrollBar.vertical: Controls.ScrollBar { visible: !applicationWindow().controlsVisible } Controls.ScrollBar.horizontal: Controls.ScrollBar { visible: !applicationWindow().controlsVisible } 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(root.width*0.7, initialWidth * pinch.scale), Math.max(root.height*0.7, initialHeight * pinch.scale), pinch.center) } onPinchFinished: { // Move its content within bounds. if (flick.contentWidth < root.width || flick.contentHeight < root.height) { zoomAnim.x = 0; zoomAnim.y = 0; zoomAnim.width = root.width; zoomAnim.height = root.height; zoomAnim.running = true; } else { flick.returnToBounds(); } } ParallelAnimation { id: zoomAnim property real x: 0 property real y: 0 property real width: root.width property real height: root.height NumberAnimation { target: flick property: "contentWidth" from: flick.contentWidth to: zoomAnim.width duration: Kirigami.Units.longDuration easing.type: Easing.InOutQuad } NumberAnimation { target: flick property: "contentHeight" from: flick.contentHeight to: zoomAnim.height duration: Kirigami.Units.longDuration easing.type: Easing.InOutQuad } NumberAnimation { target: flick property: "contentY" from: flick.contentY to: zoomAnim.y duration: Kirigami.Units.longDuration easing.type: Easing.InOutQuad } NumberAnimation { target: flick property: "contentX" from: flick.contentX to: zoomAnim.x duration: Kirigami.Units.longDuration easing.type: Easing.InOutQuad } } Image { id: image width: flick.contentWidth height: flick.contentHeight fillMode: Image.PreserveAspectFit source: currentImageSource autoTransform: true asynchronous: true onStatusChanged: { if (status === Image.Ready && listView.currentIndex === index) { imgColors.update(); } } Timer { id: doubleClickTimer interval: 150 onTriggered: applicationWindow().controlsVisible = !applicationWindow().controlsVisible } MouseArea { anchors.fill: parent onClicked: { contextDrawer.drawerOpen = false doubleClickTimer.restart(); } onDoubleClicked: { doubleClickTimer.running = false; applicationWindow().controlsVisible = false; if (flick.interactive) { zoomAnim.x = 0; zoomAnim.y = 0; zoomAnim.width = root.width; zoomAnim.height = root.height; zoomAnim.running = true; } else { zoomAnim.x = mouse.x * 2; zoomAnim.y = mouse.y *2; zoomAnim.width = root.width * 3; zoomAnim.height = root.height * 3; zoomAnim.running = true; } } onWheel: { if (wheel.modifiers & Qt.ControlModifier) { if (wheel.angleDelta.y != 0) { var factor = 1 + wheel.angleDelta.y / 600; zoomAnim.running = false; zoomAnim.width = Math.min(Math.max(root.width, zoomAnim.width * factor), root.width * 4); zoomAnim.height = Math.min(Math.max(root.height, zoomAnim.height * factor), root.height * 4); //actual factors, may be less than factor var xFactor = zoomAnim.width / flick.contentWidth; var yFactor = zoomAnim.height / flick.contentHeight; zoomAnim.x = flick.contentX * xFactor + (((wheel.x - flick.contentX) * xFactor) - (wheel.x - flick.contentX)) zoomAnim.y = flick.contentY * yFactor + (((wheel.y - flick.contentY) * yFactor) - (wheel.y - flick.contentY)) zoomAnim.running = true; } else if (wheel.pixelDelta.y != 0) { flick.resizeContent(Math.min(Math.max(root.width, flick.contentWidth + wheel.pixelDelta.y), root.width * 4), Math.min(Math.max(root.height, flick.contentHeight + wheel.pixelDelta.y), root.height * 4), wheel); } } else { flick.contentX += wheel.pixelDelta.x; flick.contentY += wheel.pixelDelta.y; } } } } } } } Controls.RoundButton { anchors { left: parent.left leftMargin: Kirigami.Units.largeSpacing verticalCenter: parent.verticalCenter } width: Kirigami.Units.gridUnit * 2 height: width icon.name: "arrow-left" visible: !Kirigami.Settings.isMobile && applicationWindow().controlsVisible && listView.currentIndex > 0 Keys.forwardTo: [listView] onClicked: { listView.currentIndex -= 1 } } Controls.RoundButton { anchors { right: parent.right rightMargin: Kirigami.Units.largeSpacing verticalCenter: parent.verticalCenter } width: Kirigami.Units.gridUnit * 2 height: width icon.name: "arrow-right" visible: !Kirigami.Settings.isMobile && applicationWindow().controlsVisible && listView.currentIndex < listView.count - 1 Keys.forwardTo: [listView] onClicked: { listView.currentIndex += 1 } } Component { id: editorComponent EditorView { width: root.width height: root.height imagePath: listView.currentItem.currentImageSource } } }