src/eric7/WebBrowser/OpenSearch/OpenSearchManager.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9162
8b75b1668583
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2009 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a manager for open search engines.
8 """
9
10 import os
11 import contextlib
12
13 from PyQt6.QtCore import (
14 pyqtSignal, QObject, QUrl, QFile, QDir, QIODevice, QUrlQuery
15 )
16 from PyQt6.QtWidgets import QLineEdit, QInputDialog
17 from PyQt6.QtNetwork import QNetworkRequest, QNetworkReply
18
19 from EricWidgets.EricApplication import ericApp
20 from EricWidgets import EricMessageBox
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 = ericApp()
46 super().__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.OpenModeFlag.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 EricMessageBox.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.EchoMode.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(
286 QUrl.ComponentFormattingOption.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 os.unlink(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 qdir = QDir()
347 if not qdir.mkpath(dirName):
348 return
349 qdir.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 = qdir.filePath(name)
357
358 file = QFile(fileName)
359 if not file.open(QIODevice.OpenModeFlag.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.setWebBrowser("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.setWebBrowser("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 os.path.exists(dirName):
388 return False
389
390 success = False
391
392 qdir = QDir(dirName)
393 for name in qdir.entryList(["*.xml"]):
394 fileName = qdir.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 (
415 self.__current not in self.__engines and
416 len(self.__engines) > 0
417 ):
418 self.__current = list(self.__engines.keys())[0]
419
420 self.__loading = False
421 self.currentEngineChanged.emit()
422
423 def restoreDefaults(self):
424 """
425 Public method to restore the default search engines.
426 """
427 from .OpenSearchReader import OpenSearchReader
428
429 reader = OpenSearchReader()
430 defaultEnginesDirectory = os.path.join(os.path.dirname(__file__),
431 "DefaultSearchEngines")
432 for engineFileName in (
433 QDir(defaultEnginesDirectory, "*.xml").entryList()
434 ):
435 engineFile = QFile(os.path.join(defaultEnginesDirectory,
436 engineFileName))
437 if not engineFile.open(QIODevice.OpenModeFlag.ReadOnly):
438 continue
439 engine = reader.read(engineFile)
440 self.__addEngineByEngine(engine)
441
442 def enginesDirectory(self):
443 """
444 Public method to determine the directory containing the search engine
445 descriptions.
446
447 @return directory name (string)
448 """
449 return os.path.join(
450 Utilities.getConfigDir(), "web_browser", "searchengines")
451
452 def __confirmAddition(self, engine):
453 """
454 Private method to confirm the addition of a new search engine.
455
456 @param engine reference to the engine to be added (OpenSearchEngine)
457 @return flag indicating the engine shall be added (boolean)
458 """
459 if engine is None or not engine.isValid():
460 return False
461
462 host = QUrl(engine.searchUrlTemplate()).host()
463
464 res = EricMessageBox.yesNo(
465 None,
466 "",
467 self.tr(
468 """<p>Do you want to add the following engine to your"""
469 """ list of search engines?<br/><br/>Name: {0}<br/>"""
470 """Searches on: {1}</p>""").format(engine.name(), host))
471 return res
472
473 def __engineFromUrlAvailable(self, reply):
474 """
475 Private slot to add a search engine from the net.
476
477 @param reply reference to the network reply
478 @type QNetworkReply
479 """
480 reply.close()
481 if reply in self.__replies:
482 self.__replies.remove(reply)
483
484 if reply.error() == QNetworkReply.NetworkError.NoError:
485 from .OpenSearchReader import OpenSearchReader
486 reader = OpenSearchReader()
487 engine = reader.read(reply)
488
489 if not engine.isValid():
490 return
491
492 if self.engineExists(engine.name()):
493 return
494
495 if not self.__confirmAddition(engine):
496 return
497
498 if not self.__addEngineByEngine(engine):
499 return
500 else:
501 # some error happened
502 from WebBrowser.WebBrowserWindow import WebBrowserWindow
503 WebBrowserWindow.getWindow().statusBar().showMessage(
504 reply.errorString(), 10000)
505
506 def convertKeywordSearchToUrl(self, keywordSearch):
507 """
508 Public method to get the search URL for a keyword search.
509
510 @param keywordSearch search string for keyword search (string)
511 @return search URL (QUrl)
512 """
513 try:
514 keyword, term = keywordSearch.split(" ", 1)
515 except ValueError:
516 return QUrl()
517
518 if not term:
519 return QUrl()
520
521 engine = self.engineForKeyword(keyword)
522 if engine:
523 return engine.searchUrl(term)
524
525 return QUrl()
526
527 def engineForKeyword(self, keyword):
528 """
529 Public method to get the engine for a keyword.
530
531 @param keyword keyword to get engine for (string)
532 @return reference to the search engine object (OpenSearchEngine)
533 """
534 if keyword and keyword in self.__keywords:
535 return self.__keywords[keyword]
536
537 return None
538
539 def setEngineForKeyword(self, keyword, engine):
540 """
541 Public method to set the engine for a keyword.
542
543 @param keyword keyword to get engine for (string)
544 @param engine reference to the search engine object (OpenSearchEngine)
545 or None to remove the keyword
546 """
547 if not keyword:
548 return
549
550 if engine is None:
551 with contextlib.suppress(KeyError):
552 del self.__keywords[keyword]
553 else:
554 self.__keywords[keyword] = engine
555
556 self.changed.emit()
557
558 def keywordsForEngine(self, engine):
559 """
560 Public method to get the keywords for a given engine.
561
562 @param engine reference to the search engine object (OpenSearchEngine)
563 @return list of keywords (list of strings)
564 """
565 return [k for k in self.__keywords if self.__keywords[k] == engine]
566
567 def setKeywordsForEngine(self, engine, keywords):
568 """
569 Public method to set the keywords for an engine.
570
571 @param engine reference to the search engine object (OpenSearchEngine)
572 @param keywords list of keywords (list of strings)
573 """
574 if engine is None:
575 return
576
577 for keyword in self.keywordsForEngine(engine):
578 del self.__keywords[keyword]
579
580 for keyword in keywords:
581 if not keyword:
582 continue
583
584 self.__keywords[keyword] = engine
585
586 self.changed.emit()
587
588 def enginesChanged(self):
589 """
590 Public slot to tell the search engine manager, that something has
591 changed.
592 """
593 self.changed.emit()

eric ide

mercurial