1271 lines
55 KiB
C
1271 lines
55 KiB
C
/*
|
|
* SuperNotepad (GTK3 + GtkSourceView4) - C
|
|
*
|
|
* Added:
|
|
* - Recent Files (GtkRecentManager + GtkRecentChooserMenu)
|
|
* - Dark mode detection (heuristic + notify on theme/prefer-dark changes)
|
|
* - Keyboard shortcuts (GtkAccelGroup)
|
|
*
|
|
* Build:
|
|
* meson setup build
|
|
* meson compile -C build
|
|
* ./build/SuperNotepad
|
|
*/
|
|
|
|
#include <gtk/gtk.h>
|
|
#include <gtksourceview/gtksource.h>
|
|
#include <glib/gstdio.h>
|
|
#include <time.h>
|
|
|
|
/* -------------------- App State -------------------- */
|
|
|
|
typedef struct {
|
|
GtkApplication *app;
|
|
GtkWidget *win;
|
|
|
|
GtkWidget *menubar;
|
|
|
|
GtkWidget *scroller;
|
|
GtkSourceView *view;
|
|
GtkSourceBuffer *buffer;
|
|
|
|
GtkWidget *status_bar_box;
|
|
GtkWidget *pos_label;
|
|
GtkWidget *state_label;
|
|
|
|
gboolean show_status_bar;
|
|
gboolean word_wrap;
|
|
gboolean show_line_numbers;
|
|
gboolean backups_enabled;
|
|
|
|
gchar *current_path; // NULL when untitled
|
|
gchar *backup_path; // derived
|
|
guint backup_timeout_id; // debounce timer id
|
|
|
|
// Recent files / shortcuts
|
|
GtkRecentManager *recent_manager;
|
|
GtkAccelGroup *accel_group;
|
|
|
|
// Dark mode detection
|
|
gboolean is_dark_mode;
|
|
|
|
// Find state
|
|
gchar *last_find;
|
|
GtkTextIter last_match_end;
|
|
gboolean has_last_match;
|
|
} AppState;
|
|
|
|
/* -------------------- Utility -------------------- */
|
|
|
|
static void free_and_null(gchar **p) {
|
|
if (*p) { g_free(*p); *p = NULL; }
|
|
}
|
|
|
|
static gchar* buffer_get_all_text(GtkTextBuffer *buf) {
|
|
GtkTextIter start, end;
|
|
gtk_text_buffer_get_start_iter(buf, &start);
|
|
gtk_text_buffer_get_end_iter(buf, &end);
|
|
return gtk_text_buffer_get_text(buf, &start, &end, TRUE);
|
|
}
|
|
|
|
static void update_title(AppState *s) {
|
|
const gchar *appname = "SuperNotepad";
|
|
gchar *title = NULL;
|
|
|
|
if (s->current_path && *s->current_path) {
|
|
gchar *base = g_path_get_basename(s->current_path);
|
|
title = g_strdup_printf("%s - %s", base, appname);
|
|
g_free(base);
|
|
} else {
|
|
title = g_strdup_printf("Untitled - %s", appname);
|
|
}
|
|
|
|
gtk_window_set_title(GTK_WINDOW(s->win), title);
|
|
g_free(title);
|
|
}
|
|
|
|
static void update_status(AppState *s) {
|
|
GtkTextIter iter;
|
|
GtkTextMark *mark = gtk_text_buffer_get_insert(GTK_TEXT_BUFFER(s->buffer));
|
|
gtk_text_buffer_get_iter_at_mark(GTK_TEXT_BUFFER(s->buffer), &iter, mark);
|
|
|
|
int line = gtk_text_iter_get_line(&iter) + 1;
|
|
int col = gtk_text_iter_get_line_offset(&iter) + 1;
|
|
|
|
gchar *pos = g_strdup_printf("Ln %d, Col %d", line, col);
|
|
gtk_label_set_text(GTK_LABEL(s->pos_label), pos);
|
|
g_free(pos);
|
|
|
|
gboolean modified = gtk_text_buffer_get_modified(GTK_TEXT_BUFFER(s->buffer));
|
|
gtk_label_set_text(GTK_LABEL(s->state_label), modified ? "Modified (unsaved)" : "Saved");
|
|
}
|
|
|
|
/* -------------------- Preferences -------------------- */
|
|
|
|
static gchar* prefs_path(void) {
|
|
gchar *dir = g_build_filename(g_get_user_config_dir(), "SuperNotepad", NULL);
|
|
g_mkdir_with_parents(dir, 0700);
|
|
gchar *path = g_build_filename(dir, "config.ini", NULL);
|
|
g_free(dir);
|
|
return path;
|
|
}
|
|
|
|
static void prefs_save(AppState *s) {
|
|
GKeyFile *kf = g_key_file_new();
|
|
|
|
g_key_file_set_boolean(kf, "ui", "show_status_bar", s->show_status_bar);
|
|
g_key_file_set_boolean(kf, "ui", "word_wrap", s->word_wrap);
|
|
g_key_file_set_boolean(kf, "ui", "show_line_numbers", s->show_line_numbers);
|
|
g_key_file_set_boolean(kf, "edit", "backups_enabled", s->backups_enabled);
|
|
|
|
gsize len = 0;
|
|
gchar *data = g_key_file_to_data(kf, &len, NULL);
|
|
|
|
gchar *path = prefs_path();
|
|
(void)g_file_set_contents(path, data, (gssize)len, NULL);
|
|
|
|
g_free(path);
|
|
g_free(data);
|
|
g_key_file_unref(kf);
|
|
}
|
|
|
|
static void prefs_load(AppState *s) {
|
|
gchar *path = prefs_path();
|
|
GKeyFile *kf = g_key_file_new();
|
|
|
|
if (g_key_file_load_from_file(kf, path, G_KEY_FILE_NONE, NULL)) {
|
|
if (g_key_file_has_key(kf, "ui", "show_status_bar", NULL))
|
|
s->show_status_bar = g_key_file_get_boolean(kf, "ui", "show_status_bar", NULL);
|
|
if (g_key_file_has_key(kf, "ui", "word_wrap", NULL))
|
|
s->word_wrap = g_key_file_get_boolean(kf, "ui", "word_wrap", NULL);
|
|
if (g_key_file_has_key(kf, "ui", "show_line_numbers", NULL))
|
|
s->show_line_numbers = g_key_file_get_boolean(kf, "ui", "show_line_numbers", NULL);
|
|
if (g_key_file_has_key(kf, "edit", "backups_enabled", NULL))
|
|
s->backups_enabled = g_key_file_get_boolean(kf, "edit", "backups_enabled", NULL);
|
|
}
|
|
|
|
g_key_file_unref(kf);
|
|
g_free(path);
|
|
}
|
|
|
|
/* -------------------- View Settings -------------------- */
|
|
|
|
static void apply_view_settings(AppState *s) {
|
|
gtk_widget_set_visible(s->status_bar_box, s->show_status_bar);
|
|
|
|
gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(s->view),
|
|
s->word_wrap ? GTK_WRAP_WORD_CHAR : GTK_WRAP_NONE);
|
|
|
|
gtk_source_view_set_show_line_numbers(s->view, s->show_line_numbers);
|
|
}
|
|
|
|
/* -------------------- Dark Mode Detection -------------------- */
|
|
|
|
static gboolean strcasestr_contains(const gchar *haystack, const gchar *needle) {
|
|
if (!haystack || !needle) return FALSE;
|
|
gchar *h = g_ascii_strdown(haystack, -1);
|
|
gchar *n = g_ascii_strdown(needle, -1);
|
|
gboolean ok = (strstr(h, n) != NULL);
|
|
g_free(h);
|
|
g_free(n);
|
|
return ok;
|
|
}
|
|
|
|
static void update_dark_mode(AppState *s) {
|
|
GtkSettings *settings = gtk_settings_get_default();
|
|
if (!settings) return;
|
|
|
|
gboolean prefer_dark = FALSE;
|
|
gchar *theme_name = NULL;
|
|
|
|
g_object_get(settings,
|
|
"gtk-application-prefer-dark-theme", &prefer_dark,
|
|
"gtk-theme-name", &theme_name,
|
|
NULL);
|
|
|
|
// Heuristic:
|
|
// - If prefer-dark is true OR theme name contains "dark", consider dark mode active.
|
|
gboolean is_dark = prefer_dark || (theme_name && strcasestr_contains(theme_name, "dark"));
|
|
|
|
if (s->is_dark_mode != is_dark) {
|
|
s->is_dark_mode = is_dark;
|
|
// For now we just track it (could later toggle CSS, icons, etc.)
|
|
g_message("Dark mode detected: %s (theme=%s prefer-dark=%s)",
|
|
s->is_dark_mode ? "ON" : "OFF",
|
|
theme_name ? theme_name : "(null)",
|
|
prefer_dark ? "true" : "false");
|
|
}
|
|
|
|
g_free(theme_name);
|
|
}
|
|
|
|
static void on_settings_notify(GtkSettings *settings, GParamSpec *pspec, gpointer user_data) {
|
|
(void)settings; (void)pspec;
|
|
AppState *s = (AppState*)user_data;
|
|
update_dark_mode(s);
|
|
}
|
|
|
|
/* -------------------- Backup Handling -------------------- */
|
|
|
|
static gchar* get_temp_untitled_backup_path(void) {
|
|
return g_build_filename(g_get_tmp_dir(), "SuperNotepad-Untitled.backup", NULL);
|
|
}
|
|
|
|
static gchar* compute_backup_path(AppState *s) {
|
|
if (s->current_path && *s->current_path) {
|
|
return g_strconcat(s->current_path, ".backup", NULL);
|
|
}
|
|
return get_temp_untitled_backup_path();
|
|
}
|
|
|
|
static void delete_backup_file_if_any(AppState *s) {
|
|
if (!s->backup_path) {
|
|
s->backup_path = compute_backup_path(s);
|
|
}
|
|
if (s->backup_path) {
|
|
(void)g_remove(s->backup_path);
|
|
}
|
|
}
|
|
|
|
static gboolean write_backup_now(gpointer user_data) {
|
|
AppState *s = (AppState*)user_data;
|
|
s->backup_timeout_id = 0;
|
|
|
|
if (!s->backups_enabled) return G_SOURCE_REMOVE;
|
|
if (!gtk_text_buffer_get_modified(GTK_TEXT_BUFFER(s->buffer))) return G_SOURCE_REMOVE;
|
|
|
|
free_and_null(&s->backup_path);
|
|
s->backup_path = compute_backup_path(s);
|
|
|
|
gchar *text = buffer_get_all_text(GTK_TEXT_BUFFER(s->buffer));
|
|
(void)g_file_set_contents(s->backup_path, text, -1, NULL);
|
|
g_free(text);
|
|
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
static void schedule_backup(AppState *s) {
|
|
if (!s->backups_enabled) return;
|
|
if (!gtk_text_buffer_get_modified(GTK_TEXT_BUFFER(s->buffer))) return;
|
|
|
|
if (s->backup_timeout_id) {
|
|
g_source_remove(s->backup_timeout_id);
|
|
s->backup_timeout_id = 0;
|
|
}
|
|
s->backup_timeout_id = g_timeout_add(1000, write_backup_now, s);
|
|
}
|
|
|
|
/* -------------------- Recent Files Helpers -------------------- */
|
|
|
|
static void add_path_to_recent(AppState *s, const gchar *path) {
|
|
if (!s->recent_manager || !path || !*path) return;
|
|
|
|
GError *err = NULL;
|
|
gchar *uri = g_filename_to_uri(path, NULL, &err);
|
|
if (!uri) {
|
|
if (err) g_clear_error(&err);
|
|
return;
|
|
}
|
|
|
|
gtk_recent_manager_add_item(s->recent_manager, uri);
|
|
g_free(uri);
|
|
}
|
|
|
|
/* -------------------- File I/O -------------------- */
|
|
|
|
static gboolean save_to_path(AppState *s, const gchar *path) {
|
|
gchar *text = buffer_get_all_text(GTK_TEXT_BUFFER(s->buffer));
|
|
GError *err = NULL;
|
|
|
|
gboolean ok = g_file_set_contents(path, text, -1, &err);
|
|
g_free(text);
|
|
|
|
if (!ok) {
|
|
GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win),
|
|
GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
|
|
"Save failed:\n%s", err ? err->message : "Unknown error");
|
|
gtk_dialog_run(GTK_DIALOG(d));
|
|
gtk_widget_destroy(d);
|
|
g_clear_error(&err);
|
|
return FALSE;
|
|
}
|
|
|
|
free_and_null(&s->current_path);
|
|
s->current_path = g_strdup(path);
|
|
|
|
gtk_text_buffer_set_modified(GTK_TEXT_BUFFER(s->buffer), FALSE);
|
|
update_title(s);
|
|
update_status(s);
|
|
|
|
delete_backup_file_if_any(s);
|
|
add_path_to_recent(s, path);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean open_from_path(AppState *s, const gchar *path) {
|
|
if (!path || !*path) return FALSE;
|
|
|
|
gchar *contents = NULL;
|
|
gsize len = 0;
|
|
GError *err = NULL;
|
|
|
|
if (!g_file_get_contents(path, &contents, &len, &err)) {
|
|
GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win),
|
|
GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
|
|
"Open failed:\n%s", err ? err->message : "Unknown error");
|
|
gtk_dialog_run(GTK_DIALOG(d));
|
|
gtk_widget_destroy(d);
|
|
g_clear_error(&err);
|
|
return FALSE;
|
|
}
|
|
|
|
gtk_text_buffer_set_text(GTK_TEXT_BUFFER(s->buffer), contents, (gint)len);
|
|
gtk_text_buffer_set_modified(GTK_TEXT_BUFFER(s->buffer), FALSE);
|
|
|
|
free_and_null(&s->current_path);
|
|
s->current_path = g_strdup(path);
|
|
|
|
free_and_null(&s->backup_path);
|
|
free_and_null(&s->last_find);
|
|
s->has_last_match = FALSE;
|
|
|
|
update_title(s);
|
|
update_status(s);
|
|
|
|
g_free(contents);
|
|
|
|
add_path_to_recent(s, path);
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean do_save_as(AppState *s) {
|
|
GtkWidget *dlg = gtk_file_chooser_dialog_new("Save As",
|
|
GTK_WINDOW(s->win),
|
|
GTK_FILE_CHOOSER_ACTION_SAVE,
|
|
"_Cancel", GTK_RESPONSE_CANCEL,
|
|
"_Save", GTK_RESPONSE_ACCEPT,
|
|
NULL);
|
|
|
|
gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dlg), TRUE);
|
|
if (s->current_path) {
|
|
gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dlg), s->current_path);
|
|
}
|
|
|
|
int resp = gtk_dialog_run(GTK_DIALOG(dlg));
|
|
if (resp != GTK_RESPONSE_ACCEPT) { gtk_widget_destroy(dlg); return FALSE; }
|
|
|
|
char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dlg));
|
|
gtk_widget_destroy(dlg);
|
|
if (!filename) return FALSE;
|
|
|
|
gboolean ok = save_to_path(s, filename);
|
|
g_free(filename);
|
|
return ok;
|
|
}
|
|
|
|
/* Clean close/save prompt */
|
|
static gboolean ensure_saved_or_cancel(AppState *s) {
|
|
if (!gtk_text_buffer_get_modified(GTK_TEXT_BUFFER(s->buffer))) return TRUE;
|
|
|
|
GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win),
|
|
GTK_DIALOG_MODAL,
|
|
GTK_MESSAGE_QUESTION,
|
|
GTK_BUTTONS_NONE,
|
|
"Save changes to this document?");
|
|
gtk_dialog_add_buttons(GTK_DIALOG(d),
|
|
"Cancel", GTK_RESPONSE_CANCEL,
|
|
"Don't Save", GTK_RESPONSE_REJECT,
|
|
"Save", GTK_RESPONSE_ACCEPT,
|
|
NULL);
|
|
|
|
int resp = gtk_dialog_run(GTK_DIALOG(d));
|
|
gtk_widget_destroy(d);
|
|
|
|
if (resp == GTK_RESPONSE_CANCEL) return FALSE;
|
|
if (resp == GTK_RESPONSE_REJECT) return TRUE;
|
|
|
|
if (s->current_path) {
|
|
return save_to_path(s, s->current_path);
|
|
}
|
|
return do_save_as(s);
|
|
}
|
|
|
|
static gboolean on_window_delete_event(GtkWidget *w, GdkEvent *e, gpointer user_data) {
|
|
(void)w; (void)e;
|
|
AppState *s = (AppState*)user_data;
|
|
|
|
if (!ensure_saved_or_cancel(s)) return TRUE; // block close
|
|
return FALSE; // allow close
|
|
}
|
|
|
|
/* -------------------- Actions: File Menu -------------------- */
|
|
|
|
static void action_new(GtkWidget *w, gpointer user_data) {
|
|
(void)w;
|
|
AppState *s = (AppState*)user_data;
|
|
|
|
if (!ensure_saved_or_cancel(s)) return;
|
|
|
|
gtk_text_buffer_set_text(GTK_TEXT_BUFFER(s->buffer), "", -1);
|
|
gtk_text_buffer_set_modified(GTK_TEXT_BUFFER(s->buffer), FALSE);
|
|
|
|
free_and_null(&s->current_path);
|
|
free_and_null(&s->backup_path);
|
|
free_and_null(&s->last_find);
|
|
s->has_last_match = FALSE;
|
|
|
|
update_title(s);
|
|
update_status(s);
|
|
delete_backup_file_if_any(s);
|
|
}
|
|
|
|
static void action_open(GtkWidget *w, gpointer user_data) {
|
|
(void)w;
|
|
AppState *s = (AppState*)user_data;
|
|
|
|
if (!ensure_saved_or_cancel(s)) return;
|
|
|
|
GtkWidget *dlg = gtk_file_chooser_dialog_new("Open",
|
|
GTK_WINDOW(s->win),
|
|
GTK_FILE_CHOOSER_ACTION_OPEN,
|
|
"_Cancel", GTK_RESPONSE_CANCEL,
|
|
"_Open", GTK_RESPONSE_ACCEPT,
|
|
NULL);
|
|
|
|
int resp = gtk_dialog_run(GTK_DIALOG(dlg));
|
|
if (resp != GTK_RESPONSE_ACCEPT) { gtk_widget_destroy(dlg); return; }
|
|
|
|
char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dlg));
|
|
gtk_widget_destroy(dlg);
|
|
if (!filename) return;
|
|
|
|
(void)open_from_path(s, filename);
|
|
g_free(filename);
|
|
}
|
|
|
|
static void action_save(GtkWidget *w, gpointer user_data) {
|
|
(void)w;
|
|
AppState *s = (AppState*)user_data;
|
|
|
|
if (!s->current_path) {
|
|
(void)do_save_as(s);
|
|
return;
|
|
}
|
|
(void)save_to_path(s, s->current_path);
|
|
}
|
|
|
|
static void action_save_as(GtkWidget *w, gpointer user_data) {
|
|
(void)w;
|
|
AppState *s = (AppState*)user_data;
|
|
(void)do_save_as(s);
|
|
}
|
|
|
|
static void action_exit(GtkWidget *w, gpointer user_data) {
|
|
(void)w;
|
|
AppState *s = (AppState*)user_data;
|
|
gtk_window_close(GTK_WINDOW(s->win));
|
|
}
|
|
|
|
/* Recent chooser callback */
|
|
static void on_recent_item_activated(GtkRecentChooser *chooser, gpointer user_data) {
|
|
AppState *s = (AppState*)user_data;
|
|
|
|
if (!ensure_saved_or_cancel(s)) return;
|
|
|
|
gchar *uri = gtk_recent_chooser_get_current_uri(chooser);
|
|
if (!uri) return;
|
|
|
|
GError *err = NULL;
|
|
gchar *filename = g_filename_from_uri(uri, NULL, &err);
|
|
g_free(uri);
|
|
|
|
if (!filename) {
|
|
if (err) g_clear_error(&err);
|
|
return;
|
|
}
|
|
|
|
(void)open_from_path(s, filename);
|
|
g_free(filename);
|
|
}
|
|
|
|
/* -------------------- Print: Real pagination -------------------- */
|
|
|
|
typedef struct {
|
|
GtkSourcePrintCompositor *comp;
|
|
} PrintState;
|
|
|
|
static void on_print_begin(GtkPrintOperation *op, GtkPrintContext *ctx, gpointer user_data) {
|
|
PrintState *ps = (PrintState*)user_data;
|
|
|
|
while (!gtk_source_print_compositor_paginate(ps->comp, ctx)) { }
|
|
|
|
int pages = gtk_source_print_compositor_get_n_pages(ps->comp);
|
|
gtk_print_operation_set_n_pages(op, pages);
|
|
}
|
|
|
|
static void on_print_draw_page(GtkPrintOperation *op, GtkPrintContext *ctx, int page_nr, gpointer user_data) {
|
|
(void)op;
|
|
PrintState *ps = (PrintState*)user_data;
|
|
gtk_source_print_compositor_draw_page(ps->comp, ctx, page_nr);
|
|
}
|
|
|
|
static void on_print_done(GtkPrintOperation *op, GtkPrintOperationResult res, gpointer user_data) {
|
|
(void)op; (void)res;
|
|
PrintState *ps = (PrintState*)user_data;
|
|
if (ps->comp) g_object_unref(ps->comp);
|
|
g_free(ps);
|
|
}
|
|
|
|
static void action_print(GtkWidget *w, gpointer user_data) {
|
|
(void)w;
|
|
AppState *s = (AppState*)user_data;
|
|
|
|
PrintState *ps = g_new0(PrintState, 1);
|
|
ps->comp = gtk_source_print_compositor_new(GTK_SOURCE_BUFFER(s->buffer));
|
|
|
|
GtkPrintOperation *op = gtk_print_operation_new();
|
|
gtk_print_operation_set_job_name(op, "SuperNotepad Print");
|
|
|
|
g_signal_connect(op, "begin-print", G_CALLBACK(on_print_begin), ps);
|
|
g_signal_connect(op, "draw-page", G_CALLBACK(on_print_draw_page), ps);
|
|
g_signal_connect(op, "done", G_CALLBACK(on_print_done), ps);
|
|
|
|
GError *err = NULL;
|
|
GtkPrintOperationResult r = gtk_print_operation_run(op,
|
|
GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG,
|
|
GTK_WINDOW(s->win),
|
|
&err);
|
|
|
|
if (r == GTK_PRINT_OPERATION_RESULT_ERROR) {
|
|
GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win),
|
|
GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
|
|
"Print failed:\n%s", err ? err->message : "Unknown error");
|
|
gtk_dialog_run(GTK_DIALOG(d));
|
|
gtk_widget_destroy(d);
|
|
g_clear_error(&err);
|
|
}
|
|
|
|
g_object_unref(op);
|
|
}
|
|
|
|
/* -------------------- Actions: Edit Menu -------------------- */
|
|
|
|
static void action_undo(GtkWidget *w, gpointer user_data) {
|
|
(void)w;
|
|
AppState *s = (AppState*)user_data;
|
|
if (gtk_source_buffer_can_undo(s->buffer)) gtk_source_buffer_undo(s->buffer);
|
|
}
|
|
|
|
static void action_cut(GtkWidget *w, gpointer user_data) {
|
|
(void)w;
|
|
AppState *s = (AppState*)user_data;
|
|
GtkClipboard *cb = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
|
|
gtk_text_buffer_cut_clipboard(GTK_TEXT_BUFFER(s->buffer), cb, TRUE);
|
|
}
|
|
|
|
static void action_copy(GtkWidget *w, gpointer user_data) {
|
|
(void)w;
|
|
AppState *s = (AppState*)user_data;
|
|
GtkClipboard *cb = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
|
|
gtk_text_buffer_copy_clipboard(GTK_TEXT_BUFFER(s->buffer), cb);
|
|
}
|
|
|
|
static void action_paste(GtkWidget *w, gpointer user_data) {
|
|
(void)w;
|
|
AppState *s = (AppState*)user_data;
|
|
GtkClipboard *cb = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
|
|
gtk_text_buffer_paste_clipboard(GTK_TEXT_BUFFER(s->buffer), cb, NULL, TRUE);
|
|
}
|
|
|
|
static void action_delete(GtkWidget *w, gpointer user_data) {
|
|
(void)w;
|
|
AppState *s = (AppState*)user_data;
|
|
|
|
GtkTextIter start, end;
|
|
if (gtk_text_buffer_get_selection_bounds(GTK_TEXT_BUFFER(s->buffer), &start, &end)) {
|
|
gtk_text_buffer_delete(GTK_TEXT_BUFFER(s->buffer), &start, &end);
|
|
}
|
|
}
|
|
|
|
static void action_select_all(GtkWidget *w, gpointer user_data) {
|
|
(void)w;
|
|
AppState *s = (AppState*)user_data;
|
|
|
|
GtkTextIter start, end;
|
|
gtk_text_buffer_get_start_iter(GTK_TEXT_BUFFER(s->buffer), &start);
|
|
gtk_text_buffer_get_end_iter(GTK_TEXT_BUFFER(s->buffer), &end);
|
|
gtk_text_buffer_select_range(GTK_TEXT_BUFFER(s->buffer), &start, &end);
|
|
}
|
|
|
|
static void action_time_date(GtkWidget *w, gpointer user_data) {
|
|
(void)w;
|
|
AppState *s = (AppState*)user_data;
|
|
|
|
time_t t = time(NULL);
|
|
struct tm lt;
|
|
#if defined(_WIN32)
|
|
localtime_s(<, &t);
|
|
#else
|
|
localtime_r(&t, <);
|
|
#endif
|
|
|
|
char buf[128];
|
|
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", <);
|
|
|
|
GtkTextIter iter;
|
|
GtkTextMark *mark = gtk_text_buffer_get_insert(GTK_TEXT_BUFFER(s->buffer));
|
|
gtk_text_buffer_get_iter_at_mark(GTK_TEXT_BUFFER(s->buffer), &iter, mark);
|
|
gtk_text_buffer_insert(GTK_TEXT_BUFFER(s->buffer), &iter, buf, -1);
|
|
}
|
|
|
|
/* -------------------- Find / Replace / Go To -------------------- */
|
|
|
|
static gboolean do_find_from_iter(AppState *s, const gchar *needle, GtkTextIter *from, gboolean wrap) {
|
|
if (!needle || !*needle) return FALSE;
|
|
|
|
GtkTextIter start = *from;
|
|
GtkTextIter match_start, match_end;
|
|
|
|
gboolean found = gtk_text_iter_forward_search(&start, needle,
|
|
GTK_TEXT_SEARCH_TEXT_ONLY | GTK_TEXT_SEARCH_VISIBLE_ONLY,
|
|
&match_start, &match_end, NULL);
|
|
|
|
if (!found && wrap) {
|
|
GtkTextIter begin;
|
|
gtk_text_buffer_get_start_iter(GTK_TEXT_BUFFER(s->buffer), &begin);
|
|
found = gtk_text_iter_forward_search(&begin, needle,
|
|
GTK_TEXT_SEARCH_TEXT_ONLY | GTK_TEXT_SEARCH_VISIBLE_ONLY,
|
|
&match_start, &match_end, NULL);
|
|
}
|
|
|
|
if (found) {
|
|
gtk_text_buffer_select_range(GTK_TEXT_BUFFER(s->buffer), &match_start, &match_end);
|
|
gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(s->view), &match_start, 0.2, FALSE, 0, 0);
|
|
|
|
s->last_match_end = match_end;
|
|
s->has_last_match = TRUE;
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static void action_find(GtkWidget *w, gpointer user_data) {
|
|
(void)w;
|
|
AppState *s = (AppState*)user_data;
|
|
|
|
GtkWidget *dlg = gtk_dialog_new_with_buttons("Find",
|
|
GTK_WINDOW(s->win),
|
|
GTK_DIALOG_MODAL,
|
|
"_Cancel", GTK_RESPONSE_CANCEL,
|
|
"_Find", GTK_RESPONSE_ACCEPT,
|
|
NULL);
|
|
|
|
GtkWidget *box = gtk_dialog_get_content_area(GTK_DIALOG(dlg));
|
|
GtkWidget *entry = gtk_entry_new();
|
|
gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
|
|
|
|
gtk_box_pack_start(GTK_BOX(box), gtk_label_new("Find what:"), FALSE, FALSE, 6);
|
|
gtk_box_pack_start(GTK_BOX(box), entry, FALSE, FALSE, 6);
|
|
|
|
if (s->last_find) gtk_entry_set_text(GTK_ENTRY(entry), s->last_find);
|
|
|
|
gtk_dialog_set_default_response(GTK_DIALOG(dlg), GTK_RESPONSE_ACCEPT);
|
|
gtk_widget_show_all(dlg);
|
|
|
|
int resp = gtk_dialog_run(GTK_DIALOG(dlg));
|
|
if (resp != GTK_RESPONSE_ACCEPT) { gtk_widget_destroy(dlg); return; }
|
|
|
|
const gchar *needle = gtk_entry_get_text(GTK_ENTRY(entry));
|
|
free_and_null(&s->last_find);
|
|
s->last_find = g_strdup(needle);
|
|
|
|
GtkTextIter from;
|
|
GtkTextMark *mark = gtk_text_buffer_get_insert(GTK_TEXT_BUFFER(s->buffer));
|
|
gtk_text_buffer_get_iter_at_mark(GTK_TEXT_BUFFER(s->buffer), &from, mark);
|
|
|
|
gboolean ok = do_find_from_iter(s, needle, &from, TRUE);
|
|
gtk_widget_destroy(dlg);
|
|
|
|
if (!ok) {
|
|
GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win),
|
|
GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK,
|
|
"Text not found.");
|
|
gtk_dialog_run(GTK_DIALOG(d));
|
|
gtk_widget_destroy(d);
|
|
}
|
|
}
|
|
|
|
static void action_find_next(GtkWidget *w, gpointer user_data) {
|
|
(void)w;
|
|
AppState *s = (AppState*)user_data;
|
|
|
|
if (!s->last_find || !*s->last_find) {
|
|
action_find(NULL, s);
|
|
return;
|
|
}
|
|
|
|
GtkTextIter from;
|
|
if (s->has_last_match) {
|
|
from = s->last_match_end;
|
|
} else {
|
|
GtkTextMark *mark = gtk_text_buffer_get_insert(GTK_TEXT_BUFFER(s->buffer));
|
|
gtk_text_buffer_get_iter_at_mark(GTK_TEXT_BUFFER(s->buffer), &from, mark);
|
|
}
|
|
|
|
gboolean ok = do_find_from_iter(s, s->last_find, &from, TRUE);
|
|
if (!ok) {
|
|
GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win),
|
|
GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK,
|
|
"No further matches.");
|
|
gtk_dialog_run(GTK_DIALOG(d));
|
|
gtk_widget_destroy(d);
|
|
}
|
|
}
|
|
|
|
static void action_go_to(GtkWidget *w, gpointer user_data) {
|
|
(void)w;
|
|
AppState *s = (AppState*)user_data;
|
|
|
|
GtkWidget *dlg = gtk_dialog_new_with_buttons("Go To Line",
|
|
GTK_WINDOW(s->win),
|
|
GTK_DIALOG_MODAL,
|
|
"_Cancel", GTK_RESPONSE_CANCEL,
|
|
"_Go", GTK_RESPONSE_ACCEPT,
|
|
NULL);
|
|
|
|
GtkWidget *box = gtk_dialog_get_content_area(GTK_DIALOG(dlg));
|
|
GtkWidget *spin = gtk_spin_button_new_with_range(1, 1000000, 1);
|
|
|
|
gtk_box_pack_start(GTK_BOX(box), gtk_label_new("Line number:"), FALSE, FALSE, 6);
|
|
gtk_box_pack_start(GTK_BOX(box), spin, FALSE, FALSE, 6);
|
|
|
|
gtk_dialog_set_default_response(GTK_DIALOG(dlg), GTK_RESPONSE_ACCEPT);
|
|
gtk_widget_show_all(dlg);
|
|
|
|
int resp = gtk_dialog_run(GTK_DIALOG(dlg));
|
|
if (resp != GTK_RESPONSE_ACCEPT) { gtk_widget_destroy(dlg); return; }
|
|
|
|
int line = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(spin));
|
|
gtk_widget_destroy(dlg);
|
|
|
|
GtkTextIter iter;
|
|
gtk_text_buffer_get_iter_at_line(GTK_TEXT_BUFFER(s->buffer), &iter, line - 1);
|
|
gtk_text_buffer_place_cursor(GTK_TEXT_BUFFER(s->buffer), &iter);
|
|
gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(s->view), &iter, 0.2, FALSE, 0, 0);
|
|
|
|
update_status(s);
|
|
}
|
|
|
|
static void action_replace(GtkWidget *w, gpointer user_data) {
|
|
(void)w;
|
|
AppState *s = (AppState*)user_data;
|
|
|
|
GtkWidget *dlg = gtk_dialog_new_with_buttons("Replace",
|
|
GTK_WINDOW(s->win),
|
|
GTK_DIALOG_MODAL,
|
|
"_Close", GTK_RESPONSE_CLOSE,
|
|
"_Replace", 1,
|
|
"Replace _All", 2,
|
|
NULL);
|
|
|
|
GtkWidget *box = gtk_dialog_get_content_area(GTK_DIALOG(dlg));
|
|
GtkWidget *find_entry = gtk_entry_new();
|
|
GtkWidget *rep_entry = gtk_entry_new();
|
|
|
|
gtk_box_pack_start(GTK_BOX(box), gtk_label_new("Find what:"), FALSE, FALSE, 6);
|
|
gtk_box_pack_start(GTK_BOX(box), find_entry, FALSE, FALSE, 6);
|
|
gtk_box_pack_start(GTK_BOX(box), gtk_label_new("Replace with:"), FALSE, FALSE, 6);
|
|
gtk_box_pack_start(GTK_BOX(box), rep_entry, FALSE, FALSE, 6);
|
|
|
|
if (s->last_find) gtk_entry_set_text(GTK_ENTRY(find_entry), s->last_find);
|
|
|
|
gtk_widget_show_all(dlg);
|
|
|
|
for (;;) {
|
|
int resp = gtk_dialog_run(GTK_DIALOG(dlg));
|
|
if (resp == GTK_RESPONSE_CLOSE) break;
|
|
|
|
const gchar *needle = gtk_entry_get_text(GTK_ENTRY(find_entry));
|
|
const gchar *repl = gtk_entry_get_text(GTK_ENTRY(rep_entry));
|
|
|
|
if (!needle || !*needle) {
|
|
GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win),
|
|
GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK,
|
|
"Enter text to find.");
|
|
gtk_dialog_run(GTK_DIALOG(d));
|
|
gtk_widget_destroy(d);
|
|
continue;
|
|
}
|
|
|
|
free_and_null(&s->last_find);
|
|
s->last_find = g_strdup(needle);
|
|
|
|
if (resp == 1) {
|
|
GtkTextIter sel_start, sel_end;
|
|
if (gtk_text_buffer_get_selection_bounds(GTK_TEXT_BUFFER(s->buffer), &sel_start, &sel_end)) {
|
|
gchar *selected = gtk_text_buffer_get_text(GTK_TEXT_BUFFER(s->buffer), &sel_start, &sel_end, TRUE);
|
|
gboolean matches = (g_strcmp0(selected, needle) == 0);
|
|
g_free(selected);
|
|
|
|
if (matches) {
|
|
gtk_text_buffer_begin_user_action(GTK_TEXT_BUFFER(s->buffer));
|
|
gtk_text_buffer_delete(GTK_TEXT_BUFFER(s->buffer), &sel_start, &sel_end);
|
|
gtk_text_buffer_insert(GTK_TEXT_BUFFER(s->buffer), &sel_start, repl, -1);
|
|
gtk_text_buffer_end_user_action(GTK_TEXT_BUFFER(s->buffer));
|
|
|
|
GtkTextIter from = sel_start;
|
|
(void)do_find_from_iter(s, needle, &from, TRUE);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
action_find_next(NULL, s);
|
|
continue;
|
|
}
|
|
|
|
if (resp == 2) {
|
|
GtkTextIter iter;
|
|
gtk_text_buffer_get_start_iter(GTK_TEXT_BUFFER(s->buffer), &iter);
|
|
|
|
int count = 0;
|
|
gtk_text_buffer_begin_user_action(GTK_TEXT_BUFFER(s->buffer));
|
|
|
|
while (1) {
|
|
GtkTextIter ms, me;
|
|
gboolean found = gtk_text_iter_forward_search(&iter, needle,
|
|
GTK_TEXT_SEARCH_TEXT_ONLY | GTK_TEXT_SEARCH_VISIBLE_ONLY,
|
|
&ms, &me, NULL);
|
|
if (!found) break;
|
|
|
|
gtk_text_buffer_delete(GTK_TEXT_BUFFER(s->buffer), &ms, &me);
|
|
gtk_text_buffer_insert(GTK_TEXT_BUFFER(s->buffer), &ms, repl, -1);
|
|
|
|
iter = ms;
|
|
count++;
|
|
}
|
|
|
|
gtk_text_buffer_end_user_action(GTK_TEXT_BUFFER(s->buffer));
|
|
|
|
GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win),
|
|
GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK,
|
|
"Replaced %d occurrence(s).", count);
|
|
gtk_dialog_run(GTK_DIALOG(d));
|
|
gtk_widget_destroy(d);
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
gtk_widget_destroy(dlg);
|
|
}
|
|
|
|
/* -------------------- Toggles (View/Edit) -------------------- */
|
|
|
|
static void toggle_status_bar(GtkCheckMenuItem *mi, gpointer user_data) {
|
|
AppState *s = (AppState*)user_data;
|
|
s->show_status_bar = gtk_check_menu_item_get_active(mi);
|
|
apply_view_settings(s);
|
|
prefs_save(s);
|
|
}
|
|
|
|
static void toggle_word_wrap(GtkCheckMenuItem *mi, gpointer user_data) {
|
|
AppState *s = (AppState*)user_data;
|
|
s->word_wrap = gtk_check_menu_item_get_active(mi);
|
|
apply_view_settings(s);
|
|
prefs_save(s);
|
|
}
|
|
|
|
static void toggle_line_numbers(GtkCheckMenuItem *mi, gpointer user_data) {
|
|
AppState *s = (AppState*)user_data;
|
|
s->show_line_numbers = gtk_check_menu_item_get_active(mi);
|
|
apply_view_settings(s);
|
|
prefs_save(s);
|
|
}
|
|
|
|
static void toggle_backups(GtkCheckMenuItem *mi, gpointer user_data) {
|
|
AppState *s = (AppState*)user_data;
|
|
s->backups_enabled = gtk_check_menu_item_get_active(mi);
|
|
|
|
if (!s->backups_enabled) {
|
|
if (s->backup_timeout_id) {
|
|
g_source_remove(s->backup_timeout_id);
|
|
s->backup_timeout_id = 0;
|
|
}
|
|
delete_backup_file_if_any(s);
|
|
} else {
|
|
schedule_backup(s);
|
|
}
|
|
|
|
prefs_save(s);
|
|
}
|
|
|
|
/* -------------------- About -------------------- */
|
|
|
|
static void action_about(GtkWidget *w, gpointer user_data) {
|
|
(void)w;
|
|
AppState *s = (AppState*)user_data;
|
|
|
|
GtkWidget *dlg = gtk_about_dialog_new();
|
|
gtk_window_set_transient_for(GTK_WINDOW(dlg), GTK_WINDOW(s->win));
|
|
gtk_window_set_modal(GTK_WINDOW(dlg), TRUE);
|
|
|
|
gtk_about_dialog_set_program_name(GTK_ABOUT_DIALOG(dlg), "SuperNotepad");
|
|
gtk_about_dialog_set_version(GTK_ABOUT_DIALOG(dlg), "0.2");
|
|
|
|
const gchar *authors[] = { "John Paul Wohlscheid", "ChatGPT", NULL };
|
|
gtk_about_dialog_set_authors(GTK_ABOUT_DIALOG(dlg), authors);
|
|
|
|
gtk_about_dialog_set_comments(GTK_ABOUT_DIALOG(dlg),
|
|
"SuperNotepad 0.2\nLicensed under The Unlicense.");
|
|
|
|
gtk_about_dialog_set_license(GTK_ABOUT_DIALOG(dlg),
|
|
"This is free and unencumbered software released into the public domain.\n"
|
|
"For more information, see The Unlicense.");
|
|
|
|
gtk_about_dialog_set_website(GTK_ABOUT_DIALOG(dlg), "https://unlicense.org");
|
|
gtk_about_dialog_set_website_label(GTK_ABOUT_DIALOG(dlg), "The Unlicense (https://unlicense.org)");
|
|
|
|
gtk_dialog_run(GTK_DIALOG(dlg));
|
|
gtk_widget_destroy(dlg);
|
|
}
|
|
|
|
static void action_keyboard_shortcuts(GtkWidget *w, gpointer user_data) {
|
|
(void)w;
|
|
AppState *s = (AppState*)user_data;
|
|
|
|
const char *msg =
|
|
"Keyboard Shortcuts\n"
|
|
"\n"
|
|
"File\n"
|
|
" Ctrl+N New\n"
|
|
" Ctrl+O Open\n"
|
|
" Ctrl+S Save\n"
|
|
" Ctrl+Shift+S Save As\n"
|
|
" Ctrl+P Print\n"
|
|
" Ctrl+Q Exit\n"
|
|
"\n"
|
|
"Edit\n"
|
|
" Ctrl+Z Undo\n"
|
|
" Ctrl+X Cut\n"
|
|
" Ctrl+C Copy\n"
|
|
" Ctrl+V Paste\n"
|
|
" Delete Delete selection\n"
|
|
" Ctrl+F Find\n"
|
|
" F3 Find Next\n"
|
|
" Ctrl+H Replace\n"
|
|
" Ctrl+G Go To\n"
|
|
" Ctrl+A Select All\n"
|
|
" F5 Time/Date\n"
|
|
"\n"
|
|
"View\n"
|
|
" Ctrl+Shift+W Toggle Word Wrap\n"
|
|
" Ctrl+Shift+L Toggle Line Numbering\n"
|
|
" Ctrl+Alt+B Toggle Status Bar\n"
|
|
"\n"
|
|
"Help\n"
|
|
" F1 About\n";
|
|
|
|
GtkWidget *dlg = gtk_message_dialog_new(GTK_WINDOW(s->win),
|
|
GTK_DIALOG_MODAL,
|
|
GTK_MESSAGE_INFO,
|
|
GTK_BUTTONS_OK,
|
|
"%s", msg);
|
|
|
|
gtk_window_set_title(GTK_WINDOW(dlg), "Keyboard Shortcuts");
|
|
gtk_dialog_run(GTK_DIALOG(dlg));
|
|
gtk_widget_destroy(dlg);
|
|
}
|
|
|
|
/* -------------------- Buffer signals -------------------- */
|
|
|
|
static void on_mark_set(GtkTextBuffer *buf, GtkTextIter *new_loc, GtkTextMark *mark, gpointer user_data) {
|
|
(void)buf; (void)new_loc; (void)mark;
|
|
AppState *s = (AppState*)user_data;
|
|
update_status(s);
|
|
}
|
|
|
|
static void on_modified_changed(GtkTextBuffer *buf, gpointer user_data) {
|
|
(void)buf;
|
|
AppState *s = (AppState*)user_data;
|
|
update_status(s);
|
|
}
|
|
|
|
static void on_buffer_changed(GtkTextBuffer *buf, gpointer user_data) {
|
|
(void)buf;
|
|
AppState *s = (AppState*)user_data;
|
|
update_status(s);
|
|
schedule_backup(s);
|
|
}
|
|
|
|
/* -------------------- Menu Helpers + Shortcuts -------------------- */
|
|
|
|
static GtkWidget* menu_item(AppState *s, const gchar *label, GCallback cb) {
|
|
GtkWidget *mi = gtk_menu_item_new_with_label(label);
|
|
if (cb) g_signal_connect(mi, "activate", cb, s);
|
|
return mi;
|
|
}
|
|
|
|
static GtkWidget* menu_item_accel(AppState *s, const gchar *label, GCallback cb,
|
|
guint keyval, GdkModifierType mods) {
|
|
GtkWidget *mi = menu_item(s, label, cb);
|
|
if (s->accel_group && cb && keyval != 0) {
|
|
gtk_widget_add_accelerator(mi, "activate", s->accel_group,
|
|
keyval, mods, GTK_ACCEL_VISIBLE);
|
|
}
|
|
return mi;
|
|
}
|
|
|
|
static GtkWidget* check_menu_item(AppState *s, const gchar *label, gboolean active,
|
|
GCallback toggled_cb,
|
|
guint keyval, GdkModifierType mods) {
|
|
GtkWidget *mi = gtk_check_menu_item_new_with_label(label);
|
|
gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(mi), active);
|
|
if (toggled_cb) g_signal_connect(mi, "toggled", toggled_cb, s);
|
|
|
|
// For check items, accelerator triggers "activate" which toggles it.
|
|
if (s->accel_group && keyval != 0) {
|
|
gtk_widget_add_accelerator(mi, "activate", s->accel_group,
|
|
keyval, mods, GTK_ACCEL_VISIBLE);
|
|
}
|
|
return mi;
|
|
}
|
|
|
|
/* Builds the menubar (includes Open Recent) */
|
|
static GtkWidget* build_menubar(AppState *s) {
|
|
GtkWidget *menubar = gtk_menu_bar_new();
|
|
|
|
/* File */
|
|
GtkWidget *file_menu = gtk_menu_new();
|
|
GtkWidget *file_root = gtk_menu_item_new_with_label("File");
|
|
gtk_menu_item_set_submenu(GTK_MENU_ITEM(file_root), file_menu);
|
|
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu),
|
|
menu_item_accel(s, "New", G_CALLBACK(action_new), GDK_KEY_n, GDK_CONTROL_MASK));
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu),
|
|
menu_item_accel(s, "Open", G_CALLBACK(action_open), GDK_KEY_o, GDK_CONTROL_MASK));
|
|
|
|
// Open Recent (GtkRecentChooserMenu)
|
|
GtkWidget *recent_menu = gtk_recent_chooser_menu_new_for_manager(s->recent_manager);
|
|
gtk_recent_chooser_set_limit(GTK_RECENT_CHOOSER(recent_menu), 15);
|
|
gtk_recent_chooser_set_show_not_found(GTK_RECENT_CHOOSER(recent_menu), FALSE);
|
|
gtk_recent_chooser_set_show_private(GTK_RECENT_CHOOSER(recent_menu), FALSE);
|
|
gtk_recent_chooser_set_local_only(GTK_RECENT_CHOOSER(recent_menu), TRUE);
|
|
g_signal_connect(recent_menu, "item-activated", G_CALLBACK(on_recent_item_activated), s);
|
|
|
|
GtkWidget *open_recent = gtk_menu_item_new_with_label("Open Recent");
|
|
gtk_menu_item_set_submenu(GTK_MENU_ITEM(open_recent), recent_menu);
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), open_recent);
|
|
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu),
|
|
menu_item_accel(s, "Save", G_CALLBACK(action_save), GDK_KEY_s, GDK_CONTROL_MASK));
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu),
|
|
menu_item_accel(s, "Save As", G_CALLBACK(action_save_as), GDK_KEY_s, (GdkModifierType)(GDK_CONTROL_MASK | GDK_MOD1_MASK)));
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu),
|
|
menu_item_accel(s, "Print", G_CALLBACK(action_print), GDK_KEY_p, GDK_CONTROL_MASK));
|
|
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), gtk_separator_menu_item_new());
|
|
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu),
|
|
menu_item_accel(s, "Exit", G_CALLBACK(action_exit), GDK_KEY_q, GDK_CONTROL_MASK));
|
|
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(menubar), file_root);
|
|
|
|
/* Edit */
|
|
GtkWidget *edit_menu = gtk_menu_new();
|
|
GtkWidget *edit_root = gtk_menu_item_new_with_label("Edit");
|
|
gtk_menu_item_set_submenu(GTK_MENU_ITEM(edit_root), edit_menu);
|
|
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu),
|
|
menu_item_accel(s, "Undo", G_CALLBACK(action_undo), GDK_KEY_z, GDK_CONTROL_MASK));
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), gtk_separator_menu_item_new());
|
|
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu),
|
|
menu_item_accel(s, "Cut", G_CALLBACK(action_cut), GDK_KEY_x, GDK_CONTROL_MASK));
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu),
|
|
menu_item_accel(s, "Copy", G_CALLBACK(action_copy), GDK_KEY_c, GDK_CONTROL_MASK));
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu),
|
|
menu_item_accel(s, "Paste", G_CALLBACK(action_paste), GDK_KEY_v, GDK_CONTROL_MASK));
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu),
|
|
menu_item_accel(s, "Delete", G_CALLBACK(action_delete), GDK_KEY_Delete, (GdkModifierType)0));
|
|
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), gtk_separator_menu_item_new());
|
|
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu),
|
|
menu_item_accel(s, "Find", G_CALLBACK(action_find), GDK_KEY_f, GDK_CONTROL_MASK));
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu),
|
|
menu_item_accel(s, "Find Next", G_CALLBACK(action_find_next), GDK_KEY_F3, (GdkModifierType)0));
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu),
|
|
menu_item_accel(s, "Replace", G_CALLBACK(action_replace), GDK_KEY_h, GDK_CONTROL_MASK));
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu),
|
|
menu_item_accel(s, "Go To", G_CALLBACK(action_go_to), GDK_KEY_g, GDK_CONTROL_MASK));
|
|
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), gtk_separator_menu_item_new());
|
|
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu),
|
|
menu_item_accel(s, "Select All", G_CALLBACK(action_select_all), GDK_KEY_a, GDK_CONTROL_MASK));
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu),
|
|
menu_item_accel(s, "Time/Date", G_CALLBACK(action_time_date), GDK_KEY_F5, (GdkModifierType)0));
|
|
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), gtk_separator_menu_item_new());
|
|
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu),
|
|
check_menu_item(s, "Create .backup when unsaved", s->backups_enabled, G_CALLBACK(toggle_backups),
|
|
GDK_KEY_b, (GdkModifierType)(GDK_CONTROL_MASK | GDK_SHIFT_MASK)));
|
|
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(menubar), edit_root);
|
|
|
|
/* View */
|
|
GtkWidget *view_menu = gtk_menu_new();
|
|
GtkWidget *view_root = gtk_menu_item_new_with_label("View");
|
|
gtk_menu_item_set_submenu(GTK_MENU_ITEM(view_root), view_menu);
|
|
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(view_menu),
|
|
check_menu_item(s, "Status Bar", s->show_status_bar, G_CALLBACK(toggle_status_bar),
|
|
GDK_KEY_b, (GdkModifierType)(GDK_CONTROL_MASK | GDK_MOD1_MASK)));
|
|
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(view_menu),
|
|
check_menu_item(s, "Word Wrap", s->word_wrap, G_CALLBACK(toggle_word_wrap),
|
|
GDK_KEY_w, (GdkModifierType)(GDK_CONTROL_MASK | GDK_SHIFT_MASK)));
|
|
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(view_menu),
|
|
check_menu_item(s, "Line Numbering", s->show_line_numbers, G_CALLBACK(toggle_line_numbers),
|
|
GDK_KEY_l, (GdkModifierType)(GDK_CONTROL_MASK | GDK_SHIFT_MASK)));
|
|
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(menubar), view_root);
|
|
|
|
/* Help */
|
|
GtkWidget *help_menu = gtk_menu_new();
|
|
GtkWidget *help_root = gtk_menu_item_new_with_label("Help");
|
|
gtk_menu_item_set_submenu(GTK_MENU_ITEM(help_root), help_menu);
|
|
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(help_menu),
|
|
menu_item(s, "Keyboard Shortcuts", G_CALLBACK(action_keyboard_shortcuts)));
|
|
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(help_menu), gtk_separator_menu_item_new());
|
|
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(help_menu),
|
|
menu_item_accel(s, "About", G_CALLBACK(action_about), GDK_KEY_F1, (GdkModifierType)0));
|
|
|
|
|
|
gtk_menu_shell_append(GTK_MENU_SHELL(menubar), help_root);
|
|
|
|
return menubar;
|
|
}
|
|
|
|
/* -------------------- Cleanup -------------------- */
|
|
|
|
static void on_window_destroy(GtkWidget *w, gpointer user_data) {
|
|
(void)w;
|
|
AppState *s = (AppState*)user_data;
|
|
|
|
prefs_save(s);
|
|
|
|
if (s->backup_timeout_id) {
|
|
g_source_remove(s->backup_timeout_id);
|
|
s->backup_timeout_id = 0;
|
|
}
|
|
|
|
free_and_null(&s->current_path);
|
|
free_and_null(&s->backup_path);
|
|
free_and_null(&s->last_find);
|
|
|
|
g_free(s);
|
|
}
|
|
|
|
/* -------------------- App Init -------------------- */
|
|
|
|
static void app_activate(GtkApplication *app, gpointer user_data) {
|
|
(void)user_data;
|
|
|
|
AppState *s = g_new0(AppState, 1);
|
|
s->app = app;
|
|
|
|
// Defaults
|
|
s->show_status_bar = TRUE;
|
|
s->word_wrap = FALSE;
|
|
s->show_line_numbers = TRUE;
|
|
s->backups_enabled = TRUE;
|
|
|
|
// Recent manager + accel group
|
|
s->recent_manager = gtk_recent_manager_get_default();
|
|
s->accel_group = gtk_accel_group_new();
|
|
|
|
// Load saved prefs (overrides defaults if present)
|
|
prefs_load(s);
|
|
|
|
s->win = gtk_application_window_new(app);
|
|
gtk_window_set_default_size(GTK_WINDOW(s->win), 900, 600);
|
|
|
|
// Attach accelerators to window
|
|
gtk_window_add_accel_group(GTK_WINDOW(s->win), s->accel_group);
|
|
|
|
// Close prompt
|
|
g_signal_connect(s->win, "delete-event", G_CALLBACK(on_window_delete_event), s);
|
|
|
|
// Dark mode detection
|
|
GtkSettings *settings = gtk_settings_get_default();
|
|
if (settings) {
|
|
g_signal_connect(settings, "notify::gtk-theme-name", G_CALLBACK(on_settings_notify), s);
|
|
g_signal_connect(settings, "notify::gtk-application-prefer-dark-theme", G_CALLBACK(on_settings_notify), s);
|
|
}
|
|
|
|
// Main layout
|
|
GtkWidget *root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
|
gtk_container_add(GTK_CONTAINER(s->win), root);
|
|
|
|
// Menubar
|
|
s->menubar = build_menubar(s);
|
|
gtk_box_pack_start(GTK_BOX(root), s->menubar, FALSE, FALSE, 0);
|
|
|
|
// Editor
|
|
s->buffer = GTK_SOURCE_BUFFER(gtk_source_buffer_new(NULL));
|
|
s->view = GTK_SOURCE_VIEW(gtk_source_view_new_with_buffer(s->buffer));
|
|
|
|
s->scroller = gtk_scrolled_window_new(NULL, NULL);
|
|
gtk_container_add(GTK_CONTAINER(s->scroller), GTK_WIDGET(s->view));
|
|
gtk_box_pack_start(GTK_BOX(root), s->scroller, TRUE, TRUE, 0);
|
|
|
|
// Status bar area
|
|
s->status_bar_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
|
gtk_widget_set_margin_start(s->status_bar_box, 8);
|
|
gtk_widget_set_margin_end(s->status_bar_box, 8);
|
|
gtk_widget_set_margin_top(s->status_bar_box, 4);
|
|
gtk_widget_set_margin_bottom(s->status_bar_box, 4);
|
|
|
|
s->pos_label = gtk_label_new("Ln 1, Col 1");
|
|
s->state_label = gtk_label_new("Saved");
|
|
|
|
gtk_box_pack_start(GTK_BOX(s->status_bar_box), s->pos_label, FALSE, FALSE, 0);
|
|
gtk_box_pack_end(GTK_BOX(s->status_bar_box), s->state_label, FALSE, FALSE, 0);
|
|
|
|
gtk_box_pack_end(GTK_BOX(root), s->status_bar_box, FALSE, FALSE, 0);
|
|
|
|
apply_view_settings(s);
|
|
update_title(s);
|
|
update_status(s);
|
|
|
|
// Buffer signals
|
|
g_signal_connect(s->buffer, "mark-set", G_CALLBACK(on_mark_set), s);
|
|
g_signal_connect(s->buffer, "modified-changed", G_CALLBACK(on_modified_changed), s);
|
|
g_signal_connect(s->buffer, "changed", G_CALLBACK(on_buffer_changed), s);
|
|
|
|
// Cleanup
|
|
g_signal_connect(s->win, "destroy", G_CALLBACK(on_window_destroy), s);
|
|
|
|
gtk_widget_show_all(s->win);
|
|
|
|
// Run initial dark mode detection once window exists
|
|
update_dark_mode(s);
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
GtkApplication *app = gtk_application_new("com.example.SuperNotepad", G_APPLICATION_DEFAULT_FLAGS);
|
|
g_signal_connect(app, "activate", G_CALLBACK(app_activate), NULL);
|
|
|
|
int status = g_application_run(G_APPLICATION(app), argc, argv);
|
|
g_object_unref(app);
|
|
return status;
|
|
}
|