1# -*- coding: utf-8 -*- 2# 3# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me> 4# 5# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with 6# the additional special exception to link portions of this program with the OpenSSL library. 7# See LICENSE for more details. 8# 9 10from __future__ import division, unicode_literals 11 12from math import pi 13 14import gi # isort:skip (Version check required before import). 15 16gi.require_version('PangoCairo', '1.0') # NOQA: E402 17gi.require_version('cairo', '1.0') # NOQA: E402 18 19# isort:imports-thirdparty 20import cairo # Backward compat cairo <= 1.15 21from gi.repository import PangoCairo 22from gi.repository.Gtk import DrawingArea, ProgressBar, StateFlags 23from gi.repository.Pango import SCALE, Weight 24 25# isort:imports-firstparty 26from deluge.common import PY2 27from deluge.configmanager import ConfigManager 28 29COLOR_STATES = ['missing', 'waiting', 'downloading', 'completed'] 30 31 32class PiecesBar(DrawingArea): 33 # Draw in response to an draw 34 __gsignals__ = {'draw': 'override'} if not PY2 else {b'draw': b'override'} 35 36 def __init__(self): 37 super(PiecesBar, self).__init__() 38 # Get progress bar styles, in order to keep font consistency 39 pb = ProgressBar() 40 pb_style = pb.get_style_context() 41 self.text_font = pb_style.get_property('font', StateFlags.NORMAL) 42 self.text_font.set_weight(Weight.BOLD) 43 # Done with the ProgressBar styles, don't keep refs of it 44 del pb, pb_style 45 46 self.set_size_request(-1, 25) 47 self.gtkui_config = ConfigManager('gtk3ui.conf') 48 49 self.width = self.prev_width = 0 50 self.height = self.prev_height = 0 51 self.pieces = self.prev_pieces = () 52 self.num_pieces = None 53 self.text = self.prev_text = '' 54 self.fraction = self.prev_fraction = 0 55 self.progress_overlay = self.text_overlay = self.pieces_overlay = None 56 self.cr = None 57 58 self.connect('size-allocate', self.do_size_allocate_event) 59 self.show() 60 61 def do_size_allocate_event(self, widget, size): 62 self.prev_width = self.width 63 self.width = size.width 64 self.prev_height = self.height 65 self.height = size.height 66 67 # Handle the draw by drawing 68 def do_draw(self, event): 69 # Create cairo context 70 self.cr = self.props.window.cairo_create() 71 self.cr.set_line_width(max(self.cr.device_to_user_distance(0.5, 0.5))) 72 73 # Restrict Cairo to the exposed area; avoid extra work 74 self.roundcorners_clipping() 75 76 self.draw_pieces() 77 self.draw_progress_overlay() 78 self.write_text() 79 self.roundcorners_border() 80 81 # Drawn once, update width, height 82 if self.resized(): 83 self.prev_width = self.width 84 self.prev_height = self.height 85 86 def roundcorners_clipping(self): 87 self.create_roundcorners_subpath(self.cr, 0, 0, self.width, self.height) 88 self.cr.clip() 89 90 def roundcorners_border(self): 91 self.create_roundcorners_subpath( 92 self.cr, 0.5, 0.5, self.width - 1, self.height - 1 93 ) 94 self.cr.set_source_rgba(0, 0, 0, 0.9) 95 self.cr.stroke() 96 97 @staticmethod 98 def create_roundcorners_subpath(ctx, x, y, width, height): 99 aspect = 1.0 100 corner_radius = height / 10 101 radius = corner_radius / aspect 102 degrees = pi / 180 103 ctx.new_sub_path() 104 ctx.arc(x + width - radius, y + radius, radius, -90 * degrees, 0 * degrees) 105 ctx.arc( 106 x + width - radius, y + height - radius, radius, 0 * degrees, 90 * degrees 107 ) 108 ctx.arc(x + radius, y + height - radius, radius, 90 * degrees, 180 * degrees) 109 ctx.arc(x + radius, y + radius, radius, 180 * degrees, 270 * degrees) 110 ctx.close_path() 111 return ctx 112 113 def draw_pieces(self): 114 if not self.num_pieces: 115 # Nothing to draw. 116 return 117 118 if ( 119 self.resized() 120 or self.pieces != self.prev_pieces 121 or self.pieces_overlay is None 122 ): 123 # Need to recreate the cache drawing 124 self.pieces_overlay = cairo.ImageSurface( 125 cairo.FORMAT_ARGB32, self.width, self.height 126 ) 127 ctx = cairo.Context(self.pieces_overlay) 128 129 if self.pieces: 130 pieces = self.pieces 131 elif self.num_pieces: 132 # Completed torrents do not send any pieces so create list using 'completed' state. 133 pieces = [COLOR_STATES.index('completed')] * self.num_pieces 134 start_pos = 0 135 piece_width = self.width / len(pieces) 136 pieces_colors = [ 137 [ 138 color / 65535 139 for color in self.gtkui_config['pieces_color_%s' % state] 140 ] 141 for state in COLOR_STATES 142 ] 143 for state in pieces: 144 ctx.set_source_rgb(*pieces_colors[state]) 145 ctx.rectangle(start_pos, 0, piece_width, self.height) 146 ctx.fill() 147 start_pos += piece_width 148 149 self.cr.set_source_surface(self.pieces_overlay) 150 self.cr.paint() 151 152 def draw_progress_overlay(self): 153 if not self.text: 154 # Nothing useful to draw, return now! 155 return 156 157 if ( 158 self.resized() 159 or self.fraction != self.prev_fraction 160 or self.progress_overlay is None 161 ): 162 # Need to recreate the cache drawing 163 self.progress_overlay = cairo.ImageSurface( 164 cairo.FORMAT_ARGB32, self.width, self.height 165 ) 166 ctx = cairo.Context(self.progress_overlay) 167 ctx.set_source_rgba(0.1, 0.1, 0.1, 0.3) # Transparent 168 ctx.rectangle(0, 0, self.width * self.fraction, self.height) 169 ctx.fill() 170 self.cr.set_source_surface(self.progress_overlay) 171 self.cr.paint() 172 173 def write_text(self): 174 if not self.text: 175 # Nothing useful to draw, return now! 176 return 177 178 if self.resized() or self.text != self.prev_text or self.text_overlay is None: 179 # Need to recreate the cache drawing 180 self.text_overlay = cairo.ImageSurface( 181 cairo.FORMAT_ARGB32, self.width, self.height 182 ) 183 ctx = cairo.Context(self.text_overlay) 184 pl = PangoCairo.create_layout(ctx) 185 pl.set_font_description(self.text_font) 186 pl.set_width(-1) # No text wrapping 187 pl.set_text(self.text, -1) 188 plsize = pl.get_size() 189 text_width = plsize[0] // SCALE 190 text_height = plsize[1] // SCALE 191 area_width_without_text = self.width - text_width 192 area_height_without_text = self.height - text_height 193 ctx.move_to(area_width_without_text // 2, area_height_without_text // 2) 194 ctx.set_source_rgb(1, 1, 1) 195 PangoCairo.update_layout(ctx, pl) 196 PangoCairo.show_layout(ctx, pl) 197 self.cr.set_source_surface(self.text_overlay) 198 self.cr.paint() 199 200 def resized(self): 201 return self.prev_width != self.width or self.prev_height != self.height 202 203 def set_fraction(self, fraction): 204 self.prev_fraction = self.fraction 205 self.fraction = fraction 206 207 def get_fraction(self): 208 return self.fraction 209 210 def get_text(self): 211 return self.text 212 213 def set_text(self, text): 214 self.prev_text = self.text 215 self.text = text 216 217 def set_pieces(self, pieces, num_pieces): 218 self.prev_pieces = self.pieces 219 self.pieces = pieces 220 self.num_pieces = num_pieces 221 222 def get_pieces(self): 223 return self.pieces 224 225 def clear(self): 226 self.pieces = self.prev_pieces = () 227 self.num_pieces = None 228 self.text = self.prev_text = '' 229 self.fraction = self.prev_fraction = 0 230 self.progress_overlay = self.text_overlay = self.pieces_overlay = None 231 self.cr = None 232 self.update() 233 234 def update(self): 235 self.queue_draw() 236