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

120 lines
4.6 KiB
Swift

import AppKit
import MacToolsPluginKit
import SwiftUI
@MainActor
final class TranslatorPanelController: TranslatorPanelControlling {
private static let panelSize = NSSize(width: 606, height: 380)
private static let screenPadding: CGFloat = 16
private var panelWindow: TranslatorPanelWindow?
private var lastFrame: NSRect?
private let model = TranslatorPanelModel()
private let localization: PluginLocalization
var onAction: ((TranslatorPanelAction) -> Void)?
init(localization: PluginLocalization = PluginLocalization(bundle: .main)) {
self.localization = localization
}
func show(snapshot: TranslatorPanelSnapshot) {
model.snapshot = snapshot
let panel = panelWindow ?? makePanel()
panelWindow = panel
panel.setFrame(clampedFrame(for: panel.frame, panel: panel), display: true)
panel.orderFrontRegardless()
// Keep the panel non-key while capturing so the frontmost app keeps keyboard focus
// for AX selection and simulated Cmd-C. Result states become key to support buttons,
// text selection, and Esc close without activating the whole app.
if snapshot.phase != .capturing {
panel.makeKey()
}
}
func update(snapshot: TranslatorPanelSnapshot) {
model.snapshot = snapshot
}
func close() {
guard let panelWindow else { return }
lastFrame = panelWindow.frame
panelWindow.performProgrammaticClose {
panelWindow.orderOut(nil)
}
}
private func makePanel() -> TranslatorPanelWindow {
let panel = TranslatorPanelWindow(size: Self.panelSize)
panel.onCloseRequest = { [weak self] in
self?.onAction?(.close)
}
let effectView = NSVisualEffectView(frame: NSRect(origin: .zero, size: Self.panelSize))
effectView.material = .popover
effectView.blendingMode = .behindWindow
effectView.state = .active
effectView.maskImage = Self.roundedMaskImage(size: Self.panelSize, cornerRadius: 18)
let rootView = TranslatorPanelHostView(model: model, localization: localization) { [weak self] action in
self?.onAction?(action)
}
let hostingView = NSHostingView(rootView: rootView)
hostingView.frame = effectView.bounds
hostingView.autoresizingMask = [.width, .height]
effectView.addSubview(hostingView)
panel.contentView = effectView
panel.setContentSize(Self.panelSize)
let initialFrame = lastFrame ?? defaultFrame(for: panel)
panel.setFrame(clampedFrame(for: initialFrame, panel: panel), display: true)
return panel
}
private func defaultFrame(for panel: NSPanel) -> NSRect {
let visibleFrame = panel.screen?.visibleFrame ?? NSScreen.main?.visibleFrame ?? .zero
let x = visibleFrame.maxX - Self.panelSize.width - Self.screenPadding
let y = visibleFrame.maxY - Self.panelSize.height - Self.screenPadding
return clampedFrame(
NSRect(origin: CGPoint(x: x, y: y), size: Self.panelSize),
within: visibleFrame
)
}
private func clampedFrame(_ frame: NSRect, within visibleFrame: NSRect) -> NSRect {
guard !visibleFrame.isEmpty else { return frame }
let minX = visibleFrame.minX + Self.screenPadding
let maxX = visibleFrame.maxX - frame.width - Self.screenPadding
let minY = visibleFrame.minY + Self.screenPadding
let maxY = visibleFrame.maxY - frame.height - Self.screenPadding
let x = maxX >= minX ? min(max(frame.minX, minX), maxX) : visibleFrame.midX - frame.width / 2
let y = maxY >= minY ? min(max(frame.minY, minY), maxY) : visibleFrame.midY - frame.height / 2
return NSRect(origin: CGPoint(x: x, y: y), size: frame.size)
}
private func clampedFrame(for frame: NSRect, panel: NSPanel) -> NSRect {
let visibleFrame = panel.screen?.visibleFrame ?? NSScreen.main?.visibleFrame ?? .zero
return clampedFrame(frame, within: visibleFrame)
}
private static func roundedMaskImage(size: NSSize, cornerRadius: CGFloat) -> NSImage {
let image = NSImage(size: size, flipped: false) { rect in
NSColor.black.setFill()
NSBezierPath(roundedRect: rect, xRadius: cornerRadius, yRadius: cornerRadius).fill()
return true
}
image.capInsets = NSEdgeInsets(
top: cornerRadius,
left: cornerRadius,
bottom: cornerRadius,
right: cornerRadius
)
image.resizingMode = .stretch
return image
}
}