Files
plezy/macos/Runner/WindowUtilsPlugin.swift
T

235 lines
8.0 KiB
Swift

import Cocoa
import FlutterMacOS
// MARK: - ForwardingView
// A view that forwards mouse events to the Flutter view controller
class ForwardingView: NSView {
weak var flutterViewController: NSViewController?
override func mouseDown(with event: NSEvent) {
flutterViewController?.mouseDown(with: event)
}
override func mouseUp(with event: NSEvent) {
flutterViewController?.mouseUp(with: event)
}
}
// MARK: - ForwardingToolbar
// A custom toolbar that forwards mouse events from the toolbar area to Flutter
class ForwardingToolbar: NSToolbar, NSToolbarDelegate {
let flutterViewController: NSViewController
init(flutterViewController: NSViewController) {
self.flutterViewController = flutterViewController
super.init(identifier: "ForwardingToolbar")
self.delegate = self
self.showsBaselineSeparator = false
// Prevent toolbar customization UI (the "rounded box")
self.allowsUserCustomization = false
self.allowsExtensionItems = false
if #available(macOS 15.0, *) {
self.allowsDisplayModeCustomization = false
}
}
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
[.flexibleSpace, NSToolbarItem.Identifier("ForwardingItem")]
}
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
toolbarDefaultItemIdentifiers(toolbar)
}
func toolbar(
_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
willBeInsertedIntoToolbar flag: Bool
) -> NSToolbarItem? {
if itemIdentifier == NSToolbarItem.Identifier("ForwardingItem") {
let item = NSToolbarItem(itemIdentifier: itemIdentifier)
item.isBordered = false // Remove the rounded box appearance
let view = ForwardingView()
view.flutterViewController = flutterViewController
view.widthAnchor.constraint(lessThanOrEqualToConstant: 100000).isActive = true
view.widthAnchor.constraint(greaterThanOrEqualToConstant: 1).isActive = true
item.view = view
return item
}
return nil
}
}
// MARK: - WindowUtilsPlugin
class WindowUtilsPlugin: NSObject, FlutterPlugin {
private static var instance: WindowUtilsPlugin?
private var channel: FlutterMethodChannel?
private weak var window: NSWindow?
private var windowDelegate: WindowDelegate?
private var originalButtonConstraints: [NSWindow.ButtonType: [NSLayoutConstraint]] = [:]
// Centralized traffic light positions - the single source of truth
private static let customButtonPositions: [(NSWindow.ButtonType, CGPoint)] = [
(.closeButton, CGPoint(x: 20, y: 21)),
(.miniaturizeButton, CGPoint(x: 40, y: 21)),
(.zoomButton, CGPoint(x: 60, y: 21)),
]
static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(
name: "com.plezy/window_utils",
binaryMessenger: registrar.messenger
)
let instance = WindowUtilsPlugin()
instance.channel = channel
registrar.addMethodCallDelegate(instance, channel: channel)
self.instance = instance
}
static func setWindow(_ window: NSWindow) {
instance?.window = window
}
/// Apply custom traffic light positions. Called by MainFlutterWindow on startup.
static func setInitialTrafficLightPositions() {
guard let instance = instance, let window = instance.window else { return }
instance.applyTrafficLightPositions(custom: true, window: window)
}
/// Apply traffic light positions. Called by WindowDelegate during fullscreen transitions.
static func setTrafficLightPositions(custom: Bool, window: NSWindow) {
guard let instance = instance else { return }
instance.applyTrafficLightPositions(custom: custom, window: window)
}
private func applyTrafficLightPositions(custom: Bool, window: NSWindow) {
if custom {
for (buttonType, offset) in WindowUtilsPlugin.customButtonPositions {
overrideButtonPosition(window: window, buttonType: buttonType, offset: offset)
}
} else {
for (buttonType, _) in WindowUtilsPlugin.customButtonPositions {
resetButtonPosition(window: window, buttonType: buttonType)
}
}
}
func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let window = window else {
result(FlutterError(code: "NO_WINDOW", message: "Window not available", details: nil))
return
}
switch call.method {
case "initialize":
let args = call.arguments as? [String: Any]
let enableWindowDelegate = args?["enableWindowDelegate"] as? Bool ?? false
initialize(window: window, enableWindowDelegate: enableWindowDelegate)
result(nil)
case "setTrafficLightsVisible":
let args = call.arguments as? [String: Any]
let visible = args?["visible"] as? Bool ?? true
for buttonType in [NSWindow.ButtonType.closeButton, .miniaturizeButton, .zoomButton] {
window.standardWindowButton(buttonType)?.isHidden = !visible
}
result(nil)
case "enterFullscreen":
if !window.styleMask.contains(.fullScreen) {
window.toggleFullScreen(nil)
}
result(nil)
case "exitFullscreen":
if window.styleMask.contains(.fullScreen) {
window.toggleFullScreen(nil)
}
result(nil)
case "isFullscreen":
result(window.styleMask.contains(.fullScreen))
default:
result(FlutterMethodNotImplemented)
}
}
private func initialize(window: NSWindow, enableWindowDelegate: Bool) {
self.window = window
if enableWindowDelegate {
let delegate = WindowDelegate()
delegate.channel = channel
delegate.window = window
windowDelegate = delegate
window.delegate = delegate
}
}
private func withButton(
_ buttonType: NSWindow.ButtonType,
in window: NSWindow,
action: (NSButton, NSView) -> Void
) {
guard let button = window.standardWindowButton(buttonType),
let superview = button.superview
else { return }
action(button, superview)
}
private func positionConstraints(for button: NSButton, in superview: NSView)
-> [NSLayoutConstraint]
{
superview.constraints.filter { constraint in
((constraint.firstItem as? NSButton) == button
|| (constraint.secondItem as? NSButton) == button)
&& (constraint.firstAttribute == .left || constraint.firstAttribute == .leading
|| constraint.firstAttribute == .top || constraint.firstAttribute == .centerY)
}
}
private func overrideButtonPosition(
window: NSWindow, buttonType: NSWindow.ButtonType, offset: CGPoint
) {
withButton(buttonType, in: window) { button, superview in
// Store original constraints if not already stored
if originalButtonConstraints[buttonType] == nil {
let constraints = superview.constraints.filter { constraint in
(constraint.firstItem as? NSButton) == button
|| (constraint.secondItem as? NSButton) == button
}
originalButtonConstraints[buttonType] = constraints
}
// Remove existing position constraints
superview.removeConstraints(positionConstraints(for: button, in: superview))
button.translatesAutoresizingMaskIntoConstraints = false
// Add new positioning constraints
superview.addConstraints([
button.leftAnchor.constraint(equalTo: superview.leftAnchor, constant: offset.x),
button.topAnchor.constraint(equalTo: superview.topAnchor, constant: offset.y),
])
superview.layoutSubtreeIfNeeded()
}
}
private func resetButtonPosition(window: NSWindow, buttonType: NSWindow.ButtonType) {
withButton(buttonType, in: window) { button, superview in
// Remove custom constraints
superview.removeConstraints(positionConstraints(for: button, in: superview))
// Restore original constraints if we have them
if let originalConstraints = originalButtonConstraints[buttonType] {
superview.addConstraints(originalConstraints)
originalButtonConstraints.removeValue(forKey: buttonType)
}
button.translatesAutoresizingMaskIntoConstraints = true
superview.layoutSubtreeIfNeeded()
}
}
}