anastasis-gtk_rows.c (13463B)
1 /* 2 This file is part of anastasis-gtk. 3 Copyright (C) 2025 Anastasis SARL 4 5 Anastasis is free software; you can redistribute it and/or modify 6 it under the terms of the GNU General Public License as published 7 by the Free Software Foundation; either version 3, or (at your 8 option) any later version. 9 10 Anastasis is distributed in the hope that it will be useful, but 11 WITHOUT ANY WARRANTY; without even the implied warranty of 12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 General Public License for more details. 14 15 You should have received a copy of the GNU General Public License 16 along with Anastasis; see the file COPYING. If not, write to the 17 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 18 Boston, MA 02110-1301, USA. 19 */ 20 /** 21 * @file src/anastasis/anastasis-gtk_rows.c 22 * @brief Rows of the GtkColumnViews and the models holding them 23 * @author Christian Grothoff 24 */ 25 #include <gnunet/gnunet_util_lib.h> 26 #include "anastasis-gtk_helper.h" 27 #include "anastasis-gtk_rows.h" 28 29 30 /** 31 * Key under which the row a cell shows is attached to the widget of that 32 * cell. This is how #AG_row_at() finds the row that was clicked. 33 */ 34 #define ROW_KEY "anastasis-row" 35 36 37 /** 38 * One row of one of our lists. 39 */ 40 struct _AGRow 41 { 42 /** 43 * We are a GObject. 44 */ 45 GObject parent_instance; 46 47 /** 48 * The values of this row. 49 */ 50 json_t *fields; 51 52 /** 53 * Image to render this row with, or NULL. 54 */ 55 GdkPaintable *image; 56 57 /** 58 * Rows nested under this one, or NULL if there are none. 59 */ 60 GListStore *children; 61 }; 62 63 G_DEFINE_TYPE (AGRow, 64 AG_row, 65 G_TYPE_OBJECT); 66 67 68 static void 69 AG_row_finalize (GObject *object) 70 { 71 AGRow *row = AG_ROW (object); 72 73 json_decref (row->fields); 74 row->fields = NULL; 75 g_clear_object (&row->image); 76 g_clear_object (&row->children); 77 G_OBJECT_CLASS (AG_row_parent_class)->finalize (object); 78 } 79 80 81 static void 82 AG_row_class_init (AGRowClass *klass) 83 { 84 G_OBJECT_CLASS (klass)->finalize = &AG_row_finalize; 85 } 86 87 88 static void 89 AG_row_init (AGRow *row) 90 { 91 row->fields = json_object (); 92 GNUNET_assert (NULL != row->fields); 93 } 94 95 96 AGRow * 97 AG_row_new (json_t *fields) 98 { 99 AGRow *row; 100 101 GNUNET_assert (json_is_object (fields)); 102 row = g_object_new (AG_TYPE_ROW, 103 NULL); 104 json_decref (row->fields); 105 row->fields = fields; 106 return row; 107 } 108 109 110 void 111 AG_row_set (AGRow *row, 112 const char *field, 113 json_t *value) 114 { 115 GNUNET_assert (0 == 116 json_object_set_new (row->fields, 117 field, 118 value)); 119 } 120 121 122 const char * 123 AG_row_string (AGRow *row, 124 const char *field) 125 { 126 return json_string_value (json_object_get (row->fields, 127 field)); 128 } 129 130 131 bool 132 AG_row_bool (AGRow *row, 133 const char *field) 134 { 135 return json_boolean_value (json_object_get (row->fields, 136 field)); 137 } 138 139 140 uint64_t 141 AG_row_uint (AGRow *row, 142 const char *field) 143 { 144 return (uint64_t) json_integer_value (json_object_get (row->fields, 145 field)); 146 } 147 148 149 json_t * 150 AG_row_json (AGRow *row, 151 const char *field) 152 { 153 return json_object_get (row->fields, 154 field); 155 } 156 157 158 void 159 AG_row_set_image (AGRow *row, 160 GdkPaintable *image) 161 { 162 g_clear_object (&row->image); 163 row->image = image; 164 } 165 166 167 GdkPaintable * 168 AG_row_image (AGRow *row) 169 { 170 return row->image; 171 } 172 173 174 GListStore * 175 AG_row_children (AGRow *row) 176 { 177 if (NULL == row->children) 178 row->children = g_list_store_new (AG_TYPE_ROW); 179 return row->children; 180 } 181 182 183 AGRow * 184 AG_row_of (gpointer item) 185 { 186 if (NULL == item) 187 return NULL; 188 if (GTK_IS_TREE_LIST_ROW (item)) 189 { 190 AGRow *row; 191 192 row = gtk_tree_list_row_get_item (GTK_TREE_LIST_ROW (item)); 193 if (NULL == row) 194 return NULL; 195 /* we hand out a borrowed reference; the tree list row holds one of 196 its own for as long as it exists */ 197 g_object_unref (row); 198 return AG_ROW (row); 199 } 200 return AG_ROW (item); 201 } 202 203 204 GListStore * 205 AG_list_of (GtkBuilder *builder, 206 const char *name) 207 { 208 GObject *model; 209 210 model = gtk_builder_get_object (builder, 211 name); 212 if (NULL == model) 213 { 214 GNUNET_break (0); 215 return NULL; 216 } 217 return G_LIST_STORE (model); 218 } 219 220 221 GListStore * 222 AG_list (const char *name) 223 { 224 GObject *model; 225 226 model = GCG_get_main_window_object (name); 227 if (NULL == model) 228 { 229 GNUNET_break (0); 230 return NULL; 231 } 232 return G_LIST_STORE (model); 233 } 234 235 236 void 237 AG_list_clear (const char *name) 238 { 239 GListStore *list = AG_list (name); 240 241 if (NULL == list) 242 return; 243 g_list_store_remove_all (list); 244 } 245 246 247 void 248 AG_list_add_row (GListStore *list, 249 AGRow *row) 250 { 251 g_list_store_append (list, 252 row); 253 g_object_unref (row); 254 } 255 256 257 AGRow * 258 AG_list_append (GListStore *list, 259 json_t *fields) 260 { 261 AGRow *row = AG_row_new (fields); 262 263 g_list_store_append (list, 264 row); 265 g_object_unref (row); 266 return row; 267 } 268 269 270 AGRow * 271 AG_list_add (const char *name, 272 json_t *fields) 273 { 274 GListStore *list = AG_list (name); 275 276 if (NULL == list) 277 { 278 json_decref (fields); 279 return NULL; 280 } 281 return AG_list_append (list, 282 fields); 283 } 284 285 286 AGRow * 287 AG_selected (GtkSingleSelection *selection) 288 { 289 return AG_row_of (gtk_single_selection_get_selected_item (selection)); 290 } 291 292 293 /** 294 * Return the selection called @a name of the main window. 295 * 296 * @param name name of the selection in the UI file 297 * @return NULL on error 298 */ 299 static GtkSingleSelection * 300 main_window_selection (const char *name) 301 { 302 GObject *sel; 303 304 sel = GCG_get_main_window_object (name); 305 if (NULL == sel) 306 { 307 GNUNET_break (0); 308 return NULL; 309 } 310 return GTK_SINGLE_SELECTION (sel); 311 } 312 313 314 AGRow * 315 AG_selected_row (const char *name) 316 { 317 GtkSingleSelection *sel = main_window_selection (name); 318 319 if (NULL == sel) 320 return NULL; 321 return AG_selected (sel); 322 } 323 324 325 void 326 AG_select_row (const char *name, 327 guint position) 328 { 329 GtkSingleSelection *sel = main_window_selection (name); 330 331 if (NULL == sel) 332 return; 333 gtk_selection_model_select_item (GTK_SELECTION_MODEL (sel), 334 position, 335 TRUE); 336 } 337 338 339 /** 340 * Search the descendants of @a widget for the cell of a row that covers 341 * @a y in the coordinates of @a view. 342 * 343 * @param widget widget to search below 344 * @param view the column view @a y refers to 345 * @param y vertical position to look for 346 * @return NULL if no cell of any row covers @a y 347 */ 348 static AGRow * 349 find_row_at (GtkWidget *widget, 350 GtkWidget *view, 351 double y) 352 { 353 for (GtkWidget *child = gtk_widget_get_first_child (widget); 354 NULL != child; 355 child = gtk_widget_get_next_sibling (child)) 356 { 357 AGRow *row; 358 GtkWidget *cell; 359 graphene_rect_t bounds; 360 361 row = g_object_get_data (G_OBJECT (child), 362 ROW_KEY); 363 if (NULL == row) 364 { 365 row = find_row_at (child, 366 view, 367 y); 368 if (NULL != row) 369 return row; 370 continue; 371 } 372 /* our widget does not fill the cell it was put into, so we measure 373 the cell, which does tile the list without gaps */ 374 cell = gtk_widget_get_parent (child); 375 if (! gtk_widget_compute_bounds ((NULL != cell) ? cell : child, 376 view, 377 &bounds)) 378 continue; 379 if ( (y >= bounds.origin.y) && 380 (y < bounds.origin.y + bounds.size.height) ) 381 return row; 382 } 383 return NULL; 384 } 385 386 387 AGRow * 388 AG_row_at (GtkWidget *view, 389 double y) 390 { 391 return find_row_at (view, 392 view, 393 y); 394 } 395 396 397 /** 398 * Return the @a field of @a row as a number, mapping true to 1. 399 * 400 * @param row the row to inspect 401 * @param field name of the field 402 * @return 0 if there is no such field 403 */ 404 static json_int_t 405 row_number (AGRow *row, 406 const char *field) 407 { 408 json_t *val = AG_row_json (row, 409 field); 410 411 if (json_is_boolean (val)) 412 return json_is_true (val) ? 1 : 0; 413 return json_integer_value (val); 414 } 415 416 417 /** 418 * Compare two rows by the field named in @a cls. 419 * 420 * @param a first row 421 * @param b second row 422 * @param cls the `struct AG_SortSpec` of the column being sorted 423 * @return -1, 0 or 1 424 */ 425 static gint 426 compare_rows (gconstpointer a, 427 gconstpointer b, 428 gpointer cls) 429 { 430 const struct AG_SortSpec *spec = cls; 431 AGRow *ra = AG_row_of ((gpointer) a); 432 AGRow *rb = AG_row_of ((gpointer) b); 433 434 if (spec->numeric) 435 { 436 json_int_t na = row_number (ra, 437 spec->field); 438 json_int_t nb = row_number (rb, 439 spec->field); 440 441 if (na < nb) 442 return -1; 443 if (na > nb) 444 return 1; 445 return 0; 446 } 447 { 448 const char *sa = AG_row_string (ra, 449 spec->field); 450 const char *sb = AG_row_string (rb, 451 spec->field); 452 453 if (NULL == sa) 454 sa = ""; 455 if (NULL == sb) 456 sb = ""; 457 return g_utf8_collate (sa, 458 sb); 459 } 460 } 461 462 463 void 464 AG_list_make_sortable (GtkBuilder *builder, 465 const char *view, 466 const char *sort_model, 467 const struct AG_SortSpec *specs) 468 { 469 GtkColumnView *cv; 470 GtkSortListModel *slm; 471 472 cv = GTK_COLUMN_VIEW (gtk_builder_get_object (builder, 473 view)); 474 slm = GTK_SORT_LIST_MODEL (gtk_builder_get_object (builder, 475 sort_model)); 476 if ( (NULL == cv) || 477 (NULL == slm) ) 478 { 479 GNUNET_break (0); 480 return; 481 } 482 for (unsigned int i = 0; NULL != specs[i].column; i++) 483 { 484 GtkColumnViewColumn *col; 485 GtkSorter *sorter; 486 487 col = GTK_COLUMN_VIEW_COLUMN (gtk_builder_get_object (builder, 488 specs[i].column)); 489 if (NULL == col) 490 { 491 GNUNET_break (0); 492 continue; 493 } 494 sorter = GTK_SORTER (gtk_custom_sorter_new (&compare_rows, 495 (gpointer) &specs[i], 496 NULL)); 497 gtk_column_view_column_set_sorter (col, 498 sorter); 499 g_object_unref (sorter); 500 } 501 /* clicking a column header now sorts the rows the view shows */ 502 gtk_sort_list_model_set_sorter (slm, 503 gtk_column_view_get_sorter (cv)); 504 } 505 506 507 /** 508 * Return the rows nested under @a item, if any. 509 * 510 * @param item the parent row 511 * @param cls NULL 512 * @return NULL if @a item has no children 513 */ 514 static GListModel * 515 create_child_model (gpointer item, 516 gpointer cls) 517 { 518 AGRow *row = AG_ROW (item); 519 520 (void) cls; 521 if (NULL == row->children) 522 return NULL; 523 return G_LIST_MODEL (g_object_ref (row->children)); 524 } 525 526 527 /** 528 * Columns of the list of authentication methods that can be sorted by 529 * clicking their header. 530 */ 531 static const struct AG_SortSpec auth_method_sorts[] = { 532 { .column = "auth_method_type", 533 .field = "type" }, 534 { .column = NULL } 535 }; 536 537 538 /** 539 * Columns of the list of secrets that can be sorted by clicking their 540 * header. 541 */ 542 static const struct AG_SortSpec secret_sorts[] = { 543 { .column = "secret_name_column", 544 .field = "secret_name" }, 545 { .column = "backup_time_column", 546 .field = "date", 547 .numeric = true }, 548 { .column = NULL } 549 }; 550 551 552 void 553 AG_lists_setup (void) 554 { 555 GtkBuilder *builder = ANASTASIS_GTK_main_loop_get_builder (AG_ml); 556 557 /* the policy review shows a tree, which the UI file cannot express */ 558 { 559 GListStore *policies; 560 GtkTreeListModel *tlm; 561 GtkSingleSelection *sel; 562 563 policies = AG_list ("policy_review_treestore"); 564 sel = main_window_selection ("anastasis_gtk_review_policy_selection"); 565 if ( (NULL == policies) || 566 (NULL == sel) ) 567 { 568 GNUNET_break (0); 569 return; 570 } 571 tlm = gtk_tree_list_model_new (G_LIST_MODEL (g_object_ref (policies)), 572 FALSE, /* we want the GtkTreeListRows */ 573 TRUE, /* expand all */ 574 &create_child_model, 575 NULL, 576 NULL); 577 gtk_single_selection_set_model (sel, 578 G_LIST_MODEL (tlm)); 579 g_object_unref (tlm); 580 } 581 AG_list_make_sortable (builder, 582 "anastasis_gtk_authentication_methods_list", 583 "authentication_methods_sorted", 584 auth_method_sorts); 585 AG_list_make_sortable (builder, 586 "anastasis_secret_selection_treeview", 587 "secret_selection_sorted", 588 secret_sorts); 589 } 590 591 592 void 593 AG_cell_set_row (GtkWidget *widget, 594 AGRow *row) 595 { 596 g_object_set_data (G_OBJECT (widget), 597 ROW_KEY, 598 row); 599 } 600 601 602 AGRow * 603 AG_cell_get_row (GtkWidget *widget) 604 { 605 return g_object_get_data (G_OBJECT (widget), 606 ROW_KEY); 607 } 608 609 610 /* end of anastasis-gtk_rows.c */