// // KBKeyboardView.m // CustomKeyboard // #import "KBKeyboardView.h" #import "KBKeyButton.h" #import "KBKey.h" #import "KBBackspaceLongPressHandler.h" #import "KBKeyboardLayoutConfig.h" #import "KBKeyboardLayoutResolver.h" #import "KBKeyboardKeyFactory.h" #import "KBKeyboardLayoutEngine.h" #import "KBKeyboardRowBuilder.h" #import "KBKeyboardInputHandler.h" #import "KBKeyboardLegacyBuilder.h" #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; @property (nonatomic, strong) NSArray *> *keysForRows; @property (nonatomic, strong) KBBackspaceLongPressHandler *backspaceHandler; @property (nonatomic, strong) KBKeyboardLayoutConfig *layoutConfig; @property (nonatomic, strong) KBKeyboardKeyFactory *keyFactory; @property (nonatomic, strong) KBKeyboardLayoutEngine *layoutEngine; @property (nonatomic, strong) KBKeyboardRowBuilder *rowBuilder; @property (nonatomic, strong) KBKeyboardInputHandler *inputHandler; @property (nonatomic, strong) KBKeyboardLegacyBuilder *legacyBuilder; @property (nonatomic, strong) KBKeyboardLegacyLayoutProvider *legacyLayoutProvider; @property (nonatomic, strong) KBKeyboardInteractionHandler *interactionHandler; @property (nonatomic, strong) KBKeyboardRowContainerBuilder *rowContainerBuilder; /// 跨行统一字符键宽度(按最多字符键的行计算),0 表示不启用 @property (nonatomic, assign) CGFloat kb_uniformCharKeyWidth; /// 记录当前行间距,便于切换布局时判断是否需要重建容器 @property (nonatomic, assign) CGFloat kb_currentRowSpacing; /// 记录当前顶/底间距,便于切换布局时判断是否需要重建容器 @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 - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { self.backgroundColor = [UIColor clearColor]; _layoutStyle = KBKeyboardLayoutStyleLetters; // 默认小写:与需求一致,初始不开启 Shift _shiftOn = NO; _symbolsMoreOn = NO; // 数字面板默认第一页(123) // 从 App Group 读取当前 profileId 并设置布局 NSString *profileId = [[KBKeyboardLayoutResolver sharedResolver] currentProfileId]; if (profileId.length > 0) { _currentLayoutJsonId = [[KBKeyboardLayoutResolver sharedResolver] layoutJsonIdForProfileId:profileId]; NSLog(@"[KBKeyboardView] Loaded profileId: %@, layoutJsonId: %@", profileId, _currentLayoutJsonId); } else { _currentLayoutJsonId = @"letters"; NSLog(@"[KBKeyboardView] No profileId found, using default 'letters'"); } self.layoutConfig = [KBKeyboardLayoutConfig sharedConfig]; self.backspaceHandler = [[KBBackspaceLongPressHandler alloc] initWithContainerView:self]; [self buildBase]; } return self; } // 当切换大布局(字母/数字)时,重置数字二级页状态 - (void)setLayoutStyle:(KBKeyboardLayoutStyle)layoutStyle { _layoutStyle = layoutStyle; if (_layoutStyle != KBKeyboardLayoutStyleNumbers) { _symbolsMoreOn = NO; } } #pragma mark - Base Layout - (void)buildBase { KBKeyboardLayout *layout = [self kb_currentLayout]; NSArray *rows = layout.rows ?: @[]; if (rows.count == 0) { // Fallback: 至少创建 4 行容器 rows = @[[KBKeyboardRowConfig new], [KBKeyboardRowConfig new], [KBKeyboardRowConfig new], [KBKeyboardRowConfig new]]; } CGFloat rowSpacing = [self.layoutEngine rowSpacingForLayout:layout]; CGFloat topInset = [self.layoutEngine topInsetForLayout:layout]; CGFloat bottomInset = [self.layoutEngine bottomInsetForLayout:layout]; self.kb_currentRowSpacing = rowSpacing; self.kb_currentTopInset = topInset; self.kb_currentBottomInset = bottomInset; [self.rowContainerBuilder rebuildRowContainersForRows:rows inContainer:self rowViews:self.rowViews rowSpacing:rowSpacing topInset:topInset bottomInset:bottomInset]; [self.interactionHandler bringPreviewToFrontIfNeededInContainer:self]; } #pragma mark - Public - (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]; CGFloat topInset = [self.layoutEngine topInsetForLayout:layout]; CGFloat bottomInset = [self.layoutEngine bottomInsetForLayout:layout]; NSLog(@"[KBKeyboardView] reloadKeys: layoutName=%@ rows=%lu shiftRows=%lu shiftOn=%d", self.currentLayoutJsonId, (unsigned long)layout.rows.count, (unsigned long)layout.shiftRows.count, self.shiftOn); NSArray *rows = nil; if (self.shiftOn && layout.shiftRows.count > 0) { rows = layout.shiftRows; } else { rows = layout.rows ?: @[]; } NSLog(@"[KBKeyboardView] reloadKeys: usingRows=%lu currentContainers=%lu", (unsigned long)rows.count, (unsigned long)self.rowViews.count); // 行数变化时(如从 4 行布局切到 5 行注音布局),重建行容器 if (rows.count >= 4 && (rows.count != self.rowViews.count || fabs(self.kb_currentRowSpacing - rowSpacing) > 0.1 || fabs(self.kb_currentTopInset - topInset) > 0.1 || fabs(self.kb_currentBottomInset - bottomInset) > 0.1)) { self.kb_currentRowSpacing = rowSpacing; self.kb_currentTopInset = topInset; self.kb_currentBottomInset = bottomInset; [self.rowContainerBuilder rebuildRowContainersForRows:rows inContainer:self rowViews:self.rowViews rowSpacing:rowSpacing topInset:topInset bottomInset:bottomInset]; [self.interactionHandler bringPreviewToFrontIfNeededInContainer:self]; } // 移除旧按钮 for (UIView *row in self.rowViews) { [row.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; } if (rows.count < 4) { NSLog(@"[KBKeyboardView] reloadKeys: rows.count < 4, fallback to legacy"); self.kb_uniformCharKeyWidth = 0.0; [self kb_buildLegacyLayout]; [self kb_bindVariantLongPressIfNeeded]; return; } // 计算跨行统一字符键宽度(若各行字符键数量不同,则按最多键的行为基准) self.kb_uniformCharKeyWidth = [self.layoutEngine calculateUniformCharKeyWidthForRows:rows keyFactory:self.keyFactory shiftOn:self.shiftOn]; for (NSUInteger i = 0; i < rows.count && i < self.rowViews.count; i++) { [self.rowBuilder buildRow:self.rowViews[i] withRowConfig:rows[i] uniformCharWidth:self.kb_uniformCharKeyWidth shiftOn:self.shiftOn backspaceHandler:self.backspaceHandler target:self action:@selector(onKeyTapped:)]; } NSUInteger totalButtons = [self kb_totalKeyButtonCount]; NSLog(@"[KBKeyboardView] reloadKeys: totalButtons=%lu", (unsigned long)totalButtons); if (totalButtons == 0) { NSLog(@"[KBKeyboardView] config layout produced no keys, fallback to legacy."); for (UIView *row in self.rowViews) { [row.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; } [self kb_buildLegacyLayout]; [self kb_bindVariantLongPressIfNeeded]; return; } [self kb_bindVariantLongPressIfNeeded]; } - (void)didMoveToWindow { [super didMoveToWindow]; if (!self.window) { return; } if ([self kb_totalKeyButtonCount] > 0) { return; } // 兜底:系统编辑菜单切出切回等场景下,若按键丢失则自动重建。 [self reloadKeys]; // 自动重建后再触发一次上层主题应用,避免“按键恢复了但皮肤背景没恢复”。 UIView *container = self.superview; if ([container respondsToSelector:@selector(kb_applyTheme)]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [container performSelector:@selector(kb_applyTheme)]; #pragma clang diagnostic pop } } - (NSUInteger)kb_totalKeyButtonCount { NSUInteger total = 0; for (UIView *row in self.rowViews) { total += [self.interactionHandler collectKeyButtonsInView:row].count; } return total; } #pragma mark - Hit Test - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { UIView *hit = [super hitTest:point withEvent:event]; return [self.interactionHandler resolveHitView:hit point:point container:self rowViews:self.rowViews]; } #pragma mark - Config Helpers - (KBKeyboardLayoutConfig *)kb_layoutConfig { if (!self.layoutConfig) { self.layoutConfig = [KBKeyboardLayoutConfig sharedConfig]; } return self.layoutConfig; } - (KBKeyboardKeyFactory *)keyFactory { if (!_keyFactory) { _keyFactory = [[KBKeyboardKeyFactory alloc] initWithLayoutConfig:[self kb_layoutConfig]]; } return _keyFactory; } - (KBKeyboardLayoutEngine *)layoutEngine { if (!_layoutEngine) { _layoutEngine = [[KBKeyboardLayoutEngine alloc] initWithLayoutConfig:[self kb_layoutConfig]]; } return _layoutEngine; } - (KBKeyboardRowBuilder *)rowBuilder { if (!_rowBuilder) { _rowBuilder = [[KBKeyboardRowBuilder alloc] initWithLayoutConfig:[self kb_layoutConfig] layoutEngine:self.layoutEngine keyFactory:self.keyFactory]; } return _rowBuilder; } - (KBKeyboardInputHandler *)inputHandler { if (!_inputHandler) { _inputHandler = [[KBKeyboardInputHandler alloc] init]; __weak typeof(self) weakSelf = self; _inputHandler.onToggleShift = ^{ __strong typeof(weakSelf) self = weakSelf; if (!self) { return; } self.shiftOn = !self.shiftOn; [self reloadKeys]; }; _inputHandler.onToggleSymbols = ^{ __strong typeof(weakSelf) self = weakSelf; if (!self) { return; } self.symbolsMoreOn = !self.symbolsMoreOn; [self reloadKeys]; }; _inputHandler.onKeyTapped = ^(KBKey *key) { __strong typeof(weakSelf) self = weakSelf; if (!self) { return; } if ([self.delegate respondsToSelector:@selector(keyboardView:didTapKey:)]) { [self.delegate keyboardView:self didTapKey:key]; } }; } return _inputHandler; } - (KBKeyboardLegacyBuilder *)legacyBuilder { if (!_legacyBuilder) { _legacyBuilder = [[KBKeyboardLegacyBuilder alloc] init]; } return _legacyBuilder; } - (KBKeyboardLegacyLayoutProvider *)legacyLayoutProvider { if (!_legacyLayoutProvider) { _legacyLayoutProvider = [[KBKeyboardLegacyLayoutProvider alloc] init]; } return _legacyLayoutProvider; } - (KBKeyboardInteractionHandler *)interactionHandler { if (!_interactionHandler) { _interactionHandler = [[KBKeyboardInteractionHandler alloc] init]; } return _interactionHandler; } - (KBKeyboardRowContainerBuilder *)rowContainerBuilder { if (!_rowContainerBuilder) { _rowContainerBuilder = [[KBKeyboardRowContainerBuilder alloc] init]; } return _rowContainerBuilder; } - (KBKeyboardLayout *)kb_layoutForName:(NSString *)name { return [[self kb_layoutConfig] layoutForName:name]; } - (KBKeyboardLayout *)kb_currentLayout { NSString *baseLayoutName = self.currentLayoutJsonId.length > 0 ? self.currentLayoutJsonId : @"letters"; if (self.layoutStyle == KBKeyboardLayoutStyleNumbers) { // 优先查找当前语言的数字/符号布局,如 letters_es_numbers / letters_es_symbols // 如果不存在,回退到通用布局 numbers / symbolsMore NSString *numbersName = [NSString stringWithFormat:@"%@_numbers", baseLayoutName]; NSString *symbolsName = [NSString stringWithFormat:@"%@_symbols", baseLayoutName]; NSString *targetName = self.symbolsMoreOn ? symbolsName : numbersName; KBKeyboardLayout *layout = [self kb_layoutForName:targetName]; if (layout && layout.rows.count >= 4) { return layout; } // 回退到通用布局 return [self kb_layoutForName:(self.symbolsMoreOn ? @"symbolsMore" : @"numbers")]; } return [self kb_layoutForName:baseLayoutName]; } - (void)reloadLayoutWithProfileId:(NSString *)profileId { if (profileId.length == 0) { NSLog(@"[KBKeyboardView] reloadLayoutWithProfileId: empty profileId, ignoring"); return; } NSString *newLayoutJsonId = [[KBKeyboardLayoutResolver sharedResolver] layoutJsonIdForProfileId:profileId]; if ([newLayoutJsonId isEqualToString:self.currentLayoutJsonId]) { NSLog(@"[KBKeyboardView] Layout already loaded: %@", newLayoutJsonId); return; } NSLog(@"[KBKeyboardView] Switching layout from %@ to %@", self.currentLayoutJsonId, newLayoutJsonId); self.currentLayoutJsonId = newLayoutJsonId; // 重新加载键盘布局 [self reloadKeys]; } - (void)kb_buildLegacyLayout { self.keysForRows = [self.legacyLayoutProvider keysForLayoutStyleIsNumbers:(self.layoutStyle == KBKeyboardLayoutStyleNumbers) shiftOn:self.shiftOn symbolsMoreOn:self.symbolsMoreOn keyFactory:self.keyFactory]; if (self.keysForRows.count < 4) { return; } if (self.rowViews.count < 4) { return; } [self.legacyBuilder buildRow:self.rowViews[0] withKeys:self.keysForRows[0] edgeSpacerMultiplier:0.0 shiftOn:self.shiftOn backspaceHandler:self.backspaceHandler target:self action:@selector(onKeyTapped:)]; CGFloat row2Spacer = (self.layoutStyle == KBKeyboardLayoutStyleLetters) ? kKBLettersRow2EdgeSpacerMultiplier : 0.0; [self.legacyBuilder buildRow:self.rowViews[1] withKeys:self.keysForRows[1] edgeSpacerMultiplier:row2Spacer shiftOn:self.shiftOn backspaceHandler:self.backspaceHandler target:self action:@selector(onKeyTapped:)]; [self.legacyBuilder buildRow:self.rowViews[2] withKeys:self.keysForRows[2] edgeSpacerMultiplier:0.0 shiftOn:self.shiftOn backspaceHandler:self.backspaceHandler target:self action:@selector(onKeyTapped:)]; [self.legacyBuilder buildRow:self.rowViews[3] withKeys:self.keysForRows[3] edgeSpacerMultiplier:0.0 shiftOn:self.shiftOn backspaceHandler:self.backspaceHandler target:self action:@selector(onKeyTapped:)]; } #pragma mark - Actions - (void)onKeyTapped:(KBKeyButton *)sender { [self.inputHandler handleKeyTap:sender.key]; } // 在字符键按下时,显示一个上方气泡预览(类似系统键盘)。 - (void)showPreviewForButton:(KBKeyButton *)button { [self.interactionHandler showPreviewForButton:button inContainer:self]; } - (void)hidePreview { [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 { if (!_rowViews) _rowViews = [NSMutableArray array]; return _rowViews; } @end