1086 lines
34 KiB
C
1086 lines
34 KiB
C
/*
|
||
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 (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;
|
||
}
|