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