eric6/WebBrowser/History/HistoryManager.py

changeset 6942
2602857055c5
parent 6695
0a51887c13cd
child 7229
53054eb5b15a
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2009 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the history manager.
8 """
9
10 from __future__ import unicode_literals
11
12 import os
13
14 from PyQt5.QtCore import pyqtSignal, pyqtSlot, QFileInfo, QDateTime, QDate, \
15 QTime, QUrl, QTimer, QFile, QIODevice, QByteArray, QDataStream, \
16 QTemporaryFile, QObject
17
18 from E5Gui import E5MessageBox
19
20 from Utilities.AutoSaver import AutoSaver
21 import Utilities
22 import Preferences
23
24 HISTORY_VERSION_42 = 42
25 HISTORY_VERSION_60 = 60
26 HISTORY_VERSIONS = [HISTORY_VERSION_60, HISTORY_VERSION_42]
27
28
29 class HistoryEntry(object):
30 """
31 Class implementing a history entry.
32 """
33 def __init__(self, url=None, dateTime=None, title=None, visitCount=None):
34 """
35 Constructor
36
37 @param url URL of the history entry (string)
38 @param dateTime date and time this entry was created (QDateTime)
39 @param title title string for the history entry (string)
40 @param visitCount number of visits of this URL (int)
41 """
42 self.url = url and url or ""
43 self.dateTime = dateTime and dateTime or QDateTime()
44 self.title = title and title or ""
45 self.visitCount = visitCount and visitCount or 0
46
47 def __eq__(self, other):
48 """
49 Special method determining equality.
50
51 @param other reference to the history entry to compare against
52 (HistoryEntry)
53 @return flag indicating equality (boolean)
54 """
55 return other.title == self.title and \
56 other.url == self.url and \
57 other.dateTime == self.dateTime
58
59 def __lt__(self, other):
60 """
61 Special method determining less relation.
62
63 Note: History is sorted in reverse order by date and time
64
65 @param other reference to the history entry to compare against
66 (HistoryEntry)
67 @return flag indicating less (boolean)
68 """
69 return self.dateTime > other.dateTime
70
71 def userTitle(self):
72 """
73 Public method to get the title of the history entry.
74
75 @return title of the entry (string)
76 """
77 if not self.title:
78 page = QFileInfo(QUrl(self.url).path()).fileName()
79 if page:
80 return page
81 return self.url
82 return self.title
83
84 def isValid(self):
85 """
86 Public method to determine validity.
87
88 @return flag indicating validity
89 @rtype bool
90 """
91 return bool(self.url) and self.dateTime.isValid()
92
93
94 class HistoryManager(QObject):
95 """
96 Class implementing the history manager.
97
98 @signal historyCleared() emitted after the history has been cleared
99 @signal historyReset() emitted after the history has been reset
100 @signal entryAdded(HistoryEntry) emitted after a history entry has been
101 added
102 @signal entryRemoved(HistoryEntry) emitted after a history entry has been
103 removed
104 @signal entryUpdated(int) emitted after a history entry has been updated
105 @signal historySaved() emitted after the history was saved
106 """
107 historyCleared = pyqtSignal()
108 historyReset = pyqtSignal()
109 entryAdded = pyqtSignal(HistoryEntry)
110 entryRemoved = pyqtSignal(HistoryEntry)
111 entryUpdated = pyqtSignal(int)
112 historySaved = pyqtSignal()
113
114 def __init__(self, parent=None):
115 """
116 Constructor
117
118 @param parent reference to the parent object (QObject)
119 """
120 super(HistoryManager, self).__init__(parent)
121
122 self.__saveTimer = AutoSaver(self, self.save)
123 self.__daysToExpire = Preferences.getWebBrowser("HistoryLimit")
124 self.__history = []
125 self.__lastSavedUrl = ""
126
127 self.__expiredTimer = QTimer(self)
128 self.__expiredTimer.setSingleShot(True)
129 self.__expiredTimer.timeout.connect(self.__checkForExpired)
130
131 self.__frequencyTimer = QTimer(self)
132 self.__frequencyTimer.setSingleShot(True)
133 self.__frequencyTimer.timeout.connect(self.__refreshFrequencies)
134
135 self.entryAdded.connect(self.__saveTimer.changeOccurred)
136 self.entryRemoved.connect(self.__saveTimer.changeOccurred)
137
138 self.__load()
139
140 from .HistoryModel import HistoryModel
141 from .HistoryFilterModel import HistoryFilterModel
142 from .HistoryTreeModel import HistoryTreeModel
143
144 self.__historyModel = HistoryModel(self, self)
145 self.__historyFilterModel = \
146 HistoryFilterModel(self.__historyModel, self)
147 self.__historyTreeModel = \
148 HistoryTreeModel(self.__historyFilterModel, self)
149
150 self.__startFrequencyTimer()
151
152 def close(self):
153 """
154 Public method to close the history manager.
155 """
156 # remove history items on application exit
157 if self.__daysToExpire == -2:
158 self.clear()
159 self.__saveTimer.saveIfNeccessary()
160
161 def history(self):
162 """
163 Public method to return the history.
164
165 @return reference to the list of history entries (list of HistoryEntry)
166 """
167 return self.__history[:]
168
169 def setHistory(self, history, loadedAndSorted=False):
170 """
171 Public method to set a new history.
172
173 @param history reference to the list of history entries to be set
174 (list of HistoryEntry)
175 @param loadedAndSorted flag indicating that the list is sorted
176 (boolean)
177 """
178 self.__history = history[:]
179 if not loadedAndSorted:
180 self.__history.sort()
181
182 self.__checkForExpired()
183
184 if loadedAndSorted:
185 try:
186 self.__lastSavedUrl = self.__history[0].url
187 except IndexError:
188 self.__lastSavedUrl = ""
189 else:
190 self.__lastSavedUrl = ""
191 self.__saveTimer.changeOccurred()
192 self.historyReset.emit()
193
194 def __findFirstHistoryEntry(self, url):
195 """
196 Private method to find the first entry for the given URL.
197
198 @param url URL to search for
199 @type str
200 @return first entry for the given URL
201 @rtype HistoryEntry
202 """
203 for index in range(len(self.__history)):
204 if url == self.__history[index].url:
205 return self.__history[index]
206
207 # not found, return an empty entry
208 return HistoryEntry()
209
210 def __updateVisitCount(self, url, count):
211 """
212 Private method to update the visit count for all entries of the
213 given URL.
214
215 @param url URL to be updated
216 @type str
217 @param count new visit count
218 @type int
219 """
220 for index in range(len(self.__history)):
221 if url == self.__history[index].url:
222 self.__history[index].visitCount = count
223
224 def addHistoryEntry(self, view):
225 """
226 Public method to add a history entry.
227
228 @param view reference to the view to add an entry for
229 @type WebBrowserView
230 """
231 import WebBrowser.WebBrowserWindow
232 if WebBrowser.WebBrowserWindow.WebBrowserWindow.isPrivate():
233 return
234
235 url = view.url()
236 title = view.title()
237
238 if url.scheme() not in ["eric", "about", "data", "chrome"]:
239 cleanUrlStr = self.__cleanUrlStr(url)
240 firstEntry = self.__findFirstHistoryEntry(cleanUrlStr)
241 if firstEntry.isValid():
242 visitCount = firstEntry.visitCount + 1
243 self.__updateVisitCount(cleanUrlStr, visitCount)
244 else:
245 visitCount = 1
246 itm = HistoryEntry(cleanUrlStr,
247 QDateTime.currentDateTime(),
248 title,
249 visitCount)
250 self.__history.insert(0, itm)
251 self.entryAdded.emit(itm)
252 if len(self.__history) == 1:
253 self.__checkForExpired()
254
255 def updateHistoryEntry(self, url, title):
256 """
257 Public method to update a history entry.
258
259 @param url URL of the entry to update (string)
260 @param title title of the entry to update (string)
261 """
262 if QUrl(url).scheme() not in ["eric", "about", "data", "chrome"]:
263 cleanUrlStr = self.__cleanUrlStr(QUrl(url))
264 for index in range(len(self.__history)):
265 if cleanUrlStr == self.__history[index].url:
266 self.__history[index].title = title
267 self.__saveTimer.changeOccurred()
268 if not self.__lastSavedUrl:
269 self.__lastSavedUrl = self.__history[index].url
270 self.entryUpdated.emit(index)
271 break
272
273 def removeHistoryEntry(self, url, title=""):
274 """
275 Public method to remove a history entry.
276
277 @param url URL of the entry to remove (QUrl)
278 @param title title of the entry to remove (string)
279 """
280 if url.scheme() not in ["eric", "about", "data", "chrome"]:
281 cleanUrlStr = self.__cleanUrlStr(url)
282 for index in range(len(self.__history)):
283 if cleanUrlStr == self.__history[index].url and \
284 (not title or title == self.__history[index].title):
285 itm = self.__history[index]
286 self.__lastSavedUrl = ""
287 self.__history.remove(itm)
288 self.entryRemoved.emit(itm)
289 break
290
291 def __cleanUrl(self, url):
292 """
293 Private method to generate a clean URL usable for the history entry.
294
295 @param url original URL
296 @type QUrl
297 @return cleaned URL
298 @rtype QUrl
299 """
300 cleanurl = QUrl(url)
301 if cleanurl.password():
302 # don't save the password in the history
303 cleanurl.setPassword("")
304 if cleanurl.host():
305 # convert host to lower case
306 cleanurl.setHost(url.host().lower())
307
308 return cleanurl
309
310 def __cleanUrlStr(self, url):
311 """
312 Private method to generate a clean URL usable for the history entry.
313
314 @param url original URL
315 @type QUrl
316 @return cleaned URL
317 @rtype str
318 """
319 cleanurl = self.__cleanUrl(url)
320 return cleanurl.toString()
321
322 def historyModel(self):
323 """
324 Public method to get a reference to the history model.
325
326 @return reference to the history model (HistoryModel)
327 """
328 return self.__historyModel
329
330 def historyFilterModel(self):
331 """
332 Public method to get a reference to the history filter model.
333
334 @return reference to the history filter model (HistoryFilterModel)
335 """
336 return self.__historyFilterModel
337
338 def historyTreeModel(self):
339 """
340 Public method to get a reference to the history tree model.
341
342 @return reference to the history tree model (HistoryTreeModel)
343 """
344 return self.__historyTreeModel
345
346 def __checkForExpired(self):
347 """
348 Private slot to check entries for expiration.
349 """
350 if self.__daysToExpire < 0 or len(self.__history) == 0:
351 return
352
353 now = QDateTime.currentDateTime()
354 nextTimeout = 0
355
356 while self.__history:
357 checkForExpired = QDateTime(self.__history[-1].dateTime)
358 checkForExpired.setDate(
359 checkForExpired.date().addDays(self.__daysToExpire))
360 if now.daysTo(checkForExpired) > 7:
361 nextTimeout = 7 * 86400
362 else:
363 nextTimeout = now.secsTo(checkForExpired)
364 if nextTimeout > 0:
365 break
366
367 itm = self.__history.pop(-1)
368 self.__lastSavedUrl = ""
369 self.entryRemoved.emit(itm)
370 self.__saveTimer.saveIfNeccessary()
371
372 if nextTimeout > 0:
373 self.__expiredTimer.start(nextTimeout * 1000)
374
375 def daysToExpire(self):
376 """
377 Public method to get the days for entry expiration.
378
379 @return days for entry expiration (integer)
380 """
381 return self.__daysToExpire
382
383 def setDaysToExpire(self, limit):
384 """
385 Public method to set the days for entry expiration.
386
387 @param limit days for entry expiration (integer)
388 """
389 if self.__daysToExpire == limit:
390 return
391
392 self.__daysToExpire = limit
393 self.__checkForExpired()
394 self.__saveTimer.changeOccurred()
395
396 def preferencesChanged(self):
397 """
398 Public method to indicate a change of preferences.
399 """
400 self.setDaysToExpire(Preferences.getWebBrowser("HistoryLimit"))
401
402 @pyqtSlot()
403 def clear(self, period=0):
404 """
405 Public slot to clear the complete history.
406
407 @param period history period in milliseconds to be cleared (integer)
408 """
409 if period == 0:
410 self.__history = []
411 self.historyReset.emit()
412 else:
413 breakMS = QDateTime.currentMSecsSinceEpoch() - period
414 while self.__history and \
415 (QDateTime(self.__history[0].dateTime).toMSecsSinceEpoch() >
416 breakMS):
417 itm = self.__history.pop(0)
418 self.entryRemoved.emit(itm)
419 self.__lastSavedUrl = ""
420 self.__saveTimer.changeOccurred()
421 self.__saveTimer.saveIfNeccessary()
422 self.historyCleared.emit()
423
424 def getFileName(self):
425 """
426 Public method to get the file name of the history file.
427
428 @return name of the history file (string)
429 """
430 return os.path.join(Utilities.getConfigDir(), "web_browser", "history")
431
432 def reload(self):
433 """
434 Public method to reload the history.
435 """
436 self.__load()
437
438 def __load(self):
439 """
440 Private method to load the saved history entries from disk.
441 """
442 historyFile = QFile(self.getFileName())
443 if not historyFile.exists():
444 return
445 if not historyFile.open(QIODevice.ReadOnly):
446 E5MessageBox.warning(
447 None,
448 self.tr("Loading History"),
449 self.tr(
450 """<p>Unable to open history file <b>{0}</b>.<br/>"""
451 """Reason: {1}</p>""")
452 .format(historyFile.fileName, historyFile.errorString()))
453 return
454
455 history = []
456
457 # double check, that the history file is sorted as it is read
458 needToSort = False
459 lastInsertedItem = HistoryEntry()
460 data = QByteArray(historyFile.readAll())
461 stream = QDataStream(data, QIODevice.ReadOnly)
462 stream.setVersion(QDataStream.Qt_4_6)
463 while not stream.atEnd():
464 ver = stream.readUInt32()
465 if ver not in HISTORY_VERSIONS:
466 continue
467 itm = HistoryEntry()
468 itm.url = Utilities.readStringFromStream(stream)
469 stream >> itm.dateTime
470 itm.title = Utilities.readStringFromStream(stream)
471 if ver == HISTORY_VERSION_60:
472 itm.visitCount = stream.readUInt32()
473
474 if not itm.dateTime.isValid():
475 continue
476
477 if itm == lastInsertedItem:
478 if not lastInsertedItem.title and len(history) > 0:
479 history[0].title = itm.title
480 continue
481
482 if ver == HISTORY_VERSION_42:
483 firstEntry = self.__findFirstHistoryEntry(itm.url)
484 if firstEntry.isValid():
485 visitCount = firstEntry.visitCount + 1
486 self.__updateVisitCount(itm.url, visitCount)
487 else:
488 visitCount = 1
489 itm.visitCount = visitCount
490
491 if not needToSort and history and lastInsertedItem < itm:
492 needToSort = True
493
494 history.insert(0, itm)
495 lastInsertedItem = itm
496 historyFile.close()
497
498 if needToSort:
499 history.sort()
500
501 self.setHistory(history, True)
502
503 # if the history had to be sorted, rewrite the history sorted
504 if needToSort:
505 self.__lastSavedUrl = ""
506 self.__saveTimer.changeOccurred()
507
508 def save(self):
509 """
510 Public slot to save the history entries to disk.
511 """
512 historyFile = QFile(self.getFileName())
513 if not historyFile.exists():
514 self.__lastSavedUrl = ""
515
516 saveAll = self.__lastSavedUrl == ""
517 first = len(self.__history) - 1
518 if not saveAll:
519 # find the first one to save
520 for index in range(len(self.__history)):
521 if self.__history[index].url == self.__lastSavedUrl:
522 first = index - 1
523 break
524 if first == len(self.__history) - 1:
525 saveAll = True
526
527 if saveAll:
528 # use a temporary file when saving everything
529 f = QTemporaryFile()
530 f.setAutoRemove(False)
531 opened = f.open()
532 else:
533 f = historyFile
534 opened = f.open(QIODevice.Append)
535
536 if not opened:
537 E5MessageBox.warning(
538 None,
539 self.tr("Saving History"),
540 self.tr(
541 """<p>Unable to open history file <b>{0}</b>.<br/>"""
542 """Reason: {1}</p>""")
543 .format(f.fileName(), f.errorString()))
544 return
545
546 for index in range(first, -1, -1):
547 data = QByteArray()
548 stream = QDataStream(data, QIODevice.WriteOnly)
549 stream.setVersion(QDataStream.Qt_4_6)
550 itm = self.__history[index]
551 stream.writeUInt32(HISTORY_VERSION_60)
552 stream.writeString(itm.url.encode("utf-8"))
553 stream << itm.dateTime
554 stream.writeString(itm.title.encode('utf-8'))
555 stream.writeUInt32(itm.visitCount)
556 f.write(data)
557
558 f.close()
559 if saveAll:
560 if historyFile.exists() and not historyFile.remove():
561 E5MessageBox.warning(
562 None,
563 self.tr("Saving History"),
564 self.tr(
565 """<p>Error removing old history file <b>{0}</b>."""
566 """<br/>Reason: {1}</p>""")
567 .format(historyFile.fileName(),
568 historyFile.errorString()))
569 if not f.copy(historyFile.fileName()):
570 E5MessageBox.warning(
571 None,
572 self.tr("Saving History"),
573 self.tr(
574 """<p>Error moving new history file over old one """
575 """(<b>{0}</b>).<br/>Reason: {1}</p>""")
576 .format(historyFile.fileName(), f.errorString()))
577 f.remove() # get rid of the temporary file
578 self.historySaved.emit()
579 try:
580 self.__lastSavedUrl = self.__history[0].url
581 except IndexError:
582 self.__lastSavedUrl = ""
583
584 def __refreshFrequencies(self):
585 """
586 Private slot to recalculate the refresh frequencies.
587 """
588 self.__historyFilterModel.recalculateFrequencies()
589 self.__startFrequencyTimer()
590
591 def __startFrequencyTimer(self):
592 """
593 Private method to start the timer to recalculate the frequencies.
594 """
595 tomorrow = QDateTime(QDate.currentDate().addDays(1), QTime(3, 0))
596 self.__frequencyTimer.start(
597 QDateTime.currentDateTime().secsTo(tomorrow) * 1000)
598
599 def siteVisitsCount(self, scheme, host):
600 """
601 Public method to get the visit count for a web site using the given
602 scheme.
603
604 @param scheme scheme to look for
605 @type str
606 @param host host to look for
607 @type str
608 @return number of visits to this site
609 @rtype int
610 """
611 count = 0
612 url = "{0}://{1}".format(scheme.lower(), host.lower())
613
614 seenUrls = []
615
616 for index in range(len(self.__history)):
617 historyUrl = self.__history[index].url
618 if historyUrl.startswith(url) and historyUrl not in seenUrls:
619 count += self.__history[index].visitCount
620 seenUrls.append(historyUrl)
621
622 return count

eric ide

mercurial