Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
42 changes: 41 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,43 @@ def test_ask_question():
assert call.new().destroy() in mock_dialog.mock_calls


def test_show_dialog_with_icon_async_without_running_loop():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
Comment thread
piotrbartman marked this conversation as resolved.
Outdated
try:
captured = {}
mock_dialog = Mock()
mock_dialog.connect.side_effect = lambda signal, handler: captured.setdefault(
signal, handler
)

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"
)

pending = coro.send(None)
assert asyncio.isfuture(pending)

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

try:
coro.send(None)
except StopIteration as stop:
result = stop.value
else:
raise AssertionError("coroutine did not finish after response")

assert result == Gtk.ResponseType.OK
mock_dialog.destroy.assert_called_once()
finally:
asyncio.set_event_loop(None)
loop.close()


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