ViewMD -> SeeMD
This commit is contained in:
1 parent
effa685762
commit
e27c392078
23 files changed
+2994
-79
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.
|
||||
@@ -1,6 +1,7 @@
|
||||
CC = gcc
|
||||
CFLAGS = -Wall -Wextra -O2 -g `pkg-config --cflags gtk+-3.0`
|
||||
LDFLAGS = `pkg-config --libs gtk+-3.0`
|
||||
PKGS = gtk+-3.0 webkit2gtk-4.1
|
||||
CFLAGS = -Wall -Wextra -O2 -g `pkg-config --cflags $(PKGS)`
|
||||
LDFLAGS = `pkg-config --libs $(PKGS)`
|
||||
|
||||
SRCDIR = src
|
||||
OBJDIR = obj
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
# 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.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- **Native GTK viewer** - No webviews
|
||||
- **Read-only rendering** - Focused on viewing markdown files
|
||||
- **Minimal UI** - Clean toolbar with open and settings buttons
|
||||
- **Lightweight** - Pure C, no web technologies, fast startup
|
||||
- **GitHub-style preview** - Rendered Markdown and inline HTML are displayed through WebKitGTK with GitHub-like markdown styling
|
||||
- **Read-only preview** - Rendered Markdown stays non-editable until source mode is enabled
|
||||
- **Source editing mode** - Toggle between rendered preview and editable Markdown
|
||||
- **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
|
||||
- **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
|
||||
|
||||
@@ -30,6 +31,7 @@ A lightweight GTK 3 markdown viewer for desktop Linux. It is ideal as your defau
|
||||
| `[text](url)` | Link |
|
||||
| `~~strike~~` | Strikethrough |
|
||||
| `\| table \| row \|` | Markdown tables |
|
||||
| `<strong>HTML</strong>` | GitHub-style inline and block HTML rendered in preview mode |
|
||||
| `---` | Horizontal rule |
|
||||
|
||||
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
|
||||
- **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
|
||||
|
||||
### Find in Document
|
||||
@@ -113,17 +117,17 @@ sudo make uninstall
|
||||
|
||||
### Arch Linux
|
||||
```bash
|
||||
sudo pacman -S gtk3
|
||||
sudo pacman -S gtk3 webkit2gtk-4.1
|
||||
```
|
||||
|
||||
### Ubuntu/Debian
|
||||
```bash
|
||||
sudo apt install libgtk-3-dev
|
||||
sudo apt install libgtk-3-dev libwebkit2gtk-4.1-dev
|
||||
```
|
||||
|
||||
### Fedora
|
||||
```bash
|
||||
sudo dnf install gtk3-devel
|
||||
sudo dnf install gtk3-devel webkit2gtk4.1-devel
|
||||
```
|
||||
|
||||
## Packaging Templates
|
||||
@@ -133,12 +137,4 @@ sudo dnf install gtk3-devel
|
||||
- RPM: `packaging/rpm/viewmd.spec.in`
|
||||
## License
|
||||
|
||||
MIT 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)
|
||||
Released under the [MIT License](LICENSE).
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -5,7 +5,7 @@ pkgdesc="Lightweight GTK3 markdown viewer"
|
||||
arch=('x86_64')
|
||||
url="https://github.com/rabfulton/ViewMD"
|
||||
license=('MIT')
|
||||
depends=('gtk3')
|
||||
depends=('gtk3' 'webkit2gtk-4.1')
|
||||
makedepends=('git' 'gcc' 'make' 'pkgconf')
|
||||
source=("$pkgname::git+$url.git")
|
||||
sha256sums=('SKIP')
|
||||
|
||||
@@ -4,6 +4,6 @@ Section: utils
|
||||
Priority: optional
|
||||
Architecture: @ARCH@
|
||||
Maintainer: @MAINTAINER@
|
||||
Depends: libgtk-3-0
|
||||
Depends: libgtk-3-0, libwebkit2gtk-4.1-0
|
||||
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.
|
||||
@@ -11,11 +11,13 @@ BuildRequires: gcc
|
||||
BuildRequires: make
|
||||
BuildRequires: pkgconfig
|
||||
BuildRequires: gtk3-devel
|
||||
BuildRequires: webkit2gtk4.1-devel
|
||||
|
||||
Requires: gtk3
|
||||
Requires: webkit2gtk4.1
|
||||
|
||||
%description
|
||||
ViewMD is a lightweight native GTK3 markdown viewer.
|
||||
ViewMD is a lightweight GTK3 markdown viewer with GitHub-style WebKit preview rendering.
|
||||
|
||||
%prep
|
||||
%autosetup -n viewmd-%{version}
|
||||
|
||||
@@ -156,6 +156,45 @@ gboolean markyd_app_open_file(MarkydApp *self, const gchar *path) {
|
||||
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) {
|
||||
if (!self) {
|
||||
return NULL;
|
||||
|
||||
@@ -25,6 +25,8 @@ int markyd_app_run(MarkydApp *app, int argc, char **argv);
|
||||
|
||||
/* Document management */
|
||||
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 */
|
||||
const gchar *markyd_app_get_current_path(MarkydApp *app);
|
||||
|
||||
+496
-32
@@ -2,6 +2,7 @@
|
||||
#include "app.h"
|
||||
#include "markdown.h"
|
||||
#include <string.h>
|
||||
#include <webkit2/webkit2.h>
|
||||
|
||||
static gboolean on_button_release(GtkWidget *widget, GdkEventButton *event,
|
||||
gpointer user_data);
|
||||
@@ -18,6 +19,15 @@ 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 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) {
|
||||
GSList *tags;
|
||||
@@ -110,24 +120,43 @@ static gboolean scroll_to_markdown_anchor(MarkydEditor *self,
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void set_link_cursor(MarkydEditor *self, gboolean active) {
|
||||
GdkWindow *win = gtk_text_view_get_window(GTK_TEXT_VIEW(self->text_view),
|
||||
GTK_TEXT_WINDOW_TEXT);
|
||||
static void set_editor_mouse_cursor(MarkydEditor *self, gboolean over_link) {
|
||||
GdkWindow *win;
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (active) {
|
||||
GdkDisplay *display = gdk_window_get_display(win);
|
||||
GdkCursor *cursor = gdk_cursor_new_from_name(display, "pointer");
|
||||
if (!cursor) {
|
||||
cursor = gdk_cursor_new_for_display(display, GDK_HAND2);
|
||||
}
|
||||
gdk_window_set_cursor(win, cursor);
|
||||
if (cursor) {
|
||||
g_object_unref(cursor);
|
||||
}
|
||||
} else {
|
||||
display = gdk_window_get_display(win);
|
||||
cursor = gdk_cursor_new_from_name(display, over_link ? "pointer" : "text");
|
||||
if (!cursor) {
|
||||
cursor = gdk_cursor_new_for_display(display, over_link ? GDK_HAND2 : GDK_XTERM);
|
||||
}
|
||||
gdk_window_set_cursor(win, cursor);
|
||||
if (cursor) {
|
||||
g_object_unref(cursor);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -136,6 +165,11 @@ static void apply_markdown(MarkydEditor *self) {
|
||||
if (!self) {
|
||||
return;
|
||||
}
|
||||
if (self->edit_mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
render_preview(self);
|
||||
|
||||
self->updating_tags = TRUE;
|
||||
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) {
|
||||
GtkAllocation alloc;
|
||||
GdkRectangle visible = {0, 0, 0, 0};
|
||||
GtkTextView *view;
|
||||
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);
|
||||
view = GTK_TEXT_VIEW(self->text_view);
|
||||
gtk_text_view_get_visible_rect(view, &visible);
|
||||
width = visible.width;
|
||||
|
||||
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) {
|
||||
@@ -207,8 +291,12 @@ static void scale_image_widget(GtkWidget *widget, gint max_width) {
|
||||
GdkPixbuf *orig;
|
||||
gint ow;
|
||||
gint oh;
|
||||
gint requested_width;
|
||||
gint requested_height;
|
||||
gint display_width;
|
||||
gint display_height;
|
||||
|
||||
if (!widget || max_width <= 0) {
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -224,16 +312,23 @@ static void scale_image_widget(GtkWidget *widget, gint max_width) {
|
||||
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);
|
||||
requested_width = GPOINTER_TO_INT(
|
||||
g_object_get_data(G_OBJECT(widget), "viewmd-image-requested-width"));
|
||||
requested_height = GPOINTER_TO_INT(
|
||||
g_object_get_data(G_OBJECT(widget), "viewmd-image-requested-height"));
|
||||
|
||||
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) {
|
||||
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);
|
||||
const gchar *alt =
|
||||
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;
|
||||
|
||||
if (resolve_image_source_path(self, src, &path)) {
|
||||
@@ -299,6 +398,10 @@ static void render_image_widgets(MarkydEditor *self) {
|
||||
image);
|
||||
g_object_set_data_full(G_OBJECT(event_box), "viewmd-image-orig-pixbuf",
|
||||
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);
|
||||
image_widget = event_box;
|
||||
} else if (err) {
|
||||
@@ -367,20 +470,274 @@ static void schedule_markdown_apply(MarkydEditor *self) {
|
||||
if (!self || self->updating_tags || self->markdown_idle_id != 0) {
|
||||
return;
|
||||
}
|
||||
if (self->edit_mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
self->markdown_idle_id =
|
||||
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 *self = g_new0(MarkydEditor, 1);
|
||||
WebKitSettings *settings;
|
||||
|
||||
self->app = app;
|
||||
self->source_content = g_strdup("");
|
||||
self->updating_tags = FALSE;
|
||||
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();
|
||||
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_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));
|
||||
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 |
|
||||
GDK_LEAVE_NOTIFY_MASK |
|
||||
@@ -427,6 +804,7 @@ void markyd_editor_free(MarkydEditor *self) {
|
||||
self->markdown_idle_id = 0;
|
||||
}
|
||||
g_free(self->source_content);
|
||||
g_free(self->preview_base_uri);
|
||||
g_free(self);
|
||||
}
|
||||
|
||||
@@ -437,24 +815,42 @@ void markyd_editor_set_content(MarkydEditor *self, const gchar *content) {
|
||||
|
||||
g_free(self->source_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) {
|
||||
if (!self) {
|
||||
return g_strdup("");
|
||||
}
|
||||
if (self->edit_mode) {
|
||||
sync_source_from_buffer(self);
|
||||
}
|
||||
return g_strdup(self->source_content ? self->source_content : "");
|
||||
}
|
||||
|
||||
GtkWidget *markyd_editor_get_widget(MarkydEditor *self) {
|
||||
return self->text_view;
|
||||
return self ? self->widget : NULL;
|
||||
}
|
||||
|
||||
void markyd_editor_focus(MarkydEditor *self) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
gpointer 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);
|
||||
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);
|
||||
return FALSE;
|
||||
}
|
||||
@@ -543,6 +1007,6 @@ static gboolean on_leave_notify(GtkWidget *widget, GdkEventCrossing *event,
|
||||
MarkydEditor *self = (MarkydEditor *)user_data;
|
||||
(void)widget;
|
||||
(void)event;
|
||||
set_link_cursor(self, FALSE);
|
||||
clear_editor_mouse_cursor(self);
|
||||
return FALSE;
|
||||
}
|
||||
@@ -6,7 +6,10 @@
|
||||
typedef struct _MarkydApp MarkydApp;
|
||||
|
||||
typedef struct _MarkydEditor {
|
||||
GtkWidget *widget;
|
||||
GtkWidget *source_scroll;
|
||||
GtkWidget *text_view;
|
||||
GtkWidget *preview_view;
|
||||
GtkTextBuffer *buffer;
|
||||
MarkydApp *app;
|
||||
|
||||
@@ -18,6 +21,11 @@ typedef struct _MarkydEditor {
|
||||
|
||||
/* Coalesce markdown re-rendering to idle to avoid invalidating GTK iterators. */
|
||||
guint markdown_idle_id;
|
||||
|
||||
/* TRUE when the buffer is showing editable markdown source instead of preview. */
|
||||
gboolean edit_mode;
|
||||
|
||||
gchar *preview_base_uri;
|
||||
} MarkydEditor;
|
||||
|
||||
/* Lifecycle */
|
||||
@@ -28,9 +36,19 @@ void markyd_editor_free(MarkydEditor *editor);
|
||||
void markyd_editor_set_content(MarkydEditor *editor, const gchar *content);
|
||||
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 */
|
||||
GtkWidget *markyd_editor_get_widget(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). */
|
||||
void markyd_editor_refresh(MarkydEditor *editor);
|
||||
|
||||
+2067
File diff suppressed because it is too large.
Load diff
@@ -36,6 +36,8 @@ void markdown_update_accent_tags(GtkTextBuffer *buffer);
|
||||
#define VIEWMD_IMAGE_SRC_DATA "viewmd-image-src"
|
||||
#define VIEWMD_IMAGE_ALT_DATA "viewmd-image-alt"
|
||||
#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 {
|
||||
gint row;
|
||||
@@ -59,6 +61,9 @@ gchar *markdown_anchor_mark_name(const gchar *fragment);
|
||||
/* Render markdown source into the buffer and apply markdown styling. */
|
||||
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. */
|
||||
GtkWidget *markdown_create_table_widget(GtkTextChildAnchor *anchor);
|
||||
|
||||
|
||||
+321
-23
@@ -14,10 +14,14 @@ typedef struct {
|
||||
|
||||
#define TAG_SEARCH_MATCH "viewmd_search_match"
|
||||
#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_refresh_clicked(GtkButton *button, gpointer user_data);
|
||||
static void on_edit_clicked(GtkButton *button, gpointer user_data);
|
||||
static void on_save_clicked(GtkButton *button, gpointer user_data);
|
||||
static void on_settings_clicked(GtkButton *button, gpointer user_data);
|
||||
static void on_about_clicked(GtkButton *button, gpointer user_data);
|
||||
static void on_search_changed(GtkEditable *editable, gpointer user_data);
|
||||
static void on_search_prev_clicked(GtkButton *button, gpointer user_data);
|
||||
static void on_search_next_clicked(GtkButton *button, gpointer user_data);
|
||||
@@ -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 on_color_set(GtkColorButton *btn, gpointer user_data);
|
||||
static GtkWidget *create_settings_dialog(MarkydApp *app);
|
||||
static gboolean prompt_save_as(MarkydWindow *self);
|
||||
static void update_edit_button_state(MarkydWindow *self);
|
||||
static void set_edit_mode(MarkydWindow *self, gboolean edit_mode);
|
||||
static GtkWidget *create_toolbar_button(const gchar *icon_name, const gchar *label,
|
||||
const gchar *tooltip);
|
||||
static void set_toolbar_button_label(GtkWidget *button, const gchar *label);
|
||||
static void restore_editor_focus(MarkydWindow *self);
|
||||
|
||||
static gboolean geometry_debug_enabled(void) {
|
||||
const gchar *v = g_getenv("VIEWMD_DEBUG_GEOMETRY");
|
||||
@@ -128,6 +139,8 @@ static void clear_search_matches(MarkydWindow *self) {
|
||||
return;
|
||||
}
|
||||
|
||||
markyd_editor_find_clear(self->editor);
|
||||
|
||||
gtk_text_buffer_get_bounds(self->editor->buffer, &start, &end);
|
||||
gtk_text_buffer_remove_tag_by_name(self->editor->buffer, TAG_SEARCH_MATCH,
|
||||
&start, &end);
|
||||
@@ -480,6 +493,14 @@ static void update_search_matches(MarkydWindow *self) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!markyd_editor_get_edit_mode(self->editor)) {
|
||||
markyd_editor_find(self->editor, query);
|
||||
gtk_widget_set_sensitive(self->btn_search_prev, TRUE);
|
||||
gtk_widget_set_sensitive(self->btn_search_next, TRUE);
|
||||
gtk_label_set_text(GTK_LABEL(self->lbl_search_status), "Preview search");
|
||||
return;
|
||||
}
|
||||
|
||||
ensure_search_tags(self);
|
||||
gtk_text_buffer_get_start_iter(self->editor->buffer, &iter);
|
||||
gtk_text_buffer_get_end_iter(self->editor->buffer, &end);
|
||||
@@ -548,6 +569,11 @@ static void on_search_prev_clicked(GtkButton *button, gpointer user_data) {
|
||||
(void)button;
|
||||
|
||||
if (!self || !self->search_matches || self->search_matches->len == 0) {
|
||||
if (self && self->editor && !markyd_editor_get_edit_mode(self->editor) &&
|
||||
self->search_entry &&
|
||||
gtk_entry_get_text_length(GTK_ENTRY(self->search_entry)) > 0) {
|
||||
markyd_editor_find_next(self->editor, TRUE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -565,6 +591,11 @@ static void on_search_next_clicked(GtkButton *button, gpointer user_data) {
|
||||
(void)button;
|
||||
|
||||
if (!self || !self->search_matches || self->search_matches->len == 0) {
|
||||
if (self && self->editor && !markyd_editor_get_edit_mode(self->editor) &&
|
||||
self->search_entry &&
|
||||
gtk_entry_get_text_length(GTK_ENTRY(self->search_entry)) > 0) {
|
||||
markyd_editor_find_next(self->editor, FALSE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -624,9 +655,108 @@ static void on_editor_buffer_changed(GtkTextBuffer *buffer, gpointer user_data)
|
||||
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 *self = g_new0(MarkydWindow, 1);
|
||||
GtkWidget *left_buttons;
|
||||
GtkWidget *toolbar_buttons;
|
||||
GtkWidget *main_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_window_set_titlebar(GTK_WINDOW(self->window), self->header_bar);
|
||||
|
||||
self->lbl_title = gtk_label_new("ViewMD");
|
||||
gtk_widget_set_halign(self->lbl_title, GTK_ALIGN_CENTER);
|
||||
gtk_header_bar_set_custom_title(GTK_HEADER_BAR(self->header_bar),
|
||||
self->lbl_title);
|
||||
self->lbl_title = gtk_button_new_with_label("ViewMD");
|
||||
gtk_button_set_relief(GTK_BUTTON(self->lbl_title), GTK_RELIEF_NONE);
|
||||
gtk_widget_set_focus_on_click(self->lbl_title, FALSE);
|
||||
gtk_widget_set_tooltip_text(self->lbl_title, "About 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 =
|
||||
gtk_button_new_from_icon_name("document-open-symbolic", GTK_ICON_SIZE_BUTTON);
|
||||
gtk_widget_set_tooltip_text(self->btn_open, "Open Markdown Document");
|
||||
create_toolbar_button("document-open-symbolic", "Open",
|
||||
"Open Markdown Document");
|
||||
g_signal_connect(self->btn_open, "clicked", G_CALLBACK(on_open_clicked), self);
|
||||
|
||||
self->btn_refresh =
|
||||
gtk_button_new_from_icon_name("view-refresh-symbolic", GTK_ICON_SIZE_BUTTON);
|
||||
gtk_widget_set_tooltip_text(self->btn_refresh, "Reload Current Document");
|
||||
create_toolbar_button("view-refresh-symbolic", "Reload",
|
||||
"Reload Current Document");
|
||||
g_signal_connect(self->btn_refresh, "clicked", G_CALLBACK(on_refresh_clicked),
|
||||
self);
|
||||
|
||||
self->btn_edit =
|
||||
create_toolbar_button("accessories-text-editor-symbolic", "Edit",
|
||||
"Edit Markdown Source");
|
||||
g_signal_connect(self->btn_edit, "clicked", G_CALLBACK(on_edit_clicked), self);
|
||||
|
||||
self->btn_save =
|
||||
create_toolbar_button("document-save-symbolic", "Save",
|
||||
"Save Markdown Document");
|
||||
g_signal_connect(self->btn_save, "clicked", G_CALLBACK(on_save_clicked), self);
|
||||
|
||||
self->btn_settings =
|
||||
gtk_button_new_from_icon_name("emblem-system-symbolic", GTK_ICON_SIZE_BUTTON);
|
||||
gtk_widget_set_tooltip_text(self->btn_settings, "Settings");
|
||||
create_toolbar_button("emblem-system-symbolic", "Settings", "Settings");
|
||||
g_signal_connect(self->btn_settings, "clicked", G_CALLBACK(on_settings_clicked),
|
||||
self);
|
||||
|
||||
left_buttons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_box_pack_start(GTK_BOX(left_buttons), self->btn_open, FALSE, FALSE, 0);
|
||||
gtk_box_pack_start(GTK_BOX(left_buttons), self->btn_refresh, FALSE, FALSE, 0);
|
||||
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);
|
||||
toolbar_buttons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_widget_set_halign(toolbar_buttons, GTK_ALIGN_CENTER);
|
||||
gtk_box_pack_start(GTK_BOX(toolbar_buttons), self->btn_open, FALSE, FALSE, 0);
|
||||
gtk_box_pack_start(GTK_BOX(toolbar_buttons), self->btn_refresh, FALSE, FALSE, 0);
|
||||
gtk_box_pack_start(GTK_BOX(toolbar_buttons), self->btn_edit, FALSE, FALSE, 0);
|
||||
gtk_box_pack_start(GTK_BOX(toolbar_buttons), self->btn_save, FALSE, FALSE, 0);
|
||||
gtk_box_pack_start(GTK_BOX(toolbar_buttons), self->btn_settings, FALSE, FALSE, 0);
|
||||
gtk_header_bar_set_custom_title(GTK_HEADER_BAR(self->header_bar),
|
||||
toolbar_buttons);
|
||||
gtk_header_bar_pack_end(GTK_HEADER_BAR(self->header_bar), self->lbl_title);
|
||||
|
||||
main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_container_add(GTK_CONTAINER(self->window), main_box);
|
||||
@@ -737,14 +886,10 @@ MarkydWindow *markyd_window_new(MarkydApp *app) {
|
||||
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_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->scroll = self->editor->source_scroll;
|
||||
gtk_box_pack_start(GTK_BOX(main_box), markyd_editor_get_widget(self->editor),
|
||||
TRUE, TRUE, 0);
|
||||
self->search_matches = g_array_new(FALSE, FALSE, sizeof(SearchMatch));
|
||||
self->search_current_index = -1;
|
||||
g_signal_connect(self->editor->buffer, "changed",
|
||||
@@ -918,6 +1063,94 @@ gboolean markyd_window_is_visible(MarkydWindow *self) {
|
||||
return gtk_widget_get_visible(self->window);
|
||||
}
|
||||
|
||||
static void update_edit_button_state(MarkydWindow *self) {
|
||||
gboolean editing;
|
||||
|
||||
if (!self || !self->editor || !self->btn_edit) {
|
||||
return;
|
||||
}
|
||||
|
||||
editing = markyd_editor_get_edit_mode(self->editor);
|
||||
set_toolbar_button_label(self->btn_edit, editing ? "Preview" : "Edit");
|
||||
gtk_widget_set_tooltip_text(self->btn_edit,
|
||||
editing ? "Preview Markdown" : "Edit Markdown Source");
|
||||
}
|
||||
|
||||
static void set_edit_mode(MarkydWindow *self, gboolean edit_mode) {
|
||||
if (!self || !self->editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
markyd_editor_set_edit_mode(self->editor, edit_mode);
|
||||
update_edit_button_state(self);
|
||||
markyd_editor_focus(self->editor);
|
||||
if (self->search_revealer &&
|
||||
gtk_revealer_get_reveal_child(GTK_REVEALER(self->search_revealer)) &&
|
||||
self->search_entry &&
|
||||
gtk_entry_get_text_length(GTK_ENTRY(self->search_entry)) > 0) {
|
||||
update_search_matches(self);
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean prompt_save_as(MarkydWindow *self) {
|
||||
GtkWidget *dialog;
|
||||
GtkFileFilter *md_filter;
|
||||
gint response;
|
||||
gboolean saved = FALSE;
|
||||
|
||||
if (!self || !self->window) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
dialog = gtk_file_chooser_dialog_new(
|
||||
"Save Markdown Document", GTK_WINDOW(self->window),
|
||||
GTK_FILE_CHOOSER_ACTION_SAVE, "_Cancel", GTK_RESPONSE_CANCEL, "_Save",
|
||||
GTK_RESPONSE_ACCEPT, NULL);
|
||||
gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog), TRUE);
|
||||
|
||||
md_filter = gtk_file_filter_new();
|
||||
gtk_file_filter_set_name(md_filter, "Markdown files (*.md, *.markdown)");
|
||||
gtk_file_filter_add_pattern(md_filter, "*.md");
|
||||
gtk_file_filter_add_pattern(md_filter, "*.markdown");
|
||||
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), md_filter);
|
||||
|
||||
md_filter = gtk_file_filter_new();
|
||||
gtk_file_filter_set_name(md_filter, "All files");
|
||||
gtk_file_filter_add_pattern(md_filter, "*");
|
||||
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), md_filter);
|
||||
|
||||
{
|
||||
const gchar *current = markyd_app_get_current_path(self->app);
|
||||
if (current && current[0] != '\0') {
|
||||
gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dialog), current);
|
||||
} else {
|
||||
gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), "untitled.md");
|
||||
}
|
||||
}
|
||||
|
||||
response = gtk_dialog_run(GTK_DIALOG(dialog));
|
||||
if (response == GTK_RESPONSE_ACCEPT) {
|
||||
gchar *path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
|
||||
if (path) {
|
||||
saved = markyd_app_save_file(self->app, path);
|
||||
if (!saved) {
|
||||
GtkWidget *error_dialog = gtk_message_dialog_new(
|
||||
GTK_WINDOW(self->window),
|
||||
GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR,
|
||||
GTK_BUTTONS_CLOSE, "Failed to save document");
|
||||
gtk_message_dialog_format_secondary_text(
|
||||
GTK_MESSAGE_DIALOG(error_dialog), "%s", path);
|
||||
gtk_dialog_run(GTK_DIALOG(error_dialog));
|
||||
gtk_widget_destroy(error_dialog);
|
||||
}
|
||||
g_free(path);
|
||||
}
|
||||
}
|
||||
|
||||
gtk_widget_destroy(dialog);
|
||||
return saved;
|
||||
}
|
||||
|
||||
static void on_open_clicked(GtkButton *button, gpointer user_data) {
|
||||
MarkydWindow *self = (MarkydWindow *)user_data;
|
||||
GtkWidget *dialog;
|
||||
@@ -961,6 +1194,7 @@ static void on_open_clicked(GtkButton *button, gpointer user_data) {
|
||||
}
|
||||
|
||||
gtk_widget_destroy(dialog);
|
||||
restore_editor_focus(self);
|
||||
}
|
||||
|
||||
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);
|
||||
if (!current || current[0] == '\0') {
|
||||
restore_editor_focus(self);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -987,6 +1222,45 @@ static void on_refresh_clicked(GtkButton *button, gpointer user_data) {
|
||||
gtk_widget_destroy(error_dialog);
|
||||
}
|
||||
g_free(path);
|
||||
restore_editor_focus(self);
|
||||
}
|
||||
|
||||
static void on_edit_clicked(GtkButton *button, gpointer user_data) {
|
||||
MarkydWindow *self = (MarkydWindow *)user_data;
|
||||
(void)button;
|
||||
if (!self || !self->editor) {
|
||||
return;
|
||||
}
|
||||
set_edit_mode(self, !markyd_editor_get_edit_mode(self->editor));
|
||||
}
|
||||
|
||||
static void on_save_clicked(GtkButton *button, gpointer user_data) {
|
||||
MarkydWindow *self = (MarkydWindow *)user_data;
|
||||
const gchar *current;
|
||||
|
||||
(void)button;
|
||||
|
||||
if (!self || !self->app) {
|
||||
return;
|
||||
}
|
||||
|
||||
current = markyd_app_get_current_path(self->app);
|
||||
if (!current || current[0] == '\0') {
|
||||
prompt_save_as(self);
|
||||
restore_editor_focus(self);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!markyd_app_save_current_file(self->app)) {
|
||||
GtkWidget *error_dialog = gtk_message_dialog_new(
|
||||
GTK_WINDOW(self->window), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
|
||||
GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "Failed to save document");
|
||||
gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(error_dialog),
|
||||
"%s", current);
|
||||
gtk_dialog_run(GTK_DIALOG(error_dialog));
|
||||
gtk_widget_destroy(error_dialog);
|
||||
}
|
||||
restore_editor_focus(self);
|
||||
}
|
||||
|
||||
static void on_settings_clicked(GtkButton *button, gpointer user_data) {
|
||||
@@ -1007,6 +1281,7 @@ static void on_settings_clicked(GtkButton *button, gpointer user_data) {
|
||||
}
|
||||
|
||||
gtk_widget_destroy(dialog);
|
||||
restore_editor_focus(self);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if ((event->state & GDK_CONTROL_MASK) != 0 &&
|
||||
(event->keyval == GDK_KEY_e || event->keyval == GDK_KEY_E)) {
|
||||
set_edit_mode(self, !markyd_editor_get_edit_mode(self->editor));
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if ((event->state & GDK_CONTROL_MASK) != 0 &&
|
||||
(event->keyval == GDK_KEY_s || event->keyval == GDK_KEY_S)) {
|
||||
on_save_clicked(NULL, self);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (event->keyval == GDK_KEY_Escape &&
|
||||
self->search_revealer &&
|
||||
gtk_revealer_get_reveal_child(GTK_REVEALER(self->search_revealer))) {
|
||||
@@ -1030,12 +1317,23 @@ static gboolean on_key_press_event(GtkWidget *widget, GdkEventKey *event,
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (event->keyval == GDK_KEY_Escape && self->editor &&
|
||||
markyd_editor_get_edit_mode(self->editor)) {
|
||||
set_edit_mode(self, FALSE);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (event->keyval == GDK_KEY_Escape) {
|
||||
gtk_window_close(GTK_WINDOW(widget));
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (event->keyval == GDK_KEY_Home || event->keyval == GDK_KEY_KP_Home) {
|
||||
if (self->editor && !markyd_editor_get_edit_mode(self->editor)) {
|
||||
markyd_editor_scroll_top(self->editor);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (self->editor && self->editor->buffer && self->editor->text_view) {
|
||||
GtkTextIter start;
|
||||
GtkAdjustment *hadj = NULL;
|
||||
|
||||
@@ -11,6 +11,8 @@ typedef struct _MarkydWindow {
|
||||
GtkWidget *header_bar;
|
||||
GtkWidget *btn_open;
|
||||
GtkWidget *btn_refresh;
|
||||
GtkWidget *btn_edit;
|
||||
GtkWidget *btn_save;
|
||||
GtkWidget *btn_settings;
|
||||
GtkWidget *search_revealer;
|
||||
GtkWidget *search_entry;
|
||||
|
||||
Reference in new issue
Block a user