App-thema’s, meerschermen, annotaties en grafiekslides #1
54 changed files with 4466 additions and 15 deletions
|
|
@ -1860,8 +1860,7 @@ const _dutchSourceStrings = {
|
||||||
'Vrije Markdown': 'Frije Markdown',
|
'Vrije Markdown': 'Frije Markdown',
|
||||||
'Broncode': 'Boarnekoade',
|
'Broncode': 'Boarnekoade',
|
||||||
'Programmeertaal': 'Programmeartaal',
|
'Programmeertaal': 'Programmeartaal',
|
||||||
'Plak of typ hier je broncode...':
|
'Plak of typ hier je broncode...': 'Plak of typ hjir dyn boarnekoade...',
|
||||||
'Plak of typ hjir dyn boarnekoade...',
|
|
||||||
'Overgeslagen': 'Oerslein',
|
'Overgeslagen': 'Oerslein',
|
||||||
'Kopiëren': 'Kopiearje',
|
'Kopiëren': 'Kopiearje',
|
||||||
'Kopieer als afbeelding': 'Kopiearje as ôfbylding',
|
'Kopieer als afbeelding': 'Kopiearje as ôfbylding',
|
||||||
|
|
@ -2033,8 +2032,7 @@ const _dutchSourceStrings = {
|
||||||
'Vrije Markdown': 'Markdown liber',
|
'Vrije Markdown': 'Markdown liber',
|
||||||
'Broncode': 'Código fuente',
|
'Broncode': 'Código fuente',
|
||||||
'Programmeertaal': 'Lenguahe di programashon',
|
'Programmeertaal': 'Lenguahe di programashon',
|
||||||
'Plak of typ hier je broncode...':
|
'Plak of typ hier je broncode...': 'Pega òf tek bo código fuente akinan...',
|
||||||
'Pega òf tek bo código fuente akinan...',
|
|
||||||
'Overgeslagen': 'Saltá',
|
'Overgeslagen': 'Saltá',
|
||||||
'Kopiëren': 'Kopia',
|
'Kopiëren': 'Kopia',
|
||||||
'Kopieer als afbeelding': 'Kopia komo imágen',
|
'Kopieer als afbeelding': 'Kopia komo imágen',
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,26 @@
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
import 'app.dart';
|
import 'app.dart';
|
||||||
|
import 'widgets/presentation/audience_window.dart';
|
||||||
|
|
||||||
void main() async {
|
void main(List<String> args) async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
// Secondary windows (e.g. the audience/beamer slide window) are launched by
|
||||||
|
// desktop_multi_window with these entrypoint arguments. They run a minimal app
|
||||||
|
// and must not touch the main window's window_manager setup.
|
||||||
|
if (args.isNotEmpty && args.first == 'multi_window') {
|
||||||
|
final raw = args.length >= 3 ? args[2] : '';
|
||||||
|
final parsed = raw.isEmpty ? const {} : jsonDecode(raw);
|
||||||
|
final map = Map<String, dynamic>.from(parsed as Map);
|
||||||
|
runApp(AudienceWindowApp(args: map));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux)) {
|
if (!kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux)) {
|
||||||
await windowManager.ensureInitialized();
|
await windowManager.ensureInitialized();
|
||||||
const options = WindowOptions(
|
const options = WindowOptions(
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,8 @@ class Slide {
|
||||||
final String quote;
|
final String quote;
|
||||||
final String quoteAuthor;
|
final String quoteAuthor;
|
||||||
final String customMarkdown;
|
final String customMarkdown;
|
||||||
final String codeLanguage; // highlight.js language id for code slides ('' = plain)
|
final String
|
||||||
|
codeLanguage; // highlight.js language id for code slides ('' = plain)
|
||||||
final String cssClass;
|
final String cssClass;
|
||||||
final String notes;
|
final String notes;
|
||||||
final double advanceDuration; // 0 = no auto-advance
|
final double advanceDuration; // 0 = no auto-advance
|
||||||
|
|
|
||||||
|
|
@ -963,7 +963,7 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
||||||
var initial = visible.indexWhere((i) => i >= editor.selectedIndex);
|
var initial = visible.indexWhere((i) => i >= editor.selectedIndex);
|
||||||
if (initial < 0) initial = visible.length - 1;
|
if (initial < 0) initial = visible.length - 1;
|
||||||
if (initial < 0) initial = 0;
|
if (initial < 0) initial = 0;
|
||||||
FullscreenPresenter.show(
|
FullscreenPresenter.present(
|
||||||
context,
|
context,
|
||||||
slides: slides,
|
slides: slides,
|
||||||
projectPath: deck.projectPath,
|
projectPath: deck.projectPath,
|
||||||
|
|
|
||||||
|
|
@ -83,10 +83,7 @@ class _CodeEditorState extends State<CodeEditor> {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
EditorField(
|
EditorField(label: 'Titel (optioneel)', controller: _title),
|
||||||
label: 'Titel (optioneel)',
|
|
||||||
controller: _title,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
|
||||||
144
lib/widgets/presentation/audience_window.dart
Normal file
144
lib/widgets/presentation/audience_window.dart
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import '../../models/deck.dart';
|
||||||
|
import '../../models/settings.dart';
|
||||||
|
import '../../models/slide.dart';
|
||||||
|
import '../../services/markdown_service.dart';
|
||||||
|
import '../../utils/url_launcher_util.dart';
|
||||||
|
import '../slides/slide_preview.dart';
|
||||||
|
|
||||||
|
/// Channel the audience (beamer) window listens on for updates from the
|
||||||
|
/// presenter (laptop) window.
|
||||||
|
const audienceChannel = WindowMethodChannel(
|
||||||
|
'ocideck/audience',
|
||||||
|
mode: ChannelMode.unidirectional,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Channel the presenter window listens on; the audience window uses it to
|
||||||
|
/// forward navigation (clicks on the beamer) and audio-complete events.
|
||||||
|
const presenterChannel = WindowMethodChannel(
|
||||||
|
'ocideck/presenter',
|
||||||
|
mode: ChannelMode.unidirectional,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// The app that runs inside the secondary (beamer) window. It only renders the
|
||||||
|
/// current slide fullscreen; the presenter window drives it via [audienceChannel].
|
||||||
|
class AudienceWindowApp extends StatefulWidget {
|
||||||
|
final Map<String, dynamic> args;
|
||||||
|
|
||||||
|
const AudienceWindowApp({super.key, required this.args});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AudienceWindowApp> createState() => _AudienceWindowAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AudienceWindowAppState extends State<AudienceWindowApp> {
|
||||||
|
List<Slide> _slides = const [];
|
||||||
|
ThemeProfile _theme = const ThemeProfile();
|
||||||
|
TlpLevel _tlp = TlpLevel.none;
|
||||||
|
String? _projectPath;
|
||||||
|
int _index = 0;
|
||||||
|
int _blank = 0; // 0 = none, 1 = black, 2 = white
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final markdown = widget.args['markdown'] as String? ?? '';
|
||||||
|
_projectPath = widget.args['projectPath'] as String?;
|
||||||
|
_index = (widget.args['index'] as num?)?.toInt() ?? 0;
|
||||||
|
final deck = MarkdownService().parseDeck(markdown);
|
||||||
|
_slides = deck?.slides ?? const [];
|
||||||
|
_theme = deck?.themeProfile ?? const ThemeProfile();
|
||||||
|
_tlp = deck?.tlp ?? TlpLevel.none;
|
||||||
|
audienceChannel.setMethodCallHandler(_onPresenterCall);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
audienceChannel.setMethodCallHandler(null);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> _onPresenterCall(MethodCall call) async {
|
||||||
|
switch (call.method) {
|
||||||
|
case 'update':
|
||||||
|
final m = Map<String, dynamic>.from(call.arguments as Map);
|
||||||
|
if (!mounted) return null;
|
||||||
|
setState(() {
|
||||||
|
_index = (m['index'] as num?)?.toInt() ?? _index;
|
||||||
|
_blank = (m['blank'] as num?)?.toInt() ?? 0;
|
||||||
|
});
|
||||||
|
case 'close':
|
||||||
|
try {
|
||||||
|
final self = await WindowController.fromCurrentEngine();
|
||||||
|
await self.close();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _send(String method) {
|
||||||
|
// Best-effort: the presenter may already be gone.
|
||||||
|
presenterChannel.invokeMethod(method).catchError((_) => null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
home: Scaffold(backgroundColor: Colors.black, body: _body()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _body() {
|
||||||
|
if (_slides.isEmpty) return const SizedBox.shrink();
|
||||||
|
if (_blank != 0) {
|
||||||
|
return Container(color: _blank == 2 ? Colors.white : Colors.black);
|
||||||
|
}
|
||||||
|
final slide = _slides[_index.clamp(0, _slides.length - 1)];
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => _send('next'),
|
||||||
|
onSecondaryTap: () => _send('prev'),
|
||||||
|
child: SizedBox.expand(child: _canvas(slide)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A 16:9 slide letterboxed to fit the screen, mirroring the presenter's view.
|
||||||
|
Widget _canvas(Slide slide) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (_, constraints) {
|
||||||
|
final w = constraints.maxWidth;
|
||||||
|
final h = constraints.maxHeight;
|
||||||
|
const ratio = 16.0 / 9.0;
|
||||||
|
double slideW, slideH;
|
||||||
|
if (w / h > ratio) {
|
||||||
|
slideH = h;
|
||||||
|
slideW = h * ratio;
|
||||||
|
} else {
|
||||||
|
slideW = w;
|
||||||
|
slideH = w / ratio;
|
||||||
|
}
|
||||||
|
return Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: slideW,
|
||||||
|
height: slideH,
|
||||||
|
child: SlidePreviewWidget(
|
||||||
|
slide: slide,
|
||||||
|
projectPath: _projectPath,
|
||||||
|
themeProfile: _theme,
|
||||||
|
onLinkTap: openExternalUrl,
|
||||||
|
slideNumber: _index + 1,
|
||||||
|
slideCount: _slides.length,
|
||||||
|
tlp: _tlp,
|
||||||
|
enableMedia: true,
|
||||||
|
autoplayMedia: true,
|
||||||
|
// Audio finishing on the beamer drives the presenter's auto-advance.
|
||||||
|
onAudioComplete: () => _send('audioComplete'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:screen_retriever/screen_retriever.dart';
|
import 'package:screen_retriever/screen_retriever.dart';
|
||||||
|
|
@ -8,9 +10,11 @@ import 'package:window_manager/window_manager.dart';
|
||||||
import '../../models/deck.dart';
|
import '../../models/deck.dart';
|
||||||
import '../../models/settings.dart';
|
import '../../models/settings.dart';
|
||||||
import '../../models/slide.dart';
|
import '../../models/slide.dart';
|
||||||
|
import '../../services/markdown_service.dart';
|
||||||
import '../../utils/url_launcher_util.dart';
|
import '../../utils/url_launcher_util.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
import '../slides/slide_preview.dart';
|
import '../slides/slide_preview.dart';
|
||||||
|
import 'audience_window.dart';
|
||||||
|
|
||||||
/// Blanco-schermstand tijdens het presenteren (zoals B/W in PowerPoint).
|
/// Blanco-schermstand tijdens het presenteren (zoals B/W in PowerPoint).
|
||||||
enum _Blank { none, black, white }
|
enum _Blank { none, black, white }
|
||||||
|
|
@ -22,6 +26,11 @@ class FullscreenPresenter extends StatefulWidget {
|
||||||
final int initialIndex;
|
final int initialIndex;
|
||||||
final TlpLevel tlp;
|
final TlpLevel tlp;
|
||||||
|
|
||||||
|
/// When set, this presenter drives a separate audience (beamer) window: the
|
||||||
|
/// laptop shows the presenter view, the slide goes to [audienceWindow]. Null
|
||||||
|
/// for the classic single-screen mode.
|
||||||
|
final WindowController? audienceWindow;
|
||||||
|
|
||||||
const FullscreenPresenter({
|
const FullscreenPresenter({
|
||||||
super.key,
|
super.key,
|
||||||
required this.slides,
|
required this.slides,
|
||||||
|
|
@ -29,8 +38,51 @@ class FullscreenPresenter extends StatefulWidget {
|
||||||
required this.themeProfile,
|
required this.themeProfile,
|
||||||
required this.initialIndex,
|
required this.initialIndex,
|
||||||
this.tlp = TlpLevel.none,
|
this.tlp = TlpLevel.none,
|
||||||
|
this.audienceWindow,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Entry point used by the app: pick dual-screen mode when a second display is
|
||||||
|
/// available (macOS), otherwise the single-window presenter. Any failure to
|
||||||
|
/// open the second window falls back to single-window mode.
|
||||||
|
static Future<void> present(
|
||||||
|
BuildContext context, {
|
||||||
|
required List<Slide> slides,
|
||||||
|
required String? projectPath,
|
||||||
|
required ThemeProfile themeProfile,
|
||||||
|
required int initialIndex,
|
||||||
|
TlpLevel tlp = TlpLevel.none,
|
||||||
|
}) async {
|
||||||
|
var dual = false;
|
||||||
|
if (Platform.isMacOS) {
|
||||||
|
try {
|
||||||
|
final displays = await screenRetriever.getAllDisplays();
|
||||||
|
dual = displays.length >= 2;
|
||||||
|
} catch (_) {
|
||||||
|
dual = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!context.mounted) return;
|
||||||
|
if (dual) {
|
||||||
|
await showDualScreen(
|
||||||
|
context,
|
||||||
|
slides: slides,
|
||||||
|
projectPath: projectPath,
|
||||||
|
themeProfile: themeProfile,
|
||||||
|
initialIndex: initialIndex,
|
||||||
|
tlp: tlp,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await show(
|
||||||
|
context,
|
||||||
|
slides: slides,
|
||||||
|
projectPath: projectPath,
|
||||||
|
themeProfile: themeProfile,
|
||||||
|
initialIndex: initialIndex,
|
||||||
|
tlp: tlp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static Future<void> show(
|
static Future<void> show(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required List<Slide> slides,
|
required List<Slide> slides,
|
||||||
|
|
@ -66,6 +118,88 @@ class FullscreenPresenter extends StatefulWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dual-screen mode: open a borderless audience window on the beamer showing
|
||||||
|
/// the slide, and run the presenter view (current/next/notes/timer) in the
|
||||||
|
/// main window on the laptop. The two windows stay in sync over method
|
||||||
|
/// channels. Falls back to [show] if the second window can't be created.
|
||||||
|
static Future<void> showDualScreen(
|
||||||
|
BuildContext context, {
|
||||||
|
required List<Slide> slides,
|
||||||
|
required String? projectPath,
|
||||||
|
required ThemeProfile themeProfile,
|
||||||
|
required int initialIndex,
|
||||||
|
TlpLevel tlp = TlpLevel.none,
|
||||||
|
}) async {
|
||||||
|
// A self-contained markdown deck is the payload for the audience window; it
|
||||||
|
// carries the slides, the style profile and the TLP level in one string.
|
||||||
|
final markdown = MarkdownService().generateDeck(
|
||||||
|
Deck(
|
||||||
|
title: 'Presentatie',
|
||||||
|
slides: slides,
|
||||||
|
projectPath: projectPath,
|
||||||
|
themeProfile: themeProfile,
|
||||||
|
tlp: tlp,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final argument = jsonEncode({
|
||||||
|
'markdown': markdown,
|
||||||
|
'projectPath': projectPath,
|
||||||
|
'index': initialIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
WindowController? audience;
|
||||||
|
try {
|
||||||
|
audience = await WindowController.create(
|
||||||
|
WindowConfiguration(arguments: argument, hiddenAtLaunch: true),
|
||||||
|
);
|
||||||
|
await audience.coverScreen(external: true);
|
||||||
|
} catch (_) {
|
||||||
|
audience = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audience == null) {
|
||||||
|
if (context.mounted) {
|
||||||
|
await show(
|
||||||
|
context,
|
||||||
|
slides: slides,
|
||||||
|
projectPath: projectPath,
|
||||||
|
themeProfile: themeProfile,
|
||||||
|
initialIndex: initialIndex,
|
||||||
|
tlp: tlp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final hadWakeLock = await _wakeLockEnabled();
|
||||||
|
await _enableWakeLock();
|
||||||
|
try {
|
||||||
|
if (context.mounted) {
|
||||||
|
await Navigator.push(
|
||||||
|
context,
|
||||||
|
PageRouteBuilder(
|
||||||
|
opaque: true,
|
||||||
|
pageBuilder: (context, anim, anim2) => FullscreenPresenter(
|
||||||
|
slides: slides,
|
||||||
|
projectPath: projectPath,
|
||||||
|
themeProfile: themeProfile,
|
||||||
|
initialIndex: initialIndex,
|
||||||
|
tlp: tlp,
|
||||||
|
audienceWindow: audience,
|
||||||
|
),
|
||||||
|
transitionsBuilder: (context, animation, secondary, child) =>
|
||||||
|
FadeTransition(opacity: animation, child: child),
|
||||||
|
transitionDuration: const Duration(milliseconds: 200),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await _restoreWakeLock(hadWakeLock);
|
||||||
|
// Make sure the audience window is gone even if exit didn't close it.
|
||||||
|
audience.close().catchError((_) => null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FullscreenPresenter> createState() => _FullscreenPresenterState();
|
State<FullscreenPresenter> createState() => _FullscreenPresenterState();
|
||||||
}
|
}
|
||||||
|
|
@ -150,12 +284,38 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
List<Display> _displays = const [];
|
List<Display> _displays = const [];
|
||||||
int _displayIndex = 0;
|
int _displayIndex = 0;
|
||||||
|
|
||||||
|
/// True when this presenter drives a separate audience (beamer) window.
|
||||||
|
bool get _dual => widget.audienceWindow != null;
|
||||||
|
|
||||||
|
/// Last (index, blank) pushed to the audience window, to avoid redundant sends.
|
||||||
|
int? _lastSentIndex;
|
||||||
|
int? _lastSentBlank;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_index = widget.initialIndex;
|
_index = widget.initialIndex;
|
||||||
_startTime = DateTime.now();
|
_startTime = DateTime.now();
|
||||||
_focusNode = FocusNode();
|
_focusNode = FocusNode();
|
||||||
|
if (_dual) {
|
||||||
|
// The laptop shows the presenter view; the slide lives on the beamer.
|
||||||
|
_presenterView = true;
|
||||||
|
// Navigation triggered on the beamer (clicks) and its audio-end events
|
||||||
|
// come back over this channel.
|
||||||
|
presenterChannel.setMethodCallHandler((call) async {
|
||||||
|
switch (call.method) {
|
||||||
|
case 'next':
|
||||||
|
_next();
|
||||||
|
case 'prev':
|
||||||
|
_prev();
|
||||||
|
case 'exit':
|
||||||
|
_exit();
|
||||||
|
case 'audioComplete':
|
||||||
|
_onAudioCompleted();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
// Tik elke seconde, maar herbouw alleen in presenter view (klok/teller).
|
// Tik elke seconde, maar herbouw alleen in presenter view (klok/teller).
|
||||||
_clockTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
_clockTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||||
if (mounted && _presenterView) setState(() {});
|
if (mounted && _presenterView) setState(() {});
|
||||||
|
|
@ -174,9 +334,26 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
_typedTimer?.cancel();
|
_typedTimer?.cancel();
|
||||||
_gridScroll.dispose();
|
_gridScroll.dispose();
|
||||||
_focusNode.dispose();
|
_focusNode.dispose();
|
||||||
|
if (_dual) presenterChannel.setMethodCallHandler(null);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int get _blankCode =>
|
||||||
|
_blank == _Blank.white ? 2 : (_blank == _Blank.black ? 1 : 0);
|
||||||
|
|
||||||
|
/// Mirror the current index/blank state to the audience window when it changed.
|
||||||
|
void _syncAudience() {
|
||||||
|
final aw = widget.audienceWindow;
|
||||||
|
if (aw == null) return;
|
||||||
|
final blank = _blankCode;
|
||||||
|
if (_index == _lastSentIndex && blank == _lastSentBlank) return;
|
||||||
|
_lastSentIndex = _index;
|
||||||
|
_lastSentBlank = blank;
|
||||||
|
audienceChannel
|
||||||
|
.invokeMethod('update', {'index': _index, 'blank': blank})
|
||||||
|
.catchError((_) => null);
|
||||||
|
}
|
||||||
|
|
||||||
/// Decode the current slide's images plus its neighbours into the image cache
|
/// Decode the current slide's images plus its neighbours into the image cache
|
||||||
/// ahead of time. Because a precached [FileImage] resolves synchronously, the
|
/// ahead of time. Because a precached [FileImage] resolves synchronously, the
|
||||||
/// next slide paints its picture on the very first frame instead of flashing
|
/// next slide paints its picture on the very first frame instead of flashing
|
||||||
|
|
@ -334,7 +511,15 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
|
|
||||||
Future<void> _exit() async {
|
Future<void> _exit() async {
|
||||||
_advanceTimer?.cancel();
|
_advanceTimer?.cancel();
|
||||||
await windowManager.setFullScreen(false);
|
final aw = widget.audienceWindow;
|
||||||
|
if (aw != null) {
|
||||||
|
// Dual mode: the main window was never put in full screen; just tear down
|
||||||
|
// the audience window.
|
||||||
|
audienceChannel.invokeMethod('close').catchError((_) => null);
|
||||||
|
aw.close().catchError((_) => null);
|
||||||
|
} else {
|
||||||
|
await windowManager.setFullScreen(false);
|
||||||
|
}
|
||||||
if (mounted) Navigator.pop(context);
|
if (mounted) Navigator.pop(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -644,6 +829,9 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep the beamer window in step with whatever index/blank we now show.
|
||||||
|
_syncAudience();
|
||||||
|
|
||||||
return Focus(
|
return Focus(
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
|
|
@ -849,9 +1037,11 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
slideCount: widget.slides.length,
|
slideCount: widget.slides.length,
|
||||||
tlp: widget.tlp,
|
tlp: widget.tlp,
|
||||||
// Tijdens het presenteren speelt media en starten audio/video
|
// Tijdens het presenteren speelt media en starten audio/video
|
||||||
// vanzelf; het audio-einde stuurt de auto-advance aan.
|
// vanzelf; het audio-einde stuurt de auto-advance aan. In dual-
|
||||||
enableMedia: true,
|
// schermmodus speelt de media op het beamervenster, niet hier,
|
||||||
autoplayMedia: true,
|
// anders zou het geluid dubbel klinken.
|
||||||
|
enableMedia: !_dual,
|
||||||
|
autoplayMedia: !_dual,
|
||||||
onAudioComplete: _onAudioCompleted,
|
onAudioComplete: _onAudioCompleted,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <desktop_drop/desktop_drop_plugin.h>
|
#include <desktop_drop/desktop_drop_plugin.h>
|
||||||
|
#include <desktop_multi_window/desktop_multi_window_plugin.h>
|
||||||
#include <pasteboard/pasteboard_plugin.h>
|
#include <pasteboard/pasteboard_plugin.h>
|
||||||
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
|
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
@ -16,6 +17,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
g_autoptr(FlPluginRegistrar) desktop_drop_registrar =
|
g_autoptr(FlPluginRegistrar) desktop_drop_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin");
|
||||||
desktop_drop_plugin_register_with_registrar(desktop_drop_registrar);
|
desktop_drop_plugin_register_with_registrar(desktop_drop_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) desktop_multi_window_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopMultiWindowPlugin");
|
||||||
|
desktop_multi_window_plugin_register_with_registrar(desktop_multi_window_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) pasteboard_registrar =
|
g_autoptr(FlPluginRegistrar) pasteboard_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin");
|
||||||
pasteboard_plugin_register_with_registrar(pasteboard_registrar);
|
pasteboard_plugin_register_with_registrar(pasteboard_registrar);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
desktop_drop
|
desktop_drop
|
||||||
|
desktop_multi_window
|
||||||
pasteboard
|
pasteboard
|
||||||
screen_retriever_linux
|
screen_retriever_linux
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import desktop_drop
|
import desktop_drop
|
||||||
|
import desktop_multi_window
|
||||||
import file_picker
|
import file_picker
|
||||||
import package_info_plus
|
import package_info_plus
|
||||||
import pasteboard
|
import pasteboard
|
||||||
|
|
@ -18,6 +19,7 @@ import window_manager
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin"))
|
DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin"))
|
||||||
|
FlutterMultiWindowPlugin.register(with: registry.registrar(forPlugin: "FlutterMultiWindowPlugin"))
|
||||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
|
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
PODS:
|
PODS:
|
||||||
- desktop_drop (0.0.1):
|
- desktop_drop (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- desktop_multi_window (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
- file_picker (0.0.1):
|
- file_picker (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- FlutterMacOS (1.0.0)
|
- FlutterMacOS (1.0.0)
|
||||||
|
|
@ -25,6 +27,7 @@ PODS:
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`)
|
- desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`)
|
||||||
|
- desktop_multi_window (from `Flutter/ephemeral/.symlinks/plugins/desktop_multi_window/macos`)
|
||||||
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
|
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
|
||||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||||
|
|
@ -39,6 +42,8 @@ DEPENDENCIES:
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
desktop_drop:
|
desktop_drop:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos
|
||||||
|
desktop_multi_window:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/desktop_multi_window/macos
|
||||||
file_picker:
|
file_picker:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
|
||||||
FlutterMacOS:
|
FlutterMacOS:
|
||||||
|
|
@ -62,6 +67,7 @@ EXTERNAL SOURCES:
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
desktop_drop: 10a3e6a7fa9dbe350541f2574092fecfa345a07b
|
desktop_drop: 10a3e6a7fa9dbe350541f2574092fecfa345a07b
|
||||||
|
desktop_multi_window: 93667594ccc4b88d91a97972fd3b1b89667fa80a
|
||||||
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
|
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
|
||||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||||
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import Cocoa
|
import Cocoa
|
||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
|
import desktop_multi_window
|
||||||
|
|
||||||
class MainFlutterWindow: NSWindow {
|
class MainFlutterWindow: NSWindow {
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
|
|
@ -10,6 +11,12 @@ class MainFlutterWindow: NSWindow {
|
||||||
|
|
||||||
RegisterGeneratedPlugins(registry: flutterViewController)
|
RegisterGeneratedPlugins(registry: flutterViewController)
|
||||||
|
|
||||||
|
// Register the app's plugins in every sub-window (e.g. the audience/beamer
|
||||||
|
// window) too, so video_player, image loading, etc. work there as well.
|
||||||
|
FlutterMultiWindowPlugin.setOnWindowCreatedCallback { controller in
|
||||||
|
RegisterGeneratedPlugins(registry: controller)
|
||||||
|
}
|
||||||
|
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,13 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.1"
|
version: "0.7.1"
|
||||||
|
desktop_multi_window:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: "third_party/desktop_multi_window"
|
||||||
|
relative: true
|
||||||
|
source: path
|
||||||
|
version: "0.3.0"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,10 @@ dependencies:
|
||||||
flutter_math_fork: ^0.7.4
|
flutter_math_fork: ^0.7.4
|
||||||
highlight: ^0.7.0
|
highlight: ^0.7.0
|
||||||
wakelock_plus: ^1.5.2
|
wakelock_plus: ^1.5.2
|
||||||
|
# Vendored fork: adds native macOS window-geometry/fullscreen methods that
|
||||||
|
# the published 0.3.0 dropped, needed for the dual-screen presenter mode.
|
||||||
|
desktop_multi_window:
|
||||||
|
path: third_party/desktop_multi_window
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
||||||
25
third_party/desktop_multi_window/CHANGELOG.md
vendored
Normal file
25
third_party/desktop_multi_window/CHANGELOG.md
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
## 0.3.0
|
||||||
|
|
||||||
|
* [BREAK CHANGE] rewritten, please refer to readme
|
||||||
|
|
||||||
|
## 0.2.1
|
||||||
|
|
||||||
|
* bug fixed
|
||||||
|
|
||||||
|
## 0.2.0
|
||||||
|
* Added the ability to determine whether a created window will be resizable or not
|
||||||
|
([#101](https://github.com/MixinNetwork/flutter-plugins/issues/101) and [#130](https://github.com/MixinNetwork/flutter-plugins/pull/130))
|
||||||
|
|
||||||
|
## 0.1.0
|
||||||
|
|
||||||
|
* [BREAK CHANGE] upgrade min flutter version to 3.0.0
|
||||||
|
* fix macOS memory leak issue. [#123](https://github.com/MixinNetwork/flutter-plugins/issues/123)
|
||||||
|
|
||||||
|
## 0.0.2
|
||||||
|
|
||||||
|
* [Windows] fix free window_channel_ may cause crash. [#78](https://github.com/MixinNetwork/flutter-plugins/pull/78)
|
||||||
|
* add getAllSubWindowIds api. [#77](https://github.com/MixinNetwork/flutter-plugins/pull/77)
|
||||||
|
|
||||||
|
## 0.0.1
|
||||||
|
|
||||||
|
* Initial release. support Linux, macOS, Windows.
|
||||||
201
third_party/desktop_multi_window/LICENSE
vendored
Normal file
201
third_party/desktop_multi_window/LICENSE
vendored
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [2021] [Mixin]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
303
third_party/desktop_multi_window/README.md
vendored
Normal file
303
third_party/desktop_multi_window/README.md
vendored
Normal file
|
|
@ -0,0 +1,303 @@
|
||||||
|
# desktop_multi_window
|
||||||
|
|
||||||
|
[](https://pub.dev/packages/desktop_multi_window)
|
||||||
|
|
||||||
|
A Flutter plugin to create and manage multiple windows on desktop platforms.
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---------|-----|
|
||||||
|
| Windows | ✅ |
|
||||||
|
| Linux | ✅ |
|
||||||
|
| macOS | ✅ |
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Add `desktop_multi_window` to your `pubspec.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dependencies:
|
||||||
|
desktop_multi_window: ^latest_version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### 1. Initialize Multi-Window Support
|
||||||
|
|
||||||
|
In your `main()` function, initialize multi-window support before running your app:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
|
|
||||||
|
Future<void> main(List<String> args) async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
// Get the current window controller
|
||||||
|
final windowController = await WindowController.fromCurrentEngine();
|
||||||
|
|
||||||
|
// Parse window arguments to determine which window to show
|
||||||
|
final arguments = parseArguments(windowController.arguments);
|
||||||
|
|
||||||
|
// Run different apps based on the window type
|
||||||
|
switch (arguments.type) {
|
||||||
|
case YourArgumentDefinitions.main:
|
||||||
|
runApp(const MainWindow());
|
||||||
|
case YourArgumentDefinitions.sample:
|
||||||
|
runApp(const SampleWindow());
|
||||||
|
// Add more window types as needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create New Windows
|
||||||
|
|
||||||
|
Use `WindowController.create()` to create and manage new windows:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Create a new window
|
||||||
|
final controller = await WindowController.create(
|
||||||
|
WindowConfiguration(
|
||||||
|
hiddenAtLaunch: true,
|
||||||
|
arguments: 'YOUR_WINDOW_ARGUMENTS_HERE',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Show the window (if hidden at launch)
|
||||||
|
await controller.show();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Manage Existing Windows
|
||||||
|
|
||||||
|
Get all window controllers and manage them:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Get all windows
|
||||||
|
final controllers = await WindowController.getAll();
|
||||||
|
|
||||||
|
// Find a specific window by business ID
|
||||||
|
for (var controller in controllers) {
|
||||||
|
final args = parseArguments(controller.arguments);
|
||||||
|
// Check window type
|
||||||
|
if (args.type == YourArgumentDefinitions.sample) {
|
||||||
|
await controller.center();
|
||||||
|
await controller.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen to window changes
|
||||||
|
onWindowsChanged.listen((_) {
|
||||||
|
// Handle window changes
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Communication Between Windows
|
||||||
|
|
||||||
|
Use `WindowMethodChannel` for bidirectional communication between windows:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// In the target window, set up a method call handler
|
||||||
|
const channel = WindowMethodChannel('my_channel');
|
||||||
|
channel.setMethodCallHandler((call) async {
|
||||||
|
switch (call.method) {
|
||||||
|
case 'play':
|
||||||
|
// Handle the method call
|
||||||
|
return 'success';
|
||||||
|
default:
|
||||||
|
throw MissingPluginException('Not implemented: ${call.method}');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// From another window, invoke methods
|
||||||
|
const channel = WindowMethodChannel('my_channel');
|
||||||
|
final result = await channel.invokeMethod('play');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Extend WindowController with Custom Methods
|
||||||
|
|
||||||
|
Create an extension to add custom functionality:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
|
extension WindowControllerExtension on WindowController {
|
||||||
|
Future<void> doCustomInitialize() async {
|
||||||
|
return await setWindowMethodHandler((call) async {
|
||||||
|
switch (call.method) {
|
||||||
|
case 'window_center':
|
||||||
|
return await windowManager.center();
|
||||||
|
case 'window_close':
|
||||||
|
return await windowManager.close();
|
||||||
|
default:
|
||||||
|
throw MissingPluginException('Not implemented: ${call.method}');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> center() {
|
||||||
|
return invokeMethod('window_center');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> close() {
|
||||||
|
return invokeMethod('window_close');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And now, you can center or close the window in the other window:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final controller = await WindowController.fromWindowId(other_window_id);
|
||||||
|
|
||||||
|
// Center the window
|
||||||
|
await controller.center();
|
||||||
|
|
||||||
|
// Close the window
|
||||||
|
await controller.close();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Working with Plugins in Sub-Windows
|
||||||
|
|
||||||
|
Each window created by this plugin has its own dedicated Flutter engine. Method channels cannot be shared between engines, so plugins must be manually registered for each new window.
|
||||||
|
|
||||||
|
### Platform-Specific Plugin Registration
|
||||||
|
|
||||||
|
#### Windows
|
||||||
|
|
||||||
|
Edit `windows/runner/flutter_window.cpp`:
|
||||||
|
|
||||||
|
1. Add the include at the top of the file:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
#include "flutter_window.h"
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "flutter/generated_plugin_registrant.h"
|
||||||
|
+#include "desktop_multi_window/desktop_multi_window_plugin.h"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Register the callback in the `OnCreate()` method:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
RegisterPlugins(flutter_controller_->engine());
|
||||||
|
+ DesktopMultiWindowSetWindowCreatedCallback([](void *controller) {
|
||||||
|
+ auto *flutter_view_controller =
|
||||||
|
+ reinterpret_cast<flutter::FlutterViewController *>(controller);
|
||||||
|
+ auto *registry = flutter_view_controller->engine();
|
||||||
|
+ RegisterPlugins(registry);
|
||||||
|
+ });
|
||||||
|
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
||||||
|
```
|
||||||
|
|
||||||
|
The `RegisterPlugins` function will automatically register all plugins for each new window.
|
||||||
|
|
||||||
|
#### macOS
|
||||||
|
|
||||||
|
Edit `macos/Runner/MainFlutterWindow.swift`:
|
||||||
|
|
||||||
|
1. Add the import at the top of the file:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
import Cocoa
|
||||||
|
import FlutterMacOS
|
||||||
|
+import desktop_multi_window
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Register the callback in the `awakeFromNib()` method:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
RegisterGeneratedPlugins(registry: flutterViewController)
|
||||||
|
|
||||||
|
+ FlutterMultiWindowPlugin.setOnWindowCreatedCallback { controller in
|
||||||
|
+ // Register the plugin which you want access from other isolate.
|
||||||
|
+ RegisterGeneratedPlugins(registry: controller)
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
super.awakeFromNib()
|
||||||
|
```
|
||||||
|
|
||||||
|
The `RegisterGeneratedPlugins` function will automatically register all plugins for each new window.
|
||||||
|
|
||||||
|
#### Linux
|
||||||
|
|
||||||
|
Edit `linux/my_application.cc`:
|
||||||
|
|
||||||
|
1. Add the include at the top of the file:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
#include "my_application.h"
|
||||||
|
|
||||||
|
#include <flutter_linux/flutter_linux.h>
|
||||||
|
#ifdef GDK_WINDOWING_X11
|
||||||
|
#include <gdk/gdkx.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "flutter/generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
+#include "desktop_multi_window/desktop_multi_window_plugin.h"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Register the callback in the `my_application_activate()` function:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||||
|
|
||||||
|
+ desktop_multi_window_plugin_set_window_created_callback([](FlPluginRegistry* registry){
|
||||||
|
+ fl_register_plugins(registry);
|
||||||
|
+ });
|
||||||
|
+
|
||||||
|
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||||
|
```
|
||||||
|
|
||||||
|
The `fl_register_plugins` function will automatically register all plugins for each new window.
|
||||||
|
|
||||||
|
## Integration with window_manager
|
||||||
|
|
||||||
|
This plugin works great with [window_manager](https://pub.dev/packages/window_manager) to control window properties:
|
||||||
|
|
||||||
|
by now, you should this fork version with a bit fix
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
window_manager:
|
||||||
|
git:
|
||||||
|
url: https://github.com/boyan01/window_manager.git
|
||||||
|
path: packages/window_manager
|
||||||
|
ref: 6fae92d21b4c80ce1b8f71c1190d7970cf722bd4
|
||||||
|
```
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
|
// Configure window options
|
||||||
|
WindowOptions windowOptions = const WindowOptions(
|
||||||
|
size: Size(800, 600),
|
||||||
|
center: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
skipTaskbar: false,
|
||||||
|
titleBarStyle: TitleBarStyle.hidden,
|
||||||
|
);
|
||||||
|
|
||||||
|
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||||
|
await windowManager.show();
|
||||||
|
await windowManager.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent window from closing immediately
|
||||||
|
windowManager.setPreventClose(true);
|
||||||
|
windowManager.addListener(this); // Must implement WindowListener
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Check out the [example](example) directory for a complete working application that demonstrates:
|
||||||
|
- Creating multiple window types
|
||||||
|
- Single instance vs multi-instance windows
|
||||||
|
- Communication between windows
|
||||||
|
- Custom window extensions
|
||||||
|
- Plugin registration for video playback
|
||||||
|
- Window lifecycle management
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
3
third_party/desktop_multi_window/lib/desktop_multi_window.dart
vendored
Normal file
3
third_party/desktop_multi_window/lib/desktop_multi_window.dart
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export 'src/window_controller.dart';
|
||||||
|
export 'src/window_configuration.dart';
|
||||||
|
export 'src/window_channel.dart';
|
||||||
219
third_party/desktop_multi_window/lib/src/window_channel.dart
vendored
Normal file
219
third_party/desktop_multi_window/lib/src/window_channel.dart
vendored
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
typedef MethodCallHandler = Future<dynamic> Function(MethodCall call);
|
||||||
|
|
||||||
|
/// Channel communication mode
|
||||||
|
enum ChannelMode {
|
||||||
|
/// Unidirectional mode: All engines can invoke this channel
|
||||||
|
/// Only one engine can register as handler
|
||||||
|
unidirectional('unidirectional'),
|
||||||
|
|
||||||
|
/// Bidirectional mode: Only paired engines can invoke each other
|
||||||
|
/// Maximum of 2 engines can register, and only they can call each other
|
||||||
|
bidirectional('bidirectional');
|
||||||
|
|
||||||
|
final String value;
|
||||||
|
const ChannelMode(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exception thrown when a window channel operation fails.
|
||||||
|
class WindowChannelException implements Exception {
|
||||||
|
final String code;
|
||||||
|
final String message;
|
||||||
|
final dynamic details;
|
||||||
|
|
||||||
|
WindowChannelException(this.code, this.message, [this.details]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
if (details != null) {
|
||||||
|
return 'WindowChannelException($code, $message, $details)';
|
||||||
|
}
|
||||||
|
return 'WindowChannelException($code, $message)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A method channel for cross-window communication.
|
||||||
|
///
|
||||||
|
/// Supports two modes:
|
||||||
|
/// - [ChannelMode.unidirectional]: One engine registers as handler, all engines can invoke
|
||||||
|
/// - [ChannelMode.bidirectional]: Two engines form a pair and can only invoke each other
|
||||||
|
class WindowMethodChannel {
|
||||||
|
final String name;
|
||||||
|
final ChannelMode mode;
|
||||||
|
|
||||||
|
const WindowMethodChannel(
|
||||||
|
this.name, {
|
||||||
|
this.mode = ChannelMode.bidirectional,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Invokes a method on the target engine that has registered this channel.
|
||||||
|
///
|
||||||
|
/// For unidirectional channels: Invokes the single registered handler
|
||||||
|
/// For bidirectional channels: Invokes the peer engine in the pair
|
||||||
|
///
|
||||||
|
/// Throws [WindowChannelException] if:
|
||||||
|
/// - The channel is not registered
|
||||||
|
/// - The target engine is not available
|
||||||
|
/// - For bidirectional: caller is not part of the pair
|
||||||
|
@optionalTypeArgs
|
||||||
|
Future<T?> invokeMethod<T>(String method, [dynamic arguments]) async {
|
||||||
|
_initializeChannelManager();
|
||||||
|
try {
|
||||||
|
return await _invokeMethodOnChannel<T>(name, method, arguments);
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
throw WindowChannelException(
|
||||||
|
e.code,
|
||||||
|
e.message ?? 'Failed to invoke method on channel $name',
|
||||||
|
e.details,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the method call handler for this channel.
|
||||||
|
///
|
||||||
|
/// The communication mode is determined by the [mode] parameter passed to the constructor:
|
||||||
|
/// - [ChannelMode.unidirectional]: Only one engine can register, all can invoke
|
||||||
|
/// - [ChannelMode.bidirectional]: Up to 2 engines can register, only they can invoke each other
|
||||||
|
///
|
||||||
|
/// Pass `null` as handler to remove the handler and unregister the channel.
|
||||||
|
///
|
||||||
|
/// Throws [WindowChannelException] if:
|
||||||
|
/// - Registration fails (e.g., channel limit reached)
|
||||||
|
/// - Mode conflicts with existing registration
|
||||||
|
Future<void> setMethodCallHandler(
|
||||||
|
Future<dynamic> Function(MethodCall call)? handler,
|
||||||
|
) async {
|
||||||
|
_initializeChannelManager();
|
||||||
|
|
||||||
|
if (handler != null) {
|
||||||
|
// Update handler if already registered
|
||||||
|
if (_registeredHandlers.containsKey(name)) {
|
||||||
|
_registeredHandlers[name] = handler;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register new handler
|
||||||
|
try {
|
||||||
|
await _registerMethodHandler(name, mode);
|
||||||
|
_registeredHandlers[name] = handler;
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
throw WindowChannelException(
|
||||||
|
e.code,
|
||||||
|
e.message ?? 'Failed to register handler for channel $name',
|
||||||
|
e.details,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove handler
|
||||||
|
if (!_registeredHandlers.containsKey(name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _unregisterMethodHandler(name);
|
||||||
|
_registeredHandlers.remove(name);
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
// Even if unregistration fails, remove the handler locally
|
||||||
|
_registeredHandlers.remove(name);
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(
|
||||||
|
'Warning: Failed to unregister handler for channel $name: ${e.message}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final _registeredHandlers = <String, MethodCallHandler>{};
|
||||||
|
|
||||||
|
const _methodChannel = MethodChannel('mixin.one/desktop_multi_window/channels');
|
||||||
|
|
||||||
|
bool _initialized = false;
|
||||||
|
|
||||||
|
void _initializeChannelManager() {
|
||||||
|
if (_initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_initialized = true;
|
||||||
|
_methodChannel.setMethodCallHandler((call) async {
|
||||||
|
if (call.method == 'methodCall') {
|
||||||
|
final arguments = call.arguments as Map;
|
||||||
|
final channelName = arguments['channel'] as String;
|
||||||
|
final method = arguments['method'] as String;
|
||||||
|
final args = arguments['arguments'];
|
||||||
|
|
||||||
|
final handler = _registeredHandlers[channelName];
|
||||||
|
if (handler == null) {
|
||||||
|
throw WindowChannelException(
|
||||||
|
'NO_HANDLER',
|
||||||
|
'No method call handler registered for channel $channelName',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final methodCall = MethodCall(method, args);
|
||||||
|
return await handler.call(methodCall);
|
||||||
|
} else {
|
||||||
|
throw MissingPluginException('No handler for method ${call.method}');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _registerMethodHandler(String name, ChannelMode mode) async {
|
||||||
|
try {
|
||||||
|
await _methodChannel.invokeMethod('registerMethodHandler', {
|
||||||
|
'channel': name,
|
||||||
|
'mode': mode.value,
|
||||||
|
});
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
if (e.code == 'CHANNEL_LIMIT_REACHED') {
|
||||||
|
throw WindowChannelException(
|
||||||
|
e.code,
|
||||||
|
mode == ChannelMode.unidirectional
|
||||||
|
? 'Cannot register channel "$name": already registered in unidirectional mode'
|
||||||
|
: 'Cannot register channel "$name": maximum of 2 engines allowed per channel',
|
||||||
|
e.details,
|
||||||
|
);
|
||||||
|
} else if (e.code == 'CHANNEL_MODE_CONFLICT') {
|
||||||
|
throw WindowChannelException(
|
||||||
|
e.code,
|
||||||
|
'Cannot register channel "$name": already registered in a different mode',
|
||||||
|
e.details,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _unregisterMethodHandler(String name) async {
|
||||||
|
await _methodChannel.invokeMethod('unregisterMethodHandler', {
|
||||||
|
'channel': name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T?> _invokeMethodOnChannel<T>(
|
||||||
|
String name, String method, dynamic arguments) async {
|
||||||
|
try {
|
||||||
|
return await _methodChannel.invokeMethod<T>('invokeMethod', {
|
||||||
|
'channel': name,
|
||||||
|
'method': method,
|
||||||
|
'arguments': arguments,
|
||||||
|
});
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
if (e.code == 'CHANNEL_UNREGISTERED') {
|
||||||
|
throw WindowChannelException(
|
||||||
|
e.code,
|
||||||
|
'Channel "$name" not accessible (may be unregistered, bidirectional pair, or permission denied)',
|
||||||
|
e.details,
|
||||||
|
);
|
||||||
|
} else if (e.code == 'CHANNEL_NOT_FOUND') {
|
||||||
|
throw WindowChannelException(
|
||||||
|
e.code,
|
||||||
|
'Channel "$name" not found in target engine',
|
||||||
|
e.details,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
third_party/desktop_multi_window/lib/src/window_configuration.dart
vendored
Normal file
43
third_party/desktop_multi_window/lib/src/window_configuration.dart
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
class WindowConfiguration {
|
||||||
|
const WindowConfiguration({
|
||||||
|
required this.arguments,
|
||||||
|
this.hiddenAtLaunch = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The arguments passed to the new window.
|
||||||
|
final String arguments;
|
||||||
|
|
||||||
|
final bool hiddenAtLaunch;
|
||||||
|
|
||||||
|
factory WindowConfiguration.fromJson(Map<String, dynamic> json) {
|
||||||
|
return WindowConfiguration(
|
||||||
|
arguments: json['arguments'] as String? ?? '',
|
||||||
|
hiddenAtLaunch: json['hiddenAtLaunch'] as bool? ?? false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'arguments': arguments,
|
||||||
|
'hiddenAtLaunch': hiddenAtLaunch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'WindowConfiguration(arguments: $arguments, hiddenAtLaunch: $hiddenAtLaunch)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
return other is WindowConfiguration &&
|
||||||
|
other.arguments == arguments &&
|
||||||
|
other.hiddenAtLaunch == hiddenAtLaunch;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return arguments.hashCode ^ hiddenAtLaunch.hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
158
third_party/desktop_multi_window/lib/src/window_controller.dart
vendored
Normal file
158
third_party/desktop_multi_window/lib/src/window_controller.dart
vendored
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import 'window_channel.dart';
|
||||||
|
import 'window_configuration.dart';
|
||||||
|
|
||||||
|
final _windowEvent = _windowEventAsStream();
|
||||||
|
|
||||||
|
/// A listenable that notifies when the windows list changes.
|
||||||
|
/// Listen to this to be notified when windows are created or destroyed.
|
||||||
|
Stream<void> get onWindowsChanged => _windowEvent.map((call) {
|
||||||
|
if (call.method == 'onWindowsChanged') {
|
||||||
|
return call.method;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}).where((event) => event != null);
|
||||||
|
|
||||||
|
/// The [WindowController] instance that is used to control this window.
|
||||||
|
class WindowController {
|
||||||
|
WindowController._(this.windowId, this.arguments)
|
||||||
|
: _windowChannel = WindowMethodChannel(
|
||||||
|
'mixin.one/window_controller/$windowId',
|
||||||
|
mode: ChannelMode.unidirectional,
|
||||||
|
);
|
||||||
|
|
||||||
|
final String windowId;
|
||||||
|
final String arguments;
|
||||||
|
|
||||||
|
final WindowMethodChannel _windowChannel;
|
||||||
|
|
||||||
|
factory WindowController.fromWindowId(String id) =>
|
||||||
|
WindowController._(id, '');
|
||||||
|
|
||||||
|
static Future<WindowController> create(
|
||||||
|
WindowConfiguration configuration) async {
|
||||||
|
final windowId = await _channel.invokeMethod<String>(
|
||||||
|
'createWindow',
|
||||||
|
configuration.toJson(),
|
||||||
|
);
|
||||||
|
assert(windowId != null, 'windowId is null');
|
||||||
|
assert(windowId!.isNotEmpty, 'windowId is empty');
|
||||||
|
return WindowController._(windowId!, configuration.arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<WindowController> fromCurrentEngine() async {
|
||||||
|
final definition = await _channel
|
||||||
|
.invokeMethod<Map<dynamic, dynamic>>('getWindowDefinition');
|
||||||
|
if (definition == null) {
|
||||||
|
throw Exception('Failed to get window definition');
|
||||||
|
}
|
||||||
|
final windowId = definition['windowId'] as String;
|
||||||
|
final windowArgument = definition['windowArgument'] as String;
|
||||||
|
return WindowController._(windowId, windowArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<WindowController>> getAll() async {
|
||||||
|
final result = await _channel.invokeMethod<List<dynamic>>('getAllWindows');
|
||||||
|
if (result == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return result.cast<Map<dynamic, dynamic>>().map((e) {
|
||||||
|
final windowId = e['windowId'] as String;
|
||||||
|
final windowArgument = e['windowArgument'] as String;
|
||||||
|
return WindowController._(windowId, windowArgument);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _callWindowMethod(String method,
|
||||||
|
[Map<String, dynamic>? arguments]) {
|
||||||
|
assert(windowId.isNotEmpty, 'windowId is empty');
|
||||||
|
assert(method.startsWith('window_'), 'method must start with "window_"');
|
||||||
|
return _channel.invokeMethod(
|
||||||
|
method,
|
||||||
|
{
|
||||||
|
'windowId': windowId,
|
||||||
|
...?arguments,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> show() => _callWindowMethod('window_show', {});
|
||||||
|
|
||||||
|
Future<void> hide() => _callWindowMethod('window_hide', {});
|
||||||
|
|
||||||
|
/// Close (destroy) this window. (macOS)
|
||||||
|
Future<void> close() => _callWindowMethod('window_close', {});
|
||||||
|
|
||||||
|
/// Position/size this window in screen coordinates. (macOS)
|
||||||
|
Future<void> setFrame(Rect frame) => _callWindowMethod('window_setFrame', {
|
||||||
|
'x': frame.left,
|
||||||
|
'y': frame.top,
|
||||||
|
'width': frame.width,
|
||||||
|
'height': frame.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Make this window a borderless surface filling an entire screen. When
|
||||||
|
/// [external] is true the first non-main screen (e.g. a beamer) is used,
|
||||||
|
/// otherwise the main screen. The window does not become key, so keyboard
|
||||||
|
/// focus stays with the window that had it. (macOS)
|
||||||
|
Future<void> coverScreen({bool external = true}) =>
|
||||||
|
_callWindowMethod('window_coverScreen', {'external': external});
|
||||||
|
|
||||||
|
@optionalTypeArgs
|
||||||
|
Future<T?> invokeMethod<T>(String method, [dynamic arguments]) =>
|
||||||
|
_windowChannel.invokeMethod<T>(method, arguments);
|
||||||
|
|
||||||
|
Future<void> setWindowMethodHandler(
|
||||||
|
Future<dynamic> Function(MethodCall call)? handler) {
|
||||||
|
assert(() {
|
||||||
|
scheduleMicrotask(() async {
|
||||||
|
final c = await WindowController.fromCurrentEngine();
|
||||||
|
if (c.windowId != windowId) {
|
||||||
|
throw FlutterError(
|
||||||
|
'setWindowMethodHandler can only be called on the current window controller. '
|
||||||
|
'Current windowId: ${c.windowId}, this windowId: $windowId');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
return _windowChannel.setMethodCallHandler(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
if (other.runtimeType != runtimeType) return false;
|
||||||
|
final WindowController otherController = other as WindowController;
|
||||||
|
return windowId == otherController.windowId &&
|
||||||
|
arguments == otherController.arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => windowId.hashCode ^ arguments.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'WindowController(windowId: $windowId, arguments: $arguments)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final _channel = MethodChannel('mixin.one/desktop_multi_window');
|
||||||
|
|
||||||
|
Stream<MethodCall> _windowEventAsStream() {
|
||||||
|
late StreamController<MethodCall> controller;
|
||||||
|
controller = StreamController<MethodCall>.broadcast(
|
||||||
|
onListen: () {
|
||||||
|
_channel.setMethodCallHandler((call) async {
|
||||||
|
controller.add(call);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel: () {
|
||||||
|
_channel.setMethodCallHandler(null);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return controller.stream;
|
||||||
|
}
|
||||||
27
third_party/desktop_multi_window/linux/CMakeLists.txt
vendored
Normal file
27
third_party/desktop_multi_window/linux/CMakeLists.txt
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
set(PROJECT_NAME "desktop_multi_window")
|
||||||
|
project(${PROJECT_NAME} LANGUAGES CXX)
|
||||||
|
|
||||||
|
# This value is used when generating builds using this plugin, so it must
|
||||||
|
# not be changed
|
||||||
|
set(PLUGIN_NAME "desktop_multi_window_plugin")
|
||||||
|
|
||||||
|
add_library(${PLUGIN_NAME} SHARED
|
||||||
|
"desktop_multi_window_plugin.cc"
|
||||||
|
"multi_window_manager.cc"
|
||||||
|
"flutter_window.cc"
|
||||||
|
"window_channel_plugin.cc")
|
||||||
|
apply_standard_settings(${PLUGIN_NAME})
|
||||||
|
set_target_properties(${PLUGIN_NAME} PROPERTIES
|
||||||
|
CXX_VISIBILITY_PRESET hidden)
|
||||||
|
target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
|
||||||
|
target_include_directories(${PLUGIN_NAME} INTERFACE
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/include")
|
||||||
|
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter)
|
||||||
|
target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK)
|
||||||
|
|
||||||
|
# List of absolute paths to libraries that should be bundled with the plugin
|
||||||
|
set(desktop_multi_window_bundled_libraries
|
||||||
|
""
|
||||||
|
PARENT_SCOPE
|
||||||
|
)
|
||||||
137
third_party/desktop_multi_window/linux/desktop_multi_window_plugin.cc
vendored
Normal file
137
third_party/desktop_multi_window/linux/desktop_multi_window_plugin.cc
vendored
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
#include "include/desktop_multi_window/desktop_multi_window_plugin.h"
|
||||||
|
|
||||||
|
#include <flutter_linux/flutter_linux.h>
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "desktop_multi_window_plugin_internal.h"
|
||||||
|
#include "flutter_window.h"
|
||||||
|
#include "multi_window_manager.h"
|
||||||
|
#include "window_channel_plugin.h"
|
||||||
|
|
||||||
|
#define DESKTOP_MULTI_WINDOW_PLUGIN(obj) \
|
||||||
|
(G_TYPE_CHECK_INSTANCE_CAST((obj), desktop_multi_window_plugin_get_type(), \
|
||||||
|
DesktopMultiWindowPlugin))
|
||||||
|
|
||||||
|
struct _DesktopMultiWindowPlugin {
|
||||||
|
GObject parent_instance;
|
||||||
|
FlutterWindow* window;
|
||||||
|
};
|
||||||
|
|
||||||
|
G_DEFINE_TYPE(DesktopMultiWindowPlugin,
|
||||||
|
desktop_multi_window_plugin,
|
||||||
|
g_object_get_type())
|
||||||
|
|
||||||
|
// Called when a method call is received from Flutter.
|
||||||
|
static void desktop_multi_window_plugin_handle_method_call(
|
||||||
|
DesktopMultiWindowPlugin* self,
|
||||||
|
FlMethodCall* method_call) {
|
||||||
|
const gchar* method = fl_method_call_get_name(method_call);
|
||||||
|
|
||||||
|
// Check if this is a window-specific method (starts with "window_")
|
||||||
|
if (g_str_has_prefix(method, "window_")) {
|
||||||
|
auto* args = fl_method_call_get_args(method_call);
|
||||||
|
auto window_id_value = fl_value_lookup_string(args, "windowId");
|
||||||
|
if (window_id_value == nullptr) {
|
||||||
|
g_autoptr(FlMethodResponse) response = FL_METHOD_RESPONSE(
|
||||||
|
fl_method_error_response_new("-1", "windowId is required", nullptr));
|
||||||
|
fl_method_call_respond(method_call, response, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gchar* window_id = fl_value_get_string(window_id_value);
|
||||||
|
auto window = MultiWindowManager::Instance()->GetWindow(window_id);
|
||||||
|
if (!window) {
|
||||||
|
g_autofree gchar* error_msg =
|
||||||
|
g_strdup_printf("failed to find target window: %s", window_id);
|
||||||
|
g_autoptr(FlMethodResponse) response = FL_METHOD_RESPONSE(
|
||||||
|
fl_method_error_response_new("-1", error_msg, nullptr));
|
||||||
|
fl_method_call_respond(method_call, response, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window->HandleWindowMethod(method, args, method_call);
|
||||||
|
return; // Window handles the response
|
||||||
|
}
|
||||||
|
|
||||||
|
g_autoptr(FlMethodResponse) response = nullptr;
|
||||||
|
|
||||||
|
if (strcmp(method, "createWindow") == 0) {
|
||||||
|
auto* args = fl_method_call_get_args(method_call);
|
||||||
|
auto window_id = MultiWindowManager::Instance()->Create(args);
|
||||||
|
response = FL_METHOD_RESPONSE(
|
||||||
|
fl_method_success_response_new(fl_value_new_string(window_id.c_str())));
|
||||||
|
} else if (strcmp(method, "getWindowDefinition") == 0) {
|
||||||
|
auto window_id = self->window->GetWindowId();
|
||||||
|
auto window_argument = self->window->GetWindowArgument();
|
||||||
|
|
||||||
|
g_autoptr(FlValue) definition = fl_value_new_map();
|
||||||
|
fl_value_set_string_take(definition, "windowId",
|
||||||
|
fl_value_new_string(window_id.c_str()));
|
||||||
|
fl_value_set_string_take(definition, "windowArgument",
|
||||||
|
fl_value_new_string(window_argument.c_str()));
|
||||||
|
|
||||||
|
response = FL_METHOD_RESPONSE(fl_method_success_response_new(definition));
|
||||||
|
} else if (strcmp(method, "getAllWindows") == 0) {
|
||||||
|
auto windows = MultiWindowManager::Instance()->GetAllWindows();
|
||||||
|
response = FL_METHOD_RESPONSE(fl_method_success_response_new(windows));
|
||||||
|
} else {
|
||||||
|
response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
|
||||||
|
}
|
||||||
|
|
||||||
|
fl_method_call_respond(method_call, response, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void desktop_multi_window_plugin_dispose(GObject* object) {
|
||||||
|
G_OBJECT_CLASS(desktop_multi_window_plugin_parent_class)->dispose(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void desktop_multi_window_plugin_class_init(
|
||||||
|
DesktopMultiWindowPluginClass
|
||||||
|
* klass) {
|
||||||
|
G_OBJECT_CLASS(klass)->dispose = desktop_multi_window_plugin_dispose;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void desktop_multi_window_plugin_init(DesktopMultiWindowPlugin* self) {}
|
||||||
|
|
||||||
|
static void method_call_cb(FlMethodChannel* channel,
|
||||||
|
FlMethodCall* method_call,
|
||||||
|
gpointer user_data) {
|
||||||
|
DesktopMultiWindowPlugin* plugin = DESKTOP_MULTI_WINDOW_PLUGIN(user_data);
|
||||||
|
desktop_multi_window_plugin_handle_method_call(plugin, method_call);
|
||||||
|
}
|
||||||
|
|
||||||
|
void desktop_multi_window_plugin_register_with_registrar_internal(
|
||||||
|
FlPluginRegistrar* registrar,
|
||||||
|
FlutterWindow* window) {
|
||||||
|
DesktopMultiWindowPlugin* plugin = DESKTOP_MULTI_WINDOW_PLUGIN(
|
||||||
|
g_object_new(desktop_multi_window_plugin_get_type(), nullptr));
|
||||||
|
plugin->window = window;
|
||||||
|
|
||||||
|
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
|
||||||
|
FlMethodChannel* channel = fl_method_channel_new(
|
||||||
|
fl_plugin_registrar_get_messenger(registrar),
|
||||||
|
"mixin.one/desktop_multi_window", FL_METHOD_CODEC(codec));
|
||||||
|
fl_method_channel_set_method_call_handler(
|
||||||
|
channel, method_call_cb, g_object_ref(plugin), g_object_unref);
|
||||||
|
|
||||||
|
// Set channel to window for event notifications
|
||||||
|
window->SetChannel(channel);
|
||||||
|
|
||||||
|
// Register WindowChannel plugin for each engine
|
||||||
|
window_channel_plugin_register_with_registrar(registrar);
|
||||||
|
|
||||||
|
g_object_unref(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
void desktop_multi_window_plugin_register_with_registrar(
|
||||||
|
FlPluginRegistrar* registrar) {
|
||||||
|
auto view = fl_plugin_registrar_get_view(registrar);
|
||||||
|
auto window = gtk_widget_get_toplevel(GTK_WIDGET(view));
|
||||||
|
if (GTK_IS_WINDOW(window)) {
|
||||||
|
MultiWindowManager::Instance()->AttachMainWindow(window, registrar);
|
||||||
|
} else { // variant
|
||||||
|
g_critical("can not find GtkWindow instance for main window.");
|
||||||
|
}
|
||||||
|
}
|
||||||
12
third_party/desktop_multi_window/linux/desktop_multi_window_plugin_internal.h
vendored
Normal file
12
third_party/desktop_multi_window/linux/desktop_multi_window_plugin_internal.h
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
#ifndef DESKTOP_MULTI_WINDOW_LINUX_DESKTOP_MULTI_WINDOW_PLUGIN_INTERNAL_H_
|
||||||
|
#define DESKTOP_MULTI_WINDOW_LINUX_DESKTOP_MULTI_WINDOW_PLUGIN_INTERNAL_H_
|
||||||
|
|
||||||
|
#include "flutter_linux/flutter_linux.h"
|
||||||
|
|
||||||
|
class FlutterWindow;
|
||||||
|
|
||||||
|
void desktop_multi_window_plugin_register_with_registrar_internal(
|
||||||
|
FlPluginRegistrar* registrar,
|
||||||
|
FlutterWindow* window);
|
||||||
|
|
||||||
|
#endif // DESKTOP_MULTI_WINDOW_LINUX_DESKTOP_MULTI_WINDOW_PLUGIN_INTERNAL_H_
|
||||||
52
third_party/desktop_multi_window/linux/flutter_window.cc
vendored
Executable file
52
third_party/desktop_multi_window/linux/flutter_window.cc
vendored
Executable file
|
|
@ -0,0 +1,52 @@
|
||||||
|
#include "flutter_window.h"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
FlutterWindow::FlutterWindow(const std::string& id,
|
||||||
|
const std::string& argument,
|
||||||
|
GtkWidget* window)
|
||||||
|
: id_(id), window_argument_(argument), window_(window) {}
|
||||||
|
|
||||||
|
FlutterWindow::~FlutterWindow() = default;
|
||||||
|
|
||||||
|
void FlutterWindow::SetChannel(FlMethodChannel* channel) {
|
||||||
|
channel_ = channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlutterWindow::NotifyWindowEvent(const gchar* event, FlValue* data) {
|
||||||
|
if (channel_) {
|
||||||
|
fl_method_channel_invoke_method(channel_, event, data, nullptr, nullptr, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlutterWindow::Show() {
|
||||||
|
if (window_) {
|
||||||
|
gtk_widget_show(GTK_WIDGET(window_));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlutterWindow::Hide() {
|
||||||
|
if (window_) {
|
||||||
|
gtk_widget_hide(GTK_WIDGET(window_));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlutterWindow::HandleWindowMethod(const gchar* method,
|
||||||
|
FlValue* arguments,
|
||||||
|
FlMethodCall* method_call) {
|
||||||
|
g_autoptr(FlMethodResponse) response = nullptr;
|
||||||
|
|
||||||
|
if (strcmp(method, "window_show") == 0) {
|
||||||
|
Show();
|
||||||
|
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||||
|
} else if (strcmp(method, "window_hide") == 0) {
|
||||||
|
Hide();
|
||||||
|
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||||
|
} else {
|
||||||
|
g_autofree gchar* error_msg = g_strdup_printf("unknown method: %s", method);
|
||||||
|
response = FL_METHOD_RESPONSE(
|
||||||
|
fl_method_error_response_new("-1", error_msg, nullptr));
|
||||||
|
}
|
||||||
|
|
||||||
|
fl_method_call_respond(method_call, response, nullptr);
|
||||||
|
}
|
||||||
44
third_party/desktop_multi_window/linux/flutter_window.h
vendored
Executable file
44
third_party/desktop_multi_window/linux/flutter_window.h
vendored
Executable file
|
|
@ -0,0 +1,44 @@
|
||||||
|
#ifndef DESKTOP_MULTI_WINDOW_WINDOWS_FLUTTER_WINDOW_H_
|
||||||
|
#define DESKTOP_MULTI_WINDOW_WINDOWS_FLUTTER_WINDOW_H_
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <flutter_linux/flutter_linux.h>
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
class FlutterWindow {
|
||||||
|
public:
|
||||||
|
FlutterWindow(const std::string& id,
|
||||||
|
const std::string& argument,
|
||||||
|
GtkWidget* window);
|
||||||
|
~FlutterWindow();
|
||||||
|
|
||||||
|
std::string GetWindowId() const { return id_; }
|
||||||
|
|
||||||
|
std::string GetWindowArgument() const { return window_argument_; }
|
||||||
|
|
||||||
|
GtkWindow* GetWindow() { return GTK_WINDOW(window_); }
|
||||||
|
|
||||||
|
void SetChannel(FlMethodChannel* channel);
|
||||||
|
|
||||||
|
void NotifyWindowEvent(const gchar* event, FlValue* data);
|
||||||
|
|
||||||
|
void Show();
|
||||||
|
|
||||||
|
void Hide();
|
||||||
|
|
||||||
|
void HandleWindowMethod(const gchar* method,
|
||||||
|
FlValue* arguments,
|
||||||
|
FlMethodCall* method_call);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string id_;
|
||||||
|
std::string window_argument_;
|
||||||
|
GtkWidget* window_ = nullptr;
|
||||||
|
FlMethodChannel* channel_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DESKTOP_MULTI_WINDOW_WINDOWS_FLUTTER_WINDOW_H_
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
#ifndef FLUTTER_PLUGIN_DESKTOP_MULTI_WINDOW_PLUGIN_H_
|
||||||
|
#define FLUTTER_PLUGIN_DESKTOP_MULTI_WINDOW_PLUGIN_H_
|
||||||
|
|
||||||
|
#include <flutter_linux/flutter_linux.h>
|
||||||
|
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
|
||||||
|
#ifdef FLUTTER_PLUGIN_IMPL
|
||||||
|
#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default")))
|
||||||
|
#else
|
||||||
|
#define FLUTTER_PLUGIN_EXPORT
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef struct _DesktopMultiWindowPlugin DesktopMultiWindowPlugin;
|
||||||
|
typedef struct {
|
||||||
|
GObjectClass parent_class;
|
||||||
|
} DesktopMultiWindowPluginClass;
|
||||||
|
|
||||||
|
FLUTTER_PLUGIN_EXPORT GType desktop_multi_window_plugin_get_type();
|
||||||
|
|
||||||
|
FLUTTER_PLUGIN_EXPORT void desktop_multi_window_plugin_register_with_registrar(
|
||||||
|
FlPluginRegistrar* registrar);
|
||||||
|
|
||||||
|
typedef void (*WindowCreatedCallback)(FlPluginRegistry *registry);
|
||||||
|
|
||||||
|
FLUTTER_PLUGIN_EXPORT void desktop_multi_window_plugin_set_window_created_callback(
|
||||||
|
WindowCreatedCallback callback);
|
||||||
|
|
||||||
|
|
||||||
|
G_END_DECLS
|
||||||
|
|
||||||
|
#endif // FLUTTER_PLUGIN_DESKTOP_MULTI_WINDOW_PLUGIN_H_
|
||||||
235
third_party/desktop_multi_window/linux/multi_window_manager.cc
vendored
Executable file
235
third_party/desktop_multi_window/linux/multi_window_manager.cc
vendored
Executable file
|
|
@ -0,0 +1,235 @@
|
||||||
|
#include "multi_window_manager.h"
|
||||||
|
|
||||||
|
#include <iomanip>
|
||||||
|
#include <random>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
#include "desktop_multi_window_plugin_internal.h"
|
||||||
|
#include "flutter_window.h"
|
||||||
|
#include "include/desktop_multi_window/desktop_multi_window_plugin.h"
|
||||||
|
#include "window_configuration.h"
|
||||||
|
#ifdef GDK_WINDOWING_X11
|
||||||
|
#include <gdk/gdkx.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string GenerateWindowId() {
|
||||||
|
std::random_device rd;
|
||||||
|
std::mt19937_64 gen(rd());
|
||||||
|
std::uniform_int_distribution<uint64_t> dis;
|
||||||
|
|
||||||
|
uint64_t part1 = dis(gen);
|
||||||
|
uint64_t part2 = dis(gen);
|
||||||
|
|
||||||
|
part1 = (part1 & 0xFFFFFFFFFFFF0FFFULL) | 0x0000000000004000ULL;
|
||||||
|
part2 = (part2 & 0x3FFFFFFFFFFFFFFFULL) | 0x8000000000000000ULL;
|
||||||
|
|
||||||
|
char uuid_str[37];
|
||||||
|
snprintf(uuid_str, sizeof(uuid_str), "%08x-%04x-%04x-%04x-%012llx",
|
||||||
|
static_cast<uint32_t>(part1 >> 32),
|
||||||
|
static_cast<uint16_t>(part1 >> 16), static_cast<uint16_t>(part1),
|
||||||
|
static_cast<uint16_t>(part2 >> 48), part2 & 0xFFFFFFFFFFFFULL);
|
||||||
|
|
||||||
|
return std::string(uuid_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowCreatedCallback _g_window_created_callback = nullptr;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// static
|
||||||
|
MultiWindowManager* MultiWindowManager::Instance() {
|
||||||
|
static auto manager = std::make_shared<MultiWindowManager>();
|
||||||
|
return manager.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiWindowManager::MultiWindowManager() : windows_() {}
|
||||||
|
|
||||||
|
MultiWindowManager::~MultiWindowManager() = default;
|
||||||
|
|
||||||
|
std::string MultiWindowManager::Create(FlValue* args) {
|
||||||
|
WindowConfiguration config = WindowConfiguration::FromFlValue(args);
|
||||||
|
std::string window_id = GenerateWindowId();
|
||||||
|
|
||||||
|
// Create GTK window
|
||||||
|
GtkApplication* app = GTK_APPLICATION(g_application_get_default());
|
||||||
|
GtkWindow* window = GTK_WINDOW(gtk_application_window_new(app));
|
||||||
|
gtk_application_add_window(app, window);
|
||||||
|
|
||||||
|
gboolean use_header_bar = TRUE;
|
||||||
|
#ifdef GDK_WINDOWING_X11
|
||||||
|
GdkScreen* screen = gtk_window_get_screen(window);
|
||||||
|
if (GDK_IS_X11_SCREEN(screen)) {
|
||||||
|
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
||||||
|
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
|
||||||
|
use_header_bar = FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (use_header_bar) {
|
||||||
|
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||||
|
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||||
|
gtk_header_bar_set_title(header_bar, "");
|
||||||
|
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||||
|
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||||
|
} else {
|
||||||
|
gtk_window_set_title(window, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
gtk_window_set_default_size(window, 1280, 720);
|
||||||
|
|
||||||
|
gtk_window_set_title(window, "");
|
||||||
|
if (config.hidden_at_launch) {
|
||||||
|
gtk_widget_realize(GTK_WIDGET(window));
|
||||||
|
} else {
|
||||||
|
gtk_widget_show(GTK_WIDGET(window));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create FlutterWindow instance
|
||||||
|
auto w = std::make_unique<FlutterWindow>(window_id, config.arguments,
|
||||||
|
GTK_WIDGET(window));
|
||||||
|
windows_[window_id] = std::move(w);
|
||||||
|
|
||||||
|
// Setup Flutter project
|
||||||
|
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||||
|
const char* entrypoint_args[] = {"multi_window", window_id.c_str(),
|
||||||
|
config.arguments.c_str(), nullptr};
|
||||||
|
fl_dart_project_set_dart_entrypoint_arguments(
|
||||||
|
project, const_cast<char**>(entrypoint_args));
|
||||||
|
|
||||||
|
// Create Flutter view
|
||||||
|
auto fl_view = fl_view_new(project);
|
||||||
|
gtk_widget_show(GTK_WIDGET(fl_view));
|
||||||
|
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(fl_view));
|
||||||
|
|
||||||
|
// Issues from flutter/engine: https://github.com/flutter/engine/pull/40033
|
||||||
|
// Prevent delete-event from flutter engine shell, which will quit the whole
|
||||||
|
// appplication when the window is closed. this can be done by
|
||||||
|
// [window_manager] plugin, but we need it here if user is not using that
|
||||||
|
// plugin.
|
||||||
|
guint handler_id = g_signal_handler_find(window, G_SIGNAL_MATCH_DATA, 0, 0,
|
||||||
|
NULL, NULL, fl_view);
|
||||||
|
if (handler_id > 0) {
|
||||||
|
g_signal_handler_disconnect(window, handler_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call window created callback
|
||||||
|
if (_g_window_created_callback) {
|
||||||
|
_g_window_created_callback(FL_PLUGIN_REGISTRY(fl_view));
|
||||||
|
}
|
||||||
|
|
||||||
|
ObserveWindowClose(window_id, window);
|
||||||
|
// Register plugin
|
||||||
|
g_autoptr(FlPluginRegistrar) desktop_multi_window_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(FL_PLUGIN_REGISTRY(fl_view),
|
||||||
|
"DesktopMultiWindowPlugin");
|
||||||
|
|
||||||
|
desktop_multi_window_plugin_register_with_registrar_internal(
|
||||||
|
desktop_multi_window_registrar, windows_[window_id].get());
|
||||||
|
|
||||||
|
gtk_widget_grab_focus(GTK_WIDGET(fl_view));
|
||||||
|
|
||||||
|
// Notify all windows about the change
|
||||||
|
NotifyWindowsChanged();
|
||||||
|
|
||||||
|
return window_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiWindowManager::AttachMainWindow(GtkWidget* window_widget,
|
||||||
|
FlPluginRegistrar* registrar) {
|
||||||
|
// check window widget is in windows_
|
||||||
|
for (const auto& pair : windows_) {
|
||||||
|
if (pair.second->GetWindow() == GTK_WINDOW(window_widget)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string main_window_id = GenerateWindowId();
|
||||||
|
auto window =
|
||||||
|
std::make_unique<FlutterWindow>(main_window_id, "", window_widget);
|
||||||
|
windows_[main_window_id] = std::move(window);
|
||||||
|
|
||||||
|
ObserveWindowClose(main_window_id, GTK_WINDOW(window_widget));
|
||||||
|
desktop_multi_window_plugin_register_with_registrar_internal(
|
||||||
|
registrar, windows_[main_window_id].get());
|
||||||
|
|
||||||
|
// Notify all windows about the change
|
||||||
|
NotifyWindowsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiWindowManager::ObserveWindowClose(const std::string& window_id,
|
||||||
|
GtkWindow* window) {
|
||||||
|
g_signal_connect(
|
||||||
|
GTK_WIDGET(window), "destroy",
|
||||||
|
G_CALLBACK(+[](GtkWidget* widget, gpointer arg) {
|
||||||
|
auto* window_id_ptr = static_cast<std::string*>(arg);
|
||||||
|
|
||||||
|
GtkWidget* child = gtk_bin_get_child(GTK_BIN(widget));
|
||||||
|
if (child && FL_IS_VIEW(child)) {
|
||||||
|
gtk_container_remove(GTK_CONTAINER(widget), child);
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiWindowManager::Instance()->RemoveWindow(*window_id_ptr);
|
||||||
|
delete window_id_ptr;
|
||||||
|
}),
|
||||||
|
new std::string(window_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
FlutterWindow* MultiWindowManager::GetWindow(const std::string& window_id) {
|
||||||
|
auto it = windows_.find(window_id);
|
||||||
|
if (it != windows_.end()) {
|
||||||
|
return it->second.get();
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlValue* MultiWindowManager::GetAllWindows() {
|
||||||
|
g_autoptr(FlValue) windows = fl_value_new_list();
|
||||||
|
for (const auto& pair : windows_) {
|
||||||
|
g_autoptr(FlValue) window_info = fl_value_new_map();
|
||||||
|
fl_value_set_string_take(
|
||||||
|
window_info, "windowId",
|
||||||
|
fl_value_new_string(pair.second->GetWindowId().c_str()));
|
||||||
|
fl_value_set_string_take(
|
||||||
|
window_info, "windowArgument",
|
||||||
|
fl_value_new_string(pair.second->GetWindowArgument().c_str()));
|
||||||
|
fl_value_append_take(windows, fl_value_ref(window_info));
|
||||||
|
}
|
||||||
|
return fl_value_ref(windows);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> MultiWindowManager::GetAllWindowIds() {
|
||||||
|
std::vector<std::string> window_ids;
|
||||||
|
for (const auto& pair : windows_) {
|
||||||
|
window_ids.push_back(pair.first);
|
||||||
|
}
|
||||||
|
return window_ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiWindowManager::NotifyWindowsChanged() {
|
||||||
|
auto window_ids = GetAllWindowIds();
|
||||||
|
|
||||||
|
g_autoptr(FlValue) window_ids_list = fl_value_new_list();
|
||||||
|
for (const auto& id : window_ids) {
|
||||||
|
fl_value_append_take(window_ids_list, fl_value_new_string(id.c_str()));
|
||||||
|
}
|
||||||
|
|
||||||
|
g_autoptr(FlValue) data = fl_value_new_map();
|
||||||
|
fl_value_set_string_take(data, "windowIds", fl_value_ref(window_ids_list));
|
||||||
|
|
||||||
|
for (const auto& pair : windows_) {
|
||||||
|
pair.second->NotifyWindowEvent("onWindowsChanged", data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiWindowManager::RemoveWindow(const std::string& window_id) {
|
||||||
|
g_warning("RemoveWindow: %s", window_id.c_str());
|
||||||
|
windows_.erase(window_id);
|
||||||
|
NotifyWindowsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void desktop_multi_window_plugin_set_window_created_callback(
|
||||||
|
WindowCreatedCallback callback) {
|
||||||
|
_g_window_created_callback = callback;
|
||||||
|
}
|
||||||
47
third_party/desktop_multi_window/linux/multi_window_manager.h
vendored
Executable file
47
third_party/desktop_multi_window/linux/multi_window_manager.h
vendored
Executable file
|
|
@ -0,0 +1,47 @@
|
||||||
|
#ifndef DESKTOP_MULTI_WINDOW_WINDOWS_MULTI_WINDOW_MANAGER_H_
|
||||||
|
#define DESKTOP_MULTI_WINDOW_WINDOWS_MULTI_WINDOW_MANAGER_H_
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <flutter_linux/flutter_linux.h>
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
#include "flutter_window.h"
|
||||||
|
|
||||||
|
class MultiWindowManager
|
||||||
|
: public std::enable_shared_from_this<MultiWindowManager> {
|
||||||
|
public:
|
||||||
|
static MultiWindowManager* Instance();
|
||||||
|
|
||||||
|
MultiWindowManager();
|
||||||
|
|
||||||
|
virtual ~MultiWindowManager();
|
||||||
|
|
||||||
|
std::string Create(FlValue* args);
|
||||||
|
|
||||||
|
void AttachMainWindow(GtkWidget* main_flutter_window,
|
||||||
|
FlPluginRegistrar* registrar);
|
||||||
|
|
||||||
|
FlutterWindow* GetWindow(const std::string& window_id);
|
||||||
|
|
||||||
|
FlValue* GetAllWindows();
|
||||||
|
|
||||||
|
std::vector<std::string> GetAllWindowIds();
|
||||||
|
|
||||||
|
void RemoveWindow(const std::string& window_id);
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
void ObserveWindowClose(const std::string& window_id,
|
||||||
|
GtkWindow* window);
|
||||||
|
|
||||||
|
void NotifyWindowsChanged();
|
||||||
|
|
||||||
|
std::map<std::string, std::unique_ptr<FlutterWindow>> windows_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DESKTOP_MULTI_WINDOW_WINDOWS_MULTI_WINDOW_MANAGER_H_
|
||||||
370
third_party/desktop_multi_window/linux/window_channel_plugin.cc
vendored
Normal file
370
third_party/desktop_multi_window/linux/window_channel_plugin.cc
vendored
Normal file
|
|
@ -0,0 +1,370 @@
|
||||||
|
#include "window_channel_plugin.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
enum class ChannelMode { kUnidirectional, kBidirectional };
|
||||||
|
|
||||||
|
enum class RegistrationOutcome {
|
||||||
|
kAdded,
|
||||||
|
kAlreadyRegistered,
|
||||||
|
kLimitReached,
|
||||||
|
kModeConflict
|
||||||
|
};
|
||||||
|
|
||||||
|
struct _WindowChannelPlugin {
|
||||||
|
GObject parent_instance;
|
||||||
|
FlMethodChannel* channel;
|
||||||
|
std::vector<std::string>* registered_channels;
|
||||||
|
};
|
||||||
|
|
||||||
|
G_DEFINE_TYPE(WindowChannelPlugin, window_channel_plugin, G_TYPE_OBJECT)
|
||||||
|
|
||||||
|
class ChannelRegistry {
|
||||||
|
public:
|
||||||
|
static ChannelRegistry& GetInstance() {
|
||||||
|
static ChannelRegistry instance;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
RegistrationOutcome Register(const std::string& channel,
|
||||||
|
WindowChannelPlugin* plugin,
|
||||||
|
ChannelMode mode) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
if (mode == ChannelMode::kUnidirectional) {
|
||||||
|
return RegisterUnidirectional(channel, plugin);
|
||||||
|
} else {
|
||||||
|
return RegisterBidirectional(channel, plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
RegistrationOutcome RegisterUnidirectional(const std::string& channel,
|
||||||
|
WindowChannelPlugin* plugin) {
|
||||||
|
// Check if already used in bidirectional mode
|
||||||
|
if (bidirectional_channels_.find(channel) !=
|
||||||
|
bidirectional_channels_.end()) {
|
||||||
|
return RegistrationOutcome::kModeConflict;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto it = unidirectional_channels_.find(channel);
|
||||||
|
if (it != unidirectional_channels_.end()) {
|
||||||
|
if (it->second == plugin) {
|
||||||
|
return RegistrationOutcome::kAlreadyRegistered;
|
||||||
|
}
|
||||||
|
// Already registered by another plugin
|
||||||
|
return RegistrationOutcome::kLimitReached;
|
||||||
|
}
|
||||||
|
|
||||||
|
unidirectional_channels_[channel] = plugin;
|
||||||
|
return RegistrationOutcome::kAdded;
|
||||||
|
}
|
||||||
|
|
||||||
|
RegistrationOutcome RegisterBidirectional(const std::string& channel,
|
||||||
|
WindowChannelPlugin* plugin) {
|
||||||
|
// Check if already used in unidirectional mode
|
||||||
|
if (unidirectional_channels_.find(channel) !=
|
||||||
|
unidirectional_channels_.end()) {
|
||||||
|
return RegistrationOutcome::kModeConflict;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& plugins = bidirectional_channels_[channel];
|
||||||
|
|
||||||
|
// Check if already registered
|
||||||
|
if (plugins.find(plugin) != plugins.end()) {
|
||||||
|
return RegistrationOutcome::kAlreadyRegistered;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check limit
|
||||||
|
if (plugins.size() >= 2) {
|
||||||
|
return RegistrationOutcome::kLimitReached;
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins.insert(plugin);
|
||||||
|
return RegistrationOutcome::kAdded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
void Unregister(const std::string& channel, WindowChannelPlugin* plugin) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
// Try unidirectional
|
||||||
|
auto uni_it = unidirectional_channels_.find(channel);
|
||||||
|
if (uni_it != unidirectional_channels_.end() &&
|
||||||
|
uni_it->second == plugin) {
|
||||||
|
unidirectional_channels_.erase(uni_it);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try bidirectional
|
||||||
|
auto bi_it = bidirectional_channels_.find(channel);
|
||||||
|
if (bi_it != bidirectional_channels_.end()) {
|
||||||
|
bi_it->second.erase(plugin);
|
||||||
|
if (bi_it->second.empty()) {
|
||||||
|
bidirectional_channels_.erase(bi_it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowChannelPlugin* GetTarget(const std::string& channel,
|
||||||
|
WindowChannelPlugin* from) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
// Check unidirectional - anyone can call
|
||||||
|
auto uni_it = unidirectional_channels_.find(channel);
|
||||||
|
if (uni_it != unidirectional_channels_.end()) {
|
||||||
|
return uni_it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check bidirectional - only peer can call
|
||||||
|
auto bi_it = bidirectional_channels_.find(channel);
|
||||||
|
if (bi_it != bidirectional_channels_.end()) {
|
||||||
|
const auto& plugins = bi_it->second;
|
||||||
|
|
||||||
|
// Check if caller is in the pair
|
||||||
|
if (plugins.find(from) == plugins.end()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the peer
|
||||||
|
for (auto* plugin : plugins) {
|
||||||
|
if (plugin != from) {
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasRegistrations(const std::string& channel) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
if (unidirectional_channels_.find(channel) !=
|
||||||
|
unidirectional_channels_.end()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto it = bidirectional_channels_.find(channel);
|
||||||
|
return it != bidirectional_channels_.end() && !it->second.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
ChannelRegistry() = default;
|
||||||
|
std::mutex mutex_;
|
||||||
|
std::map<std::string, WindowChannelPlugin*> unidirectional_channels_;
|
||||||
|
std::map<std::string, std::set<WindowChannelPlugin*>>
|
||||||
|
bidirectional_channels_;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void window_channel_plugin_dispose(GObject* object) {
|
||||||
|
WindowChannelPlugin* self = (WindowChannelPlugin*)object;
|
||||||
|
|
||||||
|
if (self->registered_channels) {
|
||||||
|
for (const auto& channel : *self->registered_channels) {
|
||||||
|
ChannelRegistry::GetInstance().Unregister(channel, self);
|
||||||
|
}
|
||||||
|
delete self->registered_channels;
|
||||||
|
self->registered_channels = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
G_OBJECT_CLASS(window_channel_plugin_parent_class)->dispose(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void window_channel_plugin_class_init(WindowChannelPluginClass* klass) {
|
||||||
|
G_OBJECT_CLASS(klass)->dispose = window_channel_plugin_dispose;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void window_channel_plugin_init(WindowChannelPlugin* self) {
|
||||||
|
self->registered_channels = new std::vector<std::string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
void window_channel_plugin_invoke_method(WindowChannelPlugin* self,
|
||||||
|
const gchar* channel,
|
||||||
|
FlValue* arguments,
|
||||||
|
FlMethodCall* method_call) {
|
||||||
|
// Check if this plugin has registered this channel
|
||||||
|
auto it = std::find(self->registered_channels->begin(),
|
||||||
|
self->registered_channels->end(), std::string(channel));
|
||||||
|
if (it == self->registered_channels->end()) {
|
||||||
|
g_autofree gchar* error_msg =
|
||||||
|
g_strdup_printf("channel %s not found in this engine", channel);
|
||||||
|
fl_method_call_respond_error(method_call, "CHANNEL_NOT_FOUND", error_msg,
|
||||||
|
nullptr, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fl_method_channel_invoke_method(self->channel, "methodCall", arguments,
|
||||||
|
nullptr,
|
||||||
|
+[](GObject* source_object, GAsyncResult* res,
|
||||||
|
gpointer user_data) {
|
||||||
|
auto* call = (FlMethodCall*)user_data;
|
||||||
|
GError* error = nullptr;
|
||||||
|
auto* result = fl_method_channel_invoke_method_finish(
|
||||||
|
FL_METHOD_CHANNEL(source_object), res,
|
||||||
|
&error);
|
||||||
|
if (error != nullptr) {
|
||||||
|
fl_method_call_respond_error(
|
||||||
|
call, "INVOKE_ERROR", error->message,
|
||||||
|
nullptr, nullptr);
|
||||||
|
g_error_free(error);
|
||||||
|
} else {
|
||||||
|
fl_method_call_respond(call, result,
|
||||||
|
nullptr);
|
||||||
|
}
|
||||||
|
g_object_unref(call);
|
||||||
|
},
|
||||||
|
g_object_ref(method_call));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_method_call(FlMethodChannel* channel,
|
||||||
|
FlMethodCall* method_call,
|
||||||
|
gpointer user_data) {
|
||||||
|
WindowChannelPlugin* self = (WindowChannelPlugin*)user_data;
|
||||||
|
|
||||||
|
const gchar* method = fl_method_call_get_name(method_call);
|
||||||
|
FlValue* args = fl_method_call_get_args(method_call);
|
||||||
|
|
||||||
|
if (strcmp(method, "registerMethodHandler") == 0) {
|
||||||
|
FlValue* channel_value = fl_value_lookup_string(args, "channel");
|
||||||
|
if (channel_value == nullptr ||
|
||||||
|
fl_value_get_type(channel_value) != FL_VALUE_TYPE_STRING) {
|
||||||
|
fl_method_call_respond_error(method_call, "INVALID_ARGUMENTS",
|
||||||
|
"channel is required", nullptr, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gchar* channel_name = fl_value_get_string(channel_value);
|
||||||
|
|
||||||
|
// Get mode (default to bidirectional)
|
||||||
|
ChannelMode mode = ChannelMode::kBidirectional;
|
||||||
|
FlValue* mode_value = fl_value_lookup_string(args, "mode");
|
||||||
|
if (mode_value != nullptr &&
|
||||||
|
fl_value_get_type(mode_value) == FL_VALUE_TYPE_STRING) {
|
||||||
|
const gchar* mode_str = fl_value_get_string(mode_value);
|
||||||
|
if (strcmp(mode_str, "unidirectional") == 0) {
|
||||||
|
mode = ChannelMode::kUnidirectional;
|
||||||
|
} else if (strcmp(mode_str, "bidirectional") == 0) {
|
||||||
|
mode = ChannelMode::kBidirectional;
|
||||||
|
} else {
|
||||||
|
g_autofree gchar* error_msg = g_strdup_printf(
|
||||||
|
"invalid mode: %s, must be 'unidirectional' or 'bidirectional'",
|
||||||
|
mode_str);
|
||||||
|
fl_method_call_respond_error(method_call, "INVALID_MODE", error_msg,
|
||||||
|
nullptr, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto outcome =
|
||||||
|
ChannelRegistry::GetInstance().Register(channel_name, self, mode);
|
||||||
|
|
||||||
|
switch (outcome) {
|
||||||
|
case RegistrationOutcome::kAdded:
|
||||||
|
self->registered_channels->push_back(channel_name);
|
||||||
|
fl_method_call_respond_success(method_call, nullptr, nullptr);
|
||||||
|
break;
|
||||||
|
case RegistrationOutcome::kAlreadyRegistered:
|
||||||
|
fl_method_call_respond_success(method_call, nullptr, nullptr);
|
||||||
|
break;
|
||||||
|
case RegistrationOutcome::kLimitReached: {
|
||||||
|
g_autofree gchar* error_msg;
|
||||||
|
if (mode == ChannelMode::kUnidirectional) {
|
||||||
|
error_msg = g_strdup_printf(
|
||||||
|
"channel %s already registered in unidirectional mode",
|
||||||
|
channel_name);
|
||||||
|
} else {
|
||||||
|
error_msg = g_strdup_printf(
|
||||||
|
"channel %s already has the maximum number of registrations (2)",
|
||||||
|
channel_name);
|
||||||
|
}
|
||||||
|
fl_method_call_respond_error(method_call, "CHANNEL_LIMIT_REACHED",
|
||||||
|
error_msg, nullptr, nullptr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case RegistrationOutcome::kModeConflict: {
|
||||||
|
g_autofree gchar* error_msg = g_strdup_printf(
|
||||||
|
"channel %s is already registered in a different mode",
|
||||||
|
channel_name);
|
||||||
|
fl_method_call_respond_error(method_call, "CHANNEL_MODE_CONFLICT",
|
||||||
|
error_msg, nullptr, nullptr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (strcmp(method, "unregisterMethodHandler") == 0) {
|
||||||
|
FlValue* channel_value = fl_value_lookup_string(args, "channel");
|
||||||
|
if (channel_value == nullptr ||
|
||||||
|
fl_value_get_type(channel_value) != FL_VALUE_TYPE_STRING) {
|
||||||
|
fl_method_call_respond_error(method_call, "INVALID_ARGUMENTS",
|
||||||
|
"channel is required", nullptr, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gchar* channel_name = fl_value_get_string(channel_value);
|
||||||
|
ChannelRegistry::GetInstance().Unregister(channel_name, self);
|
||||||
|
|
||||||
|
auto it = std::find(self->registered_channels->begin(),
|
||||||
|
self->registered_channels->end(),
|
||||||
|
std::string(channel_name));
|
||||||
|
if (it != self->registered_channels->end()) {
|
||||||
|
self->registered_channels->erase(it);
|
||||||
|
}
|
||||||
|
|
||||||
|
fl_method_call_respond_success(method_call, nullptr, nullptr);
|
||||||
|
} else if (strcmp(method, "invokeMethod") == 0) {
|
||||||
|
FlValue* channel_value = fl_value_lookup_string(args, "channel");
|
||||||
|
if (channel_value == nullptr ||
|
||||||
|
fl_value_get_type(channel_value) != FL_VALUE_TYPE_STRING) {
|
||||||
|
fl_method_call_respond_error(method_call, "INVALID_ARGUMENTS",
|
||||||
|
"channel is required", nullptr, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gchar* channel_name = fl_value_get_string(channel_value);
|
||||||
|
auto* target = ChannelRegistry::GetInstance().GetTarget(channel_name, self);
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
window_channel_plugin_invoke_method(target, channel_name, args,
|
||||||
|
method_call);
|
||||||
|
} else {
|
||||||
|
g_autofree gchar* error_msg;
|
||||||
|
if (ChannelRegistry::GetInstance().HasRegistrations(channel_name)) {
|
||||||
|
error_msg = g_strdup_printf(
|
||||||
|
"channel %s not accessible from this engine (may be bidirectional "
|
||||||
|
"pair or not registered)",
|
||||||
|
channel_name);
|
||||||
|
} else {
|
||||||
|
error_msg =
|
||||||
|
g_strdup_printf("unknown registered channel %s", channel_name);
|
||||||
|
}
|
||||||
|
fl_method_call_respond_error(method_call, "CHANNEL_UNREGISTERED",
|
||||||
|
error_msg, nullptr, nullptr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fl_method_call_respond_not_implemented(method_call, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void window_channel_plugin_register_with_registrar(
|
||||||
|
FlPluginRegistrar* registrar) {
|
||||||
|
WindowChannelPlugin* plugin = (WindowChannelPlugin*)g_object_new(
|
||||||
|
window_channel_plugin_get_type(), nullptr);
|
||||||
|
|
||||||
|
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
|
||||||
|
plugin->channel = fl_method_channel_new(
|
||||||
|
fl_plugin_registrar_get_messenger(registrar),
|
||||||
|
"mixin.one/desktop_multi_window/channels", FL_METHOD_CODEC(codec));
|
||||||
|
|
||||||
|
fl_method_channel_set_method_call_handler(plugin->channel, handle_method_call,
|
||||||
|
plugin, g_object_unref);
|
||||||
|
|
||||||
|
// Keep plugin alive - it will be cleaned up when the registrar is destroyed
|
||||||
|
g_object_ref(plugin);
|
||||||
|
}
|
||||||
18
third_party/desktop_multi_window/linux/window_channel_plugin.h
vendored
Normal file
18
third_party/desktop_multi_window/linux/window_channel_plugin.h
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#ifndef DESKTOP_MULTI_WINDOW_LINUX_WINDOW_CHANNEL_PLUGIN_H_
|
||||||
|
#define DESKTOP_MULTI_WINDOW_LINUX_WINDOW_CHANNEL_PLUGIN_H_
|
||||||
|
|
||||||
|
#include <flutter_linux/flutter_linux.h>
|
||||||
|
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
|
||||||
|
G_DECLARE_FINAL_TYPE(WindowChannelPlugin,
|
||||||
|
window_channel_plugin,
|
||||||
|
WINDOW,
|
||||||
|
CHANNEL_PLUGIN,
|
||||||
|
GObject)
|
||||||
|
|
||||||
|
void window_channel_plugin_register_with_registrar(FlPluginRegistrar* registrar);
|
||||||
|
|
||||||
|
G_END_DECLS
|
||||||
|
|
||||||
|
#endif // DESKTOP_MULTI_WINDOW_LINUX_WINDOW_CHANNEL_PLUGIN_H_
|
||||||
42
third_party/desktop_multi_window/linux/window_configuration.h
vendored
Normal file
42
third_party/desktop_multi_window/linux/window_configuration.h
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <flutter_linux/flutter_linux.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
struct WindowConfiguration {
|
||||||
|
std::string arguments;
|
||||||
|
bool hidden_at_launch = false;
|
||||||
|
|
||||||
|
static WindowConfiguration FromFlValue(FlValue* value) {
|
||||||
|
WindowConfiguration config;
|
||||||
|
|
||||||
|
if (!value || fl_value_get_type(value) != FL_VALUE_TYPE_MAP) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlValue* arguments_value = fl_value_lookup_string(value, "arguments");
|
||||||
|
if (arguments_value &&
|
||||||
|
fl_value_get_type(arguments_value) == FL_VALUE_TYPE_STRING) {
|
||||||
|
config.arguments = fl_value_get_string(arguments_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
FlValue* hidden_value = fl_value_lookup_string(value, "hiddenAtLaunch");
|
||||||
|
if (hidden_value && fl_value_get_type(hidden_value) == FL_VALUE_TYPE_BOOL) {
|
||||||
|
config.hidden_at_launch = fl_value_get_bool(hidden_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlValue* ToFlValue() const {
|
||||||
|
g_autoptr(FlValue) result = fl_value_new_map();
|
||||||
|
|
||||||
|
fl_value_set_string_take(result, "arguments",
|
||||||
|
fl_value_new_string(arguments.c_str()));
|
||||||
|
|
||||||
|
fl_value_set_string_take(result, "hiddenAtLaunch",
|
||||||
|
fl_value_new_bool(hidden_at_launch));
|
||||||
|
|
||||||
|
return fl_value_ref(result);
|
||||||
|
}
|
||||||
|
};
|
||||||
171
third_party/desktop_multi_window/macos/Classes/FlutterMultiWindowPlugin.swift
vendored
Normal file
171
third_party/desktop_multi_window/macos/Classes/FlutterMultiWindowPlugin.swift
vendored
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
import Cocoa
|
||||||
|
import FlutterMacOS
|
||||||
|
|
||||||
|
public class FlutterMultiWindowPlugin: NSObject, FlutterPlugin {
|
||||||
|
|
||||||
|
private let windowId: WindowId
|
||||||
|
private let windowArgument: String
|
||||||
|
|
||||||
|
|
||||||
|
init(window: FlutterWindow) {
|
||||||
|
self.windowId = window.windowId
|
||||||
|
self.windowArgument = window.windowArgument
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||||
|
guard let app = NSApplication.shared.delegate as? FlutterAppDelegate else {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
MultiWindowManager.shared.AttachWindow(window: window, registrar: registrar)
|
||||||
|
}
|
||||||
|
|
||||||
|
public typealias OnWindowCreatedCallback = (FlutterViewController) -> Void
|
||||||
|
static var onWindowCreatedCallback: OnWindowCreatedCallback?
|
||||||
|
|
||||||
|
public static func setOnWindowCreatedCallback(_ callback: @escaping OnWindowCreatedCallback) {
|
||||||
|
onWindowCreatedCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
|
let isWindowEvent = call.method.hasPrefix("window_")
|
||||||
|
if isWindowEvent {
|
||||||
|
let arguments = call.arguments as! [String: Any?]
|
||||||
|
let windowId = arguments["windowId"] as! WindowId
|
||||||
|
guard let window = MultiWindowManager.shared.windows[windowId] else {
|
||||||
|
result(
|
||||||
|
FlutterError(
|
||||||
|
code: "-1", message: "failed to find target window. \(windowId)",
|
||||||
|
details: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
window.handleWindowMethod(method: call.method, arguments: arguments, result: result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch call.method {
|
||||||
|
case "createWindow":
|
||||||
|
let arguments = call.arguments as! [String: Any?]
|
||||||
|
let windowId = MultiWindowManager.shared.CreateWindow(arguments: arguments)
|
||||||
|
result(windowId)
|
||||||
|
case "getWindowDefinition":
|
||||||
|
let definition: [String: Any] = [
|
||||||
|
"windowId": windowId,
|
||||||
|
"windowArgument": windowArgument,
|
||||||
|
]
|
||||||
|
result(definition)
|
||||||
|
case "getAllWindows":
|
||||||
|
let windows = MultiWindowManager.shared.getAllWindows()
|
||||||
|
result(windows)
|
||||||
|
default:
|
||||||
|
result(FlutterMethodNotImplemented)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MultiWindowManager: NSObject {
|
||||||
|
|
||||||
|
static let shared: MultiWindowManager = MultiWindowManager()
|
||||||
|
|
||||||
|
private override init() {}
|
||||||
|
|
||||||
|
var windows: [WindowId: FlutterWindow] = [:]
|
||||||
|
|
||||||
|
func AttachWindow(window: NSWindow, registrar: FlutterPluginRegistrar) {
|
||||||
|
// check window exists
|
||||||
|
for (_, flutterWindow) in windows {
|
||||||
|
if flutterWindow.window == window {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let windowId = WindowId.generate()
|
||||||
|
let flutterWindow = FlutterWindow(windowId: windowId, windowArgument: "", window: window)
|
||||||
|
windows[windowId] = flutterWindow
|
||||||
|
|
||||||
|
let channel = registerMultiWindowChannel(window: flutterWindow, with: registrar)
|
||||||
|
flutterWindow.setChannel(channel)
|
||||||
|
|
||||||
|
notifyWindowsChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateWindow(arguments: [String: Any?]) -> WindowId {
|
||||||
|
let windowId = WindowId.generate()
|
||||||
|
|
||||||
|
let config = WindowConfiguration.fromJson(arguments)
|
||||||
|
|
||||||
|
let window = CustomWindow(configuration: config)
|
||||||
|
|
||||||
|
let project = FlutterDartProject()
|
||||||
|
project.dartEntrypointArguments = ["multi_window", windowId, config.arguments]
|
||||||
|
let flutterViewController = FlutterViewController(project: project)
|
||||||
|
window.contentViewController = flutterViewController
|
||||||
|
window.setFrame(NSRect(x: 0, y: 0, width: 800, height: 600), display: true)
|
||||||
|
|
||||||
|
window.orderFront(nil)
|
||||||
|
window.setIsVisible(!config.hiddenAtLaunch)
|
||||||
|
|
||||||
|
FlutterMultiWindowPlugin.onWindowCreatedCallback?(flutterViewController)
|
||||||
|
|
||||||
|
let registrar = flutterViewController.registrar(forPlugin: "DesktopMultiWindowPlugin")
|
||||||
|
|
||||||
|
let flutterWindow = FlutterWindow(
|
||||||
|
windowId: windowId, windowArgument: config.arguments, window: window)
|
||||||
|
windows[windowId] = flutterWindow
|
||||||
|
|
||||||
|
let channel = registerMultiWindowChannel(window: flutterWindow, with: registrar)
|
||||||
|
flutterWindow.setChannel(channel)
|
||||||
|
|
||||||
|
notifyWindowsChanged()
|
||||||
|
|
||||||
|
return windowId
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeWindow(windowId: WindowId) {
|
||||||
|
if windows.removeValue(forKey: windowId) != nil {
|
||||||
|
notifyWindowsChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAllWindowIds() -> [WindowId] {
|
||||||
|
return Array(windows.keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAllWindows() -> [[String: String]] {
|
||||||
|
return windows.values.map { window in
|
||||||
|
[
|
||||||
|
"windowId": window.windowId,
|
||||||
|
"windowArgument": window.windowArgument,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func notifyWindowsChanged() {
|
||||||
|
for (_, window) in windows {
|
||||||
|
window.notifyWindowEvent("onWindowsChanged", data: [:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// register multi window method channel for all engine. include main or created by this plugin
|
||||||
|
private func registerMultiWindowChannel(
|
||||||
|
window: FlutterWindow, with registrar: FlutterPluginRegistrar
|
||||||
|
) -> FlutterMethodChannel {
|
||||||
|
let channel = FlutterMethodChannel(
|
||||||
|
name: "mixin.one/desktop_multi_window", binaryMessenger: registrar.messenger)
|
||||||
|
registrar.addMethodCallDelegate(FlutterMultiWindowPlugin(window: window), channel: channel)
|
||||||
|
|
||||||
|
// register window method channel plugin
|
||||||
|
WindowChannel.register(with: registrar)
|
||||||
|
|
||||||
|
return channel
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
147
third_party/desktop_multi_window/macos/Classes/FlutterWindow.swift
vendored
Normal file
147
third_party/desktop_multi_window/macos/Classes/FlutterWindow.swift
vendored
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
import Cocoa
|
||||||
|
import FlutterMacOS
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
typealias WindowId = String
|
||||||
|
|
||||||
|
extension WindowId {
|
||||||
|
static func generate() -> WindowId {
|
||||||
|
return UUID().uuidString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomWindow: NSWindow {
|
||||||
|
|
||||||
|
init(configuration: WindowConfiguration) {
|
||||||
|
super.init(
|
||||||
|
contentRect: NSRect(x: 10, y: 10, width: 800, height: 600),
|
||||||
|
styleMask: [.miniaturizable, .closable, .titled, .resizable], backing: .buffered,
|
||||||
|
defer: false)
|
||||||
|
|
||||||
|
self.isReleasedWhenClosed = false
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
debugPrint("Child window deinit")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class FlutterWindow: NSObject {
|
||||||
|
let windowId: WindowId
|
||||||
|
let windowArgument: String
|
||||||
|
private(set) var window: NSWindow
|
||||||
|
private var channel: FlutterMethodChannel?
|
||||||
|
|
||||||
|
private var willBecomeActiveObserver: NSObjectProtocol?
|
||||||
|
private var didResignActiveObserver: NSObjectProtocol?
|
||||||
|
private var closeObserver: NSObjectProtocol?
|
||||||
|
|
||||||
|
init(windowId: WindowId, windowArgument: String, window: NSWindow) {
|
||||||
|
self.windowId = windowId
|
||||||
|
self.windowArgument = windowArgument
|
||||||
|
self.window = window
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
willBecomeActiveObserver = NotificationCenter.default.addObserver(
|
||||||
|
forName: NSApplication.willBecomeActiveNotification,
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { [weak self] notification in
|
||||||
|
self?.didChangeOcclusionState(notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
didResignActiveObserver = NotificationCenter.default.addObserver(
|
||||||
|
forName: NSApplication.didResignActiveNotification,
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { [weak self] notification in
|
||||||
|
self?.didChangeOcclusionState(notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
closeObserver = NotificationCenter.default.addObserver(
|
||||||
|
forName: NSWindow.willCloseNotification, object: window, queue: .main
|
||||||
|
) { [windowId] _ in
|
||||||
|
MultiWindowManager.shared.removeWindow(windowId: windowId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if let willBecomeActiveObserver = willBecomeActiveObserver {
|
||||||
|
NotificationCenter.default.removeObserver(willBecomeActiveObserver)
|
||||||
|
}
|
||||||
|
if let didResignActiveObserver = didResignActiveObserver {
|
||||||
|
NotificationCenter.default.removeObserver(didResignActiveObserver)
|
||||||
|
}
|
||||||
|
if let closeObserver = closeObserver {
|
||||||
|
NotificationCenter.default.removeObserver(closeObserver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func didChangeOcclusionState(_ notification: Notification) {
|
||||||
|
if let controller = window.contentViewController as? FlutterViewController {
|
||||||
|
controller.engine.handleDidChangeOcclusionState(notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setChannel(_ channel: FlutterMethodChannel) {
|
||||||
|
self.channel = channel
|
||||||
|
}
|
||||||
|
|
||||||
|
func notifyWindowEvent(_ event: String, data: [String: Any]) {
|
||||||
|
if let channel = channel {
|
||||||
|
channel.invokeMethod(event, arguments: data)
|
||||||
|
} else {
|
||||||
|
debugPrint("Channel not set for window \(windowId), cannot notify event \(event)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleWindowMethod(method: String, arguments: Any?, result: @escaping FlutterResult) {
|
||||||
|
let args = arguments as? [String: Any?]
|
||||||
|
switch method {
|
||||||
|
case "window_show":
|
||||||
|
window.makeKeyAndOrderFront(nil)
|
||||||
|
window.setIsVisible(true)
|
||||||
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
result(nil)
|
||||||
|
case "window_hide":
|
||||||
|
window.orderOut(nil)
|
||||||
|
result(nil)
|
||||||
|
case "window_close":
|
||||||
|
window.close()
|
||||||
|
result(nil)
|
||||||
|
case "window_setFrame":
|
||||||
|
if let x = args?["x"] as? Double,
|
||||||
|
let y = args?["y"] as? Double,
|
||||||
|
let w = args?["width"] as? Double,
|
||||||
|
let h = args?["height"] as? Double
|
||||||
|
{
|
||||||
|
window.setFrame(NSRect(x: x, y: y, width: w, height: h), display: true)
|
||||||
|
}
|
||||||
|
result(nil)
|
||||||
|
case "window_coverScreen":
|
||||||
|
// Make this window a borderless surface that fills an entire screen —
|
||||||
|
// used to show the audience slide fullscreen on the beamer while the
|
||||||
|
// main (presenter) window stays on the laptop. We deliberately do not
|
||||||
|
// make it the key window, so the keyboard stays with the presenter.
|
||||||
|
let external = (args?["external"] as? Bool) ?? true
|
||||||
|
let screens = NSScreen.screens
|
||||||
|
var target: NSScreen? = NSScreen.main
|
||||||
|
if external, let ext = screens.first(where: { $0 != NSScreen.main }) {
|
||||||
|
target = ext
|
||||||
|
}
|
||||||
|
if let screen = target ?? screens.first {
|
||||||
|
window.styleMask = [.borderless]
|
||||||
|
window.level = .normal
|
||||||
|
window.isOpaque = true
|
||||||
|
window.setFrame(screen.frame, display: true)
|
||||||
|
window.orderFrontRegardless()
|
||||||
|
window.setIsVisible(true)
|
||||||
|
}
|
||||||
|
result(nil)
|
||||||
|
default:
|
||||||
|
result(FlutterError(code: "-1", message: "unknown method \(method)", details: nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
281
third_party/desktop_multi_window/macos/Classes/WindowChannel.swift
vendored
Normal file
281
third_party/desktop_multi_window/macos/Classes/WindowChannel.swift
vendored
Normal file
|
|
@ -0,0 +1,281 @@
|
||||||
|
//
|
||||||
|
// WindowChannel.swift
|
||||||
|
// desktop_multi_window
|
||||||
|
//
|
||||||
|
// Created by Bin Yang on 2022/1/28.
|
||||||
|
//
|
||||||
|
|
||||||
|
import FlutterMacOS
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
typealias ChannelId = String
|
||||||
|
|
||||||
|
/// Channel communication mode
|
||||||
|
enum ChannelMode: String {
|
||||||
|
/// Unidirectional mode: All engines can invoke this channel
|
||||||
|
case unidirectional = "unidirectional"
|
||||||
|
/// Bidirectional mode: Only paired engines can invoke each other
|
||||||
|
case bidirectional = "bidirectional"
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ChannelRegistry {
|
||||||
|
static let shared = ChannelRegistry()
|
||||||
|
|
||||||
|
private let lock = NSLock()
|
||||||
|
|
||||||
|
// Unidirectional channels: channel -> single window
|
||||||
|
private var unidirectionalChannels = [String: WeakBox<WindowChannel>]()
|
||||||
|
|
||||||
|
// Bidirectional channels: channel -> pair of windows
|
||||||
|
private var bidirectionalChannels = [String: NSHashTable<AnyObject>]()
|
||||||
|
|
||||||
|
enum RegistrationOutcome {
|
||||||
|
case added
|
||||||
|
case alreadyRegistered
|
||||||
|
case limitReached
|
||||||
|
case modeConflict
|
||||||
|
}
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
// Helper class to wrap weak reference
|
||||||
|
private class WeakBox<T: AnyObject> {
|
||||||
|
weak var value: T?
|
||||||
|
init(_ value: T) {
|
||||||
|
self.value = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func register(_ channel: String, window: WindowChannel, mode: ChannelMode) -> RegistrationOutcome {
|
||||||
|
lock.lock(); defer { lock.unlock() }
|
||||||
|
|
||||||
|
switch mode {
|
||||||
|
case .unidirectional:
|
||||||
|
return registerUnidirectional(channel, window: window)
|
||||||
|
case .bidirectional:
|
||||||
|
return registerBidirectional(channel, window: window)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func registerUnidirectional(_ channel: String, window: WindowChannel) -> RegistrationOutcome {
|
||||||
|
// Check if channel is already used in bidirectional mode
|
||||||
|
if bidirectionalChannels[channel] != nil {
|
||||||
|
return .modeConflict
|
||||||
|
}
|
||||||
|
|
||||||
|
if let existing = unidirectionalChannels[channel]?.value {
|
||||||
|
if existing === window {
|
||||||
|
return .alreadyRegistered
|
||||||
|
}
|
||||||
|
// Already registered by another window
|
||||||
|
return .limitReached
|
||||||
|
}
|
||||||
|
|
||||||
|
unidirectionalChannels[channel] = WeakBox(window)
|
||||||
|
return .added
|
||||||
|
}
|
||||||
|
|
||||||
|
private func registerBidirectional(_ channel: String, window: WindowChannel) -> RegistrationOutcome {
|
||||||
|
// Check if channel is already used in unidirectional mode
|
||||||
|
if unidirectionalChannels[channel] != nil {
|
||||||
|
return .modeConflict
|
||||||
|
}
|
||||||
|
|
||||||
|
let table: NSHashTable<AnyObject>
|
||||||
|
if let existing = bidirectionalChannels[channel] {
|
||||||
|
table = existing
|
||||||
|
} else {
|
||||||
|
table = NSHashTable<AnyObject>.weakObjects()
|
||||||
|
bidirectionalChannels[channel] = table
|
||||||
|
}
|
||||||
|
|
||||||
|
let activeWindows = table.allObjects.compactMap { $0 as? WindowChannel }
|
||||||
|
|
||||||
|
if activeWindows.contains(where: { $0 === window }) {
|
||||||
|
return .alreadyRegistered
|
||||||
|
}
|
||||||
|
|
||||||
|
if activeWindows.count >= 2 {
|
||||||
|
return .limitReached
|
||||||
|
}
|
||||||
|
|
||||||
|
table.add(window)
|
||||||
|
return .added
|
||||||
|
}
|
||||||
|
|
||||||
|
func unregister(_ channel: String, window: WindowChannel) {
|
||||||
|
lock.lock(); defer { lock.unlock() }
|
||||||
|
|
||||||
|
// Try unidirectional
|
||||||
|
if let existing = unidirectionalChannels[channel]?.value, existing === window {
|
||||||
|
unidirectionalChannels.removeValue(forKey: channel)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try bidirectional
|
||||||
|
if let table = bidirectionalChannels[channel] {
|
||||||
|
table.remove(window)
|
||||||
|
if table.allObjects.isEmpty {
|
||||||
|
bidirectionalChannels.removeValue(forKey: channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTarget(for channel: String, from window: WindowChannel) -> WindowChannel? {
|
||||||
|
lock.lock(); defer { lock.unlock() }
|
||||||
|
|
||||||
|
// Check unidirectional
|
||||||
|
if let target = unidirectionalChannels[channel]?.value {
|
||||||
|
// Anyone can call unidirectional channel
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check bidirectional - only peer can call
|
||||||
|
if let table = bidirectionalChannels[channel] {
|
||||||
|
let candidates = table.allObjects.compactMap { $0 as? WindowChannel }
|
||||||
|
if candidates.isEmpty {
|
||||||
|
bidirectionalChannels.removeValue(forKey: channel)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if caller is in the pair
|
||||||
|
guard candidates.contains(where: { $0 === window }) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the peer
|
||||||
|
return candidates.first { $0 !== window }
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasRegistrations(for channel: String) -> Bool {
|
||||||
|
lock.lock(); defer { lock.unlock() }
|
||||||
|
|
||||||
|
if let box = unidirectionalChannels[channel], box.value != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if let table = bidirectionalChannels[channel] {
|
||||||
|
let hasActive = !table.allObjects.isEmpty
|
||||||
|
if !hasActive {
|
||||||
|
bidirectionalChannels.removeValue(forKey: channel)
|
||||||
|
}
|
||||||
|
return hasActive
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class WindowChannel: NSObject, FlutterPlugin {
|
||||||
|
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||||
|
let channel = FlutterMethodChannel(
|
||||||
|
name: "mixin.one/desktop_multi_window/channels", binaryMessenger: registrar.messenger)
|
||||||
|
let instance = WindowChannel(methodChannel: channel)
|
||||||
|
registrar.addMethodCallDelegate(instance, channel: channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(methodChannel: FlutterMethodChannel) {
|
||||||
|
self.methodChannel = methodChannel
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
private let methodChannel: FlutterMethodChannel
|
||||||
|
|
||||||
|
private var methodChannels: [String] = []
|
||||||
|
|
||||||
|
func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
|
switch call.method {
|
||||||
|
case "registerMethodHandler":
|
||||||
|
let arguments = call.arguments as! [String: Any?]
|
||||||
|
let channel = arguments["channel"] as! String
|
||||||
|
let modeString = arguments["mode"] as? String ?? "bidirectional"
|
||||||
|
|
||||||
|
guard let mode = ChannelMode(rawValue: modeString) else {
|
||||||
|
result(
|
||||||
|
FlutterError(
|
||||||
|
code: "INVALID_MODE",
|
||||||
|
message: "invalid mode: \(modeString), must be 'unidirectional' or 'bidirectional'",
|
||||||
|
details: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let outcome = ChannelRegistry.shared.register(channel, window: self, mode: mode)
|
||||||
|
switch outcome {
|
||||||
|
case .added:
|
||||||
|
methodChannels.append(channel)
|
||||||
|
result(nil)
|
||||||
|
case .alreadyRegistered:
|
||||||
|
result(nil)
|
||||||
|
case .limitReached:
|
||||||
|
let message = mode == .unidirectional
|
||||||
|
? "channel \(channel) already registered in unidirectional mode"
|
||||||
|
: "channel \(channel) already has the maximum number of registrations (2)"
|
||||||
|
result(
|
||||||
|
FlutterError(
|
||||||
|
code: "CHANNEL_LIMIT_REACHED",
|
||||||
|
message: message,
|
||||||
|
details: nil))
|
||||||
|
case .modeConflict:
|
||||||
|
result(
|
||||||
|
FlutterError(
|
||||||
|
code: "CHANNEL_MODE_CONFLICT",
|
||||||
|
message: "channel \(channel) is already registered in a different mode",
|
||||||
|
details: nil))
|
||||||
|
}
|
||||||
|
case "unregisterMethodHandler":
|
||||||
|
let arguments = call.arguments as! [String: Any?]
|
||||||
|
let channel = arguments["channel"] as! String
|
||||||
|
|
||||||
|
ChannelRegistry.shared.unregister(channel, window: self)
|
||||||
|
|
||||||
|
if let index = methodChannels.firstIndex(of: channel) {
|
||||||
|
methodChannels.remove(at: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
result(nil)
|
||||||
|
case "invokeMethod":
|
||||||
|
let arguments = call.arguments as! [String: Any?]
|
||||||
|
let channel = arguments["channel"] as! String
|
||||||
|
|
||||||
|
if let targetChannel = ChannelRegistry.shared.getTarget(for: channel, from: self) {
|
||||||
|
targetChannel.invokeMethod(channel: channel, arguments: call.arguments, result: result)
|
||||||
|
} else {
|
||||||
|
let message: String
|
||||||
|
if ChannelRegistry.shared.hasRegistrations(for: channel) {
|
||||||
|
message = "channel \(channel) not accessible from this engine (may be bidirectional pair or not registered)"
|
||||||
|
} else {
|
||||||
|
message = "unknown registered channel \(channel)"
|
||||||
|
}
|
||||||
|
result(
|
||||||
|
FlutterError(
|
||||||
|
code: "CHANNEL_UNREGISTERED", message: message,
|
||||||
|
details: nil))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result(FlutterMethodNotImplemented)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func invokeMethod(channel: String, arguments: Any?, result: @escaping FlutterResult) {
|
||||||
|
// check channelIds contains channel
|
||||||
|
if !methodChannels.contains(channel) {
|
||||||
|
result(
|
||||||
|
FlutterError(
|
||||||
|
code: "CHANNEL_NOT_FOUND", message: "channel \(channel) not found in this engine",
|
||||||
|
details: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
methodChannel.invokeMethod("methodCall", arguments: arguments, result: result)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
for channel in methodChannels {
|
||||||
|
ChannelRegistry.shared.unregister(channel, window: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
third_party/desktop_multi_window/macos/Classes/WindowConfiguration.swift
vendored
Normal file
51
third_party/desktop_multi_window/macos/Classes/WindowConfiguration.swift
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import Foundation
|
||||||
|
import Cocoa
|
||||||
|
|
||||||
|
|
||||||
|
struct WindowConfiguration: Codable {
|
||||||
|
|
||||||
|
let arguments: String
|
||||||
|
let hiddenAtLaunch: Bool
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case arguments
|
||||||
|
case hiddenAtLaunch
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
arguments = try container.decodeIfPresent(String.self, forKey: .arguments) ?? ""
|
||||||
|
hiddenAtLaunch = try container.decodeIfPresent(Bool.self, forKey: .hiddenAtLaunch) ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
init(arguments: String, hiddenAtLaunch: Bool) {
|
||||||
|
self.arguments = arguments
|
||||||
|
self.hiddenAtLaunch = hiddenAtLaunch
|
||||||
|
}
|
||||||
|
|
||||||
|
static let defaultConfiguration = WindowConfiguration(
|
||||||
|
arguments: "",
|
||||||
|
hiddenAtLaunch: false
|
||||||
|
)
|
||||||
|
|
||||||
|
static func fromJson(_ json: [String: Any?]) -> WindowConfiguration {
|
||||||
|
guard let jsonData = try? JSONSerialization.data(withJSONObject: json, options: []) else {
|
||||||
|
debugPrint("invalid json object: \(json)")
|
||||||
|
return defaultConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
return try decoder.decode(WindowConfiguration.self, from: jsonData)
|
||||||
|
} catch {
|
||||||
|
debugPrint("Failed to parse window configuration: \(error)")
|
||||||
|
return defaultConfiguration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(arguments, forKey: .arguments)
|
||||||
|
try container.encode(hiddenAtLaunch, forKey: .hiddenAtLaunch)
|
||||||
|
}
|
||||||
|
}
|
||||||
22
third_party/desktop_multi_window/macos/desktop_multi_window.podspec
vendored
Normal file
22
third_party/desktop_multi_window/macos/desktop_multi_window.podspec
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
#
|
||||||
|
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
|
||||||
|
# Run `pod lib lint flutter_multi_window.podspec` to validate before publishing.
|
||||||
|
#
|
||||||
|
Pod::Spec.new do |s|
|
||||||
|
s.name = 'desktop_multi_window'
|
||||||
|
s.version = '0.0.1'
|
||||||
|
s.summary = 'A new flutter plugin project.'
|
||||||
|
s.description = <<-DESC
|
||||||
|
A new flutter plugin project.
|
||||||
|
DESC
|
||||||
|
s.homepage = 'http://example.com'
|
||||||
|
s.license = { :file => '../LICENSE' }
|
||||||
|
s.author = { 'Your Company' => 'email@example.com' }
|
||||||
|
s.source = { :path => '.' }
|
||||||
|
s.source_files = 'Classes/**/*'
|
||||||
|
s.dependency 'FlutterMacOS'
|
||||||
|
|
||||||
|
s.platform = :osx, '10.11'
|
||||||
|
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
|
||||||
|
s.swift_version = '5.0'
|
||||||
|
end
|
||||||
28
third_party/desktop_multi_window/pubspec.yaml
vendored
Normal file
28
third_party/desktop_multi_window/pubspec.yaml
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
name: desktop_multi_window
|
||||||
|
description: A flutter plugin that create and manager multi window in desktop.
|
||||||
|
version: 0.3.0
|
||||||
|
homepage: https://github.com/MixinNetwork/flutter-plugins/tree/main/packages/desktop_multi_window
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ">=3.0.0 <4.0.0"
|
||||||
|
flutter: ">=3.0.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_lints: ^2.0.0
|
||||||
|
|
||||||
|
# The following section is specific to Flutter.
|
||||||
|
flutter:
|
||||||
|
plugin:
|
||||||
|
platforms:
|
||||||
|
macos:
|
||||||
|
pluginClass: FlutterMultiWindowPlugin
|
||||||
|
windows:
|
||||||
|
pluginClass: DesktopMultiWindowPlugin
|
||||||
|
linux:
|
||||||
|
pluginClass: DesktopMultiWindowPlugin
|
||||||
29
third_party/desktop_multi_window/windows/CMakeLists.txt
vendored
Normal file
29
third_party/desktop_multi_window/windows/CMakeLists.txt
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
set(PROJECT_NAME "desktop_multi_window")
|
||||||
|
project(${PROJECT_NAME} LANGUAGES CXX)
|
||||||
|
|
||||||
|
# This value is used when generating builds using this plugin, so it must
|
||||||
|
# not be changed
|
||||||
|
set(PLUGIN_NAME "desktop_multi_window_plugin")
|
||||||
|
|
||||||
|
add_library(${PLUGIN_NAME} SHARED
|
||||||
|
"desktop_multi_window_plugin.cpp"
|
||||||
|
"multi_window_manager.cc"
|
||||||
|
"flutter_window.cc"
|
||||||
|
"window_channel_plugin.cc"
|
||||||
|
"win32_window.cpp"
|
||||||
|
)
|
||||||
|
apply_standard_settings(${PLUGIN_NAME})
|
||||||
|
set_target_properties(${PLUGIN_NAME} PROPERTIES
|
||||||
|
CXX_VISIBILITY_PRESET hidden)
|
||||||
|
target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
|
||||||
|
target_include_directories(${PLUGIN_NAME} INTERFACE
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/include")
|
||||||
|
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin flutter_wrapper_app)
|
||||||
|
target_link_libraries(${PLUGIN_NAME} PRIVATE "dwmapi.lib")
|
||||||
|
|
||||||
|
# List of absolute paths to libraries that should be bundled with the plugin
|
||||||
|
set(desktop_multi_window_bundled_libraries
|
||||||
|
""
|
||||||
|
PARENT_SCOPE
|
||||||
|
)
|
||||||
118
third_party/desktop_multi_window/windows/desktop_multi_window_plugin.cpp
vendored
Normal file
118
third_party/desktop_multi_window/windows/desktop_multi_window_plugin.cpp
vendored
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
#include "include/desktop_multi_window/desktop_multi_window_plugin.h"
|
||||||
|
#include "multi_window_plugin_internal.h"
|
||||||
|
|
||||||
|
#include <flutter/method_channel.h>
|
||||||
|
#include <flutter/plugin_registrar_windows.h>
|
||||||
|
#include <flutter/standard_method_codec.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "flutter_window_wrapper.h"
|
||||||
|
#include "multi_window_manager.h"
|
||||||
|
#include "window_channel_plugin.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class DesktopMultiWindowPlugin : public flutter::Plugin {
|
||||||
|
public:
|
||||||
|
DesktopMultiWindowPlugin(FlutterWindowWrapper* window,
|
||||||
|
flutter::PluginRegistrarWindows* registrar);
|
||||||
|
|
||||||
|
~DesktopMultiWindowPlugin() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void HandleMethodCall(
|
||||||
|
const flutter::MethodCall<flutter::EncodableValue>& method_call,
|
||||||
|
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
||||||
|
|
||||||
|
FlutterWindowWrapper* window_;
|
||||||
|
flutter::PluginRegistrarWindows* registrar_;
|
||||||
|
};
|
||||||
|
|
||||||
|
DesktopMultiWindowPlugin::DesktopMultiWindowPlugin(
|
||||||
|
FlutterWindowWrapper* window,
|
||||||
|
flutter::PluginRegistrarWindows* registrar)
|
||||||
|
: window_(window), registrar_(registrar) {
|
||||||
|
auto channel =
|
||||||
|
std::make_shared<flutter::MethodChannel<flutter::EncodableValue>>(
|
||||||
|
registrar->messenger(), "mixin.one/desktop_multi_window",
|
||||||
|
&flutter::StandardMethodCodec::GetInstance());
|
||||||
|
channel->SetMethodCallHandler([this](const auto& call, auto result) {
|
||||||
|
HandleMethodCall(call, std::move(result));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set channel to window for event notifications
|
||||||
|
window_->SetChannel(channel);
|
||||||
|
|
||||||
|
// Register WindowChannel plugin for each engine
|
||||||
|
WindowChannelPluginRegisterWithRegistrar(registrar);
|
||||||
|
}
|
||||||
|
|
||||||
|
DesktopMultiWindowPlugin::~DesktopMultiWindowPlugin() {
|
||||||
|
MultiWindowManager::Instance()->RemoveWindow(window_->GetWindowId());
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopMultiWindowPlugin::HandleMethodCall(
|
||||||
|
const flutter::MethodCall<flutter::EncodableValue>& method_call,
|
||||||
|
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
||||||
|
// Check if this is a window-specific method (starts with "window_")
|
||||||
|
const auto& method = method_call.method_name();
|
||||||
|
if (method.rfind("window_", 0) == 0) {
|
||||||
|
auto* arguments =
|
||||||
|
std::get_if<flutter::EncodableMap>(method_call.arguments());
|
||||||
|
auto window_id = std::get<std::string>(
|
||||||
|
arguments->at(flutter::EncodableValue("windowId")));
|
||||||
|
|
||||||
|
auto window = MultiWindowManager::Instance()->GetWindow(window_id);
|
||||||
|
if (!window) {
|
||||||
|
result->Error("-1", "failed to find target window: " + window_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window->HandleWindowMethod(method, arguments, std::move(result));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method == "createWindow") {
|
||||||
|
auto args = std::get_if<flutter::EncodableMap>(method_call.arguments());
|
||||||
|
auto window_id = MultiWindowManager::Instance()->Create(args);
|
||||||
|
result->Success(flutter::EncodableValue(window_id));
|
||||||
|
return;
|
||||||
|
} else if (method == "getWindowDefinition") {
|
||||||
|
flutter::EncodableMap definition;
|
||||||
|
definition[flutter::EncodableValue("windowId")] =
|
||||||
|
flutter::EncodableValue(window_->GetWindowId());
|
||||||
|
definition[flutter::EncodableValue("windowArgument")] =
|
||||||
|
flutter::EncodableValue(window_->GetWindowArgument());
|
||||||
|
result->Success(flutter::EncodableValue(definition));
|
||||||
|
return;
|
||||||
|
} else if (method == "getAllWindows") {
|
||||||
|
auto windows = MultiWindowManager::Instance()->GetAllWindows();
|
||||||
|
result->Success(flutter::EncodableValue(windows));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result->NotImplemented();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void DesktopMultiWindowPluginRegisterWithRegistrar(
|
||||||
|
FlutterDesktopPluginRegistrarRef registrar) {
|
||||||
|
// Attach MainWindow
|
||||||
|
auto hwnd = FlutterDesktopViewGetHWND(
|
||||||
|
FlutterDesktopPluginRegistrarGetView(registrar));
|
||||||
|
MultiWindowManager::Instance()->AttachFlutterMainWindow(
|
||||||
|
GetAncestor(hwnd, GA_ROOT), registrar);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InternalMultiWindowPluginRegisterWithRegistrar(
|
||||||
|
FlutterDesktopPluginRegistrarRef registrar,
|
||||||
|
FlutterWindowWrapper* window) {
|
||||||
|
auto plugin_registrar =
|
||||||
|
flutter::PluginRegistrarManager::GetInstance()
|
||||||
|
->GetRegistrar<flutter::PluginRegistrarWindows>(registrar);
|
||||||
|
auto plugin =
|
||||||
|
std::make_unique<DesktopMultiWindowPlugin>(window, plugin_registrar);
|
||||||
|
plugin_registrar->AddPlugin(std::move(plugin));
|
||||||
|
}
|
||||||
71
third_party/desktop_multi_window/windows/flutter_window.cc
vendored
Normal file
71
third_party/desktop_multi_window/windows/flutter_window.cc
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
#include "flutter_window.h"
|
||||||
|
|
||||||
|
#include "flutter_windows.h"
|
||||||
|
|
||||||
|
#include "tchar.h"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "multi_window_manager.h"
|
||||||
|
#include "multi_window_plugin_internal.h"
|
||||||
|
|
||||||
|
FlutterWindow::FlutterWindow(const std::string& id,
|
||||||
|
const WindowConfiguration config)
|
||||||
|
: id_(id), window_argument_(config.arguments) {}
|
||||||
|
|
||||||
|
bool FlutterWindow::OnCreate() {
|
||||||
|
// Called when the window is created
|
||||||
|
RECT frame = GetClientArea();
|
||||||
|
|
||||||
|
flutter::DartProject project(L"data");
|
||||||
|
std::vector<std::string> entrypoint_args = {"multi_window", id_,
|
||||||
|
window_argument_};
|
||||||
|
project.set_dart_entrypoint_arguments(entrypoint_args);
|
||||||
|
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
|
||||||
|
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;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto view_handle = flutter_controller_->view()->GetNativeWindow();
|
||||||
|
SetChildContent(view_handle);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlutterWindow::OnDestroy() {
|
||||||
|
if (flutter_controller_) {
|
||||||
|
flutter_controller_ = nullptr;
|
||||||
|
}
|
||||||
|
MultiWindowManager::Instance()->RemoveManagedFlutterWindowLater(id_);
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT FlutterWindow::MessageHandler(HWND hwnd,
|
||||||
|
UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept {
|
||||||
|
// Give Flutter, including plugins, an opportunity to handle window messages.
|
||||||
|
if (flutter_controller_) {
|
||||||
|
std::optional<LRESULT> result =
|
||||||
|
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
|
||||||
|
lparam);
|
||||||
|
if (result) {
|
||||||
|
return *result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (message) {
|
||||||
|
case WM_FONTCHANGE:
|
||||||
|
flutter_controller_->engine()->ReloadSystemFonts();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
|
||||||
|
}
|
||||||
|
|
||||||
|
FlutterWindow::~FlutterWindow() {
|
||||||
|
// Cleanup is handled by Win32Window::Destroy()
|
||||||
|
}
|
||||||
44
third_party/desktop_multi_window/windows/flutter_window.h
vendored
Normal file
44
third_party/desktop_multi_window/windows/flutter_window.h
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
#ifndef DESKTOP_MULTI_WINDOW_WINDOWS_FLUTTER_WINDOW_H_
|
||||||
|
#define DESKTOP_MULTI_WINDOW_WINDOWS_FLUTTER_WINDOW_H_
|
||||||
|
|
||||||
|
#include <Windows.h>
|
||||||
|
|
||||||
|
#include <flutter/flutter_view_controller.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "win32_window.h"
|
||||||
|
#include "window_configuration.h"
|
||||||
|
|
||||||
|
class FlutterWindow : public Win32Window {
|
||||||
|
public:
|
||||||
|
FlutterWindow(const std::string& id, const WindowConfiguration config);
|
||||||
|
~FlutterWindow() override;
|
||||||
|
|
||||||
|
std::string GetWindowId() const { return id_; }
|
||||||
|
|
||||||
|
std::string GetWindowArgument() const { return window_argument_; }
|
||||||
|
|
||||||
|
flutter::FlutterViewController* GetFlutterViewController() const {
|
||||||
|
return flutter_controller_.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Win32Window overrides
|
||||||
|
bool OnCreate() override;
|
||||||
|
void OnDestroy() override;
|
||||||
|
LRESULT MessageHandler(HWND hwnd,
|
||||||
|
UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string id_;
|
||||||
|
std::string window_argument_;
|
||||||
|
|
||||||
|
// The Flutter instance hosted by this window.
|
||||||
|
std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DESKTOP_MULTI_WINDOW_WINDOWS_FLUTTER_WINDOW_H_
|
||||||
69
third_party/desktop_multi_window/windows/flutter_window_wrapper.h
vendored
Normal file
69
third_party/desktop_multi_window/windows/flutter_window_wrapper.h
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
#ifndef DESKTOP_MULTI_WINDOW_WINDOWS_FLUTTER_WINDOW_WRAPPER_H_
|
||||||
|
#define DESKTOP_MULTI_WINDOW_WINDOWS_FLUTTER_WINDOW_WRAPPER_H_
|
||||||
|
|
||||||
|
#include <Windows.h>
|
||||||
|
#include <flutter/encodable_value.h>
|
||||||
|
#include <flutter/method_channel.h>
|
||||||
|
#include <flutter/method_result.h>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class FlutterWindowWrapper {
|
||||||
|
public:
|
||||||
|
FlutterWindowWrapper(const std::string& window_id,
|
||||||
|
HWND hwnd,
|
||||||
|
const std::string& window_argument = "")
|
||||||
|
: window_id_(window_id), hwnd_(hwnd), window_argument_(window_argument) {}
|
||||||
|
|
||||||
|
~FlutterWindowWrapper() = default;
|
||||||
|
|
||||||
|
std::string GetWindowId() const { return window_id_; }
|
||||||
|
|
||||||
|
std::string GetWindowArgument() const { return window_argument_; }
|
||||||
|
|
||||||
|
HWND GetWindowHandle() { return hwnd_; }
|
||||||
|
|
||||||
|
void SetChannel(
|
||||||
|
std::shared_ptr<flutter::MethodChannel<flutter::EncodableValue>>
|
||||||
|
channel) {
|
||||||
|
channel_ = channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NotifyWindowEvent(const std::string& event,
|
||||||
|
const flutter::EncodableMap& data) {
|
||||||
|
if (channel_) {
|
||||||
|
channel_->InvokeMethod(event,
|
||||||
|
std::make_unique<flutter::EncodableValue>(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleWindowMethod(
|
||||||
|
const std::string& method,
|
||||||
|
const flutter::EncodableMap* arguments,
|
||||||
|
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
|
||||||
|
if (method == "window_show") {
|
||||||
|
if (hwnd_) {
|
||||||
|
::ShowWindow(hwnd_, SW_SHOW);
|
||||||
|
}
|
||||||
|
result->Success();
|
||||||
|
} else if (method == "window_hide") {
|
||||||
|
if (hwnd_) {
|
||||||
|
::ShowWindow(hwnd_, SW_HIDE);
|
||||||
|
}
|
||||||
|
result->Success();
|
||||||
|
} else {
|
||||||
|
result->Error("-1", "unknown method: " + method);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void SetWindowHandle(HWND hwnd) { hwnd_ = hwnd; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string window_id_;
|
||||||
|
HWND hwnd_;
|
||||||
|
std::string window_argument_;
|
||||||
|
std::shared_ptr<flutter::MethodChannel<flutter::EncodableValue>> channel_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DESKTOP_MULTI_WINDOW_WINDOWS_FLUTTER_WINDOW_WRAPPER_H_
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
#ifndef FLUTTER_PLUGIN_DESKTOP_MULTI_WINDOW_PLUGIN_H_
|
||||||
|
#define FLUTTER_PLUGIN_DESKTOP_MULTI_WINDOW_PLUGIN_H_
|
||||||
|
|
||||||
|
#include <flutter_plugin_registrar.h>
|
||||||
|
|
||||||
|
#ifdef FLUTTER_PLUGIN_IMPL
|
||||||
|
#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport)
|
||||||
|
#else
|
||||||
|
#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(__cplusplus)
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
#if defined(__cplusplus)
|
||||||
|
} // extern "C"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // FLUTTER_PLUGIN_DESKTOP_MULTI_WINDOW_PLUGIN_H_
|
||||||
190
third_party/desktop_multi_window/windows/multi_window_manager.cc
vendored
Normal file
190
third_party/desktop_multi_window/windows/multi_window_manager.cc
vendored
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
#include "multi_window_manager.h"
|
||||||
|
|
||||||
|
#include <rpc.h>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <memory>
|
||||||
|
#include <random>
|
||||||
|
#include <sstream>
|
||||||
|
#pragma comment(lib, "rpcrt4.lib")
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include "flutter_window.h"
|
||||||
|
#include "flutter_window_wrapper.h"
|
||||||
|
#include "include/desktop_multi_window/desktop_multi_window_plugin.h"
|
||||||
|
#include "multi_window_plugin_internal.h"
|
||||||
|
#include "win32_window.h"
|
||||||
|
#include "window_configuration.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string GenerateWindowId() {
|
||||||
|
UUID uuid;
|
||||||
|
UuidCreate(&uuid);
|
||||||
|
|
||||||
|
RPC_CSTR uuid_str = nullptr;
|
||||||
|
UuidToStringA(&uuid, &uuid_str);
|
||||||
|
|
||||||
|
std::string result(reinterpret_cast<char*>(uuid_str));
|
||||||
|
RpcStringFreeA(&uuid_str);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowCreatedCallback _g_window_created_callback = nullptr;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// static
|
||||||
|
MultiWindowManager* MultiWindowManager::Instance() {
|
||||||
|
static auto manager = std::make_shared<MultiWindowManager>();
|
||||||
|
return manager.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiWindowManager::MultiWindowManager() : windows_() {}
|
||||||
|
|
||||||
|
std::string MultiWindowManager::Create(const flutter::EncodableMap* args) {
|
||||||
|
std::string window_id = GenerateWindowId();
|
||||||
|
WindowConfiguration config = WindowConfiguration::FromEncodableMap(args);
|
||||||
|
|
||||||
|
auto flutter_window = std::make_unique<FlutterWindow>(window_id, config);
|
||||||
|
|
||||||
|
std::wstring title = L"";
|
||||||
|
Win32Window::Point origin(10, 10);
|
||||||
|
Win32Window::Size size(800, 600);
|
||||||
|
|
||||||
|
if (!flutter_window->Create(title, origin, size)) {
|
||||||
|
std::cerr << "Failed to create window." << std::endl;
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
::ShowWindow(flutter_window->GetHandle(),
|
||||||
|
config.hidden_at_launch ? SW_HIDE : SW_SHOW);
|
||||||
|
|
||||||
|
auto wrapper = std::make_unique<FlutterWindowWrapper>(
|
||||||
|
window_id, flutter_window->GetHandle(), config.arguments);
|
||||||
|
|
||||||
|
windows_[window_id] = std::move(wrapper);
|
||||||
|
|
||||||
|
if (_g_window_created_callback) {
|
||||||
|
_g_window_created_callback(flutter_window->GetFlutterViewController());
|
||||||
|
}
|
||||||
|
auto registrar = flutter_window->GetFlutterViewController()
|
||||||
|
->engine()
|
||||||
|
->GetRegistrarForPlugin("DesktopMultiWindowPlugin");
|
||||||
|
InternalMultiWindowPluginRegisterWithRegistrar(registrar,
|
||||||
|
windows_[window_id].get());
|
||||||
|
|
||||||
|
// keep flutter_window alive
|
||||||
|
managed_flutter_windows_[window_id] = std::move(flutter_window);
|
||||||
|
|
||||||
|
// Notify all windows about the change
|
||||||
|
NotifyWindowsChanged();
|
||||||
|
|
||||||
|
CleanupRemovedWindows();
|
||||||
|
|
||||||
|
return window_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiWindowManager::AttachFlutterMainWindow(
|
||||||
|
HWND window_handle,
|
||||||
|
FlutterDesktopPluginRegistrarRef registrar) {
|
||||||
|
// check if window already exists
|
||||||
|
for (const auto& [id, window] : windows_) {
|
||||||
|
if (GetAncestor(window->GetWindowHandle(), GA_ROOT) == window_handle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string window_id = GenerateWindowId();
|
||||||
|
auto wrapper =
|
||||||
|
std::make_unique<FlutterWindowWrapper>(window_id, window_handle);
|
||||||
|
|
||||||
|
windows_[window_id] = std::move(wrapper);
|
||||||
|
|
||||||
|
InternalMultiWindowPluginRegisterWithRegistrar(registrar,
|
||||||
|
windows_[window_id].get());
|
||||||
|
|
||||||
|
// Notify all windows about the change
|
||||||
|
NotifyWindowsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
FlutterWindowWrapper* MultiWindowManager::GetWindow(
|
||||||
|
const std::string& window_id) {
|
||||||
|
auto it = windows_.find(window_id);
|
||||||
|
if (it != windows_.end()) {
|
||||||
|
return it->second.get();
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter::EncodableList MultiWindowManager::GetAllWindows() {
|
||||||
|
flutter::EncodableList windows;
|
||||||
|
for (const auto& [id, window] : windows_) {
|
||||||
|
flutter::EncodableMap window_info;
|
||||||
|
window_info[flutter::EncodableValue("windowId")] =
|
||||||
|
flutter::EncodableValue(window->GetWindowId());
|
||||||
|
window_info[flutter::EncodableValue("windowArgument")] =
|
||||||
|
flutter::EncodableValue(window->GetWindowArgument());
|
||||||
|
windows.push_back(flutter::EncodableValue(window_info));
|
||||||
|
}
|
||||||
|
return windows;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> MultiWindowManager::GetAllWindowIds() {
|
||||||
|
std::vector<std::string> window_ids;
|
||||||
|
for (const auto& [id, window] : windows_) {
|
||||||
|
window_ids.push_back(id);
|
||||||
|
}
|
||||||
|
return window_ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiWindowManager::RemoveWindow(const std::string& window_id) {
|
||||||
|
auto it = windows_.find(window_id);
|
||||||
|
if (it != windows_.end()) {
|
||||||
|
windows_.erase(it);
|
||||||
|
NotifyWindowsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
// quit application if no windows left
|
||||||
|
if (windows_.empty()) {
|
||||||
|
PostQuitMessage(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiWindowManager::RemoveManagedFlutterWindowLater(
|
||||||
|
const std::string& window_id) {
|
||||||
|
pending_remove_ids_.push_back(window_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME:maybe need a more robust way to cleanup removed windows
|
||||||
|
void MultiWindowManager::CleanupRemovedWindows() {
|
||||||
|
for (auto& id : pending_remove_ids_) {
|
||||||
|
auto it = managed_flutter_windows_.find(id);
|
||||||
|
if (it != managed_flutter_windows_.end()) {
|
||||||
|
std::cout << "Destroyed managed flutter window: " << id << std::endl;
|
||||||
|
managed_flutter_windows_.erase(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pending_remove_ids_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiWindowManager::NotifyWindowsChanged() {
|
||||||
|
auto window_ids = GetAllWindowIds();
|
||||||
|
flutter::EncodableList window_ids_list;
|
||||||
|
for (const auto& id : window_ids) {
|
||||||
|
window_ids_list.push_back(flutter::EncodableValue(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter::EncodableMap data;
|
||||||
|
data[flutter::EncodableValue("windowIds")] =
|
||||||
|
flutter::EncodableValue(window_ids_list);
|
||||||
|
|
||||||
|
for (const auto& [id, window] : windows_) {
|
||||||
|
window->NotifyWindowEvent("onWindowsChanged", data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopMultiWindowSetWindowCreatedCallback(
|
||||||
|
WindowCreatedCallback callback) {
|
||||||
|
_g_window_created_callback = callback;
|
||||||
|
}
|
||||||
44
third_party/desktop_multi_window/windows/multi_window_manager.h
vendored
Normal file
44
third_party/desktop_multi_window/windows/multi_window_manager.h
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
#ifndef DESKTOP_MULTI_WINDOW_WINDOWS_MULTI_WINDOW_MANAGER_H_
|
||||||
|
#define DESKTOP_MULTI_WINDOW_WINDOWS_MULTI_WINDOW_MANAGER_H_
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "flutter_plugin_registrar.h"
|
||||||
|
#include "flutter_window.h"
|
||||||
|
#include "flutter_window_wrapper.h"
|
||||||
|
|
||||||
|
class MultiWindowManager {
|
||||||
|
public:
|
||||||
|
static MultiWindowManager* Instance();
|
||||||
|
|
||||||
|
MultiWindowManager();
|
||||||
|
|
||||||
|
std::string Create(const flutter::EncodableMap* args);
|
||||||
|
|
||||||
|
void AttachFlutterMainWindow(HWND main_window_handle,
|
||||||
|
FlutterDesktopPluginRegistrarRef registrar);
|
||||||
|
|
||||||
|
FlutterWindowWrapper* GetWindow(const std::string& window_id);
|
||||||
|
|
||||||
|
void RemoveWindow(const std::string& window_id);
|
||||||
|
|
||||||
|
void RemoveManagedFlutterWindowLater(const std::string& window_id);
|
||||||
|
|
||||||
|
flutter::EncodableList GetAllWindows();
|
||||||
|
|
||||||
|
std::vector<std::string> GetAllWindowIds();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void NotifyWindowsChanged();
|
||||||
|
|
||||||
|
void CleanupRemovedWindows();
|
||||||
|
|
||||||
|
std::map<std::string, std::unique_ptr<FlutterWindowWrapper>> windows_;
|
||||||
|
std::map<std::string, std::unique_ptr<FlutterWindow>>
|
||||||
|
managed_flutter_windows_;
|
||||||
|
std::vector<std::string> pending_remove_ids_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DESKTOP_MULTI_WINDOW_WINDOWS_MULTI_WINDOW_MANAGER_H_
|
||||||
12
third_party/desktop_multi_window/windows/multi_window_plugin_internal.h
vendored
Normal file
12
third_party/desktop_multi_window/windows/multi_window_plugin_internal.h
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
#ifndef DESKTOP_MULTI_WINDOW_WINDOWS_MULTI_WINDOW_PLUGIN_INTERNAL_H_
|
||||||
|
#define DESKTOP_MULTI_WINDOW_WINDOWS_MULTI_WINDOW_PLUGIN_INTERNAL_H_
|
||||||
|
|
||||||
|
#include "flutter_plugin_registrar.h"
|
||||||
|
|
||||||
|
class FlutterWindowWrapper;
|
||||||
|
|
||||||
|
void InternalMultiWindowPluginRegisterWithRegistrar(
|
||||||
|
FlutterDesktopPluginRegistrarRef registrar,
|
||||||
|
FlutterWindowWrapper* window);
|
||||||
|
|
||||||
|
#endif // DESKTOP_MULTI_WINDOW_WINDOWS_MULTI_WINDOW_PLUGIN_INTERNAL_H_
|
||||||
301
third_party/desktop_multi_window/windows/win32_window.cpp
vendored
Normal file
301
third_party/desktop_multi_window/windows/win32_window.cpp
vendored
Normal file
|
|
@ -0,0 +1,301 @@
|
||||||
|
#include "win32_window.h"
|
||||||
|
|
||||||
|
#include <dwmapi.h>
|
||||||
|
#include <flutter_windows.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
/// Window attribute that enables dark mode window decorations.
|
||||||
|
///
|
||||||
|
/// Redefined in case the developer's machine has a Windows SDK older than
|
||||||
|
/// version 10.0.22000.0.
|
||||||
|
/// See:
|
||||||
|
/// https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
|
||||||
|
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
|
||||||
|
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
|
||||||
|
#endif
|
||||||
|
|
||||||
|
constexpr const wchar_t kWindowClassName[] =
|
||||||
|
L"FLUTTER_MULTI_WINDOW_WIN32_WINDOW";
|
||||||
|
|
||||||
|
/// Registry key for app theme preference.
|
||||||
|
///
|
||||||
|
/// A value of 0 indicates apps should use dark mode. A non-zero or missing
|
||||||
|
/// value indicates apps should use light mode.
|
||||||
|
constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
|
||||||
|
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
|
||||||
|
constexpr const wchar_t kGetPreferredBrightnessRegValue[] =
|
||||||
|
L"AppsUseLightTheme";
|
||||||
|
|
||||||
|
// The number of Win32Window objects that currently exist.
|
||||||
|
static int g_active_window_count = 0;
|
||||||
|
|
||||||
|
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
|
||||||
|
|
||||||
|
// Scale helper to convert logical scaler values to physical using passed in
|
||||||
|
// scale factor
|
||||||
|
int Scale(int source, double scale_factor) {
|
||||||
|
return static_cast<int>(source * scale_factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<EnableNonClientDpiScaling*>(
|
||||||
|
GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
|
||||||
|
if (enable_non_client_dpi_scaling != nullptr) {
|
||||||
|
enable_non_client_dpi_scaling(hwnd);
|
||||||
|
}
|
||||||
|
FreeLibrary(user32_module);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Manages the Win32Window's window class registration.
|
||||||
|
class WindowClassRegistrar {
|
||||||
|
public:
|
||||||
|
~WindowClassRegistrar() = default;
|
||||||
|
|
||||||
|
// Returns the singleton registrar instance.
|
||||||
|
static WindowClassRegistrar* GetInstance() {
|
||||||
|
if (!instance_) {
|
||||||
|
instance_ = new WindowClassRegistrar();
|
||||||
|
}
|
||||||
|
return instance_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the name of the window class, registering the class if it hasn't
|
||||||
|
// previously been registered.
|
||||||
|
const wchar_t* GetWindowClass();
|
||||||
|
|
||||||
|
// Unregisters the window class. Should only be called if there are no
|
||||||
|
// instances of the window.
|
||||||
|
void UnregisterWindowClass();
|
||||||
|
|
||||||
|
private:
|
||||||
|
WindowClassRegistrar() = default;
|
||||||
|
|
||||||
|
static WindowClassRegistrar* instance_;
|
||||||
|
|
||||||
|
bool class_registered_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
|
||||||
|
|
||||||
|
const wchar_t* WindowClassRegistrar::GetWindowClass() {
|
||||||
|
if (!class_registered_) {
|
||||||
|
WNDCLASS window_class{};
|
||||||
|
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||||
|
window_class.lpszClassName = kWindowClassName;
|
||||||
|
window_class.style = CS_HREDRAW | CS_VREDRAW;
|
||||||
|
window_class.cbClsExtra = 0;
|
||||||
|
window_class.cbWndExtra = 0;
|
||||||
|
window_class.hInstance = GetModuleHandle(nullptr);
|
||||||
|
|
||||||
|
TCHAR exePath[MAX_PATH];
|
||||||
|
GetModuleFileName(NULL, exePath, MAX_PATH);
|
||||||
|
HICON hIcon = ExtractIcon(GetModuleHandle(NULL), exePath, 0);
|
||||||
|
if (hIcon) {
|
||||||
|
window_class.hIcon = hIcon;
|
||||||
|
} else {
|
||||||
|
window_class.hIcon = LoadIcon(window_class.hInstance, IDI_APPLICATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
window_class.hbrBackground = 0;
|
||||||
|
window_class.lpszMenuName = nullptr;
|
||||||
|
window_class.lpfnWndProc = Win32Window::WndProc;
|
||||||
|
RegisterClass(&window_class);
|
||||||
|
class_registered_ = true;
|
||||||
|
}
|
||||||
|
return kWindowClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowClassRegistrar::UnregisterWindowClass() {
|
||||||
|
UnregisterClass(kWindowClassName, nullptr);
|
||||||
|
class_registered_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Win32Window::Win32Window() {
|
||||||
|
++g_active_window_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
Win32Window::~Win32Window() {
|
||||||
|
--g_active_window_count;
|
||||||
|
if (g_active_window_count == 0) {
|
||||||
|
WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Win32Window::Create(const std::wstring& title,
|
||||||
|
const Point& origin,
|
||||||
|
const Size& size) {
|
||||||
|
if (window_handle_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wchar_t* window_class =
|
||||||
|
WindowClassRegistrar::GetInstance()->GetWindowClass();
|
||||||
|
|
||||||
|
const POINT target_point = {static_cast<LONG>(origin.x),
|
||||||
|
static_cast<LONG>(origin.y)};
|
||||||
|
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
|
||||||
|
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
|
||||||
|
double scale_factor = dpi / 96.0;
|
||||||
|
|
||||||
|
HWND window = CreateWindow(
|
||||||
|
window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
|
||||||
|
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
|
||||||
|
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
|
||||||
|
nullptr, nullptr, GetModuleHandle(nullptr), this);
|
||||||
|
|
||||||
|
if (!window) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateTheme(window);
|
||||||
|
|
||||||
|
return OnCreate();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Win32Window::Show() {
|
||||||
|
return ShowWindow(window_handle_, SW_SHOWNORMAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// static
|
||||||
|
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
|
||||||
|
UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept {
|
||||||
|
if (message == WM_NCCREATE) {
|
||||||
|
auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
|
||||||
|
SetWindowLongPtr(window, GWLP_USERDATA,
|
||||||
|
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
|
||||||
|
|
||||||
|
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
|
||||||
|
EnableFullDpiSupportIfAvailable(window);
|
||||||
|
that->window_handle_ = window;
|
||||||
|
} else if (Win32Window* that = GetThisFromHandle(window)) {
|
||||||
|
return that->MessageHandler(window, message, wparam, lparam);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefWindowProc(window, message, wparam, lparam);
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT
|
||||||
|
Win32Window::MessageHandler(HWND hwnd,
|
||||||
|
UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept {
|
||||||
|
switch (message) {
|
||||||
|
case WM_DESTROY:
|
||||||
|
window_handle_ = nullptr;
|
||||||
|
Destroy();
|
||||||
|
if (quit_on_close_) {
|
||||||
|
PostQuitMessage(0);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
case WM_DPICHANGED: {
|
||||||
|
auto newRectSize = reinterpret_cast<RECT*>(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 = GetClientArea();
|
||||||
|
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;
|
||||||
|
|
||||||
|
case WM_DWMCOLORIZATIONCOLORCHANGED:
|
||||||
|
UpdateTheme(hwnd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefWindowProc(window_handle_, message, wparam, lparam);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::Destroy() {
|
||||||
|
OnDestroy();
|
||||||
|
|
||||||
|
if (window_handle_) {
|
||||||
|
DestroyWindow(window_handle_);
|
||||||
|
window_handle_ = nullptr;
|
||||||
|
}
|
||||||
|
if (g_active_window_count == 0) {
|
||||||
|
WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
|
||||||
|
return reinterpret_cast<Win32Window*>(
|
||||||
|
GetWindowLongPtr(window, GWLP_USERDATA));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::SetChildContent(HWND content) {
|
||||||
|
child_content_ = content;
|
||||||
|
SetParent(content, window_handle_);
|
||||||
|
RECT frame = GetClientArea();
|
||||||
|
|
||||||
|
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
|
||||||
|
frame.bottom - frame.top, true);
|
||||||
|
|
||||||
|
SetFocus(child_content_);
|
||||||
|
}
|
||||||
|
|
||||||
|
RECT Win32Window::GetClientArea() {
|
||||||
|
RECT frame;
|
||||||
|
GetClientRect(window_handle_, &frame);
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
HWND Win32Window::GetHandle() {
|
||||||
|
return window_handle_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::SetQuitOnClose(bool quit_on_close) {
|
||||||
|
quit_on_close_ = quit_on_close;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Win32Window::OnCreate() {
|
||||||
|
// No-op; provided for subclasses.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::OnDestroy() {
|
||||||
|
// No-op; provided for subclasses.
|
||||||
|
}
|
||||||
|
|
||||||
|
void Win32Window::UpdateTheme(HWND const window) {
|
||||||
|
DWORD light_mode;
|
||||||
|
DWORD light_mode_size = sizeof(light_mode);
|
||||||
|
LSTATUS result =
|
||||||
|
RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
|
||||||
|
kGetPreferredBrightnessRegValue, RRF_RT_REG_DWORD, nullptr,
|
||||||
|
&light_mode, &light_mode_size);
|
||||||
|
|
||||||
|
if (result == ERROR_SUCCESS) {
|
||||||
|
BOOL enable_dark_mode = light_mode == 0;
|
||||||
|
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||||||
|
&enable_dark_mode, sizeof(enable_dark_mode));
|
||||||
|
}
|
||||||
|
}
|
||||||
102
third_party/desktop_multi_window/windows/win32_window.h
vendored
Normal file
102
third_party/desktop_multi_window/windows/win32_window.h
vendored
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
#ifndef RUNNER_WIN32_WINDOW_H_
|
||||||
|
#define RUNNER_WIN32_WINDOW_H_
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// A class abstraction for a high DPI-aware Win32 Window. Intended to be
|
||||||
|
// inherited from by classes that wish to specialize with custom
|
||||||
|
// rendering and input handling
|
||||||
|
class Win32Window {
|
||||||
|
public:
|
||||||
|
struct Point {
|
||||||
|
unsigned int x;
|
||||||
|
unsigned int y;
|
||||||
|
Point(unsigned int x, unsigned int y) : x(x), y(y) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Size {
|
||||||
|
unsigned int width;
|
||||||
|
unsigned int height;
|
||||||
|
Size(unsigned int width, unsigned int height)
|
||||||
|
: width(width), height(height) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
Win32Window();
|
||||||
|
virtual ~Win32Window();
|
||||||
|
|
||||||
|
// Creates a win32 window with |title| that is positioned and sized using
|
||||||
|
// |origin| and |size|. New windows are created on the default monitor. Window
|
||||||
|
// sizes are specified to the OS in physical pixels, hence to ensure a
|
||||||
|
// consistent size this function will scale the inputted width and height as
|
||||||
|
// as appropriate for the default monitor. The window is invisible until
|
||||||
|
// |Show| is called. Returns true if the window was created successfully.
|
||||||
|
bool Create(const std::wstring& title, const Point& origin, const Size& size);
|
||||||
|
|
||||||
|
// Show the current window. Returns true if the window was successfully shown.
|
||||||
|
bool Show();
|
||||||
|
|
||||||
|
// Release OS resources associated with window.
|
||||||
|
void Destroy();
|
||||||
|
|
||||||
|
// Inserts |content| into the window tree.
|
||||||
|
void SetChildContent(HWND content);
|
||||||
|
|
||||||
|
// Returns the backing Window handle to enable clients to set icon and other
|
||||||
|
// window properties. Returns nullptr if the window has been destroyed.
|
||||||
|
HWND GetHandle();
|
||||||
|
|
||||||
|
// If true, closing this window will quit the application.
|
||||||
|
void SetQuitOnClose(bool quit_on_close);
|
||||||
|
|
||||||
|
// Return a RECT representing the bounds of the current client area.
|
||||||
|
RECT GetClientArea();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Processes and route salient window messages for mouse handling,
|
||||||
|
// size change and DPI. Delegates handling of these to member overloads that
|
||||||
|
// inheriting classes can handle.
|
||||||
|
virtual LRESULT MessageHandler(HWND window,
|
||||||
|
UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept;
|
||||||
|
|
||||||
|
// Called when CreateAndShow is called, allowing subclass window-related
|
||||||
|
// setup. Subclasses should return false if setup fails.
|
||||||
|
virtual bool OnCreate();
|
||||||
|
|
||||||
|
// Called when Destroy is called.
|
||||||
|
virtual void OnDestroy();
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class WindowClassRegistrar;
|
||||||
|
|
||||||
|
// OS callback called by message pump. Handles the WM_NCCREATE message which
|
||||||
|
// is passed when the non-client area is being created and enables automatic
|
||||||
|
// non-client DPI scaling so that the non-client area automatically
|
||||||
|
// responds to changes in DPI. All other messages are handled by
|
||||||
|
// MessageHandler.
|
||||||
|
static LRESULT CALLBACK WndProc(HWND const window,
|
||||||
|
UINT const message,
|
||||||
|
WPARAM const wparam,
|
||||||
|
LPARAM const lparam) noexcept;
|
||||||
|
|
||||||
|
// Retrieves a class instance pointer for |window|
|
||||||
|
static Win32Window* GetThisFromHandle(HWND const window) noexcept;
|
||||||
|
|
||||||
|
// Update the window frame's theme to match the system theme.
|
||||||
|
static void UpdateTheme(HWND const window);
|
||||||
|
|
||||||
|
bool quit_on_close_ = false;
|
||||||
|
|
||||||
|
// window handle for top level window.
|
||||||
|
HWND window_handle_ = nullptr;
|
||||||
|
|
||||||
|
// window handle for hosted content.
|
||||||
|
HWND child_content_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // RUNNER_WIN32_WINDOW_H_
|
||||||
347
third_party/desktop_multi_window/windows/window_channel_plugin.cc
vendored
Normal file
347
third_party/desktop_multi_window/windows/window_channel_plugin.cc
vendored
Normal file
|
|
@ -0,0 +1,347 @@
|
||||||
|
#include "window_channel_plugin.h"
|
||||||
|
|
||||||
|
#include <flutter/encodable_value.h>
|
||||||
|
#include <flutter/method_result_functions.h>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
enum class ChannelMode { kUnidirectional, kBidirectional };
|
||||||
|
|
||||||
|
enum class RegistrationOutcome {
|
||||||
|
kAdded,
|
||||||
|
kAlreadyRegistered,
|
||||||
|
kLimitReached,
|
||||||
|
kModeConflict
|
||||||
|
};
|
||||||
|
|
||||||
|
class WindowChannelPlugin;
|
||||||
|
|
||||||
|
class ChannelRegistry {
|
||||||
|
public:
|
||||||
|
static ChannelRegistry& GetInstance() {
|
||||||
|
static ChannelRegistry instance;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
RegistrationOutcome Register(const std::string& channel,
|
||||||
|
WindowChannelPlugin* plugin,
|
||||||
|
ChannelMode mode) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
if (mode == ChannelMode::kUnidirectional) {
|
||||||
|
return RegisterUnidirectional(channel, plugin);
|
||||||
|
} else {
|
||||||
|
return RegisterBidirectional(channel, plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
RegistrationOutcome RegisterUnidirectional(const std::string& channel,
|
||||||
|
WindowChannelPlugin* plugin) {
|
||||||
|
// Check if already used in bidirectional mode
|
||||||
|
if (bidirectional_channels_.find(channel) !=
|
||||||
|
bidirectional_channels_.end()) {
|
||||||
|
return RegistrationOutcome::kModeConflict;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto it = unidirectional_channels_.find(channel);
|
||||||
|
if (it != unidirectional_channels_.end()) {
|
||||||
|
if (it->second == plugin) {
|
||||||
|
return RegistrationOutcome::kAlreadyRegistered;
|
||||||
|
}
|
||||||
|
// Already registered by another plugin
|
||||||
|
return RegistrationOutcome::kLimitReached;
|
||||||
|
}
|
||||||
|
|
||||||
|
unidirectional_channels_[channel] = plugin;
|
||||||
|
return RegistrationOutcome::kAdded;
|
||||||
|
}
|
||||||
|
|
||||||
|
RegistrationOutcome RegisterBidirectional(const std::string& channel,
|
||||||
|
WindowChannelPlugin* plugin) {
|
||||||
|
// Check if already used in unidirectional mode
|
||||||
|
if (unidirectional_channels_.find(channel) !=
|
||||||
|
unidirectional_channels_.end()) {
|
||||||
|
return RegistrationOutcome::kModeConflict;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& plugins = bidirectional_channels_[channel];
|
||||||
|
|
||||||
|
// Check if already registered
|
||||||
|
if (plugins.find(plugin) != plugins.end()) {
|
||||||
|
return RegistrationOutcome::kAlreadyRegistered;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check limit
|
||||||
|
if (plugins.size() >= 2) {
|
||||||
|
return RegistrationOutcome::kLimitReached;
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins.insert(plugin);
|
||||||
|
return RegistrationOutcome::kAdded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
void Unregister(const std::string& channel, WindowChannelPlugin* plugin) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
// Try unidirectional
|
||||||
|
auto uni_it = unidirectional_channels_.find(channel);
|
||||||
|
if (uni_it != unidirectional_channels_.end() &&
|
||||||
|
uni_it->second == plugin) {
|
||||||
|
unidirectional_channels_.erase(uni_it);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try bidirectional
|
||||||
|
auto bi_it = bidirectional_channels_.find(channel);
|
||||||
|
if (bi_it != bidirectional_channels_.end()) {
|
||||||
|
bi_it->second.erase(plugin);
|
||||||
|
if (bi_it->second.empty()) {
|
||||||
|
bidirectional_channels_.erase(bi_it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowChannelPlugin* GetTarget(const std::string& channel,
|
||||||
|
WindowChannelPlugin* from) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
// Check unidirectional - anyone can call
|
||||||
|
auto uni_it = unidirectional_channels_.find(channel);
|
||||||
|
if (uni_it != unidirectional_channels_.end()) {
|
||||||
|
return uni_it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check bidirectional - only peer can call
|
||||||
|
auto bi_it = bidirectional_channels_.find(channel);
|
||||||
|
if (bi_it != bidirectional_channels_.end()) {
|
||||||
|
const auto& plugins = bi_it->second;
|
||||||
|
|
||||||
|
// Check if caller is in the pair
|
||||||
|
if (plugins.find(from) == plugins.end()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the peer
|
||||||
|
for (auto* plugin : plugins) {
|
||||||
|
if (plugin != from) {
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasRegistrations(const std::string& channel) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
if (unidirectional_channels_.find(channel) !=
|
||||||
|
unidirectional_channels_.end()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto it = bidirectional_channels_.find(channel);
|
||||||
|
return it != bidirectional_channels_.end() && !it->second.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
ChannelRegistry() = default;
|
||||||
|
std::mutex mutex_;
|
||||||
|
std::map<std::string, WindowChannelPlugin*> unidirectional_channels_;
|
||||||
|
std::map<std::string, std::set<WindowChannelPlugin*>>
|
||||||
|
bidirectional_channels_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class WindowChannelPlugin : public flutter::Plugin {
|
||||||
|
public:
|
||||||
|
WindowChannelPlugin(flutter::PluginRegistrarWindows* registrar)
|
||||||
|
: registrar_(registrar) {
|
||||||
|
channel_ = std::make_unique<flutter::MethodChannel<>>(
|
||||||
|
registrar->messenger(), "mixin.one/desktop_multi_window/channels",
|
||||||
|
&flutter::StandardMethodCodec::GetInstance());
|
||||||
|
|
||||||
|
channel_->SetMethodCallHandler(
|
||||||
|
[this](const flutter::MethodCall<>& call,
|
||||||
|
std::unique_ptr<flutter::MethodResult<>> result) {
|
||||||
|
HandleMethodCall(call, std::move(result));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
~WindowChannelPlugin() {
|
||||||
|
for (const auto& channel : registered_channels_) {
|
||||||
|
ChannelRegistry::GetInstance().Unregister(channel, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InvokeMethod(const std::string& channel,
|
||||||
|
const flutter::EncodableValue& arguments,
|
||||||
|
std::unique_ptr<flutter::MethodResult<>> result) {
|
||||||
|
// Check if this plugin has registered this channel
|
||||||
|
if (std::find(registered_channels_.begin(), registered_channels_.end(),
|
||||||
|
channel) == registered_channels_.end()) {
|
||||||
|
result->Error("CHANNEL_NOT_FOUND",
|
||||||
|
"channel " + channel + " not found in this engine");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
channel_->InvokeMethod("methodCall", std::make_unique<flutter::EncodableValue>(arguments),
|
||||||
|
std::move(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void HandleMethodCall(const flutter::MethodCall<>& call,
|
||||||
|
std::unique_ptr<flutter::MethodResult<>> result) {
|
||||||
|
const auto& method = call.method_name();
|
||||||
|
|
||||||
|
if (method == "registerMethodHandler") {
|
||||||
|
auto* args = std::get_if<flutter::EncodableMap>(call.arguments());
|
||||||
|
if (!args) {
|
||||||
|
result->Error("INVALID_ARGUMENTS", "arguments must be a map");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto channel_it = args->find(flutter::EncodableValue("channel"));
|
||||||
|
if (channel_it == args->end()) {
|
||||||
|
result->Error("INVALID_ARGUMENTS", "channel is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* channel = std::get_if<std::string>(&channel_it->second);
|
||||||
|
if (!channel) {
|
||||||
|
result->Error("INVALID_ARGUMENTS", "channel must be a string");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get mode (default to bidirectional)
|
||||||
|
ChannelMode mode = ChannelMode::kBidirectional;
|
||||||
|
auto mode_it = args->find(flutter::EncodableValue("mode"));
|
||||||
|
if (mode_it != args->end()) {
|
||||||
|
auto* mode_str = std::get_if<std::string>(&mode_it->second);
|
||||||
|
if (mode_str) {
|
||||||
|
if (*mode_str == "unidirectional") {
|
||||||
|
mode = ChannelMode::kUnidirectional;
|
||||||
|
} else if (*mode_str == "bidirectional") {
|
||||||
|
mode = ChannelMode::kBidirectional;
|
||||||
|
} else {
|
||||||
|
result->Error("INVALID_MODE",
|
||||||
|
"invalid mode: " + *mode_str +
|
||||||
|
", must be 'unidirectional' or 'bidirectional'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto outcome = ChannelRegistry::GetInstance().Register(*channel, this, mode);
|
||||||
|
switch (outcome) {
|
||||||
|
case RegistrationOutcome::kAdded:
|
||||||
|
registered_channels_.push_back(*channel);
|
||||||
|
result->Success();
|
||||||
|
break;
|
||||||
|
case RegistrationOutcome::kAlreadyRegistered:
|
||||||
|
result->Success();
|
||||||
|
break;
|
||||||
|
case RegistrationOutcome::kLimitReached: {
|
||||||
|
std::string message = mode == ChannelMode::kUnidirectional
|
||||||
|
? "channel " + *channel +
|
||||||
|
" already registered in "
|
||||||
|
"unidirectional mode"
|
||||||
|
: "channel " + *channel +
|
||||||
|
" already has the maximum number of "
|
||||||
|
"registrations (2)";
|
||||||
|
result->Error("CHANNEL_LIMIT_REACHED", message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case RegistrationOutcome::kModeConflict:
|
||||||
|
result->Error("CHANNEL_MODE_CONFLICT",
|
||||||
|
"channel " + *channel +
|
||||||
|
" is already registered in a different mode");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (method == "unregisterMethodHandler") {
|
||||||
|
auto* args = std::get_if<flutter::EncodableMap>(call.arguments());
|
||||||
|
if (!args) {
|
||||||
|
result->Error("INVALID_ARGUMENTS", "arguments must be a map");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto channel_it = args->find(flutter::EncodableValue("channel"));
|
||||||
|
if (channel_it == args->end()) {
|
||||||
|
result->Error("INVALID_ARGUMENTS", "channel is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* channel = std::get_if<std::string>(&channel_it->second);
|
||||||
|
if (!channel) {
|
||||||
|
result->Error("INVALID_ARGUMENTS", "channel must be a string");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChannelRegistry::GetInstance().Unregister(*channel, this);
|
||||||
|
|
||||||
|
auto it = std::find(registered_channels_.begin(),
|
||||||
|
registered_channels_.end(), *channel);
|
||||||
|
if (it != registered_channels_.end()) {
|
||||||
|
registered_channels_.erase(it);
|
||||||
|
}
|
||||||
|
|
||||||
|
result->Success();
|
||||||
|
} else if (method == "invokeMethod") {
|
||||||
|
auto* args = std::get_if<flutter::EncodableMap>(call.arguments());
|
||||||
|
if (!args) {
|
||||||
|
result->Error("INVALID_ARGUMENTS", "arguments must be a map");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto channel_it = args->find(flutter::EncodableValue("channel"));
|
||||||
|
if (channel_it == args->end()) {
|
||||||
|
result->Error("INVALID_ARGUMENTS", "channel is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* channel = std::get_if<std::string>(&channel_it->second);
|
||||||
|
if (!channel) {
|
||||||
|
result->Error("INVALID_ARGUMENTS", "channel must be a string");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* target = ChannelRegistry::GetInstance().GetTarget(*channel, this);
|
||||||
|
if (target) {
|
||||||
|
target->InvokeMethod(*channel, *call.arguments(), std::move(result));
|
||||||
|
} else {
|
||||||
|
std::string message;
|
||||||
|
if (ChannelRegistry::GetInstance().HasRegistrations(*channel)) {
|
||||||
|
message = "channel " + *channel +
|
||||||
|
" not accessible from this engine (may be bidirectional "
|
||||||
|
"pair or not registered)";
|
||||||
|
} else {
|
||||||
|
message = "unknown registered channel " + *channel;
|
||||||
|
}
|
||||||
|
result->Error("CHANNEL_UNREGISTERED", message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result->NotImplemented();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter::PluginRegistrarWindows* registrar_;
|
||||||
|
std::unique_ptr<flutter::MethodChannel<>> channel_;
|
||||||
|
std::vector<std::string> registered_channels_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void WindowChannelPluginRegisterWithRegistrar(
|
||||||
|
flutter::PluginRegistrarWindows* registrar) {
|
||||||
|
auto plugin = std::make_unique<WindowChannelPlugin>(registrar);
|
||||||
|
registrar->AddPlugin(std::move(plugin));
|
||||||
|
}
|
||||||
17
third_party/desktop_multi_window/windows/window_channel_plugin.h
vendored
Normal file
17
third_party/desktop_multi_window/windows/window_channel_plugin.h
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
#ifndef DESKTOP_MULTI_WINDOW_WINDOWS_WINDOW_CHANNEL_PLUGIN_H_
|
||||||
|
#define DESKTOP_MULTI_WINDOW_WINDOWS_WINDOW_CHANNEL_PLUGIN_H_
|
||||||
|
|
||||||
|
#include <flutter/method_channel.h>
|
||||||
|
#include <flutter/plugin_registrar_windows.h>
|
||||||
|
#include <flutter/standard_method_codec.h>
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
void WindowChannelPluginRegisterWithRegistrar(
|
||||||
|
flutter::PluginRegistrarWindows* registrar);
|
||||||
|
|
||||||
|
#endif // DESKTOP_MULTI_WINDOW_WINDOWS_WINDOW_CHANNEL_PLUGIN_H_
|
||||||
34
third_party/desktop_multi_window/windows/window_configuration.h
vendored
Normal file
34
third_party/desktop_multi_window/windows/window_configuration.h
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <flutter/encodable_value.h>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
struct WindowConfiguration {
|
||||||
|
std::string arguments;
|
||||||
|
bool hidden_at_launch = false;
|
||||||
|
|
||||||
|
static WindowConfiguration FromEncodableMap(
|
||||||
|
const flutter::EncodableMap* map) {
|
||||||
|
WindowConfiguration config;
|
||||||
|
|
||||||
|
if (!map) return config;
|
||||||
|
|
||||||
|
try {
|
||||||
|
auto it = map->find(flutter::EncodableValue("arguments"));
|
||||||
|
if (it != map->end()) {
|
||||||
|
config.arguments = std::get<std::string>(it->second);
|
||||||
|
}
|
||||||
|
|
||||||
|
it = map->find(flutter::EncodableValue("hiddenAtLaunch"));
|
||||||
|
if (it != map->end()) {
|
||||||
|
config.hidden_at_launch = std::get<bool>(it->second);
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "Failed to parse WindowConfiguration: " << e.what()
|
||||||
|
<< std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <desktop_drop/desktop_drop_plugin.h>
|
#include <desktop_drop/desktop_drop_plugin.h>
|
||||||
|
#include <desktop_multi_window/desktop_multi_window_plugin.h>
|
||||||
#include <pasteboard/pasteboard_plugin.h>
|
#include <pasteboard/pasteboard_plugin.h>
|
||||||
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
@ -15,6 +16,8 @@
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
DesktopDropPluginRegisterWithRegistrar(
|
DesktopDropPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("DesktopDropPlugin"));
|
registry->GetRegistrarForPlugin("DesktopDropPlugin"));
|
||||||
|
DesktopMultiWindowPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("DesktopMultiWindowPlugin"));
|
||||||
PasteboardPluginRegisterWithRegistrar(
|
PasteboardPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("PasteboardPlugin"));
|
registry->GetRegistrarForPlugin("PasteboardPlugin"));
|
||||||
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
|
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
desktop_drop
|
desktop_drop
|
||||||
|
desktop_multi_window
|
||||||
pasteboard
|
pasteboard
|
||||||
screen_retriever_windows
|
screen_retriever_windows
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue