(This is a revised question - including answer - following on from macOS: Take emoji from characterPalette which describes the problems encountered in more detail)
Background/use case
I have an app where, instead of creating and maintaining an icon library, I let users type an emoji as a placeholder graphic. This works beautifully within the context of my app, but I am not happy with the input mechanism I use.
Problem
I would like to simplify this so I open the characterPalette, select an emoji, and display it either as the button's StringValue or in a Label (=non-editable NSTextField).
This does not seem possible. Unlike NSColorPanel or NSFontPanel, the characterPanel is not exposed to the Cocoa framework, so I cannot take its selectedValue, set its action, or catch a notification. The documentation for orderFrontCharacterPalette simply says Opens the character palette which ... is not helpful.
Attempted solutions and problems encountered
I tried to work with making my receiver the firstResponder, but unlike NSTextView, NSTextField cannot process emoji. I found a workaround using an NSTextView with an NSBox in front, making it the firstResponder, and using NSApp.orderFrontCharacterPalette(sender)but found that under various circumstances which all seem to involve an extra drawing call – setting the button's title, showing a label in SystemFont Mini size (regular size worked fine) the CharacterPalette will open (=the system menu now offers 'Hide Emoji & Symbols') without being displayed. (This persists until the application closes, even if you try to open the CharacterPalette through the regular menu/shortcut)
For the partial solution involving NSTextInputClient (the no-show seems to be a persistent bug), see answer below.
The emoji picker needs a minimal implementation of NSTextInputClient. For example a button:
class MyButton: NSButton, NSTextInputClient {
    
    override var acceptsFirstResponder: Bool {
        get {
            return true
        }
    }
    
    override func becomeFirstResponder() -> Bool {
        return true
    }
    
    override func resignFirstResponder() -> Bool {
        return true
    }
    func insertText(_ string: Any, replacementRange: NSRange) {
        // this method is called when the user selects an emoji
        if let string = string as? String {
            self.title = string
        }
    }
    
    func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange) {
    }
    
    func unmarkText() {
    }
    
    func selectedRange() -> NSRange {
        return NSMakeRange(0, 0)
    }
    
    func markedRange() -> NSRange {
        return NSMakeRange(NSNotFound, 0)
    }
    
    func hasMarkedText() -> Bool {
        return false
    }
    
    func attributedSubstring(forProposedRange range: NSRange, actualRange: NSRangePointer?) -> NSAttributedString? {
        return nil
    }
    
    func validAttributesForMarkedText() -> [NSAttributedString.Key] {
        return []
    }
    
    func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect {
        // the emoji picker uses the returned rect to position itself
        var rect = self.bounds
        rect.origin.x = NSMidX(rect)
        rect.size.width = 0
        return self.window!.convertToScreen(self.convert(rect, to:nil))
    }
    
    func characterIndex(for point: NSPoint) -> Int {
        return 0
    }
    
}
NSTextInputClient needs a NSTextInputContext. NSView returns a context from inputContext if the class conforms to NSTextInputClient unless isEditable is implemented and returns false. A label doesn't return a NSTextInputContext, the solution is to override inputContext:
class MyTextField: NSTextField, NSTextInputClient {
    
    var myInputContext : NSTextInputContext?
    override var inputContext: NSTextInputContext? {
        get {
            if myInputContext == nil {
                myInputContext = NSTextInputContext(client:self)
            }
            return myInputContext
        }
    }
    // and the same methods as the button, set self.stringValue instead of self.title in insertText(_:replacementRange:)
 }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With