--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/HelpViewer/HelpBookmarksWidget.py Thu Jul 07 11:23:56 2022 +0200 @@ -0,0 +1,534 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a widget showing the list of bookmarks. +""" + +import contextlib +import datetime +import json +import os + +from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QPoint, QUrl +from PyQt6.QtGui import QClipboard, QGuiApplication +from PyQt6.QtWidgets import ( + QAbstractItemView, QApplication, QDialog, QListWidget, QListWidgetItem, + QMenu +) + +from EricWidgets import EricFileDialog, EricMessageBox + +import Preferences + +from .HelpBookmarkPropertiesDialog import HelpBookmarkPropertiesDialog + + +class HelpBookmarksWidget(QListWidget): + """ + Class implementing a widget showing the list of bookmarks. + + @signal escapePressed() emitted when the ESC key was pressed + @signal openUrl(QUrl, str) emitted to open an entry in the current tab + @signal newTab(QUrl, str) emitted to open an entry in a new tab + @signal newBackgroundTab(QUrl, str) emitted to open an entry in a + new background tab + """ + escapePressed = pyqtSignal() + openUrl = pyqtSignal(QUrl) + newTab = pyqtSignal(QUrl) + newBackgroundTab = pyqtSignal(QUrl) + + UrlRole = Qt.ItemDataRole.UserRole + 1 + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (defaults to None) + @type QWidget (optional) + """ + super().__init__(parent) + self.setObjectName("HelpBookmarksWidget") + + self.__helpViewer = parent + + self.setAlternatingRowColors(True) + self.setSelectionMode( + QAbstractItemView.SelectionMode.ExtendedSelection) + self.setSortingEnabled(True) + + self.setContextMenuPolicy( + Qt.ContextMenuPolicy.CustomContextMenu) + self.customContextMenuRequested.connect( + self.__showContextMenu) + + self.__bookmarks = [] + self.__loadBookmarks() + + self.itemDoubleClicked.connect(self.__bookmarkActivated) + + @pyqtSlot(QPoint) + def __showContextMenu(self, point): + """ + Private slot to handle the customContextMenuRequested signal of + the viewlist. + + @param point position to open the menu at + @type QPoint + """ + selectedItemsCount = len(self.selectedItems()) + if selectedItemsCount == 0: + # background menu + self.__showBackgroundMenu(point) + elif selectedItemsCount == 1: + # single bookmark menu + self.__showBookmarkContextMenu(point) + else: + # multiple selected bookmarks + self.__showBookmarksContextMenu(point) + + @pyqtSlot(QPoint) + def __showBackgroundMenu(self, point): + """ + Private slot to show the background menu (i.e. no selection). + + @param point position to open the menu at + @type QPoint + """ + menu = QMenu() + openBookmarks = menu.addAction(self.tr("Open All Bookmarks")) + menu.addSeparator() + newBookmark = menu.addAction(self.tr("New Bookmark")) + addBookmark = menu.addAction(self.tr("Bookmark Page")) + menu.addSeparator() + deleteBookmarks = menu.addAction(self.tr("Delete All Bookmarks")) + menu.addSeparator() + exportBookmarks = menu.addAction(self.tr("Export All Bookmarks")) + importBookmarks = menu.addAction(self.tr("Import Bookmarks")) + + act = menu.exec(self.mapToGlobal(point)) + if act == openBookmarks: + self.__openBookmarks(selected=False) + elif act == newBookmark: + self.__newBookmark() + elif act == addBookmark: + self.__bookmarkCurrentPage() + elif act == deleteBookmarks: + self.__deleteBookmarks([ + self.item(row) for row in range(self.count()) + ]) + elif act == exportBookmarks: + self.__exportBookmarks(selected=False) + elif act == importBookmarks: + self.__importBookmarks() + + @pyqtSlot(QPoint) + def __showBookmarkContextMenu(self, point): + """ + Private slot to show the context menu for a bookmark. + + @param point position to open the menu at + @type QPoint + """ + itm = self.selectedItems()[0] + url = itm.data(self.UrlRole) + validUrl = ( + url is not None and not url.isEmpty() and url.isValid() + ) + + menu = QMenu() + curPage = menu.addAction(self.tr("Open Link")) + curPage.setEnabled(validUrl) + newPage = menu.addAction(self.tr("Open Link in New Page")) + newPage.setEnabled(validUrl) + newBackgroundPage = menu.addAction( + self.tr("Open Link in Background Page")) + newBackgroundPage.setEnabled(validUrl) + menu.addSeparator() + copyUrl = menu.addAction(self.tr("Copy URL to Clipboard")) + copyUrl.setEnabled(validUrl) + menu.addSeparator() + newBookmark = menu.addAction(self.tr("New Bookmark")) + addBookmark = menu.addAction(self.tr("Bookmark Page")) + menu.addSeparator() + editBookmark = menu.addAction(self.tr("Edit Bookmark")) + menu.addSeparator() + deleteBookmark = menu.addAction(self.tr("Delete Bookmark")) + menu.addSeparator() + exportBookmarks = menu.addAction(self.tr("Export All Bookmarks")) + importBookmarks = menu.addAction(self.tr("Import Bookmarks")) + + act = menu.exec(self.mapToGlobal(point)) + if act == curPage: + self.openUrl.emit(url) + elif act == newPage: + self.newTab.emit(url) + elif act == newBackgroundPage: + self.newBackgroundTab.emit(url) + elif act == copyUrl: + # copy the URL to both clipboard areas + QGuiApplication.clipboard().setText( + url.toString(), QClipboard.Mode.Clipboard) + QGuiApplication.clipboard().setText( + url.toString(), QClipboard.Mode.Selection) + elif act == newBookmark: + self.__newBookmark() + elif act == addBookmark: + self.__bookmarkCurrentPage() + elif act == editBookmark: + self.__editBookmark(itm) + elif act == deleteBookmark: + self.__deleteBookmarks([itm]) + elif act == exportBookmarks: + self.__exportBookmarks(selected=False) + elif act == importBookmarks: + self.__importBookmarks() + + @pyqtSlot(QPoint) + def __showBookmarksContextMenu(self, point): + """ + Private slot to show the context menu for multiple bookmark. + + @param point position to open the menu at + @type QPoint + """ + menu = QMenu() + openBookmarks = menu.addAction(self.tr("Open Selected Bookmarks")) + menu.addSeparator() + deleteBookmarks = menu.addAction(self.tr("Delete Selected Bookmarks")) + menu.addSeparator() + exportBookmarks = menu.addAction(self.tr("Export Selected Bookmarks")) + exportAllBookmarks = menu.addAction(self.tr("Export All Bookmarks")) + importBookmarks = menu.addAction(self.tr("Import Bookmarks")) + + act = menu.exec(self.mapToGlobal(point)) + if act == openBookmarks: + self.__openBookmarks(selected=True) + elif act == deleteBookmarks: + self.__deleteBookmarks(self.selectedItems()) + elif act == exportBookmarks: + self.__exportBookmarks(selected=True) + elif act == exportAllBookmarks: + self.__exportBookmarks(selected=False) + elif act == importBookmarks: + self.__importBookmarks() + + @pyqtSlot(str, str) + def __addBookmark(self, title, url): + """ + Private slot to add a bookmark entry. + + @param title title for the bookmark + @type str + @param url URL for the bookmark + @type str + """ + url = url.strip() + + itm = QListWidgetItem(title, self) + itm.setData(self.UrlRole, QUrl(url)) + itm.setToolTip(url) + + @pyqtSlot(str, QUrl) + def addBookmark(self, title, url): + """ + Public slot to add a bookmark with given data. + + @param title title for the bookmark + @type str + @param url URL for the bookmark + @type QUrl + """ + dlg = HelpBookmarkPropertiesDialog(title, url.toString(), self) + if dlg.exec() == QDialog.DialogCode.Accepted: + title, url = dlg.getData() + self.__addBookmark(title, url) + self.sortItems() + self.__saveBookmarks() + + @pyqtSlot() + def __bookmarkCurrentPage(self): + """ + Private slot to bookmark the current page. + """ + currentViewer = self.__helpViewer.currentViewer() + title = currentViewer.pageTitle() + url = currentViewer.link() + self.addBookmark(title, url) + + @pyqtSlot() + def __newBookmark(self): + """ + Private slot to create a new bookmark. + """ + dlg = HelpBookmarkPropertiesDialog(parent=self) + if dlg.exec() == QDialog.DialogCode.Accepted: + title, url = dlg.getData() + self.__addBookmark(title, url) + self.sortItems() + self.__saveBookmarks() + + @pyqtSlot() + def __editBookmark(self, itm): + """ + Private slot to edit a bookmark. + + @param itm reference to the bookmark item to be edited + @type QListWidgetItem + """ + dlg = HelpBookmarkPropertiesDialog( + itm.text(), itm.data(self.UrlRole).toString(), self) + if dlg.exec() == QDialog.DialogCode.Accepted: + title, url = dlg.getData() + itm.setText(title) + itm.setData(self.UrlRole, QUrl(url)) + itm.setToolTip(url) + self.sortItems() + self.__saveBookmarks() + + @pyqtSlot(QListWidgetItem) + def __bookmarkActivated(self, itm): + """ + Private slot handling the activation of a bookmark. + + @param itm reference to the activated item + @type QListWidgetItem + """ + url = itm.data(self.UrlRole) + if url and not url.isEmpty() and url.isValid(): + buttons = QApplication.mouseButtons() + modifiers = QApplication.keyboardModifiers() + + if buttons & Qt.MouseButton.MiddleButton: + self.newTab.emit(url) + else: + if ( + modifiers & ( + Qt.KeyboardModifier.ControlModifier | + Qt.KeyboardModifier.ShiftModifier + ) == ( + Qt.KeyboardModifier.ControlModifier | + Qt.KeyboardModifier.ShiftModifier + ) + ): + self.newBackgroundTab.emit(url) + elif modifiers & Qt.KeyboardModifier.ControlModifier: + self.newTab.emit(url) + elif ( + modifiers & Qt.KeyboardModifier.ShiftModifier and + not self.__internal + ): + self.newWindow.emit(url) + else: + self.openUrl.emit(url) + + def __openBookmarks(self, selected=False): + """ + Private method to open all or selected bookmarks. + + @param selected flag indicating to open the selected bookmarks + (defaults to False) + @type bool (optional) + """ + items = ( + self.selectedItems() + if selected else + [self.item(row) for row in range(self.count())] + ) + + for itm in items: + url = itm.data(self.UrlRole) + if url is not None and not url.isEmpty() and url.isValid(): + self.newTab.emit(url) + + def __deleteBookmarks(self, items): + """ + Private method to delete the given bookmark items. + + @param items list of bookmarks to be deleted + @type list of QListWidgetItem + """ + from UI.DeleteFilesConfirmationDialog import ( + DeleteFilesConfirmationDialog + ) + dlg = DeleteFilesConfirmationDialog( + self, + self.tr("Delete Bookmarks"), + self.tr("Shall these bookmarks really be deleted?"), + [itm.text() for itm in items] + ) + if dlg.exec() == QDialog.DialogCode.Accepted: + for itm in items: + self.takeItem(self.row(itm)) + del itm + self.__saveBookmarks() + + def __loadBookmarks(self): + """ + Private method to load the defined bookmarks. + """ + bookmarksStr = Preferences.getHelp("Bookmarks") + with contextlib.suppress(ValueError): + bookmarks = json.loads(bookmarksStr) + + self.clear() + for bookmark in bookmarks: + self.__addBookmark(bookmark["title"], bookmark["url"]) + self.sortItems() + + def __saveBookmarks(self): + """ + Private method to save the defined bookmarks. + """ + bookmarks = [] + for row in range(self.count()): + itm = self.item(row) + bookmarks.append({ + "title": itm.text(), + "url": itm.data(self.UrlRole).toString(), + }) + Preferences.setHelp("Bookmarks", json.dumps(bookmarks)) + + @pyqtSlot() + def __exportBookmarks(self, selected=False): + """ + Private slot to export the bookmarks into a JSON file. + + @param selected flag indicating to export the selected bookmarks + (defaults to False) + @type bool (optional) + """ + filename, selectedFilter = EricFileDialog.getSaveFileNameAndFilter( + self, + self.tr("Export Bookmarks"), + "", + self.tr("eric Bookmarks Files (*.json);;All Files (*)"), + None, + EricFileDialog.DontConfirmOverwrite + ) + if filename: + ext = os.path.splitext(filename)[1] + if not ext: + ex = selectedFilter.split("(*")[1].split(")")[0] + if ex: + filename += ex + + if os.path.exists(filename): + ok = EricMessageBox.yesNo( + self, + self.tr("Export Bookmarks"), + self.tr("""The file <b>{0}</b> already exists. Do you""" + """ want to overwrite it?""").format(filename)) + if not ok: + return + + bookmarksDict = { + "creator": "eric7", + "version": 1, + "created": datetime.datetime.now().isoformat( + sep=" ", timespec="seconds"), + "bookmarks": [] + } + bookmarkItems = ( + self.selectedItems() + if selected else + [self.item(row) for row in range(self.count())] + ) + for bookmarkItem in bookmarkItems: + bookmarksDict["bookmarks"].append({ + "type": "url", + "title": bookmarkItem.text(), + "url": bookmarkItem.data(self.UrlRole).toString(), + }) + + jsonStr = json.dumps(bookmarksDict, indent=2, sort_keys=True) + try: + with open(filename, "w") as f: + f.write(jsonStr) + except OSError as err: + EricMessageBox.critical( + self, + self.tr("Export Bookmarks"), + self.tr("""<p>The bookmarks could not be exported""" + """ to <b>{0}</b>.</p><p>Reason: {1}</p>""") + .format(filename, str(err))) + + @pyqtSlot() + def __importBookmarks(self): + """ + Private slot to import bookmarks from a JSON file. + """ + from .HelpBookmarksImportDialog import HelpBookmarksImportDialog + + dlg = HelpBookmarksImportDialog(self) + if dlg.exec() == QDialog.DialogCode.Accepted: + replace, filename = dlg.getData() + + try: + with open(filename, "r") as f: + jsonStr = f.read() + bookmarks = json.loads(jsonStr) + except (OSError, json.JSONDecodeError) as err: + EricMessageBox.critical( + self, + self.tr("Import Bookmarks"), + self.tr( + "<p>The bookmarks file <b>{0}</b> could not be " + "read.</p><p>Reason: {1}</p>" + ).format(filename, str(err)) + ) + return + + if not isinstance(bookmarks, dict): + EricMessageBox.critical( + self, + self.tr("Import Bookmarks"), + self.tr( + "The bookmarks file <b>{0}</b> has invalid contents." + ).format(filename) + ) + return + + try: + if bookmarks["creator"] != "eric7": + EricMessageBox.critical( + self, + self.tr("Import Bookmarks"), + self.tr( + "The bookmarks file <b>{0}</b> was not created" + " with 'eric7'." + ).format(filename) + ) + return + + if bookmarks["version"] != 1: + EricMessageBox.critical( + self, + self.tr("Import Bookmarks"), + self.tr( + "The bookmarks file <b>{0}</b> has an unsupported" + " format version." + ).format(filename) + ) + return + + if replace: + self.clear() + + for bookmark in bookmarks["bookmarks"]: + if bookmark["type"] == "url": + self.__addBookmark(bookmark["title"], bookmark["url"]) + self.sortItems() + self.__saveBookmarks() + + except KeyError: + EricMessageBox.critical( + self, + self.tr("Import Bookmarks"), + self.tr( + "The bookmarks file <b>{0}</b> has invalid contents." + ).format(filename) + )