Exemple #1
0
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}&nbsp;({value})")
            else:
                pkgs.append(f"{key}&nbsp;(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)
Exemple #2
0
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()
Exemple #3
0
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)
Exemple #4
0
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)
Exemple #5
0
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)
Exemple #6
0
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)