Czech translation update (via transifex.net)
[cinaest] / src / movie-list-view.vala
1 /* This file is part of Cinaest.
2  *
3  * Copyright (C) 2009 Philipp Zabel
4  *
5  * Cinaest is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * Cinaest is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with Cinaest. If not, see <http://www.gnu.org/licenses/>.
17  */
18
19 using Gtk;
20 using Hildon;
21
22 public class MovieListView : PannableArea {
23         public MovieListStore store;
24         TreeView tree;
25         IconView icons;
26
27         private bool more_movies_available;
28         private CellRendererText title_renderer;
29         private CellRendererText secondary_renderer;
30         private CellRendererText rating_renderer;
31         private CellRendererText date_renderer;
32         private MoviePoster.Factory poster_factory;
33         private int last_a = -1;
34         private int last_b = -1;
35
36         private bool poster_mode_;
37         public bool poster_mode {
38                 get {
39                         return poster_mode_;
40                 }
41                 set {
42                         if (value & !poster_mode_) {
43                                 remove (tree);
44                                 tree.set_model (null);
45                                 icons.set_model (store);
46                                 store.view = (Widget) icons;
47                                 add (icons);
48                         } else if (!value & poster_mode_) {
49                                 remove (icons);
50                                 icons.set_model (null);
51                                 tree.set_model (store);
52                                 store.view = (Widget) tree;
53                                 add (tree);
54                         }
55                         if (value != poster_mode_) {
56                                 poster_mode_ = value;
57                                 last_a = -1;
58                                 last_b = -1;
59
60                                 // FIXME - just calling on_adjustment_value_changed () here doesn't work
61                                 // because icons.get_visible_range () returns wrong paths.
62                                 check_posters (0, 10);
63                                 jump_to (0, 0);
64                         }
65                 }
66         }
67
68         public signal void movie_activated (Movie movie);
69
70         private Gtk.TreeView create_treeview (Gtk.Window window, bool show_date) {
71                 // Tree View
72                 var tree = (TreeView) Hildon.gtk_tree_view_new_with_model (UIMode.NORMAL, store);
73                 tree.set_headers_visible (false);
74                 tree.set_rules_hint (true);
75
76                 // Tree selection object
77                 var selection = tree.get_selection ();
78                 selection.set_mode (SelectionMode.SINGLE);
79
80                 // Poster column
81                 var poster_column = new TreeViewColumn ();
82                 poster_column.set_title (_("Poster"));
83
84                 // Add poster icon to column
85                 var pixbuf_renderer = new CellRendererPixbuf ();
86                 pixbuf_renderer.width = Poster.ICON_WIDTH + Hildon.MARGIN_DEFAULT;
87                 pixbuf_renderer.xalign = 0.0f;
88                 poster_column.pack_start (pixbuf_renderer, false);
89                 poster_column.add_attribute (pixbuf_renderer, "pixbuf", MovieListStore.Columns.ICON);
90
91                 tree.append_column (poster_column);
92
93                 // Title column with secondary text
94                 var title_column = new TreeViewColumn ();
95                 title_column.set_title (_("Movie"));
96                 title_column.set_sort_column_id (MovieListStore.Columns.TITLE);
97                 title_column.set_reorderable (false);
98                 title_column.set_sizing (TreeViewColumnSizing.AUTOSIZE);
99                 title_column.set_expand (true);
100
101                 // Add text to column
102                 var vbox_renderer = new CellRendererVBox ();
103
104                 title_renderer = new CellRendererText ();
105                 title_renderer.yalign = 1.0f;
106                 title_renderer.ellipsize = Pango.EllipsizeMode.END;
107
108                 vbox_renderer.append (title_renderer, true);
109
110                 // Add secondary text to column (Genres, Director, etc.)
111                 secondary_renderer = new CellRendererText ();
112                 secondary_renderer.yalign = 0;
113                 secondary_renderer.ellipsize = Pango.EllipsizeMode.END;
114                 secondary_renderer.attributes = get_attributes (window, "SmallSystemFont", "SecondaryTextColor");
115
116                 vbox_renderer.append (secondary_renderer, true);
117
118                 title_column.pack_start (vbox_renderer, true);
119                 title_column.set_cell_data_func (vbox_renderer, title_data_func);
120
121                 tree.append_column (title_column);
122
123                 // Year column
124                 var year_column = new TreeViewColumn ();
125                 year_column.set_title (_("Year"));
126                 year_column.set_sort_column_id (MovieListStore.Columns.YEAR);
127                 year_column.set_reorderable (false);
128                 year_column.set_sort_order (SortType.DESCENDING);
129                 tree.append_column (year_column);
130
131                 // Rating column
132                 var rating_column = new TreeViewColumn ();
133                 rating_column.set_title (_("Rating"));
134                 rating_column.set_sort_column_id (MovieListStore.Columns.RATING);
135                 rating_column.set_reorderable (false);
136                 rating_column.set_sort_order (SortType.DESCENDING);
137                 rating_column.xalign = 1.0f;
138
139                 vbox_renderer = new CellRendererVBox ();
140
141                 rating_renderer = new CellRendererText ();
142                 rating_renderer.xalign = 1.0f;
143                 if (show_date)
144                         rating_renderer.yalign = 1.0f;
145
146                 vbox_renderer.append (rating_renderer, true);
147
148                 date_renderer = new CellRendererText ();
149                 date_renderer.yalign = 0;
150                 date_renderer.attributes = get_attributes (window, "SmallSystemFont", "SecondaryTextColor");
151
152                 if (show_date)
153                         vbox_renderer.append (date_renderer, true);
154
155                 rating_column.pack_start (vbox_renderer, true);
156                 rating_column.set_cell_data_func (vbox_renderer, rating_data_func);
157
158                 tree.append_column (rating_column);
159                 tree.show ();
160                 return tree;
161         }
162
163         private Gtk.IconView create_iconview () {
164                 var iconview = (Gtk.IconView) Hildon.gtk_icon_view_new (Hildon.UIMode.NORMAL);
165                 iconview.set_column_spacing (0);
166                 iconview.set_pixbuf_column (MovieListStore.Columns.POSTER);
167                 iconview.margin = 0;
168                 iconview.item_width = Poster.SMALL_WIDTH;
169                 iconview.column_spacing = Hildon.MARGIN_HALF;
170                 iconview.row_spacing = Hildon.MARGIN_HALF;
171                 iconview.show ();
172
173                 return iconview;
174         }
175
176         public MovieListView (Gtk.Window window, bool show_date = false) {
177                 store = new MovieListStore ();
178
179                 // Sort by title
180                 store.set_sort_column_id (MovieListStore.Columns.TITLE, SortType.ASCENDING);
181
182                 Gdk.Color color;
183                 window.ensure_style ();
184                 if (window.style.lookup_color ("SecondaryTextColor", out color)) {
185                         store.year_markup = "<span size=\"small\" fgcolor=\"%s\">(%%d)</span>".printf (color.to_string ());
186                 }
187
188                 tree = create_treeview (window, show_date);
189                 store.view = (Widget) tree;
190
191                 icons = create_iconview ();
192
193                 add (tree);
194
195                 // Connect signals
196                 get_vadjustment ().value_changed.connect (on_adjustment_value_changed);
197                 tree.row_activated.connect (on_row_activated);
198                 icons.item_activated.connect (on_item_activated);
199                 store.row_changed.connect (on_row_changed);
200                 store.row_inserted.connect (on_row_changed);
201                 store.search_finished.connect (on_search_finished);
202         }
203
204         construct {
205                 hscrollbar_policy = Gtk.PolicyType.NEVER;
206
207                 poster_factory = MoviePoster.Factory.get_instance ();
208         }
209
210         public void set_hildon_ui_mode (UIMode mode) {
211                 var selection = tree.get_selection ();
212
213                 if (mode == UIMode.NORMAL) {
214                         selection.set_mode (SelectionMode.NONE);
215                         icons.set_selection_mode (SelectionMode.NONE);
216                 }
217                 Hildon.gtk_tree_view_set_ui_mode (tree, mode);
218                 Hildon.gtk_icon_view_set_ui_mode (icons, mode);
219                 if (mode == UIMode.EDIT) {
220                         selection.set_mode (SelectionMode.MULTIPLE);
221                         icons.set_selection_mode (SelectionMode.MULTIPLE);
222                 }
223         }
224
225         private Pango.AttrList get_attributes (Gtk.Window window, string font_name, string color_name) {
226                 Pango.AttrList attr_list = new Pango.AttrList ();
227                 var style = Gtk.rc_get_style_by_paths (Gtk.Settings.get_default (), font_name, null, typeof (void));
228                 if (style != null) {
229                         var attr_font_desc = new Pango.AttrFontDesc (style.font_desc.copy ());
230                         attr_list.insert ((owned) attr_font_desc);
231                 }
232                 Gdk.Color color;
233                 window.ensure_style ();
234                 if (window.style.lookup_color (color_name, out color)) {
235                         Pango.Attribute attr_color = Pango.attr_foreground_new (color.red, color.green, color.blue);
236                         attr_list.insert ((owned) attr_color);
237                 }
238                 return attr_list;
239         }
240
241         public void unselect_all () {
242                 tree.get_selection ().unselect_all ();
243                 icons.unselect_all ();
244         }
245
246         public List<Movie> get_selected_movies () {
247                 var movies = new List<Movie> ();
248                 List<TreePath> paths;
249
250                 if (poster_mode_)
251                         paths = icons.get_selected_items ();
252                 else
253                         paths = tree.get_selection ().get_selected_rows (null);
254
255                 // get selected movies from the store
256                 foreach (TreePath path in paths) {
257                         TreeIter iter;
258
259                         if (store.get_iter (out iter, path)) {
260                                 Movie movie;
261
262                                 store.get (iter, MovieListStore.Columns.MOVIE, out movie);
263                                 if (movie != null) {
264                                         movies.append (movie);
265                                 }
266                         }
267                 }
268
269                 return movies;
270         }
271
272         // TODO: after scrolling down 80% of the list, load more
273         //       results if available.
274         private void on_adjustment_value_changed () {
275                 if (more_movies_available) {
276                         var vadj = get_vadjustment ();
277                         if (vadj.value > 0.8 * vadj.upper) {
278                                 Banner.show_information (this, null, _("More results available - refine search to reduce the dataset"));
279                                 more_movies_available = false;
280                         }
281                 }
282
283                 TreePath a_;
284                 TreePath b_;
285                 bool range;
286                 if (poster_mode_) {
287                         range = icons.get_visible_range (out a_, out b_);
288                 } else {
289                         range = tree.get_visible_range (out a_, out b_);
290                 }
291                 if (range) {
292                         // We know the list store is flat
293                         int a = a_.get_indices ()[0];
294                         int b = b_.get_indices ()[0];
295                         assert (a <= b);
296
297                         if (a == last_a && b == last_b)
298                                 return;
299
300                         if (a > last_b || b < last_a) {
301                                 check_posters (a, b);
302                         } else if (a >= last_a && b > last_b) {
303                                 check_posters (last_b + 1, b);
304                         } else if (b <= last_b && a < last_a) {
305                                 check_posters (a, last_a - 1);
306                         }
307
308                         last_a = a;
309                         last_b = b;
310                 }
311         }
312
313         private void check_posters (int a, int b) {
314                 for (int i = a; i <= b; i++) {
315                         var path = new TreePath.from_indices (i);
316                         TreeIter iter;
317                         if (!store.get_iter (out iter, path))
318                                 continue;
319
320                         Movie movie;
321                         store.get (iter, MovieListStore.Columns.MOVIE, out movie);
322                         if (movie == null)
323                                 continue;
324
325                         if (poster_mode_) {
326                                 if (movie.poster == null || movie.poster.small == null) try {
327                                         poster_factory.queue (movie,
328                                                               Poster.SMALL_WIDTH,
329                                                               Poster.SMALL_HEIGHT,
330                                                               true,
331                                                               receive_poster_small);
332                                 } catch (Error e) {
333                                         warning ("Failed to queue poster request: %s\n", e.message);
334                                 }
335                         } else {
336                                 if (movie.poster == null || movie.poster.icon == null) try {
337                                         poster_factory.queue (movie,
338                                                               Poster.ICON_WIDTH,
339                                                               Poster.ICON_HEIGHT,
340                                                               true,
341                                                               receive_poster_icon);
342                                 } catch (Error e) {
343                                         warning ("Failed to queue poster request: %s\n", e.message);
344                                 }
345                         }
346                 }
347         }
348
349         private void on_row_activated (TreeView tree, TreePath path, TreeViewColumn column) {
350                 on_item_activated (path);
351         }
352
353         private void on_item_activated (TreePath path) {
354                 TreeIter iter;
355
356                 if (store.get_iter (out iter, path)) {
357                         Movie movie;
358                         store.get (iter, MovieListStore.Columns.MOVIE, out movie);
359                         movie_activated (movie);
360                 }
361         }
362
363         private void on_row_changed (TreePath path, TreeIter iter) {
364                 TreePath a;
365                 TreePath b;
366                 bool range;
367
368                 if (poster_mode_) {
369                         range = icons.get_visible_range (out a, out b);
370                 } else {
371                         range = tree.get_visible_range (out a, out b);
372                 }
373                 if (!range ||
374                     (range && path.compare (a) >= 0 && path.compare (b) <= 0)) {
375                         Movie movie;
376
377                         store.get (iter, MovieListStore.Columns.MOVIE, out movie);
378                         if (movie == null)
379                                 return;
380
381                         int i = path.get_indices ()[0];
382                         check_posters (i, i);
383                 }
384         }
385
386         private void receive_poster_small (Gdk.Pixbuf pixbuf, Movie movie) {
387                 Poster poster;
388                 if (movie.poster != null)
389                         poster = movie.poster;
390                 else
391                         poster = new Poster ();
392                 poster.small = pixbuf;
393                 // Notify store of the poster change
394                 movie.poster = poster;
395         }
396
397         private void receive_poster_icon (Gdk.Pixbuf pixbuf, Movie movie) {
398                 Poster poster;
399                 if (movie.poster != null)
400                         poster = movie.poster;
401                 else
402                         poster = new Poster ();
403                 poster.icon = pixbuf;
404                 // Notify store of the poster change
405                 movie.poster = poster;
406         }
407
408         private void on_search_finished (int movies) {
409                 more_movies_available = (movies > 100); // FIXME
410         }
411
412         private void title_data_func (CellLayout cell_layout, CellRenderer cell, TreeModel model, TreeIter iter) {
413                 Movie movie;
414                 string markup;
415
416                 model.get (iter, MovieListStore.Columns.MOVIE, out movie,
417                                  MovieListStore.Columns.MARKUP, out markup);
418                 title_renderer.markup = markup;
419                 secondary_renderer.text = movie.secondary;
420         }
421
422         private void rating_data_func (CellLayout cell_layout, CellRenderer cell, TreeModel model, TreeIter iter) {
423                 string rating;
424                 Movie movie;
425
426                 model.get (iter, MovieListStore.Columns.RATING, out rating,
427                                  MovieListStore.Columns.MOVIE, out movie);
428                 rating_renderer.text = rating;
429                 if (movie.julian_date != 0) {
430                         var date = Date ();
431                         date.set_julian (movie.julian_date);
432                         var s = new char[64];
433                         date.strftime (s, "%x");
434                         date_renderer.text = (string) s;
435                 } else {
436                         date_renderer.text = "";
437                 }
438         }
439 }