Fix run determination when inserting text
[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 = narrative_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 += narrative_item_get_height(n, i);
477         return py;
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;
485         struct narrative_item *item;
486
487         item = &n->items[cursor_para];
488         cx = item->space_l;
489         cy = para_top(n, cursor_para) + item->space_t;
490
491         cairo_new_path(cr);
492         cairo_rectangle(cr, cx-5.0, cy-5.0, item->obj_w+10.0, item->obj_h+10.0);
493         cairo_set_source_rgba(cr, 0.7, 0.7, 1.0, 0.5);
494         cairo_set_line_width(cr, 5.0);
495         cairo_stroke(cr);
496 }
497
498
499 static void get_cursor_pos(Narrative *n, struct edit_pos cpos,
500                            double *x, double *y, double *h)
501 {
502         size_t offs;
503         PangoRectangle rect;
504         struct narrative_item *item;
505
506         item = &n->items[cpos.para];
507         if ( !narrative_item_is_text(n, cpos.para) ) {
508                 *x = item->space_l;
509                 *y = para_top(n, cpos.para) + item->space_t;
510                 *h = item->obj_h + item->space_t + item->space_b;
511                 return;
512         }
513
514         if ( item->layout == NULL ) {
515                 fprintf(stderr, "get_cursor_pos: No layout\n");
516                 return;
517         }
518
519         offs = narrative_pos_trail_to_offset(n, cpos.para, cpos.pos, cpos.trail);
520         pango_layout_get_cursor_pos(item->layout, offs, &rect, NULL);
521         *x = pango_units_to_double(rect.x) + item->space_l;
522         *y = pango_units_to_double(rect.y) + para_top(n, cpos.para) + item->space_t;
523         *h = pango_units_to_double(rect.height);
524 }
525
526
527 static void draw_caret(cairo_t *cr, Narrative *n, struct edit_pos cpos, double ww)
528 {
529         assert(n != NULL);
530
531         if ( narrative_item_is_text(n, cpos.para) ) {
532
533                 /* Normal text-style cursor */
534
535                 double cx, clow, chigh, h;
536                 const double t = 1.8;
537
538                 get_cursor_pos(n, cpos, &cx, &clow, &h);
539
540                 chigh = clow+h;
541
542                 cairo_move_to(cr, cx, clow);
543                 cairo_line_to(cr, cx, chigh);
544
545                 cairo_move_to(cr, cx-t, clow-t);
546                 cairo_line_to(cr, cx, clow);
547                 cairo_move_to(cr, cx+t, clow-t);
548                 cairo_line_to(cr, cx, clow);
549
550                 cairo_move_to(cr, cx-t, chigh+t);
551                 cairo_line_to(cr, cx, chigh);
552                 cairo_move_to(cr, cx+t, chigh+t);
553                 cairo_line_to(cr, cx, chigh);
554
555                 cairo_set_source_rgb(cr, 0.86, 0.0, 0.0);
556                 cairo_set_line_width(cr, 1.0);
557                 cairo_stroke(cr);
558
559         } else {
560
561                 /* Block highlight cursor */
562
563                 double cx, cy, cw, ch;
564                 struct narrative_item *item;
565
566                 item = &n->items[cpos.para];
567
568                 cx = item->space_l - 5.5;
569                 cy = para_top(n, cpos.para) + item->space_t - 5.5;
570                 cw = item->obj_w + 11.0;
571                 ch = item->obj_h + 11.0;
572
573                 cairo_new_path(cr);
574                 cairo_rectangle(cr, cx, cy, cw, ch);
575                 cairo_set_source_rgb(cr, 0.86, 0.0, 0.0);
576                 cairo_set_line_width(cr, 1.0);
577                 cairo_stroke(cr);
578
579         }
580 }
581
582
583 static void draw_overlay(cairo_t *cr, GtkNarrativeView *e)
584 {
585         if ( e->para_highlight ) {
586                 draw_para_highlight(cr, e->n, e->cpos.para, e->w);
587         } else {
588                 draw_caret(cr, e->n, e->cpos, e->w);
589         }
590 }
591
592
593 static gboolean draw_sig(GtkWidget *da, cairo_t *cr, GtkNarrativeView *e)
594 {
595         if ( e->rewrap_needed ) {
596                 rewrap_range(e, 0, e->n->n_items);
597                 e->rewrap_needed = 0;
598         }
599
600         /* Ultimate background */
601         cairo_set_source_rgba(cr, 0.8, 0.8, 1.0, 1.0);
602         cairo_paint(cr);
603
604         cairo_translate(cr, -e->h_scroll_pos, -e->scroll_pos);
605
606         /* Rendering background */
607         cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 1.0);
608         cairo_rectangle(cr, 0.0, 0.0, e->w, e->h);
609         cairo_fill(cr);
610
611         /* Contents */
612         narrative_render_cairo(e->n, cr, narrative_get_stylesheet(e->n));
613
614         /* Editing overlay */
615         cairo_translate(cr, e->n->space_l, e->n->space_t);
616         draw_overlay(cr, e);
617
618         return FALSE;
619 }
620
621
622 static void check_cursor_visible(GtkNarrativeView *e)
623 {
624         double x, y, h;
625
626         get_cursor_pos(e->n, e->cpos, &x, &y, &h);
627
628         /* Off the bottom? */
629         if ( y - e->scroll_pos + h > e->visible_height ) {
630                 e->scroll_pos = y + h - e->visible_height;
631                 e->scroll_pos += e->n->space_b;
632         }
633
634         /* Off the top? */
635         if ( y < e->scroll_pos ) {
636                 e->scroll_pos = y - e->n->space_t;
637         }
638 }
639
640
641 static size_t end_offset_of_para(Narrative *n, int pnum)
642 {
643         int i;
644         size_t len;
645         assert(pnum >= 0);
646         if ( !narrative_item_is_text(n, pnum) ) return 0;
647         for ( i=0; i<n->items[pnum].n_runs; i++ ) {
648                 len += strlen(n->items[pnum].runs[i].text);
649         }
650         return len;
651 }
652
653
654 static void sort_positions(struct edit_pos *a, struct edit_pos *b)
655 {
656         if ( a->para > b->para ) {
657                 size_t tpos;
658                 int tpara, ttrail;
659                 tpara = b->para;   tpos = b->pos;  ttrail = b->trail;
660                 b->para = a->para;  b->pos = a->pos;  b->trail = a->trail;
661                 a->para = tpara;    a->pos = tpos;    a->trail = ttrail;
662         }
663
664         if ( (a->para == b->para) && (a->pos > b->pos) )
665         {
666                 size_t tpos = b->pos;
667                 int ttrail = b->trail;
668                 b->pos = a->pos;  b->trail = a->trail;
669                 a->pos = tpos;    a->trail = ttrail;
670         }
671 }
672
673
674 static void cursor_moveh(Narrative *n, struct edit_pos *cp, signed int dir)
675 {
676         struct narrative_item *item = &n->items[cp->para];
677         int np = cp->pos;
678         int otrail = cp->trail;
679
680         if ( !narrative_item_is_text(n, cp->para) ) {
681                 if ( dir > 0 ) {
682                         np = G_MAXINT;
683                         cp->trail = 0;
684                 } else {
685                         np = -1;
686                         cp->trail = 0;
687                 }
688         } else {
689                 if ( item->layout == NULL ) return;
690                 pango_layout_move_cursor_visually(item->layout, 1, cp->pos,
691                                                   cp->trail, dir,
692                                                   &np, &cp->trail);
693         }
694
695         if ( np == -1 ) {
696                 if ( cp->para > 0 ) {
697                         size_t end_offs;
698                         cp->para--;
699                         end_offs = end_offset_of_para(n, cp->para);
700                         if ( end_offs > 0 ) {
701                                 cp->pos = end_offs - 1;
702                                 cp->trail = 1;
703                         } else {
704                                 /* Jumping into an empty paragraph */
705                                 cp->pos = 0;
706                                 cp->trail = 0;
707                         }
708                         return;
709                 } else {
710                         /* Can't move any further */
711                         return;
712                 }
713         }
714
715         if ( np == G_MAXINT ) {
716                 if ( cp->para < n->n_items-1 ) {
717                         cp->para++;
718                         cp->pos = 0;
719                         cp->trail = 0;
720                         return;
721                 } else {
722                         /* Can't move any further, undo change to cp->trail */
723                         cp->trail = otrail;
724                         return;
725                 }
726         }
727
728         cp->pos = np;
729 }
730
731
732 static void unset_selection(GtkNarrativeView *e)
733 {
734         int a, b;
735
736         a = e->sel_start.para;
737         b = e->sel_end.para;
738         if ( a > b ) {
739                 a = e->sel_end.para;
740                 b = e->sel_start.para;
741         }
742         e->sel_start.para = 0;
743         e->sel_start.pos = 0;
744         e->sel_start.trail = 0;
745         e->sel_end.para = 0;
746         e->sel_end.pos = 0;
747         e->sel_end.trail = 0;
748         rewrap_range(e, a, b);
749 }
750
751
752 static int positions_equal(struct edit_pos a, struct edit_pos b)
753 {
754         if ( a.para != b.para ) return 0;
755         if ( a.pos != b.pos ) return 0;
756         if ( a.trail != b.trail ) return 0;
757         return 1;
758 }
759
760
761 static void do_backspace(GtkNarrativeView *e, signed int dir)
762 {
763         struct edit_pos p1, p2;
764         size_t o1, o2;
765
766         if ( !positions_equal(e->sel_start, e->sel_end) ) {
767
768                 /* Block delete */
769                 p1 = e->sel_start;
770                 p2 = e->sel_end;
771
772         } else {
773
774                 /* Delete one character, as represented visually */
775                 p2 = e->cpos;
776                 p1 = p2;
777                 cursor_moveh(e->n, &p1, dir);
778         }
779
780         sort_positions(&p1, &p2);
781         o1 = narrative_pos_trail_to_offset(e->n, p1.para, p1.pos, p1.trail);
782         o2 = narrative_pos_trail_to_offset(e->n, p2.para, p2.pos, p2.trail);
783         narrative_delete_block(e->n, p1.para, o1, p2.para, o2);
784         e->cpos = p1;
785         unset_selection(e);
786
787         /* The only paragraphs which still exist and might have been
788          * affected by the deletion are sel_start.para and the one
789          * immediately afterwards. */
790         rewrap_range(e, p1.para, p1.para+1);
791         update_size(e);
792         check_cursor_visible(e);
793         emit_change_sig(e);
794         redraw(e);
795 }
796
797
798 static void insert_text_in_paragraph(struct narrative_item *item, size_t offs,
799                                      char *t)
800 {
801         char *n;
802         int run;
803         size_t pos;
804
805         pos = 0;
806         for ( run=0; run<item->n_runs; run++ ) {
807                 size_t npos = pos + strlen(item->runs[run].text);
808                 if ( npos >= offs ) break;
809                 pos = npos;
810         }
811         offs -= pos;
812
813         n = malloc(strlen(t) + strlen(item->runs[run].text) + 1);
814         if ( n == NULL ) return;
815         strncpy(n, item->runs[run].text, offs);
816         n[offs] = '\0';
817         strcat(n, t);
818         strcat(n, item->runs[run].text+offs);
819         free(item->runs[run].text);
820         item->runs[run].text = n;
821 }
822
823
824 static void insert_text(char *t, GtkNarrativeView *e)
825 {
826         struct narrative_item *item;
827
828         if ( !positions_equal(e->sel_start, e->sel_end) ) {
829                 do_backspace(e, 0);
830         }
831
832         item = &e->n->items[e->cpos.para];
833
834         if ( narrative_item_is_text(e->n, e->cpos.para) ) {
835
836                 size_t off = narrative_pos_trail_to_offset(e->n, e->cpos.para, e->cpos.pos, e->cpos.trail);
837
838                 if ( strcmp(t, "\n") == 0 ) {
839                         narrative_split_item(e->n, e->cpos.para, off);
840                         rewrap_range(e, e->cpos.para, e->cpos.para+1);
841                 } else {
842                         insert_text_in_paragraph(item, off, t);
843                         rewrap_range(e, e->cpos.para, e->cpos.para);
844                 }
845
846                 update_size(e);
847                 cursor_moveh(e->n, &e->cpos, +1);
848
849         } /* else do nothing */
850
851         emit_change_sig(e);
852         check_cursor_visible(e);
853         redraw(e);
854 }
855
856
857 static gboolean im_commit_sig(GtkIMContext *im, gchar *str,
858                               GtkNarrativeView *e)
859 {
860         insert_text(str, e);
861         return FALSE;
862 }
863
864
865 static int find_cursor(Narrative *n, double x, double y, struct edit_pos *pos)
866 {
867         double cur_y;
868         struct narrative_item *item;
869         int i = 0;
870
871         cur_y = n->space_t;
872
873         do {
874                 cur_y += narrative_item_get_height(n, i++);
875         } while ( (cur_y < y) && (i<n->n_items) );
876
877         pos->para = i-1;
878         item = &n->items[pos->para];
879         if ( !narrative_item_is_text(n, pos->para) ) {
880                 pos->pos = 0;
881                 return 0;
882         }
883
884         pango_layout_xy_to_index(item->layout,
885                                  pango_units_from_double(x - n->space_l - item->space_l),
886                                  pango_units_from_double(y - n->space_t - para_top(n, pos->para)),
887                                  &pos->pos, &pos->trail);
888
889         return 0;
890 }
891
892
893 static gboolean button_press_sig(GtkWidget *da, GdkEventButton *event,
894                                  GtkNarrativeView *e)
895 {
896         gdouble x, y;
897
898         x = event->x;
899         y = event->y + e->scroll_pos;
900
901         /* Clicked an existing frame, no immediate dragging */
902         e->drag_status = DRAG_STATUS_COULD_DRAG;
903         unset_selection(e);
904         find_cursor(e->n, x, y, &e->sel_start);
905         e->sel_end = e->sel_start;
906         e->cpos = e->sel_start;
907
908         if ( event->type == GDK_2BUTTON_PRESS ) {
909                 struct narrative_item *item = &e->n->items[e->cpos.para];
910                 if ( item->type == NARRATIVE_ITEM_SLIDE ) {
911                         g_signal_emit_by_name(e, "slide-double-clicked",
912                                               item->slide);
913                 }
914         }
915
916         gtk_widget_grab_focus(GTK_WIDGET(da));
917         redraw(e);
918         return FALSE;
919 }
920
921
922 static void sorti(int *a, int *b)
923 {
924         if ( *a > *b ) {
925                 int tmp = *a;
926                 *a = *b;
927                 *b = tmp;
928         }
929 }
930
931
932 static gboolean motion_sig(GtkWidget *da, GdkEventMotion *event,
933                            GtkNarrativeView *e)
934 {
935         gdouble x, y;
936         struct edit_pos old_sel_end;
937         int minp, maxp;
938
939         x = event->x;
940         y = event->y + e->scroll_pos;
941
942         if ( e->drag_status == DRAG_STATUS_COULD_DRAG ) {
943                 /* We just got a motion signal, and the status was "could drag",
944                  * therefore the drag has started. */
945                 e->drag_status = DRAG_STATUS_DRAGGING;
946         }
947
948         old_sel_end = e->sel_end;
949         find_cursor(e->n, x, y, &e->sel_end);
950
951         minp = e->sel_start.para;
952         maxp = e->sel_end.para;
953         sorti(&minp, &maxp);
954         if ( !positions_equal(e->sel_start, old_sel_end) ) {
955                 if ( old_sel_end.para > maxp ) maxp = old_sel_end.para;
956                 if ( old_sel_end.para < minp ) minp = old_sel_end.para;
957         }
958
959         rewrap_range(e, minp, maxp);
960         find_cursor(e->n, x, y, &e->cpos);
961         redraw(e);
962
963         gdk_event_request_motions(event);
964         return FALSE;
965 }
966
967
968 static gboolean key_press_sig(GtkWidget *da, GdkEventKey *event,
969                               GtkNarrativeView *e)
970 {
971         gboolean r;
972         int claim = 0;
973
974         /* Throw the event to the IM context and let it sort things out */
975         r = gtk_im_context_filter_keypress(GTK_IM_CONTEXT(e->im_context),
976                                            event);
977         if ( r ) return FALSE;  /* IM ate it */
978
979         switch ( event->keyval ) {
980
981                 case GDK_KEY_Left :
982                 cursor_moveh(e->n, &e->cpos, -1);
983                 check_cursor_visible(e);
984                 redraw(e);
985                 claim = 1;
986                 break;
987
988                 case GDK_KEY_Right :
989                 cursor_moveh(e->n, &e->cpos, +1);
990                 check_cursor_visible(e);
991                 redraw(e);
992                 claim = 1;
993                 break;
994
995                 case GDK_KEY_Up :
996                 cursor_moveh(e->n, &e->cpos, -1);
997                 check_cursor_visible(e);
998                 redraw(e);
999                 claim = 1;
1000                 break;
1001
1002                 case GDK_KEY_Down :
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_Return :
1010                 im_commit_sig(NULL, "\n", e);
1011                 claim = 1;
1012                 break;
1013
1014                 case GDK_KEY_BackSpace :
1015                 do_backspace(e, -1);
1016                 claim = 1;
1017                 break;
1018
1019                 case GDK_KEY_Delete :
1020                 do_backspace(e, +1);
1021                 claim = 1;
1022                 break;
1023
1024                 case GDK_KEY_C :
1025                 case GDK_KEY_c :
1026                 if ( event->state == GDK_CONTROL_MASK ) {
1027                         copy_selection(e);
1028                 }
1029                 break;
1030
1031                 case GDK_KEY_V :
1032                 case GDK_KEY_v :
1033                 if ( event->state == GDK_CONTROL_MASK ) {
1034                         sc_editor_paste(e);
1035                 }
1036                 break;
1037
1038
1039         }
1040
1041         if ( claim ) return TRUE;
1042         return FALSE;
1043 }
1044
1045
1046 static gboolean dnd_motion(GtkWidget *widget, GdkDragContext *drag_context,
1047                            gint x, gint y, guint time, GtkNarrativeView *e)
1048 {
1049         return TRUE;
1050 }
1051
1052
1053 static gboolean dnd_drop(GtkWidget *widget, GdkDragContext *drag_context,
1054                          gint x, gint y, guint time, GtkNarrativeView *e)
1055 {
1056         GdkAtom target;
1057
1058         target = gtk_drag_dest_find_target(widget, drag_context, NULL);
1059
1060         if ( target == GDK_NONE ) {
1061                 gtk_drag_finish(drag_context, FALSE, FALSE, time);
1062         } else {
1063                 gtk_drag_get_data(widget, drag_context, target, time);
1064         }
1065
1066         return TRUE;
1067 }
1068
1069
1070 static void dnd_receive(GtkWidget *widget, GdkDragContext *drag_context,
1071                         gint x, gint y, GtkSelectionData *seldata,
1072                         guint info, guint time, GtkNarrativeView *e)
1073 {
1074 }
1075
1076
1077 static void dnd_leave(GtkWidget *widget, GdkDragContext *drag_context,
1078                       guint time, GtkNarrativeView *nview)
1079 {
1080         nview->drag_status = DRAG_STATUS_NONE;
1081 }
1082
1083
1084 static gint realise_sig(GtkWidget *da, GtkNarrativeView *e)
1085 {
1086         GdkWindow *win;
1087
1088         /* Keyboard and input method stuff */
1089         e->im_context = gtk_im_multicontext_new();
1090         win = gtk_widget_get_window(GTK_WIDGET(e));
1091         gtk_im_context_set_client_window(GTK_IM_CONTEXT(e->im_context), win);
1092         gdk_window_set_accept_focus(win, TRUE);
1093         g_signal_connect(G_OBJECT(e->im_context), "commit", G_CALLBACK(im_commit_sig), e);
1094         g_signal_connect(G_OBJECT(e), "key-press-event", G_CALLBACK(key_press_sig), e);
1095
1096         return FALSE;
1097 }
1098
1099
1100 GtkWidget *gtk_narrative_view_new(Narrative *n)
1101 {
1102         GtkNarrativeView *nview;
1103         GtkTargetEntry targets[1];
1104
1105         nview = g_object_new(GTK_TYPE_NARRATIVE_VIEW, NULL);
1106
1107         nview->w = 100;
1108         nview->h = 100;
1109         nview->scroll_pos = 0;
1110         nview->n = n;
1111         nview->rewrap_needed = 0;
1112         nview->para_highlight = 0;
1113
1114         gtk_widget_set_size_request(GTK_WIDGET(nview),
1115                                     nview->w, nview->h);
1116
1117         g_signal_connect(G_OBJECT(nview), "destroy",
1118                          G_CALLBACK(destroy_sig), nview);
1119         g_signal_connect(G_OBJECT(nview), "realize",
1120                          G_CALLBACK(realise_sig), nview);
1121         g_signal_connect(G_OBJECT(nview), "button-press-event",
1122                          G_CALLBACK(button_press_sig), nview);
1123         g_signal_connect(G_OBJECT(nview), "motion-notify-event",
1124                          G_CALLBACK(motion_sig), nview);
1125         g_signal_connect(G_OBJECT(nview), "configure-event",
1126                          G_CALLBACK(resize_sig), nview);
1127
1128         /* Drag and drop */
1129         targets[0].target = "text/uri-list";
1130         targets[0].flags = 0;
1131         targets[0].info = 1;
1132         gtk_drag_dest_set(GTK_WIDGET(nview), 0, targets, 1,
1133                           GDK_ACTION_PRIVATE);
1134         g_signal_connect(nview, "drag-data-received",
1135                          G_CALLBACK(dnd_receive), nview);
1136         g_signal_connect(nview, "drag-motion",
1137                          G_CALLBACK(dnd_motion), nview);
1138         g_signal_connect(nview, "drag-drop",
1139                          G_CALLBACK(dnd_drop), nview);
1140         g_signal_connect(nview, "drag-leave",
1141                          G_CALLBACK(dnd_leave), nview);
1142
1143         gtk_widget_set_can_focus(GTK_WIDGET(nview), TRUE);
1144         gtk_widget_add_events(GTK_WIDGET(nview),
1145                               GDK_POINTER_MOTION_HINT_MASK
1146                                | GDK_BUTTON1_MOTION_MASK
1147                                | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
1148                                | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK
1149                                | GDK_SCROLL_MASK);
1150
1151         g_signal_connect(G_OBJECT(nview), "draw",
1152                          G_CALLBACK(draw_sig), nview);
1153
1154         gtk_widget_grab_focus(GTK_WIDGET(nview));
1155
1156         gtk_widget_show(GTK_WIDGET(nview));
1157
1158         return GTK_WIDGET(nview);
1159 }
1160
1161
1162 void gtk_narrative_view_set_para_highlight(GtkNarrativeView *e, int para_highlight)
1163 {
1164         e->para_highlight = para_highlight;
1165         redraw(e);
1166 }
1167
1168
1169 int gtk_narrative_view_get_cursor_para(GtkNarrativeView *e)
1170 {
1171         return e->cpos.para;
1172 }
1173
1174
1175 void gtk_narrative_view_set_cursor_para(GtkNarrativeView *e, signed int pos)
1176 {
1177         double h;
1178         int i;
1179
1180         if ( pos < 0 ) pos = e->n->n_items-1;
1181         e->cpos.para = pos;
1182         e->cpos.pos = 0;
1183         e->cpos.trail = 0;
1184
1185         h = 0;
1186         for ( i=0; i<e->cpos.para; i++ ) {
1187                 h += narrative_item_get_height(e->n, i);
1188         }
1189         h += narrative_item_get_height(e->n, e->cpos.para)/2;
1190         e->scroll_pos = h - (e->visible_height/2);
1191         set_vertical_params(e);
1192
1193         redraw(e);
1194 }
1195
1196
1197 void gtk_narrative_view_add_slide_at_cursor(GtkNarrativeView *e)
1198 {
1199         Slide *s;
1200
1201         s = slide_new();
1202         if ( s == NULL ) return;
1203
1204         if ( narrative_item_is_text(e->n, e->cpos.para) ) {
1205                 size_t off = narrative_pos_trail_to_offset(e->n, e->cpos.para,
1206                                                            e->cpos.pos, e->cpos.trail);
1207                 if ( (off > 0) && (off < end_offset_of_para(e->n, e->cpos.para)) ) {
1208                         narrative_split_item(e->n, e->cpos.para, off);
1209                         narrative_insert_slide(e->n, s, e->cpos.para+1);
1210                 } else if ( off == 0 ) {
1211                         narrative_insert_slide(e->n, s, e->cpos.para);
1212                 } else {
1213                         narrative_insert_slide(e->n, s, e->cpos.para+1);
1214                 }
1215         } else {
1216                 narrative_insert_slide(e->n, s, e->cpos.para+1);
1217         }
1218
1219         rewrap_range(e, e->cpos.para, e->cpos.para+2);
1220         e->cpos.para++;
1221         e->cpos.pos = 0;
1222         e->cpos.trail = 0;
1223         update_size(e);
1224         check_cursor_visible(e);
1225         emit_change_sig(e);
1226         redraw(e);
1227 }
1228
1229
1230 extern void gtk_narrative_view_redraw(GtkNarrativeView *e)
1231 {
1232         e->rewrap_needed = 1;
1233         emit_change_sig(e);
1234         redraw(e);
1235 }