App-thema’s, meerschermen, annotaties en grafiekslides #1
6 changed files with 225 additions and 6 deletions
|
|
@ -42,8 +42,8 @@ class FullscreenPresenter extends StatefulWidget {
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Entry point used by the app: pick dual-screen mode when a second display is
|
/// 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
|
/// available on desktop, otherwise the single-window presenter. Any failure
|
||||||
/// open the second window falls back to single-window mode.
|
/// to open the second window falls back to single-window mode.
|
||||||
static Future<void> present(
|
static Future<void> present(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required List<Slide> slides,
|
required List<Slide> slides,
|
||||||
|
|
@ -52,15 +52,21 @@ class FullscreenPresenter extends StatefulWidget {
|
||||||
required int initialIndex,
|
required int initialIndex,
|
||||||
TlpLevel tlp = TlpLevel.none,
|
TlpLevel tlp = TlpLevel.none,
|
||||||
}) async {
|
}) async {
|
||||||
var dual = false;
|
var displayCount = 0;
|
||||||
if (Platform.isMacOS) {
|
if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
|
||||||
try {
|
try {
|
||||||
final displays = await screenRetriever.getAllDisplays();
|
final displays = await screenRetriever.getAllDisplays();
|
||||||
dual = displays.length >= 2;
|
displayCount = displays.length;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
dual = false;
|
displayCount = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
final dual = shouldUseDualScreen(
|
||||||
|
isMacOS: Platform.isMacOS,
|
||||||
|
isWindows: Platform.isWindows,
|
||||||
|
isLinux: Platform.isLinux,
|
||||||
|
displayCount: displayCount,
|
||||||
|
);
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
if (dual) {
|
if (dual) {
|
||||||
await showDualScreen(
|
await showDualScreen(
|
||||||
|
|
@ -204,6 +210,16 @@ class FullscreenPresenter extends StatefulWidget {
|
||||||
State<FullscreenPresenter> createState() => _FullscreenPresenterState();
|
State<FullscreenPresenter> createState() => _FullscreenPresenterState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
bool shouldUseDualScreen({
|
||||||
|
required bool isMacOS,
|
||||||
|
required bool isWindows,
|
||||||
|
required bool isLinux,
|
||||||
|
required int displayCount,
|
||||||
|
}) {
|
||||||
|
return (isMacOS || isWindows || isLinux) && displayCount >= 2;
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> _wakeLockEnabled() async {
|
Future<bool> _wakeLockEnabled() async {
|
||||||
try {
|
try {
|
||||||
return await WakelockPlus.enabled;
|
return await WakelockPlus.enabled;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include <gdk/gdkx.h>
|
#include <gdk/gdkx.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include "desktop_multi_window/desktop_multi_window_plugin.h"
|
||||||
#include "flutter/generated_plugin_registrant.h"
|
#include "flutter/generated_plugin_registrant.h"
|
||||||
|
|
||||||
struct _MyApplication {
|
struct _MyApplication {
|
||||||
|
|
@ -89,6 +90,8 @@ static void my_application_activate(GApplication* application) {
|
||||||
gtk_widget_realize(GTK_WIDGET(view));
|
gtk_widget_realize(GTK_WIDGET(view));
|
||||||
|
|
||||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
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));
|
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,57 @@ void main() {
|
||||||
Slide.create(SlideType.bullets).copyWith(title: 'Tweede', bullets: ['b']),
|
Slide.create(SlideType.bullets).copyWith(title: 'Tweede', bullets: ['b']),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
test('dual-screen mode is available on every desktop platform', () {
|
||||||
|
expect(
|
||||||
|
shouldUseDualScreen(
|
||||||
|
isMacOS: true,
|
||||||
|
isWindows: false,
|
||||||
|
isLinux: false,
|
||||||
|
displayCount: 2,
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
shouldUseDualScreen(
|
||||||
|
isMacOS: false,
|
||||||
|
isWindows: true,
|
||||||
|
isLinux: false,
|
||||||
|
displayCount: 2,
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
shouldUseDualScreen(
|
||||||
|
isMacOS: false,
|
||||||
|
isWindows: false,
|
||||||
|
isLinux: true,
|
||||||
|
displayCount: 2,
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dual-screen mode requires a desktop platform and two displays', () {
|
||||||
|
expect(
|
||||||
|
shouldUseDualScreen(
|
||||||
|
isMacOS: true,
|
||||||
|
isWindows: false,
|
||||||
|
isLinux: false,
|
||||||
|
displayCount: 1,
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
shouldUseDualScreen(
|
||||||
|
isMacOS: false,
|
||||||
|
isWindows: false,
|
||||||
|
isLinux: false,
|
||||||
|
displayCount: 2,
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('starts in audience view without presenter chrome', (
|
testWidgets('starts in audience view without presenter chrome', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,32 @@
|
||||||
#include "flutter_window.h"
|
#include "flutter_window.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool ReadExternalArgument(FlValue* arguments) {
|
||||||
|
if (arguments == nullptr ||
|
||||||
|
fl_value_get_type(arguments) != FL_VALUE_TYPE_MAP) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
FlValue* external = fl_value_lookup_string(arguments, "external");
|
||||||
|
if (external == nullptr ||
|
||||||
|
fl_value_get_type(external) != FL_VALUE_TYPE_BOOL) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return fl_value_get_bool(external);
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean CloseWindowOnIdle(gpointer data) {
|
||||||
|
GtkWidget* window = GTK_WIDGET(data);
|
||||||
|
gtk_widget_destroy(window);
|
||||||
|
g_object_unref(window);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
FlutterWindow::FlutterWindow(const std::string& id,
|
FlutterWindow::FlutterWindow(const std::string& id,
|
||||||
const std::string& argument,
|
const std::string& argument,
|
||||||
GtkWidget* window)
|
GtkWidget* window)
|
||||||
|
|
@ -42,6 +67,49 @@ void FlutterWindow::HandleWindowMethod(const gchar* method,
|
||||||
} else if (strcmp(method, "window_hide") == 0) {
|
} else if (strcmp(method, "window_hide") == 0) {
|
||||||
Hide();
|
Hide();
|
||||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||||
|
} else if (strcmp(method, "window_close") == 0) {
|
||||||
|
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||||
|
if (window_) {
|
||||||
|
g_idle_add(CloseWindowOnIdle, g_object_ref(window_));
|
||||||
|
}
|
||||||
|
} else if (strcmp(method, "window_coverScreen") == 0) {
|
||||||
|
if (!window_) {
|
||||||
|
response = FL_METHOD_RESPONSE(
|
||||||
|
fl_method_error_response_new("-1", "window is not available",
|
||||||
|
nullptr));
|
||||||
|
} else {
|
||||||
|
GtkWindow* window = GTK_WINDOW(window_);
|
||||||
|
GdkScreen* screen = gtk_window_get_screen(window);
|
||||||
|
GdkWindow* gdk_window = gtk_widget_get_window(window_);
|
||||||
|
const gint monitor_count = gdk_screen_get_n_monitors(screen);
|
||||||
|
gint current_monitor = gdk_window
|
||||||
|
? gdk_screen_get_monitor_at_window(screen,
|
||||||
|
gdk_window)
|
||||||
|
: gdk_screen_get_primary_monitor(screen);
|
||||||
|
if (current_monitor < 0) {
|
||||||
|
current_monitor = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
gint target_monitor = current_monitor;
|
||||||
|
if (ReadExternalArgument(arguments) && monitor_count > 1) {
|
||||||
|
for (gint i = 0; i < monitor_count; ++i) {
|
||||||
|
if (i != current_monitor) {
|
||||||
|
target_monitor = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GdkRectangle bounds;
|
||||||
|
gdk_screen_get_monitor_geometry(screen, target_monitor, &bounds);
|
||||||
|
gtk_window_unfullscreen(window);
|
||||||
|
gtk_window_set_decorated(window, FALSE);
|
||||||
|
gtk_window_move(window, bounds.x, bounds.y);
|
||||||
|
gtk_window_resize(window, bounds.width, bounds.height);
|
||||||
|
gtk_window_fullscreen_on_monitor(window, screen, target_monitor);
|
||||||
|
gtk_widget_show(window_);
|
||||||
|
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
g_autofree gchar* error_msg = g_strdup_printf("unknown method: %s", method);
|
g_autofree gchar* error_msg = g_strdup_printf("unknown method: %s", method);
|
||||||
response = FL_METHOD_RESPONSE(
|
response = FL_METHOD_RESPONSE(
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,42 @@
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct MonitorSearch {
|
||||||
|
HMONITOR current = nullptr;
|
||||||
|
HMONITOR external = nullptr;
|
||||||
|
HMONITOR fallback = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline BOOL CALLBACK FindPresentationMonitor(HMONITOR monitor,
|
||||||
|
HDC,
|
||||||
|
LPRECT,
|
||||||
|
LPARAM data) {
|
||||||
|
auto* search = reinterpret_cast<MonitorSearch*>(data);
|
||||||
|
if (!search->fallback) {
|
||||||
|
search->fallback = monitor;
|
||||||
|
}
|
||||||
|
if (monitor != search->current && !search->external) {
|
||||||
|
search->external = monitor;
|
||||||
|
}
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool ReadExternalArgument(const flutter::EncodableMap* arguments) {
|
||||||
|
if (!arguments) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const auto it = arguments->find(flutter::EncodableValue("external"));
|
||||||
|
if (it == arguments->end()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const auto* external = std::get_if<bool>(&it->second);
|
||||||
|
return external ? *external : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
class FlutterWindowWrapper {
|
class FlutterWindowWrapper {
|
||||||
public:
|
public:
|
||||||
FlutterWindowWrapper(const std::string& window_id,
|
FlutterWindowWrapper(const std::string& window_id,
|
||||||
|
|
@ -51,6 +87,45 @@ class FlutterWindowWrapper {
|
||||||
::ShowWindow(hwnd_, SW_HIDE);
|
::ShowWindow(hwnd_, SW_HIDE);
|
||||||
}
|
}
|
||||||
result->Success();
|
result->Success();
|
||||||
|
} else if (method == "window_close") {
|
||||||
|
result->Success();
|
||||||
|
if (hwnd_) {
|
||||||
|
::PostMessage(hwnd_, WM_CLOSE, 0, 0);
|
||||||
|
}
|
||||||
|
} else if (method == "window_coverScreen") {
|
||||||
|
if (!hwnd_) {
|
||||||
|
result->Error("-1", "window is not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MonitorSearch search;
|
||||||
|
search.current = ::MonitorFromWindow(hwnd_, MONITOR_DEFAULTTONEAREST);
|
||||||
|
::EnumDisplayMonitors(
|
||||||
|
nullptr, nullptr, FindPresentationMonitor,
|
||||||
|
reinterpret_cast<LPARAM>(&search));
|
||||||
|
|
||||||
|
HMONITOR target = search.current;
|
||||||
|
if (ReadExternalArgument(arguments) && search.external) {
|
||||||
|
target = search.external;
|
||||||
|
} else if (!target) {
|
||||||
|
target = search.fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
MONITORINFO monitor_info{sizeof(MONITORINFO)};
|
||||||
|
if (!target || !::GetMonitorInfo(target, &monitor_info)) {
|
||||||
|
result->Error("-1", "unable to find a display");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RECT bounds = monitor_info.rcMonitor;
|
||||||
|
::SetWindowLongPtr(hwnd_, GWL_STYLE, WS_POPUP | WS_VISIBLE);
|
||||||
|
::SetWindowLongPtr(hwnd_, GWL_EXSTYLE,
|
||||||
|
::GetWindowLongPtr(hwnd_, GWL_EXSTYLE) &
|
||||||
|
~WS_EX_WINDOWEDGE);
|
||||||
|
::SetWindowPos(hwnd_, HWND_TOP, bounds.left, bounds.top,
|
||||||
|
bounds.right - bounds.left, bounds.bottom - bounds.top,
|
||||||
|
SWP_FRAMECHANGED | SWP_SHOWWINDOW);
|
||||||
|
result->Success();
|
||||||
} else {
|
} else {
|
||||||
result->Error("-1", "unknown method: " + method);
|
result->Error("-1", "unknown method: " + method);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
|
#include "desktop_multi_window/desktop_multi_window_plugin.h"
|
||||||
#include "flutter/generated_plugin_registrant.h"
|
#include "flutter/generated_plugin_registrant.h"
|
||||||
|
|
||||||
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
|
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
|
||||||
|
|
@ -25,6 +26,11 @@ bool FlutterWindow::OnCreate() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
RegisterPlugins(flutter_controller_->engine());
|
RegisterPlugins(flutter_controller_->engine());
|
||||||
|
DesktopMultiWindowSetWindowCreatedCallback([](void* controller) {
|
||||||
|
auto* flutter_view_controller =
|
||||||
|
reinterpret_cast<flutter::FlutterViewController*>(controller);
|
||||||
|
RegisterPlugins(flutter_view_controller->engine());
|
||||||
|
});
|
||||||
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
||||||
|
|
||||||
flutter_controller_->engine()->SetNextFrameCallback([&]() {
|
flutter_controller_->engine()->SetNextFrameCallback([&]() {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue