/* * tool_text.c * * Colloquium - A tiny presentation program * * Copyright (c) 2011 Thomas White * * This program 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 . * */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include "presentation.h" #include "objects.h" #include "mainwindow.h" #include "slide_render.h" #include "loadsave.h" enum text_drag_reason { TEXT_DRAG_REASON_NONE, TEXT_DRAG_REASON_RESIZE, }; struct text_toolinfo { struct toolinfo base; PangoContext *pc; enum text_drag_reason drag_reason; enum corner drag_corner; double box_x; double box_y; double box_width; double box_height; double drag_initial_x; double drag_initial_y; }; struct text_object { struct object base; char *text; size_t text_len; int insertion_point; int insertion_trail; PangoLayout *layout; PangoFontDescription *fontdesc; double offs_x; double offs_y; int furniture; PangoContext **pc; }; static void calculate_size_from_style(struct text_object *o, double *peright, double *pebottom, double *pmw, double *pmh) { double max_width, max_height; double ebottom, eright, mw, mh; eright = o->base.parent->parent->slide_width - o->base.style->margin_right; ebottom = o->base.parent->parent->slide_height - o->base.style->margin_bottom; mw = o->base.parent->parent->slide_width; mh = o->base.parent->parent->slide_height; *peright = eright; *pebottom = ebottom; *pmw = mw; *pmh = mh; max_width = mw - o->base.style->margin_left - o->base.style->margin_right; /* Use the provided maximum width if it exists and is smaller */ if ( o->base.style->use_max_width && (o->base.style->max_width < max_width) ) { max_width = o->base.style->max_width; } max_height = mh - o->base.style->margin_top - o->base.style->margin_bottom; pango_layout_set_width(o->layout, max_width*PANGO_SCALE); pango_layout_set_height(o->layout, max_height*PANGO_SCALE); pango_layout_set_wrap(o->layout, PANGO_WRAP_WORD_CHAR); pango_layout_set_ellipsize(o->layout, PANGO_ELLIPSIZE_MIDDLE); switch ( o->base.style->halign ) { case J_LEFT : pango_layout_set_alignment(o->layout, PANGO_ALIGN_LEFT); break; case J_RIGHT : pango_layout_set_alignment(o->layout, PANGO_ALIGN_RIGHT); break; case J_CENTER : pango_layout_set_alignment(o->layout, PANGO_ALIGN_CENTER); break; } } static void calculate_position_from_style(struct text_object *o, double eright, double ebottom, double mw, double mh) { double xo, yo; xo = o->base.bb_width; yo = o->base.bb_height; switch ( o->base.style->halign ) { case J_LEFT : o->base.x = o->base.style->margin_left; break; case J_RIGHT : o->base.x = eright - xo; break; case J_CENTER : o->base.x = mw/2.0 - xo/2.0 + o->base.style->offset_x; break; } /* Correct if centering crashes into opposite margin */ if ( o->base.style->halign == J_CENTER ) { if ( o->base.x < o->base.style->margin_left ) { o->base.x = o->base.style->margin_left; } if ( o->base.x + xo > eright ) { o->base.x = eright - xo; } } switch ( o->base.style->valign ) { case V_TOP : o->base.y = o->base.style->margin_top; break; case V_BOTTOM : o->base.y = ebottom - yo; break; case V_CENTER : o->base.y = mh/2.0 - yo/2.0 + o->base.style->offset_y; break; } if ( o->base.style->valign == V_CENTER ) { if ( o->base.y < o->base.style->margin_top ) { o->base.y = o->base.style->margin_top; } if ( o->base.y+yo + yo > ebottom ) { o->base.y = ebottom - yo; } } } static void update_text(struct text_object *o, cairo_t *cr) { PangoRectangle logical; double eright = 0.0; double ebottom = 0.0; double mw = 0.0; double mh = 0.0; struct object *ex; struct text_object *ext; switch ( o->base.style->role ) { case S_ROLE_SLIDENUMBER: if ( o->text_len < 32 ) { free(o->text); o->text = malloc(32); o->text_len = 32; } snprintf(o->text, o->text_len-1, "Slide %i", 1+slide_number(o->base.parent->parent, o->base.parent)); break; case S_ROLE_PTITLE: ex = o->base.parent->roles[S_ROLE_PTITLE_REF]; ext = (struct text_object *)ex; if ( (ext != NULL) && (ext->text != NULL) ) { free(o->text); o->text = strdup(ext->text); o->text_len = strlen(o->text); } if ( ext == NULL ) { free(o->text); o->text = strdup(""); o->text_len = strlen(o->text); } break; case S_ROLE_PDATE: ex = o->base.parent->roles[S_ROLE_PDATE_REF]; ext = (struct text_object *)ex; if ( (ext != NULL) && (ext->text != NULL) ) { free(o->text); o->text = strdup(ext->text); o->text_len = strlen(o->text); } if ( ext == NULL ) { free(o->text); o->text = strdup(""); o->text_len = strlen(o->text); } break; default: break; } if ( o->layout == NULL ) { if ( *o->pc != NULL ) { o->layout = pango_layout_new(*o->pc); } else { /* Can't render yet */ return; } } if ( cr != NULL ) pango_cairo_update_layout(cr, o->layout); o->furniture = o->base.style != o->base.parent->parent->ss->styles[0]; pango_layout_set_text(o->layout, o->text, -1); o->fontdesc = pango_font_description_from_string(o->base.style->font); pango_layout_set_font_description(o->layout, o->fontdesc); if ( o->furniture ) { calculate_size_from_style(o, &eright, &ebottom, &mw, &mh); pango_layout_get_extents(o->layout, NULL, &logical); o->base.bb_width = logical.width / PANGO_SCALE; o->base.bb_height = logical.height / PANGO_SCALE; o->offs_x = logical.x / PANGO_SCALE; o->offs_y = logical.y / PANGO_SCALE; } else { pango_layout_set_width(o->layout, o->base.bb_width*PANGO_SCALE); pango_layout_set_height(o->layout, o->base.bb_height*PANGO_SCALE); pango_layout_set_wrap(o->layout, PANGO_WRAP_WORD_CHAR); pango_layout_set_ellipsize(o->layout, PANGO_ELLIPSIZE_MIDDLE); } if ( o->furniture ) { calculate_position_from_style(o, eright, ebottom, mw, mh); } } void insert_text(struct object *op, char *t) { struct text_object *o = (struct text_object *)op; char *tmp; size_t tlen, olen, offs; int i; assert(o->base.type == OBJ_TEXT); tlen = strlen(t); olen = strlen(o->text); if ( tlen + olen + 1 > o->text_len ) { char *try; try = realloc(o->text, o->text_len + tlen + 64); if ( try == NULL ) return; /* Failed to insert */ o->text = try; o->text_len += 64; o->text_len += tlen; } tmp = malloc(o->text_len); if ( tmp == NULL ) return; offs = o->insertion_point + o->insertion_trail; for ( i=0; itext[i]; } for ( i=0; iinsertion_point; i++ ) { tmp[i+offs+tlen] = o->text[i+offs]; } tmp[olen+tlen] = '\0'; memcpy(o->text, tmp, o->text_len); free(tmp); redraw_slide(op->parent); o->insertion_point += tlen; o->base.empty = 0; } void move_cursor(struct object *op, int dir) { struct text_object *o = (struct text_object *)op; int new_idx, new_trail; pango_layout_move_cursor_visually(o->layout, TRUE, o->insertion_point, o->insertion_trail, dir, &new_idx, &new_trail); if ( (new_idx >= 0) && (new_idx < G_MAXINT) ) { o->insertion_point = new_idx; o->insertion_trail = new_trail; } } void move_cursor_left(struct object *op) { move_cursor(op, -1); redraw_overlay(op->parent->parent); } void move_cursor_right(struct object *op) { move_cursor(op, +1); redraw_overlay(op->parent->parent); } void handle_text_backspace(struct object *op) { int old_idx, new_idx; struct text_object *o = (struct text_object *)op; assert(o->base.type == OBJ_TEXT); if ( o->insertion_point == 0 ) return; /* Nothing to delete */ old_idx = o->insertion_point + o->insertion_trail; move_cursor_left(op); new_idx = o->insertion_point + o->insertion_trail; memmove(o->text+new_idx, o->text+old_idx, o->text_len-new_idx); if ( strlen(o->text) == 0 ) o->base.empty = 1; redraw_slide(op->parent); } static void render_text_object(cairo_t *cr, struct object *op) { struct text_object *o = (struct text_object *)op; GdkColor col; if ( o->layout == NULL ) { return; } update_text(o, cr); cairo_move_to(cr, o->base.x - o->offs_x, o->base.y - o->offs_y); gdk_color_parse(o->base.style->colour, &col); gdk_cairo_set_source_color(cr, &col); /* FIXME: Honour alpha as well */ pango_cairo_show_layout(cr, o->layout); } static void draw_caret(cairo_t *cr, struct text_object *o) { double xposd, yposd, cx; double clow, chigh; PangoRectangle pos; const double t = 1.8; assert(o->base.type == OBJ_TEXT); pango_layout_get_cursor_pos(o->layout, o->insertion_point+o->insertion_trail, &pos, NULL); xposd = pos.x/PANGO_SCALE; cx = o->base.x - o->offs_x + xposd; yposd = pos.y/PANGO_SCALE; clow = o->base.y - o->offs_y + yposd; chigh = clow + (pos.height/PANGO_SCALE); 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 update_text_object(struct object *op) { struct text_object *o = (struct text_object *)op; update_text(o, NULL); } static void delete_text_object(struct object *op) { struct text_object *o = (struct text_object *)op; if ( o->layout != NULL ) g_object_unref(o->layout); if ( o->fontdesc != NULL ) pango_font_description_free(o->fontdesc); } static void serialize(struct object *op, struct serializer *ser) { struct text_object *o = (struct text_object *)op; serialize_s(ser, "style", op->style->name); if ( op->style == op->parent->parent->ss->styles[0] ) { serialize_f(ser, "x", op->x); serialize_f(ser, "y", op->y); serialize_f(ser, "w", op->bb_width); serialize_f(ser, "h", op->bb_height); } serialize_s(ser, "text", o->text); } static struct object *new_text_object(double x, double y, struct style *sty, struct text_toolinfo *ti) { struct text_object *new; new = calloc(1, sizeof(*new)); if ( new == NULL ) return NULL; /* Base properties */ new->base.x = x; new->base.y = y; new->base.bb_width = 10.0; new->base.bb_height = 40.0; new->base.type = OBJ_TEXT; new->base.empty = 1; new->base.parent = NULL; new->base.style = sty; /* Text-specific stuff */ new->text = malloc(1); new->text[0] = '\0'; new->text_len = 1; new->insertion_point = 0; new->insertion_trail = 0; if ( ti->pc != NULL ) { new->layout = pango_layout_new(ti->pc); new->pc = NULL; } else { new->layout = NULL; new->pc = &ti->pc; } new->fontdesc = NULL; /* Methods for this object */ new->base.render_object = render_text_object; new->base.delete_object = delete_text_object; new->base.update_object = update_text_object; new->base.serialize = serialize; return (struct object *)new; } static struct object *add_text_object(struct slide *s, double x, double y, struct style *sty, struct text_toolinfo *ti) { struct object *o; o = new_text_object(x, y, sty, ti); if ( add_object_to_slide(s, o) ) { delete_object(o); return NULL; } redraw_slide(o->parent); return o; } static void calculate_box_size(struct object *o, double x, double y, struct text_toolinfo *ti) { double ddx, ddy; ddx = x - ti->drag_initial_x; ddy = y - ti->drag_initial_y; switch ( ti->drag_corner ) { case CORNER_BR : ti->box_x = o->x; ti->box_y = o->y; ti->box_width = o->bb_width + ddx; ti->box_height = o->bb_height + ddy; break; case CORNER_BL : ti->box_x = o->x + ddx; ti->box_y = o->y; ti->box_width = o->bb_width - ddx; ti->box_height = o->bb_height + ddy; break; case CORNER_TL : ti->box_x = o->x + ddx; ti->box_y = o->y + ddy; ti->box_width = o->bb_width - ddx; ti->box_height = o->bb_height - ddy; break; case CORNER_TR : ti->box_x = o->x; ti->box_y = o->y + ddy; ti->box_width = o->bb_width + ddx; ti->box_height = o->bb_height - ddy; break; case CORNER_NONE : break; } if ( ti->box_width < 20.0 ) ti->box_width = 20.0; if ( ti->box_height < 20.0 ) ti->box_height = 20.0; } static void click_select(struct presentation *p, struct toolinfo *tip, double x, double y, GdkEventButton *event, enum drag_status *drag_status, enum drag_reason *drag_reason) { int xp, yp; double xo, yo; gboolean v; enum corner c; struct text_toolinfo *ti = (struct text_toolinfo *)tip; struct text_object *o = (struct text_object *)p->editing_object; int idx, trail; assert(o->base.type == OBJ_TEXT); xo = x - o->base.x; yo = y - o->base.y; /* Within the resizing region? */ c = which_corner(x, y, &o->base); if ( (c != CORNER_NONE) && !o->furniture ) { ti->drag_reason = TEXT_DRAG_REASON_RESIZE; ti->drag_corner = c; ti->drag_initial_x = x; ti->drag_initial_y = y; calculate_box_size((struct object *)o, x, y, ti); /* Tell the MCP what we did, and return */ *drag_status = DRAG_STATUS_DRAGGING; *drag_reason = DRAG_REASON_TOOL; return; } xp = (xo + o->offs_x)*PANGO_SCALE; yp = (yo + o->offs_y)*PANGO_SCALE; v = pango_layout_xy_to_index(o->layout, xp, yp, &idx, &trail); o->insertion_point = idx; o->insertion_trail = trail; } static void drag(struct toolinfo *tip, struct presentation *p, struct object *o, double x, double y) { struct text_toolinfo *ti = (struct text_toolinfo *)tip; if ( ti->drag_reason == TEXT_DRAG_REASON_RESIZE ) { calculate_box_size(o, x, y, ti); redraw_overlay(p); } } static void end_drag(struct toolinfo *tip, struct presentation *p, struct object *o, double x, double y) { struct text_toolinfo *ti = (struct text_toolinfo *)tip; calculate_box_size((struct object *)o, x, y, ti); o->x = ti->box_x; o->y = ti->box_y; o->bb_width = ti->box_width; o->bb_height = ti->box_height; update_text((struct text_object *)o, NULL); redraw_slide(o->parent); ti->drag_reason = TEXT_DRAG_REASON_NONE; } static void update_subsequent_refs(struct presentation *p, int start, enum object_role role, struct object *o) { int i; for ( i=start; inum_slides; i++ ) { struct slide *s = p->slides[i]; s->roles[role] = o; } } static void create_default(struct presentation *p, struct style *sty, struct slide *s, struct toolinfo *tip) { struct object *n; struct text_object *o; struct text_toolinfo *ti = (struct text_toolinfo *)tip; n = add_text_object(s, 0.0, 0.0, sty, ti); o = (struct text_object *)n; o->furniture = 1; n->empty = 0; if ( sty->role != S_ROLE_NONE ) { s->roles[sty->role] = n; } switch ( sty->role ) { case S_ROLE_SLIDENUMBER: case S_ROLE_PTITLE: case S_ROLE_PAUTHOR: case S_ROLE_PDATE: break; default: p->editing_object = n; break; } /* If we just created a new presentation title (etc), set this object as * the presentation title reference for all subsequent slides. * Any relevant objects will get updated when the current object gets * deselected. */ if ( sty->role == S_ROLE_PTITLE_REF ) { update_subsequent_refs(p, slide_number(p, s)+1, S_ROLE_PTITLE_REF, n); } if ( sty->role == S_ROLE_PAUTHOR_REF ) { update_subsequent_refs(p, slide_number(p, s)+1, S_ROLE_PAUTHOR_REF, n); } if ( sty->role == S_ROLE_PDATE_REF ) { update_subsequent_refs(p, slide_number(p, s)+1, S_ROLE_PDATE_REF, n); } redraw_slide(((struct object *)o)->parent); } static void create_region(struct toolinfo *tip, struct presentation *p, double x1, double y1, double x2, double y2) { struct object *n; struct text_toolinfo *ti = (struct text_toolinfo *)tip; struct text_object *o; n = add_text_object(p->cur_edit_slide, 0.0, 0.0, p->ss->styles[0], ti); n->x = x1y = y1bb_width = fabs(x1-x2); n->bb_height = fabs(y1-y2); o = (struct text_object *)n; o->furniture = 0; redraw_slide(((struct object *)o)->parent); p->editing_object = n; } static void select_object(struct object *o, struct toolinfo *tip) { /* Do nothing */ } static void update_subsequent(struct presentation *p, int start, enum object_role role, struct object *o) { int i; for ( i=start; inum_slides; i++ ) { struct slide *s = p->slides[i]; if ( s->roles[role] != NULL ) { s->roles[role]->update_object(s->roles[role]); } } } static int deselect_object(struct object *o, struct toolinfo *tip) { struct slide *s; struct presentation *p; if ( o == NULL ) return 0; s = o->parent; p = s->parent; if ( o->empty ) { delete_object(o); return 1; } if ( o->style->role == S_ROLE_PTITLE_REF ) { update_subsequent(p, slide_number(p, s)+1, S_ROLE_PTITLE, o); } if ( o->style->role == S_ROLE_PAUTHOR_REF ) { update_subsequent(p, slide_number(p, s)+1, S_ROLE_PAUTHOR, o); } if ( o->style->role == S_ROLE_PDATE_REF ) { update_subsequent(p, slide_number(p, s)+1, S_ROLE_PDATE, o); } return 0; } static void draw_overlay(struct toolinfo *tip, cairo_t *cr, struct object *n) { struct text_toolinfo *ti = (struct text_toolinfo *)tip; struct text_object *o = (struct text_object *)n; if ( n != NULL ) { draw_editing_box(cr, n->x, n->y, n->bb_width, n->bb_height); if ( !o->furniture ) { /* Draw resize handles */ draw_resize_handle(cr, n->x, n->y+n->bb_height-20.0); draw_resize_handle(cr, n->x+n->bb_width-20.0, n->y); draw_resize_handle(cr, n->x, n->y); draw_resize_handle(cr, n->x+n->bb_width-20.0, n->y+n->bb_height-20.0); } draw_caret(cr, o); } if ( ti->drag_reason == TEXT_DRAG_REASON_RESIZE ) { draw_rubberband_box(cr, ti->box_x, ti->box_y, ti->box_width, ti->box_height); } } static void key_pressed(struct object *o, guint keyval, struct toolinfo *tip) { if ( o == NULL ) return; switch ( keyval ) { case GDK_KEY_BackSpace : handle_text_backspace(o); break; case GDK_KEY_Left : move_cursor_left(o); break; case GDK_KEY_Right : move_cursor_right(o); break; } } static void im_commit(struct object *o, gchar *str, struct toolinfo *tip) { insert_text(o, str); } static int valid_object(struct object *o) { if ( o->type == OBJ_TEXT ) return 1; return 0; } static void realise(struct toolinfo *ti, GtkWidget *w, struct presentation *p) { struct text_toolinfo *tip = (struct text_toolinfo *)ti; tip->pc = gtk_widget_get_pango_context(w); ti->tbox = gtk_label_new("Text tool"); g_object_ref(ti->tbox); gtk_widget_show(ti->tbox); } static struct object *deserialize(struct presentation *p, struct ds_node *root, struct toolinfo *tip) { struct object *o; struct text_object *to; char *style; char *text; struct style *sty; double x, y, w, h; struct text_toolinfo *ti = (struct text_toolinfo *)tip; if ( get_field_s(root, "style", &style) ) { fprintf(stderr, "Couldn't find style for object '%s'\n", root->key); return NULL; } sty = find_style(p->ss, style); if ( sty == NULL ) { fprintf(stderr, "Style '%s' not found in style sheet.\n", style); free(style); return NULL; } free(style); if ( sty == p->ss->styles[0] ) { if ( get_field_f(root, "x", &x) ) { fprintf(stderr, "Couldn't find x position for object '%s'\n", root->key); return NULL; } if ( get_field_f(root, "y", &y) ) { fprintf(stderr, "Couldn't find y position for object '%s'\n", root->key); return NULL; } if ( get_field_f(root, "w", &w) ) { fprintf(stderr, "Couldn't find width for object '%s'\n", root->key); return NULL; } if ( get_field_f(root, "h", &h) ) { fprintf(stderr, "Couldn't find height for object '%s'\n", root->key); return NULL; } } else { /* Furniture */ x = 0.0; y = 0.0; w = 0.0; h = 0.0; } o = new_text_object(x, y, sty, ti); o->bb_width = w; o->bb_height = h; /* Apply the correct text */ if ( get_field_s(root, "text", &text) ) { fprintf(stderr, "Couldn't find text for object '%s'\n", root->key); return NULL; } to = (struct text_object *)o; free(to->text); to->text = text; to->text_len = strlen(text); o->empty = 0; return o; } struct toolinfo *initialise_text_tool(GtkWidget *w) { struct text_toolinfo *ti; ti = malloc(sizeof(*ti)); ti->base.click_select = click_select; ti->base.create_default = create_default; ti->base.select = select_object; ti->base.deselect = deselect_object; ti->base.drag = drag; ti->base.end_drag = end_drag; ti->base.create_region = create_region; ti->base.draw_editing_overlay = draw_overlay; ti->base.key_pressed = key_pressed; ti->base.im_commit = im_commit; ti->base.valid_object = valid_object; ti->base.realise = realise; ti->base.deserialize = deserialize; ti->pc = NULL; ti->drag_reason = DRAG_REASON_NONE; return (struct toolinfo *)ti; }