Skip to content

Commit

Permalink
Merge pull request #135 from mtgto/workaround-settings
Browse files Browse the repository at this point in the history
空文字挿入の互換性を入力メニューから設定できる
  • Loading branch information
mtgto authored Mar 10, 2024
2 parents 008a38f + b65cd0b commit 60812e4
Show file tree
Hide file tree
Showing 11 changed files with 273 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ macSKKが入力メソッドとして選択されているときに入力メニ
| selectedInputSource | String | キー配列 (KeyLayout) のID |
| showAnnotation | Boolean | 注釈を変換候補のそばに表示するか |
| inlineCandidateCount | Number | インラインで表示する変換候補の数 |
| workarounds | Array | 互換性設定がされているアプリケーション |

## 機能

Expand Down
8 changes: 8 additions & 0 deletions macSKK.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
CEC376E82965199500D9C432 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC376E72965199500D9C432 /* SettingsView.swift */; };
CEC376EB2965211200D9C432 /* KeyEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC376EA2965211200D9C432 /* KeyEventView.swift */; };
CED0984D2B779E4700F2E844 /* UNNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0984C2B779E4700F2E844 /* UNNotifier.swift */; };
CED098512B95F32600F2E844 /* WorkaroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED098502B95F32600F2E844 /* WorkaroundView.swift */; };
CED7CA2C2A8394F7004EF988 /* FetchUpdateServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED7CA2B2A8394F7004EF988 /* FetchUpdateServiceProtocol.swift */; };
CED7CA2E2A8394F7004EF988 /* FetchUpdateService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED7CA2D2A8394F7004EF988 /* FetchUpdateService.swift */; };
CED7CA302A8394F7004EF988 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED7CA2F2A8394F7004EF988 /* main.swift */; };
Expand All @@ -91,6 +92,7 @@
CEF08257296D8CBD00646366 /* CandidatesPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF08256296D8CBD00646366 /* CandidatesPanel.swift */; };
CEF0825B296D8FF000646366 /* CandidatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0825A296D8FF000646366 /* CandidatesView.swift */; };
CEF0825D296DB58600646366 /* CandidatesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0825C296DB58600646366 /* CandidatesViewModel.swift */; };
CEF3D86C2B9C022900BD1D3A /* WorkaroundApplicationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF3D86B2B9C022900BD1D3A /* WorkaroundApplicationView.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -197,6 +199,7 @@
CEC376E72965199500D9C432 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
CEC376EA2965211200D9C432 /* KeyEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyEventView.swift; sourceTree = "<group>"; };
CED0984C2B779E4700F2E844 /* UNNotifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotifier.swift; sourceTree = "<group>"; };
CED098502B95F32600F2E844 /* WorkaroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkaroundView.swift; sourceTree = "<group>"; };
CED7CA292A8394F7004EF988 /* FetchUpdateService.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = FetchUpdateService.xpc; sourceTree = BUILT_PRODUCTS_DIR; };
CED7CA2B2A8394F7004EF988 /* FetchUpdateServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchUpdateServiceProtocol.swift; sourceTree = "<group>"; };
CED7CA2D2A8394F7004EF988 /* FetchUpdateService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchUpdateService.swift; sourceTree = "<group>"; };
Expand All @@ -216,6 +219,7 @@
CEF08256296D8CBD00646366 /* CandidatesPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CandidatesPanel.swift; sourceTree = "<group>"; };
CEF0825A296D8FF000646366 /* CandidatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CandidatesView.swift; sourceTree = "<group>"; };
CEF0825C296DB58600646366 /* CandidatesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CandidatesViewModel.swift; sourceTree = "<group>"; };
CEF3D86B2B9C022900BD1D3A /* WorkaroundApplicationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkaroundApplicationView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -382,6 +386,8 @@
CE6DBACC2A864A3B00F5A227 /* DictionariesView.swift */,
CEB141712A87D16C005E7252 /* DictionaryView.swift */,
CE97887A2A9B93EB00F9B196 /* DirectModeView.swift */,
CED098502B95F32600F2E844 /* WorkaroundView.swift */,
CEF3D86B2B9C022900BD1D3A /* WorkaroundApplicationView.swift */,
CEE3717429653112000DB2C3 /* SoftwareUpdateView.swift */,
CE496C8B2B43968A001C623C /* LogView.swift */,
CEC376EA2965211200D9C432 /* KeyEventView.swift */,
Expand Down Expand Up @@ -609,11 +615,13 @@
CED0984D2B779E4700F2E844 /* UNNotifier.swift in Sources */,
CE7F9AD92AADEBF9001B1877 /* AppDelegate.swift in Sources */,
CEF0825B296D8FF000646366 /* CandidatesView.swift in Sources */,
CED098512B95F32600F2E844 /* WorkaroundView.swift in Sources */,
CE84A3E0295717CB009394C4 /* StateMachine.swift in Sources */,
CED7CA3C2A839603004EF988 /* UpdateChecker.swift in Sources */,
CE5ECF362957034B00E7BE7D /* macSKKApp.swift in Sources */,
CEC061C82ABB0A0100A11614 /* CompletionPanel.swift in Sources */,
CE4CB5CC2AD557D90046FA34 /* NumberEntry.swift in Sources */,
CEF3D86C2B9C022900BD1D3A /* WorkaroundApplicationView.swift in Sources */,
CE84A3DE29571797009394C4 /* Action.swift in Sources */,
CE485A882A8FA195008271EF /* Release+UNNotification.swift in Sources */,
CED7CA3A2A839505004EF988 /* FetchUpdateServiceProtocol.swift in Sources */,
Expand Down
22 changes: 20 additions & 2 deletions macSKK/InputController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class InputController: IMKInputController {
private let selectedWord = PassthroughSubject<Word.Word?, Never>()
/// 入力を処理しないで直接入力させるかどうか
private var directMode: Bool = false
/// モード変更時に空白文字を一瞬追加するワークアラウンドを適用するかどうか
private var insertBlankString: Bool = false
/// 最後にイベントを受け取ったときのカーソル位置
private var cursorPosition: NSRect = .zero
/// 最後にイベントを受け取ったときのウィンドウレベル + 1
Expand Down Expand Up @@ -81,10 +83,10 @@ class InputController: IMKInputController {
}
textInput.setMarkedText(NSAttributedString(attributedText), selectionRange: cursorRange, replacementRange: Self.notFoundRange)
case .modeChanged(let inputMode, let cursorPosition):
// KittyやLINE, Alacrittyでq/lによるモード切り替えでq/lが入力されたり、C-jで改行が入力されるのを回避するワークアラウンド
// KittyやAlacrittyなど、q/lによるモード切り替えでq/lが入力されたり、C-jで改行が入力されるのを回避するワークアラウンド
// AquaSKKの空文字列挿入を参考にしています。
// https://github.com/codefirst/aquaskk/blob/4.7.5/platform/mac/src/server/SKKInputController.mm#L405-L412
if ["net.kovidgoyal.kitty", "jp.naver.line.mac", "org.alacritty"].contains(textInput.bundleIdentifier()) {
if self.insertBlankString {
textInput.setMarkedText(String(format: "%c", 0x0c), selectionRange: Self.notFoundRange, replacementRange: Self.notFoundRange)
textInput.setMarkedText("", selectionRange: Self.notFoundRange, replacementRange: Self.notFoundRange)
}
Expand Down Expand Up @@ -151,6 +153,11 @@ class InputController: IMKInputController {
self?.directMode = bundleIdentifiers.contains(bundleIdentifier)
}
}.store(in: &cancellables)
insertBlankStringBundleIdentifiers.sink { [weak self] bundleIdentifiers in
if let bundleIdentifier = self?.targetApp.bundleIdentifier {
self?.insertBlankString = bundleIdentifiers.contains(bundleIdentifier)
}
}.store(in: &cancellables)
stateMachine.yomiEvent.sink { [weak self] yomi in
if let self {
if let completion = dictionary.findCompletion(prefix: yomi) {
Expand Down Expand Up @@ -220,6 +227,10 @@ class InputController: IMKInputController {
keyEquivalent: "")
directModeItem.state = directMode ? .on : .off
preferenceMenu.addItem(directModeItem)
// NOTE: IMKInputControllerのmenuではsubmenuを指定してもOSに無視されるみたい
let insertBlankStringMenuItem = NSMenuItem(title: NSLocalizedString("MenuItemInsertBlankString", comment: "空文字挿入 (互換性)"), action: #selector(toggleInsertBlankString), keyEquivalent: "")
insertBlankStringMenuItem.state = insertBlankString ? .on : .off
preferenceMenu.addItem(insertBlankStringMenuItem)
}
#if DEBUG
// デバッグ用
Expand Down Expand Up @@ -299,6 +310,13 @@ class InputController: IMKInputController {
}
}

/// 現在最前面にあるアプリで、ワークアラウンドの空文字挿入の有効無効を切り換える
@objc func toggleInsertBlankString() {
if let bundleIdentifier = targetApp.bundleIdentifier {
NotificationCenter.default.post(name: notificationNameToggleInsertBlankString, object: bundleIdentifier)
}
}

#if DEBUG
@objc func showPanel() {
let point = NSPoint(x: 100, y: 500)
Expand Down
6 changes: 6 additions & 0 deletions macSKK/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ struct SettingsView: View {
case dictionaries = "SettingsNameDictionaries"
case softwareUpdate = "SettingsNameSoftwareUpdate"
case directMode = "SettingsNameDirectMode"
case workaround = "SettingsNameWorkaround"
case log = "SettingsNameLog"
#if DEBUG
case keyEvent = "SettingsNameKeyEvent"
Expand All @@ -36,6 +37,8 @@ struct SettingsView: View {
Label(section.localizedStringKey, systemImage: "gear.badge")
case .directMode:
Label(section.localizedStringKey, systemImage: "hand.raised.app")
case .workaround:
Label(section.localizedStringKey, systemImage: "shield.checkered")
case .log:
Label(section.localizedStringKey, systemImage: "doc.plaintext")
#if DEBUG
Expand Down Expand Up @@ -65,6 +68,9 @@ struct SettingsView: View {
case .directMode:
DirectModeView(settingsViewModel: settingsViewModel)
.navigationTitle(selectedSection.localizedStringKey)
case .workaround:
WorkaroundView(settingsViewModel: settingsViewModel)
.navigationTitle(selectedSection.localizedStringKey)
case .log:
LogView(log: NSLocalizedString("LoadingStatusLoading", comment: "Loading…"))
.navigationTitle(selectedSection.localizedStringKey)
Expand Down
60 changes: 60 additions & 0 deletions macSKK/Settings/SettingsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,21 @@ struct DirectModeApplication: Identifiable, Equatable {
}
}

// 回避策が設定されたアプリケーション
struct WorkaroundApplication: Identifiable, Equatable {
typealias ID = String
let bundleIdentifier: String
let insertBlankString: Bool
var icon: NSImage?
var displayName: String?

var id: ID { bundleIdentifier }

static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.id == rhs.id
}
}

@MainActor
final class SettingsViewModel: ObservableObject {
/// CheckUpdaterで取得した最新のリリース。取得前はnil
Expand All @@ -104,6 +119,8 @@ final class SettingsViewModel: ObservableObject {
@Published var showAnnotation: Bool
/// インラインで表示する変換候補の数。
@Published var inlineCandidateCount: Int
/// ワークアラウンドが設定されたアプリケーション
@Published var workaroundApplications: [WorkaroundApplication]
// 辞書ディレクトリ
let dictionariesDirectoryUrl: URL
private var cancellables = Set<AnyCancellable>()
Expand All @@ -120,6 +137,13 @@ final class SettingsViewModel: ObservableObject {
}
showAnnotation = UserDefaults.standard.bool(forKey: UserDefaultsKeys.showAnnotation)
inlineCandidateCount = UserDefaults.standard.integer(forKey: UserDefaultsKeys.inlineCandidateCount)
workaroundApplications = UserDefaults.standard.array(forKey: UserDefaultsKeys.workarounds)?.compactMap { workaround in
if let workaround = workaround as? Dictionary<String, Any>, let bundleIdentifier = workaround["bundleIdentifier"] as? String, let insertBlankString = workaround["insertBlankString"] as? Bool {
WorkaroundApplication(bundleIdentifier: bundleIdentifier, insertBlankString: insertBlankString)
} else {
nil
}
} ?? []

// SKK-JISYO.Lのようなファイルの読み込みが遅いのでバックグラウンドで処理
$dictSettings.filter({ !$0.isEmpty }).receive(on: DispatchQueue.global()).sink { dictSettings in
Expand Down Expand Up @@ -161,6 +185,15 @@ final class SettingsViewModel: ObservableObject {
}
.store(in: &cancellables)

$workaroundApplications.dropFirst().sink { applications in
let settings = applications.map { ["bundleIdentifier": $0.bundleIdentifier, "insertBlankString": $0.insertBlankString] }
UserDefaults.standard.set(settings, forKey: UserDefaultsKeys.workarounds)
}.store(in: &cancellables)

$workaroundApplications.sink { applications in
insertBlankStringBundleIdentifiers.send(applications.filter { $0.insertBlankString }.map { $0.bundleIdentifier })
}.store(in: &cancellables)

NotificationCenter.default.publisher(for: notificationNameToggleDirectMode)
.sink { [weak self] notification in
if let bundleIdentifier = notification.object as? String {
Expand All @@ -175,6 +208,21 @@ final class SettingsViewModel: ObservableObject {
}
.store(in: &cancellables)

NotificationCenter.default.publisher(for: notificationNameToggleInsertBlankString)
.sink { [weak self] notification in
// 現状はワークアラウンドの種類が空文字挿入しかないのでBundle Identifierでただ検索している
if let self, let bundleIdentifier = notification.object as? String {
if let index = self.workaroundApplications.firstIndex(where: { $0.bundleIdentifier == bundleIdentifier }) {
logger.log("Bundle Identifier \"\(bundleIdentifier, privacy: .public)\" の空文字挿入の互換性が解除されました。")
self.workaroundApplications.remove(at: index)
} else {
logger.log("Bundle Identifier \"\(bundleIdentifier, privacy: .public)\" の空文字挿入の互換性が設定されました。")
self.workaroundApplications.append(WorkaroundApplication(bundleIdentifier: bundleIdentifier, insertBlankString: true))
}
}
}
.store(in: &cancellables)

// 空以外のdictSettingsがセットされたときに一回だけ実行する
$dictSettings.filter({ !$0.isEmpty }).first().sink { [weak self] _ in
self?.setupNotification()
Expand Down Expand Up @@ -230,6 +278,7 @@ final class SettingsViewModel: ObservableObject {
selectedInputSourceId = InputSource.defaultInputSourceId
showAnnotation = true
inlineCandidateCount = 3
workaroundApplications = []
}

// DictionaryViewのPreviewProvider用
Expand All @@ -250,6 +299,12 @@ final class SettingsViewModel: ObservableObject {
self.inputSources = inputSources
}

// WorkaroundViewのPreviewProvider用
internal convenience init(workaroundApplications: [WorkaroundApplication]) throws {
try self.init()
self.workaroundApplications = workaroundApplications
}

/**
* 辞書ファイルが追加・削除された通知を受け取りdictSettingsを更新する処理をセットアップします。
*
Expand Down Expand Up @@ -305,6 +360,11 @@ final class SettingsViewModel: ObservableObject {
directModeApplications[index].icon = icon
}

func updateWorkaroundApplication(index: Int, displayName: String, icon: NSImage) {
workaroundApplications[index].displayName = displayName
workaroundApplications[index].icon = icon
}

/// 利用可能なキー配列を読み込む
func loadInputSources() {
if let inputSources = InputSource.fetch() {
Expand Down
1 change: 1 addition & 0 deletions macSKK/Settings/UserDefaultsKeys.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ struct UserDefaultsKeys {
static let selectedInputSource = "selectedInputSource"
static let showAnnotation = "showAnnotation"
static let inlineCandidateCount = "inlineCandidateCount"
static let workarounds = "workarounds"
}
51 changes: 51 additions & 0 deletions macSKK/Settings/WorkaroundApplicationView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2024 mtgto <[email protected]>
// SPDX-License-Identifier: GPL-3.0-or-later

import SwiftUI

struct WorkaroundApplicationView: View {
@StateObject var settingsViewModel: SettingsViewModel
@Binding var bundleIdentifier: String
@Binding var insertBlankString: Bool
@Binding var isShowingSheet: Bool

var body: some View {
Form {
Section {
TextField("Bundle Identifier", text: $bundleIdentifier)
Toggle("Insert Blank String", isOn: $insertBlankString)
.toggleStyle(.switch)
} header: {
Text("SettingsHeaderWorkaroundApplication")
} footer: {
HStack {
Spacer()
Button(role: .cancel) {
isShowingSheet = false
} label: {
Text("Cancel")
}
.keyboardShortcut(.cancelAction)
Button {
settingsViewModel.workaroundApplications.append(
WorkaroundApplication(bundleIdentifier: bundleIdentifier, insertBlankString: insertBlankString))
isShowingSheet = false
} label: {
Text("Add")
}
.keyboardShortcut(.defaultAction)
.disabled(bundleIdentifier.isEmpty)
}
}
}
.formStyle(.grouped)
.frame(width: 480)
}
}

#Preview {
WorkaroundApplicationView(settingsViewModel: try! SettingsViewModel(),
bundleIdentifier: .constant("net.mtgto.inputmethod.macSKK"),
insertBlankString: .constant(true),
isShowingSheet: .constant(true))
}
Loading

0 comments on commit 60812e4

Please sign in to comment.