Files
xianyan/macos/Runner/MainFlutterWindow.swift
Developer 88a3f6d65f feat: 新增仪表盘页面与macOS多项优化
1. 新增TDashboard翻译类型与多语言文案
2. 完善macOS权限管理与Impeller渲染适配
3. 更新服务器部署配置与协议文件上传脚本
4. 修复翻译导入服务与根类型编译问题
2026-06-26 06:34:05 +08:00

702 lines
31 KiB
Swift
Raw 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.
/// ============================================================
/// APP macOS
/// : 2026-06-02
/// : 2026-06-26
/// : Info.plist FLTEnableImpeller Impeller
/// apps.xy.xianyan/macos MethodChannel
/// : 1. FlutterDartProject macOS commandLineArguments
/// iOS
/// 2. Impeller setenv("FLUTTER_ENGINE_SWITCH_0")
/// macOS embedder 使 debug
/// macOS Info.plist FLTEnableImpeller key true
/// 3. currentImpellerEnabled true FLTEnableImpeller=true
/// 4. setenv Flutter
/// 5. hideSystemTrafficButtons(of:)
/// 6. Intel Mac Apple Silicon
/// ============================================================
import Cocoa
import FlutterMacOS
import macos_window_utils
class MainFlutterWindow: NSWindow, NSTouchBarDelegate {
// ============================================================
// MARK: -
// ============================================================
/// Touch Bar + 退
private var platformChannel: FlutterMethodChannel?
/// Touch Bar [{label: "", action: "bold"}, ...]
private var touchBarItems: [[String: String]] = []
/// Impeller
///
/// awakeFromNib shouldEnableImpeller()
/// Flutter 使 Impeller
/// UserDefaults
/// UserDefaults
///
static var currentImpellerEnabled: Bool = false
// ============================================================
// MARK: - CPU
// ============================================================
/// Intel Macx86_64
///
/// Intel Mac GPU + NSVisualEffectView
///
/// Intel Mac 使
private static func isIntelMac() -> Bool {
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
let machine = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value)))
}
return machine == "x86_64"
}
/// Impeller
///
/// Impeller Flutter Metal
/// Intel Mac /
///
///
/// - Apple Silicon (arm64): Impeller Metal
/// - Intel Mac (x86_64): Metal bug
///
///
///
/// - Returns: Impeller
private static func shouldEnableImpeller() -> Bool {
let defaults = UserDefaults.standard
//
// UserDefaults.bool key false""""
// object(forKey:) key
if defaults.object(forKey: "xianyan_enable_impeller") != nil {
return defaults.bool(forKey: "xianyan_enable_impeller")
}
// Apple Silicon Intel Mac
return !Self.isIntelMac()
}
override func awakeFromNib() {
let windowFrame = self.frame
// ============================================================
// Impeller
// ============================================================
// macOS Impeller Info.plist FLTEnableImpeller key
// key Info.plist true Impeller
//
//
// 1. FlutterDartProject.commandLineArguments macOS iOS
// 2. setenv("FLUTTER_ENGINE_SWITCH_0", ...) macOS embedder
// 使 debug/profile Windows/Linux embedder
//
// FLTEnableImpeller
// Impeller Info.plist
// UI Flutter
//
// Info.plist FLTEnableImpeller=true Mac Impeller
// Intel Mac Info.plist key false
let enableImpeller = Self.shouldEnableImpeller()
let impellerArg = enableImpeller ? "--enable-impeller=true" : "--enable-impeller=false"
// macOS Flutter
setenv("FLUTTER_ENGINE_SWITCH_0", impellerArg, 1)
NSLog("[MainFlutterWindow] Impeller 用户设置: enabled=\(enableImpeller), arg=\(impellerArg), arch=\(Self.isIntelMac() ? "x86_64" : "arm64")")
NSLog("[MainFlutterWindow] Impeller 实际状态: 由 Info.plist FLTEnableImpeller=true 控制(始终启用)")
// Impeller
// FLTEnableImpeller=true Info.plist 使 Impeller
// Dart getImpellerRunningStatus
Self.currentImpellerEnabled = true
// FlutterViewController Info.plist FLTEnableImpeller
let flutterViewController = FlutterViewController()
// 使 MacOSWindowUtilsViewController FlutterViewController
// macos_window_utils setMaterial/setNSVisualEffectViewState/
// addVisualEffectSubview
// `contentViewController as! MacOSWindowUtilsViewController`
// 使 FlutterViewController contentViewController
// "Could not cast value of type 'FlutterViewController' to
// 'MacOSWindowUtilsViewController'"
// MacOSWindowUtilsViewController.loadView() NSVisualEffectView
// addChild(flutterViewController)
//
// Mac Intel Mac使 NSVisualEffectView
// macOS
let macOSWindowUtilsViewController = MacOSWindowUtilsViewController(
flutterViewController: flutterViewController
)
self.contentViewController = macOSWindowUtilsViewController
self.setFrame(windowFrame, display: true)
// Flutter Impeller
// macos_window_utils setWindowBackgroundColorToClear()
// [NSWindow setBackgroundColor:] NSThemeFrame._updateBackdropView removeFromSuperview
// Impeller raster 线 SetupRenderPass Release
// Dart
//
// titlebarAppearsTransparent / titleVisibility / fullSizeContentView
// Mac Flutter DesktopWindowTitleBar
// "" + Flutter
//
// isOpaque / backgroundColor Mac
// NSVisualEffectView
self.titlebarAppearsTransparent = true
self.titleVisibility = .hidden
if !self.styleMask.contains(.fullSizeContentView) {
self.styleMask.insert(.fullSizeContentView)
}
self.isOpaque = false
self.backgroundColor = NSColor.clear
// MainFlutterWindowManipulator.start(mainFlutterWindow: self)
// macos_window_utils mainFlutterWindow
// Dart WindowManipulator.initialize() reset()
// mainFlutterWindow == nil start(nil)
// setWindowBackgroundColorToDefaultColor() [NSWindow setBackgroundColor:]
// NSThemeFrame._updateBackdropView removeFromSuperview
// Impeller raster 线 SetupRenderPass EXC_BAD_ACCESS
//
// start() showTitle()/makeTitlebarOpaque()/disableFullSizeContentView()/
// setWindowBackgroundColorToDefaultColor() start()
// Flutter Impeller raster 线
// [NSWindow setBackgroundColor:] 线
//
// mainFlutterWindow: selfstart() isProvidedWindow=true
// configureMainFlutterWindow() contentViewController
// MacOSWindowUtilsViewController FlutterViewController
// configure
//
// Mac Intel Mac start() macos_window_utils
// mainFlutterWindow
MainFlutterWindowManipulator.start(mainFlutterWindow: self)
// start()
// Mac start()
self.titlebarAppearsTransparent = true
self.titleVisibility = .hidden
if !self.styleMask.contains(.fullSizeContentView) {
self.styleMask.insert(.fullSizeContentView)
}
self.isOpaque = false
self.backgroundColor = NSColor.clear
// 绿
// Flutter DesktopWindowTitleBar macOS 绿
// ""
// setDarkMode setDarkMode
//
// Intel Mac Flutter
hideSystemTrafficButtons(of: self)
RegisterGeneratedPlugins(registry: flutterViewController)
// Flutter MethodChannel MissingPluginException
registerPlatformChannel(controller: flutterViewController)
// MethodChannelapps.xy.xianyan/macos.app
// Dart getCpuArchitecture MissingPluginException
AppDelegate.registerAppChannel(controller: flutterViewController)
// 退退
// macOS 退
NotificationCenter.default.addObserver(
self,
selector: #selector(handleFullScreenExit(_:)),
name: NSWindow.didExitFullScreenNotification,
object: self
)
// mainMenu menuItem identifierXIB objectId
// 便 setMenuLanguage identifier .strings
// NSApp.mainMenu XIB awakeFromNib mainMenu
self.assignMenuItemIdentifiers()
super.awakeFromNib()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// ============================================================
// MARK: - 退
// ============================================================
/// 退
///
/// Enter Full Screen
/// 绿退
///
/// macos_window_utils `setWindowBackgroundColorToClear()`
/// `isOpaque=false` + `backgroundColor=NSColor.clear` macOS
/// `isOpaque=true`, `backgroundColor=`
/// 退
/// `titlebarAppearsTransparent`
///
/// `NSWindow.didExitFullScreenNotification`
/// 退 `isOpaque=false` + `backgroundColor=clear` +
/// `titlebarAppearsTransparent=true` MethodChannel Dart
/// NSVisualEffectView
@objc private func handleFullScreenExit(_ notification: Notification) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
// Mac
// macOS 退
self.titlebarAppearsTransparent = true
self.isOpaque = false
self.backgroundColor = NSColor.clear
// 绿
// macOS standardWindowButton
// 退 Flutter
self.hideSystemTrafficButtons(of: self)
//
self.contentView?.needsDisplay = true
// Dart NSVisualEffectView
self.platformChannel?.invokeMethod("onFullScreenExited", arguments: nil)
}
}
// ============================================================
// MARK: - MethodChannel
// ============================================================
/// apps.xy.xianyan/macos Dart MacosPlatformService
private func registerPlatformChannel(controller: FlutterViewController) {
let channel = FlutterMethodChannel(
name: "apps.xy.xianyan/macos",
binaryMessenger: controller.engine.binaryMessenger
)
platformChannel = channel
channel.setMethodCallHandler { [weak self] (call, result) in
guard let self = self else {
result(FlutterError(code: "window_deallocated",
message: "MainFlutterWindow 已释放",
details: nil))
return
}
switch call.method {
// ---------- ----------
case "setDarkMode":
let isDark = (call.arguments as? Bool) ?? false
self.setDarkMode(isDark: isDark)
result(nil)
case "getSystemAppearance":
result(self.getSystemAppearance())
// ---------- ----------
case "setWindowTitle":
let title = (call.arguments as? String) ?? ""
self.title = title
result(nil)
case "setTitleBarTransparent":
let transparent = (call.arguments as? Bool) ?? false
self.titlebarAppearsTransparent = transparent
result(nil)
case "setTitleBarStyle":
let style = (call.arguments as? String) ?? "auto"
self.setTitleBarStyle(style: style)
result(nil)
case "setToolbarVisible":
let visible = (call.arguments as? Bool) ?? true
if visible {
self.toolbar?.isVisible = true
} else {
self.toolbar?.isVisible = false
}
result(nil)
case "setFullscreen":
let fullscreen = (call.arguments as? Bool) ?? false
self.setFullscreen(fullscreen: fullscreen)
result(nil)
case "isFullscreen":
result(self.styleMask.contains(.fullScreen))
case "setMinSize":
guard let args = call.arguments as? [String: Any],
let width = args["width"] as? Double,
let height = args["height"] as? Double else {
result(FlutterError(code: "invalid_args",
message: "setMinSize 需要 {width, height}",
details: nil))
return
}
self.minSize = NSSize(width: width, height: height)
result(nil)
// ---------- ----------
case "performHapticFeedback":
let type = (call.arguments as? String) ?? "generic"
self.performHapticFeedback(type: type)
result(nil)
// ---------- Touch Bar ----------
case "setTouchBarItems":
let args = call.arguments as? [String: Any] ?? [:]
let rawItems = args["items"] as? [[String: Any]] ?? []
self.touchBarItems = rawItems.compactMap { item in
guard let label = item["label"] as? String,
let action = item["action"] as? String else { return nil }
return ["label": label, "action": action]
}
// Touch Bar
self.touchBar = nil
if !self.touchBarItems.isEmpty {
self.touchBar = self.makeTouchBar()
}
result(nil)
// ---------- NSSharingService ----------
case "showShareSheet":
let args = call.arguments as? [String: Any] ?? [:]
var shareItems: [Any] = []
if let text = args["text"] as? String {
shareItems.append(text)
}
if let urlString = args["url"] as? String, let url = URL(string: urlString) {
shareItems.append(url)
}
if let imageBytes = args["imageBytes"] as? FlutterStandardTypedData,
let image = NSImage(data: imageBytes.data) {
shareItems.append(image)
}
guard !shareItems.isEmpty else {
result(FlutterError(code: "no_content",
message: "没有可分享的内容",
details: nil))
return
}
let picker = NSSharingServicePicker(items: shareItems)
if let view = self.contentView {
picker.show(relativeTo: view.bounds, of: view, preferredEdge: .minY)
}
result(nil)
// ---------- ----------
case "setMenuLanguage":
let args = call.arguments as? [String: Any] ?? [:]
let languageId = (args["languageId"] as? String) ?? "system"
self.setMenuLanguage(languageId: languageId)
result(nil)
default:
result(FlutterMethodNotImplemented)
}
}
}
// ============================================================
// MARK: -
// ============================================================
/// NSWindow.appearance
///
/// titlebarAppearsTransparent = true
/// Flutter DesktopWindowTitleBar
private func setDarkMode(isDark: Bool) {
let appearance: NSAppearance = isDark
? NSAppearance(named: .darkAqua)!
: NSAppearance(named: .aqua)!
NSApp.appearance = appearance
//
for window in NSApplication.shared.windows {
window.appearance = appearance
// Flutter
window.titlebarAppearsTransparent = true
// 0
if let contentView = window.contentView {
contentView.superview?.wantsLayer = true
}
}
// 绿
// hideSystemTrafficButtons()
hideSystemTrafficButtons()
}
/// 绿closeButton/miniaturizeButton/zoomButton
///
/// Flutter DesktopWindowTitleBar macOS 绿
/// ""
///
///
/// 1. `awakeFromNib`
/// 2. `setDarkMode`
/// 3. `handleFullScreenExit` 退
///
/// - Parameter window: nil `NSApplication.shared.windows`
/// setDarkMode
private func hideSystemTrafficButtons(of window: NSWindow? = nil) {
let windows = window.map { [$0] } ?? NSApplication.shared.windows
for win in windows {
win.standardWindowButton(.closeButton)?.isHidden = true
win.standardWindowButton(.miniaturizeButton)?.isHidden = true
win.standardWindowButton(.zoomButton)?.isHidden = true
}
}
/// light / dark
private func getSystemAppearance() -> String {
let appearance = NSApp.effectiveAppearance.bestMatch(from: [
NSAppearance.Name.darkAqua,
NSAppearance.Name.aqua,
]) ?? NSAppearance.Name.aqua
return appearance == .darkAqua ? "dark" : "light"
}
// ============================================================
// MARK: -
// ============================================================
/// auto / light / dark
private func setTitleBarStyle(style: String) {
let appearance: NSAppearance?
switch style {
case "light":
appearance = NSAppearance(named: .aqua)
case "dark":
appearance = NSAppearance(named: .darkAqua)
default: // auto
appearance = nil
}
for window in NSApplication.shared.windows {
window.appearance = appearance
}
}
///
private func setFullscreen(fullscreen: Bool) {
let isCurrentlyFullscreen = self.styleMask.contains(.fullScreen)
if fullscreen && !isCurrentlyFullscreen {
self.toggleFullScreen(nil)
} else if !fullscreen && isCurrentlyFullscreen {
self.toggleFullScreen(nil)
}
}
// ============================================================
// MARK: -
// ============================================================
/// macOS NSHapticFeedbackManager
private func performHapticFeedback(type: String) {
let feedbackLevel: NSHapticFeedbackManager.FeedbackPattern
switch type {
case "alignment":
feedbackLevel = .alignment
case "levelChange":
feedbackLevel = .levelChange
default:
feedbackLevel = .generic
}
NSHapticFeedbackManager.defaultPerformer.perform(
feedbackLevel,
performanceTime: .now
)
}
// ============================================================
// MARK: -
// ============================================================
/// XIB menuItem objectId
/// awakeFromNib menuItem identifier
/// separatorItem
/// MainMenu.xib menuItem
private static let menuItemIds: [String] = [
// App Menu +
"1Xt-HY-uBw", "5kV-Vb-QxS", "BOF-NM-1cW", "NMo-om-nkz",
"Olw-nP-bQN", "Vdr-fp-XzO", "Kd2-mp-pUS", "4sb-4s-VLi",
// Edit Menu +
"5QF-Oa-p0T", "dRJ-4n-Yzg", "6dh-zS-Vam", "uRl-iY-unG",
"x3v-GG-iWU", "gVA-U4-sdL", "WeT-3V-zwk", "pa3-QI-u2k",
"Ruw-6m-B2m",
// Edit > Find
"4EN-yA-p0u", "Xz5-n4-O0W", "YEy-JH-Tfz", "q09-fT-Sye",
"OwM-mh-QMV", "buJ-ug-pKt", "S0p-oC-mLd",
// Edit > Spelling and Grammar
"Dv1-io-Yv7", "HFo-cy-zxI", "hz2-CU-CR7", "rbD-Rh-wIN",
"mK6-2p-4JG", "78Y-hA-62v",
// Edit > Substitutions
"9ic-FL-obx", "z6F-FW-3nz", "9yt-4B-nSM", "hQb-2v-fYv",
"rgM-f4-ycn", "cwL-P1-jid", "tRr-pd-1PS", "HFQ-gK-NFA",
// Edit > Transformations
"2oI-Rn-ZJC", "vmV-6d-7jI", "d9M-CD-aMd", "UEZ-Bs-lqG",
// Edit > Speech
"xrE-MZ-jX0", "Ynk-f8-cLZ", "Oyz-dy-DGm",
// View Menu
"H8h-7b-M4v",
// Window Menu +
"aUF-d1-5bR", "OY7-WF-poV", "R4o-n2-Eq4", "LE2-aR-0XJ",
// Help Menu
"EPT-qC-fAb",
]
/// mainMenu menuItem identifierXIB objectId
///
/// XIB menuItem.identifier nil objectId
/// mainMenu menuItemIds objectId
/// menuItem.identifier objectId menuItem
/// separatorItemXIB separator objectId
private func assignMenuItemIdentifiers() {
var index = 0
func assign(_ menu: NSMenu) {
for item in menu.items {
if item.isSeparatorItem { continue }
if index < MainFlutterWindow.menuItemIds.count {
item.identifier = NSUserInterfaceItemIdentifier(
rawValue: MainFlutterWindow.menuItemIds[index]
)
index += 1
}
if let submenu = item.submenu {
assign(submenu)
}
}
}
guard let mainMenu = NSApp.mainMenu else {
NSLog("[assignMenuItemIdentifiers] NSApp.mainMenu 为 nil")
return
}
assign(mainMenu)
NSLog("[assignMenuItemIdentifiers] 已为 \(index) 个菜单项设置 identifier")
}
/// macOS
///
/// [languageId] ID
/// - 'system' / 'auto' 使 preferredLocalizations
/// - 'zh-CN' / 'zh-Hans'
/// - 'zh-TW' / 'zh-Hant'
/// - 'en'
/// - 'ja'
/// - 'ko'
///
///
/// 1. languageId lproj
/// 2. MainMenu.strings [String: String]
/// 3. NSApp.mainMenu menuItem.identifierXIB objectId
/// .strings key"id.title"
/// 4. "APP_NAME" CFBundleName
///
/// action
private func setMenuLanguage(languageId: String) {
// 1. lproj
let lprojName: String
switch languageId {
case "system", "auto":
// preferredLocalizations(from:) Bundle
//
let preferred = Bundle.preferredLocalizations(from: [
"en", "zh-Hans", "zh-Hant", "ja", "ko",
])
lprojName = preferred.first ?? "en"
case "zh-CN", "zh-Hans":
lprojName = "zh-Hans"
case "zh-TW", "zh-Hant":
lprojName = "zh-Hant"
case "en":
lprojName = "en"
case "ja":
lprojName = "ja"
case "ko":
lprojName = "ko"
default:
// 退
lprojName = "en"
}
// 2. MainMenu.strings
guard let stringsPath = Bundle.main.path(
forResource: "MainMenu",
ofType: "strings",
inDirectory: nil,
forLocalization: lprojName
) else {
NSLog("[setMenuLanguage] 未找到 \(lprojName).lproj/MainMenu.strings")
return
}
guard let strings = NSDictionary(contentsOfFile: stringsPath) as? [String: String] else {
NSLog("[setMenuLanguage] 加载 \(lprojName).lproj/MainMenu.strings 失败")
return
}
// 3. mainMenu
let appName = (Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String) ?? "闲言"
var updatedCount = 0
func updateMenu(_ menu: NSMenu) {
for item in menu.items {
if item.isSeparatorItem { continue }
if let identifier = item.identifier?.rawValue {
let key = "\(identifier).title"
if let localized = strings[key] {
// APP_NAME
item.title = localized.replacingOccurrences(of: "APP_NAME", with: appName)
updatedCount += 1
}
}
if let submenu = item.submenu {
updateMenu(submenu)
}
}
}
guard let mainMenu = NSApp.mainMenu else {
NSLog("[setMenuLanguage] NSApp.mainMenu 为 nil")
return
}
updateMenu(mainMenu)
NSLog("[setMenuLanguage] 菜单栏语言已切换为 \(lprojName),更新 \(updatedCount) 个菜单项")
}
// ============================================================
// MARK: - Touch Bar
// ============================================================
/// Touch Bar Touch Bar MacBook
override func makeTouchBar() -> NSTouchBar? {
guard !touchBarItems.isEmpty else { return nil }
let touchBar = NSTouchBar()
touchBar.delegate = self
touchBar.customizationIdentifier = NSTouchBar.CustomizationIdentifier("apps.xy.xianyan.touchbar")
touchBar.defaultItemIdentifiers = touchBarItems.enumerated().map {
NSTouchBarItem.Identifier("apps.xy.xianyan.touchbar.item.\($0.offset)")
}
return touchBar
}
/// NSTouchBarDelegate Touch Bar
func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? {
guard let index = Int(identifier.rawValue.components(separatedBy: ".").last ?? ""),
index < touchBarItems.count else {
return nil
}
let item = touchBarItems[index]
let button = NSButton(title: item["label"] ?? "", target: self, action: #selector(touchBarButtonClicked(_:)))
button.identifier = NSUserInterfaceItemIdentifier(rawValue: item["action"] ?? "")
let touchBarItem = NSCustomTouchBarItem(identifier: identifier)
touchBarItem.view = button
return touchBarItem
}
/// Touch Bar channel.invokeMethod("touchBarAction", action) Dart
@objc private func touchBarButtonClicked(_ sender: NSButton) {
guard let action = sender.identifier?.rawValue else { return }
platformChannel?.invokeMethod("touchBarAction", arguments: action)
}
}