/*SPDX-FileCopyrightText: 2013 Marco Martin <mart@kde.org>SPDX-FileCopyrightText: 2022 NiccolΓ² Venerandi <niccolo@venerandi.com>SPDX-License-Identifier: GPL-2.0-or-later*/pragma ComponentBehavior: Boundimport QtQuickimport QtQuick.Layoutsimport org.kde.plasma.plasmoidimport org.kde.plasma.core as PlasmaCoreimport org.kde.ksvg as KSvgimport org.kde.plasma.components as PC3import org.kde.draganddrop as DragDropimport org.kde.kirigami as Kirigamiimport "LayoutManager.js" as LayoutManagerContainmentItem {id: rootwidth: 640height: 48//BEGIN propertiesLayout.preferredWidth: Math.max(addWidgetsButton.implicitWidth * addWidgetsButton.visible, fixedWidth || currentLayout.implicitWidth + currentLayout.horizontalDisplacement)Layout.preferredHeight: fixedHeight || currentLayout.implicitHeight + currentLayout.verticalDisplacementLayout.fillWidth: {return currentLayout.children.filter(child => child?.applet?.plasmoid?.pluginName === "org.kde.plasma.panelspacer").some(child => child.applet.plasmoid.configuration.expanding)}Layout.fillHeight: Layout.fillWidthproperty Item toolBoxproperty var layoutManager: LayoutManagerproperty ConfigOverlay configOverlayproperty bool isHorizontal: Plasmoid.formFactor !== PlasmaCore.Types.Verticalproperty int fixedWidth: 0property int fixedHeight: 0property bool hasSpacer// True when e.g. the task manager is drag and dropping tasks.property bool appletRequestsInhibitDnD: falseproperty bool reverse: Application.layoutDirection === Qt.RightToLeft//END properties//BEGIN functionsfunction checkLastSpacer() {for (var i = 0; i < appletsModel.count; ++i) {const applet = appletsModel.get(i).applet;if (!applet || !applet.visible || !applet.Layout || applet.isPlaceholder) {continue;}if ((isHorizontal && applet.Layout.fillWidth) ||(!isHorizontal && applet.Layout.fillHeight)) {hasSpacer = true;return;}}hasSpacer = false;}function plasmoidLocationString(): string {switch (Plasmoid.location) {case PlasmaCore.Types.LeftEdge:return "west";case PlasmaCore.Types.TopEdge:return "north";case PlasmaCore.Types.RightEdge:return "east";case PlasmaCore.Types.BottomEdge:return "south";}return "";}// This function is called externally by the Plasma scripting language to setup// the panel layoutfunction reorderApplets(order) {LayoutManager.reorderApplets(order);}//END functions//BEGIN connectionsContainment.onAppletAdded: (applet, geometry) => {let pos = Qt.point(geometry.x, geometry.y)if ("positionBeforeDeletion" in applet) {pos = applet.positionBeforeDeletiondelete applet.positionBeforeDeletion}LayoutManager.addApplet(applet, pos.x, pos.y);root.checkLastSpacer();// When a new preset panel is added, avoid calling save() multiple timesQt.callLater(LayoutManager.save);}Containment.onAppletRemoved: (applet) => {let plasmoidItem = root.itemFor(applet);if (plasmoidItem) {appletsModel.remove(plasmoidItem.parent.index);applet["positionBeforeDeletion"] = Qt.point(plasmoidItem.parent.x, plasmoidItem.parent.y)}checkLastSpacer();LayoutManager.save();}Plasmoid.onUserConfiguringChanged: {if (!Plasmoid.userConfiguring) {if (root.configOverlay) {if (root.configOverlay.dragAndDropping) {root.configOverlay.finishDragOperation()}root.configOverlay.destroy();root.configOverlay = null;}return;}if (Plasmoid.immutable) {return;}Containment.applets.forEach(applet => applet.expanded = false);const component = Qt.createComponent("ConfigOverlay.qml");configOverlay = component.createObject(this, {"anchors.fill": dropArea,"anchors.rightMargin": Qt.binding(() => isHorizontal ? toolBox.height : 0),"anchors.bottomMargin": Qt.binding(() => !isHorizontal ? toolBox.height : 0),"layoutManager": Qt.binding(() => root.layoutManager),"appletsModel": appletsModel,"currentLayout": currentLayout,"appletContainerComponent": appletContainerComponent,"dropArea": dropArea,"isHorizontal": Qt.binding(() => root.isHorizontal),"rootLength": Qt.binding(() => root.isHorizontal ? root.width : root.height),"reverse": Qt.binding(() => root.reverse),"lastSpacer": lastSpacer,"addWidgetsButton": addWidgetsButton});component.destroy();}//END connectionsTapHandler {acceptedButtons: Qt.LeftButtonacceptedDevices: PointerDevice.TouchScreen | PointerDevice.StylusonLongPressed: Plasmoid.internalAction("configure").trigger()}DragDrop.DropArea {id: dropAreaanchors.fill: parent// These are invisible and only used to read panel margins// Both will fallback to "standard" panel margins if the theme does not// define a normal or a thick margin.KSvg.FrameSvgItem {id: panelSvgvisible: falseimagePath: "widgets/panel-background"prefix: [root.plasmoidLocationString(), ""]}KSvg.FrameSvgItem {id: thickPanelSvgvisible: falseprefix: ['thick'].concat(panelSvg.prefix)imagePath: "widgets/panel-background"}property bool marginAreasEnabled: panelSvg.margins != thickPanelSvg.marginsproperty var marginHighlightSvg: KSvg.Svg{imagePath: "widgets/margins-highlight"}//Margins are either the size of the margins in the SVG, unless that prevents the panel from being at least half a smallMedium icon) tall at which point we set the margin to whatever allows it to be that...or if it still won't fit, 1.//the size a margin should be to force a panel to be the required size abovereadonly property real spacingAtMinSize: Math.floor(Math.max(1, (root.isHorizontal ? root.height : root.width) - Kirigami.Units.iconSizes.smallMedium)/2)Component.onCompleted: {LayoutManager.plasmoid = root.Plasmoid;LayoutManager.root = root;LayoutManager.layout = currentLayout;LayoutManager.marginHighlights = [];LayoutManager.appletsModel = appletsModel;LayoutManager.restore();root.Plasmoid.internalAction("configure").visible = Qt.binding(function() {return !root.Plasmoid.immutable;});root.Plasmoid.internalAction("configure").enabled = Qt.binding(function() {return !root.Plasmoid.immutable;});}onDragEnter: event => {if (Plasmoid.immutable || root.appletRequestsInhibitDnD) {event.ignore();return;}//during drag operations we disable panel auto resizeroot.fixedWidth = root.Layout.preferredWidthroot.fixedHeight = root.Layout.preferredHeightappletsModel.insert(LayoutManager.indexAtCoordinates(event.x, event.y), {applet: dndSpacer})}onDragMove: event => {LayoutManager.move(dndSpacer.parent, LayoutManager.indexAtCoordinates(event.x, event.y));}onDragLeave: event => {/** When reordering task items, dragLeave signal will be emitted directly* without dragEnter, and in this case parent.index is undefined, so also* check if dndSpacer is in appletsModel.*/const spacerContainer = dndSpacer.parent as AppletContainerif (typeof(spacerContainer?.index) === "number" && spacerContainer.index > -1) {appletsModel.remove(spacerContainer.index);root.fixedWidth = root.fixedHeight = 0;}}onDrop: event => {appletsModel.remove((dndSpacer.parent as AppletContainer).index);root.processMimeData(event.mimeData, event.x, event.y);event.accept(event.proposedAction);root.fixedWidth = root.fixedHeight = 0;}//BEGIN componentsComponent {id: appletContainerComponent// This loader conditionally manages the BusyIndicator, it's not// loading the applet. The applet becomes a regular child item.AppletContainer {id: containerfunction getMargins(side, returnAllMargins = false, overrideFillArea = null, overrideThickArea = null): real {if (!applet || !applet.Plasmoid) {return 0;}//Margins are either the size of the margins in the SVG, unless that prevents the panel from being at least half a smallMedium icon + smallSpace) tall at which point we set the margin to whatever allows it to be that...or if it still won't fit, 1.let fillArea = overrideFillArea === null ? applet && (applet.Plasmoid.constraintHints & Plasmoid.CanFillArea) : overrideFillArealet inThickArea = overrideThickArea === null ? container.inThickArea : overrideThickAreavar layout = {top: root.isHorizontal, bottom: root.isHorizontal,right: !root.isHorizontal, left: !root.isHorizontal};return ((layout[side] || returnAllMargins) && !fillArea) ? Math.round(Math.min(dropArea.spacingAtMinSize, (inThickArea ? thickPanelSvg.fixedMargins[side] : panelSvg.fixedMargins[side]))) : 0;}Layout.topMargin: getMargins('top')Layout.bottomMargin: getMargins('bottom')Layout.leftMargin: getMargins('left')Layout.rightMargin: getMargins('right')// Always fill width/height, in order to still shrink the applet when there is not enough space.// When the applet doesn't want to expand set a Layout.maximumWidth accordingly// https://bugs.kde.org/show_bug.cgi?id=473420Layout.fillWidth: trueLayout.fillHeight: trueonWantsToFillWidthChanged: root.checkLastSpacer()onWantsToFillHeightChanged: root.checkLastSpacer()availWidth: root.width - Layout.leftMargin - Layout.rightMarginavailHeight: root.height - Layout.topMargin - Layout.bottomMarginfunction findPositive(first, second) {return first > 0 ? first : second}// BEGIN BUG 454095: do not combine these expressions to a function or the bindings won't workLayout.minimumWidth: root.isHorizontal ? findPositive(applet?.Layout.minimumWidth, availHeight) : availWidthLayout.minimumHeight: !root.isHorizontal ? findPositive(applet?.Layout.minimumHeight, availWidth) : availHeightLayout.preferredWidth: root.isHorizontal ? findPositive(applet?.Layout.preferredWidth, Layout.minimumWidth) : availWidthLayout.preferredHeight: !root.isHorizontal ? findPositive(applet?.Layout.preferredHeight, Layout.minimumHeight) : availHeightLayout.maximumWidth: root.isHorizontal ? (wantsToFillWidth ? findPositive(applet?.Layout.maximumWidth, root.width) : Math.min(applet?.Layout.maximumWidth, Layout.preferredWidth)) : availWidthLayout.maximumHeight: !root.isHorizontal ? (wantsToFillHeight ? findPositive(applet?.Layout.maximumHeight, root.height) : Math.min(applet?.Layout.maximumHeight, Layout.preferredHeight)) : availHeight// END BUG 454095Item {id: marginHighlightElementsanchors.fill: parent// index -1 is for floating applets, which do not need a margin highlightopacity: Plasmoid.containment.corona.editMode && dropArea.marginAreasEnabled && !(root.configOverlay?.dragAndDropping ?? false) && container.index != -1 ? 1 : 0Behavior on opacity {NumberAnimation {duration: Kirigami.Units.longDurationeasing.type: Easing.InOutQuad}}component SideMargin: KSvg.SvgItem {property string side; property bool fill: trueproperty int inset; property int paddingproperty var west: ({'left': 'top', 'top': 'left', 'right': 'top', 'bottom': 'left'})property var mirror: ({'left': 'right', 'top': 'bottom', 'right': 'left', 'bottom': 'top'})property var onComponentCompleted: {let left = west[side]let right = mirror[left]let up = mirror[side]anchors[up] = undefinedif (root.isHorizontal) {height = padding;} else {width = padding;}anchors[left+'Margin'] = - currentLayout.rowSpacing/2 - (container.appletIndex == 0 ? dropArea.anchors[left + 'Margin'] + currentLayout.x : 0)anchors[right+'Margin'] = - currentLayout.rowSpacing/2 - (container.appletIndex == appletsModel.count-1 ? dropArea.anchors[right + 'Margin'] + currentLayout.toolBoxSize : 0)anchors[side+'Margin'] = - inset}elementId: fill ? 'fill' : (root.isHorizontal ? side + (container.inThickArea ? 'left' : 'right') : (container.inThickArea ? 'top' : 'bottom') + side)svg: dropArea.marginHighlightSvganchors {top: parent?.top; left: parent?.left; right: parent?.right; bottom: parent?.bottom}}Repeater {model: ['top', 'bottom', 'right', 'left']SideMargin {required property string modelDataside: modelDatainset: container.getMargins(side)visible: (modelData === 'top' || modelData === 'bottom') === root.isHorizontalpadding: container.getMargins(side, false, false, container.isMarginSeparator ? false : container.inThickArea)}}Repeater {model: ['top', 'bottom', 'right', 'left']SideMargin {required property string modelDataside: modelDatainset: -container.getMargins(side, false, false, false)padding: container.getMargins(side, false, false, true) + insetvisible: container.isMarginSeparator && (modelData === 'top' || modelData === 'bottom') === root.isHorizontalfill: false}}}onAppletChanged: {if (applet) {applet.parent = containerapplet.anchors.fill = container} else {appletsModel.remove(index)}}active: applet && applet.Plasmoid.busysourceComponent: PC3.BusyIndicator {z: 999}property int oldX: 0property int oldY: 0onXChanged: if (oldX) animateFrom(oldX, y)onYChanged: if (oldY) animateFrom(x, oldY)transform: Translate{id: translation}function animateFrom(xa, ya) {if (root.isHorizontal) translation.x = xa - xelse translation.y = ya - yoldX = oldY = 0translAnim.running = true}NumberAnimation {id: translAnimduration: Kirigami.Units.shortDurationeasing.type: Easing.OutCubictarget: translationproperties: "x,y"to: 0}}}//END components//BEGIN UI elementsanchors {leftMargin: root.isHorizontal ? Math.min(dropArea.spacingAtMinSize, panelSvg.fixedMargins.left + currentLayout.rowSpacing) : 0rightMargin: root.isHorizontal ? Math.min(dropArea.spacingAtMinSize, panelSvg.fixedMargins.right + currentLayout.rowSpacing) : 0topMargin: root.isHorizontal ? 0 : Math.min(dropArea.spacingAtMinSize, panelSvg.fixedMargins.top + currentLayout.rowSpacing)bottomMargin: root.isHorizontal ? 0 : Math.min(dropArea.spacingAtMinSize, panelSvg.fixedMargins.bottom + currentLayout.rowSpacing)}Item {id: dndSpacerproperty bool busy: falseLayout.preferredWidth: widthLayout.preferredHeight: heightwidth: root.isHorizontal ? Kirigami.Units.iconSizes.sizeForLabels * 5 : currentLayout.widthheight: root.isHorizontal ? currentLayout.height : Kirigami.Units.iconSizes.sizeForLabels * 5}ListModel {id: appletsModel}GridLayout {id: currentLayoutRepeater {model: appletsModeldelegate: appletContainerComponent}rowSpacing: Kirigami.Units.smallSpacingcolumnSpacing: Kirigami.Units.smallSpacingx: 0readonly property int toolBoxSize: !root.toolBox || !Plasmoid.containment.corona.editMode ? 0 : (root.isHorizontal ? root.toolBox.width : root.toolBox.height)property int horizontalDisplacement: dropArea.anchors.leftMargin + dropArea.anchors.rightMargin + (root.isHorizontal ? currentLayout.toolBoxSize : 0)property int verticalDisplacement: dropArea.anchors.topMargin + dropArea.anchors.bottomMargin + (root.isHorizontal ? 0 : currentLayout.toolBoxSize)// BEGIN BUG 454095: use lastSpacer to left align applets, as implicitWidth is updated too latewidth: root.width - horizontalDisplacementheight: root.height - verticalDisplacementItem {id: lastSpacervisible: !root.hasSpacerLayout.fillWidth: trueLayout.fillHeight: true/*** This index will be used when adding a new panel.** @see LayoutManager.indexAtCoordinates*/readonly property alias index: appletsModel.count}// END BUG 454095rows: root.isHorizontal ? 1 : currentLayout.children.lengthcolumns: root.isHorizontal ? currentLayout.children.length : 1flow: root.isHorizontal ? GridLayout.LeftToRight : GridLayout.TopToBottomlayoutDirection: Application.layoutDirection}}MouseArea {anchors.fill: parentvisible: Containment.corona.editMode && !Plasmoid.userConfiguringhoverEnabled: trueonClicked: Plasmoid.internalAction("configure").trigger()Rectangle {anchors.fill: parentcolor: Kirigami.Theme.highlightColoropacity: 0.5visible: parent.containsMouse}PlasmaCore.ToolTipArea {id: toolTipAreaanchors.fill: parent// ContainmentItem may add & markers to the menu for mnemonicsKirigami.MnemonicData.label: Plasmoid.internalAction("configure").textmainText: Kirigami.MnemonicData.plainTextLabelicon: "configure"}Accessible.name: toolTipArea.mainTextAccessible.description: i18ndc("plasma_shell_org.kde.plasma.desktop", "@info:whatsthis Accessible description for entering Panel edit mode click area", "Open Panel configuration ui")Accessible.role: Accessible.Button}PC3.ToolButton {id: addWidgetsButtonanchors.centerIn: parentvisible: appletsModel.count === 0text: root.isHorizontal ? i18ndc("plasma_shell_org.kde.plasma.desktop", "@action:button opens widget explorer", "Add Widgetsβ¦") : undefinedicon.name: "list-add-symbolic"onClicked: Plasmoid.internalAction("add widgets").trigger()}//END UI elements}