diff --git a/mne/viz/tests/test_raw.py b/mne/viz/tests/test_raw.py index ee5dbf8e4e5..339b4f49c56 100644 --- a/mne/viz/tests/test_raw.py +++ b/mne/viz/tests/test_raw.py @@ -28,7 +28,7 @@ set_config, ) from mne.viz import plot_raw, plot_sensors -from mne.viz.utils import _fake_click +from mne.viz.utils import _fake_click, _fake_keypress base_dir = Path(__file__).parents[2] / "io" / "tests" / "data" raw_fname = base_dir / "test_raw.fif" @@ -1089,6 +1089,8 @@ def test_plot_sensors(raw): pytest.raises(ValueError, plot_sensors, raw.info, kind="sasaasd") plt.close("all") + print(raw.ch_names) + # Test lasso selection. fig, sels = raw.plot_sensors("select", show_names=True) ax = fig.axes[0] @@ -1100,6 +1102,13 @@ def test_plot_sensors(raw): _fake_click(fig, ax, (-0.13, 0.13), xform="data", kind="motion") _fake_click(fig, ax, (-0.13, 0.13), xform="data", kind="release") assert fig.lasso.selection == ["MEG 0121"] + + # Add another sensor with a single click. + _fake_keypress(fig, "control") + _fake_click(fig, ax, (-0.1278, 0.0318), xform="data") + _fake_click(fig, ax, (-0.1278, 0.0318), xform="data", kind="release") + _fake_keypress(fig, "control", kind="release") + assert fig.lasso.selection == ["MEG 0121", "MEG 0131"] plt.close("all") raw.info["dev_head_t"] = None # like empty room diff --git a/mne/viz/tests/test_topo.py b/mne/viz/tests/test_topo.py index dbc29832c09..48d031739b9 100644 --- a/mne/viz/tests/test_topo.py +++ b/mne/viz/tests/test_topo.py @@ -23,7 +23,7 @@ ) from mne.viz.evoked import _line_plot_onselect from mne.viz.topo import _imshow_tfr, _plot_update_evoked_topo_proj, iter_topography -from mne.viz.utils import _fake_click +from mne.viz.utils import _fake_click, _fake_keypress base_dir = Path(__file__).parents[2] / "io" / "tests" / "data" evoked_fname = base_dir / "test-ave.fif" @@ -310,7 +310,24 @@ def test_plot_topo_select(): """Test selecting sensors in an ERP topography plot.""" # Show topography evoked = _get_epochs().average() - plot_evoked_topo(evoked, select=True) + fig = plot_evoked_topo(evoked, select=True) + ax = fig.axes[0] + + # Lasso select 3 out of the 6 sensors. + _fake_click(fig, ax, (0.05, 0.5), xform="data") + _fake_click(fig, ax, (0.2, 0.5), xform="data", kind="motion") + _fake_click(fig, ax, (0.2, 0.6), xform="data", kind="motion") + _fake_click(fig, ax, (0.05, 0.6), xform="data", kind="motion") + _fake_click(fig, ax, (0.05, 0.5), xform="data", kind="motion") + _fake_click(fig, ax, (0.05, 0.5), xform="data", kind="release") + assert fig.lasso.selection == ["MEG 0132", "MEG 0133", "MEG 0131"] + + # Add another sensor with a single click. + _fake_keypress(fig, "control") + _fake_click(fig, ax, (0.11, 0.65), xform="data") + _fake_click(fig, ax, (0.21, 0.65), xform="data", kind="release") + _fake_keypress(fig, "control", kind="release") + assert fig.lasso.selection == ["MEG 0111", "MEG 0132", "MEG 0133", "MEG 0131"] def test_plot_tfr_topo(): diff --git a/mne/viz/tests/test_utils.py b/mne/viz/tests/test_utils.py index 59bf06fe16c..55dc0f1e65c 100644 --- a/mne/viz/tests/test_utils.py +++ b/mne/viz/tests/test_utils.py @@ -293,6 +293,10 @@ def test_select_from_collection(): _fake_click(fig, ax, (0.5, 1), xform="data", kind="release") assert lasso.selection == [] + # Doing a single click on a patch should not select it. + _fake_click(fig, ax, (1, 1), xform="data") + assert lasso.selection == [] + # Make a selection with two patches in it. _fake_click(fig, ax, (0, 0.5), xform="data") _fake_click(fig, ax, (3, 0.5), xform="data", kind="motion") @@ -302,24 +306,24 @@ def test_select_from_collection(): _fake_click(fig, ax, (0, 0.5), xform="data", kind="release") assert lasso.selection == ["A", "B"] - # Use SHIFT key to lasso an additional patch. - _fake_keypress(fig, "shift") + # Use Control key to lasso an additional patch. + _fake_keypress(fig, "control") _fake_click(fig, ax, (0.5, -0.5), xform="data") _fake_click(fig, ax, (1.5, -0.5), xform="data", kind="motion") _fake_click(fig, ax, (1.5, 0.5), xform="data", kind="motion") _fake_click(fig, ax, (0.5, 0.5), xform="data", kind="motion") _fake_click(fig, ax, (0.5, 0.5), xform="data", kind="release") - _fake_keypress(fig, "shift", kind="release") + _fake_keypress(fig, "control", kind="release") assert lasso.selection == ["A", "B", "D"] - # Use ALT key to remove a patch. - _fake_keypress(fig, "alt") + # Use CTRL+SHIFT to remove a patch. + _fake_keypress(fig, "ctrl+shift") _fake_click(fig, ax, (0.5, 0.5), xform="data") _fake_click(fig, ax, (1.5, 0.5), xform="data", kind="motion") _fake_click(fig, ax, (1.5, 1.5), xform="data", kind="motion") _fake_click(fig, ax, (0.5, 1.5), xform="data", kind="motion") _fake_click(fig, ax, (0.5, 1.5), xform="data", kind="release") - _fake_keypress(fig, "alt", kind="release") + _fake_keypress(fig, "ctrl+shift", kind="release") assert lasso.selection == ["B", "D"] # Check that the two selected patches have a different appearance. @@ -327,3 +331,15 @@ def test_select_from_collection(): ec = lasso.collection.get_edgecolors() assert (fc[:, -1] == [0.5, 1.0, 0.5, 1.0]).all() assert (ec[:, -1] == [0.25, 1.0, 0.25, 1.0]).all() + + # Test adding and removing single channels. + lasso.select_one(2) # should not do anything without modifier keys + assert lasso.selection == ["B", "D"] + _fake_keypress(fig, "control") + lasso.select_one(2) # add to selection + _fake_keypress(fig, "control", kind="release") + assert lasso.selection == ["B", "C", "D"] + _fake_keypress(fig, "ctrl+shift") + lasso.select_one(1) # remove from selection + assert lasso.selection == ["C", "D"] + _fake_keypress(fig, "ctrl+shift", kind="release") diff --git a/mne/viz/topo.py b/mne/viz/topo.py index f2073435003..6a4e5ff1079 100644 --- a/mne/viz/topo.py +++ b/mne/viz/topo.py @@ -275,10 +275,9 @@ def on_select(): publish(fig, ChannelsSelect(ch_names=fig.lasso.selection)) def on_channels_select(event): - ch_inds = {name: i for i, name in enumerate(shown_ch_names)} - selection_inds = [ - ch_inds[name] for name in event.ch_names if name in ch_inds - ] + selection_inds = np.flatnonzero( + np.isin(shown_ch_names, event.ch_names) + ) fig.lasso.select_many(selection_inds) fig.lasso.callbacks.append(on_select) @@ -381,9 +380,15 @@ def _plot_topo( def _plot_topo_onpick(event, show_func): """Onpick callback that shows a single channel in a new figure.""" - # make sure that the swipe gesture in OS-X doesn't open many figures orig_ax = event.inaxes - if orig_ax.figure.canvas._key in ["shift", "alt"]: + fig = orig_ax.figure + + # If we are doing lasso select, allow it to handle the click instead. + if fig.lasso is not None and event.key in ["control", "ctrl+shift"]: + return + + # make sure that the swipe gesture in OS-X doesn't open many figures + if fig.canvas._key in ["shift", "alt"]: return import matplotlib.pyplot as plt diff --git a/mne/viz/utils.py b/mne/viz/utils.py index ff628c4c41c..b01462acfda 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -952,7 +952,7 @@ def plot_sensors( Whether to plot the sensors as 3d, topomap or as an interactive sensor selection dialog. Available options ``'topomap'``, ``'3d'``, ``'select'``. If ``'select'``, a set of channels can be selected - interactively by using lasso selector or clicking while holding the shift + interactively by using lasso selector or clicking while holding the control key. The selected channels are returned along with the figure instance. Defaults to ``'topomap'``. ch_type : None | str @@ -1163,10 +1163,10 @@ def _onpick_sensor(event, fig, ax, pos, ch_names, show_names): if event.mouseevent.inaxes != ax: return - if event.mouseevent.key in ["shift", "alt"] and fig.lasso is not None: + if fig.lasso is not None and event.mouseevent.key in ["control", "ctrl+shift"]: + # Add the sensor to the selection instead of showing its name. for ind in event.ind: fig.lasso.select_one(ind) - return if show_names: return # channel names already visible @@ -1278,10 +1278,7 @@ def on_select(): publish(fig, ChannelsSelect(ch_names=fig.lasso.selection)) def on_channels_select(event): - ch_inds = {name: i for i, name in enumerate(ch_names)} - selection_inds = [ - ch_inds[name] for name in event.ch_names if name in ch_inds - ] + selection_inds = np.flatnonzero(np.isin(ch_names, event.ch_names)) fig.lasso.select_many(selection_inds) fig.lasso.callbacks.append(on_select) @@ -1614,6 +1611,9 @@ class SelectFromCollection: This tool highlights selected objects by fading other objects out (i.e., reducing their alpha values). + Holding down the Control key will add to the current selection, and holding down + Control+Shift will remove from the current selection. + Parameters ---------- ax : instance of Axes @@ -1711,14 +1711,17 @@ def on_select(self, verts): """Select a subset from the collection.""" from matplotlib.path import Path - if len(verts) <= 3: # Seems to be a good way to exclude single clicks. + # Don't respond to single clicks without extra keys being hold down. + # Figures like plot_evoked_topo want to do something else with them. + print(verts, self.canvas._key) + if len(verts) <= 3 and self.canvas._key not in ["control", "ctrl+shift"]: return path = Path(verts) inds = np.nonzero([path.intersects_path(p) for p in self.paths])[0] - if self.canvas._key == "shift": # Appending selection. + if self.canvas._key == "control": # Appending selection. self.selection_inds = np.union1d(self.selection_inds, inds).astype("int") - elif self.canvas._key == "alt": # Removing selection. + elif self.canvas._key == "ctrl+shift": self.selection_inds = np.setdiff1d(self.selection_inds, inds).astype("int") else: self.selection_inds = inds @@ -1728,9 +1731,9 @@ def on_select(self, verts): def select_one(self, ind): """Select or deselect one sensor.""" - if self.canvas._key == "shift": + if self.canvas._key == "control": self.selection_inds = np.union1d(self.selection_inds, [ind]) - elif self.canvas._key == "alt": + elif self.canvas._key == "ctrl+shift": self.selection_inds = np.setdiff1d(self.selection_inds, [ind]) else: return # don't notify()