This commit is contained in:
2026-03-02 09:19:06 +08:00
parent da4649101e
commit 781e557e80
34 changed files with 3926 additions and 87 deletions

View File

@@ -0,0 +1,37 @@
//
// KBKeyboardLayoutResolver.h
// CustomKeyboard
//
// 扩展侧布局解析器:根据 profileId 解析对应的布局配置
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface KBKeyboardLayoutResolver : NSObject
+ (instancetype)sharedResolver;
/// 根据 profileId 获取对应的布局 JSON ID
/// @param profileId 输入配置 ID如 "es_ES_azerty"
/// @return 布局 JSON ID如 "letters_azerty"),如果未找到返回 "letters"
- (NSString *)layoutJsonIdForProfileId:(NSString *)profileId;
/// 根据 profileId 获取对应的联想引擎类型
/// @param profileId 输入配置 ID
/// @return 联想引擎类型(如 "latin", "pinyin_traditional", "bopomofo"
- (NSString *)suggestionEngineForProfileId:(NSString *)profileId;
/// 从 App Group 读取当前选中的 profileId
- (nullable NSString *)currentProfileId;
/// 从 App Group 读取当前选中的语言代码
- (nullable NSString *)currentLanguageCode;
/// 从 App Group 读取当前选中的布局变体
- (nullable NSString *)currentLayoutVariant;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,69 @@
//
// KBKeyboardLayoutResolver.m
// CustomKeyboard
//
#import "KBKeyboardLayoutResolver.h"
#import "KBInputProfileManager.h"
#import "KBConfig.h"
@implementation KBKeyboardLayoutResolver
+ (instancetype)sharedResolver {
static KBKeyboardLayoutResolver *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
- (NSString *)layoutJsonIdForProfileId:(NSString *)profileId {
if (profileId.length == 0) {
return @"letters";
}
NSString *layoutJsonId = [[KBInputProfileManager sharedManager] layoutJsonIdForProfileId:profileId];
if (layoutJsonId.length > 0) {
return layoutJsonId;
}
// 退
NSLog(@"[KBKeyboardLayoutResolver] No layoutJsonId found for profileId: %@, using default 'letters'", profileId);
return @"letters";
}
- (NSString *)suggestionEngineForProfileId:(NSString *)profileId {
if (profileId.length == 0) {
return @"latin";
}
NSString *engine = [[KBInputProfileManager sharedManager] suggestionEngineForProfileId:profileId];
if (engine.length > 0) {
return engine;
}
// 退
NSLog(@"[KBKeyboardLayoutResolver] No suggestionEngine found for profileId: %@, using default 'latin'", profileId);
return @"latin";
}
- (nullable NSString *)currentProfileId {
NSUserDefaults *appGroup = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
NSString *profileId = [appGroup stringForKey:AppGroup_SelectedKeyboardProfileId];
return profileId;
}
- (nullable NSString *)currentLanguageCode {
NSUserDefaults *appGroup = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
NSString *languageCode = [appGroup stringForKey:AppGroup_SelectedKeyboardLanguageCode];
return languageCode;
}
- (nullable NSString *)currentLayoutVariant {
NSUserDefaults *appGroup = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
NSString *layoutVariant = [appGroup stringForKey:AppGroup_SelectedKeyboardLayoutVariant];
return layoutVariant;
}
@end

View File

@@ -7,9 +7,18 @@
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, KBSuggestionEngineType) {
KBSuggestionEngineTypeLatin = 0, // 拉丁字母(英语、西班牙语、葡萄牙语、印尼语)
KBSuggestionEngineTypePinyinSimplified, // 简体拼音
KBSuggestionEngineTypePinyinTraditional, // 繁体拼音
KBSuggestionEngineTypeBopomofo // 注音(繁体)
};
/// Simple local suggestion engine (prefix match + lightweight ranking).
@interface KBSuggestionEngine : NSObject
@property (nonatomic, assign) KBSuggestionEngineType engineType;
+ (instancetype)shared;
/// Returns suggestions for prefix (lowercase expected), limited by count.
@@ -18,6 +27,9 @@ NS_ASSUME_NONNULL_BEGIN
/// Record a selection to slightly boost ranking next time.
- (void)recordSelection:(NSString *)word;
/// 设置联想引擎类型(根据 profileId 的 suggestionEngine 字段)
- (void)setEngineTypeFromString:(NSString *)engineTypeString;
@end
NS_ASSUME_NONNULL_END

View File

@@ -10,6 +10,8 @@
@property (nonatomic, copy) NSArray<NSString *> *words;
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSNumber *> *selectionCounts;
@property (nonatomic, strong) NSSet<NSString *> *priorityWords;
@property (nonatomic, copy) NSArray<NSString *> *traditionalChineseWords;
@property (nonatomic, copy) NSArray<NSString *> *simplifiedChineseWords;
@end
@implementation KBSuggestionEngine
@@ -25,49 +27,32 @@
- (instancetype)init {
if (self = [super init]) {
_engineType = KBSuggestionEngineTypeLatin;
_selectionCounts = [NSMutableDictionary dictionary];
NSArray<NSString *> *defaults = [self.class kb_defaultWords];
_priorityWords = [NSSet setWithArray:defaults];
_words = [self kb_loadWords];
_traditionalChineseWords = [self kb_loadTraditionalChineseWords];
_simplifiedChineseWords = [self kb_loadSimplifiedChineseWords];
}
return self;
}
- (NSArray<NSString *> *)suggestionsForPrefix:(NSString *)prefix limit:(NSUInteger)limit {
if (prefix.length == 0 || limit == 0) { return @[]; }
NSString *lower = prefix.lowercaseString;
NSMutableArray<NSString *> *matches = [NSMutableArray array];
for (NSString *word in self.words) {
if ([word hasPrefix:lower]) {
[matches addObject:word];
if (matches.count >= limit * 3) {
// Avoid scanning too many matches for long lists.
break;
}
}
//
switch (self.engineType) {
case KBSuggestionEngineTypePinyinTraditional:
return [self kb_traditionalPinyinSuggestionsForPrefix:prefix limit:limit];
case KBSuggestionEngineTypePinyinSimplified:
return [self kb_simplifiedPinyinSuggestionsForPrefix:prefix limit:limit];
case KBSuggestionEngineTypeBopomofo:
return [self kb_bopomofoSuggestionsForPrefix:prefix limit:limit];
case KBSuggestionEngineTypeLatin:
default:
return [self kb_latinSuggestionsForPrefix:prefix limit:limit];
}
if (matches.count == 0) { return @[]; }
[matches sortUsingComparator:^NSComparisonResult(NSString *a, NSString *b) {
NSInteger ca = self.selectionCounts[a].integerValue;
NSInteger cb = self.selectionCounts[b].integerValue;
if (ca != cb) {
return (cb > ca) ? NSOrderedAscending : NSOrderedDescending;
}
BOOL pa = [self.priorityWords containsObject:a];
BOOL pb = [self.priorityWords containsObject:b];
if (pa != pb) {
return pa ? NSOrderedAscending : NSOrderedDescending;
}
return [a compare:b];
}];
if (matches.count > limit) {
return [matches subarrayWithRange:NSMakeRange(0, limit)];
}
return matches.copy;
}
- (void)recordSelection:(NSString *)word {
@@ -164,4 +149,148 @@
];
}
#pragma mark - Engine Type Management
- (void)setEngineTypeFromString:(NSString *)engineTypeString {
if ([engineTypeString isEqualToString:@"latin"]) {
self.engineType = KBSuggestionEngineTypeLatin;
} else if ([engineTypeString isEqualToString:@"pinyin_traditional"]) {
self.engineType = KBSuggestionEngineTypePinyinTraditional;
} else if ([engineTypeString isEqualToString:@"pinyin_simplified"]) {
self.engineType = KBSuggestionEngineTypePinyinSimplified;
} else if ([engineTypeString isEqualToString:@"bopomofo"]) {
self.engineType = KBSuggestionEngineTypeBopomofo;
} else {
self.engineType = KBSuggestionEngineTypeLatin;
}
NSLog(@"[KBSuggestionEngine] Engine type set to: %@", engineTypeString);
}
#pragma mark - Latin Suggestions
- (NSArray<NSString *> *)kb_latinSuggestionsForPrefix:(NSString *)prefix limit:(NSUInteger)limit {
NSString *lower = prefix.lowercaseString;
NSMutableArray<NSString *> *matches = [NSMutableArray array];
for (NSString *word in self.words) {
if ([word hasPrefix:lower]) {
[matches addObject:word];
if (matches.count >= limit * 3) {
break;
}
}
}
if (matches.count == 0) { return @[]; }
[matches sortUsingComparator:^NSComparisonResult(NSString *a, NSString *b) {
NSInteger ca = self.selectionCounts[a].integerValue;
NSInteger cb = self.selectionCounts[b].integerValue;
if (ca != cb) {
return (cb > ca) ? NSOrderedAscending : NSOrderedDescending;
}
BOOL pa = [self.priorityWords containsObject:a];
BOOL pb = [self.priorityWords containsObject:b];
if (pa != pb) {
return pa ? NSOrderedAscending : NSOrderedDescending;
}
return [a compare:b];
}];
if (matches.count > limit) {
return [matches subarrayWithRange:NSMakeRange(0, limit)];
}
return matches.copy;
}
#pragma mark - Traditional Chinese Pinyin Suggestions
- (NSArray<NSString *> *)kb_traditionalPinyinSuggestionsForPrefix:(NSString *)prefix limit:(NSUInteger)limit {
//
NSString *lower = prefix.lowercaseString;
NSMutableArray<NSString *> *matches = [NSMutableArray array];
// 使
//
for (NSString *word in self.traditionalChineseWords) {
// TODO:
//
[matches addObject:word];
if (matches.count >= limit) {
break;
}
}
return matches.copy;
}
#pragma mark - Simplified Chinese Pinyin Suggestions
- (NSArray<NSString *> *)kb_simplifiedPinyinSuggestionsForPrefix:(NSString *)prefix limit:(NSUInteger)limit {
//
NSString *lower = prefix.lowercaseString;
NSMutableArray<NSString *> *matches = [NSMutableArray array];
// 使
for (NSString *word in self.simplifiedChineseWords) {
// TODO:
[matches addObject:word];
if (matches.count >= limit) {
break;
}
}
return matches.copy;
}
#pragma mark - Bopomofo (Zhuyin) Suggestions
- (NSArray<NSString *> *)kb_bopomofoSuggestionsForPrefix:(NSString *)prefix limit:(NSUInteger)limit {
//
NSMutableArray<NSString *> *matches = [NSMutableArray array];
// 使
//
//
// ˊˇˋ˙
for (NSString *word in self.traditionalChineseWords) {
// TODO:
[matches addObject:word];
if (matches.count >= limit) {
break;
}
}
return matches.copy;
}
#pragma mark - Chinese Word Loading
- (NSArray<NSString *> *)kb_loadTraditionalChineseWords {
//
//
return @[
@"你好", @"謝謝", @"對不起", @"再見", @"早安",
@"晚安", @"請問", @"不好意思", @"沒關係", @"加油",
@"台灣", @"台北", @"高雄", @"台中", @"台南",
@"朋友", @"家人", @"工作", @"學習", @"生活",
@"時間", @"地點", @"方法", @"問題", @"答案",
@"喜歡", @"愛", @"想念", @"開心", @"快樂",
@"美麗", @"漂亮", @"帥氣", @"可愛", @"溫柔"
];
}
- (NSArray<NSString *> *)kb_loadSimplifiedChineseWords {
//
return @[
@"你好", @"谢谢", @"对不起", @"再见", @"早安",
@"晚安", @"请问", @"不好意思", @"没关系", @"加油",
@"中国", @"北京", @"上海", @"广州", @"深圳",
@"朋友", @"家人", @"工作", @"学习", @"生活",
@"时间", @"地点", @"方法", @"问题", @"答案",
@"喜欢", @"爱", @"想念", @"开心", @"快乐",
@"美丽", @"漂亮", @"帅气", @"可爱", @"温柔"
];
}
@end