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