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