299 lines
10 KiB
Swift
299 lines
10 KiB
Swift
import AppKit
|
|
import ApplicationServices
|
|
import CoreGraphics
|
|
import SwiftUI
|
|
import MacToolsPluginKit
|
|
|
|
public final class MenuBarHiddenPluginFactory: NSObject, MacToolsPluginBundleFactory {
|
|
public static func makeProvider(context: PluginRuntimeContext) throws -> any PluginProvider {
|
|
MenuBarHiddenPluginProvider(context: context)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private struct MenuBarHiddenPluginProvider: PluginProvider {
|
|
let context: PluginRuntimeContext
|
|
|
|
func makePlugins() -> [any MacToolsPlugin] {
|
|
[
|
|
MenuBarHiddenPlugin(
|
|
context: context,
|
|
localization: PluginLocalization(bundle: context.resourceBundle)
|
|
),
|
|
]
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
final class MenuBarHiddenPlugin: MacToolsPlugin,
|
|
PluginPrimaryPanel,
|
|
PluginComponentPanel,
|
|
MenuBarHostStatusItemRecovering,
|
|
PluginPanelSurfaceLifecycleHandling,
|
|
PluginFeatureVisibilityLifecycleHandling
|
|
{
|
|
let metadata: PluginMetadata
|
|
|
|
let primaryPanelDescriptor = PluginPrimaryPanelDescriptor(
|
|
controlStyle: .switch,
|
|
menuActionBehavior: .keepPresented
|
|
)
|
|
|
|
var descriptor: PluginComponentDescriptor {
|
|
let items = controller.snapshot.hiddenItems + controller.snapshot.alwaysHiddenItems
|
|
let height = MenuBarHiddenComponentIconLayout.spanHeight(
|
|
forItems: items,
|
|
iconCache: controller.manager.iconCache
|
|
)
|
|
return PluginComponentDescriptor(
|
|
span: PluginComponentSpan(width: 4, height: height)!
|
|
)
|
|
}
|
|
|
|
private let context: PluginRuntimeContext
|
|
private let localization: PluginLocalization
|
|
private let controller: MenuBarHiddenController
|
|
private var launchObserver: NSObjectProtocol?
|
|
private var isFeatureVisible = true
|
|
|
|
var hostStatusItemFrameProvider: (() -> NSRect?)? {
|
|
get { controller.manager.hostStatusItemFrameProvider }
|
|
set {
|
|
controller.manager.hostStatusItemFrameProvider = newValue
|
|
activateAfterHostStatusItem()
|
|
}
|
|
}
|
|
|
|
var resetHostStatusItemPosition: (() -> Void)? {
|
|
get { controller.manager.resetHostStatusItemPosition }
|
|
set { controller.manager.resetHostStatusItemPosition = newValue }
|
|
}
|
|
|
|
var onStateChange: (() -> Void)? {
|
|
didSet {
|
|
controller.onStateChange = onStateChange
|
|
controller.manager.iconCache.onLayoutMetricsChange = onStateChange
|
|
}
|
|
}
|
|
|
|
var requestPermissionGuidance: ((String) -> Void)?
|
|
var shortcutBindingResolver: ((String) -> ShortcutBinding?)?
|
|
|
|
init(
|
|
context: PluginRuntimeContext = PluginRuntimeContext(pluginID: MenuBarHiddenConstants.pluginID),
|
|
controller: MenuBarHiddenController? = nil,
|
|
localization: PluginLocalization = PluginLocalization(bundle: .main)
|
|
) {
|
|
self.context = context
|
|
self.localization = localization
|
|
self.metadata = PluginMetadata(
|
|
id: MenuBarHiddenConstants.pluginID,
|
|
title: localization.string("metadata.title", defaultValue: "隐藏菜单栏图标"),
|
|
iconName: "menubar.arrow.up.rectangle",
|
|
iconTint: Color(nsColor: .systemBlue),
|
|
order: 12,
|
|
defaultDescription: localization.string(
|
|
"metadata.description",
|
|
defaultValue: "隐藏菜单栏图标并支持拖拽布局与点击转发"
|
|
)
|
|
)
|
|
let ctrl = controller ?? MenuBarHiddenController(context: context, localization: localization)
|
|
self.controller = ctrl
|
|
|
|
if controller != nil {
|
|
ctrl.activate()
|
|
}
|
|
}
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
func activate(context _: PluginRuntimeContext) {
|
|
guard isFeatureVisible else { return }
|
|
activateAfterHostStatusItem()
|
|
}
|
|
|
|
func deactivate(reason: PluginDeactivationReason) {
|
|
guard reason.requiresStateCleanup else { return }
|
|
if let launchObserver {
|
|
NotificationCenter.default.removeObserver(launchObserver)
|
|
self.launchObserver = nil
|
|
}
|
|
controller.setHiddenIconsPanelVisible(false)
|
|
controller.deactivate()
|
|
}
|
|
|
|
func featureVisibilityDidChange(_ isVisible: Bool) {
|
|
isFeatureVisible = isVisible
|
|
if isVisible {
|
|
activate(context: context)
|
|
} else {
|
|
deactivate(reason: .disabled)
|
|
}
|
|
}
|
|
|
|
func panelSurfaceDidBecomeVisible(_ surface: PluginPanelSurface) {
|
|
guard surface == .component else {
|
|
return
|
|
}
|
|
|
|
controller.setHiddenIconsPanelVisible(true)
|
|
controller.refreshPermissions()
|
|
}
|
|
|
|
func panelSurfaceDidBecomeHidden(_ surface: PluginPanelSurface) {
|
|
guard surface == .component else {
|
|
return
|
|
}
|
|
|
|
controller.setHiddenIconsPanelVisible(false)
|
|
}
|
|
|
|
private func activateAfterHostStatusItem() {
|
|
guard isFeatureVisible else { return }
|
|
|
|
// The host app's NSStatusItem is created inside applicationDidFinishLaunching.
|
|
// NSStatusBar inserts later items to the LEFT of earlier ones, so we must
|
|
// install the divider AFTER the host icon exists. Otherwise expanding the
|
|
// divider would push our own icon off-screen.
|
|
if hostStatusItemFrameProvider?() != nil {
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.controller.activate()
|
|
}
|
|
} else if launchObserver == nil {
|
|
launchObserver = NotificationCenter.default.addObserver(
|
|
forName: NSApplication.didFinishLaunchingNotification,
|
|
object: nil,
|
|
queue: .main
|
|
) { [weak self] _ in
|
|
DispatchQueue.main.async { self?.activateAfterHostStatusItem() }
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Panel state
|
|
|
|
var primaryPanelState: PluginPanelState {
|
|
return PluginPanelState(
|
|
subtitle: controller.panelSubtitle,
|
|
isOn: controller.isEnabled,
|
|
isExpanded: false,
|
|
isEnabled: true,
|
|
isVisible: true,
|
|
detail: nil,
|
|
errorMessage: nil
|
|
)
|
|
}
|
|
|
|
var componentPanelState: PluginComponentState {
|
|
let shouldShowHiddenIconsCard = controller.currentPermissions().canManageItems
|
|
&& controller.showsHiddenIconsInPanel
|
|
return PluginComponentState(
|
|
subtitle: shouldShowHiddenIconsCard ? controller.componentSubtitle : "",
|
|
isActive: controller.isEnabled,
|
|
isEnabled: shouldShowHiddenIconsCard,
|
|
isVisible: shouldShowHiddenIconsCard,
|
|
errorMessage: nil
|
|
)
|
|
}
|
|
|
|
// MARK: - Permissions
|
|
//
|
|
// The toggle works without permissions; only drag-reorder and click
|
|
// forwarding need them. The host UI uses these to display permission
|
|
// status cards.
|
|
|
|
var permissionRequirements: [PluginPermissionRequirement] {
|
|
[
|
|
PluginPermissionRequirement(
|
|
id: "accessibility",
|
|
kind: .accessibility,
|
|
title: localization.string("permission.accessibility.title", defaultValue: "辅助功能"),
|
|
description: localization.string(
|
|
"permission.accessibility.description",
|
|
defaultValue: "用于合成鼠标事件以拖拽菜单栏图标并转发点击"
|
|
)
|
|
),
|
|
PluginPermissionRequirement(
|
|
id: "screen-recording",
|
|
kind: .screenRecording,
|
|
title: localization.string("permission.screenRecording.title", defaultValue: "屏幕录制"),
|
|
description: localization.string(
|
|
"permission.screenRecording.description",
|
|
defaultValue: "用于捕获菜单栏图标的真实外观,仅授权后才能在面板中查看与点击"
|
|
)
|
|
),
|
|
]
|
|
}
|
|
|
|
func permissionState(for permissionID: String) -> PluginPermissionState {
|
|
let permissions = controller.currentPermissions()
|
|
switch permissionID {
|
|
case "accessibility":
|
|
let granted = permissions.hasAccessibility
|
|
return PluginPermissionState(
|
|
isGranted: granted,
|
|
footnote: granted ? nil : localization.string(
|
|
"permission.accessibility.footnote",
|
|
defaultValue: "未授权时仍可使用隐藏开关,但无法拖拽或转发点击"
|
|
)
|
|
)
|
|
case "screen-recording":
|
|
let granted = permissions.hasScreenRecording
|
|
return PluginPermissionState(
|
|
isGranted: granted,
|
|
footnote: granted ? nil : localization.string(
|
|
"permission.screenRecording.footnote",
|
|
defaultValue: "未授权时仍可使用隐藏开关,但布局栏与弹窗不可用"
|
|
)
|
|
)
|
|
default:
|
|
return PluginPermissionState(isGranted: true, footnote: nil)
|
|
}
|
|
}
|
|
|
|
func handlePermissionAction(id permissionID: String) {
|
|
controller.refreshPermissions()
|
|
switch permissionID {
|
|
case "accessibility":
|
|
NSWorkspace.shared.open(
|
|
URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")!
|
|
)
|
|
case "screen-recording":
|
|
NSWorkspace.shared.open(
|
|
URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture")!
|
|
)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
// MARK: - Configuration
|
|
|
|
var settingsSections: [PluginSettingsSection] { [] }
|
|
var shortcutDefinitions: [PluginShortcutDefinition] { [] }
|
|
|
|
var configuration: PluginConfiguration? {
|
|
PluginConfiguration(description: metadata.defaultDescription, prefersFullHeight: false) { [controller] _ in
|
|
MenuBarHiddenSettingsView(controller: controller)
|
|
}
|
|
}
|
|
|
|
// MARK: - Actions
|
|
|
|
func handleAction(_ action: PluginPanelAction) {
|
|
guard case let .setSwitch(enabled) = action else { return }
|
|
controller.isEnabled = enabled
|
|
}
|
|
|
|
// MARK: - Component panel
|
|
|
|
func makeView(context: PluginComponentContext) -> AnyView {
|
|
AnyView(
|
|
MenuBarHiddenComponentView(
|
|
controller: controller,
|
|
context: context
|
|
)
|
|
)
|
|
}
|
|
}
|