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:
zonble 2022-01-31 02:33:28 +08:00
parent 2dd398f6ca
commit 2ebc789030
19 changed files with 547 additions and 57 deletions

View File

@ -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 */,

View File

@ -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() {
} }

View File

@ -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

View File

@ -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)
} }
} }

View File

@ -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

View File

@ -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 */

View File

@ -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);
}

View File

@ -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;

View File

@ -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) {

View File

@ -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();

View File

@ -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)
} }
} }
} }

View File

@ -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)>"
}
}
} }

View File

@ -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;

View File

@ -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
@ -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,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

View File

@ -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

View File

@ -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);
}
} }
} }

View File

@ -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
}
} }

View File

@ -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";

View File

@ -98,3 +98,5 @@
"Half-width punctuation on" = "已經切換到半型標點模式"; "Half-width punctuation on" = "已經切換到半型標點模式";
"Half-width punctuation off" = "已經切回到全型標點模式"; "Half-width punctuation off" = "已經切回到全型標點模式";
"Associated Phrases" = "聯想詞";