class TestClipboardHandling(unittest.TestCase): class DummyReTextTab(): def __init__(self): self.markupClass = None def getActiveMarkupClass(self): return self.markupClass def setUp(self): self.p = self self.editor = ReTextEdit(self) self.dummytab = self.DummyReTextTab() self.editor.tab = self.dummytab def _create_image(self): image = QImage(80, 60, QImage.Format_RGB32) image.fill(Qt.green) return image def test_allowTextOnClipboard(self): mimeData = QMimeData() mimeData.setText('hello') self.assertTrue(self.editor.canInsertFromMimeData(mimeData)) def test_allowImageOnClipboard(self): mimeData = QMimeData() mimeData.setImageData(self._create_image()) self.assertTrue(self.editor.canInsertFromMimeData(mimeData)) def test_pasteText(self): mimeData = QMimeData() mimeData.setText('pasted text') self.editor.insertFromMimeData(mimeData) self.assertTrue('pasted text' in self.editor.toPlainText()) @patch.object(ReTextEdit, 'getImageFilenameAndLink', return_value=('/tmp/myimage.jpg', 'myimage.jpg')) @patch.object(QImage, 'save') def test_pasteImage_Markdown(self, _mock_image, _mock_editor): mimeData = QMimeData() mimeData.setImageData(self._create_image()) self.dummytab.markupClass = MarkdownMarkup self.editor.insertFromMimeData(mimeData) self.assertTrue('![myimage](myimage.jpg)' in self.editor.toPlainText()) @patch.object(ReTextEdit, 'getImageFilenameAndLink', return_value=('/tmp/myimage.jpg', 'myimage.jpg')) @patch.object(QImage, 'save') def test_pasteImage_RestructuredText(self, _mock_image, _mock_editor): mimeData = QMimeData() mimeData.setImageData(self._create_image()) self.dummytab.markupClass = ReStructuredTextMarkup self.editor.insertFromMimeData(mimeData) self.assertTrue('.. image:: myimage.jpg' in self.editor.toPlainText())
class ReTextTab(QObject): def __init__(self, parent, fileName, previewState=PreviewDisabled): QObject.__init__(self, parent) self.p = parent self.fileName = fileName self.editBox = ReTextEdit(self) self.previewBox = self.createPreviewBox() self.markup = parent.getMarkup(fileName) self.previewState = previewState self.previewBlocked = False textDocument = self.editBox.document() self.highlighter = ReTextHighlighter(textDocument) if enchant_available and parent.actionEnableSC.isChecked(): self.highlighter.dictionary = enchant.Dict(parent.sl) self.highlighter.rehighlight() self.highlighter.docType = self.markup.name self.editBox.textChanged.connect(self.updateLivePreviewBox) self.editBox.undoAvailable.connect(parent.actionUndo.setEnabled) self.editBox.redoAvailable.connect(parent.actionRedo.setEnabled) self.editBox.copyAvailable.connect(parent.actionCopy.setEnabled) textDocument.modificationChanged.connect(parent.modificationChanged) self.updateBoxesVisibility() def createWebView(self): webView = QWebView() if not globalSettings.handleWebLinks: webView.page().setLinkDelegationPolicy(QWebPage.DelegateExternalLinks) webView.page().linkClicked.connect(QDesktopServices.openUrl) settings = webView.settings() settings.setAttribute(QWebSettings.LocalContentCanAccessFileUrls, False) settings.setDefaultTextEncoding('utf-8') return webView def createPreviewBox(self): if globalSettings.useWebKit: return self.createWebView() browser = QTextBrowser() # TODO: honor globalSettings.handleWebLinks? browser.setOpenExternalLinks(True) return browser def getSplitter(self): splitter = QSplitter(Qt.Horizontal) # Give both boxes a minimum size so the minimumSizeHint will be # ignored when splitter.setSizes is called below for widget in self.editBox, self.previewBox: widget.setMinimumWidth(125) splitter.addWidget(widget) splitter.setSizes((50, 50)) splitter.setChildrenCollapsible(False) return splitter def getDocumentTitle(self, baseName=False): if self.markup and not baseName: text = self.editBox.toPlainText() try: return self.markup.get_document_title(text) except Exception: self.p.printError() if self.fileName: fileinfo = QFileInfo(self.fileName) basename = fileinfo.completeBaseName() return (basename if basename else fileinfo.fileName()) return self.tr("New document") def getHtml(self, includeStyleSheet=True, includeTitle=True, includeMeta=False, webenv=False): if self.markup is None: markupClass = self.p.getMarkupClass(self.fileName) errMsg = self.tr('Could not parse file contents, check if ' 'you have the <a href="%s">necessary module</a> ' 'installed!') try: errMsg %= markupClass.attributes[MODULE_HOME_PAGE] except (AttributeError, KeyError): # Remove the link if markupClass doesn't have the needed attribute errMsg = errMsg.replace('<a href="%s">', '').replace('</a>', '') return '<p style="color: red">%s</p>' % errMsg text = self.editBox.toPlainText() headers = '' if includeStyleSheet: headers += '<style type="text/css">\n' + self.p.ss + '</style>\n' cssFileName = self.getDocumentTitle(baseName=True) + '.css' if QFile(cssFileName).exists(): headers += ('<link rel="stylesheet" type="text/css" href="%s">\n' % cssFileName) if includeMeta: headers += ('<meta name="generator" content="ReText %s">\n' % app_version) fallbackTitle = self.getDocumentTitle() if includeTitle else '' return self.markup.get_whole_html(text, custom_headers=headers, include_stylesheet=includeStyleSheet, fallback_title=fallbackTitle, webenv=webenv) def updatePreviewBox(self): self.previewBlocked = False if isinstance(self.previewBox, QTextEdit): scrollbar = self.previewBox.verticalScrollBar() disttobottom = scrollbar.maximum() - scrollbar.value() else: frame = self.previewBox.page().mainFrame() scrollpos = frame.scrollPosition() try: html = self.getHtml() except Exception: return self.p.printError() if isinstance(self.previewBox, QTextEdit): self.previewBox.setHtml(html) self.previewBox.document().setDefaultFont(globalSettings.font) scrollbar.setValue(scrollbar.maximum() - disttobottom) else: settings = self.previewBox.settings() settings.setFontFamily(QWebSettings.StandardFont, globalSettings.font.family()) settings.setFontSize(QWebSettings.DefaultFontSize, globalSettings.font.pointSize()) self.previewBox.setHtml(html, QUrl.fromLocalFile(self.fileName)) frame.setScrollPosition(scrollpos) def updateLivePreviewBox(self): if self.previewState == PreviewLive and not self.previewBlocked: self.previewBlocked = True QTimer.singleShot(1000, self.updatePreviewBox) def updateBoxesVisibility(self): self.editBox.setVisible(self.previewState < PreviewNormal) self.previewBox.setVisible(self.previewState > PreviewDisabled) def saveTextToFile(self, fileName=None, addToWatcher=True): if fileName is None: fileName = self.fileName self.p.fileSystemWatcher.removePath(fileName) savefile = QFile(fileName) result = savefile.open(QFile.WriteOnly) if result: savestream = QTextStream(savefile) if globalSettings.defaultCodec: savestream.setCodec(globalSettings.defaultCodec) savestream << self.editBox.toPlainText() savefile.close() if result and addToWatcher: self.p.fileSystemWatcher.addPath(fileName) return result def installFakeVimHandler(self): if ReTextFakeVimHandler: fakeVimEditor = ReTextFakeVimHandler(self.editBox, self) fakeVimEditor.setSaveAction(self.actionSave) fakeVimEditor.setQuitAction(self.actionQuit) # TODO: action is bool, really call remove? self.p.actionFakeVimMode.triggered.connect(fakeVimEditor.remove)
class ReTextTab(QSplitter): fileNameChanged = pyqtSignal() modificationStateChanged = pyqtSignal() activeMarkupChanged = pyqtSignal() # Make _fileName a read-only property to make sure that any # modification happens through the proper functions. These functions # will make sure that the fileNameChanged signal is emitted when # applicable. @property def fileName(self): return self._fileName def __init__(self, parent, fileName, previewState=PreviewDisabled): super().__init__(Qt.Horizontal, parent=parent) self.p = parent self._fileName = fileName self.editBox = ReTextEdit(self) self.previewBox = self.createPreviewBox(self.editBox) self.activeMarkupClass = None self.markup = None self.converted = None self.previewState = previewState self.previewOutdated = False self.conversionPending = False self.cssFileExists = False self.converterProcess = converterprocess.ConverterProcess() self.converterProcess.conversionDone.connect(self.updatePreviewBox) textDocument = self.editBox.document() self.highlighter = ReTextHighlighter(textDocument) if enchant is not None and parent.actionEnableSC.isChecked(): self.highlighter.dictionary = enchant.Dict(parent.sl or None) # Rehighlighting is tied to the change in markup class that # happens at the end of this function self.editBox.textChanged.connect(self.triggerPreviewUpdate) self.editBox.undoAvailable.connect(parent.actionUndo.setEnabled) self.editBox.redoAvailable.connect(parent.actionRedo.setEnabled) self.editBox.copyAvailable.connect(parent.enableCopy) # Give both boxes a minimum size so the minimumSizeHint will be # ignored when splitter.setSizes is called below for widget in self.editBox, self.previewBox: widget.setMinimumWidth(125) self.addWidget(widget) self.setSizes((50, 50)) self.setChildrenCollapsible(False) textDocument.modificationChanged.connect(self.handleModificationChanged) self.updateActiveMarkupClass() def handleModificationChanged(self): self.modificationStateChanged.emit() def createPreviewBox(self, editBox): # Use closures to avoid a hard reference from ReTextWebKitPreview # to self, which would keep the tab and its resources alive # even after other references to it have disappeared. def editorPositionToSourceLine(editorPosition): viewportPosition = editorPosition - editBox.verticalScrollBar().value() sourceLine = editBox.cursorForPosition(QPoint(0,viewportPosition)).blockNumber() return sourceLine def sourceLineToEditorPosition(sourceLine): doc = editBox.document() block = doc.findBlockByNumber(sourceLine) rect = doc.documentLayout().blockBoundingRect(block) return rect.top() if ReTextWebKitPreview and globalSettings.useWebKit: preview = ReTextWebKitPreview(self, editorPositionToSourceLine, sourceLineToEditorPosition) elif ReTextWebEnginePreview and globalSettings.useWebEngine: preview = ReTextWebEnginePreview(self, editorPositionToSourceLine, sourceLineToEditorPosition) else: preview = ReTextPreview(self) return preview def getActiveMarkupClass(self): ''' Return the currently active markup class for this tab. No objects should be created of this class, it should only be used to retrieve markup class specific information. ''' return self.activeMarkupClass def updateActiveMarkupClass(self): ''' Update the active markup class based on the default class and the current filename. If the active markup class changes, the highlighter is rerun on the input text, the markup object of this tab is replaced with one of the new class and the activeMarkupChanged signal is emitted. ''' previousMarkupClass = self.activeMarkupClass self.activeMarkupClass = find_markup_class_by_name(globalSettings.defaultMarkup) if self._fileName: markupClass = get_markup_for_file_name( self._fileName, return_class=True) if markupClass: self.activeMarkupClass = markupClass if self.activeMarkupClass != previousMarkupClass: self.highlighter.docType = self.activeMarkupClass.name if self.activeMarkupClass else None self.highlighter.rehighlight() self.activeMarkupChanged.emit() self.triggerPreviewUpdate() def getDocumentTitleFromConverted(self, converted): if converted: try: return converted.get_document_title() except Exception: self.p.printError() return self.getBaseName() def getBaseName(self): if self._fileName: fileinfo = QFileInfo(self._fileName) basename = fileinfo.completeBaseName() return (basename if basename else fileinfo.fileName()) return self.tr("New document") def getHtmlFromConverted(self, converted, includeStyleSheet=True, webenv=False): if converted is None: markupClass = self.getActiveMarkupClass() errMsg = self.tr('Could not parse file contents, check if ' 'you have the <a href="%s">necessary module</a> ' 'installed!') try: errMsg %= markupClass.attributes[MODULE_HOME_PAGE] except (AttributeError, KeyError): # Remove the link if markupClass doesn't have the needed attribute errMsg = errMsg.replace('<a href="%s">', '').replace('</a>', '') return '<p style="color: red">%s</p>' % errMsg headers = '' if includeStyleSheet and self.p.ss is not None: headers += '<style type="text/css">\n' + self.p.ss + '</style>\n' elif includeStyleSheet: style = 'td, th { border: 1px solid #c3c3c3; padding: 0 3px 0 3px; }\n' style += 'table { border-collapse: collapse; }\n' style += 'img { max-width: 100%; }\n' # QTextDocument seems to use media=screen even for printing if globalSettings.useWebKit: # https://github.com/retext-project/retext/pull/187 palette = QApplication.palette() style += '@media screen { html { color: %s; } }\n' % palette.color(QPalette.WindowText).name() # https://github.com/retext-project/retext/issues/408 style += '@media print { html { background-color: white; } }\n' headers += '<style type="text/css">\n' + style + '</style>\n' baseName = self.getBaseName() if self.cssFileExists: headers += ('<link rel="stylesheet" type="text/css" href="%s.css">\n' % baseName) headers += ('<meta name="generator" content="ReText %s">\n' % app_version) return converted.get_whole_html( custom_headers=headers, include_stylesheet=includeStyleSheet, fallback_title=baseName, webenv=webenv) def getDocumentForExport(self, includeStyleSheet=True, webenv=False): markupClass = self.getActiveMarkupClass() if markupClass and markupClass.available(): exportMarkup = markupClass(filename=self._fileName) text = self.editBox.toPlainText() converted = exportMarkup.convert(text) else: converted = None return (self.getDocumentTitleFromConverted(converted), self.getHtmlFromConverted(converted, includeStyleSheet=includeStyleSheet, webenv=webenv), self.previewBox) def updatePreviewBox(self): self.conversionPending = False try: self.converted = self.converterProcess.get_result() except converterprocess.MarkupNotAvailableError: self.converted = None except converterprocess.ConversionError: return self.p.printError() if isinstance(self.previewBox, QTextEdit): scrollbar = self.previewBox.verticalScrollBar() scrollbarValue = scrollbar.value() distToBottom = scrollbar.maximum() - scrollbarValue try: html = self.getHtmlFromConverted(self.converted) except Exception: return self.p.printError() if isinstance(self.previewBox, QTextEdit): self.previewBox.setHtml(html) self.previewBox.document().setDefaultFont(globalSettings.font) # If scrollbar was at bottom (and that was not the same as top), # set it to bottom again if scrollbarValue: newValue = scrollbar.maximum() - distToBottom scrollbar.setValue(newValue) else: self.previewBox.updateFontSettings() # Always provide a baseUrl otherwise QWebView will # refuse to show images or other external objects if self._fileName: baseUrl = QUrl.fromLocalFile(self._fileName) else: baseUrl = QUrl.fromLocalFile(QDir.currentPath()) self.previewBox.setHtml(html, baseUrl) if self.previewOutdated: self.triggerPreviewUpdate() def triggerPreviewUpdate(self): self.previewOutdated = True if self.previewState == PreviewDisabled: return if not self.conversionPending: self.conversionPending = True QTimer.singleShot(500, self.startPendingConversion) def startPendingConversion(self): self.previewOutdated = False requested_extensions = ['ReText.mdx_posmap'] if globalSettings.syncScroll else [] self.converterProcess.start_conversion(self.getActiveMarkupClass().name, self.fileName, requested_extensions, self.editBox.toPlainText(), QDir.currentPath()) def updateBoxesVisibility(self): self.editBox.setVisible(self.previewState < PreviewNormal) self.previewBox.setVisible(self.previewState > PreviewDisabled) def rebuildPreviewBox(self): self.previewBox.disconnectExternalSignals() self.previewBox.setParent(None) self.previewBox.deleteLater() self.previewBox = self.createPreviewBox(self.editBox) self.previewBox.setMinimumWidth(125) self.addWidget(self.previewBox) self.setSizes((50, 50)) self.triggerPreviewUpdate() self.updateBoxesVisibility() def detectFileEncoding(self, fileName): ''' Detect content encoding of specific file. It will return None if it can't determine the encoding. ''' try: import chardet except ImportError: return with open(fileName, 'rb') as inputFile: raw = inputFile.read(2048) result = chardet.detect(raw) if result['confidence'] > 0.9: if result['encoding'].lower() == 'ascii': # UTF-8 files can be falsely detected as ASCII files if they # don't contain non-ASCII characters in first 2048 bytes. # We map ASCII to UTF-8 to avoid such situations. return 'utf-8' return result['encoding'] def readTextFromFile(self, fileName=None, encoding=None): previousFileName = self._fileName if fileName: self._fileName = fileName # Only try to detect encoding if it is not specified if encoding is None and globalSettings.detectEncoding: encoding = self.detectFileEncoding(self._fileName) # TODO: why do we open the file twice: for detecting encoding # and for actual read? Can we open it just once? openfile = QFile(self._fileName) openfile.open(QFile.ReadOnly) stream = QTextStream(openfile) encoding = encoding or globalSettings.defaultCodec if encoding: stream.setCodec(encoding) # If encoding is specified or detected, we should save the file with # the same encoding self.editBox.document().setProperty("encoding", encoding) text = stream.readAll() openfile.close() if previousFileName != self._fileName: self.updateActiveMarkupClass() self.editBox.setPlainText(text) self.editBox.document().setModified(False) cssFileName = self.getBaseName() + '.css' self.cssFileExists = QFile.exists(cssFileName) if previousFileName != self._fileName: self.fileNameChanged.emit() def writeTextToFile(self, fileName=None): # Just writes the text to file, without any changes to tab object # Used directly for e.g. export extensions # Get text from the cursor to avoid tweaking special characters, # see https://bugreports.qt.io/browse/QTBUG-57552 and # https://github.com/retext-project/retext/issues/216 cursor = self.editBox.textCursor() cursor.select(QTextCursor.Document) text = cursor.selectedText().replace('\u2029', '\n') savefile = QFile(fileName or self._fileName) result = savefile.open(QFile.WriteOnly) if result: savestream = QTextStream(savefile) # Save the file with original encoding encoding = self.editBox.document().property("encoding") if encoding is not None: savestream.setCodec(encoding) savestream << text savefile.close() return result def saveTextToFile(self, fileName=None): # Sets fileName as tab fileName and writes the text to that file if self._fileName: self.p.fileSystemWatcher.removePath(self._fileName) result = self.writeTextToFile(fileName) if result: self.editBox.document().setModified(False) self.p.fileSystemWatcher.addPath(fileName or self._fileName) if fileName and self._fileName != fileName: self._fileName = fileName self.updateActiveMarkupClass() self.fileNameChanged.emit() return result def goToLine(self,line): block = self.editBox.document().findBlockByLineNumber(line) if block.isValid(): newCursor = QTextCursor(block) self.editBox.setTextCursor(newCursor) def find(self, text, flags, replaceText=None, wrap=False): cursor = self.editBox.textCursor() if wrap and flags & QTextDocument.FindBackward: cursor.movePosition(QTextCursor.End) elif wrap: cursor.movePosition(QTextCursor.Start) if replaceText is not None and cursor.selectedText() == text: newCursor = cursor else: newCursor = self.editBox.document().find(text, cursor, flags) if not newCursor.isNull(): if replaceText is not None: newCursor.insertText(replaceText) newCursor.movePosition(QTextCursor.Left, QTextCursor.MoveAnchor, len(replaceText)) newCursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, len(replaceText)) self.editBox.setTextCursor(newCursor) if self.editBox.cursorRect().bottom() >= self.editBox.height() - 3: scrollValue = self.editBox.verticalScrollBar().value() areaHeight = self.editBox.fontMetrics().height() self.editBox.verticalScrollBar().setValue(scrollValue + areaHeight) return True if not wrap: return self.find(text, flags, replaceText, True) return False def replaceAll(self, text, replaceText): cursor = self.editBox.textCursor() cursor.beginEditBlock() cursor.movePosition(QTextCursor.Start) flags = QTextDocument.FindFlags() cursor = lastCursor = self.editBox.document().find(text, cursor, flags) while not cursor.isNull(): cursor.insertText(replaceText) lastCursor = cursor cursor = self.editBox.document().find(text, cursor, flags) if not lastCursor.isNull(): lastCursor.movePosition(QTextCursor.Left, QTextCursor.MoveAnchor, len(replaceText)) lastCursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, len(replaceText)) self.editBox.setTextCursor(lastCursor) self.editBox.textCursor().endEditBlock() return not lastCursor.isNull() def openSourceFile(self, linkPath): """Finds and opens the source file for link target fileToOpen. When links like [test](test) are clicked, the file test.md is opened. It has to be located next to the current opened file. Relative paths like [test](../test) or [test](folder/test) are also possible. """ fileToOpen = self.resolveSourceFile(linkPath) if exists(fileToOpen) and get_markup_for_file_name(fileToOpen, return_class=True): self.p.openFileWrapper(fileToOpen) return fileToOpen if get_markup_for_file_name(fileToOpen, return_class=True): if not QFile.exists(fileToOpen) and QFileInfo(fileToOpen).dir().exists(): if self.promptFileCreation(fileToOpen): self.p.openFileWrapper(fileToOpen) return fileToOpen def promptFileCreation(self, fileToCreate): """ Prompt user if a file should be created for the clicked link, and try to create it. Return True on success. """ buttonReply = QMessageBox.question(self, self.tr('Create missing file?'), self.tr("The file '%s' does not exist.\n\nDo you want to create it?") % fileToCreate, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if buttonReply == QMessageBox.Yes: return self.createFile(fileToCreate) elif buttonReply == QMessageBox.No: return False def resolveSourceFile(self, linkPath): """ Finds the actual path of the file to open in a new tab. When the link has no extension, eg: [Test](test), the extension of the current file is assumed (eg test.md for a markdown file). When the link is an html file eg: [Test](test.html), the extension of the current file is assumed (eg test.md for a markdown file). Relative paths like [test](../test) or [test](folder/test) are also possible. """ basename, ext = splitext(linkPath) if self.fileName: currentExt = splitext(self.fileName)[1] if ext in ('.html', '') and (exists(basename+currentExt) or not exists(linkPath)): ext = currentExt return basename+ext def createFile(self, fileToCreate): """Try to create file, return True if successful""" try: # Create file: open(fileToCreate, 'x').close() return True except OSError as err: QMessageBox.warning(self, self.tr("File could not be created"), self.tr("Could not create file '%s': %s") % (fileToCreate, err)) return False
class ReTextTab(QSplitter): fileNameChanged = pyqtSignal() modificationStateChanged = pyqtSignal() activeMarkupChanged = pyqtSignal() # Make _fileName a read-only property to make sure that any # modification happens through the proper functions. These functions # will make sure that the fileNameChanged signal is emitted when # applicable. @property def fileName(self): return self._fileName def __init__(self, parent, fileName, previewState=PreviewDisabled): super(QSplitter, self).__init__(Qt.Horizontal, parent=parent) self.p = parent self._fileName = fileName self.editBox = ReTextEdit(self) self.previewBox = self.createPreviewBox(self.editBox) self.activeMarkupClass = None self.markup = None self.converted = None self.previewState = previewState self.previewOutdated = False self.conversionPending = False self.converterProcess = converterprocess.ConverterProcess() self.converterProcess.conversionDone.connect(self.updatePreviewBox) textDocument = self.editBox.document() self.highlighter = ReTextHighlighter(textDocument) if enchant is not None and parent.actionEnableSC.isChecked(): self.highlighter.dictionary = enchant.Dict(parent.sl or None) # Rehighlighting is tied to the change in markup class that # happens at the end of this function self.editBox.textChanged.connect(self.triggerPreviewUpdate) self.editBox.undoAvailable.connect(parent.actionUndo.setEnabled) self.editBox.redoAvailable.connect(parent.actionRedo.setEnabled) self.editBox.copyAvailable.connect(parent.actionCopy.setEnabled) # Give both boxes a minimum size so the minimumSizeHint will be # ignored when splitter.setSizes is called below for widget in self.editBox, self.previewBox: widget.setMinimumWidth(125) self.addWidget(widget) self.setSizes((50, 50)) self.setChildrenCollapsible(False) textDocument.modificationChanged.connect(self.handleModificationChanged) self.updateActiveMarkupClass() def handleModificationChanged(self): self.modificationStateChanged.emit() def createPreviewBox(self, editBox): # Use closures to avoid a hard reference from ReTextWebPreview # to self, which would keep the tab and its resources alive # even after other references to it have disappeared. def editorPositionToSourceLine(editorPosition): viewportPosition = editorPosition - editBox.verticalScrollBar().value() sourceLine = editBox.cursorForPosition(QPoint(0, viewportPosition)).blockNumber() return sourceLine def sourceLineToEditorPosition(sourceLine): doc = editBox.document() block = doc.findBlockByNumber(sourceLine) rect = doc.documentLayout().blockBoundingRect(block) return rect.top() if globalSettings.useWebKit: preview = ReTextWebPreview(editBox, editorPositionToSourceLine, sourceLineToEditorPosition) else: preview = ReTextPreview(self) return preview def getActiveMarkupClass(self): """ Return the currently active markup class for this tab. No objects should be created of this class, it should only be used to retrieve markup class specific information. """ return self.activeMarkupClass def updateActiveMarkupClass(self): """ Update the active markup class based on the default class and the current filename. If the active markup class changes, the highlighter is rerun on the input text, the markup object of this tab is replaced with one of the new class and the activeMarkupChanged signal is emitted. """ previousMarkupClass = self.activeMarkupClass self.activeMarkupClass = find_markup_class_by_name(globalSettings.defaultMarkup) if self._fileName: markupClass = get_markup_for_file_name(self._fileName, return_class=True) if markupClass: self.activeMarkupClass = markupClass if self.activeMarkupClass != previousMarkupClass: self.highlighter.docType = self.activeMarkupClass.name if self.activeMarkupClass else None self.highlighter.rehighlight() self.activeMarkupChanged.emit() self.triggerPreviewUpdate() def getDocumentTitleFromConverted(self, converted): if converted: try: return converted.get_document_title() except Exception: self.p.printError() return self.getBaseName() def getBaseName(self): if self._fileName: fileinfo = QFileInfo(self._fileName) basename = fileinfo.completeBaseName() return basename if basename else fileinfo.fileName() return self.tr("New document") def getHtmlFromConverted(self, converted, includeStyleSheet=True, webenv=False): if converted is None: markupClass = self.getActiveMarkupClass() errMsg = self.tr( "Could not parse file contents, check if " 'you have the <a href="%s">necessary module</a> ' "installed!" ) try: errMsg %= markupClass.attributes[MODULE_HOME_PAGE] except (AttributeError, KeyError): # Remove the link if markupClass doesn't have the needed attribute errMsg = errMsg.replace('<a href="%s">', "").replace("</a>", "") return '<p style="color: red">%s</p>' % errMsg headers = "" if includeStyleSheet: headers += '<style type="text/css">\n' + self.p.ss + "</style>\n" baseName = self.getBaseName() cssFileName = baseName + ".css" if QFile.exists(cssFileName): headers += '<link rel="stylesheet" type="text/css" href="%s">\n' % cssFileName headers += '<meta name="generator" content="ReText %s">\n' % app_version return converted.get_whole_html( custom_headers=headers, include_stylesheet=includeStyleSheet, fallback_title=baseName, webenv=webenv ) def getDocumentForExport(self, includeStyleSheet, webenv): markupClass = self.getActiveMarkupClass() if markupClass and markupClass.available(): exportMarkup = markupClass(filename=self._fileName) text = self.editBox.toPlainText() converted = exportMarkup.convert(text) else: converted = None return ( self.getDocumentTitleFromConverted(converted), self.getHtmlFromConverted(converted, includeStyleSheet=includeStyleSheet, webenv=webenv), self.previewBox, ) def updatePreviewBox(self): self.conversionPending = False try: self.converted = self.converterProcess.get_result() except converterprocess.MarkupNotAvailableError: self.converted = None except converterprocess.ConversionError: return self.p.printError() if isinstance(self.previewBox, QTextEdit): scrollbar = self.previewBox.verticalScrollBar() scrollbarValue = scrollbar.value() distToBottom = scrollbar.maximum() - scrollbarValue try: html = self.getHtmlFromConverted(self.converted) except Exception: return self.p.printError() if isinstance(self.previewBox, QTextEdit): self.previewBox.setHtml(html) self.previewBox.document().setDefaultFont(globalSettings.font) # If scrollbar was at bottom (and that was not the same as top), # set it to bottom again if scrollbarValue: newValue = scrollbar.maximum() - distToBottom scrollbar.setValue(newValue) else: self.previewBox.updateFontSettings() # Always provide a baseUrl otherwise QWebView will # refuse to show images or other external objects if self._fileName: baseUrl = QUrl.fromLocalFile(self._fileName) else: baseUrl = QUrl.fromLocalFile(QDir.currentPath()) self.previewBox.setHtml(html, baseUrl) if self.previewOutdated: self.triggerPreviewUpdate() def triggerPreviewUpdate(self): self.previewOutdated = True if not self.conversionPending: self.conversionPending = True QTimer.singleShot(500, self.startPendingConversion) def startPendingConversion(self): self.previewOutdated = False requested_extensions = ["ReText.mdx_posmap"] if globalSettings.syncScroll else [] self.converterProcess.start_conversion( self.getActiveMarkupClass().name, self.fileName, requested_extensions, self.editBox.toPlainText() ) def updateBoxesVisibility(self): self.editBox.setVisible(self.previewState < PreviewNormal) self.previewBox.setVisible(self.previewState > PreviewDisabled) def detectFileEncoding(self, fileName): """ Detect content encoding of specific file. It will return None if it can't determine the encoding. """ try: import chardet except ImportError: return with open(fileName, "rb") as inputFile: raw = inputFile.read(2048) result = chardet.detect(raw) if result["confidence"] > 0.9: if result["encoding"].lower() == "ascii": # UTF-8 files can be falsely detected as ASCII files if they # don't contain non-ASCII characters in first 2048 bytes. # We map ASCII to UTF-8 to avoid such situations. return "utf-8" return result["encoding"] def readTextFromFile(self, fileName=None, encoding=None): previousFileName = self._fileName if fileName: self._fileName = fileName # Only try to detect encoding if it is not specified if encoding is None and globalSettings.detectEncoding: encoding = self.detectFileEncoding(self._fileName) # TODO: why do we open the file twice: for detecting encoding # and for actual read? Can we open it just once? openfile = QFile(self._fileName) openfile.open(QFile.ReadOnly) stream = QTextStream(openfile) encoding = encoding or globalSettings.defaultCodec if encoding: stream.setCodec(encoding) # If encoding is specified or detected, we should save the file with # the same encoding self.editBox.document().setProperty("encoding", encoding) text = stream.readAll() openfile.close() self.editBox.setPlainText(text) self.editBox.document().setModified(False) if previousFileName != self._fileName: self.updateActiveMarkupClass() self.fileNameChanged.emit() def writeTextToFile(self, fileName=None): # Just writes the text to file, without any changes to tab object # Used directly for i.e. export extensions savefile = QFile(fileName or self._fileName) result = savefile.open(QFile.WriteOnly) if result: savestream = QTextStream(savefile) # Save the file with original encoding encoding = self.editBox.document().property("encoding") if encoding is not None: savestream.setCodec(encoding) savestream << self.editBox.toPlainText() savefile.close() return result def saveTextToFile(self, fileName=None): # Sets fileName as tab fileName and writes the text to that file if self._fileName: self.p.fileSystemWatcher.removePath(self._fileName) result = self.writeTextToFile(fileName) if result: self.editBox.document().setModified(False) self.p.fileSystemWatcher.addPath(fileName or self._fileName) if fileName and self._fileName != fileName: self._fileName = fileName self.updateActiveMarkupClass() self.fileNameChanged.emit() return result def find(self, text, flags, replaceText=None, wrap=False): cursor = self.editBox.textCursor() if wrap and flags & QTextDocument.FindBackward: cursor.movePosition(QTextCursor.End) elif wrap: cursor.movePosition(QTextCursor.Start) if replaceText is not None and cursor.selectedText() == text: newCursor = cursor else: newCursor = self.editBox.document().find(text, cursor, flags) if not newCursor.isNull(): if replaceText is not None: newCursor.insertText(replaceText) newCursor.movePosition(QTextCursor.Left, QTextCursor.MoveAnchor, len(replaceText)) newCursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, len(replaceText)) self.editBox.setTextCursor(newCursor) return True if not wrap: return self.find(text, flags, replaceText, True) return False def replaceAll(self, text, replaceText): cursor = self.editBox.textCursor() cursor.beginEditBlock() cursor.movePosition(QTextCursor.Start) flags = QTextDocument.FindFlags() cursor = lastCursor = self.editBox.document().find(text, cursor, flags) while not cursor.isNull(): cursor.insertText(replaceText) lastCursor = cursor cursor = self.editBox.document().find(text, cursor, flags) if not lastCursor.isNull(): lastCursor.movePosition(QTextCursor.Left, QTextCursor.MoveAnchor, len(replaceText)) lastCursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, len(replaceText)) self.editBox.setTextCursor(lastCursor) self.editBox.textCursor().endEditBlock() return not lastCursor.isNull()
class ReTextTab(QObject): def __init__(self, parent, fileName, previewState=PreviewDisabled): QObject.__init__(self, parent) self.p = parent self.fileName = fileName self.editBox = ReTextEdit(self) self.previewBox = self.createPreviewBox() self.markup = self.getMarkup() self.previewState = previewState self.previewBlocked = False textDocument = self.editBox.document() self.highlighter = ReTextHighlighter(textDocument) if enchant_available and parent.actionEnableSC.isChecked(): self.highlighter.dictionary = enchant.Dict(parent.sl) self.highlighter.rehighlight() self.highlighter.docType = self.markup.name self.editBox.textChanged.connect(self.updateLivePreviewBox) self.editBox.undoAvailable.connect(parent.actionUndo.setEnabled) self.editBox.redoAvailable.connect(parent.actionRedo.setEnabled) self.editBox.copyAvailable.connect(parent.actionCopy.setEnabled) textDocument.modificationChanged.connect(parent.modificationChanged) self.updateBoxesVisibility() def createWebView(self): webView = QWebView() if not globalSettings.handleWebLinks: webView.page().setLinkDelegationPolicy(QWebPage.DelegateExternalLinks) webView.page().linkClicked.connect(QDesktopServices.openUrl) settings = webView.settings() settings.setAttribute(QWebSettings.LocalContentCanAccessFileUrls, False) settings.setDefaultTextEncoding('utf-8') return webView def createPreviewBox(self): if globalSettings.useWebKit: return self.createWebView() if globalSettings.useJekyllPreview: return JekyllPreview(self) return ReTextPreview(self) def getSplitter(self): splitter = QSplitter(Qt.Horizontal) # Give both boxes a minimum size so the minimumSizeHint will be # ignored when splitter.setSizes is called below for widget in self.editBox, self.previewBox: widget.setMinimumWidth(125) splitter.addWidget(widget) splitter.setSizes((50, 50)) splitter.setChildrenCollapsible(False) splitter.tab = self return splitter def getMarkupClass(self): if self.fileName: markupClass = get_markup_for_file_name( self.fileName, return_class=True) if markupClass: return markupClass return self.p.defaultMarkup def getMarkup(self): markupClass = self.getMarkupClass() if markupClass and markupClass.available(): return markupClass(filename=self.fileName) def getDocumentTitle(self, baseName=False): if self.markup and not baseName: text = self.editBox.toPlainText() try: return self.markup.get_document_title(text) except Exception: self.p.printError() if self.fileName: fileinfo = QFileInfo(self.fileName) basename = fileinfo.completeBaseName() return (basename if basename else fileinfo.fileName()) return self.tr("New document") def getHtml(self, includeStyleSheet=True, includeTitle=True, includeMeta=False, webenv=False): if self.markup is None: markupClass = self.getMarkupClass() errMsg = self.tr('Could not parse file contents, check if ' 'you have the <a href="%s">necessary module</a> ' 'installed!') try: errMsg %= markupClass.attributes[MODULE_HOME_PAGE] except (AttributeError, KeyError): # Remove the link if markupClass doesn't have the needed attribute errMsg = errMsg.replace('<a href="%s">', '').replace('</a>', '') return '<p style="color: red">%s</p>' % errMsg text = self.editBox.toPlainText() headers = '' if includeStyleSheet: headers += '<style type="text/css">\n' + self.p.ss + '</style>\n' cssFileName = self.getDocumentTitle(baseName=True) + '.css' if QFile(cssFileName).exists(): headers += ('<link rel="stylesheet" type="text/css" href="%s">\n' % cssFileName) if includeMeta: headers += ('<meta name="generator" content="ReText %s">\n' % app_version) fallbackTitle = self.getDocumentTitle() if includeTitle else '' return self.markup.get_whole_html(text, custom_headers=headers, include_stylesheet=includeStyleSheet, fallback_title=fallbackTitle, webenv=webenv) def updatePreviewBox(self): self.previewBlocked = False if isinstance(self.previewBox, QTextEdit): scrollbar = self.previewBox.verticalScrollBar() scrollbarValue = scrollbar.value() distToBottom = scrollbar.maximum() - scrollbarValue elif isinstance(self.previewBox, JekyllPreview) and self.editBox.toPlainText().startswith('---') \ and QFileInfo(self.fileName).exists(): def loadPreview(): fileToLoad = self.fileName.replace(self.p.jekyll_path, JEKYLL_BASE_URL) fileToLoad = fileToLoad.replace('.' + QFileInfo(self.fileName).completeSuffix(), '') self.previewBox.load(QUrl(fileToLoad)) self.previewBox.reload() # run one Jekyll server process for all tabs if not hasattr(self.p, 'jekyll_process'): def getJekyllFolder(dir): if QFileInfo(dir, '_config.yml').exists(): return dir if dir.cdUp(): return getJekyllFolder(dir) return None self.p.jekyll_path = getJekyllFolder(QFileInfo(self.fileName).absoluteDir()).absolutePath() cmd = ['jekyll', 'serve', '--source', self.p.jekyll_path, '--destination', self.p.jekyll_path + '/_site', '--incremental'] self.p.jekyll_process = subprocess.Popen(cmd, stdout=subprocess.PIPE) atexit.register(self.p.jekyll_process.terminate) # kill the process at exit def load(check_string): line = self.p.jekyll_process.stdout.readline() if check_string in line: loadPreview() else: self.previewBox.setHtml(str(line)) QTimer.singleShot(20, lambda: load(check_string)) load(b'Server running') # if the document was changed: give Jekyll some time to rebuild if hasattr(self, 'opened_before'): self.saveTextToFile() QTimer.singleShot(300, loadPreview) else: loadPreview() self.opened_before = True return else: frame = self.previewBox.page().mainFrame() scrollpos = frame.scrollPosition() try: html = self.getHtml() except Exception: return self.p.printError() if isinstance(self.previewBox, QTextEdit): self.previewBox.setHtml(html) self.previewBox.document().setDefaultFont(globalSettings.font) # If scrollbar was at bottom (and that was not the same as top), # set it to bottom again if scrollbarValue: newValue = scrollbar.maximum() - distToBottom scrollbar.setValue(newValue) else: settings = self.previewBox.settings() settings.setFontFamily(QWebSettings.StandardFont, globalSettings.font.family()) settings.setFontSize(QWebSettings.DefaultFontSize, globalSettings.font.pointSize()) self.previewBox.setHtml(html, QUrl.fromLocalFile(self.fileName)) frame.setScrollPosition(scrollpos) def updateLivePreviewBox(self): if self.previewState == PreviewLive and not self.previewBlocked: self.previewBlocked = True QTimer.singleShot(1000, self.updatePreviewBox) def updateBoxesVisibility(self): self.editBox.setVisible(self.previewState < PreviewNormal) self.previewBox.setVisible(self.previewState > PreviewDisabled) def setMarkupClass(self, markupClass): self.markup = None if markupClass and markupClass.available: self.markup = markupClass(filename=self.fileName) self.highlighter.docType = markupClass.name if markupClass else None self.highlighter.rehighlight() def readTextFromFile(self, encoding=None): openfile = QFile(self.fileName) openfile.open(QFile.ReadOnly) stream = QTextStream(openfile) encoding = encoding or globalSettings.defaultCodec if encoding: stream.setCodec(encoding) text = stream.readAll() openfile.close() markupClass = get_markup_for_file_name(self.fileName, return_class=True) self.setMarkupClass(markupClass) modified = bool(encoding) and (self.editBox.toPlainText() != text) self.editBox.setPlainText(text) self.editBox.document().setModified(modified) def saveTextToFile(self, fileName=None, addToWatcher=True): if fileName is None: fileName = self.fileName self.p.fileSystemWatcher.removePath(fileName) savefile = QFile(fileName) result = savefile.open(QFile.WriteOnly) if result: savestream = QTextStream(savefile) if globalSettings.defaultCodec: savestream.setCodec(globalSettings.defaultCodec) savestream << self.editBox.toPlainText() savefile.close() if result and addToWatcher: self.p.fileSystemWatcher.addPath(fileName) return result def installFakeVimHandler(self): if ReTextFakeVimHandler: fakeVimEditor = ReTextFakeVimHandler(self.editBox, self) fakeVimEditor.setSaveAction(self.actionSave) fakeVimEditor.setQuitAction(self.actionQuit) # TODO: action is bool, really call remove? self.p.actionFakeVimMode.triggered.connect(fakeVimEditor.remove)
class ReTextTab(QSplitter): fileNameChanged = pyqtSignal() modificationStateChanged = pyqtSignal() activeMarkupChanged = pyqtSignal() # Make _fileName a read-only property to make sure that any # modification happens through the proper functions. These functions # will make sure that the fileNameChanged signal is emitted when # applicable. @property def fileName(self): return self._fileName def __init__(self, parent, fileName, defaultMarkup, previewState=PreviewDisabled): super(QSplitter, self).__init__(Qt.Horizontal, parent=parent) self.p = parent self._fileName = fileName self.editBox = ReTextEdit(self) self.previewBox = self.createPreviewBox(self.editBox) self.defaultMarkupClass = defaultMarkup self.activeMarkupClass = None self.markup = None self.converted = None self.previewState = previewState self.previewOutdated = False self.conversionPending = False self.converterProcess = converterprocess.ConverterProcess() self.converterProcess.conversionDone.connect(self.updatePreviewBox) textDocument = self.editBox.document() self.highlighter = ReTextHighlighter(textDocument) if enchant is not None and parent.actionEnableSC.isChecked(): self.highlighter.dictionary = enchant.Dict(parent.sl or None) # Rehighlighting is tied to the change in markup class that # happens at the end of this function self.editBox.textChanged.connect(self.triggerPreviewUpdate) self.editBox.undoAvailable.connect(parent.actionUndo.setEnabled) self.editBox.redoAvailable.connect(parent.actionRedo.setEnabled) self.editBox.copyAvailable.connect(parent.actionCopy.setEnabled) # Give both boxes a minimum size so the minimumSizeHint will be # ignored when splitter.setSizes is called below for widget in self.editBox, self.previewBox: widget.setMinimumWidth(125) self.addWidget(widget) self.setSizes((50, 50)) self.setChildrenCollapsible(False) textDocument.modificationChanged.connect( self.handleModificationChanged) self.updateActiveMarkupClass() def handleModificationChanged(self): self.modificationStateChanged.emit() def createPreviewBox(self, editBox): # Use closures to avoid a hard reference from ReTextWebPreview # to self, which would keep the tab and its resources alive # even after other references to it have disappeared. def editorPositionToSourceLine(editorPosition): viewportPosition = editorPosition - editBox.verticalScrollBar( ).value() sourceLine = editBox.cursorForPosition(QPoint( 0, viewportPosition)).blockNumber() return sourceLine def sourceLineToEditorPosition(sourceLine): doc = editBox.document() block = doc.findBlockByNumber(sourceLine) rect = doc.documentLayout().blockBoundingRect(block) return rect.top() if globalSettings.useWebKit: preview = ReTextWebPreview(editBox, editorPositionToSourceLine, sourceLineToEditorPosition) else: preview = ReTextPreview(self) return preview def setDefaultMarkupClass(self, markupClass): ''' Set the default markup class to use in case a markup that matches the filename cannot be found. This function calls updateActiveMarkupClass so it can decide if the active markup class also has to change. ''' self.defaultMarkupClass = markupClass self.updateActiveMarkupClass() def getActiveMarkupClass(self): ''' Return the currently active markup class for this tab. No objects should be created of this class, it should only be used to retrieve markup class specific information. ''' return self.activeMarkupClass def updateActiveMarkupClass(self): ''' Update the active markup class based on the default class and the current filename. If the active markup class changes, the highlighter is rerun on the input text, the markup object of this tab is replaced with one of the new class and the activeMarkupChanged signal is emitted. ''' previousMarkupClass = self.activeMarkupClass self.activeMarkupClass = self.defaultMarkupClass if self._fileName: markupClass = get_markup_for_file_name(self._fileName, return_class=True) if markupClass: self.activeMarkupClass = markupClass if self.activeMarkupClass != previousMarkupClass: self.highlighter.docType = self.activeMarkupClass.name if self.activeMarkupClass else None self.highlighter.rehighlight() self.activeMarkupChanged.emit() self.triggerPreviewUpdate() def getDocumentTitleFromConverted(self, converted): if converted: try: return converted.get_document_title() except Exception: self.p.printError() return self.getBaseName() def getBaseName(self): if self._fileName: fileinfo = QFileInfo(self._fileName) basename = fileinfo.completeBaseName() return (basename if basename else fileinfo.fileName()) return self.tr("New document") def getHtmlFromConverted(self, converted, includeStyleSheet=True, webenv=False): if converted is None: markupClass = self.getActiveMarkupClass() errMsg = self.tr('Could not parse file contents, check if ' 'you have the <a href="%s">necessary module</a> ' 'installed!') try: errMsg %= markupClass.attributes[MODULE_HOME_PAGE] except (AttributeError, KeyError): # Remove the link if markupClass doesn't have the needed attribute errMsg = errMsg.replace('<a href="%s">', '').replace('</a>', '') return '<p style="color: red">%s</p>' % errMsg headers = '' if includeStyleSheet: headers += '<style type="text/css">\n' + self.p.ss + '</style>\n' baseName = self.getBaseName() cssFileName = baseName + '.css' if QFile.exists(cssFileName): headers += ('<link rel="stylesheet" type="text/css" href="%s">\n' % cssFileName) headers += ('<meta name="generator" content="ReText %s">\n' % app_version) return converted.get_whole_html(custom_headers=headers, include_stylesheet=includeStyleSheet, fallback_title=baseName, webenv=webenv) def getDocumentForExport(self, includeStyleSheet, webenv): markupClass = self.getActiveMarkupClass() if markupClass and markupClass.available(): exportMarkup = markupClass(filename=self._fileName) text = self.editBox.toPlainText() converted = exportMarkup.convert(text) else: converted = None return (self.getDocumentTitleFromConverted(converted), self.getHtmlFromConverted(converted, includeStyleSheet=includeStyleSheet, webenv=webenv), self.previewBox) def updatePreviewBox(self): self.conversionPending = False try: self.converted = self.converterProcess.get_result() except converterprocess.MarkupNotAvailableError: self.converted = None except converterprocess.ConversionError: return self.p.printError() if isinstance(self.previewBox, QTextEdit): scrollbar = self.previewBox.verticalScrollBar() scrollbarValue = scrollbar.value() distToBottom = scrollbar.maximum() - scrollbarValue try: html = self.getHtmlFromConverted(self.converted) except Exception: return self.p.printError() if isinstance(self.previewBox, QTextEdit): self.previewBox.setHtml(html) self.previewBox.document().setDefaultFont(globalSettings.font) # If scrollbar was at bottom (and that was not the same as top), # set it to bottom again if scrollbarValue: newValue = scrollbar.maximum() - distToBottom scrollbar.setValue(newValue) else: self.previewBox.updateFontSettings() # Always provide a baseUrl otherwise QWebView will # refuse to show images or other external objects if self._fileName: baseUrl = QUrl.fromLocalFile(self._fileName) else: baseUrl = QUrl.fromLocalFile(QDir.currentPath()) self.previewBox.setHtml(html, baseUrl) if self.previewOutdated: self.triggerPreviewUpdate() def triggerPreviewUpdate(self): self.previewOutdated = True if not self.conversionPending: self.conversionPending = True QTimer.singleShot(500, self.startPendingConversion) def startPendingConversion(self): self.previewOutdated = False requested_extensions = ['ReText.mdx_posmap' ] if globalSettings.syncScroll else [] self.converterProcess.start_conversion( self.getActiveMarkupClass().name, self.fileName, requested_extensions, self.editBox.toPlainText()) def updateBoxesVisibility(self): self.editBox.setVisible(self.previewState < PreviewNormal) self.previewBox.setVisible(self.previewState > PreviewDisabled) def detectFileEncoding(self, fileName): ''' Detect content encoding of specific file. It will return None if it can't determine the encoding. ''' try: import chardet except ImportError: return with open(fileName, 'rb') as inputFile: raw = inputFile.read(2048) result = chardet.detect(raw) if result['confidence'] > 0.9: if result['encoding'].lower() == 'ascii': # UTF-8 files can be falsely detected as ASCII files if they # don't contain non-ASCII characters in first 2048 bytes. # We map ASCII to UTF-8 to avoid such situations. return 'utf-8' return result['encoding'] def readTextFromFile(self, fileName=None, encoding=None): previousFileName = self._fileName if fileName: self._fileName = fileName openfile = QFile(self._fileName) openfile.open(QFile.ReadOnly) stream = QTextStream(openfile) # Only try to detect encoding if it is not specified if encoding is None and globalSettings.detectEncoding: encoding = self.detectFileEncoding(fileName) encoding = encoding or globalSettings.defaultCodec if encoding: stream.setCodec(encoding) # If encoding is specified or detected, we should save the file with # the same encoding self.editBox.document().setProperty("encoding", encoding) text = stream.readAll() openfile.close() self.editBox.setPlainText(text) self.editBox.document().setModified(False) if previousFileName != self._fileName: self.updateActiveMarkupClass() self.fileNameChanged.emit() def writeTextToFile(self, fileName=None): # Just writes the text to file, without any changes to tab object # Used directly for i.e. export extensions savefile = QFile(fileName or self._fileName) result = savefile.open(QFile.WriteOnly) if result: savestream = QTextStream(savefile) # Save the file with original encoding encoding = self.editBox.document().property("encoding") if encoding is not None: savestream.setCodec(encoding) savestream << self.editBox.toPlainText() savefile.close() return result def saveTextToFile(self, fileName=None): # Sets fileName as tab fileName and writes the text to that file if self._fileName: self.p.fileSystemWatcher.removePath(self._fileName) result = self.writeTextToFile(fileName) if result: self.editBox.document().setModified(False) self.p.fileSystemWatcher.addPath(fileName or self._fileName) if fileName and self._fileName != fileName: self._fileName = fileName self.updateActiveMarkupClass() self.fileNameChanged.emit() return result def find(self, text, flags, replaceText=None, wrap=False): cursor = self.editBox.textCursor() if wrap and flags & QTextDocument.FindBackward: cursor.movePosition(QTextCursor.End) elif wrap: cursor.movePosition(QTextCursor.Start) if replaceText is not None and cursor.selectedText() == text: newCursor = cursor else: newCursor = self.editBox.document().find(text, cursor, flags) if not newCursor.isNull(): if replaceText is not None: newCursor.insertText(replaceText) newCursor.movePosition(QTextCursor.Left, QTextCursor.MoveAnchor, len(replaceText)) newCursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, len(replaceText)) self.editBox.setTextCursor(newCursor) return True if not wrap: return self.find(text, flags, replaceText, True) return False def replaceAll(self, text, replaceText): cursor = self.editBox.textCursor() cursor.beginEditBlock() cursor.movePosition(QTextCursor.Start) flags = QTextDocument.FindFlags() cursor = lastCursor = self.editBox.document().find(text, cursor, flags) while not cursor.isNull(): cursor.insertText(replaceText) lastCursor = cursor cursor = self.editBox.document().find(text, cursor, flags) if not lastCursor.isNull(): lastCursor.movePosition(QTextCursor.Left, QTextCursor.MoveAnchor, len(replaceText)) lastCursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, len(replaceText)) self.editBox.setTextCursor(lastCursor) self.editBox.textCursor().endEditBlock() return not lastCursor.isNull()
class ReTextTab(QSplitter): fileNameChanged = pyqtSignal() modificationStateChanged = pyqtSignal() activeMarkupChanged = pyqtSignal() # Make _fileName a read-only property to make sure that any # modification happens through the proper functions. These functions # will make sure that the fileNameChanged signal is emitted when # applicable. @property def fileName(self): return self._fileName def __init__(self, parent, fileName, defaultMarkup, previewState=PreviewDisabled): super(QSplitter, self).__init__(Qt.Horizontal, parent=parent) self.p = parent self._fileName = fileName self.editBox = ReTextEdit(self) self.previewBox = self.createPreviewBox(self.editBox) self.defaultMarkupClass = defaultMarkup self.activeMarkupClass = None self.markup = None self.converted = None self.previewState = previewState self.previewOutdated = False self.conversionPending = False self.converterProcess = converterprocess.ConverterProcess() self.converterProcess.conversionDone.connect(self.updatePreviewBox) textDocument = self.editBox.document() self.highlighter = ReTextHighlighter(textDocument) if enchant is not None and parent.actionEnableSC.isChecked(): self.highlighter.dictionary = enchant.Dict(parent.sl or None) # Rehighlighting is tied to the change in markup class that # happens at the end of this function self.editBox.textChanged.connect(self.triggerPreviewUpdate) self.editBox.undoAvailable.connect(parent.actionUndo.setEnabled) self.editBox.redoAvailable.connect(parent.actionRedo.setEnabled) self.editBox.copyAvailable.connect(parent.actionCopy.setEnabled) # Give both boxes a minimum size so the minimumSizeHint will be # ignored when splitter.setSizes is called below for widget in self.editBox, self.previewBox: widget.setMinimumWidth(125) self.addWidget(widget) self.setSizes((50, 50)) self.setChildrenCollapsible(False) textDocument.modificationChanged.connect(self.handleModificationChanged) self.updateActiveMarkupClass() def handleModificationChanged(self): self.modificationStateChanged.emit() def createPreviewBox(self, editBox): # Use closures to avoid a hard reference from ReTextWebPreview # to self, which would keep the tab and its resources alive # even after other references to it have disappeared. def editorPositionToSourceLine(editorPosition): viewportPosition = editorPosition - editBox.verticalScrollBar().value() sourceLine = editBox.cursorForPosition(QPoint(0,viewportPosition)).blockNumber() return sourceLine def sourceLineToEditorPosition(sourceLine): doc = editBox.document() block = doc.findBlockByNumber(sourceLine) rect = doc.documentLayout().blockBoundingRect(block) return rect.top() if globalSettings.useWebKit: preview = ReTextWebPreview(editBox, editorPositionToSourceLine, sourceLineToEditorPosition) else: preview = ReTextPreview(self) return preview def setDefaultMarkupClass(self, markupClass): ''' Set the default markup class to use in case a markup that matches the filename cannot be found. This function calls updateActiveMarkupClass so it can decide if the active markup class also has to change. ''' self.defaultMarkupClass = markupClass self.updateActiveMarkupClass() def getActiveMarkupClass(self): ''' Return the currently active markup class for this tab. No objects should be created of this class, it should only be used to retrieve markup class specific information. ''' return self.activeMarkupClass def updateActiveMarkupClass(self): ''' Update the active markup class based on the default class and the current filename. If the active markup class changes, the highlighter is rerun on the input text, the markup object of this tab is replaced with one of the new class and the activeMarkupChanged signal is emitted. ''' previousMarkupClass = self.activeMarkupClass self.activeMarkupClass = self.defaultMarkupClass if self._fileName: markupClass = get_markup_for_file_name( self._fileName, return_class=True) if markupClass: self.activeMarkupClass = markupClass if self.activeMarkupClass != previousMarkupClass: self.highlighter.docType = self.activeMarkupClass.name if self.activeMarkupClass else None self.highlighter.rehighlight() self.activeMarkupChanged.emit() self.triggerPreviewUpdate() def getDocumentTitleFromConverted(self, converted): if converted: try: return converted.get_document_title() except Exception: self.p.printError() return self.getBaseName() def getBaseName(self): if self._fileName: fileinfo = QFileInfo(self._fileName) basename = fileinfo.completeBaseName() return (basename if basename else fileinfo.fileName()) return self.tr("New document") def getHtmlFromConverted(self, converted, includeStyleSheet=True, webenv=False): if converted is None: markupClass = self.getActiveMarkupClass() errMsg = self.tr('Could not parse file contents, check if ' 'you have the <a href="%s">necessary module</a> ' 'installed!') try: errMsg %= markupClass.attributes[MODULE_HOME_PAGE] except (AttributeError, KeyError): # Remove the link if markupClass doesn't have the needed attribute errMsg = errMsg.replace('<a href="%s">', '').replace('</a>', '') return '<p style="color: red">%s</p>' % errMsg headers = '' if includeStyleSheet: headers += '<style type="text/css">\n' + self.p.ss + '</style>\n' baseName = self.getBaseName() cssFileName = baseName + '.css' if QFile.exists(cssFileName): headers += ('<link rel="stylesheet" type="text/css" href="%s">\n' % cssFileName) headers += ('<meta name="generator" content="ReText %s">\n' % app_version) return converted.get_whole_html( custom_headers=headers, include_stylesheet=includeStyleSheet, fallback_title=baseName, webenv=webenv) def getDocumentForExport(self, includeStyleSheet, webenv): markupClass = self.getActiveMarkupClass() if markupClass and markupClass.available(): exportMarkup = markupClass(filename=self._fileName) text = self.editBox.toPlainText() converted = exportMarkup.convert(text) else: converted = None return (self.getDocumentTitleFromConverted(converted), self.getHtmlFromConverted(converted, includeStyleSheet=includeStyleSheet, webenv=webenv), self.previewBox) def updatePreviewBox(self): self.conversionPending = False try: self.converted = self.converterProcess.get_result() except converterprocess.MarkupNotAvailableError: self.converted = None except converterprocess.ConversionError: return self.p.printError() if isinstance(self.previewBox, QTextEdit): scrollbar = self.previewBox.verticalScrollBar() scrollbarValue = scrollbar.value() distToBottom = scrollbar.maximum() - scrollbarValue try: html = self.getHtmlFromConverted(self.converted) except Exception: return self.p.printError() if isinstance(self.previewBox, QTextEdit): self.previewBox.setHtml(html) self.previewBox.document().setDefaultFont(globalSettings.font) # If scrollbar was at bottom (and that was not the same as top), # set it to bottom again if scrollbarValue: newValue = scrollbar.maximum() - distToBottom scrollbar.setValue(newValue) else: self.previewBox.updateFontSettings() # Always provide a baseUrl otherwise QWebView will # refuse to show images or other external objects if self._fileName: baseUrl = QUrl.fromLocalFile(self._fileName) else: baseUrl = QUrl.fromLocalFile(QDir.currentPath()) self.previewBox.setHtml(html, baseUrl) if self.previewOutdated: self.triggerPreviewUpdate() def triggerPreviewUpdate(self): self.previewOutdated = True if not self.conversionPending: self.conversionPending = True QTimer.singleShot(500, self.startPendingConversion) def startPendingConversion(self): self.previewOutdated = False requested_extensions = ['ReText.mdx_posmap'] if globalSettings.syncScroll else [] self.converterProcess.start_conversion(self.getActiveMarkupClass().name, self.fileName, requested_extensions, self.editBox.toPlainText()) def updateBoxesVisibility(self): self.editBox.setVisible(self.previewState < PreviewNormal) self.previewBox.setVisible(self.previewState > PreviewDisabled) def readTextFromFile(self, fileName=None, encoding=None): previousFileName = self._fileName if fileName: self._fileName = fileName openfile = QFile(self._fileName) openfile.open(QFile.ReadOnly) stream = QTextStream(openfile) encoding = encoding or globalSettings.defaultCodec if encoding: stream.setCodec(encoding) text = stream.readAll() openfile.close() modified = bool(encoding) and (self.editBox.toPlainText() != text) self.editBox.setPlainText(text) self.editBox.document().setModified(modified) if previousFileName != self._fileName: self.updateActiveMarkupClass() self.fileNameChanged.emit() def writeTextToFile(self, fileName=None): # Just writes the text to file, without any changes to tab object # Used directly for i.e. export extensions savefile = QFile(fileName or self._fileName) result = savefile.open(QFile.WriteOnly) if result: savestream = QTextStream(savefile) if globalSettings.defaultCodec: savestream.setCodec(globalSettings.defaultCodec) savestream << self.editBox.toPlainText() savefile.close() return result def saveTextToFile(self, fileName=None): # Sets fileName as tab fileName and writes the text to that file if self._fileName: self.p.fileSystemWatcher.removePath(self._fileName) result = self.writeTextToFile(fileName) if result: self.editBox.document().setModified(False) self.p.fileSystemWatcher.addPath(fileName or self._fileName) if result and self._fileName != fileName: self._fileName = fileName self.updateActiveMarkupClass() self.fileNameChanged.emit() return result def installFakeVimHandler(self): if ReTextFakeVimHandler: fakeVimEditor = ReTextFakeVimHandler(self.editBox, self) fakeVimEditor.setSaveAction(self.actionSave) fakeVimEditor.setQuitAction(self.actionQuit) # TODO: action is bool, really call remove? self.p.actionFakeVimMode.triggered.connect(fakeVimEditor.remove) def find(self, text, flags): cursor = self.editBox.textCursor() newCursor = self.editBox.document().find(text, cursor, flags) if not newCursor.isNull(): self.editBox.setTextCursor(newCursor) return True cursor.movePosition(QTextCursor.End if (flags & QTextDocument.FindBackward) else QTextCursor.Start) newCursor = self.editBox.document().find(text, cursor, flags) if not newCursor.isNull(): self.editBox.setTextCursor(newCursor) return True return False
class ReTextTab(QObject): def __init__(self, parent, fileName, previewState=PreviewDisabled): QObject.__init__(self, parent) self.p = parent self.fileName = fileName self.editBox = ReTextEdit(self) self.previewBox = self.createPreviewBox(self.editBox) self.markup = self.getMarkup() self.previewState = previewState self.previewBlocked = False textDocument = self.editBox.document() self.highlighter = ReTextHighlighter(textDocument) if enchant_available and parent.actionEnableSC.isChecked(): self.highlighter.dictionary = enchant.Dict(parent.sl or None) self.highlighter.rehighlight() self.highlighter.docType = self.markup.name self.editBox.textChanged.connect(self.updateLivePreviewBox) self.editBox.undoAvailable.connect(parent.actionUndo.setEnabled) self.editBox.redoAvailable.connect(parent.actionRedo.setEnabled) self.editBox.copyAvailable.connect(parent.actionCopy.setEnabled) textDocument.modificationChanged.connect(parent.modificationChanged) self.updateBoxesVisibility() def editorPositionToSourceLine(self, editorPosition): viewportPosition = editorPosition - self.editBox.verticalScrollBar( ).value() sourceLine = self.editBox.cursorForPosition(QPoint( 0, viewportPosition)).blockNumber() return sourceLine def sourceLineToEditorPosition(self, sourceLine): doc = self.editBox.document() block = doc.findBlockByNumber(sourceLine) rect = doc.documentLayout().blockBoundingRect(block) return rect.top() def createPreviewBox(self, editBox): if globalSettings.useWebKit: preview = ReTextWebPreview(editBox, self.editorPositionToSourceLine, self.sourceLineToEditorPosition) else: preview = ReTextPreview(self) return preview def getSplitter(self): splitter = QSplitter(Qt.Horizontal) # Give both boxes a minimum size so the minimumSizeHint will be # ignored when splitter.setSizes is called below for widget in self.editBox, self.previewBox: widget.setMinimumWidth(125) splitter.addWidget(widget) splitter.setSizes((50, 50)) splitter.setChildrenCollapsible(False) splitter.tab = self return splitter def getMarkupClass(self): if self.fileName: markupClass = get_markup_for_file_name(self.fileName, return_class=True) if markupClass: return markupClass return self.p.defaultMarkup def getMarkup(self): markupClass = self.getMarkupClass() if markupClass and markupClass.available(): return markupClass(filename=self.fileName) def getDocumentTitle(self, baseName=False): if self.markup and not baseName: text = self.editBox.toPlainText() try: return self.markup.get_document_title(text) except Exception: self.p.printError() if self.fileName: fileinfo = QFileInfo(self.fileName) basename = fileinfo.completeBaseName() return (basename if basename else fileinfo.fileName()) return self.tr("New document") def getHtml(self, includeStyleSheet=True, webenv=False, syncScroll=False): if self.markup is None: markupClass = self.getMarkupClass() errMsg = self.tr('Could not parse file contents, check if ' 'you have the <a href="%s">necessary module</a> ' 'installed!') try: errMsg %= markupClass.attributes[MODULE_HOME_PAGE] except (AttributeError, KeyError): # Remove the link if markupClass doesn't have the needed attribute errMsg = errMsg.replace('<a href="%s">', '').replace('</a>', '') return '<p style="color: red">%s</p>' % errMsg text = self.editBox.toPlainText() headers = '' if includeStyleSheet: headers += '<style type="text/css">\n' + self.p.ss + '</style>\n' baseName = self.getDocumentTitle(baseName=True) cssFileName = baseName + '.css' if QFile.exists(cssFileName): headers += ('<link rel="stylesheet" type="text/css" href="%s">\n' % cssFileName) headers += ('<meta name="generator" content="ReText %s">\n' % app_version) self.markup.requested_extensions = [] if syncScroll: self.markup.requested_extensions.append('ReText.mdx_posmap') return self.markup.get_whole_html(text, custom_headers=headers, include_stylesheet=includeStyleSheet, fallback_title=baseName, webenv=webenv) def updatePreviewBox(self): self.previewBlocked = False if isinstance(self.previewBox, QTextEdit): scrollbar = self.previewBox.verticalScrollBar() scrollbarValue = scrollbar.value() distToBottom = scrollbar.maximum() - scrollbarValue try: html = self.getHtml(syncScroll=globalSettings.syncScroll) except Exception: return self.p.printError() if isinstance(self.previewBox, QTextEdit): self.previewBox.setHtml(html) self.previewBox.document().setDefaultFont(globalSettings.font) # If scrollbar was at bottom (and that was not the same as top), # set it to bottom again if scrollbarValue: newValue = scrollbar.maximum() - distToBottom scrollbar.setValue(newValue) else: settings = self.previewBox.settings() settings.setFontFamily(QWebSettings.StandardFont, globalSettings.font.family()) settings.setFontSize(QWebSettings.DefaultFontSize, globalSettings.font.pointSize()) self.previewBox.setHtml(html, QUrl.fromLocalFile(self.fileName)) def updateLivePreviewBox(self): if self.previewState == PreviewLive and not self.previewBlocked: self.previewBlocked = True QTimer.singleShot(1000, self.updatePreviewBox) def updateBoxesVisibility(self): self.editBox.setVisible(self.previewState < PreviewNormal) self.previewBox.setVisible(self.previewState > PreviewDisabled) def setMarkupClass(self, markupClass): self.markup = None if markupClass and markupClass.available: self.markup = markupClass(filename=self.fileName) self.highlighter.docType = markupClass.name if markupClass else None self.highlighter.rehighlight() def readTextFromFile(self, encoding=None): openfile = QFile(self.fileName) openfile.open(QFile.ReadOnly) stream = QTextStream(openfile) encoding = encoding or globalSettings.defaultCodec if encoding: stream.setCodec(encoding) text = stream.readAll() openfile.close() markupClass = get_markup_for_file_name(self.fileName, return_class=True) self.setMarkupClass(markupClass) modified = bool(encoding) and (self.editBox.toPlainText() != text) self.editBox.setPlainText(text) self.editBox.document().setModified(modified) def saveTextToFile(self, fileName=None, addToWatcher=True): if fileName is None: fileName = self.fileName self.p.fileSystemWatcher.removePath(fileName) savefile = QFile(fileName) result = savefile.open(QFile.WriteOnly) if result: savestream = QTextStream(savefile) if globalSettings.defaultCodec: savestream.setCodec(globalSettings.defaultCodec) savestream << self.editBox.toPlainText() savefile.close() if result and addToWatcher: self.p.fileSystemWatcher.addPath(fileName) return result def installFakeVimHandler(self): if ReTextFakeVimHandler: fakeVimEditor = ReTextFakeVimHandler(self.editBox, self) fakeVimEditor.setSaveAction(self.actionSave) fakeVimEditor.setQuitAction(self.actionQuit) # TODO: action is bool, really call remove? self.p.actionFakeVimMode.triggered.connect(fakeVimEditor.remove)
class ReTextTab(QSplitter): fileNameChanged = pyqtSignal() modificationStateChanged = pyqtSignal() activeMarkupChanged = pyqtSignal() # Make _fileName a read-only property to make sure that any # modification happens through the proper functions. These functions # will make sure that the fileNameChanged signal is emitted when # applicable. @property def fileName(self): return self._fileName def __init__(self, parent, fileName, defaultMarkup, previewState=PreviewDisabled): super(QSplitter, self).__init__(Qt.Horizontal, parent=parent) self.p = parent self._fileName = fileName self.editBox = ReTextEdit(self) self.previewBox = self.createPreviewBox(self.editBox) self.defaultMarkupClass = defaultMarkup self.activeMarkupClass = None self.markup = None self.converted = None self.previewState = previewState self.previewOutdated = False self.conversionPending = False self.converterProcess = converterprocess.ConverterProcess() self.converterProcess.conversionDone.connect(self.updatePreviewBox) textDocument = self.editBox.document() self.highlighter = ReTextHighlighter(textDocument) if enchant_available and parent.actionEnableSC.isChecked(): self.highlighter.dictionary = enchant.Dict(parent.sl or None) # Rehighlighting is tied to the change in markup class that # happens at the end of this function self.editBox.textChanged.connect(self.triggerPreviewUpdate) self.editBox.undoAvailable.connect(parent.actionUndo.setEnabled) self.editBox.redoAvailable.connect(parent.actionRedo.setEnabled) self.editBox.copyAvailable.connect(parent.actionCopy.setEnabled) # Give both boxes a minimum size so the minimumSizeHint will be # ignored when splitter.setSizes is called below for widget in self.editBox, self.previewBox: widget.setMinimumWidth(125) self.addWidget(widget) self.setSizes((50, 50)) self.setChildrenCollapsible(False) textDocument.modificationChanged.connect(self.handleModificationChanged) self.updateActiveMarkupClass() def handleModificationChanged(self): self.modificationStateChanged.emit() def createPreviewBox(self, editBox): # Use closures to avoid a hard reference from ReTextWebPreview # to self, which would keep the tab and its resources alive # even after other references to it have disappeared. def editorPositionToSourceLine(editorPosition): viewportPosition = editorPosition - editBox.verticalScrollBar().value() sourceLine = editBox.cursorForPosition(QPoint(0,viewportPosition)).blockNumber() return sourceLine def sourceLineToEditorPosition(sourceLine): doc = editBox.document() block = doc.findBlockByNumber(sourceLine) rect = doc.documentLayout().blockBoundingRect(block) return rect.top() if globalSettings.useWebKit: preview = ReTextWebPreview(editBox, editorPositionToSourceLine, sourceLineToEditorPosition) else: preview = ReTextPreview(self) return preview def setDefaultMarkupClass(self, markupClass): ''' Set the default markup class to use in case a markup that matches the filename cannot be found. This function calls updateActiveMarkupClass so it can decide if the active markup class also has to change. ''' self.defaultMarkupClass = markupClass self.updateActiveMarkupClass() def getActiveMarkupClass(self): ''' Return the currently active markup class for this tab. No objects should be created of this class, it should only be used to retrieve markup class specific information. ''' return self.activeMarkupClass def updateActiveMarkupClass(self): ''' Update the active markup class based on the default class and the current filename. If the active markup class changes, the highlighter is rerun on the input text, the markup object of this tab is replaced with one of the new class and the activeMarkupChanged signal is emitted. ''' previousMarkupClass = self.activeMarkupClass self.activeMarkupClass = self.defaultMarkupClass if self._fileName: markupClass = get_markup_for_file_name( self._fileName, return_class=True) if markupClass: self.activeMarkupClass = markupClass if self.activeMarkupClass != previousMarkupClass: self.highlighter.docType = self.activeMarkupClass.name if self.activeMarkupClass else None self.highlighter.rehighlight() self.activeMarkupChanged.emit() self.triggerPreviewUpdate() def getDocumentTitleFromConverted(self, converted): if converted: try: return converted.get_document_title() except Exception: self.p.printError() return getBaseName() def getBaseName(self): if self._fileName: fileinfo = QFileInfo(self._fileName) basename = fileinfo.completeBaseName() return (basename if basename else fileinfo.fileName()) return self.tr("New document") def getHtmlFromConverted(self, converted, includeStyleSheet=True, webenv=False): if converted is None: markupClass = self.getActiveMarkupClass() errMsg = self.tr('Could not parse file contents, check if ' 'you have the <a href="%s">necessary module</a> ' 'installed!') try: errMsg %= markupClass.attributes[MODULE_HOME_PAGE] except (AttributeError, KeyError): # Remove the link if markupClass doesn't have the needed attribute errMsg = errMsg.replace('<a href="%s">', '').replace('</a>', '') return '<p style="color: red">%s</p>' % errMsg headers = '' if includeStyleSheet: headers += '<style type="text/css">\n' + self.p.ss + '</style>\n' baseName = self.getBaseName() cssFileName = baseName + '.css' if QFile.exists(cssFileName): headers += ('<link rel="stylesheet" type="text/css" href="%s">\n' % cssFileName) headers += ('<meta name="generator" content="ReText %s">\n' % app_version) return converted.get_whole_html( custom_headers=headers, include_stylesheet=includeStyleSheet, fallback_title=baseName, webenv=webenv) def getDocumentForExport(self, includeStyleSheet, webenv): markupClass = self.getActiveMarkupClass() if markupClass and markupClass.available(): exportMarkup = markupClass(filename=self._fileName) text = self.editBox.toPlainText() converted = exportMarkup.convert(text) else: converted = None return (self.getDocumentTitleFromConverted(converted), self.getHtmlFromConverted(converted, includeStyleSheet=includeStyleSheet, webenv=webenv), self.previewBox) def updatePreviewBox(self): self.conversionPending = False try: self.converted = self.converterProcess.get_result() except converterprocess.ConversionError: self.converted = None if isinstance(self.previewBox, QTextEdit): scrollbar = self.previewBox.verticalScrollBar() scrollbarValue = scrollbar.value() distToBottom = scrollbar.maximum() - scrollbarValue try: html = self.getHtmlFromConverted(self.converted) except Exception: return self.p.printError() if isinstance(self.previewBox, QTextEdit): self.previewBox.setHtml(html) self.previewBox.document().setDefaultFont(globalSettings.font) # If scrollbar was at bottom (and that was not the same as top), # set it to bottom again if scrollbarValue: newValue = scrollbar.maximum() - distToBottom scrollbar.setValue(newValue) else: settings = self.previewBox.settings() settings.setFontFamily(QWebSettings.StandardFont, globalSettings.font.family()) settings.setFontSize(QWebSettings.DefaultFontSize, globalSettings.font.pointSize()) self.previewBox.setHtml(html, QUrl.fromLocalFile(self._fileName)) if self.previewOutdated: self.triggerPreviewUpdate() def triggerPreviewUpdate(self): self.previewOutdated = True if not self.conversionPending: self.conversionPending = True QTimer.singleShot(500, self.startPendingConversion) def startPendingConversion(self): self.previewOutdated = False requested_extensions = ['ReText.mdx_posmap'] if globalSettings.syncScroll else [] self.converterProcess.start_conversion(self.getActiveMarkupClass().name, self.fileName, requested_extensions, self.editBox.toPlainText()) def updateBoxesVisibility(self): self.editBox.setVisible(self.previewState < PreviewNormal) self.previewBox.setVisible(self.previewState > PreviewDisabled) def readTextFromFile(self, fileName=None, encoding=None): previousFileName = self._fileName if fileName: self._fileName = fileName openfile = QFile(self._fileName) openfile.open(QFile.ReadOnly) stream = QTextStream(openfile) encoding = encoding or globalSettings.defaultCodec if encoding: stream.setCodec(encoding) text = stream.readAll() openfile.close() modified = bool(encoding) and (self.editBox.toPlainText() != text) self.editBox.setPlainText(text) self.editBox.document().setModified(modified) if previousFileName != self._fileName: self.updateActiveMarkupClass() self.fileNameChanged.emit() def saveTextToFile(self, fileName=None, addToWatcher=True): previousFileName = self._fileName if fileName: self._fileName = fileName self.p.fileSystemWatcher.removePath(previousFileName) savefile = QFile(self._fileName) result = savefile.open(QFile.WriteOnly) if result: savestream = QTextStream(savefile) if globalSettings.defaultCodec: savestream.setCodec(globalSettings.defaultCodec) savestream << self.editBox.toPlainText() savefile.close() self.editBox.document().setModified(False) if result and addToWatcher: self.p.fileSystemWatcher.addPath(self._fileName) if previousFileName != self._fileName: self.updateActiveMarkupClass() self.fileNameChanged.emit() return result def installFakeVimHandler(self): if ReTextFakeVimHandler: fakeVimEditor = ReTextFakeVimHandler(self.editBox, self) fakeVimEditor.setSaveAction(self.actionSave) fakeVimEditor.setQuitAction(self.actionQuit) # TODO: action is bool, really call remove? self.p.actionFakeVimMode.triggered.connect(fakeVimEditor.remove) def find(self, text, flags): cursor = self.editBox.textCursor() newCursor = self.editBox.document().find(text, cursor, flags) if not newCursor.isNull(): self.editBox.setTextCursor(newCursor) return True cursor.movePosition(QTextCursor.End if (flags & QTextDocument.FindBackward) else QTextCursor.Start) newCursor = self.editBox.document().find(text, cursor, flags) if not newCursor.isNull(): self.editBox.setTextCursor(newCursor) return True return False
class ReTextTab(QObject): def __init__(self, parent, fileName, previewState=PreviewDisabled): QObject.__init__(self, parent) self.p = parent self.fileName = fileName self.editBox = ReTextEdit(self) self.previewBox = self.createPreviewBox(self.editBox) self.markup = self.getMarkup() self.previewState = previewState self.previewBlocked = False textDocument = self.editBox.document() self.highlighter = ReTextHighlighter(textDocument) if enchant_available and parent.actionEnableSC.isChecked(): self.highlighter.dictionary = enchant.Dict(parent.sl or None) self.highlighter.rehighlight() self.highlighter.docType = self.markup.name self.editBox.textChanged.connect(self.updateLivePreviewBox) self.editBox.undoAvailable.connect(parent.actionUndo.setEnabled) self.editBox.redoAvailable.connect(parent.actionRedo.setEnabled) self.editBox.copyAvailable.connect(parent.actionCopy.setEnabled) textDocument.modificationChanged.connect(parent.modificationChanged) self.updateBoxesVisibility() def editorPositionToSourceLine(self, editorPosition): viewportPosition = editorPosition - self.editBox.verticalScrollBar().value() sourceLine = self.editBox.cursorForPosition(QPoint(0,viewportPosition)).blockNumber() return sourceLine def sourceLineToEditorPosition(self, sourceLine): doc = self.editBox.document() block = doc.findBlockByNumber(sourceLine) rect = doc.documentLayout().blockBoundingRect(block) return rect.top() def createPreviewBox(self, editBox): if globalSettings.useWebKit: preview = ReTextWebPreview(editBox, self.editorPositionToSourceLine, self.sourceLineToEditorPosition) else: preview = ReTextPreview(self) return preview def getSplitter(self): splitter = QSplitter(Qt.Horizontal) # Give both boxes a minimum size so the minimumSizeHint will be # ignored when splitter.setSizes is called below for widget in self.editBox, self.previewBox: widget.setMinimumWidth(125) splitter.addWidget(widget) splitter.setSizes((50, 50)) splitter.setChildrenCollapsible(False) splitter.tab = self return splitter def getMarkupClass(self): if self.fileName: markupClass = get_markup_for_file_name( self.fileName, return_class=True) if markupClass: return markupClass return self.p.defaultMarkup def getMarkup(self): markupClass = self.getMarkupClass() if markupClass and markupClass.available(): return markupClass(filename=self.fileName) def getDocumentTitle(self, baseName=False): if self.markup and not baseName: text = self.editBox.toPlainText() try: return self.markup.get_document_title(text) except Exception: self.p.printError() if self.fileName: fileinfo = QFileInfo(self.fileName) basename = fileinfo.completeBaseName() return (basename if basename else fileinfo.fileName()) return self.tr("New document") def getHtml(self, includeStyleSheet=True, webenv=False, syncScroll=False): if self.markup is None: markupClass = self.getMarkupClass() errMsg = self.tr('Could not parse file contents, check if ' 'you have the <a href="%s">necessary module</a> ' 'installed!') try: errMsg %= markupClass.attributes[MODULE_HOME_PAGE] except (AttributeError, KeyError): # Remove the link if markupClass doesn't have the needed attribute errMsg = errMsg.replace('<a href="%s">', '').replace('</a>', '') return '<p style="color: red">%s</p>' % errMsg text = self.editBox.toPlainText() headers = '' if includeStyleSheet: headers += '<style type="text/css">\n' + self.p.ss + '</style>\n' baseName = self.getDocumentTitle(baseName=True) cssFileName = baseName + '.css' if QFile.exists(cssFileName): headers += ('<link rel="stylesheet" type="text/css" href="%s">\n' % cssFileName) headers += ('<meta name="generator" content="ReText %s">\n' % app_version) self.markup.requested_extensions = [] if syncScroll: self.markup.requested_extensions.append('ReText.mdx_posmap') return self.markup.get_whole_html(text, custom_headers=headers, include_stylesheet=includeStyleSheet, fallback_title=baseName, webenv=webenv) def updatePreviewBox(self): self.previewBlocked = False if isinstance(self.previewBox, QTextEdit): scrollbar = self.previewBox.verticalScrollBar() scrollbarValue = scrollbar.value() distToBottom = scrollbar.maximum() - scrollbarValue try: html = self.getHtml(syncScroll=globalSettings.syncScroll) except Exception: return self.p.printError() if isinstance(self.previewBox, QTextEdit): self.previewBox.setHtml(html) self.previewBox.document().setDefaultFont(globalSettings.font) # If scrollbar was at bottom (and that was not the same as top), # set it to bottom again if scrollbarValue: newValue = scrollbar.maximum() - distToBottom scrollbar.setValue(newValue) else: settings = self.previewBox.settings() settings.setFontFamily(QWebSettings.StandardFont, globalSettings.font.family()) settings.setFontSize(QWebSettings.DefaultFontSize, globalSettings.font.pointSize()) self.previewBox.setHtml(html, QUrl.fromLocalFile(self.fileName)) def updateLivePreviewBox(self): if self.previewState == PreviewLive and not self.previewBlocked: self.previewBlocked = True QTimer.singleShot(1000, self.updatePreviewBox) def updateBoxesVisibility(self): self.editBox.setVisible(self.previewState < PreviewNormal) self.previewBox.setVisible(self.previewState > PreviewDisabled) def setMarkupClass(self, markupClass): self.markup = None if markupClass and markupClass.available: self.markup = markupClass(filename=self.fileName) self.highlighter.docType = markupClass.name if markupClass else None self.highlighter.rehighlight() def readTextFromFile(self, encoding=None): openfile = QFile(self.fileName) openfile.open(QFile.ReadOnly) stream = QTextStream(openfile) encoding = encoding or globalSettings.defaultCodec if encoding: stream.setCodec(encoding) text = stream.readAll() openfile.close() markupClass = get_markup_for_file_name(self.fileName, return_class=True) self.setMarkupClass(markupClass) modified = bool(encoding) and (self.editBox.toPlainText() != text) self.editBox.setPlainText(text) self.editBox.document().setModified(modified) def saveTextToFile(self, fileName=None, addToWatcher=True): if fileName is None: fileName = self.fileName self.p.fileSystemWatcher.removePath(fileName) savefile = QFile(fileName) result = savefile.open(QFile.WriteOnly) if result: savestream = QTextStream(savefile) if globalSettings.defaultCodec: savestream.setCodec(globalSettings.defaultCodec) savestream << self.editBox.toPlainText() savefile.close() if result and addToWatcher: self.p.fileSystemWatcher.addPath(fileName) return result def installFakeVimHandler(self): if ReTextFakeVimHandler: fakeVimEditor = ReTextFakeVimHandler(self.editBox, self) fakeVimEditor.setSaveAction(self.actionSave) fakeVimEditor.setQuitAction(self.actionQuit) # TODO: action is bool, really call remove? self.p.actionFakeVimMode.triggered.connect(fakeVimEditor.remove)