weather-summary.c 45.6 KB
Newer Older
Harald Judt's avatar
Harald Judt committed
1
/*  Copyright (c) 2003-2014 Xfce Development Team
2
 *
3 4 5 6
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
7
 *
8 9 10 11
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
12
 *
13 14 15 16
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
17 18 19
 */

#ifdef HAVE_CONFIG_H
20
#include <config.h>
21 22
#endif

23
#include <libxfce4ui/libxfce4ui.h>
24

Nick Schermer's avatar
Nick Schermer committed
25 26 27 28 29 30
#include "weather-parsers.h"
#include "weather-data.h"
#include "weather.h"
#include "weather-summary.h"
#include "weather-translate.h"
#include "weather-icon.h"
31

32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
static gboolean
lnk_clicked(GtkTextTag *tag,
            GObject *obj,
            GdkEvent *event,
            GtkTextIter *iter,
            GtkWidget *textview);


#define BORDER 8

#define APPEND_BTEXT(text)                                          \
    gtk_text_buffer_insert_with_tags(GTK_TEXT_BUFFER(buffer),       \
                                     &iter, text, -1, btag, NULL);

#define APPEND_TEXT_ITEM_REAL(text)                 \
    gtk_text_buffer_insert(GTK_TEXT_BUFFER(buffer), \
                           &iter, text, -1);        \
    g_free(value);

51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
/*
 * TRANSLATORS: This format string belongs to the macro used for
 * printing the "Label: Value Unit" lines on the details tab, e.g.
 * "Temperature: 10 °C" or "Latitude: 95.7°".
 * The %s stand for:
 *   - label
 *   - ": " if label is not empty, else empty
 *   - value
 *   - space if unit is not degree "°" (but this is not °C or °F!)
 *   - unit
 * Usually, you should leave this unchanged, BUT...
 * RTL TRANSLATORS: In case you did not translate the measurement
 * unit, use LRM (left-to-right mark) etc. to align it properly with
 * its numeric value.
 */
Harald Judt's avatar
Harald Judt committed
66 67 68 69 70 71 72 73 74 75
#define APPEND_TEXT_ITEM(text, item)                        \
    rawvalue = get_data(conditions, data->units, item,      \
                        FALSE, data->night_time);           \
    unit = get_unit(data->units, item);                     \
    value = g_strdup_printf(_("\t%s%s%s%s%s\n"),            \
                            text, text ? ": " : "",         \
                            rawvalue,                       \
                            strcmp(unit, "°") ? " " : "",   \
                            unit);                          \
    g_free(rawvalue);                                       \
76 77 78 79 80 81 82 83 84
    APPEND_TEXT_ITEM_REAL(value);

#define APPEND_LINK_ITEM(prefix, text, url, lnk_tag)                    \
    gtk_text_buffer_insert(GTK_TEXT_BUFFER(buffer),                     \
                           &iter, prefix, -1);                          \
    gtk_text_buffer_insert_with_tags(GTK_TEXT_BUFFER(buffer),           \
                                     &iter, text, -1, lnk_tag, NULL);   \
    gtk_text_buffer_insert(GTK_TEXT_BUFFER(buffer),                     \
                           &iter, "\n", -1);                            \
Harald Judt's avatar
Harald Judt committed
85 86
    g_object_set_data_full(G_OBJECT(lnk_tag), "url",                    \
                           g_strdup(url), g_free);                      \
87 88
    g_signal_connect(G_OBJECT(lnk_tag), "event",                        \
                     G_CALLBACK(lnk_clicked), NULL);
89

Harald Judt's avatar
Harald Judt committed
90 91
#define ATTACH_DAYTIME_HEADER(title, pos)               \
    if (data->forecast_layout == FC_LAYOUT_CALENDAR)    \
Sean Davis's avatar
Sean Davis committed
92
        gtk_grid_attach                       \
Sean Davis's avatar
Sean Davis committed
93
            (GTK_GRID (grid),                          \
Harald Judt's avatar
Harald Judt committed
94
             add_forecast_header(title, 90.0, &darkbg), \
Sean Davis's avatar
Sean Davis committed
95
             0, pos, 1, 1);                         \
Harald Judt's avatar
Harald Judt committed
96
    else                                                \
Sean Davis's avatar
Sean Davis committed
97
        gtk_grid_attach                       \
Sean Davis's avatar
Sean Davis committed
98
            (GTK_GRID (grid),                          \
Harald Judt's avatar
Harald Judt committed
99
             add_forecast_header(title, 0.0, &darkbg),  \
Sean Davis's avatar
Sean Davis committed
100
             pos, 0, 1, 1);                         \
Harald Judt's avatar
Harald Judt committed
101

102
#define APPEND_TOOLTIP_ITEM(description, item)                  \
103 104
    value = get_data(fcdata, data->units, item,                 \
                     data->round, data->night_time);            \
105 106 107 108 109 110 111
    if (strcmp(value, "")) {                                    \
        unit = get_unit(data->units, item);                     \
        g_string_append_printf(text, description, value,        \
                               strcmp(unit, "°") ? " " : "",    \
                               unit);                           \
    } else                                                      \
        g_string_append_printf(text, description, "-", "", ""); \
112 113
    g_free(value);

114

Sean Davis's avatar
Sean Davis committed
115 116 117 118 119 120 121 122 123 124 125
static void
weather_widget_set_border_width (GtkWidget *widget,
                                 gint       border_width)
{
    gtk_widget_set_margin_start (widget, border_width);
    gtk_widget_set_margin_top (widget, border_width);
    gtk_widget_set_margin_end (widget, border_width);
    gtk_widget_set_margin_bottom (widget, border_width);
}


126 127 128 129 130 131
static gboolean
lnk_clicked(GtkTextTag *tag,
            GObject *obj,
            GdkEvent *event,
            GtkTextIter *iter,
            GtkWidget *textview)
132
{
133 134
    const gchar *url;
    gchar *str;
135

136 137 138 139 140 141 142 143 144 145
    if (event->type == GDK_BUTTON_RELEASE) {
        url = g_object_get_data(G_OBJECT(tag), "url");
        str = g_strdup_printf("exo-open --launch WebBrowser %s", url);
        g_spawn_command_line_async(str, NULL);
        g_free(str);
    } else if (event->type == GDK_LEAVE_NOTIFY)
        gdk_window_set_cursor(gtk_text_view_get_window(GTK_TEXT_VIEW(obj),
                                                       GTK_TEXT_WINDOW_TEXT),
                              NULL);
    return FALSE;
146 147
}

148

149
static gboolean
150
icon_clicked (GtkWidget *widget,
151
              GdkEventButton *event,
152
              gpointer user_data)
153
{
154
    return lnk_clicked(user_data, NULL, (GdkEvent *) (event), NULL, NULL);
155 156
}

157 158 159 160

static gboolean
view_motion_notify(GtkWidget *widget,
                   GdkEventMotion *event,
Harald Judt's avatar
Harald Judt committed
161
                   summary_details *sum)
162
{
Harald Judt's avatar
Harald Judt committed
163 164 165 166 167
    GtkTextIter iter;
    GtkTextTag *tag;
    GSList *tags;
    GSList *cur;
    gint bx, by;
168

Harald Judt's avatar
Harald Judt committed
169
    if (event->x != -1 && event->y != -1) {
Harald Judt's avatar
Harald Judt committed
170
        gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW(sum->text_view),
171 172
                                              GTK_TEXT_WINDOW_WIDGET,
                                              event->x, event->y, &bx, &by);
Harald Judt's avatar
Harald Judt committed
173
        gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(sum->text_view),
174 175 176
                                           &iter, bx, by);
        tags = gtk_text_iter_get_tags(&iter);
        for (cur = tags; cur != NULL; cur = cur->next) {
Harald Judt's avatar
Harald Judt committed
177
            tag = cur->data;
178
            if (g_object_get_data(G_OBJECT(tag), "url")) {
Harald Judt's avatar
Harald Judt committed
179 180 181 182
                gdk_window_set_cursor
                    (gtk_text_view_get_window(GTK_TEXT_VIEW(sum->text_view),
                                              GTK_TEXT_WINDOW_TEXT),
                     sum->hand_cursor);
183 184 185
                return FALSE;
            }
        }
186
    }
Harald Judt's avatar
Harald Judt committed
187 188 189 190 191
    if (!sum->on_icon)
        gdk_window_set_cursor
            (gtk_text_view_get_window(GTK_TEXT_VIEW(sum->text_view),
                                      GTK_TEXT_WINDOW_TEXT),
             sum->text_cursor);
192
    return FALSE;
193 194
}

195 196 197 198

static gboolean
icon_motion_notify(GtkWidget *widget,
                   GdkEventMotion *event,
Harald Judt's avatar
Harald Judt committed
199
                   summary_details *sum)
200
{
Harald Judt's avatar
Harald Judt committed
201 202 203 204 205
    sum->on_icon = TRUE;
    gdk_window_set_cursor
        (gtk_text_view_get_window(GTK_TEXT_VIEW(sum->text_view),
                                  GTK_TEXT_WINDOW_TEXT),
         sum->hand_cursor);
206
    return FALSE;
207 208
}

209 210 211 212

static gboolean
view_leave_notify(GtkWidget *widget,
                  GdkEventMotion *event,
Harald Judt's avatar
Harald Judt committed
213
                  summary_details *sum)
214
{
Harald Judt's avatar
Harald Judt committed
215 216 217 218 219
    sum->on_icon = FALSE;
    gdk_window_set_cursor
        (gtk_text_view_get_window(GTK_TEXT_VIEW(sum->text_view),
                                  GTK_TEXT_WINDOW_TEXT),
         sum->text_cursor);
220
    return FALSE;
221 222
}

223 224 225

static void
view_scrolled_cb(GtkAdjustment *adj,
Harald Judt's avatar
Harald Judt committed
226
                 summary_details *sum)
227
{
Harald Judt's avatar
Harald Judt committed
228
    gint x, y, x1, y1;
229 230
    GtkAllocation allocation;
    GtkRequisition requisition;
Harald Judt's avatar
Harald Judt committed
231

Harald Judt's avatar
Harald Judt committed
232
    if (sum->icon_ebox) {
Sean Davis's avatar
Sean Davis committed
233 234 235 236 237
        gtk_widget_get_allocation (GTK_WIDGET (sum->text_view), &allocation);
        G_GNUC_BEGIN_IGNORE_DEPRECATIONS
        gtk_widget_get_requisition (GTK_WIDGET (sum->text_view), &requisition);
        G_GNUC_END_IGNORE_DEPRECATIONS

238 239 240 241 242 243 244 245 246 247 248
        /* TRANSLATORS: DO NOT TRANSLATE THIS STRING. This string is
           not visible to the user but controls the alignment of the
           met.no image on the details tab in the summary window,
           which is needed for RTL languages because the text is on
           the right side.
           If you are a RTL translator (hebrew, arabic), set this
           string to "RTL" to align the image to the left.
           If not, leave this string untouched or untranslated.
           Whatever you do, it should not have the value "RTL".
           If you know of a better way to determine LTR/RTL that makes
           this tweak unnecessary, please tell the developer.
249
        */
250
        if (!strcmp(_("LTR"), "RTL"))
251
            x1 = -30;
252
        else
253 254
            x1 = allocation.width - 191 - 15;
        y1 = requisition.height - 60 - 15;
Harald Judt's avatar
Harald Judt committed
255
        gtk_text_view_buffer_to_window_coords(GTK_TEXT_VIEW(sum->text_view),
256 257
                                              GTK_TEXT_WINDOW_TEXT,
                                              x1, y1, &x, &y);
Harald Judt's avatar
Harald Judt committed
258 259
        gtk_text_view_move_child(GTK_TEXT_VIEW(sum->text_view),
                                 sum->icon_ebox, x, y);
260
    }
261 262
}

263 264 265 266 267

static void
view_size_allocate_cb(GtkWidget *widget,
                      GtkAllocation *allocation,
                      gpointer data)
268
{
Harald Judt's avatar
Harald Judt committed
269
    view_scrolled_cb(NULL, data);
270
}
271

272 273 274

static gchar *
get_logo_path(void)
275
{
276 277 278 279 280 281 282
    gchar *cache_dir, *logo_path;

    cache_dir = get_cache_directory();
    logo_path = g_strconcat(cache_dir, G_DIR_SEPARATOR_S,
                            "weather_logo.gif", NULL);
    g_free(cache_dir);
    return logo_path;
283 284
}

285

286
static void
287 288 289
logo_fetched(SoupSession *session,
             SoupMessage *msg,
             gpointer user_data)
290
{
291
    if (msg && msg->response_body && msg->response_body->length > 0) {
292 293 294
        gchar *path = get_logo_path();
        GError *error = NULL;
        GdkPixbuf *pixbuf = NULL;
295 296
        if (!g_file_set_contents(path, msg->response_body->data,
                                 msg->response_body->length, &error)) {
297 298 299
            g_warning(_("Error downloading met.no logo image to %s, "
                        "reason: %s\n"), path,
                      error ? error->message : _("unknown"));
300 301 302 303 304 305 306 307 308 309 310
            g_error_free(error);
            g_free(path);
            return;
        }
        pixbuf = gdk_pixbuf_new_from_file(path, NULL);
        g_free(path);
        if (pixbuf) {
            gtk_image_set_from_pixbuf(GTK_IMAGE(user_data), pixbuf);
            g_object_unref(pixbuf);
        }
    }
311 312
}

313 314

static GtkWidget *
315
weather_summary_get_logo(plugin_data *data)
316
{
317
    GtkWidget *image = gtk_image_new();
Harald Judt's avatar
Harald Judt committed
318
    GdkPixbuf *pixbuf;
319 320 321 322 323
    gchar *path = get_logo_path();

    pixbuf = gdk_pixbuf_new_from_file(path, NULL);
    g_free(path);
    if (pixbuf == NULL)
324
        weather_http_queue_request(data->session,
325
                                   "https://met.no/filestore/met.no-logo.gif",
326
                                   logo_fetched, image);
327 328 329 330 331
    else {
        gtk_image_set_from_pixbuf(GTK_IMAGE(image), pixbuf);
        g_object_unref(pixbuf);
    }
    return image;
332
}
333

334

335
static GtkWidget *
336
create_summary_tab(plugin_data *data)
337
{
338 339
    GtkTextBuffer *buffer;
    GtkTextIter iter;
340
    GtkTextTag *btag, *ltag_img, *ltag_metno, *ltag_wiki, *ltag_geonames;
Harald Judt's avatar
Harald Judt committed
341
    GtkWidget *view, *frame, *scrolled, *icon;
342 343 344 345
    GtkAdjustment *adj;
    GdkColor lnk_color;
    xml_time *conditions;
    const gchar *unit;
Harald Judt's avatar
Harald Judt committed
346
    gchar *value, *rawvalue, *wind;
347 348
    gchar *last_download, *next_download;
    gchar *interval_start, *interval_end, *point;
349
    gchar *sunrise, *sunset, *moonrise, *moonset;
Harald Judt's avatar
Harald Judt committed
350
    summary_details *sum;
351

Harald Judt's avatar
Harald Judt committed
352 353
    sum = g_slice_new0(summary_details);
    sum->on_icon = FALSE;
Sean Davis's avatar
Sean Davis committed
354 355
    sum->hand_cursor = gdk_cursor_new_for_display (gdk_display_get_default(), GDK_HAND2);
    sum->text_cursor = gdk_cursor_new_for_display (gdk_display_get_default(), GDK_XTERM);
Harald Judt's avatar
Harald Judt committed
356 357 358
    data->summary_details = sum;

    sum->text_view = view = gtk_text_view_new();
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
    gtk_text_view_set_editable(GTK_TEXT_VIEW(view), FALSE);
    gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(view), FALSE);
    frame = gtk_frame_new(NULL);
    scrolled = gtk_scrolled_window_new(NULL, NULL);

    gtk_container_add(GTK_CONTAINER(scrolled), view);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
                                   GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);

    gtk_container_set_border_width(GTK_CONTAINER(frame), BORDER);
    gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN);
    gtk_container_add(GTK_CONTAINER(frame), scrolled);

    buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(view));
    gtk_text_buffer_get_iter_at_offset(GTK_TEXT_BUFFER(buffer), &iter, 0);
    btag = gtk_text_buffer_create_tag(buffer, NULL, "weight",
                                      PANGO_WEIGHT_BOLD, NULL);

    conditions = get_current_conditions(data->weatherdata);
    APPEND_BTEXT(_("Coordinates\n"));
    APPEND_TEXT_ITEM(_("Altitude"), ALTITUDE);
    APPEND_TEXT_ITEM(_("Latitude"), LATITUDE);
    APPEND_TEXT_ITEM(_("Longitude"), LONGITUDE);

383 384
    /* TRANSLATORS: Please use as many \t as appropriate to align the
       date/time values as in the original. */
385 386 387 388 389 390 391 392 393 394 395 396
    APPEND_BTEXT(_("\nDownloads\n"));
    last_download = format_date(data->weather_update->last, NULL, TRUE);
    next_download = format_date(data->weather_update->next, NULL, TRUE);
    value = g_strdup_printf(_("\tWeather data:\n"
                              "\tLast:\t%s\n"
                              "\tNext:\t%s\n"
                              "\tCurrent failed attempts: %d\n\n"),
                            last_download,
                            next_download,
                            data->weather_update->attempt);
    g_free(last_download);
    g_free(next_download);
397 398
    APPEND_TEXT_ITEM_REAL(value);

399 400 401 402 403 404 405 406 407 408
    /* Check for deprecated API and issue a warning if necessary */
    if (data->weather_update->http_status_code == 203)
        APPEND_BTEXT
            (_("\tMet.no LocationforecastLTS API states that this version\n"
               "\tof the webservice is deprecated, and the plugin needs to be\n"
               "\tadapted to use a newer version, or it will stop working within\n"
               "\ta few months.\n"
               "\tPlease file a bug on https://bugzilla.xfce.org if no one\n"
               "\telse has done so yet.\n\n"));

409 410 411 412 413 414 415 416 417 418 419 420 421
    last_download = format_date(data->astro_update->last, NULL, TRUE);
    next_download = format_date(data->astro_update->next, NULL, TRUE);
    value = g_strdup_printf(_("\tAstronomical data:\n"
                              "\tLast:\t%s\n"
                              "\tNext:\t%s\n"
                              "\tCurrent failed attempts: %d\n"),
                            last_download,
                            next_download,
                            data->astro_update->attempt);
    g_free(last_download);
    g_free(next_download);
    APPEND_TEXT_ITEM_REAL(value);

422 423 424 425 426 427 428 429 430
    /* Check for deprecated sunrise API and issue a warning if necessary */
    if (data->astro_update->http_status_code == 203)
        APPEND_BTEXT
            (_("\n\tMet.no sunrise API states that this version of the webservice\n"
               "\tis deprecated, and the plugin needs to be adapted to use\n"
               "\ta newer version, or it will stop working within a few months.\n"
               "\tPlease file a bug on https://bugzilla.xfce.org if no one\n"
               "\telse has done so yet.\n"));

431
    /* calculation times */
432
    APPEND_BTEXT(_("\nTimes Used for Calculations\n"));
433
    point = format_date(conditions->point, NULL, TRUE);
434
    value = g_strdup_printf
Harald Judt's avatar
Harald Judt committed
435
        (_("\tTemperatures, wind, atmosphere and cloud data calculated\n"
436
           "\tfor:\t\t%s\n"),
437
         point);
438
    g_free(point);
439 440
    APPEND_TEXT_ITEM_REAL(value);

441 442
    interval_start = format_date(conditions->start, NULL, TRUE);
    interval_end = format_date(conditions->end, NULL, TRUE);
443
    value = g_strdup_printf
Harald Judt's avatar
Harald Judt committed
444
        (_("\n\tPrecipitation and the weather symbol have been calculated\n"
445
           "\tusing the following time interval:\n"
446 447
           "\tStart:\t%s\n"
           "\tEnd:\t%s\n"),
448 449
         interval_start,
         interval_end);
450 451
    g_free(interval_end);
    g_free(interval_start);
452 453
    APPEND_TEXT_ITEM_REAL(value);

454
    /* sun and moon */
455
    APPEND_BTEXT(_("\nAstronomical Data\n"));
456 457
    if (data->current_astro) {
        if (data->current_astro->sun_never_rises) {
458 459
            value = g_strdup(_("\tSunrise:\t\tThe sun never rises today.\n"));
            APPEND_TEXT_ITEM_REAL(value);
460
        } else if (data->current_astro->sun_never_sets) {
461 462 463
            value = g_strdup(_("\tSunset:\t\tThe sun never sets today.\n"));
            APPEND_TEXT_ITEM_REAL(value);
        } else {
464
            sunrise = format_date(data->current_astro->sunrise, NULL, TRUE);
465
            value = g_strdup_printf(_("\tSunrise:\t\t%s\n"), sunrise);
466
            g_free(sunrise);
467 468
            APPEND_TEXT_ITEM_REAL(value);

469
            sunset = format_date(data->current_astro->sunset, NULL, TRUE);
470
            value = g_strdup_printf(_("\tSunset:\t\t%s\n\n"), sunset);
471
            g_free(sunset);
472 473 474
            APPEND_TEXT_ITEM_REAL(value);
        }

475
        if (data->current_astro->moon_phase)
476 477
            value = g_strdup_printf(_("\tMoon phase:\t%s\n"),
                                    translate_moon_phase
478
                                    (data->current_astro->moon_phase));
479 480 481 482
        else
            value = g_strdup(_("\tMoon phase:\tUnknown\n"));
        APPEND_TEXT_ITEM_REAL(value);

483
        if (data->current_astro->moon_never_rises) {
484 485
            value =
                g_strdup(_("\tMoonrise:\tThe moon never rises today.\n"));
486
            APPEND_TEXT_ITEM_REAL(value);
487
        } else if (data->current_astro->moon_never_sets) {
488 489
            value =
                g_strdup(_("\tMoonset:\tThe moon never sets today.\n"));
490 491
            APPEND_TEXT_ITEM_REAL(value);
        } else {
492
            moonrise = format_date(data->current_astro->moonrise, NULL, TRUE);
493
            value = g_strdup_printf(_("\tMoonrise:\t%s\n"), moonrise);
494
            g_free(moonrise);
495 496
            APPEND_TEXT_ITEM_REAL(value);

497
            moonset = format_date(data->current_astro->moonset, NULL, TRUE);
498
            value = g_strdup_printf(_("\tMoonset:\t%s\n"), moonset);
499
            g_free(moonset);
500 501 502 503 504 505 506 507
            APPEND_TEXT_ITEM_REAL(value);
        }
    } else {
        value = g_strdup(_("\tData not available, will use sane "
                           "default values for night and day.\n"));
        APPEND_TEXT_ITEM_REAL(value);
    }

Harald Judt's avatar
Harald Judt committed
508 509
    /* temperatures */
    APPEND_BTEXT(_("\nTemperatures\n"));
510
    APPEND_TEXT_ITEM(_("Temperature"), TEMPERATURE);
Harald Judt's avatar
Harald Judt committed
511
    APPEND_TEXT_ITEM(_("Dew point"), DEWPOINT);
512
    APPEND_TEXT_ITEM(_("Apparent temperature"), APPARENT_TEMPERATURE);
513 514 515

    /* wind */
    APPEND_BTEXT(_("\nWind\n"));
516 517 518 519
    wind = get_data(conditions, data->units, WIND_SPEED,
                    FALSE, data->night_time);
    rawvalue = get_data(conditions, data->units, WIND_BEAUFORT,
                        FALSE, data->night_time);
520 521
    value = g_strdup_printf(_("\tSpeed: %s %s (%s on the Beaufort scale)\n"),
                            wind, get_unit(data->units, WIND_SPEED),
522
                            rawvalue);
523 524 525 526
    g_free(rawvalue);
    g_free(wind);
    APPEND_TEXT_ITEM_REAL(value);

527
    /* wind direction */
528 529
    rawvalue = get_data(conditions, data->units, WIND_DIRECTION_DEG,
                        FALSE, data->night_time);
530 531
    wind = get_data(conditions, data->units, WIND_DIRECTION,
                        FALSE, data->night_time);
532
    value = g_strdup_printf(_("\tDirection: %s (%s%s)\n"),
533
                            wind, rawvalue,
534
                            get_unit(data->units, WIND_DIRECTION_DEG));
535 536 537 538 539
    g_free(rawvalue);
    g_free(wind);
    APPEND_TEXT_ITEM_REAL(value);

    /* precipitation */
540 541
    APPEND_BTEXT(_("\nPrecipitation\n"));
    APPEND_TEXT_ITEM(_("Precipitation amount"), PRECIPITATION);
542 543 544

    /* atmosphere */
    APPEND_BTEXT(_("\nAtmosphere\n"));
545 546
    APPEND_TEXT_ITEM(_("Barometric pressure"), PRESSURE);
    APPEND_TEXT_ITEM(_("Relative humidity"), HUMIDITY);
547 548 549 550 551

    /* clouds */
    APPEND_BTEXT(_("\nClouds\n"));
    APPEND_TEXT_ITEM(_("Fog"), FOG);
    APPEND_TEXT_ITEM(_("Low clouds"), CLOUDS_LOW);
552
    APPEND_TEXT_ITEM(_("Middle clouds"), CLOUDS_MID);
553 554 555
    APPEND_TEXT_ITEM(_("High clouds"), CLOUDS_HIGH);
    APPEND_TEXT_ITEM(_("Cloudiness"), CLOUDINESS);

Harald Judt's avatar
Harald Judt committed
556 557
    /* credits */
    gdk_color_parse("#0000ff", &lnk_color);
558 559 560 561 562 563 564 565 566
    ltag_img = gtk_text_buffer_create_tag(buffer, "lnk0", "foreground-gdk",
                                          &lnk_color, NULL);
    ltag_metno = gtk_text_buffer_create_tag(buffer, "lnk1", "foreground-gdk",
                                            &lnk_color, NULL);
    ltag_wiki = gtk_text_buffer_create_tag(buffer, "lnk2", "foreground-gdk",
                                           &lnk_color, NULL);
    ltag_geonames = gtk_text_buffer_create_tag(buffer, "lnk3",
                                               "foreground-gdk",
                                               &lnk_color, NULL);
Harald Judt's avatar
Harald Judt committed
567 568
    APPEND_BTEXT(_("\nCredits\n"));
    APPEND_LINK_ITEM(_("\tEncyclopedic information partly taken from\n\t\t"),
569 570 571 572
                     _("Wikipedia"), "http://wikipedia.org", ltag_wiki);
    APPEND_LINK_ITEM(_("\n\tElevation and timezone data provided by\n\t\t"),
                     _("GeoNames"),
                     "http://geonames.org/", ltag_geonames);
Harald Judt's avatar
Harald Judt committed
573 574
    APPEND_LINK_ITEM(_("\n\tWeather and astronomical data from\n\t\t"),
                     _("The Norwegian Meteorological Institute"),
575 576
                     "http://met.no/", ltag_metno);
    g_object_set_data_full(G_OBJECT(ltag_img), "url",   /* url for image */
Harald Judt's avatar
Harald Judt committed
577
                           g_strdup("http://met.no"), g_free);
578 579

    g_signal_connect(G_OBJECT(view), "motion-notify-event",
Harald Judt's avatar
Harald Judt committed
580
                     G_CALLBACK(view_motion_notify), sum);
581
    g_signal_connect(G_OBJECT(view), "leave-notify-event",
Harald Judt's avatar
Harald Judt committed
582
                     G_CALLBACK(view_leave_notify), sum);
583

Harald Judt's avatar
Harald Judt committed
584
    icon = weather_summary_get_logo(data);
585

Harald Judt's avatar
Harald Judt committed
586 587
    if (icon) {
        sum->icon_ebox = gtk_event_box_new();
588 589
        gtk_event_box_set_visible_window(GTK_EVENT_BOX(sum->icon_ebox), FALSE);
        gtk_container_add(GTK_CONTAINER(sum->icon_ebox), icon);
590
        gtk_text_view_add_child_in_window(GTK_TEXT_VIEW(view),
Harald Judt's avatar
Harald Judt committed
591
                                          sum->icon_ebox,
592
                                          GTK_TEXT_WINDOW_TEXT, 0, 0);
Harald Judt's avatar
Harald Judt committed
593
        gtk_widget_show_all(sum->icon_ebox);
594 595 596
        adj =
            gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scrolled));
        g_signal_connect(G_OBJECT(adj), "value-changed",
Harald Judt's avatar
Harald Judt committed
597
                         G_CALLBACK(view_scrolled_cb), sum);
598
        g_signal_connect(G_OBJECT(view), "size_allocate",
Harald Judt's avatar
Harald Judt committed
599 600
                         G_CALLBACK(view_size_allocate_cb), sum);
        g_signal_connect(G_OBJECT(sum->icon_ebox), "button-release-event",
601
                         G_CALLBACK(icon_clicked), ltag_img);
Harald Judt's avatar
Harald Judt committed
602 603 604 605 606 607
        g_signal_connect(G_OBJECT(sum->icon_ebox), "enter-notify-event",
                         G_CALLBACK(icon_motion_notify), sum);
        g_signal_connect(G_OBJECT(sum->icon_ebox), "motion-notify-event",
                         G_CALLBACK(icon_motion_notify), sum);
        g_signal_connect(G_OBJECT(sum->icon_ebox), "leave-notify-event",
                         G_CALLBACK(view_leave_notify), sum);
608
    }
Harald Judt's avatar
Harald Judt committed
609

610
    return frame;
611
}
612

613

Harald Judt's avatar
Harald Judt committed
614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634
static gchar *
get_dayname(gint day)
{
    struct tm fcday_tm;
    time_t now_t = time(NULL), fcday_t;
    gint weekday;

    fcday_tm = *localtime(&now_t);
    fcday_t = time_calc_day(fcday_tm, day);
    weekday = localtime(&fcday_t)->tm_wday;
    switch (day) {
    case 0:
        return g_strdup_printf(_("Today"));
    case 1:
        return g_strdup_printf(_("Tomorrow"));
    default:
        return translate_day(weekday);
    }
}


635 636 637 638 639 640 641 642
static gchar *
forecast_cell_get_tooltip_text(plugin_data *data,
                               xml_time *fcdata)
{
    GString *text;
    gchar *result, *value;
    const gchar *unit;

643 644 645 646
    /* TRANSLATORS: Please use spaces as needed or desired to properly
       align the values; Monospace font is enforced with <tt> tags for
       alignment, and the text is enclosed in <small> tags because
       that looks much better and saves space.
Harald Judt's avatar
Harald Judt committed
647
    */
648
    text = g_string_new(_("<b>Times used for calculations</b>\n"));
649
    value = format_date(fcdata->start, NULL, TRUE);
650 651 652 653
    g_string_append_printf(text, _("<tt><small>"
                                   "Interval start:       %s"
                                   "</small></tt>\n"),
                           value);
654
    g_free(value);
655
    value = format_date(fcdata->end, NULL, TRUE);
656 657 658 659
    g_string_append_printf(text, _("<tt><small>"
                                   "Interval end:         %s"
                                   "</small></tt>\n"),
                           value);
660
    g_free(value);
661
    value = format_date(fcdata->point, NULL, TRUE);
662 663 664 665
    g_string_append_printf(text, _("<tt><small>"
                                   "Data calculated for:  %s"
                                   "</small></tt>\n\n"),
                           value);
666 667
    g_free(value);

Harald Judt's avatar
Harald Judt committed
668
    g_string_append(text, _("<b>Temperatures</b>\n"));
669 670 671 672 673 674 675
    APPEND_TOOLTIP_ITEM(_("<tt><small>"
                          "Dew point:            %s%s%s"
                          "</small></tt>\n"),
                        DEWPOINT);
    APPEND_TOOLTIP_ITEM(_("<tt><small>"
                          "Apparent temperature: %s%s%s"
                          "</small></tt>\n\n"),
676
                        APPARENT_TEMPERATURE);
Harald Judt's avatar
Harald Judt committed
677

678
    g_string_append(text, _("<b>Atmosphere</b>\n"));
679 680 681 682 683 684 685 686
    APPEND_TOOLTIP_ITEM(_("<tt><small>"
                          "Barometric pressure:  %s%s%s"
                          "</small></tt>\n"),
                        PRESSURE);
    APPEND_TOOLTIP_ITEM(_("<tt><small>"
                          "Relative humidity:    %s%s%s"
                          "</small></tt>\n\n"),
                        HUMIDITY);
687

688
    g_string_append(text, _("<b>Precipitation</b>\n"));
689 690 691
    APPEND_TOOLTIP_ITEM(_("<tt><small>"
                          "Amount:        %s%s%s"
                          "</small></tt>\n\n"),
692
                        PRECIPITATION);
693 694

    g_string_append(text, _("<b>Clouds</b>\n"));
695 696 697
    /* TRANSLATORS: Clouds percentages are aligned to the right in the
       tooltip, the %5s are needed for that and are used both for
       rounded and unrounded values. */
698
    APPEND_TOOLTIP_ITEM(_("<tt><small>"
699
                          "Fog:           %5s%s%s"
700 701
                          "</small></tt>\n"), FOG);
    APPEND_TOOLTIP_ITEM(_("<tt><small>"
702
                          "Low clouds:    %5s%s%s"
703 704
                          "</small></tt>\n"), CLOUDS_LOW);
    APPEND_TOOLTIP_ITEM(_("<tt><small>"
705
                          "Middle clouds: %5s%s%s"
706 707
                          "</small></tt>\n"), CLOUDS_MID);
    APPEND_TOOLTIP_ITEM(_("<tt><small>"
708
                          "High clouds:   %5s%s%s"
709 710
                          "</small></tt>\n"), CLOUDS_HIGH);
    APPEND_TOOLTIP_ITEM(_("<tt><small>"
711
                          "Cloudiness:    %5s%s%s"
712
                          "</small></tt>"), CLOUDINESS);
713 714 715 716 717 718 719 720

    /* Free GString only and return its character data */
    result = text->str;
    g_string_free(text, FALSE);
    return result;
}


721 722 723 724
static gchar *
forecast_day_header_tooltip_text(xml_astro *astro)
{
    GString *text;
Harald Judt's avatar
Harald Judt committed
725
    gchar *result, *day, *sunrise, *sunset, *moonrise, *moonset;
726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801

    /* TRANSLATORS: Please use spaces as needed or desired to properly
       align the values; Monospace font is enforced with <tt> tags for
       alignment, and the text is enclosed in <small> tags because
       that looks much better and saves space.
    */

    text = g_string_new("");
    if (astro) {
        day = format_date(astro->day, "%Y-%m-%d", TRUE);
        g_string_append_printf(text, _("<b>%s</b>\n"), day);
        g_free(day);

        if (astro->sun_never_rises)
            g_string_append(text, _("<tt><small>"
                                    "Sunrise: The sun never rises this day."
                                    "</small></tt>\n"));
        else if (astro->sun_never_sets)
            g_string_append(text, _("<tt><small>"
                                    "Sunset: The sun never sets this day."
                                    "</small></tt>\n"));
        else {
            sunrise = format_date(astro->sunrise, NULL, TRUE);
            g_string_append_printf(text, _("<tt><small>"
                                           "Sunrise: %s"
                                           "</small></tt>\n"), sunrise);
            g_free(sunrise);

            sunset = format_date(astro->sunset, NULL, TRUE);
            g_string_append_printf(text, _("<tt><small>"
                                           "Sunset:  %s"
                                           "</small></tt>\n\n"), sunset);
            g_free(sunset);
        }

        if (astro->moon_phase)
            g_string_append_printf(text, _("<tt><small>"
                                           "Moon phase: %s"
                                           "</small></tt>\n"),
                                   translate_moon_phase(astro->moon_phase));
        else
            g_string_append(text, _("<tt><small>"
                                    "Moon phase: Unknown"
                                    "</small></tt>\n"));

        if (astro->moon_never_rises)
            g_string_append(text, _("<tt><small>"
                                    "Moonrise: The moon never rises this day."
                                    "</small></tt>\n"));
        else if (astro->moon_never_sets)
            g_string_append(text,
                            _("<tt><small>"
                              "Moonset: The moon never sets this day."
                              "</small></tt>\n"));
        else {
            moonrise = format_date(astro->moonrise, NULL, TRUE);
            g_string_append_printf(text, _("<tt><small>"
                                           "Moonrise: %s"
                                           "</small></tt>\n"), moonrise);
            g_free(moonrise);

            moonset = format_date(astro->moonset, NULL, TRUE);
            g_string_append_printf(text, _("<tt><small>"
                                           "Moonset:  %s"
                                           "</small></tt>"), moonset);
            g_free(moonset);
        }
    }

    /* Free GString only and return its character data */
    result = text->str;
    g_string_free(text, FALSE);
    return result;
}


802
static GtkWidget *
Harald Judt's avatar
Harald Judt committed
803 804
wrap_forecast_cell(const GtkWidget *widget,
                   const GdkColor *color)
805
{
806
    GtkWidget *ebox;
Harald Judt's avatar
Harald Judt committed
807

808 809 810 811 812 813 814 815 816 817 818
    ebox = gtk_event_box_new();
    if (color == NULL)
        gtk_event_box_set_visible_window(GTK_EVENT_BOX(ebox), FALSE);
    else {
        gtk_event_box_set_visible_window(GTK_EVENT_BOX(ebox), TRUE);
        gtk_widget_modify_bg(GTK_WIDGET(ebox), GTK_STATE_NORMAL, color);
    }
    gtk_container_add(GTK_CONTAINER(ebox), GTK_WIDGET(widget));
    return ebox;
}

819

820
static GtkWidget *
821 822 823
add_forecast_header(const gchar *text,
                    const gdouble angle,
                    const GdkColor *color)
824
{
Sean Davis's avatar
Sean Davis committed
825
    GtkWidget *label;
826 827 828
    gchar *str;

    label = gtk_label_new(NULL);
Harald Judt's avatar
Harald Judt committed
829
    gtk_label_set_angle(GTK_LABEL(label), angle);
830 831 832
    str = g_strdup_printf("<span foreground=\"white\"><b>%s</b></span>", text ? text : "");
    gtk_label_set_markup(GTK_LABEL(label), str);
    g_free(str);
Sean Davis's avatar
Sean Davis committed
833 834 835 836 837 838 839 840 841 842 843

    if (angle) {
        gtk_widget_set_hexpand (GTK_WIDGET (label), FALSE);
        gtk_widget_set_vexpand (GTK_WIDGET (label), TRUE);
    } else {
        gtk_widget_set_hexpand (GTK_WIDGET (label), TRUE);
        gtk_widget_set_vexpand (GTK_WIDGET (label), FALSE);
    }
    weather_widget_set_border_width (GTK_WIDGET (label), 4);

    return wrap_forecast_cell(label, color);
844 845
}

846

847
static GtkWidget *
848
add_forecast_cell(plugin_data *data,
849
                  GArray *daydata,
Harald Judt's avatar
Harald Judt committed
850 851
                  gint day,
                  gint daytime)
852
{
Harald Judt's avatar
Harald Judt committed
853
    GtkWidget *box, *label, *image;
854
    GdkPixbuf *icon;
855
    const GdkColor black = {0, 0x0000, 0x0000, 0x0000};
Harald Judt's avatar
Harald Judt committed
856 857 858
    gchar *wind_speed, *wind_direction, *value, *rawvalue;
    xml_time *fcdata;

859
    box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
Harald Judt's avatar
Harald Judt committed
860

861
    fcdata = make_forecast_data(data->weatherdata, daydata, day, daytime);
Harald Judt's avatar
Harald Judt committed
862 863 864 865 866 867 868 869 870
    if (fcdata == NULL)
        return box;

    if (fcdata->location == NULL) {
        xml_time_free(fcdata);
        return box;
    }

    /* symbol */
871 872
    rawvalue = get_data(fcdata, data->units, SYMBOL,
                        FALSE, data->night_time);
Harald Judt's avatar
Harald Judt committed
873 874 875 876 877 878 879 880
    icon = get_icon(data->icon_theme, rawvalue, 48, (daytime == NIGHT));
    g_free(rawvalue);
    image = gtk_image_new_from_pixbuf(icon);
    gtk_box_pack_start(GTK_BOX(box), GTK_WIDGET(image), TRUE, TRUE, 0);
    if (G_LIKELY(icon))
        g_object_unref(G_OBJECT(icon));

    /* symbol description */
881 882
    rawvalue = get_data(fcdata, data->units, SYMBOL,
                        FALSE, data->night_time);
Harald Judt's avatar
Harald Judt committed
883 884 885 886 887 888 889 890 891 892 893
    value = g_strdup_printf("%s",
                            translate_desc(rawvalue, (daytime == NIGHT)));
    g_free(rawvalue);
    label = gtk_label_new(NULL);
    gtk_label_set_markup(GTK_LABEL(label), value);
    if (!(day % 2))
        gtk_widget_modify_fg(GTK_WIDGET(label), GTK_STATE_NORMAL, &black);
    gtk_box_pack_start(GTK_BOX(box), GTK_WIDGET(label), TRUE, TRUE, 0);
    g_free(value);

    /* temperature */
894 895
    rawvalue = get_data(fcdata, data->units, TEMPERATURE,
                        data->round, data->night_time);
Harald Judt's avatar
Harald Judt committed
896 897 898 899 900 901 902 903 904 905
    value = g_strdup_printf("%s %s", rawvalue,
                            get_unit(data->units, TEMPERATURE));
    g_free(rawvalue);
    label = gtk_label_new(value);
    if (!(day % 2))
        gtk_widget_modify_fg(GTK_WIDGET(label), GTK_STATE_NORMAL, &black);
    gtk_box_pack_start(GTK_BOX(box), GTK_WIDGET(label), TRUE, TRUE, 0);
    g_free(value);

    /* wind direction and speed */
906 907
    wind_direction = get_data(fcdata, data->units, WIND_DIRECTION,
                              FALSE, data->night_time);
908 909
    wind_speed = get_data(fcdata, data->units, WIND_SPEED,
                          data->round, data->night_time);
Harald Judt's avatar
Harald Judt committed
910 911 912 913 914 915 916 917 918 919 920 921
    value = g_strdup_printf("%s %s %s", wind_direction, wind_speed,
                            get_unit(data->units, WIND_SPEED));
    g_free(wind_speed);
    g_free(wind_direction);
    label = gtk_label_new(value);
    if (!(day % 2))
        gtk_widget_modify_fg(GTK_WIDGET(label), GTK_STATE_NORMAL, &black);
    gtk_box_pack_start(GTK_BOX(box), label, TRUE, TRUE, 0);
    g_free(value);

    gtk_widget_set_size_request(GTK_WIDGET(box), 150, -1);

922 923 924 925
    value = forecast_cell_get_tooltip_text(data, fcdata);
    gtk_widget_set_tooltip_markup(GTK_WIDGET(box), value);
    g_free(value);

Harald Judt's avatar
Harald Judt committed
926 927 928 929 930 931
    xml_time_free(fcdata);
    return box;
}


static GtkWidget *
932
make_forecast(plugin_data *data)
Harald Judt's avatar
Harald Judt committed
933
{
Sean Davis's avatar
Sean Davis committed
934
    GtkWidget *grid, *ebox, *box;
Harald Judt's avatar
Harald Judt committed
935
    GtkWidget *forecast_box;
936 937
    const GdkColor lightbg = {0, 0xeaea, 0xeaea, 0xeaea};
    const GdkColor darkbg = {0, 0x6666, 0x6666, 0x6666};
938
    GArray *daydata;
939 940
    xml_astro *astro;
    gchar *dayname, *text;
Harald Judt's avatar
Harald Judt committed
941 942 943
    gint i;
    daytime daytime;

Sean Davis's avatar
Sean Davis committed
944
    grid = gtk_grid_new ();
945

Sean Davis's avatar
Sean Davis committed
946 947
    gtk_grid_set_row_spacing(GTK_GRID (grid), 0);
    gtk_grid_set_column_spacing(GTK_GRID (grid), 0);
948 949

    /* empty upper left corner */
950
    box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
Sean Davis's avatar
Sean Davis committed
951
    gtk_grid_attach (GTK_GRID (grid),
Sean Davis's avatar
Sean Davis committed
952 953
                     wrap_forecast_cell(box, &darkbg),
                     0, 0, 1, 1);
954 955

    /* daytime headers */
Harald Judt's avatar
Harald Judt committed
956 957 958 959
    ATTACH_DAYTIME_HEADER(_("Morning"), 1);
    ATTACH_DAYTIME_HEADER(_("Afternoon"), 2);
    ATTACH_DAYTIME_HEADER(_("Evening"), 3);
    ATTACH_DAYTIME_HEADER(_("Night"), 4);
960

961
    for (i = 0; i < data->forecast_days; i++) {
962
        /* forecast day headers */
Harald Judt's avatar
Harald Judt committed
963 964 965
        dayname = get_dayname(i);
        if (data->forecast_layout == FC_LAYOUT_CALENDAR)
            ebox = add_forecast_header(dayname, 0.0, &darkbg);
966
        else
Harald Judt's avatar
Harald Judt committed
967
            ebox = add_forecast_header(dayname, 90.0, &darkbg);
Harald Judt's avatar
Harald Judt committed
968
        g_free(dayname);
969

970 971 972 973 974
        /* add tooltip to forecast day header */
        astro = get_astro_data_for_day(data->astrodata, i);
        text = forecast_day_header_tooltip_text(astro);
        gtk_widget_set_tooltip_markup(GTK_WIDGET(ebox), text);

Harald Judt's avatar
Harald Judt committed
975
        if (data->forecast_layout == FC_LAYOUT_CALENDAR)
Sean Davis's avatar
Sean Davis committed
976
            gtk_grid_attach (GTK_GRID (grid), GTK_WIDGET(ebox),
Sean Davis's avatar
Sean Davis committed
977
                             i+1, 0, 1, 1);
Harald Judt's avatar
Harald Judt committed
978
        else
Sean Davis's avatar
Sean Davis committed
979
            gtk_grid_attach (GTK_GRID (grid), GTK_WIDGET(ebox),
Sean Davis's avatar
Sean Davis committed
980
                             0, i+1, 1, 1);
981

982 983 984
        /* to speed up things, first get forecast data for all daytimes */
        daydata = get_point_data_for_day(data->weatherdata, i);

985
        /* get forecast data for each daytime */
986
        for (daytime = MORNING; daytime <= NIGHT; daytime++) {
987
            forecast_box = add_forecast_cell(data, daydata, i, daytime);
Sean Davis's avatar
Sean Davis committed
988 989 990 991
            weather_widget_set_border_width (GTK_WIDGET (forecast_box), 4);
            gtk_widget_set_hexpand (GTK_WIDGET (forecast_box), TRUE);
            gtk_widget_set_vexpand (GTK_WIDGET (forecast_box), TRUE);

992
            if (i % 2)
Sean Davis's avatar
Sean Davis committed
993
                ebox = wrap_forecast_cell(forecast_box, NULL);
994
            else
Sean Davis's avatar
Sean Davis committed
995
                ebox = wrap_forecast_cell(forecast_box, &lightbg);
Harald Judt's avatar
Harald Judt committed
996

Harald Judt's avatar
Harald Judt committed
997
            if (data->forecast_layout == FC_LAYOUT_CALENDAR)
Sean Davis's avatar
Sean Davis committed
998
                gtk_grid_attach (GTK_GRID (grid),
Sean Davis's avatar
Sean Davis committed
999 1000
                                 GTK_WIDGET(ebox),
                                 i+1, 1+daytime, 1, 1);
Harald Judt's avatar
Harald Judt committed
1001
            else
Sean Davis's avatar
Sean Davis committed
1002
                gtk_grid_attach (GTK_GRID (grid),
Sean Davis's avatar
Sean Davis committed