1 /*
2  * Copyright © 2004-2010 Jens Oknelid, paskharen@gmail.com
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17  *
18  * In addition, as a special exception, compiling, linking, and/or
19  * using OpenSSL with this program is allowed.
20  */
21 
22 #include "treeview.hh"
23 #include "settingsmanager.hh"
24 #include "WulforUtil.hh"
25 #include <glib/gi18n.h>
26 
27 using namespace std;
28 
TreeView()29 TreeView::TreeView()
30 {
31     view = NULL;
32     count = 0;
33     padding = false;
34     gtypes = NULL;
35 }
36 
~TreeView()37 TreeView::~TreeView()
38 {
39     if (!name.empty())
40         saveSettings();
41     if (gtypes)
42         delete [] gtypes;
43     g_object_unref(menu);
44 }
45 
setView(GtkTreeView * view)46 void TreeView::setView(GtkTreeView *view)
47 {
48     this->view = view;
49     gtk_tree_view_set_headers_clickable(view, TRUE);
50     gtk_tree_view_set_rubber_banding(view, TRUE);
51 }
52 
setView(GtkTreeView * view,bool padding,const string & name)53 void TreeView::setView(GtkTreeView *view, bool padding, const string &name)
54 {
55     this->view = view;
56     this->padding = padding;
57     this->name = name;
58     gtk_tree_view_set_headers_clickable(view, TRUE);
59     gtk_tree_view_set_rubber_banding(view, TRUE);
60 }
61 
get()62 GtkTreeView *TreeView::get()
63 {
64     return view;
65 }
66 
67 /*
68  * We can't use getValue() for strings since it would cause a memory leak.
69  */
getString(GtkTreeIter * i,const string & column,GtkTreeModel * m)70 string TreeView::getString(GtkTreeIter *i, const string &column, GtkTreeModel *m)
71 {
72     if(!m)
73         m = gtk_tree_view_get_model(view);
74     string value;
75     gchar* temp;
76     dcassert(gtk_tree_model_get_column_type(m, col(column)) == G_TYPE_STRING);
77     gtk_tree_model_get(m, i, col(column), &temp, -1);
78 
79     if (temp)
80     {
81         value = string(temp);
82         g_free(temp);
83     }
84 
85     return value;
86 }
87 
insertColumn(const string & title,const GType & gtype,const columnType type,const int width,const string & linkedCol)88 void TreeView::insertColumn(const string &title, const GType &gtype, const columnType type, const int width, const string &linkedCol)
89 {
90     // All insertColumn's have to be called before any insertHiddenColumn's.
91     dcassert(hiddenColumns.empty());
92 
93     // Title must be unique.
94     dcassert(!title.empty() && columns.find(title) == columns.end());
95 
96     columns[title] = Column(title, count, gtype, type, width, linkedCol);
97     sortedColumns[count] = title;
98     ++count;
99 }
100 
insertColumn(const string & title,const GType & gtype,const columnType type,const int width,const string & linkedCol,const string & linkedTextColor)101 void TreeView::insertColumn(const string &title, const GType &gtype, const columnType type, const int width,
102     const string &linkedCol, const string &linkedTextColor)
103 {
104     // All insertColumn's have to be called before any insertHiddenColumn's.
105     dcassert(hiddenColumns.empty());
106 
107     // Title must be unique.
108     dcassert(!title.empty() && columns.find(title) == columns.end());
109 
110     columns[title] = Column(title, count, gtype, type, width, linkedCol, linkedTextColor);
111     sortedColumns[count] = title;
112     ++count;
113 }
114 
insertHiddenColumn(const string & title,const GType & gtype)115 void TreeView::insertHiddenColumn(const string &title, const GType &gtype)
116 {
117     // Title must be unique.
118     dcassert(!title.empty());
119     dcassert(hiddenColumns.find(title) == hiddenColumns.end());
120     dcassert(columns.find(title) == columns.end());
121 
122     hiddenColumns[title] = Column(title, count, gtype);
123     sortedHiddenColumns[count] = title;
124     ++count;
125 }
126 
finalize()127 void TreeView::finalize()
128 {
129     dcassert(count > 0);
130 
131     menu = GTK_MENU(gtk_menu_new());
132     g_object_ref_sink(menu);
133     visibleColumns = columns.size();
134 
135     if (!name.empty())
136         restoreSettings();
137 
138     for (SortedColIter iter = sortedColumns.begin(); iter != sortedColumns.end(); ++iter)
139     {
140         Column& col = columns[iter->second];
141         addColumn_gui(col);
142 
143         colMenuItems[col.title] = gtk_check_menu_item_new_with_label(col.title.c_str());
144         gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(colMenuItems[col.title]), col.visible);
145         g_signal_connect(colMenuItems[col.title], "activate", G_CALLBACK(toggleColumnVisibility), (gpointer)this);
146         gtk_menu_shell_append(GTK_MENU_SHELL(menu), colMenuItems[col.title]);
147 
148         if (!col.visible)
149             visibleColumns--;
150     }
151 
152     if (padding)
153     {
154         GtkTreeViewColumn *col = gtk_tree_view_column_new();
155         // Set to fixed so that gtk_tree_view_set_fixed_height() doesn't complain.
156         gtk_tree_view_column_set_sizing(col, GTK_TREE_VIEW_COLUMN_FIXED);
157         gtk_tree_view_insert_column(view, col, count);
158     }
159 }
160 
161 /*
162  * This is the total number of columns, including hidden columns.
163  */
getColCount()164 int TreeView::getColCount()
165 {
166     return count;
167 }
168 
169 /*
170  * Slow method. Shouldn't be used unless necessary.
171  */
getRowCount()172 int TreeView::getRowCount()
173 {
174     GtkTreeIter iter;
175     GtkTreeModel *m = gtk_tree_view_get_model(view);
176     int numRows = 0;
177     gboolean valid = gtk_tree_model_get_iter_first(m, &iter);
178 
179     while (valid)
180     {
181         ++numRows;
182         valid = gtk_tree_model_iter_next(m, &iter);
183     }
184 
185     return numRows;
186 }
187 
getGTypes()188 GType* TreeView::getGTypes()
189 {
190     int i = 0;
191     if (gtypes)
192         return gtypes;
193     gtypes = new GType[count];
194 
195     for (SortedColIter iter = sortedColumns.begin(); iter != sortedColumns.end(); ++iter)
196         gtypes[i++] = columns[iter->second].gtype;
197     for (SortedColIter iter = sortedHiddenColumns.begin(); iter != sortedHiddenColumns.end(); ++iter)
198         gtypes[i++] = hiddenColumns[iter->second].gtype;
199 
200     return gtypes;
201 }
202 
speedDataFunc(GtkTreeViewColumn * col,GtkCellRenderer * renderer,GtkTreeModel * model,GtkTreeIter * iter,gpointer column)203 void TreeView::speedDataFunc(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer column)
204 {
205     string speedString;
206     int64_t speed;
207     gtk_tree_model_get(model, iter, static_cast<Column*>(column)->pos, &speed, -1);
208 
209     if (speed >= 0)
210     {
211         speedString = dcpp::Util::formatBytes(speed) + "/" + _("s");
212     }
213 
214     g_object_set(renderer, "text", speedString.c_str(), NULL);
215 }
216 
sizeDataFunc(GtkTreeViewColumn * col,GtkCellRenderer * renderer,GtkTreeModel * model,GtkTreeIter * iter,gpointer column)217 void TreeView::sizeDataFunc(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer column)
218 {
219     string sizeString;
220     int64_t size;
221     gtk_tree_model_get(model, iter, static_cast<Column*>(column)->pos, &size, -1);
222 
223     if (size >= 0)
224     {
225         sizeString = dcpp::Util::formatBytes(size);
226     }
227 
228     g_object_set(renderer, "text", sizeString.c_str(), NULL);
229 }
230 
timeLeftDataFunc(GtkTreeViewColumn * col,GtkCellRenderer * renderer,GtkTreeModel * model,GtkTreeIter * iter,gpointer column)231 void TreeView::timeLeftDataFunc(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer column)
232 {
233     string timeLeftString;
234     int64_t seconds;
235     gtk_tree_model_get(model, iter, static_cast<Column*>(column)->pos, &seconds, -1);
236 
237     if (seconds >= 0)
238     {
239         timeLeftString = dcpp::Util::formatSeconds(seconds);
240     }
241 
242     g_object_set(renderer, "text", timeLeftString.c_str(), NULL);
243 }
244 
addColumn_gui(Column & column)245 void TreeView::addColumn_gui(Column& column)
246 {
247     GtkTreeViewColumn *col = NULL;
248     GtkCellRenderer *renderer;
249 
250     switch (column.type)
251     {
252         case INT:
253         case STRING:
254             col = gtk_tree_view_column_new_with_attributes(column.title.c_str(),
255                 gtk_cell_renderer_text_new(), "text", column.pos, NULL);
256             break;
257         case SIZE:
258             renderer = gtk_cell_renderer_text_new();
259             col = gtk_tree_view_column_new_with_attributes(column.title.c_str(),
260                     renderer, "text", column.pos, NULL);
261             gtk_tree_view_column_set_cell_data_func(col, renderer, TreeView::sizeDataFunc, &column, NULL);
262             break;
263         case SPEED:
264             renderer = gtk_cell_renderer_text_new();
265             col = gtk_tree_view_column_new_with_attributes(column.title.c_str(),
266                     renderer, "text", column.pos, NULL);
267             gtk_tree_view_column_set_cell_data_func(col, renderer, TreeView::speedDataFunc, &column, NULL);
268             break;
269         case TIME_LEFT:
270             renderer = gtk_cell_renderer_text_new();
271             col = gtk_tree_view_column_new_with_attributes(column.title.c_str(),
272                     renderer, "text", column.pos, NULL);
273             gtk_tree_view_column_set_cell_data_func(col, renderer, TreeView::timeLeftDataFunc, &column, NULL);
274             break;
275         case STRINGR:
276             renderer = gtk_cell_renderer_text_new();
277             g_object_set(renderer, "xalign", 1.0, NULL);
278             col = gtk_tree_view_column_new_with_attributes(column.title.c_str(), renderer, "text", column.pos, NULL);
279             gtk_tree_view_column_set_alignment(col, 1.0);
280             break;
281         case BOOL:
282             renderer = gtk_cell_renderer_toggle_new();
283             col = gtk_tree_view_column_new_with_attributes(column.title.c_str(), renderer, "active", column.pos, NULL);
284             break;
285         case PIXBUF:
286             col = gtk_tree_view_column_new_with_attributes(column.title.c_str(),
287                 gtk_cell_renderer_pixbuf_new(), "pixbuf", column.pos, NULL);
288             break;
289         case PIXBUF_STRING:
290             renderer = gtk_cell_renderer_pixbuf_new();
291             col = gtk_tree_view_column_new();
292             gtk_tree_view_column_set_title(col, column.title.c_str());
293             gtk_tree_view_column_pack_start(col, renderer, false);
294             gtk_tree_view_column_add_attribute(col, renderer, "pixbuf", TreeView::col(column.linkedCol));
295             renderer = gtk_cell_renderer_text_new();
296             gtk_tree_view_column_pack_start(col, renderer, true);
297             gtk_tree_view_column_add_attribute(col, renderer, "text", column.pos);
298             break;
299         case ICON_STRING:
300             renderer = gtk_cell_renderer_pixbuf_new();
301             col = gtk_tree_view_column_new();
302             gtk_tree_view_column_set_title(col, column.title.c_str());
303             gtk_tree_view_column_pack_start(col, renderer, false);
304             gtk_tree_view_column_add_attribute(col, renderer, "stock-id", TreeView::col(column.linkedCol));
305             renderer = gtk_cell_renderer_text_new();
306             gtk_tree_view_column_pack_start(col, renderer, true);
307             gtk_tree_view_column_add_attribute(col, renderer, "text", column.pos);
308             break;
309         case ICON_STRING_TEXT_COLOR:
310             // icon
311             renderer = gtk_cell_renderer_pixbuf_new();
312             col = gtk_tree_view_column_new();
313             gtk_tree_view_column_set_title(col, column.title.c_str());
314             gtk_tree_view_column_pack_start(col, renderer, false);
315             gtk_tree_view_column_add_attribute(col, renderer, "stock-id", TreeView::col(column.linkedCol));
316             // text
317             renderer = gtk_cell_renderer_text_new();
318             gtk_tree_view_column_pack_start(col, renderer, true);
319             gtk_tree_view_column_add_attribute(col, renderer, "text", column.pos);
320             //gtk_tree_view_column_add_attribute(col, renderer, "foreground", TreeView::col(column.linkedTextColor));
321             break;
322         case EDIT_STRING:
323             renderer = gtk_cell_renderer_text_new();
324             g_object_set(renderer, "editable", TRUE, NULL);
325             col = gtk_tree_view_column_new_with_attributes(column.title.c_str(), renderer, "text", column.pos, NULL);
326             break;
327         case PROGRESS:
328             renderer = gtk_cell_renderer_progress_new();
329             g_object_set(renderer, "xalign", 0.0, NULL); // Doesn't work yet. See: http://bugzilla.gnome.org/show_bug.cgi?id=334576
330             col = gtk_tree_view_column_new_with_attributes(column.title.c_str(),
331                 renderer, "text", column.pos, "value", TreeView::col(column.linkedCol), NULL);
332             break;
333     }
334 
335     if (!col)
336         return;
337 
338     // If columns are too small, they can't be manipulated
339     if (column.width >= 20)
340     {
341         gtk_tree_view_column_set_fixed_width(col, column.width);
342         gtk_tree_view_column_set_resizable(col, TRUE);
343     }
344     if (column.width != -1)
345         gtk_tree_view_column_set_sizing(col, GTK_TREE_VIEW_COLUMN_FIXED);
346 
347     //make columns sortable
348     if (column.type != BOOL && column.type != PIXBUF && column.type != EDIT_STRING)
349     {
350         gtk_tree_view_column_set_sort_column_id(col, column.pos);
351         gtk_tree_view_column_set_sort_indicator(col, TRUE);
352     }
353 
354     gtk_tree_view_column_set_clickable(col, TRUE);
355     gtk_tree_view_column_set_reorderable(col, true);
356     gtk_tree_view_column_set_visible(col, column.visible);
357 
358     gtk_tree_view_insert_column(view, col, column.pos);
359 
360     /*
361      * Breaks GTK+ API, but is the only way to attach a signal to a gtktreeview column header. See GTK bug #141937.
362      * @todo: Replace when GTK adds a way to add a signal to the entire header (remove visibleColumns var, too).
363      */
364 #if GTK_CHECK_VERSION(3, 0, 0)
365     g_signal_connect(gtk_tree_view_column_get_button(col) , "button-release-event", G_CALLBACK(popupMenu_gui), (gpointer)this);
366 #else
367     g_signal_connect(col->button, "button-release-event", G_CALLBACK(popupMenu_gui), (gpointer)this);
368 #endif
369 }
370 
setSortColumn_gui(const string & column,const string & sortColumn)371 void TreeView::setSortColumn_gui(const string &column, const string &sortColumn)
372 {
373     GtkTreeViewColumn *gtkColumn = gtk_tree_view_get_column(view, col(column));
374     gtk_tree_view_column_set_sort_column_id(gtkColumn, col(sortColumn));
375 }
376 
col(const string & title)377 int TreeView::col(const string &title)
378 {
379     dcassert(!title.empty());
380     dcassert(columns.find(title) != columns.end() || hiddenColumns.find(title) != hiddenColumns.end());
381     int retval = -1;
382 
383     if (columns.find(title) != columns.end())
384         retval = columns[title].pos;
385     else
386         retval = hiddenColumns[title].id;
387 
388     dcassert(retval >= 0 && (retval < count));
389     return retval;
390 }
391 
popupMenu_gui(GtkWidget * widget,GdkEventButton * event,gpointer data)392 gboolean TreeView::popupMenu_gui(GtkWidget *widget, GdkEventButton *event, gpointer data)
393 {
394     TreeView *tv = (TreeView*)data;
395 
396     if (event->button == 3)
397     {
398         gtk_menu_popup(tv->menu, NULL, NULL, NULL, NULL, (event != NULL) ? event->button : 0, gdk_event_get_time((GdkEvent*)event));
399         gtk_widget_show_all(GTK_WIDGET(tv->menu));
400         return true;
401     }
402     else
403         return false;
404 }
405 
toggleColumnVisibility(GtkMenuItem * item,gpointer data)406 void TreeView::toggleColumnVisibility(GtkMenuItem *item, gpointer data)
407 {
408     TreeView *tv = (TreeView*)data;
409     GtkTreeViewColumn *column = NULL;
410     gboolean visible;
411     SortedColIter iter;
412     string title = string(gtk_label_get_text(GTK_LABEL(gtk_bin_get_child(GTK_BIN(item)))));
413 
414     // Function col(title) doesn't work here, so we have to find column manually.
415     for (iter = tv->sortedColumns.begin(); iter != tv->sortedColumns.end(); ++iter)
416     {
417         column = gtk_tree_view_get_column(tv->view, iter->first);
418         if (string(gtk_tree_view_column_get_title(column)) == title)
419             break;
420     }
421 
422     if (!column)
423         return;
424 
425     visible = !gtk_tree_view_column_get_visible(column);
426 
427     // Can't let number of visible columns fall below 1, otherwise there's no way to unhide columns.
428     if (!visible && tv->visibleColumns <= 1)
429     {
430         gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(tv->colMenuItems[title]), true);
431         return;
432     }
433 
434     gtk_tree_view_column_set_visible(column, visible);
435     gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(tv->colMenuItems[title]), visible);
436 
437     if (visible)
438     {
439         tv->visibleColumns++;
440         // Seems to be a bug in gtk where sometimes columns are unresizable after being made visible
441         gtk_tree_view_column_set_resizable(column, TRUE);
442     }
443     else
444         tv->visibleColumns--;
445 }
446 
restoreSettings()447 void TreeView::restoreSettings()
448 {
449     vector<int> columnOrder, columnWidth, columnVisibility;
450     columnOrder = WulforUtil::splitString(WGETS(name + "-order"), ",");
451     columnWidth = WulforUtil::splitString(WGETS(name + "-width"), ",");
452     columnVisibility = WulforUtil::splitString(WGETS(name + "-visibility"), ",");
453 
454     if (columns.size() == columnOrder.size() &&
455         columnOrder.size() == columnWidth.size() &&
456         columnWidth.size() == columnVisibility.size())
457     {
458         for (ColIter iter = columns.begin(); iter != columns.end(); ++iter)
459         {
460             for (size_t i = 0; i < columns.size(); i++)
461             {
462                 if (iter->second.id == columnOrder.at(i))
463                 {
464                     iter->second.pos = i;
465                     sortedColumns[i] = iter->second.title;
466                     if (columnWidth.at(i) >= 20)
467                         iter->second.width = columnWidth.at(i);
468                     if (columnVisibility.at(i) == 0 || columnVisibility.at(i) == 1)
469                         iter->second.visible = columnVisibility.at(i);
470                     break;
471                 }
472             }
473         }
474     }
475 }
476 
saveSettings()477 void TreeView::saveSettings()
478 {
479     string columnOrder, columnWidth, columnVisibility, title;
480     GtkTreeViewColumn *col;
481     gint width;
482 
483     dcassert(!columns.empty());
484 
485     for (size_t i = 0; i < columns.size(); i++)
486     {
487         col = gtk_tree_view_get_column(view, i);
488         if (!col)
489             continue;
490 
491         title = string(gtk_tree_view_column_get_title(col));
492         width = gtk_tree_view_column_get_width(col);
493 
494         // A col was moved to the right of the padding col
495         if (title.empty())
496             return;
497 
498         columnOrder += dcpp::Util::toString(columns[title].id) + ",";
499         if (width >= 20)
500             columnWidth += dcpp::Util::toString(width) + ",";
501         else
502             columnWidth += dcpp::Util::toString(columns[title].width) + ",";
503         columnVisibility += dcpp::Util::toString(gtk_tree_view_column_get_visible(col)) + ",";
504     }
505 
506     if (!columnOrder.empty())
507     {
508         columnOrder.erase(columnOrder.size() - 1, 1);
509         columnWidth.erase(columnWidth.size() - 1, 1);
510         columnVisibility.erase(columnVisibility.size() - 1, 1);
511 
512         WSET(name + "-order", columnOrder);
513         WSET(name + "-width", columnWidth);
514         WSET(name + "-visibility", columnVisibility);
515     }
516 }
517