Files
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

222 lines
6.8 KiB
Swift
Raw Permalink 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 AppKit
import Foundation
import OSLog
import SwiftUI
import MacToolsPluginKit
// MARK: - Factory
public final class QuitAppsPluginFactory: NSObject, MacToolsPluginBundleFactory {
public static func makeProvider(context: PluginRuntimeContext) throws -> any PluginProvider {
QuitAppsPluginProvider(context: context)
}
}
@MainActor
private struct QuitAppsPluginProvider: PluginProvider {
let context: PluginRuntimeContext
func makePlugins() -> [any MacToolsPlugin] {
[QuitAppsPlugin(localization: PluginLocalization(bundle: context.resourceBundle))]
}
}
// MARK: - Plugin
@MainActor
final class QuitAppsPlugin: MacToolsPlugin, PluginPrimaryPanel, DropZoneAnchorProviding {
// MARK: Metadata
let metadata: PluginMetadata
let primaryPanelDescriptor: PluginPrimaryPanelDescriptor
// MARK: DropZoneAnchorProviding
var anchorRectProvider: (() -> NSRect?)?
// MARK: Callbacks
var onStateChange: (() -> Void)?
var requestPermissionGuidance: ((String) -> Void)?
var shortcutBindingResolver: ((String) -> ShortcutBinding?)?
// MARK: Private State
private let logger = Logger(
subsystem: Bundle.main.bundleIdentifier ?? "cc.ggbond.mactools",
category: "QuitAppsPlugin"
)
private let localization: PluginLocalization
private var selectionWindow: QuitAppsSelectionWindow?
private var runningAppCount: Int = 0
private var appObservers: [NSObjectProtocol] = []
init(localization: PluginLocalization = PluginLocalization(bundle: .main)) {
self.localization = localization
self.metadata = PluginMetadata(
id: "quit-apps",
title: localization.string("metadata.title", defaultValue: "退出应用"),
iconName: "power",
iconTint: Color(nsColor: .systemRed),
order: 96,
defaultDescription: localization.string(
"metadata.description",
defaultValue: "选择并退出正在运行的应用"
)
)
self.primaryPanelDescriptor = PluginPrimaryPanelDescriptor(
controlStyle: .button,
menuActionBehavior: .dismissBeforeHandling,
buttonTitleProvider: { localization.string("panel.button.choose", defaultValue: "选择") }
)
}
// MARK: PluginPrimaryPanel
var primaryPanelState: PluginPanelState {
PluginPanelState(
subtitle: runningAppCount > 0
? localization.format(
"panel.subtitle.runningCountFormat",
defaultValue: "正在运行 %d 个应用",
runningAppCount
)
: localization.string("panel.subtitle.none", defaultValue: "无正在运行的应用"),
isOn: false,
isExpanded: false,
isEnabled: runningAppCount > 0,
isVisible: true,
detail: nil,
errorMessage: nil
)
}
var permissionRequirements: [PluginPermissionRequirement] { [] }
var settingsSections: [PluginSettingsSection] { [] }
var shortcutDefinitions: [PluginShortcutDefinition] { [] }
// MARK: Lifecycle
func activate(context: PluginRuntimeContext) {
refreshRunningAppCount()
setupAppObservers()
}
func deactivate(reason: PluginDeactivationReason) {
removeAppObservers()
closeSelectionWindow()
}
func refresh() {
refreshRunningAppCount()
}
// MARK: Action
func handleAction(_ action: PluginPanelAction) {
switch action {
case .invokeAction(let controlID):
if controlID == "execute" {
showSelectionWindow()
}
default:
break
}
}
func permissionState(for permissionID: String) -> PluginPermissionState {
PluginPermissionState(isGranted: true, footnote: nil)
}
// MARK: Private App Count
private func refreshRunningAppCount() {
let count = Self.countUserFacingApps()
if runningAppCount != count {
runningAppCount = count
onStateChange?()
}
}
nonisolated private static func countUserFacingApps() -> Int {
NSWorkspace.shared.runningApplications.filter { app in
app.activationPolicy == .regular
&& app.bundleIdentifier != Bundle.main.bundleIdentifier
}.count
}
private func setupAppObservers() {
guard appObservers.isEmpty else { return }
let nc = NSWorkspace.shared.notificationCenter
let launched = nc.addObserver(
forName: NSWorkspace.didLaunchApplicationNotification,
object: nil,
queue: .main
) { [weak self] _ in
Task { @MainActor [weak self] in self?.refreshRunningAppCount() }
}
let terminated = nc.addObserver(
forName: NSWorkspace.didTerminateApplicationNotification,
object: nil,
queue: .main
) { [weak self] _ in
Task { @MainActor [weak self] in self?.refreshRunningAppCount() }
}
appObservers = [launched, terminated]
}
private func removeAppObservers() {
let nc = NSWorkspace.shared.notificationCenter
for obs in appObservers { nc.removeObserver(obs) }
appObservers.removeAll()
}
// MARK: Private Window
private func showSelectionWindow() {
if let existing = selectionWindow, existing.isVisible {
existing.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
return
}
let window = QuitAppsSelectionWindow(
localization: localization,
onDismiss: { [weak self] in
self?.closeSelectionWindow()
}
)
positionWindow(window)
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
selectionWindow = window
}
private func closeSelectionWindow() {
selectionWindow?.cleanup()
selectionWindow?.orderOut(nil)
selectionWindow = nil
}
private func positionWindow(_ window: NSWindow) {
let windowSize = window.frame.size
if let anchorRect = anchorRectProvider?() {
let screenMaxX = NSScreen.main?.frame.maxX ?? 1440
let rawX = anchorRect.midX - windowSize.width / 2
let x = max(8, min(rawX, screenMaxX - windowSize.width - 8))
let y = anchorRect.minY - windowSize.height - 4
window.setFrameOrigin(NSPoint(x: x, y: y))
return
}
guard let screen = NSScreen.main else { return }
let menuBarThickness = NSStatusBar.system.thickness
let x = screen.frame.midX - windowSize.width / 2
let y = screen.frame.maxY - menuBarThickness - windowSize.height - 12
window.setFrameOrigin(NSPoint(x: x, y: y))
}
}