-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Open selected file explorer item from keyboard #6036
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
f44e936
3cd5ab3
a7693bb
7208c6c
d068303
ea9e264
54d53f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,197 @@ | ||||||||||||||||||||||||||||||||
| import AppKit | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| enum FileExplorerKeyboardActivation { | ||||||||||||||||||||||||||||||||
| static func isDefaultOpenEvent(_ event: NSEvent) -> Bool { | ||||||||||||||||||||||||||||||||
| guard event.type == .keyDown else { return false } | ||||||||||||||||||||||||||||||||
| let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) | ||||||||||||||||||||||||||||||||
| .subtracting([.numericPad, .function, .capsLock]) | ||||||||||||||||||||||||||||||||
| guard flags.intersection([.command, .control, .option, .shift]).isEmpty else { | ||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return event.keyCode == 36 || event.keyCode == 76 | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| static func matchesOpenSelectionShortcut( | ||||||||||||||||||||||||||||||||
| _ event: NSEvent, | ||||||||||||||||||||||||||||||||
| shortcutForAction: (KeyboardShortcutSettings.Action) -> StoredShortcut = KeyboardShortcutSettings.shortcut(for:) | ||||||||||||||||||||||||||||||||
| ) -> Bool { | ||||||||||||||||||||||||||||||||
| isDefaultOpenEvent(event) || shortcutForAction(.openFileExplorerSelection).matches(event: event) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /// NSOutlineView subclass that disables expand/collapse animations and adds leading margin. | ||||||||||||||||||||||||||||||||
| final class FileExplorerNSOutlineView: NSOutlineView { | ||||||||||||||||||||||||||||||||
| /// Leading margin applied to disclosure triangles and content. | ||||||||||||||||||||||||||||||||
| static let leadingMargin: CGFloat = 8 | ||||||||||||||||||||||||||||||||
| var onQuickSearchChanged: ((String?) -> Void)? | ||||||||||||||||||||||||||||||||
| private var quickSearchActive = false | ||||||||||||||||||||||||||||||||
| private var quickSearchQuery = "" | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| override func keyDown(with event: NSEvent) { | ||||||||||||||||||||||||||||||||
| if let mode = AppDelegate.shared?.rightSidebarModeShortcut(for: event) { | ||||||||||||||||||||||||||||||||
| if fileExplorerCoordinator?.handleModeShortcut(mode, in: window) == true { | ||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if handleOpenSelectionShortcut(event) { | ||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if quickSearchActive, handleQuickSearchKey(event) { | ||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if let delta = RightSidebarKeyboardNavigation.moveDelta(for: event) { | ||||||||||||||||||||||||||||||||
| endQuickSearch() | ||||||||||||||||||||||||||||||||
| fileExplorerCoordinator?.moveSelection(in: self, by: delta) | ||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if let action = RightSidebarKeyboardNavigation.disclosureAction(for: event) { | ||||||||||||||||||||||||||||||||
| endQuickSearch() | ||||||||||||||||||||||||||||||||
| fileExplorerCoordinator?.performDisclosureAction(action, in: self) | ||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if RightSidebarKeyboardNavigation.isPlainSlash(event) { | ||||||||||||||||||||||||||||||||
| beginQuickSearch() | ||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if RightSidebarKeyboardNavigation.isPlainPrintableText(event) { | ||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| super.keyDown(with: event) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| override func performKeyEquivalent(with event: NSEvent) -> Bool { | ||||||||||||||||||||||||||||||||
| if handleOpenSelectionShortcut(event) { | ||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| if quickSearchActive, handleQuickSearchKey(event) { | ||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| if let delta = RightSidebarKeyboardNavigation.moveDelta(for: event) { | ||||||||||||||||||||||||||||||||
| endQuickSearch() | ||||||||||||||||||||||||||||||||
| fileExplorerCoordinator?.moveSelection(in: self, by: delta) | ||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| if let action = RightSidebarKeyboardNavigation.disclosureAction(for: event) { | ||||||||||||||||||||||||||||||||
| endQuickSearch() | ||||||||||||||||||||||||||||||||
| fileExplorerCoordinator?.performDisclosureAction(action, in: self) | ||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return super.performKeyEquivalent(with: event) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| override func becomeFirstResponder() -> Bool { | ||||||||||||||||||||||||||||||||
| let result = super.becomeFirstResponder() | ||||||||||||||||||||||||||||||||
| if result { | ||||||||||||||||||||||||||||||||
| redrawVisibleRows() | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return result | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| override func resignFirstResponder() -> Bool { | ||||||||||||||||||||||||||||||||
| let result = super.resignFirstResponder() | ||||||||||||||||||||||||||||||||
| if result { | ||||||||||||||||||||||||||||||||
| endQuickSearch() | ||||||||||||||||||||||||||||||||
| redrawVisibleRows() | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return result | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| override func expandItem(_ item: Any?, expandChildren: Bool) { | ||||||||||||||||||||||||||||||||
| NSAnimationContext.beginGrouping() | ||||||||||||||||||||||||||||||||
| NSAnimationContext.current.duration = 0 | ||||||||||||||||||||||||||||||||
| super.expandItem(item, expandChildren: expandChildren) | ||||||||||||||||||||||||||||||||
| NSAnimationContext.endGrouping() | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| override func collapseItem(_ item: Any?, collapseChildren: Bool) { | ||||||||||||||||||||||||||||||||
| NSAnimationContext.beginGrouping() | ||||||||||||||||||||||||||||||||
| NSAnimationContext.current.duration = 0 | ||||||||||||||||||||||||||||||||
| super.collapseItem(item, collapseChildren: collapseChildren) | ||||||||||||||||||||||||||||||||
| NSAnimationContext.endGrouping() | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| override func frameOfOutlineCell(atRow row: Int) -> NSRect { | ||||||||||||||||||||||||||||||||
| var frame = super.frameOfOutlineCell(atRow: row) | ||||||||||||||||||||||||||||||||
| frame.origin.x += Self.leadingMargin | ||||||||||||||||||||||||||||||||
| return frame | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| override func frameOfCell(atColumn column: Int, row: Int) -> NSRect { | ||||||||||||||||||||||||||||||||
| var frame = super.frameOfCell(atColumn: column, row: row) | ||||||||||||||||||||||||||||||||
| let cellShift: CGFloat = Self.leadingMargin - 6 | ||||||||||||||||||||||||||||||||
| frame.origin.x += cellShift | ||||||||||||||||||||||||||||||||
| frame.size.width -= cellShift | ||||||||||||||||||||||||||||||||
| return frame | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private func redrawVisibleRows() { | ||||||||||||||||||||||||||||||||
| setNeedsDisplay(bounds) | ||||||||||||||||||||||||||||||||
| let visibleRows = rows(in: visibleRect) | ||||||||||||||||||||||||||||||||
| guard visibleRows.location != NSNotFound else { return } | ||||||||||||||||||||||||||||||||
| let upperBound = min(visibleRows.location + visibleRows.length, numberOfRows) | ||||||||||||||||||||||||||||||||
| guard visibleRows.location < upperBound else { return } | ||||||||||||||||||||||||||||||||
| for row in visibleRows.location..<upperBound { | ||||||||||||||||||||||||||||||||
| rowView(atRow: row, makeIfNecessary: false)?.needsDisplay = true | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private var fileExplorerCoordinator: FileExplorerPanelView.Coordinator? { | ||||||||||||||||||||||||||||||||
| dataSource as? FileExplorerPanelView.Coordinator | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private func handleOpenSelectionShortcut(_ event: NSEvent) -> Bool { | ||||||||||||||||||||||||||||||||
| guard FileExplorerKeyboardActivation.matchesOpenSelectionShortcut(event) else { | ||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| endQuickSearch() | ||||||||||||||||||||||||||||||||
| return fileExplorerCoordinator?.openSelectedItem(in: self) ?? false | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+158
to
+170
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private func beginQuickSearch() { | ||||||||||||||||||||||||||||||||
| quickSearchActive = true | ||||||||||||||||||||||||||||||||
| quickSearchQuery = "" | ||||||||||||||||||||||||||||||||
| onQuickSearchChanged?(quickSearchQuery) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private func endQuickSearch() { | ||||||||||||||||||||||||||||||||
| guard quickSearchActive || !quickSearchQuery.isEmpty else { return } | ||||||||||||||||||||||||||||||||
| quickSearchActive = false | ||||||||||||||||||||||||||||||||
| quickSearchQuery = "" | ||||||||||||||||||||||||||||||||
| onQuickSearchChanged?(nil) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private func handleQuickSearchKey(_ event: NSEvent) -> Bool { | ||||||||||||||||||||||||||||||||
| if event.keyCode == 53 { | ||||||||||||||||||||||||||||||||
| endQuickSearch() | ||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| if event.keyCode == 36 || event.keyCode == 76 { | ||||||||||||||||||||||||||||||||
| endQuickSearch() | ||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| if event.keyCode == 51 { | ||||||||||||||||||||||||||||||||
| if !quickSearchQuery.isEmpty { | ||||||||||||||||||||||||||||||||
| quickSearchQuery.removeLast() | ||||||||||||||||||||||||||||||||
| onQuickSearchChanged?(quickSearchQuery) | ||||||||||||||||||||||||||||||||
| fileExplorerCoordinator?.selectBestQuickSearchMatch(in: self, query: quickSearchQuery) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| guard RightSidebarKeyboardNavigation.isPlainPrintableText(event) else { | ||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| guard let text = event.charactersIgnoringModifiers, !text.isEmpty else { | ||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| quickSearchQuery += text | ||||||||||||||||||||||||||||||||
| onQuickSearchChanged?(quickSearchQuery) | ||||||||||||||||||||||||||||||||
| fileExplorerCoordinator?.selectBestQuickSearchMatch(in: self, query: quickSearchQuery) | ||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.