diff --git a/Source/InputMethodController.swift b/Source/InputMethodController.swift index 3b0982f6..aaf247c5 100644 --- a/Source/InputMethodController.swift +++ b/Source/InputMethodController.swift @@ -492,6 +492,10 @@ extension McBopomofoInputMethodController { } (client as? IMKTextInput)?.attributes(forCharacterIndex: Int(cursor), lineHeightRectangle: &lineHeightRect) + if lineHeightRect.origin.x == 0 && lineHeightRect.origin.y == 0 && cursor > 0 { + cursor -= 1 + (client as? IMKTextInput)?.attributes(forCharacterIndex: Int(cursor), lineHeightRectangle: &lineHeightRect) + } if useVerticalMode { gCurrentCandidateController?.set(windowTopLeftPoint: NSMakePoint(lineHeightRect.origin.x + lineHeightRect.size.width + 4.0, lineHeightRect.origin.y - 4.0), bottomOutOfScreenAdjustmentHeight: lineHeightRect.size.height + 4.0) @@ -507,6 +511,10 @@ extension McBopomofoInputMethodController { cursor -= 1 } (client as? IMKTextInput)?.attributes(forCharacterIndex: Int(cursor), lineHeightRectangle: &lineHeightRect) + if lineHeightRect.origin.x == 0 && lineHeightRect.origin.y == 0 && cursor > 0 { + cursor -= 1 + (client as? IMKTextInput)?.attributes(forCharacterIndex: Int(cursor), lineHeightRectangle: &lineHeightRect) + } McBopomofoInputMethodController.tooltipController.show(tooltip: tooltip, at: lineHeightRect.origin) } diff --git a/Source/InputState.swift b/Source/InputState.swift index 6483c626..fdbe1ebb 100644 --- a/Source/InputState.swift +++ b/Source/InputState.swift @@ -104,28 +104,28 @@ class InputState: NSObject { /// Represents that the composing buffer is not empty. @objc (InputStateNotEmpty) class NotEmpty: InputState { - @objc private(set) var composingBuffer: String = "" - @objc private(set) var cursorIndex: UInt = 0 + @objc private(set) var composingBuffer: String + @objc private(set) var cursorIndex: UInt + @objc private(set) var phrases: [InputPhrase] - @objc init(composingBuffer: String, cursorIndex: UInt) { + @objc init(composingBuffer: String, cursorIndex: UInt, phrases: [InputPhrase]) { self.composingBuffer = composingBuffer self.cursorIndex = cursorIndex + self.phrases = phrases } override var description: String { - "" + "" } } /// Represents that the user is inputting text. @objc (InputStateInputting) class Inputting: NotEmpty { - @objc var bpmfReading: String = "" - @objc var bpmfReadingCursorIndex: UInt8 = 0 @objc var poppedText: String = "" - @objc override init(composingBuffer: String, cursorIndex: UInt) { - super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex) + @objc override init(composingBuffer: String, cursorIndex: UInt, phrases: [InputPhrase]) { + super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex, phrases: phrases) } @objc var attributedString: NSAttributedString { @@ -137,7 +137,7 @@ class InputState: NSObject { } override var description: String { - "" + ", poppedText:\(poppedText)>" } } @@ -147,6 +147,7 @@ class InputState: NSObject { /// Represents that the user is marking a range in the composing buffer. @objc (InputStateMarking) class Marking: NotEmpty { + @objc private(set) var markerIndex: UInt @objc private(set) var markedRange: NSRange @objc var tooltip: String { @@ -170,15 +171,12 @@ class InputState: NSObject { return String(format: NSLocalizedString("You are now selecting \"%@\". Press enter to add a new phrase.", comment: ""), text) } - @objc private(set) var readings: [String] - - @objc init(composingBuffer: String, cursorIndex: UInt, markerIndex: UInt, readings: [String]) { + @objc init(composingBuffer: String, cursorIndex: UInt, markerIndex: UInt, phrases: [InputPhrase]) { self.markerIndex = markerIndex let begin = min(cursorIndex, markerIndex) let end = max(cursorIndex, markerIndex) markedRange = NSMakeRange(Int(begin), Int(end - begin)) - self.readings = readings - super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex) + super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex, phrases: phrases) } @objc var attributedString: NSAttributedString { @@ -197,16 +195,16 @@ class InputState: NSObject { .underlineStyle: NSUnderlineStyle.single.rawValue, .markedClauseSegment: 2 ], range: NSRange(location: end, - length: composingBuffer.count - end)) + length: (composingBuffer as NSString).length - end)) return attributedSting } override var description: String { - "" + "" } @objc func convertToInputting() -> Inputting { - let state = Inputting(composingBuffer: composingBuffer, cursorIndex: cursorIndex) + let state = Inputting(composingBuffer: composingBuffer, cursorIndex: cursorIndex, phrases: phrases) return state } @@ -217,7 +215,19 @@ class InputState: NSObject { @objc var userPhrase: String { let text = (composingBuffer as NSString).substring(with: markedRange) let end = markedRange.location + markedRange.length - let readings = readings[markedRange.location..= end { + break + } + if length >= markedRange.location { + selectedPhrases.append(component) + } + length += (component.text as NSString).length + } + + let readings = selectedPhrases.map { $0.reading } let joined = readings.joined(separator: "-") return "\(text) \(joined)" } @@ -229,10 +239,10 @@ class InputState: NSObject { @objc private(set) var candidates: [String] @objc private(set) var useVerticalMode: Bool - @objc init(composingBuffer: String, cursorIndex: UInt, candidates: [String], useVerticalMode: Bool) { + @objc init(composingBuffer: String, cursorIndex: UInt, candidates: [String], phrases: [InputPhrase], useVerticalMode: Bool) { self.candidates = candidates self.useVerticalMode = useVerticalMode - super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex) + super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex, phrases: phrases) } @objc var attributedString: NSAttributedString { @@ -244,7 +254,7 @@ class InputState: NSObject { } override var description: String { - "" + "" } } @@ -266,3 +276,48 @@ class InputState: NSObject { } } + +class InputPhrase: NSObject { + @objc private (set) var text: String + @objc private (set) var reading: String + + @objc init(text: String, reading: String) { + self.text = text + self.reading = reading + super.init() + } +} + +class StringUtils: NSObject { + + static func convertToCharIndex(from utf16Index: Int, in string: String) -> Int { + var length = 0 + for (i, c) in string.enumerated() { + if length >= utf16Index { + return i + } + length += c.utf16.count + } + return string.count + } + + @objc (nextUtf16PositionForIndex:in:) + static func nextUtf16Position(for index: Int, in string: String) -> Int { + var index = convertToCharIndex(from: index, in: string) + if index < string.count { + index += 1 + } + let count = string[.. Int { + var index = convertToCharIndex(from: index, in: string) + if index > 0 { + index -= 1 + } + let count = string[..cursorIndex() > 0) { - InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:currentState.composingBuffer cursorIndex:currentState.cursorIndex markerIndex:currentState.cursorIndex - 1 readings: [self _currentReadings]]; + if (currentState.cursorIndex > 0) { + NSInteger previousPosition = [StringUtils previousUtf16PositionForIndex:currentState.cursorIndex in:currentState.composingBuffer]; + InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:currentState.composingBuffer cursorIndex:currentState.cursorIndex markerIndex:previousPosition phrases:currentState.phrases]; stateCallback(marking); } else { errorCallback(); @@ -613,8 +614,9 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot if ([input isShiftHold]) { // Shift + Right - if (_builder->cursorIndex() < _builder->length()) { - InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:currentState.composingBuffer cursorIndex:currentState.cursorIndex markerIndex:currentState.cursorIndex + 1 readings: [self _currentReadings]]; + if (currentState.cursorIndex < currentState.composingBuffer.length) { + NSInteger nextPosition = [StringUtils nextUtf16PositionForIndex:currentState.cursorIndex in:currentState.composingBuffer]; + InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:currentState.composingBuffer cursorIndex:currentState.cursorIndex markerIndex:nextPosition phrases:currentState.phrases]; stateCallback(marking); } else { errorCallback(); @@ -840,8 +842,8 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot && ([input isShiftHold])) { NSUInteger index = state.markerIndex; if (index > 0) { - index -= 1; - InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:state.composingBuffer cursorIndex:state.cursorIndex markerIndex:index readings:state.readings]; + index = [StringUtils previousUtf16PositionForIndex:index in:state.composingBuffer]; + InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:state.composingBuffer cursorIndex:state.cursorIndex markerIndex:index phrases:state.phrases]; stateCallback(marking); } else { errorCallback(); @@ -855,8 +857,8 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot && ([input isShiftHold])) { NSUInteger index = state.markerIndex; if (index < state.composingBuffer.length) { - index += 1; - InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:state.composingBuffer cursorIndex:state.cursorIndex markerIndex:index readings:state.readings]; + index = [StringUtils nextUtf16PositionForIndex:index in:state.composingBuffer]; + InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:state.composingBuffer cursorIndex:state.cursorIndex markerIndex:index phrases:state.phrases]; stateCallback(marking); } else { errorCallback(); @@ -1129,6 +1131,8 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot size_t readingCursorIndex = 0; size_t builderCursorIndex = _builder->cursorIndex(); + NSMutableArray *phrases = [[NSMutableArray alloc] init]; + // we must do some Unicode codepoint counting to find the actual cursor location for the client // i.e. we need to take UTF-16 into consideration, for which a surrogate pair takes 2 UniChars // locations @@ -1141,6 +1145,10 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot NSString *valueString = [NSString stringWithUTF8String:nodeStr.c_str()]; [composingBuffer appendString:valueString]; + NSString *readingString = [NSString stringWithUTF8String:(*wi).node->currentKeyValue().key.c_str()]; + InputPhrase *phrase = [[InputPhrase alloc] initWithText:valueString reading:readingString]; + [phrases addObject:phrase]; + // this re-aligns the cursor index in the composed string // (the actual cursor on the screen) with the builder's logical // cursor (reading) cursor; each built node has a "spanning length" @@ -1169,7 +1177,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot NSString *composedText = [head stringByAppendingString:[reading stringByAppendingString:tail]]; NSInteger cursorIndex = composedStringCursorIndex + [reading length]; - InputStateInputting *newState = [[InputStateInputting alloc] initWithComposingBuffer:composedText cursorIndex:cursorIndex]; + InputStateInputting *newState = [[InputStateInputting alloc] initWithComposingBuffer:composedText cursorIndex:cursorIndex phrases:phrases]; return newState; } @@ -1239,7 +1247,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot } } - InputStateChoosingCandidate *state = [[InputStateChoosingCandidate alloc] initWithComposingBuffer:currentState.composingBuffer cursorIndex:currentState.cursorIndex candidates:candidatesArray useVerticalMode:useVerticalMode]; + InputStateChoosingCandidate *state = [[InputStateChoosingCandidate alloc] initWithComposingBuffer:currentState.composingBuffer cursorIndex:currentState.cursorIndex candidates:candidatesArray phrases:currentState.phrases useVerticalMode:useVerticalMode]; return state; }