From 1ccff7ac028b3baf6ddf853d9fabe7393fda01c1 Mon Sep 17 00:00:00 2001 From: Qijia Liu Date: Wed, 10 Jul 2024 00:44:29 -0400 Subject: [PATCH] list some preset Apps for AppIM --- macosfrontend/macosfrontend.h | 2 +- src/config/appimoptionview.swift | 89 ++++++++++++++++++++++++++++++++ src/config/optionmodels.swift | 35 +++++++++---- src/config/optionviews.swift | 67 +----------------------- 4 files changed, 115 insertions(+), 78 deletions(-) create mode 100644 src/config/appimoptionview.swift diff --git a/macosfrontend/macosfrontend.h b/macosfrontend/macosfrontend.h index 353804a..8c41174 100644 --- a/macosfrontend/macosfrontend.h +++ b/macosfrontend/macosfrontend.h @@ -22,7 +22,7 @@ #include "webview_candidate_window.hpp" #define TERMINAL_USE_EN \ - R"JSON({"appPath": "/System/Applications/Utilities/Terminal.app/", "appName": "Terminal", "appId": "com.apple.Terminal", "imName": "keyboard-us"})JSON" + R"JSON({"appPath": "/System/Applications/Utilities/Terminal.app", "appId": "com.apple.Terminal", "imName": "keyboard-us"})JSON" namespace fcitx { diff --git a/src/config/appimoptionview.swift b/src/config/appimoptionview.swift new file mode 100644 index 0000000..5812cff --- /dev/null +++ b/src/config/appimoptionview.swift @@ -0,0 +1,89 @@ +import Fcitx +import SwiftUI +import SwiftyJSON + +// Should only list Apps that are not available in App selector. +private let presetApps: [String] = [ + "/System/Library/CoreServices/Spotlight.app", + "/System/Library/Input Methods/CharacterPalette.app", // emoji picker +] + +private func image(_ appPath: String) -> Image { + let icon = NSWorkspace.shared.icon(forFile: appPath) + return Image(nsImage: icon) +} + +struct AppIMOptionView: OptionView { + let label: String + let overrideLabel: String? = nil + let openPanel = NSOpenPanel() + @ObservedObject var model: AppIMOption + @State private var appIcon: NSImage? = nil + @State private var imNameMap: [String: String] = [:] + + func selections() -> [String] { + if model.appPath.isEmpty || presetApps.contains(model.appPath) { + return [""] + presetApps + } + return [""] + [model.appPath] + presetApps + } + + var body: some View { + HStack { + if !model.appPath.isEmpty { + image(model.appPath) + } + Picker("", selection: $model.appPath) { + ForEach(selections(), id: \.self) { key in + if key.isEmpty { + Text("Select App") + } else { + HStack { + if model.appPath != key { + image(key) + } + Text(appNameFromPath(key)).tag(key) + } + } + } + } + Button { + openSelector() + } label: { + Image(systemName: "folder") + } + Picker( + NSLocalizedString("uses", comment: "App X *uses* some input method"), + selection: $model.imName + ) { + ForEach(Array(imNameMap.keys), id: \.self) { key in + Text(imNameMap[key] ?? "").tag(key) + } + } + }.padding(.bottom, 8) + .onAppear { + imNameMap = [:] + let curGroup = JSON(parseJSON: String(Fcitx.imGetCurrentGroup())) + for (_, inputMethod) in curGroup { + let imName = inputMethod["name"].stringValue + let nativeName = inputMethod["displayName"].stringValue + imNameMap[imName] = nativeName + } + } + } + + private func openSelector() { + openPanel.allowsMultipleSelection = false + openPanel.canChooseDirectories = false + openPanel.allowedContentTypes = [.application] + openPanel.directoryURL = URL(fileURLWithPath: "/Applications") + openPanel.begin { response in + if response == .OK { + let selectedApp = openPanel.urls.first + if let appURL = selectedApp { + model.appPath = appURL.localPath() + } + } + } + } +} diff --git a/src/config/optionmodels.swift b/src/config/optionmodels.swift index a77446e..39d42d4 100644 --- a/src/config/optionmodels.swift +++ b/src/config/optionmodels.swift @@ -329,19 +329,32 @@ protocol EmptyConstructible { class FontOption: StringOption {} +private func bundleIdentifier(_ appPath: String) -> String { + guard let bundle = Bundle(path: appPath) else { + return "" + } + return bundle.bundleIdentifier ?? "" +} + +func appNameFromPath(_ path: String) -> String { + let name = URL(filePath: path).lastPathComponent + return name.hasSuffix(".app") ? String(name.dropLast(4)) : name +} + class AppIMOption: Option, ObservableObject, EmptyConstructible { typealias Storage = String let defaultValue: String var value: String - @Published var appId: String { - didSet { updateValue() } - } - @Published var appName: String { - didSet { updateValue() } - } + + // Source of truth as it can generate other fields. @Published var appPath: String { didSet { updateValue() } } + // Actually used by frontend. + @Published var appId: String + // Shown in config UI. + @Published var appName: String + @Published var imName: String { didSet { updateValue() } } @@ -353,8 +366,9 @@ class AppIMOption: Option, ObservableObject, EmptyConstructible { if let data = (value ?? defaultValue).data(using: .utf8) { let json = try JSON(data: data) appId = try String?.decode(json: json["appId"]) ?? "" - appName = try String?.decode(json: json["appName"]) ?? "" - appPath = try String?.decode(json: json["appPath"]) ?? "" + let path = try String?.decode(json: json["appPath"]) ?? "" + appName = appNameFromPath(path) + appPath = path imName = try String?.decode(json: json["imName"]) ?? "" } else { throw NSError() @@ -368,9 +382,10 @@ class AppIMOption: Option, ObservableObject, EmptyConstructible { } private func updateValue() { + appId = bundleIdentifier(appPath) + appName = appNameFromPath(appPath) let json = JSON([ "appId": appId.encodeValueJSON(), - "appName": appName.encodeValueJSON(), "appPath": appPath.encodeValueJSON(), "imName": imName.encodeValueJSON(), ]) @@ -389,8 +404,6 @@ class AppIMOption: Option, ObservableObject, EmptyConstructible { } func resetToDefault() { - appId = "" - appName = "" appPath = "" imName = "" } diff --git a/src/config/optionviews.swift b/src/config/optionviews.swift index dc9106d..8b93822 100644 --- a/src/config/optionviews.swift +++ b/src/config/optionviews.swift @@ -436,71 +436,6 @@ struct FontOptionView: OptionView { } } -func bundleIdentifier(_ appPath: String) -> String { - guard let bundle = Bundle(path: appPath) else { - return "" - } - return bundle.bundleIdentifier ?? "" -} - -struct AppIMOptionView: OptionView { - let label: String - let overrideLabel: String? = nil - let openPanel = NSOpenPanel() - @ObservedObject var model: AppIMOption - @State private var appIcon: NSImage? = nil - @State private var imNameMap: [String: String] = [:] - - var body: some View { - HStack { - if !model.appPath.isEmpty { - let icon = NSWorkspace.shared.icon(forFile: model.appPath) - Image(nsImage: icon) - .padding(.trailing, 8) - } - Button(action: openSelector) { - Text(model.appName.isEmpty ? NSLocalizedString("Select App", comment: "") : model.appName) - } - Picker( - NSLocalizedString("uses", comment: "App X *uses* some input method"), - selection: $model.imName - ) { - ForEach(Array(imNameMap.keys), id: \.self) { key in - Text(imNameMap[key] ?? "").tag(key) - } - } - }.padding(.bottom, 8) - .onAppear { - imNameMap = [:] - let curGroup = JSON(parseJSON: String(Fcitx.imGetCurrentGroup())) - for (_, inputMethod) in curGroup { - let imName = inputMethod["name"].stringValue - let nativeName = inputMethod["displayName"].stringValue - imNameMap[imName] = nativeName - } - } - } - - private func openSelector() { - openPanel.allowsMultipleSelection = false - openPanel.canChooseDirectories = false - openPanel.allowedContentTypes = [.application] - openPanel.directoryURL = URL(fileURLWithPath: "/Applications") - openPanel.begin { response in - if response == .OK { - let selectedApp = openPanel.urls.first - if let appURL = selectedApp { - let path = appURL.localPath() - model.appId = bundleIdentifier(path) - let name = appURL.lastPathComponent - model.appName = name.hasSuffix(".app") ? String(name.dropLast(4)) : name - model.appPath = path - } - } - } - } -} - struct PunctuationMapOptionView: OptionView { let label: String let overrideLabel: String? = nil @@ -566,7 +501,7 @@ struct GroupOptionView: OptionView { // content in the right column. GridRow { subLabel - .frame(minWidth: 100, maxWidth: 300, alignment: .trailing) + .frame(minWidth: 100, maxWidth: 250, alignment: .trailing) .help(NSLocalizedString("Right click to reset this item", comment: "")) .contextMenu { Button {