diff --git a/Fire.xcodeproj/project.pbxproj b/Fire.xcodeproj/project.pbxproj index 874131a..c82d4ed 100644 --- a/Fire.xcodeproj/project.pbxproj +++ b/Fire.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ 4500AC622869F8CC006F3FCC /* PunctutionPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4500AC612869F8CC006F3FCC /* PunctutionPane.swift */; }; + 4500AC64286F2B42006F3FCC /* UserDictPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4500AC63286F2B42006F3FCC /* UserDictPane.swift */; }; + 4500AC68287036CB006F3FCC /* DictManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4500AC67287036CB006F3FCC /* DictManager.swift */; }; 450B7D9A26A2847D00808A4D /* ApplicationPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450B7D9926A2847D00808A4D /* ApplicationPane.swift */; }; 451E6048232E227B007B0463 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451E6047232E227B007B0463 /* AppDelegate.swift */; }; 451E6056232E24A5007B0463 /* FireInputController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451E6055232E24A5007B0463 /* FireInputController.swift */; }; @@ -78,6 +80,8 @@ /* Begin PBXFileReference section */ 4500AC612869F8CC006F3FCC /* PunctutionPane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PunctutionPane.swift; sourceTree = ""; }; + 4500AC63286F2B42006F3FCC /* UserDictPane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDictPane.swift; sourceTree = ""; }; + 4500AC67287036CB006F3FCC /* DictManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictManager.swift; sourceTree = ""; }; 450B7D9926A2847D00808A4D /* ApplicationPane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPane.swift; sourceTree = ""; }; 451E6044232E227B007B0463 /* Fire.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Fire.app; sourceTree = BUILT_PRODUCTS_DIR; }; 451E6047232E227B007B0463 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -200,6 +204,7 @@ 45577EAF254575480064325B /* types.swift */, 45ACDE1A2841C52500658F46 /* Bridging-Header.h */, 453377E42849E76A0064E4F2 /* StatusBar.swift */, + 4500AC67287036CB006F3FCC /* DictManager.swift */, ); path = Fire; sourceTree = ""; @@ -250,6 +255,7 @@ 45DB6EC827E609CB00A39925 /* ThemePane.swift */, 45EBB54F283A311C00A56CBA /* StatisticsPane.swift */, 4500AC612869F8CC006F3FCC /* PunctutionPane.swift */, + 4500AC63286F2B42006F3FCC /* UserDictPane.swift */, ); path = Preferences; sourceTree = ""; @@ -466,6 +472,7 @@ buildActionMask = 2147483647; files = ( 673C417225468FFA00F462A3 /* ModifierKeyUpChecker.swift in Sources */, + 4500AC64286F2B42006F3FCC /* UserDictPane.swift in Sources */, 45577EA1254552110064325B /* ThesaurusPane.swift in Sources */, 451E6056232E24A5007B0463 /* FireInputController.swift in Sources */, 451E605F232E400B007B0463 /* Fire.swift in Sources */, @@ -483,6 +490,7 @@ 45577EB0254575480064325B /* types.swift in Sources */, 45577EB4254576720064325B /* FirePreferencesController.swift in Sources */, 450B7D9A26A2847D00808A4D /* ApplicationPane.swift in Sources */, + 4500AC68287036CB006F3FCC /* DictManager.swift in Sources */, 45DCE62226A31F140009FED1 /* ApplicationSettingCache.swift in Sources */, 459DE990232EB26600A3ACD1 /* CandidatesView.swift in Sources */, 45DB6EC727E5B8FE00A39925 /* ThemeConfig.swift in Sources */, diff --git a/Fire/CandidatesView.swift b/Fire/CandidatesView.swift index 8d2c1eb..0a1722e 100644 --- a/Fire/CandidatesView.swift +++ b/Fire/CandidatesView.swift @@ -10,7 +10,7 @@ import SwiftUI import Defaults func getShownCode(candidate: Candidate, origin: String) -> String { - if candidate.type == "py" || !candidate.code.hasPrefix(origin) { + if candidate.type == CandidateType.py || !candidate.code.hasPrefix(origin) { return "(\(candidate.code))" } if candidate.code.hasPrefix(origin) { @@ -188,11 +188,11 @@ struct CandidatesView: View { struct ContentView_Previews: PreviewProvider { static var previews: some View { CandidatesView(candidates: [ - Candidate(code: "a", text: "工", type: "wb"), - Candidate(code: "ab", text: "戈", type: "wb"), - Candidate(code: "abc", text: "啊", type: "wb"), - Candidate(code: "abcg", text: "阿", type: "wb"), - Candidate(code: "addd", text: "吖", type: "wb") + Candidate(code: "a", text: "工", type: CandidateType.wb), + Candidate(code: "ab", text: "戈", type: CandidateType.wb), + Candidate(code: "abc", text: "啊", type: CandidateType.wb), + Candidate(code: "abcg", text: "阿", type: CandidateType.wb), + Candidate(code: "addd", text: "吖", type: CandidateType.wb) ], origin: "a") } } diff --git a/Fire/CandidatesWindow.swift b/Fire/CandidatesWindow.swift index 98dfb6f..012faab 100644 --- a/Fire/CandidatesWindow.swift +++ b/Fire/CandidatesWindow.swift @@ -43,7 +43,7 @@ class CandidatesWindow: NSWindow, NSWindowDelegate { print("candidates: \(candidatesData)") self.setFrameTopLeftPoint(topLeft) self.orderFront(nil) - NSApp.setActivationPolicy(.prohibited) +// NSApp.setActivationPolicy(.prohibited) } override init( diff --git a/Fire/DictManager.swift b/Fire/DictManager.swift new file mode 100644 index 0000000..be3c65e --- /dev/null +++ b/Fire/DictManager.swift @@ -0,0 +1,277 @@ +// +// DictManager.swift +// Fire +// +// Created by 虚幻 on 2022/7/2. +// Copyright © 2022 qwertyyb. All rights reserved. +// + +import Foundation +import Defaults + +class DictManager { + static let shared = DictManager() + static let userDictUpdated = Notification.Name("DictManager.userDictUpdated") + + let userDictFilePath = NSSearchPathForDirectoriesInDomains( + .applicationSupportDirectory, + .userDomainMask, true).first! + "/" + Bundle.main.bundleIdentifier! + "/user-dict.txt" + + private var database: OpaquePointer? + private var queryStatement: OpaquePointer? + + private init() { + Defaults.observe(keys: .codeMode, .candidateCount) { () in + self.prepareStatement() + } + .tieToLifetime(of: self) + } + deinit { + close() + } + func reinit() { + close() + prepareStatement() + } + func close() { + queryStatement = nil + sqlite3_close_v2(database) + sqlite3_shutdown() + database = nil + } + + private func getStatementSql() -> String { + let candidateCount = Defaults[.candidateCount] + let codeMode = Defaults[.codeMode] + // 比显示的候选词数量多查一个,以此判断有没有下一页 + let sql = """ + select + \(codeMode == .wubiPinyin ? "max(wbcode)" : "min(wbcode)"), + text, + type, min(query) as query + from wb_py_dict + where query like :query \( + codeMode == .wubi ? "and type = 'wb'" + : codeMode == .pinyin ? "and type = 'py'" : "") + group by text + order by query, id + limit :offset, \(candidateCount + 1) + """ + return sql + } + + private func prepareStatement() { + if database == nil { + sqlite3_open_v2(getDatabaseURL().path, &database, SQLITE_OPEN_READWRITE, nil) + } + if queryStatement != nil { + sqlite3_finalize(queryStatement) + queryStatement = nil + } + if sqlite3_prepare_v2(database, getStatementSql(), -1, &queryStatement, nil) == SQLITE_OK { + print("prepare ok") + } else if let err = sqlite3_errmsg(database) { + print("prepare fail: \(err)") + } + } + + private func getMinIdFromDictTable() -> Int { + let sql = "select min(id) from wb_py_dict" + var queryStmt: OpaquePointer? + if sqlite3_prepare_v2(database, sql, -1, &queryStmt, nil) == SQLITE_OK { + if sqlite3_step(queryStmt) == SQLITE_ROW { + let minId = sqlite3_column_int(queryStmt, 0) + sqlite3_finalize(queryStmt) + queryStmt = nil + return Int(minId) + } + } + NSLog("[Fire.getMinIdFromDictTable] errmsg: \(String(cString: sqlite3_errmsg(queryStmt)))") + sqlite3_finalize(queryStmt) + queryStmt = nil + return 0 + } + + private func replaceTextWithVars(_ text: String) -> String { + let date = Date() + let formatter = DateFormatter() + formatter.dateFormat = "yyyy MM dd HH mm ss" + let arr = formatter.string(from: date).split(separator: " ") + let vars: [String: String] = [ + "{yyyy}": String(arr[0]), + "{MM}": String(arr[1]), + "{dd}": String(arr[2]), + "{HH}": String(arr[3]), + "{mm}": String(arr[4]), + "{ss}": String(arr[5]) + ] + var newText = text + vars.forEach { (key, val) in + newText = newText.replacingOccurrences(of: key, with: val) + } + print("[replaceTextWithVars] \(text), \(newText)") + return newText + } + + func getCandidates(query: String = String(), page: Int = 1) -> (candidates: [Candidate], hasNext: Bool) { + if query.count <= 0 { + return ([], false) + } + NSLog("get local candidate, origin: \(query), query: ", query) + var candidates: [Candidate] = [] + sqlite3_reset(queryStatement) + sqlite3_clear_bindings(queryStatement) + sqlite3_bind_text(queryStatement, + sqlite3_bind_parameter_index(queryStatement, ":code"), + query, -1, + SQLITE_TRANSIENT + ) + sqlite3_bind_text(queryStatement, + sqlite3_bind_parameter_index(queryStatement, ":query"), + "\(query)%", -1, + SQLITE_TRANSIENT + ) + sqlite3_bind_int(queryStatement, + sqlite3_bind_parameter_index(queryStatement, ":offset"), + Int32((page - 1) * Defaults[.candidateCount]) + ) + while sqlite3_step(queryStatement) == SQLITE_ROW { + let code = String.init(cString: sqlite3_column_text(queryStatement, 0)) + var text = String.init(cString: sqlite3_column_text(queryStatement, 1)) + let type = CandidateType(rawValue: String.init(cString: sqlite3_column_text(queryStatement, 2)))! + if type == .user { + text = replaceTextWithVars(text) + } + let candidate = Candidate(code: code, text: text, type: type) + candidates.append(candidate) + } + let count = Defaults[.candidateCount] + let allCount = candidates.count + candidates = Array(candidates.prefix(count)) + + if candidates.isEmpty { + candidates.append(Candidate(code: query, text: query, type: CandidateType.placeholder)) + } + return (candidates, hasNext: allCount > count) + } + + func setCandidateToFirst(query: String, candidate: Candidate) { + let newCandidate = Candidate(code: query, text: candidate.text, type: CandidateType.user) + _ = prependCandidate(candidate: newCandidate) + NotificationQueue.default.enqueue(Notification(name: DictManager.userDictUpdated), postingStyle: .whenIdle) + } + + func prependCandidate(candidate: Candidate) -> Bool { + let sql = """ + insert into wb_py_dict(id, wbcode, text, type, query) + values ( + (select MIN(id) - 1 from wb_py_dict), :code, :text, :type, :code + ); + """ + var insertStatement: OpaquePointer? + if sqlite3_prepare_v2(database, sql, -1, &insertStatement, nil) == SQLITE_OK { + sqlite3_bind_text(insertStatement, + sqlite3_bind_parameter_index(insertStatement, ":code"), + candidate.code, -1, SQLITE_TRANSIENT) + sqlite3_bind_text(insertStatement, + sqlite3_bind_parameter_index(insertStatement, ":text"), + candidate.text, -1, SQLITE_TRANSIENT) + sqlite3_bind_text(insertStatement, + sqlite3_bind_parameter_index(insertStatement, ":type"), + CandidateType.user.rawValue, -1, SQLITE_TRANSIENT) + if sqlite3_step(insertStatement) == SQLITE_DONE { + sqlite3_finalize(insertStatement) + insertStatement = nil + return true + } + } + sqlite3_finalize(insertStatement) + insertStatement = nil + print("errmsg: \(String(cString: sqlite3_errmsg(database)!))") + return false + } + + func prependCandidates(candidates: [Candidate]) { + if candidates.count <= 0 { + return + } + // 2.1 先获取最小id + let minId = getMinIdFromDictTable() + // 2.2 添加对应id + let values = candidates.enumerated().map { (n, candidate) in + "(\(minId - candidates.count + n), '\(candidate.code)', '\(candidate.text)', '\(candidate.type)', '\(candidate.code)')" + }.joined(separator: ",") + let sql = """ + insert into wb_py_dict(id, wbcode, text, type, query) + values \(values) + """ + sqlite3_exec(database, sql, nil, nil, nil) + } + + func updateUserDict(_ dictContent: String) { + // 1. 先删除之前的用户词库 + sqlite3_exec(database, "delete from wb_py_dict where type = '\(CandidateType.user.rawValue)'", nil, nil, nil) + // 2. 添加用户词库 + let lines = dictContent.split(whereSeparator: \.isNewline) + let candidates = lines.map { (line) -> [Candidate] in + let strs = line.split(whereSeparator: \.isWhitespace) + if strs.count <= 1 { + return [] + } + let code = String(strs.first!) + let candidateTexts = strs[1...] + return candidateTexts.map { text in + Candidate(code: code, text: String(text), type: CandidateType.user) + } + }.reduce([] as [Candidate]) { partialResult, cur in + partialResult + cur + } + prependCandidates(candidates: candidates) + NotificationQueue.default.enqueue(Notification(name: DictManager.userDictUpdated), postingStyle: .whenIdle) + } + + func getUserCandidates() -> [Candidate] { + var stmt: OpaquePointer? + let sql = "select query, text from wb_py_dict where type = '\(CandidateType.user.rawValue)'" + if sqlite3_prepare_v2(database, sql, -1, &stmt, nil) == SQLITE_OK { + var candidates: [Candidate] = [] + while sqlite3_step(stmt) == SQLITE_ROW { + let code = String(cString: sqlite3_column_text(stmt, 0)) + let text = String(cString: sqlite3_column_text(stmt, 1)) + candidates.append(Candidate(code: code, text: text, type: .user)) + } + sqlite3_finalize(stmt) + stmt = nil + return candidates + } + sqlite3_finalize(stmt) + stmt = nil + return [] + } + + func getUserDictContent() -> String { + // 获取用户候选词(包括调整顺序的词) + struct UserDictLine { + let code: String + var texts: [String] + } + let candidates = getUserCandidates() + NSLog("[DictManager.exportUserDictToFile] candidates: \(candidates)") + var list: [UserDictLine] = [] + candidates.forEach { candidate in + let index = list.firstIndex { dictItem in + dictItem.code == candidate.code + } + if index == nil { + list.append(UserDictLine(code: candidate.code, texts: [candidate.text])) + } else if !list[index!].texts.contains(candidate.text) { + list[index!].texts.append(candidate.text) + } + } + let content = list.map { dictItem in + ([dictItem.code] + dictItem.texts).joined(separator: "\t") + } + .joined(separator: "\n") + return content + } +} diff --git a/Fire/Fire.swift b/Fire/Fire.swift index 2b5b998..1ce4045 100644 --- a/Fire/Fire.swift +++ b/Fire/Fire.swift @@ -37,27 +37,10 @@ class Fire: NSObject { static let candidateInserted = Notification.Name("Fire.candidateInserted") static let inputModeChanged = Notification.Name("Fire.inputModeChanged") - private var database: OpaquePointer? - private var queryStatement: OpaquePointer? - private var preferencesObserver: Defaults.Observation! - let appSettingCache = ApplicationSettingCache() var inputMode: InputMode = .zhhans - override init() { - super.init() - - preferencesObserver = Defaults.observe(keys: .codeMode, .candidateCount) { () in - self.prepareStatement() - } - } - - deinit { - preferencesObserver.invalidate() - close() - } - func transformPunctution(_ origin: String)-> String? { let isPunctution = punctution.keys.contains(origin) if !isPunctution { @@ -93,59 +76,6 @@ class Fire: NSObject { ]) } - private func getStatementSql() -> String { - let candidateCount = Defaults[.candidateCount] - // 比显示的候选词数量多查一个,以此判断有没有下一页 - var sql = """ - select - max(wbcode), text, type, min(query) as query - from wb_py_dict - where query like :query - group by text - order by query, id - limit :offset, \(candidateCount + 1) - """ - let codeMode = Defaults[.codeMode] - if codeMode != .wubiPinyin { - sql = """ - select - min(code) as wbcode, - text, - '\(codeMode == .wubi ? "wb" : "py")' as type, - min(code) as query - from \(codeMode == .wubi ? "wb_dict" : "py_dict") - where code like :query - group by text - order by query, id - limit :offset, \(candidateCount + 1) - """ - } - print(sql) - return sql - } - - func close() { - queryStatement = nil - sqlite3_close_v2(database) - sqlite3_shutdown() - database = nil - } - - func prepareStatement() { - if database == nil { - sqlite3_open_v2(getDatabaseURL().path, &database, SQLITE_OPEN_READWRITE, nil) - } - if queryStatement != nil { - sqlite3_finalize(queryStatement) - queryStatement = nil - } - if sqlite3_prepare_v2(database, getStatementSql(), -1, &queryStatement, nil) == SQLITE_OK { - print("prepare ok") - } else if let err = sqlite3_errmsg(database) { - print("prepare fail: \(err)") - } - } - private func getQueryFromOrigin(_ origin: String) -> String { if origin.isEmpty { return origin @@ -167,84 +97,14 @@ class Fire: NSObject { return ([], false) } let query = getQueryFromOrigin(origin) - NSLog("get local candidate, origin: \(origin), query: ", query) - var candidates: [Candidate] = [] - sqlite3_reset(queryStatement) - sqlite3_clear_bindings(queryStatement) - sqlite3_bind_text(queryStatement, - sqlite3_bind_parameter_index(queryStatement, ":code"), - origin, -1, - SQLITE_TRANSIENT - ) - sqlite3_bind_text(queryStatement, - sqlite3_bind_parameter_index(queryStatement, ":query"), - "\(query)%", -1, - SQLITE_TRANSIENT - ) - sqlite3_bind_int(queryStatement, - sqlite3_bind_parameter_index(queryStatement, ":offset"), - Int32((page - 1) * Defaults[.candidateCount]) - ) - while sqlite3_step(queryStatement) == SQLITE_ROW { - let code = String.init(cString: sqlite3_column_text(queryStatement, 0)) - let text = String.init(cString: sqlite3_column_text(queryStatement, 1)) - let type = String.init(cString: sqlite3_column_text(queryStatement, 2)) - let candidate = Candidate(code: code, text: text, type: type) - candidates.append(candidate) - } - let count = Defaults[.candidateCount] - let allCount = candidates.count - candidates = Array(candidates.prefix(count)) - - if candidates.isEmpty { - candidates.append(Candidate(code: origin, text: origin, type: "wb", isPlaceholder: true)) - } - return (candidates, hasNext: allCount > count) - } - - func setFirstCandidate(wbcode: String, candidate: Candidate) -> Bool { - var sql = """ - insert into wb_py_dict(id, wbcode, text, type, query) - values ( - (select MIN(id) - 1 from wb_py_dict), :code, :text, :type, :code - ); - """ - let codeMode = Defaults[.codeMode] - if codeMode != .wubiPinyin { - sql = """ - insert into \(codeMode == .wubi ? "wb_dict" : "py_dict")(id, code, text) - values( - (select MIN(id) - 1 from \(codeMode == .wubi ? "wb_dict" : "py_dict")), - :code, - :text - ); - """ - } - var insertStatement: OpaquePointer? - if sqlite3_prepare_v2(database, sql, -1, &insertStatement, nil) == SQLITE_OK { - sqlite3_bind_text(insertStatement, - sqlite3_bind_parameter_index(insertStatement, ":code"), - wbcode, -1, SQLITE_TRANSIENT) - sqlite3_bind_text(insertStatement, - sqlite3_bind_parameter_index(insertStatement, ":text"), - candidate.text, -1, SQLITE_TRANSIENT) - sqlite3_bind_text(insertStatement, - sqlite3_bind_parameter_index(insertStatement, ":type"), - "wb", -1, SQLITE_TRANSIENT) - let strp = sqlite3_expanded_sql(queryStatement)! - print(String(cString: strp)) - if sqlite3_step(insertStatement) == SQLITE_DONE { - sqlite3_finalize(insertStatement) - insertStatement = nil - return true - } else { - print("errmsg: \(String(cString: sqlite3_errmsg(database)!))") - return false + let (candidates, hasNext) = DictManager.shared.getCandidates(query: query, page: page) + let transformed = candidates.map { (candidate) -> Candidate in + if candidate.type == .user { + return Candidate(code: candidate.code, text: candidate.text, type: .user) } - } else { - print("prepare_errmsg: \(String(cString: sqlite3_errmsg(database)!))") + return candidate } - return false + return (transformed, hasNext) } static let shared = Fire() diff --git a/Fire/FireInputController.swift b/Fire/FireInputController.swift index b79dc92..bb2d045 100644 --- a/Fire/FireInputController.swift +++ b/Fire/FireInputController.swift @@ -82,12 +82,10 @@ class FireInputController: IMKInputController { if event.modifierFlags == .control && num > 0 && num <= _candidates.count { NSLog("hotkey: control + \(num)") - if Fire.shared.setFirstCandidate(wbcode: _originalString, candidate: _candidates[num - 1]) { - self.curPage = 1 - self.refreshCandidatesWindow() - return true - } - return nil + DictManager.shared.setCandidateToFirst(query: _originalString, candidate: _candidates[num-1]) + self.curPage = 1 + self.refreshCandidatesWindow() + return true } return nil } @@ -248,6 +246,16 @@ class FireInputController: IMKInputController { // ---- handlers end ------- + override func recognizedEvents(_ sender: Any!) -> Int { + // 当在当前应用下输入时 NSEvent.addGlobalMonitorForEvents 回调不会被调用,需要针对当前app, 使用原始的方式处理flagsChanged事件 + let isCurrentApp = client().bundleIdentifier() == Bundle.main.bundleIdentifier + var events = NSEvent.EventTypeMask(arrayLiteral: .keyDown) + if isCurrentApp { + events = NSEvent.EventTypeMask(arrayLiteral: .keyDown, .flagsChanged) + } + return Int(events.rawValue) + } + override func handle(_ event: NSEvent!, client sender: Any!) -> Bool { NSLog("[FireInputController] handle: \(event.debugDescription)") diff --git a/Fire/FireMenu.swift b/Fire/FireMenu.swift index bd90cb8..574b192 100644 --- a/Fire/FireMenu.swift +++ b/Fire/FireMenu.swift @@ -25,12 +25,17 @@ extension FireInputController { NSApp.setActivationPolicy(.accessory) FirePreferencesController.shared.show() } + @objc func showUserDictPrefs(_ sender: Any!) { + NSApp.setActivationPolicy(.accessory) + FirePreferencesController.shared.showPane("用户词库") + } override func menu() -> NSMenu! { let menu = NSMenu() menu.items = [ - NSMenuItem(title: "关于业火输入法", action: #selector(openAbout(_:)), keyEquivalent: ""), + NSMenuItem(title: "首选项", action: #selector(showPreferences(_:)), keyEquivalent: ""), + NSMenuItem(title: "用户词库", action: #selector(showUserDictPrefs(_:)), keyEquivalent: ""), NSMenuItem(title: "检查更新", action: #selector(checkForUpdates(_:)), keyEquivalent: ""), - NSMenuItem(title: "首选项", action: #selector(showPreferences(_:)), keyEquivalent: "") + NSMenuItem(title: "关于业火输入法", action: #selector(openAbout(_:)), keyEquivalent: "") ] return menu } diff --git a/Fire/Preferences/FirePreferencesController.swift b/Fire/Preferences/FirePreferencesController.swift index 9cd43e6..f2ef977 100644 --- a/Fire/Preferences/FirePreferencesController.swift +++ b/Fire/Preferences/FirePreferencesController.swift @@ -16,8 +16,8 @@ class FirePreferencesController: NSObject, NSWindowDelegate { var isVisible: Bool { controller?.window?.isVisible ?? false } - - func show() { + + private func initController() { if let controller = controller { controller.show() return @@ -38,6 +38,13 @@ class FirePreferencesController: NSObject, NSWindowDelegate { ) { PunctutionPane() }, + Preferences.Pane( + identifier: Preferences.PaneIdentifier(rawValue: "用户词库"), + title: "用户词库", + toolbarIcon: NSImage(named: NSImage.multipleDocumentsName) ?? NSImage(named: "general")! + ) { + UserDictPane() + }, Preferences.Pane( identifier: Preferences.PaneIdentifier(rawValue: "应用"), title: "应用", @@ -70,6 +77,15 @@ class FirePreferencesController: NSObject, NSWindowDelegate { style: .toolbarItems ) self.controller?.window?.delegate = self - self.controller?.show() + } + + func showPane(_ name: String) { + initController() + controller?.show(preferencePane: Preferences.PaneIdentifier(rawValue: name)) + } + + func show() { + initController() + controller?.show() } } diff --git a/Fire/Preferences/ThesaurusPane.swift b/Fire/Preferences/ThesaurusPane.swift index c844008..e689d56 100644 --- a/Fire/Preferences/ThesaurusPane.swift +++ b/Fire/Preferences/ThesaurusPane.swift @@ -80,9 +80,9 @@ struct ThesaurusPane: View { } } Button(action: { - Fire.shared.close() + DictManager.shared.close() buildDict() - Fire.shared.prepareStatement() + DictManager.shared.reinit() }, label: { Text("建立索引") }) diff --git a/Fire/Preferences/UserDictPane.swift b/Fire/Preferences/UserDictPane.swift new file mode 100644 index 0000000..ada107b --- /dev/null +++ b/Fire/Preferences/UserDictPane.swift @@ -0,0 +1,82 @@ +// +// UserDictPane.swift +// Fire +// +// Created by 虚幻 on 2022/7/1. +// Copyright © 2022 qwertyyb. All rights reserved. +// + +import SwiftUI +import Preferences +import Combine + +class UserDictTextModel: ObservableObject { + @Published var text = "" + private var cancellable = Set() + + init() { + refresh() + NotificationCenter.default.publisher(for: DictManager.userDictUpdated).sink { notification in + self.refresh() + } + .store(in: &cancellable) + } + + func refresh() { + NSLog("[UserDictTextModel.refresh]") + self.text = DictManager.shared.getUserDictContent() + } +} + +struct UserDictPane: View { + @StateObject private var userDictTextModel = UserDictTextModel() + @State private var saved = false + var body: some View { + Preferences.Container(contentWidth: 450) { + Preferences.Section(title: "") { + Text("用户词库") + if #available(macOS 11.0, *) { + TextEditor(text: $userDictTextModel.text) + .font(Font.system(size: 14)) + .frame(height: 400) + .lineSpacing(6) + Text("1. 编码需在行首") + .font(Font.system(size: 11)) + Text("2. 编码和候选项之间需用空格分隔") + .font(Font.system(size: 11)) + Text("3. 可以有多个候选项,每个候选项使用空格分隔") + .font(Font.system(size: 11)) + Text("4. 候选项可使用{yyyy}/{MM}/{dd}/{HH}/{mm}/{ss}代替当前年/月/日/时/分/秒") + .font(Font.system(size: 11)) + HStack { + Spacer() + if #available(macOS 12.0, *) { + Button("保存") { + DictManager.shared.updateUserDict(userDictTextModel.text) + saved = true + } + .alert("保存成功", isPresented: $saved) { + } + } else { + // Fallback on earlier versions + Button("保存") { + DictManager.shared.updateUserDict(userDictTextModel.text) + print("saved") + } + } + Spacer() + } + } else { + // Fallback on earlier versions + Text("暂不支持,请升级系统至11.0及以上") + } + } + } + } +} + +struct UserDictPane_Previews: PreviewProvider { + static var previews: some View { + UserDictPane() + } +} diff --git a/Fire/Utils/Statistics.swift b/Fire/Utils/Statistics.swift index 159c4b7..9a8c621 100644 --- a/Fire/Utils/Statistics.swift +++ b/Fire/Utils/Statistics.swift @@ -36,7 +36,7 @@ class Statistics { if !Defaults[.enableStatistics] { return } - if candidate.isPlaceholder { return } + if candidate.type == CandidateType.placeholder { return } let sql = "insert into data(text, type, code, createdAt) values (:text, :type, :code, :createdAt)" var insertStatement: OpaquePointer? if sqlite3_prepare_v2(database, sql, -1, &insertStatement, nil) == SQLITE_OK { @@ -47,7 +47,7 @@ class Statistics { candidate.text, -1, SQLITE_TRANSIENT) sqlite3_bind_text(insertStatement, sqlite3_bind_parameter_index(insertStatement, ":type"), - candidate.type, -1, SQLITE_TRANSIENT) + candidate.type.rawValue, -1, SQLITE_TRANSIENT) sqlite3_bind_text(insertStatement, sqlite3_bind_parameter_index(insertStatement, ":code"), candidate.code, -1, SQLITE_TRANSIENT) diff --git a/Fire/types.swift b/Fire/types.swift index 86fbc76..cfa9037 100644 --- a/Fire/types.swift +++ b/Fire/types.swift @@ -22,7 +22,7 @@ enum InputModeTipWindowType: Int, Decodable, Encodable { case none } -enum ModifierKey: Codable { +enum ModifierKey: String, Codable { case shift case leftShift case rightShift @@ -142,11 +142,17 @@ enum InputModeSetting: String, Codable { case recentUsed } +enum CandidateType: String { + case wb = "wb" // 五笔 + case py = "py" // 拼音 + case user = "user" // 用户词库 + case placeholder = "placeholder" // 运行时类型,无匹配时表示占位 +} + struct Candidate: Hashable { let code: String let text: String - let type: String // wb | py - var isPlaceholder: Bool = false // 是否是占位符,当没有其他候选词时,会显示占位 + let type: CandidateType } enum CodeMode: Int, CaseIterable, Decodable, Encodable {