mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-08-29 07:27:16 +00:00
Merge 34e64cbe4dc7f134fe37e4ea12940cefcaaf3821 into 1f59e814f90c3f489f48d68262cb1bf640bf6181
This commit is contained in:
commit
e4561b1ad1
@ -6,6 +6,9 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
struct CommandSuggestionsView: View {
|
||||
@EnvironmentObject private var privateConversationModel: PrivateConversationModel
|
||||
@ -14,6 +17,16 @@ struct CommandSuggestionsView: View {
|
||||
|
||||
@Binding var messageText: String
|
||||
|
||||
/// Row highlighted for keyboard navigation (macOS). Reset whenever the
|
||||
/// filtered list changes so the highlight never outlives its list.
|
||||
@State private var selectedIndex = 0
|
||||
#if os(macOS)
|
||||
/// Arrow keys never reach SwiftUI while the composer's field editor has
|
||||
/// focus (the single-line field consumes moveUp:/moveDown: itself), so
|
||||
/// navigation uses a local key monitor scoped to the panel's lifetime.
|
||||
@State private var keyMonitor: Any?
|
||||
#endif
|
||||
|
||||
/// The command already typed in full, once arguments have begun.
|
||||
private var typedCommandAlias: String? {
|
||||
guard messageText.hasPrefix("/"),
|
||||
@ -43,23 +56,91 @@ struct CommandSuggestionsView: View {
|
||||
// off-center.
|
||||
if !filteredCommands.isEmpty {
|
||||
let isUsageReminder = typedCommandAlias != nil
|
||||
let commands = filteredCommands
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
ForEach(filteredCommands) { command in
|
||||
ForEach(Array(commands.enumerated()), id: \.element.id) { index, command in
|
||||
Button {
|
||||
// In usage-reminder mode the row is informational; an
|
||||
// insert here would wipe the arguments being typed.
|
||||
guard !isUsageReminder else { return }
|
||||
messageText = command.alias + " "
|
||||
accept(command)
|
||||
} label: {
|
||||
buttonRow(for: command)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.background(rowHighlight(index: index, isUsageReminder: isUsageReminder))
|
||||
}
|
||||
}
|
||||
.themedOverlayPanel()
|
||||
.onChange(of: commands) { _ in
|
||||
selectedIndex = 0
|
||||
}
|
||||
#if os(macOS)
|
||||
.onAppear { installKeyMonitor() }
|
||||
.onDisappear { removeKeyMonitor() }
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Inserts the command with a trailing space, ready for arguments.
|
||||
private func accept(_ command: CommandInfo) {
|
||||
messageText = command.alias + " "
|
||||
}
|
||||
|
||||
private func rowHighlight(index: Int, isUsageReminder: Bool) -> Color {
|
||||
#if os(macOS)
|
||||
if !isUsageReminder && index == selectedIndex {
|
||||
return palette.secondary.opacity(0.15)
|
||||
}
|
||||
#endif
|
||||
return .clear
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
private func installKeyMonitor() {
|
||||
guard keyMonitor == nil else { return }
|
||||
keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
|
||||
handleKeyDown(event)
|
||||
}
|
||||
}
|
||||
|
||||
private func removeKeyMonitor() {
|
||||
if let keyMonitor {
|
||||
NSEvent.removeMonitor(keyMonitor)
|
||||
}
|
||||
keyMonitor = nil
|
||||
}
|
||||
|
||||
/// Standard autocomplete navigation: arrows move the highlight,
|
||||
/// return/tab insert the highlighted command. Returning nil consumes the
|
||||
/// event, so return completes instead of sending while the list is up;
|
||||
/// the usage-reminder row stays informational and passes everything
|
||||
/// through (return sends the composed command).
|
||||
private func handleKeyDown(_ event: NSEvent) -> NSEvent? {
|
||||
let commands = filteredCommands
|
||||
guard !commands.isEmpty,
|
||||
typedCommandAlias == nil,
|
||||
event.modifierFlags.intersection([.command, .option, .control]).isEmpty else {
|
||||
return event
|
||||
}
|
||||
|
||||
switch event.keyCode {
|
||||
case 126: // up arrow
|
||||
selectedIndex = max(0, selectedIndex - 1)
|
||||
return nil
|
||||
case 125: // down arrow
|
||||
selectedIndex = min(commands.count - 1, selectedIndex + 1)
|
||||
return nil
|
||||
case 36, 48: // return, tab
|
||||
accept(commands[min(selectedIndex, commands.count - 1)])
|
||||
return nil
|
||||
default:
|
||||
return event
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
private func buttonRow(for command: CommandInfo) -> some View {
|
||||
HStack {
|
||||
Text(command.alias)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user