Fix various typos
[gpodder.git] / src / gpodder / gtkui / draw.py
blobf2904b3d9f74109b8c6e4c3720263052ac02e815
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
26 import io
27 import math
29 import cairo
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
46 RRECT_LEFT_SIDE = 1
47 RRECT_RIGHT_SIDE = 2
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
54 x = int(x)
55 offset = 0
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)
62 ctx.line_to(x, y + r)
63 ctx.curve_to(x, y, x, y, x + r, y)
64 ctx.line_to(x + int(left_side_width) - offset, y)
65 if close:
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)
75 if close:
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/
83 """
84 degrees = math.pi / 180.
85 ctx.new_sub_path()
86 if width > radius:
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)
91 ctx.close_path()
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)
98 if font_desc is None:
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:
114 bar_height = 10
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)
120 ctx.stroke()
121 rounded_rectangle(ctx,
122 w_width / 2 - width / 2,
123 w_height / 2 + height, int(width * add_progress) + .5, bar_height)
124 ctx.fill()
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..
132 if size is None:
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)
142 border = 1.5
143 height = int(size * .4)
144 width = size - 2 * border
145 y = (size - height) / 2 + .5
146 x = border
148 # Background
149 ctx.rectangle(x, y, width, height)
150 ctx.set_source_rgb(bgc.red, bgc.green, bgc.blue)
151 ctx.fill()
153 # Filling
154 if percentage > 0:
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)
158 ctx.fill()
160 # Border
161 ctx.rectangle(x, y, width, height)
162 ctx.set_source_rgb(txc.red, txc.green, txc.blue)
163 ctx.set_line_width(1)
164 ctx.stroke()
166 del ctx
167 return surface
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)
174 padding_right = 7
176 x_border = border * 2
178 if widget is None:
179 # Use GTK+ style of a normal Button
180 widget = Gtk.Label()
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
204 + padding_right))
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)
213 ctx.clip()
215 if left_text == '0':
216 left_text = None
217 if right_text == '0':
218 right_text = None
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)
232 ctx.fill()
233 xpos, ypos, width_left, height = x + 1, y + 1, left_side_width, rect_height - 2
234 if right_text is None:
235 width_left -= 2
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)
239 ctx.stroke()
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)
244 ctx.stroke()
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(
256 x + left_side_width,
258 x + left_side_width + right_side_width / 2,
259 y + rect_height)
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)
266 ctx.fill()
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)
273 ctx.stroke()
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)
277 ctx.stroke()
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)
286 return surface
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):
299 if widget is None:
300 # Use GTK+ style of a normal Button
301 widget = Gtk.Label()
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.
317 bio = io.BytesIO()
318 try:
319 s.write_to_png(bio)
320 except:
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())
334 pbl.close()
336 pixbuf = pbl.get_pixbuf()
337 return 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
354 # Background
355 ctx.rectangle(padding, padding, bar_width, bar_height)
356 ctx.set_source_rgba(*COLOR_BG)
357 ctx.fill()
359 # Foreground
360 ctx.rectangle(padding, padding + bar_height - bar_height_fill, bar_width, bar_height_fill)
361 ctx.set_source_rgba(*COLOR_FG)
362 ctx.fill()
363 ctx.rectangle(padding + bar_width / 3,
364 padding + bar_height - bar_height_fill,
365 bar_width / 4,
366 bar_height_fill)
367 ctx.set_source_rgba(*COLOR_FG_HIGH)
368 ctx.fill()
370 # Border
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.)
374 ctx.stroke()
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.
386 p = widget
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)
391 p = p.get_parent()
392 return color
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
402 p = widget
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)
407 p = p.get_parent()
408 return color
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):
422 style_ctx.save()
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))
429 colors = {
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")
447 style_ctx.restore()
449 with open('/tmp/colors.html', 'w') as f:
450 f.write("""<html>
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; }
456 </style>
457 <table>""")
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)
467 if c:
468 widget_path.iter_add_class(widget_path.length() - 1, c)
469 if r:
470 widget_path.iter_add_region(widget_path.length() - 1, r, 0)
471 style_ctx.set_path(widget_path)
473 investigate_stylecontext(
474 style_ctx,
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
497 if not pixbuf:
498 return
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)
510 cr.paint()
512 cell.props.surface = surface