eric7/WebBrowser/History/HistoryManager.py

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

eric ide

mercurial