1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

configurable timeouts, keyboard improvements

This commit is contained in:
bbedward
2025-08-12 14:28:53 -04:00
parent acc8272994
commit 1e23c6b12c
12 changed files with 440 additions and 276 deletions

View File

@@ -12,7 +12,6 @@ import qs.Widgets
Item {
id: root
// Keyboard controller defined outside modal to ensure proper creation order
NotificationKeyboardController {
id: modalKeyboardController
listView: null
@@ -34,7 +33,6 @@ Item {
notificationModalOpen = true
modalKeyboardController.reset()
// Set the listView reference when modal is shown
if (modalKeyboardController && notificationListRef) {
modalKeyboardController.listView = notificationListRef
modalKeyboardController.rebuildFlatNavigation()
@@ -111,123 +109,36 @@ Item {
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
spacing: Theme.spacingM
Rectangle {
width: parent.width
height: 48
color: "transparent"
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "notifications"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Notification Center"
font.pixelSize: Theme.fontSizeXLarge
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Rectangle {
width: 32
height: 32
radius: Theme.cornerRadius
color: helpButtonArea.containsMouse ? Theme.primaryHover : (modalKeyboardController.showKeyboardHints ? Theme.primaryPressed : "transparent")
border.color: Theme.primary
border.width: 1
StyledText {
text: "?"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.primary
anchors.centerIn: parent
}
MouseArea {
id: helpButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: modalKeyboardController.showKeyboardHints = !modalKeyboardController.showKeyboardHints
}
}
Rectangle {
width: clearAllText.implicitWidth + Theme.spacingM
height: 32
radius: Theme.cornerRadius
color: clearAllArea.containsMouse ? Theme.primaryHover : "transparent"
border.color: Theme.primary
border.width: 1
visible: NotificationService.groupedNotifications.length > 0
StyledText {
id: clearAllText
text: "Clear All"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
anchors.centerIn: parent
}
MouseArea {
id: clearAllArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NotificationService.clearAllNotifications()
}
}
}
NotificationHeader {
id: notificationHeader
keyboardController: modalKeyboardController
}
NotificationSettings {
id: notificationSettings
expanded: notificationHeader.showSettings
}
Rectangle {
KeyboardNavigatedNotificationList {
id: notificationList
width: parent.width
height: parent.height - y
radius: Theme.cornerRadius
color: Theme.surfaceLight
border.color: Theme.outlineLight
border.width: 1
clip: false
KeyboardNavigatedNotificationList {
id: notificationList
anchors.fill: parent
anchors.margins: Theme.spacingS
keyboardController: modalKeyboardController
enableKeyboardNavigation: true
Component.onCompleted: {
notificationModal.notificationListRef = notificationList
if (modalKeyboardController) {
modalKeyboardController.listView = notificationList
modalKeyboardController.rebuildFlatNavigation()
}
keyboardController: modalKeyboardController
Component.onCompleted: {
notificationModal.notificationListRef = notificationList
if (modalKeyboardController) {
modalKeyboardController.listView = notificationList
modalKeyboardController.rebuildFlatNavigation()
}
}
}
}
// Keyboard hints overlay
NotificationKeyboardHints {
id: keyboardHints
anchors.bottom: parent.bottom

View File

@@ -7,12 +7,19 @@ DankListView {
id: listView
property var keyboardController: null
property bool enableKeyboardNavigation: false
property int currentSelectedGroupIndex: -1
property bool keyboardActive: false
property bool autoScrollDisabled: false
onIsUserScrollingChanged: {
if (isUserScrolling && keyboardController && keyboardController.keyboardNavigationActive) {
autoScrollDisabled = true
}
}
function enableAutoScroll() {
autoScrollDisabled = false
}
// Compatibility aliases for NotificationList
property alias count: listView.count
property alias listContentHeight: listView.contentHeight
@@ -20,14 +27,13 @@ DankListView {
model: NotificationService.groupedNotifications
spacing: Theme.spacingL
// Timer to periodically ensure selected item stays visible during active keyboard navigation
Timer {
id: positionPreservationTimer
interval: 200
running: keyboardController && keyboardController.keyboardNavigationActive
running: keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled
repeat: true
onTriggered: {
if (keyboardController && keyboardController.keyboardNavigationActive) {
if (keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled) {
keyboardController.ensureVisible()
}
}
@@ -38,13 +44,11 @@ DankListView {
anchors.centerIn: parent
}
// Override position restoration during keyboard nav
onModelChanged: {
if (keyboardController && keyboardController.keyboardNavigationActive) {
// Rebuild navigation and preserve position aggressively
keyboardController.rebuildFlatNavigation()
Qt.callLater(function() {
if (keyboardController && keyboardController.keyboardNavigationActive) {
if (keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled) {
keyboardController.ensureVisible()
}
})
@@ -71,17 +75,15 @@ DankListView {
notificationGroup: modelData
isGroupSelected: {
// Force re-evaluation when selection changes
if (!keyboardController || !keyboardController.keyboardNavigationActive) return false
keyboardController.selectionVersion // Trigger re-evaluation
keyboardController.selectionVersion
if (!listView.keyboardActive) return false
const selection = keyboardController.getCurrentSelection()
return selection.type === "group" && selection.groupIndex === index
}
selectedNotificationIndex: {
// Force re-evaluation when selection changes
if (!keyboardController || !keyboardController.keyboardNavigationActive) return -1
keyboardController.selectionVersion // Trigger re-evaluation
keyboardController.selectionVersion
if (!listView.keyboardActive) return -1
const selection = keyboardController.getCurrentSelection()
return (selection.type === "notification" && selection.groupIndex === index)
@@ -95,7 +97,6 @@ DankListView {
}
// Connect to notification changes and rebuild navigation
Connections {
function onGroupedNotificationsChanged() {
if (keyboardController) {
@@ -106,10 +107,11 @@ DankListView {
keyboardController.rebuildFlatNavigation()
// If keyboard navigation is active, ensure selected item stays visible
if (keyboardController.keyboardNavigationActive) {
Qt.callLater(function() {
keyboardController.ensureVisible()
if (!autoScrollDisabled) {
keyboardController.ensureVisible()
}
})
}
}
@@ -118,7 +120,9 @@ DankListView {
function onExpandedGroupsChanged() {
if (keyboardController && keyboardController.keyboardNavigationActive) {
Qt.callLater(function() {
keyboardController.ensureVisible()
if (!autoScrollDisabled) {
keyboardController.ensureVisible()
}
})
}
}
@@ -126,7 +130,9 @@ DankListView {
function onExpandedMessagesChanged() {
if (keyboardController && keyboardController.keyboardNavigationActive) {
Qt.callLater(function() {
keyboardController.ensureVisible()
if (!autoScrollDisabled) {
keyboardController.ensureVisible()
}
})
}
}

View File

@@ -34,25 +34,40 @@ Rectangle {
return baseHeight
}
radius: Theme.cornerRadius
color: {
// Keyboard selection highlighting for collapsed groups
if (isGroupSelected && keyboardNavigationActive && !expanded) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15)
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
// Subtle group highlighting when navigating within expanded group
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
color: {
// Keyboard selection highlighting for groups (both collapsed and expanded)
if (isGroupSelected && keyboardNavigationActive) {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2)
}
// Very subtle group highlighting when navigating within expanded group
if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12)
}
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
}
border.color: {
// Keyboard selection highlighting for collapsed groups
if (isGroupSelected && keyboardNavigationActive && !expanded) {
return Theme.primary
// Keyboard selection highlighting for groups (both collapsed and expanded)
if (isGroupSelected && keyboardNavigationActive) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5)
}
// Subtle group border when navigating within expanded group
if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
}
// Critical notification styling
if (notificationGroup?.latestNotification?.urgency === NotificationUrgency.Critical) {
@@ -61,13 +76,13 @@ Rectangle {
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
}
border.width: {
// Keyboard selection highlighting for collapsed groups
if (isGroupSelected && keyboardNavigationActive && !expanded) {
return 2
// Keyboard selection highlighting for groups (both collapsed and expanded)
if (isGroupSelected && keyboardNavigationActive) {
return 1.5
}
// Subtle group border when navigating within expanded group
if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) {
return 1.5
return 1
}
// Critical notification styling
if (notificationGroup?.latestNotification?.urgency === NotificationUrgency.Critical) {
@@ -295,18 +310,6 @@ Rectangle {
width: parent.width
height: 40
// Subtle background for expanded group header when navigating within
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius / 2
color: (keyboardNavigationActive && selectedNotificationIndex >= 0)
? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
: "transparent"
border.color: (keyboardNavigationActive && selectedNotificationIndex >= 0)
? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
: "transparent"
border.width: (keyboardNavigationActive && selectedNotificationIndex >= 0) ? 1 : 0
}
Row {
anchors.left: parent.left
@@ -317,8 +320,7 @@ Rectangle {
StyledText {
text: notificationGroup?.appName || ""
color: (keyboardNavigationActive && selectedNotificationIndex >= 0)
? Theme.primary : Theme.surfaceText
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
anchors.verticalCenter: parent.verticalCenter
@@ -374,9 +376,23 @@ Rectangle {
return baseHeight
}
radius: Theme.cornerRadius
color: isSelected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : "transparent"
border.color: isSelected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
border.width: isSelected ? 2 : 1
color: isSelected ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.25) : "transparent"
border.color: isSelected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
border.width: isSelected ? 1 : 1
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on height {
enabled: false
@@ -548,7 +564,7 @@ Rectangle {
StyledText {
id: actionText
text: {
const baseText = modelData.text || ""
const baseText = modelData.text || "View"
if (keyboardNavigationActive && (isGroupSelected || selectedNotificationIndex >= 0)) {
return `${baseText} (${index + 1})`
}
@@ -638,7 +654,7 @@ Rectangle {
StyledText {
id: actionText
text: {
const baseText = modelData.text || ""
const baseText = modelData.text || "View"
if (keyboardNavigationActive && isGroupSelected) {
return `${baseText} (${index + 1})`
}

View File

@@ -18,15 +18,13 @@ PanelWindow {
property real triggerWidth: 40
property string triggerSection: "right"
// Keyboard navigation controller
NotificationKeyboardController {
id: keyboardController
listView: null // Set later to avoid binding loop
listView: null
isOpen: notificationHistoryVisible
onClose: function() { notificationHistoryVisible = false }
}
// Keyboard hints overlay
NotificationKeyboardHints {
id: keyboardHints
anchors.bottom: mainRect.bottom
@@ -72,18 +70,6 @@ PanelWindow {
Rectangle {
id: mainRect
function calculateHeight() {
let baseHeight = Theme.spacingL * 2
baseHeight += notificationHeader.height
baseHeight += Theme.spacingM
let listHeight = notificationList.listContentHeight
if (NotificationService.groupedNotifications.length === 0)
listHeight = 200
baseHeight += Math.min(listHeight, 600)
return Math.max(300, baseHeight)
}
readonly property real popupWidth: 400
readonly property real calculatedX: {
var centerX = root.triggerX + (root.triggerWidth / 2) - (popupWidth / 2)
@@ -105,7 +91,19 @@ PanelWindow {
}
width: popupWidth
height: calculateHeight()
height: {
let baseHeight = Theme.spacingL * 2
baseHeight += notificationHeader.height
// Use the final content height when expanded, not the animating height
baseHeight += (notificationSettings.expanded ? notificationSettings.contentHeight : 0)
baseHeight += Theme.spacingM * 2
let listHeight = notificationList.listContentHeight
if (NotificationService.groupedNotifications.length === 0)
listHeight = 200
baseHeight += Math.min(listHeight, 600)
return Math.max(300, Math.min(baseHeight, Screen.height * 0.8))
}
x: calculatedX
y: root.triggerY
color: Theme.popupBackground()
@@ -158,15 +156,19 @@ PanelWindow {
NotificationHeader {
id: notificationHeader
keyboardController: keyboardController
}
NotificationSettings {
id: notificationSettings
expanded: notificationHeader.showSettings
}
KeyboardNavigatedNotificationList {
id: notificationList
width: parent.width
height: parent.height - notificationHeader.height - contentColumnInner.spacing
// keyboardController set via Component.onCompleted to avoid binding loop
enableKeyboardNavigation: true
height: parent.height - notificationHeader.height - notificationSettings.height - contentColumnInner.spacing * 2
Component.onCompleted: {
if (keyboardController && notificationList) {
@@ -176,33 +178,15 @@ PanelWindow {
}
}
} // Column
} // FocusScope
Connections {
function onNotificationsChanged() {
mainRect.height = mainRect.calculateHeight()
}
function onGroupedNotificationsChanged() {
mainRect.height = mainRect.calculateHeight()
}
function onExpandedGroupsChanged() {
mainRect.height = mainRect.calculateHeight()
}
function onExpandedMessagesChanged() {
mainRect.height = mainRect.calculateHeight()
}
target: NotificationService
}
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}

View File

@@ -6,6 +6,9 @@ import qs.Widgets
Item {
id: root
property var keyboardController: null
property bool showSettings: false
width: parent.width
height: 32
@@ -68,15 +71,80 @@ Item {
}
}
Rectangle {
id: clearAllButton
width: 120
height: 28
radius: Theme.cornerRadius
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: NotificationService.notifications.length > 0
spacing: Theme.spacingXS
// Settings button
Rectangle {
id: settingsButton
width: 28
height: 28
radius: Theme.cornerRadius
color: settingsArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.12) : (root.showSettings ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent")
border.color: settingsArea.containsMouse ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
DankIcon {
name: "settings"
size: 16
color: settingsArea.containsMouse ? Theme.primary : Theme.surfaceText
anchors.centerIn: parent
}
MouseArea {
id: settingsArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.showSettings = !root.showSettings
}
}
// Keyboard help button
Rectangle {
id: helpButton
width: 28
height: 28
radius: Theme.cornerRadius
visible: keyboardController !== null
color: helpArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.12) : (keyboardController && keyboardController.showKeyboardHints ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent")
border.color: helpArea.containsMouse ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
StyledText {
text: "?"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: helpArea.containsMouse ? Theme.primary : Theme.surfaceText
anchors.centerIn: parent
}
MouseArea {
id: helpArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (keyboardController) {
keyboardController.showKeyboardHints = !keyboardController.showKeyboardHints
}
}
}
}
Rectangle {
id: clearAllButton
width: 120
height: 28
radius: Theme.cornerRadius
visible: NotificationService.notifications.length > 0
color: clearArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.12) : Qt.rgba(
Theme.surfaceVariant.r,
@@ -129,5 +197,6 @@ Item {
easing.type: Theme.standardEasing
}
}
}
}
}

View File

@@ -5,29 +5,23 @@ import qs.Services
QtObject {
id: controller
// Properties that need to be set by parent
property var listView: null
property bool isOpen: false
property var onClose: null // Function to call when closing
property var onClose: null
// Property that changes to trigger binding updates
property int selectionVersion: 0
// Keyboard navigation state
property bool keyboardNavigationActive: false
property int selectedFlatIndex: 0
property var flatNavigation: []
property int flatNavigationVersion: 0 // For triggering bindings
property bool showKeyboardHints: false
// Track selection by ID for position preservation
property string selectedNotificationId: ""
property string selectedGroupKey: ""
property string selectedItemType: ""
property bool isTogglingGroup: false
property bool isRebuilding: false
// Build flat navigation array
function rebuildFlatNavigation() {
isRebuilding = true
@@ -133,6 +127,11 @@ QtObject {
keyboardNavigationActive = true
if (flatNavigation.length === 0) return
// Re-enable auto-scrolling when arrow keys are used
if (listView && listView.enableAutoScroll) {
listView.enableAutoScroll()
}
selectedFlatIndex = Math.min(selectedFlatIndex + 1, flatNavigation.length - 1)
updateSelectedIdFromIndex()
selectionVersion++
@@ -143,6 +142,11 @@ QtObject {
keyboardNavigationActive = true
if (flatNavigation.length === 0) return
// Re-enable auto-scrolling when arrow keys are used
if (listView && listView.enableAutoScroll) {
listView.enableAutoScroll()
}
selectedFlatIndex = Math.max(selectedFlatIndex - 1, 0)
updateSelectedIdFromIndex()
selectionVersion++
@@ -200,18 +204,14 @@ QtObject {
if (!group) return
if (currentItem.type === "group") {
// On group: expand/collapse the group (only if it has > 1 notification)
const notificationCount = group.notifications ? group.notifications.length : 0
if (notificationCount >= 2) {
toggleGroupExpanded()
}
} else if (currentItem.type === "notification") {
// On individual notification: execute first action if available
const notification = group.notifications[currentItem.notificationIndex]
const actions = notification?.actions || []
if (actions.length > 0) {
} else {
executeAction(0)
}
} else if (currentItem.type === "notification") {
executeAction(0)
}
}
@@ -313,9 +313,18 @@ QtObject {
nextTargetGroupKey = groups[currentItem.groupIndex - 1].key
}
} else if (isLastNotificationInList) {
nextTargetType = "group"
nextTargetGroupKey = currentGroupKey
nextTargetNotificationIndex = -1
// If group still has notifications after this one is removed, select the new last one
if (group.count > 1) {
nextTargetType = "notification"
nextTargetGroupKey = currentGroupKey
// After removing current notification, the new last index will be count-2
nextTargetNotificationIndex = group.count - 2
} else {
// Group will be empty or collapsed, select the group header
nextTargetType = "group"
nextTargetGroupKey = currentGroupKey
nextTargetNotificationIndex = -1
}
} else {
nextTargetType = "notification"
nextTargetGroupKey = currentGroupKey

View File

@@ -1,27 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
DankListView {
id: root
property alias count: root.count
property alias listContentHeight: root.contentHeight
width: parent.width
height: parent.height
clip: true
model: NotificationService.groupedNotifications
spacing: Theme.spacingL
NotificationEmptyState {
visible: root.count === 0
anchors.centerIn: parent
}
delegate: NotificationCard {
notificationGroup: modelData
}
}

View File

@@ -0,0 +1,179 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property bool expanded: false
readonly property real contentHeight: contentColumn.height + Theme.spacingL * 2
width: parent.width
height: expanded ? Math.min(contentHeight, 400) : 0
visible: expanded
clip: true
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
border.width: 1
Behavior on height {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
// Ensure smooth opacity transition
opacity: expanded ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
readonly property var timeoutOptions: [
{ text: "Never", value: 0 },
{ text: "1 second", value: 1000 },
{ text: "3 seconds", value: 3000 },
{ text: "5 seconds", value: 5000 },
{ text: "8 seconds", value: 8000 },
{ text: "10 seconds", value: 10000 },
{ text: "15 seconds", value: 15000 },
{ text: "30 seconds", value: 30000 },
{ text: "1 minute", value: 60000 },
{ text: "2 minutes", value: 120000 },
{ text: "5 minutes", value: 300000 },
{ text: "10 minutes", value: 600000 }
]
function getTimeoutText(value) {
if (value === undefined || value === null || isNaN(value)) {
return "5 seconds"
}
for (let i = 0; i < timeoutOptions.length; i++) {
if (timeoutOptions[i].value === value) {
return timeoutOptions[i].text
}
}
if (value === 0) return "Never"
if (value < 1000) return value + "ms"
if (value < 60000) return Math.round(value / 1000) + " seconds"
return Math.round(value / 60000) + " minutes"
}
Column {
id: contentColumn
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
StyledText {
text: "Notification Settings"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
}
Item {
width: parent.width
height: 36
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: SessionData.doNotDisturb ? "notifications_off" : "notifications"
size: Theme.iconSizeSmall
color: SessionData.doNotDisturb ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Do Not Disturb"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
checked: SessionData.doNotDisturb
onToggled: SessionData.setDoNotDisturb(!SessionData.doNotDisturb)
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
}
StyledText {
text: "Notification Timeouts"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
}
DankDropdown {
width: parent.width
text: "Low Priority"
description: "Timeout for low priority notifications"
currentValue: getTimeoutText(SettingsData.notificationTimeoutLow)
options: timeoutOptions.map(opt => opt.text)
onValueChanged: value => {
for (let i = 0; i < timeoutOptions.length; i++) {
if (timeoutOptions[i].text === value) {
SettingsData.setNotificationTimeoutLow(timeoutOptions[i].value)
break
}
}
}
}
DankDropdown {
width: parent.width
text: "Normal Priority"
description: "Timeout for normal priority notifications"
currentValue: getTimeoutText(SettingsData.notificationTimeoutNormal)
options: timeoutOptions.map(opt => opt.text)
onValueChanged: value => {
for (let i = 0; i < timeoutOptions.length; i++) {
if (timeoutOptions[i].text === value) {
SettingsData.setNotificationTimeoutNormal(timeoutOptions[i].value)
break
}
}
}
}
DankDropdown {
width: parent.width
text: "Critical Priority"
description: "Timeout for critical priority notifications"
currentValue: getTimeoutText(SettingsData.notificationTimeoutCritical)
options: timeoutOptions.map(opt => opt.text)
onValueChanged: value => {
for (let i = 0; i < timeoutOptions.length; i++) {
if (timeoutOptions[i].text === value) {
SettingsData.setNotificationTimeoutCritical(timeoutOptions[i].value)
break
}
}
}
}
}
}

View File

@@ -63,7 +63,6 @@ PanelWindow {
SettingsData.notificationOverlayEnabled
// If overlay is enabled for all notifications, or if it's a critical notification
const shouldUseOverlay = (SettingsData.notificationOverlayEnabled) ||
(notificationData.urgency === NotificationUrgency.Critical)
@@ -398,7 +397,7 @@ PanelWindow {
StyledText {
id: actionText
text: modelData.text || ""
text: modelData.text || "View"
color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium

View File

@@ -172,16 +172,44 @@ QtObject {
if (activeWindows.length <= maxTargetNotifications + 1)
return
// Find the bottom-most non-critical notification to remove
const candidates = activeWindows.filter(p => {
return p.notificationData && p.notificationData.notification &&
p.notificationData.notification.urgency !== 2 // NotificationUrgency.Critical = 2
}).sort((a, b) => b.screenY - a.screenY) // Sort by Y position, highest first
const expiredCandidates = activeWindows.filter(p => {
if (!p.notificationData || !p.notificationData.notification) return false
if (p.notificationData.notification.urgency === 2) return false
const timeoutMs = p.notificationData.timer ? p.notificationData.timer.interval : 5000
if (timeoutMs === 0) return false
return !p.notificationData.timer.running
}).sort((a, b) => b.screenY - a.screenY)
const toRemove = candidates[0] // Get the bottom-most non-critical notification
if (toRemove && !toRemove.exiting) {
toRemove.notificationData.removedByLimit = true
toRemove.notificationData.popup = false
if (expiredCandidates.length > 0) {
const toRemove = expiredCandidates[0]
if (toRemove && !toRemove.exiting) {
toRemove.notificationData.removedByLimit = true
toRemove.notificationData.popup = false
}
return
}
const timeoutCandidates = activeWindows.filter(p => {
if (!p.notificationData || !p.notificationData.notification) return false
if (p.notificationData.notification.urgency === 2) return false
const timeoutMs = p.notificationData.timer ? p.notificationData.timer.interval : 5000
return timeoutMs > 0
}).sort((a, b) => {
const aTimeout = a.notificationData.timer ? a.notificationData.timer.interval : 5000
const bTimeout = b.notificationData.timer ? b.notificationData.timer.interval : 5000
if (aTimeout !== bTimeout) return aTimeout - bTimeout
return b.screenY - a.screenY
})
if (timeoutCandidates.length > 0) {
const toRemove = timeoutCandidates[0]
if (toRemove && !toRemove.exiting) {
toRemove.notificationData.removedByLimit = true
toRemove.notificationData.popup = false
}
}
}

View File

@@ -217,6 +217,9 @@ binds {
Mod+M hotkey-overlay-title="Task Manager" {
spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "processlist" "toggle";
}
Mod+N hotkey-overlay-title="Notification Center" {
spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "notifications" "toggle";
}
Mod+Comma hotkey-overlay-title="Settings" {
spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "settings" "toggle";
}

View File

@@ -35,10 +35,9 @@ Singleton {
}
}
// Global timer to update all notification timestamps
Timer {
id: timeUpdateTimer
interval: 30000 // Update every 30 seconds
interval: 30000
repeat: true
running: root.allWrappers.length > 0
triggeredOnStart: false
@@ -50,7 +49,6 @@ Singleton {
property bool timeUpdateTick: false
property bool clockFormatChanged: false
// Android 16-style grouped notifications
readonly property var groupedNotifications: getGroupedNotifications()
readonly property var groupedPopups: getGroupedPopups()
@@ -189,7 +187,6 @@ Singleton {
readonly property string appIcon: notification.appIcon
readonly property string appName: {
if (notification.appName == "") {
// try to get the app name from the desktop entry
const entry = DesktopEntries.byId(notification.desktopEntry)
if (entry && entry.name) {
return entry.name.toLowerCase()
@@ -210,8 +207,6 @@ Singleton {
readonly property int urgency: notification.urgency
readonly property list<NotificationAction> actions: notification.actions
readonly property bool hasImage: image && image.length > 0
readonly property bool hasAppIcon: appIcon && appIcon.length > 0
readonly property Connections conn: Connections {
target: wrapper.notification.Retainable
@@ -316,7 +311,6 @@ Singleton {
visibleNotifications = [...visibleNotifications, next]
next.popup = true
// Start timeout timer if timeout > 0 (0 means never timeout)
if (next.timer.interval > 0) {
next.timer.start()
}
@@ -336,7 +330,6 @@ Singleton {
}
function releaseWrapper(w) {
// Remove from visible
let v = visibleNotifications.slice()
const vi = v.indexOf(w)
if (vi !== -1) {
@@ -344,7 +337,6 @@ Singleton {
visibleNotifications = v
}
// Remove from queue
let q = notificationQueue.slice()
const qi = q.indexOf(w)
if (qi !== -1) {
@@ -352,20 +344,16 @@ Singleton {
notificationQueue = q
}
// Destroy wrapper if non-persistent
if (w && w.destroy && !w.isPersistent) {
w.destroy()
}
}
// Android 16-style notification grouping functions
function getGroupKey(wrapper) {
// Priority 1: Use desktopEntry if available
if (wrapper.desktopEntry && wrapper.desktopEntry !== "") {
return wrapper.desktopEntry.toLowerCase()
}
// Priority 2: Use appName as fallback
return wrapper.appName.toLowerCase()
}
@@ -526,7 +514,6 @@ Singleton {
}
}
// Watch for clock format changes to update notification timestamps
Connections {
target: typeof SettingsData !== "undefined" ? SettingsData : null
function onUse24HourClockChanged() {