# Authors: The MNE-Python contributors.
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.
import pytest
from mne.utils import _check_qt_version
# These will skip all tests in this scope
pytest.importorskip("nibabel")
def test_gui_api(renderer_notebook, nbexec, *, backend="qt"):
"""Test GUI API."""
import contextlib
import warnings
import mne
try:
# Function
backend # noqa
except Exception:
# Notebook standalone mode
backend = "notebook"
# nbexec does not expose renderer_notebook so I use a
# temporary variable to synchronize the tests
if backend == "notebook":
mne.viz.set_3d_backend("notebook")
renderer = mne.viz.backends.renderer._get_renderer(size=(300, 300))
# theme
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
renderer._window_set_theme("/does/not/exist")
if backend == "qt":
assert len(w) == 1
assert "not found" in str(w[0].message), str(w[0].message)
else:
assert len(w) == 0
with mne.utils._record_warnings() as w:
renderer._window_set_theme("dark")
w = [ww for ww in w if "is not yet supported" in str(ww.message)]
assert len(w) == 0, [ww.message for ww in w]
# window without 3d plotter
if backend == "qt":
window = renderer._window_create()
widget = renderer._window_create()
central_layout = renderer._layout_create(orientation="grid")
renderer._layout_add_widget(central_layout, widget, row=0, col=0)
renderer._window_initialize(window=window, central_layout=central_layout)
from unittest.mock import Mock
mock = Mock()
@contextlib.contextmanager
def _check_widget_trigger(
widget, mock, before, after, call_count=True, get_value=True
):
if get_value:
assert widget.get_value() == before
old_call_count = mock.call_count
try:
yield
finally:
if get_value:
assert widget.get_value() == after
if call_count:
assert mock.call_count == old_call_count + 1
# --- BEGIN: dock ---
renderer._dock_initialize(name="", area="left")
# label (not interactive)
widget = renderer._dock_add_label(
value="",
align=False,
selectable=True,
)
widget = renderer._dock_add_label(
value="",
align=True,
)
widget.update()
# labels are disabled by default with the notebook backend
widget.set_enabled(False)
assert not widget.is_enabled()
widget.set_enabled(True)
assert widget.is_enabled()
# ToolButton
widget = renderer._dock_add_button(
name="",
callback=mock,
style="toolbutton",
tooltip="button",
)
with _check_widget_trigger(widget, mock, None, None, get_value=False):
widget.set_value(True)
# PushButton
widget = renderer._dock_add_button(
name="",
callback=mock,
)
with _check_widget_trigger(widget, mock, None, None, get_value=False):
widget.set_value(True)
# slider
widget = renderer._dock_add_slider(
name="",
value=0,
rng=[0, 10],
callback=mock,
tooltip="slider",
)
with _check_widget_trigger(widget, mock, 0, 5):
widget.set_value(5)
# check box
widget = renderer._dock_add_check_box(
name="",
value=False,
callback=mock,
tooltip="check box",
)
with _check_widget_trigger(widget, mock, False, True):
widget.set_value(True)
# spin box
renderer._dock_add_spin_box(
name="",
value=0,
rng=[0, 1],
callback=mock,
step=0.1,
tooltip="spin box",
)
widget = renderer._dock_add_spin_box(
name="",
value=0,
rng=[0, 1],
callback=mock,
step=None,
)
with _check_widget_trigger(widget, mock, 0, 0.5):
widget.set_value(0.5)
# combo box
widget = renderer._dock_add_combo_box(
name="",
value="foo",
rng=["foo", "bar"],
callback=mock,
tooltip="combo box",
)
with _check_widget_trigger(widget, mock, "foo", "bar"):
widget.set_value("bar")
# radio buttons
widget = renderer._dock_add_radio_buttons(
value="foo",
rng=["foo", "bar"],
callback=mock,
)
with _check_widget_trigger(widget, mock, None, None, get_value=False):
widget.set_value(1, "bar")
assert widget.get_value(0) == "foo"
assert widget.get_value(1) == "bar"
widget.set_enabled(False)
# text field
widget = renderer._dock_add_text(
name="",
value="foo",
placeholder="",
callback=mock,
)
with _check_widget_trigger(widget, mock, "foo", "bar"):
widget.set_value("bar")
widget.set_style(dict(border="2px solid #ff0000"))
# file button
renderer._dock_add_file_button(
name="",
desc="",
func=mock,
is_directory=True,
tooltip="file button",
)
renderer._dock_add_file_button(
name="",
desc="",
func=mock,
initial_directory="",
)
renderer._dock_add_file_button(
name="",
desc="",
func=mock,
)
widget = renderer._dock_add_file_button(name="", desc="", func=mock, save=True)
# XXX: the internal file dialogs may hang without signals
widget.set_enabled(False)
renderer._dock_initialize(name="", area="right")
renderer._dock_named_layout(name="")
for collapse in (None, True, False):
renderer._dock_add_group_box(name="", collapse=collapse)
renderer._dock_add_stretch()
renderer._dock_add_layout()
renderer._dock_finalize()
renderer._dock_hide()
renderer._dock_show()
# --- END: dock ---
# --- BEGIN: tool bar ---
renderer._tool_bar_initialize(
name="default",
window=None,
)
# button
assert "reset" not in renderer.actions
renderer._tool_bar_add_button(
name="reset",
desc="",
func=mock,
icon_name="help",
)
assert "reset" in renderer.actions
# icon
renderer._tool_bar_update_button_icon(
name="reset",
icon_name="reset",
)
# text
renderer._tool_bar_add_text(
name="",
value="",
placeholder="",
)
# spacer
renderer._tool_bar_add_spacer()
# file button
assert "help" not in renderer.actions
renderer._tool_bar_add_file_button(
name="help",
desc="",
func=mock,
shortcut=None,
)
renderer.actions["help"].trigger()
if renderer._kind == "qt":
dialog = renderer._window.children()[-1]
assert "FileDialog" in repr(dialog)
dialog.close()
dialog.deleteLater()
# play button
assert "play" not in renderer.actions
renderer._tool_bar_add_play_button(
name="play",
desc="",
func=mock,
shortcut=None,
)
assert "play" in renderer.actions
# --- END: tool bar ---
# --- BEGIN: menu bar ---
renderer._menu_initialize()
# submenu
renderer._menu_add_submenu(name="foo", desc="foo")
assert "foo" in renderer._menus
assert "foo" in renderer._menu_actions
# button
renderer._menu_add_button(
menu_name="foo",
name="bar",
desc="bar",
func=mock,
)
assert "bar" in renderer._menu_actions["foo"]
with _check_widget_trigger(None, mock, "", "", get_value=False):
renderer._menu_actions["foo"]["bar"].trigger()
# --- END: menu bar ---
# --- BEGIN: status bar ---
renderer._status_bar_initialize()
renderer._status_bar_update()
# label
widget = renderer._status_bar_add_label(value="foo", stretch=0)
assert widget.get_value() == "foo"
# progress bar
widget = renderer._status_bar_add_progress_bar(stretch=0)
# by default, get_value() is -1 for Qt and 0 for Ipywidgets
widget.set_value(0)
assert widget.get_value() == 0
# --- END: status bar ---
# --- BEGIN: tooltips ---
widget = renderer._dock_add_button(name="", callback=mock, tooltip="foo")
assert widget.get_tooltip() == "foo"
# Change it …
widget.set_tooltip("bar")
assert widget.get_tooltip() == "bar"
# --- END: tooltips ---
# --- BEGIN: dialog ---
# dialogs are not supported yet on notebook
if renderer._kind == "qt":
# warning
buttons = ["Save", "Cancel"]
widget = renderer._dialog_create(
title="",
text="",
info_text="",
callback=mock,
buttons=buttons,
modal=False,
)
widget.show()
for button in buttons:
with _check_widget_trigger(None, mock, "", "", get_value=False):
widget.trigger(button=button)
assert mock.call_args.args == (button,)
assert not widget._widget.isVisible()
# buttons list empty means OK button (default)
button = "Ok"
widget = renderer._dialog_create(
title="",
text="",
info_text="",
callback=mock,
icon="NoIcon",
modal=False,
)
widget.show()
with _check_widget_trigger(None, mock, "", "", get_value=False):
widget.trigger(button=button)
assert mock.call_args.args == (button,)
widget.trigger(button="Ok")
# --- END: dialog ---
# --- BEGIN: keypress ---
renderer._keypress_initialize()
renderer._keypress_add("a", mock)
# keypress is not supported yet on notebook
if renderer._kind == "qt":
with _check_widget_trigger(None, mock, "", "", get_value=False):
renderer._keypress_trigger("a")
# --- END: keypress ---
renderer.show()
renderer._window_close_connect(lambda: mock("first"), after=False)
renderer._window_close_connect(lambda: mock("last"))
old_call_count = mock.call_count
renderer.close()
if renderer._kind == "qt":
assert mock.call_count == old_call_count + 2
assert mock.call_args_list[-1].args == ("last",)
assert mock.call_args_list[-2].args == ("first",)
assert renderer._window.isVisible() is False
del renderer
def test_gui_api_qt(renderer_interactive_pyvistaqt):
"""Test GUI API with the Qt backend."""
_, api = _check_qt_version(return_api=True)
# TODO: After merging https://github.com/mne-tools/mne-python/pull/11567
# The Qt CI run started failing about 50% of the time, so let's skip this
# for now.
if api == "PySide6":
pytest.skip("PySide6 causes segfaults on CIs sometimes")
test_gui_api(None, None, backend="qt")