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