/* * 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 /* -------------------- App State -------------------- */ typedef struct { GtkApplication *app; GtkWidget *win; GtkWidget *menubar; GtkWidget *scroller; GtkSourceView *view; GtkSourceBuffer *buffer; GtkWidget *status_bar_box; GtkWidget *pos_label; GtkWidget *state_label; gboolean show_status_bar; gboolean word_wrap; gboolean show_line_numbers; gboolean backups_enabled; 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; GtkTextIter last_match_end; gboolean has_last_match; } AppState; /* -------------------- Utility -------------------- */ static void free_and_null(gchar **p) { if (*p) { g_free(*p); *p = NULL; } } static gchar* buffer_get_all_text(GtkTextBuffer *buf) { GtkTextIter start, end; gtk_text_buffer_get_start_iter(buf, &start); gtk_text_buffer_get_end_iter(buf, &end); return gtk_text_buffer_get_text(buf, &start, &end, TRUE); } static void update_title(AppState *s) { const gchar *appname = "SuperNotepad"; gchar *title = NULL; if (s->current_path && *s->current_path) { gchar *base = g_path_get_basename(s->current_path); title = g_strdup_printf("%s - %s", base, appname); g_free(base); } else { title = g_strdup_printf("Untitled - %s", appname); } gtk_window_set_title(GTK_WINDOW(s->win), title); g_free(title); } static void update_status(AppState *s) { GtkTextIter iter; GtkTextMark *mark = gtk_text_buffer_get_insert(GTK_TEXT_BUFFER(s->buffer)); gtk_text_buffer_get_iter_at_mark(GTK_TEXT_BUFFER(s->buffer), &iter, mark); int line = gtk_text_iter_get_line(&iter) + 1; int col = gtk_text_iter_get_line_offset(&iter) + 1; gchar *pos = g_strdup_printf("Ln %d, Col %d", line, col); gtk_label_set_text(GTK_LABEL(s->pos_label), pos); g_free(pos); gboolean modified = gtk_text_buffer_get_modified(GTK_TEXT_BUFFER(s->buffer)); gtk_label_set_text(GTK_LABEL(s->state_label), modified ? "Modified (unsaved)" : "Saved"); } /* -------------------- 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)); (void)g_file_set_contents(s->backup_path, text, -1, NULL); g_free(text); return G_SOURCE_REMOVE; } static void schedule_backup(AppState *s) { if (!s->backups_enabled) return; if (!gtk_text_buffer_get_modified(GTK_TEXT_BUFFER(s->buffer))) 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); } /* -------------------- Recent Files Helpers -------------------- */ 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 save_to_path(AppState *s, const gchar *path) { gchar *text = buffer_get_all_text(GTK_TEXT_BUFFER(s->buffer)); GError *err = NULL; gboolean ok = g_file_set_contents(path, text, -1, &err); g_free(text); 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_run(GTK_DIALOG(d)); gtk_widget_destroy(d); g_clear_error(&err); return FALSE; } free_and_null(&s->current_path); s->current_path = g_strdup(path); gtk_text_buffer_set_modified(GTK_TEXT_BUFFER(s->buffer), FALSE); update_title(s); update_status(s); 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 (!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; update_title(s); update_status(s); delete_backup_file_if_any(s); } static void action_open(GtkWidget *w, gpointer user_data) { (void)w; AppState *s = (AppState*)user_data; 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); 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; (void)open_from_path(s, filename); g_free(filename); } static void action_save(GtkWidget *w, gpointer user_data) { (void)w; AppState *s = (AppState*)user_data; if (!s->current_path) { (void)do_save_as(s); return; } (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; (void)do_save_as(s); } static void action_exit(GtkWidget *w, gpointer user_data) { (void)w; AppState *s = (AppState*)user_data; gtk_window_close(GTK_WINDOW(s->win)); } /* Recent chooser callback */ static void on_recent_item_activated(GtkRecentChooser *chooser, gpointer user_data) { AppState *s = (AppState*)user_data; if (!ensure_saved_or_cancel(s)) return; 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) { 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; PrintState *ps = (PrintState*)user_data; gtk_source_print_compositor_draw_page(ps->comp, ctx, page_nr); } 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; 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"); 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 r = gtk_print_operation_run(op, GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG, GTK_WINDOW(s->win), &err); 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_run(GTK_DIALOG(d)); gtk_widget_destroy(d); g_clear_error(&err); } g_object_unref(op); } /* -------------------- Actions: Edit Menu -------------------- */ static void action_undo(GtkWidget *w, gpointer user_data) { (void)w; AppState *s = (AppState*)user_data; if (gtk_source_buffer_can_undo(s->buffer)) gtk_source_buffer_undo(s->buffer); } static void action_cut(GtkWidget *w, gpointer user_data) { (void)w; AppState *s = (AppState*)user_data; GtkClipboard *cb = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); gtk_text_buffer_cut_clipboard(GTK_TEXT_BUFFER(s->buffer), cb, TRUE); } static void action_copy(GtkWidget *w, gpointer user_data) { (void)w; AppState *s = (AppState*)user_data; GtkClipboard *cb = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); gtk_text_buffer_copy_clipboard(GTK_TEXT_BUFFER(s->buffer), cb); } static void action_paste(GtkWidget *w, gpointer user_data) { (void)w; AppState *s = (AppState*)user_data; GtkClipboard *cb = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); gtk_text_buffer_paste_clipboard(GTK_TEXT_BUFFER(s->buffer), cb, NULL, TRUE); } static void action_delete(GtkWidget *w, gpointer user_data) { (void)w; AppState *s = (AppState*)user_data; GtkTextIter start, end; if (gtk_text_buffer_get_selection_bounds(GTK_TEXT_BUFFER(s->buffer), &start, &end)) { gtk_text_buffer_delete(GTK_TEXT_BUFFER(s->buffer), &start, &end); } } static void action_select_all(GtkWidget *w, gpointer user_data) { (void)w; AppState *s = (AppState*)user_data; GtkTextIter start, end; gtk_text_buffer_get_start_iter(GTK_TEXT_BUFFER(s->buffer), &start); gtk_text_buffer_get_end_iter(GTK_TEXT_BUFFER(s->buffer), &end); gtk_text_buffer_select_range(GTK_TEXT_BUFFER(s->buffer), &start, &end); } static void action_time_date(GtkWidget *w, gpointer user_data) { (void)w; AppState *s = (AppState*)user_data; time_t t = time(NULL); struct tm lt; #if defined(_WIN32) localtime_s(<, &t); #else localtime_r(&t, <); #endif char buf[128]; strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", <); GtkTextIter iter; GtkTextMark *mark = gtk_text_buffer_get_insert(GTK_TEXT_BUFFER(s->buffer)); gtk_text_buffer_get_iter_at_mark(GTK_TEXT_BUFFER(s->buffer), &iter, mark); gtk_text_buffer_insert(GTK_TEXT_BUFFER(s->buffer), &iter, buf, -1); } /* -------------------- Find / Replace / Go To -------------------- */ static gboolean do_find_from_iter(AppState *s, const gchar *needle, GtkTextIter *from, gboolean wrap) { if (!needle || !*needle) return FALSE; GtkTextIter start = *from; 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); 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); } if (found) { gtk_text_buffer_select_range(GTK_TEXT_BUFFER(s->buffer), &match_start, &match_end); gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(s->view), &match_start, 0.2, FALSE, 0, 0); s->last_match_end = match_end; s->has_last_match = TRUE; return TRUE; } return FALSE; } static void action_find(GtkWidget *w, gpointer user_data) { (void)w; 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); 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); if (s->last_find) gtk_entry_set_text(GTK_ENTRY(entry), s->last_find); gtk_dialog_set_default_response(GTK_DIALOG(dlg), GTK_RESPONSE_ACCEPT); gtk_widget_show_all(dlg); int resp = gtk_dialog_run(GTK_DIALOG(dlg)); if (resp != GTK_RESPONSE_ACCEPT) { gtk_widget_destroy(dlg); return; } const gchar *needle = gtk_entry_get_text(GTK_ENTRY(entry)); free_and_null(&s->last_find); s->last_find = g_strdup(needle); GtkTextIter from; 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); gboolean ok = do_find_from_iter(s, needle, &from, TRUE); gtk_widget_destroy(dlg); 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_run(GTK_DIALOG(d)); gtk_widget_destroy(d); } } 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 { 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); } 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_run(GTK_DIALOG(d)); gtk_widget_destroy(d); } } static void action_go_to(GtkWidget *w, gpointer user_data) { (void)w; 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); GtkWidget *box = gtk_dialog_get_content_area(GTK_DIALOG(dlg)); GtkWidget *spin = gtk_spin_button_new_with_range(1, 1000000, 1); gtk_box_pack_start(GTK_BOX(box), gtk_label_new("Line number:"), FALSE, FALSE, 6); gtk_box_pack_start(GTK_BOX(box), spin, FALSE, FALSE, 6); gtk_dialog_set_default_response(GTK_DIALOG(dlg), GTK_RESPONSE_ACCEPT); gtk_widget_show_all(dlg); int resp = gtk_dialog_run(GTK_DIALOG(dlg)); if (resp != GTK_RESPONSE_ACCEPT) { gtk_widget_destroy(dlg); return; } int line = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(spin)); gtk_widget_destroy(dlg); GtkTextIter iter; 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); } static void action_replace(GtkWidget *w, gpointer user_data) { (void)w; 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); GtkWidget *box = gtk_dialog_get_content_area(GTK_DIALOG(dlg)); GtkWidget *find_entry = gtk_entry_new(); GtkWidget *rep_entry = gtk_entry_new(); gtk_box_pack_start(GTK_BOX(box), gtk_label_new("Find what:"), FALSE, FALSE, 6); gtk_box_pack_start(GTK_BOX(box), find_entry, FALSE, FALSE, 6); gtk_box_pack_start(GTK_BOX(box), gtk_label_new("Replace with:"), FALSE, FALSE, 6); gtk_box_pack_start(GTK_BOX(box), rep_entry, FALSE, FALSE, 6); if (s->last_find) gtk_entry_set_text(GTK_ENTRY(find_entry), s->last_find); gtk_widget_show_all(dlg); for (;;) { int resp = gtk_dialog_run(GTK_DIALOG(dlg)); if (resp == GTK_RESPONSE_CLOSE) break; const gchar *needle = gtk_entry_get_text(GTK_ENTRY(find_entry)); const gchar *repl = gtk_entry_get_text(GTK_ENTRY(rep_entry)); 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_run(GTK_DIALOG(d)); gtk_widget_destroy(d); continue; } free_and_null(&s->last_find); s->last_find = g_strdup(needle); if (resp == 1) { 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); gboolean matches = (g_strcmp0(selected, needle) == 0); g_free(selected); if (matches) { gtk_text_buffer_begin_user_action(GTK_TEXT_BUFFER(s->buffer)); 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)); GtkTextIter from = sel_start; (void)do_find_from_iter(s, needle, &from, TRUE); continue; } } action_find_next(NULL, s); continue; } if (resp == 2) { GtkTextIter iter; gtk_text_buffer_get_start_iter(GTK_TEXT_BUFFER(s->buffer), &iter); int count = 0; gtk_text_buffer_begin_user_action(GTK_TEXT_BUFFER(s->buffer)); 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); 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; 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_run(GTK_DIALOG(d)); gtk_widget_destroy(d); continue; } } gtk_widget_destroy(dlg); } /* -------------------- 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); } 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) { if (s->backup_timeout_id) { g_source_remove(s->backup_timeout_id); s->backup_timeout_id = 0; } delete_backup_file_if_any(s); } else { schedule_backup(s); } prefs_save(s); } /* -------------------- About -------------------- */ static void action_about(GtkWidget *w, gpointer user_data) { (void)w; AppState *s = (AppState*)user_data; 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); } 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; AppState *s = (AppState*)user_data; update_status(s); } static void on_modified_changed(GtkTextBuffer *buf, gpointer user_data) { (void)buf; AppState *s = (AppState*)user_data; update_status(s); } static void on_buffer_changed(GtkTextBuffer *buf, gpointer user_data) { (void)buf; AppState *s = (AppState*)user_data; update_status(s); schedule_backup(s); } /* -------------------- Menu Helpers + Shortcuts -------------------- */ 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* 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; } 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); // 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; } /* Builds the menubar (includes Open Recent) */ 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_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)); // 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); 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); 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)); 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_accel(s, "Exit", G_CALLBACK(action_exit), GDK_KEY_q, GDK_CONTROL_MASK)); 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_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_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)); 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_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)); 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_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)); gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), gtk_separator_menu_item_new()); 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; }