1 | # -*- coding: utf-8 -*-
|
---|
2 | #
|
---|
3 | # piecesbar.py
|
---|
4 | #
|
---|
5 | # Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
---|
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 | # In addition, as a special exception, the copyright holders give
|
---|
26 | # permission to link the code of portions of this program with the OpenSSL
|
---|
27 | # library.
|
---|
28 | # You must obey the GNU General Public License in all respects for all of
|
---|
29 | # the code used other than OpenSSL. If you modify file(s) with this
|
---|
30 | # exception, you may extend this exception to your version of the file(s),
|
---|
31 | # but you are not obligated to do so. If you do not wish to do so, delete
|
---|
32 | # this exception statement from your version. If you delete this exception
|
---|
33 | # statement from all source files in the program, then also delete it here.
|
---|
34 | #
|
---|
35 | #
|
---|
36 |
|
---|
37 | import gtk
|
---|
38 | import cairo
|
---|
39 | import pango
|
---|
40 | import pangocairo
|
---|
41 | import logging
|
---|
42 | from math import pi
|
---|
43 | from deluge.configmanager import ConfigManager
|
---|
44 |
|
---|
45 | log = logging.getLogger(__name__)
|
---|
46 |
|
---|
47 |
|
---|
48 | COLOR_STATES = {
|
---|
49 | 0: "missing",
|
---|
50 | 1: "waiting",
|
---|
51 | 2: "downloading",
|
---|
52 | 3: "completed"
|
---|
53 | }
|
---|
54 |
|
---|
55 |
|
---|
56 | class PiecesBar(gtk.DrawingArea):
|
---|
57 | # Draw in response to an expose-event
|
---|
58 | __gsignals__ = {"expose-event": "override"}
|
---|
59 |
|
---|
60 | def __init__(self):
|
---|
61 | gtk.DrawingArea.__init__(self)
|
---|
62 | # Get progress bar styles, in order to keep font consistency
|
---|
63 | pb = gtk.ProgressBar()
|
---|
64 | pb_style = pb.get_style()
|
---|
65 | self.__text_font = pb_style.font_desc
|
---|
66 | self.__text_font.set_weight(pango.WEIGHT_BOLD)
|
---|
67 | # Done with the ProgressBar styles, don't keep refs of it
|
---|
68 | del pb, pb_style
|
---|
69 |
|
---|
70 | self.set_size_request(-1, 25)
|
---|
71 | self.gtkui_config = ConfigManager("gtkui.conf")
|
---|
72 | self.__width = self.__old_width = 0
|
---|
73 | self.__height = self.__old_height = 0
|
---|
74 | self.__pieces = self.__old_pieces = ()
|
---|
75 | self.__num_pieces = self.__old_num_pieces = None
|
---|
76 | self.__text = self.__old_text = ""
|
---|
77 | self.__fraction = self.__old_fraction = 0.0
|
---|
78 | self.__state = self.__old_state = None
|
---|
79 | self.__progress_overlay = self.__text_overlay = self.__pieces_overlay = None
|
---|
80 | self.__cr = None
|
---|
81 |
|
---|
82 | self.connect('size-allocate', self.do_size_allocate_event)
|
---|
83 | self.set_colormap(gtk.gdk.colormap_get_system())
|
---|
84 | self.show()
|
---|
85 |
|
---|
86 | def do_size_allocate_event(self, widget, size):
|
---|
87 | self.__old_width = self.__width
|
---|
88 | self.__width = size.width
|
---|
89 | self.__old_height = self.__height
|
---|
90 | self.__height = size.height
|
---|
91 |
|
---|
92 | # Handle the expose-event by drawing
|
---|
93 | def do_expose_event(self, event):
|
---|
94 | # Create cairo context
|
---|
95 | self.__cr = self.window.cairo_create()
|
---|
96 | self.__cr.set_line_width(max(self.__cr.device_to_user_distance(0.5, 0.5)))
|
---|
97 |
|
---|
98 | # Restrict Cairo to the exposed area; avoid extra work
|
---|
99 | self.__roundcorners_clipping()
|
---|
100 |
|
---|
101 | if not self.__pieces and self.__num_pieces is not None:
|
---|
102 | # Special case. Completed torrents do not send any pieces in their
|
---|
103 | # status.
|
---|
104 | self.__draw_pieces_completed()
|
---|
105 | elif self.__pieces:
|
---|
106 | self.__draw_pieces()
|
---|
107 |
|
---|
108 | self.__draw_progress_overlay()
|
---|
109 | self.__write_text()
|
---|
110 | self.__roundcorners_border()
|
---|
111 |
|
---|
112 | # Drawn once, update width, eight
|
---|
113 | if self.__resized():
|
---|
114 | self.__old_width = self.__width
|
---|
115 | self.__old_height = self.__height
|
---|
116 |
|
---|
117 | def __roundcorners_clipping(self):
|
---|
118 | self.__create_roundcorners_subpath(
|
---|
119 | self.__cr, 0, 0, self.__width, self.__height
|
---|
120 | )
|
---|
121 | self.__cr.clip()
|
---|
122 |
|
---|
123 | def __roundcorners_border(self):
|
---|
124 | self.__create_roundcorners_subpath(
|
---|
125 | self.__cr, 0.5, 0.5, self.__width-1, self.__height-1
|
---|
126 | )
|
---|
127 | self.__cr.set_source_rgba(0.0, 0.0, 0.0, 0.9)
|
---|
128 | self.__cr.stroke()
|
---|
129 |
|
---|
130 | def __create_roundcorners_subpath(self, ctx, x, y, width, height):
|
---|
131 | aspect = 1.0
|
---|
132 | corner_radius = height/10.0
|
---|
133 | radius = corner_radius/aspect
|
---|
134 | degrees = pi/180.0
|
---|
135 | ctx.new_sub_path()
|
---|
136 | ctx.arc(x + width - radius, y + radius, radius, -90 * degrees, 0 * degrees)
|
---|
137 | ctx.arc(x + width - radius, y + height - radius, radius, 0 * degrees, 90 * degrees)
|
---|
138 | ctx.arc(x + radius, y + height - radius, radius, 90 * degrees, 180 * degrees)
|
---|
139 | ctx.arc(x + radius, y + radius, radius, 180 * degrees, 270 * degrees)
|
---|
140 | ctx.close_path()
|
---|
141 | return ctx
|
---|
142 |
|
---|
143 | def __draw_pieces(self):
|
---|
144 | if (self.__resized() or self.__pieces != self.__old_pieces or
|
---|
145 | self.__pieces_overlay is None):
|
---|
146 | # Need to recreate the cache drawing
|
---|
147 | self.__pieces_overlay = cairo.ImageSurface(
|
---|
148 | cairo.FORMAT_ARGB32, self.__width, self.__height
|
---|
149 | )
|
---|
150 | ctx = cairo.Context(self.__pieces_overlay)
|
---|
151 | start_pos = 0
|
---|
152 | num_pieces = self.__num_pieces and self.__num_pieces or len(self.__pieces)
|
---|
153 | piece_width = self.__width*1.0/num_pieces
|
---|
154 |
|
---|
155 | for state in self.__pieces:
|
---|
156 | color = self.gtkui_config["pieces_color_%s" % COLOR_STATES[state]]
|
---|
157 | ctx.set_source_rgb(
|
---|
158 | color[0]/65535.0,
|
---|
159 | color[1]/65535.0,
|
---|
160 | color[2]/65535.0,
|
---|
161 | )
|
---|
162 | ctx.rectangle(start_pos, 0, piece_width, self.__height)
|
---|
163 | ctx.fill()
|
---|
164 | start_pos += piece_width
|
---|
165 |
|
---|
166 | self.__cr.set_source_surface(self.__pieces_overlay)
|
---|
167 | self.__cr.paint()
|
---|
168 |
|
---|
169 | def __draw_pieces_completed(self):
|
---|
170 | if (self.__resized() or self.__pieces != self.__old_pieces or
|
---|
171 | self.__pieces_overlay is None):
|
---|
172 | # Need to recreate the cache drawing
|
---|
173 | self.__pieces_overlay = cairo.ImageSurface(
|
---|
174 | cairo.FORMAT_ARGB32, self.__width, self.__height
|
---|
175 | )
|
---|
176 | ctx = cairo.Context(self.__pieces_overlay)
|
---|
177 | piece_width = self.__width*1.0/self.__num_pieces
|
---|
178 | start = 0
|
---|
179 | for _ in range(self.__num_pieces):
|
---|
180 | # Like this to keep same aspect ratio
|
---|
181 | color = self.gtkui_config["pieces_color_%s" % COLOR_STATES[3]]
|
---|
182 | ctx.set_source_rgb(
|
---|
183 | color[0]/65535.0,
|
---|
184 | color[1]/65535.0,
|
---|
185 | color[2]/65535.0,
|
---|
186 | )
|
---|
187 | ctx.rectangle(start, 0, piece_width, self.__height)
|
---|
188 | ctx.fill()
|
---|
189 | start += piece_width
|
---|
190 |
|
---|
191 | self.__cr.set_source_surface(self.__pieces_overlay)
|
---|
192 | self.__cr.paint()
|
---|
193 |
|
---|
194 | def __draw_progress_overlay(self):
|
---|
195 | if not self.__state:
|
---|
196 | # Nothing useful to draw, return now!
|
---|
197 | return
|
---|
198 | if (self.__resized() or self.__fraction != self.__old_fraction) or self.__progress_overlay is None:
|
---|
199 | # Need to recreate the cache drawing
|
---|
200 | self.__progress_overlay = cairo.ImageSurface(
|
---|
201 | cairo.FORMAT_ARGB32, self.__width, self.__height
|
---|
202 | )
|
---|
203 | ctx = cairo.Context(self.__progress_overlay)
|
---|
204 | ctx.set_source_rgba(0.1, 0.1, 0.1, 0.3) # Transparent
|
---|
205 | ctx.rectangle(0.0, 0.0, self.__width*self.__fraction, self.__height)
|
---|
206 | ctx.fill()
|
---|
207 | self.__cr.set_source_surface(self.__progress_overlay)
|
---|
208 | self.__cr.paint()
|
---|
209 |
|
---|
210 | def __write_text(self):
|
---|
211 | if not self.__state:
|
---|
212 | # Nothing useful to draw, return now!
|
---|
213 | return
|
---|
214 | if (self.__resized() or self.__text != self.__old_text or
|
---|
215 | self.__fraction != self.__old_fraction or
|
---|
216 | self.__state != self.__old_state or
|
---|
217 | self.__text_overlay is None):
|
---|
218 | # Need to recreate the cache drawing
|
---|
219 | self.__text_overlay = cairo.ImageSurface(
|
---|
220 | cairo.FORMAT_ARGB32, self.__width, self.__height
|
---|
221 | )
|
---|
222 | ctx = cairo.Context(self.__text_overlay)
|
---|
223 | pg = pangocairo.CairoContext(ctx)
|
---|
224 | pl = pg.create_layout()
|
---|
225 | pl.set_font_description(self.__text_font)
|
---|
226 | pl.set_width(-1) # No text wrapping
|
---|
227 |
|
---|
228 | text = ""
|
---|
229 | if self.__text:
|
---|
230 | text += self.__text
|
---|
231 | else:
|
---|
232 | if self.__state:
|
---|
233 | text += _(self.__state) + " "
|
---|
234 | if self.__fraction == 1.0:
|
---|
235 | format = "%d%%"
|
---|
236 | else:
|
---|
237 | format = "%.2f%%"
|
---|
238 | text += format % (self.__fraction*100)
|
---|
239 | log.trace("PiecesBar text %r", text)
|
---|
240 | pl.set_text(text)
|
---|
241 | plsize = pl.get_size()
|
---|
242 | text_width = plsize[0]/pango.SCALE
|
---|
243 | text_height = plsize[1]/pango.SCALE
|
---|
244 | area_width_without_text = self.__width - text_width
|
---|
245 | area_height_without_text = self.__height - text_height
|
---|
246 | ctx.move_to(area_width_without_text/2, area_height_without_text/2)
|
---|
247 | ctx.set_source_rgb(1.0, 1.0, 1.0)
|
---|
248 | pg.update_layout(pl)
|
---|
249 | pg.show_layout(pl)
|
---|
250 | self.__cr.set_source_surface(self.__text_overlay)
|
---|
251 | self.__cr.paint()
|
---|
252 |
|
---|
253 | def __resized(self):
|
---|
254 | return (self.__old_width != self.__width or
|
---|
255 | self.__old_height != self.__height)
|
---|
256 |
|
---|
257 | def set_fraction(self, fraction):
|
---|
258 | self.__old_fraction = self.__fraction
|
---|
259 | self.__fraction = fraction
|
---|
260 |
|
---|
261 | def get_fraction(self):
|
---|
262 | return self.__fraction
|
---|
263 |
|
---|
264 | def get_text(self):
|
---|
265 | return self.__text
|
---|
266 |
|
---|
267 | def set_text(self, text):
|
---|
268 | self.__old_text = self.__text
|
---|
269 | self.__text = text
|
---|
270 |
|
---|
271 | def set_pieces(self, pieces, num_pieces):
|
---|
272 | self.__old_pieces = self.__pieces
|
---|
273 | self.__pieces = pieces
|
---|
274 | self.__num_pieces = num_pieces
|
---|
275 |
|
---|
276 | def get_pieces(self):
|
---|
277 | return self.__pieces
|
---|
278 |
|
---|
279 | def set_state(self, state):
|
---|
280 | self.__old_state = self.__state
|
---|
281 | self.__state = state
|
---|
282 |
|
---|
283 | def get_state(self):
|
---|
284 | return self.__state
|
---|
285 |
|
---|
286 | def update_from_status(self, status):
|
---|
287 | log.trace("Updating PiecesBar from status")
|
---|
288 | self.set_fraction(status["progress"]/100)
|
---|
289 | torrent_state = status["state"]
|
---|
290 | self.set_state(torrent_state)
|
---|
291 | if torrent_state == "Checking":
|
---|
292 | self.update()
|
---|
293 | # Skip the pieces assignment
|
---|
294 | return
|
---|
295 |
|
---|
296 | self.set_pieces(status['pieces'], status['num_pieces'])
|
---|
297 | self.update()
|
---|
298 |
|
---|
299 | def clear(self):
|
---|
300 | self.__pieces = self.__old_pieces = ()
|
---|
301 | self.__num_pieces = self.__old_num_pieces = None
|
---|
302 | self.__text = self.__old_text = ""
|
---|
303 | self.__fraction = self.__old_fraction = 0.0
|
---|
304 | self.__state = self.__old_state = None
|
---|
305 | self.__progress_overlay = self.__text_overlay = self.__pieces_overlay = None
|
---|
306 | self.__cr = None
|
---|
307 | self.update()
|
---|
308 |
|
---|
309 | def update(self):
|
---|
310 | self.queue_draw()
|
---|