pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell import Qt5Compat.GraphicalEffects import qs import qs.modules.common import qs.modules.common.functions import qs.modules.common.widgets import qs.modules.common.widgets.widgetCanvas /* * To make an overlay widget: * 1. Create a modules/overlay//.qml, using this as the base class and declare your widget content as contentItem * 2. Add an entry to OverlayContext.availableWidgets with identifier= * 3. Add an entry in Persistent.states.overlay. with x, y, pinned, clickthrough properties set to reasonable defaults * 4. Add an entry in OverlayWidgetDelegateChooser with roleValue= and Declare your widget in there * Use existing entries as reference. */ AbstractOverlayWidget { id: root required property Item contentItem property bool fancyBorders: true property bool showCenterButton: false property bool showClickabilityButton: true required property var modelData readonly property string identifier: modelData.identifier readonly property string materialSymbol: modelData.materialSymbol ?? "widgets" property string title: identifier.replace(/([A-Z])/g, " $1").replace(/^./, function(str){ return str.toUpperCase(); }) property var persistentStateEntry: Persistent.states.overlay[identifier] property real radius: Appearance.rounding.windowRounding property real minWidth: 250 property real padding: 6 property real contentRadius: radius - padding draggable: GlobalStates.overlayOpen x: Math.round(persistentStateEntry.x) // Round or it'll be blurry y: Math.round(persistentStateEntry.y) // Round or it'll be blurry pinned: persistentStateEntry.pinned clickthrough: persistentStateEntry.clickthrough drag { minimumX: 0 minimumY: 0 maximumX: root.parent?.width - root.width maximumY: root.parent?.height - root.height } opacity: (GlobalStates.overlayOpen || !clickthrough) ? 1.0 : Config.options.overlay.clickthroughOpacity // Guarded states & registration funcs property bool open: Persistent.states.overlay.open property bool actuallyPinned: pinned && open property bool actuallyClickable: !clickthrough && actuallyPinned && open onActuallyPinnedChanged: reportPinnedState(); onActuallyClickableChanged: reportClickableState(); function reportPinnedState() { OverlayContext.pin(identifier, actuallyPinned); } function reportClickableState() { OverlayContext.registerClickableWidget(contentItem, actuallyClickable); } // Self-registeration with OverlayContext Component.onCompleted: { reportPinnedState(); reportClickableState(); } // Hooks onReleased: savePosition(); function close() { Persistent.states.overlay.open = Persistent.states.overlay.open.filter(type => type !== root.identifier); } function togglePinned() { persistentStateEntry.pinned = !persistentStateEntry.pinned; } function toggleClickthrough() { persistentStateEntry.clickthrough = !persistentStateEntry.clickthrough; } function savePosition(xPos = root.x, yPos = root.y) { persistentStateEntry.x = xPos; persistentStateEntry.y = yPos; } function center() { const targetX = (root.parent.width - contentColumn.width) / 2 const targetY = (root.parent.height - contentItem.height) / 2 - titleBar.implicitHeight + border.border.width root.x = targetX root.y = targetY root.savePosition(targetX, targetY) } visible: GlobalStates.overlayOpen || actuallyPinned implicitWidth: Math.max(contentColumn.implicitWidth, minWidth) implicitHeight: contentColumn.implicitHeight Rectangle { id: border anchors.fill: parent color: (root.fancyBorders && GlobalStates.overlayOpen) ? Appearance.colors.colLayer1 : "transparent" radius: root.radius border.color: ColorUtils.transparentize(Appearance.colors.colOutlineVariant, GlobalStates.overlayOpen ? 0 : 1) border.width: 1 Behavior on color { animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) } layer.enabled: GlobalStates.overlayOpen layer.effect: OpacityMask { maskSource: Rectangle { width: border.width height: border.height radius: root.radius } } ColumnLayout { id: contentColumn z: root.fancyBorders ? 0 : -1 anchors.fill: parent spacing: 0 // Title bar Rectangle { id: titleBar opacity: GlobalStates.overlayOpen ? 1 : 0 Layout.fillWidth: true implicitWidth: titleBarRow.implicitWidth + root.padding * 2 implicitHeight: titleBarRow.implicitHeight + root.padding * 2 color: root.fancyBorders ? "transparent" : Appearance.colors.colLayer1 // border.color: Appearance.colors.colOutlineVariant // border.width: 1 RowLayout { id: titleBarRow anchors { fill: parent margins: root.padding } spacing: 2 MaterialSymbol { text: root.materialSymbol Layout.leftMargin: 6 iconSize: 20 Layout.alignment: Qt.AlignVCenter Layout.rightMargin: 4 } StyledText { text: root.title Layout.fillWidth: true elide: Text.ElideRight } TitlebarButton { visible: root.showCenterButton materialSymbol: "recenter" onClicked: root.center() StyledToolTip { text: "Center" } } TitlebarButton { visible: (root.pinned && root.showClickabilityButton) materialSymbol: "mouse" toggled: !root.clickthrough onClicked: root.toggleClickthrough() StyledToolTip { text: "Clickable when pinned" } } TitlebarButton { materialSymbol: "keep" toggled: root.pinned onClicked: root.togglePinned() StyledToolTip { text: "Pin" } } TitlebarButton { materialSymbol: "close" onClicked: root.close() StyledToolTip { text: "Close" } } } } // Content Item { id: contentContainer Layout.fillWidth: true Layout.fillHeight: true Layout.margins: root.fancyBorders ? root.padding : 0 Layout.topMargin: -border.border.width // Border of a rectangle is drawn inside its bounds, so we do this to make the gap not too big Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter implicitWidth: root.contentItem.implicitWidth implicitHeight: root.contentItem.implicitHeight children: [root.contentItem] } } } component TitlebarButton: RippleButton { id: titlebarButton required property string materialSymbol buttonRadius: height / 2 implicitHeight: contentItem.implicitHeight implicitWidth: implicitHeight padding: 0 colBackgroundToggled: Appearance.colors.colSecondaryContainer colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover colRippleToggled: Appearance.colors.colSecondaryContainerActive contentItem: Item { anchors.centerIn: parent implicitWidth: 30 implicitHeight: 30 MaterialSymbol { id: iconWidget anchors.centerIn: parent iconSize: 20 text: titlebarButton.materialSymbol fill: titlebarButton.toggled color: titlebarButton.toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnSurface } } } }