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