/* SPDX-FileCopyrightText: 2011-2013 Sebastian Kügler SPDX-FileCopyrightText: 2011-2019 Marco Martin SPDX-FileCopyrightText: 2014-2015 Eike Hein SPDX-License-Identifier: GPL-2.0-or-later */ import QtQuick 2.15 import QtQuick.Layouts 1.15 import org.kde.plasma.plasmoid 2.0 import org.kde.plasma.core as PlasmaCore import org.kde.ksvg 1.0 as KSvg import org.kde.kquickcontrolsaddons 2.0 as KQuickControlsAddons import org.kde.kirigami 2.20 as Kirigami import org.kde.private.desktopcontainment.folder 0.1 as Folder import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager import "code/FolderTools.js" as FolderTools ContainmentItem { id: root switchWidth: { switchSize(); } switchHeight: { switchSize(); } // Only exists because the default CompactRepresentation doesn't: // - open on drag // - allow defining a custom drop handler // TODO remove once it gains that feature (perhaps optionally?) compactRepresentation: (isFolder && !isContainment) ? compactRepresentation : null objectName: isFolder ? "folder" : "desktop" width: isPopup ? undefined : preferredWidth(false) // Initial size when adding to e.g. desktop. height: isPopup ? undefined : preferredHeight(false) // Initial size when adding to e.g. desktop. function switchSize() { // Support expanding into the full representation on very thick vertical panels. if (isPopup && Plasmoid.formFactor === PlasmaCore.Types.Vertical) { return Kirigami.Units.iconSizes.small * 8; } return 0; } LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft LayoutMirroring.childrenInherit: true property bool isFolder: (Plasmoid.pluginName === "org.kde.plasma.folder") property bool isContainment: Plasmoid.isContainment property bool isPopup: (Plasmoid.location !== PlasmaCore.Types.Floating) property bool useListViewMode: isPopup && Plasmoid.configuration.viewMode === 0 property Component appletAppearanceComponent property Item toolBox property int handleDelay: 800 property real haloOpacity: 0.5 property int iconSize: Kirigami.Units.iconSizes.small property int iconWidth: iconSize property int iconHeight: iconWidth readonly property int hoverActivateDelay: 750 // Magic number that matches Dolphin's auto-expand folders delay. readonly property Loader folderViewLayer: fullRepresentationItem.folderViewLayer readonly property ContainmentLayoutManager.AppletsLayout appletsLayout: fullRepresentationItem.appletsLayout // Plasmoid.title is set by a Binding {} in FolderViewLayer toolTipSubText: "" Plasmoid.icon: (!Plasmoid.configuration.useCustomIcon && folderViewLayer.ready) ? symbolicizeIconName(folderViewLayer.view.model.iconName) : Plasmoid.configuration.icon onIconHeightChanged: updateGridSize() // We want to do this here rather than in the model because we don't always want // symbolic icons everywhere, but we do know that we always want them in this // specific representation right here function symbolicizeIconName(iconName) { const symbolicSuffix = "-symbolic"; if (iconName.endsWith(symbolicSuffix)) { return iconName; } return iconName + symbolicSuffix; } function updateGridSize() { // onIconHeightChanged can be triggered before this component is complete and all the children are created if (!toolBoxSvg) { return; } appletsLayout.cellWidth = root.iconWidth + toolBoxSvg.elementSize("left").width + toolBoxSvg.elementSize("right").width; appletsLayout.cellHeight = root.iconHeight + toolBoxSvg.elementSize("top").height + toolBoxSvg.elementSize("bottom").height; appletsLayout.defaultItemWidth = appletsLayout.cellWidth * 6; appletsLayout.defaultItemHeight = appletsLayout.cellHeight * 6; } function addLauncher(desktopUrl) { if (!isFolder) { return; } folderViewLayer.view.linkHere(desktopUrl); } function preferredWidth(forMinimumSize: bool): real { if (isContainment || !folderViewLayer.ready) { return -1; } else if (useListViewMode) { return (forMinimumSize ? folderViewLayer.view.cellHeight * 4 : Kirigami.Units.iconSizes.small * 16); } return (folderViewLayer.view.cellWidth * (forMinimumSize ? 1 : 3)) + (Kirigami.Units.iconSizes.small * 2); } function preferredHeight(forMinimumSize: bool): real { let height; if (isContainment || !folderViewLayer.ready) { return -1; } else if (useListViewMode) { height = (folderViewLayer.view.cellHeight * (forMinimumSize ? 1 : 15)) + Kirigami.Units.smallSpacing; } else { height = (folderViewLayer.view.cellHeight * (forMinimumSize ? 1 : 2)) + Kirigami.Units.iconSizes.small; } if (Plasmoid.configuration.labelMode !== 0) { height += folderViewLayer.item.labelHeight; } return height; } function isDrag(fromX, fromY, toX, toY) { const length = Math.abs(fromX - toX) + Math.abs(fromY - toY); return length >= Qt.styleHints.startDragDistance; } onFocusChanged: { if (focus && isFolder) { folderViewLayer.item.forceActiveFocus(); } } onExternalData: (mimetype, data) => { Plasmoid.configuration.url = data } component ShortDropBehavior : Behavior { NumberAnimation { duration: Kirigami.Units.shortDuration easing.type: Easing.InOutQuad } } component LongDropBehavior : Behavior { NumberAnimation { duration: Kirigami.Units.longDuration easing.type: Easing.InOutQuad } } KSvg.FrameSvgItem { id: highlightItemSvg visible: false imagePath: isPopup ? "widgets/viewitem" : "" prefix: "hover" } KSvg.FrameSvgItem { id: listItemSvg visible: false imagePath: isPopup ? "widgets/viewitem" : "" prefix: "normal" } KSvg.Svg { id: toolBoxSvg imagePath: "widgets/toolbox" property int rightBorder: elementSize("right").width property int topBorder: elementSize("top").height property int bottomBorder: elementSize("bottom").height property int leftBorder: elementSize("left").width } // FIXME: the use and existence of this property is a workaround preloadFullRepresentation: true fullRepresentation: FolderViewDropArea { id: dropArea anchors { fill: parent leftMargin: (isContainment && root.availableScreenRect) ? root.availableScreenRect.x : 0 topMargin: (isContainment && root.availableScreenRect) ? root.availableScreenRect.y : 0 rightMargin: (isContainment && root.availableScreenRect && parent) ? (parent.width - root.availableScreenRect.x - root.availableScreenRect.width) : 0 bottomMargin: (isContainment && root.availableScreenRect && parent) ? (parent.height - root.availableScreenRect.y - root.availableScreenRect.height) : 0 } LongDropBehavior on anchors.topMargin { } LongDropBehavior on anchors.leftMargin { } LongDropBehavior on anchors.rightMargin { } LongDropBehavior on anchors.bottomMargin { } property alias folderViewLayer: folderViewLayer property alias appletsLayout: appletsLayout Layout.minimumWidth: preferredWidth(!isPopup) Layout.minimumHeight: preferredHeight(!isPopup) Layout.preferredWidth: preferredWidth(false) Layout.preferredHeight: preferredHeight(false) // Maximum size is intentionally unbounded preventStealing: true onDragEnter: event => { if (isContainment && Plasmoid.immutable && !(isFolder && FolderTools.isFileDrag(event))) { event.ignore(); } // Don't allow any drops while listing. if (isFolder && folderViewLayer.view.status === Folder.FolderModel.Listing) { event.ignore(); } // Firefox tabs are regular drags. Since all of our drop handling is asynchronous // we would accept this drop and have Firefox not spawn a new window. (Bug 337711) if (event.mimeData.formats.indexOf("application/x-moz-tabbrowser-tab") !== -1) { event.ignore(); } } onDragMove: event => { // TODO: We should reject drag moves onto file items that don't accept drops // (cf. QAbstractItemModel::flags() here, but DeclarativeDropArea currently // is currently incapable of rejecting drag events. // Trigger autoscroll. if (isFolder && FolderTools.isFileDrag(event)) { handleDragMove(folderViewLayer.view, mapToItem(folderViewLayer.view, event.x, event.y)); } else if (isContainment) { appletsLayout.showPlaceHolderAt( Qt.rect(event.x - appletsLayout.minimumItemWidth / 2, event.y - appletsLayout.minimumItemHeight / 2, appletsLayout.minimumItemWidth, appletsLayout.minimumItemHeight) ); } } onDragLeave: event => { // Cancel autoscroll. if (isFolder) { handleDragEnd(folderViewLayer.view); } if (isContainment) { appletsLayout.hidePlaceHolder(); } } onDrop: event => { if (isFolder && FolderTools.isFileDrag(event)) { handleDragEnd(folderViewLayer.view); folderViewLayer.view.drop(root, event, mapToItem(folderViewLayer.view, event.x, event.y)); } else if (isContainment) { root.processMimeData(event.mimeData, event.x - appletsLayout.placeHolder.width / 2, event.y - appletsLayout.placeHolder.height / 2); event.accept(event.proposedAction); appletsLayout.hidePlaceHolder(); } } Component { id: compactRepresentation CompactRepresentation { folderView: folderViewLayer.view } } Connections { target: Plasmoid.containment.corona ignoreUnknownSignals: true function onEditModeChanged() { appletsLayout.editMode = Plasmoid.containment.corona.editMode; } } ContainmentLayoutManager.AppletsLayout { id: appletsLayout anchors.fill: parent relayoutLock: width !== root.availableScreenRect.width || height !== root.availableScreenRect.height // NOTE: use root.availableScreenRect and not own width and height as they are updated not atomically configKey: "ItemGeometries-" + Math.round(root.screenGeometry.width) + "x" + Math.round(root.screenGeometry.height) fallbackConfigKey: root.availableScreenRect.width > root.availableScreenRect.height ? "ItemGeometriesHorizontal" : "ItemGeometriesVertical" containment: Plasmoid containmentItem: root editModeCondition: Plasmoid.immutable ? ContainmentLayoutManager.AppletsLayout.Locked : ContainmentLayoutManager.AppletsLayout.AfterPressAndHold // Sets the containment in edit mode when we go in edit mode as well onEditModeChanged: Plasmoid.containment.corona.editMode = editMode; minimumItemWidth: Kirigami.Units.iconSizes.small * 3 minimumItemHeight: minimumItemWidth cellWidth: Kirigami.Units.iconSizes.small cellHeight: cellWidth eventManagerToFilter: folderViewLayer.item?.view.view ?? null appletContainerComponent: ContainmentLayoutManager.BasicAppletContainer { id: appletContainer editModeCondition: Plasmoid.immutable ? ContainmentLayoutManager.ItemContainer.Locked : ContainmentLayoutManager.ItemContainer.AfterPressAndHold configOverlaySource: "ConfigOverlay.qml" onUserDrag: (newPosition, dragCenter) => { const pos = mapToItem(root.parent, dragCenter.x, dragCenter.y); const newCont = root.containmentItemAt(pos.x, pos.y); if (newCont && newCont.plasmoid !== Plasmoid) { const newPos = newCont.mapFromApplet(Plasmoid, pos.x, pos.y); // First go out of applet edit mode, get rid of the config overlay, release mouse grabs in preparation of applet reparenting cancelEdit(); newCont.Plasmoid.addApplet(appletContainer.applet.plasmoid, Qt.rect(newPos.x, newPos.y, appletContainer.applet.width, appletContainer.applet.height)); appletsLayout.hidePlaceHolder(); } } ShortDropBehavior on x { } ShortDropBehavior on y { } } placeHolder: ContainmentLayoutManager.PlaceHolder {} Loader { id: folderViewLayer anchors.fill: parent property bool ready: status === Loader.Ready property Item view: item?.view ?? null property QtObject model: item?.model ?? null focus: true active: isFolder asynchronous: false source: "FolderViewLayer.qml" onFocusChanged: { if (!focus && model) { model.clearSelection(); } } Connections { target: folderViewLayer.view // `FolderViewDropArea` is not a FocusScope. We need to forward manually. function onPressed() { folderViewLayer.forceActiveFocus(); } } } } PlasmaCore.Action { id: configAction text: i18n("Desktop and Wallpaper") icon.name: "preferences-desktop-wallpaper" shortcut: "alt+d,alt+s" onTriggered: Plasmoid.containment.configureRequested(Plasmoid) } Component.onCompleted: { if (!Plasmoid.isContainment) { return; } Plasmoid.setInternalAction("configure", configAction) updateGridSize(); } } }