diff --git a/oAI/Views/Screens/ModelInfoView.swift b/oAI/Views/Screens/ModelInfoView.swift index d7df0f5..b5c843a 100644 --- a/oAI/Views/Screens/ModelInfoView.swift +++ b/oAI/Views/Screens/ModelInfoView.swift @@ -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