Skip to content

Commit

Permalink
Merge pull request #12 from mtgto/private-mode
Browse files Browse the repository at this point in the history
マイ辞書に一時的に保存しなくするプライベートモードを追加
  • Loading branch information
mtgto authored Aug 15, 2023
2 parents 3d50924 + 9448aad commit 9c8f95d
Show file tree
Hide file tree
Showing 24 changed files with 259 additions and 37 deletions.
18 changes: 18 additions & 0 deletions macSKK/Assets.xcassets/icon-direct-locked.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"images" : [
{
"filename" : "icon-direct-locked.png",
"idiom" : "mac",
"scale" : "1x"
},
{
"filename" : "icon-direct-locked@2x.png",
"idiom" : "mac",
"scale" : "2x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions macSKK/Assets.xcassets/icon-eisu-locked.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"images" : [
{
"filename" : "icon-eisu-locked.png",
"idiom" : "mac",
"scale" : "1x"
},
{
"filename" : "icon-eisu-locked@2x.png",
"idiom" : "mac",
"scale" : "2x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions macSKK/Assets.xcassets/icon-hankaku-locked.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"images" : [
{
"filename" : "icon-hankaku-locked.png",
"idiom" : "mac",
"scale" : "1x"
},
{
"filename" : "icon-hankaku-locked@2x.png",
"idiom" : "mac",
"scale" : "2x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions macSKK/Assets.xcassets/icon-hiragana-locked.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"images" : [
{
"filename" : "icon-hiragana-locked.png",
"idiom" : "mac",
"scale" : "1x"
},
{
"filename" : "icon-hiragana-locked@2x.png",
"idiom" : "mac",
"scale" : "2x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions macSKK/Assets.xcassets/icon-katakana-locked.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"images" : [
{
"filename" : "icon-katakana-locked.png",
"idiom" : "mac",
"scale" : "1x"
},
{
"filename" : "icon-katakana-locked@2x.png",
"idiom" : "mac",
"scale" : "2x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 25 additions & 17 deletions macSKK/InputController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import InputMethodKit
@objc(InputController)
class InputController: IMKInputController {
private let stateMachine = StateMachine()
private let preferenceMenu = NSMenu()
private var cancellables: Set<AnyCancellable> = []
private static let notFoundRange = NSRange(location: NSNotFound, length: NSNotFound)
private let inputModePanel = InputModePanel()
Expand All @@ -19,19 +18,6 @@ class InputController: IMKInputController {
// AppleのAPIドキュメントにはメインスレッドであるとは書かれてないけど、まあ大丈夫じゃないかな
@MainActor
override init!(server: IMKServer!, delegate: Any!, client inputClient: Any!) {
preferenceMenu.addItem(
withTitle: NSLocalizedString("MenuItemPreference", comment: "Preferences…"),
action: #selector(showSettings), keyEquivalent: "")
preferenceMenu.addItem(
withTitle: NSLocalizedString("MenuItemSaveDict", comment: "Save User Dictionary"),
action: #selector(saveDict), keyEquivalent: "")
#if DEBUG
// デバッグ用
preferenceMenu.addItem(
withTitle: "Show Panel",
action: #selector(showPanel), keyEquivalent: "")
#endif

super.init(server: server, delegate: delegate, client: inputClient)

guard let textInput = inputClient as? IMKTextInput else {
Expand Down Expand Up @@ -66,7 +52,7 @@ class InputController: IMKInputController {
attributedText, selectionRange: cursorRange, replacementRange: Self.notFoundRange)
case .modeChanged(let inputMode, let cursorPosition):
textInput.selectMode(inputMode.rawValue)
self.inputModePanel.show(at: cursorPosition.origin, mode: inputMode)
self.inputModePanel.show(at: cursorPosition.origin, mode: inputMode, privateMode: self.stateMachine.privateMode)
}
}.store(in: &cancellables)
stateMachine.candidateEvent.sink { candidates in
Expand Down Expand Up @@ -117,6 +103,24 @@ class InputController: IMKInputController {
}

override func menu() -> NSMenu! {
let preferenceMenu = NSMenu()
preferenceMenu.addItem(
withTitle: NSLocalizedString("MenuItemPreference", comment: "Preferences…"),
action: #selector(showSettings), keyEquivalent: "")
preferenceMenu.addItem(
withTitle: NSLocalizedString("MenuItemSaveDict", comment: "Save User Dictionary"),
action: #selector(saveDict), keyEquivalent: "")
let privateModeItem = NSMenuItem(title: NSLocalizedString("MenuPrivateMode", comment: "Private mode"),
action: #selector(togglePrivateMode),
keyEquivalent: "")
privateModeItem.state = stateMachine.privateMode ? .on : .off
preferenceMenu.addItem(privateModeItem)
#if DEBUG
// デバッグ用
preferenceMenu.addItem(
withTitle: "Show Panel",
action: #selector(showPanel), keyEquivalent: "")
#endif
return preferenceMenu
}

Expand Down Expand Up @@ -145,7 +149,7 @@ class InputController: IMKInputController {
// カーソル位置あたりを取得する
var cursorPosition: NSRect = .zero
_ = textInput.attributes(forCharacterIndex: 0, lineHeightRectangle: &cursorPosition)
inputModePanel.show(at: cursorPosition.origin, mode: inputMode)
inputModePanel.show(at: cursorPosition.origin, mode: inputMode, privateMode: stateMachine.privateMode)
}

@objc func showSettings() {
Expand All @@ -166,9 +170,13 @@ class InputController: IMKInputController {
}
}

@objc func togglePrivateMode() {
stateMachine.togglePrivateMode()
}

@objc func showPanel() {
let point = NSPoint(x: 100, y: 500)
self.inputModePanel.show(at: point, mode: .hiragana)
self.inputModePanel.show(at: point, mode: .hiragana, privateMode: stateMachine.privateMode)
}

// MARK: -
Expand Down
12 changes: 6 additions & 6 deletions macSKK/InputModePanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,23 @@ class InputModePanel: NSPanel {
setContentSize(imageSize)
}

func show(at point: NSPoint, mode: InputMode) {
func show(at point: NSPoint, mode: InputMode, privateMode: Bool) {
// 画像の高さ分だけ下にずらす
let origin = NSPoint(x: point.x, y: point.y - imageSize.height)
let rect = NSRect(origin: origin, size: imageSize)
setFrame(rect, display: true)
level = .floating
switch mode {
case .hiragana:
imageView.image = NSImage(named: "icon-hiragana")
imageView.image = NSImage(named: privateMode ? "icon-hiragana-locked" : "icon-hiragana")
case .katakana:
imageView.image = NSImage(named: "icon-katakana")
imageView.image = NSImage(named: privateMode ? "icon-katakana-locked" : "icon-katakana")
case .hankaku:
imageView.image = NSImage(named: "icon-hankaku")
imageView.image = NSImage(named: privateMode ? "icon-hankaku-locked" : "icon-hankaku")
case .eisu:
imageView.image = NSImage(named: "icon-eisu")
imageView.image = NSImage(named: privateMode ? "icon-eisu-locked" : "icon-eisu")
case .direct:
imageView.image = NSImage(named: "icon-direct")
imageView.image = NSImage(named: privateMode ? "icon-direct-locked" : "icon-direct")
}

alphaValue = 1.0
Expand Down
11 changes: 10 additions & 1 deletion macSKK/StateMachine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ enum InputMethodEvent: Equatable {

class StateMachine {
private(set) var state: IMEState
/// ユーザー辞書に保存しないプライベートモードかどうか
private(set) var privateMode: Bool
let inputMethodEvent: AnyPublisher<InputMethodEvent, Never>
private let inputMethodEventSubject = PassthroughSubject<InputMethodEvent, Never>()
let candidateEvent: AnyPublisher<Candidates?, Never>
Expand All @@ -30,10 +32,11 @@ class StateMachine {
/// 変換候補パネルに一度に表示する変換候補の数
let displayCandidateCount = 9

init(initialState: IMEState = IMEState()) {
init(initialState: IMEState = IMEState(), privateMode: Bool = false) {
state = initialState
inputMethodEvent = inputMethodEventSubject.eraseToAnyPublisher()
candidateEvent = candidateEventSubject.removeDuplicates().eraseToAnyPublisher()
self.privateMode = privateMode
}

func handle(_ action: Action) -> Bool {
Expand Down Expand Up @@ -832,6 +835,12 @@ class StateMachine {
}
}

/// プライベートモードの有効無効を反転します。
func togglePrivateMode() {
privateMode = !privateMode
dictionary.privateMode = privateMode
}

private func addFixedText(_ text: String) {
if let specialState = state.specialState {
// state.markedTextを更新してinputMethodEventSubjectにstate.displayText()をsendする
Expand Down
78 changes: 67 additions & 11 deletions macSKK/UserDict.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import Combine
import Foundation

/// ユーザー辞書。マイ辞書 (単語登録対象。ファイル名固定) とファイル辞書 をまとめて参照することができる。
///
/// TODO: ファイル辞書にしかない単語を削除しようとしたときにどうやってそれを記録するか。NG登録?
class UserDict: DictProtocol {
static let userDictFilename = "skk-jisyo.utf8"
let dictionariesDirectoryURL: URL
Expand All @@ -12,7 +15,21 @@ class UserDict: DictProtocol {
let source: DispatchSourceFileSystemObject
/// 有効になっている辞書
private(set) var dicts: [DictProtocol]
var privateMode: Bool = false {
didSet {
// プライベートモードを解除したときにそれまでのエントリを削除する
if !privateMode {
logger.log("プライベートモードが解除されました")
privateUserDictEntries = [:]
}
}
}
/// 非プライベートモードのユーザー辞書。変換や単語登録すると更新されマイ辞書ファイルに永続化されます。
var userDictEntries: [String: [Word]] = [:]
/// プライベートモードのユーザー辞書。プライベートモードが有効な時に変換や単語登録するとuserDictEntriesとは別に更新されます。
/// マイ辞書ファイルには永続化されません。
/// プライベートモード時に変換・登録された単語だけ登録されるので、このあと非プライベートモードに遷移するとリセットされます。
private(set) var privateUserDictEntries: [String: [Word]] = [:]
private let savePublisher = PassthroughSubject<Void, Never>()
private var cancellables: Set<AnyCancellable> = []

Expand Down Expand Up @@ -78,6 +95,14 @@ class UserDict: DictProtocol {
// MARK: DictProtocol
func refer(_ word: String) -> [Word] {
var result = userDictEntries[word] ?? []
if privateMode {
let founds = privateUserDictEntries[word] ?? []
founds.forEach { found in
if !result.contains(found) {
result.append(found)
}
}
}
dicts.forEach { dict in
dict.refer(word).forEach { found in
if !result.contains(found) {
Expand All @@ -88,34 +113,65 @@ class UserDict: DictProtocol {
return result
}

/// ユーザー辞書にエントリを追加する
/// ユーザー辞書にエントリを追加する。
///
/// プライベートモード時にはメモリ上に記録はされるが、通常モード時とは分けて記録しているため
/// プライベートモード時に追加されたエントリはマイ辞書に永続化されないといった違いがある。
///
/// - Parameters:
/// - yomi: SKK辞書の見出し。複数のひらがな、もしくは複数のひらがな + ローマ字からなる文字列
/// - word: SKK辞書の変換候補。
func add(yomi: String, word: Word) {
if var words = userDictEntries[yomi] {
var entries: [String: [Word]] = privateMode ? privateUserDictEntries : userDictEntries
if var words = entries[yomi] {
let index = words.firstIndex { $0.word == word.word }
if let index {
words.remove(at: index)
}
userDictEntries[yomi] = [word] + words
entries[yomi] = [word] + words
} else {
userDictEntries[yomi] = [word]
entries[yomi] = [word]
}
if privateMode {
privateUserDictEntries = entries
} else {
userDictEntries = entries
savePublisher.send(())
}
savePublisher.send(())
}

/// ユーザー辞書からエントリを削除する
/// ユーザー辞書からエントリを削除する。
///
/// プライベートモードが有効なときの仕様はあんまり自信がないが、ひとまず次のように定義します。
/// - 非プライベート時
/// - 非プライベートモード用の辞書からのみエントリを削除する
/// - もしプライベートモード用の辞書にエントリがあっても削除しない
/// - ファイル形式の辞書にだけエントリがあった場合はなにもしない
/// - プライベートモード時
/// - プライベートモード用の辞書からのみエントリを削除する
/// - もし非プライベートモード用の辞書にエントリがあっても削除しない
/// - ファイル形式の辞書にだけエントリがあった場合はなにもしない
///
/// - Parameters:
/// - yomi: SKK辞書の見出し。複数のひらがな、もしくは複数のひらがな + ローマ字からなる文字列
/// - word: SKK辞書の変換候補。
/// - Returns: エントリを削除できたかどうか
func delete(yomi: String, word: Word) -> Bool {
if var entries = userDictEntries[yomi] {
if let index = entries.firstIndex(of: word) {
entries.remove(at: index)
userDictEntries[yomi] = entries
return true
if privateMode {
if var entries = privateUserDictEntries[yomi] {
if let index = entries.firstIndex(of: word) {
entries.remove(at: index)
privateUserDictEntries[yomi] = entries
return true
}
}
} else {
if var entries = userDictEntries[yomi] {
if let index = entries.firstIndex(of: word) {
entries.remove(at: index)
userDictEntries[yomi] = entries
return true
}
}
}
return false
Expand Down
1 change: 1 addition & 0 deletions macSKK/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"MenuItemPreference" = "Preferences…";
"MenuItemSaveDict" = "Save User Dictionary";
"MenuPrivateMode" = "Private mode";
"SettingsNameSoftwareUpdate" = "Software Update";
"SettingsNameDictionaries" = "Dictionaries";
"SettingsNameKeyEvent" = "Key Event";
Expand Down
1 change: 1 addition & 0 deletions macSKK/ja.lproj/Localizable.strings
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"MenuItemPreference" = "設定…";
"MenuItemSaveDict" = "ユーザー辞書を今すぐ保存";
"MenuPrivateMode" = "プライベートモード";
"SettingsNameSoftwareUpdate" = "ソフトウェアアップデート";
"SettingsNameDictionaries" = "辞書";
"SettingsNameKeyEvent" = "キーイベント";
Expand Down
Loading

0 comments on commit 9c8f95d

Please sign in to comment.