class MainWindow(QMainWindow): """MNELAB main window.""" def __init__(self, model): """Initialize MNELAB main window. Parameters ---------- model : mnelab.model.Model instance The main window needs to connect to a model containing all data sets. This decouples the GUI from the data (model/view). """ super().__init__() mp.set_start_method("spawn", force=True) # required for Linux/macOS self.model = model # data model self.setWindowTitle("MNELAB") # restore settings settings = read_settings() self.recent = settings["recent"] # list of recent files if settings["geometry"]: self.restoreGeometry(settings["geometry"]) else: self.setGeometry(300, 300, 1000, 750) # default window size self.move(QApplication.desktop().screen().rect().center() - self.rect().center()) # center window if settings["state"]: self.restoreState(settings["state"]) self.actions = {} # contains all actions # initialize menus file_menu = self.menuBar().addMenu("&File") icon = QIcon(":/open_file.svg") self.actions["open_file"] = file_menu.addAction( icon, "&Open...", self.open_data, QKeySequence.Open) self.recent_menu = file_menu.addMenu("Open recent") self.recent_menu.aboutToShow.connect(self._update_recent_menu) self.recent_menu.triggered.connect(self._load_recent) if not self.recent: self.recent_menu.setEnabled(False) self.actions["close_file"] = file_menu.addAction( "&Close", self.model.remove_data, QKeySequence.Close) self.actions["close_all"] = file_menu.addAction( "Close all", self.close_all) file_menu.addSeparator() icon = QIcon(":/meta_info.svg") self.actions["meta_info"] = file_menu.addAction( icon, "Show information...", self.meta_info) file_menu.addSeparator() self.actions["import_bads"] = file_menu.addAction( "Import bad channels...", lambda: self.import_file( model.import_bads, "Import bad channels", "*.csv")) self.actions["import_events"] = file_menu.addAction( "Import events...", lambda: self.import_file( model.import_events, "Import events", "*.csv")) self.actions["import_annotations"] = file_menu.addAction( "Import annotations...", lambda: self.import_file( model.import_annotations, "Import annotations", "*.csv")) self.actions["import_ica"] = file_menu.addAction( "Import &ICA...", lambda: self.open_file( model.import_ica, "Import ICA", "*.fif *.fif.gz")) file_menu.addSeparator() self.export_menu = file_menu.addMenu("Export data") for name, ext in EXPORT_FORMATS.items(): self.actions["export_data_" + ext] = self.export_menu.addAction( f"{name} ({ext[1:].upper()})...", partial(self.export_file, model.export_data, "Export data", ext)) self.actions["export_bads"] = file_menu.addAction( "Export &bad channels...", lambda: self.export_file( model.export_bads, "Export bad channels", "*.csv")) self.actions["export_events"] = file_menu.addAction( "Export &events...", lambda: self.export_file( model.export_events, "Export events", "*.csv")) self.actions["export_annotations"] = file_menu.addAction( "Export &annotations...", lambda: self.export_file( model.export_annotations, "Export annotations", "*.csv")) self.actions["export_ica"] = file_menu.addAction( "Export ICA...", lambda: self.export_file( model.export_ica, "Export ICA", "*.fif *.fif.gz")) file_menu.addSeparator() self.actions["quit"] = file_menu.addAction("&Quit", self.close, QKeySequence.Quit) edit_menu = self.menuBar().addMenu("&Edit") self.actions["pick_chans"] = edit_menu.addAction( "Pick &channels...", self.pick_channels) icon = QIcon(":/chan_props.svg") self.actions["chan_props"] = edit_menu.addAction( icon, "Channel &properties...", self.channel_properties) self.actions["set_montage"] = edit_menu.addAction( "Set &montage...", self.set_montage) edit_menu.addSeparator() self.actions["set_ref"] = edit_menu.addAction("&Set reference...", self.set_reference) edit_menu.addSeparator() self.actions["annotations"] = edit_menu.addAction( "Annotations...", self.edit_annotations) self.actions["events"] = edit_menu.addAction("Events...", self.edit_events) edit_menu.addSeparator() self.actions["crop"] = edit_menu.addAction("&Crop data...", self.crop) plot_menu = self.menuBar().addMenu("&Plot") icon = QIcon(":/plot_data.svg") self.actions["plot_data"] = plot_menu.addAction( icon, "&Data...", self.plot_data) icon = QIcon(":/plot_psd.svg") self.actions["plot_psd"] = plot_menu.addAction( icon, "&Power spectral density...", self.plot_psd) icon = QIcon(":/plot_montage.svg") self.actions["plot_montage"] = plot_menu.addAction( icon, "&Montage...", self.plot_montage) plot_menu.addSeparator() self.actions["plot_ica_components"] = plot_menu.addAction( "ICA &components...", self.plot_ica_components) self.actions["plot_ica_sources"] = plot_menu.addAction( "ICA &sources...", self.plot_ica_sources) tools_menu = self.menuBar().addMenu("&Tools") icon = QIcon(":/filter.svg") self.actions["filter"] = tools_menu.addAction(icon, "&Filter data...", self.filter_data) icon = QIcon(":/find_events.svg") self.actions["find_events"] = tools_menu.addAction( icon, "Find &events...", self.find_events) self.actions["events_from_annotations"] = tools_menu.addAction( "Create events from annotations", self.events_from_annotations) tools_menu.addSeparator() icon = QIcon(":/run_ica.svg") self.actions["run_ica"] = tools_menu.addAction(icon, "Run &ICA...", self.run_ica) self.actions["apply_ica"] = tools_menu.addAction( "Apply &ICA", self.apply_ica) tools_menu.addSeparator() self.actions["interpolate_bads"] = tools_menu.addAction( "Interpolate bad channels...", self.interpolate_bads) tools_menu.addSeparator() icon = QIcon(":/epoch_data.svg") self.actions["epoch_data"] = tools_menu.addAction( icon, "Create Epochs...", self.epoch_data) view_menu = self.menuBar().addMenu("&View") self.actions["history"] = view_menu.addAction("&History...", self.show_history) self.actions["toolbar"] = view_menu.addAction("&Toolbar", self._toggle_toolbar) self.actions["toolbar"].setCheckable(True) self.actions["statusbar"] = view_menu.addAction( "&Statusbar", self._toggle_statusbar) self.actions["statusbar"].setCheckable(True) help_menu = self.menuBar().addMenu("&Help") self.actions["about"] = help_menu.addAction("&About", self.show_about) self.actions["about_qt"] = help_menu.addAction("About &Qt", self.show_about_qt) # actions that are always enabled self.always_enabled = [ "open_file", "about", "about_qt", "quit", "toolbar", "statusbar" ] # set up toolbar self.toolbar = self.addToolBar("toolbar") self.toolbar.setObjectName("toolbar") self.toolbar.addAction(self.actions["open_file"]) self.toolbar.addAction(self.actions["meta_info"]) self.toolbar.addSeparator() self.toolbar.addAction(self.actions["chan_props"]) self.toolbar.addSeparator() self.toolbar.addAction(self.actions["plot_data"]) self.toolbar.addAction(self.actions["plot_psd"]) self.toolbar.addAction(self.actions["plot_montage"]) self.toolbar.addSeparator() self.toolbar.addAction(self.actions["filter"]) self.toolbar.addAction(self.actions["find_events"]) self.toolbar.addAction(self.actions["epoch_data"]) self.toolbar.addAction(self.actions["run_ica"]) self.setUnifiedTitleAndToolBarOnMac(True) if settings["toolbar"]: self.toolbar.show() self.actions["toolbar"].setChecked(True) else: self.toolbar.hide() self.actions["toolbar"].setChecked(False) # set up data model for sidebar (list of open files) self.names = QStringListModel() self.names.dataChanged.connect(self._update_names) splitter = QSplitter() self.sidebar = QListView() self.sidebar.setFrameStyle(QFrame.NoFrame) self.sidebar.setFocusPolicy(Qt.NoFocus) self.sidebar.setModel(self.names) self.sidebar.clicked.connect(self._update_data) splitter.addWidget(self.sidebar) self.infowidget = InfoWidget() splitter.addWidget(self.infowidget) width = splitter.size().width() splitter.setSizes((int(width * 0.3), int(width * 0.7))) self.setCentralWidget(splitter) self.status_label = QLabel() self.statusBar().addPermanentWidget(self.status_label) if settings["statusbar"]: self.statusBar().show() self.actions["statusbar"].setChecked(True) else: self.statusBar().hide() self.actions["statusbar"].setChecked(False) self.setAcceptDrops(True) self.data_changed() def data_changed(self): # update sidebar self.names.setStringList(self.model.names) self.sidebar.setCurrentIndex(self.names.index(self.model.index)) # update info widget if self.model.data: self.infowidget.set_values(self.model.get_info()) else: self.infowidget.clear() # update status bar if self.model.data: mb = self.model.nbytes / 1024**2 self.status_label.setText("Total Memory: {:.2f} MB".format(mb)) else: self.status_label.clear() # toggle actions if len(self.model) == 0: # disable if no data sets are currently open enabled = False else: enabled = True for name, action in self.actions.items(): # toggle if name not in self.always_enabled: action.setEnabled(enabled) if self.model.data: # toggle if specific conditions are met bads = bool(self.model.current["data"].info["bads"]) self.actions["export_bads"].setEnabled(enabled and bads) events = self.model.current["events"] is not None self.actions["export_events"].setEnabled(enabled and events) if self.model.current["dtype"] == "raw": annot = bool(self.model.current["data"].annotations) else: annot = False self.actions["export_annotations"].setEnabled(enabled and annot) self.actions["annotations"].setEnabled(enabled and annot) montage = bool(self.model.current["montage"]) self.actions["plot_montage"].setEnabled(enabled and montage) ica = bool(self.model.current["ica"]) self.actions["apply_ica"].setEnabled(enabled and ica) self.actions["export_ica"].setEnabled(enabled and ica) self.actions["plot_ica_components"].setEnabled(enabled and ica and montage) self.actions["plot_ica_sources"].setEnabled(enabled and ica) self.actions["interpolate_bads"].setEnabled(enabled and montage and bads) self.actions["events"].setEnabled(enabled and events) self.actions["events_from_annotations"].setEnabled(enabled and annot) self.actions["find_events"].setEnabled( enabled and self.model.current["dtype"] == "raw") self.actions["epoch_data"].setEnabled( enabled and events and self.model.current["dtype"] == "raw") self.actions["crop"].setEnabled( enabled and self.model.current["dtype"] == "raw") self.actions["meta_info"].setEnabled( enabled and self.model.current["ftype"] == "Extensible Data Format") # add to recent files if len(self.model) > 0: self._add_recent(self.model.current["fname"]) def open_data(self, fname=None): """Open raw file.""" if fname is None: fname = QFileDialog.getOpenFileName(self, "Open raw", filter="*")[0] if fname: if not (isfile(fname) or isdir(fname)): self._remove_recent(fname) QMessageBox.critical(self, "File does not exist", f"File {fname} does not exist anymore.") return name, ext, ftype = split_fname(fname, IMPORT_FORMATS) if ext in [".xdf", ".xdfz", ".xdf.gz"]: streams = parse_chunks(parse_xdf(fname)) rows, disabled = [], [] for idx, s in enumerate(streams): rows.append([ s["stream_id"], s["name"], s["type"], s["channel_count"], s["channel_format"], s["nominal_srate"] ]) is_marker = (s["nominal_srate"] == 0 or s["channel_format"] == "string") if is_marker: # disable marker streams disabled.append(idx) enabled = list(set(range(len(rows))) - set(disabled)) if enabled: selected = enabled[0] else: selected = None dialog = XDFStreamsDialog(self, rows, selected=selected, disabled=disabled) if dialog.exec_(): row = dialog.view.selectionModel().selectedRows()[0].row() stream_id = dialog.model.data(dialog.model.index(row, 0)) self.model.load(fname, stream_id=stream_id) else: # all other file formats try: self.model.load(fname) except FileNotFoundError as e: QMessageBox.critical(self, "File not found", str(e)) except UnknownFileTypeError as e: QMessageBox.critical(self, "Unknown file type", str(e)) def open_file(self, f, text, ffilter): """Open file.""" fname = QFileDialog.getOpenFileName(self, text, filter="*")[0] if fname: f(fname) def export_file(self, f, text, ffilter): """Export to file.""" fname = QFileDialog.getSaveFileName(self, text, filter="*")[0] if fname: f(fname, ffilter) def import_file(self, f, text, ffilter): """Import file.""" fname = QFileDialog.getOpenFileName(self, text, filter="*")[0] if fname: try: f(fname) except LabelsNotFoundError as e: QMessageBox.critical(self, "Channel labels not found", str(e)) except InvalidAnnotationsError as e: QMessageBox.critical(self, "Invalid annotations", str(e)) def close_all(self): """Close all currently open data sets.""" msg = QMessageBox.question(self, "Close all data sets", "Close all data sets?") if msg == QMessageBox.Yes: while len(self.model) > 0: self.model.remove_data() def meta_info(self): xml = get_xml(self.model.current["fname"]) dialog = MetaInfoDialog(self, xml) dialog.exec_() def pick_channels(self): """Pick channels in current data set.""" channels = self.model.current["data"].info["ch_names"] dialog = PickChannelsDialog(self, channels, selected=channels) if dialog.exec_(): picks = [item.data(0) for item in dialog.channels.selectedItems()] drops = set(channels) - set(picks) if drops: self.auto_duplicate() self.model.drop_channels(drops) self.model.history.append(f"raw.drop({drops})") def channel_properties(self): """Show channel properties dialog.""" info = self.model.current["data"].info dialog = ChannelPropertiesDialog(self, info) if dialog.exec_(): dialog.model.sort(0) bads = [] renamed = {} types = {} for i in range(dialog.model.rowCount()): new_label = dialog.model.item(i, 1).data(Qt.DisplayRole) old_label = info["ch_names"][i] if new_label != old_label: renamed[old_label] = new_label new_type = dialog.model.item(i, 2).data(Qt.DisplayRole).lower() old_type = channel_type(info, i).lower() if new_type != old_type: types[new_label] = new_type if dialog.model.item(i, 3).checkState() == Qt.Checked: bads.append(info["ch_names"][i]) self.model.set_channel_properties(bads, renamed, types) def set_montage(self): """Set montage.""" montages = mne.channels.get_builtin_montages() # TODO: currently it is not possible to remove an existing montage dialog = MontageDialog(self, montages, selected=self.model.current["montage"]) if dialog.exec_(): name = dialog.montages.selectedItems()[0].data(0) montage = mne.channels.make_standard_montage(name) ch_names = self.model.current["data"].info["ch_names"] # check if at least one channel name matches a name in the montage if set(ch_names) & set(montage.ch_names): self.model.set_montage(name) else: QMessageBox.critical( self, "No matching channel names", "Channel names defined in the montage do " "not match any channel name in the data.") def edit_annotations(self): fs = self.model.current["data"].info["sfreq"] pos = self.model.current["data"].annotations.onset pos = (pos * fs).astype(int).tolist() dur = self.model.current["data"].annotations.duration dur = (dur * fs).astype(int).tolist() desc = self.model.current["data"].annotations.description.tolist() dialog = AnnotationsDialog(self, pos, dur, desc) if dialog.exec_(): rows = dialog.table.rowCount() onset, duration, description = [], [], [] for i in range(rows): data = dialog.table.item(i, 0).data(Qt.DisplayRole) onset.append(float(data) / fs) data = dialog.table.item(i, 1).data(Qt.DisplayRole) duration.append(float(data) / fs) data = dialog.table.item(i, 2).data(Qt.DisplayRole) description.append(data) self.model.set_annotations(onset, duration, description) def edit_events(self): pos = self.model.current["events"][:, 0].tolist() desc = self.model.current["events"][:, 2].tolist() dialog = EventsDialog(self, pos, desc) if dialog.exec_(): rows = dialog.table.rowCount() events = np.zeros((rows, 3), dtype=int) for i in range(rows): pos = int(dialog.table.item(i, 0).data(Qt.DisplayRole)) desc = int(dialog.table.item(i, 1).data(Qt.DisplayRole)) events[i] = pos, 0, desc self.model.set_events(events) def crop(self): """Filter data.""" fs = self.model.current["data"].info["sfreq"] length = self.model.current["data"].n_times / fs dialog = CropDialog(self, 0, length) if dialog.exec_(): self.auto_duplicate() if dialog.start is None: dialog.start = 0 self.model.crop(dialog.start, dialog.stop) def plot_data(self): """Plot data.""" # self.bad is needed to update history if bad channels are selected in # the interactive plot window (see also self.eventFilter) self.bads = self.model.current["data"].info["bads"] events = self.model.current["events"] nchan = self.model.current["data"].info["nchan"] fig = self.model.current["data"].plot(events=events, n_channels=nchan, title=self.model.current["name"], scalings="auto", show=False) self.model.history.append("data.plot(n_channels={})".format(nchan)) win = fig.canvas.manager.window win.setWindowTitle(self.model.current["name"]) win.findChild(QStatusBar).hide() win.installEventFilter(self) # detect if the figure is closed # prevent closing the window with the escape key try: fig._mne_params["close_key"] = None except AttributeError: # does not exist in older MNE versions pass fig.show() def plot_psd(self): """Plot power spectral density (PSD).""" kwds = {"show": False} if self.model.current["type"] == "raw": kwds.update({"average": False, "spatial_colors": False}) fig = self.model.current["data"].plot_psd(**kwds) win = fig.canvas.manager.window win.setWindowTitle("Power spectral density") fig.show() def plot_montage(self): """Plot current montage.""" fig = self.model.current["data"].plot_sensors(show_names=True, show=False) win = fig.canvas.manager.window win.setWindowTitle("Montage") win.findChild(QStatusBar).hide() win.findChild(QToolBar).hide() fig.show() def plot_ica_components(self): self.model.current["ica"].plot_components( inst=self.model.current["data"]) def plot_ica_sources(self): self.model.current["ica"].plot_sources(inst=self.model.current["data"]) def run_ica(self): """Run ICA calculation.""" dialog = RunICADialog(self, self.model.current["data"].info["nchan"], have["picard"], have["sklearn"]) if dialog.exec_(): calc = CalcDialog(self, "Calculating ICA", "Calculating ICA.") method = dialog.method.currentText() exclude_bad_segments = dialog.exclude_bad_segments.isChecked() fit_params = {} if not dialog.extended.isHidden(): fit_params["extended"] = dialog.extended.isChecked() if not dialog.ortho.isHidden(): fit_params["ortho"] = dialog.ortho.isChecked() ica = mne.preprocessing.ICA(method=dialog.methods[method], fit_params=fit_params) self.model.history.append(f"ica = mne.preprocessing.ICA(" f"method={dialog.methods[method]}, " f"fit_params={fit_params})") pool = mp.Pool(1) kwds = {"reject_by_annotation": exclude_bad_segments} res = pool.apply_async(func=ica.fit, args=(self.model.current["data"], ), kwds=kwds, callback=lambda x: calc.accept()) if not calc.exec_(): pool.terminate() else: self.model.current["ica"] = res.get(timeout=1) self.model.history.append(f"ica.fit(inst=raw, " f"reject_by_annotation=" f"{exclude_bad_segments})") self.data_changed() def apply_ica(self): """Apply current fitted ICA.""" self.auto_duplicate() self.model.apply_ica() def interpolate_bads(self): """Interpolate bad channels""" dialog = InterpolateBadsDialog(self) if dialog.exec_(): duplicated = self.auto_duplicate() try: self.model.interpolate_bads(dialog.reset_bads, dialog.mode, dialog.origin) except ValueError as e: if duplicated: # undo self.model.remove_data() self.model.index -= 1 self.data_changed() msgbox = ErrorMessageBox(self, "Could not interpolate bad channels", str(e), traceback.format_exc()) msgbox.show() def filter_data(self): """Filter data.""" dialog = FilterDialog(self) if dialog.exec_(): self.auto_duplicate() self.model.filter(dialog.low, dialog.high) def find_events(self): info = self.model.current["data"].info # use first stim channel as default in dialog default_stim = 0 for i in range(info["nchan"]): if mne.io.pick.channel_type(info, i) == "stim": default_stim = i break dialog = FindEventsDialog(self, info["ch_names"], default_stim) if dialog.exec_(): stim_channel = dialog.stimchan.currentText() consecutive = dialog.consecutive.isChecked() initial_event = dialog.initial_event.isChecked() uint_cast = dialog.uint_cast.isChecked() min_dur = dialog.minduredit.value() shortest_event = dialog.shortesteventedit.value() self.model.find_events(stim_channel=stim_channel, consecutive=consecutive, initial_event=initial_event, uint_cast=uint_cast, min_duration=min_dur, shortest_event=shortest_event) def events_from_annotations(self): self.model.events_from_annotations() def epoch_data(self): """Epoch raw data.""" dialog = EpochDialog(self, self.model.current["events"]) if dialog.exec_(): events = [ int(item.text()) for item in dialog.events.selectedItems() ] tmin = dialog.tmin.value() tmax = dialog.tmax.value() if dialog.baseline.isChecked(): baseline = dialog.a.value(), dialog.b.value() else: baseline = None duplicated = self.auto_duplicate() try: self.model.epoch_data(events, tmin, tmax, baseline) except ValueError as e: if duplicated: # undo self.model.remove_data() self.model.index -= 1 self.data_changed() msgbox = ErrorMessageBox(self, "Could not create epochs", str(e), traceback.format_exc()) msgbox.show() def set_reference(self): """Set reference.""" dialog = ReferenceDialog(self) if dialog.exec_(): self.auto_duplicate() if dialog.average.isChecked(): self.model.set_reference("average") else: ref = [c.strip() for c in dialog.channellist.text().split(",")] self.model.set_reference(ref) def show_history(self): """Show history.""" dialog = HistoryDialog(self, "\n".join(self.model.history)) dialog.exec_() def show_about(self): """Show About dialog.""" msg_box = QMessageBox(self) text = (f"<img src=':/mnelab_logo.png'>" f"<p>MNELAB {__version__}</p>") msg_box.setText(text) mnelab_url = "github.com/cbrnr/mnelab" mne_url = "github.com/mne-tools/mne-python" pkgs = [] for key, value in have.items(): if value: pkgs.append(f"{key} ({value})") else: pkgs.append(f"{key} (not installed)") text = (f'<nobr><p>This program uses Python ' f'{".".join(str(k) for k in version_info[:3])} and the ' f'following packages:</p></nobr>' f'<p>{", ".join(pkgs)}</p>' f'<nobr><p>MNELAB repository: ' f'<a href=https://{mnelab_url}>{mnelab_url}</a></p></nobr>' f'<nobr><p>MNE repository: ' f'<a href=https://{mne_url}>{mne_url}</a></p></nobr>' f'<p>Licensed under the BSD 3-clause license.</p>' f'<p>Copyright 2017-2020 by Clemens Brunner.</p>') msg_box.setInformativeText(text) msg_box.exec_() def show_about_qt(self): """Show About Qt dialog.""" QMessageBox.aboutQt(self, "About Qt") def auto_duplicate(self): """Automatically duplicate current data set. If the current data set is stored in a file (i.e. was loaded directly from a file), a new data set is automatically created. If the current data set is not stored in a file (i.e. was created by operations in MNELAB), a dialog box asks the user if the current data set should be overwritten or duplicated. Returns ------- duplicated : bool True if the current data set was automatically duplicated, False if the current data set was overwritten. """ # if current data is stored in a file create a new data set if self.model.current["fname"]: self.model.duplicate_data() return True # otherwise ask the user else: msg = QMessageBox.question(self, "Overwrite existing data set", "Overwrite existing data set?") if msg == QMessageBox.No: # create new data set self.model.duplicate_data() return True return False def _add_recent(self, fname): """Add a file to recent file list. Parameters ---------- fname : str File name. """ if fname in self.recent: # avoid duplicates self.recent.remove(fname) self.recent.insert(0, fname) while len(self.recent) > MAX_RECENT: # prune list self.recent.pop() write_settings(recent=self.recent) if not self.recent_menu.isEnabled(): self.recent_menu.setEnabled(True) def _remove_recent(self, fname): """Remove file from recent file list. Parameters ---------- fname : str File name. """ if fname in self.recent: self.recent.remove(fname) write_settings(recent=self.recent) if not self.recent: self.recent_menu.setEnabled(False) @pyqtSlot(QModelIndex) def _update_data(self, selected): """Update index and information based on the state of the sidebar. Parameters ---------- selected : QModelIndex Index of the selected row. """ if selected.row() != self.model.index: self.model.index = selected.row() self.data_changed() @pyqtSlot(QModelIndex, QModelIndex) def _update_names(self, start, stop): """Update names in DataSets after changes in sidebar.""" for index in range(start.row(), stop.row() + 1): self.model.data[index]["name"] = self.names.stringList()[index] @pyqtSlot() def _update_recent_menu(self): self.recent_menu.clear() for recent in self.recent: self.recent_menu.addAction(recent) @pyqtSlot(QAction) def _load_recent(self, action): self.open_data(fname=action.text()) @pyqtSlot() def _toggle_toolbar(self): if self.toolbar.isHidden(): self.toolbar.show() else: self.toolbar.hide() write_settings(toolbar=not self.toolbar.isHidden()) @pyqtSlot() def _toggle_statusbar(self): if self.statusBar().isHidden(): self.statusBar().show() else: self.statusBar().hide() write_settings(statusbar=not self.statusBar().isHidden()) @pyqtSlot(QDropEvent) def dragEnterEvent(self, event): if event.mimeData().hasUrls(): event.acceptProposedAction() @pyqtSlot(QDropEvent) def dropEvent(self, event): mime = event.mimeData() if mime.hasUrls(): urls = mime.urls() for url in urls: try: self.open_data(url.toLocalFile()) except FileNotFoundError as e: QMessageBox.critical(self, "File not found", str(e)) @pyqtSlot(QEvent) def closeEvent(self, event): """Close application. Parameters ---------- event : QEvent Close event. """ write_settings(geometry=self.saveGeometry(), state=self.saveState()) if self.model.history: print("\nCommand History") print("===============") print("\n".join(self.model.history)) QApplication.quit() def eventFilter(self, source, event): # currently the only source is the raw plot window if event.type() == QEvent.Close: self.data_changed() bads = self.model.current["data"].info["bads"] if self.bads != bads: self.model.history.append(f"data.info['bads'] = {bads}") return QObject.eventFilter(self, source, event)
class MainWindow(QMainWindow): """MNELAB main window. """ def __init__(self): super().__init__() self.datasets = DataSets() self._max_recent = 6 # maximum number of recent files self.history = [] # command history settings = self._read_settings() self.recent = settings["recent"] if settings["recent"] else [] self.setGeometry(300, 300, 800, 600) self.setWindowTitle("MNELAB") menubar = self.menuBar() file_menu = menubar.addMenu("&File") file_menu.addAction("&Open...", self.open_file, QKeySequence.Open) self.recent_menu = file_menu.addMenu("Open Recent") self.recent_menu.aboutToShow.connect(self._update_recent_menu) self.recent_menu.triggered.connect(self._load_recent) if not self.recent: self.recent_menu.setEnabled(False) self.close_file_action = file_menu.addAction("&Close", self.close_file, QKeySequence.Close) file_menu.addSeparator() file_menu.addAction("&Quit", self.close, QKeySequence.Quit) plot_menu = menubar.addMenu("&Plot") self.plot_raw_action = plot_menu.addAction("&Raw data", self.plot_raw) tools_menu = menubar.addMenu("&Tools") self.filter_action = tools_menu.addAction("&Filter data...", self.filter_data) self.run_ica_action = tools_menu.addAction("&Run ICA...") self.import_ica_action = tools_menu.addAction("&Load ICA...", self.load_ica) view_menu = menubar.addMenu("&View") view_menu.addAction("Show/hide statusbar", self._toggle_statusbar) help_menu = menubar.addMenu("&Help") help_menu.addAction("&About", self.show_about) help_menu.addAction("About &Qt", self.show_about_qt) self.names = QStringListModel() splitter = QSplitter() self.sidebar = QListView() self.sidebar.setFocusPolicy(0) self.sidebar.setFrameStyle(0) self.sidebar.setModel(self.names) self.sidebar.clicked.connect(self._update_data) splitter.addWidget(self.sidebar) self.infowidget = InfoWidget() splitter.addWidget(self.infowidget) width = splitter.size().width() splitter.setSizes((width * 0.25, width * 0.75)) self.setCentralWidget(splitter) self.status_label = QLabel() self.statusBar().addPermanentWidget(self.status_label) if settings["statusbar"]: self.statusBar().show() else: self.statusBar().hide() self._toggle_actions(False) self.show() def open_file(self): """Open file. """ fname = QFileDialog.getOpenFileName(self, "Open file", filter="*.bdf *.edf")[0] if fname: self.load_file(fname) def load_file(self, fname): raw = mne.io.read_raw_edf(fname, stim_channel=None, preload=True) name, _ = splitext(split(fname)[-1]) self.history.append("raw = mne.io.read_raw_edf('{}', " "stim_channel=None, preload=True)".format(fname)) self.datasets.insert_data(DataSet(name=name, fname=fname, raw=raw)) self._update_sidebar() self._update_main() self._add_recent(fname) self._update_statusbar() self._toggle_actions() def close_file(self): """Close current file. """ self.datasets.remove_data() self._update_sidebar() self._update_main() self._update_statusbar() if not self.datasets: self.infowidget.clear() self._toggle_actions(False) self.status_label.clear() def get_info(self): """Get basic information on current file. """ raw = self.datasets.current.raw fname = self.datasets.current.fname nchan = raw.info["nchan"] chans = Counter([channel_type(raw.info, i) for i in range(nchan)]) return {"File name": fname if fname else "-", "Number of channels": raw.info["nchan"], "Channels": ", ".join( [" ".join([str(v), k.upper()]) for k, v in chans.items()]), "Samples": raw.n_times, "Sampling frequency": str(raw.info["sfreq"]) + " Hz", "Length": str(raw.n_times / raw.info["sfreq"]) + " s", "Size in memory": "{:.2f} MB".format( raw._data.nbytes / 1024 ** 2), "Size on disk": "-" if not fname else "{:.2f} MB".format( getsize(fname) / 1024 ** 2)} def plot_raw(self): """Plot raw data. """ events = self.datasets.current.events self.datasets.current.raw.plot(events=events) def load_ica(self): """Load ICA solution from a file. """ fname = QFileDialog.getOpenFileName(self, "Load ICA", filter="*.fif *.fif.gz") if fname[0]: self.state.ica = mne.preprocessing.read_ica(fname[0]) def filter_data(self): dialog = FilterDialog() if dialog.exec_(): low, high = dialog.low, dialog.high self.datasets.current.raw.filter(low, high) self.history.append("raw.filter({}, {})".format(low, high)) if QMessageBox.question(self, "Add new data set", "Store the current signals in a new data " "set?") == QMessageBox.Yes: new = DataSet(name="NEW", fname="", raw=self.datasets.current.raw) self.datasets.insert_data(new) self._update_sidebar() self._update_main() self._update_statusbar() def show_about(self): """Show About dialog. """ QMessageBox.about(self, "About MNELAB", "Licensed under the BSD 3-clause license.\n" "Copyright 2017 by Clemens Brunner.") def show_about_qt(self): """Show About Qt dialog. """ QMessageBox.aboutQt(self, "About Qt") def _update_sidebar(self): self.names.setStringList(self.datasets.names) self.sidebar.setCurrentIndex(self.names.index(self.datasets.index)) def _update_main(self): if self.datasets: self.infowidget.set_values(self.get_info()) else: self.infowidget.clear() def _update_statusbar(self): if self.datasets: mb = self.datasets.nbytes / 1024 ** 2 self.status_label.setText("Total Memory: {:.2f} MB".format(mb)) else: self.status_label.clear() def _toggle_actions(self, enabled=True): """Toggle actions. """ self.close_file_action.setEnabled(enabled) self.plot_raw_action.setEnabled(enabled) self.filter_action.setEnabled(enabled) self.run_ica_action.setEnabled(enabled) self.import_ica_action.setEnabled(enabled) def _add_recent(self, fname): if fname in self.recent: # avoid duplicates self.recent.remove(fname) self.recent.insert(0, fname) while len(self.recent) > self._max_recent: # prune list self.recent.pop() self._write_settings() if not self.recent_menu.isEnabled(): self.recent_menu.setEnabled(True) def _write_settings(self): settings = QSettings() if self.recent: settings.setValue("recent", self.recent) settings.setValue("statusbar", not self.statusBar().isHidden()) def _read_settings(self): settings = QSettings() recent = settings.value("recent") statusbar = settings.value("statusbar") if (statusbar is None) or (statusbar == "true"): statusbar = True else: statusbar = False return {"recent": recent, "statusbar": statusbar} @pyqtSlot(QModelIndex) def _update_data(self, selected): """Update index and information based on the state of the sidebar. """ if selected.row() != self.datasets.index: self.datasets.index = selected.row() self.datasets.update_current() self._update_main() @pyqtSlot() def _update_recent_menu(self): self.recent_menu.clear() for recent in self.recent: self.recent_menu.addAction(recent) @pyqtSlot(QAction) def _load_recent(self, action): self.load_file(action.text()) @pyqtSlot() def _toggle_statusbar(self): if self.statusBar().isHidden(): self.statusBar().show() else: self.statusBar().hide() self._write_settings() def closeEvent(self, event): print("\nCommand History") print("===============") print("\n".join(self.history)) event.accept()
class RequestsOperations(QWidget): def __init__(self, parent): super().__init__(parent) self.initUI() self.__ops = [] def initUI(self): self.layout = QFormLayout() self.layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow) self.layout.setLabelAlignment(Qt.AlignLeft) self.layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop) twoListsOfSets = QWidget() twoListsOfSets.setLayout(QHBoxLayout()) twoListsOfSets.layout().setContentsMargins(5, 10, 5, 5) twoListsOfSets.layout().setSpacing(0) effect = QGraphicsDropShadowEffect() effect.setBlurRadius(10) effect.setColor(QColor(0, 0, 0, 160)) effect.setOffset(0.0) self.requestList = QListView() self.requestList.setSpacing(3) self.requestList.setAutoFillBackground(True) self.requestList.setGraphicsEffect(effect) self.requestList.setFrameStyle(QFrame.NoFrame) self.requestList.viewport().setAutoFillBackground(False) self.requestList.setFlow(QListView.LeftToRight) self.requestList.setWrapping(True) self.requestList.setResizeMode(QListView.Adjust) self.requestList.setUniformItemSizes(True) self.requestsModel = QStandardItemModel() self.requestList.setModel(self.requestsModel) effect = QGraphicsDropShadowEffect() effect.setBlurRadius(10) effect.setColor(QColor(0, 0, 0, 160)) effect.setOffset(0.0) self.requestList2 = QListView() self.requestList2.setSpacing(3) self.requestList2.setAutoFillBackground(True) self.requestList2.setGraphicsEffect(effect) self.requestList2.setFrameStyle(QFrame.NoFrame) self.requestList2.viewport().setAutoFillBackground(False) self.requestList2.setFlow(QListView.LeftToRight) self.requestList2.setWrapping(True) self.requestList2.setResizeMode(QListView.Adjust) self.requestList2.setUniformItemSizes(True) self.requestsModel2 = QStandardItemModel() self.requestList2.setModel(self.requestsModel2) twoListsOfSets.layout().addWidget(self.requestList) twoListsOfSets.layout().addWidget(self.requestList2) self.layout.addRow("SETS", twoListsOfSets) self.layout.addRow(HorizontalLine(self)) self.operationSelection = QGroupBox() self.operationSelection.setFlat(True) self.operationSelection.setLayout(QVBoxLayout()) self.buttonIntersection = QRadioButton("Intersection") self.operationSelection.layout().addWidget(self.buttonIntersection) self.buttonIntersection.clicked.connect( self.__disableSecondRequestList) self.buttonIntersection.click() self.buttonUnion = QRadioButton("Union") self.operationSelection.layout().addWidget(self.buttonUnion) self.buttonUnion.clicked.connect(self.__disableSecondRequestList) self.buttonDiff = QRadioButton("Difference") self.operationSelection.layout().addWidget(self.buttonDiff) self.buttonDiff.clicked.connect(self.__enableSecondRequestList) self.layout.addRow("OPERATION", self.operationSelection) self.buttonApplyWidget = QWidget() self.buttonApplyWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.buttonApplyLayout = QHBoxLayout() self.buttonApplyLayout.setContentsMargins(0, 0, 0, 0) self.buttonApplyWidget.setLayout(self.buttonApplyLayout) self.buttonApply = QPushButton("Apply") self.buttonApply.clicked.connect(self.__applyOp) self.operationSelection.layout().addWidget(self.buttonApply) self.buttonApplyLayout.addWidget(self.buttonApply, alignment=Qt.AlignRight) self.layout.addRow("", self.buttonApplyWidget) self.layout.addRow(HorizontalLine(self)) self.layout.addRow("RESULTS", None) self.resultingSets = QTableView() self.resultingSets.horizontalHeader().setSectionResizeMode( QHeaderView.Stretch) self.resultingSets.verticalHeader().setSectionResizeMode( QHeaderView.ResizeToContents) self.resultingSets.setModel(OperationsTableModel()) self.layout.addRow(self.resultingSets) self.layout.addRow(HorizontalLine(self)) self.outputSetSelection = QComboBox() self.outputSetSelection.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed) self.layout.addRow("OUTPUT SET", self.outputSetSelection) self.setLayout(self.layout) def outputSet(self): return self.outputSetSelection.currentText() def setOutputSet(self, outputSetName): self.outputSetSelection.setCurrentText(outputSetName) @property def ops(self): return copy.deepcopy(self.__ops) def __applyOp(self): includedSets = [ self.requestsModel.item(i).text() for i in range(self.requestsModel.rowCount()) if self.requestsModel.item(i).data(Qt.CheckStateRole) == QVariant( Qt.Checked) ] if self.buttonUnion.isChecked(): if len(includedSets) > 1: opName = SetNameManagement.getUniqueSetName() self.addOp(OverpassUnion(opName), includedSets) logging.info("Union created.") else: logging.error("The union must have at least two sets.") elif self.buttonIntersection.isChecked(): if len(includedSets) > 1: opName = SetNameManagement.getUniqueSetName() self.addOp(OverpassIntersection(opName), includedSets) logging.info("Intersection created.") else: logging.error("The intersection must have at least two sets.") elif self.buttonDiff.isChecked(): excludedSets = [ self.requestsModel2.item(i).text() for i in range(self.requestsModel2.rowCount()) if self.requestsModel2.item(i).data(Qt.CheckStateRole) == QVariant(Qt.Checked) ] if len(includedSets) == 1 and len(excludedSets) > 0: opName = SetNameManagement.getUniqueSetName() self.addOp(OverpassDiff(includedSets[0], opName), excludedSets) logging.info("Difference created.") else: logging.error( "The difference must have only one set selected in the first list and at least one in the other." ) logging.debug("LINE") def addOp(self, op, sets=None): SetNameManagement.assign(op.name) self.__ops.append(op) if sets is not None: op.addSets(sets) self.resultingSets.model().addOp(op.name, op) self.addRequest(op.name) self.cleanRequestList() def __enableSecondRequestList(self): self.requestList2.show() def __disableSecondRequestList(self): self.requestList2.hide() def addRequest(self, name): self.requestsModel.beginInsertRows(QModelIndex(), self.requestsModel.rowCount(), self.requestsModel.rowCount()) item = QStandardItem(name) item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) item.setData(QVariant(Qt.Unchecked), Qt.CheckStateRole) self.requestsModel.appendRow(item) self.requestsModel.endInsertRows() self.requestsModel2.beginInsertRows(QModelIndex(), self.requestsModel2.rowCount(), self.requestsModel2.rowCount()) item = QStandardItem(name) item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) item.setData(QVariant(Qt.Unchecked), Qt.CheckStateRole) self.requestsModel2.appendRow(item) self.requestsModel2.endInsertRows() self.outputSetSelection.addItem(name) def removeSetAndDependencies(self, setName): removeList = [setName] for set in removeList: logging.info("Removing set '{}'.".format(set)) removeList.extend( [i for i in self.__removeSet(set) if i not in removeList]) logging.debug("LINE") def __removeSet(self, setName): dependencies = [] for op in self.__ops: op.removeSet(setName) if not op.isValid(): dependencies.append(op.name) for i in range(self.requestsModel.rowCount()): if self.requestsModel.item(i).text() == setName: self.requestsModel.beginRemoveRows(QModelIndex(), i, i) self.requestsModel.removeRow(i) self.requestsModel.endInsertRows() self.requestsModel2.beginRemoveRows(QModelIndex(), i, i) self.requestsModel2.removeRow(i) self.requestsModel2.endInsertRows() self.outputSetSelection.removeItem(i) break for op in self.__ops: if op.name == setName: self.resultingSets.model().removeOp(setName) self.__ops.remove(op) break SetNameManagement.releaseName(setName) return dependencies def reset(self): while len(self.ops) > 0: self.removeSetAndDependencies(self.ops[0].name) def cleanRequestList(self): for i in range(self.requestsModel.rowCount()): self.requestsModel.item(i).setData(QVariant(Qt.Unchecked), Qt.CheckStateRole) self.requestsModel2.item(i).setData(QVariant(Qt.Unchecked), Qt.CheckStateRole) def keyPressEvent(self, event): if event.key() == Qt.Key_Backspace and self.resultingSets.hasFocus(): advice = "Are you sure?\nAll sets containing this one will be deleted if they are no longer valid" reply = QMessageBox.question(self, "Remove request operation", advice) if reply == QMessageBox.Yes: select = self.resultingSets.selectionModel() while len(select.selectedRows()) > 0: self.removeSetAndDependencies( self.resultingSets.model().getOpByIndex( select.selectedRows()[0].row())) event.accept()
class DisambiguationWidget(QWidget): def __init__(self, getRequestFunction, setFiltersFunction, parent=None): super().__init__(parent) self.getRequestFunction = getRequestFunction self.setFiltersFunction = setFiltersFunction # LAYOUT self.layout = QFormLayout() self.layout.setContentsMargins(10, 10, 10, 10) self.layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow) self.layout.setLabelAlignment(Qt.AlignLeft) self.layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop) # TYPE self.onlyDisconnectedCB = QCheckBox() self.onlyDisconnectedCB.setText("Only disconnected ways") self.columnSelection = QListView() self.columnSelection.setSpacing(3) self.columnSelection.setAutoFillBackground(True) self.columnSelection.setFrameStyle(QFrame.NoFrame) self.columnSelection.viewport().setAutoFillBackground(False) self.columnSelection.setFlow(QListView.LeftToRight) self.columnSelection.setWrapping(True) self.columnSelection.setResizeMode(QListView.Adjust) self.columnSelectionModel = QStandardItemModel() self.columnSelection.setModel(self.columnSelectionModel) self.applyButton = QPushButton("Apply") self.applyButton.clicked.connect(self.showTable) self.tableView = QTableView() self.tableView.doubleClicked.connect( lambda signal: self.setFiltersFunction( self.setSelection.currentText(), self.tableView.model().getDictDataFromCell(signal))) horizontalHeader = self.tableView.horizontalHeader() horizontalHeader.setSectionResizeMode(QHeaderView.ResizeToContents) horizontalHeader.setStretchLastSection(True) verticalHeader = self.tableView.verticalHeader() verticalHeader.sectionDoubleClicked.connect( lambda i: self.setFiltersFunction( self.setSelection.currentText(), self.tableView.model().getDictData(i))) self.tableView.setMinimumHeight(300) self.tableButtons = QWidget() tableButtonsLayout = QHBoxLayout() tableButtonsLayout.setAlignment(Qt.AlignRight) self.tableButtons.setLayout(tableButtonsLayout) tableButtonsLayout.setSpacing(0) tableButtonsLayout.setContentsMargins(0, 0, 0, 0) buttonMore = IconButton( QIcon(os.path.join(picturesDir, "showMore.png")), self.tableButtons.windowHandle(), self.tableButtons.height()) buttonMore.setToolTip("Show more") buttonMore.setFlat(True) buttonMore.clicked.connect(self.showMore) tableButtonsLayout.addWidget(buttonMore) buttonLess = IconButton( QIcon(os.path.join(picturesDir, "showLess.png")), self.tableButtons.windowHandle(), self.tableButtons.height()) buttonLess.setToolTip("Show less") buttonLess.setFlat(True) buttonLess.clicked.connect(self.showLess) tableButtonsLayout.addWidget(buttonLess) self.setSelection = QComboBox() self.setSelection.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) self.layout.addRow("SET", self.setSelection) self.layout.addRow("TYPE", self.onlyDisconnectedCB) self.layout.addRow("KEYS", self.columnSelection) self.layout.addRow(self.applyButton) self.layout.addRow(self.tableView) self.layout.addRow(self.tableButtons) self.setLayout(self.layout) def showMore(self): if self.tableView.model() is not None: self.tableView.model().showMore() def showLess(self): if self.tableView.model() is not None: self.tableView.model().showLess() def addFilterFromCell(self, signal): key = self.tableView.model().headerData(signal.column(), Qt.Horizontal, Qt.DisplayRole) value = self.tableView.model().itemData(signal).get(0) return self.setFiltersFunction( self.setSelection.currentText(), ([OverpassFilter(key, TagComparison.EQUAL, value, False, True) ], [])) def showTable(self): request = self.getRequestFunction(self.setSelection.currentText()) if request is not None: query = OverpassQuery(request.name) query.addRequest(request) try: writeXMLResponse(query.getQL(), tableDir) except OverpassRequestException as e: logging.error(str(e)) return except OSError: logging.error( "There was a problem creating the file with the request response." ) return except RuntimeError as e: logging.error(str(e)) return except Exception: logging.error(traceback.format_exc()) return jsonResponse = ox.overpass_json_from_file(tableDir) if len(jsonResponse["elements"]) == 0: logging.warning("There are no elements to show in the table.") logging.debug("LINE") else: self.disconnectedWaysTable = DisconnectedWaysTable( jsonResponse) self.similarWaysTable = SimilarWaysTable(jsonResponse) self.showHideOnlyDisconnected() self.columnSelectionModel.clear() for key in self.similarWaysTable.getAllColumns(): self.columnSelectionModel.beginInsertRows( QModelIndex(), self.columnSelectionModel.rowCount(), self.columnSelectionModel.rowCount()) item = QStandardItem(key) self.columnSelectionModel.itemChanged.connect( self.updateColumns) item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) item.setData( QVariant(Qt.Checked if key in self.similarWaysTable. getSelectedColumns() else Qt.Unchecked), Qt.CheckStateRole) self.columnSelectionModel.appendRow(item) self.columnSelectionModel.endInsertRows() self.disconnectedWaysTable.updateColumns( self.similarWaysTable.getSelectedColumns()) self.onlyDisconnectedCB.stateChanged.connect( self.showHideOnlyDisconnected) logging.info("Showing table.") logging.debug("LINE") else: logging.warning( "There is no requests. It is not possible to show the table.") def updateColumns(self): self.disconnectedWaysTable.updateColumns(self.getSelectedKeys()) self.similarWaysTable.updateColumns(self.getSelectedKeys()) def getSelectedKeys(self): return [ self.columnSelectionModel.item(i).text() for i in range(self.columnSelectionModel.rowCount()) if self.columnSelectionModel.item(i).data(Qt.CheckStateRole) == QVariant(Qt.Checked) ] def getSelectedRowNetworkx(self): if self.tableView.model() is None: logging.warning("The table has not been created yet.") logging.debug("LINE") else: indexes = self.tableView.selectionModel().selectedRows() return self.tableView.model().getRowJson(indexes) def getHtmlFromSelectedRow(self): selectedRows = self.getSelectedRowNetworkx() if selectedRows: return buildHTMLWithNetworkx(selectedRows) else: raise RuntimeError("No row is selected") def showHideOnlyDisconnected(self): if self.onlyDisconnectedCB.isChecked(): self.tableView.setModel(self.disconnectedWaysTable) else: self.tableView.setModel(self.similarWaysTable) def showTableSelection(self): try: self.changePage( buildHTMLWithNetworkx(self.getSelectedRowNetworkx())) except (OverpassRequestException, OsmnxException) as e: logging.error(str(e)) logging.warning( "Before open NETEDIT you must run a query with the row filters applied." ) except ox.EmptyOverpassResponse: logging.error("There are no elements with the given row.") except OSError: logging.error( "There was a problem creating the file with the row selection." ) except Exception: logging.error(traceback.format_exc()) logging.debug("LINE") def addSet(self, setName): self.applyButton.setEnabled(True) self.setSelection.addItem(setName) def removeSet(self, setName): for i in range(self.setSelection.count()): if self.setSelection.itemText(i) == setName: self.setSelection.removeItem(i) break
class MainWindow(QMainWindow): """MNELAB main window.""" def __init__(self, model): """Initialize MNELAB main window. Parameters ---------- model : mnelab.model.Model instance The main window needs to connect to a model containing all data sets. This decouples the GUI from the data (model/view). """ super().__init__() self.model = model # data model self.setWindowTitle("MNELAB") # restore settings settings = read_settings() self.recent = settings["recent"] # list of recent files if settings["geometry"]: self.restoreGeometry(settings["geometry"]) else: self.setGeometry(300, 300, 1000, 750) # default window size self.move(QApplication.desktop().screen().rect().center() - self.rect().center()) # center window if settings["state"]: self.restoreState(settings["state"]) # initialize menus file_menu = self.menuBar().addMenu("&File") file_menu.addAction( "&Open...", lambda: self.open_file(model.load, "Open raw", SUPPORTED_FORMATS), QKeySequence.Open) self.recent_menu = file_menu.addMenu("Open recent") self.recent_menu.aboutToShow.connect(self._update_recent_menu) self.recent_menu.triggered.connect(self._load_recent) if not self.recent: self.recent_menu.setEnabled(False) self.close_file_action = file_menu.addAction("&Close", self.model.remove_data, QKeySequence.Close) self.close_all_action = file_menu.addAction("Close all", self.close_all) file_menu.addSeparator() self.import_bad_action = file_menu.addAction( "Import bad channels...", lambda: self.import_file( model.import_bads, "Import bad channels", "*.csv")) self.import_events_action = file_menu.addAction( "Import events...", lambda: self.import_file( model.import_events, "Import events", "*.csv")) self.import_anno_action = file_menu.addAction( "Import annotations...", lambda: self.import_file( model.import_annotations, "Import annotations", "*.csv")) self.import_ica_action = file_menu.addAction( "Import &ICA...", lambda: self.open_file( model.import_ica, "Import ICA", "*.fif *.fif.gz")) file_menu.addSeparator() self.export_raw_action = file_menu.addAction( "Export &raw...", lambda: self.export_file(model.export_raw, "Export raw", "*.fif")) self.export_bad_action = file_menu.addAction( "Export &bad channels...", lambda: self.export_file( model.export_bads, "Export bad channels", "*.csv")) self.export_events_action = file_menu.addAction( "Export &events...", lambda: self.export_file( model.export_events, "Export events", "*.csv")) self.export_anno_action = file_menu.addAction( "Export &annotations...", lambda: self.export_file( model.export_annotations, "Export annotations", "*.csv")) self.export_ica_action = file_menu.addAction( "Export ICA...", lambda: self.export_file( model.export_ica, "Export ICA", "*.fif *.fif.gz")) file_menu.addSeparator() file_menu.addAction("&Quit", self.close, QKeySequence.Quit) edit_menu = self.menuBar().addMenu("&Edit") self.pick_chans_action = edit_menu.addAction("Pick &channels...", self.pick_channels) self.chan_props_action = edit_menu.addAction("Channel &properties...", self.channel_properties) self.set_montage_action = edit_menu.addAction("Set &montage...", self.set_montage) edit_menu.addSeparator() self.setref_action = edit_menu.addAction("&Set reference...", self.set_reference) edit_menu.addSeparator() self.events_action = edit_menu.addAction("Events...", self.edit_events) plot_menu = self.menuBar().addMenu("&Plot") self.plot_raw_action = plot_menu.addAction("&Raw data", self.plot_raw) self.plot_psd_action = plot_menu.addAction( "&Power spectral " "density...", self.plot_psd) self.plot_montage_action = plot_menu.addAction("Current &montage", self.plot_montage) plot_menu.addSeparator() self.plot_ica_components_action = plot_menu.addAction( "ICA components...", self.plot_ica_components) tools_menu = self.menuBar().addMenu("&Tools") self.filter_action = tools_menu.addAction("&Filter data...", self.filter_data) self.find_events_action = tools_menu.addAction("Find &events...", self.find_events) self.run_ica_action = tools_menu.addAction("Run &ICA...", self.run_ica) view_menu = self.menuBar().addMenu("&View") statusbar_action = view_menu.addAction("Statusbar", self._toggle_statusbar) statusbar_action.setCheckable(True) help_menu = self.menuBar().addMenu("&Help") help_menu.addAction("&About", self.show_about) help_menu.addAction("About &Qt", self.show_about_qt) # set up data model for sidebar (list of open files) self.names = QStringListModel() self.names.dataChanged.connect(self._update_names) splitter = QSplitter() self.sidebar = QListView() self.sidebar.setFrameStyle(QFrame.NoFrame) self.sidebar.setFocusPolicy(Qt.NoFocus) self.sidebar.setModel(self.names) self.sidebar.clicked.connect(self._update_data) splitter.addWidget(self.sidebar) self.infowidget = InfoWidget() splitter.addWidget(self.infowidget) width = splitter.size().width() splitter.setSizes((width * 0.3, width * 0.7)) self.setCentralWidget(splitter) self.status_label = QLabel() self.statusBar().addPermanentWidget(self.status_label) if settings["statusbar"]: self.statusBar().show() statusbar_action.setChecked(True) else: self.statusBar().hide() statusbar_action.setChecked(False) self.setAcceptDrops(True) self.data_changed() def data_changed(self): # update sidebar self.names.setStringList(self.model.names) self.sidebar.setCurrentIndex(self.names.index(self.model.index)) # update info widget if self.model.data: self.infowidget.set_values(self.model.get_info()) else: self.infowidget.clear() # update status bar if self.model.data: mb = self.model.nbytes / 1024**2 self.status_label.setText("Total Memory: {:.2f} MB".format(mb)) else: self.status_label.clear() # toggle actions if len(self.model) == 0: # disable if no data sets are currently open enabled = False else: enabled = True self.close_file_action.setEnabled(enabled) self.close_all_action.setEnabled(enabled) self.export_raw_action.setEnabled(enabled) if self.model.data: bads = bool(self.model.current["raw"].info["bads"]) self.export_bad_action.setEnabled(enabled and bads) events = self.model.current["events"] is not None self.export_events_action.setEnabled(enabled and events) annot = self.model.current["raw"].annotations is not None self.export_anno_action.setEnabled(enabled and annot) montage = bool(self.model.current["montage"]) self.plot_montage_action.setEnabled(enabled and montage) ica = bool(self.model.current["ica"]) self.export_ica_action.setEnabled(enabled and ica) self.plot_ica_components_action.setEnabled(enabled and ica and montage) self.events_action.setEnabled(enabled and events) else: self.export_bad_action.setEnabled(enabled) self.export_events_action.setEnabled(enabled) self.export_anno_action.setEnabled(enabled) self.plot_montage_action.setEnabled(enabled) self.export_ica_action.setEnabled(enabled) self.plot_ica_components_action.setEnabled(enabled) self.events_action.setEnabled(enabled) self.import_bad_action.setEnabled(enabled) self.import_events_action.setEnabled(enabled) self.import_anno_action.setEnabled(enabled) self.pick_chans_action.setEnabled(enabled) self.chan_props_action.setEnabled(enabled) self.set_montage_action.setEnabled(enabled) self.plot_raw_action.setEnabled(enabled) self.plot_psd_action.setEnabled(enabled) self.filter_action.setEnabled(enabled) self.setref_action.setEnabled(enabled) self.find_events_action.setEnabled(enabled) self.run_ica_action.setEnabled(enabled) self.import_ica_action.setEnabled(enabled) # add to recent files if len(self.model) > 0: self._add_recent(self.model.current["fname"]) def open_file(self, f, text, ffilter): """Open file.""" fname = QFileDialog.getOpenFileName(self, text, filter=ffilter)[0] if fname: f(fname) def export_file(self, f, text, ffilter): """Export to file.""" fname = QFileDialog.getSaveFileName(self, text, filter=ffilter)[0] if fname: f(fname) def import_file(self, f, text, ffilter): """Import file.""" fname = QFileDialog.getOpenFileName(self, text, filter=ffilter)[0] if fname: try: f(fname) except LabelsNotFoundError as e: QMessageBox.critical(self, "Channel labels not found", str(e)) except InvalidAnnotationsError as e: QMessageBox.critical(self, "Invalid annotations", str(e)) def close_all(self): """Close all currently open data sets.""" msg = QMessageBox.question(self, "Close all data sets", "Close all data sets?") if msg == QMessageBox.Yes: while len(self.model) > 0: self.model.remove_data() def pick_channels(self): """Pick channels in current data set.""" channels = self.model.current["raw"].info["ch_names"] dialog = PickChannelsDialog(self, channels, selected=channels) if dialog.exec_(): picks = [item.data(0) for item in dialog.channels.selectedItems()] drops = set(channels) - set(picks) if drops: self.auto_duplicate() self.model.drop_channels(drops) self.model.history.append(f"raw.drop({drops})") def channel_properties(self): """Show channel properties dialog.""" info = self.model.current["raw"].info dialog = ChannelPropertiesDialog(self, info) if dialog.exec_(): dialog.model.sort(0) bads = [] renamed = {} types = {} for i in range(dialog.model.rowCount()): new_label = dialog.model.item(i, 1).data(Qt.DisplayRole) old_label = info["ch_names"][i] if new_label != old_label: renamed[old_label] = new_label new_type = dialog.model.item(i, 2).data(Qt.DisplayRole).lower() old_type = channel_type(info, i).lower() if new_type != old_type: types[new_label] = new_type if dialog.model.item(i, 3).checkState() == Qt.Checked: bads.append(info["ch_names"][i]) self.model.set_channel_properties(bads, renamed, types) def set_montage(self): """Set montage.""" montages = mne.channels.get_builtin_montages() # TODO: currently it is not possible to remove an existing montage dialog = MontageDialog(self, montages, selected=self.model.current["montage"]) if dialog.exec_(): name = dialog.montages.selectedItems()[0].data(0) montage = mne.channels.read_montage(name) ch_names = self.model.current["raw"].info["ch_names"] # check if at least one channel name matches a name in the montage if set(ch_names) & set(montage.ch_names): self.model.set_montage(name) else: QMessageBox.critical( self, "No matching channel names", "Channel names defined in the montage do " "not match any channel name in the data.") def edit_events(self): pos = self.model.current["events"][:, 0].tolist() desc = self.model.current["events"][:, 2].tolist() dialog = EventsDialog(self, pos, desc) if dialog.exec_(): pass def plot_raw(self): """Plot raw data.""" events = self.model.current["events"] nchan = self.model.current["raw"].info["nchan"] fig = self.model.current["raw"].plot(events=events, n_channels=nchan, title=self.model.current["name"], show=False) self.model.history.append("raw.plot(n_channels={})".format(nchan)) win = fig.canvas.manager.window win.setWindowTitle("Raw data") win.findChild(QStatusBar).hide() win.installEventFilter(self) # detect if the figure is closed # prevent closing the window with the escape key try: key_events = fig.canvas.callbacks.callbacks["key_press_event"][8] except KeyError: pass else: # this requires MNE >=0.15 key_events.func.keywords["params"]["close_key"] = None fig.show() def plot_psd(self): """Plot power spectral density (PSD).""" fig = self.model.current["raw"].plot_psd(average=False, spatial_colors=False, show=False) win = fig.canvas.manager.window win.setWindowTitle("Power spectral density") fig.show() def plot_montage(self): """Plot current montage.""" fig = self.model.current["raw"].plot_sensors(show_names=True, show=False) win = fig.canvas.manager.window win.setWindowTitle("Montage") win.findChild(QStatusBar).hide() win.findChild(QToolBar).hide() fig.show() def plot_ica_components(self): self.model.current["ica"].plot_components() def run_ica(self): """Run ICA calculation.""" try: import picard except ImportError: have_picard = False else: have_picard = True dialog = RunICADialog(self, self.model.current["raw"].info["nchan"], have_picard) if dialog.exec_(): calc = CalcDialog(self, "Calculating ICA", "Calculating ICA.") method = dialog.method.currentText() exclude_bad_segments = dialog.exclude_bad_segments.isChecked() ica = mne.preprocessing.ICA(method=dialog.methods[method]) pool = mp.Pool(1) kwds = {"reject_by_annotation": exclude_bad_segments} res = pool.apply_async(func=ica.fit, args=(self.model.current["raw"], ), kwds=kwds, callback=lambda x: calc.accept()) if not calc.exec_(): pool.terminate() else: self.model.current["ica"] = res.get(timeout=1) self.data_changed() def filter_data(self): """Filter data.""" dialog = FilterDialog(self) if dialog.exec_(): self.auto_duplicate() self.model.filter(dialog.low, dialog.high) def find_events(self): dialog = FindEventsDialog(self) if dialog.exec_(): consecutive = dialog.consecutive.isChecked() initial_event = dialog.initial_event.isChecked() uint_cast = dialog.uint_cast.isChecked() min_dur = dialog.min_dur shortest_event = dialog.shortest_event self.model.find_events(consecutive=consecutive, initial_event=initial_event, uint_cast=uint_cast, min_duration=min_dur, shortest_event=shortest_event) def set_reference(self): """Set reference.""" dialog = ReferenceDialog(self) if dialog.exec_(): self.auto_duplicate() if dialog.average.isChecked(): self.model.set_reference("average") else: ref = [c.strip() for c in dialog.channellist.text().split(",")] self.model.set_reference(ref) def show_about(self): """Show About dialog.""" msg = f"""<p style="font-weight: bold">MNELAB {__version__}</p> <p style="font-weight: normal"> <a href="https://github.com/cbrnr/mnelab">MNELAB</a> - a graphical user interface for <a href="https://github.com/mne-tools/mne-python">MNE</a>.</p> <p style="font-weight: normal"> This program uses MNE version {mne.__version__}.</p> <p style="font-weight: normal"> Licensed under the BSD 3-clause license.</p> <p style="font-weight: normal"> Copyright 2017-2018 by Clemens Brunner.</p>""" QMessageBox.about(self, "About MNELAB", msg) def show_about_qt(self): """Show About Qt dialog.""" QMessageBox.aboutQt(self, "About Qt") def auto_duplicate(self): # if current data is stored in a file create a new data set if self.model.current["fname"]: self.model.duplicate_data() # otherwise ask the user else: msg = QMessageBox.question(self, "Overwrite existing data set", "Overwrite existing data set?") if msg == QMessageBox.No: # create new data set self.model.duplicate_data() def _add_recent(self, fname): """Add a file to recent file list. Parameters ---------- fname : str File name. """ if fname in self.recent: # avoid duplicates self.recent.remove(fname) self.recent.insert(0, fname) while len(self.recent) > MAX_RECENT: # prune list self.recent.pop() write_settings(recent=self.recent) if not self.recent_menu.isEnabled(): self.recent_menu.setEnabled(True) def _remove_recent(self, fname): """Remove file from recent file list. Parameters ---------- fname : str File name. """ if fname in self.recent: self.recent.remove(fname) write_settings(recent=self.recent) if not self.recent: self.recent_menu.setEnabled(False) @pyqtSlot(QModelIndex) def _update_data(self, selected): """Update index and information based on the state of the sidebar. Parameters ---------- selected : QModelIndex Index of the selected row. """ if selected.row() != self.model.index: self.model.index = selected.row() self.data_changed() @pyqtSlot(QModelIndex, QModelIndex) def _update_names(self, start, stop): """Update names in DataSets after changes in sidebar.""" for index in range(start.row(), stop.row() + 1): self.model.data[index]["name"] = self.names.stringList()[index] @pyqtSlot() def _update_recent_menu(self): self.recent_menu.clear() for recent in self.recent: self.recent_menu.addAction(recent) @pyqtSlot(QAction) def _load_recent(self, action): self.model.load(action.text()) @pyqtSlot() def _toggle_statusbar(self): if self.statusBar().isHidden(): self.statusBar().show() else: self.statusBar().hide() write_settings(statusbar=not self.statusBar().isHidden()) @pyqtSlot(QDropEvent) def dragEnterEvent(self, event): if event.mimeData().hasUrls(): event.acceptProposedAction() @pyqtSlot(QDropEvent) def dropEvent(self, event): mime = event.mimeData() if mime.hasUrls(): urls = mime.urls() for url in urls: self.load_file(url.toLocalFile()) @pyqtSlot(QEvent) def closeEvent(self, event): """Close application. Parameters ---------- event : QEvent Close event. """ write_settings(geometry=self.saveGeometry(), state=self.saveState()) if self.model.history: print("\nCommand History") print("===============") print("\n".join(self.model.history)) QApplication.quit() def eventFilter(self, source, event): # currently the only source is the raw plot window if event.type() == QEvent.Close: self.data_changed() return QObject.eventFilter(self, source, event)
class MainWindow(QMainWindow): """MNELAB main window. """ def __init__(self): super().__init__() self.MAX_RECENT = 6 # maximum number of recent files self.SUPPORTED_FORMATS = "*.bdf *.edf" self.all = DataSets() # contains currently loaded data sets self.history = [] # command history settings = self._read_settings() self.recent = settings["recent"] # list of recent files if settings["geometry"]: self.restoreGeometry(settings["geometry"]) else: self.setGeometry(300, 300, 1000, 750) # default window size self.move(QApplication.desktop().screen().rect().center() - self.rect().center()) # center window if settings["state"]: self.restoreState(settings["state"]) self.setWindowTitle("MNELAB") menubar = self.menuBar() file_menu = menubar.addMenu("&File") file_menu.addAction("&Open...", self.open_file, QKeySequence.Open) self.recent_menu = file_menu.addMenu("Open recent") self.recent_menu.aboutToShow.connect(self._update_recent_menu) self.recent_menu.triggered.connect(self._load_recent) if not self.recent: self.recent_menu.setEnabled(False) self.close_file_action = file_menu.addAction("&Close", self.close_file, QKeySequence.Close) self.close_all_action = file_menu.addAction("Close all", self.close_all) file_menu.addSeparator() self.import_bad_action = file_menu.addAction("Import bad channels...", self.import_bads) self.export_bad_action = file_menu.addAction("Export &bad channels...", self.export_bads) file_menu.addSeparator() file_menu.addAction("&Quit", self.close, QKeySequence.Quit) edit_menu = menubar.addMenu("&Edit") self.pick_chans_action = edit_menu.addAction("Pick &channels...", self.pick_channels) self.set_bads_action = edit_menu.addAction("&Bad channels...", self.set_bads) edit_menu.addSeparator() self.setref_action = edit_menu.addAction("&Set reference...", self.set_reference) plot_menu = menubar.addMenu("&Plot") self.plot_raw_action = plot_menu.addAction("&Raw data", self.plot_raw) self.plot_psd_action = plot_menu.addAction("&Power spectral " "density...", self.plot_psd) tools_menu = menubar.addMenu("&Tools") self.filter_action = tools_menu.addAction("&Filter data...", self.filter_data) self.find_events_action = tools_menu.addAction("Find &events...", self.find_events) self.run_ica_action = tools_menu.addAction("Run &ICA...") self.import_ica_action = tools_menu.addAction("&Load ICA...", self.load_ica) view_menu = menubar.addMenu("&View") statusbar_action = view_menu.addAction("Statusbar", self._toggle_statusbar) statusbar_action.setCheckable(True) help_menu = menubar.addMenu("&Help") help_menu.addAction("&About", self.show_about) help_menu.addAction("About &Qt", self.show_about_qt) self.names = QStringListModel() self.names.dataChanged.connect(self._update_names) splitter = QSplitter() self.sidebar = QListView() self.sidebar.setFrameStyle(QFrame.NoFrame) self.sidebar.setFocusPolicy(Qt.NoFocus) self.sidebar.setModel(self.names) self.sidebar.clicked.connect(self._update_data) splitter.addWidget(self.sidebar) self.infowidget = InfoWidget() splitter.addWidget(self.infowidget) width = splitter.size().width() splitter.setSizes((width * 0.3, width * 0.7)) self.setCentralWidget(splitter) self.status_label = QLabel() self.statusBar().addPermanentWidget(self.status_label) if settings["statusbar"]: self.statusBar().show() statusbar_action.setChecked(True) else: self.statusBar().hide() statusbar_action.setChecked(False) self.setAcceptDrops(True) self._toggle_actions(False) self.show() def open_file(self): """Show open file dialog. """ fname = QFileDialog.getOpenFileName(self, "Open file", filter=self.SUPPORTED_FORMATS)[0] if fname: self.load_file(fname) def load_file(self, fname): """Load file. Parameters ---------- fname : str File name. """ # TODO: check if fname exists raw = mne.io.read_raw_edf(fname, stim_channel=-1, preload=True) name, ext = splitext(split(fname)[-1]) self.history.append("raw = mne.io.read_raw_edf('{}', " "stim_channel=None, preload=True)".format(fname)) self.all.insert_data(DataSet(name=name, fname=fname, ftype=ext[1:].upper(), raw=raw)) self.find_events() self._update_sidebar(self.all.names, self.all.index) self._update_infowidget() self._update_statusbar() self._add_recent(fname) self._toggle_actions() def export_bads(self): """Export bad channels info to a CSV file. """ fname = QFileDialog.getSaveFileName(self, "Export bad channels", filter="*.csv")[0] if fname: name, ext = splitext(split(fname)[-1]) ext = ext if ext else ".csv" # automatically add extension fname = join(split(fname)[0], name + ext) with open(fname, "w") as f: f.write(",".join(self.all.current.raw.info["bads"])) def import_bads(self): """Import bad channels info from a CSV file. """ fname = QFileDialog.getOpenFileName(self, "Import bad channels", filter="*.csv")[0] if fname: with open(fname) as f: bads = f.read().replace(" ", "").split(",") if set(bads) - set(self.all.current.raw.info["ch_names"]): QMessageBox.critical(self, "Channel labels not found", "Some channel labels from the file " "are not present in the data.") else: self.all.current.raw.info["bads"] = bads self.all.data[self.all.index].raw.info["bads"] = bads def close_file(self): """Close current file. """ self.all.remove_data() self._update_sidebar(self.all.names, self.all.index) self._update_infowidget() self._update_statusbar() if not self.all: self._toggle_actions(False) def close_all(self): """Close all currently open data sets. """ msg = QMessageBox.question(self, "Close all data sets", "Close all data sets?") if msg == QMessageBox.Yes: while self.all: self.close_file() def get_info(self): """Get basic information on current file. Returns ------- info : dict Dictionary with information on current file. """ raw = self.all.current.raw fname = self.all.current.fname ftype = self.all.current.ftype reference = self.all.current.reference events = self.all.current.events nchan = raw.info["nchan"] chans = Counter([channel_type(raw.info, i) for i in range(nchan)]) if events is not None: nevents = events.shape[0] unique = [str(e) for e in set(events[:, 2])] events = "{} ({})".format(nevents, ", ".join(unique)) else: events = "-" if isinstance(reference, list): reference = ",".join(reference) if raw.annotations is not None: annots = len(raw.annotations.description) else: annots = "-" return {"File name": fname if fname else "-", "File type": ftype if ftype else "-", "Number of channels": nchan, "Channels": ", ".join( [" ".join([str(v), k.upper()]) for k, v in chans.items()]), "Samples": raw.n_times, "Sampling frequency": str(raw.info["sfreq"]) + " Hz", "Length": str(raw.n_times / raw.info["sfreq"]) + " s", "Events": events, "Annotations": annots, "Reference": reference if reference else "-", "Size in memory": "{:.2f} MB".format( raw._data.nbytes / 1024 ** 2), "Size on disk": "-" if not fname else "{:.2f} MB".format( getsize(fname) / 1024 ** 2)} def pick_channels(self): """Pick channels in current data set. """ channels = self.all.current.raw.info["ch_names"] dialog = PickChannelsDialog(self, channels) if dialog.exec_(): picks = [item.data(0) for item in dialog.channels.selectedItems()] drops = set(channels) - set(picks) tmp = self.all.current.raw.drop_channels(drops) name = self.all.current.name + " (channels dropped)" new = DataSet(raw=tmp, name=name, events=self.all.current.events) self.history.append("raw.drop({})".format(drops)) self._update_datasets(new) def set_bads(self): """Set bad channels. """ channels = self.all.current.raw.info["ch_names"] selected = self.all.current.raw.info["bads"] dialog = PickChannelsDialog(self, channels, selected, "Bad channels") if dialog.exec_(): bads = [item.data(0) for item in dialog.channels.selectedItems()] self.all.current.raw.info["bads"] = bads self.all.data[self.all.index].raw.info["bads"] = bads self._toggle_actions(True) def plot_raw(self): """Plot raw data. """ events = self.all.current.events nchan = self.all.current.raw.info["nchan"] fig = self.all.current.raw.plot(events=events, n_channels=nchan, title=self.all.current.name, show=False) self.history.append("raw.plot(n_channels={})".format(nchan)) win = fig.canvas.manager.window win.setWindowTitle("Raw data") win.findChild(QStatusBar).hide() win.installEventFilter(self) # detect if the figure is closed # prevent closing the window with the escape key try: key_events = fig.canvas.callbacks.callbacks["key_press_event"][8] except KeyError: pass else: # this requires MNE >=0.15 key_events.func.keywords["params"]["close_key"] = None fig.show() def plot_psd(self): """Plot power spectral density (PSD). """ fig = self.all.current.raw.plot_psd(average=False, spatial_colors=False, show=False) win = fig.canvas.manager.window win.setWindowTitle("Power spectral density") fig.show() def load_ica(self): """Load ICA solution from a file. """ fname = QFileDialog.getOpenFileName(self, "Load ICA", filter="*.fif *.fif.gz") if fname[0]: self.state.ica = mne.preprocessing.read_ica(fname[0]) def find_events(self): events = mne.find_events(self.all.current.raw, consecutive=False) if events.shape[0] > 0: # if events were found self.all.current.events = events self.all.data[self.all.index].events = events self._update_infowidget() def filter_data(self): """Filter data. """ dialog = FilterDialog(self) if dialog.exec_(): low, high = dialog.low, dialog.high tmp = filter_data(self.all.current.raw._data, self.all.current.raw.info["sfreq"], l_freq=low, h_freq=high) name = self.all.current.name + " ({}-{} Hz)".format(low, high) new = DataSet(raw=mne.io.RawArray(tmp, self.all.current.raw.info), name=name, events=self.all.current.events) self.history.append("raw.filter({}, {})".format(low, high)) self._update_datasets(new) def set_reference(self): """Set reference. """ dialog = ReferenceDialog(self) if dialog.exec_(): if dialog.average.isChecked(): tmp, _ = mne.set_eeg_reference(self.all.current.raw, None) tmp.apply_proj() name = self.all.current.name + " (average ref)" new = DataSet(raw=tmp, name=name, reference="average", events=self.all.current.events) else: ref = [c.strip() for c in dialog.channellist.text().split(",")] refstr = ",".join(ref) if set(ref) - set(self.all.current.raw.info["ch_names"]): # add new reference channel(s) to data try: tmp = mne.add_reference_channels(self.all.current.raw, ref) except RuntimeError: QMessageBox.critical(self, "Cannot add new channels", "Cannot add new channels to " "average referenced data.") return else: # re-reference to existing channel(s) tmp, _ = mne.set_eeg_reference(self.all.current.raw, ref) name = self.all.current.name + " (ref {})".format(refstr) new = DataSet(raw=tmp, name=name, reference=refstr, events=self.all.current.events) self._update_datasets(new) def show_about(self): """Show About dialog. """ msg = """<b>MNELAB {}</b><br/><br/> <a href="https://github.com/cbrnr/mnelab">MNELAB</a> - a graphical user interface for <a href="https://github.com/mne-tools/mne-python">MNE</a>.<br/><br/> This program uses MNE version {}.<br/><br/> Licensed under the BSD 3-clause license.<br/> Copyright 2017 by Clemens Brunner.""".format(__version__, mne.__version__) QMessageBox.about(self, "About MNELAB", msg) def show_about_qt(self): """Show About Qt dialog. """ QMessageBox.aboutQt(self, "About Qt") def _update_datasets(self, dataset): # if current data is stored in a file create a new data set if self.all.current.fname: self.all.insert_data(dataset) # otherwise ask if the current data set should be overwritten or if a # new data set should be created else: msg = QMessageBox.question(self, "Overwrite existing data set", "Overwrite existing data set?") if msg == QMessageBox.No: # create new data set self.all.insert_data(dataset) else: # overwrite existing data set self.all.update_data(dataset) self._update_sidebar(self.all.names, self.all.index) self._update_infowidget() self._update_statusbar() def _update_sidebar(self, names, index): """Update (overwrite) sidebar with names and current index. """ self.names.setStringList(names) self.sidebar.setCurrentIndex(self.names.index(index)) def _update_infowidget(self): if self.all: self.infowidget.set_values(self.get_info()) else: self.infowidget.clear() def _update_statusbar(self): if self.all: mb = self.all.nbytes / 1024 ** 2 self.status_label.setText("Total Memory: {:.2f} MB".format(mb)) else: self.status_label.clear() def _toggle_actions(self, enabled=True): """Toggle actions. Parameters ---------- enabled : bool Specifies whether actions are enabled (True) or disabled (False). """ self.close_file_action.setEnabled(enabled) self.close_all_action.setEnabled(enabled) if self.all.data: bads = bool(self.all.current.raw.info["bads"]) self.export_bad_action.setEnabled(enabled and bads) else: self.export_bad_action.setEnabled(enabled) self.import_bad_action.setEnabled(enabled) self.pick_chans_action.setEnabled(enabled) self.set_bads_action.setEnabled(enabled) self.plot_raw_action.setEnabled(enabled) self.plot_psd_action.setEnabled(enabled) self.filter_action.setEnabled(enabled) self.setref_action.setEnabled(enabled) self.find_events_action.setEnabled(enabled) self.run_ica_action.setEnabled(enabled) self.import_ica_action.setEnabled(enabled) def _add_recent(self, fname): """Add a file to recent file list. Parameters ---------- fname : str File name. """ if fname in self.recent: # avoid duplicates self.recent.remove(fname) self.recent.insert(0, fname) while len(self.recent) > self.MAX_RECENT: # prune list self.recent.pop() self._write_settings() if not self.recent_menu.isEnabled(): self.recent_menu.setEnabled(True) def _write_settings(self): """Write application settings. """ settings = QSettings() if self.recent: settings.setValue("recent", self.recent) settings.setValue("statusbar", not self.statusBar().isHidden()) settings.setValue("geometry", self.saveGeometry()) settings.setValue("state", self.saveState()) def _read_settings(self): """Read application settings. Returns ------- settings : dict The restored settings values are returned in a dictionary for further processing. """ settings = QSettings() recent = settings.value("recent") if not recent: recent = [] # default is empty list statusbar = settings.value("statusbar") if statusbar is None: # default is True statusbar = True geometry = settings.value("geometry") state = settings.value("state") return {"recent": recent, "statusbar": statusbar, "geometry": geometry, "state": state} @pyqtSlot(QModelIndex) def _update_data(self, selected): """Update index and information based on the state of the sidebar. Parameters ---------- selected : QModelIndex Index of the selected row. """ if selected.row() != self.all.index: self.all.index = selected.row() self.all.update_current() self._update_infowidget() @pyqtSlot(QModelIndex, QModelIndex) def _update_names(self, start, stop): """Update names in DataSets after changes in sidebar. """ for index in range(start.row(), stop.row() + 1): self.all.data[index].name = self.names.stringList()[index] if self.all.index in range(start.row(), stop.row() + 1): self.all.current.name = self.all.names[self.all.index] @pyqtSlot() def _update_recent_menu(self): self.recent_menu.clear() for recent in self.recent: self.recent_menu.addAction(recent) @pyqtSlot(QAction) def _load_recent(self, action): self.load_file(action.text()) @pyqtSlot() def _toggle_statusbar(self): if self.statusBar().isHidden(): self.statusBar().show() else: self.statusBar().hide() self._write_settings() @pyqtSlot(QDropEvent) def dragEnterEvent(self, event): if event.mimeData().hasUrls(): event.acceptProposedAction() @pyqtSlot(QDropEvent) def dropEvent(self, event): mime = event.mimeData() if mime.hasUrls(): urls = mime.urls() for url in urls: self.load_file(url.toLocalFile()) @pyqtSlot(QEvent) def closeEvent(self, event): """Close application. Parameters ---------- event : QEvent Close event. """ self._write_settings() if self.history: print("\nCommand History") print("===============") print("\n".join(self.history)) QApplication.quit() def eventFilter(self, source, event): # currently the only source is the raw plot window if event.type() == QEvent.Close: self._update_infowidget() return QObject.eventFilter(self, source, event)
class MainWindow(QMainWindow): """MNELAB main window. """ def __init__(self): super().__init__() # restore settings settings = self._read_settings() self.recent = settings["recent"] # list of recent files if settings["geometry"]: self.restoreGeometry(settings["geometry"]) else: self.setGeometry(300, 300, 1000, 750) # default window size self.move(QApplication.desktop().screen().rect().center() - self.rect().center()) # center window if settings["state"]: self.restoreState(settings["state"]) self.setWindowTitle("MNELAB") # initialize menus menubar = self.menuBar() file_menu = menubar.addMenu("&File") file_menu.addAction("&Open...", self.open_file, QKeySequence.Open) self.recent_menu = file_menu.addMenu("Open recent") self.recent_menu.aboutToShow.connect(self._update_recent_menu) self.recent_menu.triggered.connect(self._load_recent) if not self.recent: self.recent_menu.setEnabled(False) self.close_file_action = file_menu.addAction("&Close", self.close_file, QKeySequence.Close) self.close_all_action = file_menu.addAction("Close all", self.close_all) file_menu.addSeparator() self.import_bad_action = file_menu.addAction("Import bad channels...", self.import_bads) self.import_anno_action = file_menu.addAction("Import annotations...", self.import_annotations) file_menu.addSeparator() self.export_raw_action = file_menu.addAction("Export &raw...", self.export_raw) self.export_bad_action = file_menu.addAction("Export &bad channels...", self.export_bads) self.export_anno_action = file_menu.addAction("Export &annotations...", self.export_annotations) self.export_events_action = file_menu.addAction("Export &events...", self.export_events) file_menu.addSeparator() file_menu.addAction("&Quit", self.close, QKeySequence.Quit) edit_menu = menubar.addMenu("&Edit") self.pick_chans_action = edit_menu.addAction("Pick &channels...", self.pick_channels) self.chan_props_action = edit_menu.addAction("Channel &properties...", self.channel_properties) self.set_montage_action = edit_menu.addAction("Set &montage...", self.set_montage) edit_menu.addSeparator() self.setref_action = edit_menu.addAction("&Set reference...", self.set_reference) plot_menu = menubar.addMenu("&Plot") self.plot_raw_action = plot_menu.addAction("&Raw data", self.plot_raw) self.plot_psd_action = plot_menu.addAction("&Power spectral " "density...", self.plot_psd) self.plot_montage_action = plot_menu.addAction("Current &montage", self.plot_montage) tools_menu = menubar.addMenu("&Tools") self.filter_action = tools_menu.addAction("&Filter data...", self.filter_data) self.find_events_action = tools_menu.addAction("Find &events...", self.find_events) self.run_ica_action = tools_menu.addAction("Run &ICA...") self.import_ica_action = tools_menu.addAction("&Load ICA...", self.load_ica) view_menu = menubar.addMenu("&View") statusbar_action = view_menu.addAction("Statusbar", self._toggle_statusbar) statusbar_action.setCheckable(True) help_menu = menubar.addMenu("&Help") help_menu.addAction("&About", self.show_about) help_menu.addAction("About &Qt", self.show_about_qt) # set up data model for sidebar (list of open files) self.names = QStringListModel() self.names.dataChanged.connect(self._update_names) splitter = QSplitter() self.sidebar = QListView() self.sidebar.setFrameStyle(QFrame.NoFrame) self.sidebar.setFocusPolicy(Qt.NoFocus) self.sidebar.setModel(self.names) self.sidebar.clicked.connect(self._update_data) splitter.addWidget(self.sidebar) self.infowidget = InfoWidget() splitter.addWidget(self.infowidget) width = splitter.size().width() splitter.setSizes((width * 0.3, width * 0.7)) self.setCentralWidget(splitter) self.status_label = QLabel() self.statusBar().addPermanentWidget(self.status_label) if settings["statusbar"]: self.statusBar().show() statusbar_action.setChecked(True) else: self.statusBar().hide() statusbar_action.setChecked(False) self.setAcceptDrops(True) self._toggle_actions(False) self.show() def open_file(self): """Show open file dialog. """ fname = QFileDialog.getOpenFileName(self, "Open file", filter=SUPPORTED_FORMATS)[0] if fname: self.load_file(fname) def load_file(self, fname): """Load file. Parameters ---------- fname : str File name. """ if not exists(fname): QMessageBox.critical(self, "File not found", "{} does not exist.".format(fname)) self._remove_recent(fname) return name, ext = splitext(split(fname)[-1]) ftype = ext[1:].upper() if ext not in SUPPORTED_FORMATS: raise ValueError("File format {} is not supported.".format(ftype)) if ext in [".edf", ".bdf"]: raw = mne.io.read_raw_edf(fname, stim_channel=-1, preload=True) history.append("raw = mne.io.read_raw_edf('{}', " "stim_channel=-1, preload=True)".format(fname)) elif ext in [".fif"]: raw = mne.io.read_raw_fif(fname, preload=True) history.append("raw = mne.io.read_raw_fif('{}'," "preload=True)".format(fname)) elif ext in [".vhdr"]: raw = mne.io.read_raw_brainvision(fname, preload=True) history.append("raw = mne.io.read_raw_brainvision('{}', " "preload=True)".format(fname)) data.insert_data(DataSet(name=name, fname=fname, ftype=ftype, raw=raw)) self.find_events() self._update_sidebar(data.names, data.index) self._update_infowidget() self._update_statusbar() self._add_recent(fname) self._toggle_actions() def export_raw(self): """Export raw to FIF file. """ fname = QFileDialog.getSaveFileName(self, "Export raw", filter="*.fif")[0] if fname: name, ext = splitext(split(fname)[-1]) ext = ext if ext else ".fif" # automatically add extension fname = join(split(fname)[0], name + ext) data.current.raw.save(fname) def export_bads(self): """Export bad channels info to a CSV file. """ fname = QFileDialog.getSaveFileName(self, "Export bad channels", filter="*.csv")[0] if fname: name, ext = splitext(split(fname)[-1]) ext = ext if ext else ".csv" # automatically add extension fname = join(split(fname)[0], name + ext) with open(fname, "w") as f: f.write(",".join(data.current.raw.info["bads"])) def import_bads(self): """Import bad channels info from a CSV file. """ fname = QFileDialog.getOpenFileName(self, "Import bad channels", filter="*.csv")[0] if fname: with open(fname) as f: bads = f.read().replace(" ", "").split(",") if set(bads) - set(data.current.raw.info["ch_names"]): QMessageBox.critical(self, "Channel labels not found", "Some channel labels from the file " "are not present in the data.") else: data.current.raw.info["bads"] = bads data.data[data.index].raw.info["bads"] = bads def export_events(self): """Export events to a CSV file. The resulting CSV file has two columns. The first column contains the position (in samples), whereas the second column contains the type of the events. The first line is a header containing the column names. """ fname = QFileDialog.getSaveFileName(self, "Export events", filter="*.csv")[0] if fname: name, ext = splitext(split(fname)[-1]) ext = ext if ext else ".csv" # automatically add extension fname = join(split(fname)[0], name + ext) np.savetxt(fname, data.current.events[:, [0, 2]], fmt="%d", delimiter=",", header="pos,type", comments="") def export_annotations(self): """Export annotations to a CSV file. The resulting CSV file has three columns. The first column contains the annotation type, the second column contains the onset (in s), and the third column contains the duration (in s). The first line is a header containing the column names. """ fname = QFileDialog.getSaveFileName(self, "Export annotations", filter="*.csv")[0] if fname: name, ext = splitext(split(fname)[-1]) ext = ext if ext else ".csv" # automatically add extension fname = join(split(fname)[0], name + ext) anns = data.current.raw.annotations with open(fname, "w") as f: f.write("type,onset,duration\n") for a in zip(anns.description, anns.onset, anns.duration): f.write(",".join([a[0], str(a[1]), str(a[2])])) f.write("\n") def import_annotations(self): fname = QFileDialog.getOpenFileName(self, "Import annotations", filter="*.csv")[0] if fname: descs, onsets, durations = [], [], [] fs = data.current.raw.info["sfreq"] with open(fname) as f: f.readline() # skip header for line in f: ann = line.split(",") onset = float(ann[1].strip()) duration = float(ann[2].strip()) if onset > data.current.raw.n_times / fs: QMessageBox.critical(self, "Invalid annotations", "One or more annotations are " "outside of the data range.") return descs.append(ann[0].strip()) onsets.append(onset) durations.append(duration) annotations = mne.Annotations(onsets, durations, descs) data.raw.annotations = annotations data.data[data.index].raw.annotations = annotations self._update_infowidget() def close_file(self): """Close current file. """ data.remove_data() self._update_sidebar(data.names, data.index) self._update_infowidget() self._update_statusbar() if not data: self._toggle_actions(False) def close_all(self): """Close all currently open data sets. """ msg = QMessageBox.question(self, "Close all data sets", "Close all data sets?") if msg == QMessageBox.Yes: while data: self.close_file() def get_info(self): """Get basic information on current file. Returns ------- info : dict Dictionary with information on current file. """ raw = data.current.raw fname = data.current.fname ftype = data.current.ftype reference = data.current.reference events = data.current.events montage = data.current.montage if raw.info["bads"]: nbads = len(raw.info["bads"]) nchan = "{} ({} bad)".format(raw.info["nchan"], nbads) else: nchan = raw.info["nchan"] chans = Counter([channel_type(raw.info, i) for i in range(raw.info["nchan"])]) if events is not None: nevents = events.shape[0] unique = [str(e) for e in sorted(set(events[:, 2]))] if len(unique) > 20: # do not show all events first = ", ".join(unique[:10]) last = ", ".join(unique[-10:]) events = "{} ({})".format(nevents, first + ", ..., " + last) else: events = "{} ({})".format(nevents, ", ".join(unique)) else: events = "-" if isinstance(reference, list): reference = ",".join(reference) if raw.annotations is not None: annots = len(raw.annotations.description) else: annots = "-" return {"File name": fname if fname else "-", "File type": ftype if ftype else "-", "Number of channels": nchan, "Channels": ", ".join( [" ".join([str(v), k.upper()]) for k, v in chans.items()]), "Samples": raw.n_times, "Sampling frequency": str(raw.info["sfreq"]) + " Hz", "Length": str(raw.n_times / raw.info["sfreq"]) + " s", "Events": events, "Annotations": annots, "Reference": reference if reference else "-", "Montage": montage if montage is not None else "-", "Size in memory": "{:.2f} MB".format( raw._data.nbytes / 1024 ** 2), "Size on disk": "-" if not fname else "{:.2f} MB".format( getsize(fname) / 1024 ** 2)} def pick_channels(self): """Pick channels in current data set. """ channels = data.current.raw.info["ch_names"] dialog = PickChannelsDialog(self, channels, selected=channels) if dialog.exec_(): picks = [item.data(0) for item in dialog.channels.selectedItems()] drops = set(channels) - set(picks) tmp = data.current.raw.drop_channels(drops) name = data.current.name + " (channels dropped)" new = DataSet(raw=tmp, name=name, events=data.current.events) history.append("raw.drop({})".format(drops)) self._update_datasets(new) def channel_properties(self): info = data.current.raw.info dialog = ChannelPropertiesDialog(self, info) if dialog.exec_(): dialog.model.sort(0) bads = [] renamed = {} types = {} for i in range(dialog.model.rowCount()): new_label = dialog.model.item(i, 1).data(Qt.DisplayRole) old_label = info["ch_names"][i] if new_label != old_label: renamed[old_label] = new_label new_type = dialog.model.item(i, 2).data(Qt.DisplayRole).lower() old_type = channel_type(info, i).lower() if new_type != old_type: types[new_label] = new_type if dialog.model.item(i, 3).checkState() == Qt.Checked: bads.append(info["ch_names"][i]) info["bads"] = bads data.data[data.index].raw.info["bads"] = bads if renamed: mne.rename_channels(info, renamed) mne.rename_channels(data.data[data.index].raw.info, renamed) if types: data.current.raw.set_channel_types(types) data.data[data.index].raw.set_channel_types(types) self._update_infowidget() self._toggle_actions(True) def set_montage(self): """Set montage. """ path = join(mne.__path__[0], "channels", "data", "montages") supported = (".elc", ".txt", ".csd", ".sfp", ".elp", ".hpts", ".loc", ".locs", ".eloc", ".bvef") files = [splitext(f) for f in listdir(path)] montages = sorted([f for f, ext in files if ext in supported], key=str.lower) # TODO: currently it is not possible to remove an existing montage dialog = MontageDialog(self, montages, selected=data.current.montage) if dialog.exec_(): name = dialog.montages.selectedItems()[0].data(0) montage = mne.channels.read_montage(name) ch_names = data.current.raw.info["ch_names"] # check if at least one channel name matches a name in the montage if set(ch_names) & set(montage.ch_names): data.current.montage = name data.data[data.index].montage = name data.current.raw.set_montage(montage) data.data[data.index].raw.set_montage(montage) self._update_infowidget() self._toggle_actions() else: QMessageBox.critical(self, "No matching channel names", "Channel names defined in the montage do " "not match any channel name in the data.") def plot_raw(self): """Plot raw data. """ events = data.current.events nchan = data.current.raw.info["nchan"] fig = data.current.raw.plot(events=events, n_channels=nchan, title=data.current.name, show=False) history.append("raw.plot(n_channels={})".format(nchan)) win = fig.canvas.manager.window win.setWindowTitle("Raw data") win.findChild(QStatusBar).hide() win.installEventFilter(self) # detect if the figure is closed # prevent closing the window with the escape key try: key_events = fig.canvas.callbacks.callbacks["key_press_event"][8] except KeyError: pass else: # this requires MNE >=0.15 key_events.func.keywords["params"]["close_key"] = None fig.show() def plot_psd(self): """Plot power spectral density (PSD). """ fig = data.current.raw.plot_psd(average=False, spatial_colors=False, show=False) win = fig.canvas.manager.window win.setWindowTitle("Power spectral density") fig.show() def plot_montage(self): """Plot montage. """ montage = mne.channels.read_montage(data.current.montage) fig = montage.plot(show_names=True, show=False) win = fig.canvas.manager.window win.setWindowTitle("Montage") win.findChild(QStatusBar).hide() win.findChild(QToolBar).hide() fig.show() def load_ica(self): """Load ICA solution from a file. """ fname = QFileDialog.getOpenFileName(self, "Load ICA", filter="*.fif *.fif.gz") if fname[0]: self.state.ica = mne.preprocessing.read_ica(fname[0]) def find_events(self): events = mne.find_events(data.current.raw, consecutive=False) if events.shape[0] > 0: # if events were found data.current.events = events data.data[data.index].events = events self._update_infowidget() def filter_data(self): """Filter data. """ dialog = FilterDialog(self) if dialog.exec_(): low, high = dialog.low, dialog.high tmp = filter_data(data.current.raw._data, data.current.raw.info["sfreq"], l_freq=low, h_freq=high, fir_design="firwin") name = data.current.name + " ({}-{} Hz)".format(low, high) new = DataSet(raw=mne.io.RawArray(tmp, data.current.raw.info), name=name, events=data.current.events) history.append("raw.filter({}, {})".format(low, high)) self._update_datasets(new) def set_reference(self): """Set reference. """ dialog = ReferenceDialog(self) if dialog.exec_(): if dialog.average.isChecked(): tmp, _ = mne.set_eeg_reference(data.current.raw, None) tmp.apply_proj() name = data.current.name + " (average ref)" new = DataSet(raw=tmp, name=name, reference="average", events=data.current.events) else: ref = [c.strip() for c in dialog.channellist.text().split(",")] refstr = ",".join(ref) if set(ref) - set(data.current.raw.info["ch_names"]): # add new reference channel(s) to data try: tmp = mne.add_reference_channels(data.current.raw, ref) except RuntimeError: QMessageBox.critical(self, "Cannot add new channels", "Cannot add new channels to " "average referenced data.") return else: # re-reference to existing channel(s) tmp, _ = mne.set_eeg_reference(data.current.raw, ref) name = data.current.name + " (ref {})".format(refstr) new = DataSet(raw=tmp, name=name, reference=refstr, events=data.current.events) self._update_datasets(new) def show_about(self): """Show About dialog. """ msg = """<b>MNELAB {}</b><br/><br/> <a href="https://github.com/cbrnr/mnelab">MNELAB</a> - a graphical user interface for <a href="https://github.com/mne-tools/mne-python">MNE</a>.<br/><br/> This program uses MNE version {}.<br/><br/> Licensed under the BSD 3-clause license.<br/> Copyright 2017 by Clemens Brunner.""".format(__version__, mne.__version__) QMessageBox.about(self, "About MNELAB", msg) def show_about_qt(self): """Show About Qt dialog. """ QMessageBox.aboutQt(self, "About Qt") def _update_datasets(self, dataset): # if current data is stored in a file create a new data set if data.current.fname: data.insert_data(dataset) # otherwise ask if the current data set should be overwritten or if a # new data set should be created else: msg = QMessageBox.question(self, "Overwrite existing data set", "Overwrite existing data set?") if msg == QMessageBox.No: # create new data set data.insert_data(dataset) else: # overwrite existing data set data.update_data(dataset) self._update_sidebar(data.names, data.index) self._update_infowidget() self._update_statusbar() def _update_sidebar(self, names, index): """Update (overwrite) sidebar with names and current index. """ self.names.setStringList(names) self.sidebar.setCurrentIndex(self.names.index(index)) def _update_infowidget(self): if data: self.infowidget.set_values(self.get_info()) else: self.infowidget.clear() def _update_statusbar(self): if data: mb = data.nbytes / 1024 ** 2 self.status_label.setText("Total Memory: {:.2f} MB".format(mb)) else: self.status_label.clear() def _toggle_actions(self, enabled=True): """Toggle actions. Parameters ---------- enabled : bool Specifies whether actions are enabled (True) or disabled (False). """ self.close_file_action.setEnabled(enabled) self.close_all_action.setEnabled(enabled) self.export_raw_action.setEnabled(enabled) if data.data: bads = bool(data.current.raw.info["bads"]) self.export_bad_action.setEnabled(enabled and bads) events = data.current.events is not None self.export_events_action.setEnabled(enabled and events) annot = data.current.raw.annotations is not None self.export_anno_action.setEnabled(enabled and annot) montage = bool(data.current.montage) self.plot_montage_action.setEnabled(enabled and montage) else: self.export_bad_action.setEnabled(enabled) self.export_events_action.setEnabled(enabled) self.export_anno_action.setEnabled(enabled) self.plot_montage_action.setEnabled(enabled) self.import_bad_action.setEnabled(enabled) self.import_anno_action.setEnabled(enabled) self.pick_chans_action.setEnabled(enabled) self.chan_props_action.setEnabled(enabled) self.set_montage_action.setEnabled(enabled) self.plot_raw_action.setEnabled(enabled) self.plot_psd_action.setEnabled(enabled) self.filter_action.setEnabled(enabled) self.setref_action.setEnabled(enabled) self.find_events_action.setEnabled(enabled) self.run_ica_action.setEnabled(enabled) self.import_ica_action.setEnabled(enabled) def _add_recent(self, fname): """Add a file to recent file list. Parameters ---------- fname : str File name. """ if fname in self.recent: # avoid duplicates self.recent.remove(fname) self.recent.insert(0, fname) while len(self.recent) > MAX_RECENT: # prune list self.recent.pop() self._write_settings() if not self.recent_menu.isEnabled(): self.recent_menu.setEnabled(True) def _remove_recent(self, fname): """Remove file from recent file list. Parameters ---------- fname : str File name. """ if fname in self.recent: self.recent.remove(fname) self._write_settings() if not self.recent: self.recent_menu.setEnabled(False) def _write_settings(self): """Write application settings. """ settings = QSettings() settings.setValue("recent", self.recent) settings.setValue("statusbar", not self.statusBar().isHidden()) settings.setValue("geometry", self.saveGeometry()) settings.setValue("state", self.saveState()) def _read_settings(self): """Read application settings. Returns ------- settings : dict The restored settings values are returned in a dictionary for further processing. """ settings = QSettings() recent = settings.value("recent") if not recent: recent = [] # default is empty list statusbar = settings.value("statusbar") if statusbar is None: # default is True statusbar = True geometry = settings.value("geometry") state = settings.value("state") return {"recent": recent, "statusbar": statusbar, "geometry": geometry, "state": state} @pyqtSlot(QModelIndex) def _update_data(self, selected): """Update index and information based on the state of the sidebar. Parameters ---------- selected : QModelIndex Index of the selected row. """ if selected.row() != data.index: data.index = selected.row() data.update_current() self._update_infowidget() @pyqtSlot(QModelIndex, QModelIndex) def _update_names(self, start, stop): """Update names in DataSets after changes in sidebar. """ for index in range(start.row(), stop.row() + 1): data.data[index].name = self.names.stringList()[index] if data.index in range(start.row(), stop.row() + 1): data.current.name = data.names[data.index] @pyqtSlot() def _update_recent_menu(self): self.recent_menu.clear() for recent in self.recent: self.recent_menu.addAction(recent) @pyqtSlot(QAction) def _load_recent(self, action): self.load_file(action.text()) @pyqtSlot() def _toggle_statusbar(self): if self.statusBar().isHidden(): self.statusBar().show() else: self.statusBar().hide() self._write_settings() @pyqtSlot(QDropEvent) def dragEnterEvent(self, event): if event.mimeData().hasUrls(): event.acceptProposedAction() @pyqtSlot(QDropEvent) def dropEvent(self, event): mime = event.mimeData() if mime.hasUrls(): urls = mime.urls() for url in urls: self.load_file(url.toLocalFile()) @pyqtSlot(QEvent) def closeEvent(self, event): """Close application. Parameters ---------- event : QEvent Close event. """ self._write_settings() if history: print("\nCommand History") print("===============") print("\n".join(history)) QApplication.quit() def eventFilter(self, source, event): # currently the only source is the raw plot window if event.type() == QEvent.Close: self._update_infowidget() self._toggle_actions() return QObject.eventFilter(self, source, event)
class MainWindow(QMainWindow): """MNELAB main window.""" def __init__(self, model): """Initialize MNELAB main window. Parameters ---------- model : mnelab.model.Model instance The main window needs to connect to a model containing all data sets. This decouples the GUI from the data (model/view). """ super().__init__() self.model = model # data model self.setWindowTitle("MNELAB") # restore settings settings = read_settings() self.recent = settings["recent"] # list of recent files if settings["geometry"]: self.restoreGeometry(settings["geometry"]) else: self.setGeometry(300, 300, 1000, 750) # default window size self.move(QApplication.desktop().screen().rect().center() - self.rect().center()) # center window if settings["state"]: self.restoreState(settings["state"]) self.actions = {} # contains all actions # initialize menus file_menu = self.menuBar().addMenu("&File") self.actions["open_file"] = file_menu.addAction( "&Open...", lambda: self.open_file(model.load, "Open raw", SUPPORTED_FORMATS), QKeySequence.Open) self.recent_menu = file_menu.addMenu("Open recent") self.recent_menu.aboutToShow.connect(self._update_recent_menu) self.recent_menu.triggered.connect(self._load_recent) if not self.recent: self.recent_menu.setEnabled(False) self.actions["close_file"] = file_menu.addAction( "&Close", self.model.remove_data, QKeySequence.Close) self.actions["close_all"] = file_menu.addAction( "Close all", self.close_all) file_menu.addSeparator() self.actions["import_bads"] = file_menu.addAction( "Import bad channels...", lambda: self.import_file(model.import_bads, "Import bad channels", "*.csv *.txt")) self.actions["import_events"] = file_menu.addAction( "Import events...", lambda: self.import_file(model.import_events, "Import events", "*.csv *.mrk")) self.actions["import_annotations"] = file_menu.addAction( "Import annotations...", lambda: self.import_file(model.import_annotations, "Import annotations", "*.csv *.mrk")) self.actions["import_ica"] = file_menu.addAction( "Import &ICA...", lambda: self.open_file(model.import_ica, "Import ICA", "*.fif *.fif.gz")) file_menu.addSeparator() self.actions["export_data"] = file_menu.addAction( "Export data...", lambda: self.export_file(model.export_data, "Export", SUPPORTED_EXPORT_FORMATS)) self.actions["export_bads"] = file_menu.addAction( "Export &bad channels...", lambda: self.export_file(model.export_bads, "Export bad channels", "*.csv")) self.actions["export_events"] = file_menu.addAction( "Export &events...", lambda: self.export_file(model.export_events, "Export events", "*.csv")) self.actions["export_annotations"] = file_menu.addAction( "Export &annotations...", lambda: self.export_file(model.export_annotations, "Export annotations", "*.csv")) self.actions["export_ica"] = file_menu.addAction( "Export ICA...", lambda: self.export_file(model.export_ica, "Export ICA", "*.fif *.fif.gz")) self.actions["export_psd"] = file_menu.addAction( "Export Power Spectrum Density...", lambda: self.export_file(model.export_psd, "Export Power Spectrum Density", "*.hdf")) self.actions["export_tfr"] = file_menu.addAction( "Export Time-Frequency...", lambda: self.export_file(model.export_tfr, "Export Time-Frequency", "*.hdf")) file_menu.addSeparator() self.actions["quit"] = file_menu.addAction( "&Quit", self.close, QKeySequence.Quit) edit_menu = self.menuBar().addMenu("&Edit") self.actions["pick_chans"] = edit_menu.addAction( "Pick &channels...", self.pick_channels) self.actions["chan_props"] = edit_menu.addAction( "Edit channel &properties...", self.channel_properties) self.actions["set_montage"] = edit_menu.addAction( "Edit &montage...", self.set_montage) self.actions["events"] = edit_menu.addAction( "Edit &events...", self.edit_events) plot_menu = self.menuBar().addMenu("&Plot") self.actions["plot_raw"] = plot_menu.addAction( "Plot &Data...", self.plot_raw) self.actions["plot_image"] = plot_menu.addAction( "Plot data as &Image...", self.plot_image) self.actions["plot_states"] = plot_menu.addAction( "Plot &States...", self.plot_states) self.actions["plot_topomaps"] = plot_menu.addAction( "Plot &Topomaps...", self.plot_topomaps) self.actions["plot_montage"] = plot_menu.addAction( "Plot Current &montage", self.plot_montage) tools_menu = self.menuBar().addMenu("&Preprocessing") self.actions["filter"] = tools_menu.addAction( "&Filter data...", self.filter_data) self.actions["resample"] = tools_menu.addAction( "&Downsample...", self.resample) self.actions["interpolate_bads"] = tools_menu.addAction( "Interpolate bad channels...", self.interpolate_bads) self.actions["set_ref"] = tools_menu.addAction( "&Set reference...", self.set_reference) ica_menu = self.menuBar().addMenu("&ICA") self.actions["run_ica"] = ica_menu.addAction( "Run &ICA...", self.run_ica) ica_menu.addSeparator() self.actions["plot_ica_components"] = ica_menu.addAction( "Plot ICA &components...", self.plot_ica_components_with_timeseries) self.actions["plot_ica_sources"] = ica_menu.addAction( "Plot &ICA sources...", self.plot_ica_sources) self.actions["plot_correlation_matrix"] = ica_menu.addAction( "Plot correlation &matrix...", self.plot_correlation_matrix) self.actions["plot_overlay"] = ica_menu.addAction( "Plot overlay...", self.plot_ica_overlay) ica_menu.addSeparator() self.actions["apply_ica"] = ica_menu.addAction( "Apply &ICA...", self.apply_ica) freq_menu = self.menuBar().addMenu("&Frequencies") self.actions["plot_psd"] = freq_menu.addAction( "Compute &Power spectral density...", self.plot_psd) self.actions["plot_tfr"] = freq_menu.addAction( "Compute &Time-Frequency...", self.plot_tfr) freq_menu.addSeparator() self.actions["open_tfr"] = freq_menu.addAction( "&Open Time-Frequency file...", self.open_tfr) self.actions["open_psd"] = freq_menu.addAction( "&Open Power Spectrum Density file...", self.open_psd) events_menu = self.menuBar().addMenu("&Events") self.actions["plot_events"] = events_menu.addAction( "&Plot events...", self.plot_events) events_menu.addSeparator() self.actions["find_events"] = events_menu.addAction( "Find &events...", self.find_events) self.actions["add_events"] = events_menu.addAction( "Setup events as annotation...", self.add_events) epochs_menu = self.menuBar().addMenu("Epochs") self.actions["epoch_data"] = epochs_menu.addAction( "Cut data into epochs...", self.epoch_data) self.actions["evoke_data"] = epochs_menu.addAction( "Average epochs...", self.evoke_data) batch_menu = self.menuBar().addMenu("&Batch") self.actions["open_batch"] = batch_menu.addAction( "Open &Batch processing window", self.open_batch) view_menu = self.menuBar().addMenu("&View") self.actions["statusbar"] = view_menu.addAction( "Statusbar", self._toggle_statusbar) self.actions["statusbar"].setCheckable(True) help_menu = self.menuBar().addMenu("&Help") self.actions["about"] = help_menu.addAction("&About", self.show_about) self.actions["about_qt"] = help_menu.addAction( "About &Qt", self.show_about_qt) # actions that are always enabled self.always_enabled = ["open_file", "about", "about_qt", "quit", "statusbar", "open_batch", "open_tfr", "open_psd"] # set up data model for sidebar (list of open files) self.names = QStringListModel() self.names.dataChanged.connect(self._update_names) splitter = QSplitter() self.sidebar = QListView() self.sidebar.setFrameStyle(QFrame.NoFrame) self.sidebar.setFocusPolicy(Qt.NoFocus) self.sidebar.setModel(self.names) self.sidebar.clicked.connect(self._update_data) splitter.addWidget(self.sidebar) self.infowidget = InfoWidget() splitter.addWidget(self.infowidget) width = splitter.size().width() splitter.setSizes((width * 0.3, width * 0.7)) self.setCentralWidget(splitter) self.status_label = QLabel() self.statusBar().addPermanentWidget(self.status_label) if settings["statusbar"]: self.statusBar().show() self.actions["statusbar"].setChecked(True) else: self.statusBar().hide() self.actions["statusbar"].setChecked(False) self.setAcceptDrops(True) self.data_changed() def data_changed(self): # update sidebar self.names.setStringList(self.model.names) self.sidebar.setCurrentIndex(self.names.index(self.model.index)) # update info widget if self.model.data: self.infowidget.set_values(self.model.get_info()) else: self.infowidget.clear() # update status bar if self.model.data: mb = self.model.nbytes / 1024 ** 2 self.status_label.setText("Total Memory: {:.2f} MB".format(mb)) else: self.status_label.clear() # toggle actions if len(self.model) == 0: # disable if no data sets are currently open enabled = False else: enabled = True for name, action in self.actions.items(): # toggle if name not in self.always_enabled: action.setEnabled(enabled) if self.model.data: # toggle if specific conditions are met if self.model.current["raw"]: raw = True evoked = False bads = bool(self.model.current["raw"].info["bads"]) annot = self.model.current["raw"].annotations is not None events = self.model.current["events"] is not None self.actions["import_annotations"].setEnabled(True) self.actions["import_events"].setEnabled(True) self.actions["evoke_data"].setEnabled(False) self.actions["plot_image"].setEnabled(False) self.actions["plot_tfr"].setEnabled(False) else: raw = False annot = False events = False self.actions["find_events"].setEnabled(False) self.actions["import_annotations"].setEnabled(False) self.actions["import_events"].setEnabled(False) self.actions["plot_image"].setEnabled(True) if self.model.current["epochs"]: evoked = False bads = bool(self.model.current["epochs"].info["bads"]) self.actions["evoke_data"].setEnabled(True) else: evoked = True bads = bool(self.model.current["evoked"].info["bads"]) self.actions["evoke_data"].setEnabled(False) self.actions["export_bads"].setEnabled(enabled and bads) self.actions["export_events"].setEnabled(enabled and events) self.actions["export_annotations"].setEnabled(enabled and annot) montage = bool(self.model.current["montage"]) self.actions["run_ica"].setEnabled(enabled and montage and not evoked) self.actions["plot_montage"].setEnabled(enabled and montage) self.actions["interpolate_bads"].setEnabled(enabled and montage) ica = bool(self.model.current["ica"]) self.actions["export_ica"].setEnabled(enabled and ica) self.actions["plot_events"].setEnabled(raw and events) self.actions["plot_ica_components"].setEnabled(enabled and ica and montage) self.actions["plot_ica_sources"].setEnabled(enabled and ica and montage) self.actions["plot_correlation_matrix"].setEnabled( enabled and ica and montage) self.actions["plot_overlay"].setEnabled( enabled and ica and montage) self.actions["run_ica"].setEnabled(montage and not evoked) self.actions["apply_ica"].setEnabled(enabled and ica and montage) self.actions["events"].setEnabled(enabled and events) self.actions["epoch_data"].setEnabled(enabled and events) self.actions["add_events"].setEnabled(enabled and events) self.actions["plot_states"].setEnabled(montage and evoked) self.actions["plot_topomaps"].setEnabled(montage and evoked) self.actions["export_tfr"].setEnabled( self.model.current["tfr"] is not None) self.actions["export_psd"].setEnabled( self.model.current["psd"] is not None) # add to recent files if len(self.model) > 0: self._add_recent(self.model.current["fname"]) def open_file(self, f, text, ffilter): """Open file.""" fname = QFileDialog.getOpenFileName(self, text, filter=ffilter)[0] if fname: f(fname) def export_file(self, f, text, ffilter): """Export to file.""" # BUG on windows fname = QFileDialog.getSaveFileName(self, # text, filter=ffilter)[0] fname = QFileDialog.getSaveFileName(self, text, filter=ffilter)[0] if fname: f(fname) def import_file(self, f, text, ffilter): """Import file.""" fname = QFileDialog.getOpenFileName(self, text, filter=ffilter)[0] if fname: try: f(fname) except LabelsNotFoundError as e: QMessageBox.critical(self, "Channel labels not found", str(e)) except InvalidAnnotationsError as e: QMessageBox.critical(self, "Invalid annotations", str(e)) def close_all(self): """Close all currently open data sets.""" msg = QMessageBox.question(self, "Close all data sets", "Close all data sets?") if msg == QMessageBox.Yes: while len(self.model) > 0: self.model.remove_data() def pick_channels(self): """Pick channels in current data set.""" if self.model.current["raw"]: channels = self.model.current["raw"].info["ch_names"] elif self.model.current["epochs"]: channels = self.model.current["epochs"].info["ch_names"] else: channels = self.model.current["evoked"].info["ch_names"] dialog = PickChannelsDialog(self, channels, selected=channels) if dialog.exec_(): picks = [item.data(0) for item in dialog.channels.selectedItems()] drops = set(channels) - set(picks) if drops: self.auto_duplicate() self.model.drop_channels(drops) self.model.history.append(f"data.drop({drops})") def channel_properties(self): """Show channel properties dialog.""" if self.model.current["raw"]: info = self.model.current["raw"].info elif self.model.current["epochs"]: info = self.model.current["epochs"].info else: info = self.model.current["evoked"].info dialog = ChannelPropertiesDialog(self, info) if dialog.exec_(): dialog.model.sort(0) bads = [] renamed = {} types = {} for i in range(dialog.model.rowCount()): new_label = dialog.model.item(i, 1).data(Qt.DisplayRole) old_label = info["ch_names"][i] if new_label != old_label: renamed[old_label] = new_label new_type = dialog.model.item(i, 2).data(Qt.DisplayRole).lower() types[new_label] = new_type if dialog.model.item(i, 3).checkState() == Qt.Checked: bads.append(info["ch_names"][i]) self.model.set_channel_properties(bads, renamed, types) def set_montage(self): """Set montage.""" montages = mne.channels.get_builtin_montages() # TODO: currently it is not possible to remove an existing montage dialog = MontageDialog(self, montages, selected=self.model.current["montage"]) if dialog.exec_(): if dialog.montage_path == '': name = dialog.montages.selectedItems()[0].data(0) montage = mne.channels.read_montage(name) self.model.history.append("montage = mne.channels." + ("read_montage({})").format(name)) else: from .utils.montage import xyz_to_montage montage = xyz_to_montage(dialog.montage_path) self.model.history.append("montage = xyz_to_montage({})" .format(dialog.montage_path)) if self.model.current["raw"]: ch_names = self.model.current["raw"].info["ch_names"] elif self.model.current["epochs"]: ch_names = self.model.current["epochs"].info["ch_names"] elif self.model.current["evoked"]: ch_names = self.model.current["evoked"].info["ch_names"] # check if at least one channel name matches a name in the montage if set(ch_names) & set(montage.ch_names): self.model.set_montage(montage) else: QMessageBox.critical(self, "No matching channel names", "Channel names defined in the montage do " "not match any channel name in the data.") def edit_events(self): pos = self.model.current["events"][:, 0].tolist() desc = self.model.current["events"][:, 2].tolist() dialog = EventsDialog(self, pos, desc) if dialog.exec_(): pass def plot_raw(self): """Plot data.""" events = self.model.current["events"] if self.model.current["raw"]: nchan = self.model.current["raw"].info["nchan"] fig = self.model.current["raw"].plot( events=events, title=self.model.current["name"], scalings="auto", show=False) self.model.history.append("raw.plot(n_channels={})".format(nchan)) elif self.model.current["epochs"]: nchan = self.model.current["epochs"].info["nchan"] fig = self.model.current["epochs"].plot( title=self.model.current["name"], scalings="auto", show=False) self.model.history.append( "epochs.plot(n_channels={})".format(nchan)) elif self.model.current["evoked"]: nchan = self.model.current["evoked"].info["nchan"] fig = self.model.current["evoked"].plot(show=False, gfp=True, spatial_colors=True, selectable=False) self.model.history.append( "epochs.plot(n_channels={})".format(nchan)) win = fig.canvas.manager.window win.setWindowTitle(self.model.current["name"]) win.findChild(QStatusBar).hide() win.installEventFilter(self) # detect if the figure is closed # prevent closing the window with the escape key try: key_events = fig.canvas.callbacks.callbacks["key_press_event"][8] except KeyError: pass else: # this requires MNE >=0.15 # This line causes bug... I don't know why exactly # AttributeError: '_StrongRef' object has no attribute 'func' # # key_events.func.keywords["params"]["close_key"] = None pass fig.show() def plot_image(self): if self.model.current["epochs"]: try: epochs = self.model.current["epochs"] dialog = NavEpochsDialog(None, epochs) dialog.setWindowModality(Qt.WindowModal) dialog.setWindowTitle(self.model.current["name"]) dialog.exec() except Exception as e: print(e) elif self.model.current["evoked"]: fig = self.model.current["evoked"].plot_image(show=False) self.model.history.append("evoked.plot_image()") win = fig.canvas.manager.window win.findChild(QStatusBar).hide() win.setWindowTitle(self.model.current["name"]) win.installEventFilter(self) # detect if the figure is closed fig.show() def plot_states(self): if self.model.current["evoked"]: dialog = EvokedStatesDialog(None, self.model.current["evoked"]) dialog.setWindowModality(Qt.NonModal) dialog.setWindowTitle(self.model.current["name"]) dialog.show() def plot_topomaps(self): if self.model.current["evoked"]: dialog = EvokedTopoDialog(None, self.model.current["evoked"]) dialog.setWindowModality(Qt.NonModal) dialog.setWindowTitle(self.model.current["name"]) dialog.show() def plot_events(self): events = self.model.current["events"] fig = mne.viz.plot_events(events, show=False) win = fig.canvas.manager.window win.setWindowModality(Qt.WindowModal) win.setWindowTitle(self.model.current["name"]) win.findChild(QStatusBar).hide() win.findChild(QToolBar).hide() fig.show() def plot_psd(self): """Plot power spectral density (PSD).""" if self.model.current["raw"]: raw = self.model.current["raw"] dialog = PSDDialog(None, raw) dialog.setWindowModality(Qt.WindowModal) dialog.setWindowTitle('PSD of ' + self.model.current["name"]) dialog.exec_() elif self.model.current["epochs"]: epochs = self.model.current["epochs"] dialog = PSDDialog(None, epochs) dialog.setWindowModality(Qt.WindowModal) dialog.setWindowTitle('PSD of ' + self.model.current["name"]) dialog.exec_() elif self.model.current["evoked"]: evoked = self.model.current["evoked"] dialog = PSDDialog(None, evoked) dialog.setWindowModality(Qt.WindowModal) dialog.setWindowTitle('PSD of ' + self.model.current["name"]) dialog.exec_() try: psd = dialog.psd except Exception as e: psd = None if psd is not None: self.model.current["psd"] = psd self.data_changed() def open_psd(self): fname = QFileDialog.getOpenFileName(self, "Open TFR", filter="*.h5 *.hdf")[0] try: psd = EpochsPSD().init_from_hdf(fname) win = EpochsPSDWindow(psd, parent=None) win.setWindowTitle(fname) win.exec() except Exception as e: print(e) try: psd = RawPSD().init_from_hdf(fname) win = RawPSDWindow(psd, parent=None) win.setWindowModality(Qt.WindowModal) win.setWindowTitle(fname) win.exec() except Exception: pass def plot_tfr(self): """Plot Time-Frequency.""" if self.model.current["epochs"]: data = self.model.current["epochs"] elif self.model.current["evoked"]: data = self.model.current["evoked"] dialog = TimeFreqDialog(None, data) dialog.setWindowModality(Qt.WindowModal) dialog.setWindowTitle('TFR of ' + self.model.current["name"]) dialog.exec_() try: tfr = dialog.avgTFR self.data_changed() except Exception as e: tfr = None if tfr is not None: self.model.current["tfr"] = tfr self.data_changed() def open_tfr(self): try: fname = QFileDialog.getOpenFileName(self, "Open TFR", filter="*.h5 *.hdf")[0] avgTFR = AvgEpochsTFR().init_from_hdf(fname) win = AvgTFRWindow(avgTFR, parent=None) win.setWindowModality(Qt.WindowModal) win.setWindowTitle(fname) win.exec() except Exception as e: print(e) def plot_montage(self): """Plot current montage.""" if self.model.current["raw"]: data = self.model.current["raw"] elif self.model.current["epochs"]: data = self.model.current["epochs"] elif self.model.current["evoked"]: data = self.model.current["evoked"] chans = Counter([mne.io.pick.channel_type(data.info, i) for i in range(data.info["nchan"])]) fig = plt.figure() types = [] for type in chans.keys(): if type in ['eeg', 'mag', 'grad']: types.append(type) for i, type in enumerate(types): ax = fig.add_subplot(1, len(types), i + 1) ax.set_title(type + '({} channels)'.format(chans[type])) data.plot_sensors(show_names=True, show=False, ch_type=type, axes=ax, title='') win = fig.canvas.manager.window win.resize(len(types) * 600, 600) win.setWindowTitle(self.model.current["name"]) win.findChild(QStatusBar).hide() win.findChild(QToolBar).hide() fig.show() def plot_ica_components_with_timeseries(self): if self.model.current["raw"]: try: fig = plot_ica_components_with_timeseries( self.model.current["ica"], inst=self.model.current["raw"]) except Exception as e: QMessageBox.critical(self, "Unexpected error ", str(e)) elif self.model.current["epochs"]: try: fig = plot_ica_components_with_timeseries( self.model.current["ica"], inst=self.model.current["epochs"]) except Exception as e: try: fig = self.model.current["ica"].plot_ica_components( inst=self.model.current["epochs"]) except Exception as e: QMessageBox.critical(self, "Unexpected error ", str(e)) def plot_ica_sources(self): if self.model.current["raw"]: fig = (self.model.current["ica"] .plot_sources(inst=self.model.current["raw"])) elif self.model.current["epochs"]: fig = (self.model.current["ica"] .plot_sources(inst=self.model.current["epochs"])) win = fig.canvas.manager.window win.setWindowTitle("ICA Sources") win.findChild(QStatusBar).hide() win.installEventFilter(self) # detect if the figure is closed def plot_correlation_matrix(self): if self.model.current["raw"]: try: plot_cormat(self.model.current["raw"], self.model.current["ica"]) except ValueError as e: QMessageBox.critical( self, "Can't compute correlation with template ", str(e)) except Exception as e: QMessageBox.critical( self, "Unexpected error ", str(e)) elif self.model.current["epochs"]: try: plot_cormat(self.model.current["epochs"], self.model.current["ica"]) except ValueError as e: QMessageBox.critical( self, "Can't compute correlation with template ", str(e)) except Exception as e: QMessageBox.critical( self, "Unexpected error ", str(e)) def plot_ica_overlay(self): if self.model.current["raw"]: plot_overlay(self.model.current["ica"], self.model.current["raw"]) elif self.model.current["epochs"]: plot_overlay(self.model.current["ica"], self.model.current["epochs"]) return() def run_ica(self): """Run ICA calculation.""" try: import picard import mne.preprocessing.ICA except ImportError: have_picard = False import mne else: have_picard = True try: import sklearn # required for FastICA except ImportError: have_sklearn = False else: have_sklearn = True if self.model.current["raw"]: data = self.model.current["raw"] inst_type = "raw" elif self.model.current["epochs"]: data = self.model.current["epochs"] inst_type = "epochs" nchan = len(mne.pick_types(data.info, meg=True, eeg=True, exclude='bads')) dialog = RunICADialog(self, nchan, have_picard, have_sklearn) if dialog.exec_(): calc = CalcDialog(self, "Calculating ICA", "Calculating ICA.") method = dialog.method.currentText() exclude_bad_segments = dialog.exclude_bad_segments.isChecked() decim = int(dialog.decim.text()) n_components = int(dialog.n_components.text()) max_pca_components = int(dialog.max_pca_components.text()) n_pca_components = int(dialog.pca_components.text()) random_state = int(dialog.random_seed.text()) max_iter = int(dialog.max_iter.text()) ica = mne.preprocessing.ICA( method=dialog.methods[method], n_components=n_components, max_pca_components=max_pca_components, n_pca_components=n_pca_components, random_state=random_state, max_iter=max_iter) pool = mp.Pool(1) kwds = {"reject_by_annotation": exclude_bad_segments, "decim": decim} res = pool.apply_async(func=ica.fit, args=(data,), kwds=kwds, callback=lambda x: calc.accept()) if not calc.exec_(): pool.terminate() else: self.auto_duplicate() self.model.current["ica"] = res.get(timeout=1) self.model.history.append( "ica=ICA(" + ("method={} ,").format(dialog.methods[method]) + ("n_components={}, ").format(n_components) + ("max_pca_components={}, ").format(max_pca_components) + ("n_pca_components={}, ").format(n_pca_components) + ("random_state={}, ").format(random_state) + ("max_iter={})").format(max_iter)) self.model.history.append( "ica.fit(" + ("inst={}, ").format(inst_type) + ("decim={}, ").format(decim) + ("reject_by_annotation={})" .format(exclude_bad_segments))) self.data_changed() def apply_ica(self): """Set reference.""" self.auto_duplicate() self.model.apply_ica() def resample(self): """Resample data.""" dialog = ResampleDialog(self) if dialog.exec_(): sfreq = dialog.sfreq if sfreq is not None: self.auto_duplicate() if self.model.current['raw']: self.model.current['raw'].resample(sfreq) elif self.model.current['epochs']: self.model.current['epochs'].resample(sfreq) elif self.model.current['evoked']: self.model.current['evoked'].resample(sfreq) self.model.current["name"] += " (resampled)" self.data_changed() def filter_data(self): """Filter data.""" if self.model.current['raw']: israw = True else: israw = False dialog = FilterDialog(self, israw) if dialog.exec_(): if dialog.low or dialog.high or dialog.notch_freqs: self.auto_duplicate() self.model.filter(dialog.low, dialog.high, dialog.notch_freqs) def find_events(self): info = self.model.current["raw"].info # use first stim channel as default in dialog default_stim = 0 for i in range(info["nchan"]): if mne.io.pick.channel_type(info, i) == "stim": default_stim = i break dialog = FindEventsDialog(self, info["ch_names"], default_stim) if dialog.exec_(): stim_channel = dialog.stimchan.currentText() consecutive = dialog.consecutive.isChecked() initial_event = dialog.initial_event.isChecked() uint_cast = dialog.uint_cast.isChecked() min_dur = dialog.minduredit.value() shortest_event = dialog.shortesteventedit.value() self.model.find_events(stim_channel=stim_channel, consecutive=consecutive, initial_event=initial_event, uint_cast=uint_cast, min_duration=min_dur, shortest_event=shortest_event) def interpolate_bads(self): """Interpolate bad channels.""" self.auto_duplicate() self.model.interpolate_bads() def add_events(self): """Setup the events in the data as a STIM channel.""" self.auto_duplicate() self.model.add_events() def epoch_data(self): """Cut raw data into epochs.""" dialog = EpochingDialog(self, self.model.current["events"], self.model.current["raw"]) if dialog.exec_(): selected = [int(item.text()) for item in dialog.labels.selectedItems()] try: tmin = float(dialog.tmin.text()) tmax = float(dialog.tmax.text()) except ValueError as e: show_error('Unable to compute epochs...', info=str(e)) else: if dialog.baseline.isChecked(): try: a = float(float(dialog.a.text())) except ValueError: a = None try: b = float(float(dialog.b.text())) except ValueError: b = None baseline = (a, b) else: baseline = None self.auto_duplicate() self.model.epoch_data(selected, tmin, tmax, baseline) def evoke_data(self): """Compute the mean of epochs.""" self.auto_duplicate() self.model.evoke_data() def set_reference(self): """Set reference.""" dialog = ReferenceDialog(self) if dialog.exec_(): self.auto_duplicate() if dialog.average.isChecked(): self.model.set_reference("average") else: ref = [c.strip() for c in dialog.channellist.text().split(",")] self.model.set_reference(ref) def open_batch(self): """Open batch processing dialog.""" dialog = BatchDialog(self) dialog.exec_() def show_about(self): """Show About dialog.""" msg_box = QMessageBox(self) text = ("<h3>MNELAB</h3>" "<nobr><p>MNELAB is a graphical user interface for MNE.</p>" "</nobr>") msg_box.setText(text) mnelab_url = "github.com/cbrnr/mnelab" mne_url = "github.com/mne-tools/mne-python" text = (f'<nobr><p>This program uses MNE version {mne.__version__} ' f'(Python {".".join(str(k) for k in version_info[:3])}).</p>' f'</nobr>' f'<nobr><p>MNELAB repository: ' f'<a href=https://{mnelab_url}>{mnelab_url}</a></p></nobr>' f'<nobr><p>MNE repository: ' f'<a href=https://{mne_url}>{mne_url}</a></p></nobr>' f'<p>Licensed under the BSD 3-clause license.</p>' f'<p>Copyright 2017-2019 by Clemens Brunner.</p>') msg_box.setInformativeText(text) msg_box.exec_() def show_about_qt(self): """Show About Qt dialog.""" QMessageBox.aboutQt(self, "About Qt") def auto_duplicate(self): # if current data is stored in a file create a new data set if self.model.current["fname"]: self.model.duplicate_data() # otherwise ask the user else: msg = QMessageBox.question(self, "Overwrite existing data set", "Overwrite existing data set?") if msg == QMessageBox.No: # create new data set self.model.duplicate_data() def _add_recent(self, fname): """Add a file to recent file list. Parameters ---------- fname : str File name. """ if fname in self.recent: # avoid duplicates self.recent.remove(fname) self.recent.insert(0, fname) while len(self.recent) > MAX_RECENT: # prune list self.recent.pop() write_settings(recent=self.recent) if not self.recent_menu.isEnabled(): self.recent_menu.setEnabled(True) def _remove_recent(self, fname): """Remove file from recent file list. Parameters ---------- fname : str File name. """ if fname in self.recent: self.recent.remove(fname) write_settings(recent=self.recent) if not self.recent: self.recent_menu.setEnabled(False) @pyqtSlot(QModelIndex) def _update_data(self, selected): """Update index and information based on the state of the sidebar. Parameters ---------- selected : QModelIndex Index of the selected row. """ if selected.row() != self.model.index: self.model.index = selected.row() self.data_changed() @pyqtSlot(QModelIndex, QModelIndex) def _update_names(self, start, stop): """Update names in DataSets after changes in sidebar.""" for index in range(start.row(), stop.row() + 1): self.model.data[index]["name"] = self.names.stringList()[index] @pyqtSlot() def _update_recent_menu(self): self.recent_menu.clear() for recent in self.recent: self.recent_menu.addAction(recent) @pyqtSlot(QAction) def _load_recent(self, action): self.model.load(action.text()) @pyqtSlot() def _toggle_statusbar(self): if self.statusBar().isHidden(): self.statusBar().show() else: self.statusBar().hide() write_settings(statusbar=not self.statusBar().isHidden()) @pyqtSlot(QDropEvent) def dragEnterEvent(self, event): if event.mimeData().hasUrls(): event.acceptProposedAction() @pyqtSlot(QDropEvent) def dropEvent(self, event): mime = event.mimeData() if mime.hasUrls(): urls = mime.urls() for url in urls: self.model.load(url.toLocalFile()) @pyqtSlot(QEvent) def closeEvent(self, event): """Close application. Parameters ---------- event : QEvent Close event. """ write_settings(geometry=self.saveGeometry(), state=self.saveState()) if self.model.history: print("\nCommand History") print("===============") print("\n".join(self.model.history)) QApplication.quit() def eventFilter(self, source, event): # currently the only source is the raw plot window if event.type() == QEvent.Close: self.data_changed() return QObject.eventFilter(self, source, event)