eric7/WebBrowser/OpenSearch/OpenSearchManager.py

branch
eric7
changeset 8312
800c432b34c8
parent 8240
93b8a353c4bf
child 8318
962bce857696
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/WebBrowser/OpenSearch/OpenSearchManager.py	Sat May 15 18:45:04 2021 +0200
@@ -0,0 +1,593 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2009 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a manager for open search engines.
+"""
+
+import os
+import contextlib
+
+from PyQt5.QtCore import (
+    pyqtSignal, QObject, QUrl, QFile, QDir, QIODevice, QUrlQuery
+)
+from PyQt5.QtWidgets import QLineEdit, QInputDialog
+from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
+
+from E5Gui.E5Application import e5App
+from E5Gui import E5MessageBox
+
+from Utilities.AutoSaver import AutoSaver
+import Utilities
+import Preferences
+
+
+class OpenSearchManager(QObject):
+    """
+    Class implementing a manager for open search engines.
+    
+    @signal changed() emitted to indicate a change
+    @signal currentEngineChanged() emitted to indicate a change of
+            the current search engine
+    """
+    changed = pyqtSignal()
+    currentEngineChanged = pyqtSignal()
+    
+    def __init__(self, parent=None):
+        """
+        Constructor
+        
+        @param parent reference to the parent object (QObject)
+        """
+        if parent is None:
+            parent = e5App()
+        super().__init__(parent)
+        
+        self.__replies = []
+        self.__engines = {}
+        self.__keywords = {}
+        self.__current = ""
+        self.__loading = False
+        self.__saveTimer = AutoSaver(self, self.save)
+        
+        self.changed.connect(self.__saveTimer.changeOccurred)
+        
+        self.load()
+    
+    def close(self):
+        """
+        Public method to close the open search engines manager.
+        """
+        self.__saveTimer.saveIfNeccessary()
+    
+    def currentEngineName(self):
+        """
+        Public method to get the name of the current search engine.
+        
+        @return name of the current search engine (string)
+        """
+        return self.__current
+    
+    def setCurrentEngineName(self, name):
+        """
+        Public method to set the current engine by name.
+        
+        @param name name of the new current engine (string)
+        """
+        if name not in self.__engines:
+            return
+        
+        self.__current = name
+        self.currentEngineChanged.emit()
+        self.changed.emit()
+    
+    def currentEngine(self):
+        """
+        Public method to get a reference to the current engine.
+        
+        @return reference to the current engine (OpenSearchEngine)
+        """
+        if not self.__current or self.__current not in self.__engines:
+            return None
+        
+        return self.__engines[self.__current]
+    
+    def setCurrentEngine(self, engine):
+        """
+        Public method to set the current engine.
+        
+        @param engine reference to the new current engine (OpenSearchEngine)
+        """
+        if engine is None:
+            return
+        
+        for engineName in self.__engines:
+            if self.__engines[engineName] == engine:
+                self.setCurrentEngineName(engineName)
+                break
+    
+    def engine(self, name):
+        """
+        Public method to get a reference to the named engine.
+        
+        @param name name of the engine (string)
+        @return reference to the engine (OpenSearchEngine)
+        """
+        if name not in self.__engines:
+            return None
+        
+        return self.__engines[name]
+    
+    def engineExists(self, name):
+        """
+        Public method to check, if an engine exists.
+        
+        @param name name of the engine (string)
+        @return flag indicating an existing engine (boolean)
+        """
+        return name in self.__engines
+    
+    def allEnginesNames(self):
+        """
+        Public method to get a list of all engine names.
+        
+        @return sorted list of all engine names (list of strings)
+        """
+        return sorted(self.__engines.keys())
+    
+    def enginesCount(self):
+        """
+        Public method to get the number of available engines.
+        
+        @return number of engines (integer)
+        """
+        return len(self.__engines)
+    
+    def addEngine(self, engine):
+        """
+        Public method to add a new search engine.
+        
+        @param engine URL of the engine definition file (QUrl) or
+            name of a file containing the engine definition (string)
+            or reference to an engine object (OpenSearchEngine)
+        @return flag indicating success (boolean)
+        """
+        from .OpenSearchEngine import OpenSearchEngine
+        if isinstance(engine, QUrl):
+            return self.__addEngineByUrl(engine)
+        elif isinstance(engine, OpenSearchEngine):
+            return self.__addEngineByEngine(engine)
+        else:
+            return self.__addEngineByFile(engine)
+    
+    def __addEngineByUrl(self, url):
+        """
+        Private method to add a new search engine given its URL.
+        
+        @param url URL of the engine definition file (QUrl)
+        @return flag indicating success (boolean)
+        """
+        if not url.isValid():
+            return False
+        
+        from WebBrowser.WebBrowserWindow import WebBrowserWindow
+
+        reply = WebBrowserWindow.networkManager().get(QNetworkRequest(url))
+        reply.finished.connect(lambda: self.__engineFromUrlAvailable(reply))
+        reply.setParent(self)
+        self.__replies.append(reply)
+        
+        return True
+    
+    def __addEngineByFile(self, filename):
+        """
+        Private method to add a new search engine given a filename.
+        
+        @param filename name of a file containing the engine definition
+            (string)
+        @return flag indicating success (boolean)
+        """
+        file_ = QFile(filename)
+        if not file_.open(QIODevice.OpenModeFlag.ReadOnly):
+            return False
+        
+        from .OpenSearchReader import OpenSearchReader
+        reader = OpenSearchReader()
+        engine = reader.read(file_)
+        
+        if not self.__addEngineByEngine(engine):
+            return False
+        
+        return True
+    
+    def __addEngineByEngine(self, engine):
+        """
+        Private method to add a new search engine given a reference to an
+        engine.
+        
+        @param engine reference to an engine object (OpenSearchEngine)
+        @return flag indicating success (boolean)
+        """
+        if engine is None:
+            return False
+        
+        if not engine.isValid():
+            return False
+        
+        if engine.name() in self.__engines:
+            return False
+        
+        engine.setParent(self)
+        self.__engines[engine.name()] = engine
+        
+        self.changed.emit()
+        
+        return True
+    
+    def addEngineFromForm(self, res, view):
+        """
+        Public method to add a new search engine from a form.
+        
+        @param res result of the JavaScript run on by
+            WebBrowserView.__addSearchEngine()
+        @type dict or None
+        @param view reference to the web browser view
+        @type WebBrowserView
+        """
+        if not res:
+            return
+        
+        method = res["method"]
+        actionUrl = QUrl(res["action"])
+        inputName = res["inputName"]
+        
+        if method != "get":
+            E5MessageBox.warning(
+                self,
+                self.tr("Method not supported"),
+                self.tr(
+                    """{0} method is not supported.""").format(method.upper()))
+            return
+        
+        if actionUrl.isRelative():
+            actionUrl = view.url().resolved(actionUrl)
+        
+        searchUrlQuery = QUrlQuery(actionUrl)
+        searchUrlQuery.addQueryItem(inputName, "{searchTerms}")
+        
+        inputFields = res["inputs"]
+        for inputField in inputFields:
+            name = inputField[0]
+            value = inputField[1]
+            
+            if not name or name == inputName or not value:
+                continue
+            
+            searchUrlQuery.addQueryItem(name, value)
+        
+        engineName, ok = QInputDialog.getText(
+            view,
+            self.tr("Engine name"),
+            self.tr("Enter a name for the engine"),
+            QLineEdit.EchoMode.Normal)
+        if not ok:
+            return
+        
+        actionUrl.setQuery(searchUrlQuery)
+        
+        from .OpenSearchEngine import OpenSearchEngine
+        engine = OpenSearchEngine()
+        engine.setName(engineName)
+        engine.setDescription(engineName)
+        engine.setSearchUrlTemplate(
+            actionUrl.toDisplayString(
+                QUrl.ComponentFormattingOption.FullyDecoded))
+        engine.setImage(view.icon().pixmap(16, 16).toImage())
+        
+        self.__addEngineByEngine(engine)
+    
+    def removeEngine(self, name):
+        """
+        Public method to remove an engine.
+        
+        @param name name of the engine (string)
+        """
+        if len(self.__engines) <= 1:
+            return
+        
+        if name not in self.__engines:
+            return
+        
+        engine = self.__engines[name]
+        for keyword in [k for k in self.__keywords
+                        if self.__keywords[k] == engine]:
+            del self.__keywords[keyword]
+        del self.__engines[name]
+        
+        file_ = QDir(self.enginesDirectory()).filePath(
+            self.generateEngineFileName(name))
+        QFile.remove(file_)
+        
+        if name == self.__current:
+            self.setCurrentEngineName(list(self.__engines.keys())[0])
+        
+        self.changed.emit()
+    
+    def generateEngineFileName(self, engineName):
+        """
+        Public method to generate a valid engine file name.
+        
+        @param engineName name of the engine (string)
+        @return valid engine file name (string)
+        """
+        fileName = ""
+        
+        # strip special characters
+        for c in engineName:
+            if c.isspace():
+                fileName += '_'
+                continue
+            
+            if c.isalnum():
+                fileName += c
+        
+        fileName += ".xml"
+        
+        return fileName
+    
+    def saveDirectory(self, dirName):
+        """
+        Public method to save the search engine definitions to files.
+        
+        @param dirName name of the directory to write the files to (string)
+        """
+        qdir = QDir()
+        if not qdir.mkpath(dirName):
+            return
+        qdir.setPath(dirName)
+        
+        from .OpenSearchWriter import OpenSearchWriter
+        writer = OpenSearchWriter()
+        
+        for engine in list(self.__engines.values()):
+            name = self.generateEngineFileName(engine.name())
+            fileName = qdir.filePath(name)
+            
+            file = QFile(fileName)
+            if not file.open(QIODevice.OpenModeFlag.WriteOnly):
+                continue
+            
+            writer.write(file, engine)
+    
+    def save(self):
+        """
+        Public method to save the search engines configuration.
+        """
+        if self.__loading:
+            return
+        
+        self.saveDirectory(self.enginesDirectory())
+        
+        Preferences.setWebBrowser("WebSearchEngine", self.__current)
+        keywords = []
+        for k in self.__keywords:
+            if self.__keywords[k]:
+                keywords.append((k, self.__keywords[k].name()))
+        Preferences.setWebBrowser("WebSearchKeywords", keywords)
+    
+    def loadDirectory(self, dirName):
+        """
+        Public method to load the search engine definitions from files.
+        
+        @param dirName name of the directory to load the files from (string)
+        @return flag indicating success (boolean)
+        """
+        if not QFile.exists(dirName):
+            return False
+        
+        success = False
+        
+        qdir = QDir(dirName)
+        for name in qdir.entryList(["*.xml"]):
+            fileName = qdir.filePath(name)
+            if self.__addEngineByFile(fileName):
+                success = True
+        
+        return success
+    
+    def load(self):
+        """
+        Public method to load the search engines configuration.
+        """
+        self.__loading = True
+        self.__current = Preferences.getWebBrowser("WebSearchEngine")
+        keywords = Preferences.getWebBrowser("WebSearchKeywords")
+        
+        if not self.loadDirectory(self.enginesDirectory()):
+            self.restoreDefaults()
+        
+        for keyword, engineName in keywords:
+            self.__keywords[keyword] = self.engine(engineName)
+        
+        if (
+            self.__current not in self.__engines and
+            len(self.__engines) > 0
+        ):
+            self.__current = list(self.__engines.keys())[0]
+        
+        self.__loading = False
+        self.currentEngineChanged.emit()
+    
+    def restoreDefaults(self):
+        """
+        Public method to restore the default search engines.
+        """
+        from .OpenSearchReader import OpenSearchReader
+
+        reader = OpenSearchReader()
+        defaultEnginesDirectory = os.path.join(os.path.dirname(__file__),
+                                               "DefaultSearchEngines")
+        for engineFileName in (
+            QDir(defaultEnginesDirectory, "*.xml").entryList()
+        ):
+            engineFile = QFile(os.path.join(defaultEnginesDirectory,
+                                            engineFileName))
+            if not engineFile.open(QIODevice.OpenModeFlag.ReadOnly):
+                continue
+            engine = reader.read(engineFile)
+            self.__addEngineByEngine(engine)
+    
+    def enginesDirectory(self):
+        """
+        Public method to determine the directory containing the search engine
+        descriptions.
+        
+        @return directory name (string)
+        """
+        return os.path.join(
+            Utilities.getConfigDir(), "web_browser", "searchengines")
+    
+    def __confirmAddition(self, engine):
+        """
+        Private method to confirm the addition of a new search engine.
+        
+        @param engine reference to the engine to be added (OpenSearchEngine)
+        @return flag indicating the engine shall be added (boolean)
+        """
+        if engine is None or not engine.isValid():
+            return False
+        
+        host = QUrl(engine.searchUrlTemplate()).host()
+        
+        res = E5MessageBox.yesNo(
+            None,
+            "",
+            self.tr(
+                """<p>Do you want to add the following engine to your"""
+                """ list of search engines?<br/><br/>Name: {0}<br/>"""
+                """Searches on: {1}</p>""").format(engine.name(), host))
+        return res
+    
+    def __engineFromUrlAvailable(self, reply):
+        """
+        Private slot to add a search engine from the net.
+        
+        @param reply reference to the network reply
+        @type QNetworkReply
+        """
+        reply.close()
+        if reply in self.__replies:
+            self.__replies.remove(reply)
+        
+        if reply.error() == QNetworkReply.NetworkError.NoError:
+            from .OpenSearchReader import OpenSearchReader
+            reader = OpenSearchReader()
+            engine = reader.read(reply)
+            
+            if not engine.isValid():
+                return
+            
+            if self.engineExists(engine.name()):
+                return
+            
+            if not self.__confirmAddition(engine):
+                return
+            
+            if not self.__addEngineByEngine(engine):
+                return
+        else:
+            # some error happened
+            from WebBrowser.WebBrowserWindow import WebBrowserWindow
+            WebBrowserWindow.getWindow().statusBar().showMessage(
+                reply.errorString(), 10000)
+    
+    def convertKeywordSearchToUrl(self, keywordSearch):
+        """
+        Public method to get the search URL for a keyword search.
+        
+        @param keywordSearch search string for keyword search (string)
+        @return search URL (QUrl)
+        """
+        try:
+            keyword, term = keywordSearch.split(" ", 1)
+        except ValueError:
+            return QUrl()
+        
+        if not term:
+            return QUrl()
+        
+        engine = self.engineForKeyword(keyword)
+        if engine:
+            return engine.searchUrl(term)
+        
+        return QUrl()
+    
+    def engineForKeyword(self, keyword):
+        """
+        Public method to get the engine for a keyword.
+        
+        @param keyword keyword to get engine for (string)
+        @return reference to the search engine object (OpenSearchEngine)
+        """
+        if keyword and keyword in self.__keywords:
+            return self.__keywords[keyword]
+        
+        return None
+    
+    def setEngineForKeyword(self, keyword, engine):
+        """
+        Public method to set the engine for a keyword.
+        
+        @param keyword keyword to get engine for (string)
+        @param engine reference to the search engine object (OpenSearchEngine)
+            or None to remove the keyword
+        """
+        if not keyword:
+            return
+        
+        if engine is None:
+            with contextlib.suppress(KeyError):
+                del self.__keywords[keyword]
+        else:
+            self.__keywords[keyword] = engine
+        
+        self.changed.emit()
+    
+    def keywordsForEngine(self, engine):
+        """
+        Public method to get the keywords for a given engine.
+        
+        @param engine reference to the search engine object (OpenSearchEngine)
+        @return list of keywords (list of strings)
+        """
+        return [k for k in self.__keywords if self.__keywords[k] == engine]
+    
+    def setKeywordsForEngine(self, engine, keywords):
+        """
+        Public method to set the keywords for an engine.
+        
+        @param engine reference to the search engine object (OpenSearchEngine)
+        @param keywords list of keywords (list of strings)
+        """
+        if engine is None:
+            return
+        
+        for keyword in self.keywordsForEngine(engine):
+            del self.__keywords[keyword]
+        
+        for keyword in keywords:
+            if not keyword:
+                continue
+            
+            self.__keywords[keyword] = engine
+        
+        self.changed.emit()
+    
+    def enginesChanged(self):
+        """
+        Public slot to tell the search engine manager, that something has
+        changed.
+        """
+        self.changed.emit()

eric ide

mercurial