9db5629788313671cfc5cfcd5be78c2d5c4cd8ea
[colloquium.git] / libstorycode / gtk / gtknarrativeview.c
1 /*
2  * gtknarrativeview.c
3  *
4  * Copyright © 2013-2019 Thomas White <taw@bitwiz.org.uk>
5  *
6  * This file is part of Colloquium.
7  *
8  * Colloquium is free software: you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation, either version 3 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
20  *
21  */
22
23
24 #ifdef HAVE_CONFIG_H
25 #include <config.h>
26 #endif
27
28 #include <stdlib.h>
29 #include <string.h>
30 #include <gtk/gtk.h>
31 #include <assert.h>
32 #include <gdk/gdkkeysyms.h>
33 #include <gdk-pixbuf/gdk-pixbuf.h>
34 #include <math.h>
35
36 #include <narrative.h>
37 #include <narrative_render_cairo.h>
38
39 #include "gtknarrativeview.h"
40 #include "narrative_priv.h"
41
42
43 static void scroll_interface_init(GtkScrollable *iface)
44 {
45 }
46
47
48 enum
49 {
50         GTKNARRATIVEVIEW_0,
51         GTKNARRATIVEVIEW_VADJ,
52         GTKNARRATIVEVIEW_HADJ,
53         GTKNARRATIVEVIEW_VPOL,
54         GTKNARRATIVEVIEW_HPOL,
55 };
56
57
58 G_DEFINE_TYPE_WITH_CODE(GtkNarrativeView, gtk_narrative_view,
59                         GTK_TYPE_DRAWING_AREA,
60                         G_IMPLEMENT_INTERFACE(GTK_TYPE_SCROLLABLE,
61                                               scroll_interface_init))
62
63 static void redraw(GtkNarrativeView *e)
64 {
65         gint w, h;
66         w = gtk_widget_get_allocated_width(GTK_WIDGET(e));
67         h = gtk_widget_get_allocated_height(GTK_WIDGET(e));
68         gtk_widget_queue_draw_area(GTK_WIDGET(e), 0, 0, w, h);
69 }
70
71
72 static void horizontal_adjust(GtkAdjustment *adj, GtkNarrativeView *e)
73 {
74         e->h_scroll_pos = gtk_adjustment_get_value(adj);
75         redraw(e);
76 }
77
78
79 static void set_horizontal_params(GtkNarrativeView *e)
80 {
81         if ( e->hadj == NULL ) return;
82         gtk_adjustment_configure(e->hadj, e->h_scroll_pos, 0, e->w, 100,
83                                  e->visible_width, e->visible_width);
84 }
85
86
87 static void vertical_adjust(GtkAdjustment *adj, GtkNarrativeView *e)
88 {
89         e->scroll_pos = gtk_adjustment_get_value(adj);
90         redraw(e);
91 }
92
93
94 static void set_vertical_params(GtkNarrativeView *e)
95 {
96         double page;
97
98         if ( e->vadj == NULL ) return;
99
100         /* Ensure we do not scroll off the top of the document */
101         if ( e->scroll_pos < 0.0 ) e->scroll_pos = 0.0;
102
103         /* Ensure we do not scroll off the bottom of the document */
104         if ( e->scroll_pos > e->h - e->visible_height ) {
105                 e->scroll_pos = e->h - e->visible_height;
106         }
107
108         /* If we can show the whole document, show it at the top */
109         if ( e->h < e->visible_height ) {
110                 e->scroll_pos = 0.0;
111         }
112
113         if ( e->h > e->visible_height ) {
114                 page = e->visible_height;
115         } else {
116                 page = e->h;
117         }
118
119         gtk_adjustment_configure(e->vadj, e->scroll_pos, 0, e->h, 100,
120                                  e->visible_height, page);
121 }
122
123
124 static void rewrap_range(GtkNarrativeView *e, int min, int max)
125 {
126         PangoContext *pc;
127         PangoLanguage *lang;
128         const char *langname;
129
130         pc = gtk_widget_get_pango_context(GTK_WIDGET(e));
131
132         langname = narrative_get_language(e->n);
133         lang = pango_language_from_string(langname);
134
135         /* Wrap everything with the current width, to get the total height */
136         narrative_wrap_range(e->n,
137                              narrative_get_stylesheet(e->n),
138                              lang, pc, e->w,
139                              narrative_get_imagestore(e->n),
140                              min, max, e->sel_start, e->sel_end);
141 }
142
143
144 static void update_size(GtkNarrativeView *e)
145 {
146         e->w = e->visible_width;
147         e->h = narrative_get_height(e->n);
148
149         set_vertical_params(e);
150         set_horizontal_params(e);
151 }
152
153
154 static gboolean resize_sig(GtkWidget *widget, GdkEventConfigure *event,
155                            GtkNarrativeView *e)
156 {
157         e->visible_height = event->height;
158         e->visible_width = event->width;
159         e->w = e->visible_width;
160
161         rewrap_range(e, 0, e->n->n_items-1);
162
163         update_size(e);
164
165         return FALSE;
166 }
167
168
169 static void emit_change_sig(GtkNarrativeView *e)
170 {
171         g_signal_emit_by_name(e, "changed");
172 }
173
174
175 static void sc_editor_set_property(GObject *obj, guint id, const GValue *val,
176                                    GParamSpec *spec)
177 {
178         GtkNarrativeView *e = GTK_NARRATIVE_VIEW(obj);
179
180         switch ( id ) {
181
182                 case GTKNARRATIVEVIEW_VPOL :
183                 e->vpol = g_value_get_enum(val);
184                 break;
185
186                 case GTKNARRATIVEVIEW_HPOL :
187                 e->hpol = g_value_get_enum(val);
188                 break;
189
190                 case GTKNARRATIVEVIEW_VADJ :
191                 e->vadj = g_value_get_object(val);
192                 set_vertical_params(e);
193                 if ( e->vadj != NULL ) {
194                         g_signal_connect(G_OBJECT(e->vadj), "value-changed",
195                                          G_CALLBACK(vertical_adjust), e);
196                 }
197                 break;
198
199                 case GTKNARRATIVEVIEW_HADJ :
200                 e->hadj = g_value_get_object(val);
201                 set_horizontal_params(e);
202                 if ( e->hadj != NULL ) {
203                         g_signal_connect(G_OBJECT(e->hadj), "value-changed",
204                                          G_CALLBACK(horizontal_adjust), e);
205                 }
206                 break;
207
208                 default :
209                 printf("setting %i\n", id);
210                 break;
211
212         }
213 }
214
215
216 static void sc_editor_get_property(GObject *obj, guint id, GValue *val,
217                                    GParamSpec *spec)
218 {
219         GtkNarrativeView *e = GTK_NARRATIVE_VIEW(obj);
220
221         switch ( id ) {
222
223                 case GTKNARRATIVEVIEW_VADJ :
224                 g_value_set_object(val, e->vadj);
225                 break;
226
227                 case GTKNARRATIVEVIEW_HADJ :
228                 g_value_set_object(val, e->hadj);
229                 break;
230
231                 case GTKNARRATIVEVIEW_VPOL :
232                 g_value_set_enum(val, e->vpol);
233                 break;
234
235                 case GTKNARRATIVEVIEW_HPOL :
236                 g_value_set_enum(val, e->hpol);
237                 break;
238
239                 default :
240                 G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, id, spec);
241                 break;
242
243         }
244 }
245
246
247 static GtkSizeRequestMode get_request_mode(GtkWidget *widget)
248 {
249         return GTK_SIZE_REQUEST_CONSTANT_SIZE;
250 }
251
252
253 static void get_preferred_width(GtkWidget *widget, gint *min, gint *natural)
254 {
255         *min = 100;
256         *natural = 640;
257 }
258
259
260 static void get_preferred_height(GtkWidget *widget, gint *min, gint *natural)
261 {
262         *min = 1000;
263         *natural = 1000;
264 }
265
266
267 static void gtk_narrative_view_class_init(GtkNarrativeViewClass *klass)
268 {
269         GObjectClass *goc = G_OBJECT_CLASS(klass);
270         goc->set_property = sc_editor_set_property;
271         goc->get_property = sc_editor_get_property;
272         g_object_class_override_property(goc, GTKNARRATIVEVIEW_VADJ, "vadjustment");
273         g_object_class_override_property(goc, GTKNARRATIVEVIEW_HADJ, "hadjustment");
274         g_object_class_override_property(goc, GTKNARRATIVEVIEW_VPOL, "vscroll-policy");
275         g_object_class_override_property(goc, GTKNARRATIVEVIEW_HPOL, "hscroll-policy");
276
277         GTK_WIDGET_CLASS(klass)->get_request_mode = get_request_mode;
278         GTK_WIDGET_CLASS(klass)->get_preferred_width = get_preferred_width;
279         GTK_WIDGET_CLASS(klass)->get_preferred_height = get_preferred_height;
280         GTK_WIDGET_CLASS(klass)->get_preferred_height_for_width = NULL;
281
282         g_signal_new("changed", GTK_TYPE_NARRATIVE_VIEW,
283                      G_SIGNAL_RUN_LAST, 0,
284                      NULL, NULL, NULL, G_TYPE_NONE, 0);
285         g_signal_new("slide-double-clicked", GTK_TYPE_NARRATIVE_VIEW,
286                      G_SIGNAL_RUN_LAST, 0,
287                      NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_POINTER);
288 }
289
290
291 static void gtk_narrative_view_init(GtkNarrativeView *e)
292 {
293         e->vpol = GTK_SCROLL_NATURAL;
294         e->hpol = GTK_SCROLL_NATURAL;
295         e->vadj = gtk_adjustment_new(0, 0, 100, 1, 10, 10);
296         e->hadj = gtk_adjustment_new(0, 0, 100, 1, 10, 10);
297 }
298
299
300 static void paste_storycode_received(GtkClipboard *cb, GtkSelectionData *seldata,
301                                      gpointer vp)
302 {
303 //      GtkNarrativeView *e = vp;
304 //      SCBlock *nf;
305 //      const guchar *t;
306 //
307 //      t = gtk_selection_data_get_data(seldata);
308 //
309 //      printf("received storycode paste\n");
310 //      printf("'%s'\n", t);
311 //      if ( t == NULL ) return;
312 //
313 //      /* FIXME: It might not be a new frame */
314 //      nf = sc_parse((char *)t);
315 //      show_sc_blocks(nf);
316 //      sc_block_append_block(sc_block_child(e->scblocks), nf);
317 //      full_rerender(e);
318 }
319
320
321 static void paste_text_received(GtkClipboard *cb, GtkSelectionData *seldata,
322                                 gpointer vp)
323 {
324 //      GtkNarrativeView *e = vp;
325 //      SCBlock *bl;
326 //      guchar *t;
327 //      SCBlock *cur_bl;
328 //      size_t cur_sc_pos;
329 //      size_t offs;
330 //      Paragraph *para;
331 //
332 //      t = gtk_selection_data_get_text(seldata);
333 //
334 //      printf("received text paste\n");
335 //      printf("'%s'\n", t);
336 //      if ( t == NULL ) return;
337 //
338 //      bl = sc_parse((char *)t);
339 //
340 //      if ( e->cursor_frame == NULL ) {
341 //              fprintf(stderr, _("No frame selected for paste\n"));
342 //              return;
343 //      }
344 //
345 //      para = e->cursor_frame->paras[e->cpos.para];
346 //      offs = pos_trail_to_offset(para, e->cpos.pos, e->cpos.trail);
347 //
348 //      get_sc_pos(e->cursor_frame, e->cpos.para, offs, &cur_bl, &cur_sc_pos);
349 //      sc_insert_block(cur_bl, cur_sc_pos, bl);
350 //      full_rerender(e);
351 }
352
353
354 static void paste_targets_received(GtkClipboard *cb, GdkAtom *targets,
355                                    gint n_targets, gpointer vp)
356 {
357         GtkNarrativeView *e = vp;
358         int i;
359         int have_sc = 0;
360         int index_sc, index_text;
361         int have_text = 0;
362
363         if ( targets == NULL ) {
364                 fprintf(stderr, "No paste targets offered.\n");
365                 return;
366         }
367
368         for ( i=0; i<n_targets; i++ ) {
369                 gchar *name = gdk_atom_name(targets[i]);
370                 if ( g_strcmp0(name, "text/x-storycode") == 0 ) {
371                         have_sc = 1;
372                         index_sc = i;
373                 }
374                 if ( g_strcmp0(name, "text/plain") == 0 ) {
375                         have_text = 1;
376                         index_text = i;
377                 }
378                 g_free(name);
379         }
380
381         if ( have_sc ) {
382                 printf("storycode is offered\n");
383                 gtk_clipboard_request_contents(cb, targets[index_sc],
384                                                paste_storycode_received, e);
385         } else if ( have_text ) {
386                 printf("text is offered\n");
387                 gtk_clipboard_request_contents(cb, targets[index_text],
388                                                paste_text_received, e);
389         } else {
390                 printf("nothing useful is offered\n");
391         }
392 }
393
394
395 void sc_editor_paste(GtkNarrativeView *e)
396 {
397         GtkClipboard *cb;
398         GdkAtom atom;
399
400         printf("pasting\n");
401
402         atom = gdk_atom_intern("CLIPBOARD", FALSE);
403         if ( atom == GDK_NONE ) return;
404         cb = gtk_clipboard_get(atom);
405         gtk_clipboard_request_targets(cb, paste_targets_received, e);
406 }
407
408
409 static void clipboard_get(GtkClipboard *cb, GtkSelectionData *seldata,
410                           guint info, gpointer data)
411 {
412         char *t = data;
413
414         printf("clipboard get\n");
415
416         if ( info == 0 ) {
417                 printf("sending SC frame\n");
418                 gtk_selection_data_set(seldata,
419                                        gtk_selection_data_get_target(seldata),
420                                        8, (const guchar *)t, strlen(t)+1);
421         } else {
422                 GdkAtom target;
423                 gchar *name;
424                 target = gtk_selection_data_get_target(seldata);
425                 name = gdk_atom_name(target);
426                 fprintf(stderr, "Don't know what to send for %s\n", name);
427                 g_free(name);
428         }
429 }
430
431
432 static void clipboard_clear(GtkClipboard *cb, gpointer data)
433 {
434         free(data);
435 }
436
437
438 static void copy_selection(GtkNarrativeView *e)
439 {
440 //      char *t;
441 //      GtkClipboard *cb;
442 //      GdkAtom atom;
443 //      GtkTargetEntry targets[1];
444 //
445 //      atom = gdk_atom_intern("CLIPBOARD", FALSE);
446 //      if ( atom == GDK_NONE ) return;
447 //
448 //      cb = gtk_clipboard_get(atom);
449 //
450 //      targets[0].target = "text/x-storycode";
451 //      targets[0].flags = 0;
452 //      targets[0].info = 0;
453 //
454 //      printf("copying selection\n");
455 //
456 //      bl = block_at_cursor(e->cursor_frame, e->cpos.para, 0);
457 //      if ( bl == NULL ) return;
458 //
459 //      t = serialise_sc_block(bl);
460 //
461 //      gtk_clipboard_set_with_data(cb, targets, 1,
462 //                                  clipboard_get, clipboard_clear, t);
463 }
464
465
466 static gint destroy_sig(GtkWidget *window, GtkNarrativeView *e)
467 {
468         return 0;
469 }
470
471
472 static double para_top(Narrative *n, int pnum)
473 {
474         int i;
475         double py = 0.0;
476         for ( i=0; i<pnum; i++ ) py += n->items[i].h;
477         return py + n->items[pnum].space_t;
478 }
479
480
481 static void draw_para_highlight(cairo_t *cr, Narrative *n, int cursor_para,
482                                 double w)
483 {
484         double cx, cy, cw, ch;
485         struct narrative_item *item;
486
487         item = &n->items[cursor_para];
488         cx = n->space_l;
489         cy = n->space_t + para_top(n, cursor_para);
490         cw = w - n->space_l - n->space_r;
491
492
493         if ( item->type == NARRATIVE_ITEM_SLIDE ) {
494                 ch = item->slide_h;
495         } else {
496                 if ( item->layout != NULL ) {
497                         PangoRectangle rect;
498                         pango_layout_get_extents(item->layout, NULL, &rect);
499                         ch = pango_units_to_double(rect.height);
500                 } else {
501                         ch = 0.0;
502                         fprintf(stderr, "No layout when drawing highlight box\n");
503                 }
504         }
505
506         cairo_new_path(cr);
507         cairo_rectangle(cr, cx, cy, cw, ch);
508         cairo_set_source_rgba(cr, 0.7, 0.7, 1.0, 0.5);
509         cairo_set_line_width(cr, 5.0);
510         cairo_stroke(cr);
511 }
512
513
514 static size_t pos_trail_to_offset(struct narrative_item *item, int offs, int trail)
515 {
516         glong char_offs;
517         char *ptr;
518
519         if ( item->type == NARRATIVE_ITEM_SLIDE ) {
520                 return offs;
521         }
522
523         char_offs = g_utf8_pointer_to_offset(item->text, item->text+offs);
524         char_offs += trail;
525         ptr = g_utf8_offset_to_pointer(item->text, char_offs);
526         return ptr - item->text;
527 }
528
529
530 static void get_cursor_pos(Narrative *n, struct edit_pos cpos,
531                            double *x, double *y, double *h)
532 {
533         size_t offs;
534         PangoRectangle rect;
535         struct narrative_item *item;
536
537         item = &n->items[cpos.para];
538         if ( item->type == NARRATIVE_ITEM_SLIDE ) {
539                 *x = n->space_l + item->space_l;
540                 *y = n->space_t + para_top(n, cpos.para);
541                 *h = item->slide_h;
542                 return;
543         }
544
545         if ( item->layout == NULL ) {
546                 fprintf(stderr, "get_cursor_pos: No layout\n");
547                 return;
548         }
549
550         offs = pos_trail_to_offset(item, cpos.pos, cpos.trail);
551         pango_layout_get_cursor_pos(item->layout, offs, &rect, NULL);
552         *x = pango_units_to_double(rect.x) + n->space_l + item->space_l;
553         *y = pango_units_to_double(rect.y) + n->space_t + para_top(n, cpos.para);
554         *h = pango_units_to_double(rect.height);
555 }
556
557
558 static void draw_caret(cairo_t *cr, Narrative *n, struct edit_pos cpos,
559                        int hgh, double ww)
560 {
561         double cx, clow, chigh, h;
562         const double t = 1.8;
563
564         if ( hgh ) {
565                 draw_para_highlight(cr, n, cpos.para, ww);
566                 return;
567         }
568
569         assert(n != NULL);
570
571         if ( n->items[cpos.para].type == NARRATIVE_ITEM_SLIDE ) {
572                 draw_para_highlight(cr, n, cpos.para, ww);
573                 return;
574         }
575
576         get_cursor_pos(n, cpos, &cx, &clow, &h);
577
578         chigh = clow+h;
579
580         cairo_move_to(cr, cx, clow);
581         cairo_line_to(cr, cx, chigh);
582
583         cairo_move_to(cr, cx-t, clow-t);
584         cairo_line_to(cr, cx, clow);
585         cairo_move_to(cr, cx+t, clow-t);
586         cairo_line_to(cr, cx, clow);
587
588         cairo_move_to(cr, cx-t, chigh+t);
589         cairo_line_to(cr, cx, chigh);
590         cairo_move_to(cr, cx+t, chigh+t);
591         cairo_line_to(cr, cx, chigh);
592
593         cairo_set_source_rgb(cr, 0.86, 0.0, 0.0);
594         cairo_set_line_width(cr, 1.0);
595         cairo_stroke(cr);
596 }
597
598
599 static void draw_overlay(cairo_t *cr, GtkNarrativeView *e)
600 {
601         draw_caret(cr, e->n, e->cpos, e->para_highlight, e->w);
602 }
603
604
605 static gboolean draw_sig(GtkWidget *da, cairo_t *cr, GtkNarrativeView *e)
606 {
607         if ( e->rewrap_needed ) {
608                 rewrap_range(e, 0, e->n->n_items);
609                 e->rewrap_needed = 0;
610         }
611
612         /* Ultimate background */
613         cairo_set_source_rgba(cr, 0.8, 0.8, 1.0, 1.0);
614         cairo_paint(cr);
615
616         cairo_translate(cr, -e->h_scroll_pos, -e->scroll_pos);
617
618         /* Rendering background */
619         cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 1.0);
620         cairo_rectangle(cr, 0.0, 0.0, e->w, e->h);
621         cairo_fill(cr);
622
623         /* Contents */
624         narrative_render_cairo(e->n, cr, narrative_get_stylesheet(e->n));
625
626         /* Editing overlay */
627         draw_overlay(cr, e);
628
629         return FALSE;
630 }
631
632
633 static void check_cursor_visible(GtkNarrativeView *e)
634 {
635         double x, y, h;
636
637         get_cursor_pos(e->n, e->cpos, &x, &y, &h);
638
639         /* Off the bottom? */
640         if ( y - e->scroll_pos + h > e->visible_height ) {
641                 e->scroll_pos = y + h - e->visible_height;
642                 e->scroll_pos += e->n->space_b;
643         }
644
645         /* Off the top? */
646         if ( y < e->scroll_pos ) {
647                 e->scroll_pos = y - e->n->space_t;
648         }
649 }
650
651
652 static size_t end_offset_of_para(Narrative *n, int pnum)
653 {
654         assert(pnum >= 0);
655         if ( n->items[pnum].type == NARRATIVE_ITEM_SLIDE ) return 0;
656         return strlen(n->items[pnum].text);
657 }
658
659
660 static void sort_positions(struct edit_pos *a, struct edit_pos *b)
661 {
662         if ( a->para > b->para ) {
663                 size_t tpos;
664                 int tpara, ttrail;
665                 tpara = b->para;   tpos = b->pos;  ttrail = b->trail;
666                 b->para = a->para;  b->pos = a->pos;  b->trail = a->trail;
667                 a->para = tpara;    a->pos = tpos;    a->trail = ttrail;
668         }
669
670         if ( (a->para == b->para) && (a->pos > b->pos) )
671         {
672                 size_t tpos = b->pos;
673                 int ttrail = b->trail;
674                 b->pos = a->pos;  b->trail = a->trail;
675                 a->pos = tpos;    a->trail = ttrail;
676         }
677 }
678
679
680 static void cursor_moveh(Narrative *n, struct edit_pos *cp, signed int dir)
681 {
682         struct narrative_item *item = &n->items[cp->para];
683         int np = cp->pos;
684         int otrail = cp->trail;
685
686         if ( item->type == NARRATIVE_ITEM_SLIDE ) {
687                 if ( dir > 0 ) {
688                         np = G_MAXINT;
689                         cp->trail = 0;
690                 } else {
691                         np = -1;
692                         cp->trail = 0;
693                 }
694         } else {
695                 if ( item->layout == NULL ) return;
696                 pango_layout_move_cursor_visually(item->layout, 1, cp->pos,
697                                                   cp->trail, dir,
698                                                   &np, &cp->trail);
699         }
700
701         if ( np == -1 ) {
702                 if ( cp->para > 0 ) {
703                         size_t end_offs;
704                         cp->para--;
705                         end_offs = end_offset_of_para(n, cp->para);
706                         if ( end_offs > 0 ) {
707                                 cp->pos = end_offs - 1;
708                                 cp->trail = 1;
709                         } else {
710                                 /* Jumping into an empty paragraph */
711                                 cp->pos = 0;
712                                 cp->trail = 0;
713                         }
714                         return;
715                 } else {
716                         /* Can't move any further */
717                         return;
718                 }
719         }
720
721         if ( np == G_MAXINT ) {
722                 if ( cp->para < n->n_items-1 ) {
723                         cp->para++;
724                         cp->pos = 0;
725                         cp->trail = 0;
726                         return;
727                 } else {
728                         /* Can't move any further, undo change to cp->trail */
729                         cp->trail = otrail;
730                         return;
731                 }
732         }
733
734         cp->pos = np;
735 }
736
737
738 static void unset_selection(GtkNarrativeView *e)
739 {
740         int a, b;
741
742         a = e->sel_start.para;
743         b = e->sel_end.para;
744         if ( a > b ) {
745                 a = e->sel_end.para;
746                 b = e->sel_start.para;
747         }
748         e->sel_start.para = 0;
749         e->sel_start.pos = 0;
750         e->sel_start.trail = 0;
751         e->sel_end.para = 0;
752         e->sel_end.pos = 0;
753         e->sel_end.trail = 0;
754         rewrap_range(e, a, b);
755 }
756
757
758 static int positions_equal(struct edit_pos a, struct edit_pos b)
759 {
760         if ( a.para != b.para ) return 0;
761         if ( a.pos != b.pos ) return 0;
762         if ( a.trail != b.trail ) return 0;
763         return 1;
764 }
765
766
767 static void do_backspace(GtkNarrativeView *e, signed int dir)
768 {
769         struct edit_pos p1, p2;
770         size_t o1, o2;
771
772         if ( !positions_equal(e->sel_start, e->sel_end) ) {
773
774                 /* Block delete */
775                 p1 = e->sel_start;
776                 p2 = e->sel_end;
777
778         } else {
779
780                 /* Delete one character, as represented visually */
781                 p2 = e->cpos;
782                 p1 = p2;
783                 cursor_moveh(e->n, &p1, dir);
784         }
785
786         sort_positions(&p1, &p2);
787         o1 = pos_trail_to_offset(&e->n->items[p1.para], p1.pos, p1.trail);
788         o2 = pos_trail_to_offset(&e->n->items[p2.para], p2.pos, p2.trail);
789         narrative_delete_block(e->n, p1.para, o1, p2.para, o2);
790         e->cpos = p1;
791         unset_selection(e);
792
793         /* The only paragraphs which still exist and might have been
794          * affected by the deletion are sel_start.para and the one
795          * immediately afterwards. */
796         rewrap_range(e, p1.para, p1.para+1);
797         update_size(e);
798         check_cursor_visible(e);
799         emit_change_sig(e);
800         redraw(e);
801 }
802
803
804 static void insert_text_in_paragraph(struct narrative_item *item, size_t offs,
805                                      char *t)
806 {
807         char *n = malloc(strlen(t) + strlen(item->text) + 1);
808         if ( n == NULL ) return;
809         strncpy(n, item->text, offs);
810         n[offs] = '\0';
811         strcat(n, t);
812         strcat(n, item->text+offs);
813         free(item->text);
814         item->text = n;
815 }
816
817
818 static void split_paragraph_at_cursor(Narrative *n, struct edit_pos *pos)
819 {
820         size_t off;
821
822         if ( n->items[pos->para].type != NARRATIVE_ITEM_SLIDE ) {
823                 off = pos_trail_to_offset(&n->items[pos->para],
824                                           pos->pos, pos->trail);
825         } else {
826                 off = 0;
827         }
828
829         if ( (off > 0) && (off < strlen(n->items[pos->para].text)) )  {
830                 narrative_split_item(n, pos->para, off);
831         } else if ( off == 0 ) {
832                 pos->para--;
833                 pos->pos = 0;
834                 pos->trail = 0;
835         }
836 }
837
838
839 static void insert_text(char *t, GtkNarrativeView *e)
840 {
841         struct narrative_item *item;
842
843         if ( !positions_equal(e->sel_start, e->sel_end) ) {
844                 do_backspace(e, 0);
845         }
846
847         item = &e->n->items[e->cpos.para];
848
849         if ( strcmp(t, "\n") == 0 ) {
850                 split_paragraph_at_cursor(e->n, &e->cpos);
851                 rewrap_range(e, e->cpos.para, e->cpos.para+1);
852                 update_size(e);
853                 cursor_moveh(e->n, &e->cpos, +1);
854                 check_cursor_visible(e);
855                 emit_change_sig(e);
856                 redraw(e);
857                 return;
858         }
859
860         if ( item->type != NARRATIVE_ITEM_SLIDE ) {
861
862                 size_t off;
863
864                 off = pos_trail_to_offset(item, e->cpos.pos, e->cpos.trail);
865                 insert_text_in_paragraph(item, off, t);
866                 rewrap_range(e, e->cpos.para, e->cpos.para);
867                 update_size(e);
868                 cursor_moveh(e->n, &e->cpos, +1);
869
870         } /* else do nothing: pressing enter is OK, though */
871
872         emit_change_sig(e);
873         check_cursor_visible(e);
874         redraw(e);
875 }
876
877
878 static gboolean im_commit_sig(GtkIMContext *im, gchar *str,
879                               GtkNarrativeView *e)
880 {
881         insert_text(str, e);
882         return FALSE;
883 }
884
885
886 static int find_cursor(Narrative *n, double x, double y, struct edit_pos *pos)
887 {
888         double cur_y;
889         struct narrative_item *item;
890         int i = 0;
891
892         cur_y = n->space_t;
893
894         do {
895                 cur_y += n->items[i++].h;
896         } while ( (cur_y < y) && (i<n->n_items) );
897
898         pos->para = i-1;
899         item = &n->items[pos->para];
900         if ( item->type == NARRATIVE_ITEM_SLIDE ) {
901                 pos->pos = 0;
902                 return 0;
903         }
904
905         pango_layout_xy_to_index(item->layout,
906                                  pango_units_from_double(x - n->space_l - item->space_l),
907                                  pango_units_from_double(y - n->space_t - para_top(n, pos->para)),
908                                  &pos->pos, &pos->trail);
909
910         return 0;
911 }
912
913
914 static gboolean button_press_sig(GtkWidget *da, GdkEventButton *event,
915                                  GtkNarrativeView *e)
916 {
917         gdouble x, y;
918
919         x = event->x;
920         y = event->y + e->scroll_pos;
921
922         /* Clicked an existing frame, no immediate dragging */
923         e->drag_status = DRAG_STATUS_COULD_DRAG;
924         unset_selection(e);
925         find_cursor(e->n, x, y, &e->sel_start);
926         e->sel_end = e->sel_start;
927         e->cpos = e->sel_start;
928
929         if ( event->type == GDK_2BUTTON_PRESS ) {
930                 struct narrative_item *item = &e->n->items[e->cpos.para];
931                 if ( item->type == NARRATIVE_ITEM_SLIDE ) {
932                         g_signal_emit_by_name(e, "slide-double-clicked",
933                                               item->slide);
934                 }
935         }
936
937         gtk_widget_grab_focus(GTK_WIDGET(da));
938         redraw(e);
939         return FALSE;
940 }
941
942
943 static void sorti(int *a, int *b)
944 {
945         if ( *a > *b ) {
946                 int tmp = *a;
947                 *a = *b;
948                 *b = tmp;
949         }
950 }
951
952
953 static gboolean motion_sig(GtkWidget *da, GdkEventMotion *event,
954                            GtkNarrativeView *e)
955 {
956         gdouble x, y;
957         struct edit_pos old_sel_end;
958         int minp, maxp;
959
960         x = event->x;
961         y = event->y + e->scroll_pos;
962
963         if ( e->drag_status == DRAG_STATUS_COULD_DRAG ) {
964                 /* We just got a motion signal, and the status was "could drag",
965                  * therefore the drag has started. */
966                 e->drag_status = DRAG_STATUS_DRAGGING;
967         }
968
969         old_sel_end = e->sel_end;
970         find_cursor(e->n, x, y, &e->sel_end);
971
972         minp = e->sel_start.para;
973         maxp = e->sel_end.para;
974         sorti(&minp, &maxp);
975         if ( !positions_equal(e->sel_start, old_sel_end) ) {
976                 if ( old_sel_end.para > maxp ) maxp = old_sel_end.para;
977                 if ( old_sel_end.para < minp ) minp = old_sel_end.para;
978         }
979
980         rewrap_range(e, minp, maxp);
981         find_cursor(e->n, x, y, &e->cpos);
982         redraw(e);
983
984         gdk_event_request_motions(event);
985         return FALSE;
986 }
987
988
989 static gboolean key_press_sig(GtkWidget *da, GdkEventKey *event,
990                               GtkNarrativeView *e)
991 {
992         gboolean r;
993         int claim = 0;
994
995         /* Throw the event to the IM context and let it sort things out */
996         r = gtk_im_context_filter_keypress(GTK_IM_CONTEXT(e->im_context),
997                                            event);
998         if ( r ) return FALSE;  /* IM ate it */
999
1000         switch ( event->keyval ) {
1001
1002                 case GDK_KEY_Left :
1003                 cursor_moveh(e->n, &e->cpos, -1);
1004                 check_cursor_visible(e);
1005                 redraw(e);
1006                 claim = 1;
1007                 break;
1008
1009                 case GDK_KEY_Right :
1010                 cursor_moveh(e->n, &e->cpos, +1);
1011                 check_cursor_visible(e);
1012                 redraw(e);
1013                 claim = 1;
1014                 break;
1015
1016                 case GDK_KEY_Up :
1017                 cursor_moveh(e->n, &e->cpos, -1);
1018                 check_cursor_visible(e);
1019                 redraw(e);
1020                 claim = 1;
1021                 break;
1022
1023                 case GDK_KEY_Down :
1024                 cursor_moveh(e->n, &e->cpos, +1);
1025                 check_cursor_visible(e);
1026                 redraw(e);
1027                 claim = 1;
1028                 break;
1029
1030                 case GDK_KEY_Return :
1031                 im_commit_sig(NULL, "\n", e);
1032                 claim = 1;
1033                 break;
1034
1035                 case GDK_KEY_BackSpace :
1036                 do_backspace(e, -1);
1037                 claim = 1;
1038                 break;
1039
1040                 case GDK_KEY_Delete :
1041                 do_backspace(e, +1);
1042                 claim = 1;
1043                 break;
1044
1045                 case GDK_KEY_C :
1046                 case GDK_KEY_c :
1047                 if ( event->state == GDK_CONTROL_MASK ) {
1048                         copy_selection(e);
1049                 }
1050                 break;
1051
1052                 case GDK_KEY_V :
1053                 case GDK_KEY_v :
1054                 if ( event->state == GDK_CONTROL_MASK ) {
1055                         sc_editor_paste(e);
1056                 }
1057                 break;
1058
1059
1060         }
1061
1062         if ( claim ) return TRUE;
1063         return FALSE;
1064 }
1065
1066
1067 static gboolean dnd_motion(GtkWidget *widget, GdkDragContext *drag_context,
1068                            gint x, gint y, guint time, GtkNarrativeView *e)
1069 {
1070         return TRUE;
1071 }
1072
1073
1074 static gboolean dnd_drop(GtkWidget *widget, GdkDragContext *drag_context,
1075                          gint x, gint y, guint time, GtkNarrativeView *e)
1076 {
1077         GdkAtom target;
1078
1079         target = gtk_drag_dest_find_target(widget, drag_context, NULL);
1080
1081         if ( target == GDK_NONE ) {
1082                 gtk_drag_finish(drag_context, FALSE, FALSE, time);
1083         } else {
1084                 gtk_drag_get_data(widget, drag_context, target, time);
1085         }
1086
1087         return TRUE;
1088 }
1089
1090
1091 static void dnd_receive(GtkWidget *widget, GdkDragContext *drag_context,
1092                         gint x, gint y, GtkSelectionData *seldata,
1093                         guint info, guint time, GtkNarrativeView *e)
1094 {
1095 }
1096
1097
1098 static void dnd_leave(GtkWidget *widget, GdkDragContext *drag_context,
1099                       guint time, GtkNarrativeView *nview)
1100 {
1101         nview->drag_status = DRAG_STATUS_NONE;
1102 }
1103
1104
1105 static gint realise_sig(GtkWidget *da, GtkNarrativeView *e)
1106 {
1107         GdkWindow *win;
1108
1109         /* Keyboard and input method stuff */
1110         e->im_context = gtk_im_multicontext_new();
1111         win = gtk_widget_get_window(GTK_WIDGET(e));
1112         gtk_im_context_set_client_window(GTK_IM_CONTEXT(e->im_context), win);
1113         gdk_window_set_accept_focus(win, TRUE);
1114         g_signal_connect(G_OBJECT(e->im_context), "commit", G_CALLBACK(im_commit_sig), e);
1115         g_signal_connect(G_OBJECT(e), "key-press-event", G_CALLBACK(key_press_sig), e);
1116
1117         return FALSE;
1118 }
1119
1120
1121 GtkWidget *gtk_narrative_view_new(Narrative *n)
1122 {
1123         GtkNarrativeView *nview;
1124         GtkTargetEntry targets[1];
1125
1126         nview = g_object_new(GTK_TYPE_NARRATIVE_VIEW, NULL);
1127
1128         nview->w = 100;
1129         nview->h = 100;
1130         nview->scroll_pos = 0;
1131         nview->n = n;
1132         nview->rewrap_needed = 0;
1133         nview->para_highlight = 0;
1134
1135         gtk_widget_set_size_request(GTK_WIDGET(nview),
1136                                     nview->w, nview->h);
1137
1138         g_signal_connect(G_OBJECT(nview), "destroy",
1139                          G_CALLBACK(destroy_sig), nview);
1140         g_signal_connect(G_OBJECT(nview), "realize",
1141                          G_CALLBACK(realise_sig), nview);
1142         g_signal_connect(G_OBJECT(nview), "button-press-event",
1143                          G_CALLBACK(button_press_sig), nview);
1144         g_signal_connect(G_OBJECT(nview), "motion-notify-event",
1145                          G_CALLBACK(motion_sig), nview);
1146         g_signal_connect(G_OBJECT(nview), "configure-event",
1147                          G_CALLBACK(resize_sig), nview);
1148
1149         /* Drag and drop */
1150         targets[0].target = "text/uri-list";
1151         targets[0].flags = 0;
1152         targets[0].info = 1;
1153         gtk_drag_dest_set(GTK_WIDGET(nview), 0, targets, 1,
1154                           GDK_ACTION_PRIVATE);
1155         g_signal_connect(nview, "drag-data-received",
1156                          G_CALLBACK(dnd_receive), nview);
1157         g_signal_connect(nview, "drag-motion",
1158                          G_CALLBACK(dnd_motion), nview);
1159         g_signal_connect(nview, "drag-drop",
1160                          G_CALLBACK(dnd_drop), nview);
1161         g_signal_connect(nview, "drag-leave",
1162                          G_CALLBACK(dnd_leave), nview);
1163
1164         gtk_widget_set_can_focus(GTK_WIDGET(nview), TRUE);
1165         gtk_widget_add_events(GTK_WIDGET(nview),
1166                               GDK_POINTER_MOTION_HINT_MASK
1167                                | GDK_BUTTON1_MOTION_MASK
1168                                | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
1169                                | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK
1170                                | GDK_SCROLL_MASK);
1171
1172         g_signal_connect(G_OBJECT(nview), "draw",
1173                          G_CALLBACK(draw_sig), nview);
1174
1175         gtk_widget_grab_focus(GTK_WIDGET(nview));
1176
1177         gtk_widget_show(GTK_WIDGET(nview));
1178
1179         return GTK_WIDGET(nview);
1180 }
1181
1182
1183 void gtk_narrative_view_set_para_highlight(GtkNarrativeView *e, int para_highlight)
1184 {
1185         e->para_highlight = para_highlight;
1186         redraw(e);
1187 }
1188
1189
1190 int gtk_narrative_view_get_cursor_para(GtkNarrativeView *e)
1191 {
1192         return e->cpos.para;
1193 }
1194
1195
1196 void gtk_narrative_view_set_cursor_para(GtkNarrativeView *e, signed int pos)
1197 {
1198         double h;
1199         int i;
1200
1201         if ( pos < 0 ) pos = e->n->n_items-1;
1202         e->cpos.para = pos;
1203         e->cpos.pos = 0;
1204         e->cpos.trail = 0;
1205
1206         h = 0;
1207         for ( i=0; i<e->cpos.para; i++ ) {
1208                 h += narrative_item_get_height(e->n, i);
1209         }
1210         h += narrative_item_get_height(e->n, e->cpos.para)/2;
1211         e->scroll_pos = h - (e->visible_height/2);
1212         set_vertical_params(e);
1213
1214         redraw(e);
1215 }
1216
1217
1218 void gtk_narrative_view_add_slide_at_cursor(GtkNarrativeView *e)
1219 {
1220         Slide *s;
1221
1222         s = slide_new();
1223         if ( s == NULL ) return;
1224
1225         split_paragraph_at_cursor(e->n, &e->cpos);
1226         narrative_insert_slide(e->n, s, e->cpos.para+1);
1227
1228         rewrap_range(e, e->cpos.para, e->cpos.para+2);
1229         e->cpos.para++;
1230         e->cpos.pos = 0;
1231         e->cpos.trail = 0;
1232         update_size(e);
1233         check_cursor_visible(e);
1234         emit_change_sig(e);
1235         redraw(e);
1236 }
1237
1238
1239 extern void gtk_narrative_view_redraw(GtkNarrativeView *e)
1240 {
1241         e->rewrap_needed = 1;
1242         emit_change_sig(e);
1243         redraw(e);
1244 }