|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2012 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the speed dial. |
|
8 """ |
|
9 |
|
10 from __future__ import unicode_literals |
|
11 try: |
|
12 str = unicode |
|
13 except NameError: |
|
14 pass |
|
15 |
|
16 import os |
|
17 |
|
18 from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QCryptographicHash, \ |
|
19 QByteArray, QUrl, qWarning |
|
20 from PyQt5.QtWebKitWidgets import QWebPage |
|
21 |
|
22 from E5Gui import E5MessageBox |
|
23 |
|
24 from Utilities.AutoSaver import AutoSaver |
|
25 import Utilities |
|
26 |
|
27 |
|
28 class SpeedDial(QObject): |
|
29 """ |
|
30 Class implementing the speed dial. |
|
31 |
|
32 @signal pagesChanged() emitted after the list of pages changed |
|
33 @signal speedDialSaved() emitted after the speed dial data was saved |
|
34 """ |
|
35 pagesChanged = pyqtSignal() |
|
36 speedDialSaved = pyqtSignal() |
|
37 |
|
38 def __init__(self, parent=None): |
|
39 """ |
|
40 Constructor |
|
41 |
|
42 @param parent reference to the parent object (QObject) |
|
43 """ |
|
44 super(SpeedDial, self).__init__(parent) |
|
45 |
|
46 self.__regenerateScript = True |
|
47 |
|
48 self.__webPages = [] |
|
49 self.__webFrames = [] |
|
50 |
|
51 self.__initialScript = "" |
|
52 self.__thumbnailsDirectory = "" |
|
53 |
|
54 self.__thumbnailers = [] |
|
55 |
|
56 self.__initialize() |
|
57 |
|
58 self.pagesChanged.connect(self.__pagesChanged) |
|
59 |
|
60 self.__saveTimer = AutoSaver(self, self.save) |
|
61 self.pagesChanged.connect(self.__saveTimer.changeOccurred) |
|
62 |
|
63 def addWebFrame(self, frame): |
|
64 """ |
|
65 Public method to add a web frame. |
|
66 |
|
67 @param frame reference to the frame to be added (QWebFrame) |
|
68 """ |
|
69 if frame not in self.__webFrames: |
|
70 self.__webFrames.append(frame) |
|
71 |
|
72 def addPage(self, url, title): |
|
73 """ |
|
74 Public method to add a page for the given data. |
|
75 |
|
76 @param url URL of the page (QUrl) |
|
77 @param title title of the page (string) |
|
78 """ |
|
79 if url.isEmpty(): |
|
80 return |
|
81 |
|
82 from .Page import Page |
|
83 page = Page(url.toString(), title) |
|
84 self.__webPages.append(page) |
|
85 |
|
86 self.pagesChanged.emit() |
|
87 |
|
88 def removePage(self, url): |
|
89 """ |
|
90 Public method to remove a page. |
|
91 |
|
92 @param url URL of the page (QUrl) |
|
93 """ |
|
94 page = self.pageForUrl(url) |
|
95 if not page.url: |
|
96 return |
|
97 |
|
98 self.removeImageForUrl(page.url) |
|
99 self.__webPages.remove(page) |
|
100 |
|
101 self.pagesChanged.emit() |
|
102 |
|
103 def __imageFileName(self, url): |
|
104 """ |
|
105 Private method to generate the image file name for a URL. |
|
106 |
|
107 @param url URL to generate the file name from (string) |
|
108 @return name of the image file (string) |
|
109 """ |
|
110 return os.path.join( |
|
111 self.__thumbnailsDirectory, |
|
112 str(QCryptographicHash.hash(QByteArray(url.encode("utf-8")), |
|
113 QCryptographicHash.Md5).toHex(), encoding="utf-8") + ".png") |
|
114 |
|
115 def initialScript(self): |
|
116 """ |
|
117 Public method to get the 'initial' JavaScript script. |
|
118 |
|
119 @return initial JavaScript script (string) |
|
120 """ |
|
121 if self.__regenerateScript: |
|
122 self.__regenerateScript = False |
|
123 self.__initialScript = "" |
|
124 |
|
125 for page in self.__webPages: |
|
126 if page.broken: |
|
127 imgSource = "qrc:icons/brokenPage.png" |
|
128 else: |
|
129 imgSource = self.__imageFileName(page.url) |
|
130 if not os.path.exists(imgSource): |
|
131 self.loadThumbnail(page.url) |
|
132 imgSource = "qrc:icons/loading.gif" |
|
133 |
|
134 if not page.url: |
|
135 imgSource = "" |
|
136 else: |
|
137 imgSource = QUrl.fromLocalFile(imgSource).toString() |
|
138 |
|
139 self.__initialScript += \ |
|
140 "addBox('{0}', '{1}', '{2}');\n".format( |
|
141 page.url, Utilities.html_uencode(page.title), |
|
142 imgSource) |
|
143 |
|
144 return self.__initialScript |
|
145 |
|
146 def getFileName(self): |
|
147 """ |
|
148 Public method to get the file name of the user agents file. |
|
149 |
|
150 @return name of the user agents file (string) |
|
151 """ |
|
152 return os.path.join( |
|
153 Utilities.getConfigDir(), "browser", "speedDial.xml") |
|
154 |
|
155 def __initialize(self): |
|
156 """ |
|
157 Private method to initialize the speed dial. |
|
158 """ |
|
159 self.__thumbnailsDirectory = os.path.join( |
|
160 Utilities.getConfigDir(), "browser", "thumbnails") |
|
161 # Create directory if it does not exist yet |
|
162 if not os.path.exists(self.__thumbnailsDirectory): |
|
163 os.makedirs(self.__thumbnailsDirectory) |
|
164 |
|
165 self.__load() |
|
166 |
|
167 def reload(self): |
|
168 """ |
|
169 Public method to reload the speed dial data. |
|
170 """ |
|
171 self.__load() |
|
172 |
|
173 def __load(self): |
|
174 """ |
|
175 Private method to load the speed dial configuration. |
|
176 """ |
|
177 allPages, pagesPerRow, speedDialSize = [], 0, 0 |
|
178 |
|
179 speedDialFile = self.getFileName() |
|
180 if os.path.exists(speedDialFile): |
|
181 from .SpeedDialReader import SpeedDialReader |
|
182 reader = SpeedDialReader() |
|
183 allPages, pagesPerRow, speedDialSize = reader.read(speedDialFile) |
|
184 |
|
185 self.__pagesPerRow = pagesPerRow if pagesPerRow else 4 |
|
186 self.__speedDialSize = speedDialSize if speedDialSize else 231 |
|
187 |
|
188 if allPages: |
|
189 self.__webPages = allPages |
|
190 self.pagesChanged.emit() |
|
191 else: |
|
192 allPages = \ |
|
193 'url:"https://eric-ide.python-projects.org/"|'\ |
|
194 'title:"Eric Web Site";'\ |
|
195 'url:"https://www.riverbankcomputing.com/"|'\ |
|
196 'title:"PyQt Web Site";'\ |
|
197 'url:"http://www.qt.io/"|title:"Qt Web Site";'\ |
|
198 'url:"http://blog.qt.io/"|title:"Qt Blog";'\ |
|
199 'url:"https://www.python.org"|'\ |
|
200 'title:"Python Language Website";'\ |
|
201 'url:"http://www.google.com"|title:"Google";' |
|
202 self.changed(allPages) |
|
203 |
|
204 def save(self): |
|
205 """ |
|
206 Public method to save the speed dial configuration. |
|
207 """ |
|
208 from .SpeedDialWriter import SpeedDialWriter |
|
209 speedDialFile = self.getFileName() |
|
210 writer = SpeedDialWriter() |
|
211 if not writer.write(speedDialFile, self.__webPages, |
|
212 self.__pagesPerRow, self.__speedDialSize): |
|
213 E5MessageBox.critical( |
|
214 None, |
|
215 self.tr("Saving Speed Dial data"), |
|
216 self.tr( |
|
217 """<p>Speed Dial data could not be saved to""" |
|
218 """ <b>{0}</b></p>""").format(speedDialFile)) |
|
219 else: |
|
220 self.speedDialSaved.emit() |
|
221 |
|
222 def resetDials(self): |
|
223 """ |
|
224 Public method to reset the speed dials to the default values. |
|
225 """ |
|
226 ok = E5MessageBox.yesNo( |
|
227 None, |
|
228 self.tr("Reset Speed Dials"), |
|
229 self.tr("""Are you sure you want to reset the speed dials to""" |
|
230 """ the default pages?""")) |
|
231 if ok: |
|
232 speedDialFile = self.getFileName() |
|
233 if os.path.exists(speedDialFile): |
|
234 os.remove(speedDialFile) |
|
235 |
|
236 self.__load() |
|
237 |
|
238 def close(self): |
|
239 """ |
|
240 Public method to close the user agents manager. |
|
241 """ |
|
242 self.__saveTimer.saveIfNeccessary() |
|
243 |
|
244 def pageForUrl(self, url): |
|
245 """ |
|
246 Public method to get the page for the given URL. |
|
247 |
|
248 @param url URL to be searched for (QUrl) |
|
249 @return page for the URL (Page) |
|
250 """ |
|
251 urlString = url.toString() |
|
252 for page in self.__webPages: |
|
253 if page.url == urlString: |
|
254 return page |
|
255 |
|
256 from .Page import Page |
|
257 return Page() |
|
258 |
|
259 def urlForShortcut(self, key): |
|
260 """ |
|
261 Public method to get the URL for the given shortcut key. |
|
262 |
|
263 @param key shortcut key (integer) |
|
264 @return URL for the key (QUrl) |
|
265 """ |
|
266 if key < 0 or len(self.__webPages) <= key: |
|
267 return QUrl() |
|
268 |
|
269 return QUrl.fromEncoded(self.__webPages[key].url.encode("utf-8")) |
|
270 |
|
271 @pyqtSlot(str) |
|
272 def changed(self, allPages): |
|
273 """ |
|
274 Public slot to react on changed pages. |
|
275 |
|
276 @param allPages string giving all pages (string) |
|
277 """ |
|
278 if not allPages: |
|
279 return |
|
280 |
|
281 entries = allPages.split('";') |
|
282 self.__webPages = [] |
|
283 |
|
284 from .Page import Page |
|
285 for entry in entries: |
|
286 if not entry: |
|
287 continue |
|
288 |
|
289 tmp = entry.split('"|') |
|
290 if len(tmp) == 2: |
|
291 broken = False |
|
292 elif len(tmp) == 3: |
|
293 broken = "brokenPage" in tmp[2][5:] |
|
294 else: |
|
295 continue |
|
296 |
|
297 page = Page(tmp[0][5:], tmp[1][7:], broken) |
|
298 self.__webPages.append(page) |
|
299 |
|
300 self.pagesChanged.emit() |
|
301 |
|
302 @pyqtSlot(str) |
|
303 @pyqtSlot(str, bool) |
|
304 def loadThumbnail(self, url, loadTitle=False): |
|
305 """ |
|
306 Public slot to load a thumbnail of the given URL. |
|
307 |
|
308 @param url URL of the thumbnail (string) |
|
309 @param loadTitle flag indicating to get the title for the thumbnail |
|
310 from the site (boolean) |
|
311 """ |
|
312 if not url: |
|
313 return |
|
314 |
|
315 from .PageThumbnailer import PageThumbnailer |
|
316 thumbnailer = PageThumbnailer(self) |
|
317 thumbnailer.setUrl(QUrl.fromEncoded(url.encode("utf-8"))) |
|
318 thumbnailer.setLoadTitle(loadTitle) |
|
319 thumbnailer.thumbnailCreated.connect( |
|
320 lambda imag: self.__thumbnailCreated(imag, thumbnailer)) |
|
321 self.__thumbnailers.append(thumbnailer) |
|
322 |
|
323 thumbnailer.start() |
|
324 |
|
325 @pyqtSlot(str) |
|
326 def removeImageForUrl(self, url): |
|
327 """ |
|
328 Public slot to remove the image for a URL. |
|
329 |
|
330 @param url URL to remove the image for (string) |
|
331 """ |
|
332 fileName = self.__imageFileName(url) |
|
333 if os.path.exists(fileName): |
|
334 os.remove(fileName) |
|
335 |
|
336 @pyqtSlot(str, result=str) |
|
337 def urlFromUserInput(self, url): |
|
338 """ |
|
339 Public slot to get the URL from user input. |
|
340 |
|
341 @param url URL entered by the user (string) |
|
342 @return sanitized URL (string) |
|
343 """ |
|
344 return QUrl.fromUserInput(url).toString() |
|
345 |
|
346 @pyqtSlot(str, result=str) |
|
347 def unescapeTitle(self, title): |
|
348 """ |
|
349 Public slot to unescape the titel string. |
|
350 |
|
351 @param title escaped title (string) |
|
352 @return un-escaped title (string) |
|
353 """ |
|
354 return Utilities.html_udecode(title) |
|
355 |
|
356 @pyqtSlot(int) |
|
357 def setPagesInRow(self, count): |
|
358 """ |
|
359 Public slot to set the number of pages per row. |
|
360 |
|
361 @param count number of pages per row (integer) |
|
362 """ |
|
363 self.__pagesPerRow = count |
|
364 self.__saveTimer.changeOccurred() |
|
365 |
|
366 def pagesInRow(self): |
|
367 """ |
|
368 Public method to get the number of dials per row. |
|
369 |
|
370 @return number of dials per row (integer) |
|
371 """ |
|
372 return self.__pagesPerRow |
|
373 |
|
374 @pyqtSlot(int) |
|
375 def setSdSize(self, size): |
|
376 """ |
|
377 Public slot to set the size of the speed dial. |
|
378 |
|
379 @param size size of the speed dial (integer) |
|
380 """ |
|
381 self.__speedDialSize = size |
|
382 self.__saveTimer.changeOccurred() |
|
383 |
|
384 def sdSize(self): |
|
385 """ |
|
386 Public method to get the speed dial size. |
|
387 |
|
388 @return speed dial size (integer) |
|
389 """ |
|
390 return self.__speedDialSize |
|
391 |
|
392 def __thumbnailCreated(self, image, thumbnailer): |
|
393 """ |
|
394 Private slot to handle the creation of a thumbnail image. |
|
395 |
|
396 @param image thumbnail image |
|
397 @type QPixmap |
|
398 @param thumbnailer reference to the page thumbnailer |
|
399 @type PageThumbnailer |
|
400 """ |
|
401 if thumbnailer in self.__thumbnailers: |
|
402 loadTitle = thumbnailer.loadTitle() |
|
403 title = thumbnailer.title() |
|
404 url = thumbnailer.url().toString() |
|
405 fileName = self.__imageFileName(url) |
|
406 |
|
407 if image.isNull(): |
|
408 fileName = "qrc:icons/brokenPage.png" |
|
409 title = self.tr("Unable to load") |
|
410 loadTitle = True |
|
411 page = self.pageForUrl(thumbnailer.url()) |
|
412 page.broken = True |
|
413 else: |
|
414 if not image.save(fileName): |
|
415 qWarning( |
|
416 "SpeedDial.__thumbnailCreated: Cannot save thumbnail" |
|
417 " to {0}".format(fileName)) |
|
418 |
|
419 fileName = QUrl.fromLocalFile(fileName).toString() |
|
420 |
|
421 self.__regenerateScript = True |
|
422 |
|
423 for frame in self.__cleanFrames(): |
|
424 frame.evaluateJavaScript("setImageToUrl('{0}', '{1}');".format( |
|
425 url, fileName)) |
|
426 if loadTitle: |
|
427 frame.evaluateJavaScript( |
|
428 "setTitleToUrl('{0}', '{1}');".format( |
|
429 url, Utilities.html_uencode(title))) |
|
430 |
|
431 thumbnailer.deleteLater() |
|
432 self.__thumbnailers.remove(thumbnailer) |
|
433 |
|
434 def __cleanFrames(self): |
|
435 """ |
|
436 Private method to clean all frames. |
|
437 |
|
438 @return list of speed dial frames (list of QWebFrame) |
|
439 """ |
|
440 frames = [] |
|
441 |
|
442 for frame in self.__webFrames[:]: |
|
443 if frame.url().toString() == "eric:speeddial": |
|
444 frames.append(frame) |
|
445 else: |
|
446 self.__webFrames.remove(frame) |
|
447 |
|
448 return frames |
|
449 |
|
450 def __pagesChanged(self): |
|
451 """ |
|
452 Private slot to react on a change of the pages configuration. |
|
453 """ |
|
454 # update all speed dial pages |
|
455 self.__regenerateScript = True |
|
456 for frame in self.__cleanFrames(): |
|
457 frame.page().triggerAction(QWebPage.Reload) |