1 | #
|
---|
2 | # filtertreeview.py
|
---|
3 | #
|
---|
4 | # Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
---|
5 | # Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
|
---|
6 | #
|
---|
7 | # Deluge is free software.
|
---|
8 | #
|
---|
9 | # You may redistribute it and/or modify it under the terms of the
|
---|
10 | # GNU General Public License, as published by the Free Software
|
---|
11 | # Foundation; either version 3 of the License, or (at your option)
|
---|
12 | # any later version.
|
---|
13 | #
|
---|
14 | # deluge is distributed in the hope that it will be useful,
|
---|
15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
|
---|
16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
---|
17 | # See the GNU General Public License for more details.
|
---|
18 | #
|
---|
19 | # You should have received a copy of the GNU General Public License
|
---|
20 | # along with deluge. If not, write to:
|
---|
21 | # The Free Software Foundation, Inc.,
|
---|
22 | # 51 Franklin Street, Fifth Floor
|
---|
23 | # Boston, MA 02110-1301, USA.
|
---|
24 | #
|
---|
25 |
|
---|
26 |
|
---|
27 | import gtk
|
---|
28 | import gtk.glade
|
---|
29 | import pkg_resources
|
---|
30 |
|
---|
31 | import deluge.component as component
|
---|
32 | import deluge.common
|
---|
33 | from deluge.log import LOG as log
|
---|
34 | from deluge.ui.client import client
|
---|
35 | from deluge.configmanager import ConfigManager
|
---|
36 |
|
---|
37 | STATE_PIX = {
|
---|
38 | "All": "all",
|
---|
39 | "Downloading": "downloading",
|
---|
40 | "Seeding": "seeding",
|
---|
41 | "Paused": "inactive",
|
---|
42 | "Checking": "checking",
|
---|
43 | "Queued": "queued",
|
---|
44 | "Error": "alert",
|
---|
45 | "Active": "active"
|
---|
46 | }
|
---|
47 |
|
---|
48 |
|
---|
49 | TRANSLATE = {
|
---|
50 | "state": "States",
|
---|
51 | "tracker_host": "Trackers",
|
---|
52 | "label": "Labels"
|
---|
53 | }
|
---|
54 |
|
---|
55 | FILTER_COLUMN = 5
|
---|
56 |
|
---|
57 | def _t(text):
|
---|
58 | if text in TRANSLATE:
|
---|
59 | text = TRANSLATE[text]
|
---|
60 | return _(text)
|
---|
61 |
|
---|
62 |
|
---|
63 | #sidebar-treeview
|
---|
64 | class FilterTreeView(component.Component):
|
---|
65 | def __init__(self):
|
---|
66 | component.Component.__init__(self, "FilterTreeView", interval=2)
|
---|
67 | self.window = component.get("MainWindow")
|
---|
68 | glade = self.window.main_glade
|
---|
69 | self.hpaned = glade.get_widget("hpaned")
|
---|
70 | self.scrolled = glade.get_widget("scrolledwindow_sidebar")
|
---|
71 | self.sidebar = component.get("SideBar")
|
---|
72 | self.config = ConfigManager("gtkui.conf")
|
---|
73 | self.tracker_icons = component.get("TrackerIcons")
|
---|
74 |
|
---|
75 | self.label_view = gtk.TreeView()
|
---|
76 | self.sidebar.add_tab(self.label_view, "filters", _("Filters"))
|
---|
77 |
|
---|
78 | #set filter to all when hidden:
|
---|
79 | self.sidebar.notebook.connect("hide", self._on_hide)
|
---|
80 |
|
---|
81 | #menu
|
---|
82 | glade_menu = gtk.glade.XML(pkg_resources.resource_filename("deluge.ui.gtkui",
|
---|
83 | "glade/filtertree_menu.glade"))
|
---|
84 | self.menu = glade_menu.get_widget("filtertree_menu")
|
---|
85 | glade_menu.signal_autoconnect({
|
---|
86 | "select_all": self.on_select_all,
|
---|
87 | "pause_all": self.on_pause_all,
|
---|
88 | "resume_all": self.on_resume_all
|
---|
89 | })
|
---|
90 |
|
---|
91 | self.default_menu_items = self.menu.get_children()
|
---|
92 |
|
---|
93 | # Create the liststore
|
---|
94 | #cat, value, label, count, pixmap, visible
|
---|
95 | self.treestore = gtk.TreeStore(str, str, str, int, gtk.gdk.Pixbuf, bool)
|
---|
96 |
|
---|
97 | # Create the column
|
---|
98 | column = gtk.TreeViewColumn(_("Filters"))
|
---|
99 | column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
|
---|
100 | render = gtk.CellRendererPixbuf()
|
---|
101 | self.renderpix = render
|
---|
102 | column.pack_start(render, expand=False)
|
---|
103 | column.add_attribute(render, 'pixbuf', 4)
|
---|
104 | render = gtk.CellRendererText()
|
---|
105 | column.pack_start(render, expand=False)
|
---|
106 | column.set_cell_data_func(render, self.render_cell_data,None)
|
---|
107 |
|
---|
108 | self.label_view.append_column(column)
|
---|
109 |
|
---|
110 | #style:
|
---|
111 | self.label_view.set_show_expanders(True)
|
---|
112 | self.label_view.set_headers_visible(False)
|
---|
113 | self.label_view.set_level_indentation(-35)
|
---|
114 |
|
---|
115 | self.label_view.set_model(self.treestore)
|
---|
116 | self.label_view.get_selection().connect("changed", self.on_selection_changed)
|
---|
117 | self.create_model_filter()
|
---|
118 |
|
---|
119 | #init.....
|
---|
120 | self.label_view.connect("button-press-event", self.on_button_press_event)
|
---|
121 |
|
---|
122 | #colors using current theme.
|
---|
123 | style = self.window.window.get_style()
|
---|
124 | self.colour_background = style.bg[gtk.STATE_NORMAL]
|
---|
125 | self.colour_foreground = style.fg[gtk.STATE_NORMAL]
|
---|
126 |
|
---|
127 | def start(self):
|
---|
128 | #add Cat nodes:
|
---|
129 | self.cat_nodes = {}
|
---|
130 | self.filters = {}
|
---|
131 |
|
---|
132 | #initial order of state filter:
|
---|
133 | self.cat_nodes["state"] = self.treestore.append(None, ["cat", "state", _("State"), 0, None, False])
|
---|
134 | self.update_row("state", "All" , 0)
|
---|
135 | self.update_row("state", "Downloading" , 0)
|
---|
136 | self.update_row("state", "Seeding" , 0)
|
---|
137 | self.update_row("state", "Active" , 0)
|
---|
138 | self.update_row("state", "Paused" , 0)
|
---|
139 | self.update_row("state", "Queued" , 0)
|
---|
140 |
|
---|
141 | # We set to this expand the rows on start-up
|
---|
142 | self.expand_rows = True
|
---|
143 |
|
---|
144 | self.selected_path = None
|
---|
145 |
|
---|
146 | def stop(self):
|
---|
147 | self.treestore.clear()
|
---|
148 |
|
---|
149 | def create_model_filter(self):
|
---|
150 | self.model_filter = self.treestore.filter_new()
|
---|
151 | self.model_filter.set_visible_column(FILTER_COLUMN)
|
---|
152 | self.label_view.set_model(self.model_filter)
|
---|
153 |
|
---|
154 | def cb_update_filter_tree(self, filter_items):
|
---|
155 | #create missing cat_nodes
|
---|
156 | for cat in filter_items:
|
---|
157 | if not cat in self.cat_nodes:
|
---|
158 | self.cat_nodes[cat] = self.treestore.append(None, ["cat", cat, _t(cat), 0, None, False])
|
---|
159 |
|
---|
160 | #update rows
|
---|
161 | visible_filters = []
|
---|
162 | for cat,filters in filter_items.iteritems():
|
---|
163 | for value, count in filters:
|
---|
164 | self.update_row(cat, value , count)
|
---|
165 | visible_filters.append((cat, value))
|
---|
166 |
|
---|
167 | # hide root-categories not returned by core-part of the plugin.
|
---|
168 | for cat in self.cat_nodes:
|
---|
169 | if cat in filter_items:
|
---|
170 | self.treestore.set_value(self.cat_nodes[cat], FILTER_COLUMN, True)
|
---|
171 | else:
|
---|
172 | self.treestore.set_value(self.cat_nodes[cat], FILTER_COLUMN, False)
|
---|
173 |
|
---|
174 | # hide items not returned by core-plugin.
|
---|
175 | for f in self.filters:
|
---|
176 | if not f in visible_filters:
|
---|
177 | self.treestore.set_value(self.filters[f], FILTER_COLUMN, False)
|
---|
178 |
|
---|
179 | if self.expand_rows:
|
---|
180 | self.label_view.expand_all()
|
---|
181 | self.expand_rows = False
|
---|
182 |
|
---|
183 | if not self.selected_path:
|
---|
184 | self.select_default_filter()
|
---|
185 |
|
---|
186 | def update_row(self, cat, value , count):
|
---|
187 | if (cat, value) in self.filters:
|
---|
188 | row = self.filters[(cat, value)]
|
---|
189 | self.treestore.set_value(row, 3, count)
|
---|
190 | else:
|
---|
191 | pix = self.get_pixmap(cat, value)
|
---|
192 | label = value
|
---|
193 | if cat == "state":
|
---|
194 | label = _(value)
|
---|
195 | row = self.treestore.append(self.cat_nodes[cat],[cat, value, label, count , pix, True])
|
---|
196 | self.filters[(cat, value)] = row
|
---|
197 |
|
---|
198 | if cat == "tracker_host" or cat == "label":
|
---|
199 | self.tracker_icons.get_async(value, lambda filename: self.set_row_image(cat, value, filename))
|
---|
200 |
|
---|
201 | self.treestore.set_value(row, FILTER_COLUMN, True)
|
---|
202 | return row
|
---|
203 |
|
---|
204 | def render_cell_data(self, column, cell, model, row, data):
|
---|
205 | "cell renderer"
|
---|
206 | cat = model.get_value(row, 0)
|
---|
207 | value = model.get_value(row, 1)
|
---|
208 | label = model.get_value(row, 2)
|
---|
209 | count = model.get_value(row, 3)
|
---|
210 | pix = model.get_value(row, 4)
|
---|
211 |
|
---|
212 | if label == "" and cat == "label":
|
---|
213 | label = _("no label")
|
---|
214 |
|
---|
215 | if pix:
|
---|
216 | self.renderpix.set_property("visible", True)
|
---|
217 | else:
|
---|
218 | self.renderpix.set_property("visible", False)
|
---|
219 |
|
---|
220 | if cat == "cat":
|
---|
221 | txt = label
|
---|
222 | cell.set_property("cell-background-gdk", self.colour_background)
|
---|
223 | cell.set_property("foreground-gdk", self.colour_foreground)
|
---|
224 | else:
|
---|
225 | txt = "%s (%s)" % (label, count)
|
---|
226 | cell.set_property("cell-background", None)
|
---|
227 | cell.set_property("foreground", None)
|
---|
228 |
|
---|
229 | cell.set_property('text', txt)
|
---|
230 |
|
---|
231 | def get_pixmap(self, cat, value):
|
---|
232 | if cat == "state":
|
---|
233 | pix = STATE_PIX.get(value, "dht")
|
---|
234 | return gtk.gdk.pixbuf_new_from_file(deluge.common.get_pixmap("%s16.png" % pix))
|
---|
235 |
|
---|
236 | return None
|
---|
237 |
|
---|
238 | def set_row_image(self, cat, value, filename):
|
---|
239 | pix = None
|
---|
240 | try: #assume we could get trashed images here..
|
---|
241 | pix = gtk.gdk.pixbuf_new_from_file_at_size(filename, 16, 16)
|
---|
242 | except Exception, e:
|
---|
243 | log.debug(e)
|
---|
244 |
|
---|
245 | if not pix:
|
---|
246 | pix = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, 16, 16)
|
---|
247 | pix.fill(0x00000000)
|
---|
248 | row = self.filters[(cat, value)]
|
---|
249 | self.treestore.set_value(row, 4, pix)
|
---|
250 | return False
|
---|
251 |
|
---|
252 |
|
---|
253 | def on_selection_changed(self, selection):
|
---|
254 | try:
|
---|
255 | (model, row) = self.label_view.get_selection().get_selected()
|
---|
256 | if not row:
|
---|
257 | log.debug("nothing selected")
|
---|
258 | return
|
---|
259 |
|
---|
260 | cat = model.get_value(row, 0)
|
---|
261 | value = model.get_value(row, 1)
|
---|
262 |
|
---|
263 | filter_dict = {cat: [value]}
|
---|
264 | if value == "All" or cat == "cat":
|
---|
265 | filter_dict = {}
|
---|
266 |
|
---|
267 | component.get("TorrentView").set_filter(filter_dict)
|
---|
268 |
|
---|
269 | self.selected_path = model.get_path(row)
|
---|
270 |
|
---|
271 | except Exception, e:
|
---|
272 | log.debug(e)
|
---|
273 | # paths is likely None .. so lets return None
|
---|
274 | return None
|
---|
275 |
|
---|
276 | def update(self):
|
---|
277 | try:
|
---|
278 | hide_cat = []
|
---|
279 | if not self.config["sidebar_show_trackers"]:
|
---|
280 | hide_cat = ["tracker_host"]
|
---|
281 | client.core.get_filter_tree(self.config["sidebar_show_zero"], hide_cat).addCallback(self.cb_update_filter_tree)
|
---|
282 | except Exception, e:
|
---|
283 | log.debug(e)
|
---|
284 |
|
---|
285 |
|
---|
286 | ### Callbacks ###
|
---|
287 | def on_button_press_event(self, widget, event):
|
---|
288 | """This is a callback for showing the right-click context menu.
|
---|
289 | NOT YET!
|
---|
290 | """
|
---|
291 | x, y = event.get_coords()
|
---|
292 | path = self.label_view.get_path_at_pos(int(x), int(y))
|
---|
293 | if not path:
|
---|
294 | return
|
---|
295 | path = path[0]
|
---|
296 | cat = self.model_filter[path][0]
|
---|
297 |
|
---|
298 | if event.button == 1:
|
---|
299 | # Prevent selecting a category label
|
---|
300 | if cat == "cat":
|
---|
301 | if self.label_view.row_expanded(path):
|
---|
302 | self.label_view.collapse_row(path)
|
---|
303 | else:
|
---|
304 | self.label_view.expand_row(path, False)
|
---|
305 | if not self.selected_path:
|
---|
306 | self.select_default_filter()
|
---|
307 | else:
|
---|
308 | self.label_view.get_selection().select_path(self.selected_path)
|
---|
309 | return True
|
---|
310 |
|
---|
311 | elif event.button == 3:
|
---|
312 | #assign current cat, value to self:
|
---|
313 | x, y = event.get_coords()
|
---|
314 | path = self.label_view.get_path_at_pos(int(x), int(y))
|
---|
315 | if not path:
|
---|
316 | return
|
---|
317 | row = self.model_filter.get_iter(path[0])
|
---|
318 | self.cat = self.model_filter.get_value(row, 0)
|
---|
319 | self.value = self.model_filter.get_value(row, 1)
|
---|
320 | self.count = self.model_filter.get_value(row, 3)
|
---|
321 |
|
---|
322 | #Show the pop-up menu
|
---|
323 | self.set_menu_sensitivity()
|
---|
324 | self.menu.hide()
|
---|
325 | self.menu.popup(None, None, None, event.button, event.time)
|
---|
326 | self.menu.show()
|
---|
327 |
|
---|
328 | if cat == "cat":
|
---|
329 | # Do not select the row
|
---|
330 | return True
|
---|
331 |
|
---|
332 | def set_menu_sensitivity(self):
|
---|
333 | #select-all/pause/resume
|
---|
334 | sensitive = (self.cat != "cat" and self.count <> 0)
|
---|
335 | for item in self.default_menu_items:
|
---|
336 | item.set_sensitive(sensitive)
|
---|
337 |
|
---|
338 | def select_all(self):
|
---|
339 | "for use in popup menu"
|
---|
340 | component.get("TorrentView").treeview.get_selection().select_all()
|
---|
341 |
|
---|
342 | def on_select_all(self, event):
|
---|
343 | self.select_all()
|
---|
344 |
|
---|
345 | def on_pause_all(self, event):
|
---|
346 | self.select_all()
|
---|
347 | func = getattr(component.get("MenuBar"), "on_menuitem_%s_activate" % "pause")
|
---|
348 | func(event)
|
---|
349 |
|
---|
350 | def on_resume_all(self, event):
|
---|
351 | self.select_all()
|
---|
352 | func = getattr(component.get("MenuBar"), "on_menuitem_%s_activate" % "resume")
|
---|
353 | func(event)
|
---|
354 |
|
---|
355 | def _on_hide(self, *args):
|
---|
356 | self.select_default_filter()
|
---|
357 |
|
---|
358 | def select_default_filter(self):
|
---|
359 | row = self.filters[("state", "All")]
|
---|
360 | path = self.treestore.get_path(row)
|
---|
361 | self.label_view.get_selection().select_path(path)
|
---|