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