1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2018 The gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 # draw.py -- Draw routines for gPodder-specific graphics
23 # Thomas Perl <thp@perli.net>, 2007-11-25
31 import gi
# isort:skip
32 gi
.require_version('Gdk', '3.0') # isort:skip
33 gi
.require_version('Gtk', '3.0') # isort:skip
34 gi
.require_version('PangoCairo', '1.0') # isort:skip
35 from gi
.repository
import Gdk
, GdkPixbuf
, Gtk
, Pango
, PangoCairo
# isort:skip
38 class TextExtents(object):
39 def __init__(self
, ctx
, text
):
40 tuple = ctx
.text_extents(text
)
41 (self
.x_bearing
, self
.y_bearing
, self
.width
, self
.height
, self
.x_advance
, self
.y_advance
) = tuple
44 EPISODE_LIST_ICON_SIZE
= 16
50 def draw_rounded_rectangle(ctx
, x
, y
, w
, h
, r
=10, left_side_width
=None,
51 sides_to_draw
=0, close
=False):
52 assert left_side_width
is not None
56 if close
: offset
= 0.5
58 if sides_to_draw
& RRECT_LEFT_SIDE
:
59 ctx
.move_to(x
+ int(left_side_width
) - offset
, y
+ h
)
60 ctx
.line_to(x
+ r
, y
+ h
)
61 ctx
.curve_to(x
, y
+ h
, x
, y
+ h
, x
, y
+ h
- r
)
63 ctx
.curve_to(x
, y
, x
, y
, x
+ r
, y
)
64 ctx
.line_to(x
+ int(left_side_width
) - offset
, y
)
66 ctx
.line_to(x
+ int(left_side_width
) - offset
, y
+ h
)
68 if sides_to_draw
& RRECT_RIGHT_SIDE
:
69 ctx
.move_to(x
+ int(left_side_width
) + offset
, y
)
70 ctx
.line_to(x
+ w
- r
, y
)
71 ctx
.curve_to(x
+ w
, y
, x
+ w
, y
, x
+ w
, y
+ r
)
72 ctx
.line_to(x
+ w
, y
+ h
- r
)
73 ctx
.curve_to(x
+ w
, y
+ h
, x
+ w
, y
+ h
, x
+ w
- r
, y
+ h
)
74 ctx
.line_to(x
+ int(left_side_width
) + offset
, y
+ h
)
76 ctx
.line_to(x
+ int(left_side_width
) + offset
, y
)
79 def rounded_rectangle(ctx
, x
, y
, width
, height
, radius
=4.):
80 """Simple rounded rectangle algorithm
82 http://www.cairographics.org/samples/rounded_rectangle/
84 degrees
= math
.pi
/ 180.
87 ctx
.arc(x
+ width
- radius
, y
+ radius
, radius
, -90. * degrees
, 0)
88 ctx
.arc(x
+ width
- radius
, y
+ height
- radius
, radius
, 0, 90. * degrees
)
89 ctx
.arc(x
+ radius
, y
+ height
- radius
, radius
, 90. * degrees
, 180. * degrees
)
90 ctx
.arc(x
+ radius
, y
+ radius
, radius
, 180. * degrees
, 270. * degrees
)
94 def draw_text_box_centered(ctx
, widget
, w_width
, w_height
, text
, font_desc
=None, add_progress
=None):
95 style_context
= widget
.get_style_context()
96 text_color
= style_context
.get_color(Gtk
.StateFlags
.PRELIGHT
)
99 font_desc
= style_context
.get_font(Gtk
.StateFlags
.NORMAL
)
100 font_desc
.set_size(14 * Pango
.SCALE
)
102 pango_context
= widget
.create_pango_context()
103 layout
= Pango
.Layout(pango_context
)
104 layout
.set_font_description(font_desc
)
105 layout
.set_text(text
, -1)
106 width
, height
= layout
.get_pixel_size()
108 ctx
.move_to(w_width
/ 2 - width
/ 2, w_height
/ 2 - height
/ 2)
109 ctx
.set_source_rgba(text_color
.red
, text_color
.green
, text_color
.blue
, 0.5)
110 PangoCairo
.show_layout(ctx
, layout
)
112 # Draw an optional progress bar below the text (same width)
113 if add_progress
is not None:
115 ctx
.set_source_rgba(*text_color
)
116 ctx
.set_line_width(1.)
117 rounded_rectangle(ctx
,
118 w_width
/ 2 - width
/ 2 - .5,
119 w_height
/ 2 + height
- .5, width
+ 1, bar_height
+ 1)
121 rounded_rectangle(ctx
,
122 w_width
/ 2 - width
/ 2,
123 w_height
/ 2 + height
, int(width
* add_progress
) + .5, bar_height
)
127 def draw_cake(percentage
, text
=None, emblem
=None, size
=None):
128 # Download percentage bar icon - it turns out the cake is a lie (d'oh!)
129 # ..but the initial idea was to have a cake-style indicator, but that
130 # didn't work as well as the progress bar, but the name stuck..
133 size
= EPISODE_LIST_ICON_SIZE
135 surface
= cairo
.ImageSurface(cairo
.FORMAT_ARGB32
, size
, size
)
136 ctx
= cairo
.Context(surface
)
138 bgc
= get_background_color(Gtk
.StateFlags
.ACTIVE
)
139 fgc
= get_background_color(Gtk
.StateFlags
.SELECTED
)
140 txc
= get_foreground_color(Gtk
.StateFlags
.NORMAL
)
143 height
= int(size
* .4)
144 width
= size
- 2 * border
145 y
= (size
- height
) / 2 + .5
149 ctx
.rectangle(x
, y
, width
, height
)
150 ctx
.set_source_rgb(bgc
.red
, bgc
.green
, bgc
.blue
)
155 fill_width
= max(1, min(width
- 2, (width
- 2) * percentage
+ .5))
156 ctx
.rectangle(x
+ 1, y
+ 1, fill_width
, height
- 2)
157 ctx
.set_source_rgb(0.289, 0.5625, 0.84765625)
161 ctx
.rectangle(x
, y
, width
, height
)
162 ctx
.set_source_rgb(txc
.red
, txc
.green
, txc
.blue
)
163 ctx
.set_line_width(1)
170 def draw_text_pill(left_text
, right_text
, x
=0, y
=0, border
=2, radius
=14,
171 widget
=None, scale
=1):
173 # Padding (in px) at the right edge of the image (for Ubuntu; bug 1533)
176 x_border
= border
* 2
179 # Use GTK+ style of a normal Button
182 style_context
= widget
.get_style_context()
183 font_desc
= style_context
.get_font(Gtk
.StateFlags
.NORMAL
)
184 font_desc
.set_weight(Pango
.Weight
.BOLD
)
186 pango_context
= widget
.create_pango_context()
187 layout_left
= Pango
.Layout(pango_context
)
188 layout_left
.set_font_description(font_desc
)
189 layout_left
.set_text(left_text
, -1)
190 layout_right
= Pango
.Layout(pango_context
)
191 layout_right
.set_font_description(font_desc
)
192 layout_right
.set_text(right_text
, -1)
194 width_left
, height_left
= layout_left
.get_pixel_size()
195 width_right
, height_right
= layout_right
.get_pixel_size()
197 text_height
= max(height_left
, height_right
)
199 left_side_width
= width_left
+ x_border
* 2
200 right_side_width
= width_right
+ x_border
* 2
202 image_height
= int(scale
* (y
+ text_height
+ border
* 2))
203 image_width
= int(scale
* (x
+ left_side_width
+ right_side_width
206 surface
= cairo
.ImageSurface(cairo
.FORMAT_ARGB32
, image_width
, image_height
)
207 surface
.set_device_scale(scale
, scale
)
209 ctx
= cairo
.Context(surface
)
211 # Clip so as to not draw on the right padding (for Ubuntu; bug 1533)
212 ctx
.rectangle(0, 0, image_width
- padding_right
, image_height
)
217 if right_text
== '0':
220 rect_width
= left_side_width
+ right_side_width
221 rect_height
= text_height
+ border
* 2
222 if left_text
is not None:
223 draw_rounded_rectangle(ctx
, x
, y
, rect_width
, rect_height
, radius
,
224 left_side_width
, RRECT_LEFT_SIDE
, right_text
is None)
225 linear
= cairo
.LinearGradient(x
, y
, x
+ left_side_width
/ 2, y
+ rect_height
/ 2)
226 linear
.add_color_stop_rgba(0, .8, .8, .8, .5)
227 linear
.add_color_stop_rgba(.4, .8, .8, .8, .7)
228 linear
.add_color_stop_rgba(.6, .8, .8, .8, .6)
229 linear
.add_color_stop_rgba(.9, .8, .8, .8, .8)
230 linear
.add_color_stop_rgba(1, .8, .8, .8, .9)
231 ctx
.set_source(linear
)
233 xpos
, ypos
, width_left
, height
= x
+ 1, y
+ 1, left_side_width
, rect_height
- 2
234 if right_text
is None:
236 draw_rounded_rectangle(ctx
, xpos
, ypos
, rect_width
, height
, radius
, width_left
, RRECT_LEFT_SIDE
, right_text
is None)
237 ctx
.set_source_rgba(1., 1., 1., .3)
238 ctx
.set_line_width(1)
240 draw_rounded_rectangle(ctx
, x
, y
, rect_width
, rect_height
, radius
,
241 left_side_width
, RRECT_LEFT_SIDE
, right_text
is None)
242 ctx
.set_source_rgba(.2, .2, .2, .6)
243 ctx
.set_line_width(1)
246 ctx
.move_to(x
+ x_border
, y
+ 1 + border
)
247 ctx
.set_source_rgba(0, 0, 0, 1)
248 PangoCairo
.show_layout(ctx
, layout_left
)
249 ctx
.move_to(x
- 1 + x_border
, y
+ border
)
250 ctx
.set_source_rgba(1, 1, 1, 1)
251 PangoCairo
.show_layout(ctx
, layout_left
)
253 if right_text
is not None:
254 draw_rounded_rectangle(ctx
, x
, y
, rect_width
, rect_height
, radius
, left_side_width
, RRECT_RIGHT_SIDE
, left_text
is None)
255 linear
= cairo
.LinearGradient(
258 x
+ left_side_width
+ right_side_width
/ 2,
260 linear
.add_color_stop_rgba(0, .2, .2, .2, .9)
261 linear
.add_color_stop_rgba(.4, .2, .2, .2, .8)
262 linear
.add_color_stop_rgba(.6, .2, .2, .2, .6)
263 linear
.add_color_stop_rgba(.9, .2, .2, .2, .7)
264 linear
.add_color_stop_rgba(1, .2, .2, .2, .5)
265 ctx
.set_source(linear
)
267 xpos
, ypos
, width
, height
= x
, y
+ 1, rect_width
- 1, rect_height
- 2
268 if left_text
is None:
269 xpos
, width
= x
+ 1, rect_width
- 2
270 draw_rounded_rectangle(ctx
, xpos
, ypos
, width
, height
, radius
, left_side_width
, RRECT_RIGHT_SIDE
, left_text
is None)
271 ctx
.set_source_rgba(1., 1., 1., .3)
272 ctx
.set_line_width(1)
274 draw_rounded_rectangle(ctx
, x
, y
, rect_width
, rect_height
, radius
, left_side_width
, RRECT_RIGHT_SIDE
, left_text
is None)
275 ctx
.set_source_rgba(.1, .1, .1, .6)
276 ctx
.set_line_width(1)
279 ctx
.move_to(x
+ left_side_width
+ x_border
, y
+ 1 + border
)
280 ctx
.set_source_rgba(0, 0, 0, 1)
281 PangoCairo
.show_layout(ctx
, layout_right
)
282 ctx
.move_to(x
- 1 + left_side_width
+ x_border
, y
+ border
)
283 ctx
.set_source_rgba(1, 1, 1, 1)
284 PangoCairo
.show_layout(ctx
, layout_right
)
289 def draw_cake_pixbuf(percentage
, text
=None, emblem
=None, size
=None):
290 return cairo_surface_to_pixbuf(draw_cake(percentage
, text
, emblem
, size
=size
))
293 def draw_pill_pixbuf(left_text
, right_text
, widget
=None, scale
=1):
294 return cairo_surface_to_pixbuf(draw_text_pill(left_text
, right_text
,
295 widget
=widget
, scale
=scale
))
298 def cake_size_from_widget(widget
=None):
300 # Use GTK+ style of a normal Button
302 style_context
= widget
.get_style_context()
303 font_desc
= style_context
.get_font(Gtk
.StateFlags
.NORMAL
)
304 pango_context
= widget
.create_pango_context()
305 layout
= Pango
.Layout(pango_context
)
306 layout
.set_font_description(font_desc
)
307 layout
.set_text("1", -1)
308 # use text height as size
309 return layout
.get_pixel_size()[1]
312 def cairo_surface_to_pixbuf(s
):
314 Converts a Cairo surface to a Gtk Pixbuf by
315 encoding it as PNG and using the PixbufLoader.
321 # Write an empty PNG file to the StringIO, so
322 # in case of an error we have "something" to
323 # load. This happens in PyCairo < 1.1.6, see:
324 # http://webcvs.cairographics.org/pycairo/NEWS?view=markup
325 # Thanks to Chris Arnold for reporting this bug
326 bio
.write('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4'
327 'c6QAAAAZiS0dEAP8A\n/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAA'
328 'AAd0SU1FB9cMEQkqIyxn3RkAAAAZdEVYdENv\nbW1lbnQAQ3JlYXRlZCB3a'
329 'XRoIEdJTVBXgQ4XAAAADUlEQVQI12NgYGBgAAAABQABXvMqOgAAAABJ\nRU'
330 '5ErkJggg==\n'.decode('base64'))
332 pbl
= GdkPixbuf
.PixbufLoader()
333 pbl
.write(bio
.getvalue())
336 pixbuf
= pbl
.get_pixbuf()
340 def progressbar_pixbuf(width
, height
, percentage
):
341 COLOR_BG
= (.4, .4, .4, .4)
342 COLOR_FG
= (.2, .9, .2, 1.)
343 COLOR_FG_HIGH
= (1., 1., 1., .5)
344 COLOR_BORDER
= (0., 0., 0., 1.)
346 surface
= cairo
.ImageSurface(cairo
.FORMAT_ARGB32
, width
, height
)
347 ctx
= cairo
.Context(surface
)
349 padding
= int(width
/ 8.0)
350 bar_width
= 2 * padding
351 bar_height
= height
- 2 * padding
352 bar_height_fill
= bar_height
* percentage
355 ctx
.rectangle(padding
, padding
, bar_width
, bar_height
)
356 ctx
.set_source_rgba(*COLOR_BG
)
360 ctx
.rectangle(padding
, padding
+ bar_height
- bar_height_fill
, bar_width
, bar_height_fill
)
361 ctx
.set_source_rgba(*COLOR_FG
)
363 ctx
.rectangle(padding
+ bar_width
/ 3,
364 padding
+ bar_height
- bar_height_fill
,
367 ctx
.set_source_rgba(*COLOR_FG_HIGH
)
371 ctx
.rectangle(padding
- .5, padding
- .5, bar_width
+ 1, bar_height
+ 1)
372 ctx
.set_source_rgba(* COLOR_BORDER
)
373 ctx
.set_line_width(1.)
376 return cairo_surface_to_pixbuf(surface
)
379 def get_background_color(state
=Gtk
.StateFlags
.NORMAL
, widget
=Gtk
.TreeView()):
381 @param state state flag (e.g. Gtk.StateFlags.SELECTED to get selected background)
382 @param widget specific widget to get info from.
383 defaults to TreeView which has all one usually wants.
384 @return background color from theme for widget or from its parents if transparent.
387 color
= Gdk
.RGBA(0, 0, 0, 0)
388 while p
is not None and color
.alpha
== 0:
389 style_context
= p
.get_style_context()
390 color
= style_context
.get_background_color(state
)
395 def get_foreground_color(state
=Gtk
.StateFlags
.NORMAL
, widget
=Gtk
.TreeView()):
397 @param state state flag (e.g. Gtk.StateFlags.SELECTED to get selected text color)
398 @param widget specific widget to get info from
399 defaults to TreeView which has all one usually wants.
400 @return text color from theme for widget or its parents if transparent
403 color
= Gdk
.RGBA(0, 0, 0, 0)
404 while p
is not None and color
.alpha
== 0:
405 style_context
= p
.get_style_context()
406 color
= style_context
.get_color(state
)
411 def investigate_widget_colors(type_classes_and_widgets
):
413 investigate using Gtk.StyleContext to get widget style properties
414 I tried to compare gettings values from static and live widgets.
415 To sum up, better use the live widget, because you'll get the correct path, classes, regions automatically.
416 See "CSS Nodes" in widget documentation for classes and sub-nodes (=regions).
417 WidgetPath and Region are replaced by CSSNodes in gtk4.
418 Not sure it's legitimate usage, though: I got different results from one run to another.
419 Run `GTK_DEBUG=interactive ./bin/gpodder` for gtk widget inspection
421 def investigate_stylecontext(style_ctx
, label
):
423 for statename
, state
in [
424 ('normal', Gtk
.StateFlags
.NORMAL
),
425 ('active', Gtk
.StateFlags
.ACTIVE
),
426 ('link', Gtk
.StateFlags
.LINK
),
427 ('visited', Gtk
.StateFlags
.VISITED
)]:
428 f
.write("<dt>%s %s</dt><dd>\n" % (label
, statename
))
430 'get_color': style_ctx
.get_color(state
),
431 'get_background_color': style_ctx
.get_background_color(state
),
432 'color': style_ctx
.get_property('color', state
),
433 'background-color': style_ctx
.get_property('background-color', state
),
434 'outline-color': style_ctx
.get_property('outline-color', state
),
436 f
.write("<p>PREVIEW: <span style='background-color: %s; color: %s'>get_color + get_background_color</span>"
437 % (colors
['get_background_color'].to_string(),
438 colors
['get_color'].to_string()))
439 f
.write("<span style='background-color: %s; color: %s; border solid 2px %s;'>color + background-color properties</span></p>\n"
440 % (colors
['background-color'].to_string(),
441 colors
['color'].to_string(),
442 colors
['outline-color'].to_string()))
443 f
.write("<p>VALUES: ")
444 for p
, v
in colors
.items():
445 f
.write("%s=<span style='background-color: %s;'>%s</span>" % (p
, v
.to_string(), v
.to_string()))
446 f
.write("</p></dd>\n")
449 with
open('/tmp/colors.html', 'w') as f
:
451 <style type='text/css'>
452 body {color: red; background: yellow;}
453 span { display: inline-block; margin-right: 1ch; }
454 dd { margin-bottom: 1em; }
455 td { vertical-align: top; }
458 for type_and_class
, w
in type_classes_and_widgets
:
459 f
.write("<tr><td><dl>\n")
460 # Create an empty style context
461 style_ctx
= Gtk
.StyleContext()
462 # Create an empty widget path
463 widget_path
= Gtk
.WidgetPath()
464 # Specify the widget class type you want to get colors from
465 for t
, c
, r
in type_and_class
:
466 widget_path
.append_type(t
)
468 widget_path
.iter_add_class(widget_path
.length() - 1, c
)
470 widget_path
.iter_add_region(widget_path
.length() - 1, r
, 0)
471 style_ctx
.set_path(widget_path
)
473 investigate_stylecontext(
475 'STATIC {}'.format(' '.join('{}.{}({})'.format(t
.__name
__, c
, r
) for t
, c
, r
in type_and_class
)))
477 f
.write("</dl></td><td><dl>\n")
479 investigate_stylecontext(w
.get_style_context(), 'LIVE {}'.format(type(w
).__name
__))
481 f
.write("</dl></td></tr>\n")
482 f
.write("</table></html>\n")
485 def draw_iconcell_scale(column
, cell
, model
, iter, scale
):
487 Draw cell's pixbuf to a surface with proper scaling for high resolution
488 displays. To be used as gtk.TreeViewColumn.set_cell_data_func.
490 :param column: gtk.TreeViewColumn (ignored)
491 :param cell: gtk.CellRenderer
492 :param model: gtk.TreeModel (ignored)
493 :param iter: gtk.TreeIter (ignored)
494 :param scale: factor of the target display (e.g. 1 or 2)
496 pixbuf
= cell
.props
.pixbuf
500 width
= pixbuf
.get_width()
501 height
= pixbuf
.get_height()
502 scale_inv
= 1 / scale
504 surface
= cairo
.ImageSurface(cairo
.FORMAT_ARGB32
, width
, height
)
505 surface
.set_device_scale(scale
, scale
)
507 cr
= cairo
.Context(surface
)
508 cr
.scale(scale_inv
, scale_inv
)
509 Gdk
.cairo_set_source_pixbuf(cr
, cell
.props
.pixbuf
, 0, 0)
512 cell
.props
.surface
= surface