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