1# Copyright (C) 2012-13 Thomas Vogt 2# Copyright (C) 2012-17 Nick Boultbee 3# Copyright (C) 2008 Andreas Bombe 4# Copyright (C) 2005 Michael Urman 5# Based on osd.py (C) 2005 Ton van den Heuvel, Joe Wreshnig 6# (C) 2004 Gustavo J. A. M. Carneiro 7# 8# This program is free software; you can redistribute it and/or modify 9# it under the terms of the GNU General Public License as published by 10# the Free Software Foundation; either version 2 of the License, or 11# (at your option) any later version. 12 13from collections import namedtuple 14from math import pi 15 16import gi 17gi.require_version("PangoCairo", "1.0") 18 19from gi.repository import Gtk, GObject, GLib 20from gi.repository import Gdk 21from gi.repository import Pango, PangoCairo 22import cairo 23 24from quodlibet.qltk.image import get_surface_for_pixbuf, get_surface_extents 25from quodlibet import qltk 26from quodlibet import app 27from quodlibet import pattern 28 29 30class OSDWindow(Gtk.Window): 31 32 __gsignals__ = { 33 'fade-finished': (GObject.SignalFlags.RUN_LAST, None, (bool,)), 34 } 35 36 MARGIN = 50 37 """never any closer to the screen edge than this""" 38 39 BORDER = 20 40 """text/cover this far apart, from edge""" 41 42 FADETIME = 0.3 43 """take this many seconds to fade in or out""" 44 45 MS = 40 46 """wait this many milliseconds between steps""" 47 48 def __init__(self, conf, song): 49 Gtk.Window.__init__(self, type=Gtk.WindowType.POPUP) 50 self.set_type_hint(Gdk.WindowTypeHint.NOTIFICATION) 51 52 screen = self.get_screen() 53 rgba = screen.get_rgba_visual() 54 if rgba is not None: 55 self.set_visual(rgba) 56 57 self.conf = conf 58 self.iteration_source = None 59 self.fading_in = False 60 self.fade_start_time = 0 61 62 mgeo = screen.get_monitor_geometry(conf.monitor) 63 textwidth = mgeo.width - 2 * (self.BORDER + self.MARGIN) 64 65 scale_factor = self.get_scale_factor() 66 cover_pixbuf = app.cover_manager.get_pixbuf( 67 song, conf.coversize * scale_factor, conf.coversize * scale_factor) 68 coverheight = 0 69 coverwidth = 0 70 if cover_pixbuf: 71 self.cover_surface = get_surface_for_pixbuf(self, cover_pixbuf) 72 coverwidth = cover_pixbuf.get_width() // scale_factor 73 coverheight = cover_pixbuf.get_height() // scale_factor 74 textwidth -= coverwidth + self.BORDER 75 else: 76 self.cover_surface = None 77 78 layout = self.create_pango_layout('') 79 layout.set_alignment((Pango.Alignment.LEFT, Pango.Alignment.CENTER, 80 Pango.Alignment.RIGHT)[conf.align]) 81 layout.set_spacing(Pango.SCALE * 7) 82 layout.set_font_description(Pango.FontDescription(conf.font)) 83 try: 84 layout.set_markup(pattern.XMLFromMarkupPattern(conf.string) % song) 85 except pattern.error: 86 layout.set_markup("") 87 layout.set_width(Pango.SCALE * textwidth) 88 layoutsize = layout.get_pixel_size() 89 if layoutsize[0] < textwidth: 90 layout.set_width(Pango.SCALE * layoutsize[0]) 91 layoutsize = layout.get_pixel_size() 92 self.title_layout = layout 93 94 winw = layoutsize[0] + 2 * self.BORDER 95 if coverwidth: 96 winw += coverwidth + self.BORDER 97 winh = max(coverheight, layoutsize[1]) + 2 * self.BORDER 98 self.set_default_size(winw, winh) 99 100 rect = namedtuple("Rect", ["x", "y", "width", "height"]) 101 rect.x = self.BORDER 102 rect.y = (winh - coverheight) // 2 103 rect.width = coverwidth 104 rect.height = coverheight 105 106 self.cover_rectangle = rect 107 108 winx = int((mgeo.width - winw) * conf.pos_x) 109 winx = max(self.MARGIN, min(mgeo.width - self.MARGIN - winw, winx)) 110 winy = int((mgeo.height - winh) * conf.pos_y) 111 winy = max(self.MARGIN, min(mgeo.height - self.MARGIN - winh, winy)) 112 self.move(winx + mgeo.x, winy + mgeo.y) 113 114 def do_draw(self, cr): 115 if self.is_composited(): 116 self.draw_title_info(cr) 117 else: 118 # manual transparency rendering follows 119 walloc = self.get_allocation() 120 wpos = self.get_position() 121 122 if not getattr(self, "_bg_sf", None): 123 # copy the root surface into a temp image surface 124 root_win = self.get_root_window() 125 bg_sf = cairo.ImageSurface(cairo.FORMAT_ARGB32, 126 walloc.width, walloc.height) 127 pb = Gdk.pixbuf_get_from_window( 128 root_win, wpos[0], wpos[1], walloc.width, walloc.height) 129 bg_cr = cairo.Context(bg_sf) 130 Gdk.cairo_set_source_pixbuf(bg_cr, pb, 0, 0) 131 bg_cr.paint() 132 self._bg_sf = bg_sf 133 134 if not getattr(self, "_fg_sf", None): 135 # draw the window content in another temp surface 136 fg_sf = cairo.ImageSurface(cairo.FORMAT_ARGB32, 137 walloc.width, walloc.height) 138 fg_cr = cairo.Context(fg_sf) 139 fg_cr.set_source_surface(fg_sf) 140 self.draw_title_info(fg_cr) 141 self._fg_sf = fg_sf 142 143 # first draw the background so we have 'transparancy' 144 cr.set_operator(cairo.OPERATOR_SOURCE) 145 cr.set_source_surface(self._bg_sf) 146 cr.paint() 147 148 # then draw the window content with the right opacity 149 cr.set_operator(cairo.OPERATOR_OVER) 150 cr.set_source_surface(self._fg_sf) 151 cr.paint_with_alpha(self.get_opacity()) 152 153 @staticmethod 154 def rounded_rectangle(cr, x, y, radius, width, height): 155 cr.move_to(x + radius, y) 156 cr.line_to(x + width - radius, y) 157 cr.arc(x + width - radius, y + radius, radius, 158 - 90.0 * pi / 180.0, 0.0 * pi / 180.0) 159 cr.line_to(x + width, y + height - radius) 160 cr.arc(x + width - radius, y + height - radius, radius, 161 0.0 * pi / 180.0, 90.0 * pi / 180.0) 162 cr.line_to(x + radius, y + height) 163 cr.arc(x + radius, y + height - radius, radius, 164 90.0 * pi / 180.0, 180.0 * pi / 180.0) 165 cr.line_to(x, y + radius) 166 cr.arc(x + radius, y + radius, radius, 167 180.0 * pi / 180.0, 270.0 * pi / 180.0) 168 cr.close_path() 169 170 @property 171 def corners_factor(self): 172 if self.conf.corners != 0: 173 return 0.14 174 return 0.0 175 176 def draw_conf_rect(self, cr, x, y, width, height, radius): 177 if self.conf.corners != 0: 178 self.rounded_rectangle(cr, x, y, radius, width, height) 179 else: 180 cr.rectangle(x, y, width, height) 181 182 def draw_title_info(self, cr): 183 cr.save() 184 do_shadow = (self.conf.shadow[0] != -1.0) 185 do_outline = (self.conf.outline[0] != -1.0) 186 187 self.set_name("osd_bubble") 188 qltk.add_css(self, """ 189 #osd_bubble { 190 background-color:rgba(0,0,0,0); 191 } 192 """) 193 194 cr.set_operator(cairo.OPERATOR_OVER) 195 cr.set_source_rgba(*self.conf.fill) 196 radius = min(25, self.corners_factor * min(*self.get_size())) 197 self.draw_conf_rect(cr, 0, 0, self.get_size()[0], 198 self.get_size()[1], radius) 199 cr.fill() 200 201 # draw border 202 if do_outline: 203 # Make border darker and more translucent than the fill 204 f = self.conf.fill 205 rgba = (f[0] / 1.25, f[1] / 1.25, f[2] / 1.25, f[3] / 2.0) 206 cr.set_source_rgba(*rgba) 207 self.draw_conf_rect(cr, 208 1, 1, 209 self.get_size()[0] - 2, self.get_size()[1] - 2, 210 radius) 211 cr.set_line_width(2.0) 212 cr.stroke() 213 214 textx = self.BORDER 215 216 if self.cover_surface is not None: 217 rect = self.cover_rectangle 218 textx += rect.width + self.BORDER 219 surface = self.cover_surface 220 transmat = cairo.Matrix() 221 222 if do_shadow: 223 cr.set_source_rgba(*self.conf.shadow) 224 self.draw_conf_rect(cr, 225 rect.x + 2, rect.y + 2, 226 rect.width, rect.height, 227 0.6 * self.corners_factor * rect.width) 228 cr.fill() 229 230 if do_outline: 231 cr.set_source_rgba(*self.conf.outline) 232 self.draw_conf_rect(cr, 233 rect.x, rect.y, 234 rect.width, rect.height, 235 0.6 * self.corners_factor * rect.width) 236 cr.stroke() 237 238 cr.set_source_surface(surface, 0, 0) 239 width, height = get_surface_extents(surface)[2:] 240 241 transmat.scale(width / float(rect.width), 242 height / float(rect.height)) 243 transmat.translate(-rect.x, -rect.y) 244 cr.get_source().set_matrix(transmat) 245 self.draw_conf_rect(cr, 246 rect.x, rect.y, 247 rect.width, rect.height, 248 0.6 * self.corners_factor * rect.width) 249 cr.fill() 250 251 PangoCairo.update_layout(cr, self.title_layout) 252 height = self.title_layout.get_pixel_size()[1] 253 texty = (self.get_size()[1] - height) // 2 254 255 if do_shadow: 256 cr.set_source_rgba(*self.conf.shadow) 257 cr.move_to(textx + 2, texty + 2) 258 PangoCairo.show_layout(cr, self.title_layout) 259 if do_outline: 260 cr.set_source_rgba(*self.conf.outline) 261 cr.move_to(textx, texty) 262 PangoCairo.layout_path(cr, self.title_layout) 263 cr.stroke() 264 cr.set_source_rgb(*self.conf.text[:3]) 265 cr.move_to(textx, texty) 266 PangoCairo.show_layout(cr, self.title_layout) 267 cr.restore() 268 269 def fade_in(self): 270 self.do_fade_inout(True) 271 272 def fade_out(self): 273 self.do_fade_inout(False) 274 275 def do_fade_inout(self, fadein): 276 fadein = bool(fadein) 277 self.fading_in = fadein 278 now = GLib.get_real_time() 279 280 fraction = self.get_opacity() 281 if not fadein: 282 fraction = 1.0 - fraction 283 self.fade_start_time = now - fraction * self.FADETIME 284 285 if self.iteration_source is None: 286 self.iteration_source = GLib.timeout_add(self.MS, 287 self.fade_iteration_callback) 288 289 def fade_iteration_callback(self): 290 delta = GLib.get_real_time() - self.fade_start_time 291 fraction = delta / self.FADETIME 292 293 if self.fading_in: 294 self.set_opacity(fraction) 295 else: 296 self.set_opacity(1.0 - fraction) 297 298 if not self.is_composited(): 299 self.queue_draw() 300 301 if fraction >= 1.0: 302 self.iteration_source = None 303 self.emit('fade-finished', self.fading_in) 304 return False 305 return True 306