diff --git a/CustomKeyboard/Resource/kb_diacritics_map.json b/CustomKeyboard/Resource/kb_diacritics_map.json new file mode 100644 index 0000000..0d243fb --- /dev/null +++ b/CustomKeyboard/Resource/kb_diacritics_map.json @@ -0,0 +1,80 @@ +{ + "__comment": "长按字符变体映射:languages.. = 变体数组(第一个建议为 baseChar 本身)。默认只配置小写;大写由代码自动派生。", + "languages": { + "common": { + "__comment": "通用符号长按变体(适用于所有语言)。如需语言特化(西语 ¿/¡ 等),在对应语言下覆盖同名 key 即可。", + "-": ["-", "–", "—", "−"], + "/": ["/", "\\"], + ":": [":", ":"], + ";": [";", ";"], + "(": ["(", "(", "[", "{", "<"], + ")": [")", ")", "]", "}", ">"], + ".": [".", "…", "..."], + ",": [",", ","], + "\"": ["\"", "“", "”"], + "“": ["“", "”", "\""], + "'": ["'", "‘", "’"], + "‘": ["‘", "’", "'"], + "?": ["?", "?"], + "!": ["!", "!"], + "_": ["_", "—"], + "\\": ["\\", "|"], + "|": ["|", "¦"], + "~": ["~", "~"], + "<": ["<", "«", "‹"], + ">": [">", "»", "›"], + "#": ["#", "№"], + "%": ["%", "‰"], + "*": ["*", "•", "·"], + "+": ["+", "±"], + "=": ["=", "≠", "≈"], + "·": ["·", "•"], + "$": ["$", "€", "£", "¥", "₩"], + "€": ["€", "$", "£", "¥"], + "¥": ["¥", "¥", "$", "€", "£"], + "¥": ["¥", "¥", "$", "€", "£"], + "0": ["0", "°"], + "1": ["1", "¹"], + "2": ["2", "²"], + "3": ["3", "³"] + }, + "en": { + "__comment": "英文(通用拉丁增强):用于输入外来词/人名等。仅配置小写;大写自动派生。", + "a": ["a", "à", "á", "â", "ä", "æ", "ã", "å", "ā"], + "c": ["c", "ç"], + "e": ["e", "è", "é", "ê", "ë", "ē", "ė", "ę"], + "i": ["i", "ì", "í", "î", "ï", "ī", "į"], + "n": ["n", "ñ"], + "o": ["o", "ò", "ó", "ô", "ö", "œ", "õ", "ø", "ō"], + "u": ["u", "ù", "ú", "û", "ü", "ū"], + "y": ["y", "ÿ"] + }, + "pt": { + "a": ["a", "á", "à", "â", "ã", "ä"], + "e": ["e", "é", "è", "ê", "ë"], + "i": ["i", "í", "ì", "î", "ï"], + "o": ["o", "ó", "ò", "ô", "õ", "ö"], + "u": ["u", "ú", "ù", "û", "ü"], + "c": ["c", "ç"] + }, + "es": { + "a": ["a", "á"], + "e": ["e", "é"], + "i": ["i", "í"], + "o": ["o", "ó"], + "u": ["u", "ú", "ü"], + "n": ["n", "ñ"], + "?": ["?", "¿"], + "!": ["!", "¡"] + }, + "zh-hant-pinyin": { + "__comment": "繁体拼音:长按元音输出声调字符;v 用于 ü / ǖǘǚǜ(常见拼音输入习惯)", + "a": ["a", "ā", "á", "ǎ", "à"], + "e": ["e", "ē", "é", "ě", "è"], + "i": ["i", "ī", "í", "ǐ", "ì"], + "o": ["o", "ō", "ó", "ǒ", "ò"], + "u": ["u", "ū", "ú", "ǔ", "ù", "ü"], + "v": ["v", "ü", "ǖ", "ǘ", "ǚ", "ǜ"] + } + } +} diff --git a/CustomKeyboard/View/KBKeyboardView/KBKeyboardView.m b/CustomKeyboard/View/KBKeyboardView/KBKeyboardView.m index e574bc3..e682b64 100644 --- a/CustomKeyboard/View/KBKeyboardView/KBKeyboardView.m +++ b/CustomKeyboard/View/KBKeyboardView/KBKeyboardView.m @@ -17,9 +17,215 @@ #import "KBKeyboardLegacyLayoutProvider.h" #import "KBKeyboardInteractionHandler.h" #import "KBKeyboardRowContainerBuilder.h" +#import "KBSkinManager.h" +#import +#import // 第二行字母行的左右占位比例(用于居中) static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5; +static const NSTimeInterval kKBKeyVariantLongPressMinDuration = 0.35; +static const CGFloat kKBKeyVariantPopupPaddingX = 8.0; +static const CGFloat kKBKeyVariantPopupPaddingY = 6.0; +static const CGFloat kKBKeyVariantItemWidth = 34.0; +static const CGFloat kKBKeyVariantItemHeight = 40.0; +static const CGFloat kKBKeyVariantPopupAnchorGap = 6.0; +static NSString * const kKBDiacriticsConfigFileName = @"kb_diacritics_map"; +static const void *kKBDiacriticsLongPressBoundKey = &kKBDiacriticsLongPressBoundKey; + +static inline NSString *kb_normalizeLanguageCode(NSString *languageCode) { + NSString *lc = (languageCode ?: @"").lowercaseString; + return lc.length > 0 ? lc : @"en"; +} + +static inline NSString *kb_baseLanguageCode(NSString *languageCode) { + NSString *lc = kb_normalizeLanguageCode(languageCode); + NSRange r = [lc rangeOfString:@"-"]; + if (r.location == NSNotFound) { return lc; } + if (r.location == 0) { return lc; } + return [lc substringToIndex:r.location]; +} + +static NSDictionary *> *> *kb_diacriticsLanguagesMap(void) { + static NSDictionary *> *> *cached = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSString *path = [[NSBundle mainBundle] pathForResource:kKBDiacriticsConfigFileName ofType:@"json"]; + NSData *data = path.length ? [NSData dataWithContentsOfFile:path] : nil; + if (data.length == 0) { return; } + + NSError *error = nil; + id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + if (error || ![json isKindOfClass:[NSDictionary class]]) { + NSLog(@"[KBKeyboardView] Failed to parse %@.json: %@", kKBDiacriticsConfigFileName, error); + return; + } + + NSDictionary *dict = (NSDictionary *)json; + id languages = dict[@"languages"]; + if (![languages isKindOfClass:[NSDictionary class]]) { return; } + cached = (NSDictionary *)languages; + }); + return cached ?: @{}; +} + +static inline NSUInteger kb_composedCharacterCount(NSString *string) { + if (string.length == 0) { return 0; } + __block NSUInteger count = 0; + [string enumerateSubstringsInRange:NSMakeRange(0, string.length) + options:NSStringEnumerationByComposedCharacterSequences + usingBlock:^(__unused NSString *substring, + __unused NSRange substringRange, + __unused NSRange enclosingRange, + __unused BOOL *stop) { + count += 1; + }]; + return count; +} + +@interface KBKeyVariantPopupView : UIView +@property (nonatomic, copy) NSArray *variants; +@property (nonatomic, assign) NSInteger selectedIndex; +- (void)configureWithVariants:(NSArray *)variants selectedIndex:(NSInteger)selectedIndex; +- (void)updateSelectionWithPointInSelf:(CGPoint)point; +- (nullable NSString *)selectedVariant; +- (CGSize)preferredSize; +- (void)applyTheme; +@end + +#pragma mark - KBKeyVariantPopupView + +@interface KBKeyVariantPopupView () +@property (nonatomic, strong) UIView *contentView; +@property (nonatomic, strong) NSMutableArray *itemLabels; +@end + +@implementation KBKeyVariantPopupView + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + self.layer.cornerRadius = 10.0; + self.layer.masksToBounds = NO; + self.layer.borderWidth = 0.5; + self.layer.borderColor = [UIColor colorWithWhite:0 alpha:0.15].CGColor; + self.layer.shadowColor = [UIColor colorWithWhite:0 alpha:0.28].CGColor; + self.layer.shadowOpacity = 0.7; + self.layer.shadowOffset = CGSizeMake(0, 2); + self.layer.shadowRadius = 6.0; + + [self addSubview:self.contentView]; + [self.contentView mas_makeConstraints:^(MASConstraintMaker *make) { + make.edges.equalTo(self).insets(UIEdgeInsetsMake(kKBKeyVariantPopupPaddingY, + kKBKeyVariantPopupPaddingX, + kKBKeyVariantPopupPaddingY, + kKBKeyVariantPopupPaddingX)); + }]; + + [self applyTheme]; + } + return self; +} + +- (void)applyTheme { + KBSkinTheme *t = [KBSkinManager shared].current; + UIColor *bg = t.keyBackground ?: [UIColor whiteColor]; + self.backgroundColor = bg; +} + +- (UIView *)contentView { + if (!_contentView) { + _contentView = [UIView new]; + _contentView.backgroundColor = [UIColor clearColor]; + } + return _contentView; +} + +- (NSMutableArray *)itemLabels { + if (!_itemLabels) { + _itemLabels = [NSMutableArray array]; + } + return _itemLabels; +} + +- (CGSize)preferredSize { + NSUInteger count = self.variants.count; + if (count == 0) { return CGSizeMake(0, 0); } + CGFloat width = kKBKeyVariantPopupPaddingX * 2 + kKBKeyVariantItemWidth * (CGFloat)count; + CGFloat height = kKBKeyVariantPopupPaddingY * 2 + kKBKeyVariantItemHeight; + return CGSizeMake(width, height); +} + +- (void)configureWithVariants:(NSArray *)variants selectedIndex:(NSInteger)selectedIndex { + self.variants = variants ?: @[]; + self.selectedIndex = MAX(0, MIN(selectedIndex, (NSInteger)self.variants.count - 1)); + + [self.itemLabels makeObjectsPerformSelector:@selector(removeFromSuperview)]; + [self.itemLabels removeAllObjects]; + + UILabel *previous = nil; + for (NSInteger i = 0; i < (NSInteger)self.variants.count; i++) { + UILabel *label = [UILabel new]; + label.textAlignment = NSTextAlignmentCenter; + label.text = self.variants[i]; + label.font = [UIFont systemFontOfSize:20 weight:UIFontWeightSemibold]; + label.layer.cornerRadius = 8.0; + label.layer.masksToBounds = YES; + + [self.contentView addSubview:label]; + [self.itemLabels addObject:label]; + + [label mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.bottom.equalTo(self.contentView); + make.width.mas_equalTo(kKBKeyVariantItemWidth); + if (previous) { + make.left.equalTo(previous.mas_right); + } else { + make.left.equalTo(self.contentView.mas_left); + } + }]; + previous = label; + } + + if (previous) { + [previous mas_makeConstraints:^(MASConstraintMaker *make) { + make.right.equalTo(self.contentView.mas_right); + }]; + } + + [self kb_refreshSelectionAppearance]; +} + +- (void)updateSelectionWithPointInSelf:(CGPoint)point { + if (self.variants.count == 0) { return; } + CGPoint p = [self convertPoint:point toView:self.contentView]; + CGFloat contentWidth = CGRectGetWidth(self.contentView.bounds); + if (contentWidth <= 0) { return; } + + NSInteger count = (NSInteger)self.variants.count; + CGFloat unit = contentWidth / (CGFloat)count; + NSInteger index = (NSInteger)floor(p.x / MAX(unit, 1.0)); + index = MAX(0, MIN(index, count - 1)); + if (index == self.selectedIndex) { return; } + self.selectedIndex = index; + [self kb_refreshSelectionAppearance]; +} + +- (nullable NSString *)selectedVariant { + if (self.selectedIndex < 0 || self.selectedIndex >= (NSInteger)self.variants.count) { return nil; } + return self.variants[self.selectedIndex]; +} + +- (void)kb_refreshSelectionAppearance { + KBSkinTheme *t = [KBSkinManager shared].current; + UIColor *textColor = t.keyTextColor ?: [UIColor blackColor]; + UIColor *selectedBg = t.keyHighlightBackground ?: (t.accentColor ?: [UIColor colorWithWhite:0 alpha:0.12]); + for (NSInteger i = 0; i < (NSInteger)self.itemLabels.count; i++) { + UILabel *label = self.itemLabels[i]; + label.textColor = textColor; + label.backgroundColor = (i == self.selectedIndex) ? selectedBg : [UIColor clearColor]; + } +} + +@end @interface KBKeyboardView () @property (nonatomic, strong) NSMutableArray *rowViews; @@ -41,6 +247,14 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5; /// 记录当前顶/底间距,便于切换布局时判断是否需要重建容器 @property (nonatomic, assign) CGFloat kb_currentTopInset; @property (nonatomic, assign) CGFloat kb_currentBottomInset; + +/// 长按字符变体(如葡语重音字符) +@property (nonatomic, strong) KBKeyVariantPopupView *kb_variantPopupView; +@property (nonatomic, weak) KBKeyButton *kb_variantAnchorButton; +@property (nonatomic, copy) NSString *kb_variantBaseOutput; +@property (nonatomic, assign) BOOL kb_variantBaseDeleted; +@property (nonatomic, assign) NSUInteger kb_variantBaseDeleteCount; +@property (nonatomic, copy) NSString *kb_currentLanguageCode; @end @implementation KBKeyboardView @@ -107,6 +321,8 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5; - (void)reloadKeys { [self.backspaceHandler bindDeleteButton:nil showClearLabel:NO]; + [self kb_hideVariantPopupIfNeeded]; + self.kb_currentLanguageCode = [[KBKeyboardLayoutResolver sharedResolver] currentLanguageCode] ?: @"en"; KBKeyboardLayout *layout = [self kb_currentLayout]; CGFloat rowSpacing = [self.layoutEngine rowSpacingForLayout:layout]; @@ -153,6 +369,7 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5; NSLog(@"[KBKeyboardView] reloadKeys: rows.count < 4, fallback to legacy"); self.kb_uniformCharKeyWidth = 0.0; [self kb_buildLegacyLayout]; + [self kb_bindVariantLongPressIfNeeded]; return; } @@ -179,7 +396,11 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5; [row.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; } [self kb_buildLegacyLayout]; + [self kb_bindVariantLongPressIfNeeded]; + return; } + + [self kb_bindVariantLongPressIfNeeded]; } - (void)didMoveToWindow { @@ -404,6 +625,219 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5; [self.interactionHandler hidePreview]; } +#pragma mark - Key Variant (Diacritics) + +- (void)kb_hideVariantPopupIfNeeded { + if (!self.kb_variantPopupView || self.kb_variantPopupView.hidden) { return; } + self.kb_variantPopupView.hidden = YES; + self.kb_variantPopupView.alpha = 1.0; + self.kb_variantAnchorButton = nil; + self.kb_variantBaseOutput = nil; + self.kb_variantBaseDeleted = NO; + self.kb_variantBaseDeleteCount = 0; +} + +- (void)kb_bindVariantLongPressIfNeeded { + NSString *lc = kb_normalizeLanguageCode(self.kb_currentLanguageCode); + NSString *baseLc = kb_baseLanguageCode(self.kb_currentLanguageCode); + NSDictionary *> *> *languages = kb_diacriticsLanguagesMap(); + if (languages.count == 0) { return; } + NSDictionary *commonMap = [languages[@"common"] isKindOfClass:[NSDictionary class]] ? languages[@"common"] : nil; + if (!commonMap && ![languages[lc] isKindOfClass:[NSDictionary class]] && ![languages[baseLc] isKindOfClass:[NSDictionary class]]) { return; } + + for (UIView *row in self.rowViews) { + NSArray *buttons = [self.interactionHandler collectKeyButtonsInView:row]; + for (KBKeyButton *btn in buttons) { + if (![btn isKindOfClass:[KBKeyButton class]]) { continue; } + if (btn.key.type != KBKeyTypeCharacter) { continue; } + NSArray *variants = [self kb_variantsForKey:btn.key languageCode:lc]; + if (variants.count <= 1) { continue; } + + NSNumber *bound = objc_getAssociatedObject(btn, kKBDiacriticsLongPressBoundKey); + if (bound.boolValue) { continue; } + objc_setAssociatedObject(btn, kKBDiacriticsLongPressBoundKey, @(YES), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self + action:@selector(onKeyVariantLongPress:)]; + longPress.minimumPressDuration = kKBKeyVariantLongPressMinDuration; + longPress.allowableMovement = CGFLOAT_MAX; + longPress.cancelsTouchesInView = NO; + [btn addGestureRecognizer:longPress]; + } + } +} + +- (void)onKeyVariantLongPress:(UILongPressGestureRecognizer *)gr { + KBKeyButton *button = (KBKeyButton *)gr.view; + if (![button isKindOfClass:[KBKeyButton class]]) { return; } + if (button.key.type != KBKeyTypeCharacter) { return; } + + NSString *lc = kb_normalizeLanguageCode(self.kb_currentLanguageCode); + NSArray *variants = [self kb_variantsForKey:button.key languageCode:lc]; + if (variants.count <= 1) { return; } + + switch (gr.state) { + case UIGestureRecognizerStateBegan: { + [self hidePreview]; + self.kb_variantAnchorButton = button; + self.kb_variantBaseOutput = (button.key.output.length > 0 ? button.key.output : button.key.title) ?: @""; + self.kb_variantBaseDeleted = NO; + self.kb_variantBaseDeleteCount = kb_composedCharacterCount(self.kb_variantBaseOutput); + + if (!self.kb_variantPopupView) { + self.kb_variantPopupView = [[KBKeyVariantPopupView alloc] initWithFrame:CGRectZero]; + self.kb_variantPopupView.hidden = YES; + [self addSubview:self.kb_variantPopupView]; + } else if (self.kb_variantPopupView.superview != self) { + [self addSubview:self.kb_variantPopupView]; + } + [self.kb_variantPopupView applyTheme]; + [self.kb_variantPopupView configureWithVariants:variants selectedIndex:0]; + + CGSize preferred = [self.kb_variantPopupView preferredSize]; + CGRect anchorFrame = [button convertRect:button.bounds toView:self]; + CGFloat x = CGRectGetMidX(anchorFrame) - preferred.width * 0.5; + CGFloat maxX = CGRectGetWidth(self.bounds) - 6.0 - preferred.width; + x = MIN(MAX(6.0, x), MAX(6.0, maxX)); + CGFloat y = CGRectGetMinY(anchorFrame) - preferred.height - kKBKeyVariantPopupAnchorGap; + y = MAX(2.0, y); + self.kb_variantPopupView.frame = CGRectMake(x, y, preferred.width, preferred.height); + + self.kb_variantPopupView.alpha = 0.0; + self.kb_variantPopupView.hidden = NO; + [self bringSubviewToFront:self.kb_variantPopupView]; + [UIView animateWithDuration:0.12 + delay:0 + options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseOut + animations:^{ + self.kb_variantPopupView.alpha = 1.0; + } + completion:nil]; + } break; + case UIGestureRecognizerStateChanged: { + if (self.kb_variantPopupView.hidden) { return; } + CGPoint p = [gr locationInView:self.kb_variantPopupView]; + [self.kb_variantPopupView updateSelectionWithPointInSelf:p]; + if (!self.kb_variantBaseDeleted && self.kb_variantPopupView.selectedIndex != 0) { + [self kb_emitBackspaceCountForVariantReplacement:self.kb_variantBaseDeleteCount]; + self.kb_variantBaseDeleted = YES; + } + } break; + case UIGestureRecognizerStateEnded: { + if (self.kb_variantPopupView.hidden) { return; } + CGPoint p = [gr locationInView:self.kb_variantPopupView]; + [self.kb_variantPopupView updateSelectionWithPointInSelf:p]; + [self kb_commitSelectedVariantAndCleanupWithIdentifier:button.key.identifier]; + } break; + case UIGestureRecognizerStateCancelled: + case UIGestureRecognizerStateFailed: { + if (self.kb_variantPopupView.hidden) { return; } + // 取消时尽量保持“原本已经插入的 base 字符”不变:若已删除则补回 + if (self.kb_variantBaseDeleted && self.kb_variantBaseOutput.length > 0) { + [self kb_emitCharacterText:self.kb_variantBaseOutput identifier:button.key.identifier]; + } + [self kb_hideVariantPopupIfNeeded]; + } break; + default: + break; + } +} + +- (void)kb_emitBackspaceCountForVariantReplacement:(NSUInteger)count { + if (count == 0) { count = 1; } + if (![self.delegate respondsToSelector:@selector(keyboardView:didTapKey:)]) { return; } + KBKey *backspace = [KBKey keyWithTitle:@"" type:KBKeyTypeBackspace]; + for (NSUInteger i = 0; i < count; i++) { + [self.delegate keyboardView:self didTapKey:backspace]; + } +} + +- (void)kb_emitCharacterText:(NSString *)text identifier:(NSString *)identifier { + if (text.length == 0) { return; } + if (![self.delegate respondsToSelector:@selector(keyboardView:didTapKey:)]) { return; } + KBKey *key = [KBKey keyWithIdentifier:identifier title:text output:text type:KBKeyTypeCharacter]; + [self.delegate keyboardView:self didTapKey:key]; +} + +- (void)kb_commitSelectedVariantAndCleanupWithIdentifier:(NSString *)identifier { + NSInteger index = self.kb_variantPopupView.selectedIndex; + NSString *selected = [self.kb_variantPopupView selectedVariant] ?: @""; + NSString *base = self.kb_variantBaseOutput ?: @""; + + if (index <= 0 || selected.length == 0) { + if (self.kb_variantBaseDeleted && base.length > 0) { + [self kb_emitCharacterText:base identifier:identifier]; + } + [self kb_hideVariantPopupIfNeeded]; + return; + } + + if (!self.kb_variantBaseDeleted) { + [self kb_emitBackspaceCountForVariantReplacement:self.kb_variantBaseDeleteCount]; + self.kb_variantBaseDeleted = YES; + } + [self kb_emitCharacterText:selected identifier:identifier]; + [self kb_hideVariantPopupIfNeeded]; +} + +- (NSArray *)kb_variantsForKey:(KBKey *)key languageCode:(NSString *)languageCode { + if (!key || key.type != KBKeyTypeCharacter) { return @[]; } + NSString *base = (key.output.length > 0 ? key.output : key.title) ?: @""; + if (base.length == 0) { return @[]; } + + NSDictionary *> *> *languages = kb_diacriticsLanguagesMap(); + NSString *lc = kb_normalizeLanguageCode(languageCode); + NSDictionary *> *langMap = [languages[lc] isKindOfClass:[NSDictionary class]] ? languages[lc] : nil; + if (langMap.count == 0) { + NSString *baseLc = kb_baseLanguageCode(languageCode); + langMap = [languages[baseLc] isKindOfClass:[NSDictionary class]] ? languages[baseLc] : nil; + } + NSDictionary *> *commonMap = [languages[@"common"] isKindOfClass:[NSDictionary class]] ? languages[@"common"] : nil; + if (langMap.count == 0 && commonMap.count == 0) { return @[]; } + + BOOL hasLetter = NO; + NSCharacterSet *letters = [NSCharacterSet letterCharacterSet]; + for (NSUInteger i = 0; i < base.length; i++) { + unichar c = [base characterAtIndex:i]; + if ([letters characterIsMember:c]) { + hasLetter = YES; + break; + } + } + BOOL upper = hasLetter && [base isEqualToString:base.uppercaseString] && ![base isEqualToString:base.lowercaseString]; + NSString *queryKey = upper ? base.lowercaseString : base; + + id raw = langMap[queryKey]; + if (!(raw && [raw isKindOfClass:[NSArray class]] && ((NSArray *)raw).count > 0)) { + raw = commonMap[queryKey]; + } + if (![raw isKindOfClass:[NSArray class]]) { return @[]; } + + NSMutableArray *variants = [NSMutableArray array]; + for (id obj in (NSArray *)raw) { + if (![obj isKindOfClass:[NSString class]]) { continue; } + NSString *s = (NSString *)obj; + if (s.length == 0) { continue; } + [variants addObject:s]; + } + + if (variants.count == 0) { return @[]; } + if (![[variants firstObject] isEqualToString:queryKey]) { + [variants insertObject:queryKey atIndex:0]; + } + if (variants.count <= 1) { return @[]; } + + if (!upper) { + return variants.copy; + } + + NSMutableArray *upperVariants = [NSMutableArray arrayWithCapacity:variants.count]; + for (NSString *s in variants) { + [upperVariants addObject:s.uppercaseString]; + } + return upperVariants.copy; +} + #pragma mark - Lazy - (NSMutableArray *)rowViews { diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index df4ddf0..9cf503a 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -336,6 +336,7 @@ B7F1A1E12F90000100000001 /* portuguese_words.json in Resources */ = {isa = PBXBuildFile; fileRef = B7F1A1E32F90000100000001 /* portuguese_words.json */; }; B7F1A1E22F90000100000001 /* indonesian_words.json in Resources */ = {isa = PBXBuildFile; fileRef = B7F1A1E42F90000100000001 /* indonesian_words.json */; }; B7F1A1E52F90000100000001 /* english_words.json in Resources */ = {isa = PBXBuildFile; fileRef = B7F1A1E62F90000100000001 /* english_words.json */; }; + B7F1A1F32FA0000100000001 /* kb_diacritics_map.json in Resources */ = {isa = PBXBuildFile; fileRef = B7F1A1F22FA0000100000001 /* kb_diacritics_map.json */; }; EB72B60040437E3C0A4890FC /* KBShopThemeDetailModel.m in Sources */ = {isa = PBXBuildFile; fileRef = B9F60894E529C3EDAF6BAC3D /* KBShopThemeDetailModel.m */; }; ECC9EE02174D86E8D792472F /* Pods_keyBoard.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 967065BB5230E43F293B3AF9 /* Pods_keyBoard.framework */; }; /* End PBXBuildFile section */ @@ -943,6 +944,7 @@ B7F1A1E32F90000100000001 /* portuguese_words.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = portuguese_words.json; sourceTree = ""; }; B7F1A1E42F90000100000001 /* indonesian_words.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = indonesian_words.json; sourceTree = ""; }; B7F1A1E62F90000100000001 /* english_words.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = english_words.json; sourceTree = ""; }; + B7F1A1F22FA0000100000001 /* kb_diacritics_map.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = kb_diacritics_map.json; sourceTree = ""; }; B8CA018AB878499327504AAD /* Pods-CustomKeyboard.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CustomKeyboard.debug.xcconfig"; path = "Target Support Files/Pods-CustomKeyboard/Pods-CustomKeyboard.debug.xcconfig"; sourceTree = ""; }; B9F60894E529C3EDAF6BAC3D /* KBShopThemeDetailModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBShopThemeDetailModel.m; sourceTree = ""; }; E2A844CD2D8584596DBE6316 /* KBShopThemeTagModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBShopThemeTagModel.m; sourceTree = ""; }; @@ -991,6 +993,7 @@ A1B2C3EC2F20000000000001 /* kb_words.txt */, A1B2C3F02F20000000000002 /* kb_keyboard_layout_config.json */, 043213A52F5561FD0065C888 /* kb_keyboard_layouts_i18n.json */, + B7F1A1F22FA0000100000001 /* kb_diacritics_map.json */, 0498BDF42EEC50EE006CC1D5 /* emoji_categories.json */, 041007D12ECE012000D203BB /* KBSkinIconMap.strings */, 043213AB2F556DF80065C888 /* KBSkinIconMap_es.strings */, @@ -2395,6 +2398,7 @@ A1B2C3ED2F20000000000001 /* kb_words.txt in Resources */, A1B2C3F12F20000000000002 /* kb_keyboard_layout_config.json in Resources */, 0498BDF52EEC50EE006CC1D5 /* emoji_categories.json in Resources */, + B7F1A1F32FA0000100000001 /* kb_diacritics_map.json in Resources */, 043213AF2F556DF80065C888 /* KBSkinIconMap_pt.strings in Resources */, 043213B02F556DF80065C888 /* KBSkinIconMap_id.strings in Resources */, 043213B12F556DF80065C888 /* KBSkinIconMap_es.strings in Resources */,