209 lines
7.3 KiB
Swift
209 lines
7.3 KiB
Swift
import AppKit
|
|
|
|
// MARK: - Slider Menu Item View
|
|
class SliderMenuItemView: NSView {
|
|
let slider: NSSlider
|
|
let valueLabel: NSTextField
|
|
private let titleLabel: NSTextField
|
|
var onChanged: ((Double) -> Void)?
|
|
|
|
init(title: String, value: Double, minValue: Double, maxValue: Double, width: CGFloat = 280) {
|
|
titleLabel = NSTextField(labelWithString: title)
|
|
slider = NSSlider(value: value, minValue: minValue, maxValue: maxValue,
|
|
target: nil, action: nil)
|
|
valueLabel = NSTextField(labelWithString: String(format: "%.2fx", value))
|
|
|
|
super.init(frame: NSRect(x: 0, y: 0, width: width, height: 50))
|
|
|
|
titleLabel.font = NSFont.systemFont(ofSize: 12, weight: .medium)
|
|
titleLabel.textColor = .labelColor
|
|
titleLabel.frame = NSRect(x: 16, y: 28, width: width - 32, height: 16)
|
|
addSubview(titleLabel)
|
|
|
|
let sliderW = width - 80
|
|
slider.frame = NSRect(x: 16, y: 4, width: sliderW, height: 20)
|
|
slider.target = self
|
|
slider.action = #selector(sliderMoved)
|
|
slider.isContinuous = true
|
|
addSubview(slider)
|
|
|
|
valueLabel.font = NSFont.monospacedSystemFont(ofSize: 11, weight: .regular)
|
|
valueLabel.textColor = .secondaryLabelColor
|
|
valueLabel.alignment = .right
|
|
valueLabel.frame = NSRect(x: sliderW + 20, y: 4, width: 44, height: 20)
|
|
addSubview(valueLabel)
|
|
}
|
|
|
|
required init?(coder: NSCoder) { fatalError() }
|
|
|
|
@objc private func sliderMoved() {
|
|
let v = slider.doubleValue
|
|
valueLabel.stringValue = String(format: "%.2fx", v)
|
|
onChanged?(v)
|
|
}
|
|
|
|
func update(value: Double) {
|
|
slider.doubleValue = value
|
|
valueLabel.stringValue = String(format: "%.2fx", value)
|
|
}
|
|
}
|
|
|
|
// MARK: - Color Swatch Menu Item View
|
|
class ColorSwatchMenuItemView: NSView {
|
|
private let titleLabel: NSTextField
|
|
private var swatchButtons: [NSButton] = []
|
|
private var selectedIndex: Int = -1
|
|
var onChanged: ((NSColor) -> Void)?
|
|
|
|
static let presetColors: [(String, NSColor)] = [
|
|
("Teal", .systemTeal),
|
|
("Blue", .systemBlue),
|
|
("Purple", .systemPurple),
|
|
("Pink", .systemPink),
|
|
("Red", .systemRed),
|
|
("Orange", .systemOrange),
|
|
("Yellow", .systemYellow),
|
|
("Green", .systemGreen),
|
|
("Indigo", .systemIndigo),
|
|
]
|
|
|
|
init(title: String, currentColor: NSColor, width: CGFloat = 280) {
|
|
titleLabel = NSTextField(labelWithString: title)
|
|
super.init(frame: NSRect(x: 0, y: 0, width: width, height: 56))
|
|
|
|
titleLabel.font = NSFont.systemFont(ofSize: 12, weight: .medium)
|
|
titleLabel.textColor = .labelColor
|
|
titleLabel.frame = NSRect(x: 16, y: 34, width: width - 32, height: 16)
|
|
addSubview(titleLabel)
|
|
|
|
let swatchSize: CGFloat = 22
|
|
let gap: CGFloat = 4
|
|
let count = Self.presetColors.count
|
|
let totalW = CGFloat(count) * swatchSize + CGFloat(count - 1) * gap
|
|
var x = (width - totalW) / 2
|
|
|
|
for (i, (_, color)) in Self.presetColors.enumerated() {
|
|
let btn = NSButton(frame: NSRect(x: x, y: 6, width: swatchSize, height: swatchSize))
|
|
btn.wantsLayer = true
|
|
btn.isBordered = false
|
|
btn.title = ""
|
|
btn.layer?.backgroundColor = color.cgColor
|
|
btn.layer?.cornerRadius = swatchSize / 2
|
|
btn.layer?.borderWidth = 2
|
|
btn.layer?.borderColor = NSColor.clear.cgColor
|
|
btn.tag = i
|
|
btn.target = self
|
|
btn.action = #selector(swatchClicked(_:))
|
|
addSubview(btn)
|
|
swatchButtons.append(btn)
|
|
x += swatchSize + gap
|
|
}
|
|
|
|
highlightClosest(to: currentColor)
|
|
}
|
|
|
|
required init?(coder: NSCoder) { fatalError() }
|
|
|
|
@objc private func swatchClicked(_ sender: NSButton) {
|
|
let idx = sender.tag
|
|
guard idx >= 0 && idx < Self.presetColors.count else { return }
|
|
setSelected(idx)
|
|
onChanged?(Self.presetColors[idx].1)
|
|
}
|
|
|
|
private func setSelected(_ index: Int) {
|
|
// Clear previous
|
|
for btn in swatchButtons {
|
|
btn.layer?.borderColor = NSColor.clear.cgColor
|
|
}
|
|
// Highlight new
|
|
if index >= 0 && index < swatchButtons.count {
|
|
swatchButtons[index].layer?.borderColor = NSColor.white.cgColor
|
|
selectedIndex = index
|
|
}
|
|
}
|
|
|
|
func highlightClosest(to target: NSColor) {
|
|
let t = target.usingColorSpace(.sRGB) ?? target
|
|
var bestIdx = 0
|
|
var bestDist: CGFloat = .greatestFiniteMagnitude
|
|
for (i, (_, color)) in Self.presetColors.enumerated() {
|
|
let c = color.usingColorSpace(.sRGB) ?? color
|
|
let dr = t.redComponent - c.redComponent
|
|
let dg = t.greenComponent - c.greenComponent
|
|
let db = t.blueComponent - c.blueComponent
|
|
let dist = dr*dr + dg*dg + db*db
|
|
if dist < bestDist { bestDist = dist; bestIdx = i }
|
|
}
|
|
setSelected(bestIdx)
|
|
}
|
|
|
|
func update(color: NSColor) {
|
|
highlightClosest(to: color)
|
|
}
|
|
}
|
|
|
|
// MARK: - Settings Menu Builder
|
|
class SettingsMenuBuilder {
|
|
private var intensityView: SliderMenuItemView?
|
|
private var durationView: SliderMenuItemView?
|
|
private var colorView: ColorSwatchMenuItemView?
|
|
|
|
func buildSettingsSubmenu() -> NSMenuItem {
|
|
let submenu = NSMenu(title: "Settings")
|
|
|
|
// Color swatches
|
|
let cv = ColorSwatchMenuItemView(title: "Gradient Color:", currentColor: Settings.shared.gradientColor)
|
|
cv.onChanged = { color in Settings.shared.gradientColor = color }
|
|
colorView = cv
|
|
let colorItem = NSMenuItem()
|
|
colorItem.view = cv
|
|
submenu.addItem(colorItem)
|
|
|
|
submenu.addItem(NSMenuItem.separator())
|
|
|
|
// Intensity
|
|
let iv = SliderMenuItemView(title: "Intensity", value: Settings.shared.intensityScale,
|
|
minValue: 0.25, maxValue: 2.0)
|
|
iv.onChanged = { val in Settings.shared.intensityScale = val }
|
|
intensityView = iv
|
|
let intensityItem = NSMenuItem()
|
|
intensityItem.view = iv
|
|
submenu.addItem(intensityItem)
|
|
|
|
submenu.addItem(NSMenuItem.separator())
|
|
|
|
// Blink Duration
|
|
let dv = SliderMenuItemView(title: "Blink Duration", value: Settings.shared.blinkDurationScale,
|
|
minValue: 0.25, maxValue: 3.0)
|
|
dv.onChanged = { val in Settings.shared.blinkDurationScale = val }
|
|
durationView = dv
|
|
let durationItem = NSMenuItem()
|
|
durationItem.view = dv
|
|
submenu.addItem(durationItem)
|
|
|
|
submenu.addItem(NSMenuItem.separator())
|
|
|
|
// Reset
|
|
let resetItem = NSMenuItem(title: "Reset to Defaults", action: #selector(resetDefaults), keyEquivalent: "")
|
|
resetItem.target = self
|
|
submenu.addItem(resetItem)
|
|
|
|
let parentItem = NSMenuItem(title: "Settings", action: nil, keyEquivalent: ",")
|
|
parentItem.submenu = submenu
|
|
return parentItem
|
|
}
|
|
|
|
func refreshValues() {
|
|
let s = Settings.shared
|
|
intensityView?.update(value: s.intensityScale)
|
|
durationView?.update(value: s.blinkDurationScale)
|
|
colorView?.update(color: s.gradientColor)
|
|
}
|
|
|
|
@objc private func resetDefaults() {
|
|
Settings.shared.resetToDefaults()
|
|
refreshValues()
|
|
}
|
|
}
|