Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3b229c464 | ||
|
|
effa685762 | ||
|
|
a30ce043f4 | ||
|
|
3d36df9847 | ||
|
|
0cce704eec | ||
|
|
55eff8c494 | ||
|
|
c40a410d7d | ||
|
|
ab0b9b37d7 | ||
|
|
9bfb95e5a7 |
No files matched your search
+21
@@ -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,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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in new issue
Block a user