class PropertyFileEditor(QDialog): def __init__(self, subFormats, parent = None): super().__init__(parent) self._settings = SubSettings() self.__createPropertyFilesDirectory() self.__initSubFormats(subFormats) self.__initGui() def __createPropertyFilesDirectory(self): pfileDir = self._settings.getPropertyFilesPath() try: os.makedirs(pfileDir) except OSError as exc: if exc.errno == errno.EEXIST and os.path.isdir(pfileDir): pass else: raise def __initSubFormats(self, formats): self._formats = {} for f in formats: self._formats[f.NAME] = f def __initGui(self): layout = QVBoxLayout() layout.setSpacing(10) layout.addWidget(self._createFpsBox()) layout.addWidget(self._createEncodingBox()) layout.addWidget(self._createFormatBox()) layout.addWidget(self._createButtons()) self.setLayout(layout) self.setWindowTitle(_("Subtitle Properties Editor")) self.setModal(True) # Some signals self._closeButton.clicked.connect(self.close) self._openButton.clicked.connect(self.openProperties) self._saveButton.clicked.connect(self.saveProperties) self._autoEncoding.toggled.connect(self._inputEncoding.setDisabled) self._changeEncoding.toggled.connect(self._outputEncoding.setEnabled) def _createFpsBox(self): groupbox = QGroupBox(_("FPS")) layout = QHBoxLayout() self._autoFps = QCheckBox(_("Auto FPS"), self) self._fps = QComboBox(self) self._fps.addItems(["23.976", "24", "25", "29.97", "30"]) self._fps.setEditable(True) layout.addWidget(self._autoFps) layout.addWidget(self._fps) groupbox.setLayout(layout) return groupbox def _createEncodingBox(self): groupbox = QGroupBox(_("File Encoding")) layout = QGridLayout() self._autoEncoding = QCheckBox(_("Auto input encoding"), self) self._inputEncoding = QComboBox(self) self._inputEncoding.addItems(ALL_ENCODINGS) self._inputEncoding.setDisabled(self._autoEncoding.isChecked()) inputLabel = QLabel(_("Input encoding")) self._changeEncoding = QCheckBox(_("Change encoding on save"), self) self._outputEncoding = QComboBox(self) self._outputEncoding.addItems(ALL_ENCODINGS) self._outputEncoding.setEnabled(self._changeEncoding.isChecked()) outputLabel = QLabel(_("Output encoding")) layout.addWidget(self._autoEncoding, 0, 0) layout.addWidget(self._inputEncoding, 1, 0) layout.addWidget(inputLabel, 1, 1) layout.addWidget(self._changeEncoding, 2, 0) layout.addWidget(self._outputEncoding, 3, 0) layout.addWidget(outputLabel, 3, 1) groupbox.setLayout(layout) return groupbox def _createFormatBox(self): groupbox = QGroupBox(_("Subtitle format")) layout = QGridLayout() displayedFormats = list(self._formats.keys()) displayedFormats.sort() self._outputFormat = QComboBox(self) self._outputFormat.addItems(displayedFormats) formatLabel = QLabel(_("Output format")) layout.addWidget(self._outputFormat, 0, 0) layout.addWidget(formatLabel, 0, 1) groupbox.setLayout(layout) return groupbox def _createButtons(self): widget = QWidget(self) layout = QHBoxLayout() self._openButton = QPushButton(_("Open")) self._saveButton = QPushButton(_("Save")) self._closeButton = QPushButton(_("Close")) layout.addWidget(self._openButton) layout.addWidget(self._saveButton) layout.addWidget(self._closeButton) widget.setLayout(layout) return widget def _createSubtitleProperties(self): subProperties = SubtitleProperties(list(self._formats.values())) subProperties.autoFps = self._autoFps.isChecked() subProperties.fps = self._fps.currentText() subProperties.autoInputEncoding = self._autoEncoding.isChecked() subProperties.changeEncoding = self._changeEncoding.isChecked() subProperties.inputEncoding = self._inputEncoding.currentText() subProperties.outputEncoding = self._outputEncoding.currentText() subProperties.outputFormat = self._formats.get(self._outputFormat.currentText()) return subProperties def changeProperties(self, subProperties): self._autoFps.setChecked(subProperties.autoFps) self._fps.setEditText(str(subProperties.fps)) self._autoEncoding.setChecked(subProperties.autoInputEncoding) self._changeEncoding.setChecked(subProperties.changeEncoding) self._inputEncoding.setCurrentIndex( self._inputEncoding.findText(subProperties.inputEncoding)) self._outputEncoding.setCurrentIndex( self._outputEncoding.findText(subProperties.outputEncoding)) if self._formats.get(subProperties.outputFormat.NAME) is subProperties.outputFormat: self._outputFormat.setCurrentIndex( self._outputFormat.findText(subProperties.outputFormat.NAME)) else: self.close() raise RuntimeError(_("Subtitle format (%s) doesn't match any of known formats!") % subProperties.outputFormat.NAME) def saveProperties(self): subProperties = None try: subProperties = self._createSubtitleProperties() except Exception as e: dialog = QMessageBox(self) dialog.setIcon(QMessageBox.Critical) dialog.setWindowTitle(_("Incorrect value")) dialog.setText(_("Could not save SPF file because of incorrect parameters.")); dialog.setDetailedText(str(e)); dialog.exec() return fileDialog = FileDialog( parent = self, caption = _('Save Subtitle Properties'), directory = self._settings.getPropertyFilesPath() ) fileDialog.setAcceptMode(QFileDialog.AcceptSave) fileDialog.setFileMode(QFileDialog.AnyFile) if fileDialog.exec(): filename = fileDialog.selectedFiles()[0] if not filename.endswith(".spf"): filename = "%s%s" % (filename, ".spf") self._settings.setPropertyFilesPath(os.path.dirname(filename)) subProperties.save(filename) self._settings.addPropertyFile(filename) self.close() def openProperties(self): fileDialog = FileDialog( parent = self, caption = _("Open Subtitle Properties"), directory = self._settings.getPropertyFilesPath(), filter = _("Subtitle Properties (*.spf);;All files (*)") ) fileDialog.setFileMode(QFileDialog.ExistingFile) if fileDialog.exec(): filename = fileDialog.selectedFiles()[0] self._settings.setPropertyFilesPath(os.path.dirname(filename)) subProperties = SubtitleProperties(list(self._formats.values()), filename) self.changeProperties(subProperties)
class FileList(SubTab): requestOpen = pyqtSignal(str, bool) requestRemove = pyqtSignal(str) selectionChanged = pyqtSignal() def __init__(self, name, subtitleData, parent = None): super(FileList, self).__init__(name, parent) self._subtitleData = subtitleData self._settings = SubSettings() self.__initGui() self.__connectSignals() def __initGui(self): self._contextMenu = None mainLayout = QVBoxLayout(self) mainLayout.setContentsMargins(0, 3, 0, 0) mainLayout.setSpacing(0) self.__fileList = SubtitleList() fileListHeader = self.__fileList.header() self.__resizeHeader(fileListHeader) self.__fileList.setSelectionMode(QAbstractItemView.ExtendedSelection) self.__fileList.setColumnCount(4) self.__fileList.setHeaderLabels([ _("File name"), _("Input encoding"), _("Output encoding"), _("Subtitle format"), _("FPS")]) mainLayout.addWidget(self.__fileList) self.setContextMenuPolicy(Qt.CustomContextMenu) self.setLayout(mainLayout) def __resizeHeader(self, header): # TODO: add an option (in subconvert settings) to set the following: # header.setResizeMode(0, QHeaderView.ResizeToContents); header.setStretchLastSection(False) header.setDefaultSectionSize(130) header.resizeSection(0, 500) def __initContextMenu(self): if self._contextMenu is not None: self._contextMenu.deleteLater() self._contextMenu = None self._contextMenu = QMenu() af = ActionFactory(self) selectedItems = self.__fileList.selectedItems() anyItemSelected = len(selectedItems) > 0 # Open in tab actionOpenInTab = af.create( icon = "window-new", title = _("&Open in tab"), connection = self.requestOpeningSelectedFiles) actionOpenInTab.setEnabled(anyItemSelected) self._contextMenu.addAction(actionOpenInTab) self._contextMenu.addSeparator() # Property Files pfileMenu = self._contextMenu.addMenu(_("Use Subtitle &Properties")) pfileMenu.setEnabled(anyItemSelected) for pfile in self._settings.getLatestPropertyFiles(): # A hacky way to store pfile in lambda action = af.create( title = pfile, connection = lambda _, pfile=pfile: self._useSubProperties(pfile) ) pfileMenu.addAction(action) pfileMenu.addSeparator() pfileMenu.addAction(af.create( title = _("Open file"), connection = self._chooseSubProperties)) self._contextMenu.addSeparator() # Single properties fpsMenu = self._contextMenu.addMenu(_("&Frames per second")) fpsMenu.setEnabled(anyItemSelected) for fps in FPS_VALUES: fpsStr = str(fps) action = af.create( title = fpsStr, connection = lambda _, fps=fps: self.changeSelectedFilesFps(fps)) fpsMenu.addAction(action) formatsMenu = self._contextMenu.addMenu(_("Subtitles forma&t")) formatsMenu.setEnabled(anyItemSelected) for fmt in self._subtitleData.supportedFormats: action = af.create( title = fmt.NAME, connection = lambda _, fmt=fmt: self.changeSelectedFilesFormat(fmt) ) formatsMenu.addAction(action) inputEncodingsMenu = self._contextMenu.addMenu(_("Input &encoding")) inputEncodingsMenu.setEnabled(anyItemSelected) outputEncodingsMenu = self._contextMenu.addMenu(_("&Output encoding")) outputEncodingsMenu.setEnabled(anyItemSelected) for encoding in ALL_ENCODINGS: outAction = af.create( title = encoding, connection = lambda _, enc=encoding: self.changeSelectedFilesOutputEncoding(enc) ) outputEncodingsMenu.addAction(outAction) inAction = af.create( title = encoding, connection = lambda _, enc=encoding: self.changeSelectedFilesInputEncoding(enc) ) inputEncodingsMenu.addAction(inAction) offset = af.create(None, _("&Offset"), None, None, self._offsetDialog) offset.setEnabled(anyItemSelected) self._contextMenu.addAction(offset) self._contextMenu.addSeparator() # Link/unlink video actionLink = af.create(None, _("&Link video"), None, None, self.linkVideo) actionLink.setEnabled(anyItemSelected) self._contextMenu.addAction(actionLink) actionLink = af.create( None, _("U&nlink video"), None, None, lambda: self.changeSelectedFilesVideoPath(None)) actionLink.setEnabled(anyItemSelected) self._contextMenu.addAction(actionLink) actionLink = af.create(None, _("&Get FPS"), None, None, self.detectSelectedFilesFps) actionLink.setEnabled(anyItemSelected) self._contextMenu.addAction(actionLink) self._contextMenu.addSeparator() # Show/Remove files # Key shortcuts are actually only a hack to provide some kind of info to user that he can # use "enter/return" and "delete" to open/close subtitles. Keyboard is handled via # keyPressed -> _handleKeyPress. This is because __fileList has focus most of time anyway # (I think...) actionOpen = af.create( None, _("&Show subtitles"), None, "Enter", lambda: self._handleKeyPress(Qt.Key_Enter)) actionOpen.setEnabled(anyItemSelected) self._contextMenu.addAction(actionOpen) actionClose = af.create( None, _("&Close subtitles"), None, "Delete", lambda: self._handleKeyPress(Qt.Key_Delete)) actionClose.setEnabled(anyItemSelected) self._contextMenu.addAction(actionClose) self._contextMenu.addSeparator() # Undo/redo actionUndo = af.create("undo", _("&Undo"), None, None, self.undoSelectedFiles) actionUndo.setEnabled(anyItemSelected) self._contextMenu.addAction(actionUndo) actionRedo = af.create("redo", _("&Redo"), None, None, self.redoSelectedFiles) actionRedo.setEnabled(anyItemSelected) self._contextMenu.addAction(actionRedo) def __connectSignals(self): self.__fileList.mouseButtonDoubleClicked.connect(self._handleDoubleClick) self.__fileList.mouseButtonClicked.connect(self._handleClick) self.__fileList.keyPressed.connect(self._handleKeyPress) self.__fileList.selectionModel().selectionChanged.connect(self._selectionChangedHandle) self.customContextMenuRequested.connect(self._showContextMenu) self._subtitleData.fileAdded.connect(self._addFile) self._subtitleData.fileRemoved.connect(self._removeFile) self._subtitleData.fileChanged.connect(self._updateFile) def _addFile(self, filePath): data = self._subtitleData.data(filePath) item = QTreeWidgetItem( [filePath, data.inputEncoding, data.outputEncoding, data.outputFormat.NAME, str(data.fps)]) item.setToolTip(0, filePath) subtitleIcon = QIcon(":/img/ok.png") item.setIcon(0, subtitleIcon) videoIcon = QIcon(":/img/film.png") if data.videoPath is not None else QIcon() item.setIcon(4, videoIcon) self.__fileList.addTopLevelItem(item) self._subtitleData.history(filePath).cleanChanged.connect( lambda clean: self._cleanStateChanged(filePath, clean)) def _removeFile(self, filePath): items = self.__fileList.findItems(filePath, Qt.MatchExactly) for item in items: index = self.__fileList.indexOfTopLevelItem(item) toDelete = self.__fileList.takeTopLevelItem(index) toDelete = None def _updateFile(self, filePath): items = self.__fileList.findItems(filePath, Qt.MatchExactly) if len(items) > 0: data = self._subtitleData.data(filePath) for item in items: item.setText(1, data.inputEncoding) item.setText(2, data.outputEncoding) item.setText(3, data.outputFormat.NAME) item.setText(4, str(data.fps)) videoIcon = QIcon(":/img/film.png") if data.videoPath is not None else QIcon() item.setIcon(4, videoIcon) def _cleanStateChanged(self, filePath, clean): items = self.__fileList.findItems(filePath, Qt.MatchExactly) for item in items: if clean: icon = QIcon(":/img/ok.png") else: icon = QIcon(":/img/not_clean.png") item.setIcon(0, icon) def _selectionChangedHandle(self, selected, deselected): self.selectionChanged.emit() @property def selectedItems(self): return self.__fileList.selectedItems() def canClose(self): return False @property def isStatic(self): return True def getCurrentFile(self): return self.__fileList.currentItem() def _handleClick(self, button): item = self.__fileList.currentItem() if item is not None and button == Qt.MiddleButton: self.requestOpen.emit(item.text(0), True) def _handleDoubleClick(self, button): item = self.__fileList.currentItem() if item is not None and button == Qt.LeftButton: self.requestOpen.emit(item.text(0), False) def _handleKeyPress(self, key): items = self.__fileList.selectedItems() if key in (Qt.Key_Enter, Qt.Key_Return): for item in items: self.requestOpen.emit(item.text(0), False) elif key == Qt.Key_Delete: for item in items: self.requestRemove.emit(item.text(0)) def _showContextMenu(self): self.__initContextMenu() # redraw menu self._contextMenu.exec(QCursor.pos()) def changeSelectedSubtitleProperties(self, subProperties): # TODO: indicate the change somehow items = self.__fileList.selectedItems() applier = PropertiesFileApplier(subProperties) for item in items: filePath = item.text(0) data = self._subtitleData.data(filePath) applier.applyFor(filePath, data) command = ChangeData(filePath, data, _("Property file: %s") % filePath) self._subtitleData.execute(command) def _chooseSubProperties(self): fileDialog = FileDialog( parent = self, caption = _("Open Subtitle Properties"), directory = self._settings.getPropertyFilesPath(), filter = _("Subtitle Properties (*.spf);;All files (*)") ) fileDialog.setFileMode(QFileDialog.ExistingFile) if fileDialog.exec(): filename = fileDialog.selectedFiles()[0] self._useSubProperties(filename) def _useSubProperties(self, propertyPath): if propertyPath: try: subProperties = SubtitleProperties( self._subtitleData.supportedFormats, propertyPath) except: log.error(_("Cannot read %s as Subtitle Property file.") % propertyPath) self._settings.removePropertyFile(propertyPath) return # Don't change the call order. We don't want to change settings or redraw context menu # if something goes wrong. self.changeSelectedSubtitleProperties(subProperties) self._settings.addPropertyFile(propertyPath) @property def filePaths(self): fileList = self.__fileList # shorten notation return [fileList.topLevelItem(i).text(0) for i in range(fileList.topLevelItemCount())] 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.changeSelectedFilesVideoPath(movieFilePath) def requestOpeningSelectedFiles(self): # Open all files, but focus only the last one items = self.__fileList.selectedItems() for item in items: self.requestOpen.emit(item.text(0), True) self.requestOpen.emit(items[-1].text(0), False) def changeSelectedFilesFps(self, fps): items = self.__fileList.selectedItems() for item in items: filePath = item.text(0) data = self._subtitleData.data(filePath) if data.fps != fps: data.subtitles.changeFps(fps) data.fps = fps command = ChangeData(filePath, data, _("FPS: %s") % fps) self._subtitleData.execute(command) def changeSelectedFilesVideoPath(self, path): items = self.__fileList.selectedItems() for item in items: filePath = item.text(0) data = self._subtitleData.data(filePath) if data.videoPath != path: data.videoPath = path command = ChangeData(filePath, data, _("Video path: %s") % path) self._subtitleData.execute(command) def _offsetDialog(self): dialog = OffsetDialog(self, FrameTime(25, seconds=0)) if dialog.exec(): self.offsetSelectedFiles(dialog.frametime.fullSeconds) def offsetSelectedFiles(self, seconds): if seconds == 0: return items = self.__fileList.selectedItems() for item in items: filePath = item.text(0) data = self._subtitleData.data(filePath) fps = data.subtitles.fps if fps is None: log.error(_("No FPS for '%s' (empty subtitles)." % filePath)) continue ft = FrameTime(fps, seconds=seconds) data.subtitles.offset(ft) command = ChangeData(filePath, data, _("Offset by: %s") % ft.toStr()) self._subtitleData.execute(command) def detectSelectedFilesFps(self): items = self.__fileList.selectedItems() for item in items: filePath = item.text(0) data = self._subtitleData.data(filePath) if data.videoPath is not None: fpsInfo = File.detectFpsFromMovie(data.videoPath) if data.videoPath != fpsInfo.videoPath or data.fps != fpsInfo.fps: data.videoPath = fpsInfo.videoPath data.subtitles.changeFps(fpsInfo.fps) data.fps = fpsInfo.fps command = ChangeData(filePath, data, _("Detected FPS: %s") % data.fps) self._subtitleData.execute(command) def changeSelectedFilesFormat(self, fmt): items = self.__fileList.selectedItems() for item in items: filePath = item.text(0) data = self._subtitleData.data(filePath) if data.outputFormat != fmt: data.outputFormat = fmt command = ChangeData(filePath, data, _("Format: %s ") % fmt.NAME) self._subtitleData.execute(command) def changeSelectedFilesInputEncoding(self, inputEncoding): items = self.__fileList.selectedItems() for item in items: filePath = item.text(0) data = self._subtitleData.data(filePath) if data.inputEncoding != inputEncoding: try: data.encode(inputEncoding) except UnicodeDecodeError: # TODO: indicate with something more than log entry log.error(_("Cannot decode subtitles to '%s' encoding.") % inputEncoding) else: command = ChangeData(filePath, data, _("Input encoding: %s") % inputEncoding) self._subtitleData.execute(command) def changeSelectedFilesOutputEncoding(self, outputEncoding): items = self.__fileList.selectedItems() for item in items: filePath = item.text(0) data = self._subtitleData.data(filePath) if data.outputEncoding != outputEncoding: data.outputEncoding = outputEncoding command = ChangeData(filePath, data, _("Output encoding: %s") % outputEncoding) self._subtitleData.execute(command) def undoSelectedFiles(self): items = self.__fileList.selectedItems() for item in items: filePath = item.text(0) history = self._subtitleData.history(filePath) if history.canUndo(): history.undo() def redoSelectedFiles(self): items = self.__fileList.selectedItems() for item in items: filePath = item.text(0) history = self._subtitleData.history(filePath) if history.canRedo(): history.redo()