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

list some preset Apps for AppIM #175

Merged
merged 1 commit into from
Jul 10, 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
2 changes: 1 addition & 1 deletion macosfrontend/macosfrontend.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
89 changes: 89 additions & 0 deletions src/config/appimoptionview.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
}
}
35 changes: 24 additions & 11 deletions src/config/optionmodels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
}
Expand All @@ -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()
Expand All @@ -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(),
])
Expand All @@ -389,8 +404,6 @@ class AppIMOption: Option, ObservableObject, EmptyConstructible {
}

func resetToDefault() {
appId = ""
appName = ""
appPath = ""
imName = ""
}
Expand Down
67 changes: 1 addition & 66 deletions src/config/optionviews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down