Merge pull request #263 from zonble/dev/associated_phrases
Implements the associated phrases function.
This commit is contained in:
commit
beaa6f5404
|
@ -52,6 +52,9 @@
|
||||||
D47B92C027972AD100458394 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47B92BF27972AC800458394 /* main.swift */; };
|
D47B92C027972AD100458394 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47B92BF27972AC800458394 /* main.swift */; };
|
||||||
D47D73A427A5D43900255A50 /* KeyHandlerBopomofoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47D73A327A5D43900255A50 /* KeyHandlerBopomofoTests.swift */; };
|
D47D73A427A5D43900255A50 /* KeyHandlerBopomofoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47D73A327A5D43900255A50 /* KeyHandlerBopomofoTests.swift */; };
|
||||||
D47D73C327A7200500255A50 /* FSEventStreamHelper in Frameworks */ = {isa = PBXBuildFile; productRef = D47D73C227A7200500255A50 /* FSEventStreamHelper */; };
|
D47D73C327A7200500255A50 /* FSEventStreamHelper in Frameworks */ = {isa = PBXBuildFile; productRef = D47D73C227A7200500255A50 /* FSEventStreamHelper */; };
|
||||||
|
D47D73A827A6C84F00255A50 /* associated-phrases.cin in Resources */ = {isa = PBXBuildFile; fileRef = D47D73A727A6C84F00255A50 /* associated-phrases.cin */; };
|
||||||
|
D47D73A927A6C84F00255A50 /* associated-phrases.cin in Resources */ = {isa = PBXBuildFile; fileRef = D47D73A727A6C84F00255A50 /* associated-phrases.cin */; };
|
||||||
|
D47D73AC27A6CAE600255A50 /* AssociatedPhrases.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D47D73AA27A6CAE600255A50 /* AssociatedPhrases.cpp */; };
|
||||||
D47F7DCE278BFB57002F9DD7 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCD278BFB57002F9DD7 /* PreferencesWindowController.swift */; };
|
D47F7DCE278BFB57002F9DD7 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCD278BFB57002F9DD7 /* PreferencesWindowController.swift */; };
|
||||||
D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */; };
|
D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */; };
|
||||||
D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */; };
|
D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */; };
|
||||||
|
@ -205,6 +208,9 @@
|
||||||
D47B92BF27972AC800458394 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
D47B92BF27972AC800458394 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
||||||
D47D73A327A5D43900255A50 /* KeyHandlerBopomofoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyHandlerBopomofoTests.swift; sourceTree = "<group>"; };
|
D47D73A327A5D43900255A50 /* KeyHandlerBopomofoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyHandlerBopomofoTests.swift; sourceTree = "<group>"; };
|
||||||
D47D73C027A71FFA00255A50 /* FSEventStreamHelper */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FSEventStreamHelper; path = Packages/FSEventStreamHelper; sourceTree = "<group>"; };
|
D47D73C027A71FFA00255A50 /* FSEventStreamHelper */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FSEventStreamHelper; path = Packages/FSEventStreamHelper; sourceTree = "<group>"; };
|
||||||
|
D47D73A727A6C84F00255A50 /* associated-phrases.cin */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "associated-phrases.cin"; sourceTree = "<group>"; };
|
||||||
|
D47D73AA27A6CAE600255A50 /* AssociatedPhrases.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = AssociatedPhrases.cpp; sourceTree = "<group>"; };
|
||||||
|
D47D73AB27A6CAE600255A50 /* AssociatedPhrases.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AssociatedPhrases.h; sourceTree = "<group>"; };
|
||||||
D47F7DCD278BFB57002F9DD7 /* PreferencesWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesWindowController.swift; sourceTree = "<group>"; };
|
D47F7DCD278BFB57002F9DD7 /* PreferencesWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesWindowController.swift; sourceTree = "<group>"; };
|
||||||
D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonModalAlertWindowController.swift; sourceTree = "<group>"; };
|
D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonModalAlertWindowController.swift; sourceTree = "<group>"; };
|
||||||
D47F7DD1278C1263002F9DD7 /* UserOverrideModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UserOverrideModel.h; sourceTree = "<group>"; };
|
D47F7DD1278C1263002F9DD7 /* UserOverrideModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UserOverrideModel.h; sourceTree = "<group>"; };
|
||||||
|
@ -337,6 +343,8 @@
|
||||||
D41355DA278E6D17005E5CBD /* McBopomofoLM.h */,
|
D41355DA278E6D17005E5CBD /* McBopomofoLM.h */,
|
||||||
D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */,
|
D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */,
|
||||||
D47F7DD1278C1263002F9DD7 /* UserOverrideModel.h */,
|
D47F7DD1278C1263002F9DD7 /* UserOverrideModel.h */,
|
||||||
|
D47D73AA27A6CAE600255A50 /* AssociatedPhrases.cpp */,
|
||||||
|
D47D73AB27A6CAE600255A50 /* AssociatedPhrases.h */,
|
||||||
);
|
);
|
||||||
path = Engine;
|
path = Engine;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -426,6 +434,7 @@
|
||||||
6A38BBDD15FC115800A8A51F /* Data */ = {
|
6A38BBDD15FC115800A8A51F /* Data */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D47D73A727A6C84F00255A50 /* associated-phrases.cin */,
|
||||||
6A38BBF615FC117A00A8A51F /* data.txt */,
|
6A38BBF615FC117A00A8A51F /* data.txt */,
|
||||||
6AD7CBC715FE555000691B5B /* data-plain-bpmf.txt */,
|
6AD7CBC715FE555000691B5B /* data-plain-bpmf.txt */,
|
||||||
);
|
);
|
||||||
|
@ -648,6 +657,7 @@
|
||||||
6AE210B315FC63CC003659FE /* PlainBopomofo@2x.tiff in Resources */,
|
6AE210B315FC63CC003659FE /* PlainBopomofo@2x.tiff in Resources */,
|
||||||
6AD7CBC815FE555000691B5B /* data-plain-bpmf.txt in Resources */,
|
6AD7CBC815FE555000691B5B /* data-plain-bpmf.txt in Resources */,
|
||||||
6A187E2616004C5900466B2E /* MainMenu.xib in Resources */,
|
6A187E2616004C5900466B2E /* MainMenu.xib in Resources */,
|
||||||
|
D47D73A827A6C84F00255A50 /* associated-phrases.cin in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -671,6 +681,7 @@
|
||||||
files = (
|
files = (
|
||||||
D4E569E527A414CB00AC2CEF /* data.txt in Resources */,
|
D4E569E527A414CB00AC2CEF /* data.txt in Resources */,
|
||||||
D4E569E427A414CB00AC2CEF /* data-plain-bpmf.txt in Resources */,
|
D4E569E427A414CB00AC2CEF /* data-plain-bpmf.txt in Resources */,
|
||||||
|
D47D73A927A6C84F00255A50 /* associated-phrases.cin in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -712,6 +723,7 @@
|
||||||
D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */,
|
D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */,
|
||||||
D456576E279E4F7B00DF6BC9 /* KeyHandlerInput.swift in Sources */,
|
D456576E279E4F7B00DF6BC9 /* KeyHandlerInput.swift in Sources */,
|
||||||
D47F7DCE278BFB57002F9DD7 /* PreferencesWindowController.swift in Sources */,
|
D47F7DCE278BFB57002F9DD7 /* PreferencesWindowController.swift in Sources */,
|
||||||
|
D47D73AC27A6CAE600255A50 /* AssociatedPhrases.cpp in Sources */,
|
||||||
D41355DB278E6D17005E5CBD /* McBopomofoLM.cpp in Sources */,
|
D41355DB278E6D17005E5CBD /* McBopomofoLM.cpp in Sources */,
|
||||||
D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */,
|
D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */,
|
||||||
6A0D4F4515FC0EB100ABF4B3 /* Mandarin.cpp in Sources */,
|
6A0D4F4515FC0EB100ABF4B3 /* Mandarin.cpp in Sources */,
|
||||||
|
|
|
@ -23,6 +23,18 @@
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
|
@objc(VTCandidateKeyLabel)
|
||||||
|
public class CandidateKeyLabel: NSObject {
|
||||||
|
@objc public private(set) var key: String
|
||||||
|
@objc public private(set) var displayedText: String
|
||||||
|
|
||||||
|
public init(key: String, displayedText: String) {
|
||||||
|
self.key = key
|
||||||
|
self.displayedText = displayedText
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc(VTCandidateControllerDelegate)
|
@objc(VTCandidateControllerDelegate)
|
||||||
public protocol CandidateControllerDelegate: AnyObject {
|
public protocol CandidateControllerDelegate: AnyObject {
|
||||||
func candidateCountForController(_ controller: CandidateController) -> UInt
|
func candidateCountForController(_ controller: CandidateController) -> UInt
|
||||||
|
@ -40,6 +52,7 @@ public class CandidateController: NSWindowController {
|
||||||
@objc public var selectedCandidateIndex: UInt = UInt.max
|
@objc public var selectedCandidateIndex: UInt = UInt.max
|
||||||
@objc public var visible: Bool = false {
|
@objc public var visible: Bool = false {
|
||||||
didSet {
|
didSet {
|
||||||
|
NSObject.cancelPreviousPerformRequests(withTarget: self)
|
||||||
if visible {
|
if visible {
|
||||||
window?.perform(#selector(NSWindow.orderFront(_:)), with: self, afterDelay: 0.0)
|
window?.perform(#selector(NSWindow.orderFront(_:)), with: self, afterDelay: 0.0)
|
||||||
} else {
|
} else {
|
||||||
|
@ -61,9 +74,12 @@ public class CandidateController: NSWindowController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc public var keyLabels: [String] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
@objc public var keyLabels: [CandidateKeyLabel] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"].map {
|
||||||
|
CandidateKeyLabel(key: $0, displayedText: $0)
|
||||||
|
}
|
||||||
@objc public var keyLabelFont: NSFont = NSFont.systemFont(ofSize: 14)
|
@objc public var keyLabelFont: NSFont = NSFont.systemFont(ofSize: 14)
|
||||||
@objc public var candidateFont: NSFont = NSFont.systemFont(ofSize: 18)
|
@objc public var candidateFont: NSFont = NSFont.systemFont(ofSize: 18)
|
||||||
|
@objc public var tooltip: String = ""
|
||||||
|
|
||||||
@objc public func reloadData() {
|
@objc public func reloadData() {
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,23 @@ fileprivate class HorizontalCandidateView: NSView {
|
||||||
private var elementWidths: [CGFloat] = []
|
private var elementWidths: [CGFloat] = []
|
||||||
private var trackingHighlightedIndex: UInt = UInt.max
|
private var trackingHighlightedIndex: UInt = UInt.max
|
||||||
|
|
||||||
|
private let tooltipPadding: CGFloat = 2.0
|
||||||
|
private var tooltipSize: NSSize = NSSize.zero
|
||||||
|
|
||||||
|
override var toolTip: String? {
|
||||||
|
didSet {
|
||||||
|
if let toolTip = toolTip, !toolTip.isEmpty {
|
||||||
|
let baseSize = NSSize(width: 10240.0, height: 10240.0)
|
||||||
|
var tooltipRect = (toolTip as NSString).boundingRect(with: baseSize, options: .usesLineFragmentOrigin, attributes: keyLabelAttrDict)
|
||||||
|
tooltipRect.size.height += tooltipPadding * 2
|
||||||
|
tooltipRect.size.width += tooltipPadding * 2
|
||||||
|
self.tooltipSize = tooltipRect.size
|
||||||
|
} else {
|
||||||
|
self.tooltipSize = NSSize.zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override var isFlipped: Bool {
|
override var isFlipped: Bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
@ -50,10 +67,12 @@ fileprivate class HorizontalCandidateView: NSView {
|
||||||
result.width += CGFloat(elementWidths.count)
|
result.width += CGFloat(elementWidths.count)
|
||||||
result.height = keyLabelHeight + candidateTextHeight + 1.0
|
result.height = keyLabelHeight + candidateTextHeight + 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.height += tooltipSize.height
|
||||||
|
result.width = max(tooltipSize.width, result.width)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc(setKeyLabels:displayedCandidates:)
|
|
||||||
func set(keyLabels labels: [String], displayedCandidates candidates: [String]) {
|
func set(keyLabels labels: [String], displayedCandidates candidates: [String]) {
|
||||||
let count = min(labels.count, candidates.count)
|
let count = min(labels.count, candidates.count)
|
||||||
keyLabels = Array(labels[0..<count])
|
keyLabels = Array(labels[0..<count])
|
||||||
|
@ -64,14 +83,13 @@ fileprivate class HorizontalCandidateView: NSView {
|
||||||
for index in 0..<count {
|
for index in 0..<count {
|
||||||
let labelRect = (keyLabels[index] as NSString).boundingRect(with: baseSize, options: .usesLineFragmentOrigin, attributes: keyLabelAttrDict)
|
let labelRect = (keyLabels[index] as NSString).boundingRect(with: baseSize, options: .usesLineFragmentOrigin, attributes: keyLabelAttrDict)
|
||||||
let candidateRect = (displayedCandidates[index] as NSString).boundingRect(with: baseSize, options: .usesLineFragmentOrigin, attributes: candidateAttrDict)
|
let candidateRect = (displayedCandidates[index] as NSString).boundingRect(with: baseSize, options: .usesLineFragmentOrigin, attributes: candidateAttrDict)
|
||||||
let cellWidth = max(keyLabelHeight * 2,
|
let cellWidth = max(candidateTextHeight,
|
||||||
max(labelRect.size.width, candidateRect.size.width)) + cellPadding;
|
max(labelRect.size.width, candidateRect.size.width)) + cellPadding;
|
||||||
newWidths.append(cellWidth)
|
newWidths.append(cellWidth)
|
||||||
}
|
}
|
||||||
elementWidths = newWidths
|
elementWidths = newWidths
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc(setKeyLabelFont:candidateFont:)
|
|
||||||
func set(keyLabelFont labelFont: NSFont, candidateFont: NSFont) {
|
func set(keyLabelFont labelFont: NSFont, candidateFont: NSFont) {
|
||||||
let paraStyle = NSMutableParagraphStyle()
|
let paraStyle = NSMutableParagraphStyle()
|
||||||
paraStyle.setParagraphStyle(NSParagraphStyle.default)
|
paraStyle.setParagraphStyle(NSParagraphStyle.default)
|
||||||
|
@ -108,13 +126,21 @@ fileprivate class HorizontalCandidateView: NSView {
|
||||||
NSColor.darkGray.setStroke()
|
NSColor.darkGray.setStroke()
|
||||||
}
|
}
|
||||||
|
|
||||||
NSBezierPath.strokeLine(from: NSPoint(x: bounds.size.width, y: 0.0), to: NSPoint(x: bounds.size.width, y: bounds.size.height))
|
if let toolTip = toolTip {
|
||||||
|
lightGray.setFill()
|
||||||
|
NSBezierPath.fill(NSMakeRect(0, 0, bounds.width, tooltipSize.height))
|
||||||
|
let tooltipFrame = NSRect(x: 0, y: 0, width: tooltipSize.width, height: tooltipSize.height)
|
||||||
|
(toolTip as NSString).draw(in: tooltipFrame, withAttributes: keyLabelAttrDict)
|
||||||
|
NSBezierPath.strokeLine(from: NSPoint(x: 0, y: tooltipSize.height - 2), to: NSPoint(x: bounds.width, y: tooltipSize.height - 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
NSBezierPath.strokeLine(from: NSPoint(x: bounds.width, y: 0), to: NSPoint(x: bounds.width, y: bounds.height))
|
||||||
|
|
||||||
var accuWidth: CGFloat = 0
|
var accuWidth: CGFloat = 0
|
||||||
for index in 0..<elementWidths.count {
|
for index in 0..<elementWidths.count {
|
||||||
let currentWidth = elementWidths[index]
|
let currentWidth = elementWidths[index]
|
||||||
let labelRect = NSRect(x: accuWidth, y: 0.0, width: currentWidth, height: keyLabelHeight)
|
let labelRect = NSRect(x: accuWidth, y: tooltipSize.height, width: currentWidth, height: keyLabelHeight)
|
||||||
let candidateRect = NSRect(x: accuWidth, y: keyLabelHeight + 1.0, width: currentWidth, height: candidateTextHeight)
|
let candidateRect = NSRect(x: accuWidth, y: tooltipSize.height + keyLabelHeight + 1.0, width: currentWidth, height: candidateTextHeight)
|
||||||
(index == highlightedIndex ? darkGray : lightGray).setFill()
|
(index == highlightedIndex ? darkGray : lightGray).setFill()
|
||||||
NSBezierPath.fill(labelRect)
|
NSBezierPath.fill(labelRect)
|
||||||
(keyLabels[index] as NSString).draw(in: labelRect, withAttributes: keyLabelAttrDict)
|
(keyLabels[index] as NSString).draw(in: labelRect, withAttributes: keyLabelAttrDict)
|
||||||
|
@ -346,7 +372,8 @@ extension HorizontalCandidateController {
|
||||||
let candidate = delegate.candidateController(self, candidateAtIndex: index)
|
let candidate = delegate.candidateController(self, candidateAtIndex: index)
|
||||||
candidates.append(candidate)
|
candidates.append(candidate)
|
||||||
}
|
}
|
||||||
candidateView.set(keyLabels: keyLabels, displayedCandidates: candidates)
|
candidateView.set(keyLabels: keyLabels.map { $0.displayedText}, displayedCandidates: candidates)
|
||||||
|
candidateView.toolTip = tooltip
|
||||||
var newSize = candidateView.sizeForView
|
var newSize = candidateView.sizeForView
|
||||||
var frameRect = candidateView.frame
|
var frameRect = candidateView.frame
|
||||||
frameRect.size = newSize
|
frameRect.size = newSize
|
||||||
|
|
|
@ -85,6 +85,12 @@ private let kCandidateTextLeftMargin: CGFloat = 8.0
|
||||||
private let kCandidateTextPaddingWithMandatedTableViewPadding: CGFloat = 18.0
|
private let kCandidateTextPaddingWithMandatedTableViewPadding: CGFloat = 18.0
|
||||||
private let kCandidateTextLeftMarginWithMandatedTableViewPadding: CGFloat = 0.0
|
private let kCandidateTextLeftMarginWithMandatedTableViewPadding: CGFloat = 0.0
|
||||||
|
|
||||||
|
private class BackgroundView: NSView {
|
||||||
|
override func draw(_ dirtyRect: NSRect) {
|
||||||
|
NSColor.windowBackgroundColor.setFill()
|
||||||
|
NSBezierPath.fill(self.bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc(VTVerticalCandidateController)
|
@objc(VTVerticalCandidateController)
|
||||||
public class VerticalCandidateController: CandidateController {
|
public class VerticalCandidateController: CandidateController {
|
||||||
|
@ -95,6 +101,8 @@ public class VerticalCandidateController: CandidateController {
|
||||||
private var candidateTextPadding: CGFloat = kCandidateTextPadding
|
private var candidateTextPadding: CGFloat = kCandidateTextPadding
|
||||||
private var candidateTextLeftMargin: CGFloat = kCandidateTextLeftMargin
|
private var candidateTextLeftMargin: CGFloat = kCandidateTextLeftMargin
|
||||||
private var maxCandidateAttrStringWidth: CGFloat = 0
|
private var maxCandidateAttrStringWidth: CGFloat = 0
|
||||||
|
private let tooltipPadding: CGFloat = 2.0
|
||||||
|
private var tooltipView: NSTextField
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
var contentRect = NSRect(x: 128.0, y: 128.0, width: 0.0, height: 0.0)
|
var contentRect = NSRect(x: 128.0, y: 128.0, width: 0.0, height: 0.0)
|
||||||
|
@ -102,6 +110,14 @@ public class VerticalCandidateController: CandidateController {
|
||||||
let panel = NSPanel(contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false)
|
let panel = NSPanel(contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false)
|
||||||
panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1)
|
panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1)
|
||||||
panel.hasShadow = true
|
panel.hasShadow = true
|
||||||
|
panel.contentView = BackgroundView()
|
||||||
|
|
||||||
|
tooltipView = NSTextField(frame: NSRect.zero)
|
||||||
|
tooltipView.isEditable = false
|
||||||
|
tooltipView.isSelectable = false
|
||||||
|
tooltipView.isBezeled = false
|
||||||
|
tooltipView.drawsBackground = true
|
||||||
|
tooltipView.lineBreakMode = .byTruncatingTail
|
||||||
|
|
||||||
contentRect.origin = NSPoint.zero
|
contentRect.origin = NSPoint.zero
|
||||||
var stripRect = contentRect
|
var stripRect = contentRect
|
||||||
|
@ -400,6 +416,20 @@ extension VerticalCandidateController: NSTableViewDataSource, NSTableViewDelegat
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var tooltipHeight: CGFloat = 0
|
||||||
|
var tooltipWidth: CGFloat = 0
|
||||||
|
|
||||||
|
if !tooltip.isEmpty {
|
||||||
|
tooltipView.stringValue = tooltip
|
||||||
|
let size = tooltipView.intrinsicContentSize
|
||||||
|
tooltipWidth = size.width + tooltipPadding * 2
|
||||||
|
tooltipHeight = size.height + tooltipPadding * 2
|
||||||
|
self.window?.contentView?.addSubview(tooltipView)
|
||||||
|
} else {
|
||||||
|
tooltipView.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
let candidateFontSize = ceil(candidateFont.pointSize)
|
let candidateFontSize = ceil(candidateFont.pointSize)
|
||||||
let keyLabelFontSize = ceil(keyLabelFont.pointSize)
|
let keyLabelFontSize = ceil(keyLabelFont.pointSize)
|
||||||
let fontSize = max(candidateFontSize, keyLabelFontSize)
|
let fontSize = max(candidateFontSize, keyLabelFontSize)
|
||||||
|
@ -420,7 +450,8 @@ extension VerticalCandidateController: NSTableViewDataSource, NSTableViewDelegat
|
||||||
}
|
}
|
||||||
|
|
||||||
keyLabelStripView.keyLabelFont = keyLabelFont
|
keyLabelStripView.keyLabelFont = keyLabelFont
|
||||||
keyLabelStripView.keyLabels = Array(keyLabels[0..<Int(keyLabelCount)])
|
let keyLabels = keyLabels[0..<Int(keyLabelCount)].map { $0.displayedText }
|
||||||
|
keyLabelStripView.keyLabels = keyLabels
|
||||||
keyLabelStripView.labelOffsetY = (keyLabelFontSize >= candidateFontSize) ? 0.0 : floor((candidateFontSize - keyLabelFontSize) / 2.0)
|
keyLabelStripView.labelOffsetY = (keyLabelFontSize >= candidateFontSize) ? 0.0 : floor((candidateFontSize - keyLabelFontSize) / 2.0)
|
||||||
|
|
||||||
let rowHeight = ceil(fontSize * 1.25)
|
let rowHeight = ceil(fontSize * 1.25)
|
||||||
|
@ -438,8 +469,8 @@ extension VerticalCandidateController: NSTableViewDataSource, NSTableViewDelegat
|
||||||
let rowSpacing = tableView.intercellSpacing.height
|
let rowSpacing = tableView.intercellSpacing.height
|
||||||
let stripWidth = ceil(maxKeyLabelWidth * 1.20)
|
let stripWidth = ceil(maxKeyLabelWidth * 1.20)
|
||||||
let tableViewStartWidth = ceil(maxCandidateAttrStringWidth + scrollerWidth)
|
let tableViewStartWidth = ceil(maxCandidateAttrStringWidth + scrollerWidth)
|
||||||
let windowWidth = stripWidth + 1.0 + tableViewStartWidth
|
let windowWidth = max(stripWidth + 1.0 + tableViewStartWidth, tooltipWidth)
|
||||||
let windowHeight = CGFloat(keyLabelCount) * (rowHeight + rowSpacing)
|
let windowHeight = CGFloat(keyLabelCount) * (rowHeight + rowSpacing) + tooltipHeight
|
||||||
|
|
||||||
var frameRect = self.window?.frame ?? NSRect.zero
|
var frameRect = self.window?.frame ?? NSRect.zero
|
||||||
let topLeftPoint = NSMakePoint(frameRect.origin.x, frameRect.origin.y + frameRect.size.height)
|
let topLeftPoint = NSMakePoint(frameRect.origin.x, frameRect.origin.y + frameRect.size.height)
|
||||||
|
@ -447,8 +478,9 @@ extension VerticalCandidateController: NSTableViewDataSource, NSTableViewDelegat
|
||||||
frameRect.size = NSMakeSize(windowWidth, windowHeight)
|
frameRect.size = NSMakeSize(windowWidth, windowHeight)
|
||||||
frameRect.origin = NSMakePoint(topLeftPoint.x, topLeftPoint.y - frameRect.size.height)
|
frameRect.origin = NSMakePoint(topLeftPoint.x, topLeftPoint.y - frameRect.size.height)
|
||||||
|
|
||||||
keyLabelStripView.frame = NSRect(x: 0.0, y: 0.0, width: stripWidth, height: windowHeight)
|
keyLabelStripView.frame = NSRect(x: 0.0, y: 0, width: stripWidth, height: windowHeight - tooltipHeight)
|
||||||
scrollView.frame = NSRect(x: stripWidth + 1.0, y: 0.0, width: tableViewStartWidth, height: windowHeight)
|
scrollView.frame = NSRect(x: stripWidth + 1.0, y: 0, width: (windowWidth - stripWidth - 1), height: windowHeight - tooltipHeight)
|
||||||
|
tooltipView.frame = NSRect(x: tooltipPadding, y: windowHeight - tooltipHeight + tooltipPadding, width: windowWidth, height: tooltipHeight)
|
||||||
self.window?.setFrame(frameRect, display: false)
|
self.window?.setFrame(frameRect, display: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
//
|
||||||
|
// AssociatedPhrases.cpp
|
||||||
|
//
|
||||||
|
// Copyright (c) 2017 The McBopomofo Project.
|
||||||
|
//
|
||||||
|
// 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:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be
|
||||||
|
// included in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "AssociatedPhrases.h"
|
||||||
|
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <fstream>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "KeyValueBlobReader.h"
|
||||||
|
|
||||||
|
namespace McBopomofo {
|
||||||
|
|
||||||
|
AssociatedPhrases::AssociatedPhrases()
|
||||||
|
: fd(-1)
|
||||||
|
, data(0)
|
||||||
|
, length(0)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
AssociatedPhrases::~AssociatedPhrases()
|
||||||
|
{
|
||||||
|
if (data) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool AssociatedPhrases::isLoaded()
|
||||||
|
{
|
||||||
|
if (data) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AssociatedPhrases::open(const char *path)
|
||||||
|
{
|
||||||
|
if (data) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fd = ::open(path, O_RDONLY);
|
||||||
|
if (fd == -1) {
|
||||||
|
printf("open:: file not exist");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct stat sb;
|
||||||
|
if (fstat(fd, &sb) == -1) {
|
||||||
|
printf("open:: cannot open file");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
length = (size_t)sb.st_size;
|
||||||
|
|
||||||
|
data = mmap(NULL, length, PROT_READ, MAP_SHARED, fd, 0);
|
||||||
|
if (!data) {
|
||||||
|
::close(fd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyValueBlobReader reader(static_cast<char*>(data), length);
|
||||||
|
KeyValueBlobReader::KeyValue keyValue;
|
||||||
|
KeyValueBlobReader::State state;
|
||||||
|
while ((state = reader.Next(&keyValue)) == KeyValueBlobReader::State::HAS_PAIR) {
|
||||||
|
keyRowMap[keyValue.key].emplace_back(keyValue.key, keyValue.value);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssociatedPhrases::close()
|
||||||
|
{
|
||||||
|
if (data) {
|
||||||
|
munmap(data, length);
|
||||||
|
::close(fd);
|
||||||
|
data = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
keyRowMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<std::string> AssociatedPhrases::valuesForKey(const std::string& key)
|
||||||
|
{
|
||||||
|
std::vector<std::string> v;
|
||||||
|
auto iter = keyRowMap.find(key);
|
||||||
|
if (iter != keyRowMap.end()) {
|
||||||
|
const std::vector<Row>& rows = iter->second;
|
||||||
|
for (const auto& row : rows) {
|
||||||
|
std::string_view value = row.value;
|
||||||
|
v.push_back({value.data(), value.size()});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool AssociatedPhrases::hasValuesForKey(const std::string& key)
|
||||||
|
{
|
||||||
|
return keyRowMap.find(key) != keyRowMap.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
}; // namespace McBopomofo
|
|
@ -0,0 +1,66 @@
|
||||||
|
//
|
||||||
|
// AssociatedPhrases.h
|
||||||
|
//
|
||||||
|
// Copyright (c) 2017 The McBopomofo Project.
|
||||||
|
//
|
||||||
|
// 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:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be
|
||||||
|
// included in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef ASSOCIATEDPHRASES_H
|
||||||
|
#define ASSOCIATEDPHRASES_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <map>
|
||||||
|
#include <iostream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace McBopomofo {
|
||||||
|
|
||||||
|
class AssociatedPhrases
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AssociatedPhrases();
|
||||||
|
~AssociatedPhrases();
|
||||||
|
|
||||||
|
const bool isLoaded();
|
||||||
|
bool open(const char *path);
|
||||||
|
void close();
|
||||||
|
const std::vector<std::string> valuesForKey(const std::string& key);
|
||||||
|
const bool hasValuesForKey(const std::string& key);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
struct Row {
|
||||||
|
Row(std::string_view& k, std::string_view& v) : key(k), value(v) {}
|
||||||
|
std::string_view key;
|
||||||
|
std::string_view value;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::map<std::string_view, std::vector<Row>> keyRowMap;
|
||||||
|
|
||||||
|
int fd;
|
||||||
|
void *data;
|
||||||
|
size_t length;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* AssociatedPhrases_hpp */
|
|
@ -37,11 +37,7 @@ McBopomofoLM::~McBopomofoLM()
|
||||||
m_userPhrases.close();
|
m_userPhrases.close();
|
||||||
m_excludedPhrases.close();
|
m_excludedPhrases.close();
|
||||||
m_phraseReplacement.close();
|
m_phraseReplacement.close();
|
||||||
}
|
m_associatedPhrases.close();
|
||||||
|
|
||||||
bool McBopomofoLM::isDataModelLoaded()
|
|
||||||
{
|
|
||||||
return m_languageModel.isLoaded();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void McBopomofoLM::loadLanguageModel(const char* languageModelDataPath)
|
void McBopomofoLM::loadLanguageModel(const char* languageModelDataPath)
|
||||||
|
@ -52,6 +48,24 @@ void McBopomofoLM::loadLanguageModel(const char* languageModelDataPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool McBopomofoLM::isDataModelLoaded()
|
||||||
|
{
|
||||||
|
return m_languageModel.isLoaded();
|
||||||
|
}
|
||||||
|
|
||||||
|
void McBopomofoLM::loadAssociatedPhrases(const char* associatedPhrasesPath)
|
||||||
|
{
|
||||||
|
if (associatedPhrasesPath) {
|
||||||
|
m_associatedPhrases.close();
|
||||||
|
m_associatedPhrases.open(associatedPhrasesPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool McBopomofoLM::isAssociatedPhrasesLoaded()
|
||||||
|
{
|
||||||
|
return m_associatedPhrases.isLoaded();
|
||||||
|
}
|
||||||
|
|
||||||
void McBopomofoLM::loadUserPhrases(const char* userPhrasesDataPath,
|
void McBopomofoLM::loadUserPhrases(const char* userPhrasesDataPath,
|
||||||
const char* excludedPhrasesDataPath)
|
const char* excludedPhrasesDataPath)
|
||||||
{
|
{
|
||||||
|
@ -189,3 +203,13 @@ const vector<Unigram> McBopomofoLM::filterAndTransformUnigrams(const vector<Unig
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const vector<std::string> McBopomofoLM::associatedPhrasesForKey(const string& key)
|
||||||
|
{
|
||||||
|
return m_associatedPhrases.valuesForKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool McBopomofoLM::hasAssociatedPhrasesForKey(const string& key)
|
||||||
|
{
|
||||||
|
return m_associatedPhrases.hasValuesForKey(key);
|
||||||
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
#include "UserPhrasesLM.h"
|
#include "UserPhrasesLM.h"
|
||||||
#include "ParselessLM.h"
|
#include "ParselessLM.h"
|
||||||
#include "PhraseReplacementMap.h"
|
#include "PhraseReplacementMap.h"
|
||||||
|
#include "AssociatedPhrases.h"
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
namespace McBopomofo {
|
namespace McBopomofo {
|
||||||
|
@ -61,11 +62,18 @@ public:
|
||||||
McBopomofoLM();
|
McBopomofoLM();
|
||||||
~McBopomofoLM();
|
~McBopomofoLM();
|
||||||
|
|
||||||
/// Asks to load the primary language model a the given path.
|
/// Asks to load the primary language model at the given path.
|
||||||
/// @param languageModelPath Thw path of the language model.
|
/// @param languageModelPath The path of the language model.
|
||||||
void loadLanguageModel(const char* languageModelPath);
|
void loadLanguageModel(const char* languageModelPath);
|
||||||
/// If the data model is already loaded.
|
/// If the data model is already loaded.
|
||||||
bool isDataModelLoaded();
|
bool isDataModelLoaded();
|
||||||
|
|
||||||
|
/// Asks to load the associated phrases at the given path.
|
||||||
|
/// @param associatedPhrasesPath The path of the associated phrases.
|
||||||
|
void loadAssociatedPhrases(const char* associatedPhrasesPath);
|
||||||
|
/// If the associated phrases already loaded.
|
||||||
|
bool isAssociatedPhrasesLoaded();
|
||||||
|
|
||||||
/// Asks to load the user phrases and excluded phrases at the given path.
|
/// Asks to load the user phrases and excluded phrases at the given path.
|
||||||
/// @param userPhrasesPath The path of user phrases.
|
/// @param userPhrasesPath The path of user phrases.
|
||||||
/// @param excludedPhrasesPath The path of excluded phrases.
|
/// @param excludedPhrasesPath The path of excluded phrases.
|
||||||
|
@ -96,6 +104,10 @@ public:
|
||||||
/// Sets a lambda to let the values of unigrams could be converted by it.
|
/// Sets a lambda to let the values of unigrams could be converted by it.
|
||||||
void setExternalConverter(std::function<string(string)> externalConverter);
|
void setExternalConverter(std::function<string(string)> externalConverter);
|
||||||
|
|
||||||
|
const vector<std::string> associatedPhrasesForKey(const string& key);
|
||||||
|
bool hasAssociatedPhrasesForKey(const string& key);
|
||||||
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
/// Filters and converts the input unigrams and return a new list of unigrams.
|
/// Filters and converts the input unigrams and return a new list of unigrams.
|
||||||
///
|
///
|
||||||
|
@ -112,6 +124,7 @@ protected:
|
||||||
UserPhrasesLM m_userPhrases;
|
UserPhrasesLM m_userPhrases;
|
||||||
UserPhrasesLM m_excludedPhrases;
|
UserPhrasesLM m_excludedPhrases;
|
||||||
PhraseReplacementMap m_phraseReplacement;
|
PhraseReplacementMap m_phraseReplacement;
|
||||||
|
AssociatedPhrases m_associatedPhrases;
|
||||||
bool m_phraseReplacementEnabled;
|
bool m_phraseReplacementEnabled;
|
||||||
bool m_externalConverterEnabled;
|
bool m_externalConverterEnabled;
|
||||||
std::function<string(string)> m_externalConverter;
|
std::function<string(string)> m_externalConverter;
|
||||||
|
|
|
@ -47,6 +47,14 @@ UserPhrasesLM::~UserPhrasesLM()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool UserPhrasesLM::isLoaded()
|
||||||
|
{
|
||||||
|
if (data) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool UserPhrasesLM::open(const char *path)
|
bool UserPhrasesLM::open(const char *path)
|
||||||
{
|
{
|
||||||
if (data) {
|
if (data) {
|
||||||
|
|
|
@ -37,6 +37,7 @@ public:
|
||||||
UserPhrasesLM();
|
UserPhrasesLM();
|
||||||
~UserPhrasesLM();
|
~UserPhrasesLM();
|
||||||
|
|
||||||
|
bool isLoaded();
|
||||||
bool open(const char *path);
|
bool open(const char *path);
|
||||||
void close();
|
void close();
|
||||||
void dump();
|
void dump();
|
||||||
|
|
|
@ -76,6 +76,11 @@ class McBopomofoInputMethodController: IMKInputController {
|
||||||
let inputMode = keyHandler.inputMode
|
let inputMode = keyHandler.inputMode
|
||||||
let optionKeyPressed = NSEvent.modifierFlags.contains(.option)
|
let optionKeyPressed = NSEvent.modifierFlags.contains(.option)
|
||||||
|
|
||||||
|
if inputMode == .plainBopomofo {
|
||||||
|
let associatedPhrasesItem = menu.addItem(withTitle: NSLocalizedString("Associated Phrases", comment: ""), action: #selector(toggleAssociatedPhrasesEnabled(_:)), keyEquivalent: "")
|
||||||
|
associatedPhrasesItem.state = Preferences.associatedPhrasesEnabled.state
|
||||||
|
}
|
||||||
|
|
||||||
if inputMode == .bopomofo && optionKeyPressed {
|
if inputMode == .bopomofo && optionKeyPressed {
|
||||||
let phaseReplacementItem = menu.addItem(withTitle: NSLocalizedString("Use Phrase Replacement", comment: ""), action: #selector(togglePhraseReplacement(_:)), keyEquivalent: "")
|
let phaseReplacementItem = menu.addItem(withTitle: NSLocalizedString("Use Phrase Replacement", comment: ""), action: #selector(togglePhraseReplacement(_:)), keyEquivalent: "")
|
||||||
phaseReplacementItem.state = Preferences.phraseReplacementEnabled.state
|
phaseReplacementItem.state = Preferences.phraseReplacementEnabled.state
|
||||||
|
@ -200,6 +205,10 @@ class McBopomofoInputMethodController: IMKInputController {
|
||||||
NotifierController.notify(message: enabled ? NSLocalizedString("Half-width punctuation on", comment: "") : NSLocalizedString("Half-width punctuation off", comment: ""))
|
NotifierController.notify(message: enabled ? NSLocalizedString("Half-width punctuation on", comment: "") : NSLocalizedString("Half-width punctuation off", comment: ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func toggleAssociatedPhrasesEnabled(_ sender: Any?) {
|
||||||
|
_ = Preferences.toggleAssociatedPhrasesEnabled()
|
||||||
|
}
|
||||||
|
|
||||||
@objc func togglePhraseReplacement(_ sender: Any?) {
|
@objc func togglePhraseReplacement(_ sender: Any?) {
|
||||||
let enabled = Preferences.togglePhraseReplacementEnabled()
|
let enabled = Preferences.togglePhraseReplacementEnabled()
|
||||||
LanguageModelManager.phraseReplacementEnabled = enabled
|
LanguageModelManager.phraseReplacementEnabled = enabled
|
||||||
|
@ -276,6 +285,8 @@ extension McBopomofoInputMethodController {
|
||||||
handle(state: newState, previous: previous, client: client)
|
handle(state: newState, previous: previous, client: client)
|
||||||
} else if let newState = newState as? InputState.ChoosingCandidate {
|
} else if let newState = newState as? InputState.ChoosingCandidate {
|
||||||
handle(state: newState, previous: previous, client: client)
|
handle(state: newState, previous: previous, client: client)
|
||||||
|
} else if let newState = newState as? InputState.AssociatedPhrases {
|
||||||
|
handle(state: newState, previous: previous, client: client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -407,9 +418,17 @@ extension McBopomofoInputMethodController {
|
||||||
// the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound,
|
// the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound,
|
||||||
// i.e. the client app needs to take care of where to put this composing buffer
|
// i.e. the client app needs to take care of where to put this composing buffer
|
||||||
client.setMarkedText(state.attributedString, selectionRange: NSMakeRange(Int(state.cursorIndex), 0), replacementRange: NSMakeRange(NSNotFound, NSNotFound))
|
client.setMarkedText(state.attributedString, selectionRange: NSMakeRange(Int(state.cursorIndex), 0), replacementRange: NSMakeRange(NSNotFound, NSNotFound))
|
||||||
if previous is InputState.ChoosingCandidate == false {
|
|
||||||
show(candidateWindowWith: state, client: client)
|
show(candidateWindowWith: state, client: client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func handle(state: InputState.AssociatedPhrases, previous: InputState, client: Any?) {
|
||||||
|
hideTooltip()
|
||||||
|
guard let client = client as? IMKTextInput else {
|
||||||
|
gCurrentCandidateController?.visible = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client.setMarkedText("", selectionRange: NSMakeRange(0, 0), replacementRange: NSMakeRange(NSNotFound, NSNotFound))
|
||||||
|
show(candidateWindowWith: state, client: client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -417,8 +436,17 @@ extension McBopomofoInputMethodController {
|
||||||
|
|
||||||
extension McBopomofoInputMethodController {
|
extension McBopomofoInputMethodController {
|
||||||
|
|
||||||
private func show(candidateWindowWith state: InputState.ChoosingCandidate, client: Any!) {
|
private func show(candidateWindowWith state: InputState, client: Any!) {
|
||||||
if state.useVerticalMode {
|
let useVerticalMode: Bool = {
|
||||||
|
if let state = state as? InputState.ChoosingCandidate {
|
||||||
|
return state.useVerticalMode
|
||||||
|
} else if let state = state as? InputState.AssociatedPhrases {
|
||||||
|
return state.useVerticalMode
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}()
|
||||||
|
|
||||||
|
if useVerticalMode {
|
||||||
gCurrentCandidateController = McBopomofoInputMethodController.verticalCandidateController
|
gCurrentCandidateController = McBopomofoInputMethodController.verticalCandidateController
|
||||||
} else if Preferences.useHorizontalCandidateList {
|
} else if Preferences.useHorizontalCandidateList {
|
||||||
gCurrentCandidateController = McBopomofoInputMethodController.horizontalCandidateController
|
gCurrentCandidateController = McBopomofoInputMethodController.horizontalCandidateController
|
||||||
|
@ -442,10 +470,11 @@ extension McBopomofoInputMethodController {
|
||||||
|
|
||||||
let candidateKeys = Preferences.candidateKeys
|
let candidateKeys = Preferences.candidateKeys
|
||||||
let keyLabels = candidateKeys.count > 4 ? Array(candidateKeys) : Array(Preferences.defaultCandidateKeys)
|
let keyLabels = candidateKeys.count > 4 ? Array(candidateKeys) : Array(Preferences.defaultCandidateKeys)
|
||||||
|
let keyLabelPrefix = state is InputState.AssociatedPhrases ? "⇧ " : ""
|
||||||
|
gCurrentCandidateController?.keyLabels = keyLabels.map {
|
||||||
|
CandidateKeyLabel(key: String($0), displayedText: keyLabelPrefix + String($0))
|
||||||
|
}
|
||||||
|
|
||||||
gCurrentCandidateController?.keyLabels = Array(keyLabels.map {
|
|
||||||
String($0)
|
|
||||||
})
|
|
||||||
gCurrentCandidateController?.delegate = self
|
gCurrentCandidateController?.delegate = self
|
||||||
gCurrentCandidateController?.reloadData()
|
gCurrentCandidateController?.reloadData()
|
||||||
currentCandidateClient = client
|
currentCandidateClient = client
|
||||||
|
@ -453,14 +482,18 @@ extension McBopomofoInputMethodController {
|
||||||
gCurrentCandidateController?.visible = true
|
gCurrentCandidateController?.visible = true
|
||||||
|
|
||||||
var lineHeightRect = NSMakeRect(0.0, 0.0, 16.0, 16.0)
|
var lineHeightRect = NSMakeRect(0.0, 0.0, 16.0, 16.0)
|
||||||
var cursor = state.cursorIndex
|
var cursor: UInt = 0
|
||||||
|
|
||||||
|
if let state = state as? InputState.ChoosingCandidate {
|
||||||
|
cursor = state.cursorIndex
|
||||||
if cursor == state.composingBuffer.count && cursor != 0 {
|
if cursor == state.composingBuffer.count && cursor != 0 {
|
||||||
cursor -= 1
|
cursor -= 1
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
(client as? IMKTextInput)?.attributes(forCharacterIndex: Int(cursor), lineHeightRectangle: &lineHeightRect)
|
(client as? IMKTextInput)?.attributes(forCharacterIndex: Int(cursor), lineHeightRectangle: &lineHeightRect)
|
||||||
|
|
||||||
if state.useVerticalMode {
|
if useVerticalMode {
|
||||||
gCurrentCandidateController?.set(windowTopLeftPoint: NSMakePoint(lineHeightRect.origin.x + lineHeightRect.size.width + 4.0, lineHeightRect.origin.y - 4.0), bottomOutOfScreenAdjustmentHeight: lineHeightRect.size.height + 4.0)
|
gCurrentCandidateController?.set(windowTopLeftPoint: NSMakePoint(lineHeightRect.origin.x + lineHeightRect.size.width + 4.0, lineHeightRect.origin.y - 4.0), bottomOutOfScreenAdjustmentHeight: lineHeightRect.size.height + 4.0)
|
||||||
} else {
|
} else {
|
||||||
gCurrentCandidateController?.set(windowTopLeftPoint: NSMakePoint(lineHeightRect.origin.x, lineHeightRect.origin.y - 4.0), bottomOutOfScreenAdjustmentHeight: lineHeightRect.size.height + 4.0)
|
gCurrentCandidateController?.set(windowTopLeftPoint: NSMakePoint(lineHeightRect.origin.x, lineHeightRect.origin.y - 4.0), bottomOutOfScreenAdjustmentHeight: lineHeightRect.size.height + 4.0)
|
||||||
|
@ -513,6 +546,8 @@ extension McBopomofoInputMethodController: CandidateControllerDelegate {
|
||||||
func candidateCountForController(_ controller: CandidateController) -> UInt {
|
func candidateCountForController(_ controller: CandidateController) -> UInt {
|
||||||
if let state = state as? InputState.ChoosingCandidate {
|
if let state = state as? InputState.ChoosingCandidate {
|
||||||
return UInt(state.candidates.count)
|
return UInt(state.candidates.count)
|
||||||
|
} else if let state = state as? InputState.AssociatedPhrases {
|
||||||
|
return UInt(state.candidates.count)
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
@ -520,15 +555,15 @@ extension McBopomofoInputMethodController: CandidateControllerDelegate {
|
||||||
func candidateController(_ controller: CandidateController, candidateAtIndex index: UInt) -> String {
|
func candidateController(_ controller: CandidateController, candidateAtIndex index: UInt) -> String {
|
||||||
if let state = state as? InputState.ChoosingCandidate {
|
if let state = state as? InputState.ChoosingCandidate {
|
||||||
return state.candidates[Int(index)]
|
return state.candidates[Int(index)]
|
||||||
|
} else if let state = state as? InputState.AssociatedPhrases {
|
||||||
|
return state.candidates[Int(index)]
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func candidateController(_ controller: CandidateController, didSelectCandidateAtIndex index: UInt) {
|
func candidateController(_ controller: CandidateController, didSelectCandidateAtIndex index: UInt) {
|
||||||
gCurrentCandidateController?.visible = false
|
|
||||||
guard let state = state as? InputState.ChoosingCandidate else {
|
if let state = state as? InputState.ChoosingCandidate {
|
||||||
return
|
|
||||||
}
|
|
||||||
let selectedValue = state.candidates[Int(index)]
|
let selectedValue = state.candidates[Int(index)]
|
||||||
keyHandler.fixNode(withValue: selectedValue)
|
keyHandler.fixNode(withValue: selectedValue)
|
||||||
|
|
||||||
|
@ -538,10 +573,26 @@ extension McBopomofoInputMethodController: CandidateControllerDelegate {
|
||||||
|
|
||||||
if keyHandler.inputMode == .plainBopomofo {
|
if keyHandler.inputMode == .plainBopomofo {
|
||||||
keyHandler.clear()
|
keyHandler.clear()
|
||||||
handle(state: .Committing(poppedText: inputting.composingBuffer), client: currentCandidateClient)
|
let composingBuffer = inputting.composingBuffer
|
||||||
|
handle(state: .Committing(poppedText: composingBuffer), client: currentCandidateClient)
|
||||||
|
if Preferences.associatedPhrasesEnabled,
|
||||||
|
let associatePhrases = keyHandler.buildAssociatePhraseState(withKey: composingBuffer, useVerticalMode: state.useVerticalMode) as? InputState.AssociatedPhrases {
|
||||||
|
self.handle(state: associatePhrases, client: self.currentCandidateClient)
|
||||||
|
} else {
|
||||||
handle(state: .Empty(), client: currentDeferredClient)
|
handle(state: .Empty(), client: currentDeferredClient)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
handle(state: inputting, client: currentCandidateClient)
|
handle(state: inputting, client: currentCandidateClient)
|
||||||
}
|
}
|
||||||
|
} else if let state = state as? InputState.AssociatedPhrases {
|
||||||
|
let selectedValue = state.candidates[Int(index)]
|
||||||
|
handle(state: .Committing(poppedText: selectedValue), client: currentCandidateClient)
|
||||||
|
if Preferences.associatedPhrasesEnabled,
|
||||||
|
let associatePhrases = keyHandler.buildAssociatePhraseState(withKey: selectedValue, useVerticalMode: state.useVerticalMode) as? InputState.AssociatedPhrases {
|
||||||
|
self.handle(state: associatePhrases, client: self.currentCandidateClient)
|
||||||
|
} else {
|
||||||
|
handle(state: .Empty(), client: currentDeferredClient)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -170,7 +170,7 @@ class InputState: NSObject {
|
||||||
return String(format: NSLocalizedString("You are now selecting \"%@\". Press enter to add a new phrase.", comment: ""), text)
|
return String(format: NSLocalizedString("You are now selecting \"%@\". Press enter to add a new phrase.", comment: ""), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private(set) var readings: [String] = []
|
@objc private(set) var readings: [String]
|
||||||
|
|
||||||
@objc init(composingBuffer: String, cursorIndex: UInt, markerIndex: UInt, readings: [String]) {
|
@objc init(composingBuffer: String, cursorIndex: UInt, markerIndex: UInt, readings: [String]) {
|
||||||
self.markerIndex = markerIndex
|
self.markerIndex = markerIndex
|
||||||
|
@ -226,8 +226,8 @@ class InputState: NSObject {
|
||||||
/// Represents that the user is choosing in a candidates list.
|
/// Represents that the user is choosing in a candidates list.
|
||||||
@objc (InputStateChoosingCandidate)
|
@objc (InputStateChoosingCandidate)
|
||||||
class ChoosingCandidate: NotEmpty {
|
class ChoosingCandidate: NotEmpty {
|
||||||
@objc private(set) var candidates: [String] = []
|
@objc private(set) var candidates: [String]
|
||||||
@objc private(set) var useVerticalMode: Bool = false
|
@objc private(set) var useVerticalMode: Bool
|
||||||
|
|
||||||
@objc init(composingBuffer: String, cursorIndex: UInt, candidates: [String], useVerticalMode: Bool) {
|
@objc init(composingBuffer: String, cursorIndex: UInt, candidates: [String], useVerticalMode: Bool) {
|
||||||
self.candidates = candidates
|
self.candidates = candidates
|
||||||
|
@ -248,4 +248,21 @@ class InputState: NSObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents that the user is choosing in a candidates list
|
||||||
|
/// in the associated phrases mode.
|
||||||
|
@objc (InputStateAssociatedPhrases)
|
||||||
|
class AssociatedPhrases: InputState {
|
||||||
|
@objc private(set) var candidates: [String] = []
|
||||||
|
@objc private(set) var useVerticalMode: Bool = false
|
||||||
|
@objc init(candidates: [String], useVerticalMode: Bool) {
|
||||||
|
self.candidates = candidates
|
||||||
|
self.useVerticalMode = useVerticalMode
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override var description: String {
|
||||||
|
"<InputState.AssociatedPhrases, candidates:\(candidates), useVerticalMode:\(useVerticalMode)>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,7 @@ candidateSelectionCallback:(void (^)(void))candidateSelectionCallback
|
||||||
- (void)clear;
|
- (void)clear;
|
||||||
|
|
||||||
- (InputState *)buildInputtingState;
|
- (InputState *)buildInputtingState;
|
||||||
|
- (nullable InputState *)buildAssociatePhraseStateWithKey:(NSString *)key useVerticalMode:(BOOL)useVerticalMode;
|
||||||
|
|
||||||
@property (strong, nonatomic) InputMode inputMode;
|
@property (strong, nonatomic) InputMode inputMode;
|
||||||
@property (weak, nonatomic) id <KeyHandlerDelegate> delegate;
|
@property (weak, nonatomic) id <KeyHandlerDelegate> delegate;
|
||||||
|
|
|
@ -232,7 +232,9 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot
|
||||||
|
|
||||||
// if the composing buffer is empty and there's no reading, and there is some function key combination, we ignore it
|
// if the composing buffer is empty and there's no reading, and there is some function key combination, we ignore it
|
||||||
BOOL isFunctionKey = ([input isCommandHold] || [input isControlHold] || [input isOptionHold] || [input isNumericPad]);
|
BOOL isFunctionKey = ([input isCommandHold] || [input isControlHold] || [input isOptionHold] || [input isNumericPad]);
|
||||||
if (![state isKindOfClass:[InputStateNotEmpty class]] && isFunctionKey) {
|
if (![state isKindOfClass:[InputStateNotEmpty class]] &&
|
||||||
|
![state isKindOfClass:[InputStateAssociatedPhrases class]] &&
|
||||||
|
isFunctionKey) {
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,7 +278,17 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot
|
||||||
|
|
||||||
// MARK: Handle Candidates
|
// MARK: Handle Candidates
|
||||||
if ([state isKindOfClass:[InputStateChoosingCandidate class]]) {
|
if ([state isKindOfClass:[InputStateChoosingCandidate class]]) {
|
||||||
return [self _handleCandidateState:(InputStateChoosingCandidate *) state input:input stateCallback:stateCallback candidateSelectionCallback:candidateSelectionCallback errorCallback:errorCallback];
|
return [self _handleCandidateState:state input:input stateCallback:stateCallback candidateSelectionCallback:candidateSelectionCallback errorCallback:errorCallback];
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Handle Associated Phrases
|
||||||
|
if ([state isKindOfClass:[InputStateAssociatedPhrases class]]) {
|
||||||
|
BOOL result = [self _handleCandidateState:state input:input stateCallback:stateCallback candidateSelectionCallback:candidateSelectionCallback errorCallback:errorCallback];
|
||||||
|
if (result) {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
state = [[InputStateEmpty alloc] init];
|
||||||
|
stateCallback(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Handle Marking
|
// MARK: Handle Marking
|
||||||
|
@ -350,10 +362,23 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot
|
||||||
InputStateChoosingCandidate *choosingCandidates = [self _buildCandidateState:inputting useVerticalMode:input.useVerticalMode];
|
InputStateChoosingCandidate *choosingCandidates = [self _buildCandidateState:inputting useVerticalMode:input.useVerticalMode];
|
||||||
if (choosingCandidates.candidates.count == 1) {
|
if (choosingCandidates.candidates.count == 1) {
|
||||||
[self clear];
|
[self clear];
|
||||||
InputStateCommitting *committing = [[InputStateCommitting alloc] initWithPoppedText:choosingCandidates.candidates.firstObject];
|
NSString *text = choosingCandidates.candidates.firstObject;
|
||||||
|
InputStateCommitting *committing = [[InputStateCommitting alloc] initWithPoppedText:text];
|
||||||
stateCallback(committing);
|
stateCallback(committing);
|
||||||
|
|
||||||
|
if (!Preferences.associatedPhrasesEnabled) {
|
||||||
InputStateEmpty *empty = [[InputStateEmpty alloc] init];
|
InputStateEmpty *empty = [[InputStateEmpty alloc] init];
|
||||||
stateCallback(empty);
|
stateCallback(empty);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
InputStateAssociatedPhrases *associatedPhrases = (InputStateAssociatedPhrases *)[self buildAssociatePhraseStateWithKey:text useVerticalMode:input.useVerticalMode];
|
||||||
|
if (associatedPhrases) {
|
||||||
|
stateCallback(associatedPhrases);
|
||||||
|
} else {
|
||||||
|
InputStateEmpty *empty = [[InputStateEmpty alloc] init];
|
||||||
|
stateCallback(empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
stateCallback(choosingCandidates);
|
stateCallback(choosingCandidates);
|
||||||
}
|
}
|
||||||
|
@ -835,7 +860,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
- (BOOL)_handleCandidateState:(InputStateChoosingCandidate *)state
|
- (BOOL)_handleCandidateState:(InputState *)state
|
||||||
input:(KeyHandlerInput *)input
|
input:(KeyHandlerInput *)input
|
||||||
stateCallback:(void (^)(InputState *))stateCallback
|
stateCallback:(void (^)(InputState *))stateCallback
|
||||||
candidateSelectionCallback:(void (^)(void))candidateSelectionCallback
|
candidateSelectionCallback:(void (^)(void))candidateSelectionCallback
|
||||||
|
@ -848,7 +873,12 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot
|
||||||
BOOL cancelCandidateKey = (charCode == 27) || (charCode == 8) || [input isDelete];
|
BOOL cancelCandidateKey = (charCode == 27) || (charCode == 8) || [input isDelete];
|
||||||
|
|
||||||
if (cancelCandidateKey) {
|
if (cancelCandidateKey) {
|
||||||
if (_inputMode == InputModePlainBopomofo) {
|
if ([state isKindOfClass: [InputStateAssociatedPhrases class]]) {
|
||||||
|
[self clear];
|
||||||
|
InputStateEmptyIgnoringPreviousState *empty = [[InputStateEmptyIgnoringPreviousState alloc] init];
|
||||||
|
stateCallback(empty);
|
||||||
|
}
|
||||||
|
else if (_inputMode == InputModePlainBopomofo) {
|
||||||
[self clear];
|
[self clear];
|
||||||
InputStateEmptyIgnoringPreviousState *empty = [[InputStateEmptyIgnoringPreviousState alloc] init];
|
InputStateEmptyIgnoringPreviousState *empty = [[InputStateEmptyIgnoringPreviousState alloc] init];
|
||||||
stateCallback(empty);
|
stateCallback(empty);
|
||||||
|
@ -860,6 +890,12 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot
|
||||||
}
|
}
|
||||||
|
|
||||||
if (charCode == 13 || [input isEnter]) {
|
if (charCode == 13 || [input isEnter]) {
|
||||||
|
if ([state isKindOfClass: [InputStateAssociatedPhrases class]]) {
|
||||||
|
[self clear];
|
||||||
|
InputStateEmptyIgnoringPreviousState *empty = [[InputStateEmptyIgnoringPreviousState alloc] init];
|
||||||
|
stateCallback(empty);
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
[self.delegate keyHandler:self didSelectCandidateAtIndex:gCurrentCandidateController.selectedCandidateIndex candidateController:gCurrentCandidateController];
|
[self.delegate keyHandler:self didSelectCandidateAtIndex:gCurrentCandidateController.selectedCandidateIndex candidateController:gCurrentCandidateController];
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
@ -975,27 +1011,51 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (([input isEnd] || input.emacsKey == McBopomofoEmacsKeyEnd) && [state.candidates count] > 0) {
|
NSArray *candidates;
|
||||||
if (gCurrentCandidateController.selectedCandidateIndex == [state.candidates count] - 1) {
|
|
||||||
|
if ([state isKindOfClass: [InputStateChoosingCandidate class]]) {
|
||||||
|
candidates = [(InputStateChoosingCandidate *)state candidates];
|
||||||
|
} else if ([state isKindOfClass: [InputStateAssociatedPhrases class]]) {
|
||||||
|
candidates = [(InputStateAssociatedPhrases *)state candidates];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!candidates) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (([input isEnd] || input.emacsKey == McBopomofoEmacsKeyEnd) && candidates.count > 0) {
|
||||||
|
if (gCurrentCandidateController.selectedCandidateIndex == candidates.count - 1) {
|
||||||
errorCallback();
|
errorCallback();
|
||||||
} else {
|
} else {
|
||||||
gCurrentCandidateController.selectedCandidateIndex = [state.candidates count] - 1;
|
gCurrentCandidateController.selectedCandidateIndex = candidates.count - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
candidateSelectionCallback();
|
candidateSelectionCallback();
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ([state isKindOfClass:[InputStateAssociatedPhrases class]]) {
|
||||||
|
if (![input isShiftHold]) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NSInteger index = NSNotFound;
|
NSInteger index = NSNotFound;
|
||||||
|
NSString *match;
|
||||||
|
if ([state isKindOfClass:[InputStateAssociatedPhrases class]]) {
|
||||||
|
match = input.inputTextIgnoringModifiers;
|
||||||
|
} else {
|
||||||
|
match = inputText;
|
||||||
|
}
|
||||||
|
|
||||||
for (NSUInteger j = 0, c = [gCurrentCandidateController.keyLabels count]; j < c; j++) {
|
for (NSUInteger j = 0, c = [gCurrentCandidateController.keyLabels count]; j < c; j++) {
|
||||||
if ([inputText compare:[gCurrentCandidateController.keyLabels objectAtIndex:j] options:NSCaseInsensitiveSearch] == NSOrderedSame) {
|
VTCandidateKeyLabel *label = gCurrentCandidateController.keyLabels[j];
|
||||||
|
if ([match compare:label.key options:NSCaseInsensitiveSearch] == NSOrderedSame) {
|
||||||
index = j;
|
index = j;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[gCurrentCandidateController.keyLabels indexOfObject:inputText];
|
|
||||||
|
|
||||||
if (index != NSNotFound) {
|
if (index != NSNotFound) {
|
||||||
NSUInteger candidateIndex = [gCurrentCandidateController candidateIndexAtKeyLabelIndex:index];
|
NSUInteger candidateIndex = [gCurrentCandidateController candidateIndexAtKeyLabelIndex:index];
|
||||||
if (candidateIndex != NSUIntegerMax) {
|
if (candidateIndex != NSUIntegerMax) {
|
||||||
|
@ -1004,6 +1064,10 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ([state isKindOfClass:[InputStateAssociatedPhrases class]]) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
if (_inputMode == InputModePlainBopomofo) {
|
if (_inputMode == InputModePlainBopomofo) {
|
||||||
string layout = [self _currentLayout];
|
string layout = [self _currentLayout];
|
||||||
string punctuationNamePrefix = Preferences.halfWidthPunctuationEnabled ? string("_half_punctuation_") : string("_punctuation_");
|
string punctuationNamePrefix = Preferences.halfWidthPunctuationEnabled ? string("_half_punctuation_") : string("_punctuation_");
|
||||||
|
@ -1191,4 +1255,20 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot
|
||||||
return readingsArray;
|
return readingsArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (nullable InputState *)buildAssociatePhraseStateWithKey:(NSString *)key useVerticalMode:(BOOL)useVerticalMode
|
||||||
|
{
|
||||||
|
string cppKey = string(key.UTF8String);
|
||||||
|
if (_languageModel->hasAssociatedPhrasesForKey(cppKey)) {
|
||||||
|
vector<string> phrases = _languageModel->associatedPhrasesForKey(cppKey);
|
||||||
|
NSMutableArray <NSString *> *array = [NSMutableArray array];
|
||||||
|
for (auto phrase: phrases) {
|
||||||
|
NSString *item = [[NSString alloc] initWithUTF8String:phrase.c_str()];
|
||||||
|
[array addObject:item];
|
||||||
|
}
|
||||||
|
InputStateAssociatedPhrases *associatedPhrases = [[InputStateAssociatedPhrases alloc] initWithCandidates:array useVerticalMode:useVerticalMode];
|
||||||
|
return associatedPhrases;
|
||||||
|
}
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -40,8 +40,9 @@ enum KeyCode: UInt16 {
|
||||||
class KeyHandlerInput: NSObject {
|
class KeyHandlerInput: NSObject {
|
||||||
@objc private (set) var useVerticalMode: Bool
|
@objc private (set) var useVerticalMode: Bool
|
||||||
@objc private (set) var inputText: String?
|
@objc private (set) var inputText: String?
|
||||||
|
@objc private (set) var inputTextIgnoringModifiers: String?
|
||||||
@objc private (set) var charCode: UInt16
|
@objc private (set) var charCode: UInt16
|
||||||
private var keyCode: UInt16
|
@objc private (set) var keyCode: UInt16
|
||||||
private var flags: NSEvent.ModifierFlags
|
private var flags: NSEvent.ModifierFlags
|
||||||
private var cursorForwardKey: KeyCode
|
private var cursorForwardKey: KeyCode
|
||||||
private var cursorBackwardKey: KeyCode
|
private var cursorBackwardKey: KeyCode
|
||||||
|
@ -50,8 +51,9 @@ class KeyHandlerInput: NSObject {
|
||||||
private var verticalModeOnlyChooseCandidateKey: KeyCode
|
private var verticalModeOnlyChooseCandidateKey: KeyCode
|
||||||
@objc private (set) var emacsKey: McBopomofoEmacsKey
|
@objc private (set) var emacsKey: McBopomofoEmacsKey
|
||||||
|
|
||||||
@objc init(inputText: String?, keyCode: UInt16, charCode: UInt16, flags: NSEvent.ModifierFlags, isVerticalMode: Bool) {
|
@objc init(inputText: String?, keyCode: UInt16, charCode: UInt16, flags: NSEvent.ModifierFlags, isVerticalMode: Bool, inputTextIgnoringModifiers: String? = nil) {
|
||||||
self.inputText = inputText
|
self.inputText = inputText
|
||||||
|
self.inputTextIgnoringModifiers = inputTextIgnoringModifiers ?? inputText
|
||||||
self.keyCode = keyCode
|
self.keyCode = keyCode
|
||||||
self.charCode = charCode
|
self.charCode = charCode
|
||||||
self.flags = flags
|
self.flags = flags
|
||||||
|
@ -67,6 +69,7 @@ class KeyHandlerInput: NSObject {
|
||||||
|
|
||||||
@objc init(event: NSEvent, isVerticalMode: Bool) {
|
@objc init(event: NSEvent, isVerticalMode: Bool) {
|
||||||
inputText = event.characters
|
inputText = event.characters
|
||||||
|
inputTextIgnoringModifiers = event.charactersIgnoringModifiers
|
||||||
keyCode = event.keyCode
|
keyCode = event.keyCode
|
||||||
flags = event.modifierFlags
|
flags = event.modifierFlags
|
||||||
useVerticalMode = isVerticalMode
|
useVerticalMode = isVerticalMode
|
||||||
|
|
|
@ -53,6 +53,13 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomo
|
||||||
lm.loadLanguageModel([dataPath UTF8String]);
|
lm.loadLanguageModel([dataPath UTF8String]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void LTLoadAssociatedPhrases(McBopomofoLM &lm)
|
||||||
|
{
|
||||||
|
Class cls = NSClassFromString(@"McBopomofoInputMethodController");
|
||||||
|
NSString *dataPath = [[NSBundle bundleForClass:cls] pathForResource:@"associated-phrases" ofType:@"cin"];
|
||||||
|
lm.loadAssociatedPhrases([dataPath UTF8String]);
|
||||||
|
}
|
||||||
|
|
||||||
+ (void)loadDataModels
|
+ (void)loadDataModels
|
||||||
{
|
{
|
||||||
if (!gLanguageModelMcBopomofo.isDataModelLoaded()) {
|
if (!gLanguageModelMcBopomofo.isDataModelLoaded()) {
|
||||||
|
@ -61,6 +68,9 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomo
|
||||||
if (!gLanguageModelPlainBopomofo.isDataModelLoaded()) {
|
if (!gLanguageModelPlainBopomofo.isDataModelLoaded()) {
|
||||||
LTLoadLanguageModelFile(@"data-plain-bpmf", gLanguageModelPlainBopomofo);
|
LTLoadLanguageModelFile(@"data-plain-bpmf", gLanguageModelPlainBopomofo);
|
||||||
}
|
}
|
||||||
|
if (!gLanguageModelPlainBopomofo.isAssociatedPhrasesLoaded()) {
|
||||||
|
LTLoadAssociatedPhrases(gLanguageModelPlainBopomofo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (void)loadDataModel:(InputMode)mode
|
+ (void)loadDataModel:(InputMode)mode
|
||||||
|
@ -70,10 +80,14 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomo
|
||||||
LTLoadLanguageModelFile(@"data", gLanguageModelMcBopomofo);
|
LTLoadLanguageModelFile(@"data", gLanguageModelMcBopomofo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([mode isEqualToString:InputModePlainBopomofo]) {
|
if ([mode isEqualToString:InputModePlainBopomofo]) {
|
||||||
if (!gLanguageModelPlainBopomofo.isDataModelLoaded()) {
|
if (!gLanguageModelPlainBopomofo.isDataModelLoaded()) {
|
||||||
LTLoadLanguageModelFile(@"data-plain-bpmf", gLanguageModelPlainBopomofo);
|
LTLoadLanguageModelFile(@"data-plain-bpmf", gLanguageModelPlainBopomofo);
|
||||||
}
|
}
|
||||||
|
if (!gLanguageModelPlainBopomofo.isAssociatedPhrasesLoaded()) {
|
||||||
|
LTLoadAssociatedPhrases(gLanguageModelPlainBopomofo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,8 @@ private let kCandidateKeys = "CandidateKeys"
|
||||||
private let kPhraseReplacementEnabledKey = "PhraseReplacementEnabled"
|
private let kPhraseReplacementEnabledKey = "PhraseReplacementEnabled"
|
||||||
private let kChineseConversionEngineKey = "ChineseConversionEngine"
|
private let kChineseConversionEngineKey = "ChineseConversionEngine"
|
||||||
private let kChineseConversionStyle = "ChineseConversionStyle"
|
private let kChineseConversionStyle = "ChineseConversionStyle"
|
||||||
|
private let kAssociatedPhrasesEnabledKey = "AssociatedPhrasesEnabled"
|
||||||
|
//private let kAssociatedPhrasesKeys = "AssociatedPhrasesKeys"
|
||||||
|
|
||||||
private let kDefaultCandidateListTextSize: CGFloat = 16
|
private let kDefaultCandidateListTextSize: CGFloat = 16
|
||||||
private let kMinCandidateListTextSize: CGFloat = 12
|
private let kMinCandidateListTextSize: CGFloat = 12
|
||||||
|
@ -57,6 +59,7 @@ private let kMinComposingBufferSize = 4
|
||||||
private let kMaxComposingBufferSize = 20
|
private let kMaxComposingBufferSize = 20
|
||||||
|
|
||||||
private let kDefaultKeys = "123456789"
|
private let kDefaultKeys = "123456789"
|
||||||
|
private let kDefaultAssociatedPhrasesKeys = "!@#$%^&*("
|
||||||
|
|
||||||
// MARK: Property wrappers
|
// MARK: Property wrappers
|
||||||
|
|
||||||
|
@ -215,6 +218,8 @@ class Preferences: NSObject {
|
||||||
defaults.removeObject(forKey: kPhraseReplacementEnabledKey)
|
defaults.removeObject(forKey: kPhraseReplacementEnabledKey)
|
||||||
defaults.removeObject(forKey: kChineseConversionEngineKey)
|
defaults.removeObject(forKey: kChineseConversionEngineKey)
|
||||||
defaults.removeObject(forKey: kChineseConversionStyle)
|
defaults.removeObject(forKey: kChineseConversionStyle)
|
||||||
|
defaults.removeObject(forKey: kAssociatedPhrasesEnabledKey)
|
||||||
|
// defaults.removeObject(forKey: kAssociatedPhrasesKeys)
|
||||||
}
|
}
|
||||||
|
|
||||||
@UserDefault(key: kKeyboardLayoutPreferenceKey, defaultValue: 0)
|
@UserDefault(key: kKeyboardLayoutPreferenceKey, defaultValue: 0)
|
||||||
|
@ -365,4 +370,12 @@ class Preferences: NSObject {
|
||||||
ChineseConversionStyle(rawValue: chineseConversionStyle)?.name
|
ChineseConversionStyle(rawValue: chineseConversionStyle)?.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UserDefault(key: kAssociatedPhrasesEnabledKey, defaultValue: false)
|
||||||
|
@objc static var associatedPhrasesEnabled: Bool
|
||||||
|
|
||||||
|
@objc static func toggleAssociatedPhrasesEnabled() -> Bool {
|
||||||
|
associatedPhrasesEnabled = !associatedPhrasesEnabled
|
||||||
|
return associatedPhrasesEnabled
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -133,7 +133,9 @@ import Carbon
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func changeSelectionKeyAction(_ sender: Any) {
|
@IBAction func changeSelectionKeyAction(_ sender: Any) {
|
||||||
guard let keys = (sender as AnyObject).stringValue?.trimmingCharacters(in: .whitespacesAndNewlines) else {
|
guard let keys = (sender as AnyObject).stringValue?
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
.lowercased() else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
|
|
|
@ -98,3 +98,5 @@
|
||||||
"Half-width punctuation on" = "Half-width punctuation on";
|
"Half-width punctuation on" = "Half-width punctuation on";
|
||||||
|
|
||||||
"Half-width punctuation off" = "Half-width punctuation off";
|
"Half-width punctuation off" = "Half-width punctuation off";
|
||||||
|
|
||||||
|
"Associated Phrases" = "Associated Phrases";
|
||||||
|
|
|
@ -98,3 +98,5 @@
|
||||||
"Half-width punctuation on" = "已經切換到半型標點模式";
|
"Half-width punctuation on" = "已經切換到半型標點模式";
|
||||||
|
|
||||||
"Half-width punctuation off" = "已經切回到全型標點模式";
|
"Half-width punctuation off" = "已經切回到全型標點模式";
|
||||||
|
|
||||||
|
"Associated Phrases" = "聯想詞";
|
||||||
|
|
Loading…
Reference in New Issue