From b2a06ec5ebc86c0241e635782ae5a032ab5aad0a Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Thu, 02 Jul 2026 02:43:51 +0800
Subject: [PATCH] Prepare TagLauncher 8.3.2 App Store candidate
---
src/Apptag/AppGridSupport.swift | 155 ++++++++++++++++++++++++++++++++++++++++++++++++---
1 files changed, 144 insertions(+), 11 deletions(-)
diff --git a/src/Apptag/AppGridSupport.swift b/src/Apptag/AppGridSupport.swift
index 8ff238e..397a995 100644
--- a/src/Apptag/AppGridSupport.swift
+++ b/src/Apptag/AppGridSupport.swift
@@ -59,19 +59,20 @@
if isEditing {
VStack(alignment: .trailing, spacing: 4) {
- TextField(tr("appNote.placeholder"), text: limitedDraft)
- .textFieldStyle(.plain)
- .font(.system(size: 15, weight: .medium))
- .foregroundStyle(.white)
+ AppBubbleNoteTextField(
+ text: $draftNote,
+ placeholder: tr("appNote.placeholder"),
+ isFocused: noteFocused,
+ onCommit: onCommit,
+ onCancel: onCancel
+ )
.padding(.horizontal, 12)
.frame(height: 36)
.background(
RoundedRectangle(cornerRadius: 9)
.fill(Color.white.opacity(0.12))
)
- .focused(noteFocused)
- .onSubmit(onCommit)
- Text("\(draftNote.count) / \(TagDatabase.maxAppNoteLength)")
+ Text("\(min(draftNote.count, TagDatabase.maxAppNoteLength)) / \(TagDatabase.maxAppNoteLength)")
.font(.system(size: 10, weight: .medium))
.foregroundStyle(.white.opacity(0.35))
@@ -113,13 +114,145 @@
.offset(x: arrowOffset, y: placement == .above ? 8 : -8)
}
}
+}
- private var limitedDraft: Binding<String> {
- Binding(
- get: { draftNote },
- set: { draftNote = String($0.prefix(TagDatabase.maxAppNoteLength)) }
+private struct AppBubbleNoteTextField: NSViewRepresentable {
+ @Binding var text: String
+ let placeholder: String
+ var isFocused: FocusState<Bool>.Binding
+ let onCommit: () -> Void
+ let onCancel: () -> Void
+
+ func makeCoordinator() -> Coordinator {
+ Coordinator(
+ text: $text,
+ isFocused: isFocused,
+ onCommit: onCommit,
+ onCancel: onCancel
)
}
+
+ func makeNSView(context: Context) -> AppBubbleNativeTextField {
+ let field = AppBubbleNativeTextField()
+ field.isBordered = false
+ field.isBezeled = false
+ field.drawsBackground = false
+ field.focusRingType = .none
+ field.font = NSFont.systemFont(ofSize: 15, weight: .medium)
+ field.textColor = .white
+ field.placeholderString = placeholder
+ field.delegate = context.coordinator
+ field.target = context.coordinator
+ field.action = #selector(Coordinator.submitAction(_:))
+ field.cell?.usesSingleLineMode = true
+ field.cell?.wraps = false
+ field.lineBreakMode = .byTruncatingTail
+ field.setAccessibilityLabel(placeholder)
+ context.coordinator.field = field
+ return field
+ }
+
+ func updateNSView(_ field: AppBubbleNativeTextField, context: Context) {
+ context.coordinator.text = $text
+ context.coordinator.isFocused = isFocused
+ context.coordinator.onCommit = onCommit
+ context.coordinator.onCancel = onCancel
+ context.coordinator.field = field
+ field.placeholderString = placeholder
+ field.setAccessibilityLabel(placeholder)
+
+ if !field.hasMarkedTextInput, field.stringValue != text {
+ field.stringValue = text
+ }
+
+ guard isFocused.wrappedValue, field.window != nil, field.currentEditor() == nil else { return }
+ DispatchQueue.main.async { [weak field] in
+ guard let field, field.window != nil, field.currentEditor() == nil else { return }
+ field.window?.makeFirstResponder(field)
+ }
+ }
+
+ final class Coordinator: NSObject, NSTextFieldDelegate {
+ var text: Binding<String>
+ var isFocused: FocusState<Bool>.Binding
+ var onCommit: () -> Void
+ var onCancel: () -> Void
+ weak var field: AppBubbleNativeTextField?
+
+ init(
+ text: Binding<String>,
+ isFocused: FocusState<Bool>.Binding,
+ onCommit: @escaping () -> Void,
+ onCancel: @escaping () -> Void
+ ) {
+ self.text = text
+ self.isFocused = isFocused
+ self.onCommit = onCommit
+ self.onCancel = onCancel
+ }
+
+ func controlTextDidChange(_ notification: Notification) {
+ guard let field = notification.object as? AppBubbleNativeTextField,
+ !field.hasMarkedTextInput
+ else { return }
+ syncFinalText(from: field)
+ }
+
+ func controlTextDidEndEditing(_ notification: Notification) {
+ if let field = notification.object as? AppBubbleNativeTextField {
+ syncFinalText(from: field)
+ }
+ isFocused.wrappedValue = false
+ }
+
+ func control(
+ _ control: NSControl,
+ textView: NSTextView,
+ doCommandBy commandSelector: Selector
+ ) -> Bool {
+ if textView.hasMarkedText() {
+ return false
+ }
+
+ switch commandSelector {
+ case #selector(NSResponder.insertNewline(_:)):
+ if let field = control as? AppBubbleNativeTextField {
+ syncFinalText(from: field)
+ }
+ onCommit()
+ return true
+ case #selector(NSResponder.cancelOperation(_:)):
+ isFocused.wrappedValue = false
+ onCancel()
+ return true
+ default:
+ return false
+ }
+ }
+
+ @objc func submitAction(_ sender: Any?) {
+ guard let field = sender as? AppBubbleNativeTextField,
+ !field.hasMarkedTextInput
+ else { return }
+ syncFinalText(from: field)
+ onCommit()
+ }
+
+ private func syncFinalText(from field: AppBubbleNativeTextField) {
+ guard !field.hasMarkedTextInput else { return }
+ let limited = String(field.stringValue.prefix(TagDatabase.maxAppNoteLength))
+ if field.stringValue != limited {
+ field.stringValue = limited
+ }
+ text.wrappedValue = limited
+ }
+ }
+}
+
+private final class AppBubbleNativeTextField: NSTextField {
+ var hasMarkedTextInput: Bool {
+ (currentEditor() as? NSTextView)?.hasMarkedText() == true
+ }
}
private struct BubbleArrow: Shape {
--
Gitblit v1.9.3