|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2017 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(self.__listFileDownloaded) |
|
198 reply.downloadProgress.connect(self.__downloadProgress) |
|
199 self.__replies.append(reply) |
|
200 else: |
|
201 E5MessageBox.warning( |
|
202 self, |
|
203 self.tr("Error populating list of dictionaries"), |
|
204 self.tr( |
|
205 """<p>Could not download the dictionaries list file""" |
|
206 """ from {0}.</p><p>Error: {1}</p>""" |
|
207 ).format(url, self.tr("Computer is offline."))) |
|
208 |
|
209 def __listFileDownloaded(self): |
|
210 """ |
|
211 Private method called, after the dictionaries list file has been |
|
212 downloaded from the Internet. |
|
213 """ |
|
214 self.__refreshButton.setEnabled(True) |
|
215 self.__cancelButton.setEnabled(False) |
|
216 self.__onlineStateChanged(self.__isOnline()) |
|
217 |
|
218 self.downloadProgress.setValue(0) |
|
219 |
|
220 reply = self.sender() |
|
221 if reply in self.__replies: |
|
222 self.__replies.remove(reply) |
|
223 reply.deleteLater() |
|
224 |
|
225 if reply.error() != QNetworkReply.NoError: |
|
226 if not self.__downloadCancelled: |
|
227 E5MessageBox.warning( |
|
228 self, |
|
229 self.tr("Error downloading dictionaries list"), |
|
230 self.tr( |
|
231 """<p>Could not download the dictionaries list""" |
|
232 """ from {0}.</p><p>Error: {1}</p>""" |
|
233 ).format(self.repositoryUrlEdit.text(), |
|
234 reply.errorString()) |
|
235 ) |
|
236 self.downloadProgress.setValue(0) |
|
237 return |
|
238 |
|
239 listFileData = reply.readAll() |
|
240 |
|
241 # extract the dictionaries |
|
242 from E5XML.SpellCheckDictionariesReader import \ |
|
243 SpellCheckDictionariesReader |
|
244 reader = SpellCheckDictionariesReader(listFileData, self.addEntry) |
|
245 reader.readXML() |
|
246 url = Preferences.getWebBrowser("SpellCheckDictionariesUrl") |
|
247 if url != self.dictionariesUrlEdit.text(): |
|
248 self.dictionariesUrlEdit.setText(url) |
|
249 E5MessageBox.warning( |
|
250 self, |
|
251 self.tr("Dictionaries URL Changed"), |
|
252 self.tr( |
|
253 """The URL of the spell check dictionaries has""" |
|
254 """ changed. Select the "Refresh" button to get""" |
|
255 """ the new dictionaries list.""" |
|
256 ) |
|
257 ) |
|
258 |
|
259 if self.locationComboBox.count() == 0: |
|
260 # no writable locations available |
|
261 E5MessageBox.warning( |
|
262 self, |
|
263 self.tr("Error installing dictionaries"), |
|
264 self.tr( |
|
265 """<p>None of the dictionary locations is writable by""" |
|
266 """ you. Please download required dictionaries manually""" |
|
267 """ and install them as administrator.</p>""" |
|
268 ) |
|
269 ) |
|
270 |
|
271 self.__checkInstalledDictionaries() |
|
272 |
|
273 def __downloadCancel(self): |
|
274 """ |
|
275 Private slot to cancel the current download. |
|
276 """ |
|
277 if self.__replies: |
|
278 reply = self.__replies[0] |
|
279 self.__downloadCancelled = True |
|
280 self.__dictionariesToDownload = [] |
|
281 reply.abort() |
|
282 |
|
283 def __downloadProgress(self, done, total): |
|
284 """ |
|
285 Private slot to show the download progress. |
|
286 |
|
287 @param done number of bytes downloaded so far |
|
288 @type int |
|
289 @param total total bytes to be downloaded |
|
290 @type int |
|
291 """ |
|
292 if total: |
|
293 self.downloadProgress.setMaximum(total) |
|
294 self.downloadProgress.setValue(done) |
|
295 |
|
296 def addEntry(self, short, filename, url, documentationDir, locales): |
|
297 """ |
|
298 Public method to add an entry to the list. |
|
299 |
|
300 @param short data for the description field |
|
301 @type str |
|
302 @param filename data for the filename field |
|
303 @type str |
|
304 @param url download URL for the dictionary entry |
|
305 @type str |
|
306 @param documentationDir name of the directory containing the |
|
307 dictionary documentation |
|
308 @type str |
|
309 @param locales list of locales |
|
310 @type list of str |
|
311 """ |
|
312 itm = QListWidgetItem( |
|
313 self.tr("{0} ({1})").format(short, " ".join(locales)), |
|
314 self.dictionariesList) |
|
315 itm.setCheckState(Qt.Unchecked) |
|
316 |
|
317 itm.setData(ManageDictionariesDialog.FilenameRole, filename) |
|
318 itm.setData(ManageDictionariesDialog.UrlRole, url) |
|
319 itm.setData(ManageDictionariesDialog.DocumentationDirRole, |
|
320 documentationDir) |
|
321 itm.setData(ManageDictionariesDialog.LocalesRole, locales) |
|
322 |
|
323 def __checkInstalledDictionaries(self): |
|
324 """ |
|
325 Private method to check all installed dictionaries. |
|
326 |
|
327 Note: A dictionary is assumed to be installed, if at least one of its |
|
328 binary dictionaries (*.bdic) is found in the selected dictionaries |
|
329 location. |
|
330 """ |
|
331 if self.locationComboBox.currentText(): |
|
332 installedLocales = set([ |
|
333 os.path.splitext(os.path.basename(dic))[0] |
|
334 for dic in glob.glob( |
|
335 os.path.join(self.locationComboBox.currentText(), "*.bdic") |
|
336 ) |
|
337 ]) |
|
338 |
|
339 for row in range(self.dictionariesList.count()): |
|
340 itm = self.dictionariesList.item(row) |
|
341 locales = set(itm.data(ManageDictionariesDialog.LocalesRole)) |
|
342 if locales.intersection(installedLocales): |
|
343 itm.setCheckState(Qt.Checked) |
|
344 else: |
|
345 itm.setCheckState(Qt.Unchecked) |
|
346 else: |
|
347 for row in range(self.dictionariesList.count()): |
|
348 itm = self.dictionariesList.item(row) |
|
349 itm.setCheckState(Qt.Unchecked) |
|
350 |
|
351 def __installSelected(self): |
|
352 """ |
|
353 Private method to install the selected dictionaries. |
|
354 """ |
|
355 if self.__isOnline() and bool(self.locationComboBox.currentText()): |
|
356 self.__dictionariesToDownload = [ |
|
357 itm.data(ManageDictionariesDialog.UrlRole) |
|
358 for itm in self.dictionariesList.selectedItems() |
|
359 ] |
|
360 |
|
361 self.__refreshButton.setEnabled(False) |
|
362 self.__installButton.setEnabled(False) |
|
363 self.__uninstallButton.setEnabled(False) |
|
364 self.__cancelButton.setEnabled(True) |
|
365 |
|
366 self.__downloadCancelled = False |
|
367 |
|
368 self.__downloadDictionary() |
|
369 |
|
370 def __downloadDictionary(self): |
|
371 """ |
|
372 Private slot to download a dictionary. |
|
373 """ |
|
374 if self.__isOnline(): |
|
375 if self.__dictionariesToDownload: |
|
376 url = self.__dictionariesToDownload.pop(0) |
|
377 self.statusLabel.setText(url) |
|
378 |
|
379 self.__downloadCancelled = False |
|
380 |
|
381 request = QNetworkRequest(QUrl(url)) |
|
382 request.setAttribute(QNetworkRequest.CacheLoadControlAttribute, |
|
383 QNetworkRequest.AlwaysNetwork) |
|
384 reply = WebBrowserWindow.networkManager().get(request) |
|
385 reply.finished.connect(self.__installDictionary) |
|
386 reply.downloadProgress.connect(self.__downloadProgress) |
|
387 self.__replies.append(reply) |
|
388 else: |
|
389 self.__installationFinished() |
|
390 else: |
|
391 E5MessageBox.warning( |
|
392 self, |
|
393 self.tr("Error downloading dictionary"), |
|
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("Computer is offline."))) |
|
398 |
|
399 self.__installationFinished() |
|
400 |
|
401 def __installDictionary(self): |
|
402 """ |
|
403 Private slot to install the downloaded dictionary. |
|
404 """ |
|
405 reply = self.sender() |
|
406 if reply in self.__replies: |
|
407 self.__replies.remove(reply) |
|
408 reply.deleteLater() |
|
409 |
|
410 if reply.error() != QNetworkReply.NoError: |
|
411 if not self.__downloadCancelled: |
|
412 E5MessageBox.warning( |
|
413 self, |
|
414 self.tr("Error downloading dictionary file"), |
|
415 self.tr( |
|
416 """<p>Could not download the requested dictionary""" |
|
417 """ file from {0}.</p><p>Error: {1}</p>""" |
|
418 ).format(reply.url(), reply.errorString()) |
|
419 ) |
|
420 self.downloadProgress.setValue(0) |
|
421 return |
|
422 |
|
423 archiveData = reply.readAll() |
|
424 archiveFile = io.BytesIO(bytes(archiveData)) |
|
425 archive = zipfile.ZipFile(archiveFile, "r") |
|
426 if archive.testzip() is not None: |
|
427 E5MessageBox.critical( |
|
428 self, |
|
429 self.tr("Error downloading dictionary"), |
|
430 self.tr( |
|
431 """<p>The downloaded dictionary archive is invalid.""" |
|
432 """ Skipping it.</p>""") |
|
433 ) |
|
434 else: |
|
435 installDir = self.locationComboBox.currentText() |
|
436 archive.extractall(installDir) |
|
437 |
|
438 if self.__dictionariesToDownload: |
|
439 self.__downloadDictionary() |
|
440 else: |
|
441 self.__installationFinished() |
|
442 |
|
443 def __installationFinished(self): |
|
444 """ |
|
445 Private method called after all selected dictionaries have been |
|
446 installed. |
|
447 """ |
|
448 self.__refreshButton.setEnabled(True) |
|
449 self.__cancelButton.setEnabled(False) |
|
450 self.__onlineStateChanged(self.__isOnline()) |
|
451 |
|
452 self.dictionariesList.clearSelection() |
|
453 self.downloadProgress.setValue(0) |
|
454 |
|
455 self.__checkInstalledDictionaries() |
|
456 |
|
457 def __uninstallSelected(self): |
|
458 """ |
|
459 Private method to uninstall the selected dictionaries. |
|
460 """ |
|
461 installLocation = self.locationComboBox.currentText() |
|
462 if not installLocation: |
|
463 return |
|
464 |
|
465 itemsToDelete = [ |
|
466 itm |
|
467 for itm in self.dictionariesList.selectedItems() |
|
468 if itm.checkState() == Qt.Checked |
|
469 ] |
|
470 for itm in itemsToDelete: |
|
471 documentationDir = itm.data( |
|
472 ManageDictionariesDialog.DocumentationDirRole) |
|
473 shutil.rmtree(os.path.join(installLocation, documentationDir), |
|
474 True) |
|
475 |
|
476 locales = itm.data(ManageDictionariesDialog.LocalesRole) |
|
477 for locale in locales: |
|
478 bdic = os.path.join(installLocation, locale + ".bdic") |
|
479 try: |
|
480 os.remove(bdic) |
|
481 except OSError: |
|
482 # ignore silently |
|
483 pass |
|
484 |
|
485 self.dictionariesList.clearSelection() |
|
486 |
|
487 self.__checkInstalledDictionaries() |