Files
brov-macbook/NotchBuddy/Sources/App/ShortcutsSettingsView.swift
T

197 lines
6.7 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
import AppKit
// MARK: - ShortcutsSettingsView
/// "Shortcuts" section inside SettingsView.
struct ShortcutsSettingsView: View {
// Conflicts detected by HotKeyCenter (system already owns that combo)
@State private var systemConflicts: Set<ShortcutAction> = []
// Internal duplicates (two Coucou shortcuts share the same combo)
@State private var internalDups: Set<ShortcutAction> = []
// Per-action state (enabled flag + current spec)
@State private var enabled: [ShortcutAction: Bool] = [:]
@State private var specs: [ShortcutAction: ShortcutSpec] = [:]
var body: some View {
VStack(alignment: .leading, spacing: 18) {
globalSection
islandSection
}
.onAppear { reload() }
}
// MARK: - Global shortcuts
private var globalSection: some View {
GroupBox("Глобальные сочетания — работают в любом приложении") {
VStack(alignment: .leading, spacing: 0) {
ForEach(globalActions, id: \.self) { action in
shortcutRow(action)
if action != globalActions.last {
Divider().padding(.leading, 24)
}
}
Divider().padding(.top, 8)
HStack {
Spacer()
Button("Сбросить всё по умолчанию") { resetAll() }
.buttonStyle(.bordered)
.controlSize(.small)
}
.padding(.top, 6)
}
.padding(6)
}
}
/// Shortcuts listed here. The App Store build hides the ones it can't run.
private var globalActions: [ShortcutAction] {
#if APPSTORE
ShortcutAction.allCases.filter { !$0.isNonAppStore }
#else
ShortcutAction.allCases
#endif
}
@ViewBuilder
private func shortcutRow(_ action: ShortcutAction) -> some View {
let isEnabled = enabled[action] ?? action.enabledByDefault
let spec = specs[action] ?? ShortcutLogic.defaults[action]!
let hasSysConf = systemConflicts.contains(action)
let hasIntConf = internalDups.contains(action)
HStack(spacing: 10) {
Toggle("", isOn: Binding(
get: { isEnabled },
set: { v in toggleAction(action, enabled: v) }
))
.labelsHidden()
.toggleStyle(.checkbox)
.frame(width: 18)
Text(action.displayName)
.font(.system(size: 12))
.frame(minWidth: 160, alignment: .leading)
Spacer(minLength: 4)
ShortcutRecorderButton(flags: specFlagsBinding(action), code: specCodeBinding(action))
.disabled(!isEnabled)
.onChange(of: specs[action]) { _, _ in specChanged(action) }
if hasSysConf {
conflictTag("Конфликт с системой", color: .orange)
} else if hasIntConf {
conflictTag("Повтор", color: .red)
}
}
.padding(.vertical, 5)
.opacity(isEnabled ? 1 : 0.5)
}
private func conflictTag(_ text: String, color: Color) -> some View {
Text(text)
.font(.system(size: 10, weight: .medium))
.foregroundColor(color)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(color.opacity(0.1))
.clipShape(Capsule())
}
// MARK: - Island-local shortcuts (read-only)
private var islandSection: some View {
GroupBox("Сочетания острова — работают, когда остров в фокусе") {
VStack(alignment: .leading, spacing: 0) {
ForEach(Array(ShortcutLogic.islandShortcuts.enumerated()), id: \.offset) { _, pair in
HStack(spacing: 10) {
Text(pair.key)
.font(.system(size: 11, design: .monospaced))
.frame(width: 90, alignment: .leading)
Text(pair.description)
.font(.system(size: 12))
.foregroundColor(.secondary)
Spacer()
}
.padding(.vertical, 4)
if pair.key != ShortcutLogic.islandShortcuts.last?.key {
Divider().padding(.leading, 100)
}
}
}
.padding(6)
}
}
// MARK: - Bindings
private func specFlagsBinding(_ action: ShortcutAction) -> Binding<UInt> {
Binding(
get: { (specs[action] ?? ShortcutLogic.defaults[action]!).nsFlags },
set: { v in
var s = specs[action] ?? ShortcutLogic.defaults[action]!
s.nsFlags = v
specs[action] = s
}
)
}
private func specCodeBinding(_ action: ShortcutAction) -> Binding<UInt16> {
Binding(
get: { (specs[action] ?? ShortcutLogic.defaults[action]!).keyCode },
set: { v in
var s = specs[action] ?? ShortcutLogic.defaults[action]!
s.keyCode = v
specs[action] = s
}
)
}
// MARK: - Actions
private func reload() {
for action in ShortcutAction.allCases {
enabled[action] = ShortcutLogic.isEnabled(action)
specs[action] = ShortcutLogic.hotKey(for: action)
}
systemConflicts = HotKeyCenter.shared.conflicts
updateInternalDups()
}
private func toggleAction(_ action: ShortcutAction, enabled v: Bool) {
enabled[action] = v
ShortcutLogic.setEnabled(v, for: action)
HotKeyCenter.shared.reregister(action)
systemConflicts = HotKeyCenter.shared.conflicts
}
private func specChanged(_ action: ShortcutAction) {
guard let s = specs[action] else { return }
ShortcutLogic.save(s, for: action)
HotKeyCenter.shared.reregister(action)
systemConflicts = HotKeyCenter.shared.conflicts
updateInternalDups()
}
private func resetAll() {
for action in ShortcutAction.allCases {
ShortcutLogic.resetToDefault(action)
HotKeyCenter.shared.reregister(action)
}
reload()
}
private func updateInternalDups() {
var all: [ShortcutAction: ShortcutSpec] = [:]
for action in ShortcutAction.allCases {
if ShortcutLogic.isEnabled(action) {
all[action] = ShortcutLogic.hotKey(for: action)
}
}
internalDups = ShortcutLogic.duplicates(in: all)
}
}