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 }