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