예제 #1
0
파일: document.py 프로젝트: vi/enki
class Document(QWidget):
    """
    Base class for documents on workspace, such as opened source file, Qt Designer and Qt Assistant, ...
    Inherit this class, if you want to create new document type

    This class may requre redesign, if we need to add support for non-textual or non-unicode editor.
    DO redesign instead of doing dirty hacks
    """

    documentDataChanged = pyqtSignal()
    """
    documentDataChanged()

    **Signal** emitted, when document icon or toolTip has changed
    (i.e. document has been modified externally)
    """

    _EOL_CONVERTOR = {r'\r\n': '\r\n',
                      r'\n': '\n',
                      r'\r': '\r'}

    def __init__( self, parentObject, filePath, createNew=False):
        """Create editor and open file.
        If file is None or createNew is True, empty not saved file is created
        IO Exceptions are not catched, therefore, must be catched on upper level
        """
        QWidget.__init__( self, parentObject)
        self._neverSaved = filePath is None or createNew
        self._filePath = filePath
        self._externallyRemoved = False
        self._externallyModified = False
        # File opening should be implemented in the document classes

        self._fileWatcher = _FileWatcher(filePath)
        self._fileWatcher.modified.connect(self._onWatcherFileModified)
        self._fileWatcher.removed.connect(self._onWatcherFileRemoved)

        if filePath and self._neverSaved:
            core.mainWindow().appendMessage('New file "%s" is going to be created' % filePath, 5000)

        self.qutepart = Qutepart(self)

        self.qutepart.setStyleSheet('QPlainTextEdit {border: 0}')

        self.qutepart.userWarning.connect(lambda text: core.mainWindow().statusBar().showMessage(text, 5000))

        self._applyQpartSettings()
        core.uiSettingsManager().dialogAccepted.connect(self._applyQpartSettings)

        layout = QVBoxLayout(self)
        layout.setMargin(0)
        layout.addWidget(self.qutepart)
        self.setFocusProxy(self.qutepart)

        if not self._neverSaved:
            originalText = self._readFile(filePath)
            self.qutepart.text = originalText
        else:
            originalText = ''

        #autodetect eol, if need
        self._configureEolMode(originalText)

        self._tryDetectSyntax()

    def _tryDetectSyntax(self):
        if len(self.qutepart.lines) > (100 * 1000) and \
           self.qutepart.language() is None:
            """Qutepart uses too lot of memory when highlighting really big files
            It may crash the editor, so, do not highlight really big files.
            But, do not disable highlighting for files, which already was highlighted
            """
            return

        self.qutepart.detectSyntax(sourceFilePath=self.filePath(),
                                   firstLine=self.qutepart.lines[0])

    def del_(self):
        """Explicytly called destructor
        """
        self._fileWatcher.disable()

        # avoid emit on text change, document shall behave like it is already dead
        self.qutepart.document().modificationChanged.disconnect()
        self.qutepart.text = ''  # stop background highlighting, free memory

    def _onWatcherFileModified(self, modified):
        """File has been modified
        """
        self._externallyModified = modified
        self.documentDataChanged.emit()

    def _onWatcherFileRemoved(self, isRemoved):
        """File has been removed
        """
        self._externallyRemoved = isRemoved
        self.documentDataChanged.emit()

    def _readFile(self, filePath):
        """Read the file contents.
        Shows QMessageBox for UnicodeDecodeError, but raises IOError, if failed to read file
        """
        with open(filePath, 'rb') as openedFile:  # Exception is ok, raise it up
            self._filePath = os.path.abspath(filePath)  # abspath won't fail, if file exists
            data = openedFile.read()

        self._fileWatcher.setContents(data)

        try:
            text = unicode(data, 'utf8')
        except UnicodeDecodeError, ex:
            QMessageBox.critical(None,
                                 self.tr("Can not decode file"),
                                 filePath + '\n' +
                                 unicode(str(ex), 'utf8') +
                                 '\nProbably invalid encoding was set. ' +
                                 'You may corrupt your file, if saved it')
            text = unicode(data, 'utf8', 'replace')

        # Strip last EOL. It will be restored, when saving
        if text.endswith('\r\n'):
            text = text[:-2]
        elif text.endswith('\r') or text.endswith('\n'):
            text = text[:-1]

        return text
예제 #2
0
class Document(QWidget):
    """
    Document is a opened file representation.

    It contains file management methods and uses `Qutepart <http://qutepart.rtfd.org/>`_ as an editor widget.
    Qutepart is available as ``qutepart`` attribute.
    """

    documentDataChanged = pyqtSignal()
    """
    documentDataChanged()

    **Signal** emitted, when document icon or toolTip has changed
    (i.e. document has been modified externally)
    """

    _EOL_CONVERTOR = {r'\r\n': '\r\n', r'\n': '\n', r'\r': '\r'}

    def __init__(self, parentObject, filePath, createNew=False):
        """Create editor and open file.
        If file is None or createNew is True, empty not saved file is created
        IO Exceptions are not catched, therefore, must be catched on upper level
        """
        QWidget.__init__(self, parentObject)
        self._neverSaved = filePath is None or createNew
        self._filePath = filePath
        self._externallyRemoved = False
        self._externallyModified = False
        # File opening should be implemented in the document classes

        self._fileWatcher = _FileWatcher(filePath)
        self._fileWatcher.modified.connect(self._onWatcherFileModified)
        self._fileWatcher.removed.connect(self._onWatcherFileRemoved)

        self.qutepart = Qutepart(self)

        self.qutepart.setStyleSheet('QPlainTextEdit {border: 0}')

        self.qutepart.userWarning.connect(
            lambda text: core.mainWindow().statusBar().showMessage(text, 5000))

        self._applyQpartSettings()
        core.uiSettingsManager().dialogAccepted.connect(
            self._applyQpartSettings)

        layout = QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self.qutepart)
        self.setFocusProxy(self.qutepart)

        if not self._neverSaved:
            originalText = self._readFile(filePath)
            self.qutepart.text = originalText
        else:
            originalText = ''

        # autodetect eol, if need
        self._configureEolMode(originalText)

        self._tryDetectSyntax()

        QApplication.instance().installEventFilter(self)

    def _tryDetectSyntax(self):
        if len(self.qutepart.lines) > (100 * 1000) and \
           self.qutepart.language() is None:
            """Qutepart uses too lot of memory when highlighting really big files
            It may crash the editor, so, do not highlight really big files.
            But, do not disable highlighting for files, which already was highlighted
            """
            return

        self.qutepart.detectSyntax(sourceFilePath=self.filePath(),
                                   firstLine=self.qutepart.lines[0])

    def terminate(self):
        """Explicytly called destructor
        """
        self._fileWatcher.term()

        # avoid emitting signals, document shall behave like it is already dead
        self.qutepart.document().modificationChanged.disconnect()
        self.qutepart.cursorPositionChanged.disconnect()  #
        self.qutepart.textChanged.disconnect()

        self.qutepart.terminate()  # stop background highlighting, free memory
        sip.delete(self)

    @pyqtSlot(bool)
    def _onWatcherFileModified(self, modified):
        """File has been modified
        """
        self._externallyModified = modified
        self.documentDataChanged.emit()

    @pyqtSlot(bool)
    def _onWatcherFileRemoved(self, isRemoved):
        """File has been removed
        """
        self._externallyRemoved = isRemoved
        self.documentDataChanged.emit()

    def _readFile(self, filePath):
        """Read the file contents.
        Shows QMessageBox for UnicodeDecodeError
        """
        with open(filePath,
                  'rb') as openedFile:  # Exception is ok, raise it up
            self._filePath = os.path.abspath(
                filePath)  # abspath won't fail, if file exists
            data = openedFile.read()

        self._fileWatcher.setContents(data)

        try:
            text = str(data, 'utf8')
        except UnicodeDecodeError as ex:
            QMessageBox.critical(
                None, self.tr("Can not decode file"), filePath + '\n' +
                str(ex) + '\nProbably invalid encoding was set. ' +
                'You may corrupt your file, if saved it')
            text = str(data, 'utf8', 'replace')

        # Strip last EOL. Qutepart adds it when saving file
        if text.endswith('\r\n'):
            text = text[:-2]
        elif text.endswith('\r') or text.endswith('\n'):
            text = text[:-1]

        return text

    def isExternallyModified(self):
        """Check if document's file has been modified externally.

        This method does not do any file system access, but only returns cached info
        """
        return self._externallyModified

    def isExternallyRemoved(self):
        """Check if document's file has been deleted externally.

        This method does not do any file system access, but only returns cached info
        """
        return self._externallyRemoved

    def isNeverSaved(self):
        """Check if document has been created, but never has been saved on disk
        """
        return self._neverSaved

    def filePath(self):
        """Return the document file absolute path.

        ``None`` if not set (new document)"""
        return self._filePath

    def fileName(self):
        """Document file name without a path.

        ``None`` if not set (new document)"""
        if self._filePath:
            return os.path.basename(self._filePath)
        else:
            return None

    def setFilePath(self, newPath):
        """Change document file path.

        Used when saving first time, or on Save As action
        """
        core.workspace().documentClosed.emit(self)
        self._filePath = newPath
        self._fileWatcher.setPath(newPath)
        self._neverSaved = True
        core.workspace().documentOpened.emit(self)
        core.workspace().currentDocumentChanged.emit(self, self)

    def _stripTrailingWhiteSpace(self):
        lineHasTrailingSpace = ((line and line[-1].isspace())
                                for line in self.qutepart.lines)
        if any(lineHasTrailingSpace):
            with self.qutepart:
                for lineNo, line in enumerate(self.qutepart.lines):
                    if line and line[-1].isspace():
                        self.qutepart.lines[lineNo] = line.rstrip()
        else:
            pass  # Do not enter with statement, because it causes wrong textChanged signal

    def _saveToFs(self, filePath):
        """Low level method. Always saves file, even if not modified
        """
        # Create directory
        dirPath = os.path.dirname(filePath)
        if not os.path.exists(dirPath):
            try:
                os.makedirs(dirPath)
            except OSError as ex:
                error = str(ex)
                QMessageBox.critical(
                    None, self.tr("Cannot save file"),
                    self.tr("Cannot create directory '%s'. Error '%s'." %
                            (dirPath, error)))
                return

        text = self.qutepart.textForSaving()

        # Write file
        data = text.encode('utf8')

        self._fileWatcher.disable()
        try:
            with open(filePath, 'wb') as openedFile:
                openedFile.write(data)
            self._fileWatcher.setContents(data)
        except IOError as ex:
            QMessageBox.critical(None, self.tr("Cannot write to file"),
                                 str(ex))
            return
        finally:
            self._fileWatcher.enable()

        # Update states
        self._neverSaved = False
        self._externallyRemoved = False
        self._externallyModified = False
        self.qutepart.document().setModified(False)
        self.documentDataChanged.emit()

        if self.qutepart.language() is None:
            self._tryDetectSyntax()

    def saveFile(self):
        """Save the file to file system.

        Show QFileDialog if file name is not known.
        Return False, if user cancelled QFileDialog, True otherwise
        """
        # Get path
        if not self._filePath:
            path, _ = QFileDialog.getSaveFileName(self,
                                                  self.tr('Save file as...'))
            if path:
                self.setFilePath(path)
            else:
                return False

        if core.config()['Qutepart']['StripTrailingWhitespace']:
            self._stripTrailingWhiteSpace()

        self._saveToFs(self.filePath())
        return True

    def saveFileAs(self):
        """Ask for new file name with dialog. Save file
        """
        if self._filePath:
            default_filename = os.path.basename(self._filePath)
        else:
            default_filename = ''
        path, _ = QFileDialog.getSaveFileName(self, self.tr('Save file as...'),
                                              default_filename)
        if not path:
            return

        self.setFilePath(path)
        self._saveToFs(path)

    def reload(self):
        """Reload the file from the disk

        If child class reimplemented this method, it MUST call method of the parent class
        for update internal bookkeeping"""

        text = self._readFile(self.filePath())
        pos = self.qutepart.cursorPosition
        self.qutepart.text = text
        self._externallyModified = False
        self._externallyRemoved = False
        self.qutepart.cursorPosition = pos
        self.qutepart.centerCursor()

    def modelToolTip(self):
        """Tool tip for the opened files model
        """
        toolTip = self.filePath()

        if toolTip is None:
            return None

        if self.qutepart.document().isModified():
            toolTip += "<br/><font color='blue'>%s</font>" % self.tr(
                "Locally Modified")
        if self._externallyModified:
            toolTip += "<br/><font color='red'>%s</font>" % self.tr(
                "Externally Modified")
        if self._externallyRemoved:
            toolTip += "<br/><font color='red'>%s</font>" % self.tr(
                "Externally Deleted")
        return '<html>' + toolTip + '</html>'

    def modelIcon(self):
        """Icon for the opened files model
        """
        if self.isNeverSaved():  # never has been saved
            icon = "save.png"
        elif self._externallyRemoved and self.qutepart.document().isModified():
            icon = 'modified-externally-deleted.png'
        elif self._externallyRemoved:
            icon = "close.png"
        elif self._externallyModified and self.qutepart.document().isModified(
        ):
            icon = "modified-externally-modified.png"
        elif self._externallyModified:
            icon = "modified-externally.png"
        elif self.qutepart.document().isModified():
            icon = "save.png"
        else:
            icon = "transparent.png"
        return QIcon(":/enkiicons/" + icon)

    def invokeGoTo(self):
        """Show GUI dialog, go to line, if user accepted it
        """
        line = self.qutepart.cursorPosition[0]
        gotoLine, accepted = QInputDialog.getInt(
            self, self.tr("Go To Line..."),
            self.tr("Enter the line you want to go to:"), line, 1,
            len(self.qutepart.lines), 1)
        if accepted:
            gotoLine -= 1
            self.qutepart.cursorPosition = gotoLine, None
            self.setFocus()

    def printFile(self):
        """Print file
        """
        raise NotImplemented()

    def _configureEolMode(self, originalText):
        """Detect end of line mode automatically and apply detected mode
        """
        modes = set()
        for line in originalText.splitlines(True):
            if line.endswith('\r\n'):
                modes.add('\r\n')
            elif line.endswith('\n'):
                modes.add('\n')
            elif line.endswith('\r'):
                modes.add('\r')

        if len(modes) == 1:  # exactly one
            detectedMode = modes.pop()
        else:
            detectedMode = None

        default = self._EOL_CONVERTOR[core.config()["Qutepart"]["EOL"]["Mode"]]

        if len(modes) > 1:
            message = "%s contains mix of End Of Line symbols. It will be saved with '%s'" % \
                (self.filePath(), repr(default))
            core.mainWindow().appendMessage(message)
            self.qutepart.eol = default
            self.qutepart.document().setModified(True)
        elif core.config()["Qutepart"]["EOL"]["AutoDetect"]:
            if detectedMode is not None:
                self.qutepart.eol = detectedMode
            else:  # empty set, not detected
                self.qutepart.eol = default
        else:  # no mix, no autodetect. Force EOL
            if detectedMode is not None and \
                    detectedMode != default:
                message = "%s: End Of Line mode is '%s', but file will be saved with '%s'. " \
                          "EOL autodetection is disabled in the settings" % \
                    (self.fileName(), repr(detectedMode), repr(default))
                core.mainWindow().appendMessage(message)
                self.qutepart.document().setModified(True)

            self.qutepart.eol = default

    @pyqtSlot()
    def _applyQpartSettings(self):
        """Apply qutepart settings
        """
        conf = core.config()['Qutepart']
        self.qutepart.setFont(
            QFont(conf['Font']['Family'], conf['Font']['Size']))

        self.qutepart.indentUseTabs = conf['Indentation']['UseTabs']
        self.qutepart.indentWidth = conf['Indentation']['Width']

        if conf['Edge']['Enabled']:
            self.qutepart.lineLengthEdge = conf['Edge']['Column']
        else:
            self.qutepart.lineLengthEdge = None
        self.qutepart.lineLengthEdgeColor = QColor(conf['Edge']['Color'])

        self.qutepart.completionEnabled = conf['AutoCompletion']['Enabled']
        self.qutepart.completionThreshold = conf['AutoCompletion']['Threshold']

        self.qutepart.setLineWrapMode(
            QPlainTextEdit.
            WidgetWidth if conf['Wrap']['Enabled'] else QPlainTextEdit.NoWrap)
        if conf['Wrap']['Mode'] == 'WrapAtWord':
            self.qutepart.setWordWrapMode(
                QTextOption.WrapAtWordBoundaryOrAnywhere)
        elif conf['Wrap']['Mode'] == 'WrapAnywhere':
            self.qutepart.setWordWrapMode(QTextOption.WrapAnywhere)
        else:
            assert 'Invalid wrap mode', conf['Wrap']['Mode']

        # EOL is managed by _configureEolMode(). But, if autodetect is disabled, we may apply new value here
        if not conf['EOL']['AutoDetect']:
            self.qutepart.eol = self._EOL_CONVERTOR[conf['EOL']['Mode']]

        # Whitespace visibility is managed by qpartsettings plugin

    def eventFilter(self, obj, event):
        """An event filter that looks for focus in events, closing any floating
           docks when found."""

        # Note: We can't ``def focusInEvent(self, focusEvent)`` since qutepart
        # is the focus proxy, meaning this won't be called. Hence, the neeed for
        # an event listener.
        if (event.type() == QEvent.FocusIn
                and (obj == self or obj == self.focusProxy())):
            for dock in core.mainWindow().findChildren(DockWidget):
                # Close all unpinned docks. The exception: if the Open Files
                # dock is waiting for the Ctrl button to be released, keep it
                # open; it will be be closed when Ctrl is released.
                if not dock.isPinned() and (not getattr(
                        dock, '_waitForCtrlRelease', False)):
                    dock._close()

        return QWidget.eventFilter(self, obj, event)
예제 #3
0
class Document(QWidget):
    """
    Document is a opened file representation.

    It contains file management methods and uses `Qutepart <http://qutepart.rtfd.org/>`_ as an editor widget.
    Qutepart is available as ``qutepart`` attribute.
    """

    documentDataChanged = pyqtSignal()
    """
    documentDataChanged()

    **Signal** emitted, when document icon or toolTip has changed
    (i.e. document has been modified externally)
    """

    _EOL_CONVERTOR = {r'\r\n': '\r\n',
                      r'\n': '\n',
                      r'\r': '\r'}

    def __init__(self, parentObject, filePath, createNew=False):
        """Create editor and open file.
        If file is None or createNew is True, empty not saved file is created
        IO Exceptions are not catched, therefore, must be catched on upper level
        """
        QWidget.__init__(self, parentObject)
        self._neverSaved = filePath is None or createNew
        self._filePath = filePath
        self._externallyRemoved = False
        self._externallyModified = False
        # File opening should be implemented in the document classes

        self._fileWatcher = _FileWatcher(filePath)
        self._fileWatcher.modified.connect(self._onWatcherFileModified)
        self._fileWatcher.removed.connect(self._onWatcherFileRemoved)

        self.qutepart = Qutepart(self)

        self.qutepart.setStyleSheet('QPlainTextEdit {border: 0}')

        self.qutepart.userWarning.connect(lambda text: core.mainWindow().statusBar().showMessage(text, 5000))

        self._applyQpartSettings()
        core.uiSettingsManager().dialogAccepted.connect(self._applyQpartSettings)

        layout = QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self.qutepart)
        self.setFocusProxy(self.qutepart)

        if not self._neverSaved:
            originalText = self._readFile(filePath)
            self.qutepart.text = originalText
        else:
            originalText = ''

        # autodetect eol, if need
        self._configureEolMode(originalText)

        self._tryDetectSyntax()

    def _tryDetectSyntax(self):
        if len(self.qutepart.lines) > (100 * 1000) and \
           self.qutepart.language() is None:
            """Qutepart uses too lot of memory when highlighting really big files
            It may crash the editor, so, do not highlight really big files.
            But, do not disable highlighting for files, which already was highlighted
            """
            return

        self.qutepart.detectSyntax(sourceFilePath=self.filePath(),
                                   firstLine=self.qutepart.lines[0])

    def terminate(self):
        """Explicytly called destructor
        """
        self._fileWatcher.term()

        # avoid emitting signals, document shall behave like it is already dead
        self.qutepart.document().modificationChanged.disconnect()
        self.qutepart.cursorPositionChanged.disconnect()  #
        self.qutepart.textChanged.disconnect()

        self.qutepart.terminate()  # stop background highlighting, free memory

    @pyqtSlot(bool)
    def _onWatcherFileModified(self, modified):
        """File has been modified
        """
        self._externallyModified = modified
        self.documentDataChanged.emit()

    @pyqtSlot(bool)
    def _onWatcherFileRemoved(self, isRemoved):
        """File has been removed
        """
        self._externallyRemoved = isRemoved
        self.documentDataChanged.emit()

    def _readFile(self, filePath):
        """Read the file contents.
        Shows QMessageBox for UnicodeDecodeError
        """
        with open(filePath, 'rb') as openedFile:  # Exception is ok, raise it up
            self._filePath = os.path.abspath(filePath)  # abspath won't fail, if file exists
            data = openedFile.read()

        self._fileWatcher.setContents(data)

        try:
            text = str(data, 'utf8')
        except UnicodeDecodeError as ex:
            QMessageBox.critical(None,
                                 self.tr("Can not decode file"),
                                 filePath + '\n' +
                                 str(ex) +
                                 '\nProbably invalid encoding was set. ' +
                                 'You may corrupt your file, if saved it')
            text = str(data, 'utf8', 'replace')

        # Strip last EOL. Qutepart adds it when saving file
        if text.endswith('\r\n'):
            text = text[:-2]
        elif text.endswith('\r') or text.endswith('\n'):
            text = text[:-1]

        return text

    def isExternallyModified(self):
        """Check if document's file has been modified externally.

        This method does not do any file system access, but only returns cached info
        """
        return self._externallyModified

    def isExternallyRemoved(self):
        """Check if document's file has been deleted externally.

        This method does not do any file system access, but only returns cached info
        """
        return self._externallyRemoved

    def isNeverSaved(self):
        """Check if document has been created, but never has been saved on disk
        """
        return self._neverSaved

    def filePath(self):
        """Return the document file absolute path.

        ``None`` if not set (new document)"""
        return self._filePath

    def fileName(self):
        """Document file name without a path.

        ``None`` if not set (new document)"""
        if self._filePath:
            return os.path.basename(self._filePath)
        else:
            return None

    def setFilePath(self, newPath):
        """Change document file path.

        Used when saving first time, or on Save As action
        """
        core.workspace().documentClosed.emit(self)
        self._filePath = newPath
        self._fileWatcher.setPath(newPath)
        self._neverSaved = True
        core.workspace().documentOpened.emit(self)
        core.workspace().currentDocumentChanged.emit(self, self)

    def _stripTrailingWhiteSpace(self):
        lineHasTrailingSpace = ((line and line[-1].isspace())
                                for line in self.qutepart.lines)
        if any(lineHasTrailingSpace):
            with self.qutepart:
                for lineNo, line in enumerate(self.qutepart.lines):
                    if line and line[-1].isspace():
                        self.qutepart.lines[lineNo] = line.rstrip()
        else:
            pass  # Do not enter with statement, because it causes wrong textChanged signal

    def _saveToFs(self, filePath):
        """Low level method. Always saves file, even if not modified
        """
        # Create directory
        dirPath = os.path.dirname(filePath)
        if not os.path.exists(dirPath):
            try:
                os.makedirs(dirPath)
            except OSError as ex:
                error = str(ex)
                QMessageBox.critical(None,
                                     self.tr("Cannot save file"),
                                     self.tr("Cannot create directory '%s'. Error '%s'." % (dirPath, error)))
                return

        text = self.qutepart.textForSaving()

        # Write file
        data = text.encode('utf8')

        self._fileWatcher.disable()
        try:
            with open(filePath, 'wb') as openedFile:
                openedFile.write(data)
            self._fileWatcher.setContents(data)
        except IOError as ex:
            QMessageBox.critical(None,
                                 self.tr("Cannot write to file"),
                                 str(ex))
            return
        finally:
            self._fileWatcher.enable()

        # Update states
        self._neverSaved = False
        self._externallyRemoved = False
        self._externallyModified = False
        self.qutepart.document().setModified(False)
        self.documentDataChanged.emit()

        if self.qutepart.language() is None:
            self._tryDetectSyntax()

    def saveFile(self):
        """Save the file to file system.

        Show QFileDialog if file name is not known.
        Return False, if user cancelled QFileDialog, True otherwise
        """
        # Get path
        if not self._filePath:
            path, _ = QFileDialog.getSaveFileName(self, self.tr('Save file as...'))
            if path:
                self.setFilePath(path)
            else:
                return False

        if core.config()['Qutepart']['StripTrailingWhitespace']:
            self._stripTrailingWhiteSpace()

        self._saveToFs(self.filePath())
        return True

    def saveFileAs(self):
        """Ask for new file name with dialog. Save file
        """
        if self._filePath:
            default_filename = os.path.basename(self._filePath)
        else:
            default_filename = ''
        path, _ = QFileDialog.getSaveFileName(self, self.tr('Save file as...'), default_filename)
        if not path:
            return

        self.setFilePath(path)
        self._saveToFs(path)

    def reload(self):
        """Reload the file from the disk

        If child class reimplemented this method, it MUST call method of the parent class
        for update internal bookkeeping"""

        text = self._readFile(self.filePath())
        pos = self.qutepart.cursorPosition
        self.qutepart.text = text
        self._externallyModified = False
        self._externallyRemoved = False
        self.qutepart.cursorPosition = pos
        self.qutepart.centerCursor()

    def modelToolTip(self):
        """Tool tip for the opened files model
        """
        toolTip = self.filePath()

        if toolTip is None:
            return None

        if self.qutepart.document().isModified():
            toolTip += "<br/><font color='blue'>%s</font>" % self.tr("Locally Modified")
        if self._externallyModified:
            toolTip += "<br/><font color='red'>%s</font>" % self.tr("Externally Modified")
        if self._externallyRemoved:
            toolTip += "<br/><font color='red'>%s</font>" % self.tr("Externally Deleted")
        return '<html>' + toolTip + '</html>'

    def modelIcon(self):
        """Icon for the opened files model
        """
        if self.isNeverSaved():  # never has been saved
            icon = "save.png"
        elif self._externallyRemoved and self.qutepart.document().isModified():
            icon = 'modified-externally-deleted.png'
        elif self._externallyRemoved:
            icon = "close.png"
        elif self._externallyModified and self.qutepart.document().isModified():
            icon = "modified-externally-modified.png"
        elif self._externallyModified:
            icon = "modified-externally.png"
        elif self.qutepart.document().isModified():
            icon = "save.png"
        else:
            icon = "transparent.png"
        return QIcon(":/enkiicons/" + icon)

    def invokeGoTo(self):
        """Show GUI dialog, go to line, if user accepted it
        """
        line = self.qutepart.cursorPosition[0]
        gotoLine, accepted = QInputDialog.getInt(self, self.tr("Go To Line..."),
                                                 self.tr("Enter the line you want to go to:"),
                                                 line, 1, len(self.qutepart.lines), 1)
        if accepted:
            gotoLine -= 1
            self.qutepart.cursorPosition = gotoLine, None
            self.setFocus()

    def printFile(self):
        """Print file
        """
        raise NotImplemented()

    def _configureEolMode(self, originalText):
        """Detect end of line mode automatically and apply detected mode
        """
        modes = set()
        for line in originalText.splitlines(True):
            if line.endswith('\r\n'):
                modes.add('\r\n')
            elif line.endswith('\n'):
                modes.add('\n')
            elif line.endswith('\r'):
                modes.add('\r')

        if len(modes) == 1:  # exactly one
            detectedMode = modes.pop()
        else:
            detectedMode = None

        default = self._EOL_CONVERTOR[core.config()["Qutepart"]["EOL"]["Mode"]]

        if len(modes) > 1:
            message = "%s contains mix of End Of Line symbols. It will be saved with '%s'" % \
                (self.filePath(), repr(default))
            core.mainWindow().appendMessage(message)
            self.qutepart.eol = default
            self.qutepart.document().setModified(True)
        elif core.config()["Qutepart"]["EOL"]["AutoDetect"]:
            if detectedMode is not None:
                self.qutepart.eol = detectedMode
            else:  # empty set, not detected
                self.qutepart.eol = default
        else:  # no mix, no autodetect. Force EOL
            if detectedMode is not None and \
                    detectedMode != default:
                message = "%s: End Of Line mode is '%s', but file will be saved with '%s'. " \
                          "EOL autodetection is disabled in the settings" % \
                    (self.fileName(), repr(detectedMode), repr(default))
                core.mainWindow().appendMessage(message)
                self.qutepart.document().setModified(True)

            self.qutepart.eol = default

    @pyqtSlot()
    def _applyQpartSettings(self):
        """Apply qutepart settings
        """
        conf = core.config()['Qutepart']
        self.qutepart.setFont(QFont(conf['Font']['Family'], conf['Font']['Size']))

        self.qutepart.indentUseTabs = conf['Indentation']['UseTabs']
        self.qutepart.indentWidth = conf['Indentation']['Width']

        if conf['Edge']['Enabled']:
            self.qutepart.lineLengthEdge = conf['Edge']['Column']
        else:
            self.qutepart.lineLengthEdge = None
        self.qutepart.lineLengthEdgeColor = QColor(conf['Edge']['Color'])

        self.qutepart.completionEnabled = conf['AutoCompletion']['Enabled']
        self.qutepart.completionThreshold = conf['AutoCompletion']['Threshold']

        self.qutepart.setLineWrapMode(QPlainTextEdit.WidgetWidth if conf['Wrap']['Enabled'] else QPlainTextEdit.NoWrap)
        if conf['Wrap']['Mode'] == 'WrapAtWord':
            self.qutepart.setWordWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere)
        elif conf['Wrap']['Mode'] == 'WrapAnywhere':
            self.qutepart.setWordWrapMode(QTextOption.WrapAnywhere)
        else:
            assert 'Invalid wrap mode', conf['Wrap']['Mode']

        # EOL is managed by _configureEolMode(). But, if autodetect is disabled, we may apply new value here
        if not conf['EOL']['AutoDetect']:
            self.qutepart.eol = self._EOL_CONVERTOR[conf['EOL']['Mode']]
예제 #4
0
class Document(QWidget):
    """
    Document is a opened file representation.

    It contains file management methods and uses `Qutepart <http://qutepart.rtfd.org/>`_ as an editor widget.
    Qutepart is available as ``qutepart`` attribute.
    """

    documentDataChanged = pyqtSignal()
    """
    documentDataChanged()

    **Signal** emitted, when document icon or toolTip has changed
    (i.e. document has been modified externally)
    """

    _EOL_CONVERTOR = {r'\r\n': '\r\n', r'\n': '\n', r'\r': '\r'}

    def __init__(self, parentObject, filePath, createNew=False):
        """Create editor and open file.
        If file is None or createNew is True, empty not saved file is created
        IO Exceptions are not catched, therefore, must be catched on upper level
        """
        QWidget.__init__(self, parentObject)
        self._neverSaved = filePath is None or createNew
        self._filePath = filePath
        self._externallyRemoved = False
        self._externallyModified = False
        # File opening should be implemented in the document classes

        self._fileWatcher = _FileWatcher(filePath)
        self._fileWatcher.modified.connect(self._onWatcherFileModified)
        self._fileWatcher.removed.connect(self._onWatcherFileRemoved)

        self.qutepart = Qutepart(self)

        self.qutepart.setStyleSheet('QPlainTextEdit {border: 0}')

        self.qutepart.userWarning.connect(
            lambda text: core.mainWindow().statusBar().showMessage(text, 5000))

        self._applyQpartSettings()
        core.uiSettingsManager().dialogAccepted.connect(
            self._applyQpartSettings)

        layout = QVBoxLayout(self)
        layout.setMargin(0)
        layout.addWidget(self.qutepart)
        self.setFocusProxy(self.qutepart)

        if not self._neverSaved:
            originalText = self._readFile(filePath)
            self.qutepart.text = originalText
        else:
            originalText = ''

        #autodetect eol, if need
        self._configureEolMode(originalText)

        self._tryDetectSyntax()

    def _tryDetectSyntax(self):
        if len(self.qutepart.lines) > (100 * 1000) and \
           self.qutepart.language() is None:
            """Qutepart uses too lot of memory when highlighting really big files
            It may crash the editor, so, do not highlight really big files.
            But, do not disable highlighting for files, which already was highlighted
            """
            return

        self.qutepart.detectSyntax(sourceFilePath=self.filePath(),
                                   firstLine=self.qutepart.lines[0])

    def del_(self):
        """Explicytly called destructor
        """
        self._fileWatcher.disable()

        # avoid emitting signals, document shall behave like it is already dead
        self.qutepart.document().modificationChanged.disconnect()
        self.qutepart.cursorPositionChanged.disconnect()  #
        self.qutepart.textChanged.disconnect()

        self.qutepart.terminate()  # stop background highlighting, free memory

    def _onWatcherFileModified(self, modified):
        """File has been modified
        """
        self._externallyModified = modified
        self.documentDataChanged.emit()

    def _onWatcherFileRemoved(self, isRemoved):
        """File has been removed
        """
        self._externallyRemoved = isRemoved
        self.documentDataChanged.emit()

    def _readFile(self, filePath):
        """Read the file contents.
        Shows QMessageBox for UnicodeDecodeError
        """
        with open(filePath,
                  'rb') as openedFile:  # Exception is ok, raise it up
            self._filePath = os.path.abspath(
                filePath)  # abspath won't fail, if file exists
            data = openedFile.read()

        self._fileWatcher.setContents(data)

        try:
            text = unicode(data, 'utf8')
        except UnicodeDecodeError, ex:
            QMessageBox.critical(
                None, self.tr("Can not decode file"),
                filePath + '\n' + unicode(str(ex), 'utf8') +
                '\nProbably invalid encoding was set. ' +
                'You may corrupt your file, if saved it')
            text = unicode(data, 'utf8', 'replace')

        # Strip last EOL. Qutepart adds it when saving file
        if text.endswith('\r\n'):
            text = text[:-2]
        elif text.endswith('\r') or text.endswith('\n'):
            text = text[:-1]

        return text