1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
|
#
# Authors: The MNE-Python contributors.
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.
import collections.abc
import functools
import os
import platform
import signal
import sys
from colorsys import rgb_to_hls
from contextlib import contextmanager
from ctypes import c_char_p, c_void_p, cdll
from pathlib import Path
import numpy as np
from ...fixes import _compare_version
from ...utils import _check_qt_version, _validate_type, logger, warn
from ..utils import _get_cmap
VALID_BROWSE_BACKENDS = (
"qt",
"matplotlib",
)
VALID_3D_BACKENDS = (
"pyvistaqt", # default 3d backend
"notebook",
)
ALLOWED_QUIVER_MODES = ("2darrow", "arrow", "cone", "cylinder", "sphere", "oct")
_ICONS_PATH = Path(__file__).parents[2] / "icons"
def _get_colormap_from_array(
colormap=None, normalized_colormap=False, default_colormap="coolwarm"
):
from matplotlib.colors import ListedColormap
if colormap is None:
cmap = _get_cmap(default_colormap)
elif isinstance(colormap, str):
cmap = _get_cmap(colormap)
elif normalized_colormap:
cmap = ListedColormap(colormap)
else:
cmap = ListedColormap(np.array(colormap) / 255.0)
return cmap
def _check_color(color):
from matplotlib.colors import colorConverter
if isinstance(color, str):
color = colorConverter.to_rgb(color)
elif isinstance(color, collections.abc.Iterable):
np_color = np.array(color)
if np_color.size % 3 != 0 and np_color.size % 4 != 0:
raise ValueError("The expected valid format is RGB or RGBA.")
if np_color.dtype in (np.int64, np.int32):
if (np_color < 0).any() or (np_color > 255).any():
raise ValueError("Values out of range [0, 255].")
elif np_color.dtype == np.float64:
if (np_color < 0.0).any() or (np_color > 1.0).any():
raise ValueError("Values out of range [0.0, 1.0].")
else:
raise TypeError(
"Expected data type is `np.int64`, `np.int32`, or `np.float64` but "
f"{np_color.dtype} was given."
)
else:
raise TypeError(
f"Expected type is `str` or iterable but {type(color)} was given."
)
return color
def _alpha_blend_background(ctable, background_color):
alphas = ctable[:, -1][:, np.newaxis] / 255.0
use_table = ctable.copy()
use_table[:, -1] = 255.0
return (use_table * alphas) + background_color * (1 - alphas)
@functools.lru_cache(1)
def _qt_init_icons():
from qtpy.QtGui import QIcon
QIcon.setThemeSearchPaths([str(_ICONS_PATH)] + QIcon.themeSearchPaths())
QIcon.setFallbackThemeName("light")
return str(_ICONS_PATH)
@contextmanager
def _qt_disable_paint(widget):
paintEvent = widget.paintEvent
widget.paintEvent = lambda *args, **kwargs: None
try:
yield
finally:
widget.paintEvent = paintEvent
_QT_ICON_KEYS = dict(app=None)
def _init_mne_qtapp(enable_icon=True, pg_app=False, splash=False):
"""Get QApplication-instance for MNE-Python.
Parameter
---------
enable_icon: bool
If to set an MNE-icon for the app.
pg_app: bool
If to create the QApplication with pyqtgraph. For an until know
undiscovered reason the pyqtgraph-browser won't show without
mkQApp from pyqtgraph.
splash : bool | str
If not False, display a splash screen. If str, set the message
to the given string.
Returns
-------
app : ``qtpy.QtWidgets.QApplication``
Instance of QApplication.
splash : ``qtpy.QtWidgets.QSplashScreen``
Instance of QSplashScreen. Only returned if splash is True or a
string.
"""
from qtpy.QtCore import Qt
from qtpy.QtGui import QGuiApplication, QIcon, QPixmap
from qtpy.QtWidgets import QApplication, QSplashScreen
app_name = "MNE-Python"
organization_name = "MNE"
# Fix from cbrnr/mnelab for app name in menu bar
# This has to come *before* the creation of the QApplication to work.
# It also only affects the title bar, not the application dock.
# There seems to be no way to change the application dock from "python"
# at runtime.
if sys.platform.startswith("darwin"):
try:
# set bundle name on macOS (app name shown in the menu bar)
from Foundation import NSBundle
bundle = NSBundle.mainBundle()
info = bundle.localizedInfoDictionary() or bundle.infoDictionary()
if "CFBundleName" not in info:
info["CFBundleName"] = app_name
except ModuleNotFoundError:
pass
# First we need to check to make sure the display is valid, otherwise
# Qt might segfault on us
app = QApplication.instance()
if not (app or _display_is_valid()):
raise RuntimeError("Cannot connect to a valid display")
if pg_app:
from pyqtgraph import mkQApp
old_argv = sys.argv
try:
sys.argv = []
app = mkQApp(app_name)
finally:
sys.argv = old_argv
elif not app:
app = QApplication([app_name])
app.setApplicationName(app_name)
app.setOrganizationName(organization_name)
qt_version = _check_qt_version(check_usable_display=False)
# HiDPI is enabled by default in Qt6, requires to be explicitly set for Qt5
if _compare_version(qt_version, "<", "6.0"):
app.setAttribute(Qt.AA_UseHighDpiPixmaps)
if enable_icon or splash:
icons_path = _qt_init_icons()
if (
enable_icon
and app.windowIcon().cacheKey() != _QT_ICON_KEYS["app"]
and app.windowIcon().isNull() # don't overwrite existing icon (e.g. MNELAB)
):
# Set icon
kind = "bigsur_" if platform.mac_ver()[0] >= "10.16" else "default_"
icon = QIcon(f"{icons_path}/mne_{kind}icon.png")
app.setWindowIcon(icon)
_QT_ICON_KEYS["app"] = app.windowIcon().cacheKey()
out = app
if splash:
pixmap = QPixmap(f"{icons_path}/mne_splash.png")
pixmap.setDevicePixelRatio(QGuiApplication.primaryScreen().devicePixelRatio())
args = (pixmap,)
if _should_raise_window():
args += (Qt.WindowStaysOnTopHint,)
qsplash = QSplashScreen(*args)
qsplash.setAttribute(Qt.WA_ShowWithoutActivating, True)
if isinstance(splash, str):
alignment = int(Qt.AlignBottom | Qt.AlignHCenter)
qsplash.showMessage(splash, alignment=alignment, color=Qt.white)
qsplash.show()
app.processEvents()
out = (out, qsplash)
return out
def _display_is_valid():
# Adapted from matplotilb _c_internal_utils.py
if sys.platform != "linux":
return True
if os.getenv("DISPLAY"): # if it's not there, don't bother
libX11 = cdll.LoadLibrary("libX11.so.6")
libX11.XOpenDisplay.restype = c_void_p
libX11.XOpenDisplay.argtypes = [c_char_p]
display = libX11.XOpenDisplay(None)
if display is not None:
libX11.XCloseDisplay.argtypes = [c_void_p]
libX11.XCloseDisplay(display)
return True
# not found, try Wayland
if os.getenv("WAYLAND_DISPLAY"):
libwayland = cdll.LoadLibrary("libwayland-client.so.0")
if libwayland is not None:
if all(
hasattr(libwayland, f"wl_display_{kind}connect") for kind in ("", "dis")
):
libwayland.wl_display_connect.restype = c_void_p
libwayland.wl_display_connect.argtypes = [c_char_p]
display = libwayland.wl_display_connect(None)
if display:
libwayland.wl_display_disconnect.argtypes = [c_void_p]
libwayland.wl_display_disconnect(display)
return True
return False
# https://stackoverflow.com/questions/5160577/ctrl-c-doesnt-work-with-pyqt
def _qt_app_exec(app):
# adapted from matplotlib
old_signal = signal.getsignal(signal.SIGINT)
is_python_signal_handler = old_signal is not None
if is_python_signal_handler:
signal.signal(signal.SIGINT, signal.SIG_DFL)
try:
# Make IPython Console accessible again in Spyder
app.lastWindowClosed.connect(app.quit)
app.exec_()
finally:
# reset the SIGINT exception handler
if is_python_signal_handler:
signal.signal(signal.SIGINT, old_signal)
def _qt_detect_theme():
try:
import darkdetect
theme = darkdetect.theme().lower()
except ModuleNotFoundError:
logger.info(
'For automatic theme detection, "darkdetect" has to'
" be installed! You can install it with "
"`pip install darkdetect`"
)
theme = "light"
except Exception:
theme = "light"
return theme
def _qt_get_stylesheet(theme):
_validate_type(theme, ("path-like",), "theme")
theme = str(theme)
stylesheet = "" # no stylesheet
if theme in ("auto", "dark", "light"):
if theme == "auto":
return stylesheet
assert theme in ("dark", "light")
system_theme = _qt_detect_theme()
if theme == system_theme:
return stylesheet
_, api = _check_qt_version(return_api=True)
# On macOS or Qt 6, we shouldn't need to set anything when the requested
# theme matches that of the current OS state
try:
import qdarkstyle
except ModuleNotFoundError:
logger.info(
f'To use {theme} mode when in {system_theme} mode, "qdarkstyle" has'
"to be installed! You can install it with:\n"
"pip install qdarkstyle\n"
)
else:
if api in ("PySide6", "PyQt6") and _compare_version(
qdarkstyle.__version__, "<", "3.2.3"
):
warn(
f"Setting theme={repr(theme)} is not supported for {api} in "
f"qdarkstyle {qdarkstyle.__version__}, it will be ignored. "
"Consider upgrading qdarkstyle to >=3.2.3."
)
else:
stylesheet = qdarkstyle.load_stylesheet(
getattr(
getattr(qdarkstyle, theme).palette,
f"{theme.capitalize()}Palette",
)
)
return stylesheet
else:
try:
file = open(theme)
except OSError:
warn(
"Requested theme file not found, will use light instead: "
f"{repr(theme)}"
)
else:
with file as fid:
stylesheet = fid.read()
return stylesheet
def _should_raise_window():
from matplotlib import rcParams
return rcParams["figure.raise_window"]
def _qt_raise_window(widget):
# Set raise_window like matplotlib if possible
if _should_raise_window():
widget.activateWindow()
widget.raise_()
def _qt_is_dark(widget):
# Ideally this would use CIELab, but this should be good enough
win = widget.window()
bgcolor = win.palette().color(win.backgroundRole()).getRgbF()[:3]
return rgb_to_hls(*bgcolor)[1] < 0.5
def _pixmap_to_ndarray(pixmap):
from qtpy.QtGui import QImage
img = pixmap.toImage()
img = img.convertToFormat(QImage.Format.Format_RGBA8888)
ptr = img.bits()
count = img.height() * img.width() * 4
if hasattr(ptr, "setsize"): # PyQt
ptr.setsize(count)
data = np.frombuffer(ptr, dtype=np.uint8, count=count).copy()
data.shape = (img.height(), img.width(), 4)
return data / 255.0
def _notebook_vtk_works():
if sys.platform != "linux":
return True
# check if it's OSMesa -- if it is, continue
try:
from vtkmodules import vtkRenderingOpenGL2
vtkRenderingOpenGL2.vtkOSOpenGLRenderWindow
except Exception:
pass
else:
return True # has vtkOSOpenGLRenderWindow (OSMesa build)
# if it's not OSMesa, we need to check display validity
if _display_is_valid():
return True
return False
def _qt_safe_window(
*, splash="figure.splash", window="figure.plotter.app_window", always_close=True
):
def dec(meth, splash=splash, always_close=always_close):
@functools.wraps(meth)
def func(self, *args, **kwargs):
close_splash = always_close
error = False
try:
meth(self, *args, **kwargs)
except Exception:
close_splash = error = True
raise
finally:
for attr, do_close in ((splash, close_splash), (window, error)):
if attr is None or not do_close:
continue
parent = self
name = attr.split(".")[-1]
try:
for n in attr.split(".")[:-1]:
parent = getattr(parent, n)
if name:
widget = getattr(parent, name, False)
else: # empty string means "self"
widget = parent
if widget:
widget.close()
del widget
except Exception:
pass
finally:
try:
delattr(parent, name)
except Exception:
pass
return func
return dec
|