--- /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()