1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-13 07:42:46 -04:00
Files
DankMaterialShell/.agents/skills/dms-plugin-dev/references/widget-plugin-guide.md
T

9.4 KiB

Widget Plugin Guide

Widgets are bar plugins that display pills in DankBar, optionally open popouts, and can integrate with the Control Center.

Base Component

Widgets use PluginComponent from qs.Modules.Plugins.

import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins

PluginComponent {
    property var popoutService: null

    horizontalBarPill: Component { /* ... */ }
    verticalBarPill: Component { /* ... */ }
    popoutContent: Component { /* ... */ }
    popoutWidth: 400
    popoutHeight: 300
}

Injected Properties

These are automatically set by the plugin host:

Property Type Description
axis object Bar axis info (horizontal/vertical)
section string Bar section: "left", "center", or "right"
parentScreen object Screen reference for multi-monitor
widgetThickness real Widget size perpendicular to bar edge
barThickness real Bar thickness parallel to edge
pluginId string This plugin's ID
pluginService object PluginService reference
pluginData object Reactive plugin settings data

Bar Pills

Define horizontalBarPill (for top/bottom bars) and verticalBarPill (for left/right bars).

Horizontal Bar Pill

horizontalBarPill: Component {
    StyledRect {
        width: content.implicitWidth + Theme.spacingM * 2
        height: parent.widgetThickness
        radius: Theme.cornerRadius
        color: Theme.surfaceContainerHigh

        Row {
            id: content
            anchors.centerIn: parent
            spacing: Theme.spacingS

            DankIcon {
                name: "star"
                color: Theme.surfaceText
                font.pixelSize: Theme.iconSize
                anchors.verticalCenter: parent.verticalCenter
            }

            StyledText {
                text: "Label"
                color: Theme.surfaceText
                font.pixelSize: Theme.fontSizeMedium
                anchors.verticalCenter: parent.verticalCenter
            }
        }
    }
}

Vertical Bar Pill

verticalBarPill: Component {
    StyledRect {
        width: parent.widgetThickness
        height: content.implicitHeight + Theme.spacingM * 2
        radius: Theme.cornerRadius
        color: Theme.surfaceContainerHigh

        Column {
            id: content
            anchors.centerIn: parent
            spacing: Theme.spacingS

            DankIcon {
                name: "star"
                color: Theme.surfaceText
                font.pixelSize: Theme.iconSizeSmall
            }
        }
    }
}

Important: Always define both pills. If a pill is missing, the widget disappears when the bar is on that orientation's edge.

Popout Content

Open a popout window when the bar pill is clicked:

PluginComponent {
    popoutWidth: 400
    popoutHeight: 300

    popoutContent: Component {
        PopoutComponent {
            headerText: "My Plugin"
            detailsText: "Optional subtitle"
            showCloseButton: true

            Column {
                width: parent.width
                spacing: Theme.spacingM

                StyledText {
                    text: "Content here"
                    color: Theme.surfaceText
                    font.pixelSize: Theme.fontSizeMedium
                }
            }
        }
    }
}

PopoutComponent properties:

Property Type Default Description
headerText string "" Main header (bold, large). Hidden if empty.
detailsText string "" Subtitle below header. Hidden if empty.
showCloseButton bool false Show X button in top-right corner.
closePopout function (injected) Call to close the popout programmatically.
headerHeight int (readonly) Height of header area (0 if hidden).
detailsHeight int (readonly) Height of details area (0 if hidden).

Content sizing: Content children render below the header/details. Calculate available height: popoutHeight - headerHeight - detailsHeight - spacing

Custom Click Actions

Override the default popout behavior:

PluginComponent {
    // Simple no-args handler
    pillClickAction: () => {
        popoutService?.toggleControlCenter()
    }

    // With position params (x, y, width, section, screen)
    pillClickAction: (x, y, width, section, screen) => {
        popoutService?.toggleControlCenter(x, y, width, section, screen)
    }

    pillRightClickAction: () => {
        popoutService?.openSettings()
    }
}

Control Center Integration

Add CC properties to show your widget in the Control Center grid:

PluginComponent {
    ccWidgetIcon: "toggle_on"
    ccWidgetPrimaryText: "Feature Name"
    ccWidgetSecondaryText: isActive ? "Active" : "Off"
    ccWidgetIsActive: isActive

    onCcWidgetToggled: {
        isActive = !isActive
        pluginService?.savePluginData(pluginId, "active", isActive)
    }
}

CC properties:

Property Type Description
ccWidgetIcon string Material icon name
ccWidgetPrimaryText string Main label
ccWidgetSecondaryText string Subtitle / status text
ccWidgetIsActive bool Active state (changes styling)

CC signals:

Signal When fired
ccWidgetToggled() Icon area clicked
ccWidgetExpanded() Expand area clicked (CompoundPill only)

CC sizing rules:

  • 25% width - SmallToggleButton (icon only)
  • 50% width - ToggleButton (no detail) or CompoundPill (with ccDetailContent)
  • Users can resize in CC edit mode

Detail Content (CompoundPill)

Add an expandable panel below the CC widget:

ccDetailContent: Component {
    Rectangle {
        implicitHeight: 200
        color: Theme.surfaceContainerHigh
        radius: Theme.cornerRadius

        Column {
            anchors.fill: parent
            anchors.margins: Theme.spacingM
            spacing: Theme.spacingS

            // Detail UI here
        }
    }
}

Visibility Control

Conditionally show/hide the bar pill:

PluginComponent {
    visibilityCommand: "pgrep -x myapp"
    visibilityInterval: 5000  // check every 5 seconds
}

Popout Namespace

For plugins with multiple popout instances, use layerNamespacePlugin to isolate popout state:

PluginComponent {
    layerNamespacePlugin: true
}

Reading Plugin Data

Access saved settings reactively via the injected pluginData:

PluginComponent {
    property string displayText: pluginData?.text || "Default"

    Connections {
        target: pluginService
        function onPluginDataChanged(changedId) {
            if (changedId === pluginId)
                displayText = pluginService.loadPluginData(pluginId, "text", "Default")
        }
    }
}

Complete Example

Based on the ExampleEmojiPlugin pattern:

import QtQuick
import Quickshell
import qs.Common
import qs.Widgets
import qs.Services
import qs.Modules.Plugins

PluginComponent {
    id: root
    property var popoutService: null

    property var emojis: ["star", "heart", "smile"]
    property int currentIndex: 0

    Timer {
        interval: 2000
        running: true
        repeat: true
        onTriggered: currentIndex = (currentIndex + 1) % emojis.length
    }

    popoutWidth: 350
    popoutHeight: 400

    horizontalBarPill: Component {
        StyledRect {
            width: label.implicitWidth + Theme.spacingM * 2
            height: parent.widgetThickness
            radius: Theme.cornerRadius
            color: Theme.surfaceContainerHigh

            StyledText {
                id: label
                anchors.centerIn: parent
                text: root.emojis[root.currentIndex]
                font.pixelSize: Theme.fontSizeLarge
            }
        }
    }

    verticalBarPill: Component {
        StyledRect {
            width: parent.widgetThickness
            height: label.implicitHeight + Theme.spacingM * 2
            radius: Theme.cornerRadius
            color: Theme.surfaceContainerHigh

            StyledText {
                id: label
                anchors.centerIn: parent
                text: root.emojis[root.currentIndex]
                font.pixelSize: Theme.fontSizeMedium
            }
        }
    }

    popoutContent: Component {
        PopoutComponent {
            headerText: "Emoji Picker"
            showCloseButton: true

            DankGridView {
                width: parent.width
                height: 300
                cellWidth: 50
                cellHeight: 50
                model: root.emojis

                delegate: Rectangle {
                    width: 48
                    height: 48
                    radius: Theme.cornerRadius
                    color: mouseArea.containsMouse ? Theme.surfaceContainerHighest : "transparent"

                    Text {
                        anchors.centerIn: parent
                        text: modelData
                        font.pixelSize: 24
                    }

                    MouseArea {
                        id: mouseArea
                        anchors.fill: parent
                        hoverEnabled: true
                        onClicked: {
                            Quickshell.execDetached(["dms", "cl", "copy", modelData])
                            ToastService?.showInfo("Copied " + modelData)
                        }
                    }
                }
            }
        }
    }
}