diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index d63c9800..10e4203c 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -108,6 +108,8 @@ Singleton { } property bool clipboardEnterToPaste: false + property bool clipboardRememberTypeFilter: false + property string clipboardTypeFilter: "all" property var clipboardVisibleEntryActions: ["pin", "edit", "delete"] property var launcherPluginVisibility: ({}) diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index ac3bd13c..fc845fd1 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -591,6 +591,8 @@ var SPEC = { builtInPluginSettings: { def: {} }, clipboardEnterToPaste: { def: false }, + clipboardRememberTypeFilter: { def: false }, + clipboardTypeFilter: { def: "all" }, clipboardVisibleEntryActions: { def: ["pin", "edit", "delete"] }, launcherPluginVisibility: { def: {} }, diff --git a/quickshell/Modals/Clipboard/ClipboardContent.qml b/quickshell/Modals/Clipboard/ClipboardContent.qml index 04113c85..b9ffe824 100644 --- a/quickshell/Modals/Clipboard/ClipboardContent.qml +++ b/quickshell/Modals/Clipboard/ClipboardContent.qml @@ -11,6 +11,14 @@ Item { 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; + } + anchors.fill: parent Column { @@ -36,27 +44,81 @@ Item { onCloseClicked: modal.hide() } - DankTextField { - id: searchField + Item { + id: searchRow width: parent.width - placeholderText: "" - leftIconName: "search" - showClearButton: true - focus: true - ignoreTabKeys: true - keyForwardTargets: [modal.modalFocusScope] - onTextChanged: { - modal.searchText = text; - modal.updateFilteredModel(); + 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(); + } + + Keys.onEscapePressed: function (event) { + modal.hide(); + event.accepted = true; + } + + Component.onCompleted: { + Qt.callLater(function () { + forceActiveFocus(); + }); + } } - Keys.onEscapePressed: function (event) { - modal.hide(); - event.accepted = true; + + 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 : "transparent" + tooltipText: I18n.tr("Filter by type", "Clipboard history type filter button tooltip") + onClicked: filterMenuLoader.item?.openDropdownMenu() } - Component.onCompleted: { - Qt.callLater(function () { - forceActiveFocus(); - }); + + 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]; + } + } + } } } } diff --git a/quickshell/Modals/Clipboard/ClipboardHeader.qml b/quickshell/Modals/Clipboard/ClipboardHeader.qml index 89372806..4d38e3f5 100644 --- a/quickshell/Modals/Clipboard/ClipboardHeader.qml +++ b/quickshell/Modals/Clipboard/ClipboardHeader.qml @@ -38,6 +38,7 @@ Item { font.weight: Font.Medium anchors.verticalCenter: parent.verticalCenter } + } Row { diff --git a/quickshell/Modals/Clipboard/ClipboardHistoryContent.qml b/quickshell/Modals/Clipboard/ClipboardHistoryContent.qml index 4733ccfd..8e3a6853 100644 --- a/quickshell/Modals/Clipboard/ClipboardHistoryContent.qml +++ b/quickshell/Modals/Clipboard/ClipboardHistoryContent.qml @@ -16,6 +16,7 @@ FocusScope { property string mode: "history" property string searchText: ClipboardService.searchText + property string activeFilter: SettingsData.clipboardRememberTypeFilter ? SettingsData.clipboardTypeFilter : "all" readonly property bool clipboardAvailable: ClipboardService.clipboardAvailable readonly property bool wtypeAvailable: ClipboardService.wtypeAvailable @@ -50,6 +51,16 @@ FocusScope { } onSearchTextChanged: ClipboardService.searchText = searchText + onActiveFilterChanged: { + ClipboardService.activeFilter = activeFilter; + ClipboardService.selectedIndex = 0; + ClipboardService.keyboardNavigationActive = false; + ClipboardService.updateFilteredModel(); + if (SettingsData.clipboardRememberTypeFilter) { + SettingsData.set("clipboardTypeFilter", activeFilter); + } + } + function hide() { closeRequested(); } @@ -118,6 +129,8 @@ FocusScope { function resetState() { activeImageLoads = 0; mode = "history"; + historyContent.closeFilterMenu(); + activeFilter = SettingsData.clipboardRememberTypeFilter ? SettingsData.clipboardTypeFilter : "all"; ClipboardService.reset(); keyboardController.reset(); } diff --git a/quickshell/Modules/Settings/ClipboardTab.qml b/quickshell/Modules/Settings/ClipboardTab.qml index ab842b0c..0ca8cf95 100644 --- a/quickshell/Modules/Settings/ClipboardTab.qml +++ b/quickshell/Modules/Settings/ClipboardTab.qml @@ -464,6 +464,16 @@ Item { onToggled: checked => SettingsData.set("clipboardEnterToPaste", checked) } + SettingsToggleRow { + tab: "clipboard" + tags: ["clipboard", "filter", "type", "remember", "behavior"] + settingKey: "clipboardRememberTypeFilter" + text: I18n.tr("Remember Type Filter", "Clipboard behavior setting title") + description: I18n.tr("Keep the clipboard type filter when reopening history", "Clipboard behavior setting description") + checked: SettingsData.clipboardRememberTypeFilter + onToggled: checked => SettingsData.set("clipboardRememberTypeFilter", checked) + } + SettingsButtonGroupRow { tab: "clipboard" tags: ["clipboard", "actions", "buttons", "hide", "density", "pin", "edit", "delete"] diff --git a/quickshell/Services/ClipboardService.qml b/quickshell/Services/ClipboardService.qml index 5d01a46e..fbd7a3f6 100644 --- a/quickshell/Services/ClipboardService.qml +++ b/quickshell/Services/ClipboardService.qml @@ -23,6 +23,7 @@ Singleton { property int pinnedCount: 0 property int totalCount: 0 property string searchText: "" + property string activeFilter: "all" property int selectedIndex: 0 property bool keyboardNavigationActive: false property int refCount: 0 @@ -50,14 +51,21 @@ Singleton { } function updateFilteredModel() { - const query = searchText.trim(); - let filtered = []; + let filtered = internalEntries; - if (query.length === 0) { - filtered = internalEntries; - } else { + if (activeFilter !== "all") { + filtered = filtered.filter(entry => + getEntryType(entry) === activeFilter + ); + } + + const query = searchText.trim(); + + if (query.length > 0) { const lowerQuery = query.toLowerCase(); - filtered = internalEntries.filter(entry => entry.preview.toLowerCase().includes(lowerQuery)); + filtered = filtered.filter(entry => + entry.preview.toLowerCase().includes(lowerQuery) + ); } filtered.sort((a, b) => { @@ -72,11 +80,13 @@ Singleton { totalCount = clipboardEntries.length; const activeCount = Math.max(unpinnedEntries.length, pinnedEntries.length); + if (activeCount === 0) { keyboardNavigationActive = false; selectedIndex = 0; return; } + if (selectedIndex >= activeCount) { selectedIndex = activeCount - 1; } diff --git a/quickshell/Widgets/DankDropdown.qml b/quickshell/Widgets/DankDropdown.qml index 2d8fc3b8..9aedde31 100644 --- a/quickshell/Widgets/DankDropdown.qml +++ b/quickshell/Widgets/DankDropdown.qml @@ -49,40 +49,59 @@ Item { property bool alignPopupRight: false property int dropdownWidth: 200 property bool compactMode: text === "" && description === "" + property bool showTrigger: true + property Item popupAnchorItem: null property bool addHorizontalPadding: false property string emptyText: "" property bool usePopupTransparency: !checkParentDisablesTransparency() signal valueChanged(string value) + property bool menuOpen: false + function closeDropdownMenu() { + if (!root.menuOpen && !dropdownMenu.opened && !dropdownMenu.visible) + return; + root.menuOpen = false; dropdownMenu.close(); } - function openDropdownMenu() { - if (dropdownMenu.visible) { - dropdownMenu.close(); - return; - } - if (root.options.length === 0) - return; - - dropdownMenu.open(); - + function positionDropdownMenu() { let currentIndex = root.options.indexOf(root.currentValue); listView.positionViewAtIndex(currentIndex >= 0 ? currentIndex : 0, ListView.Beginning); - const pos = dropdown.mapToItem(Overlay.overlay, 0, 0); + const anchorItem = root.popupAnchorItem || dropdown; + const pos = anchorItem.mapToItem(Overlay.overlay, 0, 0); const popupW = dropdownMenu.width; const popupH = dropdownMenu.height; const overlayH = Overlay.overlay.height; - const goUp = root.openUpwards || pos.y + dropdown.height + popupH + 4 > overlayH; - dropdownMenu.x = root.alignPopupRight ? pos.x + dropdown.width - popupW : pos.x - (root.popupWidthOffset / 2); - dropdownMenu.y = goUp ? pos.y - popupH - 4 : pos.y + dropdown.height + 4; + const goUp = root.openUpwards || pos.y + anchorItem.height + popupH + 4 > overlayH; + dropdownMenu.x = root.alignPopupRight ? pos.x + anchorItem.width - popupW : pos.x - (root.popupWidthOffset / 2); + dropdownMenu.y = goUp ? pos.y - popupH - 4 : pos.y + anchorItem.height + 4; + } + + function showDropdownMenu() { + if (root.options.length === 0) + return; + if (root.menuOpen) + return; + + root.menuOpen = true; + dropdownMenu.open(); + positionDropdownMenu(); + if (root.enableFuzzySearch) searchField.forceActiveFocus(); } + function openDropdownMenu() { + if (root.menuOpen) { + closeDropdownMenu(); + return; + } + showDropdownMenu(); + } + function resetSearch() { searchField.text = ""; dropdownMenu.fzfFinder = null; @@ -90,11 +109,11 @@ Item { dropdownMenu.selectedIndex = -1; } - width: compactMode ? dropdownWidth : parent.width - implicitHeight: compactMode ? 40 : Math.max(60, labelColumn.implicitHeight + Theme.spacingM) + width: !showTrigger ? 0 : (compactMode ? dropdownWidth : parent.width) + implicitHeight: !showTrigger ? 0 : (compactMode ? 40 : Math.max(60, labelColumn.implicitHeight + Theme.spacingM)) Component.onDestruction: { - if (dropdownMenu.visible) + if (root.menuOpen || dropdownMenu.opened || dropdownMenu.visible) dropdownMenu.close(); } @@ -107,7 +126,7 @@ Item { anchors.leftMargin: root.addHorizontalPadding ? Theme.spacingM : 0 anchors.rightMargin: Theme.spacingL spacing: Theme.spacingXS - visible: !root.compactMode + visible: !root.compactMode && root.showTrigger StyledText { text: root.text @@ -132,6 +151,7 @@ Item { Rectangle { id: dropdown + visible: root.showTrigger width: root.compactMode ? parent.width : (root.popupWidth === -1 ? undefined : (root.popupWidth > 0 ? root.popupWidth : root.dropdownWidth)) height: 40 anchors.right: parent.right @@ -259,6 +279,7 @@ Item { } onOpened: { + root.menuOpen = true; selectedIndex = -1; if (searchField.text.length > 0) { initFinder(); @@ -269,6 +290,8 @@ Item { } } + onClosed: root.menuOpen = false + parent: Overlay.overlay width: root.popupWidth === -1 ? undefined : (root.popupWidth > 0 ? root.popupWidth : (dropdown.width + root.popupWidthOffset)) height: { diff --git a/quickshell/Widgets/DankTextField.qml b/quickshell/Widgets/DankTextField.qml index 50cb3428..9155d36b 100644 --- a/quickshell/Widgets/DankTextField.qml +++ b/quickshell/Widgets/DankTextField.qml @@ -35,6 +35,7 @@ StyledRect { property color leftIconFocusedColor: Theme.primary property bool showClearButton: false property bool showPasswordToggle: false + property real rightAccessoryWidth: 0 property bool passwordVisible: false property bool usePopupTransparency: !checkParentDisablesTransparency() property color backgroundColor: usePopupTransparency ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : Theme.surfaceContainerHigh @@ -46,7 +47,7 @@ StyledRect { property real cornerRadius: Theme.cornerRadius readonly property real leftPadding: Theme.spacingM + (leftIconName ? leftIconSize + Theme.spacingM : 0) readonly property real rightPadding: { - let p = Theme.spacingS; + let p = Theme.spacingS + rightAccessoryWidth; if (showPasswordToggle) p += 20 + Theme.spacingXS; if (showClearButton && text.length > 0)