1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-21 10:35:26 -04:00

Enhance/clipboard history interactions (#2668)

* feat(clipboard): add entry context menu

* feat(clipboard): add click to paste option

* feat(clipboard): add optional copy and paste action buttons

* fix(clipboard): default selection and esc behavior

* fix(clipboard): prevent clear and filter overlap

* Update to remove dead kb nav & add escape action on context menu

---------

Co-authored-by: purian23 <purian23@gmail.com>
This commit is contained in:
jbwfu
2026-06-21 15:50:51 +08:00
committed by GitHub
parent 465cf7355b
commit 59fd6db83e
13 changed files with 570 additions and 19 deletions
+1
View File
@@ -107,6 +107,7 @@ Singleton {
saveSettings();
}
property bool clipboardClickToPaste: false
property bool clipboardEnterToPaste: false
property bool clipboardRememberTypeFilter: false
property string clipboardTypeFilter: "all"
@@ -600,6 +600,7 @@ var SPEC = {
desktopWidgetGroups: { def: [] },
builtInPluginSettings: { def: {} },
clipboardClickToPaste: { def: false },
clipboardEnterToPaste: { def: false },
clipboardRememberTypeFilter: { def: false },
clipboardTypeFilter: { def: "all" },
@@ -2,6 +2,7 @@ import QtQuick
import Quickshell
import qs.Common
import qs.Widgets
import qs.Services
Item {
id: clipboardContent
@@ -19,8 +20,62 @@ Item {
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.openState
anchors.fill: parent
ClipboardContextMenu {
id: contextMenu
modal: clipboardContent.modal
parentHandler: clipboardContent
}
Column {
id: headerColumn
anchors.top: parent.top
@@ -64,6 +119,12 @@ Item {
onTextChanged: {
modal.searchText = text;
modal.updateFilteredModel();
ClipboardService.selectedIndex = 0;
ClipboardService.keyboardNavigationActive = true;
Qt.callLater(function () {
clipboardListView.positionViewAtBeginning();
savedListView.positionViewAtBeginning();
});
}
Keys.onEscapePressed: function (event) {
@@ -202,10 +263,12 @@ Item {
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)
}
}
@@ -276,10 +339,12 @@ Item {
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)
}
}
@@ -0,0 +1,400 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
visible: false
width: 0
height: 0
property var entry: null
property var modal: null
property var parentHandler: null
property real menuMargin: 8
property var targetScreen: null
property real anchorX: 0
property real anchorY: 0
property bool openState: false
property bool renderActive: false
readonly property bool blurActive: renderActive && openState && BlurService.enabled && Theme.connectedSurfaceBlurEnabled
readonly property bool hasPinnedDuplicate: !!entry && !entry.pinned && ClipboardService.getPinnedEntryByHash(entry.hash) !== null
readonly property bool canEditEntry: !!entry && !(entry.isImage ?? false)
readonly property string pinText: entry?.pinned || hasPinnedDuplicate ? I18n.tr("Unpin") : I18n.tr("Pin")
readonly property string pinIcon: entry?.pinned || hasPinnedDuplicate ? "keep_off" : "push_pin"
readonly property var menuItems: {
const items = [
{
type: "item",
icon: "content_copy",
text: I18n.tr("Copy"),
action: copyEntry
},
{
type: "item",
icon: pinIcon,
text: pinText,
action: togglePin
}
];
if (canEditEntry) {
items.push({
type: "item",
icon: "edit",
text: I18n.tr("Edit"),
action: editEntry
});
}
items.push({
type: "item",
icon: "delete",
text: I18n.tr("Delete"),
action: deleteEntry
}, {
type: "separator"
}, {
type: "item",
icon: "content_paste",
text: I18n.tr("Paste"),
action: pasteEntry
});
return items;
}
readonly property real minMenuWidth: 160
readonly property real maxMenuWidth: Math.max(0, (targetScreen?.width ?? 500) - menuMargin * 2)
readonly property real maxMenuHeight: Math.max(0, (targetScreen?.height ?? 600) - menuMargin * 2)
readonly property string longestMenuText: {
let longest = "";
for (let i = 0; i < menuItems.length; i++) {
const text = menuItems[i].text || "";
if (text.length > longest.length)
longest = text;
}
return longest;
}
readonly property real naturalMenuWidth: Math.max(minMenuWidth, menuTextMetrics.width + Theme.iconSize + Theme.spacingS * 5)
readonly property real effectiveMenuWidth: Math.max(0, Math.min(maxMenuWidth, naturalMenuWidth))
readonly property real naturalMenuHeight: menuItemsHeight() + Theme.spacingS * 2
readonly property real effectiveMenuHeight: Math.min(maxMenuHeight, naturalMenuHeight)
readonly property bool menuScrolls: naturalMenuHeight > effectiveMenuHeight + 0.5
TextMetrics {
id: menuTextMetrics
text: root.longestMenuText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Normal
}
function menuItemsHeight() {
let h = 0;
for (let i = 0; i < menuItems.length; i++) {
h += menuItems[i].type === "separator" ? 5 : 32;
}
if (menuItems.length > 1)
h += menuItems.length - 1;
return h;
}
function show(x, y, targetEntry) {
if (!targetEntry)
return;
entry = targetEntry;
const host = modal?.surfaceHost ?? null;
const modalWindow = modal?.Window?.window ?? null;
const screenRef = host?.effectiveScreen ?? host?.screen ?? modalWindow?.screen ?? parentHandler?.Window?.window?.screen ?? null;
const screenX = screenRef?.x || 0;
const screenY = screenRef?.y || 0;
const hostX = host?.alignedX;
const hostY = host?.renderedAlignedY ?? host?.alignedY;
const globalPos = (!isNaN(hostX) && !isNaN(hostY)) ? ({
x: screenX + hostX + x,
y: screenY + hostY + y
}) : (parentHandler ? parentHandler.mapToGlobal(x, y) : ({
x: screenX + x,
y: screenY + y
}));
targetScreen = screenRef;
anchorX = globalPos.x - screenX + 4;
anchorY = globalPos.y - screenY + 4;
renderActive = true;
openState = true;
Qt.callLater(() => menuFlickable.contentY = 0);
}
function hide() {
if (!renderActive)
return;
openState = false;
}
function showFromWindowPoint(x, y) {
if (!parentHandler || typeof parentHandler.contextEntryAtScreen !== "function") {
hide();
return;
}
const hit = parentHandler.contextEntryAtScreen(x, y);
if (!hit || !hit.entry) {
hide();
return;
}
show(hit.x, hit.y, hit.entry);
}
function copyEntry() {
if (!entry)
return;
modal?.copyEntry(entry);
hide();
}
function togglePin() {
if (!entry)
return;
if (entry.pinned) {
modal?.unpinEntry(entry);
} else {
const duplicate = ClipboardService.getPinnedEntryByHash(entry.hash);
if (duplicate)
modal?.unpinEntry(duplicate);
else
modal?.pinEntry(entry);
}
hide();
}
function editEntry() {
if (!entry || !canEditEntry)
return;
modal?.editEntry(entry);
hide();
}
function deleteEntry() {
if (!entry)
return;
if (entry.pinned)
modal?.deletePinnedEntry(entry);
else
modal?.deleteEntry(entry);
hide();
}
function pasteEntry() {
if (!entry)
return;
modal?.pasteEntry(entry);
hide();
}
PanelWindow {
id: menuWindow
screen: root.targetScreen
visible: root.renderActive
color: "transparent"
WlrLayershell.namespace: "dms:clipboard-context-menu"
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors {
top: true
left: true
right: true
bottom: true
}
WindowBlur {
targetWindow: menuWindow
blurX: root.blurActive ? menuContainer.x : 0
blurY: root.blurActive ? menuContainer.y : 0
blurWidth: root.blurActive ? menuContainer.width : 0
blurHeight: root.blurActive ? menuContainer.height : 0
blurRadius: Theme.cornerRadius
}
MouseArea {
anchors.fill: parent
z: -1
enabled: root.renderActive
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
root.showFromWindowPoint(mouse.x, mouse.y);
return;
}
root.hide();
}
}
Item {
anchors.fill: parent
Rectangle {
id: menuContainer
x: Math.max(root.menuMargin, Math.min(menuWindow.width - width - root.menuMargin, root.anchorX))
y: Math.max(root.menuMargin, Math.min(menuWindow.height - height - root.menuMargin, root.anchorY))
width: root.effectiveMenuWidth
height: root.effectiveMenuHeight
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: BlurService.enabled ? BlurService.borderWidth : 1
opacity: root.openState ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
onRunningChanged: {
if (!running && !root.openState) {
root.renderActive = false;
}
}
}
}
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: -1
}
Flickable {
id: menuFlickable
anchors.fill: parent
anchors.margins: Theme.spacingS
clip: true
contentWidth: width
contentHeight: menuColumn.implicitHeight
boundsBehavior: Flickable.StopAtBounds
interactive: root.menuScrolls
Column {
id: menuColumn
width: menuFlickable.width
spacing: 1
Repeater {
model: root.menuItems
Item {
id: menuItemDelegate
required property var modelData
width: menuColumn.width
height: modelData.type === "separator" ? 5 : 32
Rectangle {
visible: menuItemDelegate.modelData.type === "separator"
width: parent.width - Theme.spacingS * 2
height: parent.height
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Rectangle {
visible: menuItemDelegate.modelData.type === "item"
width: parent.width
height: parent.height
radius: Theme.cornerRadius
color: itemMouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Item {
width: Theme.iconSize - 2
height: Theme.iconSize - 2
anchors.verticalCenter: parent.verticalCenter
DankIcon {
visible: (menuItemDelegate.modelData?.icon ?? "").length > 0
name: menuItemDelegate.modelData?.icon ?? ""
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: menuItemDelegate.modelData.text || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
width: parent.width - (Theme.iconSize - 2) - Theme.spacingS
}
}
DankRipple {
id: menuItemRipple
rippleColor: Theme.surfaceText
cornerRadius: Theme.cornerRadius
}
MouseArea {
id: itemMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onPressed: mouse => menuItemRipple.trigger(mouse.x, mouse.y)
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
root.hide();
return;
}
const menuItem = menuItemDelegate.modelData;
if (menuItem.action)
menuItem.action();
}
}
}
}
}
}
}
}
}
}
}
+42 -4
View File
@@ -14,10 +14,12 @@ Rectangle {
required property var listView
signal copyRequested
signal pasteRequested
signal deleteRequested
signal pinRequested(var targetEntry)
signal unpinRequested(var targetEntry)
signal editRequested
signal contextMenuRequested(real mouseX, real mouseY)
readonly property string entryType: modal ? modal.getEntryType(entry) : "text"
readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : ""
@@ -25,11 +27,13 @@ Rectangle {
readonly property bool hasPinnedDuplicate: pinnedDuplicateEntry !== null
readonly property bool effectivePinned: entry.pinned || hasPinnedDuplicate
readonly property var visibleEntryActions: SettingsData.clipboardVisibleEntryActions || ["pin", "edit", "delete"]
readonly property bool showCopyAction: visibleEntryActions.includes("copy")
readonly property bool showPasteAction: visibleEntryActions.includes("paste")
readonly property bool showPinAction: visibleEntryActions.includes("pin")
readonly property bool showEditAction: visibleEntryActions.includes("edit")
readonly property bool showDeleteAction: visibleEntryActions.includes("delete")
readonly property bool showPinnedIndicator: hasPinnedDuplicate && !showPinAction
readonly property bool showAnyAction: showPinAction || showEditAction || showDeleteAction || showPinnedIndicator
readonly property bool showAnyAction: showCopyAction || showPasteAction || showPinAction || showEditAction || showDeleteAction || showPinnedIndicator
radius: Theme.cornerRadius
color: {
@@ -86,6 +90,22 @@ Rectangle {
}
}
DankActionButton {
iconName: "content_copy"
iconSize: Theme.iconSize - 6
iconColor: Theme.surfaceText
visible: root.showCopyAction
onClicked: copyRequested()
}
DankActionButton {
iconName: "content_paste"
iconSize: Theme.iconSize - 6
iconColor: Theme.surfaceText
visible: root.showPasteAction
onClicked: pasteRequested()
}
DankActionButton {
iconName: "push_pin"
iconSize: Theme.iconSize - 6
@@ -199,10 +219,28 @@ Rectangle {
anchors.bottom: parent.bottom
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onPressed: mouse => {
const pos = mouseArea.mapToItem(root, mouse.x, mouse.y);
rippleLayer.trigger(pos.x, pos.y);
if (mouse.button === Qt.LeftButton) {
const pos = mouseArea.mapToItem(root, mouse.x, mouse.y);
rippleLayer.trigger(pos.x, pos.y);
}
}
onClicked: {
if (SettingsData.clipboardClickToPaste) {
pasteRequested()
} else {
copyRequested()
}
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: mouse => {
const scenePos = mapToItem(null, mouse.x, mouse.y);
contextMenuRequested(scenePos.x, scenePos.y);
}
onClicked: copyRequested()
}
}
@@ -8,6 +8,7 @@ FocusScope {
id: root
property var clearConfirmDialog: null
property var surfaceHost: null
property string activeTab: "recents"
property bool showKeyboardHints: false
@@ -32,6 +33,11 @@ FocusScope {
property alias searchField: historyContent.searchField
property alias editorView: editorView
property alias keyboardController: keyboardController
readonly property alias contextMenuActive: historyContent.contextMenuActive
function closeContextMenu() {
historyContent.closeContextMenu();
}
signal closeRequested
signal instantCloseRequested
@@ -42,7 +48,7 @@ FocusScope {
return;
}
ClipboardService.selectedIndex = 0;
ClipboardService.keyboardNavigationActive = false;
ClipboardService.keyboardNavigationActive = true;
}
onPinnedCountChanged: {
if (activeTab === "saved" && pinnedCount === 0) {
@@ -54,7 +60,7 @@ FocusScope {
onActiveFilterChanged: {
ClipboardService.activeFilter = activeFilter;
ClipboardService.selectedIndex = 0;
ClipboardService.keyboardNavigationActive = false;
ClipboardService.keyboardNavigationActive = true;
ClipboardService.updateFilteredModel();
if (SettingsData.clipboardRememberTypeFilter) {
SettingsData.set("clipboardTypeFilter", activeFilter);
@@ -92,6 +98,10 @@ FocusScope {
ClipboardService.pasteEntry(entry, () => root.requestClose(true));
}
function pasteEntry(entry) {
ClipboardService.pasteEntry(entry, () => root.requestClose(true));
}
function copyEntry(entry) {
ClipboardService.copyEntry(entry, () => root.requestClose(false));
}
@@ -159,6 +169,7 @@ FocusScope {
function resetState() {
activeImageLoads = 0;
mode = "history";
historyContent.closeContextMenu();
historyContent.closeFilterMenu();
activeFilter = SettingsData.clipboardRememberTypeFilter ? SettingsData.clipboardTypeFilter : "all";
ClipboardService.reset();
@@ -129,6 +129,7 @@ DankModal {
content: Component {
ClipboardHistoryContent {
surfaceHost: clipboardHistoryModal
clearConfirmDialog: clearConfirmDialog
onCloseRequested: clipboardHistoryModal.hide()
onInstantCloseRequested: clipboardHistoryModal.instantHide()
@@ -140,6 +140,7 @@ DankPopout {
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
surfaceHost: root
clearConfirmDialog: clearConfirmDialog
onCloseRequested: root.hide()
onInstantCloseRequested: root.hide()
@@ -9,7 +9,7 @@ QtObject {
function reset() {
ClipboardService.selectedIndex = 0;
ClipboardService.keyboardNavigationActive = false;
ClipboardService.keyboardNavigationActive = true;
modal.showKeyboardHints = false;
}
@@ -89,13 +89,16 @@ QtObject {
return;
}
if (modal.contextMenuActive) {
if (event.key === Qt.Key_Escape)
modal.closeContextMenu();
event.accepted = true;
return;
}
switch (event.key) {
case Qt.Key_Escape:
if (ClipboardService.keyboardNavigationActive) {
ClipboardService.keyboardNavigationActive = false;
} else {
modal.hide();
}
modal.hide();
event.accepted = true;
return;
case Qt.Key_Down:
@@ -1883,10 +1883,10 @@ Item {
}
if (!selectedItem)
return;
executeItem(selectedItem);
executeItem(selectedItem, true);
}
function executeItem(item) {
function executeItem(item, isKeyboard = false) {
if (!item)
return;
@@ -1929,7 +1929,8 @@ Item {
AppSearchService.executeBuiltInLauncherItem(item.data);
break;
case "clipboard":
if (SettingsData.clipboardEnterToPaste) {
var shouldPaste = isKeyboard ? SettingsData.clipboardEnterToPaste : SettingsData.clipboardClickToPaste;
if (shouldPaste) {
ClipboardService.pasteEntry(item.data, function () {
root.itemExecuted();
});
+13 -3
View File
@@ -152,8 +152,8 @@ Item {
}
]
readonly property var entryActionKeys: ["pin", "edit", "delete"]
readonly property var entryActionLabels: [I18n.tr("Pin"), I18n.tr("Edit"), I18n.tr("Delete")]
readonly property var entryActionKeys: ["copy", "paste", "pin", "edit", "delete"]
readonly property var entryActionLabels: [I18n.tr("Copy"), I18n.tr("Paste"), I18n.tr("Pin"), I18n.tr("Edit"), I18n.tr("Delete")]
function getMaxHistoryText(value) {
if (value <= 0)
@@ -454,6 +454,16 @@ Item {
onToggled: checked => root.saveConfig("clearAtStartup", checked)
}
SettingsToggleRow {
tab: "clipboard"
tags: ["clipboard", "click", "paste", "behavior"]
settingKey: "clipboardClickToPaste"
text: I18n.tr("Click to Paste")
description: I18n.tr("Click an entry to paste directly instead of copying", "Clipboard behavior setting description")
checked: SettingsData.clipboardClickToPaste
onToggled: checked => SettingsData.set("clipboardClickToPaste", checked)
}
SettingsToggleRow {
tab: "clipboard"
tags: ["clipboard", "enter", "paste", "behavior"]
@@ -476,7 +486,7 @@ Item {
SettingsButtonGroupRow {
tab: "clipboard"
tags: ["clipboard", "actions", "buttons", "hide", "density", "pin", "edit", "delete"]
tags: ["clipboard", "actions", "buttons", "hide", "density", "copy", "paste", "pin", "edit", "delete"]
settingKey: "clipboardVisibleEntryActions"
text: I18n.tr("Visible Entry Actions")
description: I18n.tr("Choose which action buttons appear on clipboard entries")
+1 -1
View File
@@ -202,7 +202,7 @@ StyledRect {
id: rightButtonsRow
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS + root.rightAccessoryWidth
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
visible: showPasswordToggle || (showClearButton && text.length > 0)
@@ -7130,6 +7130,25 @@
"icon": "settings",
"description": "Clear all history when server starts"
},
{
"section": "clipboardClickToPaste",
"label": "Click to Paste",
"tabIndex": 23,
"category": "System",
"keywords": [
"behavior",
"click",
"clipboard",
"copying",
"directly",
"entry",
"linux",
"os",
"paste",
"system"
],
"description": "Click an entry to paste directly instead of copying"
},
{
"section": "_tab_23",
"label": "Clipboard",