Finished implementing a downloader and installer/uninstaller for web browser spell check dictionaries.

Sun, 03 Sep 2017 17:08:26 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sun, 03 Sep 2017 17:08:26 +0200
changeset 5870
82e04c70f969
parent 5869
70709a460358
child 5871
6dccd8542cd1

Finished implementing a downloader and installer/uninstaller for web browser spell check dictionaries.

E5XML/SpellCheckDictionariesReader.py file | annotate | diff | comparison | revisions
E5XML/XMLStreamReaderBase.py file | annotate | diff | comparison | revisions
Preferences/ConfigurationPages/WebBrowserSpellCheckingPage.py file | annotate | diff | comparison | revisions
Preferences/ConfigurationPages/WebBrowserSpellCheckingPage.ui file | annotate | diff | comparison | revisions
WebBrowser/SpellCheck/InstallDictionariesDialog.py file | annotate | diff | comparison | revisions
WebBrowser/SpellCheck/InstallDictionariesDialog.ui file | annotate | diff | comparison | revisions
WebBrowser/SpellCheck/ManageDictionariesDialog.py file | annotate | diff | comparison | revisions
WebBrowser/SpellCheck/ManageDictionariesDialog.ui file | annotate | diff | comparison | revisions
changelog file | annotate | diff | comparison | revisions
eric6.e4p file | annotate | diff | comparison | revisions
--- a/E5XML/SpellCheckDictionariesReader.py	Sun Sep 03 12:22:51 2017 +0200
+++ b/E5XML/SpellCheckDictionariesReader.py	Sun Sep 03 17:08:26 2017 +0200
@@ -34,6 +34,7 @@
         self.__entryCallback = entryCallback
         
         self.version = ""
+        self.baseUrl = ""
     
     def readXML(self):
         """
@@ -46,6 +47,7 @@
                     self.version = self.attribute(
                         "version",
                         dictionariesListFileFormatVersion)
+                    self.baseUrl = self.attribute("baseurl", "")
                     if self.version not in self.supportedVersions:
                         self.raiseUnsupportedFormatVersion(self.version)
                 elif self.name() == "DictionariesUrl":
@@ -63,14 +65,18 @@
         Private method to read the plug-in info.
         """
         dictionaryInfo = {"short": "",
-                      "filename": "",
-                      }
+                          "filename": "",
+                          "documentation": "",
+                          "locales": [],
+                          }
         
         while not self.atEnd():
             self.readNext()
             if self.isEndElement() and self.name() == "Dictionary":
                 self.__entryCallback(
-                    dictionaryInfo["short"], dictionaryInfo["filename"])
+                    dictionaryInfo["short"], dictionaryInfo["filename"],
+                    self.baseUrl + dictionaryInfo["filename"],
+                    dictionaryInfo["documentation"], dictionaryInfo["locales"])
                 break
             
             if self.isStartElement():
@@ -78,5 +84,9 @@
                     dictionaryInfo["short"] = self.readElementText()
                 elif self.name() == "Filename":
                     dictionaryInfo["filename"] = self.readElementText()
+                elif self.name() == "Documentation":
+                    dictionaryInfo["documentation"] = self.readElementText()
+                elif self.name() == "Locales":
+                    dictionaryInfo["locales"] = self.readElementText().split()
                 else:
                     self.raiseUnexpectedStartTag(self.name())
--- a/E5XML/XMLStreamReaderBase.py	Sun Sep 03 12:22:51 2017 +0200
+++ b/E5XML/XMLStreamReaderBase.py	Sun Sep 03 17:08:26 2017 +0200
@@ -50,13 +50,21 @@
         Public method to show an error message.
         """
         if self.hasError():
-            msg = QCoreApplication.translate(
-                "XMLStreamReaderBase",
-                "<p>XML parse error in file <b>{0}</b>, line {1},"
-                " column {2}</p><p>Error: {3}</p>").format(
-                self.device().fileName(),
-                self.lineNumber(), self.columnNumber(),
-                self.errorString())
+            if self.device() is not None:
+                msg = QCoreApplication.translate(
+                    "XMLStreamReaderBase",
+                    "<p>XML parse error in file <b>{0}</b>, line {1},"
+                    " column {2}</p><p>Error: {3}</p>").format(
+                    self.device().fileName(),
+                    self.lineNumber(), self.columnNumber(),
+                    self.errorString())
+            else:
+                msg = QCoreApplication.translate(
+                    "XMLStreamReaderBase",
+                    "<p>XML parse error (line {0},"
+                    " column {1})</p><p>Error: {2}</p>").format(
+                    self.lineNumber(), self.columnNumber(),
+                    self.errorString())
             E5MessageBox.warning(
                 None,
                 QCoreApplication.translate(
--- a/Preferences/ConfigurationPages/WebBrowserSpellCheckingPage.py	Sun Sep 03 12:22:51 2017 +0200
+++ b/Preferences/ConfigurationPages/WebBrowserSpellCheckingPage.py	Sun Sep 03 17:08:26 2017 +0200
@@ -61,12 +61,21 @@
             }
         self.spellCheckDictionaryDirectoriesEdit.setPlainText(
             "\n".join(self.__dictionaryDirectories))
+        # try to create these directories, if they don't exist
+        for directory in self.__dictionaryDirectories:
+            if not os.path.exists(directory):
+                try:
+                    os.makedirs(directory)
+                except os.error:
+                    # ignore it
+                    pass
         
         self.__writeableDirectories = []
         for directory in self.__dictionaryDirectories:
             if os.access(directory, os.W_OK):
                 self.__writeableDirectories.append(directory)
-        self.installButton.setEnabled(bool(self.__writeableDirectories))
+        self.manageDictionariesButton.setEnabled(
+            bool(self.__writeableDirectories))
         
         self.__populateDictionariesList()
     
@@ -109,8 +118,10 @@
             self.noLanguagesLabel.hide()
             self.spellCheckLanguagesList.show()
         else:
+            # no dictionaries available, disable spell checking
             self.noLanguagesLabel.show()
             self.spellCheckLanguagesList.hide()
+            self.spellCheckEnabledCheckBox.setChecked(False)
     
     def save(self):
         """
@@ -158,15 +169,14 @@
         return languageString
     
     @pyqtSlot()
-    def on_installButton_clicked(self):
+    def on_manageDictionariesButton_clicked(self):
         """
-        Private slot to install spell checking dictionaries.
+        Private slot to manage spell checking dictionaries.
         """
-        from WebBrowser.SpellCheck.InstallDictionariesDialog import \
-            InstallDictionariesDialog
-        dlg = InstallDictionariesDialog(self.__writeableDirectories, self)
+        from WebBrowser.SpellCheck.ManageDictionariesDialog import \
+            ManageDictionariesDialog
+        dlg = ManageDictionariesDialog(self.__writeableDirectories, self)
         dlg.exec_()
-        # TODO: Implement this dialog
         
         self.__populateDictionariesList()
 
--- a/Preferences/ConfigurationPages/WebBrowserSpellCheckingPage.ui	Sun Sep 03 12:22:51 2017 +0200
+++ b/Preferences/ConfigurationPages/WebBrowserSpellCheckingPage.ui	Sun Sep 03 17:08:26 2017 +0200
@@ -117,12 +117,12 @@
       </spacer>
      </item>
      <item>
-      <widget class="QPushButton" name="installButton">
+      <widget class="QPushButton" name="manageDictionariesButton">
        <property name="toolTip">
-        <string>Press to open a dialog to install spell checking dictionaries</string>
+        <string>Press to open a dialog to manage spell checking dictionaries</string>
        </property>
        <property name="text">
-        <string>Install Dictionaries...</string>
+        <string>Manage Dictionaries...</string>
        </property>
       </widget>
      </item>
--- a/WebBrowser/SpellCheck/InstallDictionariesDialog.py	Sun Sep 03 12:22:51 2017 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,270 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (c) 2017 Detlev Offenbach <detlev@die-offenbachs.de>
-#
-
-"""
-Module implementing a dialog to install spell checking dictionaries.
-"""
-
-from __future__ import unicode_literals
-
-from PyQt5.QtCore import pyqtSlot, Qt, QUrl
-from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QAbstractButton, \
-    QListWidgetItem
-from PyQt5.QtNetwork import QNetworkConfigurationManager, QNetworkRequest, \
-    QNetworkReply
-
-from E5Gui import E5MessageBox
-
-from .Ui_InstallDictionariesDialog import Ui_InstallDictionariesDialog
-
-from WebBrowser.WebBrowserWindow import WebBrowserWindow
-
-import Preferences
-
-
-class InstallDictionariesDialog(QDialog, Ui_InstallDictionariesDialog):
-    """
-    Class implementing a dialog to install spell checking dictionaries.
-    """
-    FilenameRole = Qt.UserRole
-    
-    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(InstallDictionariesDialog, self).__init__(parent)
-        self.setupUi(self)
-        
-        self.__refreshButton = self.buttonBox.addButton(
-            self.tr("Refresh"), QDialogButtonBox.ActionRole)
-        self.__installButton = self.buttonBox.addButton(
-            self.tr("Install Selected"), QDialogButtonBox.ActionRole)
-        self.__installButton.setEnabled(False)
-        self.__cancelButton = self.buttonBox.addButton(
-            self.tr("Cancel"), QDialogButtonBox.ActionRole)
-        self.__cancelButton.setEnabled(False)
-        
-        self.locationComboBox.addItems(writeableDirectories)
-        
-        self.dictionariesUrlEdit.setText(
-            Preferences.getWebBrowser("SpellCheckDictionariesUrl"))
-        
-        if Preferences.getUI("DynamicOnlineCheck"):
-            self.__networkConfigurationManager = \
-                QNetworkConfigurationManager(self)
-            self.__onlineStateChanged(
-                self.__networkConfigurationManager.isOnline())
-            self.__networkConfigurationManager.onlineStateChanged.connect(
-                self.__onlineStateChanged)
-        else:
-            self.__networkConfigurationManager = None
-            self.__onlineStateChanged(True)
-        self.__replies = []
-        
-        self.__downloadCancelled = False
-        self.__dictionariesToDownload = []
-        
-        self.__populateList()
-    
-    @pyqtSlot(bool)
-    def __onlineStateChanged(self, online):
-        """
-        Private slot handling online state changes.
-        
-        @param online flag indicating the online status
-        @type bool
-        """
-        self.__refreshButton.setEnabled(online)
-        if online:
-            msg = self.tr("Network Status: online")
-        else:
-            msg = self.tr("Network Status: offline")
-        self.statusLabel.setText(msg)
-    
-    def __isOnline(self):
-        """
-        Private method to check the online status.
-        
-        @return flag indicating the online status
-        @rtype bool
-        """
-        if self.__networkConfigurationManager is not None:
-            return self.__networkConfigurationManager.isOnline()
-        else:
-            return True
-    
-    @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()
-    
-    @pyqtSlot()
-    def on_dictionariesList_itemSelectionChanged(self):
-        """
-        Private slot to handle a change of the selection.
-        """
-        self.__installButton.setEnabled(
-            len(self.dictionariesList.selectedItems()) > 0)
-    
-    def __populateList(self):
-        """
-        Private method to populate the list of available plugins.
-        """
-        self.dictionariesList.clear()
-        self.downloadProgress.setValue(0)
-        
-        if self.__isOnline():
-            self.__refreshButton.setEnabled(False)
-            self.__installButton.setEnabled(False)
-            self.__cancelButton.setEnabled(True)
-            
-            url = self.dictionariesUrlEdit.text()
-            self.statusLabel.setText(url)
-            
-            self.__downloadCancelled = False
-            
-            request = QNetworkRequest(QUrl(url))
-            request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
-                                 QNetworkRequest.AlwaysNetwork)
-            reply = WebBrowserWindow.networkManager().get(request)
-            reply.finished.connect(self.__listFileDownloaded)
-            reply.downloadProgress.connect(self.__downloadProgress)
-            self.__replies.append(reply)
-    
-    def __listFileDownloaded(self):
-        """
-        Private method called, after the dictionaries list file has been
-        downloaded from the Internet.
-        """
-        self.__refreshButton.setEnabled(True)
-        self.__cancelButton.setEnabled(False)
-        self.__onlineStateChanged(self.__isOnline())
-        
-        reply = self.sender()
-        if reply in self.__replies:
-            self.__replies.remove(reply)
-        if reply.error() != QNetworkReply.NoError:
-            if not self.__downloadCancelled:
-                E5MessageBox.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.repositoryUrlEdit.text(),
-                             reply.errorString())
-                )
-            self.downloadProgress.setValue(0)
-            reply.deleteLater()
-            return
-        
-        listFileData = reply.readAll()
-        reply.deleteLater()
-        
-        # extract the dictionaries
-        from E5XML.SpellCheckDictionariesReader import \
-            SpellCheckDictionariesReader
-        reader = SpellCheckDictionariesReader(listFileData, self.addEntry)
-        reader.readXML()
-        url = Preferences.getWebBrowser("SpellCheckDictionariesUrl")
-        if url != self.dictionariesUrlEdit.text():
-            self.dictionariesUrlEdit.setText(url)
-            E5MessageBox.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."""))
-    
-    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):
-        """
-        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
-        """
-        itm = QListWidgetItem(short, self.dictionariesList)
-        itm.setData(InstallDictionariesDialog.FilenameRole, filename)
-    
-    def __installSelected(self):
-        """
-        Private method to install the selected dictionaries.
-        """
-        self.__dictionariesToDownload = [
-            itm.data(InstallDictionariesDialog.FilenameRole)
-            for itm in self.dictionariesList.selectedItems()
-        ]
-        
-        self.__refreshButton.setEnabled(False)
-        self.__installButton.setEnabled(False)
-        self.__cancelButton.setEnabled(True)
-        
-        self.__downloadCancelled = False
-        
-        self.__downloadDictionary()
-    
-    def __downloadDictionary(self):
-        """
-        Private slot to download a dictionary.
-        """
-        # TODO: implement this
-        # use __installDictionary as finish slot
-    
-    def __installDictionary(self):
-        """
-        Private slot to install the downloaded dictionary.
-        """
-        # TODO: implement this
-        
-        if not bool(self.__dictionariesToDownload):
-            self.__installationFinished()
-    
-    def __installationFinished(self):
-        """
-        Private method called after all selected dictionaries have been
-        installed.
-        """
-        # TODO: implement this
--- a/WebBrowser/SpellCheck/InstallDictionariesDialog.ui	Sun Sep 03 12:22:51 2017 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,171 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>InstallDictionariesDialog</class>
- <widget class="QDialog" name="InstallDictionariesDialog">
-  <property name="geometry">
-   <rect>
-    <x>0</x>
-    <y>0</y>
-    <width>676</width>
-    <height>653</height>
-   </rect>
-  </property>
-  <property name="windowTitle">
-   <string>Spellcheck Dictionaries</string>
-  </property>
-  <property name="sizeGripEnabled">
-   <bool>true</bool>
-  </property>
-  <layout class="QVBoxLayout" name="verticalLayout">
-   <item>
-    <layout class="QHBoxLayout" name="horizontalLayout_2">
-     <item>
-      <widget class="QLabel" name="label">
-       <property name="text">
-        <string>Installation Location:</string>
-       </property>
-      </widget>
-     </item>
-     <item>
-      <widget class="QComboBox" name="locationComboBox">
-       <property name="sizePolicy">
-        <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
-         <horstretch>0</horstretch>
-         <verstretch>0</verstretch>
-        </sizepolicy>
-       </property>
-       <property name="toolTip">
-        <string>Select the location for the dictionaries installation</string>
-       </property>
-      </widget>
-     </item>
-    </layout>
-   </item>
-   <item>
-    <widget class="QListWidget" name="dictionariesList">
-     <property name="toolTip">
-      <string>Shows the list of available dictionaries</string>
-     </property>
-     <property name="alternatingRowColors">
-      <bool>true</bool>
-     </property>
-     <property name="selectionMode">
-      <enum>QAbstractItemView::ExtendedSelection</enum>
-     </property>
-     <property name="sortingEnabled">
-      <bool>true</bool>
-     </property>
-    </widget>
-   </item>
-   <item>
-    <widget class="Line" name="line">
-     <property name="orientation">
-      <enum>Qt::Horizontal</enum>
-     </property>
-    </widget>
-   </item>
-   <item>
-    <widget class="QProgressBar" name="downloadProgress">
-     <property name="toolTip">
-      <string>Shows the progress of the current download</string>
-     </property>
-     <property name="value">
-      <number>0</number>
-     </property>
-    </widget>
-   </item>
-   <item>
-    <widget class="QLabel" name="statusLabel">
-     <property name="text">
-      <string/>
-     </property>
-    </widget>
-   </item>
-   <item>
-    <layout class="QHBoxLayout" name="horizontalLayout">
-     <item>
-      <widget class="QLabel" name="label_4">
-       <property name="text">
-        <string>Dictionaries URL:</string>
-       </property>
-      </widget>
-     </item>
-     <item>
-      <widget class="QLineEdit" name="dictionariesUrlEdit">
-       <property name="toolTip">
-        <string>Shows the dictionaries URL</string>
-       </property>
-       <property name="readOnly">
-        <bool>true</bool>
-       </property>
-      </widget>
-     </item>
-     <item>
-      <widget class="QPushButton" name="dictionariesUrlEditButton">
-       <property name="toolTip">
-        <string>Press to edit the dictionaries URL</string>
-       </property>
-       <property name="text">
-        <string>Edit URL</string>
-       </property>
-       <property name="checkable">
-        <bool>true</bool>
-       </property>
-      </widget>
-     </item>
-    </layout>
-   </item>
-   <item>
-    <widget class="QDialogButtonBox" name="buttonBox">
-     <property name="orientation">
-      <enum>Qt::Horizontal</enum>
-     </property>
-     <property name="standardButtons">
-      <set>QDialogButtonBox::Close</set>
-     </property>
-    </widget>
-   </item>
-  </layout>
- </widget>
- <tabstops>
-  <tabstop>locationComboBox</tabstop>
-  <tabstop>dictionariesList</tabstop>
-  <tabstop>dictionariesUrlEdit</tabstop>
-  <tabstop>dictionariesUrlEditButton</tabstop>
- </tabstops>
- <resources/>
- <connections>
-  <connection>
-   <sender>buttonBox</sender>
-   <signal>accepted()</signal>
-   <receiver>InstallDictionariesDialog</receiver>
-   <slot>accept()</slot>
-   <hints>
-    <hint type="sourcelabel">
-     <x>248</x>
-     <y>254</y>
-    </hint>
-    <hint type="destinationlabel">
-     <x>157</x>
-     <y>274</y>
-    </hint>
-   </hints>
-  </connection>
-  <connection>
-   <sender>buttonBox</sender>
-   <signal>rejected()</signal>
-   <receiver>InstallDictionariesDialog</receiver>
-   <slot>reject()</slot>
-   <hints>
-    <hint type="sourcelabel">
-     <x>316</x>
-     <y>260</y>
-    </hint>
-    <hint type="destinationlabel">
-     <x>286</x>
-     <y>274</y>
-    </hint>
-   </hints>
-  </connection>
- </connections>
-</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WebBrowser/SpellCheck/ManageDictionariesDialog.py	Sun Sep 03 17:08:26 2017 +0200
@@ -0,0 +1,487 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2017 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog to install spell checking dictionaries.
+"""
+
+from __future__ import unicode_literals
+
+import os
+import io
+import zipfile
+import glob
+import shutil
+
+from PyQt5.QtCore import pyqtSlot, Qt, QUrl
+from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QAbstractButton, \
+    QListWidgetItem
+from PyQt5.QtNetwork import QNetworkConfigurationManager, QNetworkRequest, \
+    QNetworkReply
+
+from E5Gui import E5MessageBox
+
+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.UserRole
+    UrlRole = Qt.UserRole + 1
+    DocumentationDirRole = Qt.UserRole + 2
+    LocalesRole = Qt.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(ManageDictionariesDialog, self).__init__(parent)
+        self.setupUi(self)
+        
+        self.__refreshButton = self.buttonBox.addButton(
+            self.tr("Refresh"), QDialogButtonBox.ActionRole)
+        self.__installButton = self.buttonBox.addButton(
+            self.tr("Install Selected"), QDialogButtonBox.ActionRole)
+        self.__installButton.setEnabled(False)
+        self.__uninstallButton = self.buttonBox.addButton(
+            self.tr("Uninstall Selected"), QDialogButtonBox.ActionRole)
+        self.__uninstallButton.setEnabled(False)
+        self.__cancelButton = self.buttonBox.addButton(
+            self.tr("Cancel"), QDialogButtonBox.ActionRole)
+        self.__cancelButton.setEnabled(False)
+        
+        self.locationComboBox.addItems(writeableDirectories)
+        
+        self.dictionariesUrlEdit.setText(
+            Preferences.getWebBrowser("SpellCheckDictionariesUrl"))
+        
+        if Preferences.getUI("DynamicOnlineCheck"):
+            self.__networkConfigurationManager = \
+                QNetworkConfigurationManager(self)
+            self.__onlineStateChanged(
+                self.__networkConfigurationManager.isOnline())
+            self.__networkConfigurationManager.onlineStateChanged.connect(
+                self.__onlineStateChanged)
+        else:
+            self.__networkConfigurationManager = None
+            self.__onlineStateChanged(True)
+        self.__replies = []
+        
+        self.__downloadCancelled = False
+        self.__dictionariesToDownload = []
+        
+        self.__populateList()
+    
+    @pyqtSlot(bool)
+    def __onlineStateChanged(self, online):
+        """
+        Private slot handling online state changes.
+        
+        @param online flag indicating the online status
+        @type bool
+        """
+        self.__refreshButton.setEnabled(online)
+        
+        if online:
+            msg = self.tr("Network Status: online")
+        else:
+            msg = self.tr("Network Status: offline")
+        self.statusLabel.setText(msg)
+        
+        self.on_dictionariesList_itemSelectionChanged()
+    
+    def __isOnline(self):
+        """
+        Private method to check the online status.
+        
+        @return flag indicating the online status
+        @rtype bool
+        """
+        if self.__networkConfigurationManager is not None:
+            return self.__networkConfigurationManager.isOnline()
+        else:
+            return True
+    
+    @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.__isOnline()
+        )
+        
+        self.__uninstallButton.setEnabled(
+            self.locationComboBox.count() > 0 and
+            len([itm
+                 for itm in self.dictionariesList.selectedItems()
+                 if itm.checkState() == Qt.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.__isOnline():
+            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.CacheLoadControlAttribute,
+                                 QNetworkRequest.AlwaysNetwork)
+            reply = WebBrowserWindow.networkManager().get(request)
+            reply.finished.connect(self.__listFileDownloaded)
+            reply.downloadProgress.connect(self.__downloadProgress)
+            self.__replies.append(reply)
+        else:
+            E5MessageBox.warning(
+                self,
+                self.tr("Error populating list of dictionaries"),
+                self.tr(
+                    """<p>Could not download the dictionaries list file"""
+                    """ from {0}.</p><p>Error: {1}</p>"""
+                ).format(url, self.tr("Computer is offline.")))
+    
+    def __listFileDownloaded(self):
+        """
+        Private method called, after the dictionaries list file has been
+        downloaded from the Internet.
+        """
+        self.__refreshButton.setEnabled(True)
+        self.__cancelButton.setEnabled(False)
+        self.__onlineStateChanged(self.__isOnline())
+        
+        self.downloadProgress.setValue(0)
+        
+        reply = self.sender()
+        if reply in self.__replies:
+            self.__replies.remove(reply)
+        reply.deleteLater()
+        
+        if reply.error() != QNetworkReply.NoError:
+            if not self.__downloadCancelled:
+                E5MessageBox.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.repositoryUrlEdit.text(),
+                             reply.errorString())
+                )
+            self.downloadProgress.setValue(0)
+            return
+        
+        listFileData = reply.readAll()
+        
+        # extract the dictionaries
+        from E5XML.SpellCheckDictionariesReader import \
+            SpellCheckDictionariesReader
+        reader = SpellCheckDictionariesReader(listFileData, self.addEntry)
+        reader.readXML()
+        url = Preferences.getWebBrowser("SpellCheckDictionariesUrl")
+        if url != self.dictionariesUrlEdit.text():
+            self.dictionariesUrlEdit.setText(url)
+            E5MessageBox.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
+            E5MessageBox.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.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 = set([
+                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.Checked)
+                else:
+                    itm.setCheckState(Qt.Unchecked)
+        else:
+            for row in range(self.dictionariesList.count()):
+                itm = self.dictionariesList.item(row)
+                itm.setCheckState(Qt.Unchecked)
+    
+    def __installSelected(self):
+        """
+        Private method to install the selected dictionaries.
+        """
+        if self.__isOnline() 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.__isOnline():
+            if self.__dictionariesToDownload:
+                url = self.__dictionariesToDownload.pop(0)
+                self.statusLabel.setText(url)
+                
+                self.__downloadCancelled = False
+                
+                request = QNetworkRequest(QUrl(url))
+                request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
+                                     QNetworkRequest.AlwaysNetwork)
+                reply = WebBrowserWindow.networkManager().get(request)
+                reply.finished.connect(self.__installDictionary)
+                reply.downloadProgress.connect(self.__downloadProgress)
+                self.__replies.append(reply)
+            else:
+                self.__installationFinished()
+        else:
+            E5MessageBox.warning(
+                self,
+                self.tr("Error downloading dictionary"),
+                self.tr(
+                    """<p>Could not download the requested dictionary file"""
+                    """ from {0}.</p><p>Error: {1}</p>"""
+                ).format(url, self.tr("Computer is offline.")))
+            
+            self.__installationFinished()
+    
+    def __installDictionary(self):
+        """
+        Private slot to install the downloaded dictionary.
+        """
+        reply = self.sender()
+        if reply in self.__replies:
+            self.__replies.remove(reply)
+        reply.deleteLater()
+        
+        if reply.error() != QNetworkReply.NoError:
+            if not self.__downloadCancelled:
+                E5MessageBox.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:
+            E5MessageBox.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.__onlineStateChanged(self.__isOnline())
+        
+        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.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")
+                try:
+                    os.remove(bdic)
+                except OSError:
+                    # ignore silently
+                    pass
+        
+        self.dictionariesList.clearSelection()
+        
+        self.__checkInstalledDictionaries()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WebBrowser/SpellCheck/ManageDictionariesDialog.ui	Sun Sep 03 17:08:26 2017 +0200
@@ -0,0 +1,171 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ManageDictionariesDialog</class>
+ <widget class="QDialog" name="ManageDictionariesDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>676</width>
+    <height>653</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Spell Check Dictionaries</string>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>true</bool>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_2">
+     <item>
+      <widget class="QLabel" name="label">
+       <property name="text">
+        <string>Installation Location:</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QComboBox" name="locationComboBox">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="toolTip">
+        <string>Select the location for the dictionaries installation</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QListWidget" name="dictionariesList">
+     <property name="toolTip">
+      <string>Shows the list of available dictionaries</string>
+     </property>
+     <property name="alternatingRowColors">
+      <bool>true</bool>
+     </property>
+     <property name="selectionMode">
+      <enum>QAbstractItemView::ExtendedSelection</enum>
+     </property>
+     <property name="sortingEnabled">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="Line" name="line">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QProgressBar" name="downloadProgress">
+     <property name="toolTip">
+      <string>Shows the progress of the current download</string>
+     </property>
+     <property name="value">
+      <number>0</number>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLabel" name="statusLabel">
+     <property name="text">
+      <string/>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QLabel" name="label_4">
+       <property name="text">
+        <string>Dictionaries URL:</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QLineEdit" name="dictionariesUrlEdit">
+       <property name="toolTip">
+        <string>Shows the dictionaries URL</string>
+       </property>
+       <property name="readOnly">
+        <bool>true</bool>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="dictionariesUrlEditButton">
+       <property name="toolTip">
+        <string>Press to edit the dictionaries URL</string>
+       </property>
+       <property name="text">
+        <string>Edit URL</string>
+       </property>
+       <property name="checkable">
+        <bool>true</bool>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Close</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <tabstops>
+  <tabstop>locationComboBox</tabstop>
+  <tabstop>dictionariesList</tabstop>
+  <tabstop>dictionariesUrlEdit</tabstop>
+  <tabstop>dictionariesUrlEditButton</tabstop>
+ </tabstops>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>ManageDictionariesDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>ManageDictionariesDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
--- a/changelog	Sun Sep 03 12:22:51 2017 +0200
+++ b/changelog	Sun Sep 03 17:08:26 2017 +0200
@@ -2,6 +2,9 @@
 ----------
 Version 17.10:
 - bug fixes
+- Web Browser (NG)
+  -- added a downloader and installer/uninstaller dialog for spell check
+     dictionaries
 
 Version 17.09:
 - bug fixes
--- a/eric6.e4p	Sun Sep 03 12:22:51 2017 +0200
+++ b/eric6.e4p	Sun Sep 03 17:08:26 2017 +0200
@@ -1428,7 +1428,7 @@
     <Source>WebBrowser/SpeedDial/SpeedDialReader.py</Source>
     <Source>WebBrowser/SpeedDial/SpeedDialWriter.py</Source>
     <Source>WebBrowser/SpeedDial/__init__.py</Source>
-    <Source>WebBrowser/SpellCheck/InstallDictionariesDialog.py</Source>
+    <Source>WebBrowser/SpellCheck/ManageDictionariesDialog.py</Source>
     <Source>WebBrowser/SpellCheck/__init__.py</Source>
     <Source>WebBrowser/StatusBar/ImagesIcon.py</Source>
     <Source>WebBrowser/StatusBar/JavaScriptIcon.py</Source>
@@ -1951,7 +1951,7 @@
     <Form>WebBrowser/SearchWidget.ui</Form>
     <Form>WebBrowser/Session/SessionManagerDialog.ui</Form>
     <Form>WebBrowser/SiteInfo/SiteInfoDialog.ui</Form>
-    <Form>WebBrowser/SpellCheck/InstallDictionariesDialog.ui</Form>
+    <Form>WebBrowser/SpellCheck/ManageDictionariesDialog.ui</Form>
     <Form>WebBrowser/StatusBar/JavaScriptSettingsDialog.ui</Form>
     <Form>WebBrowser/Sync/SyncCheckPage.ui</Form>
     <Form>WebBrowser/Sync/SyncDataPage.ui</Form>

eric ide

mercurial