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 }