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