class MainWindow(QMainWindow): def __init__(self, args, parser): super(MainWindow, self).__init__() log.debug(_("Theme search paths: %s") % QIcon.themeSearchPaths()) log.debug(_("Used theme name: '%s'") % QIcon.themeName()) self.setObjectName("main_window") self._subtitleData = DataController(parser, self) self.__initGui() self.__initActions() self.__initMenuBar() self.__initShortcuts() self.__updateMenuItemsState() self.__connectSignals() self.restoreWidgetState() self.handleArgs(args) def __initGui(self): self._settings = SubSettings() self.mainWidget = QWidget(self) mainLayout = QVBoxLayout() mainLayout.setContentsMargins(3, 3, 3, 3) mainLayout.setSpacing(2) self.setCentralWidget(self.mainWidget) self._videoWidget = VideoWidget(self) self._tabs = SubtitleWindow.SubTabWidget(self._subtitleData, self._videoWidget) mainLayout.addWidget(self._videoWidget, 1) mainLayout.addWidget(self._tabs, 3) self.statusBar() self.mainWidget.setLayout(mainLayout) self.setAcceptDrops(True) self.setWindowIcon(QIcon(":/img/logo.png")) self.setWindowTitle('Subconvert') def __connectSignals(self): self._tabs.tabChanged.connect(self.__updateMenuItemsState) self._tabs.tabChanged.connect(self.__updateWindowTitle) self._tabs.fileList.selectionChanged.connect(self.__updateMenuItemsState) self._subtitleData.fileAdded.connect(self.__updateMenuItemsState, Qt.QueuedConnection) self._subtitleData.fileChanged.connect(self.__updateMenuItemsState, Qt.QueuedConnection) self._subtitleData.fileRemoved.connect(self.__updateMenuItemsState, Qt.QueuedConnection) self._subtitleData.subtitlesAdded.connect(self.__updateMenuItemsState, Qt.QueuedConnection) self._subtitleData.subtitlesRemoved.connect( self.__updateMenuItemsState, Qt.QueuedConnection) self._subtitleData.subtitlesChanged.connect( self.__updateMenuItemsState, Qt.QueuedConnection) def __initActions(self): self._actions = {} af = ActionFactory(self) # open / save self._actions["openFile"] = af.create( "document-open", _("&Open"), _("Open subtitle file."), "ctrl+o", self.openFile) self._actions["saveFile"] = af.create( "document-save", _("&Save"), _("Save current file."), "ctrl+s", self.saveFile) self._actions["saveFileAs"] = af.create( "document-save-as",_("S&ave as..."), _("Save current file as..."), "ctrl++shift+s", self.saveFileAs) self._actions["saveAllFiles"] = af.create( "document-save", _("Sa&ve all"), _("Save all opened files."), None, self.saveAll) # app exit self._actions["exit"] = af.create( "application-exit", _("&Exit"), _("Exit Subconvert."), None, qApp.quit) # tab management self._actions["nextTab"] = af.create( None, None, None, "ctrl+tab", self.nextTab) self._actions["previousTab"] = af.create( None, None, None, "ctrl+shift+tab", self.previousTab) self._actions["closeTab"] = af.create( None, None, None, "ctrl+w", self.closeTab) # Subtitles self._actions["undo"] = af.create( "undo", _("&Undo"), None, "ctrl+z", self.undo) self._actions["redo"] = af.create( "redo", _("&Redo"), None, "ctrl+shift+z", self.redo) for fps in FPS_VALUES: fpsStr = str(fps) self._actions[fpsStr] = af.create( None, fpsStr, None, None, lambda _, fps=fps: self.changeFps(fps)) for encoding in ALL_ENCODINGS: self._actions["in_%s" % encoding] = af.create( None, encoding, None, None, lambda _, enc = encoding: self.changeInputEncoding(enc)) self._actions["out_%s" % encoding] = af.create( None, encoding, None, None, lambda _, enc = encoding: self.changeOutputEncoding(enc)) for fmt in self._subtitleData.supportedFormats: self._actions[fmt.NAME] = af.create( None, fmt.NAME, None, None, lambda _, fmt = fmt: self.changeSubFormat(fmt)) self._actions["linkVideo"] = af.create( None, _("&Link video"), None, "ctrl+l", self.linkVideo) self._actions["unlinkVideo"] = af.create( None, _("U&nlink video"), None, "ctrl+u", lambda: self._setVideoLink(None)) self._actions["fpsFromMovie"] = af.create( None, _("&Get FPS"), None, "ctrl+g", self.getFpsFromMovie) self._actions["insertSub"] = af.create( "list-add", _("&Insert subtitle"), None, "insert", connection = lambda: self._tabs.currentPage().insertNewSubtitle()) self._actions["addSub"] = af.create( "list-add", _("&Add subtitle"), None, "alt+insert", connection = lambda: self._tabs.currentPage().addNewSubtitle()) self._actions["offset"] = af.create( None, _("&Offset"), None, None, self.offset) self._actions["removeSub"] = af.create( "list-remove", _("&Remove subtitles"), None, "delete", connection = lambda: self._tabs.currentPage().removeSelectedSubtitles()) self._actions["findSub"] = af.create( "edit-find", _("&Find..."), None, "ctrl+f", connection = lambda: self._tabs.currentPage().highlight()) # Video self._videoRatios = [(4, 3), (14, 9), (14, 10), (16, 9), (16, 10)] self._actions["openVideo"] = af.create( "document-open", _("&Open video"), None, "ctrl+m", self.openVideo) self._actions["togglePlayback"] = af.create( "media-playback-start", _("&Play/pause"), _("Toggle video playback"), "space", self._videoWidget.togglePlayback) self._actions["forward"] = af.create( "media-skip-forward", _("&Forward"), None, "ctrl+right", self._videoWidget.forward) self._actions["rewind"] = af.create( "media-skip-backward", _("&Rewind"), None, "ctrl+left", self._videoWidget.rewind) self._actions["frameStep"] = af.create( None, _("Next &frame"), _("Go to the next frame in a video"), ".", self._videoWidget.nextFrame) for ratio in self._videoRatios: self._actions["changeRatio_%d_%d" % ratio] = af.create( None, "%d:%d" % ratio, None, None, lambda _, r=ratio: self._videoWidget.changePlayerAspectRatio(r[0], r[1])) self._actions["changeRatio_fill"] = af.create( None, _("Fill"), None, None, self._videoWidget.fillPlayer) self._actions["videoJump"] = af.create( None, _("&Jump to subtitle"), None, "ctrl+j", self.jumpToSelectedSubtitle) # SPF editor self._actions["spfEditor"] = af.create( "accessories-text-editor", _("Subtitle &Properties Editor"), None, None, self.openPropertyEditor) # View self._actions["togglePlayer"] = af.create( None, _("&Video player"), _("Show or hide video player"), "F3", self.togglePlayer) self._actions["togglePanel"] = af.create( None, _("Side &panel"), _("Show or hide left panel"), "F4", self._tabs.togglePanel) # Help self._actions["helpPage"] = af.create( "help-contents", _("&Help"), _("Open &help page"), "F1", self.openHelp) self._actions["aboutSubconvert"] = af.create( "help-about", _("&About Subconvert"), None, None, self.openAboutDialog) def __initMenuBar(self): menubar = self.menuBar() fileMenu = menubar.addMenu(_('&File')) fileMenu.addAction(self._actions["openFile"]) fileMenu.addSeparator() fileMenu.addAction(self._actions["saveFile"]) fileMenu.addAction(self._actions["saveFileAs"]) fileMenu.addAction(self._actions["saveAllFiles"]) fileMenu.addSeparator() fileMenu.addAction(self._actions["exit"]) subtitlesMenu = menubar.addMenu(_("&Subtitles")) subtitlesMenu.addAction(self._actions["undo"]) subtitlesMenu.addAction(self._actions["redo"]) subtitlesMenu.addSeparator() subtitlesMenu.addAction(self._actions["insertSub"]) subtitlesMenu.addAction(self._actions["addSub"]) subtitlesMenu.addAction(self._actions["removeSub"]) subtitlesMenu.addAction(self._actions["findSub"]) subtitlesMenu.addSeparator() self._fpsMenu = subtitlesMenu.addMenu(_("&Frames per second")) self._fpsMenu.addSeparator() for fps in FPS_VALUES: self._fpsMenu.addAction(self._actions[str(fps)]) self._subFormatMenu = subtitlesMenu.addMenu(_("Subtitles forma&t")) for fmt in self._subtitleData.supportedFormats: self._subFormatMenu.addAction(self._actions[fmt.NAME]) self._inputEncodingMenu = subtitlesMenu.addMenu(_("Input &encoding")) self._outputEncodingMenu = subtitlesMenu.addMenu(_("&Output encoding")) for encoding in ALL_ENCODINGS: self._inputEncodingMenu.addAction(self._actions["in_%s" % encoding]) self._outputEncodingMenu.addAction(self._actions["out_%s" % encoding]) subtitlesMenu.addAction(self._actions["offset"]) subtitlesMenu.addSeparator() subtitlesMenu.addAction(self._actions["linkVideo"]) subtitlesMenu.addAction(self._actions["unlinkVideo"]) subtitlesMenu.addAction(self._actions["fpsFromMovie"]) subtitlesMenu.addSeparator() videoMenu = menubar.addMenu(_("&Video")) videoMenu.addAction(self._actions["openVideo"]) videoMenu.addSeparator() playbackMenu = videoMenu.addMenu(_("&Playback")) playbackMenu.addAction(self._actions["togglePlayback"]) playbackMenu.addSeparator() playbackMenu.addAction(self._actions["forward"]) playbackMenu.addAction(self._actions["rewind"]) playbackMenu.addAction(self._actions["frameStep"]) self._ratioMenu = videoMenu.addMenu(_("&Aspect ratio")) for ratio in self._videoRatios: self._ratioMenu.addAction(self._actions["changeRatio_%d_%d" % ratio]) self._ratioMenu.addSeparator() self._ratioMenu.addAction(self._actions["changeRatio_fill"]) videoMenu.addSeparator() videoMenu.addAction(self._actions["videoJump"]) viewMenu = menubar.addMenu(_("Vie&w")) viewMenu.addAction(self._actions["togglePlayer"]) viewMenu.addAction(self._actions["togglePanel"]) toolsMenu = menubar.addMenu(_("&Tools")) toolsMenu.addAction(self._actions["spfEditor"]) helpMenu = menubar.addMenu(_("&Help")) helpMenu.addAction(self._actions["helpPage"]) helpMenu.addAction(self._actions["aboutSubconvert"]) def __initShortcuts(self): self.addAction(self._actions["nextTab"]) self.addAction(self._actions["previousTab"]) self.addAction(self._actions["closeTab"]) def cleanup(self): self._videoWidget.close() def __getAllSubExtensions(self): formats = self._subtitleData.supportedFormats exts = [_('Default')] exts.extend(set([ f.EXTENSION for f in formats ])) exts.sort() return exts def _writeFile(self, filePath, newFilePath=None): if newFilePath is None: newFilePath = filePath data = self._subtitleData.data(filePath) converter = SubConverter() content = converter.convert(data.outputFormat, data.subtitles) if File.exists(newFilePath): file_ = File(newFilePath) file_.overwrite(content, data.outputEncoding) else: File.write(newFilePath, content, data.outputEncoding) self._subtitleData.setCleanState(filePath) self.__updateMenuItemsState() def __updateWindowTitle(self): tab = self._tabs.currentPage() if tab.isStatic: self.setWindowTitle("Subconvert") else: self.setWindowTitle("%s - Subconvert" % tab.name) def _openFiles(self, paths, encoding): unsuccessfullFiles = [] for filePath in paths: # TODO: separate reading file and adding to the list. # TODO: there should be readDataFromFile(filePath, properties=None), # TODO: which should set default properties from Subtitle Properties File command = NewSubtitles(filePath, encoding) try: self._subtitleData.execute(command) except DoubleFileEntry: pass # file already opened except Exception as e: log.exception(e) unsuccessfullFiles.append("%s: %s" % (filePath, str(e))) if len(unsuccessfullFiles) > 0: dialog = CannotOpenFilesMsg(self) dialog.setFileList(unsuccessfullFiles) dialog.exec() def _setVideoLink(self, videoPath): currentTab = self._tabs.currentPage() if currentTab.isStatic: currentTab.changeSelectedFilesVideoPath(videoPath) else: currentTab.changeVideoPath(videoPath) @pyqtSlot() def __updateMenuItemsState(self): tab = self._tabs.currentPage() dataAvailable = self._subtitleData.count() != 0 anyTabOpen = tab is not None tabIsStatic = tab.isStatic if anyTabOpen else False if tabIsStatic: cleanState = False anyItemSelected = len(tab.selectedItems) > 0 else: cleanState = tab.history.isClean() anyItemSelected = False canUndo = (tabIsStatic and anyItemSelected) or (not tabIsStatic and tab.history.canUndo()) canRedo = (tabIsStatic and anyItemSelected) or (not tabIsStatic and tab.history.canRedo()) canEdit = (tabIsStatic and anyItemSelected) or (not tabIsStatic) self._actions["saveAllFiles"].setEnabled(dataAvailable) self._actions["saveFile"].setEnabled(not tabIsStatic and not cleanState) self._actions["saveFileAs"].setEnabled(not tabIsStatic) self._actions["undo"].setEnabled(canUndo) self._actions["redo"].setEnabled(canRedo) self._fpsMenu.setEnabled(canEdit) self._subFormatMenu.setEnabled(canEdit) self._inputEncodingMenu.setEnabled(canEdit) self._outputEncodingMenu.setEnabled(canEdit) self._actions["offset"].setEnabled(canEdit) self._actions["linkVideo"].setEnabled(canEdit) self._actions["unlinkVideo"].setEnabled(canEdit) self._actions["fpsFromMovie"].setEnabled(canEdit) self._actions["insertSub"].setEnabled(not tabIsStatic) self._actions["addSub"].setEnabled(not tabIsStatic) self._actions["removeSub"].setEnabled(not tabIsStatic) self._actions["findSub"].setEnabled(not tabIsStatic) self._actions["videoJump"].setEnabled(not tabIsStatic) def closeEvent(self, ev): self.saveWidgetState() def saveWidgetState(self): self._settings.setGeometry(self, self.saveGeometry()) self._settings.setState(self, self.saveState()) self._videoWidget.saveWidgetState(self._settings) self._tabs.saveWidgetState(self._settings) def restoreWidgetState(self): self.restoreGeometry(self._settings.getGeometry(self)) self.restoreState(self._settings.getState(self)) self._videoWidget.restoreWidgetState(self._settings) self._tabs.restoreWidgetState(self._settings) def handleArgs(self, args): self._openFiles(args.files, args.inputEncoding) def nextTab(self): if self._tabs.count() > 0: index = self._tabs.currentIndex() + 1 if index > self._tabs.count() - 1: index = 0 self._tabs.showTab(index) def previousTab(self): if self._tabs.count() > 0: index = self._tabs.currentIndex() - 1 if index < 0: index = self._tabs.count() - 1 self._tabs.showTab(index) def closeTab(self): if self._tabs.count() > 0: index = self._tabs.currentIndex() self._tabs.closeTab(index) def openFile(self): sub_extensions = self.__getAllSubExtensions() str_sub_exts = ' '.join(['*.%s' % ext for ext in sub_extensions[1:]]) fileDialog = FileDialog( parent = self, caption = _("Open file"), directory = self._settings.getLatestDirectory(), filter = _("Subtitles (%s);;All files (*)") % str_sub_exts ) fileDialog.addEncodings(True) fileDialog.setFileMode(QFileDialog.ExistingFiles) if fileDialog.exec(): filenames = fileDialog.selectedFiles() encoding = fileDialog.getEncoding() self._settings.setLatestDirectory(os.path.dirname(filenames[0])) self._openFiles(filenames, encoding) @pyqtSlot() def saveFile(self, newFilePath = None): currentTab = self._tabs.currentPage() try: self._writeFile(currentTab.filePath, newFilePath) except SubException as msg: dialog = QMessageBox(self) dialog.setIcon(QMessageBox.Critical) dialog.setWindowTitle(_("Couldn't save file")) dialog.setText(str(msg)) dialog.exec() @pyqtSlot() def saveFileAs(self): fileDialog = FileDialog( parent = self, caption = _('Save as...'), directory = self._settings.getLatestDirectory() ) currentTab = self._tabs.currentPage() fileDialog.addFormats(self._subtitleData.supportedFormats) fileDialog.setSubFormat(currentTab.outputFormat) fileDialog.addEncodings(False) fileDialog.setEncoding(currentTab.outputEncoding) fileDialog.setAcceptMode(QFileDialog.AcceptSave) fileDialog.setFileMode(QFileDialog.AnyFile) if fileDialog.exec(): newFilePath = fileDialog.selectedFiles()[0] data = currentTab.data outputFormat = fileDialog.getSubFormat() outputEncoding = fileDialog.getEncoding() # user can overwrite previous output encoding if data.outputFormat != outputFormat or data.outputEncoding != outputEncoding: # save user changes data.outputFormat = outputFormat data.outputEncoding = outputEncoding if self._subtitleData.fileExists(newFilePath): command = ChangeData(newFilePath, data, _("Overwritten by %s") % currentTab.name) else: command = CreateSubtitlesFromData(newFilePath, data) self._subtitleData.execute(command) self._tabs.openTab(newFilePath) self.saveFile(newFilePath) self._settings.setLatestDirectory(os.path.dirname(newFilePath)) def saveAll(self): dialog = MessageBoxWithList(self) dialog.setIcon(QMessageBox.Critical) # TODO # When asked to save file, it should be should checked if it's parsed and parse it if it's # not (in future the parsing moment might be moved to increase responsibility when opening # a lot of files - i.e. only a file list will be printed and files will be actually parsed # on their first use (e.g. on tab open, fps change etc.). I'll have to think about it). # END OF TODO for filePath in self._tabs.fileList.filePaths: try: if not self._subtitleData.isCleanState(filePath): self._writeFile(filePath) except SubException as msg: dialog.addToList(str(msg)) if dialog.listCount() > 0: dialog.setWindowTitle(P_( "Error on saving a file", "Error on saving files", dialog.listCount() )) dialog.setText(P_( "Following error occured when trying to save a file:", "Following errors occured when trying to save files:", dialog.listCount() )) dialog.exec() def undo(self): currentTab = self._tabs.currentPage() if currentTab.isStatic: currentTab.undoSelectedFiles() else: currentTab.history.undo() def redo(self): currentTab = self._tabs.currentPage() if currentTab.isStatic: currentTab.redoSelectedFiles() else: currentTab.history.redo() def changeInputEncoding(self, encoding): currentTab = self._tabs.currentPage() if currentTab.isStatic: currentTab.changeSelectedFilesInputEncoding(encoding) else: currentTab.changeInputEncoding(encoding) def changeOutputEncoding(self, encoding): currentTab = self._tabs.currentPage() if currentTab.isStatic: currentTab.changeSelectedFilesOutputEncoding(encoding) else: currentTab.changeOutputEncoding(encoding) def changeSubFormat(self, fmt): currentTab = self._tabs.currentPage() if currentTab.isStatic: currentTab.changeSelectedFilesFormat(fmt) else: currentTab.changeSubFormat(fmt) def offset(self): currentTab = self._tabs.currentPage() # fps isn't used, but we need one to init starting FrameTime dialog = OffsetDialog(self, FrameTime(25, seconds=0)) if dialog.exec(): if currentTab.isStatic: currentTab.offsetSelectedFiles(dialog.frametime.fullSeconds) else: currentTab.offset(dialog.frametime.fullSeconds) def changeFps(self, fps): currentTab = self._tabs.currentPage() if currentTab.isStatic: currentTab.changeSelectedFilesFps(fps) else: currentTab.changeFps(fps) def togglePlayer(self): if self._videoWidget.isHidden(): self._videoWidget.show() else: self._videoWidget.hide() def getFpsFromMovie(self): currentTab = self._tabs.currentPage() if currentTab.isStatic: currentTab.detectSelectedFilesFps() else: currentTab.detectFps() def openVideo(self): movieExtensions = "%s%s" % ("*.", ' *.'.join(File.MOVIE_EXTENSIONS)) fileDialog = FileDialog( parent = self, caption = _("Select a video"), directory = self._settings.getLatestDirectory(), filter = _("Video files (%s);;All files (*)") % movieExtensions) fileDialog.setFileMode(QFileDialog.ExistingFile) if fileDialog.exec(): movieFilePath = fileDialog.selectedFiles()[0] self._videoWidget.openFile(movieFilePath) def jumpToSelectedSubtitle(self): currentTab = self._tabs.currentPage() subtitleList = currentTab.selectedSubtitles() if len(subtitleList) > 0: self._videoWidget.jumpTo(subtitleList[0].start) def openPropertyEditor(self): editor = PropertyFileEditor(self._subtitleData.supportedFormats, self) editor.exec() def openAboutDialog(self): spacer = QSpacerItem(650, 0) dialog = QMessageBox(self) dialog.setIconPixmap(QPixmap(":/img/logo.png")) dialog.setWindowTitle(_("About Subconvert")) dialog.setText(aboutText) dialogLayout = dialog.layout() dialogLayout.addItem(spacer, dialogLayout.rowCount(), 0, 1, dialogLayout.columnCount()) dialog.exec() def openHelp(self): url = "https://github.com/mgoral/subconvert/wiki" if QDesktopServices.openUrl(QUrl(url)) is False: dialog = QMessageBox(self) dialog.setIcon(QMessageBox.Critical) dialog.setWindowTitle(_("Couldn't open URL")) dialog.setText(_("""Failed to open URL: <a href="%(url)s">%(url)s</a>.""") % {"url": url}) dialog.exec() def dragEnterEvent(self, event): mime = event.mimeData() # search for at least local file in dragged urls (and accept drag event only in that case) if mime.hasUrls(): urls = mime.urls() for url in urls: if url.isLocalFile() and os.path.isfile(url.toLocalFile()): event.acceptProposedAction() return def dropEvent(self, event): mime = event.mimeData() urls = mime.urls() subPaths = [] moviePaths = [] for url in urls: if url.isLocalFile(): filePath = url.toLocalFile() if not os.path.isdir(filePath): fileName, fileExtension = os.path.splitext(filePath) if fileExtension.strip('.').lower() in File.MOVIE_EXTENSIONS: moviePaths.append(filePath) else: subPaths.append(filePath) # open all subtitles and only the first movie if len(moviePaths) > 0: self._videoWidget.openFile(moviePaths[0]) if len(subPaths) > 0: self._openFiles(subPaths, None) def linkVideo(self): movieExtensions = "%s%s" % ("*.", ' *.'.join(File.MOVIE_EXTENSIONS)) fileDialog = FileDialog( parent = self, caption = _("Select a video"), directory = self._settings.getLatestDirectory(), filter = _("Video files (%s);;All files (*)") % movieExtensions) fileDialog.setFileMode(QFileDialog.ExistingFile) if fileDialog.exec(): movieFilePath = fileDialog.selectedFiles()[0] self._setVideoLink(movieFilePath)