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 |
|
---|
28 | import pygtk
|
---|
29 | pygtk.require('2.0')
|
---|
30 | import gtk, gtk.glade
|
---|
31 | import gettext
|
---|
32 | import gobject
|
---|
33 | from urlparse import urlparse
|
---|
34 |
|
---|
35 | import deluge.common
|
---|
36 | import deluge.component as component
|
---|
37 | from deluge.ui.client import client
|
---|
38 | from deluge.log import LOG as log
|
---|
39 | import deluge.ui.gtkui.listview as listview
|
---|
40 | from deluge.ui.tracker_icons import TrackerIcons
|
---|
41 |
|
---|
42 | # Status icons.. Create them from file only once to avoid constantly
|
---|
43 | # re-creating them.
|
---|
44 | icon_downloading = gtk.gdk.pixbuf_new_from_file(
|
---|
45 | deluge.common.get_pixmap("downloading16.png"))
|
---|
46 | icon_seeding = gtk.gdk.pixbuf_new_from_file(
|
---|
47 | deluge.common.get_pixmap("seeding16.png"))
|
---|
48 | icon_inactive = gtk.gdk.pixbuf_new_from_file(
|
---|
49 | deluge.common.get_pixmap("inactive16.png"))
|
---|
50 | icon_alert = gtk.gdk.pixbuf_new_from_file(
|
---|
51 | deluge.common.get_pixmap("alert16.png"))
|
---|
52 | icon_queued = gtk.gdk.pixbuf_new_from_file(
|
---|
53 | deluge.common.get_pixmap("queued16.png"))
|
---|
54 | icon_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
|
---|
58 | ICON_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 |
|
---|
68 | def 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 |
|
---|
77 | def 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 |
|
---|
92 | def 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 |
|
---|
104 | def 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 |
|
---|
111 | def 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 |
|
---|
125 | class 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()
|
---|