diff --git a/containments/desktop/package/contents/ui/FolderItemDelegate.qml b/containments/desktop/package/contents/ui/FolderItemDelegate.qml index 5477bf09f..bf95261ba 100644 --- a/containments/desktop/package/contents/ui/FolderItemDelegate.qml +++ b/containments/desktop/package/contents/ui/FolderItemDelegate.qml @@ -1,480 +1,482 @@ /*************************************************************************** * Copyright (C) 2014-2015 by Eike Hein * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, 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 General Public License for more details. * * * * You should have received a copy of the GNU 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 . * ***************************************************************************/ import QtQuick 2.4 import QtGraphicalEffects 1.0 import org.kde.plasma.plasmoid 2.0 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.kquickcontrolsaddons 2.0 Item { id: main property int index: model.index property string name: model.blank ? "" : model.display property bool blank: model.blank property bool isDir: loader.item ? loader.item.isDir : false property QtObject popupDialog: loader.item ? loader.item.popupDialog : null property Item iconArea: loader.item ? loader.item.iconArea : null property Item label: loader.item ? loader.item.label : null property Item labelArea: loader.item ? loader.item.labelArea : null property Item actionsOverlay: loader.item ? loader.item.actionsOverlay : null property Item hoverArea: loader.item ? loader.item.hoverArea : null property Item frame: loader.item ? loader.item.frame : null property Item toolTip: loader.item ? loader.item.toolTip : null function openPopup() { if (isDir) { loader.item.openPopup(); } } function closePopup() { if (popupDialog) { popupDialog.requestDestroy(); loader.item.popupDialog = null; } } Loader { id: loader anchors.fill: parent visible: status === Loader.Ready active: !model.blank sourceComponent: delegateImplementation asynchronous: true } Component { id: delegateImplementation Item { id: impl anchors.fill: parent property bool blank: model.blank property bool selected: model.blank ? false : model.selected property bool isDir: model.blank ? false : model.isDir property bool hovered: (main.GridView.view.hoveredItem == main) property QtObject popupDialog: null property Item iconArea: icon property Item label: label property Item labelArea: frameLoader.textShadow || label property Item actionsOverlay: actions property Item hoverArea: toolTip property Item frame: frameLoader property Item toolTip: toolTip property Item selectionButton: null property Item popupButton: null onSelectedChanged: { if (selected && !blank) { frameLoader.grabToImage(function(result) { dir.addItemDragImage(positioner.map(index), main.x + frameLoader.x, main.y + frameLoader.y, frameLoader.width, frameLoader.height, result.image); }); } } onHoveredChanged: { if (hovered) { if (plasmoid.configuration.selectionMarkers && Qt.styleHints.singleClickActivation) { selectionButton = selectionButtonComponent.createObject(actions); } if (model.isDir) { if (!main.GridView.view.isRootView || root.containsDrag) { hoverActivateTimer.restart(); } if (plasmoid.configuration.popups && !root.useListViewMode) { popupButton = popupButtonComponent.createObject(actions); } } } else if (!hovered) { if (popupDialog != null) { closePopup(); } if (selectionButton) { selectionButton.destroy(); selectionButton = null; } if (popupButton) { popupButton.destroy(); popupButton = null; } } } function openPopup() { if (folderViewDialogComponent.status == Component.Ready) { impl.popupDialog = folderViewDialogComponent.createObject(impl); impl.popupDialog.visualParent = icon; impl.popupDialog.url = model.linkDestinationUrl; impl.popupDialog.visible = true; } } PlasmaCore.ToolTipArea { id: toolTip active: (plasmoid.configuration.toolTips && popupDialog == null && !model.blank) interactive: false location: root.useListViewMode ? (plasmoid.location == PlasmaCore.Types.LeftEdge ? PlasmaCore.Types.LeftEdge : PlasmaCore.Types.RightEdge) : plasmoid.location onContainsMouseChanged: { if (containsMouse && !model.blank) { toolTip.icon = model.decoration; toolTip.mainText = model.display; if (model.size != undefined) { toolTip.subText = model.type + "\n" + model.size; } else { toolTip.subText = model.type; } main.GridView.view.hoveredItem = main; } } states: [ State { // icon view when: !root.useListViewMode AnchorChanges { target: toolTip anchors.horizontalCenter: parent.horizontalCenter } PropertyChanges { target: toolTip y: frameLoader.y + icon.y width: Math.max(icon.paintedWidth, label.paintedWidth) height: (label.y + label.paintedHeight) - y } }, State { // list view when: root.useListViewMode AnchorChanges { target: toolTip anchors.horizontalCenter: undefined } PropertyChanges { target: toolTip x: frameLoader.x y: frameLoader.y width: frameLoader.width height: frameLoader.height } } ] } Loader { id: frameLoader x: root.useListViewMode ? 0 : units.smallSpacing y: root.useListViewMode ? 0 : units.smallSpacing property Item textShadow: null property string prefix: "" sourceComponent: frameComponent active: state !== "" asynchronous: true width: { if (root.useListViewMode) { if (main.GridView.view.overflowing) { return parent.width - units.smallSpacing; } else { return parent.width; } } return parent.width - (units.smallSpacing * 2); } height: { if (root.useListViewMode) { return parent.height; } // Note: frameLoader.y = units.smallSpacing (acts as top margin) return (units.smallSpacing // icon.anchors.topMargin (acts as top padding) + icon.height + units.smallSpacing // label.anchors.topMargin (acts as spacing between icon and label) + (label.lineCount * theme.mSize(theme.defaultFont).height) + units.smallSpacing); // leftover (acts as bottom padding) } PlasmaCore.IconItem { id: icon z: 2 states: [ State { // icon view when: !root.useListViewMode AnchorChanges { target: icon anchors.top: parent.top anchors.horizontalCenter: parent.horizontalCenter } }, State { // list view when: root.useListViewMode AnchorChanges { target: icon anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter } } ] anchors { topMargin: units.smallSpacing leftMargin: units.smallSpacing } width: root.useListViewMode ? main.GridView.view.iconSize : (parent.width - 2 * units.smallSpacing) height: main.GridView.view.iconSize opacity: { if (root.useListViewMode && selectionButton) { return 0.3; } if (model.isHidden) { return 0.6; } return 1.0; } animated: false usesPlasmaTheme: false + smooth: true + source: model.decoration overlays: model.overlays } PlasmaComponents.Label { id: label z: 2 // So we can position a textShadowComponent below if needed. states: [ State { // icon view when: !root.useListViewMode AnchorChanges { target: label anchors.top: icon.bottom anchors.horizontalCenter: parent.horizontalCenter } PropertyChanges { target: label anchors.topMargin: units.smallSpacing width: Math.min(label.implicitWidth + units.smallSpacing, parent.width - units.smallSpacing * 4) maximumLineCount: plasmoid.configuration.textLines horizontalAlignment: Text.AlignHCenter } }, State { // list view when: root.useListViewMode AnchorChanges { target: label anchors.left: icon.right anchors.verticalCenter: parent.verticalCenter } PropertyChanges { target: label anchors.leftMargin: units.smallSpacing * 2 anchors.rightMargin: units.smallSpacing * 2 width: parent.width - icon.width - (units.smallSpacing * 4) maximumLineCount: 1 horizontalAlignment: Text.AlignLeft } } ] height: undefined // Unset PlasmaComponents.Label's default. textFormat: Text.PlainText wrapMode: (maximumLineCount == 1) ? Text.NoWrap : Text.Wrap elide: Text.ElideRight color: (frameLoader.textShadow && frameLoader.textShadow.visible ? "#fff" : PlasmaCore.ColorScope.textColor) opacity: model.isHidden ? 0.6 : 1 text: model.blank ? "" : model.display font.italic: model.isLink visible: editor.targetItem != main } Component { id: frameComponent PlasmaCore.FrameSvgItem { prefix: frameLoader.prefix imagePath: "widgets/viewitem" } } Component { id: selectionButtonComponent FolderItemActionButton { element: model.selected ? "remove" : "add" onClicked: dir.toggleSelected(positioner.map(index)) } } Component { id: popupButtonComponent FolderItemActionButton { visible: main.GridView.view.isRootView && (popupDialog == null) element: "open" onClicked: { dir.setSelected(positioner.map(index)) openPopup(); } } } Component { id: textShadowComponent DropShadow { anchors.fill: label z: 1 horizontalOffset: 2 verticalOffset: 2 radius: 9.0 samples: 18 spread: 0.15 color: "black" opacity: model.isHidden ? 0.6 : 1 source: label visible: editor.targetItem != main } } states: [ State { name: "selected" when: model.selected PropertyChanges { target: frameLoader prefix: "selected" } }, State { name: "hover" when: hovered && !model.selected && plasmoid.configuration.iconHoverEffect PropertyChanges { target: frameLoader prefix: "hover" } }, State { name: "selected+hover" when: hovered && model.selected && plasmoid.configuration.iconHoverEffect PropertyChanges { target: frameLoader prefix: "selected+hover" } } ] } Column { id: actions visible: { if (main.GridView.view.isRootView && root.containsDrag) { return false; } if (!main.GridView.view.isRootView && main.GridView.view.dialog.containsDrag) { return false; } if (popupDialog) { return false; } return true; } anchors { left: frameLoader.left top: frameLoader.top leftMargin: root.useListViewMode ? (icon.x + (icon.width / 2)) - (width / 2) : 0 topMargin: root.useListViewMode ? (icon.y + (icon.height / 2)) - (height / 2) : 0 } width: implicitWidth height: implicitHeight } Component.onCompleted: { if (root.isContainment && main.GridView.view.isRootView) { frameLoader.textShadow = textShadowComponent.createObject(frameLoader); } } } } } diff --git a/containments/desktop/package/contents/ui/FolderView.qml b/containments/desktop/package/contents/ui/FolderView.qml index 788147b60..ef1aaa2fc 100644 --- a/containments/desktop/package/contents/ui/FolderView.qml +++ b/containments/desktop/package/contents/ui/FolderView.qml @@ -1,1341 +1,1341 @@ /*************************************************************************** * Copyright (C) 2014-2015 by Eike Hein * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, 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 General Public License for more details. * * * * You should have received a copy of the GNU 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 . * ***************************************************************************/ import QtQuick 2.4 import QtQuick.Layouts 1.1 import org.kde.plasma.plasmoid 2.0 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.extras 2.0 as PlasmaExtras import org.kde.kquickcontrolsaddons 2.0 import org.kde.private.desktopcontainment.folder 0.1 as Folder import "FolderTools.js" as FolderTools FocusScope { id: main signal pressed property QtObject model: dir property Item rubberBand: null property alias isRootView: gridView.isRootView property alias currentIndex: gridView.currentIndex property alias url: dir.url property alias positions: positioner.positions property alias errorString: dir.errorString property alias dragging: dir.dragging property alias locked: dir.locked property alias sortMode: dir.sortMode property alias filterMode: dir.filterMode property alias filterPattern: dir.filterPattern property alias filterMimeTypes: dir.filterMimeTypes property alias flow: gridView.flow property alias layoutDirection: gridView.layoutDirection property alias cellWidth: gridView.cellWidth property alias cellHeight: gridView.cellHeight property alias overflowing: gridView.overflowing property alias scrollLeft: gridView.scrollLeft property alias scrollRight: gridView.scrollRight property alias scrollUp: gridView.scrollUp property alias scrollDown: gridView.scrollDown property alias hoveredItem: gridView.hoveredItem property var history: [] property Item backButton: null property var dialog: null function rename() { if (gridView.currentIndex != -1) { editor.targetItem = gridView.currentItem; } } function cancelRename() { editor.targetItem = null; } function linkHere(sourceUrl) { dir.linkHere(sourceUrl); } function handleDragMove(x, y) { var child = childAt(x, y); if (child != null && child == backButton) { hoveredItem = null; backButton.handleDragMove(); } else { if (backButton && backButton.containsDrag) { backButton.endDragMove(); } var pos = mapToItem(gridView.contentItem, x, y); var item = gridView.itemAt(pos.x, pos.y); if (item && item.isDir) { hoveredItem = item; } else { hoveredItem = null; } } } function endDragMove() { if (backButton && backButton.active) { backButton.endDragMove(); } else if (hoveredItem && !hoveredItem.popupDialog) { hoveredItem = null; } } function dropItemAt(pos) { var item = gridView.itemAt(pos.x, pos.y); if (item) { if (item.blank) { return -1; } var hOffset = Math.abs(Math.min(gridView.contentX, gridView.originX)); var hPos = mapToItem(item.hoverArea, pos.x + hOffset, pos.y); if ((hPos.x < 0 || hPos.y < 0 || hPos.x > item.hoverArea.width || hPos.y > item.hoverArea.height)) { return -1; } else { return positioner.map(item.index); } } return -1; } function drop(target, event, pos) { var dropPos = mapToItem(gridView.contentItem, pos.x, pos.y); var dropIndex = gridView.indexAt(dropPos.x, dropPos.y); var dragPos = mapToItem(gridView.contentItem, listener.dragX, listener.dragY); var dragIndex = gridView.indexAt(dragPos.x, dragPos.y); if (listener.dragX == -1 || dragIndex != dropIndex) { dir.drop(target, event, dropItemAt(dropPos)); } } Connections { target: dir onPopupMenuAboutToShow: { if (!plasmoid.immutable) { plasmoid.processMimeData(mimeData, x, y, dropJob); } } } Connections { target: plasmoid onExpandedChanged: { if (plasmoid.expanded && dir.status === Folder.FolderModel.Ready && !gridView.model) { gridView.model = positioner; } } } // Lower the toolBox when an item is hovered, so it doesn't interfere with // its interaction (e.g. the selection button in the top left, cf. Bug 337060) Binding { target: toolBox property: "z" // 999 is the default "z" for desktop ToolBoxRoot value: main.hoveredItem ? -100 : 999 when: toolBox } Binding { target: plasmoid property: "busy" value: !gridView.model && dir.status === Folder.FolderModel.Listing } function makeBackButton() { return Qt.createQmlObject("BackButtonItem {}", main); } function doCd(row) { history.push(url); updateHistory(); dir.cd(row); } function doBack() { url = history.pop(); updateHistory(); } // QML doesn't detect change in the array(history) property, so update it explicitly. function updateHistory() { history = history; } Connections { target: root onIsPopupChanged: { if (backButton == null && root.useListViewMode) { backButton = makeBackButton(); } else if (backButton != null) { backButton.destroy(); } } } MouseEventListener { id: listener anchors { topMargin: backButton != null ? backButton.height : undefined fill: parent } property alias hoveredItem: gridView.hoveredItem property Item pressedItem: null property int pressX: -1 property int pressY: -1 property int dragX: -1 property int dragY: -1 property variant cPress: null property bool doubleClickInProgress: false acceptedButtons: { if (hoveredItem == null && main.isRootView) { return root.isPopup ? (Qt.LeftButton | Qt.MiddleButton | Qt.BackButton) : Qt.LeftButton; } return root.isPopup ? (Qt.LeftButton | Qt.MiddleButton | Qt.RightButton | Qt.BackButton) : (Qt.LeftButton | Qt.RightButton); } hoverEnabled: true onPressXChanged: { cPress = mapToItem(gridView.contentItem, pressX, pressY); } onPressYChanged: { cPress = mapToItem(gridView.contentItem, pressX, pressY); } onPressed: { // Ignore press events outside the viewport (i.e. on scrollbars). if (!scrollArea.viewport.contains(Qt.point(mouse.x,mouse.y))) { return; } scrollArea.focus = true; if (mouse.buttons & Qt.BackButton) { if (root.isPopup && dir.resolvedUrl != dir.resolve(plasmoid.configuration.url)) { doBack(); mouse.accepted = true; } return; } if (childAt(mouse.x, mouse.y) != editor) { editor.commit(); } pressX = mouse.x; pressY = mouse.y; if (!hoveredItem || hoveredItem.blank) { if (!gridView.ctrlPressed) { dir.clearSelection(); } if (mouse.buttons & Qt.RightButton) { clearPressState(); dir.openContextMenu(); mouse.accepted = true; } } else { pressedItem = hoveredItem; var pos = mapToItem(hoveredItem.actionsOverlay, mouse.x, mouse.y); if (!(pos.x <= hoveredItem.actionsOverlay.width && pos.y <= hoveredItem.actionsOverlay.height)) { if (gridView.shiftPressed && gridView.currentIndex != -1) { positioner.setRangeSelected(gridView.anchorIndex, hoveredItem.index); } else { // FIXME TODO: Clicking one item with others selected should deselect the others, // which doesn't happen right now because initiating a drag after the press should // still drag all of them: The deselect needs to happen on release instead so we // can distinguish. if (!gridView.ctrlPressed && !dir.isSelected(positioner.map(hoveredItem.index))) { dir.clearSelection(); } if (gridView.ctrlPressed) { dir.toggleSelected(positioner.map(hoveredItem.index)); } else { dir.setSelected(positioner.map(hoveredItem.index)); } } gridView.currentIndex = hoveredItem.index; if (mouse.buttons & Qt.RightButton) { if (pressedItem.toolTip && pressedItem.toolTip.active) { pressedItem.toolTip.hideToolTip(); } clearPressState(); dir.openContextMenu(); mouse.accepted = true; } } } main.pressed(); } onCanceled: pressCanceled() onReleased: pressCanceled() onClicked: { clearPressState(); if (mouse.button === Qt.RightButton || childAt(mouse.x, mouse.y) == editor) { return; } if (!hoveredItem || hoveredItem.blank || gridView.currentIndex == -1 || gridView.ctrlPressed || gridView.shiftPressed) { // Bug 357367: Replay mouse event, so containment actions assigned to left mouse button work. eventGenerator.sendMouseEvent(plasmoid, EventGenerator.MouseButtonPress, mouse.x, mouse.y, mouse.button, mouse.buttons, mouse.modifiers); return; } var pos = mapToItem(hoveredItem, mouse.x, mouse.y); // Moving from an item to its preview popup dialog doesn't unset hoveredItem // even though the cursor has left it, so we need to check whether the click // actually occured inside the item we expect it in before going ahead. If it // didn't, clean up (e.g. dismissing the dialog as a side-effect of unsetting // hoveredItem) and abort. if (pos.x < 0 || pos.x > hoveredItem.width || pos.y < 0 || pos.y > hoveredItem.height) { hoveredItem = null; dir.clearSelection(); return; // If the hoveredItem is clicked while having a preview popup dialog open, // only dismiss the dialog and abort. } else if (hoveredItem.popupDialog) { hoveredItem.closePopup(); return; } pos = mapToItem(hoveredItem.actionsOverlay, mouse.x, mouse.y); if (!(pos.x <= hoveredItem.actionsOverlay.width && pos.y <= hoveredItem.actionsOverlay.height)) { if (Qt.styleHints.singleClickActivation || doubleClickInProgress) { var func = root.useListViewMode && (mouse.button == Qt.LeftButton) && hoveredItem.isDir ? doCd : dir.run; func(positioner.map(gridView.currentIndex)); hoveredItem = null; } else { doubleClickInProgress = true; doubleClickTimer.interval = Qt.styleHints.mouseDoubleClickInterval; doubleClickTimer.start(); } } } onPositionChanged: { gridView.ctrlPressed = (mouse.modifiers & Qt.ControlModifier); gridView.shiftPressed = (mouse.modifiers & Qt.ShiftModifier); var cPos = mapToItem(gridView.contentItem, mouse.x, mouse.y); var item = gridView.itemAt(cPos.x, cPos.y); var leftEdge = Math.min(gridView.contentX, gridView.originX); if (!item || item.blank) { if (gridView.hoveredItem && !root.containsDrag && (!dialog || !dialog.containsDrag) && !gridView.hoveredItem.popupDialog) { gridView.hoveredItem = null; } } else { var fPos = mapToItem(item.frame, mouse.x, mouse.y); if (fPos.x < 0 || fPos.y < 0 || fPos.x > item.frame.width || fPos.y > item.frame.height) { gridView.hoveredItem = null; } } // Trigger autoscroll. if (pressX != -1) { gridView.scrollLeft = (mouse.x <= 0 && gridView.contentX > leftEdge); gridView.scrollRight = (mouse.x >= gridView.width && gridView.contentX < gridView.contentItem.width - gridView.width); gridView.scrollUp = (mouse.y <= 0 && gridView.contentY > 0); gridView.scrollDown = (mouse.y >= gridView.height && gridView.contentY < gridView.contentItem.height - gridView.height); } // Update rubberband geometry. if (main.rubberBand) { var rB = main.rubberBand; if (cPos.x < cPress.x) { rB.x = Math.max(leftEdge, cPos.x); rB.width = Math.abs(rB.x - cPress.x); } else { rB.x = cPress.x; var ceil = Math.max(gridView.width, gridView.contentItem.width) + leftEdge; rB.width = Math.min(ceil - rB.x, Math.abs(rB.x - cPos.x)); } if (cPos.y < cPress.y) { rB.y = Math.max(0, cPos.y); rB.height = Math.abs(rB.y - cPress.y); } else { rB.y = cPress.y; var ceil = Math.max(gridView.height, gridView.contentItem.height); rB.height = Math.min(ceil - rB.y, Math.abs(rB.y - cPos.y)); } // Ensure rubberband is at least 1px in size or else it will become // invisible and not match any items. rB.width = Math.max(1, rB.width); rB.height = Math.max(1, rB.height); gridView.rectangleSelect(rB.x, rB.y, rB.width, rB.height); return; } // Drag initiation. if (pressX != -1 && root.isDrag(pressX, pressY, mouse.x, mouse.y)) { if (pressedItem != null && dir.isSelected(positioner.map(pressedItem.index))) { pressedItem.toolTip.hideToolTip(); dragX = mouse.x; dragY = mouse.y; gridView.verticalDropHitscanOffset = pressedItem.iconArea.y + (pressedItem.iconArea.height / 2) dir.dragSelected(mouse.x, mouse.y); dragX = -1; dragY = -1; clearPressState(); } else { // Disable rubberband in popup list view mode. if (root.useListViewMode) { return; } dir.pinSelection(); main.rubberBand = Qt.createQmlObject("import QtQuick 2.0; import org.kde.private.desktopcontainment.folder 0.1 as Folder;" + "Folder.RubberBand { x: " + cPress.x + "; y: " + cPress.y + "; width: 0; height: 0; z: 99999; }", gridView.contentItem); gridView.interactive = false; } } } onContainsMouseChanged: { if (!containsMouse && !main.rubberBand) { clearPressState(); if (gridView.hoveredItem && !gridView.hoveredItem.popupDialog) { gridView.hoveredItem = null; } } } onHoveredItemChanged: { doubleClickInProgress = false; if (!hoveredItem) { hoverActivateTimer.stop(); } } function pressCanceled() { if (main.rubberBand) { main.rubberBand.visible = false; main.rubberBand.enabled = false; main.rubberBand.destroy(); main.rubberBand = null; gridView.interactive = true; gridView.cachedRectangleSelection = null; dir.unpinSelection(); } clearPressState(); gridView.cancelAutoscroll(); } function clearPressState() { pressedItem = null; pressX = -1; pressY = -1; } Timer { id: doubleClickTimer onTriggered: { listener.doubleClickInProgress = false; } } Timer { id: hoverActivateTimer interval: root.hoverActivateDelay onTriggered: { if (!hoveredItem) { return; } if (root.useListViewMode) { doCd(index); } else { hoveredItem.openPopup(); } } } PlasmaExtras.ScrollArea { id: scrollArea anchors.fill: parent focus: true property bool ready: false readonly property int viewportWidth: scrollArea.ready && viewport ? Math.ceil(viewport.width) : 0 readonly property int viewportHeight: scrollArea.ready && viewport ? Math.ceil(viewport.height) : 0 Component.onCompleted: { scrollArea.ready = true; } GridView { id: gridView property bool isRootView: false property int iconSize: makeIconSize() property int verticalDropHitscanOffset: 0 property Item hoveredItem: null property int anchorIndex: 0 property bool ctrlPressed: false property bool shiftPressed: false property bool overflowing: (visibleArea.heightRatio < 1.0 || visibleArea.widthRatio < 1.0) property bool scrollLeft: false property bool scrollRight: false property bool scrollUp: false property bool scrollDown: false property variant cachedRectangleSelection: null currentIndex: -1 keyNavigationWraps: false boundsBehavior: Flickable.StopAtBounds function calcExtraSpacing(cellSize, containerSize) { var availableColumns = Math.floor(containerSize / cellSize); var extraSpacing = 0; if (availableColumns > 0) { var allColumnSize = availableColumns * cellSize; var extraSpace = Math.max(containerSize - allColumnSize, 0); extraSpacing = extraSpace / availableColumns; } return extraSpacing; } cellWidth: { if (root.useListViewMode) { return gridView.width; } else { var iconWidth = iconSize + (2 * units.largeSpacing) + (2 * units.smallSpacing); if (root.isContainment && isRootView && scrollArea.viewportWidth > 0) { var extraWidth = calcExtraSpacing(iconWidth, scrollArea.viewportWidth); return iconWidth + extraWidth; } else { return iconWidth; } } } cellHeight: { if (root.useListViewMode) { return Math.ceil((Math.max(theme.mSize(theme.defaultFont).height, iconSize) + Math.max(highlightItemSvg.margins.top + highlightItemSvg.margins.bottom, listItemSvg.margins.top + listItemSvg.margins.bottom)) / 2) * 2; } else { var iconHeight = iconSize + (theme.mSize(theme.defaultFont).height * plasmoid.configuration.textLines) + (4 * units.smallSpacing); if (root.isContainment && isRootView && scrollArea.viewportHeight > 0) { var extraHeight = calcExtraSpacing(iconHeight, scrollArea.viewportHeight); return iconHeight + extraHeight; } else { return iconHeight; } } } delegate: FolderItemDelegate { width: gridView.cellWidth height: gridView.cellHeight } onContentXChanged: { if (hoveredItem) { hoverActivateTimer.stop(); } editor.targetItem = null; dir.setDragHotSpotScrollOffset(contentX, contentY); if (contentX == 0) { scrollLeft = false; } if (contentX == contentItem.width - width) { scrollRight = false; } // Update rubberband geomety. if (main.rubberBand) { var rB = main.rubberBand; if (scrollLeft) { rB.x = Math.min(gridView.contentX, gridView.originX); rB.width = listener.cPress.x; } if (scrollRight) { var lastCol = gridView.contentX + gridView.width; rB.width = lastCol - rB.x; } gridView.rectangleSelect(rB.x, rB.y, rB.width, rB.height); } } onContentYChanged: { if (hoveredItem) { hoverActivateTimer.stop(); } editor.targetItem = null; dir.setDragHotSpotScrollOffset(contentX, contentY); if (contentY == 0) { scrollUp = false; } if (contentY == contentItem.height - height) { scrollDown = false; } // Update rubberband geometry. if (main.rubberBand) { var rB = main.rubberBand; if (scrollUp) { rB.y = 0; rB.height = listener.cPress.y; } if (scrollDown) { var lastRow = gridView.contentY + gridView.height; rB.height = lastRow - rB.y; } gridView.rectangleSelect(rB.x, rB.y, rB.width, rB.height); } } onScrollLeftChanged: { if (scrollLeft && gridView.visibleArea.widthRatio < 1.0) { smoothX.enabled = true; contentX = (gridView.flow == GridView.FlowLeftToRight) ? gridView.contentX : gridView.originX; } else { contentX = contentX; smoothX.enabled = false; } } onScrollRightChanged: { if (scrollRight && gridView.visibleArea.widthRatio < 1.0) { smoothX.enabled = true; contentX = ((gridView.flow == GridView.FlowLeftToRight) ? gridView.contentX : gridView.originX) + (contentItem.width - width); } else { contentX = contentX; smoothX.enabled = false; } } onScrollUpChanged: { if (scrollUp && gridView.visibleArea.heightRatio < 1.0) { smoothY.enabled = true; contentY = 0; } else { contentY = contentY; smoothY.enabled = false; } } onScrollDownChanged: { if (scrollDown && gridView.visibleArea.heightRatio < 1.0) { smoothY.enabled = true; contentY = contentItem.height - height; } else { contentY = contentY; smoothY.enabled = false; } } onFlowChanged: { // FIXME TODO: Preserve positions. if (positioner.enabled) { positioner.reset(); } } onLayoutDirectionChanged: { // FIXME TODO: Preserve positions. if (positioner.enabled) { positioner.reset(); } } onCurrentIndexChanged: { positionViewAtIndex(currentIndex, GridView.Contain); } onCachedRectangleSelectionChanged: { if (cachedRectangleSelection == null) { return; } if (cachedRectangleSelection.length) { // Set current index to start of selection. // cachedRectangleSelection is pre-sorted. currentIndex = cachedRectangleSelection[0]; } dir.updateSelection(cachedRectangleSelection.map(positioner.map), gridView.ctrlPressed); } function makeIconSize() { if (root.useListViewMode) { return units.iconSizes.small; } return FolderTools.iconSizeFromTheme(plasmoid.configuration.iconSize); } function updateSelection(modifier) { if (modifier & Qt.ShiftModifier) { positioner.setRangeSelected(anchorIndex, currentIndex); } else { dir.clearSelection(); dir.setSelected(positioner.map(currentIndex)); } } function cancelAutoscroll() { scrollLeft = false; scrollRight = false; scrollUp = false; scrollDown = false; } function rectangleSelect(x, y, width, height) { var rows = (gridView.flow == GridView.FlowLeftToRight); var axis = rows ? gridView.width : gridView.height; var step = rows ? cellWidth : cellHeight; var perStripe = Math.floor(axis / step); var stripes = Math.ceil(gridView.count / perStripe); var cWidth = gridView.cellWidth - (2 * units.smallSpacing); var cHeight = gridView.cellHeight - (2 * units.smallSpacing); var midWidth = gridView.cellWidth / 2; var midHeight = gridView.cellHeight / 2; var indices = []; for (var s = 0; s < stripes; s++) { for (var i = 0; i < perStripe; i++) { var index = (s * perStripe) + i; if (index >= gridView.count) { break; } if (positioner.isBlank(index)) { continue; } var itemX = ((rows ? i : s) * gridView.cellWidth); var itemY = ((rows ? s : i) * gridView.cellHeight); if (gridView.effectiveLayoutDirection == Qt.RightToLeft) { itemX -= (rows ? gridView.contentX : gridView.originX); itemX += cWidth; itemX = (rows ? gridView.width : gridView.contentItem.width) - itemX; } // Check if the rubberband intersects this cell first to avoid doing more // expensive work. if (main.rubberBand.intersects(Qt.rect(itemX + units.smallSpacing, itemY + units.smallSpacing, cWidth, cHeight))) { var item = gridView.contentItem.childAt(itemX + midWidth, itemY + midHeight); // If this is a visible item, check for intersection with the actual // icon or label rects for better feel. if (item && item.iconArea) { var iconRect = Qt.rect(itemX + item.iconArea.x, itemY + item.iconArea.y, item.iconArea.width, item.iconArea.height); if (main.rubberBand.intersects(iconRect)) { indices.push(index); continue; } var labelRect = Qt.rect(itemX + item.labelArea.x, itemY + item.labelArea.y, item.labelArea.width, item.labelArea.height); if (main.rubberBand.intersects(labelRect)) { indices.push(index); continue; } } else { // Otherwise be content with the cell intersection. indices.push(index); } } } } gridView.cachedRectangleSelection = indices; } function runOrCdSelected() { if (currentIndex != -1 && dir.hasSelection()) { if (root.useListViewMode && currentItem.isDir) { doCd(positioner.map(currentIndex)); } else { dir.runSelected(); } } } Behavior on contentX { id: smoothX; enabled: false; SmoothedAnimation { velocity: 700 } } Behavior on contentY { id: smoothY; enabled: false; SmoothedAnimation { velocity: 700 } } Keys.onReturnPressed: runOrCdSelected() Keys.onEnterPressed: runOrCdSelected() Keys.onMenuPressed: { if (currentIndex != -1 && dir.hasSelection() && currentItem) { dir.setSelected(positioner.map(currentIndex)); dir.openContextMenu(currentItem.frame); } else { // Otherwise let the containment handle it. event.accepted = false; } } Keys.onEscapePressed: { if (!editor.targetItem) { dir.clearSelection(); event.accepted = false; } } Folder.ShortCut { Component.onCompleted: { installAsEventFilterFor(gridView); } onDeleteFile: { dir.deleteSelected(); } onRenameFile: { rename(); } } Keys.onPressed: { if (event.matches(StandardKey.Delete)) { if (dir.hasSelection()) { dir.action("trash").trigger(); } } else if (event.key == Qt.Key_Control) { ctrlPressed = true; } else if (event.key == Qt.Key_Shift) { shiftPressed = true; if (currentIndex != -1) { anchorIndex = currentIndex; } } else if (event.key == Qt.Key_Home) { currentIndex = 0; updateSelection(event.modifiers); } else if (event.key == Qt.Key_End) { currentIndex = count - 1; updateSelection(event.modifiers); } else if (event.matches(StandardKey.Copy)) { dir.copy(); } else if (event.matches(StandardKey.Paste)) { dir.paste(); } else if (event.matches(StandardKey.Cut)) { dir.cut(); } else if (event.matches(StandardKey.Undo)) { dir.undo(); } else if (event.matches(StandardKey.Refresh)) { dir.refresh(); } else if (event.matches(StandardKey.SelectAll)) { positioner.setRangeSelected(0, count - 1); } } Keys.onReleased: { if (event.key == Qt.Key_Control) { ctrlPressed = false; } else if (event.key == Qt.Key_Shift) { shiftPressed = false; anchorIndex = 0; } } Keys.onLeftPressed: { if (root.isPopup && dir.resolvedUrl != dir.resolve(plasmoid.configuration.url)) { doBack(); } else if (positioner.enabled) { var newIndex = positioner.nearestItem(currentIndex, FolderTools.effectiveNavDirection(gridView.flow, gridView.effectiveLayoutDirection, Qt.LeftArrow)); if (newIndex != -1) { currentIndex = newIndex; updateSelection(event.modifiers); } } else { var oldIndex = currentIndex; moveCurrentIndexLeft(); if (oldIndex == currentIndex) { return; } updateSelection(event.modifiers); } } Keys.onRightPressed: { if (root.isPopup && currentIndex != -1 && dir.hasSelection()) { var func = root.isPopup ? doCd : dir.run; func(positioner.map(currentIndex)); } else if (positioner.enabled) { var newIndex = positioner.nearestItem(currentIndex, FolderTools.effectiveNavDirection(gridView.flow, gridView.effectiveLayoutDirection, Qt.RightArrow)); if (newIndex != -1) { currentIndex = newIndex; updateSelection(event.modifiers); } } else { var oldIndex = currentIndex; moveCurrentIndexRight(); if (oldIndex == currentIndex) { return; } updateSelection(event.modifiers); } } Keys.onUpPressed: { if (positioner.enabled) { var newIndex = positioner.nearestItem(currentIndex, FolderTools.effectiveNavDirection(gridView.flow, gridView.effectiveLayoutDirection, Qt.UpArrow)); if (newIndex != -1) { currentIndex = newIndex; updateSelection(event.modifiers); } } else { var oldIndex = currentIndex; moveCurrentIndexUp(); if (oldIndex == currentIndex) { return; } updateSelection(event.modifiers); } } Keys.onDownPressed: { if (positioner.enabled) { var newIndex = positioner.nearestItem(currentIndex, FolderTools.effectiveNavDirection(gridView.flow, gridView.effectiveLayoutDirection, Qt.DownArrow)); if (newIndex != -1) { currentIndex = newIndex; updateSelection(event.modifiers); } } else { var oldIndex = currentIndex; moveCurrentIndexDown(); if (oldIndex == currentIndex) { return; } updateSelection(event.modifiers); } } Keys.onBackPressed: { if (root.isPopup && dir.resolvedUrl != dir.resolve(plasmoid.configuration.url)) { doBack(); } } Connections { target: units onIconSizesChanged: { gridView.iconSize = gridView.makeIconSize(); } } Connections { target: plasmoid.configuration onIconSizeChanged: { gridView.iconSize = gridView.makeIconSize(); } } Connections { target: plasmoid.configuration onUrlChanged: { history = []; updateHistory(); } } } } Folder.WheelInterceptor { anchors.fill: parent enabled: root.isContainment && !gridView.overflowing destination: plasmoid } Folder.FolderModel { id: dir usedByContainment: root.isContainment && main.isRootView sortDesc: plasmoid.configuration.sortDesc sortDirsFirst: plasmoid.configuration.sortDirsFirst parseDesktopFiles: (plasmoid.configuration.url == "desktop:/") previews: plasmoid.configuration.previews previewPlugins: plasmoid.configuration.previewPlugins appletInterface: plasmoid onListingCompleted: { if (!gridView.model && plasmoid.expanded) { gridView.model = positioner; } } onMove: { var rows = (gridView.flow == GridView.FlowLeftToRight); var axis = rows ? gridView.width : gridView.height; var step = rows ? cellWidth : cellHeight; var perStripe = Math.floor(axis / step); var dropPos = mapToItem(gridView.contentItem, x, y); var leftEdge = Math.min(gridView.contentX, gridView.originX); var moves = [] var itemX = -1; var itemY = -1; var col = -1; var row = -1; var from = -1; var to = -1; for (var i = 0; i < urls.length; i++) { from = positioner.indexForUrl(urls[i]); to = -1; if (from == -1) { continue; } var offset = dir.dragCursorOffset(positioner.map(from)); if (offset.x == -1) { continue; } itemX = dropPos.x + offset.x + (listener.dragX % cellWidth) + (cellWidth / 2); itemY = dropPos.y + offset.y + (listener.dragY % cellHeight) + gridView.verticalDropHitscanOffset; if (gridView.effectiveLayoutDirection == Qt.RightToLeft) { itemX -= (rows ? gridView.contentX : gridView.originX); itemX = (rows ? gridView.width : gridView.contentItem.width) - itemX; } col = Math.floor(itemX / gridView.cellWidth); row = Math.floor(itemY / gridView.cellHeight); if ((rows ? col : row) < perStripe) { to = ((rows ? row : col) * perStripe) + (rows ? col : row); if (to < 0) { return; } } if (from != to) { moves.push(from); moves.push(to); } } if (moves.length) { positioner.move(moves); gridView.forceLayout(); } dir.clearSelection(); } } Folder.Positioner { id: positioner enabled: (isContainment && dir.sortMode == -1) folderModel: dir perStripe: Math.floor(((gridView.flow == GridView.FlowLeftToRight) ? gridView.width : gridView.height) / ((gridView.flow == GridView.FlowLeftToRight) ? gridView.cellWidth : gridView.cellHeight)); } Folder.ItemViewAdapter { id: viewAdapter adapterView: gridView adapterModel: positioner - adapterIconSize: gridView.iconSize; + adapterIconSize: gridView.iconSize * 2 adapterVisibleArea: Qt.rect(gridView.contentX, gridView.contentY, gridView.contentWidth, gridView.contentHeight) Component.onCompleted: { gridView.movementStarted.connect(viewAdapter.viewScrolled); dir.viewAdapter = viewAdapter; } } PlasmaComponents.TextArea { id: editor visible: false wrapMode: root.useListViewMode ? TextEdit.NoWrap : TextEdit.Wrap textMargin: 0 horizontalAlignment: root.useListViewMode ? TextEdit.AlignHLeft : TextEdit.AlignHCenter property Item targetItem: null onTargetItemChanged: { if (targetItem != null) { var xy = getXY(); x = xy[0]; y = xy[1]; width = getWidth(); height = getInitHeight(); text = targetItem.label.text; adjustSize(); editor.select(0, dir.fileExtensionBoundary(positioner.map(targetItem.index))); if(isPopup) { flickableItem.contentX = Math.max(flickableItem.contentWidth - contentItem.width, 0); } else { flickableItem.contentY = Math.max(flickableItem.contentHeight - contentItem.height, 0); } visible = true; } else { x: 0 y: 0 visible = false; } } onVisibleChanged: { if (visible) { focus = true; } else { scrollArea.focus = true; } } Keys.onPressed: { switch(event.key) { case Qt.Key_Return: case Qt.Key_Enter: commit(); break; case Qt.Key_Escape: if (targetItem) { targetItem = null; event.accepted = true; } break; case Qt.Key_Home: editor.select(0, 0); break; case Qt.Key_End: editor.select(text.length, text.length); break; default: adjustSize(); break; } } Keys.onReleased: { adjustSize(); } function getXY() { var pos = main.mapFromItem(targetItem, targetItem.labelArea.x, targetItem.labelArea.y); var _x, _y; if (root.useListViewMode) { _x = targetItem.labelArea.x - __style.padding.left; _y = pos.y - __style.padding.top; } else { _x = targetItem.x + Math.abs(Math.min(gridView.contentX, gridView.originX)); _x += (targetItem.width/2 - targetItem.label.paintedWidth/2 - __style.padding.left); _x += scrollArea.viewport.x; if (verticalScrollBarPolicy == Qt.ScrollBarAlwaysOn && gridView.effectiveLayoutDirection == Qt.RightToLeft) { _x -= __verticalScrollBar.parent.verticalScrollbarOffset; } _y = pos.y + units.smallSpacing - __style.padding.top; } return([ _x, _y ]); } function getWidth(addWidthVerticalScroller) { var _width = root.useListViewMode ? targetItem.width - units.largeSpacing * 2 : targetItem.label.paintedWidth; return _width + __style.padding.left + __style.padding.right + (addWidthVerticalScroller ? __verticalScrollBar.parent.verticalScrollbarOffset : 0); } function getHeight(addWidthHoriozontalScroller, init) { var _height; if(isPopup || init) { _height = targetItem.labelArea.height + __style.padding.top + __style.padding.bottom; } else { var realHeight = contentHeight + __style.padding.top + __style.padding.bottom; var maxHeight = theme.mSize(theme.defaultFont).height * (plasmoid.configuration.textLines + 1) + __style.padding.top + __style.padding.bottom; _height = Math.min(realHeight, maxHeight); } return(_height + (addWidthHoriozontalScroller ? __horizontalScrollBar.parent.horizontalScrollbarOffset : 0)); } function getInitHeight() { return(getHeight(false, true)); } function adjustSize() { if(isPopup) { if(contentWidth + __style.padding.left + __style.padding.right > width) { visible = true; horizontalScrollBarPolicy = Qt.ScrollBarAlwaysOn; height = getHeight(true); } else { horizontalScrollBarPolicy = Qt.ScrollBarAlwaysOff; height = getHeight(); } } else { height = getHeight(); if(contentHeight + __style.padding.top + __style.padding.bottom > height) { visible = true; verticalScrollBarPolicy = Qt.ScrollBarAlwaysOn; width = getWidth(true); } else { verticalScrollBarPolicy = Qt.ScrollBarAlwaysOff; width = getWidth(); } } var xy = getXY(); x = xy[0]; y = xy[1]; } function commit() { if (targetItem) { dir.rename(positioner.map(targetItem.index), text); targetItem = null; } } } Component.onCompleted: { dir.requestRename.connect(rename); } } Component.onCompleted: { if (backButton == null && root.useListViewMode) { backButton = makeBackButton(); } } } diff --git a/containments/desktop/plugins/folder/foldermodel.cpp b/containments/desktop/plugins/folder/foldermodel.cpp index 84c0538f7..94c074760 100644 --- a/containments/desktop/plugins/folder/foldermodel.cpp +++ b/containments/desktop/plugins/folder/foldermodel.cpp @@ -1,1965 +1,1968 @@ /*************************************************************************** * Copyright (C) 2006 David Faure * * Copyright (C) 2008 Fredrik Höglund * * Copyright (C) 2008 Rafael Fernández López * * Copyright (C) 2011 Marco Martin * * Copyright (C) 2014 by Eike Hein * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, 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 General Public License for more details. * * * * You should have received a copy of the GNU 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 "foldermodel.h" #include "itemviewadapter.h" #include "positioner.h" #include "screenmapper.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include Q_LOGGING_CATEGORY(FOLDERMODEL, "plasma.containments.desktop.folder.foldermodel") DirLister::DirLister(QObject *parent) : KDirLister(parent) { } DirLister:: ~DirLister() { } void DirLister::handleError(KIO::Job *job) { if (!autoErrorHandlingEnabled()) { emit error(job->errorString()); return; } KDirLister::handleError(job); } FolderModel::FolderModel(QObject *parent) : QSortFilterProxyModel(parent), m_dirWatch(nullptr), m_dragInProgress(false), m_urlChangedWhileDragging(false), m_dropTargetPositionsCleanup(new QTimer(this)), m_previewGenerator(nullptr), m_viewAdapter(nullptr), m_actionCollection(this), m_newMenu(nullptr), m_fileItemActions(nullptr), m_usedByContainment(false), m_locked(true), m_sortMode(0), m_sortDesc(false), m_sortDirsFirst(true), m_parseDesktopFiles(false), m_previews(false), m_filterMode(NoFilter), m_filterPatternMatchAll(true), m_complete(false), m_screenMapper(ScreenMapper::instance()) { //needed to pass the job around with qml qmlRegisterType(); DirLister *dirLister = new DirLister(this); dirLister->setDelayedMimeTypes(true); dirLister->setAutoErrorHandlingEnabled(false, nullptr); connect(dirLister, &DirLister::error, this, &FolderModel::dirListFailed); connect(dirLister, &KCoreDirLister::itemsDeleted, this, &FolderModel::evictFromIsDirCache); connect(dirLister, &KCoreDirLister::started, this, std::bind(&FolderModel::setStatus, this, Status::Listing)); void (KCoreDirLister::*myCompletedSignal)() = &KCoreDirLister::completed; QObject::connect(dirLister, myCompletedSignal, this, [this] { setStatus(Status::Ready); emit listingCompleted(); }); void (KCoreDirLister::*myCanceledSignal)() = &KCoreDirLister::canceled; QObject::connect(dirLister, myCanceledSignal, this, [this] { setStatus(Status::Canceled); emit listingCanceled(); }); m_dirModel = new KDirModel(this); m_dirModel->setDirLister(dirLister); m_dirModel->setDropsAllowed(KDirModel::DropOnDirectory | KDirModel::DropOnLocalExecutable); /* * position dropped items at the desired target position * delay this via queued connection, such that the row is available and can be mapped * when we emit the move request */ connect(this, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &parent, int first, int last) { for (int i = first; i <= last; ++i) { const auto idx = index(i, 0, parent); const auto url = itemForIndex(idx).url(); auto it = m_dropTargetPositions.find(url.fileName()); if (it != m_dropTargetPositions.end()) { const auto pos = it.value(); m_dropTargetPositions.erase(it); setSortMode(-1); emit move(pos.x(), pos.y(), {url}); } } }); /* * Dropped files may not actually show up as new files, e.g. when we overwrite * an existing file. Or files that fail to be listed by the dirLister, or... * To ensure we don't grow the map indefinitely, clean it up periodically. * The cleanup timer is (re)started whenever we modify the map. We use a quite * high interval of 10s. This should ensure, that we don't accidentally wipe * the mapping when we actually still want to use it. Since the time between * adding an entry in the map and it showing up in the model should be * small, this should rarely, if ever happen. */ m_dropTargetPositionsCleanup->setInterval(10000); m_dropTargetPositionsCleanup->setSingleShot(true); connect(m_dropTargetPositionsCleanup, &QTimer::timeout, this, [this]() { if (!m_dropTargetPositions.isEmpty()) { qCDebug(FOLDERMODEL) << "clearing drop target positions after timeout:" << m_dropTargetPositions; m_dropTargetPositions.clear(); } }); m_selectionModel = new QItemSelectionModel(this, this); connect(m_selectionModel, &QItemSelectionModel::selectionChanged, this, &FolderModel::selectionChanged); setSourceModel(m_dirModel); setSortLocaleAware(true); setFilterCaseSensitivity(Qt::CaseInsensitive); setDynamicSortFilter(true); sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder); createActions(); } FolderModel::~FolderModel() { if (m_usedByContainment) { // disconnect so we don't handle signals from the screen mapper when // removeScreen is called m_screenMapper->disconnect(this); m_screenMapper->removeScreen(m_screen, url()); } } QHash< int, QByteArray > FolderModel::roleNames() const { return staticRoleNames(); } QHash< int, QByteArray > FolderModel::staticRoleNames() { QHash roleNames; roleNames[Qt::DisplayRole] = "display"; roleNames[Qt::DecorationRole] = "decoration"; roleNames[BlankRole] = "blank"; roleNames[OverlaysRole] = "overlays"; roleNames[SelectedRole] = "selected"; roleNames[IsDirRole] = "isDir"; roleNames[IsLinkRole] = "isLink"; roleNames[IsHiddenRole] = "isHidden"; roleNames[UrlRole] = "url"; roleNames[LinkDestinationUrl] = "linkDestinationUrl"; roleNames[SizeRole] = "size"; roleNames[TypeRole] = "type"; return roleNames; } void FolderModel::classBegin() { } void FolderModel::componentComplete() { m_complete = true; invalidate(); } void FolderModel::invalidateIfComplete() { if (!m_complete) { return; } invalidate(); } void FolderModel::invalidateFilterIfComplete() { if (!m_complete) { return; } invalidateFilter(); } void FolderModel::newFileMenuItemCreated(const QUrl &url) { if (m_usedByContainment) { m_screenMapper->addMapping(url.toString(), m_screen, ScreenMapper::DelayedSignal); m_dropTargetPositions.insert(url.fileName(), m_menuPosition); m_menuPosition = {}; m_dropTargetPositionsCleanup->start(); } } QString FolderModel::url() const { return m_url; } void FolderModel::setUrl(const QString& url) { const QUrl &resolvedUrl = resolve(url); if (url == m_url) { m_dirModel->dirLister()->updateDirectory(resolvedUrl); return; } const auto oldUrl = m_url; beginResetModel(); m_url = url; m_isDirCache.clear(); m_dirModel->dirLister()->openUrl(resolvedUrl); clearDragImages(); m_dragIndexes.clear(); endResetModel(); emit urlChanged(); emit resolvedUrlChanged(); m_errorString.clear(); emit errorStringChanged(); if (m_dirWatch) { delete m_dirWatch; m_dirWatch = nullptr; } if (resolvedUrl.isValid()) { m_dirWatch = new KDirWatch(this); connect(m_dirWatch, &KDirWatch::created, this, &FolderModel::iconNameChanged); connect(m_dirWatch, &KDirWatch::dirty, this, &FolderModel::iconNameChanged); m_dirWatch->addFile(resolvedUrl.toLocalFile() + QLatin1String("/.directory")); } if (m_dragInProgress) { m_urlChangedWhileDragging = true; } emit iconNameChanged(); if (m_usedByContainment) { m_screenMapper->removeScreen(m_screen, oldUrl); m_screenMapper->addScreen(m_screen, url); } } QUrl FolderModel::resolvedUrl() const { return m_dirModel->dirLister()->url(); } QUrl FolderModel::resolve(const QString& url) { QUrl resolvedUrl; if (url.startsWith(QLatin1Char('~'))) { resolvedUrl = QUrl::fromLocalFile(KShell::tildeExpand(url)); } else { resolvedUrl = QUrl::fromUserInput(url); } return resolvedUrl; } QString FolderModel::iconName() const { const KFileItem rootItem(m_dirModel->dirLister()->url()); if (!rootItem.isFinalIconKnown()) { rootItem.determineMimeType(); } return rootItem.iconName(); } FolderModel::Status FolderModel::status() const { return m_status; } void FolderModel::setStatus(Status status) { if (m_status != status) { m_status = status; emit statusChanged(); } } QString FolderModel::errorString() const { return m_errorString; } bool FolderModel::dragging() const { return m_dragInProgress; } bool FolderModel::usedByContainment() const { return m_usedByContainment; } void FolderModel::setUsedByContainment(bool used) { if (m_usedByContainment != used) { m_usedByContainment = used; QAction *action = m_actionCollection.action(QStringLiteral("refresh")); if (action) { action->setText(m_usedByContainment ? i18n("&Refresh Desktop") : i18n("&Refresh View")); action->setIcon(m_usedByContainment ? QIcon::fromTheme(QStringLiteral("user-desktop")) : QIcon::fromTheme(QStringLiteral("view-refresh"))); } m_screenMapper->disconnect(this); connect(m_screenMapper, &ScreenMapper::screensChanged, this, &FolderModel::invalidateFilterIfComplete); connect(m_screenMapper, &ScreenMapper::screenMappingChanged, this, &FolderModel::invalidateFilterIfComplete); emit usedByContainmentChanged(); } } bool FolderModel::locked() const { return m_locked; } void FolderModel::setLocked(bool locked) { if (m_locked != locked) { m_locked = locked; emit lockedChanged(); } } void FolderModel::dirListFailed(const QString& error) { m_errorString = error; emit errorStringChanged(); } int FolderModel::sortMode() const { return m_sortMode; } void FolderModel::setSortMode(int mode) { if (m_sortMode != mode) { m_sortMode = mode; if (mode == -1 /* Unsorted */) { setDynamicSortFilter(false); } else { invalidateIfComplete(); sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder); setDynamicSortFilter(true); } emit sortModeChanged(); } } bool FolderModel::sortDesc() const { return m_sortDesc; } void FolderModel::setSortDesc(bool desc) { if (m_sortDesc != desc) { m_sortDesc = desc; if (m_sortMode != -1 /* Unsorted */) { invalidateIfComplete(); sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder); } emit sortDescChanged(); } } bool FolderModel::sortDirsFirst() const { return m_sortDirsFirst; } void FolderModel::setSortDirsFirst(bool enable) { if (m_sortDirsFirst != enable) { m_sortDirsFirst = enable; if (m_sortMode != -1 /* Unsorted */) { invalidateIfComplete(); sort(m_sortMode, m_sortDesc ? Qt::DescendingOrder : Qt::AscendingOrder); } emit sortDirsFirstChanged(); } } bool FolderModel::parseDesktopFiles() const { return m_parseDesktopFiles; } void FolderModel::setParseDesktopFiles(bool enable) { if (m_parseDesktopFiles != enable) { m_parseDesktopFiles = enable; emit parseDesktopFilesChanged(); } } QObject* FolderModel::viewAdapter() const { return m_viewAdapter; } void FolderModel::setViewAdapter(QObject* adapter) { if (m_viewAdapter != adapter) { KAbstractViewAdapter *abstractViewAdapter = dynamic_cast(adapter); m_viewAdapter = abstractViewAdapter; if (m_viewAdapter && !m_previewGenerator) { m_previewGenerator = new KFilePreviewGenerator(abstractViewAdapter, this); m_previewGenerator->setPreviewShown(m_previews); m_previewGenerator->setEnabledPlugins(m_previewPlugins); } emit viewAdapterChanged(); } } bool FolderModel::previews() const { return m_previews; } void FolderModel::setPreviews(bool previews) { if (m_previews != previews) { m_previews = previews; if (m_previewGenerator) { m_previewGenerator->setPreviewShown(m_previews); } emit previewsChanged(); } } QStringList FolderModel::previewPlugins() const { return m_previewPlugins; } void FolderModel::setPreviewPlugins(const QStringList& previewPlugins) { if (m_previewPlugins != previewPlugins) { m_previewPlugins = previewPlugins; if (m_previewGenerator) { m_previewGenerator->setPreviewShown(false); m_previewGenerator->setEnabledPlugins(m_previewPlugins); m_previewGenerator->setPreviewShown(true); } emit previewPluginsChanged(); } } int FolderModel::filterMode() const { return m_filterMode; } void FolderModel::setFilterMode(int filterMode) { if (m_filterMode != (FilterMode)filterMode) { m_filterMode = (FilterMode)filterMode; invalidateFilterIfComplete(); emit filterModeChanged(); } } QString FolderModel::filterPattern() const { return m_filterPattern; } void FolderModel::setFilterPattern(const QString &pattern) { if (m_filterPattern == pattern) { return; } m_filterPattern = pattern; m_filterPatternMatchAll = (pattern == QLatin1String("*")); const QStringList patterns = pattern.split(QLatin1Char(' ')); m_regExps.clear(); m_regExps.reserve(patterns.count()); foreach (const QString &pattern, patterns) { QRegExp rx(pattern); rx.setPatternSyntax(QRegExp::Wildcard); rx.setCaseSensitivity(Qt::CaseInsensitive); m_regExps.append(rx); } invalidateFilterIfComplete(); emit filterPatternChanged(); } QStringList FolderModel::filterMimeTypes() const { return m_mimeSet.toList(); } void FolderModel::setFilterMimeTypes(const QStringList &mimeList) { const QSet &set = QSet::fromList(mimeList); if (m_mimeSet != set) { m_mimeSet = set; invalidateFilterIfComplete(); emit filterMimeTypesChanged(); } } void FolderModel::setScreen(int screen) { if (m_screen == screen) return; m_screen = screen; if (m_usedByContainment) { m_screenMapper->addScreen(screen, url()); } emit screenChanged(); } KFileItem FolderModel::rootItem() const { return m_dirModel->dirLister()->rootItem(); } void FolderModel::up() { const QUrl &up = KIO::upUrl(resolvedUrl()); if (up.isValid()) { setUrl(up.toString()); } } void FolderModel::cd(int row) { if (row < 0) { return; } const QModelIndex idx = index(row, 0); bool isDir = data(idx, IsDirRole).toBool(); if (isDir) { const KFileItem item = itemForIndex(idx); if (m_parseDesktopFiles && item.isDesktopFile()) { const KDesktopFile file(item.targetUrl().path()); if (file.hasLinkType()) { setUrl(file.readUrl()); } } else { setUrl(item.url().toString()); } } } void FolderModel::run(int row) { if (row < 0) { return; } KFileItem item = itemForIndex(index(row, 0)); QUrl url(item.targetUrl()); // FIXME TODO: This can go once we depend on a KIO w/ fe1f50caaf2. if (url.scheme().isEmpty()) { url.setScheme(QStringLiteral("file")); } KRun *run = new KRun(url, nullptr); // On desktop:/ we want to be able to run .desktop files right away, // otherwise ask for security reasons. We also don't use the targetUrl() // from above since we don't want the resolved /home/foo/Desktop URL. run->setShowScriptExecutionPrompt(item.url().scheme() != QLatin1String("desktop") || item.url().adjusted(QUrl::RemoveFilename).path() != QLatin1String("/")); } void FolderModel::runSelected() { if (!m_selectionModel->hasSelection()) { return; } if (m_selectionModel->selectedIndexes().count() == 1) { run(m_selectionModel->selectedIndexes().constFirst().row()); return; } KFileItemActions fileItemActions(this); KFileItemList items; foreach (const QModelIndex &index, m_selectionModel->selectedIndexes()) { // Skip over directories. if (!index.data(IsDirRole).toBool()) { items << itemForIndex(index); } } fileItemActions.runPreferredApplications(items, QString()); } void FolderModel::rename(int row, const QString& name) { if (row < 0) { return; } QModelIndex idx = index(row, 0); m_dirModel->setData(mapToSource(idx), name, Qt::EditRole); } int FolderModel::fileExtensionBoundary(int row) { const QModelIndex idx = index(row, 0); const QString &name = data(idx, Qt::DisplayRole).toString(); int boundary = name.length(); if (data(idx, IsDirRole).toBool()) { return boundary; } QMimeDatabase db; const QString &ext = db.suffixForFileName(name); if (ext.isEmpty()) { boundary = name.lastIndexOf(QLatin1Char('.')); if (boundary < 1) { boundary = name.length(); } } else { boundary -= ext.length() + 1; } return boundary; } bool FolderModel::hasSelection() const { return m_selectionModel->hasSelection(); } bool FolderModel::isSelected(int row) { if (row < 0) { return false; } return m_selectionModel->isSelected(index(row, 0)); } void FolderModel::setSelected(int row) { if (row < 0) { return; } m_selectionModel->select(index(row, 0), QItemSelectionModel::Select); } void FolderModel::toggleSelected(int row) { if (row < 0) { return; } m_selectionModel->select(index(row, 0), QItemSelectionModel::Toggle); } void FolderModel::setRangeSelected(int anchor, int to) { if (anchor < 0 || to < 0) { return; } QItemSelection selection(index(anchor, 0), index(to, 0)); m_selectionModel->select(selection, QItemSelectionModel::ClearAndSelect); } void FolderModel::updateSelection(const QVariantList &rows, bool toggle) { QItemSelection newSelection; int iRow = -1; foreach (const QVariant &row, rows) { iRow = row.toInt(); if (iRow < 0) { return; } const QModelIndex &idx = index(iRow, 0); newSelection.select(idx, idx); } if (toggle) { QItemSelection pinnedSelection = m_pinnedSelection; pinnedSelection.merge(newSelection, QItemSelectionModel::Toggle); m_selectionModel->select(pinnedSelection, QItemSelectionModel::ClearAndSelect); } else { m_selectionModel->select(newSelection, QItemSelectionModel::ClearAndSelect); } } void FolderModel::clearSelection() { if (m_selectionModel->hasSelection()) { m_selectionModel->clear(); } } void FolderModel::pinSelection() { m_pinnedSelection = m_selectionModel->selection(); } void FolderModel::unpinSelection() { m_pinnedSelection = QItemSelection(); } void FolderModel::addItemDragImage(int row, int x, int y, int width, int height, const QVariant &image) { if (row < 0) { return; } delete m_dragImages.take(row); DragImage *dragImage = new DragImage(); dragImage->row = row; dragImage->rect = QRect(x, y, width, height); dragImage->image = image.value(); dragImage->blank = false; m_dragImages.insert(row, dragImage); } void FolderModel::clearDragImages() { if (!m_dragImages.isEmpty()) { foreach (DragImage *image, m_dragImages) { delete image; } m_dragImages.clear(); } } void FolderModel::setDragHotSpotScrollOffset(int x, int y) { m_dragHotSpotScrollOffset.setX(x); m_dragHotSpotScrollOffset.setY(y); } QPoint FolderModel::dragCursorOffset(int row) { DragImage *image = m_dragImages.value(row); if (!image) { return QPoint(0, 0); } return image->cursorOffset; } void FolderModel::addDragImage(QDrag *drag, int x, int y) { if (!drag || m_dragImages.isEmpty()) { return; } QRegion region; foreach (DragImage *image, m_dragImages) { image->blank = isBlank(image->row); image->rect.translate(-m_dragHotSpotScrollOffset.x(), -m_dragHotSpotScrollOffset.y()); if (!image->blank && !image->image.isNull()) { region = region.united(image->rect); } } QRect rect = region.boundingRect(); QPoint offset = rect.topLeft(); rect.translate(-offset.x(), -offset.y()); QImage dragImage(rect.size(), QImage::Format_RGBA8888); dragImage.fill(Qt::transparent); QPainter painter(&dragImage); QPoint pos; foreach (DragImage *image, m_dragImages) { if (!image->blank && !image->image.isNull()) { pos = image->rect.translated(-offset.x(), -offset.y()).topLeft(); image->cursorOffset.setX(pos.x() - (x - offset.x())); image->cursorOffset.setY(pos.y() - (y - offset.y())); painter.drawImage(pos, image->image); } // FIXME HACK: Operate on copy. image->rect.translate(m_dragHotSpotScrollOffset.x(), m_dragHotSpotScrollOffset.y()); } drag->setPixmap(QPixmap::fromImage(dragImage)); drag->setHotSpot(QPoint(x - offset.x(), y - offset.y())); } void FolderModel::dragSelected(int x, int y) { if (m_dragInProgress) { return; } m_dragInProgress = true; emit draggingChanged(); m_urlChangedWhileDragging = false; // Avoid starting a drag synchronously in a mouse handler or interferes with // child event filtering in parent items (and thus e.g. press-and-hold hand- // ling in a containment). QMetaObject::invokeMethod(this, "dragSelectedInternal", Qt::QueuedConnection, Q_ARG(int, x), Q_ARG(int, y)); } void FolderModel::dragSelectedInternal(int x, int y) { if (!m_viewAdapter || !m_selectionModel->hasSelection()) { m_dragInProgress = false; emit draggingChanged(); return; } ItemViewAdapter *adapter = qobject_cast(m_viewAdapter); QQuickItem *item = qobject_cast(adapter->adapterView()); QDrag *drag = new QDrag(item); addDragImage(drag, x, y); m_dragIndexes = m_selectionModel->selectedIndexes(); qSort(m_dragIndexes.begin(), m_dragIndexes.end()); // TODO: Optimize to emit contiguous groups. emit dataChanged(m_dragIndexes.first(), m_dragIndexes.last(), QVector() << BlankRole); QModelIndexList sourceDragIndexes; sourceDragIndexes.reserve(m_dragIndexes.count()); foreach (const QModelIndex &index, m_dragIndexes) { sourceDragIndexes.append(mapToSource(index)); } drag->setMimeData(m_dirModel->mimeData(sourceDragIndexes)); // Due to spring-loading (aka auto-expand), the URL might change // while the drag is in-flight - in that case we don't want to // unnecessarily emit dataChanged() for (possibly invalid) indices // after it ends. const QUrl currentUrl(m_dirModel->dirLister()->url()); item->grabMouse(); drag->exec(supportedDragActions()); item->ungrabMouse(); m_dragInProgress = false; emit draggingChanged(); m_urlChangedWhileDragging = false; if (m_dirModel->dirLister()->url() == currentUrl) { const QModelIndex first(m_dragIndexes.first()); const QModelIndex last(m_dragIndexes.last()); m_dragIndexes.clear(); // TODO: Optimize to emit contiguous groups. emit dataChanged(first, last, QVector() << BlankRole); } } static bool isDropBetweenSharedViews(const QList &urls, const QUrl &folderUrl) { for (const auto &url : urls) { if (folderUrl != url.adjusted(QUrl::RemoveFilename)) { return false; } } return true; } void FolderModel::drop(QQuickItem *target, QObject* dropEvent, int row) { QMimeData *mimeData = qobject_cast(dropEvent->property("mimeData").value()); if (!mimeData) { return; } QModelIndex idx; KFileItem item; if (row > -1 && row < rowCount()) { idx = index(row, 0); item = itemForIndex(idx); } QUrl dropTargetUrl; // So we get to run mostLocalUrl() over the current URL. if (item.isNull()) { item = rootItem(); } if (item.isNull()) { dropTargetUrl = m_dirModel->dirLister()->url(); } else if (m_parseDesktopFiles && item.isDesktopFile()) { const KDesktopFile file(item.targetUrl().path()); if (file.hasLinkType()) { dropTargetUrl = QUrl(file.readUrl()); } else { dropTargetUrl = item.mostLocalUrl(); } } else { dropTargetUrl = item.mostLocalUrl(); } auto dropTargetFolderUrl = dropTargetUrl; if (dropTargetFolderUrl.fileName() == QLatin1String(".")) { // the target URL for desktop:/ is e.g. 'file://home/user/Desktop/.' dropTargetFolderUrl = dropTargetFolderUrl.adjusted(QUrl::RemoveFilename); } // use dropTargetUrl to resolve desktop:/ to the actual file location which is also used by the mime data /* QMimeData operates on local URLs, but the dir lister and thus screen mapper and positioner may * use a fancy scheme like desktop:/ instead. Ensure we always use the latter to properly map URLs, * i.e. go from file:///home/user/Desktop/file to desktop:/file */ auto mappableUrl = [this, dropTargetFolderUrl](const QUrl &url) -> QString { QString mappedUrl = url.toString(); if (dropTargetFolderUrl != m_dirModel->dirLister()->url()) { const auto local = dropTargetFolderUrl.toString(); const auto internal = m_dirModel->dirLister()->url().toString(); if (mappedUrl.startsWith(local)) { mappedUrl.replace(0, local.size(), internal); } } return mappedUrl; }; const int x = dropEvent->property("x").toInt(); const int y = dropEvent->property("y").toInt(); const QPoint dropPos = {x, y}; if (m_dragInProgress && row == -1 && !m_urlChangedWhileDragging) { if (m_locked || mimeData->urls().isEmpty()) { return; } setSortMode(-1); for (const auto &url : mimeData->urls()) { m_dropTargetPositions.insert(url.fileName(), dropPos); m_screenMapper->addMapping(mappableUrl(url), m_screen, ScreenMapper::DelayedSignal); m_screenMapper->removeItemFromDisabledScreen(mappableUrl(url)); } emit move(x, y, mimeData->urls()); return; } if (mimeData->hasFormat(QStringLiteral("application/x-kde-ark-dndextract-service")) && mimeData->hasFormat(QStringLiteral("application/x-kde-ark-dndextract-path"))) { const QString remoteDBusClient = mimeData->data(QStringLiteral("application/x-kde-ark-dndextract-service")); const QString remoteDBusPath = mimeData->data(QStringLiteral("application/x-kde-ark-dndextract-path")); QDBusMessage message = QDBusMessage::createMethodCall(remoteDBusClient, remoteDBusPath, QStringLiteral("org.kde.ark.DndExtract"), QStringLiteral("extractSelectedFilesTo")); message.setArguments({dropTargetUrl.toDisplayString(QUrl::PreferLocalFile)}); QDBusConnection::sessionBus().call(message, QDBus::NoBlock); return; } if (idx.isValid() && !(flags(idx) & Qt::ItemIsDropEnabled)) { return; } if (m_usedByContainment) { if (isDropBetweenSharedViews(mimeData->urls(), dropTargetFolderUrl)) { setSortMode(-1); for (const auto &url : mimeData->urls()) { m_dropTargetPositions.insert(url.fileName(), dropPos); m_screenMapper->addMapping(mappableUrl(url), m_screen, ScreenMapper::DelayedSignal); m_screenMapper->removeItemFromDisabledScreen(mappableUrl(url)); } m_dropTargetPositionsCleanup->start(); return; } } Qt::DropAction proposedAction((Qt::DropAction)dropEvent->property("proposedAction").toInt()); Qt::DropActions possibleActions(dropEvent->property("possibleActions").toInt()); Qt::MouseButtons buttons(dropEvent->property("buttons").toInt()); Qt::KeyboardModifiers modifiers(dropEvent->property("modifiers").toInt()); auto pos = target->mapToScene(dropPos).toPoint(); pos = target->window()->mapToGlobal(pos); QDropEvent ev(pos, possibleActions, mimeData, buttons, modifiers); ev.setDropAction(proposedAction); KIO::DropJob *dropJob = KIO::drop(&ev, dropTargetUrl); dropJob->uiDelegate()->setAutoErrorHandlingEnabled(true); // The QMimeData we extract from the DropArea's drop event is deleted as soon as this method // ends but we need to keep a copy for when popupMenuAboutToShow fires. QMimeData *mimeCopy = new QMimeData(); for (const QString &format : mimeData->formats()) { mimeCopy->setData(format, mimeData->data(format)); } connect(dropJob, &KIO::DropJob::popupMenuAboutToShow, this, [this, mimeCopy, x, y, dropJob](const KFileItemListProperties &) { emit popupMenuAboutToShow(dropJob, mimeCopy, x, y); mimeCopy->deleteLater(); }); /* * Position files that come from a drag'n'drop event at the drop event * target position. To do so, we first listen to copy job to figure out * the target URL. Then we store the position of this drop event in the * hash and eventually trigger a move request when we get notified about * the new file event from the source model. */ connect(dropJob, &KIO::DropJob::copyJobStarted, this, [this, dropPos, dropTargetUrl](KIO::CopyJob* copyJob) { auto map = [this, dropPos, dropTargetUrl](const QUrl &targetUrl) { m_dropTargetPositions.insert(targetUrl.fileName(), dropPos); m_dropTargetPositionsCleanup->start(); if (m_usedByContainment) { // assign a screen for the item before the copy is actually done, so // filterAcceptsRow doesn't assign the default screen to it QUrl url = QUrl::fromUserInput(m_url, {}, QUrl::AssumeLocalFile); // if the folderview's folder is a standard path, just use the targetUrl for mapping if (targetUrl.toString().startsWith(url.toString())) { m_screenMapper->addMapping(targetUrl.toString(), m_screen, ScreenMapper::DelayedSignal); } else if (targetUrl.toString().startsWith(dropTargetUrl.toString())) { // if the folderview's folder is a special path, like desktop:// , we need to convert // the targetUrl file:// path to a desktop:/ path for mapping auto destPath = dropTargetUrl.path(); auto filePath = targetUrl.path(); if (filePath.startsWith(destPath)) { url.setPath(filePath.remove(0, destPath.length())); m_screenMapper->addMapping(url.toString(), m_screen, ScreenMapper::DelayedSignal); } } } }; // remember drop target position for target URL and forget about the source URL connect(copyJob, &KIO::CopyJob::copyingDone, this, [this, map](KIO::Job *, const QUrl &, const QUrl &targetUrl, const QDateTime &, bool, bool) { map(targetUrl); }); connect(copyJob, &KIO::CopyJob::copyingLinkDone, this, [this, map](KIO::Job *, const QUrl &, const QString &, const QUrl &targetUrl) { map(targetUrl); }); }); } void FolderModel::dropCwd(QObject* dropEvent) { QMimeData *mimeData = qobject_cast(dropEvent->property("mimeData").value()); if (!mimeData) { return; } if (mimeData->hasFormat(QStringLiteral("application/x-kde-ark-dndextract-service")) && mimeData->hasFormat(QStringLiteral("application/x-kde-ark-dndextract-path"))) { const QString remoteDBusClient = mimeData->data(QStringLiteral("application/x-kde-ark-dndextract-service")); const QString remoteDBusPath = mimeData->data(QStringLiteral("application/x-kde-ark-dndextract-path")); QDBusMessage message = QDBusMessage::createMethodCall(remoteDBusClient, remoteDBusPath, QStringLiteral("org.kde.ark.DndExtract"), QStringLiteral("extractSelectedFilesTo")); message.setArguments(QVariantList() << m_dirModel->dirLister()->url().adjusted(QUrl::PreferLocalFile).toString()); QDBusConnection::sessionBus().call(message, QDBus::NoBlock); } else { Qt::DropAction proposedAction((Qt::DropAction)dropEvent->property("proposedAction").toInt()); Qt::DropActions possibleActions(dropEvent->property("possibleActions").toInt()); Qt::MouseButtons buttons(dropEvent->property("buttons").toInt()); Qt::KeyboardModifiers modifiers(dropEvent->property("modifiers").toInt()); QDropEvent ev(QPoint(), possibleActions, mimeData, buttons, modifiers); ev.setDropAction(proposedAction); KIO::DropJob *dropJob = KIO::drop(&ev, m_dirModel->dirLister()->url().adjusted(QUrl::PreferLocalFile)); dropJob->uiDelegate()->setAutoErrorHandlingEnabled(true); } } void FolderModel::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { QModelIndexList indices = selected.indexes(); indices.append(deselected.indexes()); QVector roles; roles.append(SelectedRole); foreach(const QModelIndex &index, indices) { emit dataChanged(index, index, roles); } if (!m_selectionModel->hasSelection()) { clearDragImages(); } else { foreach (const QModelIndex &idx, deselected.indexes()) { if (m_dragImages.contains(idx.row())) { DragImage *image = m_dragImages.value(idx.row()); delete image; m_dragImages.remove(idx.row()); } } } } bool FolderModel::isBlank(int row) const { if (row < 0) { return true; } return data(index(row, 0), BlankRole).toBool(); } QVariant FolderModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) { return QVariant(); } if (role == BlankRole) { return m_dragIndexes.contains(index); } else if (role == OverlaysRole) { const KFileItem item = itemForIndex(index); return item.overlays(); } else if (role == SelectedRole) { return m_selectionModel->isSelected(index); } else if (role == IsDirRole) { const QUrl &url = data(index, UrlRole).value(); if (m_isDirCache.contains(url)) { return m_isDirCache[url]; } else { return isDir(mapToSource(index), m_dirModel); } } else if (role == IsLinkRole) { const KFileItem item = itemForIndex(index); return item.isLink(); } else if (role == IsHiddenRole) { const KFileItem item = itemForIndex(index); return item.isHidden(); } else if (role == UrlRole) { return itemForIndex(index).url(); } else if (role == LinkDestinationUrl) { const KFileItem item = itemForIndex(index); if (m_parseDesktopFiles && item.isDesktopFile()) { const KDesktopFile file(item.targetUrl().path()); if (file.hasLinkType()) { return file.readUrl(); } } return item.url(); } else if (role == SizeRole) { bool isDir = data(index, IsDirRole).toBool(); if (!isDir) { return m_dirModel->data(mapToSource(QSortFilterProxyModel::index(index.row(), 1)), Qt::DisplayRole); } } else if (role == TypeRole) { return m_dirModel->data(mapToSource(QSortFilterProxyModel::index(index.row(), 6)), Qt::DisplayRole); } else if (role == FileNameRole) { return itemForIndex(index).url().fileName(); } return QSortFilterProxyModel::data(index, role); } int FolderModel::indexForUrl(const QUrl& url) const { return mapFromSource(m_dirModel->indexForUrl(url)).row(); } KFileItem FolderModel::itemForIndex(const QModelIndex &index) const { return m_dirModel->itemForIndex(mapToSource(index)); } bool FolderModel::isDir(const QModelIndex &index, const KDirModel *dirModel) const { KFileItem item = dirModel->itemForIndex(index); if (item.isDir()) { return true; } if (m_parseDesktopFiles && item.isDesktopFile()) { // Check if the desktop file is a link to a directory KDesktopFile file(item.targetUrl().path()); if (file.hasLinkType()) { const QUrl url(file.readUrl()); if (!m_isDirCache.contains(item.url()) && KProtocolInfo::protocolClass(url.scheme()) == QStringLiteral(":local")) { KIO::StatJob *job = KIO::stat(url, KIO::HideProgressInfo); job->setProperty("org.kde.plasma.folder_url", item.url()); job->setSide(KIO::StatJob::SourceSide); job->setDetails(0); connect(job, &KJob::result, this, &FolderModel::statResult); } } } return false; } void FolderModel::statResult(KJob *job) { KIO::StatJob *statJob = static_cast(job); const QUrl &url = statJob->property("org.kde.plasma.folder_url").value(); const QModelIndex &idx = index(indexForUrl(url), 0); if (idx.isValid()) { m_isDirCache[url] = statJob->statResult().isDir(); emit dataChanged(idx, idx, QVector() << IsDirRole); } } void FolderModel::evictFromIsDirCache(const KFileItemList& items) { foreach (const KFileItem &item, items) { m_screenMapper->removeFromMap(item.url().toString()); m_isDirCache.remove(item.url()); } } bool FolderModel::lessThan(const QModelIndex &left, const QModelIndex &right) const { const KDirModel *dirModel = static_cast(sourceModel()); if (m_sortDirsFirst || left.column() == KDirModel::Size) { bool leftIsDir = isDir(left, dirModel); bool rightIsDir = isDir(right, dirModel); if (leftIsDir && !rightIsDir) { return (sortOrder() == Qt::AscendingOrder); } if (!leftIsDir && rightIsDir) { return (sortOrder() == Qt::DescendingOrder); } } const KFileItem leftItem = dirModel->data(left, KDirModel::FileItemRole).value(); const KFileItem rightItem = dirModel->data(right, KDirModel::FileItemRole).value(); const int column = left.column(); int result = 0; switch (column) { case KDirModel::Size: { if (isDir(left, dirModel) && isDir(right, dirModel)) { const int leftChildCount = dirModel->data(left, KDirModel::ChildCountRole).toInt(); const int rightChildCount = dirModel->data(right, KDirModel::ChildCountRole).toInt(); if (leftChildCount < rightChildCount) result = -1; else if (leftChildCount > rightChildCount) result = +1; } else { const KIO::filesize_t leftSize = leftItem.size(); const KIO::filesize_t rightSize = rightItem.size(); if (leftSize < rightSize) result = -1; else if (leftSize > rightSize) result = +1; } break; } case KDirModel::ModifiedTime: { const QDateTime leftTime = leftItem.time(KFileItem::ModificationTime); const QDateTime rightTime = rightItem.time(KFileItem::ModificationTime); if (leftTime < rightTime) result = -1; else if (leftTime > rightTime) result = +1; break; } case KDirModel::Type: result = QString::compare(dirModel->data(left, Qt::DisplayRole).toString(), dirModel->data(right, Qt::DisplayRole).toString()); break; default: break; } if (result != 0) return result < 0; QCollator collator; result = collator.compare(leftItem.text(), rightItem.text()); if (result != 0) return result < 0; result = collator.compare(leftItem.name(), rightItem.name()); if (result != 0) return result < 0; return QString::compare(leftItem.url().url(), rightItem.url().url(), Qt::CaseSensitive); } Qt::DropActions FolderModel::supportedDragActions() const { return Qt::CopyAction | Qt::MoveAction | Qt::LinkAction; } inline bool FolderModel::matchMimeType(const KFileItem &item) const { if (m_mimeSet.isEmpty()) { return false; } if (m_mimeSet.contains(QStringLiteral("all/all")) || m_mimeSet.contains(QStringLiteral("all/allfiles"))) { return true; } const QString mimeType = item.determineMimeType().name(); return m_mimeSet.contains(mimeType); } inline bool FolderModel::matchPattern(const KFileItem &item) const { if (m_filterPatternMatchAll) { return true; } const QString name = item.name(); QListIterator i(m_regExps); while (i.hasNext()) { if (i.next().exactMatch(name)) { return true; } } return false; } bool FolderModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { const KDirModel *dirModel = static_cast(sourceModel()); const KFileItem item = dirModel->itemForIndex(dirModel->index(sourceRow, KDirModel::Name, sourceParent)); if (m_usedByContainment) { const QString name = item.url().toString(); const int screen = m_screenMapper->screenForItem(name); // don't do anything if the folderview is not associated with a screen if (m_screen != -1) { if (screen == -1) { // The item is not associated with a screen, probably because this is the first // time we see it or the folderview was previously used as a regular applet. // Associated with this folderview if the view is on the first available screen if (m_screen == m_screenMapper->firstAvailableScreen(url())) { m_screenMapper->addMapping(name, m_screen, ScreenMapper::DelayedSignal); } else { return false; } } else if (m_screen != screen) { // the item belongs to a different screen, filter it out return false; } } } if (m_filterMode == NoFilter) { return true; } if (m_filterMode == FilterShowMatches) { return (matchPattern(item) && matchMimeType(item)); } else { return !(matchPattern(item) && matchMimeType(item)); } } void FolderModel::createActions() { KIO::FileUndoManager *manager = KIO::FileUndoManager::self(); QAction *cut = KStandardAction::cut(this, &FolderModel::cut, this); QAction *copy = KStandardAction::copy(this, &FolderModel::copy, this); QAction *undo = KStandardAction::undo(manager, &KIO::FileUndoManager::undo, this); undo->setEnabled(manager->undoAvailable()); undo->setShortcutContext(Qt::WidgetShortcut); connect(manager, SIGNAL(undoAvailable(bool)), undo, SLOT(setEnabled(bool))); connect(manager, &KIO::FileUndoManager::undoTextChanged, this, &FolderModel::undoTextChanged); QAction *paste = KStandardAction::paste(this, &FolderModel::paste, this); QAction *pasteTo = KStandardAction::paste(this, &FolderModel::pasteTo, this); QAction *reload = new QAction(i18n("&Reload"), this); connect(reload, &QAction::triggered, this, &FolderModel::refresh); QAction *refresh = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("&Refresh View"), this); connect(refresh, &QAction::triggered, this, &FolderModel::refresh); QAction *rename = new QAction(QIcon::fromTheme(QStringLiteral("edit-rename")), i18n("&Rename"), this); connect(rename, &QAction::triggered, this, &FolderModel::requestRename); QAction *trash = new QAction(QIcon::fromTheme(QStringLiteral("user-trash")), i18n("&Move to Trash"), this); connect(trash, &QAction::triggered, this, &FolderModel::moveSelectedToTrash); QAction *emptyTrash = new QAction(QIcon::fromTheme(QStringLiteral("trash-empty")), i18n("&Empty Trash Bin"), this); connect(emptyTrash, &QAction::triggered, this, &FolderModel::emptyTrashBin); QAction *restoreFromTrash = new QAction(i18nc("Restore from trash", "Restore"), this); connect(restoreFromTrash, &QAction::triggered, this, &FolderModel::restoreSelectedFromTrash); QAction *del = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("&Delete"), this); connect(del, &QAction::triggered, this, &FolderModel::deleteSelected); QAction *actOpen = new QAction(QIcon::fromTheme(QStringLiteral("window-new")), i18n("&Open"), this); connect(actOpen, &QAction::triggered, this, &FolderModel::openSelected); m_actionCollection.addAction(QStringLiteral("open"), actOpen); m_actionCollection.addAction(QStringLiteral("cut"), cut); m_actionCollection.addAction(QStringLiteral("undo"), undo); m_actionCollection.addAction(QStringLiteral("copy"), copy); m_actionCollection.addAction(QStringLiteral("paste"), paste); m_actionCollection.addAction(QStringLiteral("pasteto"), pasteTo); m_actionCollection.addAction(QStringLiteral("reload"), reload); m_actionCollection.addAction(QStringLiteral("refresh"), refresh); m_actionCollection.addAction(QStringLiteral("rename"), rename); m_actionCollection.addAction(QStringLiteral("trash"), trash); m_actionCollection.addAction(QStringLiteral("del"), del); m_actionCollection.addAction(QStringLiteral("restoreFromTrash"), restoreFromTrash); m_actionCollection.addAction(QStringLiteral("emptyTrash"), emptyTrash); m_newMenu = new KNewFileMenu(&m_actionCollection, QStringLiteral("newMenu"), QApplication::desktop()); m_newMenu->setModal(false); connect(m_newMenu, &KNewFileMenu::directoryCreated, this, &FolderModel::newFileMenuItemCreated); connect(m_newMenu, &KNewFileMenu::fileCreated, this, &FolderModel::newFileMenuItemCreated); m_copyToMenu = new KFileCopyToMenu(nullptr); } QAction* FolderModel::action(const QString &name) const { return m_actionCollection.action(name); } QObject* FolderModel::newMenu() const { return m_newMenu->menu(); } void FolderModel::updateActions() { if (m_newMenu) { m_newMenu->checkUpToDate(); m_newMenu->setPopupFiles(m_dirModel->dirLister()->url()); // we need to set here as well, when the menu is shown via AppletInterface::eventFilter m_menuPosition = QCursor::pos(); } const bool isTrash = (resolvedUrl().scheme() == QLatin1String("trash")); QAction *emptyTrash = m_actionCollection.action(QStringLiteral("emptyTrash")); if (emptyTrash) { if (isTrash) { emptyTrash->setVisible(true); KConfig trashConfig(QStringLiteral("trashrc"), KConfig::SimpleConfig); emptyTrash->setEnabled(!trashConfig.group("Status").readEntry("Empty", true)); } else { emptyTrash->setVisible(false); } } if (QAction *restoreFromTrash = m_actionCollection.action(QStringLiteral("restoreFromTrash"))) { restoreFromTrash->setVisible(isTrash); } QAction *paste = m_actionCollection.action(QStringLiteral("paste")); if (paste) { bool enable = false; const QString pasteText = KIO::pasteActionText(QApplication::clipboard()->mimeData(), &enable, rootItem()); if (enable) { paste->setText(pasteText); paste->setEnabled(true); } else { paste->setText(i18n("&Paste")); paste->setEnabled(false); } QAction* pasteTo = m_actionCollection.action(QStringLiteral("pasteto")); if (pasteTo) { pasteTo->setEnabled(paste->isEnabled()); pasteTo->setText(paste->text()); } } } void FolderModel::openContextMenu(QQuickItem *visualParent) { QModelIndexList indexes = m_selectionModel->selectedIndexes(); if (m_usedByContainment && !KAuthorized::authorize(QStringLiteral("action/kdesktop_rmb"))) { return; } updateActions(); QMenu *menu = new QMenu(); if (!m_fileItemActions) { m_fileItemActions = new KFileItemActions(this); m_fileItemActions->setParentWidget(QApplication::desktop()); } if (indexes.isEmpty()) { menu->addAction(m_actionCollection.action(QStringLiteral("newMenu"))); menu->addSeparator(); menu->addAction(m_actionCollection.action(QStringLiteral("paste"))); menu->addAction(m_actionCollection.action(QStringLiteral("undo"))); menu->addAction(m_actionCollection.action(QStringLiteral("refresh"))); menu->addAction(m_actionCollection.action(QStringLiteral("emptyTrash"))); menu->addSeparator(); KFileItemListProperties itemProperties(KFileItemList() << rootItem()); m_fileItemActions->setItemListProperties(itemProperties); menu->addAction(m_fileItemActions->preferredOpenWithAction(QString())); } else { KFileItemList items; QList urls; bool hasRemoteFiles = false; bool isTrashLink = false; items.reserve(indexes.count()); urls.reserve(indexes.count()); foreach (const QModelIndex &index, indexes) { KFileItem item = itemForIndex(index); if (!item.isNull()) { hasRemoteFiles |= item.localPath().isEmpty(); items.append(item); urls.append(item.url()); } } KFileItemListProperties itemProperties(items); // Check if we're showing the menu for the trash link if (items.count() == 1 && items.at(0).isDesktopFile()) { KDesktopFile file(items.at(0).localPath()); if (file.hasLinkType() && file.readUrl() == QLatin1String("trash:/")) { isTrashLink = true; } } // Start adding the actions: menu->addAction(m_actionCollection.action(QStringLiteral("open"))); menu->addSeparator(); if (itemProperties.supportsDeleting()) { menu->addAction(m_actionCollection.action(QStringLiteral("cut"))); } menu->addAction(m_actionCollection.action(QStringLiteral("copy"))); if (itemProperties.isDirectory() && itemProperties.supportsWriting()) { menu->addAction(m_actionCollection.action(QStringLiteral("pasteto"))); } menu->addAction(m_actionCollection.action(QStringLiteral("rename"))); menu->addAction(m_actionCollection.action(QStringLiteral("restoreFromTrash"))); KSharedConfig::Ptr globalConfig = KSharedConfig::openConfig(QStringLiteral("kdeglobals"), KConfig::NoGlobals); KConfigGroup cg(globalConfig, "KDE"); bool showDeleteCommand = cg.readEntry("ShowDeleteCommand", false); // When we're showing the menu for the trash link, offer "Empty Trash" instead // of the "Move to Trash" action. if (isTrashLink) { QAction *emptyTrashAction = m_actionCollection.action(QStringLiteral("emptyTrash")); if (emptyTrashAction) { // We explicitly force the action visible here, as it relies on the KFileItemList // we collected above. In updateActions() we don't have it and since this is always // called before we open the menu, it would correct visibility again when opening // the context menu for other items later. emptyTrashAction->setVisible(true); menu->addAction(emptyTrashAction); } } else { if (!hasRemoteFiles && itemProperties.supportsMoving()) { menu->addAction(m_actionCollection.action(QStringLiteral("trash"))); } else { showDeleteCommand = true; } } if (showDeleteCommand && itemProperties.supportsDeleting()) { menu->addAction(m_actionCollection.action(QStringLiteral("del"))); } // "Open with" actions m_fileItemActions->setItemListProperties(itemProperties); m_fileItemActions->addOpenWithActionsTo(menu); // Service actions m_fileItemActions->addServiceActionsTo(menu); menu->addSeparator(); // Plugin actions m_fileItemActions->addPluginActionsTo(menu); // Copy To, Move To KSharedConfig::Ptr dolphin = KSharedConfig::openConfig(QStringLiteral("dolphinrc")); if (KConfigGroup(dolphin, "General").readEntry("ShowCopyMoveMenu", false)) { m_copyToMenu->setUrls(urls); m_copyToMenu->setReadOnly(!itemProperties.supportsMoving()); m_copyToMenu->addActionsTo(menu); menu->addSeparator(); } // Properties if (KPropertiesDialog::canDisplay(items)) { QAction *act = new QAction(menu); act->setText(i18n("&Properties")); QObject::connect(act, &QAction::triggered, [this, items]() { KPropertiesDialog::showDialog(items, nullptr, false /*non modal*/); }); menu->addAction(act); } } if (visualParent) { m_menuPosition = visualParent->mapToGlobal(QPointF(0, visualParent->height())).toPoint(); } else { m_menuPosition = QCursor::pos(); } menu->popup(m_menuPosition); connect(menu, &QMenu::aboutToHide, [menu]() { menu->deleteLater(); }); } void FolderModel::linkHere(const QUrl &sourceUrl) { KIO::CopyJob *job = KIO::link(sourceUrl, m_dirModel->dirLister()->url()); KIO::FileUndoManager::self()->recordCopyJob(job); } QList FolderModel::selectedUrls() const { const auto indexes = m_selectionModel->selectedIndexes(); QList urls; urls.reserve(indexes.count()); for (const QModelIndex &index : indexes) { urls.append(itemForIndex(index).url()); } return urls; } void FolderModel::copy() { if (!m_selectionModel->hasSelection()) { return; } QMimeData *mimeData = QSortFilterProxyModel::mimeData(m_selectionModel->selectedIndexes()); QApplication::clipboard()->setMimeData(mimeData); } void FolderModel::cut() { if (!m_selectionModel->hasSelection()) { return; } QMimeData *mimeData = QSortFilterProxyModel::mimeData(m_selectionModel->selectedIndexes()); KIO::setClipboardDataCut(mimeData, true); QApplication::clipboard()->setMimeData(mimeData); } void FolderModel::paste() { KIO::paste(QApplication::clipboard()->mimeData(), m_dirModel->dirLister()->url()); } void FolderModel::pasteTo() { const QList urls = selectedUrls(); Q_ASSERT(urls.count() == 1); KIO::paste(QApplication::clipboard()->mimeData(), urls.first()); } void FolderModel::refresh() { m_errorString.clear(); emit errorStringChanged(); m_dirModel->dirLister()->updateDirectory(m_dirModel->dirLister()->url()); } QObject *FolderModel::appletInterface() const { return m_appletInterface; } void FolderModel::setAppletInterface(QObject *appletInterface) { if (m_appletInterface != appletInterface) { Q_ASSERT(!m_appletInterface); m_appletInterface = appletInterface; if (appletInterface) { Plasma::Applet *applet = appletInterface->property("_plasma_applet").value(); if (applet) { Plasma::Containment *containment = applet->containment(); if (containment) { Plasma::Corona *corona = containment->corona(); if (corona) { m_screenMapper->setCorona(corona); } setScreen(containment->screen()); connect(containment, &Plasma::Containment::screenChanged, this, &FolderModel::setScreen); } } } emit appletInterfaceChanged(); } } void FolderModel::moveSelectedToTrash() { if (!m_selectionModel->hasSelection()) { return; } const QList urls = selectedUrls(); KIO::JobUiDelegate uiDelegate; + if (uiDelegate.askDeleteConfirmation(urls, KIO::JobUiDelegate::Trash, KIO::JobUiDelegate::DefaultConfirmation)) { KIO::Job* job = KIO::trash(urls); job->uiDelegate()->setAutoErrorHandlingEnabled(true); KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Trash, urls, QUrl(QStringLiteral("trash:/")), job); } } void FolderModel::deleteSelected() { if (!m_selectionModel->hasSelection()) { return; } const QList urls = selectedUrls(); KIO::JobUiDelegate uiDelegate; + if (uiDelegate.askDeleteConfirmation(urls, KIO::JobUiDelegate::Delete, KIO::JobUiDelegate::DefaultConfirmation)) { KIO::Job* job = KIO::del(urls); job->uiDelegate()->setAutoErrorHandlingEnabled(true); } } void FolderModel::openSelected() { if (!m_selectionModel->hasSelection()) { return; } const QList urls = selectedUrls(); for (const QUrl &url : urls) { (void) new KRun(url, nullptr); } } void FolderModel::undo() { if (QAction *action = m_actionCollection.action(QStringLiteral("undo"))) { // trigger() doesn't check enabled and would crash if invoked nonetheless. if (action->isEnabled()) { action->trigger(); } } } void FolderModel::emptyTrashBin() { KIO::JobUiDelegate uiDelegate; uiDelegate.setWindow(QApplication::desktop()); + if (uiDelegate.askDeleteConfirmation(QList(), KIO::JobUiDelegate::EmptyTrash, KIO::JobUiDelegate::DefaultConfirmation)) { KIO::Job* job = KIO::emptyTrash(); job->uiDelegate()->setAutoErrorHandlingEnabled(true); } } void FolderModel::restoreSelectedFromTrash() { if (!m_selectionModel->hasSelection()) { return; } const auto &urls = selectedUrls(); KIO::RestoreJob *job = KIO::restoreFromTrash(urls); job->uiDelegate()->setAutoErrorHandlingEnabled(true); } void FolderModel::undoTextChanged(const QString &text) { if (QAction *action = m_actionCollection.action(QStringLiteral("undo"))) { action->setText(text); } }