eric6/Helpviewer/History/HistoryManager.py

branch
maintenance
changeset 7286
7eb04391adf7
parent 7226
babe80d84a3e
parent 7285
1ff497f33f31
child 7287
1c17f2191bdd
equal deleted inserted replaced
7226:babe80d84a3e 7286:7eb04391adf7
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
17 from PyQt5.QtWebKit import QWebHistoryInterface, QWebSettings
18
19 from E5Gui import E5MessageBox
20
21 from Utilities.AutoSaver import AutoSaver
22 import Utilities
23 import Preferences
24
25 HISTORY_VERSION = 42
26
27
28 class HistoryEntry(object):
29 """
30 Class implementing a history entry.
31 """
32 def __init__(self, url=None, dateTime=None, title=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 """
40 self.url = url and url or ""
41 self.dateTime = dateTime and dateTime or QDateTime()
42 self.title = title and title or ""
43
44 def __eq__(self, other):
45 """
46 Special method determining equality.
47
48 @param other reference to the history entry to compare against
49 (HistoryEntry)
50 @return flag indicating equality (boolean)
51 """
52 return other.title == self.title and \
53 other.url == self.url and \
54 other.dateTime == self.dateTime
55
56 def __lt__(self, other):
57 """
58 Special method determining less relation.
59
60 Note: History is sorted in reverse order by date and time
61
62 @param other reference to the history entry to compare against
63 (HistoryEntry)
64 @return flag indicating less (boolean)
65 """
66 return self.dateTime > other.dateTime
67
68 def userTitle(self):
69 """
70 Public method to get the title of the history entry.
71
72 @return title of the entry (string)
73 """
74 if not self.title:
75 page = QFileInfo(QUrl(self.url).path()).fileName()
76 if page:
77 return page
78 return self.url
79 return self.title
80
81
82 class HistoryManager(QWebHistoryInterface):
83 """
84 Class implementing the history manager.
85
86 @signal historyCleared() emitted after the history has been cleared
87 @signal historyReset() emitted after the history has been reset
88 @signal entryAdded(HistoryEntry) emitted after a history entry has been
89 added
90 @signal entryRemoved(HistoryEntry) emitted after a history entry has been
91 removed
92 @signal entryUpdated(int) emitted after a history entry has been updated
93 @signal historySaved() emitted after the history was saved
94 """
95 historyCleared = pyqtSignal()
96 historyReset = pyqtSignal()
97 entryAdded = pyqtSignal(HistoryEntry)
98 entryRemoved = pyqtSignal(HistoryEntry)
99 entryUpdated = pyqtSignal(int)
100 historySaved = pyqtSignal()
101
102 def __init__(self, parent=None):
103 """
104 Constructor
105
106 @param parent reference to the parent object (QObject)
107 """
108 super(HistoryManager, self).__init__(parent)
109
110 self.__saveTimer = AutoSaver(self, self.save)
111 self.__daysToExpire = Preferences.getHelp("HistoryLimit")
112 self.__history = []
113 self.__lastSavedUrl = ""
114
115 self.__expiredTimer = QTimer(self)
116 self.__expiredTimer.setSingleShot(True)
117 self.__expiredTimer.timeout.connect(self.__checkForExpired)
118
119 self.__frequencyTimer = QTimer(self)
120 self.__frequencyTimer.setSingleShot(True)
121 self.__frequencyTimer.timeout.connect(self.__refreshFrequencies)
122
123 self.entryAdded.connect(self.__saveTimer.changeOccurred)
124 self.entryRemoved.connect(self.__saveTimer.changeOccurred)
125
126 self.__load()
127
128 from .HistoryModel import HistoryModel
129 from .HistoryFilterModel import HistoryFilterModel
130 from .HistoryTreeModel import HistoryTreeModel
131
132 self.__historyModel = HistoryModel(self, self)
133 self.__historyFilterModel = \
134 HistoryFilterModel(self.__historyModel, self)
135 self.__historyTreeModel = \
136 HistoryTreeModel(self.__historyFilterModel, self)
137
138 super(HistoryManager, self).setDefaultInterface(self)
139 self.__startFrequencyTimer()
140
141 def close(self):
142 """
143 Public method to close the history manager.
144 """
145 # remove history items on application exit
146 if self.__daysToExpire == -2:
147 self.clear()
148 self.__saveTimer.saveIfNeccessary()
149
150 def history(self):
151 """
152 Public method to return the history.
153
154 @return reference to the list of history entries (list of HistoryEntry)
155 """
156 return self.__history[:]
157
158 def setHistory(self, history, loadedAndSorted=False):
159 """
160 Public method to set a new history.
161
162 @param history reference to the list of history entries to be set
163 (list of HistoryEntry)
164 @param loadedAndSorted flag indicating that the list is sorted
165 (boolean)
166 """
167 self.__history = history[:]
168 if not loadedAndSorted:
169 self.__history.sort()
170
171 self.__checkForExpired()
172
173 if loadedAndSorted:
174 try:
175 self.__lastSavedUrl = self.__history[0].url
176 except IndexError:
177 self.__lastSavedUrl = ""
178 else:
179 self.__lastSavedUrl = ""
180 self.__saveTimer.changeOccurred()
181 self.historyReset.emit()
182
183 def historyContains(self, url):
184 """
185 Public method to check the history for an entry.
186
187 @param url URL to check for (string)
188 @return flag indicating success (boolean)
189 """
190 return self.__historyFilterModel.historyContains(url)
191
192 def _addHistoryEntry(self, itm):
193 """
194 Protected method to add a history item.
195
196 @param itm reference to the history item to add (HistoryEntry)
197 """
198 globalSettings = QWebSettings.globalSettings()
199 if globalSettings.testAttribute(QWebSettings.PrivateBrowsingEnabled):
200 return
201
202 self.__history.insert(0, itm)
203 self.entryAdded.emit(itm)
204 if len(self.__history) == 1:
205 self.__checkForExpired()
206
207 def _removeHistoryEntry(self, itm):
208 """
209 Protected method to remove a history item.
210
211 @param itm reference to the history item to remove (HistoryEntry)
212 """
213 self.__lastSavedUrl = ""
214 self.__history.remove(itm)
215 self.entryRemoved.emit(itm)
216
217 def addHistoryEntry(self, url):
218 """
219 Public method to add a history entry.
220
221 @param url URL to be added (string)
222 """
223 cleanurl = QUrl(url)
224 if cleanurl.scheme() not in ["eric", "about"]:
225 if cleanurl.password():
226 # don't save the password in the history
227 cleanurl.setPassword("")
228 if cleanurl.host():
229 cleanurl.setHost(cleanurl.host().lower())
230 itm = HistoryEntry(cleanurl.toString(),
231 QDateTime.currentDateTime())
232 self._addHistoryEntry(itm)
233
234 def updateHistoryEntry(self, url, title):
235 """
236 Public method to update a history entry.
237
238 @param url URL of the entry to update (string)
239 @param title title of the entry to update (string)
240 """
241 cleanurl = QUrl(url)
242 if cleanurl.scheme() not in ["eric", "about"]:
243 for index in range(len(self.__history)):
244 if url == self.__history[index].url:
245 self.__history[index].title = title
246 self.__saveTimer.changeOccurred()
247 if not self.__lastSavedUrl:
248 self.__lastSavedUrl = self.__history[index].url
249 self.entryUpdated.emit(index)
250 break
251
252 def removeHistoryEntry(self, url, title=""):
253 """
254 Public method to remove a history entry.
255
256 @param url URL of the entry to remove (QUrl)
257 @param title title of the entry to remove (string)
258 """
259 for index in range(len(self.__history)):
260 if url == QUrl(self.__history[index].url) and \
261 (not title or title == self.__history[index].title):
262 self._removeHistoryEntry(self.__history[index])
263 break
264
265 def historyModel(self):
266 """
267 Public method to get a reference to the history model.
268
269 @return reference to the history model (HistoryModel)
270 """
271 return self.__historyModel
272
273 def historyFilterModel(self):
274 """
275 Public method to get a reference to the history filter model.
276
277 @return reference to the history filter model (HistoryFilterModel)
278 """
279 return self.__historyFilterModel
280
281 def historyTreeModel(self):
282 """
283 Public method to get a reference to the history tree model.
284
285 @return reference to the history tree model (HistoryTreeModel)
286 """
287 return self.__historyTreeModel
288
289 def __checkForExpired(self):
290 """
291 Private slot to check entries for expiration.
292 """
293 if self.__daysToExpire < 0 or len(self.__history) == 0:
294 return
295
296 now = QDateTime.currentDateTime()
297 nextTimeout = 0
298
299 while self.__history:
300 checkForExpired = QDateTime(self.__history[-1].dateTime)
301 checkForExpired.setDate(
302 checkForExpired.date().addDays(self.__daysToExpire))
303 if now.daysTo(checkForExpired) > 7:
304 nextTimeout = 7 * 86400
305 else:
306 nextTimeout = now.secsTo(checkForExpired)
307 if nextTimeout > 0:
308 break
309
310 itm = self.__history.pop(-1)
311 self.__lastSavedUrl = ""
312 self.entryRemoved.emit(itm)
313 self.__saveTimer.saveIfNeccessary()
314
315 if nextTimeout > 0:
316 self.__expiredTimer.start(nextTimeout * 1000)
317
318 def daysToExpire(self):
319 """
320 Public method to get the days for entry expiration.
321
322 @return days for entry expiration (integer)
323 """
324 return self.__daysToExpire
325
326 def setDaysToExpire(self, limit):
327 """
328 Public method to set the days for entry expiration.
329
330 @param limit days for entry expiration (integer)
331 """
332 if self.__daysToExpire == limit:
333 return
334
335 self.__daysToExpire = limit
336 self.__checkForExpired()
337 self.__saveTimer.changeOccurred()
338
339 def preferencesChanged(self):
340 """
341 Public method to indicate a change of preferences.
342 """
343 self.setDaysToExpire(Preferences.getHelp("HistoryLimit"))
344
345 @pyqtSlot()
346 def clear(self, period=0):
347 """
348 Public slot to clear the complete history.
349
350 @param period history period in milliseconds to be cleared (integer)
351 """
352 if period == 0:
353 self.__history = []
354 self.historyReset.emit()
355 else:
356 breakMS = QDateTime.currentMSecsSinceEpoch() - period
357 while self.__history and \
358 (QDateTime(self.__history[0].dateTime).toMSecsSinceEpoch() >
359 breakMS):
360 itm = self.__history.pop(0)
361 self.entryRemoved.emit(itm)
362 self.__lastSavedUrl = ""
363 self.__saveTimer.changeOccurred()
364 self.__saveTimer.saveIfNeccessary()
365 self.historyCleared.emit()
366
367 def getFileName(self):
368 """
369 Public method to get the file name of the history file.
370
371 @return name of the history file (string)
372 """
373 return os.path.join(Utilities.getConfigDir(), "browser", "history")
374
375 def reload(self):
376 """
377 Public method to reload the history.
378 """
379 self.__load()
380
381 def __load(self):
382 """
383 Private method to load the saved history entries from disk.
384 """
385 historyFile = QFile(self.getFileName())
386 if not historyFile.exists():
387 return
388 if not historyFile.open(QIODevice.ReadOnly):
389 E5MessageBox.warning(
390 None,
391 self.tr("Loading History"),
392 self.tr(
393 """<p>Unable to open history file <b>{0}</b>.<br/>"""
394 """Reason: {1}</p>""")
395 .format(historyFile.fileName, historyFile.errorString()))
396 return
397
398 history = []
399
400 # double check, that the history file is sorted as it is read
401 needToSort = False
402 lastInsertedItem = HistoryEntry()
403 data = QByteArray(historyFile.readAll())
404 stream = QDataStream(data, QIODevice.ReadOnly)
405 stream.setVersion(QDataStream.Qt_4_6)
406 while not stream.atEnd():
407 ver = stream.readUInt32()
408 if ver != HISTORY_VERSION:
409 continue
410 itm = HistoryEntry()
411 itm.url = Utilities.readStringFromStream(stream)
412 stream >> itm.dateTime
413 itm.title = Utilities.readStringFromStream(stream)
414
415 if not itm.dateTime.isValid():
416 continue
417
418 if itm == lastInsertedItem:
419 if not lastInsertedItem.title and len(history) > 0:
420 history[0].title = itm.title
421 continue
422
423 if not needToSort and history and lastInsertedItem < itm:
424 needToSort = True
425
426 history.insert(0, itm)
427 lastInsertedItem = itm
428 historyFile.close()
429
430 if needToSort:
431 history.sort()
432
433 self.setHistory(history, True)
434
435 # if the history had to be sorted, rewrite the history sorted
436 if needToSort:
437 self.__lastSavedUrl = ""
438 self.__saveTimer.changeOccurred()
439
440 def save(self):
441 """
442 Public slot to save the history entries to disk.
443 """
444 historyFile = QFile(self.getFileName())
445 if not historyFile.exists():
446 self.__lastSavedUrl = ""
447
448 saveAll = self.__lastSavedUrl == ""
449 first = len(self.__history) - 1
450 if not saveAll:
451 # find the first one to save
452 for index in range(len(self.__history)):
453 if self.__history[index].url == self.__lastSavedUrl:
454 first = index - 1
455 break
456 if first == len(self.__history) - 1:
457 saveAll = True
458
459 if saveAll:
460 # use a temporary file when saving everything
461 f = QTemporaryFile()
462 f.setAutoRemove(False)
463 opened = f.open()
464 else:
465 f = historyFile
466 opened = f.open(QIODevice.Append)
467
468 if not opened:
469 E5MessageBox.warning(
470 None,
471 self.tr("Saving History"),
472 self.tr(
473 """<p>Unable to open history file <b>{0}</b>.<br/>"""
474 """Reason: {1}</p>""")
475 .format(f.fileName(), f.errorString()))
476 return
477
478 for index in range(first, -1, -1):
479 data = QByteArray()
480 stream = QDataStream(data, QIODevice.WriteOnly)
481 stream.setVersion(QDataStream.Qt_4_6)
482 itm = self.__history[index]
483 stream.writeUInt32(HISTORY_VERSION)
484 stream.writeString(itm.url.encode("utf-8"))
485 stream << itm.dateTime
486 stream.writeString(itm.title.encode('utf-8'))
487 f.write(data)
488
489 f.close()
490 if saveAll:
491 if historyFile.exists() and not historyFile.remove():
492 E5MessageBox.warning(
493 None,
494 self.tr("Saving History"),
495 self.tr(
496 """<p>Error removing old history file <b>{0}</b>."""
497 """<br/>Reason: {1}</p>""")
498 .format(historyFile.fileName(),
499 historyFile.errorString()))
500 if not f.copy(historyFile.fileName()):
501 E5MessageBox.warning(
502 None,
503 self.tr("Saving History"),
504 self.tr(
505 """<p>Error moving new history file over old one """
506 """(<b>{0}</b>).<br/>Reason: {1}</p>""")
507 .format(historyFile.fileName(), f.errorString()))
508 f.remove() # get rid of the temporary file
509 self.historySaved.emit()
510 try:
511 self.__lastSavedUrl = self.__history[0].url
512 except IndexError:
513 self.__lastSavedUrl = ""
514
515 def __refreshFrequencies(self):
516 """
517 Private slot to recalculate the refresh frequencies.
518 """
519 self.__historyFilterModel.recalculateFrequencies()
520 self.__startFrequencyTimer()
521
522 def __startFrequencyTimer(self):
523 """
524 Private method to start the timer to recalculate the frequencies.
525 """
526 tomorrow = QDateTime(QDate.currentDate().addDays(1), QTime(3, 0))
527 self.__frequencyTimer.start(
528 QDateTime.currentDateTime().secsTo(tomorrow) * 1000)

eric ide

mercurial