1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2009 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing a window for showing the QtHelp index.
8"""
9
10from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QUrl, QEvent
11from PyQt5.QtWidgets import (
12    QWidget, QVBoxLayout, QLabel, QLineEdit, QMenu, QDialog, QApplication
13)
14
15
16class HelpIndexWidget(QWidget):
17    """
18    Class implementing a window for showing the QtHelp index.
19
20    @signal escapePressed() emitted when the ESC key was pressed
21    @signal openUrl(QUrl, str) emitted to open an entry in the current tab
22    @signal newTab(QUrl, str) emitted to open an entry in a new tab
23    @signal newBackgroundTab(QUrl, str) emitted to open an entry in a
24        new background tab
25    @signal newWindow(QUrl, str) emitted to open an entry in a new window
26    """
27    escapePressed = pyqtSignal()
28    openUrl = pyqtSignal(QUrl)
29    newTab = pyqtSignal(QUrl)
30    newBackgroundTab = pyqtSignal(QUrl)
31    newWindow = pyqtSignal(QUrl)
32
33    def __init__(self, engine, parent=None):
34        """
35        Constructor
36
37        @param engine reference to the help engine (QHelpEngine)
38        @param parent reference to the parent widget (QWidget)
39        """
40        super().__init__(parent)
41
42        self.__engine = engine
43
44        self.__searchEdit = None
45        self.__index = None
46
47        self.__layout = QVBoxLayout(self)
48        label = QLabel(self.tr("&Look for:"))
49        self.__layout.addWidget(label)
50
51        self.__searchEdit = QLineEdit()
52        label.setBuddy(self.__searchEdit)
53        self.__searchEdit.textChanged.connect(self.__filterIndices)
54        self.__searchEdit.installEventFilter(self)
55        self.__layout.addWidget(self.__searchEdit)
56
57        self.__index = self.__engine.indexWidget()
58        self.__index.setContextMenuPolicy(
59            Qt.ContextMenuPolicy.CustomContextMenu)
60
61        self.__engine.indexModel().indexCreationStarted.connect(
62            self.__disableSearchEdit)
63        self.__engine.indexModel().indexCreated.connect(
64            self.__enableSearchEdit)
65        self.__index.linkActivated.connect(self.__linkActivated)
66        self.__index.linksActivated.connect(self.__linksActivated)
67        self.__index.customContextMenuRequested.connect(
68            self.__showContextMenu)
69        self.__searchEdit.returnPressed.connect(
70            self.__index.activateCurrentItem)
71        self.__layout.addWidget(self.__index)
72
73    @pyqtSlot(QUrl, str)
74    def __linkActivated(self, url, keyword, modifiers=None):
75        """
76        Private slot to handle the activation of a keyword entry.
77
78        @param url URL of the selected entry
79        @type QUrl
80        @param keyword keyword for the URL
81        @type str
82        @param modifiers keyboard modifiers
83        @type Qt.KeyboardModifiers or None
84        """
85        if modifiers is None:
86            modifiers = QApplication.keyboardModifiers()
87        if not url.isEmpty() and url.isValid():
88            if (
89                modifiers & (
90                    Qt.KeyboardModifier.ControlModifier |
91                    Qt.KeyboardModifier.ShiftModifier
92                ) == (
93                    Qt.KeyboardModifier.ControlModifier |
94                    Qt.KeyboardModifier.ShiftModifier
95                )
96            ):
97                self.newBackgroundTab.emit(url)
98            elif modifiers & Qt.KeyboardModifier.ControlModifier:
99                self.newTab.emit(url)
100            elif modifiers & Qt.KeyboardModifier.ShiftModifier:
101                self.newWindow.emit(url)
102            else:
103                self.openUrl.emit(url)
104
105    def __linksActivated(self, links, keyword):
106        """
107        Private slot to handle the activation of an entry with multiple links.
108
109        @param links dictionary containing the links
110        @type dict of key:str and value:QUrl
111        @param keyword keyword for the entry
112        @type str
113        """
114        modifiers = QApplication.keyboardModifiers()
115        url = (
116            QUrl(links[list(links.keys())[0]])
117            if len(links) == 1 else
118            self.__selectLink(links, keyword)
119        )
120        self.__linkActivated(url, keyword, modifiers)
121
122    def __selectLink(self, links, keyword):
123        """
124        Private method to give the user a chance to select among the
125        returned links.
126
127        @param links dictionary of document title and URL to select from
128        @type dictionary of str (key) and QUrl (value)
129        @param keyword keyword for the link set
130        @type str
131        @return selected link
132        @rtype QUrl
133        """
134        link = QUrl()
135        from .HelpTopicDialog import HelpTopicDialog
136        dlg = HelpTopicDialog(self, keyword, links)
137        if dlg.exec() == QDialog.DialogCode.Accepted:
138            link = dlg.link()
139        return link
140
141    def __filterIndices(self, indexFilter):
142        """
143        Private slot to filter the indexes according to the given filter.
144
145        @param indexFilter filter to be used (string)
146        """
147        if '*' in indexFilter:
148            self.__index.filterIndices(indexFilter, indexFilter)
149        else:
150            self.__index.filterIndices(indexFilter)
151
152    def __enableSearchEdit(self):
153        """
154        Private slot to enable the search edit.
155        """
156        self.__searchEdit.setEnabled(True)
157        self.__filterIndices(self.__searchEdit.text())
158
159    def __disableSearchEdit(self):
160        """
161        Private slot to enable the search edit.
162        """
163        self.__searchEdit.setEnabled(False)
164
165    def focusInEvent(self, evt):
166        """
167        Protected method handling focus in events.
168
169        @param evt reference to the focus event object (QFocusEvent)
170        """
171        if evt.reason() != Qt.FocusReason.MouseFocusReason:
172            self.__searchEdit.selectAll()
173            self.__searchEdit.setFocus()
174
175    def eventFilter(self, watched, event):
176        """
177        Public method called to filter the event queue.
178
179        @param watched the QObject being watched (QObject)
180        @param event the event that occurred (QEvent)
181        @return flag indicating whether the event was handled (boolean)
182        """
183        if (
184            self.__searchEdit and watched == self.__searchEdit and
185            event.type() == QEvent.Type.KeyPress
186        ):
187            idx = self.__index.currentIndex()
188            if event.key() == Qt.Key.Key_Up:
189                idx = self.__index.model().index(
190                    idx.row() - 1, idx.column(), idx.parent())
191                if idx.isValid():
192                    self.__index.setCurrentIndex(idx)
193            elif event.key() == Qt.Key.Key_Down:
194                idx = self.__index.model().index(
195                    idx.row() + 1, idx.column(), idx.parent())
196                if idx.isValid():
197                    self.__index.setCurrentIndex(idx)
198            elif event.key() == Qt.Key.Key_Escape:
199                self.escapePressed.emit()
200
201        return QWidget.eventFilter(self, watched, event)
202
203    def __showContextMenu(self, pos):
204        """
205        Private slot showing the context menu.
206
207        @param pos position to show the menu at (QPoint)
208        """
209        idx = self.__index.indexAt(pos)
210        if idx.isValid():
211            menu = QMenu()
212            curTab = menu.addAction(self.tr("Open Link"))
213            newTab = menu.addAction(self.tr("Open Link in New Tab"))
214            newBackgroundTab = menu.addAction(
215                self.tr("Open Link in Background Tab"))
216            newWindow = menu.addAction(self.tr("Open Link in New Window"))
217            menu.move(self.__index.mapToGlobal(pos))
218
219            act = menu.exec()
220            model = self.__index.model()
221            if model is not None:
222                keyword = model.data(idx, Qt.ItemDataRole.DisplayRole)
223                links = model.linksForKeyword(keyword)
224                if len(links) == 1:
225                    link = QUrl(links[list(links.keys())[0]])
226                else:
227                    link = self.__selectLink(links, keyword)
228
229                if not link.isEmpty() and link.isValid():
230                    if act == curTab:
231                        self.openUrl.emit(link)
232                    elif act == newTab:
233                        self.newTab.emit(link)
234                    elif act == newBackgroundTab:
235                        self.newBackgroundTab.emit(link)
236                    elif act == newWindow:
237                        self.newWindow.emit(link)
238