Implements the associated phrases function.
Since we use states manage the input flow in McBopomofo, implementing this function becomes easy. What I did is to create a new state, Associated Phrases state, and let the key handler to emit such a state just after emitting a Committing state. When the input method controller is under Associated Phrase state, it shows the candidate window with a tooltip, and only accept candidate keys with the shift key. The key handler uses the characters without modifiers in an NSEvent object to find if there is any matching candidate label, so I added a new member "inputTextIgnoringModifiers" to KeyHandlerInput. I use KeyValueBlobReader to read the associated phrases. I use the cin file from OpenVanilla project but removed the head and tail of the file to pass KeyValueBlobReader's validation.
This commit is contained in:
parent
2dd398f6ca
commit
2ebc789030
|
@ -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 */,
|
||||||
|
|
|
@ -40,6 +40,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 {
|
||||||
|
@ -64,6 +65,7 @@ public class CandidateController: NSWindowController {
|
||||||
@objc public var keyLabels: [String] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
@objc public var keyLabels: [String] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
||||||
@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,6 +67,9 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +84,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
@ -108,13 +128,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)
|
||||||
|
@ -347,6 +375,7 @@ extension HorizontalCandidateController {
|
||||||
candidates.append(candidate)
|
candidates.append(candidate)
|
||||||
}
|
}
|
||||||
candidateView.set(keyLabels: keyLabels, displayedCandidates: candidates)
|
candidateView.set(keyLabels: keyLabels, 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)
|
||||||
|
@ -438,8 +468,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 +477,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) {
|
||||||
|
|
|
@ -36,7 +36,8 @@ class UserPhrasesLM : public Formosa::Gramambular::LanguageModel
|
||||||
public:
|
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,18 @@ 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
|
||||||
|
@ -426,6 +455,12 @@ extension McBopomofoInputMethodController {
|
||||||
gCurrentCandidateController = McBopomofoInputMethodController.verticalCandidateController
|
gCurrentCandidateController = McBopomofoInputMethodController.verticalCandidateController
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if state is InputState.AssociatedPhrases {
|
||||||
|
gCurrentCandidateController?.tooltip = NSLocalizedString("Associated Phrases", comment: "")
|
||||||
|
} else {
|
||||||
|
gCurrentCandidateController?.tooltip = ""
|
||||||
|
}
|
||||||
|
|
||||||
// set the attributes for the candidate panel (which uses NSAttributedString)
|
// set the attributes for the candidate panel (which uses NSAttributedString)
|
||||||
let textSize = Preferences.candidateListTextSize
|
let textSize = Preferences.candidateListTextSize
|
||||||
let keyLabelSize = max(textSize / 2, kMinKeyLabelSize)
|
let keyLabelSize = max(textSize / 2, kMinKeyLabelSize)
|
||||||
|
@ -453,14 +488,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 cursor == state.composingBuffer.count && cursor != 0 {
|
|
||||||
cursor -= 1
|
if let state = state as? InputState.ChoosingCandidate {
|
||||||
|
cursor = state.cursorIndex
|
||||||
|
if cursor == state.composingBuffer.count && cursor != 0 {
|
||||||
|
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 +552,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,28 +561,39 @@ 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 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let selectedValue = state.candidates[Int(index)]
|
|
||||||
keyHandler.fixNode(withValue: selectedValue)
|
|
||||||
|
|
||||||
guard let inputting = keyHandler.buildInputtingState() as? InputState.Inputting else {
|
if let state = state as? InputState.ChoosingCandidate {
|
||||||
return
|
let selectedValue = state.candidates[Int(index)]
|
||||||
}
|
keyHandler.fixNode(withValue: selectedValue)
|
||||||
|
|
||||||
if keyHandler.inputMode == .plainBopomofo {
|
guard let inputting = keyHandler.buildInputtingState() as? InputState.Inputting else {
|
||||||
keyHandler.clear()
|
return
|
||||||
handle(state: .Committing(poppedText: inputting.composingBuffer), client: currentCandidateClient)
|
}
|
||||||
|
|
||||||
|
if keyHandler.inputMode == .plainBopomofo {
|
||||||
|
keyHandler.clear()
|
||||||
|
let text = inputting.composingBuffer
|
||||||
|
handle(state: .Committing(poppedText: text), client: currentCandidateClient)
|
||||||
|
if Preferences.associatedPhrasesEnabled,
|
||||||
|
let associatePhrases = keyHandler.buildAssociatePhraseState(withKey: text, useVerticalMode: state.useVerticalMode) as? InputState.AssociatedPhrases {
|
||||||
|
self.handle(state: associatePhrases, client: self.currentCandidateClient)
|
||||||
|
} else {
|
||||||
|
handle(state: .Empty(), client: currentDeferredClient)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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)
|
||||||
handle(state: .Empty(), client: currentDeferredClient)
|
handle(state: .Empty(), client: currentDeferredClient)
|
||||||
} else {
|
|
||||||
handle(state: inputting, client: currentCandidateClient)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
InputStateEmpty *empty = [[InputStateEmpty alloc] init];
|
|
||||||
stateCallback(empty);
|
if (!Preferences.associatedPhrasesEnabled) {
|
||||||
|
InputStateEmpty *empty = [[InputStateEmpty alloc] init];
|
||||||
|
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
|
||||||
|
@ -845,10 +870,15 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot
|
||||||
UniChar charCode = input.charCode;
|
UniChar charCode = input.charCode;
|
||||||
VTCandidateController *gCurrentCandidateController = [self.delegate candidateControllerForKeyHandler:self];
|
VTCandidateController *gCurrentCandidateController = [self.delegate candidateControllerForKeyHandler:self];
|
||||||
|
|
||||||
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,9 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot
|
||||||
}
|
}
|
||||||
|
|
||||||
if (charCode == 13 || [input isEnter]) {
|
if (charCode == 13 || [input isEnter]) {
|
||||||
|
if ([state isKindOfClass: [InputStateAssociatedPhrases class]] && ![input isShiftHold]) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
[self.delegate keyHandler:self didSelectCandidateAtIndex:gCurrentCandidateController.selectedCandidateIndex candidateController:gCurrentCandidateController];
|
[self.delegate keyHandler:self didSelectCandidateAtIndex:gCurrentCandidateController.selectedCandidateIndex candidateController:gCurrentCandidateController];
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
@ -975,27 +1008,47 @@ 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];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([state isKindOfClass: [InputStateAssociatedPhrases class]]) {
|
||||||
|
candidates = [(InputStateAssociatedPhrases *)state candidates];
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = inputText;
|
||||||
|
|
||||||
|
if ([state isKindOfClass: [InputStateAssociatedPhrases class]]) {
|
||||||
|
match = input.inputTextIgnoringModifiers;
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
if ([match compare:[gCurrentCandidateController.keyLabels objectAtIndex:j] 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 +1057,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 +1248,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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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