Files
SuperNotepad/main.c

1086 lines
34 KiB
C
Raw Normal View History

2026-02-16 14:53:28 -05:00
/*
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;
}