添加变体
This commit is contained in:
80
CustomKeyboard/Resource/kb_diacritics_map.json
Normal file
80
CustomKeyboard/Resource/kb_diacritics_map.json
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"__comment": "长按字符变体映射:languages.<lang>.<baseChar> = 变体数组(第一个建议为 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", "ü", "ǖ", "ǘ", "ǚ", "ǜ"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,215 @@
|
||||
#import "KBKeyboardLegacyLayoutProvider.h"
|
||||
#import "KBKeyboardInteractionHandler.h"
|
||||
#import "KBKeyboardRowContainerBuilder.h"
|
||||
#import "KBSkinManager.h"
|
||||
#import <Masonry/Masonry.h>
|
||||
#import <objc/runtime.h>
|
||||
|
||||
// 第二行字母行的左右占位比例(用于居中)
|
||||
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<NSString *, NSDictionary<NSString *, NSArray<NSString *> *> *> *kb_diacriticsLanguagesMap(void) {
|
||||
static NSDictionary<NSString *, NSDictionary<NSString *, NSArray<NSString *> *> *> *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<NSString *> *variants;
|
||||
@property (nonatomic, assign) NSInteger selectedIndex;
|
||||
- (void)configureWithVariants:(NSArray<NSString *> *)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<UILabel *> *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<UILabel *> *)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<NSString *> *)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<UIView *> *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<NSString *, NSDictionary<NSString *, NSArray<NSString *> *> *> *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<KBKeyButton *> *buttons = [self.interactionHandler collectKeyButtonsInView:row];
|
||||
for (KBKeyButton *btn in buttons) {
|
||||
if (![btn isKindOfClass:[KBKeyButton class]]) { continue; }
|
||||
if (btn.key.type != KBKeyTypeCharacter) { continue; }
|
||||
NSArray<NSString *> *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<NSString *> *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<NSString *> *)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<NSString *, NSDictionary<NSString *, NSArray<NSString *> *> *> *languages = kb_diacriticsLanguagesMap();
|
||||
NSString *lc = kb_normalizeLanguageCode(languageCode);
|
||||
NSDictionary<NSString *, NSArray<NSString *> *> *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<NSString *, NSArray<NSString *> *> *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<NSString *> *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<NSString *> *upperVariants = [NSMutableArray arrayWithCapacity:variants.count];
|
||||
for (NSString *s in variants) {
|
||||
[upperVariants addObject:s.uppercaseString];
|
||||
}
|
||||
return upperVariants.copy;
|
||||
}
|
||||
|
||||
#pragma mark - Lazy
|
||||
|
||||
- (NSMutableArray<UIView *> *)rowViews {
|
||||
|
||||
@@ -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 = "<group>"; };
|
||||
B7F1A1E42F90000100000001 /* indonesian_words.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = indonesian_words.json; sourceTree = "<group>"; };
|
||||
B7F1A1E62F90000100000001 /* english_words.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = english_words.json; sourceTree = "<group>"; };
|
||||
B7F1A1F22FA0000100000001 /* kb_diacritics_map.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = kb_diacritics_map.json; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
B9F60894E529C3EDAF6BAC3D /* KBShopThemeDetailModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBShopThemeDetailModel.m; sourceTree = "<group>"; };
|
||||
E2A844CD2D8584596DBE6316 /* KBShopThemeTagModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBShopThemeTagModel.m; sourceTree = "<group>"; };
|
||||
@@ -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 */,
|
||||
|
||||
Reference in New Issue
Block a user