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

252 lines
7.4 KiB
Swift

import AppKit
import Combine
import Foundation
import ApplicationServices
import MacToolsPluginKit
// MARK: - MenuBarHiddenController
//
// View-model layer. Forwards published state from the manager to SwiftUI and
// owns panel visibility hooks. Business logic lives in `MenuBarHiddenManager`.
@MainActor
final class MenuBarHiddenController: ObservableObject {
@Published private(set) var snapshot: MenuBarHiddenSnapshot = .empty
@Published private(set) var permissions = MenuBarHiddenPermissionsStatus(
hasAccessibility: false,
hasScreenRecording: false
)
let localization: PluginLocalization
let manager: MenuBarHiddenManager
private let observer: MenuBarHiddenObserver
private var cancellables = Set<AnyCancellable>()
private var popupPanel: MenuBarHiddenPopupPanel?
private var isActivated = false
private var isSettingsVisible = false
private var isHiddenIconsPanelVisible = false
var onStateChange: (() -> Void)?
init(
context: PluginRuntimeContext,
localization: PluginLocalization = PluginLocalization(bundle: .main),
permissionProvider: @escaping () -> MenuBarHiddenPermissionsStatus = {
MenuBarHiddenPermissionsStatus(
hasAccessibility: AXIsProcessTrusted(),
hasScreenRecording: MenuBarHiddenScreenRecordingPermission.isGranted()
)
}
) {
self.localization = localization
let store = MenuBarHiddenStore(storage: context.storage)
self.manager = MenuBarHiddenManager(
store: store,
localization: localization,
permissionProvider: permissionProvider
)
self.observer = MenuBarHiddenObserver()
observer.onRefresh = { [weak self] reason in
self?.manager.refresh(reason: reason)
}
observer.onDraggingChanged = { [weak self] isDragging, startLocation in
self?.manager.setDraggingMenuBarItem(isDragging, startLocation: startLocation)
}
manager.$snapshot
.receive(on: DispatchQueue.main)
.sink { [weak self] snap in
self?.snapshot = snap
self?.onStateChange?()
}
.store(in: &cancellables)
manager.$permissions
.receive(on: DispatchQueue.main)
.sink { [weak self] perms in
self?.permissions = perms
self?.onStateChange?()
}
.store(in: &cancellables)
}
// MARK: - Lifecycle
func activate() {
guard !isActivated else { return }
isActivated = true
manager.activate()
updateObservation()
}
func deactivate() {
isActivated = false
observer.stop()
manager.deactivate()
popupPanel?.orderOut(nil)
popupPanel = nil
}
#if DEBUG
func replaceSnapshotForTesting(
visibleItems: [MenuBarItem] = [],
hiddenItems: [MenuBarItem] = [],
alwaysHiddenItems: [MenuBarItem] = []
) {
let permissions = MenuBarHiddenPermissionsStatus(
hasAccessibility: true,
hasScreenRecording: true
)
manager.replaceSnapshotForTesting(
visibleItems: visibleItems,
hiddenItems: hiddenItems,
alwaysHiddenItems: alwaysHiddenItems,
permissions: permissions
)
snapshot = MenuBarHiddenSnapshot(
visibleItems: visibleItems,
hiddenItems: hiddenItems,
alwaysHiddenItems: alwaysHiddenItems,
permissions: permissions
)
self.permissions = permissions
}
#endif
// MARK: - Forwarded state / actions
var isEnabled: Bool {
get { manager.isEnabled }
set {
manager.isEnabled = newValue
updateObservation()
}
}
var isAlwaysHiddenEnabled: Bool {
get { manager.isAlwaysHiddenEnabled }
set {
manager.isAlwaysHiddenEnabled = newValue
objectWillChange.send()
onStateChange?()
}
}
var showsHiddenIconsInPanel: Bool {
get { manager.showsHiddenIconsInPanel }
set {
manager.showsHiddenIconsInPanel = newValue
objectWillChange.send()
onStateChange?()
}
}
var canShowHiddenIconsInPanel: Bool {
manager.canShowHiddenIconsInPanel
}
func setSettingsVisible(_ visible: Bool) {
isSettingsVisible = visible
manager.setSettingsVisible(visible)
if visible {
manager.refreshPermissions()
}
updateObservation()
}
func setHiddenIconsPanelVisible(_ visible: Bool) {
isHiddenIconsPanelVisible = visible
manager.setHiddenIconsPanelVisible(visible)
if visible {
manager.refreshPermissions()
}
updateObservation()
}
func refreshPermissions() {
manager.refreshPermissions()
}
func currentPermissions() -> MenuBarHiddenPermissionsStatus {
manager.currentPermissions()
}
func moveItem(
id: MenuBarItemTag,
to section: MenuBarHiddenSection,
placement: MenuBarHiddenMovePlacement
) {
manager.moveItem(id: id, to: section, placement: placement)
}
func clickItem(_ item: MenuBarItem, button: CGMouseButton) {
manager.clickItem(item, button: button)
}
func clickItemAfterPopupCloses(_ item: MenuBarItem, button: CGMouseButton) {
let panel = popupPanel
closePopup()
Task { @MainActor [weak self] in
await panel?.waitUntilClosed(timeout: .milliseconds(200))
self?.manager.clickItem(item, button: button)
}
}
// MARK: - Popup
func showPopup(anchor: NSRect?) {
guard permissions.canManageItems else { return }
let panel = popupPanel ?? MenuBarHiddenPopupPanel(controller: self)
popupPanel = panel
panel.show(anchor: anchor)
setHiddenIconsPanelVisible(true)
}
func closePopup() {
popupPanel?.orderOut(nil)
setHiddenIconsPanelVisible(false)
if popupPanel != nil {
updateObservation()
}
}
private func updateObservation() {
guard isActivated else {
observer.stop()
return
}
let uiVisible = isSettingsVisible || isHiddenIconsPanelVisible
observer.start()
if uiVisible {
observer.startPolling()
} else {
observer.stopPolling()
}
}
// MARK: - Derived display strings
var componentSubtitle: String {
guard permissions.canManageItems else { return "" }
let count = snapshot.hiddenItems.count + snapshot.alwaysHiddenItems.count
switch count {
case 0:
return localization.string("component.subtitle.empty", defaultValue: "暂无隐藏图标")
case 1:
return localization.format("component.subtitle.count.singular", defaultValue: "%d 个隐藏图标", count)
default:
return localization.format("component.subtitle.count", defaultValue: "%d 个隐藏图标", count)
}
}
var panelSubtitle: String {
if isEnabled {
return localization.string("panel.subtitle.enabled", defaultValue: "已启用")
}
return localization.string("panel.subtitle.disabled", defaultValue: "已关闭")
}
}