Fix Escape beeping instead of dismissing Model Info after clicking description
.onExitCommand alone wasn't enough: clicking into the multi-line .textSelection(.enabled) description handed it real AppKit first-responder status, and its own cancelOperation: handling for Escape consumed the key event before it ever reached the modal's exit-command handler. Replaced text-selection on the description with an explicit Copy button (same pattern as the chat message copy button in MessageRow.swift) so it can no longer grab keyboard focus at all. infoRow's single-line values keep .textSelection(.enabled) — only the multi-line description reproduced the bug.
This commit is contained in:
@@ -28,6 +28,7 @@ struct ModelInfoView: View {
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Bindable private var settings = SettingsService.shared
|
||||
@State private var showDescriptionCopied = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
@@ -80,13 +81,34 @@ struct ModelInfoView: View {
|
||||
// lay out). The modal itself already scrolls, so a long description just
|
||||
// means more scrolling, which sidesteps the whole bug class.
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Description")
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(.secondary)
|
||||
HStack(spacing: 6) {
|
||||
Text("Description")
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
// A Copy button instead of .textSelection(.enabled): clicking into
|
||||
// a multi-line selectable Text hands it real AppKit first-responder
|
||||
// status, and Escape then gets consumed by that text view's own
|
||||
// cancelOperation: handling before it ever reaches this modal's
|
||||
// onExitCommand — the beep-instead-of-dismiss bug Rune reported.
|
||||
// infoRow's single-line values keep .textSelection(.enabled); only
|
||||
// this multi-line block reproduced the bug.
|
||||
Button(action: copyDescription) {
|
||||
HStack(spacing: 3) {
|
||||
Image(systemName: showDescriptionCopied ? "checkmark" : "doc.on.doc")
|
||||
.font(.system(size: 11))
|
||||
if showDescriptionCopied {
|
||||
Text("Copied!")
|
||||
.font(.system(size: 11))
|
||||
}
|
||||
}
|
||||
.foregroundColor(showDescriptionCopied ? .green : .secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
Text(desc)
|
||||
.font(.body)
|
||||
.foregroundColor(.primary)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
.padding(.leading, 4)
|
||||
}
|
||||
@@ -212,6 +234,29 @@ struct ModelInfoView: View {
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
.frame(minWidth: 550, idealWidth: 650, minHeight: 550, idealHeight: 750)
|
||||
// Nearly every value in this modal has .textSelection(.enabled) (infoRow's value text,
|
||||
// the description). Once one of those has text-selection focus, Escape can get
|
||||
// intercepted by AppKit's text-selection machinery instead of reaching the close
|
||||
// button's .keyboardShortcut(.escape) — no action is bound there, so it just beeps
|
||||
// instead of dismissing. onExitCommand is macOS's dedicated hook for the "Escape/Cancel"
|
||||
// user command and fires regardless of which child currently holds focus, so it's a more
|
||||
// reliable place to handle this than a single button's keyboardShortcut alone.
|
||||
.onExitCommand { dismiss() }
|
||||
}
|
||||
|
||||
private func copyDescription() {
|
||||
guard let desc = model.description else { return }
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.clearContents()
|
||||
pasteboard.setString(desc, forType: .string)
|
||||
withAnimation {
|
||||
showDescriptionCopied = true
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
|
||||
withAnimation {
|
||||
showDescriptionCopied = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Layout Helpers
|
||||
|
||||
Reference in New Issue
Block a user