Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion qubes_config/tests/test_gtk_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
# You should have received a copy of the GNU Lesser General Public License along
# with this program; if not, see <http://www.gnu.org/licenses/>.
"""Tests for gtk utils"""
from unittest.mock import patch, call
import asyncio
from unittest.mock import patch, call, Mock

import gi

Expand All @@ -32,6 +33,8 @@
ask_question,
show_error,
is_theme_light,
show_dialog_with_icon_async,
RESPONSES_OK,
)


Expand Down Expand Up @@ -74,6 +77,35 @@ def test_ask_question():
assert call.new().destroy() in mock_dialog.mock_calls


def test_show_dialog_with_icon_async_without_running_loop():
captured = {}
mock_dialog = Mock()
mock_dialog.connect.side_effect = captured.setdefault

with patch(
"qubes_config.widgets.gtk_utils._setup_dialog",
return_value=mock_dialog,
):
coro = show_dialog_with_icon_async(
None, "Title", "Text", RESPONSES_OK, "qubes-info"
)

# step to `await future`: must reach it (fallback loop) instead of
# raising RuntimeError('no running event loop')
pending = coro.send(None)
assert asyncio.isfuture(pending)

captured["response"](mock_dialog, Gtk.ResponseType.OK) # click

try:
coro.send(None)
except StopIteration as stop:
assert stop.value == Gtk.ResponseType.OK
mock_dialog.destroy.assert_called_once()
else:
raise AssertionError("coroutine did not finish after response")


def test_get_theme():
"""test getting light/dark theme"""
# first test dark theme
Expand Down
67 changes: 59 additions & 8 deletions qubes_config/widgets/gtk_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
# You should have received a copy of the GNU Lesser General Public License along
# with this program; if not, see <http://www.gnu.org/licenses/>.
"""Utility functions using Gtk"""
import asyncio
import contextlib
import importlib.resources
import os
Expand Down Expand Up @@ -114,20 +115,21 @@ def ask_question(parent, title: str, text: str):
)


def show_dialog_with_icon(
def _setup_dialog(
parent: Optional[Gtk.Widget],
title: str,
text: Union[str, Gtk.Widget],
buttons: Dict[str, Gtk.ResponseType],
icon_name: str,
) -> Gtk.ResponseType:
"""
Helper function to show a dialog with icon given by name.
"""
) -> Gtk.Dialog:
icon = Gtk.Image.new_from_pixbuf(load_icon(icon_name, 48, 48))
dialog = show_dialog(parent, title, text, buttons, icon)
response = dialog.run()
dialog.destroy()
return show_dialog(parent, title, text, buttons, icon)


def _process_response(
response: Gtk.ResponseType,
buttons: Dict[str, Gtk.ResponseType],
) -> Gtk.ResponseType:
if response == Gtk.ResponseType.DELETE_EVENT:
if Gtk.ResponseType.CANCEL in buttons.values():
# treat exiting from the window as cancel if it's one of the
Expand All @@ -139,6 +141,55 @@ def show_dialog_with_icon(
return response


def show_dialog_with_icon(
parent: Optional[Gtk.Widget],
title: str,
text: Union[str, Gtk.Widget],
buttons: Dict[str, Gtk.ResponseType],
icon_name: str,
) -> Gtk.ResponseType:
"""Helper function to show a dialog with icon given by name."""
dialog = _setup_dialog(parent, title, text, buttons, icon_name)

response = dialog.run()
dialog.destroy()

return _process_response(response, buttons)


async def show_dialog_with_icon_async(
parent: Optional[Gtk.Widget],
title: str,
text: Union[str, Gtk.Widget],
buttons: Dict[str, Gtk.ResponseType],
icon_name: str,
) -> Gtk.ResponseType:
"""Async, non-blocking variant of :py:func:`show_dialog_with_icon`."""
dialog = _setup_dialog(parent, title, text, buttons, icon_name)
try:
loop = asyncio.get_running_loop()
except RuntimeError:
# To be solved during migration to newer asyncio versions.
# May be awaited before the loop is "running", e.g.
# the updater's "nothing to do" dialog runs during GApplication.run(),
# where there is not set the running loop yet.
loop = asyncio.get_event_loop()
future: asyncio.Future = loop.create_future()

def _on_response(_dialog, response_):
if not future.done():
future.set_result(response_)

dialog.connect("response", _on_response)

try:
response = await future
finally:
dialog.destroy()

return _process_response(response, buttons)


def show_dialog(
parent: Gtk.Widget,
title: str,
Expand Down
Loading