src/eric7/WebBrowser/Bookmarks/BookmarksManager.py

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

eric ide

mercurial