aboutsummaryrefslogtreecommitdiff
path: root/src-old
diff options
context:
space:
mode:
Diffstat (limited to 'src-old')
-rw-r--r--src-old/colloquium.c476
-rw-r--r--src-old/colloquium.h45
-rw-r--r--src-old/debugger.c229
-rw-r--r--src-old/debugger.h32
-rw-r--r--src-old/frame.c1820
-rw-r--r--src-old/frame.h194
-rw-r--r--src-old/imagestore.c369
-rw-r--r--src-old/imagestore.h43
-rw-r--r--src-old/narrative_window.c927
-rw-r--r--src-old/narrative_window.h42
-rw-r--r--src-old/pr_clock.c438
-rw-r--r--src-old/pr_clock.h37
-rw-r--r--src-old/presentation.c362
-rw-r--r--src-old/presentation.h83
-rw-r--r--src-old/print.c314
-rw-r--r--src-old/print.h32
-rw-r--r--src-old/render.c332
-rw-r--r--src-old/render.h62
-rw-r--r--src-old/sc_editor.c2197
-rw-r--r--src-old/sc_editor.h199
-rw-r--r--src-old/sc_interp.c1148
-rw-r--r--src-old/sc_interp.h80
-rw-r--r--src-old/sc_parse.c856
-rw-r--r--src-old/sc_parse.h85
-rw-r--r--src-old/slide_window.c282
-rw-r--r--src-old/slide_window.h37
-rw-r--r--src-old/slideshow.c240
-rw-r--r--src-old/slideshow.h83
-rw-r--r--src-old/stylesheet.c288
-rw-r--r--src-old/stylesheet.h54
-rw-r--r--src-old/stylesheet_editor.c794
-rw-r--r--src-old/stylesheet_editor.h128
-rw-r--r--src-old/testcard.c288
-rw-r--r--src-old/testcard.h32
-rw-r--r--src-old/utils.c137
-rw-r--r--src-old/utils.h47
36 files changed, 12812 insertions, 0 deletions
diff --git a/src-old/colloquium.c b/src-old/colloquium.c
new file mode 100644
index 0000000..02ffa0a
--- /dev/null
+++ b/src-old/colloquium.c
@@ -0,0 +1,476 @@
+/*
+ * colloquium.c
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <gtk/gtk.h>
+#include <getopt.h>
+#include <string.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include "colloquium.h"
+#include "presentation.h"
+#include "narrative_window.h"
+#include "utils.h"
+
+
+struct _colloquium
+{
+ GtkApplication parent_instance;
+ GtkBuilder *builder;
+ char *mydir;
+ int first_run;
+ char *imagestore;
+ int hidepointer;
+};
+
+
+typedef GtkApplicationClass ColloquiumClass;
+
+
+G_DEFINE_TYPE(Colloquium, colloquium, GTK_TYPE_APPLICATION)
+
+
+static void colloquium_activate(GApplication *papp)
+{
+ Colloquium *app = COLLOQUIUM(papp);
+ if ( !app->first_run ) {
+ struct presentation *p;
+ p = new_presentation(app->imagestore);
+ narrative_window_new(p, papp);
+ }
+}
+
+
+static void new_sig(GSimpleAction *action, GVariant *parameter, gpointer vp)
+{
+ GApplication *app = vp;
+ g_application_activate(app);
+}
+
+
+static void open_intro_doc(Colloquium *app)
+{
+ GFile *file = g_file_new_for_uri("resource:///uk/me/bitwiz/Colloquium/demo.sc");
+ g_application_open(G_APPLICATION(app), &file, 1, "");
+ g_object_unref(file);
+}
+
+
+static void intro_sig(GSimpleAction *action, GVariant *parameter, gpointer vp)
+{
+ GApplication *app = vp;
+ open_intro_doc(COLLOQUIUM(app));
+}
+
+
+void open_about_dialog(GtkWidget *parent)
+{
+ GtkWidget *window;
+
+ const gchar *authors[] = {
+ "Thomas White <taw@bitwiz.org.uk>",
+ NULL
+ };
+
+ window = gtk_about_dialog_new();
+ gtk_window_set_transient_for(GTK_WINDOW(window), GTK_WINDOW(parent));
+
+ gtk_about_dialog_set_program_name(GTK_ABOUT_DIALOG(window),
+ "Colloquium");
+ gtk_about_dialog_set_logo_icon_name(GTK_ABOUT_DIALOG(window),
+ "colloquium");
+ gtk_about_dialog_set_version(GTK_ABOUT_DIALOG(window),
+ PACKAGE_VERSION);
+ gtk_about_dialog_set_copyright(GTK_ABOUT_DIALOG(window),
+ "© 2017-2018 Thomas White <taw@bitwiz.me.uk>");
+ gtk_about_dialog_set_comments(GTK_ABOUT_DIALOG(window),
+ /* Description of the program */
+ _("Narrative-based presentation system"));
+ gtk_about_dialog_set_license_type(GTK_ABOUT_DIALOG(window), GTK_LICENSE_GPL_3_0);
+ gtk_about_dialog_set_website(GTK_ABOUT_DIALOG(window),
+ "https://www.bitwiz.me.uk/");
+ gtk_about_dialog_set_website_label(GTK_ABOUT_DIALOG(window),
+ "https://www.bitwiz.me.uk/");
+ gtk_about_dialog_set_authors(GTK_ABOUT_DIALOG(window), authors);
+ gtk_about_dialog_set_translator_credits(GTK_ABOUT_DIALOG(window),
+ _("translator-credits"));
+
+ g_signal_connect(window, "response", G_CALLBACK(gtk_widget_destroy),
+ NULL);
+
+ gtk_widget_show_all(window);
+}
+
+
+static void quit_sig(GSimpleAction *action, GVariant *parameter, gpointer vp)
+{
+ GApplication *app = vp;
+ g_application_quit(app);
+}
+
+
+static GFile **gslist_to_array(GSList *item, int *n)
+{
+ int i = 0;
+ int len = g_slist_length(item);
+ GFile **files = malloc(len * sizeof(GFile *));
+
+ if ( files == NULL ) return NULL;
+
+ while ( item != NULL ) {
+ if ( i == len ) {
+ fprintf(stderr, "WTF? Too many files\n");
+ break;
+ }
+ files[i++] = item->data;
+ item = item->next;
+ }
+
+ *n = len;
+ return files;
+}
+
+
+static gint open_response_sig(GtkWidget *d, gint response, GApplication *papp)
+{
+ if ( response == GTK_RESPONSE_ACCEPT ) {
+
+ GSList *files;
+ int n_files = 0;
+ GFile **files_array;
+ int i;
+
+ files = gtk_file_chooser_get_files(GTK_FILE_CHOOSER(d));
+ files_array = gslist_to_array(files, &n_files);
+ if ( files_array == NULL ) {
+ fprintf(stderr, "Failed to convert file list\n");
+ return 0;
+ }
+ g_slist_free(files);
+ g_application_open(papp, files_array, n_files, "");
+
+ for ( i=0; i<n_files; i++ ) {
+ g_object_unref(files_array[i]);
+ }
+
+ }
+
+ gtk_widget_destroy(d);
+
+ return 0;
+}
+
+
+static void open_sig(GSimpleAction *action, GVariant *parameter, gpointer vp)
+{
+ GtkWidget *d;
+ GApplication *app = vp;
+
+ d = gtk_file_chooser_dialog_new(_("Open Presentation"),
+ gtk_application_get_active_window(GTK_APPLICATION(app)),
+ GTK_FILE_CHOOSER_ACTION_OPEN,
+ _("_Cancel"), GTK_RESPONSE_CANCEL,
+ _("_Open"), GTK_RESPONSE_ACCEPT,
+ NULL);
+
+ gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(d), TRUE);
+
+ g_signal_connect(G_OBJECT(d), "response",
+ G_CALLBACK(open_response_sig), app);
+
+ gtk_widget_show_all(d);
+}
+
+
+GActionEntry app_entries[] = {
+
+ { "new", new_sig, NULL, NULL, NULL },
+ { "open", open_sig, NULL, NULL, NULL },
+ { "intro", intro_sig, NULL, NULL, NULL },
+ { "quit", quit_sig, NULL, NULL, NULL },
+};
+
+
+static void colloquium_open(GApplication *papp, GFile **files, gint n_files,
+ const gchar *hint)
+{
+ int i;
+ Colloquium *app = COLLOQUIUM(papp);
+
+ for ( i = 0; i<n_files; i++ ) {
+ struct presentation *p;
+ p = new_presentation(app->imagestore);
+ if ( load_presentation(p, files[i]) == 0 ) {
+ narrative_window_new(p, papp);
+ } else {
+ char *uri = g_file_get_uri(files[i]);
+ fprintf(stderr, _("Failed to load presentation '%s'\n"),
+ uri);
+ g_free(uri);
+ }
+ }
+}
+
+
+static void colloquium_finalize(GObject *object)
+{
+ G_OBJECT_CLASS(colloquium_parent_class)->finalize(object);
+}
+
+
+static void create_config(const char *filename)
+{
+
+ FILE *fh;
+ fh = fopen(filename, "w");
+ if ( fh == NULL ) {
+ fprintf(stderr, _("Failed to create config\n"));
+ return;
+ }
+
+ fprintf(fh, "imagestore: %s\n",
+ g_get_user_special_dir(G_USER_DIRECTORY_PICTURES));
+ fprintf(fh, "hidepointer: no\n");
+
+ fclose(fh);
+}
+
+
+static int yesno(const char *a)
+{
+ if ( a == NULL ) return 0;
+
+ if ( strcmp(a, "1") == 0 ) return 1;
+ if ( strcasecmp(a, "yes") == 0 ) return 1;
+ if ( strcasecmp(a, "true") == 0 ) return 1;
+
+ if ( strcasecmp(a, "0") == 0 ) return 0;
+ if ( strcasecmp(a, "no") == 0 ) return 0;
+ if ( strcasecmp(a, "false") == 0 ) return 0;
+
+ fprintf(stderr, "Don't understand '%s', assuming false\n", a);
+ return 0;
+}
+
+
+static void read_config(const char *filename, Colloquium *app)
+{
+ FILE *fh;
+ char line[1024];
+
+ fh = fopen(filename, "r");
+ if ( fh == NULL ) {
+ fprintf(stderr, _("Failed to open config %s\n"), filename);
+ return;
+ }
+
+ do {
+
+ if ( fgets(line, 1024, fh) == NULL ) break;
+ chomp(line);
+
+ if ( strncmp(line, "imagestore: ", 11) == 0 ) {
+ app->imagestore = strdup(line+12);
+ }
+
+ if ( strncmp(line, "hidepointer: ", 12) == 0 ) {
+ app->hidepointer = yesno(line+13);
+ }
+ } while ( !feof(fh) );
+
+ fclose(fh);
+}
+
+
+const char *colloquium_get_imagestore(Colloquium *app)
+{
+ return app->imagestore;
+}
+
+
+int colloquium_get_hidepointer(Colloquium *app)
+{
+ return app->hidepointer;
+}
+
+
+static void colloquium_startup(GApplication *papp)
+{
+ Colloquium *app = COLLOQUIUM(papp);
+ const char *configdir;
+ char *tmp;
+
+ G_APPLICATION_CLASS(colloquium_parent_class)->startup(papp);
+
+ g_action_map_add_action_entries(G_ACTION_MAP(app), app_entries,
+ G_N_ELEMENTS(app_entries), app);
+
+ app->builder = gtk_builder_new_from_resource("/uk/me/bitwiz/Colloquium/menus.ui");
+ gtk_builder_add_from_resource(app->builder, "/uk/me/bitwiz/Colloquium/windows.ui", NULL);
+ gtk_application_set_menubar(GTK_APPLICATION(app),
+ G_MENU_MODEL(gtk_builder_get_object(app->builder, "menubar")));
+
+ if ( gtk_application_prefers_app_menu(GTK_APPLICATION(app)) ) {
+ /* Set the application menu only if it will be shown by the
+ * desktop environment. All the entries are already in the
+ * normal menus, so don't let GTK create a fallback menu in the
+ * menu bar. */
+ GMenuModel *mmodel = G_MENU_MODEL(gtk_builder_get_object(app->builder, "app-menu"));
+ gtk_application_set_app_menu(GTK_APPLICATION(app), mmodel);
+ }
+
+ configdir = g_get_user_config_dir();
+ app->mydir = malloc(strlen(configdir)+14);
+ strcpy(app->mydir, configdir);
+ strcat(app->mydir, "/colloquium");
+
+ if ( !g_file_test(app->mydir, G_FILE_TEST_IS_DIR) ) {
+
+ /* Folder not created yet */
+ open_intro_doc(app);
+ app->first_run = 1;
+
+ if ( g_mkdir(app->mydir, S_IRUSR | S_IWUSR | S_IXUSR) ) {
+ fprintf(stderr, _("Failed to create config folder\n"));
+ }
+ }
+
+ /* Read config file */
+ tmp = malloc(strlen(app->mydir)+32);
+ if ( tmp != NULL ) {
+
+ tmp[0] = '\0';
+ strcat(tmp, app->mydir);
+ strcat(tmp, "/config");
+
+ /* Create default config file if it doesn't exist */
+ if ( !g_file_test(tmp, G_FILE_TEST_EXISTS) ) {
+ create_config(tmp);
+ }
+
+ read_config(tmp, app);
+ free(tmp);
+ }
+}
+
+
+static void colloquium_shutdown(GApplication *app)
+{
+ G_APPLICATION_CLASS(colloquium_parent_class)->shutdown(app);
+}
+
+
+static void colloquium_class_init(ColloquiumClass *class)
+{
+ GApplicationClass *app_class = G_APPLICATION_CLASS(class);
+ GObjectClass *object_class = G_OBJECT_CLASS(class);
+
+ app_class->startup = colloquium_startup;
+ app_class->shutdown = colloquium_shutdown;
+ app_class->activate = colloquium_activate;
+ app_class->open = colloquium_open;
+
+ object_class->finalize = colloquium_finalize;
+}
+
+
+static void colloquium_init(Colloquium *app)
+{
+ app->imagestore = NULL;
+ app->hidepointer = 0;
+}
+
+
+static Colloquium *colloquium_new()
+{
+ Colloquium *app;
+
+ g_set_application_name("Colloquium");
+ app = g_object_new(colloquium_get_type(),
+ "application-id", "uk.org.bitwiz.Colloquium",
+ "flags", G_APPLICATION_HANDLES_OPEN,
+ "register-session", TRUE,
+ NULL);
+
+ app->first_run = 0; /* Will be updated at "startup" if appropriate */
+
+ return app;
+}
+
+
+static void show_help(const char *s)
+{
+ printf(_("Syntax: %s [options] [<file.sc>]\n\n"), s);
+ printf(_("Narrative-based presentation system.\n\n"
+ " -h, --help Display this help message.\n"));
+}
+
+
+int main(int argc, char *argv[])
+{
+ int c;
+ int status;
+ Colloquium *app;
+
+ /* Long options */
+ const struct option longopts[] = {
+ {"help", 0, NULL, 'h'},
+ {0, 0, NULL, 0}
+ };
+
+ /* Short options */
+ while ((c = getopt_long(argc, argv, "h", longopts, NULL)) != -1) {
+
+ switch (c)
+ {
+ case 'h' :
+ show_help(argv[0]);
+ return 0;
+
+ case 0 :
+ break;
+
+ default :
+ return 1;
+ }
+
+ }
+
+#if !GLIB_CHECK_VERSION(2,36,0)
+ g_type_init();
+#endif
+
+ bindtextdomain("colloquium", LOCALEDIR);
+ textdomain("colloquium");
+
+ app = colloquium_new();
+ status = g_application_run(G_APPLICATION(app), argc, argv);
+ g_object_unref(app);
+ return status;
+}
diff --git a/src-old/colloquium.h b/src-old/colloquium.h
new file mode 100644
index 0000000..89f600f
--- /dev/null
+++ b/src-old/colloquium.h
@@ -0,0 +1,45 @@
+/*
+ * colloquium.h
+ *
+ * Copyright © 2014-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef COLLOQUIUM_H
+#define COLLOQUIUM_H
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <glib-object.h>
+
+
+typedef struct _colloquium Colloquium;
+
+#define COLLOQUIUM(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), \
+ GTK_TYPE_APPLICATION, Colloquium))
+
+
+extern const char *colloquium_get_imagestore(Colloquium *app);
+extern int colloquium_get_hidepointer(Colloquium *app);
+
+extern void open_about_dialog(GtkWidget *parent);
+
+
+#endif /* COLLOQUIUM_H */
diff --git a/src-old/debugger.c b/src-old/debugger.c
new file mode 100644
index 0000000..f15478a
--- /dev/null
+++ b/src-old/debugger.c
@@ -0,0 +1,229 @@
+/*
+ * print.c
+ *
+ * Copyright © 2017-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <gtk/gtk.h>
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "presentation.h"
+#include "narrative_window.h"
+#include "render.h"
+#include "frame.h"
+#include "utils.h"
+
+
+#define MAX_DEBUG_RUNS (1024)
+
+struct debugwindow
+{
+ GtkWidget *window;
+ GtkWidget *drawingarea;
+ struct frame *fr;
+ guint timeout;
+};
+
+
+static void plot_hr(cairo_t *cr, double *ypos, double width)
+{
+ cairo_move_to(cr, 10.0, *ypos+5.5);
+ cairo_line_to(cr, width-20.0, *ypos+5.5);
+ cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 1.0);
+ cairo_set_line_width(cr, 1.0);
+ cairo_stroke(cr);
+ *ypos += 10.0;
+}
+
+
+static void plot_text(cairo_t *cr, double *ypos, PangoFontDescription *fontdesc,
+ const char *tmp)
+{
+ PangoLayout *layout;
+ PangoRectangle ext;
+
+ cairo_move_to(cr, 10.0, *ypos);
+
+ layout = pango_cairo_create_layout(cr);
+ pango_layout_set_text(layout, tmp, -1);
+ pango_layout_set_font_description(layout, fontdesc);
+
+ pango_layout_get_extents(layout, NULL, &ext);
+ *ypos += ext.height/PANGO_SCALE;
+
+ cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 1.0);
+ pango_cairo_show_layout(cr, layout);
+
+ g_object_unref(layout);
+}
+
+
+static const char *str_type(enum para_type t)
+{
+ switch ( t ) {
+
+ /* Text paragraph */
+ case PARA_TYPE_TEXT : return "text";
+
+ /* Callback paragraph */
+ case PARA_TYPE_CALLBACK : return "callback";
+
+ /* Unknown paragraph type */
+ default : return "unknown";
+ }
+}
+
+static void debug_text_para(Paragraph *para, cairo_t *cr, double *ypos,
+ PangoFontDescription *fontdesc)
+{
+ int i, nrun;
+ char tmp[256];
+
+ nrun = para_debug_num_runs(para);
+ /* How many text runs */
+ snprintf(tmp, 255, " %i runs", nrun);
+ plot_text(cr, ypos, fontdesc, tmp);
+
+ for ( i=0; i<nrun; i++ ) {
+ SCBlock *scblock;
+ if ( para_debug_run_info(para, i, &scblock) ) {
+ /* Failed to get debug info for paragraph */
+ plot_text(cr, ypos, fontdesc, "Error");
+ } else {
+ snprintf(tmp, 255, " Run %i: SCBlock %p", i, scblock);
+ plot_text(cr, ypos, fontdesc, tmp);
+ }
+ }
+
+ snprintf(tmp, 255, "Newline at end: %p",
+ para_debug_get_newline_at_end(para));
+ plot_text(cr, ypos, fontdesc, tmp);
+}
+
+
+static gboolean dbg_draw_sig(GtkWidget *da, cairo_t *cr, struct debugwindow *dbgw)
+{
+ int width, height;
+ char tmp[256];
+ PangoFontDescription *fontdesc;
+ int i;
+ double ypos = 10.0;
+
+ /* Background */
+ width = gtk_widget_get_allocated_width(GTK_WIDGET(da));
+ height = gtk_widget_get_allocated_height(GTK_WIDGET(da));
+ cairo_set_source_rgba(cr, 1.0, 0.8, 0.8, 1.0);
+ cairo_rectangle(cr, 0.0, 0.0, width, height);
+ cairo_fill(cr);
+
+ if ( dbgw->fr == NULL ) return FALSE;
+
+ fontdesc = pango_font_description_new();
+ pango_font_description_set_family_static(fontdesc, "Serif");
+ pango_font_description_set_style(fontdesc, PANGO_STYLE_ITALIC);
+ pango_font_description_set_absolute_size(fontdesc, 15*PANGO_SCALE);
+
+ snprintf(tmp, 255, "Frame %p has %i paragraphs", dbgw->fr, dbgw->fr->n_paras);
+ plot_text(cr, &ypos, fontdesc, tmp);
+
+ for ( i=0; i<dbgw->fr->n_paras; i++ ) {
+
+ enum para_type t = para_type(dbgw->fr->paras[i]);
+ SCBlock *scblock = para_scblock(dbgw->fr->paras[i]);
+
+ plot_hr(cr, &ypos, width);
+ snprintf(tmp, 255, "Paragraph %i: type %s", i, str_type(t));
+ plot_text(cr, &ypos, fontdesc, tmp);
+
+ snprintf(tmp, 255, "SCBlock %p", scblock);
+ plot_text(cr, &ypos, fontdesc, tmp);
+
+ if ( t == PARA_TYPE_TEXT ) {
+ debug_text_para(dbgw->fr->paras[i], cr, &ypos, fontdesc);
+ }
+
+ plot_text(cr, &ypos, fontdesc, "");
+
+ }
+
+ pango_font_description_free(fontdesc);
+
+ return FALSE;
+}
+
+
+static gboolean queue_redraw(void *vp)
+{
+ struct debugwindow *dbgw = vp;
+ gint w, h;
+ w = gtk_widget_get_allocated_width(GTK_WIDGET(dbgw->drawingarea));
+ h = gtk_widget_get_allocated_height(GTK_WIDGET(dbgw->drawingarea));
+ gtk_widget_queue_draw_area(GTK_WIDGET(dbgw->drawingarea), 0, 0, w, h);
+ return TRUE;
+}
+
+
+static gboolean close_sig(GtkWidget *widget, GdkEvent *event, struct debugwindow *dbgw)
+{
+ g_source_remove(dbgw->timeout);
+ free(dbgw);
+ return FALSE;
+}
+
+
+void open_debugger(struct frame *fr)
+{
+ struct debugwindow *dbgw;
+ GtkWidget *scroll;
+
+ if ( fr == NULL ) return;
+
+ dbgw = calloc(1, sizeof(struct debugwindow));
+ if ( dbgw == NULL ) return;
+
+ dbgw->fr = fr;
+
+ dbgw->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ gtk_window_set_role(GTK_WINDOW(dbgw->window), "debugger");
+ gtk_window_set_title(GTK_WINDOW(dbgw->window), "Colloquium debugger");
+
+ scroll = gtk_scrolled_window_new(NULL, NULL);
+ gtk_container_add(GTK_CONTAINER(dbgw->window), scroll);
+
+ dbgw->drawingarea = gtk_drawing_area_new();
+ gtk_container_add(GTK_CONTAINER(scroll), dbgw->drawingarea);
+ gtk_widget_set_size_request(dbgw->drawingarea, 100, 8000);
+
+ g_signal_connect(G_OBJECT(dbgw->drawingarea), "draw",
+ G_CALLBACK(dbg_draw_sig), dbgw);
+
+ g_signal_connect(G_OBJECT(dbgw->window), "delete-event",
+ G_CALLBACK(close_sig), dbgw);
+
+ dbgw->timeout = g_timeout_add(1000, queue_redraw, dbgw);
+
+ gtk_widget_show_all(dbgw->window);
+}
diff --git a/src-old/debugger.h b/src-old/debugger.h
new file mode 100644
index 0000000..d6191bf
--- /dev/null
+++ b/src-old/debugger.h
@@ -0,0 +1,32 @@
+/*
+ * debugger.h
+ *
+ * Copyright © 2017-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef DEBUGGER_H
+#define DEBUGGER_H
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+extern void open_debugger(struct frame *fr);
+
+#endif /* DEBUGGER_H */
diff --git a/src-old/frame.c b/src-old/frame.c
new file mode 100644
index 0000000..08b8fd9
--- /dev/null
+++ b/src-old/frame.c
@@ -0,0 +1,1820 @@
+/*
+ * frame.c
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+
+#include "sc_parse.h"
+#include "frame.h"
+#include "imagestore.h"
+#include "utils.h"
+
+struct text_run
+{
+ SCBlock *scblock;
+ char *real_text; /* Usually NULL */
+ PangoFontDescription *fontdesc;
+ double col[4];
+};
+
+struct _paragraph
+{
+ enum para_type type;
+ double height;
+ float space[4];
+ SCBlock *newline_at_end;
+ int empty;
+
+ /* For PARA_TYPE_TEXT */
+ int n_runs;
+ struct text_run *runs;
+ PangoLayout *layout;
+ PangoAlignment alignment;
+
+ /* For anything other than PARA_TYPE_TEXT
+ * (for text paragraphs, these things are in the runs) */
+ SCBlock *scblock;
+
+ /* For PARA_TYPE_IMAGE */
+ char *filename;
+ double image_w;
+ double image_h;
+ int image_real_w;
+ int image_real_h;
+
+ /* For PARA_TYPE_CALLBACK */
+ double cb_w;
+ double cb_h;
+ SCCallbackDrawFunc draw_func;
+ SCCallbackClickFunc click_func;
+ void *bvp;
+ void *vp;
+};
+
+
+/* Returns the height of the paragraph including all spacing, padding etc */
+double paragraph_height(Paragraph *para)
+{
+ return para->height + para->space[2] + para->space[3];
+}
+
+
+static int alloc_ro(struct frame *fr)
+{
+ struct frame **new_ro;
+
+ new_ro = realloc(fr->children,
+ fr->max_children*sizeof(struct frame *));
+ if ( new_ro == NULL ) return 1;
+
+ fr->children = new_ro;
+
+ return 0;
+}
+
+
+struct frame *frame_new()
+{
+ struct frame *n;
+
+ n = calloc(1, sizeof(struct frame));
+ if ( n == NULL ) return NULL;
+
+ n->children = NULL;
+ n->max_children = 32;
+ if ( alloc_ro(n) ) {
+ fprintf(stderr, "Couldn't allocate children\n");
+ free(n);
+ return NULL;
+ }
+ n->num_children = 0;
+
+ n->scblocks = NULL;
+ n->n_paras = 0;
+ n->paras = NULL;
+
+ return n;
+}
+
+
+static void free_paragraph(Paragraph *para)
+{
+ int i;
+
+ for ( i=0; i<para->n_runs; i++ ) {
+ pango_font_description_free(para->runs[i].fontdesc);
+ free(para->runs[i].real_text); /* free(NULL) is OK */
+ }
+ free(para->runs);
+ if ( para->layout != NULL ) g_object_unref(para->layout);
+ free(para);
+}
+
+
+void frame_free(struct frame *fr)
+{
+ int i;
+
+ if ( fr == NULL ) return;
+
+ /* Free paragraphs */
+ if ( fr->paras != NULL ) {
+ for ( i=0; i<fr->n_paras; i++ ) {
+ free_paragraph(fr->paras[i]);
+ }
+ free(fr->paras);
+ }
+
+ /* Free all children */
+ for ( i=0; i<fr->num_children; i++ ) {
+ frame_free(fr->children[i]);
+ }
+ free(fr->children);
+
+ free(fr);
+}
+
+
+struct frame *add_subframe(struct frame *fr)
+{
+ struct frame *n;
+
+ n = frame_new();
+ if ( n == NULL ) return NULL;
+
+ if ( fr->num_children == fr->max_children ) {
+ fr->max_children += 32;
+ if ( alloc_ro(fr) ) return NULL;
+ }
+
+ fr->children[fr->num_children++] = n;
+
+ return n;
+}
+
+
+void show_frame_hierarchy(struct frame *fr, const char *t)
+{
+ int i;
+ char tn[1024];
+
+ strcpy(tn, t);
+ strcat(tn, " ");
+
+ printf("%s%p (%.2f x %.2f)\n", t, fr, fr->w, fr->h);
+
+ for ( i=0; i<fr->num_children; i++ ) {
+ show_frame_hierarchy(fr->children[i], tn);
+ }
+
+}
+
+
+struct frame *find_frame_with_scblocks(struct frame *fr, SCBlock *scblocks)
+{
+ int i;
+
+ if ( fr->scblocks == scblocks ) return fr;
+
+ for ( i=0; i<fr->num_children; i++ ) {
+ struct frame *tt;
+ tt = find_frame_with_scblocks(fr->children[i], scblocks);
+ if ( tt != NULL ) return tt;
+ }
+
+ return NULL;
+}
+
+
+static const char *text_for_run(const struct text_run *run)
+{
+ if ( run == NULL ) {
+ fprintf(stderr, "NULL run passed to text_for_run\n");
+ return 0;
+ }
+
+ if ( run->scblock == NULL ) {
+ fprintf(stderr, "NULL scblock in text_for_run\n");
+ return 0;
+ }
+
+ if ( run->real_text != NULL ) {
+ return run->real_text;
+ }
+
+ return sc_block_contents(run->scblock);
+}
+
+
+static size_t run_text_len(const struct text_run *run)
+{
+ if ( run == NULL ) {
+ fprintf(stderr, "NULL run passed to run_text_len\n");
+ return 0;
+ }
+
+ if ( run->scblock == NULL ) {
+ fprintf(stderr, "NULL scblock in run_text_len\n");
+ return 0;
+ }
+
+ if ( run->real_text != NULL ) {
+ return strlen(run->real_text);
+ }
+
+ if ( sc_block_contents(run->scblock) == NULL ) {
+ fprintf(stderr, "NULL scblock contents in run_text_len\n");
+ return 0;
+ }
+
+ return strlen(sc_block_contents(run->scblock));
+}
+
+
+void wrap_paragraph(Paragraph *para, PangoContext *pc, double w,
+ size_t sel_start, size_t sel_end)
+{
+ size_t total_len = 0;
+ int i;
+ char *text;
+ PangoAttrList *attrs;
+ PangoRectangle rect;
+ size_t pos = 0;
+
+ w -= para->space[0] + para->space[1];
+
+ if ( para->type == PARA_TYPE_IMAGE ) {
+ if ( para->image_w < 0.0 ) {
+ para->image_w = w;
+ para->image_h = w*((float)para->image_real_h/para->image_real_w);
+ }
+ para->height = para->image_h;
+ return;
+ }
+
+ if ( para->type != PARA_TYPE_TEXT ) return;
+
+ for ( i=0; i<para->n_runs; i++ ) {
+ total_len += run_text_len(&para->runs[i]);
+ }
+
+ /* Allocate the complete text */
+ text = malloc(total_len+1);
+ if ( text == NULL ) {
+ fprintf(stderr, "Couldn't allocate combined text (%lli)\n",
+ (long long int)total_len);
+ return;
+ }
+
+ /* Allocate the attributes */
+ attrs = pango_attr_list_new();
+
+ /* Put all of the text together */
+ text[0] = '\0';
+ for ( i=0; i<para->n_runs; i++ ) {
+
+ PangoAttribute *attr;
+ const char *run_text;
+ size_t run_len;
+ guint16 r, g, b;
+
+ run_text = text_for_run(&para->runs[i]);
+ run_len = strlen(run_text);
+
+ attr = pango_attr_font_desc_new(para->runs[i].fontdesc);
+ attr->start_index = pos;
+ attr->end_index = pos + run_len;
+ pango_attr_list_insert(attrs, attr);
+
+ r = para->runs[i].col[0] * 65535;
+ g = para->runs[i].col[1] * 65535;
+ b = para->runs[i].col[2] * 65535;
+ attr = pango_attr_foreground_new(r, g, b);
+ attr->start_index = pos;
+ attr->end_index = pos + run_len;
+ pango_attr_list_insert(attrs, attr);
+
+ pos += run_len;
+ strncat(text, run_text, run_len);
+
+ }
+
+ /* Add attributes for selected text */
+ if ( sel_start > 0 || sel_end > 0 ) {
+ PangoAttribute *attr;
+ attr = pango_attr_background_new(42919, 58853, 65535);
+ attr->start_index = sel_start;
+ attr->end_index = sel_end;
+ pango_attr_list_insert(attrs, attr);
+ }
+
+ if ( para->layout == NULL ) {
+ para->layout = pango_layout_new(pc);
+ }
+ pango_layout_set_width(para->layout, pango_units_from_double(w));
+ pango_layout_set_text(para->layout, text, total_len);
+ pango_layout_set_alignment(para->layout, para->alignment);
+ pango_layout_set_attributes(para->layout, attrs);
+ free(text);
+ pango_attr_list_unref(attrs);
+
+ pango_layout_get_extents(para->layout, NULL, &rect);
+ para->height = pango_units_to_double(rect.height);
+}
+
+static SCBlock *get_newline_at_end(Paragraph *para)
+{
+ return para->newline_at_end;
+}
+
+
+SCBlock *para_debug_get_newline_at_end(Paragraph *para)
+{
+ return get_newline_at_end(para);
+}
+
+
+void set_newline_at_end(Paragraph *para, SCBlock *bl)
+{
+ para->newline_at_end = bl;
+}
+
+
+void add_run(Paragraph *para, SCBlock *scblock,
+ PangoFontDescription *fdesc, double col[4], const char *real_text)
+{
+ struct text_run *runs_new;
+
+ runs_new = realloc(para->runs,
+ (para->n_runs+1)*sizeof(struct text_run));
+ if ( runs_new == NULL ) {
+ fprintf(stderr, "Failed to add run.\n");
+ return;
+ }
+
+ para->runs = runs_new;
+ para->runs[para->n_runs].scblock = scblock;
+ if ( real_text != NULL ) {
+ para->runs[para->n_runs].real_text = strdup(real_text);
+ } else {
+ para->runs[para->n_runs].real_text = NULL;
+ }
+ para->runs[para->n_runs].fontdesc = pango_font_description_copy(fdesc);
+ para->runs[para->n_runs].col[0] = col[0];
+ para->runs[para->n_runs].col[1] = col[1];
+ para->runs[para->n_runs].col[2] = col[2];
+ para->runs[para->n_runs].col[3] = col[3];
+ para->empty = 0;
+ para->n_runs++;
+}
+
+
+Paragraph *create_paragraph(struct frame *fr, SCBlock *bl)
+{
+ Paragraph **paras_new;
+ Paragraph *pnew;
+
+ paras_new = realloc(fr->paras, (fr->n_paras+1)*sizeof(Paragraph *));
+ if ( paras_new == NULL ) return NULL;
+
+ pnew = calloc(1, sizeof(struct _paragraph));
+ if ( pnew == NULL ) return NULL;
+
+ fr->paras = paras_new;
+ fr->paras[fr->n_paras++] = pnew;
+
+ /* For now, assume the paragraph is going to be for text.
+ * However, this can easily be changed */
+ pnew->type = PARA_TYPE_TEXT;
+ pnew->scblock = bl;
+ pnew->n_runs = 0;
+ pnew->runs = NULL;
+ pnew->layout = NULL;
+ pnew->height = 0.0;
+ pnew->alignment = PANGO_ALIGN_LEFT;
+ pnew->empty = 1;
+
+ return pnew;
+}
+
+
+/* Create a new paragraph in 'fr' just after paragraph 'pos' */
+Paragraph *insert_paragraph(struct frame *fr, int pos)
+{
+ Paragraph **paras_new;
+ Paragraph *pnew;
+ int i;
+
+ if ( pos >= fr->n_paras ) {
+ fprintf(stderr, "insert_paragraph(): pos too high!\n");
+ return NULL;
+ }
+
+ paras_new = realloc(fr->paras, (fr->n_paras+1)*sizeof(Paragraph *));
+ if ( paras_new == NULL ) return NULL;
+
+ pnew = calloc(1, sizeof(struct _paragraph));
+ if ( pnew == NULL ) return NULL;
+
+ fr->paras = paras_new;
+ fr->n_paras++;
+
+ for ( i=fr->n_paras-1; i>pos; i-- ) {
+ fr->paras[i] = fr->paras[i-1];
+ }
+ fr->paras[pos+1] = pnew;
+
+ return pnew;
+}
+
+
+Paragraph *add_callback_para(struct frame *fr, SCBlock *bl, double w, double h,
+ SCCallbackDrawFunc draw_func,
+ SCCallbackClickFunc click_func, void *bvp,
+ void *vp)
+{
+ Paragraph *pnew;
+
+ if ( (fr->n_paras > 0) && (fr->paras[fr->n_paras-1]->empty) ) {
+ pnew = fr->paras[fr->n_paras-1];
+ } else {
+ pnew = create_paragraph(fr, bl);
+ if ( pnew == NULL ) {
+ fprintf(stderr, "Failed to add callback paragraph\n");
+ return NULL;
+ }
+ }
+
+ pnew->type = PARA_TYPE_CALLBACK;
+ pnew->scblock = bl;
+ pnew->cb_w = w;
+ pnew->cb_h = h;
+ pnew->draw_func = draw_func;
+ pnew->click_func = click_func;
+ pnew->bvp = bvp;
+ pnew->vp = vp;
+ pnew->height = h;
+ pnew->empty = 0;
+
+ return pnew;
+}
+
+
+void add_image_para(struct frame *fr, SCBlock *scblock,
+ const char *filename,
+ ImageStore *is, double w, double h, int editable)
+{
+ Paragraph *pnew;
+ int wi, hi;
+
+ if ( is == NULL ) {
+ fprintf(stderr, "Adding image without ImageStore!\n");
+ return;
+ }
+
+ if ( (fr->n_paras > 0) && (fr->paras[fr->n_paras-1]->empty) ) {
+ pnew = fr->paras[fr->n_paras-1];
+ } else {
+ pnew = create_paragraph(fr, scblock);
+ if ( pnew == NULL ) {
+ fprintf(stderr, "Failed to add image paragraph\n");
+ return;
+ }
+ }
+
+ if ( imagestore_get_size(is, filename, &wi, &hi) ) {
+ fprintf(stderr, _("Couldn't determine size of image '%s'\n"),
+ filename);
+ wi = 100;
+ hi = 100;
+ }
+
+ pnew->type = PARA_TYPE_IMAGE;
+ pnew->scblock = scblock;
+ pnew->filename = strdup(filename);
+ pnew->image_w = w;
+ pnew->image_h = h;
+ pnew->image_real_w = wi;
+ pnew->image_real_h = hi;
+ pnew->height = h;
+ pnew->space[0] = 0.0;
+ pnew->space[1] = 0.0;
+ pnew->space[2] = 0.0;
+ pnew->space[3] = 0.0;
+ pnew->empty = 0;
+}
+
+
+double total_height(struct frame *fr)
+{
+ int i;
+ double t = 0.0;
+ for ( i=0; i<fr->n_paras; i++ ) {
+ t += paragraph_height(fr->paras[i]);
+ }
+ return t;
+}
+
+
+Paragraph *last_para(struct frame *fr)
+{
+ if ( fr == NULL ) return NULL;
+ if ( fr->paras == NULL ) return NULL;
+ return fr->paras[fr->n_paras-1];
+}
+
+
+static void render_from_surf(cairo_surface_t *surf, cairo_t *cr,
+ double w, double h, int border)
+{
+ double x, y;
+ int sw, sh;
+
+ x = 0.0; y = 0.0;
+ cairo_user_to_device(cr, &x, &y);
+ x = rint(x); y = rint(y);
+ cairo_device_to_user(cr, &x, &y);
+
+ sw = cairo_image_surface_get_width(surf);
+ sh = cairo_image_surface_get_height(surf);
+
+ cairo_save(cr);
+ cairo_scale(cr, w/sw, h/sh);
+ cairo_new_path(cr);
+ cairo_rectangle(cr, x, y, sw, sh);
+ cairo_set_source_surface(cr, surf, 0.0, 0.0);
+ cairo_pattern_t *patt = cairo_get_source(cr);
+ cairo_pattern_set_extend(patt, CAIRO_EXTEND_PAD);
+ cairo_pattern_set_filter(patt, CAIRO_FILTER_BEST);
+ cairo_fill(cr);
+ cairo_restore(cr);
+
+ if ( border ) {
+ cairo_new_path(cr);
+ cairo_rectangle(cr, x+0.5, y+0.5, w, h);
+ cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
+ cairo_set_line_width(cr, 1.0);
+ cairo_stroke(cr);
+ }
+}
+
+
+void render_paragraph(cairo_t *cr, Paragraph *para, ImageStore *is)
+{
+ cairo_surface_t *surf;
+ cairo_surface_type_t type;
+ double w, h;
+
+ cairo_translate(cr, para->space[0], para->space[2]);
+
+ type = cairo_surface_get_type(cairo_get_target(cr));
+
+ switch ( para->type ) {
+
+ case PARA_TYPE_TEXT :
+ cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 1.0);
+ pango_cairo_update_layout(cr, para->layout);
+ pango_cairo_show_layout(cr, para->layout);
+ cairo_fill(cr);
+ break;
+
+ case PARA_TYPE_IMAGE :
+ w = para->image_w;
+ h = para->image_h;
+ cairo_user_to_device_distance(cr, &w, &h);
+ surf = lookup_image(is, para->filename, w);
+ if ( surf != NULL ) {
+ render_from_surf(surf, cr, para->image_w, para->image_h, 0);
+ } else {
+ printf("surf = NULL!\n");
+ }
+ break;
+
+ case PARA_TYPE_CALLBACK :
+ w = para->cb_w;
+ h = para->cb_h;
+ cairo_user_to_device_distance(cr, &w, &h);
+ if ( type == CAIRO_SURFACE_TYPE_PDF ) {
+ w *= 6; h *= 6;
+ }
+ surf = para->draw_func(w, h,
+ para->bvp, para->vp);
+ render_from_surf(surf, cr, para->cb_w, para->cb_h, 1);
+ cairo_surface_destroy(surf); /* FIXME: Cache like crazy */
+ break;
+
+ }
+}
+
+
+static size_t end_offset_of_para(struct frame *fr, int pn)
+{
+ int i;
+ size_t total = 0;
+ for ( i=0; i<fr->paras[pn]->n_runs; i++ ) {
+ total += run_text_len(&fr->paras[pn]->runs[i]);
+ }
+ return total;
+}
+
+
+/* Local x,y in paragraph -> text offset */
+static size_t text_para_pos(Paragraph *para, double x, double y, int *ptrail)
+{
+ int idx;
+ pango_layout_xy_to_index(para->layout, pango_units_from_double(x),
+ pango_units_from_double(y), &idx, ptrail);
+ return idx;
+}
+
+
+void show_edit_pos(struct edit_pos a)
+{
+ printf("para %i, pos %li, trail %i\n", a.para, (long int)a.pos, a.trail);
+}
+
+
+int positions_equal(struct edit_pos a, struct edit_pos b)
+{
+ if ( a.para != b.para ) return 0;
+ if ( a.pos != b.pos ) return 0;
+ if ( a.trail != b.trail ) return 0;
+ return 1;
+}
+
+
+void sort_positions(struct edit_pos *a, struct edit_pos *b)
+{
+ if ( a->para > b->para ) {
+ size_t tpos;
+ int tpara, ttrail;
+ tpara = b->para; tpos = b->pos; ttrail = b->trail;
+ b->para = a->para; b->pos = a->pos; b->trail = a->trail;
+ a->para = tpara; a->pos = tpos; a->trail = ttrail;
+ }
+
+ if ( (a->para == b->para) && (a->pos > b->pos) )
+ {
+ size_t tpos = b->pos;
+ int ttrail = b->trail;
+ b->pos = a->pos; b->trail = a->trail;
+ a->pos = tpos; a->trail = ttrail;
+ }
+}
+
+
+static PangoFontDescription *last_font_desc_and_col(struct frame *fr, int pn, double *col)
+{
+ int i;
+
+ for ( i=pn-1; i>=0; i-- ) {
+ if ( fr->paras[i]->type != PARA_TYPE_TEXT ) continue;
+ if ( fr->paras[i]->n_runs == 0 ) continue;
+ col[0] = fr->paras[i]->runs[fr->paras[i]->n_runs-1].col[0];
+ col[1] = fr->paras[i]->runs[fr->paras[i]->n_runs-1].col[1];
+ col[2] = fr->paras[i]->runs[fr->paras[i]->n_runs-1].col[2];
+ col[3] = fr->paras[i]->runs[fr->paras[i]->n_runs-1].col[3];
+ return fr->paras[i]->runs[fr->paras[i]->n_runs-1].fontdesc;
+ }
+
+ /* No previous text at all, so use the default text style
+ * (which is valid for new text in the frame, so this is OK) */
+ col[0] = fr->col[0];
+ col[1] = fr->col[1];
+ col[2] = fr->col[2];
+ col[3] = fr->col[3];
+ return fr->fontdesc;
+}
+
+
+void ensure_run(struct frame *fr, struct edit_pos cpos)
+{
+ SCBlock *bl;
+ Paragraph *para;
+ PangoFontDescription *fontdesc;
+ double col[4];
+
+ para = fr->paras[cpos.para];
+
+ if ( para->n_runs > 0 ) return;
+
+ if ( para->type != PARA_TYPE_TEXT ) return;
+
+ if ( para->scblock != NULL ) {
+
+ bl = sc_block_prepend(para->scblock, fr->scblocks);
+ if ( bl == NULL ) {
+ fprintf(stderr, "Couldn't prepend block\n");
+ return;
+ }
+ sc_block_set_contents(bl, strdup(""));
+
+ } else {
+
+ /* If the paragraph's SCBlock is NULL, it means this paragraph
+ * is right at the end of the document. The last thing in the
+ * document is something like \newpara. */
+ bl = sc_block_append_inside(fr->scblocks, NULL, NULL, strdup(""));
+
+ }
+
+ para->scblock = bl;
+
+ fontdesc = last_font_desc_and_col(fr, cpos.para, col);
+ add_run(para, bl, fontdesc, col, NULL);
+ wrap_paragraph(para, NULL, fr->w - fr->pad_l - fr->pad_r, 0, 0);
+}
+
+
+int find_cursor(struct frame *fr, double x, double y, struct edit_pos *pos)
+{
+ double pad;
+ int i;
+
+ if ( fr == NULL ) {
+ fprintf(stderr, "Cursor frame is NULL.\n");
+ return 1;
+ }
+
+ pad = fr->pad_t;
+
+ for ( i=0; i<fr->n_paras; i++ ) {
+ double npos = pad + paragraph_height(fr->paras[i]);
+ if ( npos > y ) {
+ pos->para = i;
+ if ( fr->paras[i]->type == PARA_TYPE_TEXT ) {
+ pos->pos = text_para_pos(fr->paras[i],
+ x - fr->pad_l - fr->paras[i]->space[0],
+ y - pad - fr->paras[i]->space[2],
+ &pos->trail);
+ } else {
+ pos->pos = 0;
+ }
+ return 0;
+ }
+ pad = npos;
+ }
+
+ if ( fr->n_paras == 0 ) {
+ printf("No paragraphs in frame.\n");
+ return 1;
+ }
+
+ /* Pretend it's in the last paragraph */
+ pad -= fr->paras[fr->n_paras-1]->height;
+ pos->para = fr->n_paras - 1;
+ pos->pos = text_para_pos(fr->paras[fr->n_paras-1],
+ x - fr->pad_l, y - pad, &pos->trail);
+ return 0;
+}
+
+
+int get_para_highlight(struct frame *fr, int cursor_para,
+ double *cx, double *cy, double *cw, double *ch)
+{
+ Paragraph *para;
+ int i;
+ double py = 0.0;
+
+ if ( fr == NULL ) {
+ fprintf(stderr, "Cursor frame is NULL.\n");
+ return 1;
+ }
+
+ if ( cursor_para >= fr->n_paras ) {
+ fprintf(stderr, "Highlight paragraph number is too high!\n");
+ return 1;
+ }
+
+ para = fr->paras[cursor_para];
+ for ( i=0; i<cursor_para; i++ ) {
+ py += paragraph_height(fr->paras[i]);
+ }
+
+ *cx = fr->pad_l;
+ *cy = fr->pad_t + py;
+ *cw = fr->w - fr->pad_l - fr->pad_r;
+ *ch = paragraph_height(para);
+ return 0;
+}
+
+
+int get_cursor_pos(struct frame *fr, int cursor_para, int cursor_pos,
+ double *cx, double *cy, double *ch)
+{
+ Paragraph *para;
+ PangoRectangle rect;
+ int i;
+ double py = 0.0;
+
+ if ( fr == NULL ) {
+ fprintf(stderr, "Cursor frame is NULL.\n");
+ return 1;
+ }
+
+ if ( cursor_para >= fr->n_paras ) {
+ fprintf(stderr, "Cursor paragraph number is too high!\n");
+ return 1;
+ }
+
+ para = fr->paras[cursor_para];
+ for ( i=0; i<cursor_para; i++ ) {
+ py += paragraph_height(fr->paras[i]);
+ }
+
+ if ( para->type != PARA_TYPE_TEXT ) {
+ return 1;
+ }
+
+ pango_layout_get_cursor_pos(para->layout, cursor_pos, &rect, NULL);
+
+ *cx = pango_units_to_double(rect.x) + fr->pad_l + para->space[0];
+ *cy = pango_units_to_double(rect.y) + fr->pad_t + py + para->space[2];
+ *ch = pango_units_to_double(rect.height);
+ return 0;
+}
+
+
+//void cursor_moveh(struct frame *fr, int *cpara, size_t *cpos, int *ctrail,
+void cursor_moveh(struct frame *fr, struct edit_pos *cp, signed int dir)
+{
+ Paragraph *para = fr->paras[cp->para];
+ int np = cp->pos;
+
+ pango_layout_move_cursor_visually(para->layout, 1, cp->pos, cp->trail,
+ dir, &np, &cp->trail);
+ if ( np == -1 ) {
+ if ( cp->para > 0 ) {
+ size_t end_offs;
+ cp->para--;
+ end_offs = end_offset_of_para(fr, cp->para);
+ if ( end_offs > 0 ) {
+ cp->pos = end_offs - 1;
+ cp->trail = 1;
+ } else {
+ /* Jumping into an empty paragraph */
+ cp->pos = 0;
+ cp->trail = 0;
+ }
+ return;
+ } else {
+ /* Can't move any further */
+ return;
+ }
+ }
+
+ if ( np == G_MAXINT ) {
+ if ( cp->para < fr->n_paras-1 ) {
+ cp->para++;
+ cp->pos = 0;
+ cp->trail = 0;
+ return;
+ } else {
+ /* Can't move any further */
+ return;
+ }
+ }
+
+ cp->pos = np;
+}
+
+
+void check_callback_click(struct frame *fr, int para)
+{
+ Paragraph *p = fr->paras[para];
+ if ( p->type == PARA_TYPE_CALLBACK ) {
+ p->click_func(0.0, 0.0, p->bvp, p->vp);
+ }
+}
+
+
+static int get_paragraph_offset(Paragraph *para, int nrun)
+{
+ int i;
+ size_t t = 0;
+
+ for ( i=0; i<nrun; i++ ) {
+ struct text_run *run = &para->runs[i];
+ t += run_text_len(run);
+ }
+ return t;
+}
+
+
+static int which_run(Paragraph *para, size_t offs)
+{
+ int i;
+ size_t t = 0;
+
+ for ( i=0; i<para->n_runs; i++ ) {
+ struct text_run *run = &para->runs[i];
+ t += run_text_len(run);
+ if ( t > offs ) return i;
+ }
+
+ /* Maybe offs points exactly to the end of the last run? */
+ if ( t == offs ) return para->n_runs-1;
+
+ return para->n_runs;
+}
+
+
+size_t pos_trail_to_offset(Paragraph *para, size_t offs, int trail)
+{
+ glong char_offs;
+ size_t run_offs;
+ const char *run_text;
+ struct text_run *run;
+ int nrun;
+ char *ptr;
+ size_t para_offset_of_run;
+
+ nrun = which_run(para, offs);
+
+ if ( nrun == para->n_runs ) {
+ fprintf(stderr, "pos_trail_to_offset: Offset too high\n");
+ return 0;
+ }
+
+ if ( para->n_runs == 0 ) {
+ return 0;
+ }
+
+ run = &para->runs[nrun];
+
+ if ( para->type != PARA_TYPE_TEXT ) return 0;
+
+ if ( run == NULL ) {
+ fprintf(stderr, "pos_trail_to_offset: No run\n");
+ return 0;
+ }
+
+ if ( run->scblock == NULL ) {
+ fprintf(stderr, "pos_trail_to_offset: SCBlock = NULL?\n");
+ return 0;
+ }
+
+ /* Get the text for the run */
+ run_text = text_for_run(run);
+ if ( run_text == NULL ) {
+ fprintf(stderr, "pos_trail_to_offset: No contents "
+ "(%p name=%s, options=%s)\n",
+ run->scblock, sc_block_name(run->scblock),
+ sc_block_options(run->scblock));
+ return 0;
+ }
+
+
+ /* Turn the paragraph offset into a run offset */
+ para_offset_of_run = get_paragraph_offset(para, nrun);
+ run_offs = offs - para_offset_of_run;
+
+ char_offs = g_utf8_pointer_to_offset(run_text, run_text+run_offs);
+ char_offs += trail;
+
+ if ( char_offs > g_utf8_strlen(run_text, -1) ) {
+ printf("Offset outside string! '%s'\n"
+ "char_offs %li offs %li len %li\n",
+ run_text, (long int)char_offs, (long int)offs,
+ (long int)g_utf8_strlen(run_text, -1));
+ }
+
+ ptr = g_utf8_offset_to_pointer(run_text, char_offs);
+ return ptr - run_text + para_offset_of_run;
+}
+
+
+int position_editable(struct frame *fr, struct edit_pos cp)
+{
+ Paragraph *para;
+ int run;
+ size_t paraoffs;
+
+ if ( fr == NULL ) {
+ fprintf(stderr, "Frame is NULL.\n");
+ return 0;
+ }
+
+ if ( cp.para >= fr->n_paras ) {
+ fprintf(stderr, "Paragraph number is too high!\n");
+ return 0;
+ }
+
+ para = fr->paras[cp.para];
+
+ if ( para->type != PARA_TYPE_TEXT ) {
+ fprintf(stderr, "Paragraph is not text.\n");
+ return 0;
+ }
+
+ paraoffs = pos_trail_to_offset(para, cp.pos, cp.trail);
+ run = which_run(para, paraoffs);
+ if ( run == para->n_runs ) {
+ fprintf(stderr, "Couldn't find run!\n");
+ return 0;
+ }
+
+ if ( para->runs[run].real_text != NULL ) return 0;
+
+ return 1;
+}
+
+
+void insert_text_in_paragraph(Paragraph *para, size_t offs, const char *t)
+{
+ int nrun;
+
+ /* Find which run we are in */
+ nrun = which_run(para, offs);
+ if ( nrun == para->n_runs ) {
+ fprintf(stderr, "Couldn't find run to insert into.\n");
+ return;
+ }
+
+ if ( para->n_runs == 0 ) {
+ printf("No runs in paragraph?\n");
+ } else {
+ struct text_run *run;
+ size_t run_offs;
+ run = &para->runs[nrun];
+ run_offs = offs - get_paragraph_offset(para, nrun);
+ sc_insert_text(run->scblock, run_offs, t);
+ }
+}
+
+
+static SCBlock *pos_to_scblock(struct frame *fr, struct edit_pos p,
+ enum para_type *type)
+{
+ int run;
+ size_t paraoffs;
+ Paragraph *para;
+
+ para = fr->paras[p.para];
+ if ( type != NULL ) {
+ *type = para->type;
+ }
+
+ if ( para->type != PARA_TYPE_TEXT ) {
+ return para->scblock;
+ }
+
+ paraoffs = pos_trail_to_offset(para, p.pos, p.trail);
+
+ run = which_run(para, paraoffs);
+ assert(run < para->n_runs);
+
+ return para->runs[run].scblock;
+}
+
+
+static size_t pos_to_offset(struct frame *fr, struct edit_pos p)
+{
+ int run;
+ size_t paraoffs;
+ Paragraph *para;
+
+ para = fr->paras[p.para];
+ if ( para->type != PARA_TYPE_TEXT ) {
+ return 0;
+ }
+
+ /* Offset of this position into the paragraph */
+ paraoffs = pos_trail_to_offset(para, p.pos, p.trail);
+
+ run = which_run(para, paraoffs);
+ assert(run < para->n_runs);
+
+ /* Offset of this position into the run
+ * (and therefore into the SCBlock) */
+ return paraoffs - get_paragraph_offset(para, run);
+}
+
+
+static int pos_to_run_number(struct frame *fr, struct edit_pos p)
+{
+ int run;
+ size_t paraoffs;
+ Paragraph *para;
+
+ para = fr->paras[p.para];
+ if ( para->type != PARA_TYPE_TEXT ) {
+ return 0;
+ }
+
+ paraoffs = pos_trail_to_offset(para, p.pos, p.trail);
+
+ run = which_run(para, paraoffs);
+ assert(run < para->n_runs);
+
+ return run;
+}
+
+
+static void delete_run(Paragraph *para, int nrun)
+{
+ printf("deleting run %i of %i from para %p\n", nrun, para->n_runs, para);
+ memmove(&para->runs[nrun], &para->runs[nrun+1],
+ (para->n_runs-nrun-1)*sizeof(struct text_run));
+ para->n_runs--;
+}
+
+
+static Paragraph *scan_runs_for_scblock(struct frame *fr, int pn1, int pn2,
+ SCBlock *bl, int *run)
+{
+ int i;
+
+ for ( i=pn1; i<=pn2; i++ ) {
+
+ int j;
+
+ /* Non-text paragraph - just one thing to check */
+ if ( fr->paras[i]->scblock == bl ) {
+ *run = 0;
+ return fr->paras[i];
+ }
+
+ /* Check all runs */
+ for ( j=0; j<fr->paras[i]->n_runs; j++ ) {
+ if ( fr->paras[i]->runs[j].scblock == bl ) {
+ *run = j;
+ return fr->paras[i];
+ }
+ }
+ }
+ return NULL;
+}
+
+
+static Paragraph *find_run_for_scblock_next(struct frame *fr, int pn1, int pn2,
+ SCBlock *bl, int *run)
+{
+ if ( sc_block_child(bl) != NULL ) {
+ Paragraph *para;
+ para = find_run_for_scblock_next(fr, pn1, pn2,
+ sc_block_child(bl), run);
+ if ( para != NULL ) return para;
+ }
+
+ do {
+ Paragraph *para;
+ para = scan_runs_for_scblock(fr, pn1, pn2, bl, run);
+ if ( para != NULL ) return para;
+ bl = sc_block_next(bl);
+ } while ( bl != NULL );
+
+ return NULL;
+}
+
+
+/* Find the run which contains the text from "bl",
+ * taking into account that it might be a child block, for example:
+ * {some text}
+ * \italic <---- bl points here
+ * {more text} <---- but this block is referenced by the run
+ * {final text}
+ */
+static Paragraph *find_run_for_scblock(struct frame *fr, int pn1, int pn2,
+ SCBlock *bl, int *run)
+{
+ Paragraph *para;
+
+ show_sc_block(bl, "searching ");
+ para = scan_runs_for_scblock(fr, pn1, pn2, bl, run);
+ if ( para != NULL ) return para;
+
+ if ( sc_block_child(bl) != NULL ) {
+ para = find_run_for_scblock_next(fr, pn1, pn2, sc_block_child(bl), run);
+ if ( para != NULL ) return para;
+ }
+
+ return NULL;
+}
+
+
+static int paragraph_number(struct frame *fr, Paragraph *p, int *err)
+{
+ int i;
+ for ( i=0; i<fr->n_paras; i++ ) {
+ if ( fr->paras[i] == p ) return i;
+ }
+ fprintf(stderr, "Couldn't find paragraph %p\n", p);
+ *err = 1;
+ return 0;
+}
+
+
+static int find_para(struct frame *fr, Paragraph *para)
+{
+ int i;
+
+ for ( i=0; i<fr->n_paras; i++ ) {
+ if ( fr->paras[i] == para ) return i;
+ }
+
+ return fr->n_paras;
+}
+
+
+static void delete_paragraph(struct frame *fr, Paragraph *para, int *pnp)
+{
+ int pn = find_para(fr, para);
+ if ( pn == fr->n_paras ) {
+ fprintf(stderr, "Couldn't find paragraph to delete (%p)\n", para);
+ return;
+ }
+
+ printf("deleting paragraph %i (%p)\n", pn, para);
+
+ memmove(&fr->paras[pn], &fr->paras[pn+1],
+ (fr->n_paras-pn-1)*sizeof(Paragraph *));
+ fr->n_paras--;
+
+ if ( (pnp != NULL) && (*pnp > pn) ) {
+ (*pnp)--;
+ }
+}
+
+
+static void delete_run_for_scblock(struct frame *fr,
+ Paragraph *p1, Paragraph *p2, SCBlock *bl,
+ int *pnp)
+{
+ int pn1, pn2;
+ int err = 0;
+ Paragraph *para;
+ int run;
+
+ pn1 = paragraph_number(fr, p1, &err);
+ pn2 = paragraph_number(fr, p2, &err);
+ if ( err ) return;
+
+ para = find_run_for_scblock(fr, pn1, pn2, bl, &run);
+ if ( para == NULL ) {
+ fprintf(stderr, "Couldn't find block %p between paragraphs %p and %p\n",
+ bl, p1, p2);
+ return;
+ }
+
+ if ( (run==0) && (para->scblock == bl ) ) {
+ delete_paragraph(fr, para, pnp);
+ } else {
+ delete_run(para, run);
+ }
+}
+
+
+static signed int merge_paragraph_runs(Paragraph *p1, Paragraph *p2)
+{
+ struct text_run *runs_new;
+ int i, spos;
+
+ /* All the runs from p2 get added to p1 */
+ runs_new = realloc(p1->runs,
+ (p1->n_runs+p2->n_runs)*sizeof(struct text_run));
+ if ( runs_new == NULL ) {
+ fprintf(stderr, "Failed to allocate merged runs.\n");
+ return -1;
+ }
+ p1->runs = runs_new;
+
+ spos = p1->n_runs;
+
+ /* The end of the united paragraph should now be the end of the
+ * second one */
+ set_newline_at_end(p1, get_newline_at_end(p2));
+
+ for ( i=0; i<p2->n_runs; i++ ) {
+ p1->runs[p1->n_runs] = p2->runs[i];
+ p1->n_runs++;
+ }
+ free(p2->runs);
+ free(p2);
+
+ return spos;
+}
+
+
+static void merge_paragraphs_by_newpara(struct frame *fr, SCBlock *np)
+{
+ int i;
+ Paragraph *p1;
+ Paragraph *p2;
+
+ for ( i=0; i<fr->n_paras-1; i++ ) {
+ if ( fr->paras[i]->newline_at_end == np ) {
+
+ int j;
+ signed int spos;
+
+ p1 = fr->paras[i];
+ p2 = fr->paras[i+1];
+
+ printf("-------------------------------\n");
+ show_para(p1);
+ printf("---x--------x------------------\n");
+ show_para(p2);
+ spos = merge_paragraph_runs(p1, p2);
+ if ( spos < 0 ) {
+ fprintf(stderr, "Failed to merge paragraphs\n");
+ return;
+ }
+ printf("-------------------------------\n");
+ show_para(p1);
+
+ for ( j=i+1; j<fr->n_paras-1; j++ ) {
+ fr->paras[j] = fr->paras[j+1];
+ }
+ fr->n_paras--;
+
+ return;
+
+ }
+ }
+
+ fprintf(stderr, "Couldn't find paragraphs to merge by newpara\n");
+}
+
+
+static int find_block_inside(SCBlock *needle, SCBlock *bl)
+{
+ if ( needle == bl ) return 1;
+
+ if ( sc_block_child(bl) != NULL ) {
+ if ( find_block_inside(needle, sc_block_child(bl)) ) return 1;
+ }
+
+ if ( sc_block_next(bl) != NULL ) {
+ if ( find_block_inside(needle, sc_block_next(bl)) ) return 1;
+ }
+
+ return 0;
+}
+
+
+/* Return true if "top" either IS "child", or contains "child" somewhere
+ * underneath, even if via a macro expansion */
+static int block_is_under(SCBlock *needle, SCBlock *top)
+{
+ if ( needle == top ) return 1;
+
+ if ( sc_block_child(top) != NULL ) {
+ if ( find_block_inside(needle, sc_block_child(top)) ) return 1;
+ }
+
+ /* Do not look at top->next here */
+
+ return 0;
+}
+
+
+void delete_text_from_frame(struct frame *fr, struct edit_pos p1, struct edit_pos p2,
+ double wrapw)
+{
+ int i;
+ SCBlock *p1scblock, *p2scblock;
+ enum para_type type1, type2;
+ size_t p2offs;
+ SCBlock *scblock;
+ int wrap_end;
+
+ sort_positions(&p1, &p2);
+
+ /* To make sure there are no nasty surprises ahead, run through the
+ * paragraphs we're about to touch, and make sure they all have at least
+ * an empty dummy run */
+ for ( i=p1.para; i<=p2.para; i++ ) {
+ struct edit_pos ep;
+ ep.para = i;
+ ep.pos = 0;
+ ep.trail = 0;
+ ensure_run(fr, ep);
+ }
+
+ if ( !position_editable(fr, p1) || !position_editable(fr, p2) ) {
+ fprintf(stderr, "Delete outside editable region\n");
+ return;
+ }
+
+ /* Find SC positions for start and end */
+ p1scblock = pos_to_scblock(fr, p1, &type1);
+ p2scblock = pos_to_scblock(fr, p2, &type2);
+ p2offs = pos_to_offset(fr, p2);
+ wrap_end = p2.para;
+
+ printf("SCBlocks %p to %p\n", p1scblock, p2scblock);
+ if ( p1scblock == NULL ) {
+ fprintf(stderr, "Starting block NULL. Not deleting.\n");
+ return;
+ }
+ if ( p2scblock == NULL ) {
+ fprintf(stderr, "Ending block NULL. Not deleting.\n");
+ return;
+ }
+ //show_sc_blocks(p1scblock);
+
+ if ( (p1scblock == p2scblock) && (type1 == PARA_TYPE_TEXT) ) {
+
+ size_t p1offs;
+ printf("Simple case, one SCBlock\n");
+
+ assert(type1 == type2);
+
+ /* Remove the text and update the run length */
+ p1offs = pos_to_offset(fr, p1);
+ scblock_delete_text(p1scblock, p1offs, p2offs);
+
+ wrap_paragraph(fr->paras[p1.para], NULL, wrapw, 0, 0);
+
+ return;
+ }
+
+ /* Starting point for iteration over blocks in middle of range.
+ * Record this now, because p1scblock might be about to get deleted */
+ scblock = sc_block_next(p1scblock);
+
+ /* First SCBlock in range: delete whole thing or second half */
+ printf("First block %p\n", p1scblock);
+ if ( type1 == PARA_TYPE_TEXT ) {
+
+ size_t p1offs = pos_to_offset(fr, p1);
+ int p1run = pos_to_run_number(fr, p1);
+ printf(" offs %li\n", (long int)p1offs);
+ if ( p1offs != 0 ) {
+ printf("Partial delete\n");
+ printf("contents '%s'\n", sc_block_contents(p1scblock));
+ printf("from offs %li\n", (long int)p1offs);
+ scblock_delete_text(p1scblock, p1offs, -1);
+ } else {
+ printf("Deleting the whole text SCBlock\n");
+ sc_block_delete(&fr->scblocks, p1scblock);
+ delete_run(fr->paras[p1.para], p1run);
+ }
+
+ } else {
+ printf("Deleting the whole non-text SCBlock\n");
+ sc_block_delete(&fr->scblocks, p1scblock);
+ }
+
+ /* Delete all the complete SCBlocks in the middle of the range */
+ if ( !block_is_under(p2scblock, scblock) ) {
+ do {
+
+ SCBlock *next;
+
+ /* For each SC block in middle of range: */
+ printf("Deleting %p\n", scblock);
+ if ( scblock == NULL ) {
+ fprintf(stderr, "nothing?\n");
+ break;
+ }
+ printf("name is '%s'\n", sc_block_name(scblock));
+
+ if ( (sc_block_name(scblock) != NULL)
+ && (strcmp(sc_block_name(scblock), "newpara") == 0) )
+ {
+ /* Deleting newpara block, merge the paragraphs */
+ merge_paragraphs_by_newpara(fr, scblock);
+ p2.para--;
+ }
+
+ next = sc_block_next(scblock);
+ delete_run_for_scblock(fr, fr->paras[p1.para],
+ fr->paras[p2.para], scblock,
+ &p2.para);
+ sc_block_delete(&fr->scblocks, scblock);
+
+ scblock = next;
+
+ } while ( !block_is_under(p2scblock, scblock) );
+ }
+
+ /* Last SCBlock in range: delete whole thing or first half */
+ printf("Last block %p (%s)\n", p2scblock, sc_block_name(p2scblock));
+ if ( type2 == PARA_TYPE_TEXT ) {
+ size_t len;
+ printf(" offs %li\n", (long int)p2offs);
+ if ( sc_block_contents(p2scblock) != NULL ) {
+ len = strlen(sc_block_contents(p2scblock));
+ } else {
+ len = 0;
+ }
+ printf(" len %li\n", (long int)len);
+ if ( (len > 0) && (p2offs == len) ) {
+ printf("Deleting the whole text SCBlock\n");
+ printf("deleting block %p\n", p2scblock);
+ show_sc_block(p2scblock, "");
+ sc_block_delete(&fr->scblocks, p2scblock);
+ delete_run_for_scblock(fr, fr->paras[p1.para],
+ fr->paras[p2.para], p2scblock,
+ NULL);
+ } else if ( p2offs > 0 ) {
+ printf("Partial delete\n");
+ printf("contents '%s'\n", sc_block_contents(p2scblock));
+ printf("up to offs %li\n", (long int)p2offs);
+ scblock_delete_text(p2scblock, 0, p2offs);
+ } /* else do nothing */
+ } else {
+ printf("Deleting the whole non-text SCBlock\n");
+ sc_block_delete(&fr->scblocks, p2scblock);
+ }
+
+ /* If any paragraphs have been deleted, this will wrap too many
+ * paragraphs, but it doesn't matter as long as we don't wrap
+ * past the end of the frame's contents. */
+ for ( i=p1.para; i<=wrap_end; i++ ) {
+ if ( i >= fr->n_paras ) break;
+ printf("Wrapping para %i (%p)\n", i, fr->paras[i]);
+ wrap_paragraph(fr->paras[i], NULL, wrapw, 0, 0);
+ }
+ printf("All done.\n");
+}
+
+
+void show_para(Paragraph *p)
+{
+ printf("Paragraph %p\n", p);
+
+ if ( p->type == PARA_TYPE_TEXT ) {
+
+ int i;
+
+ printf("%i runs:\n", p->n_runs);
+ for ( i=0; i<p->n_runs; i++ ) {
+ printf(" Run %2i: SCBlock %p %s '%s'\n",
+ i, p->runs[i].scblock,
+ pango_font_description_to_string(p->runs[i].fontdesc),
+ sc_block_contents(p->runs[i].scblock));
+ }
+
+ } else if ( p->type == PARA_TYPE_IMAGE ) {
+ printf(" Image: %s\n", p->filename);
+ } else {
+ printf(" Other paragraph type\n");
+ }
+}
+
+
+static SCBlock *split_text_paragraph(struct frame *fr, int pn, size_t pos,
+ PangoContext *pc)
+{
+ Paragraph *pnew;
+ int i;
+ SCBlock *nnp;
+ size_t run_offs;
+ int run;
+ Paragraph *para = fr->paras[pn];
+ struct text_run *rr;
+
+ pnew = insert_paragraph(fr, pn);
+ if ( pnew == NULL ) {
+ fprintf(stderr, "Failed to insert paragraph\n");
+ return NULL;
+ }
+
+ /* Determine which run the cursor is in */
+ run = which_run(para, pos);
+
+ /* Create the new (second) paragraph */
+ pnew->type = PARA_TYPE_TEXT;
+ pnew->n_runs = 0;
+ pnew->runs = NULL;
+ for ( i=0; i<4; i++ ) pnew->space[i] = para->space[i];
+
+ rr = &para->runs[run];
+ run_offs = pos - get_paragraph_offset(para, run);
+ printf("split at run %i\n", run);
+
+ /* Easy case: splitting at a run boundary */
+ if ( run_offs == run_text_len(rr) ) {
+
+ /* Even easier case: splitting at the end of the paragraph */
+ if ( run == para->n_runs-1 ) {
+
+ SCBlock *np;
+ SCBlock *end;
+
+ printf("Simple new para\n");
+
+ if ( get_newline_at_end(para) == NULL ) {
+
+ /* The current paragraph doesn't have
+ * a \newpara yet */
+
+ np = sc_block_append(rr->scblock,
+ strdup("newpara"), NULL,
+ NULL, NULL);
+ set_newline_at_end(para, np);
+
+ } else {
+
+ SCBlock *op;
+
+ /* If the current paragraph did have \newpara,
+ * then the new one needs one too */
+ np = sc_block_append(rr->scblock,
+ strdup("newpara"),
+ NULL, NULL, NULL);
+ op = get_newline_at_end(para);
+ set_newline_at_end(para, np);
+ set_newline_at_end(pnew, op);
+
+
+ }
+
+ /* Add an empty run + SCBlock to type into */
+ end = sc_block_append(np, NULL, NULL, strdup(""), NULL);
+
+ pnew->n_runs = 0;
+ add_run(pnew, end, rr->fontdesc, rr->col, NULL);
+ pnew->scblock = end;
+
+ wrap_paragraph(pnew, pc, fr->w - fr->pad_l - fr->pad_r, 0, 0);
+
+ return end;
+
+ }
+
+ } else {
+
+ /* Split the run (and SCBlock) into two */
+ double col[4] = {0,0,0,0};
+ struct text_run *rn;
+
+ printf("Splitting run %i. Before:\n", run);
+ show_para(para);
+
+ add_run(para, NULL, NULL, col, NULL);
+ /* -2 here because add_run increased para->n_runs by 1 */
+ memmove(&para->runs[run+2], &para->runs[run+1],
+ (para->n_runs - run - 2)*sizeof(struct text_run));
+
+ rr = &para->runs[run]; /* Because add_run realloced the runs */
+ rn = &para->runs[run+1];
+
+ rn->scblock = sc_block_split(rr->scblock, run_offs);
+
+ rn->fontdesc = pango_font_description_copy(rr->fontdesc);
+ rn->col[0] = rr->col[0];
+ rn->col[1] = rr->col[1];
+ rn->col[2] = rr->col[2];
+ rn->col[3] = rr->col[3];
+
+ printf("After:\n");
+ show_para(para);
+
+ }
+
+ /* All later runs just get moved to the new paragraph */
+ for ( i=run+1; i<para->n_runs; i++ ) {
+ double col[4] = {0,0,0,0};
+ printf("Moving run %i to pos %i\n", i, pnew->n_runs);
+ add_run(pnew, NULL, NULL, col, NULL);
+ pnew->runs[pnew->n_runs-1] = para->runs[i];
+ }
+ pnew->scblock = pnew->runs[0].scblock;
+
+ /* Truncate the first paragraph at the appropriate position */
+ para->n_runs = run+1;
+
+ printf("Final paragraphs:\n");
+ printf("First:\n");
+ show_para(para);
+ printf("Second:\n");
+ show_para(pnew);
+
+ /* Add a \newpara after the end of the first paragraph's SC */
+ nnp = sc_block_append(rr->scblock, strdup("newpara"), NULL, NULL, NULL);
+ set_newline_at_end(pnew, get_newline_at_end(para));
+ set_newline_at_end(para, nnp);
+
+ wrap_paragraph(para, pc, fr->w - fr->pad_l - fr->pad_r, 0, 0);
+ wrap_paragraph(pnew, pc, fr->w - fr->pad_l - fr->pad_r, 0, 0);
+
+ return sc_block_next(nnp);
+}
+
+
+SCBlock *split_paragraph(struct frame *fr, int pn, size_t pos, PangoContext *pc)
+{
+ Paragraph *para = fr->paras[pn];
+
+ if ( para->type == PARA_TYPE_TEXT ) {
+ return split_text_paragraph(fr, pn, pos, pc);
+ } else {
+ /* Other types can't be split */
+ return NULL;
+ }
+}
+
+
+SCBlock *block_at_cursor(struct frame *fr, int pn, size_t pos)
+{
+ Paragraph *para = fr->paras[pn];
+
+ if ( para->type != PARA_TYPE_CALLBACK ) return NULL;
+
+ return para->scblock;
+}
+
+
+int get_sc_pos(struct frame *fr, int pn, size_t pos,
+ SCBlock **bl, size_t *ppos)
+{
+ Paragraph *para = fr->paras[pn];
+ int nrun;
+ struct text_run *run;
+
+ nrun = which_run(para, pos);
+ if ( nrun == para->n_runs ) {
+ fprintf(stderr, "Couldn't find run to insert into.\n");
+ return 1;
+ }
+ run = &para->runs[nrun];
+
+ *ppos = pos - get_paragraph_offset(para, nrun);
+ *bl = run->scblock;
+
+ return 0;
+}
+
+
+void set_para_spacing(Paragraph *para, float space[4])
+{
+ if ( para == NULL ) return;
+ para->space[0] = space[0];
+ para->space[1] = space[1];
+ para->space[2] = space[2];
+ para->space[3] = space[3];
+}
+
+
+void set_para_alignment(Paragraph *para, PangoAlignment align)
+{
+ if ( para == NULL ) return;
+ para->alignment = align;
+}
+
+
+void *get_para_bvp(Paragraph *para)
+{
+ if ( para->type != PARA_TYPE_CALLBACK ) return NULL;
+ return para->bvp;
+}
+
+
+SCBlock *para_scblock(Paragraph *para)
+{
+ return para->scblock;
+}
+
+
+enum para_type para_type(Paragraph *para)
+{
+ return para->type;
+}
+
+
+int para_debug_num_runs(Paragraph *para)
+{
+ if ( para->type != PARA_TYPE_TEXT ) return 0;
+ return para->n_runs;
+}
+
+
+int para_debug_run_info(Paragraph *para, int i, SCBlock **scblock)
+{
+ if ( para->type != PARA_TYPE_TEXT ) return 1;
+ if ( i >= para->n_runs ) return 1;
+
+ *scblock = para->runs[i].scblock;
+ return 0;
+}
diff --git a/src-old/frame.h b/src-old/frame.h
new file mode 100644
index 0000000..d0525f8
--- /dev/null
+++ b/src-old/frame.h
@@ -0,0 +1,194 @@
+/*
+ * frame.h
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef FRAME_H
+#define FRAME_H
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <pango/pango.h>
+#include <cairo.h>
+
+
+typedef enum
+{
+ GRAD_NONE,
+ GRAD_HORIZ,
+ GRAD_VERT
+} GradientType;
+
+enum para_type
+{
+ PARA_TYPE_TEXT,
+ PARA_TYPE_IMAGE,
+ PARA_TYPE_CALLBACK
+};
+
+typedef struct _paragraph Paragraph;
+
+#include "sc_parse.h"
+#include "sc_interp.h"
+#include "imagestore.h"
+
+
+struct edit_pos
+{
+ int para; /* Paragraph number */
+ size_t pos; /* Byte position within paragraph
+ * Yes, really. See pango_layout_xy_to_index */
+ int trail;
+};
+
+
+struct frame
+{
+ struct frame **children;
+ int num_children;
+ int max_children;
+
+ SCBlock *scblocks;
+
+ Paragraph **paras;
+ int n_paras;
+ int max_paras;
+
+ /* The font which will be used by default for this frame */
+ PangoFontDescription *fontdesc;
+ double col[4];
+
+ /* The rectangle allocated to this frame, determined by the renderer */
+ double x;
+ double y;
+ double w;
+ double h;
+
+ double pad_t;
+ double pad_b;
+ double pad_l;
+ double pad_r;
+
+ /* Background properties for this frame */
+ double bgcol[4];
+ double bgcol2[4];
+ GradientType grad;
+
+ /* True if this frame should be deleted on the next mouse click */
+ int empty;
+
+ /* True if the aspect ratio of this frame should be maintained */
+ int is_image;
+
+ /* True if this frame can be resized and moved */
+ int resizable;
+};
+
+
+extern struct frame *frame_new(void);
+extern void frame_free(struct frame *fr);
+extern struct frame *add_subframe(struct frame *fr);
+extern struct frame *find_frame_with_scblocks(struct frame *top,
+ SCBlock *scblocks);
+
+extern double total_height(struct frame *fr);
+
+extern Paragraph *last_para(struct frame *fr);
+extern void show_para(Paragraph *p);
+extern void set_para_spacing(Paragraph *para, float space[4]);
+extern void set_para_alignment(Paragraph *para, PangoAlignment align);
+
+extern double paragraph_height(Paragraph *para);
+extern void render_paragraph(cairo_t *cr, Paragraph *para, ImageStore *is);
+
+extern void set_newline_at_end(Paragraph *para, SCBlock *bl);
+extern void show_edit_pos(struct edit_pos a);
+
+extern void add_run(Paragraph *para, SCBlock *scblock,
+ PangoFontDescription *fdesc, double col[4],
+ const char *real_text);
+
+extern Paragraph *insert_paragraph(struct frame *fr, int pos);
+
+extern Paragraph *add_callback_para(struct frame *fr, SCBlock *scblock,
+ double w, double h,
+ SCCallbackDrawFunc draw_func,
+ SCCallbackClickFunc click_func, void *bvp,
+ void *vp);
+
+extern void add_image_para(struct frame *fr, SCBlock *scblock,
+ const char *filename,
+ ImageStore *is, double w, double h, int editable);
+
+extern void wrap_paragraph(Paragraph *para, PangoContext *pc, double w,
+ size_t sel_start, size_t sel_end);
+
+extern int find_cursor(struct frame *fr, double x, double y,
+ struct edit_pos *pos);
+
+extern void sort_positions(struct edit_pos *a, struct edit_pos *b);
+
+extern void ensure_run(struct frame *fr, struct edit_pos cpos);
+
+extern int positions_equal(struct edit_pos a, struct edit_pos b);
+
+extern int get_para_highlight(struct frame *fr, int cursor_para,
+ double *cx, double *cy, double *cw, double *ch);
+
+extern int position_editable(struct frame *fr, struct edit_pos cp);
+
+extern int get_cursor_pos(struct frame *fr, int cursor_para, int cursor_pos,
+ double *cx, double *cy, double *ch);
+
+extern void cursor_moveh(struct frame *fr, struct edit_pos *cp, signed int dir);
+
+extern void check_callback_click(struct frame *fr, int para);
+
+extern size_t pos_trail_to_offset(Paragraph *para, size_t offs, int trail);
+
+extern void insert_text_in_paragraph(Paragraph *para, size_t offs,
+ const char *t);
+
+extern void delete_text_from_frame(struct frame *fr, struct edit_pos p1, struct edit_pos p2,
+ double wrap_w);
+
+extern SCBlock *split_paragraph(struct frame *fr, int pn, size_t pos,
+ PangoContext *pc);
+extern SCBlock *block_at_cursor(struct frame *fr, int para, size_t pos);
+
+extern void show_frame_hierarchy(struct frame *fr, const char *t);
+
+extern int get_sc_pos(struct frame *fr, int pn, size_t pos,
+ SCBlock **bl, size_t *ppos);
+
+extern void *get_para_bvp(Paragraph *para);
+
+extern Paragraph *create_paragraph(struct frame *fr, SCBlock *bl);
+
+extern enum para_type para_type(Paragraph *para);
+extern SCBlock *para_scblock(Paragraph *para);
+
+extern SCBlock *para_debug_get_newline_at_end(Paragraph *para);
+extern int para_debug_num_runs(Paragraph *para);
+extern int para_debug_run_info(Paragraph *para, int i, SCBlock **scblock);
+
+#endif /* FRAME_H */
diff --git a/src-old/imagestore.c b/src-old/imagestore.c
new file mode 100644
index 0000000..61ebcfb
--- /dev/null
+++ b/src-old/imagestore.c
@@ -0,0 +1,369 @@
+/*
+ * imagestore.c
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <libgen.h>
+#include <cairo.h>
+#include <glib.h>
+#include <gio/gio.h>
+#include <gdk/gdk.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+
+#include "imagestore.h"
+#include "utils.h"
+
+#define MAX_SIZES (32)
+
+struct image_record
+{
+ char *filename;
+ cairo_surface_t *surf[MAX_SIZES];
+ int w[MAX_SIZES];
+ int last_used[MAX_SIZES];
+ int n_sizes;
+ int h_last_used;
+ int full_width;
+ int full_height;
+};
+
+
+struct _imagestore
+{
+ int n_images;
+ struct image_record *images;
+ int max_images;
+ GFile *pparent;
+ GFile *iparent;
+ const char *storename;
+};
+
+
+static int alloc_images(ImageStore *is, int new_max_images)
+{
+ struct image_record *images_new;
+
+ images_new = realloc(is->images,
+ sizeof(struct image_record)*new_max_images);
+ if ( images_new == NULL ) return 1;
+
+ is->images = images_new;
+ is->max_images = new_max_images;
+ return 0;
+}
+
+
+ImageStore *imagestore_new(const char *storename)
+{
+ ImageStore *is;
+
+ is = calloc(1, sizeof(ImageStore));
+ if ( is == NULL ) return NULL;
+
+ is->images = NULL;
+ is->pparent = NULL;
+ if ( storename != NULL ) {
+ is->iparent = g_file_new_for_uri(storename);
+ } else {
+ is->iparent = NULL;
+ }
+ is->n_images = 0;
+ is->max_images = 0;
+ if ( alloc_images(is, 32) ) {
+ free(is);
+ return NULL;
+ }
+
+ return is;
+}
+
+
+void imagestore_set_parent(ImageStore *is, GFile *parent)
+{
+ if ( is->pparent != NULL ) {
+ g_object_unref(is->pparent);
+ }
+ is->pparent = parent;
+}
+
+
+void imagestore_destroy(ImageStore *is)
+{
+ int i;
+
+ for ( i=0; i<is->n_images; i++ ) {
+ int j;
+ free(is->images[i].filename);
+ for ( j=0; j<is->images[i].n_sizes; j++ ) {
+ if ( is->images[i].surf[j] != NULL ) {
+ g_object_unref(is->images[i].surf[j]);
+ }
+ }
+ }
+ g_object_unref(is->pparent);
+ g_object_unref(is->iparent);
+ free(is->images);
+ free(is);
+}
+
+
+static cairo_surface_t *pixbuf_to_surface(GdkPixbuf *t)
+{
+ cairo_surface_t *surf;
+ cairo_t *cr;
+ int w, h;
+
+ if ( t == NULL ) return NULL;
+
+ w = gdk_pixbuf_get_width(t);
+ h = gdk_pixbuf_get_height(t);
+
+ surf = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
+ if ( surf == NULL ) return NULL;
+
+ cr = cairo_create(surf);
+
+ gdk_cairo_set_source_pixbuf(cr, t, 0, 0);
+ cairo_pattern_t *patt = cairo_get_source(cr);
+ cairo_pattern_set_extend(patt, CAIRO_EXTEND_PAD);
+ cairo_pattern_set_filter(patt, CAIRO_FILTER_BEST);
+ cairo_paint(cr);
+
+ cairo_destroy(cr);
+
+ return surf;
+}
+
+
+static int try_load(GFile *file, GdkPixbuf **pixbuf, gint w, gint h)
+{
+ GFileInputStream *stream;
+ GError *error = NULL;
+
+ stream = g_file_read(file, NULL, &error);
+ if ( stream != NULL ) {
+ GError *pberr = NULL;
+ *pixbuf = gdk_pixbuf_new_from_stream_at_scale(G_INPUT_STREAM(stream),
+ w, h, TRUE,
+ NULL, &pberr);
+ g_object_unref(stream);
+ g_object_unref(file);
+ return 1;
+ }
+
+ return 0;
+}
+
+
+static GdkPixbuf *load_image(const char *uri, GFile *pparent, GFile *iparent,
+ gint w, gint h)
+{
+ GFile *file;
+ GdkPixbuf *pixbuf;
+
+ /* Literal pathname */
+ file = g_file_new_for_path(uri);
+ if ( try_load(file, &pixbuf, w, h) ) return pixbuf;
+
+ /* Try the file prefixed with the directory the presentation is in */
+ if ( pparent != NULL ) {
+ file = g_file_get_child(pparent, uri);
+ if ( try_load(file, &pixbuf, w, h) ) return pixbuf;
+ }
+
+ /* Try prefixing with imagestore folder */
+ if ( iparent != NULL ) {
+ file = g_file_get_child(iparent, uri);
+ if ( try_load(file, &pixbuf, w, h) ) return pixbuf;
+ }
+
+ return NULL;
+}
+
+
+static struct image_record *add_image_record(ImageStore *is,
+ const char *filename)
+{
+ int idx;
+
+ if ( is->n_images == is->max_images ) {
+ if ( alloc_images(is, is->max_images+32) ) {
+ fprintf(stderr, "Couldn't allocate memory for image "
+ "records.\n");
+ return NULL;
+ }
+ }
+
+ idx = is->n_images++;
+
+ is->images[idx].n_sizes = 0;
+ is->images[idx].filename = strdup(filename);
+ is->images[idx].h_last_used = 0;
+
+ return &is->images[idx];
+}
+
+
+
+int imagestore_get_size(ImageStore *is, const char *filename,
+ int *w, int *h)
+{
+ GdkPixbuf *pixbuf;
+ struct image_record *ir;
+ int i;
+
+ for ( i=0; i<is->n_images; i++ ) {
+ if ( strcmp(is->images[i].filename, filename) == 0 ) {
+ *w = is->images[i].full_width;
+ *h = is->images[i].full_height;
+ return 0;
+ }
+ }
+
+ pixbuf = load_image(filename, is->pparent, is->iparent, -1, -1);
+ if ( pixbuf == NULL ) return 1;
+
+
+ *w = gdk_pixbuf_get_width(pixbuf);
+ *h = gdk_pixbuf_get_height(pixbuf);
+
+ ir = add_image_record(is, filename);
+ if ( ir != NULL ) {
+ ir->full_width = *w;
+ ir->full_height = *h;
+ } /* otherwise can't cache, too bad */
+
+ g_object_unref(pixbuf);
+
+ return 0;
+}
+
+
+static int find_earliest(struct image_record *im)
+{
+ int i, earliest, l;
+
+ earliest = im->h_last_used;
+ l = im->n_sizes;
+
+ for ( i=0; i<im->n_sizes; i++ ) {
+ if ( im->last_used[i] < earliest ) {
+ earliest = im->last_used[i];
+ l = i;
+ }
+ }
+
+ assert(l != im->n_sizes);
+
+ cairo_surface_destroy(im->surf[l]);
+
+ return l;
+}
+
+
+static cairo_surface_t *add_image_size(struct image_record *im,
+ const char *filename,
+ GFile *pparent, GFile *iparent,
+ int w)
+{
+ cairo_surface_t *surf;
+ GdkPixbuf *t;
+ int pos;
+
+ t = load_image(filename, pparent, iparent, w, -1);
+ if ( t == NULL ) return NULL;
+ surf = pixbuf_to_surface(t);
+ g_object_unref(t);
+
+ /* Add surface to list */
+ if ( im->n_sizes == MAX_SIZES ) {
+ pos = find_earliest(im);
+ } else {
+ pos = im->n_sizes++;
+ }
+
+ im->w[pos] = w;
+ im->surf[pos] = surf;
+ im->last_used[pos] = im->h_last_used++;
+
+ return surf;
+}
+
+
+static cairo_surface_t *add_new_image(ImageStore *is, const char *filename, int w)
+{
+ struct image_record *ir;
+ ir = add_image_record(is, filename);
+ if ( ir == NULL ) return NULL;
+ return add_image_size(ir, filename, is->pparent, is->iparent, w);
+}
+
+
+void show_imagestore(ImageStore *is)
+{
+ int i;
+
+ printf("Store %p contains %i records.\n", is, is->n_images);
+
+ for ( i=0; i<is->n_images; i++ ) {
+
+ printf("%s :\n", is->images[i].filename);
+ printf("\n");
+
+ }
+}
+
+
+cairo_surface_t *lookup_image(ImageStore *is, const char *filename, int w)
+{
+ int i, j;
+ int found = 0;
+ cairo_surface_t *surf;
+
+ for ( i=0; i<is->n_images; i++ ) {
+ if ( strcmp(is->images[i].filename, filename) == 0 ) {
+ found = 1;
+ break;
+ }
+ }
+ if ( !found ) {
+ return add_new_image(is, filename, w);
+ }
+
+ for ( j=0; j<is->images[i].n_sizes; j++ ) {
+ if ( is->images[i].w[j] == w ) {
+ is->images[i].last_used[j] = is->images[i].h_last_used++;
+ return is->images[i].surf[j];
+ }
+ }
+
+ /* We don't have this size yet */
+ surf = add_image_size(&is->images[i], filename, is->pparent,
+ is->iparent, w);
+ return surf;
+}
diff --git a/src-old/imagestore.h b/src-old/imagestore.h
new file mode 100644
index 0000000..0d4df24
--- /dev/null
+++ b/src-old/imagestore.h
@@ -0,0 +1,43 @@
+/*
+ * imagestore.h
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef IMAGESTORE_H
+#define IMAGESTORE_H
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <cairo.h>
+#include <gio/gio.h>
+
+typedef struct _imagestore ImageStore;
+
+extern ImageStore *imagestore_new(const char *storename);
+extern void imagestore_destroy(ImageStore *is);
+extern void imagestore_set_parent(ImageStore *is, GFile *parent);
+extern void show_imagestore(ImageStore *is);
+
+extern cairo_surface_t *lookup_image(ImageStore *is, const char *uri, int w);
+extern int imagestore_get_size(ImageStore *is, const char *uri, int *w, int *h);
+
+#endif /* IMAGESTORE_H */
diff --git a/src-old/narrative_window.c b/src-old/narrative_window.c
new file mode 100644
index 0000000..e0c59ef
--- /dev/null
+++ b/src-old/narrative_window.c
@@ -0,0 +1,927 @@
+/*
+ * narrative_window.c
+ *
+ * Copyright © 2014-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <gtk/gtk.h>
+#include <assert.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include "colloquium.h"
+#include "presentation.h"
+#include "narrative_window.h"
+#include "sc_editor.h"
+#include "sc_parse.h"
+#include "render.h"
+#include "testcard.h"
+#include "pr_clock.h"
+#include "print.h"
+#include "utils.h"
+#include "stylesheet_editor.h"
+
+
+struct _narrative_window
+{
+ GtkWidget *window;
+ GtkToolItem *bfirst;
+ GtkToolItem *bprev;
+ GtkToolItem *bnext;
+ GtkToolItem *blast;
+ SCEditor *sceditor;
+ GApplication *app;
+ struct presentation *p;
+ SCBlock *dummy_top;
+ SCSlideshow *show;
+ int show_no_slides;
+ PRClock *pr_clock;
+ SlideWindow *slidewindows[16];
+ int n_slidewindows;
+};
+
+
+static void show_error(NarrativeWindow *nw, const char *err)
+{
+ GtkWidget *mw;
+
+ mw = gtk_message_dialog_new(GTK_WINDOW(nw->window),
+ GTK_DIALOG_DESTROY_WITH_PARENT,
+ GTK_MESSAGE_ERROR,
+ GTK_BUTTONS_CLOSE, "%s", err);
+
+ g_signal_connect_swapped(mw, "response",
+ G_CALLBACK(gtk_widget_destroy), mw);
+
+ gtk_widget_show(mw);
+}
+
+
+static void update_toolbar(NarrativeWindow *nw)
+{
+ int cur_para;
+
+ cur_para = sc_editor_get_cursor_para(nw->sceditor);
+ if ( cur_para == 0 ) {
+ gtk_widget_set_sensitive(GTK_WIDGET(nw->bfirst), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(nw->bprev), FALSE);
+ } else {
+ gtk_widget_set_sensitive(GTK_WIDGET(nw->bfirst), TRUE);
+ gtk_widget_set_sensitive(GTK_WIDGET(nw->bprev), TRUE);
+ }
+
+ if ( cur_para == sc_editor_get_num_paras(nw->sceditor)-1 ) {
+ gtk_widget_set_sensitive(GTK_WIDGET(nw->bnext), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(nw->blast), FALSE);
+ } else {
+ gtk_widget_set_sensitive(GTK_WIDGET(nw->bnext), TRUE);
+ gtk_widget_set_sensitive(GTK_WIDGET(nw->blast), TRUE);
+ }
+}
+
+
+struct saveas_info
+{
+ NarrativeWindow *nw;
+
+ /* Radio buttons for how to save stylesheet */
+ GtkWidget *privatess;
+ GtkWidget *folderss;
+ GtkWidget *noss;
+};
+
+
+static gint saveas_response_sig(GtkWidget *d, gint response,
+ struct saveas_info *si)
+{
+ if ( response == GTK_RESPONSE_ACCEPT ) {
+
+ GFile *file = gtk_file_chooser_get_file(GTK_FILE_CHOOSER(d));
+ GFile *ssfile = NULL;
+
+ if ( gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(si->privatess)) ) {
+ gchar *ssuri;
+ ssuri = g_file_get_uri(file);
+ if ( ssuri != NULL ) {
+ size_t l = strlen(ssuri);
+ if ( ssuri[l-3] == '.' && ssuri[l-2] == 's' && ssuri[l-1] =='c' ) {
+ ssuri[l-1] = 's';
+ ssfile = g_file_new_for_uri(ssuri);
+ g_free(ssuri);
+ }
+ }
+ } else if ( gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(si->folderss)) ) {
+ GFile *parent;
+ parent = g_file_get_parent(file);
+ if ( parent != NULL ) {
+ ssfile = g_file_get_child(parent, "stylesheet.ss");
+ g_object_unref(parent);
+ }
+ } else if ( gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(si->noss)) ) {
+ /* Do nothing */
+ } else {
+ fprintf(stderr, "Couldn't determine where to save stylesheet!\n");
+ }
+
+ if ( save_presentation(si->nw->p, file, ssfile) ) {
+ show_error(si->nw, _("Failed to save presentation"));
+ }
+
+ /* save_presentation keeps a reference to both of these */
+ g_object_unref(file);
+ if ( ssfile != NULL ) g_object_unref(ssfile);
+
+ }
+
+ gtk_widget_destroy(d);
+ free(si);
+
+ return 0;
+}
+
+
+static void saveas_sig(GSimpleAction *action, GVariant *parameter, gpointer vp)
+{
+ GtkWidget *d;
+ GtkWidget *box;
+ NarrativeWindow *nw = vp;
+ struct saveas_info *si;
+
+ si = malloc(sizeof(struct saveas_info));
+ if ( si == NULL ) return;
+
+ si->nw = nw;
+
+ d = gtk_file_chooser_dialog_new(_("Save presentation"),
+ GTK_WINDOW(nw->window),
+ GTK_FILE_CHOOSER_ACTION_SAVE,
+ _("_Cancel"), GTK_RESPONSE_CANCEL,
+ _("_Save"), GTK_RESPONSE_ACCEPT,
+ NULL);
+ gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(d),
+ TRUE);
+
+ box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
+ gtk_file_chooser_set_extra_widget(GTK_FILE_CHOOSER(d), box);
+ si->privatess = gtk_radio_button_new_with_label(NULL,
+ _("Create/update private stylesheet for this presentation"));
+ gtk_box_pack_start(GTK_BOX(box), si->privatess, FALSE, FALSE, 0);
+ si->folderss = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(si->privatess),
+ _("Create/update default stylesheet for presentations in folder"));
+ gtk_box_pack_start(GTK_BOX(box), si->folderss, FALSE, FALSE, 0);
+ si->noss = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(si->privatess),
+ _("Don't save stylesheet at all"));
+ gtk_box_pack_start(GTK_BOX(box), si->noss, FALSE, FALSE, 0);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(si->privatess), TRUE);
+
+ g_signal_connect(G_OBJECT(d), "response",
+ G_CALLBACK(saveas_response_sig), si);
+
+ gtk_widget_show_all(d);
+}
+
+
+static void about_sig(GSimpleAction *action, GVariant *parameter, gpointer vp)
+{
+ NarrativeWindow *nw = vp;
+ open_about_dialog(nw->window);
+}
+
+
+static void save_sig(GSimpleAction *action, GVariant *parameter, gpointer vp)
+{
+ NarrativeWindow *nw = vp;
+
+ if ( nw->p->file == NULL ) {
+ return saveas_sig(NULL, NULL, nw);
+ }
+
+ save_presentation(nw->p, nw->p->file, nw->p->stylesheet_from);
+}
+
+
+static void delete_slide_sig(GSimpleAction *action, GVariant *parameter,
+ gpointer vp)
+{
+ SCBlock *ns;
+ NarrativeWindow *nw = vp;
+
+ /* Get the SCBlock corresponding to the slide */
+ ns = sc_editor_get_cursor_bvp(nw->sceditor);
+ if ( ns == NULL ) {
+ fprintf(stderr, "Not a slide!\n");
+ return;
+ }
+
+ sc_block_delete(&nw->dummy_top, ns);
+
+ /* Full rerender */
+ sc_editor_set_scblock(nw->sceditor, nw->dummy_top);
+ nw->p->saved = 0;
+ update_titlebar(nw);
+}
+
+
+static gint load_ss_response_sig(GtkWidget *d, gint response,
+ NarrativeWindow *nw)
+{
+ if ( response == GTK_RESPONSE_ACCEPT ) {
+
+ GFile *file;
+ Stylesheet *new_ss;
+
+ file = gtk_file_chooser_get_file(GTK_FILE_CHOOSER(d));
+
+ new_ss = stylesheet_load(file);
+ if ( new_ss != NULL ) {
+
+ stylesheet_free(nw->p->stylesheet);
+ nw->p->stylesheet = new_ss;
+ sc_editor_set_stylesheet(nw->sceditor, new_ss);
+
+ /* Full rerender */
+ sc_editor_set_scblock(nw->sceditor, nw->dummy_top);
+
+ } else {
+ fprintf(stderr, _("Failed to load stylesheet\n"));
+ }
+
+ g_object_unref(file);
+
+ }
+
+ gtk_widget_destroy(d);
+
+ return 0;
+}
+
+
+static void stylesheet_changed_sig(GtkWidget *da, NarrativeWindow *nw)
+{
+ int i;
+
+ /* It might have changed (been created) since last time */
+ sc_editor_set_stylesheet(nw->sceditor, nw->p->stylesheet);
+
+ /* Full rerender, first block may have changed */
+ sc_editor_set_scblock(nw->sceditor, nw->dummy_top);
+
+ /* Full rerender of all slide windows */
+ for ( i=0; i<nw->n_slidewindows; i++ ) {
+ slide_window_update(nw->slidewindows[i]);
+ }
+}
+
+
+static void edit_ss_sig(GSimpleAction *action, GVariant *parameter,
+ gpointer vp)
+{
+ NarrativeWindow *nw = vp;
+ StylesheetEditor *se;
+
+ se = stylesheet_editor_new(nw->p);
+ gtk_window_set_transient_for(GTK_WINDOW(se), GTK_WINDOW(nw->window));
+ g_signal_connect(G_OBJECT(se), "changed",
+ G_CALLBACK(stylesheet_changed_sig), nw);
+ gtk_widget_show_all(GTK_WIDGET(se));
+}
+
+
+static void load_ss_sig(GSimpleAction *action, GVariant *parameter,
+ gpointer vp)
+{
+ //SCBlock *nsblock;
+ //SCBlock *templ;
+ NarrativeWindow *nw = vp;
+ GtkWidget *d;
+
+ d = gtk_file_chooser_dialog_new(_("Load stylesheet"),
+ GTK_WINDOW(nw->window),
+ GTK_FILE_CHOOSER_ACTION_OPEN,
+ _("_Cancel"), GTK_RESPONSE_CANCEL,
+ _("_Open"), GTK_RESPONSE_ACCEPT,
+ NULL);
+
+ g_signal_connect(G_OBJECT(d), "response",
+ G_CALLBACK(load_ss_response_sig), nw);
+
+ gtk_widget_show_all(d);
+}
+
+
+static void add_slide_sig(GSimpleAction *action, GVariant *parameter,
+ gpointer vp)
+{
+ SCBlock *nsblock;
+ SCBlock *templ;
+ NarrativeWindow *nw = vp;
+
+ sc_editor_ensure_cursor(nw->sceditor);
+
+ /* Split the current paragraph */
+ nsblock = split_paragraph_at_cursor(nw->sceditor);
+
+ /* FIXME: Template from JSON */
+ templ = sc_parse("\\slide{}");
+
+ /* Link the new SCBlock in */
+ if ( nsblock != NULL ) {
+ sc_block_append_p(nsblock, templ);
+ } else {
+ fprintf(stderr, "Failed to split paragraph\n");
+ }
+
+ sc_editor_set_scblock(nw->sceditor, nw->dummy_top);
+ nw->p->saved = 0;
+ update_titlebar(nw);
+}
+
+
+static void first_para_sig(GSimpleAction *action, GVariant *parameter,
+ gpointer vp)
+{
+ NarrativeWindow *nw = vp;
+ sc_editor_set_cursor_para(nw->sceditor, 0);
+ pr_clock_set_pos(nw->pr_clock, sc_editor_get_cursor_para(nw->sceditor),
+ sc_editor_get_num_paras(nw->sceditor));
+ update_toolbar(nw);
+}
+
+
+static void ss_prev_para(SCSlideshow *ss, void *vp)
+{
+ NarrativeWindow *nw = vp;
+ if ( sc_editor_get_cursor_para(nw->sceditor) == 0 ) return;
+ sc_editor_set_cursor_para(nw->sceditor,
+ sc_editor_get_cursor_para(nw->sceditor)-1);
+ pr_clock_set_pos(nw->pr_clock, sc_editor_get_cursor_para(nw->sceditor),
+ sc_editor_get_num_paras(nw->sceditor));
+ update_toolbar(nw);
+}
+
+
+static void prev_para_sig(GSimpleAction *action, GVariant *parameter,
+ gpointer vp)
+{
+ NarrativeWindow *nw = vp;
+ ss_prev_para(nw->show, nw);
+}
+
+
+static void ss_next_para(SCSlideshow *ss, void *vp)
+{
+ NarrativeWindow *nw = vp;
+ SCBlock *ns;
+
+ sc_editor_set_cursor_para(nw->sceditor,
+ sc_editor_get_cursor_para(nw->sceditor)+1);
+
+ /* If we only have one monitor, don't try to do paragraph counting */
+ if ( ss->single_monitor && !nw->show_no_slides ) {
+ int i, max;
+ max = sc_editor_get_num_paras(nw->sceditor);
+ for ( i=sc_editor_get_cursor_para(nw->sceditor); i<max; i++ ) {
+ SCBlock *ns;
+ sc_editor_set_cursor_para(nw->sceditor, i);
+ ns = sc_editor_get_cursor_bvp(nw->sceditor);
+ if ( ns != NULL ) break;
+ }
+ }
+
+ pr_clock_set_pos(nw->pr_clock, sc_editor_get_cursor_para(nw->sceditor),
+ sc_editor_get_num_paras(nw->sceditor));
+ ns = sc_editor_get_cursor_bvp(nw->sceditor);
+ if ( ns != NULL ) {
+ sc_slideshow_set_slide(nw->show, ns);
+ }
+ update_toolbar(nw);
+}
+
+
+static void next_para_sig(GSimpleAction *action, GVariant *parameter,
+ gpointer vp)
+{
+ NarrativeWindow *nw = vp;
+ ss_next_para(nw->show, nw);
+}
+
+
+static void last_para_sig(GSimpleAction *action, GVariant *parameter,
+ gpointer vp)
+{
+ NarrativeWindow *nw = vp;
+ sc_editor_set_cursor_para(nw->sceditor, -1);
+ pr_clock_set_pos(nw->pr_clock, sc_editor_get_cursor_para(nw->sceditor),
+ sc_editor_get_num_paras(nw->sceditor));
+ update_toolbar(nw);
+}
+
+
+static void open_clock_sig(GSimpleAction *action, GVariant *parameter, gpointer vp)
+{
+ NarrativeWindow *nw = vp;
+ nw->pr_clock = pr_clock_new();
+}
+
+
+static void testcard_sig(GSimpleAction *action, GVariant *parameter,
+ gpointer vp)
+{
+ NarrativeWindow *nw = vp;
+ show_testcard(nw->p);
+}
+
+
+static gint export_pdf_response_sig(GtkWidget *d, gint response,
+ struct presentation *p)
+{
+ if ( response == GTK_RESPONSE_ACCEPT ) {
+ char *filename;
+ filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(d));
+ export_pdf(p, filename);
+ g_free(filename);
+ }
+
+ gtk_widget_destroy(d);
+
+ return 0;
+}
+
+
+static void print_sig(GSimpleAction *action, GVariant *parameter, gpointer vp)
+{
+ NarrativeWindow *nw = vp;
+ run_printing(nw->p, nw->window);
+}
+
+
+static void exportpdf_sig(GSimpleAction *action, GVariant *parameter,
+ gpointer vp)
+{
+ NarrativeWindow *nw = vp;
+ GtkWidget *d;
+
+ d = gtk_file_chooser_dialog_new(_("Export PDF"),
+ NULL,
+ GTK_FILE_CHOOSER_ACTION_SAVE,
+ _("_Cancel"), GTK_RESPONSE_CANCEL,
+ _("_Export"), GTK_RESPONSE_ACCEPT,
+ NULL);
+ gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(d),
+ TRUE);
+
+ g_signal_connect(G_OBJECT(d), "response",
+ G_CALLBACK(export_pdf_response_sig), nw->p);
+
+ gtk_widget_show_all(d);
+}
+
+
+
+static gboolean nw_button_press_sig(GtkWidget *da, GdkEventButton *event,
+ NarrativeWindow *nw)
+{
+ return 0;
+}
+
+
+static void changed_sig(GtkWidget *da, NarrativeWindow *nw)
+{
+ nw->p->saved = 0;
+ update_titlebar(nw);
+}
+
+
+static void scroll_down(NarrativeWindow *nw)
+{
+ gdouble inc, val;
+ GtkAdjustment *vadj;
+
+ vadj = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(nw->sceditor));
+ inc = gtk_adjustment_get_step_increment(GTK_ADJUSTMENT(vadj));
+ val = gtk_adjustment_get_value(GTK_ADJUSTMENT(vadj));
+ gtk_adjustment_set_value(GTK_ADJUSTMENT(vadj), inc+val);
+}
+
+
+static gboolean nw_destroy_sig(GtkWidget *da, NarrativeWindow *nw)
+{
+ g_application_release(nw->app);
+ return FALSE;
+}
+
+
+static gboolean nw_key_press_sig(GtkWidget *da, GdkEventKey *event,
+ NarrativeWindow *nw)
+{
+ switch ( event->keyval ) {
+
+ case GDK_KEY_B :
+ case GDK_KEY_b :
+ if ( nw->show != NULL ) {
+ scroll_down(nw);
+ return TRUE;
+ }
+ break;
+
+ case GDK_KEY_Page_Up :
+ if ( nw->show != NULL ) {
+ ss_prev_para(nw->show, nw);
+ return TRUE;
+ }
+ break;
+
+ case GDK_KEY_Page_Down :
+ if ( nw->show != NULL) {
+ ss_next_para(nw->show, nw);
+ return TRUE;
+ }
+ break;
+
+ case GDK_KEY_Escape :
+ if ( nw->show != NULL ) {
+ gtk_widget_destroy(GTK_WIDGET(nw->show));
+ return TRUE;
+ }
+ break;
+
+ case GDK_KEY_F5 :
+ if ( nw->show != NULL ) {
+ /* Trap F5 so that full rerender does NOT happen */
+ return TRUE;
+ }
+
+ }
+
+ return FALSE;
+}
+
+
+static gboolean ss_destroy_sig(GtkWidget *da, NarrativeWindow *nw)
+{
+ nw->show = NULL;
+ sc_editor_set_para_highlight(nw->sceditor, 0);
+
+ gtk_widget_set_sensitive(GTK_WIDGET(nw->bfirst), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(nw->bprev), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(nw->bnext), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(nw->blast), FALSE);
+
+ return FALSE;
+}
+
+
+static void start_slideshow_here_sig(GSimpleAction *action, GVariant *parameter,
+ gpointer vp)
+{
+ NarrativeWindow *nw = vp;
+ void *bvp;
+
+ if ( num_slides(nw->p) == 0 ) return;
+
+ bvp = sc_editor_get_cursor_bvp(nw->sceditor);
+ if ( bvp == NULL ) return;
+
+ nw->show = sc_slideshow_new(nw->p, GTK_APPLICATION(nw->app));
+ if ( nw->show == NULL ) return;
+
+ nw->show_no_slides = 0;
+
+ g_signal_connect(G_OBJECT(nw->show), "key-press-event",
+ G_CALLBACK(nw_key_press_sig), nw);
+ g_signal_connect(G_OBJECT(nw->show), "destroy",
+ G_CALLBACK(ss_destroy_sig), nw);
+ sc_slideshow_set_slide(nw->show, bvp);
+ sc_editor_set_para_highlight(nw->sceditor, 1);
+ gtk_widget_show_all(GTK_WIDGET(nw->show));
+ update_toolbar(nw);
+}
+
+
+static void start_slideshow_noslides_sig(GSimpleAction *action, GVariant *parameter,
+ gpointer vp)
+{
+ NarrativeWindow *nw = vp;
+
+ if ( num_slides(nw->p) == 0 ) return;
+
+ nw->show = sc_slideshow_new(nw->p, GTK_APPLICATION(nw->app));
+ if ( nw->show == NULL ) return;
+
+ nw->show_no_slides = 1;
+
+ g_signal_connect(G_OBJECT(nw->show), "key-press-event",
+ G_CALLBACK(nw_key_press_sig), nw);
+ g_signal_connect(G_OBJECT(nw->show), "destroy",
+ G_CALLBACK(ss_destroy_sig), nw);
+ sc_slideshow_set_slide(nw->show, first_slide(nw->p));
+ sc_editor_set_para_highlight(nw->sceditor, 1);
+ sc_editor_set_cursor_para(nw->sceditor, 0);
+ update_toolbar(nw);
+}
+
+
+static void start_slideshow_sig(GSimpleAction *action, GVariant *parameter,
+ gpointer vp)
+{
+ NarrativeWindow *nw = vp;
+
+ if ( num_slides(nw->p) == 0 ) return;
+
+ nw->show = sc_slideshow_new(nw->p, GTK_APPLICATION(nw->app));
+ if ( nw->show == NULL ) return;
+
+ nw->show_no_slides = 0;
+
+ g_signal_connect(G_OBJECT(nw->show), "key-press-event",
+ G_CALLBACK(nw_key_press_sig), nw);
+ g_signal_connect(G_OBJECT(nw->show), "destroy",
+ G_CALLBACK(ss_destroy_sig), nw);
+ sc_slideshow_set_slide(nw->show, first_slide(nw->p));
+ sc_editor_set_para_highlight(nw->sceditor, 1);
+ sc_editor_set_cursor_para(nw->sceditor, 0);
+ gtk_widget_show_all(GTK_WIDGET(nw->show));
+ update_toolbar(nw);
+}
+
+
+static int create_thumbnail(SCInterpreter *scin, SCBlock *bl,
+ double *w, double *h, void **bvp, void *vp)
+{
+ struct presentation *p = vp;
+
+ *w = 270.0*(p->slide_width / p->slide_height);
+ *h = 270.0;
+ *bvp = bl;
+
+ return 1;
+}
+
+
+static cairo_surface_t *render_thumbnail(int w, int h, void *bvp, void *vp)
+{
+ struct presentation *p = vp;
+ SCBlock *scblocks = bvp;
+ cairo_surface_t *surf;
+ struct frame *top;
+ int sn = slide_number(p, scblocks);
+
+ /* FIXME: Cache like crazy here */
+ surf = render_sc(scblocks, w, h, p->slide_width, p->slide_height,
+ p->stylesheet, NULL, p->is, sn, &top, p->lang);
+ frame_free(top);
+
+ return surf;
+}
+
+
+static int click_thumbnail(double x, double y, void *bvp, void *vp)
+{
+ struct presentation *p = vp;
+ SCBlock *scblocks = bvp;
+ NarrativeWindow *nw = p->narrative_window;
+
+ if ( p->narrative_window->show != NULL ) {
+ sc_slideshow_set_slide(nw->show, scblocks);
+ } else {
+ if ( nw->n_slidewindows >= 16 ) {
+ show_error(nw, _("Too many open slide windows"));
+ } else {
+ nw->slidewindows[nw->n_slidewindows++] = slide_window_open(p, scblocks,
+ p->narrative_window->app);
+ }
+ }
+
+ return 0;
+}
+
+
+GActionEntry nw_entries[] = {
+
+ { "about", about_sig, NULL, NULL, NULL },
+ { "save", save_sig, NULL, NULL, NULL },
+ { "saveas", saveas_sig, NULL, NULL, NULL },
+ { "deleteslide", delete_slide_sig, NULL, NULL, NULL },
+ { "slide", add_slide_sig, NULL, NULL, NULL },
+ { "loadstylesheet", load_ss_sig, NULL, NULL, NULL },
+ { "stylesheet", edit_ss_sig, NULL, NULL, NULL },
+ { "startslideshow", start_slideshow_sig, NULL, NULL, NULL },
+ { "startslideshowhere", start_slideshow_here_sig, NULL, NULL, NULL },
+ { "startslideshownoslides", start_slideshow_noslides_sig, NULL, NULL, NULL },
+ { "clock", open_clock_sig, NULL, NULL, NULL },
+ { "testcard", testcard_sig, NULL, NULL, NULL },
+ { "first", first_para_sig, NULL, NULL, NULL },
+ { "prev", prev_para_sig, NULL, NULL, NULL },
+ { "next", next_para_sig, NULL, NULL, NULL },
+ { "last", last_para_sig, NULL, NULL, NULL },
+ { "print", print_sig, NULL, NULL, NULL },
+ { "exportpdf", exportpdf_sig, NULL, NULL, NULL },
+};
+
+
+void update_titlebar(NarrativeWindow *nw)
+{
+ char *title;
+ char *title_new;
+
+ title = get_titlebar_string(nw->p);
+ title_new = realloc(title, strlen(title)+16);
+ if ( title_new == NULL ) {
+ free(title);
+ return;
+ } else {
+ title = title_new;
+ }
+
+ strcat(title, " - Colloquium");
+ if ( !nw->p->saved ) {
+ strcat(title, " *");
+ }
+ gtk_window_set_title(GTK_WINDOW(nw->window), title);
+
+ /* FIXME: Update all slide windows belonging to this NW */
+
+ free(title);
+}
+
+
+void narrative_window_sw_closed(NarrativeWindow *nw, SlideWindow *sw)
+{
+ int i;
+ int found = 0;
+
+ for ( i=0; i<nw->n_slidewindows; i++ ) {
+ if ( nw->slidewindows[i] == sw ) {
+
+ int j;
+ for ( j=i; j<nw->n_slidewindows-1; j++ ) {
+ nw->slidewindows[j] = nw->slidewindows[j+1];
+ }
+ nw->n_slidewindows--;
+ found = 1;
+ }
+ }
+
+ if ( !found ) {
+ fprintf(stderr, "Couldn't find slide window in narrative record\n");
+ }
+}
+
+
+NarrativeWindow *narrative_window_new(struct presentation *p, GApplication *papp)
+{
+ NarrativeWindow *nw;
+ GtkWidget *vbox;
+ GtkWidget *scroll;
+ GtkWidget *toolbar;
+ GtkToolItem *button;
+ SCCallbackList *cbl;
+ GtkWidget *image;
+ Colloquium *app = COLLOQUIUM(papp);
+
+ if ( p->narrative_window != NULL ) {
+ fprintf(stderr, "Narrative window is already open!\n");
+ return NULL;
+ }
+
+ nw = calloc(1, sizeof(NarrativeWindow));
+ if ( nw == NULL ) return NULL;
+
+ nw->app = papp;
+ nw->p = p;
+ nw->n_slidewindows = 0;
+
+ nw->window = gtk_application_window_new(GTK_APPLICATION(app));
+ p->narrative_window = nw;
+ update_titlebar(nw);
+
+ g_action_map_add_action_entries(G_ACTION_MAP(nw->window), nw_entries,
+ G_N_ELEMENTS(nw_entries), nw);
+
+ vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_container_add(GTK_CONTAINER(nw->window), vbox);
+
+ /* If the presentation is completely empty, give ourselves at least
+ * something to work with */
+ if ( nw->p->scblocks == NULL ) {
+ nw->p->scblocks = sc_parse("");
+ }
+
+ /* Put everything we have inside \presentation{}.
+ * SCEditor will start processing one level down */
+ nw->dummy_top = sc_block_new_parent(nw->p->scblocks, "presentation");
+
+ nw->sceditor = sc_editor_new(nw->dummy_top, p->stylesheet, p->lang,
+ colloquium_get_imagestore(app));
+ cbl = sc_callback_list_new();
+ sc_callback_list_add_callback(cbl, "slide", create_thumbnail,
+ render_thumbnail, click_thumbnail, p);
+ sc_editor_set_callbacks(nw->sceditor, cbl);
+ sc_editor_set_imagestore(nw->sceditor, p->is);
+
+ toolbar = gtk_toolbar_new();
+ gtk_toolbar_set_style(GTK_TOOLBAR(toolbar), GTK_TOOLBAR_ICONS);
+ gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(toolbar), FALSE, FALSE, 0);
+
+ /* Fullscreen */
+ image = gtk_image_new_from_icon_name("view-fullscreen",
+ GTK_ICON_SIZE_LARGE_TOOLBAR);
+ button = gtk_tool_button_new(image, _("Start slideshow"));
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(button),
+ "win.startslideshow");
+ gtk_container_add(GTK_CONTAINER(toolbar), GTK_WIDGET(button));
+
+ button = gtk_separator_tool_item_new();
+ gtk_container_add(GTK_CONTAINER(toolbar), GTK_WIDGET(button));
+
+ /* Add slide */
+ image = gtk_image_new_from_icon_name("list-add",
+ GTK_ICON_SIZE_LARGE_TOOLBAR);
+ button = gtk_tool_button_new(image, _("Add slide"));
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(button),
+ "win.slide");
+ gtk_container_add(GTK_CONTAINER(toolbar), GTK_WIDGET(button));
+
+ button = gtk_separator_tool_item_new();
+ gtk_container_add(GTK_CONTAINER(toolbar), GTK_WIDGET(button));
+
+ image = gtk_image_new_from_icon_name("go-top",
+ GTK_ICON_SIZE_LARGE_TOOLBAR);
+ nw->bfirst = gtk_tool_button_new(image, _("First slide"));
+ gtk_container_add(GTK_CONTAINER(toolbar), GTK_WIDGET(nw->bfirst));
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(nw->bfirst),
+ "win.first");
+
+ image = gtk_image_new_from_icon_name("go-up",
+ GTK_ICON_SIZE_LARGE_TOOLBAR);
+ nw->bprev = gtk_tool_button_new(image, _("Previous slide"));
+ gtk_container_add(GTK_CONTAINER(toolbar), GTK_WIDGET(nw->bprev));
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(nw->bprev),
+ "win.prev");
+
+ image = gtk_image_new_from_icon_name("go-down",
+ GTK_ICON_SIZE_LARGE_TOOLBAR);
+ nw->bnext = gtk_tool_button_new(image, _("Next slide"));
+ gtk_container_add(GTK_CONTAINER(toolbar), GTK_WIDGET(nw->bnext));
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(nw->bnext),
+ "win.next");
+
+ image = gtk_image_new_from_icon_name("go-bottom",
+ GTK_ICON_SIZE_LARGE_TOOLBAR);
+ nw->blast = gtk_tool_button_new(image, _("Last slide"));
+ gtk_container_add(GTK_CONTAINER(toolbar), GTK_WIDGET(nw->blast));
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(nw->blast),
+ "win.last");
+
+ update_toolbar(nw);
+
+ scroll = gtk_scrolled_window_new(NULL, NULL);
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
+ GTK_POLICY_NEVER, GTK_POLICY_ALWAYS);
+ gtk_container_add(GTK_CONTAINER(scroll), GTK_WIDGET(nw->sceditor));
+
+ sc_editor_set_flow(nw->sceditor, 1);
+ sc_editor_set_background(nw->sceditor, 0.9, 0.9, 0.9);
+ sc_editor_set_min_border(nw->sceditor, 0.0);
+ sc_editor_set_top_frame_editable(nw->sceditor, 1);
+
+ g_signal_connect(G_OBJECT(nw->sceditor), "button-press-event",
+ G_CALLBACK(nw_button_press_sig), nw);
+ g_signal_connect(G_OBJECT(nw->sceditor), "changed",
+ G_CALLBACK(changed_sig), nw);
+ g_signal_connect(G_OBJECT(nw->sceditor), "key-press-event",
+ G_CALLBACK(nw_key_press_sig), nw);
+ g_signal_connect(G_OBJECT(nw->window), "destroy",
+ G_CALLBACK(nw_destroy_sig), nw);
+
+ gtk_window_set_default_size(GTK_WINDOW(nw->window), 768, 768);
+ gtk_box_pack_start(GTK_BOX(vbox), scroll, TRUE, TRUE, 0);
+ gtk_container_set_focus_child(GTK_CONTAINER(nw->window),
+ GTK_WIDGET(nw->sceditor));
+
+ gtk_widget_show_all(nw->window);
+ g_application_hold(papp);
+
+ return nw;
+}
diff --git a/src-old/narrative_window.h b/src-old/narrative_window.h
new file mode 100644
index 0000000..24b4a4b
--- /dev/null
+++ b/src-old/narrative_window.h
@@ -0,0 +1,42 @@
+/*
+ * narrative_window.h
+ *
+ * Copyright © 2014-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef NARRATIVE_WINDOW_H
+#define NARRATIVE_WINDOW_H
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+
+typedef struct _narrative_window NarrativeWindow;
+
+#include "slide_window.h"
+
+extern NarrativeWindow *narrative_window_new(struct presentation *p,
+ GApplication *app);
+
+extern void update_titlebar(NarrativeWindow *nw);
+
+extern void narrative_window_sw_closed(NarrativeWindow *nw, SlideWindow *sw);
+
+#endif /* NARRATIVE_WINDOW_H */
diff --git a/src-old/pr_clock.c b/src-old/pr_clock.c
new file mode 100644
index 0000000..8085c89
--- /dev/null
+++ b/src-old/pr_clock.c
@@ -0,0 +1,438 @@
+/*
+ * pr_clock.c
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <gtk/gtk.h>
+
+#include "presentation.h"
+#include "pr_clock.h"
+#include "utils.h"
+
+
+struct pr_clock
+{
+ int open;
+
+ GtkWidget *window;
+ GtkWidget *entry;
+ GtkWidget *startbutton;
+ GtkWidget *da;
+ GtkWidget *wallclock;
+ GtkWidget *elapsed;
+ GtkWidget *remaining;
+ GtkWidget *status;
+ GTimeZone *tz;
+
+ GDateTime *start;
+ double time_elapsed_at_start;
+ guint timer_id;
+
+ int running;
+ double time_allowed;
+ double time_elapsed;
+ int pos;
+ int end;
+ int pos_reached;
+
+ double t;
+ double tf;
+};
+
+
+static char *format_span(int n)
+{
+ char tmp[32];
+ int hours, mins, sec;
+ char *s;
+
+ if ( n < 0 ) {
+ s = "-";
+ n = -n;
+ } else {
+ s = "";
+ }
+
+ sec = n % 60;
+ mins = ((n-sec) % (60*60))/60;
+ hours = (n-sec-mins) / (60*60);
+
+ snprintf(tmp, 31, "%s%i:%02i:%02i", s, hours, mins, sec);
+
+ return strdup(tmp);
+}
+
+
+static char *format_span_nice(int n)
+{
+ char tmp[64];
+ int hours, mins, sec;
+ char *s;
+
+ if ( n < 0 ) {
+ s = "behind";
+ n = -n;
+ } else {
+ s = "ahead";
+ }
+
+ sec = n % 60;
+ mins = ((n-sec) % (60*60))/60;
+ hours = (n-sec-mins) / (60*60);
+
+ if ( n <= 60 ) {
+ snprintf(tmp, 63, "%i seconds %s", n, s);
+ return strdup(tmp);
+ }
+
+ if ( n < 60*60 ) {
+ snprintf(tmp, 63, "%i min %i seconds %s", mins, sec, s);
+ return strdup(tmp);
+ }
+
+ snprintf(tmp, 63, "%i hours, %i min, %i seconds %s",
+ hours, mins, sec, s);
+ return strdup(tmp);
+}
+
+
+static gboolean update_clock(gpointer data)
+{
+ struct pr_clock *n = data;
+ gchar *d;
+ GDateTime *dt;
+ GTimeSpan sp;
+ double time_remaining;
+ double delta;
+ gint w, h;
+ char *tmp;
+
+ if ( !n->open ) {
+ g_date_time_unref(n->start);
+ g_time_zone_unref(n->tz);
+ free(n);
+ return FALSE;
+ }
+
+ dt = g_date_time_new_now(n->tz);
+
+ if ( n->running ) {
+
+ sp = g_date_time_difference(dt, n->start);
+ n->time_elapsed = n->time_elapsed_at_start +
+ sp / G_TIME_SPAN_SECOND;
+
+ time_remaining = n->time_allowed - n->time_elapsed;
+
+ tmp = format_span(n->time_elapsed);
+ gtk_label_set_text(GTK_LABEL(n->elapsed), tmp);
+ free(tmp);
+
+ tmp = format_span(time_remaining);
+ gtk_label_set_text(GTK_LABEL(n->remaining), tmp);
+ free(tmp);
+
+ } else {
+
+ n->time_elapsed = n->time_elapsed_at_start;
+
+ time_remaining = n->time_allowed - n->time_elapsed;
+
+ tmp = format_span(n->time_elapsed);
+ gtk_label_set_text(GTK_LABEL(n->elapsed), tmp);
+ free(tmp);
+
+ tmp = format_span(time_remaining);
+ gtk_label_set_text(GTK_LABEL(n->remaining), tmp);
+ free(tmp);
+
+ }
+
+ d = g_date_time_format(dt, "%H:%M:%S");
+ g_date_time_unref(dt);
+
+ gtk_label_set_text(GTK_LABEL(n->wallclock), d);
+ free(d);
+
+ n->t = n->time_elapsed / n->time_allowed;
+
+ if ( n->time_allowed == 0.0 ) n->t = 0.0;
+ if ( n->time_elapsed > n->time_allowed ) n->t = 1.0;
+
+ if ( n->end > 0 ) {
+ n->tf = (double)n->pos_reached / (n->end-1);
+ } else {
+ n->tf = 0.0;
+ }
+
+ delta = (n->tf - n->t)*n->time_allowed;
+ tmp = format_span_nice(delta);
+ gtk_label_set_text(GTK_LABEL(n->status), tmp);
+ free(tmp);
+
+ w = gtk_widget_get_allocated_width(GTK_WIDGET(n->da));
+ h = gtk_widget_get_allocated_height(GTK_WIDGET(n->da));
+ gtk_widget_queue_draw_area(n->da, 0, 0, w, h);
+
+ return TRUE;
+}
+
+
+void pr_clock_set_pos(PRClock *n, int pos, int end)
+{
+ if ( n == NULL ) return;
+ n->pos = pos;
+ if ( n->pos > n->pos_reached ) {
+ n->pos_reached = pos;
+ }
+ n->end = end;
+ update_clock(n);
+}
+
+
+static gint close_clock_sig(GtkWidget *w, PRClock *n)
+{
+ g_source_remove(n->timer_id);
+ free(n);
+ return FALSE;
+}
+
+
+static gboolean clock_draw_sig(GtkWidget *da, cairo_t *cr, struct pr_clock *n)
+{
+ int width, height;
+ double s;
+ double ff;
+
+ width = gtk_widget_get_allocated_width(GTK_WIDGET(da));
+ height = gtk_widget_get_allocated_height(GTK_WIDGET(da));
+ s = width-20.0;
+
+ /* Overall background */
+ cairo_rectangle(cr, 10.0, 0.0, s, height);
+ cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
+ cairo_fill(cr);
+
+ cairo_rectangle(cr, 10.0, 0.0, s*n->t, height);
+ cairo_set_source_rgb(cr, 0.0, 0.0, 1.0);
+ cairo_fill(cr);
+
+ if ( n->tf > n->t ) {
+ cairo_rectangle(cr, 10.0+s*n->t, 0.0, (n->tf - n->t)*s, height);
+ cairo_set_source_rgb(cr, 0.0, 1.0, 0.0);
+ cairo_fill(cr);
+ } else {
+ cairo_rectangle(cr, 10.0+s*n->t, 0.0, (n->tf - n->t)*s, height);
+ cairo_set_source_rgb(cr, 1.0, 0.0, 0.0);
+ cairo_fill(cr);
+ }
+
+ ff = (double)n->pos / (n->end-1);
+ if ( n->end == 1 ) ff = 0.0;
+ cairo_move_to(cr, 10.0+ff*s, 0.0);
+ cairo_line_to(cr, 10.0+ff*s, height);
+ cairo_set_line_width(cr, 2.0);
+ cairo_set_source_rgb(cr, 1.0, 0.0, 1.0);
+ cairo_stroke(cr);
+
+ if ( !n->running ) {
+ cairo_move_to(cr, 10.0, height*0.8);
+ cairo_set_font_size(cr, height*0.8);
+ cairo_select_font_face(cr, "sans-serif",
+ CAIRO_FONT_SLANT_NORMAL,
+ CAIRO_FONT_WEIGHT_BOLD);
+ cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
+ cairo_show_text(cr, _("Timer is NOT running!"));
+ }
+
+ return FALSE;
+}
+
+
+static void set_sig(GtkEditable *w, struct pr_clock *n)
+{
+ const gchar *t;
+ char *check;
+
+ t = gtk_entry_get_text(GTK_ENTRY(n->entry));
+ n->time_allowed = 60.0 * strtod(t, &check);
+ if ( check == t ) {
+ fprintf(stderr, "Invalid time '%s'\n", t);
+ n->time_allowed = 0.0;
+ }
+
+ update_clock(n);
+}
+
+
+static gboolean reset_sig(GtkWidget *w, gpointer data)
+{
+ struct pr_clock *n = data;
+
+ n->time_elapsed = 0;
+ n->time_elapsed_at_start = 0;
+
+ if ( n->start != NULL ) {
+ g_date_time_unref(n->start);
+ }
+
+ n->start = g_date_time_new_now(n->tz);
+
+ update_clock(n);
+
+ return FALSE;
+}
+
+
+static gboolean setpos_sig(GtkWidget *w, gpointer data)
+{
+ struct pr_clock *n = data;
+ n->pos_reached = n->pos;
+ update_clock(n);
+ return FALSE;
+}
+
+
+static gboolean start_sig(GtkWidget *w, gpointer data)
+{
+ struct pr_clock *n = data;
+
+ if ( n->running ) {
+ n->running = 0;
+ n->time_elapsed_at_start = n->time_elapsed;
+ gtk_label_set_text(GTK_LABEL(gtk_bin_get_child(GTK_BIN(w))),
+ _("Start"));
+ } else {
+ n->time_elapsed_at_start = n->time_elapsed;
+ if ( n->start != NULL ) {
+ g_date_time_unref(n->start);
+ }
+ n->start = g_date_time_new_now(n->tz);
+ n->running = 1;
+ gtk_label_set_text(GTK_LABEL(gtk_bin_get_child(GTK_BIN(w))),
+ _("Stop"));
+ }
+
+ update_clock(n);
+
+ return FALSE;
+}
+
+
+PRClock *pr_clock_new()
+{
+ struct pr_clock *n;
+ GtkWidget *vbox;
+ GtkWidget *hbox;
+ GtkWidget *resetbutton;
+ GtkWidget *setposbutton;
+ GtkWidget *grid;
+ GtkWidget *label;
+
+ n = malloc(sizeof(struct pr_clock));
+ if ( n == NULL ) return NULL;
+ n->open = 1;
+
+ n->tz = g_time_zone_new_local();
+
+ n->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ gtk_window_set_default_size(GTK_WINDOW(n->window), 600, 150);
+
+ vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_container_add(GTK_CONTAINER(n->window), vbox);
+
+ hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 10);
+
+ label = gtk_label_new(_("Length (mins):"));
+ gtk_label_set_markup(GTK_LABEL(label), _("<b>Length (mins):</b>"));
+ g_object_set(G_OBJECT(label), "halign", GTK_ALIGN_END, NULL);
+ gtk_box_pack_start(GTK_BOX(hbox), label, TRUE, TRUE, 10);
+
+ n->entry = gtk_entry_new();
+ gtk_box_pack_start(GTK_BOX(hbox), n->entry, TRUE, TRUE, 0);
+
+ n->startbutton = gtk_button_new_with_label(_("Start"));
+ gtk_box_pack_start(GTK_BOX(hbox), n->startbutton, TRUE, TRUE, 10);
+
+ resetbutton = gtk_button_new_with_label(_("Reset"));
+ gtk_box_pack_start(GTK_BOX(hbox), resetbutton, TRUE, TRUE, 10);
+
+ setposbutton = gtk_button_new_with_label(_("Set position"));
+ gtk_box_pack_start(GTK_BOX(hbox), setposbutton, TRUE, TRUE, 10);
+
+ n->da = gtk_drawing_area_new();
+ gtk_box_pack_start(GTK_BOX(vbox), n->da, TRUE, TRUE, 0);
+ g_signal_connect(G_OBJECT(n->da), "draw", G_CALLBACK(clock_draw_sig), n);
+ g_signal_connect(G_OBJECT(n->window), "destroy",
+ G_CALLBACK(close_clock_sig), n); /* FIXME: Uniqueness */
+
+ grid = gtk_grid_new();
+ gtk_grid_set_row_spacing(GTK_GRID(grid), 10);
+ gtk_grid_set_column_homogeneous(GTK_GRID(grid), TRUE);
+ gtk_box_pack_start(GTK_BOX(vbox), grid, FALSE, FALSE, 10);
+ label = gtk_label_new(_("Time elapsed"));
+ gtk_label_set_markup(GTK_LABEL(label), _("<b>Time elapsed</b>"));
+ gtk_grid_attach(GTK_GRID(grid), label, 0, 0, 1, 1);
+ label = gtk_label_new(_("Time remaining"));
+ gtk_label_set_markup(GTK_LABEL(label), _("<b>Time remaining</b>"));
+ gtk_grid_attach(GTK_GRID(grid), label, 1, 0, 1, 1);
+ n->status = gtk_label_new("<status>");
+ gtk_grid_attach(GTK_GRID(grid), n->status, 2, 0, 1, 1);
+ n->elapsed = gtk_label_new("<elapsed>");
+ gtk_grid_attach(GTK_GRID(grid), n->elapsed, 0, 1, 1, 1);
+ n->remaining = gtk_label_new("<remaining>");
+ gtk_grid_attach(GTK_GRID(grid), n->remaining, 1, 1, 1, 1);
+ n->wallclock = gtk_label_new("<wall clock>");
+ gtk_grid_attach(GTK_GRID(grid), n->wallclock, 2, 1, 1, 1);
+
+ g_signal_connect(G_OBJECT(n->startbutton), "clicked",
+ G_CALLBACK(start_sig), n);
+ g_signal_connect(G_OBJECT(resetbutton), "clicked",
+ G_CALLBACK(reset_sig), n);
+ g_signal_connect(G_OBJECT(setposbutton), "clicked",
+ G_CALLBACK(setpos_sig), n);
+ g_signal_connect(G_OBJECT(n->entry), "changed",
+ G_CALLBACK(set_sig), n);
+
+ n->running = 0;
+ n->time_allowed = 0;
+ n->time_elapsed = 0;
+ n->time_elapsed_at_start = 0;
+ n->pos = 0;
+ n->pos_reached = 0;
+ n->end = 0;
+ n->start = NULL;
+ update_clock(n);
+ n->timer_id = g_timeout_add_seconds(1, update_clock, n);
+
+ gtk_window_set_title(GTK_WINDOW(n->window), _("Presentation clock"));
+
+ gtk_widget_show_all(n->window);
+ return n;
+}
diff --git a/src-old/pr_clock.h b/src-old/pr_clock.h
new file mode 100644
index 0000000..97d2d0d
--- /dev/null
+++ b/src-old/pr_clock.h
@@ -0,0 +1,37 @@
+/*
+ * pr_clock.h
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef CLOCK_H
+#define CLOCK_H
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+typedef struct pr_clock PRClock;
+
+extern PRClock *pr_clock_new(void);
+
+extern void pr_clock_set_pos(PRClock *n, int pos, int end);
+
+
+#endif /* CLOCK_H */
diff --git a/src-old/presentation.c b/src-old/presentation.c
new file mode 100644
index 0000000..d7d9c08
--- /dev/null
+++ b/src-old/presentation.c
@@ -0,0 +1,362 @@
+/*
+ * presentation.c
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <gtk/gtk.h>
+
+#include "presentation.h"
+#include "slide_window.h"
+#include "frame.h"
+#include "imagestore.h"
+#include "render.h"
+#include "sc_interp.h"
+#include "utils.h"
+
+
+void free_presentation(struct presentation *p)
+{
+ /* FIXME: Loads of stuff leaks here */
+ g_object_unref(p->file);
+ g_object_unref(p->stylesheet_from);
+ imagestore_destroy(p->is);
+ free(p);
+}
+
+
+char *get_titlebar_string(struct presentation *p)
+{
+ if ( p == NULL || p->file == NULL ) {
+ return strdup(_("(untitled)"));
+ } else {
+ char *bn = g_file_get_basename(p->file);
+ return bn;
+ }
+}
+
+
+static void find_and_load_stylesheet(struct presentation *p, GFile *file)
+{
+ GFile *ssfile;
+
+ if ( file != NULL ) {
+
+ /* First choice: /same/directory/<presentation>.ss */
+ gchar *ssuri = g_file_get_uri(file);
+ if ( ssuri != NULL ) {
+ size_t l = strlen(ssuri);
+ if ( ssuri[l-3] == '.' && ssuri[l-2] == 's' && ssuri[l-1] =='c' ) {
+ ssuri[l-1] = 's';
+ ssfile = g_file_new_for_uri(ssuri);
+ p->stylesheet = stylesheet_load(ssfile);
+ p->stylesheet_from = ssfile;
+ g_free(ssuri);
+ }
+ }
+
+ /* Second choice: /same/directory/stylesheet.ss */
+ if ( p->stylesheet == NULL ) {
+ GFile *parent = g_file_get_parent(file);
+ if ( parent != NULL ) {
+ ssfile = g_file_get_child(parent, "stylesheet.ss");
+ if ( ssfile != NULL ) {
+ p->stylesheet = stylesheet_load(ssfile);
+ p->stylesheet_from = ssfile;
+ }
+ g_object_unref(parent);
+ }
+ }
+
+ }
+
+ /* Third choice: <cwd>/stylesheet.ss */
+ if ( p->stylesheet == NULL ) {
+ ssfile = g_file_new_for_path("./stylesheet.ss");
+ p->stylesheet = stylesheet_load(ssfile);
+ p->stylesheet_from = ssfile;
+ }
+
+ /* Fourth choice: internal default stylesheet */
+ if ( p->stylesheet == NULL ) {
+ ssfile = g_file_new_for_uri("resource:///uk/me/bitwiz/Colloquium/default.ss");
+ p->stylesheet = stylesheet_load(ssfile);
+ p->stylesheet_from = NULL;
+ g_object_unref(ssfile);
+ }
+
+ /* Last resort is NULL stylesheet and SCInterpreter's defaults */
+ /* We keep a reference to the GFile */
+}
+
+
+struct presentation *new_presentation(const char *imagestore)
+{
+ struct presentation *new;
+
+ new = calloc(1, sizeof(struct presentation));
+ if ( new == NULL ) return NULL;
+
+ new->file = NULL;
+ new->stylesheet_from = NULL;
+
+ new->scblocks = NULL;
+
+ /* Default slide size */
+ new->slide_width = 1024.0;
+ new->slide_height = 768.0;
+
+ new->completely_empty = 1;
+ new->saved = 1;
+ new->stylesheet = NULL;
+ new->is = imagestore_new(imagestore);
+
+ new->lang = pango_language_get_default();
+
+ find_and_load_stylesheet(new, NULL);
+
+ return new;
+}
+
+
+int save_presentation(struct presentation *p, GFile *file, GFile *ssfile)
+{
+ GFileOutputStream *fh;
+ int r;
+ int sr;
+ GError *error = NULL;
+
+ fh = g_file_replace(file, NULL, FALSE, G_FILE_CREATE_NONE, NULL, &error);
+ if ( fh == NULL ) {
+ fprintf(stderr, _("Open failed: %s\n"), error->message);
+ return 1;
+ }
+ r = save_sc_block(G_OUTPUT_STREAM(fh), p->scblocks);
+ if ( r ) {
+ fprintf(stderr, _("Couldn't save presentation\n"));
+ }
+ g_object_unref(fh);
+
+ if ( ssfile != NULL ) {
+ char *uri = g_file_get_uri(ssfile);
+ printf(_("Saving stylesheet to %s\n"), uri);
+ g_free(uri);
+ sr = stylesheet_save(p->stylesheet, ssfile);
+ if ( sr ) {
+ fprintf(stderr, _("Couldn't save stylesheet\n"));
+ }
+ if ( p->stylesheet_from != ssfile ) {
+ if ( p->stylesheet_from != NULL ) {
+ g_object_unref(p->stylesheet_from);
+ }
+ p->stylesheet_from = ssfile;
+ g_object_ref(p->stylesheet_from);
+ }
+ } else {
+ fprintf(stderr, _("Not saving the stylesheet\n"));
+ sr = 0;
+ }
+
+ if ( r || sr ) return 1;
+
+ imagestore_set_parent(p->is, g_file_get_parent(file));
+
+ if ( p->file != file ) {
+ if ( p->file != NULL ) g_object_unref(p->file);
+ p->file = file;
+ g_object_ref(p->file);
+ }
+ p->saved = 1;
+ update_titlebar(p->narrative_window);
+ return 0;
+}
+
+
+int slide_number(struct presentation *p, SCBlock *sl)
+{
+ SCBlock *bl = p->scblocks;
+ int n = 0;
+
+ while ( bl != NULL ) {
+ if ( safe_strcmp(sc_block_name(bl), "slide") == 0 ) {
+ n++;
+ if ( bl == sl ) return n;
+ }
+ bl = sc_block_next(bl);
+ }
+
+ return 0;
+}
+
+
+int num_slides(struct presentation *p)
+{
+ SCBlock *bl = p->scblocks;
+ int n = 0;
+
+ while ( bl != NULL ) {
+ if ( safe_strcmp(sc_block_name(bl), "slide") == 0 ) {
+ n++;
+ }
+ bl = sc_block_next(bl);
+ }
+
+ return n;
+}
+
+
+SCBlock *first_slide(struct presentation *p)
+{
+ SCBlock *bl = p->scblocks;
+
+ while ( bl != NULL ) {
+ if ( safe_strcmp(sc_block_name(bl), "slide") == 0 ) {
+ return bl;
+ }
+ bl = sc_block_next(bl);
+ }
+
+ fprintf(stderr, "Couldn't find first slide!\n");
+ return NULL;
+}
+
+
+SCBlock *last_slide(struct presentation *p)
+{
+ SCBlock *bl = p->scblocks;
+ SCBlock *l = NULL;
+
+ while ( bl != NULL ) {
+ if ( safe_strcmp(sc_block_name(bl), "slide") == 0 ) {
+ l = bl;
+ }
+ bl = sc_block_next(bl);
+ }
+
+ if ( l == NULL ) {
+ fprintf(stderr, "Couldn't find last slide!\n");
+ }
+ return l;
+}
+
+
+SCBlock *next_slide(struct presentation *p, SCBlock *sl)
+{
+ SCBlock *bl = sl;
+ int found = 0;
+
+ while ( bl != NULL ) {
+ if ( safe_strcmp(sc_block_name(bl), "slide") == 0 ) {
+ if ( found ) return bl;
+ }
+ if ( bl == sl ) {
+ found = 1;
+ }
+ bl = sc_block_next(bl);
+ }
+
+ fprintf(stderr, "Couldn't find next slide!\n");
+ return NULL;
+}
+
+
+SCBlock *prev_slide(struct presentation *p, SCBlock *sl)
+{
+ SCBlock *bl = p->scblocks;
+ SCBlock *l = NULL;
+
+ while ( bl != NULL ) {
+ if ( bl == sl ) {
+ if ( l == NULL ) return sl; /* Already on first slide */
+ return l;
+ }
+ if ( safe_strcmp(sc_block_name(bl), "slide") == 0 ) {
+ l = bl;
+ }
+ bl = sc_block_next(bl);
+ }
+
+ fprintf(stderr, "Couldn't find prev slide!\n");
+ return NULL;
+}
+
+
+static void set_slide_size_from_stylesheet(struct presentation *p)
+{
+ char *result;
+
+ result = stylesheet_lookup(p->stylesheet, "$.slide", "size");
+ if ( result != NULL ) {
+ float v[2];
+ if ( parse_double(result, v) == 0 ) {
+ p->slide_width = v[0];
+ p->slide_height = v[1];
+ }
+ }
+}
+
+
+int load_presentation(struct presentation *p, GFile *file)
+{
+ int r = 0;
+ char *everything;
+
+ assert(p->completely_empty);
+
+ if ( !g_file_load_contents(file, NULL, &everything, NULL, NULL, NULL) ) {
+ fprintf(stderr, _("Failed to load '%s'\n"), g_file_get_uri(file));
+ return 1;
+ }
+
+ p->scblocks = sc_parse(everything);
+ g_free(everything);
+
+ p->lang = pango_language_get_default();
+
+ if ( p->scblocks == NULL ) r = 1;
+
+ if ( r ) {
+ p->completely_empty = 1;
+ fprintf(stderr, _("Parse error.\n"));
+ return r; /* Error */
+ }
+
+ p->stylesheet = NULL;
+
+ find_and_load_stylesheet(p, file);
+
+ set_slide_size_from_stylesheet(p);
+
+ assert(p->file == NULL);
+ p->file = file;
+ g_object_ref(file);
+
+ imagestore_set_parent(p->is, g_file_get_parent(file));
+
+ return 0;
+}
+
diff --git a/src-old/presentation.h b/src-old/presentation.h
new file mode 100644
index 0000000..b288d8e
--- /dev/null
+++ b/src-old/presentation.h
@@ -0,0 +1,83 @@
+/*
+ * presentation.h
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef PRESENTATION_H
+#define PRESENTATION_H
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+struct presentation;
+
+#include "imagestore.h"
+#include "sc_parse.h"
+#include "slideshow.h"
+#include "narrative_window.h"
+#include "slide_window.h"
+#include "stylesheet.h"
+
+struct menu_pl;
+
+struct presentation
+{
+ GFile *file;
+ GFile *stylesheet_from;
+ int completely_empty;
+ int saved;
+ PangoLanguage *lang;
+
+ ImageStore *is;
+
+ NarrativeWindow *narrative_window;
+
+ struct pr_clock *clock;
+
+ /* This is the "native" size of the slide. It only exists to give
+ * font size some meaning in the context of a somewhat arbitrary DPI */
+ double slide_width;
+ double slide_height;
+
+ SCBlock *scblocks;
+ Stylesheet *stylesheet;
+};
+
+
+extern struct presentation *new_presentation(const char *imagestore);
+extern void free_presentation(struct presentation *p);
+
+extern char *get_titlebar_string(struct presentation *p);
+
+extern int slide_number(struct presentation *p, SCBlock *sl);
+extern int num_slides(struct presentation *p);
+extern SCBlock *first_slide(struct presentation *p);
+extern SCBlock *last_slide(struct presentation *p);
+extern SCBlock *next_slide(struct presentation *p, SCBlock *sl);
+extern SCBlock *prev_slide(struct presentation *p, SCBlock *sl);
+
+extern int load_presentation(struct presentation *p, GFile *file);
+extern int save_presentation(struct presentation *p, GFile *file, GFile *ssfile);
+
+#define UNUSED __attribute__((unused))
+
+
+#endif /* PRESENTATION_H */
diff --git a/src-old/print.c b/src-old/print.c
new file mode 100644
index 0000000..43c967e
--- /dev/null
+++ b/src-old/print.c
@@ -0,0 +1,314 @@
+/*
+ * print.c
+ *
+ * Copyright © 2016-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <gtk/gtk.h>
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "presentation.h"
+#include "narrative_window.h"
+#include "render.h"
+#include "utils.h"
+
+
+static GtkPrintSettings *print_settings = NULL;
+
+struct print_stuff
+{
+ struct presentation *p;
+
+ /* Printing config */
+ GtkWidget *combo;
+ int slides_only;
+
+ /* When printing slides only */
+ SCBlock *slide;
+
+ /* When printing narrative */
+ int nar_line;
+ struct frame *top;
+ int start_paras[256];
+ int slide_number;
+};
+
+
+static void print_widget_apply(GtkPrintOperation *op, GtkWidget *widget,
+ void *vp)
+{
+ const char *id;
+ struct print_stuff *ps = vp;
+
+ id = gtk_combo_box_get_active_id(GTK_COMBO_BOX(ps->combo));
+ if ( strcmp(id, "slides") == 0 ) {
+ ps->slides_only = 1;
+ } else {
+ ps->slides_only = 0;
+ }
+}
+
+
+static GObject *print_widget(GtkPrintOperation *op, void *vp)
+{
+ GtkWidget *vbox;
+ GtkWidget *cbox;
+ struct print_stuff *ps = vp;
+
+ vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
+ gtk_container_set_border_width(GTK_CONTAINER(vbox), 10);
+
+ /* What do you want to print? */
+ cbox = gtk_combo_box_text_new();
+ gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(cbox), "slides",
+ _("Print the slides only"));
+ gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(cbox), "narrative",
+ _("Print the narrative"));
+ gtk_box_pack_start(GTK_BOX(vbox), cbox, FALSE, FALSE, 10);
+ gtk_combo_box_set_active(GTK_COMBO_BOX(cbox), 1);
+ ps->combo = cbox;
+
+ gtk_widget_show_all(vbox);
+ return G_OBJECT(vbox);
+
+}
+
+
+static void print_slide_only(GtkPrintOperation *op, GtkPrintContext *ctx,
+ struct print_stuff *ps, int page)
+{
+ cairo_t *cr;
+ PangoContext *pc;
+ double w, h;
+ struct frame *top;
+ const double sw = ps->p->slide_width;
+ const double sh = ps->p->slide_height;
+ double slide_width, slide_height;
+
+ cr = gtk_print_context_get_cairo_context(ctx);
+ pc = gtk_print_context_create_pango_context(ctx);
+ w = gtk_print_context_get_width(ctx);
+ h = gtk_print_context_get_height(ctx);
+
+ cairo_rectangle(cr, 0.0, 0.0, w, h);
+ cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
+ cairo_fill(cr);
+
+ if ( sw/sh > w/h ) {
+ /* Slide is too wide. Letterboxing top/bottom */
+ slide_width = w;
+ slide_height = w * sh/sw;
+ } else {
+ /* Letterboxing at sides */
+ slide_width = h * sw/sh;
+ slide_height = h;
+ }
+
+ printf("%f x %f ---> %f x %f\n", w, h, slide_width, slide_height);
+
+ top = interp_and_shape(ps->slide, ps->p->stylesheet, NULL,
+ ps->p->is, page+1, pc, sw, sh, ps->p->lang);
+
+ recursive_wrap(top, pc);
+
+ cairo_scale(cr, slide_width/sw, slide_width/sw);
+
+ recursive_draw(top, cr, ps->p->is,
+ 0.0, ps->p->slide_height);
+
+ ps->slide = next_slide(ps->p, ps->slide);
+}
+
+
+static int print_create_thumbnail(SCInterpreter *scin, SCBlock *bl,
+ double *w, double *h, void **bvp, void *vp)
+{
+ struct print_stuff *ps = vp;
+ struct presentation *p = ps->p;
+ SCBlock *b;
+
+ *w = 270.0*(p->slide_width / p->slide_height);
+ *h = 270.0;
+ b = bl;
+
+ *bvp = b;
+
+ return 1;
+}
+
+
+static cairo_surface_t *print_render_thumbnail(int w, int h, void *bvp, void *vp)
+{
+ struct print_stuff *ps = vp;
+ struct presentation *p = ps->p;
+ SCBlock *scblocks = bvp;
+ cairo_surface_t *surf;
+ struct frame *top;
+
+ surf = render_sc(scblocks, w, h, p->slide_width, p->slide_height,
+ p->stylesheet, NULL, p->is, ps->slide_number++,
+ &top, p->lang);
+ frame_free(top);
+
+ return surf;
+}
+
+
+static void begin_narrative_print(GtkPrintOperation *op, GtkPrintContext *ctx,
+ struct print_stuff *ps)
+{
+ SCCallbackList *cbl;
+ PangoContext *pc;
+ int i, n_pages;
+ double h, page_height;
+ SCBlock *dummy_top;
+
+ cbl = sc_callback_list_new();
+ ps->slide_number = 1;
+ sc_callback_list_add_callback(cbl, "slide", print_create_thumbnail,
+ print_render_thumbnail, NULL, ps);
+
+ pc = gtk_print_context_create_pango_context(ctx);
+
+ dummy_top = sc_block_new_parent(ps->p->scblocks, "presentation");
+ ps->top = interp_and_shape(dummy_top, ps->p->stylesheet, cbl,
+ ps->p->is, 0, pc,
+ gtk_print_context_get_width(ctx),
+ gtk_print_context_get_height(ctx),
+ ps->p->lang);
+ recursive_wrap(ps->top, pc);
+
+ /* Count pages */
+ page_height = gtk_print_context_get_height(ctx);
+ h = 0.0;
+ n_pages = 1;
+ ps->start_paras[0] = 0;
+ for ( i=0; i<ps->top->n_paras; i++ ) {
+ if ( h + paragraph_height(ps->top->paras[i]) > page_height ) {
+ /* Paragraph does not fit on page */
+ ps->start_paras[n_pages] = i;
+ n_pages++;
+ h = 0.0;
+ }
+ h += paragraph_height(ps->top->paras[i]);
+ }
+ gtk_print_operation_set_n_pages(op, n_pages);
+ g_object_unref(pc);
+}
+
+
+static void print_narrative(GtkPrintOperation *op, GtkPrintContext *ctx,
+ struct print_stuff *ps, gint page)
+{
+ int i;
+ double h, page_height;
+ cairo_t *cr;
+
+ page_height = gtk_print_context_get_height(ctx);
+ cr = gtk_print_context_get_cairo_context(ctx);
+
+ h = 0.0;
+ for ( i=ps->start_paras[page]; i<ps->top->n_paras; i++ ) {
+
+ /* Will this paragraph fit? */
+ h += paragraph_height(ps->top->paras[i]);
+ if ( h > page_height ) return;
+
+ cairo_save(cr);
+ render_paragraph(cr, ps->top->paras[i], ps->p->is);
+ cairo_restore(cr);
+
+ cairo_translate(cr, 0.0, paragraph_height(ps->top->paras[i]));
+
+ }
+
+
+}
+
+
+
+static void print_begin(GtkPrintOperation *op, GtkPrintContext *ctx, void *vp)
+{
+ struct print_stuff *ps = vp;
+
+ if ( ps->slides_only ) {
+ gtk_print_operation_set_n_pages(op, num_slides(ps->p));
+ ps->slide = first_slide(ps->p);
+ } else {
+ begin_narrative_print(op, ctx, ps);
+ }
+}
+
+
+static void print_draw(GtkPrintOperation *op, GtkPrintContext *ctx, gint page,
+ void *vp)
+{
+ struct print_stuff *ps = vp;
+ if ( ps->slides_only ) {
+ print_slide_only(op, ctx, ps, page);
+ } else {
+ print_narrative(op, ctx, ps, page);
+ }
+}
+
+
+void run_printing(struct presentation *p, GtkWidget *parent)
+{
+ GtkPrintOperation *print;
+ GtkPrintOperationResult res;
+ struct print_stuff *ps;
+
+ ps = malloc(sizeof(struct print_stuff));
+ if ( ps == NULL ) return;
+ ps->p = p;
+ ps->nar_line = 0;
+
+ print = gtk_print_operation_new();
+ if ( print_settings != NULL ) {
+ gtk_print_operation_set_print_settings(print, print_settings);
+ }
+
+ g_signal_connect(print, "create-custom-widget",
+ G_CALLBACK(print_widget), ps);
+ g_signal_connect(print, "custom-widget-apply",
+ G_CALLBACK(print_widget_apply), ps);
+ g_signal_connect(print, "begin_print", G_CALLBACK(print_begin), ps);
+ g_signal_connect(print, "draw_page", G_CALLBACK(print_draw), ps);
+
+ res = gtk_print_operation_run(print,
+ GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG,
+ GTK_WINDOW(parent), NULL);
+
+ if ( res == GTK_PRINT_OPERATION_RESULT_APPLY ) {
+ if ( print_settings != NULL ) {
+ g_object_unref(print_settings);
+ }
+ print_settings = g_object_ref(
+ gtk_print_operation_get_print_settings(print));
+ }
+ g_object_unref(print);
+}
+
diff --git a/src-old/print.h b/src-old/print.h
new file mode 100644
index 0000000..265b7c1
--- /dev/null
+++ b/src-old/print.h
@@ -0,0 +1,32 @@
+/*
+ * print.h
+ *
+ * Copyright © 2016-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef PRINT_H
+#define PRINT_H
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+extern void run_printing(struct presentation *p, GtkWidget *parent);
+
+#endif /* PRINT_H */
diff --git a/src-old/render.c b/src-old/render.c
new file mode 100644
index 0000000..6ac09fc
--- /dev/null
+++ b/src-old/render.c
@@ -0,0 +1,332 @@
+/*
+ * render.c
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <cairo.h>
+#include <cairo-pdf.h>
+#include <pango/pangocairo.h>
+#include <assert.h>
+#include <string.h>
+#include <stdlib.h>
+#include <math.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+#include <gdk/gdk.h>
+
+#include "sc_parse.h"
+#include "sc_interp.h"
+#include "presentation.h"
+#include "frame.h"
+#include "render.h"
+#include "imagestore.h"
+#include "utils.h"
+
+
+static void do_background(cairo_t *cr, struct frame *fr)
+{
+ cairo_pattern_t *patt = NULL;
+
+ cairo_new_path(cr);
+ cairo_rectangle(cr, 0.0, 0.0, fr->w, fr->h);
+
+ switch ( fr->grad ) {
+
+ case GRAD_NONE:
+ cairo_set_source_rgba(cr, fr->bgcol[0],
+ fr->bgcol[1],
+ fr->bgcol[2],
+ fr->bgcol[3]);
+ break;
+
+ case GRAD_VERT:
+ patt = cairo_pattern_create_linear(0.0, 0.0,
+ 0.0, fr->h);
+ cairo_pattern_add_color_stop_rgba(patt, 0.0, fr->bgcol[0],
+ fr->bgcol[1],
+ fr->bgcol[2],
+ fr->bgcol[3]);
+ cairo_pattern_add_color_stop_rgba(patt, 1.0, fr->bgcol2[0],
+ fr->bgcol2[1],
+ fr->bgcol2[2],
+ fr->bgcol2[3]);
+ cairo_set_source(cr, patt);
+ break;
+
+ case GRAD_HORIZ:
+ patt = cairo_pattern_create_linear(0.0, 0.0,
+ fr->w, 0.0);
+ cairo_pattern_add_color_stop_rgba(patt, 0.0, fr->bgcol[0],
+ fr->bgcol[1],
+ fr->bgcol[2],
+ fr->bgcol[3]);
+ cairo_pattern_add_color_stop_rgba(patt, 1.0, fr->bgcol2[0],
+ fr->bgcol2[1],
+ fr->bgcol2[2],
+ fr->bgcol2[3]);
+ cairo_set_source(cr, patt);
+ break;
+
+ }
+
+ cairo_fill(cr);
+ if ( patt != NULL ) cairo_pattern_destroy(patt);
+}
+
+
+static int draw_frame(cairo_t *cr, struct frame *fr, ImageStore *is,
+ double min_y, double max_y)
+{
+ int i;
+ double hpos = 0.0;
+
+ cairo_save(cr);
+ do_background(cr, fr);
+
+ /* Actually render the contents */
+ cairo_translate(cr, fr->pad_l, fr->pad_t);
+ for ( i=0; i<fr->n_paras; i++ ) {
+
+ double cur_h = paragraph_height(fr->paras[i]);
+
+ cairo_save(cr);
+ cairo_translate(cr, 0.0, hpos);
+
+ if ( (hpos + cur_h > min_y) && (hpos < max_y) ) {
+ render_paragraph(cr, fr->paras[i], is);
+ } /* else paragraph is not visible */
+
+ hpos += cur_h;
+ cairo_restore(cr);
+
+ }
+ cairo_restore(cr);
+
+ return 0;
+}
+
+
+int recursive_draw(struct frame *fr, cairo_t *cr,
+ ImageStore *is,
+ double min_y, double max_y)
+{
+ int i;
+
+ draw_frame(cr, fr, is, min_y, max_y);
+
+ for ( i=0; i<fr->num_children; i++ ) {
+ cairo_save(cr);
+ cairo_translate(cr, fr->children[i]->x, fr->children[i]->y);
+ recursive_draw(fr->children[i], cr, is,
+ min_y - fr->children[i]->y,
+ max_y - fr->children[i]->y);
+ cairo_restore(cr);
+ }
+
+ return 0;
+}
+
+
+void wrap_frame(struct frame *fr, PangoContext *pc)
+{
+ int i;
+ double w;
+
+ w = fr->w - fr->pad_l - fr->pad_r;
+
+ for ( i=0; i<fr->n_paras; i++ ) {
+ wrap_paragraph(fr->paras[i], pc, w, 0, 0);
+ }
+}
+
+
+int recursive_wrap(struct frame *fr, PangoContext *pc)
+{
+ int i;
+
+ wrap_frame(fr, pc);
+
+ for ( i=0; i<fr->num_children; i++ ) {
+ recursive_wrap(fr->children[i], pc);
+ }
+
+ return 0;
+}
+
+
+struct frame *interp_and_shape(SCBlock *scblocks, Stylesheet *stylesheet,
+ SCCallbackList *cbl, ImageStore *is,
+ int slide_number,
+ PangoContext *pc, double w, double h,
+ PangoLanguage *lang)
+{
+ SCInterpreter *scin;
+ char snum[64];
+ struct frame *top;
+
+ top = frame_new();
+ top->resizable = 0;
+ top->x = 0.0;
+ top->y = 0.0;
+ top->w = w;
+ top->h = h;
+ top->scblocks = scblocks;
+
+ scin = sc_interp_new(pc, lang, is, top);
+ if ( scin == NULL ) {
+ fprintf(stderr, "Failed to set up interpreter.\n");
+ frame_free(top);
+ return NULL;
+ }
+
+ sc_interp_set_callbacks(scin, cbl);
+
+ snprintf(snum, 63, "%i", slide_number);
+ sc_interp_set_constant(scin, SCCONST_SLIDENUMBER, snum);
+
+ top->fontdesc = pango_font_description_copy(sc_interp_get_fontdesc(scin));
+ top->col[0] = sc_interp_get_fgcol(scin)[0];
+ top->col[1] = sc_interp_get_fgcol(scin)[1];
+ top->col[2] = sc_interp_get_fgcol(scin)[2];
+ top->col[3] = sc_interp_get_fgcol(scin)[3];
+
+ sc_interp_add_block(scin, scblocks, stylesheet);
+
+ sc_interp_destroy(scin);
+
+ return top;
+}
+
+
+static struct frame *render_sc_with_context(SCBlock *scblocks,
+ cairo_t *cr, double log_w, double log_h,
+ Stylesheet *stylesheet, SCCallbackList *cbl,
+ ImageStore *is,
+ int slide_number, PangoLanguage *lang,
+ PangoContext *pc)
+{
+ struct frame *top;
+
+ cairo_rectangle(cr, 0.0, 0.0, log_w, log_h);
+ cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
+ cairo_fill(cr);
+
+ top = interp_and_shape(scblocks, stylesheet, cbl, is,
+ slide_number, pc, log_w, log_h, lang);
+
+ recursive_wrap(top, pc);
+
+ recursive_draw(top, cr, is, 0.0, log_h);
+
+ return top;
+}
+
+
+cairo_surface_t *render_sc(SCBlock *scblocks, int w, int h,
+ double log_w, double log_h,
+ Stylesheet *stylesheet, SCCallbackList *cbl,
+ ImageStore *is,
+ int slide_number, struct frame **ptop,
+ PangoLanguage *lang)
+{
+ cairo_surface_t *surf;
+ cairo_t *cr;
+ struct frame *top;
+ PangoContext *pc;
+
+ surf = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
+ cr = cairo_create(surf);
+ pc = pango_cairo_create_context(cr);
+ cairo_scale(cr, w/log_w, h/log_h);
+ top = render_sc_with_context(scblocks, cr, log_w, log_h,
+ stylesheet, cbl, is, slide_number,
+ lang, pc);
+ g_object_unref(pc);
+ cairo_destroy(cr);
+
+ *ptop = top;
+
+ return surf;
+}
+
+
+int export_pdf(struct presentation *p, const char *filename)
+{
+ double r;
+ double w = 2048.0;
+ double scale;
+ cairo_surface_t *surf;
+ cairo_t *cr;
+ SCBlock *bl;
+ int i;
+ PangoContext *pc;
+
+ r = p->slide_height / p->slide_width;
+
+ surf = cairo_pdf_surface_create(filename, w, w*r);
+ if ( cairo_surface_status(surf) != CAIRO_STATUS_SUCCESS ) {
+ fprintf(stderr, _("Couldn't create Cairo surface\n"));
+ return 1;
+ }
+
+ cr = cairo_create(surf);
+ scale = w / p->slide_width;
+ pc = pango_cairo_create_context(cr);
+
+ i = 1;
+ bl = p->scblocks;
+ while ( bl != NULL ) {
+
+ if ( safe_strcmp(sc_block_name(bl), "slide") != 0 ) {
+ bl = sc_block_next(bl);
+ continue;
+ }
+
+ cairo_save(cr);
+
+ cairo_scale(cr, scale, scale);
+
+ cairo_rectangle(cr, 0.0, 0.0, p->slide_width, p->slide_height);
+ cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
+ cairo_fill(cr);
+
+ render_sc_with_context(bl, cr, p->slide_width,
+ p->slide_height, p->stylesheet, NULL,
+ p->is, i, p->lang, pc);
+
+ cairo_restore(cr);
+
+ cairo_show_page(cr);
+
+ bl = sc_block_next(bl);
+ i++;
+
+ }
+
+ g_object_unref(pc);
+ cairo_surface_finish(surf);
+ cairo_destroy(cr);
+
+ return 0;
+}
diff --git a/src-old/render.h b/src-old/render.h
new file mode 100644
index 0000000..0cfae26
--- /dev/null
+++ b/src-old/render.h
@@ -0,0 +1,62 @@
+/*
+ * render.h
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef RENDER_H
+#define RENDER_H
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "presentation.h"
+#include "imagestore.h"
+#include "sc_interp.h"
+#include "frame.h"
+
+/* Convienience function to run the entire pipeline */
+extern cairo_surface_t *render_sc(SCBlock *scblocks, int w, int h,
+ double log_w, double log_h,
+ Stylesheet *stylesheet, SCCallbackList *cbl,
+ ImageStore *is,
+ int slide_number, struct frame **ptop,
+ PangoLanguage *lang);
+
+/* Interpret StoryCode and measure boxes.
+ * Needs to be followed by: wrap_contents() (recursively)
+ * recursive_draw()
+ */
+extern struct frame *interp_and_shape(SCBlock *scblocks, Stylesheet *stylesheet,
+ SCCallbackList *cbl,
+ ImageStore *is,
+ int slide_number, PangoContext *pc,
+ double w, double h, PangoLanguage *lang);
+
+extern void wrap_frame(struct frame *fr, PangoContext *pc);
+extern int recursive_wrap(struct frame *fr, PangoContext *pc);
+
+extern int export_pdf(struct presentation *p, const char *filename);
+
+extern int recursive_draw(struct frame *fr, cairo_t *cr,
+ ImageStore *is,
+ double min_y, double max_y);
+
+#endif /* RENDER_H */
diff --git a/src-old/sc_editor.c b/src-old/sc_editor.c
new file mode 100644
index 0000000..8e79fd8
--- /dev/null
+++ b/src-old/sc_editor.c
@@ -0,0 +1,2197 @@
+/*
+ * sc_editor.c
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdlib.h>
+#include <string.h>
+#include <gtk/gtk.h>
+#include <assert.h>
+#include <gdk/gdkkeysyms.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+#include <math.h>
+
+#include "colloquium.h"
+#include "presentation.h"
+#include "slide_window.h"
+#include "render.h"
+#include "frame.h"
+#include "sc_parse.h"
+#include "sc_interp.h"
+#include "sc_editor.h"
+#include "slideshow.h"
+#include "debugger.h"
+#include "utils.h"
+
+
+static void scroll_interface_init(GtkScrollable *iface)
+{
+}
+
+
+enum
+{
+ SCEDITOR_0,
+ SCEDITOR_VADJ,
+ SCEDITOR_HADJ,
+ SCEDITOR_VPOL,
+ SCEDITOR_HPOL,
+};
+
+
+G_DEFINE_TYPE_WITH_CODE(SCEditor, sc_editor, GTK_TYPE_DRAWING_AREA,
+ G_IMPLEMENT_INTERFACE(GTK_TYPE_SCROLLABLE,
+ scroll_interface_init))
+
+static void debug_paragraphs(SCEditor *e)
+{
+ struct frame *fr = e->cursor_frame;
+ int i;
+
+ printf("Paragraphs in current frame:\n");
+ for ( i=0; i<fr->n_paras; i++ ) {
+ show_para(fr->paras[i]);
+ }
+}
+
+
+static void horizontal_adjust(GtkAdjustment *adj, SCEditor *e)
+{
+ e->h_scroll_pos = gtk_adjustment_get_value(adj);
+ sc_editor_redraw(e);
+}
+
+
+static void set_horizontal_params(SCEditor *e)
+{
+ if ( e->hadj == NULL ) return;
+ gtk_adjustment_configure(e->hadj, e->h_scroll_pos, 0, e->w, 100,
+ e->visible_width, e->visible_width);
+}
+
+
+static void vertical_adjust(GtkAdjustment *adj, SCEditor *e)
+{
+ e->scroll_pos = gtk_adjustment_get_value(adj);
+ sc_editor_redraw(e);
+}
+
+
+static void set_vertical_params(SCEditor *e)
+{
+ double page;
+
+ if ( e->vadj == NULL ) return;
+
+ /* Ensure we do not scroll off the top of the document */
+ if ( e->scroll_pos < 0.0 ) e->scroll_pos = 0.0;
+
+ /* Ensure we do not scroll off the bottom of the document */
+ if ( e->scroll_pos > e->h - e->visible_height ) {
+ e->scroll_pos = e->h - e->visible_height;
+ }
+
+ /* If we can show the whole document, show it at the top */
+ if ( e->h < e->visible_height ) {
+ e->scroll_pos = 0.0;
+ }
+
+ if ( e->h > e->visible_height ) {
+ page = e->visible_height;
+ } else {
+ page = e->h;
+ }
+
+ gtk_adjustment_configure(e->vadj, e->scroll_pos, 0, e->h, 100,
+ e->visible_height, page);
+}
+
+
+static void update_size(SCEditor *e)
+{
+ if ( e->flow ) {
+
+ double total = total_height(e->top);
+
+ e->w = e->top->w;
+ e->h = total + e->top->pad_t + e->top->pad_b;
+
+ e->log_w = e->w;
+ e->log_h = e->h;
+ e->top->h = e->h;
+ } else {
+ e->top->w = e->log_w;
+ e->top->h = e->log_h;
+ }
+
+ if ( e->flow && (e->top->h < e->visible_height) ) {
+ e->top->h = e->visible_height;
+ }
+
+ set_vertical_params(e);
+ set_horizontal_params(e);
+}
+
+
+static gboolean resize_sig(GtkWidget *widget, GdkEventConfigure *event,
+ SCEditor *e)
+{
+ PangoContext *pc;
+
+ pc = gdk_pango_context_get();
+
+ if ( e->scale ) {
+
+ double sx, sy;
+ double aw, ah;
+
+ e->w = event->width;
+ e->h = event->height;
+ sx = (double)e->w / e->log_w;
+ sy = (double)e->h / e->log_h;
+ e->view_scale = (sx < sy) ? sx : sy;
+
+ /* Actual size (in device units) */
+ aw = e->view_scale * e->log_w;
+ ah = e->view_scale * e->log_h;
+
+ e->border_offs_x = (event->width - aw)/2.0;
+ e->border_offs_y = (event->height - ah)/2.0;
+
+ }
+
+ e->visible_height = event->height;
+ e->visible_width = event->width;
+
+ /* Interpret and shape, if not already done */
+ if ( e->top == NULL ) {
+ double w, h;
+ if ( e->flow ) {
+ w = event->width;
+ h = 0.0;
+ } else {
+ w = e->log_w;
+ h = e->log_h;
+ }
+ e->top = interp_and_shape(e->scblocks, e->stylesheet, e->cbl,
+ e->is, e->slidenum, pc,
+ w, h, e->lang);
+ e->top->scblocks = e->scblocks;
+ recursive_wrap(e->top, pc);
+ }
+
+ if ( e->flow ) {
+ /* Wrap using current width */
+ e->top->w = event->width;
+ e->top->h = 0.0; /* To be updated in a moment */
+ e->top->x = 0.0;
+ e->top->y = 0.0;
+ /* Only the top level needs to be wrapped */
+ wrap_frame(e->top, pc);
+ }
+
+ update_size(e);
+
+ g_object_unref(pc);
+
+ return FALSE;
+}
+
+
+static void emit_change_sig(SCEditor *e)
+{
+ g_signal_emit_by_name(e, "changed");
+}
+
+
+void sc_editor_set_flow(SCEditor *e, int flow)
+{
+ e->flow = flow;
+}
+
+
+static void sc_editor_set_property(GObject *obj, guint id, const GValue *val,
+ GParamSpec *spec)
+{
+ SCEditor *e = SC_EDITOR(obj);
+
+ switch ( id ) {
+
+ case SCEDITOR_VPOL :
+ e->vpol = g_value_get_enum(val);
+ break;
+
+ case SCEDITOR_HPOL :
+ e->hpol = g_value_get_enum(val);
+ break;
+
+ case SCEDITOR_VADJ :
+ e->vadj = g_value_get_object(val);
+ set_vertical_params(e);
+ if ( e->vadj != NULL ) {
+ g_signal_connect(G_OBJECT(e->vadj), "value-changed",
+ G_CALLBACK(vertical_adjust), e);
+ }
+ break;
+
+ case SCEDITOR_HADJ :
+ e->hadj = g_value_get_object(val);
+ set_horizontal_params(e);
+ if ( e->hadj != NULL ) {
+ g_signal_connect(G_OBJECT(e->hadj), "value-changed",
+ G_CALLBACK(horizontal_adjust), e);
+ }
+ break;
+
+ default :
+ printf("setting %i\n", id);
+ break;
+
+ }
+}
+
+
+static void sc_editor_get_property(GObject *obj, guint id, GValue *val,
+ GParamSpec *spec)
+{
+ SCEditor *e = SC_EDITOR(obj);
+
+ switch ( id ) {
+
+ case SCEDITOR_VADJ :
+ g_value_set_object(val, e->vadj);
+ break;
+
+ case SCEDITOR_HADJ :
+ g_value_set_object(val, e->hadj);
+ break;
+
+ case SCEDITOR_VPOL :
+ g_value_set_enum(val, e->vpol);
+ break;
+
+ case SCEDITOR_HPOL :
+ g_value_set_enum(val, e->hpol);
+ break;
+
+ default :
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, id, spec);
+ break;
+
+ }
+}
+
+
+static GtkSizeRequestMode get_request_mode(GtkWidget *widget)
+{
+ return GTK_SIZE_REQUEST_CONSTANT_SIZE;
+}
+
+
+static void get_preferred_width(GtkWidget *widget, gint *min, gint *natural)
+{
+ SCEditor *e = SC_EDITOR(widget);
+ if ( e->flow ) {
+ *min = 100;
+ *natural = 640;
+ } else {
+ *min = e->w;
+ *natural = e->w;
+ }
+}
+
+
+static void get_preferred_height(GtkWidget *widget, gint *min, gint *natural)
+{
+ SCEditor *e = SC_EDITOR(widget);
+ if ( e->flow ) {
+ *min = 1000;
+ *natural = 1000;
+ } else {
+ *min = e->h;
+ *natural = e->h;
+ }
+}
+
+
+static void sc_editor_class_init(SCEditorClass *klass)
+{
+ GObjectClass *goc = G_OBJECT_CLASS(klass);
+ goc->set_property = sc_editor_set_property;
+ goc->get_property = sc_editor_get_property;
+ g_object_class_override_property(goc, SCEDITOR_VADJ, "vadjustment");
+ g_object_class_override_property(goc, SCEDITOR_HADJ, "hadjustment");
+ g_object_class_override_property(goc, SCEDITOR_VPOL, "vscroll-policy");
+ g_object_class_override_property(goc, SCEDITOR_HPOL, "hscroll-policy");
+
+ GTK_WIDGET_CLASS(klass)->get_request_mode = get_request_mode;
+ GTK_WIDGET_CLASS(klass)->get_preferred_width = get_preferred_width;
+ GTK_WIDGET_CLASS(klass)->get_preferred_height = get_preferred_height;
+ GTK_WIDGET_CLASS(klass)->get_preferred_height_for_width = NULL;
+
+ g_signal_new("changed", SC_TYPE_EDITOR, G_SIGNAL_RUN_LAST, 0,
+ NULL, NULL, NULL, G_TYPE_NONE, 0);
+}
+
+
+static void sc_editor_init(SCEditor *e)
+{
+ e->vpol = GTK_SCROLL_NATURAL;
+ e->hpol = GTK_SCROLL_NATURAL;
+ e->vadj = gtk_adjustment_new(0, 0, 100, 1, 10, 10);
+ e->hadj = gtk_adjustment_new(0, 0, 100, 1, 10, 10);
+}
+
+
+void sc_editor_set_background(SCEditor *e, double r, double g, double b)
+{
+ e->bgcol[0] = r;
+ e->bgcol[1] = g;
+ e->bgcol[2] = b;
+}
+
+
+void sc_editor_ensure_cursor(SCEditor *e)
+{
+ if ( e->cursor_frame != NULL ) return;
+ e->cursor_frame = e->top;
+ e->cpos.para = 0;
+ e->cpos.pos = 0;
+ e->cpos.trail = 0;
+ e->selection = NULL;
+}
+
+
+static void sc_editor_remove_cursor(SCEditor *e)
+{
+ e->cursor_frame = NULL;
+ e->cpos.para = 0;
+ e->cpos.pos = 0;
+ e->cpos.trail = 0;
+ e->selection = NULL;
+}
+
+
+/* (Re-)run the entire rendering pipeline.
+ * NB "full" means "full". All frame, line and box handles will become
+ * invalid. The cursor position will be unset. */
+static void full_rerender(SCEditor *e)
+{
+ PangoContext *pc;
+
+ frame_free(e->top);
+ sc_editor_remove_cursor(e);
+
+ pc = gdk_pango_context_get();
+
+ e->top = interp_and_shape(e->scblocks, e->stylesheet, e->cbl,
+ e->is, e->slidenum,
+ pc, e->log_w, 0.0, e->lang);
+
+ e->top->x = 0.0;
+ e->top->y = 0.0;
+ e->top->w = e->w;
+ e->top->h = 0.0; /* To be updated in a moment */
+
+ recursive_wrap(e->top, pc);
+ update_size(e);
+
+ sc_editor_redraw(e);
+
+ g_object_unref(pc);
+}
+
+
+void sc_editor_redraw(SCEditor *e)
+{
+ gint w, h;
+
+ w = gtk_widget_get_allocated_width(GTK_WIDGET(e));
+ h = gtk_widget_get_allocated_height(GTK_WIDGET(e));
+
+ gtk_widget_queue_draw_area(GTK_WIDGET(e), 0, 0, w, h);
+}
+
+
+static void paste_storycode_received(GtkClipboard *cb, GtkSelectionData *seldata,
+ gpointer vp)
+{
+ SCEditor *e = vp;
+ SCBlock *nf;
+ const guchar *t;
+
+ t = gtk_selection_data_get_data(seldata);
+
+ printf("received storycode paste\n");
+ printf("'%s'\n", t);
+ if ( t == NULL ) return;
+
+ /* FIXME: It might not be a new frame */
+ nf = sc_parse((char *)t);
+ show_sc_blocks(nf);
+ sc_block_append_block(sc_block_child(e->scblocks), nf);
+ full_rerender(e);
+}
+
+
+static void paste_text_received(GtkClipboard *cb, GtkSelectionData *seldata,
+ gpointer vp)
+{
+ SCEditor *e = vp;
+ SCBlock *bl;
+ guchar *t;
+ SCBlock *cur_bl;
+ size_t cur_sc_pos;
+ size_t offs;
+ Paragraph *para;
+
+ t = gtk_selection_data_get_text(seldata);
+
+ printf("received text paste\n");
+ printf("'%s'\n", t);
+ if ( t == NULL ) return;
+
+ bl = sc_parse((char *)t);
+
+ if ( e->cursor_frame == NULL ) {
+ fprintf(stderr, _("No frame selected for paste\n"));
+ return;
+ }
+
+ para = e->cursor_frame->paras[e->cpos.para];
+ offs = pos_trail_to_offset(para, e->cpos.pos, e->cpos.trail);
+
+ get_sc_pos(e->cursor_frame, e->cpos.para, offs, &cur_bl, &cur_sc_pos);
+ sc_insert_block(cur_bl, cur_sc_pos, bl);
+ full_rerender(e);
+}
+
+
+static void paste_targets_received(GtkClipboard *cb, GdkAtom *targets,
+ gint n_targets, gpointer vp)
+{
+ SCEditor *e = vp;
+ int i;
+ int have_sc = 0;
+ int index_sc, index_text;
+ int have_text = 0;
+
+ if ( targets == NULL ) {
+ fprintf(stderr, "No paste targets offered.\n");
+ return;
+ }
+
+ for ( i=0; i<n_targets; i++ ) {
+ gchar *name = gdk_atom_name(targets[i]);
+ if ( g_strcmp0(name, "text/x-storycode") == 0 ) {
+ have_sc = 1;
+ index_sc = i;
+ }
+ if ( g_strcmp0(name, "text/plain") == 0 ) {
+ have_text = 1;
+ index_text = i;
+ }
+ g_free(name);
+ }
+
+ if ( have_sc ) {
+ printf("storycode is offered\n");
+ gtk_clipboard_request_contents(cb, targets[index_sc],
+ paste_storycode_received, e);
+ } else if ( have_text ) {
+ printf("text is offered\n");
+ gtk_clipboard_request_contents(cb, targets[index_text],
+ paste_text_received, e);
+ } else {
+ printf("nothing useful is offered\n");
+ }
+}
+
+
+void sc_editor_paste(SCEditor *e)
+{
+ GtkClipboard *cb;
+ GdkAtom atom;
+
+ printf("pasting\n");
+
+ atom = gdk_atom_intern("CLIPBOARD", FALSE);
+ if ( atom == GDK_NONE ) return;
+ cb = gtk_clipboard_get(atom);
+ gtk_clipboard_request_targets(cb, paste_targets_received, e);
+}
+
+
+void sc_editor_add_storycode(SCEditor *e, const char *sc)
+{
+ SCBlock *nf;
+ nf = sc_parse(sc);
+ sc_block_append_block(sc_block_child(e->scblocks), nf);
+ full_rerender(e);
+}
+
+
+static void clipboard_get(GtkClipboard *cb, GtkSelectionData *seldata,
+ guint info, gpointer data)
+{
+ char *t = data;
+
+ printf("clipboard get\n");
+
+ if ( info == 0 ) {
+ printf("sending SC frame\n");
+ gtk_selection_data_set(seldata,
+ gtk_selection_data_get_target(seldata),
+ 8, (const guchar *)t, strlen(t)+1);
+ } else {
+ GdkAtom target;
+ gchar *name;
+ target = gtk_selection_data_get_target(seldata);
+ name = gdk_atom_name(target);
+ fprintf(stderr, "Don't know what to send for %s\n", name);
+ g_free(name);
+ }
+}
+
+
+static void clipboard_clear(GtkClipboard *cb, gpointer data)
+{
+ free(data);
+}
+
+
+void sc_editor_copy_selected_frame(SCEditor *e)
+{
+ char *t;
+ GtkClipboard *cb;
+ GdkAtom atom;
+ GtkTargetEntry targets[1];
+
+ if ( e->selection == NULL ) return;
+
+ atom = gdk_atom_intern("CLIPBOARD", FALSE);
+ if ( atom == GDK_NONE ) return;
+
+ cb = gtk_clipboard_get(atom);
+
+ targets[0].target = "text/x-storycode";
+ targets[0].flags = 0;
+ targets[0].info = 0;
+
+ /* FIXME: Offer image, PDF etc? */
+
+ printf("copying frame\n");
+
+ t = serialise_sc_block(e->selection->scblocks);
+
+ gtk_clipboard_set_with_data(cb, targets, 1,
+ clipboard_get, clipboard_clear, t);
+}
+
+
+static void copy_selection(SCEditor *e)
+{
+ char *t;
+ GtkClipboard *cb;
+ GdkAtom atom;
+ GtkTargetEntry targets[1];
+ SCBlock *bl;
+
+ if ( e->selection == NULL ) return;
+
+ atom = gdk_atom_intern("CLIPBOARD", FALSE);
+ if ( atom == GDK_NONE ) return;
+
+ cb = gtk_clipboard_get(atom);
+
+
+ targets[0].target = "text/x-storycode";
+ targets[0].flags = 0;
+ targets[0].info = 0;
+
+ printf("copying selection\n");
+
+ bl = block_at_cursor(e->cursor_frame, e->cpos.para, 0);
+ if ( bl == NULL ) return;
+
+ t = serialise_sc_block(bl);
+
+ gtk_clipboard_set_with_data(cb, targets, 1,
+ clipboard_get, clipboard_clear, t);
+}
+
+
+void sc_editor_delete_selected_frame(SCEditor *e)
+{
+ SCBlock *scb_old = e->scblocks;
+ sc_block_delete(&e->scblocks, e->selection->scblocks);
+ assert(scb_old == e->scblocks);
+ full_rerender(e);
+ emit_change_sig(e);
+}
+
+
+static gint destroy_sig(GtkWidget *window, SCEditor *e)
+{
+ return 0;
+}
+
+
+static void draw_editing_box(cairo_t *cr, struct frame *fr)
+{
+ const double dash[] = {2.0, 2.0};
+ double xmin, ymin, width, height;
+ double ptot_w, ptot_h;
+
+ xmin = fr->x;
+ ymin = fr->y;
+ width = fr->w;
+ height = fr->h;
+
+ cairo_new_path(cr);
+ cairo_rectangle(cr, xmin, ymin, width, height);
+ cairo_set_source_rgb(cr, 0.0, 0.69, 1.0);
+ cairo_set_line_width(cr, 0.5);
+ cairo_stroke(cr);
+
+ cairo_new_path(cr);
+ ptot_w = fr->pad_l + fr->pad_r;
+ ptot_h = fr->pad_t + fr->pad_b;
+ cairo_rectangle(cr, xmin+fr->pad_l, ymin+fr->pad_t,
+ width-ptot_w, height-ptot_h);
+ cairo_set_dash(cr, dash, 2, 0.0);
+ cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
+ cairo_set_line_width(cr, 0.1);
+ cairo_stroke(cr);
+
+ cairo_set_dash(cr, NULL, 0, 0.0);
+}
+
+
+static void draw_para_highlight(cairo_t *cr, struct frame *fr, int cursor_para)
+{
+ double cx, cy, w, h;
+
+ if ( get_para_highlight(fr, cursor_para, &cx, &cy, &w, &h) != 0 ) {
+ return;
+ }
+
+ cairo_new_path(cr);
+ cairo_rectangle(cr, cx+fr->x, cy+fr->y, w, h);
+ cairo_set_source_rgba(cr, 0.7, 0.7, 1.0, 0.5);
+ cairo_set_line_width(cr, 5.0);
+ cairo_stroke(cr);
+}
+
+
+static void draw_caret(cairo_t *cr, struct frame *fr, struct edit_pos cpos,
+ int hgh)
+{
+ double cx, clow, chigh, h;
+ const double t = 1.8;
+ size_t offs;
+ Paragraph *para;
+
+ if ( hgh ) {
+ draw_para_highlight(cr, fr, cpos.para);
+ return;
+ }
+
+ assert(fr != NULL);
+
+ para = fr->paras[cpos.para];
+ if ( para_type(para) != PARA_TYPE_TEXT ) {
+ draw_para_highlight(cr, fr, cpos.para);
+ return;
+ }
+
+ offs = pos_trail_to_offset(para, cpos.pos, cpos.trail);
+ get_cursor_pos(fr, cpos.para, offs, &cx, &clow, &h);
+
+ cx += fr->x;
+ clow += fr->y;
+ chigh = clow + h;
+
+ cairo_move_to(cr, cx, clow);
+ cairo_line_to(cr, cx, chigh);
+
+ cairo_move_to(cr, cx-t, clow-t);
+ cairo_line_to(cr, cx, clow);
+ cairo_move_to(cr, cx+t, clow-t);
+ cairo_line_to(cr, cx, clow);
+
+ cairo_move_to(cr, cx-t, chigh+t);
+ cairo_line_to(cr, cx, chigh);
+ cairo_move_to(cr, cx+t, chigh+t);
+ cairo_line_to(cr, cx, chigh);
+
+ cairo_set_source_rgb(cr, 0.86, 0.0, 0.0);
+ cairo_set_line_width(cr, 1.0);
+ cairo_stroke(cr);
+}
+
+
+static void draw_resize_handle(cairo_t *cr, double x, double y)
+{
+ cairo_new_path(cr);
+ cairo_rectangle(cr, x, y, 20.0, 20.0);
+ cairo_set_source_rgba(cr, 0.9, 0.9, 0.9, 0.5);
+ cairo_fill(cr);
+}
+
+
+static void draw_overlay(cairo_t *cr, SCEditor *e)
+{
+ if ( e->selection != NULL ) {
+
+ double x, y, w, h;
+
+ draw_editing_box(cr, e->selection);
+
+ x = e->selection->x;
+ y = e->selection->y;
+ w = e->selection->w;
+ h = e->selection->h;
+
+ if ( e->selection->resizable ) {
+ /* Draw resize handles */
+ draw_resize_handle(cr, x, y+h-20.0);
+ draw_resize_handle(cr, x+w-20.0, y);
+ draw_resize_handle(cr, x, y);
+ draw_resize_handle(cr, x+w-20.0, y+h-20.0);
+ }
+
+ draw_caret(cr, e->cursor_frame, e->cpos, e->para_highlight);
+
+ }
+
+ if ( (e->drag_status == DRAG_STATUS_DRAGGING)
+ && ((e->drag_reason == DRAG_REASON_CREATE)
+ || (e->drag_reason == DRAG_REASON_IMPORT)) )
+ {
+ cairo_new_path(cr);
+ cairo_rectangle(cr, e->start_corner_x, e->start_corner_y,
+ e->drag_corner_x - e->start_corner_x,
+ e->drag_corner_y - e->start_corner_y);
+ cairo_set_source_rgb(cr, 0.5, 0.5, 0.5);
+ cairo_set_line_width(cr, 0.5);
+ cairo_stroke(cr);
+ }
+
+ if ( (e->drag_status == DRAG_STATUS_DRAGGING)
+ && ((e->drag_reason == DRAG_REASON_RESIZE)
+ || (e->drag_reason == DRAG_REASON_MOVE)) )
+ {
+ cairo_new_path(cr);
+ cairo_rectangle(cr, e->box_x, e->box_y,
+ e->box_width, e->box_height);
+ cairo_set_source_rgb(cr, 0.5, 0.5, 0.5);
+ cairo_set_line_width(cr, 0.5);
+ cairo_stroke(cr);
+ }
+}
+
+
+static gboolean draw_sig(GtkWidget *da, cairo_t *cr, SCEditor *e)
+{
+ /* Ultimate background */
+ if ( e->bg_pixbuf != NULL ) {
+ gdk_cairo_set_source_pixbuf(cr, e->bg_pixbuf, 0.0, 0.0);
+ cairo_pattern_t *patt = cairo_get_source(cr);
+ cairo_pattern_set_extend(patt, CAIRO_EXTEND_REPEAT);
+ cairo_paint(cr);
+ } else {
+ cairo_set_source_rgba(cr, 0.8, 0.8, 1.0, 1.0);
+ cairo_paint(cr);
+ }
+
+ cairo_translate(cr, e->border_offs_x, e->border_offs_y);
+ cairo_translate(cr, -e->h_scroll_pos, -e->scroll_pos);
+ cairo_scale(cr, e->view_scale, e->view_scale);
+
+ /* Rendering background */
+ cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 1.0);
+ cairo_rectangle(cr, 0.0, 0.0, e->log_w, e->log_h);
+ cairo_fill(cr);
+
+ /* Contents */
+ recursive_draw(e->top, cr, e->is,
+ e->scroll_pos/e->view_scale,
+ (e->scroll_pos + e->visible_height)/e->view_scale);
+
+ /* Editing overlay */
+ draw_overlay(cr, e);
+
+ return FALSE;
+}
+
+
+SCBlock *split_paragraph_at_cursor(SCEditor *e)
+{
+ size_t offs;
+ Paragraph *para;
+
+ if ( e->cursor_frame == NULL ) return NULL;
+
+ para = e->cursor_frame->paras[e->cpos.para];
+ offs = pos_trail_to_offset(para, e->cpos.pos, e->cpos.trail);
+ return split_paragraph(e->cursor_frame, e->cpos.para, offs, e->pc);
+}
+
+
+static void check_cursor_visible(SCEditor *e)
+{
+ double x, y, h;
+ size_t offs;
+ Paragraph *para;
+
+ if ( e->cursor_frame == NULL ) return;
+
+ para = e->cursor_frame->paras[e->cpos.para];
+ offs = pos_trail_to_offset(para, e->cpos.pos, e->cpos.trail);
+ get_cursor_pos(e->cursor_frame, e->cpos.para, offs, &x, &y, &h);
+
+ /* Off the bottom? */
+ if ( y - e->scroll_pos + h > e->visible_height ) {
+ e->scroll_pos = y + h - e->visible_height;
+ e->scroll_pos += e->cursor_frame->pad_b;
+ }
+
+ /* Off the top? */
+ if ( y < e->scroll_pos ) {
+ e->scroll_pos = y - e->cursor_frame->pad_t;
+ }
+}
+
+
+static void do_backspace(struct frame *fr, SCEditor *e)
+{
+ double wrapw = e->cursor_frame->w - e->cursor_frame->pad_l - e->cursor_frame->pad_r;
+
+ if ( e->sel_active ) {
+
+ /* Delete the selected block */
+ delete_text_from_frame(e->cursor_frame, e->sel_start, e->sel_end, wrapw);
+
+ /* Cursor goes at start of deletion */
+ sort_positions(&e->sel_start, &e->sel_end);
+ e->cpos = e->sel_start;
+ e->sel_active = 0;
+
+ } else {
+
+ if ( para_type(e->cursor_frame->paras[e->cpos.para]) == PARA_TYPE_TEXT ) {
+
+ /* Delete one character */
+ struct edit_pos p1, p2;
+
+ p1 = e->cpos;
+
+ p2 = p1;
+
+ cursor_moveh(e->cursor_frame, &p2, -1);
+ show_edit_pos(p1);
+ show_edit_pos(p2);
+
+ delete_text_from_frame(e->cursor_frame, p1, p2, wrapw);
+ e->cpos = p2;
+
+ } else {
+
+ /* FIXME: Implement this */
+ fprintf(stderr, "Deleting non-text paragraph\n");
+
+ }
+
+ }
+
+ emit_change_sig(e);
+ sc_editor_redraw(e);
+}
+
+
+static void insert_text(char *t, SCEditor *e)
+{
+ Paragraph *para;
+
+ if ( e->cursor_frame == NULL ) return;
+
+ if ( e->sel_active ) {
+ do_backspace(e->cursor_frame, e);
+ }
+
+ if ( strcmp(t, "\n") == 0 ) {
+ split_paragraph_at_cursor(e);
+ if ( e->flow ) update_size(e);
+ cursor_moveh(e->cursor_frame, &e->cpos, +1);
+ check_cursor_visible(e);
+ emit_change_sig(e);
+ sc_editor_redraw(e);
+ return;
+ }
+
+ para = e->cursor_frame->paras[e->cpos.para];
+
+ /* Is this paragraph even a text one? */
+ if ( para_type(para) == PARA_TYPE_TEXT ) {
+
+ size_t off;
+
+ /* Yes. The "easy" case */
+
+ if ( !position_editable(e->cursor_frame, e->cpos) ) {
+ fprintf(stderr, "Position not editable\n");
+ return;
+ }
+
+ off = pos_trail_to_offset(para, e->cpos.pos, e->cpos.trail);
+ insert_text_in_paragraph(para, off, t);
+ wrap_paragraph(para, NULL,
+ e->cursor_frame->w - e->cursor_frame->pad_l
+ - e->cursor_frame->pad_r, 0, 0);
+ if ( e->flow ) update_size(e);
+
+ cursor_moveh(e->cursor_frame, &e->cpos, +1);
+
+ } else {
+
+ SCBlock *bd;
+ SCBlock *ad;
+ Paragraph *pnew;
+
+ bd = para_scblock(para);
+ if ( bd == NULL ) {
+ fprintf(stderr, "No SCBlock for para\n");
+ return;
+ }
+
+ /* No. Create a new text paragraph straight afterwards */
+ ad = sc_block_insert_after(bd, NULL, NULL, strdup(t));
+ if ( ad == NULL ) {
+ fprintf(stderr, "Failed to add SCBlock\n");
+ return;
+ }
+
+ pnew = insert_paragraph(e->cursor_frame, e->cpos.para);
+ if ( pnew == NULL ) {
+ fprintf(stderr, "Failed to insert paragraph\n");
+ return;
+ }
+ add_run(pnew, ad, e->cursor_frame->fontdesc,
+ e->cursor_frame->col, NULL);
+
+ wrap_frame(e->cursor_frame, e->pc);
+
+ e->cpos.para += 1;
+ e->cpos.pos = 0;
+ e->cpos.trail = 1;
+
+ }
+
+ emit_change_sig(e);
+ check_cursor_visible(e);
+ sc_editor_redraw(e);
+}
+
+
+static gboolean im_commit_sig(GtkIMContext *im, gchar *str,
+ SCEditor *e)
+{
+ insert_text(str, e);
+ return FALSE;
+}
+
+
+static int within_frame(struct frame *fr, double x, double y)
+{
+ if ( fr == NULL ) return 0;
+ if ( x < fr->x ) return 0;
+ if ( y < fr->y ) return 0;
+ if ( x > fr->x + fr->w ) return 0;
+ if ( y > fr->y + fr->h ) return 0;
+ return 1;
+}
+
+
+static struct frame *find_frame_at_position(struct frame *fr,
+ double x, double y)
+{
+ int i;
+
+ for ( i=0; i<fr->num_children; i++ ) {
+
+ if ( within_frame(fr->children[i], x, y) ) {
+ return find_frame_at_position(fr->children[i], x, y);
+ }
+
+ }
+
+ if ( within_frame(fr, x, y) ) return fr;
+ return NULL;
+}
+
+
+static enum corner which_corner(double xp, double yp, struct frame *fr)
+{
+ double x, y; /* Relative to object position */
+
+ x = xp - fr->x;
+ y = yp - fr->y;
+
+ if ( x < 0.0 ) return CORNER_NONE;
+ if ( y < 0.0 ) return CORNER_NONE;
+ if ( x > fr->w ) return CORNER_NONE;
+ if ( y > fr->h ) return CORNER_NONE;
+
+ /* Top left? */
+ if ( (x<20.0) && (y<20.0) ) return CORNER_TL;
+ if ( (x>fr->w-20.0) && (y<20.0) ) return CORNER_TR;
+ if ( (x<20.0) && (y>fr->h-20.0) ) return CORNER_BL;
+ if ( (x>fr->w-20.0) && (y>fr->h-20.0) ) return CORNER_BR;
+
+ return CORNER_NONE;
+}
+
+
+static void calculate_box_size(struct frame *fr, SCEditor *e,
+ double x, double y)
+{
+ double ddx, ddy, dlen, mult;
+ double vx, vy, dbx, dby;
+
+ ddx = x - e->start_corner_x;
+ ddy = y - e->start_corner_y;
+
+ if ( !fr->is_image ) {
+
+ switch ( e->drag_corner ) {
+
+ case CORNER_BR :
+ e->box_x = fr->x;
+ e->box_y = fr->y;
+ e->box_width = fr->w + ddx;
+ e->box_height = fr->h + ddy;
+ break;
+
+ case CORNER_BL :
+ e->box_x = fr->x + ddx;
+ e->box_y = fr->y;
+ e->box_width = fr->w - ddx;
+ e->box_height = fr->h + ddy;
+ break;
+
+ case CORNER_TL :
+ e->box_x = fr->x + ddx;
+ e->box_y = fr->y + ddy;
+ e->box_width = fr->w - ddx;
+ e->box_height = fr->h - ddy;
+ break;
+
+ case CORNER_TR :
+ e->box_x = fr->x;
+ e->box_y = fr->y + ddy;
+ e->box_width = fr->w + ddx;
+ e->box_height = fr->h - ddy;
+ break;
+
+ case CORNER_NONE :
+ break;
+
+ }
+ return;
+
+
+ }
+
+ switch ( e->drag_corner ) {
+
+ case CORNER_BR :
+ vx = fr->w;
+ vy = fr->h;
+ break;
+
+ case CORNER_BL :
+ vx = -fr->w;
+ vy = fr->h;
+ break;
+
+ case CORNER_TL :
+ vx = -fr->w;
+ vy = -fr->h;
+ break;
+
+ case CORNER_TR :
+ vx = fr->w;
+ vy = -fr->h;
+ break;
+
+ case CORNER_NONE :
+ default:
+ vx = 0.0;
+ vy = 0.0;
+ break;
+
+ }
+
+ dlen = (ddx*vx + ddy*vy) / e->diagonal_length;
+ mult = (dlen+e->diagonal_length) / e->diagonal_length;
+
+ e->box_width = fr->w * mult;
+ e->box_height = fr->h * mult;
+ dbx = e->box_width - fr->w;
+ dby = e->box_height - fr->h;
+
+ if ( e->box_width < 40.0 ) {
+ mult = 40.0 / fr->w;
+ }
+ if ( e->box_height < 40.0 ) {
+ mult = 40.0 / fr->h;
+ }
+ e->box_width = fr->w * mult;
+ e->box_height = fr->h * mult;
+ dbx = e->box_width - fr->w;
+ dby = e->box_height - fr->h;
+
+ switch ( e->drag_corner ) {
+
+ case CORNER_BR :
+ e->box_x = fr->x;
+ e->box_y = fr->y;
+ break;
+
+ case CORNER_BL :
+ e->box_x = fr->x - dbx;
+ e->box_y = fr->y;
+ break;
+
+ case CORNER_TL :
+ e->box_x = fr->x - dbx;
+ e->box_y = fr->y - dby;
+ break;
+
+ case CORNER_TR :
+ e->box_x = fr->x;
+ e->box_y = fr->y - dby;
+ break;
+
+ case CORNER_NONE :
+ break;
+
+ }
+}
+
+
+static void check_paragraph(struct frame *fr, PangoContext *pc,
+ SCBlock *scblocks)
+{
+ if ( fr->n_paras > 0 ) return;
+ Paragraph *para = last_para(fr);
+
+ if ( scblocks == NULL ) {
+ /* We have no SCBlocks at all! Better create one... */
+ scblocks = sc_parse("");
+ fr->scblocks = scblocks;
+ }
+
+ /* We are creating the first paragraph. It uses the last SCBlock
+ * in the chain */
+ while ( sc_block_next(scblocks) != NULL ) {
+ scblocks = sc_block_next(scblocks);
+ }
+ scblocks = sc_block_append(scblocks, NULL, NULL, strdup(""), NULL);
+
+ add_run(para, scblocks, fr->fontdesc, fr->col, NULL);
+ wrap_paragraph(para, pc, fr->w - fr->pad_l - fr->pad_r, 0, 0);
+}
+
+
+static void rewrap_paragraph_range(struct frame *fr, int a, int b,
+ struct edit_pos sel_start,
+ struct edit_pos sel_end,
+ int sel_active)
+{
+ int i;
+ int sel_s, sel_e;
+ Paragraph *para;
+
+ if ( a > b ) {
+ int t = a;
+ a = b; b = t;
+ }
+
+ if ( fr == NULL ) return;
+ if ( fr->paras == NULL ) return;
+
+ sort_positions(&sel_start, &sel_end);
+
+ //printf("frame %p\n", fr);
+ //printf("start: ");
+ //show_edit_pos(sel_start);
+ //printf(" end: ");
+ //show_edit_pos(sel_end);
+
+ para = fr->paras[sel_start.para];
+ sel_s = pos_trail_to_offset(para, sel_start.pos, sel_start.trail);
+ para = fr->paras[sel_end.para];
+ sel_e = pos_trail_to_offset(para, sel_end.pos, sel_end.trail);
+
+ for ( i=a; i<=b; i++ ) {
+ size_t srt, end;
+ if ( sel_active ) {
+ if ( i == sel_start.para ) {
+ srt = sel_s;
+ } else {
+ srt = 0;
+ }
+ if ( i == sel_end.para ) {
+ end = sel_e;
+ } else {
+ end = G_MAXUINT;
+ }
+ if ( i > sel_start.para && i < sel_end.para ) {
+ end = G_MAXUINT;
+ }
+ } else {
+ srt = 0;
+ end = 0;
+ }
+ wrap_paragraph(fr->paras[i], NULL,
+ fr->w - fr->pad_l - fr->pad_r, srt, end);
+ }
+}
+
+
+static void unset_selection(SCEditor *e)
+{
+ int a, b;
+
+ if ( !e->sel_active ) return;
+
+ a = e->sel_start.para;
+ b = e->sel_end.para;
+ if ( a > b ) {
+ a = e->sel_end.para;
+ b = e->sel_start.para;
+ }
+ e->sel_active = 0;
+ rewrap_paragraph_range(e->cursor_frame, a, b, e->sel_start, e->sel_end, 0);
+}
+
+
+static gboolean button_press_sig(GtkWidget *da, GdkEventButton *event,
+ SCEditor *e)
+{
+ enum corner c;
+ gdouble x, y;
+ struct frame *clicked;
+ int shift;
+
+ x = event->x - e->border_offs_x;
+ y = event->y - e->border_offs_y + e->scroll_pos;
+ x /= e->view_scale;
+ y /= e->view_scale;
+ shift = event->state & GDK_SHIFT_MASK;
+
+ if ( within_frame(e->selection, x, y) ) {
+ clicked = e->selection;
+ } else {
+ clicked = find_frame_at_position(e->top, x, y);
+ }
+
+ /* Clicked within the currently selected frame
+ * -> resize, move or select text */
+ if ( (e->selection != NULL) && (clicked == e->selection) ) {
+
+ struct frame *fr;
+
+ fr = e->selection;
+
+ /* Within the resizing region? */
+ c = which_corner(x, y, fr);
+ if ( (c != CORNER_NONE) && fr->resizable && shift ) {
+
+ e->drag_reason = DRAG_REASON_RESIZE;
+ e->drag_corner = c;
+
+ e->start_corner_x = x;
+ e->start_corner_y = y;
+ e->diagonal_length = pow(fr->w, 2.0);
+ e->diagonal_length += pow(fr->h, 2.0);
+ e->diagonal_length = sqrt(e->diagonal_length);
+
+ calculate_box_size(fr, e, x, y);
+
+ e->drag_status = DRAG_STATUS_COULD_DRAG;
+ e->drag_reason = DRAG_REASON_RESIZE;
+
+ } else {
+
+ /* Position cursor and prepare for possible drag */
+ e->cursor_frame = clicked;
+ check_paragraph(e->cursor_frame, e->pc, sc_block_child(fr->scblocks));
+ find_cursor(clicked, x-fr->x, y-fr->y, &e->cpos);
+ ensure_run(e->cursor_frame, e->cpos);
+
+ e->start_corner_x = x;
+ e->start_corner_y = y;
+
+ if ( event->type == GDK_2BUTTON_PRESS ) {
+ check_callback_click(e->cursor_frame, e->cpos.para);
+ }
+
+ if ( fr->resizable && shift ) {
+ e->drag_status = DRAG_STATUS_COULD_DRAG;
+ e->drag_reason = DRAG_REASON_MOVE;
+ } else if ( !e->para_highlight ) {
+ e->drag_status = DRAG_STATUS_COULD_DRAG;
+ e->drag_reason = DRAG_REASON_TEXTSEL;
+ unset_selection(e);
+ find_cursor(clicked, x-fr->x, y-fr->y, &e->sel_start);
+ }
+
+ }
+
+ } else if ( (clicked == NULL)
+ || ( !e->top_editable && (clicked == e->top) ) )
+ {
+ /* Clicked no object. Deselect old object.
+ * If shift held, set up for creating a new one. */
+ e->selection = NULL;
+ unset_selection(e);
+
+ if ( shift ) {
+ e->start_corner_x = x;
+ e->start_corner_y = y;
+ e->drag_status = DRAG_STATUS_COULD_DRAG;
+ e->drag_reason = DRAG_REASON_CREATE;
+ } else {
+ e->drag_status = DRAG_STATUS_NONE;
+ e->drag_reason = DRAG_REASON_NONE;
+ }
+
+ } else {
+
+ /* Clicked an existing frame, no immediate dragging */
+ e->drag_status = DRAG_STATUS_COULD_DRAG;
+ e->drag_reason = DRAG_REASON_TEXTSEL;
+ unset_selection(e);
+ find_cursor(clicked, x-clicked->x, y-clicked->y,
+ &e->sel_start);
+ find_cursor(clicked, x-clicked->x, y-clicked->y,
+ &e->sel_end);
+ e->selection = clicked;
+ e->cursor_frame = clicked;
+ if ( clicked == e->top ) {
+ check_paragraph(e->cursor_frame, e->pc, clicked->scblocks);
+ } else {
+ check_paragraph(e->cursor_frame, e->pc,
+ sc_block_child(clicked->scblocks));
+ }
+ find_cursor(clicked, x-clicked->x, y-clicked->y, &e->cpos);
+ ensure_run(e->cursor_frame, e->cpos);
+
+ }
+
+ gtk_widget_grab_focus(GTK_WIDGET(da));
+ sc_editor_redraw(e);
+ return FALSE;
+}
+
+
+static gboolean motion_sig(GtkWidget *da, GdkEventMotion *event,
+ SCEditor *e)
+{
+ struct frame *fr = e->selection;
+ gdouble x, y;
+
+ x = event->x - e->border_offs_x;
+ y = event->y - e->border_offs_y + e->scroll_pos;
+ x /= e->view_scale;
+ y /= e->view_scale;
+
+ if ( e->drag_status == DRAG_STATUS_COULD_DRAG ) {
+
+ /* We just got a motion signal, and the status was "could drag",
+ * therefore the drag has started. */
+ e->drag_status = DRAG_STATUS_DRAGGING;
+
+ }
+
+ switch ( e->drag_reason ) {
+
+ case DRAG_REASON_NONE :
+ break;
+
+ case DRAG_REASON_CREATE :
+ e->drag_corner_x = x;
+ e->drag_corner_y = y;
+ sc_editor_redraw(e);
+ break;
+
+ case DRAG_REASON_IMPORT :
+ /* Do nothing, handled by dnd_motion() */
+ break;
+
+ case DRAG_REASON_RESIZE :
+ calculate_box_size(fr, e, x, y);
+ sc_editor_redraw(e);
+ break;
+
+ case DRAG_REASON_MOVE :
+ e->box_x = (fr->x - e->start_corner_x) + x;
+ e->box_y = (fr->y - e->start_corner_y) + y;
+ e->box_width = fr->w;
+ e->box_height = fr->h;
+ sc_editor_redraw(e);
+ break;
+
+ case DRAG_REASON_TEXTSEL :
+ unset_selection(e);
+ find_cursor(fr, x-fr->x, y-fr->y, &e->sel_end);
+ rewrap_paragraph_range(fr, e->sel_start.para, e->sel_end.para,
+ e->sel_start, e->sel_end, 1);
+ find_cursor(fr, x-fr->x, y-fr->y, &e->cpos);
+ e->sel_active = !positions_equal(e->sel_start, e->sel_end);
+ sc_editor_redraw(e);
+ break;
+
+ }
+
+ gdk_event_request_motions(event);
+ return FALSE;
+}
+
+
+static struct frame *create_frame(SCEditor *e, double x, double y,
+ double w, double h)
+{
+ struct frame *parent;
+ struct frame *fr;
+ SCBlock *scblocks;
+
+ parent = e->top;
+
+ if ( w < 0.0 ) {
+ x += w;
+ w = -w;
+ }
+
+ if ( h < 0.0 ) {
+ y += h;
+ h = -h;
+ }
+
+ /* Add to frame structure */
+ fr = add_subframe(parent);
+
+ /* Add to SC */
+ scblocks = sc_block_append_end(sc_block_child(e->scblocks), "f", NULL, NULL);
+ fr->scblocks = scblocks;
+ sc_block_append_inside(scblocks, NULL, NULL, strdup(""));
+
+ fr->x = x;
+ fr->y = y;
+ fr->w = w;
+ fr->h = h;
+ fr->is_image = 0;
+ fr->empty = 1;
+ fr->resizable = 1;
+
+ update_geom(fr);
+
+ full_rerender(e);
+ return find_frame_with_scblocks(e->top, scblocks);
+}
+
+
+static void do_resize(SCEditor *e, double x, double y, double w, double h)
+{
+ struct frame *fr;
+
+ assert(e->selection != NULL);
+
+ if ( w < 0.0 ) {
+ w = -w;
+ x -= w;
+ }
+
+ if ( h < 0.0 ) {
+ h = -h;
+ y -= h;
+ }
+
+ fr = e->selection;
+ fr->x = x;
+ fr->y = y;
+ fr->w = w;
+ fr->h = h;
+ update_geom(fr);
+
+ full_rerender(e);
+ sc_editor_redraw(e);
+}
+
+
+static gboolean button_release_sig(GtkWidget *da, GdkEventButton *event,
+ SCEditor *e)
+{
+ gdouble x, y;
+ struct frame *fr;
+
+ x = event->x - e->border_offs_x;
+ y = event->y - e->border_offs_y;
+ x /= e->view_scale;
+ y /= e->view_scale;
+
+ /* Not dragging? Then I don't care. */
+ if ( e->drag_status != DRAG_STATUS_DRAGGING ) return FALSE;
+
+ e->drag_corner_x = x;
+ e->drag_corner_y = y;
+ e->drag_status = DRAG_STATUS_NONE;
+
+ switch ( e->drag_reason )
+ {
+
+ case DRAG_REASON_NONE :
+ printf("Release on pointless drag.\n");
+ break;
+
+ case DRAG_REASON_CREATE :
+ fr = create_frame(e, e->start_corner_x, e->start_corner_y,
+ e->drag_corner_x - e->start_corner_x,
+ e->drag_corner_y - e->start_corner_y);
+ if ( fr != NULL ) {
+ check_paragraph(fr, e->pc, sc_block_child(fr->scblocks));
+ e->selection = fr;
+ e->cursor_frame = fr;
+ e->cpos.para = 0;
+ e->cpos.pos = 0;
+ e->cpos.trail = 0;
+ } else {
+ fprintf(stderr, _("Failed to create frame!\n"));
+ }
+ break;
+
+ case DRAG_REASON_IMPORT :
+ /* Do nothing, handled in dnd_drop() or dnd_leave() */
+ break;
+
+ case DRAG_REASON_RESIZE :
+ do_resize(e, e->box_x, e->box_y, e->box_width, e->box_height);
+ break;
+
+ case DRAG_REASON_MOVE :
+ do_resize(e, e->box_x, e->box_y, e->box_width, e->box_height);
+ break;
+
+ case DRAG_REASON_TEXTSEL :
+ /* Do nothing (text is already selected) */
+ break;
+
+ }
+
+ e->drag_reason = DRAG_REASON_NONE;
+
+ gtk_widget_grab_focus(GTK_WIDGET(da));
+ sc_editor_redraw(e);
+ return FALSE;
+}
+
+
+static gboolean key_press_sig(GtkWidget *da, GdkEventKey *event,
+ SCEditor *e)
+{
+ gboolean r;
+ int claim = 0;
+
+ /* Throw the event to the IM context and let it sort things out */
+ r = gtk_im_context_filter_keypress(GTK_IM_CONTEXT(e->im_context),
+ event);
+ if ( r ) return FALSE; /* IM ate it */
+
+ switch ( event->keyval ) {
+
+ case GDK_KEY_Escape :
+ if ( !e->para_highlight ) {
+ sc_editor_remove_cursor(e);
+ sc_editor_redraw(e);
+ claim = 1;
+ }
+ break;
+
+ case GDK_KEY_Left :
+ if ( e->selection != NULL ) {
+ cursor_moveh(e->cursor_frame, &e->cpos, -1);
+ sc_editor_redraw(e);
+ }
+ claim = 1;
+ break;
+
+ case GDK_KEY_Right :
+ if ( e->selection != NULL ) {
+ cursor_moveh(e->cursor_frame, &e->cpos, +1);
+ sc_editor_redraw(e);
+ }
+ claim = 1;
+ break;
+
+ case GDK_KEY_Up :
+ if ( e->selection != NULL ) {
+ cursor_moveh(e->cursor_frame, &e->cpos, -1);
+ sc_editor_redraw(e);
+ }
+ claim = 1;
+ break;
+
+ case GDK_KEY_Down :
+ if ( e->selection != NULL ) {
+ cursor_moveh(e->cursor_frame, &e->cpos, +1);
+ sc_editor_redraw(e);
+ }
+ claim = 1;
+ break;
+
+
+ case GDK_KEY_Return :
+ im_commit_sig(NULL, "\n", e);
+ claim = 1;
+ break;
+
+ case GDK_KEY_BackSpace :
+ if ( e->selection != NULL ) {
+ do_backspace(e->selection, e);
+ claim = 1;
+ }
+ break;
+
+ case GDK_KEY_F5 :
+ full_rerender(e);
+ break;
+
+ case GDK_KEY_F6 :
+ show_edit_pos(e->cpos);
+ break;
+
+ case GDK_KEY_F7 :
+ if ( e->cursor_frame != NULL ) {
+ if ( event->state & GDK_CONTROL_MASK ) {
+ debug_paragraphs(e);
+ } else if ( event->state & GDK_SHIFT_MASK ) {
+ printf("Cursor frame block = %p\n", e->cursor_frame->scblocks);
+ printf("Editor top block = %p\n", e->scblocks);
+ show_sc_block(e->cursor_frame->scblocks, "");
+ } else {
+ open_debugger(e->cursor_frame);
+ }
+ } else {
+ if ( event->state & GDK_SHIFT_MASK ) {
+ printf("Debugging the top frame:\n");
+ printf("Editor top block = %p\n", e->scblocks);
+ show_sc_block(e->top->scblocks, "");
+ }
+ }
+ break;
+
+ case GDK_KEY_C :
+ case GDK_KEY_c :
+ if ( event->state == GDK_CONTROL_MASK ) {
+ copy_selection(e);
+ }
+ break;
+
+ case GDK_KEY_V :
+ case GDK_KEY_v :
+ if ( event->state == GDK_CONTROL_MASK ) {
+ sc_editor_paste(e);
+ }
+ break;
+
+
+ }
+
+ if ( claim ) return TRUE;
+ return FALSE;
+}
+
+
+static gboolean dnd_motion(GtkWidget *widget, GdkDragContext *drag_context,
+ gint x, gint y, guint time, SCEditor *e)
+{
+ GdkAtom target;
+
+ /* If we haven't already requested the data, do so now */
+ if ( !e->drag_preview_pending && !e->have_drag_data ) {
+
+ target = gtk_drag_dest_find_target(widget, drag_context, NULL);
+
+ if ( target != GDK_NONE ) {
+ gtk_drag_get_data(widget, drag_context, target, time);
+ e->drag_preview_pending = 1;
+ } else {
+ e->import_acceptable = 0;
+ gdk_drag_status(drag_context, 0, time);
+ }
+
+ }
+
+ if ( e->have_drag_data && e->import_acceptable ) {
+
+ gdk_drag_status(drag_context, GDK_ACTION_LINK, time);
+ e->start_corner_x = x - e->import_width/2.0;
+ e->start_corner_y = y - e->import_height/2.0;
+ e->drag_corner_x = x + e->import_width/2.0;
+ e->drag_corner_y = y + e->import_height/2.0;
+
+ sc_editor_redraw(e);
+
+ }
+
+ return TRUE;
+}
+
+
+static gboolean dnd_drop(GtkWidget *widget, GdkDragContext *drag_context,
+ gint x, gint y, guint time, SCEditor *e)
+{
+ GdkAtom target;
+
+ target = gtk_drag_dest_find_target(widget, drag_context, NULL);
+
+ if ( target == GDK_NONE ) {
+ gtk_drag_finish(drag_context, FALSE, FALSE, time);
+ } else {
+ gtk_drag_get_data(widget, drag_context, target, time);
+ }
+
+ return TRUE;
+}
+
+
+/* Scale the image down if it's a silly size */
+static void check_import_size(SCEditor *e)
+{
+ if ( e->import_width > e->w ) {
+
+ int new_import_width;
+
+ new_import_width = e->w/2;
+ e->import_height = (new_import_width * e->import_height) /
+ e->import_width;
+ e->import_width = new_import_width;
+
+ }
+
+ if ( e->import_height > e->h ) {
+
+ int new_import_height;
+
+ new_import_height = e->w/2;
+ e->import_width = (new_import_height*e->import_width) /
+ e->import_height;
+ e->import_height = new_import_height;
+
+ }
+}
+
+
+static void dnd_receive(GtkWidget *widget, GdkDragContext *drag_context,
+ gint x, gint y, GtkSelectionData *seldata,
+ guint info, guint time, SCEditor *e)
+{
+ if ( e->drag_preview_pending ) {
+
+ gchar *filename = NULL;
+ GdkPixbufFormat *f;
+ gchar **uris;
+ int w, h;
+
+ e->have_drag_data = 1;
+ e->drag_preview_pending = 0;
+ uris = gtk_selection_data_get_uris(seldata);
+ if ( uris != NULL ) {
+ filename = g_filename_from_uri(uris[0], NULL, NULL);
+ }
+ g_strfreev(uris);
+
+ if ( filename == NULL ) {
+
+ /* This doesn't even look like a sensible URI.
+ * Bail out. */
+ gdk_drag_status(drag_context, 0, time);
+ if ( e->drag_highlight ) {
+ gtk_drag_unhighlight(widget);
+ e->drag_highlight = 0;
+ }
+ e->import_acceptable = 0;
+ return;
+
+ }
+ chomp(filename);
+
+ f = gdk_pixbuf_get_file_info(filename, &w, &h);
+ g_free(filename);
+
+ e->import_width = w;
+ e->import_height = h;
+
+ if ( f == NULL ) {
+
+ gdk_drag_status(drag_context, 0, time);
+ if ( e->drag_highlight ) {
+ gtk_drag_unhighlight(widget);
+ e->drag_highlight = 0;
+ }
+ e->drag_status = DRAG_STATUS_NONE;
+ e->drag_reason = DRAG_REASON_NONE;
+ e->import_acceptable = 0;
+
+ } else {
+
+ /* Looks like a sensible image */
+ gdk_drag_status(drag_context, GDK_ACTION_PRIVATE, time);
+ e->import_acceptable = 1;
+
+ if ( !e->drag_highlight ) {
+ gtk_drag_highlight(widget);
+ e->drag_highlight = 1;
+ }
+
+ check_import_size(e);
+ e->drag_reason = DRAG_REASON_IMPORT;
+ e->drag_status = DRAG_STATUS_DRAGGING;
+
+ }
+
+ } else {
+
+ gchar **uris;
+ char *filename = NULL;
+
+ uris = gtk_selection_data_get_uris(seldata);
+ if ( uris != NULL ) {
+ filename = g_filename_from_uri(uris[0], NULL, NULL);
+ }
+ g_strfreev(uris);
+
+ if ( filename != NULL ) {
+
+ struct frame *fr;
+ char *opts;
+ size_t len;
+ int w, h;
+
+ gtk_drag_finish(drag_context, TRUE, FALSE, time);
+ chomp(filename);
+
+ w = e->drag_corner_x - e->start_corner_x;
+ h = e->drag_corner_y - e->start_corner_y;
+
+ len = strlen(filename)+64;
+ opts = malloc(len);
+ if ( opts == NULL ) {
+ free(filename);
+ fprintf(stderr, "Failed to allocate SC\n");
+ return;
+ }
+ snprintf(opts, len, "1fx1f+0+0,filename=\"%s\"",
+ filename);
+
+ fr = create_frame(e, e->start_corner_x,
+ e->start_corner_y, w, h);
+ fr->is_image = 1;
+ fr->empty = 0;
+ sc_block_set_name(sc_block_child(fr->scblocks), strdup("image"));
+ sc_block_set_options(sc_block_child(fr->scblocks), opts);
+ full_rerender(e);
+ sc_editor_remove_cursor(e);
+ sc_editor_redraw(e);
+ free(filename);
+
+ } else {
+
+ gtk_drag_finish(drag_context, FALSE, FALSE, time);
+
+ }
+
+ }
+}
+
+
+static void dnd_leave(GtkWidget *widget, GdkDragContext *drag_context,
+ guint time, SCEditor *sceditor)
+{
+ if ( sceditor->drag_highlight ) {
+ gtk_drag_unhighlight(widget);
+ }
+ sceditor->have_drag_data = 0;
+ sceditor->drag_highlight = 0;
+ sceditor->drag_status = DRAG_STATUS_NONE;
+ sceditor->drag_reason = DRAG_REASON_NONE;
+}
+
+
+static gint realise_sig(GtkWidget *da, SCEditor *e)
+{
+ GdkWindow *win;
+
+ /* Keyboard and input method stuff */
+ e->im_context = gtk_im_multicontext_new();
+ win = gtk_widget_get_window(GTK_WIDGET(e));
+ gtk_im_context_set_client_window(GTK_IM_CONTEXT(e->im_context), win);
+ gdk_window_set_accept_focus(win, TRUE);
+ g_signal_connect(G_OBJECT(e->im_context), "commit", G_CALLBACK(im_commit_sig), e);
+ g_signal_connect(G_OBJECT(e), "key-press-event", G_CALLBACK(key_press_sig), e);
+
+ /* FIXME: Can do this "properly" by setting up a separate font map */
+ e->pc = gtk_widget_get_pango_context(GTK_WIDGET(e));
+
+ return FALSE;
+}
+
+
+void sc_editor_set_scblock(SCEditor *e, SCBlock *scblocks)
+{
+ e->scblocks = scblocks;
+ full_rerender(e);
+}
+
+
+static void update_size_request(SCEditor *e)
+{
+ gtk_widget_set_size_request(GTK_WIDGET(e), 0, e->h + 2.0*e->min_border);
+}
+
+
+void sc_editor_set_logical_size(SCEditor *e, double w, double h)
+{
+ e->log_w = w;
+ e->log_h = h;
+ if ( gtk_widget_get_mapped(GTK_WIDGET(e)) ) {
+ full_rerender(e);
+ sc_editor_redraw(e);
+ }
+}
+
+
+void sc_editor_set_slidenum(SCEditor *e, int slidenum)
+{
+ e->slidenum = slidenum;
+}
+
+
+void sc_editor_set_min_border(SCEditor *e, double min_border)
+{
+ e->min_border = min_border;
+ update_size_request(e);
+}
+
+
+void sc_editor_set_top_frame_editable(SCEditor *e, int top_frame_editable)
+{
+ e->top_editable = top_frame_editable;
+}
+
+
+void sc_editor_set_stylesheet(SCEditor *e, Stylesheet *stylesheet)
+{
+ e->stylesheet = stylesheet;
+}
+
+
+void sc_editor_set_callbacks(SCEditor *e, SCCallbackList *cbl)
+{
+ if ( e->cbl != NULL ) sc_callback_list_free(e->cbl);
+ e->cbl = cbl;
+}
+
+
+void sc_editor_set_para_highlight(SCEditor *e, int para_highlight)
+{
+ e->para_highlight = para_highlight;
+ sc_editor_redraw(e);
+}
+
+int sc_editor_get_cursor_para(SCEditor *e)
+{
+ if ( e->cursor_frame == NULL ) return 0;
+ return e->cpos.para;
+}
+
+
+void *sc_editor_get_cursor_bvp(SCEditor *e)
+{
+ Paragraph *para;
+ if ( e->cursor_frame == NULL ) return 0;
+ para = e->cursor_frame->paras[e->cpos.para];
+ return get_para_bvp(para);
+}
+
+
+void sc_editor_set_cursor_para(SCEditor *e, signed int pos)
+{
+ double h;
+ int i;
+
+ if ( e->cursor_frame == NULL ) {
+ e->cursor_frame = e->top;
+ e->selection = e->top;
+ }
+
+ if ( pos < 0 ) {
+ e->cpos.para = e->cursor_frame->n_paras - 1;
+ } else if ( pos >= e->cursor_frame->n_paras ) {
+ e->cpos.para = e->cursor_frame->n_paras - 1;
+ } else {
+ e->cpos.para = pos;
+ }
+ e->cpos.pos = 0;
+ e->cpos.trail = 0;
+
+ h = 0;
+ for ( i=0; i<e->cpos.para; i++ ) {
+ h += paragraph_height(e->cursor_frame->paras[i]);
+ }
+ h += (paragraph_height(e->cursor_frame->paras[e->cpos.para]))/2;
+ e->scroll_pos = h - (e->visible_height/2);
+ set_vertical_params(e);
+
+ sc_editor_redraw(e);
+}
+
+
+int sc_editor_get_num_paras(SCEditor *e)
+{
+ if ( e->cursor_frame == NULL ) return 1;
+ return e->cursor_frame->n_paras;
+}
+
+
+void sc_editor_set_scale(SCEditor *e, int scale)
+{
+ e->scale = scale;
+ if ( !scale ) {
+ e->view_scale = 1.0;
+ }
+}
+
+
+void sc_editor_set_imagestore(SCEditor *e, ImageStore *is)
+{
+ if ( e->is != NULL ) {
+ fprintf(stderr, "WARNING: Changing imagestore\n");
+ }
+ e->is = is;
+}
+
+
+SCEditor *sc_editor_new(SCBlock *scblocks, Stylesheet *stylesheet,
+ PangoLanguage *lang, const char *storename)
+{
+ SCEditor *sceditor;
+ GtkTargetEntry targets[1];
+ GError *err;
+
+ sceditor = g_object_new(SC_TYPE_EDITOR, NULL);
+
+ sceditor->scblocks = scblocks;
+ sceditor->w = 100;
+ sceditor->h = 100;
+ sceditor->log_w = 100;
+ sceditor->log_h = 100;
+ sceditor->border_offs_x = 0;
+ sceditor->border_offs_y = 0;
+ sceditor->is = NULL;
+ sceditor->slidenum = 0;
+ sceditor->min_border = 0.0;
+ sceditor->top_editable = 0;
+ sceditor->cbl = NULL;
+ sceditor->scroll_pos = 0;
+ sceditor->flow = 0;
+ sceditor->scale = 0;
+ sceditor->view_scale = 1.0;
+ sceditor->lang = lang;
+
+ sceditor->para_highlight = 0;
+ sc_editor_remove_cursor(sceditor);
+
+ sceditor->stylesheet = stylesheet;
+
+ err = NULL;
+ sceditor->bg_pixbuf = gdk_pixbuf_new_from_resource("/uk/me/bitwiz/Colloquium/sky.png",
+ &err);
+ if ( sceditor->bg_pixbuf == NULL ) {
+ fprintf(stderr, _("Failed to load background: %s\n"),
+ err->message);
+ }
+
+ gtk_widget_set_size_request(GTK_WIDGET(sceditor),
+ sceditor->w, sceditor->h);
+
+ g_signal_connect(G_OBJECT(sceditor), "destroy",
+ G_CALLBACK(destroy_sig), sceditor);
+ g_signal_connect(G_OBJECT(sceditor), "realize",
+ G_CALLBACK(realise_sig), sceditor);
+ g_signal_connect(G_OBJECT(sceditor), "button-press-event",
+ G_CALLBACK(button_press_sig), sceditor);
+ g_signal_connect(G_OBJECT(sceditor), "button-release-event",
+ G_CALLBACK(button_release_sig), sceditor);
+ g_signal_connect(G_OBJECT(sceditor), "motion-notify-event",
+ G_CALLBACK(motion_sig), sceditor);
+ g_signal_connect(G_OBJECT(sceditor), "configure-event",
+ G_CALLBACK(resize_sig), sceditor);
+
+ /* Drag and drop */
+ targets[0].target = "text/uri-list";
+ targets[0].flags = 0;
+ targets[0].info = 1;
+ gtk_drag_dest_set(GTK_WIDGET(sceditor), 0, targets, 1,
+ GDK_ACTION_PRIVATE);
+ g_signal_connect(sceditor, "drag-data-received",
+ G_CALLBACK(dnd_receive), sceditor);
+ g_signal_connect(sceditor, "drag-motion",
+ G_CALLBACK(dnd_motion), sceditor);
+ g_signal_connect(sceditor, "drag-drop",
+ G_CALLBACK(dnd_drop), sceditor);
+ g_signal_connect(sceditor, "drag-leave",
+ G_CALLBACK(dnd_leave), sceditor);
+
+ gtk_widget_set_can_focus(GTK_WIDGET(sceditor), TRUE);
+ gtk_widget_add_events(GTK_WIDGET(sceditor),
+ GDK_POINTER_MOTION_HINT_MASK
+ | GDK_BUTTON1_MOTION_MASK
+ | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
+ | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK
+ | GDK_SCROLL_MASK);
+
+ g_signal_connect(G_OBJECT(sceditor), "draw",
+ G_CALLBACK(draw_sig), sceditor);
+
+ gtk_widget_grab_focus(GTK_WIDGET(sceditor));
+
+ gtk_widget_show(GTK_WIDGET(sceditor));
+
+ return sceditor;
+}
diff --git a/src-old/sc_editor.h b/src-old/sc_editor.h
new file mode 100644
index 0000000..d3c111b
--- /dev/null
+++ b/src-old/sc_editor.h
@@ -0,0 +1,199 @@
+/*
+ * sc_editor.h
+ *
+ * Copyright © 2014-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SC_EDITOR_H
+#define SC_EDITOR_H
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <gtk/gtk.h>
+#include <glib-object.h>
+
+#include "frame.h"
+#include "sc_interp.h"
+#include "stylesheet.h"
+
+struct presentation;
+
+
+#define SC_TYPE_EDITOR (sc_editor_get_type())
+
+#define SC_EDITOR(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), \
+ SC_TYPE_EDITOR, SCEditor))
+
+#define SC_IS_EDITOR(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), \
+ SC_TYPE_EDITOR))
+
+#define SC_EDITOR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((obj), \
+ SC_TYPE_EDITOR, SCEditorClass))
+
+#define SC_IS_EDITOR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((obj), \
+ SC_TYPE_EDITOR))
+
+#define SC_EDITOR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), \
+ SC_TYPE_EDITOR, SCEditorClass))
+
+enum drag_reason
+{
+ DRAG_REASON_NONE,
+ DRAG_REASON_CREATE,
+ DRAG_REASON_IMPORT,
+ DRAG_REASON_RESIZE,
+ DRAG_REASON_MOVE,
+ DRAG_REASON_TEXTSEL
+};
+
+
+enum corner
+{
+ CORNER_NONE,
+ CORNER_TL,
+ CORNER_TR,
+ CORNER_BL,
+ CORNER_BR
+};
+
+
+enum drag_status
+{
+ DRAG_STATUS_NONE,
+ DRAG_STATUS_COULD_DRAG,
+ DRAG_STATUS_DRAGGING,
+};
+
+
+struct _sceditor
+{
+ GtkDrawingArea parent_instance;
+ PangoLanguage *lang;
+
+ /*< private >*/
+ GtkIMContext *im_context;
+ int w; /* Surface size in pixels */
+ int h;
+ double log_w; /* Size of surface in "SC units" */
+ double log_h;
+ SCBlock *scblocks;
+ Stylesheet *stylesheet;
+ ImageStore *is;
+ SCCallbackList *cbl;
+ struct frame *top;
+ int para_highlight;
+
+ /* Redraw/scroll stuff */
+ GtkScrollablePolicy hpol;
+ GtkScrollablePolicy vpol;
+ GtkAdjustment *hadj;
+ GtkAdjustment *vadj;
+ double scroll_pos;
+ double h_scroll_pos;
+ int visible_height;
+ int visible_width;
+ int flow;
+ int scale; /* Whether the SCEditor should scale to fit */
+ double view_scale; /* The scale factor, if scale=1 */
+
+ /* Pointers to the frame currently being edited */
+ struct frame *selection;
+ int top_editable;
+
+ PangoContext *pc;
+
+ /* Location of the cursor */
+ struct frame *cursor_frame;
+ struct edit_pos cpos;
+
+ /* Border surrounding actual slide within drawingarea */
+ double border_offs_x;
+ double border_offs_y;
+ double min_border;
+ double bgcol[3];
+ GdkPixbuf *bg_pixbuf;
+
+ /* Rubber band boxes and related stuff */
+ double start_corner_x;
+ double start_corner_y;
+ double drag_corner_x;
+ double drag_corner_y;
+ double diagonal_length;
+ double box_x;
+ double box_y;
+ double box_width;
+ double box_height;
+ enum drag_reason drag_reason;
+ enum drag_status drag_status;
+ enum corner drag_corner;
+ int sel_active;
+ struct edit_pos sel_start; /* Where the user dragged from */
+ struct edit_pos sel_end;
+
+ /* Stuff to do with drag and drop import of "content" */
+ int drag_preview_pending;
+ int have_drag_data;
+ int drag_highlight;
+ double import_width;
+ double import_height;
+ int import_acceptable;
+
+ /* Stuff that doesn't really belong here */
+ int slidenum;
+};
+
+struct _sceditorclass
+{
+ GtkDrawingAreaClass parent_class;
+};
+
+typedef struct _sceditor SCEditor;
+typedef struct _sceditorclass SCEditorClass;
+
+extern void sc_editor_set_scblock(SCEditor *e, SCBlock *scblocks);
+extern void sc_editor_set_stylesheet(SCEditor *e, Stylesheet *stylesheet);
+extern SCEditor *sc_editor_new(SCBlock *scblocks, Stylesheet *stylesheet,
+ PangoLanguage *lang, const char *storename);
+extern void sc_editor_set_logical_size(SCEditor *e, double w, double h);
+extern void sc_editor_set_flow(SCEditor *e, int flow);
+extern void sc_editor_set_scale(SCEditor *e, int scale);
+extern void sc_editor_redraw(SCEditor *e);
+extern void sc_editor_set_background(SCEditor *e, double r, double g, double b);
+extern void sc_editor_set_slidenum(SCEditor *e, int slidenum);
+extern void sc_editor_set_min_border(SCEditor *e, double min_border);
+extern void sc_editor_set_top_frame_editable(SCEditor *e,
+ int top_frame_editable);
+extern void sc_editor_set_callbacks(SCEditor *e, SCCallbackList *cbl);
+extern void sc_editor_paste(SCEditor *e);
+extern void sc_editor_add_storycode(SCEditor *e, const char *sc);
+extern void sc_editor_copy_selected_frame(SCEditor *e);
+extern void sc_editor_delete_selected_frame(SCEditor *e);
+extern void sc_editor_ensure_cursor(SCEditor *e);
+extern SCBlock *split_paragraph_at_cursor(SCEditor *e);
+
+extern void sc_editor_set_imagestore(SCEditor *e, ImageStore *is);
+extern void sc_editor_set_para_highlight(SCEditor *e, int para_highlight);
+extern int sc_editor_get_cursor_para(SCEditor *e);
+extern void *sc_editor_get_cursor_bvp(SCEditor *e);
+extern void sc_editor_set_cursor_para(SCEditor *e, signed int pos);
+extern int sc_editor_get_num_paras(SCEditor *e);
+
+#endif /* SC_EDITOR_H */
diff --git a/src-old/sc_interp.c b/src-old/sc_interp.c
new file mode 100644
index 0000000..07f09a5
--- /dev/null
+++ b/src-old/sc_interp.c
@@ -0,0 +1,1148 @@
+/*
+ * sc_interp.c
+ *
+ * Copyright © 2014-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+#include <pango/pangocairo.h>
+#include <gdk/gdk.h>
+
+#include "imagestore.h"
+#include "sc_parse.h"
+#include "sc_interp.h"
+#include "presentation.h"
+#include "utils.h"
+
+
+struct sc_state
+{
+ PangoFontDescription *fontdesc;
+ PangoFont *font;
+ PangoAlignment alignment;
+ double col[4];
+ int ascent;
+ int height;
+ float paraspace[4];
+ char *constants[NUM_SC_CONSTANTS];
+
+ struct frame *fr; /* The current frame */
+};
+
+struct _scinterp
+{
+ PangoContext *pc;
+ PangoLanguage *lang;
+ ImageStore *is;
+
+ struct slide_constants *s_constants;
+ struct presentation_constants *p_constants;
+
+ struct sc_state *state;
+ int j; /* Index of the current state */
+ int max_state;
+
+ SCCallbackList *cbl;
+};
+
+struct _sccallbacklist
+{
+ int n_callbacks;
+ int max_callbacks;
+ char **names;
+ SCCallbackBoxFunc *box_funcs;
+ SCCallbackDrawFunc *draw_funcs;
+ SCCallbackClickFunc *click_funcs;
+ void **vps;
+};
+
+
+static int sc_interp_add_blocks(SCInterpreter *scin, SCBlock *bl, Stylesheet *ss)
+{
+ while ( bl != NULL ) {
+ if ( sc_interp_add_block(scin, bl, ss) ) return 1;
+ bl = sc_block_next(bl);
+ }
+
+ return 0;
+}
+
+
+SCCallbackList *sc_callback_list_new()
+{
+ SCCallbackList *cbl;
+
+ cbl = malloc(sizeof(struct _sccallbacklist));
+ if ( cbl == NULL ) return NULL;
+
+ cbl->names = calloc(8, sizeof(char *));
+ if ( cbl->names == NULL ) {
+ free(cbl);
+ return NULL;
+ }
+
+ cbl->box_funcs = calloc(8, sizeof(cbl->box_funcs[0]));
+ if ( cbl->box_funcs == NULL ) {
+ free(cbl->names);
+ free(cbl);
+ return NULL;
+ }
+
+ cbl->draw_funcs = calloc(8, sizeof(cbl->draw_funcs[0]));
+ if ( cbl->draw_funcs == NULL ) {
+ free(cbl->box_funcs);
+ free(cbl->names);
+ free(cbl);
+ return NULL;
+ }
+
+ cbl->click_funcs = calloc(8, sizeof(cbl->click_funcs[0]));
+ if ( cbl->click_funcs == NULL ) {
+ free(cbl->draw_funcs);
+ free(cbl->box_funcs);
+ free(cbl->names);
+ free(cbl);
+ return NULL;
+ }
+
+ cbl->vps = calloc(8, sizeof(cbl->vps[0]));
+ if ( cbl->vps == NULL ) {
+ free(cbl->click_funcs);
+ free(cbl->draw_funcs);
+ free(cbl->box_funcs);
+ free(cbl->names);
+ free(cbl);
+ return NULL;
+ }
+
+ cbl->max_callbacks = 8;
+ cbl->n_callbacks = 0;
+
+ return cbl;
+}
+
+
+void sc_callback_list_free(SCCallbackList *cbl)
+{
+ int i;
+
+ if ( cbl == NULL ) return;
+
+ for ( i=0; i<cbl->n_callbacks; i++ ) {
+ free(cbl->names[i]);
+ }
+
+ free(cbl->names);
+ free(cbl->box_funcs);
+ free(cbl->draw_funcs);
+ free(cbl->vps);
+ free(cbl);
+
+}
+
+
+void sc_callback_list_add_callback(SCCallbackList *cbl, const char *name,
+ SCCallbackBoxFunc box_func,
+ SCCallbackDrawFunc draw_func,
+ SCCallbackClickFunc click_func,
+ void *vp)
+{
+ if ( cbl->n_callbacks == cbl->max_callbacks ) {
+
+ SCCallbackBoxFunc *box_funcs_new;
+ SCCallbackDrawFunc *draw_funcs_new;
+ SCCallbackClickFunc *click_funcs_new;
+ char **names_new;
+ void **vps_new;
+ int mcn = cbl->max_callbacks + 8;
+
+ names_new = realloc(cbl->names, mcn*sizeof(char *));
+ box_funcs_new = realloc(cbl->box_funcs,
+ mcn*sizeof(SCCallbackBoxFunc));
+ draw_funcs_new = realloc(cbl->draw_funcs,
+ mcn*sizeof(SCCallbackDrawFunc));
+ click_funcs_new = realloc(cbl->click_funcs,
+ mcn*sizeof(SCCallbackClickFunc));
+ vps_new = realloc(cbl->vps, mcn*sizeof(void *));
+
+ if ( (names_new == NULL) || (box_funcs_new == NULL)
+ || (vps_new == NULL) || (draw_funcs_new == NULL)
+ || (click_funcs_new == NULL) ) {
+ fprintf(stderr, "Failed to grow callback list\n");
+ return;
+ }
+
+ cbl->names = names_new;
+ cbl->box_funcs = box_funcs_new;
+ cbl->draw_funcs = draw_funcs_new;
+ cbl->click_funcs = click_funcs_new;
+ cbl->vps = vps_new;
+ cbl->max_callbacks = mcn;
+
+ }
+
+ cbl->names[cbl->n_callbacks] = strdup(name);
+ cbl->box_funcs[cbl->n_callbacks] = box_func;
+ cbl->draw_funcs[cbl->n_callbacks] = draw_func;
+ cbl->click_funcs[cbl->n_callbacks] = click_func;
+ cbl->vps[cbl->n_callbacks] = vp;
+ cbl->n_callbacks++;
+}
+
+
+void sc_interp_set_callbacks(SCInterpreter *scin, SCCallbackList *cbl)
+{
+ if ( scin->cbl != NULL ) {
+ fprintf(stderr, "WARNING: Interpreter already has a callback "
+ "list.\n");
+ }
+ scin->cbl = cbl;
+}
+
+
+static int check_callback(SCInterpreter *scin, SCBlock *bl)
+{
+ int i;
+ const char *name = sc_block_name(bl);
+ SCCallbackList *cbl = scin->cbl;
+
+ /* No callback list -> easy */
+ if ( cbl == NULL ) return 0;
+
+ /* No name -> definitely not a callback */
+ if ( name == NULL ) return 0;
+
+ for ( i=0; i<cbl->n_callbacks; i++ ) {
+
+ double w, h;
+ int r;
+ void *bvp;
+
+ if ( strcmp(cbl->names[i], name) != 0 ) continue;
+ r = cbl->box_funcs[i](scin, bl, &w, &h, &bvp, cbl->vps[i]);
+ if ( r ) {
+ struct sc_state *st = &scin->state[scin->j];
+ Paragraph *pnew;
+ pnew = add_callback_para(sc_interp_get_frame(scin),
+ bl, w, h,
+ cbl->draw_funcs[i],
+ cbl->click_funcs[i],
+ bvp, cbl->vps[i]);
+ if ( pnew != NULL ) {
+ set_para_spacing(pnew, st->paraspace);
+ }
+
+ }
+ return 1;
+
+ }
+
+ return 0;
+}
+
+
+PangoFontDescription *sc_interp_get_fontdesc(SCInterpreter *scin)
+{
+ struct sc_state *st = &scin->state[scin->j];
+ return st->fontdesc;
+}
+
+
+double *sc_interp_get_fgcol(SCInterpreter *scin)
+{
+ struct sc_state *st = &scin->state[scin->j];
+ return st->col;
+}
+
+
+static void set_frame_default_style(struct frame *fr, SCInterpreter *scin)
+{
+ if ( fr == NULL ) return;
+
+ if ( fr->fontdesc != NULL ) {
+ pango_font_description_free(fr->fontdesc);
+ }
+ fr->fontdesc = pango_font_description_copy(sc_interp_get_fontdesc(scin));
+ fr->col[0] = sc_interp_get_fgcol(scin)[0];
+ fr->col[1] = sc_interp_get_fgcol(scin)[1];
+ fr->col[2] = sc_interp_get_fgcol(scin)[2];
+ fr->col[3] = sc_interp_get_fgcol(scin)[3];
+}
+
+
+static void update_font(SCInterpreter *scin)
+{
+ PangoFontMetrics *metrics;
+ struct sc_state *st = &scin->state[scin->j];
+
+ if ( scin->pc == NULL ) return;
+
+ st->font = pango_font_map_load_font(pango_context_get_font_map(scin->pc),
+ scin->pc, st->fontdesc);
+ if ( st->font == NULL ) {
+ char *f = pango_font_description_to_string(st->fontdesc);
+ fprintf(stderr, "Couldn't load font '%s' (font map %p, pc %p)\n",
+ f, pango_context_get_font_map(scin->pc), scin->pc);
+ g_free(f);
+ return;
+ }
+
+ /* FIXME: Language for box */
+ metrics = pango_font_get_metrics(st->font, NULL);
+ st->ascent = pango_font_metrics_get_ascent(metrics);
+ st->height = st->ascent + pango_font_metrics_get_descent(metrics);
+ pango_font_metrics_unref(metrics);
+ set_frame_default_style(sc_interp_get_frame(scin), scin);
+}
+
+
+static void set_font(SCInterpreter *scin, const char *font_name)
+{
+ struct sc_state *st = &scin->state[scin->j];
+
+ st->fontdesc = pango_font_description_from_string(font_name);
+ if ( st->fontdesc == NULL ) {
+ fprintf(stderr, "Couldn't describe font.\n");
+ return;
+ }
+
+ update_font(scin);
+}
+
+
+static void copy_top_fontdesc(SCInterpreter *scin)
+{
+ struct sc_state *st = &scin->state[scin->j];
+
+ /* If this is the first stack frame, don't even check */
+ if ( scin->j == 0 ) return;
+
+ /* If the fontdesc at the top of the stack is the same as the one
+ * below, make a copy because we're about to do something to it (which
+ * should not affect the next level up). */
+ if ( st->fontdesc == scin->state[scin->j-1].fontdesc ) {
+ st->fontdesc = pango_font_description_copy(st->fontdesc);
+ }
+}
+
+
+static void set_fontsize(SCInterpreter *scin, const char *size_str)
+{
+ struct sc_state *st = &scin->state[scin->j];
+ int size;
+ char *end;
+
+ if ( size_str[0] == '\0' ) return;
+
+ size = strtoul(size_str, &end, 10);
+ if ( end[0] != '\0' ) {
+ fprintf(stderr, _("Invalid font size '%s'\n"), size_str);
+ return;
+ }
+
+ copy_top_fontdesc(scin);
+ pango_font_description_set_size(st->fontdesc, size*PANGO_SCALE);
+ update_font(scin);
+}
+
+
+static void set_bold(SCInterpreter *scin)
+{
+ struct sc_state *st = &scin->state[scin->j];
+ copy_top_fontdesc(scin);
+ pango_font_description_set_weight(st->fontdesc, PANGO_WEIGHT_BOLD);
+ update_font(scin);
+}
+
+
+static void set_oblique(SCInterpreter *scin)
+{
+ struct sc_state *st = &scin->state[scin->j];
+ copy_top_fontdesc(scin);
+ pango_font_description_set_style(st->fontdesc, PANGO_STYLE_OBLIQUE);
+ update_font(scin);
+}
+
+
+static void set_italic(SCInterpreter *scin)
+{
+ struct sc_state *st = &scin->state[scin->j];
+ copy_top_fontdesc(scin);
+ pango_font_description_set_style(st->fontdesc, PANGO_STYLE_ITALIC);
+ update_font(scin);
+}
+
+
+static void set_alignment(SCInterpreter *scin, PangoAlignment align)
+{
+ struct sc_state *st = &scin->state[scin->j];
+ st->alignment = align;
+}
+
+
+/* This sets the colour for the font at the top of the stack */
+static void set_colour(SCInterpreter *scin, const char *colour)
+{
+ GdkRGBA col;
+ struct sc_state *st = &scin->state[scin->j];
+
+ if ( colour == NULL ) {
+ printf(_("Invalid colour\n"));
+ st->col[0] = 0.0;
+ st->col[1] = 0.0;
+ st->col[2] = 0.0;
+ st->col[3] = 1.0;
+ return;
+ }
+
+ gdk_rgba_parse(&col, colour);
+
+ st->col[0] = col.red;
+ st->col[1] = col.green;
+ st->col[2] = col.blue;
+ st->col[3] = col.alpha;
+ set_frame_default_style(sc_interp_get_frame(scin), scin);
+}
+
+
+static void set_bgcol(SCInterpreter *scin, const char *colour)
+{
+ GdkRGBA col;
+ struct frame *fr = sc_interp_get_frame(scin);
+
+ if ( fr == NULL ) return;
+
+ if ( colour == NULL ) {
+ printf(_("Invalid colour\n"));
+ return;
+ }
+
+ gdk_rgba_parse(&col, colour);
+
+ fr->bgcol[0] = col.red;
+ fr->bgcol[1] = col.green;
+ fr->bgcol[2] = col.blue;
+ fr->bgcol[3] = col.alpha;
+ fr->grad = GRAD_NONE;
+}
+
+
+static void set_bggrad(SCInterpreter *scin, const char *options,
+ GradientType grad)
+{
+ struct frame *fr = sc_interp_get_frame(scin);
+ GdkRGBA col1, col2;
+
+ if ( fr == NULL ) return;
+
+ if ( options == NULL ) {
+ printf(_("Invalid colour\n"));
+ return;
+ }
+
+ if ( parse_colour_duo(options, &col1, &col2) == 0 ) {
+
+ fr->bgcol[0] = col1.red;
+ fr->bgcol[1] = col1.green;
+ fr->bgcol[2] = col1.blue;
+ fr->bgcol[3] = col1.alpha;
+
+ fr->bgcol2[0] = col2.red;
+ fr->bgcol2[1] = col2.green;
+ fr->bgcol2[2] = col2.blue;
+ fr->bgcol2[3] = col2.alpha;
+
+ fr->grad = grad;
+
+ }
+}
+
+
+static char *get_constant(SCInterpreter *scin, unsigned int constant)
+{
+ struct sc_state *st = &scin->state[scin->j];
+ if ( constant >= NUM_SC_CONSTANTS ) return NULL;
+ return st->constants[constant];
+}
+
+
+void sc_interp_set_constant(SCInterpreter *scin, unsigned int constant,
+ const char *val)
+{
+ struct sc_state *st = &scin->state[scin->j];
+ if ( constant >= NUM_SC_CONSTANTS ) return;
+ if ( val == NULL ) return;
+ st->constants[constant] = strdup(val);
+}
+
+
+void sc_interp_save(SCInterpreter *scin)
+{
+ if ( scin->j+1 == scin->max_state ) {
+
+ struct sc_state *stack_new;
+
+ stack_new = realloc(scin->state, sizeof(struct sc_state)
+ * (scin->max_state+8));
+ if ( stack_new == NULL ) {
+ fprintf(stderr, "Failed to add to stack.\n");
+ return;
+ }
+
+ scin->state = stack_new;
+ scin->max_state += 8;
+
+ }
+
+ /* When n_fonts=0, we leave the first font uninitialised. This allows
+ * the stack to be "bootstrapped", but requires the first caller to do
+ * set_font and set_colour straight away. */
+ scin->state[scin->j+1] = scin->state[scin->j];
+
+ scin->j++;
+}
+
+
+void sc_interp_restore(SCInterpreter *scin)
+{
+ struct sc_state *st = &scin->state[scin->j];
+
+ if ( scin->j > 0 ) {
+
+ int i;
+
+ if ( st->fontdesc != scin->state[scin->j-1].fontdesc )
+ {
+ pango_font_description_free(st->fontdesc);
+ } /* else the font is the same as the previous one, and we
+ * don't need to free it just yet */
+
+ for ( i=0; i<NUM_SC_CONSTANTS; i++ ) {
+ if ( st->constants[i] != scin->state[scin->j-1].constants[i] ) {
+ free(st->constants[i]);
+ } /* same logic as above */
+ }
+ }
+
+ scin->j--;
+}
+
+
+struct frame *sc_interp_get_frame(SCInterpreter *scin)
+{
+ struct sc_state *st = &scin->state[scin->j];
+ return st->fr;
+}
+
+
+static void set_frame(SCInterpreter *scin, struct frame *fr)
+{
+ struct sc_state *st = &scin->state[scin->j];
+ st->fr = fr;
+}
+
+
+SCInterpreter *sc_interp_new(PangoContext *pc, PangoLanguage *lang,
+ ImageStore *is, struct frame *top)
+{
+ SCInterpreter *scin;
+ struct sc_state *st;
+ int i;
+
+ scin = malloc(sizeof(SCInterpreter));
+ if ( scin == NULL ) return NULL;
+
+ scin->state = malloc(8*sizeof(struct sc_state));
+ if ( scin->state == NULL ) {
+ free(scin);
+ return NULL;
+ }
+ scin->j = 0;
+ scin->max_state = 8;
+
+ scin->pc = pc;
+ scin->is = is;
+ scin->s_constants = NULL;
+ scin->p_constants = NULL;
+ scin->cbl = NULL;
+ scin->lang = lang;
+
+ /* Initial state */
+ st = &scin->state[0];
+ st->fr = NULL;
+ st->paraspace[0] = 0.0;
+ st->paraspace[1] = 0.0;
+ st->paraspace[2] = 0.0;
+ st->paraspace[3] = 0.0;
+ st->fontdesc = NULL;
+ st->col[0] = 0.0;
+ st->col[1] = 0.0;
+ st->col[2] = 0.0;
+ st->col[3] = 1.0;
+ st->alignment = PANGO_ALIGN_LEFT;
+ for ( i=0; i<NUM_SC_CONSTANTS; i++ ) {
+ st->constants[i] = NULL;
+ }
+
+ /* The "ultimate" default font */
+ if ( scin->pc != NULL ) {
+ set_frame(scin, top);
+ set_font(scin, "Cantarell Regular 14");
+ set_colour(scin, "#000000");
+ }
+
+ return scin;
+}
+
+
+void sc_interp_destroy(SCInterpreter *scin)
+{
+ /* Empty the stack */
+ while ( scin->j > 0 ) {
+ sc_interp_restore(scin);
+ }
+
+ if ( scin->state[0].fontdesc != NULL ) {
+ pango_font_description_free(scin->state[0].fontdesc);
+ }
+
+ free(scin->state);
+ free(scin);
+}
+
+
+static void set_padding(struct frame *fr, const char *opts)
+{
+ float p[4];
+
+ if ( parse_tuple(opts, p) ) return;
+
+ if ( fr == NULL ) return;
+
+ fr->pad_l = p[0];
+ fr->pad_r = p[1];
+ fr->pad_t = p[2];
+ fr->pad_b = p[3];
+}
+
+
+static void set_paraspace(SCInterpreter *scin, const char *opts)
+{
+ float p[4];
+ struct sc_state *st = &scin->state[scin->j];
+
+ if ( parse_tuple(opts, p) ) return;
+
+ st->paraspace[0] = p[0];
+ st->paraspace[1] = p[1];
+ st->paraspace[2] = p[2];
+ st->paraspace[3] = p[3];
+
+ set_para_spacing(last_para(sc_interp_get_frame(scin)), p);
+}
+
+
+void update_geom(struct frame *fr)
+{
+ char geom[256];
+ snprintf(geom, 255, "%.1fux%.1fu+%.1f+%.1f",
+ fr->w, fr->h, fr->x, fr->y);
+
+ /* FIXME: What if there are other options? */
+ sc_block_set_options(fr->scblocks, strdup(geom));
+}
+
+
+static int calculate_dims(const char *opt, struct frame *parent,
+ double *wp, double *hp, double *xp, double *yp)
+{
+ LengthUnits h_units, w_units;
+
+ if ( parse_dims(opt, wp, hp, &w_units, &h_units, xp, yp) ) {
+ return 1;
+ }
+
+ if ( w_units == UNITS_FRAC ) {
+ if ( parent != NULL ) {
+ double pw = parent->w;
+ pw -= parent->pad_l;
+ pw -= parent->pad_r;
+ *wp = pw * *wp;
+ } else {
+ *wp = -1.0;
+ }
+
+ }
+ if ( h_units == UNITS_FRAC ) {
+ if ( parent != NULL ) {
+ double ph = parent->h;
+ ph -= parent->pad_t;
+ ph -= parent->pad_b;
+ *hp = ph * *hp;
+ } else {
+ *hp = -1.0;
+ }
+ }
+
+ return 0;
+}
+
+
+static int parse_frame_option(const char *opt, struct frame *fr,
+ struct frame *parent)
+{
+ if ( (index(opt, 'x') != NULL) && (index(opt, '+') != NULL)
+ && (index(opt, '+') != rindex(opt, '+')) ) {
+ return calculate_dims(opt, parent, &fr->w, &fr->h, &fr->x, &fr->y);
+ }
+
+ fprintf(stderr, _("Unrecognised frame option '%s'\n"), opt);
+
+ return 1;
+}
+
+
+static int parse_frame_options(struct frame *fr, struct frame *parent,
+ const char *opth)
+{
+ int i;
+ size_t len;
+ size_t start;
+ char *opt;
+
+ if ( opth == NULL ) return 1;
+
+ opt = strdup(opth);
+
+ len = strlen(opt);
+ start = 0;
+
+ for ( i=0; i<len; i++ ) {
+
+ /* FIXME: comma might be escaped or quoted */
+ if ( opt[i] == ',' ) {
+ opt[i] = '\0';
+ if ( parse_frame_option(opt+start, fr, parent) ) {
+ return 1;
+ }
+ start = i+1;
+ }
+
+ }
+
+ if ( start != len ) {
+ if ( parse_frame_option(opt+start, fr, parent) ) return 1;
+ }
+
+ free(opt);
+
+ return 0;
+}
+
+
+static int parse_image_option(const char *opt, struct frame *parent,
+ double *wp, double *hp, char **filenamep)
+{
+ if ( (index(opt, 'x') != NULL) && (index(opt, '+') != NULL)
+ && (index(opt, '+') != rindex(opt, '+')) ) {
+ double dum;
+ return calculate_dims(opt, NULL, wp, hp, &dum, &dum);
+ }
+
+ if ( strncmp(opt, "filename=\"", 10) == 0 ) {
+ char *fn;
+ fn = strdup(opt+10);
+ if ( fn[strlen(fn)-1] != '\"' ) {
+ fprintf(stderr, "Unterminated filename?\n");
+ free(fn);
+ return 1;
+ }
+ fn[strlen(fn)-1] = '\0';
+ *filenamep = fn;
+ return 0;
+ }
+
+ fprintf(stderr, _("Unrecognised image option '%s'\n"), opt);
+
+ return 1;
+}
+
+
+static int parse_image_options(const char *opth, struct frame *parent,
+ double *wp, double *hp, char **filenamep)
+{
+ int i;
+ size_t len;
+ size_t start;
+ char *opt;
+
+ if ( opth == NULL ) return 1;
+
+ opt = strdup(opth);
+
+ len = strlen(opt);
+ start = 0;
+
+ for ( i=0; i<len; i++ ) {
+
+ /* FIXME: comma might be escaped or quoted */
+ if ( opt[i] == ',' ) {
+ opt[i] = '\0';
+ if ( parse_image_option(opt+start, parent,
+ wp, hp, filenamep) ) return 1;
+ start = i+1;
+ }
+
+ }
+
+ if ( start != len ) {
+ if ( parse_image_option(opt+start, parent,
+ wp, hp, filenamep) ) return 1;
+ }
+
+ free(opt);
+
+ return 0;
+}
+
+
+static void maybe_recurse_before(SCInterpreter *scin, SCBlock *child)
+{
+ if ( child == NULL ) return;
+
+ sc_interp_save(scin);
+}
+
+
+static void maybe_recurse_after(SCInterpreter *scin, SCBlock *child,
+ Stylesheet *ss)
+{
+ if ( child == NULL ) return;
+
+ sc_interp_add_blocks(scin, child, ss);
+ sc_interp_restore(scin);
+}
+
+
+static void add_newpara(SCBlock *bl, SCInterpreter *scin)
+{
+ Paragraph *last_para;
+ Paragraph *para;
+ struct sc_state *st = &scin->state[scin->j];
+ struct frame *fr = sc_interp_get_frame(scin);
+
+ if ( fr->paras == NULL ) return;
+ last_para = fr->paras[fr->n_paras-1];
+
+ set_newline_at_end(last_para, bl);
+
+ /* The block after the \newpara will always be the first one of the
+ * next paragraph, by definition, even if it's \f or another \newpara */
+ para = create_paragraph(fr, sc_block_next(bl));
+ set_para_alignment(para, st->alignment);
+ set_para_spacing(para, st->paraspace);
+}
+
+
+/* Add the SCBlock to the text in 'frame', at the end */
+static int add_text(struct frame *fr, PangoContext *pc, SCBlock *bl,
+ PangoLanguage *lang, int editable, SCInterpreter *scin,
+ const char *real_text)
+{
+ const char *text = sc_block_contents(bl);
+ PangoFontDescription *fontdesc;
+ double *col;
+ struct sc_state *st = &scin->state[scin->j];
+ Paragraph *para;
+
+ /* Empty block? */
+ if ( text == NULL && real_text == NULL ) return 1;
+
+ fontdesc = sc_interp_get_fontdesc(scin);
+ col = sc_interp_get_fgcol(scin);
+
+ para = last_para(fr);
+ if ( (para == NULL) || (para_type(para) != PARA_TYPE_TEXT) ) {
+ /* Last paragraph is not text.
+ * or: no paragraphs yet.
+ * Either way: Create the first one */
+ para = create_paragraph(fr, bl);
+ }
+
+ set_para_alignment(para, st->alignment);
+ add_run(para, bl, fontdesc, col, real_text);
+ set_para_spacing(para, st->paraspace);
+
+ return 0;
+}
+
+
+static void apply_style(SCInterpreter *scin, Stylesheet *ss, const char *path)
+{
+ char *result;
+
+ if ( ss == NULL ) return;
+
+ /* Font */
+ result = stylesheet_lookup(ss, path, "font");
+ if ( result != NULL ) set_font(scin, result);
+
+ /* Foreground colour */
+ result = stylesheet_lookup(ss, path, "fgcol");
+ if ( result != NULL ) set_colour(scin, result);
+
+ /* Background (vertical gradient) */
+ result = stylesheet_lookup(ss, path, "bggradv");
+ if ( result != NULL ) set_bggrad(scin, result, GRAD_VERT);
+
+ /* Background (horizontal gradient) */
+ result = stylesheet_lookup(ss, path, "bggradh");
+ if ( result != NULL ) set_bggrad(scin, result, GRAD_HORIZ);
+
+ /* Background (solid colour) */
+ result = stylesheet_lookup(ss, path, "bgcol");
+ if ( result != NULL ) set_bgcol(scin, result);
+
+ /* Padding */
+ result = stylesheet_lookup(ss, path, "pad");
+ if ( result != NULL ) set_padding(sc_interp_get_frame(scin), result);
+
+ /* Paragraph spacing */
+ result = stylesheet_lookup(ss, path, "paraspace");
+ if ( result != NULL ) set_paraspace(scin, result);
+
+ /* Alignment */
+ result = stylesheet_lookup(ss, path, "alignment");
+ if ( result != NULL ) {
+ if ( strcmp(result, "center") == 0 ) {
+ set_alignment(scin, PANGO_ALIGN_CENTER);
+ }
+ if ( strcmp(result, "left") == 0 ) {
+ set_alignment(scin, PANGO_ALIGN_LEFT);
+ }
+ if ( strcmp(result, "right") == 0 ) {
+ set_alignment(scin, PANGO_ALIGN_RIGHT);
+ }
+ }
+}
+
+
+static void output_frame(SCInterpreter *scin, SCBlock *bl, Stylesheet *ss,
+ const char *stylename)
+{
+ struct frame *fr;
+ SCBlock *child = sc_block_child(bl);
+ const char *options = sc_block_options(bl);
+ char *result;
+
+ fr = add_subframe(sc_interp_get_frame(scin));
+ if ( fr == NULL ) {
+ fprintf(stderr, _("Failed to add frame.\n"));
+ return;
+ }
+
+ fr->scblocks = bl;
+ fr->resizable = 1;
+
+ /* Lowest priority: current state of interpreter */
+ set_frame_default_style(fr, scin);
+
+ /* Next priority: geometry from stylesheet */
+ result = stylesheet_lookup(ss, stylename, "geometry");
+ if ( result != NULL ) {
+ parse_frame_options(fr, sc_interp_get_frame(scin), result);
+ }
+
+ /* Highest priority: parameters to \f (or \slidetitle etc) */
+ parse_frame_options(fr, sc_interp_get_frame(scin), options);
+
+ maybe_recurse_before(scin, child);
+ set_frame(scin, fr);
+ apply_style(scin, ss, stylename);
+ maybe_recurse_after(scin, child, ss);
+}
+
+
+static int check_outputs(SCBlock *bl, SCInterpreter *scin, Stylesheet *ss)
+{
+ const char *name = sc_block_name(bl);
+ const char *options = sc_block_options(bl);
+
+ if ( name == NULL ) {
+ add_text(sc_interp_get_frame(scin),
+ scin->pc, bl, scin->lang, 1, scin, NULL);
+
+ } else if ( strcmp(name, "image")==0 ) {
+ double w, h;
+ char *filename;
+ if ( parse_image_options(options, sc_interp_get_frame(scin),
+ &w, &h, &filename) == 0 )
+ {
+ add_image_para(sc_interp_get_frame(scin), bl,
+ filename, scin->is, w, h, 1);
+ free(filename);
+ } else {
+ fprintf(stderr, _("Invalid image options '%s'\n"),
+ options);
+ }
+
+ } else if ( strcmp(name, "f")==0 ) {
+ output_frame(scin, bl, ss, "$.slide.frame");
+
+ } else if ( strcmp(name, "slidetitle")==0 ) {
+ output_frame(scin, bl, ss, "$.slide.slidetitle");
+
+ } else if ( strcmp(name, "prestitle")==0 ) {
+ output_frame(scin, bl, ss, "$.slide.prestitle");
+
+ } else if ( strcmp(name, "author")==0 ) {
+ output_frame(scin, bl, ss, "$.slide.author");
+
+ } else if ( strcmp(name, "footer")==0 ) {
+ output_frame(scin, bl, ss, "$.slide.footer");
+
+ } else if ( strcmp(name, "newpara")==0 ) {
+ add_newpara(bl, scin);
+
+ } else if ( strcmp(name, "slidenumber")==0 ) {
+ char *con = get_constant(scin, SCCONST_SLIDENUMBER);
+ if ( con != NULL ) {
+ add_text(sc_interp_get_frame(scin), scin->pc, bl,
+ scin->lang, 1, scin, con);
+ }
+
+ } else {
+ return 0;
+ }
+
+ return 1; /* handled */
+}
+
+
+int sc_interp_add_block(SCInterpreter *scin, SCBlock *bl, Stylesheet *ss)
+{
+ const char *name = sc_block_name(bl);
+ const char *options = sc_block_options(bl);
+ SCBlock *child = sc_block_child(bl);
+
+ //printf("Running this --------->\n");
+ //show_sc_blocks(bl);
+ //printf("<------------\n");
+
+ if ( check_callback(scin, bl) ) {
+ /* Handled in check_callback, don't do anything else */
+
+ } else if ((sc_interp_get_frame(scin) != NULL)
+ && check_outputs(bl, scin, ss) ) {
+ /* Block handled as output thing */
+
+ } else if ( name == NULL ) {
+ /* Dummy to ensure name != NULL below */
+
+ } else if ( strcmp(name, "presentation") == 0 ) {
+ maybe_recurse_before(scin, child);
+ apply_style(scin, ss, "$.narrative");
+ maybe_recurse_after(scin, child, ss);
+
+ } else if ( strcmp(name, "slide") == 0 ) {
+ maybe_recurse_before(scin, child);
+ apply_style(scin, ss, "$.slide");
+ maybe_recurse_after(scin, child, ss);
+
+ } else if ( strcmp(name, "font") == 0 ) {
+ maybe_recurse_before(scin, child);
+ set_font(scin, options);
+ maybe_recurse_after(scin, child, ss);
+
+ } else if ( strcmp(name, "fontsize") == 0 ) {
+ maybe_recurse_before(scin, child);
+ set_fontsize(scin, options);
+ maybe_recurse_after(scin, child, ss);
+
+ } else if ( strcmp(name, "bold") == 0 ) {
+ maybe_recurse_before(scin, child);
+ set_bold(scin);
+ maybe_recurse_after(scin, child, ss);
+
+ } else if ( strcmp(name, "oblique") == 0 ) {
+ maybe_recurse_before(scin, child);
+ set_oblique(scin);
+ maybe_recurse_after(scin, child, ss);
+
+ } else if ( strcmp(name, "italic") == 0 ) {
+ maybe_recurse_before(scin, child);
+ set_italic(scin);
+ maybe_recurse_after(scin, child, ss);
+
+ } else if ( strcmp(name, "lalign") == 0 ) {
+ maybe_recurse_before(scin, child);
+ set_alignment(scin, PANGO_ALIGN_LEFT);
+ maybe_recurse_after(scin, child, ss);
+
+ } else if ( strcmp(name, "ralign") == 0 ) {
+ maybe_recurse_before(scin, child);
+ set_alignment(scin, PANGO_ALIGN_RIGHT);
+ maybe_recurse_after(scin, child, ss);
+
+ } else if ( strcmp(name, "center") == 0 ) {
+ maybe_recurse_before(scin, child);
+ set_alignment(scin, PANGO_ALIGN_CENTER);
+ maybe_recurse_after(scin, child, ss);
+
+ } else if ( strcmp(name, "fgcol") == 0 ) {
+ maybe_recurse_before(scin, child);
+ set_colour(scin, options);
+ maybe_recurse_after(scin, child, ss);
+
+ } else if ( strcmp(name, "pad") == 0 ) {
+ maybe_recurse_before(scin, child);
+ set_padding(sc_interp_get_frame(scin), options);
+ maybe_recurse_after(scin, child, ss);
+
+ } else if ( strcmp(name, "bgcol") == 0 ) {
+ set_bgcol(scin, options);
+
+ } else if ( strcmp(name, "bggradh") == 0 ) {
+ set_bggrad(scin, options, GRAD_HORIZ);
+
+ } else if ( strcmp(name, "bggradv") == 0 ) {
+ set_bggrad(scin, options, GRAD_VERT);
+
+ } else if ( strcmp(name, "paraspace") == 0 ) {
+ maybe_recurse_before(scin, child);
+ set_paraspace(scin, options);
+ maybe_recurse_after(scin, child, ss);
+
+ } else {
+
+ fprintf(stderr, "Don't know what to do with this:\n");
+ show_sc_block(bl, "");
+
+ }
+
+ return 0;
+}
+
diff --git a/src-old/sc_interp.h b/src-old/sc_interp.h
new file mode 100644
index 0000000..764b532
--- /dev/null
+++ b/src-old/sc_interp.h
@@ -0,0 +1,80 @@
+/*
+ * sc_interp.h
+ *
+ * Copyright © 2014-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SC_INTERP_H
+#define SC_INTERP_H
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <pango/pangocairo.h>
+
+struct frame;
+
+#define SCCONST_SLIDENUMBER (0)
+#define NUM_SC_CONSTANTS (1)
+
+struct presentation;
+typedef struct _scinterp SCInterpreter;
+typedef struct _sccallbacklist SCCallbackList;
+typedef int (*SCCallbackBoxFunc)(SCInterpreter *scin, SCBlock *bl,
+ double *w, double *h, void **, void *);
+typedef cairo_surface_t *(*SCCallbackDrawFunc)(int w, int h, void *, void *);
+typedef int (*SCCallbackClickFunc)(double x, double y, void *, void *);
+
+#include "frame.h"
+#include "imagestore.h"
+#include "stylesheet.h"
+
+extern SCInterpreter *sc_interp_new(PangoContext *pc, PangoLanguage *lang,
+ ImageStore *is, struct frame *top);
+extern void sc_interp_destroy(SCInterpreter *scin);
+
+extern void sc_interp_save(SCInterpreter *scin);
+extern void sc_interp_restore(SCInterpreter *scin);
+
+extern int sc_interp_add_block(SCInterpreter *scin, SCBlock *bl, Stylesheet *ss);
+
+extern void sc_interp_set_constant(SCInterpreter *scin, unsigned int constant,
+ const char *val);
+
+/* Callback lists */
+extern SCCallbackList *sc_callback_list_new();
+extern void sc_callback_list_free(SCCallbackList *cbl);
+extern void sc_callback_list_add_callback(SCCallbackList *cbl, const char *name,
+ SCCallbackBoxFunc box_func,
+ SCCallbackDrawFunc draw_func,
+ SCCallbackClickFunc click_func,
+ void *vp);
+extern void sc_interp_set_callbacks(SCInterpreter *scin, SCCallbackList *cbl);
+
+/* Get the current state of the interpreter */
+extern struct frame *sc_interp_get_frame(SCInterpreter *scin);
+extern PangoFont *sc_interp_get_font(SCInterpreter *scin);
+extern PangoFontDescription *sc_interp_get_fontdesc(SCInterpreter *scin);
+extern double *sc_interp_get_fgcol(SCInterpreter *scin);
+
+extern void update_geom(struct frame *fr);
+
+
+#endif /* SC_INTERP_H */
diff --git a/src-old/sc_parse.c b/src-old/sc_parse.c
new file mode 100644
index 0000000..78f5799
--- /dev/null
+++ b/src-old/sc_parse.c
@@ -0,0 +1,856 @@
+/*
+ * sc_parse.c
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <ctype.h>
+#include <glib.h>
+#include <gio/gio.h>
+
+#include "sc_parse.h"
+#include "utils.h"
+
+struct _scblock
+{
+ char *name;
+ char *options;
+ char *contents;
+
+ SCBlock *next;
+ SCBlock *child;
+};
+
+
+SCBlock *sc_block_new()
+{
+ SCBlock *bl;
+
+ bl = calloc(1, sizeof(SCBlock));
+ if ( bl == NULL ) return NULL;
+
+ return bl;
+}
+
+
+SCBlock *sc_block_next(const SCBlock *bl)
+{
+ assert(bl != NULL);
+ return bl->next;
+}
+
+
+SCBlock *sc_block_child(const SCBlock *bl)
+{
+ return bl->child;
+}
+
+
+const char *sc_block_name(const SCBlock *bl)
+{
+ return bl->name;
+}
+
+
+const char *sc_block_options(const SCBlock *bl)
+{
+ return bl->options;
+}
+
+
+const char *sc_block_contents(const SCBlock *bl)
+{
+ return bl->contents;
+}
+
+
+static SCBlock *sc_find_previous(SCBlock *top, SCBlock *find)
+{
+ if ( top->next == find ) return top;
+
+ if ( top->child != NULL ) {
+ SCBlock *t = sc_find_previous(top->child, find);
+ if ( t != NULL ) return t;
+ }
+ if ( top->next != NULL ) {
+ SCBlock *t = sc_find_previous(top->next, find);
+ if ( t != NULL ) return t;
+ }
+ return NULL;
+}
+
+
+/* Add a new block before "bl" */
+SCBlock *sc_block_prepend(SCBlock *bl, SCBlock *top)
+{
+ SCBlock *bln;
+ SCBlock *prev;
+
+ prev = sc_find_previous(top, bl);
+ if ( prev == NULL ) {
+ fprintf(stderr, "Couldn't find previous\n");
+ return NULL;
+ }
+
+ bln = sc_block_new();
+ if ( bln == NULL ) return NULL;
+
+ prev->next = bln;
+ bln->next = bl;
+ return bln;
+}
+
+
+/* Append "bln" after "bl" */
+void sc_block_append_p(SCBlock *bl, SCBlock *bln)
+{
+ if ( bl != NULL ) {
+ bln->next = bl->next;
+ bl->next = bln;
+ }
+}
+
+
+/* Insert a new block after "bl". "name", "options" and "contents"
+ * will not be copied. Returns the block just created, or NULL on error.
+ * If *blfp points to NULL, it will updated to point at the new block. */
+SCBlock *sc_block_append(SCBlock *bl, char *name, char *opt, char *contents,
+ SCBlock **blfp)
+{
+ SCBlock *bln = sc_block_new();
+
+ if ( bln == NULL ) return NULL;
+
+ bln->name = name;
+ bln->options = opt;
+ bln->contents = contents;
+ bln->child = NULL;
+ bln->next = NULL;
+
+ sc_block_append_p(bl, bln);
+
+ if ( (blfp != NULL) && (*blfp == NULL) ) {
+ *blfp = bln;
+ }
+
+ return bln;
+}
+
+
+/* Insert a new block at the end of the chain starting 'bl'.
+ * "name", "options" and "contents" will not be copied. Returns the block just
+ * created, or NULL on error. */
+SCBlock *sc_block_append_end(SCBlock *bl, char *name, char *opt, char *contents)
+{
+ SCBlock *bln = sc_block_new();
+
+ if ( bln == NULL ) return NULL;
+
+ while ( bl->next != NULL ) {
+ bln->next = bl->next;
+ bl = bl->next;
+ };
+
+ return sc_block_append(bl, name, opt, contents, NULL);
+}
+
+
+void sc_block_append_block(SCBlock *bl, SCBlock *bln)
+{
+
+ if ( bl == NULL ) return;
+
+ while ( bl->next != NULL ) bl = bl->next;
+
+ bl->next = bln;
+ bln->next = NULL;
+}
+
+
+/* Append a new block to the chain inside "parent".
+ * "name", "options" and "contents" will not be copied. Returns the block just
+ * created, or NULL on error. */
+SCBlock *sc_block_append_inside(SCBlock *parent,
+ char *name, char *opt, char *contents)
+{
+ SCBlock *bln;
+ SCBlock *bl;
+ SCBlock **ptr;
+
+ bl = parent->child;
+ if ( bl != NULL ) {
+ while ( bl->next != NULL ) bl = bl->next;
+ ptr = &bl->next;
+ } else {
+ ptr = &parent->child;
+ }
+
+ bln = sc_block_new();
+ if ( bln == NULL ) return NULL;
+
+ bln->name = name;
+ bln->options = opt;
+ bln->contents = contents;
+ bln->child = NULL;
+ bln->next = NULL;
+
+ *ptr = bln;
+
+ return bln;
+}
+
+
+/* Insert a new block to the chain, just after "afterme".
+ * "name", "options" and "contents" will not be copied. Returns the block just
+ * created, or NULL on error. */
+SCBlock *sc_block_insert_after(SCBlock *afterme,
+ char *name, char *opt, char *contents)
+{
+ SCBlock *bl;
+
+ bl = sc_block_new();
+ if ( bl == NULL ) return NULL;
+
+ bl->name = name;
+ bl->options = opt;
+ bl->contents = contents;
+ bl->child = NULL;
+ bl->next = afterme->next;
+ afterme->next = bl;
+
+ return bl;
+}
+
+
+static SCBlock *sc_find_parent(SCBlock *top, SCBlock *find)
+{
+ if ( top->child == find ) return top;
+ if ( top->next == find ) return top;
+
+ if ( top->child != NULL ) {
+ SCBlock *t = sc_find_parent(top->child, find);
+ if ( t != NULL ) return t;
+ }
+ if ( top->next != NULL ) {
+ SCBlock *t = sc_find_parent(top->next, find);
+ if ( t != NULL ) return t;
+ }
+ return NULL;
+}
+
+
+/* Unlink "deleteme", which is somewhere under "top" */
+static int sc_block_unlink(SCBlock **top, SCBlock *deleteme)
+{
+ SCBlock *parent = sc_find_parent(*top, deleteme);
+ if ( parent == NULL ) {
+ /* Maybe it's the first block? */
+ if ( *top == deleteme ) {
+ fprintf(stderr, "Unlinking at top\n");
+ *top = (*top)->next;
+ return 0;
+ } else {
+ fprintf(stderr, "Couldn't find block parent!\n");
+ return 1;
+ }
+ }
+
+ if ( parent->next == deleteme ) {
+ parent->next = deleteme->next;
+ }
+
+ if ( parent->child == deleteme ) {
+ parent->child = deleteme->next;
+ }
+ return 0;
+}
+
+
+/* Delete "deleteme", which is somewhere under "top" */
+int sc_block_delete(SCBlock **top, SCBlock *deleteme)
+{
+ int r;
+ r = sc_block_unlink(top, deleteme);
+ if ( !r ) {
+ sc_block_free(deleteme);
+ }
+ return r;
+}
+
+
+/* Frees "bl" and all its children (but not the blocks following it) */
+void sc_block_free(SCBlock *bl)
+{
+ if ( bl == NULL ) return;
+
+ if ( bl->child != NULL ) {
+ SCBlock *ch = bl->child;
+ while ( ch != NULL ) {
+ SCBlock *next = ch->next;
+ sc_block_free(ch);
+ ch = next;
+ }
+ }
+
+ free(bl);
+}
+
+
+/* Serialise one block (including children) */
+char *serialise_sc_block(const SCBlock *bl)
+{
+ char *a;
+ SCBlock *ch;
+ size_t len = 3;
+
+ if ( bl == NULL ) return strdup("");
+
+ if ( bl->name != NULL ) len += 1+strlen(bl->name);
+ if ( bl->options != NULL ) len += 2+strlen(bl->options);
+ if ( bl->contents != NULL ) len += 2+strlen(bl->contents);
+ a = malloc(len);
+ if ( a == NULL ) return NULL;
+ a[0] = '\0';
+
+ if ( bl->name == NULL ) {
+ strcat(a, bl->contents);
+ } else if ( strcmp(bl->name, "newpara") == 0 ) {
+ strcat(a, "\n");
+
+ } else {
+
+ strcat(a, "\\");
+ strcat(a, bl->name);
+ if ( bl->options != NULL ) {
+ strcat(a, "[");
+ strcat(a, bl->options);
+ strcat(a, "]");
+ }
+ if ( (bl->contents != NULL) || (bl->child != NULL) ) {
+ strcat(a, "{");
+ }
+ if ( bl->contents != NULL ) {
+ strcat(a, bl->contents);
+ }
+
+ /* Special case to prevent "\somethingSome text" */
+ if ( (bl->name != NULL) && (bl->options == NULL)
+ && (bl->contents == NULL) && (bl->next != NULL)
+ && (bl->next->name == NULL) && (bl->child == NULL) )
+ {
+ strcat(a, "{}");
+ }
+
+ }
+
+ /* Add ALL child blocks of this one */
+ ch = bl->child;
+ while ( ch != NULL ) {
+
+ char *anew;
+ char *c = serialise_sc_block(ch);
+ if ( c == NULL ) {
+ free(a);
+ return NULL;
+ }
+
+ len += strlen(c);
+
+ anew = realloc(a, len);
+ if ( anew == NULL ) {
+ return NULL;
+ } else {
+ a = anew;
+ }
+
+ strcat(a, c);
+ free(c);
+
+ ch = ch->next;
+
+ }
+
+ if ( (bl->name != NULL) &&
+ ((bl->contents != NULL) || (bl->child != NULL)) ) {
+ strcat(a, "}");
+ }
+
+ return a;
+}
+
+
+int save_sc_block(GOutputStream *fh, const SCBlock *bl)
+{
+ while ( bl != NULL ) {
+ GError *error = NULL;
+ char *a = serialise_sc_block(bl);
+ gssize r;
+ if ( a == NULL ) {
+ fprintf(stderr, "Failed to serialise block\n");
+ return 1;
+ }
+ r = g_output_stream_write(fh, a, strlen(a), NULL, &error);
+ if ( r == -1 ) {
+ fprintf(stderr, "Write failed: %s\n", error->message);
+ return 1;
+ }
+ free(a);
+ bl = bl->next;
+ }
+ return 0;
+}
+
+
+static void recursive_show_sc_blocks(const char *prefix, const SCBlock *bl)
+{
+ while ( bl != NULL ) {
+ show_sc_block(bl, prefix);
+ bl = bl->next;
+ }
+}
+
+
+void show_sc_block(const SCBlock *bl, const char *prefix)
+{
+ printf("%s (%p) ", prefix, bl);
+ if ( bl == NULL ) return;
+ if ( bl->name != NULL ) printf("\\%s ", bl->name);
+ if ( bl->options != NULL ) printf("[%s] ", bl->options);
+ if ( bl->contents != NULL ) printf("{%s} ", bl->contents);
+ printf("\n");
+
+ if ( bl->child != NULL ) {
+ char new_prefix[strlen(prefix)+3];
+ strcpy(new_prefix, " ");
+ strcat(new_prefix, prefix);
+ recursive_show_sc_blocks(new_prefix, bl->child);
+ }
+}
+
+
+void show_sc_blocks(const SCBlock *bl)
+{
+ recursive_show_sc_blocks("", bl);
+}
+
+
+static int get_subexpr(const char *sc, char *bk, char **pcontents, int *err)
+{
+ size_t ml;
+ int i;
+ int bct = 1;
+ int found = 0;
+ char *contents;
+
+ *err = 0;
+
+ ml = strlen(sc);
+ contents = malloc(ml+1);
+ if ( contents == NULL ) {
+ *err = -1;
+ return 0;
+ }
+ *pcontents = contents;
+
+ for ( i=0; i<ml; i++ ) {
+ if ( sc[i] == bk[0] ) {
+ bct++;
+ } else if ( sc[i] == bk[1] ) {
+ bct--;
+ }
+ if ( bct == 0 ) {
+ found = 1;
+ break;
+ }
+ contents[i] = sc[i];
+ }
+
+ if ( (!found) || (bct != 0) ) {
+ *err = 1;
+ return 0;
+ }
+
+ contents[i] = '\0';
+ return i+1;
+}
+
+
+static size_t read_block(const char *sc, char **pname, char **options,
+ char **contents, int *err)
+{
+ size_t l, i, j;
+ char *name;
+ int done;
+
+ *err = 0;
+
+ l = strlen(sc);
+ i = 0; j = 0;
+ name = malloc(l+1);
+ if ( name == NULL ) {
+ *err = 1;
+ return 0;
+ }
+
+ done = 0;
+ do {
+
+ char c = sc[i];
+
+ if ( isalnum(c) ) {
+ name[j++] = c;
+ i++;
+ } else {
+ /* Found the end of the name */
+ done = 1;
+ }
+
+ } while ( !done && (i<l) );
+
+ name[j] = '\0';
+ *pname = name;
+
+ if ( sc[i] == '[' ) {
+
+ i += get_subexpr(sc+i+1, "[]", options, err) + 1;
+ if ( *err ) {
+ printf("Couldn't find end of options '%s'\n", sc+i);
+ return 0;
+ }
+
+ } else {
+ *options = NULL;
+ }
+
+ if ( sc[i] == '{' ) {
+
+ i += get_subexpr(sc+i+1, "{}", contents, err) + 1;
+ if ( *err ) {
+ printf("Couldn't find end of content '%s'\n", sc+i);
+ return 0;
+ }
+
+ } else {
+ *contents = NULL;
+ }
+
+ return i+1;
+}
+
+
+static void separate_newlines(SCBlock *bl)
+{
+ while ( bl != NULL ) {
+
+ const char *contents = sc_block_contents(bl);
+
+ if ( contents != NULL ) {
+ char *npos = strchr(contents, '\n');
+ if ( npos != NULL ) {
+ SCBlock *nb = NULL;
+ if ( npos == contents ) {
+ bl->name = strdup("newpara");
+ bl->contents = NULL;
+ nb = bl;
+ } else {
+ sc_block_append(bl, strdup("newpara"),
+ NULL, NULL, &nb);
+ }
+
+ /* Add any text after the \n */
+ if ( strlen(npos+1) > 0 ) {
+ sc_block_append(nb, NULL, NULL,
+ strdup(npos+1), &nb);
+ }
+ npos[0] = '\0';
+ }
+ }
+
+ if ( sc_block_child(bl) != NULL ) {
+ separate_newlines(sc_block_child(bl));
+ }
+
+ bl = sc_block_next(bl);
+
+ }
+}
+
+
+SCBlock *sc_parse(const char *sc)
+{
+ SCBlock *bl;
+ SCBlock *blf = NULL;
+ char *tbuf;
+ size_t len, i, j;
+
+ if ( sc == NULL ) return NULL;
+
+ if ( strlen(sc) == 0 ) {
+ SCBlock *bl = sc_block_new();
+ sc_block_set_contents(bl, g_strdup(""));
+ return bl;
+ }
+
+ bl = NULL;
+
+ len = strlen(sc);
+ tbuf = malloc(len+1);
+ if ( tbuf == NULL ) {
+ sc_block_free(bl);
+ return NULL;
+ }
+
+ i = 0; j = 0;
+ do {
+
+ if ( sc[i] == '\\' ) {
+
+ int err;
+ char *name = NULL;
+ char *opt = NULL;
+ char *contents = NULL;
+
+ /* Is this an escaped backslash? */
+ if ( sc[i+1] == '\\' ) {
+ tbuf[j++] = '\\';
+ i += 2; /* Skip both backslashes */
+ continue;
+ }
+
+ /* No, it's a real block. Dispatch the previous block */
+ if ( j != 0 ) {
+ tbuf[j] = '\0';
+ bl = sc_block_append(bl, NULL, NULL,
+ strdup(tbuf), &blf);
+ if ( bl == NULL ) {
+ fprintf(stderr, "Block add failed.\n");
+ sc_block_free(blf);
+ free(tbuf);
+ return NULL;
+ }
+ j = 0;
+ }
+
+ i += read_block(sc+i+1, &name, &opt, &contents, &err);
+ if ( err ) {
+ printf(_("Parse error\n"));
+ sc_block_free(blf);
+ free(tbuf);
+ return NULL;
+ }
+
+ bl = sc_block_append(bl, name, opt, contents, &blf);
+ if ( bl == NULL ) {
+ fprintf(stderr, "Block add failed.\n");
+ sc_block_free(blf);
+ free(tbuf);
+ return NULL;
+ }
+ bl->child = sc_parse(contents);
+ free(bl->contents);
+ bl->contents = NULL;
+
+ } else {
+
+ tbuf[j++] = sc[i++];
+ }
+
+ } while ( i<len );
+
+ /* Add final block, if it exists */
+ if ( j > 0 ) {
+
+ /* Leftover buffer is empty? */
+ if ( (j==1) && (tbuf[0]=='\0') ) return bl;
+
+ tbuf[j] = '\0';
+ bl = sc_block_append(bl, NULL, NULL, tbuf, &blf);
+ if ( bl == NULL ) {
+ fprintf(stderr, "Block add failed.\n");
+ sc_block_free(blf);
+ free(tbuf);
+ return NULL;
+ }
+ j = 0;
+ }
+
+ separate_newlines(blf);
+
+ return blf;
+}
+
+
+void sc_block_set_name(SCBlock *bl, char *nam)
+{
+ if ( bl == NULL ) {
+ fprintf(stderr, "sc_block_set_name: NULL block\n");
+ return;
+ }
+ free(bl->name);
+ bl->name = nam;
+}
+
+
+void sc_block_set_options(SCBlock *bl, char *opt)
+{
+ free(bl->options);
+ bl->options = opt;
+}
+
+
+void sc_block_set_contents(SCBlock *bl, char *con)
+{
+ g_free(bl->contents);
+ bl->contents = con;
+}
+
+
+void sc_insert_text(SCBlock *b1, size_t o1, const char *t)
+{
+ size_t len;
+ char *cnew;
+ char *tmp;
+ char *p1;
+
+ if ( b1->contents == NULL ) {
+ b1->contents = strdup(t);
+ return;
+ }
+ len = strlen(b1->contents)+1+strlen(t);
+
+ cnew = realloc(b1->contents, len);
+ if ( cnew == NULL ) return;
+
+ tmp = malloc(len);
+ if ( tmp == NULL ) {
+ free(cnew);
+ return;
+ }
+
+ p1 = cnew + o1;
+ strcpy(tmp, p1);
+ strcpy(p1, t);
+ strcpy(p1+strlen(t), tmp);
+ free(tmp);
+ b1->contents = cnew;
+}
+
+
+void sc_insert_block(SCBlock *b1, int o1, SCBlock *ins)
+{
+ SCBlock *second;
+ char *p1 = g_utf8_offset_to_pointer(b1->contents, o1);
+
+ /* Create a new block containing the second half of b1 */
+ second = sc_block_new();
+ sc_block_set_contents(second, g_strdup(p1));
+
+ /* Chop off b1 at the insertion point */
+ sc_block_set_contents(b1, g_utf8_substring(b1->contents, 0, o1));
+
+ /* Link the new block into the chain */
+ SCBlock *old_next = b1->next;
+ b1->next = ins;
+ ins->next = second;
+ second->next = old_next;
+}
+
+
+/* Delete text from SCBlock contents. o2=-1 means "to the end".
+ * Returns the number of bytes deleted. */
+size_t scblock_delete_text(SCBlock *b, ssize_t o1, ssize_t o2)
+{
+ size_t len;
+
+ if ( b->contents == NULL ) {
+ fprintf(stderr, "Deleting text from block \\%s\n", b->name);
+ return 0;
+ }
+
+ if ( (o2 != -1) && (o1 > o2) ) {
+ ssize_t t = o2;
+ o2 = o1;
+ o1 = t;
+ }
+
+ len = strlen(b->contents);
+ if ( o2 < 0 ) o2 = len;
+ if ( (o1 >= o2) || (o1 > len) || (o2 > len) ) {
+ fprintf(stderr, "Invalid delete: %i %i %i\n",
+ (int)o1, (int)o2, (int)len);
+ return 0;
+ }
+ memmove(b->contents+o1, b->contents+o2, len-o2+1);
+
+ return o2-o1;
+}
+
+
+static char *s_strdup(const char *a)
+{
+ if ( a == NULL ) return NULL;
+ return strdup(a);
+}
+
+
+SCBlock *sc_block_split(SCBlock *bl, size_t pos)
+{
+ SCBlock *n = sc_block_new();
+
+ if ( bl->child != NULL ) {
+ fprintf(stderr, "Splitting a block with a child!\n");
+ return NULL;
+ }
+
+ /* Second block */
+ n->name = s_strdup(bl->name);
+ n->options = s_strdup(bl->options);
+ if ( bl->contents != NULL ) {
+ n->contents = strdup(bl->contents+pos);
+ /* Truncate the first block */
+ bl->contents[pos] = '\0';
+ } else {
+ n->contents = NULL;
+ }
+
+ n->next = bl->next;
+ bl->next = n;
+
+ return n;
+}
+
+
+/* Return a new block which is the parent for "bl" */
+SCBlock *sc_block_new_parent(SCBlock *bl, const char *name)
+{
+ SCBlock *n = sc_block_new();
+ if ( n == NULL ) return NULL;
+ n->name = s_strdup(name);
+ n->child = bl;
+ return n;
+}
diff --git a/src-old/sc_parse.h b/src-old/sc_parse.h
new file mode 100644
index 0000000..17ce2dd
--- /dev/null
+++ b/src-old/sc_parse.h
@@ -0,0 +1,85 @@
+/*
+ * sc_parse.h
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SC_PARSE_H
+#define SC_PARSE_H
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <gio/gio.h>
+
+typedef struct _scblock SCBlock;
+
+extern SCBlock *sc_parse(const char *sc);
+
+extern SCBlock *sc_block_new(void);
+extern void sc_block_free(SCBlock *bl);
+
+extern SCBlock *sc_block_next(const SCBlock *bl);
+extern SCBlock *sc_block_child(const SCBlock *bl);
+extern const char *sc_block_name(const SCBlock *bl);
+extern const char *sc_block_options(const SCBlock *bl);
+extern const char *sc_block_contents(const SCBlock *bl);
+
+extern SCBlock *sc_block_append(SCBlock *bl,
+ char *name, char *opt, char *contents,
+ SCBlock **blfp);
+
+extern SCBlock *sc_block_new_parent(SCBlock *bl, const char *name);
+
+extern SCBlock *sc_block_prepend(SCBlock *bl, SCBlock *top);
+
+extern void sc_block_append_p(SCBlock *bl, SCBlock *bln);
+
+extern void sc_block_append_block(SCBlock *bl, SCBlock *bln);
+
+extern SCBlock *sc_block_append_end(SCBlock *bl,
+ char *name, char *opt, char *contents);
+
+extern SCBlock *sc_block_append_inside(SCBlock *parent,
+ char *name, char *opt, char *contents);
+
+extern SCBlock *sc_block_insert_after(SCBlock *afterme,
+ char *name, char *opt, char *contents);
+
+extern int sc_block_delete(SCBlock **top, SCBlock *deleteme);
+
+
+extern void sc_block_set_name(SCBlock *bl, char *nam);
+extern void sc_block_set_options(SCBlock *bl, char *opt);
+extern void sc_block_set_contents(SCBlock *bl, char *con);
+extern void sc_insert_text(SCBlock *b1, size_t o1, const char *t);
+extern void sc_insert_block(SCBlock *b1, int o1, SCBlock *ins);
+extern SCBlock *sc_block_split(SCBlock *bl, size_t pos);
+
+extern void show_sc_blocks(const SCBlock *bl);
+extern void show_sc_block(const SCBlock *bl, const char *prefix);
+
+extern char *serialise_sc_block(const SCBlock *bl);
+extern int save_sc_block(GOutputStream *fh, const SCBlock *bl);
+
+extern size_t scblock_delete_text(SCBlock *b, ssize_t o1, ssize_t o2);
+
+#endif /* SC_PARSE_H */
diff --git a/src-old/slide_window.c b/src-old/slide_window.c
new file mode 100644
index 0000000..ed37a50
--- /dev/null
+++ b/src-old/slide_window.c
@@ -0,0 +1,282 @@
+/*
+ * slide_window.c
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdlib.h>
+#include <string.h>
+#include <gtk/gtk.h>
+#include <assert.h>
+#include <gdk/gdkkeysyms.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+#include <math.h>
+
+#include "colloquium.h"
+#include "presentation.h"
+#include "slide_window.h"
+#include "render.h"
+#include "frame.h"
+#include "slideshow.h"
+#include "pr_clock.h"
+#include "sc_parse.h"
+#include "sc_interp.h"
+#include "sc_editor.h"
+#include "utils.h"
+
+
+struct _slidewindow
+{
+ struct presentation *p;
+ GtkWidget *window;
+ GtkToolItem *bfirst;
+ GtkToolItem *bprev;
+ GtkToolItem *bnext;
+ GtkToolItem *blast;
+
+ /* The slide being displayed. Note that the SCEditor is using the
+ * child of this block, but we need to keep a record of it for changing
+ * the slide. */
+ SCBlock *scblocks;
+
+ SCEditor *sceditor;
+};
+
+
+static void insert_slidetitle_sig(GSimpleAction *action, GVariant *parameter,
+ gpointer vp)
+{
+ SlideWindow *sw = vp;
+ sc_editor_add_storycode(sw->sceditor, "\\slidetitle{Slide title}");
+}
+
+
+static void paste_sig(GSimpleAction *action, GVariant *parameter,
+ gpointer vp)
+{
+ SlideWindow *sw = vp;
+ sc_editor_paste(sw->sceditor);
+}
+
+
+static void copy_frame_sig(GSimpleAction *action, GVariant *parameter,
+ gpointer vp)
+{
+ SlideWindow *sw = vp;
+ sc_editor_copy_selected_frame(sw->sceditor);
+}
+
+
+static void delete_frame_sig(GSimpleAction *action, GVariant *parameter,
+ gpointer vp)
+{
+ SlideWindow *sw = vp;
+ sc_editor_delete_selected_frame(sw->sceditor);
+}
+
+
+void slide_window_update(SlideWindow *sw)
+{
+ sc_editor_set_scblock(sw->sceditor, sw->scblocks);
+}
+
+
+/* Change the editor's slide to "np" */
+static void change_edit_slide(SlideWindow *sw, SCBlock *np)
+{
+ sc_editor_set_slidenum(sw->sceditor, slide_number(sw->p, np));
+ sc_editor_set_scblock(sw->sceditor, np);
+ sw->scblocks = np;
+}
+
+
+static void change_slide_first(SlideWindow *sw)
+{
+ change_edit_slide(sw, first_slide(sw->p));
+}
+
+
+static void change_slide_backwards(SlideWindow *sw)
+{
+ SCBlock *tt;
+ tt = prev_slide(sw->p, sw->scblocks);
+ if ( tt == NULL ) return;
+ change_edit_slide(sw, tt);
+}
+
+
+static void change_slide_forwards(SlideWindow *sw)
+{
+ SCBlock *tt;
+ tt = next_slide(sw->p, sw->scblocks);
+ if ( tt == NULL ) return;
+ change_edit_slide(sw, tt);
+}
+
+
+static void change_slide_last(SlideWindow *sw)
+{
+ change_edit_slide(sw, last_slide(sw->p));
+}
+
+
+static void first_slide_sig(GSimpleAction *action, GVariant *parameter,
+ gpointer vp)
+{
+ SlideWindow *sw = vp;
+ change_slide_first(sw);
+}
+
+
+static void prev_slide_sig(GSimpleAction *action, GVariant *parameter,
+ gpointer vp)
+{
+ SlideWindow *sw = vp;
+ change_slide_backwards(sw);
+}
+
+
+static void next_slide_sig(GSimpleAction *action, GVariant *parameter,
+ gpointer vp)
+{
+ SlideWindow *sw = vp;
+ change_slide_forwards(sw);
+}
+
+
+static void last_slide_sig(GSimpleAction *action, GVariant *parameter,
+ gpointer vp)
+{
+ SlideWindow *sw = vp;
+ change_slide_last(sw);
+}
+
+
+static gboolean sw_close_sig(GtkWidget *w, SlideWindow *sw)
+{
+ narrative_window_sw_closed(sw->p->narrative_window, sw);
+ return FALSE;
+}
+
+
+static gboolean sw_key_press_sig(GtkWidget *da, GdkEventKey *event,
+ SlideWindow *sw)
+{
+ switch ( event->keyval ) {
+
+ case GDK_KEY_Page_Up :
+ change_slide_backwards(sw);
+ break;
+
+ case GDK_KEY_Page_Down :
+ change_slide_forwards(sw);
+ break;
+
+ }
+
+ return FALSE;
+}
+
+
+static void about_sig(GSimpleAction *action, GVariant *parameter, gpointer vp)
+{
+ SlideWindow *sw = vp;
+ open_about_dialog(sw->window);
+}
+
+
+GActionEntry sw_entries[] = {
+
+ { "about", about_sig, NULL, NULL, NULL },
+ { "paste", paste_sig, NULL, NULL, NULL },
+ { "copyframe", copy_frame_sig, NULL, NULL, NULL },
+ { "deleteframe", delete_frame_sig, NULL, NULL, NULL },
+ { "first", first_slide_sig, NULL, NULL, NULL },
+ { "prev", prev_slide_sig, NULL, NULL, NULL },
+ { "next", next_slide_sig, NULL, NULL, NULL },
+ { "last", last_slide_sig, NULL, NULL, NULL },
+ { "slidetitle", insert_slidetitle_sig, NULL, NULL, NULL },
+};
+
+
+SlideWindow *slide_window_open(struct presentation *p, SCBlock *scblocks,
+ GApplication *papp)
+{
+ GtkWidget *window;
+ SlideWindow *sw;
+ Colloquium *app = COLLOQUIUM(papp);
+
+ sw = calloc(1, sizeof(SlideWindow));
+ if ( sw == NULL ) return NULL;
+
+ window = gtk_application_window_new(GTK_APPLICATION(app));
+ gtk_window_set_role(GTK_WINDOW(window), "slide");
+ sw->window = window;
+ sw->p = p;
+
+ g_action_map_add_action_entries(G_ACTION_MAP(window), sw_entries,
+ G_N_ELEMENTS(sw_entries), sw);
+
+ g_signal_connect(G_OBJECT(window), "destroy",
+ G_CALLBACK(sw_close_sig), sw);
+
+ sw->scblocks = scblocks;
+ sw->sceditor = sc_editor_new(scblocks, p->stylesheet, p->lang,
+ colloquium_get_imagestore(app));
+ sc_editor_set_slidenum(sw->sceditor, slide_number(sw->p, scblocks));
+ sc_editor_set_scale(sw->sceditor, 1);
+ sc_editor_set_imagestore(sw->sceditor, p->is);
+
+// scroll = gtk_scrolled_window_new(NULL, NULL);
+// gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
+// GTK_POLICY_AUTOMATIC,
+// GTK_POLICY_AUTOMATIC);
+// gtk_container_add(GTK_CONTAINER(scroll), GTK_WIDGET(sw->sceditor));
+// gtk_window_set_focus(GTK_WINDOW(window), GTK_WIDGET(sw->sceditor));
+ g_signal_connect(G_OBJECT(sw->sceditor), "key-press-event",
+ G_CALLBACK(sw_key_press_sig), sw);
+
+ sc_editor_set_logical_size(sw->sceditor,
+ p->slide_width, p->slide_height);
+
+ gtk_window_set_default_size(GTK_WINDOW(window),
+ p->slide_width, p->slide_height);
+
+ gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(sw->sceditor));
+
+ /* Default size */
+// gtk_scrolled_window_set_min_content_width(GTK_SCROLLED_WINDOW(scroll),
+// 1024);
+// gtk_scrolled_window_set_min_content_height(GTK_SCROLLED_WINDOW(scroll),
+// 768);
+ gtk_window_set_resizable(GTK_WINDOW(sw->window), TRUE);
+
+ gtk_widget_show_all(window);
+// gtk_scrolled_window_set_min_content_width(GTK_SCROLLED_WINDOW(scroll),
+// 100);
+// gtk_scrolled_window_set_min_content_height(GTK_SCROLLED_WINDOW(scroll),
+// 100);
+
+ return sw;
+}
diff --git a/src-old/slide_window.h b/src-old/slide_window.h
new file mode 100644
index 0000000..681ec39
--- /dev/null
+++ b/src-old/slide_window.h
@@ -0,0 +1,37 @@
+/*
+ * slide_window.h
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SLIDEWINDOW_H
+#define SLIDEWINDOW_H
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+typedef struct _slidewindow SlideWindow;
+
+extern SlideWindow *slide_window_open(struct presentation *p, SCBlock *scblocks,
+ GApplication *app);
+
+extern void slide_window_update(SlideWindow *sw);
+
+#endif /* SLIDEWINDOW_H */
diff --git a/src-old/slideshow.c b/src-old/slideshow.c
new file mode 100644
index 0000000..6e1c49d
--- /dev/null
+++ b/src-old/slideshow.c
@@ -0,0 +1,240 @@
+/*
+ * slideshow.c
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <gtk/gtk.h>
+#include <gdk/gdkkeysyms.h>
+
+#include "colloquium.h"
+#include "presentation.h"
+#include "render.h"
+#include "pr_clock.h"
+#include "frame.h"
+#include "utils.h"
+
+G_DEFINE_TYPE_WITH_CODE(SCSlideshow, sc_slideshow, GTK_TYPE_WINDOW, NULL)
+
+
+static void sc_slideshow_init(SCSlideshow *ss)
+{
+}
+
+
+void sc_slideshow_class_init(SCSlideshowClass *klass)
+{
+}
+
+
+static void slideshow_rerender(SCSlideshow *ss)
+{
+ int n;
+ gint w, h;
+
+ if ( ss->cur_slide == NULL ) return;
+
+ if ( ss->surface != NULL ) {
+ cairo_surface_destroy(ss->surface);
+ }
+
+ n = slide_number(ss->p, ss->cur_slide);
+ ss->surface = render_sc(ss->cur_slide,
+ ss->slide_width, ss->slide_height,
+ ss->p->slide_width, ss->p->slide_height,
+ ss->p->stylesheet, NULL, ss->p->is, n,
+ &ss->top, ss->p->lang);
+
+ w = gtk_widget_get_allocated_width(GTK_WIDGET(ss->drawingarea));
+ h = gtk_widget_get_allocated_height(GTK_WIDGET(ss->drawingarea));
+
+ gtk_widget_queue_draw_area(ss->drawingarea, 0, 0, w, h);
+}
+
+
+static gint ssh_destroy_sig(GtkWidget *widget, SCSlideshow *ss)
+{
+ if ( ss->blank_cursor != NULL ) {
+ g_object_unref(ss->blank_cursor);
+ }
+ if ( ss->surface != NULL ) {
+ cairo_surface_destroy(ss->surface);
+ }
+ if ( ss->inhibit_cookie ) {
+ gtk_application_uninhibit(ss->app, ss->inhibit_cookie);
+ }
+ return FALSE;
+}
+
+
+static gboolean ss_draw_sig(GtkWidget *da, cairo_t *cr, SCSlideshow *ss)
+{
+ double width, height;
+
+ width = gtk_widget_get_allocated_width(GTK_WIDGET(da));
+ height = gtk_widget_get_allocated_height(GTK_WIDGET(da));
+
+ /* Overall background */
+ cairo_rectangle(cr, 0.0, 0.0, width, height);
+ cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
+ cairo_fill(cr);
+
+ if ( !ss->blank ) {
+
+ /* Draw the slide from the cache */
+ cairo_rectangle(cr, ss->xoff, ss->yoff,
+ ss->slide_width, ss->slide_height);
+ cairo_set_source_surface(cr, ss->surface, ss->xoff, ss->yoff);
+ cairo_fill(cr);
+
+ }
+
+ return FALSE;
+}
+
+
+static gboolean ss_realize_sig(GtkWidget *w, SCSlideshow *ss)
+{
+ if ( (ss->app == NULL) || colloquium_get_hidepointer(COLLOQUIUM(ss->app)) ) {
+
+ /* Hide the pointer */
+ GdkWindow *win;
+ win = gtk_widget_get_window(w);
+ ss->blank_cursor = gdk_cursor_new_for_display(gdk_display_get_default(),
+ GDK_BLANK_CURSOR);
+ gdk_window_set_cursor(GDK_WINDOW(win), ss->blank_cursor);
+
+ } else {
+ ss->blank_cursor = NULL;
+ }
+
+ slideshow_rerender(ss);
+
+ return FALSE;
+}
+
+
+static void ss_size_sig(GtkWidget *widget, GdkRectangle *rect, SCSlideshow *ss)
+{
+ const double sw = ss->p->slide_width;
+ const double sh = ss->p->slide_height;
+
+ if ( sw/sh > (double)rect->width/rect->height ) {
+ /* Slide is too wide. Letterboxing top/bottom */
+ ss->slide_width = rect->width;
+ ss->slide_height = rect->width * sh/sw;
+ } else {
+ /* Letterboxing at sides */
+ ss->slide_width = rect->height * sw/sh;
+ ss->slide_height = rect->height;
+ }
+
+ ss->xoff = (rect->width - ss->slide_width)/2.0;
+ ss->yoff = (rect->height - ss->slide_height)/2.0;
+
+ printf("screen %i %i\n", rect->width, rect->height);
+ printf("slide %f %f\n", sw, sh);
+ printf("rendering slide at %i %i\n", ss->slide_width, ss->slide_height);
+ printf("offset %i %i\n", ss->xoff, ss->yoff);
+
+ slideshow_rerender(ss);
+}
+
+
+void sc_slideshow_set_slide(SCSlideshow *ss, SCBlock *ns)
+{
+ ss->cur_slide = ns;
+ slideshow_rerender(ss);
+}
+
+
+SCSlideshow *sc_slideshow_new(struct presentation *p, GtkApplication *app)
+{
+ GdkDisplay *display;
+ int n_monitors;
+ SCSlideshow *ss;
+
+ ss = g_object_new(SC_TYPE_SLIDESHOW, NULL);
+ if ( ss == NULL ) return NULL;
+
+ ss->blank = 0;
+ ss->p = p;
+ ss->cur_slide = NULL;
+ ss->blank_cursor = NULL;
+ ss->surface = NULL;
+ ss->app = app;
+
+ ss->drawingarea = gtk_drawing_area_new();
+ gtk_container_add(GTK_CONTAINER(ss), ss->drawingarea);
+
+ gtk_widget_set_can_focus(GTK_WIDGET(ss->drawingarea), TRUE);
+ gtk_widget_add_events(GTK_WIDGET(ss->drawingarea),
+ GDK_KEY_PRESS_MASK);
+
+ g_signal_connect(G_OBJECT(ss), "destroy",
+ G_CALLBACK(ssh_destroy_sig), ss);
+ g_signal_connect(G_OBJECT(ss), "realize",
+ G_CALLBACK(ss_realize_sig), ss);
+ g_signal_connect(G_OBJECT(ss), "size-allocate",
+ G_CALLBACK(ss_size_sig), ss);
+
+ g_signal_connect(G_OBJECT(ss->drawingarea), "draw",
+ G_CALLBACK(ss_draw_sig), ss);
+
+ gtk_widget_grab_focus(GTK_WIDGET(ss->drawingarea));
+
+ display = gdk_display_get_default();
+ n_monitors = gdk_display_get_n_monitors(display);
+
+ GdkMonitor *mon_ss;
+ if ( n_monitors == 1 ) {
+ mon_ss = gdk_display_get_primary_monitor(display);
+ printf(_("Single monitor mode\n"));
+ ss->single_monitor = 1;
+ } else {
+ mon_ss = gdk_display_get_monitor(display, 1);
+ printf(_("Dual monitor mode\n"));
+ ss->single_monitor = 0;
+ }
+
+ /* Workaround because gtk_window_fullscreen_on_monitor doesn't work */
+ GdkRectangle rect;
+ gdk_monitor_get_geometry(mon_ss, &rect);
+ gtk_window_move(GTK_WINDOW(ss), rect.x, rect.y);
+ gtk_window_fullscreen(GTK_WINDOW(ss));
+
+ ss->linked = 1;
+
+ if ( app != NULL ) {
+ ss->inhibit_cookie = gtk_application_inhibit(app, GTK_WINDOW(ss),
+ GTK_APPLICATION_INHIBIT_IDLE,
+ _("Presentation slide show is running"));
+ }
+
+ return ss;
+}
+
diff --git a/src-old/slideshow.h b/src-old/slideshow.h
new file mode 100644
index 0000000..ff16d73
--- /dev/null
+++ b/src-old/slideshow.h
@@ -0,0 +1,83 @@
+/*
+ * slideshow.h
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SLIDESHOW_H
+#define SLIDESHOW_H
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <gtk/gtk.h>
+
+#define SC_TYPE_SLIDESHOW (sc_slideshow_get_type())
+
+#define SC_SLIDESHOW(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), \
+ SC_TYPE_SLIDESHOW, SCEditor))
+
+#define SC_IS_SLIDESHOW(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), \
+ SC_TYPE_SLIDESHOW))
+
+#define SC_SLIDESHOW_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((obj), \
+ SC_TYPE_SLIDESHOW, SCEditorClass))
+
+#define SC_IS_SLIDESHOW_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((obj), \
+ SC_TYPE_SLIDESHOW))
+
+#define SC_SLIDESHOW_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), \
+ SC_TYPE_SLIDESHOW, SCSlideShowClass))
+
+struct _scslideshow
+{
+ GtkWindow parent_instance;
+
+ /* <private> */
+ struct presentation *p;
+ SCBlock *cur_slide;
+ GtkWidget *drawingarea;
+ GdkCursor *blank_cursor;
+ int blank;
+ int slide_width;
+ int slide_height;
+ int xoff;
+ int yoff;
+ int linked;
+ cairo_surface_t *surface;
+ struct frame *top;
+ int single_monitor;
+ uint inhibit_cookie;
+ GtkApplication *app;
+};
+
+
+struct _scslideshowclass
+{
+ GtkWindowClass parent_class;
+};
+
+typedef struct _scslideshow SCSlideshow;
+typedef struct _scslideshowclass SCSlideshowClass;
+
+extern SCSlideshow *sc_slideshow_new(struct presentation *p, GtkApplication *app);
+extern void sc_slideshow_set_slide(SCSlideshow *ss, SCBlock *ns);
+
+#endif /* SLIDESHOW_H */
diff --git a/src-old/stylesheet.c b/src-old/stylesheet.c
new file mode 100644
index 0000000..a6e2531
--- /dev/null
+++ b/src-old/stylesheet.c
@@ -0,0 +1,288 @@
+/*
+ * stylesheet.c
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <json-glib/json-glib.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <gio/gio.h>
+#include <gdk/gdk.h>
+
+#include "stylesheet.h"
+#include "utils.h"
+
+
+struct _stylesheet {
+ JsonNode *root;
+};
+
+
+static int find_comma(const char *a)
+{
+ int i = 0;
+ int in_brackets = 0;
+ size_t len = strlen(a);
+
+ do {
+ if ( (a[i] == ',') && !in_brackets ) return i;
+ if ( a[i] == '(' ) in_brackets++;
+ if ( a[i] == ')' ) in_brackets--;
+ i++;
+ } while ( i < len );
+ return 0;
+}
+
+
+int parse_colour_duo(const char *a, GdkRGBA *col1, GdkRGBA *col2)
+{
+ char *acopy;
+ int cpos;
+
+ acopy = strdup(a);
+ if ( acopy == NULL ) return 1;
+
+ cpos = find_comma(acopy);
+ if ( cpos == 0 ) {
+ fprintf(stderr, _("Invalid background gradient '%s'\n"), a);
+ return 1;
+ }
+
+ acopy[cpos] = '\0';
+
+ if ( gdk_rgba_parse(col1, acopy) != TRUE ) {
+ fprintf(stderr, _("Failed to parse colour: %s\n"), acopy);
+ }
+ if ( gdk_rgba_parse(col2, &acopy[cpos+1]) != TRUE ) {
+ fprintf(stderr, _("Failed to parse colour: %s\n"), &acopy[cpos+1]);
+ }
+
+ free(acopy);
+ return 0;
+}
+
+
+Stylesheet *stylesheet_load(GFile *file)
+{
+ JsonParser *parser;
+ gboolean r;
+ GError *err = NULL;
+ Stylesheet *ss;
+ char *everything;
+ gsize len;
+
+ printf("Trying stylesheet '%s'\n", g_file_get_uri(file));
+
+ ss = calloc(1, sizeof(Stylesheet));
+ if ( ss == NULL ) return NULL;
+
+ parser = json_parser_new();
+
+ if ( !g_file_load_contents(file, NULL, &everything, &len, NULL, NULL) ) {
+ fprintf(stderr, _("Failed to load stylesheet '%s'\n"),
+ g_file_get_uri(file));
+ free(ss);
+ return NULL;
+ }
+
+ r = json_parser_load_from_data(parser, everything, len, &err);
+ if ( r == FALSE ) {
+ fprintf(stderr, _("Failed to load stylesheet: %s\n"),
+ err->message);
+ free(ss);
+ return NULL;
+ }
+
+ ss->root = json_parser_steal_root(parser);
+ g_object_unref(parser);
+
+ return ss;
+}
+
+
+static JsonObject *find_stylesheet_object(Stylesheet *ss, const char *path,
+ JsonNode **freeme)
+{
+ JsonNode *node;
+ JsonObject *obj;
+ JsonArray *array;
+ GError *err = NULL;
+
+ node = json_path_query(path, ss->root, &err);
+ array = json_node_get_array(node);
+
+ if ( json_array_get_length(array) == 0 ) {
+ json_node_unref(node);
+ return NULL;
+ }
+
+ if ( json_array_get_length(array) > 1 ) {
+ json_node_unref(node);
+ fprintf(stderr, "More than one result in SS lookup (%s)!\n", path);
+ return NULL;
+ }
+
+ obj = json_array_get_object_element(array, 0);
+ if ( obj == NULL ) {
+ printf("%s not a JSON object\n", path);
+ json_node_unref(node);
+ return NULL;
+ }
+
+ *freeme = node;
+ return obj;
+}
+
+
+char *stylesheet_lookup(Stylesheet *ss, const char *path, const char *key)
+{
+ JsonObject *obj;
+ char *ret = NULL;
+ JsonNode *node = NULL;
+
+ if ( ss == NULL ) {
+ fprintf(stderr, "No stylesheet!\n");
+ return NULL;
+ }
+
+ obj = find_stylesheet_object(ss, path, &node);
+ if ( obj == NULL ) return NULL;
+
+ if ( json_object_has_member(obj, key) ) {
+
+ const gchar *v;
+ v = json_object_get_string_member(obj, key);
+ if ( v != NULL ) {
+ ret = strdup(v);
+ } else {
+ fprintf(stderr, "Error retrieving %s.%s\n", path, key);
+ }
+
+ } /* else not found, too bad */
+
+ if ( node != NULL ) json_node_unref(node);
+ return ret;
+}
+
+
+int stylesheet_set(Stylesheet *ss, const char *path, const char *key,
+ const char *new_val)
+{
+ JsonObject *obj;
+ JsonNode *node = NULL;
+ int r = 1;
+
+ if ( ss == NULL ) {
+ fprintf(stderr, "No stylesheet!\n");
+ return 1;
+ }
+
+ obj = find_stylesheet_object(ss, path, &node);
+ if ( obj != NULL ) {
+ json_object_set_string_member(obj, key, new_val);
+ r = 0;
+ } /* else most likely the object (e.g. "$.slide", "$.slide.frame",
+ * "$.narrative" etc doesn't exist */
+
+ if ( node != NULL ) json_node_unref(node);
+ return r;
+}
+
+
+int stylesheet_delete(Stylesheet *ss, const char *path, const char *key)
+{
+ JsonObject *obj;
+ JsonNode *node = NULL;
+ int r = 1;
+
+ if ( ss == NULL ) {
+ fprintf(stderr, "No stylesheet!\n");
+ return 1;
+ }
+
+ obj = find_stylesheet_object(ss, path, &node);
+ if ( obj != NULL ) {
+ json_object_remove_member(obj, key);
+ r = 0;
+ } /* else most likely the object (e.g. "$.slide", "$.slide.frame",
+ * "$.narrative" etc doesn't exist */
+
+ if ( node != NULL ) json_node_unref(node);
+ return r;
+}
+
+
+void stylesheet_free(Stylesheet *ss)
+{
+ json_node_unref(ss->root);
+ free(ss);
+}
+
+
+int stylesheet_save(Stylesheet *ss, GFile *file)
+{
+ JsonGenerator *gen;
+ GError *error = NULL;
+ GFileOutputStream *fh;
+
+ fh = g_file_replace(file, NULL, FALSE, G_FILE_CREATE_NONE, NULL, &error);
+ if ( fh == NULL ) {
+ fprintf(stderr, _("Open failed: %s\n"), error->message);
+ return 1;
+ }
+
+ gen = json_generator_new();
+ json_generator_set_root(gen, ss->root);
+ json_generator_set_pretty(gen, TRUE);
+ json_generator_set_indent(gen, 1);
+ json_generator_set_indent_char(gen, '\t');
+ if ( !json_generator_to_stream(gen, G_OUTPUT_STREAM(fh), NULL, &error) ) {
+ fprintf(stderr, _("Open failed: %s\n"), error->message);
+ return 1;
+ }
+ g_object_unref(fh);
+ return 0;
+}
+
+
+char *stylesheet_data(Stylesheet *ss)
+{
+ return json_to_string(ss->root, FALSE);
+}
+
+
+void stylesheet_set_data(Stylesheet *ss, const char *data)
+{
+ JsonNode *new_root;
+ GError *err = NULL;
+ new_root = json_from_string(data, &err);
+ if ( new_root == NULL ) {
+ fprintf(stderr, "Internal parse error: %s\n", err->message);
+ return;
+ }
+ json_node_unref(ss->root);
+ ss->root = new_root;
+}
diff --git a/src-old/stylesheet.h b/src-old/stylesheet.h
new file mode 100644
index 0000000..16d0a0a
--- /dev/null
+++ b/src-old/stylesheet.h
@@ -0,0 +1,54 @@
+/*
+ * stylesheet.h
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef STYLESHEET_H
+#define STYLESHEET_H
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <gio/gio.h>
+#include <gdk/gdk.h>
+
+typedef struct _stylesheet Stylesheet;
+
+extern Stylesheet *stylesheet_load(GFile *file);
+
+extern int stylesheet_save(Stylesheet *ss, GFile *file);
+
+extern char *stylesheet_data(Stylesheet *ss);
+
+extern void stylesheet_set_data(Stylesheet *ss, const char *data);
+
+extern int parse_colour_duo(const char *a, GdkRGBA *col1, GdkRGBA *col2);
+
+extern char *stylesheet_lookup(Stylesheet *ss, const char *path, const char *key);
+
+extern int stylesheet_set(Stylesheet *ss, const char *path, const char *key,
+ const char *new_val);
+
+extern int stylesheet_delete(Stylesheet *ss, const char *path, const char *key);
+
+extern void stylesheet_free(Stylesheet *ss);
+
+#endif /* STYLESHEET_H */
diff --git a/src-old/stylesheet_editor.c b/src-old/stylesheet_editor.c
new file mode 100644
index 0000000..292bbf1
--- /dev/null
+++ b/src-old/stylesheet_editor.c
@@ -0,0 +1,794 @@
+/*
+ * stylesheet_editor.c
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <gtk/gtk.h>
+
+#include "stylesheet_editor.h"
+#include "presentation.h"
+#include "sc_interp.h"
+#include "stylesheet.h"
+#include "utils.h"
+
+
+G_DEFINE_TYPE_WITH_CODE(StylesheetEditor, stylesheet_editor,
+ GTK_TYPE_DIALOG, NULL)
+
+
+struct _sspriv
+{
+ struct presentation *p;
+ const gchar *furniture;
+ char *ssdata;
+};
+
+
+static void set_font_from_ss(Stylesheet *ss, const char *path, GtkWidget *w)
+{
+ char *result = stylesheet_lookup(ss, path, "font");
+ if ( result != NULL ) {
+ gtk_font_button_set_font_name(GTK_FONT_BUTTON(w), result);
+ }
+}
+
+
+static void set_col_from_ss(Stylesheet *ss, const char *path, GtkWidget *w)
+{
+ char *result = stylesheet_lookup(ss, path, "fgcol");
+ if ( result != NULL ) {
+ GdkRGBA rgba;
+ if ( gdk_rgba_parse(&rgba, result) == TRUE ) {
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(w), &rgba);
+ }
+ }
+}
+
+
+static void set_vals_from_ss(Stylesheet *ss, const char *path, const char *key,
+ GtkWidget *wl, GtkWidget *wr,
+ GtkWidget *wt, GtkWidget *wb)
+{
+ char *result = stylesheet_lookup(ss, path, key);
+ if ( result != NULL ) {
+ float v[4];
+ if ( parse_tuple(result, v) == 0 ) {
+ gtk_spin_button_set_value(GTK_SPIN_BUTTON(wl), v[0]);
+ gtk_spin_button_set_value(GTK_SPIN_BUTTON(wr), v[1]);
+ gtk_spin_button_set_value(GTK_SPIN_BUTTON(wt), v[2]);
+ gtk_spin_button_set_value(GTK_SPIN_BUTTON(wb), v[3]);
+ } else {
+ fprintf(stderr, _("Failed to parse quad: %s\n"), result);
+ }
+ } else {
+ printf("Not found %s.%s\n", path, key);
+ }
+}
+
+
+static void set_geom_from_ss(Stylesheet *ss, const char *path, const char *key,
+ GtkWidget *ww, GtkWidget *wh,
+ GtkWidget *wx, GtkWidget *wy,
+ GtkWidget *wwu, GtkWidget *whu)
+{
+ char *result = stylesheet_lookup(ss, path, key);
+ if ( result != NULL ) {
+ double x, y, w, h;
+ LengthUnits wu, hu;
+ if ( parse_dims(result, &w, &h, &wu, &hu, &x, &y) == 0 ) {
+ if ( wu == UNITS_FRAC ) {
+ w *= 100;
+ gtk_combo_box_set_active_id(GTK_COMBO_BOX(wwu), "percent");
+ } else {
+ gtk_combo_box_set_active_id(GTK_COMBO_BOX(wwu), "units");
+ }
+ if ( hu == UNITS_FRAC ) {
+ h *= 100;
+ gtk_combo_box_set_active_id(GTK_COMBO_BOX(whu), "percent");
+ } else {
+ gtk_combo_box_set_active_id(GTK_COMBO_BOX(whu), "units");
+ }
+ gtk_spin_button_set_value(GTK_SPIN_BUTTON(ww), w);
+ gtk_spin_button_set_value(GTK_SPIN_BUTTON(wh), h);
+ gtk_spin_button_set_value(GTK_SPIN_BUTTON(wx), x);
+ gtk_spin_button_set_value(GTK_SPIN_BUTTON(wy), y);
+ } else {
+ fprintf(stderr, _("Failed to parse dims: %s\n"), result);
+ }
+ } else {
+ printf("Not found %s.%s\n", path, key);
+ }
+}
+
+static void set_size_from_ss(Stylesheet *ss, const char *path,
+ GtkWidget *ww, GtkWidget *wh)
+{
+ char *result = stylesheet_lookup(ss, path, "size");
+ if ( result != NULL ) {
+ float v[2];
+ if ( parse_double(result, v) == 0 ) {
+ gtk_spin_button_set_value(GTK_SPIN_BUTTON(ww), v[0]);
+ gtk_spin_button_set_value(GTK_SPIN_BUTTON(wh), v[1]);
+ } else {
+ fprintf(stderr, _("Failed to parse double: %s\n"), result);
+ }
+ } else {
+ printf("Not found %s.size\n", path);
+ }
+}
+
+
+static int alignment_ok(const char *a)
+{
+ if ( a == NULL ) return 0;
+ if ( strcmp(a, "left") == 0 ) return 1;
+ if ( strcmp(a, "center") == 0 ) return 1;
+ if ( strcmp(a, "right") == 0 ) return 1;
+ return 0;
+}
+
+
+static void set_alignment_from_ss(Stylesheet *ss, const char *path,
+ GtkWidget *d)
+{
+ char *result = stylesheet_lookup(ss, path, "alignment");
+ if ( alignment_ok(result) ) {
+ gtk_combo_box_set_active_id(GTK_COMBO_BOX(d), result);
+ }
+}
+
+
+static void set_bg_from_ss(Stylesheet *ss, const char *path, GtkWidget *wcol,
+ GtkWidget *wcol2, GtkWidget *wgrad)
+{
+ char *result;
+ int found = 0;
+
+ result = stylesheet_lookup(ss, path, "bgcol");
+ if ( result != NULL ) {
+ GdkRGBA rgba;
+ found = 1;
+ if ( gdk_rgba_parse(&rgba, result) == TRUE ) {
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(wcol), &rgba);
+ gtk_combo_box_set_active_id(GTK_COMBO_BOX(wgrad), "flat");
+ gtk_widget_set_sensitive(wcol, TRUE);
+ gtk_widget_set_sensitive(wcol2, FALSE);
+ } else {
+ fprintf(stderr, _("Failed to parse colour: %s\n"), result);
+ }
+ }
+
+ result = stylesheet_lookup(ss, path, "bggradv");
+ if ( result != NULL ) {
+ GdkRGBA rgba1, rgba2;
+ found = 1;
+ if ( parse_colour_duo(result, &rgba1, &rgba2) == 0 ) {
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(wcol), &rgba1);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(wcol2), &rgba2);
+ gtk_combo_box_set_active_id(GTK_COMBO_BOX(wgrad), "vert");
+ gtk_widget_set_sensitive(wcol, TRUE);
+ gtk_widget_set_sensitive(wcol2, TRUE);
+ }
+ }
+
+ result = stylesheet_lookup(ss, path, "bggradh");
+ if ( result != NULL ) {
+ GdkRGBA rgba1, rgba2;
+ found = 1;
+ if ( parse_colour_duo(result, &rgba1, &rgba2) == 0 ) {
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(wcol), &rgba1);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(wcol2), &rgba2);
+ gtk_combo_box_set_active_id(GTK_COMBO_BOX(wgrad), "horiz");
+ gtk_widget_set_sensitive(wcol, TRUE);
+ gtk_widget_set_sensitive(wcol2, TRUE);
+ }
+ }
+
+ if ( !found ) {
+ GdkRGBA rgba;
+ rgba.red = 1.0;
+ rgba.green = 1.0;
+ rgba.blue = 1.0;
+ rgba.alpha = 0.0;
+ gtk_combo_box_set_active_id(GTK_COMBO_BOX(wgrad), "flat");
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(wcol), &rgba);
+ gtk_widget_set_sensitive(wcol, TRUE);
+ gtk_widget_set_sensitive(wcol2, FALSE);
+ }
+}
+
+
+static void set_furniture(StylesheetEditor *se, const char *furniture)
+{
+ set_geom_from_ss(se->priv->p->stylesheet, furniture, "geometry",
+ se->furniture_w,
+ se->furniture_h,
+ se->furniture_x,
+ se->furniture_y,
+ se->furniture_w_units,
+ se->furniture_h_units);
+
+ set_vals_from_ss(se->priv->p->stylesheet, furniture, "pad",
+ se->furniture_padding_l,
+ se->furniture_padding_r,
+ se->furniture_padding_t,
+ se->furniture_padding_b);
+
+ set_vals_from_ss(se->priv->p->stylesheet, furniture, "paraspace",
+ se->furniture_paraspace_l,
+ se->furniture_paraspace_r,
+ se->furniture_paraspace_t,
+ se->furniture_paraspace_b);
+
+ set_font_from_ss(se->priv->p->stylesheet, furniture, se->furniture_font);
+ set_col_from_ss(se->priv->p->stylesheet, furniture, se->furniture_fgcol);
+ set_alignment_from_ss(se->priv->p->stylesheet, furniture,
+ se->furniture_alignment);
+ set_bg_from_ss(se->priv->p->stylesheet, furniture, se->furniture_bgcol,
+ se->furniture_bgcol2, se->furniture_bggrad);
+}
+
+
+static void set_values_from_presentation(StylesheetEditor *se)
+{
+ Stylesheet *ss = se->priv->p->stylesheet;
+
+ /* Narrative */
+ set_font_from_ss(ss, "$.narrative", se->narrative_style_font);
+ set_col_from_ss(ss, "$.narrative", se->narrative_style_fgcol);
+ set_alignment_from_ss(ss, "$.narrative", se->narrative_style_alignment);
+ set_bg_from_ss(ss, "$.narrative", se->narrative_style_bgcol,
+ se->narrative_style_bgcol2,
+ se->narrative_style_bggrad);
+ set_vals_from_ss(ss, "$.narrative", "pad", se->narrative_style_padding_l,
+ se->narrative_style_padding_r,
+ se->narrative_style_padding_t,
+ se->narrative_style_padding_b);
+ set_vals_from_ss(ss, "$.narrative", "paraspace", se->narrative_style_paraspace_l,
+ se->narrative_style_paraspace_r,
+ se->narrative_style_paraspace_t,
+ se->narrative_style_paraspace_b);
+
+ /* Slides */
+ set_size_from_ss(ss, "$.slide", se->slide_size_w, se->slide_size_h);
+ set_bg_from_ss(ss, "$.slide", se->slide_style_bgcol,
+ se->slide_style_bgcol2,
+ se->slide_style_bggrad);
+
+
+ /* Frames */
+ set_font_from_ss(ss, "$.slide.frame", se->frame_style_font);
+ set_col_from_ss(ss, "$.slide.frame", se->frame_style_fgcol);
+ set_alignment_from_ss(ss, "$.slide.frame", se->frame_style_alignment);
+ set_bg_from_ss(ss, "$.slide.frame", se->frame_style_bgcol,
+ se->frame_style_bgcol2,
+ se->frame_style_bggrad);
+ set_vals_from_ss(ss, "$.slide.frame", "pad", se->frame_style_padding_l,
+ se->frame_style_padding_r,
+ se->frame_style_padding_t,
+ se->frame_style_padding_b);
+ set_vals_from_ss(ss, "$.slide.frame", "paraspace", se->frame_style_paraspace_l,
+ se->frame_style_paraspace_r,
+ se->frame_style_paraspace_t,
+ se->frame_style_paraspace_b);
+
+ set_furniture(se, se->priv->furniture);
+}
+
+
+static GradientType id_to_gradtype(const gchar *id)
+{
+ assert(id != NULL);
+ if ( strcmp(id, "flat") == 0 ) return GRAD_NONE;
+ if ( strcmp(id, "horiz") == 0 ) return GRAD_HORIZ;
+ if ( strcmp(id, "vert") == 0 ) return GRAD_VERT;
+ return GRAD_NONE;
+}
+
+
+static void update_bg(struct presentation *p, const char *style_name,
+ GtkWidget *bggradw, GtkWidget *col1w, GtkWidget*col2w)
+{
+ GradientType g;
+ const gchar *id;
+ GdkRGBA rgba;
+ gchar *col1;
+ gchar *col2;
+ gchar *gradient;
+
+ id = gtk_combo_box_get_active_id(GTK_COMBO_BOX(bggradw));
+ g = id_to_gradtype(id);
+
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(col1w), &rgba);
+ if ( rgba.alpha < 0.000001 ) rgba.alpha = 0.0;
+ col1 = gdk_rgba_to_string(&rgba);
+
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(col2w), &rgba);
+ col2 = gdk_rgba_to_string(&rgba);
+
+ gradient = g_strconcat(col1, ",", col2, NULL);
+
+ switch ( g ) {
+
+ case GRAD_NONE :
+ stylesheet_set(p->stylesheet, style_name, "bgcol",
+ col1);
+ stylesheet_delete(p->stylesheet, style_name, "bggradv");
+ stylesheet_delete(p->stylesheet, style_name, "bggradh");
+ break;
+
+ case GRAD_HORIZ :
+ stylesheet_set(p->stylesheet, style_name, "bggradh",
+ gradient);
+ stylesheet_delete(p->stylesheet, style_name, "bggradv");
+ stylesheet_delete(p->stylesheet, style_name, "bgcol");
+ break;
+
+ case GRAD_VERT :
+ stylesheet_set(p->stylesheet, style_name, "bggradv",
+ gradient);
+ stylesheet_delete(p->stylesheet, style_name, "bggradh");
+ stylesheet_delete(p->stylesheet, style_name, "bgcol");
+ break;
+
+ }
+
+ g_free(gradient);
+ g_free(col1);
+ g_free(col2);
+}
+
+
+static void update_spacing(struct presentation *p, const char *style_name,
+ const char *key, GtkWidget *wl, GtkWidget *wr,
+ GtkWidget *wt, GtkWidget *wb)
+{
+ int v[4];
+ char tmp[256];
+
+ v[0] = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(wl));
+ v[1] = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(wr));
+ v[2] = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(wt));
+ v[3] = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(wb));
+
+ if ( snprintf(tmp, 256, "%i,%i,%i,%i", v[0], v[1], v[2], v[3]) >= 256 ) {
+ fprintf(stderr, "Spacing too long\n");
+ } else {
+ stylesheet_set(p->stylesheet, style_name, key, tmp);
+ }
+}
+
+
+static char units_id_to_char(const char *id)
+{
+ if ( strcmp(id, "units") == 0 ) return 'u';
+ if ( strcmp(id, "percent") == 0 ) return 'f';
+ return 'u';
+}
+
+
+static void update_ss_dims(struct presentation *p, const char *style_name,
+ const char *key, GtkWidget *ww, GtkWidget *wh,
+ GtkWidget *wx, GtkWidget *wy,
+ GtkWidget *wwu, GtkWidget *whu)
+{
+ float w, h, x, y;
+ char w_units, h_units;
+ const gchar *uid;
+ char tmp[256];
+
+ w = gtk_spin_button_get_value(GTK_SPIN_BUTTON(ww));
+ h = gtk_spin_button_get_value(GTK_SPIN_BUTTON(wh));
+ x = gtk_spin_button_get_value(GTK_SPIN_BUTTON(wx));
+ y = gtk_spin_button_get_value(GTK_SPIN_BUTTON(wy));
+ uid = gtk_combo_box_get_active_id(GTK_COMBO_BOX(wwu));
+ w_units = units_id_to_char(uid);
+ uid = gtk_combo_box_get_active_id(GTK_COMBO_BOX(whu));
+ h_units = units_id_to_char(uid);
+
+ if ( w_units == 'f' ) w /= 100.0;
+ if ( h_units == 'f' ) h /= 100.0;
+
+ if ( snprintf(tmp, 256, "%.2f%cx%.2f%c+%.0f+%0.f",
+ w, w_units, h, h_units, x, y) >= 256 )
+ {
+ fprintf(stderr, "Spacing too long\n");
+ } else {
+ stylesheet_set(p->stylesheet, style_name, key, tmp);
+ }
+}
+
+
+static void revert_sig(GtkButton *button, StylesheetEditor *se)
+{
+ stylesheet_set_data(se->priv->p->stylesheet,
+ se->priv->ssdata);
+ set_values_from_presentation(se);
+ g_signal_emit_by_name(se, "changed");
+}
+
+
+static void set_font(GtkFontButton *widget, StylesheetEditor *se,
+ const char *style_name)
+{
+ const gchar *font;
+ font = gtk_font_button_get_font_name(GTK_FONT_BUTTON(widget));
+
+ stylesheet_set(se->priv->p->stylesheet, style_name, "font", font);
+ set_values_from_presentation(se);
+ g_signal_emit_by_name(se, "changed");
+}
+
+
+static void set_col(GtkColorButton *widget, StylesheetEditor *se,
+ const char *style_name, const char *col_name)
+{
+ GdkRGBA rgba;
+ gchar *col;
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(widget), &rgba);
+ col = gdk_rgba_to_string(&rgba);
+ stylesheet_set(se->priv->p->stylesheet, style_name, "fgcol", col);
+ g_free(col);
+ set_values_from_presentation(se);
+ g_signal_emit_by_name(se, "changed");
+}
+
+
+static void narrative_font_sig(GtkFontButton *widget, StylesheetEditor *se)
+{
+ set_font(widget, se, "$.narrative");
+}
+
+
+static void narrative_fgcol_sig(GtkColorButton *widget, StylesheetEditor *se)
+{
+ set_col(widget, se, "$.narrative", "fgcol");
+}
+
+
+static void narrative_bg_sig(GtkColorButton *widget, StylesheetEditor *se)
+{
+ update_bg(se->priv->p, "$.narrative",
+ se->narrative_style_bggrad,
+ se->narrative_style_bgcol,
+ se->narrative_style_bgcol2);
+ set_values_from_presentation(se);
+ g_signal_emit_by_name(se, "changed");
+}
+
+
+static void narrative_alignment_sig(GtkComboBoxText *widget, StylesheetEditor *se)
+{
+ const gchar *id = gtk_combo_box_get_active_id(GTK_COMBO_BOX(widget));
+ stylesheet_set(se->priv->p->stylesheet, "$.narrative", "alignment", id);
+ set_values_from_presentation(se);
+ g_signal_emit_by_name(se, "changed");
+}
+
+
+static void slide_size_sig(GtkSpinButton *widget, StylesheetEditor *se)
+{
+ int w, h;
+ char tmp[256];
+
+ w = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(se->slide_size_w));
+ h = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(se->slide_size_h));
+
+ if ( snprintf(tmp, 256, "%ix%i", w, h) >= 256 ) {
+ fprintf(stderr, _("Slide size too long\n"));
+ } else {
+ stylesheet_set(se->priv->p->stylesheet, "$.slide", "size", tmp);
+ se->priv->p->slide_width = w;
+ se->priv->p->slide_height = h;
+ }
+
+ set_values_from_presentation(se);
+ g_signal_emit_by_name(se, "changed");
+}
+
+
+static void slide_bg_sig(GtkColorButton *widget, StylesheetEditor *se)
+{
+ update_bg(se->priv->p, "$.slide",
+ se->slide_style_bggrad,
+ se->slide_style_bgcol,
+ se->slide_style_bgcol2);
+ set_values_from_presentation(se);
+ g_signal_emit_by_name(se, "changed");
+}
+
+
+static void frame_font_sig(GtkFontButton *widget, StylesheetEditor *se)
+{
+ set_font(widget, se, "$.slide.frame");
+}
+
+
+static void frame_fgcol_sig(GtkColorButton *widget, StylesheetEditor *se)
+{
+ set_col(widget, se, "$.slide.frame", "fgcol");
+}
+
+
+static void frame_bg_sig(GtkColorButton *widget, StylesheetEditor *se)
+{
+ update_bg(se->priv->p, "$.slide.frame",
+ se->frame_style_bggrad,
+ se->frame_style_bgcol,
+ se->frame_style_bgcol2);
+ set_values_from_presentation(se);
+ g_signal_emit_by_name(se, "changed");
+}
+
+
+static void frame_padding_sig(GtkSpinButton *widget, StylesheetEditor *se)
+{
+ update_spacing(se->priv->p, "$.slide.frame", "pad",
+ se->frame_style_padding_l,
+ se->frame_style_padding_r,
+ se->frame_style_padding_t,
+ se->frame_style_padding_b);
+ set_values_from_presentation(se);
+ g_signal_emit_by_name(se, "changed");
+}
+
+
+static void frame_paraspace_sig(GtkSpinButton *widget, StylesheetEditor *se)
+{
+ update_spacing(se->priv->p, "$.slide.frame", "paraspace",
+ se->frame_style_paraspace_l,
+ se->frame_style_paraspace_r,
+ se->frame_style_paraspace_t,
+ se->frame_style_paraspace_b);
+ set_values_from_presentation(se);
+ g_signal_emit_by_name(se, "changed");
+}
+
+
+static void frame_alignment_sig(GtkComboBoxText *widget, StylesheetEditor *se)
+{
+ const gchar *id = gtk_combo_box_get_active_id(GTK_COMBO_BOX(widget));
+ stylesheet_set(se->priv->p->stylesheet, "$.slide.frame", "alignment", id);
+ set_values_from_presentation(se);
+ g_signal_emit_by_name(se, "changed");
+}
+
+
+static void narrative_padding_sig(GtkSpinButton *widget, StylesheetEditor *se)
+{
+ update_spacing(se->priv->p, "$.narrative", "pad",
+ se->narrative_style_padding_l,
+ se->narrative_style_padding_r,
+ se->narrative_style_padding_t,
+ se->narrative_style_padding_b);
+ set_values_from_presentation(se);
+ g_signal_emit_by_name(se, "changed");
+}
+
+
+static void narrative_paraspace_sig(GtkSpinButton *widget, StylesheetEditor *se)
+{
+ update_spacing(se->priv->p, "$.narrative", "paraspace",
+ se->narrative_style_paraspace_l,
+ se->narrative_style_paraspace_r,
+ se->narrative_style_paraspace_t,
+ se->narrative_style_paraspace_b);
+ set_values_from_presentation(se);
+ g_signal_emit_by_name(se, "changed");
+}
+
+
+static void furniture_selector_change_sig(GtkComboBoxText *widget, StylesheetEditor *se)
+{
+ se->priv->furniture = gtk_combo_box_get_active_id(GTK_COMBO_BOX(widget));
+ set_furniture(se, se->priv->furniture);
+}
+
+
+static void furniture_font_sig(GtkFontButton *widget, StylesheetEditor *se)
+{
+ set_font(widget, se, se->priv->furniture);
+}
+
+
+static void furniture_fgcol_sig(GtkColorButton *widget, StylesheetEditor *se)
+{
+ set_col(widget, se,se->priv->furniture, "fgcol");
+}
+
+
+static void furniture_bg_sig(GtkColorButton *widget, StylesheetEditor *se)
+{
+ update_bg(se->priv->p, se->priv->furniture, se->furniture_bggrad,
+ se->furniture_bgcol, se->furniture_bgcol2);
+ set_values_from_presentation(se);
+ g_signal_emit_by_name(se, "changed");
+}
+
+
+static void furniture_paraspace_sig(GtkSpinButton *widget, StylesheetEditor *se)
+{
+ update_spacing(se->priv->p, se->priv->furniture, "pad",
+ se->furniture_padding_l,
+ se->furniture_padding_r,
+ se->furniture_padding_t,
+ se->furniture_padding_b);
+ set_values_from_presentation(se);
+ g_signal_emit_by_name(se, "changed");
+}
+
+
+static void furniture_padding_sig(GtkSpinButton *widget, StylesheetEditor *se)
+{
+ update_spacing(se->priv->p, se->priv->furniture, "pad",
+ se->furniture_padding_l,
+ se->furniture_padding_r,
+ se->furniture_padding_t,
+ se->furniture_padding_b);
+ set_values_from_presentation(se);
+ g_signal_emit_by_name(se, "changed");
+}
+
+
+static void furniture_alignment_sig(GtkComboBoxText *widget, StylesheetEditor *se)
+{
+ const gchar *id = gtk_combo_box_get_active_id(GTK_COMBO_BOX(widget));
+ stylesheet_set(se->priv->p->stylesheet, se->priv->furniture,
+ "alignment", id);
+ set_values_from_presentation(se);
+ g_signal_emit_by_name(se, "changed");
+}
+
+
+static void furniture_dims_sig(GtkSpinButton *widget, StylesheetEditor *se)
+{
+ update_ss_dims(se->priv->p, se->priv->furniture, "geometry",
+ se->furniture_w, se->furniture_h,
+ se->furniture_x, se->furniture_y,
+ se->furniture_w_units, se->furniture_h_units);
+ set_values_from_presentation(se);
+ g_signal_emit_by_name(se, "changed");
+}
+
+
+static void stylesheet_editor_finalize(GObject *obj)
+{
+ StylesheetEditor *se = COLLOQUIUM_STYLESHEET_EDITOR(obj);
+ free(se->priv->ssdata);
+ G_OBJECT_CLASS(stylesheet_editor_parent_class)->finalize(obj);
+}
+
+
+static void stylesheet_editor_init(StylesheetEditor *se)
+{
+ se->priv = G_TYPE_INSTANCE_GET_PRIVATE(se, COLLOQUIUM_TYPE_STYLESHEET_EDITOR,
+ StylesheetEditorPrivate);
+ gtk_widget_init_template(GTK_WIDGET(se));
+}
+
+
+#define SE_BIND_CHILD(a, b) \
+ gtk_widget_class_bind_template_child(widget_class, StylesheetEditor, a); \
+ gtk_widget_class_bind_template_callback(widget_class, b);
+
+void stylesheet_editor_class_init(StylesheetEditorClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
+ GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+
+ gtk_widget_class_set_template_from_resource(widget_class,
+ "/uk/me/bitwiz/Colloquium/stylesheeteditor.ui");
+
+ g_type_class_add_private(gobject_class, sizeof(StylesheetEditorPrivate));
+ gobject_class->finalize = stylesheet_editor_finalize;
+
+ /* Narrative style */
+ SE_BIND_CHILD(narrative_style_font, narrative_font_sig);
+ SE_BIND_CHILD(narrative_style_fgcol, narrative_fgcol_sig);
+ SE_BIND_CHILD(narrative_style_bgcol, narrative_bg_sig);
+ SE_BIND_CHILD(narrative_style_bgcol2, narrative_bg_sig);
+ SE_BIND_CHILD(narrative_style_bggrad, narrative_bg_sig);
+ SE_BIND_CHILD(narrative_style_paraspace_l, narrative_paraspace_sig);
+ SE_BIND_CHILD(narrative_style_paraspace_r, narrative_paraspace_sig);
+ SE_BIND_CHILD(narrative_style_paraspace_t, narrative_paraspace_sig);
+ SE_BIND_CHILD(narrative_style_paraspace_b, narrative_paraspace_sig);
+ SE_BIND_CHILD(narrative_style_padding_l, narrative_padding_sig);
+ SE_BIND_CHILD(narrative_style_padding_r, narrative_padding_sig);
+ SE_BIND_CHILD(narrative_style_padding_t, narrative_padding_sig);
+ SE_BIND_CHILD(narrative_style_padding_b, narrative_padding_sig);
+ SE_BIND_CHILD(narrative_style_alignment, narrative_alignment_sig);
+
+ /* Slide style */
+ SE_BIND_CHILD(slide_size_w, slide_size_sig);
+ SE_BIND_CHILD(slide_size_h, slide_size_sig);
+ SE_BIND_CHILD(slide_style_bgcol, slide_bg_sig);
+ SE_BIND_CHILD(slide_style_bgcol2, slide_bg_sig);
+ SE_BIND_CHILD(slide_style_bggrad, slide_bg_sig);
+
+ /* Slide->frame style */
+ SE_BIND_CHILD(frame_style_font, frame_font_sig);
+ SE_BIND_CHILD(frame_style_fgcol, frame_fgcol_sig);
+ SE_BIND_CHILD(frame_style_bgcol, frame_bg_sig);
+ SE_BIND_CHILD(frame_style_bgcol2, frame_bg_sig);
+ SE_BIND_CHILD(frame_style_bggrad, frame_bg_sig);
+ SE_BIND_CHILD(frame_style_paraspace_l, frame_paraspace_sig);
+ SE_BIND_CHILD(frame_style_paraspace_r, frame_paraspace_sig);
+ SE_BIND_CHILD(frame_style_paraspace_t, frame_paraspace_sig);
+ SE_BIND_CHILD(frame_style_paraspace_b, frame_paraspace_sig);
+ SE_BIND_CHILD(frame_style_padding_l, frame_padding_sig);
+ SE_BIND_CHILD(frame_style_padding_r, frame_padding_sig);
+ SE_BIND_CHILD(frame_style_padding_t, frame_padding_sig);
+ SE_BIND_CHILD(frame_style_padding_b, frame_padding_sig);
+ SE_BIND_CHILD(frame_style_alignment, frame_alignment_sig);
+
+ /* Furniture */
+ SE_BIND_CHILD(furniture_selector, furniture_selector_change_sig);
+ SE_BIND_CHILD(furniture_paraspace_l, furniture_paraspace_sig);
+ SE_BIND_CHILD(furniture_paraspace_r, furniture_paraspace_sig);
+ SE_BIND_CHILD(furniture_paraspace_t, furniture_paraspace_sig);
+ SE_BIND_CHILD(furniture_paraspace_b, furniture_paraspace_sig);
+ SE_BIND_CHILD(furniture_padding_l, furniture_padding_sig);
+ SE_BIND_CHILD(furniture_padding_r, furniture_padding_sig);
+ SE_BIND_CHILD(furniture_padding_t, furniture_padding_sig);
+ SE_BIND_CHILD(furniture_padding_b, furniture_padding_sig);
+ SE_BIND_CHILD(furniture_font, furniture_font_sig);
+ SE_BIND_CHILD(furniture_fgcol, furniture_fgcol_sig);
+ SE_BIND_CHILD(furniture_bgcol, furniture_bg_sig);
+ SE_BIND_CHILD(furniture_bgcol2, furniture_bg_sig);
+ SE_BIND_CHILD(furniture_bggrad, furniture_bg_sig);
+ SE_BIND_CHILD(furniture_alignment, furniture_alignment_sig);
+ SE_BIND_CHILD(furniture_w, furniture_dims_sig);
+ SE_BIND_CHILD(furniture_h, furniture_dims_sig);
+ SE_BIND_CHILD(furniture_x, furniture_dims_sig);
+ SE_BIND_CHILD(furniture_y, furniture_dims_sig);
+ SE_BIND_CHILD(furniture_w_units, furniture_dims_sig);
+ SE_BIND_CHILD(furniture_h_units, furniture_dims_sig);
+
+ gtk_widget_class_bind_template_callback(widget_class, revert_sig);
+
+ g_signal_new("changed", COLLOQUIUM_TYPE_STYLESHEET_EDITOR,
+ G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0);
+}
+
+
+StylesheetEditor *stylesheet_editor_new(struct presentation *p)
+{
+ StylesheetEditor *se;
+
+ se = g_object_new(COLLOQUIUM_TYPE_STYLESHEET_EDITOR, NULL);
+ if ( se == NULL ) return NULL;
+
+ se->priv->p = p;
+ se->priv->furniture = gtk_combo_box_get_active_id(GTK_COMBO_BOX(se->furniture_selector));
+ set_values_from_presentation(se);
+
+ se->priv->ssdata = stylesheet_data(p->stylesheet);
+
+ return se;
+}
+
diff --git a/src-old/stylesheet_editor.h b/src-old/stylesheet_editor.h
new file mode 100644
index 0000000..a7c77b6
--- /dev/null
+++ b/src-old/stylesheet_editor.h
@@ -0,0 +1,128 @@
+/*
+ * stylesheet_editor.h
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef STYLESHEET_EDITOR_H
+#define STYLESHEET_EDITOR_H
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <gtk/gtk.h>
+
+#include "presentation.h"
+#include "frame.h"
+
+#define COLLOQUIUM_TYPE_STYLESHEET_EDITOR (stylesheet_editor_get_type())
+
+#define COLLOQUIUM_STYLESHEET_EDITOR(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), \
+ COLLOQUIUM_TYPE_STYLESHEET_EDITOR, \
+ StylesheetEditor))
+
+#define COLLOQUIUM_IS_STYLESHEET_EDITOR(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), \
+ COLLOQUIUM_TYPE_STYLESHEET_EDITOR))
+
+#define COLLOQUIUM_STYLESHEET_EDITOR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((obj), \
+ COLLOQUIUM_TYPE_STYLESHEET_EDITOR, \
+ StylesheetEditorClass))
+
+#define COLLOQUIUM_IS_STYLESHEET_EDITOR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((obj), \
+ COLLOQUIUM_TYPE_STYLESHEET_EDITOR))
+
+#define COLLOQUIUM_STYLESHEET_EDITOR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), \
+ COLLOQUIUM_TYPE_STYLESHEET_EDITOR, \
+ StylesheetEditorClass))
+
+
+typedef struct _sspriv StylesheetEditorPrivate;
+
+struct _stylesheeteditor
+{
+ GtkDialog parent_instance;
+ GtkWidget *narrative_style_font;
+ GtkWidget *narrative_style_fgcol;
+ GtkWidget *narrative_style_bgcol;
+ GtkWidget *narrative_style_bgcol2;
+ GtkWidget *narrative_style_bggrad;
+ GtkWidget *narrative_style_paraspace_l;
+ GtkWidget *narrative_style_paraspace_r;
+ GtkWidget *narrative_style_paraspace_t;
+ GtkWidget *narrative_style_paraspace_b;
+ GtkWidget *narrative_style_padding_l;
+ GtkWidget *narrative_style_padding_r;
+ GtkWidget *narrative_style_padding_t;
+ GtkWidget *narrative_style_padding_b;
+ GtkWidget *narrative_style_alignment;
+ GtkWidget *slide_size_w;
+ GtkWidget *slide_size_h;
+ GtkWidget *slide_style_bgcol;
+ GtkWidget *slide_style_bgcol2;
+ GtkWidget *slide_style_bggrad;
+ GtkWidget *frame_style_font;
+ GtkWidget *frame_style_fgcol;
+ GtkWidget *frame_style_bgcol;
+ GtkWidget *frame_style_bgcol2;
+ GtkWidget *frame_style_bggrad;
+ GtkWidget *frame_style_paraspace_l;
+ GtkWidget *frame_style_paraspace_r;
+ GtkWidget *frame_style_paraspace_t;
+ GtkWidget *frame_style_paraspace_b;
+ GtkWidget *frame_style_padding_l;
+ GtkWidget *frame_style_padding_r;
+ GtkWidget *frame_style_padding_t;
+ GtkWidget *frame_style_padding_b;
+ GtkWidget *frame_style_alignment;
+ GtkWidget *furniture_selector;
+ GtkWidget *furniture_paraspace_l;
+ GtkWidget *furniture_paraspace_r;
+ GtkWidget *furniture_paraspace_t;
+ GtkWidget *furniture_paraspace_b;
+ GtkWidget *furniture_padding_l;
+ GtkWidget *furniture_padding_r;
+ GtkWidget *furniture_padding_t;
+ GtkWidget *furniture_padding_b;
+ GtkWidget *furniture_font;
+ GtkWidget *furniture_fgcol;
+ GtkWidget *furniture_bgcol;
+ GtkWidget *furniture_bgcol2;
+ GtkWidget *furniture_bggrad;
+ GtkWidget *furniture_alignment;
+ GtkWidget *furniture_w;
+ GtkWidget *furniture_h;
+ GtkWidget *furniture_x;
+ GtkWidget *furniture_y;
+ GtkWidget *furniture_w_units;
+ GtkWidget *furniture_h_units;
+ StylesheetEditorPrivate *priv;
+};
+
+struct _stylesheeteditorclass
+{
+ GtkDialogClass parent_class;
+};
+
+typedef struct _stylesheeteditor StylesheetEditor;
+typedef struct _stylesheeteditorclass StylesheetEditorClass;
+
+extern StylesheetEditor *stylesheet_editor_new(struct presentation *p);
+
+#endif /* STYLESHEET_EDITOR_H */
diff --git a/src-old/testcard.c b/src-old/testcard.c
new file mode 100644
index 0000000..2b6598f
--- /dev/null
+++ b/src-old/testcard.c
@@ -0,0 +1,288 @@
+/*
+ * testcard.c
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <gtk/gtk.h>
+#include <gdk/gdkkeysyms.h>
+
+#include "presentation.h"
+#include "utils.h"
+
+
+struct testcard
+{
+ GtkWidget *window;
+ char geom[256];
+ int slide_width;
+ int slide_height;
+ GtkWidget *drawingarea;
+ struct presentation *p;
+};
+
+static gint tc_destroy_sig(GtkWidget *widget, struct testcard *tc)
+{
+ free(tc);
+ return FALSE;
+}
+
+
+static void arrow_left(cairo_t *cr, double size)
+{
+ cairo_rel_line_to(cr, size, size);
+ cairo_rel_line_to(cr, 0.0, -2*size);
+ cairo_rel_line_to(cr, -size, size);
+}
+
+
+static void arrow_right(cairo_t *cr, double size)
+{
+ cairo_rel_line_to(cr, -size, size);
+ cairo_rel_line_to(cr, 0.0, -2*size);
+ cairo_rel_line_to(cr, size, size);
+}
+
+
+static void arrow_down(cairo_t *cr, double size)
+{
+ cairo_rel_line_to(cr, -size, -size);
+ cairo_rel_line_to(cr, 2*size, 0.0);
+ cairo_rel_line_to(cr, -size, size);
+}
+
+
+static void arrow_up(cairo_t *cr, double size)
+{
+ cairo_rel_line_to(cr, -size, size);
+ cairo_rel_line_to(cr, 2*size, 0.0);
+ cairo_rel_line_to(cr, -size, -size);
+}
+
+
+static void colour_box(cairo_t *cr, double x, double y,
+ double r, double g, double b, const char *label)
+{
+ const double w = 50.0;
+ const double h = 50.0;
+ cairo_text_extents_t size;
+
+ cairo_rectangle(cr, x+0.5, y+0.5, w, h);
+ cairo_set_source_rgb(cr, r, g, b);
+ cairo_fill_preserve(cr);
+ cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
+ cairo_set_line_width(cr, 1.0);
+ cairo_stroke(cr);
+
+ cairo_set_font_size(cr, 24.0);
+ cairo_text_extents(cr, label, &size);
+ cairo_move_to(cr, x+(w/2.0)-(size.width/2.0), y-10.0);
+ cairo_show_text(cr, label);
+
+}
+
+
+static gboolean tc_draw_sig(GtkWidget *da, cairo_t *cr, struct testcard *tc)
+{
+ double xoff, yoff;
+ double width, height;
+ int h;
+ PangoLayout *pl;
+ PangoFontDescription *desc;
+ char tmp[1024];
+ int plw, plh;
+ double xp, yp;
+
+ width = gtk_widget_get_allocated_width(GTK_WIDGET(da));
+ height = gtk_widget_get_allocated_height(GTK_WIDGET(da));
+
+ /* Overall background */
+ cairo_rectangle(cr, 0.0, 0.0, width, height);
+ cairo_set_source_rgb(cr, 0.0, 0.0, 1.0);
+ cairo_fill(cr);
+
+ /* FIXME: Assumes that monitor and slide sizes are such that
+ * letterboxing at sides. This needn't be the case. */
+ h = tc->slide_width * tc->p->slide_height / tc->p->slide_width;
+
+ /* Get the overall size */
+ xoff = (width - tc->slide_width)/2.0;
+ yoff = (height - h)/2.0;
+
+ /* Background of slide */
+ cairo_rectangle(cr, xoff, yoff, tc->slide_width, h);
+ cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
+ cairo_fill(cr);
+
+ /* Arrows showing edges of screen */
+ cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
+ cairo_move_to(cr, 0.0, height/2);
+ arrow_left(cr, 100.0);
+ cairo_fill(cr);
+ cairo_move_to(cr, width, height/2);
+ arrow_right(cr, 100.0);
+ cairo_fill(cr);
+ cairo_move_to(cr, width/2, height);
+ arrow_down(cr, 100.0);
+ cairo_fill(cr);
+ cairo_move_to(cr, width/2, 0.0);
+ arrow_up(cr, 100.0);
+ cairo_fill(cr);
+
+ /* Arrows showing edges of slide */
+ cairo_translate(cr, xoff, yoff);
+ cairo_set_source_rgb(cr, 0.5, 0.0, 0.0);
+ cairo_move_to(cr, 0.0, 100+h/2);
+ arrow_left(cr, 80.0);
+ cairo_fill(cr);
+ cairo_move_to(cr, tc->slide_width, 100+h/2);
+ arrow_right(cr, 80.0);
+ cairo_fill(cr);
+ cairo_move_to(cr, 100+tc->slide_width/2, h);
+ arrow_down(cr, 80.0);
+ cairo_fill(cr);
+ cairo_move_to(cr, 100+tc->slide_width/2, 0.0);
+ arrow_up(cr, 80.0);
+ cairo_fill(cr);
+
+ /* Stuff in the middle */
+ yp = (tc->slide_height-400)/2.0;
+ cairo_save(cr);
+ cairo_translate(cr, 0.0, yp);
+
+ snprintf(tmp, 1024, _("Colloquium %s test card\n"
+ "Screen resolution %.0f × %.0f\n"
+ "Slide resolution %i × %i"),
+ PACKAGE_VERSION, width, height,
+ tc->slide_width, h);
+
+ pl = pango_cairo_create_layout(cr);
+ desc = pango_font_description_from_string("Sans 24");
+ pango_layout_set_font_description(pl, desc);
+ pango_layout_set_text(pl, tmp, -1);
+
+ pango_layout_get_size(pl, &plw, &plh);
+ plw = pango_units_to_double(plw);
+ plh = pango_units_to_double(plh);
+ cairo_move_to(cr, (tc->slide_width-plw)/2, 0.0);
+ cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
+ pango_cairo_show_layout(cr, pl);
+
+ /* Colour boxes */
+ xp = (tc->slide_width-450)/2.0;
+ colour_box(cr, xp+0, 200, 1.0, 0.0, 0.0, _("Red"));
+ colour_box(cr, xp+80, 200, 0.0, 1.0, 0.0, _("Green"));
+ colour_box(cr, xp+160, 200, 0.0, 0.0, 1.0, _("Blue"));
+ colour_box(cr, xp+240, 200, 1.0, 1.0, 0.0, _("Yellow"));
+ colour_box(cr, xp+320, 200, 1.0, 0.0, 1.0, _("Pink"));
+ colour_box(cr, xp+400, 200, 0.0, 1.0, 1.0, _("Cyan"));
+
+ /* Shades of grey */
+ double i;
+ for ( i=0; i<=1.0; i+=0.2 ) {
+ char label[32];
+ snprintf(label, 31, "%.0f%%", i*100.0);
+ colour_box(cr, xp+(i*5*80), 300, i, i, i, label);
+ }
+ cairo_restore(cr);
+
+ return FALSE;
+}
+
+
+static void size_sig(GtkWidget *widget, GdkRectangle *rect, struct testcard *ss)
+{
+ int w;
+
+ w = rect->height * ss->p->slide_width/ss->p->slide_height;
+ if ( w > rect->width ) w = rect->width;
+ ss->slide_width = w;
+ ss->slide_height = rect->height;
+}
+
+
+
+static gboolean tc_key_press_sig(GtkWidget *da, GdkEventKey *event,
+ struct testcard *tc)
+{
+ if ( !event->is_modifier ) gtk_widget_destroy(tc->window);
+ return FALSE;
+}
+
+
+void show_testcard(struct presentation *p)
+{
+ GdkDisplay *display;
+ int n_monitors;
+ struct testcard *tc;
+
+ tc = calloc(1, sizeof(struct testcard));
+ if ( tc == NULL ) return;
+
+ tc->p = p;
+
+ tc->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+
+ tc->drawingarea = gtk_drawing_area_new();
+ gtk_container_add(GTK_CONTAINER(tc->window), tc->drawingarea);
+
+ gtk_widget_set_can_focus(GTK_WIDGET(tc->drawingarea), TRUE);
+ gtk_widget_add_events(GTK_WIDGET(tc->drawingarea), GDK_KEY_PRESS_MASK);
+
+ g_signal_connect(G_OBJECT(tc->drawingarea), "key-press-event",
+ G_CALLBACK(tc_key_press_sig), tc);
+ g_signal_connect(G_OBJECT(tc->window), "destroy",
+ G_CALLBACK(tc_destroy_sig), tc);
+ g_signal_connect(G_OBJECT(tc->window), "size-allocate",
+ G_CALLBACK(size_sig), tc);
+ g_signal_connect(G_OBJECT(tc->drawingarea), "draw",
+ G_CALLBACK(tc_draw_sig), tc);
+
+ gtk_widget_grab_focus(GTK_WIDGET(tc->drawingarea));
+
+ display = gdk_display_get_default();
+ n_monitors = gdk_display_get_n_monitors(display);
+
+ GdkMonitor *mon_ss;
+ if ( n_monitors == 1 ) {
+ mon_ss = gdk_display_get_primary_monitor(display);
+ printf(_("Single monitor mode\n"));
+ } else {
+ mon_ss = gdk_display_get_monitor(display, 1);
+ printf(_("Dual monitor mode\n"));
+ }
+
+ /* Workaround because gtk_window_fullscreen_on_monitor doesn't work */
+ GdkRectangle rect;
+ gdk_monitor_get_geometry(mon_ss, &rect);
+ gtk_window_move(GTK_WINDOW(tc->window), rect.x, rect.y);
+ gtk_window_fullscreen(GTK_WINDOW(tc->window));
+
+ gtk_widget_show_all(GTK_WIDGET(tc->window));
+
+}
+
diff --git a/src-old/testcard.h b/src-old/testcard.h
new file mode 100644
index 0000000..bb1ce2b
--- /dev/null
+++ b/src-old/testcard.h
@@ -0,0 +1,32 @@
+/*
+ * testcard.h
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef TESTCARD_H
+#define TESTCARD_H
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+extern void show_testcard(struct presentation *p);
+
+#endif /* TESTCARD_H */
diff --git a/src-old/utils.c b/src-old/utils.c
new file mode 100644
index 0000000..b41f344
--- /dev/null
+++ b/src-old/utils.c
@@ -0,0 +1,137 @@
+/*
+ * utils.c
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "utils.h"
+
+void chomp(char *s)
+{
+ size_t i;
+
+ if ( !s ) return;
+
+ for ( i=0; i<strlen(s); i++ ) {
+ if ( (s[i] == '\n') || (s[i] == '\r') ) {
+ s[i] = '\0';
+ return;
+ }
+ }
+}
+
+
+int safe_strcmp(const char *a, const char *b)
+{
+ if ( a == NULL ) return 1;
+ if ( b == NULL ) return 1;
+ return strcmp(a, b);
+}
+
+
+int parse_double(const char *a, float v[2])
+{
+ int nn;
+
+ nn = sscanf(a, "%fx%f", &v[0], &v[1]);
+ if ( nn != 2 ) {
+ fprintf(stderr, _("Invalid size '%s'\n"), a);
+ return 1;
+ }
+
+ return 0;
+}
+
+
+int parse_tuple(const char *a, float v[4])
+{
+ int nn;
+
+ nn = sscanf(a, "%f,%f,%f,%f", &v[0], &v[1], &v[2], &v[3]);
+ if ( nn != 4 ) {
+ fprintf(stderr, _("Invalid tuple '%s'\n"), a);
+ return 1;
+ }
+
+ return 0;
+}
+
+
+static LengthUnits get_units(const char *t)
+{
+ size_t len = strlen(t);
+
+ if ( t[len-1] == 'f' ) return UNITS_FRAC;
+ if ( t[len-1] == 'u' ) return UNITS_SLIDE;
+
+ fprintf(stderr, _("Invalid units in '%s'\n"), t);
+ return UNITS_SLIDE;
+}
+
+
+int parse_dims(const char *opt, double *wp, double *hp,
+ LengthUnits *wup, LengthUnits *hup,
+ double *xp, double *yp)
+{
+ char *w;
+ char *h;
+ char *x;
+ char *y;
+ char *check;
+
+ w = strdup(opt);
+ h = index(w, 'x');
+ h[0] = '\0'; h++;
+
+ x = index(h, '+');
+ if ( x == NULL ) goto invalid;
+ x[0] = '\0'; x++;
+
+ y = index(x, '+');
+ if ( x == NULL ) goto invalid;
+ y[0] = '\0'; y++;
+
+ *wp = strtod(w, &check);
+ if ( check == w ) goto invalid;
+ *wup = get_units(w);
+
+ *hp = strtod(h, &check);
+ if ( check == h ) goto invalid;
+ *hup = get_units(h);
+
+ *xp= strtod(x, &check);
+ if ( check == x ) goto invalid;
+ *yp = strtod(y, &check);
+ if ( check == y ) goto invalid;
+
+ return 0;
+
+invalid:
+ fprintf(stderr, _("Invalid dimensions '%s'\n"), opt);
+ return 1;
+}
diff --git a/src-old/utils.h b/src-old/utils.h
new file mode 100644
index 0000000..af3c7b8
--- /dev/null
+++ b/src-old/utils.h
@@ -0,0 +1,47 @@
+/*
+ * utils.h
+ *
+ * Copyright © 2013-2018 Thomas White <taw@bitwiz.org.uk>
+ *
+ * This file is part of Colloquium.
+ *
+ * Colloquium is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef UTILS_H
+#define UTILS_H
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+typedef enum
+{
+ UNITS_SLIDE,
+ UNITS_FRAC
+} LengthUnits;
+
+extern void chomp(char *s);
+extern int safe_strcmp(const char *a, const char *b);
+extern int parse_double(const char *a, float v[2]);
+extern int parse_tuple(const char *a, float v[4]);
+extern int parse_dims(const char *opt, double *wp, double *hp,
+ LengthUnits *wup, LengthUnits *hup,
+ double *xp, double *yp);
+
+#include <libintl.h>
+#define _(x) gettext(x)
+
+#endif /* UTILS_H */