Annotation of gnutrition/gui.c, revision 1.1
1.1 ! asm 1: // SPDX-License-Identifier: GPL-3.0-or-later
! 2: /*
! 3: * $Id$
! 4: *
! 5: * gui.c - GTK 3 user interface for GNUtrition
! 6: *
! 7: * Copyright (C) 2026 Free Software Foundation, Inc.
! 8: *
! 9: * Author: Jason Self <jself@gnu.org>
! 10: * Anton McClure <asm@gnu.org>
! 11: */
! 12:
! 13: #ifdef HAVE_CONFIG_H
! 14: #include <config.h>
! 15: #endif
! 16:
! 17: #include "gui.h"
! 18: #include "budget.h"
! 19: #include "db.h"
! 20: #include "dbus.h"
! 21: #include "log.h"
! 22: #include "i18n.h"
! 23:
! 24: #include <gtk/gtk.h>
! 25: #include <stdio.h>
! 26: #include <stdlib.h>
! 27: #include <string.h>
! 28: #include <time.h>
! 29:
! 30: #define PROGRAM_NAME "gnutrition"
! 31:
! 32: /* G_APPLICATION_DEFAULT_FLAGS was added in GLib 2.74. */
! 33: #if !GLIB_CHECK_VERSION(2, 74, 0)
! 34: #define G_APPLICATION_DEFAULT_FLAGS G_APPLICATION_FLAGS_NONE
! 35: #endif
! 36:
! 37: /* Application state shared across callbacks. */
! 38: struct gui_state
! 39: {
! 40: sqlite3 *food_db;
! 41: sqlite3 *log_db;
! 42: int calories;
! 43:
! 44: /* Main window widgets. */
! 45: GtkWidget *window;
! 46: GtkWidget *dashboard_box;
! 47: GtkWidget *log_list_box;
! 48: GtkWidget *budget_label;
! 49: GtkWidget *log_frame;
! 50:
! 51: /* Currently displayed log date (ISO 8601). */
! 52: char log_date[11];
! 53:
! 54: /* Progress bars for budget dashboard. */
! 55: GtkWidget *pb_vegetables;
! 56: GtkWidget *pb_fruits;
! 57: GtkWidget *pb_grains;
! 58: GtkWidget *pb_dairy;
! 59: GtkWidget *pb_protein;
! 60: GtkWidget *pb_oils;
! 61:
! 62: /* D-Bus service. */
! 63: struct dbus_context dbus_ctx;
! 64: guint dbus_owner_id;
! 65: };
! 66:
! 67: /* Return today's date as YYYY-MM-DD in a static buffer. */
! 68: static const char *
! 69: today_date (void)
! 70: {
! 71: static char buf[11];
! 72: time_t now;
! 73: struct tm *tm;
! 74:
! 75: now = time (NULL);
! 76: tm = localtime (&now);
! 77: strftime (buf, sizeof buf, "%Y-%m-%d", tm);
! 78: return buf;
! 79: }
! 80:
! 81: /* Format an ISO 8601 date (YYYY-MM-DD) for display using the
! 82: locale's preferred date representation. Returns a pointer to a
! 83: static buffer; not reentrant. */
! 84: static const char *
! 85: format_date (const char *iso_date)
! 86: {
! 87: static char buf[64];
! 88: struct tm tm;
! 89:
! 90: memset (&tm, 0, sizeof tm);
! 91: if (sscanf (iso_date, "%d-%d-%d",
! 92: &tm.tm_year, &tm.tm_mon, &tm.tm_mday) != 3)
! 93: return iso_date;
! 94: tm.tm_year -= 1900;
! 95: tm.tm_mon -= 1;
! 96: if (strftime (buf, sizeof buf, "%x", &tm) == 0)
! 97: return iso_date;
! 98: return buf;
! 99: }
! 100:
! 101: /* Update a single progress bar for a budget category. */
! 102: static void
! 103: update_progress_bar (GtkWidget *pb, double budget, double consumed,
! 104: const char *label)
! 105: {
! 106: double fraction;
! 107: char text[128];
! 108:
! 109: if (budget > 0.0)
! 110: fraction = consumed / budget;
! 111: else
! 112: fraction = 0.0;
! 113: if (fraction > 1.0)
! 114: fraction = 1.0;
! 115:
! 116: gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (pb), fraction);
! 117: snprintf (text, sizeof text, "%s: %.1f / %.1f", label, consumed, budget);
! 118: gtk_progress_bar_set_text (GTK_PROGRESS_BAR (pb), text);
! 119: gtk_progress_bar_set_show_text (GTK_PROGRESS_BAR (pb), TRUE);
! 120: }
! 121:
! 122: /* Refresh the budget dashboard progress bars for the currently
! 123: displayed log date. */
! 124: static void
! 125: refresh_dashboard (struct gui_state *state)
! 126: {
! 127: struct daily_budget budget;
! 128: struct daily_budget consumed;
! 129: struct log_list entries;
! 130: const char *date;
! 131:
! 132: budget = budget_for_calories (state->calories);
! 133: memset (&consumed, 0, sizeof consumed);
! 134: date = state->log_date;
! 135:
! 136: if (log_get_day (state->log_db, date, &entries) == 0)
! 137: {
! 138: size_t i;
! 139: for (i = 0; i < entries.count; i++)
! 140: {
! 141: struct fped_entry fped;
! 142: if (db_get_fped (state->food_db, entries.items[i].food_code,
! 143: &fped) == 0)
! 144: {
! 145: double s = entries.items[i].servings;
! 146: consumed.vegetables += fped.vegetables * s;
! 147: consumed.fruits += fped.fruits * s;
! 148: consumed.grains += fped.grains * s;
! 149: consumed.dairy += fped.dairy * s;
! 150: consumed.protein += fped.protein * s;
! 151: consumed.oils += fped.oils * s;
! 152: }
! 153: }
! 154: log_list_free (&entries);
! 155: }
! 156:
! 157: update_progress_bar (state->pb_vegetables, budget.vegetables,
! 158: consumed.vegetables, _("Vegetables (cup-eq)"));
! 159: update_progress_bar (state->pb_fruits, budget.fruits,
! 160: consumed.fruits, _("Fruits (cup-eq)"));
! 161: update_progress_bar (state->pb_grains, budget.grains,
! 162: consumed.grains, _("Grains (oz-eq)"));
! 163: update_progress_bar (state->pb_dairy, budget.dairy,
! 164: consumed.dairy, _("Dairy (cup-eq)"));
! 165: update_progress_bar (state->pb_protein, budget.protein,
! 166: consumed.protein, _("Protein (oz-eq)"));
! 167: update_progress_bar (state->pb_oils, budget.oils,
! 168: consumed.oils, _("Oils (g)"));
! 169:
! 170: /* Update the budget label to reflect the current calorie target
! 171: and the date being displayed. */
! 172: if (state->budget_label)
! 173: {
! 174: char budget_text[192];
! 175: snprintf (budget_text, sizeof budget_text,
! 176: _("USDA Healthy US-Style Eating Pattern (%d kcal) - %s"),
! 177: state->calories, format_date (state->log_date));
! 178: gtk_label_set_text (GTK_LABEL (state->budget_label), budget_text);
! 179: }
! 180: }
! 181:
! 182: /* Forward declarations for mutual recursion. */
! 183: static void refresh_log_list (struct gui_state *state);
! 184: static void refresh_all (struct gui_state *state);
! 185:
! 186: /* Callback for the "Delete" button on a log entry row. */
! 187: static void
! 188: on_log_delete_clicked (GtkButton *button, gpointer user_data)
! 189: {
! 190: struct gui_state *state = user_data;
! 191: int entry_id;
! 192: GtkWidget *confirm;
! 193: int response;
! 194:
! 195: (void) button;
! 196:
! 197: entry_id = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button),
! 198: "entry-id"));
! 199:
! 200: confirm = gtk_message_dialog_new (GTK_WINDOW (state->window),
! 201: GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
! 202: GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO,
! 203: _("Delete this log entry?"));
! 204: gtk_window_set_title (GTK_WINDOW (confirm), _("Confirm Delete"));
! 205: response = gtk_dialog_run (GTK_DIALOG (confirm));
! 206: gtk_widget_destroy (confirm);
! 207:
! 208: if (response == GTK_RESPONSE_YES)
! 209: {
! 210: log_delete (state->log_db, entry_id);
! 211: refresh_all (state);
! 212: }
! 213: }
! 214:
! 215: /* Callback for the Edit dialog "Save" button. */
! 216: static void
! 217: on_edit_save_clicked (GtkDialog *dialog, gint response_id,
! 218: gpointer user_data)
! 219: {
! 220: struct gui_state *state = user_data;
! 221:
! 222: if (response_id == GTK_RESPONSE_OK)
! 223: {
! 224: GtkWidget *spin;
! 225: GtkWidget *calendar;
! 226: int entry_id;
! 227: double quantity;
! 228: char date[11];
! 229: guint year, month, day;
! 230:
! 231: spin = g_object_get_data (G_OBJECT (dialog), "spin-quantity");
! 232: calendar = g_object_get_data (G_OBJECT (dialog), "calendar");
! 233: entry_id = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (dialog),
! 234: "entry-id"));
! 235: quantity = gtk_spin_button_get_value (GTK_SPIN_BUTTON (spin));
! 236:
! 237: gtk_calendar_get_date (GTK_CALENDAR (calendar), &year, &month, &day);
! 238: snprintf (date, sizeof date, "%04u-%02u-%02u", year, month + 1, day);
! 239:
! 240: log_update (state->log_db, entry_id, date, quantity);
! 241: refresh_all (state);
! 242: }
! 243:
! 244: gtk_widget_destroy (GTK_WIDGET (dialog));
! 245: }
! 246:
! 247: /* Callback for the "Edit" button on a log entry row. */
! 248: static void
! 249: on_log_edit_clicked (GtkButton *button, gpointer user_data)
! 250: {
! 251: struct gui_state *state = user_data;
! 252: int entry_id;
! 253: double servings;
! 254: const char *entry_date;
! 255: const char *description;
! 256: GtkWidget *dialog;
! 257: GtkWidget *content;
! 258: GtkWidget *spin;
! 259: GtkWidget *calendar;
! 260: GtkWidget *lbl;
! 261: struct tm tm;
! 262:
! 263: entry_id = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button),
! 264: "entry-id"));
! 265: {
! 266: double *srv_ptr = g_object_get_data (G_OBJECT (button),
! 267: "entry-servings");
! 268: servings = srv_ptr ? *srv_ptr : 1.0;
! 269: }
! 270: entry_date = g_object_get_data (G_OBJECT (button), "entry-date");
! 271: description = g_object_get_data (G_OBJECT (button), "entry-desc");
! 272:
! 273: dialog = gtk_dialog_new_with_buttons (
! 274: description ? description : _("Edit Log Entry"),
! 275: GTK_WINDOW (state->window),
! 276: GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
! 277: _("Save"), GTK_RESPONSE_OK,
! 278: _("Cancel"), GTK_RESPONSE_CANCEL,
! 279: NULL);
! 280: gtk_window_set_default_size (GTK_WINDOW (dialog), 400, 350);
! 281:
! 282: content = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
! 283:
! 284: /* Quantity spinner. */
! 285: {
! 286: GtkWidget *hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
! 287: lbl = gtk_label_new (_("Servings:"));
! 288: spin = gtk_spin_button_new_with_range (0.1, 100.0, 0.5);
! 289: gtk_spin_button_set_value (GTK_SPIN_BUTTON (spin), servings);
! 290: gtk_box_pack_start (GTK_BOX (hbox), lbl, FALSE, FALSE, 6);
! 291: gtk_box_pack_start (GTK_BOX (hbox), spin, FALSE, FALSE, 6);
! 292: gtk_box_pack_start (GTK_BOX (content), hbox, FALSE, FALSE, 6);
! 293: }
! 294:
! 295: /* Date selector. */
! 296: {
! 297: GtkWidget *date_frame = gtk_frame_new (_("Date"));
! 298: calendar = gtk_calendar_new ();
! 299:
! 300: /* Set the calendar to the entry's date. */
! 301: memset (&tm, 0, sizeof tm);
! 302: if (entry_date
! 303: && sscanf (entry_date, "%d-%d-%d",
! 304: &tm.tm_year, &tm.tm_mon, &tm.tm_mday) == 3)
! 305: {
! 306: gtk_calendar_select_month (GTK_CALENDAR (calendar),
! 307: (guint) (tm.tm_mon - 1),
! 308: (guint) tm.tm_year);
! 309: gtk_calendar_select_day (GTK_CALENDAR (calendar),
! 310: (guint) tm.tm_mday);
! 311: }
! 312:
! 313: gtk_container_add (GTK_CONTAINER (date_frame), calendar);
! 314: gtk_box_pack_start (GTK_BOX (content), date_frame, FALSE, FALSE, 6);
! 315: }
! 316:
! 317: g_object_set_data (G_OBJECT (dialog), "spin-quantity", spin);
! 318: g_object_set_data (G_OBJECT (dialog), "calendar", calendar);
! 319: g_object_set_data (G_OBJECT (dialog), "entry-id",
! 320: GINT_TO_POINTER (entry_id));
! 321:
! 322: g_signal_connect (dialog, "response",
! 323: G_CALLBACK (on_edit_save_clicked), state);
! 324: gtk_widget_show_all (dialog);
! 325: }
! 326:
! 327: /* Refresh the food log list for the currently selected date. */
! 328: static void
! 329: refresh_log_list (struct gui_state *state)
! 330: {
! 331: struct log_list entries;
! 332: const char *date;
! 333: GList *children;
! 334: GList *iter;
! 335: char frame_title[128];
! 336:
! 337: /* Remove existing rows. */
! 338: children = gtk_container_get_children (GTK_CONTAINER (state->log_list_box));
! 339: for (iter = children; iter; iter = iter->next)
! 340: gtk_widget_destroy (GTK_WIDGET (iter->data));
! 341: g_list_free (children);
! 342:
! 343: date = state->log_date;
! 344:
! 345: /* Update the log frame title to show the selected date. */
! 346: if (state->log_frame)
! 347: {
! 348: if (strcmp (date, today_date ()) == 0)
! 349: snprintf (frame_title, sizeof frame_title,
! 350: _("Food Log - %s (Today)"), format_date (date));
! 351: else
! 352: snprintf (frame_title, sizeof frame_title,
! 353: _("Food Log - %s"), format_date (date));
! 354: gtk_frame_set_label (GTK_FRAME (state->log_frame), frame_title);
! 355: }
! 356:
! 357: if (log_get_day (state->log_db, date, &entries) == 0)
! 358: {
! 359: size_t i;
! 360: if (entries.count == 0)
! 361: {
! 362: GtkWidget *label = gtk_label_new (_("No entries for this date."));
! 363: gtk_widget_set_halign (label, GTK_ALIGN_START);
! 364: gtk_container_add (GTK_CONTAINER (state->log_list_box), label);
! 365: }
! 366: else
! 367: {
! 368: for (i = 0; i < entries.count; i++)
! 369: {
! 370: char row_text[512];
! 371: GtkWidget *hbox;
! 372: GtkWidget *label;
! 373: GtkWidget *edit_btn;
! 374: GtkWidget *del_btn;
! 375: double *srv_copy;
! 376:
! 377: hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
! 378:
! 379: snprintf (row_text, sizeof row_text,
! 380: "%d - %s (%.1f %s)",
! 381: entries.items[i].food_code,
! 382: entries.items[i].description,
! 383: entries.items[i].servings,
! 384: _("servings"));
! 385: label = gtk_label_new (row_text);
! 386: gtk_widget_set_halign (label, GTK_ALIGN_START);
! 387: gtk_label_set_xalign (GTK_LABEL (label), 0.0);
! 388: gtk_box_pack_start (GTK_BOX (hbox), label, TRUE, TRUE, 0);
! 389:
! 390: edit_btn = gtk_button_new_with_label (_("Edit"));
! 391: del_btn = gtk_button_new_with_label (_("Delete"));
! 392:
! 393: /* Store entry metadata on the buttons. */
! 394: g_object_set_data (G_OBJECT (edit_btn), "entry-id",
! 395: GINT_TO_POINTER (entries.items[i].id));
! 396: srv_copy = g_new (double, 1);
! 397: *srv_copy = entries.items[i].servings;
! 398: g_object_set_data_full (G_OBJECT (edit_btn),
! 399: "entry-servings",
! 400: srv_copy, g_free);
! 401: g_object_set_data_full (G_OBJECT (edit_btn), "entry-date",
! 402: g_strdup (entries.items[i].date),
! 403: g_free);
! 404: g_object_set_data_full (G_OBJECT (edit_btn), "entry-desc",
! 405: g_strdup (entries.items[i].description),
! 406: g_free);
! 407:
! 408: g_object_set_data (G_OBJECT (del_btn), "entry-id",
! 409: GINT_TO_POINTER (entries.items[i].id));
! 410:
! 411: g_signal_connect (edit_btn, "clicked",
! 412: G_CALLBACK (on_log_edit_clicked), state);
! 413: g_signal_connect (del_btn, "clicked",
! 414: G_CALLBACK (on_log_delete_clicked), state);
! 415:
! 416: gtk_box_pack_end (GTK_BOX (hbox), del_btn, FALSE, FALSE, 0);
! 417: gtk_box_pack_end (GTK_BOX (hbox), edit_btn, FALSE, FALSE, 0);
! 418:
! 419: gtk_container_add (GTK_CONTAINER (state->log_list_box), hbox);
! 420: }
! 421: }
! 422: log_list_free (&entries);
! 423: }
! 424:
! 425: gtk_widget_show_all (state->log_list_box);
! 426: }
! 427:
! 428: /* Refresh both the dashboard and log list. */
! 429: static void
! 430: refresh_all (struct gui_state *state)
! 431: {
! 432: refresh_dashboard (state);
! 433: refresh_log_list (state);
! 434: }
! 435:
! 436: /* --- Search Dialog --- */
! 437:
! 438: enum
! 439: {
! 440: COL_FOOD_CODE,
! 441: COL_DESCRIPTION,
! 442: NUM_COLS
! 443: };
! 444:
! 445: /* Callback for food detail dialog "Add" button. */
! 446: static void
! 447: on_food_add_clicked (GtkDialog *dialog, gint response_id,
! 448: gpointer user_data)
! 449: {
! 450: struct gui_state *state = user_data;
! 451:
! 452: if (response_id == GTK_RESPONSE_OK)
! 453: {
! 454: GtkWidget *content;
! 455: GList *children;
! 456: GtkWidget *spin;
! 457: GtkWidget *calendar;
! 458: int food_code;
! 459: double quantity;
! 460: const char *description;
! 461: char date[11];
! 462: guint year, month, day;
! 463:
! 464: content = gtk_dialog_get_content_area (dialog);
! 465: children = gtk_container_get_children (GTK_CONTAINER (content));
! 466:
! 467: /* The spin button is attached as object data on the dialog. */
! 468: spin = g_object_get_data (G_OBJECT (dialog), "spin-quantity");
! 469: calendar = g_object_get_data (G_OBJECT (dialog), "calendar");
! 470: food_code = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (dialog),
! 471: "food-code"));
! 472: description = g_object_get_data (G_OBJECT (dialog), "food-desc");
! 473: quantity = gtk_spin_button_get_value (GTK_SPIN_BUTTON (spin));
! 474:
! 475: gtk_calendar_get_date (GTK_CALENDAR (calendar), &year, &month, &day);
! 476: snprintf (date, sizeof date, "%04u-%02u-%02u", year, month + 1, day);
! 477:
! 478: log_add (state->log_db, food_code, description, date, quantity);
! 479: refresh_all (state);
! 480:
! 481: g_list_free (children);
! 482: }
! 483:
! 484: gtk_widget_destroy (GTK_WIDGET (dialog));
! 485: }
! 486:
! 487: /* Populate the nutrient grid with values scaled by SERVINGS. */
! 488: static void
! 489: populate_nutrient_grid (GtkWidget *grid, sqlite3 *food_db,
! 490: int food_code, double servings)
! 491: {
! 492: GtkWidget *lbl;
! 493: struct nutrient_list nutrients;
! 494: struct fped_entry fped;
! 495: int row;
! 496: size_t i;
! 497: GList *children, *iter;
! 498:
! 499: /* Clear existing grid content. */
! 500: children = gtk_container_get_children (GTK_CONTAINER (grid));
! 501: for (iter = children; iter; iter = iter->next)
! 502: gtk_widget_destroy (GTK_WIDGET (iter->data));
! 503: g_list_free (children);
! 504:
! 505: row = 0;
! 506: if (db_get_nutrients (food_db, food_code, &nutrients) == 0)
! 507: {
! 508: lbl = gtk_label_new (NULL);
! 509: gtk_label_set_markup (GTK_LABEL (lbl), _("<b>Nutrient</b>"));
! 510: gtk_widget_set_halign (lbl, GTK_ALIGN_START);
! 511: gtk_grid_attach (GTK_GRID (grid), lbl, 0, row, 1, 1);
! 512: lbl = gtk_label_new (NULL);
! 513: gtk_label_set_markup (GTK_LABEL (lbl), _("<b>Value</b>"));
! 514: gtk_widget_set_halign (lbl, GTK_ALIGN_END);
! 515: gtk_grid_attach (GTK_GRID (grid), lbl, 1, row, 1, 1);
! 516: row++;
! 517:
! 518: for (i = 0; i < nutrients.count; i++)
! 519: {
! 520: char val_buf[32];
! 521: lbl = gtk_label_new (nutrients.items[i].name);
! 522: gtk_widget_set_halign (lbl, GTK_ALIGN_START);
! 523: gtk_grid_attach (GTK_GRID (grid), lbl, 0, row, 1, 1);
! 524: snprintf (val_buf, sizeof val_buf, "%.2f",
! 525: nutrients.items[i].value * servings);
! 526: lbl = gtk_label_new (val_buf);
! 527: gtk_widget_set_halign (lbl, GTK_ALIGN_END);
! 528: gtk_grid_attach (GTK_GRID (grid), lbl, 1, row, 1, 1);
! 529: row++;
! 530: }
! 531: nutrient_list_free (&nutrients);
! 532: }
! 533:
! 534: /* FPED info. */
! 535: if (db_get_fped (food_db, food_code, &fped) == 0)
! 536: {
! 537: char val_buf[32];
! 538: row++;
! 539: lbl = gtk_label_new (NULL);
! 540: gtk_label_set_markup (GTK_LABEL (lbl),
! 541: _("<b>Food Pattern Equivalents</b>"));
! 542: gtk_widget_set_halign (lbl, GTK_ALIGN_START);
! 543: gtk_grid_attach (GTK_GRID (grid), lbl, 0, row, 2, 1);
! 544: row++;
! 545:
! 546: lbl = gtk_label_new (_("Vegetables (cup-eq)"));
! 547: gtk_widget_set_halign (lbl, GTK_ALIGN_START);
! 548: gtk_grid_attach (GTK_GRID (grid), lbl, 0, row, 1, 1);
! 549: snprintf (val_buf, sizeof val_buf, "%.2f",
! 550: fped.vegetables * servings);
! 551: lbl = gtk_label_new (val_buf);
! 552: gtk_widget_set_halign (lbl, GTK_ALIGN_END);
! 553: gtk_grid_attach (GTK_GRID (grid), lbl, 1, row, 1, 1);
! 554: row++;
! 555:
! 556: lbl = gtk_label_new (_("Fruits (cup-eq)"));
! 557: gtk_widget_set_halign (lbl, GTK_ALIGN_START);
! 558: gtk_grid_attach (GTK_GRID (grid), lbl, 0, row, 1, 1);
! 559: snprintf (val_buf, sizeof val_buf, "%.2f", fped.fruits * servings);
! 560: lbl = gtk_label_new (val_buf);
! 561: gtk_widget_set_halign (lbl, GTK_ALIGN_END);
! 562: gtk_grid_attach (GTK_GRID (grid), lbl, 1, row, 1, 1);
! 563: row++;
! 564:
! 565: lbl = gtk_label_new (_("Grains (oz-eq)"));
! 566: gtk_widget_set_halign (lbl, GTK_ALIGN_START);
! 567: gtk_grid_attach (GTK_GRID (grid), lbl, 0, row, 1, 1);
! 568: snprintf (val_buf, sizeof val_buf, "%.2f", fped.grains * servings);
! 569: lbl = gtk_label_new (val_buf);
! 570: gtk_widget_set_halign (lbl, GTK_ALIGN_END);
! 571: gtk_grid_attach (GTK_GRID (grid), lbl, 1, row, 1, 1);
! 572: row++;
! 573:
! 574: lbl = gtk_label_new (_("Dairy (cup-eq)"));
! 575: gtk_widget_set_halign (lbl, GTK_ALIGN_START);
! 576: gtk_grid_attach (GTK_GRID (grid), lbl, 0, row, 1, 1);
! 577: snprintf (val_buf, sizeof val_buf, "%.2f", fped.dairy * servings);
! 578: lbl = gtk_label_new (val_buf);
! 579: gtk_widget_set_halign (lbl, GTK_ALIGN_END);
! 580: gtk_grid_attach (GTK_GRID (grid), lbl, 1, row, 1, 1);
! 581: row++;
! 582:
! 583: lbl = gtk_label_new (_("Protein (oz-eq)"));
! 584: gtk_widget_set_halign (lbl, GTK_ALIGN_START);
! 585: gtk_grid_attach (GTK_GRID (grid), lbl, 0, row, 1, 1);
! 586: snprintf (val_buf, sizeof val_buf, "%.2f", fped.protein * servings);
! 587: lbl = gtk_label_new (val_buf);
! 588: gtk_widget_set_halign (lbl, GTK_ALIGN_END);
! 589: gtk_grid_attach (GTK_GRID (grid), lbl, 1, row, 1, 1);
! 590: row++;
! 591:
! 592: lbl = gtk_label_new (_("Oils (g)"));
! 593: gtk_widget_set_halign (lbl, GTK_ALIGN_START);
! 594: gtk_grid_attach (GTK_GRID (grid), lbl, 0, row, 1, 1);
! 595: snprintf (val_buf, sizeof val_buf, "%.2f", fped.oils * servings);
! 596: lbl = gtk_label_new (val_buf);
! 597: gtk_widget_set_halign (lbl, GTK_ALIGN_END);
! 598: gtk_grid_attach (GTK_GRID (grid), lbl, 1, row, 1, 1);
! 599: }
! 600:
! 601: gtk_widget_show_all (grid);
! 602: }
! 603:
! 604: /* Callback when the servings spin button changes in the food detail
! 605: dialog. Rebuilds the nutrient grid to reflect the new quantity. */
! 606: static void
! 607: on_detail_quantity_changed (GtkSpinButton *spin, gpointer user_data)
! 608: {
! 609: GtkWidget *dialog = GTK_WIDGET (user_data);
! 610: GtkWidget *grid;
! 611: sqlite3 *food_db;
! 612: int food_code;
! 613: double servings;
! 614:
! 615: grid = g_object_get_data (G_OBJECT (dialog), "nutrient-grid");
! 616: food_db = g_object_get_data (G_OBJECT (dialog), "food-db");
! 617: food_code = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (dialog),
! 618: "food-code"));
! 619: servings = gtk_spin_button_get_value (spin);
! 620:
! 621: populate_nutrient_grid (grid, food_db, food_code, servings);
! 622: }
! 623:
! 624: /* Show a food detail dialog with nutrient info and an Add button. */
! 625: static void
! 626: show_food_detail (struct gui_state *state, int food_code,
! 627: const char *description)
! 628: {
! 629: GtkWidget *dialog;
! 630: GtkWidget *content;
! 631: GtkWidget *scroll;
! 632: GtkWidget *grid;
! 633: GtkWidget *spin;
! 634: GtkWidget *calendar;
! 635: GtkWidget *lbl;
! 636:
! 637: dialog = gtk_dialog_new_with_buttons (description,
! 638: GTK_WINDOW (state->window),
! 639: GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
! 640: _("Add to Log"), GTK_RESPONSE_OK,
! 641: _("Cancel"), GTK_RESPONSE_CANCEL,
! 642: NULL);
! 643: gtk_window_set_default_size (GTK_WINDOW (dialog), 500, 500);
! 644:
! 645: content = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
! 646:
! 647: /* Quantity spinner and date picker. */
! 648: {
! 649: GtkWidget *hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
! 650: lbl = gtk_label_new (_("Servings:"));
! 651: spin = gtk_spin_button_new_with_range (0.1, 100.0, 0.5);
! 652: gtk_spin_button_set_value (GTK_SPIN_BUTTON (spin), 1.0);
! 653: gtk_box_pack_start (GTK_BOX (hbox), lbl, FALSE, FALSE, 6);
! 654: gtk_box_pack_start (GTK_BOX (hbox), spin, FALSE, FALSE, 6);
! 655: gtk_box_pack_start (GTK_BOX (content), hbox, FALSE, FALSE, 6);
! 656: }
! 657:
! 658: /* Date selector. */
! 659: {
! 660: GtkWidget *date_frame = gtk_frame_new (_("Log Date"));
! 661: calendar = gtk_calendar_new ();
! 662: gtk_container_add (GTK_CONTAINER (date_frame), calendar);
! 663: gtk_box_pack_start (GTK_BOX (content), date_frame, FALSE, FALSE, 6);
! 664: }
! 665:
! 666: g_object_set_data (G_OBJECT (dialog), "spin-quantity", spin);
! 667: g_object_set_data (G_OBJECT (dialog), "calendar", calendar);
! 668: g_object_set_data (G_OBJECT (dialog), "food-code",
! 669: GINT_TO_POINTER (food_code));
! 670: g_object_set_data_full (G_OBJECT (dialog), "food-desc",
! 671: g_strdup (description), g_free);
! 672: g_object_set_data (G_OBJECT (dialog), "food-db", state->food_db);
! 673:
! 674: /* Nutrient info in a scrolled grid. */
! 675: scroll = gtk_scrolled_window_new (NULL, NULL);
! 676: gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll),
! 677: GTK_POLICY_AUTOMATIC,
! 678: GTK_POLICY_AUTOMATIC);
! 679: grid = gtk_grid_new ();
! 680: gtk_grid_set_column_spacing (GTK_GRID (grid), 12);
! 681: gtk_grid_set_row_spacing (GTK_GRID (grid), 2);
! 682:
! 683: g_object_set_data (G_OBJECT (dialog), "nutrient-grid", grid);
! 684:
! 685: populate_nutrient_grid (grid, state->food_db, food_code, 1.0);
! 686:
! 687: gtk_container_add (GTK_CONTAINER (scroll), grid);
! 688: gtk_box_pack_start (GTK_BOX (content), scroll, TRUE, TRUE, 0);
! 689:
! 690: g_signal_connect (spin, "value-changed",
! 691: G_CALLBACK (on_detail_quantity_changed), dialog);
! 692: g_signal_connect (dialog, "response",
! 693: G_CALLBACK (on_food_add_clicked), state);
! 694: gtk_widget_show_all (dialog);
! 695: }
! 696:
! 697: /* Callback when a row in the search results tree view is activated. */
! 698: static void
! 699: on_search_row_activated (GtkTreeView *tree_view, GtkTreePath *path,
! 700: GtkTreeViewColumn *column, gpointer user_data)
! 701: {
! 702: struct gui_state *state = user_data;
! 703: GtkTreeModel *model;
! 704: GtkTreeIter iter;
! 705:
! 706: (void) column;
! 707:
! 708: model = gtk_tree_view_get_model (tree_view);
! 709: if (gtk_tree_model_get_iter (model, &iter, path))
! 710: {
! 711: gint food_code;
! 712: gchar *description;
! 713: gtk_tree_model_get (model, &iter,
! 714: COL_FOOD_CODE, &food_code,
! 715: COL_DESCRIPTION, &description,
! 716: -1);
! 717: show_food_detail (state, food_code, description);
! 718: g_free (description);
! 719: }
! 720: }
! 721:
! 722: /* Callback for search entry text changes (live search). */
! 723: static void
! 724: on_search_changed (GtkSearchEntry *entry, gpointer user_data)
! 725: {
! 726: GtkListStore *store = user_data;
! 727: struct gui_state *state;
! 728: const gchar *query;
! 729: struct food_list results;
! 730: size_t i;
! 731:
! 732: state = g_object_get_data (G_OBJECT (entry), "gui-state");
! 733: query = gtk_entry_get_text (GTK_ENTRY (entry));
! 734:
! 735: gtk_list_store_clear (store);
! 736:
! 737: if (query[0] == '\0')
! 738: return;
! 739:
! 740: if (db_search_foods (state->food_db, query, &results) == 0)
! 741: {
! 742: for (i = 0; i < results.count; i++)
! 743: {
! 744: GtkTreeIter iter;
! 745: gtk_list_store_append (store, &iter);
! 746: gtk_list_store_set (store, &iter,
! 747: COL_FOOD_CODE,
! 748: results.items[i].food_code,
! 749: COL_DESCRIPTION,
! 750: results.items[i].description,
! 751: -1);
! 752: }
! 753: food_list_free (&results);
! 754: }
! 755: }
! 756:
! 757: /* Show the Search dialog. */
! 758: static void
! 759: on_search_clicked (GtkButton *button, gpointer user_data)
! 760: {
! 761: struct gui_state *state = user_data;
! 762: GtkWidget *dialog;
! 763: GtkWidget *content;
! 764: GtkWidget *search_entry;
! 765: GtkWidget *scroll;
! 766: GtkWidget *tree;
! 767: GtkListStore *store;
! 768: GtkCellRenderer *renderer;
! 769: GtkTreeViewColumn *col;
! 770:
! 771: (void) button;
! 772:
! 773: dialog = gtk_dialog_new_with_buttons (_("Search Foods"),
! 774: GTK_WINDOW (state->window),
! 775: GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
! 776: _("Close"), GTK_RESPONSE_CLOSE,
! 777: NULL);
! 778: gtk_window_set_default_size (GTK_WINDOW (dialog), 600, 450);
! 779:
! 780: content = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
! 781:
! 782: /* Search entry. */
! 783: search_entry = gtk_search_entry_new ();
! 784: gtk_entry_set_placeholder_text (GTK_ENTRY (search_entry),
! 785: _("Type to search foods..."));
! 786: gtk_box_pack_start (GTK_BOX (content), search_entry, FALSE, FALSE, 6);
! 787:
! 788: /* Results list. */
! 789: store = gtk_list_store_new (NUM_COLS, G_TYPE_INT, G_TYPE_STRING);
! 790: tree = gtk_tree_view_new_with_model (GTK_TREE_MODEL (store));
! 791: g_object_unref (store);
! 792:
! 793: renderer = gtk_cell_renderer_text_new ();
! 794: col = gtk_tree_view_column_new_with_attributes (_("Code"), renderer,
! 795: "text", COL_FOOD_CODE,
! 796: NULL);
! 797: gtk_tree_view_append_column (GTK_TREE_VIEW (tree), col);
! 798:
! 799: renderer = gtk_cell_renderer_text_new ();
! 800: col = gtk_tree_view_column_new_with_attributes (_("Description"),
! 801: renderer,
! 802: "text", COL_DESCRIPTION,
! 803: NULL);
! 804: gtk_tree_view_column_set_expand (col, TRUE);
! 805: gtk_tree_view_append_column (GTK_TREE_VIEW (tree), col);
! 806:
! 807: g_object_set_data (G_OBJECT (search_entry), "gui-state", state);
! 808: g_signal_connect (search_entry, "search-changed",
! 809: G_CALLBACK (on_search_changed), store);
! 810: g_signal_connect (tree, "row-activated",
! 811: G_CALLBACK (on_search_row_activated), state);
! 812:
! 813: scroll = gtk_scrolled_window_new (NULL, NULL);
! 814: gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll),
! 815: GTK_POLICY_AUTOMATIC,
! 816: GTK_POLICY_AUTOMATIC);
! 817: gtk_container_add (GTK_CONTAINER (scroll), tree);
! 818: gtk_box_pack_start (GTK_BOX (content), scroll, TRUE, TRUE, 0);
! 819:
! 820: gtk_widget_show_all (dialog);
! 821: g_signal_connect (dialog, "response",
! 822: G_CALLBACK (gtk_widget_destroy), NULL);
! 823: }
! 824:
! 825: /* Profile dialog response handler. */
! 826: static void
! 827: on_profile_response (GtkDialog *dialog, gint response_id,
! 828: gpointer user_data)
! 829: {
! 830: struct gui_state *state = user_data;
! 831:
! 832: if (response_id == GTK_RESPONSE_OK)
! 833: {
! 834: GtkWidget *age_spin, *height_spin, *weight_spin, *activity_combo, *gender_combo;
! 835: struct user_profile prof;
! 836:
! 837: age_spin = g_object_get_data (G_OBJECT (dialog), "age-spin");
! 838: height_spin = g_object_get_data (G_OBJECT (dialog), "height-spin");
! 839: weight_spin = g_object_get_data (G_OBJECT (dialog), "weight-spin");
! 840: activity_combo = g_object_get_data (G_OBJECT (dialog), "activity-combo");
! 841: gender_combo = g_object_get_data (G_OBJECT (dialog), "gender-combo");
! 842:
! 843: prof.age_years = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (age_spin));
! 844: prof.height_cm = gtk_spin_button_get_value (GTK_SPIN_BUTTON (height_spin));
! 845: prof.weight_kg = gtk_spin_button_get_value (GTK_SPIN_BUTTON (weight_spin));
! 846: prof.activity_level = gtk_combo_box_get_active (GTK_COMBO_BOX (activity_combo));
! 847: prof.gender = gtk_combo_box_get_active (GTK_COMBO_BOX (gender_combo));
! 848:
! 849: prof.calorie_target = budget_estimate_calories (prof.age_years,
! 850: prof.height_cm, prof.weight_kg,
! 851: prof.activity_level, prof.gender);
! 852:
! 853: if (log_save_profile (state->log_db, &prof) == 0)
! 854: {
! 855: state->calories = prof.calorie_target;
! 856: state->dbus_ctx.calories = prof.calorie_target;
! 857: refresh_all (state);
! 858: }
! 859: }
! 860:
! 861: gtk_widget_destroy (GTK_WIDGET (dialog));
! 862: }
! 863:
! 864: /* Show the Profile dialog. */
! 865: static void
! 866: on_profile_clicked (GtkButton *button, gpointer user_data)
! 867: {
! 868: struct gui_state *state = user_data;
! 869: GtkWidget *dialog, *lbl, *content, *grid;
! 870: GtkWidget *age_spin, *height_spin, *weight_spin;
! 871: GtkWidget *activity_combo, *gender_combo;
! 872: struct user_profile prof;
! 873: int rc;
! 874: int row;
! 875:
! 876: (void) button;
! 877:
! 878: dialog = gtk_dialog_new_with_buttons (_("Profile"),
! 879: GTK_WINDOW (state->window),
! 880: GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
! 881: _("Save"), GTK_RESPONSE_OK,
! 882: _("Cancel"), GTK_RESPONSE_CANCEL,
! 883: NULL);
! 884:
! 885: content = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
! 886: grid = gtk_grid_new ();
! 887: gtk_grid_set_column_spacing (GTK_GRID (grid), 12);
! 888: gtk_grid_set_row_spacing (GTK_GRID (grid), 6);
! 889: gtk_widget_set_margin_start (grid, 12);
! 890: gtk_widget_set_margin_end (grid, 12);
! 891: gtk_widget_set_margin_top (grid, 12);
! 892: gtk_widget_set_margin_bottom (grid, 12);
! 893:
! 894: row = 0;
! 895:
! 896: /* Age. */
! 897: lbl = gtk_label_new (_("Age (years):"));
! 898: gtk_widget_set_halign (lbl, GTK_ALIGN_END);
! 899: gtk_grid_attach (GTK_GRID (grid), lbl, 0, row, 1, 1);
! 900: age_spin = gtk_spin_button_new_with_range (1, 120, 1);
! 901: gtk_grid_attach (GTK_GRID (grid), age_spin, 1, row, 1, 1);
! 902: row++;
! 903:
! 904: /* Height. */
! 905: lbl = gtk_label_new (_("Height (cm):"));
! 906: gtk_widget_set_halign (lbl, GTK_ALIGN_END);
! 907: gtk_grid_attach (GTK_GRID (grid), lbl, 0, row, 1, 1);
! 908: height_spin = gtk_spin_button_new_with_range (30.0, 300.0, 0.5);
! 909: gtk_grid_attach (GTK_GRID (grid), height_spin, 1, row, 1, 1);
! 910: row++;
! 911:
! 912: /* Weight. */
! 913: lbl = gtk_label_new (_("Weight (kg):"));
! 914: gtk_widget_set_halign (lbl, GTK_ALIGN_END);
! 915: gtk_grid_attach (GTK_GRID (grid), lbl, 0, row, 1, 1);
! 916: weight_spin = gtk_spin_button_new_with_range (1.0, 500.0, 0.5);
! 917: gtk_grid_attach (GTK_GRID (grid), weight_spin, 1, row, 1, 1);
! 918: row++;
! 919:
! 920: /* Activity level. */
! 921: lbl = gtk_label_new (_("Activity level:"));
! 922: gtk_widget_set_halign (lbl, GTK_ALIGN_END);
! 923: gtk_grid_attach (GTK_GRID (grid), lbl, 0, row, 1, 1);
! 924: activity_combo = gtk_combo_box_text_new ();
! 925: gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (activity_combo),
! 926: _("Sedentary"));
! 927: gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (activity_combo),
! 928: _("Light"));
! 929: gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (activity_combo),
! 930: _("Moderate"));
! 931: gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (activity_combo),
! 932: _("Very active"));
! 933: gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (activity_combo),
! 934: _("Extra active"));
! 935: gtk_combo_box_set_active (GTK_COMBO_BOX (activity_combo),
! 936: ACTIVITY_SEDENTARY);
! 937: gender_combo = gtk_combo_box_text_new ();
! 938: gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (gender_combo), _("Neutral"));
! 939: gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (gender_combo), _("Female"));
! 940: gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (gender_combo), _("Male"));
! 941: gtk_grid_attach (GTK_GRID (grid), activity_combo, 1, row, 1, 1);
! 942:
! 943: /* Load existing profile. */
! 944: rc = log_get_profile (state->log_db, &prof);
! 945: if (rc == 0)
! 946: {
! 947: gtk_spin_button_set_value (GTK_SPIN_BUTTON (age_spin),
! 948: prof.age_years);
! 949: gtk_spin_button_set_value (GTK_SPIN_BUTTON (height_spin),
! 950: prof.height_cm);
! 951: gtk_spin_button_set_value (GTK_SPIN_BUTTON (weight_spin),
! 952: prof.weight_kg);
! 953: gtk_combo_box_set_active (GTK_COMBO_BOX (activity_combo),
! 954: prof.activity_level);
! 955: gtk_combo_box_set_active (GTK_COMBO_BOX (gender_combo),
! 956: prof.gender);
! 957: }
! 958: else
! 959: {
! 960: gtk_combo_box_set_active (GTK_COMBO_BOX (gender_combo),
! 961: 0);
! 962: }
! 963:
! 964: g_object_set_data (G_OBJECT (dialog), "age-spin", age_spin);
! 965: g_object_set_data (G_OBJECT (dialog), "height-spin", height_spin);
! 966: g_object_set_data (G_OBJECT (dialog), "weight-spin", weight_spin);
! 967: g_object_set_data (G_OBJECT (dialog), "activity-combo", activity_combo);
! 968: g_object_set_data (G_OBJECT (dialog), "gender-combo", gender_combo);
! 969:
! 970: GtkWidget *gender_label = gtk_label_new (_("Gender:"));
! 971: gtk_widget_set_halign (gender_label, GTK_ALIGN_END);
! 972: gtk_grid_attach (GTK_GRID (grid), gender_label, 0, 4, 1, 1);
! 973: gtk_grid_attach (GTK_GRID (grid), gender_combo, 1, 4, 1, 1);
! 974:
! 975: gtk_box_pack_start (GTK_BOX (content), grid, TRUE, TRUE, 0);
! 976:
! 977: g_signal_connect (dialog, "response",
! 978: G_CALLBACK (on_profile_response), state);
! 979: gtk_widget_show_all (dialog);
! 980: }
! 981:
! 982: /* Navigate the food log to the previous date. */
! 983: static void
! 984: on_log_prev_clicked (GtkButton *button, gpointer user_data)
! 985: {
! 986: struct gui_state *state = user_data;
! 987: struct date_list dates;
! 988: size_t i;
! 989:
! 990: (void) button;
! 991:
! 992: if (log_get_dates (state->log_db, &dates) != 0 || dates.count == 0)
! 993: return;
! 994:
! 995: /* Find the current date in the list and go to the previous one. */
! 996: for (i = 0; i < dates.count; i++)
! 997: {
! 998: if (strcmp (dates.dates[i], state->log_date) == 0)
! 999: break;
! 1000: }
! 1001:
! 1002: if (i > 0 && i <= dates.count)
! 1003: {
! 1004: /* Move to the previous date. If current date was not found
! 1005: (i == dates.count), pick the last date before it. */
! 1006: size_t target = (i < dates.count) ? i - 1 : dates.count - 1;
! 1007: strncpy (state->log_date, dates.dates[target],
! 1008: sizeof state->log_date - 1);
! 1009: state->log_date[sizeof state->log_date - 1] = '\0';
! 1010: refresh_all (state);
! 1011: }
! 1012:
! 1013: date_list_free (&dates);
! 1014: }
! 1015:
! 1016: /* Navigate the food log to the next date. */
! 1017: static void
! 1018: on_log_next_clicked (GtkButton *button, gpointer user_data)
! 1019: {
! 1020: struct gui_state *state = user_data;
! 1021: struct date_list dates;
! 1022: size_t i;
! 1023:
! 1024: (void) button;
! 1025:
! 1026: if (log_get_dates (state->log_db, &dates) != 0 || dates.count == 0)
! 1027: return;
! 1028:
! 1029: /* Find the current date in the list and go to the next one. */
! 1030: for (i = 0; i < dates.count; i++)
! 1031: {
! 1032: if (strcmp (dates.dates[i], state->log_date) == 0)
! 1033: break;
! 1034: }
! 1035:
! 1036: if (i < dates.count && i + 1 < dates.count)
! 1037: {
! 1038: strncpy (state->log_date, dates.dates[i + 1],
! 1039: sizeof state->log_date - 1);
! 1040: state->log_date[sizeof state->log_date - 1] = '\0';
! 1041: refresh_all (state);
! 1042: }
! 1043:
! 1044: date_list_free (&dates);
! 1045: }
! 1046:
! 1047: /* Navigate the food log back to today's date. */
! 1048: static void
! 1049: on_log_today_clicked (GtkButton *button, gpointer user_data)
! 1050: {
! 1051: struct gui_state *state = user_data;
! 1052:
! 1053: (void) button;
! 1054:
! 1055: strncpy (state->log_date, today_date (), sizeof state->log_date - 1);
! 1056: state->log_date[sizeof state->log_date - 1] = '\0';
! 1057: refresh_all (state);
! 1058: }
! 1059:
! 1060: /* Show the About dialog. */
! 1061: static void
! 1062: on_about_clicked (GtkButton *button, gpointer user_data)
! 1063: {
! 1064: struct gui_state *state = user_data;
! 1065: GtkWidget *dialog;
! 1066: char version_text[512];
! 1067:
! 1068: (void) button;
! 1069:
! 1070: snprintf (version_text, sizeof version_text,
! 1071: _("GNUtrition %s\n"
! 1072: "Copyright (C) 2026 Free Software Foundation, Inc.\n"
! 1073: "License GPLv3+: GNU GPL version 3 or later "
! 1074: "<http://gnu.org/licenses/gpl.html>\n"
! 1075: "This is free software: you are free to change "
! 1076: "and redistribute it.\n"
! 1077: "There is NO WARRANTY, to the extent permitted by law."),
! 1078: PACKAGE_VERSION);
! 1079:
! 1080: dialog = gtk_message_dialog_new (GTK_WINDOW (state->window),
! 1081: GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
! 1082: GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE,
! 1083: "%s", version_text);
! 1084: gtk_window_set_title (GTK_WINDOW (dialog), _("About GNUtrition"));
! 1085: g_signal_connect (dialog, "response",
! 1086: G_CALLBACK (gtk_widget_destroy), NULL);
! 1087: gtk_widget_show_all (dialog);
! 1088: }
! 1089:
! 1090: /* Create a single GtkProgressBar for a budget category. */
! 1091: static GtkWidget *
! 1092: make_progress_bar (void)
! 1093: {
! 1094: GtkWidget *pb;
! 1095:
! 1096: pb = gtk_progress_bar_new ();
! 1097: gtk_progress_bar_set_show_text (GTK_PROGRESS_BAR (pb), TRUE);
! 1098: gtk_widget_set_hexpand (pb, TRUE);
! 1099: return pb;
! 1100: }
! 1101:
! 1102: /* Build and show the main window. */
! 1103: static void
! 1104: on_activate (GtkApplication *app, gpointer user_data)
! 1105: {
! 1106: struct gui_state *state = user_data;
! 1107: GtkWidget *header;
! 1108: GtkWidget *search_btn, *profile_btn, *about_btn;
! 1109: GtkWidget *main_box;
! 1110: GtkWidget *dash_frame, *log_frame;
! 1111: GtkWidget *dash_box;
! 1112: GtkWidget *log_scroll;
! 1113: GtkWidget *log_vbox;
! 1114: GtkWidget *log_nav_box;
! 1115: GtkWidget *prev_btn, *today_btn, *next_btn;
! 1116: char budget_text[128];
! 1117:
! 1118: /* Initialize the log date to today. */
! 1119: strncpy (state->log_date, today_date (), sizeof state->log_date - 1);
! 1120: state->log_date[sizeof state->log_date - 1] = '\0';
! 1121:
! 1122: /* Main window. */
! 1123: state->window = gtk_application_window_new (app);
! 1124: gtk_window_set_title (GTK_WINDOW (state->window), _("GNUtrition"));
! 1125: gtk_window_set_default_size (GTK_WINDOW (state->window), 700, 550);
! 1126:
! 1127: /* Header bar. */
! 1128: header = gtk_header_bar_new ();
! 1129: gtk_header_bar_set_title (GTK_HEADER_BAR (header), _("GNUtrition"));
! 1130: gtk_header_bar_set_show_close_button (GTK_HEADER_BAR (header), TRUE);
! 1131:
! 1132: search_btn = gtk_button_new_with_label (_("Search"));
! 1133: profile_btn = gtk_button_new_with_label (_("Profile"));
! 1134: about_btn = gtk_button_new_with_label (_("About"));
! 1135:
! 1136: gtk_header_bar_pack_start (GTK_HEADER_BAR (header), search_btn);
! 1137: gtk_header_bar_pack_end (GTK_HEADER_BAR (header), about_btn);
! 1138: gtk_header_bar_pack_end (GTK_HEADER_BAR (header), profile_btn);
! 1139:
! 1140: gtk_window_set_titlebar (GTK_WINDOW (state->window), header);
! 1141:
! 1142: g_signal_connect (search_btn, "clicked",
! 1143: G_CALLBACK (on_search_clicked), state);
! 1144: g_signal_connect (profile_btn, "clicked",
! 1145: G_CALLBACK (on_profile_clicked), state);
! 1146: g_signal_connect (about_btn, "clicked",
! 1147: G_CALLBACK (on_about_clicked), state);
! 1148:
! 1149: /* Main layout: vertical box with dashboard on top and log on bottom. */
! 1150: main_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
! 1151: gtk_widget_set_margin_start (main_box, 12);
! 1152: gtk_widget_set_margin_end (main_box, 12);
! 1153: gtk_widget_set_margin_top (main_box, 12);
! 1154: gtk_widget_set_margin_bottom (main_box, 12);
! 1155:
! 1156: /* Dashboard frame. */
! 1157: dash_frame = gtk_frame_new (_("Daily Budget"));
! 1158: dash_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 4);
! 1159: gtk_widget_set_margin_start (dash_box, 8);
! 1160: gtk_widget_set_margin_end (dash_box, 8);
! 1161: gtk_widget_set_margin_top (dash_box, 4);
! 1162: gtk_widget_set_margin_bottom (dash_box, 8);
! 1163:
! 1164: snprintf (budget_text, sizeof budget_text,
! 1165: _("USDA Healthy US-Style Eating Pattern (%d kcal)"),
! 1166: state->calories);
! 1167: state->budget_label = gtk_label_new (budget_text);
! 1168: gtk_widget_set_halign (state->budget_label, GTK_ALIGN_START);
! 1169: gtk_box_pack_start (GTK_BOX (dash_box), state->budget_label,
! 1170: FALSE, FALSE, 2);
! 1171:
! 1172: state->pb_vegetables = make_progress_bar ();
! 1173: state->pb_fruits = make_progress_bar ();
! 1174: state->pb_grains = make_progress_bar ();
! 1175: state->pb_dairy = make_progress_bar ();
! 1176: state->pb_protein = make_progress_bar ();
! 1177: state->pb_oils = make_progress_bar ();
! 1178:
! 1179: gtk_box_pack_start (GTK_BOX (dash_box), state->pb_vegetables,
! 1180: FALSE, FALSE, 2);
! 1181: gtk_box_pack_start (GTK_BOX (dash_box), state->pb_fruits,
! 1182: FALSE, FALSE, 2);
! 1183: gtk_box_pack_start (GTK_BOX (dash_box), state->pb_grains,
! 1184: FALSE, FALSE, 2);
! 1185: gtk_box_pack_start (GTK_BOX (dash_box), state->pb_dairy,
! 1186: FALSE, FALSE, 2);
! 1187: gtk_box_pack_start (GTK_BOX (dash_box), state->pb_protein,
! 1188: FALSE, FALSE, 2);
! 1189: gtk_box_pack_start (GTK_BOX (dash_box), state->pb_oils,
! 1190: FALSE, FALSE, 2);
! 1191:
! 1192: gtk_container_add (GTK_CONTAINER (dash_frame), dash_box);
! 1193: state->dashboard_box = dash_box;
! 1194:
! 1195: /* Log frame with date navigation. */
! 1196: log_frame = gtk_frame_new (_("Food Log"));
! 1197: state->log_frame = log_frame;
! 1198:
! 1199: log_vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 2);
! 1200:
! 1201: /* Date navigation bar. */
! 1202: log_nav_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4);
! 1203: gtk_widget_set_margin_start (log_nav_box, 8);
! 1204: gtk_widget_set_margin_end (log_nav_box, 8);
! 1205: gtk_widget_set_margin_top (log_nav_box, 4);
! 1206:
! 1207: prev_btn = gtk_button_new_with_label ("\342\227\200"); /* U+25C0 â */
! 1208: today_btn = gtk_button_new_with_label (_("Today"));
! 1209: next_btn = gtk_button_new_with_label ("\342\226\266"); /* U+25B6 â¶ */
! 1210:
! 1211: gtk_box_pack_start (GTK_BOX (log_nav_box), prev_btn, FALSE, FALSE, 0);
! 1212: gtk_box_pack_start (GTK_BOX (log_nav_box), today_btn, FALSE, FALSE, 0);
! 1213: gtk_box_pack_start (GTK_BOX (log_nav_box), next_btn, FALSE, FALSE, 0);
! 1214:
! 1215: g_signal_connect (prev_btn, "clicked",
! 1216: G_CALLBACK (on_log_prev_clicked), state);
! 1217: g_signal_connect (today_btn, "clicked",
! 1218: G_CALLBACK (on_log_today_clicked), state);
! 1219: g_signal_connect (next_btn, "clicked",
! 1220: G_CALLBACK (on_log_next_clicked), state);
! 1221:
! 1222: gtk_box_pack_start (GTK_BOX (log_vbox), log_nav_box, FALSE, FALSE, 0);
! 1223:
! 1224: log_scroll = gtk_scrolled_window_new (NULL, NULL);
! 1225: gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (log_scroll),
! 1226: GTK_POLICY_AUTOMATIC,
! 1227: GTK_POLICY_AUTOMATIC);
! 1228: state->log_list_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 2);
! 1229: gtk_widget_set_margin_start (state->log_list_box, 8);
! 1230: gtk_widget_set_margin_end (state->log_list_box, 8);
! 1231: gtk_widget_set_margin_top (state->log_list_box, 4);
! 1232: gtk_widget_set_margin_bottom (state->log_list_box, 4);
! 1233: gtk_container_add (GTK_CONTAINER (log_scroll), state->log_list_box);
! 1234: gtk_box_pack_start (GTK_BOX (log_vbox), log_scroll, TRUE, TRUE, 0);
! 1235:
! 1236: gtk_container_add (GTK_CONTAINER (log_frame), log_vbox);
! 1237:
! 1238: gtk_box_pack_start (GTK_BOX (main_box), dash_frame, FALSE, FALSE, 0);
! 1239: gtk_box_pack_start (GTK_BOX (main_box), log_frame, TRUE, TRUE, 0);
! 1240: gtk_container_add (GTK_CONTAINER (state->window), main_box);
! 1241:
! 1242: /* Populate data. */
! 1243: refresh_all (state);
! 1244:
! 1245: gtk_widget_show_all (state->window);
! 1246: }
! 1247:
! 1248: int
! 1249: gui_run (sqlite3 *food_db, sqlite3 *log_db, int calories,
! 1250: int argc, char **argv)
! 1251: {
! 1252: GtkApplication *app;
! 1253: struct gui_state state;
! 1254: int status;
! 1255:
! 1256: memset (&state, 0, sizeof state);
! 1257: state.food_db = food_db;
! 1258: state.log_db = log_db;
! 1259: state.calories = calories;
! 1260:
! 1261: /* Start D-Bus service. */
! 1262: state.dbus_ctx.food_db = food_db;
! 1263: state.dbus_ctx.log_db = log_db;
! 1264: state.dbus_ctx.calories = calories;
! 1265: state.dbus_owner_id = dbus_service_start (&state.dbus_ctx);
! 1266:
! 1267: app = gtk_application_new ("org.gnu.gnutrition",
! 1268: G_APPLICATION_DEFAULT_FLAGS);
! 1269: g_signal_connect (app, "activate", G_CALLBACK (on_activate), &state);
! 1270:
! 1271: status = g_application_run (G_APPLICATION (app), argc, argv);
! 1272: g_object_unref (app);
! 1273:
! 1274: /* Stop D-Bus service. */
! 1275: dbus_service_stop (state.dbus_owner_id);
! 1276:
! 1277: return status;
! 1278: }
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>