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