replace interval submenu with a Settings window (frequency)

New SettingsWindowController (programmatic NSWindow, same pattern as
LogWindowController) with a preset popup and a custom-minutes field for
the refresh frequency. Changes apply immediately, per macOS convention.

The ⏱ Refresh Interval submenu, its custom text-field-in-menu view and
the two backing actions are removed in favor of "⚙️ Settings…" (Cmd+,).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
maxsoch 2026-07-06 22:04:31 +02:00
parent be49480732
commit 98d8cbe2a9
2 changed files with 138 additions and 48 deletions

View file

@ -20,6 +20,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
var versionItem: NSMenuItem! var versionItem: NSMenuItem!
var logWindow: LogWindowController? var logWindow: LogWindowController?
var settingsWindow: SettingsWindowController?
var logBuffer: String = "" var logBuffer: String = ""
var brewPath: String? var brewPath: String?
@ -68,40 +69,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
let intervalItem = NSMenuItem(title: "⏱ Refresh Interval", action: nil, keyEquivalent: "") menu.addItem(NSMenuItem(title: "⚙️ Settings…", action: #selector(showSettings), keyEquivalent: ","))
let intervalSubmenu = NSMenu()
let options: [(String, TimeInterval)] = [
("1 heure", 3600),
("6 heures", 21600),
]
for (label, interval) in options {
let item = NSMenuItem(title: label, action: #selector(setRefreshInterval(_:)), keyEquivalent: "")
item.representedObject = interval
intervalSubmenu.addItem(item)
}
let customMenuItem = NSMenuItem()
let customView = NSView(frame: NSRect(x: 0, y: 0, width: 200, height: 30))
let textField = NSTextField(frame: NSRect(x: 10, y: 4, width: 120, height: 22))
textField.placeholderString = "Secondes..."
textField.stringValue = "\(Int(refreshInterval))"
let confirmButton = NSButton(frame: NSRect(x: 138, y: 4, width: 52, height: 22))
confirmButton.title = "✓ Set"
confirmButton.bezelStyle = .rounded
confirmButton.target = self
confirmButton.action = #selector(applyCustomInterval(_:))
customView.addSubview(textField)
customView.addSubview(confirmButton)
customMenuItem.view = customView
intervalSubmenu.addItem(customMenuItem)
intervalItem.submenu = intervalSubmenu
menu.addItem(intervalItem)
versionItem = NSMenuItem(title: "🏷 Brew: ...", action: nil, keyEquivalent: "") versionItem = NSMenuItem(title: "🏷 Brew: ...", action: nil, keyEquivalent: "")
menu.addItem(versionItem) menu.addItem(versionItem)
@ -141,20 +109,15 @@ class AppDelegate: NSObject, NSApplicationDelegate {
} }
} }
@objc func setRefreshInterval(_ sender: NSMenuItem) { @objc func showSettings() {
guard let interval = sender.representedObject as? TimeInterval else { return } if settingsWindow == nil {
refreshInterval = interval // le set sauvegarde dans UserDefaults settingsWindow = SettingsWindowController(appDelegate: self)
restartTimer()
} }
settingsWindow?.syncUI()
@objc func applyCustomInterval(_ sender: NSButton) { settingsWindow?.showWindow(nil)
guard let view = sender.superview, // accessory app: without activation the window can open behind others
let textField = view.subviews.first(where: { $0 is NSTextField }) as? NSTextField, NSApp.activate(ignoringOtherApps: true)
let seconds = Int(textField.stringValue), seconds > 0 else { return } settingsWindow?.window?.makeKeyAndOrderFront(nil)
refreshInterval = TimeInterval(seconds)
restartTimer()
statusItem.menu?.cancelTracking()
} }
func resolveBrewPath() -> String { func resolveBrewPath() -> String {

View file

@ -0,0 +1,127 @@
import Cocoa
class SettingsWindowController: NSWindowController {
weak var appDelegate: AppDelegate?
let presets: [(label: String, interval: TimeInterval)] = [
("30 minutes", 1800),
("1 hour", 3600),
("6 hours", 21600),
("12 hours", 43200),
("24 hours", 86400),
]
var intervalPopup: NSPopUpButton!
var customMinutesField: NSTextField!
convenience init(appDelegate: AppDelegate) {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 380, height: 200),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
window.title = "BrewBar Settings"
// the controller keeps the window alive; without this, closing the
// window would deallocate it and reopening would crash
window.isReleasedWhenClosed = false
self.init(window: window)
self.appDelegate = appDelegate
buildUI()
syncUI()
window.center()
}
// MARK: - Layout
func buildUI() {
guard let contentView = window?.contentView else { return }
intervalPopup = NSPopUpButton()
for preset in presets {
intervalPopup.addItem(withTitle: preset.label)
}
intervalPopup.addItem(withTitle: "Custom")
intervalPopup.target = self
intervalPopup.action = #selector(presetPicked(_:))
customMinutesField = NSTextField()
customMinutesField.placeholderString = "60"
customMinutesField.alignment = .right
customMinutesField.target = self
customMinutesField.action = #selector(customMinutesEntered(_:))
customMinutesField.widthAnchor.constraint(equalToConstant: 60).isActive = true
let frequencyRow = NSStackView(views: [
NSTextField(labelWithString: "Check for updates every:"),
intervalPopup,
])
frequencyRow.orientation = .horizontal
let customRow = NSStackView(views: [
customMinutesField,
NSTextField(labelWithString: "minutes"),
])
customRow.orientation = .horizontal
let stack = NSStackView(views: [
frequencyRow,
customRow,
])
stack.orientation = .vertical
stack.alignment = .leading
stack.spacing = 12
stack.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(stack)
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20),
stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
stack.trailingAnchor.constraint(lessThanOrEqualTo: contentView.trailingAnchor, constant: -20),
stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20),
])
contentView.layoutSubtreeIfNeeded()
window?.setContentSize(contentView.fittingSize)
}
/// Push current values into the controls. Called on every open, since
/// values may have changed while the window was closed.
func syncUI() {
guard let appDelegate else { return }
let interval = appDelegate.refreshInterval
if let presetIndex = presets.firstIndex(where: { $0.interval == interval }) {
intervalPopup.selectItem(at: presetIndex)
} else {
intervalPopup.selectItem(at: presets.count) // Custom
}
customMinutesField.stringValue = "\(Int(interval / 60))"
}
// MARK: - Actions
@objc func presetPicked(_ sender: NSPopUpButton) {
guard sender.indexOfSelectedItem < presets.count else {
window?.makeFirstResponder(customMinutesField)
return
}
applyInterval(presets[sender.indexOfSelectedItem].interval)
}
@objc func customMinutesEntered(_ sender: NSTextField) {
guard let minutes = Int(sender.stringValue), minutes > 0 else {
syncUI() // reject invalid input by restoring the current value
return
}
applyInterval(TimeInterval(minutes * 60))
}
func applyInterval(_ interval: TimeInterval) {
appDelegate?.refreshInterval = interval
appDelegate?.restartTimer()
syncUI()
}
}