eric6/WebBrowser/OpenSearch/OpenSearchManager.py

changeset 6942
2602857055c5
parent 6891
93f82da09f22
child 7229
53054eb5b15a
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2009 - 2019 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 False
174
175 from WebBrowser.WebBrowserWindow import WebBrowserWindow
176
177 reply = WebBrowserWindow.networkManager().get(QNetworkRequest(url))
178 reply.finished.connect(lambda: self.__engineFromUrlAvailable(reply))
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 def addEngineFromForm(self, res, view):
230 """
231 Public method to add a new search engine from a form.
232
233 @param res result of the JavaScript run on by
234 WebBrowserView.__addSearchEngine()
235 @type dict or None
236 @param view reference to the web browser view
237 @type WebBrowserView
238 """
239 if not res:
240 return
241
242 method = res["method"]
243 actionUrl = QUrl(res["action"])
244 inputName = res["inputName"]
245
246 if method != "get":
247 E5MessageBox.warning(
248 self,
249 self.tr("Method not supported"),
250 self.tr(
251 """{0} method is not supported.""").format(method.upper()))
252 return
253
254 if actionUrl.isRelative():
255 actionUrl = view.url().resolved(actionUrl)
256
257 searchUrlQuery = QUrlQuery(actionUrl)
258 searchUrlQuery.addQueryItem(inputName, "{searchTerms}")
259
260 inputFields = res["inputs"]
261 for inputField in inputFields:
262 name = inputField[0]
263 value = inputField[1]
264
265 if not name or name == inputName or not value:
266 continue
267
268 searchUrlQuery.addQueryItem(name, value)
269
270 engineName, ok = QInputDialog.getText(
271 view,
272 self.tr("Engine name"),
273 self.tr("Enter a name for the engine"),
274 QLineEdit.Normal)
275 if not ok:
276 return
277
278 actionUrl.setQuery(searchUrlQuery)
279
280 from .OpenSearchEngine import OpenSearchEngine
281 engine = OpenSearchEngine()
282 engine.setName(engineName)
283 engine.setDescription(engineName)
284 engine.setSearchUrlTemplate(
285 actionUrl.toDisplayString(QUrl.FullyDecoded))
286 engine.setImage(view.icon().pixmap(16, 16).toImage())
287
288 self.__addEngineByEngine(engine)
289
290 def removeEngine(self, name):
291 """
292 Public method to remove an engine.
293
294 @param name name of the engine (string)
295 """
296 if len(self.__engines) <= 1:
297 return
298
299 if name not in self.__engines:
300 return
301
302 engine = self.__engines[name]
303 for keyword in [k for k in self.__keywords
304 if self.__keywords[k] == engine]:
305 del self.__keywords[keyword]
306 del self.__engines[name]
307
308 file_ = QDir(self.enginesDirectory()).filePath(
309 self.generateEngineFileName(name))
310 QFile.remove(file_)
311
312 if name == self.__current:
313 self.setCurrentEngineName(list(self.__engines.keys())[0])
314
315 self.changed.emit()
316
317 def generateEngineFileName(self, engineName):
318 """
319 Public method to generate a valid engine file name.
320
321 @param engineName name of the engine (string)
322 @return valid engine file name (string)
323 """
324 fileName = ""
325
326 # strip special characters
327 for c in engineName:
328 if c.isspace():
329 fileName += '_'
330 continue
331
332 if c.isalnum():
333 fileName += c
334
335 fileName += ".xml"
336
337 return fileName
338
339 def saveDirectory(self, dirName):
340 """
341 Public method to save the search engine definitions to files.
342
343 @param dirName name of the directory to write the files to (string)
344 """
345 qdir = QDir()
346 if not qdir.mkpath(dirName):
347 return
348 qdir.setPath(dirName)
349
350 from .OpenSearchWriter import OpenSearchWriter
351 writer = OpenSearchWriter()
352
353 for engine in list(self.__engines.values()):
354 name = self.generateEngineFileName(engine.name())
355 fileName = qdir.filePath(name)
356
357 file = QFile(fileName)
358 if not file.open(QIODevice.WriteOnly):
359 continue
360
361 writer.write(file, engine)
362
363 def save(self):
364 """
365 Public method to save the search engines configuration.
366 """
367 if self.__loading:
368 return
369
370 self.saveDirectory(self.enginesDirectory())
371
372 Preferences.setWebBrowser("WebSearchEngine", self.__current)
373 keywords = []
374 for k in self.__keywords:
375 if self.__keywords[k]:
376 keywords.append((k, self.__keywords[k].name()))
377 Preferences.setWebBrowser("WebSearchKeywords", keywords)
378
379 def loadDirectory(self, dirName):
380 """
381 Public method to load the search engine definitions from files.
382
383 @param dirName name of the directory to load the files from (string)
384 @return flag indicating success (boolean)
385 """
386 if not QFile.exists(dirName):
387 return False
388
389 success = False
390
391 qdir = QDir(dirName)
392 for name in qdir.entryList(["*.xml"]):
393 fileName = qdir.filePath(name)
394 if self.__addEngineByFile(fileName):
395 success = True
396
397 return success
398
399 def load(self):
400 """
401 Public method to load the search engines configuration.
402 """
403 self.__loading = True
404 self.__current = Preferences.getWebBrowser("WebSearchEngine")
405 keywords = Preferences.getWebBrowser("WebSearchKeywords")
406
407 if not self.loadDirectory(self.enginesDirectory()):
408 self.restoreDefaults()
409
410 for keyword, engineName in keywords:
411 self.__keywords[keyword] = self.engine(engineName)
412
413 if self.__current not in self.__engines and \
414 len(self.__engines) > 0:
415 self.__current = list(self.__engines.keys())[0]
416
417 self.__loading = False
418 self.currentEngineChanged.emit()
419
420 def restoreDefaults(self):
421 """
422 Public method to restore the default search engines.
423 """
424 from .OpenSearchReader import OpenSearchReader
425 from .DefaultSearchEngines import DefaultSearchEngines_rc
426 # __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, reply):
477 """
478 Private slot to add a search engine from the net.
479
480 @param reply reference to the network reply
481 @type QNetworkReply
482 """
483 reply.close()
484 if reply in self.__replies:
485 self.__replies.remove(reply)
486
487 if reply.error() == QNetworkReply.NoError:
488 from .OpenSearchReader import OpenSearchReader
489 reader = OpenSearchReader()
490 engine = reader.read(reply)
491
492 if not engine.isValid():
493 return
494
495 if self.engineExists(engine.name()):
496 return
497
498 if not self.__confirmAddition(engine):
499 return
500
501 if not self.__addEngineByEngine(engine):
502 return
503 else:
504 # some error happened
505 from WebBrowser.WebBrowserWindow import WebBrowserWindow
506 WebBrowserWindow.getWindow().statusBar().showMessage(
507 reply.errorString(), 10000)
508
509 def convertKeywordSearchToUrl(self, keywordSearch):
510 """
511 Public method to get the search URL for a keyword search.
512
513 @param keywordSearch search string for keyword search (string)
514 @return search URL (QUrl)
515 """
516 try:
517 keyword, term = keywordSearch.split(" ", 1)
518 except ValueError:
519 return QUrl()
520
521 if not term:
522 return QUrl()
523
524 engine = self.engineForKeyword(keyword)
525 if engine:
526 return engine.searchUrl(term)
527
528 return QUrl()
529
530 def engineForKeyword(self, keyword):
531 """
532 Public method to get the engine for a keyword.
533
534 @param keyword keyword to get engine for (string)
535 @return reference to the search engine object (OpenSearchEngine)
536 """
537 if keyword and keyword in self.__keywords:
538 return self.__keywords[keyword]
539
540 return None
541
542 def setEngineForKeyword(self, keyword, engine):
543 """
544 Public method to set the engine for a keyword.
545
546 @param keyword keyword to get engine for (string)
547 @param engine reference to the search engine object (OpenSearchEngine)
548 or None to remove the keyword
549 """
550 if not keyword:
551 return
552
553 if engine is None:
554 try:
555 del self.__keywords[keyword]
556 except KeyError:
557 pass
558 else:
559 self.__keywords[keyword] = engine
560
561 self.changed.emit()
562
563 def keywordsForEngine(self, engine):
564 """
565 Public method to get the keywords for a given engine.
566
567 @param engine reference to the search engine object (OpenSearchEngine)
568 @return list of keywords (list of strings)
569 """
570 return [k for k in self.__keywords if self.__keywords[k] == engine]
571
572 def setKeywordsForEngine(self, engine, keywords):
573 """
574 Public method to set the keywords for an engine.
575
576 @param engine reference to the search engine object (OpenSearchEngine)
577 @param keywords list of keywords (list of strings)
578 """
579 if engine is None:
580 return
581
582 for keyword in self.keywordsForEngine(engine):
583 del self.__keywords[keyword]
584
585 for keyword in keywords:
586 if not keyword:
587 continue
588
589 self.__keywords[keyword] = engine
590
591 self.changed.emit()
592
593 def enginesChanged(self):
594 """
595 Public slot to tell the search engine manager, that something has
596 changed.
597 """
598 self.changed.emit()

eric ide

mercurial