|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2009 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a scheme access handler for QtHelp. |
|
8 """ |
|
9 |
|
10 import mimetypes |
|
11 import os |
|
12 |
|
13 from PyQt5.QtCore import ( |
|
14 pyqtSignal, QByteArray, QIODevice, QBuffer, QMutex |
|
15 ) |
|
16 from PyQt5.QtWebEngineCore import ( |
|
17 QWebEngineUrlSchemeHandler, QWebEngineUrlRequestJob |
|
18 ) |
|
19 |
|
20 from E5Utilities.E5MutexLocker import E5MutexLocker |
|
21 |
|
22 QtDocPath = "qthelp://org.qt-project." |
|
23 |
|
24 ExtensionMap = { |
|
25 ".bmp": "image/bmp", |
|
26 ".css": "text/css", |
|
27 ".gif": "image/gif", |
|
28 ".html": "text/html", |
|
29 ".htm": "text/html", |
|
30 ".ico": "image/x-icon", |
|
31 ".jpeg": "image/jpeg", |
|
32 ".jpg": "image/jpeg", |
|
33 ".js": "application/x-javascript", |
|
34 ".mng": "video/x-mng", |
|
35 ".pbm": "image/x-portable-bitmap", |
|
36 ".pgm": "image/x-portable-graymap", |
|
37 ".pdf": "application/pdf", |
|
38 ".png": "image/png", |
|
39 ".ppm": "image/x-portable-pixmap", |
|
40 ".rss": "application/rss+xml", |
|
41 ".svg": "image/svg+xml", |
|
42 ".svgz": "image/svg+xml", |
|
43 ".text": "text/plain", |
|
44 ".tif": "image/tiff", |
|
45 ".tiff": "image/tiff", |
|
46 ".txt": "text/plain", |
|
47 ".xbm": "image/x-xbitmap", |
|
48 ".xml": "text/xml", |
|
49 ".xpm": "image/x-xpm", |
|
50 ".xsl": "text/xsl", |
|
51 ".xhtml": "application/xhtml+xml", |
|
52 ".wml": "text/vnd.wap.wml", |
|
53 ".wmlc": "application/vnd.wap.wmlc", |
|
54 } |
|
55 |
|
56 |
|
57 class QtHelpSchemeHandler(QWebEngineUrlSchemeHandler): |
|
58 """ |
|
59 Class implementing a scheme handler for the qthelp: scheme. |
|
60 """ |
|
61 def __init__(self, engine, parent=None): |
|
62 """ |
|
63 Constructor |
|
64 |
|
65 @param engine reference to the help engine |
|
66 @type QHelpEngine |
|
67 @param parent reference to the parent object |
|
68 @type QObject |
|
69 """ |
|
70 super().__init__(parent) |
|
71 |
|
72 self.__engine = engine |
|
73 |
|
74 self.__replies = [] |
|
75 |
|
76 def requestStarted(self, job): |
|
77 """ |
|
78 Public method handling the URL request. |
|
79 |
|
80 @param job URL request job |
|
81 @type QWebEngineUrlRequestJob |
|
82 """ |
|
83 if job.requestUrl().scheme() == "qthelp": |
|
84 reply = QtHelpSchemeReply(job, self.__engine) |
|
85 reply.closed.connect(lambda: self.__replyClosed(reply)) |
|
86 self.__replies.append(reply) |
|
87 job.reply(reply.mimeType(), reply) |
|
88 else: |
|
89 job.fail(QWebEngineUrlRequestJob.Error.UrlInvalid) |
|
90 |
|
91 def __replyClosed(self, reply): |
|
92 """ |
|
93 Private slot handling the closed signal of a reply. |
|
94 |
|
95 @param reply reference to the network reply |
|
96 @type QtHelpSchemeReply |
|
97 """ |
|
98 if reply in self.__replies: |
|
99 self.__replies.remove(reply) |
|
100 |
|
101 |
|
102 class QtHelpSchemeReply(QIODevice): |
|
103 """ |
|
104 Class implementing a reply for a requested qthelp: page. |
|
105 |
|
106 @signal closed emitted to signal that the web engine has read |
|
107 the data |
|
108 """ |
|
109 closed = pyqtSignal() |
|
110 |
|
111 def __init__(self, job, engine, parent=None): |
|
112 """ |
|
113 Constructor |
|
114 |
|
115 @param job reference to the URL request |
|
116 @type QWebEngineUrlRequestJob |
|
117 @param engine reference to the help engine |
|
118 @type QHelpEngine |
|
119 @param parent reference to the parent object |
|
120 @type QObject |
|
121 """ |
|
122 super().__init__(parent) |
|
123 |
|
124 self.__job = job |
|
125 self.__engine = engine |
|
126 self.__mutex = QMutex() |
|
127 |
|
128 self.__buffer = QBuffer() |
|
129 |
|
130 # determine mimetype |
|
131 url = self.__job.requestUrl() |
|
132 strUrl = url.toString() |
|
133 |
|
134 # For some reason the url to load maybe wrong (passed from web engine) |
|
135 # though the css file and the references inside should work that way. |
|
136 # One possible problem might be that the css is loaded at the same |
|
137 # level as the html, thus a path inside the css like |
|
138 # (../images/foo.png) might cd out of the virtual folder |
|
139 if ( |
|
140 not self.__engine.findFile(url).isValid() and |
|
141 strUrl.startswith(QtDocPath) |
|
142 ): |
|
143 newUrl = self.__job.requestUrl() |
|
144 if not newUrl.path().startswith("/qdoc/"): |
|
145 newUrl.setPath("/qdoc" + newUrl.path()) |
|
146 url = newUrl |
|
147 strUrl = url.toString() |
|
148 |
|
149 self.__mimeType = mimetypes.guess_type(strUrl)[0] |
|
150 if self.__mimeType is None: |
|
151 # do our own (limited) guessing |
|
152 self.__mimeType = self.__mimeFromUrl(url) |
|
153 |
|
154 self.__loadQtHelpPage(url) |
|
155 |
|
156 def __loadQtHelpPage(self, url): |
|
157 """ |
|
158 Private method to load the requested QtHelp page. |
|
159 |
|
160 @param url URL of the requested page |
|
161 @type QUrl |
|
162 """ |
|
163 data = ( |
|
164 self.__engine.fileData(url) |
|
165 if self.__engine.findFile(url).isValid() else |
|
166 QByteArray(self.tr( |
|
167 """<html>""" |
|
168 """<head><title>Error 404...</title></head>""" |
|
169 """<body><div align="center"><br><br>""" |
|
170 """<h1>The page could not be found</h1><br>""" |
|
171 """<h3>'{0}'</h3></div></body>""" |
|
172 """</html>""").format(url.toString()) |
|
173 .encode("utf-8")) |
|
174 ) |
|
175 |
|
176 with E5MutexLocker(self.__mutex): |
|
177 self.__buffer.setData(data) |
|
178 self.__buffer.open(QIODevice.OpenModeFlag.ReadOnly) |
|
179 self.open(QIODevice.OpenModeFlag.ReadOnly) |
|
180 |
|
181 self.readyRead.emit() |
|
182 |
|
183 def bytesAvailable(self): |
|
184 """ |
|
185 Public method to get the number of available bytes. |
|
186 |
|
187 @return number of available bytes |
|
188 @rtype int |
|
189 """ |
|
190 with E5MutexLocker(self.__mutex): |
|
191 return self.__buffer.bytesAvailable() |
|
192 |
|
193 def readData(self, maxlen): |
|
194 """ |
|
195 Public method to retrieve data from the reply object. |
|
196 |
|
197 @param maxlen maximum number of bytes to read (integer) |
|
198 @return string containing the data (bytes) |
|
199 """ |
|
200 with E5MutexLocker(self.__mutex): |
|
201 return self.__buffer.read(maxlen) |
|
202 |
|
203 def close(self): |
|
204 """ |
|
205 Public method used to cloase the reply. |
|
206 """ |
|
207 super().close() |
|
208 self.closed.emit() |
|
209 |
|
210 def __mimeFromUrl(self, url): |
|
211 """ |
|
212 Private method to guess the mime type given an URL. |
|
213 |
|
214 @param url URL to guess the mime type from (QUrl) |
|
215 @return mime type for the given URL (string) |
|
216 """ |
|
217 path = url.path() |
|
218 ext = os.path.splitext(path)[1].lower() |
|
219 if ext in ExtensionMap: |
|
220 return ExtensionMap[ext] |
|
221 else: |
|
222 return "application/octet-stream" |
|
223 |
|
224 def mimeType(self): |
|
225 """ |
|
226 Public method to get the reply mime type. |
|
227 |
|
228 @return mime type of the reply |
|
229 @rtype bytes |
|
230 """ |
|
231 return self.__mimeType.encode("utf-8") |