Files
keyboard/CustomKeyboard/View/KBFunctionView.m
2025-11-12 19:46:07 +08:00

597 lines
26 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// KBFunctionView.m
// CustomKeyboard
//
// Created by Mac on 2025/10/28.
//
#import "KBFunctionView.h"
#import "KBResponderUtils.h" // 统一查找 UIInputViewController 的工具
#import "KBFunctionBarView.h"
#import "KBFunctionPasteView.h"
#import "KBFunctionTagCell.h"
#import "Masonry.h"
#import <MBProgressHUD.h>
#import "KBFullAccessGuideView.h"
#import "KBFullAccessManager.h"
#import "KBSkinManager.h"
#import "KBURLOpenBridge.h" // 兜底从扩展侧直接尝试 openURL:
#import "KBStreamTextView.h" // 流式文本视图
#import "KBStreamOverlayView.h" // 带关闭按钮的流式层
#import "KBFunctionTagListView.h"
#import "KBStreamFetcher.h" // 网络流封装
@interface KBFunctionView () <KBFunctionBarViewDelegate, KBStreamOverlayViewDelegate, KBFunctionTagListViewDelegate>
// UI
@property (nonatomic, strong) KBFunctionBarView *barViewInternal;
@property (nonatomic, strong) KBFunctionPasteView *pasteViewInternal;
@property (nonatomic, strong) KBFunctionTagListView *tagListView;
@property (nonatomic, strong) UIView *rightButtonContainer; // 右侧竖排按钮容器
@property (nonatomic, strong) UIButton *pasteButtonInternal;
@property (nonatomic, strong) UIButton *deleteButtonInternal;
@property (nonatomic, strong) UIButton *clearButtonInternal;
@property (nonatomic, strong) UIButton *sendButtonInternal;
// 叠层:流式文本视图 + 关闭按钮
@property (nonatomic, strong, nullable) KBStreamOverlayView *streamOverlay;
// 网络流式(封装)
@property (nonatomic, strong, nullable) KBStreamFetcher *streamFetcher;
@property (nonatomic, assign) BOOL streamHasOutput; // 是否已输出过正文(首段去首个 \t 用)
@property (nonatomic, strong, nullable) NSNumber *loadingTagIndex; // 当前显示loading的标签index
@property (nonatomic, copy, nullable) NSString *loadingTagTitle;
// Data
@property (nonatomic, strong) NSArray<NSString *> *itemsInternal;
// 剪贴板自动检测
@property (nonatomic, strong) NSTimer *pasteboardTimer; // 轮询定时器(轻量、主线程)
@property (nonatomic, assign) NSInteger lastHandledPBCount; // 上次处理过的 changeCount避免重复弹窗
@end
@implementation KBFunctionView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
// 背景使用当前主题强调色
[self kb_applyTheme];
[self setupUI];
[self reloadDemoData];
// 初始化剪贴板监控状态
_lastHandledPBCount = [UIPasteboard generalPasteboard].changeCount;
// 监听“完全访问”状态变化,动态启停剪贴板监控,避免在未开完全访问时触发 TCC/XPC 错误日志
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kb_fullAccessChanged) name:KBFullAccessChangedNotification object:nil];
}
return self;
}
#pragma mark - Theme
- (void)kb_applyTheme {
KBSkinManager *mgr = [KBSkinManager shared];
UIColor *accent = mgr.current.accentColor ?: [UIColor colorWithRed:0.77 green:0.93 blue:0.82 alpha:1.0];
BOOL hasImg = ([mgr currentBackgroundImage] != nil);
self.backgroundColor = hasImg ? [accent colorWithAlphaComponent:0.65] : accent;
}
- (void)dealloc {
[self stopPasteboardMonitor];
[self kb_stopNetworkStreaming];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - UI
- (void)setupUI {
// 1. 顶部 Bar
[self addSubview:self.barViewInternal];
[self.barViewInternal mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self);
make.top.equalTo(self.mas_top).offset(6);
make.height.mas_equalTo(48);
}];
// 右侧竖排按钮容器
[self addSubview:self.rightButtonContainer];
[self.rightButtonContainer mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(self.mas_right).offset(-6);
make.top.equalTo(self.barViewInternal.mas_bottom).offset(8);
make.bottom.equalTo(self.mas_bottom).offset(-10);
make.width.mas_equalTo(72);
}];
// 右侧四个按钮
[self.rightButtonContainer addSubview:self.pasteButtonInternal];
[self.rightButtonContainer addSubview:self.deleteButtonInternal];
[self.rightButtonContainer addSubview:self.clearButtonInternal];
[self.rightButtonContainer addSubview:self.sendButtonInternal];
// 竖向排布:粘贴、删除、清空为等高;发送优先更高,但允许在空间不足时压缩
CGFloat smallH = 44;
CGFloat bigH = 56;
// 原 10 在键盘总高度 276 下容易超出容器,改为 8 以避免 AutoLayout 冲突
CGFloat vSpace = 8;
[self.pasteButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.rightButtonContainer.mas_top);
make.left.right.equalTo(self.rightButtonContainer);
make.height.mas_equalTo(smallH);
}];
[self.deleteButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.pasteButtonInternal.mas_bottom).offset(vSpace);
make.left.right.equalTo(self.rightButtonContainer);
make.height.equalTo(self.pasteButtonInternal);
}];
[self.clearButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.deleteButtonInternal.mas_bottom).offset(vSpace);
make.left.right.equalTo(self.rightButtonContainer);
make.height.equalTo(self.pasteButtonInternal);
}];
[self.sendButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.clearButtonInternal.mas_bottom).offset(vSpace);
make.left.right.equalTo(self.rightButtonContainer);
// 允许在空间不足时缩短到 smallH避免产生约束冲突
make.height.greaterThanOrEqualTo(@(smallH));
make.height.lessThanOrEqualTo(@(bigH));
make.bottom.lessThanOrEqualTo(self.rightButtonContainer.mas_bottom);
}];
// 2. 粘贴区(位于右侧按钮左侧)
[self addSubview:self.pasteViewInternal];
[self.pasteViewInternal mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.mas_left).offset(12);
make.right.equalTo(self.rightButtonContainer.mas_left).offset(-12);
make.top.equalTo(self.barViewInternal.mas_bottom).offset(8);
make.height.mas_equalTo(48);
}];
// 3. Tag List View
[self addSubview:self.tagListView];
[self.tagListView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.mas_left).offset(12);
make.right.equalTo(self.rightButtonContainer.mas_left).offset(-12);
make.top.equalTo(self.pasteViewInternal.mas_bottom).offset(10);
make.bottom.equalTo(self.mas_bottom).offset(-10);
}];
}
#pragma mark - Data
- (void)reloadDemoData {
// 演示数据(可由外部替换)
self.itemsInternal = @[@"高情商", @"暖味拉扯", @"风趣幽默", @"撩女生", @"社交惬匿", @"情场高手", @"一枚暖男", @"聊天搭子", @"表达爱意", @"更多话术"];
[self.tagListView setItems:self.itemsInternal];
}
// UICollectionView 逻辑已下沉至 KBFunctionTagListView
- (void)kb_showStreamTextViewIfNeededWithTitle:(NSString *)title {
// 已有则不重复创建
if (self.streamOverlay.superview) { return; }
// 隐藏标签列表,使用同一区域展示流式文本
self.tagListView.hidden = YES;
KBStreamOverlayView *overlay = [[KBStreamOverlayView alloc] init];
overlay.delegate = (id)self;
[self addSubview:overlay];
[overlay mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.mas_left).offset(12);
// 流式期间占满右侧宽度:不再贴 rightButtonContainer让文本有最大宽度
make.right.equalTo(self.mas_right).offset(-12);
make.top.equalTo(self.pasteViewInternal.mas_bottom).offset(10);
make.bottom.equalTo(self.mas_bottom).offset(-10);
}];
// 隐藏右侧按钮栏,最大化文本宽度;关闭 overlay 时再恢复
self.rightButtonContainer.hidden = YES;
// 适当缩小内部左右留白,进一步提升可用宽度
overlay.textView.contentHorizontalPadding = 8.0;
self.streamOverlay = overlay;
// 只创建 UI网络在点击 cell 时启动,避免重复 start 导致首包重复
}
- (void)kb_onTapStreamDelete {
// 关闭并销毁流式视图,恢复标签列表
[self kb_stopNetworkStreaming];
[self.streamOverlay removeFromSuperview];
self.streamOverlay = nil;
self.tagListView.hidden = NO;
// 恢复右侧按钮栏
self.rightButtonContainer.hidden = NO;
}
// 叠层关闭回调
- (void)streamOverlayDidTapClose:(KBStreamOverlayView *)overlay {
[self kb_onTapStreamDelete];
}
#pragma mark - Network Streaming (GET)
// 后端测试地址(内网)。若被 ATS 拦截,请在扩展 target 的 Info.plist 中允许 HTTP 或添加例外域。
static NSString * const kKBStreamDemoURL = @"http://192.168.1.144:7529/api/demo/talk";
- (void)kb_startNetworkStreamingWithSeed:(NSString *)seedTitle {
[self kb_stopNetworkStreaming];
if (![[KBFullAccessManager shared] hasFullAccess]) { return; }
NSURL *url = [NSURL URLWithString:kKBStreamDemoURL];
if (!url) { return; }
self.streamHasOutput = NO; // 重置首段处理标记
__weak typeof(self) weakSelf = self;
KBStreamFetcher *fetcher = [KBStreamFetcher fetcherWithURL:url];
// 由本类统一做 /t->\t 与首段去 \tfetcher 只负责增量与协议解析
fetcher.disableCompression = YES;
fetcher.acceptEventStream = NO; // 响应头若是 SSE 仍会自动解析
// 将 \t 与首段去 \t 的处理下沉到 Fetcher避免 UI 抖动
fetcher.treatSlashTAsTab = YES;
fetcher.trimLeadingTabOnce = YES;
fetcher.flushInterval = 0.05; // 更接近“立刻显示”的节奏
fetcher.onChunk = ^(NSString *chunk) {
__strong typeof(weakSelf) self = weakSelf; if (!self) return;
[self kb_appendChunkToStreamView:chunk];
};
fetcher.onFinish = ^(NSError * _Nullable error) {
__strong typeof(weakSelf) self = weakSelf; if (!self) return;
// 若还未出现任何数据且仍在 loading则取消小菊花
if (!self.streamHasOutput && self.loadingTagIndex) {
[self.tagListView setLoading:NO atIndex:self.loadingTagIndex.integerValue];
self.loadingTagIndex = nil; self.loadingTagTitle = nil;
}
if (error) { [KBHUD showInfo:@"拉取失败"]; }
if (self.streamOverlay) { [self.streamOverlay finish]; }
};
self.streamFetcher = fetcher;
[self.streamFetcher start];
}
- (void)kb_stopNetworkStreaming {
[self.streamFetcher cancel];
self.streamFetcher = nil;
self.streamHasOutput = NO;
}
#pragma mark - Helpers
/// 统一处理需要输出到 KBStreamTextView 的分片:
/// - 目前网络层KBStreamFetcher已做 “/t->\t、首段去一个 \t、段间去一个空格”
/// - 这里仅负责附加到视图与标记首段状态,避免 UI 抖动
- (void)kb_appendChunkToStreamView:(NSString *)chunk {
if (chunk.length == 0) return;
// 第一次有数据才创建 overlay并取消 cell 上的小菊花
if (!self.streamOverlay) {
[self kb_showStreamTextViewIfNeededWithTitle:self.loadingTagTitle ?: @""];
if (self.loadingTagIndex) {
[self.tagListView setLoading:NO atIndex:self.loadingTagIndex.integerValue];
self.loadingTagIndex = nil; self.loadingTagTitle = nil;
}
}
if (!self.streamOverlay) return;
[self.streamOverlay appendChunk:chunk];
self.streamHasOutput = YES;
}
#pragma mark - KBFunctionTagListViewDelegate
- (void)tagListView:(KBFunctionTagListView *)view didSelectIndex:(NSInteger)index title:(NSString *)title {
// 权限全部打开(键盘已启用 + 完全访问)。在扩展进程中仅需判断“完全访问”。
if ([[KBFullAccessManager shared] hasFullAccess]) {
// 先在 cell 上显示小菊花,等有数据回来再弹出 overlay
[self.tagListView setLoading:YES atIndex:index];
self.loadingTagIndex = @(index);
self.loadingTagTitle = title ?: @"";
[self kb_startNetworkStreamingWithSeed:self.loadingTagTitle];
return;
}
// 未开启完全访问:保持原有引导路径
[KBHUD showInfo:@"处理中…"];
UIInputViewController *ivc = KBFindInputViewController(self);
if (!ivc) return;
NSString *encodedTitle = [title stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]] ?: @"";
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=functionView&index=%ld&title=%@", KB_UL_LOGIN, (long)index, encodedTitle]];
if (!ul) return;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[ivc.extensionContext openURL:ul completionHandler:^(BOOL ok) {
if (ok) return; // Universal Link 成功
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=functionView&index=%ld&title=%@", KB_APP_SCHEME, (long)index, encodedTitle]];
[ivc.extensionContext openURL:scheme completionHandler:^(BOOL ok2) {
if (ok2) return;
BOOL bridged = NO;
@try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
bridged = [KBURLOpenBridge openURLViaResponder:scheme from:self];
#pragma clang diagnostic pop
} @catch (__unused NSException *e) { bridged = NO; }
if (!bridged) {
dispatch_async(dispatch_get_main_queue(), ^{ [[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self]; });
}
}];
}];
});
}
// 用户点击功能标签:优先 UL 拉起主App失败再 Scheme两次都失败则提示开启完全访问。
// 若已开启“完全访问”,则直接在键盘侧创建 KBStreamTextView并在其右上角提供删除按钮关闭。
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
// 权限全部打开(键盘已启用 + 完全访问)。在扩展进程中仅需判断“完全访问”。
if ([[KBFullAccessManager shared] hasFullAccess]) {
NSString *title = (indexPath.item < self.itemsInternal.count) ? self.itemsInternal[indexPath.item] : @"";
[self kb_showStreamTextViewIfNeededWithTitle:title];
return;
}
[KBHUD showInfo:@"处理中…"];
UIInputViewController *ivc = KBFindInputViewController(self);
if (!ivc) return;
NSString *title = (indexPath.item < self.itemsInternal.count) ? self.itemsInternal[indexPath.item] : @"";
NSString *encodedTitle = [title stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]] ?: @"";
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=functionView&index=%ld&title=%@", KB_UL_LOGIN, (long)indexPath.item, encodedTitle]];
if (!ul) return;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[ivc.extensionContext openURL:ul completionHandler:^(BOOL ok) {
if (ok) return; // Universal Link 成功
// 统一使用主 App 注册的自定义 Scheme
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=functionView&index=%ld&title=%@", KB_APP_SCHEME, (long)indexPath.item, encodedTitle]];
[ivc.extensionContext openURL:scheme completionHandler:^(BOOL ok2) {
if (ok2) return;
// 兜底:在用户点击触发的场景下,尝试通过响应链调用 openURL:
// 以提升在“备忘录”等宿主中的成功率。
BOOL bridged = NO;
@try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
bridged = [KBURLOpenBridge openURLViaResponder:scheme from:self];
#pragma clang diagnostic pop
} @catch (__unused NSException *e) { bridged = NO; }
if (!bridged) {
// 两条路都失败:大概率未开完全访问或宿主拦截。统一交由 Manager 引导。
dispatch_async(dispatch_get_main_queue(), ^{ [[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self]; });
}
}];
}];
});
}
#pragma mark - Button Actions
- (void)onTapPaste {
// 用户点击“粘贴”时才读取剪贴板:
// - iOS16+ 会在跨 App 首次读取时自动弹出系统权限弹窗;
// - iOS15 及以下不会弹窗,直接返回内容;
// 注意:不要在非用户触发的时机主动读取(如 viewDidLoad否则会造成“立刻弹窗”的体验。
UIPasteboard *pb = [UIPasteboard generalPasteboard];
NSString *text = pb.string; // 读取纯文本(可能触发系统粘贴权限弹窗)
if (text.length > 0) {
// 将粘贴内容展示到左侧“粘贴区”的占位文案上
self.pasteView.placeholderLabel.text = text;
// 如果需要多行展示,可按需放开(高度由外部约束决定,默认一行会截断)
// self.pasteView.placeholderLabel.numberOfLines = 0;
} else {
// 无可用文本或用户拒绝了粘贴权限;保持占位文案不变
NSLog(@"粘贴板无可用文本或未授权粘贴");
}
}
#pragma mark - 自动监控剪贴板(复制即弹窗)
// 说明:
// - 仅在视图可见时开启轮询,避免不必要的读取与打扰;
// - 当检测到 changeCount 变化,立即读 pasteboard.string
// * iOS16+:此处会触发系统“是否允许粘贴”弹窗;
// * iOS15不会弹窗直接得到文本
// - 无论允许/拒绝,都把本次 changeCount 记为已处理,避免一直重复询问。
- (void)startPasteboardMonitor {
// 未开启“完全访问”时不做自动读取,避免宿主/系统拒绝并刷错误日志
if (![[KBFullAccessManager shared] hasFullAccess]) return;
if (self.pasteboardTimer) return;
KBWeakSelf
self.pasteboardTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 repeats:YES block:^(NSTimer * _Nonnull timer) {
__strong typeof(weakSelf) self = weakSelf; if (!self) return;
UIPasteboard *pb = [UIPasteboard generalPasteboard];
NSInteger cc = pb.changeCount;
if (cc <= self.lastHandledPBCount) return; // 没有新复制
self.lastHandledPBCount = cc; // 标记已处理,避免重复
// 实际读取触发系统弹窗iOS16+
NSString *text = pb.string;
if (text.length > 0) {
self.pasteView.placeholderLabel.text = text;
}
}];
}
- (void)stopPasteboardMonitor {
[self.pasteboardTimer invalidate];
self.pasteboardTimer = nil;
}
- (void)didMoveToWindow {
[super didMoveToWindow];
[self kb_refreshPasteboardMonitor];
}
- (void)setHidden:(BOOL)hidden {
BOOL wasHidden = self.isHidden;
[super setHidden:hidden];
if (wasHidden != hidden) {
[self kb_refreshPasteboardMonitor];
}
}
// 根据窗口可见性与完全访问状态,统一启停粘贴板监控
- (void)kb_refreshPasteboardMonitor {
BOOL visible = (self.window && !self.isHidden);
if (visible && [[KBFullAccessManager shared] hasFullAccess]) {
[self startPasteboardMonitor];
} else {
[self stopPasteboardMonitor];
}
}
- (void)kb_fullAccessChanged {
dispatch_async(dispatch_get_main_queue(), ^{ [self kb_refreshPasteboardMonitor]; });
}
- (void)onTapDelete {
NSLog(@"点击:删除");
UIInputViewController *ivc = KBFindInputViewController(self);
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
[proxy deleteBackward];
}
- (void)onTapClear {
NSLog(@"点击:清空");
// 连续删除:仅清空光标之前的输入(不改动 pasteView 的内容)
UIInputViewController *ivc = KBFindInputViewController(self);
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
// 逐批读取 documentContextBeforeInput 并删除,避免 50 字符窗口限制带来的残留
NSInteger guard = 0; // 上限保护,避免极端情况下长时间阻塞
while (guard < 10000) {
NSString *before = proxy.documentContextBeforeInput ?: @"";
NSInteger count = before.length;
if (count <= 0) { break; } // 光标前已无内容
for (NSInteger i = 0; i < count; i++) {
[proxy deleteBackward];
}
guard += count;
}
}
- (void)onTapSend {
NSLog(@"点击:发送");
// 发送:插入换行。大多数聊天类 App 会把回车视为“发送”
UIInputViewController *ivc = KBFindInputViewController(self);
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
[proxy insertText:@"\n"];
}
#pragma mark - Lazy
- (KBFunctionBarView *)barViewInternal {
if (!_barViewInternal) {
_barViewInternal = [[KBFunctionBarView alloc] init];
_barViewInternal.delegate = self; // 顶部功能Bar事件下发到本View
}
return _barViewInternal;
}
#pragma mark - KBFunctionBarViewDelegate
- (void)functionBarView:(KBFunctionBarView *)bar didTapLeftAtIndex:(NSInteger)index {
// 将事件继续透传给上层(如键盘控制器),用于切换界面或其它业务
if ([self.delegate respondsToSelector:@selector(functionView:didTapToolActionAtIndex:)]) {
[self.delegate functionView:self didTapToolActionAtIndex:index];
}
}
- (void)functionBarView:(KBFunctionBarView *)bar didTapRightAtIndex:(NSInteger)index {
// 右侧按钮点击,如收藏/宫格等,按需继续向外抛出(这里暂不定义单独协议方法,可在此内部处理或扩展)
}
- (KBFunctionPasteView *)pasteViewInternal {
if (!_pasteViewInternal) {
_pasteViewInternal = [[KBFunctionPasteView alloc] init];
}
return _pasteViewInternal;
}
- (KBFunctionTagListView *)tagListView {
if (!_tagListView) {
_tagListView = [[KBFunctionTagListView alloc] init];
_tagListView.delegate = (id)self;
}
return _tagListView;
}
- (UIView *)rightButtonContainer {
if (!_rightButtonContainer) {
_rightButtonContainer = [[UIView alloc] init];
_rightButtonContainer.backgroundColor = [UIColor clearColor];
}
return _rightButtonContainer;
}
- (UIButton *)buildRightButtonWithTitle:(NSString *)title color:(UIColor *)color {
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
btn.backgroundColor = color;
btn.layer.cornerRadius = 12.0;
btn.layer.masksToBounds = YES;
btn.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
[btn setTitle:title forState:UIControlStateNormal];
[btn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
return btn;
}
- (UIButton *)pasteButtonInternal {
if (!_pasteButtonInternal) {
_pasteButtonInternal = [self buildRightButtonWithTitle:@"粘贴" color:[UIColor colorWithRed:0.13 green:0.73 blue:0.60 alpha:1.0]];
[_pasteButtonInternal addTarget:self action:@selector(onTapPaste) forControlEvents:UIControlEventTouchUpInside];
}
return _pasteButtonInternal;
}
- (UIButton *)deleteButtonInternal {
if (!_deleteButtonInternal) {
// 浅灰底深色文字,更接近截图里“删除”样式
_deleteButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem];
_deleteButtonInternal.backgroundColor = [UIColor colorWithWhite:0.92 alpha:1.0];
_deleteButtonInternal.layer.cornerRadius = 12.0;
_deleteButtonInternal.layer.masksToBounds = YES;
_deleteButtonInternal.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
[_deleteButtonInternal setTitle:@"删除" forState:UIControlStateNormal];
[_deleteButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[_deleteButtonInternal addTarget:self action:@selector(onTapDelete) forControlEvents:UIControlEventTouchUpInside];
}
return _deleteButtonInternal;
}
- (UIButton *)clearButtonInternal {
if (!_clearButtonInternal) {
_clearButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem];
_clearButtonInternal.backgroundColor = [UIColor colorWithWhite:0.92 alpha:1.0];
_clearButtonInternal.layer.cornerRadius = 12.0;
_clearButtonInternal.layer.masksToBounds = YES;
_clearButtonInternal.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
[_clearButtonInternal setTitle:@"清空" forState:UIControlStateNormal];
[_clearButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[_clearButtonInternal addTarget:self action:@selector(onTapClear) forControlEvents:UIControlEventTouchUpInside];
}
return _clearButtonInternal;
}
- (UIButton *)sendButtonInternal {
if (!_sendButtonInternal) {
_sendButtonInternal = [self buildRightButtonWithTitle:@"发送" color:[UIColor colorWithRed:0.13 green:0.73 blue:0.60 alpha:1.0]];
[_sendButtonInternal addTarget:self action:@selector(onTapSend) forControlEvents:UIControlEventTouchUpInside];
}
return _sendButtonInternal;
}
#pragma mark - Expose
- (UICollectionView *)collectionView { return self.tagListView.collectionView; }
- (NSArray<NSString *> *)items { return self.itemsInternal; }
- (KBFunctionBarView *)barView { return self.barViewInternal; }
- (KBFunctionPasteView *)pasteView { return self.pasteViewInternal; }
- (UIButton *)pasteButton { return self.pasteButtonInternal; }
- (UIButton *)deleteButton { return self.deleteButtonInternal; }
- (UIButton *)clearButton { return self.clearButtonInternal; }
- (UIButton *)sendButton { return self.sendButtonInternal; }
#pragma mark - Find Owner Controller
// 工具方法已提取到 KBResponderUtils.h
@end