/* SPDX-FileCopyrightText: 2013 Marco Martin SPDX-FileCopyrightText: 2022 Niccolò Venerandi SPDX-License-Identifier: GPL-2.0-or-later */ import QtQuick import QtQuick.Layouts import org.kde.plasma.plasmoid import org.kde.plasma.core as PlasmaCore import org.kde.ksvg as KSvg import org.kde.plasma.components as PC3 import org.kde.kquickcontrolsaddons import org.kde.draganddrop as DragDrop import org.kde.kirigami as Kirigami import "LayoutManager.js" as LayoutManager ContainmentItem { id: root width: 640 height: 40 // filled in by the shell (Panel.qml) with the plasma-workspace PanelView property var panel: null property int panelThickness: root.panel.thickness property bool panelFloating: root.panel.floating property bool panelFloatingApplets: root.panel.floatingApplets onPanelThicknessChanged: { if(root.panel) { if(root.panel.userConfiguring) { Plasmoid.configuration.panelThickness = root.panel.thickness; Plasmoid.configuration.writeConfig(); } } } onPanelFloatingChanged: { if(root.panel) { if(root.panel.userConfiguring) { Plasmoid.configuration.panelFloating = root.panel.floating; Plasmoid.configuration.writeConfig(); } } } onPanelFloatingAppletsChanged: { if(root.panel) { if(root.panel.userConfiguring) { Plasmoid.configuration.panelFloatingApplets = root.panel.floatingApplets; Plasmoid.configuration.writeConfig(); } } } onPanelChanged: { if(root.panel) { root.panel.floating = Plasmoid.configuration.panelFloating; root.panel.floatingApplets = Plasmoid.configuration.panelFloatingApplets; root.panel.thickness = Plasmoid.configuration.panelThickness; } } //BEGIN properties Layout.preferredWidth: fixedWidth || currentLayout.implicitWidth + currentLayout.horizontalDisplacement Layout.preferredHeight: fixedHeight || currentLayout.implicitHeight + currentLayout.verticalDisplacement property Item isShowDesktopApplet: gradientRect.updateProps, getShowDesktopApplet() property alias sevenTasksReference: gradientRect.applet function getShowDesktopApplet() { if(currentLayout.visibleChildren.length === 0) return null; var item = currentLayout.visibleChildren[currentLayout.visibleChildren.length-2]; if(item) { if(typeof item.applet !== "undefined") { if(item.applet.Plasmoid.pluginName === "io.gitgud.wackyideas.win7showdesktop") { return item; } } } return null; } property Item toolBox property var layoutManager: LayoutManager property Item configOverlay property bool isHorizontal: Plasmoid.formFactor !== PlasmaCore.Types.Vertical property int fixedWidth: 0 property int fixedHeight: 0 property bool hasSpacer // True when a widget is being drag and dropped within the panel. property bool dragAndDropping: false // True when e.g. the task manager is drag and dropping tasks. property bool appletRequestsInhibitDnD: false //END properties //BEGIN functions function checkLastSpacer() { for (var i = 0; i < appletsModel.count; ++i) { const applet = appletsModel.get(i).applet; if (!applet || !applet.visible || !applet.Layout) { 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 ""; } //END functions //BEGIN connections Containment.onAppletAdded: (applet, geometry) => { LayoutManager.addApplet(applet, geometry.x, geometry.y); root.checkLastSpacer(); // When a new preset panel is added, avoid calling save() multiple times Qt.callLater(LayoutManager.save); /*Qt.callLater(() => { gradientRect.applet = Qt.binding(() => { return gradientRect.findApplet(); }); gradientRect.nextApplet = Qt.binding(() => { return gradientRect.findNextApplet(); }); });*/ } Containment.onAppletRemoved: (applet) => { let plasmoidItem = root.itemFor(applet); if (plasmoidItem) { appletsModel.remove(plasmoidItem.parent.index); } checkLastSpacer(); LayoutManager.save(); } Plasmoid.onUserConfiguringChanged: { if (!Plasmoid.userConfiguring) { gradientRect.updateProps = !gradientRect.updateProps if (root.configOverlay) { root.configOverlay.destroy(); root.configOverlay = null; } return; } if (Plasmoid.immutable) { return; } Containment.applets.forEach(applet => applet.expanded = false); const component = Qt.createComponent("ConfigOverlay.qml"); root.configOverlay = component.createObject(root, { "anchors.fill": dropArea, "anchors.rightMargin": root.isHorizontal ? toolBox.width : 0, "anchors.bottomMargin": !root.isHorizontal ? toolBox.height : 0, }); component.destroy(); } //END connections DragDrop.DropArea { id: dropArea anchors.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: panelSvg visible: false imagePath: "widgets/panel-background" prefix: [root.plasmoidLocationString(), ""] } KSvg.FrameSvgItem { id: thickPanelSvg visible: false prefix: ['thick'].concat(panelSvg.prefix) imagePath: "widgets/panel-background" } property bool marginAreasEnabled: panelSvg.margins != thickPanelSvg.margins property 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 above readonly property real spacingAtMinSize: Math.floor(Math.max(1, (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 resize root.fixedWidth = root.Layout.preferredWidth root.fixedHeight = root.Layout.preferredHeight appletsModel.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. */ if (typeof(dndSpacer.parent.index) === "number") { if(dndSpacer.parent.index >= 0) appletsModel.remove(dndSpacer.parent.index); root.fixedWidth = root.fixedHeight = 0; } } onDrop: event => { if(dndSpacer.parent.index > 0) appletsModel.remove(dndSpacer.parent.index); root.processMimeData(event.mimeData, event.x, event.y); event.accept(event.proposedAction); root.fixedWidth = root.fixedHeight = 0; } //BEGIN components Component { id: appletContainerComponent // This loader conditionally manages the BusyIndicator, it's not // loading the applet. The applet becomes a regular child item. Loader { id: container required property Item applet required property int index property Item dragging property bool isAppletContainer: true property bool isMarginSeparator: ((applet.plasmoid?.constraintHints & Plasmoid.MarginAreasSeparator) == Plasmoid.MarginAreasSeparator) property int appletIndex: index // To make sure it's always readable even inside other models property bool inThickArea: false visible: applet.plasmoid?.status !== PlasmaCore.Types.HiddenStatus || (!Plasmoid.immutable && Plasmoid.userConfiguring) || Containment.corona.editMode; //when the applet moves caused by its resize, don't animate. //this is completely heuristic, but looks way less "jumpy" property bool movingForResize: false function 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) : overrideFillArea let inThickArea = overrideThickArea === null ? container.inThickArea : overrideThickArea var layout = { top: isHorizontal, bottom: isHorizontal, right: !isHorizontal, left: !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: 0//getMargins('left') Layout.rightMargin: 0//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=473420 Layout.fillWidth: true Layout.fillHeight: true property bool wantsToFillWidth: applet?.Layout.fillWidth property bool wantsToFillHeight: applet?.Layout.fillHeight onWantsToFillWidthChanged: checkLastSpacer() onWantsToFillHeightChanged: checkLastSpacer() property int availWidth: root.width - Layout.leftMargin - Layout.rightMargin property int availHeight: root.height - Layout.topMargin - Layout.bottomMargin function findPositive(first, second) {return first > 0 ? first : second} // BEGIN BUG 454095: do not combine these expressions to a function or the bindings won't work Layout.minimumWidth: root.isHorizontal ? findPositive(applet?.Layout.minimumWidth, availHeight) : availWidth Layout.minimumHeight: !root.isHorizontal ? findPositive(applet?.Layout.minimumHeight, availWidth) : availHeight Layout.preferredWidth: root.isHorizontal ? findPositive(applet?.Layout.preferredWidth, Layout.minimumWidth) : availWidth Layout.preferredHeight: !root.isHorizontal ? findPositive(applet?.Layout.preferredHeight, Layout.minimumHeight) : availHeight Layout.maximumWidth: root.isHorizontal ? (wantsToFillWidth ? findPositive(applet?.Layout.maximumWidth, root.width) : Math.min(applet?.Layout.maximumWidth, Layout.preferredWidth)) : availWidth Layout.maximumHeight: !root.isHorizontal ? (wantsToFillHeight ? findPositive(applet?.Layout.maximumHeight, root.height) : Math.min(applet?.Layout.maximumHeight, Layout.preferredHeight)) : availHeight // END BUG 454095 Item { id: marginHighlightElements anchors.fill: parent // index -1 is for floating applets, which do not need a margin highlight opacity: Plasmoid.containment.corona.editMode && dropArea.marginAreasEnabled && !root.dragAndDropping && index != -1 ? 1 : 0 Behavior on opacity { NumberAnimation { duration: Kirigami.Units.longDuration easing.type: Easing.InOutQuad } } component SideMargin: KSvg.SvgItem { property string side; property bool fill: true property int inset; property int padding property 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] = undefined if (root.isHorizontal) { height = padding; } else { width = padding; } anchors[left+'Margin'] = - currentLayout.rowSpacing/2 - (appletIndex == 0 ? dropArea.anchors[left + 'Margin'] + currentLayout.x : 0) anchors[right+'Margin'] = - currentLayout.rowSpacing/2 - (appletIndex == appletsModel.count-1 ? dropArea.anchors[right + 'Margin'] + currentLayout.toolBoxSize : 0) anchors[side+'Margin'] = - inset } elementId: fill ? 'fill' : (root.isHorizontal ? side + (inThickArea ? 'left' : 'right') : (inThickArea ? 'top' : 'bottom') + side) svg: dropArea.marginHighlightSvg anchors {top: parent.top; left: parent.left; right: parent.right; bottom: parent.bottom} } Repeater { model: ['top', 'bottom', 'right', 'left'] SideMargin { side: modelData inset: container.getMargins(side) visible: (modelData === 'top' || modelData === 'bottom') === root.isHorizontal padding: container.getMargins(side, false, false, isMarginSeparator ? false : inThickArea) } } Repeater { model: ['top', 'bottom', 'right', 'left'] SideMargin { side: modelData inset: -container.getMargins(side, false, false, false) padding: container.getMargins(side, false, false, true) + inset visible: isMarginSeparator && (modelData === 'top' || modelData === 'bottom') === root.isHorizontal fill: false } } } onAppletChanged: { if (applet) { applet.parent = container applet.anchors.fill = container //applet.Plasmoid.internalAction("configure").text = "Properties" } else { appletsModel.remove(index) } } active: applet && applet.Plasmoid.busy sourceComponent: PC3.BusyIndicator { z: 999 } property int oldX: 0 property int oldY: 0 onXChanged: if (oldX) animateFrom(oldX, y) onYChanged: if (oldY) animateFrom(x, oldY) transform: Translate{id: translation} function animateFrom(xa, ya) { if (isHorizontal) translation.x = xa - x else translation.y = ya - y oldX = oldY = 0 translAnim.running = true } NumberAnimation { id: translAnim duration: Kirigami.Units.shortDuration easing.type: Easing.OutCubic target: translation properties: "x,y" to: 0 } } } //END components //BEGIN UI elements anchors { leftMargin: 0//isHorizontal ? Math.min(dropArea.spacingAtMinSize, panelSvg.fixedMargins.left) : 0 rightMargin: 0//isHorizontal ? Math.min(dropArea.spacingAtMinSize, panelSvg.fixedMargins.right) : 0 topMargin: isHorizontal ? 0 : Math.min(dropArea.spacingAtMinSize, panelSvg.fixedMargins.top) bottomMargin: isHorizontal ? 0 : Math.min(dropArea.spacingAtMinSize, panelSvg.fixedMargins.bottom) } Item { id: dndSpacer property bool busy: false Layout.preferredWidth: width Layout.preferredHeight: height width: isHorizontal ? Kirigami.Units.iconSizes.sizeForLabels * 5 : currentLayout.width height: isHorizontal ? currentLayout.height : Kirigami.Units.iconSizes.sizeForLabels * 5 } ListModel { id: appletsModel } Rectangle { id: gradientRect anchors.fill: currentLayout anchors.topMargin: plasmoidLocationString() === "south" ? panelSvg.fixedMargins.top : 0 function updateBottomMargin() { if(plasmoidLocationString() === "north") return panelSvg.fixedMargins.bottom else if(plasmoidLocationString() === "west" || plasmoidLocationString() === "east") { if(root.isShowDesktopApplet) { return root.isShowDesktopApplet.Layout.maximumHeight; } return 0; } else return 0; } function updateRightMargin() { if(plasmoidLocationString() === "east") return panelSvg.fixedMargins.right; else if(plasmoidLocationString() === "south" || plasmoidLocationString() === "north") { var result = -parent.anchors.rightMargin; if(root.isShowDesktopApplet) { result += root.isShowDesktopApplet.Layout.maximumWidth; } return result; } else return 0; } anchors.bottomMargin: updateProps, updateBottomMargin() anchors.leftMargin: plasmoidLocationString() === "west" ? panelSvg.fixedMargins.left : 0 anchors.rightMargin: updateProps, updateRightMargin() gradient: Gradient { orientation: isHorizontal ? Gradient.Horizontal : Gradient.Vertical GradientStop { position: 0.0; color: gradientRect.tint } GradientStop { position: gradientRect.gradStart; color: gradientRect.tint } GradientStop { position: gradientRect.gradStart+0.02; color: gradientRect.gradColor} GradientStop { position: gradientRect.gradEnd-0.02; color: gradientRect.gradColor} GradientStop { position: gradientRect.gradEnd; color: gradientRect.tint } } property int updateProps: 0 property string tint: "#36000000" property string gradColor: iconsOnlyApplet ? "transparent" : gradientRect.tint property double gradStart: { if(root.isHorizontal) { return (iconsOnlyApplet ? (applet.x / applet.availWidth) : 0.1) } else { return (iconsOnlyApplet ? (applet.y / applet.availHeight) : 0.1) } } property double gradEnd: { if(root.isHorizontal) { if(nextApplet) { return iconsOnlyApplet ? (nextApplet.x / nextApplet.availWidth) : 0.1; } else { return (iconsOnlyApplet ? ((applet.x+applet.Layout.maximumWidth) / applet.availWidth) : 0.1) } } else { if(nextApplet) { return iconsOnlyApplet ? (nextApplet.y / nextApplet.availHeight) : 0.1; } else { return (iconsOnlyApplet ? ((applet.y+applet.Layout.maximumHeight) / applet.availHeight) : 0.1) } } } property bool iconsOnlyApplet: applet ? applet.applet.iconsOnly : false property int index: applet ? applet.index : -1 property int count: appletsModel.count property string targetPlasmoid: "io.gitgud.wackyideas.seventasks" function findApplet() { for(var i = 0; i < currentLayout.visibleChildren.length; i++) { if(currentLayout.visibleChildren[i].applet.Plasmoid.pluginName === targetPlasmoid) { return currentLayout.visibleChildren[i]; } } return null } function findNextApplet() { for(var i = 0; i < currentLayout.visibleChildren.length; i++) { if(currentLayout.visibleChildren[i].applet.Plasmoid.pluginName === targetPlasmoid) { if(i == currentLayout.visibleChildren.length-1) { return null; } return currentLayout.visibleChildren[i+1]; } } return null; } property Item nextApplet: updateProps, findNextApplet(); property Item applet: updateProps, findApplet(); } GridLayout { id: currentLayout Repeater { model: appletsModel delegate: appletContainerComponent } rowSpacing: Kirigami.Units.smallSpacing columnSpacing: Kirigami.Units.smallSpacing x: Qt.application.layoutDirection === Qt.RightToLeft && isHorizontal ? toolBoxSize : 0; readonly property int toolBoxSize: !toolBox || !Plasmoid.containment.corona.editMode || Qt.application.layoutDirection === Qt.RightToLeft ? 0 : (isHorizontal ? toolBox.width : toolBox.height) property int horizontalDisplacement: dropArea.anchors.leftMargin + dropArea.anchors.rightMargin + (isHorizontal ? currentLayout.toolBoxSize : 0) property int verticalDisplacement: dropArea.anchors.topMargin + dropArea.anchors.bottomMargin + (isHorizontal ? 0 : currentLayout.toolBoxSize) // BEGIN BUG 454095: use lastSpacer to left align applets, as implicitWidth is updated too late width: root.width - horizontalDisplacement height: root.height - verticalDisplacement Item { id: lastSpacer visible: !root.hasSpacer Layout.fillWidth: true Layout.fillHeight: true /** * This index will be used when adding a new panel. * * @see LayoutManager.indexAtCoordinates */ readonly property alias index: appletsModel.count } // END BUG 454095 rows: isHorizontal ? 1 : currentLayout.children.length columns: isHorizontal ? currentLayout.children.length : 1 flow: isHorizontal ? GridLayout.LeftToRight : GridLayout.TopToBottom layoutDirection: Qt.application.layoutDirection } } MouseArea { anchors.fill: parent visible: Containment.corona.editMode && !Plasmoid.userConfiguring hoverEnabled: true onClicked: Plasmoid.internalAction("configure").trigger() Rectangle { anchors.fill: parent color: Kirigami.Theme.highlightColor opacity: 0.5 visible: parent.containsMouse } PlasmaCore.ToolTipArea { id: toolTipArea anchors.fill: parent mainText: Plasmoid.internalAction("configure").text icon: "configure" } Accessible.name: Plasmoid.internalAction("configure").text Accessible.description: i18nd("plasma_shell_org.kde.plasma.desktop", "Open Panel configuration ui") Accessible.role: Accessible.Button } PC3.ToolButton { id: addWidgetsButton anchors.centerIn: parent visible: appletsModel.count === 0 text: isHorizontal ? i18nd("plasma_shell_org.kde.plasma.desktop", "Add Widgets…") : undefined icon.name: "list-add-symbolic" onClicked: Plasmoid.internalAction("add widgets").trigger() } //END UI elements }