9 Commits
Author SHA1 Message Date
NotAlexNoyleandClaude Opus 4.8 e3b229c464 Add MIT License
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 14:17:33 -04:00
rabfulton effa685762 Merge pull request #1 from FenrirTheGray/fix/reload-file-crash
Fix error when reloading file more than once
2026-04-10 11:28:26 +01:00
Aleksandar Colovic a30ce043f4 Fix crash when reloading file more than once
markyd_app_open_file() invalidates the current path string, so calling
it with the pointer from markyd_app_get_current_path() works the first
time but causes a use-after-free on subsequent reloads. Copy the path
with g_strdup() before passing it to open_file, and free it afterwards.
2026-04-08 18:46:24 +02:00
Robert Fulton 3d36df9847 Update README.md 2026-02-26 02:09:07 +00:00
Robert Fulton 0cce704eec Support hanging indents for list items 2026-02-26 02:06:58 +00:00
Robert Fulton 55eff8c494 Fix for search within tables 2026-02-26 01:50:11 +00:00
Robert Fulton c40a410d7d Add support for local images 2026-02-26 01:18:04 +00:00
Robert Fulton ab0b9b37d7 Add search on document 2026-02-23 17:52:51 +00:00
Robert Fulton 9bfb95e5a7 Update README.md 2026-02-19 11:30:50 +00:00
7 changed files with 1257 additions and 34 deletions

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Robert Fulton
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+11 -1
View File
@@ -11,6 +11,8 @@ A lightweight GTK 3 markdown viewer for desktop Linux. It is ideal as your defau
- **Minimal UI** - Clean toolbar with open and settings buttons
- **Lightweight** - Pure C, no web technologies, fast startup
- **Hyperlink support** - Left click opens links and internal anchors
- **Document search** - `Ctrl+F` with next/previous match navigation
- **Local image support** - Local images are resized to fit the the document window
## Supported Markdown
@@ -27,7 +29,7 @@ A lightweight GTK 3 markdown viewer for desktop Linux. It is ideal as your defau
| `> quote` | Block quote |
| `[text](url)` | Link |
| `~~strike~~` | Strikethrough |
| `| table | row |` | Markdown tables |
| `\| table \| row \|` | Markdown tables |
| `---` | Horizontal rule |
Code blocks currently support a beta version of keyword highlighting for fenced languages `c`, `java`, and `python`.
@@ -59,8 +61,16 @@ sudo dnf install ./viewmd-*.rpm
Run `viewmd` to start the application.
- **Open button**: Open a markdown document
- **Reload button**: Reload the currently open document from disk
- **Settings button**: Adjust theme, fonts, and markdown accent colors
### Find in Document
- Press `Ctrl+F` to open search.
- Type to highlight matches as you search.
- Press `Enter` for next match and `Shift+Enter` for previous match.
- Press `Esc` to close search.
### Set as Default `.md` Viewer
After installing, associate markdown MIME types with `viewmd.desktop`:
+203 -4
View File
@@ -1,4 +1,5 @@
#include "editor.h"
#include "app.h"
#include "markdown.h"
#include <string.h>
@@ -10,9 +11,13 @@ static gboolean on_leave_notify(GtkWidget *widget, GdkEventCrossing *event,
gpointer user_data);
static void apply_markdown(MarkydEditor *self);
static void schedule_markdown_apply(MarkydEditor *self);
static void render_image_widgets(MarkydEditor *self);
static void render_table_widgets(MarkydEditor *self);
static const gchar *TABLE_WIDGET_DATA_KEY = "viewmd-table-widget";
static void refresh_image_widget_scales(MarkydEditor *self);
static gboolean resolve_image_source_path(MarkydEditor *self, const gchar *src,
gchar **out_path);
static void on_text_view_size_allocate(GtkWidget *widget, GtkAllocation *allocation,
gpointer user_data);
static gchar *get_url_from_iter_tags(GtkTextIter *iter) {
GSList *tags;
@@ -135,10 +140,193 @@ static void apply_markdown(MarkydEditor *self) {
self->updating_tags = TRUE;
markdown_apply_tags(self->buffer,
self->source_content ? self->source_content : "");
render_image_widgets(self);
render_table_widgets(self);
refresh_image_widget_scales(self);
self->updating_tags = FALSE;
}
static gboolean resolve_image_source_path(MarkydEditor *self, const gchar *src,
gchar **out_path) {
gchar *path = NULL;
if (out_path) {
*out_path = NULL;
}
if (!self || !src || src[0] == '\0' || !out_path) {
return FALSE;
}
if (g_uri_parse_scheme(src) != NULL) {
if (g_str_has_prefix(src, "file://")) {
path = g_filename_from_uri(src, NULL, NULL);
} else {
return FALSE;
}
} else if (g_path_is_absolute(src)) {
path = g_strdup(src);
} else {
const gchar *current_path = markyd_app_get_current_path(self->app);
if (current_path && current_path[0] != '\0') {
gchar *dir = g_path_get_dirname(current_path);
path = g_build_filename(dir, src, NULL);
g_free(dir);
} else {
path = g_strdup(src);
}
}
if (!path) {
return FALSE;
}
if (!g_file_test(path, G_FILE_TEST_EXISTS)) {
g_free(path);
return FALSE;
}
*out_path = path;
return TRUE;
}
static gint get_image_max_width(MarkydEditor *self) {
GtkAllocation alloc;
gint width;
if (!self || !self->text_view) {
return 0;
}
gtk_widget_get_allocation(self->text_view, &alloc);
width = alloc.width - gtk_text_view_get_left_margin(GTK_TEXT_VIEW(self->text_view)) -
gtk_text_view_get_right_margin(GTK_TEXT_VIEW(self->text_view)) - 24;
return MAX(width, 64);
}
static void scale_image_widget(GtkWidget *widget, gint max_width) {
GtkWidget *image;
GdkPixbuf *orig;
gint ow;
gint oh;
if (!widget || max_width <= 0) {
return;
}
image = g_object_get_data(G_OBJECT(widget), "viewmd-image-widget-child");
orig = g_object_get_data(G_OBJECT(widget), "viewmd-image-orig-pixbuf");
if (!GTK_IS_IMAGE(image) || !GDK_IS_PIXBUF(orig)) {
return;
}
ow = gdk_pixbuf_get_width(orig);
oh = gdk_pixbuf_get_height(orig);
if (ow <= 0 || oh <= 0) {
return;
}
if (ow > max_width) {
gint nh = (gint)(((gdouble)oh * (gdouble)max_width) / (gdouble)ow);
GdkPixbuf *scaled =
gdk_pixbuf_scale_simple(orig, max_width, MAX(nh, 1), GDK_INTERP_BILINEAR);
gtk_image_set_from_pixbuf(GTK_IMAGE(image), scaled);
if (scaled) {
g_object_unref(scaled);
}
} else {
gtk_image_set_from_pixbuf(GTK_IMAGE(image), orig);
}
}
static void refresh_image_widget_scales(MarkydEditor *self) {
GtkTextIter iter;
GtkTextIter end;
gint max_width;
if (!self || !self->buffer) {
return;
}
max_width = get_image_max_width(self);
gtk_text_buffer_get_bounds(self->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), VIEWMD_IMAGE_ANCHOR_DATA) != NULL) {
GtkWidget *image_widget =
g_object_get_data(G_OBJECT(anchor), VIEWMD_IMAGE_WIDGET_DATA);
if (image_widget) {
scale_image_widget(image_widget, max_width);
}
}
gtk_text_iter_forward_char(&iter);
}
}
static void render_image_widgets(MarkydEditor *self) {
GtkTextIter iter;
GtkTextIter end;
gint max_width;
if (!self || !self->buffer || !self->text_view) {
return;
}
max_width = get_image_max_width(self);
gtk_text_buffer_get_bounds(self->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), VIEWMD_IMAGE_ANCHOR_DATA) != NULL) {
GtkWidget *image_widget =
g_object_get_data(G_OBJECT(anchor), VIEWMD_IMAGE_WIDGET_DATA);
if (!image_widget) {
const gchar *src =
g_object_get_data(G_OBJECT(anchor), VIEWMD_IMAGE_SRC_DATA);
const gchar *alt =
g_object_get_data(G_OBJECT(anchor), VIEWMD_IMAGE_ALT_DATA);
gchar *path = NULL;
if (resolve_image_source_path(self, src, &path)) {
GError *err = NULL;
GdkPixbuf *orig = gdk_pixbuf_new_from_file(path, &err);
if (orig) {
GtkWidget *event_box = gtk_event_box_new();
GtkWidget *image = gtk_image_new();
gtk_event_box_set_visible_window(GTK_EVENT_BOX(event_box), FALSE);
gtk_widget_set_halign(event_box, GTK_ALIGN_START);
gtk_container_add(GTK_CONTAINER(event_box), image);
g_object_set_data(G_OBJECT(event_box), "viewmd-image-widget-child",
image);
g_object_set_data_full(G_OBJECT(event_box), "viewmd-image-orig-pixbuf",
orig, g_object_unref);
scale_image_widget(event_box, max_width);
image_widget = event_box;
} else if (err) {
g_error_free(err);
}
}
if (!image_widget) {
GtkWidget *fallback = gtk_label_new(alt && alt[0] != '\0' ? alt : src);
gtk_widget_set_halign(fallback, GTK_ALIGN_START);
gtk_style_context_add_class(gtk_widget_get_style_context(fallback),
"dim-label");
image_widget = fallback;
}
gtk_text_view_add_child_at_anchor(GTK_TEXT_VIEW(self->text_view),
image_widget, anchor);
gtk_widget_show_all(image_widget);
g_object_set_data(G_OBJECT(anchor), VIEWMD_IMAGE_WIDGET_DATA, image_widget);
g_free(path);
} else {
scale_image_widget(image_widget, max_width);
}
}
gtk_text_iter_forward_char(&iter);
}
}
static void render_table_widgets(MarkydEditor *self) {
GtkTextIter iter;
GtkTextIter end;
@@ -152,14 +340,15 @@ static void render_table_widgets(MarkydEditor *self) {
GtkTextChildAnchor *anchor = gtk_text_iter_get_child_anchor(&iter);
if (anchor &&
g_object_get_data(G_OBJECT(anchor), VIEWMD_TABLE_ANCHOR_DATA) != NULL) {
GtkWidget *table = g_object_get_data(G_OBJECT(anchor), TABLE_WIDGET_DATA_KEY);
GtkWidget *table =
g_object_get_data(G_OBJECT(anchor), VIEWMD_TABLE_WIDGET_DATA);
if (!table) {
table = markdown_create_table_widget(anchor);
if (table) {
gtk_text_view_add_child_at_anchor(GTK_TEXT_VIEW(self->text_view), table,
anchor);
gtk_widget_show_all(table);
g_object_set_data(G_OBJECT(anchor), TABLE_WIDGET_DATA_KEY, table);
g_object_set_data(G_OBJECT(anchor), VIEWMD_TABLE_WIDGET_DATA, table);
}
}
}
@@ -215,10 +404,20 @@ MarkydEditor *markyd_editor_new(MarkydApp *app) {
G_CALLBACK(on_motion_notify), self);
g_signal_connect(self->text_view, "leave-notify-event",
G_CALLBACK(on_leave_notify), self);
g_signal_connect(self->text_view, "size-allocate",
G_CALLBACK(on_text_view_size_allocate), self);
return self;
}
static void on_text_view_size_allocate(GtkWidget *widget, GtkAllocation *allocation,
gpointer user_data) {
MarkydEditor *self = (MarkydEditor *)user_data;
(void)widget;
(void)allocation;
refresh_image_widget_scales(self);
}
void markyd_editor_free(MarkydEditor *self) {
if (!self) {
return;
+251 -18
View File
@@ -29,6 +29,11 @@ typedef struct {
guint next_index;
} ListState;
typedef struct {
gint left_margin;
gint indent;
} ListLayoutSpec;
typedef struct {
MD_BLOCKTYPE type;
guint pushed_tags;
@@ -74,6 +79,9 @@ typedef struct {
ViewmdTableRow *table_current_row;
GString *table_cell_text;
guint table_current_col;
gboolean in_image;
gchar *image_src;
GString *image_alt;
GArray *code_blocks; /* CodeBlockRange */
gint current_code_start_offset;
const MarkydLanguageHighlight *current_code_language;
@@ -274,6 +282,14 @@ static void update_newline_state(RenderCtx *ctx, const gchar *text, gsize len) {
}
}
static void note_non_newline_output(RenderCtx *ctx) {
if (!ctx) {
return;
}
ctx->has_output = TRUE;
ctx->trailing_newlines = 0;
}
static void apply_tag_by_offsets(GtkTextBuffer *buffer, GtkTextTag *tag,
gint start_offset, gint end_offset) {
GtkTextIter start;
@@ -337,6 +353,61 @@ static void ensure_newlines(RenderCtx *ctx, guint min_newlines) {
}
}
static guint decimal_digits(guint value) {
guint digits = 1;
while (value >= 10) {
value /= 10;
digits++;
}
return digits;
}
static guint list_marker_columns(RenderCtx *ctx) {
ListState *list;
if (!ctx || !ctx->list_stack || ctx->list_stack->len == 0) {
return 2; /* "• " */
}
list = &g_array_index(ctx->list_stack, ListState, ctx->list_stack->len - 1);
if (list->ordered) {
return decimal_digits(MAX(1, list->next_index)) + 2; /* "12. " */
}
return 2; /* "• " */
}
static ListLayoutSpec compute_list_layout(guint depth, guint quote_depth,
guint marker_cols) {
ListLayoutSpec spec = {0, 0};
guint depth_index = (depth > 0) ? (depth - 1) : 0;
gint marker_px = (gint)CLAMP((gint)marker_cols * 9, 16, 64);
gint base_indent = 20 + ((gint)depth_index * 22) + ((gint)quote_depth * 24);
spec.left_margin = base_indent + marker_px;
spec.indent = -marker_px;
return spec;
}
static GtkTextTag *ensure_list_layout_tag(RenderCtx *ctx, guint depth,
guint quote_depth,
guint marker_cols) {
gchar *name;
GtkTextTag *tag;
ListLayoutSpec spec;
if (!ctx || !ctx->buffer) {
return NULL;
}
name = g_strdup_printf("list_layout_%u_%u_%u", depth, quote_depth, marker_cols);
tag = lookup_tag(ctx->buffer, name);
if (!tag) {
spec = compute_list_layout(depth, quote_depth, marker_cols);
tag = gtk_text_buffer_create_tag(ctx->buffer, name, "left-margin",
spec.left_margin, "indent", spec.indent, NULL);
}
g_free(name);
return tag;
}
static void push_active_tag(RenderCtx *ctx, GtkTextTag *tag, guint *counter) {
if (!ctx || !tag) {
return;
@@ -589,6 +660,109 @@ static void table_capture_append(RenderCtx *ctx, const gchar *text) {
g_free(escaped);
}
static gchar *table_cell_markup_to_plain(const gchar *markup) {
gchar *plain = NULL;
GError *error = NULL;
if (!markup || markup[0] == '\0') {
return g_strdup("");
}
if (pango_parse_markup(markup, -1, 0, NULL, &plain, NULL, &error)) {
return plain;
}
if (error) {
g_error_free(error);
}
return g_strdup(markup);
}
static void table_search_index_free(gpointer data) {
ViewmdTableSearchIndex *index = (ViewmdTableSearchIndex *)data;
if (!index) {
return;
}
if (index->cells) {
g_array_free(index->cells, TRUE);
}
g_free(index);
}
static void table_emit_hidden_search_text(RenderCtx *ctx, ViewmdTable *table,
GtkTextChildAnchor *anchor) {
ViewmdTableSearchIndex *index;
gboolean saved_has_output;
guint saved_trailing_newlines;
const gchar *cell_sep = " ";
if (!ctx || !ctx->buffer || !table || !anchor || !table->rows ||
table->rows->len == 0 || table->col_count == 0) {
return;
}
/* Hidden search text must not affect visible block spacing state. */
saved_has_output = ctx->has_output;
saved_trailing_newlines = ctx->trailing_newlines;
index = g_new0(ViewmdTableSearchIndex, 1);
index->cells = g_array_new(FALSE, FALSE, sizeof(ViewmdTableSearchCellRange));
index->start_offset = gtk_text_iter_get_offset(&ctx->iter);
for (guint r = 0; r < table->rows->len; r++) {
ViewmdTableRow *row = g_ptr_array_index(table->rows, r);
if (!row) {
continue;
}
for (guint c = 0; c < table->col_count; c++) {
const gchar *cell_markup = "";
gchar *plain;
gint cell_start;
gint cell_end;
if (c < row->cells->len) {
cell_markup = g_ptr_array_index(row->cells, c);
if (!cell_markup) {
cell_markup = "";
}
}
plain = table_cell_markup_to_plain(cell_markup);
cell_start = gtk_text_iter_get_offset(&ctx->iter);
if (plain && plain[0] != '\0') {
gtk_text_buffer_insert(ctx->buffer, &ctx->iter, plain, -1);
}
cell_end = gtk_text_iter_get_offset(&ctx->iter);
if (cell_end > cell_start) {
ViewmdTableSearchCellRange cell_range = {(gint)r, (gint)c, cell_start,
cell_end};
g_array_append_val(index->cells, cell_range);
}
g_free(plain);
if (!(r + 1 == table->rows->len && c + 1 == table->col_count)) {
gtk_text_buffer_insert(ctx->buffer, &ctx->iter, cell_sep, -1);
}
}
}
index->end_offset = gtk_text_iter_get_offset(&ctx->iter);
if (index->end_offset > index->start_offset) {
apply_tag_by_name_offsets(ctx->buffer, TAG_INVISIBLE, index->start_offset,
index->end_offset);
g_object_set_data_full(G_OBJECT(anchor), VIEWMD_TABLE_SEARCH_INDEX_DATA, index,
table_search_index_free);
} else {
table_search_index_free(index);
}
ctx->has_output = saved_has_output;
ctx->trailing_newlines = saved_trailing_newlines;
}
static void table_capture_span_enter(RenderCtx *ctx, MD_SPANTYPE type) {
if (!ctx || !ctx->table_cell_text) {
return;
@@ -707,35 +881,53 @@ static void table_emit_anchor(RenderCtx *ctx) {
}
anchor = gtk_text_buffer_create_child_anchor(ctx->buffer, &ctx->iter);
note_non_newline_output(ctx);
g_object_set_data(G_OBJECT(anchor), VIEWMD_TABLE_ANCHOR_DATA, GINT_TO_POINTER(1));
g_object_set_data_full(G_OBJECT(anchor), TABLE_MODEL_DATA_KEY, ctx->table_model,
viewmd_table_free);
/* Force at least one hard line break after the embedded table widget so
* following markdown never shares the same visual line. */
insert_cstr(ctx, "\n");
/* Keep table text searchable via Ctrl+F without showing duplicate content. */
table_emit_hidden_search_text(ctx, ctx->table_model, anchor);
ctx->table_model = NULL;
}
static void image_emit_anchor(RenderCtx *ctx) {
GtkTextChildAnchor *anchor;
gchar *alt = NULL;
if (!ctx || !ctx->buffer || !ctx->image_src || ctx->image_src[0] == '\0') {
return;
}
anchor = gtk_text_buffer_create_child_anchor(ctx->buffer, &ctx->iter);
note_non_newline_output(ctx);
g_object_set_data(G_OBJECT(anchor), VIEWMD_IMAGE_ANCHOR_DATA, GINT_TO_POINTER(1));
g_object_set_data_full(G_OBJECT(anchor), VIEWMD_IMAGE_SRC_DATA,
g_strdup(ctx->image_src), g_free);
if (ctx->image_alt && ctx->image_alt->len > 0) {
alt = g_strdup(ctx->image_alt->str);
} else {
alt = g_strdup("");
}
g_object_set_data_full(G_OBJECT(anchor), VIEWMD_IMAGE_ALT_DATA, alt, g_free);
/* Keep following markdown on a new visual line after embedded images. */
insert_cstr(ctx, "\n");
}
static void insert_list_marker(RenderCtx *ctx) {
ListState *list;
gchar *ordered = NULL;
gint marker_start_offset;
gint marker_end_offset;
guint depth_indent;
if (!ctx || ctx->list_stack->len == 0) {
return;
}
list = &g_array_index(ctx->list_stack, ListState, ctx->list_stack->len - 1);
depth_indent = ((ctx->list_stack->len > 0) ? (ctx->list_stack->len - 1) * 2 : 0) +
(ctx->quote_depth * 2);
for (guint i = 0; i < depth_indent; i++) {
insert_cstr(ctx, " ");
}
marker_start_offset = gtk_text_iter_get_offset(&ctx->iter);
if (list->ordered) {
@@ -825,13 +1017,19 @@ static int on_enter_block(MD_BLOCKTYPE type, void *detail, void *userdata) {
}
case MD_BLOCK_LI:
ensure_newlines(ctx, 1);
push_active_tag_by_name(ctx, TAG_LIST,
&g_array_index(ctx->block_stack, BlockState,
ctx->block_stack->len - 1)
.pushed_tags);
insert_list_marker(ctx);
ctx->list_item_prefix_pending = TRUE;
{
guint marker_cols = list_marker_columns(ctx);
GtkTextTag *layout_tag = ensure_list_layout_tag(
ctx, ctx->list_stack->len, ctx->quote_depth, marker_cols);
ensure_newlines(ctx, 1);
push_active_tag(ctx, layout_tag,
&g_array_index(ctx->block_stack, BlockState,
ctx->block_stack->len - 1)
.pushed_tags);
insert_list_marker(ctx);
ctx->list_item_prefix_pending = TRUE;
}
break;
case MD_BLOCK_HR: {
@@ -1005,6 +1203,19 @@ static int on_enter_span(MD_SPANTYPE type, void *detail, void *userdata) {
break;
}
case MD_SPAN_IMG: {
MD_SPAN_IMG_DETAIL *img = (MD_SPAN_IMG_DETAIL *)detail;
g_free(ctx->image_src);
ctx->image_src = attr_to_string(img ? &img->src : NULL);
if (!ctx->image_alt) {
ctx->image_alt = g_string_new(NULL);
} else {
g_string_set_size(ctx->image_alt, 0);
}
ctx->in_image = TRUE;
break;
}
default:
break;
}
@@ -1019,6 +1230,15 @@ static int on_leave_span(MD_SPANTYPE type, void *detail, void *userdata) {
if (ctx->span_stack->len > 0) {
SpanState state =
g_array_index(ctx->span_stack, SpanState, ctx->span_stack->len - 1);
if (state.type == MD_SPAN_IMG && ctx->in_image) {
image_emit_anchor(ctx);
ctx->in_image = FALSE;
g_free(ctx->image_src);
ctx->image_src = NULL;
if (ctx->image_alt) {
g_string_set_size(ctx->image_alt, 0);
}
}
if (ctx->table_cell_text) {
table_capture_span_leave(ctx, state.type);
}
@@ -1045,7 +1265,12 @@ static int on_text(MD_TEXTTYPE type, const MD_CHAR *text, MD_SIZE size,
if (ctx->list_item_prefix_pending && rendered[0] != '\0') {
ctx->list_item_prefix_pending = FALSE;
}
if (ctx->table_cell_text) {
if (ctx->in_image) {
if (!ctx->image_alt) {
ctx->image_alt = g_string_new(NULL);
}
g_string_append(ctx->image_alt, rendered);
} else if (ctx->table_cell_text) {
table_capture_append(ctx, rendered);
} else {
insert_cstr(ctx, rendered);
@@ -1241,6 +1466,10 @@ GtkWidget *markdown_create_table_widget(GtkTextChildAnchor *anchor) {
gtk_style_context_add_class(gtk_widget_get_style_context(cell),
"viewmd-table-header-cell");
}
g_object_set_data(G_OBJECT(cell), VIEWMD_TABLE_CELL_ROW_DATA,
GINT_TO_POINTER((gint)r));
g_object_set_data(G_OBJECT(cell), VIEWMD_TABLE_CELL_COL_DATA,
GINT_TO_POINTER((gint)c));
gtk_style_context_add_class(gtk_widget_get_style_context(label),
"viewmd-table-label");
gtk_widget_set_hexpand(cell, FALSE);
@@ -1322,6 +1551,10 @@ void markdown_apply_tags(GtkTextBuffer *buffer, const gchar *source) {
if (ctx.table_cell_text) {
g_string_free(ctx.table_cell_text, TRUE);
}
g_free(ctx.image_src);
if (ctx.image_alt) {
g_string_free(ctx.image_alt, TRUE);
}
if (ctx.table_model) {
viewmd_table_free(ctx.table_model);
}
+29
View File
@@ -20,6 +20,35 @@ void markdown_update_accent_tags(GtkTextBuffer *buffer);
/* GObject data key used to mark table child anchors with parsed table data. */
#define VIEWMD_TABLE_ANCHOR_DATA "viewmd-table-anchor"
/* GObject data key set on table anchors for hidden searchable index metadata. */
#define VIEWMD_TABLE_SEARCH_INDEX_DATA "viewmd-table-search-index"
/* GObject data key set on table anchors for attached table widget instance. */
#define VIEWMD_TABLE_WIDGET_DATA "viewmd-table-widget"
/* GObject data keys set on each table cell widget. */
#define VIEWMD_TABLE_CELL_ROW_DATA "viewmd-table-cell-row"
#define VIEWMD_TABLE_CELL_COL_DATA "viewmd-table-cell-col"
/* CSS classes for table search highlight states. */
#define VIEWMD_TABLE_CELL_MATCH_CLASS "viewmd-table-cell-match"
#define VIEWMD_TABLE_CELL_CURRENT_CLASS "viewmd-table-cell-current"
/* GObject data keys for image anchors and metadata. */
#define VIEWMD_IMAGE_ANCHOR_DATA "viewmd-image-anchor"
#define VIEWMD_IMAGE_SRC_DATA "viewmd-image-src"
#define VIEWMD_IMAGE_ALT_DATA "viewmd-image-alt"
#define VIEWMD_IMAGE_WIDGET_DATA "viewmd-image-widget"
typedef struct {
gint row;
gint col;
gint start_offset;
gint end_offset;
} ViewmdTableSearchCellRange;
typedef struct {
gint start_offset;
gint end_offset;
GArray *cells; /* ViewmdTableSearchCellRange */
} ViewmdTableSearchIndex;
/* Normalize heading/link text into anchor slug form. Caller owns result. */
gchar *markdown_normalize_anchor_slug(const gchar *text);
+735 -11
View File
@@ -4,9 +4,49 @@
#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 "viewmd_search_match"
#define TAG_SEARCH_CURRENT "viewmd_search_current"
static void on_open_clicked(GtkButton *button, gpointer user_data);
static void on_refresh_clicked(GtkButton *button, gpointer user_data);
static void on_settings_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,
@@ -31,9 +71,564 @@ static gboolean geometry_debug_enabled(void) {
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;
}
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, VIEWMD_TABLE_CELL_MATCH_CLASS);
} else {
gtk_style_context_remove_class(style, VIEWMD_TABLE_CELL_MATCH_CLASS);
}
if (current) {
gtk_style_context_add_class(style, VIEWMD_TABLE_CELL_CURRENT_CLASS);
} else {
gtk_style_context_remove_class(style, VIEWMD_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), VIEWMD_TABLE_CELL_ROW_DATA));
gint cell_col = GPOINTER_TO_INT(
g_object_get_data(G_OBJECT(cell), VIEWMD_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), VIEWMD_TABLE_ANCHOR_DATA) != NULL) {
GtkWidget *table_widget =
g_object_get_data(G_OBJECT(anchor), VIEWMD_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),
VIEWMD_TABLE_CELL_MATCH_CLASS);
}
if (clear_current) {
gtk_style_context_remove_class(gtk_widget_get_style_context(cell),
VIEWMD_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) {
ViewmdTableSearchIndex *index = g_object_get_data(
G_OBJECT(anchor), VIEWMD_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) {
ViewmdTableSearchCellRange *overlap_cell = NULL;
for (guint i = 0; i < index->cells->len; i++) {
ViewmdTableSearchCellRange *cell =
&g_array_index(index->cells, ViewmdTableSearchCellRange, 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), VIEWMD_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), VIEWMD_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),
VIEWMD_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;
}
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) {
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) {
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);
}
MarkydWindow *markyd_window_new(MarkydApp *app) {
MarkydWindow *self = g_new0(MarkydWindow, 1);
GtkWidget *left_buttons;
GtkWidget *main_box;
GtkWidget *search_box;
self->app = app;
@@ -97,19 +692,68 @@ MarkydWindow *markyd_window_new(MarkydApp *app) {
gtk_box_pack_start(GTK_BOX(left_buttons), self->btn_settings, FALSE, FALSE, 0);
gtk_header_bar_pack_start(GTK_HEADER_BAR(self->header_bar), left_buttons);
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->scroll = gtk_scrolled_window_new(NULL, NULL);
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(self->scroll),
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
gtk_container_add(GTK_CONTAINER(self->window), self->scroll);
gtk_box_pack_start(GTK_BOX(main_box), self->scroll, TRUE, TRUE, 0);
self->editor = markyd_editor_new(app);
gtk_container_add(GTK_CONTAINER(self->scroll),
markyd_editor_get_widget(self->editor));
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->header_bar);
gtk_widget_show_all(self->scroll);
gtk_widget_show_all(self->window);
return self;
}
@@ -124,6 +768,10 @@ void markyd_window_apply_css(MarkydWindow *self) {
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(),
@@ -137,14 +785,26 @@ void markyd_window_apply_css(MarkydWindow *self) {
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;
@@ -187,9 +847,22 @@ void markyd_window_apply_css(MarkydWindow *self) {
"}"
".viewmd-table-cell label {"
" color: %s;"
"}"
".viewmd-table-cell." VIEWMD_TABLE_CELL_MATCH_CLASS " {"
" background-color: %s;"
"}"
".viewmd-table-cell." VIEWMD_TABLE_CELL_MATCH_CLASS " label {"
" color: %s;"
"}"
".viewmd-table-cell." VIEWMD_TABLE_CELL_CURRENT_CLASS " {"
" background-color: %s;"
"}"
".viewmd-table-cell." VIEWMD_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);
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);
@@ -198,13 +871,20 @@ void markyd_window_apply_css(MarkydWindow *self) {
gdk_screen_get_default(), GTK_STYLE_PROVIDER(css),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
(void)self;
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);
}
@@ -285,15 +965,17 @@ static void on_open_clicked(GtkButton *button, gpointer user_data) {
static void on_refresh_clicked(GtkButton *button, gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data;
const gchar *path;
const gchar *current;
gchar *path;
(void)button;
path = markyd_app_get_current_path(self->app);
if (!path || path[0] == '\0') {
current = markyd_app_get_current_path(self->app);
if (!current || current[0] == '\0') {
return;
}
path = g_strdup(current);
if (!markyd_app_open_file(self->app, path)) {
GtkWidget *error_dialog = gtk_message_dialog_new(
GTK_WINDOW(self->window),
@@ -304,6 +986,7 @@ static void on_refresh_clicked(GtkButton *button, gpointer user_data) {
gtk_dialog_run(GTK_DIALOG(error_dialog));
gtk_widget_destroy(error_dialog);
}
g_free(path);
}
static void on_settings_clicked(GtkButton *button, gpointer user_data) {
@@ -328,14 +1011,55 @@ static void on_settings_clicked(GtkButton *button, gpointer user_data) {
static gboolean on_key_press_event(GtkWidget *widget, GdkEventKey *event,
gpointer user_data) {
(void)user_data;
(void)widget;
MarkydWindow *self = (MarkydWindow *)user_data;
if (event && event->keyval == GDK_KEY_Escape) {
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->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) {
gtk_window_close(GTK_WINDOW(widget));
return TRUE;
}
if (event->keyval == GDK_KEY_Home || event->keyval == GDK_KEY_KP_Home) {
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;
}
+7
View File
@@ -12,10 +12,17 @@ typedef struct _MarkydWindow {
GtkWidget *btn_open;
GtkWidget *btn_refresh;
GtkWidget *btn_settings;
GtkWidget *search_revealer;
GtkWidget *search_entry;
GtkWidget *btn_search_prev;
GtkWidget *btn_search_next;
GtkWidget *lbl_search_status;
GtkWidget *lbl_title;
GtkWidget *scroll;
MarkydEditor *editor;
MarkydApp *app;
GArray *search_matches;
gint search_current_index;
} MarkydWindow;
/* Lifecycle */