src/eric7/WebBrowser/SpeedDial/SpeedDial.py

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

eric ide

mercurial