Files
keyboard/CustomKeyboard/KeyboardViewController.m

831 lines
36 KiB
Mathematica
Raw Normal View History

2025-10-27 19:42:27 +08:00
//
// KeyboardViewController.m
// CustomKeyboard
//
// Created by Mac on 2025/10/27.
//
#import "KeyboardViewController.h"
2025-10-28 15:10:38 +08:00
#import "KBKeyBoardMainView.h"
2025-10-28 10:18:10 +08:00
#import "KBKey.h"
2025-10-28 15:10:38 +08:00
#import "KBFunctionView.h"
2025-10-28 18:02:10 +08:00
#import "KBSettingView.h"
2025-10-28 15:10:38 +08:00
#import "Masonry.h"
2025-10-31 16:06:54 +08:00
#import "KBAuthManager.h"
2025-11-03 13:25:41 +08:00
#import "KBFullAccessManager.h"
2025-11-04 21:01:46 +08:00
#import "KBSkinManager.h"
2025-11-20 14:27:57 +08:00
#import "KBSkinInstallBridge.h"
#import "KBHostAppLauncher.h"
2025-12-17 16:22:41 +08:00
#import "KBKeyboardSubscriptionView.h"
#import "KBKeyboardSubscriptionProduct.h"
2025-12-19 19:21:08 +08:00
#import "KBBackspaceUndoManager.h"
#import "KBInputBufferManager.h"
2025-12-22 12:54:28 +08:00
#import "KBSuggestionEngine.h"
2025-11-20 14:27:57 +08:00
// 使 static kb_consumePendingShopSkin
@interface KeyboardViewController (KBSkinShopBridge)
- (void)kb_consumePendingShopSkin;
@end
2025-10-27 19:42:27 +08:00
2025-12-22 12:54:28 +08:00
// 375 稿
static const CGFloat kKBKeyboardBaseHeight = 250.0f;
2025-10-27 19:42:27 +08:00
2025-11-20 14:27:57 +08:00
static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
void *observer,
CFStringRef name,
const void *object,
CFDictionaryRef userInfo) {
KeyboardViewController *strongSelf = (__bridge KeyboardViewController *)observer;
if (!strongSelf) { return; }
dispatch_async(dispatch_get_main_queue(), ^{
if ([strongSelf respondsToSelector:@selector(kb_consumePendingShopSkin)]) {
[strongSelf kb_consumePendingShopSkin];
}
});
}
2025-12-17 16:22:41 +08:00
@interface KeyboardViewController () <KBKeyBoardMainViewDelegate, KBFunctionViewDelegate, KBKeyboardSubscriptionViewDelegate>
2025-10-28 10:18:10 +08:00
@property (nonatomic, strong) UIButton *nextKeyboardButton; //
2026-01-07 13:11:23 +08:00
@property (nonatomic, strong) UIView *contentView;
2025-10-28 15:10:38 +08:00
@property (nonatomic, strong) KBKeyBoardMainView *keyBoardMainView; // 0
@property (nonatomic, strong) KBFunctionView *functionView; // 0
2025-10-28 18:02:10 +08:00
@property (nonatomic, strong) KBSettingView *settingView; //
2025-11-04 21:01:46 +08:00
@property (nonatomic, strong) UIImageView *bgImageView; //
2025-12-17 16:22:41 +08:00
@property (nonatomic, strong) KBKeyboardSubscriptionView *subscriptionView;
2025-12-22 12:54:28 +08:00
@property (nonatomic, strong) KBSuggestionEngine *suggestionEngine;
@property (nonatomic, copy) NSString *currentWord;
@property (nonatomic, assign) BOOL suppressSuggestions;
2026-01-07 13:11:23 +08:00
@property (nonatomic, strong) MASConstraint *contentWidthConstraint;
@property (nonatomic, strong) MASConstraint *contentHeightConstraint;
@property (nonatomic, strong) NSLayoutConstraint *kb_heightConstraint;
@property (nonatomic, strong) NSLayoutConstraint *kb_widthConstraint;
@property (nonatomic, assign) CGFloat kb_lastPortraitWidth;
@property (nonatomic, assign) CGFloat kb_lastKeyboardHeight;
2025-10-27 19:42:27 +08:00
@end
@implementation KeyboardViewController
2025-10-30 20:23:34 +08:00
{
BOOL _kb_didTriggerLoginDeepLinkOnce;
}
2025-10-27 19:42:27 +08:00
- (void)viewDidLoad {
[super viewDidLoad];
// /
[[KBBackspaceUndoManager shared] registerNonClearAction];
2025-10-27 19:42:27 +08:00
[self setupUI];
2025-12-22 12:54:28 +08:00
self.suggestionEngine = [KBSuggestionEngine shared];
self.currentWord = @"";
2025-10-31 15:08:30 +08:00
// HUD App KeyWindow
[KBHUD setContainerView:self.view];
2025-11-03 13:25:41 +08:00
// 访便
[[KBFullAccessManager shared] bindInputController:self];
__unused id token = [[NSNotificationCenter defaultCenter] addObserverForName:KBFullAccessChangedNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(__unused NSNotification * _Nonnull note) {
// 访 UI
}];
2025-11-04 21:01:46 +08:00
//
__unused id token2 = [[NSNotificationCenter defaultCenter] addObserverForName:KBSkinDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(__unused NSNotification * _Nonnull note) {
[self kb_applyTheme];
}];
[self kb_applyTheme];
2025-11-20 14:27:57 +08:00
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
(__bridge const void *)(self),
KBSkinInstallNotificationCallback,
(__bridge CFStringRef)KBDarwinSkinInstallRequestNotification,
NULL,
CFNotificationSuspensionBehaviorDeliverImmediately);
[self kb_consumePendingShopSkin];
2025-11-18 14:41:35 +08:00
2025-10-27 19:42:27 +08:00
}
2025-11-27 15:34:33 +08:00
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
// /
[[KBBackspaceUndoManager shared] registerNonClearAction];
[[KBInputBufferManager shared] resetWithText:@""];
2025-11-27 15:34:33 +08:00
[[KBLocalizationManager shared] reloadFromSharedStorageIfNeeded];
// /QQ 宿 documentContext liveText manualSnapshot
[[KBInputBufferManager shared] updateFromExternalContextBefore:self.textDocumentProxy.documentContextBeforeInput
after:self.textDocumentProxy.documentContextAfterInput];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[[KBBackspaceUndoManager shared] registerNonClearAction];
}
- (void)textDidChange:(id<UITextInput>)textInput {
[super textDidChange:textInput];
[[KBInputBufferManager shared] updateFromExternalContextBefore:self.textDocumentProxy.documentContextBeforeInput
after:self.textDocumentProxy.documentContextAfterInput];
2025-11-27 15:34:33 +08:00
}
2025-10-27 19:42:27 +08:00
- (void)setupUI {
self.view.translatesAutoresizingMaskIntoConstraints = NO;
2026-01-07 13:11:23 +08:00
//
CGFloat portraitWidth = [self kb_portraitWidth];
CGFloat keyboardHeight = [self kb_keyboardHeightForWidth:portraitWidth];
CGFloat screenWidth = CGRectGetWidth([UIScreen mainScreen].bounds);
2025-11-21 21:50:40 +08:00
CGFloat outerVerticalInset = KBFit(4.0f);
2025-11-21 20:59:39 +08:00
NSLayoutConstraint *h = [self.view.heightAnchor constraintEqualToConstant:keyboardHeight];
NSLayoutConstraint *w = [self.view.widthAnchor constraintEqualToConstant:screenWidth];
2026-01-07 13:11:23 +08:00
self.kb_heightConstraint = h;
self.kb_widthConstraint = w;
2025-11-21 20:59:39 +08:00
h.priority = UILayoutPriorityRequired;
w.priority = UILayoutPriorityRequired;
[NSLayoutConstraint activateConstraints:@[h, w]];
// UIInputView
if ([self.view isKindOfClass:[UIInputView class]]) {
UIInputView *iv = (UIInputView *)self.view;
if ([iv respondsToSelector:@selector(setAllowsSelfSizing:)]) {
iv.allowsSelfSizing = NO;
}
}
2026-01-07 13:11:23 +08:00
//
[self.view addSubview:self.contentView];
[self.contentView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.view);
make.bottom.equalTo(self.view);
self.contentWidthConstraint = make.width.mas_equalTo(portraitWidth);
self.contentHeightConstraint = make.height.mas_equalTo(keyboardHeight);
}];
//
[self.contentView addSubview:self.bgImageView];
2025-11-04 21:01:46 +08:00
[self.bgImageView mas_makeConstraints:^(MASConstraintMaker *make) {
2026-01-07 13:11:23 +08:00
make.edges.equalTo(self.contentView);
2025-11-04 21:01:46 +08:00
}];
2025-10-28 15:10:38 +08:00
//
self.functionView.hidden = YES;
2026-01-07 13:11:23 +08:00
[self.contentView addSubview:self.functionView];
2025-10-28 15:10:38 +08:00
[self.functionView mas_makeConstraints:^(MASConstraintMaker *make) {
2026-01-07 13:11:23 +08:00
make.edges.equalTo(self.contentView);
2025-10-28 10:18:10 +08:00
}];
2025-10-28 15:10:38 +08:00
2026-01-07 13:11:23 +08:00
[self.contentView addSubview:self.keyBoardMainView];
2025-10-28 15:10:38 +08:00
[self.keyBoardMainView mas_makeConstraints:^(MASConstraintMaker *make) {
2026-01-07 13:11:23 +08:00
make.edges.equalTo(self.contentView);
2025-10-28 10:18:10 +08:00
}];
2025-10-27 19:42:27 +08:00
}
2025-10-28 10:18:10 +08:00
2025-10-28 15:10:38 +08:00
#pragma mark - Private
2025-10-28 14:30:03 +08:00
2025-12-22 12:54:28 +08:00
// MARK: - Suggestions
- (void)kb_updateCurrentWordWithInsertedText:(NSString *)text {
if (text.length == 0) { return; }
if ([self kb_isAlphabeticString:text]) {
NSString *current = self.currentWord ?: @"";
self.currentWord = [current stringByAppendingString:text];
self.suppressSuggestions = NO;
[self kb_updateSuggestionsForCurrentWord];
} else {
[self kb_clearCurrentWord];
}
}
- (void)kb_clearCurrentWord {
self.currentWord = @"";
[self.keyBoardMainView kb_setSuggestions:@[]];
self.suppressSuggestions = NO;
}
- (void)kb_scheduleContextRefreshResetSuppression:(BOOL)resetSuppression {
dispatch_async(dispatch_get_main_queue(), ^{
[self kb_refreshCurrentWordFromDocumentContextResetSuppression:resetSuppression];
});
}
- (void)kb_refreshCurrentWordFromDocumentContextResetSuppression:(BOOL)resetSuppression {
NSString *context = self.textDocumentProxy.documentContextBeforeInput ?: @"";
NSString *word = [self kb_extractTrailingWordFromContext:context];
self.currentWord = word ?: @"";
if (resetSuppression) {
self.suppressSuggestions = NO;
}
[self kb_updateSuggestionsForCurrentWord];
}
- (NSString *)kb_extractTrailingWordFromContext:(NSString *)context {
if (context.length == 0) { return @""; }
static NSCharacterSet *letters = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
letters = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"];
});
NSInteger idx = (NSInteger)context.length - 1;
while (idx >= 0) {
unichar ch = [context characterAtIndex:(NSUInteger)idx];
if (![letters characterIsMember:ch]) {
break;
}
idx -= 1;
}
NSUInteger start = (NSUInteger)(idx + 1);
if (start >= context.length) { return @""; }
return [context substringFromIndex:start];
}
- (BOOL)kb_isAlphabeticString:(NSString *)text {
if (text.length == 0) { return NO; }
static NSCharacterSet *letters = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
letters = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"];
});
for (NSUInteger i = 0; i < text.length; i++) {
if (![letters characterIsMember:[text characterAtIndex:i]]) {
return NO;
}
}
return YES;
}
- (void)kb_updateSuggestionsForCurrentWord {
NSString *prefix = self.currentWord ?: @"";
if (prefix.length == 0) {
[self.keyBoardMainView kb_setSuggestions:@[]];
return;
}
if (self.suppressSuggestions) {
[self.keyBoardMainView kb_setSuggestions:@[]];
return;
}
NSArray<NSString *> *items = [self.suggestionEngine suggestionsForPrefix:prefix limit:5];
NSArray<NSString *> *cased = [self kb_applyCaseToSuggestions:items prefix:prefix];
[self.keyBoardMainView kb_setSuggestions:cased];
}
- (NSArray<NSString *> *)kb_applyCaseToSuggestions:(NSArray<NSString *> *)items prefix:(NSString *)prefix {
if (items.count == 0 || prefix.length == 0) { return items; }
BOOL allUpper = [prefix isEqualToString:prefix.uppercaseString];
BOOL firstUpper = [[prefix substringToIndex:1] isEqualToString:[[prefix substringToIndex:1] uppercaseString]];
if (!allUpper && !firstUpper) { return items; }
NSMutableArray<NSString *> *result = [NSMutableArray arrayWithCapacity:items.count];
for (NSString *word in items) {
if (allUpper) {
[result addObject:word.uppercaseString];
} else {
NSString *first = [[word substringToIndex:1] uppercaseString];
NSString *rest = (word.length > 1) ? [word substringFromIndex:1] : @"";
[result addObject:[first stringByAppendingString:rest]];
}
}
return result.copy;
}
2025-10-28 15:10:38 +08:00
/// /
- (void)showFunctionPanel:(BOOL)show {
//
self.functionView.hidden = !show;
self.keyBoardMainView.hidden = show;
2025-12-17 16:22:41 +08:00
if (show) {
2026-01-06 19:25:34 +08:00
[[KBMaiPointReporter sharedReporter] reportPageExposureWithEventName:@"enter_keyboard_function_panel"
pageId:@"keyboard_function_panel"
extra:nil
completion:nil];
2025-12-17 16:22:41 +08:00
[self hideSubscriptionPanel];
2026-01-06 19:25:34 +08:00
} else {
[[KBMaiPointReporter sharedReporter] reportPageExposureWithEventName:@"enter_keyboard_main_panel"
pageId:@"keyboard_main_panel"
extra:nil
completion:nil];
2025-12-17 16:22:41 +08:00
}
2025-10-28 15:18:12 +08:00
2025-10-28 15:10:38 +08:00
//
if (show) {
2026-01-07 13:11:23 +08:00
[self.contentView bringSubviewToFront:self.functionView];
2025-10-28 15:10:38 +08:00
} else {
2026-01-07 13:11:23 +08:00
[self.contentView bringSubviewToFront:self.keyBoardMainView];
2025-10-28 15:10:38 +08:00
}
2025-10-28 10:18:10 +08:00
}
2025-10-28 18:02:10 +08:00
/// / keyBoardMainView /
- (void)showSettingView:(BOOL)show {
if (show) {
2026-01-06 19:25:34 +08:00
[[KBMaiPointReporter sharedReporter] reportPageExposureWithEventName:@"enter_keyboard_settings"
pageId:@"keyboard_settings"
extra:nil
completion:nil];
2025-10-30 20:23:34 +08:00
// if (!self.settingView) {
self.settingView = [[KBSettingView alloc] init];
self.settingView.hidden = YES;
2026-01-07 13:11:23 +08:00
[self.contentView addSubview:self.settingView];
2025-10-30 20:23:34 +08:00
[self.settingView mas_makeConstraints:^(MASConstraintMaker *make) {
//
2026-01-07 13:11:23 +08:00
make.edges.equalTo(self.contentView);
2025-10-30 20:23:34 +08:00
}];
[self.settingView.backButton addTarget:self action:@selector(onTapSettingsBack) forControlEvents:UIControlEventTouchUpInside];
// }
2026-01-07 13:11:23 +08:00
[self.contentView bringSubviewToFront:self.settingView];
2025-10-28 18:02:10 +08:00
// keyBoardMainView self.view
2026-01-07 13:11:23 +08:00
[self.contentView layoutIfNeeded];
2025-10-28 18:02:10 +08:00
CGFloat w = CGRectGetWidth(self.keyBoardMainView.bounds);
2026-01-07 13:11:23 +08:00
if (w <= 0) { w = CGRectGetWidth(self.contentView.bounds); }
if (w <= 0) { w = [self kb_portraitWidth]; }
2025-10-28 18:02:10 +08:00
self.settingView.transform = CGAffineTransformMakeTranslation(w, 0);
self.settingView.hidden = NO;
[UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.settingView.transform = CGAffineTransformIdentity;
} completion:nil];
} else {
if (!self.settingView || self.settingView.hidden) return;
CGFloat w = CGRectGetWidth(self.keyBoardMainView.bounds);
2026-01-07 13:11:23 +08:00
if (w <= 0) { w = CGRectGetWidth(self.contentView.bounds); }
if (w <= 0) { w = [self kb_portraitWidth]; }
2025-10-28 18:02:10 +08:00
[UIView animateWithDuration:0.22 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
self.settingView.transform = CGAffineTransformMakeTranslation(w, 0);
} completion:^(BOOL finished) {
self.settingView.hidden = YES;
}];
}
}
2025-12-17 16:22:41 +08:00
- (void)showSubscriptionPanel {
// 1) 访
if (![[KBFullAccessManager shared] hasFullAccess]) {
// 访
// [KBHUD showInfo:KBLocalized(@"处理中…")];
[[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self.view];
return;
}
//
// 2) -> App App
if (!KBAuthManager.shared.isLoggedIn) {
NSString *schemeStr = [NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME];
NSURL *scheme = [NSURL URLWithString:schemeStr];
// UIApplication App
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
return;
}
2026-01-06 19:25:34 +08:00
[[KBMaiPointReporter sharedReporter] reportPageExposureWithEventName:@"enter_keyboard_subscription_panel"
pageId:@"keyboard_subscription_panel"
extra:nil
completion:nil];
2025-12-17 16:22:41 +08:00
[self showFunctionPanel:NO];
KBKeyboardSubscriptionView *panel = self.subscriptionView;
if (!panel.superview) {
panel.hidden = YES;
2026-01-07 13:11:23 +08:00
[self.contentView addSubview:panel];
2025-12-17 16:22:41 +08:00
[panel mas_makeConstraints:^(MASConstraintMaker *make) {
2026-01-07 13:11:23 +08:00
make.edges.equalTo(self.contentView);
2025-12-17 16:22:41 +08:00
}];
}
2026-01-07 13:11:23 +08:00
[self.contentView bringSubviewToFront:panel];
2025-12-17 16:22:41 +08:00
panel.hidden = NO;
panel.alpha = 0.0;
2026-01-07 13:11:23 +08:00
CGFloat height = CGRectGetHeight(self.contentView.bounds);
2025-12-17 16:22:41 +08:00
if (height <= 0) { height = 260; }
panel.transform = CGAffineTransformMakeTranslation(0, height);
[panel refreshProductsIfNeeded];
[UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
panel.alpha = 1.0;
panel.transform = CGAffineTransformIdentity;
} completion:nil];
}
- (void)hideSubscriptionPanel {
if (!self.subscriptionView || self.subscriptionView.hidden) { return; }
CGFloat height = CGRectGetHeight(self.subscriptionView.bounds);
2026-01-07 13:11:23 +08:00
if (height <= 0) { height = CGRectGetHeight(self.contentView.bounds); }
2025-12-17 16:22:41 +08:00
KBKeyboardSubscriptionView *panel = self.subscriptionView;
[UIView animateWithDuration:0.22 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
panel.alpha = 0.0;
panel.transform = CGAffineTransformMakeTranslation(0, height);
} completion:^(BOOL finished) {
panel.hidden = YES;
panel.alpha = 1.0;
panel.transform = CGAffineTransformIdentity;
}];
}
2025-10-28 10:18:10 +08:00
2025-10-28 15:10:38 +08:00
// MARK: - KBKeyBoardMainViewDelegate
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapKey:(KBKey *)key {
2025-10-28 10:18:10 +08:00
switch (key.type) {
2025-12-22 12:54:28 +08:00
case KBKeyTypeCharacter: {
[[KBBackspaceUndoManager shared] registerNonClearAction];
2025-12-22 12:54:28 +08:00
NSString *text = key.output ?: key.title ?: @"";
[self.textDocumentProxy insertText:text];
[self kb_updateCurrentWordWithInsertedText:text];
[[KBInputBufferManager shared] appendText:text];
2025-12-22 12:54:28 +08:00
} break;
2025-10-28 10:18:10 +08:00
case KBKeyTypeBackspace:
[[KBInputBufferManager shared] refreshFromProxyIfPossible:self.textDocumentProxy];
[[KBInputBufferManager shared] prepareSnapshotForDeleteWithContextBefore:self.textDocumentProxy.documentContextBeforeInput
after:self.textDocumentProxy.documentContextAfterInput];
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:self.textDocumentProxy count:1];
2025-12-22 12:54:28 +08:00
[self kb_scheduleContextRefreshResetSuppression:NO];
[[KBInputBufferManager shared] applyHoldDeleteCount:1];
2025-12-22 12:54:28 +08:00
break;
2025-10-28 10:18:10 +08:00
case KBKeyTypeSpace:
[[KBBackspaceUndoManager shared] registerNonClearAction];
2025-12-22 12:54:28 +08:00
[self.textDocumentProxy insertText:@" "];
[self kb_clearCurrentWord];
[[KBInputBufferManager shared] appendText:@" "];
2025-12-22 12:54:28 +08:00
break;
2025-10-28 10:18:10 +08:00
case KBKeyTypeReturn:
[[KBBackspaceUndoManager shared] registerNonClearAction];
2025-12-22 12:54:28 +08:00
[self.textDocumentProxy insertText:@"\n"];
[self kb_clearCurrentWord];
[[KBInputBufferManager shared] appendText:@"\n"];
2025-12-22 12:54:28 +08:00
break;
2025-10-28 10:18:10 +08:00
case KBKeyTypeGlobe:
[self advanceToNextInputMode]; break;
case KBKeyTypeCustom:
[[KBBackspaceUndoManager shared] registerNonClearAction];
2025-12-19 20:08:13 +08:00
//
2025-10-29 12:59:22 +08:00
[self showFunctionPanel:YES];
2025-12-22 12:54:28 +08:00
[self kb_clearCurrentWord];
2025-10-29 12:59:22 +08:00
break;
2025-10-28 15:10:38 +08:00
case KBKeyTypeModeChange:
2025-10-28 10:18:10 +08:00
case KBKeyTypeShift:
2025-10-28 15:10:38 +08:00
// KBKeyBoardMainView/KBKeyboardView
2025-10-28 10:18:10 +08:00
break;
}
}
2025-10-28 15:10:38 +08:00
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapToolActionAtIndex:(NSInteger)index {
2026-01-06 19:25:34 +08:00
NSDictionary *extra = @{@"index": @(index)};
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_toolbar_action"
pageId:@"keyboard_main_panel"
elementId:@"toolbar_action"
extra:extra
completion:nil];
2025-12-19 20:08:13 +08:00
if (index == 0) {
[self showFunctionPanel:YES];
2025-12-22 12:54:28 +08:00
[self kb_clearCurrentWord];
2025-12-19 20:08:13 +08:00
return;
}
[self showFunctionPanel:NO];
2025-10-28 15:10:38 +08:00
}
2025-10-28 18:02:10 +08:00
- (void)keyBoardMainViewDidTapSettings:(KBKeyBoardMainView *)keyBoardMainView {
2026-01-06 19:25:34 +08:00
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_settings_btn"
pageId:@"keyboard_main_panel"
elementId:@"settings_btn"
extra:nil
completion:nil];
2025-10-28 18:02:10 +08:00
[self showSettingView:YES];
}
2025-12-15 13:24:43 +08:00
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didSelectEmoji:(NSString *)emoji {
if (emoji.length == 0) { return; }
2025-12-19 19:21:08 +08:00
[[KBBackspaceUndoManager shared] registerNonClearAction];
2025-12-15 13:24:43 +08:00
[self.textDocumentProxy insertText:emoji];
2025-12-22 12:54:28 +08:00
[self kb_clearCurrentWord];
[[KBInputBufferManager shared] appendText:emoji];
2025-12-15 13:24:43 +08:00
}
2025-12-19 19:21:08 +08:00
- (void)keyBoardMainViewDidTapUndo:(KBKeyBoardMainView *)keyBoardMainView {
2026-01-06 19:25:34 +08:00
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_undo_btn"
pageId:@"keyboard_main_panel"
elementId:@"undo_btn"
extra:nil
completion:nil];
2025-12-19 19:21:08 +08:00
[[KBBackspaceUndoManager shared] performUndoFromResponder:self.view];
2025-12-22 12:54:28 +08:00
[self kb_scheduleContextRefreshResetSuppression:YES];
2025-12-19 19:21:08 +08:00
}
2025-12-15 13:24:43 +08:00
- (void)keyBoardMainViewDidTapEmojiSearch:(KBKeyBoardMainView *)keyBoardMainView {
2026-01-06 19:25:34 +08:00
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_emoji_search_btn"
pageId:@"keyboard_main_panel"
elementId:@"emoji_search_btn"
extra:nil
completion:nil];
2025-12-15 13:24:43 +08:00
[KBHUD showInfo:KBLocalized(@"Search coming soon")];
}
2025-12-22 12:54:28 +08:00
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didSelectSuggestion:(NSString *)suggestion {
if (suggestion.length == 0) { return; }
2026-01-06 19:25:34 +08:00
NSDictionary *extra = @{@"suggestion_len": @(suggestion.length)};
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_suggestion_item"
pageId:@"keyboard_main_panel"
elementId:@"suggestion_item"
extra:extra
completion:nil];
[[KBBackspaceUndoManager shared] registerNonClearAction];
2025-12-22 12:54:28 +08:00
NSString *current = self.currentWord ?: @"";
if (current.length > 0) {
for (NSUInteger i = 0; i < current.length; i++) {
[self.textDocumentProxy deleteBackward];
}
}
[self.textDocumentProxy insertText:suggestion];
self.currentWord = suggestion;
[self.suggestionEngine recordSelection:suggestion];
self.suppressSuggestions = YES;
[self.keyBoardMainView kb_setSuggestions:@[]];
[[KBInputBufferManager shared] replaceTailWithText:suggestion deleteCount:current.length];
2025-12-22 12:54:28 +08:00
}
2025-10-28 15:18:12 +08:00
// MARK: - KBFunctionViewDelegate
- (void)functionView:(KBFunctionView *)functionView didTapToolActionAtIndex:(NSInteger)index {
// index == 0
if (index == 0) {
[self showFunctionPanel:NO];
}
}
2025-11-26 21:16:56 +08:00
- (void)functionView:(KBFunctionView *_Nullable)functionView didRightTapToolActionAtIndex:(NSInteger)index{
2026-01-06 19:25:34 +08:00
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_function_right_action"
pageId:@"keyboard_function_panel"
elementId:@"right_action"
extra:@{@"action": @"login_or_recharge"}
completion:nil];
if (!KBAuthManager.shared.isLoggedIn) {
NSString *schemeStr = [NSString stringWithFormat:@"%@://login?src=keyboard", KB_APP_SCHEME];
NSURL *scheme = [NSURL URLWithString:schemeStr];
// UIApplication App
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
return;
}
2025-11-26 21:16:56 +08:00
NSString *schemeStr = [NSString stringWithFormat:@"%@://recharge?src=keyboard", KB_APP_SCHEME];
NSURL *scheme = [NSURL URLWithString:schemeStr];
//
// if (!ul && !scheme) { return; }
//
// UIApplication App
BOOL ok = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
if (!ok) {
//
// XXX App /
[KBHUD showInfo:@"请回到桌面手动打开App进行充值"];
}
}
2025-10-28 15:18:12 +08:00
2025-12-17 17:01:49 +08:00
- (void)functionViewDidRequestSubscription:(KBFunctionView *)functionView {
[self showSubscriptionPanel];
}
2025-12-17 16:22:41 +08:00
#pragma mark - KBKeyboardSubscriptionViewDelegate
- (void)subscriptionViewDidTapClose:(KBKeyboardSubscriptionView *)view {
2026-01-06 19:25:34 +08:00
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_subscription_close_btn"
pageId:@"keyboard_subscription_panel"
elementId:@"close_btn"
extra:nil
completion:nil];
2025-12-17 16:22:41 +08:00
[self hideSubscriptionPanel];
}
- (void)subscriptionView:(KBKeyboardSubscriptionView *)view didTapPurchaseForProduct:(KBKeyboardSubscriptionProduct *)product {
2026-01-06 19:25:34 +08:00
NSMutableDictionary *extra = [NSMutableDictionary dictionary];
if ([product.productId isKindOfClass:NSString.class] && product.productId.length > 0) {
extra[@"product_id"] = product.productId;
}
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_subscription_product_btn"
pageId:@"keyboard_subscription_panel"
elementId:@"product_btn"
extra:extra.copy
completion:nil];
2025-12-17 16:22:41 +08:00
[self hideSubscriptionPanel];
[self kb_openRechargeForProduct:product];
}
2025-10-28 15:10:38 +08:00
#pragma mark - lazy
- (KBKeyBoardMainView *)keyBoardMainView{
if (!_keyBoardMainView) {
_keyBoardMainView = [[KBKeyBoardMainView alloc] init];
2025-10-28 15:18:12 +08:00
_keyBoardMainView.delegate = self;
2025-10-28 15:10:38 +08:00
}
return _keyBoardMainView;
}
- (KBFunctionView *)functionView{
if (!_functionView) {
_functionView = [[KBFunctionView alloc] init];
2025-10-28 15:18:12 +08:00
_functionView.delegate = self; // Bar
2025-10-28 15:10:38 +08:00
}
return _functionView;
}
2025-10-28 18:02:10 +08:00
- (KBSettingView *)settingView {
if (!_settingView) {
_settingView = [[KBSettingView alloc] init];
}
return _settingView;
}
2025-10-28 15:10:38 +08:00
2025-12-17 16:22:41 +08:00
- (KBKeyboardSubscriptionView *)subscriptionView {
if (!_subscriptionView) {
_subscriptionView = [[KBKeyboardSubscriptionView alloc] init];
_subscriptionView.delegate = self;
_subscriptionView.hidden = YES;
_subscriptionView.alpha = 0.0;
}
return _subscriptionView;
}
2025-10-28 15:10:38 +08:00
2025-10-28 18:02:10 +08:00
#pragma mark - Actions
2025-12-17 16:22:41 +08:00
- (void)kb_openRechargeForProduct:(KBKeyboardSubscriptionProduct *)product {
if (![product isKindOfClass:KBKeyboardSubscriptionProduct.class] || product.productId.length == 0) {
[KBHUD showInfo:KBLocalized(@"Product unavailable")];
return;
}
NSString *encodedId = [self.class kb_urlEncodedString:product.productId];
NSString *title = [product displayTitle];
NSString *encodedTitle = [self.class kb_urlEncodedString:title];
2025-12-17 19:45:39 +08:00
NSMutableArray<NSString *> *params = [NSMutableArray arrayWithObjects:@"autoPay=1", @"prefill=1", nil];
2025-12-17 16:22:41 +08:00
if (encodedId.length) {
[params addObject:[NSString stringWithFormat:@"productId=%@", encodedId]];
}
if (encodedTitle.length) {
[params addObject:[NSString stringWithFormat:@"productTitle=%@", encodedTitle]];
}
NSString *query = [params componentsJoinedByString:@"&"];
NSString *urlString = [NSString stringWithFormat:@"%@://recharge?src=keyboard&%@", KB_APP_SCHEME, query];
NSURL *scheme = [NSURL URLWithString:urlString];
BOOL success = [KBHostAppLauncher openHostAppURL:scheme fromResponder:self.view];
if (!success) {
[KBHUD showInfo:KBLocalized(@"Please open the App to finish purchase")];
}
}
+ (NSString *)kb_urlEncodedString:(NSString *)value {
if (value.length == 0) { return @""; }
NSString *reserved = @"!*'();:@&=+$,/?%#[]";
NSMutableCharacterSet *allowed = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
[allowed removeCharactersInString:reserved];
return [value stringByAddingPercentEncodingWithAllowedCharacters:allowed] ?: @"";
}
2025-10-28 18:02:10 +08:00
- (void)onTapSettingsBack {
2026-01-06 19:25:34 +08:00
[[KBMaiPointReporter sharedReporter] reportClickWithEventName:@"click_keyboard_settings_back_btn"
pageId:@"keyboard_settings"
elementId:@"back_btn"
extra:nil
completion:nil];
2025-10-28 18:02:10 +08:00
[self showSettingView:NO];
}
2025-10-28 15:18:12 +08:00
2025-11-20 14:27:57 +08:00
- (void)dealloc {
CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(),
(__bridge const void *)(self),
(__bridge CFStringRef)KBDarwinSkinInstallRequestNotification,
NULL);
}
2025-10-30 20:23:34 +08:00
// App App
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
2025-11-13 14:11:44 +08:00
// if (!_kb_didTriggerLoginDeepLinkOnce) {
// _kb_didTriggerLoginDeepLinkOnce = YES;
// // App
// if (!KBAuthManager.shared.isLoggedIn) {
// [self kb_tryOpenContainerForLoginIfNeeded];
// }
// }
2025-10-30 20:23:34 +08:00
}
2026-01-07 13:11:23 +08:00
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[self kb_updateKeyboardLayoutIfNeeded];
}
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
__weak typeof(self) weakSelf = self;
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
[weakSelf kb_updateKeyboardLayoutIfNeeded];
} completion:^(__unused id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
[weakSelf kb_updateKeyboardLayoutIfNeeded];
}];
}
2025-11-13 14:11:44 +08:00
//- (void)kb_tryOpenContainerForLoginIfNeeded {
// // 使 App Scheme
// NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=keyboard", KB_APP_SCHEME]];
// if (!url) return;
// KBWeakSelf
// [self.extensionContext openURL:url completionHandler:^(__unused BOOL success) {
// // 使
// __unused typeof(weakSelf) selfStrong = weakSelf;
// }];
//}
2025-11-04 21:01:46 +08:00
#pragma mark - Theme
- (void)kb_applyTheme {
KBSkinTheme *t = [KBSkinManager shared].current;
UIImage *img = [[KBSkinManager shared] currentBackgroundImage];
2025-12-23 15:26:32 +08:00
NSLog(@"⌨️[Keyboard] apply theme id=%@ hasBg=%d", t.skinId, (img != nil));
2025-11-04 21:01:46 +08:00
self.bgImageView.image = img;
BOOL hasImg = (img != nil);
self.view.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground;
2026-01-07 13:11:23 +08:00
self.contentView.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground;
2025-11-04 21:01:46 +08:00
self.keyBoardMainView.backgroundColor = hasImg ? [UIColor clearColor] : t.keyboardBackground;
//
if ([self.keyBoardMainView respondsToSelector:@selector(kb_applyTheme)]) {
// method declared in KBKeyBoardMainView.h
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.keyBoardMainView performSelector:@selector(kb_applyTheme)];
#pragma clang diagnostic pop
}
if ([self.functionView respondsToSelector:@selector(kb_applyTheme)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.functionView performSelector:@selector(kb_applyTheme)];
#pragma clang diagnostic pop
}
}
2025-11-20 14:27:57 +08:00
- (void)kb_consumePendingShopSkin {
KBWeakSelf
[KBSkinInstallBridge consumePendingRequestFromBundle:NSBundle.mainBundle
completion:^(BOOL success, NSError * _Nullable error) {
if (!success) {
if (error) {
NSLog(@"[Keyboard] skin request failed: %@", error);
[KBHUD showInfo:KBLocalized(@"皮肤资源准备失败,请稍后再试")];
}
return;
}
[weakSelf kb_applyTheme];
[KBHUD showInfo:KBLocalized(@"皮肤已更新,立即体验吧")];
}];
}
2026-01-07 13:11:23 +08:00
#pragma mark - Layout Helpers
- (CGFloat)kb_portraitWidth {
CGSize s = [UIScreen mainScreen].bounds.size;
return MIN(s.width, s.height);
}
- (CGFloat)kb_keyboardHeightForWidth:(CGFloat)width {
if (width <= 0) { width = KB_DESIGN_WIDTH; }
return kKBKeyboardBaseHeight * (width / KB_DESIGN_WIDTH);
}
- (void)kb_updateKeyboardLayoutIfNeeded {
CGFloat portraitWidth = [self kb_portraitWidth];
CGFloat keyboardHeight = [self kb_keyboardHeightForWidth:portraitWidth];
CGFloat containerWidth = CGRectGetWidth(self.view.superview.bounds);
if (containerWidth <= 0) {
containerWidth = CGRectGetWidth(self.view.window.bounds);
}
if (containerWidth <= 0) {
containerWidth = CGRectGetWidth([UIScreen mainScreen].bounds);
}
BOOL widthChanged = (fabs(self.kb_lastPortraitWidth - portraitWidth) >= 0.5);
BOOL heightChanged = (fabs(self.kb_lastKeyboardHeight - keyboardHeight) >= 0.5);
if (!widthChanged && !heightChanged && containerWidth > 0 && self.kb_widthConstraint.constant == containerWidth) {
return;
}
self.kb_lastPortraitWidth = portraitWidth;
self.kb_lastKeyboardHeight = keyboardHeight;
if (self.kb_heightConstraint) {
self.kb_heightConstraint.constant = keyboardHeight;
}
if (containerWidth > 0 && self.kb_widthConstraint) {
self.kb_widthConstraint.constant = containerWidth;
}
if (self.contentWidthConstraint) {
[self.contentWidthConstraint setOffset:portraitWidth];
}
if (self.contentHeightConstraint) {
[self.contentHeightConstraint setOffset:keyboardHeight];
}
[self.view layoutIfNeeded];
}
2025-11-04 21:01:46 +08:00
#pragma mark - Lazy
2026-01-07 13:11:23 +08:00
- (UIView *)contentView {
if (!_contentView) {
_contentView = [[UIView alloc] init];
_contentView.backgroundColor = [UIColor clearColor];
}
return _contentView;
}
2025-11-04 21:01:46 +08:00
- (UIImageView *)bgImageView {
if (!_bgImageView) {
_bgImageView = [[UIImageView alloc] init];
_bgImageView.contentMode = UIViewContentModeScaleAspectFill;
_bgImageView.clipsToBounds = YES;
}
return _bgImageView;
}
2025-10-28 15:18:12 +08:00
@end