WebBrowser/OpenSearch/OpenSearchManager.py

branch
QtWebEngine
changeset 4741
f9e1adc69076
parent 4631
5c1a96925da4
child 4745
285bfd224a1b
equal deleted inserted replaced
4735:84e78ee0f361 4741:f9e1adc69076
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a manager for open search engines.
8 """
9
10 from __future__ import unicode_literals
11
12 import os
13
14 from PyQt5.QtCore import pyqtSignal, QObject, QUrl, QFile, QDir, QIODevice, \
15 QUrlQuery
16 from PyQt5.QtWidgets import QLineEdit, QInputDialog
17 from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
18
19 from E5Gui.E5Application import e5App
20 from E5Gui import E5MessageBox
21
22 from Utilities.AutoSaver import AutoSaver
23 import Utilities
24 import Preferences
25
26
27 class OpenSearchManager(QObject):
28 """
29 Class implementing a manager for open search engines.
30
31 @signal changed() emitted to indicate a change
32 @signal currentEngineChanged() emitted to indicate a change of
33 the current search engine
34 """
35 changed = pyqtSignal()
36 currentEngineChanged = pyqtSignal()
37
38 def __init__(self, parent=None):
39 """
40 Constructor
41
42 @param parent reference to the parent object (QObject)
43 """
44 if parent is None:
45 parent = e5App()
46 super(OpenSearchManager, self).__init__(parent)
47
48 self.__replies = []
49 self.__engines = {}
50 self.__keywords = {}
51 self.__current = ""
52 self.__loading = False
53 self.__saveTimer = AutoSaver(self, self.save)
54
55 self.changed.connect(self.__saveTimer.changeOccurred)
56
57 self.load()
58
59 def close(self):
60 """
61 Public method to close the open search engines manager.
62 """
63 self.__saveTimer.saveIfNeccessary()
64
65 def currentEngineName(self):
66 """
67 Public method to get the name of the current search engine.
68
69 @return name of the current search engine (string)
70 """
71 return self.__current
72
73 def setCurrentEngineName(self, name):
74 """
75 Public method to set the current engine by name.
76
77 @param name name of the new current engine (string)
78 """
79 if name not in self.__engines:
80 return
81
82 self.__current = name
83 self.currentEngineChanged.emit()
84 self.changed.emit()
85
86 def currentEngine(self):
87 """
88 Public method to get a reference to the current engine.
89
90 @return reference to the current engine (OpenSearchEngine)
91 """
92 if not self.__current or self.__current not in self.__engines:
93 return None
94
95 return self.__engines[self.__current]
96
97 def setCurrentEngine(self, engine):
98 """
99 Public method to set the current engine.
100
101 @param engine reference to the new current engine (OpenSearchEngine)
102 """
103 if engine is None:
104 return
105
106 for engineName in self.__engines:
107 if self.__engines[engineName] == engine:
108 self.setCurrentEngineName(engineName)
109 break
110
111 def engine(self, name):
112 """
113 Public method to get a reference to the named engine.
114
115 @param name name of the engine (string)
116 @return reference to the engine (OpenSearchEngine)
117 """
118 if name not in self.__engines:
119 return None
120
121 return self.__engines[name]
122
123 def engineExists(self, name):
124 """
125 Public method to check, if an engine exists.
126
127 @param name name of the engine (string)
128 @return flag indicating an existing engine (boolean)
129 """
130 return name in self.__engines
131
132 def allEnginesNames(self):
133 """
134 Public method to get a list of all engine names.
135
136 @return sorted list of all engine names (list of strings)
137 """
138 return sorted(self.__engines.keys())
139
140 def enginesCount(self):
141 """
142 Public method to get the number of available engines.
143
144 @return number of engines (integer)
145 """
146 return len(self.__engines)
147
148 def addEngine(self, engine):
149 """
150 Public method to add a new search engine.
151
152 @param engine URL of the engine definition file (QUrl) or
153 name of a file containing the engine definition (string)
154 or reference to an engine object (OpenSearchEngine)
155 @return flag indicating success (boolean)
156 """
157 from .OpenSearchEngine import OpenSearchEngine
158 if isinstance(engine, QUrl):
159 return self.__addEngineByUrl(engine)
160 elif isinstance(engine, OpenSearchEngine):
161 return self.__addEngineByEngine(engine)
162 else:
163 return self.__addEngineByFile(engine)
164
165 def __addEngineByUrl(self, url):
166 """
167 Private method to add a new search engine given its URL.
168
169 @param url URL of the engine definition file (QUrl)
170 @return flag indicating success (boolean)
171 """
172 if not url.isValid():
173 return
174
175 from WebBrowser.WebBrowserWindow import WebBrowserWindow
176
177 reply = WebBrowserWindow.networkManager().get(QNetworkRequest(url))
178 reply.finished.connect(self.__engineFromUrlAvailable)
179 reply.setParent(self)
180 self.__replies.append(reply)
181
182 return True
183
184 def __addEngineByFile(self, filename):
185 """
186 Private method to add a new search engine given a filename.
187
188 @param filename name of a file containing the engine definition
189 (string)
190 @return flag indicating success (boolean)
191 """
192 file_ = QFile(filename)
193 if not file_.open(QIODevice.ReadOnly):
194 return False
195
196 from .OpenSearchReader import OpenSearchReader
197 reader = OpenSearchReader()
198 engine = reader.read(file_)
199
200 if not self.__addEngineByEngine(engine):
201 return False
202
203 return True
204
205 def __addEngineByEngine(self, engine):
206 """
207 Private method to add a new search engine given a reference to an
208 engine.
209
210 @param engine reference to an engine object (OpenSearchEngine)
211 @return flag indicating success (boolean)
212 """
213 if engine is None:
214 return False
215
216 if not engine.isValid():
217 return False
218
219 if engine.name() in self.__engines:
220 return False
221
222 engine.setParent(self)
223 self.__engines[engine.name()] = engine
224
225 self.changed.emit()
226
227 return True
228
229 # TODO: Open Search: implement this right
230 def addEngineFromForm(self, res, view):
231 """
232 Private method to add a new search engine from a form.
233
234 @param res result of the JavaScript run on by
235 WebBrowserView.__addSearchEngine()
236 @type dict or None
237 @param view reference to the web browser view
238 @type WebBrowserView
239 """
240 if not res:
241 return
242
243 method = res["method"]
244 isPost = method == "post"
245 actionUrl = QUrl(res["action"])
246 inputName = res["inputName"]
247
248 if method != "get":
249 E5MessageBox.warning(
250 self,
251 self.tr("Method not supported"),
252 self.tr(
253 """{0} method is not supported.""").format(method.upper()))
254 return
255
256 if actionUrl.isRelative():
257 actionUrl = view.url().resolved(actionUrl)
258
259 searchUrlQuery = QUrlQuery(actionUrl)
260 searchUrlQuery.addQueryItem(inputName, "{searchTerms}")
261
262 inputFields = res["inputs"]
263 for inputField in inputFields:
264 name = inputField[0]
265 value = inputField[1]
266
267 if not name or name == inputName or not value:
268 continue
269
270 searchUrlQuery.addQueryItem(name, value)
271
272 engineName, ok = QInputDialog.getText(
273 view,
274 self.tr("Engine name"),
275 self.tr("Enter a name for the engine"),
276 QLineEdit.Normal)
277 if not ok:
278 return
279
280 actionUrl.setQuery(searchUrlQuery)
281
282 from .OpenSearchEngine import OpenSearchEngine
283 engine = OpenSearchEngine()
284 engine.setName(engineName)
285 engine.setDescription(engineName)
286 engine.setSearchUrlTemplate(actionUrl.toDisplayString(QUrl.FullyDecoded))
287 engine.setImage(view.icon().pixmap(16, 16).toImage())
288
289 self.__addEngineByEngine(engine)
290
291 def removeEngine(self, name):
292 """
293 Public method to remove an engine.
294
295 @param name name of the engine (string)
296 """
297 if len(self.__engines) <= 1:
298 return
299
300 if name not in self.__engines:
301 return
302
303 engine = self.__engines[name]
304 for keyword in [k for k in self.__keywords
305 if self.__keywords[k] == engine]:
306 del self.__keywords[keyword]
307 del self.__engines[name]
308
309 file_ = QDir(self.enginesDirectory()).filePath(
310 self.generateEngineFileName(name))
311 QFile.remove(file_)
312
313 if name == self.__current:
314 self.setCurrentEngineName(list(self.__engines.keys())[0])
315
316 self.changed.emit()
317
318 def generateEngineFileName(self, engineName):
319 """
320 Public method to generate a valid engine file name.
321
322 @param engineName name of the engine (string)
323 @return valid engine file name (string)
324 """
325 fileName = ""
326
327 # strip special characters
328 for c in engineName:
329 if c.isspace():
330 fileName += '_'
331 continue
332
333 if c.isalnum():
334 fileName += c
335
336 fileName += ".xml"
337
338 return fileName
339
340 def saveDirectory(self, dirName):
341 """
342 Public method to save the search engine definitions to files.
343
344 @param dirName name of the directory to write the files to (string)
345 """
346 dir = QDir()
347 if not dir.mkpath(dirName):
348 return
349 dir.setPath(dirName)
350
351 from .OpenSearchWriter import OpenSearchWriter
352 writer = OpenSearchWriter()
353
354 for engine in list(self.__engines.values()):
355 name = self.generateEngineFileName(engine.name())
356 fileName = dir.filePath(name)
357
358 file = QFile(fileName)
359 if not file.open(QIODevice.WriteOnly):
360 continue
361
362 writer.write(file, engine)
363
364 def save(self):
365 """
366 Public method to save the search engines configuration.
367 """
368 if self.__loading:
369 return
370
371 self.saveDirectory(self.enginesDirectory())
372
373 Preferences.setHelp("WebSearchEngine", self.__current)
374 keywords = []
375 for k in self.__keywords:
376 if self.__keywords[k]:
377 keywords.append((k, self.__keywords[k].name()))
378 Preferences.setHelp("WebSearchKeywords", keywords)
379
380 def loadDirectory(self, dirName):
381 """
382 Public method to load the search engine definitions from files.
383
384 @param dirName name of the directory to load the files from (string)
385 @return flag indicating success (boolean)
386 """
387 if not QFile.exists(dirName):
388 return False
389
390 success = False
391
392 dir = QDir(dirName)
393 for name in dir.entryList(["*.xml"]):
394 fileName = dir.filePath(name)
395 if self.__addEngineByFile(fileName):
396 success = True
397
398 return success
399
400 def load(self):
401 """
402 Public method to load the search engines configuration.
403 """
404 self.__loading = True
405 self.__current = Preferences.getWebBrowser("WebSearchEngine")
406 keywords = Preferences.getWebBrowser("WebSearchKeywords")
407
408 if not self.loadDirectory(self.enginesDirectory()):
409 self.restoreDefaults()
410
411 for keyword, engineName in keywords:
412 self.__keywords[keyword] = self.engine(engineName)
413
414 if self.__current not in self.__engines and \
415 len(self.__engines) > 0:
416 self.__current = list(self.__engines.keys())[0]
417
418 self.__loading = False
419 self.currentEngineChanged.emit()
420
421 def restoreDefaults(self):
422 """
423 Public method to restore the default search engines.
424 """
425 from .OpenSearchReader import OpenSearchReader
426 from .DefaultSearchEngines import DefaultSearchEngines_rc # __IGNORE_WARNING__
427
428 defaultEngineFiles = ["Amazoncom.xml", "Bing.xml",
429 "DeEn_Beolingus.xml", "DuckDuckGo.xml",
430 "Facebook.xml", "Google.xml",
431 "Google_Im_Feeling_Lucky.xml", "LEO_DeuEng.xml",
432 "LinuxMagazin.xml", "Reddit.xml", "Wikia.xml",
433 "Wikia_en.xml", "Wikipedia.xml",
434 "Wiktionary.xml", "Yahoo.xml", "YouTube.xml", ]
435 # Keep this list in sync with the contents of the resource file.
436
437 reader = OpenSearchReader()
438 for engineFileName in defaultEngineFiles:
439 engineFile = QFile(":/" + engineFileName)
440 if not engineFile.open(QIODevice.ReadOnly):
441 continue
442 engine = reader.read(engineFile)
443 self.__addEngineByEngine(engine)
444
445 def enginesDirectory(self):
446 """
447 Public method to determine the directory containing the search engine
448 descriptions.
449
450 @return directory name (string)
451 """
452 return os.path.join(
453 Utilities.getConfigDir(), "web_browser", "searchengines")
454
455 def __confirmAddition(self, engine):
456 """
457 Private method to confirm the addition of a new search engine.
458
459 @param engine reference to the engine to be added (OpenSearchEngine)
460 @return flag indicating the engine shall be added (boolean)
461 """
462 if engine is None or not engine.isValid():
463 return False
464
465 host = QUrl(engine.searchUrlTemplate()).host()
466
467 res = E5MessageBox.yesNo(
468 None,
469 "",
470 self.tr(
471 """<p>Do you want to add the following engine to your"""
472 """ list of search engines?<br/><br/>Name: {0}<br/>"""
473 """Searches on: {1}</p>""").format(engine.name(), host))
474 return res
475
476 def __engineFromUrlAvailable(self):
477 """
478 Private slot to add a search engine from the net.
479 """
480 reply = self.sender()
481 if reply is None:
482 return
483
484 if reply.error() != QNetworkReply.NoError:
485 reply.close()
486 if reply in self.__replies:
487 self.__replies.remove(reply)
488 return
489
490 from .OpenSearchReader import OpenSearchReader
491 reader = OpenSearchReader()
492 engine = reader.read(reply)
493
494 reply.close()
495 if reply in self.__replies:
496 self.__replies.remove(reply)
497
498 if not engine.isValid():
499 return
500
501 if self.engineExists(engine.name()):
502 return
503
504 if not self.__confirmAddition(engine):
505 return
506
507 if not self.__addEngineByEngine(engine):
508 return
509
510 def convertKeywordSearchToUrl(self, keywordSearch):
511 """
512 Public method to get the search URL for a keyword search.
513
514 @param keywordSearch search string for keyword search (string)
515 @return search URL (QUrl)
516 """
517 try:
518 keyword, term = keywordSearch.split(" ", 1)
519 except ValueError:
520 return QUrl()
521
522 if not term:
523 return QUrl()
524
525 engine = self.engineForKeyword(keyword)
526 if engine:
527 return engine.searchUrl(term)
528
529 return QUrl()
530
531 def engineForKeyword(self, keyword):
532 """
533 Public method to get the engine for a keyword.
534
535 @param keyword keyword to get engine for (string)
536 @return reference to the search engine object (OpenSearchEngine)
537 """
538 if keyword and keyword in self.__keywords:
539 return self.__keywords[keyword]
540
541 return None
542
543 def setEngineForKeyword(self, keyword, engine):
544 """
545 Public method to set the engine for a keyword.
546
547 @param keyword keyword to get engine for (string)
548 @param engine reference to the search engine object (OpenSearchEngine)
549 or None to remove the keyword
550 """
551 if not keyword:
552 return
553
554 if engine is None:
555 try:
556 del self.__keywords[keyword]
557 except KeyError:
558 pass
559 else:
560 self.__keywords[keyword] = engine
561
562 self.changed.emit()
563
564 def keywordsForEngine(self, engine):
565 """
566 Public method to get the keywords for a given engine.
567
568 @param engine reference to the search engine object (OpenSearchEngine)
569 @return list of keywords (list of strings)
570 """
571 return [k for k in self.__keywords if self.__keywords[k] == engine]
572
573 def setKeywordsForEngine(self, engine, keywords):
574 """
575 Public method to set the keywords for an engine.
576
577 @param engine reference to the search engine object (OpenSearchEngine)
578 @param keywords list of keywords (list of strings)
579 """
580 if engine is None:
581 return
582
583 for keyword in self.keywordsForEngine(engine):
584 del self.__keywords[keyword]
585
586 for keyword in keywords:
587 if not keyword:
588 continue
589
590 self.__keywords[keyword] = engine
591
592 self.changed.emit()
593
594 def enginesChanged(self):
595 """
596 Public slot to tell the search engine manager, that something has
597 changed.
598 """
599 self.changed.emit()

eric ide

mercurial