src/eric7/WebBrowser/SpellCheck/ManageDictionariesDialog.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8881
54e42bc2437a
child 9221
bf71ee032bb4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/WebBrowser/SpellCheck/ManageDictionariesDialog.py	Thu Jul 07 11:23:56 2022 +0200
@@ -0,0 +1,485 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2017 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog to install spell checking dictionaries.
+"""
+
+import os
+import io
+import zipfile
+import glob
+import shutil
+import contextlib
+
+from PyQt6.QtCore import pyqtSlot, Qt, QUrl
+from PyQt6.QtWidgets import (
+    QDialog, QDialogButtonBox, QAbstractButton, QListWidgetItem
+)
+from PyQt6.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkInformation
+
+from EricWidgets import EricMessageBox
+
+from .Ui_ManageDictionariesDialog import Ui_ManageDictionariesDialog
+
+from WebBrowser.WebBrowserWindow import WebBrowserWindow
+
+import Preferences
+
+
+class ManageDictionariesDialog(QDialog, Ui_ManageDictionariesDialog):
+    """
+    Class implementing a dialog to install spell checking dictionaries.
+    """
+    FilenameRole = Qt.ItemDataRole.UserRole
+    UrlRole = Qt.ItemDataRole.UserRole + 1
+    DocumentationDirRole = Qt.ItemDataRole.UserRole + 2
+    LocalesRole = Qt.ItemDataRole.UserRole + 3
+    
+    def __init__(self, writeableDirectories, parent=None):
+        """
+        Constructor
+        
+        @param writeableDirectories list of writable directories
+        @type list of str
+        @param parent reference to the parent widget
+        @type QWidget
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+        
+        self.__refreshButton = self.buttonBox.addButton(
+            self.tr("Refresh"), QDialogButtonBox.ButtonRole.ActionRole)
+        self.__installButton = self.buttonBox.addButton(
+            self.tr("Install Selected"),
+            QDialogButtonBox.ButtonRole.ActionRole)
+        self.__installButton.setEnabled(False)
+        self.__uninstallButton = self.buttonBox.addButton(
+            self.tr("Uninstall Selected"),
+            QDialogButtonBox.ButtonRole.ActionRole)
+        self.__uninstallButton.setEnabled(False)
+        self.__cancelButton = self.buttonBox.addButton(
+            self.tr("Cancel"), QDialogButtonBox.ButtonRole.ActionRole)
+        self.__cancelButton.setEnabled(False)
+        
+        self.locationComboBox.addItems(writeableDirectories)
+        
+        self.dictionariesUrlEdit.setText(
+            Preferences.getWebBrowser("SpellCheckDictionariesUrl"))
+        
+        if (
+            Preferences.getUI("DynamicOnlineCheck") and
+            QNetworkInformation.load(QNetworkInformation.Feature.Reachability)
+        ):
+            self.__reachabilityChanged(
+                QNetworkInformation.instance().reachability())
+            QNetworkInformation.instance().reachabilityChanged.connect(
+                self.__reachabilityChanged)
+        else:
+            # assume to be 'always online' if no backend could be loaded or
+            # dynamic online check is switched of
+            self.__reachabilityChanged(QNetworkInformation.Reachability.Online)
+        self.__replies = []
+        
+        self.__downloadCancelled = False
+        self.__dictionariesToDownload = []
+        
+        self.__populateList()
+    
+    def __reachabilityChanged(self, reachability):
+        """
+        Private slot handling reachability state changes.
+        
+        @param reachability new reachability state
+        @type QNetworkInformation.Reachability
+        """
+        online = reachability == QNetworkInformation.Reachability.Online
+        self.__online = online
+        
+        self.__refreshButton.setEnabled(online)
+        
+        msg = (
+            self.tr("Internet Reachability Status: Reachable")
+            if online else
+            self.tr("Internet Reachability Status: Not Reachable")
+        )
+        self.statusLabel.setText(msg)
+        
+        self.on_dictionariesList_itemSelectionChanged()
+    
+    @pyqtSlot(QAbstractButton)
+    def on_buttonBox_clicked(self, button):
+        """
+        Private slot to handle the click of a button of the button box.
+        
+        @param button reference to the button pressed
+        @type QAbstractButton
+        """
+        if button == self.__refreshButton:
+            self.__populateList()
+        elif button == self.__cancelButton:
+            self.__downloadCancel()
+        elif button == self.__installButton:
+            self.__installSelected()
+        elif button == self.__uninstallButton:
+            self.__uninstallSelected()
+    
+    @pyqtSlot()
+    def on_dictionariesList_itemSelectionChanged(self):
+        """
+        Private slot to handle a change of the selection.
+        """
+        self.__installButton.setEnabled(
+            self.locationComboBox.count() > 0 and
+            len(self.dictionariesList.selectedItems()) > 0 and
+            self.__online
+        )
+        
+        self.__uninstallButton.setEnabled(
+            self.locationComboBox.count() > 0 and
+            len([itm
+                 for itm in self.dictionariesList.selectedItems()
+                 if itm.checkState() == Qt.CheckState.Checked
+                 ])
+        )
+    
+    @pyqtSlot(bool)
+    def on_dictionariesUrlEditButton_toggled(self, checked):
+        """
+        Private slot to set the read only status of the dictionaries URL line
+        edit.
+        
+        @param checked state of the push button (boolean)
+        """
+        self.dictionariesUrlEdit.setReadOnly(not checked)
+    
+    @pyqtSlot(str)
+    def on_locationComboBox_currentTextChanged(self, txt):
+        """
+        Private slot to handle a change of the installation location.
+        
+        @param txt installation location
+        @type str
+        """
+        self.__checkInstalledDictionaries()
+    
+    def __populateList(self):
+        """
+        Private method to populate the list of available plugins.
+        """
+        self.dictionariesList.clear()
+        self.downloadProgress.setValue(0)
+        
+        url = self.dictionariesUrlEdit.text()
+        
+        if self.__online:
+            self.__refreshButton.setEnabled(False)
+            self.__installButton.setEnabled(False)
+            self.__uninstallButton.setEnabled(False)
+            self.__cancelButton.setEnabled(True)
+            
+            self.statusLabel.setText(url)
+            
+            self.__downloadCancelled = False
+            
+            request = QNetworkRequest(QUrl(url))
+            request.setAttribute(
+                QNetworkRequest.Attribute.CacheLoadControlAttribute,
+                QNetworkRequest.CacheLoadControl.AlwaysNetwork)
+            reply = WebBrowserWindow.networkManager().get(request)
+            reply.finished.connect(
+                lambda: self.__listFileDownloaded(reply))
+            reply.downloadProgress.connect(self.__downloadProgress)
+            self.__replies.append(reply)
+        else:
+            EricMessageBox.warning(
+                self,
+                self.tr("Error populating list of dictionaries"),
+                self.tr(
+                    """<p>Could not download the dictionaries list"""
+                    """ from {0}.</p><p>Error: {1}</p>"""
+                ).format(url, self.tr("No connection to Internet.")))
+    
+    def __listFileDownloaded(self, reply):
+        """
+        Private method called, after the dictionaries list file has been
+        downloaded from the Internet.
+        
+        @param reply reference to the network reply
+        @type QNetworkReply
+        """
+        self.__refreshButton.setEnabled(True)
+        self.__cancelButton.setEnabled(False)
+        
+        self.downloadProgress.setValue(0)
+        
+        if reply in self.__replies:
+            self.__replies.remove(reply)
+        reply.deleteLater()
+        
+        if reply.error() != QNetworkReply.NetworkError.NoError:
+            if not self.__downloadCancelled:
+                EricMessageBox.warning(
+                    self,
+                    self.tr("Error downloading dictionaries list"),
+                    self.tr(
+                        """<p>Could not download the dictionaries list"""
+                        """ from {0}.</p><p>Error: {1}</p>"""
+                    ).format(self.dictionariesUrlEdit.text(),
+                             reply.errorString())
+                )
+            self.downloadProgress.setValue(0)
+            return
+        
+        listFileData = reply.readAll()
+        
+        # extract the dictionaries
+        from EricXML.SpellCheckDictionariesReader import (
+            SpellCheckDictionariesReader
+        )
+        reader = SpellCheckDictionariesReader(listFileData, self.addEntry)
+        reader.readXML()
+        url = Preferences.getWebBrowser("SpellCheckDictionariesUrl")
+        if url != self.dictionariesUrlEdit.text():
+            self.dictionariesUrlEdit.setText(url)
+            EricMessageBox.warning(
+                self,
+                self.tr("Dictionaries URL Changed"),
+                self.tr(
+                    """The URL of the spell check dictionaries has"""
+                    """ changed. Select the "Refresh" button to get"""
+                    """ the new dictionaries list."""
+                )
+            )
+        
+        if self.locationComboBox.count() == 0:
+            # no writable locations available
+            EricMessageBox.warning(
+                self,
+                self.tr("Error installing dictionaries"),
+                self.tr(
+                    """<p>None of the dictionary locations is writable by"""
+                    """ you. Please download required dictionaries manually"""
+                    """ and install them as administrator.</p>"""
+                )
+            )
+        
+        self.__checkInstalledDictionaries()
+    
+    def __downloadCancel(self):
+        """
+        Private slot to cancel the current download.
+        """
+        if self.__replies:
+            reply = self.__replies[0]
+            self.__downloadCancelled = True
+            self.__dictionariesToDownload = []
+            reply.abort()
+    
+    def __downloadProgress(self, done, total):
+        """
+        Private slot to show the download progress.
+        
+        @param done number of bytes downloaded so far
+        @type int
+        @param total total bytes to be downloaded
+        @type int
+        """
+        if total:
+            self.downloadProgress.setMaximum(total)
+            self.downloadProgress.setValue(done)
+    
+    def addEntry(self, short, filename, url, documentationDir, locales):
+        """
+        Public method to add an entry to the list.
+        
+        @param short data for the description field
+        @type str
+        @param filename data for the filename field
+        @type str
+        @param url download URL for the dictionary entry
+        @type str
+        @param documentationDir name of the directory containing the
+            dictionary documentation
+        @type str
+        @param locales list of locales
+        @type list of str
+        """
+        itm = QListWidgetItem(
+            self.tr("{0} ({1})").format(short, " ".join(locales)),
+            self.dictionariesList)
+        itm.setCheckState(Qt.CheckState.Unchecked)
+        
+        itm.setData(ManageDictionariesDialog.FilenameRole, filename)
+        itm.setData(ManageDictionariesDialog.UrlRole, url)
+        itm.setData(ManageDictionariesDialog.DocumentationDirRole,
+                    documentationDir)
+        itm.setData(ManageDictionariesDialog.LocalesRole, locales)
+    
+    def __checkInstalledDictionaries(self):
+        """
+        Private method to check all installed dictionaries.
+        
+        Note: A dictionary is assumed to be installed, if at least one of its
+        binary dictionaries (*.bdic) is found in the selected dictionaries
+        location.
+        """
+        if self.locationComboBox.currentText():
+            installedLocales = {
+                os.path.splitext(os.path.basename(dic))[0]
+                for dic in glob.glob(
+                    os.path.join(self.locationComboBox.currentText(), "*.bdic")
+                )
+            }
+            
+            for row in range(self.dictionariesList.count()):
+                itm = self.dictionariesList.item(row)
+                locales = set(itm.data(ManageDictionariesDialog.LocalesRole))
+                if locales.intersection(installedLocales):
+                    itm.setCheckState(Qt.CheckState.Checked)
+                else:
+                    itm.setCheckState(Qt.CheckState.Unchecked)
+        else:
+            for row in range(self.dictionariesList.count()):
+                itm = self.dictionariesList.item(row)
+                itm.setCheckState(Qt.CheckState.Unchecked)
+    
+    def __installSelected(self):
+        """
+        Private method to install the selected dictionaries.
+        """
+        if self.__online and bool(self.locationComboBox.currentText()):
+            self.__dictionariesToDownload = [
+                itm.data(ManageDictionariesDialog.UrlRole)
+                for itm in self.dictionariesList.selectedItems()
+            ]
+            
+            self.__refreshButton.setEnabled(False)
+            self.__installButton.setEnabled(False)
+            self.__uninstallButton.setEnabled(False)
+            self.__cancelButton.setEnabled(True)
+            
+            self.__downloadCancelled = False
+            
+            self.__downloadDictionary()
+    
+    def __downloadDictionary(self):
+        """
+        Private slot to download a dictionary.
+        """
+        if self.__online:
+            if self.__dictionariesToDownload:
+                url = self.__dictionariesToDownload.pop(0)
+                self.statusLabel.setText(url)
+                
+                self.__downloadCancelled = False
+                
+                request = QNetworkRequest(QUrl(url))
+                request.setAttribute(
+                    QNetworkRequest.Attribute.CacheLoadControlAttribute,
+                    QNetworkRequest.CacheLoadControl.AlwaysNetwork)
+                reply = WebBrowserWindow.networkManager().get(request)
+                reply.finished.connect(
+                    lambda: self.__installDictionary(reply))
+                reply.downloadProgress.connect(self.__downloadProgress)
+                self.__replies.append(reply)
+            else:
+                self.__installationFinished()
+        else:
+            EricMessageBox.warning(
+                self,
+                self.tr("Error downloading dictionary file"),
+                self.tr(
+                    """<p>Could not download the requested dictionary file"""
+                    """ from {0}.</p><p>Error: {1}</p>"""
+                ).format(url, self.tr("No connection to Internet.")))
+            
+            self.__installationFinished()
+    
+    def __installDictionary(self, reply):
+        """
+        Private slot to install the downloaded dictionary.
+        
+        @param reply reference to the network reply
+        @type QNetworkReply
+        """
+        if reply in self.__replies:
+            self.__replies.remove(reply)
+        reply.deleteLater()
+        
+        if reply.error() != QNetworkReply.NetworkError.NoError:
+            if not self.__downloadCancelled:
+                EricMessageBox.warning(
+                    self,
+                    self.tr("Error downloading dictionary file"),
+                    self.tr(
+                        """<p>Could not download the requested dictionary"""
+                        """ file from {0}.</p><p>Error: {1}</p>"""
+                    ).format(reply.url(), reply.errorString())
+                )
+            self.downloadProgress.setValue(0)
+            return
+        
+        archiveData = reply.readAll()
+        archiveFile = io.BytesIO(bytes(archiveData))
+        archive = zipfile.ZipFile(archiveFile, "r")
+        if archive.testzip() is not None:
+            EricMessageBox.critical(
+                self,
+                self.tr("Error downloading dictionary"),
+                self.tr(
+                    """<p>The downloaded dictionary archive is invalid."""
+                    """ Skipping it.</p>""")
+            )
+        else:
+            installDir = self.locationComboBox.currentText()
+            archive.extractall(installDir)
+        
+        if self.__dictionariesToDownload:
+            self.__downloadDictionary()
+        else:
+            self.__installationFinished()
+    
+    def __installationFinished(self):
+        """
+        Private method called after all selected dictionaries have been
+        installed.
+        """
+        self.__refreshButton.setEnabled(True)
+        self.__cancelButton.setEnabled(False)
+        
+        self.dictionariesList.clearSelection()
+        self.downloadProgress.setValue(0)
+        
+        self.__checkInstalledDictionaries()
+    
+    def __uninstallSelected(self):
+        """
+        Private method to uninstall the selected dictionaries.
+        """
+        installLocation = self.locationComboBox.currentText()
+        if not installLocation:
+            return
+        
+        itemsToDelete = [
+            itm
+            for itm in self.dictionariesList.selectedItems()
+            if itm.checkState() == Qt.CheckState.Checked
+        ]
+        for itm in itemsToDelete:
+            documentationDir = itm.data(
+                ManageDictionariesDialog.DocumentationDirRole)
+            shutil.rmtree(os.path.join(installLocation, documentationDir),
+                          True)
+            
+            locales = itm.data(ManageDictionariesDialog.LocalesRole)
+            for locale in locales:
+                bdic = os.path.join(installLocation, locale + ".bdic")
+                with contextlib.suppress(OSError):
+                    os.remove(bdic)
+        
+        self.dictionariesList.clearSelection()
+        
+        self.__checkInstalledDictionaries()

eric ide

mercurial