1# -*- coding: utf-8 -*- 2 3# Copyright (c) 2009 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> 4# 5 6""" 7Module implementing the bookmarks manager. 8""" 9 10import os 11import contextlib 12 13from PyQt5.QtCore import ( 14 pyqtSignal, QT_TRANSLATE_NOOP, QObject, QFile, QIODevice, QXmlStreamReader, 15 QDateTime, QFileInfo, QUrl, QCoreApplication 16) 17from PyQt5.QtWidgets import QUndoStack, QUndoCommand, QDialog 18 19from E5Gui import E5MessageBox, E5FileDialog 20 21from .BookmarkNode import BookmarkNode 22 23from Utilities.AutoSaver import AutoSaver 24import Utilities 25 26BOOKMARKBAR = QT_TRANSLATE_NOOP("BookmarksManager", "Bookmarks Bar") 27BOOKMARKMENU = QT_TRANSLATE_NOOP("BookmarksManager", "Bookmarks Menu") 28 29StartRoot = 0 30StartMenu = 1 31StartToolBar = 2 32 33 34class 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 521class 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 561class 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 593class 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) 643