import AppKit import CoreGraphics import Foundation import OSLog import SwiftUI import MacToolsPluginKit public final class DisplayResolutionPluginFactory: NSObject, MacToolsPluginBundleFactory { public static func makeProvider(context: PluginRuntimeContext) throws -> any PluginProvider { DisplayResolutionPluginProvider(context: context) } } @MainActor private struct DisplayResolutionPluginProvider: PluginProvider { let context: PluginRuntimeContext func makePlugins() -> [any MacToolsPlugin] { [DisplayResolutionPlugin(localization: PluginLocalization(bundle: context.resourceBundle))] } } private enum ControlID { static let displayNavigation = "display-navigation" static let openSystemSettings = "display-open-system-settings" } @MainActor protocol DisplaySystemSettingsLauncher { @discardableResult func openDisplaySettings() -> Bool } @MainActor struct WorkspaceDisplaySystemSettingsLauncher: DisplaySystemSettingsLauncher { // Opens System Settings > Displays. The URL works for Ventura+ System Settings and the // legacy System Preferences app. private static let systemDisplaySettingsURL = URL(string: "x-apple.systempreferences:com.apple.preference.displays")! @discardableResult func openDisplaySettings() -> Bool { NSWorkspace.shared.open(Self.systemDisplaySettingsURL) } } @MainActor final class DisplayResolutionPlugin: MacToolsPlugin, PluginPrimaryPanel, DisplayTopologyRefreshing { private static let openSystemSettingsIcon = "gearshape" let metadata: PluginMetadata let primaryPanelDescriptor = PluginPrimaryPanelDescriptor( controlStyle: .disclosure, menuActionBehavior: .keepPresented ) var onStateChange: (() -> Void)? var requestPermissionGuidance: ((String) -> Void)? var shortcutBindingResolver: ((String) -> ShortcutBinding?)? private var isExpanded = false private var selectedDisplayID: CGDirectDisplayID? private var lastErrorMessage: String? private let controller: DisplayResolutionControlling private let systemSettingsLauncher: DisplaySystemSettingsLauncher private let localization: PluginLocalization private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "cc.ggbond.mactools", category: "DisplayResolutionPlugin") private var snapshot = DisplayResolutionSnapshot(displays: []) init( controller: DisplayResolutionControlling = DisplayResolutionController(), systemSettingsLauncher: DisplaySystemSettingsLauncher = WorkspaceDisplaySystemSettingsLauncher(), localization: PluginLocalization = PluginLocalization(bundle: .main) ) { self.localization = localization self.controller = controller self.systemSettingsLauncher = systemSettingsLauncher self.metadata = PluginMetadata( id: "display-resolution", title: localization.string("metadata.title", defaultValue: "显示器分辨率"), iconName: "display", iconTint: Color(nsColor: .systemBlue), order: 30, defaultDescription: localization.string( "metadata.description", defaultValue: "查看并切换每个显示器的分辨率" ) ) refreshSnapshot() } var primaryPanelState: PluginPanelState { let displays = snapshot.displays let panelDisplays = displays.filter { !$0.modes.isEmpty } if !panelDisplays.contains(where: { $0.display.id == selectedDisplayID }) { selectedDisplayID = nil } guard !displays.isEmpty else { selectedDisplayID = nil return PluginPanelState( subtitle: localization.string("panel.subtitle.noDisplays", defaultValue: "未检测到可用显示器"), isOn: false, isExpanded: false, isEnabled: false, isVisible: true, detail: nil, errorMessage: nil ) } guard !panelDisplays.isEmpty else { selectedDisplayID = nil return PluginPanelState( subtitle: localization.string("panel.subtitle.noModes", defaultValue: "未检测到可用分辨率"), isOn: false, isExpanded: false, isEnabled: false, isVisible: true, detail: nil, errorMessage: nil ) } return PluginPanelState( subtitle: subtitleForRowState(panelDisplays), isOn: false, isExpanded: isExpanded, isEnabled: true, isVisible: true, detail: isExpanded ? buildDetail(for: panelDisplays) : nil, errorMessage: lastErrorMessage ) } var permissionRequirements: [PluginPermissionRequirement] { [] } var settingsSections: [PluginSettingsSection] { [] } var shortcutDefinitions: [PluginShortcutDefinition] { [] } func refresh() { refreshSnapshot() } func refreshDisplayTopology() { refreshSnapshot() } func handleAction(_ action: PluginPanelAction) { switch action { case let .setDisclosureExpanded(value): isExpanded = value if !value { selectedDisplayID = nil } lastErrorMessage = nil onStateChange?() case let .setNavigationSelection(controlID, optionID): guard controlID == ControlID.displayNavigation, let rawDisplayID = UInt32(optionID) else { return } let displayID = CGDirectDisplayID(rawDisplayID) selectedDisplayID = displayID lastErrorMessage = nil onStateChange?() case let .clearNavigationSelection(controlID): guard controlID == ControlID.displayNavigation else { return } selectedDisplayID = nil lastErrorMessage = nil onStateChange?() case let .setSelection(controlID, optionID): guard let displayID = Self.parseDisplayID(from: controlID), let modeId = Int32(optionID) else { logger.error("invalid selection payload controlID=\(controlID, privacy: .public) optionID=\(optionID, privacy: .public)") return } refreshSnapshot() guard snapshot.displays.contains(where: { $0.display.id == displayID }) else { handleApplyFailure(.displayUnavailable(displayID: displayID), displayID: displayID, modeId: modeId) return } guard let target = snapshot.displays.first(where: { $0.display.id == displayID })? .allModes .first(where: { $0.modeId == modeId }) else { handleApplyFailure(.modeNotFound(modeId: modeId), displayID: displayID, modeId: modeId) return } logger.info("applying \(target.width)×\(target.height) on display \(displayID)") switch controller.applyResolution(target, for: displayID) { case .success: refreshSnapshot() lastErrorMessage = nil logger.info("applied \(target.width)×\(target.height) on display \(displayID)") onStateChange?() case .failure(let error): handleApplyFailure(error, displayID: displayID, modeId: modeId) } case let .invokeAction(controlID): guard controlID == ControlID.openSystemSettings else { return } let opened = systemSettingsLauncher.openDisplaySettings() if !opened { logger.error("failed to open system display settings via NSWorkspace") } case .setSwitch, .setDate, .setSlider: return } } func permissionState(for permissionID: String) -> PluginPermissionState { PluginPermissionState(isGranted: true, footnote: nil) } func handlePermissionAction(id: String) {} func handleSettingsAction(id: String) {} func handleShortcutAction(id: String) {} nonisolated static func visibleModes(_ modes: [DisplayResolutionInfo]) -> [DisplayResolutionInfo] { guard let first = modes.first else { return [] } let nativeAspect = modes.first(where: { $0.isNative })?.aspectRatio ?? first.aspectRatio return modes.filter { mode in abs(mode.aspectRatio - nativeAspect) < 0.005 || mode.isCurrent } } nonisolated static func optionTitle( for mode: DisplayResolutionInfo, localization: PluginLocalization = PluginLocalization(bundle: .main) ) -> String { var title = "\(mode.width)×\(mode.height)" if mode.isNative { title += localization.string("resolution.badge.native", defaultValue: " (原生)") } else if mode.isDefault { title += localization.string("resolution.badge.default", defaultValue: " (默认)") } else if mode.isHiDPI { title += " (HiDPI)" } else { title += " (LoDPI)" } return title } nonisolated static func parseDisplayID(from controlID: String) -> CGDirectDisplayID? { let prefix = "display." guard controlID.hasPrefix(prefix) else { return nil } return CGDirectDisplayID(controlID.dropFirst(prefix.count)) } private func refreshSnapshot() { let displays = controller.listConnectedDisplays() let panelDisplays = displays.map { display in let modes = controller.listAvailableResolutions(for: display.id) return PanelDisplay( display: display, modes: Self.visibleModes(modes), allModes: modes ) } snapshot = DisplayResolutionSnapshot(displays: panelDisplays) } private func subtitleForRowState(_ displays: [PanelDisplay]) -> String { if displays.count == 1, let display = displays.first { let current = display.modes.first(where: { $0.isCurrent }) return current.map { let displayName = display.display.isMain ? localization.string("display.main", defaultValue: "主屏") : display.display.name return "\(displayName) \($0.displayTitle)" } ?? metadata.defaultDescription } return localization.format("panel.subtitle.displayCountFormat", defaultValue: "%d 个显示器", displays.count) } private func buildDetail(for displays: [PanelDisplay]) -> PluginPanelDetail { let displayNavigation = PluginPanelControl( id: ControlID.displayNavigation, kind: .navigationList, options: displays.map { display in let currentSummary = display.modes.first(where: { $0.isCurrent })?.displayTitle ?? localization.string("display.currentResolution.unknown", defaultValue: "未知") return PluginPanelControlOption( id: String(display.display.id), title: display.display.name, subtitle: currentSummary ) }, selectedOptionID: selectedDisplayID.map(String.init), dateValue: nil, minimumDate: nil, displayedComponents: nil, datePickerStyle: nil, sectionTitle: nil, isEnabled: true ) let openSystemSettings = PluginPanelControl( id: ControlID.openSystemSettings, kind: .actionRow, options: [], selectedOptionID: nil, dateValue: nil, minimumDate: nil, displayedComponents: nil, datePickerStyle: nil, sectionTitle: nil, actionTitle: localization.string("panel.action.openSystemSettings", defaultValue: "打开系统显示器设置"), actionIconSystemName: Self.openSystemSettingsIcon, actionBehavior: .dismissBeforeHandling, showsLeadingDivider: true, isEnabled: true ) let navigationSecondaryPanels = displays.map { display in PluginPanelNavigationSecondaryPanel( controlID: ControlID.displayNavigation, optionID: String(display.display.id), panel: secondaryPanel(for: display) ) } let selectedSecondaryPanel = selectedDisplayID.flatMap { selectedID in displays.first(where: { $0.display.id == selectedID }).map(secondaryPanel(for:)) } return PluginPanelDetail( primaryControls: [displayNavigation, openSystemSettings], secondaryPanel: selectedSecondaryPanel, navigationSecondaryPanels: navigationSecondaryPanels ) } private func secondaryPanel(for display: PanelDisplay) -> PluginPanelSecondaryPanel { let resolutionControl = PluginPanelControl( id: "display.\(display.display.id)", kind: .selectList, options: display.modes.map { PluginPanelControlOption( id: String($0.modeId), title: Self.optionTitle(for: $0, localization: localization), subtitle: nil ) }, selectedOptionID: display.modes.first(where: { $0.isCurrent }).map { String($0.modeId) }, dateValue: nil, minimumDate: nil, displayedComponents: nil, datePickerStyle: nil, sectionTitle: nil, isEnabled: true ) return PluginPanelSecondaryPanel(title: display.display.name, controls: [resolutionControl]) } private func handleApplyFailure( _ error: DisplayResolutionError, displayID: CGDirectDisplayID, modeId: Int32 ) { logger.error( "apply failed display=\(displayID) modeId=\(modeId) reason=\(error.localizedDescription, privacy: .public)" ) lastErrorMessage = localization.format( "error.applyFailedFormat", defaultValue: "切换失败:%@", error.localizedDescription(localization: localization) ) onStateChange?() } } private struct PanelDisplay { let display: DisplayInfo let modes: [DisplayResolutionInfo] let allModes: [DisplayResolutionInfo] } private struct DisplayResolutionSnapshot { let displays: [PanelDisplay] }