sdiagram

Simple diagram tool — suckless-style node graph editor
git clone git clone https://git.krisyotam.com/krisyotam/sdiagram.git
Log | Files | Refs | LICENSE

canvas.c (15263B)


      1 /* sdiagram - simple diagram tool
      2  * See LICENSE file for copyright and license details. */
      3 
      4 #include <math.h>
      5 #include <stdio.h>
      6 #include <string.h>
      7 
      8 #include "sdiagram.h"
      9 #include "config.h"
     10 
     11 #define PBUF_SZ 4096
     12 
     13 /* append to pango buffer with bounds check */
     14 static int
     15 papp(char *o, char *end, const char *s)
     16 {
     17 	int n;
     18 
     19 	n = snprintf(o, end - o, "%s", s);
     20 	return (n > 0 && o + n < end) ? n : 0;
     21 }
     22 
     23 /* convert markdown to Pango markup.
     24  * supports: **bold**, *italic*, `code`,
     25  * # h1, ## h2, - lists, --- rules */
     26 static void
     27 md_to_pango(const char *md, char *out, size_t outsz)
     28 {
     29 	char *o, *end;
     30 	int in_b, in_i, in_c, sol;
     31 
     32 	o = out;
     33 	end = out + outsz - 1;
     34 	in_b = in_i = in_c = 0;
     35 	sol = 1;
     36 
     37 	while (*md && o < end) {
     38 		/* line-level at start of line */
     39 		if (sol) {
     40 			if (md[0] == '#' && md[1] == '#'
     41 			    && md[2] == ' ') {
     42 				o += papp(o, end, "<b>");
     43 				md += 3;
     44 				while (*md && *md != '\n' && o < end) {
     45 					if (*md == '&')
     46 						o += papp(o, end, "&amp;");
     47 					else if (*md == '<')
     48 						o += papp(o, end, "&lt;");
     49 					else
     50 						*o++ = *md;
     51 					md++;
     52 				}
     53 				o += papp(o, end, "</b>");
     54 				if (*md == '\n') { *o++ = '\n'; md++; }
     55 				sol = 1;
     56 				continue;
     57 			}
     58 			if (md[0] == '#' && md[1] == ' ') {
     59 				o += papp(o, end,
     60 					"<span size=\"large\">"
     61 					"<b>");
     62 				md += 2;
     63 				while (*md && *md != '\n' && o < end) {
     64 					if (*md == '&')
     65 						o += papp(o, end, "&amp;");
     66 					else if (*md == '<')
     67 						o += papp(o, end, "&lt;");
     68 					else
     69 						*o++ = *md;
     70 					md++;
     71 				}
     72 				o += papp(o, end,
     73 					"</b></span>");
     74 				if (*md == '\n') { *o++ = '\n'; md++; }
     75 				sol = 1;
     76 				continue;
     77 			}
     78 			if ((md[0] == '-' || md[0] == '*')
     79 			    && md[1] == ' '
     80 			    && !(md[0] == '*' && md[1] == '*')) {
     81 				o += papp(o, end, "  \xe2\x80\xa2 ");
     82 				md += 2;
     83 				sol = 0;
     84 				continue;
     85 			}
     86 			if (md[0] == '-' && md[1] == '-'
     87 			    && md[2] == '-') {
     88 				o += papp(o, end,
     89 					"\xe2\x94\x80\xe2\x94\x80"
     90 					"\xe2\x94\x80\xe2\x94\x80"
     91 					"\xe2\x94\x80\xe2\x94\x80"
     92 					"\xe2\x94\x80\xe2\x94\x80");
     93 				while (*md == '-') md++;
     94 				if (*md == '\n') { *o++ = '\n'; md++; }
     95 				sol = 1;
     96 				continue;
     97 			}
     98 			sol = 0;
     99 		}
    100 
    101 		/* inside code: no markdown processing */
    102 		if (in_c) {
    103 			if (*md == '`') {
    104 				o += papp(o, end, "</tt>");
    105 				in_c = 0;
    106 				md++;
    107 				continue;
    108 			}
    109 			if (*md == '&')
    110 				o += papp(o, end, "&amp;");
    111 			else if (*md == '<')
    112 				o += papp(o, end, "&lt;");
    113 			else
    114 				*o++ = *md;
    115 			if (*md == '\n') sol = 1;
    116 			md++;
    117 			continue;
    118 		}
    119 
    120 		/* backtick: start code */
    121 		if (*md == '`') {
    122 			o += papp(o, end, "<tt>");
    123 			in_c = 1;
    124 			md++;
    125 			continue;
    126 		}
    127 
    128 		/* bold ** */
    129 		if (md[0] == '*' && md[1] == '*') {
    130 			if (in_b)
    131 				o += papp(o, end, "</b>");
    132 			else
    133 				o += papp(o, end, "<b>");
    134 			in_b = !in_b;
    135 			md += 2;
    136 			continue;
    137 		}
    138 
    139 		/* italic * */
    140 		if (md[0] == '*' && md[1] != '*') {
    141 			if (in_i)
    142 				o += papp(o, end, "</i>");
    143 			else
    144 				o += papp(o, end, "<i>");
    145 			in_i = !in_i;
    146 			md++;
    147 			continue;
    148 		}
    149 
    150 		/* escape XML */
    151 		if (*md == '&')
    152 			o += papp(o, end, "&amp;");
    153 		else if (*md == '<')
    154 			o += papp(o, end, "&lt;");
    155 		else
    156 			*o++ = *md;
    157 
    158 		if (*md == '\n')
    159 			sol = 1;
    160 		md++;
    161 	}
    162 
    163 	/* close unclosed tags */
    164 	if (in_c) o += papp(o, end, "</tt>");
    165 	if (in_b) o += papp(o, end, "</b>");
    166 	if (in_i) o += papp(o, end, "</i>");
    167 	*o = '\0';
    168 }
    169 
    170 static void
    171 set_color(cairo_t *cr, const double *c)
    172 {
    173 	cairo_set_source_rgba(cr, c[0], c[1], c[2], c[3]);
    174 }
    175 
    176 static void
    177 rect_edge(double cx, double cy, double w, double h,
    178 	double tx, double ty, double *ex, double *ey)
    179 {
    180 	double dx, dy, sx, sy, s;
    181 
    182 	dx = tx - cx;
    183 	dy = ty - cy;
    184 	if (dx == 0 && dy == 0) {
    185 		*ex = cx;
    186 		*ey = cy;
    187 		return;
    188 	}
    189 	sx = (dx != 0) ? fabs((w / 2) / dx) : 1e9;
    190 	sy = (dy != 0) ? fabs((h / 2) / dy) : 1e9;
    191 	s = (sx < sy) ? sx : sy;
    192 	*ex = cx + dx * s;
    193 	*ey = cy + dy * s;
    194 }
    195 
    196 static void
    197 draw_grid(cairo_t *cr, Diagram *d, int w, int h)
    198 {
    199 	double gs, x0, y0, x, y;
    200 
    201 	if (!grid_on)
    202 		return;
    203 
    204 	gs = grid_size * d->zoom;
    205 	if (gs < 8.0)
    206 		return;
    207 
    208 	set_color(cr, d->theme->grid);
    209 	cairo_set_line_width(cr, 0.5);
    210 
    211 	x0 = fmod(d->panx * d->zoom, gs);
    212 	y0 = fmod(d->pany * d->zoom, gs);
    213 	if (x0 > 0) x0 -= gs;
    214 	if (y0 > 0) y0 -= gs;
    215 
    216 	for (x = x0; x < w; x += gs) {
    217 		cairo_move_to(cr, x, 0);
    218 		cairo_line_to(cr, x, h);
    219 	}
    220 	for (y = y0; y < h; y += gs) {
    221 		cairo_move_to(cr, 0, y);
    222 		cairo_line_to(cr, w, y);
    223 	}
    224 	cairo_stroke(cr);
    225 }
    226 
    227 static void
    228 draw_conn(cairo_t *cr, Diagram *d, Conn *c, int sel)
    229 {
    230 	int fi, ti;
    231 	Node *fn, *tn;
    232 	double x1, y1, x2, y2, fh, th;
    233 	double fcx, fcy, tcx, tcy;
    234 	const Theme *t;
    235 
    236 	fi = node_by_id(d, c->from);
    237 	ti = node_by_id(d, c->to);
    238 	if (fi < 0 || ti < 0)
    239 		return;
    240 
    241 	t = d->theme;
    242 	fn = &d->nodes[fi];
    243 	tn = &d->nodes[ti];
    244 	fh = node_height(fn);
    245 	th = node_height(tn);
    246 
    247 	fcx = fn->x + fn->w / 2;
    248 	fcy = fn->y + fh / 2;
    249 	tcx = tn->x + tn->w / 2;
    250 	tcy = tn->y + th / 2;
    251 
    252 	rect_edge(fcx, fcy, fn->w, fh, tcx, tcy, &x1, &y1);
    253 	rect_edge(tcx, tcy, tn->w, th, fcx, fcy, &x2, &y2);
    254 
    255 	if (sel)
    256 		set_color(cr, t->sel);
    257 	else
    258 		set_color(cr, t->conn);
    259 	cairo_set_line_width(cr, conn_lw);
    260 	cairo_move_to(cr, x1, y1);
    261 	cairo_line_to(cr, x2, y2);
    262 	cairo_stroke(cr);
    263 
    264 	/* arrowhead */
    265 	{
    266 		double dx, dy, len, ax, ay, px, py, sz;
    267 
    268 		dx = x2 - x1;
    269 		dy = y2 - y1;
    270 		len = hypot(dx, dy);
    271 		if (len < 1.0)
    272 			return;
    273 		dx /= len;
    274 		dy /= len;
    275 		sz = 8.0;
    276 		ax = x2 - dx * sz;
    277 		ay = y2 - dy * sz;
    278 		px = -dy;
    279 		py = dx;
    280 		cairo_move_to(cr, x2, y2);
    281 		cairo_line_to(cr,
    282 			ax + px * sz * 0.35,
    283 			ay + py * sz * 0.35);
    284 		cairo_line_to(cr,
    285 			ax - px * sz * 0.35,
    286 			ay - py * sz * 0.35);
    287 		cairo_close_path(cr);
    288 		cairo_fill(cr);
    289 	}
    290 
    291 	/* label */
    292 	if (c->label[0]) {
    293 		PangoLayout *layout;
    294 		PangoFontDescription *fd;
    295 		int tw, tth;
    296 
    297 		fd = pango_font_description_from_string(font_body);
    298 		layout = pango_cairo_create_layout(cr);
    299 		pango_layout_set_font_description(layout, fd);
    300 		pango_layout_set_text(layout, c->label, -1);
    301 		pango_layout_get_pixel_size(layout, &tw, &tth);
    302 		set_color(cr, t->fg);
    303 		cairo_move_to(cr, (x1 + x2) / 2 - tw / 2,
    304 			(y1 + y2) / 2 - tth / 2);
    305 		pango_cairo_show_layout(cr, layout);
    306 		g_object_unref(layout);
    307 		pango_font_description_free(fd);
    308 	}
    309 }
    310 
    311 /* draw a bento-style node: header | image | comment sections
    312  * separated by borders, all sharp corners */
    313 static void
    314 draw_node(cairo_t *cr, Diagram *d, Node *n)
    315 {
    316 	PangoLayout *layout;
    317 	PangoFontDescription *fd;
    318 	const Theme *t;
    319 	double h, yoff;
    320 	int tw, th;
    321 
    322 	t = d->theme;
    323 	h = node_height(n);
    324 	n->h = h;
    325 
    326 	/* card background */
    327 	cairo_rectangle(cr, n->x, n->y, n->w, h);
    328 	set_color(cr, t->card);
    329 	cairo_fill(cr);
    330 
    331 	/* header background */
    332 	cairo_rectangle(cr, n->x, n->y, n->w, node_hdr_h);
    333 	cairo_set_source_rgb(cr, n->hdr[0], n->hdr[1], n->hdr[2]);
    334 	cairo_fill(cr);
    335 
    336 	/* title text */
    337 	fd = pango_font_description_from_string(font_title);
    338 	pango_font_description_set_weight(fd, PANGO_WEIGHT_MEDIUM);
    339 	layout = pango_cairo_create_layout(cr);
    340 	pango_layout_set_font_description(layout, fd);
    341 	pango_layout_set_text(layout, n->text, -1);
    342 	pango_layout_set_width(layout,
    343 		(int)((n->w - node_pad * 2) * PANGO_SCALE));
    344 	pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END);
    345 	pango_layout_get_pixel_size(layout, &tw, &th);
    346 	set_color(cr, t->primary_fg);
    347 	cairo_move_to(cr, n->x + node_pad,
    348 		n->y + (node_hdr_h - th) / 2);
    349 	pango_cairo_show_layout(cr, layout);
    350 	g_object_unref(layout);
    351 	pango_font_description_free(fd);
    352 
    353 	yoff = n->y + node_hdr_h;
    354 
    355 	/* header bottom border */
    356 	set_color(cr, t->border);
    357 	cairo_set_line_width(cr, 1.0);
    358 	cairo_move_to(cr, n->x, yoff);
    359 	cairo_line_to(cr, n->x + n->w, yoff);
    360 	cairo_stroke(cr);
    361 
    362 	/* image section: flush, natural size */
    363 	if (n->thumb) {
    364 		double ih;
    365 
    366 		ih = gdk_pixbuf_get_height(n->thumb);
    367 		gdk_cairo_set_source_pixbuf(cr,
    368 			n->thumb, n->x, yoff);
    369 		cairo_paint(cr);
    370 		yoff += ih;
    371 
    372 		/* border below image */
    373 		if (n->desc[0]) {
    374 			set_color(cr, t->border);
    375 			cairo_set_line_width(cr, 1.0);
    376 			cairo_move_to(cr, n->x, yoff);
    377 			cairo_line_to(cr, n->x + n->w, yoff);
    378 			cairo_stroke(cr);
    379 		}
    380 	}
    381 
    382 	/* description: rendered markdown */
    383 	if (n->desc[0]) {
    384 		char pbuf[PBUF_SZ];
    385 
    386 		md_to_pango(n->desc, pbuf, sizeof(pbuf));
    387 		fd = pango_font_description_from_string(font_body);
    388 		layout = pango_cairo_create_layout(cr);
    389 		pango_layout_set_font_description(layout, fd);
    390 		pango_layout_set_markup(layout, pbuf, -1);
    391 		pango_layout_set_width(layout,
    392 			(int)((n->w - node_pad * 2)
    393 			* PANGO_SCALE));
    394 		pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR);
    395 		set_color(cr, t->muted_fg);
    396 		cairo_move_to(cr, n->x + node_pad,
    397 			yoff + 4.0);
    398 		pango_cairo_show_layout(cr, layout);
    399 		g_object_unref(layout);
    400 		pango_font_description_free(fd);
    401 	}
    402 
    403 	/* outer border */
    404 	cairo_rectangle(cr, n->x, n->y, n->w, h);
    405 	if (n->selected) {
    406 		set_color(cr, t->sel);
    407 		cairo_set_line_width(cr, 2.0);
    408 	} else {
    409 		set_color(cr, t->border);
    410 		cairo_set_line_width(cr, 1.0);
    411 	}
    412 	cairo_stroke(cr);
    413 }
    414 
    415 void
    416 canvas_draw(GtkDrawingArea *area, cairo_t *cr,
    417 	int w, int h, gpointer data)
    418 {
    419 	Diagram *d;
    420 	int i;
    421 
    422 	(void)area;
    423 	d = (Diagram *)data;
    424 
    425 	/* background */
    426 	set_color(cr, d->theme->bg);
    427 	cairo_paint(cr);
    428 
    429 	draw_grid(cr, d, w, h);
    430 
    431 	/* pan + zoom */
    432 	cairo_save(cr);
    433 	cairo_translate(cr, d->panx * d->zoom,
    434 		d->pany * d->zoom);
    435 	cairo_scale(cr, d->zoom, d->zoom);
    436 
    437 	for (i = 0; i < d->nconns; i++)
    438 		draw_conn(cr, d, &d->conns[i],
    439 			i == d->selconn);
    440 
    441 	for (i = 0; i < d->nnodes; i++)
    442 		draw_node(cr, d, &d->nodes[i]);
    443 
    444 	cairo_restore(cr);
    445 
    446 	/* connect mode indicator */
    447 	if (d->mode == MODE_CONNECT) {
    448 		PangoLayout *layout;
    449 		PangoFontDescription *fd;
    450 
    451 		fd = pango_font_description_from_string(
    452 			"Sans Bold 10");
    453 		layout = pango_cairo_create_layout(cr);
    454 		pango_layout_set_font_description(layout, fd);
    455 		pango_layout_set_text(layout,
    456 			"CONNECT MODE  click two nodes", -1);
    457 		set_color(cr, d->theme->muted_fg);
    458 		cairo_move_to(cr, 10, h - 24);
    459 		pango_cairo_show_layout(cr, layout);
    460 		g_object_unref(layout);
    461 		pango_font_description_free(fd);
    462 	}
    463 }
    464 
    465 void
    466 canvas_click(GtkGestureClick *g, int np,
    467 	double x, double y, gpointer data)
    468 {
    469 	Diagram *d;
    470 	double cx, cy;
    471 	int hit, i;
    472 
    473 	(void)g;
    474 	d = (Diagram *)data;
    475 	screen_to_canvas(d, x, y, &cx, &cy);
    476 
    477 	for (i = 0; i < d->nnodes; i++)
    478 		d->nodes[i].selected = 0;
    479 	d->sel = -1;
    480 	d->selconn = -1;
    481 
    482 	hit = node_at(d, cx, cy);
    483 
    484 	if (d->mode == MODE_CONNECT) {
    485 		if (hit >= 0) {
    486 			if (d->connfrom < 0) {
    487 				d->connfrom = hit;
    488 				d->nodes[hit].selected = 1;
    489 			} else if (hit != d->connfrom) {
    490 				conn_add(d,
    491 					d->nodes[d->connfrom].id,
    492 					d->nodes[hit].id, "");
    493 				d->mode = MODE_NORMAL;
    494 				d->connfrom = -1;
    495 			}
    496 		}
    497 		redraw(d);
    498 		status_update(d);
    499 		return;
    500 	}
    501 
    502 	if (hit >= 0) {
    503 		d->sel = hit;
    504 		d->nodes[hit].selected = 1;
    505 		if (np == 2)
    506 			edit_node_text(d, hit);
    507 	} else {
    508 		d->selconn = conn_at(d, cx, cy);
    509 		if (d->selconn < 0 && np == 2) {
    510 			hit = node_add(d, cx - node_min_w / 2,
    511 				cy - node_min_h / 2, NULL);
    512 			d->sel = hit;
    513 			d->nodes[hit].selected = 1;
    514 			edit_node_text(d, hit);
    515 		}
    516 	}
    517 	redraw(d);
    518 	status_update(d);
    519 }
    520 
    521 void
    522 canvas_rclick(GtkGestureClick *g, int np,
    523 	double x, double y, gpointer data)
    524 {
    525 	Diagram *d;
    526 	double cx, cy;
    527 	int hit, i;
    528 
    529 	(void)g;
    530 	(void)np;
    531 	d = (Diagram *)data;
    532 	screen_to_canvas(d, x, y, &cx, &cy);
    533 
    534 	/* select what was right-clicked */
    535 	for (i = 0; i < d->nnodes; i++)
    536 		d->nodes[i].selected = 0;
    537 	d->selconn = -1;
    538 
    539 	hit = node_at(d, cx, cy);
    540 	if (hit >= 0) {
    541 		d->sel = hit;
    542 		d->nodes[hit].selected = 1;
    543 	} else {
    544 		d->sel = -1;
    545 		d->selconn = conn_at(d, cx, cy);
    546 	}
    547 
    548 	d->ctx_cx = cx;
    549 	d->ctx_cy = cy;
    550 	show_context_menu(d, x, y);
    551 	redraw(d);
    552 }
    553 
    554 void
    555 canvas_drag_begin(GtkGestureDrag *g,
    556 	double x, double y, gpointer data)
    557 {
    558 	Diagram *d;
    559 	double cx, cy;
    560 	int hit;
    561 	GdkModifierType mods;
    562 	GdkEvent *ev;
    563 
    564 	d = (Diagram *)data;
    565 	screen_to_canvas(d, x, y, &cx, &cy);
    566 
    567 	ev = gtk_gesture_get_last_event(
    568 		GTK_GESTURE(g), NULL);
    569 	mods = ev ? gdk_event_get_modifier_state(ev) : 0;
    570 
    571 	hit = node_at(d, cx, cy);
    572 
    573 	if ((mods & GDK_CONTROL_MASK) || hit < 0) {
    574 		d->panning = 1;
    575 		d->pansx = d->panx;
    576 		d->pansy = d->pany;
    577 	} else {
    578 		d->dragging = 1;
    579 		d->dragox = cx - d->nodes[hit].x;
    580 		d->dragoy = cy - d->nodes[hit].y;
    581 		d->sel = hit;
    582 		{
    583 			int i;
    584 			for (i = 0; i < d->nnodes; i++)
    585 				d->nodes[i].selected = 0;
    586 		}
    587 		d->nodes[hit].selected = 1;
    588 	}
    589 }
    590 
    591 void
    592 canvas_drag_update(GtkGestureDrag *g,
    593 	double ox, double oy, gpointer data)
    594 {
    595 	Diagram *d;
    596 	double sx, sy, cx, cy;
    597 
    598 	d = (Diagram *)data;
    599 	sx = sy = cx = cy = 0;
    600 
    601 	if (d->panning) {
    602 		d->panx = d->pansx + ox / d->zoom;
    603 		d->pany = d->pansy + oy / d->zoom;
    604 	}
    605 
    606 	if (d->dragging && d->sel >= 0) {
    607 		gtk_gesture_drag_get_start_point(
    608 			GTK_GESTURE_DRAG(g), &sx, &sy);
    609 		screen_to_canvas(d, sx + ox, sy + oy,
    610 			&cx, &cy);
    611 		d->nodes[d->sel].x = cx - d->dragox;
    612 		d->nodes[d->sel].y = cy - d->dragoy;
    613 		d->modified = 1;
    614 	}
    615 
    616 	redraw(d);
    617 }
    618 
    619 void
    620 canvas_drag_end(GtkGestureDrag *g,
    621 	double ox, double oy, gpointer data)
    622 {
    623 	Diagram *d;
    624 
    625 	(void)g;
    626 	(void)ox;
    627 	(void)oy;
    628 	d = (Diagram *)data;
    629 	d->dragging = 0;
    630 	d->panning = 0;
    631 	status_update(d);
    632 }
    633 
    634 gboolean
    635 canvas_scroll(GtkEventControllerScroll *c,
    636 	double dx, double dy, gpointer data)
    637 {
    638 	Diagram *d;
    639 	double oz;
    640 
    641 	(void)c;
    642 	(void)dx;
    643 	d = (Diagram *)data;
    644 
    645 	oz = d->zoom;
    646 	d->zoom -= dy * zoom_step;
    647 	if (d->zoom < zoom_min) d->zoom = zoom_min;
    648 	if (d->zoom > zoom_max) d->zoom = zoom_max;
    649 
    650 	if (d->zoom != oz) {
    651 		redraw(d);
    652 		status_update(d);
    653 	}
    654 	return TRUE;
    655 }
    656 
    657 gboolean
    658 canvas_key(GtkEventControllerKey *c,
    659 	guint kv, guint kc, GdkModifierType st, gpointer data)
    660 {
    661 	Diagram *d;
    662 
    663 	(void)c;
    664 	(void)kc;
    665 	d = (Diagram *)data;
    666 
    667 	if (st & GDK_CONTROL_MASK) {
    668 		switch (kv) {
    669 		case GDK_KEY_s:
    670 			do_save(d);
    671 			return TRUE;
    672 		case GDK_KEY_o:
    673 			do_open(d);
    674 			return TRUE;
    675 		case GDK_KEY_n:
    676 			do_new(d);
    677 			return TRUE;
    678 		}
    679 	}
    680 
    681 	switch (kv) {
    682 	case GDK_KEY_n:
    683 		if (!(st & GDK_CONTROL_MASK)) {
    684 			int idx;
    685 			idx = node_add(d, -d->panx + 200,
    686 				-d->pany + 200, NULL);
    687 			d->sel = idx;
    688 			d->nodes[idx].selected = 1;
    689 			edit_node_text(d, idx);
    690 			redraw(d);
    691 		}
    692 		return TRUE;
    693 	case GDK_KEY_c:
    694 		if (d->mode == MODE_CONNECT) {
    695 			d->mode = MODE_NORMAL;
    696 			d->connfrom = -1;
    697 		} else {
    698 			d->mode = MODE_CONNECT;
    699 			d->connfrom = -1;
    700 		}
    701 		redraw(d);
    702 		status_update(d);
    703 		return TRUE;
    704 	case GDK_KEY_e:
    705 		if (d->sel >= 0)
    706 			edit_node_text(d, d->sel);
    707 		return TRUE;
    708 	case GDK_KEY_i:
    709 		if (d->sel >= 0)
    710 			do_import_image(d);
    711 		return TRUE;
    712 	case GDK_KEY_Delete: /* FALLTHROUGH */
    713 	case GDK_KEY_x:
    714 		if (d->sel >= 0) {
    715 			node_del(d, d->sel);
    716 			redraw(d);
    717 			status_update(d);
    718 		} else if (d->selconn >= 0) {
    719 			conn_del(d, d->selconn);
    720 			redraw(d);
    721 			status_update(d);
    722 		}
    723 		return TRUE;
    724 	case GDK_KEY_Escape:
    725 		d->mode = MODE_NORMAL;
    726 		d->connfrom = -1;
    727 		{
    728 			int i;
    729 			for (i = 0; i < d->nnodes; i++)
    730 				d->nodes[i].selected = 0;
    731 		}
    732 		d->sel = -1;
    733 		d->selconn = -1;
    734 		redraw(d);
    735 		status_update(d);
    736 		return TRUE;
    737 	case GDK_KEY_plus: /* FALLTHROUGH */
    738 	case GDK_KEY_equal:
    739 		d->zoom += zoom_step;
    740 		if (d->zoom > zoom_max)
    741 			d->zoom = zoom_max;
    742 		redraw(d);
    743 		status_update(d);
    744 		return TRUE;
    745 	case GDK_KEY_minus:
    746 		d->zoom -= zoom_step;
    747 		if (d->zoom < zoom_min)
    748 			d->zoom = zoom_min;
    749 		redraw(d);
    750 		status_update(d);
    751 		return TRUE;
    752 	case GDK_KEY_0:
    753 		d->zoom = 1.0;
    754 		d->panx = 0;
    755 		d->pany = 0;
    756 		redraw(d);
    757 		status_update(d);
    758 		return TRUE;
    759 	}
    760 	return FALSE;
    761 }