eric7/WebBrowser/Bookmarks/BookmarksManager.py

branch
eric7
changeset 8312
800c432b34c8
parent 8243
cc717c2ae956
child 8314
e3642a6a1e71
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2009 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the bookmarks manager.
8 """
9
10 import os
11 import contextlib
12
13 from PyQt5.QtCore import (
14 pyqtSignal, QT_TRANSLATE_NOOP, QObject, QFile, QIODevice, QXmlStreamReader,
15 QDateTime, QFileInfo, QUrl, QCoreApplication
16 )
17 from PyQt5.QtWidgets import QUndoStack, QUndoCommand, QDialog
18
19 from E5Gui import E5MessageBox, E5FileDialog
20
21 from .BookmarkNode import BookmarkNode
22
23 from Utilities.AutoSaver import AutoSaver
24 import Utilities
25
26 BOOKMARKBAR = QT_TRANSLATE_NOOP("BookmarksManager", "Bookmarks Bar")
27 BOOKMARKMENU = QT_TRANSLATE_NOOP("BookmarksManager", "Bookmarks Menu")
28
29 StartRoot = 0
30 StartMenu = 1
31 StartToolBar = 2
32
33
34 class BookmarksManager(QObject):
35 """
36 Class implementing the bookmarks manager.
37
38 @signal entryAdded(BookmarkNode) emitted after a bookmark node has been
39 added
40 @signal entryRemoved(BookmarkNode, int, BookmarkNode) emitted after a
41 bookmark node has been removed
42 @signal entryChanged(BookmarkNode) emitted after a bookmark node has been
43 changed
44 @signal bookmarksSaved() emitted after the bookmarks were saved
45 @signal bookmarksReloaded() emitted after the bookmarks were reloaded
46 """
47 entryAdded = pyqtSignal(BookmarkNode)
48 entryRemoved = pyqtSignal(BookmarkNode, int, BookmarkNode)
49 entryChanged = pyqtSignal(BookmarkNode)
50 bookmarksSaved = pyqtSignal()
51 bookmarksReloaded = pyqtSignal()
52
53 def __init__(self, parent=None):
54 """
55 Constructor
56
57 @param parent reference to the parent object (QObject)
58 """
59 super().__init__(parent)
60
61 self.__saveTimer = AutoSaver(self, self.save)
62 self.entryAdded.connect(self.__saveTimer.changeOccurred)
63 self.entryRemoved.connect(self.__saveTimer.changeOccurred)
64 self.entryChanged.connect(self.__saveTimer.changeOccurred)
65
66 self.__initialize()
67
68 def __initialize(self):
69 """
70 Private method to initialize some data.
71 """
72 self.__loaded = False
73 self.__bookmarkRootNode = None
74 self.__toolbar = None
75 self.__menu = None
76 self.__bookmarksModel = None
77 self.__commands = QUndoStack()
78
79 @classmethod
80 def getFileName(cls):
81 """
82 Class method to get the file name of the bookmark file.
83
84 @return name of the bookmark file (string)
85 """
86 return os.path.join(Utilities.getConfigDir(), "web_browser",
87 "bookmarks.xbel")
88
89 def close(self):
90 """
91 Public method to close the bookmark manager.
92 """
93 self.__saveTimer.saveIfNeccessary()
94
95 def undoRedoStack(self):
96 """
97 Public method to get a reference to the undo stack.
98
99 @return reference to the undo stack (QUndoStack)
100 """
101 return self.__commands
102
103 def changeExpanded(self):
104 """
105 Public method to handle a change of the expanded state.
106 """
107 self.__saveTimer.changeOccurred()
108
109 def reload(self):
110 """
111 Public method used to initiate a reloading of the bookmarks.
112 """
113 self.__initialize()
114 self.load()
115 self.bookmarksReloaded.emit()
116
117 def load(self):
118 """
119 Public method to load the bookmarks.
120
121 @exception RuntimeError raised to indicate an error loading the
122 bookmarks
123 """
124 if self.__loaded:
125 return
126
127 self.__loaded = True
128
129 bookmarkFile = self.getFileName()
130 if not QFile.exists(bookmarkFile):
131 bookmarkFile = QFile(os.path.join(
132 os.path.dirname(__file__), "DefaultBookmarks.xbel"))
133 bookmarkFile.open(QIODevice.OpenModeFlag.ReadOnly)
134
135 from .XbelReader import XbelReader
136 reader = XbelReader()
137 self.__bookmarkRootNode = reader.read(bookmarkFile)
138 if reader.error() != QXmlStreamReader.Error.NoError:
139 E5MessageBox.warning(
140 None,
141 self.tr("Loading Bookmarks"),
142 self.tr(
143 """Error when loading bookmarks on line {0},"""
144 """ column {1}:\n {2}""")
145 .format(reader.lineNumber(),
146 reader.columnNumber(),
147 reader.errorString()))
148
149 others = []
150 for index in range(
151 len(self.__bookmarkRootNode.children()) - 1, -1, -1):
152 node = self.__bookmarkRootNode.children()[index]
153 if node.type() == BookmarkNode.Folder:
154 if (
155 (node.title == self.tr("Toolbar Bookmarks") or
156 node.title == BOOKMARKBAR) and
157 self.__toolbar is None
158 ):
159 node.title = self.tr(BOOKMARKBAR)
160 self.__toolbar = node
161
162 if (
163 (node.title == self.tr("Menu") or
164 node.title == BOOKMARKMENU) and
165 self.__menu is None
166 ):
167 node.title = self.tr(BOOKMARKMENU)
168 self.__menu = node
169 else:
170 others.append(node)
171 self.__bookmarkRootNode.remove(node)
172
173 if len(self.__bookmarkRootNode.children()) > 0:
174 raise RuntimeError("Error loading bookmarks.")
175
176 if self.__toolbar is None:
177 self.__toolbar = BookmarkNode(BookmarkNode.Folder,
178 self.__bookmarkRootNode)
179 self.__toolbar.title = self.tr(BOOKMARKBAR)
180 else:
181 self.__bookmarkRootNode.add(self.__toolbar)
182
183 if self.__menu is None:
184 self.__menu = BookmarkNode(BookmarkNode.Folder,
185 self.__bookmarkRootNode)
186 self.__menu.title = self.tr(BOOKMARKMENU)
187 else:
188 self.__bookmarkRootNode.add(self.__menu)
189
190 for node in others:
191 self.__menu.add(node)
192
193 def save(self):
194 """
195 Public method to save the bookmarks.
196 """
197 if not self.__loaded:
198 return
199
200 from .XbelWriter import XbelWriter
201 writer = XbelWriter()
202 bookmarkFile = self.getFileName()
203
204 # save root folder titles in English (i.e. not localized)
205 self.__menu.title = BOOKMARKMENU
206 self.__toolbar.title = BOOKMARKBAR
207 if not writer.write(bookmarkFile, self.__bookmarkRootNode):
208 E5MessageBox.warning(
209 None,
210 self.tr("Saving Bookmarks"),
211 self.tr("""Error saving bookmarks to <b>{0}</b>.""")
212 .format(bookmarkFile))
213
214 # restore localized titles
215 self.__menu.title = self.tr(BOOKMARKMENU)
216 self.__toolbar.title = self.tr(BOOKMARKBAR)
217
218 self.bookmarksSaved.emit()
219
220 def addBookmark(self, parent, node, row=-1):
221 """
222 Public method to add a bookmark.
223
224 @param parent reference to the node to add to (BookmarkNode)
225 @param node reference to the node to add (BookmarkNode)
226 @param row row number (integer)
227 """
228 if not self.__loaded:
229 return
230
231 self.setTimestamp(node, BookmarkNode.TsAdded,
232 QDateTime.currentDateTime())
233
234 command = InsertBookmarksCommand(self, parent, node, row)
235 self.__commands.push(command)
236
237 def removeBookmark(self, node):
238 """
239 Public method to remove a bookmark.
240
241 @param node reference to the node to be removed (BookmarkNode)
242 """
243 if not self.__loaded:
244 return
245
246 parent = node.parent()
247 row = parent.children().index(node)
248 command = RemoveBookmarksCommand(self, parent, row)
249 self.__commands.push(command)
250
251 def setTitle(self, node, newTitle):
252 """
253 Public method to set the title of a bookmark.
254
255 @param node reference to the node to be changed (BookmarkNode)
256 @param newTitle title to be set (string)
257 """
258 if not self.__loaded:
259 return
260
261 command = ChangeBookmarkCommand(self, node, newTitle, True)
262 self.__commands.push(command)
263
264 def setUrl(self, node, newUrl):
265 """
266 Public method to set the URL of a bookmark.
267
268 @param node reference to the node to be changed (BookmarkNode)
269 @param newUrl URL to be set (string)
270 """
271 if not self.__loaded:
272 return
273
274 command = ChangeBookmarkCommand(self, node, newUrl, False)
275 self.__commands.push(command)
276
277 def setNodeChanged(self, node):
278 """
279 Public method to signal changes of bookmarks other than title, URL
280 or timestamp.
281
282 @param node reference to the bookmark (BookmarkNode)
283 """
284 self.__saveTimer.changeOccurred()
285
286 def setTimestamp(self, node, timestampType, timestamp):
287 """
288 Public method to set the URL of a bookmark.
289
290 @param node reference to the node to be changed (BookmarkNode)
291 @param timestampType type of the timestamp to set
292 (BookmarkNode.TsAdded, BookmarkNode.TsModified,
293 BookmarkNode.TsVisited)
294 @param timestamp timestamp to set (QDateTime)
295 """
296 if not self.__loaded:
297 return
298
299 if timestampType == BookmarkNode.TsAdded:
300 node.added = timestamp
301 elif timestampType == BookmarkNode.TsModified:
302 node.modified = timestamp
303 elif timestampType == BookmarkNode.TsVisited:
304 node.visited = timestamp
305 self.__saveTimer.changeOccurred()
306
307 def incVisitCount(self, node):
308 """
309 Public method to increment the visit count of a bookmark.
310
311 @param node reference to the node to be changed (BookmarkNode)
312 """
313 if not self.__loaded:
314 return
315
316 if node:
317 node.visitCount += 1
318 self.__saveTimer.changeOccurred()
319
320 def setVisitCount(self, node, count):
321 """
322 Public method to set the visit count of a bookmark.
323
324 @param node reference to the node to be changed (BookmarkNode)
325 @param count visit count to be set (int or str)
326 """
327 with contextlib.suppress(ValueError):
328 node.visitCount = int(count)
329 self.__saveTimer.changeOccurred()
330
331 def bookmarks(self):
332 """
333 Public method to get a reference to the root bookmark node.
334
335 @return reference to the root bookmark node (BookmarkNode)
336 """
337 if not self.__loaded:
338 self.load()
339
340 return self.__bookmarkRootNode
341
342 def menu(self):
343 """
344 Public method to get a reference to the bookmarks menu node.
345
346 @return reference to the bookmarks menu node (BookmarkNode)
347 """
348 if not self.__loaded:
349 self.load()
350
351 return self.__menu
352
353 def toolbar(self):
354 """
355 Public method to get a reference to the bookmarks toolbar node.
356
357 @return reference to the bookmarks toolbar node (BookmarkNode)
358 """
359 if not self.__loaded:
360 self.load()
361
362 return self.__toolbar
363
364 def bookmarksModel(self):
365 """
366 Public method to get a reference to the bookmarks model.
367
368 @return reference to the bookmarks model (BookmarksModel)
369 """
370 if self.__bookmarksModel is None:
371 from .BookmarksModel import BookmarksModel
372 self.__bookmarksModel = BookmarksModel(self, self)
373 return self.__bookmarksModel
374
375 def importBookmarks(self):
376 """
377 Public method to import bookmarks.
378 """
379 from .BookmarksImportDialog import BookmarksImportDialog
380 dlg = BookmarksImportDialog()
381 if dlg.exec() == QDialog.DialogCode.Accepted:
382 importRootNode = dlg.getImportedBookmarks()
383 if importRootNode is not None:
384 self.addBookmark(self.menu(), importRootNode)
385
386 def exportBookmarks(self):
387 """
388 Public method to export the bookmarks.
389 """
390 fileName, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
391 None,
392 self.tr("Export Bookmarks"),
393 "eric6_bookmarks.xbel",
394 self.tr("XBEL bookmarks (*.xbel);;"
395 "XBEL bookmarks (*.xml);;"
396 "HTML Bookmarks (*.html)"))
397 if not fileName:
398 return
399
400 ext = QFileInfo(fileName).suffix()
401 if not ext:
402 ex = selectedFilter.split("(*")[1].split(")")[0]
403 if ex:
404 fileName += ex
405
406 ext = QFileInfo(fileName).suffix()
407 if ext == "html":
408 from .NsHtmlWriter import NsHtmlWriter
409 writer = NsHtmlWriter()
410 else:
411 from .XbelWriter import XbelWriter
412 writer = XbelWriter()
413 if not writer.write(fileName, self.__bookmarkRootNode):
414 E5MessageBox.critical(
415 None,
416 self.tr("Exporting Bookmarks"),
417 self.tr("""Error exporting bookmarks to <b>{0}</b>.""")
418 .format(fileName))
419
420 def faviconChanged(self, url):
421 """
422 Public slot to update the icon image for an URL.
423
424 @param url URL of the icon to update (QUrl or string)
425 """
426 if isinstance(url, QUrl):
427 url = url.toString()
428 nodes = self.bookmarksForUrl(url)
429 for node in nodes:
430 self.bookmarksModel().entryChanged(node)
431
432 def bookmarkForUrl(self, url, start=StartRoot):
433 """
434 Public method to get a bookmark node for a given URL.
435
436 @param url URL of the bookmark to search for (QUrl or string)
437 @param start indicator for the start of the search
438 (StartRoot, StartMenu, StartToolBar)
439 @return bookmark node for the given url (BookmarkNode)
440 """
441 if start == StartMenu:
442 startNode = self.__menu
443 elif start == StartToolBar:
444 startNode = self.__toolbar
445 else:
446 startNode = self.__bookmarkRootNode
447 if startNode is None:
448 return None
449
450 if isinstance(url, QUrl):
451 url = url.toString()
452
453 return self.__searchBookmark(url, startNode)
454
455 def __searchBookmark(self, url, startNode):
456 """
457 Private method get a bookmark node for a given URL.
458
459 @param url URL of the bookmark to search for (string)
460 @param startNode reference to the node to start searching
461 (BookmarkNode)
462 @return bookmark node for the given url (BookmarkNode)
463 """
464 bm = None
465 for node in startNode.children():
466 if node.type() == BookmarkNode.Folder:
467 bm = self.__searchBookmark(url, node)
468 elif (
469 node.type() == BookmarkNode.Bookmark and
470 node.url == url
471 ):
472 bm = node
473 if bm is not None:
474 return bm
475 return None
476
477 def bookmarksForUrl(self, url, start=StartRoot):
478 """
479 Public method to get a list of bookmark nodes for a given URL.
480
481 @param url URL of the bookmarks to search for (QUrl or string)
482 @param start indicator for the start of the search
483 (StartRoot, StartMenu, StartToolBar)
484 @return list of bookmark nodes for the given url (list of BookmarkNode)
485 """
486 if start == StartMenu:
487 startNode = self.__menu
488 elif start == StartToolBar:
489 startNode = self.__toolbar
490 else:
491 startNode = self.__bookmarkRootNode
492 if startNode is None:
493 return []
494
495 if isinstance(url, QUrl):
496 url = url.toString()
497
498 return self.__searchBookmarks(url, startNode)
499
500 def __searchBookmarks(self, url, startNode):
501 """
502 Private method get a list of bookmark nodes for a given URL.
503
504 @param url URL of the bookmarks to search for (string)
505 @param startNode reference to the node to start searching
506 (BookmarkNode)
507 @return list of bookmark nodes for the given url (list of BookmarkNode)
508 """
509 bm = []
510 for node in startNode.children():
511 if node.type() == BookmarkNode.Folder:
512 bm.extend(self.__searchBookmarks(url, node))
513 elif (
514 node.type() == BookmarkNode.Bookmark and
515 node.url == url
516 ):
517 bm.append(node)
518 return bm
519
520
521 class RemoveBookmarksCommand(QUndoCommand):
522 """
523 Class implementing the Remove undo command.
524 """
525 def __init__(self, bookmarksManager, parent, row):
526 """
527 Constructor
528
529 @param bookmarksManager reference to the bookmarks manager
530 (BookmarksManager)
531 @param parent reference to the parent node (BookmarkNode)
532 @param row row number of bookmark (integer)
533 """
534 super().__init__(
535 QCoreApplication.translate("BookmarksManager", "Remove Bookmark"))
536
537 self._row = row
538 self._bookmarksManager = bookmarksManager
539 try:
540 self._node = parent.children()[row]
541 except IndexError:
542 self._node = BookmarkNode()
543 self._parent = parent
544
545 def undo(self):
546 """
547 Public slot to perform the undo action.
548 """
549 self._parent.add(self._node, self._row)
550 self._bookmarksManager.entryAdded.emit(self._node)
551
552 def redo(self):
553 """
554 Public slot to perform the redo action.
555 """
556 self._parent.remove(self._node)
557 self._bookmarksManager.entryRemoved.emit(
558 self._parent, self._row, self._node)
559
560
561 class InsertBookmarksCommand(RemoveBookmarksCommand):
562 """
563 Class implementing the Insert undo command.
564 """
565 def __init__(self, bookmarksManager, parent, node, row):
566 """
567 Constructor
568
569 @param bookmarksManager reference to the bookmarks manager
570 (BookmarksManager)
571 @param parent reference to the parent node (BookmarkNode)
572 @param node reference to the node to be inserted (BookmarkNode)
573 @param row row number of bookmark (integer)
574 """
575 RemoveBookmarksCommand.__init__(self, bookmarksManager, parent, row)
576 self.setText(QCoreApplication.translate(
577 "BookmarksManager", "Insert Bookmark"))
578 self._node = node
579
580 def undo(self):
581 """
582 Public slot to perform the undo action.
583 """
584 RemoveBookmarksCommand.redo(self)
585
586 def redo(self):
587 """
588 Public slot to perform the redo action.
589 """
590 RemoveBookmarksCommand.undo(self)
591
592
593 class ChangeBookmarkCommand(QUndoCommand):
594 """
595 Class implementing the Insert undo command.
596 """
597 def __init__(self, bookmarksManager, node, newValue, title):
598 """
599 Constructor
600
601 @param bookmarksManager reference to the bookmarks manager
602 (BookmarksManager)
603 @param node reference to the node to be changed (BookmarkNode)
604 @param newValue new value to be set (string)
605 @param title flag indicating a change of the title (True) or
606 the URL (False) (boolean)
607 """
608 super().__init__()
609
610 self._bookmarksManager = bookmarksManager
611 self._title = title
612 self._newValue = newValue
613 self._node = node
614
615 if self._title:
616 self._oldValue = self._node.title
617 self.setText(QCoreApplication.translate(
618 "BookmarksManager", "Name Change"))
619 else:
620 self._oldValue = self._node.url
621 self.setText(QCoreApplication.translate(
622 "BookmarksManager", "Address Change"))
623
624 def undo(self):
625 """
626 Public slot to perform the undo action.
627 """
628 if self._title:
629 self._node.title = self._oldValue
630 else:
631 self._node.url = self._oldValue
632 self._bookmarksManager.entryChanged.emit(self._node)
633
634 def redo(self):
635 """
636 Public slot to perform the redo action.
637 """
638 if self._title:
639 self._node.title = self._newValue
640 else:
641 self._node.url = self._newValue
642 self._bookmarksManager.entryChanged.emit(self._node)

eric ide

mercurial