Skip to content

Commit

Permalink
[GTKUI] Fix cairo crashes by not storing current context
Browse files Browse the repository at this point in the history
Windows users have reported Deluge crashes when resizing the window with
Piecesbar or Stats plugins enabled:

    Expression: CAIRO_REFERENCE_COUNT_HAS_REFERENCE(&surface->ref_count)

This is similar to issues fixed in GNU Radio which is a problem due to
storing the current cairo context which is then being destroyed and
recreated within GTK causing a reference count error

Fixes: https://dev.deluge-torrent.org/ticket/3339
Refs: gnuradio/gnuradio#6352
  • Loading branch information
cas-- committed Aug 28, 2023
1 parent a459e78 commit 439c039
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 118 deletions.
144 changes: 74 additions & 70 deletions deluge/plugins/Stats/deluge_stats/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,20 +104,19 @@ def set_stats(self, stats):
def set_interval(self, interval):
self.interval = interval

def draw_to_context(self, context, width, height):
self.ctx = context
def draw_to_context(self, ctx, width, height):
self.width, self.height = width, height
self.draw_rect(white, 0, 0, self.width, self.height)
self.draw_graph()
return self.ctx
self.draw_rect(ctx, white, 0, 0, self.width, self.height)
self.draw_graph(ctx)

def draw(self, width, height):
"""Create surface with context for use in tests"""
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
ctx = cairo.Context(surface)
self.draw_to_context(ctx, width, height)
return surface

def draw_x_axis(self, bounds):
def draw_x_axis(self, ctx, bounds):
(left, top, right, bottom) = bounds
duration = self.length * self.interval
start = self.last_update - duration
Expand All @@ -142,13 +141,13 @@ def draw_x_axis(self, bounds):
)
# + 0.5 to allign x to nearest pixel
x = int(ratio * (seconds_to_step + i * x_step) + left) + 0.5
self.draw_x_text(text, x, bottom)
self.draw_dotted_line(gray, x, top - 0.5, x, bottom + 0.5)
self.draw_x_text(ctx, text, x, bottom)
self.draw_dotted_line(ctx, gray, x, top - 0.5, x, bottom + 0.5)

self.draw_line(gray, left, bottom + 0.5, right, bottom + 0.5)
self.draw_line(ctx, gray, left, bottom + 0.5, right, bottom + 0.5)

def draw_graph(self):
font_extents = self.ctx.font_extents()
def draw_graph(self, ctx):
font_extents = ctx.font_extents()
x_axis_space = font_extents[2] + 2 + self.line_size / 2
plot_height = self.height - x_axis_space
# lets say we need 2n-1*font height pixels to plot the y ticks
Expand All @@ -171,18 +170,18 @@ def draw_graph(self):
# find the width of the y_ticks
y_tick_text = [self.left_axis['formatter'](tick) for tick in y_ticks]

def space_required(text):
te = self.ctx.text_extents(text)
def space_required(ctx, text):
te = ctx.text_extents(text)
return math.ceil(te[4] - te[0])

y_tick_width = max(space_required(text) for text in y_tick_text)
y_tick_width = max(space_required(ctx, text) for text in y_tick_text)

top = font_extents[2] / 2
# bounds(left, top, right, bottom)
bounds = (y_tick_width + 4, top + 2, self.width, self.height - x_axis_space)

self.draw_x_axis(bounds)
self.draw_left_axis(bounds, y_ticks, y_tick_text)
self.draw_x_axis(ctx, bounds)
self.draw_left_axis(ctx, bounds, y_ticks, y_tick_text)

def intervalise(self, x, limit=None):
"""Given a value x create an array of tick points to got with the graph
Expand Down Expand Up @@ -229,7 +228,7 @@ def intervalise(self, x, limit=None):
]
return intervals

def draw_left_axis(self, bounds, y_ticks, y_tick_text):
def draw_left_axis(self, ctx, bounds, y_ticks, y_tick_text):
(left, top, right, bottom) = bounds
stats = {}
for stat in self.stat_info:
Expand All @@ -246,94 +245,99 @@ def draw_left_axis(self, bounds, y_ticks, y_tick_text):
for i, y_val in enumerate(y_ticks):
y = int(bottom - y_val * ratio) - 0.5
if i != 0:
self.draw_dotted_line(gray, left, y, right, y)
self.draw_y_text(y_tick_text[i], left, y)
self.draw_line(gray, left, top, left, bottom)
self.draw_dotted_line(ctx, gray, left, y, right, y)
self.draw_y_text(ctx, y_tick_text[i], left, y)
self.draw_line(ctx, gray, left, top, left, bottom)

for stat, info in stats.items():
if len(info['values']) > 0:
self.draw_value_poly(info['values'], info['color'], max_value, bounds)
self.draw_value_poly(
info['values'], info['fill_color'], max_value, bounds, info['fill']
ctx, info['values'], info['color'], max_value, bounds
)
self.draw_value_poly(
ctx,
info['values'],
info['fill_color'],
max_value,
bounds,
info['fill'],
)

def draw_legend(self):
pass

def trace_path(self, values, max_value, bounds):
def trace_path(self, ctx, values, max_value, bounds):
(left, top, right, bottom) = bounds
ratio = (bottom - top) / max_value
line_width = self.line_size

self.ctx.set_line_width(line_width)
self.ctx.move_to(right, bottom)
ctx.set_line_width(line_width)
ctx.move_to(right, bottom)

self.ctx.line_to(right, int(bottom - values[0] * ratio))
ctx.line_to(right, int(bottom - values[0] * ratio))

x = right
step = (right - left) / (self.length - 1)
for i, value in enumerate(values):
if i == self.length - 1:
x = left

self.ctx.line_to(x, int(bottom - value * ratio))
ctx.line_to(x, int(bottom - value * ratio))
x -= step

self.ctx.line_to(int(right - (len(values) - 1) * step), bottom)
self.ctx.close_path()
ctx.line_to(int(right - (len(values) - 1) * step), bottom)
ctx.close_path()

def draw_value_poly(self, values, color, max_value, bounds, fill=False):
self.trace_path(values, max_value, bounds)
self.ctx.set_source_rgba(*color)
def draw_value_poly(self, ctx, values, color, max_value, bounds, fill=False):
self.trace_path(ctx, values, max_value, bounds)
ctx.set_source_rgba(*color)

if fill:
self.ctx.fill()
ctx.fill()
else:
self.ctx.stroke()
ctx.stroke()

def draw_x_text(self, text, x, y):
def draw_x_text(self, ctx, text, x, y):
"""Draws text below and horizontally centered about x,y"""
fe = self.ctx.font_extents()
te = self.ctx.text_extents(text)
fe = ctx.font_extents()
te = ctx.text_extents(text)
height = fe[2]
x_bearing = te[0]
width = te[2]
self.ctx.move_to(int(x - width / 2 + x_bearing), int(y + height))
self.ctx.set_source_rgba(*self.black)
self.ctx.show_text(text)
ctx.move_to(int(x - width / 2 + x_bearing), int(y + height))
ctx.set_source_rgba(*self.black)
ctx.show_text(text)

def draw_y_text(self, text, x, y):
def draw_y_text(self, ctx, text, x, y):
"""Draws text left of and vertically centered about x,y"""
fe = self.ctx.font_extents()
te = self.ctx.text_extents(text)
fe = ctx.font_extents()
te = ctx.text_extents(text)
descent = fe[1]
ascent = fe[0]
x_bearing = te[0]
width = te[4]
self.ctx.move_to(
int(x - width - x_bearing - 2), int(y + (ascent - descent) / 2)
)
self.ctx.set_source_rgba(*self.black)
self.ctx.show_text(text)

def draw_rect(self, color, x, y, height, width):
self.ctx.set_source_rgba(*color)
self.ctx.rectangle(x, y, height, width)
self.ctx.fill()

def draw_line(self, color, x1, y1, x2, y2):
self.ctx.set_source_rgba(*color)
self.ctx.set_line_width(1)
self.ctx.move_to(x1, y1)
self.ctx.line_to(x2, y2)
self.ctx.stroke()

def draw_dotted_line(self, color, x1, y1, x2, y2):
self.ctx.set_source_rgba(*color)
self.ctx.set_line_width(1)
dash, offset = self.ctx.get_dash()
self.ctx.set_dash(self.dash_length, 0)
self.ctx.move_to(x1, y1)
self.ctx.line_to(x2, y2)
self.ctx.stroke()
self.ctx.set_dash(dash, offset)
ctx.move_to(int(x - width - x_bearing - 2), int(y + (ascent - descent) / 2))
ctx.set_source_rgba(*self.black)
ctx.show_text(text)

def draw_rect(self, ctx, color, x, y, height, width):
ctx.set_source_rgba(*color)
ctx.rectangle(x, y, height, width)
ctx.fill()

def draw_line(self, ctx, color, x1, y1, x2, y2):
ctx.set_source_rgba(*color)
ctx.set_line_width(1)
ctx.move_to(x1, y1)
ctx.line_to(x2, y2)
ctx.stroke()

def draw_dotted_line(self, ctx, color, x1, y1, x2, y2):
ctx.set_source_rgba(*color)
ctx.set_line_width(1)
dash, offset = ctx.get_dash()
ctx.set_dash(self.dash_length, 0)
ctx.move_to(x1, y1)
ctx.line_to(x2, y2)
ctx.stroke()
ctx.set_dash(dash, offset)
88 changes: 40 additions & 48 deletions deluge/ui/gtk3/piecesbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ def __init__(self):
self.text = self.prev_text = ''
self.fraction = self.prev_fraction = 0
self.progress_overlay = self.text_overlay = self.pieces_overlay = None
self.cr = None

self.connect('size-allocate', self.do_size_allocate_event)
self.show()
Expand All @@ -63,34 +62,30 @@ def do_size_allocate_event(self, widget, size):
self.height = size.height

# Handle the draw by drawing
def do_draw(self, event):
# Create cairo context
self.cr = self.props.window.cairo_create()
self.cr.set_line_width(max(self.cr.device_to_user_distance(0.5, 0.5)))
def do_draw(self, ctx):
ctx.set_line_width(max(ctx.device_to_user_distance(0.5, 0.5)))

# Restrict Cairo to the exposed area; avoid extra work
self.roundcorners_clipping()
self.roundcorners_clipping(ctx)

self.draw_pieces()
self.draw_progress_overlay()
self.write_text()
self.roundcorners_border()
self.draw_pieces(ctx)
self.draw_progress_overlay(ctx)
self.write_text(ctx)
self.roundcorners_border(ctx)

# Drawn once, update width, height
if self.resized():
self.prev_width = self.width
self.prev_height = self.height

def roundcorners_clipping(self):
self.create_roundcorners_subpath(self.cr, 0, 0, self.width, self.height)
self.cr.clip()
def roundcorners_clipping(self, ctx):
self.create_roundcorners_subpath(ctx, 0, 0, self.width, self.height)
ctx.clip()

def roundcorners_border(self):
self.create_roundcorners_subpath(
self.cr, 0.5, 0.5, self.width - 1, self.height - 1
)
self.cr.set_source_rgba(0, 0, 0, 0.9)
self.cr.stroke()
def roundcorners_border(self, ctx):
self.create_roundcorners_subpath(ctx, 0.5, 0.5, self.width - 1, self.height - 1)
ctx.set_source_rgba(0, 0, 0, 0.9)
ctx.stroke()

@staticmethod
def create_roundcorners_subpath(ctx, x, y, width, height):
Expand All @@ -106,11 +101,9 @@ def create_roundcorners_subpath(ctx, x, y, width, height):
ctx.arc(x + radius, y + height - radius, radius, 90 * degrees, 180 * degrees)
ctx.arc(x + radius, y + radius, radius, 180 * degrees, 270 * degrees)
ctx.close_path()
return ctx

def draw_pieces(self):
def draw_pieces(self, ctx):
if not self.num_pieces:
# Nothing to draw.
return

if (
Expand All @@ -122,7 +115,7 @@ def draw_pieces(self):
self.pieces_overlay = cairo.ImageSurface(
cairo.FORMAT_ARGB32, self.width, self.height
)
ctx = cairo.Context(self.pieces_overlay)
pieces_ctx = cairo.Context(self.pieces_overlay)

if self.pieces:
pieces = self.pieces
Expand All @@ -139,17 +132,16 @@ def draw_pieces(self):
for state in COLOR_STATES
]
for state in pieces:
ctx.set_source_rgb(*pieces_colors[state])
ctx.rectangle(start_pos, 0, piece_width, self.height)
ctx.fill()
pieces_ctx.set_source_rgb(*pieces_colors[state])
pieces_ctx.rectangle(start_pos, 0, piece_width, self.height)
pieces_ctx.fill()
start_pos += piece_width

self.cr.set_source_surface(self.pieces_overlay)
self.cr.paint()
ctx.set_source_surface(self.pieces_overlay)
ctx.paint()

def draw_progress_overlay(self):
def draw_progress_overlay(self, ctx):
if not self.text:
# Nothing useful to draw, return now!
return

if (
Expand All @@ -161,25 +153,24 @@ def draw_progress_overlay(self):
self.progress_overlay = cairo.ImageSurface(
cairo.FORMAT_ARGB32, self.width, self.height
)
ctx = cairo.Context(self.progress_overlay)
ctx.set_source_rgba(0.1, 0.1, 0.1, 0.3) # Transparent
ctx.rectangle(0, 0, self.width * self.fraction, self.height)
ctx.fill()
self.cr.set_source_surface(self.progress_overlay)
self.cr.paint()

def write_text(self):
progress_ctx = cairo.Context(self.progress_overlay)
progress_ctx.set_source_rgba(0.1, 0.1, 0.1, 0.3) # Transparent
progress_ctx.rectangle(0, 0, self.width * self.fraction, self.height)
progress_ctx.fill()
ctx.set_source_surface(self.progress_overlay)
ctx.paint()

def write_text(self, ctx):
if not self.text:
# Nothing useful to draw, return now!
return

if self.resized() or self.text != self.prev_text or self.text_overlay is None:
# Need to recreate the cache drawing
self.text_overlay = cairo.ImageSurface(
cairo.FORMAT_ARGB32, self.width, self.height
)
ctx = cairo.Context(self.text_overlay)
pl = PangoCairo.create_layout(ctx)
text_ctx = cairo.Context(self.text_overlay)
pl = PangoCairo.create_layout(text_ctx)
pl.set_font_description(self.text_font)
pl.set_width(-1) # No text wrapping
pl.set_text(self.text, -1)
Expand All @@ -188,12 +179,14 @@ def write_text(self):
text_height = plsize[1] // SCALE
area_width_without_text = self.width - text_width
area_height_without_text = self.height - text_height
ctx.move_to(area_width_without_text // 2, area_height_without_text // 2)
ctx.set_source_rgb(1, 1, 1)
PangoCairo.update_layout(ctx, pl)
PangoCairo.show_layout(ctx, pl)
self.cr.set_source_surface(self.text_overlay)
self.cr.paint()
text_ctx.move_to(
area_width_without_text // 2, area_height_without_text // 2
)
text_ctx.set_source_rgb(1, 1, 1)
PangoCairo.update_layout(text_ctx, pl)
PangoCairo.show_layout(text_ctx, pl)
ctx.set_source_surface(self.text_overlay)
ctx.paint()

def resized(self):
return self.prev_width != self.width or self.prev_height != self.height
Expand Down Expand Up @@ -226,7 +219,6 @@ def clear(self):
self.text = self.prev_text = ''
self.fraction = self.prev_fraction = 0
self.progress_overlay = self.text_overlay = self.pieces_overlay = None
self.cr = None
self.update()

def update(self):
Expand Down

0 comments on commit 439c039

Please sign in to comment.