Repo // Introducing reverse-lookup window.
This commit is contained in:
parent
9ec8428af6
commit
892a574378
|
@ -0,0 +1,193 @@
|
|||
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
|
||||
// ====================
|
||||
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
|
||||
// ... with NTL restriction stating that:
|
||||
// No trademark license is granted to use the trade names, trademarks, service
|
||||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import Cocoa
|
||||
import LangModelAssembly
|
||||
|
||||
class CtlRevLookupWindow: NSWindowController, NSWindowDelegate {
|
||||
static var shared: CtlRevLookupWindow?
|
||||
|
||||
static func show() {
|
||||
if shared == nil { Self.shared = .init(window: FrmRevLookupWindow()) }
|
||||
guard let shared = Self.shared, let window = shared.window as? FrmRevLookupWindow else { return }
|
||||
shared.window = window
|
||||
window.delegate = shared
|
||||
window.setPosition(vertical: .bottom, horizontal: .right, padding: 20)
|
||||
window.orderFrontRegardless() // 逼著視窗往最前方顯示
|
||||
window.level = .statusBar
|
||||
window.titlebarAppearsTransparent = true
|
||||
shared.showWindow(shared)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
}
|
||||
|
||||
class FrmRevLookupWindow: NSWindow {
|
||||
typealias LMRevLookup = vChewingLM.LMRevLookup
|
||||
|
||||
static let lmRevLookupCore = LMRevLookup(path: LMMgr.getBundleDataPath("data-bpmf-reverse-lookup"))
|
||||
|
||||
// 全字庫資料接近十萬筆索引,只放到單個 Dictionary 內的話、每次查詢時都會把輸入法搞崩潰。只能分卷處理。
|
||||
static let lmRevLookupCNS1 = LMRevLookup(path: LMMgr.getBundleDataPath("data-bpmf-reverse-lookup-CNS1"))
|
||||
static let lmRevLookupCNS2 = LMRevLookup(path: LMMgr.getBundleDataPath("data-bpmf-reverse-lookup-CNS2"))
|
||||
static let lmRevLookupCNS3 = LMRevLookup(path: LMMgr.getBundleDataPath("data-bpmf-reverse-lookup-CNS3"))
|
||||
static let lmRevLookupCNS4 = LMRevLookup(path: LMMgr.getBundleDataPath("data-bpmf-reverse-lookup-CNS4"))
|
||||
static let lmRevLookupCNS5 = LMRevLookup(path: LMMgr.getBundleDataPath("data-bpmf-reverse-lookup-CNS5"))
|
||||
static let lmRevLookupCNS6 = LMRevLookup(path: LMMgr.getBundleDataPath("data-bpmf-reverse-lookup-CNS6"))
|
||||
|
||||
public lazy var inputField = NSTextField()
|
||||
public lazy var resultView = NSTextView()
|
||||
private lazy var clipView = NSClipView()
|
||||
private lazy var scrollView = NSScrollView()
|
||||
private lazy var button = NSButton()
|
||||
private lazy var view = NSView()
|
||||
|
||||
init() {
|
||||
super.init(
|
||||
contentRect: CGRect(x: 196, y: 240, width: 480, height: 340),
|
||||
styleMask: [.titled, .closable],
|
||||
backing: .buffered, defer: true
|
||||
)
|
||||
setupUI()
|
||||
setupLayout()
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
contentView = view
|
||||
|
||||
allowsToolTipsWhenApplicationIsInactive = false
|
||||
autorecalculatesKeyViewLoop = false
|
||||
isReleasedWhenClosed = false
|
||||
title = "Reverse Lookup (Phonabets)".localized
|
||||
|
||||
view.addSubview(inputField)
|
||||
view.addSubview(scrollView)
|
||||
view.addSubview(button)
|
||||
|
||||
view.wantsLayer = true
|
||||
|
||||
button.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.alignment = .center
|
||||
button.bezelStyle = .recessed
|
||||
button.font = NSFont.systemFont(ofSize: 12, weight: .bold)
|
||||
button.imageScaling = .scaleProportionallyDown
|
||||
button.title = "👓"
|
||||
button.cell.map { $0 as? NSButtonCell }??.isBordered = true
|
||||
button.target = self
|
||||
button.action = #selector(keyboardConfirmed(_:))
|
||||
button.keyEquivalent = String(utf16CodeUnits: [unichar(NSEvent.SpecialKey.enter.rawValue)], count: 1) as String
|
||||
|
||||
scrollView.borderType = .noBorder
|
||||
scrollView.hasHorizontalScroller = false
|
||||
scrollView.horizontalLineScroll = 10
|
||||
scrollView.horizontalPageScroll = 10
|
||||
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||
scrollView.verticalLineScroll = 10
|
||||
scrollView.verticalPageScroll = 10
|
||||
|
||||
clipView.documentView = resultView
|
||||
|
||||
clipView.autoresizingMask = [.width, .height]
|
||||
clipView.drawsBackground = false
|
||||
clipView.frame = CGRect(x: 0, y: 0, width: 480, height: 320)
|
||||
|
||||
resultView.autoresizingMask = [.width, .height]
|
||||
resultView.backgroundColor = NSColor.textBackgroundColor
|
||||
resultView.frame = CGRect(x: 0, y: 0, width: 480, height: 320)
|
||||
resultView.importsGraphics = false
|
||||
resultView.insertionPointColor = NSColor.textColor
|
||||
resultView.isEditable = false
|
||||
resultView.isRichText = false
|
||||
resultView.isVerticallyResizable = true
|
||||
resultView.maxSize = CGSize(width: 774, height: 10_000_000)
|
||||
resultView.minSize = CGSize(width: 480, height: 320)
|
||||
resultView.smartInsertDeleteEnabled = true
|
||||
resultView.textColor = NSColor.textColor
|
||||
resultView.wantsLayer = true
|
||||
resultView.font = NSFont.systemFont(ofSize: 13)
|
||||
resultView.string = "Maximum 15 results returnable.".localized
|
||||
|
||||
scrollView.contentView = clipView
|
||||
|
||||
inputField.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
||||
inputField.translatesAutoresizingMaskIntoConstraints = false
|
||||
inputField.backgroundColor = NSColor.textBackgroundColor
|
||||
inputField.drawsBackground = true
|
||||
inputField.font = NSFont.systemFont(ofSize: 13)
|
||||
inputField.isBezeled = true
|
||||
inputField.isEditable = true
|
||||
inputField.isSelectable = true
|
||||
inputField.lineBreakMode = .byClipping
|
||||
inputField.textColor = NSColor.controlTextColor
|
||||
inputField.cell.map { $0 as? NSTextFieldCell }??.isScrollable = true
|
||||
inputField.cell.map { $0 as? NSTextFieldCell }??.sendsActionOnEndEditing = true
|
||||
inputField.cell.map { $0 as? NSTextFieldCell }??.usesSingleLineMode = true
|
||||
inputField.action = #selector(keyboardConfirmed(_:))
|
||||
inputField.toolTip =
|
||||
"Maximum 15 results returnable.".localized
|
||||
}
|
||||
|
||||
@objc func keyboardConfirmed(_: Any?) {
|
||||
if inputField.stringValue.isEmpty { return }
|
||||
resultView.string = "\n" + "Loading…".localized
|
||||
DispatchQueue.main.async { [self] in
|
||||
self.updateResult(with: self.inputField.stringValue)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateResult(with input: String) {
|
||||
guard !input.isEmpty else { return }
|
||||
button.isEnabled = false
|
||||
inputField.isEnabled = false
|
||||
let strBuilder = NSMutableString()
|
||||
strBuilder.append("\n")
|
||||
strBuilder.append("Char\tReading(s)\n".localized)
|
||||
strBuilder.append("==\t====\n")
|
||||
var i = 0
|
||||
theLoop: for char in input.charComponents {
|
||||
if i == 15 {
|
||||
strBuilder.append("Maximum 15 results returnable.".localized + "\n")
|
||||
break theLoop
|
||||
}
|
||||
var arrResult = Self.lmRevLookupCore.query(with: char) ?? []
|
||||
// 一般情況下,威注音語彙庫的倉庫內的全字庫資料檔案有做過排序,所以每個分卷的索引都是不重複的。
|
||||
arrResult +=
|
||||
Self.lmRevLookupCNS1.query(with: char)
|
||||
?? Self.lmRevLookupCNS2.query(with: char)
|
||||
?? Self.lmRevLookupCNS3.query(with: char)
|
||||
?? Self.lmRevLookupCNS4.query(with: char)
|
||||
?? Self.lmRevLookupCNS5.query(with: char)
|
||||
?? Self.lmRevLookupCNS6.query(with: char)
|
||||
?? []
|
||||
arrResult = arrResult.deduplicated
|
||||
if !arrResult.isEmpty {
|
||||
strBuilder.append(char + "\t")
|
||||
strBuilder.append(arrResult.joined(separator: ", "))
|
||||
strBuilder.append("\n")
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
resultView.string = strBuilder.description
|
||||
button.isEnabled = true
|
||||
inputField.isEnabled = true
|
||||
}
|
||||
|
||||
private func setupLayout() {
|
||||
inputField.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
|
||||
button.leadingAnchor.constraint(equalTo: inputField.trailingAnchor, constant: 5).isActive = true
|
||||
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20).isActive = true
|
||||
inputField.lastBaselineAnchor.constraint(equalTo: button.lastBaselineAnchor).isActive = true
|
||||
scrollView.topAnchor.constraint(equalTo: button.bottomAnchor).isActive = true
|
||||
view.trailingAnchor.constraint(equalTo: button.trailingAnchor, constant: 3).isActive = true
|
||||
inputField.topAnchor.constraint(equalTo: button.topAnchor).isActive = true
|
||||
view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
|
||||
view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
|
||||
inputField.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
|
||||
inputField.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
|
||||
}
|
||||
}
|
|
@ -16,6 +16,10 @@
|
|||
"Loading…" = "Loading…";
|
||||
"Consolidate" = "Consolidate";
|
||||
"Reload" = "Reload";
|
||||
"Loading complete." = "Loading complete.";
|
||||
"Char\tReading(s)\n" = "Char\tReading(s)\n";
|
||||
"Reverse Lookup (Phonabets)" = "Reverse Lookup (Phonabets)";
|
||||
"Maximum 15 results returnable." = "Maximum 15 results returnable.";
|
||||
"Example:\nCandidate Reading-Reading #Comment" = "Example:\nCandidate Reading-Reading #Comment";
|
||||
"Example:\nCandidate Reading-Reading Weight #Comment\nCandidate Reading-Reading #Comment" = "Example:\nCandidate Reading-Reading Weight #Comment\nCandidate Reading-Reading #Comment";
|
||||
"Example:\nInitial RestPhrase\nInitial RestPhrase1 RestPhrase2 RestPhrase3..." = "Example:\nInitial RestPhrase\nInitial RestPhrase1 RestPhrase2 RestPhrase3...";
|
||||
|
|
|
@ -16,6 +16,10 @@
|
|||
"Loading…" = "Loading…";
|
||||
"Consolidate" = "Consolidate";
|
||||
"Reload" = "Reload";
|
||||
"Loading complete." = "Loading complete.";
|
||||
"Char\tReading(s)\n" = "Char\tReading(s)\n";
|
||||
"Reverse Lookup (Phonabets)" = "Reverse Lookup (Phonabets)";
|
||||
"Maximum 15 results returnable." = "Maximum 15 results returnable.";
|
||||
"Example:\nCandidate Reading-Reading #Comment" = "Example:\nCandidate Reading-Reading #Comment";
|
||||
"Example:\nCandidate Reading-Reading Weight #Comment\nCandidate Reading-Reading #Comment" = "Example:\nCandidate Reading-Reading Weight #Comment\nCandidate Reading-Reading #Comment";
|
||||
"Example:\nInitial RestPhrase\nInitial RestPhrase1 RestPhrase2 RestPhrase3..." = "Example:\nInitial RestPhrase\nInitial RestPhrase1 RestPhrase2 RestPhrase3...";
|
||||
|
|
|
@ -16,6 +16,10 @@
|
|||
"Loading…" = "読み込む中…";
|
||||
"Consolidate" = "整理";
|
||||
"Reload" = "再読込";
|
||||
"Loading complete." = "読込完了。";
|
||||
"Char\tReading(s)\n" = "漢字\t音読\n";
|
||||
"Reverse Lookup (Phonabets)" = "注音音読逆引参照";
|
||||
"Maximum 15 results returnable." = "参照結果は最初の15件のみ表示可能。";
|
||||
"Example:\nCandidate Reading-Reading #Comment" = "【模範例】\n候補 音読-音読 #メモ";
|
||||
"Example:\nCandidate Reading-Reading Weight #Comment\nCandidate Reading-Reading #Comment" = "【模範例】\n候補 音読-音読 優先度 #メモ\n候補 音読-音読 #メモ";
|
||||
"Example:\nInitial RestPhrase\nInitial RestPhrase1 RestPhrase2 RestPhrase3..." = "【模範例】\n頭文字 残候補\n頭文字 残候補1 残候補2 残候補3...";
|
||||
|
|
|
@ -16,6 +16,10 @@
|
|||
"Loading…" = "正在载入…";
|
||||
"Consolidate" = "整理";
|
||||
"Reload" = "重新载入";
|
||||
"Loading complete." = "载入完毕。";
|
||||
"Char\tReading(s)\n" = "汉字\t读音\n";
|
||||
"Reverse Lookup (Phonabets)" = "注音反查";
|
||||
"Maximum 15 results returnable." = "仅能给出前 15 笔结果。";
|
||||
"Example:\nCandidate Reading-Reading #Comment" = "【范例】\n候选字词 读音-读音 #注解";
|
||||
"Example:\nCandidate Reading-Reading Weight #Comment\nCandidate Reading-Reading #Comment" = "【范例】\n候选字词 读音-读音 权重 #注解\n候选字词 读音-读音 #注解";
|
||||
"Example:\nInitial RestPhrase\nInitial RestPhrase1 RestPhrase2 RestPhrase3..." = "【范例】\n首字 候选\n首字 候选1 候选2 候选3...";
|
||||
|
|
|
@ -16,6 +16,10 @@
|
|||
"Loading…" = "正在載入…";
|
||||
"Consolidate" = "整理";
|
||||
"Reload" = "重新載入";
|
||||
"Loading complete." = "載入完畢。";
|
||||
"Char\tReading(s)\n" = "漢字\t讀音\n";
|
||||
"Reverse Lookup (Phonabets)" = "注音反查";
|
||||
"Maximum 15 results returnable." = "僅能給出前 15 筆結果。";
|
||||
"Example:\nCandidate Reading-Reading #Comment" = "【範例】\n候選字詞 讀音-讀音 #註解";
|
||||
"Example:\nCandidate Reading-Reading Weight #Comment\nCandidate Reading-Reading #Comment" = "【範例】\n候選字詞 讀音-讀音 權重 #註解\n候選字詞 讀音-讀音 #註解";
|
||||
"Example:\nInitial RestPhrase\nInitial RestPhrase1 RestPhrase2 RestPhrase3..." = "【範例】\n首字 候選\n首字 候選1 候選2 候選3...";
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
5B253E822945AF6700680C67 /* data-bpmf-reverse-lookup-CNS4.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5B253E7C2945AF6700680C67 /* data-bpmf-reverse-lookup-CNS4.plist */; };
|
||||
5B253E832945AF6700680C67 /* data-bpmf-reverse-lookup-CNS5.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5B253E7D2945AF6700680C67 /* data-bpmf-reverse-lookup-CNS5.plist */; };
|
||||
5B2E009428FD1E8100E78D6E /* VwrPrefPaneCassette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2E009328FD1E8100E78D6E /* VwrPrefPaneCassette.swift */; };
|
||||
5B30BF282944867800BD87A9 /* CtlRevLookupWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B30BF272944867800BD87A9 /* CtlRevLookupWindow.swift */; };
|
||||
5B3133BF280B229700A4A505 /* InputHandler_HandleStates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B3133BE280B229700A4A505 /* InputHandler_HandleStates.swift */; };
|
||||
5B40113928D7050D00A9D4CB /* Shared in Frameworks */ = {isa = PBXBuildFile; productRef = 5B40113828D7050D00A9D4CB /* Shared */; };
|
||||
5B40113C28D71C0100A9D4CB /* Uninstaller in Frameworks */ = {isa = PBXBuildFile; productRef = 5B40113B28D71C0100A9D4CB /* Uninstaller */; };
|
||||
|
@ -215,6 +216,7 @@
|
|||
5B2DB17127AF8771006D874E /* Makefile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.make; name = Makefile; path = Data/Makefile; sourceTree = "<group>"; };
|
||||
5B2E009328FD1E8100E78D6E /* VwrPrefPaneCassette.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VwrPrefPaneCassette.swift; sourceTree = "<group>"; };
|
||||
5B2F2BB3286216A500B8557B /* vChewingTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = vChewingTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
5B30BF272944867800BD87A9 /* CtlRevLookupWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CtlRevLookupWindow.swift; sourceTree = "<group>"; };
|
||||
5B30F11227BA568800484E24 /* vChewingKeyLayout.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = vChewingKeyLayout.bundle; sourceTree = "<group>"; };
|
||||
5B312684287800DE001AA720 /* FAQ.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = FAQ.md; sourceTree = "<group>"; };
|
||||
5B3133BE280B229700A4A505 /* InputHandler_HandleStates.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = InputHandler_HandleStates.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||
|
@ -470,6 +472,7 @@
|
|||
5B0EF55E28CDBF8E00F8F7CE /* CtlClientListMgr.swift */,
|
||||
D47F7DCD278BFB57002F9DD7 /* CtlPrefWindow.swift */,
|
||||
5BCC631529407BBB00A2D84F /* CtlPrefWindow_PhraseEditor.swift */,
|
||||
5B30BF272944867800BD87A9 /* CtlRevLookupWindow.swift */,
|
||||
);
|
||||
path = WindowControllers;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1090,6 +1093,7 @@
|
|||
5B78EE0D28A562B4009456C1 /* VwrPrefPaneDevZone.swift in Sources */,
|
||||
5B6C141228A9D4B30098ADF8 /* SessionCtl_HandleEvent.swift in Sources */,
|
||||
D47F7DCE278BFB57002F9DD7 /* CtlPrefWindow.swift in Sources */,
|
||||
5B30BF282944867800BD87A9 /* CtlRevLookupWindow.swift in Sources */,
|
||||
5BD0113D2818543900609769 /* InputHandler_Core.swift in Sources */,
|
||||
5BF56F9A28C39D1800DD6839 /* IMEStateData.swift in Sources */,
|
||||
5B21176C287539BB000443A9 /* SessionCtl_HandleStates.swift in Sources */,
|
||||
|
|
Loading…
Reference in New Issue