diff --git a/packages/desktop_multi_window/.gitignore b/packages/desktop_multi_window/.gitignore index 9be145fd..2db326cc 100644 --- a/packages/desktop_multi_window/.gitignore +++ b/packages/desktop_multi_window/.gitignore @@ -18,7 +18,7 @@ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. -#.vscode/ +.vscode/ # Flutter/Dart/Pub related # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. @@ -27,3 +27,8 @@ .dart_tool/ .packages build/ + +.editorconfig + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/packages/desktop_multi_window/example/lib/event_widget.dart b/packages/desktop_multi_window/example/lib/event_widget.dart index a785f5c6..2a1e0d75 100644 --- a/packages/desktop_multi_window/example/lib/event_widget.dart +++ b/packages/desktop_multi_window/example/lib/event_widget.dart @@ -41,6 +41,8 @@ class MessageItem { class _EventWidgetState extends State { final messages = []; + int? _selectedWindowId; + List _windowIds = [0]; final textInputController = TextEditingController(); @@ -49,6 +51,7 @@ class _EventWidgetState extends State { @override void initState() { super.initState(); + _updateWindowIds(); DesktopMultiWindow.setMethodHandler(_handleMethodCallback); } @@ -58,8 +61,18 @@ class _EventWidgetState extends State { super.dispose(); } - Future _handleMethodCallback( - MethodCall call, int fromWindowId) async { + Future _updateWindowIds() async { + // Get all sub-window IDs + final List subWindowIds = await DesktopMultiWindow.getAllSubWindowIds(); + setState(() { + // Combine main window (0) with sub-window IDs + _windowIds = [0, ...subWindowIds]; + // Set default selection if none selected + _selectedWindowId ??= _windowIds.first; + }); + } + + Future _handleMethodCallback(MethodCall call, int fromWindowId) async { if (call.arguments.toString() == "ping") { return "pong"; } @@ -82,11 +95,13 @@ class _EventWidgetState extends State { if (text.isEmpty) { return; } - final windowId = int.tryParse(windowInputController.text); - textInputController.clear(); - final result = - await DesktopMultiWindow.invokeMethod(windowId!, "onSend", text); - debugPrint("onSend result: $result"); + if (_selectedWindowId != null) { + textInputController.clear(); + final result = await DesktopMultiWindow.invokeMethod(_selectedWindowId!, "onSend", text); + debugPrint("onSend result: $result"); + } else { + debugPrint("No window selected"); + } } return Column( @@ -95,36 +110,79 @@ class _EventWidgetState extends State { child: ListView.builder( itemCount: messages.length, reverse: true, - itemBuilder: (context, index) => - _MessageItemWidget(item: messages[index]), + itemBuilder: (context, index) => _MessageItemWidget(item: messages[index]), ), ), - Row( - children: [ - SizedBox( - width: 100, - child: TextField( - controller: windowInputController, - decoration: const InputDecoration( - labelText: 'Window ID', + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, -1), + ), + ], + ), + child: Row( + children: [ + const Text('To: '), + MouseRegion( + cursor: SystemMouseCursors.click, + child: DropdownButton( + value: _selectedWindowId, + items: _windowIds.map((int id) { + return DropdownMenuItem( + value: id, + child: Text(id == 0 ? 'Main Window' : 'Window $id'), + ); + }).toList(), + onTap: () { + // Update window list before showing dropdown + _updateWindowIds(); + }, + onChanged: (int? newValue) { + setState(() { + _selectedWindowId = newValue; + }); + }, ), - inputFormatters: [FilteringTextInputFormatter.digitsOnly], ), - ), - Expanded( - child: TextField( - controller: textInputController, - decoration: const InputDecoration( - hintText: 'Enter message', + const SizedBox(width: 16), + Expanded( + child: TextField( + controller: textInputController, + decoration: InputDecoration( + hintText: 'Enter message', + filled: true, + fillColor: Theme.of(context).colorScheme.surface, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.outline, + ), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + onSubmitted: (text) => submit(), ), - onSubmitted: (text) => submit(), ), - ), - IconButton( - icon: const Icon(Icons.send), - onPressed: submit, - ), - ], + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.send), + onPressed: submit, + tooltip: 'Send message', + style: IconButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primaryContainer, + foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer, + ), + ), + ], + ), ), ], ); diff --git a/packages/desktop_multi_window/example/lib/main.dart b/packages/desktop_multi_window/example/lib/main.dart index 34d10cc5..c218d7ce 100644 --- a/packages/desktop_multi_window/example/lib/main.dart +++ b/packages/desktop_multi_window/example/lib/main.dart @@ -1,17 +1,20 @@ import 'dart:convert'; +import 'dart:io'; import 'package:collection/collection.dart'; import 'package:desktop_lifecycle/desktop_lifecycle.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_multi_window_example/event_widget.dart'; +import 'window_events_widget.dart'; + void main(List args) { + WidgetsFlutterBinding.ensureInitialized(); if (args.firstOrNull == 'multi_window') { final windowId = int.parse(args[1]); - final argument = args[2].isEmpty - ? const {} - : jsonDecode(args[2]) as Map; + final argument = args[2].isEmpty ? const {} : jsonDecode(args[2]) as Map; runApp(_ExampleSubWindow( windowController: WindowController.fromWindowId(windowId), args: argument, @@ -23,54 +26,209 @@ void main(List args) { class _ExampleMainWindow extends StatefulWidget { const _ExampleMainWindow({Key? key}) : super(key: key); - @override State<_ExampleMainWindow> createState() => _ExampleMainWindowState(); } class _ExampleMainWindowState extends State<_ExampleMainWindow> { + int? _selectedWindowId; + List _windowIds = []; + + late final AppLifecycleListener? _appLifecycleListener; + + // final options = WindowOptions( + // macos: MacOSWindowOptions.nspanel( + // title: 'Sub Window', + // backgroundColor: Colors.transparent, + // level: MacOSWindowLevel.floating, + // styleMask: {MacOSWindowStyleMask.borderless, MacOSWindowStyleMask.nonactivatingPanel, MacOSWindowStyleMask.utility}, + // isOpaque: false, + // hasShadow: false, + // ), + // ); + + final options = WindowOptions( + macos: MacOSWindowOptions.nswindow( + title: 'Sub Window', + backgroundColor: Colors.transparent, + level: MacOsWindowLevel.floating, + isOpaque: false, + hasShadow: false, + ), + windows: const WindowsWindowOptions( + style: WindowsWindowStyle.WS_OVERLAPPEDWINDOW, + exStyle: WindowsExtendedWindowStyle.WS_EX_APPWINDOW, + width: 1280, + height: 720, + backgroundColor: Colors.transparent, + ), + ); + + @override + void initState() { + if (Platform.isMacOS) { + _appLifecycleListener = AppLifecycleListener(onStateChange: _handleStateChange); + } + super.initState(); + SchedulerBinding.instance.addPostFrameCallback((timeStamp) async { + final window = await DesktopMultiWindow.createWindow( + jsonEncode({ + 'args1': 'Sub window', + 'args2': 100, + 'args3': true, + 'business': 'business_test', + }), + options, + ); + window + ..setFrame(const Offset(0, 0) & const Size(1280, 720)) + ..center() + ..setTitle('Another window') + ..show(); + await _updateWindowIds(); + }); + _updateWindowIds(); + } + + void _handleStateChange(AppLifecycleState state) { + // workaround applies for all sub-windows + if (Platform.isMacOS && state == AppLifecycleState.hidden) { + SchedulerBinding.instance.handleAppLifecycleStateChanged(AppLifecycleState.inactive); + } + } + + @override + void dispose() { + if (Platform.isMacOS) { + _appLifecycleListener?.dispose(); + } + super.dispose(); + } + + Future _updateWindowIds() async { + // Get all sub-window IDs + final List subWindowIds = await DesktopMultiWindow.getAllSubWindowIds(); + setState(() { + _windowIds = subWindowIds; + if (_windowIds.isNotEmpty) { + _selectedWindowId ??= _windowIds.first; + } + }); + } + @override Widget build(BuildContext context) { return MaterialApp( + color: Colors.transparent, home: Scaffold( appBar: AppBar( title: const Text('Plugin example app'), ), + backgroundColor: Colors.transparent, body: Column( children: [ - TextButton( - onPressed: () async { - final window = - await DesktopMultiWindow.createWindow(jsonEncode({ - 'args1': 'Sub window', - 'args2': 100, - 'args3': true, - 'business': 'business_test', - })); - window - ..setFrame(const Offset(0, 0) & const Size(1280, 720)) - ..center() - ..setTitle('Another window') - ..show(); - }, - child: const Text('Create a new World!'), - ), - TextButton( - child: const Text('Send event to all sub windows'), - onPressed: () async { - final subWindowIds = - await DesktopMultiWindow.getAllSubWindowIds(); - for (final windowId in subWindowIds) { - DesktopMultiWindow.invokeMethod( - windowId, - 'broadcast', - 'Broadcast from main window', - ); - } - }, + Row( + children: [ + WindowEventsWidget(controller: WindowController.main()), + Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0), + child: ElevatedButton.icon( + onPressed: () async { + final window = await DesktopMultiWindow.createWindow( + jsonEncode({ + 'args1': 'Sub window', + 'args2': 100, + 'args3': true, + 'business': 'business_test', + }), + options, + ); + window + ..setFrame(const Offset(0, 0) & const Size(1280, 720)) + ..center() + ..setTitle('Another window') + ..show(); + await _updateWindowIds(); + }, + icon: const Icon(Icons.add), + label: const Text('Create Window'), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primaryContainer, + foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0), + child: ElevatedButton.icon( + onPressed: () async { + final subWindowIds = await DesktopMultiWindow.getAllSubWindowIds(); + for (final windowId in subWindowIds) { + DesktopMultiWindow.invokeMethod( + windowId, + 'broadcast', + 'Broadcast from main window', + ); + } + }, + icon: const Icon(Icons.send), + label: const Text('Send event to all sub windows'), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primaryContainer, + foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + ), + ), + Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Window: '), + MouseRegion( + cursor: SystemMouseCursors.click, + child: DropdownButton( + value: _windowIds.contains(_selectedWindowId) ? _selectedWindowId : null, + items: _windowIds.map((int id) { + return DropdownMenuItem( + value: id, + child: Text(id == 0 ? 'Main Window' : 'Window $id'), + ); + }).toList(), + onTap: () async { + // Update window list before showing dropdown + await _updateWindowIds(); + }, + onChanged: (int? newValue) { + if (newValue == null) { + return; + } + setState(() { + _selectedWindowId = newValue; + }); + }, + ), + ), + TextButton( + onPressed: () async { + if (_selectedWindowId != null) { + await WindowController.fromWindowId(_selectedWindowId!).show(); + } + }, + child: const Text('Show this window'), + ), + ], + ), + ), + ], + ), + ], ), Expanded( - child: EventWidget(controller: WindowController.fromWindowId(0)), + child: EventWidget(controller: WindowController.main()), ) ], ), @@ -79,7 +237,7 @@ class _ExampleMainWindowState extends State<_ExampleMainWindow> { } } -class _ExampleSubWindow extends StatelessWidget { +class _ExampleSubWindow extends StatefulWidget { const _ExampleSubWindow({ Key? key, required this.windowController, @@ -89,6 +247,36 @@ class _ExampleSubWindow extends StatelessWidget { final WindowController windowController; final Map? args; + @override + State<_ExampleSubWindow> createState() => _ExampleSubWindowState(); +} + +class _ExampleSubWindowState extends State<_ExampleSubWindow> { + late final AppLifecycleListener? _appLifecycleListener; + + @override + void initState() { + super.initState(); + if (Platform.isMacOS) { + _appLifecycleListener = AppLifecycleListener(onStateChange: _handleStateChange); + } + } + + void _handleStateChange(AppLifecycleState state) { + // workaround applies for all sub-windows + if (Platform.isMacOS && state == AppLifecycleState.hidden) { + SchedulerBinding.instance.handleAppLifecycleStateChanged(AppLifecycleState.inactive); + } + } + + @override + void dispose() { + if (Platform.isMacOS) { + _appLifecycleListener?.dispose(); + } + super.dispose(); + } + @override Widget build(BuildContext context) { return MaterialApp( @@ -98,9 +286,9 @@ class _ExampleSubWindow extends StatelessWidget { ), body: Column( children: [ - if (args != null) + if (widget.args != null) Text( - 'Arguments: ${args.toString()}', + 'Arguments: ${widget.args.toString()}', style: const TextStyle(fontSize: 20), ), ValueListenableBuilder( @@ -115,11 +303,18 @@ class _ExampleSubWindow extends StatelessWidget { ), TextButton( onPressed: () async { - windowController.close(); + widget.windowController.close(); }, child: const Text('Close this window'), ), - Expanded(child: EventWidget(controller: windowController)), + TextButton( + onPressed: () async { + await WindowController.main().show(); + }, + child: const Text('Show main window'), + ), + WindowEventsWidget(controller: widget.windowController), + Expanded(child: EventWidget(controller: widget.windowController)), ], ), ), diff --git a/packages/desktop_multi_window/example/lib/window_events_widget.dart b/packages/desktop_multi_window/example/lib/window_events_widget.dart new file mode 100644 index 00000000..2784f891 --- /dev/null +++ b/packages/desktop_multi_window/example/lib/window_events_widget.dart @@ -0,0 +1,462 @@ +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; + +import 'windows/window_styles_selector.dart'; + +class WindowEventsWidget extends StatefulWidget { + const WindowEventsWidget({super.key, required this.controller}); + + final WindowController controller; + + @override + State createState() => _WindowEventsWidgetState(); +} + +class _WindowEventsWidgetState extends State with WindowEvents { + TextEditingController xPositionController = TextEditingController()..text = '100'; + TextEditingController yPositionController = TextEditingController()..text = '100'; + TextEditingController widthController = TextEditingController()..text = '800'; + TextEditingController heightController = TextEditingController()..text = '600'; + + TextEditingController backgroundColorRController = TextEditingController()..text = '100'; + TextEditingController backgroundColorGController = TextEditingController()..text = '100'; + TextEditingController backgroundColorBController = TextEditingController()..text = '100'; + TextEditingController backgroundColorAController = TextEditingController()..text = '100'; + + int _windowStyle = 0x00CF0000; // Default WS_OVERLAPPEDWINDOW + int _extendedStyle = 0x00000100; // Default WS_EX_WINDOWEDGE + + Offset _position = const Offset(0, 0); + Size _size = const Size(0, 0); + + Offset _mousePosition = const Offset(0, 0); + + @override + void initState() { + super.initState(); + widget.controller.addListener(this); + widget.controller.getPosition().then((position) { + setState(() { + _position = position; + }); + }); + widget.controller.getSize().then((size) { + setState(() { + _size = size; + }); + }); + } + + @override + void dispose() { + widget.controller.removeListener(this); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(width: 16), + Text( + 'Mouse Position: ${_mousePosition.dx},${_mousePosition.dy}', + style: const TextStyle( + color: Colors.red, + ), + ), + Card( + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + style: Theme.of(context).textTheme.titleMedium, + children: [ + const TextSpan(text: 'Window Position '), + TextSpan( + text: '${_position.dx},${_position.dy}', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + const TextSpan(text: ' & Size '), + TextSpan( + text: '${_size.width}x${_size.height}', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + Row( + children: [ + SizedBox( + width: 60, + child: TextField( + controller: xPositionController, + decoration: InputDecoration( + labelText: 'Left', + labelStyle: Theme.of(context).textTheme.bodySmall, + border: const OutlineInputBorder(), + contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + isDense: true, + ), + keyboardType: TextInputType.number, + ), + ), + const SizedBox(width: 4), + SizedBox( + width: 60, + child: TextField( + controller: yPositionController, + decoration: InputDecoration( + labelText: 'Top', + labelStyle: Theme.of(context).textTheme.bodySmall, + border: const OutlineInputBorder(), + contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + isDense: true, + ), + keyboardType: TextInputType.number, + ), + ), + const SizedBox(width: 4), + SizedBox( + width: 60, + child: TextField( + controller: widthController, + decoration: InputDecoration( + labelText: 'Width', + labelStyle: Theme.of(context).textTheme.bodySmall, + border: const OutlineInputBorder(), + contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + isDense: true, + ), + keyboardType: TextInputType.number, + ), + ), + const SizedBox(width: 4), + SizedBox( + width: 60, + child: TextField( + controller: heightController, + decoration: InputDecoration( + labelText: 'Height', + labelStyle: Theme.of(context).textTheme.bodySmall, + border: const OutlineInputBorder(), + contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + isDense: true, + ), + keyboardType: TextInputType.number, + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + ElevatedButton( + onPressed: () async { + final position = + Offset(double.parse(xPositionController.text), double.parse(yPositionController.text)); + final size = Size(double.parse(widthController.text), double.parse(heightController.text)); + await widget.controller.setFrame(position & size); + setState(() { + _position = position; + _size = size; + }); + }, + child: const Text('Set frame'), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primaryContainer, + foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + ), + ), + const SizedBox(width: 4), + ElevatedButton( + onPressed: () async { + await widget.controller.center(); + widget.controller.getPosition().then((position) { + setState(() { + _position = position; + }); + }); + widget.controller.getSize().then((size) { + setState(() { + _size = size; + }); + }); + }, + child: const Text('Center'), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, + foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + ), + ), + ], + ), + ], + ), + ), + ), + Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + ElevatedButton( + onPressed: () async { + await widget.controller.hide(); + }, + child: const Text('Hide'), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primaryContainer, + foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + ), + Row( + children: [ + ElevatedButton( + onPressed: () async { + await widget.controller.maximize(); + }, + child: const Text('Maximize'), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primaryContainer, + foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + ), + ElevatedButton( + onPressed: () async { + await widget.controller.unmaximize(); + }, + child: const Text('Unmaximize'), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primaryContainer, + foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + ), + ], + ), + Row( + children: [ + ElevatedButton( + onPressed: () async { + await widget.controller.minimize(); + }, + child: const Text('Minimize'), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primaryContainer, + foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + ), + ElevatedButton( + onPressed: () async { + final isFullscreen = await widget.controller.isFullScreen(); + await widget.controller.setFullScreen(!isFullscreen); + }, + child: const Text('Toggle Fullscreen'), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primaryContainer, + foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + ), + ], + ), + ], + ), + ), + ), + Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Row( + children: [ + SizedBox( + width: 60, + child: TextField( + controller: backgroundColorRController, + decoration: InputDecoration( + labelText: 'Red', + labelStyle: Theme.of(context).textTheme.bodySmall, + border: const OutlineInputBorder(), + contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + isDense: true, + ), + keyboardType: TextInputType.number, + ), + ), + SizedBox( + width: 60, + child: TextField( + controller: backgroundColorGController, + decoration: InputDecoration( + labelText: 'Green', + labelStyle: Theme.of(context).textTheme.bodySmall, + border: const OutlineInputBorder(), + contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + isDense: true, + ), + keyboardType: TextInputType.number, + ), + ), + SizedBox( + width: 60, + child: TextField( + controller: backgroundColorBController, + decoration: InputDecoration( + labelText: 'Blue', + labelStyle: Theme.of(context).textTheme.bodySmall, + border: const OutlineInputBorder(), + contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + isDense: true, + ), + keyboardType: TextInputType.number, + ), + ), + SizedBox( + width: 60, + child: TextField( + controller: backgroundColorAController, + decoration: InputDecoration( + labelText: 'Alpha', + labelStyle: Theme.of(context).textTheme.bodySmall, + border: const OutlineInputBorder(), + contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + isDense: true, + ), + keyboardType: TextInputType.number, + ), + ), + ], + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () async { + await widget.controller.setBackgroundColor(Color.fromARGB( + int.parse(backgroundColorAController.text), + int.parse(backgroundColorRController.text), + int.parse(backgroundColorGController.text), + int.parse(backgroundColorBController.text))); + }, + child: const Text('Set Background Color'), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primaryContainer, + foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + ), + ], + ), + ), + ), + ], + ), + Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + SizedBox( + height: 350, + width: 300, + child: WindowStyleSelector( + initialStyle: _windowStyle, + initialExtendedStyle: _extendedStyle, + onStyleChanged: (style, extendedStyle) async { + setState(() { + _windowStyle = style; + _extendedStyle = extendedStyle; + }); + }, + ), + ), + ElevatedButton( + onPressed: () async { + print('Setting style to $_windowStyle and extended style to $_extendedStyle'); + await widget.controller.setStyle( + styleMask: MacOsWindowStyleMask.titled, + level: MacOsWindowLevel.normal, + collectionBehavior: MacOsWindowCollectionBehavior.default_, + isOpaque: false, + hasShadow: false, + backgroundColor: Colors.red, + style: _windowStyle, + extendedStyle: _extendedStyle, + ); + }, + child: const Text('Set Style'), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primaryContainer, + foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + ), + ], + ), + ), + ), + ], + ); + } + + @override + void onWindowMove() { + widget.controller.getPosition().then((position) { + setState(() { + _position = position; + }); + }); + } + + @override + void onWindowMoved() { + widget.controller.getPosition().then((position) { + setState(() { + _position = position; + }); + }); + } + + @override + void onWindowResize() { + widget.controller.getSize().then((size) { + setState(() { + _size = size; + }); + }); + } + + @override + void onWindowResized() { + widget.controller.getSize().then((size) { + setState(() { + _size = size; + }); + }); + } + + @override + void onMouseMove(int x, int y) { + setState(() { + _mousePosition = Offset(x.toDouble(), y.toDouble()); + }); + } +} diff --git a/packages/desktop_multi_window/example/lib/windows/window_styles_selector.dart b/packages/desktop_multi_window/example/lib/windows/window_styles_selector.dart new file mode 100644 index 00000000..59092f6c --- /dev/null +++ b/packages/desktop_multi_window/example/lib/windows/window_styles_selector.dart @@ -0,0 +1,598 @@ +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; + +class WindowStyleSelector extends StatefulWidget { + final Function(int style, int extendedStyle) onStyleChanged; + final int initialStyle; + final int initialExtendedStyle; + + const WindowStyleSelector({ + Key? key, + required this.onStyleChanged, + this.initialStyle = 0x00CF0000, // WS_OVERLAPPEDWINDOW | WS_VISIBLE + this.initialExtendedStyle = 0x00000100, // WS_EX_WINDOWEDGE + }) : super(key: key); + + @override + _WindowStyleSelectorState createState() => _WindowStyleSelectorState(); +} + +class _WindowStyleSelectorState extends State with SingleTickerProviderStateMixin { + late TabController _tabController; + late int _currentStyle; + late int _currentExtendedStyle; + List _styleWarnings = []; + + // Regular window styles + final List _styles = [ + StyleOption(name: 'WS_OVERLAPPED', value: WindowsWindowStyle.WS_OVERLAPPED), + StyleOption(name: 'WS_POPUP', value: WindowsWindowStyle.WS_POPUP), + StyleOption(name: 'WS_CHILD', value: WindowsWindowStyle.WS_CHILD), + StyleOption(name: 'WS_MINIMIZE', value: WindowsWindowStyle.WS_MINIMIZE), + StyleOption(name: 'WS_VISIBLE', value: WindowsWindowStyle.WS_VISIBLE), + StyleOption(name: 'WS_DISABLED', value: WindowsWindowStyle.WS_DISABLED), + StyleOption(name: 'WS_CLIPSIBLINGS', value: WindowsWindowStyle.WS_CLIPSIBLINGS), + StyleOption(name: 'WS_CLIPCHILDREN', value: WindowsWindowStyle.WS_CLIPCHILDREN), + StyleOption(name: 'WS_MAXIMIZE', value: WindowsWindowStyle.WS_MAXIMIZE), + StyleOption(name: 'WS_BORDER', value: WindowsWindowStyle.WS_BORDER), + StyleOption(name: 'WS_DLGFRAME', value: WindowsWindowStyle.WS_DLGFRAME), + StyleOption(name: 'WS_VSCROLL', value: WindowsWindowStyle.WS_VSCROLL), + StyleOption(name: 'WS_HSCROLL', value: WindowsWindowStyle.WS_HSCROLL), + StyleOption(name: 'WS_SYSMENU', value: WindowsWindowStyle.WS_SYSMENU), + StyleOption(name: 'WS_THICKFRAME', value: WindowsWindowStyle.WS_THICKFRAME), + StyleOption(name: 'WS_GROUP', value: WindowsWindowStyle.WS_GROUP), + StyleOption(name: 'WS_TABSTOP', value: WindowsWindowStyle.WS_TABSTOP), + StyleOption(name: 'WS_MINIMIZEBOX', value: WindowsWindowStyle.WS_MINIMIZEBOX), + StyleOption(name: 'WS_MAXIMIZEBOX', value: WindowsWindowStyle.WS_MAXIMIZEBOX), + StyleOption(name: 'WS_CAPTION', value: WindowsWindowStyle.WS_CAPTION) + ]; + + // Extended window styles + final List _extendedStyles = [ + StyleOption(name: 'WS_EX_DLGMODALFRAME', value: WindowsExtendedWindowStyle.WS_EX_DLGMODALFRAME), + StyleOption(name: 'WS_EX_NOPARENTNOTIFY', value: WindowsExtendedWindowStyle.WS_EX_NOPARENTNOTIFY), + StyleOption(name: 'WS_EX_TOPMOST', value: WindowsExtendedWindowStyle.WS_EX_TOPMOST), + StyleOption(name: 'WS_EX_ACCEPTFILES', value: WindowsExtendedWindowStyle.WS_EX_ACCEPTFILES), + StyleOption(name: 'WS_EX_TRANSPARENT', value: WindowsExtendedWindowStyle.WS_EX_TRANSPARENT), + StyleOption(name: 'WS_EX_MDICHILD', value: WindowsExtendedWindowStyle.WS_EX_MDICHILD), + StyleOption(name: 'WS_EX_TOOLWINDOW', value: WindowsExtendedWindowStyle.WS_EX_TOOLWINDOW), + StyleOption(name: 'WS_EX_WINDOWEDGE', value: WindowsExtendedWindowStyle.WS_EX_WINDOWEDGE), + StyleOption(name: 'WS_EX_CLIENTEDGE', value: WindowsExtendedWindowStyle.WS_EX_CLIENTEDGE), + StyleOption(name: 'WS_EX_CONTEXTHELP', value: WindowsExtendedWindowStyle.WS_EX_CONTEXTHELP), + StyleOption(name: 'WS_EX_RIGHT', value: WindowsExtendedWindowStyle.WS_EX_RIGHT), + StyleOption(name: 'WS_EX_RTLREADING', value: WindowsExtendedWindowStyle.WS_EX_RTLREADING), + StyleOption(name: 'WS_EX_LEFTSCROLLBAR', value: WindowsExtendedWindowStyle.WS_EX_LEFTSCROLLBAR), + StyleOption(name: 'WS_EX_CONTROLPARENT', value: WindowsExtendedWindowStyle.WS_EX_CONTROLPARENT), + StyleOption(name: 'WS_EX_STATICEDGE', value: WindowsExtendedWindowStyle.WS_EX_STATICEDGE), + StyleOption(name: 'WS_EX_APPWINDOW', value: WindowsExtendedWindowStyle.WS_EX_APPWINDOW), + StyleOption(name: 'WS_EX_LAYERED', value: WindowsExtendedWindowStyle.WS_EX_LAYERED), + StyleOption(name: 'WS_EX_NOINHERITLAYOUT', value: WindowsExtendedWindowStyle.WS_EX_NOINHERITLAYOUT), + StyleOption(name: 'WS_EX_LAYOUTRTL', value: WindowsExtendedWindowStyle.WS_EX_LAYOUTRTL), + StyleOption(name: 'WS_EX_COMPOSITED', value: WindowsExtendedWindowStyle.WS_EX_COMPOSITED), + StyleOption(name: 'WS_EX_NOACTIVATE', value: WindowsExtendedWindowStyle.WS_EX_NOACTIVATE), + ]; + + // Common style presets + final Map _stylePresets = { + 'WS_OVERLAPPEDWINDOW (Default)': + 0x00CF0000, // WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX + 'WS_POPUPWINDOW': -0x80880000, // WS_POPUP | WS_BORDER | WS_SYSMENU + 'WS_CHILDWINDOW': -0x40000000, // WS_CHILD + 'WS_CAPTION': 0x00C00000, // WS_BORDER | WS_DLGFRAME + 'No Border': 0x00080000, // WS_SYSMENU only + 'Fixed Size': 0x00C80000, // WS_CAPTION | WS_SYSMENU (no WS_THICKFRAME) + }; + + // Extended style presets + final Map _extendedStylePresets = { + 'WS_EX_OVERLAPPEDWINDOW': WindowsExtendedWindowStyle.WS_EX_OVERLAPPEDWINDOW, + 'WS_EX_PALETTEWINDOW': WindowsExtendedWindowStyle.WS_EX_PALETTEWINDOW, + 'WS_EX_TOOLWINDOW': WindowsExtendedWindowStyle.WS_EX_TOOLWINDOW, + 'WS_EX_APPWINDOW': WindowsExtendedWindowStyle.WS_EX_APPWINDOW, + 'WS_EX_TOPMOST': WindowsExtendedWindowStyle.WS_EX_TOPMOST, + 'No Extended Style': 0x00000000, + }; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + _currentStyle = widget.initialStyle; + _currentExtendedStyle = widget.initialExtendedStyle; + + // Set initial checkbox states based on initial styles + _updateCheckboxesFromStyle(); + } + + void _updateCheckboxesFromStyle() { + // Update regular style checkboxes + for (var style in _styles) { + if (style.value == 0) { + // Special case for WS_OVERLAPPED (0) + style.isSelected = (_currentStyle & 0xF0000000) == 0; + } else { + style.isSelected = (_currentStyle & style.value) == style.value && style.value != 0; + } + } + + // Update extended style checkboxes + for (var style in _extendedStyles) { + style.isSelected = (_currentExtendedStyle & style.value) == style.value && style.value != 0; + } + } + + void _updateStyleFromCheckboxes() { + int newStyle = 0; + int newExtendedStyle = 0; + + // Calculate regular style + for (var style in _styles) { + if (style.isSelected && style.value != 0) { + newStyle |= style.value; + } + } + + // Calculate extended style + for (var style in _extendedStyles) { + if (style.isSelected && style.value != 0) { + newExtendedStyle |= style.value; + } + } + + // Check for incompatibilities + List warnings = _checkStyleIncompatibilities(newStyle, newExtendedStyle); + + setState(() { + _currentStyle = newStyle; + _currentExtendedStyle = newExtendedStyle; + _styleWarnings = warnings; + }); + + widget.onStyleChanged(_currentStyle, _currentExtendedStyle); + } + + List _checkStyleIncompatibilities(int style, int extendedStyle) { + List warnings = []; + + // For debugging + print('Current style: ${style.toRadixString(16)}'); + + // Check base window type - using exact matches for the high bits + bool hasPopup = style < 0 && (style & WindowsWindowStyle.WS_POPUP) == WindowsWindowStyle.WS_POPUP; + bool hasChild = style < 0 && (style & WindowsWindowStyle.WS_CHILD) == WindowsWindowStyle.WS_CHILD; + + // Only warn if BOTH popup and child are selected + if (hasPopup && hasChild) { + warnings.add("WS_POPUP and WS_CHILD can't be used together"); + } + + // Check caption-related conflicts - positive values remain the same + bool hasCaption = (style & WindowsWindowStyle.WS_CAPTION) == WindowsWindowStyle.WS_CAPTION; + bool hasSysMenu = (style & WindowsWindowStyle.WS_SYSMENU) == WindowsWindowStyle.WS_SYSMENU; + bool hasMinBox = (style & WindowsWindowStyle.WS_MINIMIZEBOX) == WindowsWindowStyle.WS_MINIMIZEBOX; + bool hasMaxBox = (style & WindowsWindowStyle.WS_MAXIMIZEBOX) == WindowsWindowStyle.WS_MAXIMIZEBOX; + + // Only check child window restrictions if it's actually a child window + if (hasChild) { + if (hasCaption) { + warnings.add("WS_CHILD windows shouldn't have WS_CAPTION"); + } + if (hasSysMenu) { + warnings.add("WS_CHILD windows shouldn't have WS_SYSMENU"); + } + if ((extendedStyle & WindowsExtendedWindowStyle.WS_EX_APPWINDOW) == WindowsExtendedWindowStyle.WS_EX_APPWINDOW) { + warnings.add("WS_CHILD can't be used with WS_EX_APPWINDOW"); + } + } + + // Min/Max box requires caption + if (!hasCaption) { + if (hasMinBox) { + warnings.add("WS_MINIMIZEBOX requires WS_CAPTION"); + } + if (hasMaxBox) { + warnings.add("WS_MAXIMIZEBOX requires WS_CAPTION"); + } + } + + // System menu usually requires caption (except for popup windows) + if (hasSysMenu && !hasCaption && !hasPopup) { + warnings.add("WS_SYSMENU usually requires WS_CAPTION (except for popup windows)"); + } + + // Window state conflicts - negative values need exact comparison + bool hasMinimize = (style & WindowsWindowStyle.WS_MINIMIZE) == WindowsWindowStyle.WS_MINIMIZE; + bool hasMaximize = (style & WindowsWindowStyle.WS_MAXIMIZE) == WindowsWindowStyle.WS_MAXIMIZE; + + if (hasMinimize && hasMaximize) { + warnings.add("Window cannot be both minimized and maximized"); + } + + // Extended style conflicts - all positive values, standard comparison works + bool hasToolWindow = (extendedStyle & WindowsExtendedWindowStyle.WS_EX_TOOLWINDOW) == WindowsExtendedWindowStyle.WS_EX_TOOLWINDOW; + bool hasAppWindow = (extendedStyle & WindowsExtendedWindowStyle.WS_EX_APPWINDOW) == WindowsExtendedWindowStyle.WS_EX_APPWINDOW; + bool hasDialogFrame = (extendedStyle & WindowsExtendedWindowStyle.WS_EX_DLGMODALFRAME) == WindowsExtendedWindowStyle.WS_EX_DLGMODALFRAME; + + if (hasToolWindow && hasAppWindow) { + warnings.add("WS_EX_TOOLWINDOW and WS_EX_APPWINDOW shouldn't be used together"); + } + + if (hasDialogFrame && hasPopup) { + warnings.add("WS_EX_DLGMODALFRAME is typically used with overlapped windows, not popup windows"); + } + + // MDI-related checks + bool hasMDIChild = (extendedStyle & WindowsExtendedWindowStyle.WS_EX_MDICHILD) == WindowsExtendedWindowStyle.WS_EX_MDICHILD; + if (hasMDIChild && !hasChild) { + warnings.add("WS_EX_MDICHILD requires WS_CHILD style"); + } + + return warnings; + } + + void _selectStylePreset(int presetValue) { + setState(() { + _currentStyle = presetValue; + _updateCheckboxesFromStyle(); + }); + widget.onStyleChanged(_currentStyle, _currentExtendedStyle); + } + + void _selectExtendedStylePreset(int presetValue) { + setState(() { + _currentExtendedStyle = presetValue; + _updateCheckboxesFromStyle(); + }); + widget.onStyleChanged(_currentStyle, _currentExtendedStyle); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + height: 32, // Even smaller height + decoration: BoxDecoration( + color: Colors.grey.shade100, + border: Border( + bottom: BorderSide( + color: Colors.grey.shade300, + width: 1, + ), + ), + ), + child: TabBar( + controller: _tabController, + tabs: const [ + Tab( + text: 'Window Styles', + height: 28, // Smaller tab height + ), + Tab( + text: 'Extended Styles', + height: 28, // Smaller tab height + ), + ], + labelColor: Theme.of(context).primaryColor, + unselectedLabelColor: Colors.grey, + indicatorColor: Theme.of(context).primaryColor, + indicatorWeight: 2, + indicatorSize: TabBarIndicatorSize.label, + labelStyle: const TextStyle( + fontSize: 12, // Smaller font + fontWeight: FontWeight.w500, + ), + unselectedLabelStyle: const TextStyle( + fontSize: 12, + ), + padding: EdgeInsets.zero, + ), + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildStylesTab(), + _buildExtendedStylesTab(), + ], + ), + ), + ], + ); + } + + Widget _buildStylesTab() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Compact presets section + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Colors.grey.shade50, + border: Border( + bottom: BorderSide( + color: Colors.grey.shade200, + width: 1, + ), + ), + ), + child: Row( + children: [ + const Text( + 'Presets:', + style: TextStyle(fontSize: 12), + ), + const SizedBox(width: 8), + Expanded( + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + value: null, + isDense: true, // Makes the dropdown more compact + hint: const Text( + 'Select a preset', + style: TextStyle(fontSize: 12), + ), + icon: const Icon(Icons.arrow_drop_down, size: 16), + style: const TextStyle(fontSize: 12, color: Colors.black), + onChanged: (String? newValue) { + if (newValue != null && _stylePresets.containsKey(newValue)) { + _selectStylePreset(_stylePresets[newValue]!); + } + }, + items: _stylePresets.keys.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text( + value, + style: const TextStyle(fontSize: 12), + ), + ); + }).toList(), + ), + ), + ), + ], + ), + ), + + // Current style value + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + child: Text( + 'Current Style: 0x${_currentStyle.toRadixString(16).padLeft(8, '0').toUpperCase()}', + style: const TextStyle(fontFamily: 'monospace', fontSize: 11), + ), + ), + + // Style checkboxes + Expanded( + child: ListView.builder( + itemCount: _styles.length, + itemBuilder: (context, index) { + final style = _styles[index]; + return CheckboxListTile( + title: Text( + style.name, + style: const TextStyle(fontSize: 12), + ), + subtitle: Text( + '0x${style.value.toRadixString(16).padLeft(8, '0').toUpperCase()}', + style: const TextStyle(fontSize: 10), + ), + value: style.isSelected, + onChanged: (bool? value) { + setState(() { + style.isSelected = value ?? false; + _updateStyleFromCheckboxes(); + }); + }, + dense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0), + visualDensity: VisualDensity.compact, + controlAffinity: ListTileControlAffinity.leading, + ); + }, + ), + ), + + if (_styleWarnings.isNotEmpty) + Container( + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.amber.shade50, + border: Border.all(color: Colors.amber.shade200), + borderRadius: BorderRadius.circular(4), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Style Warnings:', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + color: Colors.amber, + ), + ), + const SizedBox(height: 4), + ...(_styleWarnings.map( + (warning) => Text( + '• $warning', + style: const TextStyle(fontSize: 11), + ), + )), + ], + ), + ), + ], + ); + } + + Widget _buildExtendedStylesTab() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Compact presets section + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Colors.grey.shade50, + border: Border( + bottom: BorderSide( + color: Colors.grey.shade200, + width: 1, + ), + ), + ), + child: Row( + children: [ + const Text( + 'Presets:', + style: TextStyle(fontSize: 12), + ), + const SizedBox(width: 8), + Expanded( + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + value: null, + isDense: true, + hint: const Text( + 'Select a preset', + style: TextStyle(fontSize: 12), + ), + icon: const Icon(Icons.arrow_drop_down, size: 16), + style: const TextStyle(fontSize: 12, color: Colors.black), + onChanged: (String? newValue) { + if (newValue != null && _extendedStylePresets.containsKey(newValue)) { + _selectExtendedStylePreset(_extendedStylePresets[newValue]!); + } + }, + items: _extendedStylePresets.keys.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text( + value, + style: const TextStyle(fontSize: 12), + ), + ); + }).toList(), + ), + ), + ), + ], + ), + ), + + // Current extended style value + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + child: Text( + 'Current Extended Style: 0x${_currentExtendedStyle.toRadixString(16).padLeft(8, '0').toUpperCase()}', + style: const TextStyle(fontFamily: 'monospace', fontSize: 11), + ), + ), + + // Extended style checkboxes + Expanded( + child: ListView.builder( + itemCount: _extendedStyles.length, + itemBuilder: (context, index) { + final style = _extendedStyles[index]; + return CheckboxListTile( + title: Text( + style.name, + style: const TextStyle(fontSize: 12), + ), + subtitle: Text( + '0x${style.value.toRadixString(16).padLeft(8, '0').toUpperCase()}', + style: const TextStyle(fontSize: 10), + ), + value: style.isSelected, + onChanged: (bool? value) { + setState(() { + style.isSelected = value ?? false; + _updateStyleFromCheckboxes(); + }); + }, + dense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0), + visualDensity: VisualDensity.compact, + controlAffinity: ListTileControlAffinity.leading, + ); + }, + ), + ), + ], + ); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } +} + +// Helper class to store style options +class StyleOption { + final String name; + final int value; + bool isSelected; + + StyleOption({required this.name, required this.value, this.isSelected = false}); +} + +// Usage example +class WindowStyleExample extends StatefulWidget { + const WindowStyleExample({Key? key}) : super(key: key); + + @override + _WindowStyleExampleState createState() => _WindowStyleExampleState(); +} + +class _WindowStyleExampleState extends State { + int _windowStyle = 0x00CF0000; // Default WS_OVERLAPPEDWINDOW + int _extendedStyle = 0x00000100; // Default WS_EX_WINDOWEDGE + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Windows Style Selector'), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Expanded( + child: WindowStyleSelector( + initialStyle: _windowStyle, + initialExtendedStyle: _extendedStyle, + onStyleChanged: (style, extendedStyle) { + setState(() { + _windowStyle = style; + _extendedStyle = extendedStyle; + }); + }, + ), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + // Apply the window style + print('Applying window style: 0x${_windowStyle.toRadixString(16).toUpperCase()}'); + print('Applying extended style: 0x${_extendedStyle.toRadixString(16).toUpperCase()}'); + // Here you would call your platform-specific code to apply the style + }, + child: const Text('Apply Window Style'), + ), + ], + ), + ), + ); + } +} diff --git a/packages/desktop_multi_window/example/macos/Podfile b/packages/desktop_multi_window/example/macos/Podfile index dade8dfa..049abe29 100644 --- a/packages/desktop_multi_window/example/macos/Podfile +++ b/packages/desktop_multi_window/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.11' +platform :osx, '10.14' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/desktop_multi_window/example/macos/Podfile.lock b/packages/desktop_multi_window/example/macos/Podfile.lock index 819077ea..4ad453c6 100644 --- a/packages/desktop_multi_window/example/macos/Podfile.lock +++ b/packages/desktop_multi_window/example/macos/Podfile.lock @@ -19,10 +19,10 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral SPEC CHECKSUMS: - desktop_lifecycle: a600c10e12fe033c7be9078f2e929b8241f2c1e3 - desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486 - FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 + desktop_lifecycle: e4d2ff93af77bbbd473fe1a61773fdf9c5a79c91 + desktop_multi_window: 93667594ccc4b88d91a97972fd3b1b89667fa80a + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 -PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c +PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 -COCOAPODS: 1.11.3 +COCOAPODS: 1.16.2 diff --git a/packages/desktop_multi_window/example/macos/Runner.xcodeproj/project.pbxproj b/packages/desktop_multi_window/example/macos/Runner.xcodeproj/project.pbxproj index 31d3c337..bdc6b4e1 100644 --- a/packages/desktop_multi_window/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/desktop_multi_window/example/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -203,7 +203,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { @@ -295,6 +295,7 @@ }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -404,7 +405,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -483,7 +484,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -530,7 +531,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/packages/desktop_multi_window/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/desktop_multi_window/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index f5b87ed3..afc8b4a1 100644 --- a/packages/desktop_multi_window/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/desktop_multi_window/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ diff --git a/packages/desktop_multi_window/example/macos/Runner/AppDelegate.swift b/packages/desktop_multi_window/example/macos/Runner/AppDelegate.swift index d53ef643..db44369c 100644 --- a/packages/desktop_multi_window/example/macos/Runner/AppDelegate.swift +++ b/packages/desktop_multi_window/example/macos/Runner/AppDelegate.swift @@ -1,9 +1,13 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return false + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { return true } } diff --git a/packages/desktop_multi_window/example/pubspec.lock b/packages/desktop_multi_window/example/pubspec.lock index acd221f8..90d09ec7 100644 --- a/packages/desktop_multi_window/example/pubspec.lock +++ b/packages/desktop_multi_window/example/pubspec.lock @@ -5,42 +5,42 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.12.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" cupertino_icons: dependency: "direct main" description: @@ -63,15 +63,15 @@ packages: path: ".." relative: true source: path - version: "0.2.0" + version: "0.2.1" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" flutter: dependency: "direct main" description: flutter @@ -94,18 +94,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -126,10 +126,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -142,18 +142,18 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" sky_engine: dependency: transitive description: flutter @@ -163,50 +163,50 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.4" vector_math: dependency: transitive description: @@ -219,10 +219,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "14.3.1" sdks: - dart: ">=3.4.0 <4.0.0" + dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/packages/desktop_multi_window/example/pubspec.yaml b/packages/desktop_multi_window/example/pubspec.yaml index 81479a36..175fcf06 100644 --- a/packages/desktop_multi_window/example/pubspec.yaml +++ b/packages/desktop_multi_window/example/pubspec.yaml @@ -6,7 +6,7 @@ description: Demonstrates how to use the flutter_multi_window plugin. publish_to: 'none' # Remove this line if you wish to publish to pub.dev environment: - sdk: ">=2.14.0 <3.0.0" + sdk: ">=3.0.0 <4.0.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions diff --git a/packages/desktop_multi_window/example/windows/flutter/CMakeLists.txt b/packages/desktop_multi_window/example/windows/flutter/CMakeLists.txt index b2e4bd8d..4f2af69b 100644 --- a/packages/desktop_multi_window/example/windows/flutter/CMakeLists.txt +++ b/packages/desktop_multi_window/example/windows/flutter/CMakeLists.txt @@ -9,6 +9,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -91,7 +96,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/packages/desktop_multi_window/lib/desktop_multi_window.dart b/packages/desktop_multi_window/lib/desktop_multi_window.dart index eb14e4c2..b1baafad 100644 --- a/packages/desktop_multi_window/lib/desktop_multi_window.dart +++ b/packages/desktop_multi_window/lib/desktop_multi_window.dart @@ -5,8 +5,22 @@ import 'package:flutter/services.dart'; import 'src/channels.dart'; import 'src/window_controller.dart'; import 'src/window_controller_impl.dart'; +import 'src/window_options.dart'; export 'src/window_controller.dart'; +export 'src/window_options.dart'; +export 'src/windows/window_options.dart'; +export 'src/windows/extended_window_style.dart'; +export 'src/windows/window_style.dart'; +export 'src/macos/window_options.dart'; +export 'src/macos/window_level.dart'; +export 'src/macos/window_style_mask.dart'; +export 'src/macos/window_type.dart'; +export 'src/macos/window_backing.dart'; +export 'src/macos/title_visibility.dart'; +export 'src/macos/animation_behavior.dart'; +export 'src/window_events.dart'; +export 'src/macos/window_collection_behavior.dart'; class DesktopMultiWindow { /// Create a new Window. @@ -26,14 +40,20 @@ class DesktopMultiWindow { /// /// NOTE: [createWindow] will only create a new window, you need to call /// [WindowController.show] to show the window. - static Future createWindow([String? arguments]) async { + static Future createWindow([String? arguments, WindowOptions? options]) async { + + final Map args = { + if (arguments != null) 'arguments': arguments, + if (options != null) 'options': options.toJson(), + }; + final windowId = await multiWindowChannel.invokeMethod( 'createWindow', - arguments, + args, ); assert(windowId != null, 'windowId is null'); assert(windowId! > 0, 'id must be greater than 0'); - return WindowControllerMainImpl(windowId!); + return WindowControllerImpl(windowId!); } /// Invoke method on the isolate of the window. @@ -42,9 +62,8 @@ class DesktopMultiWindow { /// method. /// /// [targetWindowId] which window you want to invoke the method. - static Future invokeMethod(int targetWindowId, String method, - [dynamic arguments]) { - return windowEventChannel.invokeMethod(method, { + static Future invokeMethod(int targetWindowId, String method, [dynamic arguments]) { + return interWindowEventChannel.invokeMethod(method, { 'targetWindowId': targetWindowId, 'arguments': arguments, }); @@ -56,25 +75,22 @@ class DesktopMultiWindow { /// for example: you can not receive the method call which target window isn't /// main window in main window isolate. /// - static void setMethodHandler( - Future Function(MethodCall call, int fromWindowId)? handler) { + static void setMethodHandler(Future Function(MethodCall call, int fromWindowId)? handler) { if (handler == null) { - windowEventChannel.setMethodCallHandler(null); + interWindowEventChannel.setMethodCallHandler(null); return; } - windowEventChannel.setMethodCallHandler((call) async { + interWindowEventChannel.setMethodCallHandler((call) async { final fromWindowId = call.arguments['fromWindowId'] as int; final arguments = call.arguments['arguments']; - final result = - await handler(MethodCall(call.method, arguments), fromWindowId); + final result = await handler(MethodCall(call.method, arguments), fromWindowId); return result; }); } /// Get all sub window id. static Future> getAllSubWindowIds() async { - final result = await multiWindowChannel - .invokeMethod>('getAllSubWindowIds'); + final result = await multiWindowChannel.invokeMethod>('getAllSubWindowIds'); final ids = result?.cast() ?? const []; assert(!ids.contains(0), 'ids must not contains main window id'); assert(ids.every((id) => id > 0), 'id must be greater than 0'); diff --git a/packages/desktop_multi_window/lib/src/channels.dart b/packages/desktop_multi_window/lib/src/channels.dart index 86c4f1ba..44e7cdb1 100644 --- a/packages/desktop_multi_window/lib/src/channels.dart +++ b/packages/desktop_multi_window/lib/src/channels.dart @@ -2,6 +2,6 @@ import 'package:flutter/services.dart'; const multiWindowChannel = MethodChannel('mixin.one/flutter_multi_window'); -const windowEventChannel = MethodChannel( - 'mixin.one/flutter_multi_window_channel', -); +const windowEventsChannel = MethodChannel('mixin.one/flutter_multi_window_events_channel'); + +const interWindowEventChannel = MethodChannel('mixin.one/flutter_multi_window_inter_window_event_channel'); diff --git a/packages/desktop_multi_window/lib/src/extensions.dart b/packages/desktop_multi_window/lib/src/extensions.dart new file mode 100644 index 00000000..7e88cfab --- /dev/null +++ b/packages/desktop_multi_window/lib/src/extensions.dart @@ -0,0 +1,12 @@ +import 'dart:ui'; + +extension ColorExtension on Color { + Map toJson() { + return { + 'red': r, + 'green': g, + 'blue': b, + 'alpha': a, + }; + } +} \ No newline at end of file diff --git a/packages/desktop_multi_window/lib/src/macos/animation_behavior.dart b/packages/desktop_multi_window/lib/src/macos/animation_behavior.dart new file mode 100644 index 00000000..853e9022 --- /dev/null +++ b/packages/desktop_multi_window/lib/src/macos/animation_behavior.dart @@ -0,0 +1,17 @@ +/// The animation behavior options for a macOS window. +enum MacOsAnimationBehavior { + /// Use the default animation behavior. + defaultBehavior('default'), + + /// No animations. + none('none'), + + /// Document window animation. + documentWindow('documentWindow'), + + /// Alert panel animation. + alertPanel('alertPanel'); + + final String value; + const MacOsAnimationBehavior(this.value); +} diff --git a/packages/desktop_multi_window/lib/src/macos/title_visibility.dart b/packages/desktop_multi_window/lib/src/macos/title_visibility.dart new file mode 100644 index 00000000..a2ecbba5 --- /dev/null +++ b/packages/desktop_multi_window/lib/src/macos/title_visibility.dart @@ -0,0 +1,11 @@ +/// The title visibility options for a macOS window. +enum MacOsTitleVisibility { + /// The window title is visible. + visible('visible'), + + /// The window title is hidden. + hidden('hidden'); + + final String value; + const MacOsTitleVisibility(this.value); +} diff --git a/packages/desktop_multi_window/lib/src/macos/window_backing.dart b/packages/desktop_multi_window/lib/src/macos/window_backing.dart new file mode 100644 index 00000000..9263e5a0 --- /dev/null +++ b/packages/desktop_multi_window/lib/src/macos/window_backing.dart @@ -0,0 +1,14 @@ +/// The backing store type for a macOS window. +enum MacOsWindowBacking { + /// NSBackingStoreBuffered – the common choice. + buffered('buffered'), + + /// NSBackingStoreRetained – rarely used in modern Cocoa. + retained('retained'), + + /// NSBackingStoreNonretained. + nonretained('nonretained'); + + final String value; + const MacOsWindowBacking(this.value); +} diff --git a/packages/desktop_multi_window/lib/src/macos/window_collection_behavior.dart b/packages/desktop_multi_window/lib/src/macos/window_collection_behavior.dart new file mode 100644 index 00000000..2aeebfbf --- /dev/null +++ b/packages/desktop_multi_window/lib/src/macos/window_collection_behavior.dart @@ -0,0 +1,54 @@ +/// Window collection behavior options for macOS. +/// +/// These values correspond to NSWindowCollectionBehavior in macOS. +class MacOsWindowCollectionBehavior { + /// Default window collection behavior. + static const int default_ = 0; + + /// Window can be shown/hidden with Mission Control. + static const int managed = 1 << 0; + + /// Window is transient and won't be shown in Mission Control. + static const int transient = 1 << 1; + + /// Window can be shown in different spaces. + static const int stationary = 1 << 2; + + /// Window participates in Mission Control window selection. + static const int participatesInCycle = 1 << 3; + + /// Window ignores Mission Control window selection. + static const int ignoresCycle = 1 << 4; + + /// Window can be shown in full screen mode. + static const int fullScreenPrimary = 1 << 7; + + /// Window is an auxiliary window in full screen mode. + static const int fullScreenAuxiliary = 1 << 8; + + /// Window can move between spaces. + static const int moveToActiveSpace = 1 << 9; + + /// Window follows active space. + static const int followsActiveSpace = 1 << 10; + + /// Window can be shown on all spaces. + static const int canJoinAllSpaces = 1 << 11; + + /// Window can be shown on all spaces in full screen mode. + static const int fullScreenAllowsTiling = 1 << 12; + + /// Window disallows tiling in full screen mode. + static const int fullScreenDisallowsTiling = 1 << 13; + + /// Combine multiple behaviors using bitwise OR operator. + /// + /// Example: + /// ```dart + /// final behavior = MacOsWindowCollectionBehavior.managed | + /// MacOsWindowCollectionBehavior.participatesInCycle; + /// ``` + static int combine(List behaviors) { + return behaviors.fold(MacOsWindowCollectionBehavior.default_, (a, b) => a | b); + } +} diff --git a/packages/desktop_multi_window/lib/src/macos/window_level.dart b/packages/desktop_multi_window/lib/src/macos/window_level.dart new file mode 100644 index 00000000..ff4fcc64 --- /dev/null +++ b/packages/desktop_multi_window/lib/src/macos/window_level.dart @@ -0,0 +1,30 @@ +/// Defines macOS window levels. +/// These values determine the z-ordering of windows. +enum MacOsWindowLevel { + /// Standard window level. + normal(0), + + /// Floating window level; appears above normal windows. + floating(3), + + /// Modal panel level; typically used for dialog boxes. + modalPanel(8), + + /// Submenu window level. + subMenu(4), + + /// Main menu window level. + mainMenu(24), + + /// Status window level. + status(25), + + /// Popup menu window level. + popUpMenu(101), + + /// Screen saver window level; highest level. + screenSaver(1000); + + final int value; + const MacOsWindowLevel(this.value); +} diff --git a/packages/desktop_multi_window/lib/src/macos/window_options.dart b/packages/desktop_multi_window/lib/src/macos/window_options.dart new file mode 100644 index 00000000..47afe409 --- /dev/null +++ b/packages/desktop_multi_window/lib/src/macos/window_options.dart @@ -0,0 +1,232 @@ +import 'package:flutter/material.dart'; + +import '../extensions.dart'; +import 'window_collection_behavior.dart'; +import 'window_level.dart'; +import 'window_style_mask.dart'; +import 'window_type.dart'; +import 'window_backing.dart'; +import 'title_visibility.dart'; +import 'animation_behavior.dart'; + +class MacOSWindowOptions { + // Common properties + final MacOsWindowType type; + final MacOsWindowLevel level; + final Set styleMask; + final Set collectionBehavior; + final int left; + final int top; + final int width; + final int height; + final String title; + final bool isOpaque; + final bool hasShadow; + final bool isMovable; + final MacOsWindowBacking backing; + final Color backgroundColor; + final bool windowButtonVisibility; + + // NSWindow-only properties + final bool isModal; + final MacOsTitleVisibility titleVisibility; + final bool titlebarAppearsTransparent; + final bool ignoresMouseEvents; + final bool acceptsMouseMovedEvents; + final MacOsAnimationBehavior animationBehavior; + + const MacOSWindowOptions({ + this.type = MacOsWindowType.NSWindow, + this.level = MacOsWindowLevel.normal, + // Default style mask is arbitrary; typically you’ll override this. + this.styleMask = const { + MacOsWindowStyleMask.miniaturizable, + MacOsWindowStyleMask.closable, + MacOsWindowStyleMask.resizable, + MacOsWindowStyleMask.titled, + MacOsWindowStyleMask.fullSizeContentView, + }, + this.left = 10, + this.top = 10, + this.width = 1280, + this.height = 720, + this.title = '', + this.isOpaque = true, + this.hasShadow = true, + this.isMovable = true, + this.backing = MacOsWindowBacking.buffered, + this.backgroundColor = const Color(0x00000000), + this.windowButtonVisibility = true, + // NSWindow-specific defaults: + this.isModal = false, + this.titleVisibility = MacOsTitleVisibility.visible, + this.titlebarAppearsTransparent = false, + this.collectionBehavior = const {MacOsWindowCollectionBehavior.default_}, + this.ignoresMouseEvents = false, + this.acceptsMouseMovedEvents = false, + this.animationBehavior = MacOsAnimationBehavior.defaultBehavior, + }); /* : assert( + type != MacOSWindowType.NSPanel || (styleMask & MacOSWindowStyleMask.utility) != 0, + 'NSPanel requires the utility style mask to be set.', + ), + assert( + type != MacOSWindowType.NSPanel || isModal == false, + 'NSPanel cannot be modal.', + ), + assert( + type != MacOSWindowType.NSPanel || titleVisibility == MacOSTitleVisibility.hidden, + 'NSPanel should not have a visible title.', + ), + assert( + type != MacOSWindowType.NSPanel || collectionBehavior == 0, + 'NSPanel does not support collectionBehavior.', + );*/ + + /// Convenience factory constructor for NSPanel. + factory MacOSWindowOptions.nspanel({ + Set styleMask = const { + MacOsWindowStyleMask.titled, + MacOsWindowStyleMask.closable, + MacOsWindowStyleMask.miniaturizable, + MacOsWindowStyleMask.utility + }, + MacOsWindowLevel level = MacOsWindowLevel.floating, + int left = 10, + int top = 10, + int width = 1280, + int height = 720, + String title = '', + Color backgroundColor = const Color(0x00000000), + bool windowButtonVisibility = true, + // NSPanel-specific: force non-modal, hide title. + MacOsWindowBacking backing = MacOsWindowBacking.buffered, + bool isOpaque = true, + bool hasShadow = true, + bool isMovable = true, + // For panels, we force title to be hidden. + MacOsTitleVisibility titleVisibility = MacOsTitleVisibility.hidden, + bool titlebarAppearsTransparent = false, + Set collectionBehavior = const { + MacOsWindowCollectionBehavior.default_ + }, + bool ignoresMouseEvents = false, + bool acceptsMouseMovedEvents = false, + MacOsAnimationBehavior animationBehavior = + MacOsAnimationBehavior.defaultBehavior, + }) { + if (!styleMask.contains(MacOsWindowStyleMask.utility)) { + styleMask.add(MacOsWindowStyleMask.utility); + } + return MacOSWindowOptions( + type: MacOsWindowType.NSPanel, + level: level, + styleMask: styleMask, + left: left, + top: top, + width: width, + height: height, + title: title, + isOpaque: isOpaque, + hasShadow: hasShadow, + isMovable: isMovable, + backing: backing, + backgroundColor: backgroundColor, + windowButtonVisibility: windowButtonVisibility, + // Enforce NSPanel restrictions: + isModal: false, + titleVisibility: titleVisibility, + titlebarAppearsTransparent: titlebarAppearsTransparent, + collectionBehavior: collectionBehavior, + ignoresMouseEvents: ignoresMouseEvents, + acceptsMouseMovedEvents: acceptsMouseMovedEvents, + animationBehavior: animationBehavior, + ); + } + + /// Convenience factory constructor for NSWindow. + factory MacOSWindowOptions.nswindow({ + Set styleMask = const { + MacOsWindowStyleMask.miniaturizable, + MacOsWindowStyleMask.closable, + MacOsWindowStyleMask.resizable, + MacOsWindowStyleMask.titled, + MacOsWindowStyleMask.fullSizeContentView, + }, + MacOsWindowLevel level = MacOsWindowLevel.normal, + int left = 10, + int top = 10, + int width = 1280, + int height = 720, + String title = '', + bool isModal = false, + bool isOpaque = true, + MacOsWindowBacking backing = MacOsWindowBacking.buffered, + Color backgroundColor = const Color(0x00000000), + bool windowButtonVisibility = true, + bool hasShadow = true, + bool isMovable = true, + MacOsTitleVisibility titleVisibility = MacOsTitleVisibility.visible, + bool titlebarAppearsTransparent = false, + Set collectionBehavior = const { + MacOsWindowCollectionBehavior.default_ + }, + bool ignoresMouseEvents = false, + bool acceptsMouseMovedEvents = false, + MacOsAnimationBehavior animationBehavior = + MacOsAnimationBehavior.defaultBehavior, + }) { + return MacOSWindowOptions( + type: MacOsWindowType.NSWindow, + level: level, + styleMask: styleMask, + left: left, + top: top, + width: width, + height: height, + title: title, + isModal: isModal, + isOpaque: isOpaque, + backing: backing, + backgroundColor: backgroundColor, + windowButtonVisibility: windowButtonVisibility, + hasShadow: hasShadow, + isMovable: isMovable, + titleVisibility: titleVisibility, + titlebarAppearsTransparent: titlebarAppearsTransparent, + collectionBehavior: collectionBehavior, + ignoresMouseEvents: ignoresMouseEvents, + acceptsMouseMovedEvents: acceptsMouseMovedEvents, + animationBehavior: animationBehavior, + ); + } + + /// Converts the window options to a JSON-compatible map. + /// The resulting map only includes the keys that are allowed for the specified [type]. + Map toJson() { + // Common properties for both NSWindow and NSPanel. + return { + 'type': type.name, + 'level': level.value, + 'styleMask': styleMask.fold(0, (a, b) => a | b), + 'collectionBehavior': collectionBehavior.fold(0, (a, b) => a | b), + 'left': left, + 'top': top, + 'width': width, + 'height': height, + 'title': title, + 'isOpaque': isOpaque, + 'hasShadow': hasShadow, + 'isMovable': isMovable, + 'backing': backing.value, + 'backgroundColor': backgroundColor.toJson(), + 'windowButtonVisibility': windowButtonVisibility, + + 'isModal': isModal, + 'titleVisibility': titleVisibility.value, + 'titlebarAppearsTransparent': titlebarAppearsTransparent, + 'ignoresMouseEvents': ignoresMouseEvents, + 'acceptsMouseMovedEvents': acceptsMouseMovedEvents, + 'animationBehavior': animationBehavior.value, + }; + } +} diff --git a/packages/desktop_multi_window/lib/src/macos/window_style_mask.dart b/packages/desktop_multi_window/lib/src/macos/window_style_mask.dart new file mode 100644 index 00000000..b959517a --- /dev/null +++ b/packages/desktop_multi_window/lib/src/macos/window_style_mask.dart @@ -0,0 +1,49 @@ +// ignore_for_file: constant_identifier_names + +/// A collection of macOS window style mask constants corresponding to NSWindow.StyleMask. +/// These constants can be combined with the bitwise OR operator to configure an NSWindow or NSPanel. +/// +/// Note: Some values (like `texturedBackground`) are deprecated or less commonly used, +/// and the specific bit assignments should be verified against your target macOS version. +class MacOsWindowStyleMask { + /// A borderless window with no title bar or controls. + /// (Equivalent to an empty set of style masks.) + static const int borderless = 0; + + /// A window with a title bar. + static const int titled = 1 << 0; + + /// A window with a close button. + static const int closable = 1 << 1; + + /// A window with a minimize button. + static const int miniaturizable = 1 << 2; + + /// A window with a resizable border. + static const int resizable = 1 << 3; + + /// A window that supports full screen mode. + static const int fullScreen = 1 << 4; + + /// A window with a textured background. + /// (Deprecated in modern macOS versions but may still be available.) + static const int texturedBackground = 1 << 5; + + // Bit 6 is unused/reserved. + + /// A utility window (often used for panels) with a smaller title bar. + static const int utility = 1 << 7; + + /// A window with a unified title bar and toolbar. + static const int unifiedTitleAndToolbar = 1 << 8; + + /// A panel that does not activate when clicked. + /// (Typically used with NSPanel to prevent it from taking focus.) + static const int nonactivatingPanel = 1 << 9; + + // Bits 10 through 14 are reserved. + + /// A window whose content view extends to cover the full window area, + /// including the title bar (commonly used for modern fullscreen content). + static const int fullSizeContentView = 1 << 15; +} diff --git a/packages/desktop_multi_window/lib/src/macos/window_type.dart b/packages/desktop_multi_window/lib/src/macos/window_type.dart new file mode 100644 index 00000000..1049ed9a --- /dev/null +++ b/packages/desktop_multi_window/lib/src/macos/window_type.dart @@ -0,0 +1,6 @@ +// ignore_for_file: constant_identifier_names + +enum MacOsWindowType { + NSWindow, + NSPanel, +} diff --git a/packages/desktop_multi_window/lib/src/window_channel.dart b/packages/desktop_multi_window/lib/src/window_channel.dart index cce76314..cf4d613f 100644 --- a/packages/desktop_multi_window/lib/src/window_channel.dart +++ b/packages/desktop_multi_window/lib/src/window_channel.dart @@ -1,17 +1,17 @@ -import 'package:flutter/services.dart'; +// import 'package:flutter/services.dart'; -import 'channels.dart'; +// import 'channels.dart'; -typedef MessageHandler = Future Function(MethodCall call); +// typedef MessageHandler = Future Function(MethodCall call); -class ClientMessageChannel { - const ClientMessageChannel(); +// class ClientMessageChannel { +// const ClientMessageChannel(); - Future invokeMethod(String method, [dynamic arguments]) { - return windowEventChannel.invokeMethod(method, arguments); - } +// Future invokeMethod(String method, [dynamic arguments]) { +// return windowEventsChannel.invokeMethod(method, arguments); +// } - void setMessageHandler(MessageHandler? handler) { - windowEventChannel.setMethodCallHandler(handler); - } -} +// void setMessageHandler(MessageHandler? handler) { +// windowEventsChannel.setMethodCallHandler(handler); +// } +// } diff --git a/packages/desktop_multi_window/lib/src/window_controller.dart b/packages/desktop_multi_window/lib/src/window_controller.dart index 0489add6..e8373757 100644 --- a/packages/desktop_multi_window/lib/src/window_controller.dart +++ b/packages/desktop_multi_window/lib/src/window_controller.dart @@ -1,23 +1,39 @@ import 'dart:ui'; +import 'macos/window_level.dart'; import 'window_controller_impl.dart'; +import 'window_events.dart'; /// The [WindowController] instance that is used to control this window. abstract class WindowController { + static final List _controllers = []; + WindowController(); factory WindowController.fromWindowId(int id) { - return WindowControllerMainImpl(id); + final controller = _controllers.firstWhere( + (controller) => controller.windowId == id, + orElse: () { + final controller = WindowControllerImpl(id); + _controllers.add(controller); + return controller; + }, + ); + return controller; } factory WindowController.main() { - return WindowControllerMainImpl(0); + return WindowController.fromWindowId(0); } /// The id of the window. /// 0 means the main window. int get windowId; + void addListener(WindowEvents listener); + + void removeListener(WindowEvents listener); + /// Close the window. Future close(); @@ -27,8 +43,23 @@ abstract class WindowController { /// Hide the window. Future hide(); + /// Get the window frame rect. + Future getFrame(); + /// Set the window frame rect. - Future setFrame(Rect frame); + Future setFrame(Rect frame, {bool animate = false, double devicePixelRatio = 1.0}); + + /// Get the window size. + Future getSize(); + + /// Set the window size. + Future setSize(Size size, {bool animate = false, double devicePixelRatio = 1.0}); + + /// Get the window position. + Future getPosition(); + + /// Set the window position. + Future setPosition(Offset position, {bool animate = false, double devicePixelRatio = 1.0}); /// Center the window on the screen. Future center(); @@ -36,6 +67,39 @@ abstract class WindowController { /// Set the window's title. Future setTitle(String title); + Future isFocused(); + + Future isVisible(); + + Future isMaximized(); + + Future maximize({bool vertically = false}); + + Future unmaximize(); + + Future isMinimized(); + + Future minimize(); + + Future restore(); + + Future isFullScreen(); + + Future setFullScreen(bool isFullScreen); + + Future setStyle({ + int? styleMask, + int? collectionBehavior, + MacOsWindowLevel? level, + bool? isOpaque, + bool? hasShadow, + Color? backgroundColor, + int? style, + int? extendedStyle, + }); + + Future setBackgroundColor(Color backgroundColor); + /// Whether the window can be resized. Available only on macOS. /// /// Most useful for ensuring windows *cannot* be resized. Windows are @@ -45,4 +109,7 @@ abstract class WindowController { /// Available only on macOS. Future setFrameAutosaveName(String name); + + /// Whether the window can receive mouse events. + Future setIgnoreMouseEvents(bool ignore); } diff --git a/packages/desktop_multi_window/lib/src/window_controller_impl.dart b/packages/desktop_multi_window/lib/src/window_controller_impl.dart index e929a831..76901160 100644 --- a/packages/desktop_multi_window/lib/src/window_controller_impl.dart +++ b/packages/desktop_multi_window/lib/src/window_controller_impl.dart @@ -1,50 +1,200 @@ import 'dart:io'; import 'dart:ui'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import '../desktop_multi_window.dart'; import 'channels.dart'; -import 'window_controller.dart'; +import 'extensions.dart'; -class WindowControllerMainImpl extends WindowController { +class WindowControllerImpl extends WindowController { final MethodChannel _channel = multiWindowChannel; + final MethodChannel _windowEventsChannel = windowEventsChannel; // the id of this window final int _id; - WindowControllerMainImpl(this._id); + WindowControllerImpl(this._id); + + final ObserverList _listeners = ObserverList(); + + Future _methodCallHandler(MethodCall call) async { + for (final WindowEvents listener in listeners) { + if (!_listeners.contains(listener)) { + return; + } + + if (call.method != 'onEvent') throw UnimplementedError(); + + final String eventName = call.arguments['eventName']; + final dynamic rawEventData = call.arguments['eventData']; + final Map? eventData = + rawEventData != null ? Map.from(rawEventData as Map) : null; + + listener.onWindowEvent(eventName, eventData); + Map funcMap = { + kWindowEventClose: listener.onWindowClose, + kWindowEventShow: listener.onWindowShow, + kWindowEventHide: listener.onWindowHide, + kWindowEventFocus: listener.onWindowFocus, + kWindowEventBlur: listener.onWindowBlur, + kWindowEventMaximize: listener.onWindowMaximize, + kWindowEventUnmaximize: listener.onWindowUnmaximize, + kWindowEventMinimize: listener.onWindowMinimize, + kWindowEventRestore: listener.onWindowRestore, + kWindowEventResize: listener.onWindowResize, + kWindowEventResized: listener.onWindowResized, + kWindowEventMove: listener.onWindowMove, + kWindowEventMoved: listener.onWindowMoved, + kWindowEventEnterFullScreen: listener.onWindowEnterFullScreen, + kWindowEventLeaveFullScreen: listener.onWindowLeaveFullScreen, + kWindowEventDocked: listener.onWindowDocked, + kWindowEventUndocked: listener.onWindowUndocked, + kWindowEventMouseMove: (Map? eventData) { + final x = (eventData?['x'] as num?)?.toInt() ?? 0; + final y = (eventData?['y'] as num?)?.toInt() ?? 0; + listener.onMouseMove(x, y); + }, + }; + if (eventData != null) { + funcMap[eventName]?.call(eventData); + } else { + funcMap[eventName]?.call(); + } + } + } + + List get listeners { + final List localListeners = List.from(_listeners); + return localListeners; + } + + bool get hasListeners { + return _listeners.isNotEmpty; + } + + @override + void addListener(WindowEvents listener) { + if (_listeners.contains(listener)) { + return; + } + _listeners.add(listener); + if (hasListeners) { + _windowEventsChannel.setMethodCallHandler(_methodCallHandler); + _channel.invokeMethod('setHasListeners', { + 'windowId': _id, + 'hasListeners': true, + }); + } + } + + @override + void removeListener(WindowEvents listener) { + if (!_listeners.contains(listener)) { + return; + } + _listeners.remove(listener); + if (!hasListeners) { + _windowEventsChannel.setMethodCallHandler(null); + _channel.invokeMethod('setHasListeners', { + 'windowId': _id, + 'hasListeners': false, + }); + } + } + + double getDevicePixelRatio() { + // Subsequent version, remove this deprecated member. + // ignore: deprecated_member_use + return window.devicePixelRatio; + } @override int get windowId => _id; @override Future close() { - return _channel.invokeMethod('close', _id); + return _channel.invokeMethod('close', {'windowId': _id}); } @override Future hide() { - return _channel.invokeMethod('hide', _id); + return _channel.invokeMethod('hide', {'windowId': _id}); } @override Future show() { - return _channel.invokeMethod('show', _id); + return _channel.invokeMethod('show', {'windowId': _id}); } @override Future center() { - return _channel.invokeMethod('center', _id); + return _channel.invokeMethod('center', {'windowId': _id}); + } + + @override + Future getFrame() async { + final Map arguments = { + 'windowId': _id, + 'devicePixelRatio': getDevicePixelRatio(), + }; + final Map resultData = await _channel.invokeMethod( + 'getFrame', + arguments, + ); + return Rect.fromLTWH( + resultData['left'], + resultData['top'], + resultData['width'], + resultData['height'], + ); } @override - Future setFrame(Rect frame) { + Future setFrame(Rect frame, {bool animate = false, double devicePixelRatio = 1.0}) { return _channel.invokeMethod('setFrame', { 'windowId': _id, 'left': frame.left, 'top': frame.top, 'width': frame.width, 'height': frame.height, + 'animate': animate, + 'devicePixelRatio': devicePixelRatio, + }); + } + + @override + Future getSize() async { + final Rect frame = await getFrame(); + return frame.size; + } + + @override + Future setSize(Size size, {bool animate = false, double devicePixelRatio = 1.0}) { + return _channel.invokeMethod('setFrame', { + 'windowId': _id, + 'width': size.width, + 'height': size.height, + 'animate': animate, + 'devicePixelRatio': devicePixelRatio, + }); + } + + @override + Future getPosition() async { + final Rect frame = await getFrame(); + return frame.topLeft; + } + + @override + Future setPosition(Offset position, {bool animate = false, double devicePixelRatio = 1.0}) { + return _channel.invokeMethod('setFrame', { + 'windowId': _id, + 'left': position.dx, + 'top': position.dy, + 'animate': animate, + 'devicePixelRatio': devicePixelRatio, }); } @@ -77,4 +227,100 @@ class WindowControllerMainImpl extends WindowController { 'name': name, }); } + + @override + Future isFocused() async { + return await _channel.invokeMethod('isFocused', {'windowId': _id}); + } + + @override + Future isFullScreen() async { + return await _channel.invokeMethod('isFullScreen', {'windowId': _id}); + } + + @override + Future isMaximized() async { + return await _channel.invokeMethod('isMaximized', {'windowId': _id}); + } + + @override + Future isMinimized() async { + return await _channel.invokeMethod('isMinimized', {'windowId': _id}); + } + + @override + Future isVisible() async { + return await _channel.invokeMethod('isVisible', {'windowId': _id}); + } + + @override + Future maximize({bool vertically = false}) async { + return await _channel.invokeMethod('maximize', {'windowId': _id, 'vertically': vertically}); + } + + @override + Future unmaximize() async { + return await _channel.invokeMethod('unmaximize', {'windowId': _id}); + } + + @override + Future minimize() async { + return await _channel.invokeMethod('minimize', {'windowId': _id}); + } + + @override + Future restore() async { + return await _channel.invokeMethod('restore', {'windowId': _id}); + } + + @override + Future setFullScreen(bool isFullScreen) async { + return await _channel.invokeMethod('setFullScreen', {'windowId': _id, 'isFullScreen': isFullScreen}); + } + + @override + Future setStyle({ + // macOS parameters + int? styleMask, + int? collectionBehavior, + MacOsWindowLevel? level, + bool? isOpaque, + bool? hasShadow, + Color? backgroundColor, + // Windows parameters + int? style, + int? extendedStyle, + }) async { + if (Platform.isMacOS) { + return await _channel.invokeMethod('setStyle', { + 'windowId': _id, + if (styleMask != null) 'styleMask': styleMask, + if (collectionBehavior != null) 'collectionBehavior': collectionBehavior, + if (level != null) 'level': level.value, + if (isOpaque != null) 'isOpaque': isOpaque, + if (hasShadow != null) 'hasShadow': hasShadow, + if (backgroundColor != null) 'backgroundColor': backgroundColor.toJson(), + }); + } else if (Platform.isWindows) { + return await _channel.invokeMethod('setStyle', { + 'windowId': _id, + if (style != null) 'style': style, + if (extendedStyle != null) 'extendedStyle': extendedStyle, + if (backgroundColor != null) 'backgroundColor': backgroundColor.toJson(), + }); + } + } + + @override + Future setBackgroundColor(Color backgroundColor) async { + return await _channel.invokeMethod('setBackgroundColor', { + 'windowId': _id, + 'backgroundColor': backgroundColor.toJson(), + }); + } + + @override + Future setIgnoreMouseEvents(bool ignore) async { + return await _channel.invokeMethod('setIgnoreMouseEvents', {'windowId': _id, 'ignore': ignore}); + } } diff --git a/packages/desktop_multi_window/lib/src/window_events.dart b/packages/desktop_multi_window/lib/src/window_events.dart new file mode 100644 index 00000000..f2895988 --- /dev/null +++ b/packages/desktop_multi_window/lib/src/window_events.dart @@ -0,0 +1,87 @@ +const kWindowEventClose = 'close'; +const kWindowEventShow = 'show'; +const kWindowEventHide = 'hide'; +const kWindowEventFocus = 'focus'; +const kWindowEventBlur = 'blur'; +const kWindowEventMaximize = 'maximize'; +const kWindowEventUnmaximize = 'unmaximize'; +const kWindowEventMinimize = 'minimize'; +const kWindowEventRestore = 'restore'; +const kWindowEventResize = 'resize'; +const kWindowEventResized = 'resized'; +const kWindowEventMove = 'move'; +const kWindowEventMoved = 'moved'; +const kWindowEventEnterFullScreen = 'enter-full-screen'; +const kWindowEventLeaveFullScreen = 'leave-full-screen'; + +const kWindowEventDocked = 'docked'; +const kWindowEventUndocked = 'undocked'; + +const kWindowEventMouseMove = 'mouse-move'; + +abstract mixin class WindowEvents { + /// Emitted when the window is going to be closed. + void onWindowClose() {} + + /// Emitted when the window is shown. + void onWindowShow() {} + + /// Emitted when the window is hidden. + void onWindowHide() {} + + /// Emitted when the window gains focus. + void onWindowFocus() {} + + /// Emitted when the window loses focus. + void onWindowBlur() {} + + /// Emitted when window is maximized. + void onWindowMaximize() {} + + /// Emitted when the window exits from a maximized state. + void onWindowUnmaximize() {} + + /// Emitted when the window is minimized. + void onWindowMinimize() {} + + /// Emitted when the window is restored from a minimized state. + void onWindowRestore() {} + + /// Emitted after the window has been resized. + void onWindowResize() {} + + /// Emitted once when the window has finished being resized. + /// + /// @platforms macos,windows + void onWindowResized() {} + + /// Emitted when the window is being moved to a new position. + void onWindowMove() {} + + /// Emitted once when the window is moved to a new position. + /// + /// @platforms macos,windows + void onWindowMoved() {} + + /// Emitted when the window enters a full-screen state. + void onWindowEnterFullScreen() {} + + /// Emitted when the window leaves a full-screen state. + void onWindowLeaveFullScreen() {} + + /// Emitted when the window entered a docked state. + /// + /// @platforms windows + void onWindowDocked() {} + + /// Emitted when the window leaves a docked state. + /// + /// @platforms windows + void onWindowUndocked() {} + + /// Emitted when the mouse is moved. + void onMouseMove(int x, int y) {} + + /// Emitted all events. + void onWindowEvent(String eventName, Map? eventData) {} +} diff --git a/packages/desktop_multi_window/lib/src/window_options.dart b/packages/desktop_multi_window/lib/src/window_options.dart new file mode 100644 index 00000000..2f0cc82b --- /dev/null +++ b/packages/desktop_multi_window/lib/src/window_options.dart @@ -0,0 +1,15 @@ +import 'macos/window_options.dart'; +import 'windows/window_options.dart'; + +class WindowOptions { + final WindowsWindowOptions windows; + final MacOSWindowOptions macos; + WindowOptions({this.windows = const WindowsWindowOptions(), this.macos = const MacOSWindowOptions()}); + + Map toJson() { + return { + 'windows': windows.toJson(), + 'macos': macos.toJson(), + }; + } +} diff --git a/packages/desktop_multi_window/lib/src/windows/extended_window_style.dart b/packages/desktop_multi_window/lib/src/windows/extended_window_style.dart new file mode 100644 index 00000000..e1ae8cf0 --- /dev/null +++ b/packages/desktop_multi_window/lib/src/windows/extended_window_style.dart @@ -0,0 +1,85 @@ +// ignore_for_file: constant_identifier_names + +/// A collection of Windows extended window style constants used with CreateWindowEx. +/// +/// These constants correspond to the extended window style flags in the Windows API. +class WindowsExtendedWindowStyle { + /// Creates a window with no extended style. + static const int NO_EX_STYLE = 0; + + /// Creates a window with a double border; typically used for dialog boxes. + static const int WS_EX_DLGMODALFRAME = 0x00000001; + + /// The child window does not send a WM_PARENTNOTIFY message to its parent window when it is created or destroyed. + static const int WS_EX_NOPARENTNOTIFY = 0x00000004; + + /// Specifies a topmost window. + static const int WS_EX_TOPMOST = 0x00000008; + + /// The window accepts drag-drop files. + static const int WS_EX_ACCEPTFILES = 0x00000010; + + /// The window should be transparent. + static const int WS_EX_TRANSPARENT = 0x00000020; + + /// Designates a Multiple Document Interface (MDI) child window. + static const int WS_EX_MDICHILD = 0x00000040; + + /// Creates a tool window; a window intended to be used as a floating toolbar. + static const int WS_EX_TOOLWINDOW = 0x00000080; + + /// Specifies that the window has a border with a raised edge. + static const int WS_EX_WINDOWEDGE = 0x00000100; + + /// Specifies that the window has a border with a sunken edge. + static const int WS_EX_CLIENTEDGE = 0x00000200; + + /// Adds a question mark to the window's title bar. + static const int WS_EX_CONTEXTHELP = 0x00000400; + + /// Right-aligns the window’s title bar text. + static const int WS_EX_RIGHT = 0x00001000; + + /// Displays the window’s text in right-to-left reading order. + static const int WS_EX_RTLREADING = 0x00002000; + + /// Places the vertical scroll bar (if present) on the left rather than the right side of the window. + static const int WS_EX_LEFTSCROLLBAR = 0x00004000; + + /// The window is a control container that can be navigated with the TAB key. + static const int WS_EX_CONTROLPARENT = 0x00010000; + + /// Adds a static edge border style to a window. + static const int WS_EX_STATICEDGE = 0x00020000; + + /// Forces a top-level window onto the taskbar when visible. + static const int WS_EX_APPWINDOW = 0x00040000; + + /// Allows the window to be a layered window. + static const int WS_EX_LAYERED = 0x00080000; + + /// Prevents the window from inheriting the layout of its parent window. + static const int WS_EX_NOINHERITLAYOUT = 0x00100000; + + /// Specifies that the window should have a right-to-left layout. + static const int WS_EX_LAYOUTRTL = 0x00400000; + + /// Paints all descendants of a window in bottom-to-top painting order using double-buffering. + static const int WS_EX_COMPOSITED = 0x02000000; + + /// A window created with this style does not become the foreground window when the user clicks it. + static const int WS_EX_NOACTIVATE = 0x08000000; + + /// Common extended style combinations + + /// Standard overlapped window with extended styles + /// Combines common extended styles for main application windows + static const int WS_EX_OVERLAPPEDWINDOW = WS_EX_WINDOWEDGE | + WS_EX_CLIENTEDGE; + + /// Standard palette window with extended styles + /// Combines styles typically used for palette or toolbar windows + static const int WS_EX_PALETTEWINDOW = WS_EX_WINDOWEDGE | + WS_EX_TOOLWINDOW | + WS_EX_TOPMOST; +} diff --git a/packages/desktop_multi_window/lib/src/windows/window_options.dart b/packages/desktop_multi_window/lib/src/windows/window_options.dart new file mode 100644 index 00000000..7ea5d9dc --- /dev/null +++ b/packages/desktop_multi_window/lib/src/windows/window_options.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +import '../extensions.dart'; +import 'extended_window_style.dart'; +import 'window_style.dart'; + +class WindowsWindowOptions { + final int style; + final int exStyle; + final int left; + final int top; + final int width; + final int height; + final Color backgroundColor; + final String title; + + const WindowsWindowOptions({ + this.style = WindowsWindowStyle.WS_OVERLAPPED, // WS_OVERLAPPEDWINDOW + this.exStyle = WindowsExtendedWindowStyle.NO_EX_STYLE, + this.left = 10, + this.top = 10, + this.width = 1280, + this.height = 720, + this.title = '', + this.backgroundColor = const Color(0x00000000), + }); + + Map toJson() { + return { + 'style': style, + 'exStyle': exStyle, + 'left': left, + 'top': top, + 'width': width, + 'height': height, + 'title': title, + 'backgroundColor': backgroundColor.toJson(), + }; + } +} diff --git a/packages/desktop_multi_window/lib/src/windows/window_style.dart b/packages/desktop_multi_window/lib/src/windows/window_style.dart new file mode 100644 index 00000000..04294332 --- /dev/null +++ b/packages/desktop_multi_window/lib/src/windows/window_style.dart @@ -0,0 +1,85 @@ +// ignore_for_file: constant_identifier_names + +/// Windows Window Styles (WS_*) +/// These values control the appearance and behavior of the window +class WindowsWindowStyle { + /// Creates an overlapped window. An overlapped window has a title bar and a border. Same as WS_TILED + static const int WS_OVERLAPPED = 0x00000000; + + /// Creates a pop-up window. Cannot be used with WS_CHILD + static const int WS_POPUP = -0x80000000; + + /// Creates a child window. Cannot be used with WS_POPUP + static const int WS_CHILD = -0x40000000; + + /// Creates a window that is initially minimized. Same as WS_ICONIC + static const int WS_MINIMIZE = -0x20000000; + + /// Creates a window that is initially visible + static const int WS_VISIBLE = 0x10000000; + + /// Creates a window that is initially disabled + static const int WS_DISABLED = 0x08000000; + + /// Clips child windows relative to each other + static const int WS_CLIPSIBLINGS = 0x04000000; + + /// Excludes the area occupied by child windows when drawing within the parent window + static const int WS_CLIPCHILDREN = 0x02000000; + + /// Creates a window that is initially maximized + static const int WS_MAXIMIZE = 0x01000000; + + /// Creates a window that has a title bar (includes WS_BORDER) + static const int WS_CAPTION = 0x00C00000; + + /// Creates a window that has a thin-line border + static const int WS_BORDER = 0x00800000; + + /// Creates a window that has a border of a style typically used with dialog boxes + static const int WS_DLGFRAME = 0x00400000; + + /// Creates a window that has a vertical scroll bar + static const int WS_VSCROLL = 0x00200000; + + /// Creates a window that has a horizontal scroll bar + static const int WS_HSCROLL = 0x00100000; + + /// Creates a window that has a window menu (system menu) in its title bar + static const int WS_SYSMENU = 0x00080000; + + /// Creates a window that has a sizing border (thick frame) + /// This enables resizing the window by dragging the border + static const int WS_THICKFRAME = 0x00040000; + + /// Specifies the first control of a group of controls + static const int WS_GROUP = 0x00020000; + + /// Specifies a control that can receive keyboard focus when Tab is pressed + static const int WS_TABSTOP = 0x00010000; + + /// Creates a window that has a minimize button + static const int WS_MINIMIZEBOX = 0x00020000; + + /// Creates a window that has a maximize button + static const int WS_MAXIMIZEBOX = 0x00010000; + + /// Common window style combinations + + /// Creates a standard overlapped window + /// Combines: WS_OVERLAPPED, WS_CAPTION, WS_SYSMENU, WS_THICKFRAME, WS_MINIMIZEBOX, WS_MAXIMIZEBOX + /// This is the most common style for main application windows + static const int WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | + WS_CAPTION | + WS_SYSMENU | + WS_THICKFRAME | + WS_MINIMIZEBOX | + WS_MAXIMIZEBOX; + + /// Creates a standard popup window with a border and system menu + /// Combines: WS_POPUP, WS_BORDER, WS_SYSMENU + /// Typically used for dialog boxes, message boxes, or other temporary windows + static const int WS_POPUPWINDOW = WS_POPUP | + WS_BORDER | + WS_SYSMENU; +} \ No newline at end of file diff --git a/packages/desktop_multi_window/macos/Classes/FlutterMultiWindowPlugin.swift b/packages/desktop_multi_window/macos/Classes/FlutterMultiWindowPlugin.swift index 596dee5b..1b4e800a 100644 --- a/packages/desktop_multi_window/macos/Classes/FlutterMultiWindowPlugin.swift +++ b/packages/desktop_multi_window/macos/Classes/FlutterMultiWindowPlugin.swift @@ -3,7 +3,8 @@ import FlutterMacOS public class FlutterMultiWindowPlugin: NSObject, FlutterPlugin { static func registerInternal(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "mixin.one/flutter_multi_window", binaryMessenger: registrar.messenger) + let channel = FlutterMethodChannel( + name: "mixin.one/flutter_multi_window", binaryMessenger: registrar.messenger) let instance = FlutterMultiWindowPlugin() registrar.addMethodCallDelegate(instance, channel: channel) } @@ -11,15 +12,19 @@ public class FlutterMultiWindowPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { registerInternal(with: registrar) guard let app = NSApplication.shared.delegate as? FlutterAppDelegate else { - debugPrint("failed to find flutter main window, application delegate is not FlutterAppDelegate") + debugPrint( + "failed to find flutter main window, application delegate is not FlutterAppDelegate") return } guard let window = app.mainFlutterWindow else { debugPrint("failed to find flutter main window") return } - let mainWindowChannel = WindowChannel.register(with: registrar, windowId: 0) - MultiWindowManager.shared.attachMainWindow(window: window, mainWindowChannel) + let mainWindowEventsChannel = WindowEventsChannel.register(returns: registrar) + let mainWindowInterWindowEventChannel = InterWindowEventChannel.register( + with: registrar, windowId: 0) + MultiWindowManager.shared.attachMainWindow( + window: window, mainWindowInterWindowEventChannel, mainWindowEventsChannel) } public typealias OnWindowCreatedCallback = (FlutterViewController) -> Void @@ -32,58 +37,59 @@ public class FlutterMultiWindowPlugin: NSObject, FlutterPlugin { public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { case "createWindow": - let arguments = call.arguments as? String - let windowId = MultiWindowManager.shared.create(arguments: arguments ?? "") - result(windowId) - case "show": - let windowId = call.arguments as! Int64 - MultiWindowManager.shared.show(windowId: windowId) - result(nil) - case "hide": - let windowId = call.arguments as! Int64 - MultiWindowManager.shared.hide(windowId: windowId) - result(nil) - case "close": - let windowId = call.arguments as! Int64 - MultiWindowManager.shared.close(windowId: windowId) - result(nil) - case "center": - let windowId = call.arguments as! Int64 - MultiWindowManager.shared.center(windowId: windowId) - result(nil) - case "setFrame": - let arguments = call.arguments as! [String: Any?] - let windowId = arguments["windowId"] as! Int64 - let left = arguments["left"] as! Double - let top = arguments["top"] as! Double - let width = arguments["width"] as! Double - let height = arguments["height"] as! Double - let rect = NSRect(x: left, y: top, width: width, height: height) - MultiWindowManager.shared.setFrame(windowId: windowId, frame: rect) - result(nil) - case "setTitle": - let arguments = call.arguments as! [String: Any?] - let windowId = arguments["windowId"] as! Int64 - let title = arguments["title"] as! String - MultiWindowManager.shared.setTitle(windowId: windowId, title: title) - result(nil) - case "resizable": - let arguments = call.arguments as! [String: Any?] - let windowId = arguments["windowId"] as! Int64 - let resizable = arguments["resizable"] as! Bool - MultiWindowManager.shared.resizable(windowId: windowId, resizable: resizable) - result(nil) - case "setFrameAutosaveName": - let arguments = call.arguments as! [String: Any?] - let windowId = arguments["windowId"] as! Int64 - let frameAutosaveName = arguments["name"] as! String - MultiWindowManager.shared.setFrameAutosaveName(windowId: windowId, name: frameAutosaveName) - result(nil) + if let args = call.arguments as? [String: Any], + let optionsJson = args["options"] as? [String: Any], + let macosJson = optionsJson["macos"] as? [String: Any], + let windowOptions = WindowOptions(json: macosJson) + { + let arguments = call.arguments as? String + let windowId = MultiWindowManager.shared.create( + arguments: arguments ?? "", windowOptions: windowOptions) + result(windowId) + } else { + result( + FlutterError( + code: "INVALID_ARGUMENTS", + message: "Could not parse macOS window options.", + details: nil)) + } case "getAllSubWindowIds": let subWindowIds = MultiWindowManager.shared.getAllSubWindowIds() result(subWindowIds) default: - result(FlutterMethodNotImplemented) + guard let arguments = call.arguments as? [String: Any?] else { + result(FlutterError( + code: "INVALID_ARGUMENTS", + message: "Method call arguments must be a dictionary", + details: nil + )) + return + } + + guard let windowId = arguments["windowId"] as? Int64 else { + result(FlutterError( + code: "INVALID_WINDOW_ID", + message: "Window ID must be provided and must be an integer", + details: nil + )) + return + } + + // Verify the window exists before attempting to handle the event + if !MultiWindowManager.shared.hasWindow(windowId: windowId) { + result(FlutterError( + code: "WINDOW_NOT_FOUND", + message: "No window found with ID: \(windowId)", + details: nil + )) + return + } + MultiWindowManager.shared.handleWindowEvent( + windowId: windowId, + method: call.method, + arguments: arguments, + result: result + ) } } } diff --git a/packages/desktop_multi_window/macos/Classes/FlutterWindow.swift b/packages/desktop_multi_window/macos/Classes/FlutterWindow.swift index 1c7b6fe2..84d11541 100644 --- a/packages/desktop_multi_window/macos/Classes/FlutterWindow.swift +++ b/packages/desktop_multi_window/macos/Classes/FlutterWindow.swift @@ -8,31 +8,236 @@ import Cocoa import FlutterMacOS import Foundation +extension NSWindow { + private struct AssociatedKeys { + static var configured: Bool = false + } + var configured: Bool { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.configured) as? Bool ?? false + } + set(value) { + objc_setAssociatedObject( + self, &AssociatedKeys.configured, value, + objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + public func hiddenWindowAtLaunch() { + if !configured { + setIsVisible(false) + configured = true + } + } +} + +// Protocol for handling window events +protocol WindowEventHandler: AnyObject { + func handleWindowWillClose() + func handleWindowShouldClose() -> Bool + func handleWindowShouldZoom() -> Bool + func handleWindowDidResize() + func handleWindowDidEndLiveResize() + func handleWindowWillMove() + func handleWindowDidMove() + func handleWindowDidBecomeKey() + func handleWindowDidResignKey() + func handleWindowDidBecomeMain() + func handleWindowDidResignMain() + func handleWindowDidMiniaturize() + func handleWindowDidDeminiaturize() + func handleWindowDidEnterFullScreen() + func handleWindowDidExitFullScreen() +} + +// Proxy class that forwards delegate calls +class WindowDelegateProxy: NSObject, NSWindowDelegate { + weak var originalDelegate: NSWindowDelegate? + weak var eventHandler: WindowEventHandler? + + public func windowWillClose(_ notification: Notification) { + eventHandler?.handleWindowWillClose() + originalDelegate?.windowWillClose?(notification) + } + + public func windowShouldClose(_ sender: NSWindow) -> Bool { + let shouldClose = eventHandler?.handleWindowShouldClose() ?? true + return originalDelegate?.windowShouldClose?(sender) ?? shouldClose + } + + public func windowShouldZoom(_ window: NSWindow, toFrame newFrame: NSRect) -> Bool { + let shouldZoom = eventHandler?.handleWindowShouldZoom() ?? true + return originalDelegate?.windowShouldZoom?(window, toFrame: newFrame) ?? shouldZoom + } + + public func windowDidResize(_ notification: Notification) { + eventHandler?.handleWindowDidResize() + originalDelegate?.windowDidResize?(notification) + } + + public func windowDidEndLiveResize(_ notification: Notification) { + eventHandler?.handleWindowDidEndLiveResize() + originalDelegate?.windowDidEndLiveResize?(notification) + } + + public func windowWillMove(_ notification: Notification) { + eventHandler?.handleWindowWillMove() + originalDelegate?.windowWillMove?(notification) + } + + public func windowDidMove(_ notification: Notification) { + eventHandler?.handleWindowDidMove() + originalDelegate?.windowDidMove?(notification) + } + + public func windowDidBecomeKey(_ notification: Notification) { + eventHandler?.handleWindowDidBecomeKey() + originalDelegate?.windowDidBecomeKey?(notification) + } + + public func windowDidResignKey(_ notification: Notification) { + eventHandler?.handleWindowDidResignKey() + originalDelegate?.windowDidResignKey?(notification) + } + + public func windowDidBecomeMain(_ notification: Notification) { + eventHandler?.handleWindowDidBecomeMain() + originalDelegate?.windowDidBecomeMain?(notification) + } + + public func windowDidResignMain(_ notification: Notification) { + eventHandler?.handleWindowDidResignMain() + originalDelegate?.windowDidResignMain?(notification) + } + + public func windowDidMiniaturize(_ notification: Notification) { + eventHandler?.handleWindowDidMiniaturize() + originalDelegate?.windowDidMiniaturize?(notification) + } + + public func windowDidDeminiaturize(_ notification: Notification) { + eventHandler?.handleWindowDidDeminiaturize() + originalDelegate?.windowDidDeminiaturize?(notification) + } + + public func windowDidEnterFullScreen(_ notification: Notification) { + eventHandler?.handleWindowDidEnterFullScreen() + originalDelegate?.windowDidEnterFullScreen?(notification) + } + + public func windowDidExitFullScreen(_ notification: Notification) { + eventHandler?.handleWindowDidExitFullScreen() + originalDelegate?.windowDidExitFullScreen?(notification) + } + // Forward any unhandled messages to the original delegate + override func responds(to aSelector: Selector!) -> Bool { + return super.responds(to: aSelector) || originalDelegate?.responds(to: aSelector) == true + } + + override func forwardingTarget(for aSelector: Selector!) -> Any? { + if originalDelegate?.responds(to: aSelector) == true { + return originalDelegate + } + return super.forwardingTarget(for: aSelector) + } +} + class BaseFlutterWindow: NSObject { - private let window: NSWindow - let windowChannel: WindowChannel + let windowId: Int64 + let window: NSWindow + let interWindowEventChannel: InterWindowEventChannel + let windowEventsChannel: WindowEventsChannel + weak var delegate: WindowManagerDelegate? + + private var _isPreventClose: Bool = false + private var _isMaximized: Bool = false + private var _isMaximizable: Bool = true + + private weak var originalDelegate: NSWindowDelegate? + private let delegateProxy: WindowDelegateProxy + private var delegateObservation: NSKeyValueObservation? - init(window: NSWindow, channel: WindowChannel) { + init( + id: Int64, window: NSWindow, interWindowEventChannel: InterWindowEventChannel, + windowEventsChannel: WindowEventsChannel + ) { + self.windowId = id self.window = window - self.windowChannel = channel + self.interWindowEventChannel = interWindowEventChannel + self.windowEventsChannel = windowEventsChannel + + self.originalDelegate = window.delegate + self.delegateProxy = WindowDelegateProxy() + super.init() + + self.windowEventsChannel.methodHandler = handleMethodCall + + // Set up the proxy + self.delegateProxy.originalDelegate = self.originalDelegate + self.delegateProxy.eventHandler = self + self.window.delegate = self.delegateProxy + + // Observe delegate changes + self.delegateObservation = window.observe(\.delegate, options: [.new, .old]) { + [weak self] window, change in + guard let self = self else { return } + if let newDelegate = change.newValue as? NSWindowDelegate, + newDelegate !== self.delegateProxy + { + self.originalDelegate = newDelegate + self.delegateProxy.originalDelegate = newDelegate + window.delegate = self.delegateProxy + } + } } - func show() { - window.makeKeyAndOrderFront(nil) - NSApp.activate(ignoringOtherApps: true) + private func emitEvent(_ eventName: String) { + let args: NSDictionary = [ + "eventName": eventName + ] + windowEventsChannel.methodChannel.invokeMethod("onEvent", arguments: args, result: nil) } - func hide() { - window.orderOut(nil) + public func show() { + // window.setIsVisible(true) + DispatchQueue.main.async { + // self.window.makeKeyAndOrderFront(nil) + // NSApp.activate(ignoringOtherApps: true) + self.window.orderFront(nil) + self.handleWindowShow() + } + } + + public func hide() { + DispatchQueue.main.async { + self.window.orderOut(nil) + self.handleWindowHide() + } + } + + public func isFocused() -> Bool { + return window.isKeyWindow + } + + public func focus() { + NSApp.activate(ignoringOtherApps: false) + window.makeKeyAndOrderFront(nil) } func center() { window.center() } - func setFrame(frame: NSRect) { - window.setFrame(frame, display: false, animate: true) + func getFrame() -> NSRect { + return window.frame + } + + func setFrame(frame: NSRect, animate: Bool) { + if animate { + window.animator().setFrame(frame, display: true, animate: true) + } else { + window.setFrame(frame, display: true) + } } func setTitle(title: String) { @@ -40,7 +245,7 @@ class BaseFlutterWindow: NSObject { } func resizable(resizable: Bool) { - if (resizable) { + if resizable { window.styleMask.insert(.resizable) } else { window.styleMask.remove(.resizable) @@ -51,61 +256,396 @@ class BaseFlutterWindow: NSObject { window.close() } - func setFrameAutosaveName(name: String) { - window.setFrameAutosaveName(name) + public func isMinimized() -> Bool { + return window.isMiniaturized } -} -class FlutterWindow: BaseFlutterWindow { - let windowId: Int64 + public func maximize() { + if !isMaximized() { + window.zoom(nil) + } + } - let window: NSWindow + public func unmaximize() { + if isMaximized() { + window.zoom(nil) + } + } - weak var delegate: WindowManagerDelegate? + public func minimize() { + window.miniaturize(nil) + } - init(id: Int64, arguments: String) { - windowId = id - window = NSWindow( - contentRect: NSRect(x: 0, y: 0, width: 480, height: 270), - styleMask: [.miniaturizable, .closable, .resizable, .titled, .fullSizeContentView], - backing: .buffered, defer: false) - let project = FlutterDartProject() - project.dartEntrypointArguments = ["multi_window", "\(windowId)", arguments] - let flutterViewController = FlutterViewController(project: project) - window.contentViewController = flutterViewController + public func restore() { + window.deminiaturize(nil) + } - let plugin = flutterViewController.registrar(forPlugin: "FlutterMultiWindowPlugin") - FlutterMultiWindowPlugin.registerInternal(with: plugin) - let windowChannel = WindowChannel.register(with: plugin, windowId: id) - // Give app a chance to register plugin. - FlutterMultiWindowPlugin.onWindowCreatedCallback?(flutterViewController) + public func isFullScreen() -> Bool { + return window.styleMask.contains(.fullScreen) + } + + public func setFullScreen(isFullScreen: Bool) { + if isFullScreen { + if !window.styleMask.contains(.fullScreen) { + window.toggleFullScreen(nil) + } + } else { + if window.styleMask.contains(.fullScreen) { + window.toggleFullScreen(nil) + } + } + } + + public func isMaximized() -> Bool { + return window.isZoomed + } + + public func isVisible() -> Bool { + return window.isVisible + } + + public func isPreventClose() -> Bool { + return _isPreventClose + } + + public func isMaximizable() -> Bool { + return _isMaximizable + } + + func setFrameAutosaveName(name: String) { + window.setFrameAutosaveName(name) + } + + func handleMethodCall( + method: String, arguments: [String: Any?]?, result: @escaping FlutterResult + ) { + switch method { + case "setHasListeners": + // let hasListeners = arguments?["hasListeners"] as? Bool ?? false + // setHasListeners(hasListeners) + result(nil) + case "show": + show() + result(nil) + case "hide": + hide() + result(nil) + case "close": + close() + result(nil) + case "center": + center() + result(nil) + case "getFrame": + let frameRect = getFrame() + let data: NSDictionary = [ + "left": frameRect.topLeft.x, + "top": frameRect.topLeft.y, + "width": frameRect.size.width, + "height": frameRect.size.height, + ] + result(data) + case "setFrame": + guard let arguments = arguments else { + result( + FlutterError( + code: "INVALID_ARGUMENTS", + message: "Arguments must be a dictionary", + details: nil + )) + return + } + let animate = arguments["animate"] as? Bool ?? false - super.init(window: window, channel: windowChannel) + var frameRect = getFrame() + if arguments["width"] != nil && arguments["height"] != nil { + let width: CGFloat = CGFloat(truncating: arguments["width"] as! NSNumber) + let height: CGFloat = CGFloat(truncating: arguments["height"] as! NSNumber) - window.delegate = self - window.isReleasedWhenClosed = false - window.titleVisibility = .hidden - window.titlebarAppearsTransparent = true + frameRect.origin.y += (frameRect.size.height - height) + frameRect.size.width = width + frameRect.size.height = height + } + if arguments["left"] != nil && arguments["top"] != nil { + frameRect.topLeft.x = CGFloat(truncating: arguments["left"] as! NSNumber) + frameRect.topLeft.y = CGFloat(truncating: arguments["top"] as! NSNumber) + } + setFrame(frame: frameRect, animate: animate) + result(nil) + case "setTitle": + guard let arguments = arguments else { + result( + FlutterError( + code: "INVALID_ARGUMENTS", + message: "Arguments must be a dictionary", + details: nil + )) + return + } + let title = arguments["title"] as! String + setTitle(title: title) + result(nil) + case "resizable": + guard let arguments = arguments else { + result( + FlutterError( + code: "INVALID_ARGUMENTS", + message: "Arguments must be a dictionary", + details: nil + )) + return + } + let value = arguments["resizable"] as! Bool + resizable(resizable: value) + result(nil) + case "setFrameAutosaveName": + guard let arguments = arguments else { + result( + FlutterError( + code: "INVALID_ARGUMENTS", + message: "Arguments must be a dictionary", + details: nil + )) + return + } + let frameAutosaveName = arguments["name"] as! String + setFrameAutosaveName(name: frameAutosaveName) + result(nil) + case "isFocused": + let isFocused = isFocused() + result(isFocused) + case "isFullScreen": + let isFullScreen = isFullScreen() + result(isFullScreen) + case "isMaximized": + let isMaximized = isMaximized() + result(isMaximized) + case "isMinimized": + let isMinimized = isMinimized() + result(isMinimized) + case "isVisible": + let isVisible = isVisible() + result(isVisible) + case "maximize": + maximize() + result(nil) + case "unmaximize": + unmaximize() + result(nil) + case "minimize": + minimize() + result(nil) + case "restore": + restore() + result(nil) + case "setFullScreen": + let isFullScreen = arguments?["isFullScreen"] as? Bool ?? false + setFullScreen(isFullScreen: isFullScreen) + result(nil) + case "setStyle": + let styleMask = arguments?["styleMask"] as? UInt + let collectionBehavior = arguments?["collectionBehavior"] as? UInt + let level = arguments?["level"] as? Int + let isOpaque = arguments?["isOpaque"] as? Bool ?? true + let hasShadow = arguments?["hasShadow"] as? Bool ?? true + if let bgColor = arguments?["backgroundColor"] as? [String: Any] { + let backgroundColor = + WindowOptions.parseColor(from: bgColor) ?? NSColor.windowBackgroundColor + (window.contentViewController as? FlutterViewController)?.backgroundColor = backgroundColor + window.backgroundColor = backgroundColor + } else { + // ToDo: This might be wrong?? + window.backgroundColor = NSColor.windowBackgroundColor + (window.contentViewController as? FlutterViewController)?.backgroundColor = + NSColor.windowBackgroundColor + } + window.styleMask = NSWindow.StyleMask(rawValue: styleMask ?? 0) + window.collectionBehavior = NSWindow.CollectionBehavior(rawValue: collectionBehavior ?? 0) + window.level = NSWindow.Level(rawValue: level ?? 0) + window.isOpaque = isOpaque + window.hasShadow = hasShadow + result(nil) + case "setIgnoreMouseEvents": + let ignoresMouseEvents = arguments?["ignore"] as? Bool ?? false + window.ignoresMouseEvents = ignoresMouseEvents + result(nil) + default: + result(FlutterMethodNotImplemented) + } } deinit { - debugPrint("release window resource") - window.delegate = nil + debugPrint("Releasing window resources") + delegateObservation?.invalidate() + if window.delegate === delegateProxy { + window.delegate = originalDelegate + } if let flutterViewController = window.contentViewController as? FlutterViewController { - flutterViewController.engine.shutDownEngine() + DispatchQueue.main.async { + flutterViewController.engine.shutDownEngine() + } } window.contentViewController = nil window.windowController = nil } } -extension FlutterWindow: NSWindowDelegate { - func windowWillClose(_ notification: Notification) { +// Implementation of event handling +extension BaseFlutterWindow: WindowEventHandler { + func handleWindowWillClose() { delegate?.onClose(windowId: windowId) } - func windowShouldClose(_ sender: NSWindow) -> Bool { + func handleWindowShouldClose() -> Bool { + emitEvent("close") + if isPreventClose() { + return false + } delegate?.onClose(windowId: windowId) return true } + + func handleWindowShouldZoom() -> Bool { + emitEvent("maximize") + return isMaximizable() + } + + func handleWindowDidResize() { + emitEvent("resize") + if !_isMaximized && window.isZoomed { + _isMaximized = true + emitEvent("maximize") + } + if _isMaximized && !window.isZoomed { + _isMaximized = false + emitEvent("unmaximize") + } + } + + func handleWindowDidEndLiveResize() { + emitEvent("resized") + } + + func handleWindowWillMove() { + emitEvent("move") + } + + func handleWindowDidMove() { + emitEvent("moved") + } + + func handleWindowDidBecomeKey() { + if window is NSPanel { + emitEvent("focus") + } + } + + func handleWindowDidResignKey() { + if window is NSPanel { + emitEvent("blur") + } + } + + func handleWindowDidBecomeMain() { + emitEvent("focus") + } + + func handleWindowDidResignMain() { + emitEvent("blur") + } + + func handleWindowDidMiniaturize() { + emitEvent("minimize") + } + + func handleWindowDidDeminiaturize() { + emitEvent("restore") + } + + func handleWindowDidEnterFullScreen() { + emitEvent("enter-full-screen") + } + + func handleWindowDidExitFullScreen() { + emitEvent("leave-full-screen") + } + + func handleWindowShow() { + emitEvent("show") + } + + func handleWindowHide() { + emitEvent("hide") + } +} + +class FlutterWindow: BaseFlutterWindow { + + init(id: Int64, arguments: String, windowOptions: WindowOptions) { + + let createdWindow: NSWindow + + let contentRect: NSRect = NSRect( + x: windowOptions.x, y: windowOptions.y, width: windowOptions.width, + height: windowOptions.height) + + if windowOptions.type == "NSPanel" { + createdWindow = NSPanel( + contentRect: contentRect, + styleMask: NSWindow.StyleMask(rawValue: windowOptions.styleMask), + // styleMask: [.nonactivatingPanel, .utilityWindow], + backing: .buffered, defer: false) + } else { + createdWindow = NSWindow( + contentRect: contentRect, + styleMask: NSWindow.StyleMask(rawValue: windowOptions.styleMask), + // styleMask: [.miniaturizable, .closable, .resizable, .titled, .fullSizeContentView], + backing: .buffered, defer: false) + } + + let project = FlutterDartProject() + project.dartEntrypointArguments = ["multi_window", "\(id)", arguments] + let flutterViewController = FlutterViewController(project: project) + createdWindow.contentViewController = flutterViewController + + let plugin = flutterViewController.registrar(forPlugin: "FlutterMultiWindowPlugin") + FlutterMultiWindowPlugin.registerInternal(with: plugin) + let interWindowEventChannel = InterWindowEventChannel.register(with: plugin, windowId: id) + let windowEventsChannel = WindowEventsChannel.register(returns: plugin) + + // Give app a chance to register plugins. + FlutterMultiWindowPlugin.onWindowCreatedCallback?(flutterViewController) + + super.init( + id: id, window: createdWindow, interWindowEventChannel: interWindowEventChannel, + windowEventsChannel: windowEventsChannel) + + createdWindow.isReleasedWhenClosed = false + createdWindow.titleVisibility = .hidden + createdWindow.titlebarAppearsTransparent = true + + createdWindow.title = windowOptions.title + createdWindow.isOpaque = windowOptions.isOpaque + createdWindow.hasShadow = windowOptions.hasShadow + createdWindow.isMovable = windowOptions.isMovable + createdWindow.backgroundColor = windowOptions.backgroundColor + flutterViewController.backgroundColor = windowOptions.backgroundColor + createdWindow.standardWindowButton(.closeButton)?.isHidden = !windowOptions + .windowButtonVisibility + createdWindow.standardWindowButton(.miniaturizeButton)?.isHidden = !windowOptions + .windowButtonVisibility + createdWindow.standardWindowButton(.zoomButton)?.isHidden = !windowOptions + .windowButtonVisibility + createdWindow.level = NSWindow.Level(rawValue: windowOptions.level) + // window.backingType = windowOptions.backing == "buffered" ? .buffered : .retained + // window.collectionBehavior = NSWindow.CollectionBehavior(rawValue: windowOptions.collectionBehavior ?? 0) + createdWindow.ignoresMouseEvents = windowOptions.ignoresMouseEvents ?? false + // window.acceptsMouseMovedEvents = windowOptions.acceptsMouseMovedEvents ?? false + // window.animationBehavior = NSWindow.AnimationBehavior(rawValue: windowOptions.animationBehavior ?? 0) + + let frameRect = NSWindow.frameRect( + forContentRect: contentRect, styleMask: createdWindow.styleMask) + createdWindow.setFrame(frameRect, display: true) + NSApplication.shared.setActivationPolicy(.accessory) + } } diff --git a/packages/desktop_multi_window/macos/Classes/WindowChannel.swift b/packages/desktop_multi_window/macos/Classes/InterWindowEventChannel.swift similarity index 63% rename from packages/desktop_multi_window/macos/Classes/WindowChannel.swift rename to packages/desktop_multi_window/macos/Classes/InterWindowEventChannel.swift index 0a81dfff..605a0e65 100644 --- a/packages/desktop_multi_window/macos/Classes/WindowChannel.swift +++ b/packages/desktop_multi_window/macos/Classes/InterWindowEventChannel.swift @@ -5,36 +5,39 @@ // Created by Bin Yang on 2022/1/28. // -import Foundation - import FlutterMacOS +import Foundation -typealias MethodHandler = (Int64, Int64, String, Any?, @escaping FlutterResult) -> Void +typealias InterWindowMethodHandler = (Int64, Int64, String, Any?, @escaping FlutterResult) -> Void -class WindowChannel: NSObject, FlutterPlugin { +class InterWindowEventChannel: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { fatalError() } - public static func register(with registrar: FlutterPluginRegistrar, windowId: Int64) -> WindowChannel { - let channel = FlutterMethodChannel(name: "mixin.one/flutter_multi_window_channel", binaryMessenger: registrar.messenger) - let instance = WindowChannel(windowId: windowId, methodChannel: channel) + public static func register(with registrar: FlutterPluginRegistrar, windowId: Int64) + -> InterWindowEventChannel + { + let channel = FlutterMethodChannel( + name: "mixin.one/flutter_multi_window_inter_window_event_channel", + binaryMessenger: registrar.messenger) + let instance = InterWindowEventChannel(windowId: windowId, methodChannel: channel) registrar.addMethodCallDelegate(instance, channel: channel) return instance } + var methodHandler: InterWindowMethodHandler? + + let methodChannel: FlutterMethodChannel + + private let windowId: Int64 + init(windowId: Int64, methodChannel: FlutterMethodChannel) { self.windowId = windowId self.methodChannel = methodChannel super.init() } - var methodHandler: MethodHandler? - - private let methodChannel: FlutterMethodChannel - - private let windowId: Int64 - func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { let args = call.arguments as! [String: Any?] let targetWindowId = args["targetWindowId"] as! Int64 @@ -46,7 +49,9 @@ class WindowChannel: NSObject, FlutterPlugin { } } - func invokeMethod(fromWindowId: Int64, method: String, arguments: Any?, result: @escaping FlutterResult) { + func invokeMethod( + fromWindowId: Int64, method: String, arguments: Any?, result: @escaping FlutterResult + ) { let args = [ "fromWindowId": fromWindowId, "arguments": arguments, diff --git a/packages/desktop_multi_window/macos/Classes/MultiWindowManager.swift b/packages/desktop_multi_window/macos/Classes/MultiWindowManager.swift index 2349633e..9cbc2da0 100644 --- a/packages/desktop_multi_window/macos/Classes/MultiWindowManager.swift +++ b/packages/desktop_multi_window/macos/Classes/MultiWindowManager.swift @@ -5,113 +5,203 @@ // Created by Bin Yang on 2022/1/10. // +import Cocoa import FlutterMacOS import Foundation +extension NSRect { + var topLeft: CGPoint { + set { + let screenFrameRect = NSScreen.screens[0].frame + origin.x = newValue.x + origin.y = screenFrameRect.height - newValue.y - size.height + } + get { + let screenFrameRect = NSScreen.screens[0].frame + return CGPoint(x: origin.x, y: screenFrameRect.height - origin.y - size.height) + } + } +} + +enum WindowState { + case normal + case minimized + case maximized + case fullscreen + case hidden +} + class MultiWindowManager { static let shared = MultiWindowManager() private var id: Int64 = 0 - private var windows: [Int64: BaseFlutterWindow] = [:] + private let windowsLock = NSLock() + private var eventTap: CFMachPort? + private var runLoopSource: CFRunLoopSource? - func create(arguments: String) -> Int64 { - id += 1 - let windowId = id - - let window = FlutterWindow(id: windowId, arguments: arguments) - window.delegate = self - window.windowChannel.methodHandler = self.handleMethodCall - windows[windowId] = window - return windowId + init() { + setupMouseMonitor() } - func attachMainWindow(window: NSWindow, _ channel: WindowChannel) { - let mainWindow = BaseFlutterWindow(window: window, channel: channel) - mainWindow.windowChannel.methodHandler = self.handleMethodCall - windows[0] = mainWindow - } + private func setupMouseMonitor() { + // Request accessibility permissions if needed + if !AXIsProcessTrusted() { + debugPrint("Warning: Accessibility permissions not granted. Mouse tracking may not work.") + } - private func handleMethodCall(fromWindowId: Int64, targetWindowId: Int64, method: String, arguments: Any?, result: @escaping FlutterResult) { - guard let window = self.windows[targetWindowId] else { - result(FlutterError(code: "-1", message: "failed to find target window. \(targetWindowId)", details: nil)) + // Create event tap + let eventMask = CGEventMask(1 << CGEventType.mouseMoved.rawValue) + guard let tap = CGEvent.tapCreate( + tap: .cgSessionEventTap, + place: .headInsertEventTap, + options: .defaultTap, + eventsOfInterest: eventMask, + callback: { (proxy, type, event, refcon) -> Unmanaged? in + if type == .mouseMoved { + let location = event.location + let coordinates: [String: Any] = [ + "x": location.x, + "y": location.y + ] + + let args: [String: Any] = [ + "eventName": "mouse-move", + "eventData": coordinates + ] + + // Post to main thread since we're in a callback + DispatchQueue.main.async { + MultiWindowManager.shared.sendMouseEventToWindows(args) + } + } + return Unmanaged.passRetained(event) + }, + userInfo: nil + ) else { + debugPrint("Failed to create event tap") return } - window.windowChannel.invokeMethod(fromWindowId: fromWindowId, method: method, arguments: arguments, result: result) - } - func show(windowId: Int64) { - guard let window = windows[windowId] else { - debugPrint("window \(windowId) not exists.") + eventTap = tap + + // Create a run loop source and add it to the current run loop + guard let runLoop = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0) else { + debugPrint("Failed to create run loop source") return } - window.show() + runLoopSource = runLoop + + CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoop, .commonModes) + CGEvent.tapEnable(tap: tap, enable: true) } - func hide(windowId: Int64) { - guard let window = windows[windowId] else { - debugPrint("window \(windowId) not exists.") - return + private func sendMouseEventToWindows(_ args: [String: Any]) { + windowsLock.lock() + for (_, window) in windows { + window.windowEventsChannel.methodChannel.invokeMethod( + "onEvent", + arguments: args + ) } - window.hide() + windowsLock.unlock() } - func close(windowId: Int64) { - guard let window = windows[windowId] else { - debugPrint("window \(windowId) not exists.") - return + deinit { + if let runLoop = runLoopSource { + CFRunLoopRemoveSource(CFRunLoopGetCurrent(), runLoop, .commonModes) + } + + if let tap = eventTap { + CGEvent.tapEnable(tap: tap, enable: false) } - window.close() } - func closeAll() { - windows.forEach { _, value in - value.close() - } + func create(arguments: String, windowOptions: WindowOptions) -> Int64 { + windowsLock.lock() + defer { windowsLock.unlock() } + + id += 1 + let windowId = id + + let window = FlutterWindow(id: windowId, arguments: arguments, windowOptions: windowOptions) + window.delegate = self + window.interWindowEventChannel.methodHandler = self.handleInterWindowEvent + windows[windowId] = window + return windowId } - func center(windowId: Int64) { - guard let window = windows[windowId] else { - debugPrint("window \(windowId) not exists.") - return - } - window.center() + func attachMainWindow(window: NSWindow, _ interWindowEventChannel: InterWindowEventChannel, _ channel: WindowEventsChannel) { + windowsLock.lock() + defer { windowsLock.unlock() } + + let mainWindow = BaseFlutterWindow(id: 0, window: window, interWindowEventChannel: interWindowEventChannel, windowEventsChannel: channel) + mainWindow.interWindowEventChannel.methodHandler = self.handleInterWindowEvent + windows[0] = mainWindow } - func setFrame(windowId: Int64, frame: NSRect) { - guard let window = windows[windowId] else { - debugPrint("window \(windowId) not exists.") + private func handleInterWindowEvent( + fromWindowId: Int64, targetWindowId: Int64, method: String, arguments: Any?, + result: @escaping FlutterResult + ) { + windowsLock.lock() + guard let window = self.windows[targetWindowId] else { + windowsLock.unlock() + result( + FlutterError( + code: "-1", message: "failed to find target window. \(targetWindowId)", details: nil)) return } - window.setFrame(frame: frame) - } - func setTitle(windowId: Int64, title: String) { - guard let window = windows[windowId] else { - debugPrint("window \(windowId) not exists.") + // Check if window is still valid + if window.window.contentViewController == nil + || (window.window.contentViewController as? FlutterViewController)?.engine == nil + { + windowsLock.unlock() + result( + FlutterError( + code: "-2", message: "window engine is no longer valid \(targetWindowId)", details: nil)) return } - window.setTitle(title: title) + windowsLock.unlock() + + window.interWindowEventChannel.invokeMethod( + fromWindowId: fromWindowId, method: method, arguments: arguments, result: result) } - func resizable(windowId: Int64, resizable: Bool) { + func handleWindowEvent(windowId: Int64, method: String, arguments: [String: Any?]?, result: @escaping FlutterResult) { + windowsLock.lock() guard let window = windows[windowId] else { + windowsLock.unlock() debugPrint("window \(windowId) not exists.") return } - window.resizable(resizable: resizable) + windowsLock.unlock() + window.handleMethodCall(method: method, arguments: arguments, result: result) } - func setFrameAutosaveName(windowId: Int64, name: String) { + func getAllSubWindowIds() -> [Int64] { + windowsLock.lock() + let ids = windows.keys.filter { $0 != 0 } + windowsLock.unlock() + return ids + } + + func getWindowState(windowId: Int64) -> WindowState? { + windowsLock.lock() + defer { windowsLock.unlock() } + guard let window = windows[windowId] else { - debugPrint("window \(windowId) not exists.") - return + return nil } - window.setFrameAutosaveName(name: name) + + return window.getWindowState() } - func getAllSubWindowIds() -> [Int64] { - return windows.keys.filter { $0 != 0 } + func hasWindow(windowId: Int64) -> Bool { + windowsLock.lock() + defer { windowsLock.unlock() } + return windows[windowId] != nil } } @@ -121,6 +211,29 @@ protocol WindowManagerDelegate: AnyObject { extension MultiWindowManager: WindowManagerDelegate { func onClose(windowId: Int64) { - windows.removeValue(forKey: windowId) + if let _ = windows[windowId] { + // Give time for any pending messages to complete + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.windowsLock.lock() + self.windows.removeValue(forKey: windowId) + self.windowsLock.unlock() + } + } + } +} + +extension BaseFlutterWindow { + func getWindowState() -> WindowState { + if !window.isVisible { + return .hidden + } else if window.isMiniaturized { + return .minimized + } else if window.isZoomed { + return .maximized + } else if window.styleMask.contains(.fullScreen) { + return .fullscreen + } else { + return .normal + } } } diff --git a/packages/desktop_multi_window/macos/Classes/WindowEventsChannel.swift b/packages/desktop_multi_window/macos/Classes/WindowEventsChannel.swift new file mode 100644 index 00000000..4f20b240 --- /dev/null +++ b/packages/desktop_multi_window/macos/Classes/WindowEventsChannel.swift @@ -0,0 +1,47 @@ +// +// WindowEventsChannel.swift +// desktop_multi_window +// +// Created by Konstantin Wachendorff on 2025/03/01. +// + +import FlutterMacOS +import Foundation + +typealias WindowMethodHandler = (String, [String: Any?]?, @escaping FlutterResult) -> Void + +class WindowEventsChannel: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + fatalError() + } + + public static func register(returns registrar: FlutterPluginRegistrar) + -> WindowEventsChannel + { + let channel = FlutterMethodChannel( + name: "mixin.one/flutter_multi_window_events_channel", + binaryMessenger: registrar.messenger) + let instance = WindowEventsChannel(methodChannel: channel) + + registrar.addMethodCallDelegate(instance, channel: channel) + return instance + } + + var methodHandler: WindowMethodHandler? + + let methodChannel: FlutterMethodChannel + + init(methodChannel: FlutterMethodChannel) { + self.methodChannel = methodChannel + super.init() + } + + func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let args = call.arguments as! [String: Any?] + if let handler = methodHandler { + handler(call.method, args, result) + } else { + debugPrint("method handler not set.") + } + } +} diff --git a/packages/desktop_multi_window/macos/Classes/WindowOptions.swift b/packages/desktop_multi_window/macos/Classes/WindowOptions.swift new file mode 100644 index 00000000..b3a69d1b --- /dev/null +++ b/packages/desktop_multi_window/macos/Classes/WindowOptions.swift @@ -0,0 +1,125 @@ +import Cocoa +import Foundation + +struct WindowOptions { + // Common properties for both NSWindow and NSPanel. + let type: String + let level: Int + let styleMask: UInt + let x: Int + let y: Int + let width: Int + let height: Int + let title: String + let isOpaque: Bool + let hasShadow: Bool + let isMovable: Bool + let backing: String + let backgroundColor: NSColor + let windowButtonVisibility: Bool + + // NSWindow-only properties. + let isModal: Bool? + let titleVisibility: String? + let titlebarAppearsTransparent: Bool? + let collectionBehavior: Int? + let ignoresMouseEvents: Bool? + let acceptsMouseMovedEvents: Bool? + let animationBehavior: String? + + init?(json: [String: Any]) { + // Use default values for optional parameters + self.type = json["type"] as? String ?? "NSWindow" + self.level = json["level"] as? Int ?? Int(NSWindow.Level.normal.rawValue) + self.styleMask = json["styleMask"] as? UInt ?? UInt(NSWindow.StyleMask([.titled, .closable, .miniaturizable, .resizable]).rawValue) + self.x = json["left"] as? Int ?? 0 + self.y = json["top"] as? Int ?? 0 + self.width = json["width"] as? Int ?? 800 + self.height = json["height"] as? Int ?? 600 + self.title = json["title"] as? String ?? "" + self.isOpaque = json["isOpaque"] as? Bool ?? true + self.hasShadow = json["hasShadow"] as? Bool ?? true + self.isMovable = json["isMovable"] as? Bool ?? true + self.backing = json["backing"] as? String ?? "buffered" + + if let bgColor = json["backgroundColor"] as? [String: Any] { + self.backgroundColor = WindowOptions.parseColor(from: bgColor) ?? NSColor.windowBackgroundColor + } else { + self.backgroundColor = NSColor.windowBackgroundColor + } + + self.windowButtonVisibility = json["windowButtonVisibility"] as? Bool ?? true + + // NSWindow-specific properties. + if type == "NSWindow" { + self.isModal = json["isModal"] as? Bool + self.titleVisibility = json["titleVisibility"] as? String + self.titlebarAppearsTransparent = json["titlebarAppearsTransparent"] as? Bool + self.collectionBehavior = json["collectionBehavior"] as? Int + self.ignoresMouseEvents = json["ignoresMouseEvents"] as? Bool + self.acceptsMouseMovedEvents = json["acceptsMouseMovedEvents"] as? Bool + self.animationBehavior = json["animationBehavior"] as? String + } else { + self.isModal = nil + self.titleVisibility = nil + self.titlebarAppearsTransparent = nil + self.collectionBehavior = nil + self.ignoresMouseEvents = nil + self.acceptsMouseMovedEvents = nil + self.animationBehavior = nil + } + } + + static func parseColor(from json: [String: Any]) -> NSColor? { + // Expect red, green, blue as integers, and optionally alpha. + guard let redValue = json["red"] as? Int, + let greenValue = json["green"] as? Int, + let blueValue = json["blue"] as? Int + else { + return nil + } + // Alpha is optional, defaulting to 255 (fully opaque) + let alphaValue = json["alpha"] as? Int ?? 255 + + // Convert the 0...255 integer values into CGFloats in the 0.0...1.0 range. + let red = CGFloat(redValue) / 255.0 + let green = CGFloat(greenValue) / 255.0 + let blue = CGFloat(blueValue) / 255.0 + let alpha = CGFloat(alphaValue) / 255.0 + + return NSColor(red: red, green: green, blue: blue, alpha: alpha) + } + + func printOptions() { + print("Window Options:") + print(" Type: \(type)") + print(" Level: \(level)") + print(" StyleMask: \(String(format: "0x%X", styleMask))") + print(" Position: (\(x), \(y))") + print(" Size: \(width) x \(height)") + print(" Title: \(title)") + print(" isOpaque: \(isOpaque)") + print(" hasShadow: \(hasShadow)") + print(" isMovable: \(isMovable)") + print(" Backing: \(backing)") + + if type == "NSWindow" { + print(" NSWindow Specific:") + print(" isModal: \(isModal != nil ? "\(isModal!)" : "nil")") + print(" titleVisibility: \(titleVisibility ?? "nil")") + print( + " titlebarAppearsTransparent: \(titlebarAppearsTransparent != nil ? "\(titlebarAppearsTransparent!)" : "nil")" + ) + print( + " collectionBehavior: \(collectionBehavior != nil ? "\(collectionBehavior!)" : "nil")" + ) + print( + " ignoresMouseEvents: \(ignoresMouseEvents != nil ? "\(ignoresMouseEvents!)" : "nil")" + ) + print( + " acceptsMouseMovedEvents: \(acceptsMouseMovedEvents != nil ? "\(acceptsMouseMovedEvents!)" : "nil")" + ) + print(" animationBehavior: \(animationBehavior ?? "nil")") + } + } +} diff --git a/packages/desktop_multi_window/pubspec.yaml b/packages/desktop_multi_window/pubspec.yaml index 7ffae378..356a1b3d 100644 --- a/packages/desktop_multi_window/pubspec.yaml +++ b/packages/desktop_multi_window/pubspec.yaml @@ -4,7 +4,7 @@ version: 0.2.1 homepage: https://github.com/MixinNetwork/flutter-plugins/tree/main/packages/desktop_multi_window environment: - sdk: ">=2.17.0 <3.0.0" + sdk: ">=3.0.0 <4.0.0" flutter: ">=3.0.0" dependencies: diff --git a/packages/desktop_multi_window/windows/CMakeLists.txt b/packages/desktop_multi_window/windows/CMakeLists.txt index 54b5438f..513d8a2d 100644 --- a/packages/desktop_multi_window/windows/CMakeLists.txt +++ b/packages/desktop_multi_window/windows/CMakeLists.txt @@ -10,7 +10,8 @@ add_library(${PLUGIN_NAME} SHARED "desktop_multi_window_plugin.cpp" "multi_window_manager.cc" "flutter_window.cc" - "window_channel.cc" + "inter_window_event_channel.cc" + "window_events_channel.cc" "base_flutter_window.cc") apply_standard_settings(${PLUGIN_NAME}) set_target_properties(${PLUGIN_NAME} PROPERTIES diff --git a/packages/desktop_multi_window/windows/base_flutter_window.cc b/packages/desktop_multi_window/windows/base_flutter_window.cc index 3d41f2c9..d11c29e2 100644 --- a/packages/desktop_multi_window/windows/base_flutter_window.cc +++ b/packages/desktop_multi_window/windows/base_flutter_window.cc @@ -3,74 +3,212 @@ // #include "base_flutter_window.h" +#include "utils.h" namespace { -void CenterRectToMonitor(LPRECT prc) { - HMONITOR hMonitor; - MONITORINFO mi; - RECT rc; - int w = prc->right - prc->left; - int h = prc->bottom - prc->top; + void CenterRectToMonitor(LPRECT prc) { + HMONITOR hMonitor; + MONITORINFO mi; + RECT rc; + int w = prc->right - prc->left; + int h = prc->bottom - prc->top; - // - // get the nearest monitor to the passed rect. - // - hMonitor = MonitorFromRect(prc, MONITOR_DEFAULTTONEAREST); + // + // get the nearest monitor to the passed rect. + // + hMonitor = MonitorFromRect(prc, MONITOR_DEFAULTTONEAREST); - // - // get the work area or entire monitor rect. - // - mi.cbSize = sizeof(mi); - GetMonitorInfo(hMonitor, &mi); + // + // get the work area or entire monitor rect. + // + mi.cbSize = sizeof(mi); + GetMonitorInfo(hMonitor, &mi); - rc = mi.rcMonitor; + rc = mi.rcMonitor; - prc->left = rc.left + (rc.right - rc.left - w) / 2; - prc->top = rc.top + (rc.bottom - rc.top - h) / 2; - prc->right = prc->left + w; - prc->bottom = prc->top + h; + prc->left = rc.left + (rc.right - rc.left - w) / 2; + prc->top = rc.top + (rc.bottom - rc.top - h) / 2; + prc->right = prc->left + w; + prc->bottom = prc->top + h; -} + } -std::wstring Utf16FromUtf8(const std::string &string) { - int size_needed = MultiByteToWideChar(CP_UTF8, 0, string.c_str(), -1, nullptr, 0); - if (size_needed == 0) { - return {}; + std::wstring Utf16FromUtf8(const std::string& string) { + int size_needed = MultiByteToWideChar(CP_UTF8, 0, string.c_str(), -1, nullptr, 0); + if (size_needed == 0) { + return {}; + } + std::wstring wstrTo(size_needed, 0); + int converted_length = MultiByteToWideChar(CP_UTF8, 0, string.c_str(), -1, &wstrTo[0], size_needed); + if (converted_length == 0) { + return {}; + } + return wstrTo; } - std::wstring wstrTo(size_needed, 0); - int converted_length = MultiByteToWideChar(CP_UTF8, 0, string.c_str(), -1, &wstrTo[0], size_needed); - if (converted_length == 0) { - return {}; + + bool IsWindows11OrGreater() { + DWORD dwVersion = 0; + DWORD dwBuild = 0; + +#pragma warning(push) +#pragma warning(disable : 4996) + dwVersion = GetVersion(); + // Get the build number. + if (dwVersion < 0x80000000) + dwBuild = (DWORD)(HIWORD(dwVersion)); +#pragma warning(pop) + + return dwBuild < 22000; } - return wstrTo; + + void adjustNCCALCSIZE(HWND hwnd, NCCALCSIZE_PARAMS* sz) { + LONG l = 8; + LONG t = 8; + + // HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + // Don't use `MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST)` above. + // Because if the window is restored from minimized state, the window is not in the correct monitor. + // The monitor is always the left-most monitor. + // https://github.com/leanflutter/window_manager/issues/489 + HMONITOR monitor = MonitorFromRect(&sz->rgrc[0], MONITOR_DEFAULTTONEAREST); + if (monitor != NULL) { + MONITORINFO monitorInfo; + monitorInfo.cbSize = sizeof(MONITORINFO); + if (TRUE == GetMonitorInfo(monitor, &monitorInfo)) { + l = sz->rgrc[0].left - monitorInfo.rcWork.left; + t = sz->rgrc[0].top - monitorInfo.rcWork.top; + } else { + // GetMonitorInfo failed, use (8, 8) as default value + } + } else { + // unreachable code + } + + sz->rgrc[0].left -= l; + sz->rgrc[0].top -= t; + sz->rgrc[0].right += l; + sz->rgrc[0].bottom += t; + } + + } +BaseFlutterWindow::BaseFlutterWindow() {} + +BaseFlutterWindow::~BaseFlutterWindow() { + if (window_proc_id) { + registrar_->UnregisterTopLevelWindowProcDelegate(window_proc_id); + } } + void BaseFlutterWindow::Center() { - auto handle = GetWindowHandle(); + auto handle = GetRootWindowHandle(); if (!handle) { return; } - RECT rc; - GetWindowRect(handle, &rc); - CenterRectToMonitor(&rc); - SetWindowPos(handle, nullptr, rc.left, rc.top, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); + RECT rect; + GetWindowRect(handle, &rect); + CenterRectToMonitor(&rect); + SetWindowPos(handle, nullptr, rect.left, rect.top, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); } -void BaseFlutterWindow::SetBounds(double_t x, double_t y, double_t width, double_t height) { - auto handle = GetWindowHandle(); +void BaseFlutterWindow::SetFrame(double_t left, double_t top, double_t width, double_t height, UINT flags) { + auto handle = GetRootWindowHandle(); if (!handle) { return; } - MoveWindow(handle, int32_t(x), int32_t(y), - static_cast(width), - static_cast(height), - TRUE); + + // // Get window styles + // DWORD style = GetWindowLong(handle, GWL_STYLE); + // DWORD exStyle = GetWindowLong(handle, GWL_EXSTYLE); + + // // Calculate the required window size to achieve the desired client area size + // RECT rect = { 0, 0, static_cast(width), static_cast(height) }; + + // // Adjust for window decorations (title bar, borders, etc.) + // AdjustWindowRectEx(&rect, style, FALSE, exStyle); + + // int adjustedWidth = rect.right - rect.left; + // int adjustedHeight = rect.bottom - rect.top; + + // Move and resize the window + SetWindowPos( + handle, + NULL, + static_cast(left), + static_cast(top), + static_cast(width), + static_cast(height), + SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED | flags // | SWP_NOREDRAW + ); } -void BaseFlutterWindow::SetTitle(const std::string &title) { - auto handle = GetWindowHandle(); +void BaseFlutterWindow::SetBackgroundColor(Color backgroundColor) { + // bool isTransparent = backgroundColor.a == 0 && backgroundColor.r == 0 && backgroundColor.g == 0 && backgroundColor.b == 0; + bool isTransparent = true; + + HWND hWnd = GetRootWindowHandle(); + const HINSTANCE hModule = LoadLibrary(TEXT("user32.dll")); + if (hModule) { + typedef enum _ACCENT_STATE { + ACCENT_DISABLED = 0, + ACCENT_ENABLE_GRADIENT = 1, + ACCENT_ENABLE_TRANSPARENTGRADIENT = 2, + ACCENT_ENABLE_BLURBEHIND = 3, + ACCENT_ENABLE_ACRYLICBLURBEHIND = 4, + ACCENT_ENABLE_HOSTBACKDROP = 5, + ACCENT_INVALID_STATE = 6 + } ACCENT_STATE; + struct ACCENTPOLICY { + int nAccentState; + int nFlags; + int nColor; + int nAnimationId; + }; + struct WINCOMPATTRDATA { + int nAttribute; + PVOID pData; + ULONG ulDataSize; + }; + typedef BOOL(WINAPI* pSetWindowCompositionAttribute)(HWND, WINCOMPATTRDATA*); + const pSetWindowCompositionAttribute SetWindowCompositionAttribute = (pSetWindowCompositionAttribute)GetProcAddress(hModule, "SetWindowCompositionAttribute"); + if (SetWindowCompositionAttribute) { + int32_t accent_state = isTransparent ? ACCENT_ENABLE_TRANSPARENTGRADIENT + : ACCENT_ENABLE_GRADIENT; + ACCENTPOLICY policy = { + accent_state, 2, + (int)backgroundColor.toABGR(), + 0 }; + WINCOMPATTRDATA data = { 19, &policy, sizeof(policy) }; + SetWindowCompositionAttribute(hWnd, &data); + } + FreeLibrary(hModule); + } +} + +void BaseFlutterWindow::SetOpacity(double opacity) { + auto handle = GetRootWindowHandle(); + if (!handle) { + return; + } + + long gwlExStyle = GetWindowLong(handle, GWL_EXSTYLE); + SetWindowLong(handle, GWL_EXSTYLE, gwlExStyle | WS_EX_LAYERED); + SetLayeredWindowAttributes(handle, 0, static_cast(opacity * 255), 0x02); +} + +RECT BaseFlutterWindow::GetFrame() { + HWND hwnd = GetRootWindowHandle(); + RECT rect; + if (GetWindowRect(hwnd, &rect)) { + return rect; + } + return {}; +} + +void BaseFlutterWindow::SetTitle(const std::string& title) { + auto handle = GetRootWindowHandle(); if (!handle) { return; } @@ -78,26 +216,590 @@ void BaseFlutterWindow::SetTitle(const std::string &title) { } void BaseFlutterWindow::Close() { - auto handle = GetWindowHandle(); + auto handle = GetRootWindowHandle(); if (!handle) { return; } + closed_ = true; PostMessage(handle, WM_SYSCOMMAND, SC_CLOSE, 0); } void BaseFlutterWindow::Show() { - auto handle = GetWindowHandle(); + auto handle = GetRootWindowHandle(); if (!handle) { return; } ShowWindow(handle, SW_SHOW); - } void BaseFlutterWindow::Hide() { - auto handle = GetWindowHandle(); + auto handle = GetRootWindowHandle(); if (!handle) { return; } ShowWindow(handle, SW_HIDE); } + +void BaseFlutterWindow::Focus() { + HWND hWnd = GetRootWindowHandle(); + if (IsMinimized()) { + Restore(); + } + + ::SetWindowPos(hWnd, HWND_TOP, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE); + SetForegroundWindow(hWnd); +} + +void BaseFlutterWindow::Blur() { + auto handle = GetRootWindowHandle(); + if (!handle) { + return; + } + HWND next_hwnd = ::GetNextWindow(handle, GW_HWNDNEXT); + while (next_hwnd) { + if (::IsWindowVisible(next_hwnd)) { + ::SetForegroundWindow(next_hwnd); + return; + } + next_hwnd = ::GetNextWindow(next_hwnd, GW_HWNDNEXT); + } +} + +bool BaseFlutterWindow::IsFocused() { + return GetRootWindowHandle() == GetForegroundWindow(); +} + +bool BaseFlutterWindow::IsVisible() { + auto handle = GetRootWindowHandle(); + if (!handle) { + return false; + } + bool isVisible = IsWindowVisible(handle); + return isVisible; +} + +bool BaseFlutterWindow::IsMaximized() { + auto handle = GetRootWindowHandle(); + if (!handle) { + return false; + } + WINDOWPLACEMENT windowPlacement; + GetWindowPlacement(handle, &windowPlacement); + + return windowPlacement.showCmd == SW_MAXIMIZE; +} + +bool BaseFlutterWindow::IsMinimized() { + auto handle = GetRootWindowHandle(); + if (!handle) { + return false; + } + WINDOWPLACEMENT windowPlacement; + GetWindowPlacement(handle, &windowPlacement); + return windowPlacement.showCmd == SW_SHOWMINIMIZED; +} + +void BaseFlutterWindow::Maximize(bool vertically) { + auto handle = GetRootWindowHandle(); + if (!handle) { + return; + } + WINDOWPLACEMENT windowPlacement; + GetWindowPlacement(handle, &windowPlacement); + + if (vertically) { + POINT cursorPos; + GetCursorPos(&cursorPos); + PostMessage(handle, WM_NCLBUTTONDBLCLK, HTTOP, + MAKELPARAM(cursorPos.x, cursorPos.y)); + } else { + if (windowPlacement.showCmd != SW_MAXIMIZE) { + PostMessage(handle, WM_SYSCOMMAND, SC_MAXIMIZE, 0); + } + } +} + +void BaseFlutterWindow::Unmaximize() { + auto handle = GetRootWindowHandle(); + if (!handle) { + return; + } + WINDOWPLACEMENT windowPlacement; + GetWindowPlacement(handle, &windowPlacement); + + if (windowPlacement.showCmd != SW_NORMAL) { + PostMessage(handle, WM_SYSCOMMAND, SC_RESTORE, 0); + } +} + +void BaseFlutterWindow::Minimize() { + if (IsFullScreen()) { // Like chromium, we don't want to minimize fullscreen + // windows + return; + } + auto handle = GetRootWindowHandle(); + if (!handle) { + return; + } WINDOWPLACEMENT windowPlacement; + GetWindowPlacement(handle, &windowPlacement); + + if (windowPlacement.showCmd != SW_SHOWMINIMIZED) { + PostMessage(handle, WM_SYSCOMMAND, SC_MINIMIZE, 0); + } +} + +void BaseFlutterWindow::Restore() { + auto handle = GetRootWindowHandle(); + if (!handle) { + return; + } + WINDOWPLACEMENT windowPlacement; + GetWindowPlacement(handle, &windowPlacement); + + if (windowPlacement.showCmd != SW_NORMAL) { + PostMessage(handle, WM_SYSCOMMAND, SC_RESTORE, 0); + } +} + +bool BaseFlutterWindow::IsFullScreen() { + return g_is_window_fullscreen; +} + +void BaseFlutterWindow::SetFullScreen(bool is_full_screen) { + auto handle = GetRootWindowHandle(); + if (!handle) { + return; + } + // Previously inspired by how Chromium does this + // https://src.chromium.org/viewvc/chrome/trunk/src/ui/views/win/fullscreen_handler.cc?revision=247204&view=markup + // Instead, we use a modified implementation of how the media_kit package + // implements this (we got permission from the author, I believe) + // https://github.com/alexmercerind/media_kit/blob/1226bcff36eab27cb17d60c33e9c15ca489c1f06/media_kit_video/windows/utils.cc + + // Save current window state if not already fullscreen. + if (!g_is_window_fullscreen) { + // Save current window information. + g_maximized_before_fullscreen = ::IsZoomed(handle); + g_style_before_fullscreen = GetWindowLong(handle, GWL_STYLE); + ::GetWindowRect(handle, &g_frame_before_fullscreen); + // g_title_bar_style_before_fullscreen = title_bar_style_; + } + + g_is_window_fullscreen = is_full_screen; + + if (is_full_screen) { // Set to fullscreen + ::SendMessage(handle, WM_SYSCOMMAND, SC_MAXIMIZE, 0); + // if (!is_frameless_) { + // auto monitor = MONITORINFO{}; + // auto placement = WINDOWPLACEMENT{}; + // monitor.cbSize = sizeof(MONITORINFO); + // placement.length = sizeof(WINDOWPLACEMENT); + // ::GetWindowPlacement(handle, &placement); + // ::GetMonitorInfo( + // ::MonitorFromWindow(handle, MONITOR_DEFAULTTONEAREST), &monitor); + // ::SetWindowLongPtr(handle, GWL_STYLE, + // g_style_before_fullscreen & ~WS_OVERLAPPEDWINDOW); + // ::SetWindowPos(handle, HWND_TOP, monitor.rcMonitor.left, + // monitor.rcMonitor.top, + // monitor.rcMonitor.right - monitor.rcMonitor.left, + // monitor.rcMonitor.bottom - monitor.rcMonitor.top, + // SWP_NOOWNERZORDER | SWP_FRAMECHANGED); + // } + } else { // Restore from fullscreen + if (!g_maximized_before_fullscreen) + Restore(); + ::SetWindowLongPtr(handle, GWL_STYLE, + g_style_before_fullscreen | WS_OVERLAPPEDWINDOW); + if (::IsZoomed(handle)) { + // Refresh the parent mainWindow. + ::SetWindowPos(handle, nullptr, 0, 0, 0, 0, + SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | + SWP_FRAMECHANGED); + auto rect = RECT{}; + ::GetClientRect(handle, &rect); + auto flutter_view = ::FindWindowEx(handle, nullptr, + kFlutterViewWindowClassName, nullptr); + ::SetWindowPos(flutter_view, nullptr, rect.left, rect.top, + rect.right - rect.left, rect.bottom - rect.top, + SWP_NOACTIVATE | SWP_NOZORDER); + if (g_maximized_before_fullscreen) + PostMessage(handle, WM_SYSCOMMAND, SC_MAXIMIZE, 0); + } else { + ::SetWindowPos( + handle, nullptr, g_frame_before_fullscreen.left, + g_frame_before_fullscreen.top, + g_frame_before_fullscreen.right - g_frame_before_fullscreen.left, + g_frame_before_fullscreen.bottom - g_frame_before_fullscreen.top, + SWP_NOACTIVATE | SWP_NOZORDER); + } + } +} + +void BaseFlutterWindow::SetStyle(int32_t style, int32_t extended_style) { + auto handle = GetRootWindowHandle(); + if (!handle) { + return; + } + + // Store current window position and size + RECT windowRect; + GetWindowRect(handle, &windowRect); + + // Store current visibility state + bool wasVisible = IsWindowVisible(handle); + bool wasMaximized = IsZoomed(handle); + + if (wasMaximized) { + // Restore the window before changing styles if it's maximized + ShowWindow(handle, SW_RESTORE); + } + + // Set the new styles + SetWindowLongPtr(handle, GWL_STYLE, style); + SetWindowLongPtr(handle, GWL_EXSTYLE, extended_style); + + // Update the window's frame + SetWindowPos( + handle, + nullptr, + windowRect.left, + windowRect.top, + windowRect.right - windowRect.left, + windowRect.bottom - windowRect.top, + SWP_FRAMECHANGED | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_NOACTIVATE | + (wasVisible ? SWP_SHOWWINDOW : SWP_HIDEWINDOW) + ); + + // Get the new client area + // RECT newClientRect; + // GetClientRect(handle, &newClientRect); + + // Find the Flutter view - try different class names that might be used + // HWND flutterView = NULL; + // const wchar_t* possibleClassNames[] = { + // L"FlutterMultiWindow", + // L"FLUTTER_RUNNER_WIN32_WINDOW", + // L"FLUTTERVIEW" + // }; + + // for (const auto& className : possibleClassNames) { + // flutterView = FindWindowEx(handle, NULL, className, NULL); + // if (flutterView) break; + // } + + // // If we found the Flutter view, resize it to fill the client area + // if (flutterView) { + // SetWindowPos( + // flutterView, + // NULL, + // 0, 0, + // newClientRect.right, newClientRect.bottom, + // SWP_NOZORDER + // ); + // } else { + // // If we couldn't find the Flutter view by class name, try the first child window + // flutterView = GetWindow(handle, GW_CHILD); + // if (flutterView) { + // SetWindowPos( + // flutterView, + // NULL, + // 0, 0, + // newClientRect.right, newClientRect.bottom, + // SWP_NOZORDER + // ); + // } + // } + + // Restore maximized state if needed + if (wasMaximized) { + ShowWindow(handle, SW_MAXIMIZE); + } + + // Force a complete redraw + RedrawWindow(handle, NULL, NULL, RDW_INVALIDATE | RDW_UPDATENOW | RDW_FRAME | RDW_ALLCHILDREN); +} + +void BaseFlutterWindow::Destroy() { + if (inter_window_event_channel_) { + inter_window_event_channel_ = nullptr; + } + if (window_events_channel_) { + window_events_channel_ = nullptr; + } + + if (id_ != 0) { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + } +} + +void BaseFlutterWindow::_EmitEvent(std::string eventName) +{ + if (window_events_channel_ == nullptr) { + return; + } + if (!has_listeners_) { + return; + } + flutter::EncodableMap args = flutter::EncodableMap(); + args[flutter::EncodableValue("eventName")] = flutter::EncodableValue(eventName); + window_events_channel_->channel_->InvokeMethod("onEvent", std::make_unique(args)); +} + +bool BaseFlutterWindow::MessageHandler(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = flutter_controller_->HandleTopLevelWindowProc(hWnd, message, wParam, lParam); + if (result) { + return *result; + } + } + std::optional result = HandleWindowProc(hWnd, message, wParam, lParam); + if (result) { + return *result; + } + return DefWindowProc(window_handle_, message, wParam, lParam); +} + +std::optional BaseFlutterWindow::HandleWindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { + + auto child_content_ = flutter_controller_ ? flutter_controller_->view()->GetNativeWindow() : nullptr; + + std::optional result = std::nullopt; + + switch (message) { + case WM_USER + 37: { + if (window_events_channel_ == nullptr || !has_listeners_) { + delete reinterpret_cast*>(wParam); + return true; + } + auto ptr_to_shared = reinterpret_cast*>(wParam); + window_events_channel_->channel_->InvokeMethod("onEvent", std::make_unique(**ptr_to_shared)); + delete ptr_to_shared; + return true; + } + case WM_FONTCHANGE: { + if (flutter_controller_) { + flutter_controller_->engine()->ReloadSystemFonts(); + return true; + } + break; + } + case WM_DESTROY: { + Destroy(); + if (!destroyed_) { + destroyed_ = true; + if (auto callback = callback_.lock()) { + callback->OnWindowDestroy(id_); + } + } + break; + } + case WM_CLOSE: { + if (auto callback = callback_.lock()) { + callback->OnWindowClose(id_); + } + _EmitEvent("close"); + break; + } + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lParam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hWnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return true; + } + case WM_SIZE: { + if (IsFullScreen() && wParam == SIZE_MAXIMIZED && + last_state != STATE_FULLSCREEN_ENTERED) { + _EmitEvent("enter-full-screen"); + last_state = STATE_FULLSCREEN_ENTERED; + } else if (!IsFullScreen() && wParam == SIZE_RESTORED && + last_state == STATE_FULLSCREEN_ENTERED) { + _EmitEvent("leave-full-screen"); + last_state = STATE_NORMAL; + } else if (last_state != STATE_FULLSCREEN_ENTERED) { + if (wParam == SIZE_MAXIMIZED) { + _EmitEvent("maximize"); + last_state = STATE_MAXIMIZED; + } else if (wParam == SIZE_MINIMIZED) { + _EmitEvent("minimize"); + last_state = STATE_MINIMIZED; + return 0; + } else if (wParam == SIZE_RESTORED) { + if (last_state == STATE_MAXIMIZED) { + _EmitEvent("unmaximize"); + last_state = STATE_NORMAL; + } else if (last_state == STATE_MINIMIZED) { + _EmitEvent("restore"); + last_state = STATE_NORMAL; + } + } + } + + if (child_content_ != nullptr) { + RECT rect; + GetClientRect(hWnd, &rect); + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + return true; + } + break; + } + case WM_NCACTIVATE: { + if (wParam == 0) { + _EmitEvent("blur"); + } else { + _EmitEvent("focus"); + } + break; + } + case WM_ACTIVATE: { + + // // Extract activation state from the low-order word + // UINT activationState = LOWORD(wparam); + + // // Check if the window is minimized (nonzero high-order word) + // BOOL isMinimized = HIWORD(wparam) != 0; + + // switch (activationState) { + // case WA_INACTIVE: + // // Handle deactivation + // std::cerr << "deactivate" << std::endl; + // break; + // case WA_ACTIVE: + // // Handle activation (without mouse click) + // std::cerr << "activate" << std::endl; + // break; + // case WA_CLICKACTIVE: + // // Handle activation (with mouse click) + // std::cerr << "click-activate" << std::endl; + // break; + // default: + // break; + // } + + // // You can also use the 'isMinimized' flag as needed + // if (isMinimized) { + // // The window was minimized. + // } + if (child_content_ != nullptr) { + SetFocus(child_content_); + return true; + } + break; + } + case WM_EXITSIZEMOVE: { + if (is_resizing_) { + _EmitEvent("resized"); + is_resizing_ = false; + } + if (is_moving_) { + _EmitEvent("moved"); + is_moving_ = false; + } + break; + } + case WM_MOVING: { + is_moving_ = true; + _EmitEvent("move"); + break; + } + case WM_SIZING: { + is_resizing_ = true; + _EmitEvent("resize"); + if (aspect_ratio_ > 0) { + RECT* rect = (LPRECT)lParam; + + double aspect_ratio = aspect_ratio_; + + int new_width = static_cast(rect->right - rect->left); + int new_height = static_cast(rect->bottom - rect->top); + + bool is_resizing_horizontally = + wParam == WMSZ_LEFT || wParam == WMSZ_RIGHT || + wParam == WMSZ_TOPLEFT || wParam == WMSZ_BOTTOMLEFT; + + if (is_resizing_horizontally) { + new_height = static_cast(new_width / aspect_ratio); + } else { + new_width = static_cast(new_height * aspect_ratio); + } + + int left = rect->left; + int top = rect->top; + int right = rect->right; + int bottom = rect->bottom; + + switch (wParam) { + case WMSZ_RIGHT: + case WMSZ_BOTTOM: + right = new_width + left; + bottom = top + new_height; + break; + case WMSZ_TOP: + right = new_width + left; + top = bottom - new_height; + break; + case WMSZ_LEFT: + case WMSZ_TOPLEFT: + left = right - new_width; + top = bottom - new_height; + break; + case WMSZ_TOPRIGHT: + right = left + new_width; + top = bottom - new_height; + break; + case WMSZ_BOTTOMLEFT: + left = right - new_width; + bottom = top + new_height; + break; + case WMSZ_BOTTOMRIGHT: + right = left + new_width; + bottom = top + new_height; + break; + } + + rect->left = left; + rect->top = top; + rect->right = right; + rect->bottom = bottom; + } + break; + } + case WM_SHOWWINDOW: { + if (wParam == TRUE) { + _EmitEvent("show"); + } else { + _EmitEvent("hide"); + } + break; + } + default: { + break; + } + } + return result; +} + +void BaseFlutterWindow::SetIgnoreMouseEvents(bool ignore) { + auto handle = GetRootWindowHandle(); + if (!handle) { + return; + } + LONG ex_style = GetWindowLong(handle, GWL_EXSTYLE); + if (ignore) + ex_style |= (WS_EX_TRANSPARENT | WS_EX_LAYERED); + else + ex_style &= ~(WS_EX_TRANSPARENT | WS_EX_LAYERED); + + SetWindowLong(handle, GWL_EXSTYLE, ex_style); +} \ No newline at end of file diff --git a/packages/desktop_multi_window/windows/base_flutter_window.h b/packages/desktop_multi_window/windows/base_flutter_window.h index fe4ad2b7..eb5e8656 100644 --- a/packages/desktop_multi_window/windows/base_flutter_window.h +++ b/packages/desktop_multi_window/windows/base_flutter_window.h @@ -5,15 +5,67 @@ #ifndef MULTI_WINDOW_WINDOWS_BASE_FLUTTER_WINDOW_H_ #define MULTI_WINDOW_WINDOWS_BASE_FLUTTER_WINDOW_H_ -#include "window_channel.h" +#include + +#include "inter_window_event_channel.h" +#include "window_events_channel.h" +#include "window_options.h" + +enum WindowState { + STATE_NORMAL, + STATE_MAXIMIZED, + STATE_MINIMIZED, + STATE_FULLSCREEN_ENTERED, + STATE_DOCKED, +}; + +class BaseFlutterWindowCallback { + +public: + virtual void OnWindowClose(int64_t id) = 0; + + virtual void OnWindowDestroy(int64_t id) = 0; + +}; + +class BaseFlutterWindow +{ -class BaseFlutterWindow { +public: - public: + BaseFlutterWindow(); + ~BaseFlutterWindow(); - virtual ~BaseFlutterWindow() = default; + InterWindowEventChannel* GetInterWindowEventChannel() { + return inter_window_event_channel_.get(); + } - virtual WindowChannel *GetWindowChannel() = 0; + WindowEventsChannel* GetWindowEventsChannel() { + return window_events_channel_.get(); + } + + HWND GetWindowHandle() { + return window_handle_; + } + + HWND GetRootWindowHandle() { + if (!root_window_handle_ || !IsWindow(root_window_handle_)) { + root_window_handle_ = GetAncestor(window_handle_, GA_ROOT); + } + return root_window_handle_; + } + + bool IsDestroyed() { + return destroyed_; + } + + bool IsClosed() { + return closed_; + } + + void SetHasListeners(bool has_listeners) { + has_listeners_ = has_listeners; + } void Show(); @@ -21,16 +73,100 @@ class BaseFlutterWindow { void Close(); - void SetTitle(const std::string &title); + void SetTitle(const std::string& title); + + RECT GetFrame(); + + void SetFrame(double_t x, double_t y, double_t width, double_t height, UINT flags); + + void SetBackgroundColor(Color backgroundColor); + + void SetOpacity(double opacity); + + // void SetMinSize(double_t width, double_t height); + + // void SetMaxSize(double_t width, double_t height); + + bool IsFocused(); + + bool IsFullScreen(); + + bool IsMaximized(); + + bool IsMinimized(); + + bool IsVisible(); + + void Focus(); + + void Blur(); + + void Maximize(bool vertically); - void SetBounds(double_t x, double_t y, double_t width, double_t height); + void Unmaximize(); + + void Minimize(); + + void Restore(); + + void SetFullScreen(bool is_full_screen); + + void SetStyle(int32_t style, int32_t extended_style); + + void SetIgnoreMouseEvents(bool ignore); void Center(); - protected: + std::optional HandleWindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); + + bool has_listeners_ = false; +protected: + + int64_t id_; + + HWND window_handle_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; + + flutter::PluginRegistrarWindows* registrar_; + + std::unique_ptr inter_window_event_channel_; + + std::unique_ptr window_events_channel_; + + int window_proc_id = 0; + + void _EmitEvent(std::string eventName); + + bool MessageHandler(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); + + std::weak_ptr callback_; + + bool destroyed_ = false; + + bool closed_ = false; + + void Destroy(); + +private: + + static constexpr auto kFlutterViewWindowClassName = L"FlutterMultiWindow"; + bool g_is_window_fullscreen = false; + std::string g_title_bar_style_before_fullscreen; + RECT g_frame_before_fullscreen; + bool g_maximized_before_fullscreen; + LONG g_style_before_fullscreen; + + HWND root_window_handle_; + + double aspect_ratio_ = 0; + + bool is_moving_ = false; + bool is_resizing_ = false; - virtual HWND GetWindowHandle() = 0; + WindowState last_state = STATE_NORMAL; }; -#endif //MULTI_WINDOW_WINDOWS_BASE_FLUTTER_WINDOW_H_ +#endif // MULTI_WINDOW_WINDOWS_BASE_FLUTTER_WINDOW_H diff --git a/packages/desktop_multi_window/windows/desktop_multi_window_plugin.cpp b/packages/desktop_multi_window/windows/desktop_multi_window_plugin.cpp index 803a5de7..80fa3de6 100644 --- a/packages/desktop_multi_window/windows/desktop_multi_window_plugin.cpp +++ b/packages/desktop_multi_window/windows/desktop_multi_window_plugin.cpp @@ -9,113 +9,326 @@ #include #include "multi_window_manager.h" +#include "window_options.h" -namespace { +namespace +{ + class DesktopMultiWindowPlugin : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); -class DesktopMultiWindowPlugin : public flutter::Plugin { - public: - static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); + DesktopMultiWindowPlugin(); - DesktopMultiWindowPlugin(); + ~DesktopMultiWindowPlugin() override; - ~DesktopMultiWindowPlugin() override; - - private: - void HandleMethodCall( - const flutter::MethodCall &method_call, + private: + void HandleMethodCall( + const flutter::MethodCall& method_call, std::unique_ptr> result); -}; - -// static -void DesktopMultiWindowPlugin::RegisterWithRegistrar( - flutter::PluginRegistrarWindows *registrar) { - auto channel = - std::make_unique>( - registrar->messenger(), "mixin.one/flutter_multi_window", - &flutter::StandardMethodCodec::GetInstance()); - - auto plugin = std::make_unique(); - - channel->SetMethodCallHandler( - [plugin_pointer = plugin.get()](const auto &call, auto result) { - plugin_pointer->HandleMethodCall(call, std::move(result)); - }); - registrar->AddPlugin(std::move(plugin)); -} + }; -DesktopMultiWindowPlugin::DesktopMultiWindowPlugin() = default; + // static + void DesktopMultiWindowPlugin::RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar) { + auto channel = std::make_unique>(registrar->messenger(), "mixin.one/flutter_multi_window", &flutter::StandardMethodCodec::GetInstance()); -DesktopMultiWindowPlugin::~DesktopMultiWindowPlugin() = default; + auto plugin = std::make_unique(); -void DesktopMultiWindowPlugin::HandleMethodCall( - const flutter::MethodCall &method_call, - std::unique_ptr> result) { - if (method_call.method_name() == "createWindow") { - auto args = std::get_if(method_call.arguments()); - auto window_id = MultiWindowManager::Instance()->Create(args != nullptr ? *args : ""); - result->Success(flutter::EncodableValue(window_id)); - return; - } else if (method_call.method_name() == "show") { - auto window_id = method_call.arguments()->LongValue(); - MultiWindowManager::Instance()->Show(window_id); - result->Success(); - return; - } else if (method_call.method_name() == "hide") { - auto window_id = method_call.arguments()->LongValue(); - MultiWindowManager::Instance()->Hide(window_id); - result->Success(); - return; - } else if (method_call.method_name() == "close") { - auto window_id = method_call.arguments()->LongValue(); - MultiWindowManager::Instance()->Close(window_id); - result->Success(); - return; - } else if (method_call.method_name() == "setFrame") { - auto *arguments = std::get_if(method_call.arguments()); - auto window_id = arguments->at(flutter::EncodableValue("windowId")).LongValue(); - auto left = std::get(arguments->at(flutter::EncodableValue("left"))); - auto top = std::get(arguments->at(flutter::EncodableValue("top"))); - auto width = std::get(arguments->at(flutter::EncodableValue("width"))); - auto height = std::get(arguments->at(flutter::EncodableValue("height"))); - MultiWindowManager::Instance()->SetFrame(window_id, left, top, width, height); - result->Success(); - return; - } else if (method_call.method_name() == "center") { - auto window_id = method_call.arguments()->LongValue(); - MultiWindowManager::Instance()->Center(window_id); - result->Success(); - return; - } else if (method_call.method_name() == "setTitle") { - auto *arguments = std::get_if(method_call.arguments()); - auto window_id = arguments->at(flutter::EncodableValue("windowId")).LongValue(); - auto title = std::get(arguments->at(flutter::EncodableValue("title"))); - MultiWindowManager::Instance()->SetTitle(window_id, title); - result->Success(); - return; - } else if (method_call.method_name() == "getAllSubWindowIds") { - auto window_ids = MultiWindowManager::Instance()->GetAllSubWindowIds(); - result->Success(window_ids); - return; + channel->SetMethodCallHandler([plugin_pointer = plugin.get()](const auto& call, auto result) + { + plugin_pointer->HandleMethodCall(call, std::move(result)); + }); + registrar->AddPlugin(std::move(plugin)); } - result->NotImplemented(); -} -} // namespace + DesktopMultiWindowPlugin::DesktopMultiWindowPlugin() = default; + + DesktopMultiWindowPlugin::~DesktopMultiWindowPlugin() = default; + + void DesktopMultiWindowPlugin::HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result) { + // std::cout << "Method called: " << method_call.method_name() << std::endl; + + // if (method_call.arguments()) { + // std::cout << "Method call arguments:\n"; + // PrintEncodableValue(*method_call.arguments()); + // std::cout << "\n"; + // } + + if (method_call.method_name() == "createWindow") { + // Default values. + std::string stringArgs = ""; + WindowOptions options; + + // Check if the argument is a map. + if (method_call.arguments() && + std::holds_alternative(*method_call.arguments())) { + const auto& args_map = std::get(*method_call.arguments()); + + // If a string "arguments" is provided in the map, extract it. + auto argsIter = args_map.find(flutter::EncodableValue("arguments")); + if (argsIter != args_map.end() && std::holds_alternative(argsIter->second)) { + stringArgs = std::get(argsIter->second); + } + + // If window "options" are provided in the map, parse them. + auto optionsIter = args_map.find(flutter::EncodableValue("options")); + if (optionsIter != args_map.end() && + std::holds_alternative(optionsIter->second)) { + const auto& optsMap = std::get(optionsIter->second); -void DesktopMultiWindowPluginRegisterWithRegistrar( - FlutterDesktopPluginRegistrarRef registrar) { + // Look for the "windows" key in the options map. + auto windowsIter = optsMap.find(flutter::EncodableValue("windows")); + if (windowsIter != optsMap.end() && + std::holds_alternative(windowsIter->second)) { + const auto& windows_map = std::get(windowsIter->second); + options.Parse(windows_map); + // options.Print(); + } else { + std::cout << "Key 'windows' not found or is not a map." << std::endl; + } + } else { + std::cout << "No 'options' key found in arguments or it is not a map." << std::endl; + } + } else if (method_call.arguments() && + std::holds_alternative(*method_call.arguments())) { + // If a simple string is passed instead of a map, use it as the arguments. + stringArgs = std::get(*method_call.arguments()); + } + auto window_id = MultiWindowManager::Instance()->Create(stringArgs, options); + result->Success(flutter::EncodableValue(window_id)); + return; + } else if (method_call.method_name() == "setHasListeners") { + auto* arguments = std::get_if(method_call.arguments()); + auto window_id = arguments->at(flutter::EncodableValue("windowId")).LongValue(); + auto* null_or_has_listeners = std::get_if(ValueOrNull(*arguments, "hasListeners")); + bool has_listeners = false; + if (null_or_has_listeners != nullptr) { + has_listeners = *null_or_has_listeners; + } + MultiWindowManager::Instance()->SetHasListeners(window_id, has_listeners); + result->Success(flutter::EncodableValue(true)); + return; + } else if (method_call.method_name() == "show") { + auto* arguments = std::get_if(method_call.arguments()); + auto window_id = arguments->at(flutter::EncodableValue("windowId")).LongValue(); + MultiWindowManager::Instance()->Show(window_id); + result->Success(flutter::EncodableValue(true)); + return; + } else if (method_call.method_name() == "hide") { + auto* arguments = std::get_if(method_call.arguments()); + auto window_id = arguments->at(flutter::EncodableValue("windowId")).LongValue(); + MultiWindowManager::Instance()->Hide(window_id); + result->Success(flutter::EncodableValue(true)); + return; + } else if (method_call.method_name() == "close") { + auto* arguments = std::get_if(method_call.arguments()); + auto window_id = arguments->at(flutter::EncodableValue("windowId")).LongValue(); + MultiWindowManager::Instance()->Close(window_id); + result->Success(flutter::EncodableValue(true)); + return; + } else if (method_call.method_name() == "setFrame") { + auto* arguments = std::get_if(method_call.arguments()); + auto window_id = arguments->at(flutter::EncodableValue("windowId")).LongValue(); + + auto* null_or_devicePixelRatio = std::get_if(ValueOrNull(*arguments, "devicePixelRatio")); + double devicePixelRatio = 1.0; + if (null_or_devicePixelRatio != nullptr) { + devicePixelRatio = *null_or_devicePixelRatio; + } + + auto* null_or_animate = std::get_if(ValueOrNull(*arguments, "animate")); + bool animate = false; + if (null_or_animate != nullptr) { + animate = *null_or_animate; + } + + auto* null_or_x = std::get_if(ValueOrNull(*arguments, "left")); + auto* null_or_y = std::get_if(ValueOrNull(*arguments, "top")); + auto* null_or_width = std::get_if(ValueOrNull(*arguments, "width")); + auto* null_or_height = std::get_if(ValueOrNull(*arguments, "height")); + + int x = 0; + int y = 0; + int width = 0; + int height = 0; + UINT uFlags = NULL; + + if (null_or_x != nullptr && null_or_y != nullptr) { + x = static_cast(*null_or_x * devicePixelRatio); + y = static_cast(*null_or_y * devicePixelRatio); + } + if (null_or_width != nullptr && null_or_height != nullptr) { + width = static_cast(*null_or_width * devicePixelRatio); + height = static_cast(*null_or_height * devicePixelRatio); + } + + if (null_or_x == nullptr || null_or_y == nullptr) { + uFlags = SWP_NOMOVE; + } + if (null_or_width == nullptr || null_or_height == nullptr) { + uFlags = SWP_NOSIZE; + } + + MultiWindowManager::Instance()->SetFrame(window_id, x, y, width, height, uFlags); + result->Success(); + return; + } else if (method_call.method_name() == "getFrame") { + auto* arguments = std::get_if(method_call.arguments()); + auto window_id = arguments->at(flutter::EncodableValue("windowId")).LongValue(); + double devicePixelRatio = std::get(arguments->at(flutter::EncodableValue("devicePixelRatio"))); + auto frame = MultiWindowManager::Instance()->GetFrame(window_id, devicePixelRatio); + result->Success(frame); + return; + } else if (method_call.method_name() == "center") { + auto* arguments = std::get_if(method_call.arguments()); + auto window_id = arguments->at(flutter::EncodableValue("windowId")).LongValue(); + MultiWindowManager::Instance()->Center(window_id); + result->Success(flutter::EncodableValue(true)); + return; + } else if (method_call.method_name() == "setTitle") { + auto* arguments = std::get_if(method_call.arguments()); + auto window_id = arguments->at(flutter::EncodableValue("windowId")).LongValue(); + auto title = std::get(arguments->at(flutter::EncodableValue("title"))); + MultiWindowManager::Instance()->SetTitle(window_id, title); + result->Success(flutter::EncodableValue(true)); + return; + } else if (method_call.method_name() == "isFocused") { + auto* arguments = std::get_if(method_call.arguments()); + auto window_id = arguments->at(flutter::EncodableValue("windowId")).LongValue(); + auto is_focused = MultiWindowManager::Instance()->IsFocused(window_id); + result->Success(flutter::EncodableValue(is_focused)); + return; + } else if (method_call.method_name() == "isFullScreen") { + auto* arguments = std::get_if(method_call.arguments()); + auto window_id = arguments->at(flutter::EncodableValue("windowId")).LongValue(); + auto is_full_screen = MultiWindowManager::Instance()->IsFullScreen(window_id); + result->Success(flutter::EncodableValue(is_full_screen)); + return; + } else if (method_call.method_name() == "isMaximized") { + auto* arguments = std::get_if(method_call.arguments()); + auto window_id = arguments->at(flutter::EncodableValue("windowId")).LongValue(); + auto is_maximized = MultiWindowManager::Instance()->IsMaximized(window_id); + result->Success(flutter::EncodableValue(is_maximized)); + return; + } else if (method_call.method_name() == "isMinimized") { + auto* arguments = std::get_if(method_call.arguments()); + auto window_id = arguments->at(flutter::EncodableValue("windowId")).LongValue(); + auto is_minimized = MultiWindowManager::Instance()->IsMinimized(window_id); + result->Success(flutter::EncodableValue(is_minimized)); + return; + } else if (method_call.method_name() == "isVisible") { + auto* arguments = std::get_if(method_call.arguments()); + auto window_id = arguments->at(flutter::EncodableValue("windowId")).LongValue(); + auto is_visible = MultiWindowManager::Instance()->IsVisible(window_id); + result->Success(flutter::EncodableValue(is_visible)); + return; + } else if (method_call.method_name() == "maximize") { + auto* arguments = std::get_if(method_call.arguments()); + auto window_id = arguments->at(flutter::EncodableValue("windowId")).LongValue(); + bool vertically = std::get(arguments->at(flutter::EncodableValue("vertically"))); + std::cout << "maximize: " << window_id << " " << vertically << std::endl; + MultiWindowManager::Instance()->Maximize(window_id, vertically); + result->Success(flutter::EncodableValue(true)); + return; + } else if (method_call.method_name() == "unmaximize") { + auto* arguments = std::get_if(method_call.arguments()); + auto window_id = arguments->at(flutter::EncodableValue("windowId")).LongValue(); + MultiWindowManager::Instance()->Unmaximize(window_id); + result->Success(flutter::EncodableValue(true)); + return; + } else if (method_call.method_name() == "minimize") { + auto* arguments = std::get_if(method_call.arguments()); + auto window_id = arguments->at(flutter::EncodableValue("windowId")).LongValue(); + MultiWindowManager::Instance()->Minimize(window_id); + result->Success(flutter::EncodableValue(true)); + return; + } else if (method_call.method_name() == "restore") { + auto* arguments = std::get_if(method_call.arguments()); + auto window_id = arguments->at(flutter::EncodableValue("windowId")).LongValue(); + MultiWindowManager::Instance()->Restore(window_id); + result->Success(flutter::EncodableValue(true)); + return; + } else if (method_call.method_name() == "setFullScreen") { + auto* arguments = std::get_if(method_call.arguments()); + auto window_id = arguments->at(flutter::EncodableValue("windowId")).LongValue(); + auto is_full_screen = std::get(arguments->at(flutter::EncodableValue("isFullScreen"))); + MultiWindowManager::Instance()->SetFullScreen(window_id, is_full_screen); + result->Success(flutter::EncodableValue(true)); + return; + } else if (method_call.method_name() == "setStyle") { + auto* arguments = std::get_if(method_call.arguments()); + auto window_id = arguments->at(flutter::EncodableValue("windowId")).LongValue(); + auto style = std::get(arguments->at(flutter::EncodableValue("style"))); + auto extended_style = std::get(arguments->at(flutter::EncodableValue("extendedStyle"))); + Color backgroundColor; + try { + auto backgroundColorIter = arguments->find(flutter::EncodableValue("backgroundColor")); + if (backgroundColorIter != arguments->end() && + std::holds_alternative(backgroundColorIter->second)) { + const auto& backgroundColorMap = std::get(backgroundColorIter->second); + backgroundColor = Color::ParseColor(backgroundColorMap); + } + } catch (const std::exception& e) { + std::cerr << L"Error parsing background color: " << e.what() << std::endl; + } + MultiWindowManager::Instance()->SetBackgroundColor(window_id, backgroundColor); + MultiWindowManager::Instance()->SetStyle(window_id, style, extended_style); + result->Success(flutter::EncodableValue(true)); + return; + } else if (method_call.method_name() == "setBackgroundColor") { + auto* arguments = std::get_if(method_call.arguments()); + auto window_id = arguments->at(flutter::EncodableValue("windowId")).LongValue(); + Color backgroundColor; + try { + auto backgroundColorIter = arguments->find(flutter::EncodableValue("backgroundColor")); + if (backgroundColorIter != arguments->end() && + std::holds_alternative(backgroundColorIter->second)) { + const auto& backgroundColorMap = std::get(backgroundColorIter->second); + backgroundColor = Color::ParseColor(backgroundColorMap); + } + } catch (const std::exception& e) { + std::cerr << L"Error parsing background color: " << e.what() << std::endl; + } + MultiWindowManager::Instance()->SetBackgroundColor(window_id, backgroundColor); + result->Success(flutter::EncodableValue(true)); + return; + } else if (method_call.method_name() == "getAllSubWindowIds") { + auto window_ids = MultiWindowManager::Instance()->GetAllSubWindowIds(); + result->Success(window_ids); + return; + } else if (method_call.method_name() == "setIgnoreMouseEvents") { + auto* arguments = std::get_if(method_call.arguments()); + auto window_id = arguments->at(flutter::EncodableValue("windowId")).LongValue(); + auto ignore = std::get(arguments->at(flutter::EncodableValue("ignore"))); + MultiWindowManager::Instance()->SetIgnoreMouseEvents(window_id, ignore); + result->Success(flutter::EncodableValue(true)); + return; + } + result->NotImplemented(); + } + +} // namespace + +void DesktopMultiWindowPluginRegisterWithRegistrar(FlutterDesktopPluginRegistrarRef registrar) { + auto registrar_windows = flutter::PluginRegistrarManager::GetInstance()->GetRegistrar(registrar); InternalMultiWindowPluginRegisterWithRegistrar(registrar); // Attach MainWindow for - auto hwnd = FlutterDesktopViewGetHWND(FlutterDesktopPluginRegistrarGetView(registrar)); - auto channel = WindowChannel::RegisterWithRegistrar(registrar, 0); - MultiWindowManager::Instance()->AttachFlutterMainWindow(GetAncestor(hwnd, GA_ROOT), - std::move(channel)); + auto view = FlutterDesktopPluginRegistrarGetView(registrar); + auto hwnd = FlutterDesktopViewGetHWND(view); + + auto inter_window_event_channel = InterWindowEventChannel::RegisterWithRegistrar(registrar, 0); + auto window_events_channel = WindowEventsChannel::RegisterWithRegistrar(registrar); + MultiWindowManager::Instance()->AttachFlutterMainWindow( + GetAncestor(hwnd, GA_ROOT), + std::move(inter_window_event_channel), + std::move(window_events_channel), + registrar_windows + ); } void InternalMultiWindowPluginRegisterWithRegistrar(FlutterDesktopPluginRegistrarRef registrar) { - DesktopMultiWindowPlugin::RegisterWithRegistrar( - flutter::PluginRegistrarManager::GetInstance() - ->GetRegistrar(registrar)); + DesktopMultiWindowPlugin::RegisterWithRegistrar(flutter::PluginRegistrarManager::GetInstance()->GetRegistrar(registrar)); } diff --git a/packages/desktop_multi_window/windows/flutter_window.cc b/packages/desktop_multi_window/windows/flutter_window.cc index 4cb0501d..56d0aa68 100644 --- a/packages/desktop_multi_window/windows/flutter_window.cc +++ b/packages/desktop_multi_window/windows/flutter_window.cc @@ -4,8 +4,6 @@ #include "flutter_window.h" -#include "flutter_windows.h" - #include "tchar.h" #include @@ -16,90 +14,106 @@ namespace { -WindowCreatedCallback _g_window_created_callback = nullptr; + WindowCreatedCallback _g_window_created_callback = nullptr; -TCHAR kFlutterWindowClassName[] = _T("FlutterMultiWindow"); + TCHAR kFlutterWindowClassName[] = _T("FlutterMultiWindow"); -int32_t class_registered_ = 0; + int32_t class_registered_ = 0; -void RegisterWindowClass(WNDPROC wnd_proc) { - if (class_registered_ == 0) { - WNDCLASS window_class{}; - window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); - window_class.lpszClassName = kFlutterWindowClassName; - window_class.style = CS_HREDRAW | CS_VREDRAW; - window_class.cbClsExtra = 0; - window_class.cbWndExtra = 0; - window_class.hInstance = GetModuleHandle(nullptr); - window_class.hIcon = + void RegisterWindowClass(WNDPROC wnd_proc) { + if (class_registered_ == 0) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kFlutterWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = LoadIcon(window_class.hInstance, IDI_APPLICATION); - window_class.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1); - window_class.lpszMenuName = nullptr; - window_class.lpfnWndProc = wnd_proc; - RegisterClass(&window_class); + // window_class.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + window_class.hbrBackground = NULL; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = wnd_proc; + RegisterClass(&window_class); + } + class_registered_++; } - class_registered_++; -} -void UnregisterWindowClass() { - class_registered_--; - if (class_registered_ != 0) { - return; + void UnregisterWindowClass() { + class_registered_--; + if (class_registered_ != 0) { + return; + } + UnregisterClass(kFlutterWindowClassName, nullptr); } - UnregisterClass(kFlutterWindowClassName, nullptr); -} -// Scale helper to convert logical scaler values to physical using passed in -// scale factor -inline int Scale(int source, double scale_factor) { - return static_cast(source * scale_factor); -} + // Scale helper to convert logical scaler values to physical using passed in + // scale factor + inline int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); + } -using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); -// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. -// This API is only needed for PerMonitor V1 awareness mode. -void EnableFullDpiSupportIfAvailable(HWND hwnd) { - HMODULE user32_module = LoadLibraryA("User32.dll"); - if (!user32_module) { - return; - } - auto enable_non_client_dpi_scaling = - reinterpret_cast( - GetProcAddress(user32_module, "EnableNonClientDpiScaling")); - if (enable_non_client_dpi_scaling != nullptr) { - enable_non_client_dpi_scaling(hwnd); - FreeLibrary(user32_module); + // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. + // This API is only needed for PerMonitor V1 awareness mode. + void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } } } -} - FlutterWindow::FlutterWindow( - int64_t id, - std::string args, - const std::shared_ptr &callback -) : callback_(callback), id_(id), window_handle_(nullptr), scale_factor_(1) { + int64_t id, + std::string args, + const std::shared_ptr& callback, + WindowOptions options +) { + callback_ = callback; + id_ = id; + window_handle_ = nullptr; + scale_factor_ = 1; + RegisterWindowClass(FlutterWindow::WndProc); - const POINT target_point = {static_cast(10), - static_cast(10)}; + const POINT target_point = { static_cast(10), + static_cast(10) }; HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); scale_factor_ = dpi / 96.0; - HWND window_handle = CreateWindow( - kFlutterWindowClassName, L"", WS_OVERLAPPEDWINDOW, - Scale(target_point.x, scale_factor_), Scale(target_point.y, scale_factor_), - Scale(1280, scale_factor_), Scale(720, scale_factor_), - nullptr, nullptr, GetModuleHandle(nullptr), this); + // Create the window using CreateWindowEx with the provided extended style. + // If no extended style is desired, options.exStyle can be 0. + HWND window_handle = CreateWindowEx( + options.exStyle, // Extended window style + kFlutterWindowClassName, // Window class name + options.title.c_str(), // Window title (wide string) + options.style, // Window style + Scale(options.left, scale_factor_), + Scale(options.top, scale_factor_), + Scale(options.width, scale_factor_), + Scale(options.height, scale_factor_), + nullptr, // Parent window handle + nullptr, // Menu handle + GetModuleHandle(nullptr), // Instance handle + this); // Additional application data RECT frame; GetClientRect(window_handle, &frame); flutter::DartProject project(L"data"); - project.set_dart_entrypoint_arguments({"multi_window", std::to_string(id), std::move(args)}); + project.set_dart_entrypoint_arguments({ "multi_window", std::to_string(id), std::move(args) }); flutter_controller_ = std::make_unique( - frame.right - frame.left, frame.bottom - frame.top, project); + frame.right - frame.left, frame.bottom - frame.top, project); // Ensure that basic setup of the controller was successful. if (!flutter_controller_->engine() || !flutter_controller_->view()) { std::cerr << "Failed to setup FlutterViewController." << std::endl; @@ -108,121 +122,45 @@ FlutterWindow::FlutterWindow( SetParent(view_handle, window_handle); MoveWindow(view_handle, 0, 0, frame.right - frame.left, frame.bottom - frame.top, true); - InternalMultiWindowPluginRegisterWithRegistrar( - flutter_controller_->engine()->GetRegistrarForPlugin("DesktopMultiWindowPlugin")); - window_channel_ = WindowChannel::RegisterWithRegistrar( - flutter_controller_->engine()->GetRegistrarForPlugin("DesktopMultiWindowPlugin"), id_); + auto registrar_plugin = flutter_controller_->engine()->GetRegistrarForPlugin("DesktopMultiWindowPlugin"); + registrar_ = flutter::PluginRegistrarManager::GetInstance()->GetRegistrar(registrar_plugin); + + InternalMultiWindowPluginRegisterWithRegistrar(registrar_plugin); + inter_window_event_channel_ = InterWindowEventChannel::RegisterWithRegistrar(registrar_plugin, id_); + window_events_channel_ = WindowEventsChannel::RegisterWithRegistrar(registrar_plugin); if (_g_window_created_callback) { _g_window_created_callback(flutter_controller_.get()); } + SetBackgroundColor(options.backgroundColor); + // hide the window when created. ShowWindow(window_handle, SW_HIDE); - } // static -FlutterWindow *FlutterWindow::GetThisFromHandle(HWND window) noexcept { - return reinterpret_cast( - GetWindowLongPtr(window, GWLP_USERDATA)); +FlutterWindow* FlutterWindow::GetThisFromHandle(HWND window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); } // static LRESULT CALLBACK FlutterWindow::WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) { if (message == WM_NCCREATE) { - auto window_struct = reinterpret_cast(lparam); + auto window_struct = reinterpret_cast(lparam); SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast(window_struct->lpCreateParams)); - auto that = static_cast(window_struct->lpCreateParams); + auto that = static_cast(window_struct->lpCreateParams); EnableFullDpiSupportIfAvailable(window); that->window_handle_ = window; - } else if (FlutterWindow *that = GetThisFromHandle(window)) { + } else if (FlutterWindow* that = GetThisFromHandle(window)) { return that->MessageHandler(window, message, wparam, lparam); } return DefWindowProc(window, message, wparam, lparam); } -LRESULT FlutterWindow::MessageHandler(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { - - // Give Flutter, including plugins, an opportunity to handle window messages. - if (flutter_controller_) { - std::optional result = flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, lparam); - if (result) { - return *result; - } - } - - auto child_content_ = flutter_controller_ ? flutter_controller_->view()->GetNativeWindow() : nullptr; - - switch (message) { - case WM_FONTCHANGE: { - flutter_controller_->engine()->ReloadSystemFonts(); - break; - } - case WM_DESTROY: { - Destroy(); - if (!destroyed_) { - destroyed_ = true; - if (auto callback = callback_.lock()) { - callback->OnWindowDestroy(id_); - } - } - return 0; - } - case WM_CLOSE: { - if (auto callback = callback_.lock()) { - callback->OnWindowClose(id_); - } - break; - } - case WM_DPICHANGED: { - auto newRectSize = reinterpret_cast(lparam); - LONG newWidth = newRectSize->right - newRectSize->left; - LONG newHeight = newRectSize->bottom - newRectSize->top; - - SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, - newHeight, SWP_NOZORDER | SWP_NOACTIVATE); - - return 0; - } - case WM_SIZE: { - RECT rect; - GetClientRect(window_handle_, &rect); - if (child_content_ != nullptr) { - // Size and position the child window. - MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, - rect.bottom - rect.top, TRUE); - } - return 0; - } - - case WM_ACTIVATE: { - if (child_content_ != nullptr) { - SetFocus(child_content_); - } - return 0; - } - default: break; - } - - return DefWindowProc(window_handle_, message, wparam, lparam); -} - -void FlutterWindow::Destroy() { - if (window_channel_) { - window_channel_ = nullptr; - } - if (flutter_controller_) { - flutter_controller_ = nullptr; - } - if (window_handle_) { - DestroyWindow(window_handle_); - window_handle_ = nullptr; - } -} - FlutterWindow::~FlutterWindow() { if (window_handle_) { std::cout << "window_handle leak." << std::endl; @@ -232,4 +170,4 @@ FlutterWindow::~FlutterWindow() { void DesktopMultiWindowSetWindowCreatedCallback(WindowCreatedCallback callback) { _g_window_created_callback = callback; -} \ No newline at end of file +} diff --git a/packages/desktop_multi_window/windows/flutter_window.h b/packages/desktop_multi_window/windows/flutter_window.h index 7101eaf2..1cb98cc8 100644 --- a/packages/desktop_multi_window/windows/flutter_window.h +++ b/packages/desktop_multi_window/windows/flutter_window.h @@ -13,56 +13,30 @@ #include #include "base_flutter_window.h" -#include "window_channel.h" +#include "inter_window_event_channel.h" +#include "window_options.h" -class FlutterWindowCallback { - - public: - virtual void OnWindowClose(int64_t id) = 0; - - virtual void OnWindowDestroy(int64_t id) = 0; - -}; class FlutterWindow : public BaseFlutterWindow { - public: - - FlutterWindow(int64_t id, std::string args, const std::shared_ptr &callback); - ~FlutterWindow() override; - - WindowChannel *GetWindowChannel() override { - return window_channel_.get(); - } - - protected: +public: + FlutterWindow( + int64_t id, + std::string args, + const std::shared_ptr& callback, + WindowOptions options + ); - HWND GetWindowHandle() override { return window_handle_; } + ~FlutterWindow(); - private: - - std::weak_ptr callback_; - - HWND window_handle_; - - int64_t id_; - - // The Flutter instance hosted by this window. - std::unique_ptr flutter_controller_; - - std::unique_ptr window_channel_; +private: double scale_factor_; - bool destroyed_ = false; - static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); - static FlutterWindow *GetThisFromHandle(HWND window) noexcept; - - LRESULT MessageHandler(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam); + static FlutterWindow* GetThisFromHandle(HWND window) noexcept; - void Destroy(); }; -#endif //DESKTOP_MULTI_WINDOW_WINDOWS_FLUTTER_WINDOW_H_ +#endif // DESKTOP_MULTI_WINDOW_WINDOWS_FLUTTER_WINDOW_H_ diff --git a/packages/desktop_multi_window/windows/include/desktop_multi_window/desktop_multi_window_plugin.h b/packages/desktop_multi_window/windows/include/desktop_multi_window/desktop_multi_window_plugin.h index 24a6e04e..35ad6a12 100644 --- a/packages/desktop_multi_window/windows/include/desktop_multi_window/desktop_multi_window_plugin.h +++ b/packages/desktop_multi_window/windows/include/desktop_multi_window/desktop_multi_window_plugin.h @@ -10,18 +10,19 @@ #endif #if defined(__cplusplus) -extern "C" { +extern "C" +{ #endif -FLUTTER_PLUGIN_EXPORT void DesktopMultiWindowPluginRegisterWithRegistrar( - FlutterDesktopPluginRegistrarRef registrar); + FLUTTER_PLUGIN_EXPORT void DesktopMultiWindowPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); -// flutter_view_controller: pointer to the flutter::FlutterViewController -typedef void (*WindowCreatedCallback)(void *flutter_view_controller); -FLUTTER_PLUGIN_EXPORT void DesktopMultiWindowSetWindowCreatedCallback(WindowCreatedCallback callback); + // flutter_view_controller: pointer to the flutter::FlutterViewController + typedef void (*WindowCreatedCallback)(void* flutter_view_controller); + FLUTTER_PLUGIN_EXPORT void DesktopMultiWindowSetWindowCreatedCallback(WindowCreatedCallback callback); #if defined(__cplusplus) -} // extern "C" +} // extern "C" #endif -#endif // FLUTTER_PLUGIN_DESKTOP_MULTI_WINDOW_PLUGIN_H_ +#endif // FLUTTER_PLUGIN_DESKTOP_MULTI_WINDOW_PLUGIN_H_ diff --git a/packages/desktop_multi_window/windows/inter_window_event_channel.cc b/packages/desktop_multi_window/windows/inter_window_event_channel.cc new file mode 100644 index 00000000..f3a68feb --- /dev/null +++ b/packages/desktop_multi_window/windows/inter_window_event_channel.cc @@ -0,0 +1,48 @@ +// +// Created by yangbin on 2022/1/27. +// + +#include "inter_window_event_channel.h" +#include "flutter/standard_method_codec.h" + +#include + +std::unique_ptr +InterWindowEventChannel::RegisterWithRegistrar(FlutterDesktopPluginRegistrarRef registrar, int64_t window_id) { + auto window_registrar = flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar); + auto channel = std::make_unique>( + window_registrar->messenger(), "mixin.one/flutter_multi_window_inter_window_event_channel", + &flutter::StandardMethodCodec::GetInstance()); + return std::make_unique(window_id, std::move(channel)); +} + +InterWindowEventChannel::InterWindowEventChannel( + int64_t window_id, + std::unique_ptr> channel) : window_id_(window_id), channel_(std::move(channel)) { + channel_->SetMethodCallHandler([this](const flutter::MethodCall& call, auto result) + { + if (!handler_) { + std::cout << "InterWindowEventChannel::SetMethodCallHandler: handler_ is null" << std::endl; + return; + } + auto* args = std::get_if(call.arguments()); + auto target_window_id = args->at(flutter::EncodableValue("targetWindowId")).LongValue(); + auto arguments = args->at(flutter::EncodableValue("arguments")); + handler_(window_id_, target_window_id, call.method_name(), &arguments, std::move(result)); }); +} + +InterWindowEventChannel::~InterWindowEventChannel() { + channel_->SetMethodCallHandler(nullptr); +} + +void InterWindowEventChannel::InvokeMethod( + int64_t from_window_id, const std::string& method, + InterWindowEventChannel::Argument* arguments, + std::unique_ptr> result) { + channel_->InvokeMethod(method, std::make_unique(flutter::EncodableMap{ + {flutter::EncodableValue("fromWindowId"), flutter::EncodableValue(from_window_id)}, + {flutter::EncodableValue("arguments"), *arguments}, + }), + std::move(result)); +} diff --git a/packages/desktop_multi_window/windows/inter_window_event_channel.h b/packages/desktop_multi_window/windows/inter_window_event_channel.h new file mode 100644 index 00000000..a0198d28 --- /dev/null +++ b/packages/desktop_multi_window/windows/inter_window_event_channel.h @@ -0,0 +1,53 @@ +// +// Created by yangbin on 2022/1/27. +// + +#ifndef DESKTOP_MULTI_WINDOW_INTER_WINDOW_EVENT_CHANNEL_H +#define DESKTOP_MULTI_WINDOW_INTER_WINDOW_EVENT_CHANNEL_H + +#include +#include + +#include "flutter/event_channel.h" +#include "flutter/plugin_registrar.h" +#include "flutter/plugin_registrar_windows.h" +#include "flutter/method_channel.h" +#include "flutter/encodable_value.h" + +class InterWindowEventChannel : public flutter::Plugin { + +public: + using Argument = flutter::EncodableValue; + + static std::unique_ptr RegisterWithRegistrar(FlutterDesktopPluginRegistrarRef registrar, int64_t window_id); + + InterWindowEventChannel(int64_t window_id, std::unique_ptr> channel); + + ~InterWindowEventChannel() override; + + void InvokeMethod( + int64_t from_window_id, + const std::string& method, + Argument* arguments, + std::unique_ptr> result = nullptr); + + using MethodCallHandler = std::function> result)>; + + void SetMethodCallHandler(MethodCallHandler handler) { + handler_ = std::move(handler); + } + +private: + int64_t window_id_; + + MethodCallHandler handler_; + + std::unique_ptr> channel_; +}; + +#endif // DESKTOP_MULTI_WINDOW_INTER_WINDOW_EVENT_CHANNEL_H diff --git a/packages/desktop_multi_window/windows/multi_window_manager.cc b/packages/desktop_multi_window/windows/multi_window_manager.cc index e141173a..885dcde0 100644 --- a/packages/desktop_multi_window/windows/multi_window_manager.cc +++ b/packages/desktop_multi_window/windows/multi_window_manager.cc @@ -2,92 +2,110 @@ // Created by yangbin on 2022/1/11. // -#include "multi_window_manager.h" - #include +#include +#include -#include "flutter_window.h" +#include "inter_window_event_channel.h" +#include "multi_window_manager.h" namespace { -int64_t g_next_id_ = 0; - -class FlutterMainWindow : public BaseFlutterWindow { - - public: - - FlutterMainWindow(HWND hwnd, std::unique_ptr window_channel) - : hwnd_(hwnd), channel_(std::move(window_channel)) { - - } - - ~FlutterMainWindow() override = default; - - WindowChannel *GetWindowChannel() override { - return channel_.get(); - } - - protected: - - HWND GetWindowHandle() override { - return hwnd_; - } - - private: - - HWND hwnd_; - - std::unique_ptr channel_; - + int64_t g_next_id_ = 0; + std::shared_mutex windows_mutex_; + + + class FlutterMainWindow : public BaseFlutterWindow { + public: + FlutterMainWindow(HWND hwnd, + const std::shared_ptr& callback, + std::unique_ptr inter_window_event_channel, + std::unique_ptr window_events_channel, + flutter::PluginRegistrarWindows* registrar) { + window_handle_ = hwnd; + id_ = 0; + callback_ = callback; + inter_window_event_channel_ = std::move(inter_window_event_channel); + window_events_channel_ = std::move(window_events_channel); + registrar_ = registrar; + window_proc_id = registrar->RegisterTopLevelWindowProcDelegate( + [this](HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { + return HandleWindowProc(hWnd, message, wParam, lParam); + }); + } + }; }; -} - // static -MultiWindowManager *MultiWindowManager::Instance() { +MultiWindowManager* MultiWindowManager::Instance() { static auto manager = std::make_shared(); return manager.get(); } -MultiWindowManager::MultiWindowManager() : windows_() { +MultiWindowManager::MultiWindowManager() : windows_() {} +MultiWindowManager::~MultiWindowManager() { + if (mouse_hook_) { + UnhookWindowsHookEx(mouse_hook_); + mouse_hook_ = nullptr; + } } -int64_t MultiWindowManager::Create(std::string args) { +int64_t MultiWindowManager::Create(std::string args, WindowOptions options) { + std::unique_lock lock(windows_mutex_); g_next_id_++; int64_t id = g_next_id_; - auto window = std::make_unique(id, std::move(args), shared_from_this()); - auto channel = window->GetWindowChannel(); + auto window = std::make_unique(id, std::move(args), shared_from_this(), options); + auto channel = window->GetInterWindowEventChannel(); channel->SetMethodCallHandler([this](int64_t from_window_id, - int64_t target_window_id, - const std::string &call, - flutter::EncodableValue *arguments, - std::unique_ptr> result) { - HandleWindowChannelCall(from_window_id, target_window_id, call, arguments, std::move(result)); - }); + int64_t target_window_id, + const std::string& call, + flutter::EncodableValue* arguments, + std::unique_ptr> result) + { HandleWindowChannelCall(from_window_id, target_window_id, call, arguments, std::move(result)); }); windows_[id] = std::move(window); return id; } void MultiWindowManager::AttachFlutterMainWindow( - HWND main_window_handle, - std::unique_ptr window_channel) { + HWND main_window_handle, + std::unique_ptr inter_window_event_channel, + std::unique_ptr window_events_channel, + flutter::PluginRegistrarWindows* registrar) { + std::unique_lock lock(windows_mutex_); if (windows_.count(0) != 0) { std::cout << "Error: main window already exists" << std::endl; return; } - window_channel->SetMethodCallHandler( - [this](int64_t from_window_id, - int64_t target_window_id, - const std::string &call, - flutter::EncodableValue *arguments, - std::unique_ptr> result) { - HandleWindowChannelCall(from_window_id, target_window_id, call, arguments, std::move(result)); - }); - windows_[0] = std::make_unique(main_window_handle, std::move(window_channel)); + inter_window_event_channel->SetMethodCallHandler( + [this](int64_t from_window_id, + int64_t target_window_id, + const std::string& call, + flutter::EncodableValue* arguments, + std::unique_ptr> result) { + HandleWindowChannelCall(from_window_id, target_window_id, call, arguments, std::move(result)); + }); + auto main_window = std::make_unique( + main_window_handle, + shared_from_this(), + std::move(inter_window_event_channel), + std::move(window_events_channel), + registrar + ); + windows_[0] = std::move(main_window); + mouse_hook_ = SetWindowsHookEx(WH_MOUSE_LL, MouseProc, GetModuleHandle(NULL), 0); +} + +void MultiWindowManager::SetHasListeners(int64_t id, bool has_listeners) { + std::shared_lock lock(windows_mutex_); + auto window = windows_.find(id); + if (window != windows_.end()) { + window->second->SetHasListeners(has_listeners); + } } void MultiWindowManager::Show(int64_t id) { + std::shared_lock lock(windows_mutex_); auto window = windows_.find(id); if (window != windows_.end()) { window->second->Show(); @@ -95,6 +113,7 @@ void MultiWindowManager::Show(int64_t id) { } void MultiWindowManager::Hide(int64_t id) { + std::shared_lock lock(windows_mutex_); auto window = windows_.find(id); if (window != windows_.end()) { window->second->Hide(); @@ -102,20 +121,43 @@ void MultiWindowManager::Hide(int64_t id) { } void MultiWindowManager::Close(int64_t id) { + std::shared_lock lock(windows_mutex_); auto window = windows_.find(id); if (window != windows_.end()) { window->second->Close(); } } -void MultiWindowManager::SetFrame(int64_t id, double x, double y, double width, double height) { +void MultiWindowManager::SetFrame(int64_t id, double_t left, double_t top, double_t width, double_t height, UINT flags) { + std::shared_lock lock(windows_mutex_); + auto window = windows_.find(id); + if (window != windows_.end()) { + window->second->SetFrame(left, top, width, height, flags); + } +} + +flutter::EncodableMap MultiWindowManager::GetFrame(int64_t id, double_t devicePixelRatio) { + std::shared_lock lock(windows_mutex_); + flutter::EncodableMap resultMap = flutter::EncodableMap(); auto window = windows_.find(id); if (window != windows_.end()) { - window->second->SetBounds(x, y, width, height); + auto rect = window->second->GetFrame(); + double x = rect.left / devicePixelRatio * 1.0f; + double y = rect.top / devicePixelRatio * 1.0f; + double width = (rect.right - rect.left) / devicePixelRatio * 1.0f; + double height = (rect.bottom - rect.top) / devicePixelRatio * 1.0f; + + resultMap[flutter::EncodableValue("left")] = flutter::EncodableValue(x); + resultMap[flutter::EncodableValue("top")] = flutter::EncodableValue(y); + resultMap[flutter::EncodableValue("width")] = flutter::EncodableValue(width); + resultMap[flutter::EncodableValue("height")] = flutter::EncodableValue(height); } + return resultMap; } -void MultiWindowManager::SetTitle(int64_t id, const std::string &title) { + +void MultiWindowManager::SetTitle(int64_t id, const std::string& title) { + std::shared_lock lock(windows_mutex_); auto window = windows_.find(id); if (window != windows_.end()) { window->second->SetTitle(title); @@ -123,15 +165,125 @@ void MultiWindowManager::SetTitle(int64_t id, const std::string &title) { } void MultiWindowManager::Center(int64_t id) { + std::shared_lock lock(windows_mutex_); auto window = windows_.find(id); if (window != windows_.end()) { window->second->Center(); } } +bool MultiWindowManager::IsFocused(int64_t id) { + std::shared_lock lock(windows_mutex_); + auto window = windows_.find(id); + if (window != windows_.end()) { + return window->second->IsFocused(); + } + return false; +} + +bool MultiWindowManager::IsFullScreen(int64_t id) { + std::shared_lock lock(windows_mutex_); + auto window = windows_.find(id); + if (window != windows_.end()) { + return window->second->IsFullScreen(); + } + return false; +} + +bool MultiWindowManager::IsMaximized(int64_t id) { + std::shared_lock lock(windows_mutex_); + auto window = windows_.find(id); + if (window != windows_.end()) { + return window->second->IsMaximized(); + } + return false; +} + +bool MultiWindowManager::IsMinimized(int64_t id) { + std::shared_lock lock(windows_mutex_); + auto window = windows_.find(id); + if (window != windows_.end()) { + return window->second->IsMinimized(); + } return false; +} + +bool MultiWindowManager::IsVisible(int64_t id) { + std::shared_lock lock(windows_mutex_); + auto window = windows_.find(id); + if (window != windows_.end()) { + return window->second->IsVisible(); + } + return false; +} + +void MultiWindowManager::Maximize(int64_t id, bool vertically) { + std::shared_lock lock(windows_mutex_); + auto window = windows_.find(id); + if (window != windows_.end()) { + window->second->Maximize(vertically); + } +} + +void MultiWindowManager::Unmaximize(int64_t id) { + std::shared_lock lock(windows_mutex_); + auto window = windows_.find(id); + if (window != windows_.end()) { + window->second->Unmaximize(); + } +} + +void MultiWindowManager::Minimize(int64_t id) { + std::shared_lock lock(windows_mutex_); + auto window = windows_.find(id); + if (window != windows_.end()) { + window->second->Minimize(); + } +} + +void MultiWindowManager::Restore(int64_t id) { + std::shared_lock lock(windows_mutex_); + auto window = windows_.find(id); + if (window != windows_.end()) { + window->second->Restore(); + } +} + +void MultiWindowManager::SetFullScreen(int64_t id, bool is_full_screen) { + std::shared_lock lock(windows_mutex_); + auto window = windows_.find(id); + if (window != windows_.end()) { + window->second->SetFullScreen(is_full_screen); + } +} + +void MultiWindowManager::SetStyle(int64_t id, int32_t style, int32_t extended_style) { + std::shared_lock lock(windows_mutex_); + auto window = windows_.find(id); + if (window != windows_.end()) { + window->second->SetStyle(style, extended_style); + } +} + +void MultiWindowManager::SetBackgroundColor(int64_t id, Color backgroundColor) { + std::shared_lock lock(windows_mutex_); + auto window = windows_.find(id); + if (window != windows_.end()) { + window->second->SetBackgroundColor(backgroundColor); + } +} + +void MultiWindowManager::SetIgnoreMouseEvents(int64_t id, bool ignore) { + std::shared_lock lock(windows_mutex_); + auto window = windows_.find(id); + if (window != windows_.end()) { + window->second->SetIgnoreMouseEvents(ignore); + } +} + flutter::EncodableList MultiWindowManager::GetAllSubWindowIds() { + std::shared_lock lock(windows_mutex_); flutter::EncodableList resList = flutter::EncodableList(); - for (auto &window : windows_) { + for (auto& window : windows_) { if (window.first != 0) { resList.push_back(flutter::EncodableValue(window.first)); } @@ -139,26 +291,31 @@ flutter::EncodableList MultiWindowManager::GetAllSubWindowIds() { return resList; } -void MultiWindowManager::OnWindowClose(int64_t id) { -} +void MultiWindowManager::OnWindowClose(int64_t id) {} void MultiWindowManager::OnWindowDestroy(int64_t id) { - windows_.erase(id); + std::thread([this, id]() { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::unique_lock lock(windows_mutex_); + if (windows_.find(id) != windows_.end()) { + windows_.erase(id); + } + }).detach(); } void MultiWindowManager::HandleWindowChannelCall( - int64_t from_window_id, - int64_t target_window_id, - const std::string &call, - flutter::EncodableValue *arguments, - std::unique_ptr> result -) { + int64_t from_window_id, + int64_t target_window_id, + const std::string& call, + flutter::EncodableValue* arguments, + std::unique_ptr> result) { + std::shared_lock lock(windows_mutex_); auto target_window_entry = windows_.find(target_window_id); if (target_window_entry == windows_.end()) { result->Error("-1", "target window not found."); return; } - auto target_window_channel = target_window_entry->second->GetWindowChannel(); + auto target_window_channel = target_window_entry->second->GetInterWindowEventChannel(); if (!target_window_channel) { result->Error("-1", "target window channel not found."); return; @@ -166,3 +323,48 @@ void MultiWindowManager::HandleWindowChannelCall( target_window_channel->InvokeMethod(from_window_id, call, arguments, std::move(result)); } + + +LRESULT CALLBACK MultiWindowManager::MouseProc(int nCode, WPARAM wParam, LPARAM lParam) { + if (nCode < 0) { + return CallNextHookEx(NULL, nCode, wParam, lParam); + } + + auto* manager = MultiWindowManager::Instance(); + if (!manager) { + return CallNextHookEx(NULL, nCode, wParam, lParam); + } + + MSLLHOOKSTRUCT* hookStruct = (MSLLHOOKSTRUCT*)lParam; + + auto coordinates = std::make_shared(); + (*coordinates)[flutter::EncodableValue("x")] = flutter::EncodableValue(static_cast(hookStruct->pt.x)); + (*coordinates)[flutter::EncodableValue("y")] = flutter::EncodableValue(static_cast(hookStruct->pt.y)); + + auto args = std::make_shared(); + (*args)[flutter::EncodableValue("eventName")] = flutter::EncodableValue("mouse-move"); + (*args)[flutter::EncodableValue("eventData")] = flutter::EncodableValue(*coordinates); + + auto shared_args = std::make_shared>(args); + + std::vector windowHandles; + { + windowHandles.reserve(manager->windows_.size()); + for (const auto& window : manager->windows_) { + if (auto handle = window.second->GetRootWindowHandle()) { + if (IsWindow(handle) && window.second->has_listeners_) { + windowHandles.push_back(handle); + } + } + } + } + + for (HWND handle : windowHandles) { + if (IsWindow(handle)) { + PostMessage(handle, WM_USER + 37, + reinterpret_cast(new std::shared_ptr(*shared_args)), 0); + } + } + + return CallNextHookEx(NULL, nCode, wParam, lParam); +} \ No newline at end of file diff --git a/packages/desktop_multi_window/windows/multi_window_manager.h b/packages/desktop_multi_window/windows/multi_window_manager.h index 63bfdbb2..c76fc29d 100644 --- a/packages/desktop_multi_window/windows/multi_window_manager.h +++ b/packages/desktop_multi_window/windows/multi_window_manager.h @@ -8,20 +8,35 @@ #include #include #include +#include + #include "base_flutter_window.h" #include "flutter_window.h" +#include "utils.h" + -class MultiWindowManager : public std::enable_shared_from_this, public FlutterWindowCallback { +class MultiWindowManager : public std::enable_shared_from_this, public BaseFlutterWindowCallback { - public: - static MultiWindowManager *Instance(); +public: + static MultiWindowManager* Instance(); MultiWindowManager(); + ~MultiWindowManager(); + + int64_t Create( + std::string args, + WindowOptions options + ); - int64_t Create(std::string args); + void AttachFlutterMainWindow( + HWND main_window_handle, + std::unique_ptr inter_window_event_channel, + std::unique_ptr window_events_channel, + flutter::PluginRegistrarWindows* registrar + ); - void AttachFlutterMainWindow(HWND main_window_handle, std::unique_ptr window_channel); + void SetHasListeners(int64_t id, bool has_listeners); void Show(int64_t id); @@ -29,11 +44,39 @@ class MultiWindowManager : public std::enable_shared_from_this> windows_; + HHOOK mouse_hook_ = nullptr; + void HandleWindowChannelCall( - int64_t from_window_id, - int64_t target_window_id, - const std::string &call, - flutter::EncodableValue *arguments, - std::unique_ptr> result - ); + int64_t from_window_id, + int64_t target_window_id, + const std::string& call, + flutter::EncodableValue* arguments, + std::unique_ptr> result); + static LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam); }; -#endif //DESKTOP_MULTI_WINDOW_WINDOWS_MULTI_WINDOW_MANAGER_H_ +#endif // DESKTOP_MULTI_WINDOW_WINDOWS_MULTI_WINDOW_MANAGER_H_ diff --git a/packages/desktop_multi_window/windows/multi_window_plugin_internal.h b/packages/desktop_multi_window/windows/multi_window_plugin_internal.h index 939b7baa..91daac8f 100644 --- a/packages/desktop_multi_window/windows/multi_window_plugin_internal.h +++ b/packages/desktop_multi_window/windows/multi_window_plugin_internal.h @@ -9,4 +9,4 @@ void InternalMultiWindowPluginRegisterWithRegistrar(FlutterDesktopPluginRegistrarRef registrar); -#endif //DESKTOP_MULTI_WINDOW_WINDOWS_MULTI_WINDOW_PLUGIN_INTERNAL_H_ +#endif // DESKTOP_MULTI_WINDOW_WINDOWS_MULTI_WINDOW_PLUGIN_INTERNAL_H_ diff --git a/packages/desktop_multi_window/windows/utils.h b/packages/desktop_multi_window/windows/utils.h new file mode 100644 index 00000000..b17c3c41 --- /dev/null +++ b/packages/desktop_multi_window/windows/utils.h @@ -0,0 +1,65 @@ + + +#ifndef DESKTOP_MULTI_WINDOW_UTILS_H +#define DESKTOP_MULTI_WINDOW_UTILS_H + +#include +#include +#include + +inline int64_t GetIntegerValue(const flutter::EncodableValue& value) { + if (std::holds_alternative(value)) { + return std::get(value); + } else if (std::holds_alternative(value)) { + return std::get(value); + } + throw std::runtime_error("Value is not an integer"); +} + +inline const flutter::EncodableValue* ValueOrNull(const flutter::EncodableMap& map, const char* key) { + auto it = map.find(flutter::EncodableValue(key)); + if (it == map.end()) { + return nullptr; + } + return &(it->second); +} + +inline void PrintEncodableValue(const flutter::EncodableValue& value, int indent = 0) { + std::string indentStr(indent, ' '); + std::visit([&](auto&& arg) + { + using T = std::decay_t; + if constexpr (std::is_same_v) { + std::cout << indentStr << "null"; + } else if constexpr (std::is_same_v) { + std::cout << indentStr << (arg ? "true" : "false"); + } else if constexpr (std::is_same_v) { + std::cout << indentStr << arg; + } else if constexpr (std::is_same_v) { + std::cout << indentStr << arg; + } else if constexpr (std::is_same_v) { + std::cout << indentStr << arg; + } else if constexpr (std::is_same_v) { + std::cout << indentStr << "\"" << arg << "\""; + } else if constexpr (std::is_same_v) { + std::cout << indentStr << "[\n"; + for (const auto& elem : arg) { + PrintEncodableValue(elem, indent + 2); + std::cout << "\n"; + } + std::cout << indentStr << "]"; + } else if constexpr (std::is_same_v) { + std::cout << indentStr << "{\n"; + for (const auto& pair : arg) { + PrintEncodableValue(pair.first, indent + 2); + std::cout << ": "; + PrintEncodableValue(pair.second, indent + 2); + std::cout << "\n"; + } + std::cout << indentStr << "}"; + } else { + std::cout << indentStr << "Unknown type"; + } }, value); +} + +#endif // DESKTOP_MULTI_WINDOW_UTILS_H diff --git a/packages/desktop_multi_window/windows/window_channel.cc b/packages/desktop_multi_window/windows/window_channel.cc deleted file mode 100644 index f224ac2f..00000000 --- a/packages/desktop_multi_window/windows/window_channel.cc +++ /dev/null @@ -1,52 +0,0 @@ -// -// Created by yangbin on 2022/1/27. -// - -#include "window_channel.h" -#include "flutter/standard_method_codec.h" - -#include - -std::unique_ptr -WindowChannel::RegisterWithRegistrar(FlutterDesktopPluginRegistrarRef registrar, int64_t window_id) { - auto window_registrar = flutter::PluginRegistrarManager::GetInstance() - ->GetRegistrar(registrar); - auto channel = std::make_unique>( - window_registrar->messenger(), "mixin.one/flutter_multi_window_channel", - &flutter::StandardMethodCodec::GetInstance()); - return std::make_unique(window_id, std::move(channel)); -} - -WindowChannel::WindowChannel( - int64_t window_id, - std::unique_ptr> channel -) : window_id_(window_id), channel_(std::move(channel)) { - channel_->SetMethodCallHandler([this](const flutter::MethodCall &call, auto result) { - if (!handler_) { - std::cout << "WindowChannel::SetMethodCallHandler: handler_ is null" << std::endl; - return; - } - auto *args = std::get_if(call.arguments()); - auto target_window_id = args->at(flutter::EncodableValue("targetWindowId")).LongValue(); - auto arguments = args->at(flutter::EncodableValue("arguments")); - handler_(window_id_, target_window_id, call.method_name(), &arguments, std::move(result)); - }); -} - -WindowChannel::~WindowChannel() { - channel_->SetMethodCallHandler(nullptr); -} - -void WindowChannel::InvokeMethod( - int64_t from_window_id, const std::string &method, - WindowChannel::Argument *arguments, - std::unique_ptr> result -) { - channel_->InvokeMethod(method, std::make_unique( - flutter::EncodableMap{ - {flutter::EncodableValue("fromWindowId"), flutter::EncodableValue(from_window_id)}, - {flutter::EncodableValue("arguments"), *arguments}, - } - ), std::move(result)); -} - diff --git a/packages/desktop_multi_window/windows/window_channel.h b/packages/desktop_multi_window/windows/window_channel.h deleted file mode 100644 index 3d1c2609..00000000 --- a/packages/desktop_multi_window/windows/window_channel.h +++ /dev/null @@ -1,58 +0,0 @@ -// -// Created by yangbin on 2022/1/27. -// - -#ifndef DESKTOP_MULTI_WINDOW_WINDOW_CHANNEL_H -#define DESKTOP_MULTI_WINDOW_WINDOW_CHANNEL_H - -#include -#include - -#include "flutter/event_channel.h" -#include "flutter/plugin_registrar.h" -#include "flutter/plugin_registrar_windows.h" -#include "flutter/method_channel.h" -#include "flutter/encodable_value.h" - -class WindowChannel : public flutter::Plugin { - -public: - - using Argument = flutter::EncodableValue; - - static std::unique_ptr - RegisterWithRegistrar(FlutterDesktopPluginRegistrarRef registrar, int64_t window_id); - - WindowChannel(int64_t window_id, std::unique_ptr> channel); - - ~WindowChannel() override; - - void InvokeMethod( - int64_t from_window_id, - const std::string &method, - Argument *arguments, - std::unique_ptr> result = nullptr - ); - - using MethodCallHandler = std::function> result)>; - - void SetMethodCallHandler(MethodCallHandler handler) { - handler_ = std::move(handler); - } - -private: - int64_t window_id_; - - MethodCallHandler handler_; - - std::unique_ptr> channel_; - -}; - - -#endif //DESKTOP_MULTI_WINDOW_WINDOW_CHANNEL_H diff --git a/packages/desktop_multi_window/windows/window_events_channel.cc b/packages/desktop_multi_window/windows/window_events_channel.cc new file mode 100644 index 00000000..e477d1cf --- /dev/null +++ b/packages/desktop_multi_window/windows/window_events_channel.cc @@ -0,0 +1,34 @@ +// +// Created by Konstantin Wachendorff on 2025/03/05. +// + +#include "window_events_channel.h" +#include "flutter/standard_method_codec.h" + +#include + +std::unique_ptr WindowEventsChannel::RegisterWithRegistrar(FlutterDesktopPluginRegistrarRef registrar) { + auto window_registrar = flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar); + auto channel = std::make_unique>( + window_registrar->messenger(), "mixin.one/flutter_multi_window_events_channel", + &flutter::StandardMethodCodec::GetInstance()); + return std::make_unique(std::move(channel)); +} + +WindowEventsChannel::WindowEventsChannel(std::unique_ptr> channel) : channel_(std::move(channel)) { + channel_->SetMethodCallHandler([this](const flutter::MethodCall& call, auto result) { + if (!handler_) { + std::cout << "WindowEventsChannel::SetMethodCallHandler: handler_ is null" << std::endl; + return; + } + auto* args = std::get_if(call.arguments()); + auto arguments = args->at(flutter::EncodableValue("arguments")); + handler_(call.method_name(), &arguments, std::move(result)); + }); +} + +WindowEventsChannel::~WindowEventsChannel() { + channel_->SetMethodCallHandler(nullptr); +} + diff --git a/packages/desktop_multi_window/windows/window_events_channel.h b/packages/desktop_multi_window/windows/window_events_channel.h new file mode 100644 index 00000000..e2816c6b --- /dev/null +++ b/packages/desktop_multi_window/windows/window_events_channel.h @@ -0,0 +1,39 @@ +// +// Created by Konstantin Wachendorff on 2025/03/05. +// + +#ifndef DESKTOP_MULTI_WINDOW_WINDOW_EVENTS_CHANNEL_H +#define DESKTOP_MULTI_WINDOW_WINDOW_EVENTS_CHANNEL_H + +#include +#include + +#include "flutter/event_channel.h" +#include "flutter/plugin_registrar.h" +#include "flutter/plugin_registrar_windows.h" +#include "flutter/method_channel.h" +#include "flutter/encodable_value.h" + +class WindowEventsChannel : public flutter::Plugin { + +public: + using Argument = flutter::EncodableValue; + + static std::unique_ptr RegisterWithRegistrar(FlutterDesktopPluginRegistrarRef registrar); + + WindowEventsChannel(std::unique_ptr> channel); + + ~WindowEventsChannel() override; + + using MethodCallHandler = std::function> result)>; + + void SetMethodCallHandler(MethodCallHandler handler) { + handler_ = std::move(handler); + } + + MethodCallHandler handler_; + + std::unique_ptr> channel_; +}; + +#endif // DESKTOP_MULTI_WINDOW_WINDOW_EVENTS_CHANNEL_H diff --git a/packages/desktop_multi_window/windows/window_options.h b/packages/desktop_multi_window/windows/window_options.h new file mode 100644 index 00000000..b1e5cd84 --- /dev/null +++ b/packages/desktop_multi_window/windows/window_options.h @@ -0,0 +1,188 @@ +// window_options.h +#pragma once +#include +#include +#include +#include +#include +#include +#include + +// Helper lambda to extract int values from the map. +auto getInt = [&](const flutter::EncodableMap& map, const std::string& key, int defaultValue) -> int { + auto iter = map.find(flutter::EncodableValue(key)); + if (iter != map.end()) { + return std::visit([&](auto&& arg) -> int { + using T = std::decay_t; + if constexpr (std::is_integral_v || std::is_floating_point_v) { + return static_cast(arg); + } else { + return defaultValue; + } }, iter->second); + } + return defaultValue; +}; + +auto getInt64 = [&](const flutter::EncodableMap& map, const std::string& key, int64_t defaultValue) -> int64_t { + auto iter = map.find(flutter::EncodableValue(key)); + if (iter != map.end()) { + return std::visit([&](auto&& arg) -> int64_t { + using T = std::decay_t; + if constexpr (std::is_integral_v || std::is_floating_point_v) { + return static_cast(arg); + } else { + return defaultValue; + } }, iter->second); + } + return defaultValue; +}; + +// Helper lambda to extract double values from the map. +auto getDouble = [&](const flutter::EncodableMap& map, const std::string& key, double defaultValue) -> double { + auto iter = map.find(flutter::EncodableValue(key)); + if (iter != map.end()) { + return std::visit([&](auto&& arg) -> double { + using T = std::decay_t; + if constexpr (std::is_integral_v || std::is_floating_point_v) { + return static_cast(arg); + } else { + return defaultValue; + } }, iter->second); + } + return defaultValue; +}; + +// Helper lambda to extract string values and convert them to std::wstring. +auto getString = [&](const flutter::EncodableMap& map, const std::string& key, const std::wstring& defaultValue) -> std::wstring { + auto iter = map.find(flutter::EncodableValue(key)); + if (iter != map.end() && std::holds_alternative(iter->second)) { + std::string str = std::get(iter->second); + return std::wstring(str.begin(), str.end()); + } + return defaultValue; +}; + +struct Color { + int r, g, b, a; + + // Default constructor (black, fully opaque) + Color() : r(0), g(0), b(0), a(255) {} + + // Constructor with values + Color(int red, int green, int blue, int alpha = 255) + : r(std::clamp(red, 0, 255)) + , g(std::clamp(green, 0, 255)) + , b(std::clamp(blue, 0, 255)) + , a(std::clamp(alpha, 0, 255)) { + } + + Color(double red, double green, double blue, double alpha = 1.0) + : r(std::clamp(static_cast(red * 255), 0, 255)) + , g(std::clamp(static_cast(green * 255), 0, 255)) + , b(std::clamp(static_cast(blue * 255), 0, 255)) + , a(std::clamp(static_cast(alpha * 255), 0, 255)) { + } + + // Create from COLORREF + static Color FromColorRef(COLORREF ref) { + return Color( + GetRValue(ref), + GetGValue(ref), + GetBValue(ref), + 255 + ); + } + + // Create from ARGB + static Color FromARGB(uint32_t argb) { + return Color( + (int)((argb >> 16) & 0xFF), + (int)((argb >> 8) & 0xFF), + (int)(argb & 0xFF), + (int)((argb >> 24) & 0xFF) + ); + } + + COLORREF toColorRef() const { + return RGB(r, g, b); + } + + uint32_t toARGB() const { + return (a << 24) | (r << 16) | (g << 8) | b; + } + + uint32_t toABGR() const { + return (a << 24) | (b << 16) | (g << 8) | r; + } + + // Check if color is transparent + bool isTransparent() const { + return a == 0; + } + + // Check if color is fully opaque + bool isOpaque() const { + return a == 255; + } + + static Color ParseColor(const flutter::EncodableMap& color_map) { + // Get color components with bounds checking + auto red = getDouble(color_map, "red", 0.5); + auto green = getDouble(color_map, "green", 0.5); + auto blue = getDouble(color_map, "blue", 0.5); + auto alpha = getDouble(color_map, "alpha", 1.0); + + return Color(red, green, blue, alpha); + } +}; + +struct WindowOptions +{ + std::wstring title; + int32_t style; + int32_t exStyle; + int left; + int top; + int width; + int height; + Color backgroundColor; + + WindowOptions() + : title(L""), style(WS_OVERLAPPEDWINDOW), exStyle(0), + left(10), top(10), width(1280), height(720) { + } + + // Parse the window options from a flutter::EncodableMap. + void Parse(const flutter::EncodableMap& windows_map) { + // auto styleW = GetIntegerValue(windows_map.at(flutter::EncodableValue("style"))); + // auto extended_styleW = GetIntegerValue(windows_map.at(flutter::EncodableValue("extendedStyle"))); + style = getInt(windows_map, "style", style); + exStyle = getInt(windows_map, "exStyle", exStyle); + left = getInt(windows_map, "left", left); + top = getInt(windows_map, "top", top); + width = getInt(windows_map, "width", width); + height = getInt(windows_map, "height", height); + title = getString(windows_map, "title", title); + try { + auto backgroundColorIter = windows_map.find(flutter::EncodableValue("backgroundColor")); + if (backgroundColorIter != windows_map.end() && + std::holds_alternative(backgroundColorIter->second)) { + const auto& backgroundColorMap = std::get(backgroundColorIter->second); + backgroundColor = Color::ParseColor(backgroundColorMap); + } + } catch (const std::exception& e) { + std::cerr << L"Error parsing background color: " << e.what() << std::endl; + } + } + + // Print function for debugging. + void Print() const { + std::wcout << L"WindowOptions:" << std::endl; + std::wcout << L" Title: " << title << std::endl; + std::wcout << L" Style: 0x" << std::hex << style << std::dec << std::endl; + std::wcout << L" ExStyle: 0x" << std::hex << exStyle << std::dec << std::endl; + std::wcout << L" Position: (" << left << L", " << top << L")" << std::endl; + std::wcout << L" Size: " << width << L" x " << height << std::endl; + std::wcout << L" Background Color: " << backgroundColor.toARGB() << std::endl; + } +}; \ No newline at end of file