/* SPDX-FileCopyrightText: 2019 Marco Martin SPDX-FileCopyrightText: 2022 ivan tkachenko SPDX-FileCopyrightText: 2022 Niccolò Venerandi SPDX-License-Identifier: LGPL-2.0-or-later */ import QtQuick 2.15 import QtQuick.Layouts 1.15 import QtQuick.Window 2.15 import Qt5Compat.GraphicalEffects import QtQuick.Effects as Effects 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.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager import org.kde.kirigami as Kirigami ContainmentLayoutManager.AppletContainer { id: appletContainer editModeCondition: Plasmoid.immutable ? ContainmentLayoutManager.ItemContainer.Manual : ContainmentLayoutManager.ItemContainer.AfterPressAndHold Kirigami.Theme.inherit: false Kirigami.Theme.colorSet: (applet.plasmoid.effectiveBackgroundHints & PlasmaCore.Types.ShadowBackground) && !(applet.plasmoid.effectiveBackgroundHints & PlasmaCore.Types.StandardBackground) && !(applet.plasmoid.effectiveBackgroundHints & PlasmaCore.Types.TranslucentBackground) ? Kirigami.Theme.Complementary : Kirigami.Theme.Window onFocusChanged: { if (!focus && !dragActive) { editMode = false; } } Layout.minimumWidth: { if (!applet) { return leftPadding + rightPadding; } if (applet.preferredRepresentation !== applet.fullRepresentation && applet.compactRepresentationItem ) { return applet.compactRepresentationItem.Layout.minimumWidth + leftPadding + rightPadding; } else { return applet.Layout.minimumWidth + leftPadding + rightPadding; } } Layout.minimumHeight: { if (!applet) { return topPadding + bottomPadding; } if (applet.preferredRepresentation !== applet.fullRepresentation && applet.compactRepresentationItem ) { return applet.compactRepresentationItem.Layout.minimumHeight + topPadding + bottomPadding; } else { return applet.Layout.minimumHeight + topPadding + bottomPadding; } } Layout.preferredWidth: Math.max(applet.Layout.minimumWidth, applet.Layout.preferredWidth) Layout.preferredHeight: Math.max(applet.Layout.minimumHeight, applet.Layout.preferredHeight) Layout.maximumWidth: applet.Layout.maximumWidth Layout.maximumHeight: applet.Layout.maximumHeight leftPadding: background.margins.left topPadding: background.margins.top rightPadding: background.margins.right bottomPadding: background.margins.bottom // render via a layer if we're at an angle // resize handles are rendered outside this item, so also disable when they're showing to avoid clipping layer.enabled: (rotation % 90 !== 0) && !(configOverlayItem && configOverlayItem.visible) layer.smooth: true initialSize.width: applet.switchWidth + leftPadding + rightPadding initialSize.height: applet.switchHeight + topPadding + bottomPadding background: KSvg.FrameSvgItem { id: background property bool blurEnabled: false property Item maskItem: null prefix: blurEnabled ? "blurred" : "" imagePath: { if (!appletContainer.applet) { return ""; } if (appletContainer.applet.plasmoid.effectiveBackgroundHints & PlasmaCore.Types.TranslucentBackground) { return "widgets/translucentbackground"; } else if (appletContainer.applet.plasmoid.effectiveBackgroundHints & PlasmaCore.Types.StandardBackground) { return "widgets/background"; } else { return ""; } } function bindBlurEnabled() { // bind to api and hints automatically, refresh non-observable prefix manually blurEnabled = Qt.binding(() => GraphicsInfo.api !== GraphicsInfo.Software && (appletContainer.applet.plasmoid.effectiveBackgroundHints & PlasmaCore.Types.StandardBackground) && hasElementPrefix("blurred") ); } Component.onCompleted: bindBlurEnabled() onRepaintNeeded: bindBlurEnabled() onBlurEnabledChanged: { if (blurEnabled) { if (maskItem === null) { maskItem = maskComponent.createObject(this); } } else { if (maskItem !== null) { maskItem.destroy(); maskItem = null; } } } DropShadow { anchors { fill: parent leftMargin: appletContainer.leftPadding topMargin: appletContainer.topPadding rightMargin: appletContainer.rightPadding bottomMargin: appletContainer.bottomPadding } z: -1 horizontalOffset: 0 verticalOffset: 1 radius: 4 samples: 9 spread: 0.35 color: Qt.rgba(0, 0, 0, 0.5) opacity: 1 source: appletContainer.applet && appletContainer.applet.plasmoid.effectiveBackgroundHints & PlasmaCore.Types.ShadowBackground ? appletContainer.applet : null visible: source !== null } } Component { id: maskComponent Effects.MultiEffect { id: backgroundEffect readonly property rect appletContainerScreenRect: { const win = appletContainer.Window.window; let sceneSize = Qt.size(appletContainer.width, appletContainer.height) if (win) { sceneSize = Qt.size(win.width, win.height) } const position = appletContainer.Kirigami.ScenePosition; return clipRect( boundsForTransformedRect( Qt.rect( position.x - parent.Kirigami.ScenePosition.x, position.y - parent.Kirigami.ScenePosition.y, appletContainer.width, appletContainer.height), appletContainer.rotation, appletContainer.scale), sceneSize); } /** Apply geometry transformations, and return a bounding rectangle for a resulting shape. */ // Note: It's basically a custom QMatrix::mapRect implementation, and for // simplicity's sake should be replaced when/if mapRect becomes available in QML. function boundsForTransformedRect(rect: rect, angle: real, scale: real): rect { if (angle === 0 && scale === 1) { return rect; // hot path optimization } let cosa = Math.abs(Math.cos(angle * (Math.PI / 180))) * scale; let sina = Math.abs(Math.sin(angle * (Math.PI / 180))) * scale; let newSize = Qt.size( rect.width * cosa + rect.height * sina, rect.width * sina + rect.height * cosa); return Qt.rect( rect.left + (rect.width - newSize.width) / 2, rect.top + (rect.height - newSize.height) / 2, newSize.width, newSize.height); } /** Clip given rectangle to the bounds of given size, assuming bounds position {0,0}. * This is a pure library function, similar to QRect::intersected, * which Qt should've exposed in QML stdlib. */ function clipRect(rect: rect, bounds: size): rect { return Qt.rect( Math.max(0, Math.min(bounds.width, rect.x)), Math.max(0, Math.min(bounds.height, rect.y)), Math.max(0, rect.width + Math.min(0, rect.x) + Math.min(0, bounds.width - (rect.x + rect.width))), Math.max(0, rect.height + Math.min(0, rect.y) + Math.min(0, bounds.height - (rect.y + rect.height))), ); } parent: appletContainer.layout.containmentItem x: appletContainerScreenRect.x y: appletContainerScreenRect.y width: appletContainerScreenRect.width height: appletContainerScreenRect.height visible: appletContainer.visible z: -2 maskEnabled: true maskSource: mask // In the config they are more or less from 0 to 2, here from -1 to 1 contrast: PlasmaCore.Theme.backgroundContrast - 1.0 brightness: PlasmaCore.Theme.backgroundIntensity - 1.0 saturation: PlasmaCore.Theme.backgroundSaturation - 1.0 Item { id: mask // optimized (clipped) blurred-mask layer.enabled: true width: backgroundEffect.appletContainerScreenRect.width height: backgroundEffect.appletContainerScreenRect.height clip: true KSvg.FrameSvgItem { imagePath: "widgets/background" prefix: "blurred-mask" x: appletContainer.Kirigami.ScenePosition.x - backgroundEffect.appletContainerScreenRect.x - appletContainer.layout.containmentItem.Kirigami.ScenePosition.x y: appletContainer.Kirigami.ScenePosition.y - backgroundEffect.appletContainerScreenRect.y - appletContainer.layout.containmentItem.Kirigami.ScenePosition.y width: background.width height: background.height rotation: appletContainer.rotation scale: appletContainer.scale } } source: Effects.MultiEffect { // Blur needs to be a separed MultiEffect because otherwise it blurs // the mask as well width: backgroundEffect.appletContainerScreenRect.width height: backgroundEffect.appletContainerScreenRect.height blurEnabled: true blur: 1 blurMax: 128 source: ShaderEffectSource { width: backgroundEffect.appletContainerScreenRect.width height: backgroundEffect.appletContainerScreenRect.height sourceRect: backgroundEffect.appletContainerScreenRect sourceItem: appletContainer.layout.containmentItem.wallpaper } } } } busyIndicatorComponent: PlasmaComponents.BusyIndicator { anchors.centerIn: parent visible: applet.plasmoid.busy running: visible } configurationRequiredComponent: PlasmaComponents.Button { anchors.centerIn: parent text: i18nd("plasmashellprivateplugin", "Configure…") icon.name: "configure" visible: applet.plasmoid.configurationRequired onClicked: applet.plasmoid.internalAction("configure").trigger(); } }