diff --git a/Source/Base.lproj/preferences.xib b/Source/Base.lproj/preferences.xib index 1cb0c9b4..65098960 100644 --- a/Source/Base.lproj/preferences.xib +++ b/Source/Base.lproj/preferences.xib @@ -203,7 +203,7 @@ - + diff --git a/Source/Engine/Keyboard/EmacsKeyHelper.swift b/Source/Engine/Keyboard/EmacsKeyHelper.swift new file mode 100644 index 00000000..b5001f4d --- /dev/null +++ b/Source/Engine/Keyboard/EmacsKeyHelper.swift @@ -0,0 +1,53 @@ +// +// EmacsKeyHelper.swift +// +// Copyright (c) 2011-2022 The OpenVanilla Project. +// +// Contributors: +// Weizhong Yang (@zonble) @ OpenVanilla +// +// Based on the Syrup Project and the Formosana Library +// by Lukhnos Liu (@lukhnos). +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Cocoa + +@objc enum vChewingEmacsKey: UInt16 { + case none = 0 + case forward = 6 // F + case backward = 2 // B + case home = 1 // A + case end = 5 // E + case delete = 4 // D + case nextPage = 22 // V +} + +class EmacsKeyHelper: NSObject { + @objc static func detect(charCode: UniChar, flags: NSEvent.ModifierFlags) -> vChewingEmacsKey { + if flags.contains(.control) { + return vChewingEmacsKey(rawValue: charCode) ?? .none + } + return .none; + } +} diff --git a/Source/InputMethodController.h b/Source/InputMethodController.h index 4649650b..75714bb5 100644 --- a/Source/InputMethodController.h +++ b/Source/InputMethodController.h @@ -80,11 +80,5 @@ // current input mode NSString *_inputMode; - - // if Chinese conversion is enabled - BOOL _chineseConversionEnabled; - - // if half-width punctuation is enabled - BOOL _halfWidthPunctuationEnabled; } @end diff --git a/Source/InputMethodController.mm b/Source/InputMethodController.mm index efaa3d66..11912be4 100644 --- a/Source/InputMethodController.mm +++ b/Source/InputMethodController.mm @@ -45,8 +45,6 @@ #import "LanguageModelManager.h" #import "vChewing-Swift.h" -@import OpenCC; - // C++ namespace usages using namespace std; using namespace Formosa::Mandarin; @@ -54,40 +52,7 @@ using namespace Formosa::Gramambular; using namespace vChewing; using namespace OpenVanilla; -// default, min and max candidate list text size -static const NSInteger kDefaultCandidateListTextSize = 16; static const NSInteger kMinKeyLabelSize = 10; -static const NSInteger kMinCandidateListTextSize = 12; -static const NSInteger kMaxCandidateListTextSize = 196; - -// default, min and max composing buffer size (in codepoints) -// modern Macs can usually work up to 16 codepoints when the builder still -// walks the grid with good performance; slower Macs (like old PowerBooks) -// will start to sputter beyond 12; such is the algorithmatic complexity -// of the Viterbi algorithm used in the builder library (at O(N^2)) -static const NSInteger kDefaultComposingBufferSize = 10; -static const NSInteger kMinComposingBufferSize = 4; -static const NSInteger kMaxComposingBufferSize = 20; - -// user defaults (app perferences) key names; in this project we use -// NSUserDefaults throughout and do not wrap them in another config object -static NSString *const kKeyboardLayoutPreferenceKey = @"KeyboardLayout"; -static NSString *const kBasisKeyboardLayoutPreferenceKey = @"BasisKeyboardLayout"; // alphanumeric ("ASCII") input basis -static NSString *const kFunctionKeyKeyboardLayoutPreferenceKey = @"FunctionKeyKeyboardLayout"; // alphanumeric ("ASCII") input basis -static NSString *const kFunctionKeyKeyboardLayoutOverrideIncludeShiftKey = @"FunctionKeyKeyboardLayoutOverrideIncludeShift"; // whether include shift -static NSString *const kCandidateListTextSizeKey = @"CandidateListTextSize"; -static NSString *const kSelectPhraseAfterCursorAsCandidatePreferenceKey = @"SelectPhraseAfterCursorAsCandidate"; -static NSString *const kUseHorizontalCandidateListPreferenceKey = @"UseHorizontalCandidateList"; -static NSString *const kComposingBufferSizePreferenceKey = @"ComposingBufferSize"; -static NSString *const kChooseCandidateUsingSpaceKey = @"ChooseCandidateUsingSpaceKey"; -static NSString *const kChineseConversionEnabledKey = @"ChineseConversionEnabledKey"; -static NSString *const kHalfWidthPunctuationEnabledKey = @"HalfWidthPunctuationEnabledKey"; -static NSString *const kEscToCleanInputBufferKey = @"EscToCleanInputBufferKey"; - -// advanced (usually optional) settings -static NSString *const kCandidateTextFontName = @"CandidateTextFontName"; -static NSString *const kCandidateKeyLabelFontName = @"CandidateKeyLabelFontName"; -static NSString *const kCandidateKeys = @"CandidateKeys"; // input modes static NSString *const kBopomofoModeIdentifier = @"org.openvanilla.inputmethod.vChewing.Bopomofo"; @@ -107,16 +72,6 @@ enum { kDeleteKeyCode = 117 }; -typedef NS_ENUM(NSUInteger, vChewingEmacsKey) { - vChewingEmacsKeyNone, - vChewingEmacsKeyForward, - vChewingEmacsKeyBackward, - vChewingEmacsKeyHome, - vChewingEmacsKeyEnd, - vChewingEmacsKeyDelete, - vChewingEmacsKeyNextPage, -}; - VTCandidateController *gCurrentCandidateController = nil; // if DEBUG is defined, a DOT file (GraphViz format) will be written to the @@ -131,12 +86,6 @@ static inline NSString *LocalizationNotNeeded(NSString *s) { return s; } -// private methods -@interface vChewingInputMethodController () -+ (VTHorizontalCandidateController *)horizontalCandidateController; -+ (VTVerticalCandidateController *)verticalCandidateController; -@end - @interface vChewingInputMethodController (VTCandidateController) @end @@ -169,7 +118,7 @@ static double FindHighestScore(const vector& nodes, double epsilon) if (_bpmfReadingBuffer) { delete _bpmfReadingBuffer; } - + if (_builder) { delete _builder; } @@ -181,31 +130,29 @@ static double FindHighestScore(const vector& nodes, double epsilon) { // an instance is initialized whenever a text input client (a Mac app) requires // text input from an IME - + self = [super initWithServer:server delegate:delegate client:client]; if (self) { _candidates = [[NSMutableArray alloc] init]; - + // create the reading buffer _bpmfReadingBuffer = new BopomofoReadingBuffer(BopomofoKeyboardLayout::StandardLayout()); - + // create the lattice builder _languageModel = [LanguageModelManager languageModelBopomofo]; _userOverrideModel = [LanguageModelManager userOverrideModel]; - + _builder = new BlockReadingBuilder(_languageModel); - + // each Mandarin syllable is separated by a hyphen _builder->setJoinSeparator("-"); - + // create the composing buffer _composingBuffer = [[NSMutableString alloc] init]; - + _inputMode = kBopomofoModeIdentifier; - _chineseConversionEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:kChineseConversionEnabledKey]; - _halfWidthPunctuationEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:kHalfWidthPunctuationEnabledKey]; } - + return self; } @@ -213,18 +160,18 @@ static double FindHighestScore(const vector& nodes, double epsilon) { // a menu instance (autoreleased) is requested every time the user click on the input menu NSMenu *menu = [[NSMenu alloc] initWithTitle:LocalizationNotNeeded(@"Input Method Menu")]; - NSMenuItem *preferenceMenuItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"vChewing Preferences", @"") action:@selector(showPreferences:) keyEquivalent:@""]; - [menu addItem:preferenceMenuItem]; - + + [menu addItemWithTitle:NSLocalizedString(@"vChewing Preferences", @"") action:@selector(showPreferences:) keyEquivalent:@""]; + NSMenuItem *chineseConversionMenuItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Chinese Conversion", @"") action:@selector(toggleChineseConverter:) keyEquivalent:@"K"]; chineseConversionMenuItem.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagControl; - chineseConversionMenuItem.state = _chineseConversionEnabled ? NSControlStateValueOn : NSControlStateValueOff; + chineseConversionMenuItem.state = Preferences.chineseConversionEnabled ? NSControlStateValueOn : NSControlStateValueOff; [menu addItem:chineseConversionMenuItem]; - + NSMenuItem *halfWidthPunctuationMenuItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Use Half-Width Punctuations", @"") action:@selector(toggleHalfWidthPunctuation:) keyEquivalent:@""]; - halfWidthPunctuationMenuItem.state = _halfWidthPunctuationEnabled ? NSControlStateValueOn : NSControlStateValueOff; + halfWidthPunctuationMenuItem.state = Preferences.halfWidthPunctuationEnabled ? NSControlStateValueOn : NSControlStateValueOff; [menu addItem:halfWidthPunctuationMenuItem]; - + [menu addItem:[NSMenuItem separatorItem]]; // ------------------------------ if (_inputMode == kSimpBopomofoModeIdentifier) { @@ -232,23 +179,15 @@ static double FindHighestScore(const vector& nodes, double epsilon) [menu addItem:editExcludedPhrasesItem]; } else { - NSMenuItem *editUserPhrasesItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Edit User Phrases", @"") action:@selector(openUserPhrases:) keyEquivalent:@""]; - [menu addItem:editUserPhrasesItem]; - - NSMenuItem *editExcludedPhrasesItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Edit Excluded Phrases", @"") action:@selector(openExcludedPhrasesvChewing:) keyEquivalent:@""]; - [menu addItem:editExcludedPhrasesItem]; + [menu addItemWithTitle:NSLocalizedString(@"Edit User Phrases", @"") action:@selector(openUserPhrases:) keyEquivalent:@""]; + [menu addItemWithTitle:NSLocalizedString(@"Edit Excluded Phrases", @"") action:@selector(openExcludedPhrasesvChewing:) keyEquivalent:@""]; } - - NSMenuItem *reloadUserPhrasesItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Reload User Phrases", @"") action:@selector(reloadUserPhrases:) keyEquivalent:@""]; - [menu addItem:reloadUserPhrasesItem]; + [menu addItemWithTitle:NSLocalizedString(@"Reload User Phrases", @"") action:@selector(reloadUserPhrases:) keyEquivalent:@""]; [menu addItem:[NSMenuItem separatorItem]]; // ------------------------------ - - NSMenuItem *updateCheckItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Check for Updates…", @"") action:@selector(checkForUpdate:) keyEquivalent:@""]; - [menu addItem:updateCheckItem]; - - NSMenuItem *aboutMenuItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"About vChewing…", @"") action:@selector(showAbout:) keyEquivalent:@""]; - [menu addItem:aboutMenuItem]; + + [menu addItemWithTitle:NSLocalizedString(@"Check for Updates…", @"") action:@selector(checkForUpdate:) keyEquivalent:@""]; + [menu addItemWithTitle:NSLocalizedString(@"About vChewing…", @"") action:@selector(showAbout:) keyEquivalent:@""]; return menu; } @@ -257,67 +196,43 @@ static double FindHighestScore(const vector& nodes, double epsilon) - (void)activateServer:(id)client { [[NSUserDefaults standardUserDefaults] synchronize]; - + // Override the keyboard layout. Use US if not set. - NSString *basisKeyboardLayoutID = [[NSUserDefaults standardUserDefaults] stringForKey:kBasisKeyboardLayoutPreferenceKey]; - if (!basisKeyboardLayoutID) { - basisKeyboardLayoutID = @"com.apple.keylayout.US"; - } + NSString *basisKeyboardLayoutID = Preferences.basisKeyboardLayout; [client overrideKeyboardWithKeyboardNamed:basisKeyboardLayoutID]; - + // reset the state _currentDeferredClient = nil; _currentCandidateClient = nil; _builder->clear(); _walkedNodes.clear(); [_composingBuffer setString:@""]; - + // checks and populates the default settings - NSInteger keyboardLayout = [[NSUserDefaults standardUserDefaults] integerForKey:kKeyboardLayoutPreferenceKey]; - switch (keyboardLayout) { - case 0: + switch (Preferences.keyboardLayout) { + case KeyboardLayoutStandard: _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::StandardLayout()); break; - case 1: + case KeyboardLayoutEten: _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::ETenLayout()); break; - case 2: + case KeyboardLayoutHsu: _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::HsuLayout()); break; - case 3: + case KeyboardLayoutEten26: _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::ETen26Layout()); break; - case 4: + case KeyboardLayoutHanyuPinyin: _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::HanyuPinyinLayout()); break; - case 5: + case KeyboardLayoutIBM: _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::IBMLayout()); break; default: _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::StandardLayout()); - [[NSUserDefaults standardUserDefaults] setInteger:0 forKey:kKeyboardLayoutPreferenceKey]; + Preferences.keyboardLayout = KeyboardLayoutStandard; } - - // set the size - NSInteger textSize = [[NSUserDefaults standardUserDefaults] integerForKey:kCandidateListTextSizeKey]; - NSInteger previousTextSize = textSize; - if (textSize == 0) { - textSize = kDefaultCandidateListTextSize; - } - else if (textSize < kMinCandidateListTextSize) { - textSize = kMinCandidateListTextSize; - } - else if (textSize > kMaxCandidateListTextSize) { - textSize = kMaxCandidateListTextSize; - } - - if (textSize != previousTextSize) { - [[NSUserDefaults standardUserDefaults] setInteger:textSize forKey:kCandidateListTextSizeKey]; - } - if (![[NSUserDefaults standardUserDefaults] objectForKey:kChooseCandidateUsingSpaceKey]) { - [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kChooseCandidateUsingSpaceKey]; - } - + [(AppDelegate *)[NSApp delegate] checkForUpdate]; } @@ -328,17 +243,17 @@ static double FindHighestScore(const vector& nodes, double epsilon) _bpmfReadingBuffer->clear(); [client setMarkedText:@"" selectionRange:NSMakeRange(0, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; } - + // commit any residue in the composing buffer [self commitComposition:client]; - + _currentDeferredClient = nil; _currentCandidateClient = nil; - + gCurrentCandidateController.delegate = nil; gCurrentCandidateController.visible = NO; [_candidates removeAllObjects]; - + [self _hideTooltip]; } @@ -346,7 +261,7 @@ static double FindHighestScore(const vector& nodes, double epsilon) { NSString *newInputMode; vChewingLM *newLanguageModel; - + if ([value isKindOfClass:[NSString class]] && [value isEqual:kSimpBopomofoModeIdentifier]) { newInputMode = kSimpBopomofoModeIdentifier; newLanguageModel = [LanguageModelManager languageModelSimpBopomofo]; @@ -355,30 +270,27 @@ static double FindHighestScore(const vector& nodes, double epsilon) newInputMode = kBopomofoModeIdentifier; newLanguageModel = [LanguageModelManager languageModelBopomofo]; } - + // Only apply the changes if the value is changed if (![_inputMode isEqualToString:newInputMode]) { [[NSUserDefaults standardUserDefaults] synchronize]; - + // Remember to override the keyboard layout again -- treat this as an activate eventy - NSString *basisKeyboardLayoutID = [[NSUserDefaults standardUserDefaults] stringForKey:kBasisKeyboardLayoutPreferenceKey]; - if (!basisKeyboardLayoutID) { - basisKeyboardLayoutID = @"com.apple.keylayout.US"; - } + NSString *basisKeyboardLayoutID = Preferences.basisKeyboardLayout; [sender overrideKeyboardWithKeyboardNamed:basisKeyboardLayoutID]; - + _inputMode = newInputMode; _languageModel = newLanguageModel; - + if (!_bpmfReadingBuffer->isEmpty()) { _bpmfReadingBuffer->clear(); [self updateClientComposingBuffer:sender]; } - + if ([_composingBuffer length] > 0) { [self commitComposition:sender]; } - + if (_builder) { delete _builder; _builder = new BlockReadingBuilder(_languageModel); @@ -389,6 +301,13 @@ static double FindHighestScore(const vector& nodes, double epsilon) #pragma mark - IMKServerInput protocol methods +- (NSString *)_convertToKangXi:(NSString *)text +{ + // return [VXHanConvert convertToSimplifiedFrom:text]; // VXHanConvert 這個引擎有點落後了,不支援詞組轉換、且修改轉換表的過程很麻煩。 + // OpenCC 引擎別的都還好,就是有點肥。改日換成純 ObjC 的 OpenCC 實現方案。 + return [OpenCCBridge convertToKangXi:text]; +} + - (void)commitComposition:(id)client { // if it's Terminal, we don't commit at the first call (the client of which will not be IPMDServerClientWrapper) @@ -400,14 +319,14 @@ static double FindHighestScore(const vector& nodes, double epsilon) } return; } - + // Chinese conversion. NSString *buffer = _composingBuffer; - BOOL chineseConversionEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:kChineseConversionEnabledKey]; - if (chineseConversionEnabled) { - buffer = [OpenCCBridge convert:_composingBuffer]; + + if (Preferences.chineseConversionEnabled) { + buffer = [self _convertToKangXi:_composingBuffer]; } - + // commit the text, clear the state [client insertText:buffer replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; _builder->clear(); @@ -415,7 +334,6 @@ static double FindHighestScore(const vector& nodes, double epsilon) [_composingBuffer setString:@""]; gCurrentCandidateController.visible = NO; [_candidates removeAllObjects]; - [self _hideTooltip]; } @@ -427,13 +345,13 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } { // "updating the composing buffer" means to request the client to "refresh" the text input buffer // with our "composing text" - + [_composingBuffer setString:@""]; NSInteger composedStringCursorIndex = 0; - + size_t readingCursorIndex = 0; size_t builderCursorIndex = _builder->cursorIndex(); - + // 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 @@ -442,10 +360,10 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } string nodeStr = (*wi).node->currentKeyValue().value; vector codepoints = OVUTF8Helper::SplitStringByCodePoint(nodeStr); size_t codepointCount = codepoints.size(); - + NSString *valueString = [NSString stringWithUTF8String:nodeStr.c_str()]; [_composingBuffer appendString:valueString]; - + // 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" @@ -465,7 +383,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } } } } - + // 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 @@ -474,7 +392,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } NSString *tail = [_composingBuffer substringFromIndex:composedStringCursorIndex]; NSString *composedText = [head stringByAppendingString:[reading stringByAppendingString:tail]]; NSInteger cursorIndex = composedStringCursorIndex + [reading length]; - + if (_bpmfReadingBuffer->isEmpty() && _builder->markerCursorIndex() != SIZE_MAX) { // if there is a marked range, we need to tear the string into three parts. NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:composedText]; @@ -504,7 +422,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } NSDictionary *attrDict = @{NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle), NSMarkedClauseSegmentAttributeName: @0}; NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:composedText attributes:attrDict]; - + // the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound, // i.e. the client app needs to take care of where to put ths composing buffer [client setMarkedText:attrString selectionRange:NSMakeRange(cursorIndex, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; @@ -519,19 +437,19 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } // of the best possible Mandarain characters given the input syllables, // using the Viterbi algorithm implemented in the Gramambular library 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 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 } @@ -545,38 +463,23 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } // lose their influence over the whole MLE anyway -- so tht when // the user type along, the already composed text at front will // be popped out - - NSInteger _composingBufferSize = [[NSUserDefaults standardUserDefaults] integerForKey:kComposingBufferSizePreferenceKey]; - NSInteger previousComposingBufferSize = _composingBufferSize; - - if (_composingBufferSize == 0) { - _composingBufferSize = kDefaultComposingBufferSize; - } - else if (_composingBufferSize < kMinComposingBufferSize) { - _composingBufferSize = kMinComposingBufferSize; - } - else if (_composingBufferSize > kMaxComposingBufferSize) { - _composingBufferSize = kMaxComposingBufferSize; - } - - if (_composingBufferSize != previousComposingBufferSize) { - [[NSUserDefaults standardUserDefaults] setInteger:_composingBufferSize forKey:kComposingBufferSizePreferenceKey]; - } - - if (_builder->grid().width() > (size_t)_composingBufferSize) { + + NSInteger composingBufferSize = Preferences.composingBufferSize; + + if (_builder->grid().width() > (size_t)composingBufferSize) { if (_walkedNodes.size() > 0) { NodeAnchor &anchor = _walkedNodes[0]; NSString *popedText = [NSString stringWithUTF8String:anchor.node->currentKeyValue().value.c_str()]; // Chinese conversion. - BOOL chineseConversionEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:kChineseConversionEnabledKey]; + BOOL chineseConversionEnabled = Preferences.chineseConversionEnabled; if (chineseConversionEnabled) { - popedText = [OpenCCBridge convert:popedText]; + popedText = [self _convertToKangXi:popedText]; } [client insertText:popedText replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; _builder->removeHeadReadings(anchor.spanningLength); } } - + [self walk]; } @@ -588,67 +491,19 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } - (string)_currentLayout { - string layout = string("Standard_");; - NSInteger keyboardLayout = [[NSUserDefaults standardUserDefaults] integerForKey:kKeyboardLayoutPreferenceKey]; - switch (keyboardLayout) { - case 0: - layout = string("Standard_"); - break; - case 1: - layout = string("ETen_"); - break; - case 2: - layout = string("ETen26_"); - break; - case 3: - layout = string("Hsu_"); - break; - case 4: - layout = string("HanyuPinyin_"); - break; - case 5: - layout = string("IBM_"); - break; - default: - break; - } + NSString *keyboardLayoutName = Preferences.keyboardLayoutName; + string layout = string(keyboardLayoutName.UTF8String) + string("_"); return layout; } -- (vChewingEmacsKey)_detectEmacsKeyFromCharCode:(UniChar)charCode modifiers:(NSUInteger)flags -{ - if (flags & NSControlKeyMask) { - char c = charCode + 'a' - 1; - if (c == 'a') { - return vChewingEmacsKeyHome; - } - else if (c == 'e') { - return vChewingEmacsKeyEnd; - } - else if (c == 'f') { - return vChewingEmacsKeyForward; - } - else if (c == 'b') { - return vChewingEmacsKeyBackward; - } - else if (c == 'd') { - return vChewingEmacsKeyDelete; - } - else if (c == 'v') { - return vChewingEmacsKeyNextPage; - } - } - return vChewingEmacsKeyNone; -} - - (BOOL)handleInputText:(NSString*)inputText key:(NSInteger)keyCode modifiers:(NSUInteger)flags client:(id)client { NSRect textFrame = NSZeroRect; NSDictionary *attributes = nil; - + bool composeReading = false; BOOL useVerticalMode = NO; - + @try { attributes = [client attributesForCharacterIndex:0 lineHeightRectangle:&textFrame]; useVerticalMode = [attributes objectForKey:@"IMKTextOrientation"] && [[attributes objectForKey:@"IMKTextOrientation"] integerValue] == 0; @@ -656,35 +511,35 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } @catch (NSException *e) { // exception may raise while using Twitter.app's search filed. } - + NSInteger cursorForwardKey = useVerticalMode ? kDownKeyCode : kRightKeyCode; NSInteger cursorBackwardKey = useVerticalMode ? kUpKeyCode : kLeftKeyCode; NSInteger extraChooseCandidateKey = useVerticalMode ? kLeftKeyCode : kDownKeyCode; NSInteger absorbedArrowKey = useVerticalMode ? kRightKeyCode : kUpKeyCode; NSInteger verticalModeOnlyChooseCandidateKey = useVerticalMode ? absorbedArrowKey : 0; - + // get the unicode character code UniChar charCode = [inputText length] ? [inputText characterAtIndex:0] : 0; - - vChewingEmacsKey emacsKey = [self _detectEmacsKeyFromCharCode:charCode modifiers:flags]; - + + vChewingEmacsKey emacsKey = [EmacsKeyHelper detectWithCharCode:charCode flags:flags]; + if ([[client bundleIdentifier] isEqualToString:@"com.apple.Terminal"] && [NSStringFromClass([client class]) isEqualToString:@"IPMDServerClientWrapper"]) { // special handling for com.apple.Terminal _currentDeferredClient = client; } - + // if the inputText is empty, it's a function key combination, we ignore it if (![inputText length]) { return NO; } - + // if the composing buffer is empty and there's no reading, and there is some function key combination, we ignore it if (![_composingBuffer length] && _bpmfReadingBuffer->isEmpty() && ((flags & NSCommandKeyMask) || (flags & NSControlKeyMask) || (flags & NSAlternateKeyMask) || (flags & NSNumericPadKeyMask))) { return NO; } - + // Caps Lock processing : if Caps Lock is on, temporarily disable bopomofo. if (charCode == 8 || charCode == 13 || keyCode == absorbedArrowKey || keyCode == extraChooseCandidateKey || keyCode == cursorForwardKey || keyCode == cursorBackwardKey) { // do nothing if backspace is pressed -- we ignore the key @@ -694,40 +549,40 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } if ([_composingBuffer length]) { [self commitComposition:client]; } - + // first commit everything in the buffer. if (flags & NSShiftKeyMask) { return NO; } - + // if ASCII but not printable, don't use insertText:replacementRange: as many apps don't handle non-ASCII char insertions. if (charCode < 0x80 && !isprint(charCode)) { return NO; } - + // when shift is pressed, don't do further processing, since it outputs capital letter anyway. NSString *popedText = [inputText lowercaseString]; [client insertText:popedText replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; return YES; } - + if (flags & NSNumericPadKeyMask) { if (keyCode != kLeftKeyCode && keyCode != kRightKeyCode && keyCode != kDownKeyCode && keyCode != kUpKeyCode && charCode != 32 && isprint(charCode)) { if ([_composingBuffer length]) { [self commitComposition:client]; } - + NSString *popedText = [inputText lowercaseString]; [client insertText:popedText replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; return YES; } } - + // if we have candidate, it means we need to pass the event to the candidate handler if ([_candidates count]) { return [self _handleCandidateEventWithInputText:inputText charCode:charCode keyCode:keyCode emacsKey:(vChewingEmacsKey)emacsKey]; } - + // If we have marker index. if (_builder->markerCursorIndex() != SIZE_MAX) { // ESC @@ -747,7 +602,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } [self updateClientComposingBuffer:client]; return YES; } - // Shift + Left // Shift + Up in vertical tyinging mode + // Shift + left if ((keyCode == cursorBackwardKey || emacsKey == vChewingEmacsKeyBackward) && (flags & NSShiftKeyMask)) { if (_builder->markerCursorIndex() > 0) { @@ -759,9 +614,9 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } [self updateClientComposingBuffer:client]; return YES; } - // Shift + Right // Shift + Down in vertical tyinging mode + // Shift + Right if ((keyCode == cursorForwardKey || emacsKey == vChewingEmacsKeyForward) - && (flags & NSShiftKeyMask)) { + && (flags & NSShiftKeyMask)) { if (_builder->markerCursorIndex() < _builder->length()) { _builder->setMarkerCursorIndex(_builder->markerCursorIndex() + 1); } @@ -771,14 +626,14 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } [self updateClientComposingBuffer:client]; return YES; } - + _builder->setMarkerCursorIndex(SIZE_MAX); } - + // see if it's valid BPMF reading if (_bpmfReadingBuffer->isValidKey((char)charCode)) { _bpmfReadingBuffer->combineKey((char)charCode); - + // if we have a tone marker, we have to insert the reading to the // builder in other words, if we don't have a tone marker, we just // update the composing buffer @@ -788,55 +643,55 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } return YES; } } - + // see if we have composition if Enter/Space is hit and buffer is not empty // this is bit-OR'ed so that the tone marker key is also taken into account composeReading |= (!_bpmfReadingBuffer->isEmpty() && (charCode == 32 || charCode == 13)); if (composeReading) { // combine the reading string reading = _bpmfReadingBuffer->syllable().composedString(); - + // see if we have a unigram for this if (!_languageModel->hasUnigramsForKey(reading)) { [self beep]; [self updateClientComposingBuffer:client]; return YES; } - + // and insert it into the lattice _builder->insertReadingAtCursor(reading); - + // then walk the lattice [self popOverflowComposingTextAndWalk:client]; - + // get user override model suggestion string overrideValue = (_inputMode == kSimpBopomofoModeIdentifier) ? "" : - _userOverrideModel->suggest(_walkedNodes, _builder->cursorIndex(), [[NSDate date] timeIntervalSince1970]); - + _userOverrideModel->suggest(_walkedNodes, _builder->cursorIndex(), [[NSDate date] timeIntervalSince1970]); + if (!overrideValue.empty()) { size_t cursorIndex = [self actualCandidateCursorIndex]; vector nodes = _builder->grid().nodesCrossingOrEndingAt(cursorIndex); double highestScore = FindHighestScore(nodes, kEpsilon); _builder->grid().overrideNodeScoreForSelectedCandidate(cursorIndex, overrideValue, highestScore); } - + // then update the text _bpmfReadingBuffer->clear(); [self updateClientComposingBuffer:client]; - + if (_inputMode == kSimpBopomofoModeIdentifier) { [self _showCandidateWindowUsingVerticalMode:useVerticalMode client:client]; } - + // and tells the client that the key is consumed return YES; } - + // keyCode 125 = Down, charCode 32 = Space if (_bpmfReadingBuffer->isEmpty() && [_composingBuffer length] > 0 && (keyCode == extraChooseCandidateKey || charCode == 32 || (useVerticalMode && (keyCode == verticalModeOnlyChooseCandidateKey)))) { if (charCode == 32) { // if the spacebar is NOT set to be a selection key - if (![[NSUserDefaults standardUserDefaults] boolForKey:kChooseCandidateUsingSpaceKey]) { + if (!Preferences.chooseCandidateUsingSpace) { if (_builder->cursorIndex() >= _builder->length()) { [_composingBuffer appendString:@" "]; [self commitComposition:client]; @@ -848,17 +703,17 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } [self updateClientComposingBuffer:client]; } return YES; - + } } [self _showCandidateWindowUsingVerticalMode:useVerticalMode client:client]; return YES; } - + // Esc if (charCode == 27) { - BOOL escToClearInputBufferEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:kEscToCleanInputBufferKey]; - + BOOL escToClearInputBufferEnabled = Preferences.escToCleanInputBuffer; + if (escToClearInputBufferEnabled) { // if the optioon is enabled, we clear everythiong including the composing // buffer, walked nodes and the reading. @@ -876,10 +731,10 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } // "cancels" the current composed character and revert it to // Bopomofo reading, in odds with the expectation of users from // other platforms - + if (_bpmfReadingBuffer->isEmpty()) { // no nee to beep since the event is deliberately triggered by user - + if (![_composingBuffer length]) { return NO; } @@ -888,11 +743,11 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } _bpmfReadingBuffer->clear(); } } - + [self updateClientComposingBuffer:client]; return YES; } - + // handle cursor backward if (keyCode == cursorBackwardKey || emacsKey == vChewingEmacsKeyBackward) { if (!_bpmfReadingBuffer->isEmpty()) { @@ -902,7 +757,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } if (![_composingBuffer length]) { return NO; } - + if (flags & NSShiftKeyMask) { // Shift + left if (_builder->cursorIndex() > 0) { @@ -920,11 +775,11 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } } } } - + [self updateClientComposingBuffer:client]; return YES; } - + // handle cursor forward if (keyCode == cursorForwardKey || emacsKey == vChewingEmacsKeyForward) { if (!_bpmfReadingBuffer->isEmpty()) { @@ -934,7 +789,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } if (![_composingBuffer length]) { return NO; } - + if (flags & NSShiftKeyMask) { // Shift + Right if (_builder->cursorIndex() < _builder->length()) { @@ -951,11 +806,11 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } } } } - + [self updateClientComposingBuffer:client]; return YES; } - + if (keyCode == kHomeKeyCode || emacsKey == vChewingEmacsKeyHome) { if (!_bpmfReadingBuffer->isEmpty()) { [self beep]; @@ -964,7 +819,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } if (![_composingBuffer length]) { return NO; } - + if (_builder->cursorIndex()) { _builder->setCursorIndex(0); } @@ -972,11 +827,11 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } [self beep]; } } - + [self updateClientComposingBuffer:client]; return YES; } - + if (keyCode == kEndKeyCode || emacsKey == vChewingEmacsKeyEnd) { if (!_bpmfReadingBuffer->isEmpty()) { [self beep]; @@ -985,7 +840,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } if (![_composingBuffer length]) { return NO; } - + if (_builder->cursorIndex() != _builder->length()) { _builder->setCursorIndex(_builder->length()); } @@ -993,11 +848,11 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } [self beep]; } } - + [self updateClientComposingBuffer:client]; return YES; } - + if (keyCode == absorbedArrowKey || keyCode == extraChooseCandidateKey) { if (!_bpmfReadingBuffer->isEmpty()) { [self beep]; @@ -1005,14 +860,14 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } [self updateClientComposingBuffer:client]; return YES; } - + // Backspace if (charCode == 8) { if (_bpmfReadingBuffer->isEmpty()) { if (![_composingBuffer length]) { return NO; } - + if (_builder->cursorIndex()) { _builder->deleteReadingBeforeCursor(); [self walk]; @@ -1024,18 +879,18 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } else { _bpmfReadingBuffer->backspace(); } - + [self updateClientComposingBuffer:client]; return YES; } - + // Delete if (keyCode == kDeleteKeyCode || emacsKey == vChewingEmacsKeyDelete) { if (_bpmfReadingBuffer->isEmpty()) { if (![_composingBuffer length]) { return NO; } - + if (_builder->cursorIndex() != _builder->length()) { _builder->deleteReadingAfterCursor(); [self walk]; @@ -1047,21 +902,21 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } else { [self beep]; } - + [self updateClientComposingBuffer:client]; return YES; } - + // Enter if (charCode == 13) { if (![_composingBuffer length]) { return NO; } - + [self commitComposition:client]; return YES; } - + // punctuation list if ((char)charCode == '`') { if (_languageModel->hasUnigramsForKey(string("_punctuation_list"))) { @@ -1077,21 +932,21 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } return YES; } } - + // if nothing is matched, see if it's a punctuation key for current layout. string layout = [self _currentLayout]; - string punctuationNamePrefix = (_halfWidthPunctuationEnabled ? string("_half_punctuation_"): string("_punctuation_")); + string punctuationNamePrefix = Preferences.halfWidthPunctuationEnabled ? string("_half_punctuation_"): string("_punctuation_"); string customPunctuation = punctuationNamePrefix + layout + string(1, (char)charCode); if ([self _handlePunctuation:customPunctuation usingVerticalMode:useVerticalMode client:client]) { return YES; } - + // if nothing is matched, see if it's a punctuation key. string punctuation = punctuationNamePrefix + string(1, (char)charCode); if ([self _handlePunctuation:punctuation usingVerticalMode:useVerticalMode client:client]) { return YES; } - + // still nothing, then we update the composing buffer (some app has // strange behavior if we don't do this, "thinking" the key is not // actually consumed) @@ -1100,7 +955,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } [self updateClientComposingBuffer:client]; return YES; } - + return NO; } @@ -1115,7 +970,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } [self beep]; } [self updateClientComposingBuffer:client]; - + if (_inputMode == kSimpBopomofoModeIdentifier && _bpmfReadingBuffer->isEmpty()) { [self collectCandidates]; if ([_candidates count] == 1) { @@ -1136,11 +991,11 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } (charCode == 27) || ((_inputMode == kSimpBopomofoModeIdentifier) && (charCode == 8 || keyCode == kDeleteKeyCode)); - + if (cancelCandidateKey) { gCurrentCandidateController.visible = NO; [_candidates removeAllObjects]; - + if (_inputMode == kSimpBopomofoModeIdentifier) { _builder->clear(); _walkedNodes.clear(); @@ -1260,12 +1115,12 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } else if (keyCode == kHomeKeyCode || emacsKey == vChewingEmacsKeyHome) { if (gCurrentCandidateController.selectedCandidateIndex == 0) { [self beep]; - + } else { gCurrentCandidateController.selectedCandidateIndex = 0; } - + [self updateClientComposingBuffer:_currentCandidateClient]; return YES; } @@ -1276,7 +1131,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } else { gCurrentCandidateController.selectedCandidateIndex = [_candidates count] - 1; } - + [self updateClientComposingBuffer:_currentCandidateClient]; return YES; } @@ -1288,7 +1143,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } break; } } - + [gCurrentCandidateController.keyLabels indexOfObject:inputText]; if (index != NSNotFound) { NSUInteger candidateIndex = [gCurrentCandidateController candidateIndexAtKeyLabelIndex:index]; @@ -1297,15 +1152,15 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } return YES; } } - + if (_inputMode == kSimpBopomofoModeIdentifier) { string layout = [self _currentLayout]; string customPunctuation = string("_punctuation_") + layout + string(1, (char)charCode); string punctuation = string("_punctuation_") + string(1, (char)charCode); - + BOOL shouldAutoSelectCandidate = _bpmfReadingBuffer->isValidKey((char)charCode) || _languageModel->hasUnigramsForKey(customPunctuation) || _languageModel->hasUnigramsForKey(punctuation); - + if (shouldAutoSelectCandidate) { NSUInteger candidateIndex = [gCurrentCandidateController candidateIndexAtKeyLabelIndex:0]; if (candidateIndex != NSUIntegerMax) { @@ -1314,7 +1169,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } } } } - + [self beep]; [self updateClientComposingBuffer:_currentCandidateClient]; return YES; @@ -1329,34 +1184,27 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } - (BOOL)handleEvent:(NSEvent *)event client:(id)client { if ([event type] == NSFlagsChanged) { - NSString *functionKeyKeyboardLayoutID = [[NSUserDefaults standardUserDefaults] stringForKey:kFunctionKeyKeyboardLayoutPreferenceKey]; - if (!functionKeyKeyboardLayoutID) { - functionKeyKeyboardLayoutID = @"com.apple.keylayout.US"; - } - - NSString *basisKeyboardLayoutID = [[NSUserDefaults standardUserDefaults] stringForKey:kBasisKeyboardLayoutPreferenceKey]; - if (!basisKeyboardLayoutID) { - basisKeyboardLayoutID = @"com.apple.keylayout.US"; - } - + NSString *functionKeyKeyboardLayoutID = Preferences.functionKeyboardLayout; + NSString *basisKeyboardLayoutID = Preferences.basisKeyboardLayout; + // If no override is needed, just return NO. if ([functionKeyKeyboardLayoutID isEqualToString:basisKeyboardLayoutID]) { return NO; } - + // Function key pressed. - BOOL includeShift = [[NSUserDefaults standardUserDefaults] boolForKey:kFunctionKeyKeyboardLayoutOverrideIncludeShiftKey]; + BOOL includeShift = Preferences.functionKeyKeyboardLayoutOverrideIncludeShiftKey; if (([event modifierFlags] & ~NSShiftKeyMask) || (([event modifierFlags] & NSShiftKeyMask) && includeShift)) { // Override the keyboard layout and let the OS do its thing [client overrideKeyboardWithKeyboardNamed:functionKeyKeyboardLayoutID]; return NO; } - + // Revert back to the basis layout when the function key is released [client overrideKeyboardWithKeyboardNamed:basisKeyboardLayoutID]; return NO; } - + NSString *inputText = [event characters]; NSInteger keyCode = [event keyCode]; NSUInteger flags = [event modifierFlags]; @@ -1399,13 +1247,13 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } { // returns the candidate [_candidates removeAllObjects]; - + size_t cursorIndex = [self actualCandidateCursorIndex]; vector nodes = _builder->grid().nodesCrossingOrEndingAt(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 (vector::iterator ni = nodes.begin(), ne = nodes.end(); ni != ne; ++ni) { const vector& candidates = (*ni).node->candidates(); @@ -1418,10 +1266,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } - (size_t)actualCandidateCursorIndex { size_t cursorIndex = _builder->cursorIndex(); - - BOOL candidatePhraseLocatedAfterCursor = [[NSUserDefaults standardUserDefaults] boolForKey:kSelectPhraseAfterCursorAsCandidatePreferenceKey]; - - if (candidatePhraseLocatedAfterCursor) { + if (Preferences.selectPhraseAfterCursorAsCandidate) { // MS Phonetics IME style, phrase is *after* the cursor, i.e. cursor is always *before* the phrase if (cursorIndex < _builder->length()) { ++cursorIndex; @@ -1432,71 +1277,70 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } ++cursorIndex; } } - + return cursorIndex; } - (void)_showCandidateWindowUsingVerticalMode:(BOOL)useVerticalMode client:(id)client { // set the candidate panel style - BOOL useHorizontalCandidateList = [[NSUserDefaults standardUserDefaults] boolForKey:kUseHorizontalCandidateListPreferenceKey]; - + if (useVerticalMode) { gCurrentCandidateController = [vChewingInputMethodController verticalCandidateController]; } - else if (useHorizontalCandidateList) { + else if (Preferences.useHorizontalCandidateList) { gCurrentCandidateController = [vChewingInputMethodController horizontalCandidateController]; } else { gCurrentCandidateController = [vChewingInputMethodController verticalCandidateController]; } - + // set the attributes for the candidate panel (which uses NSAttributedString) - NSInteger textSize = [[NSUserDefaults standardUserDefaults] integerForKey:kCandidateListTextSizeKey]; - + NSInteger textSize = Preferences.candidateListTextSize; + NSInteger keyLabelSize = textSize / 2; if (keyLabelSize < kMinKeyLabelSize) { keyLabelSize = kMinKeyLabelSize; } - - NSString *ctFontName = [[NSUserDefaults standardUserDefaults] stringForKey:kCandidateTextFontName]; - NSString *klFontName = [[NSUserDefaults standardUserDefaults] stringForKey:kCandidateKeyLabelFontName]; - NSString *ckeys = [[NSUserDefaults standardUserDefaults] stringForKey:kCandidateKeys]; - + + NSString *ctFontName = Preferences.candidateTextFontName; + NSString *klFontName = Preferences.candidateKeyLabelFontName; + NSString *ckeys = Preferences.candidateKeys; + gCurrentCandidateController.keyLabelFont = klFontName ? [NSFont fontWithName:klFontName size:keyLabelSize] : [NSFont systemFontOfSize:keyLabelSize]; gCurrentCandidateController.candidateFont = ctFontName ? [NSFont fontWithName:ctFontName size:textSize] : [NSFont systemFontOfSize:textSize]; - + NSMutableArray *keyLabels = [NSMutableArray arrayWithObjects:@"1", @"2", @"3", @"4", @"5", @"6", @"7", @"8", @"9", nil]; - + if ([ckeys length] > 1) { [keyLabels removeAllObjects]; for (NSUInteger i = 0, c = [ckeys length]; i < c; i++) { [keyLabels addObject:[ckeys substringWithRange:NSMakeRange(i, 1)]]; } } - + gCurrentCandidateController.keyLabels = keyLabels; [self collectCandidates]; - + if (_inputMode == kSimpBopomofoModeIdentifier && [_candidates count] == 1) { [self commitComposition:client]; return; } - + gCurrentCandidateController.delegate = self; [gCurrentCandidateController reloadData]; - + // update the composing text, set the client [self updateClientComposingBuffer:client]; _currentCandidateClient = client; - + NSRect lineHeightRect = NSMakeRect(0.0, 0.0, 16.0, 16.0); - + NSInteger cursor = _latestReadingCursor; if (cursor == [_composingBuffer length] && cursor != 0) { cursor--; } - + // some apps (e.g. Twitter for Mac's search bar) handle this call incorrectly, hence the try-catch @try { [client attributesForCharacterIndex:cursor lineHeightRectangle:&lineHeightRect]; @@ -1504,14 +1348,14 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } @catch (NSException *exception) { NSLog(@"%@", exception); } - + if (useVerticalMode) { [gCurrentCandidateController setWindowTopLeftPoint:NSMakePoint(lineHeightRect.origin.x + lineHeightRect.size.width + 4.0, lineHeightRect.origin.y - 4.0) bottomOutOfScreenAdjustmentHeight:lineHeightRect.size.height + 4.0]; } else { [gCurrentCandidateController setWindowTopLeftPoint:NSMakePoint(lineHeightRect.origin.x, lineHeightRect.origin.y - 4.0) bottomOutOfScreenAdjustmentHeight:lineHeightRect.size.height + 4.0]; } - + gCurrentCandidateController.visible = YES; } @@ -1525,14 +1369,14 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } if (!_bpmfReadingBuffer->isEmpty()) { return @""; } - + size_t begin = min(_builder->markerCursorIndex(), _builder->cursorIndex()); size_t end = max(_builder->markerCursorIndex(), _builder->cursorIndex()); // A phrase should contian at least two characters. if (end - begin < 1) { return @""; } - + NSRange range = NSMakeRange((NSInteger)begin, (NSInteger)(end - begin)); NSString *selectedText = [_composingBuffer substringWithRange:range]; return selectedText; @@ -1546,14 +1390,14 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } if (!_bpmfReadingBuffer->isEmpty()) { return @""; } - + size_t begin = min(_builder->markerCursorIndex(), _builder->cursorIndex()); size_t end = max(_builder->markerCursorIndex(), _builder->cursorIndex()); // A phrase should contian at least two characters. if (end - begin < 2) { return @""; } - + NSRange range = NSMakeRange((NSInteger)begin, (NSInteger)(end - begin)); NSString *selectedText = [_composingBuffer substringWithRange:range]; NSMutableString *string = [[NSMutableString alloc] init]; @@ -1574,7 +1418,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } if (![currentMarkedPhrase length]) { return NO; } - + return [LanguageModelManager writeUserPhrase:currentMarkedPhrase]; } @@ -1598,12 +1442,12 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } - (void)_showTooltip:(NSString *)tooltip client:(id)client { NSRect lineHeightRect = NSMakeRect(0.0, 0.0, 16.0, 16.0); - + NSInteger cursor = _latestReadingCursor; if (cursor == [_composingBuffer length] && cursor != 0) { cursor--; } - + // some apps (e.g. Twitter for Mac's search bar) handle this call incorrectly, hence the try-catch @try { [client attributesForCharacterIndex:cursor lineHeightRectangle:&lineHeightRect]; @@ -1611,7 +1455,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } @catch (NSException *exception) { NSLog(@"%@", exception); } - + [[vChewingInputMethodController tooltipController] showTooltip:tooltip atPoint:lineHeightRect.origin]; } @@ -1647,7 +1491,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } [[NonModalAlertWindowController sharedInstance] showWithTitle:NSLocalizedString(@"Unable to create the user phrase file.", @"") content:content confirmButtonTitle:NSLocalizedString(@"OK", @"") cancelButtonTitle:nil cancelAsDefault:NO delegate:nil]; return NO; } - + return YES; } @@ -1662,25 +1506,21 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } - (void)openUserPhrases:(id)sender { - NSLog(@"openUserPhrases called"); [self _openUserFile:[LanguageModelManager userPhrasesDataPathBopomofo]]; } - (void)openExcludedPhrasesSimpBopomofo:(id)sender { - NSLog(@"openExcludedPhrasesSimpBopomofo called"); [self _openUserFile:[LanguageModelManager excludedPhrasesDataPathSimpBopomofo]]; } - (void)openExcludedPhrasesvChewing:(id)sender { - NSLog(@"openExcludedPhrasesvChewing called"); [self _openUserFile:[LanguageModelManager excludedPhrasesDataPathBopomofo]]; } - (void)reloadUserPhrases:(id)sender { - NSLog(@"reloadUserPhrases called"); [LanguageModelManager loadUserPhrasesModel]; } @@ -1693,16 +1533,16 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } - (void)toggleChineseConverter:(id)sender { - _chineseConversionEnabled = !_chineseConversionEnabled; - [[NSUserDefaults standardUserDefaults] setBool:_chineseConversionEnabled forKey:kChineseConversionEnabledKey]; - - [NotifierController notifyWithMessage:[NSString stringWithFormat:@"%@%@%@", NSLocalizedString(@"Chinese Conversion", @""), @"\n", _chineseConversionEnabled ? NSLocalizedString(@"NotificationSwitchON", @"") : NSLocalizedString(@"NotificationSwitchOFF", @"")] stay:NO]; + BOOL chineseConversionEnabled = [Preferences toggleChineseConversionEnabled]; + [NotifierController notifyWithMessage:[NSString stringWithFormat:@"%@%@%@", NSLocalizedString(@"Chinese Conversion", @""), @"\n", chineseConversionEnabled ? NSLocalizedString(@"NotificationSwitchON", @"") : NSLocalizedString(@"NotificationSwitchOFF", @"")] stay:NO]; } - (void)toggleHalfWidthPunctuation:(id)sender { - _halfWidthPunctuationEnabled = !_halfWidthPunctuationEnabled; - [[NSUserDefaults standardUserDefaults] setBool:_halfWidthPunctuationEnabled forKey:kHalfWidthPunctuationEnabledKey]; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-result" + [Preferences toogleHalfWidthPunctuationEnabled]; +#pragma GCC diagnostic pop } @end @@ -1724,21 +1564,21 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } - (void)candidateController:(VTCandidateController *)controller didSelectCandidateAtIndex:(NSUInteger)index { gCurrentCandidateController.visible = NO; - + // candidate selected, override the node with selection string selectedValue = [[_candidates objectAtIndex:index] UTF8String]; - + size_t cursorIndex = [self actualCandidateCursorIndex]; _builder->grid().fixNodeSelectedCandidate(cursorIndex, selectedValue); if (_inputMode != kSimpBopomofoModeIdentifier) { _userOverrideModel->observe(_walkedNodes, cursorIndex, selectedValue, [[NSDate date] timeIntervalSince1970]); } - + [_candidates removeAllObjects]; - + [self walk]; [self updateClientComposingBuffer:_currentCandidateClient]; - + if (_inputMode == kSimpBopomofoModeIdentifier) { [self commitComposition:_currentCandidateClient]; return; diff --git a/Source/OpenCCBridge.swift b/Source/OpenCCBridge.swift index adba4a52..53994db8 100644 --- a/Source/OpenCCBridge.swift +++ b/Source/OpenCCBridge.swift @@ -1,5 +1,5 @@ // -// PreferencesWindowController.swift +// OpenCCBridge.swift // // Copyright (c) 2011-2022 The OpenVanilla Project. // @@ -34,22 +34,23 @@ import Foundation import OpenCC -// Since SwiftyLibreCC only provide Swift classes, we create an NSObject subclass -// in Swift in order to bridge the Swift classes into our Objective-C++ project. -class OpenCCBridge : NSObject { +/// A bridge to let Objctive-C code to access SwiftyOpenCC. +/// +/// Since SwiftyOpenCC only provide Swift classes, we create an NSObject subclass +/// in Swift in order to bridge the Swift classes into our Objective-C++ project. +public class OpenCCBridge: NSObject { private static let shared = OpenCCBridge() private var converter: ChineseConverter? - private override init() { try? converter = ChineseConverter(options: .twStandardRev) super.init() } - @objc static func convert(_ string: String) -> String? { + /// Converts to Simplified Chinese. + /// + /// - Parameter string: Text in Traditional Chinese. + /// - Returns: Text in Simplified Chinese. + @objc public static func convertToKangXi(_ string: String) -> String? { shared.converter?.convert(string) } - - private func convert(_ string: String) -> String? { - converter?.convert(string) - } } diff --git a/Source/PreferencesModule.swift b/Source/PreferencesModule.swift new file mode 100644 index 00000000..4e52d9c8 --- /dev/null +++ b/Source/PreferencesModule.swift @@ -0,0 +1,289 @@ +// +// Preferences.swift +// +// Copyright (c) 2011-2022 The OpenVanilla Project. +// +// Contributors: +// Weizhong Yang (@zonble) @ OpenVanilla +// +// Based on the Syrup Project and the Formosana Library +// by Lukhnos Liu (@lukhnos). +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Cocoa + +private let kKeyboardLayoutPreferenceKey = "KeyboardLayout" +private let kBasisKeyboardLayoutPreferenceKey = "BasisKeyboardLayout"; // alphanumeric ("ASCII") input basi +private let kFunctionKeyKeyboardLayoutPreferenceKey = "FunctionKeyKeyboardLayout"; // alphanumeric ("ASCII") input basi +private let kFunctionKeyKeyboardLayoutOverrideIncludeShiftKey = "FunctionKeyKeyboardLayoutOverrideIncludeShift"; // whether include shif +private let kCandidateListTextSizeKey = "CandidateListTextSize" +private let kSelectPhraseAfterCursorAsCandidatePreferenceKey = "SelectPhraseAfterCursorAsCandidate" +private let kUseHorizontalCandidateListPreferenceKey = "UseHorizontalCandidateList" +private let kComposingBufferSizePreferenceKey = "ComposingBufferSize" +private let kChooseCandidateUsingSpaceKey = "ChooseCandidateUsingSpaceKey" +private let kChineseConversionEnabledKey = "ChineseConversionEnabled" +private let kHalfWidthPunctuationEnabledKey = "HalfWidthPunctuationEnable" +private let kEscToCleanInputBufferKey = "EscToCleanInputBuffer" + +private let kCandidateTextFontName = "CandidateTextFontName" +private let kCandidateKeyLabelFontName = "CandidateKeyLabelFontName" +private let kCandidateKeys = "CandidateKeys" +private let kChineseConversionEngineKey = "ChineseConversionEngine" + +private let kDefaultCandidateListTextSize: CGFloat = 18 +private let kMinKeyLabelSize: CGFloat = 10 +private let kMinCandidateListTextSize: CGFloat = 12 +private let kMaxCandidateListTextSize: CGFloat = 196 + +// default, min and max composing buffer size (in codepoints) +// modern Macs can usually work up to 16 codepoints when the builder still +// walks the grid with good performance; slower Macs (like old PowerBooks) +// will start to sputter beyond 12; such is the algorithmatic complexity +// of the Viterbi algorithm used in the builder library (at O(N^2)) +private let kDefaultComposingBufferSize = 10 +private let kMinComposingBufferSize = 4 +private let kMaxComposingBufferSize = 20 + +private let kDefaultKeys = "123456789" + +// MARK: Property wrappers +@propertyWrapper +struct UserDefault { + let key: String + let defaultValue: Value + var container: UserDefaults = .standard + + var wrappedValue: Value { + get { + return container.object(forKey: key) as? Value ?? defaultValue + } + set { + container.set(newValue, forKey: key) + } + } +} + +@propertyWrapper +struct CandidateListTextSize { + let key: String + let defaultValue: CGFloat = kDefaultCandidateListTextSize + lazy var container: UserDefault = { + UserDefault(key: key, defaultValue: defaultValue) }() + + var wrappedValue: CGFloat { + mutating get { + var value = container.wrappedValue + if value < kMinCandidateListTextSize { + value = kMinCandidateListTextSize + } else if value > kMaxCandidateListTextSize { + value = kMaxCandidateListTextSize + } + return value + } + set { + var value = newValue + if value < kMinCandidateListTextSize { + value = kMinCandidateListTextSize + } else if value > kMaxCandidateListTextSize { + value = kMaxCandidateListTextSize + } + container.wrappedValue = value + } + } +} + +@propertyWrapper +struct ComposingBufferSize { + let key: String + let defaultValue: Int = kDefaultComposingBufferSize + lazy var container: UserDefault = { + UserDefault(key: key, defaultValue: defaultValue) }() + + var wrappedValue: Int { + mutating get { + let currentValue = container.wrappedValue + if currentValue < kMinComposingBufferSize { + return kMinComposingBufferSize + } else if currentValue > kMaxComposingBufferSize { + return kMaxComposingBufferSize + } + return currentValue + } + set { + var value = newValue + if value < kMinComposingBufferSize { + value = kMinComposingBufferSize + } else if value > kMaxComposingBufferSize { + value = kMaxComposingBufferSize + } + container.wrappedValue = value + } + } +} + +@propertyWrapper +struct ComposingKeys { + let key: String + let defaultValue: String? = kCandidateKeys + lazy var container: UserDefault = { + UserDefault(key: key, defaultValue: defaultValue) }() + + var wrappedValue: String? { + mutating get { + let value = container.wrappedValue + if let value = value { + if value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return nil + } + } + return value + } + set { + let value = newValue + if let value = value { + if value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + container.wrappedValue = nil + return + } + } + container.wrappedValue = value + } + } +} + +// MARK: - +@objc enum KeyboardLayout: Int { + case standard + case eten + case eten26 + case hsu + case hanyuPinyin + case IBM + + var name: String { + switch (self) { + case .standard: + return "Standard" + case .eten: + return "ETen" + case .eten26: + return "ETen26" + case .hsu: + return "Hsu" + case .hanyuPinyin: + return "HanyuPinyin" + case .IBM: + return "IBM" + } + } +} + +@objc enum ChineseConversionEngine: Int { + case openCC + case vxHanConvert + + var name: String { + switch (self) { + case .openCC: + return "OpenCC" + case .vxHanConvert: + return "VXHanConvert" + } + } +} + +// MARK: - +class Preferences: NSObject { + @UserDefault(key: kKeyboardLayoutPreferenceKey, defaultValue: 0) + @objc static var keyboardLayout: Int + + @objc static var keyboardLayoutName: String { + (KeyboardLayout(rawValue: self.keyboardLayout) ?? KeyboardLayout.standard).name + } + + @UserDefault(key: kBasisKeyboardLayoutPreferenceKey, defaultValue: "com.apple.keylayout.US") + @objc static var basisKeyboardLayout: String + + @UserDefault(key: kFunctionKeyKeyboardLayoutPreferenceKey, defaultValue: "com.apple.keylayout.US") + @objc static var functionKeyboardLayout: String + + @UserDefault(key: kFunctionKeyKeyboardLayoutOverrideIncludeShiftKey, defaultValue: false) + @objc static var functionKeyKeyboardLayoutOverrideIncludeShiftKey: Bool + + @CandidateListTextSize(key: kCandidateListTextSizeKey) + @objc static var candidateListTextSize: CGFloat + + @UserDefault(key: kSelectPhraseAfterCursorAsCandidatePreferenceKey, defaultValue: false) + @objc static var selectPhraseAfterCursorAsCandidate: Bool + + @UserDefault(key: kUseHorizontalCandidateListPreferenceKey, defaultValue: false) + @objc static var useHorizontalCandidateList: Bool + + @ComposingBufferSize(key: kComposingBufferSizePreferenceKey) + @objc static var composingBufferSize: Int + + @UserDefault(key: kChooseCandidateUsingSpaceKey, defaultValue: true) + @objc static var chooseCandidateUsingSpace: Bool + + @UserDefault(key: kChineseConversionEnabledKey, defaultValue: false) + @objc static var chineseConversionEnabled: Bool + + @objc static func toggleChineseConversionEnabled() -> Bool { + chineseConversionEnabled = !chineseConversionEnabled + return chineseConversionEnabled + } + + @UserDefault(key: kHalfWidthPunctuationEnabledKey, defaultValue: false) + @objc static var halfWidthPunctuationEnabled: Bool + + @objc static func toogleHalfWidthPunctuationEnabled() -> Bool { + halfWidthPunctuationEnabled = !halfWidthPunctuationEnabled + return halfWidthPunctuationEnabled; + } + + @UserDefault(key: kEscToCleanInputBufferKey, defaultValue: false) + @objc static var escToCleanInputBuffer: Bool + + // MARK: Optional settings + @UserDefault(key: kCandidateTextFontName, defaultValue: nil) + @objc static var candidateTextFontName: String? + + @UserDefault(key: kCandidateKeyLabelFontName, defaultValue: nil) + @objc static var candidateKeyLabelFontName: String? + + @ComposingKeys(key: kCandidateKeys) + @objc static var candidateKeys: String? + + @objc static var defaultKeys: String { + kDefaultKeys + } + + @UserDefault(key: kChineseConversionEngineKey, defaultValue: 0) + @objc static var chineseConversionEngine: Int + + @objc static var chineseConversionEngineName: String? { + return ChineseConversionEngine(rawValue: chineseConversionEngine)?.name + } + +} diff --git a/Source/PreferencesWindowController.swift b/Source/PreferencesWindowController.swift index a3ce72d7..2cd38ed2 100644 --- a/Source/PreferencesWindowController.swift +++ b/Source/PreferencesWindowController.swift @@ -36,9 +36,13 @@ import Cocoa import Carbon -private let kBasisKeyboardLayoutPreferenceKey = "BasisKeyboardLayout" -private let kCandidateKeys = "CandidateKeys" -private let kDefaultKeys = "123456789" +// Extend the RangeReplaceableCollection to allow it clean duplicated characters. +extension RangeReplaceableCollection where Element: Hashable { + var charDeDuplicate: Self { + var set = Set() + return filter{ set.insert($0).inserted } + } +} // Please note that the class should be exposed as "PreferencesWindowController" // in Objective-C in order to let IMK to see the same class name as @@ -47,55 +51,78 @@ private let kDefaultKeys = "123456789" @IBOutlet weak var fontSizePopUpButton: NSPopUpButton! @IBOutlet weak var basisKeyboardLayoutButton: NSPopUpButton! @IBOutlet weak var selectionKeyComboBox: NSComboBox! - + override func awakeFromNib() { let list = TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource] var usKeyboardLayoutItem: NSMenuItem? = nil var chosenItem: NSMenuItem? = nil - + basisKeyboardLayoutButton.menu?.removeAllItems() - - let basisKeyboardLayoutID = UserDefaults.standard.string(forKey: kBasisKeyboardLayoutPreferenceKey) + + let basisKeyboardLayoutID = Preferences.basisKeyboardLayout for source in list { - if let categoryPtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceCategory) { - let category = Unmanaged.fromOpaque(categoryPtr).takeUnretainedValue() - if category != kTISCategoryKeyboardInputSource { + + func getString(_ key: CFString) -> String? { + if let ptr = TISGetInputSourceProperty(source, key) { + return String(Unmanaged.fromOpaque(ptr).takeUnretainedValue()) + } + return nil + } + + func getBool(_ key: CFString) -> Bool? { + if let ptr = TISGetInputSourceProperty(source, key) { + return Unmanaged.fromOpaque(ptr).takeUnretainedValue() == kCFBooleanTrue + } + return nil + } + + if let category = getString(kTISPropertyInputSourceCategory) { + if category != String(kTISCategoryKeyboardInputSource) { continue } } else { continue } - - if let asciiCapablePtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsASCIICapable) { - let asciiCapable = Unmanaged.fromOpaque(asciiCapablePtr).takeUnretainedValue() - if asciiCapable != kCFBooleanTrue { + + if let asciiCapable = getBool(kTISPropertyInputSourceIsASCIICapable) { + if !asciiCapable { continue } } else { continue } - - if let sourceTypePtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceType) { - let sourceType = Unmanaged.fromOpaque(sourceTypePtr).takeUnretainedValue() - if sourceType != kTISTypeKeyboardLayout { + + if let sourceType = getString(kTISPropertyInputSourceType) { + if sourceType != String(kTISTypeKeyboardLayout) { continue } } else { continue } - - guard let sourceIDPtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceID), - let localizedNamePtr = TISGetInputSourceProperty(source, kTISPropertyLocalizedName) else { - continue - } - - let sourceID = String(Unmanaged.fromOpaque(sourceIDPtr).takeUnretainedValue()) - let localizedName = String(Unmanaged.fromOpaque(localizedNamePtr).takeUnretainedValue()) - + + guard let sourceID = getString(kTISPropertyInputSourceID), + let localizedName = getString(kTISPropertyLocalizedName) else { + continue + } + let menuItem = NSMenuItem() menuItem.title = localizedName menuItem.representedObject = sourceID - + + if let iconPtr = TISGetInputSourceProperty(source, kTISPropertyIconRef) { + let icon = IconRef(iconPtr) + let image = NSImage(iconRef: icon) + + func resize( _ image: NSImage) -> NSImage { + let newImage = NSImage(size: NSSize(width: 16, height: 16)) + newImage.lockFocus() + image.draw(in: NSRect(x: 0, y: 0, width: 16, height: 16)) + newImage.unlockFocus() + return newImage + } + menuItem.image = resize(image) + } + if sourceID == "com.apple.keylayout.US" { usKeyboardLayoutItem = menuItem } @@ -104,41 +131,41 @@ private let kDefaultKeys = "123456789" } basisKeyboardLayoutButton.menu?.addItem(menuItem) } - + basisKeyboardLayoutButton.select(chosenItem ?? usKeyboardLayoutItem) selectionKeyComboBox.usesDataSource = false - selectionKeyComboBox.addItems(withObjectValues: [kDefaultKeys, "asdfghjkl", "asdfzxcvb"]) - - var candidateSelectionKeys = (UserDefaults.standard.string(forKey: kCandidateKeys) ?? kDefaultKeys) - .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + selectionKeyComboBox.removeAllItems() + selectionKeyComboBox.addItems(withObjectValues: [Preferences.defaultKeys, "ASDFGHJKL", "ASDFZXCVB"]) + + var candidateSelectionKeys = Preferences.candidateKeys ?? Preferences.defaultKeys if candidateSelectionKeys.isEmpty { - candidateSelectionKeys = kDefaultKeys + candidateSelectionKeys = Preferences.defaultKeys } - + selectionKeyComboBox.stringValue = candidateSelectionKeys } - - @IBAction func updateBasisKeyboardLayoutAction(_ sender:Any) { - if let sourceID = basisKeyboardLayoutButton.selectedItem?.representedObject { - UserDefaults.standard.set(sourceID, forKey: kBasisKeyboardLayoutPreferenceKey) + + @IBAction func updateBasisKeyboardLayoutAction(_ sender: Any) { + if let sourceID = basisKeyboardLayoutButton.selectedItem?.representedObject as? String { + Preferences.basisKeyboardLayout = sourceID } } - + @IBAction func changeSelectionKeyAction(_ sender: Any) { - let keys = (sender as AnyObject).stringValue.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + let keys = (sender as AnyObject).stringValue.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).charDeDuplicate if keys.count != 9 || !keys.canBeConverted(to: .ascii) { - selectionKeyComboBox.stringValue = kDefaultKeys - UserDefaults.standard.removeObject(forKey: kCandidateKeys) + selectionKeyComboBox.stringValue = Preferences.defaultKeys + Preferences.candidateKeys = nil NSSound.beep() return } - + selectionKeyComboBox.stringValue = keys - if keys == kDefaultKeys { - UserDefaults.standard.removeObject(forKey: kCandidateKeys) + if keys == Preferences.defaultKeys { + Preferences.candidateKeys = nil } else { - UserDefaults.standard.set(keys, forKey: kCandidateKeys) + Preferences.candidateKeys = keys } } - + } diff --git a/Source/en.lproj/Localizable.strings b/Source/en.lproj/Localizable.strings index f2c3bf79..c4f74b43 100644 --- a/Source/en.lproj/Localizable.strings +++ b/Source/en.lproj/Localizable.strings @@ -17,8 +17,8 @@ "Visit Website" = "Visit Website"; "You're currently using vChewing %@ (%@), a new version %@ (%@) is now available. Do you want to visit vChewing's website to download the version?%@" = "You're currently using vChewing %@ (%@), a new version %@ (%@) is now available. Do you want to visit vChewing's website to download the version?%@"; "Chinese Conversion" = "Convert zh-TW Kanji to KangXi Variants"; -"NotificationSwitchON" = " ON"; -"NotificationSwitchOFF" = " OFF"; +"NotificationSwitchON" = "✔ ON"; +"NotificationSwitchOFF" = "✘ OFF"; "Edit User Phrases" = "Edit User Phrases"; "Reload User Phrases" = "Reload User Phrases"; "Unable to create the user phrase file." = "Unable to create the user phrase file."; diff --git a/Source/zh-Hans.lproj/Localizable.strings b/Source/zh-Hans.lproj/Localizable.strings index 9bd59dd9..8f7bae55 100644 --- a/Source/zh-Hans.lproj/Localizable.strings +++ b/Source/zh-Hans.lproj/Localizable.strings @@ -17,8 +17,8 @@ "Visit Website" = "前往网站"; "You're currently using vChewing %@ (%@), a new version %@ (%@) is now available. Do you want to visit vChewing's website to download the version?%@" = "目前使用的威注音官方版本是 %1$@ (%2$@),网路上有更新版本 %3$@ (%4$@) 可供下载。是否要前往威注音网站下载新版来安装?%5$@"; "Chinese Conversion" = "优先使用康熙繁体字"; -"NotificationSwitchON" = " 已启用"; -"NotificationSwitchOFF" = " 已停用"; +"NotificationSwitchON" = "✔ 已启用"; +"NotificationSwitchOFF" = "✘ 已停用"; "Edit User Phrases" = "编辑自订语汇"; "Reload User Phrases" = "重载自订语汇"; "Unable to create the user phrase file." = "无法创建自订语汇档案。"; diff --git a/Source/zh-Hant.lproj/Localizable.strings b/Source/zh-Hant.lproj/Localizable.strings index 2f426952..bdc29ed1 100644 --- a/Source/zh-Hant.lproj/Localizable.strings +++ b/Source/zh-Hant.lproj/Localizable.strings @@ -17,8 +17,8 @@ "Visit Website" = "前往網站"; "You're currently using vChewing %@ (%@), a new version %@ (%@) is now available. Do you want to visit vChewing's website to download the version?%@" = "目前使用的威注音官方版本是 %1$@ (%2$@),網路上有更新版本 %3$@ (%4$@) 可供下載。是否要前往威注音網站下載新版來安裝?%5$@"; "Chinese Conversion" = "優先使用康熙繁體字"; -"NotificationSwitchON" = " 已啟用"; -"NotificationSwitchOFF" = " 已停用"; +"NotificationSwitchON" = "✔ 已啟用"; +"NotificationSwitchOFF" = "✘ 已停用"; "Edit User Phrases" = "編輯自訂語彙"; "Reload User Phrases" = "重載自訂語彙"; "Unable to create the user phrase file." = "無法創建自訂語彙檔案。"; diff --git a/vChewing.xcodeproj/project.pbxproj b/vChewing.xcodeproj/project.pbxproj index b3304943..861ac85c 100644 --- a/vChewing.xcodeproj/project.pbxproj +++ b/vChewing.xcodeproj/project.pbxproj @@ -17,6 +17,8 @@ 5B5F4F972792A4EA00922DC2 /* UserPhrasesLM.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 5B5F4F962792A4EA00922DC2 /* UserPhrasesLM.cpp */; }; 5BC2D2872793B434002C0BEC /* CMakeLists.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5BC2D2852793B434002C0BEC /* CMakeLists.txt */; }; 5BC2D2882793B434002C0BEC /* KeyValueBlobReader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 5BC2D2862793B434002C0BEC /* KeyValueBlobReader.cpp */; }; + 5BC2D28B2793B8FB002C0BEC /* EmacsKeyHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC2D28A2793B8FB002C0BEC /* EmacsKeyHelper.swift */; }; + 5BC2D28D2793B98F002C0BEC /* PreferencesModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC2D28C2793B98F002C0BEC /* PreferencesModule.swift */; }; 5BC3EE1B278FC48C00F5E44C /* VerticalCandidateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC3EE18278FC48C00F5E44C /* VerticalCandidateController.swift */; }; 5BC3EE1C278FC48C00F5E44C /* VTCandidateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC3EE19278FC48C00F5E44C /* VTCandidateController.swift */; }; 5BC3EE1D278FC48C00F5E44C /* HorizontalCandidateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC3EE1A278FC48C00F5E44C /* HorizontalCandidateController.swift */; }; @@ -105,6 +107,8 @@ 5BC2D2842793B434002C0BEC /* KeyValueBlobReader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KeyValueBlobReader.h; sourceTree = ""; }; 5BC2D2852793B434002C0BEC /* CMakeLists.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = CMakeLists.txt; sourceTree = ""; }; 5BC2D2862793B434002C0BEC /* KeyValueBlobReader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = KeyValueBlobReader.cpp; sourceTree = ""; }; + 5BC2D28A2793B8FB002C0BEC /* EmacsKeyHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmacsKeyHelper.swift; sourceTree = ""; }; + 5BC2D28C2793B98F002C0BEC /* PreferencesModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesModule.swift; sourceTree = ""; }; 5BC3EE18278FC48C00F5E44C /* VerticalCandidateController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalCandidateController.swift; sourceTree = ""; }; 5BC3EE19278FC48C00F5E44C /* VTCandidateController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VTCandidateController.swift; sourceTree = ""; }; 5BC3EE1A278FC48C00F5E44C /* HorizontalCandidateController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalCandidateController.swift; sourceTree = ""; }; @@ -272,6 +276,14 @@ path = vChewing; sourceTree = ""; }; + 5BC2D2892793B8DB002C0BEC /* Keyboard */ = { + isa = PBXGroup; + children = ( + 5BC2D28A2793B8FB002C0BEC /* EmacsKeyHelper.swift */, + ); + path = Keyboard; + sourceTree = ""; + }; 5BE798A12792E50F00337FF9 /* UI */ = { isa = PBXGroup; children = ( @@ -336,7 +348,6 @@ 6A0D4F1215FC0EB100ABF4B3 /* Engine */, 6ACA41E715FC1D9000935EF6 /* Installer */, 6A0D4F4715FC0EB900ABF4B3 /* Resources */, - 5BDF2D052791DA6700838ADB /* AppDelegate.swift */, 5BF4A6FB27844738007DC6E7 /* frmAboutWindow.h */, 5BF4A6FC27844738007DC6E7 /* frmAboutWindow.m */, 6A0D4EC615FC0D6400ABF4B3 /* InputMethodController.h */, @@ -345,10 +356,12 @@ 5B5F4F92279294A300922DC2 /* LanguageModelManager.mm */, 6A0D4EC815FC0D6400ABF4B3 /* main.m */, 6A0D4EF615FC0DA600ABF4B3 /* vChewing-Prefix.pch */, - 5BDF2D022791C71200838ADB /* NonModalAlertWindowController.swift */, - 5BDF2D002791C03B00838ADB /* PreferencesWindowController.swift */, - D427A9C025ED28CC005D43E0 /* OpenCCBridge.swift */, + 5BDF2D052791DA6700838ADB /* AppDelegate.swift */, 5BDF2CFD2791BE4400838ADB /* InputSourceHelper.swift */, + 5BDF2D022791C71200838ADB /* NonModalAlertWindowController.swift */, + D427A9C025ED28CC005D43E0 /* OpenCCBridge.swift */, + 5BC2D28C2793B98F002C0BEC /* PreferencesModule.swift */, + 5BDF2D002791C03B00838ADB /* PreferencesWindowController.swift */, D427A9BF25ED28CC005D43E0 /* vChewing-Bridging-Header.h */, 5BA923AC2791B7C20001323A /* vChewingInstaller-Bridging-Header.h */, ); @@ -380,6 +393,7 @@ 6A0D4F1215FC0EB100ABF4B3 /* Engine */ = { isa = PBXGroup; children = ( + 5BC2D2892793B8DB002C0BEC /* Keyboard */, 5BC2D2832793B434002C0BEC /* vChewing */, 5BA8DAFE27928120009C9FFF /* LanguageModel */, 6A0D4F1315FC0EB100ABF4B3 /* Gramambular */, @@ -688,6 +702,7 @@ buildActionMask = 2147483647; files = ( 5BDF2CFE2791BE4400838ADB /* InputSourceHelper.swift in Sources */, + 5BC2D28B2793B8FB002C0BEC /* EmacsKeyHelper.swift in Sources */, 5BE798A72793280C00337FF9 /* NotifierController.swift in Sources */, 5B5F4F93279294A300922DC2 /* LanguageModelManager.mm in Sources */, 6A0D4ED215FC0D6400ABF4B3 /* InputMethodController.mm in Sources */, @@ -704,6 +719,7 @@ 6A0D4F4515FC0EB100ABF4B3 /* Mandarin.cpp in Sources */, 6A0421A815FEF3F50061ED63 /* FastLM.cpp in Sources */, 5BDF2D012791C03B00838ADB /* PreferencesWindowController.swift in Sources */, + 5BC2D28D2793B98F002C0BEC /* PreferencesModule.swift in Sources */, 5BC3EE1C278FC48C00F5E44C /* VTCandidateController.swift in Sources */, 5BDF2D032791C71200838ADB /* NonModalAlertWindowController.swift in Sources */, 5BC3EE1D278FC48C00F5E44C /* HorizontalCandidateController.swift in Sources */,