예제 #1
0
파일: mainwindow.py 프로젝트: cbrnr/mnelab
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
        self.resize(settings["size"])
        self.move(settings["pos"])

        # remove None entries from self.recent
        self.recent = [recent for recent in self.recent if recent is not None]

        # trigger theme setting
        QIcon.setThemeSearchPaths([str(Path(__file__).parent / "icons")])
        self.event(QEvent(QEvent.PaletteChange))

        self.actions = {}  # contains all actions

        # initialize menus
        file_menu = self.menuBar().addMenu("&File")
        icon = QIcon.fromTheme("open-file")
        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.fromTheme("meta-info")
        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 ext, description in writers.items():
            action = "export_data" + ext.replace(".", "_")
            self.actions[action] = self.export_menu.addAction(
                f"{ext[1:].upper()} ({description[1]})...",
                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["xdf_chunks"] = file_menu.addAction(
            "Show XDF chunks...", self.xdf_chunks)
        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(
            "P&ick channels...", self.pick_channels)
        icon = QIcon.fromTheme("chan-props")
        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)
        self.actions["append_data"] = edit_menu.addAction(
            "Appen&d data...", self.append_data)

        plot_menu = self.menuBar().addMenu("&Plot")
        icon = QIcon.fromTheme("plot-data")
        self.actions["plot_data"] = plot_menu.addAction(
            icon, "&Data", self.plot_data)
        icon = QIcon.fromTheme("plot-psd")
        self.actions["plot_psd"] = plot_menu.addAction(
            icon, "&Power spectral density", self.plot_psd)
        icon = QIcon.fromTheme("plot-locations")
        self.actions["plot_locations"] = plot_menu.addAction(
            icon, "&Channel locations", self.plot_locations)
        self.actions["plot_erds"] = plot_menu.addAction(
            "&ERDS maps...", self.plot_erds)
        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.fromTheme("filter-data")
        self.actions["filter"] = tools_menu.addAction(icon, "&Filter data...",
                                                      self.filter_data)
        icon = QIcon.fromTheme("find-events")
        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)
        self.actions["annotations_from_events"] = tools_menu.addAction(
            "Create annotations from events", self.annotations_from_events)
        tools_menu.addSeparator()
        nirs_menu = tools_menu.addMenu("NIRS")
        self.actions["convert_od"] = nirs_menu.addAction(
            "Convert to &optical density", self.convert_od)
        self.actions["convert_bl"] = nirs_menu.addAction(
            "Convert to &haemoglobin", self.convert_bl)

        tools_menu.addSeparator()
        icon = QIcon.fromTheme("run-ica")
        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.fromTheme("epoch-data")
        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", "xdf_chunks", "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_locations"])
        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.toolbar.setMovable(False)
        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(f"Total Memory: {mb:.2f} 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)
            locations = has_locations(self.model.current["data"].info)
            self.actions["plot_locations"].setEnabled(enabled and locations)
            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_erds"].setEnabled(
                enabled and self.model.current["dtype"] == "epochs")
            self.actions["plot_ica_components"].setEnabled(enabled and ica
                                                           and locations)
            self.actions["plot_ica_sources"].setEnabled(enabled and ica)
            self.actions["interpolate_bads"].setEnabled(enabled and locations
                                                        and bads)
            self.actions["events"].setEnabled(enabled and events)
            self.actions["events_from_annotations"].setEnabled(enabled
                                                               and annot)
            self.actions["annotations_from_events"].setEnabled(enabled
                                                               and events)
            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")
            append = bool(self.model.get_compatibles())
            self.actions["append_data"].setEnabled(
                enabled and append
                and (self.model.current["dtype"] in ("raw", "epochs")))
            self.actions["meta_info"].setEnabled(
                enabled
                and self.model.current["ftype"] in ["XDF", "XDFZ", "XDF.GZ"])
            self.actions["convert_od"].setEnabled(
                len(
                    mne.pick_types(self.model.current["data"].info,
                                   fnirs="fnirs_cw_amplitude")))
            self.actions["convert_bl"].setEnabled(
                len(
                    mne.pick_types(self.model.current["data"].info,
                                   fnirs="fnirs_od")))
            # disable unsupported exporters for epochs (all must support raw)
            if self.model.current["dtype"] == "epochs":
                for ext, description in writers.items():
                    action = "export_data" + ext.replace(".", "_")
                    if "epoch" in description[2]:
                        self.actions[action].setEnabled(True)
                    else:
                        self.actions[action].setEnabled(False)
        # 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")[0]
        if fname:
            if not (Path(fname).is_file() or Path(fname).is_dir()):
                self._remove_recent(fname)
                QMessageBox.critical(self, "File does not exist",
                                     f"File {fname} does not exist anymore.")
                return

            ext = "".join(Path(fname).suffixes)

            if any([ext.endswith(e) for e in (".xdf", ".xdfz", ".xdf.gz")]):
                rows, disabled = [], []
                for idx, s in enumerate(resolve_streams(fname)):
                    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))
                    srate = "effective" if dialog.effective_srate else "nominal"
                    prefix_markers = dialog.prefix_markers
                    kwargs = {}
                    if srate == "nominal":
                        kwargs["srate"] = srate
                    if prefix_markers:
                        kwargs["prefix_markers"] = prefix_markers
                    self.model.load(fname, stream_id=stream_id, **kwargs)
            else:  # all other file formats
                try:
                    self.model.load(fname)
                except FileNotFoundError as e:
                    QMessageBox.critical(self, "File not found", str(e))
                except ValueError 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=ffilter)[0]
        if fname:
            f(fname)

    def xdf_chunks(self):
        """Show XDF chunks."""
        fname = QFileDialog.getOpenFileName(self,
                                            "Select XDF file",
                                            filter="*.xdf *.xdfz *.xdf.gz")[0]
        if fname:
            chunks = list_chunks(fname)
            dialog = XDFChunksDialog(self, chunks, fname)
            dialog.exec_()

    def export_file(self, f, text, ffilter="*"):
        """Export to file."""
        fname = QFileDialog.getSaveFileName(self, text, filter=ffilter)[0]
        if fname:
            if ffilter != "*":
                exts = [ext.replace("*", "") for ext in ffilter.split()]

                maxsuffixes = max([ext.count(".") for ext in exts])
                suffixes = Path(fname).suffixes
                for i in range(-maxsuffixes, 0):
                    ext = "".join(suffixes[i:])
                    if ext in exts:
                        return f(fname)
                fname = fname + exts[0]
                return 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 meta_info(self):
        """Show XDF meta info."""
        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 = list(set(channels) - set(picks))
            if drops:
                self.auto_duplicate()
                self.model.drop_channels(drops)
                self.model.history.append(f"data.drop_channels({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)
        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):
        """Crop 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()
            self.model.crop(dialog.start or 0, dialog.stop)

    def append_data(self):
        """Concatenate raw data objects to current one."""
        compatibles = self.model.get_compatibles()
        dialog = AppendDialog(self, compatibles)
        if dialog.exec():
            self.auto_duplicate()
            self.model.append_data(dialog.names)

    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)
        if events is not None:
            hist = f"data.plot(events=events, n_channels={nchan})"
        else:
            hist = f"data.plot(n_channels={nchan})"
        self.model.history.append(hist)
        win = fig.canvas.manager.window
        win.setWindowTitle(self.model.current["name"])
        win.statusBar().hide()  # not necessary since matplotlib 3.3
        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 = {}
        if self.model.current["dtype"] == "raw":
            kwds.update({"average": False, "spatial_colors": False})
        fig = self.model.current["data"].plot_psd(show=False, **kwds)
        if kwds:
            tmp = ", ".join(f"{key}={value}" for key, value in kwds.items())
            hist = f"data.plot_psd({tmp})"
        else:
            hist = "data.plot_psd()"
        self.model.history.append(hist)
        win = fig.canvas.manager.window
        win.setWindowTitle("Power spectral density")
        fig.show()

    def plot_locations(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.statusBar().hide()  # not necessary since matplotlib 3.3
        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 plot_erds(self):
        """Plot ERDS maps."""
        data = self.model.current["data"]
        t_range = [data.tmin, data.tmax]
        f_range = [1, data.info["sfreq"] / 2]

        dialog = ERDSDialog(self, t_range, f_range)

        if dialog.exec():
            freqs = np.arange(dialog.f1, dialog.f2, dialog.step)
            baseline = [dialog.b1, dialog.b2]
            times = [dialog.t1, dialog.t2]
            figs = plot_erds(data, freqs, freqs, baseline, times)
            for fig in figs:
                fig.show()

    def run_ica(self):
        """Run ICA calculation."""

        methods = ["Infomax"]
        if have["picard"]:
            methods.insert(0, "Picard")
        if have["sklearn"]:
            methods.append("FastICA")

        dialog = RunICADialog(self, self.model.current["data"].info["nchan"],
                              methods)

        if dialog.exec():
            calc = CalcDialog(self, "Calculating ICA", "Calculating ICA.")

            method = dialog.method.currentText().lower()
            exclude_bad_segments = dialog.exclude_bad_segments.isChecked()

            fit_params = {}
            if dialog.extended.isEnabled():
                fit_params["extended"] = dialog.extended.isChecked()
            if dialog.ortho.isEnabled():
                fit_params["ortho"] = dialog.ortho.isChecked()

            ica = mne.preprocessing.ICA(method=method, fit_params=fit_params)
            history = f"ica = mne.preprocessing.ICA(method='{method}'"
            if fit_params:
                history += f", fit_params={fit_params})"
            else:
                history += ")"
            self.model.history.append(history)

            pool = mp.Pool(processes=1)

            def callback(x):
                QMetaObject.invokeMethod(calc, "accept", Qt.QueuedConnection)

            res = pool.apply_async(
                func=ica.fit,
                args=(self.model.current["data"], ),
                kwds={"reject_by_annotation": exclude_bad_segments},
                callback=callback)
            pool.close()

            if not calc.exec():
                pool.terminate()
                print("ICA calculation aborted...")
            else:
                self.model.current["ica"] = res.get(timeout=1)
                self.model.history.append(
                    f"ica.fit(inst=raw, 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 annotations_from_events(self):
        self.model.annotations_from_events()

    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 convert_od(self):
        """Convert to optical density."""
        self.auto_duplicate()
        self.model.convert_od()

    def convert_bl(self):
        """Convert to haemoglobin."""
        self.auto_duplicate()
        self.model.convert_beer_lambert()

    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."""
        from . import __version__

        msg_box = QMessageBox(self)
        text = (
            f"<img src='{image_path('mnelab_logo.png')}'><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)")
        version = ".".join(str(k) for k in version_info[:3])
        text = (
            f"<nobr><p>This program uses Python {version} and the following packages:"
            f"</p></nobr><p>{', '.join(pkgs)}</p>"
            f"<nobr><p>MNELAB repository: <a href=https://{mnelab_url}>{mnelab_url}</a>"
            f"</p></nobr><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&ndash;2021 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)

    @Slot(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()
            self.model.history.append(f"data = datasets[{self.model.index}]")

    @Slot(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]

    @Slot()
    def _update_recent_menu(self):
        self.recent_menu.clear()
        for recent in self.recent:
            self.recent_menu.addAction(recent)

    @Slot(QAction)
    def _load_recent(self, action):
        self.open_data(fname=action.text())

    @Slot()
    def _toggle_toolbar(self):
        if self.toolbar.isHidden():
            self.toolbar.show()
        else:
            self.toolbar.hide()
        write_settings(toolbar=not self.toolbar.isHidden())

    @Slot()
    def _toggle_statusbar(self):
        if self.statusBar().isHidden():
            self.statusBar().show()
        else:
            self.statusBar().hide()
        write_settings(statusbar=not self.statusBar().isHidden())

    @Slot(QDropEvent)
    def dragEnterEvent(self, event):
        if event.mimeData().hasUrls():
            event.acceptProposedAction()

    @Slot(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))

    @Slot(QEvent)
    def closeEvent(self, event):
        """Close application.

        Parameters
        ----------
        event : QEvent
            Close event.
        """
        write_settings(size=self.size(), pos=self.pos())
        if self.model.history:
            print("\n# Command History\n")
            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)

    def event(self, ev):
        """Catch system events."""
        if ev.type() == QEvent.PaletteChange:  # detect theme switches
            style = interface_style()  # light or dark
            if style is not None:
                QIcon.setThemeName(style)
            else:
                QIcon.setThemeName("light")  # fallback
        return super().event(ev)
예제 #2
0
class Window(QMainWindow):
    def __init__(self):
        QMainWindow.__init__(self)

        self.images = []
        self.index = -1
        self.ratio = 1  # ratio for QLabel image
        self.mouse_position = None
        self.settings = None

        # Extensions
        self.extensions = []
        for format in QImageReader.supportedImageFormats():
            self.extensions.append(format.data().decode('utf-8'))

        # Filters
        self.filters = []
        for extension in self.extensions:
            self.filters.append('*.{0}'.format(str(extension)))

        # UI
        self.set_up_ui()

        # settings
        self.load_settings()

    def on_message_received(self, msg):
        """ on message received from single application

        Args:
            msg (string): file path
        """
        self.create_images(msg)
        self.display_image()

    def set_up_ui(self):
        # Status Bar
        self.status_bar = self.statusBar()
        self.label_name = QLabel()
        self.label_numero = QLabel()
        self.status_bar.addPermanentWidget(self.label_name, 1)
        self.status_bar.addPermanentWidget(self.label_numero, 0)

        # Main Window
        self.setWindowTitle('BaloViewer')
        self.setWindowIcon(QIcon('baloviewer.ico'))

        # Label image
        self.image = QLabel()
        self.image.setScaledContents(True)

        # Scroll area
        self.scroll_area = QScrollArea()
        self.scroll_area.setWidget(self.image)
        self.scroll_area.showMaximized()
        self.scroll_area.setFocusPolicy(Qt.FocusPolicy.NoFocus)
        self.scroll_area.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.scroll_area.viewport().installEventFilter(self)

        # image list
        self.image_gallery = ImageGallery()
        self.image_gallery.itemClicked.connect(self.image_gallery_clicked)
        self.image_gallery.viewport().installEventFilter(self)
        self.dock_widget = QDockWidget('Image Gallery', self)
        self.dock_widget.setWidget(self.image_gallery)
        self.dock_widget.setFloating(False)
        self.addDockWidget(Qt.LeftDockWidgetArea, self.dock_widget)

        # central widget
        self.setCentralWidget(self.scroll_area)

        # Action bar
        self.create_actions()
        self.create_menubar()
        self.create_toolbar()

        # option parser
        parser = OptionParser()
        parser.add_option("-f", "--file", dest="filename", help="open a file")
        (options, args) = parser.parse_args()
        parser_file = options.filename
        if parser_file is not None and os.path.isfile(parser_file):
            self.create_images(parser_file)
            self.display_image()

    def create_actions(self):
        # Action Open
        self.action_open = QAction(QIcon.fromTheme('document-open'), 'Open',
                                   self)
        self.action_open.setShortcut('Ctrl+O')
        self.action_open.setStatusTip('Open file')
        self.action_open.triggered.connect(self.open)

        # Action Save
        self.action_save = QAction(QIcon.fromTheme('document-save'), 'Save',
                                   self)
        self.action_save.setShortcut('Ctrl+S')
        self.action_save.setStatusTip('Save file')
        self.action_save.triggered.connect(self.save)

        # Action Copy
        self.action_copy = QAction(QIcon.fromTheme('edit-copy'), 'Copy', self)
        self.action_copy.setStatusTip('Copy')
        self.action_copy.triggered.connect(self.copy)

        # Action move
        self.action_move = QAction(QIcon.fromTheme('edit-cut'), 'Move', self)
        self.action_move.setStatusTip('Move')
        self.action_move.triggered.connect(self.move)

        # Action Delete
        self.action_delete = QAction(QIcon.fromTheme('edit-delete'), 'Delete',
                                     self)
        self.action_delete.setStatusTip('Delete')
        self.action_delete.triggered.connect(self.delete)

        # Action Quit
        self.action_quit = QAction(QIcon.fromTheme('application-exit'), 'Quit',
                                   self)
        self.action_quit.setShortcut('Ctrl+Q')
        self.action_quit.setStatusTip('Quit')
        self.action_quit.triggered.connect(self.close)

        # Action Rotate left
        self.action_rotate_left = QAction(
            QIcon.fromTheme('object-rotate-left'), 'Rotate left', self)
        self.action_rotate_left.setStatusTip('Rotate left')
        self.action_rotate_left.triggered.connect(self.rotate_left)

        # Action Rotate right
        self.action_rotate_right = QAction(
            QIcon.fromTheme('object-rotate-right'), 'Rotate right', self)
        self.action_rotate_right.setStatusTip('Rotate right')
        self.action_rotate_right.triggered.connect(self.rotate_right)

        # Action Mirror
        self.action_flip_horizontal = QAction(
            QIcon.fromTheme('object-flip-horizontal'), 'Flip horizontally',
            self)
        self.action_flip_horizontal.setStatusTip('Flip horizontally')
        self.action_flip_horizontal.triggered.connect(self.flip_horizontal)

        # Action Flip vertical
        self.action_flip_vertical = QAction(
            QIcon.fromTheme('object-flip-vertical'), 'Flip vertically', self)
        self.action_flip_vertical.setStatusTip('Flip vertically')
        self.action_flip_vertical.triggered.connect(self.flip_vertical)

        # Action Previous image
        self.action_previous_image = QAction(QIcon.fromTheme('go-previous'),
                                             'Previous image', self)
        self.action_previous_image.setStatusTip('Previous image')
        self.action_previous_image.triggered.connect(self.previous_image)

        # Action Full screen
        self.action_fullscreen = QAction(QIcon.fromTheme('view-fullscreen'),
                                         'Full screen', self)
        self.action_fullscreen.setStatusTip('Full screen')
        self.action_fullscreen.triggered.connect(self.fullscreen)

        # Action Normal size
        self.action_normal_size = QAction(QIcon.fromTheme('zoom-original'),
                                          'Normal size', self)
        self.action_normal_size.setStatusTip('Normal Size')
        self.action_normal_size.triggered.connect(self.normal_size)

        # Action Fit Screen
        self.action_fit_screen = QAction(QIcon.fromTheme('zoom-fit-best'),
                                         'Fit to screen', self)
        self.action_fit_screen.setStatusTip('Fit to screen')
        self.action_fit_screen.setCheckable(True)
        self.action_fit_screen.triggered.connect(self.fit_screen)

        # Action Zoom in
        self.action_zoom_in = QAction(QIcon.fromTheme('zoom-in'), 'Zoom in',
                                      self)
        self.action_zoom_in.setStatusTip('Zoom in')
        self.action_zoom_in.triggered.connect(self.zoom_in)

        # Action Zoom out
        self.action_zoom_out = QAction(QIcon.fromTheme('zoom-out'), 'Zoom out',
                                       self)
        self.action_zoom_out.setStatusTip('Zoom out')
        self.action_zoom_out.triggered.connect(self.zoom_out)

        # Action Fit height
        self.action_fit_vertical = QAction('Fit vertically', self)
        self.action_fit_vertical.setStatusTip('Fit vertically')
        self.action_fit_vertical.setCheckable(True)
        self.action_fit_vertical.triggered.connect(self.fit_height)

        # Action Fit width
        self.action_fit_horizontal = QAction('Fit horizontally', self)
        self.action_fit_horizontal.setStatusTip('Fit horizontally')
        self.action_fit_horizontal.setCheckable(True)
        self.action_fit_horizontal.triggered.connect(self.fit_width)

        # Action Fit width
        self.action_fit_horizontal = QAction('Fit horizontally', self)
        self.action_fit_horizontal.setStatusTip('Fit horizontally')
        self.action_fit_horizontal.setCheckable(True)
        self.action_fit_horizontal.triggered.connect(self.fit_width)

        # Action Image list
        self.action_image_gallery = QAction('Image gallery', self)
        self.action_image_gallery.setStatusTip('Image gallery')
        self.action_image_gallery.setCheckable(True)
        self.action_image_gallery.triggered.connect(
            self.image_gallery_triggered)

        # Action Next_image
        self.action_next_image = QAction(QIcon.fromTheme('go-next'),
                                         'Next image', self)
        self.action_next_image.setStatusTip('Next image')
        self.action_next_image.triggered.connect(self.next_image)

        # Action First image
        self.action_first_image = QAction(QIcon.fromTheme('go-first'),
                                          'First image', self)
        self.action_first_image.setStatusTip('First image')
        self.action_first_image.triggered.connect(self.first_image)

        # Action Last image
        self.action_last_image = QAction(QIcon.fromTheme('go-last'),
                                         'Last image', self)
        self.action_last_image.setStatusTip('Last image')
        self.action_last_image.triggered.connect(self.last_image)

        # Action About
        self.action_about = QAction(QIcon.fromTheme('help-about'), 'About',
                                    self)
        self.action_about.setStatusTip('About')
        self.action_about.triggered.connect(self.about)

    def create_menubar(self):
        self.menubar = self.menuBar()

        # File
        self.menu_file = self.menubar.addMenu('File')
        self.menu_file.addAction(self.action_open)
        self.menu_file.addAction(self.action_save)
        self.menu_file.addSeparator()
        self.menu_file.addAction(self.action_copy)
        self.menu_file.addAction(self.action_move)
        self.menu_file.addAction(self.action_delete)
        self.menu_file.addSeparator()
        self.menu_file.addAction(self.action_quit)

        # Edit
        self.menu_edit = self.menubar.addMenu('Edit')
        self.menu_edit.addAction(self.action_rotate_left)
        self.menu_edit.addAction(self.action_rotate_right)
        self.menu_edit.addSeparator()
        self.menu_edit.addAction(self.action_flip_horizontal)
        self.menu_edit.addAction(self.action_flip_vertical)

        # View
        self.menu_view = self.menubar.addMenu('View')
        self.menu_view.addAction(self.action_fullscreen)
        self.menu_view.addAction(self.action_normal_size)
        self.menu_view.addAction(self.action_fit_screen)
        self.menu_view.addSeparator()
        self.menu_view.addAction(self.action_zoom_in)
        self.menu_view.addAction(self.action_zoom_out)
        self.menu_view.addSeparator()
        self.menu_view.addAction(self.action_fit_vertical)
        self.menu_view.addAction(self.action_fit_horizontal)
        self.menu_view.addSeparator()
        self.menu_view.addAction(self.action_image_gallery)

        # Go
        self.menu_go = self.menubar.addMenu('Go')
        self.menu_go.addAction(self.action_previous_image)
        self.menu_go.addAction(self.action_next_image)
        self.menu_go.addSeparator()
        self.menu_go.addAction(self.action_first_image)
        self.menu_go.addAction(self.action_last_image)

        # About
        self.menu_about = self.menubar.addMenu('About')
        self.menu_about.addAction(self.action_about)

    def create_toolbar(self):
        self.toolbar = self.addToolBar('Tool bar')

        self.toolbar.addAction(self.action_open)
        self.toolbar.addAction(self.action_save)
        self.toolbar.addSeparator()
        self.toolbar.addAction(self.action_fullscreen)
        self.toolbar.addAction(self.action_normal_size)
        self.toolbar.addAction(self.action_fit_screen)
        self.toolbar.addSeparator()
        self.toolbar.addAction(self.action_zoom_in)
        self.toolbar.addAction(self.action_zoom_out)
        self.toolbar.addSeparator()
        self.toolbar.addAction(self.action_rotate_left)
        self.toolbar.addAction(self.action_rotate_right)
        self.toolbar.addSeparator()
        self.toolbar.addAction(self.action_first_image)
        self.toolbar.addAction(self.action_previous_image)
        self.toolbar.addAction(self.action_next_image)
        self.toolbar.addAction(self.action_last_image)
        self.toolbar.addSeparator()
        self.toolbar.addAction(self.action_copy)
        self.toolbar.addAction(self.action_move)

    def load_settings(self):
        self.settings = QSettings()
        check_state = self.settings.value('view/image_gallery',
                                          True,
                                          type=bool)
        self.action_image_gallery.setChecked(check_state)
        self.image_gallery_triggered()

    def contextMenuEvent(self, QContextMenuEvent):
        menu = QMenu()
        menu.addAction(self.action_fullscreen)
        menu.addSeparator()
        menu.addAction(self.action_image_gallery)
        menu.addSeparator()
        menu.addAction(self.action_previous_image)
        menu.addAction(self.action_next_image)
        menu.addSeparator()
        menu.addAction(self.action_normal_size)
        menu.addAction(self.action_fit_screen)
        menu.addAction(self.action_fit_vertical)
        menu.addAction(self.action_fit_horizontal)
        menu.addSeparator()
        menu.addAction(self.action_zoom_in)
        menu.addAction(self.action_zoom_out)
        menu.addSeparator()
        menu.addAction(self.action_copy)
        menu.addAction(self.action_move)
        menu.addSeparator()
        menu.addAction(self.action_delete)
        menu.exec_(QContextMenuEvent.globalPos())

    def eventFilter(self, obj, event):
        """ filter events for wheel events

        Args:
            obj (QWidget): scroll_area
            event (QEvent): event
        """

        # try:
        if event.type() == QEvent.Wheel:
            if event.angleDelta().y() < 0:
                self.next_image()
            else:
                self.previous_image()

            return True
        elif event.type() == QEvent.MouseButtonPress and event.button(
        ) == Qt.RightButton:
            index = self.image_gallery.select_row_pos()
            if index > -1:
                self.index = index
                self.display_image()
                return True
        # pass the event on to the parent class
        return super(QMainWindow, self).eventFilter(obj, event)

    def keyPressEvent(self, event):
        key = event.key()
        if key == Qt.Key_Delete:
            self.delete()
        elif key == Qt.Key_Left:
            self.previous_image()
        elif key == Qt.Key_Right:
            self.next_image()
        elif key == Qt.Key_PageUp:
            self.first_image()
        elif key == Qt.Key_PageDown:
            self.last_image()
        elif key == Qt.Key_Escape and self.isFullScreen():
            self.fullscreen()
        else:
            QWidget.keyPressEvent(self, event)

    def mouseDoubleClickEvent(self, QMouseEvent):
        self.fullscreen()

    def mousePressEvent(self, QMouseEvent):
        self.mouse_position = QMouseEvent.pos()

    def mouseMoveEvent(self, QMouseEvent):
        diff = QPoint(QMouseEvent.pos() - self.mouse_position)
        self.mouse_position = QMouseEvent.pos()
        self.scroll_area.verticalScrollBar().setValue(
            self.scroll_area.verticalScrollBar().value() - diff.y())
        self.scroll_area.horizontalScrollBar().setValue(
            self.scroll_area.horizontalScrollBar().value() - diff.x())

    def resizeEvent(self, event):
        if not self.index == -1:
            self.display_image()

    def create_images(self, filename):
        """Create image list

        Args:
            filename (string): file from which to retrieve the list of images in the folder
        """

        self.images.clear()
        # get images only with an allowed extension
        for ext in self.extensions:
            self.images += glob.glob(
                os.path.join(
                    glob.escape(os.path.dirname(filename)),
                    '*.' + ''.join('[%s%s]' % (e.lower(), e.upper())
                                   for e in ext)))

        self.images.sort()
        if filename in self.images:
            self.index = self.images.index(filename)
        else:
            self.index = -1

        # iamge list
        self.image_gallery.add_images(self.images)

    def remove_index(self):
        """ remove file from list images and display next or previous image
        """

        del self.images[self.index]
        self.image_gallery.remove_row(self.index)

        if len(self.images) == 0:
            self.images.clear()
            self.index = -1
            self.image.clear()
            self.image.resize(self.image.minimumSizeHint())
        elif self.index < len(self.images) - 1:
            self.display_image()
        else:
            self.index = len(self.images) - 1
            self.display_image()

    def display_image(self):
        if not self.index == -1:
            self.image.clear()
            self.image.resize(self.image.minimumSizeHint())

            file = self.images[self.index]
            if os.path.isfile(file):
                self.label_name.setText(file)
                self.label_numero.setText(
                    str(self.index + 1) + ' / ' + str(len(self.images)))

                # image list
                self.image_gallery.select_row(self.index)

                image_reader = QImageReader(file)
                if image_reader.imageCount() > 1:
                    # Animated image
                    movie = QMovie(file)
                    movie.setCacheMode(QMovie.CacheAll)
                    movie.jumpToFrame(0)
                    movie_size = movie.currentPixmap().size()
                    self.image.setMovie(movie)
                    self.image.resize(movie_size)
                    movie.start()
                else:
                    self.image.setPixmap(QPixmap(file))
                    self.image.resize(self.image.pixmap().size())

                # fit image
                if self.action_fit_screen.isChecked():
                    self.fit_screen()
                elif self.action_fit_horizontal.isChecked():
                    self.fit_width()
                elif self.action_fit_vertical.isChecked():
                    self.fit_height()

                else:
                    self.ratio = 1.0

                self.action_zoom_in.setEnabled(True)
                self.action_zoom_out.setEnabled(True)

                # scrollbar position
                self.scroll_area.verticalScrollBar().setSliderPosition(0)
                self.scroll_area.horizontalScrollBar().setSliderPosition(0)

    def resize_image(self):
        if self.action_fit_screen.isChecked():
            self.fit_screen()
        elif self.action_fit_horizontal.isChecked():
            self.fit_width()
        elif self.action_fit_vertical.isChecked():
            self.fit_height()
        elif self.image.pixmap():
            self.image.resize(self.ratio * self.image.pixmap().size())
        elif movie := self.image.movie():
            movie.jumpToFrame(0)
            movie_size = movie.currentPixmap().size()
            self.image.resize(self.ratio * movie_size)
예제 #3
0
class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        #self.dbTableConnection = SqLite()

        self.mainWidget = QWidget()

        self.setWindowTitle("Video Game Scanner Inventory")
        self.left = 2500
        self.top = 100
        self.width = 1200
        self.height = 900

        self.setGeometry(self.left, self.top, self.width, self.height)
        self.mainWindowGrid = QGridLayout()

        self.barcode = Barcode()
        self.barcodeNumber = None

        # Stores info on a single game
        self.videoGameInfo = None
        # A List of video games
        self.videoGameInfoList = []

        self.gameVariantDict = dict([])
        self.variant = 'Standard'

        self.dbHandler = SqlHandler()

        # Barcode Number and Game Variant Buttons
        self.barcodeWindow = QWidget()
        self.setBackgroundColor(self.barcodeWindow, 'orange')

        self.barcodeListLayout = QGridLayout()
        self.barcodeScannerInput = BarscodeScannerInput()
        self.barcodeScannerInput.textChanged.connect(self.updateGamePriceTable)

        self.variantButtonList = []
        self.variantButtons = VideoGameVariantButtons()
        self.standardBarcode = ''

        self.barcodeListLayout.addWidget(self.barcodeScannerInput, 0, 0)
        self.barcodeListLayout.addWidget(self.variantButtons, 1, 0)
        self.barcodeWindow.setLayout(self.barcodeListLayout)

        self.mainWindowGrid.addWidget(self.barcodeWindow, 0, 1, 2, 2)

        # Console Label - Unused (potentially for game logos)
        self.barcodeWindow2 = QWidget()
        self.setBackgroundColor(self.barcodeWindow2, 'blue')

        self.flashyBoxLayout = QGridLayout()
        self.barcodeWindow2.setLayout(self.flashyBoxLayout)

        self.mainWindowGrid.addWidget(self.barcodeWindow2, 0, 3, 2, 2)

        # Side Buttons
        self.barcodeWindow3 = QWidget()
        self.setBackgroundColor(self.barcodeWindow3, 'grey')
        self.mainWindowGrid.addWidget(self.barcodeWindow3, 0, 0, 4, 1)
        #------ Button List
        self.layout = QVBoxLayout()

        self.b1 = QPushButton("Process Table")
        self.layout.addWidget(self.b1)
        self.b1.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
        self.b1.clicked.connect(self.processTableAndInfo)

        self.b2 = QPushButton("Button2")
        self.layout.addWidget(self.b2)
        self.b2.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)

        self.b3 = QPushButton("Button3")
        self.layout.addWidget(self.b3)
        self.b3.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)

        self.b4 = QPushButton("Button4")
        self.layout.addWidget(self.b4)
        self.b4.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)

        self.barcodeWindow3.setLayout(self.layout)

        self.barcodeWindow4 = QWidget()
        self.setBackgroundColor(self.barcodeWindow4, 'pink')

        self.mainWindowGrid.addWidget(self.barcodeWindow4, 2, 1, 2, 4)

        ################ Pricing and Table Widget ####################################

        self.gridLayout = QGridLayout()
        self.barcodeWindow4.setLayout(self.gridLayout)

        #------- unprocessed table values -----------------
        self.gamePriceWidget = QWidget()
        self.setBackgroundColor(self.gamePriceWidget, 'blue')
        self.gamePriceTableLayout = QVBoxLayout()
        self.gamePriceTable = TableView()
        self.gamePriceTable.setSizePolicy(QSizePolicy.Preferred,
                                          QSizePolicy.Preferred)
        self.gamePriceTableLayout.addWidget(self.gamePriceTable)
        self.gamePriceWidget.setLayout(self.gamePriceTableLayout)
        self.gridLayout.addWidget(self.gamePriceWidget, 0, 0, 2, 6)

        #------- verify game info labels ------------------
        self.gameInfoWidget = QWidget()
        self.setBackgroundColor(self.gameInfoWidget, 'indigo')
        self.gameInfoLayout = QGridLayout()
        self.gameInfoWidget.setLayout(self.gameInfoLayout)
        self.gridLayout.addWidget(self.gameInfoWidget, 3, 0, 1, 6)

        self.gameNameBox = QLineEdit()
        self.gameSystemBox = QLineEdit()
        self.gameUsedPrice = QLineEdit()
        self.gameCompletePrice = QLineEdit()
        self.gameNewPrice = QLineEdit()
        self.totalPriceLabel = QLabel()

        self.totalPriceLabel.setFont(QFont('Arial', 15))

        self.setBackgroundColor(self.totalPriceLabel, 'beige')

        self.gameNameBox.setAlignment(QtCore.Qt.AlignCenter)
        self.gameSystemBox.setAlignment(QtCore.Qt.AlignCenter)
        self.gameUsedPrice.setAlignment(QtCore.Qt.AlignCenter)
        self.gameCompletePrice.setAlignment(QtCore.Qt.AlignCenter)
        self.gameNewPrice.setAlignment(QtCore.Qt.AlignCenter)

        self.gameInfoLayout.addWidget(self.gameNameBox, 0, 0, 1, 3)
        self.gameInfoLayout.addWidget(self.gameSystemBox, 1, 0, 1, 3)
        self.gameInfoLayout.addWidget(self.totalPriceLabel, 0, 4, 2, 2)
        self.gameInfoLayout.addWidget(self.gameUsedPrice, 2, 0, 1, 2)
        self.gameInfoLayout.addWidget(self.gameCompletePrice, 2, 2, 1, 2)
        self.gameInfoLayout.addWidget(self.gameNewPrice, 2, 4, 1, 2)

        #------------Button Box --------------------------------
        self.horizontalGroupBox = QGroupBox("Which Price To Use?")
        self.buttonLayout = QGridLayout()
        self.horizontalGroupBox.setLayout(self.buttonLayout)
        self.gridLayout.addWidget(self.horizontalGroupBox, 6, 0, 1, 6)

        self.buttonLoose = QPushButton('Loose Price', self)
        self.buttonLoose.clicked.connect(self.on_click)
        self.buttonLayout.addWidget(self.buttonLoose, 0, 0, 1, 1)

        self.buttonComplete = QPushButton('Complete Price', self)
        self.buttonComplete.clicked.connect(self.on_click)
        self.buttonLayout.addWidget(self.buttonComplete, 0, 1, 1, 1)

        self.buttonNew = QPushButton('New Price', self)
        self.buttonNew.clicked.connect(self.on_click)
        self.buttonLayout.addWidget(self.buttonNew, 0, 2, 1, 1)

        self.label1 = QLabel('Alternative Bulk Price')
        self.label1.setAlignment(QtCore.Qt.AlignCenter)
        self.buttonLayout.addWidget(self.label1, 1, 1, 1, 1)

        self.label2 = QLabel('Alternative Single Price')
        self.label2.setAlignment(QtCore.Qt.AlignCenter)
        self.buttonLayout.addWidget(self.label2, 1, 2, 1, 1)

        self.buttonBadinfo = QPushButton('Bad Info', self)
        self.buttonLayout.addWidget(self.buttonBadinfo, 2, 0, 1, 1)

        self.bulkPrice = QLineEdit()
        self.buttonLayout.addWidget(self.bulkPrice, 2, 1, 1, 1)
        self.bulkPrice.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
        self.bulkPrice.setAlignment(QtCore.Qt.AlignCenter)

        self.singleAltPrice = QLineEdit('')
        self.buttonLayout.addWidget(self.singleAltPrice, 2, 2, 1, 1)
        self.singleAltPrice.setSizePolicy(QSizePolicy.Preferred,
                                          QSizePolicy.Fixed)
        self.singleAltPrice.setAlignment(QtCore.Qt.AlignCenter)

        self.bulkPriceCheckbox = QCheckBox('Bulk Price Enabled')
        self.buttonLayout.addWidget(self.bulkPriceCheckbox, 1, 0)
        #-------------------------------------------------------------

        ##################################################

        self.mainWidget.setLayout(self.mainWindowGrid)
        self.setCentralWidget(self.mainWidget)

        self.show()

    def setBackgroundColor(self, widget, color):
        widget.setAutoFillBackground(True)
        palette = widget.palette()
        palette.setColor(QPalette.Window, QColor(color))
        widget.setPalette(palette)

    def closeEvent(self, event):
        print('I Quit')
        self.dbHandler.conn.close()
        QApplication.quit()

    #When you click 'Loose Price','Complete Price','New Price'
    #This slot puts that info on the table
    @Slot()
    def on_click(self):
        mp.putGameInfoInTable(self,
                              self.sender().text(), self.gamePriceTable,
                              self.barcodeNumber)
        self.barcodeScannerInput.setFocus()

    #Once a barcod has star, this slot processes it
    @Slot(str)
    def updateGamePriceTable(self, barcode):
        if ('*' in barcode):
            self.barcodeNumber = barcode.strip('*')
            self.standardBarcode = self.barcodeNumber
            self.barcodeScannerInput.clear()
            print(barcode)
            mp.readBarcodesFromMain(self, self.barcodeNumber,
                                    self.barcodeNumber)

    # Clicking Process Table button - clears the table and enters is into the database
    @Slot()
    def processTableAndInfo(self):
        if (len(self.videoGameInfoList) < 1):
            return
        self.dbHandler.processMainWindowTable(self.videoGameInfoList,
                                              self.gamePriceTable)
        self.bulkPrice.clear()
        self.totalPriceLabel.clear()

    # Some games variants with different prices. When clicking on a button, this slot handles swapping prices
    @Slot()
    def variantClicked(self):
        self.variant = self.sender().text()
        variantURL = self.gameVariantDict[self.sender().text()]
        items = (self.variantButtons.gameVariantLayout.itemAt(i)
                 for i in range(self.variantButtons.gameVariantLayout.count()))
        for btn in items:
            btn.widget().deleteLater()
        mp.readBarcodesFromMain(self, variantURL, self.standardBarcode)
        pass
예제 #4
0
class MainWidget(QWidget):
    data = None
    current_question = None
    current_correct_answer = None
    buttons_choices = None
    question_mode = None
    answer_mode = None
    correct_questions = 0
    total_questions = 0

    def __init__(self):
        super().__init__()

        # Create Widgets
        self.question = QLabel(Text="Press Start",
                               Alignment=QtCore.Qt.AlignCenter,
                               Font=QtGui.QFont("", 30))

        self.label_correct = QLabel(Alignment=QtCore.Qt.AlignCenter)

        self.button_quit = QPushButton("Quit", Visible=False)
        self.button_start = QPushButton("Start")
        lcd_height = 40
        self.lcd_total = QLCDNumber(SegmentStyle=QLCDNumber.Flat, FixedHeight=lcd_height)
        self.lcd_score = QLCDNumber(SegmentStyle=QLCDNumber.Flat, FixedHeight=lcd_height)

        self.stack = QStackedWidget()
        self.page1 = Page1Widget()
        self.page2 = Page2Widget()
        self.page3 = Page3Widget()
        self.stack.addWidget(self.page1)
        self.stack.addWidget(self.page2)
        self.stack.addWidget(self.page3)

        # Make Layout
        self.layout = QGridLayout(self)
        self.layout.addWidget(self.question, 2, 0, 1, 2)
        self.layout.addWidget(self.label_correct, 1, 0, 1, 2)
        self.layout.addWidget(self.lcd_total, 0, 0)
        self.layout.addWidget(self.lcd_score, 0, 1)
        self.layout.addWidget(self.stack, 3, 0, 1, 2)
        self.layout.addWidget(self.button_quit, 4, 0)
        self.layout.addWidget(self.button_start, 4, 1)

        # Connect Callbacks
        self.button_quit.clicked.connect(self.on_quit_pressed)
        self.button_start.clicked.connect(self.on_start_pressed)
        self.page2.ans_buttons[0].clicked.connect(self.on_ans_button0_clicked)
        self.page2.ans_buttons[1].clicked.connect(self.on_ans_button1_clicked)
        self.page2.ans_buttons[2].clicked.connect(self.on_ans_button2_clicked)
        self.page3.line_edit.returnPressed.connect(self.on_answer_given)

    def update_questions_from_selection(self):
        selected_groups = []
        for i in range(self.page1.list_groups.count()):
            item = self.page1.list_groups.item(i)
            if item.checkState():
                selected_groups.append(item.text())
        if len(selected_groups) == 0:
            self.data = data
        else:
            self.data = data.loc[data['Group'].isin(selected_groups), :]

    def on_quit_pressed(self):
        self.button_start.setVisible(True)
        self.button_quit.setVisible(False)
        self.question.clear()
        self.stack.setCurrentIndex(0)

    def on_start_pressed(self):
        self.update_questions_from_selection()
        self.button_start.setVisible(False)
        self.button_quit.setVisible(True)
        self.total_questions = -1
        self.correct_questions = 0
        self.stack.setCurrentIndex(int(self.page1.spinbox_level.text()))
        self.question_mode = MODES[self.page1.dropdown_quest.currentIndex()]
        self.answer_mode = MODES[self.page1.dropdown_ans.currentIndex()]
        self.update_question()

    def update_score(self):
        self.lcd_total.display(self.total_questions)
        self.lcd_score.display(self.correct_questions)

    def get_also_string(self):
        also_mode = self.page1.dropdown_also.currentText()
        if also_mode == "----":
            return ""
        return self.current_question[also_mode]

    def update_question(self):
        self.total_questions += 1
        self.update_score()
        picked_questions = self.data.sample(frac=1).iloc[:3]
        self.current_question = picked_questions.iloc[0]
        self.current_correct_answer = self.current_question[self.answer_mode]
        also_string = "   " + self.get_also_string()
        self.question.setText(self.current_question[self.question_mode] + also_string)

        self.buttons_choices = list(picked_questions[self.answer_mode])
        random.shuffle(self.buttons_choices)
        for button, answer in zip(self.page2.ans_buttons, self.buttons_choices):
            button.setText(answer)

    def correct_answer(self):
        self.correct_questions += 1
        self.label_correct.setText("Correct!")
        self.label_correct.setStyleSheet("QLabel { background-color : green;}")

    def wrong_answer(self):
        wrong_string = (f"Wrong: {self.current_question[self.question_mode]} "
                        f"= {self.current_correct_answer}")
        self.label_correct.setText(wrong_string)
        self.label_correct.setStyleSheet("QLabel { background-color : red;}")

    def on_answer_given(self):
        given_answer = self.page3.line_edit.text()
        if given_answer == self.current_correct_answer:
            self.correct_answer()
        else:
            self.wrong_answer()
        self.question.setText(self.page3.line_edit.text())
        self.page3.line_edit.clear()
        self.update_question()

    def after_button_clicked(self, ans_id):
        if self.buttons_choices[ans_id] == self.current_correct_answer:
            self.correct_answer()
        else:
            self.wrong_answer()
        self.update_question()

    def on_ans_button0_clicked(self):
        self.after_button_clicked(0)

    def on_ans_button1_clicked(self):
        self.after_button_clicked(1)

    def on_ans_button2_clicked(self):
        self.after_button_clicked(2)