Skip to content

Commit

Permalink
Fix PangoLayout error on widget reload
Browse files Browse the repository at this point in the history
We experience `PangoLayout` errors if widgets try to draw to drawers
that have been finalised. This can happen when timers schedule drawing.
We deal with this by making sure timers are cancelled when widgets are
finalised. However, we only finalise timers that have been created by
`self.timeout_add`. Timers set by `qtile.call_soon/later` are not
tracked. These method are used to set initial timers for widgets at the
end of `_configure` so, if these timers have not yet triggered before
the widget is finalised then they will still be triggered.

This PR fixes that issue by ensuring that initial timers are also set
via `timeout_add` so that they can be cancelled on finalising the
widget.

Fixes qtile#3869
  • Loading branch information
elParaguayo committed Jul 19, 2023
1 parent 3ca4c54 commit ba7efd0
Show file tree
Hide file tree
Showing 3 changed files with 32 additions and 6 deletions.
31 changes: 27 additions & 4 deletions libqtile/widget/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ def __init__(self, length, **config):
raise confreader.ConfigError("Widget width must be an int")

self.configured = False
self._futures: list[asyncio.TimerHandle] = []
self._futures: list[asyncio.Handle] = []
self._mirrors: set[_Widget] = set()

@property
Expand Down Expand Up @@ -224,9 +224,15 @@ def _configure(self, qtile, bar):
self.qtile = qtile
self.bar = bar
self.drawer = bar.window.create_drawer(self.bar.width, self.bar.height)

# Timers are added to futures list so they can be cancelled if the `finalize` method is
# called before the timers have fired.
if not self.configured:
self.qtile.call_soon(self.timer_setup)
self.qtile.call_soon(create_task, self._config_async())
timer = self.qtile.call_soon(self.timer_setup)
async_timer = self.qtile.call_soon(asyncio.create_task, self._config_async())

# Add these to our list of futures so they can be cancelled.
self._futures.extend([timer, async_timer])

async def _config_async(self):
"""
Expand Down Expand Up @@ -342,10 +348,27 @@ def call_process(self, command, **kwargs):

def _remove_dead_timers(self):
"""Remove completed and cancelled timers from the list."""

def is_ready(timer):
return timer in self.qtile._eventloop._ready

self._futures = [
timer
for timer in self._futures
if not (timer.cancelled() or timer.when() < self.qtile._eventloop.time())
# Filter out certain handles...
if not (
timer.cancelled()
# Once a scheduled timer is ready to be run its _scheduled flag is set to False
# and it's added to the loop's `_ready` queue
or (
isinstance(timer, asyncio.TimerHandle)
and not timer._scheduled
and not is_ready(timer)
)
# Callbacks scheduled via `call_soon` are put into the loop's `_ready` queue
# and are removed once they've been executed
or (isinstance(timer, asyncio.Handle) and not is_ready(timer))
)
]

def _wrapper(self, method, *method_args):
Expand Down
2 changes: 1 addition & 1 deletion test/widgets/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def cancel_timer2(self):

@expose_command()
def get_active_timers(self):
active = [x for x in self._futures if x._scheduled]
active = [x for x in self._futures if getattr(x, "_scheduled", False)]
return len(active)


Expand Down
5 changes: 4 additions & 1 deletion test/widgets/test_chord.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,10 @@ def test_chord_widget(fake_window, fake_qtile):
assert chord.background == BASE_BACKGROUND
assert chord.foreground == BASE_FOREGROUND

# Finalize the widget to prevent segfault
# Finalize the widget to prevent segfault (the drawer needs to be finalised)
# We clear the _futures attribute as there are no real timers in it and calls
# to `cancel()` them will fail.
chord._futures = []
chord.finalize()


Expand Down

0 comments on commit ba7efd0

Please sign in to comment.