refactor: replace macos_window_utils

This commit is contained in:
edde746
2025-12-27 12:38:38 +01:00
parent a698ab0930
commit 9e506d3f68
14 changed files with 453 additions and 134 deletions
+5 -52
View File
@@ -1,64 +1,17 @@
import 'package:macos_window_utils/macos_window_utils.dart';
import 'package:macos_window_utils/macos/ns_window_delegate.dart';
import 'package:macos_window_utils/macos/ns_window_button_type.dart';
import 'package:flutter/material.dart' show Offset;
import 'fullscreen_state_manager.dart'; import 'fullscreen_state_manager.dart';
import 'macos_window_delegate.dart';
/// Custom window delegate that manages titlebar configuration during fullscreen transitions /// Custom window delegate that manages fullscreen state
class FullscreenWindowDelegate extends NSWindowDelegate { /// Note: Window manipulation (toolbar, titlebar, traffic lights) is now handled
static const double _customButtonY = 21.0; /// directly in Swift's WindowDelegate. This class only updates Dart-side state.
class FullscreenWindowDelegate extends MacOSWindowDelegate {
@override @override
void windowWillEnterFullScreen() { void windowWillEnterFullScreen() {
// Notify global state manager
FullscreenStateManager().setFullscreen(true); FullscreenStateManager().setFullscreen(true);
// Remove toolbar and restore default titlebar before entering fullscreen
_prepareForFullscreen();
}
@override
void windowWillExitFullScreen() {
// Hide title and make transparent immediately (safe to do before transition)
WindowManipulator.hideTitle();
WindowManipulator.makeTitlebarTransparent();
} }
@override @override
void windowDidExitFullScreen() { void windowDidExitFullScreen() {
// Notify global state manager
FullscreenStateManager().setFullscreen(false); FullscreenStateManager().setFullscreen(false);
// Add toolbar and reposition traffic lights after transition completes
WindowManipulator.addToolbar();
// Restore custom traffic light positions
WindowManipulator.overrideStandardWindowButtonPosition(
buttonType: NSWindowButtonType.closeButton,
offset: const Offset(20, _customButtonY),
);
WindowManipulator.overrideStandardWindowButtonPosition(
buttonType: NSWindowButtonType.miniaturizeButton,
offset: const Offset(40, _customButtonY),
);
WindowManipulator.overrideStandardWindowButtonPosition(
buttonType: NSWindowButtonType.zoomButton,
offset: const Offset(60, _customButtonY),
);
}
/// Prepare titlebar for fullscreen mode
void _prepareForFullscreen() {
WindowManipulator.removeToolbar();
WindowManipulator.showTitle();
WindowManipulator.makeTitlebarOpaque();
// Set traffic lights to standard fullscreen positions (null = default)
WindowManipulator.overrideStandardWindowButtonPosition(buttonType: NSWindowButtonType.closeButton, offset: null);
WindowManipulator.overrideStandardWindowButtonPosition(
buttonType: NSWindowButtonType.miniaturizeButton,
offset: null,
);
WindowManipulator.overrideStandardWindowButtonPosition(buttonType: NSWindowButtonType.zoomButton, offset: null);
} }
} }
+14 -50
View File
@@ -1,61 +1,25 @@
import 'dart:io' show Platform; import 'dart:io' show Platform;
import 'package:flutter/material.dart' show Offset;
import 'package:macos_window_utils/macos_window_utils.dart';
import 'package:macos_window_utils/macos/ns_window_button_type.dart';
import 'fullscreen_window_delegate.dart'; import 'fullscreen_window_delegate.dart';
import 'macos_window_service.dart';
/// Service to manage macOS titlebar configuration /// Service to manage macOS titlebar configuration
class MacOSTitlebarService { class MacOSTitlebarService {
// Standard button Y position when using custom toolbar static bool _initialized = false;
static const double _customButtonY = 21.0;
/// Initialize the custom titlebar setup (transparent with toolbar) /// Initialize the custom titlebar setup.
/// This configuration automatically handles fullscreen mode natively ///
/// Note: The initial window configuration (transparent titlebar, toolbar,
/// button positions, fullscreen presentation options) is now applied in
/// MainFlutterWindow.swift / WindowDelegate.swift BEFORE frame restoration
/// to prevent the window from shrinking on launch.
///
/// This method only sets up the Dart-side callbacks.
static Future<void> setupCustomTitlebar() async { static Future<void> setupCustomTitlebar() async {
if (!Platform.isMacOS) return; if (!Platform.isMacOS || _initialized) return;
_initialized = true;
// Enable window delegate to use presentation options and fullscreen callbacks await MacOSWindowService.initialize(enableWindowDelegate: true);
await WindowManipulator.initialize(enableWindowDelegate: true);
// Register custom delegate to handle fullscreen transitions
final delegate = FullscreenWindowDelegate(); final delegate = FullscreenWindowDelegate();
WindowManipulator.addNSWindowDelegate(delegate); MacOSWindowService.addWindowDelegate(delegate);
// Make titlebar transparent but keep it functional
await WindowManipulator.makeTitlebarTransparent();
await WindowManipulator.hideTitle();
await WindowManipulator.enableFullSizeContentView();
// Add toolbar to create space for traffic lights in normal mode
await WindowManipulator.addToolbar();
// Set custom traffic light positions for normal mode
await _setCustomButtonPositions();
// Configure fullscreen presentation to auto-hide toolbar and menubar
// This tells macOS to automatically hide the toolbar when entering fullscreen
final presentationOptions = NSAppPresentationOptions.from({
NSAppPresentationOption.fullScreen,
NSAppPresentationOption.autoHideToolbar,
NSAppPresentationOption.autoHideMenuBar,
NSAppPresentationOption.autoHideDock,
});
presentationOptions.applyAsFullScreenPresentationOptions();
}
/// Set traffic light buttons to custom positions (with toolbar offset)
static Future<void> _setCustomButtonPositions() async {
await WindowManipulator.overrideStandardWindowButtonPosition(
buttonType: NSWindowButtonType.closeButton,
offset: const Offset(20, _customButtonY),
);
await WindowManipulator.overrideStandardWindowButtonPosition(
buttonType: NSWindowButtonType.miniaturizeButton,
offset: const Offset(40, _customButtonY),
);
await WindowManipulator.overrideStandardWindowButtonPosition(
buttonType: NSWindowButtonType.zoomButton,
offset: const Offset(60, _customButtonY),
);
} }
} }
+16
View File
@@ -0,0 +1,16 @@
/// Abstract class for receiving macOS window delegate callbacks.
/// Extend this class and register with MacOSWindowService to receive
/// fullscreen transition events.
abstract class MacOSWindowDelegate {
/// Called when the window is about to enter fullscreen mode.
void windowWillEnterFullScreen() {}
/// Called when the window has entered fullscreen mode.
void windowDidEnterFullScreen() {}
/// Called when the window is about to exit fullscreen mode.
void windowWillExitFullScreen() {}
/// Called when the window has exited fullscreen mode.
void windowDidExitFullScreen() {}
}
+99
View File
@@ -0,0 +1,99 @@
import 'dart:io' show Platform;
import 'package:flutter/services.dart';
import 'macos_window_delegate.dart';
/// Service for manipulating macOS window properties.
/// This is a native implementation replacing the macos_window_utils package.
///
/// Note: Titlebar, toolbar, and traffic light position management is now handled
/// directly in Swift (WindowDelegate.swift) during fullscreen transitions.
/// This service only exposes what's needed externally:
/// - Traffic light visibility (for video controls)
/// - Fullscreen enter/exit (for video controls)
/// - Delegate registration (for FullscreenStateManager updates)
class MacOSWindowService {
static const _channel = MethodChannel('com.plezy/window_utils');
static bool _initialized = false;
static bool _delegateEnabled = false;
static final List<MacOSWindowDelegate> _delegates = [];
// MARK: - Private Helpers
static Future<void> _invoke(String method, [Map<String, dynamic>? args]) async {
if (!Platform.isMacOS) return;
await _channel.invokeMethod(method, args);
}
static void _notifyDelegates(void Function(MacOSWindowDelegate) callback) {
for (final delegate in _delegates) {
callback(delegate);
}
}
static Future<dynamic> _handleMethodCall(MethodCall call) async {
switch (call.method) {
case 'windowWillEnterFullScreen':
_notifyDelegates((d) => d.windowWillEnterFullScreen());
case 'windowDidEnterFullScreen':
_notifyDelegates((d) => d.windowDidEnterFullScreen());
case 'windowWillExitFullScreen':
_notifyDelegates((d) => d.windowWillExitFullScreen());
case 'windowDidExitFullScreen':
_notifyDelegates((d) => d.windowDidExitFullScreen());
}
}
// MARK: - Initialization
/// Initialize the window service.
/// Must be called before using other methods.
/// Set [enableWindowDelegate] to true to receive fullscreen callbacks.
static Future<void> initialize({bool enableWindowDelegate = false}) async {
if (!Platform.isMacOS) return;
if (!_initialized) {
await _channel.invokeMethod('initialize', {
'enableWindowDelegate': enableWindowDelegate,
});
_initialized = true;
}
// Set up handler if not already done and delegate is requested
if (enableWindowDelegate && !_delegateEnabled) {
_channel.setMethodCallHandler(_handleMethodCall);
_delegateEnabled = true;
}
}
/// Add a delegate to receive window events.
static void addWindowDelegate(MacOSWindowDelegate delegate) {
if (!_delegates.contains(delegate)) {
_delegates.add(delegate);
}
}
/// Remove a previously added delegate.
static void removeWindowDelegate(MacOSWindowDelegate delegate) {
_delegates.remove(delegate);
}
// MARK: - Traffic Light Buttons
/// Show or hide all traffic light buttons (close, miniaturize, zoom).
static Future<void> setTrafficLightsVisible(bool visible) =>
_invoke('setTrafficLightsVisible', {'visible': visible});
// MARK: - Fullscreen
/// Enter fullscreen mode.
static Future<void> enterFullscreen() => _invoke('enterFullscreen');
/// Exit fullscreen mode.
static Future<void> exitFullscreen() => _invoke('exitFullscreen');
/// Check if the window is in fullscreen mode.
static Future<bool> isFullscreen() async {
if (!Platform.isMacOS) return false;
return await _channel.invokeMethod<bool>('isFullscreen') ?? false;
}
}
+4 -14
View File
@@ -14,7 +14,7 @@ import 'package:flutter/services.dart'
KeyEvent, KeyEvent,
KeyDownEvent, KeyDownEvent,
HardwareKeyboard; HardwareKeyboard;
import 'package:macos_window_utils/macos_window_utils.dart'; import '../../services/macos_window_service.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import '../../mpv/mpv.dart'; import '../../mpv/mpv.dart';
@@ -633,17 +633,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
} }
void _updateTrafficLightVisibility() async { void _updateTrafficLightVisibility() async {
if (Platform.isMacOS) { await MacOSWindowService.setTrafficLightsVisible(_showControls);
if (_showControls) {
await WindowManipulator.showCloseButton();
await WindowManipulator.showMiniaturizeButton();
await WindowManipulator.showZoomButton();
} else {
await WindowManipulator.hideCloseButton();
await WindowManipulator.hideMiniaturizeButton();
await WindowManipulator.hideZoomButton();
}
}
} }
Future<void> _loadPlaybackExtras() async { Future<void> _loadPlaybackExtras() async {
@@ -1081,9 +1071,9 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
// Use native macOS fullscreen - titlebar is handled automatically // Use native macOS fullscreen - titlebar is handled automatically
// Window listener will update _isFullscreen for UI // Window listener will update _isFullscreen for UI
if (isCurrentlyFullscreen) { if (isCurrentlyFullscreen) {
await WindowManipulator.exitFullscreen(); await MacOSWindowService.exitFullscreen();
} else { } else {
await WindowManipulator.enterFullscreen(); await MacOSWindowService.enterFullscreen();
} }
} else { } else {
// For Windows/Linux, use window_manager // For Windows/Linux, use window_manager
@@ -12,7 +12,6 @@ import flutter_webrtc
import gamepads_darwin import gamepads_darwin
import hotkey_manager_macos import hotkey_manager_macos
import in_app_review import in_app_review
import macos_window_utils
import os_media_controls import os_media_controls
import package_info_plus import package_info_plus
import path_provider_foundation import path_provider_foundation
@@ -32,7 +31,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
GamepadsDarwinPlugin.register(with: registry.registrar(forPlugin: "GamepadsDarwinPlugin")) GamepadsDarwinPlugin.register(with: registry.registrar(forPlugin: "GamepadsDarwinPlugin"))
HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin")) HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin"))
OsMediaControlsPlugin.register(with: registry.registrar(forPlugin: "OsMediaControlsPlugin")) OsMediaControlsPlugin.register(with: registry.registrar(forPlugin: "OsMediaControlsPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
-6
View File
@@ -17,8 +17,6 @@ PODS:
- HotKey - HotKey
- in_app_review (2.0.0): - in_app_review (2.0.0):
- FlutterMacOS - FlutterMacOS
- macos_window_utils (1.0.0):
- FlutterMacOS
- os_media_controls (0.0.1): - os_media_controls (0.0.1):
- FlutterMacOS - FlutterMacOS
- package_info_plus (0.0.1): - package_info_plus (0.0.1):
@@ -76,7 +74,6 @@ DEPENDENCIES:
- gamepads_darwin (from `Flutter/ephemeral/.symlinks/plugins/gamepads_darwin/macos`) - gamepads_darwin (from `Flutter/ephemeral/.symlinks/plugins/gamepads_darwin/macos`)
- hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`) - hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`)
- in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`) - in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`)
- macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`)
- os_media_controls (from `Flutter/ephemeral/.symlinks/plugins/os_media_controls/macos`) - os_media_controls (from `Flutter/ephemeral/.symlinks/plugins/os_media_controls/macos`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
@@ -111,8 +108,6 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos
in_app_review: in_app_review:
:path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos :path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos
macos_window_utils:
:path: Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos
os_media_controls: os_media_controls:
:path: Flutter/ephemeral/.symlinks/plugins/os_media_controls/macos :path: Flutter/ephemeral/.symlinks/plugins/os_media_controls/macos
package_info_plus: package_info_plus:
@@ -144,7 +139,6 @@ SPEC CHECKSUMS:
HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
hotkey_manager_macos: a4317849af96d2430fa89944d3c58977ca089fbe hotkey_manager_macos: a4317849af96d2430fa89944d3c58977ca089fbe
in_app_review: 66e7680752b632d83f4f0e88b34d52ed303fbff4 in_app_review: 66e7680752b632d83f4f0e88b34d52ed303fbff4
macos_window_utils: 23f54331a0fd51eea9e0ed347253bf48fd379d1d
os_media_controls: c07c04c4afdf59dda0a3f398457a46823c4ce0ed os_media_controls: c07c04c4afdf59dda0a3f398457a46823c4ce0ed
package_info_plus: f0052d280d17aa382b932f399edf32507174e870 package_info_plus: f0052d280d17aa382b932f399edf32507174e870
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
+8
View File
@@ -30,6 +30,8 @@
6AC86ED72EA70B4C0067BC66 /* plezy.icon in Resources */ = {isa = PBXBuildFile; fileRef = 6AC86ED62EA70B4C0067BC66 /* plezy.icon */; }; 6AC86ED72EA70B4C0067BC66 /* plezy.icon in Resources */ = {isa = PBXBuildFile; fileRef = 6AC86ED62EA70B4C0067BC66 /* plezy.icon */; };
6AD8B1612ED7B50000E9E1B4 /* MpvPlayerCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD8B1632ED7B50000E9E1B4 /* MpvPlayerCore.swift */; }; 6AD8B1612ED7B50000E9E1B4 /* MpvPlayerCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD8B1632ED7B50000E9E1B4 /* MpvPlayerCore.swift */; };
6AD8B1622ED7B50000E9E1B4 /* MpvPlayerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD8B1642ED7B50000E9E1B4 /* MpvPlayerPlugin.swift */; }; 6AD8B1622ED7B50000E9E1B4 /* MpvPlayerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD8B1642ED7B50000E9E1B4 /* MpvPlayerPlugin.swift */; };
6AD8B1662ED7B50000E9E1B5 /* WindowUtilsPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD8B1682ED7B50000E9E1B5 /* WindowUtilsPlugin.swift */; };
6AD8B1672ED7B50000E9E1B5 /* WindowDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD8B1692ED7B50000E9E1B5 /* WindowDelegate.swift */; };
9AECA605E2E1BECE3CA5BD01 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 78F9F23331B540A25C4A70C7 /* Pods_Runner.framework */; }; 9AECA605E2E1BECE3CA5BD01 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 78F9F23331B540A25C4A70C7 /* Pods_Runner.framework */; };
FE52EE1D2D489FA88507D14E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84260356652A3270E664FED4 /* Pods_RunnerTests.framework */; }; FE52EE1D2D489FA88507D14E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84260356652A3270E664FED4 /* Pods_RunnerTests.framework */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@@ -86,6 +88,8 @@
6AC86ED62EA70B4C0067BC66 /* plezy.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = plezy.icon; sourceTree = "<group>"; }; 6AC86ED62EA70B4C0067BC66 /* plezy.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = plezy.icon; sourceTree = "<group>"; };
6AD8B1632ED7B50000E9E1B4 /* MpvPlayerCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPlayerCore.swift; sourceTree = "<group>"; }; 6AD8B1632ED7B50000E9E1B4 /* MpvPlayerCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPlayerCore.swift; sourceTree = "<group>"; };
6AD8B1642ED7B50000E9E1B4 /* MpvPlayerPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPlayerPlugin.swift; sourceTree = "<group>"; }; 6AD8B1642ED7B50000E9E1B4 /* MpvPlayerPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPlayerPlugin.swift; sourceTree = "<group>"; };
6AD8B1682ED7B50000E9E1B5 /* WindowUtilsPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowUtilsPlugin.swift; sourceTree = "<group>"; };
6AD8B1692ED7B50000E9E1B5 /* WindowDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowDelegate.swift; sourceTree = "<group>"; };
78F9F23331B540A25C4A70C7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 78F9F23331B540A25C4A70C7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
84260356652A3270E664FED4 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 84260356652A3270E664FED4 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -196,6 +200,8 @@
children = ( children = (
33CC10F02044A3C60003C045 /* AppDelegate.swift */, 33CC10F02044A3C60003C045 /* AppDelegate.swift */,
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
6AD8B1682ED7B50000E9E1B5 /* WindowUtilsPlugin.swift */,
6AD8B1692ED7B50000E9E1B5 /* WindowDelegate.swift */,
6AD8B1652ED7B50000E9E1B4 /* MpvPlayer */, 6AD8B1652ED7B50000E9E1B4 /* MpvPlayer */,
33E51913231747F40026EE4D /* DebugProfile.entitlements */, 33E51913231747F40026EE4D /* DebugProfile.entitlements */,
33E51914231749380026EE4D /* Release.entitlements */, 33E51914231749380026EE4D /* Release.entitlements */,
@@ -460,6 +466,8 @@
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
6AD8B1612ED7B50000E9E1B4 /* MpvPlayerCore.swift in Sources */, 6AD8B1612ED7B50000E9E1B4 /* MpvPlayerCore.swift in Sources */,
6AD8B1622ED7B50000E9E1B4 /* MpvPlayerPlugin.swift in Sources */, 6AD8B1622ED7B50000E9E1B4 /* MpvPlayerPlugin.swift in Sources */,
6AD8B1662ED7B50000E9E1B5 /* WindowUtilsPlugin.swift in Sources */,
6AD8B1672ED7B50000E9E1B5 /* WindowDelegate.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
+17
View File
@@ -13,9 +13,26 @@ class MainFlutterWindow: NSWindow {
self.contentViewController = flutterViewController self.contentViewController = flutterViewController
self.setFrame(windowFrame, display: true) self.setFrame(windowFrame, display: true)
// Apply initial window configuration BEFORE frame restoration
// This prevents the window from shrinking on launch
self.titlebarAppearsTransparent = true
self.titleVisibility = .hidden
self.styleMask.insert(.fullSizeContentView)
// Add forwarding toolbar for click-through in titlebar area
let toolbar = ForwardingToolbar(flutterViewController: flutterViewController)
self.toolbar = toolbar
// Register MPV player plugin for video playback // Register MPV player plugin for video playback
MpvPlayerPlugin.register(with: flutterViewController.registrar(forPlugin: "MpvPlayerPlugin")) MpvPlayerPlugin.register(with: flutterViewController.registrar(forPlugin: "MpvPlayerPlugin"))
// Register window utils plugin for dynamic titlebar/fullscreen control from Dart
WindowUtilsPlugin.register(with: flutterViewController.registrar(forPlugin: "WindowUtilsPlugin"))
WindowUtilsPlugin.setWindow(self)
// Set custom traffic light positions using centralized values from plugin
WindowUtilsPlugin.setInitialTrafficLightPositions()
RegisterGeneratedPlugins(registry: flutterViewController) RegisterGeneratedPlugins(registry: flutterViewController)
super.awakeFromNib() super.awakeFromNib()
+1 -1
View File
@@ -213,7 +213,7 @@ class MpvPlayerCore: NSObject {
// Re-insert after background layer but before Flutter control views // Re-insert after background layer but before Flutter control views
layer.removeFromSuperlayer() layer.removeFromSuperlayer()
if let superlayer = window?.contentView?.layer { if let superlayer = window?.contentView?.layer {
superlayer.insertSublayer(layer, at: 1) superlayer.insertSublayer(layer, at: 0)
} }
} }
+65
View File
@@ -0,0 +1,65 @@
import Cocoa
import FlutterMacOS
class WindowDelegate: NSObject, NSWindowDelegate {
weak var channel: FlutterMethodChannel?
weak var window: NSWindow?
// Hardcoded presentation options for fullscreen mode
// Auto-hide toolbar, menu bar, and dock when in fullscreen
private let fullScreenPresentationOptions: NSApplication.PresentationOptions = [
.fullScreen,
.autoHideToolbar,
.autoHideMenuBar,
.autoHideDock
]
// MARK: - Private Helpers
private func emit(_ method: String) {
channel?.invokeMethod(method, arguments: nil)
}
// MARK: - NSWindowDelegate
func window(_ window: NSWindow, willUseFullScreenPresentationOptions proposedOptions: NSApplication.PresentationOptions) -> NSApplication.PresentationOptions {
return fullScreenPresentationOptions
}
func windowWillEnterFullScreen(_ notification: Notification) {
guard let window = window else { return }
// Remove toolbar before entering fullscreen
window.toolbar = nil
// Show title and make titlebar opaque for native fullscreen look
window.titleVisibility = .visible
window.titlebarAppearsTransparent = false
// Reset traffic light positions to default
WindowUtilsPlugin.setTrafficLightPositions(custom: false, window: window)
// Notify Dart for state management only
emit("windowWillEnterFullScreen")
}
func windowDidEnterFullScreen(_ notification: Notification) {
emit("windowDidEnterFullScreen")
}
func windowWillExitFullScreen(_ notification: Notification) {
guard let window = window else { return }
// Hide title and make titlebar transparent BEFORE exiting
window.titleVisibility = .hidden
window.titlebarAppearsTransparent = true
emit("windowWillExitFullScreen")
}
func windowDidExitFullScreen(_ notification: Notification) {
guard let window = window else { return }
// Restore toolbar
if let flutterVC = window.contentViewController {
let toolbar = ForwardingToolbar(flutterViewController: flutterVC)
window.toolbar = toolbar
}
// Restore custom traffic light positions
WindowUtilsPlugin.setTrafficLightPositions(custom: true, window: window)
emit("windowDidExitFullScreen")
}
}
+224
View File
@@ -0,0 +1,224 @@
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()
}
}
}
-8
View File
@@ -768,14 +768,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
macos_window_utils:
dependency: "direct main"
description:
name: macos_window_utils
sha256: d4df3501fd32ac0d2d7590cb6a8e4758337d061c8fa0db816fdd636be63a8438
url: "https://pub.dev"
source: hosted
version: "1.9.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
-1
View File
@@ -13,7 +13,6 @@ dependencies:
json_annotation: ^4.8.1 json_annotation: ^4.8.1
shared_preferences: ^2.2.2 shared_preferences: ^2.2.2
cached_network_image: ^3.4.1 cached_network_image: ^3.4.1
macos_window_utils: ^1.9.0
url_launcher: ^6.3.0 url_launcher: ^6.3.0
uuid: ^4.4.0 uuid: ^4.4.0
window_manager: ^0.4.3 window_manager: ^0.4.3