Skip to content

Commit 95ab80d

Browse files
committed
fix: make NSOpenPanel singleton
1 parent 1af804d commit 95ab80d

8 files changed

Lines changed: 134 additions & 108 deletions

File tree

scripts/check-code-style.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,8 @@ for file in $(git ls-files | grep '\.swift$'); do
1313
echo "Please use NSLocalizedString instead of LocalizedStringKey"
1414
exit 1
1515
fi
16+
if [[ $file != *PanelManager.swift ]] && grep 'NSOpenPanel(' $file; then
17+
echo "Please use selectFile instead of NSOpenPanel"
18+
exit 1
19+
fi
1620
done

src/config/AppIMView.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ struct AppIMView: OptionViewProtocol {
5858
}
5959

6060
var body: some View {
61-
let openPanel = NSOpenPanel() // macOS 26 crashes if put outside of body.
6261
HStack {
6362
if !appPath.isEmpty {
6463
appIconFromPath(appPath)
@@ -79,7 +78,6 @@ struct AppIMView: OptionViewProtocol {
7978
}
8079
Button {
8180
selectApplication(
82-
openPanel,
8381
onFinish: { path in
8482
appPath = path
8583
})
Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ private func exportZip(_ name: String, withRime: Bool) -> Bool {
7373
}
7474

7575
struct DataView: View {
76-
@State private var openPanel = NSOpenPanel()
7776
@AppStorage("ImportDataSelectedDirectory") var importDataSelectedDirectory: String?
7877
@AppStorage("ExportDataSelectedDirectory") var exportDataSelectedDirectory: String?
7978
@State private var showImportF5a = false
@@ -88,28 +87,26 @@ struct DataView: View {
8887
@State private var showExportFailure = false
8988

9089
private func importZip(_ binding: Binding<Bool>, _ validator: @escaping () -> Bool) {
91-
// Keep a single openPanel to avoid confusion.
92-
if openPanel.isVisible {
93-
openPanel.cancel(nil)
94-
openPanel = NSOpenPanel()
95-
}
96-
openPanel.allowsMultipleSelection = false
97-
openPanel.canChooseDirectories = false
98-
openPanel.allowedContentTypes = [.zip]
99-
openPanel.directoryURL = URL(
90+
let initialDirectoryURL = URL(
10091
fileURLWithPath: importDataSelectedDirectory
10192
?? homeDir.appendingPathComponent("Downloads").localPath())
102-
openPanel.begin { response in
103-
if response == .OK {
93+
let _ = selectFile(
94+
allowsMultipleSelection: false,
95+
canChooseDirectories: false,
96+
canChooseFiles: true,
97+
allowedContentTypes: [.zip],
98+
directoryURL: initialDirectoryURL
99+
) { urls, dirURL in
100+
if let file = urls.first {
104101
_ = removeFile(extractDir)
105102
mkdirP(extractPath)
106-
if let file = openPanel.urls.first, extractZip(file), validator() {
103+
if extractZip(file), validator() {
107104
binding.wrappedValue = true
108105
} else {
109106
showInvalidZip = true
110107
}
111108
}
112-
importDataSelectedDirectory = openPanel.directoryURL?.localPath()
109+
importDataSelectedDirectory = dirURL?.localPath()
113110
}
114111
}
115112

@@ -123,15 +120,15 @@ struct DataView: View {
123120
}.value
124121
showRunning = false
125122
if res {
126-
if openPanel.isVisible {
127-
openPanel.cancel(nil)
128-
openPanel = NSOpenPanel()
129-
}
130-
openPanel.allowsMultipleSelection = false
131-
openPanel.canChooseDirectories = true
132-
openPanel.canChooseFiles = false
133-
if openPanel.runModal() == .OK {
134-
if let url = openPanel.url {
123+
let initialDirectoryURL = exportDataSelectedDirectory.map { URL(fileURLWithPath: $0) }
124+
let _ = selectFile(
125+
allowsMultipleSelection: false,
126+
canChooseDirectories: true,
127+
canChooseFiles: false,
128+
allowedContentTypes: [],
129+
directoryURL: initialDirectoryURL
130+
) { urls, _ in
131+
if let url = urls.first {
135132
if moveFile(
136133
composeDir.appendingPathComponent(name),
137134
url.appendingPathComponent(name)

src/config/DictManager.swift

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ class DictVM: ObservableObject {
5959
struct DictManagerView: View {
6060
@Environment(\.dismiss) private var dismiss
6161

62-
let openPanel = NSOpenPanel()
6362
@AppStorage("DictManagerSelectedDirectory") var dictManagerSelectedDirectory: String?
6463
@State private var selectedDicts = Set<String>()
6564
@ObservedObject private var dictVM = DictVM()
@@ -105,36 +104,37 @@ struct DictManagerView: View {
105104
}
106105
VStack {
107106
Button {
108-
openPanel.allowsMultipleSelection = true
109-
openPanel.canChooseDirectories = false
110-
openPanel.allowedContentTypes = ["dict", "scel", "txt"].map {
111-
UTType.init(filenameExtension: $0)!
112-
}
113-
openPanel.directoryURL = URL(
107+
let initialDirectoryURL = URL(
114108
fileURLWithPath: dictManagerSelectedDirectory
115109
?? homeDir.appendingPathComponent("Downloads").localPath())
116-
openPanel.begin { response in
117-
if response == .OK {
118-
mkdirP(dictPath)
119-
failure =
120-
openPanel.urls.map({ file in
121-
switch file.pathExtension {
122-
case "dict":
123-
importDict(file)
124-
case "scel":
125-
importScelDict(file)
126-
case "txt":
127-
importTxtDict(file)
128-
default:
129-
false
130-
}
131-
}).filter({ !$0 }).count
132-
}
110+
let _ = selectFile(
111+
allowsMultipleSelection: true,
112+
canChooseDirectories: false,
113+
canChooseFiles: true,
114+
allowedContentTypes: ["dict", "scel", "txt"].compactMap {
115+
UTType.init(filenameExtension: $0)
116+
},
117+
directoryURL: initialDirectoryURL
118+
) { urls, dirURL in
119+
mkdirP(dictPath)
120+
failure =
121+
urls.map({ file in
122+
switch file.pathExtension {
123+
case "dict":
124+
importDict(file)
125+
case "scel":
126+
importScelDict(file)
127+
case "txt":
128+
importTxtDict(file)
129+
default:
130+
false
131+
}
132+
}).filter({ !$0 }).count
133133
if failure > 0 {
134134
showFailure = true
135135
}
136136
reloadDicts()
137-
dictManagerSelectedDirectory = openPanel.directoryURL?.localPath()
137+
dictManagerSelectedDirectory = dirURL?.localPath()
138138
}
139139
} label: {
140140
Text("Import dictionaries")

src/config/PanelManager.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// NSOpenPanel is not designed to be opened simultaneously.
2+
import Cocoa
3+
import UniformTypeIdentifiers
4+
5+
@MainActor
6+
private var openPanel: NSOpenPanel? = nil
7+
8+
@MainActor
9+
func selectFile(
10+
allowsMultipleSelection: Bool,
11+
canChooseDirectories: Bool,
12+
canChooseFiles: Bool,
13+
allowedContentTypes: [UTType],
14+
directoryURL: URL?,
15+
onSelect: @escaping ([URL], URL?) -> Void
16+
) -> Bool {
17+
if openPanel != nil {
18+
return false
19+
}
20+
let panel = NSOpenPanel()
21+
openPanel = panel
22+
panel.allowsMultipleSelection = allowsMultipleSelection
23+
panel.canChooseDirectories = canChooseDirectories
24+
panel.canChooseFiles = canChooseFiles
25+
panel.allowedContentTypes = allowedContentTypes
26+
panel.directoryURL = directoryURL
27+
panel.begin { response in
28+
if response == .OK {
29+
onSelect(panel.urls, panel.directoryURL)
30+
}
31+
openPanel = nil
32+
}
33+
return true
34+
}

src/config/VimModeView.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ struct VimModeView: OptionViewProtocol {
55
@Binding var value: Any
66

77
var body: some View {
8-
let openPanel = NSOpenPanel() // macOS 26 crashes if put outside of body.
98
HStack {
109
let appPath = appPathFromBundleIdentifier(value as? String ?? "")
1110
let appName = appNameFromPath(appPath)
@@ -22,7 +21,6 @@ struct VimModeView: OptionViewProtocol {
2221
}
2322
Button {
2423
selectApplication(
25-
openPanel,
2624
onFinish: { path in
2725
value = bundleIdentifier(path)
2826
})

src/config/plugin.swift

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,6 @@ struct PluginView: View {
123123

124124
@ObservedObject private var pluginVM = PluginVM()
125125

126-
private let openPanel = NSOpenPanel()
127-
128126
init() {
129127
refreshPlugins()
130128
}
@@ -413,29 +411,29 @@ struct PluginView: View {
413411
}.disabled(selectedAvailable.isEmpty || processing)
414412
.buttonStyle(.borderedProminent)
415413
Button {
416-
openPanel.allowsMultipleSelection = false
417-
openPanel.canChooseDirectories = false
418-
openPanel.allowedContentTypes = [UTType.init(filenameExtension: "bz2")!]
419-
openPanel.directoryURL = URL(
420-
fileURLWithPath: homeDir.appendingPathComponent("Downloads").localPath())
421-
openPanel.begin { response in
422-
if response == .OK {
423-
for url in openPanel.urls {
424-
let fileName = url.lastPathComponent
425-
for pluginName in pluginMap.keys {
426-
if fileName == getPluginFileName(pluginName, native: true) {
427-
mkdirP(cacheDir.localPath())
428-
let cacheFileURL = getCacheURL(pluginName, native: true)
429-
let _ = copyFile(url, cacheFileURL)
430-
let _ = exec(
431-
"/usr/bin/xattr", ["-dr", "com.apple.quarantine", cacheFileURL.localPath()])
432-
let _ = extractPlugin(pluginName, native: true)
433-
restart()
434-
}
414+
let _ = selectFile(
415+
allowsMultipleSelection: false,
416+
canChooseDirectories: false,
417+
canChooseFiles: true,
418+
allowedContentTypes: [UTType.init(filenameExtension: "bz2")!],
419+
directoryURL: URL(
420+
fileURLWithPath: homeDir.appendingPathComponent("Downloads").localPath())
421+
) { urls, _ in
422+
for url in urls {
423+
let fileName = url.lastPathComponent
424+
for pluginName in pluginMap.keys {
425+
if fileName == getPluginFileName(pluginName, native: true) {
426+
mkdirP(cacheDir.localPath())
427+
let cacheFileURL = getCacheURL(pluginName, native: true)
428+
let _ = copyFile(url, cacheFileURL)
429+
let _ = exec(
430+
"/usr/bin/xattr", ["-dr", "com.apple.quarantine", cacheFileURL.localPath()])
431+
let _ = extractPlugin(pluginName, native: true)
432+
restart()
435433
}
436434
}
437-
showInvalidFileName = true
438435
}
436+
showInvalidFileName = true
439437
}
440438
} label: {
441439
Text("Install manually")

src/config/ui.swift

Lines changed: 31 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -44,39 +44,37 @@ struct SelectFileButton<Label>: View where Label: View {
4444
let label: () -> Label
4545
let model: Binding<String>
4646

47-
@State private var openPanel = NSOpenPanel()
4847
@State private var duplicateFile: DuplicateFile? = nil
4948

5049
var body: some View {
5150
HStack {
5251
Button {
5352
mkdirP(directory.localPath())
54-
// Only consider the first file, but allow multiple deletion.
55-
openPanel.allowsMultipleSelection = true
56-
openPanel.canChooseDirectories = false
57-
openPanel.allowedContentTypes = allowedContentTypes
58-
openPanel.directoryURL = directory
59-
openPanel.begin { response in
60-
if response == .OK {
61-
guard let file = openPanel.urls.first else {
53+
let _ = selectFile(
54+
allowsMultipleSelection: false,
55+
canChooseDirectories: false,
56+
canChooseFiles: true,
57+
allowedContentTypes: allowedContentTypes,
58+
directoryURL: directory
59+
) { urls, _ in
60+
guard let file = urls.first else {
61+
return
62+
}
63+
var fileName = file.lastPathComponent
64+
if !directory.contains(file) {
65+
let dst = directory.appendingPathComponent(fileName)
66+
if dst.exists() {
67+
duplicateFile = DuplicateFile(url: file)
6268
return
6369
}
64-
var fileName = file.lastPathComponent
65-
if !directory.contains(file) {
66-
let dst = directory.appendingPathComponent(fileName)
67-
if dst.exists() {
68-
duplicateFile = DuplicateFile(url: file)
69-
return
70-
}
71-
if !copyFile(file, dst) {
72-
return
73-
}
74-
} else {
75-
// Need to consider subdirectory of www/img.
76-
fileName = String(file.localPath().dropFirst(directory.localPath().count))
70+
if !copyFile(file, dst) {
71+
return
7772
}
78-
onFinish(fileName)
73+
} else {
74+
// Need to consider subdirectory of www/img.
75+
fileName = String(file.localPath().dropFirst(directory.localPath().count))
7976
}
77+
onFinish(fileName)
8078
}
8179
} label: {
8280
label()
@@ -114,17 +112,16 @@ struct SelectFileButton<Label>: View where Label: View {
114112
}
115113

116114
@MainActor
117-
func selectApplication(_ openPanel: NSOpenPanel, onFinish: @escaping (String) -> Void) {
118-
openPanel.allowsMultipleSelection = false
119-
openPanel.canChooseDirectories = false
120-
openPanel.allowedContentTypes = [.application]
121-
openPanel.directoryURL = URL(fileURLWithPath: "/Applications")
122-
openPanel.begin { response in
123-
if response == .OK {
124-
let selectedApp = openPanel.urls.first
125-
if let appURL = selectedApp {
126-
onFinish(appURL.localPath())
127-
}
115+
func selectApplication(onFinish: @escaping (String) -> Void) {
116+
let _ = selectFile(
117+
allowsMultipleSelection: false,
118+
canChooseDirectories: false,
119+
canChooseFiles: true,
120+
allowedContentTypes: [.application],
121+
directoryURL: URL(fileURLWithPath: "/Applications")
122+
) { urls, _ in
123+
if let url = urls.first {
124+
onFinish(url.localPath())
128125
}
129126
}
130127
}

0 commit comments

Comments
 (0)