mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-08 04:09:15 -04:00
plugins: add support for composite plugins
- single plugin can register multiple types - e.g. daemon, bar widget, desktop widget
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginComponent {
|
||||
id: root
|
||||
|
||||
layerNamespacePlugin: "composite-example"
|
||||
|
||||
property var enabledEmojis: pluginData.emojis || ["😊", "😢", "❤️"]
|
||||
property int cycleInterval: pluginData.cycleInterval || 3000
|
||||
property int maxBarEmojis: pluginData.maxBarEmojis || 3
|
||||
|
||||
property int currentIndex: 0
|
||||
property var displayedEmojis: []
|
||||
|
||||
Timer {
|
||||
interval: root.cycleInterval
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (root.enabledEmojis.length > 0) {
|
||||
root.currentIndex = (root.currentIndex + 1) % root.enabledEmojis.length;
|
||||
root.updateDisplayedEmojis();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateDisplayedEmojis() {
|
||||
const maxToShow = Math.min(root.maxBarEmojis, root.enabledEmojis.length);
|
||||
let emojis = [];
|
||||
for (let i = 0; i < maxToShow; i++) {
|
||||
const idx = (root.currentIndex + i) % root.enabledEmojis.length;
|
||||
emojis.push(root.enabledEmojis[idx]);
|
||||
}
|
||||
root.displayedEmojis = emojis;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
updateDisplayedEmojis();
|
||||
}
|
||||
|
||||
onEnabledEmojisChanged: updateDisplayedEmojis()
|
||||
onMaxBarEmojisChanged: updateDisplayedEmojis()
|
||||
|
||||
horizontalBarPill: Component {
|
||||
Row {
|
||||
id: emojiRow
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: root.displayedEmojis
|
||||
StyledText {
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
verticalBarPill: Component {
|
||||
Column {
|
||||
id: emojiColumn
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: root.displayedEmojis
|
||||
StyledText {
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
popoutContent: Component {
|
||||
PopoutComponent {
|
||||
id: popoutColumn
|
||||
|
||||
headerText: "Emoji Picker"
|
||||
detailsText: "Click an emoji to copy it to clipboard"
|
||||
showCloseButton: true
|
||||
|
||||
property var allEmojis: ["😀", "😃", "😄", "😁", "😆", "😅", "🤣", "😂", "🙂", "🙃", "😉", "😊", "😇", "🥰", "😍", "🤩", "😘", "😗", "😚", "😙", "😋", "😛", "😜", "🤪", "😝", "🤑", "🤗", "🤭", "🤫", "🤔", "🤐", "🤨", "😐", "😑", "😶", "😏", "😒", "🙄", "😬", "🤥", "😌", "😔", "😪", "🤤", "😴", "😷", "🤒", "🤕", "🤢", "🤮", "❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍", "🤎", "💔", "👍", "👎", "👊", "✊", "🤛", "🤜", "🤞", "✌️", "🤟", "🤘"]
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
implicitHeight: root.popoutHeight - popoutColumn.headerHeight - popoutColumn.detailsHeight - Theme.spacingXL
|
||||
|
||||
DankGridView {
|
||||
id: emojiGrid
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: Math.floor(parent.width / 50) * 50
|
||||
height: parent.height
|
||||
clip: true
|
||||
cellWidth: 50
|
||||
cellHeight: 50
|
||||
model: popoutColumn.allEmojis
|
||||
|
||||
delegate: StyledRect {
|
||||
width: 45
|
||||
height: 45
|
||||
radius: Theme.cornerRadius
|
||||
color: emojiMouseArea.containsMouse ? Theme.surfaceContainerHighest : Theme.surfaceContainerHigh
|
||||
border.width: 0
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: emojiMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["dms", "cl", "copy", modelData]);
|
||||
ToastService.showInfo("Copied " + modelData + " to clipboard");
|
||||
popoutColumn.closePopout();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
popoutWidth: 400
|
||||
popoutHeight: 500
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginComponent {
|
||||
id: root
|
||||
|
||||
property string scriptPath: pluginData.scriptPath || ""
|
||||
property var popoutService: null
|
||||
|
||||
Connections {
|
||||
target: SessionData
|
||||
function onWallpaperPathChanged() {
|
||||
if (scriptPath) {
|
||||
var scriptProcess = scriptProcessComponent.createObject(root, {
|
||||
wallpaperPath: SessionData.wallpaperPath
|
||||
});
|
||||
scriptProcess.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: scriptProcessComponent
|
||||
|
||||
Process {
|
||||
property string wallpaperPath: ""
|
||||
|
||||
command: [scriptPath, wallpaperPath]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
console.log("CompositeDaemon script output:", text.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
ToastService.showError("Wallpaper Change Script Error", text.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
ToastService.showError("Wallpaper Change Script Error", "Script exited with code: " + exitCode);
|
||||
}
|
||||
destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "compositeExample"
|
||||
|
||||
function runHook(): string {
|
||||
if (!root.scriptPath)
|
||||
return "no script configured";
|
||||
var scriptProcess = scriptProcessComponent.createObject(root, {
|
||||
wallpaperPath: SessionData.wallpaperPath
|
||||
});
|
||||
scriptProcess.running = true;
|
||||
return "ran hook";
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
console.info("CompositeDaemon: Started monitoring wallpaper changes");
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
console.info("CompositeDaemon: Stopped monitoring wallpaper changes");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modules.Plugins
|
||||
|
||||
DesktopPluginComponent {
|
||||
id: root
|
||||
|
||||
minWidth: 120
|
||||
minHeight: 120
|
||||
|
||||
property bool showSeconds: pluginData.showSeconds ?? true
|
||||
property bool showDate: pluginData.showDate ?? true
|
||||
property string clockStyle: pluginData.clockStyle ?? "analog"
|
||||
property real backgroundOpacity: (pluginData.backgroundOpacity ?? 50) / 100
|
||||
|
||||
SystemClock {
|
||||
id: systemClock
|
||||
precision: root.showSeconds ? SystemClock.Seconds : SystemClock.Minutes
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: background
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainer
|
||||
opacity: root.backgroundOpacity
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
sourceComponent: root.clockStyle === "digital" ? digitalClock : analogClock
|
||||
}
|
||||
|
||||
Component {
|
||||
id: analogClock
|
||||
|
||||
Item {
|
||||
id: analogClockRoot
|
||||
|
||||
property real clockSize: Math.min(width, height) - (root.showDate ? 30 : 0)
|
||||
|
||||
Item {
|
||||
id: clockFace
|
||||
width: analogClockRoot.clockSize
|
||||
height: analogClockRoot.clockSize
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: 12
|
||||
|
||||
Rectangle {
|
||||
required property int index
|
||||
property real markAngle: index * 30
|
||||
property real markRadius: clockFace.width / 2 - 8
|
||||
|
||||
x: clockFace.width / 2 + markRadius * Math.sin(markAngle * Math.PI / 180) - width / 2
|
||||
y: clockFace.height / 2 - markRadius * Math.cos(markAngle * Math.PI / 180) - height / 2
|
||||
width: index % 3 === 0 ? 8 : 4
|
||||
height: width
|
||||
radius: width / 2
|
||||
color: index % 3 === 0 ? Theme.primary : Theme.outlineVariant
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: hourHand
|
||||
property int hours: systemClock.date?.getHours() % 12 ?? 0
|
||||
property int minutes: systemClock.date?.getMinutes() ?? 0
|
||||
|
||||
x: clockFace.width / 2 - width / 2
|
||||
y: clockFace.height / 2 - height + 4
|
||||
width: 6
|
||||
height: clockFace.height * 0.25
|
||||
radius: 3
|
||||
color: Theme.primary
|
||||
antialiasing: true
|
||||
transformOrigin: Item.Bottom
|
||||
rotation: (hours + minutes / 60) * 30
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: minuteHand
|
||||
property int minutes: systemClock.date?.getMinutes() ?? 0
|
||||
property int seconds: systemClock.date?.getSeconds() ?? 0
|
||||
|
||||
x: clockFace.width / 2 - width / 2
|
||||
y: clockFace.height / 2 - height + 4
|
||||
width: 4
|
||||
height: clockFace.height * 0.35
|
||||
radius: 2
|
||||
color: Theme.onSurface
|
||||
antialiasing: true
|
||||
transformOrigin: Item.Bottom
|
||||
rotation: (minutes + seconds / 60) * 6
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: secondHand
|
||||
visible: root.showSeconds
|
||||
property int seconds: systemClock.date?.getSeconds() ?? 0
|
||||
|
||||
x: clockFace.width / 2 - width / 2
|
||||
y: clockFace.height / 2 - height + 4
|
||||
width: 2
|
||||
height: clockFace.height * 0.4
|
||||
radius: 1
|
||||
color: Theme.error
|
||||
antialiasing: true
|
||||
transformOrigin: Item.Bottom
|
||||
rotation: seconds * 6
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 10
|
||||
height: 10
|
||||
radius: 5
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.showDate
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Theme.spacingXS
|
||||
text: systemClock.date?.toLocaleDateString(I18n.locale(), "ddd, MMM d") ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: digitalClock
|
||||
|
||||
Item {
|
||||
id: digitalRoot
|
||||
|
||||
property real timeFontSize: Math.min(width * 0.16, height * (root.showDate ? 0.4 : 0.5))
|
||||
property real dateFontSize: Math.max(Theme.fontSizeSmall, timeFontSize * 0.35)
|
||||
|
||||
Text {
|
||||
id: timeText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: root.showDate ? -digitalRoot.dateFontSize * 0.8 : 0
|
||||
text: systemClock.date?.toLocaleTimeString(Qt.locale(), root.showSeconds ? "hh:mm:ss" : "hh:mm") ?? ""
|
||||
font.pixelSize: digitalRoot.timeFontSize
|
||||
font.weight: Font.Bold
|
||||
font.family: "monospace"
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
Text {
|
||||
id: dateText
|
||||
visible: root.showDate
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: timeText.bottom
|
||||
anchors.topMargin: Theme.spacingXS
|
||||
text: systemClock.date?.toLocaleDateString(I18n.locale(), "ddd, MMM d") ?? ""
|
||||
font.pixelSize: digitalRoot.dateFontSize
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginSettings {
|
||||
id: root
|
||||
pluginId: "exampleComposite"
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: "Bar Widget — Emoji Cycler"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
SelectionSetting {
|
||||
settingKey: "emojiSet"
|
||||
label: "Emoji Set"
|
||||
description: "Choose which collection of emojis to cycle through"
|
||||
options: [
|
||||
{
|
||||
label: "Happy & Sad",
|
||||
value: "happySad"
|
||||
},
|
||||
{
|
||||
label: "Hearts",
|
||||
value: "hearts"
|
||||
},
|
||||
{
|
||||
label: "Hand Gestures",
|
||||
value: "hands"
|
||||
},
|
||||
{
|
||||
label: "All Mixed",
|
||||
value: "mixed"
|
||||
}
|
||||
]
|
||||
defaultValue: "happySad"
|
||||
|
||||
onValueChanged: {
|
||||
const sets = {
|
||||
"happySad": ["😊", "😢", "😂", "😭", "😍", "😡"],
|
||||
"hearts": ["❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍"],
|
||||
"hands": ["👍", "👎", "👊", "✌️", "🤘", "👌", "✋", "🤚"],
|
||||
"mixed": ["😊", "❤️", "👍", "🎉", "🔥", "✨", "🌟", "💯"]
|
||||
};
|
||||
root.saveValue("emojis", sets[value] || sets["happySad"]);
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
const currentSet = value || defaultValue;
|
||||
const sets = {
|
||||
"happySad": ["😊", "😢", "😂", "😭", "😍", "😡"],
|
||||
"hearts": ["❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍"],
|
||||
"hands": ["👍", "👎", "👊", "✌️", "🤘", "👌", "✋", "🤚"],
|
||||
"mixed": ["😊", "❤️", "👍", "🎉", "🔥", "✨", "🌟", "💯"]
|
||||
};
|
||||
root.saveValue("emojis", sets[currentSet] || sets["happySad"]);
|
||||
}
|
||||
}
|
||||
|
||||
SliderSetting {
|
||||
settingKey: "cycleInterval"
|
||||
label: "Cycle Speed"
|
||||
description: "How quickly emojis rotate"
|
||||
defaultValue: 3000
|
||||
minimum: 500
|
||||
maximum: 10000
|
||||
unit: "ms"
|
||||
leftIcon: "schedule"
|
||||
}
|
||||
|
||||
SliderSetting {
|
||||
settingKey: "maxBarEmojis"
|
||||
label: "Max Bar Emojis"
|
||||
description: "Maximum number of emojis to display in the bar at once"
|
||||
defaultValue: 3
|
||||
minimum: 1
|
||||
maximum: 8
|
||||
rightIcon: "emoji_emotions"
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: "Desktop Widget — Clock"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
SelectionSetting {
|
||||
settingKey: "clockStyle"
|
||||
label: "Clock Style"
|
||||
options: [
|
||||
{
|
||||
label: "Analog",
|
||||
value: "analog"
|
||||
},
|
||||
{
|
||||
label: "Digital",
|
||||
value: "digital"
|
||||
}
|
||||
]
|
||||
defaultValue: "analog"
|
||||
}
|
||||
|
||||
ToggleSetting {
|
||||
settingKey: "showSeconds"
|
||||
label: "Show Seconds"
|
||||
defaultValue: true
|
||||
}
|
||||
|
||||
ToggleSetting {
|
||||
settingKey: "showDate"
|
||||
label: "Show Date"
|
||||
defaultValue: true
|
||||
}
|
||||
|
||||
SliderSetting {
|
||||
settingKey: "backgroundOpacity"
|
||||
label: "Background Opacity"
|
||||
defaultValue: 50
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
unit: "%"
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: "Daemon — Wallpaper Hook"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StringSetting {
|
||||
settingKey: "scriptPath"
|
||||
label: "Script Path"
|
||||
description: "Script executed when the wallpaper changes. The new wallpaper path is passed as the first argument."
|
||||
placeholder: "/path/to/your/script.sh"
|
||||
defaultValue: ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
# Composite Example
|
||||
|
||||
A single plugin that provides **all three surfaces at once** by combining three of
|
||||
the standalone example plugins:
|
||||
|
||||
| Surface | Source example | File |
|
||||
|---------|----------------|------|
|
||||
| `daemon` | WallpaperWatcherDaemon | `CompositeDaemon.qml` |
|
||||
| `widget` | Emoji Cycler (bar widget + popout) | `CompositeBarWidget.qml` |
|
||||
| `desktop` | Desktop Clock | `CompositeDesktopWidget.qml` |
|
||||
|
||||
It demonstrates the `components` manifest map, where each surface points at its own
|
||||
QML file:
|
||||
|
||||
```json
|
||||
"type": "composite",
|
||||
"components": {
|
||||
"daemon": "./CompositeDaemon.qml",
|
||||
"widget": "./CompositeBarWidget.qml",
|
||||
"desktop": "./CompositeDesktopWidget.qml"
|
||||
}
|
||||
```
|
||||
|
||||
All surfaces share one settings UI (`CompositeSettings.qml`) and one plugin-settings
|
||||
namespace (`exampleComposite`), so `pluginData` is the same for every surface.
|
||||
|
||||
## Surfaces
|
||||
|
||||
- **Daemon** — watches `SessionData.wallpaperPath` and runs a user-configured script
|
||||
on change. Also registers an `IpcHandler` (`target: "compositeExample"`) exposing a
|
||||
`runHook` call, so you can trigger the hook over IPC.
|
||||
- **Bar widget** — cycles emojis in the bar; click the pill for an emoji picker popout
|
||||
that copies to the clipboard.
|
||||
- **Desktop widget** — an analog/digital clock you can drag and resize on the desktop.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Copy this directory into `$CONFIGPATH/DankMaterialShell/plugins/`.
|
||||
2. Settings → Plugins → **Scan for Plugins**, then enable **Composite Example**.
|
||||
(Composite plugins respect the enable toggle — unlike a pure `desktop` plugin they
|
||||
do not auto-load, because they also carry a daemon.)
|
||||
3. Add the bar widget via Settings → Appearance → DankBar Layout.
|
||||
4. Place the desktop clock via Settings → Desktop Widgets.
|
||||
|
||||
## Notes
|
||||
|
||||
- The daemon surface is instantiated once and lives for as long as the plugin is
|
||||
enabled. The bar and desktop surfaces are instantiated per bar/placement per screen.
|
||||
- Cross-surface runtime state (not needed here) is best shared via
|
||||
`PluginService.getGlobalVar` / `setGlobalVar` or the daemon instance, since each
|
||||
surface is a separate object.
|
||||
- `requires_dms` is `>=1.5.0` because the `components` multi-surface manifest is only
|
||||
understood by DMS 1.5.0 and later.
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"id": "exampleComposite",
|
||||
"name": "Composite Example",
|
||||
"description": "One plugin providing all three surfaces at once: a wallpaper-watcher daemon, an emoji bar widget with popout, and a desktop clock",
|
||||
"version": "1.0.0",
|
||||
"author": "DankMaterialShell",
|
||||
"type": "composite",
|
||||
"capabilities": ["daemon", "dankbar-widget", "desktop-widget", "clipboard"],
|
||||
"icon": "extension",
|
||||
"components": {
|
||||
"daemon": "./CompositeDaemon.qml",
|
||||
"widget": "./CompositeBarWidget.qml",
|
||||
"desktop": "./CompositeDesktopWidget.qml"
|
||||
},
|
||||
"settings": "./CompositeSettings.qml",
|
||||
"requires_dms": ">=1.5.0",
|
||||
"permissions": [
|
||||
"settings_read",
|
||||
"settings_write"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user