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