WebBrowser/History/HistoryManager.py

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

eric ide

mercurial