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)
Exemple #2
0
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()