|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2011 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a dialog to show some information about a site. |
|
8 """ |
|
9 |
|
10 from PyQt6.QtCore import pyqtSlot, QUrl, Qt |
|
11 from PyQt6.QtGui import QPixmap, QImage, QPainter, QColor, QBrush |
|
12 from PyQt6.QtNetwork import QNetworkRequest, QNetworkReply |
|
13 from PyQt6.QtWidgets import ( |
|
14 QDialog, QTreeWidgetItem, QGraphicsScene, QMenu, QApplication, |
|
15 QGraphicsPixmapItem |
|
16 ) |
|
17 try: |
|
18 from PyQt6.QtNetwork import QSslCertificate # __IGNORE_WARNING__ |
|
19 SSL = True |
|
20 except ImportError: |
|
21 SSL = False |
|
22 |
|
23 from EricWidgets import EricMessageBox, EricFileDialog |
|
24 |
|
25 from .Ui_SiteInfoDialog import Ui_SiteInfoDialog |
|
26 |
|
27 from ..Tools import Scripts, WebBrowserTools |
|
28 from ..WebBrowserPage import WebBrowserPage |
|
29 |
|
30 import UI.PixmapCache |
|
31 import Preferences |
|
32 |
|
33 from WebBrowser.WebBrowserWindow import WebBrowserWindow |
|
34 |
|
35 |
|
36 class SiteInfoDialog(QDialog, Ui_SiteInfoDialog): |
|
37 """ |
|
38 Class implementing a dialog to show some information about a site. |
|
39 """ |
|
40 securityStyleFormat = "QLabel {{ background-color : {0}; }}" |
|
41 |
|
42 def __init__(self, browser, parent=None): |
|
43 """ |
|
44 Constructor |
|
45 |
|
46 @param browser reference to the browser window (HelpBrowser) |
|
47 @param parent reference to the parent widget (QWidget) |
|
48 """ |
|
49 super().__init__(parent) |
|
50 self.setupUi(self) |
|
51 self.setWindowFlags(Qt.WindowType.Window) |
|
52 |
|
53 # put icons |
|
54 self.tabWidget.setTabIcon( |
|
55 0, UI.PixmapCache.getIcon("siteinfo-general")) |
|
56 self.tabWidget.setTabIcon( |
|
57 1, UI.PixmapCache.getIcon("siteinfo-media")) |
|
58 if SSL: |
|
59 self.tabWidget.setTabIcon( |
|
60 2, UI.PixmapCache.getIcon("siteinfo-security")) |
|
61 |
|
62 self.__imageReply = None |
|
63 |
|
64 self.__baseUrl = browser.url() |
|
65 title = browser.title() |
|
66 sslInfo = browser.page().getSslCertificateChain() |
|
67 |
|
68 #prepare background of image preview |
|
69 self.__imagePreviewStandardBackground = ( |
|
70 self.imagePreview.backgroundBrush() |
|
71 ) |
|
72 color1 = QColor(220, 220, 220) |
|
73 color2 = QColor(160, 160, 160) |
|
74 self.__tilePixmap = QPixmap(8, 8) |
|
75 self.__tilePixmap.fill(color1) |
|
76 tilePainter = QPainter(self.__tilePixmap) |
|
77 tilePainter.fillRect(0, 0, 4, 4, color2) |
|
78 tilePainter.fillRect(4, 4, 4, 4, color2) |
|
79 tilePainter.end() |
|
80 |
|
81 # populate General tab |
|
82 self.heading.setText("<b>{0}</b>".format(title)) |
|
83 self.siteAddressLabel.setText(self.__baseUrl.toString()) |
|
84 if self.__baseUrl.scheme() in ["https"]: |
|
85 if WebBrowserWindow.networkManager().isInsecureHost( |
|
86 self.__baseUrl.host() |
|
87 ): |
|
88 self.securityIconLabel.setPixmap( |
|
89 UI.PixmapCache.getPixmap("securityMedium")) |
|
90 self.securityLabel.setStyleSheet( |
|
91 SiteInfoDialog.securityStyleFormat.format( |
|
92 Preferences.getWebBrowser("InsecureUrlColor").name() |
|
93 ) |
|
94 ) |
|
95 self.securityLabel.setText(self.tr( |
|
96 '<b>Connection is encrypted but may be insecure.</b>')) |
|
97 else: |
|
98 self.securityIconLabel.setPixmap( |
|
99 UI.PixmapCache.getPixmap("securityHigh")) |
|
100 self.securityLabel.setStyleSheet( |
|
101 SiteInfoDialog.securityStyleFormat.format( |
|
102 Preferences.getWebBrowser("SecureUrlColor").name() |
|
103 ) |
|
104 ) |
|
105 self.securityLabel.setText( |
|
106 self.tr('<b>Connection is encrypted.</b>')) |
|
107 else: |
|
108 self.securityIconLabel.setPixmap( |
|
109 UI.PixmapCache.getPixmap("securityLow")) |
|
110 self.securityLabel.setText( |
|
111 self.tr('<b>Connection is not encrypted.</b>')) |
|
112 browser.page().runJavaScript( |
|
113 "document.charset", WebBrowserPage.SafeJsWorld, |
|
114 lambda res: self.encodingLabel.setText(res)) |
|
115 |
|
116 # populate the Security tab |
|
117 if sslInfo and SSL: |
|
118 self.sslWidget.showCertificateChain(sslInfo) |
|
119 self.tabWidget.setTabEnabled(2, SSL and bool(sslInfo)) |
|
120 self.securityDetailsButton.setEnabled(SSL and bool(sslInfo)) |
|
121 |
|
122 # populate Meta tags |
|
123 browser.page().runJavaScript(Scripts.getAllMetaAttributes(), |
|
124 WebBrowserPage.SafeJsWorld, |
|
125 self.__processMetaAttributes) |
|
126 |
|
127 # populate Media tab |
|
128 browser.page().runJavaScript(Scripts.getAllImages(), |
|
129 WebBrowserPage.SafeJsWorld, |
|
130 self.__processImageTags) |
|
131 |
|
132 self.tabWidget.setCurrentIndex(0) |
|
133 |
|
134 @pyqtSlot() |
|
135 def on_securityDetailsButton_clicked(self): |
|
136 """ |
|
137 Private slot to show security details. |
|
138 """ |
|
139 self.tabWidget.setCurrentIndex( |
|
140 self.tabWidget.indexOf(self.securityTab)) |
|
141 |
|
142 def __processImageTags(self, res): |
|
143 """ |
|
144 Private method to process the image tags. |
|
145 |
|
146 @param res result of the JavaScript script |
|
147 @type list of dict |
|
148 """ |
|
149 for img in res: |
|
150 src = img["src"] |
|
151 alt = img["alt"] |
|
152 if not alt: |
|
153 if src.find("/") == -1: |
|
154 alt = src |
|
155 else: |
|
156 pos = src.rfind("/") |
|
157 alt = src[pos + 1:] |
|
158 |
|
159 if not src or not alt: |
|
160 continue |
|
161 |
|
162 QTreeWidgetItem(self.imagesTree, [alt, src]) |
|
163 |
|
164 for col in range(self.imagesTree.columnCount()): |
|
165 self.imagesTree.resizeColumnToContents(col) |
|
166 if self.imagesTree.columnWidth(0) > 300: |
|
167 self.imagesTree.setColumnWidth(0, 300) |
|
168 self.imagesTree.setCurrentItem(self.imagesTree.topLevelItem(0)) |
|
169 self.imagesTree.setContextMenuPolicy( |
|
170 Qt.ContextMenuPolicy.CustomContextMenu) |
|
171 self.imagesTree.customContextMenuRequested.connect( |
|
172 self.__imagesTreeContextMenuRequested) |
|
173 |
|
174 def __processMetaAttributes(self, res): |
|
175 """ |
|
176 Private method to process the meta attributes. |
|
177 |
|
178 @param res result of the JavaScript script |
|
179 @type list of dict |
|
180 """ |
|
181 for meta in res: |
|
182 content = meta["content"] |
|
183 name = meta["name"] |
|
184 if not name: |
|
185 name = meta["httpequiv"] |
|
186 |
|
187 if not name or not content: |
|
188 continue |
|
189 |
|
190 if meta["charset"]: |
|
191 self.encodingLabel.setText(meta["charset"]) |
|
192 if "charset=" in content: |
|
193 self.encodingLabel.setText( |
|
194 content[content.index("charset=") + 8:]) |
|
195 |
|
196 QTreeWidgetItem(self.tagsTree, [name, content]) |
|
197 for col in range(self.tagsTree.columnCount()): |
|
198 self.tagsTree.resizeColumnToContents(col) |
|
199 |
|
200 @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem) |
|
201 def on_imagesTree_currentItemChanged(self, current, previous): |
|
202 """ |
|
203 Private slot to show a preview of the selected image. |
|
204 |
|
205 @param current current image entry (QTreeWidgetItem) |
|
206 @param previous old current entry (QTreeWidgetItem) |
|
207 """ |
|
208 if current is None: |
|
209 return |
|
210 |
|
211 imageUrl = QUrl(current.text(1)) |
|
212 if imageUrl.isRelative(): |
|
213 imageUrl = self.__baseUrl.resolved(imageUrl) |
|
214 |
|
215 pixmap = QPixmap() |
|
216 loading = False |
|
217 |
|
218 if imageUrl.scheme() == "data": |
|
219 encodedUrl = current.text(1).encode("utf-8") |
|
220 imageData = encodedUrl[encodedUrl.find(b",") + 1:] |
|
221 pixmap = WebBrowserTools.pixmapFromByteArray(imageData) |
|
222 elif imageUrl.scheme() == "file": |
|
223 pixmap = QPixmap(imageUrl.toLocalFile()) |
|
224 elif imageUrl.scheme() == "qrc": |
|
225 pixmap = QPixmap(imageUrl.toString()[3:]) |
|
226 else: |
|
227 if self.__imageReply is not None: |
|
228 self.__imageReply.deleteLater() |
|
229 self.__imageReply = None |
|
230 |
|
231 from WebBrowser.WebBrowserWindow import WebBrowserWindow |
|
232 self.__imageReply = WebBrowserWindow.networkManager().get( |
|
233 QNetworkRequest(imageUrl)) |
|
234 self.__imageReply.finished.connect(self.__imageReplyFinished) |
|
235 loading = True |
|
236 self.__showLoadingText() |
|
237 |
|
238 if not loading: |
|
239 self.__showPixmap(pixmap) |
|
240 |
|
241 @pyqtSlot() |
|
242 def __imageReplyFinished(self): |
|
243 """ |
|
244 Private slot handling the loading of an image. |
|
245 """ |
|
246 if self.__imageReply.error() != QNetworkReply.NetworkError.NoError: |
|
247 return |
|
248 |
|
249 data = self.__imageReply.readAll() |
|
250 self.__showPixmap(QPixmap.fromImage(QImage.fromData(data))) |
|
251 |
|
252 def __showPixmap(self, pixmap): |
|
253 """ |
|
254 Private method to show a pixmap in the preview pane. |
|
255 |
|
256 @param pixmap pixmap to be shown |
|
257 @type QPixmap |
|
258 """ |
|
259 scene = QGraphicsScene(self.imagePreview) |
|
260 if pixmap.isNull(): |
|
261 self.imagePreview.setBackgroundBrush( |
|
262 self.__imagePreviewStandardBackground) |
|
263 scene.addText(self.tr("Preview not available.")) |
|
264 else: |
|
265 self.imagePreview.setBackgroundBrush(QBrush(self.__tilePixmap)) |
|
266 scene.addPixmap(pixmap) |
|
267 self.imagePreview.setScene(scene) |
|
268 |
|
269 def __showLoadingText(self): |
|
270 """ |
|
271 Private method to show some text while loading an image. |
|
272 """ |
|
273 self.imagePreview.setBackgroundBrush( |
|
274 self.__imagePreviewStandardBackground) |
|
275 scene = QGraphicsScene(self.imagePreview) |
|
276 scene.addText(self.tr("Loading...")) |
|
277 self.imagePreview.setScene(scene) |
|
278 |
|
279 def __imagesTreeContextMenuRequested(self, pos): |
|
280 """ |
|
281 Private slot to show a context menu for the images list. |
|
282 |
|
283 @param pos position for the menu (QPoint) |
|
284 """ |
|
285 itm = self.imagesTree.itemAt(pos) |
|
286 if itm is None: |
|
287 return |
|
288 |
|
289 menu = QMenu() |
|
290 act1 = menu.addAction(self.tr("Copy Image Location to Clipboard")) |
|
291 act1.setData(itm.text(1)) |
|
292 act1.triggered.connect(lambda: self.__copyAction(act1)) |
|
293 act2 = menu.addAction(self.tr("Copy Image Name to Clipboard")) |
|
294 act2.setData(itm.text(0)) |
|
295 act2.triggered.connect(lambda: self.__copyAction(act2)) |
|
296 menu.addSeparator() |
|
297 act3 = menu.addAction(self.tr("Save Image")) |
|
298 act3.setData(self.imagesTree.indexOfTopLevelItem(itm)) |
|
299 act3.triggered.connect(lambda: self.__saveImage(act3)) |
|
300 menu.exec(self.imagesTree.viewport().mapToGlobal(pos)) |
|
301 |
|
302 def __copyAction(self, act): |
|
303 """ |
|
304 Private slot to copy the image URL or the image name to the clipboard. |
|
305 |
|
306 @param act reference to the action that triggered |
|
307 @type QAction |
|
308 """ |
|
309 QApplication.clipboard().setText(act.data()) |
|
310 |
|
311 def __saveImage(self, act): |
|
312 """ |
|
313 Private slot to save the selected image to disk. |
|
314 |
|
315 @param act reference to the action that triggered |
|
316 @type QAction |
|
317 """ |
|
318 index = act.data() |
|
319 itm = self.imagesTree.topLevelItem(index) |
|
320 if itm is None: |
|
321 return |
|
322 |
|
323 if ( |
|
324 not self.imagePreview.scene() or |
|
325 len(self.imagePreview.scene().items()) == 0 |
|
326 ): |
|
327 return |
|
328 |
|
329 pixmapItem = self.imagePreview.scene().items()[0] |
|
330 if not isinstance(pixmapItem, QGraphicsPixmapItem): |
|
331 return |
|
332 |
|
333 if pixmapItem.pixmap().isNull(): |
|
334 EricMessageBox.warning( |
|
335 self, |
|
336 self.tr("Save Image"), |
|
337 self.tr( |
|
338 """<p>This preview is not available.</p>""")) |
|
339 return |
|
340 |
|
341 imageFileName = WebBrowserTools.getFileNameFromUrl(QUrl(itm.text(1))) |
|
342 index = imageFileName.rfind(".") |
|
343 if index != -1: |
|
344 imageFileName = imageFileName[:index] + ".png" |
|
345 |
|
346 filename = EricFileDialog.getSaveFileName( |
|
347 self, |
|
348 self.tr("Save Image"), |
|
349 imageFileName, |
|
350 self.tr("All Files (*)"), |
|
351 EricFileDialog.DontConfirmOverwrite) |
|
352 |
|
353 if not filename: |
|
354 return |
|
355 |
|
356 if not pixmapItem.pixmap().save(filename, "PNG"): |
|
357 EricMessageBox.critical( |
|
358 self, |
|
359 self.tr("Save Image"), |
|
360 self.tr( |
|
361 """<p>Cannot write to file <b>{0}</b>.</p>""") |
|
362 .format(filename)) |
|
363 return |