|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2009 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a web search widget for the web browser. |
|
8 """ |
|
9 |
|
10 from PyQt5.QtCore import pyqtSignal, QUrl, QModelIndex, QTimer, Qt |
|
11 from PyQt5.QtGui import ( |
|
12 QStandardItem, QStandardItemModel, QFont, QIcon, QPixmap |
|
13 ) |
|
14 from PyQt5.QtWidgets import QMenu, QCompleter |
|
15 from PyQt5.QtWebEngineWidgets import QWebEnginePage |
|
16 |
|
17 import UI.PixmapCache |
|
18 |
|
19 import Preferences |
|
20 |
|
21 from E5Gui.E5LineEdit import E5ClearableLineEdit, E5LineEditSide |
|
22 |
|
23 from .WebBrowserPage import WebBrowserPage |
|
24 |
|
25 |
|
26 class 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) |