2026-02-24 13:38:51 +08:00
//
// KeyboardViewController + Panels . m
// CustomKeyboard
//
// Created by Codex on 2026 / 02 / 22.
//
# import "KeyboardViewController+Private.h"
# import "KBAuthManager.h"
# import "KBBackspaceUndoManager.h"
# import "KBChatMessage.h"
# import "KBChatPanelView.h"
# import "KBFunctionView.h"
2026-02-26 19:38:17 +08:00
# import "KBFullAccessManager.h"
2026-03-05 14:30:07 +08:00
# import "../Utils/KBExtensionAppLauncher.h"
2026-02-24 13:38:51 +08:00
# import "KBInputBufferManager.h"
# import "KBKey.h"
# import "KBKeyBoardMainView.h"
# import "KBKeyboardSubscriptionView.h"
# import "Masonry.h"
# import < SDWebImage / SDWebImage . h >
# import < AVFoundation / AVAudioPlayer . h >
@ implementation KeyboardViewController ( Panels )
# pragma mark - Panel Mode
- ( void ) kb_setPanelMode : ( KBKeyboardPanelMode ) mode animated : ( BOOL ) animated {
if ( mode = = self . kb_panelMode ) {
return ;
}
KBKeyboardPanelMode fromMode = self . kb_panelMode ;
2026-02-26 19:38:17 +08:00
// AI 入 口 先 判 完 全 访 问 : 未 开 启 时 仅 展 示 引 导 , 不 再 继 续 登 录 态 判 断 。
if ( mode = = KBKeyboardPanelModeFunction &&
! [ [ KBFullAccessManager shared ] ensureFullAccessOrGuideInView : self . view ] ) {
return ;
}
// 未 登 录 时 , 不 要 提 前 写 入 面 板 状 态 , 避 免 mode 被 错 误 卡 在 Function 导 致 后 续 点 击 无 响 应 。
BOOL islogin = YES ;
if ( mode = = KBKeyboardPanelModeFunction ) {
[ [ KBAuthManager shared ] reloadFromKeychain ] ;
islogin = KBAuthManager . shared . isLoggedIn ;
}
# if DEBUG
if ( mode = = KBKeyboardPanelModeFunction ) {
NSString * token = [ KBAuthManager shared ] . current . accessToken ? : @ "" ;
NSLog ( @ "[AuthTrace][Ext] tapAI mode=%ld isLoggedIn=%d tokenLen=%lu" ,
( long ) mode , islogin , ( unsigned long ) token . length ) ;
}
# endif
if ( mode = = KBKeyboardPanelModeFunction && ! islogin ) {
2026-03-07 13:29:29 +08:00
[ KBHUD showInfo : KBLocalized ( @ "Please sign in before using AI features" ) ] ;
2026-03-05 14:30:07 +08:00
NSURL * ul = [ NSURL URLWithString : [ NSString stringWithFormat : @ "%@?src=keyboard" , KB_UL _LOGIN ] ] ;
NSURL * scheme =
[ NSURL URLWithString : [ NSString stringWithFormat : @ "%@://login?src=keyboard" , KB_APP _SCHEME ] ] ;
__weak typeof ( self ) weakSelf = self ;
[ KBExtensionAppLauncher openPrimaryURL : ul
fallbackURL : scheme
usingInputController : self
source : ( self . view ? : ( UIResponder * ) weakSelf )
completion : ^ ( BOOL success ) {
if ( success ) {
return ;
}
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
2026-03-07 13:29:29 +08:00
[ KBHUD showInfo : KBLocalized ( @ "Please return to the Home screen and open the app to sign in" ) ] ;
2026-03-05 14:30:07 +08:00
} ) ;
} ] ;
2026-02-26 19:38:17 +08:00
return ;
}
2026-02-24 13:38:51 +08:00
self . kb_panelMode = mode ;
// 主 键 盘 视 图 是 基 础 承 载 : 确 保 存 在 ( 键 盘 隐 藏 后 会 被 释 放 )
[ self kb_ensureKeyBoardMainViewIfNeeded ] ;
// 1 ) 先 收 起 所 有 面 板 ( 再 展 开 目 标 面 板 ) , 避 免 互 相 调 用 导 致 漏 关 / 层 级 错 乱
[ self kb_setSubscriptionPanelVisible : NO animated : animated ] ;
[ self kb_setChatPanelVisible : NO animated : animated ] ;
[ self kb_setFunctionPanelVisible : NO ] ;
// 2 ) 再 展 开 目 标 面 板
2026-02-26 19:38:17 +08:00
switch ( mode ) {
case KBKeyboardPanelModeFunction :
[ self kb_setFunctionPanelVisible : YES ] ;
break ;
case KBKeyboardPanelModeChat :
[ self kb_setChatPanelVisible : YES animated : animated ] ;
break ;
case KBKeyboardPanelModeSubscription :
[ self kb_setSubscriptionPanelVisible : YES animated : animated ] ;
break ;
case KBKeyboardPanelModeMain :
default :
break ;
}
2026-02-24 13:38:51 +08:00
// 3 ) 事 件 埋 点 : 保 持 原 逻 辑 ( 仅 功 能 面 板 / 主 面 板 会 互 相 曝 光 )
if ( mode = = KBKeyboardPanelModeFunction ) {
[ [ KBMaiPointReporter sharedReporter ]
reportPageExposureWithEventName : @ "enter_keyboard_function_panel"
pageId : @ "keyboard_function_panel"
extra : nil
completion : nil ] ;
} else if ( mode = = KBKeyboardPanelModeMain &&
fromMode = = KBKeyboardPanelModeFunction ) {
[ [ KBMaiPointReporter sharedReporter ]
reportPageExposureWithEventName : @ "enter_keyboard_main_panel"
pageId : @ "keyboard_main_panel"
extra : nil
completion : nil ] ;
} else if ( mode = = KBKeyboardPanelModeSubscription ) {
[ [ KBMaiPointReporter sharedReporter ]
reportPageExposureWithEventName : @ "enter_keyboard_subscription_panel"
pageId : @ "keyboard_subscription_panel"
extra : nil
completion : nil ] ;
}
// 4 ) 层 级 : 保 证 当 前 面 板 在 最 上 层
if ( mode = = KBKeyboardPanelModeSubscription ) {
[ self . contentView bringSubviewToFront : self . subscriptionView ] ;
} else if ( mode = = KBKeyboardPanelModeChat ) {
[ self . contentView bringSubviewToFront : self . chatPanelView ] ;
} else if ( mode = = KBKeyboardPanelModeFunction ) {
[ self . contentView bringSubviewToFront : self . functionView ] ;
} else {
[ self . contentView bringSubviewToFront : self . keyBoardMainView ] ;
}
}
// / 对 外 兼 容 : 切 换 显 示 功 能 面 板 / 键 盘 主 视 图
- ( void ) showFunctionPanel : ( BOOL ) show {
if ( show ) {
[ self kb_setPanelMode : KBKeyboardPanelModeFunction animated : NO ] ;
return ;
}
if ( self . kb_panelMode = = KBKeyboardPanelModeFunction ) {
[ self kb_setPanelMode : KBKeyboardPanelModeMain animated : NO ] ;
}
}
// / 对 外 兼 容 : 显 示 / 隐 藏 聊 天 面 板 ( 覆 盖 整 个 键 盘 区 域 )
- ( void ) showChatPanel : ( BOOL ) show {
if ( show ) {
[ self kb_setPanelMode : KBKeyboardPanelModeChat animated : YES ] ;
return ;
}
if ( self . kb_panelMode = = KBKeyboardPanelModeChat ) {
[ self kb_setPanelMode : KBKeyboardPanelModeMain animated : YES ] ;
}
}
- ( void ) kb_setFunctionPanelVisible : ( BOOL ) visible {
if ( visible ) {
[ self kb_ensureFunctionViewIfNeeded ] ;
}
if ( _functionView ) {
_functionView . hidden = ! visible ;
} else if ( visible ) {
// ensure 后 按 理 已 存 在 ; 这 里 兜 底 一 次 , 避 免 异 常 情 况 下 状 态 不 一 致
self . functionView . hidden = NO ;
}
self . keyBoardMainView . hidden = visible ;
}
- ( void ) kb_setChatPanelVisible : ( BOOL ) visible animated : ( BOOL ) animated {
if ( visible = = self . chatPanelVisible ) {
return ;
}
self . chatPanelVisible = visible ;
if ( visible ) {
// 记 录 打 开 聊 天 面 板 时 宿 主 输 入 框 已 有 的 文 本 , 发 送 时 只 取 新 增 部 分
[ [ KBInputBufferManager shared ]
refreshFromProxyIfPossible : self . textDocumentProxy ] ;
self . chatPanelBaselineText = [ KBInputBufferManager shared ] . liveText ? : @ "" ;
[ self kb_ensureChatPanelViewIfNeeded ] ;
self . chatPanelView . hidden = NO ;
self . chatPanelView . alpha = 0.0 ;
if ( animated ) {
[ UIView animateWithDuration : 0.2
delay : 0
options : UIViewAnimationOptionCurveEaseOut
animations : ^ {
self . chatPanelView . alpha = 1.0 ;
}
completion : nil ] ;
} else {
self . chatPanelView . alpha = 1.0 ;
}
} else {
// 从 未 创 建 过 聊 天 面 板 时 , 直 接 返 回 , 避 免 show / hide 触 发 额 外 内 存 分 配
if ( ! _chatPanelView ) {
[ self kb_updateKeyboardLayoutIfNeeded ] ;
return ;
}
if ( animated ) {
[ UIView animateWithDuration : 0.18
delay : 0
options : UIViewAnimationOptionCurveEaseIn
animations : ^ {
self . chatPanelView . alpha = 0.0 ;
}
completion : ^ ( BOOL finished ) {
self . chatPanelView . hidden = YES ;
} ] ;
} else {
self . chatPanelView . alpha = 0.0 ;
self . chatPanelView . hidden = YES ;
}
}
[ self kb_updateKeyboardLayoutIfNeeded ] ;
}
- ( void ) kb_setSubscriptionPanelVisible : ( BOOL ) visible animated : ( BOOL ) animated {
if ( visible ) {
KBKeyboardSubscriptionView * panel = self . subscriptionView ;
if ( ! panel . superview ) {
panel . hidden = YES ;
[ self . contentView addSubview : panel ] ;
[ panel mas_makeConstraints : ^ ( MASConstraintMaker * make ) {
make . edges . equalTo ( self . contentView ) ;
} ] ;
}
[ self . contentView bringSubviewToFront : panel ] ;
panel . hidden = NO ;
panel . alpha = 0.0 ;
CGFloat height = CGRectGetHeight ( self . contentView . bounds ) ;
if ( height <= 0 ) {
height = 260 ;
}
panel . transform = CGAffineTransformMakeTranslation ( 0 , height ) ;
[ panel refreshProductsIfNeeded ] ;
if ( animated ) {
[ UIView animateWithDuration : 0.25
delay : 0
options : UIViewAnimationOptionCurveEaseOut
animations : ^ {
panel . alpha = 1.0 ;
panel . transform = CGAffineTransformIdentity ;
}
completion : nil ] ;
} else {
panel . alpha = 1.0 ;
panel . transform = CGAffineTransformIdentity ;
}
return ;
}
KBKeyboardSubscriptionView * panel = _subscriptionView ;
if ( ! panel ) {
return ;
}
if ( ! panel . superview || panel . hidden ) {
return ;
}
CGFloat height = CGRectGetHeight ( panel . bounds ) ;
if ( height <= 0 ) {
height = CGRectGetHeight ( self . contentView . bounds ) ;
}
if ( animated ) {
[ 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 ;
} ] ;
} else {
panel . hidden = YES ;
panel . alpha = 1.0 ;
panel . transform = CGAffineTransformIdentity ;
}
}
// 延 迟 创 建 : 仅 在 用 户 真 正 打 开 功 能 面 板 时 才 创 建 / 布 局 , 降 低 默 认 内 存 占 用 。
- ( void ) kb_ensureFunctionViewIfNeeded {
if ( _functionView && _functionView . superview ) {
return ;
}
KBFunctionView * v = self . functionView ;
if ( ! v . superview ) {
v . hidden = YES ;
[ self . contentView addSubview : v ] ;
[ v mas_makeConstraints : ^ ( MASConstraintMaker * make ) {
make . edges . equalTo ( self . contentView ) ;
} ] ;
}
}
// 延 迟 创 建 : 仅 在 用 户 打 开 聊 天 面 板 时 才 创 建 / 布 局 。
- ( void ) kb_ensureChatPanelViewIfNeeded {
if ( _chatPanelView && _chatPanelView . superview ) {
return ;
}
CGFloat portraitWidth = [ self kb_portraitWidth ] ;
CGFloat chatPanelHeight = [ self kb_chatPanelHeightForWidth : portraitWidth ] ;
KBChatPanelView * v = self . chatPanelView ;
if ( ! v . superview ) {
[ self . contentView addSubview : v ] ;
[ v mas_makeConstraints : ^ ( MASConstraintMaker * make ) {
make . left . right . equalTo ( self . contentView ) ;
make . bottom . equalTo ( self . keyBoardMainView . mas_top ) ;
self . chatPanelHeightConstraint =
make . height . mas_equalTo ( chatPanelHeight ) ;
} ] ;
v . hidden = YES ;
}
}
// 延 迟 创 建 : 键 盘 主 面 板 ( 按 键 区 ) 在 隐 藏 时 会 被 释 放 ; 再 次 显 示 时 需 要 重 建 。
- ( void ) kb_ensureKeyBoardMainViewIfNeeded {
if ( _keyBoardMainView && _keyBoardMainView . superview ) {
return ;
}
CGFloat portraitWidth = [ self kb_portraitWidth ] ;
CGFloat keyboardBaseHeight =
[ self kb_keyboardBaseHeightForWidth : portraitWidth ] ;
KBKeyBoardMainView * v = self . keyBoardMainView ;
if ( ! v . superview ) {
[ self . contentView addSubview : v ] ;
[ v mas_makeConstraints : ^ ( MASConstraintMaker * make ) {
make . left . right . equalTo ( self . contentView ) ;
make . bottom . equalTo ( self . contentView ) ;
self . keyBoardMainHeightConstraint =
make . height . mas_equalTo ( keyboardBaseHeight ) ;
} ] ;
}
[ self . contentView bringSubviewToFront : v ] ;
}
// 键 盘 隐 藏 时 释 放 可 重 建 资 源 ( 背 景 图 / 缓 存 / 非 必 需 面 板 ) , 降 低 扩 展 内 存 峰 值 。
- ( void ) kb_releaseMemoryWhenKeyboardHidden {
[ KBHUD setContainerView : nil ] ;
self . bgImageView . image = nil ;
self . kb_cachedGradientImage = nil ;
[ self . kb_defaultGradientLayer removeFromSuperlayer ] ;
self . kb_defaultGradientLayer = nil ;
[ [ SDImageCache sharedImageCache ] clearMemory ] ;
// 聊 天 相 关 可 能 持 有 音 频 数 据 / 临 时 文 件 , 键 盘 隐 藏 时 直 接 清 空 , 避 免 累 计 占 用 。
if ( self . chatAudioPlayer ) {
[ self . chatAudioPlayer stop ] ;
self . chatAudioPlayer = nil ;
}
if ( _chatMessages . count > 0 ) {
NSString * tmpRoot = NSTemporaryDirectory ( ) ;
for ( KBChatMessage * msg in _chatMessages . copy ) {
if ( tmpRoot . length > 0 && msg . audioFilePath . length > 0 &&
[ msg . audioFilePath hasPrefix : tmpRoot ] ) {
[ [ NSFileManager defaultManager ] removeItemAtPath : msg . audioFilePath
error : nil ] ;
}
}
[ _chatMessages removeAllObjects ] ;
}
if ( _keyBoardMainView ) {
[ _keyBoardMainView removeFromSuperview ] ;
_keyBoardMainView = nil ;
}
self . keyBoardMainHeightConstraint = nil ;
if ( _functionView ) {
[ _functionView removeFromSuperview ] ;
_functionView = nil ;
}
if ( _chatPanelView ) {
[ _chatPanelView removeFromSuperview ] ;
_chatPanelView = nil ;
}
self . chatPanelVisible = NO ;
self . kb_panelMode = KBKeyboardPanelModeMain ;
if ( _subscriptionView ) {
[ _subscriptionView removeFromSuperview ] ;
_subscriptionView = nil ;
}
}
// MARK : - KBKeyBoardMainViewDelegate
- ( void ) keyBoardMainView : ( KBKeyBoardMainView * ) keyBoardMainView
didTapKey : ( KBKey * ) key {
switch ( key . type ) {
case KBKeyTypeCharacter : {
[ [ KBBackspaceUndoManager shared ] registerNonClearAction ] ;
NSString * text = key . output ? : key . title ? : @ "" ;
[ self . textDocumentProxy insertText : text ] ;
[ self kb_updateCurrentWordWithInsertedText : text ] ;
[ [ KBInputBufferManager shared ] appendText : text ] ;
} break ;
case KBKeyTypeBackspace :
[ [ KBInputBufferManager shared ]
refreshFromProxyIfPossible : self . textDocumentProxy ] ;
[ [ KBInputBufferManager shared ]
prepareSnapshotForDeleteWithContextBefore :
self . textDocumentProxy . documentContextBeforeInput
after :
self . textDocumentProxy
. documentContextAfterInput ] ;
[ [ KBBackspaceUndoManager shared ]
captureAndDeleteBackwardFromProxy : self . textDocumentProxy
count : 1 ] ;
[ self kb_scheduleContextRefreshResetSuppression : NO ] ;
[ [ KBInputBufferManager shared ] applyHoldDeleteCount : 1 ] ;
break ;
case KBKeyTypeSpace :
[ [ KBBackspaceUndoManager shared ] registerNonClearAction ] ;
[ self . textDocumentProxy insertText : @ " " ] ;
[ self kb_clearCurrentWord ] ;
[ [ KBInputBufferManager shared ] appendText : @ " " ] ;
break ;
case KBKeyTypeReturn :
if ( self . chatPanelVisible ) {
[ self kb_handleChatSendAction ] ;
break ;
}
[ [ KBBackspaceUndoManager shared ] registerNonClearAction ] ;
[ self . textDocumentProxy insertText : @ "\n" ] ;
[ self kb_clearCurrentWord ] ;
[ [ KBInputBufferManager shared ] appendText : @ "\n" ] ;
break ;
case KBKeyTypeGlobe :
[ self advanceToNextInputMode ] ;
break ;
case KBKeyTypeCustom :
[ [ KBBackspaceUndoManager shared ] registerNonClearAction ] ;
// 点 击 自 定 义 键 切 换 到 功 能 面 板
[ self kb_setPanelMode : KBKeyboardPanelModeFunction animated : NO ] ;
[ self kb_clearCurrentWord ] ;
break ;
case KBKeyTypeModeChange :
case KBKeyTypeShift :
// 这 些 已 在 KBKeyBoardMainView / KBKeyboardView 内 部 处 理
break ;
}
}
- ( void ) keyBoardMainView : ( KBKeyBoardMainView * ) keyBoardMainView
didTapToolActionAtIndex : ( NSInteger ) index {
NSDictionary * extra = @ { @ "index" : @ ( index ) } ;
[ [ KBMaiPointReporter sharedReporter ]
reportClickWithEventName : @ "click_keyboard_toolbar_action"
pageId : @ "keyboard_main_panel"
elementId : @ "toolbar_action"
extra : extra
completion : nil ] ;
if ( index = = 0 ) {
[ self kb_setPanelMode : KBKeyboardPanelModeFunction animated : YES ] ;
[ self kb_clearCurrentWord ] ;
return ;
}
if ( index = = 1 ) {
[ self kb_setPanelMode : KBKeyboardPanelModeChat animated : YES ] ;
return ;
}
[ self kb_setPanelMode : KBKeyboardPanelModeMain animated : YES ] ;
}
- ( void ) keyBoardMainView : ( KBKeyBoardMainView * ) keyBoardMainView
didSelectEmoji : ( NSString * ) emoji {
if ( emoji . length = = 0 ) {
return ;
}
[ [ KBBackspaceUndoManager shared ] registerNonClearAction ] ;
[ self . textDocumentProxy insertText : emoji ] ;
[ self kb_clearCurrentWord ] ;
[ [ KBInputBufferManager shared ] appendText : emoji ] ;
}
- ( void ) keyBoardMainViewDidTapUndo : ( KBKeyBoardMainView * ) keyBoardMainView {
[ [ KBMaiPointReporter sharedReporter ]
reportClickWithEventName : @ "click_keyboard_undo_btn"
pageId : @ "keyboard_main_panel"
elementId : @ "undo_btn"
extra : nil
completion : nil ] ;
[ [ KBBackspaceUndoManager shared ] performUndoFromResponder : self . view ] ;
[ self kb_scheduleContextRefreshResetSuppression : YES ] ;
}
// MARK : - KBFunctionViewDelegate
- ( void ) functionView : ( KBFunctionView * ) functionView
didTapToolActionAtIndex : ( NSInteger ) index {
// 需 求 : 当 index = = 0 时 , 切 回 键 盘 主 视 图
if ( index = = 0 ) {
[ self kb_setPanelMode : KBKeyboardPanelModeMain animated : NO ] ;
}
}
- ( void ) functionView : ( KBFunctionView * _Nullable ) functionView
didRightTapToolActionAtIndex : ( NSInteger ) index {
[ [ 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 ) {
2026-03-05 14:30:07 +08:00
NSURL * ul = [ NSURL URLWithString : [ NSString stringWithFormat : @ "%@?src=keyboard" , KB_UL _LOGIN ] ] ;
NSURL * scheme =
[ NSURL URLWithString : [ NSString stringWithFormat : @ "%@://login?src=keyboard" , KB_APP _SCHEME ] ] ;
__weak typeof ( self ) weakSelf = self ;
[ KBExtensionAppLauncher openPrimaryURL : ul
fallbackURL : scheme
usingInputController : self
source : ( self . view ? : ( UIResponder * ) weakSelf )
completion : nil ] ;
2026-02-24 13:38:51 +08:00
return ;
}
2026-03-05 14:30:07 +08:00
NSURL * ul = [ NSURL URLWithString : [ NSString stringWithFormat : @ "%@?src=keyboard" , KB_UL _RECHARGE ] ] ;
NSURL * scheme =
[ NSURL URLWithString : [ NSString stringWithFormat : @ "%@://recharge?src=keyboard" , KB_APP _SCHEME ] ] ;
__weak typeof ( self ) weakSelf = self ;
[ KBExtensionAppLauncher openPrimaryURL : ul
fallbackURL : scheme
usingInputController : self
source : ( self . view ? : ( UIResponder * ) weakSelf )
completion : ^ ( BOOL success ) {
if ( success ) {
return ;
}
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
2026-03-09 17:34:08 +08:00
[ KBHUD showInfo : KBLocalized ( @ "This app does not allow the keyboard to open the main app directly. Please return to the Home screen and open the app manually to recharge" ) ] ;
2026-03-05 14:30:07 +08:00
} ) ;
} ] ;
2026-02-24 13:38:51 +08:00
}
- ( void ) functionViewDidRequestSubscription : ( KBFunctionView * ) functionView {
[ self showSubscriptionPanel ] ;
}
@ end