1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-31 08:52:49 -05:00

clipboard: add popout variant

This commit is contained in:
bbedward
2026-01-30 13:24:05 -05:00
parent f6f7b1ed72
commit f2d9066f90
9 changed files with 765 additions and 392 deletions

View File

@@ -527,6 +527,20 @@ Item {
}
}
LazyLoader {
id: clipboardHistoryPopoutLoader
active: false
ClipboardHistoryPopout {
id: clipboardHistoryPopout
Component.onCompleted: {
PopoutService.clipboardHistoryPopout = clipboardHistoryPopout;
}
}
}
ClipboardHistoryModal {
id: clipboardHistoryModalPopup
@@ -553,7 +567,7 @@ Item {
viewMode: SettingsData.appPickerViewMode || "grid"
onViewModeChanged: {
SettingsData.set("appPickerViewMode", viewMode)
SettingsData.set("appPickerViewMode", viewMode);
}
function shellEscape(str) {

View File

@@ -15,7 +15,10 @@ Item {
anchors.fill: parent
Column {
anchors.fill: parent
id: headerColumn
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
focus: false
@@ -72,15 +75,20 @@ Item {
}
}
}
}
Rectangle {
width: parent.width
height: parent.height - y - keyboardHintsContainer.height - Theme.spacingL
radius: Theme.cornerRadius
color: "transparent"
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
clip: true
// Recents Tab
DankListView {
id: clipboardListView
anchors.fill: parent
@@ -147,7 +155,6 @@ Item {
}
}
// Saved Tab
DankListView {
id: savedListView
anchors.fill: parent
@@ -192,12 +199,44 @@ Item {
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
}
}
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)
}
}
}
}
Item {
id: keyboardHintsContainer
width: parent.width
height: modal.showKeyboardHints ? ClipboardConstants.keyboardHintsHeight + Theme.spacingM : 0
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 {
@@ -205,15 +244,9 @@ Item {
easing.type: Theme.standardEasing
}
}
}
}
ClipboardKeyboardHints {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingM
visible: modal.showKeyboardHints
sourceComponent: ClipboardKeyboardHints {
wtypeAvailable: modal.wtypeAvailable
}
}
}

View File

@@ -2,8 +2,8 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell.Hyprland
import Quickshell.Io
import qs.Common
import qs.Modals.Clipboard
import qs.Modals.Common
import qs.Services
@@ -17,86 +17,35 @@ DankModal {
active: clipboardHistoryModal.useHyprlandFocusGrab && clipboardHistoryModal.shouldHaveFocus
}
property int totalCount: 0
property var clipboardEntries: []
property var pinnedEntries: []
property int pinnedCount: 0
property string searchText: ""
property int selectedIndex: 0
property bool keyboardNavigationActive: false
property string activeTab: "recents"
property bool showKeyboardHints: false
property Component clipboardContent
property int activeImageLoads: 0
readonly property int maxConcurrentLoads: 3
readonly property bool clipboardAvailable: DMSService.isConnected && (DMSService.capabilities.length === 0 || DMSService.capabilities.includes("clipboard"))
readonly property bool wtypeAvailable: SessionService.wtypeAvailable
Process {
id: wtypeProcess
command: ["wtype", "-M", "ctrl", "-P", "v", "-p", "v", "-m", "ctrl"]
running: false
}
readonly property bool clipboardAvailable: ClipboardService.clipboardAvailable
readonly property bool wtypeAvailable: ClipboardService.wtypeAvailable
readonly property int totalCount: ClipboardService.totalCount
readonly property var clipboardEntries: ClipboardService.clipboardEntries
readonly property var pinnedEntries: ClipboardService.pinnedEntries
readonly property int pinnedCount: ClipboardService.pinnedCount
readonly property var unpinnedEntries: ClipboardService.unpinnedEntries
readonly property int selectedIndex: ClipboardService.selectedIndex
readonly property bool keyboardNavigationActive: ClipboardService.keyboardNavigationActive
property string searchText: ClipboardService.searchText
onSearchTextChanged: ClipboardService.searchText = searchText
Timer {
id: pasteTimer
interval: 200
repeat: false
onTriggered: wtypeProcess.running = true
}
function pasteSelected() {
if (!keyboardNavigationActive || clipboardEntries.length === 0 || selectedIndex < 0 || selectedIndex >= clipboardEntries.length) {
return;
}
if (!wtypeAvailable) {
ToastService.showError(I18n.tr("wtype not available - install wtype for paste support"));
return;
}
const entry = clipboardEntries[selectedIndex];
DMSService.sendRequest("clipboard.copyEntry", {
"id": entry.id
}, function (response) {
if (response.error) {
ToastService.showError(I18n.tr("Failed to copy entry"));
return;
}
instantClose();
pasteTimer.start();
});
Ref {
service: ClipboardService
}
function updateFilteredModel() {
const query = searchText.trim();
let filtered = [];
if (query.length === 0) {
filtered = internalEntries;
} else {
const lowerQuery = query.toLowerCase();
filtered = internalEntries.filter(entry => entry.preview.toLowerCase().includes(lowerQuery));
ClipboardService.updateFilteredModel();
}
// Sort: pinned first, then by ID descending
filtered.sort((a, b) => {
if (a.pinned !== b.pinned)
return b.pinned ? 1 : -1;
return b.id - a.id;
});
clipboardEntries = filtered;
unpinnedEntries = filtered.filter(e => !e.pinned);
totalCount = clipboardEntries.length;
if (unpinnedEntries.length === 0) {
keyboardNavigationActive = false;
selectedIndex = 0;
} else if (selectedIndex >= unpinnedEntries.length) {
selectedIndex = unpinnedEntries.length - 1;
function pasteSelected() {
ClipboardService.pasteSelected(instantClose);
}
}
property var internalEntries: []
property var unpinnedEntries: []
property string activeTab: "recents"
function toggle() {
if (shouldBeVisible) {
@@ -112,10 +61,10 @@ DankModal {
return;
}
open();
searchText = "";
activeImageLoads = 0;
shouldHaveFocus = true;
refreshClipboard();
ClipboardService.reset();
ClipboardService.refresh();
keyboardController.reset();
Qt.callLater(function () {
@@ -128,141 +77,48 @@ DankModal {
function hide() {
close();
searchText = "";
}
onDialogClosed: {
activeImageLoads = 0;
internalEntries = [];
clipboardEntries = [];
ClipboardService.reset();
keyboardController.reset();
}
function refreshClipboard() {
DMSService.sendRequest("clipboard.getHistory", null, function (response) {
if (response.error) {
console.warn("ClipboardHistoryModal: Failed to get history:", response.error);
return;
}
internalEntries = response.result || [];
pinnedEntries = internalEntries.filter(e => e.pinned);
pinnedCount = pinnedEntries.length;
updateFilteredModel();
});
ClipboardService.refresh();
}
function copyEntry(entry) {
DMSService.sendRequest("clipboard.copyEntry", {
"id": entry.id
}, function (response) {
if (response.error) {
ToastService.showError(I18n.tr("Failed to copy entry"));
return;
}
ToastService.showInfo(entry.isImage ? I18n.tr("Image copied to clipboard") : I18n.tr("Copied to clipboard"));
hide();
});
ClipboardService.copyEntry(entry, hide);
}
function deleteEntry(entry) {
DMSService.sendRequest("clipboard.deleteEntry", {
"id": entry.id
}, function (response) {
if (response.error) {
console.warn("ClipboardHistoryModal: Failed to delete entry:", response.error);
return;
}
internalEntries = internalEntries.filter(e => e.id !== entry.id);
updateFilteredModel();
if (clipboardEntries.length === 0) {
keyboardNavigationActive = false;
selectedIndex = 0;
} else if (selectedIndex >= clipboardEntries.length) {
selectedIndex = clipboardEntries.length - 1;
}
});
ClipboardService.deleteEntry(entry);
}
function deletePinnedEntry(entry) {
clearConfirmDialog.show(I18n.tr("Delete Saved Item?"), I18n.tr("This will permanently remove this saved clipboard item. This action cannot be undone."), function () {
DMSService.sendRequest("clipboard.deleteEntry", {
"id": entry.id
}, function (response) {
if (response.error) {
console.warn("ClipboardHistoryModal: Failed to delete entry:", response.error);
return;
}
internalEntries = internalEntries.filter(e => e.id !== entry.id);
updateFilteredModel();
ToastService.showInfo(I18n.tr("Saved item deleted"));
});
}, function () {});
ClipboardService.deletePinnedEntry(entry, clearConfirmDialog);
}
function pinEntry(entry) {
DMSService.sendRequest("clipboard.getPinnedCount", null, function (countResponse) {
if (countResponse.error) {
ToastService.showError(I18n.tr("Failed to check pin limit"));
return;
}
const maxPinned = 25; // TODO: Get from config
if (countResponse.result.count >= maxPinned) {
ToastService.showError(I18n.tr("Maximum pinned entries reached") + " (" + maxPinned + ")");
return;
}
DMSService.sendRequest("clipboard.pinEntry", {
"id": entry.id
}, function (response) {
if (response.error) {
ToastService.showError(I18n.tr("Failed to pin entry"));
return;
}
ToastService.showInfo(I18n.tr("Entry pinned"));
refreshClipboard();
});
});
ClipboardService.pinEntry(entry);
}
function unpinEntry(entry) {
DMSService.sendRequest("clipboard.unpinEntry", {
"id": entry.id
}, function (response) {
if (response.error) {
ToastService.showError(I18n.tr("Failed to unpin entry"));
return;
}
ToastService.showInfo(I18n.tr("Entry unpinned"));
refreshClipboard();
});
ClipboardService.unpinEntry(entry);
}
function clearAll() {
const hasPinned = pinnedCount > 0;
DMSService.sendRequest("clipboard.clearHistory", null, function (response) {
if (response.error) {
console.warn("ClipboardHistoryModal: Failed to clear history:", response.error);
return;
}
refreshClipboard();
if (hasPinned) {
ToastService.showInfo(I18n.tr("History cleared. %1 pinned entries kept.").arg(pinnedCount));
}
});
ClipboardService.clearAll();
}
function getEntryPreview(entry) {
return entry.preview || "";
return ClipboardService.getEntryPreview(entry);
}
function getEntryType(entry) {
if (entry.isImage) {
return "image";
}
if (entry.size > ClipboardConstants.longTextThreshold) {
return "long_text";
}
return "text";
return ClipboardService.getEntryType(entry);
}
visible: false
@@ -284,20 +140,6 @@ DankModal {
modal: clipboardHistoryModal
}
Connections {
target: DMSService
function onClipboardStateUpdate(data) {
if (!clipboardHistoryModal.shouldBeVisible) {
return;
}
const newHistory = data.history || [];
internalEntries = newHistory;
pinnedEntries = newHistory.filter(e => e.pinned);
pinnedCount = pinnedEntries.length;
updateFilteredModel();
}
}
ConfirmModal {
id: clearConfirmDialog
confirmButtonText: I18n.tr("Clear All")

View File

@@ -0,0 +1,200 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modals.Clipboard
import qs.Modals.Common
import qs.Services
import qs.Widgets
DankPopout {
id: root
layerNamespace: "dms:clipboard-popout"
property var parentWidget: null
property var triggerScreen: null
property string activeTab: "recents"
property bool showKeyboardHints: false
property int activeImageLoads: 0
readonly property int maxConcurrentLoads: 3
readonly property bool clipboardAvailable: ClipboardService.clipboardAvailable
readonly property bool wtypeAvailable: ClipboardService.wtypeAvailable
readonly property int totalCount: ClipboardService.totalCount
readonly property var clipboardEntries: ClipboardService.clipboardEntries
readonly property var pinnedEntries: ClipboardService.pinnedEntries
readonly property int pinnedCount: ClipboardService.pinnedCount
readonly property var unpinnedEntries: ClipboardService.unpinnedEntries
readonly property int selectedIndex: ClipboardService.selectedIndex
readonly property bool keyboardNavigationActive: ClipboardService.keyboardNavigationActive
property string searchText: ClipboardService.searchText
onSearchTextChanged: ClipboardService.searchText = searchText
readonly property var modalFocusScope: contentLoader.item ?? null
Ref {
service: ClipboardService
}
function updateFilteredModel() {
ClipboardService.updateFilteredModel();
}
function pasteSelected() {
ClipboardService.pasteSelected(instantClose);
}
function instantClose() {
close();
}
function show() {
if (!clipboardAvailable) {
ToastService.showError(I18n.tr("Clipboard service not available"));
return;
}
open();
activeImageLoads = 0;
ClipboardService.reset();
ClipboardService.refresh();
keyboardController.reset();
Qt.callLater(function () {
if (contentLoader.item?.searchField) {
contentLoader.item.searchField.text = "";
contentLoader.item.searchField.forceActiveFocus();
}
});
}
function hide() {
close();
activeImageLoads = 0;
ClipboardService.reset();
keyboardController.reset();
}
function refreshClipboard() {
ClipboardService.refresh();
}
function copyEntry(entry) {
ClipboardService.copyEntry(entry, hide);
}
function deleteEntry(entry) {
ClipboardService.deleteEntry(entry);
}
function deletePinnedEntry(entry) {
ClipboardService.deletePinnedEntry(entry, clearConfirmDialog);
}
function pinEntry(entry) {
ClipboardService.pinEntry(entry);
}
function unpinEntry(entry) {
ClipboardService.unpinEntry(entry);
}
function clearAll() {
ClipboardService.clearAll();
}
function getEntryPreview(entry) {
return ClipboardService.getEntryPreview(entry);
}
function getEntryType(entry) {
return ClipboardService.getEntryType(entry);
}
popupWidth: ClipboardConstants.modalWidth
popupHeight: ClipboardConstants.modalHeight
triggerWidth: 55
positioning: ""
screen: triggerScreen
shouldBeVisible: false
contentHandlesKeys: true
onBackgroundClicked: hide()
onShouldBeVisibleChanged: {
if (!shouldBeVisible) {
return;
}
ClipboardService.refresh();
keyboardController.reset();
Qt.callLater(function () {
if (contentLoader.item?.searchField) {
contentLoader.item.searchField.text = "";
contentLoader.item.searchField.forceActiveFocus();
}
});
}
onPopoutClosed: {
activeImageLoads = 0;
ClipboardService.reset();
keyboardController.reset();
}
ClipboardKeyboardController {
id: keyboardController
modal: root
}
ConfirmModal {
id: clearConfirmDialog
confirmButtonText: I18n.tr("Clear All")
confirmButtonColor: Theme.primary
}
property var confirmDialog: clearConfirmDialog
content: Component {
FocusScope {
id: contentFocusScope
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
focus: true
property alias searchField: clipboardContentItem.searchField
Keys.onPressed: function (event) {
keyboardController.handleKey(event);
}
Component.onCompleted: {
if (root.shouldBeVisible)
forceActiveFocus();
}
Connections {
target: root
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) {
Qt.callLater(() => contentFocusScope.forceActiveFocus());
}
}
function onOpened() {
Qt.callLater(() => {
if (clipboardContentItem.searchField) {
clipboardContentItem.searchField.forceActiveFocus();
}
});
}
}
ClipboardContent {
id: clipboardContentItem
modal: root
clearConfirmDialog: root.confirmDialog
}
}
}
}

View File

@@ -1,4 +1,5 @@
import QtQuick
import qs.Services
QtObject {
id: keyboardController
@@ -6,48 +7,48 @@ QtObject {
required property var modal
function reset() {
modal.selectedIndex = 0;
modal.keyboardNavigationActive = false;
ClipboardService.selectedIndex = 0;
ClipboardService.keyboardNavigationActive = false;
modal.showKeyboardHints = false;
}
function selectNext() {
if (!modal.clipboardEntries || modal.clipboardEntries.length === 0) {
if (!ClipboardService.clipboardEntries || ClipboardService.clipboardEntries.length === 0) {
return;
}
modal.keyboardNavigationActive = true;
modal.selectedIndex = Math.min(modal.selectedIndex + 1, modal.clipboardEntries.length - 1);
ClipboardService.keyboardNavigationActive = true;
ClipboardService.selectedIndex = Math.min(ClipboardService.selectedIndex + 1, ClipboardService.clipboardEntries.length - 1);
}
function selectPrevious() {
if (!modal.clipboardEntries || modal.clipboardEntries.length === 0) {
if (!ClipboardService.clipboardEntries || ClipboardService.clipboardEntries.length === 0) {
return;
}
modal.keyboardNavigationActive = true;
modal.selectedIndex = Math.max(modal.selectedIndex - 1, 0);
ClipboardService.keyboardNavigationActive = true;
ClipboardService.selectedIndex = Math.max(ClipboardService.selectedIndex - 1, 0);
}
function copySelected() {
if (!modal.clipboardEntries || modal.clipboardEntries.length === 0 || modal.selectedIndex < 0 || modal.selectedIndex >= modal.clipboardEntries.length) {
if (!ClipboardService.clipboardEntries || ClipboardService.clipboardEntries.length === 0 || ClipboardService.selectedIndex < 0 || ClipboardService.selectedIndex >= ClipboardService.clipboardEntries.length) {
return;
}
const selectedEntry = modal.clipboardEntries[modal.selectedIndex];
const selectedEntry = ClipboardService.clipboardEntries[ClipboardService.selectedIndex];
modal.copyEntry(selectedEntry);
}
function deleteSelected() {
if (!modal.clipboardEntries || modal.clipboardEntries.length === 0 || modal.selectedIndex < 0 || modal.selectedIndex >= modal.clipboardEntries.length) {
if (!ClipboardService.clipboardEntries || ClipboardService.clipboardEntries.length === 0 || ClipboardService.selectedIndex < 0 || ClipboardService.selectedIndex >= ClipboardService.clipboardEntries.length) {
return;
}
const selectedEntry = modal.clipboardEntries[modal.selectedIndex];
const selectedEntry = ClipboardService.clipboardEntries[ClipboardService.selectedIndex];
modal.deleteEntry(selectedEntry);
}
function handleKey(event) {
switch (event.key) {
case Qt.Key_Escape:
if (modal.keyboardNavigationActive) {
modal.keyboardNavigationActive = false;
if (ClipboardService.keyboardNavigationActive) {
ClipboardService.keyboardNavigationActive = false;
} else {
modal.hide();
}
@@ -55,9 +56,9 @@ QtObject {
return;
case Qt.Key_Down:
case Qt.Key_Tab:
if (!modal.keyboardNavigationActive) {
modal.keyboardNavigationActive = true;
modal.selectedIndex = 0;
if (!ClipboardService.keyboardNavigationActive) {
ClipboardService.keyboardNavigationActive = true;
ClipboardService.selectedIndex = 0;
} else {
selectNext();
}
@@ -65,11 +66,11 @@ QtObject {
return;
case Qt.Key_Up:
case Qt.Key_Backtab:
if (!modal.keyboardNavigationActive) {
modal.keyboardNavigationActive = true;
modal.selectedIndex = 0;
} else if (modal.selectedIndex === 0) {
modal.keyboardNavigationActive = false;
if (!ClipboardService.keyboardNavigationActive) {
ClipboardService.keyboardNavigationActive = true;
ClipboardService.selectedIndex = 0;
} else if (ClipboardService.selectedIndex === 0) {
ClipboardService.keyboardNavigationActive = false;
} else {
selectPrevious();
}
@@ -85,9 +86,9 @@ QtObject {
switch (event.key) {
case Qt.Key_N:
case Qt.Key_J:
if (!modal.keyboardNavigationActive) {
modal.keyboardNavigationActive = true;
modal.selectedIndex = 0;
if (!ClipboardService.keyboardNavigationActive) {
ClipboardService.keyboardNavigationActive = true;
ClipboardService.selectedIndex = 0;
} else {
selectNext();
}
@@ -95,18 +96,18 @@ QtObject {
return;
case Qt.Key_P:
case Qt.Key_K:
if (!modal.keyboardNavigationActive) {
modal.keyboardNavigationActive = true;
modal.selectedIndex = 0;
} else if (modal.selectedIndex === 0) {
modal.keyboardNavigationActive = false;
if (!ClipboardService.keyboardNavigationActive) {
ClipboardService.keyboardNavigationActive = true;
ClipboardService.selectedIndex = 0;
} else if (ClipboardService.selectedIndex === 0) {
ClipboardService.keyboardNavigationActive = false;
} else {
selectPrevious();
}
event.accepted = true;
return;
case Qt.Key_C:
if (modal.keyboardNavigationActive) {
if (ClipboardService.keyboardNavigationActive) {
copySelected();
event.accepted = true;
}
@@ -123,7 +124,7 @@ QtObject {
return;
case Qt.Key_Return:
case Qt.Key_Enter:
if (modal.keyboardNavigationActive) {
if (ClipboardService.keyboardNavigationActive) {
modal.pasteSelected();
event.accepted = true;
}
@@ -131,7 +132,7 @@ QtObject {
}
}
if (modal.keyboardNavigationActive) {
if (ClipboardService.keyboardNavigationActive) {
switch (event.key) {
case Qt.Key_Return:
case Qt.Key_Enter:

View File

@@ -555,14 +555,57 @@ Item {
id: clipboardComponent
ClipboardButton {
id: clipboardWidget
widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness
axis: barWindow.axis
section: topBarContent.getWidgetSection(parent)
parentScreen: barWindow.screen
clipboardHistoryModal: PopoutService.clipboardHistoryModal
onClicked: {
clipboardHistoryModalPopup.toggle();
popoutTarget: {
clipboardHistoryPopoutLoader.active = true;
return clipboardHistoryPopoutLoader.item;
}
function openClipboardPopout(initialTab) {
clipboardHistoryPopoutLoader.active = true;
if (!clipboardHistoryPopoutLoader.item) {
return;
}
const popout = clipboardHistoryPopoutLoader.item;
const effectiveBarConfig = topBarContent.barConfig;
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
if (popout.setBarContext) {
popout.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0);
}
if (popout.setTriggerPosition) {
const globalPos = clipboardWidget.mapToItem(null, 0, 0);
const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, clipboardWidget.width, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig);
const widgetSection = topBarContent.getWidgetSection(parent) || "right";
popout.setTriggerPosition(pos.x, pos.y, pos.width, widgetSection, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig);
}
if (initialTab) {
popout.activeTab = initialTab;
}
PopoutManager.requestPopout(popout, undefined, "clipboard");
}
onClipboardClicked: openClipboardPopout("recents")
onShowSavedItemsRequested: openClipboardPopout("saved")
onClearAllRequested: {
clipboardHistoryPopoutLoader.active = true;
const popout = clipboardHistoryPopoutLoader.item;
if (!popout?.confirmDialog) {
return;
}
const hasPinned = popout.pinnedCount > 0;
const message = hasPinned ? I18n.tr("This will delete all unpinned entries. %1 pinned entries will be kept.").arg(popout.pinnedCount) : I18n.tr("This will permanently delete all clipboard history.");
popout.confirmDialog.show(I18n.tr("Clear History?"), message, function () {
if (popout && typeof popout.clearAll === "function") {
popout.clearAll();
}
}, function () {});
}
}
}

View File

@@ -4,30 +4,30 @@ import Quickshell.Wayland
import qs.Common
import qs.Modules.Plugins
import qs.Widgets
import qs.Services
BasePill {
id: root
property bool isActive: false
property var clipboardHistoryModal: null
property var popoutTarget: null
property var parentScreen: null
property Item windowRoot: (Window.window ? Window.window.contentItem : null)
property bool isAutoHideBar: false
signal clipboardClicked
signal showSavedItemsRequested
signal clearAllRequested
readonly property real minTooltipY: {
if (!parentScreen || !(axis?.isVertical ?? false)) {
return 0;
}
if (isAutoHideBar) {
return 0;
}
if (parentScreen.y > 0) {
return barThickness + barSpacing;
}
return 0;
}
@@ -51,15 +51,11 @@ BasePill {
let anchorY = relativeY;
if (isVertical) {
anchorX = edge === "left"
? (root.barThickness + root.barSpacing + gap)
: (screen.width - (root.barThickness + root.barSpacing + gap));
anchorX = edge === "left" ? (root.barThickness + root.barSpacing + gap) : (screen.width - (root.barThickness + root.barSpacing + gap));
anchorY = relativeY + root.minTooltipY;
} else {
anchorX = relativeX;
anchorY = edge === "bottom"
? (screen.height - (root.barThickness + root.barSpacing + gap))
: (root.barThickness + root.barSpacing + gap);
anchorY = edge === "bottom" ? (screen.height - (root.barThickness + root.barSpacing + gap)) : (root.barThickness + root.barSpacing + gap);
}
contextMenuWindow.showAt(anchorX, anchorY, isVertical, edge, screen);
@@ -67,10 +63,16 @@ BasePill {
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: function(mouse) {
if (mouse.button === Qt.RightButton) {
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onClicked: function (mouse) {
switch (mouse.button) {
case Qt.RightButton:
openContextMenu();
break;
case Qt.LeftButton:
clipboardClicked();
break;
}
}
}
@@ -232,23 +234,7 @@ BasePill {
cursorShape: Qt.PointingHandCursor
onClicked: {
contextMenuWindow.closeMenu();
if (root.clipboardHistoryModal && root.clipboardHistoryModal.confirmDialog) {
const hasPinned = root.clipboardHistoryModal.pinnedCount > 0;
const message = hasPinned
? I18n.tr("This will delete all unpinned entries. %1 pinned entries will be kept.").arg(root.clipboardHistoryModal.pinnedCount)
: I18n.tr("This will permanently delete all clipboard history.");
root.clipboardHistoryModal.confirmDialog.show(
I18n.tr("Clear History?"),
message,
function () {
if (root.clipboardHistoryModal && typeof root.clipboardHistoryModal.clearAll === "function") {
root.clipboardHistoryModal.clearAll();
}
},
function () {}
);
}
root.clearAllRequested();
}
}
}
@@ -288,16 +274,7 @@ BasePill {
cursorShape: Qt.PointingHandCursor
onClicked: {
contextMenuWindow.closeMenu();
if (root.clipboardHistoryModal) {
if (typeof root.clipboardHistoryModal.show === "function") {
root.clipboardHistoryModal.show();
}
Qt.callLater(function () {
if (root.clipboardHistoryModal) {
root.clipboardHistoryModal.activeTab = "saved";
}
});
}
root.showSavedItemsRequested();
}
}
}

View File

@@ -0,0 +1,262 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
Singleton {
id: root
readonly property int longTextThreshold: 200
readonly property bool clipboardAvailable: DMSService.isConnected && (DMSService.capabilities.length === 0 || DMSService.capabilities.includes("clipboard"))
readonly property bool wtypeAvailable: SessionService.wtypeAvailable
property var internalEntries: []
property var clipboardEntries: []
property var unpinnedEntries: []
property var pinnedEntries: []
property int pinnedCount: 0
property int totalCount: 0
property string searchText: ""
property int selectedIndex: 0
property bool keyboardNavigationActive: false
property int refCount: 0
signal historyCopied
signal historyCleared
Process {
id: wtypeProcess
command: ["wtype", "-M", "ctrl", "-P", "v", "-p", "v", "-m", "ctrl"]
running: false
}
Timer {
id: pasteTimer
interval: 200
repeat: false
onTriggered: wtypeProcess.running = true
}
function updateFilteredModel() {
const query = searchText.trim();
let filtered = [];
if (query.length === 0) {
filtered = internalEntries;
} else {
const lowerQuery = query.toLowerCase();
filtered = internalEntries.filter(entry => entry.preview.toLowerCase().includes(lowerQuery));
}
filtered.sort((a, b) => {
if (a.pinned !== b.pinned)
return b.pinned ? 1 : -1;
return b.id - a.id;
});
clipboardEntries = filtered;
unpinnedEntries = filtered.filter(e => !e.pinned);
totalCount = clipboardEntries.length;
if (unpinnedEntries.length === 0) {
keyboardNavigationActive = false;
selectedIndex = 0;
return;
}
if (selectedIndex >= unpinnedEntries.length) {
selectedIndex = unpinnedEntries.length - 1;
}
}
function refresh() {
if (!clipboardAvailable) {
return;
}
DMSService.sendRequest("clipboard.getHistory", null, function (response) {
if (response.error) {
console.warn("ClipboardService: Failed to get history:", response.error);
return;
}
internalEntries = response.result || [];
pinnedEntries = internalEntries.filter(e => e.pinned);
pinnedCount = pinnedEntries.length;
updateFilteredModel();
});
}
function reset() {
searchText = "";
selectedIndex = 0;
keyboardNavigationActive = false;
internalEntries = [];
clipboardEntries = [];
unpinnedEntries = [];
}
function copyEntry(entry, closeCallback) {
DMSService.sendRequest("clipboard.copyEntry", {
"id": entry.id
}, function (response) {
if (response.error) {
ToastService.showError(I18n.tr("Failed to copy entry"));
return;
}
ToastService.showInfo(entry.isImage ? I18n.tr("Image copied to clipboard") : I18n.tr("Copied to clipboard"));
historyCopied();
if (closeCallback) {
closeCallback();
}
});
}
function pasteEntry(entry, closeCallback) {
if (!wtypeAvailable) {
ToastService.showError(I18n.tr("wtype not available - install wtype for paste support"));
return;
}
DMSService.sendRequest("clipboard.copyEntry", {
"id": entry.id
}, function (response) {
if (response.error) {
ToastService.showError(I18n.tr("Failed to copy entry"));
return;
}
if (closeCallback) {
closeCallback();
}
pasteTimer.start();
});
}
function pasteSelected(closeCallback) {
if (!keyboardNavigationActive || clipboardEntries.length === 0 || selectedIndex < 0 || selectedIndex >= clipboardEntries.length) {
return;
}
pasteEntry(clipboardEntries[selectedIndex], closeCallback);
}
function deleteEntry(entry) {
DMSService.sendRequest("clipboard.deleteEntry", {
"id": entry.id
}, function (response) {
if (response.error) {
console.warn("ClipboardService: Failed to delete entry:", response.error);
return;
}
internalEntries = internalEntries.filter(e => e.id !== entry.id);
updateFilteredModel();
if (clipboardEntries.length === 0) {
keyboardNavigationActive = false;
selectedIndex = 0;
return;
}
if (selectedIndex >= clipboardEntries.length) {
selectedIndex = clipboardEntries.length - 1;
}
});
}
function deletePinnedEntry(entry, confirmDialog) {
if (!confirmDialog) {
return;
}
confirmDialog.show(I18n.tr("Delete Saved Item?"), I18n.tr("This will permanently remove this saved clipboard item. This action cannot be undone."), function () {
DMSService.sendRequest("clipboard.deleteEntry", {
"id": entry.id
}, function (response) {
if (response.error) {
console.warn("ClipboardService: Failed to delete entry:", response.error);
return;
}
internalEntries = internalEntries.filter(e => e.id !== entry.id);
updateFilteredModel();
ToastService.showInfo(I18n.tr("Saved item deleted"));
});
}, function () {});
}
function pinEntry(entry) {
DMSService.sendRequest("clipboard.getPinnedCount", null, function (countResponse) {
if (countResponse.error) {
ToastService.showError(I18n.tr("Failed to check pin limit"));
return;
}
const maxPinned = 25;
if (countResponse.result.count >= maxPinned) {
ToastService.showError(I18n.tr("Maximum pinned entries reached") + " (" + maxPinned + ")");
return;
}
DMSService.sendRequest("clipboard.pinEntry", {
"id": entry.id
}, function (response) {
if (response.error) {
ToastService.showError(I18n.tr("Failed to pin entry"));
return;
}
ToastService.showInfo(I18n.tr("Entry pinned"));
refresh();
});
});
}
function unpinEntry(entry) {
DMSService.sendRequest("clipboard.unpinEntry", {
"id": entry.id
}, function (response) {
if (response.error) {
ToastService.showError(I18n.tr("Failed to unpin entry"));
return;
}
ToastService.showInfo(I18n.tr("Entry unpinned"));
refresh();
});
}
function clearAll() {
const hasPinned = pinnedCount > 0;
const savedCount = pinnedCount;
DMSService.sendRequest("clipboard.clearHistory", null, function (response) {
if (response.error) {
console.warn("ClipboardService: Failed to clear history:", response.error);
return;
}
refresh();
historyCleared();
if (hasPinned) {
ToastService.showInfo(I18n.tr("History cleared. %1 pinned entries kept.").arg(savedCount));
}
});
}
function getEntryPreview(entry) {
return entry.preview || "";
}
function getEntryType(entry) {
if (entry.isImage) {
return "image";
}
if (entry.size > longTextThreshold) {
return "long_text";
}
return "text";
}
Connections {
target: DMSService
enabled: root.refCount > 0
function onClipboardStateUpdate(data) {
const newHistory = data.history || [];
internalEntries = newHistory;
pinnedEntries = newHistory.filter(e => e.pinned);
pinnedCount = pinnedEntries.length;
updateFilteredModel();
}
}
}

View File

@@ -20,6 +20,7 @@ Singleton {
property var settingsModal: null
property var settingsModalLoader: null
property var clipboardHistoryModal: null
property var clipboardHistoryPopout: null
property var dankLauncherV2Modal: null
property var dankLauncherV2ModalLoader: null
property var powerMenuModal: null