1 /*
2  * marker-preview.c
3  *
4  * Copyright (C) 2017-2020 - 2018 Fabio Colacio
5  *
6  * Marker is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Library General Public License as
8  * published by the Free Software Foundation; either version 3 of the
9  * License, or (at your option) any later version.
10  *
11  * Marker 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 GNU
14  * Library General Public License for more details.
15  *
16  * You should have received a copy of the GNU Library General Public
17  * License along with Marker; see the file LICENSE.md. If not,
18  * see <http://www.gnu.org/licenses/>.
19  *
20  */
21 
22 #include <string.h>
23 #include <stdlib.h>
24 
25 #include <glib.h>
26 #include <time.h>
27 
28 #include "marker-markdown.h"
29 #include "marker-prefs.h"
30 
31 #include "marker-string.h"
32 
33 #include "marker-preview.h"
34 #include "marker.h"
35 
36 #define MAX_ZOOM  4.0
37 #define MIN_ZOOM  0.1
38 
39 #define SCROLL_STEP 25
40 #define SCROLL_STEP_SCRIPT "window.scrollBy(%d,%d);"
41 #define SCROLL_SCRIPT "window.scrollTo(%d,%d);"
42 
43 #define min(a, b) ((a < b) ? a : b)
44 #define max(a, b) ((a < b) ? b : a)
45 
46 struct _MarkerPreview
47 {
48   WebKitWebView parent_instance;
49 };
50 
G_DEFINE_TYPE(MarkerPreview,marker_preview,WEBKIT_TYPE_WEB_VIEW)51 G_DEFINE_TYPE(MarkerPreview, marker_preview, WEBKIT_TYPE_WEB_VIEW)
52 
53 static gboolean
54 open_uri (WebKitPolicyDecision *decision) {
55   WebKitNavigationPolicyDecision *nav_dec = WEBKIT_NAVIGATION_POLICY_DECISION(decision);
56   WebKitNavigationAction *action = webkit_navigation_policy_decision_get_navigation_action (nav_dec);
57   WebKitURIRequest *request = webkit_navigation_action_get_request (action);
58   /* Open only http requests in default browser */
59   /* FIXME: Open also other request like ftp in default browser */
60   if (webkit_uri_request_get_http_method(request) != NULL) {
61     const gchar * uri = webkit_uri_request_get_uri(request);
62     GtkApplication * app = marker_get_app();
63     GList* windows = gtk_application_get_windows(app);
64     time_t now = time(0);
65     gtk_show_uri_on_window (windows->data, uri, now, NULL);
66     webkit_policy_decision_ignore(decision);
67     return TRUE;
68   }
69   return FALSE;
70 }
71 
72 static gboolean
navigate(WebKitPolicyDecision * decision)73 navigate(WebKitPolicyDecision *decision)
74 {
75   /** TODO FIX internal navigation
76   WebKitNavigationPolicyDecision * nav_dec = WEBKIT_NAVIGATION_POLICY_DECISION(decision);
77 
78   const gchar * uri =webkit_uri_request_get_uri(webkit_navigation_action_get_request (webkit_navigation_policy_decision_get_navigation_action (nav_dec)));
79   g_print(">> %s\n", uri);
80   **/
81 
82   /* if request is http ignore default policy and open uri in default browser*/
83   return open_uri (decision);
84 }
85 
86 static gboolean
decide_policy_cb(WebKitWebView * web_view,WebKitPolicyDecision * decision,WebKitPolicyDecisionType type)87 decide_policy_cb (WebKitWebView *web_view,
88                   WebKitPolicyDecision *decision,
89                   WebKitPolicyDecisionType type)
90 {
91     switch (type) {
92     case WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION:
93       return navigate(decision);
94     case WEBKIT_POLICY_DECISION_TYPE_RESPONSE:
95       webkit_policy_decision_use (decision);
96       break;
97     case WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION:
98       return navigate(decision);
99     default:
100       /* Making no decision results in webkit_policy_decision_use(). */
101       return FALSE;
102     }
103     return TRUE;
104 }
105 
106 
107 static gboolean
context_menu_cb(WebKitWebView * web_view,WebKitContextMenu * context_menu,GdkEvent * event,WebKitHitTestResult * hit_test_result,gpointer user_data)108 context_menu_cb  (WebKitWebView       *web_view,
109                   WebKitContextMenu   *context_menu,
110                   GdkEvent            *event,
111                   WebKitHitTestResult *hit_test_result,
112                   gpointer             user_data)
113 {
114   return TRUE;
115 }
116 
117 static void
initialize_web_extensions_cb(WebKitWebContext * context,gpointer user_data)118 initialize_web_extensions_cb (WebKitWebContext *context,
119                               gpointer          user_data)
120 {
121   /* Web Extensions get a different ID for each Web Process */
122   static guint32 unique_id = 0;
123 
124   webkit_web_context_set_web_extensions_directory (
125      context, WEB_EXTENSIONS_DIRECTORY);
126   webkit_web_context_set_web_extensions_initialization_user_data (
127      context, g_variant_new_uint32 (unique_id++));
128 }
129 
130 gboolean
key_press_event_cb(GtkWidget * widget,GdkEvent * event,gpointer user_data)131 key_press_event_cb (GtkWidget *widget,
132                     GdkEvent  *event,
133                     gpointer   user_data)
134 {
135   g_return_val_if_fail (MARKER_IS_PREVIEW (widget), FALSE);
136   MarkerPreview *preview = MARKER_PREVIEW (widget);
137 
138   GdkEventKey *key_event = (GdkEventKey *) event;
139 
140   if ((key_event->state & GDK_CONTROL_MASK) != 0)
141   {
142     switch (key_event->keyval)
143     {
144       case GDK_KEY_plus:
145         marker_preview_zoom_in (preview);
146         break;
147 
148       case GDK_KEY_minus:
149         marker_preview_zoom_out (preview);
150         break;
151 
152       case GDK_KEY_0:
153         marker_preview_zoom_original (preview);
154         break;
155     }
156   }
157   else
158   {
159     switch (key_event->keyval)
160     {
161       case GDK_KEY_j:
162         marker_preview_scroll_down (preview);
163         break;
164 
165       case GDK_KEY_k:
166         marker_preview_scroll_up (preview);
167         break;
168 
169       case GDK_KEY_h:
170         marker_preview_scroll_left (preview);
171         break;
172 
173       case GDK_KEY_l:
174         marker_preview_scroll_right (preview);
175         break;
176 
177       case GDK_KEY_g:
178         marker_preview_scroll_to_top (preview);
179         break;
180 
181       case GDK_KEY_G:
182         marker_preview_scroll_to_bottom (preview);
183         break;
184     }
185   }
186 
187   return FALSE;
188 }
189 
190 gboolean
scroll_event_cb(GtkWidget * widget,GdkEvent * event,gpointer user_data)191 scroll_event_cb (GtkWidget *widget,
192                  GdkEvent  *event,
193                  gpointer   user_data)
194 {
195   g_return_val_if_fail (MARKER_IS_PREVIEW (widget), FALSE);
196   MarkerPreview *preview = MARKER_PREVIEW (widget);
197 
198   GdkEventScroll *scroll_event = (GdkEventScroll *) event;
199   guint state = scroll_event->state;
200   if ((state & GDK_CONTROL_MASK) != 0)
201   {
202     gdouble delta_y = scroll_event->delta_y;
203 
204     if (delta_y > 0)
205     {
206       marker_preview_zoom_out (preview);
207     }
208     else if (delta_y < 0)
209     {
210       marker_preview_zoom_in (preview);
211     }
212   }
213 
214   return FALSE;
215 }
216 
217 static void
load_changed_cb(WebKitWebView * preview,WebKitLoadEvent event)218 load_changed_cb (WebKitWebView   *preview,
219                  WebKitLoadEvent  event)
220 {
221   switch (event)
222   {
223     case WEBKIT_LOAD_STARTED:
224       break;
225 
226     case WEBKIT_LOAD_REDIRECTED:
227       break;
228 
229     case WEBKIT_LOAD_COMMITTED:
230       break;
231 
232     case WEBKIT_LOAD_FINISHED:
233       break;
234   }
235 }
236 
237 static void
pdf_print_failed_cb(WebKitPrintOperation * print_op,GError * err,gpointer user_data)238 pdf_print_failed_cb (WebKitPrintOperation* print_op,
239                      GError*               err,
240                      gpointer              user_data)
241 {
242   g_printerr("print failed with error: %s\n", err->message);
243 }
244 
245 static void
scroll_js_finished_cb(GObject * object,GAsyncResult * result,gpointer user_data)246 scroll_js_finished_cb (GObject      *object,
247                        GAsyncResult *result,
248                        gpointer      user_data)
249 {
250   WebKitJavascriptResult *js_result;
251   GError *error = NULL;
252 
253   js_result = webkit_web_view_run_javascript_finish (WEBKIT_WEB_VIEW (object), result, &error);
254   if (error != NULL) {
255     g_print ("Error running scroll script: %s", error->message);
256     g_error_free (error);
257     return;
258   }
259 
260   webkit_javascript_result_unref (js_result);
261 }
262 
263 void
marker_preview_set_zoom_level(MarkerPreview * preview,gdouble zoom_level)264 marker_preview_set_zoom_level (MarkerPreview *preview,
265                                gdouble        zoom_level)
266 {
267   g_return_if_fail (MARKER_IS_PREVIEW (preview));
268   webkit_web_view_set_zoom_level (WEBKIT_WEB_VIEW (preview), zoom_level);
269   g_signal_emit_by_name (preview, "zoom-changed");
270 }
271 
272 static void
marker_preview_init(MarkerPreview * preview)273 marker_preview_init (MarkerPreview *preview)
274 {
275   g_signal_connect (webkit_web_context_get_default (),
276                     "initialize-web-extensions",
277                     G_CALLBACK (initialize_web_extensions_cb),
278                     NULL);
279 
280   g_signal_connect (preview, "scroll-event", G_CALLBACK (scroll_event_cb), NULL);
281   g_signal_connect (preview, "key-press-event", G_CALLBACK (key_press_event_cb), NULL);
282 }
283 
284 static void
marker_preview_class_init(MarkerPreviewClass * class)285 marker_preview_class_init (MarkerPreviewClass *class)
286 {
287   g_signal_newv ("zoom-changed",
288                  G_TYPE_FROM_CLASS (class),
289                  G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE,
290                  NULL, NULL, NULL, NULL,
291                  G_TYPE_NONE, 0, NULL);
292 
293 
294   WEBKIT_WEB_VIEW_CLASS(class)->load_changed = load_changed_cb;
295 }
296 
297 MarkerPreview*
marker_preview_new(void)298 marker_preview_new(void)
299 {
300   MarkerPreview * obj =  g_object_new(MARKER_TYPE_PREVIEW, NULL);
301   webkit_web_view_set_zoom_level (WEBKIT_WEB_VIEW (obj), makrer_prefs_get_zoom_level ());
302 
303 
304   /***
305   WebKitSettings * settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(obj));
306   webkit_settings_set_enable_write_console_messages_to_stdout(settings, TRUE);
307   webkit_web_view_set_settings(WEBKIT_WEB_VIEW(obj), settings);
308   ***/
309 
310   return obj;
311 }
312 
313 void
marker_preview_zoom_out(MarkerPreview * preview)314 marker_preview_zoom_out (MarkerPreview *preview)
315 {
316   g_return_if_fail (WEBKIT_IS_WEB_VIEW (preview));
317   WebKitWebView *view = WEBKIT_WEB_VIEW (preview);
318 
319   gdouble val = webkit_web_view_get_zoom_level (view) - 0.1;
320   val = max (val, MIN_ZOOM);
321 
322   marker_prefs_set_zoom_level(val);
323   webkit_web_view_set_zoom_level(view, val);
324 
325   g_signal_emit_by_name (preview, "zoom-changed");
326 }
327 
328 void
marker_preview_zoom_original(MarkerPreview * preview)329 marker_preview_zoom_original (MarkerPreview *preview)
330 {
331   g_return_if_fail (WEBKIT_IS_WEB_VIEW (preview));
332   WebKitWebView *view = WEBKIT_WEB_VIEW (preview);
333 
334   gdouble zoom = 1.0;
335 
336   marker_prefs_set_zoom_level (zoom);
337   webkit_web_view_set_zoom_level (view, zoom);
338 
339   g_signal_emit_by_name (preview, "zoom-changed");
340 }
341 
342 void
marker_preview_zoom_in(MarkerPreview * preview)343 marker_preview_zoom_in (MarkerPreview *preview)
344 {
345   g_return_if_fail (WEBKIT_IS_WEB_VIEW (preview));
346   WebKitWebView *view = WEBKIT_WEB_VIEW (preview);
347 
348   gdouble val = webkit_web_view_get_zoom_level (view) + 0.1;
349   val = min (val, MAX_ZOOM);
350 
351   marker_prefs_set_zoom_level(val);
352   webkit_web_view_set_zoom_level(view, val);
353 
354   g_signal_emit_by_name (preview, "zoom-changed");
355 }
356 
357 void
marker_preview_render_markdown(MarkerPreview * preview,const char * markdown,const char * css_theme,const char * base_uri,int cursor)358 marker_preview_render_markdown(MarkerPreview* preview,
359                                const char*    markdown,
360                                const char*    css_theme,
361                                const char*    base_uri,
362                                int            cursor)
363 {
364   MarkerMathJSMode katex_mode = MATHJS_OFF;
365   if (marker_prefs_get_use_mathjs()) {
366     katex_mode = MATHJS_LOCAL;
367   }
368   MarkerHighlightMode highlight_mode = HIGHLIGHT_OFF;
369   if (marker_prefs_get_use_highlight()){
370     highlight_mode = HIGHLIGHT_LOCAL;
371   }
372   MarkerMermaidMode mermaid_mode = MERMAID_OFF;
373   if (marker_prefs_get_use_mermaid())
374   {
375     mermaid_mode = MERMAID_LOCAL;
376   }
377 
378   char * base_folder = NULL;
379   if (base_uri)
380     base_folder = marker_string_filename_get_path(base_uri);
381   char* html = marker_markdown_to_html(markdown,
382                                        strlen(markdown),
383                                        base_folder,
384                                        katex_mode,
385                                        highlight_mode,
386                                        mermaid_mode,
387                                        css_theme,
388                                        cursor);
389 
390   WebKitWebView* web_view = WEBKIT_WEB_VIEW(preview);
391 
392   g_signal_connect(web_view,
393                    "decide-policy",
394                    G_CALLBACK(decide_policy_cb),
395                    NULL);
396   g_signal_connect(web_view,
397                    "context-menu",
398                    G_CALLBACK(context_menu_cb),
399                    NULL);
400 
401   gchar * uri;
402   if (base_uri) {
403     uri = g_filename_to_uri  (g_locale_from_utf8(base_uri, strlen(base_uri), NULL, NULL, NULL), NULL, NULL);
404   }else {
405     uri = g_strdup_printf ("file://%s", getenv("HOME"));
406   }
407   webkit_web_view_load_html(web_view
408                             ,html
409                             ,uri);
410 
411   g_free(uri);
412   free(html);
413 }
414 
415 WebKitPrintOperationResponse
marker_preview_run_print_dialog(MarkerPreview * preview,GtkWindow * parent)416 marker_preview_run_print_dialog(MarkerPreview* preview,
417                                 GtkWindow*     parent)
418 {
419   WebKitPrintOperation* print_op =
420     webkit_print_operation_new(WEBKIT_WEB_VIEW(preview));
421 
422   g_signal_connect(print_op, "failed", G_CALLBACK(pdf_print_failed_cb), NULL);
423 
424   return webkit_print_operation_run_dialog(print_op, parent);
425 }
426 
427 void
marker_preview_print_pdf(MarkerPreview * preview,const char * outfile,enum scidown_paper_size paper_size,GtkPageOrientation orientation)428 marker_preview_print_pdf(MarkerPreview*     preview,
429                          const char*        outfile,
430                          enum scidown_paper_size paper_size,
431                          GtkPageOrientation orientation)
432 
433 {
434     WebKitPrintOperation* print_op = NULL;
435     GtkPrintSettings* print_s = NULL;
436     char* uri = g_strdup_printf("file://%s", outfile);
437 
438     print_op = webkit_print_operation_new(WEBKIT_WEB_VIEW(preview));
439     g_signal_connect(print_op, "failed", G_CALLBACK(pdf_print_failed_cb), NULL);
440 
441     print_s = gtk_print_settings_new();
442     GtkPaperSize * gtk_paper_size = NULL;
443     if (paper_size != B43 && paper_size != B169)
444       gtk_paper_size = gtk_paper_size_new(paper_to_gtkstr(paper_size));
445     else if (paper_size == B43)
446       gtk_paper_size = gtk_paper_size_new_custom("B43", "B43", 166, 221, GTK_UNIT_MM);
447     else
448       gtk_paper_size = gtk_paper_size_new_custom("B43", "B43", 166, 294, GTK_UNIT_MM);
449 
450     GtkPageSetup * gtk_page_setup = gtk_page_setup_new();
451 
452     gtk_print_settings_set(print_s, GTK_PRINT_SETTINGS_OUTPUT_FILE_FORMAT, "pdf");
453     gtk_print_settings_set(print_s, GTK_PRINT_SETTINGS_OUTPUT_URI, uri);
454     gtk_print_settings_set(print_s, GTK_PRINT_SETTINGS_PRINTER, "Print to File");
455 
456     if (orientation == GTK_PAGE_ORIENTATION_PORTRAIT) {
457       gtk_page_setup_set_paper_size(gtk_page_setup, gtk_paper_size);
458       gtk_print_settings_set_paper_width(print_s, gtk_paper_size_get_width(gtk_paper_size, GTK_UNIT_MM), GTK_UNIT_MM);
459       gtk_print_settings_set_paper_height(print_s, gtk_paper_size_get_height(gtk_paper_size, GTK_UNIT_MM), GTK_UNIT_MM);
460 
461     } else {
462       gdouble width = gtk_paper_size_get_width(gtk_paper_size, GTK_UNIT_MM);
463       gdouble height = gtk_paper_size_get_height(gtk_paper_size, GTK_UNIT_MM);
464       GtkPaperSize * custom_size = gtk_paper_size_new_custom(g_strdup_printf("%s_landscape", paper_to_string(paper_size)),
465                                                              "pdf", height, width, GTK_UNIT_MM);
466       gtk_page_setup_set_paper_size(gtk_page_setup, custom_size);
467 
468       gtk_print_settings_set_paper_width(print_s, height, GTK_UNIT_MM);
469       gtk_print_settings_set_paper_height(print_s, width, GTK_UNIT_MM);
470     }
471     if (paper_size == B43 || paper_size == B169) {
472       gtk_page_setup_set_left_margin(gtk_page_setup, 0, GTK_UNIT_POINTS);
473       gtk_page_setup_set_right_margin(gtk_page_setup, 0, GTK_UNIT_POINTS);
474       gtk_page_setup_set_top_margin(gtk_page_setup, 0, GTK_UNIT_POINTS);
475       gtk_page_setup_set_bottom_margin(gtk_page_setup, 0, GTK_UNIT_POINTS);
476     }
477     gtk_print_settings_set_orientation(print_s, orientation);
478 
479     webkit_print_operation_set_print_settings(print_op, print_s);
480     webkit_print_operation_set_page_setup(print_op, gtk_page_setup);
481 
482     webkit_print_operation_print(print_op);
483 
484     g_free(uri);
485 }
486 
487 void
marker_preview_scroll_left(MarkerPreview * preview)488 marker_preview_scroll_left (MarkerPreview *preview)
489 {
490   g_autofree gchar *script = g_strdup_printf (SCROLL_STEP_SCRIPT, -SCROLL_STEP, 0);
491   webkit_web_view_run_javascript (WEBKIT_WEB_VIEW (preview), script, NULL, scroll_js_finished_cb, NULL);
492 }
493 
494 void
marker_preview_scroll_right(MarkerPreview * preview)495 marker_preview_scroll_right (MarkerPreview *preview)
496 {
497   g_autofree gchar *script = g_strdup_printf (SCROLL_STEP_SCRIPT, SCROLL_STEP, 0);
498   webkit_web_view_run_javascript (WEBKIT_WEB_VIEW (preview), script, NULL, scroll_js_finished_cb, NULL);
499 }
500 
501 void
marker_preview_scroll_up(MarkerPreview * preview)502 marker_preview_scroll_up (MarkerPreview *preview)
503 {
504   g_autofree gchar *script = g_strdup_printf (SCROLL_STEP_SCRIPT, 0, -SCROLL_STEP);
505   webkit_web_view_run_javascript (WEBKIT_WEB_VIEW (preview), script, NULL, scroll_js_finished_cb, NULL);
506 }
507 
508 void
marker_preview_scroll_down(MarkerPreview * preview)509 marker_preview_scroll_down (MarkerPreview *preview)
510 {
511   g_autofree gchar *script = g_strdup_printf (SCROLL_STEP_SCRIPT, 0, SCROLL_STEP);
512   webkit_web_view_run_javascript (WEBKIT_WEB_VIEW (preview), script, NULL, scroll_js_finished_cb, NULL);
513 }
514 
515 void
marker_preview_scroll_to_top(MarkerPreview * preview)516 marker_preview_scroll_to_top (MarkerPreview *preview)
517 {
518   g_autofree gchar *script = g_strdup_printf (SCROLL_SCRIPT, 0, 0);
519   webkit_web_view_run_javascript (WEBKIT_WEB_VIEW (preview), script, NULL, scroll_js_finished_cb, NULL);
520 }
521 
522 void
marker_preview_scroll_to_bottom(MarkerPreview * preview)523 marker_preview_scroll_to_bottom (MarkerPreview *preview)
524 {
525   const gchar *script = "window.scrollTo(0,document.body.scrollHeight);";
526   webkit_web_view_run_javascript (WEBKIT_WEB_VIEW (preview), script, NULL, scroll_js_finished_cb, NULL);
527 }
528