Presentation fixes: - Mirror the in-progress pen/highlighter stroke to the audience window live (new 'inkLive' channel) so highlights appear as they are drawn, not only after the pen lifts. - Cover the macOS menu bar on the beamer: raise the audience window above .mainMenu level so the Apple/Wi-Fi strip no longer shows during a presentation. Styling no longer lives in the file: - generateDeck no longer embeds the ThemeProfile; a saved .md holds only content. The profile is inlined only for the transient audience-window payload (inlineStyleProfile: true), never to disk. - On open, the app applies the active style profile (FileService.openDeck / activeProfileFor, DeckNotifier.loadDeck); applyMarkdown preserves the current profile. Quality pass / tests green: - Complete the consent-screen translations (English plus 7 missing strings per other language). - Pass the consent gate in widget/ui-scale tests by seeding the consent key, so the app shell renders. - Update markdown round-trip tests for the new default and add coverage for live stroke streaming and styling-free saves. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
155 lines
5.6 KiB
Swift
155 lines
5.6 KiB
Swift
import Cocoa
|
|
import FlutterMacOS
|
|
import Foundation
|
|
|
|
typealias WindowId = String
|
|
|
|
extension WindowId {
|
|
static func generate() -> WindowId {
|
|
return UUID().uuidString
|
|
}
|
|
}
|
|
|
|
class CustomWindow: NSWindow {
|
|
|
|
init(configuration: WindowConfiguration) {
|
|
super.init(
|
|
contentRect: NSRect(x: 10, y: 10, width: 800, height: 600),
|
|
styleMask: [.miniaturizable, .closable, .titled, .resizable], backing: .buffered,
|
|
defer: false)
|
|
|
|
self.isReleasedWhenClosed = false
|
|
}
|
|
|
|
deinit {
|
|
debugPrint("Child window deinit")
|
|
}
|
|
|
|
}
|
|
|
|
class FlutterWindow: NSObject {
|
|
let windowId: WindowId
|
|
let windowArgument: String
|
|
private(set) var window: NSWindow
|
|
private var channel: FlutterMethodChannel?
|
|
|
|
private var willBecomeActiveObserver: NSObjectProtocol?
|
|
private var didResignActiveObserver: NSObjectProtocol?
|
|
private var closeObserver: NSObjectProtocol?
|
|
|
|
init(windowId: WindowId, windowArgument: String, window: NSWindow) {
|
|
self.windowId = windowId
|
|
self.windowArgument = windowArgument
|
|
self.window = window
|
|
super.init()
|
|
|
|
willBecomeActiveObserver = NotificationCenter.default.addObserver(
|
|
forName: NSApplication.willBecomeActiveNotification,
|
|
object: nil,
|
|
queue: .main
|
|
) { [weak self] notification in
|
|
self?.didChangeOcclusionState(notification)
|
|
}
|
|
|
|
didResignActiveObserver = NotificationCenter.default.addObserver(
|
|
forName: NSApplication.didResignActiveNotification,
|
|
object: nil,
|
|
queue: .main
|
|
) { [weak self] notification in
|
|
self?.didChangeOcclusionState(notification)
|
|
}
|
|
|
|
closeObserver = NotificationCenter.default.addObserver(
|
|
forName: NSWindow.willCloseNotification, object: window, queue: .main
|
|
) { [windowId] _ in
|
|
MultiWindowManager.shared.removeWindow(windowId: windowId)
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
if let willBecomeActiveObserver = willBecomeActiveObserver {
|
|
NotificationCenter.default.removeObserver(willBecomeActiveObserver)
|
|
}
|
|
if let didResignActiveObserver = didResignActiveObserver {
|
|
NotificationCenter.default.removeObserver(didResignActiveObserver)
|
|
}
|
|
if let closeObserver = closeObserver {
|
|
NotificationCenter.default.removeObserver(closeObserver)
|
|
}
|
|
}
|
|
|
|
@objc func didChangeOcclusionState(_ notification: Notification) {
|
|
if let controller = window.contentViewController as? FlutterViewController {
|
|
controller.engine.handleDidChangeOcclusionState(notification)
|
|
}
|
|
}
|
|
|
|
func setChannel(_ channel: FlutterMethodChannel) {
|
|
self.channel = channel
|
|
}
|
|
|
|
func notifyWindowEvent(_ event: String, data: [String: Any]) {
|
|
if let channel = channel {
|
|
channel.invokeMethod(event, arguments: data)
|
|
} else {
|
|
debugPrint("Channel not set for window \(windowId), cannot notify event \(event)")
|
|
}
|
|
}
|
|
|
|
func handleWindowMethod(method: String, arguments: Any?, result: @escaping FlutterResult) {
|
|
let args = arguments as? [String: Any?]
|
|
switch method {
|
|
case "window_show":
|
|
window.makeKeyAndOrderFront(nil)
|
|
window.setIsVisible(true)
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
result(nil)
|
|
case "window_hide":
|
|
window.orderOut(nil)
|
|
result(nil)
|
|
case "window_close":
|
|
window.close()
|
|
result(nil)
|
|
case "window_setFrame":
|
|
if let x = args?["x"] as? Double,
|
|
let y = args?["y"] as? Double,
|
|
let w = args?["width"] as? Double,
|
|
let h = args?["height"] as? Double
|
|
{
|
|
window.setFrame(NSRect(x: x, y: y, width: w, height: h), display: true)
|
|
}
|
|
result(nil)
|
|
case "window_coverScreen":
|
|
// Make this window a borderless surface that fills an entire screen —
|
|
// used to show the audience slide fullscreen on the beamer while the
|
|
// main (presenter) window stays on the laptop. We deliberately do not
|
|
// make it the key window, so the keyboard stays with the presenter.
|
|
let external = (args?["external"] as? Bool) ?? true
|
|
let screens = NSScreen.screens
|
|
var target: NSScreen? = NSScreen.main
|
|
if external, let ext = screens.first(where: { $0 != NSScreen.main }) {
|
|
target = ext
|
|
}
|
|
if let screen = target ?? screens.first {
|
|
window.styleMask = [.borderless]
|
|
// Raise above the menu bar (.mainMenu == 24) so the macOS menu
|
|
// bar and notch area on the beamer are covered by the slide; a
|
|
// plain .normal window would sit *under* the menu bar and leave
|
|
// the Apple/Wi-Fi strip visible during the presentation. We stay
|
|
// below .popUpMenu (101) so context menus still show on top.
|
|
window.level = .statusBar
|
|
// Keep the cover in place across Spaces/displays without ever
|
|
// stealing keyboard focus from the presenter window.
|
|
window.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
|
|
window.isOpaque = true
|
|
window.setFrame(screen.frame, display: true)
|
|
window.orderFrontRegardless()
|
|
window.setIsVisible(true)
|
|
}
|
|
result(nil)
|
|
default:
|
|
result(FlutterError(code: "-1", message: "unknown method \(method)", details: nil))
|
|
}
|
|
}
|
|
|
|
}
|