src/eric7/HelpViewer/HelpBookmarksWidget.py

Thu, 07 Jul 2022 11:23:56 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Thu, 07 Jul 2022 11:23:56 +0200
branch
eric7
changeset 9209
b99e7fd55fd3
parent 8902
eric7/HelpViewer/HelpBookmarksWidget.py@ba9b8c6e4928
child 9221
bf71ee032bb4
permissions
-rw-r--r--

Reorganized the project structure to use the source layout in order to support up-to-date build systems with "pyproject.toml".

# -*- 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)
                )

eric ide

mercurial