697 lines
24 KiB
Swift
697 lines
24 KiB
Swift
import AppKit
|
|
import CoreGraphics
|
|
import Foundation
|
|
import SwiftUI
|
|
import MacToolsPluginKit
|
|
|
|
public final class DisplayBrightnessPluginFactory: NSObject, MacToolsPluginBundleFactory {
|
|
public static func makeProvider(context: PluginRuntimeContext) throws -> any PluginProvider {
|
|
DisplayBrightnessPluginProvider(context: context)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private struct DisplayBrightnessPluginProvider: PluginProvider {
|
|
let context: PluginRuntimeContext
|
|
|
|
func makePlugins() -> [any MacToolsPlugin] {
|
|
[
|
|
DisplayBrightnessPlugin(
|
|
localization: PluginLocalization(bundle: context.resourceBundle),
|
|
shortcutPreferences: DisplayBrightnessShortcutPreferences(storage: context.storage)
|
|
)
|
|
]
|
|
}
|
|
}
|
|
|
|
enum DisplayBrightnessShortcutDirection: Equatable {
|
|
case decrease
|
|
case increase
|
|
|
|
var actionID: String {
|
|
switch self {
|
|
case .decrease:
|
|
return "display-brightness.decrease"
|
|
case .increase:
|
|
return "display-brightness.increase"
|
|
}
|
|
}
|
|
|
|
func title(localization: PluginLocalization) -> String {
|
|
switch self {
|
|
case .decrease:
|
|
return localization.string("shortcut.direction.decrease", defaultValue: "降低")
|
|
case .increase:
|
|
return localization.string("shortcut.direction.increase", defaultValue: "增加")
|
|
}
|
|
}
|
|
|
|
var systemImage: String {
|
|
switch self {
|
|
case .decrease:
|
|
return "sun.min.fill"
|
|
case .increase:
|
|
return "sun.max.fill"
|
|
}
|
|
}
|
|
|
|
var multiplier: Double {
|
|
switch self {
|
|
case .decrease:
|
|
return -1
|
|
case .increase:
|
|
return 1
|
|
}
|
|
}
|
|
}
|
|
|
|
struct DisplayBrightnessShortcutAction: Equatable {
|
|
let id: String
|
|
let direction: DisplayBrightnessShortcutDirection
|
|
let targetDisplayIDs: [CGDirectDisplayID]
|
|
}
|
|
|
|
struct DisplayBrightnessShortcutAcceleration {
|
|
private var lastPressDateByActionID: [String: Date] = [:]
|
|
private var quickPressCountByActionID: [String: Int] = [:]
|
|
|
|
mutating func stepForPress(actionID: String, now: Date, fastTapWindow: TimeInterval = 0.48) -> Int {
|
|
let quickPressCount: Int
|
|
if let lastPressDate = lastPressDateByActionID[actionID],
|
|
now.timeIntervalSince(lastPressDate) <= fastTapWindow {
|
|
quickPressCount = min((quickPressCountByActionID[actionID] ?? 0) + 1, 9)
|
|
} else {
|
|
quickPressCount = 0
|
|
}
|
|
|
|
quickPressCountByActionID[actionID] = quickPressCount
|
|
lastPressDateByActionID[actionID] = now
|
|
return min(10, 1 + quickPressCount)
|
|
}
|
|
|
|
static func stepForHold(elapsed: TimeInterval, baseline: Int) -> Int {
|
|
let elapsedStep: Int
|
|
switch elapsed {
|
|
case ..<0.45:
|
|
elapsedStep = 1
|
|
case ..<0.9:
|
|
elapsedStep = 2
|
|
case ..<1.35:
|
|
elapsedStep = 3
|
|
case ..<1.8:
|
|
elapsedStep = 4
|
|
case ..<2.4:
|
|
elapsedStep = 6
|
|
case ..<3.2:
|
|
elapsedStep = 8
|
|
default:
|
|
elapsedStep = 10
|
|
}
|
|
|
|
return min(10, max(baseline, elapsedStep))
|
|
}
|
|
}
|
|
|
|
private struct DisplayBrightnessShortcutSession {
|
|
let id: UUID
|
|
let action: DisplayBrightnessShortcutAction
|
|
let task: Task<Void, Never>
|
|
}
|
|
|
|
@MainActor
|
|
final class DisplayBrightnessPlugin: MacToolsPlugin, PluginPrimaryPanel, PluginShortcutEventHandling, DisplayTopologyRefreshing {
|
|
private enum Constants {
|
|
static let displayControlPrefix = "display."
|
|
static let brightnessControlSuffix = ".brightness"
|
|
static let disableBuiltInDisplayControlID = "built-in-display-disable"
|
|
static let restoreBuiltInDisplayControlID = "built-in-display-restore"
|
|
static let shortcutGroupID = "display-brightness.shortcuts"
|
|
}
|
|
|
|
let metadata: PluginMetadata
|
|
|
|
let primaryPanelDescriptor = PluginPrimaryPanelDescriptor(
|
|
controlStyle: .disclosure,
|
|
menuActionBehavior: .keepPresented
|
|
)
|
|
|
|
var onStateChange: (() -> Void)?
|
|
var requestPermissionGuidance: ((String) -> Void)?
|
|
var shortcutBindingResolver: ((String) -> ShortcutBinding?)?
|
|
|
|
private let controller: DisplayBrightnessControlling
|
|
private let displayDisableCoordinator: any DisplayDisableCoordinating
|
|
private let showsDisplayDisableControls: Bool
|
|
private let shortcutPreferences: DisplayBrightnessShortcutPreferences
|
|
private let mouseDisplayIDProvider: @MainActor () -> CGDirectDisplayID?
|
|
private let localization: PluginLocalization
|
|
private var isExpanded = false
|
|
private var displayDisableActionTask: Task<Void, Never>?
|
|
private var displayTopologyTask: Task<Void, Never>?
|
|
private var shortcutAcceleration = DisplayBrightnessShortcutAcceleration()
|
|
private var shortcutSessions: [String: DisplayBrightnessShortcutSession] = [:]
|
|
|
|
init(
|
|
controller: DisplayBrightnessControlling? = nil,
|
|
localization: PluginLocalization = PluginLocalization(bundle: .main),
|
|
displayDisableCoordinator: (any DisplayDisableCoordinating)? = nil,
|
|
showsDisplayDisableControls: Bool = true,
|
|
shortcutPreferences: DisplayBrightnessShortcutPreferences? = nil,
|
|
mouseDisplayIDProvider: @escaping @MainActor () -> CGDirectDisplayID? = DisplayBrightnessPlugin.currentMouseDisplayID
|
|
) {
|
|
self.localization = localization
|
|
self.controller = controller ?? DisplayBrightnessController(localization: localization)
|
|
self.displayDisableCoordinator = displayDisableCoordinator ?? DisplayDisableCoordinator(
|
|
service: Self.defaultDisplayDisableService(),
|
|
store: UserDefaultsDisplayDisableStateStore(),
|
|
localization: localization
|
|
)
|
|
self.showsDisplayDisableControls = showsDisplayDisableControls
|
|
self.shortcutPreferences = shortcutPreferences ?? DisplayBrightnessShortcutPreferences(
|
|
storage: UserDefaultsPluginStorage(pluginID: "display-brightness")
|
|
)
|
|
self.mouseDisplayIDProvider = mouseDisplayIDProvider
|
|
self.metadata = PluginMetadata(
|
|
id: "display-brightness",
|
|
title: localization.string("metadata.title", defaultValue: "显示器亮度"),
|
|
iconName: "sun.max",
|
|
iconTint: Color(nsColor: .systemYellow),
|
|
order: 20,
|
|
defaultDescription: localization.string(
|
|
"metadata.description",
|
|
defaultValue: "快速调节每个显示器的亮度"
|
|
)
|
|
)
|
|
self.controller.onStateChange = { [weak self] in
|
|
self?.onStateChange?()
|
|
}
|
|
}
|
|
|
|
var primaryPanelState: PluginPanelState {
|
|
let snapshot = controller.snapshot()
|
|
|
|
guard !snapshot.displays.isEmpty else {
|
|
isExpanded = false
|
|
return PluginPanelState(
|
|
subtitle: localization.string(
|
|
"panel.subtitle.noDisplays",
|
|
defaultValue: "未检测到可调节亮度的显示器"
|
|
),
|
|
isOn: false,
|
|
isExpanded: false,
|
|
isEnabled: false,
|
|
isVisible: true,
|
|
detail: nil,
|
|
errorMessage: snapshot.errorMessage
|
|
)
|
|
}
|
|
|
|
return PluginPanelState(
|
|
subtitle: subtitle(for: snapshot.displays),
|
|
isOn: false,
|
|
isExpanded: isExpanded,
|
|
isEnabled: true,
|
|
isVisible: true,
|
|
detail: isExpanded ? buildDetail(for: snapshot.displays) : nil,
|
|
errorMessage: snapshot.errorMessage
|
|
)
|
|
}
|
|
|
|
var permissionRequirements: [PluginPermissionRequirement] { [] }
|
|
var settingsSections: [PluginSettingsSection] { [] }
|
|
var configuration: PluginConfiguration? {
|
|
PluginConfiguration(description: metadata.defaultDescription) { [shortcutPreferences, localization] _ in
|
|
DisplayBrightnessSettingsView(
|
|
preferences: shortcutPreferences,
|
|
localization: localization
|
|
)
|
|
}
|
|
}
|
|
|
|
var shortcutDefinitions: [PluginShortcutDefinition] {
|
|
[
|
|
shortcutDefinition(direction: .decrease),
|
|
shortcutDefinition(direction: .increase)
|
|
]
|
|
}
|
|
|
|
func refresh() {
|
|
controller.refresh()
|
|
displayDisableCoordinator.refreshSnapshot()
|
|
}
|
|
|
|
func refreshDisplayTopology() {
|
|
controller.refresh()
|
|
let coordinator = displayDisableCoordinator
|
|
displayTopologyTask?.cancel()
|
|
displayTopologyTask = Task { @MainActor [weak self, coordinator] in
|
|
await coordinator.reconcileTopology()
|
|
self?.onStateChange?()
|
|
}
|
|
}
|
|
|
|
func handleAction(_ action: PluginPanelAction) {
|
|
switch action {
|
|
case let .setDisclosureExpanded(value):
|
|
isExpanded = value
|
|
onStateChange?()
|
|
case let .setSlider(controlID, value, phase):
|
|
guard let displayID = Self.parseDisplayID(from: controlID) else {
|
|
DisplayBrightnessLog.plugin.error(
|
|
"invalid slider control id \(controlID, privacy: .public)"
|
|
)
|
|
return
|
|
}
|
|
|
|
controller.setBrightness(value, for: displayID, phase: phase)
|
|
onStateChange?()
|
|
case let .invokeAction(controlID):
|
|
handleInvokeAction(controlID: controlID)
|
|
case .setSwitch,
|
|
.setSelection,
|
|
.setNavigationSelection,
|
|
.clearNavigationSelection,
|
|
.setDate:
|
|
return
|
|
}
|
|
}
|
|
|
|
func permissionState(for permissionID: String) -> PluginPermissionState {
|
|
PluginPermissionState(isGranted: true, footnote: nil)
|
|
}
|
|
|
|
func handlePermissionAction(id: String) {}
|
|
func handleSettingsAction(id: String) {}
|
|
func handleShortcutAction(id: String) {
|
|
handleShortcutEvent(id: id, phase: .pressed)
|
|
}
|
|
|
|
func handleShortcutEvent(id: String, phase: PluginShortcutEventPhase) {
|
|
switch phase {
|
|
case .pressed:
|
|
startShortcutAction(id: id)
|
|
case .released:
|
|
stopShortcutAction(id: id)
|
|
}
|
|
}
|
|
|
|
func deactivate(reason: PluginDeactivationReason) {
|
|
displayDisableActionTask?.cancel()
|
|
displayTopologyTask?.cancel()
|
|
stopAllShortcutActions()
|
|
guard reason.requiresStateCleanup else { return }
|
|
displayDisableCoordinator.restoreBuiltInDisplay()
|
|
}
|
|
|
|
static func parseDisplayID(from controlID: String) -> CGDirectDisplayID? {
|
|
guard
|
|
controlID.hasPrefix(Constants.displayControlPrefix),
|
|
controlID.hasSuffix(Constants.brightnessControlSuffix)
|
|
else {
|
|
return nil
|
|
}
|
|
|
|
let startIndex = controlID.index(
|
|
controlID.startIndex,
|
|
offsetBy: Constants.displayControlPrefix.count
|
|
)
|
|
let endIndex = controlID.index(
|
|
controlID.endIndex,
|
|
offsetBy: -Constants.brightnessControlSuffix.count
|
|
)
|
|
return CGDirectDisplayID(controlID[startIndex..<endIndex])
|
|
}
|
|
|
|
private func subtitle(for displays: [DisplayBrightnessDisplay]) -> String {
|
|
if displays.count == 1, let display = displays.first {
|
|
return "\(display.display.name) \(Self.percentText(for: display.brightness))"
|
|
}
|
|
|
|
return localization.format("panel.subtitle.displayCountFormat", defaultValue: "%d 个显示器", displays.count)
|
|
}
|
|
|
|
private func buildDetail(for displays: [DisplayBrightnessDisplay]) -> PluginPanelDetail {
|
|
let brightnessControls = displays.map { display in
|
|
PluginPanelControl(
|
|
id: "\(Constants.displayControlPrefix)\(display.display.id)\(Constants.brightnessControlSuffix)",
|
|
kind: .slider,
|
|
options: [],
|
|
selectedOptionID: nil,
|
|
dateValue: nil,
|
|
minimumDate: nil,
|
|
displayedComponents: nil,
|
|
datePickerStyle: nil,
|
|
sectionTitle: display.display.name,
|
|
sliderValue: display.brightness,
|
|
sliderBounds: 0...1,
|
|
sliderStep: 0.01,
|
|
valueLabel: Self.percentText(for: display.brightness),
|
|
isEnabled: true
|
|
)
|
|
}
|
|
|
|
return PluginPanelDetail(
|
|
primaryControls: brightnessControls + displayDisableControls(),
|
|
secondaryPanel: nil
|
|
)
|
|
}
|
|
|
|
private func displayDisableControls() -> [PluginPanelControl] {
|
|
guard showsDisplayDisableControls else {
|
|
return []
|
|
}
|
|
|
|
let snapshot = displayDisableCoordinator.snapshot
|
|
switch snapshot.status {
|
|
case .unsupported:
|
|
return [displayDisableActionControl(
|
|
id: Constants.disableBuiltInDisplayControlID,
|
|
title: localization.string(
|
|
"displayDisable.action.disable",
|
|
defaultValue: "关闭内建显示屏"
|
|
),
|
|
iconName: "display",
|
|
isEnabled: false
|
|
)]
|
|
case .unavailable:
|
|
var controls = [displayDisableActionControl(
|
|
id: Constants.disableBuiltInDisplayControlID,
|
|
title: localization.string(
|
|
"displayDisable.action.disable",
|
|
defaultValue: "关闭内建显示屏"
|
|
),
|
|
iconName: "display",
|
|
isEnabled: false
|
|
)]
|
|
if snapshot.isRestoreAllowed {
|
|
controls.append(displayDisableActionControl(
|
|
id: Constants.restoreBuiltInDisplayControlID,
|
|
title: localization.string(
|
|
"displayDisable.action.restore",
|
|
defaultValue: "恢复内建显示屏"
|
|
),
|
|
iconName: "display",
|
|
isEnabled: true
|
|
))
|
|
}
|
|
return controls
|
|
case .disabled:
|
|
return [displayDisableActionControl(
|
|
id: Constants.restoreBuiltInDisplayControlID,
|
|
title: localization.string(
|
|
"displayDisable.action.restore",
|
|
defaultValue: "恢复内建显示屏"
|
|
),
|
|
iconName: "display",
|
|
isEnabled: snapshot.isRestoreAllowed
|
|
)]
|
|
case .available, .failed, .busy:
|
|
var controls: [PluginPanelControl] = []
|
|
controls.append(displayDisableActionControl(
|
|
id: Constants.disableBuiltInDisplayControlID,
|
|
title: localization.string(
|
|
"displayDisable.action.disable",
|
|
defaultValue: "关闭内建显示屏"
|
|
),
|
|
iconName: "display",
|
|
isEnabled: snapshot.isDisableAllowed
|
|
))
|
|
if snapshot.isRestoreAllowed {
|
|
controls.append(displayDisableActionControl(
|
|
id: Constants.restoreBuiltInDisplayControlID,
|
|
title: localization.string(
|
|
"displayDisable.action.restore",
|
|
defaultValue: "恢复内建显示屏"
|
|
),
|
|
iconName: "display",
|
|
isEnabled: true
|
|
))
|
|
}
|
|
return controls
|
|
}
|
|
}
|
|
|
|
private func displayDisableActionControl(
|
|
id: String,
|
|
title: String,
|
|
iconName: String,
|
|
isEnabled: Bool
|
|
) -> PluginPanelControl {
|
|
PluginPanelControl(
|
|
id: id,
|
|
kind: .actionRow,
|
|
options: [],
|
|
selectedOptionID: nil,
|
|
dateValue: nil,
|
|
minimumDate: nil,
|
|
displayedComponents: nil,
|
|
datePickerStyle: nil,
|
|
sectionTitle: nil,
|
|
actionTitle: title,
|
|
actionIconSystemName: iconName,
|
|
showsLeadingDivider: true,
|
|
isEnabled: isEnabled
|
|
)
|
|
}
|
|
|
|
private func handleInvokeAction(controlID: String) {
|
|
switch controlID {
|
|
case Constants.disableBuiltInDisplayControlID:
|
|
displayDisableActionTask?.cancel()
|
|
let coordinator = displayDisableCoordinator
|
|
displayDisableActionTask = Task { @MainActor [weak self, coordinator] in
|
|
await coordinator.disableBuiltInDisplay()
|
|
self?.onStateChange?()
|
|
}
|
|
case Constants.restoreBuiltInDisplayControlID:
|
|
displayDisableActionTask?.cancel()
|
|
displayDisableCoordinator.restoreBuiltInDisplay()
|
|
onStateChange?()
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
|
|
private static func percentText(for brightness: Double) -> String {
|
|
"\(Int((brightness * 100).rounded()))%"
|
|
}
|
|
|
|
private static func defaultDisplayDisableService() -> any DisplayDisableServicing {
|
|
SystemDisplayDisableService()
|
|
}
|
|
|
|
private func shortcutDefinition(direction: DisplayBrightnessShortcutDirection) -> PluginShortcutDefinition {
|
|
let actionID = direction.actionID
|
|
let directionTitle = direction.title(localization: localization)
|
|
|
|
return PluginShortcutDefinition(
|
|
id: actionID,
|
|
title: localization.format("shortcut.titleFormat", defaultValue: "%@亮度", directionTitle),
|
|
description: localization.format("shortcut.descriptionFormat", defaultValue: "%@显示器亮度。", directionTitle),
|
|
actionID: actionID,
|
|
scope: .global,
|
|
defaultBinding: nil,
|
|
isRequired: false,
|
|
settingsGroupID: Constants.shortcutGroupID,
|
|
settingsGroupTitle: localization.string(
|
|
"shortcut.settingsGroupTitle",
|
|
defaultValue: "亮度快捷键"
|
|
),
|
|
settingsGroupDescription: localization.string(
|
|
"shortcut.settingsGroupDescription",
|
|
defaultValue: "按所选作用范围调整显示器亮度。"
|
|
),
|
|
settingsControlTitle: directionTitle,
|
|
settingsControlSystemImage: direction.systemImage
|
|
)
|
|
}
|
|
|
|
private func startShortcutAction(id: String) {
|
|
guard shortcutSessions[id] == nil,
|
|
let direction = Self.shortcutDirection(for: id)
|
|
else {
|
|
return
|
|
}
|
|
|
|
var snapshot = controller.snapshot()
|
|
if snapshot.displays.isEmpty {
|
|
controller.refresh()
|
|
snapshot = controller.snapshot()
|
|
}
|
|
|
|
let targetDisplayIDs = shortcutTargetDisplayIDs(in: snapshot)
|
|
guard !targetDisplayIDs.isEmpty else {
|
|
return
|
|
}
|
|
|
|
let action = DisplayBrightnessShortcutAction(
|
|
id: id,
|
|
direction: direction,
|
|
targetDisplayIDs: targetDisplayIDs
|
|
)
|
|
let now = Date()
|
|
let initialStep = shortcutAcceleration.stepForPress(actionID: id, now: now)
|
|
applyShortcutAction(action, step: initialStep, phase: .changed)
|
|
scheduleShortcutRepeats(for: action, initialStep: initialStep, startDate: now)
|
|
}
|
|
|
|
private func stopShortcutAction(id: String) {
|
|
guard let session = shortcutSessions.removeValue(forKey: id) else {
|
|
return
|
|
}
|
|
|
|
session.task.cancel()
|
|
commitShortcutAction(session.action)
|
|
}
|
|
|
|
private func stopAllShortcutActions() {
|
|
for session in shortcutSessions.values {
|
|
session.task.cancel()
|
|
}
|
|
shortcutSessions.removeAll()
|
|
}
|
|
|
|
private func scheduleShortcutRepeats(
|
|
for action: DisplayBrightnessShortcutAction,
|
|
initialStep: Int,
|
|
startDate: Date
|
|
) {
|
|
let sessionID = UUID()
|
|
let task = Task { @MainActor [weak self] in
|
|
await Self.sleep(seconds: Self.initialHoldDelay)
|
|
var repeatCount = 0
|
|
|
|
while !Task.isCancelled {
|
|
guard let self,
|
|
self.shortcutSessions[action.id]?.id == sessionID
|
|
else {
|
|
return
|
|
}
|
|
|
|
guard repeatCount < Self.maximumHoldRepeatCount else {
|
|
self.stopShortcutAction(id: action.id)
|
|
return
|
|
}
|
|
|
|
let elapsed = Date().timeIntervalSince(startDate)
|
|
let step = DisplayBrightnessShortcutAcceleration.stepForHold(
|
|
elapsed: elapsed,
|
|
baseline: initialStep
|
|
)
|
|
self.applyShortcutAction(action, step: step, phase: .changed)
|
|
repeatCount += 1
|
|
await Self.sleep(seconds: Self.repeatDelay)
|
|
}
|
|
}
|
|
|
|
shortcutSessions[action.id] = DisplayBrightnessShortcutSession(
|
|
id: sessionID,
|
|
action: action,
|
|
task: task
|
|
)
|
|
}
|
|
|
|
private func applyShortcutAction(
|
|
_ action: DisplayBrightnessShortcutAction,
|
|
step: Int,
|
|
phase: PluginPanelAction.SliderPhase
|
|
) {
|
|
let delta = Double(step) / 100 * action.direction.multiplier
|
|
for display in displays(for: action.targetDisplayIDs) {
|
|
controller.setBrightness(display.brightness + delta, for: display.id, phase: phase)
|
|
}
|
|
}
|
|
|
|
private func commitShortcutAction(_ action: DisplayBrightnessShortcutAction) {
|
|
for display in displays(for: action.targetDisplayIDs) {
|
|
controller.setBrightness(display.brightness, for: display.id, phase: .ended)
|
|
}
|
|
}
|
|
|
|
private func displays(for displayIDs: [CGDirectDisplayID]) -> [DisplayBrightnessDisplay] {
|
|
let displaysByID = Dictionary(uniqueKeysWithValues: controller.snapshot().displays.map { ($0.id, $0) })
|
|
return displayIDs.compactMap { displaysByID[$0] }
|
|
}
|
|
|
|
private func shortcutTargetDisplayIDs(in snapshot: DisplayBrightnessSnapshot) -> [CGDirectDisplayID] {
|
|
switch shortcutPreferences.targetMode {
|
|
case .followsMouse:
|
|
if let mouseDisplayID = mouseDisplayIDProvider(),
|
|
snapshot.displays.contains(where: { $0.id == mouseDisplayID }) {
|
|
return [mouseDisplayID]
|
|
}
|
|
|
|
if let mainDisplay = snapshot.displays.first(where: { $0.display.isMain }) {
|
|
return [mainDisplay.id]
|
|
}
|
|
|
|
return snapshot.displays.first.map { [$0.id] } ?? []
|
|
case .allDisplays:
|
|
return snapshot.displays.map(\.id)
|
|
}
|
|
}
|
|
|
|
private static var initialHoldDelay: TimeInterval {
|
|
systemKeyboardTiming(
|
|
key: "InitialKeyRepeat",
|
|
fallback: 25,
|
|
minimum: 0.22,
|
|
maximum: 0.55
|
|
)
|
|
}
|
|
|
|
private static var repeatDelay: TimeInterval {
|
|
systemKeyboardTiming(
|
|
key: "KeyRepeat",
|
|
fallback: 6,
|
|
minimum: 0.075,
|
|
maximum: 0.16
|
|
)
|
|
}
|
|
|
|
private static var maximumHoldRepeatCount: Int { 100 }
|
|
|
|
private static func systemKeyboardTiming(
|
|
key: String,
|
|
fallback: Double,
|
|
minimum: TimeInterval,
|
|
maximum: TimeInterval
|
|
) -> TimeInterval {
|
|
let rawValue = UserDefaults.standard.object(forKey: key) as? Double ?? fallback
|
|
return min(max(rawValue * 0.014, minimum), maximum)
|
|
}
|
|
|
|
private static func sleep(seconds: TimeInterval) async {
|
|
let nanoseconds = UInt64(max(0, seconds) * 1_000_000_000)
|
|
try? await Task.sleep(nanoseconds: nanoseconds)
|
|
}
|
|
|
|
static func shortcutDirection(for actionID: String) -> DisplayBrightnessShortcutDirection? {
|
|
switch actionID {
|
|
case DisplayBrightnessShortcutDirection.decrease.actionID:
|
|
return .decrease
|
|
case DisplayBrightnessShortcutDirection.increase.actionID:
|
|
return .increase
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private static func currentMouseDisplayID() -> CGDirectDisplayID? {
|
|
let mouseLocation = NSEvent.mouseLocation
|
|
guard let screen = NSScreen.screens.first(where: { $0.frame.contains(mouseLocation) }) else {
|
|
return nil
|
|
}
|
|
|
|
return displayID(for: screen)
|
|
}
|
|
|
|
private static func displayID(for screen: NSScreen) -> CGDirectDisplayID? {
|
|
guard let screenNumber = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber else {
|
|
return nil
|
|
}
|
|
|
|
return screenNumber.uint32Value
|
|
}
|
|
}
|