Skip to content

Commit f17edb3

Browse files
committed
refactor: move settings to standalone window
- Settings opens as a separate NSWindow via ⌘, or menu item - Dropdown menu now only shows traffic details + Settings/Quit entries - SettingsView redesigned with native Form layout for proper window UI - NSWindowDelegate tracks settings window lifecycle - Cleaner menu with fewer embedded SwiftUI views
1 parent 58d4abe commit f17edb3

2 files changed

Lines changed: 103 additions & 120 deletions

File tree

NetworkStatusBar/NetworkStatusBarApp.swift

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
2626
var networkStatus: NetworkDetails = NetworkDetails()
2727
let settings = AppSettings.shared
2828
private var cancellables = Set<AnyCancellable>()
29+
private var settingsWindow: NSWindow?
2930

3031
func onUpdate(update: NetworkStates) {
3132
DispatchQueue.main.async {
@@ -87,16 +88,14 @@ class AppDelegate: NSObject, NSApplicationDelegate {
8788
hostingView.setFrameSize(NSSize(width: 280, height: 320))
8889
detailsItem.view = hostingView
8990

90-
let settingsItem = NSMenuItem()
91-
let settingsHostingView = NSHostingView(rootView: SettingsView())
92-
settingsHostingView.setFrameSize(NSSize(width: 280, height: 10))
93-
settingsItem.view = settingsHostingView
94-
9591
menu.items = [
9692
detailsItem,
9793
NSMenuItem.separator(),
98-
settingsItem,
99-
NSMenuItem.separator(),
94+
NSMenuItem(
95+
title: NSLocalizedString("settings", comment: "Settings"),
96+
action: #selector(openSettings),
97+
keyEquivalent: ","
98+
),
10099
NSMenuItem(
101100
title: NSLocalizedString("quit", comment: "quit the application"),
102101
action: #selector(quit),
@@ -107,6 +106,28 @@ class AppDelegate: NSObject, NSApplicationDelegate {
107106
statusItem?.menu = menu
108107
}
109108

109+
@objc func openSettings() {
110+
if let window = settingsWindow {
111+
window.makeKeyAndOrderFront(nil)
112+
NSApp.activate(ignoringOtherApps: true)
113+
return
114+
}
115+
116+
let settingsView = SettingsView()
117+
let hostingController = NSHostingController(rootView: settingsView)
118+
119+
let window = NSWindow(contentViewController: hostingController)
120+
window.title = NSLocalizedString("settings", comment: "Settings")
121+
window.styleMask = [.titled, .closable]
122+
window.center()
123+
window.isReleasedWhenClosed = false
124+
window.delegate = self
125+
window.makeKeyAndOrderFront(nil)
126+
127+
NSApp.activate(ignoringOtherApps: true)
128+
settingsWindow = window
129+
}
130+
110131
func applicationWillTerminate(_ notification: Notification) {
111132
networkStatus.stop()
112133
}
@@ -116,19 +137,20 @@ class AppDelegate: NSObject, NSApplicationDelegate {
116137
}
117138
}
118139

140+
extension AppDelegate: NSWindowDelegate {
141+
func windowWillClose(_ notification: Notification) {
142+
if let window = notification.object as? NSWindow, window == settingsWindow {
143+
settingsWindow = nil
144+
}
145+
}
146+
}
147+
119148
extension AppDelegate: NSMenuDelegate {
120149
func menuWillOpen(_ menu: NSMenu) {
121150
// Resize details view based on content
122151
if let detailsView = menu.items.first?.view as? NSHostingView<StatusBarDetailsView> {
123152
let fittingSize = detailsView.fittingSize
124153
detailsView.setFrameSize(NSSize(width: 280, height: max(fittingSize.height, 80)))
125154
}
126-
// Resize settings view
127-
if menu.items.count > 2,
128-
let settingsView = menu.items[2].view as? NSHostingView<SettingsView>
129-
{
130-
let fittingSize = settingsView.fittingSize
131-
settingsView.setFrameSize(NSSize(width: 280, height: fittingSize.height))
132-
}
133155
}
134156
}

NetworkStatusBar/SettingsView.swift

Lines changed: 67 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -12,123 +12,84 @@ struct SettingsView: View {
1212
@State private var newBlacklistItem: String = ""
1313

1414
var body: some View {
15-
VStack(alignment: .leading, spacing: 0) {
16-
// Title
17-
Text(NSLocalizedString("settings", comment: "Settings"))
18-
.font(.system(size: 13, weight: .semibold))
19-
.padding(.horizontal, 16)
20-
.padding(.top, 12)
21-
.padding(.bottom, 8)
22-
23-
Divider()
24-
.padding(.horizontal, 8)
25-
26-
ScrollView {
27-
VStack(alignment: .leading, spacing: 16) {
28-
// Refresh interval
29-
VStack(alignment: .leading, spacing: 6) {
30-
Text(NSLocalizedString("refresh_interval", comment: "Refresh Interval"))
31-
.font(.system(size: 11, weight: .medium))
32-
.foregroundColor(.secondary)
33-
34-
HStack(spacing: 8) {
35-
Picker("", selection: $settings.refreshInterval) {
36-
Text("1s").tag(1)
37-
Text("2s").tag(2)
38-
Text("3s").tag(3)
39-
Text("5s").tag(5)
40-
Text("10s").tag(10)
41-
}
42-
.pickerStyle(.segmented)
43-
.frame(width: 220)
44-
}
45-
}
46-
47-
Divider()
15+
Form {
16+
Section {
17+
Picker(
18+
NSLocalizedString("refresh_interval", comment: "Refresh Interval"),
19+
selection: $settings.refreshInterval
20+
) {
21+
Text("1s").tag(1)
22+
Text("2s").tag(2)
23+
Text("3s").tag(3)
24+
Text("5s").tag(5)
25+
Text("10s").tag(10)
26+
}
27+
.pickerStyle(.segmented)
28+
} header: {
29+
Text(NSLocalizedString("refresh_interval", comment: "Refresh Interval"))
30+
}
4831

49-
// Traffic threshold
50-
VStack(alignment: .leading, spacing: 6) {
51-
Text(NSLocalizedString("min_threshold", comment: "Minimum Traffic Threshold"))
52-
.font(.system(size: 11, weight: .medium))
53-
.foregroundColor(.secondary)
32+
Section {
33+
Picker(
34+
NSLocalizedString("min_threshold", comment: "Minimum Traffic Threshold"),
35+
selection: $settings.minTrafficThreshold
36+
) {
37+
Text(NSLocalizedString("show_all", comment: "Show All")).tag(0)
38+
Text("1 KB/s").tag(1024)
39+
Text("10 KB/s").tag(10240)
40+
Text("100 KB/s").tag(102400)
41+
}
42+
.pickerStyle(.segmented)
43+
} header: {
44+
Text(NSLocalizedString("min_threshold", comment: "Minimum Traffic Threshold"))
45+
}
5446

55-
Picker("", selection: $settings.minTrafficThreshold) {
56-
Text(NSLocalizedString("show_all", comment: "Show All")).tag(0)
57-
Text("1 KB/s").tag(1024)
58-
Text("10 KB/s").tag(10240)
59-
Text("100 KB/s").tag(102400)
60-
}
61-
.pickerStyle(.segmented)
62-
.frame(width: 220)
47+
Section {
48+
Text(NSLocalizedString("blacklist_desc", comment: "Blacklist description"))
49+
.font(.callout)
50+
.foregroundColor(.secondary)
51+
52+
HStack {
53+
TextField(
54+
NSLocalizedString("app_name_placeholder", comment: "App name placeholder"),
55+
text: $newBlacklistItem
56+
)
57+
.textFieldStyle(.roundedBorder)
58+
.onSubmit { addBlacklistItem() }
59+
60+
Button(action: addBlacklistItem) {
61+
Image(systemName: "plus.circle.fill")
6362
}
63+
.disabled(newBlacklistItem.trimmingCharacters(in: .whitespaces).isEmpty)
64+
}
6465

65-
Divider()
66-
67-
// Blacklist section
68-
VStack(alignment: .leading, spacing: 6) {
69-
Text(NSLocalizedString("blacklist", comment: "App Blacklist"))
70-
.font(.system(size: 11, weight: .medium))
71-
.foregroundColor(.secondary)
72-
73-
Text(NSLocalizedString("blacklist_desc", comment: "Blacklist description"))
74-
.font(.system(size: 10))
75-
.foregroundColor(.secondary)
76-
.fixedSize(horizontal: false, vertical: true)
77-
78-
// Add new item
79-
HStack(spacing: 4) {
80-
TextField(
81-
NSLocalizedString("app_name_placeholder", comment: "App name placeholder"),
82-
text: $newBlacklistItem
83-
)
84-
.textFieldStyle(.roundedBorder)
85-
.font(.system(size: 11))
86-
.onSubmit { addBlacklistItem() }
87-
88-
Button(action: addBlacklistItem) {
89-
Image(systemName: "plus.circle.fill")
90-
.foregroundColor(.accentColor)
91-
}
92-
.buttonStyle(.plain)
93-
.disabled(newBlacklistItem.trimmingCharacters(in: .whitespaces).isEmpty)
94-
}
95-
96-
// Blacklist items
97-
if settings.blacklist.isEmpty {
98-
Text(NSLocalizedString("blacklist_empty", comment: "No blacklisted apps"))
99-
.font(.system(size: 10))
100-
.foregroundColor(.secondary)
101-
.italic()
102-
.padding(.vertical, 4)
103-
} else {
104-
VStack(spacing: 2) {
105-
ForEach(settings.blacklist, id: \.self) { name in
106-
HStack {
107-
Text(name)
108-
.font(.system(size: 11))
109-
.lineLimit(1)
110-
Spacer()
111-
Button(action: { settings.removeFromBlacklist(name) }) {
112-
Image(systemName: "xmark.circle.fill")
113-
.font(.system(size: 11))
114-
.foregroundColor(.secondary)
115-
}
116-
.buttonStyle(.plain)
117-
}
118-
.padding(.horizontal, 6)
119-
.padding(.vertical, 3)
120-
.background(Color.primary.opacity(0.05))
121-
.cornerRadius(4)
66+
if settings.blacklist.isEmpty {
67+
Text(NSLocalizedString("blacklist_empty", comment: "No blacklisted apps"))
68+
.foregroundColor(.secondary)
69+
.italic()
70+
} else {
71+
List {
72+
ForEach(settings.blacklist, id: \.self) { name in
73+
HStack {
74+
Text(name)
75+
Spacer()
76+
Button(action: { settings.removeFromBlacklist(name) }) {
77+
Image(systemName: "trash")
78+
.foregroundColor(.red)
12279
}
80+
.buttonStyle(.plain)
12381
}
12482
}
12583
}
84+
.frame(minHeight: 60, maxHeight: 160)
12685
}
127-
.padding(.horizontal, 16)
128-
.padding(.vertical, 12)
86+
} header: {
87+
Text(NSLocalizedString("blacklist", comment: "App Blacklist"))
12988
}
13089
}
131-
.frame(width: 280)
90+
.navigationTitle(NSLocalizedString("settings", comment: "Settings"))
91+
.frame(width: 400, height: 420)
92+
.fixedSize()
13293
}
13394

13495
private func addBlacklistItem() {

0 commit comments

Comments
 (0)