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