UI/Previewer.py

branch
Py2 comp.
changeset 3515
1b8381afe38f
parent 3484
645c12de6b0c
parent 3459
275cb00c83e2
child 3591
2f2a4a76dd22
equal deleted inserted replaced
3506:d85fadb263a0 3515:1b8381afe38f
8 """ 8 """
9 9
10 from __future__ import unicode_literals 10 from __future__ import unicode_literals
11 11
12 import os 12 import os
13 import threading 13
14 import re 14 from PyQt4.QtCore import QTimer
15 15 from PyQt4.QtGui import QStackedWidget
16 from PyQt4.QtCore import pyqtSlot, pyqtSignal, Qt, QTimer, QSize, QUrl, QThread
17 from PyQt4.QtGui import QWidget
18 from PyQt4.QtWebKit import QWebPage
19
20 from E5Gui.E5Application import e5App
21
22 from .Ui_Previewer import Ui_Previewer
23 16
24 import Preferences 17 import Preferences
25 import Utilities 18
26 19
27 20 class Previewer(QStackedWidget):
28 class Previewer(QWidget, Ui_Previewer):
29 """ 21 """
30 Class implementing a previewer widget for HTML, Markdown and ReST files. 22 Class implementing a previewer widget containing a stack of
23 specialized previewers.
31 """ 24 """
32 def __init__(self, viewmanager, splitter, parent=None): 25 def __init__(self, viewmanager, splitter, parent=None):
33 """ 26 """
34 Constructor 27 Constructor
35 28
36 @param viewmanager reference to the viewmanager object (ViewManager) 29 @param viewmanager reference to the viewmanager object (ViewManager)
37 @param splitter reference to the embedding splitter (QSplitter) 30 @param splitter reference to the embedding splitter (QSplitter)
38 @param parent reference to the parent widget (QWidget) 31 @param parent reference to the parent widget (QWidget)
39 """ 32 """
40 super(Previewer, self).__init__(parent) 33 super(Previewer, self).__init__(parent)
41 self.setupUi(self)
42 34
43 self.__vm = viewmanager 35 self.__vm = viewmanager
44 self.__splitter = splitter 36 self.__splitter = splitter
45 37
46 self.__firstShow = True 38 self.__firstShow = True
47 39
48 self.previewView.page().setLinkDelegationPolicy( 40 self.__htmlPreviewer = None
49 QWebPage.DelegateAllLinks) 41 self.__qssPreviewer = None
50 42
51 # Don't update too often because the UI might become sluggish 43 # Don't update too often because the UI might become sluggish
52 self.__typingTimer = QTimer() 44 self.__typingTimer = QTimer()
53 self.__typingTimer.setInterval(500) # 500ms 45 self.__typingTimer.setInterval(500) # 500ms
54 self.__typingTimer.timeout.connect(self.__runProcessingThread) 46 self.__typingTimer.timeout.connect(self.__processEditor)
55
56 self.__scrollBarPositions = {}
57 self.__vScrollBarAtEnd = {}
58 self.__hScrollBarAtEnd = {}
59
60 self.__processingThread = PreviewProcessingThread()
61 self.__processingThread.htmlReady.connect(self.__setHtml)
62
63 self.__previewedPath = None
64 47
65 self.__vm.editorChangedEd.connect(self.__editorChanged) 48 self.__vm.editorChangedEd.connect(self.__editorChanged)
66 self.__vm.editorLanguageChanged.connect(self.__editorLanguageChanged) 49 self.__vm.editorLanguageChanged.connect(self.__editorLanguageChanged)
67 self.__vm.editorTextChanged.connect(self.__editorTextChanged) 50 self.__vm.editorTextChanged.connect(self.__editorTextChanged)
68 51
78 """ 61 """
79 super(Previewer, self).show() 62 super(Previewer, self).show()
80 if self.__firstShow: 63 if self.__firstShow:
81 self.__splitter.restoreState( 64 self.__splitter.restoreState(
82 Preferences.getUI("PreviewSplitterState")) 65 Preferences.getUI("PreviewSplitterState"))
83 self.jsCheckBox.setChecked(
84 Preferences.getUI("ShowFilePreviewJS"))
85 self.ssiCheckBox.setChecked(
86 Preferences.getUI("ShowFilePreviewSSI"))
87 self.__firstShow = False 66 self.__firstShow = False
88 self.__typingTimer.start() 67 self.__typingTimer.start()
89 68
90 def hide(self): 69 def hide(self):
91 """ 70 """
97 def shutdown(self): 76 def shutdown(self):
98 """ 77 """
99 Public method to perform shutdown actions. 78 Public method to perform shutdown actions.
100 """ 79 """
101 self.__typingTimer.stop() 80 self.__typingTimer.stop()
102 self.__processingThread.wait() 81 self.__htmlPreviewer and self.__htmlPreviewer.shutdown()
103 82
104 def __splitterMoved(self): 83 def __splitterMoved(self):
105 """ 84 """
106 Private slot to handle the movement of the embedding splitter's handle. 85 Private slot to handle the movement of the embedding splitter's handle.
107 """ 86 """
108 state = self.__splitter.saveState() 87 state = self.__splitter.saveState()
109 Preferences.setUI("PreviewSplitterState", state) 88 Preferences.setUI("PreviewSplitterState", state)
110
111 @pyqtSlot(bool)
112 def on_jsCheckBox_clicked(self, checked):
113 """
114 Private slot to enable/disable JavaScript.
115
116 @param checked state of the checkbox (boolean)
117 """
118 Preferences.setUI("ShowFilePreviewJS", checked)
119 self.__setJavaScriptEnabled(checked)
120
121 def __setJavaScriptEnabled(self, enable):
122 """
123 Private method to enable/disable JavaScript.
124
125 @param enable flag indicating the enable state (boolean)
126 """
127 self.jsCheckBox.setChecked(enable)
128
129 settings = self.previewView.settings()
130 settings.setAttribute(settings.JavascriptEnabled, enable)
131
132 self.__runProcessingThread()
133
134 @pyqtSlot(bool)
135 def on_ssiCheckBox_clicked(self, checked):
136 """
137 Private slot to enable/disable SSI.
138
139 @param checked state of the checkbox (boolean)
140 """
141 Preferences.setUI("ShowFilePreviewSSI", checked)
142 self.__runProcessingThread()
143 89
144 def __editorChanged(self, editor): 90 def __editorChanged(self, editor):
145 """ 91 """
146 Private slot to handle a change of the current editor. 92 Private slot to handle a change of the current editor.
147 93
152 return 98 return
153 99
154 if Preferences.getUI("ShowFilePreview") and \ 100 if Preferences.getUI("ShowFilePreview") and \
155 self.__isPreviewable(editor): 101 self.__isPreviewable(editor):
156 self.show() 102 self.show()
157 self.__runProcessingThread() 103 self.__processEditor()
158 else: 104 else:
159 self.hide() 105 self.hide()
160 106
161 def __editorLanguageChanged(self, editor): 107 def __editorLanguageChanged(self, editor):
162 """ 108 """
202 os.path.splitext(editor.getFileName())[1][1:]) 148 os.path.splitext(editor.getFileName())[1][1:])
203 return extension in \ 149 return extension in \
204 Preferences.getEditor("PreviewHtmlFileNameExtensions") + \ 150 Preferences.getEditor("PreviewHtmlFileNameExtensions") + \
205 Preferences.getEditor( 151 Preferences.getEditor(
206 "PreviewMarkdownFileNameExtensions") + \ 152 "PreviewMarkdownFileNameExtensions") + \
207 Preferences.getEditor("PreviewRestFileNameExtensions") 153 Preferences.getEditor("PreviewRestFileNameExtensions") + \
208 elif editor.getLanguage() == "HTML": 154 Preferences.getEditor("PreviewQssFileNameExtensions")
155 elif editor.getLanguage() in ["HTML", "QSS"]:
209 return True 156 return True
210 157
211 return False 158 return False
212 159
213 def __runProcessingThread(self): 160 def __processEditor(self):
214 """ 161 """
215 Private slot to schedule the processing of the current editor's text. 162 Private slot to schedule the processing of the current editor's text.
216 """ 163 """
217 self.__typingTimer.stop() 164 self.__typingTimer.stop()
218 165
232 Preferences.getEditor("PreviewMarkdownFileNameExtensions"): 179 Preferences.getEditor("PreviewMarkdownFileNameExtensions"):
233 language = "Markdown" 180 language = "Markdown"
234 elif extension in \ 181 elif extension in \
235 Preferences.getEditor("PreviewRestFileNameExtensions"): 182 Preferences.getEditor("PreviewRestFileNameExtensions"):
236 language = "ReST" 183 language = "ReST"
184 elif extension in \
185 Preferences.getEditor("PreviewQssFileNameExtensions"):
186 language = "QSS"
237 else: 187 else:
238 self.__setHtml(fn, self.tr( 188 language = ""
239 "<p>No preview available for this type of file.</p>"))
240 return
241 189
242 if fn: 190 if language in ["HTML", "Markdown", "ReST"]:
243 project = e5App().getObject("Project") 191 if self.__htmlPreviewer is None:
244 if project.isProjectFile(fn): 192 from .Previewers.PreviewerHTML import PreviewerHTML
245 rootPath = project.getProjectPath() 193 self.__htmlPreviewer = PreviewerHTML()
246 else: 194 self.addWidget(self.__htmlPreviewer)
247 rootPath = os.path.dirname(os.path.abspath(fn)) 195 self.setCurrentWidget(self.__htmlPreviewer)
248 else: 196 self.__htmlPreviewer.processEditor(editor)
249 rootPath = "" 197 elif language == "QSS":
250 198 if self.__qssPreviewer is None:
251 self.__processingThread.process( 199 from .Previewers.PreviewerQSS import PreviewerQSS
252 fn, language, editor.text(), 200 self.__qssPreviewer = PreviewerQSS()
253 self.ssiCheckBox.isChecked(), rootPath) 201 self.addWidget(self.__qssPreviewer)
254 202 self.setCurrentWidget(self.__qssPreviewer)
255 def __setHtml(self, filePath, html): 203 self.__qssPreviewer.processEditor(editor)
256 """
257 Private method to set the HTML to the view and restore the scroll bars
258 positions.
259
260 @param filePath file path of the previewed editor (string)
261 @param html processed HTML text ready to be shown (string)
262 """
263 self.__saveScrollBarPositions()
264 self.__previewedPath = Utilities.normcasepath(
265 Utilities.fromNativeSeparators(filePath))
266 self.previewView.page().mainFrame().contentsSizeChanged.connect(
267 self.__restoreScrollBarPositions)
268 self.previewView.setHtml(html, baseUrl=QUrl.fromLocalFile(filePath))
269
270 @pyqtSlot(str)
271 def on_previewView_titleChanged(self, title):
272 """
273 Private slot to handle a change of the title.
274
275 @param title new title (string)
276 """
277 if title:
278 self.titleLabel.setText(self.tr("Preview - {0}").format(title))
279 else:
280 self.titleLabel.setText(self.tr("Preview"))
281
282 def __saveScrollBarPositions(self):
283 """
284 Private method to save scroll bar positions for a previewed editor.
285 """
286 frame = self.previewView.page().mainFrame()
287 if frame.contentsSize() == QSize(0, 0):
288 return # no valid data, nothing to save
289
290 pos = frame.scrollPosition()
291 self.__scrollBarPositions[self.__previewedPath] = pos
292 self.__hScrollBarAtEnd[self.__previewedPath] = \
293 frame.scrollBarMaximum(Qt.Horizontal) == pos.x()
294 self.__vScrollBarAtEnd[self.__previewedPath] = \
295 frame.scrollBarMaximum(Qt.Vertical) == pos.y()
296
297 def __restoreScrollBarPositions(self):
298 """
299 Private method to restore scroll bar positions for a previewed editor.
300 """
301 try:
302 self.previewView.page().mainFrame().contentsSizeChanged.disconnect(
303 self.__restoreScrollBarPositions)
304 except TypeError:
305 # not connected, simply ignore it
306 pass
307
308 if self.__previewedPath not in self.__scrollBarPositions:
309 return
310
311 frame = self.previewView.page().mainFrame()
312 frame.setScrollPosition(
313 self.__scrollBarPositions[self.__previewedPath])
314
315 if self.__hScrollBarAtEnd[self.__previewedPath]:
316 frame.setScrollBarValue(
317 Qt.Horizontal, frame.scrollBarMaximum(Qt.Horizontal))
318
319 if self.__vScrollBarAtEnd[self.__previewedPath]:
320 frame.setScrollBarValue(
321 Qt.Vertical, frame.scrollBarMaximum(Qt.Vertical))
322
323 @pyqtSlot(QUrl)
324 def on_previewView_linkClicked(self, url):
325 """
326 Private slot handling the clicking of a link.
327
328 @param url url of the clicked link (QUrl)
329 """
330 e5App().getObject("UserInterface").launchHelpViewer(url.toString())
331
332
333 class PreviewProcessingThread(QThread):
334 """
335 Class implementing a thread to process some text into HTML usable by the
336 previewer view.
337
338 @signal htmlReady(str,str) emitted with the file name and processed HTML
339 to signal the availability of the processed HTML
340 """
341 htmlReady = pyqtSignal(str, str)
342
343 def __init__(self, parent=None):
344 """
345 Constructor
346
347 @param parent reference to the parent object (QObject)
348 """
349 super(PreviewProcessingThread, self).__init__()
350
351 self.__lock = threading.Lock()
352
353 def process(self, filePath, language, text, ssiEnabled, rootPath):
354 """
355 Convert the given text to HTML.
356
357 @param filePath file path of the text (string)
358 @param language language of the text (string)
359 @param text text to be processed (string)
360 @param ssiEnabled flag indicating to do some (limited) SSI processing
361 (boolean)
362 @param rootPath root path to be used for SSI processing (str)
363 """
364 with self.__lock:
365 self.__filePath = filePath
366 self.__language = language
367 self.__text = text
368 self.__ssiEnabled = ssiEnabled
369 self.__rootPath = rootPath
370 self.__haveData = True
371 if not self.isRunning():
372 self.start(QThread.LowPriority)
373
374 def run(self):
375 """
376 Thread function to convert the stored data.
377 """
378 while True:
379 # exits with break
380 with self.__lock:
381 filePath = self.__filePath
382 language = self.__language
383 text = self.__text
384 ssiEnabled = self.__ssiEnabled
385 rootPath = self.__rootPath
386 self.__haveData = False
387
388 html = self.__getHtml(language, text, ssiEnabled, filePath,
389 rootPath)
390
391 with self.__lock:
392 if not self.__haveData:
393 self.htmlReady.emit(filePath, html)
394 break
395 # else - next iteration
396
397 def __getHtml(self, language, text, ssiEnabled, filePath, rootPath):
398 """
399 Private method to process the given text depending upon the given
400 language.
401
402 @param language language of the text (string)
403 @param text to be processed (string)
404 @param ssiEnabled flag indicating to do some (limited) SSI processing
405 (boolean)
406 @param filePath file path of the text (string)
407 @param rootPath root path to be used for SSI processing (str)
408 @return processed HTML text (string)
409 """
410 if language == "HTML":
411 if ssiEnabled:
412 return self.__processSSI(text, filePath, rootPath)
413 else:
414 return text
415 elif language == "Markdown":
416 return self.__convertMarkdown(text)
417 elif language == "ReST":
418 return self.__convertReST(text)
419 else:
420 return self.tr(
421 "<p>No preview available for this type of file.</p>")
422
423 def __processSSI(self, txt, filename, root):
424 """
425 Private method to process the given text for SSI statements.
426
427 Note: Only a limited subset of SSI statements are supported.
428
429 @param txt text to be processed (string)
430 @param filename name of the file associated with the given text
431 (string)
432 @param root directory of the document root (string)
433 @return processed HTML (string)
434 """
435 if not filename:
436 return txt
437
438 # SSI include
439 incRe = re.compile(
440 r"""<!--#include[ \t]+(virtual|file)=[\"']([^\"']+)[\"']\s*-->""",
441 re.IGNORECASE)
442 baseDir = os.path.dirname(os.path.abspath(filename))
443 docRoot = root if root != "" else baseDir
444 while True:
445 incMatch = incRe.search(txt)
446 if incMatch is None:
447 break
448
449 if incMatch.group(1) == "virtual":
450 incFile = Utilities.normjoinpath(docRoot, incMatch.group(2))
451 elif incMatch.group(1) == "file":
452 incFile = Utilities.normjoinpath(baseDir, incMatch.group(2))
453 else:
454 incFile = ""
455 if os.path.exists(incFile):
456 try:
457 f = open(incFile, "r")
458 incTxt = f.read()
459 f.close()
460 except (IOError, OSError):
461 # remove SSI include
462 incTxt = ""
463 else:
464 # remove SSI include
465 incTxt = ""
466 txt = txt[:incMatch.start(0)] + incTxt + txt[incMatch.end(0):]
467
468 return txt
469
470 def __convertReST(self, text):
471 """
472 Private method to convert ReST text into HTML.
473
474 @param text text to be processed (string)
475 @return processed HTML (string)
476 """
477 try:
478 import docutils.core # __IGNORE_EXCEPTION__ __IGNORE_WARNING__
479 except ImportError:
480 return self.tr(
481 """<p>ReStructuredText preview requires the"""
482 """ <b>python-docutils</b> package.<br/>Install it with"""
483 """ your package manager or see"""
484 """ <a href="http://pypi.python.org/pypi/docutils">"""
485 """this page.</a></p>""")
486
487 return docutils.core.publish_string(text, writer_name='html')\
488 .decode("utf-8")
489
490 def __convertMarkdown(self, text):
491 """
492 Private method to convert Markdown text into HTML.
493
494 @param text text to be processed (string)
495 @return processed HTML (string)
496 """
497 try:
498 import markdown # __IGNORE_EXCEPTION__ __IGNORE_WARNING__
499 except ImportError:
500 return self.tr(
501 """<p>Markdown preview requires the <b>python-markdown</b> """
502 """package.<br/>Install it with your package manager or see """
503 """<a href="http://pythonhosted.org/Markdown/install.html">"""
504 """installation instructions.</a></p>""")
505
506 try:
507 import mdx_mathjax # __IGNORE_EXCEPTION__ __IGNORE_WARNING__
508 except ImportError:
509 #mathjax doesn't require import statement if installed as extension
510 pass
511
512 extensions = ['fenced_code', 'nl2br', 'extra']
513
514 # version 2.0 supports only extension names, not instances
515 if markdown.version_info[0] > 2 or \
516 (markdown.version_info[0] == 2 and
517 markdown.version_info[1] > 0):
518 class _StrikeThroughExtension(markdown.Extension):
519 """
520 Class is placed here, because it depends on imported markdown,
521 and markdown import is lazy.
522
523 (see http://achinghead.com/
524 python-markdown-adding-insert-delete.html this page for
525 details)
526 """
527 DEL_RE = r'(~~)(.*?)~~'
528
529 def extendMarkdown(self, md, md_globals):
530 # Create the del pattern
531 del_tag = markdown.inlinepatterns.SimpleTagPattern(
532 self.DEL_RE, 'del')
533 # Insert del pattern into markdown parser
534 md.inlinePatterns.add('del', del_tag, '>not_strong')
535
536 extensions.append(_StrikeThroughExtension())
537
538 try:
539 return markdown.markdown(text, extensions + ['mathjax'])
540 except (ImportError, ValueError):
541 # markdown raises ValueError or ImportError, depends on version
542 # It is not clear, how to distinguish missing mathjax from other
543 # errors. So keep going without mathjax.
544 return markdown.markdown(text, extensions)

eric ide

mercurial