Repo // ctlCandidateIMK, continued.
This commit is contained in:
parent
15308a1148
commit
6fcf4356ea
|
@ -214,6 +214,11 @@ struct InputSignal: CustomStringConvertible {
|
|||
18: "1", 19: "2", 20: "3", 21: "4", 23: "5", 22: "6", 26: "7", 28: "8", 25: "9", 29: "0",
|
||||
]
|
||||
|
||||
var isCandidateKey: Bool {
|
||||
mgrPrefs.candidateKeys.contains(inputText)
|
||||
|| mgrPrefs.candidateKeys.contains(inputTextIgnoringModifiers ?? "114514")
|
||||
}
|
||||
|
||||
/// 單獨用 flags 來判定數字小鍵盤輸入的方法已經失效了,所以必須再增補用 KeyCode 判定的方法。
|
||||
var isNumericPadKey: Bool { arrNumpadKeyCodes.contains(keyCode) }
|
||||
var isMainAreaNumKey: Bool { arrMainAreaNumKey.contains(keyCode) }
|
||||
|
|
|
@ -25,7 +25,7 @@ class ctlInputMethod: IMKInputController {
|
|||
static var areWeNerfing = false
|
||||
|
||||
/// 目前在用的的選字窗副本。
|
||||
static var ctlCandidateCurrent: ctlCandidateProtocol = ctlCandidateUniversal.init(.horizontal)
|
||||
static var ctlCandidateCurrent: ctlCandidateProtocol = ctlCandidateIMK.init(.horizontal)
|
||||
|
||||
/// 工具提示視窗的副本。
|
||||
static let tooltipController = TooltipController()
|
||||
|
@ -47,7 +47,7 @@ class ctlInputMethod: IMKInputController {
|
|||
}
|
||||
|
||||
/// `handle(event:)` 會利用這個參數判定某次 Shift 按鍵是否用來切換中英文輸入。
|
||||
private var rencentKeyHandledByKeyHandler = false
|
||||
var rencentKeyHandledByKeyHandler = false
|
||||
|
||||
// MARK: - 工具函式
|
||||
|
||||
|
@ -205,6 +205,12 @@ class ctlInputMethod: IMKInputController {
|
|||
}
|
||||
}
|
||||
|
||||
/// IMK 選字窗處理,當且僅當啟用了 IMK 選字窗的時候才會生效。
|
||||
if let ctlCandidateCurrent = ctlInputMethod.ctlCandidateCurrent as? ctlCandidateIMK, ctlCandidateCurrent.visible {
|
||||
ctlCandidateCurrent.interpretKeyEvents([event])
|
||||
return true
|
||||
}
|
||||
|
||||
/// 這裡仍舊需要判斷 flags。之前使輸入法狀態卡住無法敲漢字的問題已在 KeyHandler 內修復。
|
||||
/// 這裡不判斷 flags 的話,用方向鍵前後定位光標之後,再次試圖觸發組字區時、反而會在首次按鍵時失敗。
|
||||
/// 同時注意:必須在 event.type == .flagsChanged 結尾插入 return false,
|
||||
|
@ -270,14 +276,12 @@ class ctlInputMethod: IMKInputController {
|
|||
let theConverted = IME.kanjiConversionIfRequired(theCandidate.1)
|
||||
return (theCandidate.1 == theConverted) ? theCandidate.1 : "\(theConverted)(\(theCandidate.1))"
|
||||
}
|
||||
}
|
||||
if let state = state as? InputState.ChoosingCandidate {
|
||||
} else if let state = state as? InputState.SymbolTable {
|
||||
return state.candidates.map { theCandidate -> String in
|
||||
let theConverted = IME.kanjiConversionIfRequired(theCandidate.1)
|
||||
return (theCandidate.1 == theConverted) ? theCandidate.1 : "\(theConverted)(\(theCandidate.1))"
|
||||
}
|
||||
}
|
||||
if let state = state as? InputState.SymbolTable {
|
||||
} else if let state = state as? InputState.ChoosingCandidate {
|
||||
return state.candidates.map { theCandidate -> String in
|
||||
let theConverted = IME.kanjiConversionIfRequired(theCandidate.1)
|
||||
return (theCandidate.1 == theConverted) ? theCandidate.1 : "\(theConverted)(\(theCandidate.1))"
|
||||
|
@ -285,4 +289,53 @@ class ctlInputMethod: IMKInputController {
|
|||
}
|
||||
return .init()
|
||||
}
|
||||
|
||||
override open func candidateSelectionChanged(_: NSAttributedString!) {
|
||||
// 暫時不需要擴充這個函數。
|
||||
}
|
||||
|
||||
override open func candidateSelected(_ candidateString: NSAttributedString!) {
|
||||
if state is InputState.AssociatedPhrases {
|
||||
if !mgrPrefs.alsoConfirmAssociatedCandidatesByEnter {
|
||||
handle(state: InputState.EmptyIgnoringPreviousState())
|
||||
handle(state: InputState.Empty())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var indexDeducted = 0
|
||||
if let state = state as? InputState.AssociatedPhrases {
|
||||
for (i, neta) in state.candidates.map(\.1).enumerated() {
|
||||
let theConverted = IME.kanjiConversionIfRequired(neta)
|
||||
let netaShown = (neta == theConverted) ? neta : "\(theConverted)(\(neta))"
|
||||
if candidateString.string == netaShown {
|
||||
indexDeducted = i
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if let state = state as? InputState.SymbolTable {
|
||||
for (i, neta) in state.candidates.map(\.1).enumerated() {
|
||||
let theConverted = IME.kanjiConversionIfRequired(neta)
|
||||
let netaShown = (neta == theConverted) ? neta : "\(theConverted)(\(neta))"
|
||||
if candidateString.string == netaShown {
|
||||
indexDeducted = i
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if let state = state as? InputState.ChoosingCandidate {
|
||||
for (i, neta) in state.candidates.map(\.1).enumerated() {
|
||||
let theConverted = IME.kanjiConversionIfRequired(neta)
|
||||
let netaShown = (neta == theConverted) ? neta : "\(theConverted)(\(neta))"
|
||||
if candidateString.string == netaShown {
|
||||
indexDeducted = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
keyHandler(
|
||||
keyHandler,
|
||||
didSelectCandidateAt: indexDeducted,
|
||||
ctlCandidate: ctlInputMethod.ctlCandidateCurrent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,70 @@ extension ctlInputMethod: KeyHandlerDelegate {
|
|||
// MARK: - Candidate Controller Delegate
|
||||
|
||||
extension ctlInputMethod: ctlCandidateDelegate {
|
||||
func handleDelegateEvent(_ event: NSEvent!) -> Bool { handle(event, client: client()) }
|
||||
func handleDelegateEvent(_ event: NSEvent!) -> Bool {
|
||||
// 用 Shift 開關半形英數模式,僅對 macOS 10.15 及之後的 macOS 有效。
|
||||
if #available(macOS 10.15, *) {
|
||||
if ShiftKeyUpChecker.check(event) {
|
||||
if !rencentKeyHandledByKeyHandler {
|
||||
NotifierController.notify(
|
||||
message: String(
|
||||
format: "%@%@%@", NSLocalizedString("Alphanumerical Mode", comment: ""), "\n",
|
||||
toggleASCIIMode()
|
||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
||||
)
|
||||
)
|
||||
}
|
||||
rencentKeyHandledByKeyHandler = false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// 這裡仍舊需要判斷 flags。之前使輸入法狀態卡住無法敲漢字的問題已在 KeyHandler 內修復。
|
||||
/// 這裡不判斷 flags 的話,用方向鍵前後定位光標之後,再次試圖觸發組字區時、反而會在首次按鍵時失敗。
|
||||
/// 同時注意:必須在 event.type == .flagsChanged 結尾插入 return false,
|
||||
/// 否則,每次處理這種判斷時都會觸發 NSInternalInconsistencyException。
|
||||
if event.type == .flagsChanged { return false }
|
||||
|
||||
// 準備修飾鍵,用來判定要新增的詞彙是否需要賦以非常低的權重。
|
||||
ctlInputMethod.areWeNerfing = event.modifierFlags.contains([.shift, .command])
|
||||
|
||||
var textFrame = NSRect.zero
|
||||
|
||||
let attributes: [AnyHashable: Any]? = client().attributes(
|
||||
forCharacterIndex: 0, lineHeightRectangle: &textFrame
|
||||
)
|
||||
|
||||
let isTypingVertical =
|
||||
(attributes?["IMKTextOrientation"] as? NSNumber)?.intValue == 0 || false
|
||||
|
||||
if client().bundleIdentifier()
|
||||
== "org.atelierInmu.vChewing.vChewingPhraseEditor"
|
||||
{
|
||||
IME.areWeUsingOurOwnPhraseEditor = true
|
||||
} else {
|
||||
IME.areWeUsingOurOwnPhraseEditor = false
|
||||
}
|
||||
|
||||
var input = InputSignal(event: event, isVerticalTyping: isTypingVertical)
|
||||
input.isASCIIModeInput = isASCIIMode
|
||||
|
||||
// 無法列印的訊號輸入,一概不作處理。
|
||||
// 這個過程不能放在 KeyHandler 內,否則不會起作用。
|
||||
if !input.charCode.isPrintable {
|
||||
return false
|
||||
}
|
||||
|
||||
/// 將按鍵行為與當前輸入法狀態結合起來、交給按鍵調度模組來處理。
|
||||
/// 再根據返回的 result bool 數值來告知 IMK「這個按鍵事件是被處理了還是被放行了」。
|
||||
let result = keyHandler.handleCandidate(state: state, input: input) { newState in
|
||||
self.handle(state: newState)
|
||||
} errorCallback: {
|
||||
clsSFX.beep()
|
||||
}
|
||||
rencentKeyHandledByKeyHandler = result
|
||||
return result
|
||||
}
|
||||
|
||||
func candidateCountForController(_ controller: ctlCandidateProtocol) -> Int {
|
||||
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
||||
|
|
|
@ -46,6 +46,8 @@ extension ctlInputMethod {
|
|||
candidates = state.candidates
|
||||
}
|
||||
if isTypingVertical { return true }
|
||||
// 接下來的判斷並非適用於 IMK 選字窗,所以先插入排除語句。
|
||||
guard ctlInputMethod.ctlCandidateCurrent is ctlCandidateUniversal else { return false }
|
||||
// 以上是通用情形。接下來決定橫排輸入時是否使用縱排選字窗。
|
||||
// 因為在拿候選字陣列時已經排序過了,所以這裡不用再多排序。
|
||||
// 測量每頁顯示候選字的累計總長度。如果太長的話就強制使用縱排候選字窗。
|
||||
|
@ -65,11 +67,11 @@ extension ctlInputMethod {
|
|||
/// 該問題徹底解決的價值並不大,直接等到 macOS 10.x 全線淘汰之後用 SwiftUI 重寫選字窗吧。
|
||||
|
||||
if isCandidateWindowVertical { // 縱排輸入時強制使用縱排選字窗
|
||||
ctlInputMethod.ctlCandidateCurrent = ctlCandidateUniversal.init(.vertical)
|
||||
ctlInputMethod.ctlCandidateCurrent = ctlCandidateIMK.init(.vertical)
|
||||
} else if mgrPrefs.useHorizontalCandidateList {
|
||||
ctlInputMethod.ctlCandidateCurrent = ctlCandidateUniversal.init(.horizontal)
|
||||
ctlInputMethod.ctlCandidateCurrent = ctlCandidateIMK.init(.horizontal)
|
||||
} else {
|
||||
ctlInputMethod.ctlCandidateCurrent = ctlCandidateUniversal.init(.vertical)
|
||||
ctlInputMethod.ctlCandidateCurrent = ctlCandidateIMK.init(.vertical)
|
||||
}
|
||||
|
||||
// set the attributes for the candidate panel (which uses NSAttributedString)
|
||||
|
|
|
@ -18,6 +18,14 @@ public enum clsSFX {
|
|||
AudioServicesPlaySystemSound(soundID)
|
||||
}
|
||||
|
||||
static func fart() {
|
||||
let filePath = Bundle.main.path(forResource: "Fart", ofType: "m4a")!
|
||||
let fileURL = URL(fileURLWithPath: filePath)
|
||||
var soundID: SystemSoundID = 0
|
||||
AudioServicesCreateSystemSoundID(fileURL as CFURL, &soundID)
|
||||
AudioServicesPlaySystemSound(soundID)
|
||||
}
|
||||
|
||||
static func beep(count: Int = 1) {
|
||||
if count <= 1 {
|
||||
clsSFX.beep()
|
||||
|
|
|
@ -18,8 +18,6 @@ public class ctlCandidateIMK: IMKCandidates, ctlCandidateProtocol {
|
|||
}
|
||||
}
|
||||
|
||||
public var selectedCandidateIndex: Int = .max
|
||||
|
||||
public var visible: Bool = false {
|
||||
didSet {
|
||||
if visible {
|
||||
|
@ -30,10 +28,14 @@ public class ctlCandidateIMK: IMKCandidates, ctlCandidateProtocol {
|
|||
}
|
||||
}
|
||||
|
||||
public var windowTopLeftPoint: NSPoint = .init(x: 0, y: 0) {
|
||||
didSet {
|
||||
public var windowTopLeftPoint: NSPoint {
|
||||
get {
|
||||
let frameRect = candidateFrame()
|
||||
return NSPoint(x: frameRect.minX, y: frameRect.maxY)
|
||||
}
|
||||
set {
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) {
|
||||
self.set(windowTopLeftPoint: self.windowTopLeftPoint, bottomOutOfScreenAdjustmentHeight: 0)
|
||||
self.set(windowTopLeftPoint: newValue, bottomOutOfScreenAdjustmentHeight: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +62,8 @@ public class ctlCandidateIMK: IMKCandidates, ctlCandidateProtocol {
|
|||
case .vertical:
|
||||
setPanelType(kIMKSingleColumnScrollingCandidatePanel)
|
||||
}
|
||||
setAttributes([IMKCandidatesSendServerKeyEventFirst: false])
|
||||
// 設為 true 表示先交給 ctlIME 處理
|
||||
setAttributes([IMKCandidatesSendServerKeyEventFirst: true])
|
||||
}
|
||||
|
||||
public required init(_ layout: CandidateLayout = .horizontal) {
|
||||
|
@ -86,40 +89,151 @@ public class ctlCandidateIMK: IMKCandidates, ctlCandidateProtocol {
|
|||
update()
|
||||
}
|
||||
|
||||
/// 幹話:這裡很多函式內容亂寫也都無所謂了,因為都被 IMKCandidates 代管執行。
|
||||
/// 對於所有 IMK 選字窗的選字判斷動作,不是在 keyHandler 中,而是在 `ctlIME_Core` 中。
|
||||
|
||||
private var currentPageIndex: Int = 0
|
||||
|
||||
private var pageCount: Int {
|
||||
guard let delegate = delegate else {
|
||||
return 0
|
||||
}
|
||||
let totalCount = delegate.candidateCountForController(self)
|
||||
let keyLabelCount = keyLabels.count
|
||||
return totalCount / keyLabelCount + ((totalCount % keyLabelCount) != 0 ? 1 : 0)
|
||||
}
|
||||
|
||||
public func showNextPage() -> Bool {
|
||||
guard delegate != nil else { return false }
|
||||
if pageCount == 1 { return highlightNextCandidate() }
|
||||
if currentPageIndex + 1 >= pageCount { clsSFX.beep() }
|
||||
currentPageIndex = (currentPageIndex + 1 >= pageCount) ? 0 : currentPageIndex + 1
|
||||
if selectedCandidateIndex == candidates(self).count - 1 { return false }
|
||||
selectedCandidateIndex = min(selectedCandidateIndex + keyCount, candidates(self).count - 1)
|
||||
return selectCandidate(withIdentifier: selectedCandidateIndex)
|
||||
pageDownAndModifySelection(self)
|
||||
return true
|
||||
}
|
||||
|
||||
public func showPreviousPage() -> Bool {
|
||||
guard delegate != nil else { return false }
|
||||
if pageCount == 1 { return highlightPreviousCandidate() }
|
||||
if currentPageIndex == 0 { clsSFX.beep() }
|
||||
currentPageIndex = (currentPageIndex == 0) ? pageCount - 1 : currentPageIndex - 1
|
||||
if selectedCandidateIndex == 0 { return true }
|
||||
selectedCandidateIndex = max(selectedCandidateIndex - keyCount, 0)
|
||||
return selectCandidate(withIdentifier: selectedCandidateIndex)
|
||||
pageUpAndModifySelection(self)
|
||||
return true
|
||||
}
|
||||
|
||||
public func highlightNextCandidate() -> Bool {
|
||||
if selectedCandidateIndex == candidates(self).count - 1 { return false }
|
||||
selectedCandidateIndex = min(selectedCandidateIndex + 1, candidates(self).count - 1)
|
||||
return selectCandidate(withIdentifier: selectedCandidateIndex)
|
||||
guard let delegate = delegate else { return false }
|
||||
selectedCandidateIndex =
|
||||
(selectedCandidateIndex + 1 >= delegate.candidateCountForController(self))
|
||||
? 0 : selectedCandidateIndex + 1
|
||||
switch currentLayout {
|
||||
case .horizontal:
|
||||
moveRight(self)
|
||||
return true
|
||||
case .vertical:
|
||||
moveDown(self)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public func highlightPreviousCandidate() -> Bool {
|
||||
if selectedCandidateIndex == 0 { return true }
|
||||
selectedCandidateIndex = max(selectedCandidateIndex - 1, 0)
|
||||
return selectCandidate(withIdentifier: selectedCandidateIndex)
|
||||
}
|
||||
|
||||
public func candidateIndexAtKeyLabelIndex(_: Int) -> Int {
|
||||
selectedCandidateIndex
|
||||
}
|
||||
|
||||
public func set(windowTopLeftPoint: NSPoint, bottomOutOfScreenAdjustmentHeight _: CGFloat = 0) {
|
||||
setCandidateFrameTopLeft(windowTopLeftPoint)
|
||||
}
|
||||
|
||||
override public func handle(_ event: NSEvent!, client _: Any!) -> Bool {
|
||||
guard let delegate = delegate else { return false }
|
||||
return delegate.handleDelegateEvent(event)
|
||||
selectedCandidateIndex =
|
||||
(selectedCandidateIndex == 0)
|
||||
? delegate.candidateCountForController(self) - 1 : selectedCandidateIndex - 1
|
||||
switch currentLayout {
|
||||
case .horizontal:
|
||||
moveLeft(self)
|
||||
return true
|
||||
case .vertical:
|
||||
moveUp(self)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public func candidateIndexAtKeyLabelIndex(_ index: Int) -> Int {
|
||||
guard let delegate = delegate else {
|
||||
return Int.max
|
||||
}
|
||||
|
||||
let result = currentPageIndex * keyLabels.count + index
|
||||
return result < delegate.candidateCountForController(self) ? result : Int.max
|
||||
}
|
||||
|
||||
public var selectedCandidateIndex: Int {
|
||||
get {
|
||||
selectedCandidate()
|
||||
}
|
||||
set {
|
||||
selectCandidate(withIdentifier: newValue)
|
||||
}
|
||||
}
|
||||
|
||||
public func set(windowTopLeftPoint: NSPoint, bottomOutOfScreenAdjustmentHeight height: CGFloat) {
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) {
|
||||
self.doSet(
|
||||
windowTopLeftPoint: windowTopLeftPoint, bottomOutOfScreenAdjustmentHeight: height
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func doSet(windowTopLeftPoint: NSPoint, bottomOutOfScreenAdjustmentHeight height: CGFloat) {
|
||||
var adjustedPoint = windowTopLeftPoint
|
||||
var adjustedHeight = height
|
||||
|
||||
var screenFrame = NSScreen.main?.visibleFrame ?? NSRect.zero
|
||||
for screen in NSScreen.screens {
|
||||
let frame = screen.visibleFrame
|
||||
if windowTopLeftPoint.x >= frame.minX, windowTopLeftPoint.x <= frame.maxX,
|
||||
windowTopLeftPoint.y >= frame.minY, windowTopLeftPoint.y <= frame.maxY
|
||||
{
|
||||
screenFrame = frame
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if adjustedHeight > screenFrame.size.height / 2.0 {
|
||||
adjustedHeight = 0.0
|
||||
}
|
||||
|
||||
let windowSize = candidateFrame().size
|
||||
|
||||
// bottom beneath the screen?
|
||||
if adjustedPoint.y - windowSize.height < screenFrame.minY {
|
||||
adjustedPoint.y = windowTopLeftPoint.y + adjustedHeight + windowSize.height
|
||||
}
|
||||
|
||||
// top over the screen?
|
||||
if adjustedPoint.y >= screenFrame.maxY {
|
||||
adjustedPoint.y = screenFrame.maxY - 1.0
|
||||
}
|
||||
|
||||
// right
|
||||
if adjustedPoint.x + windowSize.width >= screenFrame.maxX {
|
||||
adjustedPoint.x = screenFrame.maxX - windowSize.width
|
||||
}
|
||||
|
||||
// left
|
||||
if adjustedPoint.x < screenFrame.minX {
|
||||
adjustedPoint.x = screenFrame.minX
|
||||
}
|
||||
|
||||
setCandidateFrameTopLeft(adjustedPoint)
|
||||
}
|
||||
|
||||
override public func interpretKeyEvents(_ eventArray: [NSEvent]) {
|
||||
guard !eventArray.isEmpty else { return }
|
||||
let event = eventArray[0]
|
||||
let input = InputSignal(event: event)
|
||||
guard let delegate = delegate else { return }
|
||||
if input.isEsc || input.isBackSpace || input.isDelete || input.isShiftHold {
|
||||
_ = delegate.handleDelegateEvent(event)
|
||||
} else {
|
||||
super.interpretKeyEvents(eventArray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue