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>