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 |
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) |
|