diff options
-rw-r--r-- | libstorycode/cairo/render.c | 52 | ||||
-rw-r--r-- | libstorycode/cairo/render.h | 62 | ||||
-rw-r--r-- | libstorycode/gtk/gtknarrativeview.c | 2197 | ||||
-rw-r--r-- | libstorycode/gtk/gtknarrativeview.h | 199 | ||||
-rw-r--r-- | libstorycode/gtk/gtkslideview.c | 2197 | ||||
-rw-r--r-- | libstorycode/gtk/gtkslideview.h | 199 | ||||
-rw-r--r-- | meson.build | 56 | ||||
-rw-r--r-- | src/pdfstorycode.c | 59 |
8 files changed, 5009 insertions, 12 deletions
diff --git a/libstorycode/cairo/render.c b/libstorycode/cairo/render.c new file mode 100644 index 0000000..32b9596 --- /dev/null +++ b/libstorycode/cairo/render.c @@ -0,0 +1,52 @@ +/* + * 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 "presentation.h" +#include "slide.h" +#include "narrative.h" +#include "stylesheet.h" + + +int slide_render(Slide *s, cairo_t *cr, double log_w, double log_h, + Stylesheet *stylesheet, int slide_number, PangoLanguage *lang, + PangoContext *pc) +{ + cairo_scale(cr, 1.0/log_w, 1.0/log_h); + + 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); + + return 0; +} diff --git a/libstorycode/cairo/render.h b/libstorycode/cairo/render.h new file mode 100644 index 0000000..0cfae26 --- /dev/null +++ b/libstorycode/cairo/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/libstorycode/gtk/gtknarrativeview.c b/libstorycode/gtk/gtknarrativeview.c new file mode 100644 index 0000000..8e79fd8 --- /dev/null +++ b/libstorycode/gtk/gtknarrativeview.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/libstorycode/gtk/gtknarrativeview.h b/libstorycode/gtk/gtknarrativeview.h new file mode 100644 index 0000000..d3c111b --- /dev/null +++ b/libstorycode/gtk/gtknarrativeview.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/libstorycode/gtk/gtkslideview.c b/libstorycode/gtk/gtkslideview.c new file mode 100644 index 0000000..8e79fd8 --- /dev/null +++ b/libstorycode/gtk/gtkslideview.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/libstorycode/gtk/gtkslideview.h b/libstorycode/gtk/gtkslideview.h new file mode 100644 index 0000000..d3c111b --- /dev/null +++ b/libstorycode/gtk/gtkslideview.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/meson.build b/meson.build index ee790d0..38564a0 100644 --- a/meson.build +++ b/meson.build @@ -4,8 +4,6 @@ project('colloquium', 'c', license : 'GPL3+', default_options : ['buildtype=debugoptimized']) -gnome = import('gnome') - datadir=join_paths(get_option('datadir'), 'colloquium') add_project_arguments('-DPACKAGE_VERSION="'+meson.project_version()+'"', language : 'c') @@ -14,16 +12,23 @@ add_project_arguments('-DDATADIR="'+join_paths(get_option('prefix'), datadir)+'" add_project_arguments('-DLOCALEDIR="'+join_paths(get_option('prefix'), get_option('localedir'))+'"', language : 'c') + # Localisation subdir('po') + # Dependencies -gtkdep = dependency('gtk+-3.0') -glib_dep = dependency('glib-2.0') -gio_dep = dependency('gio-2.0') +gnome = import('gnome') +gtk_dep = dependency('gtk+-3.0', required : true) +glib_dep = dependency('glib-2.0', required : true) +gio_dep = dependency('gio-2.0', required : true) +cairo_dep = dependency('cairo', required : true) +pango_dep = dependency('pango', required : true) +gdkpixbuf_dep = dependency('gdk-pixbuf-2.0', required : true) cc = meson.get_compiler('c') -mdep = cc.find_library('m', required : false) + +# Compiled-in resources gresources = gnome.compile_resources('colloquium-resources', 'data/colloquium.gresource.xml', source_dir: 'data', c_name: 'colloquium') @@ -67,11 +72,45 @@ libstorycode_dep = declare_dependency(include_directories : libstorycode_include link_with : libstorycode) +# libstorycode-cairo +libstorycode_cairo_includes = include_directories('libstorycode/cairo') + +libstorycode_cairo = library('storycode-cairo', + ['libstorycode/cairo/render.c', + ], + include_directories : libstorycode_cairo_includes, + dependencies : [cairo_dep, pango_dep, gdkpixbuf_dep, + libstorycode_dep], + install : true) + +libstorycode_cairo_dep = declare_dependency(include_directories : libstorycode_cairo_includes, + link_with : libstorycode_cairo) + + +# libstorycode-gtk +#libstorycode_gtk_includes = include_directories('libstorycode/gtk') +# +#libstorycode_gtk = library('storycode-gtk', +# ['libstorycode/gtk/gtknarrativeview.c', +# 'libstorycode/gtk/gtkslideview.c', +# ], +# include_directories : libstorycode_gtk_includes, +# dependencies : [gtk_dep, libstorycode_dep, +# libstorycode_cairo_dep], +# install : true) +# +#libstorycode_gtk_dep = declare_dependency(include_directories : libstorycode_gtk_includes, +# link_with : libstorycode_gtk) + + +# pdfstorycode executable('pdfstorycode', ['src/pdfstorycode.c', ], gresources, - dependencies : [glib_dep, gio_dep, libstorycode_dep]) + dependencies : [glib_dep, gio_dep, cairo_dep, + libstorycode_dep, libstorycode_cairo_dep]) + # Main program #executable('colloquium', @@ -98,13 +137,16 @@ executable('pdfstorycode', # dependencies : [gtkdep, mdep, jsondep], # install : true) + # Desktop file install_data(['data/colloquium.desktop'], install_dir : get_option('datadir')+'/applications') + # Icon install_data(['data/colloquium.svg'], install_dir : get_option('datadir')+'/icons/hicolor/scalable/apps') + # Tests subdir('tests') diff --git a/src/pdfstorycode.c b/src/pdfstorycode.c index 4c379f4..6a4de05 100644 --- a/src/pdfstorycode.c +++ b/src/pdfstorycode.c @@ -26,10 +26,63 @@ #include <glib.h> #include <glib/gstdio.h> #include <gio/gio.h> +#include <cairo.h> #include "storycode.h" #include "presentation.h" + +static int render_slides_to_pdf(Presentation *p, const char *filename) +{ + double w = 2048.0; + double scale; + cairo_surface_t *surf; + cairo_t *cr; + int i; + PangoContext *pc; + + surf = cairo_pdf_surface_create(filename, w, w); + if ( cairo_surface_status(surf) != CAIRO_STATUS_SUCCESS ) { + fprintf(stderr, _("Couldn't create Cairo surface\n")); + return 1; + } + + cr = cairo_create(surf); + pc = pango_cairo_create_context(cr); + + for ( i=0; i<presentation_num_slides(p); i++ ) + { + Slide *s; + + s = presentation_slide(p, i); + + cairo_pdf_surface_set_size(surf, w, h); + + 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); + + slide_render(s, cr, p->slide_width, + p->slide_height, p->stylesheet, NULL, + p->is, i, p->lang, pc); + + cairo_restore(cr); + + cairo_show_page(cr); + } + + g_object_unref(pc); + cairo_surface_finish(surf); + cairo_destroy(cr); + + return 0; +} + + int main(int argc, char *argv[]) { GFile *file; @@ -46,11 +99,7 @@ int main(int argc, char *argv[]) g_bytes_unref(bytes); /* Render each slide to PDF */ - for ( i=0; i<presentation_num_slides(p); i++ ) { - Slide *slide = presentation_slide(p, i); - printf("slide %i:\n", i); - describe_slide(slide); - } + render_slides_to_pdf(p, "slides.pdf"); return 0; } |