1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-30 00:12:50 -05:00

Fix reactivity, different settings structure, etc, etc.

This commit is contained in:
bbedward
2025-10-02 12:13:49 -04:00
parent ae461b1caf
commit 9b41eecbf1
27 changed files with 631 additions and 588 deletions

View File

@@ -79,7 +79,6 @@ Item {
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
width: parent.width
text: "Automatically lock after"
options: timeoutOptions
@@ -116,7 +115,6 @@ Item {
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
width: parent.width
text: "Turn off monitors after"
options: timeoutOptions
@@ -153,7 +151,6 @@ Item {
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
width: parent.width
text: "Suspend system after"
options: timeoutOptions
@@ -190,7 +187,6 @@ Item {
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
width: parent.width
text: "Hibernate system after"
options: timeoutOptions
visible: SessionService.hibernateSupported

View File

@@ -271,18 +271,17 @@ DankPopout {
spacing: Theme.spacingM
visible: searchField.text.length === 0
leftPadding: Theme.spacingS
topPadding: Theme.spacingXS
Item {
width: 200
height: 36
Rectangle {
width: 180
height: 40
radius: Theme.cornerRadius
color: "transparent"
DankDropdown {
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS
text: ""
dropdownWidth: 180
currentValue: appLauncher.selectedCategory
options: appLauncher.categories
optionIcons: appLauncher.categoryIcons
@@ -293,7 +292,7 @@ DankPopout {
}
Item {
width: parent.width - 310
width: parent.width - 290
height: 1
}

View File

@@ -99,17 +99,11 @@ Item {
target: PluginService
function onPluginLoaded(pluginId) {
console.log("DankBar: Plugin loaded:", pluginId)
// Force componentMap to update by triggering property change
if (topBarContent) {
topBarContent.updateComponentMap()
}
SettingsData.widgetDataChanged()
}
function onPluginUnloaded(pluginId) {
console.log("DankBar: Plugin unloaded:", pluginId)
// Force componentMap to update by triggering property change
if (topBarContent) {
topBarContent.updateComponentMap()
}
SettingsData.widgetDataChanged()
}
}

View File

@@ -273,7 +273,7 @@ DankPopout {
height: 32
radius: 16
color: closeBatteryArea.containsMouse ? Theme.errorHover : "transparent"
anchors.verticalCenter: parent.verticalCenter
anchors.top: parent.top
DankIcon {
anchors.centerIn: parent

View File

@@ -79,18 +79,15 @@ Loader {
}
if (item.pluginService !== undefined) {
console.log("WidgetHost: Injecting PluginService into plugin widget:", widgetId)
item.pluginService = PluginService
if (item.loadTimezones) {
console.log("WidgetHost: Calling loadTimezones for widget:", widgetId)
item.loadTimezones()
if (item.pluginId !== undefined) {
item.pluginId = widgetId
}
item.pluginService = PluginService
}
}
}
function getWidgetComponent(widgetId, components) {
// Build component map for built-in widgets
const componentMap = {
"launcherButton": components.launcherButtonComponent,
"workspaceSwitcher": components.workspaceSwitcherComponent,
@@ -121,12 +118,10 @@ Loader {
"systemUpdate": components.systemUpdateComponent
}
// Check for built-in component first
if (componentMap[widgetId]) {
return componentMap[widgetId]
}
// Check for plugin component
let pluginMap = PluginService.getWidgetComponents()
return pluginMap[widgetId] || null
}

View File

@@ -157,7 +157,6 @@ Rectangle {
}
DankDropdown {
width: parent.width
text: "Low Priority"
description: "Timeout for low priority notifications"
currentValue: getTimeoutText(SettingsData.notificationTimeoutLow)
@@ -173,7 +172,6 @@ Rectangle {
}
DankDropdown {
width: parent.width
text: "Normal Priority"
description: "Timeout for normal priority notifications"
currentValue: getTimeoutText(SettingsData.notificationTimeoutNormal)
@@ -189,7 +187,6 @@ Rectangle {
}
DankDropdown {
width: parent.width
text: "Critical Priority"
description: "Timeout for critical priority notifications"
currentValue: getTimeoutText(SettingsData.notificationTimeoutCritical)

View File

@@ -11,6 +11,8 @@ Item {
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
property string pluginId: ""
property var pluginService: null
property Component horizontalBarPill: null
property Component verticalBarPill: null
@@ -18,11 +20,42 @@ Item {
property real popoutWidth: 400
property real popoutHeight: 400
property var pluginData: ({})
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
Component.onCompleted: {
loadPluginData()
}
onPluginServiceChanged: {
loadPluginData()
}
onPluginIdChanged: {
loadPluginData()
}
Connections {
target: pluginService
function onPluginDataChanged(changedPluginId) {
if (changedPluginId === pluginId) {
loadPluginData()
}
}
}
function loadPluginData() {
if (!pluginService || !pluginId) {
pluginData = {}
return
}
pluginData = SettingsData.getPluginSettingsForPlugin(pluginId)
}
width: isVertical ? (hasVerticalPill ? verticalPill.width : 0) : (hasHorizontalPill ? horizontalPill.width : 0)
height: isVertical ? (hasVerticalPill ? verticalPill.height : 0) : (hasHorizontalPill ? horizontalPill.height : 0)
@@ -60,6 +93,12 @@ Item {
}
}
function closePopout() {
if (pluginPopout) {
pluginPopout.close()
}
}
PluginPopout {
id: pluginPopout
contentWidth: root.popoutWidth

View File

@@ -62,53 +62,24 @@ DankPopout {
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()
}
}
}
}
width: parent.width - Theme.spacingS * 2
height: parent.height - Theme.spacingS * 2
x: Theme.spacingS
y: Theme.spacingS
spacing: Theme.spacingS
Loader {
id: popoutContent
width: parent.width
sourceComponent: root.pluginContent
onLoaded: {
if (item && "closePopout" in item) {
item.closePopout = function() {
root.close()
}
}
}
}
}
}

View File

@@ -10,11 +10,24 @@ Item {
property var pluginService: null
default property alias content: settingsColumn.children
signal settingChanged()
implicitHeight: hasPermission ? settingsColumn.implicitHeight : errorText.implicitHeight
height: implicitHeight
readonly property bool hasPermission: pluginService && pluginService.hasPermission ? pluginService.hasPermission(pluginId, "settings_write") : true
onPluginServiceChanged: {
if (pluginService) {
for (let i = 0; i < settingsColumn.children.length; i++) {
const child = settingsColumn.children[i]
if (child.loadValue) {
child.loadValue()
}
}
}
}
function saveValue(key, value) {
if (!pluginService) {
return
@@ -25,6 +38,7 @@ Item {
}
if (pluginService.savePluginData) {
pluginService.savePluginData(pluginId, key, value)
settingChanged()
}
}

View File

@@ -0,0 +1,76 @@
import QtQuick
import qs.Common
import qs.Widgets
Column {
id: root
property string headerText: ""
property string detailsText: ""
property bool showCloseButton: false
property var closePopout: null
readonly property int headerHeight: popoutHeader.visible ? popoutHeader.height : 0
readonly property int detailsHeight: popoutDetails.visible ? popoutDetails.implicitHeight : 0
spacing: 0
Item {
id: popoutHeader
width: parent.width
height: 40
visible: headerText.length > 0
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: root.headerText
font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold
color: Theme.surfaceText
}
Rectangle {
id: closeButton
width: 32
height: 32
radius: 16
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: closeArea.containsMouse ? Theme.errorHover : "transparent"
visible: root.showCloseButton
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: {
if (root.closePopout) {
root.closePopout()
}
}
}
}
}
StyledText {
id: popoutDetails
width: parent.width
leftPadding: Theme.spacingS
bottomPadding: Theme.spacingS
text: root.detailsText
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
visible: detailsText.length > 0
wrapMode: Text.WordWrap
}
}

View File

@@ -15,6 +15,17 @@ Column {
width: parent.width
spacing: Theme.spacingS
function loadValue() {
const settings = findSettings()
if (settings && settings.pluginService) {
value = settings.loadValue(settingKey, defaultValue)
}
}
Component.onCompleted: {
loadValue()
}
readonly property var optionLabels: {
const labels = []
for (let i = 0; i < options.length; i++) {
@@ -49,13 +60,6 @@ Column {
return map
}
Component.onCompleted: {
const settings = findSettings()
if (settings) {
value = settings.loadValue(settingKey, defaultValue)
}
}
onValueChanged: {
const settings = findSettings()
if (settings) {
@@ -74,40 +78,14 @@ Column {
return null
}
Row {
DankDropdown {
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
}
text: root.label
description: root.description
currentValue: root.valueToLabel[root.value] || root.value
options: root.optionLabels
onValueChanged: newValue => {
root.value = root.labelToValue[newValue] || newValue
}
}
}

View File

@@ -19,13 +19,17 @@ Column {
width: parent.width
spacing: Theme.spacingS
Component.onCompleted: {
function loadValue() {
const settings = findSettings()
if (settings) {
if (settings && settings.pluginService) {
value = settings.loadValue(settingKey, defaultValue)
}
}
Component.onCompleted: {
loadValue()
}
onValueChanged: {
const settings = findSettings()
if (settings) {
@@ -69,6 +73,7 @@ Column {
rightIcon: root.rightIcon
unit: root.unit
wheelEnabled: false
thumbOutlineColor: Theme.surfaceContainerHighest
onSliderValueChanged: newValue => {
root.value = newValue
}

View File

@@ -8,6 +8,15 @@ import qs.Widgets
Item {
id: dankBarTab
function getWidgetsForPopup() {
return baseWidgetDefinitions.filter(widget => {
if (widget.warning && widget.warning.includes("Plugin is disabled")) {
return false
}
return true
})
}
property var baseWidgetDefinitions: {
var coreWidgets = [{
"id": "launcherButton",
@@ -179,16 +188,18 @@ Item {
"enabled": SystemUpdateService.distributionSupported
}]
// Add plugin widgets dynamically
var loadedPlugins = PluginService.getLoadedPlugins()
for (var i = 0; i < loadedPlugins.length; i++) {
var plugin = loadedPlugins[i]
// Add all available plugins (loaded and unloaded)
var allPlugins = PluginService.getAvailablePlugins()
for (var i = 0; i < allPlugins.length; i++) {
var plugin = allPlugins[i]
var isLoaded = PluginService.isPluginLoaded(plugin.id)
coreWidgets.push({
"id": plugin.id,
"text": plugin.name,
"description": plugin.description || "Plugin widget",
"icon": plugin.icon || "extension",
"enabled": true
"enabled": isLoaded,
"warning": !isLoaded ? "Plugin is disabled - enable in Plugins settings to use" : undefined
})
}
@@ -1146,7 +1157,7 @@ Item {
}
onAddWidget: sectionId => {
widgetSelectionPopup.allWidgets
= dankBarTab.baseWidgetDefinitions
= dankBarTab.getWidgetsForPopup()
widgetSelectionPopup.targetSection = sectionId
widgetSelectionPopup.safeOpen()
}
@@ -1218,7 +1229,7 @@ Item {
}
onAddWidget: sectionId => {
widgetSelectionPopup.allWidgets
= dankBarTab.baseWidgetDefinitions
= dankBarTab.getWidgetsForPopup()
widgetSelectionPopup.targetSection = sectionId
widgetSelectionPopup.safeOpen()
}
@@ -1290,7 +1301,7 @@ Item {
}
onAddWidget: sectionId => {
widgetSelectionPopup.allWidgets
= dankBarTab.baseWidgetDefinitions
= dankBarTab.getWidgetsForPopup()
widgetSelectionPopup.targetSection = sectionId
widgetSelectionPopup.safeOpen()
}

View File

@@ -520,7 +520,6 @@ Item {
DankDropdown {
id: monitorDropdown
width: parent.width - parent.leftPadding
text: "Monitor"
description: "Select monitor to configure wallpaper"
currentValue: selectedMonitorName || "No monitors"
@@ -678,7 +677,6 @@ Item {
property var intervalOptions: ["1 minute", "5 minutes", "15 minutes", "30 minutes", "1 hour", "1.5 hours", "2 hours", "3 hours", "4 hours", "6 hours", "8 hours", "12 hours"]
property var intervalValues: [60, 300, 900, 1800, 3600, 5400, 7200, 10800, 14400, 21600, 28800, 43200]
width: parent.width - parent.leftPadding
visible: {
if (SessionData.perMonitorWallpaper) {
return SessionData.getMonitorCyclingSettings(selectedMonitorName).mode === "interval"
@@ -833,7 +831,6 @@ Item {
}
DankDropdown {
width: parent.width
text: "Transition Effect"
description: "Visual effect used when wallpaper changes"
currentValue: {
@@ -851,8 +848,6 @@ Item {
width: parent.width
spacing: Theme.spacingS
visible: SessionData.wallpaperTransition === "random"
leftPadding: Theme.spacingM
rightPadding: Theme.spacingM
StyledText {
text: "Include Transitions"
@@ -866,12 +861,12 @@ Item {
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width - parent.leftPadding - parent.rightPadding
width: parent.width
}
DankButtonGroup {
id: transitionGroup
width: parent.width - parent.leftPadding - parent.rightPadding
width: parent.width
selectionMode: "multi"
model: SessionData.availableWallpaperTransitions.filter(t => t !== "none")
initialSelection: SessionData.includedTransitions
@@ -959,7 +954,6 @@ Item {
DankDropdown {
id: personalizationMatugenPaletteDropdown
width: parent.width
text: "Matugen Palette"
description: "Select the palette algorithm used for wallpaper-based colors"
options: Theme.availableMatugenSchemes.map(function (option) { return option.label })
@@ -1075,7 +1069,6 @@ Item {
}
DankDropdown {
width: parent.width
text: "Temperature"
description: "Color temperature for night mode"
currentValue: SessionData.nightModeTemperature + "K"
@@ -1424,7 +1417,6 @@ Item {
}
DankDropdown {
width: parent.width
text: "Popup Position"
description: "Choose where notification popups appear on screen"
currentValue: {
@@ -1506,7 +1498,6 @@ Item {
}
DankDropdown {
width: parent.width
text: "Font Family"
description: "Select system font family"
currentValue: {
@@ -1528,7 +1519,6 @@ Item {
}
DankDropdown {
width: parent.width
text: "Font Weight"
description: "Select font weight"
currentValue: {
@@ -1595,7 +1585,6 @@ Item {
}
DankDropdown {
width: parent.width
text: "Monospace Font"
description: "Select monospace font for process list and technical displays"
currentValue: {

View File

@@ -10,14 +10,6 @@ Item {
property string expandedPluginId: ""
Component.onCompleted: {
console.log("PluginsTab: Component completed")
console.log("PluginsTab: PluginService available:", typeof PluginService !== "undefined")
if (typeof PluginService !== "undefined") {
console.log("PluginsTab: Available plugins:", Object.keys(PluginService.availablePlugins).length)
console.log("PluginsTab: Plugin directory:", PluginService.pluginDirectory)
}
}
DankFlickable {
anchors.fill: parent
@@ -186,9 +178,6 @@ Item {
property bool hasSettings: pluginData && pluginData.settings !== undefined && pluginData.settings !== ""
property bool isExpanded: pluginsTab.expandedPluginId === pluginId
onIsExpandedChanged: {
console.log("Plugin", pluginId, "isExpanded changed to:", isExpanded)
}
color: pluginMouseArea.containsMouse ? Theme.surfacePressed : (isExpanded ? Theme.surfaceContainerHighest : Theme.surfaceContainerHigh)
border.width: 0
@@ -200,15 +189,8 @@ Item {
hoverEnabled: true
cursorShape: pluginDelegate.hasSettings ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
console.log("Plugin clicked:", pluginDelegate.pluginId, "hasSettings:", pluginDelegate.hasSettings, "isLoaded:", PluginService.isPluginLoaded(pluginDelegate.pluginId))
if (pluginDelegate.hasSettings) {
if (pluginsTab.expandedPluginId === pluginDelegate.pluginId) {
console.log("Collapsing plugin:", pluginDelegate.pluginId)
pluginsTab.expandedPluginId = ""
} else {
console.log("Expanding plugin:", pluginDelegate.pluginId)
pluginsTab.expandedPluginId = pluginDelegate.pluginId
}
pluginsTab.expandedPluginId = pluginsTab.expandedPluginId === pluginDelegate.pluginId ? "" : pluginDelegate.pluginId
}
}
}
@@ -234,7 +216,7 @@ Item {
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM - pluginToggle.width - Theme.spacingM
width: parent.width - Theme.iconSize - Theme.spacingM - toggleRow.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
@@ -267,30 +249,69 @@ Item {
}
}
DankToggle {
id: pluginToggle
Row {
id: toggleRow
anchors.verticalCenter: parent.verticalCenter
checked: PluginService.isPluginLoaded(pluginDelegate.pluginId)
onToggled: isChecked => {
const currentPluginId = pluginDelegate.pluginId
const currentPluginName = pluginDelegate.pluginName
spacing: Theme.spacingXS
if (isChecked) {
if (PluginService.enablePlugin(currentPluginId)) {
ToastService.showInfo("Plugin enabled: " + currentPluginName)
} else {
ToastService.showError("Failed to enable plugin: " + currentPluginName)
checked = false
Rectangle {
width: 28
height: 28
radius: 14
color: reloadArea.containsMouse ? Theme.surfaceContainerHighest : "transparent"
visible: PluginService.isPluginLoaded(pluginDelegate.pluginId)
DankIcon {
anchors.centerIn: parent
name: "refresh"
size: 16
color: reloadArea.containsMouse ? Theme.primary : Theme.surfaceVariantText
}
MouseArea {
id: reloadArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
const currentPluginId = pluginDelegate.pluginId
const currentPluginName = pluginDelegate.pluginName
pluginsTab.isReloading = true
if (PluginService.reloadPlugin(currentPluginId)) {
ToastService.showInfo("Plugin reloaded: " + currentPluginName)
} else {
ToastService.showError("Failed to reload plugin: " + currentPluginName)
pluginsTab.isReloading = false
}
}
} else {
if (PluginService.disablePlugin(currentPluginId)) {
ToastService.showInfo("Plugin disabled: " + currentPluginName)
if (pluginDelegate.isExpanded) {
expandedPluginId = ""
}
}
DankToggle {
id: pluginToggle
anchors.verticalCenter: parent.verticalCenter
checked: PluginService.isPluginLoaded(pluginDelegate.pluginId)
onToggled: isChecked => {
const currentPluginId = pluginDelegate.pluginId
const currentPluginName = pluginDelegate.pluginName
if (isChecked) {
if (PluginService.enablePlugin(currentPluginId)) {
ToastService.showInfo("Plugin enabled: " + currentPluginName)
} else {
ToastService.showError("Failed to enable plugin: " + currentPluginName)
checked = false
}
} else {
ToastService.showError("Failed to disable plugin: " + currentPluginName)
checked = true
if (PluginService.disablePlugin(currentPluginId)) {
ToastService.showInfo("Plugin disabled: " + currentPluginName)
if (pluginDelegate.isExpanded) {
expandedPluginId = ""
}
} else {
ToastService.showError("Failed to disable plugin: " + currentPluginName)
checked = true
}
}
}
}
@@ -358,15 +379,8 @@ Item {
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))
}
source: {
if (active && pluginDelegate.pluginSettingsPath) {
console.log("Loading plugin settings from:", pluginDelegate.pluginSettingsPath)
var path = pluginDelegate.pluginSettingsPath
if (!path.startsWith("file://")) {
path = "file://" + path
@@ -376,37 +390,9 @@ Item {
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)
}
}
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")
}
if (item.loadTimezones) {
console.log("Calling loadTimezones for WorldClock plugin")
item.loadTimezones()
}
if (item.initializeSettings) {
item.initializeSettings()
}
if (item && typeof PluginService !== "undefined") {
item.pluginService = PluginService
}
}
}
@@ -440,14 +426,19 @@ Item {
}
}
property bool isReloading: false
Connections {
target: PluginService
function onPluginLoaded() {
pluginRepeater.model = PluginService.getAvailablePlugins()
if (isReloading) {
isReloading = false
}
}
function onPluginUnloaded() {
pluginRepeater.model = PluginService.getAvailablePlugins()
if (pluginsTab.expandedPluginId !== "" && !PluginService.isPluginLoaded(pluginsTab.expandedPluginId)) {
if (!isReloading && pluginsTab.expandedPluginId !== "" && !PluginService.isPluginLoaded(pluginsTab.expandedPluginId)) {
pluginsTab.expandedPluginId = ""
}
}

View File

@@ -651,7 +651,6 @@ Item {
DankDropdown {
id: matugenPaletteDropdown
width: parent.width
text: "Matugen Palette"
description: "Select the palette algorithm used for wallpaper-based colors"
options: Theme.availableMatugenSchemes.map(function (option) { return option.label })
@@ -993,7 +992,6 @@ Item {
}
DankDropdown {
width: parent.width - Theme.iconSize - Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
text: "Icon Theme"
description: "DankShell & System Icons\n(requires restart)"

View File

@@ -121,7 +121,6 @@ Item {
}
DankDropdown {
width: parent.width
height: 50
text: "Top Bar Format"
description: "Preview: " + (SettingsData.clockDateFormat ? new Date().toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat) : new Date().toLocaleDateString(Qt.locale(), "ddd d"))
@@ -185,7 +184,6 @@ Item {
}
DankDropdown {
width: parent.width
height: 50
text: "Lock Screen Format"
description: "Preview: " + (SettingsData.lockDateFormat ? new Date().toLocaleDateString(Qt.locale(), SettingsData.lockDateFormat) : new Date().toLocaleDateString(Qt.locale(), Locale.LongFormat))

View File

@@ -142,13 +142,14 @@ Column {
spacing: Theme.spacingXS
Item {
width: 120
width: 60
height: 32
visible: modelData.id === "gpuTemp"
DankDropdown {
id: gpuDropdown
anchors.fill: parent
popupWidth: -1
currentValue: {
var selectedIndex = modelData.selectedGpuIndex
!== undefined ? modelData.selectedGpuIndex : 0
@@ -223,12 +224,7 @@ Column {
Item {
width: 32
height: 32
visible: (modelData.warning !== undefined
&& modelData.warning !== "")
&& (modelData.id === "cpuUsage"
|| modelData.id === "memUsage"
|| modelData.id === "cpuTemp"
|| modelData.id === "gpuTemp")
visible: modelData.warning !== undefined && modelData.warning !== ""
DankIcon {
name: "warning"

View File

@@ -8,18 +8,13 @@ import qs.Modules.Plugins
PluginComponent {
id: root
property var pluginService: null
property var enabledEmojis: pluginData.emojis || ["😊", "😢", "❤️"]
property int cycleInterval: pluginData.cycleInterval || 3000
property int maxBarEmojis: pluginData.maxBarEmojis || 3
// Load settings from PluginService
property var enabledEmojis: pluginService ? pluginService.loadPluginData("exampleEmojiPlugin", "emojis", ["😊", "😢", "❤️"]) : ["😊", "😢", "❤️"]
property int cycleInterval: pluginService ? pluginService.loadPluginData("exampleEmojiPlugin", "cycleInterval", 3000) : 3000
property int maxBarEmojis: pluginService ? pluginService.loadPluginData("exampleEmojiPlugin", "maxBarEmojis", 3) : 3
// Current state for cycling through emojis
property int currentIndex: 0
property var displayedEmojis: []
// Timer to cycle through emojis at the configured interval
Timer {
interval: root.cycleInterval
running: true
@@ -32,7 +27,6 @@ PluginComponent {
}
}
// Update the emojis shown in the bar when settings or index changes
function updateDisplayedEmojis() {
const maxToShow = Math.min(root.maxBarEmojis, root.enabledEmojis.length)
let emojis = []
@@ -51,58 +45,44 @@ PluginComponent {
onMaxBarEmojisChanged: updateDisplayedEmojis()
horizontalBarPill: Component {
StyledRect {
width: emojiRow.implicitWidth + Theme.spacingM * 2
height: parent.widgetThickness
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
Row {
id: emojiRow
spacing: Theme.spacingXS
Row {
id: emojiRow
anchors.centerIn: parent
spacing: Theme.spacingXS
Repeater {
model: root.displayedEmojis
StyledText {
text: modelData
font.pixelSize: Theme.fontSizeLarge
}
Repeater {
model: root.displayedEmojis
StyledText {
text: modelData
font.pixelSize: Theme.fontSizeLarge
}
}
}
}
verticalBarPill: Component {
StyledRect {
width: parent.widgetThickness
height: emojiColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
Column {
id: emojiColumn
spacing: Theme.spacingXS
Column {
id: emojiColumn
anchors.centerIn: parent
spacing: Theme.spacingXS
Repeater {
model: root.displayedEmojis
StyledText {
text: modelData
font.pixelSize: Theme.fontSizeMedium
anchors.horizontalCenter: parent.horizontalCenter
}
Repeater {
model: root.displayedEmojis
StyledText {
text: modelData
font.pixelSize: Theme.fontSizeMedium
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
popoutContent: Component {
Item {
width: parent.width
height: parent.height
PopoutComponent {
id: popoutColumn
headerText: "Emoji Picker"
detailsText: "Click an emoji to copy it to clipboard"
showCloseButton: true
// A grid of 120+ emojis for the user to pick from
property var allEmojis: [
"😀", "😃", "😄", "😁", "😆", "😅", "🤣", "😂", "🙂", "🙃",
"😉", "😊", "😇", "🥰", "😍", "🤩", "😘", "😗", "😚", "😙",
@@ -119,59 +99,43 @@ PluginComponent {
"👌", "🤌", "🤏", "👈", "👉", "👆", "👇", "☝️", "✋", "🤚"
]
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
Item {
width: parent.width
implicitHeight: root.popoutHeight - popoutColumn.headerHeight - popoutColumn.detailsHeight - Theme.spacingXL
StyledText {
text: "Click an emoji to copy it!"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
DankFlickable {
width: parent.width
height: parent.height - parent.spacing - 30
contentWidth: emojiGrid.width
contentHeight: emojiGrid.height
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
Grid {
id: emojiGrid
width: parent.width - Theme.spacingM
columns: 8
spacing: Theme.spacingS
delegate: StyledRect {
width: 45
height: 45
radius: Theme.cornerRadius
color: emojiMouseArea.containsMouse ? Theme.surfaceContainerHighest : Theme.surfaceContainerHigh
border.width: 0
Repeater {
model: allEmojis
StyledText {
anchors.centerIn: parent
text: modelData
font.pixelSize: Theme.fontSizeXLarge
}
StyledRect {
width: 45
height: 45
radius: Theme.cornerRadius
color: emojiMouseArea.containsMouse ? Theme.surfaceContainerHighest : Theme.surfaceContainerHigh
border.width: 0
MouseArea {
id: emojiMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
StyledText {
anchors.centerIn: parent
text: modelData
font.pixelSize: Theme.fontSizeXLarge
}
MouseArea {
id: emojiMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Quickshell.execDetached(["sh", "-c", "echo -n '" + modelData + "' | wl-copy"])
ToastService.show("Copied " + modelData + " to clipboard", 2000)
root.closePopout()
}
}
onClicked: {
Quickshell.execDetached(["sh", "-c", "echo -n '" + modelData + "' | wl-copy"])
ToastService.showInfo("Copied " + modelData + " to clipboard")
popoutColumn.closePopout()
}
}
}

View File

@@ -1,6 +1,6 @@
# Emoji Cycler Plugin
An example DankMaterialShell plugin that displays cycling emojis in your bar with an emoji picker popout.
An example dms plugin that displays cycling emojis in your bar with an emoji picker popout.
## Features

View File

@@ -3,7 +3,7 @@
"name": "Emoji Cycler",
"description": "Display cycling emojis in your bar with a handy emoji picker popout",
"version": "1.0.0",
"author": "DMS Plugin System",
"author": "AvengeMedia",
"icon": "mood",
"component": "./EmojiWidget.qml",
"settings": "./EmojiSettings.qml",

View File

@@ -121,15 +121,21 @@ PluginComponent {
// Define popout content (optional)
popoutContent: Component {
Column {
width: parent.width
spacing: Theme.spacingM
padding: Theme.spacingM
PopoutComponent {
headerText: "My Plugin"
detailsText: "Optional description text goes here"
showCloseButton: true
StyledText {
text: "Popout Content"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
// Your popout content goes here
Column {
width: parent.width
spacing: Theme.spacingM
StyledText {
text: "Popout Content"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
}
}
}
}
@@ -160,6 +166,42 @@ The PluginComponent automatically handles:
- Proper positioning and anchoring
- Theme integration
### PopoutComponent
PopoutComponent provides a consistent header/content layout for plugin popouts:
```qml
import qs.Modules.Plugins
PopoutComponent {
headerText: "Header Title" // Main header text (bold, large)
detailsText: "Description text" // Optional description (smaller, gray)
showCloseButton: true // Show X button in top-right
// Access to exposed properties for dynamic sizing
readonly property int headerHeight // Height of header area
readonly property int detailsHeight // Height of description area
// Your content here - use parent.width for full width
// Calculate available height: root.popoutHeight - headerHeight - detailsHeight - spacing
DankGridView {
width: parent.width
height: parent.height
// ...
}
}
```
**PopoutComponent Properties:**
- `headerText`: Main header text (optional, hidden if empty)
- `detailsText`: Description text below header (optional, hidden if empty)
- `showCloseButton`: Show close button in header (default: false)
- `closePopout`: Function to close popout (auto-injected by PluginPopout)
- `headerHeight`: Readonly height of header (0 if not visible)
- `detailsHeight`: Readonly height of description (0 if not visible)
The component automatically handles spacing and layout. Content children are rendered below the description with proper padding.
### Settings Component
Optional settings UI loaded inline in the PluginsTab accordion interface. Use the simplified settings API with auto-storage components:

View File

@@ -11,7 +11,7 @@
</div>
A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/) and designed for the [niri](https://github.com/YaLTeR/niri) and [Hyprland](https://hyprland.org/) compositors. Features Material 3 design principles with a heavy focus on functionality and customizability.
A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/) and optimized for the [niri](https://github.com/YaLTeR/niri) and [Hyprland](https://hyprland.org/) compositors.
## Screenshots
@@ -122,6 +122,8 @@ curl -fsSL https://install.danklinux.com | sh
- Configure bluetooth, wifi, and audio input+output devices.
- A lock screen
- Idle monitoring - configure auto lock, screen off, suspend, and hibernate with different knobs for battery + AC power.
- A greeter
- A comprehensive plugin system for endless customization possibilities.
**TL;DR** *dms replaces your waybar, swaylock, swayidle, hypridle, hyprlock, fuzzels, walker, mako, and basically everything you use to stitch a desktop together*
@@ -629,6 +631,14 @@ echo "app-notifications = no-clipboard-copy,no-config-reload" >> ~/.config/ghost
echo "include dank-theme.conf" >> ~/.config/kitty/kitty.conf
```
## Plugins
dms features a plugin system - meaning you can create your own widgets and load other user widgets.
More comprehensive details available in the [PLUGINS](PLUGINS/README.md) - and example [Emoji Plugin](PLUGINS/ExampleEmojiPlugin) is available for reference.
The example plugin can be installed by `cp -R ./PLUGINS/ExampleEmojiPlugin ~/.config/DankMaterialShell/plugins` - then it will appear in dms settings.
### Calendar Setup
Sync your caldev compatible calendar (Google, Office365, etc.) for dashboard integration:

View File

@@ -28,6 +28,7 @@ Singleton {
signal pluginLoaded(string pluginId)
signal pluginUnloaded(string pluginId)
signal pluginLoadFailed(string pluginId, string error)
signal pluginDataChanged(string pluginId)
Component.onCompleted: {
Qt.callLater(initializePlugins)
@@ -225,7 +226,10 @@ Singleton {
return false
}
pluginWidgetComponents[pluginId] = component
var newComponents = Object.assign({}, pluginWidgetComponents)
newComponents[pluginId] = component
pluginWidgetComponents = newComponents
plugin.loaded = true
loadedPlugins[pluginId] = plugin
@@ -254,7 +258,9 @@ Singleton {
component.destroy()
}
}
delete pluginWidgetComponents[pluginId]
var newComponents = Object.assign({}, pluginWidgetComponents)
delete newComponents[pluginId]
pluginWidgetComponents = newComponents
plugin.loaded = false
delete loadedPlugins[pluginId]
@@ -314,6 +320,7 @@ Singleton {
function savePluginData(pluginId, key, value) {
SettingsData.setPluginSetting(pluginId, key, value)
pluginDataChanged(pluginId)
return true
}

View File

@@ -1,10 +1,11 @@
import "../Common/fzf.js" as Fzf
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import qs.Common
import qs.Widgets
Rectangle {
Item {
id: root
property string text: ""
@@ -12,50 +13,33 @@ Rectangle {
property string currentValue: ""
property var options: []
property var optionIcons: []
property bool forceRecreate: false
property bool enableFuzzySearch: false
property int popupWidthOffset: 0
property int maxPopupHeight: 400
property bool openUpwards: false
property int popupWidth: 0
property bool alignPopupRight: false
property int dropdownWidth: 200
signal valueChanged(string value)
width: parent.width
height: 60
radius: Theme.cornerRadius
color: "transparent"
Component.onCompleted: forceRecreateTimer.start()
implicitHeight: Math.max(60, labelColumn.implicitHeight + Theme.spacingM)
Component.onDestruction: {
const popup = popupLoader.item
const popup = dropdownMenu
if (popup && popup.visible) {
popup.close()
}
}
onVisibleChanged: {
const popup = popupLoader.item
if (!visible && popup && popup.visible) {
popup.close()
} else if (visible) {
forceRecreateTimer.start()
}
}
Timer {
id: forceRecreateTimer
interval: 50
repeat: false
onTriggered: root.forceRecreate = !root.forceRecreate
}
Column {
id: labelColumn
anchors.left: parent.left
anchors.right: dropdown.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingL
spacing: Theme.spacingXS
StyledText {
@@ -78,15 +62,14 @@ Rectangle {
Rectangle {
id: dropdown
width: root.width <= 60 ? root.width : 180
height: 36
width: root.popupWidth === -1 ? undefined : (root.popupWidth > 0 ? root.popupWidth : root.dropdownWidth)
height: 40
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
radius: Theme.cornerRadius
color: dropdownArea.containsMouse ? Theme.primaryHover : Theme.contentBackground()
border.color: Theme.surfaceVariantAlpha
border.width: 1
color: dropdownArea.containsMouse || dropdownMenu.visible ? Theme.surfaceContainerHigh : Theme.surfaceContainer
border.color: dropdownMenu.visible ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: dropdownMenu.visible ? 2 : 1
MouseArea {
id: dropdownArea
@@ -95,42 +78,39 @@ Rectangle {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
const popup = popupLoader.item
if (!popup) {
if (dropdownMenu.visible) {
dropdownMenu.close()
return
}
if (popup.visible) {
popup.close()
return
}
dropdownMenu.searchQuery = ""
dropdownMenu.updateFilteredOptions()
if (root.openUpwards || root.alignPopupRight) {
popup.open()
Qt.callLater(() => {
if (root.openUpwards) {
const pos = dropdown.mapToItem(Overlay.overlay, 0, 0)
if (root.alignPopupRight) {
popup.x = pos.x + dropdown.width - popup.width
} else {
popup.x = pos.x - (root.popupWidthOffset / 2)
}
popup.y = pos.y - popup.height - 4
} else {
const pos = dropdown.mapToItem(Overlay.overlay, 0, dropdown.height + 4)
if (root.alignPopupRight) {
popup.x = pos.x + dropdown.width - popup.width
} else {
popup.x = pos.x - (root.popupWidthOffset / 2)
}
popup.y = pos.y
}
})
dropdownMenu.open()
const pos = dropdown.mapToItem(Overlay.overlay, 0, 0)
const popupWidth = dropdownMenu.width
const popupHeight = dropdownMenu.height
const overlayHeight = Overlay.overlay.height
if (root.openUpwards || pos.y + dropdown.height + popupHeight + 4 > overlayHeight) {
if (root.alignPopupRight) {
dropdownMenu.x = pos.x + dropdown.width - popupWidth
} else {
dropdownMenu.x = pos.x - (root.popupWidthOffset / 2)
}
dropdownMenu.y = pos.y - popupHeight - 4
} else {
const pos = dropdown.mapToItem(Overlay.overlay, 0, dropdown.height + 4)
popup.x = pos.x - (root.popupWidthOffset / 2)
popup.y = pos.y
popup.open()
if (root.alignPopupRight) {
dropdownMenu.x = pos.x + dropdown.width - popupWidth
} else {
dropdownMenu.x = pos.x - (root.popupWidthOffset / 2)
}
dropdownMenu.y = pos.y + dropdown.height + 4
}
if (root.enableFuzzySearch && searchField.visible) {
searchField.forceActiveFocus()
}
}
}
@@ -139,8 +119,10 @@ Rectangle {
id: contentRow
anchors.left: parent.left
anchors.right: expandIcon.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingS
DankIcon {
@@ -149,9 +131,9 @@ Rectangle {
return currentIndex >= 0 && root.optionIcons.length > currentIndex ? root.optionIcons[currentIndex] : ""
}
size: 18
color: Theme.surfaceVariantText
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: name !== "" && root.width > 60
visible: name !== ""
}
StyledText {
@@ -159,229 +141,220 @@ Rectangle {
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
width: root.width <= 60 ? dropdown.width - expandIcon.width - Theme.spacingS * 2 : dropdown.width - contentRow.x - expandIcon.width - Theme.spacingM - Theme.spacingS
elide: root.width <= 60 ? Text.ElideNone : Text.ElideRight
horizontalAlignment: root.width <= 60 ? Text.AlignHCenter : Text.AlignLeft
width: contentRow.width - (contentRow.children[0].visible ? contentRow.children[0].width + contentRow.spacing : 0)
elide: Text.ElideRight
}
}
DankIcon {
id: expandIcon
name: "expand_more"
name: dropdownMenu.visible ? "expand_less" : "expand_more"
size: 20
color: Theme.surfaceVariantText
color: Theme.surfaceText
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: Theme.spacingS
Behavior on rotation {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
Loader {
id: popupLoader
Popup {
id: dropdownMenu
property bool recreateFlag: root.forceRecreate
property string searchQuery: ""
property var filteredOptions: []
property int selectedIndex: -1
property var fzfFinder: new Fzf.Finder(root.options, {
"selector": option => option,
"limit": 50,
"casing": "case-insensitive"
})
active: true
onRecreateFlagChanged: {
active = false
active = true
function updateFilteredOptions() {
if (!root.enableFuzzySearch || searchQuery.length === 0) {
filteredOptions = root.options
selectedIndex = -1
return
}
const results = fzfFinder.find(searchQuery)
filteredOptions = results.map(result => result.item)
selectedIndex = -1
}
sourceComponent: Component {
Popup {
id: dropdownMenu
function selectNext() {
if (filteredOptions.length === 0) {
return
}
selectedIndex = (selectedIndex + 1) % filteredOptions.length
listView.positionViewAtIndex(selectedIndex, ListView.Contain)
}
property string searchQuery: ""
property var filteredOptions: []
property int selectedIndex: -1
property var fzfFinder: new Fzf.Finder(root.options, {
"selector": option => option,
"limit": 50,
"casing": "case-insensitive"
})
function selectPrevious() {
if (filteredOptions.length === 0) {
return
}
selectedIndex = selectedIndex <= 0 ? filteredOptions.length - 1 : selectedIndex - 1
listView.positionViewAtIndex(selectedIndex, ListView.Contain)
}
function updateFilteredOptions() {
if (!root.enableFuzzySearch || searchQuery.length === 0) {
filteredOptions = root.options
selectedIndex = -1
return
}
function selectCurrent() {
if (selectedIndex < 0 || selectedIndex >= filteredOptions.length) {
return
}
root.currentValue = filteredOptions[selectedIndex]
root.valueChanged(filteredOptions[selectedIndex])
close()
}
const results = fzfFinder.find(searchQuery)
filteredOptions = results.map(result => result.item)
selectedIndex = -1
}
parent: Overlay.overlay
width: root.popupWidth === -1 ? undefined : (root.popupWidth > 0 ? root.popupWidth : (dropdown.width + root.popupWidthOffset))
height: Math.min(root.maxPopupHeight, (root.enableFuzzySearch ? 54 : 0) + Math.min(filteredOptions.length, 10) * 36 + 16)
padding: 0
modal: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
function selectNext() {
if (filteredOptions.length === 0) {
return
}
selectedIndex = (selectedIndex + 1) % filteredOptions.length
listView.positionViewAtIndex(selectedIndex, ListView.Contain)
}
background: Rectangle {
color: "transparent"
}
function selectPrevious() {
if (filteredOptions.length === 0) {
return
}
selectedIndex = selectedIndex <= 0 ? filteredOptions.length - 1 : selectedIndex - 1
listView.positionViewAtIndex(selectedIndex, ListView.Contain)
}
contentItem: Rectangle {
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1)
border.color: Theme.primary
border.width: 2
radius: Theme.cornerRadius
function selectCurrent() {
if (selectedIndex < 0 || selectedIndex >= filteredOptions.length) {
return
}
root.currentValue = filteredOptions[selectedIndex]
root.valueChanged(filteredOptions[selectedIndex])
close()
}
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowBlur: 0.4
shadowColor: Theme.shadowStrong
shadowVerticalOffset: 4
}
parent: Overlay.overlay
width: root.popupWidth > 0 ? root.popupWidth : (dropdown.width + root.popupWidthOffset)
height: Math.min(root.maxPopupHeight, (root.enableFuzzySearch ? 54 : 0) + Math.min(filteredOptions.length, 10) * 36 + 16)
padding: 0
modal: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
onOpened: {
searchQuery = ""
updateFilteredOptions()
if (root.enableFuzzySearch && searchField.visible) {
searchField.forceActiveFocus()
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingS
background: Rectangle {
color: "transparent"
}
Rectangle {
id: searchContainer
contentItem: Rectangle {
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1)
border.color: Theme.primarySelected
border.width: 1
width: parent.width
height: 42
visible: root.enableFuzzySearch
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
DankTextField {
id: searchField
Column {
anchors.fill: parent
anchors.margins: Theme.spacingS
anchors.margins: 1
placeholderText: "Search..."
text: dropdownMenu.searchQuery
topPadding: Theme.spacingS
bottomPadding: Theme.spacingS
onTextChanged: {
dropdownMenu.searchQuery = text
dropdownMenu.updateFilteredOptions()
}
Keys.onDownPressed: dropdownMenu.selectNext()
Keys.onUpPressed: dropdownMenu.selectPrevious()
Keys.onReturnPressed: dropdownMenu.selectCurrent()
Keys.onEnterPressed: dropdownMenu.selectCurrent()
Keys.onPressed: event => {
if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
dropdownMenu.selectNext()
event.accepted = true
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
dropdownMenu.selectPrevious()
event.accepted = true
} else if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
dropdownMenu.selectNext()
event.accepted = true
} else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
dropdownMenu.selectPrevious()
event.accepted = true
}
}
}
}
Rectangle {
id: searchContainer
Item {
width: 1
height: Theme.spacingXS
visible: root.enableFuzzySearch
}
width: parent.width
height: 42
visible: root.enableFuzzySearch
radius: Theme.cornerRadius
color: Theme.surfaceVariantAlpha
DankListView {
id: listView
DankTextField {
id: searchField
width: parent.width
height: parent.height - (root.enableFuzzySearch ? searchContainer.height + Theme.spacingXS : 0)
clip: true
model: dropdownMenu.filteredOptions
spacing: 2
anchors.fill: parent
anchors.margins: 1
placeholderText: "Search..."
text: searchQuery
topPadding: Theme.spacingS
bottomPadding: Theme.spacingS
onTextChanged: {
searchQuery = text
updateFilteredOptions()
}
Keys.onDownPressed: selectNext()
Keys.onUpPressed: selectPrevious()
Keys.onReturnPressed: selectCurrent()
Keys.onEnterPressed: selectCurrent()
Keys.onPressed: event => {
if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
selectNext()
event.accepted = true
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
selectPrevious()
event.accepted = true
} else if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
selectNext()
event.accepted = true
} else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
selectPrevious()
event.accepted = true
}
}
interactive: true
flickDeceleration: 1500
maximumFlickVelocity: 2000
boundsBehavior: Flickable.DragAndOvershootBounds
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
delegate: Rectangle {
property bool isSelected: dropdownMenu.selectedIndex === index
property bool isCurrentValue: root.currentValue === modelData
property int optionIndex: root.options.indexOf(modelData)
width: ListView.view.width
height: 32
radius: Theme.cornerRadius
color: isSelected ? Theme.primaryHover : optionArea.containsMouse ? Theme.primaryHoverLight : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: optionIndex >= 0 && root.optionIcons.length > optionIndex ? root.optionIcons[optionIndex] : ""
size: 18
color: isCurrentValue ? Theme.primary : Theme.surfaceText
visible: name !== ""
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: modelData
font.pixelSize: Theme.fontSizeMedium
color: isCurrentValue ? Theme.primary : Theme.surfaceText
font.weight: isCurrentValue ? Font.Medium : Font.Normal
width: root.popupWidth > 0 ? undefined : (parent.parent.width - parent.x - Theme.spacingS)
elide: root.popupWidth > 0 ? Text.ElideNone : Text.ElideRight
wrapMode: Text.NoWrap
}
}
Item {
width: 1
height: Theme.spacingXS
visible: root.enableFuzzySearch
}
MouseArea {
id: optionArea
DankListView {
id: listView
property var popupRef: dropdownMenu
width: parent.width
height: parent.height - (root.enableFuzzySearch ? searchContainer.height + Theme.spacingXS : 0)
clip: true
model: filteredOptions
spacing: 2
interactive: true
flickDeceleration: 1500
maximumFlickVelocity: 2000
boundsBehavior: Flickable.DragAndOvershootBounds
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
delegate: Rectangle {
property bool isSelected: selectedIndex === index
property bool isCurrentValue: root.currentValue === modelData
property int optionIndex: root.options.indexOf(modelData)
width: ListView.view.width
height: 32
radius: Theme.cornerRadius
color: isSelected ? Theme.primaryHover : optionArea.containsMouse ? Theme.primaryHoverLight : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: optionIndex >= 0 && root.optionIcons.length > optionIndex ? root.optionIcons[optionIndex] : ""
size: 18
color: isCurrentValue ? Theme.primary : Theme.surfaceVariantText
visible: name !== ""
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: modelData
font.pixelSize: Theme.fontSizeMedium
color: isCurrentValue ? Theme.primary : Theme.surfaceText
font.weight: isCurrentValue ? Font.Medium : Font.Normal
width: root.popupWidth > 0 ? undefined : (parent.parent.width - parent.x - Theme.spacingS)
elide: root.popupWidth > 0 ? Text.ElideNone : Text.ElideRight
wrapMode: Text.NoWrap
}
}
MouseArea {
id: optionArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.currentValue = modelData
root.valueChanged(modelData)
listView.popupRef.close()
}
}
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.currentValue = modelData
root.valueChanged(modelData)
dropdownMenu.close()
}
}
}