messageview.py
1 """ 2 Custom message viewer with support for switching between HTML and plain 3 text rendering, HTML sanitization, lazy rendering (as you scroll down), 4 zoom and URL click warning popup 5 6 """ 7 8 from PyQt4 import QtCore, QtGui 9 10 from .safehtmlparser import SafeHTMLParser 11 from tr import _translate 12 13 14 class MessageView(QtGui.QTextBrowser): 15 """Message content viewer class, can switch between plaintext and HTML""" 16 MODE_PLAIN = 0 17 MODE_HTML = 1 18 19 def __init__(self, parent=0): 20 super(MessageView, self).__init__(parent) 21 self.mode = MessageView.MODE_PLAIN 22 self.html = None 23 self.setOpenExternalLinks(False) 24 self.setOpenLinks(False) 25 self.anchorClicked.connect(self.confirmURL) 26 self.out = "" 27 self.outpos = 0 28 self.document().setUndoRedoEnabled(False) 29 self.rendering = False 30 self.defaultFontPointSize = self.currentFont().pointSize() 31 self.verticalScrollBar().valueChanged.connect(self.lazyRender) 32 self.setWrappingWidth() 33 34 def resizeEvent(self, event): 35 """View resize event handler""" 36 super(MessageView, self).resizeEvent(event) 37 self.setWrappingWidth(event.size().width()) 38 39 def mousePressEvent(self, event): 40 """Mouse press button event handler""" 41 if event.button() == QtCore.Qt.LeftButton and self.html and self.html.has_html and self.cursorForPosition( 42 event.pos()).block().blockNumber() == 0: 43 if self.mode == MessageView.MODE_PLAIN: 44 self.showHTML() 45 else: 46 self.showPlain() 47 else: 48 super(MessageView, self).mousePressEvent(event) 49 50 def wheelEvent(self, event): 51 """Mouse wheel scroll event handler""" 52 # super will actually automatically take care of zooming 53 super(MessageView, self).wheelEvent(event) 54 if ( 55 QtGui.QApplication.queryKeyboardModifiers() & QtCore.Qt.ControlModifier 56 ) == QtCore.Qt.ControlModifier and event.orientation() == QtCore.Qt.Vertical: 57 zoom = self.currentFont().pointSize() * 100 / self.defaultFontPointSize 58 QtGui.QApplication.activeWindow().statusBar().showMessage(_translate( 59 "MainWindow", "Zoom level %1%").arg(str(zoom))) 60 61 def setWrappingWidth(self, width=None): 62 """Set word-wrapping width""" 63 self.setLineWrapMode(QtGui.QTextEdit.FixedPixelWidth) 64 if width is None: 65 width = self.width() 66 self.setLineWrapColumnOrWidth(width) 67 68 def confirmURL(self, link): 69 """Show a dialog requesting URL opening confirmation""" 70 if link.scheme() == "mailto": 71 window = QtGui.QApplication.activeWindow() 72 window.ui.lineEditTo.setText(link.path()) 73 if link.hasQueryItem("subject"): 74 window.ui.lineEditSubject.setText( 75 link.queryItemValue("subject")) 76 if link.hasQueryItem("body"): 77 window.ui.textEditMessage.setText( 78 link.queryItemValue("body")) 79 window.setSendFromComboBox() 80 window.ui.tabWidgetSend.setCurrentIndex(0) 81 window.ui.tabWidget.setCurrentIndex( 82 window.ui.tabWidget.indexOf(window.ui.send) 83 ) 84 window.ui.textEditMessage.setFocus() 85 return 86 reply = QtGui.QMessageBox.warning( 87 self, 88 QtGui.QApplication.translate( 89 "MessageView", 90 "Follow external link"), 91 QtGui.QApplication.translate( 92 "MessageView", 93 "The link \"%1\" will open in a browser. It may be a security risk, it could de-anonymise you" 94 " or download malicious data. Are you sure?").arg(str(link.toString())), 95 QtGui.QMessageBox.Yes, 96 QtGui.QMessageBox.No) 97 if reply == QtGui.QMessageBox.Yes: 98 QtGui.QDesktopServices.openUrl(link) 99 100 def loadResource(self, restype, name): 101 """ 102 Callback for loading referenced objects, such as an image. For security reasons at the moment doesn't do 103 anything) 104 """ 105 pass 106 107 def lazyRender(self): 108 """ 109 Partially render a message. This is to avoid UI freezing when loading huge messages. It continues loading as 110 you scroll down. 111 """ 112 if self.rendering: 113 return 114 self.rendering = True 115 position = self.verticalScrollBar().value() 116 cursor = QtGui.QTextCursor(self.document()) 117 while self.outpos < len(self.out) and self.verticalScrollBar().value( 118 ) >= self.document().size().height() - 2 * self.size().height(): 119 startpos = self.outpos 120 self.outpos += 10240 121 # find next end of tag 122 if self.mode == MessageView.MODE_HTML: 123 pos = self.out.find(">", self.outpos) 124 if pos > self.outpos: 125 self.outpos = pos + 1 126 cursor.movePosition(QtGui.QTextCursor.End, QtGui.QTextCursor.MoveAnchor) 127 cursor.insertHtml(QtCore.QString(self.out[startpos:self.outpos])) 128 self.verticalScrollBar().setValue(position) 129 self.rendering = False 130 131 def showPlain(self): 132 """Render message as plain text.""" 133 self.mode = MessageView.MODE_PLAIN 134 out = self.html.raw 135 if self.html.has_html: 136 out = "<div align=\"center\" style=\"text-decoration: underline;\"><b>" + str( 137 QtGui.QApplication.translate( 138 "MessageView", "HTML detected, click here to display")) + "</b></div><br/>" + out 139 self.out = out 140 self.outpos = 0 141 self.setHtml("") 142 self.lazyRender() 143 144 def showHTML(self): 145 """Render message as HTML""" 146 self.mode = MessageView.MODE_HTML 147 out = self.html.sanitised 148 out = "<div align=\"center\" style=\"text-decoration: underline;\"><b>" + str( 149 QtGui.QApplication.translate("MessageView", "Click here to disable HTML")) + "</b></div><br/>" + out 150 self.out = out 151 self.outpos = 0 152 self.setHtml("") 153 self.lazyRender() 154 155 def setContent(self, data): 156 """Set message content from argument""" 157 self.html = SafeHTMLParser() 158 self.html.reset() 159 self.html.reset_safe() 160 self.html.allow_picture = True 161 self.html.feed(data) 162 self.html.close() 163 self.showPlain()