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 extents
= ctx
.text_extents(text
)
41 (self
.x_bearing
, self
.y_bearing
, self
.width
, self
.height
, self
.x_advance
, self
.y_advance
) = extents
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
59 if sides_to_draw
& RRECT_LEFT_SIDE
:
60 ctx
.move_to(x
+ int(left_side_width
) - offset
, y
+ h
)
61 ctx
.line_to(x
+ r
, y
+ h
)
62 ctx
.curve_to(x
, y
+ h
, x
, y
+ h
, x
, y
+ h
- r
)
64 ctx
.curve_to(x
, y
, x
, y
, x
+ r
, y
)
65 ctx
.line_to(x
+ int(left_side_width
) - offset
, y
)
67 ctx
.line_to(x
+ int(left_side_width
) - offset
, y
+ h
)
69 if sides_to_draw
& RRECT_RIGHT_SIDE
:
70 ctx
.move_to(x
+ int(left_side_width
) + offset
, y
)
71 ctx
.line_to(x
+ w
- r
, y
)
72 ctx
.curve_to(x
+ w
, y
, x
+ w
, y
, x
+ w
, y
+ r
)
73 ctx
.line_to(x
+ w
, y
+ h
- r
)
74 ctx
.curve_to(x
+ w
, y
+ h
, x
+ w
, y
+ h
, x
+ w
- r
, y
+ h
)
75 ctx
.line_to(x
+ int(left_side_width
) + offset
, y
+ h
)
77 ctx
.line_to(x
+ int(left_side_width
) + offset
, y
)
80 def rounded_rectangle(ctx
, x
, y
, width
, height
, radius
=4.):
81 """Simple rounded rectangle algorithm
83 http://www.cairographics.org/samples/rounded_rectangle/
85 degrees
= math
.pi
/ 180.
88 ctx
.arc(x
+ width
- radius
, y
+ radius
, radius
, -90. * degrees
, 0)
89 ctx
.arc(x
+ width
- radius
, y
+ height
- radius
, radius
, 0, 90. * degrees
)
90 ctx
.arc(x
+ radius
, y
+ height
- radius
, radius
, 90. * degrees
, 180. * degrees
)
91 ctx
.arc(x
+ radius
, y
+ radius
, radius
, 180. * degrees
, 270. * degrees
)
95 def draw_text_box_centered(ctx
, widget
, w_width
, w_height
, text
, font_desc
=None, add_progress
=None):
96 style_context
= widget
.get_style_context()
97 text_color
= style_context
.get_color(Gtk
.StateFlags
.PRELIGHT
)
100 font_desc
= style_context
.get_font(Gtk
.StateFlags
.NORMAL
)
101 font_desc
.set_size(14 * Pango
.SCALE
)
103 pango_context
= widget
.create_pango_context()
104 layout
= Pango
.Layout(pango_context
)
105 layout
.set_font_description(font_desc
)
106 layout
.set_text(text
, -1)
107 width
, height
= layout
.get_pixel_size()
109 ctx
.move_to(w_width
/ 2 - width
/ 2, w_height
/ 2 - height
/ 2)
110 ctx
.set_source_rgba(text_color
.red
, text_color
.green
, text_color
.blue
, 0.5)
111 PangoCairo
.show_layout(ctx
, layout
)
113 # Draw an optional progress bar below the text (same width)
114 if add_progress
is not None:
116 ctx
.set_source_rgba(*text_color
)
117 ctx
.set_line_width(1.)
118 rounded_rectangle(ctx
,
119 w_width
/ 2 - width
/ 2 - .5,
120 w_height
/ 2 + height
- .5, width
+ 1, bar_height
+ 1)
122 rounded_rectangle(ctx
,
123 w_width
/ 2 - width
/ 2,
124 w_height
/ 2 + height
, int(width
* add_progress
) + .5, bar_height
)
128 def draw_cake(percentage
, text
=None, emblem
=None, size
=None):
129 # Download percentage bar icon - it turns out the cake is a lie (d'oh!)
130 # ..but the initial idea was to have a cake-style indicator, but that
131 # didn't work as well as the progress bar, but the name stuck..
134 size
= EPISODE_LIST_ICON_SIZE
136 surface
= cairo
.ImageSurface(cairo
.FORMAT_ARGB32
, size
, size
)
137 ctx
= cairo
.Context(surface
)
139 bgc
= get_background_color(Gtk
.StateFlags
.ACTIVE
)
140 # fgc = get_background_color(Gtk.StateFlags.SELECTED)
141 txc
= get_foreground_color(Gtk
.StateFlags
.NORMAL
)
144 height
= int(size
* .4)
145 width
= size
- 2 * border
146 y
= (size
- height
) / 2 + .5
150 ctx
.rectangle(x
, y
, width
, height
)
151 ctx
.set_source_rgb(bgc
.red
, bgc
.green
, bgc
.blue
)
156 fill_width
= max(1, min(width
- 2, (width
- 2) * percentage
+ .5))
157 ctx
.rectangle(x
+ 1, y
+ 1, fill_width
, height
- 2)
158 ctx
.set_source_rgb(0.289, 0.5625, 0.84765625)
162 ctx
.rectangle(x
, y
, width
, height
)
163 ctx
.set_source_rgb(txc
.red
, txc
.green
, txc
.blue
)
164 ctx
.set_line_width(1)
171 def draw_text_pill(left_text
, right_text
, x
=0, y
=0, border
=2, radius
=14,
172 widget
=None, scale
=1):
174 # Padding (in px) at the right edge of the image (for Ubuntu; bug 1533)
177 x_border
= border
* 2
180 # Use GTK+ style of a normal Button
183 style_context
= widget
.get_style_context()
184 font_desc
= style_context
.get_font(Gtk
.StateFlags
.NORMAL
)
185 font_desc
.set_weight(Pango
.Weight
.BOLD
)
187 pango_context
= widget
.create_pango_context()
188 layout_left
= Pango
.Layout(pango_context
)
189 layout_left
.set_font_description(font_desc
)
190 layout_left
.set_text(left_text
, -1)
191 layout_right
= Pango
.Layout(pango_context
)
192 layout_right
.set_font_description(font_desc
)
193 layout_right
.set_text(right_text
, -1)
195 width_left
, height_left
= layout_left
.get_pixel_size()
196 width_right
, height_right
= layout_right
.get_pixel_size()
198 text_height
= max(height_left
, height_right
)
200 left_side_width
= width_left
+ x_border
* 2
201 right_side_width
= width_right
+ x_border
* 2
203 image_height
= int(scale
* (y
+ text_height
+ border
* 2))
204 image_width
= int(scale
* (x
+ left_side_width
+ right_side_width
207 surface
= cairo
.ImageSurface(cairo
.FORMAT_ARGB32
, image_width
, image_height
)
208 surface
.set_device_scale(scale
, scale
)
210 ctx
= cairo
.Context(surface
)
212 # Clip so as to not draw on the right padding (for Ubuntu; bug 1533)
213 ctx
.rectangle(0, 0, image_width
- padding_right
, image_height
)
218 if right_text
== '0':
221 rect_width
= left_side_width
+ right_side_width
222 rect_height
= text_height
+ border
* 2
223 if left_text
is not None:
224 draw_rounded_rectangle(ctx
, x
, y
, rect_width
, rect_height
, radius
,
225 left_side_width
, RRECT_LEFT_SIDE
, right_text
is None)
226 linear
= cairo
.LinearGradient(x
, y
, x
+ left_side_width
/ 2, y
+ rect_height
/ 2)
227 linear
.add_color_stop_rgba(0, .8, .8, .8, .5)
228 linear
.add_color_stop_rgba(.4, .8, .8, .8, .7)
229 linear
.add_color_stop_rgba(.6, .8, .8, .8, .6)
230 linear
.add_color_stop_rgba(.9, .8, .8, .8, .8)
231 linear
.add_color_stop_rgba(1, .8, .8, .8, .9)
232 ctx
.set_source(linear
)
234 xpos
, ypos
, width_left
, height
= x
+ 1, y
+ 1, left_side_width
, rect_height
- 2
235 if right_text
is None:
237 draw_rounded_rectangle(ctx
, xpos
, ypos
, rect_width
, height
, radius
, width_left
, RRECT_LEFT_SIDE
, right_text
is None)
238 ctx
.set_source_rgba(1., 1., 1., .3)
239 ctx
.set_line_width(1)
241 draw_rounded_rectangle(ctx
, x
, y
, rect_width
, rect_height
, radius
,
242 left_side_width
, RRECT_LEFT_SIDE
, right_text
is None)
243 ctx
.set_source_rgba(.2, .2, .2, .6)
244 ctx
.set_line_width(1)
247 ctx
.move_to(x
+ x_border
, y
+ 1 + border
)
248 ctx
.set_source_rgba(0, 0, 0, 1)
249 PangoCairo
.show_layout(ctx
, layout_left
)
250 ctx
.move_to(x
- 1 + x_border
, y
+ border
)
251 ctx
.set_source_rgba(1, 1, 1, 1)
252 PangoCairo
.show_layout(ctx
, layout_left
)
254 if right_text
is not None:
255 draw_rounded_rectangle(ctx
, x
, y
, rect_width
, rect_height
, radius
, left_side_width
, RRECT_RIGHT_SIDE
, left_text
is None)
256 linear
= cairo
.LinearGradient(
259 x
+ left_side_width
+ right_side_width
/ 2,
261 linear
.add_color_stop_rgba(0, .2, .2, .2, .9)
262 linear
.add_color_stop_rgba(.4, .2, .2, .2, .8)
263 linear
.add_color_stop_rgba(.6, .2, .2, .2, .6)
264 linear
.add_color_stop_rgba(.9, .2, .2, .2, .7)
265 linear
.add_color_stop_rgba(1, .2, .2, .2, .5)
266 ctx
.set_source(linear
)
268 xpos
, ypos
, width
, height
= x
, y
+ 1, rect_width
- 1, rect_height
- 2
269 if left_text
is None:
270 xpos
, width
= x
+ 1, rect_width
- 2
271 draw_rounded_rectangle(ctx
, xpos
, ypos
, width
, height
, radius
, left_side_width
, RRECT_RIGHT_SIDE
, left_text
is None)
272 ctx
.set_source_rgba(1., 1., 1., .3)
273 ctx
.set_line_width(1)
275 draw_rounded_rectangle(ctx
, x
, y
, rect_width
, rect_height
, radius
, left_side_width
, RRECT_RIGHT_SIDE
, left_text
is None)
276 ctx
.set_source_rgba(.1, .1, .1, .6)
277 ctx
.set_line_width(1)
280 ctx
.move_to(x
+ left_side_width
+ x_border
, y
+ 1 + border
)
281 ctx
.set_source_rgba(0, 0, 0, 1)
282 PangoCairo
.show_layout(ctx
, layout_right
)
283 ctx
.move_to(x
- 1 + left_side_width
+ x_border
, y
+ border
)
284 ctx
.set_source_rgba(1, 1, 1, 1)
285 PangoCairo
.show_layout(ctx
, layout_right
)
290 def draw_cake_pixbuf(percentage
, text
=None, emblem
=None, size
=None):
291 return cairo_surface_to_pixbuf(draw_cake(percentage
, text
, emblem
, size
=size
))
294 def draw_pill_pixbuf(left_text
, right_text
, widget
=None, scale
=1):
295 return cairo_surface_to_pixbuf(draw_text_pill(left_text
, right_text
,
296 widget
=widget
, scale
=scale
))
299 def cake_size_from_widget(widget
=None):
301 # Use GTK+ style of a normal Button
303 style_context
= widget
.get_style_context()
304 font_desc
= style_context
.get_font(Gtk
.StateFlags
.NORMAL
)
305 pango_context
= widget
.create_pango_context()
306 layout
= Pango
.Layout(pango_context
)
307 layout
.set_font_description(font_desc
)
308 layout
.set_text("1", -1)
309 # use text height as size
310 return layout
.get_pixel_size()[1]
313 def cairo_surface_to_pixbuf(s
):
315 Converts a Cairo surface to a Gtk Pixbuf by
316 encoding it as PNG and using the PixbufLoader.
322 # Write an empty PNG file to the StringIO, so
323 # in case of an error we have "something" to
324 # load. This happens in PyCairo < 1.1.6, see:
325 # http://webcvs.cairographics.org/pycairo/NEWS?view=markup
326 # Thanks to Chris Arnold for reporting this bug
327 bio
.write('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4'
328 'c6QAAAAZiS0dEAP8A\n/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAA'
329 'AAd0SU1FB9cMEQkqIyxn3RkAAAAZdEVYdENv\nbW1lbnQAQ3JlYXRlZCB3a'
330 'XRoIEdJTVBXgQ4XAAAADUlEQVQI12NgYGBgAAAABQABXvMqOgAAAABJ\nRU'
331 '5ErkJggg==\n'.decode('base64'))
333 pbl
= GdkPixbuf
.PixbufLoader()
334 pbl
.write(bio
.getvalue())
337 pixbuf
= pbl
.get_pixbuf()
341 def progressbar_pixbuf(width
, height
, percentage
):
342 COLOR_BG
= (.4, .4, .4, .4)
343 COLOR_FG
= (.2, .9, .2, 1.)
344 COLOR_FG_HIGH
= (1., 1., 1., .5)
345 COLOR_BORDER
= (0., 0., 0., 1.)
347 surface
= cairo
.ImageSurface(cairo
.FORMAT_ARGB32
, width
, height
)
348 ctx
= cairo
.Context(surface
)
350 padding
= int(width
/ 8.0)
351 bar_width
= 2 * padding
352 bar_height
= height
- 2 * padding
353 bar_height_fill
= bar_height
* percentage
356 ctx
.rectangle(padding
, padding
, bar_width
, bar_height
)
357 ctx
.set_source_rgba(*COLOR_BG
)
361 ctx
.rectangle(padding
, padding
+ bar_height
- bar_height_fill
, bar_width
, bar_height_fill
)
362 ctx
.set_source_rgba(*COLOR_FG
)
364 ctx
.rectangle(padding
+ bar_width
/ 3,
365 padding
+ bar_height
- bar_height_fill
,
368 ctx
.set_source_rgba(*COLOR_FG_HIGH
)
372 ctx
.rectangle(padding
- .5, padding
- .5, bar_width
+ 1, bar_height
+ 1)
373 ctx
.set_source_rgba(* COLOR_BORDER
)
374 ctx
.set_line_width(1.)
377 return cairo_surface_to_pixbuf(surface
)
380 def get_background_color(state
=Gtk
.StateFlags
.NORMAL
, widget
=Gtk
.TreeView()):
382 @param state state flag (e.g. Gtk.StateFlags.SELECTED to get selected background)
383 @param widget specific widget to get info from.
384 defaults to TreeView which has all one usually wants.
385 @return background color from theme for widget or from its parents if transparent.
388 color
= Gdk
.RGBA(0, 0, 0, 0)
389 while p
is not None and color
.alpha
== 0:
390 style_context
= p
.get_style_context()
391 color
= style_context
.get_background_color(state
)
396 def get_foreground_color(state
=Gtk
.StateFlags
.NORMAL
, widget
=Gtk
.TreeView()):
398 @param state state flag (e.g. Gtk.StateFlags.SELECTED to get selected text color)
399 @param widget specific widget to get info from
400 defaults to TreeView which has all one usually wants.
401 @return text color from theme for widget or its parents if transparent
404 color
= Gdk
.RGBA(0, 0, 0, 0)
405 while p
is not None and color
.alpha
== 0:
406 style_context
= p
.get_style_context()
407 color
= style_context
.get_color(state
)
412 def investigate_widget_colors(type_classes_and_widgets
):
414 investigate using Gtk.StyleContext to get widget style properties
415 I tried to compare values from static and live widgets.
416 To sum up, better use the live widget, because you'll get the correct path, classes, regions automatically.
417 See "CSS Nodes" in widget documentation for classes and sub-nodes (=regions).
418 WidgetPath and Region are replaced by CSSNodes in gtk4.
419 Not sure it's legitimate usage, though: I got different results from one run to another.
420 Run `GTK_DEBUG=interactive ./bin/gpodder` for gtk widget inspection
422 def investigate_stylecontext(style_ctx
, label
):
424 for statename
, state
in [
425 ('normal', Gtk
.StateFlags
.NORMAL
),
426 ('active', Gtk
.StateFlags
.ACTIVE
),
427 ('link', Gtk
.StateFlags
.LINK
),
428 ('visited', Gtk
.StateFlags
.VISITED
)]:
429 f
.write("<dt>%s %s</dt><dd>\n" % (label
, statename
))
431 'get_color': style_ctx
.get_color(state
),
432 'get_background_color': style_ctx
.get_background_color(state
),
433 'color': style_ctx
.get_property('color', state
),
434 'background-color': style_ctx
.get_property('background-color', state
),
435 'outline-color': style_ctx
.get_property('outline-color', state
),
437 f
.write("<p>PREVIEW: <span style='background-color: %s; color: %s'>get_color + get_background_color</span>"
438 % (colors
['get_background_color'].to_string(),
439 colors
['get_color'].to_string()))
440 f
.write("<span style='background-color: %s; color: %s; border solid 2px %s;'>color + background-color properties</span></p>\n"
441 % (colors
['background-color'].to_string(),
442 colors
['color'].to_string(),
443 colors
['outline-color'].to_string()))
444 f
.write("<p>VALUES: ")
445 for p
, v
in colors
.items():
446 f
.write("%s=<span style='background-color: %s;'>%s</span>" % (p
, v
.to_string(), v
.to_string()))
447 f
.write("</p></dd>\n")
450 with
open('/tmp/colors.html', 'w') as f
:
452 <style type='text/css'>
453 body {color: red; background: yellow;}
454 span { display: inline-block; margin-right: 1ch; }
455 dd { margin-bottom: 1em; }
456 td { vertical-align: top; }
459 for type_and_class
, w
in type_classes_and_widgets
:
460 f
.write("<tr><td><dl>\n")
461 # Create an empty style context
462 style_ctx
= Gtk
.StyleContext()
463 # Create an empty widget path
464 widget_path
= Gtk
.WidgetPath()
465 # Specify the widget class type you want to get colors from
466 for t
, c
, r
in type_and_class
:
467 widget_path
.append_type(t
)
469 widget_path
.iter_add_class(widget_path
.length() - 1, c
)
471 widget_path
.iter_add_region(widget_path
.length() - 1, r
, 0)
472 style_ctx
.set_path(widget_path
)
474 investigate_stylecontext(
476 'STATIC {}'.format(' '.join('{}.{}({})'.format(t
.__name
__, c
, r
) for t
, c
, r
in type_and_class
)))
478 f
.write("</dl></td><td><dl>\n")
480 investigate_stylecontext(w
.get_style_context(), 'LIVE {}'.format(type(w
).__name
__))
482 f
.write("</dl></td></tr>\n")
483 f
.write("</table></html>\n")
486 def draw_iconcell_scale(column
, cell
, model
, iterator
, scale
):
488 Draw cell's pixbuf to a surface with proper scaling for high resolution
489 displays. To be used as gtk.TreeViewColumn.set_cell_data_func.
491 :param column: gtk.TreeViewColumn (ignored)
492 :param cell: gtk.CellRenderer
493 :param model: gtk.TreeModel (ignored)
494 :param iter: gtk.TreeIter (ignored)
495 :param scale: factor of the target display (e.g. 1 or 2)
497 pixbuf
= cell
.props
.pixbuf
501 width
= pixbuf
.get_width()
502 height
= pixbuf
.get_height()
503 scale_inv
= 1 / scale
505 surface
= cairo
.ImageSurface(cairo
.FORMAT_ARGB32
, width
, height
)
506 surface
.set_device_scale(scale
, scale
)
508 cr
= cairo
.Context(surface
)
509 cr
.scale(scale_inv
, scale_inv
)
510 Gdk
.cairo_set_source_pixbuf(cr
, cell
.props
.pixbuf
, 0, 0)
513 cell
.props
.surface
= surface