diff --git a/main.c b/main.c index 937797d..eed5956 100644 --- a/main.c +++ b/main.c @@ -1,24 +1,23 @@ /* - SuperNotepad (GTK3 + GtkSourceView) - C - Features: - - File/Edit/View/Help menus - - Status bar: Ln/Col + Saved/Modified - - Word wrap / Status bar / Line numbers toggles - - .backup file auto-write when modified & unsaved (toggleable) - - Find, Find Next, Replace, Go To - - Time/Date insert - - Print (simple) - - Build: - meson setup build - meson compile -C build - ./build/SuperNotepad -*/ + * SuperNotepad (GTK3 + GtkSourceView4) - C + * + * Added: + * - Recent Files (GtkRecentManager + GtkRecentChooserMenu) + * - Dark mode detection (heuristic + notify on theme/prefer-dark changes) + * - Keyboard shortcuts (GtkAccelGroup) + * + * Build: + * meson setup build + * meson compile -C build + * ./build/SuperNotepad + */ #include #include -#include #include +#include + +/* -------------------- App State -------------------- */ typedef struct { GtkApplication *app; @@ -39,9 +38,16 @@ typedef struct { gboolean show_line_numbers; gboolean backups_enabled; - gchar *current_path; // NULL when untitled - gchar *backup_path; // derived or temp - guint backup_timeout_id; // debounce + gchar *current_path; // NULL when untitled + gchar *backup_path; // derived + guint backup_timeout_id; // debounce timer id + + // Recent files / shortcuts + GtkRecentManager *recent_manager; + GtkAccelGroup *accel_group; + + // Dark mode detection + gboolean is_dark_mode; // Find state gchar *last_find; @@ -49,24 +55,12 @@ typedef struct { gboolean has_last_match; } AppState; -/* ---------- Utility ---------- */ +/* -------------------- Utility -------------------- */ static void free_and_null(gchar **p) { if (*p) { g_free(*p); *p = NULL; } } -static gchar* get_temp_untitled_backup_path(void) { - // Keep it predictable and easy to find - return g_build_filename(g_get_tmp_dir(), "SuperNotepad-Untitled.backup", NULL); -} - -static gchar* compute_backup_path(AppState *s) { - if (s->current_path && *s->current_path) { - return g_strconcat(s->current_path, ".backup", NULL); - } - return get_temp_untitled_backup_path(); -} - static gchar* buffer_get_all_text(GtkTextBuffer *buf) { GtkTextIter start, end; gtk_text_buffer_get_start_iter(buf, &start); @@ -75,15 +69,15 @@ static gchar* buffer_get_all_text(GtkTextBuffer *buf) { } static void update_title(AppState *s) { - const gchar *name = "SuperNotepad"; + const gchar *appname = "SuperNotepad"; gchar *title = NULL; - if (s->current_path) { + if (s->current_path && *s->current_path) { gchar *base = g_path_get_basename(s->current_path); - title = g_strdup_printf("%s - %s", base, name); + title = g_strdup_printf("%s - %s", base, appname); g_free(base); } else { - title = g_strdup_printf("Untitled - %s", name); + title = g_strdup_printf("Untitled - %s", appname); } gtk_window_set_title(GTK_WINDOW(s->win), title); @@ -106,28 +100,147 @@ static void update_status(AppState *s) { gtk_label_set_text(GTK_LABEL(s->state_label), modified ? "Modified (unsaved)" : "Saved"); } -/* ---------- Backup handling ---------- */ +/* -------------------- Preferences -------------------- */ + +static gchar* prefs_path(void) { + gchar *dir = g_build_filename(g_get_user_config_dir(), "SuperNotepad", NULL); + g_mkdir_with_parents(dir, 0700); + gchar *path = g_build_filename(dir, "config.ini", NULL); + g_free(dir); + return path; +} + +static void prefs_save(AppState *s) { + GKeyFile *kf = g_key_file_new(); + + g_key_file_set_boolean(kf, "ui", "show_status_bar", s->show_status_bar); + g_key_file_set_boolean(kf, "ui", "word_wrap", s->word_wrap); + g_key_file_set_boolean(kf, "ui", "show_line_numbers", s->show_line_numbers); + g_key_file_set_boolean(kf, "edit", "backups_enabled", s->backups_enabled); + + gsize len = 0; + gchar *data = g_key_file_to_data(kf, &len, NULL); + + gchar *path = prefs_path(); + (void)g_file_set_contents(path, data, (gssize)len, NULL); + + g_free(path); + g_free(data); + g_key_file_unref(kf); +} + +static void prefs_load(AppState *s) { + gchar *path = prefs_path(); + GKeyFile *kf = g_key_file_new(); + + if (g_key_file_load_from_file(kf, path, G_KEY_FILE_NONE, NULL)) { + if (g_key_file_has_key(kf, "ui", "show_status_bar", NULL)) + s->show_status_bar = g_key_file_get_boolean(kf, "ui", "show_status_bar", NULL); + if (g_key_file_has_key(kf, "ui", "word_wrap", NULL)) + s->word_wrap = g_key_file_get_boolean(kf, "ui", "word_wrap", NULL); + if (g_key_file_has_key(kf, "ui", "show_line_numbers", NULL)) + s->show_line_numbers = g_key_file_get_boolean(kf, "ui", "show_line_numbers", NULL); + if (g_key_file_has_key(kf, "edit", "backups_enabled", NULL)) + s->backups_enabled = g_key_file_get_boolean(kf, "edit", "backups_enabled", NULL); + } + + g_key_file_unref(kf); + g_free(path); +} + +/* -------------------- View Settings -------------------- */ + +static void apply_view_settings(AppState *s) { + gtk_widget_set_visible(s->status_bar_box, s->show_status_bar); + + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(s->view), + s->word_wrap ? GTK_WRAP_WORD_CHAR : GTK_WRAP_NONE); + + gtk_source_view_set_show_line_numbers(s->view, s->show_line_numbers); +} + +/* -------------------- Dark Mode Detection -------------------- */ + +static gboolean strcasestr_contains(const gchar *haystack, const gchar *needle) { + if (!haystack || !needle) return FALSE; + gchar *h = g_ascii_strdown(haystack, -1); + gchar *n = g_ascii_strdown(needle, -1); + gboolean ok = (strstr(h, n) != NULL); + g_free(h); + g_free(n); + return ok; +} + +static void update_dark_mode(AppState *s) { + GtkSettings *settings = gtk_settings_get_default(); + if (!settings) return; + + gboolean prefer_dark = FALSE; + gchar *theme_name = NULL; + + g_object_get(settings, + "gtk-application-prefer-dark-theme", &prefer_dark, + "gtk-theme-name", &theme_name, + NULL); + + // Heuristic: + // - If prefer-dark is true OR theme name contains "dark", consider dark mode active. + gboolean is_dark = prefer_dark || (theme_name && strcasestr_contains(theme_name, "dark")); + + if (s->is_dark_mode != is_dark) { + s->is_dark_mode = is_dark; + // For now we just track it (could later toggle CSS, icons, etc.) + g_message("Dark mode detected: %s (theme=%s prefer-dark=%s)", + s->is_dark_mode ? "ON" : "OFF", + theme_name ? theme_name : "(null)", + prefer_dark ? "true" : "false"); + } + + g_free(theme_name); +} + +static void on_settings_notify(GtkSettings *settings, GParamSpec *pspec, gpointer user_data) { + (void)settings; (void)pspec; + AppState *s = (AppState*)user_data; + update_dark_mode(s); +} + +/* -------------------- Backup Handling -------------------- */ + +static gchar* get_temp_untitled_backup_path(void) { + return g_build_filename(g_get_tmp_dir(), "SuperNotepad-Untitled.backup", NULL); +} + +static gchar* compute_backup_path(AppState *s) { + if (s->current_path && *s->current_path) { + return g_strconcat(s->current_path, ".backup", NULL); + } + return get_temp_untitled_backup_path(); +} + +static void delete_backup_file_if_any(AppState *s) { + if (!s->backup_path) { + s->backup_path = compute_backup_path(s); + } + if (s->backup_path) { + (void)g_remove(s->backup_path); + } +} static gboolean write_backup_now(gpointer user_data) { AppState *s = (AppState*)user_data; s->backup_timeout_id = 0; if (!s->backups_enabled) return G_SOURCE_REMOVE; - if (!gtk_text_buffer_get_modified(GTK_TEXT_BUFFER(s->buffer))) return G_SOURCE_REMOVE; free_and_null(&s->backup_path); s->backup_path = compute_backup_path(s); gchar *text = buffer_get_all_text(GTK_TEXT_BUFFER(s->buffer)); - GError *err = NULL; - - if (!g_file_set_contents(s->backup_path, text, -1, &err)) { - g_warning("Failed writing backup '%s': %s", s->backup_path, err->message); - g_clear_error(&err); - } - + (void)g_file_set_contents(s->backup_path, text, -1, NULL); g_free(text); + return G_SOURCE_REMOVE; } @@ -135,53 +248,30 @@ static void schedule_backup(AppState *s) { if (!s->backups_enabled) return; if (!gtk_text_buffer_get_modified(GTK_TEXT_BUFFER(s->buffer))) return; - // Debounce: write 1 second after last change - if (s->backup_timeout_id != 0) return; + if (s->backup_timeout_id) { + g_source_remove(s->backup_timeout_id); + s->backup_timeout_id = 0; + } s->backup_timeout_id = g_timeout_add(1000, write_backup_now, s); } -static void delete_backup_file_if_any(AppState *s) { - if (!s->backup_path) { - // compute path once, because we may never have written yet - s->backup_path = compute_backup_path(s); - } +/* -------------------- Recent Files Helpers -------------------- */ - if (s->backup_path) { - GError *err = NULL; - if (g_remove(s->backup_path) != 0) { - // ignore failures (file may not exist) - } +static void add_path_to_recent(AppState *s, const gchar *path) { + if (!s->recent_manager || !path || !*path) return; + + GError *err = NULL; + gchar *uri = g_filename_to_uri(path, NULL, &err); + if (!uri) { if (err) g_clear_error(&err); + return; } + + gtk_recent_manager_add_item(s->recent_manager, uri); + g_free(uri); } -/* ---------- File I/O ---------- */ - -static gboolean maybe_confirm_discard(AppState *s) { - if (!gtk_text_buffer_get_modified(GTK_TEXT_BUFFER(s->buffer))) return TRUE; - - GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win), - GTK_DIALOG_MODAL, - GTK_MESSAGE_WARNING, - GTK_BUTTONS_NONE, - "This document has unsaved changes."); - gtk_dialog_add_buttons(GTK_DIALOG(d), - "Cancel", GTK_RESPONSE_CANCEL, - "Discard", GTK_RESPONSE_REJECT, - "Save", GTK_RESPONSE_ACCEPT, - NULL); - - int resp = gtk_dialog_run(GTK_DIALOG(d)); - gtk_widget_destroy(d); - - if (resp == GTK_RESPONSE_ACCEPT) { - // Save - // If no path, force Save As - return FALSE; // caller will handle save flow by calling save action - } - if (resp == GTK_RESPONSE_REJECT) return TRUE; - return FALSE; -} +/* -------------------- File I/O -------------------- */ static gboolean save_to_path(AppState *s, const gchar *path) { gchar *text = buffer_get_all_text(GTK_TEXT_BUFFER(s->buffer)); @@ -192,8 +282,8 @@ static gboolean save_to_path(AppState *s, const gchar *path) { if (!ok) { GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win), - GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, - "Save failed:\n%s", err ? err->message : "Unknown error"); + GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + "Save failed:\n%s", err ? err->message : "Unknown error"); gtk_dialog_run(GTK_DIALOG(d)); gtk_widget_destroy(d); g_clear_error(&err); @@ -207,66 +297,121 @@ static gboolean save_to_path(AppState *s, const gchar *path) { update_title(s); update_status(s); - // Once saved, remove backup (it’s no longer “unsaved”) delete_backup_file_if_any(s); + add_path_to_recent(s, path); return TRUE; } +static gboolean open_from_path(AppState *s, const gchar *path) { + if (!path || !*path) return FALSE; + + gchar *contents = NULL; + gsize len = 0; + GError *err = NULL; + + if (!g_file_get_contents(path, &contents, &len, &err)) { + GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win), + GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + "Open failed:\n%s", err ? err->message : "Unknown error"); + gtk_dialog_run(GTK_DIALOG(d)); + gtk_widget_destroy(d); + g_clear_error(&err); + return FALSE; + } + + gtk_text_buffer_set_text(GTK_TEXT_BUFFER(s->buffer), contents, (gint)len); + gtk_text_buffer_set_modified(GTK_TEXT_BUFFER(s->buffer), FALSE); + + free_and_null(&s->current_path); + s->current_path = g_strdup(path); + + free_and_null(&s->backup_path); + free_and_null(&s->last_find); + s->has_last_match = FALSE; + + update_title(s); + update_status(s); + + g_free(contents); + + add_path_to_recent(s, path); + return TRUE; +} + +static gboolean do_save_as(AppState *s) { + GtkWidget *dlg = gtk_file_chooser_dialog_new("Save As", + GTK_WINDOW(s->win), + GTK_FILE_CHOOSER_ACTION_SAVE, + "_Cancel", GTK_RESPONSE_CANCEL, + "_Save", GTK_RESPONSE_ACCEPT, + NULL); + + gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dlg), TRUE); + if (s->current_path) { + gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dlg), s->current_path); + } + + int resp = gtk_dialog_run(GTK_DIALOG(dlg)); + if (resp != GTK_RESPONSE_ACCEPT) { gtk_widget_destroy(dlg); return FALSE; } + + char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dlg)); + gtk_widget_destroy(dlg); + if (!filename) return FALSE; + + gboolean ok = save_to_path(s, filename); + g_free(filename); + return ok; +} + +/* Clean close/save prompt */ +static gboolean ensure_saved_or_cancel(AppState *s) { + if (!gtk_text_buffer_get_modified(GTK_TEXT_BUFFER(s->buffer))) return TRUE; + + GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win), + GTK_DIALOG_MODAL, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_NONE, + "Save changes to this document?"); + gtk_dialog_add_buttons(GTK_DIALOG(d), + "Cancel", GTK_RESPONSE_CANCEL, + "Don't Save", GTK_RESPONSE_REJECT, + "Save", GTK_RESPONSE_ACCEPT, + NULL); + + int resp = gtk_dialog_run(GTK_DIALOG(d)); + gtk_widget_destroy(d); + + if (resp == GTK_RESPONSE_CANCEL) return FALSE; + if (resp == GTK_RESPONSE_REJECT) return TRUE; + + if (s->current_path) { + return save_to_path(s, s->current_path); + } + return do_save_as(s); +} + +static gboolean on_window_delete_event(GtkWidget *w, GdkEvent *e, gpointer user_data) { + (void)w; (void)e; + AppState *s = (AppState*)user_data; + + if (!ensure_saved_or_cancel(s)) return TRUE; // block close + return FALSE; // allow close +} + +/* -------------------- Actions: File Menu -------------------- */ + static void action_new(GtkWidget *w, gpointer user_data) { (void)w; AppState *s = (AppState*)user_data; - if (gtk_text_buffer_get_modified(GTK_TEXT_BUFFER(s->buffer))) { - GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win), - GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, - "Save changes before creating a new document?"); - gtk_dialog_add_buttons(GTK_DIALOG(d), - "Cancel", GTK_RESPONSE_CANCEL, - "Don't Save", GTK_RESPONSE_REJECT, - "Save", GTK_RESPONSE_ACCEPT, - NULL); - int resp = gtk_dialog_run(GTK_DIALOG(d)); - gtk_widget_destroy(d); - - if (resp == GTK_RESPONSE_CANCEL) return; - if (resp == GTK_RESPONSE_ACCEPT) { - // delegate to save; if no path, do Save As - // We'll just call the save handler and let it decide. - // If user cancels save dialog, abort new. - // The easiest is: do a Save As if no path, else Save. - // We'll implement inline here. - - if (s->current_path) { - if (!save_to_path(s, s->current_path)) return; - } else { - GtkWidget *dlg = gtk_file_chooser_dialog_new("Save As", - GTK_WINDOW(s->win), - GTK_FILE_CHOOSER_ACTION_SAVE, - "_Cancel", GTK_RESPONSE_CANCEL, - "_Save", GTK_RESPONSE_ACCEPT, - NULL); - gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dlg), TRUE); - int r = gtk_dialog_run(GTK_DIALOG(dlg)); - if (r == GTK_RESPONSE_ACCEPT) { - char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dlg)); - gtk_widget_destroy(dlg); - if (!filename) return; - gboolean ok = save_to_path(s, filename); - g_free(filename); - if (!ok) return; - } else { - gtk_widget_destroy(dlg); - return; - } - } - } - // else "Don't Save": continue - } + if (!ensure_saved_or_cancel(s)) return; gtk_text_buffer_set_text(GTK_TEXT_BUFFER(s->buffer), "", -1); gtk_text_buffer_set_modified(GTK_TEXT_BUFFER(s->buffer), FALSE); + free_and_null(&s->current_path); + free_and_null(&s->backup_path); free_and_null(&s->last_find); s->has_last_match = FALSE; @@ -279,52 +424,14 @@ static void action_open(GtkWidget *w, gpointer user_data) { (void)w; AppState *s = (AppState*)user_data; - if (gtk_text_buffer_get_modified(GTK_TEXT_BUFFER(s->buffer))) { - GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win), - GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, - "Save changes before opening another file?"); - gtk_dialog_add_buttons(GTK_DIALOG(d), - "Cancel", GTK_RESPONSE_CANCEL, - "Don't Save", GTK_RESPONSE_REJECT, - "Save", GTK_RESPONSE_ACCEPT, - NULL); - int resp = gtk_dialog_run(GTK_DIALOG(d)); - gtk_widget_destroy(d); - - if (resp == GTK_RESPONSE_CANCEL) return; - if (resp == GTK_RESPONSE_ACCEPT) { - if (s->current_path) { - if (!save_to_path(s, s->current_path)) return; - } else { - GtkWidget *sd = gtk_file_chooser_dialog_new("Save As", - GTK_WINDOW(s->win), - GTK_FILE_CHOOSER_ACTION_SAVE, - "_Cancel", GTK_RESPONSE_CANCEL, - "_Save", GTK_RESPONSE_ACCEPT, - NULL); - gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(sd), TRUE); - int r = gtk_dialog_run(GTK_DIALOG(sd)); - if (r == GTK_RESPONSE_ACCEPT) { - char *fn = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(sd)); - gtk_widget_destroy(sd); - if (!fn) return; - gboolean ok = save_to_path(s, fn); - g_free(fn); - if (!ok) return; - } else { - gtk_widget_destroy(sd); - return; - } - } - } - } + if (!ensure_saved_or_cancel(s)) return; GtkWidget *dlg = gtk_file_chooser_dialog_new("Open", - GTK_WINDOW(s->win), - GTK_FILE_CHOOSER_ACTION_OPEN, - "_Cancel", GTK_RESPONSE_CANCEL, - "_Open", GTK_RESPONSE_ACCEPT, - NULL); + GTK_WINDOW(s->win), + GTK_FILE_CHOOSER_ACTION_OPEN, + "_Cancel", GTK_RESPONSE_CANCEL, + "_Open", GTK_RESPONSE_ACCEPT, + NULL); int resp = gtk_dialog_run(GTK_DIALOG(dlg)); if (resp != GTK_RESPONSE_ACCEPT) { gtk_widget_destroy(dlg); return; } @@ -333,184 +440,116 @@ static void action_open(GtkWidget *w, gpointer user_data) { gtk_widget_destroy(dlg); if (!filename) return; - gchar *contents = NULL; - gsize len = 0; - GError *err = NULL; - - if (!g_file_get_contents(filename, &contents, &len, &err)) { - GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win), - GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, - "Open failed:\n%s", err ? err->message : "Unknown error"); - gtk_dialog_run(GTK_DIALOG(d)); - gtk_widget_destroy(d); - g_clear_error(&err); - g_free(filename); - return; - } - - gtk_text_buffer_set_text(GTK_TEXT_BUFFER(s->buffer), contents, (gint)len); - gtk_text_buffer_set_modified(GTK_TEXT_BUFFER(s->buffer), FALSE); - - free_and_null(&s->current_path); - s->current_path = g_strdup(filename); - - free_and_null(&s->last_find); - s->has_last_match = FALSE; - - update_title(s); - update_status(s); - - // backup path changes with opened file - free_and_null(&s->backup_path); - - g_free(contents); + (void)open_from_path(s, filename); g_free(filename); } -static void action_save_as(GtkWidget *w, gpointer user_data); - static void action_save(GtkWidget *w, gpointer user_data) { (void)w; AppState *s = (AppState*)user_data; if (!s->current_path) { - action_save_as(NULL, s); + (void)do_save_as(s); return; } - - save_to_path(s, s->current_path); + (void)save_to_path(s, s->current_path); } static void action_save_as(GtkWidget *w, gpointer user_data) { (void)w; AppState *s = (AppState*)user_data; - - GtkWidget *dlg = gtk_file_chooser_dialog_new("Save As", - GTK_WINDOW(s->win), - GTK_FILE_CHOOSER_ACTION_SAVE, - "_Cancel", GTK_RESPONSE_CANCEL, - "_Save", GTK_RESPONSE_ACCEPT, - NULL); - - gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dlg), TRUE); - - if (s->current_path) { - gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dlg), s->current_path); - } - - int resp = gtk_dialog_run(GTK_DIALOG(dlg)); - if (resp != GTK_RESPONSE_ACCEPT) { gtk_widget_destroy(dlg); return; } - - char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dlg)); - gtk_widget_destroy(dlg); - if (!filename) return; - - save_to_path(s, filename); - - g_free(filename); + (void)do_save_as(s); } static void action_exit(GtkWidget *w, gpointer user_data) { (void)w; AppState *s = (AppState*)user_data; - - if (gtk_text_buffer_get_modified(GTK_TEXT_BUFFER(s->buffer))) { - GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win), - GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, - "Save changes before exiting?"); - gtk_dialog_add_buttons(GTK_DIALOG(d), - "Cancel", GTK_RESPONSE_CANCEL, - "Don't Save", GTK_RESPONSE_REJECT, - "Save", GTK_RESPONSE_ACCEPT, - NULL); - int resp = gtk_dialog_run(GTK_DIALOG(d)); - gtk_widget_destroy(d); - - if (resp == GTK_RESPONSE_CANCEL) return; - if (resp == GTK_RESPONSE_ACCEPT) { - if (s->current_path) { - if (!save_to_path(s, s->current_path)) return; - } else { - action_save_as(NULL, s); - if (gtk_text_buffer_get_modified(GTK_TEXT_BUFFER(s->buffer))) return; // user canceled - } - } - } - gtk_window_close(GTK_WINDOW(s->win)); } -/* ---------- Print ---------- */ +/* Recent chooser callback */ +static void on_recent_item_activated(GtkRecentChooser *chooser, gpointer user_data) { + AppState *s = (AppState*)user_data; -typedef struct { - gchar *text; -} PrintData; + if (!ensure_saved_or_cancel(s)) return; -static void print_data_free(PrintData *pd) { - if (!pd) return; - g_free(pd->text); - g_free(pd); + gchar *uri = gtk_recent_chooser_get_current_uri(chooser); + if (!uri) return; + + GError *err = NULL; + gchar *filename = g_filename_from_uri(uri, NULL, &err); + g_free(uri); + + if (!filename) { + if (err) g_clear_error(&err); + return; + } + + (void)open_from_path(s, filename); + g_free(filename); } +/* -------------------- Print: Real pagination -------------------- */ + +typedef struct { + GtkSourcePrintCompositor *comp; +} PrintState; + static void on_print_begin(GtkPrintOperation *op, GtkPrintContext *ctx, gpointer user_data) { - (void)op; (void)ctx; (void)user_data; - // single page simplistic; could paginate by measuring + PrintState *ps = (PrintState*)user_data; + + while (!gtk_source_print_compositor_paginate(ps->comp, ctx)) { } + + int pages = gtk_source_print_compositor_get_n_pages(ps->comp); + gtk_print_operation_set_n_pages(op, pages); } static void on_print_draw_page(GtkPrintOperation *op, GtkPrintContext *ctx, int page_nr, gpointer user_data) { - (void)op; (void)page_nr; - PrintData *pd = (PrintData*)user_data; + (void)op; + PrintState *ps = (PrintState*)user_data; + gtk_source_print_compositor_draw_page(ps->comp, ctx, page_nr); +} - cairo_t *cr = gtk_print_context_get_cairo_context(ctx); - PangoLayout *layout = gtk_print_context_create_pango_layout(ctx); - - pango_layout_set_text(layout, pd->text ? pd->text : "", -1); - - // Wrap to page width - double width = gtk_print_context_get_width(ctx); - pango_layout_set_width(layout, (int)(width * PANGO_SCALE)); - pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR); - - cairo_move_to(cr, 0, 0); - pango_cairo_show_layout(cr, layout); - - g_object_unref(layout); +static void on_print_done(GtkPrintOperation *op, GtkPrintOperationResult res, gpointer user_data) { + (void)op; (void)res; + PrintState *ps = (PrintState*)user_data; + if (ps->comp) g_object_unref(ps->comp); + g_free(ps); } static void action_print(GtkWidget *w, gpointer user_data) { (void)w; AppState *s = (AppState*)user_data; - PrintData *pd = g_new0(PrintData, 1); - pd->text = buffer_get_all_text(GTK_TEXT_BUFFER(s->buffer)); + PrintState *ps = g_new0(PrintState, 1); + ps->comp = gtk_source_print_compositor_new(GTK_SOURCE_BUFFER(s->buffer)); GtkPrintOperation *op = gtk_print_operation_new(); gtk_print_operation_set_job_name(op, "SuperNotepad Print"); - gtk_print_operation_set_n_pages(op, 1); - g_signal_connect(op, "begin-print", G_CALLBACK(on_print_begin), pd); - g_signal_connect(op, "draw-page", G_CALLBACK(on_print_draw_page), pd); + g_signal_connect(op, "begin-print", G_CALLBACK(on_print_begin), ps); + g_signal_connect(op, "draw-page", G_CALLBACK(on_print_draw_page), ps); + g_signal_connect(op, "done", G_CALLBACK(on_print_done), ps); GError *err = NULL; - GtkPrintOperationResult res = gtk_print_operation_run(op, - GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG, - GTK_WINDOW(s->win), - &err); + GtkPrintOperationResult r = gtk_print_operation_run(op, + GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG, + GTK_WINDOW(s->win), + &err); - if (res == GTK_PRINT_OPERATION_RESULT_ERROR) { + if (r == GTK_PRINT_OPERATION_RESULT_ERROR) { GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win), - GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, - "Print failed:\n%s", err ? err->message : "Unknown error"); + GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + "Print failed:\n%s", err ? err->message : "Unknown error"); gtk_dialog_run(GTK_DIALOG(d)); gtk_widget_destroy(d); g_clear_error(&err); } g_object_unref(op); - print_data_free(pd); } -/* ---------- Edit actions ---------- */ +/* -------------------- Actions: Edit Menu -------------------- */ static void action_undo(GtkWidget *w, gpointer user_data) { (void)w; @@ -565,11 +604,12 @@ static void action_time_date(GtkWidget *w, gpointer user_data) { time_t t = time(NULL); struct tm lt; -#if defined(_WIN32) + #if defined(_WIN32) localtime_s(<, &t); -#else + #else localtime_r(&t, <); -#endif + #endif + char buf[128]; strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", <); @@ -579,7 +619,7 @@ static void action_time_date(GtkWidget *w, gpointer user_data) { gtk_text_buffer_insert(GTK_TEXT_BUFFER(s->buffer), &iter, buf, -1); } -/* ---------- Find / Replace / Go To ---------- */ +/* -------------------- Find / Replace / Go To -------------------- */ static gboolean do_find_from_iter(AppState *s, const gchar *needle, GtkTextIter *from, gboolean wrap) { if (!needle || !*needle) return FALSE; @@ -588,15 +628,15 @@ static gboolean do_find_from_iter(AppState *s, const gchar *needle, GtkTextIter GtkTextIter match_start, match_end; gboolean found = gtk_text_iter_forward_search(&start, needle, - GTK_TEXT_SEARCH_TEXT_ONLY | GTK_TEXT_SEARCH_VISIBLE_ONLY, - &match_start, &match_end, NULL); + GTK_TEXT_SEARCH_TEXT_ONLY | GTK_TEXT_SEARCH_VISIBLE_ONLY, + &match_start, &match_end, NULL); if (!found && wrap) { GtkTextIter begin; gtk_text_buffer_get_start_iter(GTK_TEXT_BUFFER(s->buffer), &begin); found = gtk_text_iter_forward_search(&begin, needle, - GTK_TEXT_SEARCH_TEXT_ONLY | GTK_TEXT_SEARCH_VISIBLE_ONLY, - &match_start, &match_end, NULL); + GTK_TEXT_SEARCH_TEXT_ONLY | GTK_TEXT_SEARCH_VISIBLE_ONLY, + &match_start, &match_end, NULL); } if (found) { @@ -616,15 +656,16 @@ static void action_find(GtkWidget *w, gpointer user_data) { AppState *s = (AppState*)user_data; GtkWidget *dlg = gtk_dialog_new_with_buttons("Find", - GTK_WINDOW(s->win), - GTK_DIALOG_MODAL, - "_Cancel", GTK_RESPONSE_CANCEL, - "_Find", GTK_RESPONSE_ACCEPT, - NULL); + GTK_WINDOW(s->win), + GTK_DIALOG_MODAL, + "_Cancel", GTK_RESPONSE_CANCEL, + "_Find", GTK_RESPONSE_ACCEPT, + NULL); GtkWidget *box = gtk_dialog_get_content_area(GTK_DIALOG(dlg)); GtkWidget *entry = gtk_entry_new(); gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE); + gtk_box_pack_start(GTK_BOX(box), gtk_label_new("Find what:"), FALSE, FALSE, 6); gtk_box_pack_start(GTK_BOX(box), entry, FALSE, FALSE, 6); @@ -649,8 +690,8 @@ static void action_find(GtkWidget *w, gpointer user_data) { if (!ok) { GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win), - GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK, - "Text not found."); + GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK, + "Text not found."); gtk_dialog_run(GTK_DIALOG(d)); gtk_widget_destroy(d); } @@ -659,14 +700,16 @@ static void action_find(GtkWidget *w, gpointer user_data) { static void action_find_next(GtkWidget *w, gpointer user_data) { (void)w; AppState *s = (AppState*)user_data; + if (!s->last_find || !*s->last_find) { action_find(NULL, s); return; } GtkTextIter from; - if (s->has_last_match) from = s->last_match_end; - else { + if (s->has_last_match) { + from = s->last_match_end; + } else { GtkTextMark *mark = gtk_text_buffer_get_insert(GTK_TEXT_BUFFER(s->buffer)); gtk_text_buffer_get_iter_at_mark(GTK_TEXT_BUFFER(s->buffer), &from, mark); } @@ -674,8 +717,8 @@ static void action_find_next(GtkWidget *w, gpointer user_data) { gboolean ok = do_find_from_iter(s, s->last_find, &from, TRUE); if (!ok) { GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win), - GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK, - "No further matches."); + GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK, + "No further matches."); gtk_dialog_run(GTK_DIALOG(d)); gtk_widget_destroy(d); } @@ -686,11 +729,11 @@ static void action_go_to(GtkWidget *w, gpointer user_data) { AppState *s = (AppState*)user_data; GtkWidget *dlg = gtk_dialog_new_with_buttons("Go To Line", - GTK_WINDOW(s->win), - GTK_DIALOG_MODAL, - "_Cancel", GTK_RESPONSE_CANCEL, - "_Go", GTK_RESPONSE_ACCEPT, - NULL); + GTK_WINDOW(s->win), + GTK_DIALOG_MODAL, + "_Cancel", GTK_RESPONSE_CANCEL, + "_Go", GTK_RESPONSE_ACCEPT, + NULL); GtkWidget *box = gtk_dialog_get_content_area(GTK_DIALOG(dlg)); GtkWidget *spin = gtk_spin_button_new_with_range(1, 1000000, 1); @@ -711,6 +754,7 @@ static void action_go_to(GtkWidget *w, gpointer user_data) { gtk_text_buffer_get_iter_at_line(GTK_TEXT_BUFFER(s->buffer), &iter, line - 1); gtk_text_buffer_place_cursor(GTK_TEXT_BUFFER(s->buffer), &iter); gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(s->view), &iter, 0.2, FALSE, 0, 0); + update_status(s); } @@ -719,12 +763,12 @@ static void action_replace(GtkWidget *w, gpointer user_data) { AppState *s = (AppState*)user_data; GtkWidget *dlg = gtk_dialog_new_with_buttons("Replace", - GTK_WINDOW(s->win), - GTK_DIALOG_MODAL, - "_Close", GTK_RESPONSE_CLOSE, - "_Replace", 1, - "Replace _All", 2, - NULL); + GTK_WINDOW(s->win), + GTK_DIALOG_MODAL, + "_Close", GTK_RESPONSE_CLOSE, + "_Replace", 1, + "Replace _All", 2, + NULL); GtkWidget *box = gtk_dialog_get_content_area(GTK_DIALOG(dlg)); GtkWidget *find_entry = gtk_entry_new(); @@ -748,8 +792,8 @@ static void action_replace(GtkWidget *w, gpointer user_data) { if (!needle || !*needle) { GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win), - GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK, - "Enter text to find."); + GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK, + "Enter text to find."); gtk_dialog_run(GTK_DIALOG(d)); gtk_widget_destroy(d); continue; @@ -759,7 +803,6 @@ static void action_replace(GtkWidget *w, gpointer user_data) { s->last_find = g_strdup(needle); if (resp == 1) { - // Replace: if selection matches needle, replace it; else find next GtkTextIter sel_start, sel_end; if (gtk_text_buffer_get_selection_bounds(GTK_TEXT_BUFFER(s->buffer), &sel_start, &sel_end)) { gchar *selected = gtk_text_buffer_get_text(GTK_TEXT_BUFFER(s->buffer), &sel_start, &sel_end, TRUE); @@ -771,9 +814,9 @@ static void action_replace(GtkWidget *w, gpointer user_data) { gtk_text_buffer_delete(GTK_TEXT_BUFFER(s->buffer), &sel_start, &sel_end); gtk_text_buffer_insert(GTK_TEXT_BUFFER(s->buffer), &sel_start, repl, -1); gtk_text_buffer_end_user_action(GTK_TEXT_BUFFER(s->buffer)); - // After replace, find next occurrence + GtkTextIter from = sel_start; - do_find_from_iter(s, needle, &from, TRUE); + (void)do_find_from_iter(s, needle, &from, TRUE); continue; } } @@ -783,7 +826,6 @@ static void action_replace(GtkWidget *w, gpointer user_data) { } if (resp == 2) { - // Replace All GtkTextIter iter; gtk_text_buffer_get_start_iter(GTK_TEXT_BUFFER(s->buffer), &iter); @@ -793,22 +835,22 @@ static void action_replace(GtkWidget *w, gpointer user_data) { while (1) { GtkTextIter ms, me; gboolean found = gtk_text_iter_forward_search(&iter, needle, - GTK_TEXT_SEARCH_TEXT_ONLY | GTK_TEXT_SEARCH_VISIBLE_ONLY, - &ms, &me, NULL); + GTK_TEXT_SEARCH_TEXT_ONLY | GTK_TEXT_SEARCH_VISIBLE_ONLY, + &ms, &me, NULL); if (!found) break; gtk_text_buffer_delete(GTK_TEXT_BUFFER(s->buffer), &ms, &me); gtk_text_buffer_insert(GTK_TEXT_BUFFER(s->buffer), &ms, repl, -1); - iter = ms; // continue after inserted text + iter = ms; count++; } gtk_text_buffer_end_user_action(GTK_TEXT_BUFFER(s->buffer)); GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win), - GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK, - "Replaced %d occurrence(s).", count); + GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK, + "Replaced %d occurrence(s).", count); gtk_dialog_run(GTK_DIALOG(d)); gtk_widget_destroy(d); @@ -819,71 +861,124 @@ static void action_replace(GtkWidget *w, gpointer user_data) { gtk_widget_destroy(dlg); } -/* ---------- View toggles ---------- */ - -static void apply_view_settings(AppState *s) { - gtk_widget_set_visible(s->status_bar_box, s->show_status_bar); - - gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(s->view), - s->word_wrap ? GTK_WRAP_WORD_CHAR : GTK_WRAP_NONE); - - gtk_source_view_set_show_line_numbers(s->view, s->show_line_numbers); -} +/* -------------------- Toggles (View/Edit) -------------------- */ static void toggle_status_bar(GtkCheckMenuItem *mi, gpointer user_data) { AppState *s = (AppState*)user_data; s->show_status_bar = gtk_check_menu_item_get_active(mi); apply_view_settings(s); + prefs_save(s); } static void toggle_word_wrap(GtkCheckMenuItem *mi, gpointer user_data) { AppState *s = (AppState*)user_data; s->word_wrap = gtk_check_menu_item_get_active(mi); apply_view_settings(s); + prefs_save(s); } static void toggle_line_numbers(GtkCheckMenuItem *mi, gpointer user_data) { AppState *s = (AppState*)user_data; s->show_line_numbers = gtk_check_menu_item_get_active(mi); apply_view_settings(s); + prefs_save(s); } -/* ---------- Backup toggle (Edit menu) ---------- */ - static void toggle_backups(GtkCheckMenuItem *mi, gpointer user_data) { AppState *s = (AppState*)user_data; s->backups_enabled = gtk_check_menu_item_get_active(mi); if (!s->backups_enabled) { - // Stop pending backup and remove any backup file if (s->backup_timeout_id) { g_source_remove(s->backup_timeout_id); s->backup_timeout_id = 0; } delete_backup_file_if_any(s); } else { - // If currently modified, schedule one schedule_backup(s); } + + prefs_save(s); } -/* ---------- Help / About ---------- */ +/* -------------------- About -------------------- */ static void action_about(GtkWidget *w, gpointer user_data) { (void)w; AppState *s = (AppState*)user_data; - GtkWidget *dlg = gtk_message_dialog_new(GTK_WINDOW(s->win), - GTK_DIALOG_MODAL, - GTK_MESSAGE_INFO, - GTK_BUTTONS_OK, - "SuperNotepage 0.1 by John Paul Wohlscheid and ChatGPT"); - gtk_window_set_title(GTK_WINDOW(dlg), "About"); + GtkWidget *dlg = gtk_about_dialog_new(); + gtk_window_set_transient_for(GTK_WINDOW(dlg), GTK_WINDOW(s->win)); + gtk_window_set_modal(GTK_WINDOW(dlg), TRUE); + + gtk_about_dialog_set_program_name(GTK_ABOUT_DIALOG(dlg), "SuperNotepad"); + gtk_about_dialog_set_version(GTK_ABOUT_DIALOG(dlg), "0.2"); + + const gchar *authors[] = { "John Paul Wohlscheid", "ChatGPT", NULL }; + gtk_about_dialog_set_authors(GTK_ABOUT_DIALOG(dlg), authors); + + gtk_about_dialog_set_comments(GTK_ABOUT_DIALOG(dlg), + "SuperNotepad 0.2\nLicensed under The Unlicense."); + + gtk_about_dialog_set_license(GTK_ABOUT_DIALOG(dlg), + "This is free and unencumbered software released into the public domain.\n" + "For more information, see The Unlicense."); + + gtk_about_dialog_set_website(GTK_ABOUT_DIALOG(dlg), "https://unlicense.org"); + gtk_about_dialog_set_website_label(GTK_ABOUT_DIALOG(dlg), "The Unlicense (https://unlicense.org)"); + gtk_dialog_run(GTK_DIALOG(dlg)); gtk_widget_destroy(dlg); } -/* ---------- Buffer signals ---------- */ +static void action_keyboard_shortcuts(GtkWidget *w, gpointer user_data) { + (void)w; + AppState *s = (AppState*)user_data; + + const char *msg = + "Keyboard Shortcuts\n" + "\n" + "File\n" + " Ctrl+N New\n" + " Ctrl+O Open\n" + " Ctrl+S Save\n" + " Ctrl+Shift+S Save As\n" + " Ctrl+P Print\n" + " Ctrl+Q Exit\n" + "\n" + "Edit\n" + " Ctrl+Z Undo\n" + " Ctrl+X Cut\n" + " Ctrl+C Copy\n" + " Ctrl+V Paste\n" + " Delete Delete selection\n" + " Ctrl+F Find\n" + " F3 Find Next\n" + " Ctrl+H Replace\n" + " Ctrl+G Go To\n" + " Ctrl+A Select All\n" + " F5 Time/Date\n" + "\n" + "View\n" + " Ctrl+Shift+W Toggle Word Wrap\n" + " Ctrl+Shift+L Toggle Line Numbering\n" + " Ctrl+Alt+B Toggle Status Bar\n" + "\n" + "Help\n" + " F1 About\n"; + + GtkWidget *dlg = gtk_message_dialog_new(GTK_WINDOW(s->win), + GTK_DIALOG_MODAL, + GTK_MESSAGE_INFO, + GTK_BUTTONS_OK, + "%s", msg); + + gtk_window_set_title(GTK_WINDOW(dlg), "Keyboard Shortcuts"); + gtk_dialog_run(GTK_DIALOG(dlg)); + gtk_widget_destroy(dlg); +} + +/* -------------------- Buffer signals -------------------- */ static void on_mark_set(GtkTextBuffer *buf, GtkTextIter *new_loc, GtkTextMark *mark, gpointer user_data) { (void)buf; (void)new_loc; (void)mark; @@ -904,182 +999,272 @@ static void on_buffer_changed(GtkTextBuffer *buf, gpointer user_data) { schedule_backup(s); } -/* ---------- Menu building helpers ---------- */ +/* -------------------- Menu Helpers + Shortcuts -------------------- */ -static GtkWidget* menu_item(const gchar *label, GCallback cb, AppState *s) { +static GtkWidget* menu_item(AppState *s, const gchar *label, GCallback cb) { GtkWidget *mi = gtk_menu_item_new_with_label(label); if (cb) g_signal_connect(mi, "activate", cb, s); return mi; } -static GtkWidget* check_menu_item(const gchar *label, gboolean active, GCallback toggled_cb, AppState *s) { - GtkWidget *mi = gtk_check_menu_item_new_with_label(label); - gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(mi), active); - if (toggled_cb) g_signal_connect(mi, "toggled", toggled_cb, s); - return mi; -} - -static GtkWidget* build_menubar(AppState *s) { - GtkWidget *menubar = gtk_menu_bar_new(); - - /* File */ - GtkWidget *file_menu = gtk_menu_new(); - GtkWidget *file_root = gtk_menu_item_new_with_label("File"); - gtk_menu_item_set_submenu(GTK_MENU_ITEM(file_root), file_menu); - - gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), menu_item("New", G_CALLBACK(action_new), s)); - gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), menu_item("Open", G_CALLBACK(action_open), s)); - gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), menu_item("Save", G_CALLBACK(action_save), s)); - gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), menu_item("Save As", G_CALLBACK(action_save_as), s)); - gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), menu_item("Print", G_CALLBACK(action_print), s)); - gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), gtk_separator_menu_item_new()); - gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), menu_item("Exit", G_CALLBACK(action_exit), s)); - - gtk_menu_shell_append(GTK_MENU_SHELL(menubar), file_root); - - /* Edit */ - GtkWidget *edit_menu = gtk_menu_new(); - GtkWidget *edit_root = gtk_menu_item_new_with_label("Edit"); - gtk_menu_item_set_submenu(GTK_MENU_ITEM(edit_root), edit_menu); - - gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), menu_item("Undo", G_CALLBACK(action_undo), s)); - gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), gtk_separator_menu_item_new()); - - gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), menu_item("Cut", G_CALLBACK(action_cut), s)); - gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), menu_item("Copy", G_CALLBACK(action_copy), s)); - gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), menu_item("Paste", G_CALLBACK(action_paste), s)); - gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), menu_item("Delete", G_CALLBACK(action_delete), s)); - - gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), gtk_separator_menu_item_new()); - - gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), menu_item("Find", G_CALLBACK(action_find), s)); - gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), menu_item("Find Next", G_CALLBACK(action_find_next), s)); - gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), menu_item("Replace", G_CALLBACK(action_replace), s)); - gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), menu_item("Go To", G_CALLBACK(action_go_to), s)); - - gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), gtk_separator_menu_item_new()); - - gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), menu_item("Select All", G_CALLBACK(action_select_all), s)); - gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), menu_item("Time/Date", G_CALLBACK(action_time_date), s)); - - gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), gtk_separator_menu_item_new()); - - // Backup toggle (you requested: option in Edit menu to turn off backup) - gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), - check_menu_item("Create .backup when unsaved", TRUE, G_CALLBACK(toggle_backups), s)); - - gtk_menu_shell_append(GTK_MENU_SHELL(menubar), edit_root); - - /* View */ - GtkWidget *view_menu = gtk_menu_new(); - GtkWidget *view_root = gtk_menu_item_new_with_label("View"); - gtk_menu_item_set_submenu(GTK_MENU_ITEM(view_root), view_menu); - - gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), - check_menu_item("Status Bar", TRUE, G_CALLBACK(toggle_status_bar), s)); - gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), - check_menu_item("Word Wrap", FALSE, G_CALLBACK(toggle_word_wrap), s)); - gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), - check_menu_item("Line Numbering", TRUE, G_CALLBACK(toggle_line_numbers), s)); - - gtk_menu_shell_append(GTK_MENU_SHELL(menubar), view_root); - - /* Help */ - GtkWidget *help_menu = gtk_menu_new(); - GtkWidget *help_root = gtk_menu_item_new_with_label("Help"); - gtk_menu_item_set_submenu(GTK_MENU_ITEM(help_root), help_menu); - - gtk_menu_shell_append(GTK_MENU_SHELL(help_menu), menu_item("About", G_CALLBACK(action_about), s)); - - gtk_menu_shell_append(GTK_MENU_SHELL(menubar), help_root); - - return menubar; -} - -/* ---------- App init ---------- */ - -static void on_window_destroy(GtkWidget *w, gpointer user_data) { - (void)w; - AppState *s = (AppState*)user_data; - - if (s->backup_timeout_id) { - g_source_remove(s->backup_timeout_id); - s->backup_timeout_id = 0; +static GtkWidget* menu_item_accel(AppState *s, const gchar *label, GCallback cb, + guint keyval, GdkModifierType mods) { + GtkWidget *mi = menu_item(s, label, cb); + if (s->accel_group && cb && keyval != 0) { + gtk_widget_add_accelerator(mi, "activate", s->accel_group, + keyval, mods, GTK_ACCEL_VISIBLE); } + return mi; + } - free_and_null(&s->current_path); - free_and_null(&s->backup_path); - free_and_null(&s->last_find); + static GtkWidget* check_menu_item(AppState *s, const gchar *label, gboolean active, + GCallback toggled_cb, + guint keyval, GdkModifierType mods) { + GtkWidget *mi = gtk_check_menu_item_new_with_label(label); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(mi), active); + if (toggled_cb) g_signal_connect(mi, "toggled", toggled_cb, s); - g_free(s); -} + // For check items, accelerator triggers "activate" which toggles it. + if (s->accel_group && keyval != 0) { + gtk_widget_add_accelerator(mi, "activate", s->accel_group, + keyval, mods, GTK_ACCEL_VISIBLE); + } + return mi; + } -static void app_activate(GtkApplication *app, gpointer user_data) { - (void)user_data; + /* Builds the menubar (includes Open Recent) */ + static GtkWidget* build_menubar(AppState *s) { + GtkWidget *menubar = gtk_menu_bar_new(); - AppState *s = g_new0(AppState, 1); - s->app = app; + /* File */ + GtkWidget *file_menu = gtk_menu_new(); + GtkWidget *file_root = gtk_menu_item_new_with_label("File"); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(file_root), file_menu); - s->show_status_bar = TRUE; - s->word_wrap = FALSE; - s->show_line_numbers = TRUE; - s->backups_enabled = TRUE; + gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), + menu_item_accel(s, "New", G_CALLBACK(action_new), GDK_KEY_n, GDK_CONTROL_MASK)); + gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), + menu_item_accel(s, "Open", G_CALLBACK(action_open), GDK_KEY_o, GDK_CONTROL_MASK)); - s->win = gtk_application_window_new(app); - gtk_window_set_default_size(GTK_WINDOW(s->win), 900, 600); + // Open Recent (GtkRecentChooserMenu) + GtkWidget *recent_menu = gtk_recent_chooser_menu_new_for_manager(s->recent_manager); + gtk_recent_chooser_set_limit(GTK_RECENT_CHOOSER(recent_menu), 15); + gtk_recent_chooser_set_show_not_found(GTK_RECENT_CHOOSER(recent_menu), FALSE); + gtk_recent_chooser_set_show_private(GTK_RECENT_CHOOSER(recent_menu), FALSE); + gtk_recent_chooser_set_local_only(GTK_RECENT_CHOOSER(recent_menu), TRUE); + g_signal_connect(recent_menu, "item-activated", G_CALLBACK(on_recent_item_activated), s); - // Main layout - GtkWidget *root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); - gtk_container_add(GTK_CONTAINER(s->win), root); + GtkWidget *open_recent = gtk_menu_item_new_with_label("Open Recent"); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(open_recent), recent_menu); + gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), open_recent); - // Menubar - s->menubar = build_menubar(s); - gtk_box_pack_start(GTK_BOX(root), s->menubar, FALSE, FALSE, 0); + gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), + menu_item_accel(s, "Save", G_CALLBACK(action_save), GDK_KEY_s, GDK_CONTROL_MASK)); + gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), + menu_item_accel(s, "Save As", G_CALLBACK(action_save_as), GDK_KEY_s, (GdkModifierType)(GDK_CONTROL_MASK | GDK_MOD1_MASK))); + gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), + menu_item_accel(s, "Print", G_CALLBACK(action_print), GDK_KEY_p, GDK_CONTROL_MASK)); - // Text editor - s->buffer = GTK_SOURCE_BUFFER(gtk_source_buffer_new(NULL)); - s->view = GTK_SOURCE_VIEW(gtk_source_view_new_with_buffer(s->buffer)); - gtk_source_view_set_show_line_numbers(s->view, TRUE); + gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), gtk_separator_menu_item_new()); - s->scroller = gtk_scrolled_window_new(NULL, NULL); - gtk_container_add(GTK_CONTAINER(s->scroller), GTK_WIDGET(s->view)); - gtk_box_pack_start(GTK_BOX(root), s->scroller, TRUE, TRUE, 0); + gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), + menu_item_accel(s, "Exit", G_CALLBACK(action_exit), GDK_KEY_q, GDK_CONTROL_MASK)); - // Status bar box - s->status_bar_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); - gtk_widget_set_margin_start(s->status_bar_box, 8); - gtk_widget_set_margin_end(s->status_bar_box, 8); - gtk_widget_set_margin_top(s->status_bar_box, 4); - gtk_widget_set_margin_bottom(s->status_bar_box, 4); + gtk_menu_shell_append(GTK_MENU_SHELL(menubar), file_root); - s->pos_label = gtk_label_new("Ln 1, Col 1"); - s->state_label = gtk_label_new("Saved"); + /* Edit */ + GtkWidget *edit_menu = gtk_menu_new(); + GtkWidget *edit_root = gtk_menu_item_new_with_label("Edit"); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(edit_root), edit_menu); - gtk_box_pack_start(GTK_BOX(s->status_bar_box), s->pos_label, FALSE, FALSE, 0); - gtk_box_pack_end(GTK_BOX(s->status_bar_box), s->state_label, FALSE, FALSE, 0); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), + menu_item_accel(s, "Undo", G_CALLBACK(action_undo), GDK_KEY_z, GDK_CONTROL_MASK)); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), gtk_separator_menu_item_new()); - gtk_box_pack_end(GTK_BOX(root), s->status_bar_box, FALSE, FALSE, 0); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), + menu_item_accel(s, "Cut", G_CALLBACK(action_cut), GDK_KEY_x, GDK_CONTROL_MASK)); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), + menu_item_accel(s, "Copy", G_CALLBACK(action_copy), GDK_KEY_c, GDK_CONTROL_MASK)); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), + menu_item_accel(s, "Paste", G_CALLBACK(action_paste), GDK_KEY_v, GDK_CONTROL_MASK)); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), + menu_item_accel(s, "Delete", G_CALLBACK(action_delete), GDK_KEY_Delete, (GdkModifierType)0)); - apply_view_settings(s); - update_title(s); - update_status(s); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), gtk_separator_menu_item_new()); - // Buffer signals - g_signal_connect(s->buffer, "mark-set", G_CALLBACK(on_mark_set), s); - g_signal_connect(s->buffer, "modified-changed", G_CALLBACK(on_modified_changed), s); - g_signal_connect(s->buffer, "changed", G_CALLBACK(on_buffer_changed), s); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), + menu_item_accel(s, "Find", G_CALLBACK(action_find), GDK_KEY_f, GDK_CONTROL_MASK)); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), + menu_item_accel(s, "Find Next", G_CALLBACK(action_find_next), GDK_KEY_F3, (GdkModifierType)0)); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), + menu_item_accel(s, "Replace", G_CALLBACK(action_replace), GDK_KEY_h, GDK_CONTROL_MASK)); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), + menu_item_accel(s, "Go To", G_CALLBACK(action_go_to), GDK_KEY_g, GDK_CONTROL_MASK)); - // Cleanup - g_signal_connect(s->win, "destroy", G_CALLBACK(on_window_destroy), s); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), gtk_separator_menu_item_new()); - gtk_widget_show_all(s->win); -} + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), + menu_item_accel(s, "Select All", G_CALLBACK(action_select_all), GDK_KEY_a, GDK_CONTROL_MASK)); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), + menu_item_accel(s, "Time/Date", G_CALLBACK(action_time_date), GDK_KEY_F5, (GdkModifierType)0)); -int main(int argc, char **argv) { - GtkApplication *app = gtk_application_new("com.example.SuperNotepad", G_APPLICATION_DEFAULT_FLAGS); - g_signal_connect(app, "activate", G_CALLBACK(app_activate), NULL); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), gtk_separator_menu_item_new()); - int status = g_application_run(G_APPLICATION(app), argc, argv); - g_object_unref(app); - return status; -} + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), + check_menu_item(s, "Create .backup when unsaved", s->backups_enabled, G_CALLBACK(toggle_backups), + GDK_KEY_b, (GdkModifierType)(GDK_CONTROL_MASK | GDK_SHIFT_MASK))); + + gtk_menu_shell_append(GTK_MENU_SHELL(menubar), edit_root); + + /* View */ + GtkWidget *view_menu = gtk_menu_new(); + GtkWidget *view_root = gtk_menu_item_new_with_label("View"); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(view_root), view_menu); + + gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), + check_menu_item(s, "Status Bar", s->show_status_bar, G_CALLBACK(toggle_status_bar), + GDK_KEY_b, (GdkModifierType)(GDK_CONTROL_MASK | GDK_MOD1_MASK))); + + gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), + check_menu_item(s, "Word Wrap", s->word_wrap, G_CALLBACK(toggle_word_wrap), + GDK_KEY_w, (GdkModifierType)(GDK_CONTROL_MASK | GDK_SHIFT_MASK))); + + gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), + check_menu_item(s, "Line Numbering", s->show_line_numbers, G_CALLBACK(toggle_line_numbers), + GDK_KEY_l, (GdkModifierType)(GDK_CONTROL_MASK | GDK_SHIFT_MASK))); + + gtk_menu_shell_append(GTK_MENU_SHELL(menubar), view_root); + + /* Help */ + GtkWidget *help_menu = gtk_menu_new(); + GtkWidget *help_root = gtk_menu_item_new_with_label("Help"); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(help_root), help_menu); + + gtk_menu_shell_append(GTK_MENU_SHELL(help_menu), + menu_item(s, "Keyboard Shortcuts", G_CALLBACK(action_keyboard_shortcuts))); + + gtk_menu_shell_append(GTK_MENU_SHELL(help_menu), gtk_separator_menu_item_new()); + + gtk_menu_shell_append(GTK_MENU_SHELL(help_menu), + menu_item_accel(s, "About", G_CALLBACK(action_about), GDK_KEY_F1, (GdkModifierType)0)); + + + gtk_menu_shell_append(GTK_MENU_SHELL(menubar), help_root); + + return menubar; + } + + /* -------------------- Cleanup -------------------- */ + + static void on_window_destroy(GtkWidget *w, gpointer user_data) { + (void)w; + AppState *s = (AppState*)user_data; + + prefs_save(s); + + if (s->backup_timeout_id) { + g_source_remove(s->backup_timeout_id); + s->backup_timeout_id = 0; + } + + free_and_null(&s->current_path); + free_and_null(&s->backup_path); + free_and_null(&s->last_find); + + g_free(s); + } + + /* -------------------- App Init -------------------- */ + + static void app_activate(GtkApplication *app, gpointer user_data) { + (void)user_data; + + AppState *s = g_new0(AppState, 1); + s->app = app; + + // Defaults + s->show_status_bar = TRUE; + s->word_wrap = FALSE; + s->show_line_numbers = TRUE; + s->backups_enabled = TRUE; + + // Recent manager + accel group + s->recent_manager = gtk_recent_manager_get_default(); + s->accel_group = gtk_accel_group_new(); + + // Load saved prefs (overrides defaults if present) + prefs_load(s); + + s->win = gtk_application_window_new(app); + gtk_window_set_default_size(GTK_WINDOW(s->win), 900, 600); + + // Attach accelerators to window + gtk_window_add_accel_group(GTK_WINDOW(s->win), s->accel_group); + + // Close prompt + g_signal_connect(s->win, "delete-event", G_CALLBACK(on_window_delete_event), s); + + // Dark mode detection + GtkSettings *settings = gtk_settings_get_default(); + if (settings) { + g_signal_connect(settings, "notify::gtk-theme-name", G_CALLBACK(on_settings_notify), s); + g_signal_connect(settings, "notify::gtk-application-prefer-dark-theme", G_CALLBACK(on_settings_notify), s); + } + + // Main layout + GtkWidget *root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_container_add(GTK_CONTAINER(s->win), root); + + // Menubar + s->menubar = build_menubar(s); + gtk_box_pack_start(GTK_BOX(root), s->menubar, FALSE, FALSE, 0); + + // Editor + s->buffer = GTK_SOURCE_BUFFER(gtk_source_buffer_new(NULL)); + s->view = GTK_SOURCE_VIEW(gtk_source_view_new_with_buffer(s->buffer)); + + s->scroller = gtk_scrolled_window_new(NULL, NULL); + gtk_container_add(GTK_CONTAINER(s->scroller), GTK_WIDGET(s->view)); + gtk_box_pack_start(GTK_BOX(root), s->scroller, TRUE, TRUE, 0); + + // Status bar area + s->status_bar_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_widget_set_margin_start(s->status_bar_box, 8); + gtk_widget_set_margin_end(s->status_bar_box, 8); + gtk_widget_set_margin_top(s->status_bar_box, 4); + gtk_widget_set_margin_bottom(s->status_bar_box, 4); + + s->pos_label = gtk_label_new("Ln 1, Col 1"); + s->state_label = gtk_label_new("Saved"); + + gtk_box_pack_start(GTK_BOX(s->status_bar_box), s->pos_label, FALSE, FALSE, 0); + gtk_box_pack_end(GTK_BOX(s->status_bar_box), s->state_label, FALSE, FALSE, 0); + + gtk_box_pack_end(GTK_BOX(root), s->status_bar_box, FALSE, FALSE, 0); + + apply_view_settings(s); + update_title(s); + update_status(s); + + // Buffer signals + g_signal_connect(s->buffer, "mark-set", G_CALLBACK(on_mark_set), s); + g_signal_connect(s->buffer, "modified-changed", G_CALLBACK(on_modified_changed), s); + g_signal_connect(s->buffer, "changed", G_CALLBACK(on_buffer_changed), s); + + // Cleanup + g_signal_connect(s->win, "destroy", G_CALLBACK(on_window_destroy), s); + + gtk_widget_show_all(s->win); + + // Run initial dark mode detection once window exists + update_dark_mode(s); + } + + int main(int argc, char **argv) { + GtkApplication *app = gtk_application_new("com.example.SuperNotepad", G_APPLICATION_DEFAULT_FLAGS); + g_signal_connect(app, "activate", G_CALLBACK(app_activate), NULL); + + int status = g_application_run(G_APPLICATION(app), argc, argv); + g_object_unref(app); + return status; + }