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