From 4b4668ae2120e4daeabe253bb50e6916d58af835 Mon Sep 17 00:00:00 2001 From: Tae Won Ha Date: Sun, 3 Jul 2016 15:18:31 +0200 Subject: [PATCH] Add a note on text input --- SwiftNeoVim/NeoVimViewEvents.swift | 7 +++- docs/notes-on-cocoa-text-input.md | 66 ++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 docs/notes-on-cocoa-text-input.md diff --git a/SwiftNeoVim/NeoVimViewEvents.swift b/SwiftNeoVim/NeoVimViewEvents.swift index 20921918..4d20604e 100644 --- a/SwiftNeoVim/NeoVimViewEvents.swift +++ b/SwiftNeoVim/NeoVimViewEvents.swift @@ -34,7 +34,7 @@ extension NeoVimView: NSTextInputClient { } public func insertText(aString: AnyObject, replacementRange: NSRange) { -// NSLog("\(#function): \(replacementRange): '\(aString)'") + NSLog("\(#function): \(replacementRange): '\(aString)'") switch aString { case let string as String: @@ -56,7 +56,7 @@ extension NeoVimView: NSTextInputClient { } public override func doCommandBySelector(aSelector: Selector) { -// NSLog("\(#function): "\(aSelector)") + NSLog("\(#function): \(aSelector)"); // FIXME: handle when ㅎ -> delete @@ -112,6 +112,7 @@ extension NeoVimView: NSTextInputClient { public func selectedRange() -> NSRange { // When the app starts and the Hangul input method is selected, this method gets called very early... guard self.grid.hasData else { + NSLog("\(#function): not found") return NSRange(location: NSNotFound, length: 0) } @@ -134,6 +135,7 @@ extension NeoVimView: NSTextInputClient { } public func hasMarkedText() -> Bool { + NSLog("\(#function)") return self.markedText != nil } @@ -141,6 +143,7 @@ extension NeoVimView: NSTextInputClient { public func attributedSubstringForProposedRange(aRange: NSRange, actualRange: NSRangePointer) -> NSAttributedString? { NSLog("\(#function): \(aRange), \(actualRange[0])") if aRange.location == NSNotFound { + NSLog("\(#function): range not found: returning nil") return nil } diff --git a/docs/notes-on-cocoa-text-input.md b/docs/notes-on-cocoa-text-input.md new file mode 100644 index 00000000..793a5a8d --- /dev/null +++ b/docs/notes-on-cocoa-text-input.md @@ -0,0 +1,66 @@ +# 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](https://developer.apple.com/reference/appkit/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: + +1. Enter `Opt-u`. +1. `hasMarkedText()` is called to check whether we already have marked text. +1. `setMarkedText("¨", selectedRange NSRange(1, 0), replacementRange: NSRange(NSNotFound, 0))` is called. In this case the first argument is an `NSString`, `selectedRange` tells us where to put the cursor relative to the string: in this case after `¨`. The range `replacemenRange` tells us whether the string should replace some of the existing text. In this case no replacement is required. +1. Enter `u`. +1. `hasMarkedText()` is called again. +1. `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 return `NSRange(CursorPosition, 0)` when nothing is selected or `NSRange(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...) + +1. `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. +1. Enter `ㅎ`. +1. `setMarkedText("ㅎ", selectedRange: NSRange(1, 0) replacementRange:NSRange(NotFound, 0))` is called. +1. Enter `ㅏ`. +1. `attributedSubstringForProposedRange(_:actualRange:)` and `selectedRange()` are called multiple times: again, for only Hangul, ignorable. +1. `setMarkedText("하", selectedRange: NSRange(1, 0), replacementRange: NSRange(NotFound, 0))` is called: delete `ㅎ` and insert `하`; not yet finalized. +1. Enter `ㅌ` +1. `attributedSubstringForProposedRange(_:actualRange:)` and `selectedRange()` are called multiple times: ignore. +1. `setMarkedText("핱", selectedRange: NSRange(1, 0), replacementRange: NSRange(NotFound, 0))` is called: delete `하` and insert `핱`; not yet finalized. +1. Enter `ㅐ` +1. `attributedSubstringForProposedRange(_:actualRange:)` and `selectedRange()` are called multiple times: ignore. +1. `setMarkedText("하", selectedRange: NSRange(1, 0), replacementRange: NSRange(NotFound, 0))` is called: delete `핱` and insert `하`; not yet finalized. +1. `insertText("하", replacementRange: NSRange(NotFound, 0))` is called to finalize the input of `하`. +1. `attributedSubstringForProposedRange(_:actualRange:)` and `selectedRange()` are called multiple times: ignore. +1. `setMarkedText("태", selectedRange: NSRange(1, 0), replacementRange: NSRange(NotFound, 0))` is called: Since the replacement range is `NotFound`, append the marked text `태` to the freshly finalized `하`. +1. ... + +## 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 by `selectedRange()`. + +Let's assume we want to enter `河`: (again `hasMarkedText()` is called here and there...) + +1. Enter `ㅎ`. +1. `setMarkedText("ㅎ", selectedRange: NSRange(1, 0) replacementRange:NSRange(NotFound, 0))` is called. +1. Enter `ㅏ`. +1. `attributedSubstringForProposedRange(_:actualRange:)`, `selectedRange()` and `hasMarkedText()` are called multiple times: again, for only Hangul, ignorable. +1. `setMarkedText("하", selectedRange: NSRange(1, 0), replacementRange: NSRange(NotFound, 0))` is called: delete `ㅎ` and insert `하`; not yet finalized. +1. Enter `Opt-Return`. +1. `setMarkedText("하", selectedRange: NSRange(1, 0), replacementRange: NSRange(NotFound, 0))` is called again. +1. `selectedRange()` is called: here we should return a range which can be consistently used by `attributedSubstringForProposedRange(_:actualRange)` and `firstRectForCharacterRange(_:actualRange)`. +1. `insertText("하", replacementRange: NSRange(NotFound, 0))` is called even we are not done yet... So our view thinks we finalized the input of `하`. +1. `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. +1. Only if the range from `selectedRange()` could be somehow consistently used in `attributedSubstringForProposedRange(_:actualRange)`, then the Hanja popup is displayed. Otherwise we get the selector `insertNewlineIgnoringFieldEditor` in `doCommandBySelector()`. +1. `setMarkedText("下" , selectedRange: NSRange(1, 0), replacementRange: NSRange(1, 1))` is called: the replacement range is not `NotFound` 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. +1. Selecting different Hanja calls the usual `setMarkedText(_:selectedRange:actualRange)` and `Return` finalizes the input of `河`. + +## Other Writing System + +Not a clue, since I only know Latin alphabet and Korean (+Hanja)...