test / unit + widget + golden + a11y (push) Failing after 28s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
StartupWMClass now matches the GTK application ID
(net.schweitz.clide) so the running window groups with the
launcher icon. Desktop file installs as
net.schweitz.clide.desktop per freedesktop convention.
Renamed my_application.{cc,h} to clide_app.{cc,h} with
matching GObject type (ClideApp / CLIDE_APP).
Co-Authored-By: Claude <noreply@anthropic.com>
300 lines
11 KiB
C++
300 lines
11 KiB
C++
#include "clide_app.h"
|
|
|
|
#include <flutter_linux/flutter_linux.h>
|
|
#ifdef GDK_WINDOWING_X11
|
|
#include <gdk/gdkx.h>
|
|
#endif
|
|
#ifdef GDK_WINDOWING_WAYLAND
|
|
#include <gdk/gdkwayland.h>
|
|
#endif
|
|
#ifdef HAS_WAYLAND_CLIENT
|
|
#include "protocols/server-decoration-client-protocol.h"
|
|
#endif
|
|
|
|
#include "flutter/generated_plugin_registrant.h"
|
|
|
|
struct _ClideApp {
|
|
GtkApplication parent_instance;
|
|
char** dart_entrypoint_arguments;
|
|
};
|
|
|
|
G_DEFINE_TYPE(ClideApp, clide_app, GTK_TYPE_APPLICATION)
|
|
|
|
// D-057: KDE server-decoration protocol — request no decorations.
|
|
#ifdef HAS_WAYLAND_CLIENT
|
|
static struct org_kde_kwin_server_decoration_manager* kde_deco_manager = nullptr;
|
|
|
|
static void registry_handler(void* data, struct wl_registry* registry,
|
|
uint32_t id, const char* interface,
|
|
uint32_t version) {
|
|
if (g_strcmp0(interface,
|
|
org_kde_kwin_server_decoration_manager_interface.name) == 0) {
|
|
kde_deco_manager = (struct org_kde_kwin_server_decoration_manager*)
|
|
wl_registry_bind(registry, id,
|
|
&org_kde_kwin_server_decoration_manager_interface, 1);
|
|
}
|
|
}
|
|
|
|
static void registry_remover(void* data, struct wl_registry* registry,
|
|
uint32_t id) {}
|
|
|
|
static const struct wl_registry_listener registry_listener = {
|
|
registry_handler, registry_remover};
|
|
|
|
static void kde_deco_default_mode(void* data,
|
|
struct org_kde_kwin_server_decoration_manager* mgr, uint32_t mode) {}
|
|
|
|
static const struct org_kde_kwin_server_decoration_manager_listener
|
|
kde_deco_manager_listener = {kde_deco_default_mode};
|
|
|
|
static void request_no_server_decorations(GtkWindow* window) {
|
|
GdkDisplay* display = gtk_widget_get_display(GTK_WIDGET(window));
|
|
if (!GDK_IS_WAYLAND_DISPLAY(display)) return;
|
|
|
|
struct wl_display* wl_dpy =
|
|
gdk_wayland_display_get_wl_display(display);
|
|
struct wl_registry* registry = wl_display_get_registry(wl_dpy);
|
|
wl_registry_add_listener(registry, ®istry_listener, nullptr);
|
|
wl_display_roundtrip(wl_dpy);
|
|
|
|
if (kde_deco_manager == nullptr) {
|
|
wl_registry_destroy(registry);
|
|
return;
|
|
}
|
|
|
|
org_kde_kwin_server_decoration_manager_add_listener(
|
|
kde_deco_manager, &kde_deco_manager_listener, nullptr);
|
|
wl_display_roundtrip(wl_dpy);
|
|
|
|
GdkWindow* gdk_win = gtk_widget_get_window(GTK_WIDGET(window));
|
|
if (gdk_win == nullptr) {
|
|
wl_registry_destroy(registry);
|
|
return;
|
|
}
|
|
|
|
struct wl_surface* surface = gdk_wayland_window_get_wl_surface(gdk_win);
|
|
if (surface == nullptr) {
|
|
wl_registry_destroy(registry);
|
|
return;
|
|
}
|
|
|
|
struct org_kde_kwin_server_decoration* deco =
|
|
org_kde_kwin_server_decoration_manager_create(kde_deco_manager, surface);
|
|
if (deco != nullptr) {
|
|
org_kde_kwin_server_decoration_request_mode(deco, 0);
|
|
wl_display_roundtrip(wl_dpy);
|
|
}
|
|
|
|
wl_registry_destroy(registry);
|
|
}
|
|
#endif
|
|
|
|
static void on_window_realize(GtkWidget* widget, gpointer data) {
|
|
GdkWindow* gdk_win = gtk_widget_get_window(widget);
|
|
if (gdk_win != nullptr) {
|
|
gdk_window_set_decorations(gdk_win, (GdkWMDecoration)0);
|
|
}
|
|
#ifdef HAS_WAYLAND_CLIENT
|
|
request_no_server_decorations(GTK_WINDOW(widget));
|
|
#endif
|
|
}
|
|
|
|
static void first_frame_cb(ClideApp* self, FlView* view) {
|
|
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
|
|
}
|
|
|
|
static void clide_app_activate(GApplication* application) {
|
|
ClideApp* self = CLIDE_APP(application);
|
|
GtkWindow* window =
|
|
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
|
|
|
// D-057: frameless custom chrome.
|
|
gtk_window_set_decorated(window, FALSE);
|
|
gtk_window_set_title(window, "clide");
|
|
g_signal_connect(window, "realize", G_CALLBACK(on_window_realize), nullptr);
|
|
|
|
gtk_window_set_default_size(window, 1280, 720);
|
|
|
|
// Resolve icon relative to the executable, not cwd.
|
|
{
|
|
g_autofree gchar* exe_path = g_file_read_link("/proc/self/exe", nullptr);
|
|
if (exe_path != nullptr) {
|
|
g_autofree gchar* exe_dir = g_path_get_dirname(exe_path);
|
|
g_autofree gchar* icon_path = g_build_filename(
|
|
exe_dir, "data", "flutter_assets", "assets", "logo",
|
|
"appicon-256.png", nullptr);
|
|
g_autoptr(GError) icon_error = nullptr;
|
|
GdkPixbuf* icon = gdk_pixbuf_new_from_file(icon_path, &icon_error);
|
|
if (icon != nullptr) {
|
|
gtk_window_set_icon(window, icon);
|
|
g_object_unref(icon);
|
|
}
|
|
}
|
|
}
|
|
|
|
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
|
fl_dart_project_set_dart_entrypoint_arguments(
|
|
project, self->dart_entrypoint_arguments);
|
|
|
|
FlView* view = fl_view_new(project);
|
|
GdkRGBA background_color;
|
|
gdk_rgba_parse(&background_color, "#000000");
|
|
fl_view_set_background_color(view, &background_color);
|
|
gtk_widget_show(GTK_WIDGET(view));
|
|
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
|
|
|
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
|
|
self);
|
|
gtk_widget_realize(GTK_WIDGET(view));
|
|
|
|
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
|
|
|
// D-057: method channel for window controls.
|
|
FlEngine* engine = fl_view_get_engine(view);
|
|
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
|
|
FlMethodChannel* channel = fl_method_channel_new(
|
|
fl_engine_get_binary_messenger(engine), "clide/window",
|
|
FL_METHOD_CODEC(codec));
|
|
g_object_set_data(G_OBJECT(window), "clide_method_channel", channel);
|
|
fl_method_channel_set_method_call_handler(
|
|
channel,
|
|
[](FlMethodChannel* channel, FlMethodCall* method_call,
|
|
gpointer user_data) {
|
|
GtkWindow* w = GTK_WINDOW(user_data);
|
|
const gchar* method = fl_method_call_get_name(method_call);
|
|
g_autoptr(FlMethodResponse) response = nullptr;
|
|
|
|
if (g_strcmp0(method, "startDrag") == 0) {
|
|
GdkDisplay* display = gtk_widget_get_display(GTK_WIDGET(w));
|
|
GdkSeat* seat = gdk_display_get_default_seat(display);
|
|
GdkDevice* pointer = gdk_seat_get_pointer(seat);
|
|
gint x = 0, y = 0;
|
|
gdk_device_get_position(pointer, nullptr, &x, &y);
|
|
gtk_window_begin_move_drag(w, 1, x, y, GDK_CURRENT_TIME);
|
|
response = FL_METHOD_RESPONSE(
|
|
fl_method_success_response_new(fl_value_new_null()));
|
|
} else if (g_strcmp0(method, "startResize") == 0) {
|
|
FlValue* args = fl_method_call_get_args(method_call);
|
|
int edge = 7;
|
|
if (fl_value_get_type(args) == FL_VALUE_TYPE_MAP) {
|
|
FlValue* ve = fl_value_lookup_string(args, "edge");
|
|
if (ve) edge = (int)fl_value_get_int(ve);
|
|
} else if (fl_value_get_type(args) == FL_VALUE_TYPE_INT) {
|
|
edge = (int)fl_value_get_int(args);
|
|
}
|
|
GdkDisplay* display = gtk_widget_get_display(GTK_WIDGET(w));
|
|
GdkSeat* seat = gdk_display_get_default_seat(display);
|
|
GdkDevice* pointer = gdk_seat_get_pointer(seat);
|
|
gint x = 0, y = 0;
|
|
gdk_device_get_position(pointer, nullptr, &x, &y);
|
|
static const GdkWindowEdge edges[] = {
|
|
GDK_WINDOW_EDGE_NORTH_WEST, GDK_WINDOW_EDGE_NORTH,
|
|
GDK_WINDOW_EDGE_NORTH_EAST, GDK_WINDOW_EDGE_WEST,
|
|
GDK_WINDOW_EDGE_EAST, GDK_WINDOW_EDGE_SOUTH_WEST,
|
|
GDK_WINDOW_EDGE_SOUTH, GDK_WINDOW_EDGE_SOUTH_EAST,
|
|
};
|
|
if (edge >= 0 && edge < 8) {
|
|
gtk_window_begin_resize_drag(w, edges[edge], 1, x, y,
|
|
GDK_CURRENT_TIME);
|
|
}
|
|
response = FL_METHOD_RESPONSE(
|
|
fl_method_success_response_new(fl_value_new_null()));
|
|
} else if (g_strcmp0(method, "minimize") == 0) {
|
|
gtk_window_iconify(w);
|
|
response = FL_METHOD_RESPONSE(
|
|
fl_method_success_response_new(fl_value_new_null()));
|
|
} else if (g_strcmp0(method, "maximize") == 0) {
|
|
if (gtk_window_is_maximized(w)) {
|
|
gtk_window_unmaximize(w);
|
|
} else {
|
|
gtk_window_maximize(w);
|
|
}
|
|
response = FL_METHOD_RESPONSE(
|
|
fl_method_success_response_new(fl_value_new_null()));
|
|
} else if (g_strcmp0(method, "close") == 0) {
|
|
gtk_window_close(w);
|
|
response = FL_METHOD_RESPONSE(
|
|
fl_method_success_response_new(fl_value_new_null()));
|
|
} else if (g_strcmp0(method, "isMaximized") == 0) {
|
|
response = FL_METHOD_RESPONSE(fl_method_success_response_new(
|
|
fl_value_new_bool(gtk_window_is_maximized(w))));
|
|
} else if (g_strcmp0(method, "pickDirectory") == 0) {
|
|
GtkWidget* dialog = gtk_file_chooser_dialog_new(
|
|
"Select a project folder", w,
|
|
GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
|
|
"_Cancel", GTK_RESPONSE_CANCEL,
|
|
"_Open", GTK_RESPONSE_ACCEPT,
|
|
nullptr);
|
|
gint res = gtk_dialog_run(GTK_DIALOG(dialog));
|
|
if (res == GTK_RESPONSE_ACCEPT) {
|
|
g_autofree gchar* folder =
|
|
gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
|
|
g_autoptr(FlValue) val = fl_value_new_string(folder);
|
|
response = FL_METHOD_RESPONSE(
|
|
fl_method_success_response_new(val));
|
|
} else {
|
|
response = FL_METHOD_RESPONSE(
|
|
fl_method_success_response_new(fl_value_new_null()));
|
|
}
|
|
gtk_widget_destroy(dialog);
|
|
} else {
|
|
response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
|
|
}
|
|
|
|
fl_method_call_respond(method_call, response, nullptr);
|
|
},
|
|
window, nullptr);
|
|
|
|
gtk_widget_grab_focus(GTK_WIDGET(view));
|
|
}
|
|
|
|
static gboolean clide_app_local_command_line(GApplication* application,
|
|
gchar*** arguments,
|
|
int* exit_status) {
|
|
ClideApp* self = CLIDE_APP(application);
|
|
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
|
|
|
|
g_autoptr(GError) error = nullptr;
|
|
if (!g_application_register(application, nullptr, &error)) {
|
|
g_warning("Failed to register: %s", error->message);
|
|
*exit_status = 1;
|
|
return TRUE;
|
|
}
|
|
|
|
g_application_activate(application);
|
|
*exit_status = 0;
|
|
return TRUE;
|
|
}
|
|
|
|
static void clide_app_startup(GApplication* application) {
|
|
G_APPLICATION_CLASS(clide_app_parent_class)->startup(application);
|
|
}
|
|
|
|
static void clide_app_shutdown(GApplication* application) {
|
|
G_APPLICATION_CLASS(clide_app_parent_class)->shutdown(application);
|
|
}
|
|
|
|
static void clide_app_dispose(GObject* object) {
|
|
ClideApp* self = CLIDE_APP(object);
|
|
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
|
G_OBJECT_CLASS(clide_app_parent_class)->dispose(object);
|
|
}
|
|
|
|
static void clide_app_class_init(ClideAppClass* klass) {
|
|
G_APPLICATION_CLASS(klass)->activate = clide_app_activate;
|
|
G_APPLICATION_CLASS(klass)->local_command_line =
|
|
clide_app_local_command_line;
|
|
G_APPLICATION_CLASS(klass)->startup = clide_app_startup;
|
|
G_APPLICATION_CLASS(klass)->shutdown = clide_app_shutdown;
|
|
G_OBJECT_CLASS(klass)->dispose = clide_app_dispose;
|
|
}
|
|
|
|
static void clide_app_init(ClideApp* self) {}
|
|
|
|
ClideApp* clide_app_new() {
|
|
g_set_prgname(APPLICATION_ID);
|
|
return CLIDE_APP(g_object_new(clide_app_get_type(),
|
|
"application-id", APPLICATION_ID, "flags",
|
|
G_APPLICATION_NON_UNIQUE, nullptr));
|
|
}
|