diff options
| author | AlexanderCurl <alexc@alexc.hu> | 2026-04-18 17:46:06 +0100 |
|---|---|---|
| committer | AlexanderCurl <alexc@alexc.hu> | 2026-04-18 17:46:06 +0100 |
| commit | 70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 (patch) | |
| tree | ab1123d4067c1b086dd6faa7ee4ea643236b565a /raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard | |
| parent | 5d94c0a7d44a2255b81815a52a7056a94a39842d (diff) | |
| download | RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.tar.gz RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.zip | |
Replaced file structures for themes in the correct format
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard')
9 files changed, 1289 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardConstants.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardConstants.qml new file mode 100644 index 0000000..f796ffe --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardConstants.qml @@ -0,0 +1,21 @@ +pragma Singleton + +import QtQuick +import Quickshell + +Singleton { + id: root + readonly property int previewLength: 100 + readonly property int longTextThreshold: 200 + readonly property int modalWidth: 650 + readonly property int modalHeight: 550 + readonly property int popoutWidth: 550 + readonly property int popoutHeight: 500 + readonly property int itemHeight: 72 + readonly property int thumbnailSize: 100 + readonly property int retryInterval: 50 + readonly property int viewportBuffer: 100 + readonly property int extendedBuffer: 200 + readonly property int keyboardHintsHeight: 80 + readonly property int headerHeight: 32 +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardContent.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardContent.qml new file mode 100644 index 0000000..e00bc22 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardContent.qml @@ -0,0 +1,260 @@ +import QtQuick +import Quickshell +import qs.Common +import qs.Widgets + +Item { + id: clipboardContent + + required property var modal + required property var clearConfirmDialog + + property alias searchField: searchField + property alias clipboardListView: clipboardListView + + anchors.fill: parent + + 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 + totalCount: modal.totalCount + showKeyboardHints: modal.showKeyboardHints + activeTab: modal.activeTab + pinnedCount: modal.pinnedCount + onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints + onTabChanged: tabName => modal.activeTab = tabName + onClearAllClicked: { + const hasPinned = modal.pinnedCount > 0; + const message = hasPinned ? I18n.tr("This will delete all unpinned entries. %1 pinned entries will be kept.").arg(modal.pinnedCount) : I18n.tr("This will permanently delete all clipboard history."); + clearConfirmDialog.show(I18n.tr("Clear History?"), message, function () { + modal.clearAll(); + modal.hide(); + }, function () {}); + } + onCloseClicked: modal.hide() + } + + DankTextField { + id: searchField + width: parent.width + placeholderText: "" + leftIconName: "search" + showClearButton: true + focus: true + ignoreTabKeys: true + keyForwardTargets: [modal.modalFocusScope] + onTextChanged: { + modal.searchText = text; + modal.updateFilteredModel(); + } + Keys.onEscapePressed: function (event) { + modal.hide(); + event.accepted = true; + } + Component.onCompleted: { + Qt.callLater(function () { + forceActiveFocus(); + }); + } + + Connections { + target: modal + function onOpened() { + Qt.callLater(function () { + searchField.forceActiveFocus(); + }); + } + } + } + } + + 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) + onDeleteRequested: clipboardContent.modal.deleteEntry(modelData) + onPinRequested: clipboardContent.modal.pinEntry(modelData) + onUnpinRequested: clipboardContent.modal.unpinEntry(modelData) + } + } + + 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) + onDeleteRequested: clipboardContent.modal.deletePinnedEntry(modelData) + onPinRequested: clipboardContent.modal.pinEntry(modelData) + 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) + } + } + } + } + + 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 { + wtypeAvailable: modal.wtypeAvailable + enterToPaste: SettingsData.clipboardEnterToPaste + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardEntry.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardEntry.qml new file mode 100644 index 0000000..53a2ece --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardEntry.qml @@ -0,0 +1,155 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + required property var entry + required property int entryIndex + required property int itemIndex + required property bool isSelected + required property var modal + required property var listView + + signal copyRequested + signal deleteRequested + signal pinRequested + signal unpinRequested + + readonly property string entryType: modal ? modal.getEntryType(entry) : "text" + readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : "" + readonly property bool hasPinnedDuplicate: !entry.pinned && ClipboardService.hashedPinnedEntry(entry.hash) + + radius: Theme.cornerRadius + color: { + if (isSelected) { + return Theme.primaryPressed; + } + return mouseArea.containsMouse ? Theme.primaryHoverLight : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency); + } + + DankRipple { + id: rippleLayer + rippleColor: Theme.surfaceText + cornerRadius: root.radius + } + + Rectangle { + id: indexBadge + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + width: 24 + height: 24 + radius: 12 + color: Theme.primarySelected + + StyledText { + anchors.centerIn: parent + text: entryIndex.toString() + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Bold + color: Theme.primary + } + } + + Row { + id: actionButtons + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + DankActionButton { + iconName: "push_pin" + iconSize: Theme.iconSize - 6 + iconColor: (entry.pinned || hasPinnedDuplicate) ? Theme.primary : Theme.surfaceText + backgroundColor: (entry.pinned || hasPinnedDuplicate) ? Theme.primarySelected : "transparent" + onClicked: entry.pinned ? unpinRequested() : pinRequested() + } + + DankActionButton { + iconName: "close" + iconSize: Theme.iconSize - 6 + iconColor: Theme.surfaceText + onClicked: deleteRequested() + } + } + + Item { + anchors.left: indexBadge.right + anchors.leftMargin: Theme.spacingM + anchors.right: actionButtons.left + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + // height: contentColumn.implicitHeight + height: ClipboardConstants.itemHeight + clip: true + + ClipboardThumbnail { + id: thumbnail + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize + height: entryType === "image" ? ClipboardConstants.itemHeight - 4 : Theme.iconSize // 100 - 4 = 96, 96:72 = 4:3 + entry: root.entry + entryType: root.entryType + modal: root.modal + listView: root.listView + itemIndex: root.itemIndex + } + + Column { + id: contentColumn + anchors.left: thumbnail.right + anchors.leftMargin: Theme.spacingM + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + StyledText { + text: { + switch (entryType) { + case "image": + return I18n.tr("Image") + " • " + entryPreview; + case "long_text": + return I18n.tr("Long Text"); + default: + return I18n.tr("Text"); + } + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + font.weight: Font.Medium + width: parent.width + elide: Text.ElideRight + } + + StyledText { + text: entryPreview + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + width: parent.width + wrapMode: Text.WordWrap + maximumLineCount: entryType === "long_text" ? 3 : 1 + elide: Text.ElideRight + textFormat: Text.PlainText + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + anchors.rightMargin: 80 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: mouse => { + const pos = mouseArea.mapToItem(root, mouse.x, mouse.y); + rippleLayer.trigger(pos.x, pos.y); + } + onClicked: copyRequested() + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHeader.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHeader.qml new file mode 100644 index 0000000..fedba15 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHeader.qml @@ -0,0 +1,87 @@ +import QtQuick +import qs.Common +import qs.Widgets +import qs.Modals.Clipboard + +Item { + id: header + + property int totalCount: 0 + property bool showKeyboardHints: false + property string activeTab: "recents" + property int pinnedCount: 0 + + signal keyboardHintsToggled + signal clearAllClicked + signal closeClicked + signal tabChanged(string tabName) + + height: ClipboardConstants.headerHeight + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: "content_paste" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Clipboard History") + ` (${totalCount})` + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankActionButton { + iconName: "push_pin" + iconSize: Theme.iconSize - 4 + iconColor: header.activeTab === "saved" ? Theme.primary : Theme.surfaceText + visible: header.pinnedCount > 0 + tooltipText: I18n.tr("Saved") + onClicked: tabChanged("saved") + } + + DankActionButton { + iconName: "history" + iconSize: Theme.iconSize - 4 + iconColor: header.activeTab === "recents" ? Theme.primary : Theme.surfaceText + tooltipText: I18n.tr("History") + onClicked: tabChanged("recents") + } + + DankActionButton { + iconName: "info" + iconSize: Theme.iconSize - 4 + iconColor: showKeyboardHints ? Theme.primary : Theme.surfaceText + tooltipText: I18n.tr("Keyboard Shortcuts") + onClicked: keyboardHintsToggled() + } + + DankActionButton { + iconName: "delete_sweep" + iconSize: Theme.iconSize + iconColor: Theme.surfaceText + tooltipText: I18n.tr("Clear All") + onClicked: clearAllClicked() + } + + DankActionButton { + iconName: "close" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: closeClicked() + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml new file mode 100644 index 0000000..2f76cc7 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml @@ -0,0 +1,174 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell.Hyprland +import qs.Common +import qs.Modals.Clipboard +import qs.Modals.Common +import qs.Services + +DankModal { + id: clipboardHistoryModal + + layerNamespace: "dms:clipboard" + + HyprlandFocusGrab { + windows: [clipboardHistoryModal.contentWindow] + active: clipboardHistoryModal.useHyprlandFocusGrab && clipboardHistoryModal.shouldHaveFocus + } + + property string activeTab: "recents" + onActiveTabChanged: { + ClipboardService.selectedIndex = 0; + ClipboardService.keyboardNavigationActive = false; + } + property bool showKeyboardHints: false + property Component clipboardContent + 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 + + Ref { + service: ClipboardService + } + + function updateFilteredModel() { + ClipboardService.updateFilteredModel(); + } + + function pasteSelected() { + ClipboardService.pasteSelected(instantClose); + } + + function toggle() { + if (shouldBeVisible) { + hide(); + } else { + show(); + } + } + + function show() { + open(); + activeImageLoads = 0; + shouldHaveFocus = true; + ClipboardService.reset(); + if (clipboardAvailable) + ClipboardService.refresh(); + keyboardController.reset(); + + Qt.callLater(function () { + if (contentLoader.item?.searchField) { + contentLoader.item.searchField.text = ""; + contentLoader.item.searchField.forceActiveFocus(); + } + }); + } + + function hide() { + close(); + } + + onDialogClosed: { + 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); + } + + visible: false + modalWidth: ClipboardConstants.modalWidth + modalHeight: ClipboardConstants.modalHeight + backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + cornerRadius: Theme.cornerRadius + borderColor: Theme.outlineMedium + borderWidth: 1 + enableShadow: true + onBackgroundClicked: hide() + modalFocusScope.Keys.onPressed: function (event) { + keyboardController.handleKey(event); + } + content: clipboardContent + + ClipboardKeyboardController { + id: keyboardController + modal: clipboardHistoryModal + } + + ConfirmModal { + id: clearConfirmDialog + confirmButtonText: I18n.tr("Clear All") + confirmButtonColor: Theme.primary + onVisibleChanged: { + if (visible) { + clipboardHistoryModal.shouldHaveFocus = false; + return; + } + Qt.callLater(function () { + if (!clipboardHistoryModal.shouldBeVisible) { + return; + } + clipboardHistoryModal.shouldHaveFocus = true; + clipboardHistoryModal.modalFocusScope.forceActiveFocus(); + if (clipboardHistoryModal.contentLoader.item?.searchField) { + clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus(); + } + }); + } + } + + property var confirmDialog: clearConfirmDialog + + clipboardContent: Component { + ClipboardContent { + modal: clipboardHistoryModal + clearConfirmDialog: clipboardHistoryModal.confirmDialog + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHistoryPopout.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHistoryPopout.qml new file mode 100644 index 0000000..c4f0296 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHistoryPopout.qml @@ -0,0 +1,197 @@ +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() { + open(); + activeImageLoads = 0; + ClipboardService.reset(); + if (clipboardAvailable) + 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.popoutWidth + popupHeight: ClipboardConstants.popoutHeight + triggerWidth: 55 + positioning: "" + screen: triggerScreen + shouldBeVisible: false + contentHandlesKeys: true + + onBackgroundClicked: hide() + + onShouldBeVisibleChanged: { + if (!shouldBeVisible) + return; + if (clipboardAvailable) + 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 + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardKeyboardController.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardKeyboardController.qml new file mode 100644 index 0000000..f2a457e --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardKeyboardController.qml @@ -0,0 +1,166 @@ +import QtQuick +import qs.Common +import qs.Services + +QtObject { + id: keyboardController + + required property var modal + + function reset() { + ClipboardService.selectedIndex = 0; + ClipboardService.keyboardNavigationActive = false; + modal.showKeyboardHints = false; + } + + function selectNext() { + const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries; + if (!entries || entries.length === 0) { + return; + } + ClipboardService.keyboardNavigationActive = true; + ClipboardService.selectedIndex = Math.min(ClipboardService.selectedIndex + 1, entries.length - 1); + } + + function selectPrevious() { + const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries; + if (!entries || entries.length === 0) { + return; + } + ClipboardService.keyboardNavigationActive = true; + ClipboardService.selectedIndex = Math.max(ClipboardService.selectedIndex - 1, 0); + } + + function copySelected() { + const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries; + if (!entries || entries.length === 0 || ClipboardService.selectedIndex < 0 || ClipboardService.selectedIndex >= entries.length) { + return; + } + const selectedEntry = entries[ClipboardService.selectedIndex]; + modal.copyEntry(selectedEntry); + } + + function deleteSelected() { + const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries; + if (!entries || entries.length === 0 || ClipboardService.selectedIndex < 0 || ClipboardService.selectedIndex >= entries.length) { + return; + } + const selectedEntry = entries[ClipboardService.selectedIndex]; + if (modal.activeTab === "saved") { + modal.deletePinnedEntry(selectedEntry); + } else { + modal.deleteEntry(selectedEntry); + } + } + + function handleKey(event) { + switch (event.key) { + case Qt.Key_Escape: + if (ClipboardService.keyboardNavigationActive) { + ClipboardService.keyboardNavigationActive = false; + } else { + modal.hide(); + } + event.accepted = true; + return; + case Qt.Key_Down: + case Qt.Key_Tab: + if (!ClipboardService.keyboardNavigationActive) { + ClipboardService.keyboardNavigationActive = true; + ClipboardService.selectedIndex = 0; + } else { + selectNext(); + } + event.accepted = true; + return; + case Qt.Key_Up: + case Qt.Key_Backtab: + 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_F10: + modal.showKeyboardHints = !modal.showKeyboardHints; + event.accepted = true; + return; + } + + if (event.modifiers & Qt.ControlModifier) { + switch (event.key) { + case Qt.Key_N: + case Qt.Key_J: + if (!ClipboardService.keyboardNavigationActive) { + ClipboardService.keyboardNavigationActive = true; + ClipboardService.selectedIndex = 0; + } else { + selectNext(); + } + event.accepted = true; + return; + case Qt.Key_P: + case Qt.Key_K: + 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 (ClipboardService.keyboardNavigationActive) { + copySelected(); + event.accepted = true; + } + return; + } + } + + if (event.modifiers & Qt.ShiftModifier) { + switch (event.key) { + case Qt.Key_Delete: + modal.clearAll(); + modal.hide(); + event.accepted = true; + return; + case Qt.Key_Return: + case Qt.Key_Enter: + if (ClipboardService.keyboardNavigationActive) { + if (SettingsData.clipboardEnterToPaste) { + copySelected(); + } else { + modal.pasteSelected(); + } + event.accepted = true; + } + return; + } + } + + if (ClipboardService.keyboardNavigationActive) { + switch (event.key) { + case Qt.Key_Return: + case Qt.Key_Enter: + if (SettingsData.clipboardEnterToPaste) { + modal.pasteSelected(); + } else { + copySelected(); + } + event.accepted = true; + return; + case Qt.Key_Delete: + deleteSelected(); + event.accepted = true; + return; + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardKeyboardHints.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardKeyboardHints.qml new file mode 100644 index 0000000..0285952 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardKeyboardHints.qml @@ -0,0 +1,51 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Rectangle { + id: keyboardHints + + property bool wtypeAvailable: false + property bool enterToPaste: false + readonly property string hintsText: { + if (!wtypeAvailable) + return I18n.tr("Shift+Del: Clear All • Esc: Close"); + return enterToPaste ? I18n.tr("Shift+Enter: Copy • Shift+Del: Clear All • Esc: Close", "Keyboard hints when enter-to-paste is enabled") : I18n.tr("Shift+Enter: Paste • Shift+Del: Clear All • Esc: Close"); + } + + height: ClipboardConstants.keyboardHintsHeight + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) + border.color: Theme.primary + border.width: 2 + opacity: visible ? 1 : 0 + z: 100 + + Column { + anchors.centerIn: parent + spacing: 2 + + StyledText { + text: keyboardHints.enterToPaste + ? I18n.tr("↑/↓: Navigate • Enter: Paste • Del: Delete • F10: Help", "Keyboard hints when enter-to-paste is enabled") + : I18n.tr("↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: keyboardHints.hintsText + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardThumbnail.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardThumbnail.qml new file mode 100644 index 0000000..26a9210 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardThumbnail.qml @@ -0,0 +1,178 @@ +import QtQuick +import QtQuick.Effects +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: thumbnail + + required property var entry + required property string entryType + required property var modal + required property var listView + required property int itemIndex + + Image { + id: thumbnailImage + + property bool isVisible: false + property string cachedImageData: "" + property bool loadQueued: false + + anchors.fill: parent + source: cachedImageData ? `data:image/png;base64,${cachedImageData}` : "" + fillMode: Image.PreserveAspectCrop + smooth: true + cache: false + visible: false + asynchronous: true + sourceSize.width: 128 + sourceSize.height: 128 + + function tryLoadImage() { + if (thumbnailImage.loadQueued || entryType !== "image" || thumbnailImage.cachedImageData) { + return; + } + thumbnailImage.loadQueued = true; + if (modal.activeImageLoads < modal.maxConcurrentLoads) { + modal.activeImageLoads++; + thumbnailImage.loadImage(); + } else { + retryTimer.restart(); + } + } + + function loadImage() { + DMSService.sendRequest("clipboard.getEntry", { + "id": entry.id + }, function (response) { + thumbnailImage.loadQueued = false; + if (modal.activeImageLoads > 0) { + modal.activeImageLoads--; + } + if (response.error) { + console.warn("ClipboardThumbnail: Failed to load image:", entry.id); + return; + } + const data = response.result?.data; + if (data) { + thumbnailImage.cachedImageData = data; + } + }); + } + + Timer { + id: retryTimer + interval: ClipboardConstants.retryInterval + onTriggered: { + if (!thumbnailImage.loadQueued) { + return; + } + if (modal.activeImageLoads < modal.maxConcurrentLoads) { + modal.activeImageLoads++; + thumbnailImage.loadImage(); + } else { + retryTimer.restart(); + } + } + } + + Component.onCompleted: { + if (entryType !== "image" || listView.height <= 0) { + return; + } + + const itemY = itemIndex * (ClipboardConstants.itemHeight + listView.spacing); + const viewTop = listView.contentY; + const viewBottom = viewTop + listView.height; + isVisible = (itemY + ClipboardConstants.itemHeight >= viewTop && itemY <= viewBottom); + + if (isVisible) { + tryLoadImage(); + } + } + + Timer { + id: visibilityTimer + interval: 100 + onTriggered: thumbnailImage.checkVisibility() + } + + function checkVisibility() { + if (entryType !== "image" || listView.height <= 0 || isVisible) { + return; + } + const itemY = itemIndex * (ClipboardConstants.itemHeight + listView.spacing); + const viewTop = listView.contentY - ClipboardConstants.viewportBuffer; + const viewBottom = viewTop + listView.height + ClipboardConstants.extendedBuffer; + const nowVisible = (itemY + ClipboardConstants.itemHeight >= viewTop && itemY <= viewBottom); + if (nowVisible) { + isVisible = true; + tryLoadImage(); + } + } + + Connections { + target: listView + + function onContentYChanged() { + if (thumbnailImage.isVisible || entryType !== "image") { + return; + } + visibilityTimer.restart(); + } + + function onHeightChanged() { + if (thumbnailImage.isVisible || entryType !== "image") { + return; + } + visibilityTimer.restart(); + } + } + } + + MultiEffect { + anchors.fill: parent + anchors.margins: 2 + source: thumbnailImage + maskEnabled: true + maskSource: clipboardRoundedRectangularMask + visible: entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != "" + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + + Item { + id: clipboardRoundedRectangularMask + width: ClipboardConstants.thumbnailSize + height: ClipboardConstants.itemHeight - 4 + layer.enabled: true + layer.smooth: true + visible: false + + Rectangle { + anchors.fill: parent + radius: Theme.cornerRadius / 2 // Thumbnail corner radius is divided by 2 so it doesnt look weird on large corner radius (eg: 32px) + color: "black" + antialiasing: true + } + } + + DankIcon { + visible: !(entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != "") + name: { + switch (entryType) { + case "image": + return "image"; + case "long_text": + return "subject"; + default: + return "content_copy"; + } + } + size: Theme.iconSize + color: Theme.primary + anchors.centerIn: parent + } +} |