source: deluge/ui/gtkui/torrentview.py@ 7b72d7

2.0.x develop extjs4-port
Last change on this file since 7b72d7 was 7b72d7, checked in by Andrew Resch <andrewresch@gmail.com>, 16 years ago

Made TrackerIcons a component to prevent trying to get an icon multiple
times
Fixed showing the wrong tracker icon in the TorrentView when the icon
could not be retrieved from the tracker

  • Property mode set to 100644
File size: 18.7 KB
Line 
1#
2# torrentview.py
3#
4# Copyright (C) 2007, 2008 Andrew Resch <andrewresch@gmail.com>
5#
6# Deluge is free software.
7#
8# You may redistribute it and/or modify it under the terms of the
9# GNU General Public License, as published by the Free Software
10# Foundation; either version 3 of the License, or (at your option)
11# any later version.
12#
13# deluge is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16# See the GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with deluge. If not, write to:
20# The Free Software Foundation, Inc.,
21# 51 Franklin Street, Fifth Floor
22# Boston, MA 02110-1301, USA.
23#
24
25
26"""The torrent view component that lists all torrents in the session."""
27
28import pygtk
29pygtk.require('2.0')
30import gtk, gtk.glade
31import gettext
32import gobject
33from urlparse import urlparse
34
35import deluge.common
36import deluge.component as component
37from deluge.ui.client import client
38from deluge.log import LOG as log
39import deluge.ui.gtkui.listview as listview
40from deluge.ui.tracker_icons import TrackerIcons
41
42# Status icons.. Create them from file only once to avoid constantly
43# re-creating them.
44icon_downloading = gtk.gdk.pixbuf_new_from_file(
45 deluge.common.get_pixmap("downloading16.png"))
46icon_seeding = gtk.gdk.pixbuf_new_from_file(
47 deluge.common.get_pixmap("seeding16.png"))
48icon_inactive = gtk.gdk.pixbuf_new_from_file(
49 deluge.common.get_pixmap("inactive16.png"))
50icon_alert = gtk.gdk.pixbuf_new_from_file(
51 deluge.common.get_pixmap("alert16.png"))
52icon_queued = gtk.gdk.pixbuf_new_from_file(
53 deluge.common.get_pixmap("queued16.png"))
54icon_checking = gtk.gdk.pixbuf_new_from_file(
55 deluge.common.get_pixmap("checking16.png"))
56
57# Holds the info for which status icon to display based on state
58ICON_STATE = {
59 "Allocating": icon_checking,
60 "Checking": icon_checking,
61 "Downloading": icon_downloading,
62 "Seeding": icon_seeding,
63 "Paused": icon_inactive,
64 "Error": icon_alert,
65 "Queued": icon_queued
66}
67
68def cell_data_statusicon(column, cell, model, row, data):
69 """Display text with an icon"""
70 try:
71 icon = ICON_STATE[model.get_value(row, data)]
72 if cell.get_property("pixbuf") != icon:
73 cell.set_property("pixbuf", icon)
74 except KeyError:
75 pass
76
77def cell_data_trackericon(column, cell, model, row, data):
78 icon_path = component.get("TrackerIcons").get(model[row][data])
79 if icon_path:
80 try:
81 icon = gtk.gdk.pixbuf_new_from_file_at_size(icon_path, 16, 16)
82 except Exception, e:
83 pass
84 else:
85 icon = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, 16, 16)
86 icon.fill(0x00000000)
87
88 if cell.get_property("pixbuf") != icon:
89 cell.set_property("pixbuf", icon)
90
91
92def cell_data_progress(column, cell, model, row, data):
93 """Display progress bar with text"""
94 (value, state_str) = model.get(row, *data)
95 if cell.get_property("value") != value:
96 cell.set_property("value", value)
97
98 textstr = "%s" % state_str
99 if state_str != "Seeding" and value < 100:
100 textstr = textstr + " %.2f%%" % value
101 if cell.get_property("text") != textstr:
102 cell.set_property("text", textstr)
103
104def cell_data_queue(column, cell, model, row, data):
105 value = model.get_value(row, data)
106 if value < 0:
107 cell.set_property("text", "")
108 else:
109 cell.set_property("text", value + 1)
110
111def queue_column_sort(model, iter1, iter2, data):
112 v1 = model[iter1][data]
113 v2 = model[iter2][data]
114 if v1 == v2:
115 return 0
116 if v2 < 0:
117 return -1
118 if v1 < 0:
119 return 1
120 if v1 > v2:
121 return 1
122 if v2 > v1:
123 return -1
124
125class TorrentView(listview.ListView, component.Component):
126 """TorrentView handles the listing of torrents."""
127 def __init__(self):
128 component.Component.__init__(self, "TorrentView", interval=2)
129 self.window = component.get("MainWindow")
130 # Call the ListView constructor
131 listview.ListView.__init__(self,
132 self.window.main_glade.get_widget("torrent_view"),
133 "torrentview.state")
134 log.debug("TorrentView Init..")
135
136 # This is where status updates are put
137 self.status = {}
138
139 # We keep a copy of the previous status to compare for changes
140 self.prev_status = {}
141
142 # Register the columns menu with the listview so it gets updated
143 # accordingly.
144 self.register_checklist_menu(
145 self.window.main_glade.get_widget("menu_columns"))
146
147 # Add the columns to the listview
148 self.add_text_column("torrent_id", hidden=True)
149 self.add_bool_column("dirty", hidden=True)
150 self.add_func_column("#", cell_data_queue, [int], status_field=["queue"], sort_func=queue_column_sort)
151 self.add_texticon_column(_("Name"), status_field=["state", "name"],
152 function=cell_data_statusicon)
153 self.add_func_column(_("Size"),
154 listview.cell_data_size,
155 [gobject.TYPE_UINT64],
156 status_field=["total_wanted"])
157 self.add_progress_column(_("Progress"),
158 status_field=["progress", "state"],
159 col_types=[float, str],
160 function=cell_data_progress)
161 self.add_func_column(_("Seeders"),
162 listview.cell_data_peer,
163 [int, int],
164 status_field=["num_seeds",
165 "total_seeds"])
166 self.add_func_column(_("Peers"),
167 listview.cell_data_peer,
168 [int, int],
169 status_field=["num_peers",
170 "total_peers"])
171 self.add_func_column(_("Down Speed"),
172 listview.cell_data_speed,
173 [float],
174 status_field=["download_payload_rate"])
175 self.add_func_column(_("Up Speed"),
176 listview.cell_data_speed,
177 [float],
178 status_field=["upload_payload_rate"])
179 self.add_func_column(_("ETA"),
180 listview.cell_data_time,
181 [int],
182 status_field=["eta"])
183 self.add_func_column(_("Ratio"),
184 listview.cell_data_ratio,
185 [float],
186 status_field=["ratio"])
187 self.add_func_column(_("Avail"),
188 listview.cell_data_ratio,
189 [float],
190 status_field=["distributed_copies"])
191 self.add_func_column(_("Added"),
192 listview.cell_data_date,
193 [float],
194 status_field=["time_added"])
195 self.add_texticon_column(_("Tracker"), status_field=["tracker_host", "tracker_host"],
196 function=cell_data_trackericon)
197
198 # Set filter to None for now
199 self.filter = None
200
201 ### Connect Signals ###
202 # Connect to the 'button-press-event' to know when to bring up the
203 # torrent menu popup.
204 self.treeview.connect("button-press-event",
205 self.on_button_press_event)
206 # Connect to the 'changed' event of TreeViewSelection to get selection
207 # changes.
208 self.treeview.get_selection().connect("changed",
209 self.on_selection_changed)
210
211 self.treeview.connect("drag-drop", self.on_drag_drop)
212
213 client.register_event_handler("TorrentStateChangedEvent", self.on_torrentstatechanged_event)
214 client.register_event_handler("TorrentAddedEvent", self.on_torrentadded_event)
215 client.register_event_handler("TorrentRemovedEvent", self.on_torrentremoved_event)
216 client.register_event_handler("SessionPausedEvent", self.on_sessionpaused_event)
217 client.register_event_handler("SessionResumedEvent", self.on_sessionresumed_event)
218 client.register_event_handler("TorrentQueueChangedEvent", self.on_torrentqueuechanged_event)
219
220 def start(self):
221 """Start the torrentview"""
222 # We need to get the core session state to know which torrents are in
223 # the session so we can add them to our list.
224 client.core.get_session_state().addCallback(self._on_session_state)
225
226 def _on_session_state(self, state):
227 log.debug("on_session_state: %s", state)
228 self.treeview.freeze_child_notify()
229 model = self.treeview.get_model()
230 for torrent_id in state:
231 self.add_row(torrent_id, update=False)
232 self.mark_dirty(torrent_id)
233 self.treeview.set_model(model)
234 self.treeview.thaw_child_notify()
235 self.update()
236
237 def stop(self):
238 """Stops the torrentview"""
239 # We need to clear the liststore
240 self.liststore.clear()
241 self.prev_status = {}
242
243 def shutdown(self):
244 """Called when GtkUi is exiting"""
245 self.save_state("torrentview.state")
246
247 def set_filter(self, filter_dict):
248 """Sets filters for the torrentview..
249 see: core.get_torrents_status
250 """
251 self.filter = dict(filter_dict) #copied version of filter_dict.
252 self.update()
253
254 def send_status_request(self, columns=None):
255 # Store the 'status_fields' we need to send to core
256 status_keys = []
257 # Store the actual columns we will be updating
258 self.columns_to_update = []
259
260 if columns is None:
261 # We need to iterate through all columns
262 columns = self.columns.keys()
263
264 # Iterate through supplied list of columns to update
265 for column in columns:
266 # Make sure column is visible and has 'status_field' set.
267 # If not, we can ignore it.
268 if self.columns[column].column.get_visible() is True \
269 and self.columns[column].hidden is False \
270 and self.columns[column].status_field is not None:
271 for field in self.columns[column].status_field:
272 status_keys.append(field)
273 self.columns_to_update.append(column)
274
275 # Remove duplicate keys
276 self.columns_to_update = list(set(self.columns_to_update))
277
278 # If there is nothing in status_keys then we must not continue
279 if status_keys is []:
280 return
281
282 # Remove duplicates from status_key list
283 status_keys = list(set(status_keys))
284
285 # Request the statuses for all these torrent_ids, this is async so we
286 # will deal with the return in a signal callback.
287 client.core.get_torrents_status(
288 self.filter, status_keys).addCallback(self._on_get_torrents_status)
289
290 def update(self):
291 # Send a status request
292 gobject.idle_add(self.send_status_request)
293
294 def update_view(self, columns=None):
295 """Update the view. If columns is not None, it will attempt to only
296 update those columns selected.
297 """
298 filter_column = self.columns["filter"].column_indices[0]
299 # Update the torrent view model with data we've received
300 status = self.status
301
302 for row in self.liststore:
303 torrent_id = row[self.columns["torrent_id"].column_indices[0]]
304
305 if not torrent_id in status.keys():
306 row[filter_column] = False
307 else:
308 row[filter_column] = True
309 if torrent_id in self.prev_status and status[torrent_id] == self.prev_status[torrent_id]:
310 # The status dict is the same, so do not update
311 continue
312
313 # Set values for each column in the row
314 for column in self.columns_to_update:
315 column_index = self.get_column_index(column)
316 for i, status_field in enumerate(self.columns[column].status_field):
317 try:
318 # Only update if different
319 row_value = status[torrent_id][status_field]
320 if row[column_index[i]] != row_value:
321 row[column_index[i]] = row_value
322 except Exception, e:
323 log.debug("%s", e)
324
325 component.get("MenuBar").update_menu()
326
327 self.prev_status = status
328
329 def _on_get_torrents_status(self, status):
330 """Callback function for get_torrents_status(). 'status' should be a
331 dictionary of {torrent_id: {key, value}}."""
332 self.status = status
333 if self.status == self.prev_status and self.prev_status:
334 # We do not bother updating since the status hasn't changed
335 self.prev_status = self.status
336 return
337 gobject.idle_add(self.update_view)
338
339 def add_row(self, torrent_id, update=True):
340 """Adds a new torrent row to the treeview"""
341 # Make sure this torrent isn't already in the list
342 for row in self.liststore:
343 if row[self.columns["torrent_id"].column_indices[0]] == torrent_id:
344 # Row already in the list
345 return
346 # Insert a new row to the liststore
347 row = self.liststore.append()
348 # Store the torrent id
349 self.liststore.set_value(
350 row,
351 self.columns["torrent_id"].column_indices[0],
352 torrent_id)
353 if update:
354 self.update()
355
356 def remove_row(self, torrent_id):
357 """Removes a row with torrent_id"""
358 for row in self.liststore:
359 if row[self.columns["torrent_id"].column_indices[0]] == torrent_id:
360 self.liststore.remove(row.iter)
361 # Force an update of the torrentview
362 self.update()
363 break
364
365 def mark_dirty(self, torrent_id = None):
366 for row in self.liststore:
367 if not torrent_id or row[self.columns["torrent_id"].column_indices[0]] == torrent_id:
368 #log.debug("marking %s dirty", torrent_id)
369 row[self.columns["dirty"].column_indices[0]] = True
370 if torrent_id: break
371
372 def get_selected_torrent(self):
373 """Returns a torrent_id or None. If multiple torrents are selected,
374 it will return the torrent_id of the first one."""
375 selected = self.get_selected_torrents()
376 if selected:
377 return selected[0]
378 else:
379 return selected
380
381 def get_selected_torrents(self):
382 """Returns a list of selected torrents or None"""
383 torrent_ids = []
384 try:
385 paths = self.treeview.get_selection().get_selected_rows()[1]
386 except AttributeError:
387 # paths is likely None .. so lets return []
388 return []
389 try:
390 for path in paths:
391 try:
392 row = self.treeview.get_model().get_iter(path)
393 except Exception, e:
394 log.debug("Unable to get iter from path: %s", e)
395 continue
396
397 child_row = self.treeview.get_model().convert_iter_to_child_iter(None, row)
398 child_row = self.treeview.get_model().get_model().convert_iter_to_child_iter(child_row)
399 if self.liststore.iter_is_valid(child_row):
400 try:
401 value = self.liststore.get_value(child_row, self.columns["torrent_id"].column_indices[0])
402 except Exception, e:
403 log.debug("Unable to get value from row: %s", e)
404 else:
405 torrent_ids.append(value)
406 if len(torrent_ids) == 0:
407 return []
408
409 return torrent_ids
410 except ValueError, TypeError:
411 return []
412
413 def get_torrent_status(self, torrent_id):
414 """Returns data stored in self.status, it may not be complete"""
415 try:
416 return self.status[torrent_id]
417 except:
418 return {}
419
420 def get_visible_torrents(self):
421 return self.status.keys()
422
423 ### Callbacks ###
424 def on_button_press_event(self, widget, event):
425 """This is a callback for showing the right-click context menu."""
426 log.debug("on_button_press_event")
427 # We only care about right-clicks
428 if event.button == 3:
429 x, y = event.get_coords()
430 path = self.treeview.get_path_at_pos(int(x), int(y))
431 if not path:
432 return
433 row = self.model_filter.get_iter(path[0])
434
435 if self.get_selected_torrents():
436 if self.model_filter.get_value(row, self.columns["torrent_id"].column_indices[0]) not in self.get_selected_torrents():
437 self.treeview.get_selection().unselect_all()
438 self.treeview.get_selection().select_iter(row)
439 else:
440 self.treeview.get_selection().select_iter(row)
441 torrentmenu = component.get("MenuBar").torrentmenu
442 torrentmenu.popup(None, None, None, event.button, event.time)
443 return True
444
445 def on_selection_changed(self, treeselection):
446 """This callback is know when the selection has changed."""
447 log.debug("on_selection_changed")
448 component.get("TorrentDetails").update()
449 component.get("MenuBar").update_menu()
450
451 def on_drag_drop(self, widget, drag_context, x, y, timestamp):
452 widget.stop_emission("drag-drop")
453
454 def on_torrentadded_event(self, torrent_id):
455 self.add_row(torrent_id)
456 self.mark_dirty(torrent_id)
457
458 def on_torrentremoved_event(self, torrent_id):
459 self.remove_row(torrent_id)
460
461 def on_torrentstatechanged_event(self, torrent_id, state):
462 # Update the torrents state
463 for row in self.liststore:
464 if not torrent_id == row[self.columns["torrent_id"].column_indices[0]]:
465 continue
466
467 row[self.get_column_index("Progress")[1]] = state
468
469 self.mark_dirty(torrent_id)
470
471 def on_sessionpaused_event(self):
472 self.mark_dirty()
473 self.update()
474
475 def on_sessionresumed_event(self):
476 self.mark_dirty()
477 self.update()
478
479 def on_torrentqueuechanged_event(self):
480 self.mark_dirty()
481 self.update()
Note: See TracBrowser for help on using the repository browser.