eric6/Helpviewer/OpenSearch/OpenSearchManager.py

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

eric ide

mercurial