Merge pull request #263 from zonble/dev/associated_phrases

Implements the associated phrases function.
This commit is contained in:
Weizhong Yang a.k.a zonble 2022-01-31 14:19:46 +08:00 committed by GitHub
commit beaa6f5404
21 changed files with 579 additions and 68 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,127 @@
//
// AssociatedPhrases.cpp
//
// Copyright (c) 2017 The McBopomofo Project.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
#include "AssociatedPhrases.h"
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <fstream>
#include <unistd.h>
#include "KeyValueBlobReader.h"
namespace McBopomofo {
AssociatedPhrases::AssociatedPhrases()
: fd(-1)
, data(0)
, length(0)
{
}
AssociatedPhrases::~AssociatedPhrases()
{
if (data) {
close();
}
}
const bool AssociatedPhrases::isLoaded()
{
if (data) {
return true;
}
return false;
}
bool AssociatedPhrases::open(const char *path)
{
if (data) {
return false;
}
fd = ::open(path, O_RDONLY);
if (fd == -1) {
printf("open:: file not exist");
return false;
}
struct stat sb;
if (fstat(fd, &sb) == -1) {
printf("open:: cannot open file");
return false;
}
length = (size_t)sb.st_size;
data = mmap(NULL, length, PROT_READ, MAP_SHARED, fd, 0);
if (!data) {
::close(fd);
return false;
}
KeyValueBlobReader reader(static_cast<char*>(data), length);
KeyValueBlobReader::KeyValue keyValue;
KeyValueBlobReader::State state;
while ((state = reader.Next(&keyValue)) == KeyValueBlobReader::State::HAS_PAIR) {
keyRowMap[keyValue.key].emplace_back(keyValue.key, keyValue.value);
}
return true;
}
void AssociatedPhrases::close()
{
if (data) {
munmap(data, length);
::close(fd);
data = 0;
}
keyRowMap.clear();
}
const std::vector<std::string> AssociatedPhrases::valuesForKey(const std::string& key)
{
std::vector<std::string> v;
auto iter = keyRowMap.find(key);
if (iter != keyRowMap.end()) {
const std::vector<Row>& rows = iter->second;
for (const auto& row : rows) {
std::string_view value = row.value;
v.push_back({value.data(), value.size()});
}
}
return v;
}
const bool AssociatedPhrases::hasValuesForKey(const std::string& key)
{
return keyRowMap.find(key) != keyRowMap.end();
}
}; // namespace McBopomofo

View File

@ -0,0 +1,66 @@
//
// AssociatedPhrases.h
//
// Copyright (c) 2017 The McBopomofo Project.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
#ifndef ASSOCIATEDPHRASES_H
#define ASSOCIATEDPHRASES_H
#include <string>
#include <map>
#include <iostream>
#include <vector>
namespace McBopomofo {
class AssociatedPhrases
{
public:
AssociatedPhrases();
~AssociatedPhrases();
const bool isLoaded();
bool open(const char *path);
void close();
const std::vector<std::string> valuesForKey(const std::string& key);
const bool hasValuesForKey(const std::string& key);
protected:
struct Row {
Row(std::string_view& k, std::string_view& v) : key(k), value(v) {}
std::string_view key;
std::string_view value;
};
std::map<std::string_view, std::vector<Row>> keyRowMap;
int fd;
void *data;
size_t length;
};
}
#endif /* AssociatedPhrases_hpp */

View File

@ -37,11 +37,7 @@ McBopomofoLM::~McBopomofoLM()
m_userPhrases.close();
m_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);
}

View File

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

View File

@ -47,6 +47,14 @@ UserPhrasesLM::~UserPhrasesLM()
}
}
bool UserPhrasesLM::isLoaded()
{
if (data) {
return true;
}
return false;
}
bool UserPhrasesLM::open(const char *path)
{
if (data) {

View File

@ -36,7 +36,8 @@ class UserPhrasesLM : public Formosa::Gramambular::LanguageModel
public:
UserPhrasesLM();
~UserPhrasesLM();
bool isLoaded();
bool open(const char *path);
void close();
void dump();

View File

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

View File

@ -170,7 +170,7 @@ class InputState: NSObject {
return String(format: NSLocalizedString("You are now selecting \"%@\". Press enter to add a new phrase.", comment: ""), text)
}
@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)>"
}
}
}

View File

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

View File

@ -232,7 +232,9 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot
// if the composing buffer is empty and there's no reading, and there is some function key combination, we ignore it
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

View File

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

View File

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

View File

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

View File

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

View File

@ -98,3 +98,5 @@
"Half-width punctuation on" = "Half-width punctuation on";
"Half-width punctuation off" = "Half-width punctuation off";
"Associated Phrases" = "Associated Phrases";

View File

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