--- a/UI/Previewer.py Mon Mar 31 19:13:22 2014 +0200 +++ b/UI/Previewer.py Mon Mar 31 19:27:31 2014 +0200 @@ -8,24 +8,17 @@ """ import os -import threading -import re -from PyQt4.QtCore import pyqtSlot, pyqtSignal, Qt, QTimer, QSize, QUrl, QThread -from PyQt4.QtGui import QWidget -from PyQt4.QtWebKit import QWebPage - -from E5Gui.E5Application import e5App - -from .Ui_Previewer import Ui_Previewer +from PyQt4.QtCore import QTimer +from PyQt4.QtGui import QStackedWidget import Preferences -import Utilities -class Previewer(QWidget, Ui_Previewer): +class Previewer(QStackedWidget): """ - Class implementing a previewer widget for HTML, Markdown and ReST files. + Class implementing a previewer widget containing a stack of + specialized previewers. """ def __init__(self, viewmanager, splitter, parent=None): """ @@ -36,29 +29,18 @@ @param parent reference to the parent widget (QWidget) """ super().__init__(parent) - self.setupUi(self) self.__vm = viewmanager self.__splitter = splitter self.__firstShow = True - self.previewView.page().setLinkDelegationPolicy( - QWebPage.DelegateAllLinks) + self.__htmlPreviewer = None # Don't update too often because the UI might become sluggish self.__typingTimer = QTimer() self.__typingTimer.setInterval(500) # 500ms - self.__typingTimer.timeout.connect(self.__runProcessingThread) - - self.__scrollBarPositions = {} - self.__vScrollBarAtEnd = {} - self.__hScrollBarAtEnd = {} - - self.__processingThread = PreviewProcessingThread() - self.__processingThread.htmlReady.connect(self.__setHtml) - - self.__previewedPath = None + self.__typingTimer.timeout.connect(self.__processEditor) self.__vm.editorChangedEd.connect(self.__editorChanged) self.__vm.editorLanguageChanged.connect(self.__editorLanguageChanged) @@ -78,10 +60,6 @@ if self.__firstShow: self.__splitter.restoreState( Preferences.getUI("PreviewSplitterState")) - self.jsCheckBox.setChecked( - Preferences.getUI("ShowFilePreviewJS")) - self.ssiCheckBox.setChecked( - Preferences.getUI("ShowFilePreviewSSI")) self.__firstShow = False self.__typingTimer.start() @@ -97,7 +75,7 @@ Public method to perform shutdown actions. """ self.__typingTimer.stop() - self.__processingThread.wait() + self.__htmlPreviewer and self.__htmlPreviewer.shutdown() def __splitterMoved(self): """ @@ -106,39 +84,6 @@ state = self.__splitter.saveState() Preferences.setUI("PreviewSplitterState", state) - @pyqtSlot(bool) - def on_jsCheckBox_clicked(self, checked): - """ - Private slot to enable/disable JavaScript. - - @param checked state of the checkbox (boolean) - """ - Preferences.setUI("ShowFilePreviewJS", checked) - self.__setJavaScriptEnabled(checked) - - def __setJavaScriptEnabled(self, enable): - """ - Private method to enable/disable JavaScript. - - @param enable flag indicating the enable state (boolean) - """ - self.jsCheckBox.setChecked(enable) - - settings = self.previewView.settings() - settings.setAttribute(settings.JavascriptEnabled, enable) - - self.__runProcessingThread() - - @pyqtSlot(bool) - def on_ssiCheckBox_clicked(self, checked): - """ - Private slot to enable/disable SSI. - - @param checked state of the checkbox (boolean) - """ - Preferences.setUI("ShowFilePreviewSSI", checked) - self.__runProcessingThread() - def __editorChanged(self, editor): """ Private slot to handle a change of the current editor. @@ -152,7 +97,7 @@ if Preferences.getUI("ShowFilePreview") and \ self.__isPreviewable(editor): self.show() - self.__runProcessingThread() + self.__processEditor() else: self.hide() @@ -208,7 +153,7 @@ return False - def __runProcessingThread(self): + def __processEditor(self): """ Private slot to schedule the processing of the current editor's text. """ @@ -232,311 +177,19 @@ elif extension in \ Preferences.getEditor("PreviewRestFileNameExtensions"): language = "ReST" - else: - self.__setHtml(fn, self.tr( - "<p>No preview available for this type of file.</p>")) - return - - if fn: - project = e5App().getObject("Project") - if project.isProjectFile(fn): - rootPath = project.getProjectPath() - else: - rootPath = os.path.dirname(os.path.abspath(fn)) + elif extension in \ + Preferences.getEditor("PreviewQssFileNameExtensions"): + language = "QSS" else: - rootPath = "" - - self.__processingThread.process( - fn, language, editor.text(), - self.ssiCheckBox.isChecked(), rootPath) - - def __setHtml(self, filePath, html): - """ - Private method to set the HTML to the view and restore the scroll bars - positions. - - @param filePath file path of the previewed editor (string) - @param html processed HTML text ready to be shown (string) - """ - self.__saveScrollBarPositions() - self.__previewedPath = Utilities.normcasepath( - Utilities.fromNativeSeparators(filePath)) - self.previewView.page().mainFrame().contentsSizeChanged.connect( - self.__restoreScrollBarPositions) - self.previewView.setHtml(html, baseUrl=QUrl.fromLocalFile(filePath)) - - @pyqtSlot(str) - def on_previewView_titleChanged(self, title): - """ - Private slot to handle a change of the title. - - @param title new title (string) - """ - if title: - self.titleLabel.setText(self.tr("Preview - {0}").format(title)) - else: - self.titleLabel.setText(self.tr("Preview")) - - def __saveScrollBarPositions(self): - """ - Private method to save scroll bar positions for a previewed editor. - """ - frame = self.previewView.page().mainFrame() - if frame.contentsSize() == QSize(0, 0): - return # no valid data, nothing to save - - pos = frame.scrollPosition() - self.__scrollBarPositions[self.__previewedPath] = pos - self.__hScrollBarAtEnd[self.__previewedPath] = \ - frame.scrollBarMaximum(Qt.Horizontal) == pos.x() - self.__vScrollBarAtEnd[self.__previewedPath] = \ - frame.scrollBarMaximum(Qt.Vertical) == pos.y() - - def __restoreScrollBarPositions(self): - """ - Private method to restore scroll bar positions for a previewed editor. - """ - try: - self.previewView.page().mainFrame().contentsSizeChanged.disconnect( - self.__restoreScrollBarPositions) - except TypeError: - # not connected, simply ignore it - pass - - if self.__previewedPath not in self.__scrollBarPositions: - return - - frame = self.previewView.page().mainFrame() - frame.setScrollPosition( - self.__scrollBarPositions[self.__previewedPath]) - - if self.__hScrollBarAtEnd[self.__previewedPath]: - frame.setScrollBarValue( - Qt.Horizontal, frame.scrollBarMaximum(Qt.Horizontal)) - - if self.__vScrollBarAtEnd[self.__previewedPath]: - frame.setScrollBarValue( - Qt.Vertical, frame.scrollBarMaximum(Qt.Vertical)) - - @pyqtSlot(QUrl) - def on_previewView_linkClicked(self, url): - """ - Private slot handling the clicking of a link. - - @param url url of the clicked link (QUrl) - """ - e5App().getObject("UserInterface").launchHelpViewer(url.toString()) - - -class PreviewProcessingThread(QThread): - """ - Class implementing a thread to process some text into HTML usable by the - previewer view. - - @signal htmlReady(str,str) emitted with the file name and processed HTML - to signal the availability of the processed HTML - """ - htmlReady = pyqtSignal(str, str) - - def __init__(self, parent=None): - """ - Constructor - - @param parent reference to the parent object (QObject) - """ - super().__init__() - - self.__lock = threading.Lock() - - def process(self, filePath, language, text, ssiEnabled, rootPath): - """ - Convert the given text to HTML. - - @param filePath file path of the text (string) - @param language language of the text (string) - @param text text to be processed (string) - @param ssiEnabled flag indicating to do some (limited) SSI processing - (boolean) - @param rootPath root path to be used for SSI processing (str) - """ - with self.__lock: - self.__filePath = filePath - self.__language = language - self.__text = text - self.__ssiEnabled = ssiEnabled - self.__rootPath = rootPath - self.__haveData = True - if not self.isRunning(): - self.start(QThread.LowPriority) - - def run(self): - """ - Thread function to convert the stored data. - """ - while True: - # exits with break - with self.__lock: - filePath = self.__filePath - language = self.__language - text = self.__text - ssiEnabled = self.__ssiEnabled - rootPath = self.__rootPath - self.__haveData = False - - html = self.__getHtml(language, text, ssiEnabled, filePath, - rootPath) + language = "" - with self.__lock: - if not self.__haveData: - self.htmlReady.emit(filePath, html) - break - # else - next iteration - - def __getHtml(self, language, text, ssiEnabled, filePath, rootPath): - """ - Private method to process the given text depending upon the given - language. - - @param language language of the text (string) - @param text to be processed (string) - @param ssiEnabled flag indicating to do some (limited) SSI processing - (boolean) - @param filePath file path of the text (string) - @param rootPath root path to be used for SSI processing (str) - @return processed HTML text (string) - """ - if language == "HTML": - if ssiEnabled: - return self.__processSSI(text, filePath, rootPath) - else: - return text - elif language == "Markdown": - return self.__convertMarkdown(text) - elif language == "ReST": - return self.__convertReST(text) - else: - return self.tr( - "<p>No preview available for this type of file.</p>") - - def __processSSI(self, txt, filename, root): - """ - Private method to process the given text for SSI statements. - - Note: Only a limited subset of SSI statements are supported. - - @param txt text to be processed (string) - @param filename name of the file associated with the given text - (string) - @param root directory of the document root (string) - @return processed HTML (string) - """ - if not filename: - return txt - - # SSI include - incRe = re.compile( - r"""<!--#include[ \t]+(virtual|file)=[\"']([^\"']+)[\"']\s*-->""", - re.IGNORECASE) - baseDir = os.path.dirname(os.path.abspath(filename)) - docRoot = root if root != "" else baseDir - while True: - incMatch = incRe.search(txt) - if incMatch is None: - break - - if incMatch.group(1) == "virtual": - incFile = Utilities.normjoinpath(docRoot, incMatch.group(2)) - elif incMatch.group(1) == "file": - incFile = Utilities.normjoinpath(baseDir, incMatch.group(2)) - else: - incFile = "" - if os.path.exists(incFile): - try: - f = open(incFile, "r") - incTxt = f.read() - f.close() - except (IOError, OSError): - # remove SSI include - incTxt = "" - else: - # remove SSI include - incTxt = "" - txt = txt[:incMatch.start(0)] + incTxt + txt[incMatch.end(0):] - - return txt - - def __convertReST(self, text): - """ - Private method to convert ReST text into HTML. - - @param text text to be processed (string) - @return processed HTML (string) - """ - try: - import docutils.core # __IGNORE_EXCEPTION__ __IGNORE_WARNING__ - except ImportError: - return self.tr( - """<p>ReStructuredText preview requires the""" - """ <b>python-docutils</b> package.<br/>Install it with""" - """ your package manager or see""" - """ <a href="http://pypi.python.org/pypi/docutils">""" - """this page.</a></p>""") - - return docutils.core.publish_string(text, writer_name='html')\ - .decode("utf-8") - - def __convertMarkdown(self, text): - """ - Private method to convert Markdown text into HTML. - - @param text text to be processed (string) - @return processed HTML (string) - """ - try: - import markdown # __IGNORE_EXCEPTION__ __IGNORE_WARNING__ - except ImportError: - return self.tr( - """<p>Markdown preview requires the <b>python-markdown</b> """ - """package.<br/>Install it with your package manager or see """ - """<a href="http://pythonhosted.org/Markdown/install.html">""" - """installation instructions.</a></p>""") - - try: - import mdx_mathjax # __IGNORE_EXCEPTION__ __IGNORE_WARNING__ - except ImportError: - #mathjax doesn't require import statement if installed as extension - pass - - extensions = ['fenced_code', 'nl2br', 'extra'] - - # version 2.0 supports only extension names, not instances - if markdown.version_info[0] > 2 or \ - (markdown.version_info[0] == 2 and - markdown.version_info[1] > 0): - class _StrikeThroughExtension(markdown.Extension): - """ - Class is placed here, because it depends on imported markdown, - and markdown import is lazy. - - (see http://achinghead.com/ - python-markdown-adding-insert-delete.html this page for - details) - """ - DEL_RE = r'(~~)(.*?)~~' - - def extendMarkdown(self, md, md_globals): - # Create the del pattern - del_tag = markdown.inlinepatterns.SimpleTagPattern( - self.DEL_RE, 'del') - # Insert del pattern into markdown parser - md.inlinePatterns.add('del', del_tag, '>not_strong') - - extensions.append(_StrikeThroughExtension()) - - try: - return markdown.markdown(text, extensions + ['mathjax']) - except (ImportError, ValueError): - # markdown raises ValueError or ImportError, depends on version - # It is not clear, how to distinguish missing mathjax from other - # errors. So keep going without mathjax. - return markdown.markdown(text, extensions) + if language in ["HTML", "Markdown", "ReST"]: + if self.__htmlPreviewer is None: + from .Previewers.PreviewerHTML import PreviewerHTML + self.__htmlPreviewer = PreviewerHTML() + self.addWidget(self.__htmlPreviewer) + self.setCurrentWidget(self.__htmlPreviewer) + self.__htmlPreviewer.processEditor(editor) + elif language == "QSS": + # TODO: add QSS + pass