1 /*
2 * plugin.c - Part of the Geany Markdown plugin
3 *
4 * Copyright 2012 Matthew Brush <mbrush@codebrainz.ca>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 * MA 02110-1301, USA.
20 */
21
22 #include "config.h"
23 #include <geanyplugin.h>
24 #include "viewer.h"
25 #include "conf.h"
26
27 GeanyData *geany_data;
28 GeanyPlugin *geany_plugin;
29
30 PLUGIN_VERSION_CHECK(224)
31
32 PLUGIN_SET_TRANSLATABLE_INFO(LOCALEDIR, GETTEXT_PACKAGE,
33 "Markdown",
34 _("Real-time Markdown preview"),
35 "0.01",
36 "Matthew Brush <mbrush@codebrainz.ca>")
37
38 /* Should be defined by build system, this is just a fallback */
39 #ifndef MARKDOWN_DOC_DIR
40 # define MARKDOWN_DOC_DIR "/usr/local/share/doc/geany-plugins/markdown"
41 #endif
42 #ifndef MARKDOWN_HELP_FILE
43 # define MARKDOWN_HELP_FILE MARKDOWN_DOC_DIR "/html/help.html"
44 #endif
45
46 #define MARKDOWN_PREVIEW_LABEL _("Markdown Preview")
47
48 /* Global data */
49 static MarkdownViewer *g_viewer = NULL;
50 static GtkWidget *g_scrolled_win = NULL;
51 static GtkWidget *g_export_html = NULL;
52
53 /* Forward declarations */
54 static void update_markdown_viewer(MarkdownViewer *viewer);
55 static gboolean on_editor_notify(GObject *obj, GeanyEditor *editor, SCNotification *notif, MarkdownViewer *viewer);
56 static void on_document_signal(GObject *obj, GeanyDocument *doc, MarkdownViewer *viewer);
57 static void on_document_filetype_set(GObject *obj, GeanyDocument *doc, GeanyFiletype *ft_old, MarkdownViewer *viewer);
58 static void on_view_pos_notify(GObject *obj, GParamSpec *pspec, MarkdownViewer *viewer);
59 static void on_export_as_html_activate(GtkMenuItem *item, MarkdownViewer *viewer);
60
61 /* Main plugin entry point on plugin load. */
plugin_init(GeanyData * data)62 void plugin_init(GeanyData *data)
63 {
64 gint page_num;
65 gchar *conf_fn;
66 MarkdownConfig *conf;
67 MarkdownConfigViewPos view_pos;
68 GtkWidget *viewer;
69 GtkNotebook *nb;
70
71 /* Setup the config object which is needed by the view. */
72 conf_fn = g_build_filename(geany->app->configdir, "plugins", "markdown",
73 "markdown.conf", NULL);
74 conf = markdown_config_new(conf_fn);
75 g_free(conf_fn);
76
77 viewer = markdown_viewer_new(conf);
78 /* store as global for plugin_cleanup() */
79 g_viewer = MARKDOWN_VIEWER(viewer);
80 view_pos = markdown_config_get_view_pos(conf);
81
82 g_scrolled_win = gtk_scrolled_window_new(NULL, NULL);
83 gtk_container_add(GTK_CONTAINER(g_scrolled_win), viewer);
84 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(g_scrolled_win),
85 GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
86
87 if (view_pos == MARKDOWN_CONFIG_VIEW_POS_MSGWIN) {
88 nb = GTK_NOTEBOOK(geany->main_widgets->message_window_notebook);
89 page_num = gtk_notebook_append_page(nb,
90 g_scrolled_win, gtk_label_new(MARKDOWN_PREVIEW_LABEL));
91 } else {
92 nb = GTK_NOTEBOOK(geany->main_widgets->sidebar_notebook);
93 page_num = gtk_notebook_append_page(nb,
94 g_scrolled_win, gtk_label_new(MARKDOWN_PREVIEW_LABEL));
95 }
96
97 gtk_widget_show_all(g_scrolled_win);
98 gtk_notebook_set_current_page(nb, page_num);
99
100 g_signal_connect(conf, "notify::view-pos", G_CALLBACK(on_view_pos_notify), viewer);
101
102 g_export_html = gtk_menu_item_new_with_label(_("Export Markdown as HTML..."));
103 gtk_menu_shell_append(GTK_MENU_SHELL(data->main_widgets->tools_menu), g_export_html);
104 g_signal_connect(g_export_html, "activate", G_CALLBACK(on_export_as_html_activate), viewer);
105 gtk_widget_show(g_export_html);
106
107 #define MD_PSC(sig, cb) \
108 plugin_signal_connect(geany_plugin, NULL, (sig), TRUE, G_CALLBACK(cb), viewer)
109 /* Geany takes care of disconnecting these for us when the plugin is unloaded,
110 * the macro is just to make the code smaller/clearer. */
111 MD_PSC("editor-notify", on_editor_notify);
112 MD_PSC("document-activate", on_document_signal);
113 MD_PSC("document-filetype-set", on_document_filetype_set);
114 MD_PSC("document-new", on_document_signal);
115 MD_PSC("document-open", on_document_signal);
116 MD_PSC("document-reload", on_document_signal);
117 #undef MD_PSC
118
119 /* Prevent segfault in plugin when it registers GTypes and gets unloaded
120 * and when reloaded tries to re-register the GTypes. */
121 plugin_module_make_resident(geany_plugin);
122
123 update_markdown_viewer(MARKDOWN_VIEWER(viewer));
124 }
125
126 /* Cleanup resources on plugin unload. */
plugin_cleanup(void)127 void plugin_cleanup(void)
128 {
129 gtk_widget_destroy(g_export_html);
130 gtk_widget_destroy(g_scrolled_win);
131 }
132
133 /* Called to show the preferences GUI. */
plugin_configure(GtkDialog * dialog)134 GtkWidget *plugin_configure(GtkDialog *dialog)
135 {
136 MarkdownConfig *conf = NULL;
137 g_object_get(g_viewer, "config", &conf, NULL);
138 return markdown_config_gui(conf, dialog);
139 }
140
141 /* Called to show the plugin's help */
plugin_help(void)142 void plugin_help(void)
143 {
144 #ifdef G_OS_WIN32
145 gchar *prefix = g_win32_get_package_installation_directory_of_module(NULL);
146 #else
147 gchar *prefix = NULL;
148 #endif
149 gchar *uri = g_strconcat("file://", prefix ? prefix : "", MARKDOWN_HELP_FILE, NULL);
150
151 utils_open_browser(uri);
152
153 g_free(uri);
154 g_free(prefix);
155 }
156
157 /* All of the various signal handlers call this function to update the
158 * MarkdownViewer on specific events. This causes a bunch of memory
159 * allocations, re-compiles the Markdown to HTML, reformats the HTML
160 * template, copies the HTML into the webview and causes it to (eventually)
161 * be redrawn. Only call it when really needed, like when the scintilla
162 * editor's text contents change and not on other editor events.
163 */
164 static void
update_markdown_viewer(MarkdownViewer * viewer)165 update_markdown_viewer(MarkdownViewer *viewer)
166 {
167 GeanyDocument *doc = document_get_current();
168
169 if (DOC_VALID(doc) && g_strcmp0(doc->file_type->name, "Markdown") == 0) {
170 gchar *text;
171 text = (gchar*) scintilla_send_message(doc->editor->sci, SCI_GETCHARACTERPOINTER, 0, 0);
172 markdown_viewer_set_markdown(viewer, text, doc->encoding);
173 gtk_widget_set_sensitive(g_export_html, TRUE);
174 } else {
175 markdown_viewer_set_markdown(viewer,
176 _("The current document does not have a Markdown filetype."), "UTF-8");
177 gtk_widget_set_sensitive(g_export_html, FALSE);
178 }
179
180 markdown_viewer_queue_update(viewer);
181 }
182
183 /* Return TRUE if event is a buffer modification that inserts or deletes
184 * text and which caused a text changed length greater than 0. */
185 #define IS_MOD_NOTIF(nt) (nt->nmhdr.code == SCN_MODIFIED && \
186 nt->length > 0 && ( \
187 (nt->modificationType & SC_MOD_INSERTTEXT) || \
188 (nt->modificationType & SC_MOD_DELETETEXT)))
189
190 /* Queue update of the markdown preview on editor text change. */
on_editor_notify(GObject * obj,GeanyEditor * editor,SCNotification * notif,MarkdownViewer * viewer)191 static gboolean on_editor_notify(GObject *obj, GeanyEditor *editor,
192 SCNotification *notif, MarkdownViewer *viewer)
193 {
194 if (IS_MOD_NOTIF(notif)) {
195 update_markdown_viewer(viewer);
196 }
197 return FALSE; /* Allow others to handle this event too */
198 }
199
200 /* Queue update of the markdown preview on document signals (new, open,
201 * activate, etc.) */
on_document_signal(GObject * obj,GeanyDocument * doc,MarkdownViewer * viewer)202 static void on_document_signal(GObject *obj, GeanyDocument *doc, MarkdownViewer *viewer)
203 {
204 update_markdown_viewer(viewer);
205 }
206
207 /* Queue update of the markdown preview when a document's filetype is set */
on_document_filetype_set(GObject * obj,GeanyDocument * doc,GeanyFiletype * ft_old,MarkdownViewer * viewer)208 static void on_document_filetype_set(GObject *obj, GeanyDocument *doc, GeanyFiletype *ft_old,
209 MarkdownViewer *viewer)
210 {
211 update_markdown_viewer(viewer);
212 }
213
214 /* Move the MarkdownViewer to the correct notebook when the view position
215 * is changed. */
216 static void
on_view_pos_notify(GObject * obj,GParamSpec * pspec,MarkdownViewer * viewer)217 on_view_pos_notify(GObject *obj, GParamSpec *pspec, MarkdownViewer *viewer)
218 {
219 gint page_num;
220 GtkNotebook *newnb;
221 GtkNotebook *snb = GTK_NOTEBOOK(geany->main_widgets->sidebar_notebook);
222 GtkNotebook *mnb = GTK_NOTEBOOK(geany->main_widgets->message_window_notebook);
223 MarkdownConfigViewPos view_pos;
224
225 g_object_ref(g_scrolled_win); /* Prevent it from being destroyed */
226
227 /* Remove the tab from whichever notebook its in (sidebar or msgwin) */
228 page_num = gtk_notebook_page_num(snb, g_scrolled_win);
229 if (page_num >= 0) {
230 gtk_notebook_remove_page(snb, page_num);
231 } else {
232 page_num = gtk_notebook_page_num(mnb, g_scrolled_win);
233 if (page_num >= 0) {
234 gtk_notebook_remove_page(mnb, page_num);
235 } else {
236 g_warning("Unable to relocate the Markdown preview tab: not found");
237 }
238 }
239
240 /* Check the user preference to get the new notebook */
241 view_pos = markdown_config_get_view_pos(MARKDOWN_CONFIG(obj));
242 newnb = (view_pos == MARKDOWN_CONFIG_VIEW_POS_MSGWIN) ? mnb : snb;
243
244 page_num = gtk_notebook_append_page(newnb, g_scrolled_win,
245 gtk_label_new(MARKDOWN_PREVIEW_LABEL));
246
247 gtk_notebook_set_current_page(newnb, page_num);
248
249 g_object_unref(g_scrolled_win); /* The new notebook owns it now */
250
251 update_markdown_viewer(viewer);
252 }
253
replace_extension(const gchar * utf8_fn,const gchar * new_ext)254 static gchar *replace_extension(const gchar *utf8_fn, const gchar *new_ext)
255 {
256 gchar *fn_noext, *new_fn, *dot;
257 fn_noext = g_filename_from_utf8(utf8_fn, -1, NULL, NULL, NULL);
258 dot = strrchr(fn_noext, '.');
259 if (dot != NULL) {
260 *dot = '\0';
261 }
262 new_fn = g_strconcat(fn_noext, new_ext, NULL);
263 g_free(fn_noext);
264 return new_fn;
265 }
266
on_export_as_html_activate(GtkMenuItem * item,MarkdownViewer * viewer)267 static void on_export_as_html_activate(GtkMenuItem *item, MarkdownViewer *viewer)
268 {
269 GtkWidget *dialog;
270 GtkFileFilter *filter;
271 gchar *fn;
272 GeanyDocument *doc;
273 gboolean saved = FALSE;
274
275 doc = document_get_current();
276 g_return_if_fail(DOC_VALID(doc));
277
278 dialog = gtk_file_chooser_dialog_new(_("Save HTML File As"),
279 GTK_WINDOW(geany_data->main_widgets->window), GTK_FILE_CHOOSER_ACTION_SAVE,
280 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
281 GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
282 NULL);
283 gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog), TRUE);
284
285 fn = replace_extension(DOC_FILENAME(doc), ".html");
286 if (g_file_test(fn, G_FILE_TEST_EXISTS)) {
287 /* If the file exists, GtkFileChooser will change to the correct
288 * directory and show the base name as a suggestion. */
289 gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dialog), fn);
290 } else {
291 /* If the file doesn't exist, change the directory and give a suggested
292 * name for the file, since GtkFileChooser won't do it. */
293 gchar *dn = g_path_get_dirname(fn);
294 gchar *bn = g_path_get_basename(fn);
295 gchar *utf8_name;
296 gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog), dn);
297 g_free(dn);
298 utf8_name = g_filename_to_utf8(bn, -1, NULL, NULL, NULL);
299 gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), utf8_name);
300 g_free(bn);
301 g_free(utf8_name);
302 }
303 g_free(fn);
304
305 filter = gtk_file_filter_new();
306 gtk_file_filter_set_name(filter, _("HTML Files"));
307 gtk_file_filter_add_mime_type(filter, "text/html");
308 gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter);
309
310 filter = gtk_file_filter_new();
311 gtk_file_filter_set_name(filter, _("All Files"));
312 gtk_file_filter_add_pattern(filter, "*");
313 gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter);
314
315 while (!saved &&
316 gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
317 gchar *html = markdown_viewer_get_html(viewer);
318 GError *error = NULL;
319 fn = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
320 if (! g_file_set_contents(fn, html, -1, &error)) {
321 dialogs_show_msgbox(GTK_MESSAGE_ERROR,
322 _("Failed to export Markdown HTML to file '%s': %s"),
323 fn, error->message);
324 g_error_free(error);
325 } else {
326 saved = TRUE;
327 }
328 g_free(fn);
329 g_free(html);
330 }
331
332 gtk_widget_destroy(dialog);
333 }
334