1 /* biji-webkit-editor.c
2  * Copyright (C) Pierre-Yves LUYTEN 2012 <py@luyten.fr>
3  *
4  * bijiben is free software: you can redistribute it and/or modify it
5  * under the terms of the GNU General Public License as published by the
6  * Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * bijiben is distributed in the hope that it will be useful, but
10  * WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12  * See the GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License along
15  * with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 #include <libxml/xmlwriter.h>
19 
20 #include "config.h"
21 #include "../../bjb-utils.h"
22 #include "../biji-string.h"
23 #include "../biji-manager.h"
24 #include "biji-webkit-editor.h"
25 #include "biji-editor-selection.h"
26 #include <jsc/jsc.h>
27 
28 #define ZOOM_LARGE  1.5f;
29 #define ZOOM_MEDIUM 1.0f;
30 #define ZOOM_SMALL  0.8f;
31 
32 /* Prop */
33 enum {
34   PROP_0,
35   PROP_NOTE,
36   NUM_PROP
37 };
38 
39 /* Signals */
40 enum {
41   EDITOR_CLOSED,
42   CONTENT_CHANGED,
43   EDITOR_SIGNALS
44 };
45 
46 /* Block Format */
47 typedef enum {
48   BLOCK_FORMAT_NONE,
49   BLOCK_FORMAT_UNORDERED_LIST,
50   BLOCK_FORMAT_ORDERED_LIST
51 } BlockFormat;
52 
53 static guint biji_editor_signals [EDITOR_SIGNALS] = { 0 };
54 
55 static GParamSpec *properties[NUM_PROP] = { NULL, };
56 
57 struct _BijiWebkitEditorPrivate
58 {
59   BijiNoteObj *note;
60   gulong content_changed;
61   gulong color_changed;
62   gboolean has_text;
63   gchar *selected_text;
64   BlockFormat block_format;
65   gboolean first_load;
66   EEditorSelection *sel;
67 };
68 
69 G_DEFINE_TYPE_WITH_PRIVATE (BijiWebkitEditor, biji_webkit_editor, WEBKIT_TYPE_WEB_VIEW);
70 
71 gboolean
biji_webkit_editor_has_selection(BijiWebkitEditor * self)72 biji_webkit_editor_has_selection (BijiWebkitEditor *self)
73 {
74   BijiWebkitEditorPrivate *priv = self->priv;
75 
76   return priv->has_text && priv->selected_text && *priv->selected_text;
77 }
78 
79 const gchar *
biji_webkit_editor_get_selection(BijiWebkitEditor * self)80 biji_webkit_editor_get_selection (BijiWebkitEditor *self)
81 {
82   return self->priv->selected_text;
83 }
84 
85 static WebKitWebContext *
biji_webkit_editor_get_web_context(void)86 biji_webkit_editor_get_web_context (void)
87 {
88   static WebKitWebContext *web_context = NULL;
89 
90   if (!web_context)
91   {
92     web_context = webkit_web_context_get_default ();
93     webkit_web_context_set_cache_model (web_context, WEBKIT_CACHE_MODEL_DOCUMENT_VIEWER);
94     webkit_web_context_set_process_model (web_context, WEBKIT_PROCESS_MODEL_SHARED_SECONDARY_PROCESS);
95     webkit_web_context_set_spell_checking_enabled (web_context, TRUE);
96     webkit_web_context_set_sandbox_enabled (web_context, TRUE);
97   }
98 
99   return web_context;
100 }
101 
102 static WebKitSettings *
biji_webkit_editor_get_web_settings(void)103 biji_webkit_editor_get_web_settings (void)
104 {
105   static WebKitSettings *settings = NULL;
106 
107   if (!settings)
108   {
109     settings = webkit_settings_new_with_settings (
110       "enable-page-cache", FALSE,
111       "enable-plugins", FALSE,
112       "enable-tabs-to-links", FALSE,
113       "allow-file-access-from-file-urls", TRUE,
114       NULL);
115   }
116 
117   return settings;
118 }
119 
120 typedef gboolean GetFormatFunc (EEditorSelection*);
121 typedef void     SetFormatFunc (EEditorSelection*, gboolean);
122 
123 static void
biji_toggle_format(EEditorSelection * sel,GetFormatFunc get_format,SetFormatFunc set_format)124 biji_toggle_format (EEditorSelection *sel,
125                     GetFormatFunc get_format,
126                     SetFormatFunc set_format)
127 {
128   set_format (sel, !get_format (sel));
129 }
130 
131 static void
biji_toggle_block_format(BijiWebkitEditor * self,BlockFormat block_format)132 biji_toggle_block_format (BijiWebkitEditor *self,
133                           BlockFormat block_format)
134 {
135   /* insert commands toggle the formatting */
136   switch (block_format)
137   {
138     case BLOCK_FORMAT_NONE:
139       break;
140     case BLOCK_FORMAT_UNORDERED_LIST:
141       webkit_web_view_execute_editing_command (WEBKIT_WEB_VIEW (self), "insertUnorderedList");
142       break;
143     case BLOCK_FORMAT_ORDERED_LIST:
144       webkit_web_view_execute_editing_command (WEBKIT_WEB_VIEW (self), "insertOrderedList");
145       break;
146     default:
147       g_assert_not_reached ();
148   }
149 }
150 
151 void
biji_webkit_editor_apply_format(BijiWebkitEditor * self,gint format)152 biji_webkit_editor_apply_format (BijiWebkitEditor *self, gint format)
153 {
154   BijiWebkitEditorPrivate *priv = self->priv;
155 
156   switch (format)
157   {
158     case BIJI_BOLD:
159       biji_toggle_format (priv->sel, e_editor_selection_get_bold,
160                                       e_editor_selection_set_bold);
161       break;
162 
163     case BIJI_ITALIC:
164       biji_toggle_format (priv->sel, e_editor_selection_get_italic,
165                                       e_editor_selection_set_italic);
166       break;
167 
168     case BIJI_STRIKE:
169       biji_toggle_format (priv->sel, e_editor_selection_get_strike_through,
170                                       e_editor_selection_set_strike_through);
171       break;
172 
173     case BIJI_BULLET_LIST:
174       biji_toggle_block_format (self, BLOCK_FORMAT_UNORDERED_LIST);
175       break;
176 
177     case BIJI_ORDER_LIST:
178       biji_toggle_block_format (self, BLOCK_FORMAT_ORDERED_LIST);
179       break;
180 
181     default:
182       g_warning ("biji_webkit_editor_apply_format : Invalid format");
183   }
184 }
185 
186 void
biji_webkit_editor_cut(BijiWebkitEditor * self)187 biji_webkit_editor_cut (BijiWebkitEditor *self)
188 {
189   webkit_web_view_execute_editing_command (WEBKIT_WEB_VIEW (self), WEBKIT_EDITING_COMMAND_CUT);
190 }
191 
192 void
biji_webkit_editor_copy(BijiWebkitEditor * self)193 biji_webkit_editor_copy (BijiWebkitEditor *self)
194 {
195   webkit_web_view_execute_editing_command (WEBKIT_WEB_VIEW (self), WEBKIT_EDITING_COMMAND_COPY);
196 }
197 
198 void
biji_webkit_editor_paste(BijiWebkitEditor * self)199 biji_webkit_editor_paste (BijiWebkitEditor *self)
200 {
201   webkit_web_view_execute_editing_command (WEBKIT_WEB_VIEW (self), "PasteAsPlainText");
202 }
203 
204 void
biji_webkit_editor_undo(BijiWebkitEditor * self)205 biji_webkit_editor_undo (BijiWebkitEditor *self)
206 {
207   webkit_web_view_execute_editing_command (WEBKIT_WEB_VIEW (self), WEBKIT_EDITING_COMMAND_UNDO);
208 }
209 
210 void
biji_webkit_editor_redo(BijiWebkitEditor * self)211 biji_webkit_editor_redo (BijiWebkitEditor *self)
212 {
213   webkit_web_view_execute_editing_command (WEBKIT_WEB_VIEW (self), WEBKIT_EDITING_COMMAND_REDO);
214 }
215 
216 static void
set_editor_color(WebKitWebView * w,GdkRGBA * col)217 set_editor_color (WebKitWebView *w, GdkRGBA *col)
218 {
219   g_autofree gchar *script = NULL;
220 
221   webkit_web_view_set_background_color (w, col);
222   script = g_strdup_printf ("document.getElementById('editable').style.color = '%s';",
223                             BJB_UTILS_COLOR_INTENSITY (col) < 0.5 ? "white" : "black");
224   webkit_web_view_run_javascript (w, script, NULL, NULL, NULL);
225 }
226 
227 void
biji_webkit_editor_set_font(BijiWebkitEditor * self,gchar * font)228 biji_webkit_editor_set_font (BijiWebkitEditor *self, gchar *font)
229 {
230   PangoFontDescription *font_desc;
231   const gchar *family;
232   gint size;
233   GdkScreen *screen;
234   double dpi;
235   guint font_size;
236 
237   /* parse : but we only parse font properties we'll be able
238    * to transfer to webkit editor
239    * Maybe is there a better way than webkitSettings,
240    * eg applying format to the whole body */
241   font_desc = pango_font_description_from_string (font);
242   family = pango_font_description_get_family (font_desc);
243   size = pango_font_description_get_size (font_desc);
244 
245   if (!pango_font_description_get_size_is_absolute (font_desc))
246     size /= PANGO_SCALE;
247 
248   screen = gtk_widget_get_screen (GTK_WIDGET (self));
249   dpi = screen ? gdk_screen_get_resolution (screen) : 96.0;
250   font_size = size / 72. * dpi;
251 
252   /* Set */
253   g_object_set (biji_webkit_editor_get_web_settings (),
254                 "default-font-family", family,
255                 "default-font-size", font_size,
256                 NULL);
257 
258   pango_font_description_free (font_desc);
259 }
260 
261 void
biji_webkit_editor_set_text_size(BijiWebkitEditor * self,BjbTextSizeType text_size)262 biji_webkit_editor_set_text_size (BijiWebkitEditor *self,
263                                   BjbTextSizeType   text_size)
264 {
265   double zoom_level = ZOOM_MEDIUM;
266 
267   if (text_size == BJB_TEXT_SIZE_LARGE)
268     {
269       zoom_level = ZOOM_LARGE;
270     }
271   else if (text_size == BJB_TEXT_SIZE_SMALL)
272     {
273       zoom_level = ZOOM_SMALL;
274     }
275 
276   webkit_web_view_set_zoom_level (WEBKIT_WEB_VIEW (self), zoom_level);
277 }
278 
279 static void
biji_webkit_editor_init(BijiWebkitEditor * self)280 biji_webkit_editor_init (BijiWebkitEditor *self)
281 {
282   self->priv = biji_webkit_editor_get_instance_private (self);
283 }
284 
285 static void
biji_webkit_editor_finalize(GObject * object)286 biji_webkit_editor_finalize (GObject *object)
287 {
288   BijiWebkitEditor *self = BIJI_WEBKIT_EDITOR (object);
289   BijiWebkitEditorPrivate *priv = self->priv;
290 
291   g_free (priv->selected_text);
292 
293   if (priv->note != NULL) {
294     g_object_remove_weak_pointer (G_OBJECT (priv->note), (gpointer*) &priv->note);
295     g_signal_handler_disconnect (priv->note, priv->color_changed);
296   }
297 
298   G_OBJECT_CLASS (biji_webkit_editor_parent_class)->finalize (object);
299 }
300 
301 static void
biji_webkit_editor_content_changed(BijiWebkitEditor * self,const char * html,const char * text)302 biji_webkit_editor_content_changed (BijiWebkitEditor *self,
303                                     const char *html,
304                                     const char *text)
305 {
306   BijiNoteObj *note = self->priv->note;
307 
308   biji_note_obj_set_html (note, (char *)html);
309   biji_note_obj_set_raw_text (note, (char *)text);
310 
311   g_signal_emit (self, biji_editor_signals[CONTENT_CHANGED], 0, NULL);
312 
313   biji_note_obj_set_mtime (note, g_get_real_time () / G_USEC_PER_SEC);
314   biji_note_obj_save_note (note);
315 }
316 
317 
318 static void
on_note_color_changed(BijiNoteObj * note,BijiWebkitEditor * self)319 on_note_color_changed (BijiNoteObj *note, BijiWebkitEditor *self)
320 {
321   GdkRGBA color;
322 
323   if (biji_note_obj_get_rgba(note,&color))
324     set_editor_color (WEBKIT_WEB_VIEW (self), &color);
325 }
326 
327 static gboolean
on_navigation_request(WebKitWebView * web_view,WebKitPolicyDecision * decision,WebKitPolicyDecisionType decision_type,gpointer user_data)328 on_navigation_request (WebKitWebView           *web_view,
329                        WebKitPolicyDecision    *decision,
330                        WebKitPolicyDecisionType decision_type,
331                        gpointer                 user_data)
332 {
333   WebKitNavigationPolicyDecision *navigation_decision;
334   WebKitNavigationAction *action;
335   const char *requested_uri;
336   GtkWidget *toplevel;
337   GError *error = NULL;
338 
339   if (decision_type != WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION)
340     return FALSE;
341 
342   navigation_decision = WEBKIT_NAVIGATION_POLICY_DECISION (decision);
343   action = webkit_navigation_policy_decision_get_navigation_action (navigation_decision);
344   requested_uri = webkit_uri_request_get_uri (webkit_navigation_action_get_request (action));
345   if (g_strcmp0 (webkit_web_view_get_uri (web_view), requested_uri) == 0)
346     return FALSE;
347 
348   toplevel = gtk_widget_get_toplevel (GTK_WIDGET (web_view));
349   g_return_val_if_fail (gtk_widget_is_toplevel (toplevel), FALSE);
350 
351   gtk_show_uri_on_window (GTK_WINDOW (toplevel),
352                           requested_uri,
353                           GDK_CURRENT_TIME,
354                           &error);
355 
356   if (error)
357   {
358     g_warning ("%s", error->message);
359     g_error_free (error);
360   }
361 
362   webkit_policy_decision_ignore (decision);
363   return TRUE;
364 }
365 
366 static void
on_load_change(WebKitWebView * web_view,WebKitLoadEvent event)367 on_load_change (WebKitWebView  *web_view,
368                 WebKitLoadEvent event)
369 {
370   BijiWebkitEditorPrivate *priv;
371   GdkRGBA color;
372 
373   if (event != WEBKIT_LOAD_FINISHED)
374     return;
375 
376   priv = BIJI_WEBKIT_EDITOR (web_view)->priv;
377 
378   /* Apply color */
379   if (biji_note_obj_get_rgba (priv->note, &color))
380     set_editor_color (web_view, &color);
381 
382   if (!priv->color_changed)
383   {
384     priv->color_changed = g_signal_connect (priv->note,
385                                             "color-changed",
386                                             G_CALLBACK (on_note_color_changed),
387                                             web_view);
388   }
389 }
390 
391 static gboolean
on_context_menu(WebKitWebView * web_view,WebKitContextMenu * context_menu,GdkEvent * event,WebKitHitTestResult * hit_test_result,gpointer user_data)392 on_context_menu (WebKitWebView       *web_view,
393                  WebKitContextMenu   *context_menu,
394                  GdkEvent            *event,
395                  WebKitHitTestResult *hit_test_result,
396                  gpointer             user_data)
397 {
398   return TRUE;
399 }
400 
401 static void
biji_webkit_editor_handle_contents_update(BijiWebkitEditor * self,JSCValue * js_value)402 biji_webkit_editor_handle_contents_update (BijiWebkitEditor *self,
403                                            JSCValue         *js_value)
404 {
405   g_autoptr (JSCValue) js_outer_html = NULL;
406   g_autoptr (JSCValue) js_inner_text = NULL;
407   g_autofree gchar *html = NULL;
408   g_autofree gchar *text = NULL;
409 
410   js_outer_html = jsc_value_object_get_property (js_value, "outerHTML");
411   html = jsc_value_to_string (js_outer_html);
412   if (!html)
413     return;
414 
415   js_inner_text = jsc_value_object_get_property (js_value, "innerText");
416   text = jsc_value_to_string (js_inner_text);
417   if (!text)
418     return;
419 
420   biji_webkit_editor_content_changed (self, html, text);
421 }
422 
423 static void
biji_webkit_editor_handle_selection_change(BijiWebkitEditor * self,JSCValue * js_value)424 biji_webkit_editor_handle_selection_change (BijiWebkitEditor *self,
425                                             JSCValue         *js_value)
426 {
427   g_autoptr (JSCValue) js_has_text     = NULL;
428   g_autoptr (JSCValue) js_text         = NULL;
429   g_autoptr (JSCValue) js_block_format = NULL;
430   g_autofree char *block_format_str = NULL;
431 
432   js_has_text = jsc_value_object_get_property (js_value, "hasText");
433   self->priv->has_text = jsc_value_to_boolean (js_has_text);
434 
435   js_text = jsc_value_object_get_property (js_value, "text");
436   g_free (self->priv->selected_text);
437   self->priv->selected_text = jsc_value_to_string (js_text);
438 
439   js_block_format = jsc_value_object_get_property (js_value, "blockFormat");
440   block_format_str = jsc_value_to_string (js_block_format);
441   if (g_strcmp0 (block_format_str, "UL") == 0)
442     self->priv->block_format = BLOCK_FORMAT_UNORDERED_LIST;
443   else if (g_strcmp0 (block_format_str, "OL") == 0)
444     self->priv->block_format = BLOCK_FORMAT_ORDERED_LIST;
445   else
446     self->priv->block_format = BLOCK_FORMAT_NONE;
447 }
448 
449 static void
on_script_message(WebKitUserContentManager * user_content,WebKitJavascriptResult * message,BijiWebkitEditor * self)450 on_script_message (WebKitUserContentManager *user_content,
451                    WebKitJavascriptResult *message,
452                    BijiWebkitEditor *self)
453 {
454   JSCValue             *js_value        = NULL;
455   g_autoptr (JSCValue)  js_message_name = NULL;
456   g_autofree char *message_name = NULL;
457 
458   js_value = webkit_javascript_result_get_js_value (message);
459   g_assert (jsc_value_is_object (js_value));
460 
461   js_message_name = jsc_value_object_get_property (js_value, "messageName");
462   message_name = jsc_value_to_string (js_message_name);
463   if (g_strcmp0 (message_name, "ContentsUpdate") == 0)
464     {
465       if (self->priv->first_load)
466         self->priv->first_load = FALSE;
467       else
468         biji_webkit_editor_handle_contents_update (self, js_value);
469     }
470   else if (g_strcmp0 (message_name, "SelectionChange") == 0)
471     biji_webkit_editor_handle_selection_change (self, js_value);
472 }
473 
474 static void
biji_webkit_editor_constructed(GObject * obj)475 biji_webkit_editor_constructed (GObject *obj)
476 {
477   BijiWebkitEditor *self;
478   BijiWebkitEditorPrivate *priv;
479   WebKitWebView *view;
480   WebKitUserContentManager *user_content;
481   g_autoptr(GBytes) html_data = NULL;
482   gchar *body;
483 
484   self = BIJI_WEBKIT_EDITOR (obj);
485   view = WEBKIT_WEB_VIEW (self);
486   priv = self->priv;
487   priv->first_load = TRUE;
488 
489   G_OBJECT_CLASS (biji_webkit_editor_parent_class)->constructed (obj);
490 
491   user_content = webkit_web_view_get_user_content_manager (view);
492   webkit_user_content_manager_register_script_message_handler (user_content, "bijiben");
493   g_signal_connect (user_content, "script-message-received::bijiben",
494                     G_CALLBACK (on_script_message), self);
495 
496   priv->sel = e_editor_selection_new (view);
497 
498   webkit_web_view_set_editable (view, !biji_note_obj_is_trashed (BIJI_NOTE_OBJ (priv->note)));
499 
500   /* Do not segfault at finalize
501    * if the note died */
502   g_object_add_weak_pointer (G_OBJECT (priv->note), (gpointer*) &priv->note);
503 
504   body = biji_note_obj_get_html (priv->note);
505 
506   if (!body)
507     body = html_from_plain_text ("");
508 
509   html_data = g_bytes_new_take (body, strlen (body));
510   webkit_web_view_load_bytes (view, html_data, "application/xhtml+xml", NULL,
511                               "file://" DATADIR G_DIR_SEPARATOR_S "bijiben" G_DIR_SEPARATOR_S);
512 
513   /* Do not be a browser */
514   g_signal_connect (view, "decide-policy",
515                     G_CALLBACK (on_navigation_request), NULL);
516   g_signal_connect (view, "load-changed",
517                     G_CALLBACK (on_load_change), NULL);
518   g_signal_connect (view, "context-menu",
519                     G_CALLBACK (on_context_menu), NULL);
520 }
521 
522 static void
biji_webkit_editor_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)523 biji_webkit_editor_get_property (GObject  *object,
524                                  guint     property_id,
525                                  GValue   *value,
526                                  GParamSpec *pspec)
527 {
528   BijiWebkitEditor *self = BIJI_WEBKIT_EDITOR (object);
529 
530   switch (property_id)
531   {
532     case PROP_NOTE:
533       g_value_set_object (value, self->priv->note);
534       break;
535     default:
536       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
537   }
538 }
539 
540 static void
biji_webkit_editor_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)541 biji_webkit_editor_set_property (GObject  *object,
542                                  guint     property_id,
543                                  const GValue *value,
544                                  GParamSpec *pspec)
545 {
546   BijiWebkitEditor *self = BIJI_WEBKIT_EDITOR (object);
547 
548   switch (property_id)
549   {
550     case PROP_NOTE:
551       self->priv->note = g_value_get_object (value);
552       break;
553     default:
554       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
555   }
556 }
557 
558 static void
biji_webkit_editor_class_init(BijiWebkitEditorClass * klass)559 biji_webkit_editor_class_init (BijiWebkitEditorClass *klass)
560 {
561   GObjectClass* object_class = G_OBJECT_CLASS (klass);
562 
563   object_class->constructed = biji_webkit_editor_constructed;
564   object_class->finalize = biji_webkit_editor_finalize;
565   object_class->get_property = biji_webkit_editor_get_property;
566   object_class->set_property = biji_webkit_editor_set_property;
567 
568   properties[PROP_NOTE] = g_param_spec_object ("note",
569                                                "Note",
570                                                "Biji Note Obj",
571                                                 BIJI_TYPE_NOTE_OBJ,
572                                                 G_PARAM_READWRITE  |
573                                                 G_PARAM_CONSTRUCT |
574                                                 G_PARAM_STATIC_STRINGS);
575 
576   g_object_class_install_property (object_class,PROP_NOTE,properties[PROP_NOTE]);
577 
578   biji_editor_signals[EDITOR_CLOSED] = g_signal_new ("closed",
579                                        G_OBJECT_CLASS_TYPE (klass),
580                                        G_SIGNAL_RUN_FIRST,
581                                        0,
582                                        NULL,
583                                        NULL,
584                                        g_cclosure_marshal_VOID__VOID,
585                                        G_TYPE_NONE,
586                                        0);
587   biji_editor_signals[CONTENT_CHANGED] = g_signal_new ("content-changed",
588                                          G_OBJECT_CLASS_TYPE (klass),
589                                          G_SIGNAL_RUN_LAST,
590                                          0,
591                                          NULL,
592                                          NULL,
593                                          g_cclosure_marshal_VOID__VOID,
594                                          G_TYPE_NONE,
595                                          0);
596 }
597 
598 BijiWebkitEditor *
biji_webkit_editor_new(BijiNoteObj * note)599 biji_webkit_editor_new (BijiNoteObj *note)
600 {
601   WebKitUserContentManager *manager = webkit_user_content_manager_new ();
602 
603   return g_object_new (BIJI_TYPE_WEBKIT_EDITOR,
604                        "web-context", biji_webkit_editor_get_web_context (),
605                        "settings", biji_webkit_editor_get_web_settings (),
606                        "user-content-manager", manager,
607                        "note", note,
608                        NULL);
609 
610   g_object_unref (manager);
611 }
612