--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/HelpViewer/HelpBookmarksWidget.py Tue Jan 04 17:13:35 2022 +0100 @@ -0,0 +1,374 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +import contextlib +import json + +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 +) + +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 Bookmark")) + + 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()) + ]) + + @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")) + + 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]) + + @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")) + + act = menu.exec(self.mapToGlobal(point)) + if act == openBookmarks: + self.__openBookmarks(selected=True) + elif act == deleteBookmarks: + self.__deleteBookmarks(self.selectedItems()) + + @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)) + + def __exportBookmarks(self): + """ + Private method to export the bookmarks into a JSON file. + """ + # TODO: not yet implemented + + def __importBookmarks(self): + """ + Private method to import bookmarks from a JSON file. + """ + # TODO: not yet implemented + # 1. read file + # 2. check, if exported from eric and compatible format version + # 3. process each entry