Files
SuperNotepad/main.c
John Paul Wohlscheid b6406a205a first commit
2026-02-16 14:53:28 -05:00

1086 lines
34 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
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 <gtk/gtk.h>
#include <gtksourceview/gtksource.h>
#include <time.h>
#include <glib/gstdio.h>
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 (its 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(&lt, &t);
#else
localtime_r(&t, &lt);
#endif
char buf[128];
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", &lt);
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;
}