--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/WebBrowser/Bookmarks/BookmarksManager.py Sun Apr 14 15:09:21 2019 +0200 @@ -0,0 +1,641 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the bookmarks manager. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import pyqtSignal, QT_TRANSLATE_NOOP, QObject, QFile, \ + QIODevice, QXmlStreamReader, QDateTime, QFileInfo, QUrl, \ + QCoreApplication +from PyQt5.QtWidgets import QUndoStack, QUndoCommand, QDialog + +from E5Gui import E5MessageBox, E5FileDialog + +from .BookmarkNode import BookmarkNode + +from Utilities.AutoSaver import AutoSaver +import Utilities + +BOOKMARKBAR = QT_TRANSLATE_NOOP("BookmarksManager", "Bookmarks Bar") +BOOKMARKMENU = QT_TRANSLATE_NOOP("BookmarksManager", "Bookmarks Menu") + +StartRoot = 0 +StartMenu = 1 +StartToolBar = 2 + + +class BookmarksManager(QObject): + """ + Class implementing the bookmarks manager. + + @signal entryAdded(BookmarkNode) emitted after a bookmark node has been + added + @signal entryRemoved(BookmarkNode, int, BookmarkNode) emitted after a + bookmark node has been removed + @signal entryChanged(BookmarkNode) emitted after a bookmark node has been + changed + @signal bookmarksSaved() emitted after the bookmarks were saved + @signal bookmarksReloaded() emitted after the bookmarks were reloaded + """ + entryAdded = pyqtSignal(BookmarkNode) + entryRemoved = pyqtSignal(BookmarkNode, int, BookmarkNode) + entryChanged = pyqtSignal(BookmarkNode) + bookmarksSaved = pyqtSignal() + bookmarksReloaded = pyqtSignal() + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (QObject) + """ + super(BookmarksManager, self).__init__(parent) + + self.__saveTimer = AutoSaver(self, self.save) + self.entryAdded.connect(self.__saveTimer.changeOccurred) + self.entryRemoved.connect(self.__saveTimer.changeOccurred) + self.entryChanged.connect(self.__saveTimer.changeOccurred) + + self.__initialize() + + def __initialize(self): + """ + Private method to initialize some data. + """ + self.__loaded = False + self.__bookmarkRootNode = None + self.__toolbar = None + self.__menu = None + self.__bookmarksModel = None + self.__commands = QUndoStack() + + @classmethod + def getFileName(cls): + """ + Class method to get the file name of the bookmark file. + + @return name of the bookmark file (string) + """ + return os.path.join(Utilities.getConfigDir(), "web_browser", + "bookmarks.xbel") + + def close(self): + """ + Public method to close the bookmark manager. + """ + self.__saveTimer.saveIfNeccessary() + + def undoRedoStack(self): + """ + Public method to get a reference to the undo stack. + + @return reference to the undo stack (QUndoStack) + """ + return self.__commands + + def changeExpanded(self): + """ + Public method to handle a change of the expanded state. + """ + self.__saveTimer.changeOccurred() + + def reload(self): + """ + Public method used to initiate a reloading of the bookmarks. + """ + self.__initialize() + self.load() + self.bookmarksReloaded.emit() + + def load(self): + """ + Public method to load the bookmarks. + + @exception RuntimeError raised to indicate an error loading the + bookmarks + """ + if self.__loaded: + return + + self.__loaded = True + + bookmarkFile = self.getFileName() + if not QFile.exists(bookmarkFile): + from . import DefaultBookmarks_rc # __IGNORE_WARNING__ + bookmarkFile = QFile(":/DefaultBookmarks.xbel") + bookmarkFile.open(QIODevice.ReadOnly) + + from .XbelReader import XbelReader + reader = XbelReader() + self.__bookmarkRootNode = reader.read(bookmarkFile) + if reader.error() != QXmlStreamReader.NoError: + E5MessageBox.warning( + None, + self.tr("Loading Bookmarks"), + self.tr( + """Error when loading bookmarks on line {0},""" + """ column {1}:\n {2}""") + .format(reader.lineNumber(), + reader.columnNumber(), + reader.errorString())) + + others = [] + for index in range( + len(self.__bookmarkRootNode.children()) - 1, -1, -1): + node = self.__bookmarkRootNode.children()[index] + if node.type() == BookmarkNode.Folder: + if (node.title == self.tr("Toolbar Bookmarks") or + node.title == BOOKMARKBAR) and \ + self.__toolbar is None: + node.title = self.tr(BOOKMARKBAR) + self.__toolbar = node + + if (node.title == self.tr("Menu") or + node.title == BOOKMARKMENU) and \ + self.__menu is None: + node.title = self.tr(BOOKMARKMENU) + self.__menu = node + else: + others.append(node) + self.__bookmarkRootNode.remove(node) + + if len(self.__bookmarkRootNode.children()) > 0: + raise RuntimeError("Error loading bookmarks.") + + if self.__toolbar is None: + self.__toolbar = BookmarkNode(BookmarkNode.Folder, + self.__bookmarkRootNode) + self.__toolbar.title = self.tr(BOOKMARKBAR) + else: + self.__bookmarkRootNode.add(self.__toolbar) + + if self.__menu is None: + self.__menu = BookmarkNode(BookmarkNode.Folder, + self.__bookmarkRootNode) + self.__menu.title = self.tr(BOOKMARKMENU) + else: + self.__bookmarkRootNode.add(self.__menu) + + for node in others: + self.__menu.add(node) + + def save(self): + """ + Public method to save the bookmarks. + """ + if not self.__loaded: + return + + from .XbelWriter import XbelWriter + writer = XbelWriter() + bookmarkFile = self.getFileName() + + # save root folder titles in English (i.e. not localized) + self.__menu.title = BOOKMARKMENU + self.__toolbar.title = BOOKMARKBAR + if not writer.write(bookmarkFile, self.__bookmarkRootNode): + E5MessageBox.warning( + None, + self.tr("Saving Bookmarks"), + self.tr("""Error saving bookmarks to <b>{0}</b>.""") + .format(bookmarkFile)) + + # restore localized titles + self.__menu.title = self.tr(BOOKMARKMENU) + self.__toolbar.title = self.tr(BOOKMARKBAR) + + self.bookmarksSaved.emit() + + def addBookmark(self, parent, node, row=-1): + """ + Public method to add a bookmark. + + @param parent reference to the node to add to (BookmarkNode) + @param node reference to the node to add (BookmarkNode) + @param row row number (integer) + """ + if not self.__loaded: + return + + self.setTimestamp(node, BookmarkNode.TsAdded, + QDateTime.currentDateTime()) + + command = InsertBookmarksCommand(self, parent, node, row) + self.__commands.push(command) + + def removeBookmark(self, node): + """ + Public method to remove a bookmark. + + @param node reference to the node to be removed (BookmarkNode) + """ + if not self.__loaded: + return + + parent = node.parent() + row = parent.children().index(node) + command = RemoveBookmarksCommand(self, parent, row) + self.__commands.push(command) + + def setTitle(self, node, newTitle): + """ + Public method to set the title of a bookmark. + + @param node reference to the node to be changed (BookmarkNode) + @param newTitle title to be set (string) + """ + if not self.__loaded: + return + + command = ChangeBookmarkCommand(self, node, newTitle, True) + self.__commands.push(command) + + def setUrl(self, node, newUrl): + """ + Public method to set the URL of a bookmark. + + @param node reference to the node to be changed (BookmarkNode) + @param newUrl URL to be set (string) + """ + if not self.__loaded: + return + + command = ChangeBookmarkCommand(self, node, newUrl, False) + self.__commands.push(command) + + def setNodeChanged(self, node): + """ + Public method to signal changes of bookmarks other than title, URL + or timestamp. + + @param node reference to the bookmark (BookmarkNode) + """ + self.__saveTimer.changeOccurred() + + def setTimestamp(self, node, timestampType, timestamp): + """ + Public method to set the URL of a bookmark. + + @param node reference to the node to be changed (BookmarkNode) + @param timestampType type of the timestamp to set + (BookmarkNode.TsAdded, BookmarkNode.TsModified, + BookmarkNode.TsVisited) + @param timestamp timestamp to set (QDateTime) + """ + if not self.__loaded: + return + + assert timestampType in [BookmarkNode.TsAdded, + BookmarkNode.TsModified, + BookmarkNode.TsVisited] + + if timestampType == BookmarkNode.TsAdded: + node.added = timestamp + elif timestampType == BookmarkNode.TsModified: + node.modified = timestamp + elif timestampType == BookmarkNode.TsVisited: + node.visited = timestamp + self.__saveTimer.changeOccurred() + + def incVisitCount(self, node): + """ + Public method to increment the visit count of a bookmark. + + @param node reference to the node to be changed (BookmarkNode) + """ + if not self.__loaded: + return + + if node: + node.visitCount += 1 + self.__saveTimer.changeOccurred() + + def setVisitCount(self, node, count): + """ + Public method to set the visit count of a bookmark. + + @param node reference to the node to be changed (BookmarkNode) + @param count visit count to be set (int or str) + """ + try: + node.visitCount = int(count) + self.__saveTimer.changeOccurred() + except ValueError: + # ignore invalid values + pass + + def bookmarks(self): + """ + Public method to get a reference to the root bookmark node. + + @return reference to the root bookmark node (BookmarkNode) + """ + if not self.__loaded: + self.load() + + return self.__bookmarkRootNode + + def menu(self): + """ + Public method to get a reference to the bookmarks menu node. + + @return reference to the bookmarks menu node (BookmarkNode) + """ + if not self.__loaded: + self.load() + + return self.__menu + + def toolbar(self): + """ + Public method to get a reference to the bookmarks toolbar node. + + @return reference to the bookmarks toolbar node (BookmarkNode) + """ + if not self.__loaded: + self.load() + + return self.__toolbar + + def bookmarksModel(self): + """ + Public method to get a reference to the bookmarks model. + + @return reference to the bookmarks model (BookmarksModel) + """ + if self.__bookmarksModel is None: + from .BookmarksModel import BookmarksModel + self.__bookmarksModel = BookmarksModel(self, self) + return self.__bookmarksModel + + def importBookmarks(self): + """ + Public method to import bookmarks. + """ + from .BookmarksImportDialog import BookmarksImportDialog + dlg = BookmarksImportDialog() + if dlg.exec_() == QDialog.Accepted: + importRootNode = dlg.getImportedBookmarks() + if importRootNode is not None: + self.addBookmark(self.menu(), importRootNode) + + def exportBookmarks(self): + """ + Public method to export the bookmarks. + """ + fileName, selectedFilter = E5FileDialog.getSaveFileNameAndFilter( + None, + self.tr("Export Bookmarks"), + "eric6_bookmarks.xbel", + self.tr("XBEL bookmarks (*.xbel);;" + "XBEL bookmarks (*.xml);;" + "HTML Bookmarks (*.html)")) + if not fileName: + return + + ext = QFileInfo(fileName).suffix() + if not ext: + ex = selectedFilter.split("(*")[1].split(")")[0] + if ex: + fileName += ex + + ext = QFileInfo(fileName).suffix() + if ext == "html": + from .NsHtmlWriter import NsHtmlWriter + writer = NsHtmlWriter() + else: + from .XbelWriter import XbelWriter + writer = XbelWriter() + if not writer.write(fileName, self.__bookmarkRootNode): + E5MessageBox.critical( + None, + self.tr("Exporting Bookmarks"), + self.tr("""Error exporting bookmarks to <b>{0}</b>.""") + .format(fileName)) + + def faviconChanged(self, url): + """ + Public slot to update the icon image for an URL. + + @param url URL of the icon to update (QUrl or string) + """ + if isinstance(url, QUrl): + url = url.toString() + nodes = self.bookmarksForUrl(url) + for node in nodes: + self.bookmarksModel().entryChanged(node) + + def bookmarkForUrl(self, url, start=StartRoot): + """ + Public method to get a bookmark node for a given URL. + + @param url URL of the bookmark to search for (QUrl or string) + @keyparam start indicator for the start of the search + (StartRoot, StartMenu, StartToolBar) + @return bookmark node for the given url (BookmarkNode) + """ + if start == StartMenu: + startNode = self.__menu + elif start == StartToolBar: + startNode = self.__toolbar + else: + startNode = self.__bookmarkRootNode + if startNode is None: + return None + + if isinstance(url, QUrl): + url = url.toString() + + return self.__searchBookmark(url, startNode) + + def __searchBookmark(self, url, startNode): + """ + Private method get a bookmark node for a given URL. + + @param url URL of the bookmark to search for (string) + @param startNode reference to the node to start searching + (BookmarkNode) + @return bookmark node for the given url (BookmarkNode) + """ + bm = None + for node in startNode.children(): + if node.type() == BookmarkNode.Folder: + bm = self.__searchBookmark(url, node) + elif node.type() == BookmarkNode.Bookmark: + if node.url == url: + bm = node + if bm is not None: + return bm + return None + + def bookmarksForUrl(self, url, start=StartRoot): + """ + Public method to get a list of bookmark nodes for a given URL. + + @param url URL of the bookmarks to search for (QUrl or string) + @keyparam start indicator for the start of the search + (StartRoot, StartMenu, StartToolBar) + @return list of bookmark nodes for the given url (list of BookmarkNode) + """ + if start == StartMenu: + startNode = self.__menu + elif start == StartToolBar: + startNode = self.__toolbar + else: + startNode = self.__bookmarkRootNode + if startNode is None: + return [] + + if isinstance(url, QUrl): + url = url.toString() + + return self.__searchBookmarks(url, startNode) + + def __searchBookmarks(self, url, startNode): + """ + Private method get a list of bookmark nodes for a given URL. + + @param url URL of the bookmarks to search for (string) + @param startNode reference to the node to start searching + (BookmarkNode) + @return list of bookmark nodes for the given url (list of BookmarkNode) + """ + bm = [] + for node in startNode.children(): + if node.type() == BookmarkNode.Folder: + bm.extend(self.__searchBookmarks(url, node)) + elif node.type() == BookmarkNode.Bookmark: + if node.url == url: + bm.append(node) + return bm + + +class RemoveBookmarksCommand(QUndoCommand): + """ + Class implementing the Remove undo command. + """ + def __init__(self, bookmarksManager, parent, row): + """ + Constructor + + @param bookmarksManager reference to the bookmarks manager + (BookmarksManager) + @param parent reference to the parent node (BookmarkNode) + @param row row number of bookmark (integer) + """ + super(RemoveBookmarksCommand, self).__init__( + QCoreApplication.translate("BookmarksManager", "Remove Bookmark")) + + self._row = row + self._bookmarksManager = bookmarksManager + try: + self._node = parent.children()[row] + except IndexError: + self._node = BookmarkNode() + self._parent = parent + + def undo(self): + """ + Public slot to perform the undo action. + """ + self._parent.add(self._node, self._row) + self._bookmarksManager.entryAdded.emit(self._node) + + def redo(self): + """ + Public slot to perform the redo action. + """ + self._parent.remove(self._node) + self._bookmarksManager.entryRemoved.emit( + self._parent, self._row, self._node) + + +class InsertBookmarksCommand(RemoveBookmarksCommand): + """ + Class implementing the Insert undo command. + """ + def __init__(self, bookmarksManager, parent, node, row): + """ + Constructor + + @param bookmarksManager reference to the bookmarks manager + (BookmarksManager) + @param parent reference to the parent node (BookmarkNode) + @param node reference to the node to be inserted (BookmarkNode) + @param row row number of bookmark (integer) + """ + RemoveBookmarksCommand.__init__(self, bookmarksManager, parent, row) + self.setText(QCoreApplication.translate( + "BookmarksManager", "Insert Bookmark")) + self._node = node + + def undo(self): + """ + Public slot to perform the undo action. + """ + RemoveBookmarksCommand.redo(self) + + def redo(self): + """ + Public slot to perform the redo action. + """ + RemoveBookmarksCommand.undo(self) + + +class ChangeBookmarkCommand(QUndoCommand): + """ + Class implementing the Insert undo command. + """ + def __init__(self, bookmarksManager, node, newValue, title): + """ + Constructor + + @param bookmarksManager reference to the bookmarks manager + (BookmarksManager) + @param node reference to the node to be changed (BookmarkNode) + @param newValue new value to be set (string) + @param title flag indicating a change of the title (True) or + the URL (False) (boolean) + """ + super(ChangeBookmarkCommand, self).__init__() + + self._bookmarksManager = bookmarksManager + self._title = title + self._newValue = newValue + self._node = node + + if self._title: + self._oldValue = self._node.title + self.setText(QCoreApplication.translate( + "BookmarksManager", "Name Change")) + else: + self._oldValue = self._node.url + self.setText(QCoreApplication.translate( + "BookmarksManager", "Address Change")) + + def undo(self): + """ + Public slot to perform the undo action. + """ + if self._title: + self._node.title = self._oldValue + else: + self._node.url = self._oldValue + self._bookmarksManager.entryChanged.emit(self._node) + + def redo(self): + """ + Public slot to perform the redo action. + """ + if self._title: + self._node.title = self._newValue + else: + self._node.url = self._newValue + self._bookmarksManager.entryChanged.emit(self._node)