mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-30 23:17:51 -04:00
* feat(clipboard): clear only the filtered entries With a search query or type filter active, the clear button deleted every unpinned entry regardless of what the list was showing, so pruning a subset meant deleting entries one by one from the context menu. When the recents list is filtered, the button now clears exactly the entries it shows and the modal stays open on that filter; with no filter the previous behaviour is untouched. A filter that matches nothing does nothing at all, rather than falling back to clearing everything. The tooltip says "Clear Filtered" while a filter is active, and the confirmation names the count. Deleting one id at a time would have cost a round trip and a full state broadcast per entry, so this adds clipboard.deleteEntries: one bolt transaction, one state update, and it skips pinned entries server-side so a bulk delete can never take out saved items. Fixes #3198 * fix(clipboard): add translator context and list deleteEntries in IPC help Review follow-up. The two strings this branch adds carried no translator context: the "Clear Filtered" tooltip in ClipboardHeader and the confirmation body for a filtered clear in ClipboardHistoryContent. Both now say where they appear, and the second one names what %1 is, per CONTRIBUTING. "Clear All" on the same line is deliberately left alone. It is an existing shared term with four other call sites, so a context there would attach a clipboard-specific comment to a term the printer tab also uses. clipboard.deleteEntries was handled but absent from the IPC method listing that `dms debug-srv` prints, which left it undiscoverable right next to clipboard.deleteEntry. Catalogs untouched. * chore: re-trigger CI The lint-and-test failure on21cefbfis an unrelated flake: TestStoreEntry_CorruptDBReturnsErrorNotPanic died with SIGBUS in the bbolt mmap. That test comes from master (c68b7e5) and passes there; this branch only adds TestDeleteEntries_*/TestHandleDeleteEntries_* and touches manager.go additively. * clipboard: survive mmap faults from corrupted db pages --------- Co-authored-by: bbedward <[email protected]>
377 lines
14 KiB
QML
377 lines
14 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import qs.Common
|
|
import qs.Widgets
|
|
import qs.Services
|
|
|
|
Item {
|
|
id: clipboardContent
|
|
|
|
required property var modal
|
|
property var transientSurfaceTracker: null
|
|
|
|
property alias searchField: searchField
|
|
property alias clipboardListView: clipboardListView
|
|
|
|
readonly property var filterOptions: [I18n.tr("All"), I18n.tr("Text"), I18n.tr("Long Text"), I18n.tr("Image")]
|
|
readonly property var filterValues: ["all", "text", "long_text", "image"]
|
|
|
|
function closeFilterMenu() {
|
|
filterMenuLoader.active = false;
|
|
filterMenuLoader.active = true;
|
|
}
|
|
|
|
function showContextMenu(entry, sceneX, sceneY) {
|
|
const localPos = mapFromItem(null, sceneX, sceneY);
|
|
contextMenu.show(localPos.x, localPos.y, entry);
|
|
}
|
|
|
|
function contextEntryAtScreen(screenX, screenY) {
|
|
const host = modal.surfaceHost ?? null;
|
|
const hostX = host?.alignedX;
|
|
const hostY = host?.renderedAlignedY ?? host?.alignedY;
|
|
|
|
if (!isNaN(hostX) && !isNaN(hostY))
|
|
return contextEntryAtLocal(screenX - hostX, screenY - hostY);
|
|
|
|
const screenRef = host?.effectiveScreen ?? host?.screen ?? modal.Window?.window?.screen ?? null;
|
|
const globalOrigin = mapToGlobal(0, 0);
|
|
const screenOriginX = screenRef?.x || 0;
|
|
const screenOriginY = screenRef?.y || 0;
|
|
return contextEntryAtLocal(screenOriginX + screenX - globalOrigin.x, screenOriginY + screenY - globalOrigin.y);
|
|
}
|
|
|
|
function contextEntryAtLocal(localX, localY) {
|
|
const listView = modal.activeTab === "saved" ? savedListView : clipboardListView;
|
|
const entries = modal.activeTab === "saved" ? modal.pinnedEntries : modal.unpinnedEntries;
|
|
|
|
if (!listView.visible || !entries)
|
|
return null;
|
|
|
|
const listPos = mapToItem(listView, localX, localY);
|
|
if (listPos.x < 0 || listPos.x > listView.width || listPos.y < 0 || listPos.y > listView.height)
|
|
return null;
|
|
|
|
const index = listView.indexAt(listPos.x + listView.contentX, listPos.y + listView.contentY);
|
|
if (index < 0 || index >= entries.length)
|
|
return null;
|
|
|
|
return {
|
|
entry: entries[index],
|
|
x: localX,
|
|
y: localY
|
|
};
|
|
}
|
|
|
|
function closeContextMenu() {
|
|
contextMenu.hide();
|
|
}
|
|
|
|
readonly property bool contextMenuActive: contextMenu.renderActive
|
|
|
|
anchors.fill: parent
|
|
|
|
ClipboardContextMenu {
|
|
id: contextMenu
|
|
modal: clipboardContent.modal
|
|
parentHandler: clipboardContent
|
|
transientSurfaceTracker: clipboardContent.transientSurfaceTracker
|
|
}
|
|
|
|
Column {
|
|
id: headerColumn
|
|
anchors.top: parent.top
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.margins: Theme.spacingM
|
|
spacing: Theme.spacingM
|
|
focus: false
|
|
|
|
ClipboardHeader {
|
|
id: header
|
|
width: parent.width
|
|
recentsCount: modal.unpinnedEntries.length
|
|
savedCount: modal.pinnedEntries.length
|
|
showKeyboardHints: modal.showKeyboardHints
|
|
activeTab: modal.activeTab
|
|
pinnedCount: modal.pinnedCount
|
|
clearsFilteredOnly: modal.clearsFilteredOnly
|
|
onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints
|
|
onTabChanged: tabName => modal.activeTab = tabName
|
|
onClearAllClicked: modal.confirmClearAll()
|
|
onCloseClicked: modal.hide()
|
|
}
|
|
|
|
Item {
|
|
id: searchRow
|
|
width: parent.width
|
|
implicitHeight: searchField.height
|
|
|
|
DankTextField {
|
|
id: searchField
|
|
|
|
width: parent.width
|
|
rightAccessoryWidth: filterButton.width + Theme.spacingS
|
|
placeholderText: ""
|
|
leftIconName: "search"
|
|
showClearButton: true
|
|
focus: true
|
|
ignoreTabKeys: true
|
|
keyForwardTargets: [modal.modalFocusScope]
|
|
|
|
onTextChanged: {
|
|
modal.searchText = text;
|
|
modal.updateFilteredModel();
|
|
ClipboardService.selectedIndex = 0;
|
|
ClipboardService.keyboardNavigationActive = true;
|
|
Qt.callLater(function () {
|
|
clipboardListView.positionViewAtBeginning();
|
|
savedListView.positionViewAtBeginning();
|
|
});
|
|
}
|
|
|
|
Keys.onEscapePressed: function (event) {
|
|
modal.hide();
|
|
event.accepted = true;
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
Qt.callLater(function () {
|
|
forceActiveFocus();
|
|
});
|
|
}
|
|
}
|
|
|
|
DankActionButton {
|
|
id: filterButton
|
|
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
iconName: "filter_list"
|
|
iconColor: modal.activeFilter !== "all" ? Theme.primary : Theme.surfaceText
|
|
backgroundColor: modal.activeFilter !== "all" ? Theme.primarySelected : Theme.withAlpha(Theme.primarySelected, 0)
|
|
tooltipText: I18n.tr("Filter by type", "Clipboard history type filter button tooltip")
|
|
onClicked: filterMenuLoader.item?.openDropdownMenu()
|
|
}
|
|
|
|
Loader {
|
|
id: filterMenuLoader
|
|
|
|
active: true
|
|
sourceComponent: filterMenuComponent
|
|
}
|
|
|
|
Component {
|
|
id: filterMenuComponent
|
|
|
|
DankDropdown {
|
|
showTrigger: false
|
|
popupAnchorItem: filterButton
|
|
popupWidth: 180
|
|
alignPopupRight: true
|
|
options: clipboardContent.filterOptions
|
|
currentValue: {
|
|
const idx = clipboardContent.filterValues.indexOf(clipboardContent.modal.activeFilter);
|
|
return idx >= 0 ? clipboardContent.filterOptions[idx] : clipboardContent.filterOptions[0];
|
|
}
|
|
|
|
onValueChanged: value => {
|
|
const idx = clipboardContent.filterOptions.indexOf(value);
|
|
if (idx >= 0) {
|
|
clipboardContent.modal.activeFilter = clipboardContent.filterValues[idx];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: listContainer
|
|
anchors.top: headerColumn.bottom
|
|
anchors.topMargin: Theme.spacingM
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.bottom: parent.bottom
|
|
anchors.leftMargin: Theme.spacingM
|
|
anchors.rightMargin: Theme.spacingM
|
|
anchors.bottomMargin: (modal.showKeyboardHints ? (ClipboardConstants.keyboardHintsHeight + Theme.spacingM * 2) : 0) + Theme.spacingXS
|
|
clip: true
|
|
|
|
DankListView {
|
|
id: clipboardListView
|
|
anchors.fill: parent
|
|
model: ScriptModel {
|
|
values: clipboardContent.modal.unpinnedEntries
|
|
objectProp: "id"
|
|
}
|
|
visible: modal.activeTab === "recents"
|
|
|
|
currentIndex: clipboardContent.modal ? clipboardContent.modal.selectedIndex : 0
|
|
spacing: Theme.spacingXS
|
|
interactive: true
|
|
flickDeceleration: 1500
|
|
maximumFlickVelocity: 2000
|
|
boundsBehavior: Flickable.DragAndOvershootBounds
|
|
boundsMovement: Flickable.FollowBoundsBehavior
|
|
pressDelay: 0
|
|
flickableDirection: Flickable.VerticalFlick
|
|
|
|
function ensureVisible(index) {
|
|
if (index < 0 || index >= count) {
|
|
return;
|
|
}
|
|
positionViewAtIndex(index, ListView.Contain);
|
|
}
|
|
|
|
onCurrentIndexChanged: {
|
|
if (clipboardContent.modal?.keyboardNavigationActive && currentIndex >= 0) {
|
|
ensureVisible(currentIndex);
|
|
}
|
|
}
|
|
|
|
StyledText {
|
|
text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No recent clipboard entries found") : I18n.tr("Connecting to clipboard service...")
|
|
anchors.centerIn: parent
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceVariantText
|
|
visible: clipboardContent.modal.unpinnedEntries.length === 0
|
|
}
|
|
|
|
delegate: ClipboardEntry {
|
|
required property int index
|
|
required property var modelData
|
|
|
|
width: clipboardListView.width
|
|
height: ClipboardConstants.itemHeight
|
|
entry: modelData
|
|
entryIndex: index + 1
|
|
itemIndex: index
|
|
isSelected: clipboardContent.modal?.keyboardNavigationActive && index === clipboardContent.modal.selectedIndex
|
|
modal: clipboardContent.modal
|
|
listView: clipboardListView
|
|
onCopyRequested: clipboardContent.modal.copyEntry(modelData)
|
|
onPasteRequested: clipboardContent.modal.pasteEntry(modelData)
|
|
onDeleteRequested: clipboardContent.modal.deleteEntry(modelData)
|
|
onPinRequested: targetEntry => clipboardContent.modal.pinEntry(targetEntry)
|
|
onUnpinRequested: targetEntry => clipboardContent.modal.unpinEntry(targetEntry)
|
|
onEditRequested: clipboardContent.modal.editEntry(modelData)
|
|
onContextMenuRequested: (mouseX, mouseY) => clipboardContent.showContextMenu(modelData, mouseX, mouseY)
|
|
}
|
|
}
|
|
|
|
DankListView {
|
|
id: savedListView
|
|
anchors.fill: parent
|
|
model: ScriptModel {
|
|
values: clipboardContent.modal.pinnedEntries
|
|
objectProp: "id"
|
|
}
|
|
visible: modal.activeTab === "saved"
|
|
|
|
currentIndex: clipboardContent.modal ? clipboardContent.modal.selectedIndex : 0
|
|
spacing: Theme.spacingXS
|
|
interactive: true
|
|
flickDeceleration: 1500
|
|
maximumFlickVelocity: 2000
|
|
boundsBehavior: Flickable.DragAndOvershootBounds
|
|
boundsMovement: Flickable.FollowBoundsBehavior
|
|
pressDelay: 0
|
|
flickableDirection: Flickable.VerticalFlick
|
|
|
|
function ensureVisible(index) {
|
|
if (index < 0 || index >= count) {
|
|
return;
|
|
}
|
|
positionViewAtIndex(index, ListView.Contain);
|
|
}
|
|
|
|
onCurrentIndexChanged: {
|
|
if (clipboardContent.modal?.keyboardNavigationActive && currentIndex >= 0) {
|
|
ensureVisible(currentIndex);
|
|
}
|
|
}
|
|
|
|
StyledText {
|
|
text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No saved clipboard entries") : I18n.tr("Connecting to clipboard service...")
|
|
anchors.centerIn: parent
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceVariantText
|
|
visible: clipboardContent.modal.pinnedEntries.length === 0
|
|
}
|
|
|
|
delegate: ClipboardEntry {
|
|
required property int index
|
|
required property var modelData
|
|
|
|
width: savedListView.width
|
|
height: ClipboardConstants.itemHeight
|
|
entry: modelData
|
|
entryIndex: index + 1
|
|
itemIndex: index
|
|
isSelected: clipboardContent.modal?.keyboardNavigationActive && index === clipboardContent.modal.selectedIndex
|
|
modal: clipboardContent.modal
|
|
listView: savedListView
|
|
onCopyRequested: clipboardContent.modal.copyEntry(modelData)
|
|
onPasteRequested: clipboardContent.modal.pasteEntry(modelData)
|
|
onDeleteRequested: clipboardContent.modal.deletePinnedEntry(modelData)
|
|
onPinRequested: targetEntry => clipboardContent.modal.pinEntry(targetEntry)
|
|
onUnpinRequested: targetEntry => clipboardContent.modal.unpinEntry(targetEntry)
|
|
onEditRequested: clipboardContent.modal.editEntry(modelData)
|
|
onContextMenuRequested: (mouseX, mouseY) => clipboardContent.showContextMenu(modelData, mouseX, mouseY)
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: bottomFade
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.bottom: parent.bottom
|
|
height: 24
|
|
z: 100
|
|
visible: {
|
|
const listView = modal.activeTab === "recents" ? clipboardListView : savedListView;
|
|
if (listView.contentHeight <= listView.height)
|
|
return false;
|
|
const atBottom = listView.contentY >= listView.contentHeight - listView.height - 5;
|
|
return !atBottom;
|
|
}
|
|
gradient: Gradient {
|
|
GradientStop {
|
|
position: 0.0
|
|
color: "transparent"
|
|
}
|
|
GradientStop {
|
|
position: 1.0
|
|
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Loader {
|
|
id: keyboardHintsLoader
|
|
anchors.bottom: parent.bottom
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.leftMargin: Theme.spacingM
|
|
anchors.rightMargin: Theme.spacingM
|
|
anchors.bottomMargin: active ? Theme.spacingM : 0
|
|
active: modal.showKeyboardHints
|
|
height: active ? ClipboardConstants.keyboardHintsHeight : 0
|
|
|
|
Behavior on height {
|
|
NumberAnimation {
|
|
duration: Theme.shortDuration
|
|
easing.type: Theme.standardEasing
|
|
}
|
|
}
|
|
|
|
sourceComponent: ClipboardKeyboardHints {
|
|
pasteAvailable: modal.pasteAvailable
|
|
enterToPaste: SettingsData.clipboardEnterToPaste
|
|
}
|
|
}
|
|
}
|