sdiagram

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

sdiagram.c (23893B)


      1 /* sdiagram - simple diagram tool
      2  * See LICENSE file for copyright and license details. */
      3 
      4 #include <stdio.h>
      5 #include <stdlib.h>
      6 #include <string.h>
      7 
      8 #include "sdiagram.h"
      9 #include "config.h"
     10 
     11 static Diagram dia;
     12 
     13 void
     14 screen_to_canvas(Diagram *d, double sx, double sy,
     15 	double *cx, double *cy)
     16 {
     17 	*cx = sx / d->zoom - d->panx;
     18 	*cy = sy / d->zoom - d->pany;
     19 }
     20 
     21 void
     22 canvas_to_screen(Diagram *d, double cx, double cy,
     23 	double *sx, double *sy)
     24 {
     25 	*sx = (cx + d->panx) * d->zoom;
     26 	*sy = (cy + d->pany) * d->zoom;
     27 }
     28 
     29 void
     30 redraw(Diagram *d)
     31 {
     32 	if (d->canvas)
     33 		gtk_widget_queue_draw(d->canvas);
     34 }
     35 
     36 void
     37 status_update(Diagram *d)
     38 {
     39 	char buf[256];
     40 	const char *mode;
     41 
     42 	if (!d->status)
     43 		return;
     44 	mode = d->mode == MODE_CONNECT ? "CONNECT" : "NORMAL";
     45 	snprintf(buf, sizeof(buf),
     46 		" %s | Nodes: %d | Conns: %d | Zoom: %.0f%%"
     47 		" | %s%s",
     48 		mode, d->nnodes, d->nconns, d->zoom * 100,
     49 		d->amode == ASSET_SYMLINK ? "Symlink" : "Copy",
     50 		d->modified ? " [modified]" : "");
     51 	gtk_label_set_text(GTK_LABEL(d->status), buf);
     52 }
     53 
     54 static void
     55 apply_theme_css(Diagram *d)
     56 {
     57 	char css[6144];
     58 	const Theme *t;
     59 	int bg[3], fg[3], bd[3], mt[3], cd[3], mfg[3], pr[3];
     60 
     61 	t = d->theme;
     62 	bg[0] = (int)(t->bg[0] * 255);
     63 	bg[1] = (int)(t->bg[1] * 255);
     64 	bg[2] = (int)(t->bg[2] * 255);
     65 	fg[0] = (int)(t->fg[0] * 255);
     66 	fg[1] = (int)(t->fg[1] * 255);
     67 	fg[2] = (int)(t->fg[2] * 255);
     68 	bd[0] = (int)(t->border[0] * 255);
     69 	bd[1] = (int)(t->border[1] * 255);
     70 	bd[2] = (int)(t->border[2] * 255);
     71 	mt[0] = (int)(t->muted[0] * 255);
     72 	mt[1] = (int)(t->muted[1] * 255);
     73 	mt[2] = (int)(t->muted[2] * 255);
     74 	cd[0] = (int)(t->card[0] * 255);
     75 	cd[1] = (int)(t->card[1] * 255);
     76 	cd[2] = (int)(t->card[2] * 255);
     77 	mfg[0] = (int)(t->muted_fg[0] * 255);
     78 	mfg[1] = (int)(t->muted_fg[1] * 255);
     79 	mfg[2] = (int)(t->muted_fg[2] * 255);
     80 	pr[0] = (int)(t->primary[0] * 255);
     81 	pr[1] = (int)(t->primary[1] * 255);
     82 	pr[2] = (int)(t->primary[2] * 255);
     83 
     84 	snprintf(css, sizeof(css),
     85 		"headerbar {"
     86 		"  background: rgb(%d,%d,%d);"
     87 		"  border-bottom: 1px solid rgb(%d,%d,%d);"
     88 		"  color: rgb(%d,%d,%d);"
     89 		"}"
     90 		"headerbar button {"
     91 		"  background: rgb(%d,%d,%d);"
     92 		"  color: rgb(%d,%d,%d);"
     93 		"  border: 1px solid rgb(%d,%d,%d);"
     94 		"}"
     95 		"headerbar button:hover {"
     96 		"  background: rgb(%d,%d,%d);"
     97 		"}"
     98 		"label {"
     99 		"  color: rgb(%d,%d,%d);"
    100 		"}"
    101 		"popover > contents {"
    102 		"  background: rgb(%d,%d,%d);"
    103 		"  border: 1px solid rgb(%d,%d,%d);"
    104 		"}"
    105 		"popover button {"
    106 		"  background: transparent;"
    107 		"  color: rgb(%d,%d,%d);"
    108 		"  padding: 6px 12px;"
    109 		"}"
    110 		"popover button:hover {"
    111 		"  background: rgb(%d,%d,%d);"
    112 		"}"
    113 		"entry {"
    114 		"  background: rgb(%d,%d,%d);"
    115 		"  color: rgb(%d,%d,%d);"
    116 		"  border: 1px solid rgb(%d,%d,%d);"
    117 		"  caret-color: rgb(%d,%d,%d);"
    118 		"}"
    119 		"separator {"
    120 		"  background: rgb(%d,%d,%d);"
    121 		"}"
    122 		"textview {"
    123 		"  background: rgb(%d,%d,%d);"
    124 		"  color: rgb(%d,%d,%d);"
    125 		"  border: 1px solid rgb(%d,%d,%d);"
    126 		"  caret-color: rgb(%d,%d,%d);"
    127 		"}"
    128 		"textview text {"
    129 		"  background: rgb(%d,%d,%d);"
    130 		"  color: rgb(%d,%d,%d);"
    131 		"}",
    132 		/* headerbar bg */ bg[0], bg[1], bg[2],
    133 		/* headerbar border */ bd[0], bd[1], bd[2],
    134 		/* headerbar fg */ fg[0], fg[1], fg[2],
    135 		/* button bg */ mt[0], mt[1], mt[2],
    136 		/* button fg */ fg[0], fg[1], fg[2],
    137 		/* button border */ bd[0], bd[1], bd[2],
    138 		/* button hover */ bd[0], bd[1], bd[2],
    139 		/* label fg */ fg[0], fg[1], fg[2],
    140 		/* popover bg */ cd[0], cd[1], cd[2],
    141 		/* popover border */ bd[0], bd[1], bd[2],
    142 		/* popover btn fg */ fg[0], fg[1], fg[2],
    143 		/* popover btn hover */ mt[0], mt[1], mt[2],
    144 		/* entry bg */ cd[0], cd[1], cd[2],
    145 		/* entry fg */ fg[0], fg[1], fg[2],
    146 		/* entry border */ bd[0], bd[1], bd[2],
    147 		/* entry caret */ pr[0], pr[1], pr[2],
    148 		/* separator */ bd[0], bd[1], bd[2],
    149 		/* textview bg */ cd[0], cd[1], cd[2],
    150 		/* textview fg */ mfg[0], mfg[1], mfg[2],
    151 		/* textview border */ bd[0], bd[1], bd[2],
    152 		/* textview caret */ fg[0], fg[1], fg[2],
    153 		/* textview text bg */ cd[0], cd[1], cd[2],
    154 		/* textview text fg */ mfg[0], mfg[1], mfg[2]
    155 	);
    156 
    157 	if (!d->theme_css) {
    158 		d->theme_css = gtk_css_provider_new();
    159 		gtk_style_context_add_provider_for_display(
    160 			gdk_display_get_default(),
    161 			GTK_STYLE_PROVIDER(d->theme_css),
    162 			GTK_STYLE_PROVIDER_PRIORITY_USER);
    163 	}
    164 	gtk_css_provider_load_from_string(d->theme_css, css);
    165 }
    166 
    167 void
    168 toggle_theme(Diagram *d)
    169 {
    170 	int i, ci;
    171 	const Theme *t;
    172 
    173 	d->dark = !d->dark;
    174 	d->theme = d->dark ? &theme_dark : &theme_light;
    175 	d->modified = 1;
    176 
    177 	/* re-color existing node headers from new theme */
    178 	t = d->theme;
    179 	for (i = 0; i < d->nnodes; i++) {
    180 		ci = i % t->nhdr;
    181 		d->nodes[i].hdr[0] = t->hdr[ci][0];
    182 		d->nodes[i].hdr[1] = t->hdr[ci][1];
    183 		d->nodes[i].hdr[2] = t->hdr[ci][2];
    184 	}
    185 
    186 	apply_theme_css(d);
    187 
    188 	if (d->theme_btn)
    189 		gtk_button_set_label(
    190 			GTK_BUTTON(d->theme_btn),
    191 			d->dark ? "Light" : "Dark");
    192 
    193 	redraw(d);
    194 	status_update(d);
    195 }
    196 
    197 /* popover text editing */
    198 
    199 static void
    200 close_popover(Diagram *d)
    201 {
    202 	if (d->popover) {
    203 		gtk_popover_popdown(GTK_POPOVER(d->popover));
    204 		gtk_widget_unparent(d->popover);
    205 		d->popover = NULL;
    206 		d->popentry = NULL;
    207 	}
    208 }
    209 
    210 static void
    211 edit_done(GtkEntry *entry, gpointer data)
    212 {
    213 	Diagram *d;
    214 	GtkEntryBuffer *buf;
    215 	const char *text;
    216 	Node *n;
    217 
    218 	d = (Diagram *)data;
    219 	buf = gtk_entry_get_buffer(entry);
    220 	text = gtk_entry_buffer_get_text(buf);
    221 
    222 	if (d->edit_mode == EDIT_TITLE) {
    223 		if (d->editing >= 0 && d->editing < d->nnodes
    224 		    && text && text[0])
    225 			snprintf(d->nodes[d->editing].text,
    226 				sizeof(d->nodes[d->editing].text),
    227 				"%s", text);
    228 	} else if (d->edit_mode == EDIT_DESC) {
    229 		if (d->editing >= 0 && d->editing < d->nnodes) {
    230 			n = &d->nodes[d->editing];
    231 			if (text)
    232 				snprintf(n->desc, sizeof(n->desc),
    233 					"%s", text);
    234 			else
    235 				n->desc[0] = '\0';
    236 		}
    237 	} else if (d->edit_mode == EDIT_CONN_LABEL) {
    238 		if (d->editing >= 0 && d->editing < d->nconns
    239 		    && text)
    240 			snprintf(d->conns[d->editing].label,
    241 				sizeof(d->conns[d->editing].label),
    242 				"%s", text);
    243 	}
    244 
    245 	d->modified = 1;
    246 	d->editing = -1;
    247 	close_popover(d);
    248 	redraw(d);
    249 	gtk_widget_grab_focus(d->canvas);
    250 }
    251 
    252 static void
    253 show_edit_popover(Diagram *d, double sx, double sy,
    254 	const char *initial)
    255 {
    256 	GdkRectangle rect;
    257 
    258 	close_popover(d);
    259 
    260 	d->popentry = gtk_entry_new();
    261 	if (initial)
    262 		gtk_entry_buffer_set_text(
    263 			gtk_entry_get_buffer(
    264 				GTK_ENTRY(d->popentry)),
    265 			initial, -1);
    266 	g_signal_connect(d->popentry, "activate",
    267 		G_CALLBACK(edit_done), d);
    268 
    269 	d->popover = gtk_popover_new();
    270 	gtk_popover_set_child(GTK_POPOVER(d->popover),
    271 		d->popentry);
    272 	gtk_popover_set_autohide(GTK_POPOVER(d->popover),
    273 		TRUE);
    274 
    275 	rect.x = (int)sx;
    276 	rect.y = (int)sy;
    277 	rect.width = 1;
    278 	rect.height = 1;
    279 
    280 	gtk_widget_set_parent(d->popover, d->canvas);
    281 	gtk_popover_set_pointing_to(GTK_POPOVER(d->popover),
    282 		&rect);
    283 	gtk_popover_popup(GTK_POPOVER(d->popover));
    284 	gtk_widget_grab_focus(d->popentry);
    285 }
    286 
    287 void
    288 edit_node_text(Diagram *d, int idx)
    289 {
    290 	double sx, sy;
    291 
    292 	if (idx < 0 || idx >= d->nnodes)
    293 		return;
    294 
    295 	d->edit_mode = EDIT_TITLE;
    296 	d->editing = idx;
    297 
    298 	canvas_to_screen(d,
    299 		d->nodes[idx].x + d->nodes[idx].w / 2,
    300 		d->nodes[idx].y, &sx, &sy);
    301 	show_edit_popover(d, sx, sy, d->nodes[idx].text);
    302 }
    303 
    304 /* inline description editing */
    305 
    306 static void
    307 desc_save_text(Diagram *d)
    308 {
    309 	GtkTextBuffer *tbuf;
    310 	GtkTextIter start, end;
    311 	char *text;
    312 
    313 	if (!d->desc_edit)
    314 		return;
    315 	if (d->editing < 0 || d->editing >= d->nnodes)
    316 		return;
    317 
    318 	tbuf = gtk_text_view_get_buffer(
    319 		GTK_TEXT_VIEW(d->desc_edit));
    320 	gtk_text_buffer_get_bounds(tbuf, &start, &end);
    321 	text = gtk_text_buffer_get_text(tbuf,
    322 		&start, &end, FALSE);
    323 	if (text) {
    324 		snprintf(d->nodes[d->editing].desc,
    325 			sizeof(d->nodes[d->editing].desc),
    326 			"%s", text);
    327 		g_free(text);
    328 	}
    329 	d->modified = 1;
    330 }
    331 
    332 /* deferred cleanup: runs after event processing */
    333 static gboolean
    334 desc_cleanup_idle(gpointer data)
    335 {
    336 	Diagram *d;
    337 	GtkWidget *tv;
    338 
    339 	d = (Diagram *)data;
    340 	tv = d->desc_edit;
    341 	if (!tv)
    342 		return G_SOURCE_REMOVE;
    343 
    344 	d->desc_edit = NULL;
    345 	d->editing = -1;
    346 	gtk_overlay_remove_overlay(
    347 		GTK_OVERLAY(d->overlay), tv);
    348 	redraw(d);
    349 	gtk_widget_grab_focus(d->canvas);
    350 	return G_SOURCE_REMOVE;
    351 }
    352 
    353 static void
    354 desc_finish(Diagram *d, int save)
    355 {
    356 	if (!d->desc_edit)
    357 		return;
    358 	if (save)
    359 		desc_save_text(d);
    360 	/* defer widget removal to avoid crash
    361 	 * during focus-out signal */
    362 	g_idle_add(desc_cleanup_idle, d);
    363 }
    364 
    365 static gboolean
    366 desc_key(GtkEventControllerKey *c, guint kv,
    367 	guint kc, GdkModifierType st, gpointer data)
    368 {
    369 	(void)c;
    370 	(void)kc;
    371 
    372 	if (kv == GDK_KEY_Escape) {
    373 		desc_finish((Diagram *)data, 0);
    374 		return TRUE;
    375 	}
    376 	if ((st & GDK_CONTROL_MASK)
    377 	    && kv == GDK_KEY_Return) {
    378 		desc_finish((Diagram *)data, 1);
    379 		return TRUE;
    380 	}
    381 	return FALSE;
    382 }
    383 
    384 static void
    385 desc_focus_out(GtkEventControllerFocus *c, gpointer data)
    386 {
    387 	(void)c;
    388 	desc_finish((Diagram *)data, 1);
    389 }
    390 
    391 void
    392 edit_node_desc(Diagram *d, int idx)
    393 {
    394 	GtkWidget *tv;
    395 	GtkTextBuffer *tbuf;
    396 	GtkEventController *key, *focus;
    397 	Node *n;
    398 	double sx, sy, desc_y, desc_h;
    399 
    400 	if (idx < 0 || idx >= d->nnodes)
    401 		return;
    402 
    403 	/* close existing edit */
    404 	if (d->desc_edit)
    405 		desc_finish(d, 1);
    406 
    407 	n = &d->nodes[idx];
    408 	d->edit_mode = EDIT_DESC;
    409 	d->editing = idx;
    410 
    411 	/* desc area starts below header + image */
    412 	desc_y = n->y + node_hdr_h;
    413 	if (n->thumb)
    414 		desc_y += gdk_pixbuf_get_height(n->thumb);
    415 
    416 	canvas_to_screen(d, n->x, desc_y, &sx, &sy);
    417 
    418 	desc_h = n->h - (desc_y - n->y);
    419 	if (desc_h < 80.0)
    420 		desc_h = 80.0;
    421 
    422 	/* create text view */
    423 	tv = gtk_text_view_new();
    424 	gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(tv),
    425 		GTK_WRAP_WORD_CHAR);
    426 	gtk_text_view_set_left_margin(GTK_TEXT_VIEW(tv), 6);
    427 	gtk_text_view_set_right_margin(GTK_TEXT_VIEW(tv), 6);
    428 	gtk_text_view_set_top_margin(GTK_TEXT_VIEW(tv), 4);
    429 	gtk_text_view_set_bottom_margin(GTK_TEXT_VIEW(tv), 4);
    430 
    431 	tbuf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(tv));
    432 	gtk_text_buffer_set_text(tbuf, n->desc, -1);
    433 
    434 	/* position over the desc section */
    435 	gtk_widget_set_halign(tv, GTK_ALIGN_START);
    436 	gtk_widget_set_valign(tv, GTK_ALIGN_START);
    437 	gtk_widget_set_margin_start(tv, (int)sx);
    438 	gtk_widget_set_margin_top(tv, (int)sy);
    439 	gtk_widget_set_size_request(tv,
    440 		(int)(n->w * d->zoom),
    441 		(int)(desc_h * d->zoom));
    442 
    443 	/* key handler: Ctrl+Enter to save, Escape to cancel */
    444 	key = gtk_event_controller_key_new();
    445 	g_signal_connect(key, "key-pressed",
    446 		G_CALLBACK(desc_key), d);
    447 	gtk_widget_add_controller(tv, key);
    448 
    449 	/* save on focus out */
    450 	focus = gtk_event_controller_focus_new();
    451 	g_signal_connect(focus, "leave",
    452 		G_CALLBACK(desc_focus_out), d);
    453 	gtk_widget_add_controller(tv, focus);
    454 
    455 	gtk_overlay_add_overlay(
    456 		GTK_OVERLAY(d->overlay), tv);
    457 	d->desc_edit = tv;
    458 
    459 	gtk_widget_grab_focus(tv);
    460 }
    461 
    462 /* context menu */
    463 
    464 static void
    465 close_ctx(Diagram *d)
    466 {
    467 	if (d->ctxmenu) {
    468 		gtk_popover_popdown(GTK_POPOVER(d->ctxmenu));
    469 		gtk_widget_unparent(d->ctxmenu);
    470 		d->ctxmenu = NULL;
    471 	}
    472 }
    473 
    474 static void
    475 ctx_edit_title(GtkButton *btn, gpointer data)
    476 {
    477 	Diagram *d;
    478 
    479 	(void)btn;
    480 	d = (Diagram *)data;
    481 	close_ctx(d);
    482 	if (d->sel >= 0)
    483 		edit_node_text(d, d->sel);
    484 }
    485 
    486 static void
    487 ctx_edit_desc(GtkButton *btn, gpointer data)
    488 {
    489 	Diagram *d;
    490 
    491 	(void)btn;
    492 	d = (Diagram *)data;
    493 	close_ctx(d);
    494 	if (d->sel >= 0)
    495 		edit_node_desc(d, d->sel);
    496 }
    497 
    498 static void
    499 ctx_add_image(GtkButton *btn, gpointer data)
    500 {
    501 	Diagram *d;
    502 
    503 	(void)btn;
    504 	d = (Diagram *)data;
    505 	close_ctx(d);
    506 	do_import_image(d);
    507 }
    508 
    509 static void
    510 ctx_remove_image(GtkButton *btn, gpointer data)
    511 {
    512 	Diagram *d;
    513 
    514 	(void)btn;
    515 	d = (Diagram *)data;
    516 	close_ctx(d);
    517 	if (d->sel >= 0) {
    518 		asset_remove(d, d->sel);
    519 		redraw(d);
    520 	}
    521 }
    522 
    523 static void
    524 ctx_delete_node(GtkButton *btn, gpointer data)
    525 {
    526 	Diagram *d;
    527 
    528 	(void)btn;
    529 	d = (Diagram *)data;
    530 	close_ctx(d);
    531 	if (d->sel >= 0) {
    532 		node_del(d, d->sel);
    533 		redraw(d);
    534 		status_update(d);
    535 	}
    536 }
    537 
    538 static void
    539 ctx_delete_conn(GtkButton *btn, gpointer data)
    540 {
    541 	Diagram *d;
    542 
    543 	(void)btn;
    544 	d = (Diagram *)data;
    545 	close_ctx(d);
    546 	if (d->selconn >= 0) {
    547 		conn_del(d, d->selconn);
    548 		redraw(d);
    549 		status_update(d);
    550 	}
    551 }
    552 
    553 static void
    554 ctx_edit_label(GtkButton *btn, gpointer data)
    555 {
    556 	Diagram *d;
    557 	double sx, sy;
    558 	int fi;
    559 	Node *fn;
    560 
    561 	(void)btn;
    562 	d = (Diagram *)data;
    563 	close_ctx(d);
    564 	if (d->selconn < 0 || d->selconn >= d->nconns)
    565 		return;
    566 
    567 	d->edit_mode = EDIT_CONN_LABEL;
    568 	d->editing = d->selconn;
    569 
    570 	fi = node_by_id(d, d->conns[d->selconn].from);
    571 	if (fi < 0)
    572 		return;
    573 	fn = &d->nodes[fi];
    574 	canvas_to_screen(d,
    575 		fn->x + fn->w / 2,
    576 		fn->y + node_height(fn) / 2,
    577 		&sx, &sy);
    578 	show_edit_popover(d, sx, sy,
    579 		d->conns[d->selconn].label);
    580 }
    581 
    582 static void
    583 ctx_add_node(GtkButton *btn, gpointer data)
    584 {
    585 	Diagram *d;
    586 	int idx;
    587 
    588 	(void)btn;
    589 	d = (Diagram *)data;
    590 	close_ctx(d);
    591 	idx = node_add(d,
    592 		d->ctx_cx - node_min_w / 2,
    593 		d->ctx_cy - node_min_h / 2, NULL);
    594 	d->sel = idx;
    595 	d->nodes[idx].selected = 1;
    596 	edit_node_text(d, idx);
    597 	redraw(d);
    598 	status_update(d);
    599 }
    600 
    601 static GtkWidget *
    602 ctx_btn(const char *label, GCallback cb, gpointer data)
    603 {
    604 	GtkWidget *btn;
    605 
    606 	btn = gtk_button_new_with_label(label);
    607 	gtk_button_set_has_frame(GTK_BUTTON(btn), FALSE);
    608 	gtk_widget_set_halign(btn, GTK_ALIGN_FILL);
    609 	g_signal_connect(btn, "clicked", cb, data);
    610 	return btn;
    611 }
    612 
    613 void
    614 show_context_menu(Diagram *d, double sx, double sy)
    615 {
    616 	GtkWidget *vbox;
    617 	GdkRectangle rect;
    618 
    619 	close_ctx(d);
    620 
    621 	vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    622 	gtk_widget_set_size_request(vbox, 140, -1);
    623 
    624 	if (d->sel >= 0) {
    625 		gtk_box_append(GTK_BOX(vbox),
    626 			ctx_btn("Edit Title",
    627 				G_CALLBACK(ctx_edit_title), d));
    628 		gtk_box_append(GTK_BOX(vbox),
    629 			ctx_btn("Edit Description",
    630 				G_CALLBACK(ctx_edit_desc), d));
    631 		gtk_box_append(GTK_BOX(vbox),
    632 			gtk_separator_new(
    633 				GTK_ORIENTATION_HORIZONTAL));
    634 		gtk_box_append(GTK_BOX(vbox),
    635 			ctx_btn("Add Image",
    636 				G_CALLBACK(ctx_add_image), d));
    637 		if (d->nodes[d->sel].asset[0])
    638 			gtk_box_append(GTK_BOX(vbox),
    639 				ctx_btn("Remove Image",
    640 					G_CALLBACK(ctx_remove_image),
    641 					d));
    642 		gtk_box_append(GTK_BOX(vbox),
    643 			gtk_separator_new(
    644 				GTK_ORIENTATION_HORIZONTAL));
    645 		gtk_box_append(GTK_BOX(vbox),
    646 			ctx_btn("Delete Node",
    647 				G_CALLBACK(ctx_delete_node), d));
    648 	} else if (d->selconn >= 0) {
    649 		gtk_box_append(GTK_BOX(vbox),
    650 			ctx_btn("Edit Label",
    651 				G_CALLBACK(ctx_edit_label), d));
    652 		gtk_box_append(GTK_BOX(vbox),
    653 			gtk_separator_new(
    654 				GTK_ORIENTATION_HORIZONTAL));
    655 		gtk_box_append(GTK_BOX(vbox),
    656 			ctx_btn("Delete Connection",
    657 				G_CALLBACK(ctx_delete_conn), d));
    658 	} else {
    659 		gtk_box_append(GTK_BOX(vbox),
    660 			ctx_btn("Add Node Here",
    661 				G_CALLBACK(ctx_add_node), d));
    662 	}
    663 
    664 	d->ctxmenu = gtk_popover_new();
    665 	gtk_popover_set_child(GTK_POPOVER(d->ctxmenu), vbox);
    666 	gtk_popover_set_autohide(GTK_POPOVER(d->ctxmenu), TRUE);
    667 
    668 	rect.x = (int)sx;
    669 	rect.y = (int)sy;
    670 	rect.width = 1;
    671 	rect.height = 1;
    672 
    673 	gtk_widget_set_parent(d->ctxmenu, d->canvas);
    674 	gtk_popover_set_pointing_to(
    675 		GTK_POPOVER(d->ctxmenu), &rect);
    676 	gtk_popover_popup(GTK_POPOVER(d->ctxmenu));
    677 }
    678 
    679 /* file dialogs */
    680 
    681 static void
    682 open_cb(GObject *src, GAsyncResult *res, gpointer data)
    683 {
    684 	GtkFileDialog *dlg;
    685 	GFile *file;
    686 	Diagram *d;
    687 	char *path;
    688 
    689 	d = (Diagram *)data;
    690 	dlg = GTK_FILE_DIALOG(src);
    691 	file = gtk_file_dialog_select_folder_finish(
    692 		dlg, res, NULL);
    693 	if (!file)
    694 		return;
    695 
    696 	path = g_file_get_path(file);
    697 	if (path) {
    698 		db_load(d, path);
    699 		apply_theme_css(d);
    700 		if (d->theme_btn)
    701 			gtk_button_set_label(
    702 				GTK_BUTTON(d->theme_btn),
    703 				d->dark ? "Light" : "Dark");
    704 		redraw(d);
    705 		status_update(d);
    706 		g_free(path);
    707 	}
    708 	g_object_unref(file);
    709 }
    710 
    711 void
    712 do_open(Diagram *d)
    713 {
    714 	GtkFileDialog *dlg;
    715 
    716 	dlg = gtk_file_dialog_new();
    717 	gtk_file_dialog_set_title(dlg, "Open Diagram");
    718 	gtk_file_dialog_select_folder(dlg,
    719 		GTK_WINDOW(d->window), NULL, open_cb, d);
    720 }
    721 
    722 static void
    723 save_as_cb(GObject *src, GAsyncResult *res, gpointer data)
    724 {
    725 	GtkFileDialog *dlg;
    726 	GFile *file;
    727 	Diagram *d;
    728 	char *path;
    729 
    730 	d = (Diagram *)data;
    731 	dlg = GTK_FILE_DIALOG(src);
    732 	file = gtk_file_dialog_select_folder_finish(
    733 		dlg, res, NULL);
    734 	if (!file)
    735 		return;
    736 
    737 	path = g_file_get_path(file);
    738 	if (path) {
    739 		db_new(d, path);
    740 		db_save(d);
    741 		status_update(d);
    742 		g_free(path);
    743 	}
    744 	g_object_unref(file);
    745 }
    746 
    747 void
    748 do_save(Diagram *d)
    749 {
    750 	if (!d->dbpath[0]) {
    751 		do_save_as(d);
    752 		return;
    753 	}
    754 	db_save(d);
    755 	status_update(d);
    756 }
    757 
    758 void
    759 do_save_as(Diagram *d)
    760 {
    761 	GtkFileDialog *dlg;
    762 
    763 	dlg = gtk_file_dialog_new();
    764 	gtk_file_dialog_set_title(dlg,
    765 		"Save Diagram (select directory)");
    766 	gtk_file_dialog_select_folder(dlg,
    767 		GTK_WINDOW(d->window), NULL, save_as_cb, d);
    768 }
    769 
    770 void
    771 do_new(Diagram *d)
    772 {
    773 	int i;
    774 
    775 	for (i = 0; i < d->nnodes; i++) {
    776 		if (d->nodes[i].thumb)
    777 			g_object_unref(d->nodes[i].thumb);
    778 	}
    779 	d->nnodes = 0;
    780 	d->nconns = 0;
    781 	d->sel = -1;
    782 	d->selconn = -1;
    783 	d->panx = 0;
    784 	d->pany = 0;
    785 	d->zoom = 1.0;
    786 	d->nextid = 1;
    787 	d->hdr_idx = 0;
    788 	d->modified = 0;
    789 	d->path[0] = '\0';
    790 	d->dbpath[0] = '\0';
    791 	d->adir[0] = '\0';
    792 	d->mode = MODE_NORMAL;
    793 	d->connfrom = -1;
    794 	redraw(d);
    795 	status_update(d);
    796 }
    797 
    798 static void
    799 import_cb(GObject *src, GAsyncResult *res, gpointer data)
    800 {
    801 	GtkFileDialog *dlg;
    802 	GFile *file;
    803 	Diagram *d;
    804 	char *path;
    805 
    806 	d = (Diagram *)data;
    807 	dlg = GTK_FILE_DIALOG(src);
    808 	file = gtk_file_dialog_open_finish(dlg, res, NULL);
    809 	if (!file)
    810 		return;
    811 
    812 	path = g_file_get_path(file);
    813 	if (path && d->sel >= 0) {
    814 		if (!d->adir[0])
    815 			snprintf(d->adir, sizeof(d->adir),
    816 				"/tmp/sdiagram-assets");
    817 		asset_import(d, d->sel, path);
    818 		redraw(d);
    819 		g_free(path);
    820 	}
    821 	g_object_unref(file);
    822 }
    823 
    824 void
    825 do_import_image(Diagram *d)
    826 {
    827 	GtkFileDialog *dlg;
    828 	GtkFileFilter *filter;
    829 	GListStore *filters;
    830 
    831 	if (d->sel < 0)
    832 		return;
    833 
    834 	filter = gtk_file_filter_new();
    835 	gtk_file_filter_set_name(filter, "Images");
    836 	gtk_file_filter_add_mime_type(filter, "image/*");
    837 
    838 	filters = g_list_store_new(GTK_TYPE_FILE_FILTER);
    839 	g_list_store_append(filters, filter);
    840 	g_object_unref(filter);
    841 
    842 	dlg = gtk_file_dialog_new();
    843 	gtk_file_dialog_set_title(dlg, "Import Image");
    844 	gtk_file_dialog_set_filters(dlg,
    845 		G_LIST_MODEL(filters));
    846 	gtk_file_dialog_open(dlg,
    847 		GTK_WINDOW(d->window), NULL, import_cb, d);
    848 	g_object_unref(filters);
    849 }
    850 
    851 /* toolbar callbacks */
    852 
    853 static void
    854 on_new(GtkButton *btn, gpointer data)
    855 {
    856 	(void)btn;
    857 	do_new((Diagram *)data);
    858 }
    859 
    860 static void
    861 on_open(GtkButton *btn, gpointer data)
    862 {
    863 	(void)btn;
    864 	do_open((Diagram *)data);
    865 }
    866 
    867 static void
    868 on_save(GtkButton *btn, gpointer data)
    869 {
    870 	(void)btn;
    871 	do_save((Diagram *)data);
    872 }
    873 
    874 static void
    875 on_add(GtkButton *btn, gpointer data)
    876 {
    877 	Diagram *d;
    878 	int idx;
    879 
    880 	(void)btn;
    881 	d = (Diagram *)data;
    882 	idx = node_add(d, -d->panx + 300,
    883 		-d->pany + 200, NULL);
    884 	d->sel = idx;
    885 	d->nodes[idx].selected = 1;
    886 	edit_node_text(d, idx);
    887 	redraw(d);
    888 	status_update(d);
    889 }
    890 
    891 static void
    892 on_connect(GtkButton *btn, gpointer data)
    893 {
    894 	Diagram *d;
    895 
    896 	(void)btn;
    897 	d = (Diagram *)data;
    898 	if (d->mode == MODE_CONNECT) {
    899 		d->mode = MODE_NORMAL;
    900 		d->connfrom = -1;
    901 	} else {
    902 		d->mode = MODE_CONNECT;
    903 		d->connfrom = -1;
    904 	}
    905 	redraw(d);
    906 	status_update(d);
    907 }
    908 
    909 static void
    910 on_image(GtkButton *btn, gpointer data)
    911 {
    912 	(void)btn;
    913 	do_import_image((Diagram *)data);
    914 }
    915 
    916 static void
    917 on_mode_toggle(GtkButton *btn, gpointer data)
    918 {
    919 	Diagram *d;
    920 
    921 	(void)btn;
    922 	d = (Diagram *)data;
    923 	d->amode = d->amode == ASSET_COPY
    924 		? ASSET_SYMLINK : ASSET_COPY;
    925 	status_update(d);
    926 }
    927 
    928 static void
    929 on_theme_toggle(GtkButton *btn, gpointer data)
    930 {
    931 	(void)btn;
    932 	toggle_theme((Diagram *)data);
    933 }
    934 
    935 static GtkWidget *
    936 make_btn(const char *label, GCallback cb, gpointer data)
    937 {
    938 	GtkWidget *btn;
    939 
    940 	btn = gtk_button_new_with_label(label);
    941 	g_signal_connect(btn, "clicked", cb, data);
    942 	return btn;
    943 }
    944 
    945 static void
    946 activate(GtkApplication *app, gpointer data)
    947 {
    948 	GtkWidget *window, *vbox, *canvas, *status;
    949 	GtkWidget *header;
    950 	GtkGesture *click, *drag, *rclick;
    951 	GtkEventController *scroll, *key;
    952 	GtkCssProvider *css;
    953 	Diagram *d;
    954 
    955 	(void)data;
    956 	d = &dia;
    957 
    958 	/* squared corners CSS */
    959 	css = gtk_css_provider_new();
    960 	gtk_css_provider_load_from_string(css, app_css);
    961 	gtk_style_context_add_provider_for_display(
    962 		gdk_display_get_default(),
    963 		GTK_STYLE_PROVIDER(css),
    964 		GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
    965 
    966 	/* theme colors for GTK chrome */
    967 	apply_theme_css(d);
    968 
    969 	/* window */
    970 	window = gtk_application_window_new(app);
    971 	gtk_window_set_title(GTK_WINDOW(window), "sdiagram");
    972 	gtk_window_set_default_size(GTK_WINDOW(window),
    973 		1200, 800);
    974 	d->window = window;
    975 
    976 	/* header bar */
    977 	header = gtk_header_bar_new();
    978 
    979 	/* theme toggle - top left */
    980 	d->theme_btn = make_btn(
    981 		d->dark ? "Light" : "Dark",
    982 		G_CALLBACK(on_theme_toggle), d);
    983 	gtk_header_bar_pack_start(GTK_HEADER_BAR(header),
    984 		d->theme_btn);
    985 	gtk_header_bar_pack_start(GTK_HEADER_BAR(header),
    986 		gtk_separator_new(GTK_ORIENTATION_VERTICAL));
    987 
    988 	gtk_header_bar_pack_start(GTK_HEADER_BAR(header),
    989 		make_btn("New", G_CALLBACK(on_new), d));
    990 	gtk_header_bar_pack_start(GTK_HEADER_BAR(header),
    991 		make_btn("Open", G_CALLBACK(on_open), d));
    992 	gtk_header_bar_pack_start(GTK_HEADER_BAR(header),
    993 		make_btn("Save", G_CALLBACK(on_save), d));
    994 	gtk_header_bar_pack_start(GTK_HEADER_BAR(header),
    995 		gtk_separator_new(GTK_ORIENTATION_VERTICAL));
    996 	gtk_header_bar_pack_start(GTK_HEADER_BAR(header),
    997 		make_btn("+ Node", G_CALLBACK(on_add), d));
    998 	gtk_header_bar_pack_start(GTK_HEADER_BAR(header),
    999 		make_btn("Connect",
   1000 			G_CALLBACK(on_connect), d));
   1001 	gtk_header_bar_pack_start(GTK_HEADER_BAR(header),
   1002 		make_btn("Image", G_CALLBACK(on_image), d));
   1003 	gtk_header_bar_pack_end(GTK_HEADER_BAR(header),
   1004 		make_btn("Copy/Link",
   1005 			G_CALLBACK(on_mode_toggle), d));
   1006 	gtk_window_set_titlebar(GTK_WINDOW(window), header);
   1007 
   1008 	/* layout */
   1009 	vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
   1010 	gtk_window_set_child(GTK_WINDOW(window), vbox);
   1011 
   1012 	/* overlay wraps canvas for inline editing */
   1013 	d->overlay = gtk_overlay_new();
   1014 	gtk_widget_set_hexpand(d->overlay, TRUE);
   1015 	gtk_widget_set_vexpand(d->overlay, TRUE);
   1016 
   1017 	/* canvas */
   1018 	canvas = gtk_drawing_area_new();
   1019 	gtk_widget_set_hexpand(canvas, TRUE);
   1020 	gtk_widget_set_vexpand(canvas, TRUE);
   1021 	gtk_widget_set_focusable(canvas, TRUE);
   1022 	gtk_drawing_area_set_draw_func(
   1023 		GTK_DRAWING_AREA(canvas),
   1024 		canvas_draw, d, NULL);
   1025 	d->canvas = canvas;
   1026 	gtk_overlay_set_child(
   1027 		GTK_OVERLAY(d->overlay), canvas);
   1028 
   1029 	/* left click */
   1030 	click = gtk_gesture_click_new();
   1031 	g_signal_connect(click, "pressed",
   1032 		G_CALLBACK(canvas_click), d);
   1033 	gtk_widget_add_controller(canvas,
   1034 		GTK_EVENT_CONTROLLER(click));
   1035 
   1036 	/* right click */
   1037 	rclick = gtk_gesture_click_new();
   1038 	gtk_gesture_single_set_button(
   1039 		GTK_GESTURE_SINGLE(rclick), 3);
   1040 	g_signal_connect(rclick, "pressed",
   1041 		G_CALLBACK(canvas_rclick), d);
   1042 	gtk_widget_add_controller(canvas,
   1043 		GTK_EVENT_CONTROLLER(rclick));
   1044 
   1045 	/* drag */
   1046 	drag = gtk_gesture_drag_new();
   1047 	g_signal_connect(drag, "drag-begin",
   1048 		G_CALLBACK(canvas_drag_begin), d);
   1049 	g_signal_connect(drag, "drag-update",
   1050 		G_CALLBACK(canvas_drag_update), d);
   1051 	g_signal_connect(drag, "drag-end",
   1052 		G_CALLBACK(canvas_drag_end), d);
   1053 	gtk_widget_add_controller(canvas,
   1054 		GTK_EVENT_CONTROLLER(drag));
   1055 
   1056 	/* scroll */
   1057 	scroll = gtk_event_controller_scroll_new(
   1058 		GTK_EVENT_CONTROLLER_SCROLL_VERTICAL);
   1059 	g_signal_connect(scroll, "scroll",
   1060 		G_CALLBACK(canvas_scroll), d);
   1061 	gtk_widget_add_controller(canvas, scroll);
   1062 
   1063 	/* keyboard */
   1064 	key = gtk_event_controller_key_new();
   1065 	g_signal_connect(key, "key-pressed",
   1066 		G_CALLBACK(canvas_key), d);
   1067 	gtk_widget_add_controller(canvas, key);
   1068 
   1069 	gtk_box_append(GTK_BOX(vbox), d->overlay);
   1070 
   1071 	/* status bar */
   1072 	status = gtk_label_new("");
   1073 	gtk_widget_set_halign(status, GTK_ALIGN_START);
   1074 	gtk_widget_set_margin_start(status, 6);
   1075 	gtk_widget_set_margin_end(status, 6);
   1076 	gtk_widget_set_margin_top(status, 3);
   1077 	gtk_widget_set_margin_bottom(status, 3);
   1078 	d->status = status;
   1079 	gtk_box_append(GTK_BOX(vbox), status);
   1080 
   1081 	status_update(d);
   1082 	gtk_window_present(GTK_WINDOW(window));
   1083 	gtk_widget_grab_focus(canvas);
   1084 }
   1085 
   1086 int
   1087 main(int argc, char *argv[])
   1088 {
   1089 	GtkApplication *app;
   1090 	int ret;
   1091 
   1092 	data_init(&dia);
   1093 
   1094 	if (argc > 1) {
   1095 		if (db_load(&dia, argv[1]) < 0)
   1096 			fprintf(stderr,
   1097 				"sdiagram: cannot open '%s'\n",
   1098 				argv[1]);
   1099 	}
   1100 
   1101 	app = gtk_application_new("org.sdiagram.app",
   1102 		G_APPLICATION_NON_UNIQUE);
   1103 	g_signal_connect(app, "activate",
   1104 		G_CALLBACK(activate), NULL);
   1105 	ret = g_application_run(G_APPLICATION(app), 1, argv);
   1106 	g_object_unref(app);
   1107 	data_free(&dia);
   1108 	return ret;
   1109 }