Warning, /utilities/filelight/src/qml/MapPage.qml is written in an unsupported language. File is not indexed.

0001 // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0002 // SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
0003 
0004 import QtQuick 2.15
0005 import QtQuick.Layouts 1.15
0006 import QtQuick.Controls 2.15 as QQC2
0007 import org.kde.kirigami 2.19 as Kirigami
0008 import org.kde.kirigami.delegates as KD
0009 import org.kde.coreaddons 1.0 as KCoreAddons
0010 import QtQuick.Shapes 1.15
0011 
0012 import org.kde.filelight 1.0
0013 
0014 Kirigami.Page {
0015     id: page
0016 
0017     property url url: RadialMap.rootUrl
0018     property QQC2.Menu contextMenu
0019 
0020     enabled: !ContextMenuContext.deleting
0021     topPadding: 0
0022     leftPadding: 0
0023     rightPadding: 0
0024     bottomPadding: 0
0025 
0026     Kirigami.Action {
0027         id: goToOverviewAction
0028         enabled: page.state === ""
0029         icon.name: "go-home"
0030         text: i18nc("@action", "Go to Overview")
0031         onTriggered: pageStack.currentIndex = 0
0032     }
0033 
0034     Kirigami.Action {
0035         id: goUpAction
0036         enabled: page.state === ""
0037         icon.name: "go-up"
0038         text: i18nc("@action", "Up")
0039         onTriggered: appWindow.slotUp()
0040         shortcut: "Alt+Up"
0041     }
0042 
0043     Kirigami.Action {
0044         id: rescanAction
0045         enabled: page.state !== "scanning"
0046         icon.name: "view-refresh"
0047         text: i18nc("@action", "Rescan")
0048         onTriggered: appWindow.rescan()
0049         shortcut: StandardKey.Refresh
0050     }
0051 
0052     Kirigami.Action {
0053         id: stopAction
0054         enabled: page.state === "scanning"
0055         icon.name: "process-stop"
0056         text: i18nc("@action", "Stop")
0057         onTriggered: appWindow.closeURL()
0058     }
0059 
0060     Kirigami.Action {
0061         id: zoomInAction
0062         enabled: page.state === ""
0063         icon.name: "zoom-in"
0064         text: i18nc("@action", "Zoom In")
0065         displayHint: Kirigami.DisplayHint.AlwaysHide
0066         onTriggered: RadialMap.zoomIn()
0067         shortcut: StandardKey.ZoomIn
0068     }
0069 
0070     Kirigami.Action {
0071         id: zoomOutAction
0072         enabled: page.state === ""
0073         icon.name: "zoom-out"
0074         text: i18nc("@action", "Zoom Out")
0075         displayHint: Kirigami.DisplayHint.AlwaysHide
0076         onTriggered: RadialMap.zoomOut()
0077         shortcut: StandardKey.ZoomOut
0078     }
0079 
0080     actions: MainContext.historyActions.concat([
0081         goUpAction,
0082         goToOverviewAction,
0083         rescanAction,
0084         stopAction,
0085         zoomInAction,
0086         zoomOutAction,
0087         configureAction,
0088         helpAction,
0089         aboutAction
0090     ])
0091 
0092     Component {
0093         id: contextMenuComponent
0094         QQC2.Menu {
0095             id: contextMenu
0096 
0097             required property Segment segment
0098 
0099             title: segment.displayName()
0100 
0101             onAboutToShow: page.contextMenu = this
0102             onAboutToHide: page.contextMenu = null
0103 
0104             QQC2.MenuItem {
0105                 action: Kirigami.Action {
0106                     icon.name: "document-open"
0107                     text: i18nc("@action Open file or directory from context menu", "Open")
0108                 }
0109                 onTriggered: Qt.openUrlExternally(contextMenu.segment.url())
0110             }
0111             QQC2.MenuItem {
0112                 visible: contextMenu.segment.isFolder() && contextMenu.segment.url().toString().startsWith("file:")
0113                 action: Kirigami.Action {
0114                     icon.name: "utilities-terminal"
0115                     text: i18nc("@action", "Open Terminal Here")
0116                     onTriggered: ContextMenuContext.openTerminal(contextMenu.segment.url())
0117                 }
0118             }
0119             QQC2.MenuItem {
0120                 visible: contextMenu.segment.isFolder()
0121                 action: Kirigami.Action {
0122                     icon.name: "zoom-in"
0123                     text: i18nc("@action focuses the filelight view on a given map segment", "Center Map Here")
0124                     onTriggered: {
0125                         MainContext.updateURL(contextMenu.segment.url())
0126                         MainContext.openUrl(contextMenu.segment.url())
0127                     }
0128                 }
0129             }
0130             QQC2.MenuItem {
0131                 visible: contextMenu.segment.isFolder()
0132                 action: Kirigami.Action {
0133                     icon.name: "list-remove"
0134                     text: i18nc("@action", "Add to Do Not Scan List")
0135                     onTriggered: ContextMenuContext.doNotScan(contextMenu.segment.url())
0136                 }
0137             }
0138             QQC2.MenuItem {
0139                 visible: contextMenu.segment.isFolder()
0140                 action: Kirigami.Action {
0141                     icon.name: "view-refresh"
0142                     text: i18nc("@action rescan filelight map", "Rescan")
0143                     onTriggered: MainContext.rescanSingleDir(contextMenu.segment.url())
0144                 }
0145             }
0146             QQC2.MenuItem {
0147                 action: Kirigami.Action {
0148                     icon.name: "edit-copy"
0149                     text: i18nc("@action", "Copy to clipboard")
0150                     onTriggered: ContextMenuContext.copyClipboard(contextMenu.segment.displayPath())
0151                 }
0152             }
0153             QQC2.MenuSeparator {}
0154             QQC2.MenuItem {
0155                 action: Kirigami.Action {
0156                     icon.name: "edit-delete"
0157                     text: i18nc("@action delete file or folder", "Delete")
0158                     onTriggered: ContextMenuContext.deleteFileFromSegment(contextMenu.segment)
0159                 }
0160             }
0161         }
0162     }
0163 
0164     property real mouseyX: -1
0165     property real mouseyY: -1
0166     property var hoveredSegment: undefined
0167     property bool hoveringListItem: false
0168 
0169     RowLayout {
0170         anchors.fill: parent
0171         spacing: 0
0172         visible: page.state === ""
0173 
0174         QQC2.ScrollView {
0175             implicitWidth: Kirigami.Units.gridUnit * 10
0176             Layout.maximumWidth: Kirigami.Units.gridUnit * 22
0177             Layout.fillWidth: true
0178             Layout.fillHeight: true
0179             Kirigami.Theme.colorSet: Kirigami.Theme.View
0180             Kirigami.Theme.inherit: false
0181 
0182             // flush against both the toolbar and the window edge. without this we get a framed rectangle
0183             background: Rectangle {
0184                      color: Kirigami.Theme.backgroundColor
0185                 }
0186             Component.onCompleted: background.visible = true
0187 
0188             ListView {
0189                 id: listview
0190                 clip: true
0191                 model: visible ? FileModel : undefined
0192                 Component.onCompleted: currentIndex = -1
0193                 reuseItems: true
0194                 focus: true
0195                 activeFocusOnTab: true
0196                 keyNavigationEnabled: true
0197                 keyNavigationWraps: true
0198                 delegate: KD.SubtitleDelegate {
0199                     width: listview.width
0200                     icon.name: ROLE_IsFolder ? "folder" : "file" // TODO mimetype?
0201                     text: model.display
0202                     subtitle: ROLE_HumanReadableSize
0203                     hoverEnabled: true
0204                     highlighted: {
0205                         if (hoveringListItem) {
0206                             return hovered
0207                         }
0208                         return (hoveredSegment === ROLE_Segment) || (hoveredSegment === "fake" && ROLE_Segment === "")
0209                     }
0210                     onHoveredChanged: {
0211                         if (hovered) {
0212                             hoveringListItem = true
0213                             hoveredSegment = ROLE_Segment !== "" ? ROLE_Segment : "fake"
0214                         }
0215                     }
0216                     onClicked: {
0217                         if (ROLE_IsFolder) {
0218                             MainContext.updateURL(ROLE_URL)
0219                             MainContext.openUrl(ROLE_URL)
0220                         }
0221                     }
0222 
0223                     QQC2.Menu {
0224                         id: contextMenu
0225 
0226                         QQC2.MenuItem {
0227                             action: Kirigami.Action {
0228                                 icon.name: "document-open"
0229                                 text: i18nc("@action Open file or directory from context menu", "Open")
0230                             }
0231                             onTriggered: Qt.openUrlExternally(ROLE_URL)
0232                         }
0233                         QQC2.MenuItem {
0234                             visible: ROLE_IsFolder && ROLE_URL.toString().startsWith("file:")
0235                             action: Kirigami.Action {
0236                                 icon.name: "utilities-terminal"
0237                                 text: i18nc("@action", "Open Terminal Here")
0238                                 onTriggered: ContextMenuContext.openTerminal(ROLE_URL)
0239                             }
0240                         }
0241                         QQC2.MenuItem {
0242                             visible: ROLE_IsFolder
0243                             action: Kirigami.Action {
0244                                 icon.name: "list-remove"
0245                                 text: i18nc("@action", "Add to Do Not Scan List")
0246                                 onTriggered: ContextMenuContext.doNotScan(ROLE_URL)
0247                             }
0248                         }
0249                         QQC2.MenuItem {
0250                             action: Kirigami.Action {
0251                                 icon.name: "edit-copy"
0252                                 text: i18nc("@action", "Copy to clipboard")
0253                                 onTriggered: ContextMenuContext.copyClipboard(ROLE_URL)
0254                             }
0255                         }
0256                         QQC2.MenuSeparator {}
0257                         QQC2.MenuItem {
0258                             action: Kirigami.Action {
0259                                 icon.name: "edit-delete"
0260                                 text: i18nc("@action delete file or folder", "Delete")
0261                                 onTriggered: ContextMenuContext.deleteFile(FileModel.file(index))
0262                             }
0263                         }
0264                     }
0265 
0266                     TapHandler {
0267                         acceptedButtons: Qt.RightButton
0268                         onTapped: contextMenu.popup()
0269                     }
0270                 }
0271             }
0272         }
0273 
0274         Kirigami.Separator {
0275             Layout.fillHeight: true
0276         }
0277 
0278         Item {
0279             id: shapeItem
0280             Layout.fillWidth: true
0281             Layout.fillHeight: true
0282             Layout.minimumHeight: Kirigami.Units.gridUnit * 10
0283             Layout.minimumWidth: Kirigami.Units.gridUnit * 10
0284             Layout.margins: Kirigami.Units.gridUnit
0285             antialiasing: true
0286             layer.enabled: true
0287             layer.samples: 32
0288             layer.smooth: true
0289 
0290             property var zOrderedShapes: []
0291             property bool hasShapes: zOrderedShapes.length > 0
0292 
0293             function objectToArray(object) {
0294                 let newArray = [];
0295                 for (let i in object) {
0296                     newArray.push(object[i]);
0297                 }
0298                 return newArray;
0299             }
0300 
0301             onChildrenChanged: {
0302                 let children = objectToArray(shapeItem.children)
0303                 children = children.filter(child => child instanceof Shape);
0304                 children.sort((a, b) => b.z - a.z); // sort by level so the higher level object (the visible one) outscores ones it paints over
0305                 zOrderedShapes = children
0306             }
0307 
0308             function tooltip({ path, size, isFolder, files, totalFiles, isRoot = false }) {
0309                 let tips = [path, size]
0310                 if (isFolder) {
0311                     const percent = Math.floor((100 * files) / totalFiles)
0312                     if (percent > 0) {
0313                         tips.push(i18ncp("Tooltip of folder, %1 is number of files", "%1 File (%2%)", "%1 Files (%2%)", files, percent))
0314                     } else {
0315                         tips.push(i18ncp("Tooltip of folder, %1 is number of files", "%1 File", "%1 Files", files))
0316                     }
0317                 }
0318                 if (isRoot) {
0319                     tips.push(i18nc("part of tooltip indicating that the item under the mouse is clickable", "Click to go up to parent directory"))
0320                 }
0321                 return tips.join("\n")
0322             }
0323 
0324             Instantiator {
0325                 id: instantiator
0326                 model: shapeItem.visible ? RadialMap.signature : undefined
0327                 active: true
0328                 asynchronous: true // arguable
0329 
0330                 Instantiator {
0331                     // FIXME weird
0332                     Component.onCompleted: idx = index // lock index by breaking the binding
0333                     property int idx: index
0334                     readonly property real signatureRadius: {
0335                         if (shapeItem.width > shapeItem.height) {
0336                             return shapeItem.height / 2 / (instantiator.model.length + 1)
0337                         }
0338                         return shapeItem.width / 2 / (instantiator.model.length + 1)
0339                     }
0340                     readonly property real shapeRadius: signatureRadius * (idx + 2)
0341 
0342                     active: true
0343                     model: modelData
0344                     onObjectAdded: shapeItem.children.push(object)
0345 
0346                     delegate: SegmentShape {
0347                         // Qt doc: Note: model, index, and modelData roles are not accessible if the delegate contains required properties, unless it has also required properties with matching names.
0348                         required property var modelData
0349 
0350                         readonly property bool segmentHover: hoveredSegment === segment.uuid || (segment.fake && hoveredSegment === "fake")
0351 
0352                         z: (instantiator.model.length - idx) // reverse order such that more central levels are above. this gives us segment appearance without having to actually paint segments (instead we stack full cicles)
0353                         segment: modelData
0354                         item: shapeItem
0355                         radius: shapeRadius
0356                         startAngle: -(modelData.start() / 16)
0357                         sweepAngle: -(modelData.length() / 16)
0358                         tooltipText: shapeItem.tooltip({
0359                             isFolder: segment.isFolder(),
0360                             path: segment.displayPath(),
0361                             size: segment.humanReadableSize(),
0362                             files: segment.files(),
0363                             totalFiles: RadialMap.numberOfChildren,
0364                         })
0365                         showTooltip: !contextMenu && !hoveringListItem && segmentHover && shapeItem.visible
0366                         fillColor: segmentHover ? Qt.darker(segment.color) : segment.color
0367                     }
0368                 }
0369             }
0370 
0371             CenterShape {
0372                 id: centerShape
0373                 z: 500 // on top of everything, arbitrary high number of shapes
0374                 visible: shapeItem.hasShapes
0375                 segment: RadialMap.rootSegment
0376                 segmentUuid: "root"
0377                 item: shapeItem
0378                 radius: {
0379                     if (instantiator.model === undefined) {
0380                         return 0
0381                     }
0382                     if (shapeItem.width > shapeItem.height) {
0383                         return shapeItem.height / 2 / (instantiator.model.length + 1)
0384                     }
0385                     return shapeItem.width / 2 / (instantiator.model.length + 1)
0386                 }
0387                 startAngle: 0
0388                 sweepAngle: 360
0389                 tooltipText: shapeItem.tooltip({
0390                     isFolder: true,
0391                     path: RadialMap.displayPath,
0392                     size: RadialMap.overallSize,
0393                     files: RadialMap.numberOfChildren,
0394                     totalFiles: RadialMap.numberOfChildren,
0395                     isRoot: true,
0396                 })
0397                 showTooltip: !contextMenu && !hoveringListItem && hoveredSegment === segmentUuid && shapeItem.visible
0398                 Kirigami.Theme.colorSet: Kirigami.Theme.View
0399                 fillColor: Kirigami.Theme.backgroundColor
0400             }
0401 
0402             QQC2.Label {
0403                 id: centerLabel
0404                 z: 501
0405                 visible: centerShape.visible
0406                 text: RadialMap.overallSize
0407                 Kirigami.Theme.colorSet: Kirigami.Theme.View
0408                 color: Kirigami.Theme.textColor
0409                 horizontalAlignment: Text.AlignHCenter
0410                 // The diagonal of the circle is the hypotenuse of the largest square
0411                 readonly property var dimension: (2  * centerShape.radius) / Math.sqrt(2)
0412                 width: dimension
0413                 height: dimension
0414                 anchors.centerIn: parent
0415 
0416                 // Let the text scale way down but lock the maximum at the actual default font height.
0417                 // This ensures that the text neatly fits into our dimensions.
0418                 fontSizeMode: Text.Fit
0419                 minimumPixelSize: 2
0420                 Component.onCompleted: font.pixelSize = font.pixelSize
0421             }
0422         }
0423     }
0424 
0425     MouseArea {
0426         x: shapeItem.x
0427         y: shapeItem.y
0428         z: 502
0429         width: shapeItem.width
0430         height: shapeItem.height
0431         hoverEnabled: true
0432         acceptedButtons: Qt.LeftButton | Qt.RightButton
0433 
0434         function findTarget(mouse) {
0435             let children = shapeItem.zOrderedShapes
0436             for (var i in children) {
0437                 const child = children[i]
0438                 if (child === centerLabel) {
0439                     continue // not part of the shape objects
0440                 }
0441                 const contains = child.contains(Qt.point(mouse.x, mouse.y))
0442                 if (contains) {
0443                     return child
0444                 }
0445             }
0446             return null
0447         }
0448 
0449         onPositionChanged: mouse => {
0450             mouse.accepted = false
0451             const child = findTarget(mouse)
0452             if (child !== null) {
0453                 hoveringListItem = false
0454                 mouseyX = mouse.x
0455                 mouseyY = mouse.y
0456                 if (child.segmentUuid === undefined) {
0457                     hoveredSegment = "root"
0458                 } else {
0459                     hoveredSegment = child.segmentUuid
0460                 }
0461             } else {
0462                 hoveredSegment = undefined
0463             }
0464         }
0465 
0466         onClicked: mouse => {
0467             const child = findTarget(mouse)
0468             if (child === null) {
0469                 return
0470             }
0471             if (mouse.button === Qt.LeftButton) {
0472                 if (child.segmentUuid === "root") {
0473                     appWindow.slotUp()
0474                     return
0475                 }
0476                 if (!child.segment.isFolder()) {
0477                     Qt.openUrlExternally(child.url)
0478                     return
0479                 }
0480                 MainContext.updateURL(child.url)
0481                 MainContext.openUrl(child.url)
0482             } else if (mouse.button === Qt.RightButton) {
0483                 console.log("click %1".arg(child))
0484                 contextMenuComponent.createObject(child, {segment: child.segment}).popup()
0485             }
0486         }
0487         onPressAndHold: {
0488             if (mouse.source === Qt.MouseEventNotSynthesized) {
0489                 const child = findTarget(mouse)
0490                 contextMenu.segment = child.segment
0491                 contextMenu.popup()
0492             }
0493         }
0494     }
0495 
0496     DropperItem {
0497         x: shapeItem.x
0498         y: shapeItem.y
0499         z: 503
0500         width: shapeItem.width
0501         height: shapeItem.height
0502         onUrlsDropped: urls => {
0503             const url = urls[0]
0504             appWindow.updateURL(url)
0505             appWindow.openURL(url)
0506         }
0507     }
0508 
0509     Kirigami.LoadingPlaceholder {
0510         id: scanPlaceholder
0511         visible: page.state === "scanning"
0512         anchors.centerIn: parent
0513 
0514         Timer {
0515             interval: 16 // = 60 fps because supposedly Qt hardcodes it all over the place (this claim is very old and it's unclear if still true)
0516             // Polish doesn't lend itself to the advanced status text https://bugs.kde.org/show_bug.cgi?id=468395
0517             running: parent.visible && Qt.uiLanguage != 'pl'
0518             repeat: true
0519             onTriggered: {
0520                 const files = ScanManager.files();
0521                 const size = ScanManager.totalSize()
0522                 scanPlaceholder.text = i18ncp("Scanned number of files and size so far", "%1 File, %2", "%1 Files, %2", String(files), KCoreAddons.Format.formatByteSize(size));
0523             }
0524         }
0525     }
0526 
0527     Kirigami.PlaceholderMessage {
0528         visible: page.state === "noData"
0529         anchors.centerIn: parent
0530         width: parent.width - (Kirigami.Units.largeSpacing * 4)
0531         text: i18n("No data available")
0532     }
0533 
0534     Connections {
0535         target: ScanManager
0536         function onAboutToEmptyCache() {
0537             RadialMap.invalidate()
0538         }
0539     }
0540 
0541     Connections {
0542         target: MainContext
0543         function onCanvasIsDirty(filth) {
0544             RadialMap.refresh(filth)
0545         }
0546     }
0547 
0548     Component.onCompleted: {
0549         appWindow.mapPage = this
0550     }
0551 
0552     states: [
0553         State {
0554             name: "scanning"
0555             when: ScanManager.running
0556         },
0557         State {
0558             name: "noData"
0559             // FIXME this toggles too soon causing incomplete shapes to show wtf - but only when instaniatior is no async
0560             when: !RadialMap.valid && shapeItem.hasShapes
0561         },
0562         State {
0563             name: "" // default state
0564         }
0565     ]
0566 }