eric7/WebBrowser/SpellCheck/ManageDictionariesDialog.py

branch
eric7
changeset 8312
800c432b34c8
parent 8240
93b8a353c4bf
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2017 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog to install spell checking dictionaries.
8 """
9
10 import os
11 import io
12 import zipfile
13 import glob
14 import shutil
15 import contextlib
16
17 from PyQt5.QtCore import pyqtSlot, Qt, QUrl
18 from PyQt5.QtWidgets import (
19 QDialog, QDialogButtonBox, QAbstractButton, QListWidgetItem
20 )
21 from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
22
23 from E5Gui import E5MessageBox
24
25 from .Ui_ManageDictionariesDialog import Ui_ManageDictionariesDialog
26
27 from WebBrowser.WebBrowserWindow import WebBrowserWindow
28
29 import Preferences
30
31
32 class ManageDictionariesDialog(QDialog, Ui_ManageDictionariesDialog):
33 """
34 Class implementing a dialog to install spell checking dictionaries.
35 """
36 FilenameRole = Qt.ItemDataRole.UserRole
37 UrlRole = Qt.ItemDataRole.UserRole + 1
38 DocumentationDirRole = Qt.ItemDataRole.UserRole + 2
39 LocalesRole = Qt.ItemDataRole.UserRole + 3
40
41 def __init__(self, writeableDirectories, parent=None):
42 """
43 Constructor
44
45 @param writeableDirectories list of writable directories
46 @type list of str
47 @param parent reference to the parent widget
48 @type QWidget
49 """
50 super().__init__(parent)
51 self.setupUi(self)
52
53 self.__refreshButton = self.buttonBox.addButton(
54 self.tr("Refresh"), QDialogButtonBox.ButtonRole.ActionRole)
55 self.__installButton = self.buttonBox.addButton(
56 self.tr("Install Selected"),
57 QDialogButtonBox.ButtonRole.ActionRole)
58 self.__installButton.setEnabled(False)
59 self.__uninstallButton = self.buttonBox.addButton(
60 self.tr("Uninstall Selected"),
61 QDialogButtonBox.ButtonRole.ActionRole)
62 self.__uninstallButton.setEnabled(False)
63 self.__cancelButton = self.buttonBox.addButton(
64 self.tr("Cancel"), QDialogButtonBox.ButtonRole.ActionRole)
65 self.__cancelButton.setEnabled(False)
66
67 self.locationComboBox.addItems(writeableDirectories)
68
69 self.dictionariesUrlEdit.setText(
70 Preferences.getWebBrowser("SpellCheckDictionariesUrl"))
71
72 self.__replies = []
73
74 self.__downloadCancelled = False
75 self.__dictionariesToDownload = []
76
77 self.__populateList()
78
79 @pyqtSlot(QAbstractButton)
80 def on_buttonBox_clicked(self, button):
81 """
82 Private slot to handle the click of a button of the button box.
83
84 @param button reference to the button pressed
85 @type QAbstractButton
86 """
87 if button == self.__refreshButton:
88 self.__populateList()
89 elif button == self.__cancelButton:
90 self.__downloadCancel()
91 elif button == self.__installButton:
92 self.__installSelected()
93 elif button == self.__uninstallButton:
94 self.__uninstallSelected()
95
96 @pyqtSlot()
97 def on_dictionariesList_itemSelectionChanged(self):
98 """
99 Private slot to handle a change of the selection.
100 """
101 self.__installButton.setEnabled(
102 self.locationComboBox.count() > 0 and
103 len(self.dictionariesList.selectedItems()) > 0
104 )
105
106 self.__uninstallButton.setEnabled(
107 self.locationComboBox.count() > 0 and
108 len([itm
109 for itm in self.dictionariesList.selectedItems()
110 if itm.checkState() == Qt.CheckState.Checked
111 ])
112 )
113
114 @pyqtSlot(bool)
115 def on_dictionariesUrlEditButton_toggled(self, checked):
116 """
117 Private slot to set the read only status of the dictionaries URL line
118 edit.
119
120 @param checked state of the push button (boolean)
121 """
122 self.dictionariesUrlEdit.setReadOnly(not checked)
123
124 @pyqtSlot(str)
125 def on_locationComboBox_currentTextChanged(self, txt):
126 """
127 Private slot to handle a change of the installation location.
128
129 @param txt installation location
130 @type str
131 """
132 self.__checkInstalledDictionaries()
133
134 def __populateList(self):
135 """
136 Private method to populate the list of available plugins.
137 """
138 self.dictionariesList.clear()
139 self.downloadProgress.setValue(0)
140
141 url = self.dictionariesUrlEdit.text()
142
143 self.__refreshButton.setEnabled(False)
144 self.__installButton.setEnabled(False)
145 self.__uninstallButton.setEnabled(False)
146 self.__cancelButton.setEnabled(True)
147
148 self.statusLabel.setText(url)
149
150 self.__downloadCancelled = False
151
152 request = QNetworkRequest(QUrl(url))
153 request.setAttribute(
154 QNetworkRequest.Attribute.CacheLoadControlAttribute,
155 QNetworkRequest.CacheLoadControl.AlwaysNetwork)
156 reply = WebBrowserWindow.networkManager().get(request)
157 reply.finished.connect(
158 lambda: self.__listFileDownloaded(reply))
159 reply.downloadProgress.connect(self.__downloadProgress)
160 self.__replies.append(reply)
161
162 def __listFileDownloaded(self, reply):
163 """
164 Private method called, after the dictionaries list file has been
165 downloaded from the Internet.
166
167 @param reply reference to the network reply
168 @type QNetworkReply
169 """
170 self.__refreshButton.setEnabled(True)
171 self.__cancelButton.setEnabled(False)
172
173 self.downloadProgress.setValue(0)
174
175 if reply in self.__replies:
176 self.__replies.remove(reply)
177 reply.deleteLater()
178
179 if reply.error() != QNetworkReply.NetworkError.NoError:
180 if not self.__downloadCancelled:
181 E5MessageBox.warning(
182 self,
183 self.tr("Error downloading dictionaries list"),
184 self.tr(
185 """<p>Could not download the dictionaries list"""
186 """ from {0}.</p><p>Error: {1}</p>"""
187 ).format(self.dictionariesUrlEdit.text(),
188 reply.errorString())
189 )
190 self.downloadProgress.setValue(0)
191 return
192
193 listFileData = reply.readAll()
194
195 # extract the dictionaries
196 from E5XML.SpellCheckDictionariesReader import (
197 SpellCheckDictionariesReader
198 )
199 reader = SpellCheckDictionariesReader(listFileData, self.addEntry)
200 reader.readXML()
201 url = Preferences.getWebBrowser("SpellCheckDictionariesUrl")
202 if url != self.dictionariesUrlEdit.text():
203 self.dictionariesUrlEdit.setText(url)
204 E5MessageBox.warning(
205 self,
206 self.tr("Dictionaries URL Changed"),
207 self.tr(
208 """The URL of the spell check dictionaries has"""
209 """ changed. Select the "Refresh" button to get"""
210 """ the new dictionaries list."""
211 )
212 )
213
214 if self.locationComboBox.count() == 0:
215 # no writable locations available
216 E5MessageBox.warning(
217 self,
218 self.tr("Error installing dictionaries"),
219 self.tr(
220 """<p>None of the dictionary locations is writable by"""
221 """ you. Please download required dictionaries manually"""
222 """ and install them as administrator.</p>"""
223 )
224 )
225
226 self.__checkInstalledDictionaries()
227
228 def __downloadCancel(self):
229 """
230 Private slot to cancel the current download.
231 """
232 if self.__replies:
233 reply = self.__replies[0]
234 self.__downloadCancelled = True
235 self.__dictionariesToDownload = []
236 reply.abort()
237
238 def __downloadProgress(self, done, total):
239 """
240 Private slot to show the download progress.
241
242 @param done number of bytes downloaded so far
243 @type int
244 @param total total bytes to be downloaded
245 @type int
246 """
247 if total:
248 self.downloadProgress.setMaximum(total)
249 self.downloadProgress.setValue(done)
250
251 def addEntry(self, short, filename, url, documentationDir, locales):
252 """
253 Public method to add an entry to the list.
254
255 @param short data for the description field
256 @type str
257 @param filename data for the filename field
258 @type str
259 @param url download URL for the dictionary entry
260 @type str
261 @param documentationDir name of the directory containing the
262 dictionary documentation
263 @type str
264 @param locales list of locales
265 @type list of str
266 """
267 itm = QListWidgetItem(
268 self.tr("{0} ({1})").format(short, " ".join(locales)),
269 self.dictionariesList)
270 itm.setCheckState(Qt.CheckState.Unchecked)
271
272 itm.setData(ManageDictionariesDialog.FilenameRole, filename)
273 itm.setData(ManageDictionariesDialog.UrlRole, url)
274 itm.setData(ManageDictionariesDialog.DocumentationDirRole,
275 documentationDir)
276 itm.setData(ManageDictionariesDialog.LocalesRole, locales)
277
278 def __checkInstalledDictionaries(self):
279 """
280 Private method to check all installed dictionaries.
281
282 Note: A dictionary is assumed to be installed, if at least one of its
283 binary dictionaries (*.bdic) is found in the selected dictionaries
284 location.
285 """
286 if self.locationComboBox.currentText():
287 installedLocales = {
288 os.path.splitext(os.path.basename(dic))[0]
289 for dic in glob.glob(
290 os.path.join(self.locationComboBox.currentText(), "*.bdic")
291 )
292 }
293
294 for row in range(self.dictionariesList.count()):
295 itm = self.dictionariesList.item(row)
296 locales = set(itm.data(ManageDictionariesDialog.LocalesRole))
297 if locales.intersection(installedLocales):
298 itm.setCheckState(Qt.CheckState.Checked)
299 else:
300 itm.setCheckState(Qt.CheckState.Unchecked)
301 else:
302 for row in range(self.dictionariesList.count()):
303 itm = self.dictionariesList.item(row)
304 itm.setCheckState(Qt.CheckState.Unchecked)
305
306 def __installSelected(self):
307 """
308 Private method to install the selected dictionaries.
309 """
310 if bool(self.locationComboBox.currentText()):
311 self.__dictionariesToDownload = [
312 itm.data(ManageDictionariesDialog.UrlRole)
313 for itm in self.dictionariesList.selectedItems()
314 ]
315
316 self.__refreshButton.setEnabled(False)
317 self.__installButton.setEnabled(False)
318 self.__uninstallButton.setEnabled(False)
319 self.__cancelButton.setEnabled(True)
320
321 self.__downloadCancelled = False
322
323 self.__downloadDictionary()
324
325 def __downloadDictionary(self):
326 """
327 Private slot to download a dictionary.
328 """
329 if self.__dictionariesToDownload:
330 url = self.__dictionariesToDownload.pop(0)
331 self.statusLabel.setText(url)
332
333 self.__downloadCancelled = False
334
335 request = QNetworkRequest(QUrl(url))
336 request.setAttribute(
337 QNetworkRequest.Attribute.CacheLoadControlAttribute,
338 QNetworkRequest.CacheLoadControl.AlwaysNetwork)
339 reply = WebBrowserWindow.networkManager().get(request)
340 reply.finished.connect(
341 lambda: self.__installDictionary(reply))
342 reply.downloadProgress.connect(self.__downloadProgress)
343 self.__replies.append(reply)
344 else:
345 self.__installationFinished()
346
347 self.__installationFinished()
348
349 def __installDictionary(self, reply):
350 """
351 Private slot to install the downloaded dictionary.
352
353 @param reply reference to the network reply
354 @type QNetworkReply
355 """
356 if reply in self.__replies:
357 self.__replies.remove(reply)
358 reply.deleteLater()
359
360 if reply.error() != QNetworkReply.NetworkError.NoError:
361 if not self.__downloadCancelled:
362 E5MessageBox.warning(
363 self,
364 self.tr("Error downloading dictionary file"),
365 self.tr(
366 """<p>Could not download the requested dictionary"""
367 """ file from {0}.</p><p>Error: {1}</p>"""
368 ).format(reply.url(), reply.errorString())
369 )
370 self.downloadProgress.setValue(0)
371 return
372
373 archiveData = reply.readAll()
374 archiveFile = io.BytesIO(bytes(archiveData))
375 archive = zipfile.ZipFile(archiveFile, "r")
376 if archive.testzip() is not None:
377 E5MessageBox.critical(
378 self,
379 self.tr("Error downloading dictionary"),
380 self.tr(
381 """<p>The downloaded dictionary archive is invalid."""
382 """ Skipping it.</p>""")
383 )
384 else:
385 installDir = self.locationComboBox.currentText()
386 archive.extractall(installDir)
387
388 if self.__dictionariesToDownload:
389 self.__downloadDictionary()
390 else:
391 self.__installationFinished()
392
393 def __installationFinished(self):
394 """
395 Private method called after all selected dictionaries have been
396 installed.
397 """
398 self.__refreshButton.setEnabled(True)
399 self.__cancelButton.setEnabled(False)
400
401 self.dictionariesList.clearSelection()
402 self.downloadProgress.setValue(0)
403
404 self.__checkInstalledDictionaries()
405
406 def __uninstallSelected(self):
407 """
408 Private method to uninstall the selected dictionaries.
409 """
410 installLocation = self.locationComboBox.currentText()
411 if not installLocation:
412 return
413
414 itemsToDelete = [
415 itm
416 for itm in self.dictionariesList.selectedItems()
417 if itm.checkState() == Qt.CheckState.Checked
418 ]
419 for itm in itemsToDelete:
420 documentationDir = itm.data(
421 ManageDictionariesDialog.DocumentationDirRole)
422 shutil.rmtree(os.path.join(installLocation, documentationDir),
423 True)
424
425 locales = itm.data(ManageDictionariesDialog.LocalesRole)
426 for locale in locales:
427 bdic = os.path.join(installLocation, locale + ".bdic")
428 with contextlib.suppress(OSError):
429 os.remove(bdic)
430
431 self.dictionariesList.clearSelection()
432
433 self.__checkInstalledDictionaries()

eric ide

mercurial