WIP: add shelf range navigation keys

This commit is contained in:
Akshay Kolli
2026-06-30 09:19:18 -07:00
parent eedccf8422
commit 14b1d3323c
8 changed files with 222 additions and 20 deletions

View File

@@ -26,6 +26,15 @@ enum ClipboardPanelShortcutAction: Equatable {
case toggleStack
}
enum ClipboardPanelNavigationAction: Equatable {
case first
case last
case next
case pageNext
case pagePrevious
case previous
}
final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanelDataSource, QLPreviewPanelDelegate {
private enum Animation {
static let showDuration: TimeInterval = 0.16
@@ -375,6 +384,10 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanel
return nil
}
guard self.shouldHandlePanelKeyEvent(event) else { return event }
if let action = Self.navigationShortcutAction(forKeyCode: event.keyCode, modifiers: event.modifierFlags) {
self.performNavigationAction(action)
return nil
}
switch event.keyCode {
case 53:
self.hide()
@@ -388,12 +401,6 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanel
case 51, 117:
self.viewModel.deleteSelected()
return nil
case 123:
self.viewModel.moveSelection(-1)
return nil
case 124:
self.viewModel.moveSelection(1)
return nil
case 35:
self.viewModel.togglePinSelected()
return nil
@@ -403,6 +410,24 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanel
}
}
private func performNavigationAction(_ action: ClipboardPanelNavigationAction) {
switch action {
case .first:
viewModel.selectFirstItem()
case .last:
viewModel.selectLastItem()
case .next:
viewModel.moveSelection(1)
case .pageNext:
viewModel.moveSelection(panelView.visibleCardPageStep)
case .pagePrevious:
viewModel.moveSelection(-panelView.visibleCardPageStep)
case .previous:
viewModel.moveSelection(-1)
}
panelView.focusSelectedCardForKeyboardNavigation()
}
private func performShortcutAction(_ action: ClipboardPanelShortcutAction) {
switch action {
case .copy:
@@ -467,6 +492,20 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanel
return quickPasteKeyCodes[keyCode]
}
static func navigationShortcutAction(forKeyCode keyCode: UInt16, modifiers: NSEvent.ModifierFlags) -> ClipboardPanelNavigationAction? {
let relevantModifiers = modifiers.intersection(.deviceIndependentFlagsMask)
guard relevantModifiers.isEmpty else { return nil }
switch keyCode {
case 115: return .first
case 119: return .last
case 124: return .next
case 121: return .pageNext
case 116: return .pagePrevious
case 123: return .previous
default: return nil
}
}
static func quickPastePlainTextIndex(forKeyCode keyCode: UInt16, modifiers: NSEvent.ModifierFlags) -> Int? {
let relevantModifiers = modifiers.intersection(.deviceIndependentFlagsMask)
guard relevantModifiers == [.command, .shift] else { return nil }

View File

@@ -722,6 +722,19 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
card.onSelect = { [weak self] selected in
self?.viewModel.selectItem(at: selected)
}
card.onMoveSelection = { [weak self] delta in
self?.moveSelectionFromFocusedCard(delta)
}
card.onPageSelection = { [weak self] direction in
guard let self else { return }
self.moveSelectionFromFocusedCard(direction * self.visibleCardPageStep)
}
card.onSelectFirst = { [weak self] in
self?.selectFirstCardFromFocusedCard()
}
card.onSelectLast = { [weak self] in
self?.selectLastCardFromFocusedCard()
}
card.onPaste = { [weak self] selected in
self?.viewModel.selectItem(at: selected)
self?.viewModel.pasteSelected()
@@ -1156,6 +1169,16 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
needsLayout = true
}
var visibleCardPageStep: Int {
let span = cardDensity.layout.width + cardDensity.cardSpacing
guard span > 0 else { return 1 }
return max(1, Int(floor(scrollView.contentView.bounds.width / span)))
}
func focusSelectedCardForKeyboardNavigation() {
focusSelectedCard()
}
func prepareForShow() {
if !searchField.stringValue.isEmpty {
searchField.stringValue = ""
@@ -1283,6 +1306,10 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
scrollView.documentView?.frame.width ?? 0
}
var debugVisibleCardPageStep: Int {
visibleCardPageStep
}
var debugCardRailOverflowFadeVisibility: [Bool] {
scrollView.overflowFadeVisibility
}
@@ -1381,6 +1408,10 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
debugPressFocusedResponder(characters: " ", keyCode: 49)
}
func debugPressFocusedResponderKeyCode(_ keyCode: UInt16) {
debugPressFocusedResponder(characters: "", keyCode: keyCode)
}
private func debugPressFocusedResponder(characters: String, keyCode: UInt16) {
guard let window,
let event = NSEvent.keyEvent(
@@ -1547,6 +1578,29 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
viewModel.searchText = searchField.stringValue
}
private func moveSelectionFromFocusedCard(_ delta: Int) {
viewModel.moveSelection(delta)
focusSelectedCard()
}
private func selectFirstCardFromFocusedCard() {
viewModel.selectFirstItem()
focusSelectedCard()
}
private func selectLastCardFromFocusedCard() {
viewModel.selectLastItem()
focusSelectedCard()
}
private func focusSelectedCard() {
guard viewModel.selectedIndex >= 0,
viewModel.selectedIndex < cardViews.count else {
return
}
window?.makeFirstResponder(cardViews[viewModel.selectedIndex])
}
@objc private func closePanel() {
onClose()
}
@@ -2148,6 +2202,10 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
}
var onSelect: (Int) -> Void = { _ in }
var onMoveSelection: (Int) -> Void = { _ in }
var onPageSelection: (Int) -> Void = { _ in }
var onSelectFirst: () -> Void = {}
var onSelectLast: () -> Void = {}
var onPaste: (Int) -> Void = { _ in }
var onCopy: (Int) -> Void = { _ in }
var onPastePlainText: (Int) -> Void = { _ in }
@@ -2289,6 +2347,18 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
} else {
onPaste(index)
}
case 115:
onSelectFirst()
case 116:
onPageSelection(-1)
case 119:
onSelectLast()
case 121:
onPageSelection(1)
case 123:
onMoveSelection(-1)
case 124:
onMoveSelection(1)
default:
super.keyDown(with: event)
}

View File

@@ -211,6 +211,17 @@ final class ClipboardPanelViewModel {
}
}
func selectLastItem() {
guard !visibleItems.isEmpty else { return }
selectedItemID = nil
let lastIndex = visibleItems.count - 1
if selectedIndex == lastIndex {
notifyMain { self.onSelectedIndexChanged?(self.selectedIndex) }
} else {
selectedIndex = lastIndex
}
}
func moveSelection(_ delta: Int) {
let count = visibleItems.count
guard count > 0 else { return }