Как перехватить значение из NSResponder и использовать эту строку?

Моя цель: у меня есть приложение для macOS, и я хочу...

  1. Есть кнопка для открытия средства выбора смайлов (справилось)
  2. Когда пользователь выбирает смайлик, перехватите это значение с помощью NSResponder.
  3. Затем используйте его на ярлыке моей кнопки.

Код ниже является упрощением. Мне нужна помощь: как я могу перехватить значение из NSResponder и распечатать его?

import SwiftUI
import AppKit

struct ContentView: View {
    private let emojiResponder = EmojiResponder()
    
    var body: some View {
        Button {
            // open the emoji picker
            if let window = NSApplication.shared.keyWindow {
                window.makeFirstResponder(emojiResponder)
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                    NSApp.orderFrontCharacterPalette(nil)
                }
            }
        } label: {
            Text("Emoji?")
        }
        .onAppear {
            emojiResponder.onEmojiSelected = { selectedEmoji in
                print(selectedEmoji)
            }
        }
    }
}

// Custom Emoji NSResponder
class EmojiResponder: NSResponder {
    var onEmojiSelected: ((String) -> Void)?
    
    // use insertTest method from NSResponder
    // I assume this allows me to get the input 
    override func insertText(_ insertString: Any) {
        guard let selectedEmoji = insertString as? String else { return }
        onEmojiSelected?(selectedEmoji)
        print(selectedEmoji)
    }
}

Может ли кто-нибудь дать мне подсказку, как это решить? Что происходит сейчас: окно выбора смайлов открывается, но после выбора смайла ничего не происходит.

Примечание. Я использую решение из этого обсуждения — https://stackoverflow.com/a/77797594/5133585. Но я надеюсь на решение без скрытого TextField.


Причина, по которой я не хочу использовать здесь TextField:

У меня есть список, который проходит через массив. В каждой строке есть 3 текстовых поля: [эмодзи] —— [имя] —— [дата]

Когда я выбираю строку и нажимаю Enter в macOS, она фокусируется на текстовом поле эмодзи. Я хочу сосредоточиться на имени TextField. Следовательно, я надеюсь удалить это текстовое поле смайлика и решить эту проблему.

См. stackoverflow.com/a/77797594/5133585

Sweeper 22.08.2024 08:30

Привет @Sweeper! Я должен был сказать, что использую это решение. Оно работает. Но со скрытым текстовым полем это немного хакерски. Я пытаюсь найти другое решение, не связанное с TextField. Я не знаю, может ли кнопка быть службой быстрого реагирования.

dev_dev 22.08.2024 08:48

Что вы подразумеваете под «Когда я выбираю и нажимаю Enter в macOS, он фокусируется на текстовом поле эмодзи»? Что вы выбираете?

Sweeper 22.08.2024 09:47

@Sweeper — Учитывая, что у меня есть список выбора, каждая строка имеет 3 текстовых поля. Когда я выбираю строку (из списка) и нажимаю Enter. Затем macOS автоматически фокусируется на первом TextField (смайлике). Надеюсь, это прояснит ситуацию.

dev_dev 22.08.2024 10:49

Так что только не ставьте его первым TextField тогда? Всё равно оно скрыто. Средство выбора смайлов должно сосредоточиться на чем-то, и это должно быть чем-то, что вы можете контролировать, чтобы получить то, что выбрано.

Sweeper 22.08.2024 10:52

Или вы можете написать подкласс NSButton, который будет действовать как кнопка «Эмодзи», тогда вы сможете сфокусироваться на этом средстве выбора эмодзи. Поможет ли это?

Sweeper 22.08.2024 10:55

Я бы предложил отредактировать вопрос, включив в него минимально воспроизводимый пример вашей попытки применить решение из связанного поста и объяснить, как воспроизвести нежелательное поведение.

Sweeper 22.08.2024 10:56
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
7
60
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Средство выбора смайлов должно сосредоточиться на чем-то, и это должно быть чем-то, что вы можете контролировать, чтобы получить то, что выбрано.

Если вам не нравится фокусироваться на скрытом TextField, вы можете сосредоточиться на кнопке, которая открывает окно выбора смайлов. Создайте подкласс NSButton, соответствующий NSTextInputClient, как в этом ответе, и оберните его в NSViewRepresentable.

struct EmojiButton: NSViewRepresentable {
    // I chose to use a callback closure here
    // but you can easily make this into a @Binding if you prefer that
    let onSelectEmoji: (String) -> Void
    
    func makeNSView(context: Context) -> TextReceiverButton {
        context.coordinator.button
    }
    
    func updateNSView(_ nsView: TextReceiverButton, context: Context) {
        nsView.receiveText = onSelectEmoji
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator()
    }
    
    @MainActor
    class Coordinator: NSObject {
        let button = TextReceiverButton(title: "Emoji", target: nil, action: nil)
        
        override init() {
            super.init()
            button.target = self
            button.action = #selector(displayEmojiInButton(_:))
        }
        
        @objc func displayEmojiInButton(_ sender: Any) {
            NSApp.orderFrontCharacterPalette(nil)
            Task {
                // this has to be done asynchronously, as in your attempt
                button.window?.makeFirstResponder(button)
            }
        }
    }
}

class TextReceiverButton: NSButton, NSTextInputClient {
    var receiveText: ((String) -> Void)?
    
    nonisolated func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange) {
        
    }
    
    nonisolated func unmarkText() {
        
    }
    
    nonisolated func selectedRange() -> NSRange {
        .init()
    }
    
    nonisolated func markedRange() -> NSRange {
        .init()
    }
    
    nonisolated func hasMarkedText() -> Bool {
        false
    }
    
    nonisolated func attributedSubstring(forProposedRange range: NSRange, actualRange: NSRangePointer?) -> NSAttributedString? {
        nil
    }
    
    nonisolated func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect {
        .zero
    }
    
    nonisolated func characterIndex(for point: NSPoint) -> Int {
        0
    }

    nonisolated func insertText(_ string: Any, replacementRange: NSRange) {
        if let receivedText = string as? String {
            Task { @MainActor in
                receiveText?(receivedText)
            }
        }
    }
    

    nonisolated func validAttributesForMarkedText() -> [NSAttributedString.Key] {
        return [.font, .paragraphStyle, .writingDirection]
    }
}

Поскольку вы упомянули выбираемый List со строками, содержащими текстовые поля, вот пример использования в аналогичной ситуации. Это просто распечатает выбранные смайлы.

struct ContentView: View {
    @State var text = ""
    @State var selection: Int?
    
    var body: some View {
        List(selection: $selection) {
            ForEach(0..<10) { i in Row().tag(i) }
        }
    }
}

struct Row: View {
    @State var name = ""
    @State var date = ""
    var body: some View {
        HStack {
            EmojiButton { 
                print("Selected", $0)
            }
            TextField("Name", text: $name)
            TextField("Date", text: $date)
        }
    }
}

Чтобы средство выбора смайлов отображалось во всплывающем окне, предварительно сфокусированное представление перед выполнением orderFrontCharacterPalette должно быть текстовым полем. Поэтому, если вы хотите, чтобы всплывающее окно отображалось на кнопке, вам все равно придется поставить невидимый NSTextView в центре кнопки.

Возможно, поскольку это текстовое поле находится на стороне AppKit, SwiftUI не знает об этом и не будет иметь нежелательного поведения фокуса при нажатии клавиши возврата.

При нажатии кнопки установите для первого ответчика режим невидимого текстового представления, а затем асинхронно вызовите orderFrontCharacterPalette.

Вот пример реализации.

struct EmojiButton: NSViewRepresentable {
    let onSelectEmoji: (String) -> Void
    
    func makeNSView(context: Context) -> NSView {
        context.coordinator.view
    }
    
    func updateNSView(_ nsView: NSView, context: Context) {
        context.coordinator.textView.receiveText = onSelectEmoji
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator()
    }
    
    @MainActor
    class Coordinator: NSObject {
        let button = NSButton(title: "Emoji", target: nil, action: nil)
        let textView = TextReceiverView(frame: .init(x: 0, y: 0, width: 1, height: 1))
        let view = NSView()
        
        override init() {
            super.init()
            
            button.target = self
            button.action = #selector(displayEmojiInButton(_:))
            
            // remove the cursor
            textView.insertionPointColor = .clear
            
            button.translatesAutoresizingMaskIntoConstraints = false
            textView.translatesAutoresizingMaskIntoConstraints = false
            // add the textView first so the button goes above it
            view.addSubview(textView)
            view.addSubview(button)
            NSLayoutConstraint.activate([
                button.leftAnchor.constraint(equalTo: view.leftAnchor),
                button.rightAnchor.constraint(equalTo: view.rightAnchor),
                button.topAnchor.constraint(equalTo: view.topAnchor),
                button.bottomAnchor.constraint(equalTo: view.bottomAnchor),
                textView.widthAnchor.constraint(equalToConstant: 1),
                textView.heightAnchor.constraint(equalToConstant: 1),
                textView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                textView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            ])
        }
        
        @objc func displayEmojiInButton(_ sender: Any) {
            button.window?.makeFirstResponder(textView)
            Task {
                try await Task.sleep(for: .milliseconds(100))
                NSApp.orderFrontCharacterPalette(nil)
            }
        }
    }
}

class TextReceiverView: NSTextView {
    
    var receiveText: ((String) -> Void)?

    override func insertText(_ string: Any, replacementRange: NSRange) {
        if let receivedText = string as? String {
            receiveText?(receivedText)
        }
    }
}

Спасибо. Это происходит в правильном направлении. Единственная проблема: средство выбора смайлов отображается в виде окна. Техника из этого: stackoverflow.com/a/77797594/5133585 заставляет палитру персонажей (выбор эмодзи) отображаться в виде всплывающей подсказки. (Это здорово, потому что оно следует за окном приложения.) Возможно ли это? В противном случае мне, возможно, придется подумать о другом подходе.

dev_dev 22.08.2024 12:18

@dev_dev иногда показывает всплывающую подсказку. Кажется, это зависит от того, на чем вы сосредоточились, прежде чем нажать кнопку. Если фокус был на чем-то, для чего можно отобразить подсказку, подсказка будет отображаться оттуда.

Sweeper 22.08.2024 12:40

@dev_dev Я обновил ответ, чтобы использовать совершенно другой подход. Попробуйте это.

Sweeper 22.08.2024 13:49

ты лучший, спасибо тебе огромное. Я попробовал это на новом файле проекта, и он работает очень хорошо. Это наставило меня на правильный путь. Я попробую это в своем реальном проекте. Похоже, в таком случае изучение AppKit будет полезно. Я постараюсь изо всех сил изучить AppKit

dev_dev 23.08.2024 03:59

Другие вопросы по теме