Implements the associated phrases function.
Since we use states manage the input flow in McBopomofo, implementing this function becomes easy. What I did is to create a new state, Associated Phrases state, and let the key handler to emit such a state just after emitting a Committing state. When the input method controller is under Associated Phrase state, it shows the candidate window with a tooltip, and only accept candidate keys with the shift key. The key handler uses the characters without modifiers in an NSEvent object to find if there is any matching candidate label, so I added a new member "inputTextIgnoringModifiers" to KeyHandlerInput. I use KeyValueBlobReader to read the associated phrases. I use the cin file from OpenVanilla project but removed the head and tail of the file to pass KeyValueBlobReader's validation.
This commit is contained in:
parent
2dd398f6ca
commit
2ebc789030
|
@ -52,6 +52,9 @@
|
|||
D47B92C027972AD100458394 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47B92BF27972AC800458394 /* main.swift */; };
|
||||
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 */,
|
||||
|
|
|
@ -40,6 +40,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 {
|
||||
|
@ -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 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,6 +67,9 @@ 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
|
||||
}
|
||||
|
||||
|
@ -64,7 +84,7 @@ 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)
|
||||
}
|
||||
|
@ -108,13 +128,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)
|
||||
|
@ -347,6 +375,7 @@ extension HorizontalCandidateController {
|
|||
candidates.append(candidate)
|
||||
}
|
||||
candidateView.set(keyLabels: keyLabels, 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)
|
||||
|
@ -438,8 +468,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 +477,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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,18 @@ 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
|
||||
|
@ -426,6 +455,12 @@ extension McBopomofoInputMethodController {
|
|||
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)
|
||||
let textSize = Preferences.candidateListTextSize
|
||||
let keyLabelSize = max(textSize / 2, kMinKeyLabelSize)
|
||||
|
@ -453,14 +488,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 +552,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 +561,39 @@ 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)
|
||||
guard let inputting = keyHandler.buildInputtingState() as? InputState.Inputting else {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
} else {
|
||||
handle(state: inputting, client: currentCandidateClient)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -170,7 +170,7 @@ class InputState: NSObject {
|
|||
return String(format: NSLocalizedString("You are now selecting \"%@\". Press enter to add a new phrase.", comment: ""), text)
|
||||
}
|
||||
|
||||
@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,9 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot
|
|||
}
|
||||
|
||||
if (charCode == 13 || [input isEnter]) {
|
||||
if ([state isKindOfClass: [InputStateAssociatedPhrases class]] && ![input isShiftHold]) {
|
||||
return NO;
|
||||
}
|
||||
[self.delegate keyHandler:self didSelectCandidateAtIndex:gCurrentCandidateController.selectedCandidateIndex candidateController:gCurrentCandidateController];
|
||||
return YES;
|
||||
}
|
||||
|
@ -975,27 +1008,47 @@ 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];
|
||||
}
|
||||
|
||||
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();
|
||||
} 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 = inputText;
|
||||
|
||||
if ([state isKindOfClass: [InputStateAssociatedPhrases class]]) {
|
||||
match = input.inputTextIgnoringModifiers;
|
||||
}
|
||||
|
||||
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;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[gCurrentCandidateController.keyLabels indexOfObject:inputText];
|
||||
|
||||
if (index != NSNotFound) {
|
||||
NSUInteger candidateIndex = [gCurrentCandidateController candidateIndexAtKeyLabelIndex:index];
|
||||
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) {
|
||||
string layout = [self _currentLayout];
|
||||
string punctuationNamePrefix = Preferences.halfWidthPunctuationEnabled ? string("_half_punctuation_") : string("_punctuation_");
|
||||
|
@ -1191,4 +1248,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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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