diff -r 4e8b98454baa -r 800c432b34c8 eric7/WebBrowser/WebBrowserWebSearchWidget.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/WebBrowser/WebBrowserWebSearchWidget.py Sat May 15 18:45:04 2021 +0200 @@ -0,0 +1,415 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a web search widget for the web browser. +""" + +from PyQt5.QtCore import pyqtSignal, QUrl, QModelIndex, QTimer, Qt +from PyQt5.QtGui import ( + QStandardItem, QStandardItemModel, QFont, QIcon, QPixmap +) +from PyQt5.QtWidgets import QMenu, QCompleter +from PyQt5.QtWebEngineWidgets import QWebEnginePage + +import UI.PixmapCache + +import Preferences + +from E5Gui.E5LineEdit import E5ClearableLineEdit, E5LineEditSide + +from .WebBrowserPage import WebBrowserPage + + +class WebBrowserWebSearchWidget(E5ClearableLineEdit): + """ + Class implementing a web search widget for the web browser. + + @signal search(QUrl) emitted when the search should be done + """ + search = pyqtSignal(QUrl) + + def __init__(self, mainWindow, parent=None): + """ + Constructor + + @param mainWindow reference to the browser main window + @type WebBrowserWindow + @param parent reference to the parent widget + @type QWidget + """ + super().__init__(parent) + + from E5Gui.E5LineEditButton import E5LineEditButton + from .OpenSearch.OpenSearchManager import OpenSearchManager + + self.__mw = mainWindow + + self.__openSearchManager = OpenSearchManager(self) + self.__openSearchManager.currentEngineChanged.connect( + self.__currentEngineChanged) + self.__currentEngine = "" + + self.__enginesMenu = QMenu(self) + self.__enginesMenu.triggered.connect( + self.__handleEnginesMenuActionTriggered) + + self.__engineButton = E5LineEditButton(self) + self.__engineButton.setMenu(self.__enginesMenu) + self.addWidget(self.__engineButton, E5LineEditSide.LEFT) + + self.__searchButton = E5LineEditButton(self) + self.__searchButton.setIcon(UI.PixmapCache.getIcon("webSearch")) + self.addWidget(self.__searchButton, E5LineEditSide.LEFT) + + self.__model = QStandardItemModel(self) + self.__completer = QCompleter() + self.__completer.setModel(self.__model) + self.__completer.setCompletionMode( + QCompleter.CompletionMode.UnfilteredPopupCompletion) + self.__completer.setWidget(self) + + self.__searchButton.clicked.connect(self.__searchButtonClicked) + self.textEdited.connect(self.__textEdited) + self.returnPressed.connect(self.__searchNow) + self.__completer.activated[QModelIndex].connect( + self.__completerActivated) + self.__completer.highlighted[QModelIndex].connect( + self.__completerHighlighted) + self.__enginesMenu.aboutToShow.connect(self.__showEnginesMenu) + + self.__suggestionsItem = None + self.__suggestions = [] + self.__suggestTimer = None + self.__suggestionsEnabled = Preferences.getWebBrowser( + "WebSearchSuggestions") + + self.__recentSearchesItem = None + self.__recentSearches = [] + self.__maxSavedSearches = 10 + + self.__engine = None + self.__loadSearches() + self.__setupCompleterMenu() + self.__currentEngineChanged() + + def __searchNow(self): + """ + Private slot to perform the web search. + """ + searchText = self.text() + if not searchText: + return + + import WebBrowser.WebBrowserWindow + if WebBrowser.WebBrowserWindow.WebBrowserWindow.isPrivate(): + return + + if searchText in self.__recentSearches: + self.__recentSearches.remove(searchText) + self.__recentSearches.insert(0, searchText) + if len(self.__recentSearches) > self.__maxSavedSearches: + self.__recentSearches = self.__recentSearches[ + :self.__maxSavedSearches] + self.__setupCompleterMenu() + + self.__mw.currentBrowser().setFocus() + self.__mw.currentBrowser().load( + self.__openSearchManager.currentEngine().searchUrl(searchText)) + + def __setupCompleterMenu(self): + """ + Private method to create the completer menu. + """ + if ( + not self.__suggestions or + (self.__model.rowCount() > 0 and + self.__model.item(0) != self.__suggestionsItem) + ): + self.__model.clear() + self.__suggestionsItem = None + else: + self.__model.removeRows(1, self.__model.rowCount() - 1) + + boldFont = QFont() + boldFont.setBold(True) + + if self.__suggestions: + if self.__model.rowCount() == 0: + if not self.__suggestionsItem: + self.__suggestionsItem = QStandardItem( + self.tr("Suggestions")) + self.__suggestionsItem.setFont(boldFont) + self.__model.appendRow(self.__suggestionsItem) + + for suggestion in self.__suggestions: + self.__model.appendRow(QStandardItem(suggestion)) + + if not self.__recentSearches: + self.__recentSearchesItem = QStandardItem( + self.tr("No Recent Searches")) + self.__recentSearchesItem.setFont(boldFont) + self.__model.appendRow(self.__recentSearchesItem) + else: + self.__recentSearchesItem = QStandardItem( + self.tr("Recent Searches")) + self.__recentSearchesItem.setFont(boldFont) + self.__model.appendRow(self.__recentSearchesItem) + for recentSearch in self.__recentSearches: + self.__model.appendRow(QStandardItem(recentSearch)) + + view = self.__completer.popup() + view.setFixedHeight(view.sizeHintForRow(0) * self.__model.rowCount() + + view.frameWidth() * 2) + + self.__searchButton.setEnabled( + bool(self.__recentSearches or self.__suggestions)) + + def __completerActivated(self, index): + """ + Private slot handling the selection of an entry from the completer. + + @param index index of the item (QModelIndex) + """ + if ( + self.__suggestionsItem and + self.__suggestionsItem.index().row() == index.row() + ): + return + + if ( + self.__recentSearchesItem and + self.__recentSearchesItem.index().row() == index.row() + ): + return + + self.__searchNow() + + def __completerHighlighted(self, index): + """ + Private slot handling the highlighting of an entry of the completer. + + @param index index of the item (QModelIndex) + @return flah indicating a successful highlighting (boolean) + """ + if ( + self.__suggestionsItem and + self.__suggestionsItem.index().row() == index.row() + ): + return False + + if ( + self.__recentSearchesItem and + self.__recentSearchesItem.index().row() == index.row() + ): + return False + + self.setText(index.data()) + return True + + def __textEdited(self, txt): + """ + Private slot to handle changes of the search text. + + @param txt search text (string) + """ + if self.__suggestionsEnabled: + if self.__suggestTimer is None: + self.__suggestTimer = QTimer(self) + self.__suggestTimer.setSingleShot(True) + self.__suggestTimer.setInterval(200) + self.__suggestTimer.timeout.connect(self.__getSuggestions) + self.__suggestTimer.start() + else: + self.__completer.setCompletionPrefix(txt) + self.__completer.complete() + + def __getSuggestions(self): + """ + Private slot to get search suggestions from the configured search + engine. + """ + searchText = self.text() + if searchText: + self.__openSearchManager.currentEngine().requestSuggestions( + searchText) + + def __newSuggestions(self, suggestions): + """ + Private slot to receive a new list of suggestions. + + @param suggestions list of suggestions (list of strings) + """ + self.__suggestions = suggestions + self.__setupCompleterMenu() + self.__completer.complete() + + def __showEnginesMenu(self): + """ + Private slot to handle the display of the engines menu. + """ + self.__enginesMenu.clear() + + from .OpenSearch.OpenSearchEngineAction import OpenSearchEngineAction + engineNames = self.__openSearchManager.allEnginesNames() + for engineName in engineNames: + engine = self.__openSearchManager.engine(engineName) + action = OpenSearchEngineAction(engine, self.__enginesMenu) + action.setData(engineName) + self.__enginesMenu.addAction(action) + + if self.__openSearchManager.currentEngineName() == engineName: + action.setCheckable(True) + action.setChecked(True) + + cb = self.__mw.currentBrowser() + from .Tools import Scripts + script = Scripts.getOpenSearchLinks() + cb.page().runJavaScript( + script, WebBrowserPage.SafeJsWorld, self.__showEnginesMenuCallback) + + def __showEnginesMenuCallback(self, res): + """ + Private method handling the open search links callback. + + @param res result of the JavaScript + @type list of dict + """ + cb = self.__mw.currentBrowser() + if res: + self.__enginesMenu.addSeparator() + for entry in res: + url = cb.url().resolved(QUrl(entry["url"])) + title = entry["title"] + if url.isEmpty(): + continue + if not title: + title = cb.title() + + action = self.__enginesMenu.addAction( + self.tr("Add '{0}'").format(title)) + action.setData(url) + action.setIcon(cb.icon()) + + self.__enginesMenu.addSeparator() + self.__enginesMenu.addAction(self.__mw.searchEnginesAction()) + + if self.__recentSearches: + act = self.__enginesMenu.addAction( + self.tr("Clear Recent Searches")) + act.setData("@@CLEAR@@") + + def __handleEnginesMenuActionTriggered(self, action): + """ + Private slot to handle an action of the menu being triggered. + + @param action reference to the action that triggered + @type QAction + """ + actData = action.data() + if isinstance(actData, QUrl): + # add search engine + self.__openSearchManager.addEngine(actData) + elif isinstance(actData, str): + # engine name or special action + if actData == "@@CLEAR@@": + self.clear() + else: + self.__openSearchManager.setCurrentEngineName(actData) + + def __searchButtonClicked(self): + """ + Private slot to show the search menu via the search button. + """ + self.__setupCompleterMenu() + self.__completer.complete() + + def clear(self): + """ + Public method to clear all private data. + """ + self.__recentSearches = [] + self.__setupCompleterMenu() + super().clear() + self.clearFocus() + + def preferencesChanged(self): + """ + Public method to handle the change of preferences. + """ + self.__suggestionsEnabled = Preferences.getWebBrowser( + "WebSearchSuggestions") + if not self.__suggestionsEnabled: + self.__suggestions = [] + self.__setupCompleterMenu() + + def saveSearches(self): + """ + Public method to save the recently performed web searches. + """ + Preferences.Prefs.settings.setValue( + 'WebBrowser/WebSearches', self.__recentSearches) + + def __loadSearches(self): + """ + Private method to load the recently performed web searches. + """ + searches = Preferences.Prefs.settings.value('WebBrowser/WebSearches') + if searches is not None: + self.__recentSearches = searches + + def openSearchManager(self): + """ + Public method to get a reference to the opensearch manager object. + + @return reference to the opensearch manager object (OpenSearchManager) + """ + return self.__openSearchManager + + def __currentEngineChanged(self): + """ + Private slot to track a change of the current search engine. + """ + if self.__openSearchManager.engineExists(self.__currentEngine): + oldEngine = self.__openSearchManager.engine(self.__currentEngine) + oldEngine.imageChanged.disconnect(self.__engineImageChanged) + if self.__suggestionsEnabled: + oldEngine.suggestions.disconnect(self.__newSuggestions) + + newEngine = self.__openSearchManager.currentEngine() + if newEngine.networkAccessManager() is None: + newEngine.setNetworkAccessManager(self.__mw.networkManager()) + newEngine.imageChanged.connect(self.__engineImageChanged) + if self.__suggestionsEnabled: + newEngine.suggestions.connect(self.__newSuggestions) + + self.setInactiveText(self.__openSearchManager.currentEngineName()) + self.__currentEngine = self.__openSearchManager.currentEngineName() + self.__engineButton.setIcon(QIcon(QPixmap.fromImage( + self.__openSearchManager.currentEngine().image()))) + self.__suggestions = [] + self.__setupCompleterMenu() + + def __engineImageChanged(self): + """ + Private slot to handle a change of the current search engine icon. + """ + self.__engineButton.setIcon(QIcon(QPixmap.fromImage( + self.__openSearchManager.currentEngine().image()))) + + def mousePressEvent(self, evt): + """ + Protected method called by a mouse press event. + + @param evt reference to the mouse event (QMouseEvent) + """ + if evt.button() == Qt.MouseButton.XButton1: + self.__mw.currentBrowser().triggerPageAction( + QWebEnginePage.WebAction.Back) + elif evt.button() == Qt.MouseButton.XButton2: + self.__mw.currentBrowser().triggerPageAction( + QWebEnginePage.WebAction.Forward) + else: + super().mousePressEvent(evt)