aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/gsr-record-page.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/gsr-record-page.c')
-rw-r--r--src/gsr-record-page.c747
1 files changed, 747 insertions, 0 deletions
diff --git a/src/gsr-record-page.c b/src/gsr-record-page.c
new file mode 100644
index 0000000..a9575c4
--- /dev/null
+++ b/src/gsr-record-page.c
@@ -0,0 +1,747 @@
+#include "gsr-record-page.h"
+#ifdef HAVE_X11
+#include "gsr-shortcut-accel-dialog.h"
+#endif
+#include "gsr-window.h"
+#include <glib/gi18n.h>
+#include <signal.h>
+#include <time.h>
+
+/* ═══════════════════════════════════════════════════════════════════
+ * GsrRecordPage — "Record" tab
+ *
+ * Groups: Output · Action · Status
+ * ═══════════════════════════════════════════════════════════════════ */
+
+struct _GsrRecordPage {
+ AdwPreferencesPage parent_instance;
+
+ const GsrInfo *info; /* borrowed */
+
+ /* ── Hotkey group ─── */
+ AdwPreferencesGroup *hotkey_group;
+#ifdef HAVE_WAYLAND
+ GtkWidget *hotkey_not_supported_label; /* Wayland only */
+ GtkWidget *hotkey_info_row; /* Wayland only */
+#endif
+
+#ifdef HAVE_X11
+ /* X11 interactive hotkey rows */
+ AdwActionRow *x11_start_stop_row;
+ GtkShortcutLabel *x11_start_stop_label;
+ char *x11_start_stop_accel; /* owned */
+ AdwActionRow *x11_pause_row;
+ GtkShortcutLabel *x11_pause_label;
+ char *x11_pause_accel; /* owned */
+#endif
+
+ /* ── Output group ─── */
+ AdwPreferencesGroup *output_group;
+ AdwActionRow *save_dir_row;
+ char *save_directory; /* owned */
+ AdwComboRow *container_row;
+
+ /* ── Action group ─── */
+ AdwPreferencesGroup *action_group;
+ GtkButton *start_button;
+ GtkButton *pause_button;
+
+ /* ── Status group ─── */
+ AdwPreferencesGroup *status_group;
+ GtkBox *status_box;
+ GtkImage *record_icon;
+ GtkLabel *timer_label;
+
+ gboolean is_active;
+ gboolean is_paused;
+ double start_time;
+ double paused_time_offset;
+ double pause_start;
+ guint timer_source_id;
+};
+
+G_DEFINE_FINAL_TYPE(GsrRecordPage, gsr_record_page, ADW_TYPE_PREFERENCES_PAGE)
+
+/* ── Helpers ─────────────────────────────────────────────────────── */
+
+static char *
+get_default_videos_dir(void)
+{
+ const char *vdir = g_get_user_special_dir(G_USER_DIRECTORY_VIDEOS);
+ if (vdir)
+ return g_strdup(vdir);
+ return g_build_filename(g_get_home_dir(), "Videos", NULL);
+}
+
+/* ── Timer helpers ───────────────────────────────────────────────── */
+
+static double
+clock_get_monotonic_seconds(void)
+{
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
+}
+
+static void
+format_timer(double seconds, char *buf, size_t buf_size)
+{
+ int total = (int)seconds;
+ int h = total / 3600;
+ int m = (total % 3600) / 60;
+ int s = total % 60;
+ snprintf(buf, buf_size, "%02d:%02d:%02d", h, m, s);
+}
+
+static gboolean
+on_timer_tick(gpointer user_data)
+{
+ GsrRecordPage *self = GSR_RECORD_PAGE(user_data);
+ double now = clock_get_monotonic_seconds();
+ double elapsed = now - self->start_time - self->paused_time_offset;
+ /* If currently paused, subtract the ongoing pause duration */
+ if (self->is_paused)
+ elapsed -= (now - self->pause_start);
+ char buf[32];
+ format_timer(elapsed, buf, sizeof(buf));
+ gtk_label_set_text(self->timer_label, buf);
+ return G_SOURCE_CONTINUE;
+}
+
+/* ── File chooser ────────────────────────────────────────────────── */
+
+static void
+on_folder_dialog_finish(GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GsrRecordPage *self = GSR_RECORD_PAGE(user_data);
+ GtkFileDialog *dialog = GTK_FILE_DIALOG(source);
+ GFile *folder = gtk_file_dialog_select_folder_finish(dialog, result, NULL);
+ if (!folder) return;
+
+ g_free(self->save_directory);
+ self->save_directory = g_file_get_path(folder);
+ adw_action_row_set_subtitle(self->save_dir_row, self->save_directory);
+ g_object_unref(folder);
+}
+
+static void
+on_save_dir_activated(AdwActionRow *row G_GNUC_UNUSED,
+ gpointer user_data)
+{
+ GsrRecordPage *self = GSR_RECORD_PAGE(user_data);
+ GtkFileDialog *dialog = gtk_file_dialog_new();
+ gtk_file_dialog_set_title(dialog, "Select save directory");
+
+ if (self->save_directory) {
+ GFile *initial = g_file_new_for_path(self->save_directory);
+ gtk_file_dialog_set_initial_folder(dialog, initial);
+ g_object_unref(initial);
+ }
+
+ GtkWindow *win = GTK_WINDOW(gtk_widget_get_root(GTK_WIDGET(self)));
+ gtk_file_dialog_select_folder(dialog, win, NULL,
+ on_folder_dialog_finish, self);
+ g_object_unref(dialog);
+}
+
+/* ── Callbacks ───────────────────────────────────────────────────── */
+
+static void
+on_start_recording_clicked(GtkButton *btn G_GNUC_UNUSED,
+ gpointer user_data)
+{
+ GsrRecordPage *self = GSR_RECORD_PAGE(user_data);
+
+ GtkRoot *root = gtk_widget_get_root(GTK_WIDGET(self));
+ GsrWindow *window = (root && GSR_IS_WINDOW(root)) ? GSR_WINDOW(root) : NULL;
+
+ if (self->is_active) {
+ /* ── Stop ─── */
+ if (window) {
+ gsr_window_stop_process(window, NULL);
+ gsr_window_set_recording_active(window, FALSE);
+ }
+ gsr_record_page_set_active(self, FALSE);
+ /* set_active(FALSE) resets is_active, is_paused, and stops timer */
+ } else {
+ /* ── Start ─── */
+ if (!window) return;
+
+ gboolean ok = gsr_window_start_process(window, GSR_ACTIVE_MODE_RECORD);
+ if (!ok) return; /* fork failed — toast already shown by window */
+
+ self->is_active = TRUE;
+ self->is_paused = FALSE;
+ self->paused_time_offset = 0.0;
+ self->pause_start = 0.0;
+ gsr_record_page_set_active(self, TRUE);
+ gsr_window_set_recording_active(window, TRUE);
+
+ /* Start display timer */
+ self->start_time = clock_get_monotonic_seconds();
+ self->timer_source_id = g_timeout_add(500, on_timer_tick, self);
+ }
+}
+
+static void
+on_pause_recording_clicked(GtkButton *btn G_GNUC_UNUSED,
+ gpointer user_data)
+{
+ GsrRecordPage *self = GSR_RECORD_PAGE(user_data);
+ if (!self->is_active) return;
+
+ GtkRoot *root = gtk_widget_get_root(GTK_WIDGET(self));
+ GsrWindow *window = (root && GSR_IS_WINDOW(root)) ? GSR_WINDOW(root) : NULL;
+
+ /* Send SIGUSR2 to toggle pause/unpause */
+ if (window)
+ gsr_window_send_signal(window, SIGUSR2);
+
+ self->is_paused = !self->is_paused;
+ gsr_record_page_set_paused(self, self->is_paused);
+
+ if (self->is_paused) {
+ self->pause_start = clock_get_monotonic_seconds();
+ } else {
+ /* Accumulate paused duration */
+ self->paused_time_offset += clock_get_monotonic_seconds() - self->pause_start;
+ self->pause_start = 0.0;
+ }
+}
+
+/* ── Build groups ────────────────────────────────────────────────── */
+
+#ifdef HAVE_WAYLAND
+static gboolean
+is_kde_wayland(void)
+{
+ const char *desktop = g_getenv("XDG_CURRENT_DESKTOP");
+ return desktop && g_strstr_len(desktop, -1, "KDE") != NULL;
+}
+#endif
+
+/* ── X11 hotkey row callbacks ────────────────────────────────────── */
+
+#ifdef HAVE_X11
+static void
+on_x11_start_stop_shortcut_set(GsrShortcutAccelDialog *dialog,
+ gpointer user_data)
+{
+ GsrRecordPage *self = GSR_RECORD_PAGE(user_data);
+ const char *accel = gsr_shortcut_accel_dialog_get_accelerator(dialog);
+
+ g_free(self->x11_start_stop_accel);
+ self->x11_start_stop_accel = g_strdup(accel);
+
+ if (self->x11_start_stop_label)
+ gtk_shortcut_label_set_accelerator(self->x11_start_stop_label,
+ accel ? accel : "");
+
+ /* Save config & regrab hotkeys */
+ GtkRoot *root = gtk_widget_get_root(GTK_WIDGET(self));
+ if (root && GSR_IS_WINDOW(root))
+ gsr_window_on_hotkey_changed(GSR_WINDOW(root));
+}
+
+static void
+on_x11_start_stop_activated(AdwActionRow *row G_GNUC_UNUSED,
+ gpointer user_data)
+{
+ GsrRecordPage *self = GSR_RECORD_PAGE(user_data);
+ GsrShortcutAccelDialog *dialog = gsr_shortcut_accel_dialog_new(
+ "Start/Stop recording", self->x11_start_stop_accel);
+ g_signal_connect(dialog, "shortcut-set",
+ G_CALLBACK(on_x11_start_stop_shortcut_set), self);
+ adw_dialog_present(ADW_DIALOG(dialog), GTK_WIDGET(self));
+}
+
+static void
+on_x11_pause_shortcut_set(GsrShortcutAccelDialog *dialog,
+ gpointer user_data)
+{
+ GsrRecordPage *self = GSR_RECORD_PAGE(user_data);
+ const char *accel = gsr_shortcut_accel_dialog_get_accelerator(dialog);
+
+ g_free(self->x11_pause_accel);
+ self->x11_pause_accel = g_strdup(accel);
+
+ if (self->x11_pause_label)
+ gtk_shortcut_label_set_accelerator(self->x11_pause_label,
+ accel ? accel : "");
+
+ /* Save config & regrab hotkeys */
+ GtkRoot *root = gtk_widget_get_root(GTK_WIDGET(self));
+ if (root && GSR_IS_WINDOW(root))
+ gsr_window_on_hotkey_changed(GSR_WINDOW(root));
+}
+
+static void
+on_x11_pause_activated(AdwActionRow *row G_GNUC_UNUSED,
+ gpointer user_data)
+{
+ GsrRecordPage *self = GSR_RECORD_PAGE(user_data);
+ GsrShortcutAccelDialog *dialog = gsr_shortcut_accel_dialog_new(
+ "Pause/Unpause recording", self->x11_pause_accel);
+ g_signal_connect(dialog, "shortcut-set",
+ G_CALLBACK(on_x11_pause_shortcut_set), self);
+ adw_dialog_present(ADW_DIALOG(dialog), GTK_WIDGET(self));
+}
+#endif /* HAVE_X11 */
+
+static void
+build_hotkey_group(GsrRecordPage *self)
+{
+ self->hotkey_group = ADW_PREFERENCES_GROUP(adw_preferences_group_new());
+ adw_preferences_group_set_title(self->hotkey_group, "Hotkeys");
+
+ GsrDisplayServer ds = self->info->system_info.display_server;
+
+#ifdef HAVE_WAYLAND
+ if (ds == GSR_DISPLAY_SERVER_WAYLAND) {
+ /* "Not supported" label — initially hidden, shown if portal fails */
+ self->hotkey_not_supported_label = gtk_label_new(
+ "Your Wayland compositor doesn't support global hotkeys.\n"
+ "Use X11 or KDE Plasma on Wayland if you want to use hotkeys.");
+ gtk_label_set_wrap(GTK_LABEL(self->hotkey_not_supported_label), TRUE);
+ gtk_widget_add_css_class(self->hotkey_not_supported_label, "dim-label");
+ gtk_widget_set_margin_top(self->hotkey_not_supported_label, 6);
+ gtk_widget_set_margin_bottom(self->hotkey_not_supported_label, 6);
+ gtk_widget_set_visible(self->hotkey_not_supported_label, FALSE);
+ adw_preferences_group_add(self->hotkey_group,
+ self->hotkey_not_supported_label);
+
+ /* Info row about Wayland hotkeys */
+ AdwActionRow *info_row = ADW_ACTION_ROW(adw_action_row_new());
+ self->hotkey_info_row = GTK_WIDGET(info_row);
+ if (is_kde_wayland()) {
+ adw_preferences_row_set_title(ADW_PREFERENCES_ROW(info_row),
+ "Hotkeys are managed by KDE Plasma");
+ adw_action_row_set_subtitle(info_row,
+ "Click to configure hotkeys in system settings");
+ GtkButton *change_btn = GTK_BUTTON(
+ gtk_button_new_with_label("Change hotkeys"));
+ gtk_widget_set_valign(GTK_WIDGET(change_btn), GTK_ALIGN_CENTER);
+ g_object_set_data(G_OBJECT(info_row), "change-button", change_btn);
+ adw_action_row_add_suffix(info_row, GTK_WIDGET(change_btn));
+ } else {
+ adw_preferences_row_set_title(ADW_PREFERENCES_ROW(info_row),
+ "Hotkeys are managed by your compositor");
+ adw_action_row_set_subtitle(info_row,
+ "Go to system settings to change hotkeys");
+ }
+ adw_preferences_group_add(self->hotkey_group, GTK_WIDGET(info_row));
+
+ /* Hotkey labels (shown after portal succeeds) */
+ AdwActionRow *start_row = ADW_ACTION_ROW(adw_action_row_new());
+ adw_preferences_row_set_title(ADW_PREFERENCES_ROW(start_row),
+ "Start/Stop recording");
+ adw_action_row_set_subtitle(start_row, "");
+ adw_preferences_group_add(self->hotkey_group, GTK_WIDGET(start_row));
+
+ AdwActionRow *pause_row = ADW_ACTION_ROW(adw_action_row_new());
+ adw_preferences_row_set_title(ADW_PREFERENCES_ROW(pause_row),
+ "Pause/Unpause recording");
+ adw_action_row_set_subtitle(pause_row, "");
+ adw_preferences_group_add(self->hotkey_group, GTK_WIDGET(pause_row));
+
+ /* Initially hidden on Wayland */
+ gtk_widget_set_visible(GTK_WIDGET(self->hotkey_info_row), FALSE);
+ gtk_widget_set_visible(GTK_WIDGET(start_row), FALSE);
+ gtk_widget_set_visible(GTK_WIDGET(pause_row), FALSE);
+ }
+#endif /* HAVE_WAYLAND */
+
+#ifdef HAVE_X11
+ if (ds == GSR_DISPLAY_SERVER_X11) {
+ /* X11: interactive shortcut rows */
+ self->x11_start_stop_row = ADW_ACTION_ROW(adw_action_row_new());
+ adw_preferences_row_set_title(ADW_PREFERENCES_ROW(self->x11_start_stop_row),
+ "Start/Stop recording");
+ gtk_list_box_row_set_activatable(GTK_LIST_BOX_ROW(self->x11_start_stop_row), TRUE);
+
+ self->x11_start_stop_label = GTK_SHORTCUT_LABEL(
+ gtk_shortcut_label_new(self->x11_start_stop_accel ? self->x11_start_stop_accel : ""));
+ gtk_widget_set_valign(GTK_WIDGET(self->x11_start_stop_label), GTK_ALIGN_CENTER);
+ adw_action_row_add_suffix(self->x11_start_stop_row,
+ GTK_WIDGET(self->x11_start_stop_label));
+ GtkImage *arrow1 = GTK_IMAGE(gtk_image_new_from_icon_name("go-next-symbolic"));
+ gtk_widget_add_css_class(GTK_WIDGET(arrow1), "dim-label");
+ adw_action_row_add_suffix(self->x11_start_stop_row, GTK_WIDGET(arrow1));
+
+ g_signal_connect(self->x11_start_stop_row, "activated",
+ G_CALLBACK(on_x11_start_stop_activated), self);
+ adw_preferences_group_add(self->hotkey_group, GTK_WIDGET(self->x11_start_stop_row));
+
+ /* Pause/Unpause row */
+ self->x11_pause_row = ADW_ACTION_ROW(adw_action_row_new());
+ adw_preferences_row_set_title(ADW_PREFERENCES_ROW(self->x11_pause_row),
+ "Pause/Unpause recording");
+ gtk_list_box_row_set_activatable(GTK_LIST_BOX_ROW(self->x11_pause_row), TRUE);
+
+ self->x11_pause_label = GTK_SHORTCUT_LABEL(
+ gtk_shortcut_label_new(self->x11_pause_accel ? self->x11_pause_accel : ""));
+ gtk_widget_set_valign(GTK_WIDGET(self->x11_pause_label), GTK_ALIGN_CENTER);
+ adw_action_row_add_suffix(self->x11_pause_row,
+ GTK_WIDGET(self->x11_pause_label));
+ GtkImage *arrow2 = GTK_IMAGE(gtk_image_new_from_icon_name("go-next-symbolic"));
+ gtk_widget_add_css_class(GTK_WIDGET(arrow2), "dim-label");
+ adw_action_row_add_suffix(self->x11_pause_row, GTK_WIDGET(arrow2));
+
+ g_signal_connect(self->x11_pause_row, "activated",
+ G_CALLBACK(on_x11_pause_activated), self);
+ adw_preferences_group_add(self->hotkey_group, GTK_WIDGET(self->x11_pause_row));
+ }
+#endif /* HAVE_X11 */
+
+ (void)ds;
+
+ adw_preferences_page_add(ADW_PREFERENCES_PAGE(self), self->hotkey_group);
+}
+
+static void
+build_output_group(GsrRecordPage *self)
+{
+ self->output_group = ADW_PREFERENCES_GROUP(adw_preferences_group_new());
+ adw_preferences_group_set_title(self->output_group, "Output");
+
+ /* Save directory */
+ self->save_directory = get_default_videos_dir();
+ self->save_dir_row = ADW_ACTION_ROW(adw_action_row_new());
+ adw_preferences_row_set_title(ADW_PREFERENCES_ROW(self->save_dir_row),
+ "Save directory");
+ adw_action_row_set_subtitle(self->save_dir_row, self->save_directory);
+ gtk_list_box_row_set_activatable(GTK_LIST_BOX_ROW(self->save_dir_row), TRUE);
+
+ GtkImage *folder_icon = GTK_IMAGE(
+ gtk_image_new_from_icon_name("document-open-symbolic"));
+ adw_action_row_add_suffix(self->save_dir_row, GTK_WIDGET(folder_icon));
+
+ g_signal_connect(self->save_dir_row, "activated",
+ G_CALLBACK(on_save_dir_activated), self);
+ adw_preferences_group_add(self->output_group,
+ GTK_WIDGET(self->save_dir_row));
+
+ /* Container format */
+ self->container_row = ADW_COMBO_ROW(adw_combo_row_new());
+ adw_preferences_row_set_title(ADW_PREFERENCES_ROW(self->container_row),
+ "Container");
+
+ GtkStringList *ct_model = gtk_string_list_new(NULL);
+ const char *containers[] = {
+ "mp4", "flv", "mkv", "mov", "ts", "m3u8", NULL
+ };
+ for (int i = 0; containers[i]; i++)
+ gtk_string_list_append(ct_model, containers[i]);
+
+ if (self->info->supported_video_codecs.vp8 ||
+ self->info->supported_video_codecs.vp9)
+ gtk_string_list_append(ct_model, "webm");
+
+ adw_combo_row_set_model(self->container_row, G_LIST_MODEL(ct_model));
+ adw_combo_row_set_selected(self->container_row, 0); /* default: mp4 */
+ adw_preferences_group_add(self->output_group,
+ GTK_WIDGET(self->container_row));
+
+ adw_preferences_page_add(ADW_PREFERENCES_PAGE(self), self->output_group);
+}
+
+static void
+build_action_group(GsrRecordPage *self)
+{
+ self->action_group = ADW_PREFERENCES_GROUP(adw_preferences_group_new());
+
+ GtkBox *box = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12));
+ gtk_widget_set_halign(GTK_WIDGET(box), GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_top(GTK_WIDGET(box), 6);
+ gtk_widget_set_margin_bottom(GTK_WIDGET(box), 6);
+
+ self->start_button = GTK_BUTTON(
+ gtk_button_new_with_label("Start recording"));
+ gtk_widget_set_hexpand(GTK_WIDGET(self->start_button), TRUE);
+ gtk_widget_add_css_class(GTK_WIDGET(self->start_button), "suggested-action");
+ g_signal_connect(self->start_button, "clicked",
+ G_CALLBACK(on_start_recording_clicked), self);
+ gtk_box_append(box, GTK_WIDGET(self->start_button));
+
+ self->pause_button = GTK_BUTTON(
+ gtk_button_new_with_label("Pause recording"));
+ gtk_widget_set_hexpand(GTK_WIDGET(self->pause_button), TRUE);
+ gtk_widget_set_sensitive(GTK_WIDGET(self->pause_button), FALSE);
+ g_signal_connect(self->pause_button, "clicked",
+ G_CALLBACK(on_pause_recording_clicked), self);
+ gtk_box_append(box, GTK_WIDGET(self->pause_button));
+
+ adw_preferences_group_add(self->action_group, GTK_WIDGET(box));
+ adw_preferences_page_add(ADW_PREFERENCES_PAGE(self), self->action_group);
+}
+
+static void
+build_status_group(GsrRecordPage *self)
+{
+ self->status_group = ADW_PREFERENCES_GROUP(adw_preferences_group_new());
+
+ self->status_box = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8));
+ gtk_widget_set_halign(GTK_WIDGET(self->status_box), GTK_ALIGN_CENTER);
+ gtk_widget_set_opacity(GTK_WIDGET(self->status_box), 0.5);
+
+ self->record_icon = GTK_IMAGE(
+ gtk_image_new_from_icon_name("media-record-symbolic"));
+ gtk_box_append(self->status_box, GTK_WIDGET(self->record_icon));
+
+ self->timer_label = GTK_LABEL(gtk_label_new("00:00:00"));
+ gtk_box_append(self->status_box, GTK_WIDGET(self->timer_label));
+
+ adw_preferences_group_add(self->status_group, GTK_WIDGET(self->status_box));
+ adw_preferences_page_add(ADW_PREFERENCES_PAGE(self), self->status_group);
+}
+
+/* ── GObject lifecycle ───────────────────────────────────────────── */
+
+static void
+gsr_record_page_finalize(GObject *object)
+{
+ GsrRecordPage *self = GSR_RECORD_PAGE(object);
+ g_free(self->save_directory);
+#ifdef HAVE_X11
+ g_free(self->x11_start_stop_accel);
+ g_free(self->x11_pause_accel);
+#endif
+ G_OBJECT_CLASS(gsr_record_page_parent_class)->finalize(object);
+}
+
+static void
+gsr_record_page_init(GsrRecordPage *self)
+{
+ (void)self;
+}
+
+static void
+gsr_record_page_class_init(GsrRecordPageClass *klass)
+{
+ GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+ obj_class->finalize = gsr_record_page_finalize;
+}
+
+/* ── Config apply/read ───────────────────────────────────────────── */
+
+/* Helper: select a combo row item by matching string from model */
+static void
+combo_row_select_string(AdwComboRow *row, const char *value)
+{
+ if (!value || !value[0]) return;
+ GListModel *model = adw_combo_row_get_model(row);
+ guint n = g_list_model_get_n_items(model);
+ for (guint i = 0; i < n; i++) {
+ GtkStringObject *obj = GTK_STRING_OBJECT(g_list_model_get_item(model, i));
+ const char *str = gtk_string_object_get_string(obj);
+ g_object_unref(obj);
+ if (g_str_equal(str, value)) {
+ adw_combo_row_set_selected(row, i);
+ return;
+ }
+ }
+}
+
+static const char *
+combo_row_get_selected_string(AdwComboRow *row)
+{
+ GtkStringObject *obj = GTK_STRING_OBJECT(
+ adw_combo_row_get_selected_item(row));
+ return obj ? gtk_string_object_get_string(obj) : "";
+}
+
+/* Map config container ID to display name */
+static const char *
+container_id_to_display(const char *id)
+{
+ if (!id) return "mp4";
+ if (g_str_equal(id, "matroska")) return "mkv";
+ if (g_str_equal(id, "mpegts")) return "ts";
+ if (g_str_equal(id, "hls")) return "m3u8";
+ return id; /* mp4, flv, mov, webm pass through */
+}
+
+static const char *
+container_display_to_id(const char *display)
+{
+ if (!display) return "mp4";
+ if (g_str_equal(display, "mkv")) return "matroska";
+ if (g_str_equal(display, "ts")) return "mpegts";
+ if (g_str_equal(display, "m3u8")) return "hls";
+ return display; /* mp4, flv, mov, webm pass through */
+}
+
+void
+gsr_record_page_apply_config(GsrRecordPage *self, const GsrConfig *config)
+{
+ const GsrRecordConfig *r = &config->record_config;
+
+ /* Save directory */
+ if (r->save_directory && r->save_directory[0]) {
+ g_free(self->save_directory);
+ self->save_directory = g_strdup(r->save_directory);
+ adw_action_row_set_subtitle(self->save_dir_row, self->save_directory);
+ }
+
+ /* Container */
+ combo_row_select_string(self->container_row,
+ container_id_to_display(r->container));
+
+ /* Hotkeys (X11 only) */
+#ifdef HAVE_X11
+ if (self->x11_start_stop_label) {
+ g_free(self->x11_start_stop_accel);
+ self->x11_start_stop_accel = gsr_config_hotkey_to_accel(&r->start_stop_hotkey);
+ gtk_shortcut_label_set_accelerator(self->x11_start_stop_label,
+ self->x11_start_stop_accel ? self->x11_start_stop_accel : "");
+ }
+ if (self->x11_pause_label) {
+ g_free(self->x11_pause_accel);
+ self->x11_pause_accel = gsr_config_hotkey_to_accel(&r->pause_unpause_hotkey);
+ gtk_shortcut_label_set_accelerator(self->x11_pause_label,
+ self->x11_pause_accel ? self->x11_pause_accel : "");
+ }
+#endif
+}
+
+void
+gsr_record_page_read_config(GsrRecordPage *self, GsrConfig *config)
+{
+ GsrRecordConfig *r = &config->record_config;
+
+ /* Save directory */
+ g_free(r->save_directory);
+ r->save_directory = g_strdup(self->save_directory ? self->save_directory : "");
+
+ /* Container */
+ g_free(r->container);
+ r->container = g_strdup(container_display_to_id(
+ combo_row_get_selected_string(self->container_row)));
+
+ /* Hotkeys */
+#ifdef HAVE_X11
+ gsr_config_hotkey_from_accel(&r->start_stop_hotkey, self->x11_start_stop_accel);
+ gsr_config_hotkey_from_accel(&r->pause_unpause_hotkey, self->x11_pause_accel);
+#endif
+}
+
+/* ── Process management API ──────────────────────────────────────── */
+
+void
+gsr_record_page_set_active(GsrRecordPage *self, gboolean active)
+{
+ if (active) {
+ gtk_button_set_label(self->start_button, "Stop recording");
+ gtk_widget_remove_css_class(GTK_WIDGET(self->start_button), "suggested-action");
+ gtk_widget_add_css_class(GTK_WIDGET(self->start_button), "destructive-action");
+ gtk_widget_set_sensitive(GTK_WIDGET(self->pause_button), TRUE);
+ gtk_widget_set_opacity(GTK_WIDGET(self->status_box), 1.0);
+ gtk_widget_add_css_class(GTK_WIDGET(self->record_icon), "recording-active");
+ gtk_widget_remove_css_class(GTK_WIDGET(self->record_icon), "recording-paused");
+ } else {
+ gtk_button_set_label(self->start_button, "Start recording");
+ gtk_widget_remove_css_class(GTK_WIDGET(self->start_button), "destructive-action");
+ gtk_widget_add_css_class(GTK_WIDGET(self->start_button), "suggested-action");
+ gtk_widget_set_sensitive(GTK_WIDGET(self->pause_button), FALSE);
+ gtk_button_set_label(self->pause_button, "Pause recording");
+ gtk_widget_set_opacity(GTK_WIDGET(self->status_box), 0.5);
+ gtk_label_set_text(self->timer_label, "00:00:00");
+ gtk_image_set_from_icon_name(self->record_icon, "media-record-symbolic");
+ gtk_widget_remove_css_class(GTK_WIDGET(self->record_icon), "recording-active");
+ gtk_widget_remove_css_class(GTK_WIDGET(self->record_icon), "recording-paused");
+
+ /* Reset internal state (handles external stop via handle_child_death) */
+ self->is_active = FALSE;
+ self->is_paused = FALSE;
+ if (self->timer_source_id) {
+ g_source_remove(self->timer_source_id);
+ self->timer_source_id = 0;
+ }
+ }
+}
+
+void
+gsr_record_page_set_paused(GsrRecordPage *self, gboolean paused)
+{
+ if (paused) {
+ gtk_button_set_label(self->pause_button, "Unpause recording");
+ gtk_image_set_from_icon_name(self->record_icon, "media-playback-pause-symbolic");
+ gtk_widget_remove_css_class(GTK_WIDGET(self->record_icon), "recording-active");
+ gtk_widget_add_css_class(GTK_WIDGET(self->record_icon), "recording-paused");
+ } else {
+ gtk_button_set_label(self->pause_button, "Pause recording");
+ gtk_image_set_from_icon_name(self->record_icon, "media-record-symbolic");
+ gtk_widget_remove_css_class(GTK_WIDGET(self->record_icon), "recording-paused");
+ gtk_widget_add_css_class(GTK_WIDGET(self->record_icon), "recording-active");
+ }
+}
+
+void
+gsr_record_page_update_timer(GsrRecordPage *self, const char *text)
+{
+ gtk_label_set_text(self->timer_label, text);
+}
+
+const char *
+gsr_record_page_get_save_dir(GsrRecordPage *self)
+{
+ return self->save_directory;
+}
+
+char *
+gsr_record_page_get_container(GsrRecordPage *self)
+{
+ return g_strdup(container_display_to_id(
+ combo_row_get_selected_string(self->container_row)));
+}
+
+/* ── Public API ──────────────────────────────────────────────────── */
+
+GsrRecordPage *
+gsr_record_page_new(const GsrInfo *info)
+{
+ GsrRecordPage *self = g_object_new(GSR_TYPE_RECORD_PAGE, NULL);
+ self->info = info;
+
+ adw_preferences_page_set_title(ADW_PREFERENCES_PAGE(self), "Record");
+ adw_preferences_page_set_icon_name(ADW_PREFERENCES_PAGE(self),
+ "media-record-symbolic");
+
+ build_hotkey_group(self);
+ build_output_group(self);
+ build_action_group(self);
+ build_status_group(self);
+
+ return self;
+}
+
+void
+gsr_record_page_activate_start_stop(GsrRecordPage *self)
+{
+ g_return_if_fail(GSR_IS_RECORD_PAGE(self));
+ gtk_widget_activate(GTK_WIDGET(self->start_button));
+}
+
+void
+gsr_record_page_activate_pause(GsrRecordPage *self)
+{
+ g_return_if_fail(GSR_IS_RECORD_PAGE(self));
+ if (!self->is_active) return;
+ gtk_widget_activate(GTK_WIDGET(self->pause_button));
+}
+
+#ifdef HAVE_WAYLAND
+void
+gsr_record_page_set_wayland_hotkeys_supported(GsrRecordPage *self,
+ gboolean supported)
+{
+ g_return_if_fail(GSR_IS_RECORD_PAGE(self));
+
+ if (self->hotkey_not_supported_label)
+ gtk_widget_set_visible(self->hotkey_not_supported_label, !supported);
+ if (self->hotkey_info_row)
+ gtk_widget_set_visible(self->hotkey_info_row, supported);
+}
+#endif /* HAVE_WAYLAND */