class PangoFileWidget(PangoDockWidget): def __init__(self, title, parent=None): super().__init__(title, parent) self.setFixedWidth(160) self.file_model = QFileSystemModel() self.file_model.setFilter(QDir.Files | QDir.NoDotAndDotDot) self.file_model.setNameFilters(["*.jpg", "*.png"]) self.file_model.setNameFilterDisables(False) self.th_provider = ThumbnailProvider() self.file_model.setIconProvider(self.th_provider) self.file_view = QListView() self.file_view.setModel(self.file_model) self.file_view.setViewMode(QListView.IconMode) self.file_view.setFlow(QListView.LeftToRight) self.file_view.setIconSize(QSize(150, 150)) self.setWidget(self.file_view) def select_next_image(self): c_idx = self.file_view.currentIndex() idx = c_idx.siblingAtRow(c_idx.row()+1) if idx.row() != -1: self.file_view.setCurrentIndex(idx) def select_prev_image(self): c_idx = self.file_view.currentIndex() idx = c_idx.siblingAtRow(c_idx.row()-1) if idx.row() != -1: self.file_view.setCurrentIndex(idx)
def _select_next(self, view: W.QListView, sorted_rows: List[int]) -> None: if not sorted_rows: return next_row = sorted_rows[0] - len(sorted_rows) + 1 row_count = view.model().rowCount() if next_row >= row_count: next_row = row_count - 1 view.setCurrentIndex(view.model().index(next_row, 0))
class Player(QWidget): fullScreenChanged = pyqtSignal(bool) def __init__(self, playlist, parent=None, add_button = None): super(Player, self).__init__(parent) self.add_button = add_button self.colorDialog = None self.trackInfo = "" self.statusInfo = "" self.duration = 0 self.player = QMediaPlayer() self.playlist = QMediaPlaylist() self.player.setPlaylist(self.playlist) self.player.durationChanged.connect(self.durationChanged) self.player.positionChanged.connect(self.positionChanged) self.player.metaDataChanged.connect(self.metaDataChanged) self.playlist.currentIndexChanged.connect(self.playlistPositionChanged) self.player.mediaStatusChanged.connect(self.statusChanged) self.player.bufferStatusChanged.connect(self.bufferingProgress) self.player.error.connect(self.displayErrorMessage) self.videoWidget = VideoWidget() self.player.setVideoOutput(self.videoWidget) self.playlistModel = PlaylistModel() self.playlistModel.setPlaylist(self.playlist) self.playlistView = QListView() self.playlistView.setModel(self.playlistModel) self.playlistView.setCurrentIndex( self.playlistModel.index(self.playlist.currentIndex(), 0)) self.playlistView.activated.connect(self.jump) self.slider = QSlider(Qt.Horizontal) self.slider.setRange(0, self.player.duration() / 1000) self.labelDuration = QLabel() self.slider.sliderMoved.connect(self.seek) openButton = QPushButton("Open Audio/Video File", clicked=self.open) controls = PlayerControls() controls.setState(self.player.state()) controls.setVolume(self.player.volume()) controls.setMuted(controls.isMuted()) controls.play.connect(self.player.play) controls.pause.connect(self.player.pause) controls.stop.connect(self.player.stop) controls.next.connect(self.playlist.next) controls.previous.connect(self.previousClicked) controls.changeVolume.connect(self.player.setVolume) controls.changeMuting.connect(self.player.setMuted) controls.changeRate.connect(self.player.setPlaybackRate) controls.stop.connect(self.videoWidget.update) self.player.stateChanged.connect(controls.setState) self.player.volumeChanged.connect(controls.setVolume) self.player.mutedChanged.connect(controls.setMuted) displayLayout = QHBoxLayout() displayLayout.addWidget(self.videoWidget, 2) displayLayout.addWidget(self.playlistView) controlLayout = QHBoxLayout() controlLayout.setContentsMargins(0, 0, 0, 0) controlLayout.addWidget(openButton) # button to add decoder if add_button: add_decoder_btn = QPushButton("Decode Keystrokes of Selected Media") add_decoder_btn.clicked.connect(add_button) controlLayout.addWidget(add_decoder_btn) # controlLayout.addStretch(1) controlLayout.addWidget(controls) layout = QVBoxLayout() layout.addLayout(displayLayout) hLayout = QHBoxLayout() hLayout.addWidget(self.slider) hLayout.addWidget(self.labelDuration) layout.addLayout(hLayout) layout.addLayout(controlLayout) self.setLayout(layout) if not self.player.isAvailable(): QMessageBox.warning(self, "Service not available", "The QMediaPlayer object does not have a valid service.\n" "Please check the media service plugins are installed.") controls.setEnabled(False) self.playlistView.setEnabled(False) openButton.setEnabled(False) self.metaDataChanged() self.addToPlaylist(playlist) def get_current_file(self): inds = self.playlistView.selectedIndexes() if len(inds) == 1: index = inds[0] location = self.playlistModel.m_playlist.media(index.row()).canonicalUrl() return location.path() def open(self): fileNames, _ = QFileDialog.getOpenFileNames(self, "Open Files") self.addToPlaylist(fileNames) def addToPlaylist(self, fileNames): for name in fileNames: fileInfo = QFileInfo(name) if fileInfo.exists(): url = QUrl.fromLocalFile(fileInfo.absoluteFilePath()) if fileInfo.suffix().lower() == 'm3u': self.playlist.load(url) else: self.playlist.addMedia(QMediaContent(url)) else: url = QUrl(name) if url.isValid(): self.playlist.addMedia(QMediaContent(url)) def durationChanged(self, duration): duration /= 1000 self.duration = duration self.slider.setMaximum(duration) def positionChanged(self, progress): progress /= 1000 if not self.slider.isSliderDown(): self.slider.setValue(progress) self.updateDurationInfo(progress) def metaDataChanged(self): if self.player.isMetaDataAvailable(): self.setTrackInfo("%s - %s" % ( self.player.metaData(QMediaMetaData.AlbumArtist), self.player.metaData(QMediaMetaData.Title))) def previousClicked(self): # Go to the previous track if we are within the first 5 seconds of # playback. Otherwise, seek to the beginning. if self.player.position() <= 5000: self.playlist.previous() else: self.player.setPosition(0) def jump(self, index): if index.isValid(): self.playlist.setCurrentIndex(index.row()) self.player.play() def playlistPositionChanged(self, position): self.playlistView.setCurrentIndex( self.playlistModel.index(position, 0)) def seek(self, seconds): self.player.setPosition(seconds * 1000) def statusChanged(self, status): self.handleCursor(status) if status == QMediaPlayer.LoadingMedia: self.setStatusInfo("Loading...") elif status == QMediaPlayer.StalledMedia: self.setStatusInfo("Media Stalled") elif status == QMediaPlayer.EndOfMedia: QApplication.alert(self) elif status == QMediaPlayer.InvalidMedia: self.displayErrorMessage() else: self.setStatusInfo("") def handleCursor(self, status): if status in (QMediaPlayer.LoadingMedia, QMediaPlayer.BufferingMedia, QMediaPlayer.StalledMedia): self.setCursor(Qt.BusyCursor) else: self.unsetCursor() def bufferingProgress(self, progress): self.setStatusInfo("Buffering %d%" % progress) def setTrackInfo(self, info): self.trackInfo = info if self.statusInfo != "": self.setWindowTitle("%s | %s" % (self.trackInfo, self.statusInfo)) else: self.setWindowTitle(self.trackInfo) def setStatusInfo(self, info): self.statusInfo = info if self.statusInfo != "": self.setWindowTitle("%s | %s" % (self.trackInfo, self.statusInfo)) else: self.setWindowTitle(self.trackInfo) def displayErrorMessage(self): self.setStatusInfo(self.player.errorString()) def updateDurationInfo(self, currentInfo): duration = self.duration if currentInfo or duration: currentTime = QTime((currentInfo/3600)%60, (currentInfo/60)%60, currentInfo%60, (currentInfo*1000)%1000) totalTime = QTime((duration/3600)%60, (duration/60)%60, duration%60, (duration*1000)%1000); format = 'hh:mm:ss' if duration > 3600 else 'mm:ss' tStr = currentTime.toString(format) + " / " + totalTime.toString(format) else: tStr = "" self.labelDuration.setText(tStr)
class Player(QWidget): fullScreenChanged = pyqtSignal(bool) def __init__(self, playlist, parent=None): super(Player, self).__init__(parent) self.setStyleSheet(stylesheet(self)) self.colorDialog = None self.trackInfo = "" self.statusInfo = "" self.duration = 0 self.url = "" self. settings = QSettings("QAudioPlayer", "QAudioPlayer") self.player = QMediaPlayer() self.playlist = QMediaPlaylist() self.player.setPlaylist(self.playlist) self.player.durationChanged.connect(self.durationChanged) self.player.positionChanged.connect(self.positionChanged) self.player.metaDataChanged.connect(self.metaDataChanged) self.playlist.currentIndexChanged.connect(self.playlistPositionChanged) self.player.mediaStatusChanged.connect(self.statusChanged) self.player.bufferStatusChanged.connect(self.bufferingProgress) self.player.error.connect(self.displayErrorMessage) self.playlistModel = PlaylistModel() self.playlistModel.setPlaylist(self.playlist) self.playlistView = QListView() # self.playlistView.setSpacing(1) self.playlistView.setStyleSheet(stylesheet(self)) self.playlistView.setModel(self.playlistModel) self.playlistView.setCurrentIndex( self.playlistModel.index(self.playlist.currentIndex(), 0)) self.playlistView.activated.connect(self.jump) self.slider = QSlider(Qt.Horizontal) self.slider.setRange(0, self.player.duration() / 1000) self.slider.setStyleSheet(stylesheet(self)) self.labelDuration = QLabel() self.slider.sliderMoved.connect(self.seek) openButton = QPushButton("Open", clicked=self.open) openButton.setIcon(openButton.style().standardIcon(QStyle.SP_DialogOpenButton)) clearButton = QPushButton("", clicked=self.clearList) clearButton.setFixedWidth(36) clearButton.setIcon(QIcon.fromTheme("edit-delete")) clearButton.setToolTip("clear List") controls = PlayerControls() controls.setState(self.player.state()) controls.setVolume(self.player.volume()) controls.setMuted(controls.isMuted()) controls.play.connect(self.player.play) controls.pause.connect(self.player.pause) controls.stop.connect(self.player.stop) controls.next.connect(self.playlist.next) controls.previous.connect(self.previousClicked) controls.changeVolume.connect(self.player.setVolume) controls.changeMuting.connect(self.player.setMuted) controls.changeRate.connect(self.player.setPlaybackRate) self.player.stateChanged.connect(controls.setState) self.player.volumeChanged.connect(controls.setVolume) self.player.mutedChanged.connect(controls.setMuted) displayLayout = QHBoxLayout() displayLayout.addWidget(self.playlistView) controlLayout = QHBoxLayout() controlLayout.setContentsMargins(0, 0, 0, 0) controlLayout.addWidget(openButton) controlLayout.addWidget(clearButton) # controlLayout.addStretch(1) controlLayout.addWidget(controls) # controlLayout.addStretch(1) layout = QVBoxLayout() layout.addLayout(displayLayout) hLayout = QHBoxLayout() hLayout.addWidget(self.slider) hLayout.addWidget(self.labelDuration) layout.addLayout(hLayout) layout.addLayout(controlLayout) self.statusBar = QStatusBar() vlayout = QVBoxLayout() vlayout.addWidget(self.statusBar) layout.addLayout(vlayout) self.statusBar.showMessage("Welcome") self.setWindowTitle("QAudioPlayer") self.setMinimumSize(300, 200) # self.setBackgroundRole(QPalette.Window) self.setContentsMargins(0,0,0,0) self.setLayout(layout) self.readSettings() if not self.player.isAvailable(): QMessageBox.warning(self, "Service not available", "The QMediaPlayer object does not have a valid service.\n" "Please check the media service plugins are installed.") controls.setEnabled(False) self.playlistView.setEnabled(False) openButton.setEnabled(False) self.colorButton.setEnabled(False) self.fullScreenButton.setEnabled(False) self.metaDataChanged() self.addToPlaylist(playlist) def readSettings(self): if self.settings.contains("url"): self.url = self.settings.value("url") self.addToPlaylist(self.url) def writeSettings(self): self.settings.setValue("url", self.url) def closeEvent(self, event): print("writing settings") self.writeSettings() print("goodbye ...") event.accept() def open(self): fileNames, _ = QFileDialog.getOpenFileNames(self, "Open Files", "/home", "Audio Files *.mp3 *.m4a *.ogg *.wav *.m3u") if fileNames: self.url = fileNames self.addToPlaylist(fileNames) print("added Files to playlist") def openOnStart(self, name): fileInfo = QFileInfo(name) if fileInfo.exists(): url = QUrl.fromLocalFile(fileInfo.absoluteFilePath()) if fileInfo.suffix().lower() == 'm3u': self.playlist.load(url) else: self.playlist.addMedia(QMediaContent(url)) else: url = QUrl(name) if url.isValid(): self.playlist.addMedia(QMediaContent(url)) print("added Files to playlist") def clearList(self): self.playlist.clear() def addToPlaylist(self, fileNames): for name in fileNames: fileInfo = QFileInfo(name) if fileInfo.exists(): url = QUrl.fromLocalFile(fileInfo.absoluteFilePath()) if fileInfo.suffix().lower() == 'm3u': self.playlist.load(url) else: self.playlist.addMedia(QMediaContent(url)) else: url = QUrl(name) if url.isValid(): self.playlist.addMedia(QMediaContent(url)) def durationChanged(self, duration): duration /= 1000 self.duration = duration self.slider.setMaximum(duration) def positionChanged(self, progress): progress /= 1000 if not self.slider.isSliderDown(): self.slider.setValue(progress) self.updateDurationInfo(progress) def metaDataChanged(self): if self.player.isMetaDataAvailable(): self.setTrackInfo("%s - %s" % ( self.player.metaData(QMediaMetaData.AlbumArtist), self.player.metaData(QMediaMetaData.Title))) def previousClicked(self): # Go to the previous track if we are within the first 5 seconds of # playback. Otherwise, seek to the beginning. if self.player.position() <= 5000: self.playlist.previous() else: self.player.setPosition(0) def jump(self, index): if index.isValid(): self.playlist.setCurrentIndex(index.row()) self.player.play() def playlistPositionChanged(self, position): self.playlistView.setCurrentIndex( self.playlistModel.index(position, 0)) def seek(self, seconds): self.player.setPosition(seconds * 1000) def statusChanged(self, status): self.handleCursor(status) if status == QMediaPlayer.LoadingMedia: self.setStatusInfo("Loading...") elif status == QMediaPlayer.StalledMedia: self.setStatusInfo("Media Stalled") elif status == QMediaPlayer.EndOfMedia: QApplication.alert(self) elif status == QMediaPlayer.InvalidMedia: self.displayErrorMessage() else: self.setStatusInfo("") def handleCursor(self, status): if status in (QMediaPlayer.LoadingMedia, QMediaPlayer.BufferingMedia, QMediaPlayer.StalledMedia): self.setCursor(Qt.BusyCursor) else: self.unsetCursor() def bufferingProgress(self, progress): self.setStatusInfo("Buffering %d%" % progress) def setTrackInfo(self, info): self.trackInfo = info if self.statusInfo != "": self.statusBar.showMessage("%s | %s" % (self.trackInfo, self.statusInfo)) else: self.statusBar.showMessage(self.trackInfo) def setStatusInfo(self, info): self.statusInfo = info if self.statusInfo != "": self.statusBar.showMessage("%s | %s" % (self.trackInfo, self.statusInfo)) else: self.statusBar.showMessage(self.trackInfo) def displayErrorMessage(self): self.setStatusInfo(self.player.errorString()) def updateDurationInfo(self, currentInfo): duration = self.duration if currentInfo or duration: currentTime = QTime((currentInfo/3600)%60, (currentInfo/60)%60, currentInfo%60, (currentInfo*1000)%1000) totalTime = QTime((duration/3600)%60, (duration/60)%60, duration%60, (duration*1000)%1000); format = 'hh:mm:ss' if duration > 3600 else 'mm:ss' tStr = currentTime.toString(format) + " / " + totalTime.toString(format) else: tStr = "" self.labelDuration.setText(tStr)
class MainWindow(QMainWindow): """MNELAB main window.""" def __init__(self, model): """Initialize MNELAB main window. Parameters ---------- model : mnelab.model.Model instance The main window needs to connect to a model containing all data sets. This decouples the GUI from the data (model/view). """ super().__init__() mp.set_start_method("spawn", force=True) # required for Linux/macOS self.model = model # data model self.setWindowTitle("MNELAB") # restore settings settings = read_settings() self.recent = settings["recent"] # list of recent files if settings["geometry"]: self.restoreGeometry(settings["geometry"]) else: self.setGeometry(300, 300, 1000, 750) # default window size self.move(QApplication.desktop().screen().rect().center() - self.rect().center()) # center window if settings["state"]: self.restoreState(settings["state"]) self.actions = {} # contains all actions # initialize menus file_menu = self.menuBar().addMenu("&File") icon = QIcon(":/open_file.svg") self.actions["open_file"] = file_menu.addAction( icon, "&Open...", self.open_data, QKeySequence.Open) self.recent_menu = file_menu.addMenu("Open recent") self.recent_menu.aboutToShow.connect(self._update_recent_menu) self.recent_menu.triggered.connect(self._load_recent) if not self.recent: self.recent_menu.setEnabled(False) self.actions["close_file"] = file_menu.addAction( "&Close", self.model.remove_data, QKeySequence.Close) self.actions["close_all"] = file_menu.addAction( "Close all", self.close_all) file_menu.addSeparator() icon = QIcon(":/meta_info.svg") self.actions["meta_info"] = file_menu.addAction( icon, "Show information...", self.meta_info) file_menu.addSeparator() self.actions["import_bads"] = file_menu.addAction( "Import bad channels...", lambda: self.import_file( model.import_bads, "Import bad channels", "*.csv")) self.actions["import_events"] = file_menu.addAction( "Import events...", lambda: self.import_file( model.import_events, "Import events", "*.csv")) self.actions["import_annotations"] = file_menu.addAction( "Import annotations...", lambda: self.import_file( model.import_annotations, "Import annotations", "*.csv")) self.actions["import_ica"] = file_menu.addAction( "Import &ICA...", lambda: self.open_file( model.import_ica, "Import ICA", "*.fif *.fif.gz")) file_menu.addSeparator() self.export_menu = file_menu.addMenu("Export data") for name, ext in EXPORT_FORMATS.items(): self.actions["export_data_" + ext] = self.export_menu.addAction( f"{name} ({ext[1:].upper()})...", partial(self.export_file, model.export_data, "Export data", ext)) self.actions["export_bads"] = file_menu.addAction( "Export &bad channels...", lambda: self.export_file( model.export_bads, "Export bad channels", "*.csv")) self.actions["export_events"] = file_menu.addAction( "Export &events...", lambda: self.export_file( model.export_events, "Export events", "*.csv")) self.actions["export_annotations"] = file_menu.addAction( "Export &annotations...", lambda: self.export_file( model.export_annotations, "Export annotations", "*.csv")) self.actions["export_ica"] = file_menu.addAction( "Export ICA...", lambda: self.export_file( model.export_ica, "Export ICA", "*.fif *.fif.gz")) file_menu.addSeparator() self.actions["quit"] = file_menu.addAction("&Quit", self.close, QKeySequence.Quit) edit_menu = self.menuBar().addMenu("&Edit") self.actions["pick_chans"] = edit_menu.addAction( "Pick &channels...", self.pick_channels) icon = QIcon(":/chan_props.svg") self.actions["chan_props"] = edit_menu.addAction( icon, "Channel &properties...", self.channel_properties) self.actions["set_montage"] = edit_menu.addAction( "Set &montage...", self.set_montage) edit_menu.addSeparator() self.actions["set_ref"] = edit_menu.addAction("&Set reference...", self.set_reference) edit_menu.addSeparator() self.actions["annotations"] = edit_menu.addAction( "Annotations...", self.edit_annotations) self.actions["events"] = edit_menu.addAction("Events...", self.edit_events) edit_menu.addSeparator() self.actions["crop"] = edit_menu.addAction("&Crop data...", self.crop) plot_menu = self.menuBar().addMenu("&Plot") icon = QIcon(":/plot_data.svg") self.actions["plot_data"] = plot_menu.addAction( icon, "&Data...", self.plot_data) icon = QIcon(":/plot_psd.svg") self.actions["plot_psd"] = plot_menu.addAction( icon, "&Power spectral density...", self.plot_psd) icon = QIcon(":/plot_montage.svg") self.actions["plot_montage"] = plot_menu.addAction( icon, "&Montage...", self.plot_montage) plot_menu.addSeparator() self.actions["plot_ica_components"] = plot_menu.addAction( "ICA &components...", self.plot_ica_components) self.actions["plot_ica_sources"] = plot_menu.addAction( "ICA &sources...", self.plot_ica_sources) tools_menu = self.menuBar().addMenu("&Tools") icon = QIcon(":/filter.svg") self.actions["filter"] = tools_menu.addAction(icon, "&Filter data...", self.filter_data) icon = QIcon(":/find_events.svg") self.actions["find_events"] = tools_menu.addAction( icon, "Find &events...", self.find_events) self.actions["events_from_annotations"] = tools_menu.addAction( "Create events from annotations", self.events_from_annotations) tools_menu.addSeparator() icon = QIcon(":/run_ica.svg") self.actions["run_ica"] = tools_menu.addAction(icon, "Run &ICA...", self.run_ica) self.actions["apply_ica"] = tools_menu.addAction( "Apply &ICA", self.apply_ica) tools_menu.addSeparator() self.actions["interpolate_bads"] = tools_menu.addAction( "Interpolate bad channels...", self.interpolate_bads) tools_menu.addSeparator() icon = QIcon(":/epoch_data.svg") self.actions["epoch_data"] = tools_menu.addAction( icon, "Create Epochs...", self.epoch_data) view_menu = self.menuBar().addMenu("&View") self.actions["history"] = view_menu.addAction("&History...", self.show_history) self.actions["toolbar"] = view_menu.addAction("&Toolbar", self._toggle_toolbar) self.actions["toolbar"].setCheckable(True) self.actions["statusbar"] = view_menu.addAction( "&Statusbar", self._toggle_statusbar) self.actions["statusbar"].setCheckable(True) help_menu = self.menuBar().addMenu("&Help") self.actions["about"] = help_menu.addAction("&About", self.show_about) self.actions["about_qt"] = help_menu.addAction("About &Qt", self.show_about_qt) # actions that are always enabled self.always_enabled = [ "open_file", "about", "about_qt", "quit", "toolbar", "statusbar" ] # set up toolbar self.toolbar = self.addToolBar("toolbar") self.toolbar.setObjectName("toolbar") self.toolbar.addAction(self.actions["open_file"]) self.toolbar.addAction(self.actions["meta_info"]) self.toolbar.addSeparator() self.toolbar.addAction(self.actions["chan_props"]) self.toolbar.addSeparator() self.toolbar.addAction(self.actions["plot_data"]) self.toolbar.addAction(self.actions["plot_psd"]) self.toolbar.addAction(self.actions["plot_montage"]) self.toolbar.addSeparator() self.toolbar.addAction(self.actions["filter"]) self.toolbar.addAction(self.actions["find_events"]) self.toolbar.addAction(self.actions["epoch_data"]) self.toolbar.addAction(self.actions["run_ica"]) self.setUnifiedTitleAndToolBarOnMac(True) if settings["toolbar"]: self.toolbar.show() self.actions["toolbar"].setChecked(True) else: self.toolbar.hide() self.actions["toolbar"].setChecked(False) # set up data model for sidebar (list of open files) self.names = QStringListModel() self.names.dataChanged.connect(self._update_names) splitter = QSplitter() self.sidebar = QListView() self.sidebar.setFrameStyle(QFrame.NoFrame) self.sidebar.setFocusPolicy(Qt.NoFocus) self.sidebar.setModel(self.names) self.sidebar.clicked.connect(self._update_data) splitter.addWidget(self.sidebar) self.infowidget = InfoWidget() splitter.addWidget(self.infowidget) width = splitter.size().width() splitter.setSizes((int(width * 0.3), int(width * 0.7))) self.setCentralWidget(splitter) self.status_label = QLabel() self.statusBar().addPermanentWidget(self.status_label) if settings["statusbar"]: self.statusBar().show() self.actions["statusbar"].setChecked(True) else: self.statusBar().hide() self.actions["statusbar"].setChecked(False) self.setAcceptDrops(True) self.data_changed() def data_changed(self): # update sidebar self.names.setStringList(self.model.names) self.sidebar.setCurrentIndex(self.names.index(self.model.index)) # update info widget if self.model.data: self.infowidget.set_values(self.model.get_info()) else: self.infowidget.clear() # update status bar if self.model.data: mb = self.model.nbytes / 1024**2 self.status_label.setText("Total Memory: {:.2f} MB".format(mb)) else: self.status_label.clear() # toggle actions if len(self.model) == 0: # disable if no data sets are currently open enabled = False else: enabled = True for name, action in self.actions.items(): # toggle if name not in self.always_enabled: action.setEnabled(enabled) if self.model.data: # toggle if specific conditions are met bads = bool(self.model.current["data"].info["bads"]) self.actions["export_bads"].setEnabled(enabled and bads) events = self.model.current["events"] is not None self.actions["export_events"].setEnabled(enabled and events) if self.model.current["dtype"] == "raw": annot = bool(self.model.current["data"].annotations) else: annot = False self.actions["export_annotations"].setEnabled(enabled and annot) self.actions["annotations"].setEnabled(enabled and annot) montage = bool(self.model.current["montage"]) self.actions["plot_montage"].setEnabled(enabled and montage) ica = bool(self.model.current["ica"]) self.actions["apply_ica"].setEnabled(enabled and ica) self.actions["export_ica"].setEnabled(enabled and ica) self.actions["plot_ica_components"].setEnabled(enabled and ica and montage) self.actions["plot_ica_sources"].setEnabled(enabled and ica) self.actions["interpolate_bads"].setEnabled(enabled and montage and bads) self.actions["events"].setEnabled(enabled and events) self.actions["events_from_annotations"].setEnabled(enabled and annot) self.actions["find_events"].setEnabled( enabled and self.model.current["dtype"] == "raw") self.actions["epoch_data"].setEnabled( enabled and events and self.model.current["dtype"] == "raw") self.actions["crop"].setEnabled( enabled and self.model.current["dtype"] == "raw") self.actions["meta_info"].setEnabled( enabled and self.model.current["ftype"] == "Extensible Data Format") # add to recent files if len(self.model) > 0: self._add_recent(self.model.current["fname"]) def open_data(self, fname=None): """Open raw file.""" if fname is None: fname = QFileDialog.getOpenFileName(self, "Open raw", filter="*")[0] if fname: if not (isfile(fname) or isdir(fname)): self._remove_recent(fname) QMessageBox.critical(self, "File does not exist", f"File {fname} does not exist anymore.") return name, ext, ftype = split_fname(fname, IMPORT_FORMATS) if ext in [".xdf", ".xdfz", ".xdf.gz"]: streams = parse_chunks(parse_xdf(fname)) rows, disabled = [], [] for idx, s in enumerate(streams): rows.append([ s["stream_id"], s["name"], s["type"], s["channel_count"], s["channel_format"], s["nominal_srate"] ]) is_marker = (s["nominal_srate"] == 0 or s["channel_format"] == "string") if is_marker: # disable marker streams disabled.append(idx) enabled = list(set(range(len(rows))) - set(disabled)) if enabled: selected = enabled[0] else: selected = None dialog = XDFStreamsDialog(self, rows, selected=selected, disabled=disabled) if dialog.exec_(): row = dialog.view.selectionModel().selectedRows()[0].row() stream_id = dialog.model.data(dialog.model.index(row, 0)) self.model.load(fname, stream_id=stream_id) else: # all other file formats try: self.model.load(fname) except FileNotFoundError as e: QMessageBox.critical(self, "File not found", str(e)) except UnknownFileTypeError as e: QMessageBox.critical(self, "Unknown file type", str(e)) def open_file(self, f, text, ffilter): """Open file.""" fname = QFileDialog.getOpenFileName(self, text, filter="*")[0] if fname: f(fname) def export_file(self, f, text, ffilter): """Export to file.""" fname = QFileDialog.getSaveFileName(self, text, filter="*")[0] if fname: f(fname, ffilter) def import_file(self, f, text, ffilter): """Import file.""" fname = QFileDialog.getOpenFileName(self, text, filter="*")[0] if fname: try: f(fname) except LabelsNotFoundError as e: QMessageBox.critical(self, "Channel labels not found", str(e)) except InvalidAnnotationsError as e: QMessageBox.critical(self, "Invalid annotations", str(e)) def close_all(self): """Close all currently open data sets.""" msg = QMessageBox.question(self, "Close all data sets", "Close all data sets?") if msg == QMessageBox.Yes: while len(self.model) > 0: self.model.remove_data() def meta_info(self): xml = get_xml(self.model.current["fname"]) dialog = MetaInfoDialog(self, xml) dialog.exec_() def pick_channels(self): """Pick channels in current data set.""" channels = self.model.current["data"].info["ch_names"] dialog = PickChannelsDialog(self, channels, selected=channels) if dialog.exec_(): picks = [item.data(0) for item in dialog.channels.selectedItems()] drops = set(channels) - set(picks) if drops: self.auto_duplicate() self.model.drop_channels(drops) self.model.history.append(f"raw.drop({drops})") def channel_properties(self): """Show channel properties dialog.""" info = self.model.current["data"].info dialog = ChannelPropertiesDialog(self, info) if dialog.exec_(): dialog.model.sort(0) bads = [] renamed = {} types = {} for i in range(dialog.model.rowCount()): new_label = dialog.model.item(i, 1).data(Qt.DisplayRole) old_label = info["ch_names"][i] if new_label != old_label: renamed[old_label] = new_label new_type = dialog.model.item(i, 2).data(Qt.DisplayRole).lower() old_type = channel_type(info, i).lower() if new_type != old_type: types[new_label] = new_type if dialog.model.item(i, 3).checkState() == Qt.Checked: bads.append(info["ch_names"][i]) self.model.set_channel_properties(bads, renamed, types) def set_montage(self): """Set montage.""" montages = mne.channels.get_builtin_montages() # TODO: currently it is not possible to remove an existing montage dialog = MontageDialog(self, montages, selected=self.model.current["montage"]) if dialog.exec_(): name = dialog.montages.selectedItems()[0].data(0) montage = mne.channels.make_standard_montage(name) ch_names = self.model.current["data"].info["ch_names"] # check if at least one channel name matches a name in the montage if set(ch_names) & set(montage.ch_names): self.model.set_montage(name) else: QMessageBox.critical( self, "No matching channel names", "Channel names defined in the montage do " "not match any channel name in the data.") def edit_annotations(self): fs = self.model.current["data"].info["sfreq"] pos = self.model.current["data"].annotations.onset pos = (pos * fs).astype(int).tolist() dur = self.model.current["data"].annotations.duration dur = (dur * fs).astype(int).tolist() desc = self.model.current["data"].annotations.description.tolist() dialog = AnnotationsDialog(self, pos, dur, desc) if dialog.exec_(): rows = dialog.table.rowCount() onset, duration, description = [], [], [] for i in range(rows): data = dialog.table.item(i, 0).data(Qt.DisplayRole) onset.append(float(data) / fs) data = dialog.table.item(i, 1).data(Qt.DisplayRole) duration.append(float(data) / fs) data = dialog.table.item(i, 2).data(Qt.DisplayRole) description.append(data) self.model.set_annotations(onset, duration, description) def edit_events(self): pos = self.model.current["events"][:, 0].tolist() desc = self.model.current["events"][:, 2].tolist() dialog = EventsDialog(self, pos, desc) if dialog.exec_(): rows = dialog.table.rowCount() events = np.zeros((rows, 3), dtype=int) for i in range(rows): pos = int(dialog.table.item(i, 0).data(Qt.DisplayRole)) desc = int(dialog.table.item(i, 1).data(Qt.DisplayRole)) events[i] = pos, 0, desc self.model.set_events(events) def crop(self): """Filter data.""" fs = self.model.current["data"].info["sfreq"] length = self.model.current["data"].n_times / fs dialog = CropDialog(self, 0, length) if dialog.exec_(): self.auto_duplicate() if dialog.start is None: dialog.start = 0 self.model.crop(dialog.start, dialog.stop) def plot_data(self): """Plot data.""" # self.bad is needed to update history if bad channels are selected in # the interactive plot window (see also self.eventFilter) self.bads = self.model.current["data"].info["bads"] events = self.model.current["events"] nchan = self.model.current["data"].info["nchan"] fig = self.model.current["data"].plot(events=events, n_channels=nchan, title=self.model.current["name"], scalings="auto", show=False) self.model.history.append("data.plot(n_channels={})".format(nchan)) win = fig.canvas.manager.window win.setWindowTitle(self.model.current["name"]) win.findChild(QStatusBar).hide() win.installEventFilter(self) # detect if the figure is closed # prevent closing the window with the escape key try: fig._mne_params["close_key"] = None except AttributeError: # does not exist in older MNE versions pass fig.show() def plot_psd(self): """Plot power spectral density (PSD).""" kwds = {"show": False} if self.model.current["type"] == "raw": kwds.update({"average": False, "spatial_colors": False}) fig = self.model.current["data"].plot_psd(**kwds) win = fig.canvas.manager.window win.setWindowTitle("Power spectral density") fig.show() def plot_montage(self): """Plot current montage.""" fig = self.model.current["data"].plot_sensors(show_names=True, show=False) win = fig.canvas.manager.window win.setWindowTitle("Montage") win.findChild(QStatusBar).hide() win.findChild(QToolBar).hide() fig.show() def plot_ica_components(self): self.model.current["ica"].plot_components( inst=self.model.current["data"]) def plot_ica_sources(self): self.model.current["ica"].plot_sources(inst=self.model.current["data"]) def run_ica(self): """Run ICA calculation.""" dialog = RunICADialog(self, self.model.current["data"].info["nchan"], have["picard"], have["sklearn"]) if dialog.exec_(): calc = CalcDialog(self, "Calculating ICA", "Calculating ICA.") method = dialog.method.currentText() exclude_bad_segments = dialog.exclude_bad_segments.isChecked() fit_params = {} if not dialog.extended.isHidden(): fit_params["extended"] = dialog.extended.isChecked() if not dialog.ortho.isHidden(): fit_params["ortho"] = dialog.ortho.isChecked() ica = mne.preprocessing.ICA(method=dialog.methods[method], fit_params=fit_params) self.model.history.append(f"ica = mne.preprocessing.ICA(" f"method={dialog.methods[method]}, " f"fit_params={fit_params})") pool = mp.Pool(1) kwds = {"reject_by_annotation": exclude_bad_segments} res = pool.apply_async(func=ica.fit, args=(self.model.current["data"], ), kwds=kwds, callback=lambda x: calc.accept()) if not calc.exec_(): pool.terminate() else: self.model.current["ica"] = res.get(timeout=1) self.model.history.append(f"ica.fit(inst=raw, " f"reject_by_annotation=" f"{exclude_bad_segments})") self.data_changed() def apply_ica(self): """Apply current fitted ICA.""" self.auto_duplicate() self.model.apply_ica() def interpolate_bads(self): """Interpolate bad channels""" dialog = InterpolateBadsDialog(self) if dialog.exec_(): duplicated = self.auto_duplicate() try: self.model.interpolate_bads(dialog.reset_bads, dialog.mode, dialog.origin) except ValueError as e: if duplicated: # undo self.model.remove_data() self.model.index -= 1 self.data_changed() msgbox = ErrorMessageBox(self, "Could not interpolate bad channels", str(e), traceback.format_exc()) msgbox.show() def filter_data(self): """Filter data.""" dialog = FilterDialog(self) if dialog.exec_(): self.auto_duplicate() self.model.filter(dialog.low, dialog.high) def find_events(self): info = self.model.current["data"].info # use first stim channel as default in dialog default_stim = 0 for i in range(info["nchan"]): if mne.io.pick.channel_type(info, i) == "stim": default_stim = i break dialog = FindEventsDialog(self, info["ch_names"], default_stim) if dialog.exec_(): stim_channel = dialog.stimchan.currentText() consecutive = dialog.consecutive.isChecked() initial_event = dialog.initial_event.isChecked() uint_cast = dialog.uint_cast.isChecked() min_dur = dialog.minduredit.value() shortest_event = dialog.shortesteventedit.value() self.model.find_events(stim_channel=stim_channel, consecutive=consecutive, initial_event=initial_event, uint_cast=uint_cast, min_duration=min_dur, shortest_event=shortest_event) def events_from_annotations(self): self.model.events_from_annotations() def epoch_data(self): """Epoch raw data.""" dialog = EpochDialog(self, self.model.current["events"]) if dialog.exec_(): events = [ int(item.text()) for item in dialog.events.selectedItems() ] tmin = dialog.tmin.value() tmax = dialog.tmax.value() if dialog.baseline.isChecked(): baseline = dialog.a.value(), dialog.b.value() else: baseline = None duplicated = self.auto_duplicate() try: self.model.epoch_data(events, tmin, tmax, baseline) except ValueError as e: if duplicated: # undo self.model.remove_data() self.model.index -= 1 self.data_changed() msgbox = ErrorMessageBox(self, "Could not create epochs", str(e), traceback.format_exc()) msgbox.show() def set_reference(self): """Set reference.""" dialog = ReferenceDialog(self) if dialog.exec_(): self.auto_duplicate() if dialog.average.isChecked(): self.model.set_reference("average") else: ref = [c.strip() for c in dialog.channellist.text().split(",")] self.model.set_reference(ref) def show_history(self): """Show history.""" dialog = HistoryDialog(self, "\n".join(self.model.history)) dialog.exec_() def show_about(self): """Show About dialog.""" msg_box = QMessageBox(self) text = (f"<img src=':/mnelab_logo.png'>" f"<p>MNELAB {__version__}</p>") msg_box.setText(text) mnelab_url = "github.com/cbrnr/mnelab" mne_url = "github.com/mne-tools/mne-python" pkgs = [] for key, value in have.items(): if value: pkgs.append(f"{key} ({value})") else: pkgs.append(f"{key} (not installed)") text = (f'<nobr><p>This program uses Python ' f'{".".join(str(k) for k in version_info[:3])} and the ' f'following packages:</p></nobr>' f'<p>{", ".join(pkgs)}</p>' f'<nobr><p>MNELAB repository: ' f'<a href=https://{mnelab_url}>{mnelab_url}</a></p></nobr>' f'<nobr><p>MNE repository: ' f'<a href=https://{mne_url}>{mne_url}</a></p></nobr>' f'<p>Licensed under the BSD 3-clause license.</p>' f'<p>Copyright 2017-2020 by Clemens Brunner.</p>') msg_box.setInformativeText(text) msg_box.exec_() def show_about_qt(self): """Show About Qt dialog.""" QMessageBox.aboutQt(self, "About Qt") def auto_duplicate(self): """Automatically duplicate current data set. If the current data set is stored in a file (i.e. was loaded directly from a file), a new data set is automatically created. If the current data set is not stored in a file (i.e. was created by operations in MNELAB), a dialog box asks the user if the current data set should be overwritten or duplicated. Returns ------- duplicated : bool True if the current data set was automatically duplicated, False if the current data set was overwritten. """ # if current data is stored in a file create a new data set if self.model.current["fname"]: self.model.duplicate_data() return True # otherwise ask the user else: msg = QMessageBox.question(self, "Overwrite existing data set", "Overwrite existing data set?") if msg == QMessageBox.No: # create new data set self.model.duplicate_data() return True return False def _add_recent(self, fname): """Add a file to recent file list. Parameters ---------- fname : str File name. """ if fname in self.recent: # avoid duplicates self.recent.remove(fname) self.recent.insert(0, fname) while len(self.recent) > MAX_RECENT: # prune list self.recent.pop() write_settings(recent=self.recent) if not self.recent_menu.isEnabled(): self.recent_menu.setEnabled(True) def _remove_recent(self, fname): """Remove file from recent file list. Parameters ---------- fname : str File name. """ if fname in self.recent: self.recent.remove(fname) write_settings(recent=self.recent) if not self.recent: self.recent_menu.setEnabled(False) @pyqtSlot(QModelIndex) def _update_data(self, selected): """Update index and information based on the state of the sidebar. Parameters ---------- selected : QModelIndex Index of the selected row. """ if selected.row() != self.model.index: self.model.index = selected.row() self.data_changed() @pyqtSlot(QModelIndex, QModelIndex) def _update_names(self, start, stop): """Update names in DataSets after changes in sidebar.""" for index in range(start.row(), stop.row() + 1): self.model.data[index]["name"] = self.names.stringList()[index] @pyqtSlot() def _update_recent_menu(self): self.recent_menu.clear() for recent in self.recent: self.recent_menu.addAction(recent) @pyqtSlot(QAction) def _load_recent(self, action): self.open_data(fname=action.text()) @pyqtSlot() def _toggle_toolbar(self): if self.toolbar.isHidden(): self.toolbar.show() else: self.toolbar.hide() write_settings(toolbar=not self.toolbar.isHidden()) @pyqtSlot() def _toggle_statusbar(self): if self.statusBar().isHidden(): self.statusBar().show() else: self.statusBar().hide() write_settings(statusbar=not self.statusBar().isHidden()) @pyqtSlot(QDropEvent) def dragEnterEvent(self, event): if event.mimeData().hasUrls(): event.acceptProposedAction() @pyqtSlot(QDropEvent) def dropEvent(self, event): mime = event.mimeData() if mime.hasUrls(): urls = mime.urls() for url in urls: try: self.open_data(url.toLocalFile()) except FileNotFoundError as e: QMessageBox.critical(self, "File not found", str(e)) @pyqtSlot(QEvent) def closeEvent(self, event): """Close application. Parameters ---------- event : QEvent Close event. """ write_settings(geometry=self.saveGeometry(), state=self.saveState()) if self.model.history: print("\nCommand History") print("===============") print("\n".join(self.model.history)) QApplication.quit() def eventFilter(self, source, event): # currently the only source is the raw plot window if event.type() == QEvent.Close: self.data_changed() bads = self.model.current["data"].info["bads"] if self.bads != bads: self.model.history.append(f"data.info['bads'] = {bads}") return QObject.eventFilter(self, source, event)
class Player(QWidget): fullScreenChanged = pyqtSignal(bool) def __init__(self, playlist, parent=None): super(Player, self).__init__(parent) self.colorDialog = None self.trackInfo = "" self.statusInfo = "" self.duration = 0 self.player = QMediaPlayer() self.playlist = QMediaPlaylist() self.player.setPlaylist(self.playlist) self.player.durationChanged.connect(self.durationChanged) self.player.positionChanged.connect(self.positionChanged) self.player.metaDataChanged.connect(self.metaDataChanged) self.playlist.currentIndexChanged.connect(self.playlistPositionChanged) self.player.mediaStatusChanged.connect(self.statusChanged) self.player.bufferStatusChanged.connect(self.bufferingProgress) self.player.videoAvailableChanged.connect(self.videoAvailableChanged) self.player.error.connect(self.displayErrorMessage) self.videoWidget = VideoWidget() self.player.setVideoOutput(self.videoWidget) self.playlistModel = PlaylistModel() self.playlistModel.setPlaylist(self.playlist) self.playlistView = QListView() self.playlistView.setModel(self.playlistModel) self.playlistView.setCurrentIndex( self.playlistModel.index(self.playlist.currentIndex(), 0)) self.playlistView.activated.connect(self.jump) self.slider = QSlider(Qt.Horizontal) self.slider.setRange(0, self.player.duration() / 1000) self.labelDuration = QLabel() self.slider.sliderMoved.connect(self.seek) # self.labelHistogram = QLabel() # self.labelHistogram.setText("Histogram:") # self.histogram = HistogramWidget() # histogramLayout = QHBoxLayout() # histogramLayout.addWidget(self.labelHistogram) # histogramLayout.addWidget(self.histogram, 1) self.probe = QVideoProbe() # self.probe.videoFrameProbed.connect(self.histogram.processFrame) self.probe.setSource(self.player) openButton = QPushButton("打开", clicked=self.open) controls = PlayerControls() controls.setState(self.player.state()) controls.setVolume(self.player.volume()) controls.setMuted(controls.isMuted()) controls.play.connect(self.player.play) controls.pause.connect(self.player.pause) controls.stop.connect(self.player.stop) controls.next.connect(self.playlist.next) controls.previous.connect(self.previousClicked) controls.changeVolume.connect(self.player.setVolume) controls.changeMuting.connect(self.player.setMuted) controls.changeRate.connect(self.player.setPlaybackRate) controls.stop.connect(self.videoWidget.update) self.player.stateChanged.connect(controls.setState) self.player.volumeChanged.connect(controls.setVolume) self.player.mutedChanged.connect(controls.setMuted) self.fullScreenButton = QPushButton("全屏") self.fullScreenButton.setCheckable(True) self.colorButton = QPushButton("颜色选项") self.colorButton.setEnabled(False) self.colorButton.clicked.connect(self.showColorDialog) displayLayout = QHBoxLayout() displayLayout.addWidget(self.videoWidget, 2) displayLayout.addWidget(self.playlistView) controlLayout = QHBoxLayout() controlLayout.setContentsMargins(0, 0, 0, 0) controlLayout.addWidget(openButton) controlLayout.addStretch(1) controlLayout.addWidget(controls) controlLayout.addStretch(1) controlLayout.addWidget(self.fullScreenButton) controlLayout.addWidget(self.colorButton) layout = QVBoxLayout() layout.addLayout(displayLayout) hLayout = QHBoxLayout() hLayout.addWidget(self.slider) hLayout.addWidget(self.labelDuration) layout.addLayout(hLayout) layout.addLayout(controlLayout) # layout.addLayout(histogramLayout) self.setLayout(layout) if not self.player.isAvailable(): QMessageBox.warning( self, "Service not available", "The QMediaPlayer object does not have a valid service.\n" "Please check the media service plugins are installed.") controls.setEnabled(False) self.playlistView.setEnabled(False) openButton.setEnabled(False) self.colorButton.setEnabled(False) self.fullScreenButton.setEnabled(False) self.metaDataChanged() self.addToPlaylist(playlist) def open(self): fileNames, _ = QFileDialog.getOpenFileNames(self, "Open Files") self.addToPlaylist(fileNames) def addToPlaylist(self, fileNames): for name in fileNames: fileInfo = QFileInfo(name) if fileInfo.exists(): url = QUrl.fromLocalFile(fileInfo.absoluteFilePath()) if fileInfo.suffix().lower() == 'm3u': self.playlist.load(url) else: self.playlist.addMedia(QMediaContent(url)) else: url = QUrl(name) if url.isValid(): self.playlist.addMedia(QMediaContent(url)) def durationChanged(self, duration): duration /= 1000 self.duration = duration self.slider.setMaximum(duration) def positionChanged(self, progress): progress /= 1000 if not self.slider.isSliderDown(): self.slider.setValue(progress) self.updateDurationInfo(progress) def metaDataChanged(self): if self.player.isMetaDataAvailable(): self.setTrackInfo( "%s - %s" % (self.player.metaData(QMediaMetaData.AlbumArtist), self.player.metaData(QMediaMetaData.Title))) def previousClicked(self): # Go to the previous track if we are within the first 5 seconds of # playback. Otherwise, seek to the beginning. if self.player.position() <= 5000: self.playlist.previous() else: self.player.setPosition(0) def jump(self, index): if index.isValid(): self.playlist.setCurrentIndex(index.row()) self.player.play() def playlistPositionChanged(self, position): self.playlistView.setCurrentIndex(self.playlistModel.index( position, 0)) def seek(self, seconds): self.player.setPosition(seconds * 1000) def statusChanged(self, status): self.handleCursor(status) if status == QMediaPlayer.LoadingMedia: self.setStatusInfo("Loading...") elif status == QMediaPlayer.StalledMedia: self.setStatusInfo("Media Stalled") elif status == QMediaPlayer.EndOfMedia: QApplication.alert(self) elif status == QMediaPlayer.InvalidMedia: self.displayErrorMessage() else: self.setStatusInfo("") def handleCursor(self, status): if status in (QMediaPlayer.LoadingMedia, QMediaPlayer.BufferingMedia, QMediaPlayer.StalledMedia): self.setCursor(Qt.BusyCursor) else: self.unsetCursor() def bufferingProgress(self, progress): self.setStatusInfo("Buffering %d%" % progress) def videoAvailableChanged(self, available): if available: self.fullScreenButton.clicked.connect( self.videoWidget.setFullScreen) self.videoWidget.fullScreenChanged.connect( self.fullScreenButton.setChecked) if self.fullScreenButton.isChecked(): self.videoWidget.setFullScreen(True) else: self.fullScreenButton.clicked.disconnect( self.videoWidget.setFullScreen) self.videoWidget.fullScreenChanged.disconnect( self.fullScreenButton.setChecked) self.videoWidget.setFullScreen(False) self.colorButton.setEnabled(available) def setTrackInfo(self, info): self.trackInfo = info if self.statusInfo != "": self.setWindowTitle("%s | %s" % (self.trackInfo, self.statusInfo)) else: self.setWindowTitle(self.trackInfo) def setStatusInfo(self, info): self.statusInfo = info if self.statusInfo != "": self.setWindowTitle("%s | %s" % (self.trackInfo, self.statusInfo)) else: self.setWindowTitle(self.trackInfo) def displayErrorMessage(self): self.setStatusInfo(self.player.errorString()) def updateDurationInfo(self, currentInfo): duration = self.duration if currentInfo or duration: currentTime = QTime((currentInfo / 3600) % 60, (currentInfo / 60) % 60, currentInfo % 60, (currentInfo * 1000) % 1000) totalTime = QTime((duration / 3600) % 60, (duration / 60) % 60, duration % 60, (duration * 1000) % 1000) format = 'hh:mm:ss' if duration > 3600 else 'mm:ss' tStr = currentTime.toString(format) + " / " + totalTime.toString( format) else: tStr = "" self.labelDuration.setText(tStr) def showColorDialog(self): if self.colorDialog is None: brightnessSlider = QSlider(Qt.Horizontal) brightnessSlider.setRange(-100, 100) brightnessSlider.setValue(self.videoWidget.brightness()) brightnessSlider.sliderMoved.connect( self.videoWidget.setBrightness) self.videoWidget.brightnessChanged.connect( brightnessSlider.setValue) contrastSlider = QSlider(Qt.Horizontal) contrastSlider.setRange(-100, 100) contrastSlider.setValue(self.videoWidget.contrast()) contrastSlider.sliderMoved.connect(self.videoWidget.setContrast) self.videoWidget.contrastChanged.connect(contrastSlider.setValue) hueSlider = QSlider(Qt.Horizontal) hueSlider.setRange(-100, 100) hueSlider.setValue(self.videoWidget.hue()) hueSlider.sliderMoved.connect(self.videoWidget.setHue) self.videoWidget.hueChanged.connect(hueSlider.setValue) saturationSlider = QSlider(Qt.Horizontal) saturationSlider.setRange(-100, 100) saturationSlider.setValue(self.videoWidget.saturation()) saturationSlider.sliderMoved.connect( self.videoWidget.setSaturation) self.videoWidget.saturationChanged.connect( saturationSlider.setValue) layout = QFormLayout() layout.addRow("亮度", brightnessSlider) layout.addRow("对比度", contrastSlider) layout.addRow("色调", hueSlider) layout.addRow("饱和度", saturationSlider) button = QPushButton("关闭") layout.addRow(button) self.colorDialog = QDialog(self) self.colorDialog.setWindowTitle("颜色选项") self.colorDialog.setLayout(layout) button.clicked.connect(self.colorDialog.close) self.colorDialog.show()
class IntracranialElectrodeLocator(QMainWindow): """Locate electrode contacts using a coregistered MRI and CT.""" def __init__(self, info, trans, aligned_ct, subject=None, subjects_dir=None, groups=None, verbose=None): """GUI for locating intracranial electrodes. .. note:: Images will be displayed using orientation information obtained from the image header. Images will be resampled to dimensions [256, 256, 256] for display. """ # initialize QMainWindow class super(IntracranialElectrodeLocator, self).__init__() if not info.ch_names: raise ValueError('No channels found in `info` to locate') # store info for modification self._info = info self._verbose = verbose # load imaging data self._subject_dir = _check_subject_dir(subject, subjects_dir) self._load_image_data(aligned_ct) self._ch_alpha = 0.5 self._radius = int(_CH_PLOT_SIZE // 100) # starting 1/200 of image # initialize channel data self._ch_index = 0 # load data, apply trans self._head_mri_t = _get_trans(trans, 'head', 'mri')[0] self._mri_head_t = invert_transform(self._head_mri_t) # load channels, convert from m to mm self._chs = {name: apply_trans(self._head_mri_t, ch['loc'][:3]) * 1000 for name, ch in zip(info.ch_names, info['chs'])} self._ch_names = list(self._chs.keys()) # set current position if np.isnan(self._chs[self._ch_names[self._ch_index]]).any(): self._ras = np.array([0., 0., 0.]) else: self._ras = self._chs[self._ch_names[self._ch_index]].copy() self._current_slice = apply_trans( self._ras_vox_t, self._ras).round().astype(int) self._group_channels(groups) # GUI design # Main plots: make one plot for each view; sagittal, coronal, axial plt_grid = QGridLayout() plts = [_make_slice_plot(), _make_slice_plot(), _make_slice_plot()] self._figs = [plts[0][1], plts[1][1], plts[2][1]] plt_grid.addWidget(plts[0][0], 0, 0) plt_grid.addWidget(plts[1][0], 0, 1) plt_grid.addWidget(plts[2][0], 1, 0) self._renderer = _get_renderer( name='IEEG Locator', size=(400, 400), bgcolor='w') # TODO: should eventually make sure the renderer here is actually # some PyVista(Qt) variant, not mayavi, otherwise the following # call will fail (hopefully it's rare that people who want to use this # have also set their MNE_3D_BACKEND=mayavi and/or don't have a working # pyvistaqt setup; also hopefully the refactoring to use the # Qt/notebook abstraction will make this easier, too): plt_grid.addWidget(self._renderer.plotter) # Channel selector self._ch_list = QListView() self._ch_list.setSelectionMode(Qt.QAbstractItemView.SingleSelection) self._ch_list.setMinimumWidth(150) self._set_ch_names() # Plots self._plot_images() # Menus button_hbox = self._get_button_bar() slider_hbox = self._get_slider_bar() bottom_hbox = self._get_bottom_bar() # Put everything together plot_ch_hbox = QHBoxLayout() plot_ch_hbox.addLayout(plt_grid) plot_ch_hbox.addWidget(self._ch_list) main_vbox = QVBoxLayout() main_vbox.addLayout(button_hbox) main_vbox.addLayout(slider_hbox) main_vbox.addLayout(plot_ch_hbox) main_vbox.addLayout(bottom_hbox) central_widget = QWidget() central_widget.setLayout(main_vbox) self.setCentralWidget(central_widget) # ready for user self._move_cursors_to_pos() self._ch_list.setFocus() # always focus on list def _load_image_data(self, ct): """Get MRI and CT data to display and transforms to/from vox/RAS.""" self._mri_data, self._vox_ras_t = _load_image( op.join(self._subject_dir, 'mri', 'brain.mgz'), 'MRI Image', verbose=self._verbose) self._ras_vox_t = np.linalg.inv(self._vox_ras_t) self._voxel_sizes = np.array(self._mri_data.shape) self._img_ranges = [[0, self._voxel_sizes[1], 0, self._voxel_sizes[2]], [0, self._voxel_sizes[0], 0, self._voxel_sizes[2]], [0, self._voxel_sizes[0], 0, self._voxel_sizes[1]]] # ready ct self._ct_data, vox_ras_t = _load_image(ct, 'CT', verbose=self._verbose) if self._mri_data.shape != self._ct_data.shape or \ not np.allclose(self._vox_ras_t, vox_ras_t, rtol=1e-6): raise ValueError('CT is not aligned to MRI, got ' f'CT shape={self._ct_data.shape}, ' f'MRI shape={self._mri_data.shape}, ' f'CT affine={vox_ras_t} and ' f'MRI affine={self._vox_ras_t}') if op.exists(op.join(self._subject_dir, 'surf', 'lh.seghead')): self._head = _read_mri_surface( op.join(self._subject_dir, 'surf', 'lh.seghead')) assert _frame_to_str[self._head['coord_frame']] == 'mri' else: warn('`seghead` not found, skipping head plot, see ' ':ref:`mne.bem.make_scalp_surfaces` to add the head') self._head = None if op.exists(op.join(self._subject_dir, 'surf', 'lh.pial')): self._lh = _read_mri_surface( op.join(self._subject_dir, 'surf', 'lh.pial')) assert _frame_to_str[self._lh['coord_frame']] == 'mri' self._rh = _read_mri_surface( op.join(self._subject_dir, 'surf', 'rh.pial')) assert _frame_to_str[self._rh['coord_frame']] == 'mri' else: warn('`pial` surface not found, skipping adding to 3D ' 'plot. This indicates the Freesurfer recon-all ' 'has been modified and these files have been deleted.') self._lh = self._rh = None def _make_ch_image(self, axis): """Make a plot to display the channel locations.""" # Make channel data higher resolution so it looks better. ch_image = np.zeros((_CH_PLOT_SIZE, _CH_PLOT_SIZE)) * np.nan vx, vy, vz = self._voxel_sizes def color_ch_radius(ch_image, xf, yf, group, radius): # Take the fraction across each dimension of the RAS # coordinates converted to xyz and put a circle in that # position in this larger resolution image ex, ey = np.round(np.array([xf, yf]) * _CH_PLOT_SIZE).astype(int) for i in range(-radius, radius + 1): for j in range(-radius, radius + 1): if (i**2 + j**2)**0.5 < radius: # negative y because y axis is inverted ch_image[-(ey + i), ex + j] = group return ch_image for name, ras in self._chs.items(): # move from middle-centered (half coords positive, half negative) # to bottom-left corner centered (all coords positive). if np.isnan(ras).any(): continue xyz = apply_trans(self._ras_vox_t, ras) # check if closest to that voxel dist = np.linalg.norm(xyz - self._current_slice) if dist < self._radius: x, y, z = xyz group = self._groups[name] r = self._radius - np.round(abs(dist)).astype(int) if axis == 0: ch_image = color_ch_radius( ch_image, y / vy, z / vz, group, r) elif axis == 1: ch_image = color_ch_radius( ch_image, x / vx, z / vx, group, r) elif axis == 2: ch_image = color_ch_radius( ch_image, x / vx, y / vy, group, r) return ch_image @verbose def _save_ch_coords(self, info=None, verbose=None): """Save the location of the electrode contacts.""" logger.info('Saving channel positions to `info`') if info is None: info = self._info for name, ch in zip(info.ch_names, info['chs']): ch['loc'][:3] = apply_trans( self._mri_head_t, self._chs[name] / 1000) # mm->m def _plot_images(self): """Use the MRI and CT to make plots.""" # Plot sagittal (0), coronal (1) or axial (2) view self._images = dict(ct=list(), chs=list(), cursor=list(), cursor2=list()) ct_min, ct_max = np.nanmin(self._ct_data), np.nanmax(self._ct_data) text_kwargs = dict(fontsize=3, color='#66CCEE', family='monospace', weight='bold', ha='center', va='center') xyz = apply_trans(self._ras_vox_t, self._ras) for axis in range(3): ct_data = np.take(self._ct_data, self._current_slice[axis], axis=axis).T self._images['ct'].append(self._figs[axis].axes[0].imshow( ct_data, cmap='gray', aspect='auto', vmin=ct_min, vmax=ct_max)) self._images['chs'].append( self._figs[axis].axes[0].imshow( self._make_ch_image(axis), aspect='auto', extent=self._img_ranges[axis], cmap=_CMAP, alpha=self._ch_alpha, vmin=0, vmax=_N_COLORS)) self._images['cursor'].append( self._figs[axis].axes[0].plot( (xyz[axis], xyz[axis]), (0, self._voxel_sizes[axis]), color=[0, 1, 0], linewidth=1, alpha=0.5)[0]) self._images['cursor2'].append( self._figs[axis].axes[0].plot( (0, self._voxel_sizes[axis]), (xyz[axis], xyz[axis]), color=[0, 1, 0], linewidth=1, alpha=0.5)[0]) # label axes self._figs[axis].text(0.5, 0.05, _IMG_LABELS[axis][0], **text_kwargs) self._figs[axis].text(0.05, 0.5, _IMG_LABELS[axis][1], **text_kwargs) self._figs[axis].axes[0].axis(self._img_ranges[axis]) self._figs[axis].canvas.mpl_connect( 'scroll_event', self._on_scroll) self._figs[axis].canvas.mpl_connect( 'button_release_event', partial(self._on_click, axis)) # add head and brain in mm (convert from m) if self._head is not None: self._renderer.mesh( *self._head['rr'].T * 1000, triangles=self._head['tris'], color='gray', opacity=0.2, reset_camera=False, render=False) if self._lh is not None and self._rh is not None: self._renderer.mesh( *self._lh['rr'].T * 1000, triangles=self._lh['tris'], color='white', opacity=0.2, reset_camera=False, render=False) self._renderer.mesh( *self._rh['rr'].T * 1000, triangles=self._rh['tris'], color='white', opacity=0.2, reset_camera=False, render=False) self._3d_chs = dict() self._plot_3d_ch_pos() self._renderer.set_camera(azimuth=90, elevation=90, distance=300, focalpoint=tuple(self._ras)) # update plots self._draw() self._renderer._update() def _scale_radius(self): """Scale the radius to mm.""" shape = np.mean(self._ct_data.shape) # this is Freesurfer shape (256) scale = np.diag(self._ras_vox_t)[:3].mean() return scale * self._radius * (shape / _CH_PLOT_SIZE) def _update_camera(self, render=False): """Update the camera position.""" self._renderer.set_camera( # needs fix, distance moves when focal point updates distance=self._renderer.plotter.camera.distance * 0.9, focalpoint=tuple(self._ras), reset_camera=False) def _plot_3d_ch(self, name, render=False): """Plot a single 3D channel.""" if name in self._3d_chs: self._renderer.plotter.remove_actor(self._3d_chs.pop(name)) if not any(np.isnan(self._chs[name])): radius = self._scale_radius() self._3d_chs[name] = self._renderer.sphere( tuple(self._chs[name]), scale=radius * 3, color=_UNIQUE_COLORS[self._groups[name] % _N_COLORS], opacity=self._ch_alpha)[0] if render: self._renderer._update() def _plot_3d_ch_pos(self, render=False): for name in self._chs: self._plot_3d_ch(name) if render: self._renderer._update() def _get_button_bar(self): """Make a bar with buttons for user interactions.""" hbox = QHBoxLayout() help_button = QPushButton('Help') help_button.released.connect(self._show_help) hbox.addWidget(help_button) hbox.addStretch(8) hbox.addWidget(QLabel('Snap to Center')) self._snap_button = QPushButton('Off') self._snap_button.setMaximumWidth(25) # not too big hbox.addWidget(self._snap_button) self._snap_button.released.connect(self._toggle_snap) self._toggle_snap() # turn on to start hbox.addStretch(1) self._toggle_brain_button = QPushButton('Show Brain') self._toggle_brain_button.released.connect(self._toggle_show_brain) hbox.addWidget(self._toggle_brain_button) hbox.addStretch(1) mark_button = QPushButton('Mark') hbox.addWidget(mark_button) mark_button.released.connect(self._mark_ch) remove_button = QPushButton('Remove') hbox.addWidget(remove_button) remove_button.released.connect(self._remove_ch) self._group_selector = ComboBox() group_model = self._group_selector.model() for i in range(_N_COLORS): self._group_selector.addItem(' ') color = QtGui.QColor() color.setRgb(*(255 * np.array(_UNIQUE_COLORS[i % _N_COLORS]) ).round().astype(int)) brush = QtGui.QBrush(color) brush.setStyle(QtCore.Qt.SolidPattern) group_model.setData(group_model.index(i, 0), brush, QtCore.Qt.BackgroundRole) self._group_selector.clicked.connect(self._select_group) self._group_selector.currentIndexChanged.connect( self._select_group) hbox.addWidget(self._group_selector) # update background color for current selection self._update_group() return hbox def _get_slider_bar(self): """Make a bar with sliders on it.""" def make_label(name): label = QLabel(name) label.setAlignment(QtCore.Qt.AlignCenter) return label def make_slider(smin, smax, sval, sfun=None): slider = QSlider(QtCore.Qt.Horizontal) slider.setMinimum(int(round(smin))) slider.setMaximum(int(round(smax))) slider.setValue(int(round(sval))) slider.setTracking(False) # only update on release if sfun is not None: slider.valueChanged.connect(sfun) slider.keyPressEvent = self._key_press_event return slider slider_hbox = QHBoxLayout() ch_vbox = QVBoxLayout() ch_vbox.addWidget(make_label('ch alpha')) ch_vbox.addWidget(make_label('ch radius')) slider_hbox.addLayout(ch_vbox) ch_slider_vbox = QVBoxLayout() self._alpha_slider = make_slider(0, 100, self._ch_alpha * 100, self._update_ch_alpha) ch_plot_max = _CH_PLOT_SIZE // 50 # max 1 / 50 of plot size ch_slider_vbox.addWidget(self._alpha_slider) self._radius_slider = make_slider(0, ch_plot_max, self._radius, self._update_radius) ch_slider_vbox.addWidget(self._radius_slider) slider_hbox.addLayout(ch_slider_vbox) ct_vbox = QVBoxLayout() ct_vbox.addWidget(make_label('CT min')) ct_vbox.addWidget(make_label('CT max')) slider_hbox.addLayout(ct_vbox) ct_slider_vbox = QVBoxLayout() ct_min = int(round(np.nanmin(self._ct_data))) ct_max = int(round(np.nanmax(self._ct_data))) self._ct_min_slider = make_slider( ct_min, ct_max, ct_min, self._update_ct_scale) ct_slider_vbox.addWidget(self._ct_min_slider) self._ct_max_slider = make_slider( ct_min, ct_max, ct_max, self._update_ct_scale) ct_slider_vbox.addWidget(self._ct_max_slider) slider_hbox.addLayout(ct_slider_vbox) return slider_hbox def _get_bottom_bar(self): """Make a bar at the bottom with information in it.""" hbox = QHBoxLayout() hbox.addStretch(10) self._intensity_label = QLabel('') # update later hbox.addWidget(self._intensity_label) RAS_label = QLabel('RAS =') self._RAS_textbox = QPlainTextEdit('') # update later self._RAS_textbox.setMaximumHeight(25) self._RAS_textbox.setMaximumWidth(200) self._RAS_textbox.focusOutEvent = self._update_RAS self._RAS_textbox.textChanged.connect(self._check_update_RAS) hbox.addWidget(RAS_label) hbox.addWidget(self._RAS_textbox) self._update_moved() # update text now return hbox def _group_channels(self, groups): """Automatically find a group based on the name of the channel.""" if groups is not None: for name in self._ch_names: if name not in groups: raise ValueError(f'{name} not found in ``groups``') _validate_type(groups[name], (float, int), f'groups[{name}]') self.groups = groups else: i = 0 self._groups = dict() base_names = dict() for name in self._ch_names: # strip all numbers from the name base_name = ''.join([letter for letter in name if not letter.isdigit() and letter != ' ']) if base_name in base_names: # look up group number by base name self._groups[name] = base_names[base_name] else: self._groups[name] = i base_names[base_name] = i i += 1 def _set_ch_names(self): """Add the channel names to the selector.""" self._ch_list_model = QtGui.QStandardItemModel(self._ch_list) for name in self._ch_names: self._ch_list_model.appendRow(QtGui.QStandardItem(name)) self._color_list_item(name=name) self._ch_list.setModel(self._ch_list_model) self._ch_list.clicked.connect(self._go_to_ch) self._ch_list.setCurrentIndex( self._ch_list_model.index(self._ch_index, 0)) self._ch_list.keyPressEvent = self._key_press_event def _select_group(self): """Change the group label to the selection.""" group = self._group_selector.currentIndex() self._groups[self._ch_names[self._ch_index]] = group # color differently if found already self._color_list_item(self._ch_names[self._ch_index]) self._update_group() def _update_group(self): """Set background for closed group menu.""" group = self._group_selector.currentIndex() rgb = (255 * np.array(_UNIQUE_COLORS[group % _N_COLORS]) ).round().astype(int) self._group_selector.setStyleSheet( 'background-color: rgb({:d},{:d},{:d})'.format(*rgb)) self._group_selector.update() def _on_scroll(self, event): """Process mouse scroll wheel event to zoom.""" self._zoom(event.step, draw=True) def _zoom(self, sign=1, draw=False): """Zoom in on the image.""" delta = _ZOOM_STEP_SIZE * sign for axis, fig in enumerate(self._figs): xmid = self._images['cursor'][axis].get_xdata()[0] ymid = self._images['cursor2'][axis].get_ydata()[0] xmin, xmax = fig.axes[0].get_xlim() ymin, ymax = fig.axes[0].get_ylim() xwidth = (xmax - xmin) / 2 - delta ywidth = (ymax - ymin) / 2 - delta if xwidth <= 0 or ywidth <= 0: return fig.axes[0].set_xlim(xmid - xwidth, xmid + xwidth) fig.axes[0].set_ylim(ymid - ywidth, ymid + ywidth) self._images['cursor'][axis].set_ydata([ymin, ymax]) self._images['cursor2'][axis].set_xdata([xmin, xmax]) if draw: self._figs[axis].canvas.draw() def _update_ch_selection(self): """Update which channel is selected.""" name = self._ch_names[self._ch_index] self._ch_list.setCurrentIndex( self._ch_list_model.index(self._ch_index, 0)) self._group_selector.setCurrentIndex(self._groups[name]) self._update_group() if not np.isnan(self._chs[name]).any(): self._ras[:] = self._chs[name] self._move_cursors_to_pos() self._update_camera(render=True) self._draw() def _go_to_ch(self, index): """Change current channel to the item selected.""" self._ch_index = index.row() self._update_ch_selection() @pyqtSlot() def _next_ch(self): """Increment the current channel selection index.""" self._ch_index = (self._ch_index + 1) % len(self._ch_names) self._update_ch_selection() @pyqtSlot() def _update_RAS(self, event): """Interpret user input to the RAS textbox.""" text = self._RAS_textbox.toPlainText().replace('\n', '') ras = text.split(',') if len(ras) != 3: ras = text.split(' ') # spaces also okay as in freesurfer ras = [var.lstrip().rstrip() for var in ras] if len(ras) != 3: self._update_moved() # resets RAS label return all_float = all([all([dig.isdigit() or dig in ('-', '.') for dig in var]) for var in ras]) if not all_float: self._update_moved() # resets RAS label return ras = np.array([float(var) for var in ras]) xyz = apply_trans(self._ras_vox_t, ras) wrong_size = any([var < 0 or var > n for var, n in zip(xyz, self._voxel_sizes)]) if wrong_size: self._update_moved() # resets RAS label return # valid RAS position, update and move self._ras = ras self._move_cursors_to_pos() @pyqtSlot() def _check_update_RAS(self): """Check whether the RAS textbox is done being edited.""" if '\n' in self._RAS_textbox.toPlainText(): self._update_RAS(event=None) self._ch_list.setFocus() # remove focus from text edit def _color_list_item(self, name=None): """Color the item in the view list for easy id of marked channels.""" name = self._ch_names[self._ch_index] if name is None else name color = QtGui.QColor('white') if not np.isnan(self._chs[name]).any(): group = self._groups[name] color.setRgb(*[int(c * 255) for c in _UNIQUE_COLORS[int(group) % _N_COLORS]]) brush = QtGui.QBrush(color) brush.setStyle(QtCore.Qt.SolidPattern) self._ch_list_model.setData( self._ch_list_model.index(self._ch_names.index(name), 0), brush, QtCore.Qt.BackgroundRole) # color text black color = QtGui.QColor('black') brush = QtGui.QBrush(color) brush.setStyle(QtCore.Qt.SolidPattern) self._ch_list_model.setData( self._ch_list_model.index(self._ch_names.index(name), 0), brush, QtCore.Qt.ForegroundRole) @pyqtSlot() def _toggle_snap(self): """Toggle snapping the contact location to the center of mass.""" if self._snap_button.text() == 'Off': self._snap_button.setText('On') self._snap_button.setStyleSheet("background-color: green") else: # text == 'On', turn off self._snap_button.setText('Off') self._snap_button.setStyleSheet("background-color: red") @pyqtSlot() def _mark_ch(self): """Mark the current channel as being located at the crosshair.""" name = self._ch_names[self._ch_index] if self._snap_button.text() == 'Off': self._chs[name][:] = self._ras else: coord = apply_trans(self._ras_vox_t, self._ras.copy()) shape = np.mean(self._mri_data.shape) # Freesurfer shape (256) voxels_max = int( 4 / 3 * np.pi * (shape * self._radius / _CH_PLOT_SIZE)**3) neighbors = _voxel_neighbors( coord, self._ct_data, thresh=0.5, voxels_max=voxels_max, use_relative=True) self._chs[name][:] = apply_trans( # to surface RAS self._vox_ras_t, np.array(list(neighbors)).mean(axis=0)) self._color_list_item() self._update_ch_images(draw=True) self._plot_3d_ch(name, render=True) self._save_ch_coords() self._next_ch() self._ch_list.setFocus() @pyqtSlot() def _remove_ch(self): """Remove the location data for the current channel.""" name = self._ch_names[self._ch_index] self._chs[name] *= np.nan self._color_list_item() self._save_ch_coords() self._update_ch_images(draw=True) self._plot_3d_ch(name, render=True) self._next_ch() self._ch_list.setFocus() def _draw(self, axis=None): """Update the figures with a draw call.""" for axis in (range(3) if axis is None else [axis]): self._figs[axis].canvas.draw() def _update_ch_images(self, axis=None, draw=False): """Update the channel image(s).""" for axis in range(3) if axis is None else [axis]: self._images['chs'][axis].set_data( self._make_ch_image(axis)) if draw: self._draw(axis) def _update_ct_images(self, axis=None, draw=False): """Update the CT image(s).""" for axis in range(3) if axis is None else [axis]: ct_data = np.take(self._ct_data, self._current_slice[axis], axis=axis).T # Threshold the CT so only bright objects (electrodes) are visible ct_data[ct_data < self._ct_min_slider.value()] = np.nan ct_data[ct_data > self._ct_max_slider.value()] = np.nan self._images['ct'][axis].set_data(ct_data) if draw: self._draw(axis) def _update_mri_images(self, axis=None, draw=False): """Update the CT image(s).""" if 'mri' in self._images: for axis in range(3) if axis is None else [axis]: self._images['mri'][axis].set_data( np.take(self._mri_data, self._current_slice[axis], axis=axis).T) if draw: self._draw(axis) def _update_images(self, axis=None, draw=True): """Update CT and channel images when general changes happen.""" self._update_ct_images(axis=axis) self._update_ch_images(axis=axis) self._update_mri_images(axis=axis) if draw: self._draw(axis) def _update_ct_scale(self): """Update CT min slider value.""" new_min = self._ct_min_slider.value() new_max = self._ct_max_slider.value() # handle inversions self._ct_min_slider.setValue(min([new_min, new_max])) self._ct_max_slider.setValue(max([new_min, new_max])) self._update_ct_images(draw=True) def _update_radius(self): """Update channel plot radius.""" self._radius = np.round(self._radius_slider.value()).astype(int) self._update_ch_images(draw=True) self._plot_3d_ch_pos(render=True) self._ch_list.setFocus() # remove focus from 3d plotter def _update_ch_alpha(self): """Update channel plot alpha.""" self._ch_alpha = self._alpha_slider.value() / 100 for axis in range(3): self._images['chs'][axis].set_alpha(self._ch_alpha) self._draw() self._plot_3d_ch_pos(render=True) self._ch_list.setFocus() # remove focus from 3d plotter def _get_click_pos(self, axis, x, y): """Get which axis was clicked and where.""" fx, fy = self._figs[axis].transFigure.inverted().transform((x, y)) xmin, xmax = self._figs[axis].axes[0].get_xlim() ymin, ymax = self._figs[axis].axes[0].get_ylim() return (fx * (xmax - xmin) + xmin, fy * (ymax - ymin) + ymin) def _move_cursors_to_pos(self): """Move the cursors to a position.""" x, y, z = apply_trans(self._ras_vox_t, self._ras) self._current_slice = np.array([x, y, z]).round().astype(int) self._move_cursor_to(0, x=y, y=z) self._move_cursor_to(1, x=x, y=z) self._move_cursor_to(2, x=x, y=y) self._zoom(0) # doesn't actually zoom just resets view to center self._update_images(draw=True) self._update_moved() def _move_cursor_to(self, axis, x, y): """Move the cursors to a position for a given subplot.""" self._images['cursor2'][axis].set_ydata([y, y]) self._images['cursor'][axis].set_xdata([x, x]) def _show_help(self): """Show the help menu.""" QMessageBox.information( self, 'Help', "Help:\n'm': mark channel location\n" "'r': remove channel location\n" "'b': toggle viewing of brain in T1\n" "'+'/'-': zoom\nleft/right arrow: left/right\n" "up/down arrow: superior/inferior\n" "page up/page down arrow: anterior/posterior") def _toggle_show_brain(self): """Toggle whether the brain/MRI is being shown.""" if 'mri' in self._images: for img in self._images['mri']: img.remove() self._images.pop('mri') self._toggle_brain_button.setText('Show Brain') else: self._images['mri'] = list() for axis in range(3): mri_data = np.take(self._mri_data, self._current_slice[axis], axis=axis).T self._images['mri'].append(self._figs[axis].axes[0].imshow( mri_data, cmap='hot', aspect='auto', alpha=0.25)) self._toggle_brain_button.setText('Hide Brain') self._draw() def _key_press_event(self, event): """Execute functions when the user presses a key.""" if event.key() == 'escape': self.close() if event.text() == 'h': self._show_help() if event.text() == 'm': self._mark_ch() if event.text() == 'r': self._remove_ch() if event.text() == 'b': self._toggle_show_brain() if event.text() in ('=', '+', '-'): self._zoom(sign=-2 * (event.text() == '-') + 1, draw=True) # Changing slices if event.key() in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_Left, QtCore.Qt.Key_Right, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown): if event.key() in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down): self._ras[2] += 2 * (event.key() == QtCore.Qt.Key_Up) - 1 elif event.key() in (QtCore.Qt.Key_Left, QtCore.Qt.Key_Right): self._ras[0] += 2 * (event.key() == QtCore.Qt.Key_Right) - 1 elif event.key() in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown): self._ras[1] += 2 * (event.key() == QtCore.Qt.Key_PageUp) - 1 self._move_cursors_to_pos() def _on_click(self, axis, event): """Move to view on MRI and CT on click.""" # Transform coordinates to figure coordinates pos = self._get_click_pos(axis, event.x, event.y) logger.info(f'Clicked axis {axis} at pos {pos}') if axis is not None and pos is not None: xyz = apply_trans(self._ras_vox_t, self._ras) if axis == 0: xyz[[1, 2]] = pos elif axis == 1: xyz[[0, 2]] = pos elif axis == 2: xyz[[0, 1]] = pos self._ras = apply_trans(self._vox_ras_t, xyz) self._move_cursors_to_pos() def _update_moved(self): """Update when cursor position changes.""" self._RAS_textbox.setPlainText('{:.2f}, {:.2f}, {:.2f}'.format( *self._ras)) self._intensity_label.setText('intensity = {:.2f}'.format( self._ct_data[tuple(self._current_slice)]))
def __init__(self, *args, **kwargs): super(KwWorkFontDialog, self).__init__(*args, **kwargs) #fontIds = [] #for font_fname in free_font_path_generator(): # fontIds = QFontDatabase.addApplicationFont(font_fname) #QFontDatabase.removeAllApplicationFonts() fontFreeFilterChecker = QCheckBox(u'Вільні') fontNoFreeFilterChecker = QCheckBox(u'Невільні') fontRating5FilterChecker = QCheckBox(u'Найкращі') fontRating4FilterChecker = QCheckBox(u'Добрі') fontRating3FilterChecker = QCheckBox(u'Задовільні') fontUseStrict_FilterChecker = QCheckBox(u'Діловодство, наука') fontUseWriting_FilterChecker = QCheckBox(u'Рукопис') fontUseFantasy_FilterChecker = QCheckBox(u'Декорації, реклама') fontFamilyListView = QListView() fontFamilyListView.setMinimumWidth(250) #fontFamilyModel = QStringListModel([str(fontDatabase.sty)]) fontStyleListView = QListView() fontStyleListView.setFixedWidth(150) fontSizeListView = QListView() fontSizeListView.setFixedWidth(60) fontSizeModel = QStringListModel([ str(v) for v in (6, 7, 8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72) ]) fontSizeListView.setModel(fontSizeModel) fontSizeListView.setEditTriggers(QAbstractItemView.NoEditTriggers) fontSampleWidget = QLabel(u'ІіЇїРр') fontSampleWidget.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) fontSampleWidget.setStyleSheet(r'''QLabel { font: normal normal 72pt "Consolas"; padding: 3px; qproperty-alignment: AlignCenter; color: black; background-color: white; }''') sampleGroup = QGroupBox(u'Зразок:') sampleLayout = QHBoxLayout(sampleGroup) sampleLayout.addWidget(fontSampleWidget) fontFilterLayout = QHBoxLayout() fontAccessFilterGroup = QGroupBox(u'Доступність:') fontAccessFilterLayout = QVBoxLayout(fontAccessFilterGroup) fontAccessFilterLayout.addWidget(fontFreeFilterChecker) fontAccessFilterLayout.addWidget(fontNoFreeFilterChecker) fontAccessFilterLayout.addStretch() fontFilterLayout.addWidget(fontAccessFilterGroup) fontUseFilterGroup = QGroupBox(u'Призначення:') fontUseFilterLayout = QVBoxLayout(fontUseFilterGroup) fontUseFilterLayout.addWidget(fontUseStrict_FilterChecker) fontUseFilterLayout.addWidget(fontUseWriting_FilterChecker) fontUseFilterLayout.addWidget(fontUseFantasy_FilterChecker) fontFilterLayout.addWidget(fontUseFilterGroup) fontRatingFilterGroup = QGroupBox(u'Оцінка:') fontRatingFilterLayout = QVBoxLayout(fontRatingFilterGroup) fontRatingFilterLayout.addWidget(fontRating5FilterChecker) fontRatingFilterLayout.addWidget(fontRating4FilterChecker) fontRatingFilterLayout.addWidget(fontRating3FilterChecker) fontFilterLayout.addWidget(fontRatingFilterGroup) fontLayout = QGridLayout() familyLayout = QVBoxLayout() familyLayout.setSpacing(3) familyLayout.addWidget(QLabel(u'Шрифт:')) familyLayout.addWidget(fontFamilyListView) fontLayout.addLayout(familyLayout, 0, 0) styleLayout = QVBoxLayout() styleLayout.setSpacing(3) styleLayout.addWidget(QLabel(u'Стиль:')) styleLayout.addWidget(fontStyleListView) fontLayout.addLayout(styleLayout, 0, 1) sizeLayout = QVBoxLayout() sizeLayout.setSpacing(3) sizeLayout.addWidget(QLabel(u'Розмір:')) sizeLayout.addWidget(fontSizeListView) fontLayout.addLayout(sizeLayout, 0, 2) fontLayout.addWidget(sampleGroup, 1, 0, 1, 3) bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) layout = QVBoxLayout(self) testButton = QPushButton(u'Тестова: відкрити шрифт') testButton.clicked.connect(lambda: QFontDialog.getFont()) layout.addWidget(testButton) layout.addLayout(fontFilterLayout) layout.addLayout(fontLayout) layout.addSpacing(5) layout.addWidget(bbox) self.setWindowTitle(u'Вибір шрифта') self.setWindowFlags(self.windowFlags() ^ Qt.WindowContextHelpButtonHint) # Налаштування. fontMaxPointSize = fontSizeListView.font() # print(QFontDatabase.families()) fontMaxPointSize.setPointSize(72) fm = QFontMetrics(fontMaxPointSize) fontSampleWidget.setMinimumHeight( fm.size(Qt.TextSingleLine | Qt.TextShowMnemonic, u'Їр').height()) fontSizeListView.setCurrentIndex( fontSizeModel.match(fontSizeModel.index(0, 0), Qt.DisplayRole, fontSizeListView.font().pointSize())[0]) fontFreeFilterChecker.setChecked(True) fontUseStrict_FilterChecker.setChecked(True) fontRating5FilterChecker.setChecked(True) fontRating4FilterChecker.setChecked(True)
class MusicPlayer(QWidget): def __init__(self, parent=None): QWidget.__init__(self, parent) self.options = self.parent().options self.ffmpeg = self.options['paths']['ffmpeg_bin'] self.thumbnails = self.options['paths']['thumbnails'] self.thumb_width = self.options['thumbnail']['width'] self.thumb_height = self.options['thumbnail']['height'] self.jumping = False self.blocked = False self.durations = {} self.playback_value = 0 self.text = ["None", "Repeat", "Random"] self.playlist_list = [] self.values = [ QMediaPlaylist.Loop, QMediaPlaylist.CurrentItemInLoop, QMediaPlaylist.Random ] # Thumbnail widget self.image_label = QLabel() # Control widgets self.playButton = QToolButton(clicked=self.play) self.playButton.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) self.stopButton = QToolButton(clicked=self.stop) self.stopButton.setIcon(self.style().standardIcon(QStyle.SP_MediaStop)) self.stopButton.setEnabled(False) self.playbackButton = QToolButton(clicked=self.playback_mode) self.playbackButton.setText(self.text[0]) self.nextButton = QToolButton(clicked=self.next_song) self.nextButton.setIcon(self.style().standardIcon( QStyle.SP_MediaSkipForward)) self.previousButton = QToolButton(clicked=self.previous_song) self.previousButton.setIcon(self.style().standardIcon( QStyle.SP_MediaSkipBackward)) self.muteButton = QToolButton(clicked=self.mute_clicked) self.muteButton.setIcon(self.style().standardIcon( QStyle.SP_MediaVolume)) self.volumeSlider = QSlider(Qt.Horizontal, sliderMoved=self.change_volume) self.volumeSlider.setRange(0, 100) self.volumeSlider.setPageStep(1) self.volumeSlider.setValue(50) # Player and playlist setup self.player = QMediaPlayer() self.player.setVolume(50) self.player.stateChanged.connect(self.waveform) self.playlist = QMediaPlaylist() self.playlist.setPlaybackMode(self.values[0]) self.playlist.setCurrentIndex(1) self.player.setPlaylist(self.playlist) self.playlistModel = PlaylistModel() self.playlistModel.setPlaylist(self.playlist) self.playlistView = QListView() self.playlistView.setModel(self.playlistModel) self.playlistView.setCurrentIndex( self.playlistModel.index(self.playlist.currentIndex(), 0)) self.playlistView.activated.connect(self.jump) self.playlistView.setContextMenuPolicy(Qt.CustomContextMenu) self.playlistView.customContextMenuRequested.connect( self.list_view_menu) self.playlist.currentIndexChanged.connect( lambda position: self.change_thumbnail(position)) song_search = QLineEdit() song_search.textChanged.connect(self.search) song_search.setClearButtonEnabled(True) # Playlist self.playlist_name = QComboBox() self.playlist_name.currentTextChanged.connect(self.switch_playlist) self.refresh_lists() self.up_button = QToolButton(clicked=self.move_up) self.up_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowUp)) self.down_button = QToolButton(clicked=self.move_down) self.down_button.setIcon(self.style().standardIcon( QStyle.SP_ArrowDown)) # Sound wave widget self.wave_graphic = WaveGraphic(self) #self.wave_graphic.hide() # Testing slider again self.slider = QSlider(Qt.Horizontal) self.slider.setRange(0, self.player.duration() / 1000) self.slider.sliderMoved.connect(self.seek) self.player.durationChanged.connect(self.durationChanged) self.player.positionChanged.connect(self.positionChanged) # Shortcuts QShortcut(QKeySequence(Qt.CTRL + Qt.Key_F), self, song_search.setFocus) # Layouts setup playlist_layout = QHBoxLayout() playlist_layout.addWidget(self.playlist_name) playlist_layout.addWidget(self.up_button) playlist_layout.addWidget(self.down_button) control_layout = QHBoxLayout() control_layout.setContentsMargins(0, 0, 0, 0) control_layout.addWidget(self.stopButton) control_layout.addWidget(self.previousButton) control_layout.addWidget(self.playButton) control_layout.addWidget(self.nextButton) control_layout.addWidget(self.muteButton) control_layout.addWidget(self.volumeSlider) control_layout.addWidget(self.playbackButton) display_layout = QVBoxLayout() display_layout.addWidget(song_search) display_layout.addWidget(self.playlistView) display_layout.addLayout(playlist_layout) music_layout = QVBoxLayout() music_layout.addWidget(self.image_label) music_layout.addWidget(self.slider) music_layout.addLayout(control_layout) main_layout = QHBoxLayout() main_layout.addLayout(music_layout) main_layout.addLayout(display_layout) main_2_layout = QVBoxLayout() main_2_layout.addLayout(main_layout) main_2_layout.addWidget(self.wave_graphic) self.setLayout(main_2_layout) def waveform(self, status): if status == QMediaPlayer.PlayingState: self.wave_graphic.start() elif status == QMediaPlayer.PausedState: self.wave_graphic.pause() else: self.wave_graphic.stop() def list_view_menu(self, point): menu = QMenu("Menu", self) recommend_action = QAction('&Recommend Songs', self) menu.addAction(recommend_action) # TODO: [FEATURE] add rename song recommend_action.triggered.connect( lambda: self.parent().call_download_manager(self.get_links())) rename_action = QAction('&Rename', self) menu.addAction(rename_action) # TODO: [FEATURE] add rename song rename_action.triggered.connect(lambda: print("rename")) # Show the context menu. menu.exec_(self.playlistView.mapToGlobal(point)) def get_links(self): title = self.playlistView.selectedIndexes()[0].data() link = getYoutubeURLFromSearch(title) if len(link) > 1: return youtube_recommendations(link) def move_up(self): index = self.playlistView.currentIndex().row() if index - 1 >= 0: item, above = self.playlist.media(index), self.playlist.media( index - 1) self.playlist.removeMedia(index) self.playlist.removeMedia(index - 1) self.playlist.insertMedia(index - 1, item) self.playlist.insertMedia(index, above) self.blocked = True self.playlistView.setCurrentIndex( self.playlistModel.index(index - 1, 0)) self.blocked = False self.stop() def move_down(self): index = self.playlistView.currentIndex().row() if index + 1 <= self.playlistModel.rowCount(): item, below = self.playlist.media(index), self.playlist.media( index + 1) self.playlist.removeMedia(index + 1) self.playlist.removeMedia(index) self.playlist.insertMedia(index, below) self.playlist.insertMedia(index + 1, item) self.blocked = True self.playlistView.setCurrentIndex( self.playlistModel.index(index + 1, 0)) self.blocked = False self.stop() def search(self, part_of_song): for index in range(self.playlistModel.rowCount()): item = self.playlistModel.data(self.playlistModel.index( index, 0)).lower() self.playlistView.setRowHidden(index, part_of_song.lower() not in item) def change_thumbnail(self, position=0): self.playlistView.setCurrentIndex(self.playlistModel.index( position, 0)) song = self.playlistView.selectedIndexes()[0].data() if self.wave_graphic.is_song_cached(song): self.wave_graphic.load_waves(song) else: wc_ = WaveConverter(song, self.wave_graphic.set_wav) wc_.convert() if self.playlistView.currentIndex().data() is None or self.blocked: return max_ratio = 0 img = None for item in listdir(self.thumbnails): if item.endswith('.jpg'): ratio = similar( item, self.playlistView.currentIndex().data().replace( '.mp3', '.jpg')) if ratio > max_ratio: max_ratio = ratio img = item if img: p = QPixmap(self.thumbnails + img) self.image_label.setPixmap( p.scaled(self.thumb_width, self.thumb_height, Qt.KeepAspectRatio)) def switch_playlist(self, current_text): self.playlist.clear() if current_text == "No Playlist": self.refresh() else: if read_playlist(current_text): songs = read_playlist(current_text).split('\n') for song in songs: self.playlist.addMedia( QMediaContent(QUrl.fromLocalFile(song))) def refresh(self): # Change it so it will go to same song. if self.playlist_name.currentText() != "No Playlist": return paths = fetch_options()['paths']['music_path'].split(';') current_songs = [ self.playlistModel.data(self.playlistModel.index(row, 0)) for row in range(self.playlistModel.rowCount()) ] for path in paths: if not path: continue for item in listdir(path): if isfile(join(path, item)) and item.endswith(".mp3") and ( item not in current_songs): self.playlist.addMedia( QMediaContent(QUrl.fromLocalFile(join(path, item)))) def refresh_lists(self): path = fetch_options()['paths']['playlist'] self.playlist_name.clear() self.playlist_list = ["No Playlist"] self.playlist_name.addItem("No Playlist") for item in listdir(path): if isfile(join(path, item)) and item.endswith(".lst"): self.playlist_list.append(item.split('.')[0]) self.playlist_name.addItem(item.split('.')[0]) def playback_mode(self): # Normal -> Loop -> Random def up_value(value, max_value=3): if value + 1 == max_value: return 0 return value + 1 self.playback_value = up_value(self.playback_value) self.playlist.setPlaybackMode(self.values[self.playback_value]) self.playbackButton.setText(self.text[self.playback_value]) def jump(self, index): if index.isValid() and not self.blocked: self.playlist.setCurrentIndex(index.row()) self.jumping = True self.play() def play(self): if self.blocked: return if self.player.state() != QMediaPlayer.PlayingState or self.jumping: self.player.play() self.playButton.setIcon(self.style().standardIcon( QStyle.SP_MediaPause)) self.jumping = False else: self.player.pause() self.playButton.setIcon(self.style().standardIcon( QStyle.SP_MediaPlay)) self.stopButton.setEnabled(True) def change_volume(self, value): self.player.setVolume(value) def stop(self): if self.player.state() != QMediaPlayer.StoppedState: self.player.stop() self.playButton.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) self.stopButton.setEnabled(False) def next_song(self): self.playlist.next() self.playlistView.setCurrentIndex( self.playlistModel.index(self.playlist.currentIndex(), 0)) def previous_song(self): self.playlist.previous() self.playlistView.setCurrentIndex( self.playlistModel.index(self.playlist.currentIndex(), 0)) def mute_clicked(self): self.player.setMuted(not self.player.isMuted()) self.muteButton.setIcon(self.style().standardIcon( QStyle.SP_MediaVolume if not self.player.isMuted() else QStyle. SP_MediaVolumeMuted)) def durationChanged(self, duration): duration /= 1000 self.slider.setMaximum(duration) def positionChanged(self, progress): progress /= 1000 if not self.slider.isSliderDown(): self.slider.setValue(progress) def seek(self, seconds): self.player.setPosition(seconds * 1000)
class LocationCompleterView(QWidget): def __init__(self): super().__init__(None) self._view = None # QListView self._delegate = None # LocationCompleterDelegate self._searchEnginesLayout = None # QHBoxLayout self._resizeHeight = -1 self._resizeTimer = None # QTimer self._forceResize = True self.setAttribute(Qt.WA_ShowWithoutActivating) self.setAttribute(Qt.WA_X11NetWmWindowTypeCombo) if gVar.app.platformName() == 'xcb': self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint | Qt.BypassWindowManagerHint) else: self.setWindowFlags(Qt.Popup) layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self._view = QListView(self) layout.addWidget(self._view) self._view.setUniformItemSizes(True) self._view.setEditTriggers(QAbstractItemView.NoEditTriggers) self._view.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel) self._view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self._view.setSelectionBehavior(QAbstractItemView.SelectRows) self._view.setSelectionMode(QAbstractItemView.SingleSelection) self._view.setMouseTracking(True) gVar.app.installEventFilter(self) self._delegate = LocationCompleterDelegate(self) self._view.setItemDelegate(self._delegate) searchFrame = QFrame(self) searchFrame.setFrameStyle(QFrame.StyledPanel | QFrame.Raised) searchLayout = QHBoxLayout(searchFrame) searchLayout.setContentsMargins(10, 4, 4, 4) searchSettingsButton = ToolButton(self) searchSettingsButton.setIcon(IconProvider.settingsIcon()) searchSettingsButton.setToolTip(_('Manage Search Engines')) searchSettingsButton.setAutoRaise(True) searchSettingsButton.setIconSize(QSize(16, 16)) searchSettingsButton.clicked.connect(self.searchEnginesDialogRequested) searchLabel = QLabel(_('Search with:')) self._searchEnginesLayout = QHBoxLayout() self._setupSearchEngines() gVar.app.searchEnginesManager().enginesChanged.connect( self._setupSearchEngines) searchLayout.addWidget(searchLabel) searchLayout.addLayout(self._searchEnginesLayout) searchLayout.addStretch() searchLayout.addWidget(searchSettingsButton) layout.addWidget(searchFrame) def model(self): ''' @return: QAbstractItemModel ''' return self._view.model() def setModel(self, model): ''' @param model QAbstractItemModel ''' self._view.setModel(model) def selectionModel(self): ''' @return: QItemSelectionModel ''' return self._view.selectionModel() def currentIndex(self): ''' @return: QModelIndex ''' return self._view.currentIndex() def setCurrentIndex(self, index): ''' @param index QModelIndex ''' self._view.setCurrentIndex(index) def adjustSize(self): maxItemsCount = 12 newHeight = self._view.sizeHintForRow(0) * min(maxItemsCount, self.model().rowCount()) if not self._resizeTimer: self._resizeTimer = QTimer(self) self._resizeTimer.setInterval(200) def func(): if self._resizeHeight > 0: self._view.setFixedHeight(self._resizeHeight) self.setFixedHeight(self.sizeHint().height()) self._resizeHeight = -1 self._resizeTimer.timeout.connect(func) if not self._forceResize: if newHeight == self._resizeHeight: return elif newHeight == self._view.height(): self._resizeHeight = -1 return elif newHeight < self._view.height(): self._resizeHeight = newHeight self._resizeTimer.start() return self._resizeHeight = -1 self._forceResize = False self._view.setFixedHeight(newHeight) self.setFixedHeight(self.sizeHint().height()) # override def eventFilter(self, obj, event): # noqa C901 ''' @param obj QObject @param event QEvent @return: bool ''' # Event filter based on QCompleter::eventFilter from qcompleter.cpp if obj == self or obj == self._view or not self.isVisible(): return False evtType = event.type() if obj == self._view.viewport(): if evtType == QEvent.MouseButtonRelease: # QMouseEvent e = event index = self._view.indexAt(e.pos()) if not index.isValid(): return False # Qt::MouseButton button = e.button() # Qt::KeyboardModifiers modifiers = e.modifiers() if button == Qt.LeftButton and modifiers == Qt.NoModifier: self.indexActivated.emit(index) return True if button == Qt.MiddleButton or (button == Qt.LeftButton and modifiers == Qt.ControlModifier): self.indexCtrlActivated.emit(index) return True if button == Qt.LeftButton and modifiers == Qt.ShiftModifier: self.indexShiftActivated.emit(index) return True return False if evtType == QEvent.KeyPress: # QKeyEvent keyEvent = event evtKey = keyEvent.key() modifiers = keyEvent.modifiers() index = self._view.currentIndex() item = self.model().index(0, 0) if item.data(LocationCompleterModel.VisitSearchItemRole): visitSearchIndex = item else: visitSearchIndex = QModelIndex() if (evtKey == Qt.Key_Up or evtKey == Qt.Key_Down) and \ self._view.currentIndex() != index: self._view.setCurrentIndex(index) # TODO: ? if evtKey in (Qt.Key_Return, Qt.Key_Enter): if index.isValid(): if modifiers == Qt.NoModifier or modifiers == Qt.KeypadModifier: self.indexActivated.emit(index) return True if modifiers == Qt.ControlModifier: self.indexCtrlActivated.emit(index) return True if modifiers == Qt.ShiftModifier: self.indexShiftActivated.emit(index) return True elif evtKey == Qt.Key_End: if modifiers & Qt.ControlModifier: self._view.setCurrentIndex(self.model().index( self.model().rowCount() - 1, 0)) return True else: self.close() elif evtKey == Qt.Key_Home: if modifiers & Qt.ControlModifier: self._view.setCurrentIndex(self.model().index(0, 0)) self._view.scrollToTop() return True else: self.close() elif evtKey == Qt.Key_Escape: self.close() return True elif evtKey == Qt.Key_F4: if modifiers == Qt.AltModifier: self.close() return False elif evtKey in (Qt.Key_Tab, Qt.Key_Backtab): if modifiers != Qt.NoModifier and modifiers != Qt.ShiftModifier: return False isBack = evtKey == Qt.Key_Backtab if evtKey == Qt.Key_Tab and modifiers == Qt.ShiftModifier: isBack = True ev = QKeyEvent(QKeyEvent.KeyPress, isBack and Qt.Key_Up or Qt.Key_Down, Qt.NoModifier) QApplication.sendEvent(self.focusProxy(), ev) return True elif evtKey in (Qt.Key_Up, Qt.Key_PageUp): if modifiers != Qt.NoModifier: return False step = evtKey == Qt.Key_PageUp and 5 or 1 if not index.isValid() or index == visitSearchIndex: rowCount = self.model().rowCount() lastIndex = self.model().index(rowCount - 1, 0) self._view.setCurrentIndex(lastIndex) elif index.row() == 0: self._view.setCurrentIndex(QModelIndex()) else: row = max(0, index.row() - step) self._view.setCurrentIndex(self.model().index(row, 0)) return True elif evtKey in (Qt.Key_Down, Qt.Key_PageDown): if modifiers != Qt.NoModifier: return False step = evtKey == Qt.Key_PageDown and 5 or 1 if not index.isValid(): firstIndex = self.model().index(0, 0) self._view.setCurrentIndex(firstIndex) elif index != visitSearchIndex and index.row( ) == self.model().rowCount() - 1: self._view.setCurrentIndex(visitSearchIndex) self._view.scrollToTop() else: row = min(self.model().rowCount() - 1, index.row() + step) self._view.setCurrentIndex(self.model().index(row, 0)) return True elif evtKey == Qt.Key_Delete: if index != visitSearchIndex and self._view.viewport().rect( ).contains(self._view.visualRect(index)): self.indexDeleteRequested.emit(index) return True elif evtKey == Qt.Key_Shift: self._delegate.setForceVisitItem(True) self._view.viewport().update() # end of switch evtKey if self.focusProxy(): self.focusProxy().event(keyEvent) return True elif evtType == QEvent.KeyRelease: if event.key() == Qt.Key_Shift: self._delegate.setForceVisitItem(False) self._view.viewport().update() return True elif evtType in (QEvent.Wheel, QEvent.MouseButtonPress): if not self.underMouse(): self.close() return False elif evtType == QEvent.FocusOut: # QFocusEvent focusEvent = event reason = focusEvent.reason() if reason != Qt.PopupFocusReason and reason != Qt.MouseFocusReason: self.close() elif evtType in (QEvent.Move, QEvent.Resize): w = obj if isinstance(w, QWidget) and w.isWindow() and self.focusProxy( ) and w == self.focusProxy().window(): self.close() # end of switch evtType return False # Q_SIGNALS closed = pyqtSignal() searchEnginesDialogRequested = pyqtSignal() loadRequested = pyqtSignal(LoadRequest) indexActivated = pyqtSignal(QModelIndex) indexCtrlActivated = pyqtSignal(QModelIndex) indexShiftActivated = pyqtSignal(QModelIndex) indexDeleteRequested = pyqtSignal(QModelIndex) # public Q_SLOTS: def close(self): self.hide() self._view.verticalScrollBar().setValue(0) self._delegate.setForceVisitItem(False) self._forceResize = True self.closed.emit() # private: def _setupSearchEngines(self): for idx in (range(self._searchEnginesLayout.count())): item = self._searchEnginesLayout.takeAt(0) item.deleteLater() engines = gVar.app.searchEnginesManager().allEngines() for engine in engines: button = ToolButton(self) button.setIcon(engine.icon) button.setToolTip(engine.name) button.setAutoRaise(True) button.setIconSize(QSize(16, 16)) def func(): text = self.model().index(0, 0).data( LocationCompleterModel.SearchStringRole) self.loadRequested.emit( gVar.app.searchEngineManager().searchResult(engine, text)) button.clicked.connect(func) self._searchEnginesLayout.addWidget(button)
class ObjectFileView(QDialog): def __init__(self, name): super().__init__() self.setWindowTitle(name) with open( os.path.join(get_jak_path(), "decompiler_out", "{}_asm.json".format(name))) as f: self.asm_data = json.loads(f.read()) main_layout = QVBoxLayout() monospaced_font = get_monospaced_font() self.header_label = QLabel() main_layout.addWidget(self.header_label) function_splitter = QSplitter() function_splitter.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) self.function_list = QTreeView() self.function_list_model = QStandardItemModel() self.functions_by_name = dict() root = self.function_list_model.invisibleRootItem() seg_roots = [] for i in range(3): seg_entry = QStandardItem(segment_id_to_name(i)) seg_entry.setFont(monospaced_font) seg_entry.setEditable(False) root.appendRow(seg_entry) seg_roots.append(seg_entry) for f in self.asm_data["functions"]: function_entry = QStandardItem(f["name"]) function_entry.setFont(monospaced_font) function_entry.setEditable(False) seg_roots[f["segment"]].appendRow(function_entry) self.functions_by_name[f["name"]] = f self.header_label.setText( "Object File {} Functions ({} total):".format( name, len(self.asm_data["functions"]))) self.function_list.setModel(self.function_list_model) self.function_list.clicked.connect(self.display_function) function_splitter.addWidget(self.function_list) layout = QVBoxLayout() self.function_header_label = QLabel("No function selected") self.function_header_label.setFont(monospaced_font) self.header_label.setSizePolicy( QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)) layout.addWidget(self.function_header_label) self.op_asm_split_view = QSplitter() self.op_asm_split_view.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) self.basic_op_pane = QListView() self.basic_op_pane.clicked.connect(self.basic_op_clicked) #layout.addWidget(self.basic_op_pane) self.op_asm_split_view.addWidget(self.basic_op_pane) self.asm_pane = QListView() self.op_asm_split_view.addWidget(self.asm_pane) layout.addWidget(self.op_asm_split_view) self.asm_display = QPlainTextEdit() self.asm_display.setMaximumHeight(80) layout.addWidget(self.asm_display) self.warnings_label = QLabel() layout.addWidget(self.warnings_label) widget = QWidget() widget.setLayout(layout) function_splitter.addWidget(widget) main_layout.addWidget(function_splitter) # add it to the window! self.setLayout(main_layout) def display_function(self, item): name = item.data() monospaced_font = get_monospaced_font() func = self.functions_by_name[name] basic_op_model = QStandardItemModel() basic_op_root = basic_op_model.invisibleRootItem() asm_model = QStandardItemModel() asm_root = asm_model.invisibleRootItem() self.basic_id_to_asm = [] self.current_function = name op_idx = 0 basic_idx = 0 for op in func["asm"]: if "label" in op: asm_item = QStandardItem(op["label"] + "\n " + op["asm_op"]) else: asm_item = QStandardItem(" " + op["asm_op"]) asm_item.setFont(monospaced_font) asm_item.setEditable(False) asm_root.appendRow(asm_item) if "basic_op" in op: if "label" in op: basic_item = QStandardItem(op["label"] + "\n " + op["basic_op"]) else: basic_item = QStandardItem(" " + op["basic_op"]) basic_item.setFont(monospaced_font) basic_item.setEditable(False) basic_op_root.appendRow(basic_item) self.basic_id_to_asm.append(op_idx) basic_idx = basic_idx + 1 op_idx = op_idx + 1 self.basic_id_to_asm.append(op_idx) self.basic_op_pane.setModel(basic_op_model) self.asm_pane.setModel(asm_model) self.warnings_label.setText(func["warnings"]) self.asm_display.setPlainText("") self.function_header_label.setText( "{}, type: {}\nfunc: {} obj: {}".format(name, func["type"], func["name"], func["parent_object"])) def basic_op_clicked(self, item): text = "" added_reg = 0 asm_idx = self.basic_id_to_asm[item.row()] asm_op = self.functions_by_name[self.current_function]["asm"][asm_idx] if "type_map" in asm_op: for reg, type_name in asm_op["type_map"].items(): text += "{}: {} ".format(reg, type_name) added_reg += 1 if added_reg >= 4: text += "\n" added_reg = 0 text += "\n" for i in range(asm_idx, self.basic_id_to_asm[item.row() + 1]): text += self.functions_by_name[ self.current_function]["asm"][i]["asm_op"] + "\n" op = self.functions_by_name[self.current_function]["asm"][asm_idx] if "referenced_string" in op: text += op["referenced_string"] self.asm_display.setPlainText(text) self.asm_display.setFont(get_monospaced_font()) self.asm_pane.setCurrentIndex(self.asm_pane.model().index(asm_idx, 0))
class Player(QWidget): fullScreenChanged = pyqtSignal(bool) videoWidget = None colorDialog = None videoWidget = None trackInfo = "" statusInfo = "" duration = 0 def __init__(self, playlist, parent=None): """ :param playlist: :param parent: """ super(Player, self).__init__(parent) self._init_audio_player() self._init_video_player() self._init_addtional_controls() control_layout, controls, open_button = self._init_player_controls_layout( ) self._init_playlist_view() self._init_layout(control_layout=control_layout) self.check_for_player_service(controls=controls, open_button=open_button) self.meta_data_changed() self.add_to_playlist(playlist) def check_for_player_service(self, controls, open_button): """ :param controls: :param open_button: :return: """ if not self.player.isAvailable(): QMessageBox.warning( self, "Service not available", "The QMediaPlayer object does not have a valid service.\n" "Please check the media service plugins are installed.") controls.setEnabled(False) self.playlistView.setEnabled(False) open_button.setEnabled(False) self.colorButton.setEnabled(False) self.fullScreenButton.setEnabled(False) def _init_layout(self, control_layout): """ :param control_layout: :return: """ display_layout = QHBoxLayout() if self.videoWidget: display_layout.addWidget(self.videoWidget, 2) if self.playlistView: display_layout.addWidget(self.playlistView) layout = QVBoxLayout() layout.addLayout(display_layout) if self.slider or self.labelDuration: h_layout = QHBoxLayout() if self.slider: h_layout.addWidget(self.slider) if self.labelDuration: h_layout.addWidget(self.labelDuration) layout.addLayout(h_layout) if control_layout: layout.addLayout(control_layout) layout.addLayout(self._get_histogram_layout()) self.setLayout(layout) def _init_playlist_view(self): """ :return: """ self.playlistView = QListView() self.playlistView.setModel(self.playlistModel) self.playlistView.setCurrentIndex( self.playlistModel.index(self.playlist.currentIndex(), 0)) self.playlistView.activated.connect(self.jump) def _init_addtional_controls(self): """ :return: """ self.slider = QSlider(Qt.Horizontal) self.slider.setRange(0, self.player.duration() / 1000) self.labelDuration = QLabel() self.slider.sliderMoved.connect(self.seek) self.fullScreenButton = QPushButton("FullScreen") self.fullScreenButton.setCheckable(True) self.colorButton = QPushButton("Color Options...") self.colorButton.setEnabled(False) self.colorButton.clicked.connect(self.show_color_dialog) def _init_player_controls_layout(self): """ :return: """ openButton = QPushButton("Open", clicked=self.open) controls = PlayerControls() controls.set_state(self.player.state()) controls.set_volume(self.player.volume()) controls.set_muted(controls.is_muted()) controls.play.connect(self.player.play) controls.pause.connect(self.player.pause) controls.stop.connect(self.player.stop) controls.next.connect(self.playlist.next) controls.previous.connect(self.previous_clicked) controls.changeVolume.connect(self.player.setVolume) controls.changeMuting.connect(self.player.setMuted) controls.changeRate.connect(self.player.setPlaybackRate) if self.videoWidget: controls.stop.connect(self.videoWidget.update) self.player.stateChanged.connect(controls.set_state) self.player.volumeChanged.connect(controls.set_volume) self.player.mutedChanged.connect(controls.set_muted) controlLayout = QHBoxLayout() controlLayout.setContentsMargins(0, 0, 0, 0) controlLayout.addWidget(openButton) controlLayout.addStretch(1) controlLayout.addWidget(controls) controlLayout.addStretch(1) if self.fullScreenButton: controlLayout.addWidget(self.fullScreenButton) if self.colorButton: controlLayout.addWidget(self.colorButton) return controlLayout, controls, openButton def _init_audio_player(self): """ :return: """ self.playlist = QMediaPlaylist() self.player = QMediaPlayer() self.player.setPlaylist(self.playlist) self.player.durationChanged.connect(self.duration_changed) self.player.positionChanged.connect(self.position_changed) self.player.metaDataChanged.connect(self.meta_data_changed) self.playlist.currentIndexChanged.connect( self.playlist_position_changed) self.player.mediaStatusChanged.connect(self.status_changed) self.player.bufferStatusChanged.connect(self.buffering_progress) self.player.videoAvailableChanged.connect(self.video_available_changed) self.player.error.connect(self.display_error_message) self.playlistModel = PlaylistModel() self.playlistModel.set_playlist(self.playlist) def _init_video_player(self): """ :return: """ self.histogram = HistogramWidget() self.videoWidget = VideoWidget() self.player.setVideoOutput(self.videoWidget) self.probe = QVideoProbe() self.probe.videoFrameProbed.connect(self.histogram.process_frame) self.probe.setSource(self.player) def _get_histogram_layout(self): """ :return: """ self.labelHistogram = QLabel() self.labelHistogram.setText("Histogram:") histogramLayout = QHBoxLayout() histogramLayout.addWidget(self.labelHistogram) if self.histogram: histogramLayout.addWidget(self.histogram, 1) return histogramLayout def open(self): """ :return: """ file_names, _ = QFileDialog.getOpenFileNames(self, "Open Files") self.add_to_playlist(file_names) def add_to_playlist(self, file_names): """ :param file_names: :return: """ for name in file_names: file_info = QFileInfo(name) if file_info.exists(): url = QUrl.fromLocalFile(file_info.absoluteFilePath()) if file_info.suffix().lower() == 'm3u': self.playlist.load(url) else: self.playlist.addMedia(QMediaContent(url)) else: url = QUrl(name) if url.isValid(): self.playlist.addMedia(QMediaContent(url)) def duration_changed(self, duration): """ :param duration: :return: """ duration /= 1000 self.duration = duration self.slider.setMaximum(duration) def position_changed(self, progress): """ :param progress: :return: """ progress /= 1000 if not self.slider.isSliderDown(): self.slider.setValue(progress) self.update_duration_info(progress) def meta_data_changed(self): """ :return: """ if self.player.isMetaDataAvailable(): self.set_track_info( "%s - %s" % (self.player.metaData(QMediaMetaData.AlbumArtist), self.player.metaData(QMediaMetaData.Title))) def previous_clicked(self): """ :return: """ if self.player.position() <= 5000: self.playlist.previous() else: self.player.setPosition(0) def jump(self, index): """ :param index: :return: """ if index.isValid(): self.playlist.setCurrentIndex(index.row()) self.player.play() def playlist_position_changed(self, position): """ :param position: :return: """ self.playlistView.setCurrentIndex(self.playlistModel.index( position, 0)) def seek(self, seconds): """ :param seconds: :return: """ self.player.setPosition(seconds * 1000) def status_changed(self, status): """ :param status: :return: """ self.handle_cursor(status) if status == QMediaPlayer.LoadingMedia: self.set_status_info("Loading...") elif status == QMediaPlayer.StalledMedia: self.set_status_info("Media Stalled") elif status == QMediaPlayer.EndOfMedia: QApplication.alert(self) elif status == QMediaPlayer.InvalidMedia: self.display_error_message() else: self.set_status_info("") def handle_cursor(self, status): """ :param status: :return: """ if status in (QMediaPlayer.LoadingMedia, QMediaPlayer.BufferingMedia, QMediaPlayer.StalledMedia): self.setCursor(Qt.BusyCursor) else: self.unsetCursor() def buffering_progress(self, progress): """ :param progress: :return: """ self.set_status_info("Buffering %d%" % progress) def video_available_changed(self, available): """ :param available: :return: """ if available: self.fullScreenButton.clicked.connect( self.videoWidget.setFullScreen) self.videoWidget.fullScreenChanged.connect( self.fullScreenButton.setChecked) if self.fullScreenButton.isChecked(): self.videoWidget.setFullScreen(True) else: self.fullScreenButton.clicked.disconnect( self.videoWidget.setFullScreen) self.videoWidget.fullScreenChanged.disconnect( self.fullScreenButton.setChecked) self.videoWidget.setFullScreen(False) self.colorButton.setEnabled(available) def set_track_info(self, info): """ :param info: :return: """ self.trackInfo = info if self.statusInfo != "": self.setWindowTitle("%s | %s" % (self.trackInfo, self.statusInfo)) else: self.setWindowTitle(self.trackInfo) def set_status_info(self, info): """ :param info: :return: """ self.statusInfo = info if self.statusInfo != "": self.setWindowTitle("%s | %s" % (self.trackInfo, self.statusInfo)) else: self.setWindowTitle(self.trackInfo) def display_error_message(self): """ :return: """ self.set_status_info(self.player.errorString()) def update_duration_info(self, current_info): """ :param current_info: :return: """ result = "" duration = self.duration if current_info or duration: current_time = QTime((current_info / 3600) % 60, (current_info / 60) % 60, current_info % 60, (current_info * 1000) % 1000) total_time = QTime((duration / 3600) % 60, (duration / 60) % 60, duration % 60, (duration * 1000) % 1000) if duration > 3600: format = 'hh:mm:ss' else: format = 'mm:ss' result = current_time.toString( format) + " / " + total_time.toString(format) self.labelDuration.setText(result) def show_color_dialog(self): """ :return: """ if self.colorDialog is None: brightness_slider = QSlider(Qt.Horizontal) brightness_slider.setRange(-100, 100) brightness_slider.setValue(self.videoWidget.brightness()) brightness_slider.sliderMoved.connect( self.videoWidget.setBrightness) self.videoWidget.brightnessChanged.connect( brightness_slider.setValue) contrast_slider = QSlider(Qt.Horizontal) contrast_slider.setRange(-100, 100) contrast_slider.setValue(self.videoWidget.contrast()) contrast_slider.sliderMoved.connect(self.videoWidget.setContrast) self.videoWidget.contrastChanged.connect(contrast_slider.setValue) hue_slider = QSlider(Qt.Horizontal) hue_slider.setRange(-100, 100) hue_slider.setValue(self.videoWidget.hue()) hue_slider.sliderMoved.connect(self.videoWidget.setHue) self.videoWidget.hueChanged.connect(hue_slider.setValue) saturation_slider = QSlider(Qt.Horizontal) saturation_slider.setRange(-100, 100) saturation_slider.setValue(self.videoWidget.saturation()) saturation_slider.sliderMoved.connect( self.videoWidget.setSaturation) self.videoWidget.saturationChanged.connect( saturation_slider.setValue) layout = QFormLayout() layout.addRow("Brightness", brightness_slider) layout.addRow("Contrast", contrast_slider) layout.addRow("Hue", hue_slider) layout.addRow("Saturation", saturation_slider) button = QPushButton("Close") layout.addRow(button) self.colorDialog = QDialog(self) self.colorDialog.setWindowTitle("Color Options") self.colorDialog.setLayout(layout) button.clicked.connect(self.colorDialog.close) self.colorDialog.show()
class Player(QWidget): fullScreenChanged = pyqtSignal(bool) def __init__(self, playlist, parent=None): super(Player, self).__init__(parent) self.colorDialog = None self.trackInfo = "" self.statusInfo = "" self.duration = 0 self.player = QMediaPlayer() self.playlist = QMediaPlaylist() self.player.setPlaylist(self.playlist) self.player.durationChanged.connect(self.durationChanged) self.player.positionChanged.connect(self.positionChanged) self.player.metaDataChanged.connect(self.metaDataChanged) self.playlist.currentIndexChanged.connect(self.playlistPositionChanged) self.player.mediaStatusChanged.connect(self.statusChanged) self.player.bufferStatusChanged.connect(self.bufferingProgress) self.player.videoAvailableChanged.connect(self.videoAvailableChanged) self.player.error.connect(self.displayErrorMessage) self.videoWidget = VideoWidget() self.player.setVideoOutput(self.videoWidget) self.playlistModel = PlaylistModel() self.playlistModel.setPlaylist(self.playlist) self.playlistView = QListView() self.playlistView.setModel(self.playlistModel) self.playlistView.setCurrentIndex( self.playlistModel.index(self.playlist.currentIndex(), 0)) self.playlistView.activated.connect(self.jump) self.slider = QSlider(Qt.Horizontal) self.slider.setRange(0, self.player.duration() / 1000) self.labelDuration = QLabel() self.slider.sliderMoved.connect(self.seek) openButton = QPushButton("Открыть файл", clicked=self.open) controls = PlayerControls() controls.setState(self.player.state()) controls.setVolume(self.player.volume()) controls.play.connect(self.player.play) controls.pause.connect(self.player.pause) controls.stop.connect(self.player.stop) controls.next.connect(self.playlist.next) controls.previous.connect(self.previousClicked) controls.changeVolume.connect(self.player.setVolume) controls.changeRate.connect(self.player.setPlaybackRate) controls.stop.connect(self.videoWidget.update) self.player.stateChanged.connect(controls.setState) self.player.volumeChanged.connect(controls.setVolume) self.fullScreenButton = QPushButton("Полный экран") self.fullScreenButton.setCheckable(True) displayLayout = QHBoxLayout() displayLayout.addWidget(self.videoWidget, 2) displayLayout.addWidget(self.playlistView) controlLayout = QHBoxLayout() controlLayout.setContentsMargins(0, 0, 0, 0) controlLayout.addWidget(openButton) controlLayout.addStretch(1) controlLayout.addWidget(controls) controlLayout.addStretch(1) controlLayout.addWidget(self.fullScreenButton) layout = QVBoxLayout() layout.addLayout(displayLayout) hLayout = QHBoxLayout() hLayout.addWidget(self.slider) hLayout.addWidget(self.labelDuration) layout.addLayout(hLayout) layout.addLayout(controlLayout) self.setLayout(layout) self.metaDataChanged() self.addToPlaylist(playlist) def open(self): fileNames, _ = QFileDialog.getOpenFileNames(self, "Выбрать файл") self.addToPlaylist(fileNames) def addToPlaylist(self, fileNames): for name in fileNames: fileInfo = QFileInfo(name) if fileInfo.exists(): url = QUrl.fromLocalFile(fileInfo.absoluteFilePath()) if fileInfo.suffix().lower() == 'm3u': self.playlist.load(url) else: self.playlist.addMedia(QMediaContent(url)) else: url = QUrl(name) if url.isValid(): self.playlist.addMedia(QMediaContent(url)) def durationChanged(self, duration): duration /= 1000 self.duration = duration self.slider.setMaximum(duration) def positionChanged(self, progress): progress /= 1000 if not self.slider.isSliderDown(): self.slider.setValue(progress) self.updateDurationInfo(progress) def metaDataChanged(self): if self.player.isMetaDataAvailable(): self.setTrackInfo("%s - %s" % ( self.player.metaData(QMediaMetaData.AlbumArtist), self.player.metaData(QMediaMetaData.Title))) def previousClicked(self): # Go to the previous track if we are within the first 5 seconds of # playback. Otherwise, seek to the beginning. if self.player.position() <= 5000: self.playlist.previous() else: self.player.setPosition(0) def jump(self, index): if index.isValid(): self.playlist.setCurrentIndex(index.row()) self.player.play() def playlistPositionChanged(self, position): self.playlistView.setCurrentIndex( self.playlistModel.index(position, 0)) def seek(self, seconds): self.player.setPosition(seconds * 1000) def statusChanged(self, status): self.handleCursor(status) if status == QMediaPlayer.LoadingMedia: self.setStatusInfo("Загрузка...") elif status == QMediaPlayer.StalledMedia: self.setStatusInfo("Видео стоп") elif status == QMediaPlayer.EndOfMedia: QApplication.alert(self) elif status == QMediaPlayer.InvalidMedia: self.displayErrorMessage() else: self.setStatusInfo("") def handleCursor(self, status): if status in (QMediaPlayer.LoadingMedia, QMediaPlayer.BufferingMedia, QMediaPlayer.StalledMedia): self.setCursor(Qt.BusyCursor) else: self.unsetCursor() def bufferingProgress(self, progress): self.setStatusInfo("Буферизация %d%" % progress) def videoAvailableChanged(self, available): if available: self.fullScreenButton.clicked.connect( self.videoWidget.setFullScreen) self.videoWidget.fullScreenChanged.connect( self.fullScreenButton.setChecked) if self.fullScreenButton.isChecked(): self.videoWidget.setFullScreen(True) else: self.fullScreenButton.clicked.disconnect( self.videoWidget.setFullScreen) self.videoWidget.fullScreenChanged.disconnect( self.fullScreenButton.setChecked) self.videoWidget.setFullScreen(False) def setTrackInfo(self, info): self.trackInfo = info if self.statusInfo != "": self.setWindowTitle("%s | %s" % (self.trackInfo, self.statusInfo)) else: self.setWindowTitle(self.trackInfo) def setStatusInfo(self, info): self.statusInfo = info if self.statusInfo != "": self.setWindowTitle("%s | %s" % (self.trackInfo, self.statusInfo)) else: self.setWindowTitle(self.trackInfo) def displayErrorMessage(self): self.setStatusInfo(self.player.errorString()) def updateDurationInfo(self, currentInfo): duration = self.duration if currentInfo or duration: currentTime = QTime((currentInfo/3600)%60, (currentInfo/60)%60, currentInfo%60, (currentInfo*1000)%1000) totalTime = QTime((duration/3600)%60, (duration/60)%60, duration%60, (duration*1000)%1000); format = 'hh:mm:ss' if duration > 3600 else 'mm:ss' tStr = currentTime.toString(format) + " / " + totalTime.toString(format) else: tStr = "" self.labelDuration.setText(tStr)
class DesktopIconWidget(QFrame): def __init__(self, parent): QFrame.__init__(self, parent) self.setFrameStyle(QFrame.Box | QFrame.Sunken) self.setStyleSheet("QListView{background:transparent;}") self.listView = QListView(self) self.setLayout(QHBoxLayout()) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().addWidget(self.listView) self.listView.setContextMenuPolicy(Qt.CustomContextMenu) self.listView.setEditTriggers(QAbstractItemView.NoEditTriggers) self.listView.setMovement(QListView.Snap) self.listView.setFlow(QListView.LeftToRight) self.listView.setResizeMode(QListView.Adjust) self.listView.setGridSize( QSize(self.logicalDpiX() / 96 * 70, self.logicalDpiY() / 96 * 70)) self.listView.setViewMode(QListView.IconMode) self.quickDesktopModel = QuickDesktopModel( self.window().platform.databaseFile) self.listView.setModel(self.quickDesktopModel) self.createActions() self.makeConnections() def createActions(self): self.actionCreateComputer = QAction(self.tr("我的电脑(&C)"), self) self.actionCreateDocuments = QAction(self.tr("我的文档(&D)"), self) self.actionCreateMusic = QAction(self.tr("我的音乐(&M)"), self) self.actionCreatePictures = QAction(self.tr("我的图片(&P)"), self) self.actionCreateShortcut = QAction(self.tr("创建快捷方式(&C)"), self) self.actionCreateShortcut.setIcon(QIcon(":/images/new.png")) self.actionCreateBookmark = QAction(self.tr("创建网络链接(&B)"), self) self.actionCreateBookmark.setIcon(QIcon(":/images/insert-link.png")) self.actionRemoveShortcut = QAction(self.tr("删除快捷方式(&R)"), self) self.actionRemoveShortcut.setIcon(QIcon(":/images/delete.png")) self.actionRenameShortcut = QAction(self.tr("重命名(&N)"), self) self.actionRenameShortcut.setIcon(QIcon(":/images/edit-rename.png")) self.actionEditShortcut = QAction(self.tr("编辑快捷方式(&E)"), self) self.actionEditShortcut.setIcon(QIcon(":/images/edit.png")) def makeConnections(self): self.listView.customContextMenuRequested.connect( self.onQuickDesktopContextMenuRequest) self.listView.activated.connect(self.runQuickDesktopShortcut) self.actionCreateComputer.triggered.connect( self.createComputerShortcut) self.actionCreateDocuments.triggered.connect( self.createDocumentsShortcut) self.actionCreateMusic.triggered.connect(self.createMusicShortcut) self.actionCreatePictures.triggered.connect( self.createPicturesShortcut) self.actionCreateShortcut.triggered.connect(self.createShortcut) self.actionCreateBookmark.triggered.connect(self.createBookmark) self.actionEditShortcut.triggered.connect(self.editShortcut) self.actionRemoveShortcut.triggered.connect(self.removeShortcut) self.actionRenameShortcut.triggered.connect(self.renameShortcut) def onQuickDesktopContextMenuRequest(self, pos): index = self.listView.indexAt(pos) self.listView.setCurrentIndex(index) menu = QMenu() menu.addAction(self.actionCreateShortcut) menu.addAction(self.actionCreateBookmark) menu2 = menu.addMenu(self.tr("创建特殊快捷方式(&S)")) if os.name == "nt": menu2.addAction(self.actionCreateComputer) menu2.addAction(self.actionCreateDocuments) menu2.addAction(self.actionCreatePictures) menu2.addAction(self.actionCreateMusic) if index.isValid(): menu.addAction(self.actionRemoveShortcut) if not self.quickDesktopModel.isSpecialShortcut(index): menu.addAction(self.actionEditShortcut) menu.addAction(self.actionRenameShortcut) try: getattr(menu, "exec")(QCursor.pos()) except AttributeError: getattr(menu, "exec_")(QCursor.pos()) def createShortcut(self): d = ShortcutDialog(self) if self.window().runDialog(d.create) == QDialog.Accepted: shortcut = d.getResult() shortcut["id"] = str(uuid.uuid4()) self.quickDesktopModel.addShortcut(shortcut) d.deleteLater() def createBookmark(self): d = BookmarkDialog(self) if self.window().runDialog(d.create) == QDialog.Accepted: shortcut = { "id": str(uuid.uuid4()), "icon": "", "openwith": "", "dir": "", } shortcut.update(d.getResult()) self.quickDesktopModel.addShortcut(shortcut) d.deleteLater() def createComputerShortcut(self): shortcut = { "id": str(uuid.uuid4()), "name": self.tr("我的电脑"), "path": COMPUTER_PATH, "icon": "", "dir": "", "openwith": "", } self.quickDesktopModel.addShortcut(shortcut) def createDocumentsShortcut(self): shortcut = { "id": str(uuid.uuid4()), "name": self.tr("我的文档"), "path": DOCUMENTS_PATH, "icon": "", "dir": "", "openwith": "", } self.quickDesktopModel.addShortcut(shortcut) def createPicturesShortcut(self): shortcut = { "id": str(uuid.uuid4()), "name": self.tr("图片收藏"), "path": PICTURES_PATH, "icon": "", "dir": "", "openwith": "", } self.quickDesktopModel.addShortcut(shortcut) def createMusicShortcut(self): shortcut = { "id": str(uuid.uuid4()), "name": self.tr("我的音乐"), "path": MUSIC_PATH, "icon": "", "dir": "", "openwith": "", } self.quickDesktopModel.addShortcut(shortcut) def renameShortcut(self): self.listView.edit(self.listView.currentIndex()) def removeShortcut(self): self.quickDesktopModel.removeShortcut(self.listView.currentIndex()) def editShortcut(self): index = self.listView.currentIndex() if not index.isValid(): return shortcut = self.quickDesktopModel.shortcutAt(index) url = QUrl.fromUserInput(shortcut["path"]) if not url.isValid(): return if url.scheme() == "special": QMessageBox.information(self, self.tr("编辑快捷方式"), self.tr("不能编辑特殊图标。")) return elif url.scheme() == "file": d = ShortcutDialog(self) else: d = BookmarkDialog(self) if self.window().runDialog(d.edit, shortcut) == QDialog.Accepted: shortcut.update(d.getResult()) self.quickDesktopModel.updateShortcut(shortcut, index) d.deleteLater() def runQuickDesktopShortcut(self): index = self.listView.currentIndex() if not index.isValid(): return if not self.quickDesktopModel.runShortcut(index): QMessageBox.information(self, self.tr("快捷面板"), \ self.tr("不能运行快捷方式。请检查文件是否存在或者程序是否正确。")) else: self.window().close()
class MainWindow(QMainWindow): """MNELAB main window. """ def __init__(self): super().__init__() # restore settings settings = self._read_settings() self.recent = settings["recent"] # list of recent files if settings["geometry"]: self.restoreGeometry(settings["geometry"]) else: self.setGeometry(300, 300, 1000, 750) # default window size self.move(QApplication.desktop().screen().rect().center() - self.rect().center()) # center window if settings["state"]: self.restoreState(settings["state"]) self.setWindowTitle("MNELAB") # initialize menus menubar = self.menuBar() file_menu = menubar.addMenu("&File") file_menu.addAction("&Open...", self.open_file, QKeySequence.Open) self.recent_menu = file_menu.addMenu("Open recent") self.recent_menu.aboutToShow.connect(self._update_recent_menu) self.recent_menu.triggered.connect(self._load_recent) if not self.recent: self.recent_menu.setEnabled(False) self.close_file_action = file_menu.addAction("&Close", self.close_file, QKeySequence.Close) self.close_all_action = file_menu.addAction("Close all", self.close_all) file_menu.addSeparator() self.import_bad_action = file_menu.addAction("Import bad channels...", self.import_bads) self.import_anno_action = file_menu.addAction("Import annotations...", self.import_annotations) file_menu.addSeparator() self.export_raw_action = file_menu.addAction("Export &raw...", self.export_raw) self.export_bad_action = file_menu.addAction("Export &bad channels...", self.export_bads) self.export_anno_action = file_menu.addAction("Export &annotations...", self.export_annotations) self.export_events_action = file_menu.addAction("Export &events...", self.export_events) file_menu.addSeparator() file_menu.addAction("&Quit", self.close, QKeySequence.Quit) edit_menu = menubar.addMenu("&Edit") self.pick_chans_action = edit_menu.addAction("Pick &channels...", self.pick_channels) self.chan_props_action = edit_menu.addAction("Channel &properties...", self.channel_properties) self.set_montage_action = edit_menu.addAction("Set &montage...", self.set_montage) edit_menu.addSeparator() self.setref_action = edit_menu.addAction("&Set reference...", self.set_reference) plot_menu = menubar.addMenu("&Plot") self.plot_raw_action = plot_menu.addAction("&Raw data", self.plot_raw) self.plot_psd_action = plot_menu.addAction("&Power spectral " "density...", self.plot_psd) self.plot_montage_action = plot_menu.addAction("Current &montage", self.plot_montage) tools_menu = menubar.addMenu("&Tools") self.filter_action = tools_menu.addAction("&Filter data...", self.filter_data) self.find_events_action = tools_menu.addAction("Find &events...", self.find_events) self.run_ica_action = tools_menu.addAction("Run &ICA...") self.import_ica_action = tools_menu.addAction("&Load ICA...", self.load_ica) view_menu = menubar.addMenu("&View") statusbar_action = view_menu.addAction("Statusbar", self._toggle_statusbar) statusbar_action.setCheckable(True) help_menu = menubar.addMenu("&Help") help_menu.addAction("&About", self.show_about) help_menu.addAction("About &Qt", self.show_about_qt) # set up data model for sidebar (list of open files) self.names = QStringListModel() self.names.dataChanged.connect(self._update_names) splitter = QSplitter() self.sidebar = QListView() self.sidebar.setFrameStyle(QFrame.NoFrame) self.sidebar.setFocusPolicy(Qt.NoFocus) self.sidebar.setModel(self.names) self.sidebar.clicked.connect(self._update_data) splitter.addWidget(self.sidebar) self.infowidget = InfoWidget() splitter.addWidget(self.infowidget) width = splitter.size().width() splitter.setSizes((width * 0.3, width * 0.7)) self.setCentralWidget(splitter) self.status_label = QLabel() self.statusBar().addPermanentWidget(self.status_label) if settings["statusbar"]: self.statusBar().show() statusbar_action.setChecked(True) else: self.statusBar().hide() statusbar_action.setChecked(False) self.setAcceptDrops(True) self._toggle_actions(False) self.show() def open_file(self): """Show open file dialog. """ fname = QFileDialog.getOpenFileName(self, "Open file", filter=SUPPORTED_FORMATS)[0] if fname: self.load_file(fname) def load_file(self, fname): """Load file. Parameters ---------- fname : str File name. """ if not exists(fname): QMessageBox.critical(self, "File not found", "{} does not exist.".format(fname)) self._remove_recent(fname) return name, ext = splitext(split(fname)[-1]) ftype = ext[1:].upper() if ext not in SUPPORTED_FORMATS: raise ValueError("File format {} is not supported.".format(ftype)) if ext in [".edf", ".bdf"]: raw = mne.io.read_raw_edf(fname, stim_channel=-1, preload=True) history.append("raw = mne.io.read_raw_edf('{}', " "stim_channel=-1, preload=True)".format(fname)) elif ext in [".fif"]: raw = mne.io.read_raw_fif(fname, preload=True) history.append("raw = mne.io.read_raw_fif('{}'," "preload=True)".format(fname)) elif ext in [".vhdr"]: raw = mne.io.read_raw_brainvision(fname, preload=True) history.append("raw = mne.io.read_raw_brainvision('{}', " "preload=True)".format(fname)) data.insert_data(DataSet(name=name, fname=fname, ftype=ftype, raw=raw)) self.find_events() self._update_sidebar(data.names, data.index) self._update_infowidget() self._update_statusbar() self._add_recent(fname) self._toggle_actions() def export_raw(self): """Export raw to FIF file. """ fname = QFileDialog.getSaveFileName(self, "Export raw", filter="*.fif")[0] if fname: name, ext = splitext(split(fname)[-1]) ext = ext if ext else ".fif" # automatically add extension fname = join(split(fname)[0], name + ext) data.current.raw.save(fname) def export_bads(self): """Export bad channels info to a CSV file. """ fname = QFileDialog.getSaveFileName(self, "Export bad channels", filter="*.csv")[0] if fname: name, ext = splitext(split(fname)[-1]) ext = ext if ext else ".csv" # automatically add extension fname = join(split(fname)[0], name + ext) with open(fname, "w") as f: f.write(",".join(data.current.raw.info["bads"])) def import_bads(self): """Import bad channels info from a CSV file. """ fname = QFileDialog.getOpenFileName(self, "Import bad channels", filter="*.csv")[0] if fname: with open(fname) as f: bads = f.read().replace(" ", "").split(",") if set(bads) - set(data.current.raw.info["ch_names"]): QMessageBox.critical(self, "Channel labels not found", "Some channel labels from the file " "are not present in the data.") else: data.current.raw.info["bads"] = bads data.data[data.index].raw.info["bads"] = bads def export_events(self): """Export events to a CSV file. The resulting CSV file has two columns. The first column contains the position (in samples), whereas the second column contains the type of the events. The first line is a header containing the column names. """ fname = QFileDialog.getSaveFileName(self, "Export events", filter="*.csv")[0] if fname: name, ext = splitext(split(fname)[-1]) ext = ext if ext else ".csv" # automatically add extension fname = join(split(fname)[0], name + ext) np.savetxt(fname, data.current.events[:, [0, 2]], fmt="%d", delimiter=",", header="pos,type", comments="") def export_annotations(self): """Export annotations to a CSV file. The resulting CSV file has three columns. The first column contains the annotation type, the second column contains the onset (in s), and the third column contains the duration (in s). The first line is a header containing the column names. """ fname = QFileDialog.getSaveFileName(self, "Export annotations", filter="*.csv")[0] if fname: name, ext = splitext(split(fname)[-1]) ext = ext if ext else ".csv" # automatically add extension fname = join(split(fname)[0], name + ext) anns = data.current.raw.annotations with open(fname, "w") as f: f.write("type,onset,duration\n") for a in zip(anns.description, anns.onset, anns.duration): f.write(",".join([a[0], str(a[1]), str(a[2])])) f.write("\n") def import_annotations(self): fname = QFileDialog.getOpenFileName(self, "Import annotations", filter="*.csv")[0] if fname: descs, onsets, durations = [], [], [] fs = data.current.raw.info["sfreq"] with open(fname) as f: f.readline() # skip header for line in f: ann = line.split(",") onset = float(ann[1].strip()) duration = float(ann[2].strip()) if onset > data.current.raw.n_times / fs: QMessageBox.critical(self, "Invalid annotations", "One or more annotations are " "outside of the data range.") return descs.append(ann[0].strip()) onsets.append(onset) durations.append(duration) annotations = mne.Annotations(onsets, durations, descs) data.raw.annotations = annotations data.data[data.index].raw.annotations = annotations self._update_infowidget() def close_file(self): """Close current file. """ data.remove_data() self._update_sidebar(data.names, data.index) self._update_infowidget() self._update_statusbar() if not data: self._toggle_actions(False) def close_all(self): """Close all currently open data sets. """ msg = QMessageBox.question(self, "Close all data sets", "Close all data sets?") if msg == QMessageBox.Yes: while data: self.close_file() def get_info(self): """Get basic information on current file. Returns ------- info : dict Dictionary with information on current file. """ raw = data.current.raw fname = data.current.fname ftype = data.current.ftype reference = data.current.reference events = data.current.events montage = data.current.montage if raw.info["bads"]: nbads = len(raw.info["bads"]) nchan = "{} ({} bad)".format(raw.info["nchan"], nbads) else: nchan = raw.info["nchan"] chans = Counter([channel_type(raw.info, i) for i in range(raw.info["nchan"])]) if events is not None: nevents = events.shape[0] unique = [str(e) for e in sorted(set(events[:, 2]))] if len(unique) > 20: # do not show all events first = ", ".join(unique[:10]) last = ", ".join(unique[-10:]) events = "{} ({})".format(nevents, first + ", ..., " + last) else: events = "{} ({})".format(nevents, ", ".join(unique)) else: events = "-" if isinstance(reference, list): reference = ",".join(reference) if raw.annotations is not None: annots = len(raw.annotations.description) else: annots = "-" return {"File name": fname if fname else "-", "File type": ftype if ftype else "-", "Number of channels": nchan, "Channels": ", ".join( [" ".join([str(v), k.upper()]) for k, v in chans.items()]), "Samples": raw.n_times, "Sampling frequency": str(raw.info["sfreq"]) + " Hz", "Length": str(raw.n_times / raw.info["sfreq"]) + " s", "Events": events, "Annotations": annots, "Reference": reference if reference else "-", "Montage": montage if montage is not None else "-", "Size in memory": "{:.2f} MB".format( raw._data.nbytes / 1024 ** 2), "Size on disk": "-" if not fname else "{:.2f} MB".format( getsize(fname) / 1024 ** 2)} def pick_channels(self): """Pick channels in current data set. """ channels = data.current.raw.info["ch_names"] dialog = PickChannelsDialog(self, channels, selected=channels) if dialog.exec_(): picks = [item.data(0) for item in dialog.channels.selectedItems()] drops = set(channels) - set(picks) tmp = data.current.raw.drop_channels(drops) name = data.current.name + " (channels dropped)" new = DataSet(raw=tmp, name=name, events=data.current.events) history.append("raw.drop({})".format(drops)) self._update_datasets(new) def channel_properties(self): info = data.current.raw.info dialog = ChannelPropertiesDialog(self, info) if dialog.exec_(): dialog.model.sort(0) bads = [] renamed = {} types = {} for i in range(dialog.model.rowCount()): new_label = dialog.model.item(i, 1).data(Qt.DisplayRole) old_label = info["ch_names"][i] if new_label != old_label: renamed[old_label] = new_label new_type = dialog.model.item(i, 2).data(Qt.DisplayRole).lower() old_type = channel_type(info, i).lower() if new_type != old_type: types[new_label] = new_type if dialog.model.item(i, 3).checkState() == Qt.Checked: bads.append(info["ch_names"][i]) info["bads"] = bads data.data[data.index].raw.info["bads"] = bads if renamed: mne.rename_channels(info, renamed) mne.rename_channels(data.data[data.index].raw.info, renamed) if types: data.current.raw.set_channel_types(types) data.data[data.index].raw.set_channel_types(types) self._update_infowidget() self._toggle_actions(True) def set_montage(self): """Set montage. """ path = join(mne.__path__[0], "channels", "data", "montages") supported = (".elc", ".txt", ".csd", ".sfp", ".elp", ".hpts", ".loc", ".locs", ".eloc", ".bvef") files = [splitext(f) for f in listdir(path)] montages = sorted([f for f, ext in files if ext in supported], key=str.lower) # TODO: currently it is not possible to remove an existing montage dialog = MontageDialog(self, montages, selected=data.current.montage) if dialog.exec_(): name = dialog.montages.selectedItems()[0].data(0) montage = mne.channels.read_montage(name) ch_names = data.current.raw.info["ch_names"] # check if at least one channel name matches a name in the montage if set(ch_names) & set(montage.ch_names): data.current.montage = name data.data[data.index].montage = name data.current.raw.set_montage(montage) data.data[data.index].raw.set_montage(montage) self._update_infowidget() self._toggle_actions() else: QMessageBox.critical(self, "No matching channel names", "Channel names defined in the montage do " "not match any channel name in the data.") def plot_raw(self): """Plot raw data. """ events = data.current.events nchan = data.current.raw.info["nchan"] fig = data.current.raw.plot(events=events, n_channels=nchan, title=data.current.name, show=False) history.append("raw.plot(n_channels={})".format(nchan)) win = fig.canvas.manager.window win.setWindowTitle("Raw data") win.findChild(QStatusBar).hide() win.installEventFilter(self) # detect if the figure is closed # prevent closing the window with the escape key try: key_events = fig.canvas.callbacks.callbacks["key_press_event"][8] except KeyError: pass else: # this requires MNE >=0.15 key_events.func.keywords["params"]["close_key"] = None fig.show() def plot_psd(self): """Plot power spectral density (PSD). """ fig = data.current.raw.plot_psd(average=False, spatial_colors=False, show=False) win = fig.canvas.manager.window win.setWindowTitle("Power spectral density") fig.show() def plot_montage(self): """Plot montage. """ montage = mne.channels.read_montage(data.current.montage) fig = montage.plot(show_names=True, show=False) win = fig.canvas.manager.window win.setWindowTitle("Montage") win.findChild(QStatusBar).hide() win.findChild(QToolBar).hide() fig.show() def load_ica(self): """Load ICA solution from a file. """ fname = QFileDialog.getOpenFileName(self, "Load ICA", filter="*.fif *.fif.gz") if fname[0]: self.state.ica = mne.preprocessing.read_ica(fname[0]) def find_events(self): events = mne.find_events(data.current.raw, consecutive=False) if events.shape[0] > 0: # if events were found data.current.events = events data.data[data.index].events = events self._update_infowidget() def filter_data(self): """Filter data. """ dialog = FilterDialog(self) if dialog.exec_(): low, high = dialog.low, dialog.high tmp = filter_data(data.current.raw._data, data.current.raw.info["sfreq"], l_freq=low, h_freq=high, fir_design="firwin") name = data.current.name + " ({}-{} Hz)".format(low, high) new = DataSet(raw=mne.io.RawArray(tmp, data.current.raw.info), name=name, events=data.current.events) history.append("raw.filter({}, {})".format(low, high)) self._update_datasets(new) def set_reference(self): """Set reference. """ dialog = ReferenceDialog(self) if dialog.exec_(): if dialog.average.isChecked(): tmp, _ = mne.set_eeg_reference(data.current.raw, None) tmp.apply_proj() name = data.current.name + " (average ref)" new = DataSet(raw=tmp, name=name, reference="average", events=data.current.events) else: ref = [c.strip() for c in dialog.channellist.text().split(",")] refstr = ",".join(ref) if set(ref) - set(data.current.raw.info["ch_names"]): # add new reference channel(s) to data try: tmp = mne.add_reference_channels(data.current.raw, ref) except RuntimeError: QMessageBox.critical(self, "Cannot add new channels", "Cannot add new channels to " "average referenced data.") return else: # re-reference to existing channel(s) tmp, _ = mne.set_eeg_reference(data.current.raw, ref) name = data.current.name + " (ref {})".format(refstr) new = DataSet(raw=tmp, name=name, reference=refstr, events=data.current.events) self._update_datasets(new) def show_about(self): """Show About dialog. """ msg = """<b>MNELAB {}</b><br/><br/> <a href="https://github.com/cbrnr/mnelab">MNELAB</a> - a graphical user interface for <a href="https://github.com/mne-tools/mne-python">MNE</a>.<br/><br/> This program uses MNE version {}.<br/><br/> Licensed under the BSD 3-clause license.<br/> Copyright 2017 by Clemens Brunner.""".format(__version__, mne.__version__) QMessageBox.about(self, "About MNELAB", msg) def show_about_qt(self): """Show About Qt dialog. """ QMessageBox.aboutQt(self, "About Qt") def _update_datasets(self, dataset): # if current data is stored in a file create a new data set if data.current.fname: data.insert_data(dataset) # otherwise ask if the current data set should be overwritten or if a # new data set should be created else: msg = QMessageBox.question(self, "Overwrite existing data set", "Overwrite existing data set?") if msg == QMessageBox.No: # create new data set data.insert_data(dataset) else: # overwrite existing data set data.update_data(dataset) self._update_sidebar(data.names, data.index) self._update_infowidget() self._update_statusbar() def _update_sidebar(self, names, index): """Update (overwrite) sidebar with names and current index. """ self.names.setStringList(names) self.sidebar.setCurrentIndex(self.names.index(index)) def _update_infowidget(self): if data: self.infowidget.set_values(self.get_info()) else: self.infowidget.clear() def _update_statusbar(self): if data: mb = data.nbytes / 1024 ** 2 self.status_label.setText("Total Memory: {:.2f} MB".format(mb)) else: self.status_label.clear() def _toggle_actions(self, enabled=True): """Toggle actions. Parameters ---------- enabled : bool Specifies whether actions are enabled (True) or disabled (False). """ self.close_file_action.setEnabled(enabled) self.close_all_action.setEnabled(enabled) self.export_raw_action.setEnabled(enabled) if data.data: bads = bool(data.current.raw.info["bads"]) self.export_bad_action.setEnabled(enabled and bads) events = data.current.events is not None self.export_events_action.setEnabled(enabled and events) annot = data.current.raw.annotations is not None self.export_anno_action.setEnabled(enabled and annot) montage = bool(data.current.montage) self.plot_montage_action.setEnabled(enabled and montage) else: self.export_bad_action.setEnabled(enabled) self.export_events_action.setEnabled(enabled) self.export_anno_action.setEnabled(enabled) self.plot_montage_action.setEnabled(enabled) self.import_bad_action.setEnabled(enabled) self.import_anno_action.setEnabled(enabled) self.pick_chans_action.setEnabled(enabled) self.chan_props_action.setEnabled(enabled) self.set_montage_action.setEnabled(enabled) self.plot_raw_action.setEnabled(enabled) self.plot_psd_action.setEnabled(enabled) self.filter_action.setEnabled(enabled) self.setref_action.setEnabled(enabled) self.find_events_action.setEnabled(enabled) self.run_ica_action.setEnabled(enabled) self.import_ica_action.setEnabled(enabled) def _add_recent(self, fname): """Add a file to recent file list. Parameters ---------- fname : str File name. """ if fname in self.recent: # avoid duplicates self.recent.remove(fname) self.recent.insert(0, fname) while len(self.recent) > MAX_RECENT: # prune list self.recent.pop() self._write_settings() if not self.recent_menu.isEnabled(): self.recent_menu.setEnabled(True) def _remove_recent(self, fname): """Remove file from recent file list. Parameters ---------- fname : str File name. """ if fname in self.recent: self.recent.remove(fname) self._write_settings() if not self.recent: self.recent_menu.setEnabled(False) def _write_settings(self): """Write application settings. """ settings = QSettings() settings.setValue("recent", self.recent) settings.setValue("statusbar", not self.statusBar().isHidden()) settings.setValue("geometry", self.saveGeometry()) settings.setValue("state", self.saveState()) def _read_settings(self): """Read application settings. Returns ------- settings : dict The restored settings values are returned in a dictionary for further processing. """ settings = QSettings() recent = settings.value("recent") if not recent: recent = [] # default is empty list statusbar = settings.value("statusbar") if statusbar is None: # default is True statusbar = True geometry = settings.value("geometry") state = settings.value("state") return {"recent": recent, "statusbar": statusbar, "geometry": geometry, "state": state} @pyqtSlot(QModelIndex) def _update_data(self, selected): """Update index and information based on the state of the sidebar. Parameters ---------- selected : QModelIndex Index of the selected row. """ if selected.row() != data.index: data.index = selected.row() data.update_current() self._update_infowidget() @pyqtSlot(QModelIndex, QModelIndex) def _update_names(self, start, stop): """Update names in DataSets after changes in sidebar. """ for index in range(start.row(), stop.row() + 1): data.data[index].name = self.names.stringList()[index] if data.index in range(start.row(), stop.row() + 1): data.current.name = data.names[data.index] @pyqtSlot() def _update_recent_menu(self): self.recent_menu.clear() for recent in self.recent: self.recent_menu.addAction(recent) @pyqtSlot(QAction) def _load_recent(self, action): self.load_file(action.text()) @pyqtSlot() def _toggle_statusbar(self): if self.statusBar().isHidden(): self.statusBar().show() else: self.statusBar().hide() self._write_settings() @pyqtSlot(QDropEvent) def dragEnterEvent(self, event): if event.mimeData().hasUrls(): event.acceptProposedAction() @pyqtSlot(QDropEvent) def dropEvent(self, event): mime = event.mimeData() if mime.hasUrls(): urls = mime.urls() for url in urls: self.load_file(url.toLocalFile()) @pyqtSlot(QEvent) def closeEvent(self, event): """Close application. Parameters ---------- event : QEvent Close event. """ self._write_settings() if history: print("\nCommand History") print("===============") print("\n".join(history)) QApplication.quit() def eventFilter(self, source, event): # currently the only source is the raw plot window if event.type() == QEvent.Close: self._update_infowidget() self._toggle_actions() return QObject.eventFilter(self, source, event)
class IntracranialElectrodeLocator(QMainWindow): """Locate electrode contacts using a coregistered MRI and CT.""" _xy_idx = ( (1, 2), (0, 2), (0, 1), ) def __init__(self, info, trans, aligned_ct, subject=None, subjects_dir=None, groups=None, verbose=None): """GUI for locating intracranial electrodes. .. note:: Images will be displayed using orientation information obtained from the image header. Images will be resampled to dimensions [256, 256, 256] for display. """ # initialize QMainWindow class super(IntracranialElectrodeLocator, self).__init__() if not info.ch_names: raise ValueError('No channels found in `info` to locate') # store info for modification self._info = info self._seeg_idx = pick_types(self._info, meg=False, seeg=True) self._verbose = verbose # channel plotting default parameters self._ch_alpha = 0.5 self._radius = int(_CH_PLOT_SIZE // 100) # starting 1/100 of image # load imaging data self._subject_dir = op.join(subjects_dir, subject) self._load_image_data(aligned_ct) # initialize channel data self._ch_index = 0 # load data, apply trans self._head_mri_t = _get_trans(trans, 'head', 'mri')[0] self._mri_head_t = invert_transform(self._head_mri_t) # load channels, convert from m to mm self._chs = { name: apply_trans(self._head_mri_t, ch['loc'][:3]) * 1000 for name, ch in zip(info.ch_names, info['chs']) } self._ch_names = list(self._chs.keys()) # set current position if np.isnan(self._chs[self._ch_names[self._ch_index]]).any(): ras = [0., 0., 0.] else: ras = self._chs[self._ch_names[self._ch_index]] self._set_ras(ras, update_plots=False) self._group_channels(groups) # GUI design # Main plots: make one plot for each view; sagittal, coronal, axial plt_grid = QGridLayout() plts = [_make_slice_plot(), _make_slice_plot(), _make_slice_plot()] self._figs = [plts[0][1], plts[1][1], plts[2][1]] plt_grid.addWidget(plts[0][0], 0, 0) plt_grid.addWidget(plts[1][0], 0, 1) plt_grid.addWidget(plts[2][0], 1, 0) self._renderer = _get_renderer(name='IEEG Locator', size=(400, 400), bgcolor='w') plt_grid.addWidget(self._renderer.plotter) # Channel selector self._ch_list = QListView() self._ch_list.setSelectionMode(Qt.QAbstractItemView.SingleSelection) max_ch_name_len = max([len(name) for name in self._chs]) self._ch_list.setMinimumWidth(max_ch_name_len * _CH_MENU_WIDTH) self._ch_list.setMaximumWidth(max_ch_name_len * _CH_MENU_WIDTH) self._set_ch_names() # Plots self._plot_images() # Menus button_hbox = self._get_button_bar() slider_hbox = self._get_slider_bar() bottom_hbox = self._get_bottom_bar() # Add lines self._lines = dict() self._lines_2D = dict() for group in set(self._groups.values()): self._update_lines(group) # Put everything together plot_ch_hbox = QHBoxLayout() plot_ch_hbox.addLayout(plt_grid) plot_ch_hbox.addWidget(self._ch_list) main_vbox = QVBoxLayout() main_vbox.addLayout(button_hbox) main_vbox.addLayout(slider_hbox) main_vbox.addLayout(plot_ch_hbox) main_vbox.addLayout(bottom_hbox) central_widget = QWidget() central_widget.setLayout(main_vbox) self.setCentralWidget(central_widget) # ready for user self._move_cursors_to_pos() self._ch_list.setFocus() # always focus on list def _load_image_data(self, ct): """Get MRI and CT data to display and transforms to/from vox/RAS.""" # allows recon-all not to be finished (T1 made in a few minutes) mri_img = 'brain' if op.isfile( op.join(self._subject_dir, 'mri', 'brain.mgz')) else 'T1' self._mri_data, self._vox_ras_t = _load_image(op.join( self._subject_dir, 'mri', f'{mri_img}.mgz'), 'MRI Image', verbose=self._verbose) self._ras_vox_t = np.linalg.inv(self._vox_ras_t) self._voxel_sizes = np.array(self._mri_data.shape) # We need our extents to land the centers of each pixel on the voxel # number. This code assumes 1mm isotropic... img_delta = 0.5 self._img_extents = list([ -img_delta, self._voxel_sizes[idx[0]] - img_delta, -img_delta, self._voxel_sizes[idx[1]] - img_delta ] for idx in self._xy_idx) ch_deltas = list(img_delta * (self._voxel_sizes[ii] / _CH_PLOT_SIZE) for ii in range(3)) self._ch_extents = list([ -ch_delta, self._voxel_sizes[idx[0]] - ch_delta, -ch_delta, self._voxel_sizes[idx[1]] - ch_delta ] for idx, ch_delta in zip(self._xy_idx, ch_deltas)) # ready ct self._ct_data, vox_ras_t = _load_image(ct, 'CT', verbose=self._verbose) if self._mri_data.shape != self._ct_data.shape or \ not np.allclose(self._vox_ras_t, vox_ras_t, rtol=1e-6): raise ValueError('CT is not aligned to MRI, got ' f'CT shape={self._ct_data.shape}, ' f'MRI shape={self._mri_data.shape}, ' f'CT affine={vox_ras_t} and ' f'MRI affine={self._vox_ras_t}') self._ct_maxima = None # don't compute until turned on if op.exists(op.join(self._subject_dir, 'surf', 'lh.seghead')): self._head = _read_mri_surface( op.join(self._subject_dir, 'surf', 'lh.seghead')) assert _frame_to_str[self._head['coord_frame']] == 'mri' else: warn('`seghead` not found, using marching cubes on CT for ' 'head plot, use :ref:`mne.bem.make_scalp_surfaces` ' 'to add the scalp surface instead of skull from the CT') self._head = None if op.exists(op.join(self._subject_dir, 'surf', 'lh.pial')): self._lh = _read_mri_surface( op.join(self._subject_dir, 'surf', 'lh.pial')) assert _frame_to_str[self._lh['coord_frame']] == 'mri' self._rh = _read_mri_surface( op.join(self._subject_dir, 'surf', 'rh.pial')) assert _frame_to_str[self._rh['coord_frame']] == 'mri' else: warn('`pial` surface not found, skipping adding to 3D ' 'plot. This indicates the Freesurfer recon-all ' 'has not finished or has been modified and ' 'these files have been deleted.') self._lh = self._rh = None def _make_ch_image(self, axis, proj=False): """Make a plot to display the channel locations.""" # Make channel data higher resolution so it looks better. ch_image = np.zeros((_CH_PLOT_SIZE, _CH_PLOT_SIZE)) * np.nan vxyz = self._voxel_sizes def color_ch_radius(ch_image, xf, yf, group, radius): # Take the fraction across each dimension of the RAS # coordinates converted to xyz and put a circle in that # position in this larger resolution image ex, ey = np.round(np.array([xf, yf]) * _CH_PLOT_SIZE).astype(int) ii = np.arange(-radius, radius + 1) ii_sq = ii * ii idx = np.where(ii_sq + ii_sq[:, np.newaxis] < radius * radius) # negative y because y axis is inverted ch_image[-(ey + ii[idx[1]]), ex + ii[idx[0]]] = group return ch_image for name, ras in self._chs.items(): # move from middle-centered (half coords positive, half negative) # to bottom-left corner centered (all coords positive). if np.isnan(ras).any(): continue xyz = apply_trans(self._ras_vox_t, ras) # check if closest to that voxel dist = np.linalg.norm(xyz - self._current_slice) if proj or dist < self._radius: group = self._groups[name] r = self._radius if proj else \ self._radius - np.round(abs(dist)).astype(int) xf, yf = (xyz / vxyz)[list(self._xy_idx[axis])] ch_image = color_ch_radius(ch_image, xf, yf, group, r) return ch_image @verbose def _save_ch_coords(self, info=None, verbose=None): """Save the location of the electrode contacts.""" logger.info('Saving channel positions to `info`') if info is None: info = self._info with info._unlock(): for name, ch in zip(info.ch_names, info['chs']): ch['loc'][:3] = apply_trans(self._mri_head_t, self._chs[name] / 1000) # mm->m def _plot_images(self): """Use the MRI and CT to make plots.""" # Plot sagittal (0), coronal (1) or axial (2) view self._images = dict(ct=list(), chs=list(), ct_bounds=list(), cursor_v=list(), cursor_h=list()) ct_min, ct_max = np.nanmin(self._ct_data), np.nanmax(self._ct_data) text_kwargs = dict(fontsize='medium', weight='bold', color='#66CCEE', family='monospace', ha='center', va='center', path_effects=[ patheffects.withStroke(linewidth=4, foreground="k", alpha=0.75) ]) xyz = apply_trans(self._ras_vox_t, self._ras) for axis in range(3): plot_x_idx, plot_y_idx = self._xy_idx[axis] fig = self._figs[axis] ax = fig.axes[0] ct_data = np.take(self._ct_data, self._current_slice[axis], axis=axis).T self._images['ct'].append( ax.imshow(ct_data, cmap='gray', aspect='auto', zorder=1, vmin=ct_min, vmax=ct_max)) img_extent = self._img_extents[axis] # x0, x1, y0, y1 w, h = np.diff(np.array(img_extent).reshape(2, 2), axis=1)[:, 0] self._images['ct_bounds'].append( Rectangle(img_extent[::2], w, h, edgecolor='w', facecolor='none', alpha=0.25, lw=0.5, zorder=1.5)) ax.add_patch(self._images['ct_bounds'][-1]) self._images['chs'].append( ax.imshow(self._make_ch_image(axis), aspect='auto', extent=self._ch_extents[axis], zorder=3, cmap=_CMAP, alpha=self._ch_alpha, vmin=0, vmax=_N_COLORS)) v_x = (xyz[plot_x_idx], ) * 2 v_y = img_extent[2:4] self._images['cursor_v'].append( ax.plot(v_x, v_y, color='lime', linewidth=0.5, alpha=0.5, zorder=8)[0]) h_y = (xyz[plot_y_idx], ) * 2 h_x = img_extent[0:2] self._images['cursor_h'].append( ax.plot(h_x, h_y, color='lime', linewidth=0.5, alpha=0.5, zorder=8)[0]) # label axes self._figs[axis].text(0.5, 0.05, _IMG_LABELS[axis][0], **text_kwargs) self._figs[axis].text(0.05, 0.5, _IMG_LABELS[axis][1], **text_kwargs) self._figs[axis].axes[0].axis(img_extent) self._figs[axis].canvas.mpl_connect('scroll_event', self._on_scroll) self._figs[axis].canvas.mpl_connect( 'button_release_event', partial(self._on_click, axis=axis)) # add head and brain in mm (convert from m) if self._head is None: logger.info('Using marching cubes on CT for the ' '3D visualization panel') rr, tris = _marching_cubes( np.where(self._ct_data < np.quantile(self._ct_data, 0.95), 0, 1), [1])[0] rr = apply_trans(self._vox_ras_t, rr) self._renderer.mesh(*rr.T, triangles=tris, color='gray', opacity=0.2, reset_camera=False, render=False) else: self._renderer.mesh(*self._head['rr'].T * 1000, triangles=self._head['tris'], color='gray', opacity=0.2, reset_camera=False, render=False) if self._lh is not None and self._rh is not None: self._renderer.mesh(*self._lh['rr'].T * 1000, triangles=self._lh['tris'], color='white', opacity=0.2, reset_camera=False, render=False) self._renderer.mesh(*self._rh['rr'].T * 1000, triangles=self._rh['tris'], color='white', opacity=0.2, reset_camera=False, render=False) self._3d_chs = dict() for name in self._chs: self._plot_3d_ch(name) self._renderer.set_camera(azimuth=90, elevation=90, distance=300, focalpoint=tuple(self._ras)) # update plots self._draw() self._renderer._update() def _update_camera(self, render=False): """Update the camera position.""" self._renderer.set_camera( # needs fix, distance moves when focal point updates distance=self._renderer.plotter.camera.distance * 0.9, focalpoint=tuple(self._ras), reset_camera=False) def _plot_3d_ch(self, name, render=False): """Plot a single 3D channel.""" if name in self._3d_chs: self._renderer.plotter.remove_actor(self._3d_chs.pop(name), render=False) if not any(np.isnan(self._chs[name])): self._3d_chs[name] = self._renderer.sphere( tuple(self._chs[name]), scale=1, color=_CMAP(self._groups[name])[:3], opacity=self._ch_alpha)[0] # The actor scale is managed differently than the glyph scale # in order not to recreate objects, we use the actor scale self._3d_chs[name].SetOrigin(self._chs[name]) self._3d_chs[name].SetScale(self._radius * _RADIUS_SCALAR) if render: self._renderer._update() def _get_button_bar(self): """Make a bar with buttons for user interactions.""" hbox = QHBoxLayout() help_button = QPushButton('Help') help_button.released.connect(self._show_help) hbox.addWidget(help_button) hbox.addStretch(8) hbox.addWidget(QLabel('Snap to Center')) self._snap_button = QPushButton('Off') self._snap_button.setMaximumWidth(25) # not too big hbox.addWidget(self._snap_button) self._snap_button.released.connect(self._toggle_snap) self._toggle_snap() # turn on to start hbox.addStretch(1) self._toggle_brain_button = QPushButton('Show Brain') self._toggle_brain_button.released.connect(self._toggle_show_brain) hbox.addWidget(self._toggle_brain_button) hbox.addStretch(1) mark_button = QPushButton('Mark') hbox.addWidget(mark_button) mark_button.released.connect(self._mark_ch) remove_button = QPushButton('Remove') hbox.addWidget(remove_button) remove_button.released.connect(self._remove_ch) self._group_selector = ComboBox() group_model = self._group_selector.model() for i in range(_N_COLORS): self._group_selector.addItem(' ') color = QtGui.QColor() color.setRgb(*(255 * np.array(_CMAP(i))).round().astype(int)) brush = QtGui.QBrush(color) brush.setStyle(QtCore.Qt.SolidPattern) group_model.setData(group_model.index(i, 0), brush, QtCore.Qt.BackgroundRole) self._group_selector.clicked.connect(self._select_group) self._group_selector.currentIndexChanged.connect(self._select_group) hbox.addWidget(self._group_selector) # update background color for current selection self._update_group() return hbox def _get_slider_bar(self): """Make a bar with sliders on it.""" def make_label(name): label = QLabel(name) label.setAlignment(QtCore.Qt.AlignCenter) return label def make_slider(smin, smax, sval, sfun=None): slider = QSlider(QtCore.Qt.Horizontal) slider.setMinimum(int(round(smin))) slider.setMaximum(int(round(smax))) slider.setValue(int(round(sval))) slider.setTracking(False) # only update on release if sfun is not None: slider.valueChanged.connect(sfun) slider.keyPressEvent = self._key_press_event return slider slider_hbox = QHBoxLayout() ch_vbox = QVBoxLayout() ch_vbox.addWidget(make_label('ch alpha')) ch_vbox.addWidget(make_label('ch radius')) slider_hbox.addLayout(ch_vbox) ch_slider_vbox = QVBoxLayout() self._alpha_slider = make_slider(0, 100, self._ch_alpha * 100, self._update_ch_alpha) ch_plot_max = _CH_PLOT_SIZE // 50 # max 1 / 50 of plot size ch_slider_vbox.addWidget(self._alpha_slider) self._radius_slider = make_slider(0, ch_plot_max, self._radius, self._update_radius) ch_slider_vbox.addWidget(self._radius_slider) slider_hbox.addLayout(ch_slider_vbox) ct_vbox = QVBoxLayout() ct_vbox.addWidget(make_label('CT min')) ct_vbox.addWidget(make_label('CT max')) slider_hbox.addLayout(ct_vbox) ct_slider_vbox = QVBoxLayout() ct_min = int(round(np.nanmin(self._ct_data))) ct_max = int(round(np.nanmax(self._ct_data))) self._ct_min_slider = make_slider(ct_min, ct_max, ct_min, self._update_ct_scale) ct_slider_vbox.addWidget(self._ct_min_slider) self._ct_max_slider = make_slider(ct_min, ct_max, ct_max, self._update_ct_scale) ct_slider_vbox.addWidget(self._ct_max_slider) slider_hbox.addLayout(ct_slider_vbox) return slider_hbox def _get_bottom_bar(self): """Make a bar at the bottom with information in it.""" hbox = QHBoxLayout() hbox.addStretch(3) self._toggle_show_mip_button = QPushButton('Show Max Intensity Proj') self._toggle_show_mip_button.released.connect(self._toggle_show_mip) hbox.addWidget(self._toggle_show_mip_button) self._toggle_show_max_button = QPushButton('Show Maxima') self._toggle_show_max_button.released.connect(self._toggle_show_max) hbox.addWidget(self._toggle_show_max_button) self._intensity_label = QLabel('') # update later hbox.addWidget(self._intensity_label) VOX_label = QLabel('VOX =') self._VOX_textbox = QPlainTextEdit('') # update later self._VOX_textbox.setMaximumHeight(25) self._VOX_textbox.setMaximumWidth(125) self._VOX_textbox.focusOutEvent = self._update_VOX self._VOX_textbox.textChanged.connect(self._check_update_VOX) hbox.addWidget(VOX_label) hbox.addWidget(self._VOX_textbox) RAS_label = QLabel('RAS =') self._RAS_textbox = QPlainTextEdit('') # update later self._RAS_textbox.setMaximumHeight(25) self._RAS_textbox.setMaximumWidth(200) self._RAS_textbox.focusOutEvent = self._update_RAS self._RAS_textbox.textChanged.connect(self._check_update_RAS) hbox.addWidget(RAS_label) hbox.addWidget(self._RAS_textbox) self._update_moved() # update text now return hbox def _group_channels(self, groups): """Automatically find a group based on the name of the channel.""" if groups is not None: for name in self._ch_names: if name not in groups: raise ValueError(f'{name} not found in ``groups``') _validate_type(groups[name], (float, int), f'groups[{name}]') self.groups = groups else: i = 0 self._groups = dict() base_names = dict() for name in self._ch_names: # strip all numbers from the name base_name = ''.join([ letter for letter in name if not letter.isdigit() and letter != ' ' ]) if base_name in base_names: # look up group number by base name self._groups[name] = base_names[base_name] else: self._groups[name] = i base_names[base_name] = i i += 1 def _update_lines(self, group, only_2D=False): """Draw lines that connect the points in a group.""" if group in self._lines_2D: # remove existing 2D lines first for line in self._lines_2D[group]: line.remove() self._lines_2D.pop(group) if only_2D: # if not in projection, don't add 2D lines if self._toggle_show_mip_button.text() == \ 'Show Max Intensity Proj': return elif group in self._lines: # if updating 3D, remove first self._renderer.plotter.remove_actor(self._lines[group], render=False) pos = np.array([ self._chs[ch] for i, ch in enumerate(self._ch_names) if self._groups[ch] == group and i in self._seeg_idx and not np.isnan(self._chs[ch]).any() ]) if len(pos) < 2: # not enough points for line return # first, the insertion will be the point farthest from the origin # brains are a longer posterior-anterior, scale for this (80%) insert_idx = np.argmax( np.linalg.norm(pos * np.array([1, 0.8, 1]), axis=1)) # second, find the farthest point from the insertion target_idx = np.argmax(np.linalg.norm(pos[insert_idx] - pos, axis=1)) # third, make a unit vector and to add to the insertion for the bolt elec_v = pos[insert_idx] - pos[target_idx] elec_v /= np.linalg.norm(elec_v) if not only_2D: self._lines[group] = self._renderer.tube( [pos[target_idx]], [pos[insert_idx] + elec_v * _BOLT_SCALAR], radius=self._radius * _TUBE_SCALAR, color=_CMAP(group)[:3])[0] if self._toggle_show_mip_button.text() == 'Hide Max Intensity Proj': # add 2D lines on each slice plot if in max intensity projection target_vox = apply_trans(self._ras_vox_t, pos[target_idx]) insert_vox = apply_trans(self._ras_vox_t, pos[insert_idx] + elec_v * _BOLT_SCALAR) lines_2D = list() for axis in range(3): x, y = self._xy_idx[axis] lines_2D.append(self._figs[axis].axes[0].plot( [target_vox[x], insert_vox[x]], [target_vox[y], insert_vox[y]], color=_CMAP(group), linewidth=0.25, zorder=7)[0]) self._lines_2D[group] = lines_2D def _set_ch_names(self): """Add the channel names to the selector.""" self._ch_list_model = QtGui.QStandardItemModel(self._ch_list) for name in self._ch_names: self._ch_list_model.appendRow(QtGui.QStandardItem(name)) self._color_list_item(name=name) self._ch_list.setModel(self._ch_list_model) self._ch_list.clicked.connect(self._go_to_ch) self._ch_list.setCurrentIndex( self._ch_list_model.index(self._ch_index, 0)) self._ch_list.keyPressEvent = self._key_press_event def _select_group(self): """Change the group label to the selection.""" group = self._group_selector.currentIndex() self._groups[self._ch_names[self._ch_index]] = group # color differently if found already self._color_list_item(self._ch_names[self._ch_index]) self._update_group() def _update_group(self): """Set background for closed group menu.""" group = self._group_selector.currentIndex() rgb = (255 * np.array(_CMAP(group))).round().astype(int) self._group_selector.setStyleSheet( 'background-color: rgb({:d},{:d},{:d})'.format(*rgb)) self._group_selector.update() def _on_scroll(self, event): """Process mouse scroll wheel event to zoom.""" self._zoom(event.step, draw=True) def _zoom(self, sign=1, draw=False): """Zoom in on the image.""" delta = _ZOOM_STEP_SIZE * sign for axis, fig in enumerate(self._figs): xmid = self._images['cursor_v'][axis].get_xdata()[0] ymid = self._images['cursor_h'][axis].get_ydata()[0] xmin, xmax = fig.axes[0].get_xlim() ymin, ymax = fig.axes[0].get_ylim() xwidth = (xmax - xmin) / 2 - delta ywidth = (ymax - ymin) / 2 - delta if xwidth <= 0 or ywidth <= 0: return fig.axes[0].set_xlim(xmid - xwidth, xmid + xwidth) fig.axes[0].set_ylim(ymid - ywidth, ymid + ywidth) if draw: self._figs[axis].canvas.draw() def _update_ch_selection(self): """Update which channel is selected.""" name = self._ch_names[self._ch_index] self._ch_list.setCurrentIndex( self._ch_list_model.index(self._ch_index, 0)) self._group_selector.setCurrentIndex(self._groups[name]) self._update_group() if not np.isnan(self._chs[name]).any(): self._set_ras(self._chs[name]) self._update_camera(render=True) self._draw() def _go_to_ch(self, index): """Change current channel to the item selected.""" self._ch_index = index.row() self._update_ch_selection() @pyqtSlot() def _next_ch(self): """Increment the current channel selection index.""" self._ch_index = (self._ch_index + 1) % len(self._ch_names) self._update_ch_selection() @pyqtSlot() def _update_RAS(self, event): """Interpret user input to the RAS textbox.""" text = self._RAS_textbox.toPlainText() ras = self._convert_text(text, 'ras') if ras is not None: self._set_ras(ras) @pyqtSlot() def _update_VOX(self, event): """Interpret user input to the RAS textbox.""" text = self._VOX_textbox.toPlainText() ras = self._convert_text(text, 'vox') if ras is not None: self._set_ras(ras) def _convert_text(self, text, text_kind): text = text.replace('\n', '') vals = text.split(',') if len(vals) != 3: vals = text.split(' ') # spaces also okay as in freesurfer vals = [var.lstrip().rstrip() for var in vals] try: vals = np.array([float(var) for var in vals]).reshape(3) except Exception: self._update_moved() # resets RAS label return if text_kind == 'vox': vox = vals ras = apply_trans(self._vox_ras_t, vox) else: assert text_kind == 'ras' ras = vals vox = apply_trans(self._ras_vox_t, ras) wrong_size = any(var < 0 or var > n - 1 for var, n in zip(vox, self._voxel_sizes)) if wrong_size: self._update_moved() # resets RAS label return return ras @property def _ras(self): return self._ras_safe def _set_ras(self, ras, update_plots=True): ras = np.asarray(ras, dtype=float) assert ras.shape == (3, ) msg = ', '.join(f'{x:0.2f}' for x in ras) logger.debug(f'Trying RAS: ({msg}) mm') # clip to valid vox = apply_trans(self._ras_vox_t, ras) vox = np.array([ np.clip(d, 0, self._voxel_sizes[ii] - 1) for ii, d in enumerate(vox) ]) # transform back, make write-only self._ras_safe = apply_trans(self._vox_ras_t, vox) self._ras_safe.flags['WRITEABLE'] = False msg = ', '.join(f'{x:0.2f}' for x in self._ras_safe) logger.debug(f'Setting RAS: ({msg}) mm') if update_plots: self._move_cursors_to_pos() @property def _vox(self): return apply_trans(self._ras_vox_t, self._ras) @property def _current_slice(self): return self._vox.round().astype(int) @pyqtSlot() def _check_update_RAS(self): """Check whether the RAS textbox is done being edited.""" if '\n' in self._RAS_textbox.toPlainText(): self._update_RAS(event=None) self._ch_list.setFocus() # remove focus from text edit @pyqtSlot() def _check_update_VOX(self): """Check whether the VOX textbox is done being edited.""" if '\n' in self._VOX_textbox.toPlainText(): self._update_VOX(event=None) self._ch_list.setFocus() # remove focus from text edit def _color_list_item(self, name=None): """Color the item in the view list for easy id of marked channels.""" name = self._ch_names[self._ch_index] if name is None else name color = QtGui.QColor('white') if not np.isnan(self._chs[name]).any(): group = self._groups[name] color.setRgb(*[int(c * 255) for c in _CMAP(group)]) brush = QtGui.QBrush(color) brush.setStyle(QtCore.Qt.SolidPattern) self._ch_list_model.setData( self._ch_list_model.index(self._ch_names.index(name), 0), brush, QtCore.Qt.BackgroundRole) # color text black color = QtGui.QColor('black') brush = QtGui.QBrush(color) brush.setStyle(QtCore.Qt.SolidPattern) self._ch_list_model.setData( self._ch_list_model.index(self._ch_names.index(name), 0), brush, QtCore.Qt.ForegroundRole) @pyqtSlot() def _toggle_snap(self): """Toggle snapping the contact location to the center of mass.""" if self._snap_button.text() == 'Off': self._snap_button.setText('On') self._snap_button.setStyleSheet("background-color: green") else: # text == 'On', turn off self._snap_button.setText('Off') self._snap_button.setStyleSheet("background-color: red") @pyqtSlot() def _mark_ch(self): """Mark the current channel as being located at the crosshair.""" name = self._ch_names[self._ch_index] if self._snap_button.text() == 'Off': self._chs[name][:] = self._ras else: shape = np.mean(self._mri_data.shape) # Freesurfer shape (256) voxels_max = int(4 / 3 * np.pi * (shape * self._radius / _CH_PLOT_SIZE)**3) neighbors = _voxel_neighbors(self._vox, self._ct_data, thresh=0.5, voxels_max=voxels_max, use_relative=True) self._chs[name][:] = apply_trans( # to surface RAS self._vox_ras_t, np.array(list(neighbors)).mean(axis=0)) self._color_list_item() self._update_lines(self._groups[name]) self._update_ch_images(draw=True) self._plot_3d_ch(name, render=True) self._save_ch_coords() self._next_ch() self._ch_list.setFocus() @pyqtSlot() def _remove_ch(self): """Remove the location data for the current channel.""" name = self._ch_names[self._ch_index] self._chs[name] *= np.nan self._color_list_item() self._save_ch_coords() self._update_lines(self._groups[name]) self._update_ch_images(draw=True) self._plot_3d_ch(name, render=True) self._next_ch() self._ch_list.setFocus() def _draw(self, axis=None): """Update the figures with a draw call.""" for axis in (range(3) if axis is None else [axis]): self._figs[axis].canvas.draw() def _update_ch_images(self, axis=None, draw=False): """Update the channel image(s).""" for axis in range(3) if axis is None else [axis]: self._images['chs'][axis].set_data(self._make_ch_image(axis)) if self._toggle_show_mip_button.text() == \ 'Hide Max Intensity Proj': self._images['mip_chs'][axis].set_data( self._make_ch_image(axis, proj=True)) if draw: self._draw(axis) def _update_ct_images(self, axis=None, draw=False): """Update the CT image(s).""" for axis in range(3) if axis is None else [axis]: ct_data = np.take(self._ct_data, self._current_slice[axis], axis=axis).T # Threshold the CT so only bright objects (electrodes) are visible ct_data[ct_data < self._ct_min_slider.value()] = np.nan ct_data[ct_data > self._ct_max_slider.value()] = np.nan self._images['ct'][axis].set_data(ct_data) if 'local_max' in self._images: ct_max_data = np.take(self._ct_maxima, self._current_slice[axis], axis=axis).T self._images['local_max'][axis].set_data(ct_max_data) if draw: self._draw(axis) def _update_mri_images(self, axis=None, draw=False): """Update the CT image(s).""" if 'mri' in self._images: for axis in range(3) if axis is None else [axis]: self._images['mri'][axis].set_data( np.take(self._mri_data, self._current_slice[axis], axis=axis).T) if draw: self._draw(axis) def _update_images(self, axis=None, draw=True): """Update CT and channel images when general changes happen.""" self._update_ct_images(axis=axis) self._update_ch_images(axis=axis) self._update_mri_images(axis=axis) if draw: self._draw(axis) def _update_ct_scale(self): """Update CT min slider value.""" new_min = self._ct_min_slider.value() new_max = self._ct_max_slider.value() # handle inversions self._ct_min_slider.setValue(min([new_min, new_max])) self._ct_max_slider.setValue(max([new_min, new_max])) self._update_ct_images(draw=True) def _update_radius(self): """Update channel plot radius.""" self._radius = np.round(self._radius_slider.value()).astype(int) if self._toggle_show_max_button.text() == 'Hide Maxima': self._update_ct_maxima() self._update_ct_images() else: self._ct_maxima = None # signals ct max is out-of-date self._update_ch_images(draw=True) for name, actor in self._3d_chs.items(): if not np.isnan(self._chs[name]).any(): actor.SetOrigin(self._chs[name]) actor.SetScale(self._radius * _RADIUS_SCALAR) self._renderer._update() self._ch_list.setFocus() # remove focus from 3d plotter def _update_ch_alpha(self): """Update channel plot alpha.""" self._ch_alpha = self._alpha_slider.value() / 100 for axis in range(3): self._images['chs'][axis].set_alpha(self._ch_alpha) self._draw() for actor in self._3d_chs.values(): actor.GetProperty().SetOpacity(self._ch_alpha) self._renderer._update() self._ch_list.setFocus() # remove focus from 3d plotter def _move_cursors_to_pos(self): """Move the cursors to a position.""" for axis in range(3): x, y = self._vox[list(self._xy_idx[axis])] self._images['cursor_v'][axis].set_xdata([x, x]) self._images['cursor_h'][axis].set_ydata([y, y]) self._zoom(0) # doesn't actually zoom just resets view to center self._update_images(draw=True) self._update_moved() def _show_help(self): """Show the help menu.""" QMessageBox.information( self, 'Help', "Help:\n'm': mark channel location\n" "'r': remove channel location\n" "'b': toggle viewing of brain in T1\n" "'+'/'-': zoom\nleft/right arrow: left/right\n" "up/down arrow: superior/inferior\n" "left angle bracket/right angle bracket: anterior/posterior") def _update_ct_maxima(self): """Compute the maximum voxels based on the current radius.""" self._ct_maxima = maximum_filter(self._ct_data, (self._radius, ) * 3) == self._ct_data self._ct_maxima[self._ct_data <= np.median(self._ct_data)] = \ False self._ct_maxima = np.where(self._ct_maxima, 1, np.nan) # transparent def _toggle_show_mip(self): """Toggle whether the maximum-intensity projection is shown.""" if self._toggle_show_mip_button.text() == 'Show Max Intensity Proj': self._toggle_show_mip_button.setText('Hide Max Intensity Proj') self._images['mip'] = list() self._images['mip_chs'] = list() ct_min, ct_max = np.nanmin(self._ct_data), np.nanmax(self._ct_data) for axis in range(3): ct_mip_data = np.max(self._ct_data, axis=axis).T self._images['mip'].append(self._figs[axis].axes[0].imshow( ct_mip_data, cmap='gray', aspect='auto', vmin=ct_min, vmax=ct_max, zorder=5)) # add circles for each channel xs, ys, colors = list(), list(), list() for name, ras in self._chs.items(): xyz = self._vox xs.append(xyz[self._xy_idx[axis][0]]) ys.append(xyz[self._xy_idx[axis][1]]) colors.append(_CMAP(self._groups[name])) self._images['mip_chs'].append(self._figs[axis].axes[0].imshow( self._make_ch_image(axis, proj=True), aspect='auto', extent=self._ch_extents[axis], zorder=6, cmap=_CMAP, alpha=1, vmin=0, vmax=_N_COLORS)) for group in set(self._groups.values()): self._update_lines(group, only_2D=True) else: for img in self._images['mip'] + self._images['mip_chs']: img.remove() self._images.pop('mip') self._images.pop('mip_chs') self._toggle_show_mip_button.setText('Show Max Intensity Proj') for group in set(self._groups.values()): # remove lines self._update_lines(group, only_2D=True) self._draw() def _toggle_show_max(self): """Toggle whether to color local maxima differently.""" if self._toggle_show_max_button.text() == 'Show Maxima': self._toggle_show_max_button.setText('Hide Maxima') # happens on initiation or if the radius is changed with it off if self._ct_maxima is None: # otherwise don't recompute self._update_ct_maxima() self._images['local_max'] = list() for axis in range(3): ct_max_data = np.take(self._ct_maxima, self._current_slice[axis], axis=axis).T self._images['local_max'].append( self._figs[axis].axes[0].imshow(ct_max_data, cmap='autumn', aspect='auto', vmin=0, vmax=1, zorder=4)) else: for img in self._images['local_max']: img.remove() self._images.pop('local_max') self._toggle_show_max_button.setText('Show Maxima') self._draw() def _toggle_show_brain(self): """Toggle whether the brain/MRI is being shown.""" if 'mri' in self._images: for img in self._images['mri']: img.remove() self._images.pop('mri') self._toggle_brain_button.setText('Show Brain') else: self._images['mri'] = list() for axis in range(3): mri_data = np.take(self._mri_data, self._current_slice[axis], axis=axis).T self._images['mri'].append(self._figs[axis].axes[0].imshow( mri_data, cmap='hot', aspect='auto', alpha=0.25, zorder=2)) self._toggle_brain_button.setText('Hide Brain') self._draw() def _key_press_event(self, event): """Execute functions when the user presses a key.""" if event.key() == 'escape': self.close() if event.text() == 'h': self._show_help() if event.text() == 'm': self._mark_ch() if event.text() == 'r': self._remove_ch() if event.text() == 'b': self._toggle_show_brain() if event.text() in ('=', '+', '-'): self._zoom(sign=-2 * (event.text() == '-') + 1, draw=True) # Changing slices if event.key() in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_Left, QtCore.Qt.Key_Right, QtCore.Qt.Key_Comma, QtCore.Qt.Key_Period, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown): ras = np.array(self._ras) if event.key() in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down): ras[2] += 2 * (event.key() == QtCore.Qt.Key_Up) - 1 elif event.key() in (QtCore.Qt.Key_Left, QtCore.Qt.Key_Right): ras[0] += 2 * (event.key() == QtCore.Qt.Key_Right) - 1 else: ras[1] += 2 * (event.key() == QtCore.Qt.Key_PageUp or event.key() == QtCore.Qt.Key_Period) - 1 self._set_ras(ras) def _on_click(self, event, axis): """Move to view on MRI and CT on click.""" if event.inaxes is self._figs[axis].axes[0]: # Data coordinates are voxel coordinates pos = (event.xdata, event.ydata) logger.info(f'Clicked {"XYZ"[axis]} ({axis}) axis at pos {pos}') xyz = self._vox xyz[list(self._xy_idx[axis])] = pos logger.debug(f'Using voxel {list(xyz)}') ras = apply_trans(self._vox_ras_t, xyz) self._set_ras(ras) def _update_moved(self): """Update when cursor position changes.""" self._RAS_textbox.setPlainText( '{:.2f}, {:.2f}, {:.2f}'.format(*self._ras)) self._VOX_textbox.setPlainText( '{:3d}, {:3d}, {:3d}'.format(*self._current_slice)) self._intensity_label.setText('intensity = {:.2f}'.format( self._ct_data[tuple(self._current_slice)])) @safe_event def closeEvent(self, event): """Clean up upon closing the window.""" self._renderer.plotter.close() self.close()
class Player(QWidget): fullScreenChanged = pyqtSignal(bool) def __init__(self, playlist, parent=None): super(Player, self).__init__(parent) self.colorDialog = None self.trackInfo = "" self.statusInfo = "" self.duration = 0 self.player = QMediaPlayer() self.playlist = QMediaPlaylist() self.player.setPlaylist(self.playlist) self.player.durationChanged.connect(self.durationChanged) self.player.positionChanged.connect(self.positionChanged) self.player.metaDataChanged.connect(self.metaDataChanged) self.playlist.currentIndexChanged.connect(self.playlistPositionChanged) self.player.mediaStatusChanged.connect(self.statusChanged) self.player.bufferStatusChanged.connect(self.bufferingProgress) self.player.videoAvailableChanged.connect(self.videoAvailableChanged) self.player.error.connect(self.displayErrorMessage) self.videoWidget = VideoWidget() self.player.setVideoOutput(self.videoWidget) self.playlistModel = PlaylistModel() self.playlistModel.setPlaylist(self.playlist) self.playlistView = QListView() self.playlistView.setModel(self.playlistModel) self.playlistView.setCurrentIndex( self.playlistModel.index(self.playlist.currentIndex(), 0)) self.playlistView.activated.connect(self.jump) self.script_box = QPlainTextEdit() self.segmentList = QTreeWidget() self.segmentList.setSortingEnabled(True) #self.segmentList.setColumnCount(5) self.segmentList.setColumnCount(4) #self.segmentList.setHeaderLabels(['Product','Start','Label','Tool','Behavior']) self.segmentList.setHeaderLabels(['Start segment', 'End segment', 'Label', 'Event']) ''' self.productTextInput = QLineEdit() self.startTextInput = QLineEdit() self.labelTextInput = QLineEdit() self.toolTextInput = QLineEdit() self.behaviorTextInput = QLineEdit() ''' self.startTextInput = QLineEdit() self.endTextInput = QLineEdit() self.labelTextInput = QLineEdit() self.contentTextInput = QLineEdit() self.addBtn = QPushButton("Add") self.addBtn.clicked.connect(self.addSegment) self.saveBtn = QPushButton("Save") self.saveBtn.clicked.connect(self.saveSegments) self.slider = QSlider(Qt.Horizontal) self.slider.setRange(0, self.player.duration() / 1000) self.labelDuration = QLabel() self.slider.sliderMoved.connect(self.seek) self.labelHistogram = QLabel() self.labelHistogram.setText("Histogram:") self.histogram = HistogramWidget() histogramLayout = QHBoxLayout() histogramLayout.addWidget(self.labelHistogram) histogramLayout.addWidget(self.histogram, 1) self.probe = QVideoProbe() self.probe.videoFrameProbed.connect(self.histogram.processFrame) self.probe.setSource(self.player) openButton = QPushButton("Open", clicked=self.open) if os.path.isdir(VIDEO_DIR): self.open_folder(VIDEO_DIR) controls = PlayerControls() controls.setState(self.player.state()) controls.setVolume(self.player.volume()) controls.setMuted(controls.isMuted()) controls.play.connect(self.player.play) controls.pause.connect(self.player.pause) controls.stop.connect(self.player.stop) controls.next.connect(self.playlist.next) controls.previous.connect(self.previousClicked) controls.changeVolume.connect(self.player.setVolume) controls.changeMuting.connect(self.player.setMuted) controls.changeRate.connect(self.player.setPlaybackRate) controls.stop.connect(self.videoWidget.update) self.player.stateChanged.connect(controls.setState) self.player.volumeChanged.connect(controls.setVolume) self.player.mutedChanged.connect(controls.setMuted) #self.segmentButton = QPushButton("Segment") #self.segmentButton.clicked.connect(self.createNewSegment) self.startSegmentButton = QPushButton("Start Segment") self.startSegmentButton.clicked.connect(self.createNewStartSegment) # self.segmentButton.setCheckable(True) self.endSegmentButton = QPushButton("End Segment") self.endSegmentButton.clicked.connect(self.createNewEndSegment) #self.fullScreenButton = QPushButton("FullScreen") #self.fullScreenButton.setCheckable(True) self.colorButton = QPushButton("Color Options...") self.colorButton.setEnabled(False) self.colorButton.clicked.connect(self.showColorDialog) displayLayout = QHBoxLayout() # videoLayout = QVBoxLayout() # videoLayout.addWidget(self.videoWidget) # videoLayout.addWidget(self.script_box) displayLayout.addWidget(self.videoWidget, 3) editLayout = QVBoxLayout() editLayout.addWidget(self.playlistView, 2) #editLayout.addWidget(self.script_box, 4) editLayout.addWidget(self.segmentList, 3) segmentInputLayout = QHBoxLayout() ''' segmentInputLayout.addWidget(self.productTextInput) segmentInputLayout.addWidget(self.startTextInput) segmentInputLayout.addWidget(self.labelTextInput) segmentInputLayout.addWidget(self.toolTextInput) segmentInputLayout.addWidget(self.behaviorTextInput) ''' segmentInputLayout.addWidget(self.startTextInput) segmentInputLayout.addWidget(self.endTextInput) segmentInputLayout.addWidget(self.labelTextInput) segmentInputLayout.addWidget(self.contentTextInput) editLayout.addLayout(segmentInputLayout,1) displayLayout.addLayout(editLayout, 2) controlLayout = QHBoxLayout() controlLayout.setContentsMargins(0, 0, 0, 0) controlLayout.addWidget(openButton) controlLayout.addStretch(1) controlLayout.addWidget(controls) controlLayout.addStretch(1) #controlLayout.addWidget(self.segmentButton) controlLayout.addWidget(self.startSegmentButton) controlLayout.addWidget(self.endSegmentButton) controlLayout.addWidget(self.addBtn) controlLayout.addWidget(self.saveBtn) #controlLayout.addWidget(self.fullScreenButton) # controlLayout.addWidget(self.colorButton) layout = QVBoxLayout() layout.addLayout(displayLayout, 2) hLayout = QHBoxLayout() hLayout.addWidget(self.slider) hLayout.addWidget(self.labelDuration) layout.addLayout(hLayout) layout.addLayout(controlLayout) # layout.addLayout(histogramLayout) self.setLayout(layout) if not self.player.isAvailable(): QMessageBox.warning(self, "Service not available", "The QMediaPlayer object does not have a valid service.\n" "Please check the media service plugins are installed.") controls.setEnabled(False) self.playlistView.setEnabled(False) openButton.setEnabled(False) self.colorButton.setEnabled(False) #self.fullScreenButton.setEnabled(False) self.metaDataChanged() self.addToPlaylist(playlist) def open(self): fileNames, _ = QFileDialog.getOpenFileNames(self, "Open Files") self.addToPlaylist(fileNames) def open_folder(self, folder_path): fileNames = [folder_path+x for x in os.listdir(folder_path) if x.endswith('.mp4')] self.addToPlaylist(fileNames) def addToPlaylist(self, fileNames): for name in fileNames: fileInfo = QFileInfo(name) if fileInfo.exists(): url = QUrl.fromLocalFile(fileInfo.absoluteFilePath()) if fileInfo.suffix().lower() == 'm3u': self.playlist.load(url) else: self.playlist.addMedia(QMediaContent(url)) else: url = QUrl(name) if url.isValid(): self.playlist.addMedia(QMediaContent(url)) def addSegment(self): item = TreeWidgetItem(self.segmentList) ''' item.setText(0, self.productTextInput.text()) item.setText(1, self.startTextInput.text()) item.setText(2, self.labelTextInput.text()) item.setText(3, self.toolTextInput.text()) item.setText(4, self.behaviorTextInput.text()) ''' item.setText(0, self.startTextInput.text()) item.setText(1, self.endTextInput.text()) item.setText(2, self.labelTextInput.text()) item.setText(3, self.contentTextInput.text()) item.setFlags(item.flags() | Qt.ItemIsEditable) self.segmentList.addTopLevelItem(item) self.segmentList.sortByColumn(0, Qt.AscendingOrder) self.clear_input_boxes() self.player.play() def saveSegments(self): itemCnt = self.segmentList.topLevelItemCount() colCnt = self.segmentList.columnCount() save_dict = {'segments':[]} for i in range(itemCnt): item = self.segmentList.topLevelItem(i) temp_data = [] for j in range(colCnt): temp_data.append(item.text(j)) #temp_dict = {'product': temp_data[0], 'start': temp_data[1], 'label': temp_data[2], 'tool': temp_data[3], 'behavior': temp_data[4]} if len(temp_data[0]) > 0 and len(temp_data[1]) > 0 and (':' in temp_data[0]) and (':' in temp_data[1]): start_interval_seconds = 0 j = 0 while j < len(temp_data[0].split(':')): start_interval_seconds += (int(temp_data[0].split(':')[- 1 - j]) * (60 ** j)) j += 1 end_interval_seconds = 0 j = 0 while j < len(temp_data[1].split(':')): end_interval_seconds += (int(temp_data[1].split(':')[- 1 - j]) * (60 ** j)) j += 1 else: start_interval_seconds = '' end_interval_seconds = '' temp_dict = {'start_segment': start_interval_seconds, 'end_segment': end_interval_seconds, 'label': temp_data[2], 'event': temp_data[3]} save_dict['segments'].append(temp_dict) import json file_name = self.playlist.currentMedia().canonicalUrl().fileName() with open(SEGMENT_DIR+file_name.replace('.mp4','.json'),'w') as file: json.dump(save_dict, file) def durationChanged(self, duration): duration /= 1000 self.duration = duration self.slider.setMaximum(duration) def positionChanged(self, progress): progress /= 1000 if not self.slider.isSliderDown(): self.slider.setValue(progress) self.updateDurationInfo(progress) def metaDataChanged(self): if self.player.isMetaDataAvailable(): self.setTrackInfo("%s - %s" % ( self.player.metaData(QMediaMetaData.AlbumArtist), self.player.metaData(QMediaMetaData.Title))) def previousClicked(self): # Go to the previous track if we are within the first 5 seconds of # playback. Otherwise, seek to the beginning. if self.player.position() <= 5000: self.playlist.previous() else: self.player.setPosition(0) def clear_input_boxes(self): ''' self.productTextInput.clear() self.startTextInput.clear() self.labelTextInput.clear() self.toolTextInput.clear() self.behaviorTextInput.clear() ''' self.startTextInput.clear() self.endTextInput.clear() self.labelTextInput.clear() self.contentTextInput.clear() def jump(self, index): if index.isValid(): self.playlist.setCurrentIndex(index.row()) self.player.play() file_name = self.playlist.currentMedia().canonicalUrl().fileName() ''' script_file_name = file_name.replace('.mp4','.txt') if os.path.isfile(SCRIPT_DIR+script_file_name): text=open(SCRIPT_DIR+script_file_name).read() self.script_box.setPlainText(text) ''' segment_file_path = SEGMENT_DIR + file_name.replace('.mp4','.json') json_dict = self.open_json(segment_file_path) self.clear_input_boxes() self.segmentList.clear() for segment in json_dict["segments"]: item = TreeWidgetItem(self.segmentList) ''' item.setText(0, segment['product']) item.setText(1, str(segment['start'])) item.setText(2, segment['label']) item.setText(3, segment['tool']) item.setText(4, segment['behavior']) ''' item.setText(0, segment['start_segment']) item.setText(1, segment['end_segment']) item.setText(2, segment['label']) item.setText(3, segment['content']) item.setFlags(item.flags() | Qt.ItemIsEditable) self.segmentList.addTopLevelItem(item) # print([str(x.text()) for x in self.segmentList.currentItem()]) def open_json(self, file_path): import json try: with open(file_path, 'r') as file: json_dict = json.loads(file.read()) except: json_dict = {"segments":[]} # json_dict = {"segments":[{"product":"Sorry","start":"File not found.","label":"","tool":"","behavior":""}]} return json_dict def playlistPositionChanged(self, position): self.playlistView.setCurrentIndex( self.playlistModel.index(position, 0)) def seek(self, seconds): self.player.setPosition(seconds * 1000) def statusChanged(self, status): self.handleCursor(status) if status == QMediaPlayer.LoadingMedia: self.setStatusInfo("Loading...") elif status == QMediaPlayer.StalledMedia: self.setStatusInfo("Media Stalled") elif status == QMediaPlayer.EndOfMedia: QApplication.alert(self) elif status == QMediaPlayer.InvalidMedia: self.displayErrorMessage() else: self.setStatusInfo("") def handleCursor(self, status): if status in (QMediaPlayer.LoadingMedia, QMediaPlayer.BufferingMedia, QMediaPlayer.StalledMedia): self.setCursor(Qt.BusyCursor) else: self.unsetCursor() def bufferingProgress(self, progress): self.setStatusInfo("Buffering %d%" % progress) def videoAvailableChanged(self, available): ''' if available: self.fullScreenButton.clicked.connect( self.videoWidget.setFullScreen) self.videoWidget.fullScreenChanged.connect( self.fullScreenButton.setChecked) if self.fullScreenButton.isChecked(): self.videoWidget.setFullScreen(True) else: self.fullScreenButton.clicked.disconnect( self.videoWidget.setFullScreen) self.videoWidget.fullScreenChanged.disconnect( self.fullScreenButton.setChecked) self.videoWidget.setFullScreen(False) ''' self.colorButton.setEnabled(available) def setTrackInfo(self, info): self.trackInfo = info if self.statusInfo != "": self.setWindowTitle("%s | %s" % (self.trackInfo, self.statusInfo)) else: self.setWindowTitle(self.trackInfo) def setStatusInfo(self, info): self.statusInfo = info if self.statusInfo != "": self.setWindowTitle("%s | %s" % (self.trackInfo, self.statusInfo)) else: self.setWindowTitle(self.trackInfo) def displayErrorMessage(self): self.setStatusInfo(self.player.errorString()) def updateDurationInfo(self, currentInfo): duration = self.duration if currentInfo or duration: currentTime = QTime((currentInfo/3600)%60, (currentInfo/60)%60, currentInfo%60, (currentInfo*1000)%1000) totalTime = QTime((duration/3600)%60, (duration/60)%60, duration%60, (duration*1000)%1000); format = 'hh:mm:ss' if duration > 3600 else 'mm:ss' tStr = currentTime.toString(format) + " / " + totalTime.toString(format) else: tStr = "" self.labelDuration.setText(tStr) ''' def createNewSegment(self): self.startTextInput.setText(str(int(self.player.position()/1000))) ''' def createNewStartSegment(self): seconds = int(self.player.position()/1000) self.startTextInput.setText("{:02d}".format(math.floor(seconds / 3600)) + ':' + "{:02d}".format( math.floor((seconds / 60)) - math.floor(seconds / 3600) * 60) + ':' + "{:02d}".format(seconds % 60)) def createNewEndSegment(self): seconds = int(self.player.position() / 1000) self.endTextInput.setText("{:02d}".format(math.floor(seconds / 3600)) + ':' + "{:02d}".format( math.floor((seconds / 60)) - math.floor(seconds / 3600) * 60) + ':' + "{:02d}".format(seconds % 60)) self.player.pause() def showColorDialog(self): if self.colorDialog is None: brightnessSlider = QSlider(Qt.Horizontal) brightnessSlider.setRange(-100, 100) brightnessSlider.setValue(self.videoWidget.brightness()) brightnessSlider.sliderMoved.connect( self.videoWidget.setBrightness) self.videoWidget.brightnessChanged.connect( brightnessSlider.setValue) contrastSlider = QSlider(Qt.Horizontal) contrastSlider.setRange(-100, 100) contrastSlider.setValue(self.videoWidget.contrast()) contrastSlider.sliderMoved.connect(self.videoWidget.setContrast) self.videoWidget.contrastChanged.connect(contrastSlider.setValue) hueSlider = QSlider(Qt.Horizontal) hueSlider.setRange(-100, 100) hueSlider.setValue(self.videoWidget.hue()) hueSlider.sliderMoved.connect(self.videoWidget.setHue) self.videoWidget.hueChanged.connect(hueSlider.setValue) saturationSlider = QSlider(Qt.Horizontal) saturationSlider.setRange(-100, 100) saturationSlider.setValue(self.videoWidget.saturation()) saturationSlider.sliderMoved.connect( self.videoWidget.setSaturation) self.videoWidget.saturationChanged.connect( saturationSlider.setValue) layout = QFormLayout() layout.addRow("Brightness", brightnessSlider) layout.addRow("Contrast", contrastSlider) layout.addRow("Hue", hueSlider) layout.addRow("Saturation", saturationSlider) button = QPushButton("Close") layout.addRow(button) self.colorDialog = QDialog(self) self.colorDialog.setWindowTitle("Color Options") self.colorDialog.setLayout(layout) button.clicked.connect(self.colorDialog.close) self.colorDialog.show()
class MainWindow(QMainWindow): """MNELAB main window. """ def __init__(self): super().__init__() self.datasets = DataSets() self._max_recent = 6 # maximum number of recent files self.history = [] # command history settings = self._read_settings() self.recent = settings["recent"] if settings["recent"] else [] self.setGeometry(300, 300, 800, 600) self.setWindowTitle("MNELAB") menubar = self.menuBar() file_menu = menubar.addMenu("&File") file_menu.addAction("&Open...", self.open_file, QKeySequence.Open) self.recent_menu = file_menu.addMenu("Open Recent") self.recent_menu.aboutToShow.connect(self._update_recent_menu) self.recent_menu.triggered.connect(self._load_recent) if not self.recent: self.recent_menu.setEnabled(False) self.close_file_action = file_menu.addAction("&Close", self.close_file, QKeySequence.Close) file_menu.addSeparator() file_menu.addAction("&Quit", self.close, QKeySequence.Quit) plot_menu = menubar.addMenu("&Plot") self.plot_raw_action = plot_menu.addAction("&Raw data", self.plot_raw) tools_menu = menubar.addMenu("&Tools") self.filter_action = tools_menu.addAction("&Filter data...", self.filter_data) self.run_ica_action = tools_menu.addAction("&Run ICA...") self.import_ica_action = tools_menu.addAction("&Load ICA...", self.load_ica) view_menu = menubar.addMenu("&View") view_menu.addAction("Show/hide statusbar", self._toggle_statusbar) help_menu = menubar.addMenu("&Help") help_menu.addAction("&About", self.show_about) help_menu.addAction("About &Qt", self.show_about_qt) self.names = QStringListModel() splitter = QSplitter() self.sidebar = QListView() self.sidebar.setFocusPolicy(0) self.sidebar.setFrameStyle(0) self.sidebar.setModel(self.names) self.sidebar.clicked.connect(self._update_data) splitter.addWidget(self.sidebar) self.infowidget = InfoWidget() splitter.addWidget(self.infowidget) width = splitter.size().width() splitter.setSizes((width * 0.25, width * 0.75)) self.setCentralWidget(splitter) self.status_label = QLabel() self.statusBar().addPermanentWidget(self.status_label) if settings["statusbar"]: self.statusBar().show() else: self.statusBar().hide() self._toggle_actions(False) self.show() def open_file(self): """Open file. """ fname = QFileDialog.getOpenFileName(self, "Open file", filter="*.bdf *.edf")[0] if fname: self.load_file(fname) def load_file(self, fname): raw = mne.io.read_raw_edf(fname, stim_channel=None, preload=True) name, _ = splitext(split(fname)[-1]) self.history.append("raw = mne.io.read_raw_edf('{}', " "stim_channel=None, preload=True)".format(fname)) self.datasets.insert_data(DataSet(name=name, fname=fname, raw=raw)) self._update_sidebar() self._update_main() self._add_recent(fname) self._update_statusbar() self._toggle_actions() def close_file(self): """Close current file. """ self.datasets.remove_data() self._update_sidebar() self._update_main() self._update_statusbar() if not self.datasets: self.infowidget.clear() self._toggle_actions(False) self.status_label.clear() def get_info(self): """Get basic information on current file. """ raw = self.datasets.current.raw fname = self.datasets.current.fname nchan = raw.info["nchan"] chans = Counter([channel_type(raw.info, i) for i in range(nchan)]) return {"File name": fname if fname else "-", "Number of channels": raw.info["nchan"], "Channels": ", ".join( [" ".join([str(v), k.upper()]) for k, v in chans.items()]), "Samples": raw.n_times, "Sampling frequency": str(raw.info["sfreq"]) + " Hz", "Length": str(raw.n_times / raw.info["sfreq"]) + " s", "Size in memory": "{:.2f} MB".format( raw._data.nbytes / 1024 ** 2), "Size on disk": "-" if not fname else "{:.2f} MB".format( getsize(fname) / 1024 ** 2)} def plot_raw(self): """Plot raw data. """ events = self.datasets.current.events self.datasets.current.raw.plot(events=events) def load_ica(self): """Load ICA solution from a file. """ fname = QFileDialog.getOpenFileName(self, "Load ICA", filter="*.fif *.fif.gz") if fname[0]: self.state.ica = mne.preprocessing.read_ica(fname[0]) def filter_data(self): dialog = FilterDialog() if dialog.exec_(): low, high = dialog.low, dialog.high self.datasets.current.raw.filter(low, high) self.history.append("raw.filter({}, {})".format(low, high)) if QMessageBox.question(self, "Add new data set", "Store the current signals in a new data " "set?") == QMessageBox.Yes: new = DataSet(name="NEW", fname="", raw=self.datasets.current.raw) self.datasets.insert_data(new) self._update_sidebar() self._update_main() self._update_statusbar() def show_about(self): """Show About dialog. """ QMessageBox.about(self, "About MNELAB", "Licensed under the BSD 3-clause license.\n" "Copyright 2017 by Clemens Brunner.") def show_about_qt(self): """Show About Qt dialog. """ QMessageBox.aboutQt(self, "About Qt") def _update_sidebar(self): self.names.setStringList(self.datasets.names) self.sidebar.setCurrentIndex(self.names.index(self.datasets.index)) def _update_main(self): if self.datasets: self.infowidget.set_values(self.get_info()) else: self.infowidget.clear() def _update_statusbar(self): if self.datasets: mb = self.datasets.nbytes / 1024 ** 2 self.status_label.setText("Total Memory: {:.2f} MB".format(mb)) else: self.status_label.clear() def _toggle_actions(self, enabled=True): """Toggle actions. """ self.close_file_action.setEnabled(enabled) self.plot_raw_action.setEnabled(enabled) self.filter_action.setEnabled(enabled) self.run_ica_action.setEnabled(enabled) self.import_ica_action.setEnabled(enabled) def _add_recent(self, fname): if fname in self.recent: # avoid duplicates self.recent.remove(fname) self.recent.insert(0, fname) while len(self.recent) > self._max_recent: # prune list self.recent.pop() self._write_settings() if not self.recent_menu.isEnabled(): self.recent_menu.setEnabled(True) def _write_settings(self): settings = QSettings() if self.recent: settings.setValue("recent", self.recent) settings.setValue("statusbar", not self.statusBar().isHidden()) def _read_settings(self): settings = QSettings() recent = settings.value("recent") statusbar = settings.value("statusbar") if (statusbar is None) or (statusbar == "true"): statusbar = True else: statusbar = False return {"recent": recent, "statusbar": statusbar} @pyqtSlot(QModelIndex) def _update_data(self, selected): """Update index and information based on the state of the sidebar. """ if selected.row() != self.datasets.index: self.datasets.index = selected.row() self.datasets.update_current() self._update_main() @pyqtSlot() def _update_recent_menu(self): self.recent_menu.clear() for recent in self.recent: self.recent_menu.addAction(recent) @pyqtSlot(QAction) def _load_recent(self, action): self.load_file(action.text()) @pyqtSlot() def _toggle_statusbar(self): if self.statusBar().isHidden(): self.statusBar().show() else: self.statusBar().hide() self._write_settings() def closeEvent(self, event): print("\nCommand History") print("===============") print("\n".join(self.history)) event.accept()
class App(QMainWindow): def __init__(self): super().__init__() # self.gfont = FontProperties(fname='assets/PingFang.ttc') self.accuser = Accuser() self.dbconn = DBConn() self.scheduler = Scheduler(self.dbconn) self.scheduler_status = False self.automaton = ReAutomaton() self.analyzer = anal.Analyzer(self.dbconn) self.init_ui() def closeEvent(self, a0): self.scheduler.browser.quit() return super().closeEvent(a0) def init_ui(self): self.setWindowTitle("Weibo Monitor") self.setGeometry(30, 30, 1024, 720) self.init_components() self.show() def init_components(self): self.gwidget = QTabWidget() self.setCentralWidget(self.gwidget) self.glayout = QHBoxLayout() self.crawl_widget = QWidget() self.ana_widget = QWidget() self.init_crawl_widget() self.init_ana_widget() self.gwidget.addTab(self.crawl_widget, "Crawl") self.gwidget.addTab(self.ana_widget, "Analyze") self.init_triggers() self.running = False def init_crawl_widget(self): self.llayout = QVBoxLayout() self.crawl_widget.setLayout(self.llayout) self.log_label = QLabel("Log") self.log_list = QTableView() self.log_model = QStandardItemModel() self.log_model.setColumnCount(3) self.log_model.setHorizontalHeaderLabels(["Status", "Time", "Message"]) self.log_list.setModel(self.log_model) self.log_list.horizontalHeader().setSectionResizeMode( QHeaderView.Stretch) self.log_list.horizontalHeader().setSectionResizeMode( 0, QHeaderView.ResizeToContents) self.log_list.horizontalHeader().setSectionResizeMode( 1, QHeaderView.ResizeToContents) self.log_list.horizontalHeader().setSectionResizeMode( 2, QHeaderView.ResizeToContents) log_widget = QWidget() log_layout = QVBoxLayout() log_layout.addWidget(self.log_label) log_layout.addWidget(self.log_list) log_widget.setLayout(log_layout) self.rule_label = QLabel("Rule") self.rule_edit = QTextEdit() rule_widget = QWidget() rule_layout = QVBoxLayout() rule_layout.addWidget(self.rule_label) rule_layout.addWidget(self.rule_edit) rule_widget.setLayout(rule_layout) self.crawl_button = QPushButton('Crawl') self.llayout.addWidget(log_widget) self.llayout.addWidget(rule_widget) self.llayout.addWidget(self.crawl_button) self.llayout.setStretch(0, 15) self.llayout.setStretch(1, 6) self.llayout.setStretch(2, 1) def init_ana_widget(self): self.rlayout = QHBoxLayout() self.ana_widget.setLayout(self.rlayout) self.fig = Figure() self.canvas = FigureCanvas(self.fig) self.canvas_label = QLabel("Result") canvas_widget = QWidget() canvas_layout = QVBoxLayout() canvas_widget.setLayout(canvas_layout) canvas_layout.addWidget(self.canvas_label) canvas_layout.addWidget(self.canvas) canvas_layout.setStretch(0, 1) canvas_layout.setStretch(1, 20) self.tracklist_panel = QWidget() self.tracklist_layout = QVBoxLayout() self.tracklist_panel.setLayout(self.tracklist_layout) self.init_topic_tracklist() self.init_weibo_tracklist() self.tracklist_layout.addWidget(self.topic_tracklist_widget) self.tracklist_layout.addWidget(self.weibo_tracklist_widget) self.rlayout.addWidget(self.tracklist_panel) self.rlayout.addWidget(canvas_widget) cfg = self.scheduler.load_config() for topic_name in cfg['topic_tracklist']: self.topic_tracklist_model.appendRow(QStandardItem(topic_name)) for weibo_id in cfg["weibo_id_tracklist"]: self.weibo_tracklist_model.appendRow(QStandardItem(str(weibo_id))) def init_topic_tracklist(self): self.topic_tracklist = QListView() self.topic_tracklist_label = QLabel("Topic Track List") self.topic_tracklist_model = QStandardItemModel() self.topic_tracklist.setModel(self.topic_tracklist_model) topic_tracklist_buttons = QWidget() topic_tracklist_buttons_layout = QHBoxLayout() topic_tracklist_buttons_layout.setSpacing(0) topic_tracklist_buttons_layout.setContentsMargins(0, 0, 0, 0) topic_tracklist_buttons.setLayout(topic_tracklist_buttons_layout) self.topic_tracklist_add_button = QPushButton("+") self.topic_tracklist_del_button = QPushButton("-") self.topic_tracklist_conf_button = QPushButton("confirm") self.topic_analyze_button = QPushButton("analyze") topic_tracklist_buttons_layout.addWidget( self.topic_tracklist_add_button) topic_tracklist_buttons_layout.addWidget( self.topic_tracklist_del_button) topic_tracklist_buttons_layout.addStretch(20) topic_tracklist_buttons_layout.addWidget( self.topic_tracklist_conf_button) topic_tracklist_buttons_layout.addWidget(self.topic_analyze_button) topic_tracklist_buttons_layout.setStretch(0, 6) topic_tracklist_buttons_layout.setStretch(1, 6) topic_tracklist_buttons_layout.setStretch(2, 6) topic_tracklist_buttons_layout.setStretch(3, 6) self.topic_tracklist_widget = QWidget() topic_tracklist_layout = QVBoxLayout() self.topic_tracklist_widget.setLayout(topic_tracklist_layout) topic_tracklist_layout.addWidget(self.topic_tracklist_label) topic_tracklist_layout.addWidget(self.topic_tracklist) topic_tracklist_layout.addWidget(topic_tracklist_buttons) def init_weibo_tracklist(self): self.weibo_tracklist = QListView() self.weibo_tracklist_label = QLabel("Weibo Track List") self.weibo_tracklist_model = QStandardItemModel() self.weibo_tracklist.setModel(self.weibo_tracklist_model) weibo_tracklist_buttons = QWidget() weibo_tracklist_buttons_layout = QHBoxLayout() weibo_tracklist_buttons_layout.setSpacing(0) weibo_tracklist_buttons_layout.setContentsMargins(0, 0, 0, 0) weibo_tracklist_buttons.setLayout(weibo_tracklist_buttons_layout) self.weibo_tracklist_add_button = QPushButton("+") self.weibo_tracklist_del_button = QPushButton("-") self.weibo_tracklist_conf_button = QPushButton("confirm") self.weibo_analyze_button = QPushButton("analyze") weibo_tracklist_buttons_layout.addWidget( self.weibo_tracklist_add_button) weibo_tracklist_buttons_layout.addWidget( self.weibo_tracklist_del_button) weibo_tracklist_buttons_layout.addStretch(20) weibo_tracklist_buttons_layout.addWidget( self.weibo_tracklist_conf_button) weibo_tracklist_buttons_layout.addWidget(self.weibo_analyze_button) weibo_tracklist_buttons_layout.setStretch(0, 6) weibo_tracklist_buttons_layout.setStretch(1, 6) weibo_tracklist_buttons_layout.setStretch(2, 6) weibo_tracklist_buttons_layout.setStretch(3, 6) self.weibo_sentiment_checkbox = QCheckBox("Comment Sentimnet") weibo_analyze_options = QWidget() weibo_analyze_options_layout = QHBoxLayout() weibo_analyze_options.setLayout(weibo_analyze_options_layout) weibo_analyze_options_layout.addWidget(self.weibo_sentiment_checkbox) self.weibo_tracklist_widget = QWidget() weibo_tracklist_layout = QVBoxLayout() self.weibo_tracklist_widget.setLayout(weibo_tracklist_layout) weibo_tracklist_layout.addWidget(self.weibo_tracklist_label) weibo_tracklist_layout.addWidget(self.weibo_tracklist) weibo_tracklist_layout.addWidget(weibo_tracklist_buttons) weibo_tracklist_layout.addWidget(weibo_analyze_options) def init_triggers(self): self.crawl_button.clicked.connect(self.crawl_button_clicked) self.topic_tracklist_add_button.clicked.connect( self.topic_add_button_clicked) self.topic_tracklist_del_button.clicked.connect( self.topic_del_button_clicked) self.topic_tracklist_conf_button.clicked.connect( self.topic_conf_button_clicked) self.topic_analyze_button.clicked.connect( self.topic_analyze_button_clicked) self.weibo_tracklist_add_button.clicked.connect( self.weibo_add_button_clicked) self.weibo_tracklist_del_button.clicked.connect( self.weibo_del_button_clicked) self.weibo_tracklist_conf_button.clicked.connect( self.weibo_conf_button_clicked) self.weibo_analyze_button.clicked.connect( self.weibo_analyze_button_clicked) @pyqtSlot() def crawl_button_clicked(self): rule_text = self.rule_edit.toPlainText().strip() if rule_text: try: rule = eval(rule_text) assert isinstance(rule, ReRule) except Exception as ex: QMessageBox.information(self, "Invalid Rule Syntax", '\n'.join(ex.args)) return self.automaton.set_rule(rule) if self.scheduler_status: self.log("Restart Crawlers", "INFO") else: self.scheduler_status = True self.crawl_button.setText("Restart Crawler") self.log("Start Crawlers", "INFO") self.scheduler.terminate() self.scheduler.wait() self.scheduler.set_weibo_handler(self.handle_weibo) self.scheduler.set_topic_handler(self.handle_topic) self.scheduler.start() @pyqtSlot() def topic_analyze_button_clicked(self): indexes = self.topic_tracklist.selectedIndexes() if not indexes: return index = indexes[0] topic_name = self.topic_tracklist_model.item(index.row()).text() if not topic_name: return topic = self.analyzer.get_topic(topic_name) if not topic: return topic_heat = self.analyzer.get_topic_heat(topic) self.fig.clear() ax = self.fig.add_subplot(111) times = list(map(lambda x: x[0], topic_heat)) heats = list(map(lambda x: x[1], topic_heat)) ax.plot(times, heats) # ax.set_title(topic_name, fontproperties=self.gfont) self.fig.autofmt_xdate() self.canvas.draw() @pyqtSlot() def weibo_analyze_button_clicked(self): indexes = self.weibo_tracklist.selectedIndexes() if not indexes: return index = indexes[0] weibo_id = self.weibo_tracklist_model.item(index.row()).text() if not weibo_id: return weibo_id = int(weibo_id) weibo = self.analyzer.get_weibo(weibo_id) if not weibo: return weibo_heat = self.analyzer.get_weibo_heat(weibo) self.fig.clear() ax = self.fig.add_subplot(111) times = list(map(lambda x: x[0], weibo_heat)) heats = list(map(lambda x: x[1], weibo_heat)) ax.plot(times, heats) topics = self.analyzer.get_weibo_topics(weibo_id) sentiment = self.analyzer.get_sentiment(weibo['text']) # ax.set_title('#'.join(topics) + "--Sentiment: {:.3f}".format(sentiment), fontproperties=self.gfont) self.fig.autofmt_xdate() self.canvas.draw() @pyqtSlot() def topic_add_button_clicked(self): item = QStandardItem("") self.topic_tracklist_model.appendRow(item) self.topic_tracklist.setCurrentIndex(item.index()) @pyqtSlot() def topic_del_button_clicked(self): index = self.topic_tracklist.selectedIndexes()[0] self.topic_tracklist_model.removeRow(index.row()) if self.topic_tracklist_model.rowCount(): self.topic_tracklist.setCurrentIndex( self.topic_tracklist_model.index(0, 0)) @pyqtSlot() def topic_conf_button_clicked(self): topics = [ self.topic_tracklist_model.item(idx).text() for idx in range(self.topic_tracklist_model.rowCount()) ] topic_tracklist = ','.join(topics) self.scheduler.config.set("TopicSpider", "topic_tracklist", topic_tracklist) with open("config.ini", "w") as fp: self.scheduler.config.write(fp) @pyqtSlot() def weibo_add_button_clicked(self): item = QStandardItem("") self.weibo_tracklist_model.appendRow(item) self.weibo_tracklist.setCurrentIndex(item.index()) @pyqtSlot() def weibo_del_button_clicked(self): index = self.weibo_tracklist.selectedIndexes()[0] self.weibo_tracklist_model.removeRow(index.row()) if self.weibo_tracklist_model.rowCount(): self.weibo_tracklist.setCurrentIndex( self.weibo_tracklist_model.index(0, 0)) @pyqtSlot() def weibo_conf_button_clicked(self): weibo_ids = [ self.weibo_tracklist_model.item(idx).text() for idx in range(self.weibo_tracklist_model.rowCount()) ] weibo_tracklist = ','.join(weibo_ids) self.scheduler.config.set("WeiboSpider", "weibo_id_tracklist", weibo_tracklist) with open("config.ini", "w") as fp: self.scheduler.config.write(fp) def handle_weibo(self, weibo): self.dbconn.process_weibo(weibo) if self.automaton.match(weibo.text): msg = "Matched Weibo {}: {}".format(weibo.weibo_id, repr(weibo.text)) self.log(msg, "WARNING") self.handle_matched_weibo(weibo) else: msg = "Process Weibo {}".format(weibo.weibo_id) self.log(msg, "NORMAL") def handle_matched_weibo(self, weibo): reason = '匹配敏感关键字规则\n' + self.rule_edit.toPlainText() if self.accuser.accuse_weibo(weibo, reason): msg = "Accuse weibo {}".format(weibo.weibo_id) self.log(msg, "INFO") def handle_topic(self, topic): self.dbconn.process_topic(topic) msg = "Process Topic {}".format(repr(topic.name)) self.log(msg, "NORMAL") def log(self, msg, status_str="INFO"): time_str = datetime.now().strftime("%Y-%m-%d-%H:%M:%S") self.log_model.appendRow([ QStandardItem(status_str), QStandardItem(time_str), QStandardItem(msg) ])
class MatchViewGroup(QGroupBox): def __init__(self, ui_template): super(MatchViewGroup, self).__init__() """ Import settings """ self.ui_template = ui_template self.settings = import_settings() self.current_match = Match() self.setTitle('Match View') """ Layout """ self.grid_layout = QGridLayout() self.setLayout(self.grid_layout) self.grid_layout.setColumnStretch(1, 1) self.grid_layout.setColumnStretch(2, 1) self.grid_layout.setColumnStretch(3, 1) self.grid_layout.setRowStretch(1, 1) """ Rounds list """ self.rounds_list = QListView() self.rounds_list_model = QStandardItemModel() self.rounds_list.setModel(self.rounds_list_model) self.rounds_list.setEditTriggers(QAbstractItemView.NoEditTriggers) self.grid_layout.addWidget(self.rounds_list, 0, 0, 2, 1) """ End game state info """ self.end_game_state_info_group = QGroupBox() self.end_game_state_info_layout = QVBoxLayout() self.end_game_state_info_group.setLayout( self.end_game_state_info_layout) self.end_game_state_info_group.setTitle('Results') self.grid_layout.addWidget(self.end_game_state_info_group, 2, 0, 2, 1) # Results self.match_seed_label = QLabel() self.end_game_state_info_layout.addWidget(self.match_seed_label) self.match_winner_label = QLabel() self.end_game_state_info_layout.addWidget(self.match_winner_label) self.player_A_results_label = QLabel() self.end_game_state_info_layout.addWidget(self.player_A_results_label) self.player_B_results_label = QLabel() self.end_game_state_info_layout.addWidget(self.player_B_results_label) self.referee_messages_label = QLabel() self.referee_messages_label.setText('Referee Messages') self.end_game_state_info_layout.addWidget(self.referee_messages_label) self.referee_messages_text = QTextBrowser() self.end_game_state_info_layout.addWidget(self.referee_messages_text) # Map view self.map_view = QGraphicsView() self.map_scene = QGraphicsScene() self.map_view.setScene(self.map_scene) self.grid_layout.addWidget(self.map_view, 1, 1, 2, 2) # Add groups and field labels to the match view self.groups = {} self.populate_fields(self.ui_template, self.grid_layout) def populate_fields(self, template, layout, i=0): groups = template['groups'] for group_ in groups: group = QGroupBox() group.setTitle(group_['title']) layout_ = QGridLayout() layout_.setSpacing(0) group.setLayout(layout_) self.groups[group_['name']] = group coordinates = [i, 0] span = [1, 1] if 'row' in group_ and 'col' in group_: coordinates = [group_['row'], group_['col']] if 'colSpan' in group_ or 'rowSpan' in group_: span = [group_.get('rowSpan', 1), group_.get('colSpan', 1)] layout.addWidget(group, *coordinates, *span) group.fields = {} if 'fields' in group_: for index, field_ in enumerate(group_['fields']): if field_['name'] == 'worm': field = QLabel() field.setPixmap(QPixmap(field_['resource'])) layout_.addWidget(field, index - 1, 0) else: field_label = QLabel() field_label.setText(field_['label']) field = QLabel() if field_['label'] != '': group.fields[field_['name']] = field layout_.addWidget(field_label, index, 0) layout_.addWidget(field, index, 1) else: group.fields[field_['name']] = field layout_.addWidget(field, index, 1) if 'groups' in group_: self.populate_fields(group_, layout_, index + 1) def match_selected(self, *args): match = args[0].indexes()[0].data() match_logs_directory = self.settings.value('match_logs_directory') directory = os.path.join(match_logs_directory, match) self.map_scene.clear() if os.path.isdir(directory): self.load_match(directory) def round_selected(self, *args): index_array = args[0].indexes() if len(index_array): index = index_array[0] self.current_match.set_current_round(index.data()) self.update_fields() self.draw_map() def draw_map(self): cells = self.current_match.current_round.state['map'] self.map_scene.clear() self.map_scene.setSceneRect( 0, 0, self.current_match.state['mapSize'] * PIXEL_SIZE, self.current_match.state['mapSize'] * PIXEL_SIZE) for row in cells: for cell in row: pic = QPixmap(IMAGE[cell['type']]) item = self.map_scene.addPixmap( pic.scaled(PIXEL_SIZE, PIXEL_SIZE)) item.setPos(cell['x'] * PIXEL_SIZE, cell['y'] * PIXEL_SIZE) if 'occupier' in cell: if cell['occupier']['playerId'] == 1: pic = QPixmap(':resources/icons/map/wormA.png') else: pic = QPixmap(':resources/icons/map/wormB.png') item = self.map_scene.addPixmap( pic.scaled(PIXEL_SIZE, PIXEL_SIZE)) item.setPos(cell['x'] * PIXEL_SIZE, cell['y'] * PIXEL_SIZE) health_label = QLabel() font = QFont() font.setBold(True) health_label.setFont(font) health_label.setText( str(cell['occupier']['id']) + ' - ' + str(cell['occupier']['health'])) item = self.map_scene.addWidget(health_label) item.setPos((cell['x'] - 0.5) * PIXEL_SIZE, (cell['y'] - 1) * PIXEL_SIZE) if 'powerup' in cell: pic = QPixmap(IMAGE[cell['powerup']['type']]) item = self.map_scene.addPixmap( pic.scaled(PIXEL_SIZE, PIXEL_SIZE)) item.setPos(cell['x'] * PIXEL_SIZE, cell['y'] * PIXEL_SIZE) def load_match(self, directory): self.current_match = Match(directory) self.list_rounds() self.update_end_game_state() self.rounds_list.setCurrentIndex(self.rounds_list_model.index(0, 0)) def update_end_game_state(self): results = self.current_match.end_game_state self.match_seed_label.setText(results['seed']) self.match_winner_label.setText(results['winner']) self.player_A_results_label.setText(results['playerA']) self.player_B_results_label.setText(results['playerB']) self.referee_messages_text.setText(results['referee']) def list_rounds(self): match_directory = self.current_match.match_directory model = self.rounds_list_model model.clear() rounds = [ round_ for round_ in os.listdir(match_directory) if 'Round' in round_ and os.path.isdir(os.path.join(match_directory)) ] for round_ in rounds: item = QStandardItem(round_) model.appendRow(item) def update_fields(self): for group_key, group in self.groups.items(): if group_key == 'match_info_group': state = self.current_match.state self.update_labels(group, state) if 'player_A_move_info' in group_key: temp = [ value for key, value in self.current_match.current_round.player_command.items() if 'A -' in key ] self.update_labels(group, temp[0]) if 'player_B_move_info' in group_key: temp = [ value for key, value in self.current_match.current_round.player_command.items() if 'B -' in key ] self.update_labels(group, temp[0]) if group_key == 'player_A_info_group': temp = [ value for key, value in self.current_match.current_round.state.items() if 'A - ' in key ] self.update_labels(group, temp[0]) if group_key == 'player_B_info_group': temp = [ value for key, value in self.current_match.current_round.state.items() if 'B - ' in key ] self.update_labels(group, temp[0]) if group_key == 'player_A_worms_info_group': players = [player for player in self.current_match.players] self.populate_worms_info(group, players[0]) if group_key == 'player_B_worms_info_group': self.populate_worms_info(group, players[1]) @staticmethod def update_labels(group, state): for field_key, field in group.fields.items(): field.setText(str(state[field_key])) if field_key == 'colour': field.setIcon() def populate_worms_info(self, group, player): # Clears the layout before re-adding worms info for i in reversed(range(group.layout().count())): group.layout().itemAt(i).widget().setParent(None) worms = self.current_match.current_round.state[player]['worms'] for worm in worms: wormGroup = QGroupBox() wormGroup_layout = QGridLayout() wormGroup_layout.setSpacing(0) wormGroup.setLayout(wormGroup_layout) group.layout().addWidget(wormGroup) for index, (key, value) in enumerate(worm.items()): label_ = QLabel() label_.setText(key + ': ') label = QLabel() label.setText(str(value)) wormGroup_layout.addWidget(label_, index, 0) wormGroup_layout.addWidget(label, index, 1)
class DesktopIconWidget(QFrame): def __init__(self, parent): QFrame.__init__(self, parent) self.setFrameStyle(QFrame.Box | QFrame.Sunken) self.setStyleSheet("QListView{background:transparent;}") self.listView = QListView(self) self.setLayout(QHBoxLayout()) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().addWidget(self.listView) self.listView.setContextMenuPolicy(Qt.CustomContextMenu) self.listView.setEditTriggers(QAbstractItemView.NoEditTriggers) self.listView.setMovement(QListView.Snap) self.listView.setFlow(QListView.LeftToRight) self.listView.setResizeMode(QListView.Adjust) self.listView.setGridSize(QSize(self.logicalDpiX() / 96 * 70, self.logicalDpiY() / 96 * 70)) self.listView.setViewMode(QListView.IconMode) self.quickDesktopModel = QuickDesktopModel(self.window().platform.databaseFile) self.listView.setModel(self.quickDesktopModel) self.createActions() self.makeConnections() def createActions(self): self.actionCreateComputer = QAction(self.tr("我的电脑(&C)"), self) self.actionCreateDocuments = QAction(self.tr("我的文档(&D)"), self) self.actionCreateMusic = QAction(self.tr("我的音乐(&M)"), self) self.actionCreatePictures = QAction(self.tr("我的图片(&P)"), self) self.actionCreateShortcut = QAction(self.tr("创建快捷方式(&C)"), self) self.actionCreateShortcut.setIcon(QIcon(":/images/new.png")) self.actionCreateBookmark = QAction(self.tr("创建网络链接(&B)"), self) self.actionCreateBookmark.setIcon(QIcon(":/images/insert-link.png")) self.actionRemoveShortcut = QAction(self.tr("删除快捷方式(&R)"), self) self.actionRemoveShortcut.setIcon(QIcon(":/images/delete.png")) self.actionRenameShortcut = QAction(self.tr("重命名(&N)"), self) self.actionRenameShortcut.setIcon(QIcon(":/images/edit-rename.png")) self.actionEditShortcut = QAction(self.tr("编辑快捷方式(&E)"), self) self.actionEditShortcut.setIcon(QIcon(":/images/edit.png")) def makeConnections(self): self.listView.customContextMenuRequested.connect(self.onQuickDesktopContextMenuRequest) self.listView.activated.connect(self.runQuickDesktopShortcut) self.actionCreateComputer.triggered.connect(self.createComputerShortcut) self.actionCreateDocuments.triggered.connect(self.createDocumentsShortcut) self.actionCreateMusic.triggered.connect(self.createMusicShortcut) self.actionCreatePictures.triggered.connect(self.createPicturesShortcut) self.actionCreateShortcut.triggered.connect(self.createShortcut) self.actionCreateBookmark.triggered.connect(self.createBookmark) self.actionEditShortcut.triggered.connect(self.editShortcut) self.actionRemoveShortcut.triggered.connect(self.removeShortcut) self.actionRenameShortcut.triggered.connect(self.renameShortcut) def onQuickDesktopContextMenuRequest(self, pos): index = self.listView.indexAt(pos) self.listView.setCurrentIndex(index) menu = QMenu() menu.addAction(self.actionCreateShortcut) menu.addAction(self.actionCreateBookmark) menu2 = menu.addMenu(self.tr("创建特殊快捷方式(&S)")) if os.name == "nt": menu2.addAction(self.actionCreateComputer) menu2.addAction(self.actionCreateDocuments) menu2.addAction(self.actionCreatePictures) menu2.addAction(self.actionCreateMusic) if index.isValid(): menu.addAction(self.actionRemoveShortcut) if not self.quickDesktopModel.isSpecialShortcut(index): menu.addAction(self.actionEditShortcut) menu.addAction(self.actionRenameShortcut) try: getattr(menu, "exec")(QCursor.pos()) except AttributeError: getattr(menu, "exec_")(QCursor.pos()) def createShortcut(self): d = ShortcutDialog(self) if self.window().runDialog(d.create) == QDialog.Accepted: shortcut = d.getResult() shortcut["id"] = str(uuid.uuid4()) self.quickDesktopModel.addShortcut(shortcut) d.deleteLater() def createBookmark(self): d = BookmarkDialog(self) if self.window().runDialog(d.create) == QDialog.Accepted: shortcut = { "id": str(uuid.uuid4()), "icon": "", "openwith": "", "dir": "", } shortcut.update(d.getResult()) self.quickDesktopModel.addShortcut(shortcut) d.deleteLater() def createComputerShortcut(self): shortcut = { "id": str(uuid.uuid4()), "name": self.tr("我的电脑"), "path": COMPUTER_PATH, "icon": "", "dir": "", "openwith": "", } self.quickDesktopModel.addShortcut(shortcut) def createDocumentsShortcut(self): shortcut = { "id": str(uuid.uuid4()), "name": self.tr("我的文档"), "path": DOCUMENTS_PATH, "icon": "", "dir": "", "openwith": "", } self.quickDesktopModel.addShortcut(shortcut) def createPicturesShortcut(self): shortcut = { "id": str(uuid.uuid4()), "name": self.tr("图片收藏"), "path": PICTURES_PATH, "icon": "", "dir": "", "openwith": "", } self.quickDesktopModel.addShortcut(shortcut) def createMusicShortcut(self): shortcut = { "id": str(uuid.uuid4()), "name": self.tr("我的音乐"), "path": MUSIC_PATH, "icon": "", "dir": "", "openwith": "", } self.quickDesktopModel.addShortcut(shortcut) def renameShortcut(self): self.listView.edit(self.listView.currentIndex()) def removeShortcut(self): self.quickDesktopModel.removeShortcut(self.listView.currentIndex()) def editShortcut(self): index = self.listView.currentIndex() if not index.isValid(): return shortcut = self.quickDesktopModel.shortcutAt(index) url = QUrl.fromUserInput(shortcut["path"]) if not url.isValid(): return if url.scheme() == "special": QMessageBox.information(self, self.tr("编辑快捷方式"), self.tr("不能编辑特殊图标。")) return elif url.scheme() == "file": d = ShortcutDialog(self) else: d = BookmarkDialog(self) if self.window().runDialog(d.edit, shortcut) == QDialog.Accepted: shortcut.update(d.getResult()) self.quickDesktopModel.updateShortcut(shortcut, index) d.deleteLater() def runQuickDesktopShortcut(self): index = self.listView.currentIndex() if not index.isValid(): return if not self.quickDesktopModel.runShortcut(index): QMessageBox.information(self, self.tr("快捷面板"), \ self.tr("不能运行快捷方式。请检查文件是否存在或者程序是否正确。")) else: self.window().close()
class MainWindow(QMainWindow): """MNELAB main window. """ def __init__(self): super().__init__() self.MAX_RECENT = 6 # maximum number of recent files self.SUPPORTED_FORMATS = "*.bdf *.edf" self.all = DataSets() # contains currently loaded data sets self.history = [] # command history settings = self._read_settings() self.recent = settings["recent"] # list of recent files if settings["geometry"]: self.restoreGeometry(settings["geometry"]) else: self.setGeometry(300, 300, 1000, 750) # default window size self.move(QApplication.desktop().screen().rect().center() - self.rect().center()) # center window if settings["state"]: self.restoreState(settings["state"]) self.setWindowTitle("MNELAB") menubar = self.menuBar() file_menu = menubar.addMenu("&File") file_menu.addAction("&Open...", self.open_file, QKeySequence.Open) self.recent_menu = file_menu.addMenu("Open recent") self.recent_menu.aboutToShow.connect(self._update_recent_menu) self.recent_menu.triggered.connect(self._load_recent) if not self.recent: self.recent_menu.setEnabled(False) self.close_file_action = file_menu.addAction("&Close", self.close_file, QKeySequence.Close) self.close_all_action = file_menu.addAction("Close all", self.close_all) file_menu.addSeparator() self.import_bad_action = file_menu.addAction("Import bad channels...", self.import_bads) self.export_bad_action = file_menu.addAction("Export &bad channels...", self.export_bads) file_menu.addSeparator() file_menu.addAction("&Quit", self.close, QKeySequence.Quit) edit_menu = menubar.addMenu("&Edit") self.pick_chans_action = edit_menu.addAction("Pick &channels...", self.pick_channels) self.set_bads_action = edit_menu.addAction("&Bad channels...", self.set_bads) edit_menu.addSeparator() self.setref_action = edit_menu.addAction("&Set reference...", self.set_reference) plot_menu = menubar.addMenu("&Plot") self.plot_raw_action = plot_menu.addAction("&Raw data", self.plot_raw) self.plot_psd_action = plot_menu.addAction("&Power spectral " "density...", self.plot_psd) tools_menu = menubar.addMenu("&Tools") self.filter_action = tools_menu.addAction("&Filter data...", self.filter_data) self.find_events_action = tools_menu.addAction("Find &events...", self.find_events) self.run_ica_action = tools_menu.addAction("Run &ICA...") self.import_ica_action = tools_menu.addAction("&Load ICA...", self.load_ica) view_menu = menubar.addMenu("&View") statusbar_action = view_menu.addAction("Statusbar", self._toggle_statusbar) statusbar_action.setCheckable(True) help_menu = menubar.addMenu("&Help") help_menu.addAction("&About", self.show_about) help_menu.addAction("About &Qt", self.show_about_qt) self.names = QStringListModel() self.names.dataChanged.connect(self._update_names) splitter = QSplitter() self.sidebar = QListView() self.sidebar.setFrameStyle(QFrame.NoFrame) self.sidebar.setFocusPolicy(Qt.NoFocus) self.sidebar.setModel(self.names) self.sidebar.clicked.connect(self._update_data) splitter.addWidget(self.sidebar) self.infowidget = InfoWidget() splitter.addWidget(self.infowidget) width = splitter.size().width() splitter.setSizes((width * 0.3, width * 0.7)) self.setCentralWidget(splitter) self.status_label = QLabel() self.statusBar().addPermanentWidget(self.status_label) if settings["statusbar"]: self.statusBar().show() statusbar_action.setChecked(True) else: self.statusBar().hide() statusbar_action.setChecked(False) self.setAcceptDrops(True) self._toggle_actions(False) self.show() def open_file(self): """Show open file dialog. """ fname = QFileDialog.getOpenFileName(self, "Open file", filter=self.SUPPORTED_FORMATS)[0] if fname: self.load_file(fname) def load_file(self, fname): """Load file. Parameters ---------- fname : str File name. """ # TODO: check if fname exists raw = mne.io.read_raw_edf(fname, stim_channel=-1, preload=True) name, ext = splitext(split(fname)[-1]) self.history.append("raw = mne.io.read_raw_edf('{}', " "stim_channel=None, preload=True)".format(fname)) self.all.insert_data(DataSet(name=name, fname=fname, ftype=ext[1:].upper(), raw=raw)) self.find_events() self._update_sidebar(self.all.names, self.all.index) self._update_infowidget() self._update_statusbar() self._add_recent(fname) self._toggle_actions() def export_bads(self): """Export bad channels info to a CSV file. """ fname = QFileDialog.getSaveFileName(self, "Export bad channels", filter="*.csv")[0] if fname: name, ext = splitext(split(fname)[-1]) ext = ext if ext else ".csv" # automatically add extension fname = join(split(fname)[0], name + ext) with open(fname, "w") as f: f.write(",".join(self.all.current.raw.info["bads"])) def import_bads(self): """Import bad channels info from a CSV file. """ fname = QFileDialog.getOpenFileName(self, "Import bad channels", filter="*.csv")[0] if fname: with open(fname) as f: bads = f.read().replace(" ", "").split(",") if set(bads) - set(self.all.current.raw.info["ch_names"]): QMessageBox.critical(self, "Channel labels not found", "Some channel labels from the file " "are not present in the data.") else: self.all.current.raw.info["bads"] = bads self.all.data[self.all.index].raw.info["bads"] = bads def close_file(self): """Close current file. """ self.all.remove_data() self._update_sidebar(self.all.names, self.all.index) self._update_infowidget() self._update_statusbar() if not self.all: self._toggle_actions(False) def close_all(self): """Close all currently open data sets. """ msg = QMessageBox.question(self, "Close all data sets", "Close all data sets?") if msg == QMessageBox.Yes: while self.all: self.close_file() def get_info(self): """Get basic information on current file. Returns ------- info : dict Dictionary with information on current file. """ raw = self.all.current.raw fname = self.all.current.fname ftype = self.all.current.ftype reference = self.all.current.reference events = self.all.current.events nchan = raw.info["nchan"] chans = Counter([channel_type(raw.info, i) for i in range(nchan)]) if events is not None: nevents = events.shape[0] unique = [str(e) for e in set(events[:, 2])] events = "{} ({})".format(nevents, ", ".join(unique)) else: events = "-" if isinstance(reference, list): reference = ",".join(reference) if raw.annotations is not None: annots = len(raw.annotations.description) else: annots = "-" return {"File name": fname if fname else "-", "File type": ftype if ftype else "-", "Number of channels": nchan, "Channels": ", ".join( [" ".join([str(v), k.upper()]) for k, v in chans.items()]), "Samples": raw.n_times, "Sampling frequency": str(raw.info["sfreq"]) + " Hz", "Length": str(raw.n_times / raw.info["sfreq"]) + " s", "Events": events, "Annotations": annots, "Reference": reference if reference else "-", "Size in memory": "{:.2f} MB".format( raw._data.nbytes / 1024 ** 2), "Size on disk": "-" if not fname else "{:.2f} MB".format( getsize(fname) / 1024 ** 2)} def pick_channels(self): """Pick channels in current data set. """ channels = self.all.current.raw.info["ch_names"] dialog = PickChannelsDialog(self, channels) if dialog.exec_(): picks = [item.data(0) for item in dialog.channels.selectedItems()] drops = set(channels) - set(picks) tmp = self.all.current.raw.drop_channels(drops) name = self.all.current.name + " (channels dropped)" new = DataSet(raw=tmp, name=name, events=self.all.current.events) self.history.append("raw.drop({})".format(drops)) self._update_datasets(new) def set_bads(self): """Set bad channels. """ channels = self.all.current.raw.info["ch_names"] selected = self.all.current.raw.info["bads"] dialog = PickChannelsDialog(self, channels, selected, "Bad channels") if dialog.exec_(): bads = [item.data(0) for item in dialog.channels.selectedItems()] self.all.current.raw.info["bads"] = bads self.all.data[self.all.index].raw.info["bads"] = bads self._toggle_actions(True) def plot_raw(self): """Plot raw data. """ events = self.all.current.events nchan = self.all.current.raw.info["nchan"] fig = self.all.current.raw.plot(events=events, n_channels=nchan, title=self.all.current.name, show=False) self.history.append("raw.plot(n_channels={})".format(nchan)) win = fig.canvas.manager.window win.setWindowTitle("Raw data") win.findChild(QStatusBar).hide() win.installEventFilter(self) # detect if the figure is closed # prevent closing the window with the escape key try: key_events = fig.canvas.callbacks.callbacks["key_press_event"][8] except KeyError: pass else: # this requires MNE >=0.15 key_events.func.keywords["params"]["close_key"] = None fig.show() def plot_psd(self): """Plot power spectral density (PSD). """ fig = self.all.current.raw.plot_psd(average=False, spatial_colors=False, show=False) win = fig.canvas.manager.window win.setWindowTitle("Power spectral density") fig.show() def load_ica(self): """Load ICA solution from a file. """ fname = QFileDialog.getOpenFileName(self, "Load ICA", filter="*.fif *.fif.gz") if fname[0]: self.state.ica = mne.preprocessing.read_ica(fname[0]) def find_events(self): events = mne.find_events(self.all.current.raw, consecutive=False) if events.shape[0] > 0: # if events were found self.all.current.events = events self.all.data[self.all.index].events = events self._update_infowidget() def filter_data(self): """Filter data. """ dialog = FilterDialog(self) if dialog.exec_(): low, high = dialog.low, dialog.high tmp = filter_data(self.all.current.raw._data, self.all.current.raw.info["sfreq"], l_freq=low, h_freq=high) name = self.all.current.name + " ({}-{} Hz)".format(low, high) new = DataSet(raw=mne.io.RawArray(tmp, self.all.current.raw.info), name=name, events=self.all.current.events) self.history.append("raw.filter({}, {})".format(low, high)) self._update_datasets(new) def set_reference(self): """Set reference. """ dialog = ReferenceDialog(self) if dialog.exec_(): if dialog.average.isChecked(): tmp, _ = mne.set_eeg_reference(self.all.current.raw, None) tmp.apply_proj() name = self.all.current.name + " (average ref)" new = DataSet(raw=tmp, name=name, reference="average", events=self.all.current.events) else: ref = [c.strip() for c in dialog.channellist.text().split(",")] refstr = ",".join(ref) if set(ref) - set(self.all.current.raw.info["ch_names"]): # add new reference channel(s) to data try: tmp = mne.add_reference_channels(self.all.current.raw, ref) except RuntimeError: QMessageBox.critical(self, "Cannot add new channels", "Cannot add new channels to " "average referenced data.") return else: # re-reference to existing channel(s) tmp, _ = mne.set_eeg_reference(self.all.current.raw, ref) name = self.all.current.name + " (ref {})".format(refstr) new = DataSet(raw=tmp, name=name, reference=refstr, events=self.all.current.events) self._update_datasets(new) def show_about(self): """Show About dialog. """ msg = """<b>MNELAB {}</b><br/><br/> <a href="https://github.com/cbrnr/mnelab">MNELAB</a> - a graphical user interface for <a href="https://github.com/mne-tools/mne-python">MNE</a>.<br/><br/> This program uses MNE version {}.<br/><br/> Licensed under the BSD 3-clause license.<br/> Copyright 2017 by Clemens Brunner.""".format(__version__, mne.__version__) QMessageBox.about(self, "About MNELAB", msg) def show_about_qt(self): """Show About Qt dialog. """ QMessageBox.aboutQt(self, "About Qt") def _update_datasets(self, dataset): # if current data is stored in a file create a new data set if self.all.current.fname: self.all.insert_data(dataset) # otherwise ask if the current data set should be overwritten or if a # new data set should be created else: msg = QMessageBox.question(self, "Overwrite existing data set", "Overwrite existing data set?") if msg == QMessageBox.No: # create new data set self.all.insert_data(dataset) else: # overwrite existing data set self.all.update_data(dataset) self._update_sidebar(self.all.names, self.all.index) self._update_infowidget() self._update_statusbar() def _update_sidebar(self, names, index): """Update (overwrite) sidebar with names and current index. """ self.names.setStringList(names) self.sidebar.setCurrentIndex(self.names.index(index)) def _update_infowidget(self): if self.all: self.infowidget.set_values(self.get_info()) else: self.infowidget.clear() def _update_statusbar(self): if self.all: mb = self.all.nbytes / 1024 ** 2 self.status_label.setText("Total Memory: {:.2f} MB".format(mb)) else: self.status_label.clear() def _toggle_actions(self, enabled=True): """Toggle actions. Parameters ---------- enabled : bool Specifies whether actions are enabled (True) or disabled (False). """ self.close_file_action.setEnabled(enabled) self.close_all_action.setEnabled(enabled) if self.all.data: bads = bool(self.all.current.raw.info["bads"]) self.export_bad_action.setEnabled(enabled and bads) else: self.export_bad_action.setEnabled(enabled) self.import_bad_action.setEnabled(enabled) self.pick_chans_action.setEnabled(enabled) self.set_bads_action.setEnabled(enabled) self.plot_raw_action.setEnabled(enabled) self.plot_psd_action.setEnabled(enabled) self.filter_action.setEnabled(enabled) self.setref_action.setEnabled(enabled) self.find_events_action.setEnabled(enabled) self.run_ica_action.setEnabled(enabled) self.import_ica_action.setEnabled(enabled) def _add_recent(self, fname): """Add a file to recent file list. Parameters ---------- fname : str File name. """ if fname in self.recent: # avoid duplicates self.recent.remove(fname) self.recent.insert(0, fname) while len(self.recent) > self.MAX_RECENT: # prune list self.recent.pop() self._write_settings() if not self.recent_menu.isEnabled(): self.recent_menu.setEnabled(True) def _write_settings(self): """Write application settings. """ settings = QSettings() if self.recent: settings.setValue("recent", self.recent) settings.setValue("statusbar", not self.statusBar().isHidden()) settings.setValue("geometry", self.saveGeometry()) settings.setValue("state", self.saveState()) def _read_settings(self): """Read application settings. Returns ------- settings : dict The restored settings values are returned in a dictionary for further processing. """ settings = QSettings() recent = settings.value("recent") if not recent: recent = [] # default is empty list statusbar = settings.value("statusbar") if statusbar is None: # default is True statusbar = True geometry = settings.value("geometry") state = settings.value("state") return {"recent": recent, "statusbar": statusbar, "geometry": geometry, "state": state} @pyqtSlot(QModelIndex) def _update_data(self, selected): """Update index and information based on the state of the sidebar. Parameters ---------- selected : QModelIndex Index of the selected row. """ if selected.row() != self.all.index: self.all.index = selected.row() self.all.update_current() self._update_infowidget() @pyqtSlot(QModelIndex, QModelIndex) def _update_names(self, start, stop): """Update names in DataSets after changes in sidebar. """ for index in range(start.row(), stop.row() + 1): self.all.data[index].name = self.names.stringList()[index] if self.all.index in range(start.row(), stop.row() + 1): self.all.current.name = self.all.names[self.all.index] @pyqtSlot() def _update_recent_menu(self): self.recent_menu.clear() for recent in self.recent: self.recent_menu.addAction(recent) @pyqtSlot(QAction) def _load_recent(self, action): self.load_file(action.text()) @pyqtSlot() def _toggle_statusbar(self): if self.statusBar().isHidden(): self.statusBar().show() else: self.statusBar().hide() self._write_settings() @pyqtSlot(QDropEvent) def dragEnterEvent(self, event): if event.mimeData().hasUrls(): event.acceptProposedAction() @pyqtSlot(QDropEvent) def dropEvent(self, event): mime = event.mimeData() if mime.hasUrls(): urls = mime.urls() for url in urls: self.load_file(url.toLocalFile()) @pyqtSlot(QEvent) def closeEvent(self, event): """Close application. Parameters ---------- event : QEvent Close event. """ self._write_settings() if self.history: print("\nCommand History") print("===============") print("\n".join(self.history)) QApplication.quit() def eventFilter(self, source, event): # currently the only source is the raw plot window if event.type() == QEvent.Close: self._update_infowidget() return QObject.eventFilter(self, source, event)
class AppWindow(QMainWindow): def __init__(self, parent=None): QMainWindow.__init__(self, parent) self.setGeometry(100, 100, 550, 400) self.setUpMenu() self.setUpLayout() self.checkBoxConnect.stateChanged.connect(self.connect_disconnect) self.addButton.clicked.connect(self.add_contact) self.deleteButton.clicked.connect(self.delete_contact) self.sendButton.clicked.connect(self.send_message) self.contactsList.clicked.connect(self.select_contact) # self.messageField.drop.connect(self.add_file_to_message) # model -controller - view self.contact_list_model = StandardItemModelContacts(self) self.messages_list_model = StandardItemModelMessages(self) self.model = ClientModel(self.contact_list_model, self.messages_list_model) self.contactsList.setModel(self.contact_list_model) self.messagesList.setModel(self.messages_list_model) self.controller = ClientController(self.model) # signal-slot self.controller.auth_recieved.connect(self.authentification_recieved) self.controller.gotPersonal.connect(self.new_personal_message) # not connected self.connected = False # nobody selected self.currently_selected = '' def setUpMenu(self): fileMenu = self.menuBar().addMenu('File') quitAct = QAction('Quit', self) quitAct.triggered.connect(self.doQuit) fileMenu.addAction(quitAct) sendMenu = self.menuBar().addMenu('Send') sendFile = QAction('Send File', self) sendFile.triggered.connect(self.doSendFile) sendMenu.addAction(sendFile) def setUpLayout(self): hBoxLayout = QHBoxLayout() mainFrame = QFrame() mainFrame.setLayout(hBoxLayout) self.contact = QLineEdit() self.contact.setFont(QFont('Calibri', 14)) self.contact.setPlaceholderText('Add Contact Here') self.contact.setMinimumHeight(30) self.contact.setMaximumHeight(30) self.addButton = QPushButton('+') self.addButton.setMinimumHeight(30) self.addButton.setMaximumHeight(30) self.addButton.setMinimumWidth(30) self.addButton.setMaximumWidth(30) self.deleteButton = QPushButton('-') self.deleteButton.setMinimumHeight(30) self.deleteButton.setMaximumHeight(30) self.deleteButton.setMinimumWidth(30) self.deleteButton.setMaximumWidth(30) self.contactsList = QListView() self.contactsList.setFrameShape(QFrame.Box) spacer = QFrame() spacer.setMaximumWidth(5) self.nameLabel = QLabel('Username') self.nameLabel.setMinimumHeight(30) self.nameLabel.setMaximumHeight(30) self.nameLabel.setFont(QFont('Calibri', 14)) self.checkBoxConnect = QCheckBox() self.checkBoxConnect.setText('Connect') self.checkBoxConnect.setChecked(False) self.checkBoxConnect.setMaximumHeight(30) self.checkBoxConnect.setMinimumHeight(30) self.checkBoxConnect.setMaximumWidth(70) self.checkBoxConnect.setMinimumWidth(70) self.messagesList = QListView() self.messagesList.setIconSize(QSize(30, 30)) self.messagesList.setFrameShape(QFrame.Box) self.messageField = MessageField() self.messageField.setMinimumHeight(50) self.messageField.setMaximumHeight(50) self.sendButton = QPushButton() self.sendButton.setText('>>') self.sendButton.setMinimumHeight(50) self.sendButton.setMaximumHeight(50) self.sendButton.setMinimumWidth(50) self.sendButton.setMaximumWidth(50) contBox = QGridLayout() contBox.addWidget(self.contact, 0, 0) contBox.addWidget(self.addButton, 0, 1) contBox.addWidget(self.deleteButton, 0, 2) contBox.addWidget(self.contactsList, 1, 0, 2, 3) contBox.addWidget(spacer, 0, 3, 3, 1) contBox.addWidget(self.nameLabel, 0, 4) contBox.addWidget(self.checkBoxConnect, 0, 5, 1, 2) contBox.addWidget(self.messagesList, 1, 4, 1, 3) contBox.addWidget(self.messageField, 2, 4, 1, 2) contBox.addWidget(self.sendButton, 2, 6) contBox.setColumnStretch(0, 3) contBox.setColumnStretch(4, 6) groupBoxCont = QFrame() groupBoxCont.setLayout(contBox) self.setCentralWidget(groupBoxCont) def doQuit(self): self.close() def doSendFile(self): filename = QFileDialog.getOpenFileName(self, 'Open file', '/home')[0] self.messageField.file = filename self.messageField.appendPlainText(filename) def connect_disconnect(self): self.checkBoxConnect.setEnabled(False) if self.checkBoxConnect.isChecked(): self.authentification() else: self.user_disconnect() self.checkBoxConnect.setEnabled(True) # Log in dialog def authentification(self): #read config file d = conf.read_conf() #show Authorization dialog auth_dialog = AuthDialog(self.model, d['username'], d['hostname'], d['port'], parent=self) auth_dialog.accepted.connect(self.auth_dialog_accepted) auth_dialog.rejected.connect(self.auth_dialog_rejected) auth_dialog.show() def auth_dialog_accepted(self): self.checkBoxConnect.setEnabled(True) self.controller.run_client() def auth_dialog_rejected(self): self.checkBoxConnect.setChecked(False) self.checkBoxConnect.setEnabled(True) @pyqtSlot(bool) def authentification_recieved(self, ok): if ok: self.connected = True self.checkBoxConnect.setText('Connected') self.nameLabel.setText('{}:'.format(self.model.username)) self.model.contactlist.clear() self.model.personal_messages = [] self.model.server_messages = [] self.model.sent_messages = [] self.model.open_db() self.controller.ask_contact_list() else: self.checkBoxConnect.setChecked(False) def user_disconnect(self): if self.connected: conf.save_conf(username=self.model.username, hostname=self.model.host, port=str(self.model.port)) self.model.clear_data() self.controller.user_disconnect() self.contact.setText('') self.nameLabel.setText('') self.checkBoxConnect.setText('Connect') self.connected = False self.currently_selected = '' @pyqtSlot(str) def get_contact_list(self): pass def add_contact(self): name = self.contact.text() if name != '': self.controller.add_contact(name) self.contact.setText('') def delete_contact(self): name = self.contact.text() if name != '': self.controller.delete_contact(name) self.contact.setText('') @pyqtSlot(QMimeData) def add_file_to_message(self, mime): self.messageField.setPlainText(mime.text()) def send_message(self): sel_list = self.contactsList.selectionModel().selectedIndexes() if len(sel_list) > 0: contact = self.model.contactlist.contacts[sel_list[0].row()] if self.messageField.file != '': self.controller.send_file(contact.name, self.messageField.file) self.messageField.file = '' else: self.controller.personal_message( contact.name, self.messageField.toPlainText()) self.messageField.clear() self.model.update_messages_for_contact(contact.name) else: pass @pyqtSlot(QModelIndex) def select_contact(self, modelindex): item = self.contact_list_model.data(modelindex, role=Qt.DisplayRole) contact_name = self.model.contactlist.contacts_name[modelindex.row()] self.contact.setText(contact_name) self.show_messages_for_contact(contact_name) self.contactsList.setCurrentIndex(modelindex) self.currently_selected = contact_name @pyqtSlot(str) def new_personal_message(self, sender): self.model.new_message_increase(sender, True) # sel_list = self.listViewContacts.selectionModel().selectedIndexes() if sender == self.currently_selected: self.show_messages_for_contact(self.currently_selected) @pyqtSlot(str) def show_messages_for_contact(self, contact_name): self.model.update_messages_for_contact(contact_name) self.model.new_message_increase(contact_name, False) self.messagesList.scrollToBottom()
class MainWindow(QWidget): def __init__(self): super(MainWindow, self).__init__() self.top: int = 300 self.left: int = 300 self.width: int = 200 self.height: int = 300 self.setWindowTitle("Albums - List View Example") self.setGeometry(self.top, self.left, self.width, self.height) self.dbm: DbManager = None self.dao: AlbumDao = None self.list_view: QListView = None self.list_model: AlbumListModel = None self.list_selection_model: QItemSelectionModel = None self.init_db() self.init_ui() def init_db(self): """ Initialises DB connection :return: """ self.dbm = DbManager() self.dbm.create() self.dao = AlbumDao(dbm=self.dbm) if not self.dao.init(): print("Failed to initialise DAO for Albums") else: print("DAO initialised for Albums") def init_ui(self): """ Initialises UI components :return: """ self.list_view = QListView() self.list_model = AlbumListModel(dao=self.dao) self.list_model.dataChanged.connect(self.on_data_changed) self.list_selection_model = QItemSelectionModel(self.list_model) self.list_view.setModel(self.list_model) self.list_view.setSelectionModel(self.list_selection_model) btn_add = QPushButton('Add', self) btn_add.setToolTip('Add Album') btn_edit = QPushButton('Edit', self) btn_edit.setToolTip('Edit Album') btn_delete = QPushButton('Delete', self) btn_delete.setToolTip('Delete Album') btn_add.clicked.connect(self.create_album) btn_edit.clicked.connect(self.edit_album) btn_delete.clicked.connect(self.delete_album) list_layout = QHBoxLayout() list_layout.addStretch(1) list_layout.addWidget(self.list_view) buttons_layout = QHBoxLayout() buttons_layout.addStretch(1) buttons_layout.addWidget(btn_add) buttons_layout.addWidget(btn_edit) buttons_layout.addWidget(btn_delete) main_layout = QVBoxLayout() main_layout.addLayout(list_layout) main_layout.addLayout(buttons_layout) self.setLayout(main_layout) self.center() self.show() def center(self): qr = self.frameGeometry() cp = QDesktopWidget().availableGeometry().center() qr.moveCenter(cp) self.move(qr.topLeft()) def create_album(self): input_name, ok_clicked = QInputDialog.getText(self, "Create a new Album", "Choose a name", QLineEdit.Normal, "") if ok_clicked: album = Album(name=input_name) created_index = self.list_model.add_album(album) self.list_view.setCurrentIndex(created_index) def edit_album(self): if len(self.list_selection_model.selectedIndexes()) > 0: current_index = self.list_selection_model.currentIndex() old_name = self.list_model.data(current_index, self.list_model.name_role) input_name, ok_clicked = QInputDialog.getText( self, "Album's name", "Change Album name", QLineEdit.Normal, old_name) if ok_clicked: self.list_model.setData(current_index, input_name, self.list_model.name_role) def delete_album(self): if len(self.list_selection_model.selectedIndexes()) > 0: current_index = self.list_selection_model.currentIndex() row = current_index.row() self.list_model.removeRow(row) # Try to select the previous album previous_index = self.list_model.index(row - 1, 0) if previous_index.isValid(): self.list_selection_model.setCurrentIndex( previous_index, QItemSelectionModel.SelectCurrent) else: # Try to select the next album next_index = self.list_model.index(row, 0) if next_index.isValid(): self.list_selection_model.setCurrentIndex( next_index, QItemSelectionModel.SelectCurrent) def on_data_changed(self, index_1: QModelIndex, index_2: QModelIndex): print('Data has been updated')
class MainWindow(QMainWindow): """MNELAB main window.""" def __init__(self, model): """Initialize MNELAB main window. Parameters ---------- model : mnelab.model.Model instance The main window needs to connect to a model containing all data sets. This decouples the GUI from the data (model/view). """ super().__init__() self.model = model # data model self.setWindowTitle("MNELAB") # restore settings settings = read_settings() self.recent = settings["recent"] # list of recent files if settings["geometry"]: self.restoreGeometry(settings["geometry"]) else: self.setGeometry(300, 300, 1000, 750) # default window size self.move(QApplication.desktop().screen().rect().center() - self.rect().center()) # center window if settings["state"]: self.restoreState(settings["state"]) # initialize menus file_menu = self.menuBar().addMenu("&File") file_menu.addAction( "&Open...", lambda: self.open_file(model.load, "Open raw", SUPPORTED_FORMATS), QKeySequence.Open) self.recent_menu = file_menu.addMenu("Open recent") self.recent_menu.aboutToShow.connect(self._update_recent_menu) self.recent_menu.triggered.connect(self._load_recent) if not self.recent: self.recent_menu.setEnabled(False) self.close_file_action = file_menu.addAction("&Close", self.model.remove_data, QKeySequence.Close) self.close_all_action = file_menu.addAction("Close all", self.close_all) file_menu.addSeparator() self.import_bad_action = file_menu.addAction( "Import bad channels...", lambda: self.import_file( model.import_bads, "Import bad channels", "*.csv")) self.import_events_action = file_menu.addAction( "Import events...", lambda: self.import_file( model.import_events, "Import events", "*.csv")) self.import_anno_action = file_menu.addAction( "Import annotations...", lambda: self.import_file( model.import_annotations, "Import annotations", "*.csv")) self.import_ica_action = file_menu.addAction( "Import &ICA...", lambda: self.open_file( model.import_ica, "Import ICA", "*.fif *.fif.gz")) file_menu.addSeparator() self.export_raw_action = file_menu.addAction( "Export &raw...", lambda: self.export_file(model.export_raw, "Export raw", "*.fif")) self.export_bad_action = file_menu.addAction( "Export &bad channels...", lambda: self.export_file( model.export_bads, "Export bad channels", "*.csv")) self.export_events_action = file_menu.addAction( "Export &events...", lambda: self.export_file( model.export_events, "Export events", "*.csv")) self.export_anno_action = file_menu.addAction( "Export &annotations...", lambda: self.export_file( model.export_annotations, "Export annotations", "*.csv")) self.export_ica_action = file_menu.addAction( "Export ICA...", lambda: self.export_file( model.export_ica, "Export ICA", "*.fif *.fif.gz")) file_menu.addSeparator() file_menu.addAction("&Quit", self.close, QKeySequence.Quit) edit_menu = self.menuBar().addMenu("&Edit") self.pick_chans_action = edit_menu.addAction("Pick &channels...", self.pick_channels) self.chan_props_action = edit_menu.addAction("Channel &properties...", self.channel_properties) self.set_montage_action = edit_menu.addAction("Set &montage...", self.set_montage) edit_menu.addSeparator() self.setref_action = edit_menu.addAction("&Set reference...", self.set_reference) edit_menu.addSeparator() self.events_action = edit_menu.addAction("Events...", self.edit_events) plot_menu = self.menuBar().addMenu("&Plot") self.plot_raw_action = plot_menu.addAction("&Raw data", self.plot_raw) self.plot_psd_action = plot_menu.addAction( "&Power spectral " "density...", self.plot_psd) self.plot_montage_action = plot_menu.addAction("Current &montage", self.plot_montage) plot_menu.addSeparator() self.plot_ica_components_action = plot_menu.addAction( "ICA components...", self.plot_ica_components) tools_menu = self.menuBar().addMenu("&Tools") self.filter_action = tools_menu.addAction("&Filter data...", self.filter_data) self.find_events_action = tools_menu.addAction("Find &events...", self.find_events) self.run_ica_action = tools_menu.addAction("Run &ICA...", self.run_ica) view_menu = self.menuBar().addMenu("&View") statusbar_action = view_menu.addAction("Statusbar", self._toggle_statusbar) statusbar_action.setCheckable(True) help_menu = self.menuBar().addMenu("&Help") help_menu.addAction("&About", self.show_about) help_menu.addAction("About &Qt", self.show_about_qt) # set up data model for sidebar (list of open files) self.names = QStringListModel() self.names.dataChanged.connect(self._update_names) splitter = QSplitter() self.sidebar = QListView() self.sidebar.setFrameStyle(QFrame.NoFrame) self.sidebar.setFocusPolicy(Qt.NoFocus) self.sidebar.setModel(self.names) self.sidebar.clicked.connect(self._update_data) splitter.addWidget(self.sidebar) self.infowidget = InfoWidget() splitter.addWidget(self.infowidget) width = splitter.size().width() splitter.setSizes((width * 0.3, width * 0.7)) self.setCentralWidget(splitter) self.status_label = QLabel() self.statusBar().addPermanentWidget(self.status_label) if settings["statusbar"]: self.statusBar().show() statusbar_action.setChecked(True) else: self.statusBar().hide() statusbar_action.setChecked(False) self.setAcceptDrops(True) self.data_changed() def data_changed(self): # update sidebar self.names.setStringList(self.model.names) self.sidebar.setCurrentIndex(self.names.index(self.model.index)) # update info widget if self.model.data: self.infowidget.set_values(self.model.get_info()) else: self.infowidget.clear() # update status bar if self.model.data: mb = self.model.nbytes / 1024**2 self.status_label.setText("Total Memory: {:.2f} MB".format(mb)) else: self.status_label.clear() # toggle actions if len(self.model) == 0: # disable if no data sets are currently open enabled = False else: enabled = True self.close_file_action.setEnabled(enabled) self.close_all_action.setEnabled(enabled) self.export_raw_action.setEnabled(enabled) if self.model.data: bads = bool(self.model.current["raw"].info["bads"]) self.export_bad_action.setEnabled(enabled and bads) events = self.model.current["events"] is not None self.export_events_action.setEnabled(enabled and events) annot = self.model.current["raw"].annotations is not None self.export_anno_action.setEnabled(enabled and annot) montage = bool(self.model.current["montage"]) self.plot_montage_action.setEnabled(enabled and montage) ica = bool(self.model.current["ica"]) self.export_ica_action.setEnabled(enabled and ica) self.plot_ica_components_action.setEnabled(enabled and ica and montage) self.events_action.setEnabled(enabled and events) else: self.export_bad_action.setEnabled(enabled) self.export_events_action.setEnabled(enabled) self.export_anno_action.setEnabled(enabled) self.plot_montage_action.setEnabled(enabled) self.export_ica_action.setEnabled(enabled) self.plot_ica_components_action.setEnabled(enabled) self.events_action.setEnabled(enabled) self.import_bad_action.setEnabled(enabled) self.import_events_action.setEnabled(enabled) self.import_anno_action.setEnabled(enabled) self.pick_chans_action.setEnabled(enabled) self.chan_props_action.setEnabled(enabled) self.set_montage_action.setEnabled(enabled) self.plot_raw_action.setEnabled(enabled) self.plot_psd_action.setEnabled(enabled) self.filter_action.setEnabled(enabled) self.setref_action.setEnabled(enabled) self.find_events_action.setEnabled(enabled) self.run_ica_action.setEnabled(enabled) self.import_ica_action.setEnabled(enabled) # add to recent files if len(self.model) > 0: self._add_recent(self.model.current["fname"]) def open_file(self, f, text, ffilter): """Open file.""" fname = QFileDialog.getOpenFileName(self, text, filter=ffilter)[0] if fname: f(fname) def export_file(self, f, text, ffilter): """Export to file.""" fname = QFileDialog.getSaveFileName(self, text, filter=ffilter)[0] if fname: f(fname) def import_file(self, f, text, ffilter): """Import file.""" fname = QFileDialog.getOpenFileName(self, text, filter=ffilter)[0] if fname: try: f(fname) except LabelsNotFoundError as e: QMessageBox.critical(self, "Channel labels not found", str(e)) except InvalidAnnotationsError as e: QMessageBox.critical(self, "Invalid annotations", str(e)) def close_all(self): """Close all currently open data sets.""" msg = QMessageBox.question(self, "Close all data sets", "Close all data sets?") if msg == QMessageBox.Yes: while len(self.model) > 0: self.model.remove_data() def pick_channels(self): """Pick channels in current data set.""" channels = self.model.current["raw"].info["ch_names"] dialog = PickChannelsDialog(self, channels, selected=channels) if dialog.exec_(): picks = [item.data(0) for item in dialog.channels.selectedItems()] drops = set(channels) - set(picks) if drops: self.auto_duplicate() self.model.drop_channels(drops) self.model.history.append(f"raw.drop({drops})") def channel_properties(self): """Show channel properties dialog.""" info = self.model.current["raw"].info dialog = ChannelPropertiesDialog(self, info) if dialog.exec_(): dialog.model.sort(0) bads = [] renamed = {} types = {} for i in range(dialog.model.rowCount()): new_label = dialog.model.item(i, 1).data(Qt.DisplayRole) old_label = info["ch_names"][i] if new_label != old_label: renamed[old_label] = new_label new_type = dialog.model.item(i, 2).data(Qt.DisplayRole).lower() old_type = channel_type(info, i).lower() if new_type != old_type: types[new_label] = new_type if dialog.model.item(i, 3).checkState() == Qt.Checked: bads.append(info["ch_names"][i]) self.model.set_channel_properties(bads, renamed, types) def set_montage(self): """Set montage.""" montages = mne.channels.get_builtin_montages() # TODO: currently it is not possible to remove an existing montage dialog = MontageDialog(self, montages, selected=self.model.current["montage"]) if dialog.exec_(): name = dialog.montages.selectedItems()[0].data(0) montage = mne.channels.read_montage(name) ch_names = self.model.current["raw"].info["ch_names"] # check if at least one channel name matches a name in the montage if set(ch_names) & set(montage.ch_names): self.model.set_montage(name) else: QMessageBox.critical( self, "No matching channel names", "Channel names defined in the montage do " "not match any channel name in the data.") def edit_events(self): pos = self.model.current["events"][:, 0].tolist() desc = self.model.current["events"][:, 2].tolist() dialog = EventsDialog(self, pos, desc) if dialog.exec_(): pass def plot_raw(self): """Plot raw data.""" events = self.model.current["events"] nchan = self.model.current["raw"].info["nchan"] fig = self.model.current["raw"].plot(events=events, n_channels=nchan, title=self.model.current["name"], show=False) self.model.history.append("raw.plot(n_channels={})".format(nchan)) win = fig.canvas.manager.window win.setWindowTitle("Raw data") win.findChild(QStatusBar).hide() win.installEventFilter(self) # detect if the figure is closed # prevent closing the window with the escape key try: key_events = fig.canvas.callbacks.callbacks["key_press_event"][8] except KeyError: pass else: # this requires MNE >=0.15 key_events.func.keywords["params"]["close_key"] = None fig.show() def plot_psd(self): """Plot power spectral density (PSD).""" fig = self.model.current["raw"].plot_psd(average=False, spatial_colors=False, show=False) win = fig.canvas.manager.window win.setWindowTitle("Power spectral density") fig.show() def plot_montage(self): """Plot current montage.""" fig = self.model.current["raw"].plot_sensors(show_names=True, show=False) win = fig.canvas.manager.window win.setWindowTitle("Montage") win.findChild(QStatusBar).hide() win.findChild(QToolBar).hide() fig.show() def plot_ica_components(self): self.model.current["ica"].plot_components() def run_ica(self): """Run ICA calculation.""" try: import picard except ImportError: have_picard = False else: have_picard = True dialog = RunICADialog(self, self.model.current["raw"].info["nchan"], have_picard) if dialog.exec_(): calc = CalcDialog(self, "Calculating ICA", "Calculating ICA.") method = dialog.method.currentText() exclude_bad_segments = dialog.exclude_bad_segments.isChecked() ica = mne.preprocessing.ICA(method=dialog.methods[method]) pool = mp.Pool(1) kwds = {"reject_by_annotation": exclude_bad_segments} res = pool.apply_async(func=ica.fit, args=(self.model.current["raw"], ), kwds=kwds, callback=lambda x: calc.accept()) if not calc.exec_(): pool.terminate() else: self.model.current["ica"] = res.get(timeout=1) self.data_changed() def filter_data(self): """Filter data.""" dialog = FilterDialog(self) if dialog.exec_(): self.auto_duplicate() self.model.filter(dialog.low, dialog.high) def find_events(self): dialog = FindEventsDialog(self) if dialog.exec_(): consecutive = dialog.consecutive.isChecked() initial_event = dialog.initial_event.isChecked() uint_cast = dialog.uint_cast.isChecked() min_dur = dialog.min_dur shortest_event = dialog.shortest_event self.model.find_events(consecutive=consecutive, initial_event=initial_event, uint_cast=uint_cast, min_duration=min_dur, shortest_event=shortest_event) def set_reference(self): """Set reference.""" dialog = ReferenceDialog(self) if dialog.exec_(): self.auto_duplicate() if dialog.average.isChecked(): self.model.set_reference("average") else: ref = [c.strip() for c in dialog.channellist.text().split(",")] self.model.set_reference(ref) def show_about(self): """Show About dialog.""" msg = f"""<p style="font-weight: bold">MNELAB {__version__}</p> <p style="font-weight: normal"> <a href="https://github.com/cbrnr/mnelab">MNELAB</a> - a graphical user interface for <a href="https://github.com/mne-tools/mne-python">MNE</a>.</p> <p style="font-weight: normal"> This program uses MNE version {mne.__version__}.</p> <p style="font-weight: normal"> Licensed under the BSD 3-clause license.</p> <p style="font-weight: normal"> Copyright 2017-2018 by Clemens Brunner.</p>""" QMessageBox.about(self, "About MNELAB", msg) def show_about_qt(self): """Show About Qt dialog.""" QMessageBox.aboutQt(self, "About Qt") def auto_duplicate(self): # if current data is stored in a file create a new data set if self.model.current["fname"]: self.model.duplicate_data() # otherwise ask the user else: msg = QMessageBox.question(self, "Overwrite existing data set", "Overwrite existing data set?") if msg == QMessageBox.No: # create new data set self.model.duplicate_data() def _add_recent(self, fname): """Add a file to recent file list. Parameters ---------- fname : str File name. """ if fname in self.recent: # avoid duplicates self.recent.remove(fname) self.recent.insert(0, fname) while len(self.recent) > MAX_RECENT: # prune list self.recent.pop() write_settings(recent=self.recent) if not self.recent_menu.isEnabled(): self.recent_menu.setEnabled(True) def _remove_recent(self, fname): """Remove file from recent file list. Parameters ---------- fname : str File name. """ if fname in self.recent: self.recent.remove(fname) write_settings(recent=self.recent) if not self.recent: self.recent_menu.setEnabled(False) @pyqtSlot(QModelIndex) def _update_data(self, selected): """Update index and information based on the state of the sidebar. Parameters ---------- selected : QModelIndex Index of the selected row. """ if selected.row() != self.model.index: self.model.index = selected.row() self.data_changed() @pyqtSlot(QModelIndex, QModelIndex) def _update_names(self, start, stop): """Update names in DataSets after changes in sidebar.""" for index in range(start.row(), stop.row() + 1): self.model.data[index]["name"] = self.names.stringList()[index] @pyqtSlot() def _update_recent_menu(self): self.recent_menu.clear() for recent in self.recent: self.recent_menu.addAction(recent) @pyqtSlot(QAction) def _load_recent(self, action): self.model.load(action.text()) @pyqtSlot() def _toggle_statusbar(self): if self.statusBar().isHidden(): self.statusBar().show() else: self.statusBar().hide() write_settings(statusbar=not self.statusBar().isHidden()) @pyqtSlot(QDropEvent) def dragEnterEvent(self, event): if event.mimeData().hasUrls(): event.acceptProposedAction() @pyqtSlot(QDropEvent) def dropEvent(self, event): mime = event.mimeData() if mime.hasUrls(): urls = mime.urls() for url in urls: self.load_file(url.toLocalFile()) @pyqtSlot(QEvent) def closeEvent(self, event): """Close application. Parameters ---------- event : QEvent Close event. """ write_settings(geometry=self.saveGeometry(), state=self.saveState()) if self.model.history: print("\nCommand History") print("===============") print("\n".join(self.model.history)) QApplication.quit() def eventFilter(self, source, event): # currently the only source is the raw plot window if event.type() == QEvent.Close: self.data_changed() return QObject.eventFilter(self, source, event)
class ListManager(QDialog): def __init__(self): super().__init__() self.setWindowFlags(Qt.WindowCloseButtonHint | Qt.WindowTitleHint) self.setWindowTitle('List Manager') self.setFixedSize(400, 300) self.ListViewer = QListView() self.ListViewer.clicked.connect(self.ClickList) self.ListViewer.doubleClicked.connect(self.Ok) self.OkButton = QPushButton('OK') self.OkButton.clicked.connect(self.Ok) self.CancelButton = QPushButton('Cancel') self.CancelButton.clicked.connect(self.Cancel) sortorderlable = QLabel('Sort Order') self.SortOrder = [QRadioButton("Asc"), QRadioButton("Desc")] self.OrderGroup = QButtonGroup() sortalglable = QLabel('Sort Algorithm') self.SortAlg = [QRadioButton("Path"), QRadioButton("Natural")] self.AlgGroup = QButtonGroup() hbox1 = QHBoxLayout() hbox2 = QHBoxLayout() vbox1 = QVBoxLayout() vbox2 = QVBoxLayout() vbox3 = QVBoxLayout() mainbox = QVBoxLayout() vbox1.addWidget(sortorderlable) for i in range(len(self.SortOrder)): vbox1.addWidget(self.SortOrder[i]) self.OrderGroup.addButton(self.SortOrder[i], i) self.SortOrder[i].clicked.connect(self.ClickRadioButton) vbox2.addWidget(sortalglable) for i in range(len(self.SortAlg)): vbox2.addWidget(self.SortAlg[i]) self.AlgGroup.addButton(self.SortAlg[i], i) self.SortAlg[i].clicked.connect(self.ClickRadioButton) vbox3.addLayout(vbox1) vbox3.addLayout(vbox2) vbox3.addStretch(1) hbox1.addWidget(self.ListViewer) hbox1.addLayout(vbox3) hbox2.addStretch(1) hbox2.addWidget(self.OkButton) hbox2.addWidget(self.CancelButton) mainbox.addLayout(hbox1) mainbox.addLayout(hbox2) self.setLayout(mainbox) self.list = [] self.path = '' def showEvent(self, event): self.center() self.LoadListToView() def closeEvent(self, event): self.Cancel() def center(self): frameGm = self.frameGeometry() screen = QApplication.desktop().screenNumber( QApplication.desktop().cursor().pos()) centerPoint = QApplication.desktop().screenGeometry(screen).center() frameGm.moveCenter(centerPoint) self.move(frameGm.topLeft()) def LoadListToView(self): if self.list: self.index = self.list.index(self.path) self.Model = QStandardItemModel(self.ListViewer) for file in self.list: item = QStandardItem(file) item.setEditable(False) self.Model.appendRow(item) self.ListViewer.setModel(self.Model) self.ListViewer.setCurrentIndex(self.Model.index(self.index, 0)) def ClickList(self, index): self.path = self.Model.data(index) self.index = self.list.index(self.path) def ClickRadioButton(self): if self.list: self.list = self.Sort(self.list, self.OrderGroup.checkedId(), self.AlgGroup.checkedId()) self.LoadListToView() def Ok(self): self.accept() def Cancel(self): self.reject() def Sort(self, list, sortid, algid): if list: if sortid == 0: isreverse = False else: isreverse = True if algid == 0: sortalg = ns.PATH else: sortalg = 0 return natsorted(list, alg=sortalg, reverse=isreverse) else: return list
class GMDir: EXT_WHITELIST = '.pdf' @property def path(self): return self._path @property def documents(self): return self._documents @property def name(self): return self._name @property def tab_index(self): return self._tab_index @property def closed(self): return self._closed @property def open_count(self): return self._open_count @property def unsaved_count(self): return self._unsaved_count def __init__(self, gm, path): assert isinstance(gm, GalleryMark) self._gm = gm self._path = path self._name = os.path.basename(path) self._documents = [] self._open_count = 0 self._unsaved_count = 0 self._tab_index = None self._tab_widget = None self._listview = QListView() self._listview.setViewMode(QListView.ListMode) self._listview.setSelectionBehavior(QListView.SelectRows) self._listview.activated.connect(self.onListViewItemActivated) self._active_document = None self._closed = False self.refresh(init=True) def refresh(self, init=False): to_remove = [] for doc in self._documents: if not os.path.exists(doc.path): to_remove.append(doc) for r in to_remove: self._documents.remove(r) for item in os.listdir(self._path): full_path = os.path.join(self._path, item) if os.path.isfile(full_path): if item.lower().endswith(self.EXT_WHITELIST): if init: self._documents.append(GMDoc(full_path)) else: found = False for doc in self._documents: if doc.path == full_path: found = True if not found: self.documents.append(GMDoc(full_path)) if self._active_document is None and len(self._documents) > 0: self.setActiveDocument(self._documents[0]) self.updateState() if self._tab_widget is not None: self._tab_widget.setTabText(self._tab_index, self.getTabName()) def onListViewItemActivated(self, model_index): doc = model_index.data(Qt.UserRole) self._gm.openDocument(doc) self.updateState() def updateState(self): self._open_count = 0 self._unsaved_count = 0 model = QStandardItemModel() for doc in self._documents: item_text = doc.name changed = doc.changed() if changed: item_text = '{}*'.format(item_text) self._unsaved_count += 1 if self._active_document is not None and doc == self._active_document: item_text = '> {}'.format(item_text) self._open_count += 1 elif doc.is_open: item_text = '+ {}'.format(item_text) self._open_count += 1 item = QStandardItem(item_text) item.setData(doc, Qt.UserRole) item.setEditable(False) model.appendRow(item) self._listview.setModel(model) def getDocumentByIndex(self, index): if 0 <= index < len(self._documents): mi = self._listview.model().index(index, 0) self._listview.setCurrentIndex(mi) return mi.data(Qt.UserRole) return None def setActiveDocument(self, doc): self._active_document = doc self.updateState() def getPreviousDocument(self): previous = None if self._active_document is not None: for i, doc in enumerate(self._documents): if doc == self._active_document: pi = i - 1 if pi >= 0: previous = self._documents[pi] return previous def getNextDocument(self): next = None if self._active_document is not None: for i, doc in enumerate(self._documents): if doc == self._active_document: ni = i + 1 if ni < len(self._documents): next = self._documents[ni] return next def getTabName(self): return '{} ({})'.format(self._name, len(self._documents)) def setupTab(self, tab_widget): assert isinstance(tab_widget, QTabWidget) container = QWidget() layout = QVBoxLayout() self.updateState() layout.addWidget(self._listview) container.setLayout(layout) tab_widget.addTab(container, self.getTabName()) self._tab_index = tab_widget.indexOf(container) self._tab_widget = tab_widget def close(self, active_doc=None): for doc in self._documents: if active_doc is not None and doc == active_doc: continue if doc.is_open: if not doc.close(): return False self._closed = True return True
class PwdManager(QDialog): def __init__(self): super().__init__() self.setWindowFlags(Qt.WindowCloseButtonHint | Qt.WindowTitleHint) self.setWindowTitle('Password Manager') self.setFixedSize(400, 300) self.PwdViewer = QListView() self.PwdViewer.clicked.connect(self.ClickPwd) self.AddButton = QPushButton('Add') self.AddButton.clicked.connect(self.AddPwd) self.DelButton = QPushButton('Delete') self.DelButton.clicked.connect(self.DelPwd) self.PwdBox = QLineEdit() self.PwdBox.textEdited.connect(self.PwdChanged) vbox = QVBoxLayout() vbox.addWidget(self.AddButton) vbox.addWidget(self.DelButton) vbox.addStretch(1) hbox = QHBoxLayout() hbox.addWidget(self.PwdViewer) hbox.addLayout(vbox) MainBox = QVBoxLayout() MainBox.addWidget(self.PwdBox) MainBox.addLayout(hbox) self.setLayout(MainBox) def showEvent(self, event): self.center() self.LoadPwdToList() if self.Model.rowCount() == 0: self.AddPwd() self.IsChanged = False def closeEvent(self, event): self.RemoveEmpty() pwdf = open('password.pwd', 'w') for index in range(self.Model.rowCount()): pwdf.write(self.Model.data(self.Model.index(index, 0)) + '\n') pwdf.close() if self.IsChanged: self.done(1) else: self.done(0) def center(self): frameGm = self.frameGeometry() screen = QApplication.desktop().screenNumber( QApplication.desktop().cursor().pos()) centerPoint = QApplication.desktop().screenGeometry(screen).center() frameGm.moveCenter(centerPoint) self.move(frameGm.topLeft()) def LoadPwdToList(self): if not os.path.exists(r'password.pwd'): open("password.pwd", "wb").close() pwdf = open('password.pwd', 'r') pwdlist = pwdf.read().splitlines() pwdf.close() self.Model = QStandardItemModel(self.PwdViewer) for pwd in pwdlist: item = QStandardItem(pwd) item.setEditable(False) self.Model.appendRow(item) self.PwdViewer.setModel(self.Model) self.PwdViewer.setCurrentIndex(self.Model.index(0, 0)) self.GetPwd(self.PwdViewer.currentIndex()) def ClickPwd(self, index): self.RemoveEmpty() if self.Model.rowCount() == 0: self.AddPwd() self.GetPwd(index) def GetPwd(self, index): self.PwdBox.setText(self.Model.data(index)) self.PwdBox.setFocus() def PwdChanged(self): self.IsChanged = True self.Model.setData(self.PwdViewer.currentIndex(), self.PwdBox.text()) if self.PwdBox.text() == '': self.DelPwd() def DelPwd(self): self.Model.removeRow(self.PwdViewer.currentIndex().row()) self.GetPwd(self.PwdViewer.currentIndex()) if self.Model.rowCount() == 0: self.AddPwd() def AddPwd(self): item = QStandardItem() item.setEditable(False) self.Model.appendRow(item) self.PwdViewer.setCurrentIndex( self.Model.index(self.Model.rowCount() - 1, 0)) self.GetPwd(self.PwdViewer.currentIndex()) def RemoveEmpty(self): for item in self.Model.findItems('', Qt.MatchFixedString): self.Model.removeRow(item.row())
class MainWindow(QWidget): def __init__(self, parent=None): super(MainWindow, self).__init__(parent) self.playlistView = QListView() self.switch_status = 2 self.video_widget = QVideoWidget() self.playlist = QMediaPlaylist() self.model = PlaylistModel(self.playlist) self.titleBar = TitleBar(self) self.currentTimeLabel = QLabel() self.timeSlider = QSlider() self.totalTimeLabel = QLabel() self.mediaPlayer = QMediaPlayer() self.open_btn = QPushButton('Open File') self.play_btn = QPushButton() self.prev_btn = QPushButton() self.stop_btn = QPushButton() self.next_btn = QPushButton() self.switch_media_widgets_btn = QPushButton() self.pseudo_label = QLabel() self.vol_label = QLabel() self.volume_slider = Slider(Qt.Horizontal) self.gui() self.set_children_focus_policy(Qt.NoFocus) def gui(self): self.currentTimeLabel.setMinimumSize(QSize(80, 0)) self.currentTimeLabel.setAlignment(Qt.AlignRight | Qt.AlignTrailing | Qt.AlignVCenter) self.timeSlider.setOrientation(Qt.Horizontal) self.totalTimeLabel.setMinimumSize(QSize(80, 0)) self.totalTimeLabel.setAlignment(Qt.AlignLeading | Qt.AlignLeft | Qt.AlignVCenter) self.playlistView.setAcceptDrops(True) self.playlistView.setProperty("showDropIndicator", True) self.playlistView.setDragDropMode(QAbstractItemView.DropOnly) self.playlistView.setAlternatingRowColors(True) self.playlistView.setUniformItemSizes(True) self.setWindowFlags(Qt.FramelessWindowHint) self.setWindowTitle('Media Player') self.titleBar.label.setText('Media Player') self.setWindowIcon(QIcon('icon_png/media_player.png')) self.setGeometry(600, 200, 850, 600) self.timeSlider.setRange(0, 0) self.play_btn.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) self.prev_btn.setIcon(self.style().standardIcon(QStyle.SP_MediaSkipBackward)) self.next_btn.setIcon(self.style().standardIcon(QStyle.SP_MediaSkipForward)) self.stop_btn.setIcon(self.style().standardIcon(QStyle.SP_MediaStop)) self.switch_media_widgets_btn.setIcon(self.style().standardIcon(QStyle.SP_FileDialogDetailedView)) self.vol_label.setText("") self.vol_label.setPixmap(QPixmap("icon_png/speaker-volume.png")) self.currentTimeLabel.setText("00:00") self.totalTimeLabel.setText("00:00") self.volume_slider.setValue(self.mediaPlayer.volume()) self.mediaPlayer.setVideoOutput(self.video_widget) self.mediaPlayer.setPlaylist(self.playlist) self.playlistView.setModel(self.model) self.video_widget.hide() sizegrip = QSizeGrip(self) self.setAcceptDrops(True) inner_h_box = QHBoxLayout() inner_h_box.addWidget(self.prev_btn) inner_h_box.addWidget(self.stop_btn) inner_h_box.addWidget(self.next_btn) vol_h_box = QHBoxLayout() vol_h_box.addWidget(self.vol_label, 0) vol_h_box.addWidget(self.volume_slider, 1) h_box = QHBoxLayout() h_box.addWidget(self.open_btn) h_box.addWidget(self.play_btn, 0) h_box.addLayout(inner_h_box, 0) h_box.addWidget(self.switch_media_widgets_btn, 0) h_box.addWidget(self.pseudo_label, 1) h_box.addLayout(vol_h_box, 0) h_box.addWidget(sizegrip, 0, Qt.AlignBottom | Qt.AlignRight) video_slider_h_box = QHBoxLayout() video_slider_h_box.addWidget(self.currentTimeLabel) video_slider_h_box.addWidget(self.timeSlider) video_slider_h_box.addWidget(self.totalTimeLabel) v_box = QVBoxLayout() v_box.addWidget(self.titleBar, 0) v_box.addWidget(self.video_widget, 1) v_box.addWidget(self.playlistView, 1) v_box.addLayout(video_slider_h_box, 0) v_box.addLayout(h_box, 0) inner_h_box.setContentsMargins(20, 0, 10, 0) vol_h_box.setContentsMargins(0, 0, 20, 0) h_box.setContentsMargins(20, 0, 0, 0) v_box.setContentsMargins(0, 0, 0, 0) video_slider_h_box.setSpacing(10) h_box.setSpacing(0) v_box.setSpacing(0) self.setLayout(v_box) self.enabler() # connections self.open_btn.clicked.connect(self.open_file) self.play_btn.clicked.connect(self.play_media) self.stop_btn.clicked.connect(self.stop_media) self.prev_btn.pressed.connect(self.play_prev) self.next_btn.pressed.connect(self.play_next) self.switch_media_widgets_btn.pressed.connect(self.switch_media) self.playlist.currentIndexChanged.connect(self.playlist_position_changed) selection_model = self.playlistView.selectionModel() selection_model.selectionChanged.connect(self.playlist_selection_changed) self.mediaPlayer.durationChanged.connect(self.update_duration) self.mediaPlayer.positionChanged.connect(self.update_position) self.timeSlider.valueChanged.connect(self.mediaPlayer.setPosition) self.mediaPlayer.stateChanged.connect(self.media_state) self.mediaPlayer.volumeChanged.connect(self.volume_changed) self.volume_slider.valueChanged.connect(self.set_volume) def set_children_focus_policy(self, policy): def recursive_set_child_focus_policy(parent_q_widget): for childQWidget in parent_q_widget.findChildren(QWidget): childQWidget.setFocusPolicy(policy) recursive_set_child_focus_policy(childQWidget) recursive_set_child_focus_policy(self) def enabler(self, state=False): if state is False: self.play_btn.setEnabled(False) self.prev_btn.setEnabled(False) self.stop_btn.setEnabled(False) self.next_btn.setEnabled(False) else: self.play_btn.setEnabled(True) self.stop_btn.setEnabled(True) self.prev_btn.setEnabled(True) self.next_btn.setEnabled(True) def switch_media(self): if self.switch_status == 0: self.video_widget.hide() self.playlistView.show() self.switch_status = 1 self.switch_media_widgets_btn.setEnabled(True) elif self.switch_status == 1: self.video_widget.show() self.playlistView.hide() self.switch_status = 0 self.switch_media_widgets_btn.setEnabled(True) else: self.video_widget.hide() self.playlistView.show() self.switch_media_widgets_btn.setEnabled(False) def play_media(self): if self.mediaPlayer.state() == QMediaPlayer.PlayingState: self.mediaPlayer.pause() else: self.mediaPlayer.play() self.ui_handler() def ui_handler(self): if not self.playlist.isEmpty(): self.enabler(True) file_path = QFileInfo(self.mediaPlayer.currentMedia().canonicalUrl().toString()).fileName() ext = os.path.splitext(file_path)[-1].lower() audio_ext = ['.flac', '.mp3'] video_ext = ['.mp4', '.m4a', '.mov', '.flv', 'avi', '3gp', '.mkv', '.wmv'] if ext in audio_ext: self.switch_status = 2 self.switch_media() if self.isFullScreen(): self.fullscreen() elif ext in video_ext: self.switch_status = 1 self.switch_media() self.setWindowTitle(file_path + ' - Media Player') self.titleBar.label.setText(file_path + ' - Media Player') def stop_media(self): if self.mediaPlayer.state() != QMediaPlayer.StoppedState: self.mediaPlayer.stop() self.setWindowTitle('Media Player') self.titleBar.label.setText('Media Player') def fullscreen(self): if self.switch_status == 2 or self.isFullScreen(): self.titleBar.show() self.timeSlider.show() self.currentTimeLabel.show() self.totalTimeLabel.show() self.volume_slider.show() self.open_btn.show() self.play_btn.show() self.prev_btn.show() self.stop_btn.show() self.next_btn.show() self.switch_media_widgets_btn.show() self.pseudo_label.show() self.vol_label.show() self.showNormal() else: self.titleBar.hide() self.timeSlider.hide() self.currentTimeLabel.hide() self.totalTimeLabel.hide() self.volume_slider.hide() self.open_btn.hide() self.play_btn.hide() self.prev_btn.hide() self.stop_btn.hide() self.next_btn.hide() self.switch_media_widgets_btn.hide() self.pseudo_label.hide() self.vol_label.hide() self.showFullScreen() def mouseDoubleClickEvent(self, event: QMouseEvent): event.accept() if event.button() == Qt.LeftButton: self.fullscreen() def media_state(self): os_sleep = WindowsInhibitor() if self.mediaPlayer.state() == QMediaPlayer.PlayingState: self.play_btn.setIcon(self.style().standardIcon(QStyle.SP_MediaPause)) if os.name == 'nt': os_sleep = WindowsInhibitor() os_sleep.inhibit() else: self.play_btn.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) if os_sleep: os_sleep.uninhibit() def play_next(self): self.playlist.next() def media_seek(self, seek): if not self.playlist.isEmpty(): player = self.mediaPlayer if (player.duration() - seek) > player.position(): player.setPosition(player.position() + seek) def play_prev(self): self.playlist.previous() def dragEnterEvent(self, e): if e.mimeData().hasUrls(): e.acceptProposedAction() def dropEvent(self, e): for url in e.mimeData().urls(): ext = os.path.splitext(url.fileName())[-1].lower() allowed_ext = ['.flac', '.mp3', '.mp4', '.m4a', '.mov', '.flv', 'avi', '3gp', '.mkv', '.wmv'] if ext in allowed_ext: self.playlist.addMedia( QMediaContent(url) ) self.model.layoutChanged.emit() if self.mediaPlayer.state() != QMediaPlayer.PlayingState: i = self.playlist.mediaCount() - len(e.mimeData().urls()) self.playlist.setCurrentIndex(i) if not self.playlist.isEmpty(): self.play_media() def open_file(self): filter_files = "Media (*.mp3 *.mp4 *.mkv);; Videos files (*.mp4 *.mkv);; Music Files(*.mp3)" paths, _ = QFileDialog.getOpenFileNames(self, "Open file", "", filter_files) if paths: self.mediaPlayer.pause() for path in paths: self.playlist.addMedia( QMediaContent( QUrl.fromLocalFile(path) ) ) i = self.playlist.mediaCount() - len(paths) self.playlist.setCurrentIndex(i) self.play_media() self.model.layoutChanged.emit() def update_duration(self, duration): self.mediaPlayer.duration() self.timeSlider.setMaximum(duration) if duration >= 0: self.totalTimeLabel.setText(hhmmss(duration)) def update_position(self, position): if position >= 0: self.currentTimeLabel.setText(hhmmss(position)) self.timeSlider.blockSignals(True) self.timeSlider.setValue(position) self.timeSlider.blockSignals(False) def playlist_selection_changed(self, ix): i = ix.indexes()[0].row() self.playlist.setCurrentIndex(i) self.ui_handler() def playlist_position_changed(self, i): if i > -1: ix = self.model.index(i) self.playlistView.setCurrentIndex(ix) def set_volume(self, value): self.mediaPlayer.setVolume(value) def volume_changed(self, value): self.volume_slider.setValue(value) def keyPressEvent(self, event): key = event.key() modifiers = int(event.modifiers()) if (modifiers and modifiers & MOD_MASK == modifiers and key > 0 and key != Qt.Key_Shift and key != Qt.Key_Alt and key != Qt.Key_Control and key != Qt.Key_Meta): key_name = QKeySequence(modifiers + key).toString() if key_name == 'Ctrl+Right': self.media_seek(30000) elif key_name == 'Ctrl+Left': self.media_seek(-30000) elif key_name == 'Ctrl+Up': self.mediaPlayer.setVolume(self.mediaPlayer.volume() + 5) elif key_name == 'Ctrl+Down': self.mediaPlayer.setVolume(self.mediaPlayer.volume() - 5) elif key_name == 'Ctrl+O': self.open_file() else: if event.key() == Qt.Key_Space: self.play_media() elif event.key() == Qt.Key_MediaPlay: self.play_media() elif event.key() == Qt.Key_MediaNext: self.play_next() elif event.key() == Qt.Key_MediaPrevious: self.play_prev() elif event.key() == Qt.Key_Escape: self.close() elif event.key() == Qt.Key_F: self.fullscreen() elif event.key() == Qt.Key_Right: self.media_seek(5000) elif event.key() == Qt.Key_Left: self.media_seek(-5000)
class Chat(QWidget): msg_signal = pyqtSignal(dict) after_close = None chats = [] cur_chat = None def __init__(self, parent=None): super().__init__(parent) self.msg_signal.connect(self.fill_msg) MsgWorker().do_recv_msg = self.do_recv_msg self.setWindowTitle('') self.op_bar = QFrame(self) self.op_bar.setStyleSheet('background-color:rgb(255, 255, 255);') self.send_bar = QFrame(self) self.send_bar.setStyleSheet('background-color:rgb(255, 255, 255);') self.rcTxt = QTextBrowser(self) self.rcTxt.setStyleSheet('background-color:rgb(255, 255, 255);') self.sdTxt = QTextEdit(self) self.sdTxt.setStyleSheet('background-color:rgb(255, 255, 255);') self.btn = QPushButton("发送", self.send_bar) self.btn.clicked.connect(self.send) self.lv = QListView(self) self.lv.setViewMode(QListView.ListMode) self.lv.setIconSize(QSize(30, 30)) self.lv.setEditTriggers(QAbstractItemView.NoEditTriggers) self.lv.setResizeMode(QListView.Adjust) self.lv_model = QStandardItemModel() self.lv.setModel(self.lv_model) self.lv.clicked.connect(self.lv_clicked) self.lv.move(0, 0) w, h = 600, 400 self.resize(800, 600) def resizeEvent(self, evt): self.after_resize(evt.size().width(), evt.size().height()) def after_resize(self, w, h): lv_width = 200 sdTxt_height = 120 bar_height = 30 self.op_bar.move(200, h - sdTxt_height - bar_height * 2) self.op_bar.resize(w - lv_width, bar_height) self.send_bar.move(200, h - bar_height) self.send_bar.resize(w - lv_width, bar_height) self.lv.resize(lv_width, h) self.rcTxt.move(lv_width, 0) self.rcTxt.resize(w - lv_width, h - sdTxt_height - bar_height * 2) self.sdTxt.move(lv_width, h - sdTxt_height - bar_height) self.sdTxt.resize(w - lv_width, sdTxt_height) def lv_clicked(self, model_index): cur_chat = self.chats[model_index.row()] if cur_chat['mode'] == 'user': self.setWindowTitle(cur_chat['data'].nick_name) def refresh_cur_chat(self): if self.cur_chat['mode'] == 'user': self.setWindowTitle(self.cur_chat['data'].nick_name) def get_in_chat_index(self, chat): if chat['mode'] == 'user': name = chat['data'].name match = lambda x: x['mode'] == 'user' and x['data'].name == name for i in range(len(self.chats)): if match(self.chats[i]): return i return -1 def chat_to(self, chat): i = self.get_in_chat_index(chat) if i == -1: if chat['mode'] == 'user': self.lv_model.appendRow( QStandardItem(QIcon("./client/image/man.png"), chat['data'].nick_name)) self.chats.append(chat) self.cur_chat = chat i = len(self.chats) - 1 else: self.cur_chat = self.chats[i] self.refresh_cur_chat() self.lv.setCurrentIndex(self.lv_model.index(i, 0)) def fill_msg(self, data): ufrom = data['from'] uto = data['to'] val = data['val'] ufrom_nickname = ufrom try: self.rcTxt.setPlainText(self.rcTxt.toPlainText() + '%s:%s\n' % (ufrom_nickname, val)) except Exception as ex: print(ex) def do_recv_msg(self, data): self.msg_signal.emit(data) def send(self): # val = self.sdTxt.toHtml() val = self.sdTxt.toPlainText() if self.cur_chat['mode'] == 'user': MsgWorker().send_msg( msg_lib.build_chat_msg(MsgWorker().user_info.name, self.cur_chat['data'].name, val)) # self.rcTxt.setHtml(self.rcTxt.toHtml()+'\n我:%s'%val) self.rcTxt.setPlainText(self.rcTxt.toPlainText() + '我:%s\n' % val) self.sdTxt.setPlainText('') def closeEvent(self, event): self.chats.clear() if self.after_close: self.after_close()
class MainWindow(QMainWindow): labelsChanged = pyqtSignal(list, name='labelsChanged') def __init__(self): QMainWindow.__init__(self) self.view = QGraphicsView() self.scene = LabelingScene() self.view.setScene(self.scene) self.setCentralWidget(self.view) self.previousImageIdx = 0 self.currentImageIdx = 0 self.setupToolBar() self.setupDockWidgets() self.setupStatusBar() self.showMaximized() self.scene.labelsChanged.connect(self.updateLabels) self.labelsChanged.connect(self.scene.setLabels) self.scene.nextImage.connect(self.nextImage) self.scene.previousImage.connect(self.previousImage) self.scene.copyLabelsFromPrevious.connect( self.copyLabelsFromPreviousImage) def setupToolBar(self): self.toolbar = QToolBar() self.toolbar.addAction(QIcon.fromTheme("document-new"), "Create a new label file", self.newFile) self.toolbar.addAction(QIcon.fromTheme("document-open"), "Open a label file", self.openFile) saveAction = self.toolbar.addAction(QIcon.fromTheme("document-save"), "Save", self.saveFile) saveAction.setShortcut(QKeySequence(QKeySequence.Save)) self.toolbar.addSeparator() self.toolbar.addAction(QIcon.fromTheme("insert-image"), "Add images to dataset", self.addImages) self.toolbar.addSeparator() self.toolbar.addAction(QIcon.fromTheme("go-previous"), "Next image", self.nextImage) self.toolbar.addAction(QIcon.fromTheme("edit-copy"), "Copy labels from previous image", self.copyLabelsFromPreviousImage) self.toolbar.addAction(QIcon.fromTheme("go-next"), "Previous image", self.previousImage) self.addToolBar(self.toolbar) def setupDockWidgets(self): self.fileListWidget = QListView() self.leftDockWidget = QDockWidget() self.leftDockWidget.setWidget(self.fileListWidget) self.fileListModel = QStringListModel(self.fileListWidget) self.fileListWidget.setModel(self.fileListModel) self.fileListWidget.setEditTriggers(QAbstractItemView.NoEditTriggers) self.fileListWidget.doubleClicked.connect( lambda index: self.loadImageAtIndex(index.row())) self.addDockWidget(Qt.LeftDockWidgetArea, self.leftDockWidget) def setupStatusBar(self): self.statusBar = QStatusBar() self.setStatusBar(self.statusBar) self.currentImageLabel = QLabel() self.statusBar.addPermanentWidget(self.currentImageLabel) self.scene.mouseMoved.connect(lambda p: self.statusBar.showMessage( "Mouse at scene pos {}, {}".format(p.x(), p.y()))) #Here we assume that the image files are stored in the same folder as the labels file. #This is usual practice, maybe in the future we could put images and labels in the same file (HDF5) def newFile(self): imageFileNames = self.normalizeFileNames( QFileDialog.getOpenFileNames( caption="Select image files to label")[0]) self.labelsFileName = QFileDialog.getSaveFileName( caption="Select labels file to save")[0] self.labelsFolder = QFileInfo(self.labelsFileName).absolutePath() print("Creating new labels file with {} images to be labeled".format( len(imageFileNames))) self.labeledImages = [] for imageFileName in imageFileNames: self.labeledImages.append(LabeledImage(imageFileName, [])) self.loadImageAtIndex(0) self.previousImageIdx = 0 self.fileListModel.setStringList(imageFileNames) def normalizeFileNames(self, fileNames): return [QFileInfo(f).fileName() for f in fileNames] def loadImageAtIndex(self, index): if (index < 0) or (index >= len(self.labeledImages)): return self.previousImageIdx = self.currentImageIdx self.currentImageIdx = index self.scene.displayLabeledImage( self.labeledImages[self.currentImageIdx], self.labelsFolder) self.fileListWidget.setCurrentIndex( self.fileListModel.index(self.currentImageIdx, 0)) msg = "{} ({}/{})".format( self.labeledImages[self.currentImageIdx].fileName, self.currentImageIdx + 1, len(self.labeledImages)) self.currentImageLabel.setText(msg) def updateLabels(self, newLabels): self.labeledImages[self.currentImageIdx].labels = newLabels def openFile(self): labelsFileName = QFileDialog.getOpenFileName( caption="Select labels file to load")[0] if labelsFileName == "" or len(labelsFileName) == 0: return self.labelsFileName = labelsFileName self.labelsFolder = QFileInfo(self.labelsFileName).absolutePath() self.labeledImages = readXML(self.labelsFileName) self.fileListModel.setStringList( [s.fileName for s in self.labeledImages]) self.loadImageAtIndex(0) self.previousImageIdx = 0 self.scene.labelsCache = self.allLabelNames() def saveFile(self): writeXML(self.labelsFileName, self.labeledImages) self.statusBar.showMessage("Saved!") def nextImage(self): self.loadImageAtIndex(self.currentImageIdx + 1) def previousImage(self): self.loadImageAtIndex(self.currentImageIdx - 1) def copyLabelsFromPreviousImage(self): self.labeledImages[self.currentImageIdx].labels = self.labeledImages[ self.previousImageIdx].labels self.labelsChanged.emit( self.labeledImages[self.currentImageIdx].labels) def allLabelNames(self): labelNames = [] for labeledImage in self.labeledImages: for label in labeledImage.labels: if label.classLabel not in labelNames: labelNames.append(label.classLabel) return labelNames def addImages(self): imageFileNames = QFileDialog.getOpenFileNames( caption="Select image files to label") if not imageFileNames[0] or len(imageFileNames[0]) == 0: return imageFileNames = imageFileNames[0] labelsDir = QFileInfo(self.labelsFileName).absoluteDir() originDir = QFileInfo(imageFileNames[0]).absoluteDir() #Copy image files to the labels folder if originDir.absolutePath() != labelsDir.absolutePath(): progDialog = QProgressDialog( "Copying image files to the labels folder", "Cancel", 0, len(imageFileNames), self) i = 0 for imageFileName in imageFileNames: progDialog.setValue(i) oldName = QFileInfo(imageFileName).fileName() newPath = labelsDir.absoluteFilePath(oldName) print("Copying {} to {}".format(imageFileName, newPath)) ok = QFile.copy(imageFileName, newPath) QApplication.processEvents() if not ok: print("Error copying {} to {}".format(imageFileName, newPath)) i += 1 progDialog.setValue(len(imageFileNames)) progDialog.close() currentImageFileNames = [s.fileName for s in self.labeledImages] newImgIdx = len(self.labeledImages) for imageFileName in imageFileNames: normalizedFileName = QFileInfo(imageFileName).fileName() if normalizedFileName in currentImageFileNames: print("File {} already in dataset, skipping".format( normalizedFileName)) continue self.labeledImages.append(LabeledImage(normalizedFileName, [])) self.fileListModel.setStringList( [s.fileName for s in self.labeledImages]) self.loadImageAtIndex(newImgIdx) print("Added {} images to dataset".format(len(imageFileNames))) print("New length of labeledImages array {}".format( len(self.labeledImages)))
class Player(QWidget): fullScreenChanged = pyqtSignal(bool) def __init__(self, playlist, parent=None): super(Player, self).__init__(parent) self.colorDialog = None self.trackInfo = "" self.statusInfo = "" self.duration = 0 self.player = QMediaPlayer() self.playlist = QMediaPlaylist() self.player.setPlaylist(self.playlist) self.player.durationChanged.connect(self.durationChanged) self.player.positionChanged.connect(self.positionChanged) self.player.metaDataChanged.connect(self.metaDataChanged) self.playlist.currentIndexChanged.connect(self.playlistPositionChanged) self.player.mediaStatusChanged.connect(self.statusChanged) self.player.bufferStatusChanged.connect(self.bufferingProgress) self.player.videoAvailableChanged.connect(self.videoAvailableChanged) self.player.error.connect(self.displayErrorMessage) self.videoWidget = VideoWidget() self.player.setVideoOutput(self.videoWidget) self.playlistModel = PlaylistModel() self.playlistModel.setPlaylist(self.playlist) self.playlistView = QListView() self.playlistView.setModel(self.playlistModel) self.playlistView.setCurrentIndex( self.playlistModel.index(self.playlist.currentIndex(), 0)) self.playlistView.activated.connect(self.jump) self.slider = QSlider(Qt.Horizontal) self.slider.setRange(0, self.player.duration() / 1000) self.labelDuration = QLabel() self.slider.sliderMoved.connect(self.seek) self.labelHistogram = QLabel() self.labelHistogram.setText("Histogram:") self.histogram = HistogramWidget() histogramLayout = QHBoxLayout() histogramLayout.addWidget(self.labelHistogram) histogramLayout.addWidget(self.histogram, 1) self.probe = QVideoProbe() self.probe.videoFrameProbed.connect(self.histogram.processFrame) self.probe.setSource(self.player) openButton = QPushButton("Open", clicked=self.open) controls = PlayerControls() controls.setState(self.player.state()) controls.setVolume(self.player.volume()) controls.setMuted(controls.isMuted()) controls.play.connect(self.player.play) controls.pause.connect(self.player.pause) controls.stop.connect(self.player.stop) controls.next.connect(self.playlist.next) controls.previous.connect(self.previousClicked) controls.changeVolume.connect(self.player.setVolume) controls.changeMuting.connect(self.player.setMuted) controls.changeRate.connect(self.player.setPlaybackRate) controls.stop.connect(self.videoWidget.update) self.player.stateChanged.connect(controls.setState) self.player.volumeChanged.connect(controls.setVolume) self.player.mutedChanged.connect(controls.setMuted) self.fullScreenButton = QPushButton("FullScreen") self.fullScreenButton.setCheckable(True) self.colorButton = QPushButton("Color Options...") self.colorButton.setEnabled(False) self.colorButton.clicked.connect(self.showColorDialog) displayLayout = QHBoxLayout() displayLayout.addWidget(self.videoWidget, 2) displayLayout.addWidget(self.playlistView) controlLayout = QHBoxLayout() controlLayout.setContentsMargins(0, 0, 0, 0) controlLayout.addWidget(openButton) controlLayout.addStretch(1) controlLayout.addWidget(controls) controlLayout.addStretch(1) controlLayout.addWidget(self.fullScreenButton) controlLayout.addWidget(self.colorButton) layout = QVBoxLayout() layout.addLayout(displayLayout) hLayout = QHBoxLayout() hLayout.addWidget(self.slider) hLayout.addWidget(self.labelDuration) layout.addLayout(hLayout) layout.addLayout(controlLayout) layout.addLayout(histogramLayout) self.setLayout(layout) if not self.player.isAvailable(): QMessageBox.warning(self, "Service not available", "The QMediaPlayer object does not have a valid service.\n" "Please check the media service plugins are installed.") controls.setEnabled(False) self.playlistView.setEnabled(False) openButton.setEnabled(False) self.colorButton.setEnabled(False) self.fullScreenButton.setEnabled(False) self.metaDataChanged() self.addToPlaylist(playlist) def open(self): fileNames, _ = QFileDialog.getOpenFileNames(self, "Open Files") self.addToPlaylist(fileNames) def addToPlaylist(self, fileNames): for name in fileNames: fileInfo = QFileInfo(name) if fileInfo.exists(): url = QUrl.fromLocalFile(fileInfo.absoluteFilePath()) if fileInfo.suffix().lower() == 'm3u': self.playlist.load(url) else: self.playlist.addMedia(QMediaContent(url)) else: url = QUrl(name) if url.isValid(): self.playlist.addMedia(QMediaContent(url)) def durationChanged(self, duration): duration /= 1000 self.duration = duration self.slider.setMaximum(duration) def positionChanged(self, progress): progress /= 1000 if not self.slider.isSliderDown(): self.slider.setValue(progress) self.updateDurationInfo(progress) def metaDataChanged(self): if self.player.isMetaDataAvailable(): self.setTrackInfo("%s - %s" % ( self.player.metaData(QMediaMetaData.AlbumArtist), self.player.metaData(QMediaMetaData.Title))) def previousClicked(self): # Go to the previous track if we are within the first 5 seconds of # playback. Otherwise, seek to the beginning. if self.player.position() <= 5000: self.playlist.previous() else: self.player.setPosition(0) def jump(self, index): if index.isValid(): self.playlist.setCurrentIndex(index.row()) self.player.play() def playlistPositionChanged(self, position): self.playlistView.setCurrentIndex( self.playlistModel.index(position, 0)) def seek(self, seconds): self.player.setPosition(seconds * 1000) def statusChanged(self, status): self.handleCursor(status) if status == QMediaPlayer.LoadingMedia: self.setStatusInfo("Loading...") elif status == QMediaPlayer.StalledMedia: self.setStatusInfo("Media Stalled") elif status == QMediaPlayer.EndOfMedia: QApplication.alert(self) elif status == QMediaPlayer.InvalidMedia: self.displayErrorMessage() else: self.setStatusInfo("") def handleCursor(self, status): if status in (QMediaPlayer.LoadingMedia, QMediaPlayer.BufferingMedia, QMediaPlayer.StalledMedia): self.setCursor(Qt.BusyCursor) else: self.unsetCursor() def bufferingProgress(self, progress): self.setStatusInfo("Buffering %d%" % progress) def videoAvailableChanged(self, available): if available: self.fullScreenButton.clicked.connect( self.videoWidget.setFullScreen) self.videoWidget.fullScreenChanged.connect( self.fullScreenButton.setChecked) if self.fullScreenButton.isChecked(): self.videoWidget.setFullScreen(True) else: self.fullScreenButton.clicked.disconnect( self.videoWidget.setFullScreen) self.videoWidget.fullScreenChanged.disconnect( self.fullScreenButton.setChecked) self.videoWidget.setFullScreen(False) self.colorButton.setEnabled(available) def setTrackInfo(self, info): self.trackInfo = info if self.statusInfo != "": self.setWindowTitle("%s | %s" % (self.trackInfo, self.statusInfo)) else: self.setWindowTitle(self.trackInfo) def setStatusInfo(self, info): self.statusInfo = info if self.statusInfo != "": self.setWindowTitle("%s | %s" % (self.trackInfo, self.statusInfo)) else: self.setWindowTitle(self.trackInfo) def displayErrorMessage(self): self.setStatusInfo(self.player.errorString()) def updateDurationInfo(self, currentInfo): duration = self.duration if currentInfo or duration: currentTime = QTime((currentInfo/3600)%60, (currentInfo/60)%60, currentInfo%60, (currentInfo*1000)%1000) totalTime = QTime((duration/3600)%60, (duration/60)%60, duration%60, (duration*1000)%1000); format = 'hh:mm:ss' if duration > 3600 else 'mm:ss' tStr = currentTime.toString(format) + " / " + totalTime.toString(format) else: tStr = "" self.labelDuration.setText(tStr) def showColorDialog(self): if self.colorDialog is None: brightnessSlider = QSlider(Qt.Horizontal) brightnessSlider.setRange(-100, 100) brightnessSlider.setValue(self.videoWidget.brightness()) brightnessSlider.sliderMoved.connect( self.videoWidget.setBrightness) self.videoWidget.brightnessChanged.connect( brightnessSlider.setValue) contrastSlider = QSlider(Qt.Horizontal) contrastSlider.setRange(-100, 100) contrastSlider.setValue(self.videoWidget.contrast()) contrastSlider.sliderMoved.connect(self.videoWidget.setContrast) self.videoWidget.contrastChanged.connect(contrastSlider.setValue) hueSlider = QSlider(Qt.Horizontal) hueSlider.setRange(-100, 100) hueSlider.setValue(self.videoWidget.hue()) hueSlider.sliderMoved.connect(self.videoWidget.setHue) self.videoWidget.hueChanged.connect(hueSlider.setValue) saturationSlider = QSlider(Qt.Horizontal) saturationSlider.setRange(-100, 100) saturationSlider.setValue(self.videoWidget.saturation()) saturationSlider.sliderMoved.connect( self.videoWidget.setSaturation) self.videoWidget.saturationChanged.connect( saturationSlider.setValue) layout = QFormLayout() layout.addRow("Brightness", brightnessSlider) layout.addRow("Contrast", contrastSlider) layout.addRow("Hue", hueSlider) layout.addRow("Saturation", saturationSlider) button = QPushButton("Close") layout.addRow(button) self.colorDialog = QDialog(self) self.colorDialog.setWindowTitle("Color Options") self.colorDialog.setLayout(layout) button.clicked.connect(self.colorDialog.close) self.colorDialog.show()