source: deluge/ui/gtkui/torrentview.py@ b60504

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

Force 16x16 pixbufs in tracker column

  • Property mode set to 100644
File size: 18.6 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 = 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 if cell.get_property("pixbuf") != icon:
85 cell.set_property("pixbuf", icon)
86
87
88def cell_data_progress(column, cell, model, row, data):
89 """Display progress bar with text"""
90 (value, state_str) = model.get(row, *data)
91 if cell.get_property("value") != value:
92 cell.set_property("value", value)
93
94 textstr = "%s" % state_str
95 if state_str != "Seeding" and value < 100:
96 textstr = textstr + " %.2f%%" % value
97 if cell.get_property("text") != textstr:
98 cell.set_property("text", textstr)
99
100def cell_data_queue(column, cell, model, row, data):
101 value = model.get_value(row, data)
102 if value < 0:
103 cell.set_property("text", "")
104 else:
105 cell.set_property("text", value + 1)
106
107def queue_column_sort(model, iter1, iter2, data):
108 v1 = model[iter1][data]
109 v2 = model[iter2][data]
110 if v1 == v2:
111 return 0
112 if v2 < 0:
113 return -1
114 if v1 < 0:
115 return 1
116 if v1 > v2:
117 return 1
118 if v2 > v1:
119 return -1
120
121class TorrentView(listview.ListView, component.Component):
122 """TorrentView handles the listing of torrents."""
123 def __init__(self):
124 component.Component.__init__(self, "TorrentView", interval=2)
125 self.window = component.get("MainWindow")
126 # Call the ListView constructor
127 listview.ListView.__init__(self,
128 self.window.main_glade.get_widget("torrent_view"),
129 "torrentview.state")
130 log.debug("TorrentView Init..")
131
132 # This is where status updates are put
133 self.status = {}
134
135 # We keep a copy of the previous status to compare for changes
136 self.prev_status = {}
137
138 # Register the columns menu with the listview so it gets updated
139 # accordingly.
140 self.register_checklist_menu(
141 self.window.main_glade.get_widget("menu_columns"))
142
143 # Add the columns to the listview
144 self.add_text_column("torrent_id", hidden=True)
145 self.add_bool_column("dirty", hidden=True)
146 self.add_func_column("#", cell_data_queue, [int], status_field=["queue"], sort_func=queue_column_sort)
147 self.add_texticon_column(_("Name"), status_field=["state", "name"],
148 function=cell_data_statusicon)
149 self.add_func_column(_("Size"),
150 listview.cell_data_size,
151 [gobject.TYPE_UINT64],
152 status_field=["total_wanted"])
153 self.add_progress_column(_("Progress"),
154 status_field=["progress", "state"],
155 col_types=[float, str],
156 function=cell_data_progress)
157 self.add_func_column(_("Seeders"),
158 listview.cell_data_peer,
159 [int, int],
160 status_field=["num_seeds",
161 "total_seeds"])
162 self.add_func_column(_("Peers"),
163 listview.cell_data_peer,
164 [int, int],
165 status_field=["num_peers",
166 "total_peers"])
167 self.add_func_column(_("Down Speed"),
168 listview.cell_data_speed,
169 [float],
170 status_field=["download_payload_rate"])
171 self.add_func_column(_("Up Speed"),
172 listview.cell_data_speed,
173 [float],
174 status_field=["upload_payload_rate"])
175 self.add_func_column(_("ETA"),
176 listview.cell_data_time,
177 [int],
178 status_field=["eta"])
179 self.add_func_column(_("Ratio"),
180 listview.cell_data_ratio,
181 [float],
182 status_field=["ratio"])
183 self.add_func_column(_("Avail"),
184 listview.cell_data_ratio,
185 [float],
186 status_field=["distributed_copies"])
187 self.add_func_column(_("Added"),
188 listview.cell_data_date,
189 [float],
190 status_field=["time_added"])
191 self.add_texticon_column(_("Tracker"), status_field=["tracker_host", "tracker_host"],
192 function=cell_data_trackericon)
193
194 # Set filter to None for now
195 self.filter = None
196
197 ### Connect Signals ###
198 # Connect to the 'button-press-event' to know when to bring up the
199 # torrent menu popup.
200 self.treeview.connect("button-press-event",
201 self.on_button_press_event)
202 # Connect to the 'changed' event of TreeViewSelection to get selection
203 # changes.
204 self.treeview.get_selection().connect("changed",
205 self.on_selection_changed)
206
207 self.treeview.connect("drag-drop", self.on_drag_drop)
208
209 client.register_event_handler("TorrentStateChangedEvent", self.on_torrentstatechanged_event)
210 client.register_event_handler("TorrentAddedEvent", self.on_torrentadded_event)
211 client.register_event_handler("TorrentRemovedEvent", self.on_torrentremoved_event)
212 client.register_event_handler("SessionPausedEvent", self.on_sessionpaused_event)
213 client.register_event_handler("SessionResumedEvent", self.on_sessionresumed_event)
214 client.register_event_handler("TorrentQueueChangedEvent", self.on_torrentqueuechanged_event)
215
216 def start(self):
217 """Start the torrentview"""
218 # We need to get the core session state to know which torrents are in
219 # the session so we can add them to our list.
220 client.core.get_session_state().addCallback(self._on_session_state)
221
222 def _on_session_state(self, state):
223 log.debug("on_session_state: %s", state)
224 self.treeview.freeze_child_notify()
225 model = self.treeview.get_model()
226 for torrent_id in state:
227 self.add_row(torrent_id, update=False)
228 self.mark_dirty(torrent_id)
229 self.treeview.set_model(model)
230 self.treeview.thaw_child_notify()
231 self.update()
232
233 def stop(self):
234 """Stops the torrentview"""
235 # We need to clear the liststore
236 self.liststore.clear()
237 self.prev_status = {}
238
239 def shutdown(self):
240 """Called when GtkUi is exiting"""
241 self.save_state("torrentview.state")
242
243 def set_filter(self, filter_dict):
244 """Sets filters for the torrentview..
245 see: core.get_torrents_status
246 """
247 self.filter = dict(filter_dict) #copied version of filter_dict.
248 self.update()
249
250 def send_status_request(self, columns=None):
251 # Store the 'status_fields' we need to send to core
252 status_keys = []
253 # Store the actual columns we will be updating
254 self.columns_to_update = []
255
256 if columns is None:
257 # We need to iterate through all columns
258 columns = self.columns.keys()
259
260 # Iterate through supplied list of columns to update
261 for column in columns:
262 # Make sure column is visible and has 'status_field' set.
263 # If not, we can ignore it.
264 if self.columns[column].column.get_visible() is True \
265 and self.columns[column].hidden is False \
266 and self.columns[column].status_field is not None:
267 for field in self.columns[column].status_field:
268 status_keys.append(field)
269 self.columns_to_update.append(column)
270
271 # Remove duplicate keys
272 self.columns_to_update = list(set(self.columns_to_update))
273
274 # If there is nothing in status_keys then we must not continue
275 if status_keys is []:
276 return
277
278 # Remove duplicates from status_key list
279 status_keys = list(set(status_keys))
280
281 # Request the statuses for all these torrent_ids, this is async so we
282 # will deal with the return in a signal callback.
283 client.core.get_torrents_status(
284 self.filter, status_keys).addCallback(self._on_get_torrents_status)
285
286 def update(self):
287 # Send a status request
288 gobject.idle_add(self.send_status_request)
289
290 def update_view(self, columns=None):
291 """Update the view. If columns is not None, it will attempt to only
292 update those columns selected.
293 """
294 filter_column = self.columns["filter"].column_indices[0]
295 # Update the torrent view model with data we've received
296 status = self.status
297
298 for row in self.liststore:
299 torrent_id = row[self.columns["torrent_id"].column_indices[0]]
300
301 if not torrent_id in status.keys():
302 row[filter_column] = False
303 else:
304 row[filter_column] = True
305 if torrent_id in self.prev_status and status[torrent_id] == self.prev_status[torrent_id]:
306 # The status dict is the same, so do not update
307 continue
308
309 # Set values for each column in the row
310 for column in self.columns_to_update:
311 column_index = self.get_column_index(column)
312 for i, status_field in enumerate(self.columns[column].status_field):
313 try:
314 # Only update if different
315 row_value = status[torrent_id][status_field]
316 if row[column_index[i]] != row_value:
317 row[column_index[i]] = row_value
318 except Exception, e:
319 log.debug("%s", e)
320
321 component.get("MenuBar").update_menu()
322
323 self.prev_status = status
324
325 def _on_get_torrents_status(self, status):
326 """Callback function for get_torrents_status(). 'status' should be a
327 dictionary of {torrent_id: {key, value}}."""
328 self.status = status
329 if self.status == self.prev_status and self.prev_status:
330 # We do not bother updating since the status hasn't changed
331 self.prev_status = self.status
332 return
333 gobject.idle_add(self.update_view)
334
335 def add_row(self, torrent_id, update=True):
336 """Adds a new torrent row to the treeview"""
337 # Make sure this torrent isn't already in the list
338 for row in self.liststore:
339 if row[self.columns["torrent_id"].column_indices[0]] == torrent_id:
340 # Row already in the list
341 return
342 # Insert a new row to the liststore
343 row = self.liststore.append()
344 # Store the torrent id
345 self.liststore.set_value(
346 row,
347 self.columns["torrent_id"].column_indices[0],
348 torrent_id)
349 if update:
350 self.update()
351
352 def remove_row(self, torrent_id):
353 """Removes a row with torrent_id"""
354 for row in self.liststore:
355 if row[self.columns["torrent_id"].column_indices[0]] == torrent_id:
356 self.liststore.remove(row.iter)
357 # Force an update of the torrentview
358 self.update()
359 break
360
361 def mark_dirty(self, torrent_id = None):
362 for row in self.liststore:
363 if not torrent_id or row[self.columns["torrent_id"].column_indices[0]] == torrent_id:
364 #log.debug("marking %s dirty", torrent_id)
365 row[self.columns["dirty"].column_indices[0]] = True
366 if torrent_id: break
367
368 def get_selected_torrent(self):
369 """Returns a torrent_id or None. If multiple torrents are selected,
370 it will return the torrent_id of the first one."""
371 selected = self.get_selected_torrents()
372 if selected:
373 return selected[0]
374 else:
375 return selected
376
377 def get_selected_torrents(self):
378 """Returns a list of selected torrents or None"""
379 torrent_ids = []
380 try:
381 paths = self.treeview.get_selection().get_selected_rows()[1]
382 except AttributeError:
383 # paths is likely None .. so lets return []
384 return []
385 try:
386 for path in paths:
387 try:
388 row = self.treeview.get_model().get_iter(path)
389 except Exception, e:
390 log.debug("Unable to get iter from path: %s", e)
391 continue
392
393 child_row = self.treeview.get_model().convert_iter_to_child_iter(None, row)
394 child_row = self.treeview.get_model().get_model().convert_iter_to_child_iter(child_row)
395 if self.liststore.iter_is_valid(child_row):
396 try:
397 value = self.liststore.get_value(child_row, self.columns["torrent_id"].column_indices[0])
398 except Exception, e:
399 log.debug("Unable to get value from row: %s", e)
400 else:
401 torrent_ids.append(value)
402 if len(torrent_ids) == 0:
403 return []
404
405 return torrent_ids
406 except ValueError, TypeError:
407 return []
408
409 def get_torrent_status(self, torrent_id):
410 """Returns data stored in self.status, it may not be complete"""
411 try:
412 return self.status[torrent_id]
413 except:
414 return {}
415
416 def get_visible_torrents(self):
417 return self.status.keys()
418
419 ### Callbacks ###
420 def on_button_press_event(self, widget, event):
421 """This is a callback for showing the right-click context menu."""
422 log.debug("on_button_press_event")
423 # We only care about right-clicks
424 if event.button == 3:
425 x, y = event.get_coords()
426 path = self.treeview.get_path_at_pos(int(x), int(y))
427 if not path:
428 return
429 row = self.model_filter.get_iter(path[0])
430
431 if self.get_selected_torrents():
432 if self.model_filter.get_value(row, self.columns["torrent_id"].column_indices[0]) not in self.get_selected_torrents():
433 self.treeview.get_selection().unselect_all()
434 self.treeview.get_selection().select_iter(row)
435 else:
436 self.treeview.get_selection().select_iter(row)
437 torrentmenu = component.get("MenuBar").torrentmenu
438 torrentmenu.popup(None, None, None, event.button, event.time)
439 return True
440
441 def on_selection_changed(self, treeselection):
442 """This callback is know when the selection has changed."""
443 log.debug("on_selection_changed")
444 component.get("TorrentDetails").update()
445 component.get("MenuBar").update_menu()
446
447 def on_drag_drop(self, widget, drag_context, x, y, timestamp):
448 widget.stop_emission("drag-drop")
449
450 def on_torrentadded_event(self, torrent_id):
451 self.add_row(torrent_id)
452 self.mark_dirty(torrent_id)
453
454 def on_torrentremoved_event(self, torrent_id):
455 self.remove_row(torrent_id)
456
457 def on_torrentstatechanged_event(self, torrent_id, state):
458 # Update the torrents state
459 for row in self.liststore:
460 if not torrent_id == row[self.columns["torrent_id"].column_indices[0]]:
461 continue
462
463 row[self.get_column_index("Progress")[1]] = state
464
465 self.mark_dirty(torrent_id)
466
467 def on_sessionpaused_event(self):
468 self.mark_dirty()
469 self.update()
470
471 def on_sessionresumed_event(self):
472 self.mark_dirty()
473 self.update()
474
475 def on_torrentqueuechanged_event(self):
476 self.mark_dirty()
477 self.update()
Note: See TracBrowser for help on using the repository browser.