ViewMD -> SeeMD

This commit is contained in:
NotAlexNoyle committed 2026-07-01 14:24:11 -04:00
1 parent effa685762
commit e27c392078
23 files changed
+2994 -79

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.
+3 -2
View File
@@ -1,6 +1,7 @@
CC = gcc CC = gcc
CFLAGS = -Wall -Wextra -O2 -g `pkg-config --cflags gtk+-3.0` PKGS = gtk+-3.0 webkit2gtk-4.1
LDFLAGS = `pkg-config --libs gtk+-3.0` CFLAGS = -Wall -Wextra -O2 -g `pkg-config --cflags $(PKGS)`
LDFLAGS = `pkg-config --libs $(PKGS)`
SRCDIR = src SRCDIR = src
OBJDIR = obj OBJDIR = obj
+14 -18
View File
@@ -1,18 +1,19 @@
# ViewMD # ViewMD
A lightweight GTK 3 markdown viewer for desktop Linux. It is ideal as your default app for opening `.md` files. Uses the excellent library [md4c](https://github.com/mity/md4c) for markdown parsing. This project is sort of a companion app/fork of my other markdown project [TrayMD](https://github.com/rabfulton/TrayMD). A lightweight GTK 3 markdown viewer and editor for desktop Linux. It is ideal as your default app for opening `.md` files. Uses the excellent library [md4c](https://github.com/mity/md4c) for markdown parsing and WebKitGTK for GitHub-style preview rendering. This project is a fork of [ViewMD](https://github.com/rabfulton/ViewMD) by NotAlexNoyle.
![ViewMD](assets/screenshot.png) ![ViewMD](assets/screenshot.png)
## Features ## Features
- **Native GTK viewer** - No webviews - **GitHub-style preview** - Rendered Markdown and inline HTML are displayed through WebKitGTK with GitHub-like markdown styling
- **Read-only rendering** - Focused on viewing markdown files - **Read-only preview** - Rendered Markdown stays non-editable until source mode is enabled
- **Minimal UI** - Clean toolbar with open and settings buttons - **Source editing mode** - Toggle between rendered preview and editable Markdown
- **Lightweight** - Pure C, no web technologies, fast startup - **Minimal UI** - Clean toolbar with open, reload, edit, save, and settings buttons
- **Lightweight** - Pure C with GTK and WebKitGTK, fast startup
- **Hyperlink support** - Left click opens links and internal anchors - **Hyperlink support** - Left click opens links and internal anchors
- **Document search** - `Ctrl+F` with next/previous match navigation - **Document search** - `Ctrl+F` with next/previous match navigation
- **Local image support** - Local images are resized to fit the the document window - **Local image support** - Local images keep natural size unless they need to fit the document window
## Supported Markdown ## Supported Markdown
@@ -30,6 +31,7 @@ A lightweight GTK 3 markdown viewer for desktop Linux. It is ideal as your defau
| `[text](url)` | Link | | `[text](url)` | Link |
| `~~strike~~` | Strikethrough | | `~~strike~~` | Strikethrough |
| `\| table \| row \|` | Markdown tables | | `\| table \| row \|` | Markdown tables |
| `<strong>HTML</strong>` | GitHub-style inline and block HTML rendered in preview mode |
| `---` | Horizontal rule | | `---` | Horizontal rule |
Code blocks currently support a beta version of keyword highlighting for fenced languages `c`, `java`, and `python`. Code blocks currently support a beta version of keyword highlighting for fenced languages `c`, `java`, and `python`.
@@ -62,6 +64,8 @@ Run `viewmd` to start the application.
- **Open button**: Open a markdown document - **Open button**: Open a markdown document
- **Reload button**: Reload the currently open document from disk - **Reload button**: Reload the currently open document from disk
- **Edit button**: Toggle editable Markdown source mode (`Ctrl+E`)
- **Save button**: Save the current Markdown source (`Ctrl+S`)
- **Settings button**: Adjust theme, fonts, and markdown accent colors - **Settings button**: Adjust theme, fonts, and markdown accent colors
### Find in Document ### Find in Document
@@ -113,17 +117,17 @@ sudo make uninstall
### Arch Linux ### Arch Linux
```bash ```bash
sudo pacman -S gtk3 sudo pacman -S gtk3 webkit2gtk-4.1
``` ```
### Ubuntu/Debian ### Ubuntu/Debian
```bash ```bash
sudo apt install libgtk-3-dev sudo apt install libgtk-3-dev libwebkit2gtk-4.1-dev
``` ```
### Fedora ### Fedora
```bash ```bash
sudo dnf install gtk3-devel sudo dnf install gtk3-devel webkit2gtk4.1-devel
``` ```
## Packaging Templates ## Packaging Templates
@@ -133,12 +137,4 @@ sudo dnf install gtk3-devel
- RPM: `packaging/rpm/viewmd.spec.in` - RPM: `packaging/rpm/viewmd.spec.in`
## License ## License
MIT License Released under the [MIT License](LICENSE).
## Other Useful Projects
- TrayMD is an app for taking notes in markdown with live editing [TrayMD](https://github.com/rabfulton/TrayMD)
- Preditor is an image viewer with a similar philosophy to viewmd [preditor](https://github.com/rabfulton/preditor)
- Try my AI panel plugin for XFCE [XFCE Ask](https://github.com/rabfulton/xfce-ask)
- For a feature complete AI application try out [ChatGTK](https://github.com/rabfulton/ChatGTK)
- A lightweight speech to text implementation [Auriscribe](https://github.com/rabfulton/Auriscribe)
- A lightweight local movie database and browser [ReelVault](https://github.com/rabfulton/ReelVault)
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -5,7 +5,7 @@ pkgdesc="Lightweight GTK3 markdown viewer"
arch=('x86_64') arch=('x86_64')
url="https://github.com/rabfulton/ViewMD" url="https://github.com/rabfulton/ViewMD"
license=('MIT') license=('MIT')
depends=('gtk3') depends=('gtk3' 'webkit2gtk-4.1')
makedepends=('git' 'gcc' 'make' 'pkgconf') makedepends=('git' 'gcc' 'make' 'pkgconf')
source=("$pkgname::git+$url.git") source=("$pkgname::git+$url.git")
sha256sums=('SKIP') sha256sums=('SKIP')
+2 -2
View File
@@ -4,6 +4,6 @@ Section: utils
Priority: optional Priority: optional
Architecture: @ARCH@ Architecture: @ARCH@
Maintainer: @MAINTAINER@ Maintainer: @MAINTAINER@
Depends: libgtk-3-0 Depends: libgtk-3-0, libwebkit2gtk-4.1-0
Description: ViewMD - lightweight GTK3 markdown viewer Description: ViewMD - lightweight GTK3 markdown viewer
ViewMD is a lightweight native GTK3 markdown viewer. ViewMD is a lightweight GTK3 markdown viewer with GitHub-style WebKit preview rendering.
+3 -1
View File
@@ -11,11 +11,13 @@ BuildRequires: gcc
BuildRequires: make BuildRequires: make
BuildRequires: pkgconfig BuildRequires: pkgconfig
BuildRequires: gtk3-devel BuildRequires: gtk3-devel
BuildRequires: webkit2gtk4.1-devel
Requires: gtk3 Requires: gtk3
Requires: webkit2gtk4.1
%description %description
ViewMD is a lightweight native GTK3 markdown viewer. ViewMD is a lightweight GTK3 markdown viewer with GitHub-style WebKit preview rendering.
%prep %prep
%autosetup -n viewmd-%{version} %autosetup -n viewmd-%{version}
+39
View File
@@ -156,6 +156,45 @@ gboolean markyd_app_open_file(MarkydApp *self, const gchar *path) {
return TRUE; return TRUE;
} }
gboolean markyd_app_save_file(MarkydApp *self, const gchar *path) {
gchar *content;
GError *error = NULL;
if (!self || !self->editor || !path || path[0] == '\0') {
return FALSE;
}
content = markyd_editor_get_content(self->editor);
if (!g_file_set_contents(path, content ? content : "", -1, &error)) {
if (error) {
g_printerr("Failed to save markdown file '%s': %s\n", path, error->message);
g_error_free(error);
}
g_free(content);
return FALSE;
}
g_free(content);
g_free(self->current_file_path);
self->current_file_path = g_strdup(path);
markyd_app_update_window_title(self);
return TRUE;
}
gboolean markyd_app_save_current_file(MarkydApp *self) {
gchar *path;
gboolean ok;
if (!self || !self->current_file_path || self->current_file_path[0] == '\0') {
return FALSE;
}
path = g_strdup(self->current_file_path);
ok = markyd_app_save_file(self, path);
g_free(path);
return ok;
}
const gchar *markyd_app_get_current_path(MarkydApp *self) { const gchar *markyd_app_get_current_path(MarkydApp *self) {
if (!self) { if (!self) {
return NULL; return NULL;
+2
View File
@@ -25,6 +25,8 @@ int markyd_app_run(MarkydApp *app, int argc, char **argv);
/* Document management */ /* Document management */
gboolean markyd_app_open_file(MarkydApp *app, const gchar *path); gboolean markyd_app_open_file(MarkydApp *app, const gchar *path);
gboolean markyd_app_save_file(MarkydApp *app, const gchar *path);
gboolean markyd_app_save_current_file(MarkydApp *app);
/* Utility */ /* Utility */
const gchar *markyd_app_get_current_path(MarkydApp *app); const gchar *markyd_app_get_current_path(MarkydApp *app);
+496 -32
View File
@@ -2,6 +2,7 @@
#include "app.h" #include "app.h"
#include "markdown.h" #include "markdown.h"
#include <string.h> #include <string.h>
#include <webkit2/webkit2.h>
static gboolean on_button_release(GtkWidget *widget, GdkEventButton *event, static gboolean on_button_release(GtkWidget *widget, GdkEventButton *event,
gpointer user_data); gpointer user_data);
@@ -18,6 +19,15 @@ static gboolean resolve_image_source_path(MarkydEditor *self, const gchar *src,
gchar **out_path); gchar **out_path);
static void on_text_view_size_allocate(GtkWidget *widget, GtkAllocation *allocation, static void on_text_view_size_allocate(GtkWidget *widget, GtkAllocation *allocation,
gpointer user_data); gpointer user_data);
static void on_buffer_changed(GtkTextBuffer *buffer, gpointer user_data);
static void sync_source_from_buffer(MarkydEditor *self);
static void show_source_buffer(MarkydEditor *self);
static void render_preview(MarkydEditor *self);
static gchar *get_preview_base_uri(MarkydEditor *self);
static gboolean on_preview_decide_policy(WebKitWebView *web_view,
WebKitPolicyDecision *decision,
WebKitPolicyDecisionType type,
gpointer user_data);
static gchar *get_url_from_iter_tags(GtkTextIter *iter) { static gchar *get_url_from_iter_tags(GtkTextIter *iter) {
GSList *tags; GSList *tags;
@@ -110,24 +120,43 @@ static gboolean scroll_to_markdown_anchor(MarkydEditor *self,
return TRUE; return TRUE;
} }
static void set_link_cursor(MarkydEditor *self, gboolean active) { static void set_editor_mouse_cursor(MarkydEditor *self, gboolean over_link) {
GdkWindow *win = gtk_text_view_get_window(GTK_TEXT_VIEW(self->text_view), GdkWindow *win;
GTK_TEXT_WINDOW_TEXT); GdkDisplay *display;
GdkCursor *cursor;
if (!self || !self->text_view) {
return;
}
win = gtk_text_view_get_window(GTK_TEXT_VIEW(self->text_view),
GTK_TEXT_WINDOW_TEXT);
if (!win) { if (!win) {
return; return;
} }
if (active) { display = gdk_window_get_display(win);
GdkDisplay *display = gdk_window_get_display(win); cursor = gdk_cursor_new_from_name(display, over_link ? "pointer" : "text");
GdkCursor *cursor = gdk_cursor_new_from_name(display, "pointer"); if (!cursor) {
if (!cursor) { cursor = gdk_cursor_new_for_display(display, over_link ? GDK_HAND2 : GDK_XTERM);
cursor = gdk_cursor_new_for_display(display, GDK_HAND2); }
} gdk_window_set_cursor(win, cursor);
gdk_window_set_cursor(win, cursor); if (cursor) {
if (cursor) { g_object_unref(cursor);
g_object_unref(cursor); }
} }
} else {
static void clear_editor_mouse_cursor(MarkydEditor *self) {
GdkWindow *win;
if (!self || !self->text_view) {
return;
}
win = gtk_text_view_get_window(GTK_TEXT_VIEW(self->text_view),
GTK_TEXT_WINDOW_TEXT);
if (win) {
gdk_window_set_cursor(win, NULL); gdk_window_set_cursor(win, NULL);
} }
} }
@@ -136,6 +165,11 @@ static void apply_markdown(MarkydEditor *self) {
if (!self) { if (!self) {
return; return;
} }
if (self->edit_mode) {
return;
}
render_preview(self);
self->updating_tags = TRUE; self->updating_tags = TRUE;
markdown_apply_tags(self->buffer, markdown_apply_tags(self->buffer,
@@ -190,16 +224,66 @@ static gboolean resolve_image_source_path(MarkydEditor *self, const gchar *src,
static gint get_image_max_width(MarkydEditor *self) { static gint get_image_max_width(MarkydEditor *self) {
GtkAllocation alloc; GtkAllocation alloc;
GdkRectangle visible = {0, 0, 0, 0};
GtkTextView *view;
gint width; gint width;
if (!self || !self->text_view) { if (!self || !self->text_view) {
return 0; return 0;
} }
gtk_widget_get_allocation(self->text_view, &alloc); view = GTK_TEXT_VIEW(self->text_view);
width = alloc.width - gtk_text_view_get_left_margin(GTK_TEXT_VIEW(self->text_view)) - gtk_text_view_get_visible_rect(view, &visible);
gtk_text_view_get_right_margin(GTK_TEXT_VIEW(self->text_view)) - 24; width = visible.width;
return MAX(width, 64);
if (width <= 0) {
gtk_widget_get_allocation(self->text_view, &alloc);
width = alloc.width;
}
width -= gtk_text_view_get_left_margin(view);
width -= gtk_text_view_get_right_margin(view);
return MAX(width, 0);
}
static void compute_image_display_size(gint orig_width, gint orig_height,
gint requested_width,
gint requested_height, gint max_width,
gint *out_width, gint *out_height) {
gint width = orig_width;
gint height = orig_height;
if (orig_width <= 0 || orig_height <= 0) {
width = MAX(requested_width, 1);
height = MAX(requested_height, 1);
} else if (requested_width > 0 && requested_height > 0) {
width = requested_width;
height = requested_height;
} else if (requested_width > 0) {
width = requested_width;
height = (gint)(((gdouble)orig_height * (gdouble)width) /
(gdouble)orig_width + 0.5);
} else if (requested_height > 0) {
height = requested_height;
width = (gint)(((gdouble)orig_width * (gdouble)height) /
(gdouble)orig_height + 0.5);
}
width = MAX(width, 1);
height = MAX(height, 1);
if (max_width > 0 && width > max_width) {
height = (gint)(((gdouble)height * (gdouble)max_width) /
(gdouble)width + 0.5);
width = max_width;
}
if (out_width) {
*out_width = MAX(width, 1);
}
if (out_height) {
*out_height = MAX(height, 1);
}
} }
static void scale_image_widget(GtkWidget *widget, gint max_width) { static void scale_image_widget(GtkWidget *widget, gint max_width) {
@@ -207,8 +291,12 @@ static void scale_image_widget(GtkWidget *widget, gint max_width) {
GdkPixbuf *orig; GdkPixbuf *orig;
gint ow; gint ow;
gint oh; gint oh;
gint requested_width;
gint requested_height;
gint display_width;
gint display_height;
if (!widget || max_width <= 0) { if (!widget) {
return; return;
} }
@@ -224,16 +312,23 @@ static void scale_image_widget(GtkWidget *widget, gint max_width) {
return; return;
} }
if (ow > max_width) { requested_width = GPOINTER_TO_INT(
gint nh = (gint)(((gdouble)oh * (gdouble)max_width) / (gdouble)ow); g_object_get_data(G_OBJECT(widget), "viewmd-image-requested-width"));
GdkPixbuf *scaled = requested_height = GPOINTER_TO_INT(
gdk_pixbuf_scale_simple(orig, max_width, MAX(nh, 1), GDK_INTERP_BILINEAR); g_object_get_data(G_OBJECT(widget), "viewmd-image-requested-height"));
gtk_image_set_from_pixbuf(GTK_IMAGE(image), scaled);
compute_image_display_size(ow, oh, requested_width, requested_height, max_width,
&display_width, &display_height);
if (display_width == ow && display_height == oh) {
gtk_image_set_from_pixbuf(GTK_IMAGE(image), orig);
} else {
GdkPixbuf *scaled = gdk_pixbuf_scale_simple(
orig, display_width, display_height, GDK_INTERP_BILINEAR);
gtk_image_set_from_pixbuf(GTK_IMAGE(image), scaled ? scaled : orig);
if (scaled) { if (scaled) {
g_object_unref(scaled); g_object_unref(scaled);
} }
} else {
gtk_image_set_from_pixbuf(GTK_IMAGE(image), orig);
} }
} }
@@ -284,6 +379,10 @@ static void render_image_widgets(MarkydEditor *self) {
g_object_get_data(G_OBJECT(anchor), VIEWMD_IMAGE_SRC_DATA); g_object_get_data(G_OBJECT(anchor), VIEWMD_IMAGE_SRC_DATA);
const gchar *alt = const gchar *alt =
g_object_get_data(G_OBJECT(anchor), VIEWMD_IMAGE_ALT_DATA); g_object_get_data(G_OBJECT(anchor), VIEWMD_IMAGE_ALT_DATA);
gint requested_width = GPOINTER_TO_INT(
g_object_get_data(G_OBJECT(anchor), VIEWMD_IMAGE_WIDTH_DATA));
gint requested_height = GPOINTER_TO_INT(
g_object_get_data(G_OBJECT(anchor), VIEWMD_IMAGE_HEIGHT_DATA));
gchar *path = NULL; gchar *path = NULL;
if (resolve_image_source_path(self, src, &path)) { if (resolve_image_source_path(self, src, &path)) {
@@ -299,6 +398,10 @@ static void render_image_widgets(MarkydEditor *self) {
image); image);
g_object_set_data_full(G_OBJECT(event_box), "viewmd-image-orig-pixbuf", g_object_set_data_full(G_OBJECT(event_box), "viewmd-image-orig-pixbuf",
orig, g_object_unref); orig, g_object_unref);
g_object_set_data(G_OBJECT(event_box), "viewmd-image-requested-width",
GINT_TO_POINTER(requested_width));
g_object_set_data(G_OBJECT(event_box), "viewmd-image-requested-height",
GINT_TO_POINTER(requested_height));
scale_image_widget(event_box, max_width); scale_image_widget(event_box, max_width);
image_widget = event_box; image_widget = event_box;
} else if (err) { } else if (err) {
@@ -367,20 +470,274 @@ static void schedule_markdown_apply(MarkydEditor *self) {
if (!self || self->updating_tags || self->markdown_idle_id != 0) { if (!self || self->updating_tags || self->markdown_idle_id != 0) {
return; return;
} }
if (self->edit_mode) {
return;
}
self->markdown_idle_id = self->markdown_idle_id =
g_idle_add_full(G_PRIORITY_LOW, apply_markdown_idle, self, NULL); g_idle_add_full(G_PRIORITY_LOW, apply_markdown_idle, self, NULL);
} }
void markyd_editor_refresh(MarkydEditor *self) { schedule_markdown_apply(self); } static void sync_source_from_buffer(MarkydEditor *self) {
GtkTextIter start;
GtkTextIter end;
gchar *text;
if (!self || !self->buffer) {
return;
}
gtk_text_buffer_get_bounds(self->buffer, &start, &end);
text = gtk_text_buffer_get_text(self->buffer, &start, &end, FALSE);
g_free(self->source_content);
self->source_content = text ? text : g_strdup("");
}
static void show_source_buffer(MarkydEditor *self) {
if (!self || !self->buffer) {
return;
}
self->updating_tags = TRUE;
gtk_text_buffer_set_text(self->buffer,
self->source_content ? self->source_content : "", -1);
self->updating_tags = FALSE;
}
static void on_buffer_changed(GtkTextBuffer *buffer, gpointer user_data) {
MarkydEditor *self = (MarkydEditor *)user_data;
(void)buffer;
if (!self || self->updating_tags || !self->edit_mode) {
return;
}
sync_source_from_buffer(self);
}
static gchar *get_preview_base_uri(MarkydEditor *self) {
const gchar *current_path;
gchar *dir;
GFile *dir_file;
gchar *uri;
gchar *base_uri;
current_path = self && self->app ? markyd_app_get_current_path(self->app) : NULL;
if (current_path && current_path[0] != '\0') {
dir = g_path_get_dirname(current_path);
} else {
dir = g_get_current_dir();
}
dir_file = g_file_new_for_path(dir);
uri = g_file_get_uri(dir_file);
base_uri = g_str_has_suffix(uri, "/") ? g_strdup(uri) : g_strconcat(uri, "/", NULL);
g_free(uri);
g_object_unref(dir_file);
g_free(dir);
return base_uri;
}
static void render_preview(MarkydEditor *self) {
gchar *base_uri;
gchar *html;
if (!self || !self->preview_view) {
return;
}
base_uri = get_preview_base_uri(self);
html = markdown_render_github_html(self->source_content ? self->source_content : "",
base_uri);
g_free(self->preview_base_uri);
self->preview_base_uri = g_strdup(base_uri);
webkit_web_view_load_html(WEBKIT_WEB_VIEW(self->preview_view), html, base_uri);
g_free(html);
g_free(base_uri);
}
static gboolean preview_uri_is_same_document(MarkydEditor *self,
const gchar *uri) {
const gchar *fragment;
if (!uri || uri[0] == '\0') {
return TRUE;
}
if (uri[0] == '#') {
return TRUE;
}
if (g_str_has_prefix(uri, "about:")) {
return TRUE;
}
fragment = strchr(uri, '#');
if (fragment && self && self->preview_base_uri &&
g_str_has_prefix(uri, self->preview_base_uri)) {
return TRUE;
}
return FALSE;
}
static gboolean path_is_markdown_file(const gchar *path) {
gchar *lower;
gboolean result;
if (!path) {
return FALSE;
}
lower = g_ascii_strdown(path, -1);
result = g_str_has_suffix(lower, ".md") ||
g_str_has_suffix(lower, ".markdown");
g_free(lower);
return result;
}
static gboolean open_local_markdown_link(MarkydEditor *self, const gchar *uri) {
gchar *path;
gchar *fragment;
gboolean opened = FALSE;
if (!self || !self->app || !uri || !g_str_has_prefix(uri, "file://")) {
return FALSE;
}
path = g_filename_from_uri(uri, NULL, NULL);
if (!path) {
return FALSE;
}
fragment = strchr(path, '#');
if (fragment) {
*fragment = '\0';
}
if (path_is_markdown_file(path) && g_file_test(path, G_FILE_TEST_EXISTS)) {
opened = markyd_app_open_file(self->app, path);
}
g_free(path);
return opened;
}
static void open_preview_uri_externally(GtkWidget *widget, const gchar *uri) {
GtkWidget *toplevel;
GError *error = NULL;
if (!uri || uri[0] == '\0') {
return;
}
toplevel = gtk_widget_get_toplevel(widget);
if (!gtk_show_uri_on_window(GTK_IS_WINDOW(toplevel) ? GTK_WINDOW(toplevel)
: NULL,
uri, GDK_CURRENT_TIME, &error)) {
if (error) {
g_printerr("Failed to open link '%s': %s\n", uri, error->message);
g_clear_error(&error);
}
}
}
static gboolean on_preview_decide_policy(WebKitWebView *web_view,
WebKitPolicyDecision *decision,
WebKitPolicyDecisionType type,
gpointer user_data) {
MarkydEditor *self = (MarkydEditor *)user_data;
WebKitNavigationPolicyDecision *navigation_decision;
WebKitNavigationAction *action;
WebKitURIRequest *request;
const gchar *uri;
if (type != WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) {
return FALSE;
}
navigation_decision = WEBKIT_NAVIGATION_POLICY_DECISION(decision);
action = webkit_navigation_policy_decision_get_navigation_action(
navigation_decision);
if (!action ||
webkit_navigation_action_get_navigation_type(action) !=
WEBKIT_NAVIGATION_TYPE_LINK_CLICKED) {
return FALSE;
}
request = webkit_navigation_action_get_request(action);
uri = request ? webkit_uri_request_get_uri(request) : NULL;
if (preview_uri_is_same_document(self, uri)) {
return FALSE;
}
if (open_local_markdown_link(self, uri)) {
webkit_policy_decision_ignore(decision);
return TRUE;
}
open_preview_uri_externally(GTK_WIDGET(web_view), uri);
webkit_policy_decision_ignore(decision);
return TRUE;
}
void markyd_editor_refresh(MarkydEditor *self) {
if (!self) {
return;
}
if (self->edit_mode) {
return;
}
schedule_markdown_apply(self);
}
void markyd_editor_set_edit_mode(MarkydEditor *self, gboolean edit_mode) {
if (!self || self->edit_mode == edit_mode) {
return;
}
if (edit_mode) {
if (self->markdown_idle_id != 0) {
g_source_remove(self->markdown_idle_id);
self->markdown_idle_id = 0;
}
self->edit_mode = TRUE;
show_source_buffer(self);
gtk_text_view_set_editable(GTK_TEXT_VIEW(self->text_view), TRUE);
gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(self->text_view), TRUE);
gtk_stack_set_visible_child_name(GTK_STACK(self->widget), "source");
gtk_widget_grab_focus(self->text_view);
} else {
sync_source_from_buffer(self);
self->edit_mode = FALSE;
gtk_text_view_set_editable(GTK_TEXT_VIEW(self->text_view), FALSE);
gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(self->text_view), FALSE);
gtk_stack_set_visible_child_name(GTK_STACK(self->widget), "preview");
schedule_markdown_apply(self);
}
}
gboolean markyd_editor_get_edit_mode(MarkydEditor *self) {
return self ? self->edit_mode : FALSE;
}
MarkydEditor *markyd_editor_new(MarkydApp *app) { MarkydEditor *markyd_editor_new(MarkydApp *app) {
MarkydEditor *self = g_new0(MarkydEditor, 1); MarkydEditor *self = g_new0(MarkydEditor, 1);
WebKitSettings *settings;
self->app = app; self->app = app;
self->source_content = g_strdup(""); self->source_content = g_strdup("");
self->updating_tags = FALSE; self->updating_tags = FALSE;
self->markdown_idle_id = 0; self->markdown_idle_id = 0;
self->edit_mode = FALSE;
self->preview_base_uri = NULL;
self->widget = gtk_stack_new();
gtk_stack_set_transition_type(GTK_STACK(self->widget),
GTK_STACK_TRANSITION_TYPE_NONE);
gtk_widget_set_hexpand(self->widget, TRUE);
gtk_widget_set_vexpand(self->widget, TRUE);
self->text_view = gtk_text_view_new(); self->text_view = gtk_text_view_new();
gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(self->text_view), gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(self->text_view),
@@ -392,8 +749,28 @@ MarkydEditor *markyd_editor_new(MarkydApp *app) {
gtk_text_view_set_editable(GTK_TEXT_VIEW(self->text_view), FALSE); gtk_text_view_set_editable(GTK_TEXT_VIEW(self->text_view), FALSE);
gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(self->text_view), FALSE); gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(self->text_view), FALSE);
self->source_scroll = gtk_scrolled_window_new(NULL, NULL);
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(self->source_scroll),
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
gtk_container_add(GTK_CONTAINER(self->source_scroll), self->text_view);
self->preview_view = webkit_web_view_new();
gtk_widget_set_hexpand(self->preview_view, TRUE);
gtk_widget_set_vexpand(self->preview_view, TRUE);
settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(self->preview_view));
webkit_settings_set_enable_javascript(settings, TRUE);
webkit_settings_set_enable_javascript_markup(settings, FALSE);
webkit_settings_set_auto_load_images(settings, TRUE);
g_signal_connect(self->preview_view, "decide-policy",
G_CALLBACK(on_preview_decide_policy), self);
gtk_stack_add_named(GTK_STACK(self->widget), self->preview_view, "preview");
gtk_stack_add_named(GTK_STACK(self->widget), self->source_scroll, "source");
gtk_stack_set_visible_child_name(GTK_STACK(self->widget), "preview");
self->buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(self->text_view)); self->buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(self->text_view));
markdown_init_tags(self->buffer); markdown_init_tags(self->buffer);
g_signal_connect(self->buffer, "changed", G_CALLBACK(on_buffer_changed), self);
gtk_widget_add_events(self->text_view, GDK_POINTER_MOTION_MASK | gtk_widget_add_events(self->text_view, GDK_POINTER_MOTION_MASK |
GDK_LEAVE_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK |
@@ -427,6 +804,7 @@ void markyd_editor_free(MarkydEditor *self) {
self->markdown_idle_id = 0; self->markdown_idle_id = 0;
} }
g_free(self->source_content); g_free(self->source_content);
g_free(self->preview_base_uri);
g_free(self); g_free(self);
} }
@@ -437,24 +815,42 @@ void markyd_editor_set_content(MarkydEditor *self, const gchar *content) {
g_free(self->source_content); g_free(self->source_content);
self->source_content = g_strdup(content ? content : ""); self->source_content = g_strdup(content ? content : "");
schedule_markdown_apply(self); if (self->edit_mode) {
show_source_buffer(self);
} else {
schedule_markdown_apply(self);
}
} }
gchar *markyd_editor_get_content(MarkydEditor *self) { gchar *markyd_editor_get_content(MarkydEditor *self) {
if (!self) { if (!self) {
return g_strdup(""); return g_strdup("");
} }
if (self->edit_mode) {
sync_source_from_buffer(self);
}
return g_strdup(self->source_content ? self->source_content : ""); return g_strdup(self->source_content ? self->source_content : "");
} }
GtkWidget *markyd_editor_get_widget(MarkydEditor *self) { GtkWidget *markyd_editor_get_widget(MarkydEditor *self) {
return self->text_view; return self ? self->widget : NULL;
} }
void markyd_editor_focus(MarkydEditor *self) { void markyd_editor_focus(MarkydEditor *self) {
GtkTextMark *insert_mark; GtkTextMark *insert_mark;
if (!self || !self->text_view || !self->buffer) { if (!self) {
return;
}
if (!self->edit_mode) {
if (self->preview_view) {
gtk_widget_grab_focus(self->preview_view);
}
return;
}
if (!self->text_view || !self->buffer) {
return; return;
} }
@@ -466,6 +862,74 @@ void markyd_editor_focus(MarkydEditor *self) {
} }
} }
void markyd_editor_scroll_top(MarkydEditor *self) {
GtkTextIter start;
if (!self) {
return;
}
if (!self->edit_mode && self->preview_view) {
webkit_web_view_evaluate_javascript(WEBKIT_WEB_VIEW(self->preview_view),
"window.scrollTo(0, 0);", -1, NULL,
NULL, NULL, NULL, NULL);
return;
}
if (!self->buffer || !self->text_view) {
return;
}
gtk_text_buffer_get_start_iter(self->buffer, &start);
gtk_text_buffer_place_cursor(self->buffer, &start);
gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(self->text_view), &start, 0.0,
FALSE, 0.0, 0.0);
}
void markyd_editor_find(MarkydEditor *self, const gchar *query) {
WebKitFindController *controller;
if (!self || self->edit_mode || !self->preview_view || !query ||
query[0] == '\0') {
return;
}
controller = webkit_web_view_get_find_controller(
WEBKIT_WEB_VIEW(self->preview_view));
webkit_find_controller_search(controller, query,
WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE |
WEBKIT_FIND_OPTIONS_WRAP_AROUND,
G_MAXUINT);
}
void markyd_editor_find_next(MarkydEditor *self, gboolean previous) {
WebKitFindController *controller;
if (!self || self->edit_mode || !self->preview_view) {
return;
}
controller = webkit_web_view_get_find_controller(
WEBKIT_WEB_VIEW(self->preview_view));
if (previous) {
webkit_find_controller_search_previous(controller);
} else {
webkit_find_controller_search_next(controller);
}
}
void markyd_editor_find_clear(MarkydEditor *self) {
WebKitFindController *controller;
if (!self || !self->preview_view) {
return;
}
controller = webkit_web_view_get_find_controller(
WEBKIT_WEB_VIEW(self->preview_view));
webkit_find_controller_search_finish(controller);
}
static gboolean on_button_release(GtkWidget *widget, GdkEventButton *event, static gboolean on_button_release(GtkWidget *widget, GdkEventButton *event,
gpointer user_data) { gpointer user_data) {
MarkydEditor *self = (MarkydEditor *)user_data; MarkydEditor *self = (MarkydEditor *)user_data;
@@ -533,7 +997,7 @@ static gboolean on_motion_notify(GtkWidget *widget, GdkEventMotion *event,
(gint)event->y, &bx, &by); (gint)event->y, &bx, &by);
gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(widget), &iter, bx, by); gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(widget), &iter, bx, by);
set_link_cursor(self, get_link_url_at_iter(self->buffer, &iter, &url)); set_editor_mouse_cursor(self, get_link_url_at_iter(self->buffer, &iter, &url));
g_free(url); g_free(url);
return FALSE; return FALSE;
} }
@@ -543,6 +1007,6 @@ static gboolean on_leave_notify(GtkWidget *widget, GdkEventCrossing *event,
MarkydEditor *self = (MarkydEditor *)user_data; MarkydEditor *self = (MarkydEditor *)user_data;
(void)widget; (void)widget;
(void)event; (void)event;
set_link_cursor(self, FALSE); clear_editor_mouse_cursor(self);
return FALSE; return FALSE;
} }
+18
View File
@@ -6,7 +6,10 @@
typedef struct _MarkydApp MarkydApp; typedef struct _MarkydApp MarkydApp;
typedef struct _MarkydEditor { typedef struct _MarkydEditor {
GtkWidget *widget;
GtkWidget *source_scroll;
GtkWidget *text_view; GtkWidget *text_view;
GtkWidget *preview_view;
GtkTextBuffer *buffer; GtkTextBuffer *buffer;
MarkydApp *app; MarkydApp *app;
@@ -18,6 +21,11 @@ typedef struct _MarkydEditor {
/* Coalesce markdown re-rendering to idle to avoid invalidating GTK iterators. */ /* Coalesce markdown re-rendering to idle to avoid invalidating GTK iterators. */
guint markdown_idle_id; guint markdown_idle_id;
/* TRUE when the buffer is showing editable markdown source instead of preview. */
gboolean edit_mode;
gchar *preview_base_uri;
} MarkydEditor; } MarkydEditor;
/* Lifecycle */ /* Lifecycle */
@@ -28,9 +36,19 @@ void markyd_editor_free(MarkydEditor *editor);
void markyd_editor_set_content(MarkydEditor *editor, const gchar *content); void markyd_editor_set_content(MarkydEditor *editor, const gchar *content);
gchar *markyd_editor_get_content(MarkydEditor *editor); gchar *markyd_editor_get_content(MarkydEditor *editor);
/* Source editing mode */
void markyd_editor_set_edit_mode(MarkydEditor *editor, gboolean edit_mode);
gboolean markyd_editor_get_edit_mode(MarkydEditor *editor);
/* Widget access */ /* Widget access */
GtkWidget *markyd_editor_get_widget(MarkydEditor *editor); GtkWidget *markyd_editor_get_widget(MarkydEditor *editor);
void markyd_editor_focus(MarkydEditor *editor); void markyd_editor_focus(MarkydEditor *editor);
void markyd_editor_scroll_top(MarkydEditor *editor);
/* Preview search */
void markyd_editor_find(MarkydEditor *editor, const gchar *query);
void markyd_editor_find_next(MarkydEditor *editor, gboolean previous);
void markyd_editor_find_clear(MarkydEditor *editor);
/* Force a refresh of markdown styling/rendering (e.g., after settings change). */ /* Force a refresh of markdown styling/rendering (e.g., after settings change). */
void markyd_editor_refresh(MarkydEditor *editor); void markyd_editor_refresh(MarkydEditor *editor);
+2067
View File
File diff suppressed because it is too large. Load diff
+5
View File
@@ -36,6 +36,8 @@ void markdown_update_accent_tags(GtkTextBuffer *buffer);
#define VIEWMD_IMAGE_SRC_DATA "viewmd-image-src" #define VIEWMD_IMAGE_SRC_DATA "viewmd-image-src"
#define VIEWMD_IMAGE_ALT_DATA "viewmd-image-alt" #define VIEWMD_IMAGE_ALT_DATA "viewmd-image-alt"
#define VIEWMD_IMAGE_WIDGET_DATA "viewmd-image-widget" #define VIEWMD_IMAGE_WIDGET_DATA "viewmd-image-widget"
#define VIEWMD_IMAGE_WIDTH_DATA "viewmd-image-width"
#define VIEWMD_IMAGE_HEIGHT_DATA "viewmd-image-height"
typedef struct { typedef struct {
gint row; gint row;
@@ -59,6 +61,9 @@ gchar *markdown_anchor_mark_name(const gchar *fragment);
/* Render markdown source into the buffer and apply markdown styling. */ /* Render markdown source into the buffer and apply markdown styling. */
void markdown_apply_tags(GtkTextBuffer *buffer, const gchar *source); void markdown_apply_tags(GtkTextBuffer *buffer, const gchar *source);
/* Render markdown source to a complete GitHub-style HTML document. */
gchar *markdown_render_github_html(const gchar *source, const gchar *base_uri);
/* Build a GTK widget for a table anchor, or NULL if not a table anchor. */ /* Build a GTK widget for a table anchor, or NULL if not a table anchor. */
GtkWidget *markdown_create_table_widget(GtkTextChildAnchor *anchor); GtkWidget *markdown_create_table_widget(GtkTextChildAnchor *anchor);
+321 -23
View File
@@ -14,10 +14,14 @@ typedef struct {
#define TAG_SEARCH_MATCH "viewmd_search_match" #define TAG_SEARCH_MATCH "viewmd_search_match"
#define TAG_SEARCH_CURRENT "viewmd_search_current" #define TAG_SEARCH_CURRENT "viewmd_search_current"
#define TOOLBAR_LABEL_DATA "viewmd-toolbar-label"
static void on_open_clicked(GtkButton *button, gpointer user_data); static void on_open_clicked(GtkButton *button, gpointer user_data);
static void on_refresh_clicked(GtkButton *button, gpointer user_data); static void on_refresh_clicked(GtkButton *button, gpointer user_data);
static void on_edit_clicked(GtkButton *button, gpointer user_data);
static void on_save_clicked(GtkButton *button, gpointer user_data);
static void on_settings_clicked(GtkButton *button, gpointer user_data); static void on_settings_clicked(GtkButton *button, gpointer user_data);
static void on_about_clicked(GtkButton *button, gpointer user_data);
static void on_search_changed(GtkEditable *editable, gpointer user_data); static void on_search_changed(GtkEditable *editable, gpointer user_data);
static void on_search_prev_clicked(GtkButton *button, 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 void on_search_next_clicked(GtkButton *button, gpointer user_data);
@@ -62,6 +66,13 @@ static gchar *rgba_to_hex(const GdkRGBA *rgba);
static void init_color_button(GtkColorButton *btn, const gchar *color_str); static void init_color_button(GtkColorButton *btn, const gchar *color_str);
static void on_color_set(GtkColorButton *btn, gpointer user_data); static void on_color_set(GtkColorButton *btn, gpointer user_data);
static GtkWidget *create_settings_dialog(MarkydApp *app); static GtkWidget *create_settings_dialog(MarkydApp *app);
static gboolean prompt_save_as(MarkydWindow *self);
static void update_edit_button_state(MarkydWindow *self);
static void set_edit_mode(MarkydWindow *self, gboolean edit_mode);
static GtkWidget *create_toolbar_button(const gchar *icon_name, const gchar *label,
const gchar *tooltip);
static void set_toolbar_button_label(GtkWidget *button, const gchar *label);
static void restore_editor_focus(MarkydWindow *self);
static gboolean geometry_debug_enabled(void) { static gboolean geometry_debug_enabled(void) {
const gchar *v = g_getenv("VIEWMD_DEBUG_GEOMETRY"); const gchar *v = g_getenv("VIEWMD_DEBUG_GEOMETRY");
@@ -128,6 +139,8 @@ static void clear_search_matches(MarkydWindow *self) {
return; return;
} }
markyd_editor_find_clear(self->editor);
gtk_text_buffer_get_bounds(self->editor->buffer, &start, &end); gtk_text_buffer_get_bounds(self->editor->buffer, &start, &end);
gtk_text_buffer_remove_tag_by_name(self->editor->buffer, TAG_SEARCH_MATCH, gtk_text_buffer_remove_tag_by_name(self->editor->buffer, TAG_SEARCH_MATCH,
&start, &end); &start, &end);
@@ -480,6 +493,14 @@ static void update_search_matches(MarkydWindow *self) {
return; return;
} }
if (!markyd_editor_get_edit_mode(self->editor)) {
markyd_editor_find(self->editor, query);
gtk_widget_set_sensitive(self->btn_search_prev, TRUE);
gtk_widget_set_sensitive(self->btn_search_next, TRUE);
gtk_label_set_text(GTK_LABEL(self->lbl_search_status), "Preview search");
return;
}
ensure_search_tags(self); ensure_search_tags(self);
gtk_text_buffer_get_start_iter(self->editor->buffer, &iter); gtk_text_buffer_get_start_iter(self->editor->buffer, &iter);
gtk_text_buffer_get_end_iter(self->editor->buffer, &end); gtk_text_buffer_get_end_iter(self->editor->buffer, &end);
@@ -548,6 +569,11 @@ static void on_search_prev_clicked(GtkButton *button, gpointer user_data) {
(void)button; (void)button;
if (!self || !self->search_matches || self->search_matches->len == 0) { if (!self || !self->search_matches || self->search_matches->len == 0) {
if (self && self->editor && !markyd_editor_get_edit_mode(self->editor) &&
self->search_entry &&
gtk_entry_get_text_length(GTK_ENTRY(self->search_entry)) > 0) {
markyd_editor_find_next(self->editor, TRUE);
}
return; return;
} }
@@ -565,6 +591,11 @@ static void on_search_next_clicked(GtkButton *button, gpointer user_data) {
(void)button; (void)button;
if (!self || !self->search_matches || self->search_matches->len == 0) { if (!self || !self->search_matches || self->search_matches->len == 0) {
if (self && self->editor && !markyd_editor_get_edit_mode(self->editor) &&
self->search_entry &&
gtk_entry_get_text_length(GTK_ENTRY(self->search_entry)) > 0) {
markyd_editor_find_next(self->editor, FALSE);
}
return; return;
} }
@@ -624,9 +655,108 @@ static void on_editor_buffer_changed(GtkTextBuffer *buffer, gpointer user_data)
update_search_matches(self); update_search_matches(self);
} }
static GtkWidget *create_toolbar_button_internal(const gchar *icon_name,
const gchar *label,
const gchar *tooltip,
gboolean toggle) {
GtkWidget *button;
GtkWidget *box;
GtkWidget *image;
GtkWidget *label_widget;
button = toggle ? gtk_toggle_button_new() : gtk_button_new();
box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
image = gtk_image_new_from_icon_name(icon_name, GTK_ICON_SIZE_BUTTON);
label_widget = gtk_label_new(label);
gtk_widget_set_halign(box, GTK_ALIGN_CENTER);
gtk_widget_set_valign(box, GTK_ALIGN_CENTER);
gtk_box_pack_start(GTK_BOX(box), image, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(box), label_widget, FALSE, FALSE, 0);
gtk_container_add(GTK_CONTAINER(button), box);
gtk_widget_set_focus_on_click(button, FALSE);
if (tooltip) {
gtk_widget_set_tooltip_text(button, tooltip);
}
g_object_set_data(G_OBJECT(button), TOOLBAR_LABEL_DATA, label_widget);
return button;
}
static GtkWidget *create_toolbar_button(const gchar *icon_name, const gchar *label,
const gchar *tooltip) {
return create_toolbar_button_internal(icon_name, label, tooltip, FALSE);
}
static void set_toolbar_button_label(GtkWidget *button, const gchar *label) {
GtkWidget *label_widget;
if (!button || !label) {
return;
}
label_widget = g_object_get_data(G_OBJECT(button), TOOLBAR_LABEL_DATA);
if (GTK_IS_LABEL(label_widget)) {
gtk_label_set_text(GTK_LABEL(label_widget), label);
}
}
static void restore_editor_focus(MarkydWindow *self) {
if (self && self->editor) {
markyd_editor_focus(self->editor);
}
}
static void on_about_popover_closed(GtkPopover *popover, gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data;
restore_editor_focus(self);
gtk_widget_destroy(GTK_WIDGET(popover));
}
static void on_about_clicked(GtkButton *button, gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data;
GtkWidget *popover;
GtkWidget *box;
GtkWidget *title;
GtkWidget *summary;
GtkWidget *details;
popover = gtk_popover_new(GTK_WIDGET(button));
gtk_popover_set_position(GTK_POPOVER(popover), GTK_POS_BOTTOM);
gtk_container_set_border_width(GTK_CONTAINER(popover), 12);
g_signal_connect(popover, "closed", G_CALLBACK(on_about_popover_closed), self);
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
gtk_widget_set_size_request(box, 300, -1);
gtk_container_add(GTK_CONTAINER(popover), box);
title = gtk_label_new(NULL);
gtk_label_set_markup(GTK_LABEL(title), "<b>ViewMD</b>");
gtk_label_set_xalign(GTK_LABEL(title), 0.0f);
gtk_box_pack_start(GTK_BOX(box), title, FALSE, FALSE, 0);
summary = gtk_label_new("A lightweight GTK markdown viewer and editor.");
gtk_label_set_xalign(GTK_LABEL(summary), 0.0f);
gtk_label_set_line_wrap(GTK_LABEL(summary), TRUE);
gtk_box_pack_start(GTK_BOX(box), summary, FALSE, FALSE, 0);
details = gtk_label_new(
"This is a fork of ViewMD by NotAlexNoyle, with additional features, "
"including GitHub-style preview rendering, HTML-in-Markdown support, "
"editing, saving, toolbar labels, and improved image sizing.");
gtk_label_set_xalign(GTK_LABEL(details), 0.0f);
gtk_label_set_line_wrap(GTK_LABEL(details), TRUE);
gtk_label_set_max_width_chars(GTK_LABEL(details), 42);
gtk_box_pack_start(GTK_BOX(box), details, FALSE, FALSE, 0);
gtk_widget_show_all(popover);
gtk_popover_popup(GTK_POPOVER(popover));
}
MarkydWindow *markyd_window_new(MarkydApp *app) { MarkydWindow *markyd_window_new(MarkydApp *app) {
MarkydWindow *self = g_new0(MarkydWindow, 1); MarkydWindow *self = g_new0(MarkydWindow, 1);
GtkWidget *left_buttons; GtkWidget *toolbar_buttons;
GtkWidget *main_box; GtkWidget *main_box;
GtkWidget *search_box; GtkWidget *search_box;
@@ -664,33 +794,52 @@ MarkydWindow *markyd_window_new(MarkydApp *app) {
gtk_header_bar_set_show_close_button(GTK_HEADER_BAR(self->header_bar), TRUE); gtk_header_bar_set_show_close_button(GTK_HEADER_BAR(self->header_bar), TRUE);
gtk_window_set_titlebar(GTK_WINDOW(self->window), self->header_bar); gtk_window_set_titlebar(GTK_WINDOW(self->window), self->header_bar);
self->lbl_title = gtk_label_new("ViewMD"); self->lbl_title = gtk_button_new_with_label("ViewMD");
gtk_widget_set_halign(self->lbl_title, GTK_ALIGN_CENTER); gtk_button_set_relief(GTK_BUTTON(self->lbl_title), GTK_RELIEF_NONE);
gtk_header_bar_set_custom_title(GTK_HEADER_BAR(self->header_bar), gtk_widget_set_focus_on_click(self->lbl_title, FALSE);
self->lbl_title); gtk_widget_set_tooltip_text(self->lbl_title, "About ViewMD");
gtk_widget_set_halign(self->lbl_title, GTK_ALIGN_END);
gtk_widget_set_margin_start(self->lbl_title, 12);
gtk_widget_set_margin_end(self->lbl_title, 12);
g_signal_connect(self->lbl_title, "clicked", G_CALLBACK(on_about_clicked),
self);
self->btn_open = self->btn_open =
gtk_button_new_from_icon_name("document-open-symbolic", GTK_ICON_SIZE_BUTTON); create_toolbar_button("document-open-symbolic", "Open",
gtk_widget_set_tooltip_text(self->btn_open, "Open Markdown Document"); "Open Markdown Document");
g_signal_connect(self->btn_open, "clicked", G_CALLBACK(on_open_clicked), self); g_signal_connect(self->btn_open, "clicked", G_CALLBACK(on_open_clicked), self);
self->btn_refresh = self->btn_refresh =
gtk_button_new_from_icon_name("view-refresh-symbolic", GTK_ICON_SIZE_BUTTON); create_toolbar_button("view-refresh-symbolic", "Reload",
gtk_widget_set_tooltip_text(self->btn_refresh, "Reload Current Document"); "Reload Current Document");
g_signal_connect(self->btn_refresh, "clicked", G_CALLBACK(on_refresh_clicked), g_signal_connect(self->btn_refresh, "clicked", G_CALLBACK(on_refresh_clicked),
self); self);
self->btn_edit =
create_toolbar_button("accessories-text-editor-symbolic", "Edit",
"Edit Markdown Source");
g_signal_connect(self->btn_edit, "clicked", G_CALLBACK(on_edit_clicked), self);
self->btn_save =
create_toolbar_button("document-save-symbolic", "Save",
"Save Markdown Document");
g_signal_connect(self->btn_save, "clicked", G_CALLBACK(on_save_clicked), self);
self->btn_settings = self->btn_settings =
gtk_button_new_from_icon_name("emblem-system-symbolic", GTK_ICON_SIZE_BUTTON); create_toolbar_button("emblem-system-symbolic", "Settings", "Settings");
gtk_widget_set_tooltip_text(self->btn_settings, "Settings");
g_signal_connect(self->btn_settings, "clicked", G_CALLBACK(on_settings_clicked), g_signal_connect(self->btn_settings, "clicked", G_CALLBACK(on_settings_clicked),
self); self);
left_buttons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); toolbar_buttons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_box_pack_start(GTK_BOX(left_buttons), self->btn_open, FALSE, FALSE, 0); gtk_widget_set_halign(toolbar_buttons, GTK_ALIGN_CENTER);
gtk_box_pack_start(GTK_BOX(left_buttons), self->btn_refresh, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(toolbar_buttons), self->btn_open, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(left_buttons), self->btn_settings, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(toolbar_buttons), self->btn_refresh, FALSE, FALSE, 0);
gtk_header_bar_pack_start(GTK_HEADER_BAR(self->header_bar), left_buttons); gtk_box_pack_start(GTK_BOX(toolbar_buttons), self->btn_edit, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(toolbar_buttons), self->btn_save, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(toolbar_buttons), self->btn_settings, FALSE, FALSE, 0);
gtk_header_bar_set_custom_title(GTK_HEADER_BAR(self->header_bar),
toolbar_buttons);
gtk_header_bar_pack_end(GTK_HEADER_BAR(self->header_bar), self->lbl_title);
main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_container_add(GTK_CONTAINER(self->window), main_box); gtk_container_add(GTK_CONTAINER(self->window), main_box);
@@ -737,14 +886,10 @@ MarkydWindow *markyd_window_new(MarkydApp *app) {
gtk_widget_set_halign(self->lbl_search_status, GTK_ALIGN_END); 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); 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_box_pack_start(GTK_BOX(main_box), self->scroll, TRUE, TRUE, 0);
self->editor = markyd_editor_new(app); self->editor = markyd_editor_new(app);
gtk_container_add(GTK_CONTAINER(self->scroll), self->scroll = self->editor->source_scroll;
markyd_editor_get_widget(self->editor)); gtk_box_pack_start(GTK_BOX(main_box), markyd_editor_get_widget(self->editor),
TRUE, TRUE, 0);
self->search_matches = g_array_new(FALSE, FALSE, sizeof(SearchMatch)); self->search_matches = g_array_new(FALSE, FALSE, sizeof(SearchMatch));
self->search_current_index = -1; self->search_current_index = -1;
g_signal_connect(self->editor->buffer, "changed", g_signal_connect(self->editor->buffer, "changed",
@@ -918,6 +1063,94 @@ gboolean markyd_window_is_visible(MarkydWindow *self) {
return gtk_widget_get_visible(self->window); return gtk_widget_get_visible(self->window);
} }
static void update_edit_button_state(MarkydWindow *self) {
gboolean editing;
if (!self || !self->editor || !self->btn_edit) {
return;
}
editing = markyd_editor_get_edit_mode(self->editor);
set_toolbar_button_label(self->btn_edit, editing ? "Preview" : "Edit");
gtk_widget_set_tooltip_text(self->btn_edit,
editing ? "Preview Markdown" : "Edit Markdown Source");
}
static void set_edit_mode(MarkydWindow *self, gboolean edit_mode) {
if (!self || !self->editor) {
return;
}
markyd_editor_set_edit_mode(self->editor, edit_mode);
update_edit_button_state(self);
markyd_editor_focus(self->editor);
if (self->search_revealer &&
gtk_revealer_get_reveal_child(GTK_REVEALER(self->search_revealer)) &&
self->search_entry &&
gtk_entry_get_text_length(GTK_ENTRY(self->search_entry)) > 0) {
update_search_matches(self);
}
}
static gboolean prompt_save_as(MarkydWindow *self) {
GtkWidget *dialog;
GtkFileFilter *md_filter;
gint response;
gboolean saved = FALSE;
if (!self || !self->window) {
return FALSE;
}
dialog = gtk_file_chooser_dialog_new(
"Save Markdown Document", GTK_WINDOW(self->window),
GTK_FILE_CHOOSER_ACTION_SAVE, "_Cancel", GTK_RESPONSE_CANCEL, "_Save",
GTK_RESPONSE_ACCEPT, NULL);
gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog), TRUE);
md_filter = gtk_file_filter_new();
gtk_file_filter_set_name(md_filter, "Markdown files (*.md, *.markdown)");
gtk_file_filter_add_pattern(md_filter, "*.md");
gtk_file_filter_add_pattern(md_filter, "*.markdown");
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), md_filter);
md_filter = gtk_file_filter_new();
gtk_file_filter_set_name(md_filter, "All files");
gtk_file_filter_add_pattern(md_filter, "*");
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), md_filter);
{
const gchar *current = markyd_app_get_current_path(self->app);
if (current && current[0] != '\0') {
gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dialog), current);
} else {
gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), "untitled.md");
}
}
response = gtk_dialog_run(GTK_DIALOG(dialog));
if (response == GTK_RESPONSE_ACCEPT) {
gchar *path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
if (path) {
saved = markyd_app_save_file(self->app, path);
if (!saved) {
GtkWidget *error_dialog = gtk_message_dialog_new(
GTK_WINDOW(self->window),
GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR,
GTK_BUTTONS_CLOSE, "Failed to save document");
gtk_message_dialog_format_secondary_text(
GTK_MESSAGE_DIALOG(error_dialog), "%s", path);
gtk_dialog_run(GTK_DIALOG(error_dialog));
gtk_widget_destroy(error_dialog);
}
g_free(path);
}
}
gtk_widget_destroy(dialog);
return saved;
}
static void on_open_clicked(GtkButton *button, gpointer user_data) { static void on_open_clicked(GtkButton *button, gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data; MarkydWindow *self = (MarkydWindow *)user_data;
GtkWidget *dialog; GtkWidget *dialog;
@@ -961,6 +1194,7 @@ static void on_open_clicked(GtkButton *button, gpointer user_data) {
} }
gtk_widget_destroy(dialog); gtk_widget_destroy(dialog);
restore_editor_focus(self);
} }
static void on_refresh_clicked(GtkButton *button, gpointer user_data) { static void on_refresh_clicked(GtkButton *button, gpointer user_data) {
@@ -972,6 +1206,7 @@ static void on_refresh_clicked(GtkButton *button, gpointer user_data) {
current = markyd_app_get_current_path(self->app); current = markyd_app_get_current_path(self->app);
if (!current || current[0] == '\0') { if (!current || current[0] == '\0') {
restore_editor_focus(self);
return; return;
} }
@@ -987,6 +1222,45 @@ static void on_refresh_clicked(GtkButton *button, gpointer user_data) {
gtk_widget_destroy(error_dialog); gtk_widget_destroy(error_dialog);
} }
g_free(path); g_free(path);
restore_editor_focus(self);
}
static void on_edit_clicked(GtkButton *button, gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data;
(void)button;
if (!self || !self->editor) {
return;
}
set_edit_mode(self, !markyd_editor_get_edit_mode(self->editor));
}
static void on_save_clicked(GtkButton *button, gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data;
const gchar *current;
(void)button;
if (!self || !self->app) {
return;
}
current = markyd_app_get_current_path(self->app);
if (!current || current[0] == '\0') {
prompt_save_as(self);
restore_editor_focus(self);
return;
}
if (!markyd_app_save_current_file(self->app)) {
GtkWidget *error_dialog = gtk_message_dialog_new(
GTK_WINDOW(self->window), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "Failed to save document");
gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(error_dialog),
"%s", current);
gtk_dialog_run(GTK_DIALOG(error_dialog));
gtk_widget_destroy(error_dialog);
}
restore_editor_focus(self);
} }
static void on_settings_clicked(GtkButton *button, gpointer user_data) { static void on_settings_clicked(GtkButton *button, gpointer user_data) {
@@ -1007,6 +1281,7 @@ static void on_settings_clicked(GtkButton *button, gpointer user_data) {
} }
gtk_widget_destroy(dialog); gtk_widget_destroy(dialog);
restore_editor_focus(self);
} }
static gboolean on_key_press_event(GtkWidget *widget, GdkEventKey *event, static gboolean on_key_press_event(GtkWidget *widget, GdkEventKey *event,
@@ -1023,6 +1298,18 @@ static gboolean on_key_press_event(GtkWidget *widget, GdkEventKey *event,
return TRUE; return TRUE;
} }
if ((event->state & GDK_CONTROL_MASK) != 0 &&
(event->keyval == GDK_KEY_e || event->keyval == GDK_KEY_E)) {
set_edit_mode(self, !markyd_editor_get_edit_mode(self->editor));
return TRUE;
}
if ((event->state & GDK_CONTROL_MASK) != 0 &&
(event->keyval == GDK_KEY_s || event->keyval == GDK_KEY_S)) {
on_save_clicked(NULL, self);
return TRUE;
}
if (event->keyval == GDK_KEY_Escape && if (event->keyval == GDK_KEY_Escape &&
self->search_revealer && self->search_revealer &&
gtk_revealer_get_reveal_child(GTK_REVEALER(self->search_revealer))) { gtk_revealer_get_reveal_child(GTK_REVEALER(self->search_revealer))) {
@@ -1030,12 +1317,23 @@ static gboolean on_key_press_event(GtkWidget *widget, GdkEventKey *event,
return TRUE; return TRUE;
} }
if (event->keyval == GDK_KEY_Escape && self->editor &&
markyd_editor_get_edit_mode(self->editor)) {
set_edit_mode(self, FALSE);
return TRUE;
}
if (event->keyval == GDK_KEY_Escape) { if (event->keyval == GDK_KEY_Escape) {
gtk_window_close(GTK_WINDOW(widget)); gtk_window_close(GTK_WINDOW(widget));
return TRUE; return TRUE;
} }
if (event->keyval == GDK_KEY_Home || event->keyval == GDK_KEY_KP_Home) { if (event->keyval == GDK_KEY_Home || event->keyval == GDK_KEY_KP_Home) {
if (self->editor && !markyd_editor_get_edit_mode(self->editor)) {
markyd_editor_scroll_top(self->editor);
return TRUE;
}
if (self->editor && self->editor->buffer && self->editor->text_view) { if (self->editor && self->editor->buffer && self->editor->text_view) {
GtkTextIter start; GtkTextIter start;
GtkAdjustment *hadj = NULL; GtkAdjustment *hadj = NULL;
+2
View File
@@ -11,6 +11,8 @@ typedef struct _MarkydWindow {
GtkWidget *header_bar; GtkWidget *header_bar;
GtkWidget *btn_open; GtkWidget *btn_open;
GtkWidget *btn_refresh; GtkWidget *btn_refresh;
GtkWidget *btn_edit;
GtkWidget *btn_save;
GtkWidget *btn_settings; GtkWidget *btn_settings;
GtkWidget *search_revealer; GtkWidget *search_revealer;
GtkWidget *search_entry; GtkWidget *search_entry;
Executable
BIN
View File
Binary file not shown.