303e31a9725453c6cfed537050f606d377b68981
[colloquium.git] / libstorycode / narrative.c
1 /*
2  * narrative.c
3  *
4  * Copyright © 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 <assert.h>
31 #include <stdio.h>
32 #include <gio/gio.h>
33
34 #include <libintl.h>
35 #define _(x) gettext(x)
36
37 #ifdef HAVE_PANGO
38 #include <pango/pango.h>
39 #endif
40
41 #ifdef HAVE_CAIRO
42 #include <cairo.h>
43 #endif
44
45 #include "stylesheet.h"
46 #include "slide.h"
47 #include "narrative.h"
48 #include "narrative_priv.h"
49 #include "imagestore.h"
50 #include "storycode.h"
51
52 Narrative *narrative_new()
53 {
54         Narrative *n;
55         n = malloc(sizeof(*n));
56         if ( n == NULL ) return NULL;
57         n->n_items = 0;
58         n->items = NULL;
59         n->stylesheet = stylesheet_new();
60         n->imagestore = imagestore_new("."); /* FIXME: From app config */
61         n->saved = 1;
62 #ifdef HAVE_PANGO
63         n->language = pango_language_to_string(pango_language_get_default());
64 #else
65         n->language = NULL;
66 #endif
67         return n;
68 }
69
70
71 static void narrative_item_destroy(struct narrative_item *item)
72 {
73         int i;
74         for ( i=0; i<item->n_runs; i++ ) {
75                 free(item->runs[i].text);
76         }
77         free(item->runs);
78 #ifdef HAVE_PANGO
79         if ( item->layout != NULL ) {
80                 g_object_unref(item->layout);
81         }
82 #endif
83 #ifdef HAVE_CAIRO
84         if ( item->slide_thumbnail != NULL ) {
85                 cairo_surface_destroy(item->slide_thumbnail);
86         }
87 #endif
88 }
89
90
91 /* Free the narrative and all contents, but not the stylesheet */
92 void narrative_free(Narrative *n)
93 {
94         int i;
95         for ( i=0; i<n->n_items; i++ ) {
96                 narrative_item_destroy(&n->items[i]);
97         }
98         free(n->items);
99         free(n);
100 }
101
102
103 void narrative_add_empty_item(Narrative *n)
104 {
105         struct text_run *runs;
106
107         runs = malloc(sizeof(struct text_run));
108         if ( runs == NULL ) return;
109
110         runs[0].text = strdup("");
111         runs[0].type = TEXT_RUN_NORMAL;
112         if ( runs[0].text == NULL ) {
113                 free(runs);
114                 return;
115         }
116
117         narrative_add_text(n, runs, 1);
118 }
119
120
121 Narrative *narrative_load(GFile *file)
122 {
123         GBytes *bytes;
124         const char *text;
125         size_t len;
126         Narrative *n;
127
128         bytes = g_file_load_bytes(file, NULL, NULL, NULL);
129         if ( bytes == NULL ) return NULL;
130
131         text = g_bytes_get_data(bytes, &len);
132         n = storycode_parse_presentation(text);
133         g_bytes_unref(bytes);
134         if ( n == NULL ) return NULL;
135
136         if ( n->n_items == 0 ) {
137                 /* Presentation is empty.  Add a dummy to start things off */
138                 narrative_add_empty_item(n);
139         }
140
141         imagestore_set_parent(n->imagestore, g_file_get_parent(file));
142         return n;
143 }
144
145
146 int narrative_save(Narrative *n, GFile *file)
147 {
148         GFileOutputStream *fh;
149         int r;
150         GError *error = NULL;
151
152         if ( file == NULL ) {
153                 fprintf(stderr, "Saving to NULL!\n");
154                 return 1;
155         }
156
157         fh = g_file_replace(file, NULL, FALSE, G_FILE_CREATE_NONE, NULL, &error);
158         if ( fh == NULL ) {
159                 fprintf(stderr, _("Open failed: %s\n"), error->message);
160                 return 1;
161         }
162         r = storycode_write_presentation(n, G_OUTPUT_STREAM(fh));
163         if ( r ) {
164                 fprintf(stderr, _("Couldn't save presentation\n"));
165         }
166         g_object_unref(fh);
167
168         imagestore_set_parent(n->imagestore, g_file_get_parent(file));
169
170         n->saved = 1;
171         return 0;
172 }
173
174
175 void narrative_set_unsaved(Narrative *n)
176 {
177         n->saved = 0;
178 }
179
180
181 int narrative_get_unsaved(Narrative *n)
182 {
183         return !n->saved;
184 }
185
186
187 int narrative_item_is_text(Narrative *n, int item)
188 {
189         if ( n->items[item].type == NARRATIVE_ITEM_SLIDE ) return 0;
190         if ( n->items[item].type == NARRATIVE_ITEM_EOP ) return 0;
191         return 1;
192 }
193
194
195 void narrative_add_stylesheet(Narrative *n, Stylesheet *ss)
196 {
197         assert(n->stylesheet == NULL);
198         n->stylesheet = ss;
199 }
200
201
202 Stylesheet *narrative_get_stylesheet(Narrative *n)
203 {
204         if ( n == NULL ) return NULL;
205         return n->stylesheet;
206 }
207
208
209 const char *narrative_get_language(Narrative *n)
210 {
211         if ( n == NULL ) return NULL;
212         return n->language;
213 }
214
215
216 ImageStore *narrative_get_imagestore(Narrative *n)
217 {
218         if ( n == NULL ) return NULL;
219         return n->imagestore;
220 }
221
222
223 static void init_item(struct narrative_item *item)
224 {
225 #ifdef HAVE_PANGO
226         item->layout = NULL;
227 #endif
228         item->runs = NULL;
229         item->n_runs = 0;
230         item->slide = NULL;
231         item->slide_thumbnail = NULL;
232 }
233
234
235 static struct narrative_item *add_item(Narrative *n)
236 {
237         struct narrative_item *new_items;
238         struct narrative_item *item;
239         new_items = realloc(n->items, (n->n_items+1)*sizeof(struct narrative_item));
240         if ( new_items == NULL ) return NULL;
241         n->items = new_items;
242         item = &n->items[n->n_items++];
243         init_item(item);
244         return item;
245 }
246
247
248 /* New item will have index 'pos' */
249 static struct narrative_item *insert_item(Narrative *n, int pos)
250 {
251         add_item(n);
252         memmove(&n->items[pos+1], &n->items[pos],
253                 (n->n_items-pos-1)*sizeof(struct narrative_item));
254         init_item(&n->items[pos]);
255         return &n->items[pos];
256 }
257
258
259 /* The new text item takes ownership of the array of runs */
260 extern void add_text_item(Narrative *n, struct text_run *runs, int n_runs,
261                           enum narrative_item_type type)
262 {
263         struct narrative_item *item;
264
265         item = add_item(n);
266         if ( item == NULL ) return;
267
268         item->type = type;
269         item->align = ALIGN_INHERIT;
270         item->layout = NULL;
271
272         item->runs = runs;
273         item->n_runs = n_runs;
274 }
275
276
277 void narrative_add_text(Narrative *n, struct text_run *runs, int n_runs)
278 {
279         add_text_item(n, runs, n_runs, NARRATIVE_ITEM_TEXT);
280 }
281
282
283 void narrative_add_prestitle(Narrative *n, struct text_run *runs, int n_runs)
284 {
285         add_text_item(n, runs, n_runs, NARRATIVE_ITEM_PRESTITLE);
286 }
287
288
289 void narrative_add_bp(Narrative *n, struct text_run *runs, int n_runs)
290 {
291         add_text_item(n, runs, n_runs, NARRATIVE_ITEM_BP);
292 }
293
294
295 void narrative_add_slide(Narrative *n, Slide *slide)
296 {
297         struct narrative_item *item;
298
299         item = add_item(n);
300         if ( item == NULL ) return;
301
302         item->type = NARRATIVE_ITEM_SLIDE;
303         item->slide = slide;
304         item->slide_thumbnail = NULL;
305 }
306
307
308 void narrative_add_eop(Narrative *n)
309 {
310         struct narrative_item *item;
311
312         item = add_item(n);
313         if ( item == NULL ) return;
314
315         item->type = NARRATIVE_ITEM_EOP;
316 }
317
318
319 void narrative_insert_slide(Narrative *n, Slide *slide, int pos)
320 {
321         struct narrative_item *item = insert_item(n, pos);
322         item->type = NARRATIVE_ITEM_SLIDE;
323         item->slide = slide;
324         item->slide_thumbnail = NULL;
325 }
326
327
328 static void delete_item(Narrative *n, int del)
329 {
330         int i;
331         narrative_item_destroy(&n->items[del]);
332         for ( i=del; i<n->n_items-1; i++ ) {
333                 n->items[i] = n->items[i+1];
334         }
335         n->n_items--;
336 }
337
338
339 /* o2 = -1 means 'to the end' */
340 static void delete_text(struct narrative_item *item, size_t o1, ssize_t o2)
341 {
342         int r1, r2;
343         size_t roffs1, roffs2;
344
345         r1 = narrative_which_run(item, o1, &roffs1);
346
347         /* This means 'delete to end' */
348         if ( o2 == -1 ) {
349                 int i;
350                 o2 = 0;
351                 for ( i=0; i<item->n_runs; i++ ) {
352                         o2 += strlen(item->runs[i].text);
353                 }
354         }
355
356         r2 = narrative_which_run(item, o2, &roffs2);
357
358         if ( r1 == r2 ) {
359
360                 /* Easy case */
361                 memmove(&item->runs[r1].text[roffs1],
362                         &item->runs[r2].text[roffs2],
363                         strlen(item->runs[r1].text)-roffs2+1);
364
365         } else {
366
367                 int n_middle;
368
369                 /* Truncate the first run */
370                 item->runs[r1].text[roffs1] = '\0';
371
372                 /* Delete any middle runs */
373                 n_middle = r2 - r1 - 1;
374                 if ( n_middle > 0 ) {
375                         memmove(&item->runs[r1+1], &item->runs[r2],
376                                 (item->n_runs-r2)*sizeof(struct text_run));
377                         item->n_runs -= n_middle;
378                         r2 -= n_middle;
379                 }
380
381                 /* Last run */
382                 memmove(item->runs[r2].text, &item->runs[r2].text[roffs2],
383                         strlen(item->runs[r2].text) - roffs2+1);
384         }
385 }
386
387
388 /* Delete from item i1 offset o1 to item i2 offset o2, inclusive */
389 void narrative_delete_block(Narrative *n, int i1, size_t o1, int i2, size_t o2)
390 {
391         int i;
392         int n_del = 0;
393         int merge = 1;
394         int middle;    /* This is where the "middle deletion" will begin */
395
396         /* Starting item */
397         if ( !narrative_item_is_text(n, i1) ) {
398                 delete_item(n, i1);
399                 if ( i1 == i2 ) return;  /* only one slide to delete */
400                 middle = i1; /* ... which is now the item just after the slide */
401                 i2--;
402                 merge = 0;
403         } else {
404                 if ( i1 == i2 ) {
405                         delete_text(&n->items[i1], o1, o2);
406                         return;  /* easy case */
407                 } else {
408                         /* Truncate i1 at o1 */
409                         delete_text(&n->items[i1], o1, -1);
410                         middle = i1+1;
411                 }
412         }
413
414         /* Middle items */
415         for ( i=middle; i<i2; i++ ) {
416                 /* Deleting the item moves all the subsequent items up, so the
417                  * index to be deleted doesn't change. */
418                 delete_item(n, middle);
419                 n_del++;
420         }
421         i2 -= n_del;
422
423         /* Last item */
424         if ( !narrative_item_is_text(n, i2) ) {
425                 delete_item(n, i2);
426                 return;
427         }
428
429         /* We end with a text item */
430         delete_text(&n->items[i2], 0, o2);
431
432         /* If the start and end points are in different paragraphs, and both
433          * of them are text (any kind), merge them.  Note that at this point,
434          * we know that i1 and i2 are different because otherwise we
435          * would've returned earlier ("easy case"). */
436         if ( merge ) {
437
438                 struct text_run *i1newruns;
439                 struct narrative_item *item1;
440                 struct narrative_item *item2;
441
442                 assert(i1 != i2);
443                 item1 = &n->items[i1];
444                 item2 = &n->items[i2];
445
446                 i1newruns = realloc(item1->runs,
447                                     (item1->n_runs+item2->n_runs)*sizeof(struct text_run));
448                 if ( i1newruns == NULL ) return;
449                 item1->runs = i1newruns;
450
451                 for ( i=0; i<item2->n_runs; i++ ) {
452                         item1->runs[i+item1->n_runs].text = strdup(item2->runs[i].text);
453                         item1->runs[i+item1->n_runs].type = item2->runs[i].type;
454                 }
455                 item1->n_runs += item2->n_runs;
456
457                 delete_item(n, i2);
458         }
459 }
460
461
462 int narrative_which_run(struct narrative_item *item, size_t item_offs, size_t *run_offs)
463 {
464         int run;
465         size_t pos = 0;
466
467         for ( run=0; run<item->n_runs; run++ ) {
468                 size_t npos = pos + strlen(item->runs[run].text);
469                 if ( npos >= item_offs ) break;
470                 pos = npos;
471         }
472         if ( run_offs != NULL ) {
473                 *run_offs = item_offs - pos;
474         }
475         return run;
476 }
477
478 void narrative_split_item(Narrative *n, int i1, size_t o1)
479 {
480         struct narrative_item *item1;
481         struct narrative_item *item2;
482
483         item2 = insert_item(n, i1+1);
484         item1 = &n->items[i1];  /* NB n->items was realloced by insert_item */
485         item2->type = NARRATIVE_ITEM_TEXT;
486
487         if ( narrative_item_is_text(n, i1) ) {
488
489                 size_t run_offs;
490                 int run = narrative_which_run(item1, o1, &run_offs);
491                 int j;
492
493                 item2->n_runs = item1->n_runs - run;
494                 item2->runs = malloc(item2->n_runs*sizeof(struct text_run));
495                 for ( j=run; j<item1->n_runs; j++ ) {
496                         item2->runs[j-run] = item1->runs[j];
497                 }
498
499                 /* Now break the run */
500                 item2->runs[0].text = strdup(item1->runs[run].text+run_offs);
501                 item1->runs[run].text[run_offs] = '\0';
502                 item1->n_runs = run + 1;
503
504         } else {
505
506                 /* Splitting a non-text run simply means creating a new
507                  * plain text item after it */
508                 item2->runs = malloc(sizeof(struct text_run));
509                 item2->n_runs = 1;
510                 item2->runs[0].text = strdup("");
511                 item2->runs[0].type = TEXT_RUN_NORMAL;;
512
513         }
514 }
515
516
517 int narrative_get_num_items(Narrative *n)
518 {
519         return n->n_items;
520 }
521
522
523 int narrative_get_num_items_to_eop(Narrative *n)
524 {
525         int i;
526         for ( i=0; i<n->n_items; i++ ) {
527                 if ( n->items[i].type == NARRATIVE_ITEM_EOP ) return i;
528         }
529         return n->n_items;
530 }
531
532
533 int narrative_get_num_slides(Narrative *n)
534 {
535         int i;
536         int ns = 0;
537         for ( i=0; i<n->n_items; i++ ) {
538                 if ( n->items[i].type == NARRATIVE_ITEM_SLIDE ) ns++;
539         }
540         return ns;
541 }
542
543
544 Slide *narrative_get_slide(Narrative *n, int para)
545 {
546         if ( para >= n->n_items ) return NULL;
547         if ( n->items[para].type != NARRATIVE_ITEM_SLIDE ) return NULL;
548         return n->items[para].slide;
549 }
550
551
552 int narrative_get_slide_number_for_para(Narrative *n, int para)
553 {
554         int i;
555         int ns = 0;
556         for ( i=0; i<para; i++ ) {
557                 if ( n->items[i].type == NARRATIVE_ITEM_SLIDE ) ns++;
558         }
559         return ns;
560 }
561
562
563 int narrative_get_slide_number_for_slide(Narrative *n, Slide *s)
564 {
565         int i;
566         int ns = 0;
567         for ( i=0; i<n->n_items; i++ ) {
568                 if ( n->items[i].type == NARRATIVE_ITEM_SLIDE ) {
569                         if ( n->items[i].slide == s ) return ns;
570                         ns++;
571                 }
572         }
573         return n->n_items;
574 }
575
576
577 Slide *narrative_get_slide_by_number(Narrative *n, int pos)
578 {
579         int i;
580         int ns = 0;
581         for ( i=0; i<n->n_items; i++ ) {
582                 if ( n->items[i].type == NARRATIVE_ITEM_SLIDE ) {
583                         if ( ns == pos ) return n->items[i].slide;
584                         ns++;
585                 }
586         }
587         return NULL;
588 }
589
590
591 static void debug_runs(struct narrative_item *item)
592 {
593         int j;
594         for ( j=0; j<item->n_runs; j++ ) {
595                 printf("Run %i: '%s'\n", j, item->runs[j].text);
596         }
597 }
598
599 void narrative_debug(Narrative *n)
600 {
601         int i;
602
603         for ( i=0; i<n->n_items; i++ ) {
604
605                 struct narrative_item *item = &n->items[i];
606                 printf("Item %i ", i);
607                 switch ( item->type ) {
608
609                         case NARRATIVE_ITEM_TEXT :
610                         printf("(text):\n");
611                         debug_runs(item);
612                         break;
613
614                         case NARRATIVE_ITEM_BP :
615                         printf("(bp):\n");
616                         debug_runs(item);
617                         break;
618
619                         case NARRATIVE_ITEM_PRESTITLE :
620                         printf("(prestitle):\n");
621                         debug_runs(item);
622                         break;
623
624                         case NARRATIVE_ITEM_EOP :
625                         printf("(EOP marker)\n");
626                         break;
627
628                         case NARRATIVE_ITEM_SLIDE :
629                         printf("Slide:\n");
630                         describe_slide(item->slide);
631                         break;
632
633                 }
634
635         }
636 }