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

fix clipboard history issues

This commit is contained in:
bbedward
2025-08-26 14:59:55 -04:00
parent b887940dce
commit 9e16368222
5 changed files with 240 additions and 166 deletions

View File

@@ -12,13 +12,14 @@ DankModal {
property int totalCount: 0 property int totalCount: 0
property var activeTheme: Theme property var activeTheme: Theme
property bool showClearConfirmation: false
property var clipboardEntries: [] property var clipboardEntries: []
property string searchText: "" property string searchText: ""
property int selectedIndex: 0 property int selectedIndex: 0
property bool keyboardNavigationActive: false property bool keyboardNavigationActive: false
property bool showKeyboardHints: false property bool showKeyboardHints: false
property Component clipboardContent property Component clipboardContent
property int activeImageLoads: 0
readonly property int maxConcurrentLoads: 3
function updateFilteredModel() { function updateFilteredModel() {
filteredClipboardModel.clear() filteredClipboardModel.clear()
@@ -56,6 +57,7 @@ DankModal {
function show() { function show() {
open() open()
clipboardHistoryModal.searchText = "" clipboardHistoryModal.searchText = ""
clipboardHistoryModal.activeImageLoads = 0
initializeThumbnailSystem() initializeThumbnailSystem()
refreshClipboard() refreshClipboard()
@@ -72,6 +74,7 @@ DankModal {
function hide() { function hide() {
close() close()
clipboardHistoryModal.searchText = "" clipboardHistoryModal.searchText = ""
clipboardHistoryModal.activeImageLoads = 0
updateFilteredModel() updateFilteredModel()
keyboardController.reset() keyboardController.reset()
@@ -273,99 +276,20 @@ DankModal {
} }
} }
DankModal { ConfirmModal {
id: clearConfirmDialog id: clearConfirmDialog
visible: showClearConfirmation confirmButtonText: "Clear All"
width: 350 confirmButtonColor: Theme.error
height: 150
onBackgroundClicked: { onVisibleChanged: {
showClearConfirmation = false if (visible) {
} clipboardHistoryModal.shouldHaveFocus = false
} else if (clipboardHistoryModal.shouldBeVisible) {
content: Component { clipboardHistoryModal.shouldHaveFocus = true
Item { clipboardHistoryModal.modalFocusScope.forceActiveFocus()
anchors.fill: parent if (clipboardHistoryModal.contentLoader.item && clipboardHistoryModal.contentLoader.item.searchField) {
clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus()
Column {
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
spacing: Theme.spacingM
StyledText {
text: "Clear All History?"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "This will permanently delete all clipboard history."
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Rectangle {
width: 100
height: 40
radius: Theme.cornerRadius
color: cancelClearButton.containsMouse ? Theme.surfaceTextPressed : Theme.surfaceVariantAlpha
StyledText {
text: "Cancel"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: cancelClearButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: showClearConfirmation = false
}
}
Rectangle {
width: 100
height: 40
radius: Theme.cornerRadius
color: confirmClearButton.containsMouse ? Theme.errorPressed : Theme.error
StyledText {
text: "Clear All"
font.pixelSize: Theme.fontSizeMedium
color: Theme.primaryText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: confirmClearButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
clearAll()
showClearConfirmation = false
hide()
}
}
}
}
} }
} }
} }
@@ -531,7 +455,15 @@ DankModal {
iconColor: Theme.error iconColor: Theme.error
hoverColor: Theme.errorHover hoverColor: Theme.errorHover
onClicked: { onClicked: {
showClearConfirmation = true clearConfirmDialog.show(
"Clear All History?",
"This will permanently delete all clipboard history.",
function() {
clearAll()
hide()
},
function() {} // No action on cancel
)
} }
} }
@@ -702,32 +634,115 @@ DankModal {
height: entryType === "image" ? 48 : Theme.iconSize height: entryType === "image" ? 48 : Theme.iconSize
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
CachingImage { Image {
id: thumbnailImageSource id: thumbnailImageSource
property string entryId: model.entry.split( property string entryId: model.entry.split(
'\t')[0] '\t')[0]
property bool isVisible: false
property string cachedImageData: ""
property bool loadQueued: false
anchors.fill: parent anchors.fill: parent
source: entryType === "image" source: ""
&& imageLoader.imageData ? `data:image/png;base64,${imageLoader.imageData}` : ""
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
smooth: true smooth: true
cache: true cache: false // Disable Qt's cache to control it ourselves
visible: false visible: false
asynchronous: true asynchronous: true
sourceSize.width: 128
sourceSize.height: 128
onCachedImageDataChanged: {
if (cachedImageData) {
source = ""
source = `data:image/png;base64,${cachedImageData}`
}
}
function tryLoadImage() {
if (!loadQueued && entryType === "image" && !cachedImageData) {
loadQueued = true
if (clipboardHistoryModal.activeImageLoads < clipboardHistoryModal.maxConcurrentLoads) {
clipboardHistoryModal.activeImageLoads++
imageLoader.running = true
} else {
// Retry after delay
retryTimer.restart()
}
}
}
Timer {
id: retryTimer
interval: 50
onTriggered: {
if (thumbnailImageSource.loadQueued && !imageLoader.running) {
if (clipboardHistoryModal.activeImageLoads < clipboardHistoryModal.maxConcurrentLoads) {
clipboardHistoryModal.activeImageLoads++
imageLoader.running = true
} else {
retryTimer.restart()
}
}
}
}
Component.onCompleted: {
if (entryType !== "image") return
// Check if item is visible on screen initially
let itemY = index * (72 + clipboardListView.spacing)
let viewTop = clipboardListView.contentY
let viewBottom = viewTop + clipboardListView.height
isVisible = (itemY + 72 >= viewTop && itemY <= viewBottom)
if (isVisible) {
tryLoadImage()
}
}
Connections {
target: clipboardListView
function onContentYChanged() {
if (entryType !== "image") return
let itemY = index * (72 + clipboardListView.spacing)
let viewTop = clipboardListView.contentY - 100 // Preload slightly before visible
let viewBottom = viewTop + clipboardListView.height + 200
let nowVisible = (itemY + 72 >= viewTop && itemY <= viewBottom)
if (nowVisible && !thumbnailImageSource.isVisible) {
thumbnailImageSource.isVisible = true
thumbnailImageSource.tryLoadImage()
}
}
}
Process { Process {
id: imageLoader id: imageLoader
property string imageData: "" running: false
command: ["sh", "-c", `cliphist decode ${thumbnailImageSource.entryId} | base64 -w 0`] command: ["sh", "-c", `cliphist decode ${thumbnailImageSource.entryId} | base64 -w 0`]
running: entryType === "image"
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
imageLoader.imageData = text.trim() let imageData = text.trim()
if (imageData && imageData.length > 0) {
thumbnailImageSource.cachedImageData = imageData
}
}
}
onExited: exitCode => {
thumbnailImageSource.loadQueued = false
if (clipboardHistoryModal.activeImageLoads > 0) {
clipboardHistoryModal.activeImageLoads--
}
if (exitCode !== 0) {
console.warn("Failed to load clipboard image:", thumbnailImageSource.entryId)
} }
} }
} }
@@ -741,6 +756,7 @@ DankModal {
maskSource: clipboardCircularMask maskSource: clipboardCircularMask
visible: entryType === "image" visible: entryType === "image"
&& thumbnailImageSource.status === Image.Ready && thumbnailImageSource.status === Image.Ready
&& thumbnailImageSource.source != ""
maskThresholdMin: 0.5 maskThresholdMin: 0.5
maskSpreadAtMin: 1 maskSpreadAtMin: 1
} }
@@ -764,8 +780,8 @@ DankModal {
DankIcon { DankIcon {
visible: !(entryType === "image" visible: !(entryType === "image"
&& thumbnailImageSource.status && thumbnailImageSource.status === Image.Ready
=== Image.Ready) && thumbnailImageSource.source != "")
name: { name: {
if (entryType === "image") if (entryType === "image")
return "image" return "image"

View File

@@ -9,17 +9,39 @@ import qs.Widgets
DankModal { DankModal {
id: root id: root
property string powerConfirmAction: "" property string confirmTitle: ""
property string powerConfirmTitle: "" property string confirmMessage: ""
property string powerConfirmMessage: "" property string confirmButtonText: "Confirm"
property string cancelButtonText: "Cancel"
property color confirmButtonColor: Theme.error
property var onConfirm: function() {}
property var onCancel: function() {}
property int selectedButton: -1 // -1 = none, 0 = Cancel, 1 = Confirm property int selectedButton: -1 // -1 = none, 0 = Cancel, 1 = Confirm
property bool keyboardNavigation: false property bool keyboardNavigation: false
function show(action, title, message) { function show(title, message, onConfirmCallback, onCancelCallback) {
powerConfirmAction = action confirmTitle = title || ""
powerConfirmTitle = title confirmMessage = message || ""
powerConfirmMessage = message confirmButtonText = "Confirm"
selectedButton = -1 // No button selected initially cancelButtonText = "Cancel"
confirmButtonColor = Theme.error
onConfirm = onConfirmCallback || function() {}
onCancel = onCancelCallback || function() {}
selectedButton = -1
keyboardNavigation = false
open()
}
function showWithOptions(options) {
confirmTitle = options.title || ""
confirmMessage = options.message || ""
confirmButtonText = options.confirmText || "Confirm"
cancelButtonText = options.cancelText || "Cancel"
confirmButtonColor = options.confirmColor || Theme.error
onConfirm = options.onConfirm || function() {}
onCancel = options.onCancel || function() {}
selectedButton = -1
keyboardNavigation = false keyboardNavigation = false
open() open()
} }
@@ -27,41 +49,35 @@ DankModal {
function selectButton() { function selectButton() {
if (selectedButton === 0) { if (selectedButton === 0) {
close() close()
if (onCancel) onCancel()
} else { } else {
close() close()
executePowerAction(powerConfirmAction) if (onConfirm) onConfirm()
}
}
function executePowerAction(action) {
switch (action) {
case "logout":
CompositorService.logout()
break
case "suspend":
SessionService.suspend()
break
case "reboot":
SessionService.reboot()
break
case "poweroff":
SessionService.poweroff()
break
} }
} }
shouldBeVisible: false shouldBeVisible: false
allowStacking: true
width: 350 width: 350
height: 160 height: 160
enableShadow: false enableShadow: true
shouldHaveFocus: true
onBackgroundClicked: { onBackgroundClicked: {
close() close()
if (onCancel) onCancel()
} }
onOpened: { onOpened: {
modalFocusScope.forceActiveFocus() modalFocusScope.forceActiveFocus()
modalFocusScope.focus = true
shouldHaveFocus = true
} }
modalFocusScope.Keys.onPressed: function(event) { modalFocusScope.Keys.onPressed: function(event) {
switch (event.key) { switch (event.key) {
case Qt.Key_Escape:
close()
if (onCancel) onCancel()
event.accepted = true
break
case Qt.Key_Left: case Qt.Key_Left:
case Qt.Key_Up: case Qt.Key_Up:
keyboardNavigation = true keyboardNavigation = true
@@ -81,7 +97,12 @@ DankModal {
break break
case Qt.Key_Return: case Qt.Key_Return:
case Qt.Key_Enter: case Qt.Key_Enter:
selectButton() if (selectedButton !== -1) {
selectButton()
} else {
selectedButton = 1
selectButton()
}
event.accepted = true event.accepted = true
break break
} }
@@ -97,25 +118,16 @@ DankModal {
spacing: Theme.spacingM spacing: Theme.spacingM
StyledText { StyledText {
text: powerConfirmTitle text: confirmTitle
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
color: { color: Theme.surfaceText
switch (powerConfirmAction) {
case "poweroff":
return Theme.error
case "reboot":
return Theme.warning
default:
return Theme.surfaceText
}
}
font.weight: Font.Medium font.weight: Font.Medium
width: parent.width width: parent.width
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
} }
StyledText { StyledText {
text: powerConfirmMessage text: confirmMessage
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText color: Theme.surfaceText
width: parent.width width: parent.width
@@ -147,7 +159,7 @@ DankModal {
border.width: (keyboardNavigation && selectedButton === 0) ? 1 : 0 border.width: (keyboardNavigation && selectedButton === 0) ? 1 : 0
StyledText { StyledText {
text: "Cancel" text: cancelButtonText
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText color: Theme.surfaceText
font.weight: Font.Medium font.weight: Font.Medium
@@ -173,18 +185,7 @@ DankModal {
height: 40 height: 40
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
let baseColor let baseColor = confirmButtonColor
switch (powerConfirmAction) {
case "poweroff":
baseColor = Theme.error
break
case "reboot":
baseColor = Theme.warning
break
default:
baseColor = Theme.primary
break
}
if (keyboardNavigation && selectedButton === 1) if (keyboardNavigation && selectedButton === 1)
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1) return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1)
else if (confirmButton.containsMouse) else if (confirmButton.containsMouse)
@@ -196,7 +197,7 @@ DankModal {
border.width: (keyboardNavigation && selectedButton === 1) ? 1 : 0 border.width: (keyboardNavigation && selectedButton === 1) ? 1 : 0
StyledText { StyledText {
text: "Confirm" text: confirmButtonText
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.primaryText color: Theme.primaryText
font.weight: Font.Medium font.weight: Font.Medium

View File

@@ -34,7 +34,7 @@ PanelWindow {
demoActive = false demoActive = false
} }
PowerConfirmModal { ConfirmModal {
id: powerModal id: powerModal
} }

View File

@@ -20,7 +20,7 @@ WlSessionLockSurface {
color: "transparent" color: "transparent"
PowerConfirmModal { ConfirmModal {
id: powerConfirmModal id: powerConfirmModal
} }

View File

@@ -113,8 +113,27 @@ ShellRoot {
onPowerActionRequested: (action, title, message) => { onPowerActionRequested: (action, title, message) => {
powerConfirmModalLoader.active = true powerConfirmModalLoader.active = true
if (powerConfirmModalLoader.item) if (powerConfirmModalLoader.item) {
powerConfirmModalLoader.item.show(action, title, message) powerConfirmModalLoader.item.confirmButtonColor =
action === "poweroff" ? Theme.error :
action === "reboot" ? Theme.warning : Theme.primary
powerConfirmModalLoader.item.show(title, message, function() {
switch (action) {
case "logout":
CompositorService.logout()
break
case "suspend":
SessionService.suspend()
break
case "reboot":
SessionService.reboot()
break
case "poweroff":
SessionService.poweroff()
break
}
}, function() {})
}
} }
onLockRequested: { onLockRequested: {
lock.activate() lock.activate()
@@ -166,8 +185,27 @@ ShellRoot {
onPowerActionRequested: (action, title, message) => { onPowerActionRequested: (action, title, message) => {
powerConfirmModalLoader.active = true powerConfirmModalLoader.active = true
if (powerConfirmModalLoader.item) if (powerConfirmModalLoader.item) {
powerConfirmModalLoader.item.show(action, title, message) powerConfirmModalLoader.item.confirmButtonColor =
action === "poweroff" ? Theme.error :
action === "reboot" ? Theme.warning : Theme.primary
powerConfirmModalLoader.item.show(title, message, function() {
switch (action) {
case "logout":
CompositorService.logout()
break
case "suspend":
SessionService.suspend()
break
case "reboot":
SessionService.reboot()
break
case "poweroff":
SessionService.poweroff()
break
}
}, function() {})
}
} }
} }
@@ -178,7 +216,7 @@ ShellRoot {
active: false active: false
PowerConfirmModal { ConfirmModal {
id: powerConfirmModal id: powerConfirmModal
} }
@@ -243,8 +281,27 @@ ShellRoot {
onPowerActionRequested: (action, title, message) => { onPowerActionRequested: (action, title, message) => {
powerConfirmModalLoader.active = true powerConfirmModalLoader.active = true
if (powerConfirmModalLoader.item) if (powerConfirmModalLoader.item) {
powerConfirmModalLoader.item.show(action, title, message) powerConfirmModalLoader.item.confirmButtonColor =
action === "poweroff" ? Theme.error :
action === "reboot" ? Theme.warning : Theme.primary
powerConfirmModalLoader.item.show(title, message, function() {
switch (action) {
case "logout":
CompositorService.logout()
break
case "suspend":
SessionService.suspend()
break
case "reboot":
SessionService.reboot()
break
case "poweroff":
SessionService.poweroff()
break
}
}, function() {})
}
} }
} }