KeyHandler // Reducing ObjC needs to Mandarin Only.
This commit is contained in:
parent
5afd7f8e36
commit
0f58d30fcf
|
@ -28,22 +28,10 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
@class InputHandler;
|
@class InputHandler;
|
||||||
@class InputState;
|
@class InputState;
|
||||||
|
@class KeyHandlerSputnik;
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
typedef NSString *const InputMode NS_TYPED_ENUM;
|
|
||||||
extern InputMode imeModeCHT;
|
|
||||||
extern InputMode imeModeCHS;
|
|
||||||
extern InputMode imeModeNULL;
|
|
||||||
|
|
||||||
struct BufferStatePackage
|
|
||||||
{
|
|
||||||
NSString *composedText;
|
|
||||||
NSInteger cursorIndex;
|
|
||||||
NSString *resultOfRear;
|
|
||||||
NSString *resultOfFront;
|
|
||||||
};
|
|
||||||
|
|
||||||
@class KeyHandler;
|
@class KeyHandler;
|
||||||
|
|
||||||
@protocol KeyHandlerDelegate <NSObject>
|
@protocol KeyHandlerDelegate <NSObject>
|
||||||
|
@ -54,48 +42,20 @@ struct BufferStatePackage
|
||||||
|
|
||||||
@interface KeyHandler : NSObject
|
@interface KeyHandler : NSObject
|
||||||
|
|
||||||
- (BOOL)isBuilderEmpty;
|
|
||||||
|
|
||||||
- (void)fixNodeWithValue:(NSString *)value NS_SWIFT_NAME(fixNode(value:));
|
|
||||||
- (void)clear;
|
|
||||||
|
|
||||||
@property(strong, nonatomic) InputMode inputMode;
|
|
||||||
@property(weak, nonatomic) id<KeyHandlerDelegate> delegate;
|
@property(weak, nonatomic) id<KeyHandlerDelegate> delegate;
|
||||||
|
|
||||||
// The following items need to be exposed to Swift:
|
// The following items need to be exposed to Swift:
|
||||||
- (void)_walk;
|
|
||||||
- (NSString *)_popOverflowComposingTextAndWalk;
|
|
||||||
- (NSArray<NSString *> *)_currentReadings;
|
|
||||||
|
|
||||||
- (BOOL)checkWhetherToneMarkerConfirmsPhoneticReadingBuffer;
|
- (BOOL)checkWhetherToneMarkerConfirmsPhoneticReadingBuffer;
|
||||||
- (BOOL)chkKeyValidity:(UniChar)value;
|
- (BOOL)chkKeyValidity:(UniChar)value;
|
||||||
- (BOOL)ifLangModelHasUnigramsForKey:(NSString *)reading;
|
|
||||||
- (BOOL)isPhoneticReadingBufferEmpty;
|
- (BOOL)isPhoneticReadingBufferEmpty;
|
||||||
- (BOOL)isPrintable:(UniChar)charCode;
|
- (BOOL)isPrintable:(UniChar)charCode;
|
||||||
- (NSArray<NSString *> *)buildAssociatePhraseArrayWithKey:(NSString *)key;
|
|
||||||
- (NSArray<NSString *> *)getCandidatesArray;
|
|
||||||
- (NSInteger)getKeyLengthAtIndexZero;
|
|
||||||
- (NSInteger)getBuilderCursorIndex;
|
|
||||||
- (NSInteger)getBuilderLength;
|
|
||||||
- (NSInteger)getPackagedCursorIndex;
|
|
||||||
- (NSString *)getComposedText;
|
|
||||||
- (NSString *)getCompositionFromPhoneticReadingBuffer;
|
- (NSString *)getCompositionFromPhoneticReadingBuffer;
|
||||||
- (NSString *)getStrLocationResult:(BOOL)isFront NS_SWIFT_NAME(getStrLocationResult(isFront:));
|
|
||||||
- (NSString *)getSyllableCompositionFromPhoneticReadingBuffer;
|
- (NSString *)getSyllableCompositionFromPhoneticReadingBuffer;
|
||||||
- (void)clearPhoneticReadingBuffer;
|
- (void)clearPhoneticReadingBuffer;
|
||||||
- (void)combinePhoneticReadingBufferKey:(UniChar)charCode;
|
- (void)combinePhoneticReadingBufferKey:(UniChar)charCode;
|
||||||
- (void)createNewBuilder;
|
|
||||||
- (void)dealWithOverrideModelSuggestions;
|
|
||||||
- (void)deleteBuilderReadingAfterCursor;
|
|
||||||
- (void)deleteBuilderReadingInFrontOfCursor;
|
|
||||||
- (void)doBackSpaceToPhoneticReadingBuffer;
|
- (void)doBackSpaceToPhoneticReadingBuffer;
|
||||||
- (void)ensurePhoneticParser;
|
- (void)ensurePhoneticParser;
|
||||||
- (void)insertReadingToBuilderAtCursor:(NSString *)reading;
|
|
||||||
- (void)packageBufferStateMaterials;
|
|
||||||
- (void)removeBuilderAndReset:(BOOL)shouldReset;
|
|
||||||
- (void)setBuilderCursorIndex:(NSInteger)value;
|
|
||||||
- (void)setInputModesToLM:(BOOL)isCHS;
|
|
||||||
- (void)syncBaseLMPrefs;
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|
|
@ -25,134 +25,23 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#import "KeyHandler.h"
|
#import "KeyHandler.h"
|
||||||
#import "Gramambular.h"
|
|
||||||
#import "LMInstantiator.h"
|
|
||||||
#import "Mandarin.h"
|
#import "Mandarin.h"
|
||||||
#import "UserOverrideModel.h"
|
|
||||||
#import "mgrLangModel_Privates.h"
|
|
||||||
#import "vChewing-Swift.h"
|
#import "vChewing-Swift.h"
|
||||||
#import <string>
|
#import <string>
|
||||||
|
|
||||||
InputMode imeModeCHS = ctlInputMethod.kIMEModeCHS;
|
|
||||||
InputMode imeModeCHT = ctlInputMethod.kIMEModeCHT;
|
|
||||||
InputMode imeModeNULL = ctlInputMethod.kIMEModeNULL;
|
|
||||||
|
|
||||||
typedef vChewing::LMInstantiator BaseLM;
|
|
||||||
typedef vChewing::UserOverrideModel UserOverrideLM;
|
|
||||||
typedef Gramambular::BlockReadingBuilder BlockBuilder;
|
|
||||||
typedef Mandarin::BopomofoReadingBuffer PhoneticBuffer;
|
typedef Mandarin::BopomofoReadingBuffer PhoneticBuffer;
|
||||||
|
|
||||||
static const double kEpsilon = 0.000001;
|
|
||||||
|
|
||||||
NSString *packagedComposedText;
|
|
||||||
NSInteger packagedCursorIndex;
|
|
||||||
NSString *packagedResultOfRear;
|
|
||||||
NSString *packagedResultOfFront;
|
|
||||||
|
|
||||||
// NON-SWIFTIFIABLE
|
|
||||||
static double FindHighestScore(const std::vector<Gramambular::NodeAnchor> &nodes, double epsilon)
|
|
||||||
{
|
|
||||||
double highestScore = 0.0;
|
|
||||||
for (auto ni = nodes.begin(), ne = nodes.end(); ni != ne; ++ni)
|
|
||||||
{
|
|
||||||
double score = ni->node->highestUnigramScore();
|
|
||||||
if (score > highestScore)
|
|
||||||
highestScore = score;
|
|
||||||
}
|
|
||||||
return highestScore + epsilon;
|
|
||||||
}
|
|
||||||
|
|
||||||
// NON-SWIFTIFIABLE
|
|
||||||
class NodeAnchorDescendingSorter
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
bool operator()(const Gramambular::NodeAnchor &a, const Gramambular::NodeAnchor &b) const
|
|
||||||
{
|
|
||||||
return a.node->key().length() > b.node->key().length();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// if DEBUG is defined, a DOT file (GraphViz format) will be written to the
|
|
||||||
// specified path every time the grid is walked
|
|
||||||
#if DEBUG
|
|
||||||
static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// NON-SWIFTIFIABLE
|
// NON-SWIFTIFIABLE
|
||||||
@implementation KeyHandler
|
@implementation KeyHandler
|
||||||
{
|
{
|
||||||
// the reading buffer that takes user input
|
// the reading buffer that takes user input
|
||||||
PhoneticBuffer *_bpmfReadingBuffer;
|
PhoneticBuffer *_bpmfReadingBuffer;
|
||||||
|
|
||||||
// language model
|
|
||||||
BaseLM *_languageModel;
|
|
||||||
|
|
||||||
// user override model
|
|
||||||
UserOverrideLM *_userOverrideModel;
|
|
||||||
|
|
||||||
// the grid (lattice) builder for the unigrams (and bigrams)
|
|
||||||
BlockBuilder *_builder;
|
|
||||||
|
|
||||||
// latest walked path (trellis) using the Viterbi algorithm
|
|
||||||
std::vector<Gramambular::NodeAnchor> _walkedNodes;
|
|
||||||
|
|
||||||
NSString *_inputMode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@synthesize delegate = _delegate;
|
@synthesize delegate = _delegate;
|
||||||
|
|
||||||
// NON-SWIFTIFIABLE DUE TO VARIABLE AVAILABLE ACCESSIBILITY RANGE.
|
// Not migrable as long as there's still ObjC++ components needed.
|
||||||
// VARIABLE: "_inputMode"
|
// Will deprecate this once Mandarin gets Swiftified.
|
||||||
- (NSString *)inputMode
|
|
||||||
{
|
|
||||||
return _inputMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// NON-SWIFTIFIABLE
|
|
||||||
- (BOOL)isBuilderEmpty
|
|
||||||
{
|
|
||||||
return (_builder->grid().width() == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// NON-SWIFTIFIABLE DUE TO VARIABLE AVAILABLE ACCESSIBILITY RANGE.
|
|
||||||
// VARIABLE: "_inputMode"
|
|
||||||
- (void)setInputMode:(NSString *)value
|
|
||||||
{
|
|
||||||
// 下面這句的「isKindOfClass」是做類型檢查,
|
|
||||||
// 為了應對出現輸入法 plist 被改壞掉這樣的極端情況。
|
|
||||||
BOOL isCHS = [value isKindOfClass:[NSString class]] && [value isEqual:imeModeCHS];
|
|
||||||
|
|
||||||
// 緊接著將新的簡繁輸入模式提報給 ctlInputMethod:
|
|
||||||
ctlInputMethod.currentInputMode = isCHS ? imeModeCHS : imeModeCHT;
|
|
||||||
mgrPrefs.mostRecentInputMode = ctlInputMethod.currentInputMode;
|
|
||||||
|
|
||||||
// 拿當前的 _inputMode 與 ctlInputMethod 的提報結果對比,不同的話則套用新設定:
|
|
||||||
if (![_inputMode isEqualToString:ctlInputMethod.currentInputMode])
|
|
||||||
{
|
|
||||||
// Reinitiate language models if necessary
|
|
||||||
[self setInputModesToLM:isCHS];
|
|
||||||
|
|
||||||
// Synchronize the sub-languageModel state settings to the new LM.
|
|
||||||
[self syncBaseLMPrefs];
|
|
||||||
|
|
||||||
[self removeBuilderAndReset:YES];
|
|
||||||
|
|
||||||
if (![self isPhoneticReadingBufferEmpty])
|
|
||||||
[self clearPhoneticReadingBuffer];
|
|
||||||
}
|
|
||||||
_inputMode = ctlInputMethod.currentInputMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// NON-SWIFTIFIABLE: Required by an ObjC(pp)-based class.
|
|
||||||
- (void)dealloc
|
|
||||||
{ // clean up everything
|
|
||||||
if (_bpmfReadingBuffer)
|
|
||||||
delete _bpmfReadingBuffer;
|
|
||||||
if (_builder)
|
|
||||||
[self removeBuilderAndReset:NO];
|
|
||||||
}
|
|
||||||
|
|
||||||
// NON-SWIFTIFIABLE: Not placeable in swift extensions.
|
|
||||||
- (instancetype)init
|
- (instancetype)init
|
||||||
{
|
{
|
||||||
self = [super init];
|
self = [super init];
|
||||||
|
@ -164,262 +53,14 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NON-SWIFTIFIABLE
|
// NON-SWIFTIFIABLE: Mandarin
|
||||||
- (void)fixNodeWithValue:(NSString *)value
|
- (void)dealloc
|
||||||
{
|
{ // clean up everything
|
||||||
NSInteger cursorIndex = [self getActualCandidateCursorIndex];
|
if (_bpmfReadingBuffer)
|
||||||
std::string stringValue(value.UTF8String);
|
delete _bpmfReadingBuffer;
|
||||||
Gramambular::NodeAnchor selectedNode = _builder->grid().fixNodeSelectedCandidate(cursorIndex, stringValue);
|
|
||||||
if (!mgrPrefs.useSCPCTypingMode)
|
|
||||||
{ // 不要針對逐字選字模式啟用臨時半衰記憶模型。
|
|
||||||
// If the length of the readings and the characters do not match,
|
|
||||||
// it often means it is a special symbol and it should not be stored
|
|
||||||
// in the user override model.
|
|
||||||
BOOL addToOverrideModel = YES;
|
|
||||||
if (selectedNode.spanningLength != [value count])
|
|
||||||
addToOverrideModel = NO;
|
|
||||||
|
|
||||||
if (addToOverrideModel)
|
|
||||||
{
|
|
||||||
double score = selectedNode.node->scoreForCandidate(stringValue);
|
|
||||||
if (score <= -12) // 威注音的 SymbolLM 的 Score 是 -12。
|
|
||||||
addToOverrideModel = NO;
|
|
||||||
}
|
|
||||||
if (addToOverrideModel)
|
|
||||||
_userOverrideModel->observe(_walkedNodes, cursorIndex, stringValue, [[NSDate date] timeIntervalSince1970]);
|
|
||||||
}
|
|
||||||
[self _walk];
|
|
||||||
|
|
||||||
if (mgrPrefs.moveCursorAfterSelectingCandidate)
|
|
||||||
{
|
|
||||||
size_t nextPosition = 0;
|
|
||||||
for (auto node : _walkedNodes)
|
|
||||||
{
|
|
||||||
if (nextPosition >= cursorIndex)
|
|
||||||
break;
|
|
||||||
nextPosition += node.spanningLength;
|
|
||||||
}
|
|
||||||
if (nextPosition <= [self getBuilderLength])
|
|
||||||
[self setBuilderCursorIndex:nextPosition];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NON-SWIFTIFIABLE
|
// MARK: - 目前到這裡了
|
||||||
- (void)clear
|
|
||||||
{
|
|
||||||
[self clearPhoneticReadingBuffer];
|
|
||||||
_builder->clear();
|
|
||||||
_walkedNodes.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - States Building
|
|
||||||
|
|
||||||
// NON-SWIFTIFIABLE
|
|
||||||
- (void)packageBufferStateMaterials
|
|
||||||
{
|
|
||||||
// We gather the data through this function, package it,
|
|
||||||
// and sent it to our Swift extension to build the InputState.Inputting there.
|
|
||||||
// Otherwise, ObjC++ always bugs for "expecting a type".
|
|
||||||
|
|
||||||
// "updating the composing buffer" means to request the client to "refresh" the text input buffer
|
|
||||||
// with our "composing text"
|
|
||||||
NSMutableString *composingBuffer = [[NSMutableString alloc] init];
|
|
||||||
NSInteger composedStringCursorIndex = 0;
|
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
size_t readingCursorIndex = 0;
|
|
||||||
size_t builderCursorIndex = [self getBuilderCursorIndex];
|
|
||||||
|
|
||||||
NSString *resultOfRear = @"";
|
|
||||||
NSString *resultOfFront = @"";
|
|
||||||
|
|
||||||
for (std::vector<Gramambular::NodeAnchor>::iterator wi = _walkedNodes.begin(), we = _walkedNodes.end(); wi != we;
|
|
||||||
++wi)
|
|
||||||
{
|
|
||||||
if ((*wi).node)
|
|
||||||
{
|
|
||||||
std::string nodeStr = (*wi).node->currentKeyValue().value;
|
|
||||||
NSString *valueString = [NSString stringWithUTF8String:nodeStr.c_str()];
|
|
||||||
[composingBuffer appendString:valueString];
|
|
||||||
|
|
||||||
NSArray<NSString *> *splited = [valueString split];
|
|
||||||
NSInteger codepointCount = splited.count;
|
|
||||||
|
|
||||||
// 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"
|
|
||||||
// (e.g. two reading blocks has a spanning length of 2), and we
|
|
||||||
// accumulate those lengths to calculate the displayed cursor
|
|
||||||
// index
|
|
||||||
size_t spanningLength = (*wi).spanningLength;
|
|
||||||
if (readingCursorIndex + spanningLength <= builderCursorIndex)
|
|
||||||
{
|
|
||||||
composedStringCursorIndex += [valueString length];
|
|
||||||
readingCursorIndex += spanningLength;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (codepointCount == spanningLength)
|
|
||||||
{
|
|
||||||
for (size_t i = 0; i < codepointCount && readingCursorIndex < builderCursorIndex; i++)
|
|
||||||
{
|
|
||||||
composedStringCursorIndex += [splited[i] length];
|
|
||||||
readingCursorIndex++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (readingCursorIndex < builderCursorIndex)
|
|
||||||
{
|
|
||||||
composedStringCursorIndex += [valueString length];
|
|
||||||
readingCursorIndex += spanningLength;
|
|
||||||
if (readingCursorIndex > builderCursorIndex)
|
|
||||||
{
|
|
||||||
readingCursorIndex = builderCursorIndex;
|
|
||||||
}
|
|
||||||
if (builderCursorIndex == 0)
|
|
||||||
{
|
|
||||||
resultOfFront =
|
|
||||||
[NSString stringWithUTF8String:_builder->readings()[builderCursorIndex].c_str()];
|
|
||||||
}
|
|
||||||
else if (builderCursorIndex >= _builder->readings().size())
|
|
||||||
{
|
|
||||||
resultOfRear = [NSString
|
|
||||||
stringWithUTF8String:_builder->readings()[_builder->readings().size() - 1].c_str()];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
resultOfFront =
|
|
||||||
[NSString stringWithUTF8String:_builder->readings()[builderCursorIndex].c_str()];
|
|
||||||
resultOfRear =
|
|
||||||
[NSString stringWithUTF8String:_builder->readings()[builderCursorIndex - 1].c_str()];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// now we gather all the info, we separate the composing buffer to two parts, head and tail,
|
|
||||||
// and insert the reading text (the Mandarin syllable) in between them;
|
|
||||||
// the reading text is what the user is typing
|
|
||||||
NSString *head = [composingBuffer substringToIndex:composedStringCursorIndex];
|
|
||||||
NSString *reading = [self getCompositionFromPhoneticReadingBuffer];
|
|
||||||
NSString *tail = [composingBuffer substringFromIndex:composedStringCursorIndex];
|
|
||||||
NSString *composedText = [head stringByAppendingString:[reading stringByAppendingString:tail]];
|
|
||||||
NSInteger cursorIndex = composedStringCursorIndex + [reading length];
|
|
||||||
|
|
||||||
packagedComposedText = composedText;
|
|
||||||
packagedCursorIndex = cursorIndex;
|
|
||||||
packagedResultOfRear = resultOfRear;
|
|
||||||
packagedResultOfFront = resultOfFront;
|
|
||||||
}
|
|
||||||
|
|
||||||
// NON-SWIFTIFIABLE DUE TO VARIABLE AVAILABLE ACCESSIBILITY RANGE.
|
|
||||||
- (NSString *)getStrLocationResult:(BOOL)isFront
|
|
||||||
{
|
|
||||||
if (isFront)
|
|
||||||
return packagedResultOfFront;
|
|
||||||
else
|
|
||||||
return packagedResultOfRear;
|
|
||||||
}
|
|
||||||
|
|
||||||
// NON-SWIFTIFIABLE DUE TO VARIABLE AVAILABLE ACCESSIBILITY RANGE.
|
|
||||||
- (NSString *)getComposedText
|
|
||||||
{
|
|
||||||
return packagedComposedText;
|
|
||||||
}
|
|
||||||
|
|
||||||
// NON-SWIFTIFIABLE DUE TO VARIABLE AVAILABLE ACCESSIBILITY RANGE.
|
|
||||||
- (NSInteger)getPackagedCursorIndex
|
|
||||||
{
|
|
||||||
return packagedCursorIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
// NON-SWIFTIFIABLE
|
|
||||||
- (void)_walk
|
|
||||||
{
|
|
||||||
// retrieve the most likely trellis, i.e. a Maximum Likelihood Estimation
|
|
||||||
// of the best possible Mandarin characters given the input syllables,
|
|
||||||
// using the Viterbi algorithm implemented in the Gramambular library
|
|
||||||
Gramambular::Walker walker(&_builder->grid());
|
|
||||||
|
|
||||||
// the reverse walk traces the trellis from the end
|
|
||||||
_walkedNodes = walker.reverseWalk(_builder->grid().width());
|
|
||||||
|
|
||||||
// then we reverse the nodes so that we get the forward-walked nodes
|
|
||||||
reverse(_walkedNodes.begin(), _walkedNodes.end());
|
|
||||||
|
|
||||||
// if DEBUG is defined, a GraphViz file is written to kGraphVizOutputfile
|
|
||||||
#if DEBUG
|
|
||||||
std::string dotDump = _builder->grid().dumpDOT();
|
|
||||||
NSString *dotStr = [NSString stringWithUTF8String:dotDump.c_str()];
|
|
||||||
NSError *error = nil;
|
|
||||||
|
|
||||||
BOOL __unused success = [dotStr writeToFile:kGraphVizOutputfile
|
|
||||||
atomically:YES
|
|
||||||
encoding:NSUTF8StringEncoding
|
|
||||||
error:&error];
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
// NON-SWIFTIFIABLE
|
|
||||||
- (NSString *)_popOverflowComposingTextAndWalk
|
|
||||||
{
|
|
||||||
// in an ideal world, we can as well let the user type forever,
|
|
||||||
// but because the Viterbi algorithm has a complexity of O(N^2),
|
|
||||||
// the walk will become slower as the number of nodes increase,
|
|
||||||
// therefore we need to auto-commit overflown texts which usually
|
|
||||||
// lose their influence over the whole MLE anyway -- so that when
|
|
||||||
// the user type along, the already composed text in the rear side
|
|
||||||
// of the buffer will be committed (i.e. "popped out").
|
|
||||||
|
|
||||||
NSString *poppedText = @"";
|
|
||||||
NSInteger composingBufferSize = mgrPrefs.composingBufferSize;
|
|
||||||
|
|
||||||
if (_builder->grid().width() > (size_t)composingBufferSize)
|
|
||||||
{
|
|
||||||
if (_walkedNodes.size() > 0)
|
|
||||||
{
|
|
||||||
Gramambular::NodeAnchor &anchor = _walkedNodes[0];
|
|
||||||
poppedText = [NSString stringWithUTF8String:anchor.node->currentKeyValue().value.c_str()];
|
|
||||||
_builder->removeHeadReadings(anchor.spanningLength);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[self _walk];
|
|
||||||
return poppedText;
|
|
||||||
}
|
|
||||||
|
|
||||||
// NON-SWIFTIFIABLE
|
|
||||||
- (NSArray<NSString *> *)_currentReadings
|
|
||||||
{
|
|
||||||
NSMutableArray<NSString *> *readingsArray = [[NSMutableArray alloc] init];
|
|
||||||
std::vector<std::string> v = _builder->readings();
|
|
||||||
for (std::vector<std::string>::iterator it_i = v.begin(); it_i != v.end(); ++it_i)
|
|
||||||
[readingsArray addObject:[NSString stringWithUTF8String:it_i->c_str()]];
|
|
||||||
return readingsArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
// NON-SWIFTIFIABLE
|
|
||||||
- (NSArray<NSString *> *)buildAssociatePhraseArrayWithKey:(NSString *)key
|
|
||||||
{
|
|
||||||
NSMutableArray<NSString *> *array = [NSMutableArray array];
|
|
||||||
std::string cppKey = std::string(key.UTF8String);
|
|
||||||
if (_languageModel->hasAssociatedPhrasesForKey(cppKey))
|
|
||||||
{
|
|
||||||
std::vector<std::string> phrases = _languageModel->associatedPhrasesForKey(cppKey);
|
|
||||||
for (auto phrase : phrases)
|
|
||||||
{
|
|
||||||
NSString *item = [[NSString alloc] initWithUTF8String:phrase.c_str()];
|
|
||||||
[array addObject:item];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - 必須用 ObjCpp 處理的部分: Mandarin
|
#pragma mark - 必須用 ObjCpp 處理的部分: Mandarin
|
||||||
|
|
||||||
|
@ -504,129 +145,6 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - 必須用 ObjCpp 處理的部分: Gramambular 等
|
|
||||||
|
|
||||||
- (void)removeBuilderAndReset:(BOOL)shouldReset
|
|
||||||
{
|
|
||||||
if (_builder)
|
|
||||||
{
|
|
||||||
delete _builder;
|
|
||||||
if (shouldReset)
|
|
||||||
[self createNewBuilder];
|
|
||||||
}
|
|
||||||
else if (shouldReset)
|
|
||||||
[self createNewBuilder];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)createNewBuilder
|
|
||||||
{
|
|
||||||
_builder = new Gramambular::BlockReadingBuilder(_languageModel);
|
|
||||||
// Each Mandarin syllable is separated by a hyphen.
|
|
||||||
_builder->setJoinSeparator("-");
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)setInputModesToLM:(BOOL)isCHS
|
|
||||||
{
|
|
||||||
_languageModel = isCHS ? [mgrLangModel lmCHS] : [mgrLangModel lmCHT];
|
|
||||||
_userOverrideModel = isCHS ? [mgrLangModel userOverrideModelCHS] : [mgrLangModel userOverrideModelCHT];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)syncBaseLMPrefs
|
|
||||||
{
|
|
||||||
if (_languageModel)
|
|
||||||
{
|
|
||||||
_languageModel->setPhraseReplacementEnabled(mgrPrefs.phraseReplacementEnabled);
|
|
||||||
_languageModel->setSymbolEnabled(mgrPrefs.symbolInputEnabled);
|
|
||||||
_languageModel->setCNSEnabled(mgrPrefs.cns11643Enabled);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----
|
|
||||||
|
|
||||||
- (BOOL)ifLangModelHasUnigramsForKey:(NSString *)reading
|
|
||||||
{
|
|
||||||
return _languageModel->hasUnigramsForKey((std::string)[reading UTF8String]);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)insertReadingToBuilderAtCursor:(NSString *)reading
|
|
||||||
{
|
|
||||||
_builder->insertReadingAtCursor((std::string)[reading UTF8String]);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)dealWithOverrideModelSuggestions
|
|
||||||
{
|
|
||||||
// 這一整段都太 C++ 且只出現一次,就整個端過來了。
|
|
||||||
// 拆開封裝的話,只會把問題搞得更麻煩而已。
|
|
||||||
std::string overrideValue = (mgrPrefs.useSCPCTypingMode)
|
|
||||||
? ""
|
|
||||||
: _userOverrideModel->suggest(_walkedNodes, [self getBuilderCursorIndex],
|
|
||||||
[[NSDate date] timeIntervalSince1970]);
|
|
||||||
|
|
||||||
if (!overrideValue.empty())
|
|
||||||
{
|
|
||||||
NSInteger cursorIndex = [self getActualCandidateCursorIndex];
|
|
||||||
std::vector<Gramambular::NodeAnchor> nodes = mgrPrefs.setRearCursorMode
|
|
||||||
? _builder->grid().nodesCrossingOrEndingAt(cursorIndex)
|
|
||||||
: _builder->grid().nodesEndingAt(cursorIndex);
|
|
||||||
double highestScore = FindHighestScore(nodes, kEpsilon);
|
|
||||||
_builder->grid().overrideNodeScoreForSelectedCandidate(cursorIndex, overrideValue,
|
|
||||||
static_cast<float>(highestScore));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)setBuilderCursorIndex:(NSInteger)value
|
|
||||||
{
|
|
||||||
_builder->setCursorIndex(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSInteger)getBuilderCursorIndex
|
|
||||||
{
|
|
||||||
return _builder->cursorIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSInteger)getBuilderLength
|
|
||||||
{
|
|
||||||
return _builder->length();
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)deleteBuilderReadingInFrontOfCursor
|
|
||||||
{
|
|
||||||
_builder->deleteReadingBeforeCursor();
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)deleteBuilderReadingAfterCursor
|
|
||||||
{
|
|
||||||
_builder->deleteReadingAfterCursor();
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSArray<NSString *> *)getCandidatesArray
|
|
||||||
{
|
|
||||||
NSMutableArray<NSString *> *candidatesArray = [[NSMutableArray alloc] init];
|
|
||||||
|
|
||||||
NSInteger cursorIndex = [self getActualCandidateCursorIndex];
|
|
||||||
std::vector<Gramambular::NodeAnchor> nodes = mgrPrefs.setRearCursorMode
|
|
||||||
? _builder->grid().nodesCrossingOrEndingAt(cursorIndex)
|
|
||||||
: _builder->grid().nodesEndingAt(cursorIndex);
|
|
||||||
|
|
||||||
// sort the nodes, so that longer nodes (representing longer phrases) are placed at the top of the candidate list
|
|
||||||
stable_sort(nodes.begin(), nodes.end(), NodeAnchorDescendingSorter());
|
|
||||||
|
|
||||||
// then use the C++ trick to retrieve the candidates for each node at/crossing the cursor
|
|
||||||
for (std::vector<Gramambular::NodeAnchor>::iterator ni = nodes.begin(), ne = nodes.end(); ni != ne; ++ni)
|
|
||||||
{
|
|
||||||
const std::vector<Gramambular::KeyValuePair> &candidates = (*ni).node->candidates();
|
|
||||||
for (std::vector<Gramambular::KeyValuePair>::const_iterator ci = candidates.begin(), ce = candidates.end();
|
|
||||||
ci != ce; ++ci)
|
|
||||||
[candidatesArray addObject:[NSString stringWithUTF8String:(*ci).value.c_str()]];
|
|
||||||
}
|
|
||||||
return candidatesArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSInteger)getKeyLengthAtIndexZero
|
|
||||||
{
|
|
||||||
return [NSString stringWithUTF8String:_walkedNodes[0].node->currentKeyValue().value.c_str()].length;
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - 威注音認為有必要單獨拿出來處理的部分,交給 Swift 則有些困難。
|
#pragma mark - 威注音認為有必要單獨拿出來處理的部分,交給 Swift 則有些困難。
|
||||||
|
|
||||||
- (BOOL)isPrintable:(UniChar)charCode
|
- (BOOL)isPrintable:(UniChar)charCode
|
||||||
|
|
|
@ -29,6 +29,10 @@ import Megrez
|
||||||
|
|
||||||
// MARK: - KeyHandler Sputnik.
|
// MARK: - KeyHandler Sputnik.
|
||||||
|
|
||||||
|
// Swift Extension 不允許直接存放這些變數,所以就寫了這個衛星型別。
|
||||||
|
// 一旦 Mandarin 模組被 Swift 化,整個 KeyHandler 就可以都用 Swift。
|
||||||
|
// 屆時會考慮將該衛星型別內的變數與常數都挪回 KeyHandler_Kernel 內。
|
||||||
|
|
||||||
class KeyHandlerSputnik: NSObject {
|
class KeyHandlerSputnik: NSObject {
|
||||||
static let kEpsilon: Double = 0.000001
|
static let kEpsilon: Double = 0.000001
|
||||||
static var inputMode: String = ""
|
static var inputMode: String = ""
|
||||||
|
|
|
@ -160,17 +160,17 @@ import Cocoa
|
||||||
let reading = getSyllableCompositionFromPhoneticReadingBuffer()
|
let reading = getSyllableCompositionFromPhoneticReadingBuffer()
|
||||||
|
|
||||||
if !ifLangModelHasUnigrams(forKey: reading) {
|
if !ifLangModelHasUnigrams(forKey: reading) {
|
||||||
IME.prtDebugIntel("B49C0979")
|
IME.prtDebugIntel("B49C0979:語彙庫內無「\(reading)」的匹配記錄。")
|
||||||
errorCallback()
|
errorCallback()
|
||||||
stateCallback(buildInputtingState())
|
stateCallback(buildInputtingState())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... and insert it into the lattice grid...
|
// ... and insert it into the lattice grid...
|
||||||
insertReadingToBuilder(atCursor: reading)
|
insertReadingToBuilderAtCursor(reading: reading)
|
||||||
|
|
||||||
// ... then walk the lattice grid...
|
// ... then walk the lattice grid...
|
||||||
let poppedText = _popOverflowComposingTextAndWalk()
|
let poppedText = popOverflowComposingTextAndWalk()
|
||||||
|
|
||||||
// ... get and tweak override model suggestion if possible...
|
// ... get and tweak override model suggestion if possible...
|
||||||
dealWithOverrideModelSuggestions()
|
dealWithOverrideModelSuggestions()
|
||||||
|
@ -233,8 +233,8 @@ import Cocoa
|
||||||
stateCallback(InputState.Committing(poppedText: " "))
|
stateCallback(InputState.Committing(poppedText: " "))
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(InputState.Empty())
|
||||||
} else if ifLangModelHasUnigrams(forKey: " ") {
|
} else if ifLangModelHasUnigrams(forKey: " ") {
|
||||||
insertReadingToBuilder(atCursor: " ")
|
insertReadingToBuilderAtCursor(reading: " ")
|
||||||
let poppedText = _popOverflowComposingTextAndWalk()
|
let poppedText = popOverflowComposingTextAndWalk()
|
||||||
let inputting = buildInputtingState()
|
let inputting = buildInputtingState()
|
||||||
inputting.poppedText = poppedText
|
inputting.poppedText = poppedText
|
||||||
stateCallback(inputting)
|
stateCallback(inputting)
|
||||||
|
@ -330,8 +330,8 @@ import Cocoa
|
||||||
if !input.isOptionHold {
|
if !input.isOptionHold {
|
||||||
if ifLangModelHasUnigrams(forKey: "_punctuation_list") {
|
if ifLangModelHasUnigrams(forKey: "_punctuation_list") {
|
||||||
if isPhoneticReadingBufferEmpty() {
|
if isPhoneticReadingBufferEmpty() {
|
||||||
insertReadingToBuilder(atCursor: "_punctuation_list")
|
insertReadingToBuilderAtCursor(reading: "_punctuation_list")
|
||||||
let poppedText: String! = _popOverflowComposingTextAndWalk()
|
let poppedText: String! = popOverflowComposingTextAndWalk()
|
||||||
let inputting = buildInputtingState()
|
let inputting = buildInputtingState()
|
||||||
inputting.poppedText = poppedText
|
inputting.poppedText = poppedText
|
||||||
stateCallback(inputting)
|
stateCallback(inputting)
|
||||||
|
@ -354,7 +354,7 @@ import Cocoa
|
||||||
|
|
||||||
// MARK: Punctuation
|
// MARK: Punctuation
|
||||||
|
|
||||||
// if nothing is matched, see if it's a punctuation key for current layout.
|
// If nothing is matched, see if it's a punctuation key for current layout.
|
||||||
|
|
||||||
var punctuationNamePrefix = ""
|
var punctuationNamePrefix = ""
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,286 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
import Megrez
|
||||||
|
|
||||||
@objc extension KeyHandler {
|
public enum InputMode: String {
|
||||||
|
case imeModeCHS = "org.atelierInmu.inputmethod.vChewing.IMECHS"
|
||||||
|
case imeModeCHT = "org.atelierInmu.inputmethod.vChewing.IMECHT"
|
||||||
|
case imeModeNULL = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Delegate.
|
||||||
|
|
||||||
|
// MARK: - Kernel.
|
||||||
|
|
||||||
|
extension KeyHandler {
|
||||||
|
var kEpsilon: Double {
|
||||||
|
KeyHandlerSputnik.kEpsilon
|
||||||
|
}
|
||||||
|
|
||||||
|
var inputMode: InputMode {
|
||||||
|
get {
|
||||||
|
switch KeyHandlerSputnik.inputMode {
|
||||||
|
case "org.atelierInmu.inputmethod.vChewing.IMECHS":
|
||||||
|
return InputMode.imeModeCHS
|
||||||
|
case "org.atelierInmu.inputmethod.vChewing.IMECHT":
|
||||||
|
return InputMode.imeModeCHT
|
||||||
|
default:
|
||||||
|
return InputMode.imeModeNULL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set { setInputMode(newValue.rawValue) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Will reenable this once Mandarin gets Swiftified.
|
||||||
|
// override public init() {
|
||||||
|
// self.ensurePhoneticParser()
|
||||||
|
// self.setInputMode(ctlInputMethod.currentInputMode)
|
||||||
|
// super.init()
|
||||||
|
// }
|
||||||
|
|
||||||
|
func clear() {
|
||||||
|
clearPhoneticReadingBuffer()
|
||||||
|
KeyHandlerSputnik.builder.clear()
|
||||||
|
KeyHandlerSputnik.walkedNodes.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 這個函數得獨立出來給 ObjC 使用。
|
||||||
|
@objc func setInputMode(_ value: String) {
|
||||||
|
// 下面這句的「isKindOfClass」是做類型檢查,
|
||||||
|
// 為了應對出現輸入法 plist 被改壞掉這樣的極端情況。
|
||||||
|
let isCHS: Bool = (value == InputMode.imeModeCHS.rawValue)
|
||||||
|
|
||||||
|
// 緊接著將新的簡繁輸入模式提報給 ctlInputMethod:
|
||||||
|
ctlInputMethod.currentInputMode = isCHS ? InputMode.imeModeCHS.rawValue : InputMode.imeModeCHT.rawValue
|
||||||
|
mgrPrefs.mostRecentInputMode = ctlInputMethod.currentInputMode
|
||||||
|
|
||||||
|
// 拿當前的 _inputMode 與 ctlInputMethod 的提報結果對比,不同的話則套用新設定:
|
||||||
|
if KeyHandlerSputnik.inputMode != ctlInputMethod.currentInputMode {
|
||||||
|
// Reinitiate language models if necessary
|
||||||
|
setInputModesToLM(isCHS: isCHS)
|
||||||
|
|
||||||
|
// Synchronize the sub-languageModel state settings to the new LM.
|
||||||
|
syncBaseLMPrefs()
|
||||||
|
|
||||||
|
// Create new grid builder.
|
||||||
|
createNewBuilder()
|
||||||
|
|
||||||
|
if !isPhoneticReadingBufferEmpty() {
|
||||||
|
clearPhoneticReadingBuffer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 直接寫到衛星模組內,省得類型轉換
|
||||||
|
KeyHandlerSputnik.inputMode = ctlInputMethod.currentInputMode
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Functions dealing with Megrez.
|
||||||
|
|
||||||
|
func walk() {
|
||||||
|
// Retrieve the most likely trellis, i.e. a Maximum Likelihood Estimation
|
||||||
|
// of the best possible Mandarin characters given the input syllables,
|
||||||
|
// using the Viterbi algorithm implemented in the Gramambular library
|
||||||
|
let walker = Megrez.Walker(grid: KeyHandlerSputnik.builder.grid())
|
||||||
|
|
||||||
|
// the reverse walk traces the trellis from the end
|
||||||
|
let walked: [Megrez.NodeAnchor] = walker.reverseWalk(at: KeyHandlerSputnik.builder.grid().width())
|
||||||
|
|
||||||
|
// then we use ".reversed()" to reverse the nodes so that we get the forward-walked nodes
|
||||||
|
KeyHandlerSputnik.walkedNodes.removeAll()
|
||||||
|
KeyHandlerSputnik.walkedNodes.append(contentsOf: walked.reversed())
|
||||||
|
}
|
||||||
|
|
||||||
|
func popOverflowComposingTextAndWalk() -> String {
|
||||||
|
// In ideal situations we can allow users to type infinitely in a buffer.
|
||||||
|
// However, Viberti algorithm has a complexity of O(N^2), the walk will
|
||||||
|
// become slower as the number of nodes increase. Therefore, we need to
|
||||||
|
// auto-commit overflown texts which usually lose their influence over
|
||||||
|
// the whole MLE anyway -- so that when the user type along, the already
|
||||||
|
// composed text in the rear side of the buffer will be committed out.
|
||||||
|
// (i.e. popped out.)
|
||||||
|
|
||||||
|
var poppedText = ""
|
||||||
|
if KeyHandlerSputnik.builder.grid().width() > mgrPrefs.composingBufferSize {
|
||||||
|
if KeyHandlerSputnik.walkedNodes.count > 0 {
|
||||||
|
let anchor: Megrez.NodeAnchor = KeyHandlerSputnik.walkedNodes[0]
|
||||||
|
if let theNode = anchor.node {
|
||||||
|
poppedText = theNode.currentKeyValue().value
|
||||||
|
}
|
||||||
|
KeyHandlerSputnik.builder.removeHeadReadings(count: anchor.spanningLength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk()
|
||||||
|
return poppedText
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAssociatePhraseArray(withKey key: String) -> [String] {
|
||||||
|
var arrResult: [String] = []
|
||||||
|
if KeyHandlerSputnik.languageModel.hasAssociatedPhrasesForKey(key) {
|
||||||
|
arrResult.append(contentsOf: KeyHandlerSputnik.languageModel.associatedPhrasesForKey(key))
|
||||||
|
}
|
||||||
|
return arrResult
|
||||||
|
}
|
||||||
|
|
||||||
|
func fixNode(value: String) {
|
||||||
|
let cursorIndex: Int = getActualCandidateCursorIndex()
|
||||||
|
let selectedNode: Megrez.NodeAnchor = KeyHandlerSputnik.builder.grid().fixNodeSelectedCandidate(
|
||||||
|
location: cursorIndex, value: value
|
||||||
|
)
|
||||||
|
// 不要針對逐字選字模式啟用臨時半衰記憶模型。
|
||||||
|
if !mgrPrefs.useSCPCTypingMode {
|
||||||
|
// If the length of the readings and the characters do not match,
|
||||||
|
// it often means it is a special symbol and it should not be stored
|
||||||
|
// in the user override model.
|
||||||
|
var addToUserOverrideModel = true
|
||||||
|
if selectedNode.spanningLength != value.count {
|
||||||
|
addToUserOverrideModel = false
|
||||||
|
}
|
||||||
|
if addToUserOverrideModel {
|
||||||
|
if let theNode = selectedNode.node {
|
||||||
|
// 威注音的 SymbolLM 的 Score 是 -12。
|
||||||
|
if theNode.scoreFor(candidate: value) <= -12 {
|
||||||
|
addToUserOverrideModel = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if addToUserOverrideModel {
|
||||||
|
KeyHandlerSputnik.userOverrideModel.observe(
|
||||||
|
walkedNodes: KeyHandlerSputnik.walkedNodes, cursorIndex: cursorIndex, candidate: value,
|
||||||
|
timestamp: NSDate().timeIntervalSince1970
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk()
|
||||||
|
|
||||||
|
if mgrPrefs.moveCursorAfterSelectingCandidate {
|
||||||
|
var nextPosition = 0
|
||||||
|
for node in KeyHandlerSputnik.walkedNodes {
|
||||||
|
if nextPosition >= cursorIndex { break }
|
||||||
|
nextPosition += node.spanningLength
|
||||||
|
}
|
||||||
|
if nextPosition <= getBuilderLength() {
|
||||||
|
setBuilderCursorIndex(value: nextPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCandidatesArray() -> [String] {
|
||||||
|
var arrCandidates: [String] = []
|
||||||
|
var arrNodes: [Megrez.NodeAnchor] = []
|
||||||
|
arrNodes.append(contentsOf: getRawNodes())
|
||||||
|
|
||||||
|
/// 原理:nodes 這個回饋結果包含一堆子陣列,分別對應不同詞長的候選字。
|
||||||
|
/// 這裡先對陣列排序、讓最長候選字的子陣列的優先權最高。
|
||||||
|
/// 這個過程不會傷到子陣列內部的排序。
|
||||||
|
if !arrNodes.isEmpty {
|
||||||
|
// sort the nodes, so that longer nodes (representing longer phrases)
|
||||||
|
// are placed at the top of the candidate list
|
||||||
|
arrNodes.sort { $0.keyLength > $1.keyLength }
|
||||||
|
|
||||||
|
// then use the Swift trick to retrieve the candidates for each node at/crossing the cursor
|
||||||
|
for currentNodeAnchor in arrNodes {
|
||||||
|
if let currentNode = currentNodeAnchor.node {
|
||||||
|
for currentCandidate in currentNode.candidates() {
|
||||||
|
arrCandidates.append(currentCandidate.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return arrCandidates
|
||||||
|
}
|
||||||
|
|
||||||
|
func dealWithOverrideModelSuggestions() {
|
||||||
|
var overrideValue =
|
||||||
|
mgrPrefs.useSCPCTypingMode
|
||||||
|
? ""
|
||||||
|
: KeyHandlerSputnik.userOverrideModel.suggest(
|
||||||
|
walkedNodes: KeyHandlerSputnik.walkedNodes, cursorIndex: getBuilderCursorIndex(),
|
||||||
|
timestamp: NSDate().timeIntervalSince1970
|
||||||
|
)
|
||||||
|
|
||||||
|
if !overrideValue.isEmpty {
|
||||||
|
KeyHandlerSputnik.builder.grid().overrideNodeScoreForSelectedCandidate(
|
||||||
|
location: getActualCandidateCursorIndex(),
|
||||||
|
value: &overrideValue,
|
||||||
|
overridingScore: findHighestScore(nodes: getRawNodes(), epsilon: kEpsilon)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func findHighestScore(nodes: [Megrez.NodeAnchor], epsilon: Double) -> Double {
|
||||||
|
var highestScore: Double = 0
|
||||||
|
for currentAnchor in nodes {
|
||||||
|
if let theNode = currentAnchor.node {
|
||||||
|
let score = theNode.highestUnigramScore()
|
||||||
|
if score > highestScore {
|
||||||
|
highestScore = score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return highestScore + epsilon
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Extracted methods and functions.
|
||||||
|
|
||||||
|
func isBuilderEmpty() -> Bool { KeyHandlerSputnik.builder.grid().width() == 0 }
|
||||||
|
|
||||||
|
func getRawNodes() -> [Megrez.NodeAnchor] {
|
||||||
|
/// 警告:不要對游標前置風格使用 nodesCrossing,否則會導致游標行為與 macOS 內建注音輸入法不一致。
|
||||||
|
/// 微軟新注音輸入法的游標後置風格也是不允許 nodeCrossing 的,但目前 Megrez 暫時缺乏對該特性的支援。
|
||||||
|
/// 所以暫時只能將威注音的游標後置風格描述成「跟 Windows 版雅虎奇摩注音一致」。
|
||||||
|
mgrPrefs.setRearCursorMode
|
||||||
|
? KeyHandlerSputnik.builder.grid().nodesCrossingOrEndingAt(location: getActualCandidateCursorIndex())
|
||||||
|
: KeyHandlerSputnik.builder.grid().nodesEndingAt(location: getActualCandidateCursorIndex())
|
||||||
|
}
|
||||||
|
|
||||||
|
func setInputModesToLM(isCHS: Bool) {
|
||||||
|
KeyHandlerSputnik.languageModel = isCHS ? mgrLangModel.lmCHS : mgrLangModel.lmCHT
|
||||||
|
KeyHandlerSputnik.userOverrideModel = isCHS ? mgrLangModel.uomCHS : mgrLangModel.uomCHT
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncBaseLMPrefs() {
|
||||||
|
KeyHandlerSputnik.languageModel.isPhraseReplacementEnabled = mgrPrefs.phraseReplacementEnabled
|
||||||
|
KeyHandlerSputnik.languageModel.isCNSEnabled = mgrPrefs.cns11643Enabled
|
||||||
|
KeyHandlerSputnik.languageModel.isSymbolEnabled = mgrPrefs.symbolInputEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNewBuilder() {
|
||||||
|
KeyHandlerSputnik.builder = Megrez.BlockReadingBuilder(lm: KeyHandlerSputnik.languageModel)
|
||||||
|
// Each Mandarin syllable is separated by a hyphen.
|
||||||
|
KeyHandlerSputnik.builder.setJoinSeparator(separator: "-")
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentReadings() -> [String] { KeyHandlerSputnik.builder.readings() }
|
||||||
|
|
||||||
|
func ifLangModelHasUnigrams(forKey reading: String) -> Bool {
|
||||||
|
KeyHandlerSputnik.languageModel.hasUnigramsFor(key: reading)
|
||||||
|
}
|
||||||
|
|
||||||
|
func insertReadingToBuilderAtCursor(reading: String) {
|
||||||
|
KeyHandlerSputnik.builder.insertReadingAtCursor(reading: reading)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setBuilderCursorIndex(value: Int) {
|
||||||
|
KeyHandlerSputnik.builder.setCursorIndex(newIndex: value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBuilderCursorIndex() -> Int {
|
||||||
|
KeyHandlerSputnik.builder.cursorIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBuilderLength() -> Int {
|
||||||
|
KeyHandlerSputnik.builder.length()
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteBuilderReadingInFrontOfCursor() {
|
||||||
|
KeyHandlerSputnik.builder.deleteReadingBeforeCursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteBuilderReadingAfterCursor() {
|
||||||
|
KeyHandlerSputnik.builder.deleteReadingAfterCursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKeyLengthAtIndexZero() -> Int {
|
||||||
|
KeyHandlerSputnik.walkedNodes[0].node?.currentKeyValue().value.count ?? 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ import Cocoa
|
||||||
&& (cursorIndex < getBuilderLength()))
|
&& (cursorIndex < getBuilderLength()))
|
||||||
|| cursorIndex == 0
|
|| cursorIndex == 0
|
||||||
{
|
{
|
||||||
if cursorIndex == 0 && !mgrPrefs.setRearCursorMode {
|
if cursorIndex == 0, !mgrPrefs.setRearCursorMode {
|
||||||
cursorIndex += getKeyLengthAtIndexZero()
|
cursorIndex += getKeyLengthAtIndexZero()
|
||||||
} else {
|
} else {
|
||||||
cursorIndex += 1
|
cursorIndex += 1
|
||||||
|
|
|
@ -25,6 +25,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
import Megrez
|
||||||
|
|
||||||
// MARK: - § State managements.
|
// MARK: - § State managements.
|
||||||
|
|
||||||
|
@ -32,45 +33,67 @@ import Cocoa
|
||||||
// MARK: - 構築狀態(State Building)
|
// MARK: - 構築狀態(State Building)
|
||||||
|
|
||||||
func buildInputtingState() -> InputState.Inputting {
|
func buildInputtingState() -> InputState.Inputting {
|
||||||
// 觸發資料封裝更新,否則下文拿到的資料會是過期的。
|
// "Updating the composing buffer" means to request the client
|
||||||
packageBufferStateMaterials()
|
// to "refresh" the text input buffer with our "composing text"
|
||||||
// 獲取封裝好的資料
|
var composingBuffer = ""
|
||||||
let composedText = getComposedText()
|
var composedStringCursorIndex = 0
|
||||||
let packagedCursorIndex = UInt(getPackagedCursorIndex())
|
|
||||||
let resultOfRear = getStrLocationResult(isFront: false)
|
|
||||||
let resultOfFront = getStrLocationResult(isFront: true)
|
|
||||||
|
|
||||||
// 初期化狀態
|
var readingCursorIndex: size_t = 0
|
||||||
let newState = InputState.Inputting(composingBuffer: composedText, cursorIndex: packagedCursorIndex)
|
let builderCursorIndex: size_t = getBuilderCursorIndex()
|
||||||
|
|
||||||
// 組建提示文本
|
// We must do some Unicode codepoint counting to find the actual cursor location for the client
|
||||||
var tooltip = ""
|
// i.e. we need to take UTF-16 into consideration, for which a surrogate pair takes 2 UniChars
|
||||||
|
// locations. These processes are inherited from the ObjC++ version of this class and might be
|
||||||
|
// unnecessary in Swift, but this deduction requires further experiments.
|
||||||
|
for walkedNode in KeyHandlerSputnik.walkedNodes {
|
||||||
|
if let theNode = walkedNode.node {
|
||||||
|
let strNodeValue = theNode.currentKeyValue().value
|
||||||
|
composingBuffer += strNodeValue
|
||||||
|
|
||||||
// 如果在用特定的模式的話,則始終顯示對應的提示。
|
let arrSplit: [NSString] = (strNodeValue as NSString).split()
|
||||||
// TODO: 該功能無法正常運作,暫時註釋掉。
|
let codepointCount = arrSplit.count
|
||||||
// if ctlInputMethod.currentKeyHandler.inputMode == InputMode.imeModeCHT {
|
|
||||||
// if mgrPrefs.chineseConversionEnabled && !mgrPrefs.shiftJISShinjitaiOutputEnabled {
|
|
||||||
// tooltip = String(
|
|
||||||
// format: "%@%@%@", NSLocalizedString("Force KangXi Writing", comment: ""), "\n",
|
|
||||||
// NSLocalizedString("NotificationSwitchON", comment: ""))
|
|
||||||
// } else if mgrPrefs.shiftJISShinjitaiOutputEnabled {
|
|
||||||
// tooltip = String(
|
|
||||||
// format: "%@%@%@", NSLocalizedString("JIS Shinjitai Output", comment: ""), "\n",
|
|
||||||
// NSLocalizedString("NotificationSwitchON", comment: ""))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 備註:因為目前的輸入法已經有了 NSString Emoji 支援,所以這個工具提示可能不會出現了。
|
// This re-aligns the cursor index in the composed string
|
||||||
// 姑且留下來用作萬一時的偵錯用途。
|
// (the actual cursor on the screen) with the builder's logical
|
||||||
if resultOfRear != "" || resultOfFront != "" {
|
// cursor (reading) cursor; each built node has a "spanning length"
|
||||||
tooltip = String(
|
// (e.g. two reading blocks has a spanning length of 2), and we
|
||||||
format: NSLocalizedString("Cursor is between \"%@\" and \"%@\".", comment: ""),
|
// accumulate those lengths to calculate the displayed cursor
|
||||||
resultOfFront, resultOfRear
|
// index.
|
||||||
)
|
let spanningLength: Int = walkedNode.spanningLength
|
||||||
|
if readingCursorIndex + spanningLength <= builderCursorIndex {
|
||||||
|
composedStringCursorIndex += (strNodeValue as NSString).length
|
||||||
|
readingCursorIndex += spanningLength
|
||||||
|
} else {
|
||||||
|
if codepointCount == spanningLength {
|
||||||
|
var i = 0
|
||||||
|
while i < codepointCount, readingCursorIndex < builderCursorIndex {
|
||||||
|
composedStringCursorIndex += arrSplit[i].length
|
||||||
|
readingCursorIndex += 1
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if readingCursorIndex < builderCursorIndex {
|
||||||
|
composedStringCursorIndex += (strNodeValue as NSString).length
|
||||||
|
readingCursorIndex += spanningLength
|
||||||
|
if readingCursorIndex > builderCursorIndex {
|
||||||
|
readingCursorIndex = builderCursorIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Now, we gather all the intel, separate the composing buffer to two parts (head and tail),
|
||||||
|
// and insert the reading text (the Mandarin syllable) in between them.
|
||||||
|
// The reading text is what the user is typing.
|
||||||
|
|
||||||
newState.tooltip = tooltip
|
let head = String((composingBuffer as NSString).substring(to: composedStringCursorIndex))
|
||||||
return newState
|
let reading = getCompositionFromPhoneticReadingBuffer()
|
||||||
|
let tail = String((composingBuffer as NSString).substring(from: composedStringCursorIndex))
|
||||||
|
let composedText = head + reading + tail
|
||||||
|
let cursorIndex = composedStringCursorIndex + reading.count
|
||||||
|
|
||||||
|
return InputState.Inputting(composingBuffer: composedText, cursorIndex: UInt(cursorIndex))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 用以生成候選詞陣列及狀態
|
// MARK: - 用以生成候選詞陣列及狀態
|
||||||
|
@ -102,7 +125,8 @@ import Cocoa
|
||||||
) -> InputState.AssociatedPhrases! {
|
) -> InputState.AssociatedPhrases! {
|
||||||
// 上一行必須要用驚嘆號,否則 Xcode 會誤導你砍掉某些實際上必需的語句。
|
// 上一行必須要用驚嘆號,否則 Xcode 會誤導你砍掉某些實際上必需的語句。
|
||||||
InputState.AssociatedPhrases(
|
InputState.AssociatedPhrases(
|
||||||
candidates: buildAssociatePhraseArray(withKey: key), useVerticalMode: useVerticalMode)
|
candidates: buildAssociatePhraseArray(withKey: key), useVerticalMode: useVerticalMode
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 用以處理就地新增自訂語彙時的行為
|
// MARK: - 用以處理就地新增自訂語彙時的行為
|
||||||
|
@ -191,8 +215,8 @@ import Cocoa
|
||||||
}
|
}
|
||||||
|
|
||||||
if isPhoneticReadingBufferEmpty() {
|
if isPhoneticReadingBufferEmpty() {
|
||||||
insertReadingToBuilder(atCursor: customPunctuation)
|
insertReadingToBuilderAtCursor(reading: customPunctuation)
|
||||||
let poppedText = _popOverflowComposingTextAndWalk()
|
let poppedText = popOverflowComposingTextAndWalk()
|
||||||
let inputting = buildInputtingState()
|
let inputting = buildInputtingState()
|
||||||
inputting.poppedText = poppedText
|
inputting.poppedText = poppedText
|
||||||
stateCallback(inputting)
|
stateCallback(inputting)
|
||||||
|
@ -256,7 +280,7 @@ import Cocoa
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
let readings: [String] = _currentReadings()
|
let readings: [String] = currentReadings()
|
||||||
let composingBuffer =
|
let composingBuffer =
|
||||||
(IME.areWeUsingOurOwnPhraseEditor)
|
(IME.areWeUsingOurOwnPhraseEditor)
|
||||||
? readings.joined(separator: "-")
|
? readings.joined(separator: "-")
|
||||||
|
@ -283,7 +307,7 @@ import Cocoa
|
||||||
if isPhoneticReadingBufferEmpty() {
|
if isPhoneticReadingBufferEmpty() {
|
||||||
if getBuilderCursorIndex() >= 0 {
|
if getBuilderCursorIndex() >= 0 {
|
||||||
deleteBuilderReadingInFrontOfCursor()
|
deleteBuilderReadingInFrontOfCursor()
|
||||||
_walk()
|
walk()
|
||||||
} else {
|
} else {
|
||||||
IME.prtDebugIntel("9D69908D")
|
IME.prtDebugIntel("9D69908D")
|
||||||
errorCallback()
|
errorCallback()
|
||||||
|
@ -316,7 +340,7 @@ import Cocoa
|
||||||
if isPhoneticReadingBufferEmpty() {
|
if isPhoneticReadingBufferEmpty() {
|
||||||
if getBuilderCursorIndex() != getBuilderLength() {
|
if getBuilderCursorIndex() != getBuilderLength() {
|
||||||
deleteBuilderReadingAfterCursor()
|
deleteBuilderReadingAfterCursor()
|
||||||
_walk()
|
walk()
|
||||||
let inputting = buildInputtingState()
|
let inputting = buildInputtingState()
|
||||||
// 這裡不用「count > 0」,因為該整數變數只要「!isEmpty」那就必定滿足這個條件。
|
// 這裡不用「count > 0」,因為該整數變數只要「!isEmpty」那就必定滿足這個條件。
|
||||||
if !inputting.composingBuffer.isEmpty {
|
if !inputting.composingBuffer.isEmpty {
|
||||||
|
@ -375,7 +399,7 @@ import Cocoa
|
||||||
}
|
}
|
||||||
|
|
||||||
if getBuilderCursorIndex() != 0 {
|
if getBuilderCursorIndex() != 0 {
|
||||||
setBuilderCursorIndex(0)
|
setBuilderCursorIndex(value: 0)
|
||||||
stateCallback(buildInputtingState())
|
stateCallback(buildInputtingState())
|
||||||
} else {
|
} else {
|
||||||
IME.prtDebugIntel("66D97F90")
|
IME.prtDebugIntel("66D97F90")
|
||||||
|
@ -405,7 +429,7 @@ import Cocoa
|
||||||
}
|
}
|
||||||
|
|
||||||
if getBuilderCursorIndex() != getBuilderLength() {
|
if getBuilderCursorIndex() != getBuilderLength() {
|
||||||
setBuilderCursorIndex(getBuilderLength())
|
setBuilderCursorIndex(value: getBuilderLength())
|
||||||
stateCallback(buildInputtingState())
|
stateCallback(buildInputtingState())
|
||||||
} else {
|
} else {
|
||||||
IME.prtDebugIntel("9B69908E")
|
IME.prtDebugIntel("9B69908E")
|
||||||
|
@ -475,7 +499,7 @@ import Cocoa
|
||||||
composingBuffer: currentState.composingBuffer,
|
composingBuffer: currentState.composingBuffer,
|
||||||
cursorIndex: currentState.cursorIndex,
|
cursorIndex: currentState.cursorIndex,
|
||||||
markerIndex: UInt(nextPosition),
|
markerIndex: UInt(nextPosition),
|
||||||
readings: _currentReadings()
|
readings: currentReadings()
|
||||||
)
|
)
|
||||||
marking.tooltipForInputting = currentState.tooltip
|
marking.tooltipForInputting = currentState.tooltip
|
||||||
stateCallback(marking)
|
stateCallback(marking)
|
||||||
|
@ -486,7 +510,7 @@ import Cocoa
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if getBuilderCursorIndex() < getBuilderLength() {
|
if getBuilderCursorIndex() < getBuilderLength() {
|
||||||
setBuilderCursorIndex(getBuilderCursorIndex() + 1)
|
setBuilderCursorIndex(value: getBuilderCursorIndex() + 1)
|
||||||
stateCallback(buildInputtingState())
|
stateCallback(buildInputtingState())
|
||||||
} else {
|
} else {
|
||||||
IME.prtDebugIntel("A96AAD58")
|
IME.prtDebugIntel("A96AAD58")
|
||||||
|
@ -526,7 +550,7 @@ import Cocoa
|
||||||
composingBuffer: currentState.composingBuffer,
|
composingBuffer: currentState.composingBuffer,
|
||||||
cursorIndex: currentState.cursorIndex,
|
cursorIndex: currentState.cursorIndex,
|
||||||
markerIndex: UInt(previousPosition),
|
markerIndex: UInt(previousPosition),
|
||||||
readings: _currentReadings()
|
readings: currentReadings()
|
||||||
)
|
)
|
||||||
marking.tooltipForInputting = currentState.tooltip
|
marking.tooltipForInputting = currentState.tooltip
|
||||||
stateCallback(marking)
|
stateCallback(marking)
|
||||||
|
@ -537,7 +561,7 @@ import Cocoa
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if getBuilderCursorIndex() > 0 {
|
if getBuilderCursorIndex() > 0 {
|
||||||
setBuilderCursorIndex(getBuilderCursorIndex() - 1)
|
setBuilderCursorIndex(value: getBuilderCursorIndex() - 1)
|
||||||
stateCallback(buildInputtingState())
|
stateCallback(buildInputtingState())
|
||||||
} else {
|
} else {
|
||||||
IME.prtDebugIntel("7045E6F3")
|
IME.prtDebugIntel("7045E6F3")
|
||||||
|
|
Loading…
Reference in New Issue