eric6/WebBrowser/SpellCheck/ManageDictionariesDialog.py

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

eric ide

mercurial