src/eric7/WebBrowser/SpellCheck/ManageDictionariesDialog.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8881
54e42bc2437a
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2017 - 2022 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 PyQt6.QtCore import pyqtSlot, Qt, QUrl
18 from PyQt6.QtWidgets import (
19 QDialog, QDialogButtonBox, QAbstractButton, QListWidgetItem
20 )
21 from PyQt6.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkInformation
22
23 from EricWidgets import EricMessageBox
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 if (
73 Preferences.getUI("DynamicOnlineCheck") and
74 QNetworkInformation.load(QNetworkInformation.Feature.Reachability)
75 ):
76 self.__reachabilityChanged(
77 QNetworkInformation.instance().reachability())
78 QNetworkInformation.instance().reachabilityChanged.connect(
79 self.__reachabilityChanged)
80 else:
81 # assume to be 'always online' if no backend could be loaded or
82 # dynamic online check is switched of
83 self.__reachabilityChanged(QNetworkInformation.Reachability.Online)
84 self.__replies = []
85
86 self.__downloadCancelled = False
87 self.__dictionariesToDownload = []
88
89 self.__populateList()
90
91 def __reachabilityChanged(self, reachability):
92 """
93 Private slot handling reachability state changes.
94
95 @param reachability new reachability state
96 @type QNetworkInformation.Reachability
97 """
98 online = reachability == QNetworkInformation.Reachability.Online
99 self.__online = online
100
101 self.__refreshButton.setEnabled(online)
102
103 msg = (
104 self.tr("Internet Reachability Status: Reachable")
105 if online else
106 self.tr("Internet Reachability Status: Not Reachable")
107 )
108 self.statusLabel.setText(msg)
109
110 self.on_dictionariesList_itemSelectionChanged()
111
112 @pyqtSlot(QAbstractButton)
113 def on_buttonBox_clicked(self, button):
114 """
115 Private slot to handle the click of a button of the button box.
116
117 @param button reference to the button pressed
118 @type QAbstractButton
119 """
120 if button == self.__refreshButton:
121 self.__populateList()
122 elif button == self.__cancelButton:
123 self.__downloadCancel()
124 elif button == self.__installButton:
125 self.__installSelected()
126 elif button == self.__uninstallButton:
127 self.__uninstallSelected()
128
129 @pyqtSlot()
130 def on_dictionariesList_itemSelectionChanged(self):
131 """
132 Private slot to handle a change of the selection.
133 """
134 self.__installButton.setEnabled(
135 self.locationComboBox.count() > 0 and
136 len(self.dictionariesList.selectedItems()) > 0 and
137 self.__online
138 )
139
140 self.__uninstallButton.setEnabled(
141 self.locationComboBox.count() > 0 and
142 len([itm
143 for itm in self.dictionariesList.selectedItems()
144 if itm.checkState() == Qt.CheckState.Checked
145 ])
146 )
147
148 @pyqtSlot(bool)
149 def on_dictionariesUrlEditButton_toggled(self, checked):
150 """
151 Private slot to set the read only status of the dictionaries URL line
152 edit.
153
154 @param checked state of the push button (boolean)
155 """
156 self.dictionariesUrlEdit.setReadOnly(not checked)
157
158 @pyqtSlot(str)
159 def on_locationComboBox_currentTextChanged(self, txt):
160 """
161 Private slot to handle a change of the installation location.
162
163 @param txt installation location
164 @type str
165 """
166 self.__checkInstalledDictionaries()
167
168 def __populateList(self):
169 """
170 Private method to populate the list of available plugins.
171 """
172 self.dictionariesList.clear()
173 self.downloadProgress.setValue(0)
174
175 url = self.dictionariesUrlEdit.text()
176
177 if self.__online:
178 self.__refreshButton.setEnabled(False)
179 self.__installButton.setEnabled(False)
180 self.__uninstallButton.setEnabled(False)
181 self.__cancelButton.setEnabled(True)
182
183 self.statusLabel.setText(url)
184
185 self.__downloadCancelled = False
186
187 request = QNetworkRequest(QUrl(url))
188 request.setAttribute(
189 QNetworkRequest.Attribute.CacheLoadControlAttribute,
190 QNetworkRequest.CacheLoadControl.AlwaysNetwork)
191 reply = WebBrowserWindow.networkManager().get(request)
192 reply.finished.connect(
193 lambda: self.__listFileDownloaded(reply))
194 reply.downloadProgress.connect(self.__downloadProgress)
195 self.__replies.append(reply)
196 else:
197 EricMessageBox.warning(
198 self,
199 self.tr("Error populating list of dictionaries"),
200 self.tr(
201 """<p>Could not download the dictionaries list"""
202 """ from {0}.</p><p>Error: {1}</p>"""
203 ).format(url, self.tr("No connection to Internet.")))
204
205 def __listFileDownloaded(self, reply):
206 """
207 Private method called, after the dictionaries list file has been
208 downloaded from the Internet.
209
210 @param reply reference to the network reply
211 @type QNetworkReply
212 """
213 self.__refreshButton.setEnabled(True)
214 self.__cancelButton.setEnabled(False)
215
216 self.downloadProgress.setValue(0)
217
218 if reply in self.__replies:
219 self.__replies.remove(reply)
220 reply.deleteLater()
221
222 if reply.error() != QNetworkReply.NetworkError.NoError:
223 if not self.__downloadCancelled:
224 EricMessageBox.warning(
225 self,
226 self.tr("Error downloading dictionaries list"),
227 self.tr(
228 """<p>Could not download the dictionaries list"""
229 """ from {0}.</p><p>Error: {1}</p>"""
230 ).format(self.dictionariesUrlEdit.text(),
231 reply.errorString())
232 )
233 self.downloadProgress.setValue(0)
234 return
235
236 listFileData = reply.readAll()
237
238 # extract the dictionaries
239 from EricXML.SpellCheckDictionariesReader import (
240 SpellCheckDictionariesReader
241 )
242 reader = SpellCheckDictionariesReader(listFileData, self.addEntry)
243 reader.readXML()
244 url = Preferences.getWebBrowser("SpellCheckDictionariesUrl")
245 if url != self.dictionariesUrlEdit.text():
246 self.dictionariesUrlEdit.setText(url)
247 EricMessageBox.warning(
248 self,
249 self.tr("Dictionaries URL Changed"),
250 self.tr(
251 """The URL of the spell check dictionaries has"""
252 """ changed. Select the "Refresh" button to get"""
253 """ the new dictionaries list."""
254 )
255 )
256
257 if self.locationComboBox.count() == 0:
258 # no writable locations available
259 EricMessageBox.warning(
260 self,
261 self.tr("Error installing dictionaries"),
262 self.tr(
263 """<p>None of the dictionary locations is writable by"""
264 """ you. Please download required dictionaries manually"""
265 """ and install them as administrator.</p>"""
266 )
267 )
268
269 self.__checkInstalledDictionaries()
270
271 def __downloadCancel(self):
272 """
273 Private slot to cancel the current download.
274 """
275 if self.__replies:
276 reply = self.__replies[0]
277 self.__downloadCancelled = True
278 self.__dictionariesToDownload = []
279 reply.abort()
280
281 def __downloadProgress(self, done, total):
282 """
283 Private slot to show the download progress.
284
285 @param done number of bytes downloaded so far
286 @type int
287 @param total total bytes to be downloaded
288 @type int
289 """
290 if total:
291 self.downloadProgress.setMaximum(total)
292 self.downloadProgress.setValue(done)
293
294 def addEntry(self, short, filename, url, documentationDir, locales):
295 """
296 Public method to add an entry to the list.
297
298 @param short data for the description field
299 @type str
300 @param filename data for the filename field
301 @type str
302 @param url download URL for the dictionary entry
303 @type str
304 @param documentationDir name of the directory containing the
305 dictionary documentation
306 @type str
307 @param locales list of locales
308 @type list of str
309 """
310 itm = QListWidgetItem(
311 self.tr("{0} ({1})").format(short, " ".join(locales)),
312 self.dictionariesList)
313 itm.setCheckState(Qt.CheckState.Unchecked)
314
315 itm.setData(ManageDictionariesDialog.FilenameRole, filename)
316 itm.setData(ManageDictionariesDialog.UrlRole, url)
317 itm.setData(ManageDictionariesDialog.DocumentationDirRole,
318 documentationDir)
319 itm.setData(ManageDictionariesDialog.LocalesRole, locales)
320
321 def __checkInstalledDictionaries(self):
322 """
323 Private method to check all installed dictionaries.
324
325 Note: A dictionary is assumed to be installed, if at least one of its
326 binary dictionaries (*.bdic) is found in the selected dictionaries
327 location.
328 """
329 if self.locationComboBox.currentText():
330 installedLocales = {
331 os.path.splitext(os.path.basename(dic))[0]
332 for dic in glob.glob(
333 os.path.join(self.locationComboBox.currentText(), "*.bdic")
334 )
335 }
336
337 for row in range(self.dictionariesList.count()):
338 itm = self.dictionariesList.item(row)
339 locales = set(itm.data(ManageDictionariesDialog.LocalesRole))
340 if locales.intersection(installedLocales):
341 itm.setCheckState(Qt.CheckState.Checked)
342 else:
343 itm.setCheckState(Qt.CheckState.Unchecked)
344 else:
345 for row in range(self.dictionariesList.count()):
346 itm = self.dictionariesList.item(row)
347 itm.setCheckState(Qt.CheckState.Unchecked)
348
349 def __installSelected(self):
350 """
351 Private method to install the selected dictionaries.
352 """
353 if self.__online and bool(self.locationComboBox.currentText()):
354 self.__dictionariesToDownload = [
355 itm.data(ManageDictionariesDialog.UrlRole)
356 for itm in self.dictionariesList.selectedItems()
357 ]
358
359 self.__refreshButton.setEnabled(False)
360 self.__installButton.setEnabled(False)
361 self.__uninstallButton.setEnabled(False)
362 self.__cancelButton.setEnabled(True)
363
364 self.__downloadCancelled = False
365
366 self.__downloadDictionary()
367
368 def __downloadDictionary(self):
369 """
370 Private slot to download a dictionary.
371 """
372 if self.__online:
373 if self.__dictionariesToDownload:
374 url = self.__dictionariesToDownload.pop(0)
375 self.statusLabel.setText(url)
376
377 self.__downloadCancelled = False
378
379 request = QNetworkRequest(QUrl(url))
380 request.setAttribute(
381 QNetworkRequest.Attribute.CacheLoadControlAttribute,
382 QNetworkRequest.CacheLoadControl.AlwaysNetwork)
383 reply = WebBrowserWindow.networkManager().get(request)
384 reply.finished.connect(
385 lambda: self.__installDictionary(reply))
386 reply.downloadProgress.connect(self.__downloadProgress)
387 self.__replies.append(reply)
388 else:
389 self.__installationFinished()
390 else:
391 EricMessageBox.warning(
392 self,
393 self.tr("Error downloading dictionary file"),
394 self.tr(
395 """<p>Could not download the requested dictionary file"""
396 """ from {0}.</p><p>Error: {1}</p>"""
397 ).format(url, self.tr("No connection to Internet.")))
398
399 self.__installationFinished()
400
401 def __installDictionary(self, reply):
402 """
403 Private slot to install the downloaded dictionary.
404
405 @param reply reference to the network reply
406 @type QNetworkReply
407 """
408 if reply in self.__replies:
409 self.__replies.remove(reply)
410 reply.deleteLater()
411
412 if reply.error() != QNetworkReply.NetworkError.NoError:
413 if not self.__downloadCancelled:
414 EricMessageBox.warning(
415 self,
416 self.tr("Error downloading dictionary file"),
417 self.tr(
418 """<p>Could not download the requested dictionary"""
419 """ file from {0}.</p><p>Error: {1}</p>"""
420 ).format(reply.url(), reply.errorString())
421 )
422 self.downloadProgress.setValue(0)
423 return
424
425 archiveData = reply.readAll()
426 archiveFile = io.BytesIO(bytes(archiveData))
427 archive = zipfile.ZipFile(archiveFile, "r")
428 if archive.testzip() is not None:
429 EricMessageBox.critical(
430 self,
431 self.tr("Error downloading dictionary"),
432 self.tr(
433 """<p>The downloaded dictionary archive is invalid."""
434 """ Skipping it.</p>""")
435 )
436 else:
437 installDir = self.locationComboBox.currentText()
438 archive.extractall(installDir)
439
440 if self.__dictionariesToDownload:
441 self.__downloadDictionary()
442 else:
443 self.__installationFinished()
444
445 def __installationFinished(self):
446 """
447 Private method called after all selected dictionaries have been
448 installed.
449 """
450 self.__refreshButton.setEnabled(True)
451 self.__cancelButton.setEnabled(False)
452
453 self.dictionariesList.clearSelection()
454 self.downloadProgress.setValue(0)
455
456 self.__checkInstalledDictionaries()
457
458 def __uninstallSelected(self):
459 """
460 Private method to uninstall the selected dictionaries.
461 """
462 installLocation = self.locationComboBox.currentText()
463 if not installLocation:
464 return
465
466 itemsToDelete = [
467 itm
468 for itm in self.dictionariesList.selectedItems()
469 if itm.checkState() == Qt.CheckState.Checked
470 ]
471 for itm in itemsToDelete:
472 documentationDir = itm.data(
473 ManageDictionariesDialog.DocumentationDirRole)
474 shutil.rmtree(os.path.join(installLocation, documentationDir),
475 True)
476
477 locales = itm.data(ManageDictionariesDialog.LocalesRole)
478 for locale in locales:
479 bdic = os.path.join(installLocation, locale + ".bdic")
480 with contextlib.suppress(OSError):
481 os.remove(bdic)
482
483 self.dictionariesList.clearSelection()
484
485 self.__checkInstalledDictionaries()

eric ide

mercurial