Merge pull request #263 from zonble/dev/associated_phrases
Implements the associated phrases function.
This commit is contained in:
commit
beaa6f5404
|
@ -52,6 +52,9 @@
|
|||
D47B92C027972AD100458394 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47B92BF27972AC800458394 /* main.swift */; };
|
||||
D47D73A427A5D43900255A50 /* KeyHandlerBopomofoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47D73A327A5D43900255A50 /* KeyHandlerBopomofoTests.swift */; };
|
||||
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 */; };
|
||||
D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -337,6 +343,8 @@
|
|||
D41355DA278E6D17005E5CBD /* McBopomofoLM.h */,
|
||||
D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */,
|
||||
D47F7DD1278C1263002F9DD7 /* UserOverrideModel.h */,
|
||||
D47D73AA27A6CAE600255A50 /* AssociatedPhrases.cpp */,
|
||||
D47D73AB27A6CAE600255A50 /* AssociatedPhrases.h */,
|
||||
);
|
||||
path = Engine;
|
||||
sourceTree = "<group>";
|
||||
|
@ -426,6 +434,7 @@
|
|||
6A38BBDD15FC115800A8A51F /* Data */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D47D73A727A6C84F00255A50 /* associated-phrases.cin */,
|
||||
6A38BBF615FC117A00A8A51F /* data.txt */,
|
||||
6AD7CBC715FE555000691B5B /* data-plain-bpmf.txt */,
|
||||
);
|
||||
|
@ -648,6 +657,7 @@
|
|||
6AE210B315FC63CC003659FE /* PlainBopomofo@2x.tiff in Resources */,
|
||||
6AD7CBC815FE555000691B5B /* data-plain-bpmf.txt in Resources */,
|
||||
6A187E2616004C5900466B2E /* MainMenu.xib in Resources */,
|
||||
D47D73A827A6C84F00255A50 /* associated-phrases.cin in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -671,6 +681,7 @@
|
|||
files = (
|
||||
D4E569E527A414CB00AC2CEF /* data.txt in Resources */,
|
||||
D4E569E427A414CB00AC2CEF /* data-plain-bpmf.txt in Resources */,
|
||||
D47D73A927A6C84F00255A50 /* associated-phrases.cin in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -712,6 +723,7 @@
|
|||
D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */,
|
||||
D456576E279E4F7B00DF6BC9 /* KeyHandlerInput.swift in Sources */,
|
||||
D47F7DCE278BFB57002F9DD7 /* PreferencesWindowController.swift in Sources */,
|
||||
D47D73AC27A6CAE600255A50 /* AssociatedPhrases.cpp in Sources */,
|
||||
D41355DB278E6D17005E5CBD /* McBopomofoLM.cpp in Sources */,
|
||||
D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */,
|
||||
6A0D4F4515FC0EB100ABF4B3 /* Mandarin.cpp in Sources */,
|
||||
|
|
|
@ -23,6 +23,18 @@
|
|||
|
||||
import Cocoa
|
||||
|
||||
@objc(VTCandidateKeyLabel)
|
||||
public class CandidateKeyLabel: NSObject {
|
||||
@objc public private(set) var key: String
|
||||
@objc public private(set) var displayedText: String
|
||||
|
||||
public init(key: String, displayedText: String) {
|
||||
self.key = key
|
||||
self.displayedText = displayedText
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
@objc(VTCandidateControllerDelegate)
|
||||
public protocol CandidateControllerDelegate: AnyObject {
|
||||
func candidateCountForController(_ controller: CandidateController) -> UInt
|
||||
|
@ -40,6 +52,7 @@ public class CandidateController: NSWindowController {
|
|||
@objc public var selectedCandidateIndex: UInt = UInt.max
|
||||
@objc public var visible: Bool = false {
|
||||
didSet {
|
||||
NSObject.cancelPreviousPerformRequests(withTarget: self)
|
||||
if visible {
|
||||
window?.perform(#selector(NSWindow.orderFront(_:)), with: self, afterDelay: 0.0)
|
||||
} else {
|
||||
|
@ -61,9 +74,12 @@ public class CandidateController: NSWindowController {
|
|||
}
|
||||
}
|
||||
|
||||
@objc public var keyLabels: [String] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
||||
@objc public var keyLabels: [CandidateKeyLabel] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"].map {
|
||||
CandidateKeyLabel(key: $0, displayedText: $0)
|
||||
}
|
||||
@objc public var keyLabelFont: NSFont = NSFont.systemFont(ofSize: 14)
|
||||
@objc public var candidateFont: NSFont = NSFont.systemFont(ofSize: 18)
|
||||
@objc public var tooltip: String = ""
|
||||
|
||||
@objc public func reloadData() {
|
||||
}
|
||||
|
|
|
@ -38,6 +38,23 @@ fileprivate class HorizontalCandidateView: NSView {
|
|||
private var elementWidths: [CGFloat] = []
|
||||
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 {
|
||||
true
|
||||
}
|
||||
|
@ -50,10 +67,12 @@ fileprivate class HorizontalCandidateView: NSView {
|
|||
result.width += CGFloat(elementWidths.count)
|
||||
result.height = keyLabelHeight + candidateTextHeight + 1.0
|
||||
}
|
||||
|
||||
result.height += tooltipSize.height
|
||||
result.width = max(tooltipSize.width, result.width)
|
||||
return result
|
||||
}
|
||||
|
||||
@objc(setKeyLabels:displayedCandidates:)
|
||||
func set(keyLabels labels: [String], displayedCandidates candidates: [String]) {
|
||||
let count = min(labels.count, candidates.count)
|
||||
keyLabels = Array(labels[0..<count])
|
||||
|
@ -64,14 +83,13 @@ fileprivate class HorizontalCandidateView: NSView {
|
|||
for index in 0..<count {
|
||||
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 cellWidth = max(keyLabelHeight * 2,
|
||||
let cellWidth = max(candidateTextHeight,
|
||||
max(labelRect.size.width, candidateRect.size.width)) + cellPadding;
|
||||
newWidths.append(cellWidth)
|
||||
}
|
||||
elementWidths = newWidths
|
||||
}
|
||||
|
||||
@objc(setKeyLabelFont:candidateFont:)
|
||||
func set(keyLabelFont labelFont: NSFont, candidateFont: NSFont) {
|
||||
let paraStyle = NSMutableParagraphStyle()
|
||||
paraStyle.setParagraphStyle(NSParagraphStyle.default)
|
||||
|
@ -108,13 +126,21 @@ fileprivate class HorizontalCandidateView: NSView {
|
|||
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
|
||||
for index in 0..<elementWidths.count {
|
||||
let currentWidth = elementWidths[index]
|
||||
let labelRect = NSRect(x: accuWidth, y: 0.0, width: currentWidth, height: keyLabelHeight)
|
||||
let candidateRect = NSRect(x: accuWidth, y: keyLabelHeight + 1.0, width: currentWidth, height: candidateTextHeight)
|
||||
let labelRect = NSRect(x: accuWidth, y: tooltipSize.height, width: currentWidth, height: keyLabelHeight)
|
||||
let candidateRect = NSRect(x: accuWidth, y: tooltipSize.height + keyLabelHeight + 1.0, width: currentWidth, height: candidateTextHeight)
|
||||
(index == highlightedIndex ? darkGray : lightGray).setFill()
|
||||
NSBezierPath.fill(labelRect)
|
||||
(keyLabels[index] as NSString).draw(in: labelRect, withAttributes: keyLabelAttrDict)
|
||||
|
@ -346,7 +372,8 @@ extension HorizontalCandidateController {
|
|||
let candidate = delegate.candidateController(self, candidateAtIndex: index)
|
||||
candidates.append(candidate)
|
||||
}
|
||||
candidateView.set(keyLabels: keyLabels, displayedCandidates: candidates)
|
||||
candidateView.set(keyLabels: keyLabels.map { $0.displayedText}, displayedCandidates: candidates)
|
||||
candidateView.toolTip = tooltip
|
||||
var newSize = candidateView.sizeForView
|
||||
var frameRect = candidateView.frame
|
||||
frameRect.size = newSize
|
||||
|
|
|
@ -85,6 +85,12 @@ private let kCandidateTextLeftMargin: CGFloat = 8.0
|
|||
private let kCandidateTextPaddingWithMandatedTableViewPadding: CGFloat = 18.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)
|
||||
public class VerticalCandidateController: CandidateController {
|
||||
|
@ -95,6 +101,8 @@ public class VerticalCandidateController: CandidateController {
|
|||
private var candidateTextPadding: CGFloat = kCandidateTextPadding
|
||||
private var candidateTextLeftMargin: CGFloat = kCandidateTextLeftMargin
|
||||
private var maxCandidateAttrStringWidth: CGFloat = 0
|
||||
private let tooltipPadding: CGFloat = 2.0
|
||||
private var tooltipView: NSTextField
|
||||
|
||||
public init() {
|
||||
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)
|
||||
panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1)
|
||||
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
|
||||
var stripRect = contentRect
|
||||
|
@ -400,6 +416,20 @@ extension VerticalCandidateController: NSTableViewDataSource, NSTableViewDelegat
|
|||
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 keyLabelFontSize = ceil(keyLabelFont.pointSize)
|
||||
let fontSize = max(candidateFontSize, keyLabelFontSize)
|
||||
|
@ -420,7 +450,8 @@ extension VerticalCandidateController: NSTableViewDataSource, NSTableViewDelegat
|
|||
}
|
||||
|
||||
keyLabelStripView.keyLabelFont = keyLabelFont
|
||||
keyLabelStripView.keyLabels = Array(keyLabels[0..<Int(keyLabelCount)])
|
||||
let keyLabels = keyLabels[0..<Int(keyLabelCount)].map { $0.displayedText }
|
||||
keyLabelStripView.keyLabels = keyLabels
|
||||
keyLabelStripView.labelOffsetY = (keyLabelFontSize >= candidateFontSize) ? 0.0 : floor((candidateFontSize - keyLabelFontSize) / 2.0)
|
||||
|
||||
let rowHeight = ceil(fontSize * 1.25)
|
||||
|
@ -438,8 +469,8 @@ extension VerticalCandidateController: NSTableViewDataSource, NSTableViewDelegat
|
|||
let rowSpacing = tableView.intercellSpacing.height
|
||||
let stripWidth = ceil(maxKeyLabelWidth * 1.20)
|
||||
let tableViewStartWidth = ceil(maxCandidateAttrStringWidth + scrollerWidth)
|
||||
let windowWidth = stripWidth + 1.0 + tableViewStartWidth
|
||||
let windowHeight = CGFloat(keyLabelCount) * (rowHeight + rowSpacing)
|
||||
let windowWidth = max(stripWidth + 1.0 + tableViewStartWidth, tooltipWidth)
|
||||
let windowHeight = CGFloat(keyLabelCount) * (rowHeight + rowSpacing) + tooltipHeight
|
||||
|
||||
var frameRect = self.window?.frame ?? NSRect.zero
|
||||
let topLeftPoint = NSMakePoint(frameRect.origin.x, frameRect.origin.y + frameRect.size.height)
|
||||
|
@ -447,8 +478,9 @@ extension VerticalCandidateController: NSTableViewDataSource, NSTableViewDelegat
|
|||
frameRect.size = NSMakeSize(windowWidth, windowHeight)
|
||||
frameRect.origin = NSMakePoint(topLeftPoint.x, topLeftPoint.y - frameRect.size.height)
|
||||
|
||||
keyLabelStripView.frame = NSRect(x: 0.0, y: 0.0, width: stripWidth, height: windowHeight)
|
||||
scrollView.frame = NSRect(x: stripWidth + 1.0, y: 0.0, width: tableViewStartWidth, height: windowHeight)
|
||||
keyLabelStripView.frame = NSRect(x: 0.0, y: 0, width: stripWidth, height: windowHeight - tooltipHeight)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -248,7 +248,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NonModalAlertWindowControlle
|
|||
}
|
||||
}
|
||||
|
||||
extension AppDelegate : FSEventStreamHelperDelegate {
|
||||
extension AppDelegate: FSEventStreamHelperDelegate {
|
||||
func helper(_ helper: FSEventStreamHelper, didReceive events: [FSEventStreamHelper.Event]) {
|
||||
DispatchQueue.main.async {
|
||||
LanguageModelManager.loadUserPhrases()
|
||||
|
|
|
@ -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_excludedPhrases.close();
|
||||
m_phraseReplacement.close();
|
||||
}
|
||||
|
||||
bool McBopomofoLM::isDataModelLoaded()
|
||||
{
|
||||
return m_languageModel.isLoaded();
|
||||
m_associatedPhrases.close();
|
||||
}
|
||||
|
||||
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,
|
||||
const char* excludedPhrasesDataPath)
|
||||
{
|
||||
|
@ -189,3 +203,13 @@ const vector<Unigram> McBopomofoLM::filterAndTransformUnigrams(const vector<Unig
|
|||
}
|
||||
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 "ParselessLM.h"
|
||||
#include "PhraseReplacementMap.h"
|
||||
#include "AssociatedPhrases.h"
|
||||
#include <unordered_set>
|
||||
|
||||
namespace McBopomofo {
|
||||
|
@ -61,11 +62,18 @@ public:
|
|||
McBopomofoLM();
|
||||
~McBopomofoLM();
|
||||
|
||||
/// Asks to load the primary language model a the given path.
|
||||
/// @param languageModelPath Thw path of the language model.
|
||||
/// Asks to load the primary language model at the given path.
|
||||
/// @param languageModelPath The path of the language model.
|
||||
void loadLanguageModel(const char* languageModelPath);
|
||||
/// If the data model is already loaded.
|
||||
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.
|
||||
/// @param userPhrasesPath The path of user 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.
|
||||
void setExternalConverter(std::function<string(string)> externalConverter);
|
||||
|
||||
const vector<std::string> associatedPhrasesForKey(const string& key);
|
||||
bool hasAssociatedPhrasesForKey(const string& key);
|
||||
|
||||
|
||||
protected:
|
||||
/// Filters and converts the input unigrams and return a new list of unigrams.
|
||||
///
|
||||
|
@ -112,6 +124,7 @@ protected:
|
|||
UserPhrasesLM m_userPhrases;
|
||||
UserPhrasesLM m_excludedPhrases;
|
||||
PhraseReplacementMap m_phraseReplacement;
|
||||
AssociatedPhrases m_associatedPhrases;
|
||||
bool m_phraseReplacementEnabled;
|
||||
bool m_externalConverterEnabled;
|
||||
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)
|
||||
{
|
||||
if (data) {
|
||||
|
|
|
@ -36,7 +36,8 @@ class UserPhrasesLM : public Formosa::Gramambular::LanguageModel
|
|||
public:
|
||||
UserPhrasesLM();
|
||||
~UserPhrasesLM();
|
||||
|
||||
|
||||
bool isLoaded();
|
||||
bool open(const char *path);
|
||||
void close();
|
||||
void dump();
|
||||
|
|
|
@ -76,6 +76,11 @@ class McBopomofoInputMethodController: IMKInputController {
|
|||
let inputMode = keyHandler.inputMode
|
||||
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 {
|
||||
let phaseReplacementItem = menu.addItem(withTitle: NSLocalizedString("Use Phrase Replacement", comment: ""), action: #selector(togglePhraseReplacement(_:)), keyEquivalent: "")
|
||||
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: ""))
|
||||
}
|
||||
|
||||
@objc func toggleAssociatedPhrasesEnabled(_ sender: Any?) {
|
||||
_ = Preferences.toggleAssociatedPhrasesEnabled()
|
||||
}
|
||||
|
||||
@objc func togglePhraseReplacement(_ sender: Any?) {
|
||||
let enabled = Preferences.togglePhraseReplacementEnabled()
|
||||
LanguageModelManager.phraseReplacementEnabled = enabled
|
||||
|
@ -276,6 +285,8 @@ extension McBopomofoInputMethodController {
|
|||
handle(state: newState, previous: previous, client: client)
|
||||
} else if let newState = newState as? InputState.ChoosingCandidate {
|
||||
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,
|
||||
// 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))
|
||||
if previous is InputState.ChoosingCandidate == false {
|
||||
show(candidateWindowWith: state, client: client)
|
||||
show(candidateWindowWith: state, client: client)
|
||||
}
|
||||
|
||||
private func handle(state: InputState.AssociatedPhrases, previous: InputState, client: Any?) {
|
||||
hideTooltip()
|
||||
guard let client = client as? IMKTextInput else {
|
||||
gCurrentCandidateController?.visible = false
|
||||
return
|
||||
}
|
||||
client.setMarkedText("", selectionRange: NSMakeRange(0, 0), replacementRange: NSMakeRange(NSNotFound, NSNotFound))
|
||||
show(candidateWindowWith: state, client: client)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -417,8 +436,17 @@ extension McBopomofoInputMethodController {
|
|||
|
||||
extension McBopomofoInputMethodController {
|
||||
|
||||
private func show(candidateWindowWith state: InputState.ChoosingCandidate, client: Any!) {
|
||||
if state.useVerticalMode {
|
||||
private func show(candidateWindowWith state: InputState, client: Any!) {
|
||||
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
|
||||
} else if Preferences.useHorizontalCandidateList {
|
||||
gCurrentCandidateController = McBopomofoInputMethodController.horizontalCandidateController
|
||||
|
@ -442,10 +470,11 @@ extension McBopomofoInputMethodController {
|
|||
|
||||
let candidateKeys = Preferences.candidateKeys
|
||||
let keyLabels = candidateKeys.count > 4 ? Array(candidateKeys) : Array(Preferences.defaultCandidateKeys)
|
||||
let keyLabelPrefix = state is InputState.AssociatedPhrases ? "⇧ " : ""
|
||||
gCurrentCandidateController?.keyLabels = keyLabels.map {
|
||||
CandidateKeyLabel(key: String($0), displayedText: keyLabelPrefix + String($0))
|
||||
}
|
||||
|
||||
gCurrentCandidateController?.keyLabels = Array(keyLabels.map {
|
||||
String($0)
|
||||
})
|
||||
gCurrentCandidateController?.delegate = self
|
||||
gCurrentCandidateController?.reloadData()
|
||||
currentCandidateClient = client
|
||||
|
@ -453,14 +482,18 @@ extension McBopomofoInputMethodController {
|
|||
gCurrentCandidateController?.visible = true
|
||||
|
||||
var lineHeightRect = NSMakeRect(0.0, 0.0, 16.0, 16.0)
|
||||
var cursor = state.cursorIndex
|
||||
if cursor == state.composingBuffer.count && cursor != 0 {
|
||||
cursor -= 1
|
||||
var cursor: UInt = 0
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
} else {
|
||||
gCurrentCandidateController?.set(windowTopLeftPoint: NSMakePoint(lineHeightRect.origin.x, lineHeightRect.origin.y - 4.0), bottomOutOfScreenAdjustmentHeight: lineHeightRect.size.height + 4.0)
|
||||
|
@ -513,6 +546,8 @@ extension McBopomofoInputMethodController: CandidateControllerDelegate {
|
|||
func candidateCountForController(_ controller: CandidateController) -> UInt {
|
||||
if let state = state as? InputState.ChoosingCandidate {
|
||||
return UInt(state.candidates.count)
|
||||
} else if let state = state as? InputState.AssociatedPhrases {
|
||||
return UInt(state.candidates.count)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
@ -520,28 +555,44 @@ extension McBopomofoInputMethodController: CandidateControllerDelegate {
|
|||
func candidateController(_ controller: CandidateController, candidateAtIndex index: UInt) -> String {
|
||||
if let state = state as? InputState.ChoosingCandidate {
|
||||
return state.candidates[Int(index)]
|
||||
} else if let state = state as? InputState.AssociatedPhrases {
|
||||
return state.candidates[Int(index)]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
if let state = state as? InputState.ChoosingCandidate {
|
||||
let selectedValue = state.candidates[Int(index)]
|
||||
keyHandler.fixNode(withValue: selectedValue)
|
||||
|
||||
if keyHandler.inputMode == .plainBopomofo {
|
||||
keyHandler.clear()
|
||||
handle(state: .Committing(poppedText: inputting.composingBuffer), client: currentCandidateClient)
|
||||
handle(state: .Empty(), client: currentDeferredClient)
|
||||
} else {
|
||||
handle(state: inputting, client: currentCandidateClient)
|
||||
guard let inputting = keyHandler.buildInputtingState() as? InputState.Inputting else {
|
||||
return
|
||||
}
|
||||
|
||||
if keyHandler.inputMode == .plainBopomofo {
|
||||
keyHandler.clear()
|
||||
let composingBuffer = inputting.composingBuffer
|
||||
handle(state: .Committing(poppedText: composingBuffer), client: currentCandidateClient)
|
||||
if Preferences.associatedPhrasesEnabled,
|
||||
let associatePhrases = keyHandler.buildAssociatePhraseState(withKey: composingBuffer, useVerticalMode: state.useVerticalMode) as? InputState.AssociatedPhrases {
|
||||
self.handle(state: associatePhrases, client: self.currentCandidateClient)
|
||||
} else {
|
||||
handle(state: .Empty(), client: currentDeferredClient)
|
||||
}
|
||||
} 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)
|
||||
if Preferences.associatedPhrasesEnabled,
|
||||
let associatePhrases = keyHandler.buildAssociatePhraseState(withKey: selectedValue, useVerticalMode: state.useVerticalMode) as? InputState.AssociatedPhrases {
|
||||
self.handle(state: associatePhrases, client: self.currentCandidateClient)
|
||||
} else {
|
||||
handle(state: .Empty(), client: currentDeferredClient)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -170,7 +170,7 @@ class InputState: NSObject {
|
|||
return String(format: NSLocalizedString("You are now selecting \"%@\". Press enter to add a new phrase.", comment: ""), text)
|
||||
}
|
||||
|
||||
@objc private(set) var readings: [String] = []
|
||||
@objc private(set) var readings: [String]
|
||||
|
||||
@objc init(composingBuffer: String, cursorIndex: UInt, markerIndex: UInt, readings: [String]) {
|
||||
self.markerIndex = markerIndex
|
||||
|
@ -226,8 +226,8 @@ class InputState: NSObject {
|
|||
/// Represents that the user is choosing in a candidates list.
|
||||
@objc (InputStateChoosingCandidate)
|
||||
class ChoosingCandidate: NotEmpty {
|
||||
@objc private(set) var candidates: [String] = []
|
||||
@objc private(set) var useVerticalMode: Bool = false
|
||||
@objc private(set) var candidates: [String]
|
||||
@objc private(set) var useVerticalMode: Bool
|
||||
|
||||
@objc init(composingBuffer: String, cursorIndex: UInt, candidates: [String], useVerticalMode: Bool) {
|
||||
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;
|
||||
|
||||
- (InputState *)buildInputtingState;
|
||||
- (nullable InputState *)buildAssociatePhraseStateWithKey:(NSString *)key useVerticalMode:(BOOL)useVerticalMode;
|
||||
|
||||
@property (strong, nonatomic) InputMode inputMode;
|
||||
@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
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -276,7 +278,17 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot
|
|||
|
||||
// MARK: Handle Candidates
|
||||
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
|
||||
|
@ -350,10 +362,23 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot
|
|||
InputStateChoosingCandidate *choosingCandidates = [self _buildCandidateState:inputting useVerticalMode:input.useVerticalMode];
|
||||
if (choosingCandidates.candidates.count == 1) {
|
||||
[self clear];
|
||||
InputStateCommitting *committing = [[InputStateCommitting alloc] initWithPoppedText:choosingCandidates.candidates.firstObject];
|
||||
NSString *text = choosingCandidates.candidates.firstObject;
|
||||
InputStateCommitting *committing = [[InputStateCommitting alloc] initWithPoppedText:text];
|
||||
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 {
|
||||
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
|
||||
stateCallback:(void (^)(InputState *))stateCallback
|
||||
candidateSelectionCallback:(void (^)(void))candidateSelectionCallback
|
||||
|
@ -845,10 +870,15 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot
|
|||
UniChar charCode = input.charCode;
|
||||
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 (_inputMode == InputModePlainBopomofo) {
|
||||
if ([state isKindOfClass: [InputStateAssociatedPhrases class]]) {
|
||||
[self clear];
|
||||
InputStateEmptyIgnoringPreviousState *empty = [[InputStateEmptyIgnoringPreviousState alloc] init];
|
||||
stateCallback(empty);
|
||||
}
|
||||
else if (_inputMode == InputModePlainBopomofo) {
|
||||
[self clear];
|
||||
InputStateEmptyIgnoringPreviousState *empty = [[InputStateEmptyIgnoringPreviousState alloc] init];
|
||||
stateCallback(empty);
|
||||
|
@ -860,6 +890,12 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot
|
|||
}
|
||||
|
||||
if (charCode == 13 || [input isEnter]) {
|
||||
if ([state isKindOfClass: [InputStateAssociatedPhrases class]]) {
|
||||
[self clear];
|
||||
InputStateEmptyIgnoringPreviousState *empty = [[InputStateEmptyIgnoringPreviousState alloc] init];
|
||||
stateCallback(empty);
|
||||
return YES;
|
||||
}
|
||||
[self.delegate keyHandler:self didSelectCandidateAtIndex:gCurrentCandidateController.selectedCandidateIndex candidateController:gCurrentCandidateController];
|
||||
return YES;
|
||||
}
|
||||
|
@ -975,27 +1011,51 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot
|
|||
return YES;
|
||||
}
|
||||
|
||||
if (([input isEnd] || input.emacsKey == McBopomofoEmacsKeyEnd) && [state.candidates count] > 0) {
|
||||
if (gCurrentCandidateController.selectedCandidateIndex == [state.candidates count] - 1) {
|
||||
NSArray *candidates;
|
||||
|
||||
if ([state isKindOfClass: [InputStateChoosingCandidate class]]) {
|
||||
candidates = [(InputStateChoosingCandidate *)state candidates];
|
||||
} else if ([state isKindOfClass: [InputStateAssociatedPhrases class]]) {
|
||||
candidates = [(InputStateAssociatedPhrases *)state candidates];
|
||||
}
|
||||
|
||||
if (!candidates) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (([input isEnd] || input.emacsKey == McBopomofoEmacsKeyEnd) && candidates.count > 0) {
|
||||
if (gCurrentCandidateController.selectedCandidateIndex == candidates.count - 1) {
|
||||
errorCallback();
|
||||
} else {
|
||||
gCurrentCandidateController.selectedCandidateIndex = [state.candidates count] - 1;
|
||||
gCurrentCandidateController.selectedCandidateIndex = candidates.count - 1;
|
||||
}
|
||||
|
||||
candidateSelectionCallback();
|
||||
return YES;
|
||||
}
|
||||
|
||||
if ([state isKindOfClass:[InputStateAssociatedPhrases class]]) {
|
||||
if (![input isShiftHold]) {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
NSInteger index = NSNotFound;
|
||||
NSString *match;
|
||||
if ([state isKindOfClass:[InputStateAssociatedPhrases class]]) {
|
||||
match = input.inputTextIgnoringModifiers;
|
||||
} else {
|
||||
match = inputText;
|
||||
}
|
||||
|
||||
for (NSUInteger j = 0, c = [gCurrentCandidateController.keyLabels count]; j < c; j++) {
|
||||
if ([inputText compare:[gCurrentCandidateController.keyLabels objectAtIndex:j] options:NSCaseInsensitiveSearch] == NSOrderedSame) {
|
||||
VTCandidateKeyLabel *label = gCurrentCandidateController.keyLabels[j];
|
||||
if ([match compare:label.key options:NSCaseInsensitiveSearch] == NSOrderedSame) {
|
||||
index = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[gCurrentCandidateController.keyLabels indexOfObject:inputText];
|
||||
|
||||
if (index != NSNotFound) {
|
||||
NSUInteger candidateIndex = [gCurrentCandidateController candidateIndexAtKeyLabelIndex:index];
|
||||
if (candidateIndex != NSUIntegerMax) {
|
||||
|
@ -1004,6 +1064,10 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot
|
|||
}
|
||||
}
|
||||
|
||||
if ([state isKindOfClass:[InputStateAssociatedPhrases class]]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (_inputMode == InputModePlainBopomofo) {
|
||||
string layout = [self _currentLayout];
|
||||
string punctuationNamePrefix = Preferences.halfWidthPunctuationEnabled ? string("_half_punctuation_") : string("_punctuation_");
|
||||
|
@ -1191,4 +1255,20 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot
|
|||
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
|
||||
|
|
|
@ -40,8 +40,9 @@ enum KeyCode: UInt16 {
|
|||
class KeyHandlerInput: NSObject {
|
||||
@objc private (set) var useVerticalMode: Bool
|
||||
@objc private (set) var inputText: String?
|
||||
@objc private (set) var inputTextIgnoringModifiers: String?
|
||||
@objc private (set) var charCode: UInt16
|
||||
private var keyCode: UInt16
|
||||
@objc private (set) var keyCode: UInt16
|
||||
private var flags: NSEvent.ModifierFlags
|
||||
private var cursorForwardKey: KeyCode
|
||||
private var cursorBackwardKey: KeyCode
|
||||
|
@ -50,8 +51,9 @@ class KeyHandlerInput: NSObject {
|
|||
private var verticalModeOnlyChooseCandidateKey: KeyCode
|
||||
@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.inputTextIgnoringModifiers = inputTextIgnoringModifiers ?? inputText
|
||||
self.keyCode = keyCode
|
||||
self.charCode = charCode
|
||||
self.flags = flags
|
||||
|
@ -67,6 +69,7 @@ class KeyHandlerInput: NSObject {
|
|||
|
||||
@objc init(event: NSEvent, isVerticalMode: Bool) {
|
||||
inputText = event.characters
|
||||
inputTextIgnoringModifiers = event.charactersIgnoringModifiers
|
||||
keyCode = event.keyCode
|
||||
flags = event.modifierFlags
|
||||
useVerticalMode = isVerticalMode
|
||||
|
|
|
@ -53,6 +53,13 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomo
|
|||
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
|
||||
{
|
||||
if (!gLanguageModelMcBopomofo.isDataModelLoaded()) {
|
||||
|
@ -61,6 +68,9 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomo
|
|||
if (!gLanguageModelPlainBopomofo.isDataModelLoaded()) {
|
||||
LTLoadLanguageModelFile(@"data-plain-bpmf", gLanguageModelPlainBopomofo);
|
||||
}
|
||||
if (!gLanguageModelPlainBopomofo.isAssociatedPhrasesLoaded()) {
|
||||
LTLoadAssociatedPhrases(gLanguageModelPlainBopomofo);
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)loadDataModel:(InputMode)mode
|
||||
|
@ -70,10 +80,14 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomo
|
|||
LTLoadLanguageModelFile(@"data", gLanguageModelMcBopomofo);
|
||||
}
|
||||
}
|
||||
|
||||
if ([mode isEqualToString:InputModePlainBopomofo]) {
|
||||
if (!gLanguageModelPlainBopomofo.isDataModelLoaded()) {
|
||||
LTLoadLanguageModelFile(@"data-plain-bpmf", gLanguageModelPlainBopomofo);
|
||||
}
|
||||
if (!gLanguageModelPlainBopomofo.isAssociatedPhrasesLoaded()) {
|
||||
LTLoadAssociatedPhrases(gLanguageModelPlainBopomofo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,8 @@ private let kCandidateKeys = "CandidateKeys"
|
|||
private let kPhraseReplacementEnabledKey = "PhraseReplacementEnabled"
|
||||
private let kChineseConversionEngineKey = "ChineseConversionEngine"
|
||||
private let kChineseConversionStyle = "ChineseConversionStyle"
|
||||
private let kAssociatedPhrasesEnabledKey = "AssociatedPhrasesEnabled"
|
||||
//private let kAssociatedPhrasesKeys = "AssociatedPhrasesKeys"
|
||||
|
||||
private let kDefaultCandidateListTextSize: CGFloat = 16
|
||||
private let kMinCandidateListTextSize: CGFloat = 12
|
||||
|
@ -57,6 +59,7 @@ private let kMinComposingBufferSize = 4
|
|||
private let kMaxComposingBufferSize = 20
|
||||
|
||||
private let kDefaultKeys = "123456789"
|
||||
private let kDefaultAssociatedPhrasesKeys = "!@#$%^&*("
|
||||
|
||||
// MARK: Property wrappers
|
||||
|
||||
|
@ -215,6 +218,8 @@ class Preferences: NSObject {
|
|||
defaults.removeObject(forKey: kPhraseReplacementEnabledKey)
|
||||
defaults.removeObject(forKey: kChineseConversionEngineKey)
|
||||
defaults.removeObject(forKey: kChineseConversionStyle)
|
||||
defaults.removeObject(forKey: kAssociatedPhrasesEnabledKey)
|
||||
// defaults.removeObject(forKey: kAssociatedPhrasesKeys)
|
||||
}
|
||||
|
||||
@UserDefault(key: kKeyboardLayoutPreferenceKey, defaultValue: 0)
|
||||
|
@ -365,4 +370,12 @@ class Preferences: NSObject {
|
|||
ChineseConversionStyle(rawValue: chineseConversionStyle)?.name
|
||||
}
|
||||
|
||||
@UserDefault(key: kAssociatedPhrasesEnabledKey, defaultValue: false)
|
||||
@objc static var associatedPhrasesEnabled: Bool
|
||||
|
||||
@objc static func toggleAssociatedPhrasesEnabled() -> Bool {
|
||||
associatedPhrasesEnabled = !associatedPhrasesEnabled
|
||||
return associatedPhrasesEnabled
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -133,7 +133,9 @@ import Carbon
|
|||
}
|
||||
|
||||
@IBAction func changeSelectionKeyAction(_ sender: Any) {
|
||||
guard let keys = (sender as AnyObject).stringValue?.trimmingCharacters(in: .whitespacesAndNewlines) else {
|
||||
guard let keys = (sender as AnyObject).stringValue?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.lowercased() else {
|
||||
return
|
||||
}
|
||||
do {
|
||||
|
|
|
@ -98,3 +98,5 @@
|
|||
"Half-width punctuation on" = "Half-width punctuation on";
|
||||
|
||||
"Half-width punctuation off" = "Half-width punctuation off";
|
||||
|
||||
"Associated Phrases" = "Associated Phrases";
|
||||
|
|
|
@ -98,3 +98,5 @@
|
|||
"Half-width punctuation on" = "已經切換到半型標點模式";
|
||||
|
||||
"Half-width punctuation off" = "已經切回到全型標點模式";
|
||||
|
||||
"Associated Phrases" = "聯想詞";
|
||||
|
|
Loading…
Reference in New Issue