1# -*- coding: utf-8 -*- 2 3# Copyright (c) 2009 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> 4# 5 6""" 7Module implementing a web search widget for the web browser. 8""" 9 10from PyQt5.QtCore import pyqtSignal, QUrl, QModelIndex, QTimer, Qt 11from PyQt5.QtGui import ( 12 QStandardItem, QStandardItemModel, QFont, QIcon, QPixmap 13) 14from PyQt5.QtWidgets import QMenu, QCompleter 15from PyQt5.QtWebEngineWidgets import QWebEnginePage 16 17import UI.PixmapCache 18 19import Preferences 20 21from E5Gui.E5LineEdit import E5ClearableLineEdit, E5LineEditSide 22 23from .WebBrowserPage import WebBrowserPage 24 25 26class WebBrowserWebSearchWidget(E5ClearableLineEdit): 27 """ 28 Class implementing a web search widget for the web browser. 29 30 @signal search(QUrl) emitted when the search should be done 31 """ 32 search = pyqtSignal(QUrl) 33 34 def __init__(self, mainWindow, parent=None): 35 """ 36 Constructor 37 38 @param mainWindow reference to the browser main window 39 @type WebBrowserWindow 40 @param parent reference to the parent widget 41 @type QWidget 42 """ 43 super().__init__(parent) 44 45 from E5Gui.E5LineEditButton import E5LineEditButton 46 from .OpenSearch.OpenSearchManager import OpenSearchManager 47 48 self.__mw = mainWindow 49 50 self.__openSearchManager = OpenSearchManager(self) 51 self.__openSearchManager.currentEngineChanged.connect( 52 self.__currentEngineChanged) 53 self.__currentEngine = "" 54 55 self.__enginesMenu = QMenu(self) 56 self.__enginesMenu.triggered.connect( 57 self.__handleEnginesMenuActionTriggered) 58 59 self.__engineButton = E5LineEditButton(self) 60 self.__engineButton.setMenu(self.__enginesMenu) 61 self.addWidget(self.__engineButton, E5LineEditSide.LEFT) 62 63 self.__searchButton = E5LineEditButton(self) 64 self.__searchButton.setIcon(UI.PixmapCache.getIcon("webSearch")) 65 self.addWidget(self.__searchButton, E5LineEditSide.LEFT) 66 67 self.__model = QStandardItemModel(self) 68 self.__completer = QCompleter() 69 self.__completer.setModel(self.__model) 70 self.__completer.setCompletionMode( 71 QCompleter.CompletionMode.UnfilteredPopupCompletion) 72 self.__completer.setWidget(self) 73 74 self.__searchButton.clicked.connect(self.__searchButtonClicked) 75 self.textEdited.connect(self.__textEdited) 76 self.returnPressed.connect(self.__searchNow) 77 self.__completer.activated[QModelIndex].connect( 78 self.__completerActivated) 79 self.__completer.highlighted[QModelIndex].connect( 80 self.__completerHighlighted) 81 self.__enginesMenu.aboutToShow.connect(self.__showEnginesMenu) 82 83 self.__suggestionsItem = None 84 self.__suggestions = [] 85 self.__suggestTimer = None 86 self.__suggestionsEnabled = Preferences.getWebBrowser( 87 "WebSearchSuggestions") 88 89 self.__recentSearchesItem = None 90 self.__recentSearches = [] 91 self.__maxSavedSearches = 10 92 93 self.__engine = None 94 self.__loadSearches() 95 self.__setupCompleterMenu() 96 self.__currentEngineChanged() 97 98 def __searchNow(self): 99 """ 100 Private slot to perform the web search. 101 """ 102 searchText = self.text() 103 if not searchText: 104 return 105 106 import WebBrowser.WebBrowserWindow 107 if WebBrowser.WebBrowserWindow.WebBrowserWindow.isPrivate(): 108 return 109 110 if searchText in self.__recentSearches: 111 self.__recentSearches.remove(searchText) 112 self.__recentSearches.insert(0, searchText) 113 if len(self.__recentSearches) > self.__maxSavedSearches: 114 self.__recentSearches = self.__recentSearches[ 115 :self.__maxSavedSearches] 116 self.__setupCompleterMenu() 117 118 self.__mw.currentBrowser().setFocus() 119 self.__mw.currentBrowser().load( 120 self.__openSearchManager.currentEngine().searchUrl(searchText)) 121 122 def __setupCompleterMenu(self): 123 """ 124 Private method to create the completer menu. 125 """ 126 if ( 127 not self.__suggestions or 128 (self.__model.rowCount() > 0 and 129 self.__model.item(0) != self.__suggestionsItem) 130 ): 131 self.__model.clear() 132 self.__suggestionsItem = None 133 else: 134 self.__model.removeRows(1, self.__model.rowCount() - 1) 135 136 boldFont = QFont() 137 boldFont.setBold(True) 138 139 if self.__suggestions: 140 if self.__model.rowCount() == 0: 141 if not self.__suggestionsItem: 142 self.__suggestionsItem = QStandardItem( 143 self.tr("Suggestions")) 144 self.__suggestionsItem.setFont(boldFont) 145 self.__model.appendRow(self.__suggestionsItem) 146 147 for suggestion in self.__suggestions: 148 self.__model.appendRow(QStandardItem(suggestion)) 149 150 if not self.__recentSearches: 151 self.__recentSearchesItem = QStandardItem( 152 self.tr("No Recent Searches")) 153 self.__recentSearchesItem.setFont(boldFont) 154 self.__model.appendRow(self.__recentSearchesItem) 155 else: 156 self.__recentSearchesItem = QStandardItem( 157 self.tr("Recent Searches")) 158 self.__recentSearchesItem.setFont(boldFont) 159 self.__model.appendRow(self.__recentSearchesItem) 160 for recentSearch in self.__recentSearches: 161 self.__model.appendRow(QStandardItem(recentSearch)) 162 163 view = self.__completer.popup() 164 view.setFixedHeight(view.sizeHintForRow(0) * self.__model.rowCount() + 165 view.frameWidth() * 2) 166 167 self.__searchButton.setEnabled( 168 bool(self.__recentSearches or self.__suggestions)) 169 170 def __completerActivated(self, index): 171 """ 172 Private slot handling the selection of an entry from the completer. 173 174 @param index index of the item (QModelIndex) 175 """ 176 if ( 177 self.__suggestionsItem and 178 self.__suggestionsItem.index().row() == index.row() 179 ): 180 return 181 182 if ( 183 self.__recentSearchesItem and 184 self.__recentSearchesItem.index().row() == index.row() 185 ): 186 return 187 188 self.__searchNow() 189 190 def __completerHighlighted(self, index): 191 """ 192 Private slot handling the highlighting of an entry of the completer. 193 194 @param index index of the item (QModelIndex) 195 @return flah indicating a successful highlighting (boolean) 196 """ 197 if ( 198 self.__suggestionsItem and 199 self.__suggestionsItem.index().row() == index.row() 200 ): 201 return False 202 203 if ( 204 self.__recentSearchesItem and 205 self.__recentSearchesItem.index().row() == index.row() 206 ): 207 return False 208 209 self.setText(index.data()) 210 return True 211 212 def __textEdited(self, txt): 213 """ 214 Private slot to handle changes of the search text. 215 216 @param txt search text (string) 217 """ 218 if self.__suggestionsEnabled: 219 if self.__suggestTimer is None: 220 self.__suggestTimer = QTimer(self) 221 self.__suggestTimer.setSingleShot(True) 222 self.__suggestTimer.setInterval(200) 223 self.__suggestTimer.timeout.connect(self.__getSuggestions) 224 self.__suggestTimer.start() 225 else: 226 self.__completer.setCompletionPrefix(txt) 227 self.__completer.complete() 228 229 def __getSuggestions(self): 230 """ 231 Private slot to get search suggestions from the configured search 232 engine. 233 """ 234 searchText = self.text() 235 if searchText: 236 self.__openSearchManager.currentEngine().requestSuggestions( 237 searchText) 238 239 def __newSuggestions(self, suggestions): 240 """ 241 Private slot to receive a new list of suggestions. 242 243 @param suggestions list of suggestions (list of strings) 244 """ 245 self.__suggestions = suggestions 246 self.__setupCompleterMenu() 247 self.__completer.complete() 248 249 def __showEnginesMenu(self): 250 """ 251 Private slot to handle the display of the engines menu. 252 """ 253 self.__enginesMenu.clear() 254 255 from .OpenSearch.OpenSearchEngineAction import OpenSearchEngineAction 256 engineNames = self.__openSearchManager.allEnginesNames() 257 for engineName in engineNames: 258 engine = self.__openSearchManager.engine(engineName) 259 action = OpenSearchEngineAction(engine, self.__enginesMenu) 260 action.setData(engineName) 261 self.__enginesMenu.addAction(action) 262 263 if self.__openSearchManager.currentEngineName() == engineName: 264 action.setCheckable(True) 265 action.setChecked(True) 266 267 cb = self.__mw.currentBrowser() 268 from .Tools import Scripts 269 script = Scripts.getOpenSearchLinks() 270 cb.page().runJavaScript( 271 script, WebBrowserPage.SafeJsWorld, self.__showEnginesMenuCallback) 272 273 def __showEnginesMenuCallback(self, res): 274 """ 275 Private method handling the open search links callback. 276 277 @param res result of the JavaScript 278 @type list of dict 279 """ 280 cb = self.__mw.currentBrowser() 281 if res: 282 self.__enginesMenu.addSeparator() 283 for entry in res: 284 url = cb.url().resolved(QUrl(entry["url"])) 285 title = entry["title"] 286 if url.isEmpty(): 287 continue 288 if not title: 289 title = cb.title() 290 291 action = self.__enginesMenu.addAction( 292 self.tr("Add '{0}'").format(title)) 293 action.setData(url) 294 action.setIcon(cb.icon()) 295 296 self.__enginesMenu.addSeparator() 297 self.__enginesMenu.addAction(self.__mw.searchEnginesAction()) 298 299 if self.__recentSearches: 300 act = self.__enginesMenu.addAction( 301 self.tr("Clear Recent Searches")) 302 act.setData("@@CLEAR@@") 303 304 def __handleEnginesMenuActionTriggered(self, action): 305 """ 306 Private slot to handle an action of the menu being triggered. 307 308 @param action reference to the action that triggered 309 @type QAction 310 """ 311 actData = action.data() 312 if isinstance(actData, QUrl): 313 # add search engine 314 self.__openSearchManager.addEngine(actData) 315 elif isinstance(actData, str): 316 # engine name or special action 317 if actData == "@@CLEAR@@": 318 self.clear() 319 else: 320 self.__openSearchManager.setCurrentEngineName(actData) 321 322 def __searchButtonClicked(self): 323 """ 324 Private slot to show the search menu via the search button. 325 """ 326 self.__setupCompleterMenu() 327 self.__completer.complete() 328 329 def clear(self): 330 """ 331 Public method to clear all private data. 332 """ 333 self.__recentSearches = [] 334 self.__setupCompleterMenu() 335 super().clear() 336 self.clearFocus() 337 338 def preferencesChanged(self): 339 """ 340 Public method to handle the change of preferences. 341 """ 342 self.__suggestionsEnabled = Preferences.getWebBrowser( 343 "WebSearchSuggestions") 344 if not self.__suggestionsEnabled: 345 self.__suggestions = [] 346 self.__setupCompleterMenu() 347 348 def saveSearches(self): 349 """ 350 Public method to save the recently performed web searches. 351 """ 352 Preferences.Prefs.settings.setValue( 353 'WebBrowser/WebSearches', self.__recentSearches) 354 355 def __loadSearches(self): 356 """ 357 Private method to load the recently performed web searches. 358 """ 359 searches = Preferences.Prefs.settings.value('WebBrowser/WebSearches') 360 if searches is not None: 361 self.__recentSearches = searches 362 363 def openSearchManager(self): 364 """ 365 Public method to get a reference to the opensearch manager object. 366 367 @return reference to the opensearch manager object (OpenSearchManager) 368 """ 369 return self.__openSearchManager 370 371 def __currentEngineChanged(self): 372 """ 373 Private slot to track a change of the current search engine. 374 """ 375 if self.__openSearchManager.engineExists(self.__currentEngine): 376 oldEngine = self.__openSearchManager.engine(self.__currentEngine) 377 oldEngine.imageChanged.disconnect(self.__engineImageChanged) 378 if self.__suggestionsEnabled: 379 oldEngine.suggestions.disconnect(self.__newSuggestions) 380 381 newEngine = self.__openSearchManager.currentEngine() 382 if newEngine.networkAccessManager() is None: 383 newEngine.setNetworkAccessManager(self.__mw.networkManager()) 384 newEngine.imageChanged.connect(self.__engineImageChanged) 385 if self.__suggestionsEnabled: 386 newEngine.suggestions.connect(self.__newSuggestions) 387 388 self.setInactiveText(self.__openSearchManager.currentEngineName()) 389 self.__currentEngine = self.__openSearchManager.currentEngineName() 390 self.__engineButton.setIcon(QIcon(QPixmap.fromImage( 391 self.__openSearchManager.currentEngine().image()))) 392 self.__suggestions = [] 393 self.__setupCompleterMenu() 394 395 def __engineImageChanged(self): 396 """ 397 Private slot to handle a change of the current search engine icon. 398 """ 399 self.__engineButton.setIcon(QIcon(QPixmap.fromImage( 400 self.__openSearchManager.currentEngine().image()))) 401 402 def mousePressEvent(self, evt): 403 """ 404 Protected method called by a mouse press event. 405 406 @param evt reference to the mouse event (QMouseEvent) 407 """ 408 if evt.button() == Qt.MouseButton.XButton1: 409 self.__mw.currentBrowser().triggerPageAction( 410 QWebEnginePage.WebAction.Back) 411 elif evt.button() == Qt.MouseButton.XButton2: 412 self.__mw.currentBrowser().triggerPageAction( 413 QWebEnginePage.WebAction.Forward) 414 else: 415 super().mousePressEvent(evt) 416