Files
SeeMD/src/window.c
T
2026-07-01 14:33:04 -04:00

1620 lines
55 KiB
C

#include "window.h"
#include "app.h"
#include "config.h"
#include "editor.h"
#include "markdown.h"
typedef struct {
gint start_offset;
gint end_offset;
GtkTextChildAnchor *table_anchor;
gint table_row;
gint table_col;
} SearchMatch;
#define TAG_SEARCH_MATCH "seemd_search_match"
#define TAG_SEARCH_CURRENT "seemd_search_current"
#define TOOLBAR_LABEL_DATA "seemd-toolbar-label"
static void on_open_clicked(GtkButton *button, gpointer user_data);
static void on_refresh_clicked(GtkButton *button, gpointer user_data);
static void on_edit_clicked(GtkButton *button, gpointer user_data);
static void on_save_clicked(GtkButton *button, gpointer user_data);
static void on_settings_clicked(GtkButton *button, gpointer user_data);
static void on_about_clicked(GtkButton *button, gpointer user_data);
static void on_search_changed(GtkEditable *editable, gpointer user_data);
static void on_search_prev_clicked(GtkButton *button, gpointer user_data);
static void on_search_next_clicked(GtkButton *button, gpointer user_data);
static gboolean on_search_entry_key_press(GtkWidget *widget, GdkEventKey *event,
gpointer user_data);
static void on_editor_buffer_changed(GtkTextBuffer *buffer, gpointer user_data);
static void ensure_search_tags(MarkydWindow *self);
static void update_search_tag_styles(MarkydWindow *self, const gchar *match_bg,
const gchar *match_fg,
const gchar *current_bg,
const gchar *current_fg);
static void clear_search_matches(MarkydWindow *self);
static void update_search_matches(MarkydWindow *self);
static void jump_to_search_match(MarkydWindow *self, gint index,
gboolean scroll_to_match);
static void clear_table_search_highlight(MarkydWindow *self, gboolean clear_match,
gboolean clear_current);
static void apply_table_search_match_highlight(MarkydWindow *self);
static gboolean resolve_table_match_location(MarkydWindow *self, gint start_offset,
gint end_offset,
GtkTextChildAnchor **out_anchor,
gint *out_row, gint *out_col);
static GtkWidget *lookup_table_cell_widget(GtkWidget *table_widget, gint row,
gint col);
static void set_table_cell_highlight(GtkWidget *cell, gboolean match,
gboolean current);
static gboolean scroll_to_table_cell(MarkydWindow *self, GtkWidget *cell);
static void show_search_ui(MarkydWindow *self);
static void hide_search_ui(MarkydWindow *self);
static gboolean on_key_press_event(GtkWidget *widget, GdkEventKey *event,
gpointer user_data);
static gboolean on_configure_event(GtkWidget *widget, GdkEventConfigure *event,
gpointer user_data);
static gboolean on_window_state_event(GtkWidget *widget,
GdkEventWindowState *event,
gpointer user_data);
static void on_font_size_changed(GtkSpinButton *spin, gpointer user_data);
static void on_font_family_changed(GtkComboBoxText *combo, gpointer user_data);
static void on_theme_changed(GtkComboBoxText *combo, gpointer user_data);
static gchar *rgba_to_hex(const GdkRGBA *rgba);
static void init_color_button(GtkColorButton *btn, const gchar *color_str);
static void on_color_set(GtkColorButton *btn, gpointer user_data);
static GtkWidget *create_settings_dialog(MarkydApp *app);
static gboolean prompt_save_as(MarkydWindow *self);
static void update_edit_button_state(MarkydWindow *self);
static void set_edit_mode(MarkydWindow *self, gboolean edit_mode);
static GtkWidget *create_toolbar_button(const gchar *icon_name, const gchar *label,
const gchar *tooltip);
static void set_toolbar_button_label(GtkWidget *button, const gchar *label);
static void restore_editor_focus(MarkydWindow *self);
static gboolean geometry_debug_enabled(void) {
const gchar *v = g_getenv("SEEMD_DEBUG_GEOMETRY");
if (!v) {
v = g_getenv("TRAYMD_DEBUG_GEOMETRY");
}
return v && v[0] != '\0' && g_strcmp0(v, "0") != 0;
}
static void ensure_search_tags(MarkydWindow *self) {
GtkTextTagTable *table;
if (!self || !self->editor || !self->editor->buffer) {
return;
}
table = gtk_text_buffer_get_tag_table(self->editor->buffer);
if (!table) {
return;
}
if (!gtk_text_tag_table_lookup(table, TAG_SEARCH_MATCH)) {
gtk_text_buffer_create_tag(self->editor->buffer, TAG_SEARCH_MATCH, "weight",
PANGO_WEIGHT_BOLD, NULL);
}
if (!gtk_text_tag_table_lookup(table, TAG_SEARCH_CURRENT)) {
gtk_text_buffer_create_tag(self->editor->buffer, TAG_SEARCH_CURRENT, "weight",
PANGO_WEIGHT_BOLD, NULL);
}
}
static void update_search_tag_styles(MarkydWindow *self, const gchar *match_bg,
const gchar *match_fg,
const gchar *current_bg,
const gchar *current_fg) {
GtkTextTagTable *table;
GtkTextTag *tag;
if (!self || !self->editor || !self->editor->buffer) {
return;
}
table = gtk_text_buffer_get_tag_table(self->editor->buffer);
if (!table) {
return;
}
tag = gtk_text_tag_table_lookup(table, TAG_SEARCH_MATCH);
if (tag) {
g_object_set(tag, "background", match_bg, "foreground", match_fg, NULL);
}
tag = gtk_text_tag_table_lookup(table, TAG_SEARCH_CURRENT);
if (tag) {
g_object_set(tag, "background", current_bg, "foreground", current_fg, NULL);
}
}
static void clear_search_matches(MarkydWindow *self) {
GtkTextIter start;
GtkTextIter end;
if (!self || !self->editor || !self->editor->buffer) {
return;
}
markyd_editor_find_clear(self->editor);
gtk_text_buffer_get_bounds(self->editor->buffer, &start, &end);
gtk_text_buffer_remove_tag_by_name(self->editor->buffer, TAG_SEARCH_MATCH,
&start, &end);
gtk_text_buffer_remove_tag_by_name(self->editor->buffer, TAG_SEARCH_CURRENT,
&start, &end);
clear_table_search_highlight(self, TRUE, TRUE);
if (self->search_matches) {
g_array_set_size(self->search_matches, 0);
}
self->search_current_index = -1;
if (self->lbl_search_status) {
gtk_label_set_text(GTK_LABEL(self->lbl_search_status), "");
}
if (self->btn_search_prev) {
gtk_widget_set_sensitive(self->btn_search_prev, FALSE);
}
if (self->btn_search_next) {
gtk_widget_set_sensitive(self->btn_search_next, FALSE);
}
}
static void set_table_cell_highlight(GtkWidget *cell, gboolean match,
gboolean current) {
GtkStyleContext *style;
if (!cell) {
return;
}
style = gtk_widget_get_style_context(cell);
if (match) {
gtk_style_context_add_class(style, SEEMD_TABLE_CELL_MATCH_CLASS);
} else {
gtk_style_context_remove_class(style, SEEMD_TABLE_CELL_MATCH_CLASS);
}
if (current) {
gtk_style_context_add_class(style, SEEMD_TABLE_CELL_CURRENT_CLASS);
} else {
gtk_style_context_remove_class(style, SEEMD_TABLE_CELL_CURRENT_CLASS);
}
}
static gboolean scroll_to_table_cell(MarkydWindow *self, GtkWidget *cell) {
gint x = 0;
gint y = 0;
gdouble doc_x;
gdouble doc_y;
GtkAllocation alloc;
GtkAdjustment *hadj;
GtkAdjustment *vadj;
if (!self || !cell || !self->editor || !self->editor->text_view || !self->scroll) {
return FALSE;
}
if (!gtk_widget_translate_coordinates(cell, self->editor->text_view, 0, 0, &x,
&y)) {
return FALSE;
}
gtk_widget_get_allocation(cell, &alloc);
hadj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(self->scroll));
vadj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(self->scroll));
doc_x = x + (hadj ? gtk_adjustment_get_value(hadj) : 0.0);
doc_y = y + (vadj ? gtk_adjustment_get_value(vadj) : 0.0);
if (hadj) {
gdouble target = MAX(0.0, doc_x - 16.0);
gdouble max_val =
gtk_adjustment_get_upper(hadj) - gtk_adjustment_get_page_size(hadj);
gtk_adjustment_set_value(hadj, CLAMP(target, 0.0, MAX(0.0, max_val)));
}
if (vadj) {
gdouble page = gtk_adjustment_get_page_size(vadj);
gdouble target = MAX(0.0, doc_y - (page * 0.25));
gdouble max_val =
gtk_adjustment_get_upper(vadj) - gtk_adjustment_get_page_size(vadj);
gtk_adjustment_set_value(vadj, CLAMP(target, 0.0, MAX(0.0, max_val)));
}
return TRUE;
}
static GtkWidget *lookup_table_cell_widget(GtkWidget *table_widget, gint row,
gint col) {
GList *wrapper_children;
GtkWidget *found = NULL;
if (!table_widget || !GTK_IS_CONTAINER(table_widget)) {
return NULL;
}
wrapper_children = gtk_container_get_children(GTK_CONTAINER(table_widget));
for (GList *w = wrapper_children; w != NULL && !found; w = w->next) {
GtkWidget *child = GTK_WIDGET(w->data);
if (!GTK_IS_GRID(child)) {
continue;
}
GList *grid_children = gtk_container_get_children(GTK_CONTAINER(child));
for (GList *g = grid_children; g != NULL; g = g->next) {
GtkWidget *cell = GTK_WIDGET(g->data);
gint cell_row = GPOINTER_TO_INT(
g_object_get_data(G_OBJECT(cell), SEEMD_TABLE_CELL_ROW_DATA));
gint cell_col = GPOINTER_TO_INT(
g_object_get_data(G_OBJECT(cell), SEEMD_TABLE_CELL_COL_DATA));
if (cell_row == row && cell_col == col) {
found = cell;
break;
}
}
g_list_free(grid_children);
}
g_list_free(wrapper_children);
return found;
}
static void clear_table_search_highlight(MarkydWindow *self, gboolean clear_match,
gboolean clear_current) {
GtkTextIter iter;
GtkTextIter end;
if (!self || !self->editor || !self->editor->buffer) {
return;
}
gtk_text_buffer_get_bounds(self->editor->buffer, &iter, &end);
while (!gtk_text_iter_equal(&iter, &end)) {
GtkTextChildAnchor *anchor = gtk_text_iter_get_child_anchor(&iter);
if (anchor &&
g_object_get_data(G_OBJECT(anchor), SEEMD_TABLE_ANCHOR_DATA) != NULL) {
GtkWidget *table_widget =
g_object_get_data(G_OBJECT(anchor), SEEMD_TABLE_WIDGET_DATA);
if (table_widget && GTK_IS_CONTAINER(table_widget)) {
GList *wrapper_children =
gtk_container_get_children(GTK_CONTAINER(table_widget));
for (GList *w = wrapper_children; w != NULL; w = w->next) {
GtkWidget *child = GTK_WIDGET(w->data);
if (!GTK_IS_GRID(child)) {
continue;
}
GList *grid_children = gtk_container_get_children(GTK_CONTAINER(child));
for (GList *g = grid_children; g != NULL; g = g->next) {
GtkWidget *cell = GTK_WIDGET(g->data);
if (clear_match) {
gtk_style_context_remove_class(gtk_widget_get_style_context(cell),
SEEMD_TABLE_CELL_MATCH_CLASS);
}
if (clear_current) {
gtk_style_context_remove_class(gtk_widget_get_style_context(cell),
SEEMD_TABLE_CELL_CURRENT_CLASS);
}
}
g_list_free(grid_children);
}
g_list_free(wrapper_children);
}
}
gtk_text_iter_forward_char(&iter);
}
}
static gboolean resolve_table_match_location(MarkydWindow *self, gint start_offset,
gint end_offset,
GtkTextChildAnchor **out_anchor,
gint *out_row, gint *out_col) {
GtkTextIter iter;
GtkTextIter end;
if (out_anchor) {
*out_anchor = NULL;
}
if (out_row) {
*out_row = -1;
}
if (out_col) {
*out_col = -1;
}
if (!self || !self->editor || !self->editor->buffer ||
end_offset <= start_offset) {
return FALSE;
}
gtk_text_buffer_get_bounds(self->editor->buffer, &iter, &end);
while (!gtk_text_iter_equal(&iter, &end)) {
GtkTextChildAnchor *anchor = gtk_text_iter_get_child_anchor(&iter);
if (anchor) {
SeemdTableSearchIndex *index = g_object_get_data(
G_OBJECT(anchor), SEEMD_TABLE_SEARCH_INDEX_DATA);
if (index && start_offset < index->end_offset &&
end_offset > index->start_offset) {
if (out_anchor) {
*out_anchor = anchor;
}
if (index->cells) {
SeemdTableSearchCellRange *overlap_cell = NULL;
for (guint i = 0; i < index->cells->len; i++) {
SeemdTableSearchCellRange *cell =
&g_array_index(index->cells, SeemdTableSearchCellRange, i);
if (start_offset >= cell->start_offset && start_offset < cell->end_offset) {
if (out_row) {
*out_row = cell->row;
}
if (out_col) {
*out_col = cell->col;
}
return TRUE;
}
if (start_offset < cell->end_offset && end_offset > cell->start_offset) {
overlap_cell = cell;
}
}
if (overlap_cell) {
if (out_row) {
*out_row = overlap_cell->row;
}
if (out_col) {
*out_col = overlap_cell->col;
}
return TRUE;
}
}
return TRUE;
}
}
gtk_text_iter_forward_char(&iter);
}
return FALSE;
}
static void apply_table_search_match_highlight(MarkydWindow *self) {
if (!self || !self->search_matches) {
return;
}
clear_table_search_highlight(self, TRUE, FALSE);
for (guint i = 0; i < self->search_matches->len; i++) {
SearchMatch *match = &g_array_index(self->search_matches, SearchMatch, i);
GtkWidget *table_widget;
GtkWidget *cell;
if (!match->table_anchor || match->table_row < 0 || match->table_col < 0) {
continue;
}
table_widget =
g_object_get_data(G_OBJECT(match->table_anchor), SEEMD_TABLE_WIDGET_DATA);
cell = lookup_table_cell_widget(table_widget, match->table_row, match->table_col);
if (cell) {
set_table_cell_highlight(cell, TRUE, FALSE);
}
}
}
static void jump_to_search_match(MarkydWindow *self, gint index,
gboolean scroll_to_match) {
GtkTextIter start;
GtkTextIter end;
SearchMatch *match;
gchar *status;
if (!self || !self->editor || !self->editor->buffer || !self->search_matches ||
self->search_matches->len == 0) {
return;
}
if (index < 0 || index >= (gint)self->search_matches->len) {
return;
}
gtk_text_buffer_get_bounds(self->editor->buffer, &start, &end);
gtk_text_buffer_remove_tag_by_name(self->editor->buffer, TAG_SEARCH_CURRENT,
&start, &end);
clear_table_search_highlight(self, FALSE, TRUE);
match = &g_array_index(self->search_matches, SearchMatch, index);
if (match->table_anchor && match->table_row >= 0 && match->table_col >= 0) {
GtkWidget *table_widget =
g_object_get_data(G_OBJECT(match->table_anchor), SEEMD_TABLE_WIDGET_DATA);
GtkWidget *cell =
lookup_table_cell_widget(table_widget, match->table_row, match->table_col);
if (cell) {
set_table_cell_highlight(cell, TRUE, TRUE);
}
} else {
gtk_text_buffer_get_iter_at_offset(self->editor->buffer, &start,
match->start_offset);
gtk_text_buffer_get_iter_at_offset(self->editor->buffer, &end,
match->end_offset);
gtk_text_buffer_apply_tag_by_name(self->editor->buffer, TAG_SEARCH_CURRENT,
&start, &end);
gtk_text_buffer_place_cursor(self->editor->buffer, &start);
}
if (scroll_to_match && self->editor->text_view) {
if (match->table_anchor) {
gboolean scrolled_to_cell = FALSE;
if (match->table_row >= 0 && match->table_col >= 0) {
GtkWidget *table_widget = g_object_get_data(G_OBJECT(match->table_anchor),
SEEMD_TABLE_WIDGET_DATA);
GtkWidget *cell = lookup_table_cell_widget(table_widget, match->table_row,
match->table_col);
if (cell) {
scrolled_to_cell = scroll_to_table_cell(self, cell);
}
}
if (!scrolled_to_cell) {
GtkTextIter anchor_iter;
gtk_text_buffer_get_iter_at_child_anchor(self->editor->buffer, &anchor_iter,
match->table_anchor);
gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(self->editor->text_view),
&anchor_iter, 0.2, FALSE, 0.0, 0.0);
}
} else {
gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(self->editor->text_view), &start,
0.2, FALSE, 0.0, 0.0);
}
}
self->search_current_index = index;
status = g_strdup_printf("%d/%u", index + 1, self->search_matches->len);
gtk_label_set_text(GTK_LABEL(self->lbl_search_status), status);
g_free(status);
}
static void update_search_matches(MarkydWindow *self) {
const gchar *query;
GtkTextIter iter;
GtkTextIter match_start;
GtkTextIter match_end;
GtkTextIter end;
if (!self || !self->editor || !self->editor->buffer || !self->search_entry) {
return;
}
query = gtk_entry_get_text(GTK_ENTRY(self->search_entry));
clear_search_matches(self);
if (!query || query[0] == '\0') {
return;
}
if (!markyd_editor_get_edit_mode(self->editor)) {
markyd_editor_find(self->editor, query);
gtk_widget_set_sensitive(self->btn_search_prev, TRUE);
gtk_widget_set_sensitive(self->btn_search_next, TRUE);
gtk_label_set_text(GTK_LABEL(self->lbl_search_status), "Preview search");
return;
}
ensure_search_tags(self);
gtk_text_buffer_get_start_iter(self->editor->buffer, &iter);
gtk_text_buffer_get_end_iter(self->editor->buffer, &end);
while (gtk_text_iter_forward_search(&iter, query,
GTK_TEXT_SEARCH_CASE_INSENSITIVE |
GTK_TEXT_SEARCH_TEXT_ONLY,
&match_start, &match_end, &end)) {
SearchMatch match = {gtk_text_iter_get_offset(&match_start),
gtk_text_iter_get_offset(&match_end), NULL, -1, -1};
gtk_text_buffer_apply_tag_by_name(self->editor->buffer, TAG_SEARCH_MATCH,
&match_start, &match_end);
resolve_table_match_location(self, match.start_offset, match.end_offset,
&match.table_anchor, &match.table_row,
&match.table_col);
g_array_append_val(self->search_matches, match);
iter = match_end;
}
if (self->search_matches->len == 0) {
gtk_label_set_text(GTK_LABEL(self->lbl_search_status), "0 matches");
return;
}
gtk_widget_set_sensitive(self->btn_search_prev, TRUE);
gtk_widget_set_sensitive(self->btn_search_next, TRUE);
apply_table_search_match_highlight(self);
jump_to_search_match(self, 0, TRUE);
}
static void show_search_ui(MarkydWindow *self) {
if (!self || !self->search_revealer || !self->search_entry) {
return;
}
gtk_revealer_set_reveal_child(GTK_REVEALER(self->search_revealer), TRUE);
gtk_widget_grab_focus(self->search_entry);
gtk_editable_select_region(GTK_EDITABLE(self->search_entry), 0, -1);
if (gtk_entry_get_text_length(GTK_ENTRY(self->search_entry)) > 0) {
update_search_matches(self);
}
}
static void hide_search_ui(MarkydWindow *self) {
if (!self || !self->search_revealer || !self->search_entry) {
return;
}
gtk_revealer_set_reveal_child(GTK_REVEALER(self->search_revealer), FALSE);
gtk_entry_set_text(GTK_ENTRY(self->search_entry), "");
clear_search_matches(self);
markyd_editor_focus(self->editor);
}
static void on_search_changed(GtkEditable *editable, gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data;
(void)editable;
update_search_matches(self);
}
static void on_search_prev_clicked(GtkButton *button, gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data;
gint index;
(void)button;
if (!self || !self->search_matches || self->search_matches->len == 0) {
if (self && self->editor && !markyd_editor_get_edit_mode(self->editor) &&
self->search_entry &&
gtk_entry_get_text_length(GTK_ENTRY(self->search_entry)) > 0) {
markyd_editor_find_next(self->editor, TRUE);
}
return;
}
index = self->search_current_index - 1;
if (index < 0) {
index = (gint)self->search_matches->len - 1;
}
jump_to_search_match(self, index, TRUE);
}
static void on_search_next_clicked(GtkButton *button, gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data;
gint index;
(void)button;
if (!self || !self->search_matches || self->search_matches->len == 0) {
if (self && self->editor && !markyd_editor_get_edit_mode(self->editor) &&
self->search_entry &&
gtk_entry_get_text_length(GTK_ENTRY(self->search_entry)) > 0) {
markyd_editor_find_next(self->editor, FALSE);
}
return;
}
index = self->search_current_index + 1;
if (index >= (gint)self->search_matches->len) {
index = 0;
}
jump_to_search_match(self, index, TRUE);
}
static gboolean on_search_entry_key_press(GtkWidget *widget, GdkEventKey *event,
gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data;
gboolean shift_pressed;
(void)widget;
if (!event) {
return FALSE;
}
shift_pressed = (event->state & GDK_SHIFT_MASK) != 0;
if (event->keyval == GDK_KEY_Return || event->keyval == GDK_KEY_KP_Enter) {
if (shift_pressed) {
on_search_prev_clicked(NULL, self);
} else {
on_search_next_clicked(NULL, self);
}
return TRUE;
}
if (event->keyval == GDK_KEY_Escape) {
hide_search_ui(self);
return TRUE;
}
return FALSE;
}
static void on_editor_buffer_changed(GtkTextBuffer *buffer, gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data;
(void)buffer;
if (!self || !self->search_revealer || !self->search_entry) {
return;
}
if (!gtk_revealer_get_reveal_child(GTK_REVEALER(self->search_revealer))) {
return;
}
if (gtk_entry_get_text_length(GTK_ENTRY(self->search_entry)) == 0) {
clear_search_matches(self);
return;
}
update_search_matches(self);
}
static GtkWidget *create_toolbar_button_internal(const gchar *icon_name,
const gchar *label,
const gchar *tooltip,
gboolean toggle) {
GtkWidget *button;
GtkWidget *box;
GtkWidget *image;
GtkWidget *label_widget;
button = toggle ? gtk_toggle_button_new() : gtk_button_new();
box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
image = gtk_image_new_from_icon_name(icon_name, GTK_ICON_SIZE_BUTTON);
label_widget = gtk_label_new(label);
gtk_widget_set_halign(box, GTK_ALIGN_CENTER);
gtk_widget_set_valign(box, GTK_ALIGN_CENTER);
gtk_box_pack_start(GTK_BOX(box), image, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(box), label_widget, FALSE, FALSE, 0);
gtk_container_add(GTK_CONTAINER(button), box);
gtk_widget_set_focus_on_click(button, FALSE);
if (tooltip) {
gtk_widget_set_tooltip_text(button, tooltip);
}
g_object_set_data(G_OBJECT(button), TOOLBAR_LABEL_DATA, label_widget);
return button;
}
static GtkWidget *create_toolbar_button(const gchar *icon_name, const gchar *label,
const gchar *tooltip) {
return create_toolbar_button_internal(icon_name, label, tooltip, FALSE);
}
static void set_toolbar_button_label(GtkWidget *button, const gchar *label) {
GtkWidget *label_widget;
if (!button || !label) {
return;
}
label_widget = g_object_get_data(G_OBJECT(button), TOOLBAR_LABEL_DATA);
if (GTK_IS_LABEL(label_widget)) {
gtk_label_set_text(GTK_LABEL(label_widget), label);
}
}
static void restore_editor_focus(MarkydWindow *self) {
if (self && self->editor) {
markyd_editor_focus(self->editor);
}
}
static void on_about_popover_closed(GtkPopover *popover, gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data;
restore_editor_focus(self);
gtk_widget_destroy(GTK_WIDGET(popover));
}
static void on_about_clicked(GtkButton *button, gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data;
GtkWidget *popover;
GtkWidget *box;
GtkWidget *title;
GtkWidget *summary;
GtkWidget *details;
popover = gtk_popover_new(GTK_WIDGET(button));
gtk_popover_set_position(GTK_POPOVER(popover), GTK_POS_BOTTOM);
gtk_container_set_border_width(GTK_CONTAINER(popover), 12);
g_signal_connect(popover, "closed", G_CALLBACK(on_about_popover_closed), self);
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
gtk_widget_set_size_request(box, 300, -1);
gtk_container_add(GTK_CONTAINER(popover), box);
title = gtk_label_new(NULL);
gtk_label_set_markup(GTK_LABEL(title), "<b>SeeMD</b>");
gtk_label_set_xalign(GTK_LABEL(title), 0.0f);
gtk_box_pack_start(GTK_BOX(box), title, FALSE, FALSE, 0);
summary = gtk_label_new("A lightweight GTK markdown viewer and editor.");
gtk_label_set_xalign(GTK_LABEL(summary), 0.0f);
gtk_label_set_line_wrap(GTK_LABEL(summary), TRUE);
gtk_box_pack_start(GTK_BOX(box), summary, FALSE, FALSE, 0);
details = gtk_label_new(
"This is a fork of SeeMD by NotAlexNoyle, with additional features, "
"including GitHub-style preview rendering, HTML-in-Markdown support, "
"editing, saving, toolbar labels, and improved image sizing.");
gtk_label_set_xalign(GTK_LABEL(details), 0.0f);
gtk_label_set_line_wrap(GTK_LABEL(details), TRUE);
gtk_label_set_max_width_chars(GTK_LABEL(details), 42);
gtk_box_pack_start(GTK_BOX(box), details, FALSE, FALSE, 0);
gtk_widget_show_all(popover);
gtk_popover_popup(GTK_POPOVER(popover));
}
MarkydWindow *markyd_window_new(MarkydApp *app) {
MarkydWindow *self = g_new0(MarkydWindow, 1);
GtkWidget *toolbar_buttons;
GtkWidget *main_box;
GtkWidget *search_box;
self->app = app;
self->window = gtk_application_window_new(app->gtk_app);
gtk_window_set_title(GTK_WINDOW(self->window), "SeeMD");
gtk_window_set_default_size(GTK_WINDOW(self->window), config->window_width,
config->window_height);
if (config->window_x >= 0 && config->window_y >= 0) {
gtk_window_move(GTK_WINDOW(self->window), config->window_x,
config->window_y);
}
if (geometry_debug_enabled()) {
g_printerr("SeeMD geometry init: x=%d y=%d w=%d h=%d maximized=%d\n",
config->window_x, config->window_y, config->window_width,
config->window_height, config->window_maximized);
}
if (config->window_maximized) {
gtk_window_maximize(GTK_WINDOW(self->window));
}
g_signal_connect(self->window, "key-press-event",
G_CALLBACK(on_key_press_event), self);
g_signal_connect(self->window, "configure-event",
G_CALLBACK(on_configure_event), self);
g_signal_connect(self->window, "window-state-event",
G_CALLBACK(on_window_state_event), self);
self->header_bar = gtk_header_bar_new();
gtk_header_bar_set_show_close_button(GTK_HEADER_BAR(self->header_bar), TRUE);
gtk_window_set_titlebar(GTK_WINDOW(self->window), self->header_bar);
self->lbl_title = gtk_button_new_with_label("SeeMD");
gtk_button_set_relief(GTK_BUTTON(self->lbl_title), GTK_RELIEF_NONE);
gtk_widget_set_focus_on_click(self->lbl_title, FALSE);
gtk_widget_set_tooltip_text(self->lbl_title, "About SeeMD");
gtk_widget_set_halign(self->lbl_title, GTK_ALIGN_END);
gtk_widget_set_margin_start(self->lbl_title, 12);
gtk_widget_set_margin_end(self->lbl_title, 12);
g_signal_connect(self->lbl_title, "clicked", G_CALLBACK(on_about_clicked),
self);
self->btn_open =
create_toolbar_button("document-open-symbolic", "Open",
"Open Markdown Document");
g_signal_connect(self->btn_open, "clicked", G_CALLBACK(on_open_clicked), self);
self->btn_refresh =
create_toolbar_button("view-refresh-symbolic", "Reload",
"Reload Current Document");
g_signal_connect(self->btn_refresh, "clicked", G_CALLBACK(on_refresh_clicked),
self);
self->btn_edit =
create_toolbar_button("accessories-text-editor-symbolic", "Edit",
"Edit Markdown Source");
g_signal_connect(self->btn_edit, "clicked", G_CALLBACK(on_edit_clicked), self);
self->btn_save =
create_toolbar_button("document-save-symbolic", "Save",
"Save Markdown Document");
g_signal_connect(self->btn_save, "clicked", G_CALLBACK(on_save_clicked), self);
self->btn_settings =
create_toolbar_button("emblem-system-symbolic", "Settings", "Settings");
g_signal_connect(self->btn_settings, "clicked", G_CALLBACK(on_settings_clicked),
self);
toolbar_buttons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_set_halign(toolbar_buttons, GTK_ALIGN_CENTER);
gtk_box_pack_start(GTK_BOX(toolbar_buttons), self->btn_open, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(toolbar_buttons), self->btn_refresh, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(toolbar_buttons), self->btn_edit, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(toolbar_buttons), self->btn_save, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(toolbar_buttons), self->btn_settings, FALSE, FALSE, 0);
gtk_header_bar_set_custom_title(GTK_HEADER_BAR(self->header_bar),
toolbar_buttons);
gtk_header_bar_pack_end(GTK_HEADER_BAR(self->header_bar), self->lbl_title);
main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_container_add(GTK_CONTAINER(self->window), main_box);
self->search_revealer = gtk_revealer_new();
gtk_revealer_set_transition_type(GTK_REVEALER(self->search_revealer),
GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN);
gtk_revealer_set_reveal_child(GTK_REVEALER(self->search_revealer), FALSE);
gtk_box_pack_start(GTK_BOX(main_box), self->search_revealer, FALSE, FALSE, 0);
search_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
gtk_widget_set_margin_top(search_box, 6);
gtk_widget_set_margin_bottom(search_box, 6);
gtk_widget_set_margin_start(search_box, 8);
gtk_widget_set_margin_end(search_box, 8);
gtk_container_add(GTK_CONTAINER(self->search_revealer), search_box);
self->search_entry = gtk_search_entry_new();
gtk_widget_set_hexpand(self->search_entry, TRUE);
gtk_widget_set_tooltip_text(self->search_entry, "Find in document");
g_signal_connect(self->search_entry, "changed", G_CALLBACK(on_search_changed),
self);
g_signal_connect(self->search_entry, "key-press-event",
G_CALLBACK(on_search_entry_key_press), self);
gtk_box_pack_start(GTK_BOX(search_box), self->search_entry, TRUE, TRUE, 0);
self->btn_search_prev =
gtk_button_new_from_icon_name("go-up-symbolic", GTK_ICON_SIZE_BUTTON);
gtk_widget_set_tooltip_text(self->btn_search_prev, "Previous Match");
g_signal_connect(self->btn_search_prev, "clicked",
G_CALLBACK(on_search_prev_clicked), self);
gtk_widget_set_sensitive(self->btn_search_prev, FALSE);
gtk_box_pack_start(GTK_BOX(search_box), self->btn_search_prev, FALSE, FALSE, 0);
self->btn_search_next =
gtk_button_new_from_icon_name("go-down-symbolic", GTK_ICON_SIZE_BUTTON);
gtk_widget_set_tooltip_text(self->btn_search_next, "Next Match");
g_signal_connect(self->btn_search_next, "clicked",
G_CALLBACK(on_search_next_clicked), self);
gtk_widget_set_sensitive(self->btn_search_next, FALSE);
gtk_box_pack_start(GTK_BOX(search_box), self->btn_search_next, FALSE, FALSE, 0);
self->lbl_search_status = gtk_label_new("");
gtk_widget_set_halign(self->lbl_search_status, GTK_ALIGN_END);
gtk_box_pack_start(GTK_BOX(search_box), self->lbl_search_status, FALSE, FALSE, 0);
self->editor = markyd_editor_new(app);
self->scroll = self->editor->source_scroll;
gtk_box_pack_start(GTK_BOX(main_box), markyd_editor_get_widget(self->editor),
TRUE, TRUE, 0);
self->search_matches = g_array_new(FALSE, FALSE, sizeof(SearchMatch));
self->search_current_index = -1;
g_signal_connect(self->editor->buffer, "changed",
G_CALLBACK(on_editor_buffer_changed), self);
ensure_search_tags(self);
markyd_window_apply_css(self);
gtk_widget_show_all(self->window);
return self;
}
void markyd_window_apply_css(MarkydWindow *self) {
static GtkCssProvider *css = NULL;
gchar *css_str;
const gchar *bg;
const gchar *fg;
const gchar *sel_bg;
const gchar *table_bg;
const gchar *table_fg;
const gchar *table_header_bg;
const gchar *table_border;
const gchar *search_match_bg;
const gchar *search_match_fg;
const gchar *search_current_bg;
const gchar *search_current_fg;
if (css) {
gtk_style_context_remove_provider_for_screen(gdk_screen_get_default(),
GTK_STYLE_PROVIDER(css));
g_object_unref(css);
}
css = gtk_css_provider_new();
if (g_strcmp0(config->theme, "light") == 0) {
bg = "#ffffff";
fg = "#111111";
sel_bg = "#cfe3ff";
search_match_bg = "#fff3b0";
search_match_fg = "#111111";
search_current_bg = "#ffd166";
search_current_fg = "#111111";
} else if (g_strcmp0(config->theme, "dark") == 0) {
bg = "#1e1e1e";
fg = "#e8e8e8";
sel_bg = "#264f78";
search_match_bg = "#3e3a12";
search_match_fg = "#f4f4e8";
search_current_bg = "#66551f";
search_current_fg = "#f4f4e8";
} else {
bg = "@theme_base_color";
fg = "@theme_text_color";
sel_bg = "@theme_selected_bg_color";
search_match_bg = "@theme_selected_bg_color";
search_match_fg = "@theme_selected_fg_color";
search_current_bg = "@theme_selected_bg_color";
search_current_fg = "@theme_selected_fg_color";
}
table_bg = bg;
table_fg = fg;
table_header_bg = table_bg;
table_border = sel_bg;
css_str = g_strdup_printf(
"textview {"
" font-family: '%s', 'Inter', 'Noto Sans', sans-serif;"
" font-size: %dpt;"
" padding: 0px;"
" background-color: %s;"
" color: %s;"
" caret-color: %s;"
"}"
"textview text {"
" background-color: %s;"
" color: %s;"
" caret-color: %s;"
"}"
"textview text selection {"
" background-color: %s;"
"}"
"scrolledwindow {"
" background-color: %s;"
" border: none;"
"}"
"window {"
" background-color: %s;"
"}"
".seemd-table-cell {"
" background-color: %s;"
" border-style: solid;"
" border-width: 1px;"
" border-color: %s;"
"}"
".seemd-table-header-cell {"
" background-color: %s;"
"}"
".seemd-table-cell label {"
" color: %s;"
"}"
".seemd-table-cell." SEEMD_TABLE_CELL_MATCH_CLASS " {"
" background-color: %s;"
"}"
".seemd-table-cell." SEEMD_TABLE_CELL_MATCH_CLASS " label {"
" color: %s;"
"}"
".seemd-table-cell." SEEMD_TABLE_CELL_CURRENT_CLASS " {"
" background-color: %s;"
"}"
".seemd-table-cell." SEEMD_TABLE_CELL_CURRENT_CLASS " label {"
" color: %s;"
"}",
config->font_family, config->font_size, bg, fg, fg, bg, fg, fg, sel_bg, bg,
bg, table_bg, table_border, table_header_bg, table_fg, search_match_bg,
search_match_fg, search_current_bg, search_current_fg);
gtk_css_provider_load_from_data(css, css_str, -1, NULL);
g_free(css_str);
gtk_style_context_add_provider_for_screen(
gdk_screen_get_default(), GTK_STYLE_PROVIDER(css),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
update_search_tag_styles(self, search_match_bg, search_match_fg,
search_current_bg, search_current_fg);
}
void markyd_window_free(MarkydWindow *self) {
if (!self)
return;
if (self->search_matches) {
g_array_free(self->search_matches, TRUE);
self->search_matches = NULL;
}
if (self->editor) {
markyd_editor_free(self->editor);
}
if (self->window) {
gtk_widget_destroy(self->window);
}
g_free(self);
}
void markyd_window_show(MarkydWindow *self) {
gtk_widget_show(self->window);
if (!config->window_maximized && config->window_x >= 0 && config->window_y >= 0) {
gtk_window_move(GTK_WINDOW(self->window), config->window_x, config->window_y);
}
gtk_window_present(GTK_WINDOW(self->window));
}
void markyd_window_hide(MarkydWindow *self) { gtk_widget_hide(self->window); }
void markyd_window_toggle(MarkydWindow *self) {
if (markyd_window_is_visible(self)) {
markyd_window_hide(self);
} else {
markyd_window_show(self);
}
}
gboolean markyd_window_is_visible(MarkydWindow *self) {
return gtk_widget_get_visible(self->window);
}
static void update_edit_button_state(MarkydWindow *self) {
gboolean editing;
if (!self || !self->editor || !self->btn_edit) {
return;
}
editing = markyd_editor_get_edit_mode(self->editor);
set_toolbar_button_label(self->btn_edit, editing ? "Preview" : "Edit");
gtk_widget_set_tooltip_text(self->btn_edit,
editing ? "Preview Markdown" : "Edit Markdown Source");
}
static void set_edit_mode(MarkydWindow *self, gboolean edit_mode) {
if (!self || !self->editor) {
return;
}
markyd_editor_set_edit_mode(self->editor, edit_mode);
update_edit_button_state(self);
markyd_editor_focus(self->editor);
if (self->search_revealer &&
gtk_revealer_get_reveal_child(GTK_REVEALER(self->search_revealer)) &&
self->search_entry &&
gtk_entry_get_text_length(GTK_ENTRY(self->search_entry)) > 0) {
update_search_matches(self);
}
}
static gboolean prompt_save_as(MarkydWindow *self) {
GtkWidget *dialog;
GtkFileFilter *md_filter;
gint response;
gboolean saved = FALSE;
if (!self || !self->window) {
return FALSE;
}
dialog = gtk_file_chooser_dialog_new(
"Save Markdown Document", GTK_WINDOW(self->window),
GTK_FILE_CHOOSER_ACTION_SAVE, "_Cancel", GTK_RESPONSE_CANCEL, "_Save",
GTK_RESPONSE_ACCEPT, NULL);
gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog), TRUE);
md_filter = gtk_file_filter_new();
gtk_file_filter_set_name(md_filter, "Markdown files (*.md, *.markdown)");
gtk_file_filter_add_pattern(md_filter, "*.md");
gtk_file_filter_add_pattern(md_filter, "*.markdown");
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), md_filter);
md_filter = gtk_file_filter_new();
gtk_file_filter_set_name(md_filter, "All files");
gtk_file_filter_add_pattern(md_filter, "*");
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), md_filter);
{
const gchar *current = markyd_app_get_current_path(self->app);
if (current && current[0] != '\0') {
gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dialog), current);
} else {
gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), "untitled.md");
}
}
response = gtk_dialog_run(GTK_DIALOG(dialog));
if (response == GTK_RESPONSE_ACCEPT) {
gchar *path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
if (path) {
saved = markyd_app_save_file(self->app, path);
if (!saved) {
GtkWidget *error_dialog = gtk_message_dialog_new(
GTK_WINDOW(self->window),
GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR,
GTK_BUTTONS_CLOSE, "Failed to save document");
gtk_message_dialog_format_secondary_text(
GTK_MESSAGE_DIALOG(error_dialog), "%s", path);
gtk_dialog_run(GTK_DIALOG(error_dialog));
gtk_widget_destroy(error_dialog);
}
g_free(path);
}
}
gtk_widget_destroy(dialog);
return saved;
}
static void on_open_clicked(GtkButton *button, gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data;
GtkWidget *dialog;
GtkFileFilter *md_filter;
gint response;
(void)button;
dialog = gtk_file_chooser_dialog_new(
"Open Markdown Document", GTK_WINDOW(self->window),
GTK_FILE_CHOOSER_ACTION_OPEN, "_Cancel", GTK_RESPONSE_CANCEL, "_Open",
GTK_RESPONSE_ACCEPT, NULL);
md_filter = gtk_file_filter_new();
gtk_file_filter_set_name(md_filter, "Markdown files (*.md, *.markdown)");
gtk_file_filter_add_pattern(md_filter, "*.md");
gtk_file_filter_add_pattern(md_filter, "*.markdown");
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), md_filter);
md_filter = gtk_file_filter_new();
gtk_file_filter_set_name(md_filter, "All files");
gtk_file_filter_add_pattern(md_filter, "*");
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), md_filter);
response = gtk_dialog_run(GTK_DIALOG(dialog));
if (response == GTK_RESPONSE_ACCEPT) {
gchar *path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
if (path) {
if (!markyd_app_open_file(self->app, path)) {
GtkWidget *error_dialog = gtk_message_dialog_new(
GTK_WINDOW(self->window),
GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR,
GTK_BUTTONS_CLOSE, "Failed to open document");
gtk_message_dialog_format_secondary_text(
GTK_MESSAGE_DIALOG(error_dialog), "%s", path);
gtk_dialog_run(GTK_DIALOG(error_dialog));
gtk_widget_destroy(error_dialog);
}
g_free(path);
}
}
gtk_widget_destroy(dialog);
restore_editor_focus(self);
}
static void on_refresh_clicked(GtkButton *button, gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data;
const gchar *current;
gchar *path;
(void)button;
current = markyd_app_get_current_path(self->app);
if (!current || current[0] == '\0') {
restore_editor_focus(self);
return;
}
path = g_strdup(current);
if (!markyd_app_open_file(self->app, path)) {
GtkWidget *error_dialog = gtk_message_dialog_new(
GTK_WINDOW(self->window),
GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR,
GTK_BUTTONS_CLOSE, "Failed to reload document");
gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(error_dialog),
"%s", path);
gtk_dialog_run(GTK_DIALOG(error_dialog));
gtk_widget_destroy(error_dialog);
}
g_free(path);
restore_editor_focus(self);
}
static void on_edit_clicked(GtkButton *button, gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data;
(void)button;
if (!self || !self->editor) {
return;
}
set_edit_mode(self, !markyd_editor_get_edit_mode(self->editor));
}
static void on_save_clicked(GtkButton *button, gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data;
const gchar *current;
(void)button;
if (!self || !self->app) {
return;
}
current = markyd_app_get_current_path(self->app);
if (!current || current[0] == '\0') {
prompt_save_as(self);
restore_editor_focus(self);
return;
}
if (!markyd_app_save_current_file(self->app)) {
GtkWidget *error_dialog = gtk_message_dialog_new(
GTK_WINDOW(self->window), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "Failed to save document");
gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(error_dialog),
"%s", current);
gtk_dialog_run(GTK_DIALOG(error_dialog));
gtk_widget_destroy(error_dialog);
}
restore_editor_focus(self);
}
static void on_settings_clicked(GtkButton *button, gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data;
GtkWidget *dialog;
gint response;
(void)button;
dialog = create_settings_dialog(self->app);
response = gtk_dialog_run(GTK_DIALOG(dialog));
if (response == GTK_RESPONSE_APPLY || response == GTK_RESPONSE_OK) {
config_save(config);
markyd_window_apply_css(self);
markdown_update_accent_tags(self->editor->buffer);
markyd_editor_refresh(self->editor);
}
gtk_widget_destroy(dialog);
restore_editor_focus(self);
}
static gboolean on_key_press_event(GtkWidget *widget, GdkEventKey *event,
gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data;
if (!event || !self) {
return FALSE;
}
if ((event->state & GDK_CONTROL_MASK) != 0 &&
(event->keyval == GDK_KEY_f || event->keyval == GDK_KEY_F)) {
show_search_ui(self);
return TRUE;
}
if ((event->state & GDK_CONTROL_MASK) != 0 &&
(event->keyval == GDK_KEY_e || event->keyval == GDK_KEY_E)) {
set_edit_mode(self, !markyd_editor_get_edit_mode(self->editor));
return TRUE;
}
if ((event->state & GDK_CONTROL_MASK) != 0 &&
(event->keyval == GDK_KEY_s || event->keyval == GDK_KEY_S)) {
on_save_clicked(NULL, self);
return TRUE;
}
if (event->keyval == GDK_KEY_Escape &&
self->search_revealer &&
gtk_revealer_get_reveal_child(GTK_REVEALER(self->search_revealer))) {
hide_search_ui(self);
return TRUE;
}
if (event->keyval == GDK_KEY_Escape && self->editor &&
markyd_editor_get_edit_mode(self->editor)) {
set_edit_mode(self, FALSE);
return TRUE;
}
if (event->keyval == GDK_KEY_Escape) {
gtk_window_close(GTK_WINDOW(widget));
return TRUE;
}
if (event->keyval == GDK_KEY_Home || event->keyval == GDK_KEY_KP_Home) {
if (self->editor && !markyd_editor_get_edit_mode(self->editor)) {
markyd_editor_scroll_top(self->editor);
return TRUE;
}
if (self->editor && self->editor->buffer && self->editor->text_view) {
GtkTextIter start;
GtkAdjustment *hadj = NULL;
GtkAdjustment *vadj = NULL;
gtk_text_buffer_get_start_iter(self->editor->buffer, &start);
gtk_text_buffer_place_cursor(self->editor->buffer, &start);
gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(self->editor->text_view), &start,
0.0, FALSE, 0.0, 0.0);
if (self->scroll) {
hadj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(self->scroll));
vadj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(self->scroll));
}
if (hadj) {
gtk_adjustment_set_value(hadj, gtk_adjustment_get_lower(hadj));
}
if (vadj) {
gtk_adjustment_set_value(vadj, gtk_adjustment_get_lower(vadj));
}
}
return TRUE;
}
return FALSE;
}
static gboolean on_configure_event(GtkWidget *widget, GdkEventConfigure *event,
gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data;
(void)self;
GdkWindow *gdk_window = gtk_widget_get_window(widget);
if (gdk_window) {
GdkWindowState state = gdk_window_get_state(gdk_window);
if (state & GDK_WINDOW_STATE_MAXIMIZED) {
return FALSE;
}
} else if (config->window_maximized) {
return FALSE;
}
{
gint x, y;
gint width, height;
gint gtk_w = 0, gtk_h = 0;
GdkRectangle frame_extents;
gboolean have_extents = FALSE;
gtk_window_get_position(GTK_WINDOW(widget), &x, &y);
config->window_x = x;
config->window_y = y;
gtk_window_get_size(GTK_WINDOW(widget), &gtk_w, &gtk_h);
width = gtk_w > 0 ? gtk_w : event->width;
height = gtk_h > 0 ? gtk_h : event->height;
if (gdk_window) {
gdk_window_get_frame_extents(gdk_window, &frame_extents);
have_extents = TRUE;
}
config->window_width = width;
config->window_height = height;
if (geometry_debug_enabled()) {
if (have_extents) {
g_printerr(
"SeeMD configure: event=%dx%d gtk=%dx%d saved=%dx%d frame=%dx%d at (%d,%d)\n",
event->width, event->height, gtk_w, gtk_h, width, height,
frame_extents.width, frame_extents.height, x, y);
} else {
g_printerr("SeeMD configure: event=%dx%d gtk=%dx%d saved=%dx%d at (%d,%d)\n",
event->width, event->height, gtk_w, gtk_h, width, height, x,
y);
}
g_printerr("SeeMD saved: x=%d y=%d w=%d h=%d\n", config->window_x,
config->window_y, config->window_width, config->window_height);
}
}
return FALSE;
}
static gboolean on_window_state_event(GtkWidget *widget,
GdkEventWindowState *event,
gpointer user_data) {
(void)widget;
(void)user_data;
config->window_maximized =
(event->new_window_state & GDK_WINDOW_STATE_MAXIMIZED) != 0;
return FALSE;
}
static void on_font_size_changed(GtkSpinButton *spin, gpointer user_data) {
(void)user_data;
config->font_size = gtk_spin_button_get_value_as_int(spin);
}
static void on_font_family_changed(GtkComboBoxText *combo, gpointer user_data) {
(void)user_data;
g_free(config->font_family);
config->font_family = gtk_combo_box_text_get_active_text(combo);
}
static void on_theme_changed(GtkComboBoxText *combo, gpointer user_data) {
(void)user_data;
g_free(config->theme);
config->theme = gtk_combo_box_text_get_active_text(combo);
}
static gchar *rgba_to_hex(const GdkRGBA *rgba) {
guint r = (guint)(CLAMP(rgba->red, 0.0, 1.0) * 255.0 + 0.5);
guint g = (guint)(CLAMP(rgba->green, 0.0, 1.0) * 255.0 + 0.5);
guint b = (guint)(CLAMP(rgba->blue, 0.0, 1.0) * 255.0 + 0.5);
return g_strdup_printf("#%02X%02X%02X", r, g, b);
}
static void init_color_button(GtkColorButton *btn, const gchar *color_str) {
GdkRGBA rgba;
if (color_str && gdk_rgba_parse(&rgba, color_str)) {
gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(btn), &rgba);
}
}
static void on_color_set(GtkColorButton *btn, gpointer user_data) {
gchar **target = (gchar **)user_data;
GdkRGBA rgba;
gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(btn), &rgba);
g_free(*target);
*target = rgba_to_hex(&rgba);
}
static GtkWidget *create_settings_dialog(MarkydApp *app) {
GtkWidget *dialog;
GtkWidget *content;
GtkWidget *grid;
GtkWidget *label;
GtkWidget *font_family_combo;
GtkWidget *font_size_spin;
GtkWidget *theme_combo;
GtkWidget *h1_color_btn;
GtkWidget *h2_color_btn;
GtkWidget *h3_color_btn;
GtkWidget *bullet_color_btn;
gint row = 0;
dialog = gtk_dialog_new_with_buttons(
"SeeMD Settings", GTK_WINDOW(app->window->window),
GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, "_Cancel",
GTK_RESPONSE_CANCEL, "_Apply", GTK_RESPONSE_APPLY, NULL);
gtk_window_set_default_size(GTK_WINDOW(dialog), 400, -1);
content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
gtk_container_set_border_width(GTK_CONTAINER(content), 16);
gtk_widget_set_margin_bottom(content, 12);
grid = gtk_grid_new();
gtk_grid_set_row_spacing(GTK_GRID(grid), 12);
gtk_grid_set_column_spacing(GTK_GRID(grid), 12);
gtk_container_add(GTK_CONTAINER(content), grid);
label = gtk_label_new("Theme:");
gtk_widget_set_halign(label, GTK_ALIGN_END);
gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1);
theme_combo = gtk_combo_box_text_new();
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(theme_combo), "dark");
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(theme_combo), "light");
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(theme_combo), "system");
if (g_strcmp0(config->theme, "light") == 0)
gtk_combo_box_set_active(GTK_COMBO_BOX(theme_combo), 1);
else if (g_strcmp0(config->theme, "system") == 0)
gtk_combo_box_set_active(GTK_COMBO_BOX(theme_combo), 2);
else
gtk_combo_box_set_active(GTK_COMBO_BOX(theme_combo), 0);
g_signal_connect(theme_combo, "changed", G_CALLBACK(on_theme_changed), NULL);
gtk_widget_set_hexpand(theme_combo, TRUE);
gtk_grid_attach(GTK_GRID(grid), theme_combo, 1, row++, 1, 1);
label = gtk_label_new("Font:");
gtk_widget_set_halign(label, GTK_ALIGN_END);
gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1);
font_family_combo = gtk_combo_box_text_new();
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(font_family_combo),
"Cantarell");
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(font_family_combo),
"Inter");
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(font_family_combo),
"Noto Sans");
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(font_family_combo),
"Ubuntu");
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(font_family_combo),
"Roboto");
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(font_family_combo),
"Monospace");
{
gint font_idx = 0;
const gchar *fonts[] = {"Cantarell", "Inter", "Noto Sans", "Ubuntu",
"Roboto", "Monospace", NULL};
for (gint i = 0; fonts[i]; i++) {
if (g_strcmp0(config->font_family, fonts[i]) == 0) {
font_idx = i;
break;
}
}
gtk_combo_box_set_active(GTK_COMBO_BOX(font_family_combo), font_idx);
}
g_signal_connect(font_family_combo, "changed",
G_CALLBACK(on_font_family_changed), NULL);
gtk_widget_set_hexpand(font_family_combo, TRUE);
gtk_grid_attach(GTK_GRID(grid), font_family_combo, 1, row++, 1, 1);
label = gtk_label_new("Font Size:");
gtk_widget_set_halign(label, GTK_ALIGN_END);
gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1);
font_size_spin = gtk_spin_button_new_with_range(8, 48, 1);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(font_size_spin), config->font_size);
g_signal_connect(font_size_spin, "value-changed",
G_CALLBACK(on_font_size_changed), NULL);
gtk_grid_attach(GTK_GRID(grid), font_size_spin, 1, row++, 1, 1);
{
GtkWidget *separator = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL);
gtk_grid_attach(GTK_GRID(grid), separator, 0, row++, 2, 1);
}
label = gtk_label_new("Heading 1:");
gtk_widget_set_halign(label, GTK_ALIGN_END);
gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1);
h1_color_btn = gtk_color_button_new();
init_color_button(GTK_COLOR_BUTTON(h1_color_btn), config->h1_color);
gtk_widget_set_halign(h1_color_btn, GTK_ALIGN_START);
g_signal_connect(h1_color_btn, "color-set", G_CALLBACK(on_color_set),
&config->h1_color);
gtk_grid_attach(GTK_GRID(grid), h1_color_btn, 1, row++, 1, 1);
label = gtk_label_new("Heading 2:");
gtk_widget_set_halign(label, GTK_ALIGN_END);
gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1);
h2_color_btn = gtk_color_button_new();
init_color_button(GTK_COLOR_BUTTON(h2_color_btn), config->h2_color);
gtk_widget_set_halign(h2_color_btn, GTK_ALIGN_START);
g_signal_connect(h2_color_btn, "color-set", G_CALLBACK(on_color_set),
&config->h2_color);
gtk_grid_attach(GTK_GRID(grid), h2_color_btn, 1, row++, 1, 1);
label = gtk_label_new("Heading 3:");
gtk_widget_set_halign(label, GTK_ALIGN_END);
gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1);
h3_color_btn = gtk_color_button_new();
init_color_button(GTK_COLOR_BUTTON(h3_color_btn), config->h3_color);
gtk_widget_set_halign(h3_color_btn, GTK_ALIGN_START);
g_signal_connect(h3_color_btn, "color-set", G_CALLBACK(on_color_set),
&config->h3_color);
gtk_grid_attach(GTK_GRID(grid), h3_color_btn, 1, row++, 1, 1);
label = gtk_label_new("List bullet:");
gtk_widget_set_halign(label, GTK_ALIGN_END);
gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1);
bullet_color_btn = gtk_color_button_new();
init_color_button(GTK_COLOR_BUTTON(bullet_color_btn),
config->list_bullet_color);
gtk_widget_set_halign(bullet_color_btn, GTK_ALIGN_START);
g_signal_connect(bullet_color_btn, "color-set", G_CALLBACK(on_color_set),
&config->list_bullet_color);
gtk_grid_attach(GTK_GRID(grid), bullet_color_btn, 1, row++, 1, 1);
gtk_widget_show_all(dialog);
return dialog;
}