~ubuntu-branches/ubuntu/vivid/liferea/vivid-proposed

« back to all changes in this revision

Viewing changes to src/ui/item_list_view.c

  • Committer: Package Import Robot
  • Author(s): bojo42
  • Date: 2012-03-29 14:17:21 UTC
  • mfrom: (1.3.9) (3.2.5 sid)
  • Revision ID: package-import@ubuntu.com-20120329141721-tbfopcrc5797wxt7
Tags: 1.8.3-0.1ubuntu1
* New upstream release (LP: #290666, #371754, #741543, #716688)
* Merge from Debian unstable (LP: #935147), remaining changes:
* debian/patches:
  - drop gtk-status-icon.patch & notification-append as in upstream
  - drop fix_systray_behavior as mostly upstreamed and rest seems unused
  - 01_ubuntu_feedlists: update & rename, move planets to "Open Source"  
  - add_X-Ubuntu-Gettext-Domain: rebase
  - libunity.patch: rebase, apply before indicator patch (liferea_shell.c)
  - libindicate_increase_version.patch: exclude from libindicate.patch
  - deactivate libindicate.patch, seems partly upstreamed and needs rework
* debian/control: libindicate-dev, libindicate-gtk-dev & libunity-dev
* debian/liferea.indicate & liferea.install: ship indicator desktop file
* debian/rules: enable libindicate

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/**
 
2
 * @file item_list_view.c  presenting items in a GtkTreeView
 
3
 *  
 
4
 * Copyright (C) 2004-2011 Lars Lindner <lars.lindner@gmail.com>
 
5
 * Copyright (C) 2004-2006 Nathan J. Conrad <t98502@users.sourceforge.net>
 
6
 *
 
7
 * This program is free software; you can redistribute it and/or modify
 
8
 * it under the terms of the GNU General Public License as published by
 
9
 * the Free Software Foundation; either version 2 of the License, or
 
10
 * (at your option) any later version. 
 
11
 *
 
12
 * This program is distributed in the hope that it will be useful,
 
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
15
 * GNU General Public License for more details.
 
16
 *
 
17
 * You should have received a copy of the GNU General Public License
 
18
 * along with this program; if not, write to the Free Software
 
19
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
20
 */
 
21
 
 
22
#include "ui/item_list_view.h"
 
23
 
 
24
#include <string.h>
 
25
#include <glib.h>
 
26
#include <gdk/gdkkeysyms.h>
 
27
 
 
28
#include "browser.h"
 
29
#include "common.h"
 
30
#include "date.h"
 
31
#include "debug.h"
 
32
#include "feed.h"
 
33
#include "feedlist.h"
 
34
#include "item.h"
 
35
#include "itemlist.h"
 
36
#include "itemview.h"
 
37
#include "newsbin.h"
 
38
#include "social.h"
 
39
#include "ui/browser_tabs.h"
 
40
#include "ui/icons.h"
 
41
#include "ui/liferea_shell.h"
 
42
#include "ui/popup_menu.h"
 
43
#include "ui/ui_common.h"
 
44
 
 
45
/**
 
46
 * Important performance considerations: Early versions had performance problems
 
47
 * with the item list loading because of the following two problems:
 
48
 *
 
49
 * 1.) Mass-adding items to a sorting enabled tree store.
 
50
 * 2.) Mass-loading items to an attached tree store.
 
51
 *
 
52
 * To avoid both problems we merge against a visible tree store only for single
 
53
 * items that are added/removed by background updates and load complete feeds or
 
54
 * collections of feeds only by adding items to a new unattached tree store.
 
55
 */
 
56
 
 
57
/** Enumeration of the columns in the itemstore. */
 
58
enum is_columns {
 
59
        IS_TIME,                /**< Time of item creation */
 
60
        IS_TIME_STR,            /**< Time of item creation as a string*/
 
61
        IS_LABEL,               /**< Displayed name */
 
62
        IS_STATEICON,           /**< Pixbuf reference to the item's state icon */
 
63
        IS_NR,                  /**< Item id, to lookup item ptr from parent feed */
 
64
        IS_PARENT,              /**< Parent node pointer */
 
65
        IS_FAVICON,             /**< Pixbuf reference to the item's feed's icon */
 
66
        IS_ENCICON,             /**< Pixbuf reference to the item's enclosure icon */
 
67
        IS_ENCLOSURE,           /**< Flag wether enclosure is attached or not */
 
68
        IS_SOURCE,              /**< Source node pointer */
 
69
        IS_STATE,               /**< Original item state (unread, flagged...) for sorting */
 
70
        ITEMSTORE_UNREAD,       /**< Flag whether "unread" icon is to be shown */
 
71
        ITEMSTORE_ALIGN,        /**< How to align title (RTL support) */
 
72
        ITEMSTORE_LEN           /**< Number of columns in the itemstore */
 
73
};
 
74
 
 
75
#define ITEM_LIST_VIEW_GET_PRIVATE(object)(G_TYPE_INSTANCE_GET_PRIVATE ((object), ITEM_LIST_VIEW_TYPE, ItemListViewPrivate))
 
76
 
 
77
struct ItemListViewPrivate {
 
78
        GtkTreeView     *treeview;
 
79
        
 
80
        GHashTable      *item_id_to_iter;       /**< hash table used for fast item id->tree iter lookup */
 
81
 
 
82
        gboolean        batch_mode;             /**< TRUE if we are in batch adding mode */
 
83
        GtkTreeStore    *batch_itemstore;       /**< GtkTreeStore prepared unattached and to be set on update() */
 
84
};
 
85
 
 
86
static GObjectClass *parent_class = NULL;
 
87
 
 
88
G_DEFINE_TYPE (ItemListView, item_list_view, G_TYPE_OBJECT);
 
89
 
 
90
static void
 
91
item_list_view_finalize (GObject *object)
 
92
{
 
93
        ItemListViewPrivate *priv = ITEM_LIST_VIEW_GET_PRIVATE (object);
 
94
 
 
95
        if (priv->item_id_to_iter)
 
96
                g_hash_table_destroy (priv->item_id_to_iter);
 
97
        if (priv->batch_itemstore)
 
98
                g_object_unref (priv->batch_itemstore);
 
99
 
 
100
        G_OBJECT_CLASS (parent_class)->finalize (object);
 
101
}
 
102
 
 
103
static void
 
104
item_list_view_class_init (ItemListViewClass *klass)
 
105
{
 
106
        GObjectClass *object_class = G_OBJECT_CLASS (klass);
 
107
 
 
108
        parent_class = g_type_class_peek_parent (klass);
 
109
 
 
110
        object_class->finalize = item_list_view_finalize;
 
111
 
 
112
        g_type_class_add_private (object_class, sizeof(ItemListViewPrivate));
 
113
}
 
114
 
 
115
/* helper functions for item <-> iter conversion */
 
116
 
 
117
gboolean
 
118
item_list_view_contains_id (ItemListView *ilv, gulong id)
 
119
{
 
120
        return (NULL != g_hash_table_lookup (ilv->priv->item_id_to_iter, GUINT_TO_POINTER (id)));
 
121
}
 
122
 
 
123
static gulong
 
124
item_list_view_iter_to_id (ItemListView *ilv, GtkTreeIter *iter)
 
125
{
 
126
        gulong  id = 0;
 
127
        
 
128
        gtk_tree_model_get (gtk_tree_view_get_model (ilv->priv->treeview), iter, IS_NR, &id, -1);
 
129
        return id;
 
130
}
 
131
 
 
132
static gboolean
 
133
item_list_view_id_to_iter (ItemListView *ilv, gulong id, GtkTreeIter *iter)
 
134
{
 
135
        GtkTreeIter *old_iter;
 
136
 
 
137
        old_iter = g_hash_table_lookup (ilv->priv->item_id_to_iter, GUINT_TO_POINTER (id));
 
138
        if (!old_iter)
 
139
                return FALSE;
 
140
        
 
141
        *iter = *old_iter;
 
142
        return TRUE;
 
143
}
 
144
 
 
145
static gint
 
146
item_list_view_date_sort_func (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer user_data)
 
147
{
 
148
        guint64 timea, timeb;
 
149
        double  diff;
 
150
 
 
151
        gtk_tree_model_get (model, a, IS_TIME, &timea, -1);
 
152
        gtk_tree_model_get (model, b, IS_TIME, &timeb, -1);
 
153
        diff = difftime ((time_t)timeb, (time_t)timea);
 
154
        
 
155
        if (diff < 0)
 
156
                return 1;
 
157
                
 
158
        if (diff > 0)
 
159
                return -1;
 
160
        
 
161
        return 0;
 
162
}
 
163
 
 
164
static gint
 
165
item_list_view_favicon_sort_func (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer user_data)
 
166
{
 
167
        nodePtr node1, node2;
 
168
        
 
169
        gtk_tree_model_get (model, a, IS_SOURCE, &node1, -1);
 
170
        gtk_tree_model_get (model, b, IS_SOURCE, &node2, -1);
 
171
        
 
172
        if (!node1->id || !node2->id)
 
173
                return 0;
 
174
                
 
175
        return strcmp (node1->id, node2->id);
 
176
}
 
177
 
 
178
void
 
179
item_list_view_set_sort_column (ItemListView *ilv, nodeViewSortType sortType, gboolean sortReversed)
 
180
{
 
181
        gint sortColumn;
 
182
        
 
183
        switch (sortType) {
 
184
                case NODE_VIEW_SORT_BY_TITLE:
 
185
                        sortColumn = IS_LABEL;
 
186
                        break;
 
187
                case NODE_VIEW_SORT_BY_PARENT:
 
188
                        sortColumn = IS_PARENT;
 
189
                        break;
 
190
                case NODE_VIEW_SORT_BY_STATE:
 
191
                        sortColumn = IS_STATE;
 
192
                        break;
 
193
                case NODE_VIEW_SORT_BY_TIME:
 
194
                default:
 
195
                        sortColumn = IS_TIME;
 
196
                        break;
 
197
        }
 
198
        
 
199
        gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (gtk_tree_view_get_model (ilv->priv->treeview)),
 
200
                                              sortColumn, 
 
201
                                              sortReversed?GTK_SORT_DESCENDING:GTK_SORT_ASCENDING);
 
202
}
 
203
 
 
204
/**
 
205
 * Creates a GtkTreeStore to be filled with ui_itemlist_set_items
 
206
 * and to be set with ui_itemlist_set_tree_store().
 
207
 */
 
208
static GtkTreeStore *
 
209
item_list_view_create_tree_store (void)
 
210
{
 
211
        return gtk_tree_store_new (ITEMSTORE_LEN,
 
212
                            G_TYPE_UINT64,      /* IS_TIME */
 
213
                            G_TYPE_STRING,      /* IS_TIME_STR */
 
214
                            G_TYPE_STRING,      /* IS_LABEL */
 
215
                            GDK_TYPE_PIXBUF,    /* IS_STATEICON */
 
216
                            G_TYPE_ULONG,       /* IS_NR */
 
217
                            G_TYPE_POINTER,     /* IS_PARENT */
 
218
                            GDK_TYPE_PIXBUF,    /* IS_FAVICON */
 
219
                            GDK_TYPE_PIXBUF,    /* IS_ENCICON */
 
220
                            G_TYPE_BOOLEAN,     /* IS_ENCLOSURE */
 
221
                            G_TYPE_POINTER,     /* IS_SOURCE */
 
222
                            G_TYPE_UINT,        /* IS_STATE */
 
223
                            G_TYPE_INT,         /* ITEMSTORE_UNREAD */
 
224
                            G_TYPE_FLOAT        /* ITEMSTORE_ALIGN */
 
225
        );
 
226
}
 
227
 
 
228
/**
 
229
 * Sets a GtkTreeView to the active GtkTreeView.
 
230
 */
 
231
static void
 
232
item_list_view_set_tree_store (ItemListView *ilv, GtkTreeStore *itemstore)
 
233
{
 
234
        GtkTreeModel    *model;
 
235
 
 
236
        /* drop old tree store */
 
237
        model = gtk_tree_view_get_model (ilv->priv->treeview);
 
238
        gtk_tree_view_set_model (ilv->priv->treeview, NULL);
 
239
        if (model)
 
240
                g_object_unref (model);
 
241
        
 
242
        gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE (itemstore), IS_TIME, item_list_view_date_sort_func, NULL, NULL);
 
243
        gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE (itemstore), IS_SOURCE, item_list_view_favicon_sort_func, NULL, NULL);
 
244
        g_signal_connect (G_OBJECT (itemstore), "sort-column-changed", G_CALLBACK (itemlist_sort_column_changed_cb), NULL);
 
245
        
 
246
        gtk_tree_view_set_model (ilv->priv->treeview, GTK_TREE_MODEL (itemstore));
 
247
 
 
248
        item_list_view_prefocus (ilv);
 
249
}
 
250
 
 
251
void
 
252
item_list_view_remove_item (ItemListView *ilv, itemPtr item)
 
253
{
 
254
        GtkTreeIter     *iter;
 
255
 
 
256
        g_assert (NULL != item);
 
257
        iter = g_hash_table_lookup (ilv->priv->item_id_to_iter, GUINT_TO_POINTER (item->id));
 
258
        if (iter) {
 
259
                /* Using the GtkTreeIter check if it is currently selected. If yes,
 
260
                   scroll down by one in the sorted GtkTreeView to ensure something
 
261
                   is selected after removing the GtkTreeIter */
 
262
                if (gtk_tree_selection_iter_is_selected (gtk_tree_view_get_selection (ilv->priv->treeview), iter))
 
263
                        ui_common_treeview_move_cursor (ilv->priv->treeview, 1);
 
264
        
 
265
                gtk_tree_store_remove (GTK_TREE_STORE (gtk_tree_view_get_model (ilv->priv->treeview)), iter);
 
266
                g_hash_table_remove (ilv->priv->item_id_to_iter, GUINT_TO_POINTER (item->id));
 
267
        } else {
 
268
                g_warning ("Fatal: item to be removed not found in iter lookup hash!");
 
269
        }
 
270
}
 
271
 
 
272
/* cleans up the item list, sets up the iter hash when called for the first time */
 
273
void
 
274
item_list_view_clear (ItemListView *ilv)
 
275
{
 
276
        GtkAdjustment           *adj;
 
277
        GtkTreeStore            *itemstore;
 
278
 
 
279
        itemstore = GTK_TREE_STORE (gtk_tree_view_get_model (ilv->priv->treeview));
 
280
        
 
281
        /* unselecting all items is important to remove items
 
282
           whose removal is deferred until unselecting */
 
283
        gtk_tree_selection_unselect_all (gtk_tree_view_get_selection (ilv->priv->treeview));
 
284
        
 
285
        adj = gtk_tree_view_get_vadjustment (ilv->priv->treeview);
 
286
        gtk_adjustment_set_value (adj, 0.0);
 
287
        gtk_tree_view_set_vadjustment (ilv->priv->treeview, adj);
 
288
 
 
289
        if (itemstore)
 
290
                gtk_tree_store_clear (itemstore);
 
291
        if (ilv->priv->item_id_to_iter)
 
292
                g_hash_table_destroy (ilv->priv->item_id_to_iter);
 
293
        
 
294
        ilv->priv->item_id_to_iter = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_free);
 
295
        
 
296
        /* enable batch mode for following item adds */
 
297
        ilv->priv->batch_mode = TRUE;
 
298
        ilv->priv->batch_itemstore = item_list_view_create_tree_store ();
 
299
}
 
300
 
 
301
static gfloat
 
302
item_list_title_alignment (gchar *title)
 
303
{
 
304
        if (!title || strlen(title) == 0)
 
305
                return 0.;
 
306
 
 
307
        /* debug5 (DEBUG_HTML, "title ***%s*** first bytes %02hhx%02hhx%02hhx pango %d",
 
308
                title, title[0], title[1], title[2], pango_find_base_dir (title, -1)); */
 
309
        int txt_direction = pango_find_base_dir (title, -1);
 
310
        int app_direction = gtk_widget_get_default_direction ();
 
311
        if ((txt_direction == PANGO_DIRECTION_LTR &&
 
312
             app_direction == GTK_TEXT_DIR_LTR) ||
 
313
            (txt_direction == PANGO_DIRECTION_RTL &&
 
314
             app_direction == GTK_TEXT_DIR_RTL))
 
315
                return 0.; /* same direction, regular ("left") alignment */
 
316
        else
 
317
                return 1.;
 
318
}
 
319
 
 
320
void
 
321
item_list_view_update_item (ItemListView *ilv, itemPtr item)
 
322
{
 
323
        GtkTreeStore    *itemstore;
 
324
        GtkTreeIter     iter;
 
325
        gchar           *title, *time_str;
 
326
        const GdkPixbuf *state_icon;
 
327
        
 
328
        if (!item_list_view_id_to_iter (ilv, item->id, &iter))
 
329
                return;
 
330
        
 
331
        time_str = (0 != item->time) ? date_format ((time_t)item->time, NULL) : g_strdup ("");
 
332
 
 
333
        title = item->title && strlen (item->title) ? item->title : _("*** No title ***");
 
334
        title = g_strstrip (g_strdup (title));
 
335
 
 
336
        state_icon = item->flagStatus ? icon_get (ICON_FLAG) :
 
337
                     !item->readStatus ? icon_get (ICON_UNREAD) :
 
338
                     NULL;
 
339
 
 
340
        if (ilv->priv->batch_mode)
 
341
                itemstore = ilv->priv->batch_itemstore;
 
342
        else
 
343
                itemstore = GTK_TREE_STORE (gtk_tree_view_get_model (ilv->priv->treeview));
 
344
        gtk_tree_store_set (itemstore,
 
345
                            &iter,
 
346
                            IS_LABEL, title,
 
347
                            IS_TIME_STR, time_str,
 
348
                            IS_STATEICON, state_icon,
 
349
                            ITEMSTORE_UNREAD, item->readStatus ? PANGO_WEIGHT_NORMAL : PANGO_WEIGHT_BOLD,
 
350
                            ITEMSTORE_ALIGN, item_list_title_alignment (title),
 
351
                            -1);
 
352
 
 
353
        g_free (time_str);
 
354
        g_free (title);
 
355
}
 
356
 
 
357
static void
 
358
item_list_view_update_item_foreach (gpointer key,
 
359
                                    gpointer value,
 
360
                                    gpointer user_data)
 
361
{
 
362
        itemPtr         item;
 
363
        
 
364
        item = item_load (GPOINTER_TO_UINT (key) /* id */);
 
365
        if (!item)
 
366
                return;
 
367
 
 
368
        item_list_view_update_item (ITEM_LIST_VIEW (user_data), item);
 
369
        
 
370
        item_unload (item);
 
371
}
 
372
 
 
373
void 
 
374
item_list_view_update_all_items (ItemListView *ilv) 
 
375
{
 
376
        g_hash_table_foreach (ilv->priv->item_id_to_iter, item_list_view_update_item_foreach, (gpointer)ilv);
 
377
}
 
378
 
 
379
void
 
380
item_list_view_update (ItemListView *ilv, gboolean hasEnclosures)
 
381
{
 
382
        /* we depend on the fact that the third column is the enclosure icon column!!! */
 
383
        gtk_tree_view_column_set_visible (gtk_tree_view_get_column (ilv->priv->treeview, 1), hasEnclosures);
 
384
 
 
385
        if (ilv->priv->batch_mode) {
 
386
                item_list_view_set_tree_store (ilv, ilv->priv->batch_itemstore);
 
387
                ilv->priv->batch_mode = FALSE;
 
388
        } else {
 
389
                /* Nothing to do in non-batch mode as items were added
 
390
                   and updated one-by-one in ui_itemlist_add_item() */
 
391
        }
 
392
}
 
393
 
 
394
static gboolean
 
395
on_item_list_view_key_press_event (GtkWidget *widget, GdkEventKey *event, gpointer data) 
 
396
{
 
397
        if ((event->type == GDK_KEY_PRESS) && (event->state == 0) && (event->keyval == GDK_Delete)) 
 
398
                on_remove_item_activate(NULL, NULL);
 
399
 
 
400
        return FALSE;
 
401
}
 
402
 
 
403
/* Show tooltip when headline's column text (IS_LABEL) is truncated. */
 
404
 
 
405
static gint get_cell_renderer_width (GtkWidget *widget, GtkCellRenderer *cell, const gchar *text, gint weight)
 
406
{
 
407
        PangoLayout *layout = gtk_widget_create_pango_layout (widget, text);
 
408
        PangoAttrList *attrbs = pango_attr_list_new();
 
409
        pango_attr_list_insert (attrbs, pango_attr_weight_new (weight));
 
410
        pango_layout_set_attributes (layout, attrbs);
 
411
        pango_attr_list_unref (attrbs);
 
412
        PangoRectangle rect;
 
413
        pango_layout_get_pixel_extents (layout, NULL, &rect);
 
414
        g_object_unref (G_OBJECT (layout));
 
415
        return (cell->xpad * 2) + rect.x + rect.width;
 
416
}
 
417
 
 
418
static gboolean
 
419
on_item_list_view_query_tooltip (GtkWidget *widget, gint x, gint y, gboolean keyboard_mode, GtkTooltip *tooltip, GtkTreeViewColumn *headline_column) 
 
420
{
 
421
        GtkTreeView *view = GTK_TREE_VIEW (widget);
 
422
        GtkTreeModel *model; GtkTreePath *path; GtkTreeIter iter;
 
423
        gboolean ret = FALSE;
 
424
 
 
425
        if (gtk_tree_view_get_tooltip_context (view, &x, &y, keyboard_mode, &model, &path, &iter)) {
 
426
                GtkTreeViewColumn *column;
 
427
                gint bx, by;
 
428
                gtk_tree_view_convert_widget_to_bin_window_coords (view, x, y, &bx, &by);
 
429
                gtk_tree_view_get_path_at_pos (view, x, y, NULL, &column, NULL, NULL);
 
430
 
 
431
                if (column == headline_column) {
 
432
                        GtkCellRenderer *cell;
 
433
                        GList *renderers = gtk_cell_layout_get_cells (GTK_CELL_LAYOUT (column));
 
434
                        cell = GTK_CELL_RENDERER (renderers->data);
 
435
                        g_list_free (renderers);
 
436
 
 
437
                        gchar *text;
 
438
                        gint weight;
 
439
                        gtk_tree_model_get (model, &iter, IS_LABEL, &text, ITEMSTORE_UNREAD, &weight, -1);
 
440
 
 
441
                        gint full_width = get_cell_renderer_width (widget, cell, text, weight);
 
442
                        gint column_width = gtk_tree_view_column_get_width (column);
 
443
                        if (full_width > column_width) {
 
444
                                gtk_tooltip_set_text (tooltip, text);
 
445
                                ret = TRUE;
 
446
                        }
 
447
                        g_free (text);
 
448
                }
 
449
 
 
450
                gtk_tree_view_set_tooltip_row (view, tooltip, path);
 
451
                gtk_tree_path_free (path);
 
452
        }
 
453
        return ret;
 
454
}
 
455
 
 
456
static gboolean
 
457
on_item_list_view_button_press_event (GtkWidget *treeview, GdkEventButton *event, gpointer user_data)
 
458
{
 
459
        ItemListView            *ilv = ITEM_LIST_VIEW (user_data);
 
460
        GtkTreePath             *path;
 
461
        GtkTreeIter             iter;
 
462
        GtkTreeViewColumn       *column;
 
463
        itemPtr                 item = NULL;
 
464
        gboolean                result = FALSE;
 
465
        
 
466
        if (event->type != GDK_BUTTON_PRESS)
 
467
                return FALSE;
 
468
 
 
469
        /* avoid handling header clicks */
 
470
        if (event->window != gtk_tree_view_get_bin_window (ilv->priv->treeview))
 
471
                return FALSE;
 
472
 
 
473
        if (!gtk_tree_view_get_path_at_pos (ilv->priv->treeview, (gint)event->x, (gint)event->y, &path, NULL, NULL, NULL))
 
474
                return FALSE;
 
475
 
 
476
        if (gtk_tree_model_get_iter (gtk_tree_view_get_model (ilv->priv->treeview), &iter, path))
 
477
                item = item_load (item_list_view_iter_to_id (ilv, &iter));
 
478
                
 
479
        gtk_tree_path_free (path);
 
480
        
 
481
        if (item) {
 
482
                GdkEventButton *eb = (GdkEventButton*)event; 
 
483
                switch (eb->button) {
 
484
                        case 1:
 
485
                                column = gtk_tree_view_get_column (ilv->priv->treeview, 0);
 
486
                                if (column) {
 
487
                                        /* Allow flag toggling when left clicking in the flagging column.
 
488
                                           We depend on the fact that the state column is the first!!! */
 
489
                                        if (event->x <= gtk_tree_view_column_get_width (column)) {
 
490
                                                itemlist_toggle_flag (item);
 
491
                                                result = TRUE;
 
492
                                        }
 
493
                                }
 
494
                                break;
 
495
                        case 2:
 
496
                                /* Middle mouse click toggles read status... */
 
497
                                itemlist_toggle_read_status (item);
 
498
                                result = TRUE;
 
499
                                break;
 
500
                        case 3:
 
501
                                item_list_view_select (ilv, item);
 
502
                                ui_popup_item_menu (item, eb->button, eb->time);
 
503
                                result = TRUE;
 
504
                                break;
 
505
                }
 
506
                item_unload (item);
 
507
        }
 
508
                
 
509
        return result;
 
510
}
 
511
 
 
512
static gboolean
 
513
on_item_list_view_popup_menu (GtkWidget *widget, gpointer user_data)
 
514
{
 
515
        GtkTreeView     *treeview = GTK_TREE_VIEW (widget);
 
516
        GtkTreeModel    *model;
 
517
        GtkTreeIter     iter;
 
518
 
 
519
        if (gtk_tree_selection_get_selected (gtk_tree_view_get_selection (treeview), &model, &iter)) {
 
520
                itemPtr item = item_load (item_list_view_iter_to_id (ITEM_LIST_VIEW (user_data), &iter));
 
521
                ui_popup_item_menu (item, 3, 0);
 
522
                item_unload (item);
 
523
                return TRUE;
 
524
        }
 
525
 
 
526
        return FALSE;
 
527
}
 
528
 
 
529
GtkTreeView *
 
530
item_list_view_get_widget (ItemListView *ilv)
 
531
{
 
532
        return ITEM_LIST_VIEW_GET_PRIVATE (ilv)->treeview;
 
533
}
 
534
 
 
535
static void
 
536
item_list_view_init (ItemListView *ilv)
 
537
{
 
538
        ilv->priv = ITEM_LIST_VIEW_GET_PRIVATE (ilv);
 
539
        ilv->priv->item_id_to_iter = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_free);
 
540
}
 
541
 
 
542
ItemListView *
 
543
item_list_view_create (GtkWidget *window) 
 
544
{
 
545
        ItemListView            *ilv;
 
546
        GtkCellRenderer         *renderer;
 
547
        GtkTreeViewColumn       *column, *headline_column;
 
548
        GtkTreeSelection        *select;
 
549
        GtkWidget               *ilscrolledwindow;
 
550
 
 
551
        ilv = g_object_new (ITEM_LIST_VIEW_TYPE, NULL);
 
552
                
 
553
        ilscrolledwindow = gtk_scrolled_window_new (NULL, NULL);
 
554
        gtk_widget_show (ilscrolledwindow);
 
555
        gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (ilscrolledwindow), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
 
556
        gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (ilscrolledwindow), GTK_SHADOW_IN);
 
557
 
 
558
        ilv->priv->treeview = GTK_TREE_VIEW (gtk_tree_view_new ());
 
559
        gtk_container_add (GTK_CONTAINER (ilscrolledwindow), GTK_WIDGET (ilv->priv->treeview));
 
560
        gtk_widget_show (GTK_WIDGET (ilv->priv->treeview));
 
561
        gtk_widget_set_name (GTK_WIDGET (ilv->priv->treeview), "itemlist");
 
562
        gtk_tree_view_set_rules_hint (ilv->priv->treeview, TRUE);
 
563
        
 
564
        g_object_set_data (G_OBJECT (window), "itemlist", ilv->priv->treeview);
 
565
 
 
566
        item_list_view_set_tree_store (ilv, item_list_view_create_tree_store ());
 
567
 
 
568
        renderer = gtk_cell_renderer_pixbuf_new ();
 
569
        column = gtk_tree_view_column_new_with_attributes ("", renderer, "pixbuf", IS_STATEICON, NULL);
 
570
        gtk_tree_view_append_column (ilv->priv->treeview, column);
 
571
        gtk_tree_view_column_set_sort_column_id (column, IS_STATE);     
 
572
        
 
573
        renderer = gtk_cell_renderer_pixbuf_new ();
 
574
        column = gtk_tree_view_column_new_with_attributes ("", renderer, "pixbuf", IS_ENCICON, NULL);
 
575
        gtk_tree_view_append_column (ilv->priv->treeview, column);
 
576
 
 
577
        renderer = gtk_cell_renderer_text_new ();
 
578
        column = gtk_tree_view_column_new_with_attributes (_("Date"), renderer, 
 
579
                                                           "text", IS_TIME_STR,
 
580
                                                           "weight", ITEMSTORE_UNREAD,
 
581
                                                           NULL);
 
582
        gtk_tree_view_append_column (ilv->priv->treeview, column);
 
583
        gtk_tree_view_column_set_sort_column_id(column, IS_TIME);
 
584
        g_object_set (column, "resizable", TRUE, NULL);
 
585
        
 
586
        renderer = gtk_cell_renderer_pixbuf_new ();
 
587
        column = gtk_tree_view_column_new_with_attributes ("", renderer, "pixbuf", IS_FAVICON, NULL);
 
588
        gtk_tree_view_column_set_sort_column_id (column, IS_SOURCE);
 
589
        gtk_tree_view_append_column (ilv->priv->treeview, column);
 
590
        
 
591
        renderer = gtk_cell_renderer_text_new ();
 
592
        headline_column = gtk_tree_view_column_new_with_attributes (_("Headline"), renderer, 
 
593
                                                           "text", IS_LABEL,
 
594
                                                           "weight", ITEMSTORE_UNREAD,
 
595
                                                           "xalign", ITEMSTORE_ALIGN,
 
596
                                                           NULL);
 
597
        gtk_tree_view_append_column (ilv->priv->treeview, headline_column);
 
598
        gtk_tree_view_column_set_sort_column_id (headline_column, IS_LABEL);
 
599
        g_object_set (headline_column, "resizable", TRUE, NULL);
 
600
        g_object_set (renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
 
601
 
 
602
        /* And connect signals */
 
603
        g_signal_connect (G_OBJECT (ilv->priv->treeview), "button_press_event", G_CALLBACK (on_item_list_view_button_press_event), ilv);
 
604
        g_signal_connect (G_OBJECT (ilv->priv->treeview), "row_activated", G_CALLBACK (on_Itemlist_row_activated), ilv);
 
605
        g_signal_connect (G_OBJECT (ilv->priv->treeview), "key-press-event", G_CALLBACK (on_item_list_view_key_press_event), ilv);
 
606
        g_signal_connect (G_OBJECT (ilv->priv->treeview), "popup_menu", G_CALLBACK (on_item_list_view_popup_menu), ilv);
 
607
 
 
608
        gtk_widget_set_has_tooltip (GTK_WIDGET (ilv->priv->treeview), TRUE);
 
609
        g_signal_connect (G_OBJECT (ilv->priv->treeview), "query-tooltip", G_CALLBACK (on_item_list_view_query_tooltip), headline_column);
 
610
 
 
611
        /* Setup the selection handler */
 
612
        select = gtk_tree_view_get_selection (ilv->priv->treeview);
 
613
        gtk_tree_selection_set_mode (select, GTK_SELECTION_SINGLE);
 
614
        g_signal_connect (G_OBJECT (select), "changed",
 
615
                          G_CALLBACK (on_itemlist_selection_changed), ilv);
 
616
                  
 
617
        return ilv;
 
618
}
 
619
 
 
620
/* typically called when filling the item tree view */
 
621
void 
 
622
item_list_view_prefocus (ItemListView *ilv)
 
623
{
 
624
        GtkWidget               *focus_widget;
 
625
        GtkTreeSelection        *itemselection;
 
626
        
 
627
        /* the following is important to prevent setting the unread
 
628
           flag for the first item in the item list when the user does
 
629
           the first click into the treeview, if we don't do a focus and
 
630
           unselect, GTK would always (exception: clicking on first item)
 
631
           generate two selection-change events (one for the clicked and
 
632
           one for the selected item)!!! */
 
633
 
 
634
        /* we need to restore the focus after we temporarily select the itemlist */
 
635
        focus_widget = gtk_window_get_focus (GTK_WINDOW (liferea_shell_get_window ()));
 
636
 
 
637
        /* prevent marking as unread before focussing, which leads to a selection */
 
638
        gtk_widget_grab_focus (GTK_WIDGET (ilv->priv->treeview));
 
639
 
 
640
        itemselection = gtk_tree_view_get_selection (ilv->priv->treeview);
 
641
        if (itemselection)
 
642
                gtk_tree_selection_unselect_all (itemselection);
 
643
        
 
644
        if (focus_widget)
 
645
                gtk_widget_grab_focus (focus_widget);
 
646
}
 
647
 
 
648
static void
 
649
item_list_view_add_item_to_tree_store (ItemListView *ilv, GtkTreeStore *itemstore, itemPtr item)
 
650
{
 
651
        gint            state = 0;
 
652
        nodePtr         node;
 
653
        GtkTreeIter     *iter;
 
654
        GtkTreeIter     old_iter;
 
655
        gboolean        exists;
 
656
                
 
657
        if (item->flagStatus)
 
658
                state += 2;
 
659
        if (!item->readStatus)
 
660
                state += 1;
 
661
                
 
662
        node = node_from_id (item->nodeId);
 
663
        if(!node)
 
664
                return; /* comment items do cause this... maybe filtering them earlier would be a good idea... */
 
665
                
 
666
        exists = item_list_view_id_to_iter (ilv, item->id, &old_iter);
 
667
        iter = &old_iter;
 
668
        
 
669
        if (!exists) 
 
670
        {
 
671
                iter = g_new0 (GtkTreeIter, 1);
 
672
                gtk_tree_store_prepend (itemstore, iter, NULL);
 
673
                g_hash_table_insert (ilv->priv->item_id_to_iter, GUINT_TO_POINTER (item->id), (gpointer)iter);
 
674
        }
 
675
 
 
676
        gtk_tree_store_set (itemstore, iter,
 
677
                                       IS_TIME, (guint64)item->time,
 
678
                                       IS_NR, item->id,
 
679
                                       IS_PARENT, node,
 
680
                                       IS_FAVICON, node->icon,
 
681
                                       IS_ENCICON, item->hasEnclosure?icon_get (ICON_ENCLOSURE):NULL,
 
682
                                       IS_ENCLOSURE, item->hasEnclosure,
 
683
                                       IS_SOURCE, node,
 
684
                                       IS_STATE, state,
 
685
                                       -1);                                    
 
686
}
 
687
 
 
688
void 
 
689
item_list_view_add_item (ItemListView *ilv, itemPtr item)
 
690
{
 
691
        GtkTreeStore    *itemstore;
 
692
 
 
693
        if (ilv->priv->batch_mode) {
 
694
                /* either merge to new unattached GtkTreeStore */
 
695
                item_list_view_add_item_to_tree_store (ilv, ilv->priv->batch_itemstore, item);
 
696
        } else {
 
697
                /* or merge to visible item store */    
 
698
                itemstore = GTK_TREE_STORE (gtk_tree_view_get_model (ilv->priv->treeview));
 
699
                item_list_view_add_item_to_tree_store (ilv, itemstore, item);
 
700
        }
 
701
        
 
702
        item_list_view_update_item (ilv, item);
 
703
}
 
704
 
 
705
void
 
706
item_list_view_enable_favicon_column (ItemListView *ilv, gboolean enabled)
 
707
{
 
708
        /* we depend on the fact that the second column is the favicon column!!! 
 
709
           if we are in search mode or have a folder or vfolder we show the favicon 
 
710
           column to give a hint where the item comes from ... */
 
711
        gtk_tree_view_column_set_visible (gtk_tree_view_get_column (ilv->priv->treeview, 3), enabled);
 
712
}
 
713
 
 
714
void
 
715
on_popup_launchitem_selected (void) 
 
716
{
 
717
        itemPtr         item;
 
718
 
 
719
        item = itemlist_get_selected ();
 
720
        if (item) {
 
721
                gchar *link = item_make_link (item);
 
722
 
 
723
                itemview_launch_URL (link, FALSE);
 
724
 
 
725
                g_free (link);
 
726
                item_unload (item);
 
727
        } else {
 
728
                liferea_shell_set_important_status_bar (_("No item has been selected"));
 
729
        }
 
730
}
 
731
 
 
732
void
 
733
on_popup_launchitem_in_tab_selected (void) 
 
734
{
 
735
        itemPtr         item;
 
736
        gchar           *link;
 
737
 
 
738
        item = itemlist_get_selected ();
 
739
        if (item) {
 
740
                link = item_make_link (item);
 
741
                if (link) {
 
742
                        browser_tabs_add_new (link, link, FALSE);
 
743
                        g_free (link);
 
744
                } else
 
745
                        ui_show_error_box (_("This item has no link specified!"));
 
746
                        
 
747
                item_unload (item);
 
748
        } else {
 
749
                liferea_shell_set_important_status_bar (_("No item has been selected"));
 
750
        }
 
751
}
 
752
 
 
753
void 
 
754
on_Itemlist_row_activated (GtkTreeView *treeview,
 
755
                           GtkTreePath *path,
 
756
                           GtkTreeViewColumn *column,
 
757
                           gpointer user_data) 
 
758
{
 
759
 
 
760
        on_popup_launchitem_selected ();
 
761
}
 
762
 
 
763
/* menu callbacks */
 
764
 
 
765
void
 
766
on_toggle_item_flag (GtkMenuItem *menuitem, gpointer user_data) 
 
767
{
 
768
        itemPtr         item;
 
769
 
 
770
        item = itemlist_get_selected ();
 
771
        if (item) {
 
772
                itemlist_toggle_flag (item);
 
773
                item_unload (item);
 
774
        }
 
775
}
 
776
 
 
777
void 
 
778
on_popup_toggle_flag (void)
 
779
{
 
780
        on_toggle_item_flag (NULL, NULL);
 
781
}
 
782
 
 
783
void 
 
784
on_toggle_unread_status (GtkMenuItem *menuitem, gpointer user_data) 
 
785
{
 
786
        itemPtr         item;
 
787
 
 
788
        item = itemlist_get_selected ();
 
789
        if (item) {
 
790
                itemlist_toggle_read_status (item);
 
791
                item_unload (item);
 
792
        }
 
793
}
 
794
 
 
795
void
 
796
on_popup_toggle_read (void)
 
797
{
 
798
        on_toggle_unread_status (NULL, NULL);
 
799
}
 
800
 
 
801
void
 
802
on_remove_items_activate (GtkMenuItem *menuitem, gpointer user_data)
 
803
{
 
804
        nodePtr         node;
 
805
        
 
806
        node = feedlist_get_selected ();
 
807
        // FIXME: use node type capability check
 
808
        if (node && (IS_FEED (node) || IS_NEWSBIN (node)))
 
809
                itemlist_remove_all_items (node);
 
810
        else
 
811
                ui_show_error_box (_("You must select a feed to delete its items!"));
 
812
}
 
813
 
 
814
void
 
815
on_remove_item_activate (GtkMenuItem *menuitem, gpointer user_data)
 
816
{
 
817
        itemPtr         item;
 
818
        
 
819
        item = itemlist_get_selected ();
 
820
        if (item) {
 
821
                itemlist_remove_item (item);
 
822
        } else {
 
823
                liferea_shell_set_important_status_bar (_("No item has been selected"));
 
824
        }
 
825
}
 
826
 
 
827
void
 
828
on_popup_remove_selected (void)
 
829
{
 
830
        on_remove_item_activate (NULL, NULL);
 
831
}
 
832
 
 
833
void
 
834
item_list_view_select (ItemListView *ilv, itemPtr item)
 
835
{
 
836
        GtkTreeView             *treeview = ilv->priv->treeview;
 
837
        GtkTreeSelection        *selection;
 
838
        
 
839
        selection = gtk_tree_view_get_selection (treeview);
 
840
        
 
841
        if (item) {
 
842
                GtkTreeIter             iter;
 
843
                GtkTreePath             *path;
 
844
                
 
845
                if (!item_list_view_id_to_iter(ilv, item->id, &iter))
 
846
                        /* This is an evil hack to fix SF #1870052: crash
 
847
                           upon hitting <enter> when no headline selected.
 
848
                           FIXME: This code is rotten! Rewrite it! Now! */
 
849
                        itemlist_selection_changed (NULL);
 
850
 
 
851
                path = gtk_tree_model_get_path (gtk_tree_view_get_model (treeview), &iter);
 
852
                gtk_tree_view_set_cursor (treeview, path, NULL, FALSE);
 
853
                gtk_tree_view_scroll_to_cell (treeview, path, NULL, FALSE, 0.0, 0.0);
 
854
                gtk_tree_path_free (path);
 
855
        } else {
 
856
                gtk_tree_selection_unselect_all (selection);
 
857
        }
 
858
}
 
859
 
 
860
itemPtr
 
861
item_list_view_find_unread_item (ItemListView *ilv, gulong startId)
 
862
{
 
863
        GtkTreeIter             iter;
 
864
        GtkTreeModel            *model;
 
865
        gboolean                valid = TRUE;
 
866
        
 
867
        model = gtk_tree_view_get_model (ilv->priv->treeview);
 
868
        
 
869
        if (startId)
 
870
                valid = item_list_view_id_to_iter (ilv, startId, &iter);
 
871
        else
 
872
                valid = gtk_tree_model_get_iter_first (model, &iter);
 
873
        
 
874
        while (valid) {
 
875
                itemPtr item = item_load (item_list_view_iter_to_id (ilv, &iter));
 
876
                if (item) {
 
877
                        if (!item->readStatus)
 
878
                                return item;
 
879
                        item_unload (item);
 
880
                }
 
881
                valid = gtk_tree_model_iter_next (model, &iter);
 
882
        }
 
883
 
 
884
        return NULL;
 
885
}
 
886
 
 
887
void
 
888
on_next_unread_item_activate (GtkMenuItem *menuitem, gpointer user_data)
 
889
{
 
890
        itemlist_select_next_unread ();
 
891
}
 
892
 
 
893
void
 
894
on_popup_next_unread_item_selected (gpointer callback_data, guint callback_action, GtkWidget *widget)
 
895
{
 
896
        itemlist_select_next_unread ();
 
897
}
 
898
 
 
899
void
 
900
on_nextbtn_clicked (GtkButton *button, gpointer user_data)
 
901
{
 
902
        itemlist_select_next_unread ();
 
903
}
 
904
 
 
905
void
 
906
on_popup_copy_URL_clipboard (void)
 
907
{
 
908
        itemPtr         item;
 
909
 
 
910
        item = itemlist_get_selected ();
 
911
        if (item) {
 
912
                gchar *link = item_make_link (item);
 
913
 
 
914
                gtk_clipboard_set_text (gtk_clipboard_get (GDK_SELECTION_PRIMARY), link, -1);
 
915
                gtk_clipboard_set_text (gtk_clipboard_get (GDK_SELECTION_CLIPBOARD), link, -1);
 
916
 
 
917
                g_free (link);
 
918
                item_unload (item);
 
919
        } else {
 
920
                liferea_shell_set_important_status_bar (_("No item has been selected"));
 
921
        }
 
922
}
 
923
 
 
924
void
 
925
on_popup_social_bm_item_selected (void)
 
926
{
 
927
        itemPtr item;
 
928
 
 
929
        item = itemlist_get_selected ();
 
930
        if (item) {
 
931
                social_add_bookmark (item);
 
932
                item_unload (item);
 
933
        }
 
934
        else
 
935
                liferea_shell_set_important_status_bar (_("No item has been selected"));
 
936
}
 
937
 
 
938
void
 
939
on_itemlist_selection_changed (GtkTreeSelection *selection, gpointer user_data)
 
940
{
 
941
        GtkTreeIter     iter;
 
942
        GtkTreeModel    *model;
 
943
        itemPtr         item = NULL;
 
944
 
 
945
        if (gtk_tree_selection_get_selected (selection, &model, &iter))
 
946
                item = item_load (item_list_view_iter_to_id (ITEM_LIST_VIEW (user_data), &iter));
 
947
 
 
948
        liferea_shell_update_item_menu (NULL != item);
 
949
        if (item)
 
950
                itemlist_selection_changed (item);
 
951
}
 
952
 
 
953
void
 
954
itemlist_sort_column_changed_cb (GtkTreeSortable *treesortable, gpointer user_data)
 
955
{
 
956
        gint            sortColumn, nodeSort;
 
957
        GtkSortType     sortType;
 
958
        gboolean        sorted, changed;
 
959
        
 
960
        if (feedlist_get_selected () == NULL)
 
961
                return;
 
962
        
 
963
        sorted = gtk_tree_sortable_get_sort_column_id (treesortable, &sortColumn, &sortType);
 
964
        if (!sorted)
 
965
                return;
 
966
                
 
967
        switch (sortColumn) {
 
968
                case IS_TIME:
 
969
                default:
 
970
                        nodeSort = NODE_VIEW_SORT_BY_TIME;
 
971
                        break;
 
972
                case IS_LABEL:
 
973
                        nodeSort = NODE_VIEW_SORT_BY_TITLE;
 
974
                        break;
 
975
                case IS_STATE:
 
976
                        nodeSort = NODE_VIEW_SORT_BY_STATE;
 
977
                        break;
 
978
                case IS_PARENT:
 
979
                case IS_SOURCE:
 
980
                        nodeSort = NODE_VIEW_SORT_BY_PARENT;
 
981
                        break;
 
982
        }
 
983
 
 
984
        changed = node_set_sort_column (feedlist_get_selected (), nodeSort, sortType == GTK_SORT_DESCENDING);
 
985
        if (changed)
 
986
                feedlist_schedule_save ();
 
987
}