/* 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 */ #include #include #include #include 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 or temp guint backup_timeout_id; // debounce // 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* 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); 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 *name = "SuperNotepad"; gchar *title = NULL; if (s->current_path) { gchar *base = g_path_get_basename(s->current_path); title = g_strdup_printf("%s - %s", base, name); g_free(base); } else { title = g_strdup_printf("Untitled - %s", name); } 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"); } /* ---------- Backup handling ---------- */ 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); } 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; // Debounce: write 1 second after last change if (s->backup_timeout_id != 0) return; 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); } if (s->backup_path) { GError *err = NULL; if (g_remove(s->backup_path) != 0) { // ignore failures (file may not exist) } if (err) g_clear_error(&err); } } /* ---------- 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; } 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); // Once saved, remove backup (it’s no longer “unsaved”) delete_backup_file_if_any(s); return TRUE; } 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 } 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->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 (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; } } } } 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; 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); 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); return; } 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); } 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 ---------- */ typedef struct { gchar *text; } PrintData; static void print_data_free(PrintData *pd) { if (!pd) return; g_free(pd->text); g_free(pd); } 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 } 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; 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 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)); 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); GError *err = NULL; GtkPrintOperationResult res = gtk_print_operation_run(op, GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG, GTK_WINDOW(s->win), &err); if (res == 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); print_data_free(pd); } /* ---------- Edit actions ---------- */ 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) { // 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); 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)); // After replace, find next occurrence GtkTextIter from = sel_start; do_find_from_iter(s, needle, &from, TRUE); continue; } } action_find_next(NULL, s); continue; } if (resp == 2) { // Replace All 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; // continue after inserted text 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); } /* ---------- 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); } 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); } 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); } 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); } /* ---------- 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); } } /* ---------- Help / 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"); 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 building helpers ---------- */ static GtkWidget* menu_item(const gchar *label, GCallback cb, AppState *s) { 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; } free_and_null(&s->current_path); free_and_null(&s->backup_path); free_and_null(&s->last_find); g_free(s); } static void app_activate(GtkApplication *app, gpointer user_data) { (void)user_data; AppState *s = g_new0(AppState, 1); s->app = app; s->show_status_bar = TRUE; s->word_wrap = FALSE; s->show_line_numbers = TRUE; s->backups_enabled = TRUE; s->win = gtk_application_window_new(app); gtk_window_set_default_size(GTK_WINDOW(s->win), 900, 600); // 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); // 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); 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 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); 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); } 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; }