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