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