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