|
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) |