Spaces:
Running
Running
| """ | |
| Event API for inter-figure communication. | |
| The event API allows figures to communicate with each other, such that a change | |
| in one figure can trigger a change in another figure. For example, moving the | |
| time cursor in one plot can update the current time in another plot. Another | |
| scenario is two drawing routines drawing into the same window, using events to | |
| stay in-sync. | |
| Authors: Marijn van Vliet <w.m.vanvliet@gmail.com> | |
| """ | |
| # Authors: The MNE-Python contributors. | |
| # License: BSD-3-Clause | |
| # Copyright the MNE-Python contributors. | |
| from __future__ import annotations # only needed for Python ≤ 3.9 | |
| import contextlib | |
| import re | |
| import weakref | |
| from dataclasses import dataclass | |
| from matplotlib.colors import Colormap | |
| from ..utils import _validate_type, fill_doc, logger, verbose, warn | |
| # Global dict {fig: channel} containing all currently active event channels. | |
| _event_channels = weakref.WeakKeyDictionary() | |
| # The event channels of figures can be linked together. This dict keeps track | |
| # of these links. Links are bi-directional, so if {fig1: fig2} exists, then so | |
| # must {fig2: fig1}. | |
| _event_channel_links = weakref.WeakKeyDictionary() | |
| # Event channels that are temporarily disabled by the disable_ui_events context | |
| # manager. | |
| _disabled_event_channels = weakref.WeakSet() | |
| # Regex pattern used when converting CamelCase to snake_case. | |
| # Detects all capital letters that are not at the beginning of a word. | |
| _camel_to_snake = re.compile(r"(?<!^)(?=[A-Z])") | |
| # List of events | |
| class UIEvent: | |
| """Abstract base class for all events. | |
| Attributes | |
| ---------- | |
| %(ui_event_name_source)s | |
| """ | |
| source = None | |
| def name(self): | |
| """The name of the event, which is the class name in snake case.""" | |
| return _camel_to_snake.sub("_", self.__class__.__name__).lower() | |
| class FigureClosing(UIEvent): | |
| """Indicates that the user has requested to close a figure. | |
| Attributes | |
| ---------- | |
| %(ui_event_name_source)s | |
| """ | |
| pass | |
| class TimeChange(UIEvent): | |
| """Indicates that the user has selected a time. | |
| Parameters | |
| ---------- | |
| time : float | |
| The new time in seconds. | |
| Attributes | |
| ---------- | |
| %(ui_event_name_source)s | |
| time : float | |
| The new time in seconds. | |
| """ | |
| time: float | |
| class PlaybackSpeed(UIEvent): | |
| """Indicates that the user has selected a different playback speed for videos. | |
| Parameters | |
| ---------- | |
| speed : float | |
| The new speed in seconds per frame. | |
| Attributes | |
| ---------- | |
| %(ui_event_name_source)s | |
| speed : float | |
| The new speed in seconds per frame. | |
| """ | |
| speed: float | |
| class ColormapRange(UIEvent): | |
| """Indicates that the user has updated the bounds of the colormap. | |
| Parameters | |
| ---------- | |
| kind : str | |
| Kind of colormap being updated. The Notes section of the drawing | |
| routine publishing this event should mention the possible kinds. | |
| ch_type : str | |
| Type of sensor the data originates from. | |
| %(fmin_fmid_fmax)s | |
| %(alpha)s | |
| cmap : str | |
| The colormap to use. Either string or matplotlib.colors.Colormap | |
| instance. | |
| Attributes | |
| ---------- | |
| kind : str | |
| Kind of colormap being updated. The Notes section of the drawing | |
| routine publishing this event should mention the possible kinds. | |
| ch_type : str | |
| Type of sensor the data originates from. | |
| unit : str | |
| The unit of the values. | |
| %(ui_event_name_source)s | |
| %(fmin_fmid_fmax)s | |
| %(alpha)s | |
| cmap : str | |
| The colormap to use. Either string or matplotlib.colors.Colormap | |
| instance. | |
| """ | |
| kind: str | |
| ch_type: str | None = None | |
| fmin: float | None = None | |
| fmid: float | None = None | |
| fmax: float | None = None | |
| alpha: bool | None = None | |
| cmap: Colormap | str | None = None | |
| class VertexSelect(UIEvent): | |
| """Indicates that the user has selected a vertex. | |
| Parameters | |
| ---------- | |
| hemi : str | |
| The hemisphere the vertex was selected on. | |
| Can be ``"lh"``, ``"rh"``, or ``"vol"``. | |
| vertex_id : int | |
| The vertex number (in the high resolution mesh) that was selected. | |
| Attributes | |
| ---------- | |
| %(ui_event_name_source)s | |
| hemi : str | |
| The hemisphere the vertex was selected on. | |
| Can be ``"lh"``, ``"rh"``, or ``"vol"``. | |
| vertex_id : int | |
| The vertex number (in the high resolution mesh) that was selected. | |
| """ | |
| hemi: str | |
| vertex_id: int | |
| class Contours(UIEvent): | |
| """Indicates that the user has changed the contour lines. | |
| Parameters | |
| ---------- | |
| kind : str | |
| The kind of contours lines being changed. The Notes section of the | |
| drawing routine publishing this event should mention the possible | |
| kinds. | |
| contours : list of float | |
| The new values at which contour lines need to be drawn. | |
| Attributes | |
| ---------- | |
| %(ui_event_name_source)s | |
| kind : str | |
| The kind of contours lines being changed. The Notes section of the | |
| drawing routine publishing this event should mention the possible | |
| kinds. | |
| contours : list of float | |
| The new values at which contour lines need to be drawn. | |
| """ | |
| kind: str | |
| contours: list[str] | |
| class ChannelsSelect(UIEvent): | |
| """Indicates that the user has selected one or more channels. | |
| Parameters | |
| ---------- | |
| ch_names : list of str | |
| The names of the channels that were selected. | |
| Attributes | |
| ---------- | |
| %(ui_event_name_source)s | |
| ch_names : list of str | |
| The names of the channels that were selected. | |
| """ | |
| ch_names: list[str] | |
| def _get_event_channel(fig): | |
| """Get the event channel associated with a figure. | |
| If the event channel doesn't exist yet, it gets created and added to the | |
| global ``_event_channels`` dict. | |
| Parameters | |
| ---------- | |
| fig : matplotlib.figure.Figure | Figure3D | |
| The figure to get the event channel for. | |
| Returns | |
| ------- | |
| channel : dict[event -> list] | |
| The event channel. An event channel is a list mapping string event | |
| names to a list of callback representing all subscribers to the | |
| channel. | |
| """ | |
| import matplotlib | |
| from ._brain import Brain | |
| from .evoked_field import EvokedField | |
| # Create the event channel if it doesn't exist yet | |
| if fig not in _event_channels: | |
| # The channel itself is a dict mapping string event names to a list of | |
| # subscribers. No subscribers yet for this new event channel. | |
| _event_channels[fig] = dict() | |
| weakfig = weakref.ref(fig) | |
| # When the figure is closed, its associated event channel should be | |
| # deleted. This is a good time to set this up. | |
| def delete_event_channel(event=None, *, weakfig=weakfig): | |
| """Delete the event channel (callback function).""" | |
| fig = weakfig() | |
| if fig is None: | |
| return | |
| publish(fig, event=FigureClosing()) # Notify subscribers of imminent close | |
| logger.debug(f"unlink(({fig})") | |
| unlink(fig) # Remove channel from the _event_channel_links dict | |
| if fig in _event_channels: | |
| logger.debug(f" del _event_channels[{fig}]") | |
| del _event_channels[fig] | |
| if fig in _disabled_event_channels: | |
| logger.debug(f" _disabled_event_channels.remove({fig})") | |
| _disabled_event_channels.remove(fig) | |
| # Hook up the above callback function to the close event of the figure | |
| # window. How this is done exactly depends on the various figure types | |
| # MNE-Python has. | |
| _validate_type(fig, (matplotlib.figure.Figure, Brain, EvokedField), "fig") | |
| if isinstance(fig, matplotlib.figure.Figure): | |
| fig.canvas.mpl_connect("close_event", delete_event_channel) | |
| else: | |
| assert hasattr(fig, "_renderer") # figures like Brain, EvokedField, etc. | |
| fig._renderer._window_close_connect(delete_event_channel, after=False) | |
| # Now the event channel exists for sure. | |
| return _event_channels[fig] | |
| def publish(fig, event, *, verbose=None): | |
| """Publish an event to all subscribers of the figure's channel. | |
| The figure's event channel and all linked event channels are searched for | |
| subscribers to the given event. Each subscriber had provided a callback | |
| function when subscribing, so we call that. | |
| Parameters | |
| ---------- | |
| fig : matplotlib.figure.Figure | Figure3D | |
| The figure that publishes the event. | |
| event : UIEvent | |
| Event to publish. | |
| %(verbose)s | |
| """ | |
| if fig in _disabled_event_channels: | |
| return | |
| # Compile a list of all event channels that the event should be published | |
| # on. | |
| channels = [_get_event_channel(fig)] | |
| links = _event_channel_links.get(fig, None) | |
| if links is not None: | |
| for linked_fig, (include_events, exclude_events) in links.items(): | |
| if (include_events is None or event.name in include_events) and ( | |
| exclude_events is None or event.name not in exclude_events | |
| ): | |
| channels.append(_get_event_channel(linked_fig)) | |
| # Publish the event by calling the registered callback functions. | |
| event.source = fig | |
| logger.debug(f"Publishing {event} on channel {fig}") | |
| for channel in channels: | |
| if event.name not in channel: | |
| channel[event.name] = set() | |
| for callback in channel[event.name]: | |
| callback(event=event) | |
| def subscribe(fig, event_name, callback, *, verbose=None): | |
| """Subscribe to an event on a figure's event channel. | |
| Parameters | |
| ---------- | |
| fig : matplotlib.figure.Figure | Figure3D | |
| The figure of which event channel to subscribe. | |
| event_name : str | |
| The name of the event to listen for. | |
| callback : callable | |
| The function that should be called whenever the event is published. | |
| %(verbose)s | |
| """ | |
| channel = _get_event_channel(fig) | |
| logger.debug(f"Subscribing to channel {channel}") | |
| if event_name not in channel: | |
| channel[event_name] = set() | |
| channel[event_name].add(callback) | |
| def unsubscribe(fig, event_names, callback=None, *, verbose=None): | |
| """Unsubscribe from an event on a figure's event channel. | |
| Parameters | |
| ---------- | |
| fig : matplotlib.figure.Figure | Figure3D | |
| The figure of which event channel to unsubscribe from. | |
| event_names : str | list of str | |
| Select which events to stop subscribing to. Can be a single string | |
| event name, a list of event names or ``"all"`` which will unsubscribe | |
| from all events. | |
| callback : callable | None | |
| The callback function that should be unsubscribed, leaving all other | |
| callback functions that may be subscribed untouched. By default | |
| (``None``) all callback functions are unsubscribed from the event. | |
| %(verbose)s | |
| """ | |
| channel = _get_event_channel(fig) | |
| # Determine which events to unsubscribe for. | |
| if event_names == "all": | |
| if callback is None: | |
| event_names = list(channel.keys()) | |
| else: | |
| event_names = list(k for k, v in channel.items() if callback in v) | |
| elif isinstance(event_names, str): | |
| event_names = [event_names] | |
| for event_name in event_names: | |
| if event_name not in channel: | |
| warn( | |
| f'Cannot unsubscribe from event "{event_name}" as we have never ' | |
| "subscribed to it." | |
| ) | |
| continue | |
| if callback is None: | |
| del channel[event_name] | |
| else: | |
| # Unsubscribe specific callback function. | |
| subscribers = channel[event_name] | |
| if callback in subscribers: | |
| subscribers.remove(callback) | |
| else: | |
| warn( | |
| f'Cannot unsubscribe {callback} from event "{event_name}" ' | |
| "as it was never subscribed to it." | |
| ) | |
| if len(subscribers) == 0: | |
| del channel[event_name] # keep things tidy | |
| def link(*figs, include_events=None, exclude_events=None, verbose=None): | |
| """Link the event channels of two figures together. | |
| When event channels are linked, any events that are published on one | |
| channel are simultaneously published on the other channel. Links are | |
| bi-directional. | |
| Parameters | |
| ---------- | |
| *figs : tuple of matplotlib.figure.Figure | tuple of Figure3D | |
| The figures whose event channel will be linked. | |
| include_events : list of str | None | |
| Select which events to publish across figures. By default (``None``), | |
| both figures will receive all of each other's events. Passing a list of | |
| event names will restrict the events being shared across the figures to | |
| only the given ones. | |
| exclude_events : list of str | None | |
| Select which events not to publish across figures. By default (``None``), | |
| no events are excluded. | |
| %(verbose)s | |
| """ | |
| if include_events is not None: | |
| include_events = set(include_events) | |
| if exclude_events is not None: | |
| exclude_events = set(exclude_events) | |
| # Make sure the event channels of the figures are setup properly. | |
| for fig in figs: | |
| _get_event_channel(fig) | |
| if fig not in _event_channel_links: | |
| _event_channel_links[fig] = weakref.WeakKeyDictionary() | |
| # Link the event channels | |
| for fig1 in figs: | |
| for fig2 in figs: | |
| if fig1 is not fig2: | |
| _event_channel_links[fig1][fig2] = (include_events, exclude_events) | |
| def unlink(fig, *, verbose=None): | |
| """Remove all links involving the event channel of the given figure. | |
| Parameters | |
| ---------- | |
| fig : matplotlib.figure.Figure | Figure3D | |
| The figure whose event channel should be unlinked from all other event | |
| channels. | |
| %(verbose)s | |
| """ | |
| linked_figs = _event_channel_links.get(fig) | |
| if linked_figs is not None: | |
| for linked_fig in linked_figs.keys(): | |
| del _event_channel_links[linked_fig][fig] | |
| if len(_event_channel_links[linked_fig]) == 0: | |
| del _event_channel_links[linked_fig] | |
| if fig in _event_channel_links: # need to check again because of weak refs | |
| del _event_channel_links[fig] | |
| def disable_ui_events(fig): | |
| """Temporarily disable generation of UI events. Use as context manager. | |
| Parameters | |
| ---------- | |
| fig : matplotlib.figure.Figure | Figure3D | |
| The figure whose UI event generation should be temporarily disabled. | |
| """ | |
| _disabled_event_channels.add(fig) | |
| try: | |
| yield | |
| finally: | |
| _disabled_event_channels.remove(fig) | |
| def _cleanup_agg(): | |
| """Call close_event for Agg canvases to help our doc build.""" | |
| import matplotlib.backends.backend_agg | |
| import matplotlib.figure | |
| for key in list(_event_channels): # we might remove keys as we go | |
| if isinstance(key, matplotlib.figure.Figure): | |
| canvas = key.canvas | |
| if isinstance(canvas, matplotlib.backends.backend_agg.FigureCanvasAgg): | |
| for cb in key.canvas.callbacks.callbacks["close_event"].values(): | |
| cb = cb() # get the true ref | |
| if cb is not None: | |
| cb() | |