5.6 KiB
Some Notes on Cocoa's Text Input
To use Cocoa's text input system, e.g. the 2-Set Korean input, your view has to implement the NSTextInputClient protocol. Apple's documentation is very scarce, so we're writing down some of our findings.
Simple Case
For simple cases like ü
, which can be entered by Opt-u
+ u
, it's quite straightforward:
- Enter
Opt-u
. hasMarkedText()
is called to check whether we already have marked text.setMarkedText("¨", selectedRange NSRange(1, 0), replacementRange: NSRange(NSNotFound, 0))
is called. In this case the first argument is anNSString
,selectedRange
tells us where to put the cursor relative to the string: in this case after¨
. The rangereplacemenRange
tells us whether the string should replace some of the existing text. In this case no replacement is required.- Enter
u
. hasMarkedText()
is called again.insertText("ü", replacementRange: NSRange(NSNotFound, 0))
is called to finalize the input. It seems that for the replacement range(NSNotFound, 0)
we should replace the previously marked text with the final string. So in this case we must first delete¨
and insertü
.
Korean (Hangul, 한글)
Let's move to a bit more complicated case: Korean. In this case more methods are involved:
selectedRange()
: all other additional methods seem to rely on this method. Ideally we should returnNSRange(CursorPosition, 0)
when nothing is selected orNSRange(SelectionBegin, SelectionLength)
when there's a selection.attributedSubstringForProposedRange(_:actualRange:)
: for entering only Hangul, this method can be ignored.
Let's assume we want to enter 하태원
: (hasMarkedText()
is called here and there...)
selectedRange()
is called multiple times when changing the input method from US to Korean. This is also the case when starting the app with Korean input selected.- Enter
ㅎ
. setMarkedText("ㅎ", selectedRange: NSRange(1, 0) replacementRange:NSRange(NotFound, 0))
is called.- Enter
ㅏ
. attributedSubstringForProposedRange(_:actualRange:)
andselectedRange()
are called multiple times: again, for only Hangul, ignorable.setMarkedText("하", selectedRange: NSRange(1, 0), replacementRange: NSRange(NotFound, 0))
is called: deleteㅎ
and insert하
; not yet finalized.- Enter
ㅌ
attributedSubstringForProposedRange(_:actualRange:)
andselectedRange()
are called multiple times: ignore.setMarkedText("핱", selectedRange: NSRange(1, 0), replacementRange: NSRange(NotFound, 0))
is called: delete하
and insert핱
; not yet finalized.- Enter
ㅐ
attributedSubstringForProposedRange(_:actualRange:)
andselectedRange()
are called multiple times: ignore.setMarkedText("하", selectedRange: NSRange(1, 0), replacementRange: NSRange(NotFound, 0))
is called: delete핱
and insert하
; not yet finalized.insertText("하", replacementRange: NSRange(NotFound, 0))
is called to finalize the input of하
.attributedSubstringForProposedRange(_:actualRange:)
andselectedRange()
are called multiple times: ignore.setMarkedText("태", selectedRange: NSRange(1, 0), replacementRange: NSRange(NotFound, 0))
is called: Since the replacement range isNotFound
, append the marked text태
to the freshly finalized하
.- ...
Hanja (한자)
Let's consider the even more complicated case: Hanja in Korean. In this case the selectedRange()
and attributedSubstringForProposedRange(_:actualRange:)
play a vital role and also
firstRectForCharacterRange(_:actualRange)
: this method is used to determine where to show the Hanja popup. The character range is determined byselectedRange()
.
Let's assume we want to enter 河
: (again hasMarkedText()
is called here and there...)
- Enter
ㅎ
. setMarkedText("ㅎ", selectedRange: NSRange(1, 0) replacementRange:NSRange(NotFound, 0))
is called.- Enter
ㅏ
. attributedSubstringForProposedRange(_:actualRange:)
,selectedRange()
andhasMarkedText()
are called multiple times: again, for only Hangul, ignorable.setMarkedText("하", selectedRange: NSRange(1, 0), replacementRange: NSRange(NotFound, 0))
is called: deleteㅎ
and insert하
; not yet finalized.- Enter
Opt-Return
. setMarkedText("하", selectedRange: NSRange(1, 0), replacementRange: NSRange(NotFound, 0))
is called again.selectedRange()
is called: here we should return a range which can be consistently used byattributedSubstringForProposedRange(_:actualRange)
andfirstRectForCharacterRange(_:actualRange)
.insertText("하", replacementRange: NSRange(NotFound, 0))
is called even we are not done yet... So our view thinks we finalized the input of하
.attributedSubstringForProposedRange(_:actualRange)
is called multiple times to get the Hangul syllable to replace with Hanja. The proposed range can be very different in each call.- Only if the range from
selectedRange()
could be somehow consistently used inattributedSubstringForProposedRange(_:actualRange)
, then the Hanja popup is displayed. Otherwise we get the selectorinsertNewlineIgnoringFieldEditor
indoCommandBySelector()
. setMarkedText("下" , selectedRange: NSRange(1, 0), replacementRange: NSRange(1, 1))
is called: the replacement range is notNotFound
which means that we first have to delete the text in the given range, in this case the finalized하
and then append the marked text.- Selecting different Hanja calls the usual
setMarkedText(_:selectedRange:actualRange)
andReturn
finalizes the input of河
.
Other Writing System
Not a clue, since I only know Latin alphabet and Korean (+Hanja)...