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