Files
FlClash/plugins/window/linux/window_plugin.cc
T
chen08209 1e97716636 feat(platform): move desktop windows to plugins/window and rework the tray
plugins/window replaces the window_manager fork on every desktop.

Changelog: Tray hotkeys, node delays and a delay test
2026-09-30 11:39:58 +08:00

664 lines
22 KiB
C++

#include "include/window/window_plugin.h"
#include <gtk/gtk.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include <cstdint>
#include <cstring>
#include "window_plugin_internal.h"
G_DEFINE_TYPE(WindowPlugin, window_plugin, g_object_get_type())
static WindowPlugin* activate_target = nullptr;
static constexpr gint64 kMinimizeFocusLossWindow =
500 * G_TIME_SPAN_MILLISECOND;
GtkWindow* window_plugin_get_window(WindowPlugin* self) {
FlView* view = fl_plugin_registrar_get_view(self->registrar);
if (view == nullptr) {
return nullptr;
}
return GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(view)));
}
GdkWindow* window_plugin_get_gdk_window(WindowPlugin* self) {
GtkWindow* window = window_plugin_get_window(self);
if (window == nullptr) {
return nullptr;
}
return gtk_widget_get_window(GTK_WIDGET(window));
}
void window_plugin_emit_event(WindowPlugin* self, const char* name) {
g_autoptr(FlValue) args = fl_value_new_map();
fl_value_set_string_take(args, "name", fl_value_new_string(name));
fl_method_channel_invoke_method(self->channel, "onEvent", args, nullptr,
nullptr, nullptr);
}
static FlMethodResponse* success_response(FlValue* value) {
return FL_METHOD_RESPONSE(fl_method_success_response_new(value));
}
static FlMethodResponse* success_null() {
g_autoptr(FlValue) value = fl_value_new_null();
return success_response(value);
}
static FlMethodResponse* success_bool(bool value) {
g_autoptr(FlValue) value_out = fl_value_new_bool(value);
return success_response(value_out);
}
static FlMethodResponse* not_ready_response() {
return FL_METHOD_RESPONSE(fl_method_error_response_new(
"not_ready", "Window is not available", nullptr));
}
static FlMethodResponse* bad_args_response(const char* method,
const char* key) {
g_autofree gchar* message = g_strdup_printf("%s requires '%s'", method, key);
return FL_METHOD_RESPONSE(
fl_method_error_response_new("bad_args", message, nullptr));
}
static FlMethodResponse* unsupported_response(const char* message) {
return FL_METHOD_RESPONSE(
fl_method_error_response_new("unsupported", message, nullptr));
}
static bool arg_bool(FlValue* args, const char* key, bool* out) {
if (args == nullptr || fl_value_get_type(args) != FL_VALUE_TYPE_MAP) {
return false;
}
FlValue* value = fl_value_lookup_string(args, key);
if (value == nullptr || fl_value_get_type(value) != FL_VALUE_TYPE_BOOL) {
return false;
}
*out = fl_value_get_bool(value);
return true;
}
static bool arg_double(FlValue* args, const char* key, double* out) {
if (args == nullptr || fl_value_get_type(args) != FL_VALUE_TYPE_MAP) {
return false;
}
FlValue* value = fl_value_lookup_string(args, key);
if (value == nullptr) {
return false;
}
if (fl_value_get_type(value) == FL_VALUE_TYPE_FLOAT) {
*out = fl_value_get_float(value);
return true;
}
if (fl_value_get_type(value) == FL_VALUE_TYPE_INT) {
*out = static_cast<double>(fl_value_get_int(value));
return true;
}
return false;
}
static bool arg_string(FlValue* args, const char* key, const char** out) {
if (args == nullptr || fl_value_get_type(args) != FL_VALUE_TYPE_MAP) {
return false;
}
FlValue* value = fl_value_lookup_string(args, key);
if (value == nullptr || fl_value_get_type(value) != FL_VALUE_TYPE_STRING) {
return false;
}
*out = fl_value_get_string(value);
return true;
}
static FlMethodResponse* handle_ensure_initialized(WindowPlugin*,
FlValue*) {
return success_null();
}
static FlMethodResponse* handle_show(WindowPlugin* self, FlValue* args) {
bool inactive;
if (!arg_bool(args, "inactive", &inactive)) {
return bad_args_response("show", "inactive");
}
GtkWindow* window = window_plugin_get_window(self);
if (window == nullptr) {
return not_ready_response();
}
if (inactive) {
gtk_window_set_focus_on_map(window, FALSE);
gtk_widget_show(GTK_WIDGET(window));
gtk_window_set_focus_on_map(window, TRUE);
} else {
gtk_widget_show(GTK_WIDGET(window));
}
return success_null();
}
static FlMethodResponse* handle_hide(WindowPlugin* self, FlValue*) {
window_geometry_on_hide(self);
return success_null();
}
static FlMethodResponse* handle_is_visible(WindowPlugin* self, FlValue*) {
GtkWindow* window = window_plugin_get_window(self);
return success_bool(window != nullptr &&
gtk_widget_is_visible(GTK_WIDGET(window)));
}
static FlMethodResponse* handle_focus(WindowPlugin* self, FlValue*) {
GtkWindow* window = window_plugin_get_window(self);
if (window == nullptr) {
return not_ready_response();
}
gtk_window_present(window);
return success_null();
}
static FlMethodResponse* handle_close(WindowPlugin* self, FlValue*) {
GtkWindow* window = window_plugin_get_window(self);
if (window == nullptr) {
return not_ready_response();
}
gtk_window_close(window);
return success_null();
}
static FlMethodResponse* handle_set_prevent_close(WindowPlugin* self,
FlValue* args) {
bool value;
if (!arg_bool(args, "value", &value)) {
return bad_args_response("setPreventClose", "value");
}
self->prevent_close = value;
return success_null();
}
static FlMethodResponse* handle_is_maximized(WindowPlugin* self,
FlValue*) {
GtkWindow* window = window_plugin_get_window(self);
return success_bool(window != nullptr && gtk_window_is_maximized(window));
}
static FlMethodResponse* handle_maximize(WindowPlugin* self, FlValue*) {
GtkWindow* window = window_plugin_get_window(self);
if (window == nullptr) {
return not_ready_response();
}
gtk_window_maximize(window);
return success_null();
}
static FlMethodResponse* handle_unmaximize(WindowPlugin* self, FlValue*) {
GtkWindow* window = window_plugin_get_window(self);
if (window == nullptr) {
return not_ready_response();
}
gtk_window_unmaximize(window);
return success_null();
}
static FlMethodResponse* handle_is_minimized(WindowPlugin* self,
FlValue*) {
GdkWindow* gdk_window = window_plugin_get_gdk_window(self);
return success_bool(gdk_window != nullptr &&
(gdk_window_get_state(gdk_window) &
GDK_WINDOW_STATE_ICONIFIED));
}
static FlMethodResponse* handle_minimize(WindowPlugin* self, FlValue*) {
GtkWindow* window = window_plugin_get_window(self);
if (window == nullptr) {
return not_ready_response();
}
gtk_window_iconify(window);
self->minimize_requested_at = g_get_monotonic_time();
return success_null();
}
static FlMethodResponse* handle_restore(WindowPlugin* self, FlValue*) {
GtkWindow* window = window_plugin_get_window(self);
if (window == nullptr) {
return not_ready_response();
}
gtk_window_deiconify(window);
gtk_window_present(window);
return success_null();
}
static FlMethodResponse* handle_is_full_screen(WindowPlugin* self,
FlValue*) {
GdkWindow* gdk_window = window_plugin_get_gdk_window(self);
return success_bool(gdk_window != nullptr &&
(gdk_window_get_state(gdk_window) &
GDK_WINDOW_STATE_FULLSCREEN));
}
static FlMethodResponse* handle_set_full_screen(WindowPlugin* self,
FlValue* args) {
bool value;
if (!arg_bool(args, "value", &value)) {
return bad_args_response("setFullScreen", "value");
}
GtkWindow* window = window_plugin_get_window(self);
if (window == nullptr) {
return not_ready_response();
}
if (value) {
gtk_window_fullscreen(window);
} else {
gtk_window_unfullscreen(window);
}
return success_null();
}
static FlMethodResponse* handle_get_bounds(WindowPlugin* self, FlValue*) {
GtkWindow* window = window_plugin_get_window(self);
if (window == nullptr) {
return not_ready_response();
}
gint x, y, width, height;
gtk_window_get_position(window, &x, &y);
gtk_window_get_size(window, &width, &height);
g_autoptr(FlValue) result = fl_value_new_map();
fl_value_set_string_take(result, "x", fl_value_new_float(x));
fl_value_set_string_take(result, "y", fl_value_new_float(y));
fl_value_set_string_take(result, "width", fl_value_new_float(width));
fl_value_set_string_take(result, "height", fl_value_new_float(height));
return success_response(result);
}
static FlMethodResponse* handle_set_bounds(WindowPlugin* self, FlValue* args) {
GtkWindow* window = window_plugin_get_window(self);
if (window == nullptr) {
return not_ready_response();
}
double x, y;
if (arg_double(args, "x", &x) && arg_double(args, "y", &y)) {
gtk_window_move(window, static_cast<gint>(x), static_cast<gint>(y));
}
double width, height;
if (arg_double(args, "width", &width) && arg_double(args, "height", &height)) {
gint w = static_cast<gint>(width);
gint h = static_cast<gint>(height);
if (gtk_widget_get_mapped(GTK_WIDGET(window))) {
gtk_window_resize(window, w, h);
} else {
// A resize request loses to the default-size hint when an unmapped
// window is mapped, so the default size is set instead.
gtk_window_set_default_size(window, w, h);
}
}
return success_null();
}
static FlMethodResponse* handle_is_position_supported([[maybe_unused]] WindowPlugin* self,
FlValue*) {
bool supported = false;
#ifdef GDK_WINDOWING_X11
GtkWindow* window = window_plugin_get_window(self);
if (window != nullptr) {
GdkScreen* screen = gtk_window_get_screen(window);
supported = screen != nullptr && GDK_IS_X11_SCREEN(screen);
}
#endif
return success_bool(supported);
}
static FlMethodResponse* handle_set_minimum_size(WindowPlugin* self,
FlValue* args) {
double width, height;
if (!arg_double(args, "width", &width)) {
return bad_args_response("setMinimumSize", "width");
}
if (!arg_double(args, "height", &height)) {
return bad_args_response("setMinimumSize", "height");
}
GdkWindow* gdk_window = window_plugin_get_gdk_window(self);
if (gdk_window == nullptr) {
return not_ready_response();
}
GdkGeometry hints;
memset(&hints, 0, sizeof(hints));
GdkWindowHints mask = static_cast<GdkWindowHints>(0);
if (width >= 0 && height >= 0) {
hints.min_width = static_cast<gint>(width);
hints.min_height = static_cast<gint>(height);
mask = GDK_HINT_MIN_SIZE;
}
gdk_window_set_geometry_hints(gdk_window, &hints, mask);
return success_null();
}
static FlMethodResponse* handle_is_always_on_top(WindowPlugin* self,
FlValue*) {
return success_bool(self->always_on_top);
}
static FlMethodResponse* handle_set_always_on_top(WindowPlugin* self,
FlValue* args) {
bool value;
if (!arg_bool(args, "value", &value)) {
return bad_args_response("setAlwaysOnTop", "value");
}
GtkWindow* window = window_plugin_get_window(self);
if (window == nullptr) {
return not_ready_response();
}
gtk_window_set_keep_above(window, value);
self->always_on_top = value;
return success_null();
}
static FlMethodResponse* handle_set_title_bar_style(WindowPlugin* self,
FlValue* args) {
const char* style;
if (!arg_string(args, "style", &style)) {
return bad_args_response("setTitleBarStyle", "style");
}
bool window_button_visibility;
if (!arg_bool(args, "windowButtonVisibility", &window_button_visibility)) {
return bad_args_response("setTitleBarStyle", "windowButtonVisibility");
}
g_free(self->style.title_bar_style);
self->style.title_bar_style = g_strdup(style);
self->style.window_button_visibility = window_button_visibility;
window_style_apply(self);
return success_null();
}
static FlMethodResponse* handle_set_skip_taskbar(WindowPlugin* self,
FlValue* args) {
bool value;
if (!arg_bool(args, "value", &value)) {
return bad_args_response("setSkipTaskbar", "value");
}
GtkWindow* window = window_plugin_get_window(self);
if (window == nullptr) {
return not_ready_response();
}
gtk_window_set_skip_taskbar_hint(window, value);
return success_null();
}
static FlMethodResponse* handle_set_rounded_corners(WindowPlugin*,
FlValue* args) {
bool value;
if (!arg_bool(args, "value", &value)) {
return bad_args_response("setRoundedCorners", "value");
}
return success_null();
}
static FlMethodResponse* handle_is_effect_supported(WindowPlugin*,
FlValue* args) {
const char* effect;
if (!arg_string(args, "effect", &effect)) {
return bad_args_response("isEffectSupported", "effect");
}
return success_bool(g_strcmp0(effect, "none") == 0);
}
static FlMethodResponse* handle_set_effect(WindowPlugin*, FlValue* args) {
const char* effect;
if (!arg_string(args, "effect", &effect)) {
return bad_args_response("setEffect", "effect");
}
if (g_strcmp0(effect, "none") != 0) {
return unsupported_response("Linux supports only the \"none\" effect");
}
return success_null();
}
static FlMethodResponse* handle_start_dragging(WindowPlugin* self,
FlValue*) {
return window_drag_start(self);
}
typedef FlMethodResponse* (*MethodHandler)(WindowPlugin*, FlValue*);
typedef struct {
const char* name;
MethodHandler handler;
} MethodEntry;
static const MethodEntry kMethods[] = {
{"ensureInitialized", handle_ensure_initialized},
{"show", handle_show},
{"hide", handle_hide},
{"isVisible", handle_is_visible},
{"focus", handle_focus},
{"close", handle_close},
{"setPreventClose", handle_set_prevent_close},
{"isMaximized", handle_is_maximized},
{"maximize", handle_maximize},
{"unmaximize", handle_unmaximize},
{"isMinimized", handle_is_minimized},
{"minimize", handle_minimize},
{"restore", handle_restore},
{"isFullScreen", handle_is_full_screen},
{"setFullScreen", handle_set_full_screen},
{"getBounds", handle_get_bounds},
{"setBounds", handle_set_bounds},
{"isPositionSupported", handle_is_position_supported},
{"setMinimumSize", handle_set_minimum_size},
{"isAlwaysOnTop", handle_is_always_on_top},
{"setAlwaysOnTop", handle_set_always_on_top},
{"setTitleBarStyle", handle_set_title_bar_style},
{"setSkipTaskbar", handle_set_skip_taskbar},
{"setRoundedCorners", handle_set_rounded_corners},
{"isEffectSupported", handle_is_effect_supported},
{"setEffect", handle_set_effect},
{"startDragging", handle_start_dragging},
};
static void window_plugin_handle_method_call(WindowPlugin* self,
FlMethodCall* method_call) {
const gchar* method = fl_method_call_get_name(method_call);
FlValue* args = fl_method_call_get_args(method_call);
g_autoptr(FlMethodResponse) response = nullptr;
for (const MethodEntry& entry : kMethods) {
if (g_strcmp0(method, entry.name) == 0) {
response = entry.handler(self, args);
break;
}
}
if (response == nullptr) {
response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
}
fl_method_call_respond(method_call, response, nullptr);
}
static void method_call_cb(FlMethodChannel*,
FlMethodCall* method_call,
gpointer user_data) {
window_plugin_handle_method_call(WINDOW_PLUGIN(user_data), method_call);
}
static gboolean on_window_close(GtkWidget*, GdkEvent*,
gpointer data) {
WindowPlugin* self = WINDOW_PLUGIN(data);
window_plugin_emit_event(self, "close");
return self->prevent_close;
}
static gboolean on_window_focus(GtkWidget*, GdkEvent*,
gpointer data) {
window_plugin_emit_event(WINDOW_PLUGIN(data), "focus");
return FALSE;
}
static gboolean on_window_blur(GtkWidget*, GdkEvent*,
gpointer data) {
window_plugin_emit_event(WINDOW_PLUGIN(data), "blur");
return FALSE;
}
static void on_window_show(GtkWidget*, gpointer data) {
window_plugin_emit_event(WINDOW_PLUGIN(data), "show");
}
static void on_window_hide(GtkWidget*, gpointer data) {
window_plugin_emit_event(WINDOW_PLUGIN(data), "hide");
}
static gboolean on_window_map_event(GtkWidget*, GdkEvent*,
gpointer data) {
window_geometry_on_map_event(WINDOW_PLUGIN(data));
return FALSE;
}
static gboolean on_window_configure_event(GtkWidget*, GdkEvent*,
gpointer data) {
window_geometry_on_configure_event(WINDOW_PLUGIN(data));
return FALSE;
}
static void send_lifecycle_hidden(WindowPlugin* self) {
g_autoptr(FlStringCodec) codec = fl_string_codec_new();
g_autoptr(FlValue) state = fl_value_new_string("AppLifecycleState.hidden");
g_autoptr(GBytes) message = fl_message_codec_encode_message(
FL_MESSAGE_CODEC(codec), state, nullptr);
fl_binary_messenger_send_on_channel(
fl_plugin_registrar_get_messenger(self->registrar), "flutter/lifecycle",
message, nullptr, nullptr, nullptr);
}
static gboolean on_window_state_event(GtkWidget*,
GdkEventWindowState* event,
gpointer data) {
WindowPlugin* self = WINDOW_PLUGIN(data);
if (self->minimize_requested_at != 0 &&
(event->changed_mask & GDK_WINDOW_STATE_FOCUSED)) {
gint64 elapsed = g_get_monotonic_time() - self->minimize_requested_at;
self->minimize_requested_at = 0;
if (!(event->new_window_state & GDK_WINDOW_STATE_FOCUSED) &&
elapsed < kMinimizeFocusLossWindow) {
send_lifecycle_hidden(self);
}
}
if (event->changed_mask & GDK_WINDOW_STATE_MAXIMIZED) {
window_plugin_emit_event(
self, (event->new_window_state & GDK_WINDOW_STATE_MAXIMIZED)
? "maximize"
: "unmaximize");
}
if (event->changed_mask & GDK_WINDOW_STATE_ICONIFIED) {
window_plugin_emit_event(
self, (event->new_window_state & GDK_WINDOW_STATE_ICONIFIED)
? "minimize"
: "restore");
}
if (event->changed_mask & GDK_WINDOW_STATE_FULLSCREEN) {
window_plugin_emit_event(
self, (event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN)
? "enter-full-screen"
: "leave-full-screen");
}
return FALSE;
}
static void connect_window_signals(WindowPlugin* self, GtkWindow* window) {
guint i = 0;
self->window_signal_ids[i++] =
g_signal_connect(window, "delete-event", G_CALLBACK(on_window_close), self);
self->window_signal_ids[i++] = g_signal_connect(
window, "focus-in-event", G_CALLBACK(on_window_focus), self);
self->window_signal_ids[i++] = g_signal_connect(
window, "focus-out-event", G_CALLBACK(on_window_blur), self);
self->window_signal_ids[i++] =
g_signal_connect(window, "show", G_CALLBACK(on_window_show), self);
self->window_signal_ids[i++] =
g_signal_connect(window, "hide", G_CALLBACK(on_window_hide), self);
self->window_signal_ids[i++] = g_signal_connect(
window, "map-event", G_CALLBACK(on_window_map_event), self);
self->window_signal_ids[i++] = g_signal_connect(
window, "configure-event", G_CALLBACK(on_window_configure_event), self);
// After the engine's lifecycle monitor, so a minimize reads as hidden.
self->window_signal_ids[i++] = g_signal_connect_after(
window, "window-state-event", G_CALLBACK(on_window_state_event), self);
self->window_signal_count = i;
}
static void window_plugin_dispose(GObject* object) {
WindowPlugin* self = WINDOW_PLUGIN(object);
if (activate_target == self) {
activate_target = nullptr;
}
GtkWindow* window = window_plugin_get_window(self);
if (window != nullptr) {
for (guint i = 0; i < self->window_signal_count; i++) {
g_signal_handler_disconnect(window, self->window_signal_ids[i]);
}
}
window_drag_dispose(self);
window_geometry_dispose(&self->geometry);
window_style_dispose(&self->style);
g_clear_object(&self->registrar);
g_clear_object(&self->channel);
G_OBJECT_CLASS(window_plugin_parent_class)->dispose(object);
}
static void window_plugin_class_init(WindowPluginClass* klass) {
G_OBJECT_CLASS(klass)->dispose = window_plugin_dispose;
}
static void window_plugin_init(WindowPlugin* self) {
window_geometry_init(&self->geometry);
window_style_init(&self->style);
window_drag_init(&self->drag);
}
void window_plugin_register_with_registrar(FlPluginRegistrar* registrar) {
WindowPlugin* plugin =
WINDOW_PLUGIN(g_object_new(window_plugin_get_type(), nullptr));
plugin->registrar = FL_PLUGIN_REGISTRAR(g_object_ref(registrar));
GtkWindow* window = window_plugin_get_window(plugin);
if (window != nullptr) {
// The engine's own delete-event handler on this widget consumes the
// signal before this plugin's runs (flutter/engine#40033), so it is
// disconnected first; matching no handler leaves this a no-op.
guint handler_id = g_signal_handler_find(
window, G_SIGNAL_MATCH_DATA, 0, 0, nullptr, nullptr,
fl_plugin_registrar_get_view(registrar));
if (handler_id > 0) {
g_signal_handler_disconnect(window, handler_id);
}
connect_window_signals(plugin, window);
}
window_drag_connect(plugin);
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
plugin->channel = fl_method_channel_new(
fl_plugin_registrar_get_messenger(registrar), "window",
FL_METHOD_CODEC(codec));
fl_method_channel_set_method_call_handler(
plugin->channel, method_call_cb, g_object_ref(plugin), g_object_unref);
activate_target = plugin;
g_object_unref(plugin);
}
void window_plugin_activate() {
if (activate_target != nullptr) {
window_plugin_emit_event(activate_target, "activate");
}
}