diff --git a/Source/UI/CandidateUI/ctlCandidateIMK.swift b/Source/UI/CandidateUI/ctlCandidateIMK.swift new file mode 100644 index 00000000..1425a190 --- /dev/null +++ b/Source/UI/CandidateUI/ctlCandidateIMK.swift @@ -0,0 +1,141 @@ +// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License). +/* +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +1. The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +2. 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 above. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +import Foundation +import InputMethodKit + +public class ctlCandidateIMK: IMKCandidates, ctlCandidateProtocol { + public var currentLayout: CandidateLayout = .horizontal + + public weak var delegate: ctlCandidateDelegate? { + didSet { + reloadData() + } + } + + public var selectedCandidateIndex: Int = .max + + public var visible: Bool = false { + didSet { + if visible { + show() + } else { + hide() + } + } + } + + public var windowTopLeftPoint: NSPoint = .init(x: 0, y: 0) { + didSet { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) { + self.set(windowTopLeftPoint: self.windowTopLeftPoint, bottomOutOfScreenAdjustmentHeight: 0) + } + } + } + + public var keyLabels: [CandidateKeyLabel] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] + .map { + CandidateKeyLabel(key: $0, displayedText: $0) + } + + public var keyLabelFont: NSFont = NSFont.monospacedDigitSystemFont( + ofSize: 14, weight: .medium + ) + public var candidateFont: NSFont = NSFont.systemFont(ofSize: 18) + public var tooltip: String = "" + + var keyCount = 0 + var displayedCandidates = [String]() + + public func specifyLayout(_ layout: CandidateLayout = .horizontal) { + currentLayout = layout + switch currentLayout { + case .horizontal: + setPanelType(kIMKScrollingGridCandidatePanel) + case .vertical: + setPanelType(kIMKSingleColumnScrollingCandidatePanel) + } + setAttributes([IMKCandidatesSendServerKeyEventFirst: false]) + } + + public required init(_ layout: CandidateLayout = .horizontal) { + super.init(server: theServer, panelType: kIMKScrollingGridCandidatePanel) + specifyLayout(layout) + visible = false + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func reloadData() { + guard let delegate = delegate else { return } + let candidates = delegate.candidatesForController(self).map { theCandidate -> String in + let theConverted = IME.kanjiConversionIfRequired(theCandidate.1) + return (theCandidate.1 == theConverted) ? theCandidate.1 : "\(theConverted)(\(theCandidate.1))" + } + setCandidateData(candidates) + keyCount = selectionKeys().count + selectedCandidateIndex = 0 + update() + } + + public func showNextPage() -> Bool { + if selectedCandidateIndex == candidates(self).count - 1 { return false } + selectedCandidateIndex = min(selectedCandidateIndex + keyCount, candidates(self).count - 1) + return selectCandidate(withIdentifier: selectedCandidateIndex) + } + + public func showPreviousPage() -> Bool { + if selectedCandidateIndex == 0 { return true } + selectedCandidateIndex = max(selectedCandidateIndex - keyCount, 0) + return selectCandidate(withIdentifier: selectedCandidateIndex) + } + + public func highlightNextCandidate() -> Bool { + if selectedCandidateIndex == candidates(self).count - 1 { return false } + selectedCandidateIndex = min(selectedCandidateIndex + 1, candidates(self).count - 1) + return selectCandidate(withIdentifier: selectedCandidateIndex) + } + + 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) + } +} diff --git a/vChewing.xcodeproj/project.pbxproj b/vChewing.xcodeproj/project.pbxproj index 8ce2f6ce..08990e45 100644 --- a/vChewing.xcodeproj/project.pbxproj +++ b/vChewing.xcodeproj/project.pbxproj @@ -116,6 +116,7 @@ 5BF9DA2A28840E6200DBD48E /* template-replacements.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5BF9DA2528840E6200DBD48E /* template-replacements.txt */; }; 5BF9DA2B28840E6200DBD48E /* template-userphrases.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5BF9DA2628840E6200DBD48E /* template-userphrases.txt */; }; 5BF9DA2D288427E000DBD48E /* template-associatedPhrases-cht.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5BF9DA2C2884247800DBD48E /* template-associatedPhrases-cht.txt */; }; + 5BFDF011289635C100417BBC /* ctlCandidateIMK.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BFDF010289635C100417BBC /* ctlCandidateIMK.swift */; }; 6A187E2616004C5900466B2E /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6A187E2816004C5900466B2E /* MainMenu.xib */; }; 6A225A1F23679F2600F685C6 /* NotarizedArchives in Resources */ = {isa = PBXBuildFile; fileRef = 6A225A1E23679F2600F685C6 /* NotarizedArchives */; }; 6A2E40F6253A69DA00D1AE1D /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6A2E40F5253A69DA00D1AE1D /* Images.xcassets */; }; @@ -320,6 +321,7 @@ 5BF9DA2528840E6200DBD48E /* template-replacements.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; lineEnding = 0; path = "template-replacements.txt"; sourceTree = ""; usesTabs = 0; }; 5BF9DA2628840E6200DBD48E /* template-userphrases.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; lineEnding = 0; path = "template-userphrases.txt"; sourceTree = ""; usesTabs = 0; }; 5BF9DA2C2884247800DBD48E /* template-associatedPhrases-cht.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; lineEnding = 0; name = "template-associatedPhrases-cht.txt"; path = "../Data/components/cht/template-associatedPhrases-cht.txt"; sourceTree = ""; }; + 5BFDF010289635C100417BBC /* ctlCandidateIMK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ctlCandidateIMK.swift; sourceTree = ""; }; 5BFDF48C27B51867009523B6 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Main.strings"; sourceTree = ""; }; 6A0D4EA215FC0D2D00ABF4B3 /* vChewing.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = vChewing.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6A0D4EF515FC0DA600ABF4B3 /* IME-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "IME-Info.plist"; sourceTree = ""; }; @@ -570,6 +572,7 @@ children = ( 5B62A34027AE7CD900A19448 /* ctlCandidate.swift */, 5B242402284B0D6500520FE4 /* ctlCandidateUniversal.swift */, + 5BFDF010289635C100417BBC /* ctlCandidateIMK.swift */, ); path = CandidateUI; sourceTree = ""; @@ -1203,6 +1206,7 @@ 5BA9FD4727FEF3C9002DE248 /* PreferencesStyleController.swift in Sources */, 5B949BDB2816DDBC00D87B5D /* LMConsolidator.swift in Sources */, 5B38F59F281E2E49007D5F5D /* 3_NodeAnchor.swift in Sources */, + 5BFDF011289635C100417BBC /* ctlCandidateIMK.swift in Sources */, 5B62A34727AE7CD900A19448 /* ctlCandidate.swift in Sources */, 5BA9FD3F27FEF3C8002DE248 /* Pane.swift in Sources */, 5BB802DA27FABA8300CF1C19 /* ctlInputMethod_Menu.swift in Sources */,