1# This file is part of MyPaint.
2# Copyright (C) 2012-2018 by the MyPaint Development Team
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# (at your option) any later version.
8
9"""Overlays for TDWs showing information about the TDW state."""
10
11
12## Imports
13
14from __future__ import division, print_function
15from math import pi
16from gettext import gettext as _
17
18from lib.gibindings import Pango
19from lib.gibindings import PangoCairo
20from lib.gibindings import GLib
21import cairo
22
23from lib.helpers import clamp
24import gui.style
25
26
27## Base classes and utils
28
29class Overlay (object):
30    """Base class/interface for objects which paint things over a TDW."""
31
32    def paint(self, cr):
33        """Paint information onto a TiledDrawWidget.
34
35        The drawing interface is very simple. `cr` is a Cairo context in either
36        display coordinates or model coordinates: which one you get depends on
37        which list the Overlay is appended to on its tdw.
38        """
39        pass
40
41
42class FadingOverlay (Overlay):
43    """Base class for temporary overlays which fade to alpha over a short time
44    """
45
46    # Overridable animation controls
47    fade_fps = 20  #: Nominal frames per second
48    fade_duration = 1.5  #: Time for fading entirely to zero, in seconds
49
50    # Animation and redrawing state
51    tdw = None
52    alpha = 1.0
53    __area = None
54    __anim_srcid = None
55
56    def __init__(self, doc):
57        Overlay.__init__(self)
58        self.tdw = doc.tdw
59
60    def paint(self, cr):
61        """Repaint the overlay and start animating if necessary.
62
63        Individual frames are handled by `paint_frame()`.
64        """
65        if self.overlay_changed():
66            self.alpha = 1.0
67        elif self.alpha <= 0:
68            # No need to draw anything.
69            return
70        self.__restart_anim_if_needed()
71        self.__area = self.paint_frame(cr)
72
73    def anim_cb(self):
74        """Animation callback.
75
76        Each step fades the alpha multiplier slightly and invalidates the area
77        last painted.
78        """
79        self.alpha -= 1 / (self.fade_fps * self.fade_duration)
80        self.alpha = clamp(self.alpha, 0.0, 1.0)
81
82        if self.__area:
83            self.tdw.queue_draw_area(*self.__area)
84        if self.alpha <= 0.0:
85            self.__anim_srcid = None
86            return False
87        else:
88            return True
89
90    def __restart_anim_if_needed(self):
91        """Restart if not currently running, without changing the alpha.
92        """
93        if self.__anim_srcid is None:
94            delay = int(1000 // self.fade_fps)
95            self.__anim_srcid = GLib.timeout_add(delay, self.anim_cb)
96
97    def stop_anim(self):
98        """Stops the animation after the next frame is drawn.
99        """
100        self.alpha = 0.0
101
102    def start_anim(self):
103        """Restarts the animation, setting alpha to 1.
104        """
105        self.alpha = 1.0
106        self.__restart_anim_if_needed()
107
108    def paint_frame(self, cr):
109        """Paint a single frame.
110        """
111        raise NotImplementedError
112
113    def overlay_changed(self):
114        """Return true if the overlay has changed.
115
116        This virtual method is called by paint() to determine whether the
117        alpha should be reset to 1.0 and the fade begun anew.
118        """
119        raise NotImplementedError
120
121
122def rounded_box(cr, x, y, w, h, r):
123    """Paint a rounded box path into a Cairo context.
124
125    The position is given by `x` and `y`, and the size by `w` and `h`. The
126    cornders are of radius `r`, and must be smaller than half the minimum
127    dimension. The path is created as a new, closed subpath.
128    """
129    assert r <= min(w, h) / 2
130    cr.new_sub_path()
131    cr.arc(x+r, y+r, r, pi, pi*1.5)
132    cr.line_to(x+w-r, y)
133    cr.arc(x+w-r, y+r, r, pi*1.5, pi*2)
134    cr.line_to(x+w, y+h-r)
135    cr.arc(x+w-r, y+h-r, r, 0, pi*0.5)
136    cr.line_to(x+r, y+h)
137    cr.arc(x+r, y+h-r, r, pi*0.5, pi)
138    cr.close_path()
139
140
141## Minor builtin overlays
142
143
144class ScaleOverlay (FadingOverlay):
145    """Overlays its TDW's current zoom, fading to transparent.
146
147    The animation is started by the normal full canvas repaint which happens
148    after the scale changes.
149    """
150
151    vmargin = 6
152    hmargin = 12
153    padding = 6
154    shown_scale = None
155
156    def overlay_changed(self):
157        return self.tdw.scale != self.shown_scale
158
159    def paint_frame(self, cr):
160        self.shown_scale = self.tdw.scale
161        text = _("Zoom: %.01f%%") % (100*self.shown_scale)
162        layout = self.tdw.create_pango_layout(text)
163
164        # Set a bold font
165        font = layout.get_font_description()
166        if font is None:  # inherited from context
167            font = layout.get_context().get_font_description()
168            font = font.copy()
169        font.set_weight(Pango.Weight.BOLD)
170        layout.set_font_description(font)
171
172        # General dimensions
173        alloc = self.tdw.get_allocation()
174        lw, lh = layout.get_pixel_size()
175
176        # Background rectangle
177        hm = self.hmargin
178        vm = self.hmargin
179        w = alloc.width
180        p = self.padding
181        area = bx, by, bw, bh = w-lw-hm-p-p, vm, lw+p+p, lh+p+p
182        rounded_box(cr, bx, by, bw, bh, p)
183        rgba = list(gui.style.TRANSIENT_INFO_BG_RGBA)
184        rgba[3] *= self.alpha
185        cr.set_source_rgba(*rgba)
186        cr.fill()
187
188        # Text
189        cr.translate(w-lw-hm-p, vm+p)
190        rgba = list(gui.style.TRANSIENT_INFO_RGBA)
191        rgba[3] *= self.alpha
192        cr.set_source_rgba(*rgba)
193        PangoCairo.show_layout(cr, layout)
194
195        # Where to invalidate
196        return area
197
198
199class LastPaintPosOverlay (FadingOverlay):
200    """Displays the last painting position after a stroke has finished.
201
202    Not especially useful, but serves as an example of how to drive an overlay
203    from user input events.
204    """
205
206    inner_line_rgba = gui.style.TRANSIENT_INFO_RGBA
207    inner_line_width = 6
208    outer_line_rgba = gui.style.TRANSIENT_INFO_BG_RGBA
209    outer_line_width = 8
210    radius = 4.0
211
212    def __init__(self, doc):
213        FadingOverlay.__init__(self, doc)
214        doc.input_stroke_started += self.input_stroke_started
215        doc.input_stroke_ended += self.input_stroke_ended
216        self.current_marker_pos = None
217        self.in_input_stroke = False
218
219    def input_stroke_started(self, doc, event):
220        self.in_input_stroke = True
221        if self.current_marker_pos is None:
222            return
223        # Clear the current marker
224        model_x, model_y = self.current_marker_pos
225        x, y = self.tdw.model_to_display(model_x, model_y)
226        area = self._calc_area(x, y)
227        self.tdw.queue_draw_area(*area)
228        self.current_marker_pos = None
229        self.stop_anim()
230
231    def input_stroke_ended(self, doc, event):
232        self.in_input_stroke = False
233        if self.tdw.last_painting_pos is None:
234            return
235        # Record the new marker position
236        model_x, model_y = self.tdw.last_painting_pos
237        x, y = self.tdw.model_to_display(model_x, model_y)
238        area = self._calc_area(x, y)
239        self.tdw.queue_draw_area(*area)
240        self.current_marker_pos = model_x, model_y
241        self.start_anim()
242
243    def overlay_changed(self):
244        return False
245
246    def _calc_area(self, x, y):
247        r = self.radius
248        lw = max(self.inner_line_width, self.outer_line_width)
249        return (int(x-r-lw), int(y-r-lw), int(2*(r+lw)), int(2*(r+lw)))
250
251    def paint_frame(self, cr):
252        if self.in_input_stroke:
253            return
254        if self.current_marker_pos is None:
255            return
256        x, y = self.tdw.model_to_display(*self.current_marker_pos)
257        area = self._calc_area(x, y)
258        x = int(x) + 0.5
259        y = int(y) + 0.5
260        r = self.radius
261        cr.set_line_cap(cairo.LINE_CAP_ROUND)
262        rgba = list(self.outer_line_rgba)
263        rgba[3] *= self.alpha
264        cr.set_source_rgba(*rgba)
265        cr.set_line_width(self.outer_line_width)
266        cr.move_to(x-r, y-r)
267        cr.line_to(x+r, y+r)
268        cr.move_to(x+r, y-r)
269        cr.line_to(x-r, y+r)
270        cr.stroke_preserve()
271        rgba = list(self.inner_line_rgba)
272        rgba[3] *= self.alpha
273        cr.set_source_rgba(*rgba)
274        cr.set_line_width(self.inner_line_width)
275        cr.stroke()
276        return area
277