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