1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-05 21:15:38 -05:00

Abstract away plugin dev a little more

This commit is contained in:
bbedward
2025-10-01 17:47:39 -04:00
parent df9e834309
commit 0ca12d275c
19 changed files with 1447 additions and 230 deletions

View File

@@ -22,6 +22,7 @@ import qs.Modules.ProcessList
import qs.Modules.Settings
import qs.Modules.DankBar
import qs.Modules.DankBar.Popouts
import qs.Modules.Plugins
import qs.Services

View File

@@ -9,6 +9,10 @@ Item {
property var components: null
property bool noBackground: false
required property var axis
property string section: "center"
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
readonly property bool isVertical: axis?.isVertical ?? false
readonly property real spacing: noBackground ? 2 : Theme.spacingXS
@@ -371,7 +375,24 @@ Item {
item.axis = root.axis
}
if (root.axis && "isVertical" in item) {
item.isVertical = root.axis.isVertical
try {
item.isVertical = root.axis.isVertical
} catch (e) {
}
}
// Inject properties for plugin widgets
if ("section" in item) {
item.section = root.section
}
if ("parentScreen" in item) {
item.parentScreen = root.parentScreen
}
if ("widgetThickness" in item) {
item.widgetThickness = root.widgetThickness
}
if ("barThickness" in item) {
item.barThickness = root.barThickness
}
// Inject PluginService for plugin widgets

View File

@@ -537,6 +537,9 @@ Item {
widgetsModel: SettingsData.dankBarLeftWidgetsModel
components: topBarContent.allComponents
noBackground: SettingsData.dankBarNoBackground
parentScreen: barWindow.screen
widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness
}
RightSection {
@@ -549,6 +552,9 @@ Item {
widgetsModel: SettingsData.dankBarRightWidgetsModel
components: topBarContent.allComponents
noBackground: SettingsData.dankBarNoBackground
parentScreen: barWindow.screen
widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness
}
CenterSection {
@@ -561,6 +567,9 @@ Item {
widgetsModel: SettingsData.dankBarCenterWidgetsModel
components: topBarContent.allComponents
noBackground: SettingsData.dankBarNoBackground
parentScreen: barWindow.screen
widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness
}
}
@@ -580,6 +589,9 @@ Item {
widgetsModel: SettingsData.dankBarLeftWidgetsModel
components: topBarContent.allComponents
noBackground: SettingsData.dankBarNoBackground
parentScreen: barWindow.screen
widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness
}
CenterSection {
@@ -593,6 +605,9 @@ Item {
widgetsModel: SettingsData.dankBarCenterWidgetsModel
components: topBarContent.allComponents
noBackground: SettingsData.dankBarNoBackground
parentScreen: barWindow.screen
widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness
}
RightSection {

View File

@@ -8,6 +8,9 @@ Item {
property var components: null
property bool noBackground: false
required property var axis
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
readonly property bool isVertical: axis?.isVertical ?? false
@@ -38,6 +41,10 @@ Item {
components: root.components
isInColumn: false
axis: root.axis
section: "left"
parentScreen: root.parentScreen
widgetThickness: root.widgetThickness
barThickness: root.barThickness
}
}
}
@@ -63,6 +70,10 @@ Item {
components: root.components
isInColumn: true
axis: root.axis
section: "left"
parentScreen: root.parentScreen
widgetThickness: root.widgetThickness
barThickness: root.barThickness
}
}
}

View File

@@ -8,6 +8,9 @@ Item {
property var components: null
property bool noBackground: false
required property var axis
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
readonly property bool isVertical: axis?.isVertical ?? false
@@ -40,6 +43,10 @@ Item {
components: root.components
isInColumn: false
axis: root.axis
section: "right"
parentScreen: root.parentScreen
widgetThickness: root.widgetThickness
barThickness: root.barThickness
}
}
}
@@ -65,6 +72,10 @@ Item {
components: root.components
isInColumn: true
axis: root.axis
section: "right"
parentScreen: root.parentScreen
widgetThickness: root.widgetThickness
barThickness: root.barThickness
}
}
}

View File

@@ -11,6 +11,10 @@ Loader {
property var components: null
property bool isInColumn: false
property var axis: null
property string section: "center"
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
asynchronous: false
@@ -21,20 +25,59 @@ Loader {
signal contentItemReady(var item)
Binding {
target: root.item
when: root.item && "parentScreen" in root.item
property: "parentScreen"
value: root.parentScreen
restoreMode: Binding.RestoreNone
}
Binding {
target: root.item
when: root.item && "section" in root.item
property: "section"
value: root.section
restoreMode: Binding.RestoreNone
}
Binding {
target: root.item
when: root.item && "widgetThickness" in root.item
property: "widgetThickness"
value: root.widgetThickness
restoreMode: Binding.RestoreNone
}
Binding {
target: root.item
when: root.item && "barThickness" in root.item
property: "barThickness"
value: root.barThickness
restoreMode: Binding.RestoreNone
}
Binding {
target: root.item
when: root.item && "axis" in root.item
property: "axis"
value: root.axis
restoreMode: Binding.RestoreNone
}
onLoaded: {
if (item) {
contentItemReady(item)
if (widgetId === "spacer") {
item.spacerSize = Qt.binding(() => spacerSize)
}
if (axis && "axis" in item) {
item.axis = axis
}
if (axis && "isVertical" in item) {
item.isVertical = axis.isVertical
try {
item.isVertical = axis.isVertical
} catch (e) {
}
}
// Inject PluginService for plugin widgets
if (item.pluginService !== undefined) {
console.log("WidgetHost: Injecting PluginService into plugin widget:", widgetId)
item.pluginService = PluginService

View File

@@ -0,0 +1,53 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property var axis: null
property string section: "center"
property var popoutTarget: null
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
property alias content: contentLoader.sourceComponent
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
signal clicked()
width: contentLoader.item ? (contentLoader.item.implicitWidth + horizontalPadding * 2) : 0
height: widgetThickness
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
color: {
if (SettingsData.dankBarNoBackground) {
return "transparent"
}
const baseColor = mouseArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
}
Loader {
id: contentLoader
anchors.centerIn: parent
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
if (popoutTarget && popoutTarget.setTriggerPosition) {
const globalPos = mapToGlobal(0, 0)
const currentScreen = parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, width)
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
}
root.clicked()
}
}
}

View File

@@ -0,0 +1,53 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property var axis: null
property string section: "center"
property var popoutTarget: null
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
property alias content: contentLoader.sourceComponent
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetThickness / 30))
signal clicked()
width: widgetThickness
height: contentLoader.item ? (contentLoader.item.implicitHeight + horizontalPadding * 2) : 0
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
color: {
if (SettingsData.dankBarNoBackground) {
return "transparent"
}
const baseColor = mouseArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
}
Loader {
id: contentLoader
anchors.centerIn: parent
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
if (popoutTarget && popoutTarget.setTriggerPosition) {
const globalPos = mapToGlobal(0, 0)
const currentScreen = parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, height)
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
}
root.clicked()
}
}
}

View File

@@ -0,0 +1,131 @@
import QtQuick
import qs.Common
import qs.Widgets
Column {
id: root
required property string settingKey
required property string label
property string description: ""
property var items: []
property Component delegate: null
width: parent.width
spacing: Theme.spacingM
Component.onCompleted: {
const settings = findSettings()
if (settings) {
items = settings.loadValue(settingKey, [])
}
}
onItemsChanged: {
const settings = findSettings()
if (settings) {
settings.saveValue(settingKey, items)
}
}
function findSettings() {
let item = parent
while (item) {
if (item.saveValue !== undefined && item.loadValue !== undefined) {
return item
}
item = item.parent
}
return null
}
function addItem(item) {
items = items.concat([item])
}
function removeItem(index) {
const newItems = items.slice()
newItems.splice(index, 1)
items = newItems
}
StyledText {
text: root.label
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: root.description
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
visible: root.description !== ""
}
Column {
width: parent.width
spacing: Theme.spacingS
Repeater {
model: root.items
delegate: root.delegate ? root.delegate : defaultDelegate
}
StyledText {
text: "No items added yet"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: root.items.length === 0
}
}
Component {
id: defaultDelegate
StyledRect {
width: parent.width
height: 40
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.width: 0
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
text: modelData
color: Theme.surfaceText
}
Rectangle {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
width: 60
height: 28
color: removeArea.containsMouse ? Theme.errorHover : Theme.error
radius: Theme.cornerRadius
StyledText {
anchors.centerIn: parent
text: "Remove"
color: Theme.errorText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
}
MouseArea {
id: removeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.removeItem(index)
}
}
}
}
}
}

View File

@@ -0,0 +1,230 @@
import QtQuick
import qs.Common
import qs.Widgets
Column {
id: root
required property string settingKey
required property string label
property string description: ""
property var fields: []
property var items: []
width: parent.width
spacing: Theme.spacingM
Component.onCompleted: {
const settings = findSettings()
if (settings) {
items = settings.loadValue(settingKey, [])
}
}
onItemsChanged: {
const settings = findSettings()
if (settings) {
settings.saveValue(settingKey, items)
}
}
function findSettings() {
let item = parent
while (item) {
if (item.saveValue !== undefined && item.loadValue !== undefined) {
return item
}
item = item.parent
}
return null
}
function addItem(item) {
items = items.concat([item])
}
function removeItem(index) {
const newItems = items.slice()
newItems.splice(index, 1)
items = newItems
}
StyledText {
text: root.label
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: root.description
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
visible: root.description !== ""
}
Flow {
width: parent.width
spacing: Theme.spacingS
Repeater {
model: root.fields
StyledText {
text: modelData.label
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
width: modelData.width || 200
}
}
}
Flow {
id: inputRow
width: parent.width
spacing: Theme.spacingS
property var inputFields: []
Repeater {
id: inputRepeater
model: root.fields
DankTextField {
width: modelData.width || 200
placeholderText: modelData.placeholder || ""
Component.onCompleted: {
inputRow.inputFields.push(this)
}
Keys.onReturnPressed: {
addButton.clicked()
}
}
}
DankButton {
id: addButton
width: 50
height: 36
text: "Add"
onClicked: {
let newItem = {}
let hasValue = false
for (let i = 0; i < root.fields.length; i++) {
const field = root.fields[i]
const input = inputRow.inputFields[i]
const value = input.text.trim()
if (value !== "") {
hasValue = true
}
if (field.required && value === "") {
return
}
newItem[field.id] = value || (field.default || "")
}
if (hasValue) {
root.addItem(newItem)
for (let i = 0; i < inputRow.inputFields.length; i++) {
inputRow.inputFields[i].text = ""
}
if (inputRow.inputFields.length > 0) {
inputRow.inputFields[0].forceActiveFocus()
}
}
}
}
}
StyledText {
text: "Current Items"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
visible: root.items.length > 0
}
Column {
width: parent.width
spacing: Theme.spacingS
Repeater {
model: root.items
StyledRect {
width: parent.width
height: 40
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.width: 0
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Repeater {
model: root.fields
StyledText {
text: {
const value = root.items[index][modelData.id]
return value || ""
}
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
width: modelData.width || 200
elide: Text.ElideRight
}
}
}
Rectangle {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
width: 60
height: 28
color: removeArea.containsMouse ? Theme.errorHover : Theme.error
radius: Theme.cornerRadius
StyledText {
anchors.centerIn: parent
text: "Remove"
color: Theme.errorText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
}
MouseArea {
id: removeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.removeItem(index)
}
}
}
}
}
StyledText {
text: "No items added yet"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: root.items.length === 0
}
}
}

View File

@@ -0,0 +1,69 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property var axis: null
property string section: "center"
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
property Component horizontalBarPill: null
property Component verticalBarPill: null
property Component popoutContent: null
property real popoutWidth: 400
property real popoutHeight: 400
readonly property bool isVertical: axis?.isVertical ?? false
readonly property bool hasHorizontalPill: horizontalBarPill !== null
readonly property bool hasVerticalPill: verticalBarPill !== null
readonly property bool hasPopout: popoutContent !== null
width: isVertical ? (hasVerticalPill ? verticalPill.width : 0) : (hasHorizontalPill ? horizontalPill.width : 0)
height: isVertical ? (hasVerticalPill ? verticalPill.height : 0) : (hasHorizontalPill ? horizontalPill.height : 0)
BaseHorizontalPill {
id: horizontalPill
visible: !isVertical && hasHorizontalPill
axis: root.axis
section: root.section
popoutTarget: hasPopout ? pluginPopout : null
parentScreen: root.parentScreen
widgetThickness: root.widgetThickness
barThickness: root.barThickness
content: root.horizontalBarPill
onClicked: {
if (hasPopout) {
pluginPopout.toggle()
}
}
}
BaseVerticalPill {
id: verticalPill
visible: isVertical && hasVerticalPill
axis: root.axis
section: root.section
popoutTarget: hasPopout ? pluginPopout : null
parentScreen: root.parentScreen
widgetThickness: root.widgetThickness
barThickness: root.barThickness
content: root.verticalBarPill
onClicked: {
if (hasPopout) {
pluginPopout.toggle()
}
}
}
PluginPopout {
id: pluginPopout
contentWidth: root.popoutWidth
contentHeight: root.popoutHeight
pluginContent: root.popoutContent
}
}

View File

@@ -0,0 +1,116 @@
import QtQuick
import qs.Common
import qs.Widgets
DankPopout {
id: root
property var triggerScreen: null
property Component pluginContent: null
property real contentWidth: 400
property real contentHeight: 400
function setTriggerPosition(x, y, width, section, screen) {
triggerX = x
triggerY = y
triggerWidth = width
triggerSection = section
triggerScreen = screen
}
popupWidth: contentWidth
popupHeight: popoutContent.item ? popoutContent.item.implicitHeight : contentHeight
screen: triggerScreen
shouldBeVisible: false
visible: shouldBeVisible
content: Component {
Rectangle {
id: popoutContainer
implicitHeight: popoutColumn.implicitHeight + Theme.spacingL * 2
color: Theme.popupBackground()
radius: Theme.cornerRadius
border.width: 0
antialiasing: true
smooth: true
focus: true
Component.onCompleted: {
if (root.shouldBeVisible) {
forceActiveFocus()
}
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
root.close()
event.accepted = true
}
}
Connections {
target: root
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) {
Qt.callLater(() => {
popoutContainer.forceActiveFocus()
})
}
}
}
Column {
id: popoutColumn
width: parent.width - Theme.spacingL * 2
anchors.left: parent.left
anchors.top: parent.top
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
Row {
width: parent.width
height: 32
visible: closeButton.visible
Item {
width: parent.width - 32
height: 32
}
Rectangle {
id: closeButton
width: 32
height: 32
radius: 16
color: closeArea.containsMouse ? Theme.errorHover : "transparent"
visible: true
DankIcon {
anchors.centerIn: parent
name: "close"
size: Theme.iconSize - 4
color: closeArea.containsMouse ? Theme.error : Theme.surfaceText
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
root.close()
}
}
}
}
Loader {
id: popoutContent
width: parent.width
sourceComponent: root.pluginContent
}
}
}
}
}

View File

@@ -0,0 +1,33 @@
import QtQuick
import qs.Common
import qs.Services
Item {
id: root
required property string pluginId
property var pluginService: null
default property alias content: settingsColumn.children
implicitHeight: settingsColumn.implicitHeight
height: implicitHeight
function saveValue(key, value) {
if (pluginService && pluginService.savePluginData) {
pluginService.savePluginData(pluginId, key, value)
}
}
function loadValue(key, defaultValue) {
if (pluginService && pluginService.loadPluginData) {
return pluginService.loadPluginData(pluginId, key, defaultValue)
}
return defaultValue
}
Column {
id: settingsColumn
width: parent.width
spacing: Theme.spacingM
}
}

View File

@@ -0,0 +1,113 @@
import QtQuick
import qs.Common
import qs.Widgets
Column {
id: root
required property string settingKey
required property string label
property string description: ""
required property var options
property string defaultValue: ""
property string value: defaultValue
width: parent.width
spacing: Theme.spacingS
readonly property var optionLabels: {
const labels = []
for (let i = 0; i < options.length; i++) {
labels.push(options[i].label || options[i])
}
return labels
}
readonly property var valueToLabel: {
const map = {}
for (let i = 0; i < options.length; i++) {
const opt = options[i]
if (typeof opt === 'object') {
map[opt.value] = opt.label
} else {
map[opt] = opt
}
}
return map
}
readonly property var labelToValue: {
const map = {}
for (let i = 0; i < options.length; i++) {
const opt = options[i]
if (typeof opt === 'object') {
map[opt.label] = opt.value
} else {
map[opt] = opt
}
}
return map
}
Component.onCompleted: {
const settings = findSettings()
if (settings) {
value = settings.loadValue(settingKey, defaultValue)
}
}
onValueChanged: {
const settings = findSettings()
if (settings) {
settings.saveValue(settingKey, value)
}
}
function findSettings() {
let item = parent
while (item) {
if (item.saveValue !== undefined && item.loadValue !== undefined) {
return item
}
item = item.parent
}
return null
}
Row {
width: parent.width
spacing: Theme.spacingM
Column {
width: parent.width * 0.4
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: root.label
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: root.description
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
visible: root.description !== ""
}
}
DankDropdown {
width: parent.width * 0.6 - Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
currentValue: root.valueToLabel[root.value] || root.value
options: root.optionLabels
onValueChanged: newValue => {
root.value = root.labelToValue[newValue] || newValue
}
}
}
}

View File

@@ -0,0 +1,65 @@
import QtQuick
import qs.Common
import qs.Widgets
Column {
id: root
required property string settingKey
required property string label
property string description: ""
property string placeholder: ""
property string defaultValue: ""
property string value: defaultValue
width: parent.width
spacing: Theme.spacingS
Component.onCompleted: {
const settings = findSettings()
if (settings) {
value = settings.loadValue(settingKey, defaultValue)
textField.text = value
}
}
function findSettings() {
let item = parent
while (item) {
if (item.saveValue !== undefined && item.loadValue !== undefined) {
return item
}
item = item.parent
}
return null
}
StyledText {
text: root.label
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: root.description
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
visible: root.description !== ""
}
DankTextField {
id: textField
width: parent.width
placeholderText: root.placeholder
onEditingFinished: {
root.value = text
const settings = findSettings()
if (settings) {
settings.saveValue(settingKey, text)
}
}
}
}

View File

@@ -0,0 +1,72 @@
import QtQuick
import qs.Common
import qs.Widgets
Row {
id: root
required property string settingKey
required property string label
property string description: ""
property bool defaultValue: false
property bool value: defaultValue
width: parent.width
spacing: Theme.spacingM
Component.onCompleted: {
const settings = findSettings()
if (settings) {
value = settings.loadValue(settingKey, defaultValue)
}
}
onValueChanged: {
const settings = findSettings()
if (settings) {
settings.saveValue(settingKey, value)
}
}
function findSettings() {
let item = parent
while (item) {
if (item.saveValue !== undefined && item.loadValue !== undefined) {
return item
}
item = item.parent
}
return null
}
Column {
width: parent.width - toggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: root.label
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: root.description
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
visible: root.description !== ""
}
}
DankToggle {
id: toggle
anchors.verticalCenter: parent.verticalCenter
checked: root.value
onToggled: isChecked => {
root.value = isChecked
}
}
}

View File

@@ -160,21 +160,17 @@ Item {
font.weight: Font.Medium
}
Item {
Column {
width: parent.width
height: Math.max(150, pluginListView.contentHeight)
spacing: Theme.spacingM
ListView {
id: pluginListView
anchors.fill: parent
Repeater {
id: pluginRepeater
model: PluginService.getAvailablePlugins()
spacing: Theme.spacingM
clip: true
delegate: StyledRect {
StyledRect {
id: pluginDelegate
width: pluginListView.width
width: parent.width
height: pluginItemColumn.implicitHeight + Theme.spacingM * 2 + settingsContainer.height
radius: Theme.cornerRadius
@@ -194,23 +190,9 @@ Item {
console.log("Plugin", pluginId, "isExpanded changed to:", isExpanded)
}
color: pluginMouseArea.containsMouse ? Theme.surfacePressed : (isExpanded ? Theme.surfaceContainerHighest : Theme.surfaceContainer)
color: pluginMouseArea.containsMouse ? Theme.surfacePressed : (isExpanded ? Theme.surfaceContainerHighest : Theme.surfaceContainerHigh)
border.width: 0
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
MouseArea {
id: pluginMouseArea
anchors.fill: parent
@@ -238,7 +220,7 @@ Item {
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
spacing: Theme.spacingM
Row {
width: parent.width
@@ -252,9 +234,9 @@ Item {
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM - pluginToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
spacing: 2
width: parent.width - 250
Row {
spacing: Theme.spacingXS
@@ -262,7 +244,7 @@ Item {
StyledText {
text: pluginDelegate.pluginName
font.pixelSize: Theme.fontSizeMedium
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
@@ -274,13 +256,6 @@ Item {
color: pluginDelegate.hasSettings ? Theme.primary : "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: pluginDelegate.hasSettings
Behavior on rotation {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
@@ -289,16 +264,11 @@ Item {
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
elide: Text.ElideRight
}
}
Item {
width: 10
height: 1
}
DankToggle {
id: pluginToggle
anchors.verticalCenter: parent.verticalCenter
checked: PluginService.isPluginLoaded(pluginDelegate.pluginId)
onToggled: (isChecked) => {
@@ -367,20 +337,9 @@ Item {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: pluginDelegate.isExpanded && pluginDelegate.hasSettings ? Math.min(500, settingsLoader.item ? settingsLoader.item.implicitHeight + Theme.spacingL * 2 : 200) : 0
height: pluginDelegate.isExpanded && pluginDelegate.hasSettings ? (settingsLoader.item ? settingsLoader.item.implicitHeight + Theme.spacingL * 2 : 0) : 0
clip: true
onHeightChanged: {
console.log("Settings container height changed:", height, "for plugin:", pluginDelegate.pluginId)
}
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
Rectangle {
anchors.fill: parent
color: Theme.surfaceContainerHighest
@@ -389,78 +348,61 @@ Item {
border.width: 0
}
DankFlickable {
Loader {
id: settingsLoader
anchors.fill: parent
anchors.margins: Theme.spacingL
contentHeight: settingsLoader.height
contentWidth: width
clip: true
active: pluginDelegate.isExpanded && pluginDelegate.hasSettings && PluginService.isPluginLoaded(pluginDelegate.pluginId)
asynchronous: false
Loader {
id: settingsLoader
width: parent.width
active: pluginDelegate.isExpanded && pluginDelegate.hasSettings && PluginService.isPluginLoaded(pluginDelegate.pluginId)
asynchronous: false
onActiveChanged: {
console.log("Settings loader active changed to:", active, "for plugin:", pluginDelegate.pluginId,
"isExpanded:", pluginDelegate.isExpanded, "hasSettings:", pluginDelegate.hasSettings,
"isLoaded:", PluginService.isPluginLoaded(pluginDelegate.pluginId))
}
onActiveChanged: {
console.log("Settings loader active changed to:", active, "for plugin:", pluginDelegate.pluginId,
"isExpanded:", pluginDelegate.isExpanded, "hasSettings:", pluginDelegate.hasSettings,
"isLoaded:", PluginService.isPluginLoaded(pluginDelegate.pluginId))
}
source: {
if (active && pluginDelegate.pluginSettingsPath) {
console.log("Loading plugin settings from:", pluginDelegate.pluginSettingsPath)
var path = pluginDelegate.pluginSettingsPath
if (!path.startsWith("file://")) {
path = "file://" + path
}
return path
source: {
if (active && pluginDelegate.pluginSettingsPath) {
console.log("Loading plugin settings from:", pluginDelegate.pluginSettingsPath)
var path = pluginDelegate.pluginSettingsPath
if (!path.startsWith("file://")) {
path = "file://" + path
}
return ""
return path
}
return ""
}
onStatusChanged: {
console.log("Settings loader status changed:", status, "for plugin:", pluginDelegate.pluginId)
if (status === Loader.Error) {
console.error("Failed to load plugin settings:", pluginDelegate.pluginSettingsPath)
} else if (status === Loader.Ready) {
console.log("Settings successfully loaded for plugin:", pluginDelegate.pluginId)
onStatusChanged: {
console.log("Settings loader status changed:", status, "for plugin:", pluginDelegate.pluginId)
if (status === Loader.Error) {
console.error("Failed to load plugin settings:", pluginDelegate.pluginSettingsPath)
} else if (status === Loader.Ready) {
console.log("Settings successfully loaded for plugin:", pluginDelegate.pluginId)
}
}
onLoaded: {
if (item) {
console.log("Plugin settings loaded for:", pluginDelegate.pluginId)
if (typeof PluginService !== "undefined") {
console.log("Making PluginService available to plugin settings")
console.log("PluginService functions available:",
"savePluginData" in PluginService,
"loadPluginData" in PluginService)
item.pluginService = PluginService
console.log("PluginService assignment completed, item.pluginService:", item.pluginService !== null)
} else {
console.error("PluginService not available in PluginsTab context")
}
}
onLoaded: {
if (item) {
console.log("Plugin settings loaded for:", pluginDelegate.pluginId)
// Make PluginService available to the loaded component
if (typeof PluginService !== "undefined") {
console.log("Making PluginService available to plugin settings")
console.log("PluginService functions available:",
"savePluginData" in PluginService,
"loadPluginData" in PluginService)
item.pluginService = PluginService
console.log("PluginService assignment completed, item.pluginService:", item.pluginService !== null)
} else {
console.error("PluginService not available in PluginsTab context")
}
// Connect to height changes for dynamic resizing
if (item.implicitHeightChanged) {
item.implicitHeightChanged.connect(function() {
console.log("Plugin settings height changed:", item.implicitHeight)
})
}
// Force load timezones for WorldClock plugin
if (item.loadTimezones) {
console.log("Calling loadTimezones for WorldClock plugin")
item.loadTimezones()
}
// Generic initialization for any plugin
if (item.initializeSettings) {
item.initializeSettings()
}
if (item.loadTimezones) {
console.log("Calling loadTimezones for WorldClock plugin")
item.loadTimezones()
}
if (item.initializeSettings) {
item.initializeSettings()
}
}
}
@@ -482,12 +424,12 @@ Item {
}
StyledText {
anchors.centerIn: parent
width: parent.width
text: "No plugins found.\nPlace plugins in " + PluginService.pluginDirectory
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
visible: pluginListView.model.length === 0
visible: pluginRepeater.model.length === 0
}
}
}
@@ -498,10 +440,10 @@ Item {
Connections {
target: PluginService
function onPluginLoaded() {
pluginListView.model = PluginService.getAvailablePlugins()
pluginRepeater.model = PluginService.getAvailablePlugins()
}
function onPluginUnloaded() {
pluginListView.model = PluginService.getAvailablePlugins()
pluginRepeater.model = PluginService.getAvailablePlugins()
if (pluginsTab.expandedPluginId !== "" && !PluginService.isPluginLoaded(pluginsTab.expandedPluginId)) {
pluginsTab.expandedPluginId = ""
}

View File

@@ -22,6 +22,8 @@ Singleton {
}
return configDirStr + "/DankMaterialShell/plugins"
}
property string systemPluginDirectory: "/etc/xdg/quickshell/dms-plugins"
property var pluginDirectories: [pluginDirectory, systemPluginDirectory]
signal pluginLoaded(string pluginId)
signal pluginUnloaded(string pluginId)
@@ -35,37 +37,54 @@ Singleton {
scanPlugins()
}
property int currentScanIndex: 0
property var scanResults: []
property var lsProcess: Process {
id: dirScanner
stdout: StdioCollector {
onStreamFinished: {
var output = text.trim()
var currentDir = pluginDirectories[currentScanIndex]
if (output) {
var directories = output.split('\n')
for (var i = 0; i < directories.length; i++) {
var dir = directories[i].trim()
if (dir) {
var manifestPath = pluginDirectory + "/" + dir + "/plugin.json"
var manifestPath = currentDir + "/" + dir + "/plugin.json"
console.log("PluginService: Found plugin directory:", dir, "checking manifest at:", manifestPath)
loadPluginManifest(manifestPath)
}
}
} else {
console.log("PluginService: No directories found in plugin directory")
console.log("PluginService: No directories found in:", currentDir)
}
}
}
onExited: function(exitCode) {
if (exitCode !== 0) {
console.error("PluginService: Failed to scan plugin directory, exit code:", exitCode)
console.log("PluginService: Directory scan failed for:", pluginDirectories[currentScanIndex], "exit code:", exitCode)
}
currentScanIndex++
if (currentScanIndex < pluginDirectories.length) {
scanNextDirectory()
} else {
currentScanIndex = 0
}
}
}
function scanPlugins() {
lsProcess.command = ["find", pluginDirectory, "-maxdepth", "1", "-type", "d", "-not", "-path", pluginDirectory, "-exec", "basename", "{}", ";"]
currentScanIndex = 0
scanNextDirectory()
}
function scanNextDirectory() {
var dir = pluginDirectories[currentScanIndex]
console.log("PluginService: Scanning directory:", dir)
lsProcess.command = ["find", "-L", dir, "-maxdepth", "1", "-type", "d", "-not", "-path", dir, "-exec", "basename", "{}", ";"]
lsProcess.running = true
}

View File

@@ -93,94 +93,301 @@ The manifest file defines plugin metadata and configuration:
### Widget Component
The main widget component is displayed in the DankBar. It receives several properties from the shell:
The main widget component uses the **PluginComponent** wrapper which provides automatic property injection and bar integration:
```qml
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
Rectangle {
id: root
PluginComponent {
// Define horizontal bar pill (optional)
horizontalBarPill: Component {
StyledRect {
width: content.implicitWidth + Theme.spacingM * 2
height: parent.widgetThickness
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
// Standard properties provided by DankBar
property bool compactMode: false
property string section: "center" // "left", "center", or "right"
property var popupTarget: null
property var parentScreen: null
property real barHeight: 48
property real widgetHeight: 30
// Widget dimensions
width: content.implicitWidth + horizontalPadding * 2
height: widgetHeight
// PluginService is injected by PluginsTab when loading settings
property var pluginService
// Access plugin data
Component.onCompleted: {
if (pluginService) {
var savedData = pluginService.loadPluginData("yourPlugin", "dataKey", defaultValue)
StyledText {
id: content
anchors.centerIn: parent
text: "Hello World"
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
}
}
}
// Save plugin data
function saveData(key, value) {
if (pluginService) {
pluginService.savePluginData("yourPlugin", key, value)
// Define vertical bar pill (optional)
verticalBarPill: Component {
// Same as horizontal but optimized for vertical layout
}
// Define popout content (optional)
popoutContent: Component {
Column {
width: parent.width
spacing: Theme.spacingM
padding: Theme.spacingM
StyledText {
text: "Popout Content"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
}
}
}
// Popout dimensions (required if popoutContent is set)
popoutWidth: 400
popoutHeight: 300
}
```
**Important Properties:**
- `compactMode`: Whether the bar is in compact display mode
- `section`: Which bar section the widget is in
- `barHeight`: Height of the entire bar
- `widgetHeight`: Recommended widget height
- `parentScreen`: Reference to the screen object
**PluginComponent Properties (automatically injected):**
- `axis`: Bar axis information (horizontal/vertical)
- `section`: Bar section ("left", "center", "right")
- `parentScreen`: Screen reference for multi-monitor support
- `widgetThickness`: Recommended widget size perpendicular to bar
- `barThickness`: Bar thickness parallel to edge
**Component Options:**
- `horizontalBarPill`: Component shown in horizontal bars
- `verticalBarPill`: Component shown in vertical bars
- `popoutContent`: Optional popout window content
- `popoutWidth`: Popout window width
- `popoutHeight`: Popout window height
The PluginComponent automatically handles:
- Bar orientation detection
- Click handlers for popouts
- Proper positioning and anchoring
- Theme integration
### Settings Component
Optional settings UI loaded inline in the PluginsTab accordion interface:
Optional settings UI loaded inline in the PluginsTab accordion interface. Use the simplified settings API with auto-storage components:
```qml
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Plugins
Column {
PluginSettings {
id: root
pluginId: "yourPlugin"
// PluginService is injected by PluginsTab
property var pluginService
spacing: Theme.spacingM
DankTextField {
id: settingInput
width: parent.width
label: "Setting Label"
text: pluginService ? pluginService.loadPluginData("yourPlugin", "settingKey", "default") : ""
onTextChanged: {
if (pluginService) {
pluginService.savePluginData("yourPlugin", "settingKey", text)
}
}
StringSetting {
settingKey: "apiKey"
label: "API Key"
description: "Your API key for accessing the service"
placeholder: "Enter API key..."
}
DankToggle {
checked: pluginService ? pluginService.loadPluginData("yourPlugin", "enabled", true) : false
onToggled: {
if (pluginService) {
pluginService.savePluginData("yourPlugin", "enabled", checked)
ToggleSetting {
settingKey: "notifications"
label: "Enable Notifications"
description: "Show desktop notifications for updates"
defaultValue: true
}
SelectionSetting {
settingKey: "updateInterval"
label: "Update Interval"
description: "How often to refresh data"
options: [
{label: "1 minute", value: "60"},
{label: "5 minutes", value: "300"},
{label: "15 minutes", value: "900"}
]
defaultValue: "300"
}
ListSetting {
id: itemList
settingKey: "items"
label: "Saved Items"
description: "List of configured items"
delegate: Component {
StyledRect {
width: parent.width
height: 40
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
text: modelData.name
color: Theme.surfaceText
}
Rectangle {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
width: 60
height: 28
color: removeArea.containsMouse ? Theme.errorHover : Theme.error
radius: Theme.cornerRadius
StyledText {
anchors.centerIn: parent
text: "Remove"
color: Theme.errorText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
}
MouseArea {
id: removeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: itemList.removeItem(index)
}
}
}
}
}
}
```
**Available Setting Components:**
All settings automatically save on change and load on component creation. No manual `pluginService.savePluginData()` calls needed!
1. **PluginSettings** - Root wrapper for all plugin settings
- `pluginId`: Your plugin ID (required)
- Auto-handles storage and provides saveValue/loadValue to children
- Place all other setting components inside this wrapper
2. **StringSetting** - Text input field
- `settingKey`: Storage key (required)
- `label`: Display label (required)
- `description`: Help text (optional)
- `placeholder`: Input placeholder (optional)
- `defaultValue`: Default value (optional)
- Layout: Vertical stack (label, description, input field)
3. **ToggleSetting** - Boolean toggle switch
- `settingKey`: Storage key (required)
- `label`: Display label (required)
- `description`: Help text (optional)
- `defaultValue`: Default boolean (optional)
- Layout: Horizontal (label/description left, toggle right)
4. **SelectionSetting** - Dropdown menu
- `settingKey`: Storage key (required)
- `label`: Display label (required)
- `description`: Help text (optional)
- `options`: Array of `{label, value}` objects or simple strings (required)
- `defaultValue`: Default value (optional)
- Layout: Horizontal (label/description left, dropdown right)
- Stores the `value` field, displays the `label` field
5. **ListSetting** - Manage list of items (manual add/remove)
- `settingKey`: Storage key (required)
- `label`: Display label (required)
- `description`: Help text (optional)
- `delegate`: Custom item delegate Component (optional)
- `addItem(item)`: Add item to list
- `removeItem(index)`: Remove item from list
- Use when you need custom UI for adding items
6. **ListSettingWithInput** - Complete list management with built-in form
- `settingKey`: Storage key (required)
- `label`: Display label (required)
- `description`: Help text (optional)
- `fields`: Array of field definitions (required)
- `id`: Field ID in saved object (required)
- `label`: Column header text (required)
- `placeholder`: Input placeholder (optional)
- `width`: Column width in pixels (optional, default 200)
- `required`: Must have value to add (optional, default false)
- `default`: Default value if empty (optional)
- Automatically generates:
- Column headers from field labels
- Input fields with placeholders
- Add button with validation
- List display showing all field values
- Remove buttons for each item
- Best for collecting structured data (servers, locations, etc.)
**Complete Settings Example:**
```qml
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginSettings {
pluginId: "myPlugin"
// Section header (optional)
StyledText {
width: parent.width
text: "General Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
// Text input
StringSetting {
settingKey: "apiKey"
label: "API Key"
description: "Your service API key"
placeholder: "sk-..."
defaultValue: ""
}
// Toggle switches
ToggleSetting {
settingKey: "enabled"
label: "Enable Feature"
description: "Turn this feature on or off"
defaultValue: true
}
// Dropdown selection
SelectionSetting {
settingKey: "theme"
label: "Theme"
description: "Choose your preferred theme"
options: [
{label: "Dark", value: "dark"},
{label: "Light", value: "light"},
{label: "Auto", value: "auto"}
]
defaultValue: "dark"
}
// Structured list with multi-field input
ListSettingWithInput {
settingKey: "locations"
label: "Locations"
description: "Track multiple locations"
fields: [
{id: "name", label: "Name", placeholder: "Home", width: 150, required: true},
{id: "timezone", label: "Timezone", placeholder: "America/New_York", width: 200, required: true}
]
}
}
```
**Key Benefits:**
- Zero boilerplate - just define your settings
- Automatic persistence to `settings.json`
- Clean, consistent UI across all plugins
- No manual `pluginService` calls needed
- Proper layout and spacing handled automatically
## PluginService API
### Properties
@@ -262,72 +469,84 @@ Create `MyWidget.qml`:
```qml
import QtQuick
import qs.Services
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
Rectangle {
id: root
PluginComponent {
horizontalBarPill: Component {
StyledRect {
width: textItem.implicitWidth + Theme.spacingM * 2
height: parent.widgetThickness
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
property bool compactMode: false
property string section: "center"
property real widgetHeight: 30
property string displayText: "Hello World"
width: textItem.implicitWidth + 16
height: widgetHeight
radius: 8
color: "#20FFFFFF"
Component.onCompleted: {
displayText = PluginService.loadPluginData("myPlugin", "text", "Hello World")
StyledText {
id: textItem
anchors.centerIn: parent
text: "Hello World"
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
}
}
}
Text {
id: textItem
anchors.centerIn: parent
text: root.displayText
color: "#FFFFFF"
font.pixelSize: 13
}
verticalBarPill: Component {
StyledRect {
width: parent.widgetThickness
height: textItem.implicitWidth + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
MouseArea {
anchors.fill: parent
onClicked: console.log("Plugin clicked!")
StyledText {
id: textItem
anchors.centerIn: parent
text: "Hello"
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeSmall
rotation: 90
}
}
}
}
```
**Note:** Use `PluginComponent` wrapper for automatic property injection and bar integration. Define separate components for horizontal and vertical orientations.
### Step 4: Create Settings Component (Optional)
Create `MySettings.qml`:
```qml
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Plugins
Column {
// PluginService is injected by PluginsTab
property var pluginService
spacing: Theme.spacingM
PluginSettings {
pluginId: "myPlugin"
StyledText {
text: "Plugin Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
width: parent.width
text: "Configure your plugin settings"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
DankTextField {
width: parent.width
StringSetting {
settingKey: "text"
label: "Display Text"
text: pluginService ? pluginService.loadPluginData("myPlugin", "text", "Hello World") : ""
onTextChanged: {
if (pluginService) {
pluginService.savePluginData("myPlugin", "text", text)
}
}
description: "Text shown in the bar widget"
placeholder: "Hello World"
defaultValue: "Hello World"
}
ToggleSetting {
settingKey: "showIcon"
label: "Show Icon"
description: "Display an icon next to the text"
defaultValue: true
}
}
```