diff --git a/azooKeyMac.xcodeproj/project.pbxproj b/azooKeyMac.xcodeproj/project.pbxproj index c6adff8..cfaaea0 100644 --- a/azooKeyMac.xcodeproj/project.pbxproj +++ b/azooKeyMac.xcodeproj/project.pbxproj @@ -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 */; }; @@ -85,6 +86,7 @@ 55A27D012BAAADDB00512DCD /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 55A9C54A2BA847A1007F6F02 /* main@2x.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = "main@2x.tiff"; sourceTree = ""; }; 55A9C54D2BA84951007F6F02 /* NSRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSRange.swift; sourceTree = ""; }; + 55CE92522C9FC08100C38E1B /* UserDictionaryEditorWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDictionaryEditorWindow.swift; sourceTree = ""; }; 55EA62E12BD6BF900056B5BA /* ConfigWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigWindow.swift; sourceTree = ""; }; E916C20A2BF5F81600548B7A /* azooKeyMacInputControllerHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = azooKeyMacInputControllerHelper.swift; sourceTree = ""; }; E96A5BC62BF4B28400AEAB72 /* InputState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputState.swift; sourceTree = ""; }; @@ -230,6 +232,7 @@ isa = PBXGroup; children = ( 55EA62E12BD6BF900056B5BA /* ConfigWindow.swift */, + 55CE92522C9FC08100C38E1B /* UserDictionaryEditorWindow.swift */, ); path = Windows; sourceTree = ""; @@ -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 */, @@ -798,7 +802,7 @@ repositoryURL = "https://github.com/ensan-hcl/AzooKeyKanaKanjiConverter"; requirement = { kind = revision; - revision = bab70ac150504d3f3829f1eb85c0f907654523e8; + revision = 8cf7aabf630f71f3b3cb617e959e4ea2ea01f5a1; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/azooKeyMac/AppDelegate.swift b/azooKeyMac/AppDelegate.swift index b5af478..4708c42 100644 --- a/azooKeyMac/AppDelegate.swift +++ b/azooKeyMac/AppDelegate.swift @@ -29,9 +29,35 @@ 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 @@ -39,22 +65,17 @@ class AppDelegate: NSObject, NSApplicationDelegate { 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: "設定") } } diff --git a/azooKeyMac/Configs/CustomCodableConfigItem.swift b/azooKeyMac/Configs/CustomCodableConfigItem.swift index 3658d3d..4733f9b 100644 --- a/azooKeyMac/Configs/CustomCodableConfigItem.swift +++ b/azooKeyMac/Configs/CustomCodableConfigItem.swift @@ -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) @@ -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" + } + +} diff --git a/azooKeyMac/InputController/SegmentsManager.swift b/azooKeyMac/InputController/SegmentsManager.swift index add777d..1a03282 100644 --- a/azooKeyMac/InputController/SegmentsManager.swift +++ b/azooKeyMac/InputController/SegmentsManager.swift @@ -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? @@ -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 { diff --git a/azooKeyMac/Windows/ConfigWindow.swift b/azooKeyMac/Windows/ConfigWindow.swift index 0893f36..02f1fd5 100644 --- a/azooKeyMac/Windows/ConfigWindow.swift +++ b/azooKeyMac/Windows/ConfigWindow.swift @@ -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 @@ -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) } } diff --git a/azooKeyMac/Windows/UserDictionaryEditorWindow.swift b/azooKeyMac/Windows/UserDictionaryEditorWindow.swift new file mode 100644 index 0000000..43f1833 --- /dev/null +++ b/azooKeyMac/Windows/UserDictionaryEditorWindow.swift @@ -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) -> 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() +}