Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ユーザ辞書機能を実装 #73

Merged
merged 3 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion azooKeyMac.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
55A27D022BAAADDB00512DCD /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 55A27D002BAAADDB00512DCD /* InfoPlist.strings */; };
55A9C54B2BA847A1007F6F02 /* main@2x.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 55A9C54A2BA847A1007F6F02 /* main@2x.tiff */; };
55A9C54E2BA84951007F6F02 /* NSRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55A9C54D2BA84951007F6F02 /* NSRange.swift */; };
55CE92532C9FC08100C38E1B /* UserDictionaryEditorWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55CE92522C9FC08100C38E1B /* UserDictionaryEditorWindow.swift */; };
55E0A5BE2BA8AE3200FACF50 /* KanaKanjiConverterModuleWithDefaultDictionary in Frameworks */ = {isa = PBXBuildFile; productRef = 55E0A5BD2BA8AE3200FACF50 /* KanaKanjiConverterModuleWithDefaultDictionary */; };
55E0A5C02BA8AE3200FACF50 /* SwiftUtils in Frameworks */ = {isa = PBXBuildFile; productRef = 55E0A5BF2BA8AE3200FACF50 /* SwiftUtils */; };
55EA62E22BD6BF900056B5BA /* ConfigWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55EA62E12BD6BF900056B5BA /* ConfigWindow.swift */; };
Expand Down Expand Up @@ -85,6 +86,7 @@
55A27D012BAAADDB00512DCD /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
55A9C54A2BA847A1007F6F02 /* main@2x.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = "main@2x.tiff"; sourceTree = "<group>"; };
55A9C54D2BA84951007F6F02 /* NSRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSRange.swift; sourceTree = "<group>"; };
55CE92522C9FC08100C38E1B /* UserDictionaryEditorWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDictionaryEditorWindow.swift; sourceTree = "<group>"; };
55EA62E12BD6BF900056B5BA /* ConfigWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigWindow.swift; sourceTree = "<group>"; };
E916C20A2BF5F81600548B7A /* azooKeyMacInputControllerHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = azooKeyMacInputControllerHelper.swift; sourceTree = "<group>"; };
E96A5BC62BF4B28400AEAB72 /* InputState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputState.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -230,6 +232,7 @@
isa = PBXGroup;
children = (
55EA62E12BD6BF900056B5BA /* ConfigWindow.swift */,
55CE92522C9FC08100C38E1B /* UserDictionaryEditorWindow.swift */,
);
path = Windows;
sourceTree = "<group>";
Expand Down Expand Up @@ -407,6 +410,7 @@
E96A5BC72BF4B28400AEAB72 /* InputState.swift in Sources */,
55EA62E22BD6BF900056B5BA /* ConfigWindow.swift in Sources */,
5523B1A42BF116CC0051DAA8 /* IntConfigItem.swift in Sources */,
55CE92532C9FC08100C38E1B /* UserDictionaryEditorWindow.swift in Sources */,
551398DD2BDD3ABF00F1DB82 /* CustomCodableConfigItem.swift in Sources */,
1A41E64226E74627009B65D7 /* AppDelegate.swift in Sources */,
55A9C54E2BA84951007F6F02 /* NSRange.swift in Sources */,
Expand Down Expand Up @@ -798,7 +802,7 @@
repositoryURL = "https://github.com/ensan-hcl/AzooKeyKanaKanjiConverter";
requirement = {
kind = revision;
revision = bab70ac150504d3f3829f1eb85c0f907654523e8;
revision = 8cf7aabf630f71f3b3cb617e959e4ea2ea01f5a1;
};
};
/* End XCRemoteSwiftPackageReference section */
Expand Down
51 changes: 36 additions & 15 deletions azooKeyMac/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,32 +29,53 @@ class NSManualApplication: NSApplication {
class AppDelegate: NSObject, NSApplicationDelegate {
var server = IMKServer()
weak var configWindow: NSWindow?
weak var userDictionaryEditorWindow: NSWindow?
var configWindowController: NSWindowController?
var userDictionaryEditorWindowController: NSWindowController?
@MainActor var kanaKanjiConverter = KanaKanjiConverter()

private static func buildSwiftUIWindow(
_ view: some View,
contentRect: NSRect = NSRect(x: 0, y: 0, width: 400, height: 300),
styleMask: NSWindow.StyleMask = [.titled, .closable, .resizable, .borderless],
title: String = ""
) -> (window: NSWindow, windowController: NSWindowController) {
// Create a new window
let window = NSWindow(
contentRect: contentRect,
styleMask: styleMask,
backing: .buffered,
defer: false
)
// Set the window title
window.title = title
window.contentViewController = NSHostingController(rootView: view)
// Keep window with in a controller
let windowController = NSWindowController(window: window)
// Show the window
window.level = .modalPanel
window.makeKeyAndOrderFront(nil)
return (window, windowController)
}

func openConfigWindow() {
if let configWindow {
// Show the window
configWindow.level = .modalPanel
configWindow.makeKeyAndOrderFront(nil)
} else {
// Create a new window
let configWindow = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 400, height: 300),
styleMask: [.titled, .closable, .resizable, .borderless],
backing: .buffered,
defer: false
)
// Set the window title
configWindow.title = "設定"
configWindow.contentViewController = NSHostingController(rootView: ConfigWindow())
// Keep window with in a controller
self.configWindowController = NSWindowController(window: configWindow)
(self.configWindow, self.configWindowController) = Self.buildSwiftUIWindow(ConfigWindow(), title: "設定")
}
}

func openUserDictionaryEditorWindow() {
if let userDictionaryEditorWindow {
// Show the window
configWindow.level = .modalPanel
configWindow.makeKeyAndOrderFront(nil)
// Assign the new window to the property to keep it in memory
self.configWindow = configWindow
userDictionaryEditorWindow.level = .modalPanel
userDictionaryEditorWindow.makeKeyAndOrderFront(nil)
} else {
(self.userDictionaryEditorWindow, self.userDictionaryEditorWindowController) = Self.buildSwiftUIWindow(UserDictionaryEditorWindow(), title: "設定")
}
}

Expand Down
49 changes: 48 additions & 1 deletion azooKeyMac/Configs/CustomCodableConfigItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ protocol CustomCodableConfigItem: ConfigItem {
extension CustomCodableConfigItem {
var value: Value {
get {
guard let data = UserDefaults.standard.data(forKey: Self.key) else {
print(#file, #line, "data is not set yet")
return Self.default
}
do {
let decoded = try JSONDecoder().decode(Value.self, from: UserDefaults.standard.data(forKey: Self.key) ?? Data())
let decoded = try JSONDecoder().decode(Value.self, from: data)
return decoded
} catch {
print(#file, #line, error)
Expand Down Expand Up @@ -58,3 +62,46 @@ extension Config {
static var key: String = "dev.ensan.inputmethod.azooKeyMac.preference.learning"
}
}

extension Config {
struct UserDictionary: CustomCodableConfigItem {
var items: Value = Self.default

struct Value: Codable {
var items: [Item]
}

struct Item: Codable, Identifiable {
init(word: String, reading: String, hint: String? = nil) {
self.id = UUID()
self.word = word
self.reading = reading
self.hint = hint
}

var id: UUID
var word: String
var reading: String
var hint: String?

var nonNullHint: String {
get {
hint ?? ""
}
set {
if newValue.isEmpty {
hint = nil
} else {
hint = newValue
}
}
}
}

static let `default`: Value = .init(items: [
.init(word: "azooKey", reading: "あずーきー", hint: "アプリ")
])
static let key: String = "dev.ensan.inputmethod.azooKeyMac.preference.user_dictionary_temporal2"
}

}
7 changes: 7 additions & 0 deletions azooKeyMac/InputController/SegmentsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ final class SegmentsManager {
private var englishConversionEnabled: Bool {
Config.EnglishConversion().value
}
private var userDictionary: Config.UserDictionary.Value {
Config.UserDictionary().value
}
private var rawCandidates: ConversionResult?

private var selectionIndex: Int?
Expand Down Expand Up @@ -241,6 +244,10 @@ final class SegmentsManager {
self.kanaKanjiConverter.stopComposition()
return
}
// ユーザ辞書情報の更新
self.kanaKanjiConverter.sendToDicdataStore(.importDynamicUserDict(userDictionary.items.map {
.init(word: $0.word, ruby: $0.reading.toKatakana(), cid: CIDData.固有名詞.cid, mid: MIDData.一般.mid, value: -5)
}))

let prefixComposingText = self.composingText.prefixToCursorPosition()
let leftSideContext = self.delegate?.getLeftSideContext(maxCount: 30).map {
Expand Down
5 changes: 5 additions & 0 deletions azooKeyMac/Windows/ConfigWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ struct ConfigWindow: View {
@ConfigState private var inferenceLimit = Config.ZenzaiInferenceLimit()
@ConfigState private var richCandidates = Config.ZenzaiRichCandidatesMode()
@ConfigState private var debugWindow = Config.DebugWindow()
@ConfigState private var userDictionary = Config.UserDictionary()

@State private var zenzaiHelpPopover = false
@State private var zenzaiRichCandidatesPopover = false
Expand Down Expand Up @@ -101,6 +102,10 @@ struct ConfigWindow: View {
Toggle("円記号の代わりにバックスラッシュを入力", isOn: $typeBackSlash)
Toggle("「、」「。」の代わりに「,」「.」を入力", isOn: $typeCommaAndPeriod)
Divider()
Button("ユーザ辞書を編集する") {
(NSApplication.shared.delegate as? AppDelegate)!.openUserDictionaryEditorWindow()
}
Divider()
Toggle("(開発者用)デバッグウィンドウを有効化", isOn: $debugWindow)
}
}
Expand Down
133 changes: 133 additions & 0 deletions azooKeyMac/Windows/UserDictionaryEditorWindow.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
//
// UserDictionaryEditorWindow.swift
// azooKeyMac
//
// Created by miwa on 2024/09/22.
//

import SwiftUI

struct UserDictionaryEditorWindow: View {

@ConfigState private var userDictionary = Config.UserDictionary()

@State private var editTargetID: UUID?
@State private var undoItem: Config.UserDictionary.Item?

@ViewBuilder
private func helpButton(helpContent: LocalizedStringKey, isPresented: Binding<Bool>) -> some View {
if #available(macOS 14, *) {
Button("ヘルプ", systemImage: "questionmark") {
isPresented.wrappedValue = true
}
.labelStyle(.iconOnly)
.buttonBorderShape(.circle)
.popover(isPresented: isPresented) {
Text(helpContent).padding()
}
}
}

private var isAdditionDisabled: Bool {
self.userDictionary.value.items.count >= 50
}

var body: some View {
VStack {
Text("ユーザ辞書の設定")
.bold()
.font(.title)
Text("この機能はβ版です。予告なく仕様を変更することがあるほか、最大50件に限定しています。")
.font(.caption)
Spacer()
if let editTargetID {
let itemBinding = Binding(
get: {
self.userDictionary.value.items.first {
$0.id == editTargetID
} ?? .init(word: "", reading: "")
},
set: {
if let index = self.userDictionary.value.items.firstIndex(where: {
$0.id == editTargetID
}) {
self.userDictionary.value.items[index] = $0
}
}
)
Form {
TextField("単語", text: itemBinding.word)
TextField("読み", text: itemBinding.reading)
TextField("ヒント", text: itemBinding.nonNullHint)
HStack {
Spacer()
Button("完了", systemImage: "checkmark") {
self.editTargetID = nil
}
Spacer()
}
}
} else {
HStack {
Spacer()
Button("追加", systemImage: "plus") {
let newItem = Config.UserDictionary.Item(word: "", reading: "", hint: nil)
self.userDictionary.value.items.append(newItem)
self.editTargetID = newItem.id
self.undoItem = nil
}
.disabled(self.isAdditionDisabled)
if self.isAdditionDisabled {
Label("50件を超えています", systemImage: "exclamationmark.octagon")
.foregroundStyle(.red)
}
if let undoItem {
Button("元に戻す", systemImage: "arrow.uturn.backward") {
let newItem = Config.UserDictionary.Item(word: "", reading: "", hint: nil)
self.userDictionary.value.items.append(undoItem)
self.undoItem = nil
}
}
Spacer()
}
}
HStack {
Spacer()
Table(self.userDictionary.value.items) {
TableColumn("") { item in
HStack {
Button("編集する", systemImage: "pencil") {
self.editTargetID = item.id
self.undoItem = nil
}
.buttonStyle(.bordered)
.labelStyle(.iconOnly)
Button("削除する", systemImage: "trash", role: .destructive) {
if let itemIndex = self.userDictionary.value.items.firstIndex(where: {
$0.id == item.id
}) {
self.undoItem = self.userDictionary.value.items[itemIndex]
self.userDictionary.value.items.remove(at: itemIndex)
}
}
.buttonStyle(.bordered)
.labelStyle(.iconOnly)
}
}
TableColumn("単語", value: \.word)
TableColumn("読み", value: \.reading)
TableColumn("ヒント", value: \.nonNullHint)
}
.disabled(editTargetID != nil)
Spacer()
}
Spacer()
}
.frame(minHeight: 300, maxHeight: 600)
.frame(minWidth: 600, maxWidth: 800)
}
}

#Preview {
UserDictionaryEditorWindow()
}