eric7/Project/ProjectTranslationsBrowser.py

branch
eric7
changeset 8312
800c432b34c8
parent 8265
0090cfa83159
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2002 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a class used to display the translations part of the
8 project.
9 """
10
11 import os
12 import shutil
13 import fnmatch
14 import functools
15 import contextlib
16
17 from PyQt5.QtCore import pyqtSignal, QProcess
18 from PyQt5.QtWidgets import QDialog, QMenu
19
20 from E5Gui import E5MessageBox
21 from E5Gui.E5Application import e5App
22
23 from .ProjectBrowserModel import (
24 ProjectBrowserFileItem, ProjectBrowserSimpleDirectoryItem,
25 ProjectBrowserDirectoryItem, ProjectBrowserTranslationType
26 )
27 from .ProjectBaseBrowser import ProjectBaseBrowser
28
29 import UI.PixmapCache
30 from UI.NotificationWidget import NotificationTypes
31
32 import Preferences
33 import Utilities
34
35
36 class ProjectTranslationsBrowser(ProjectBaseBrowser):
37 """
38 A class used to display the translations part of the project.
39
40 @signal appendStdout(str) emitted after something was received from
41 a QProcess on stdout
42 @signal appendStderr(str) emitted after something was received from
43 a QProcess on stderr
44 @signal showMenu(str, QMenu) emitted when a menu is about to be shown.
45 The name of the menu and a reference to the menu are given.
46 """
47 appendStdout = pyqtSignal(str)
48 appendStderr = pyqtSignal(str)
49 showMenu = pyqtSignal(str, QMenu)
50
51 def __init__(self, project, parent=None):
52 """
53 Constructor
54
55 @param project reference to the project object
56 @param parent parent widget of this browser (QWidget)
57 """
58 ProjectBaseBrowser.__init__(self, project,
59 ProjectBrowserTranslationType, parent)
60 self.isTranslationsBrowser = True
61
62 self.selectedItemsFilter = [ProjectBrowserFileItem,
63 ProjectBrowserSimpleDirectoryItem]
64
65 self.setWindowTitle(self.tr('Translations'))
66
67 self.setWhatsThis(self.tr(
68 """<b>Project Translations Browser</b>"""
69 """<p>This allows to easily see all translations contained in"""
70 """ the current project. Several actions can be executed via"""
71 """ the context menu.</p>"""
72 ))
73
74 self.__lreleaseProcesses = []
75 self.__pylupdateProcesses = []
76 self.lreleaseProcRunning = False
77 self.pylupdateProcRunning = False
78 self.__tmpProjects = []
79
80 def _createPopupMenus(self):
81 """
82 Protected overloaded method to generate the popup menu.
83 """
84 self.menuActions = []
85 self.multiMenuActions = []
86 self.dirMenuActions = []
87 self.dirMultiMenuActions = []
88
89 self.tsMenuActions = []
90 self.qmMenuActions = []
91 self.tsprocMenuActions = []
92 self.qmprocMenuActions = []
93
94 self.tsMultiMenuActions = []
95 self.qmMultiMenuActions = []
96 self.tsprocMultiMenuActions = []
97 self.qmprocMultiMenuActions = []
98
99 self.tsprocDirMenuActions = []
100 self.qmprocDirMenuActions = []
101
102 self.tsprocBackMenuActions = []
103 self.qmprocBackMenuActions = []
104
105 self.menu = QMenu(self)
106 if self.project.getProjectType() in [
107 "PyQt5", "PyQt5C", "PyQt6", "PyQt6C", "E6Plugin",
108 "PySide2", "PySide2C", "PySide6", "PySide6C"
109 ]:
110 act = self.menu.addAction(
111 self.tr('Generate translation'), self.__generateSelected)
112 self.tsMenuActions.append(act)
113 self.tsprocMenuActions.append(act)
114 act = self.menu.addAction(
115 self.tr('Generate translation (with obsolete)'),
116 self.__generateObsoleteSelected)
117 self.tsMenuActions.append(act)
118 self.tsprocMenuActions.append(act)
119 act = self.menu.addAction(
120 self.tr('Generate all translations'), self.__generateAll)
121 self.tsprocMenuActions.append(act)
122 act = self.menu.addAction(
123 self.tr('Generate all translations (with obsolete)'),
124 self.__generateObsoleteAll)
125 self.tsprocMenuActions.append(act)
126 self.menu.addSeparator()
127 act = self.menu.addAction(
128 self.tr('Open in Qt-Linguist'), self._openItem)
129 self.tsMenuActions.append(act)
130 act = self.menu.addAction(
131 self.tr('Open in Editor'), self.__openFileInEditor)
132 self.tsMenuActions.append(act)
133 self.menu.addSeparator()
134 act = self.menu.addAction(
135 self.tr('Release translation'), self.__releaseSelected)
136 self.tsMenuActions.append(act)
137 self.qmprocMenuActions.append(act)
138 act = self.menu.addAction(
139 self.tr('Release all translations'), self.__releaseAll)
140 self.qmprocMenuActions.append(act)
141 self.menu.addSeparator()
142 act = self.menu.addAction(
143 self.tr('Preview translation'), self.__TRPreview)
144 self.qmMenuActions.append(act)
145 act = self.menu.addAction(
146 self.tr('Preview all translations'), self.__TRPreviewAll)
147 self.menu.addSeparator()
148 else:
149 if self.hooks["extractMessages"] is not None:
150 act = self.menu.addAction(
151 self.hooksMenuEntries.get(
152 "extractMessages",
153 self.tr('Extract messages')),
154 self.__extractMessages)
155 self.menuActions.append(act)
156 self.menu.addSeparator()
157 if self.hooks["generateSelected"] is not None:
158 act = self.menu.addAction(
159 self.hooksMenuEntries.get(
160 "generateSelected",
161 self.tr('Generate translation')),
162 self.__generateSelected)
163 self.tsMenuActions.append(act)
164 self.tsprocMenuActions.append(act)
165 if self.hooks["generateSelectedWithObsolete"] is not None:
166 act = self.menu.addAction(
167 self.hooksMenuEntries.get(
168 "generateSelectedWithObsolete",
169 self.tr('Generate translation (with obsolete)')),
170 self.__generateObsoleteSelected)
171 self.tsMenuActions.append(act)
172 self.tsprocMenuActions.append(act)
173 if self.hooks["generateAll"] is not None:
174 act = self.menu.addAction(
175 self.hooksMenuEntries.get(
176 "generateAll",
177 self.tr('Generate all translations')),
178 self.__generateAll)
179 self.tsprocMenuActions.append(act)
180 if self.hooks["generateAllWithObsolete"] is not None:
181 act = self.menu.addAction(
182 self.hooksMenuEntries.get(
183 "generateAllWithObsolete",
184 self.tr(
185 'Generate all translations (with obsolete)')),
186 self.__generateObsoleteAll)
187 self.tsprocMenuActions.append(act)
188 self.menu.addSeparator()
189 if self.hooks["open"] is not None:
190 act = self.menu.addAction(
191 self.hooksMenuEntries.get(
192 "open", self.tr('Open')),
193 self._openItem)
194 self.tsMenuActions.append(act)
195 act = self.menu.addAction(
196 self.tr('Open in Editor'), self.__openFileInEditor)
197 self.tsMenuActions.append(act)
198 self.menu.addSeparator()
199 if self.hooks["releaseSelected"] is not None:
200 act = self.menu.addAction(
201 self.hooksMenuEntries.get(
202 "releaseSelected",
203 self.tr('Release translation')),
204 self.__releaseSelected)
205 self.tsMenuActions.append(act)
206 self.qmprocMenuActions.append(act)
207 if self.hooks["releaseAll"] is not None:
208 act = self.menu.addAction(
209 self.hooksMenuEntries.get(
210 "releaseAll",
211 self.tr('Release all translations')),
212 self.__releaseAll)
213 self.qmprocMenuActions.append(act)
214 self.menu.addSeparator()
215 act = self.menu.addAction(
216 self.tr('Remove from project'), self.__removeLanguageFile)
217 self.menuActions.append(act)
218 act = self.menu.addAction(
219 self.tr('Delete'), self.__deleteLanguageFile)
220 self.menuActions.append(act)
221 self.menu.addSeparator()
222 self.__addTranslationAct = self.menu.addAction(
223 self.tr('Add translation...'), self.project.addLanguage)
224 self.menu.addAction(
225 self.tr('Add translation files...'),
226 self.__addTranslationFiles)
227 self.menu.addSeparator()
228 self.menu.addAction(
229 self.tr('Copy Path to Clipboard'), self._copyToClipboard)
230 self.menu.addSeparator()
231 self.menu.addAction(self.tr('Configure...'), self._configure)
232
233 self.backMenu = QMenu(self)
234 if self.project.getProjectType() in [
235 "PyQt5", "PyQt5C", "PyQt6", "PyQt6C", "E6Plugin",
236 "PySide2", "PySide2C", "PySide6", "PySide6C"
237 ]:
238 act = self.backMenu.addAction(
239 self.tr('Generate all translations'),
240 self.__generateAll)
241 self.tsprocBackMenuActions.append(act)
242 act = self.backMenu.addAction(
243 self.tr('Generate all translations (with obsolete)'),
244 self.__generateObsoleteAll)
245 self.tsprocBackMenuActions.append(act)
246 act = self.backMenu.addAction(
247 self.tr('Release all translations'),
248 self.__releaseAll)
249 self.qmprocBackMenuActions.append(act)
250 self.backMenu.addSeparator()
251 act = self.backMenu.addAction(
252 self.tr('Preview all translations'),
253 self.__TRPreview)
254 else:
255 if self.hooks["extractMessages"] is not None:
256 act = self.backMenu.addAction(
257 self.hooksMenuEntries.get(
258 "extractMessages",
259 self.tr('Extract messages')),
260 self.__extractMessages)
261 self.backMenu.addSeparator()
262 if self.hooks["generateAll"] is not None:
263 act = self.backMenu.addAction(
264 self.hooksMenuEntries.get(
265 "generateAll",
266 self.tr('Generate all translations')),
267 self.__generateAll)
268 self.tsprocBackMenuActions.append(act)
269 if self.hooks["generateAllWithObsolete"] is not None:
270 act = self.backMenu.addAction(
271 self.hooksMenuEntries.get(
272 "generateAllWithObsolete",
273 self.tr(
274 'Generate all translations (with obsolete)')),
275 self.__generateObsoleteAll)
276 self.tsprocBackMenuActions.append(act)
277 if self.hooks["releaseAll"] is not None:
278 act = self.backMenu.addAction(
279 self.hooksMenuEntries.get(
280 "releaseAll",
281 self.tr('Release all translations')),
282 self.__releaseAll)
283 self.qmprocBackMenuActions.append(act)
284 self.backMenu.addSeparator()
285 self.__addTranslationBackAct = self.backMenu.addAction(
286 self.tr('Add translation...'), self.project.addLanguage)
287 self.backMenu.addAction(
288 self.tr('Add translation files...'),
289 self.__addTranslationFiles)
290 self.backMenu.addSeparator()
291 self.backMenu.addAction(self.tr('Configure...'), self._configure)
292 self.backMenu.setEnabled(False)
293
294 # create the menu for multiple selected files
295 self.multiMenu = QMenu(self)
296 if self.project.getProjectType() in [
297 "PyQt5", "PyQt5C", "PyQt6", "PyQt6C", "E6Plugin",
298 "PySide2", "PySide2C", "PySide6", "PySide6C"
299 ]:
300 act = self.multiMenu.addAction(
301 self.tr('Generate translations'),
302 self.__generateSelected)
303 self.tsMultiMenuActions.append(act)
304 self.tsprocMultiMenuActions.append(act)
305 act = self.multiMenu.addAction(
306 self.tr('Generate translations (with obsolete)'),
307 self.__generateObsoleteSelected)
308 self.tsMultiMenuActions.append(act)
309 self.tsprocMultiMenuActions.append(act)
310 self.multiMenu.addSeparator()
311 act = self.multiMenu.addAction(
312 self.tr('Open in Qt-Linguist'), self._openItem)
313 self.tsMultiMenuActions.append(act)
314 act = self.multiMenu.addAction(
315 self.tr('Open in Editor'), self.__openFileInEditor)
316 self.tsMultiMenuActions.append(act)
317 self.multiMenu.addSeparator()
318 act = self.multiMenu.addAction(
319 self.tr('Release translations'), self.__releaseSelected)
320 self.tsMultiMenuActions.append(act)
321 self.qmprocMultiMenuActions.append(act)
322 self.multiMenu.addSeparator()
323 act = self.multiMenu.addAction(
324 self.tr('Preview translations'), self.__TRPreview)
325 self.qmMultiMenuActions.append(act)
326 else:
327 if self.hooks["extractMessages"] is not None:
328 act = self.multiMenu.addAction(
329 self.hooksMenuEntries.get(
330 "extractMessages",
331 self.tr('Extract messages')),
332 self.__extractMessages)
333 self.multiMenuActions.append(act)
334 self.multiMenu.addSeparator()
335 if self.hooks["generateSelected"] is not None:
336 act = self.multiMenu.addAction(
337 self.hooksMenuEntries.get(
338 "generateSelected",
339 self.tr('Generate translations')),
340 self.__generateSelected)
341 self.tsMultiMenuActions.append(act)
342 self.tsprocMultiMenuActions.append(act)
343 if self.hooks["generateSelectedWithObsolete"] is not None:
344 act = self.multiMenu.addAction(
345 self.hooksMenuEntries.get(
346 "generateSelectedWithObsolete",
347 self.tr('Generate translations (with obsolete)')),
348 self.__generateObsoleteSelected)
349 self.tsMultiMenuActions.append(act)
350 self.tsprocMultiMenuActions.append(act)
351 self.multiMenu.addSeparator()
352 if self.hooks["open"] is not None:
353 act = self.multiMenu.addAction(
354 self.hooksMenuEntries.get(
355 "open", self.tr('Open')),
356 self._openItem)
357 self.tsMultiMenuActions.append(act)
358 act = self.multiMenu.addAction(
359 self.tr('Open in Editor'), self.__openFileInEditor)
360 self.tsMultiMenuActions.append(act)
361 self.multiMenu.addSeparator()
362 if self.hooks["releaseSelected"] is not None:
363 act = self.multiMenu.addAction(
364 self.hooksMenuEntries.get(
365 "releaseSelected",
366 self.tr('Release translations')),
367 self.__releaseSelected)
368 self.tsMultiMenuActions.append(act)
369 self.qmprocMultiMenuActions.append(act)
370 self.multiMenu.addSeparator()
371 act = self.multiMenu.addAction(
372 self.tr('Remove from project'), self.__removeLanguageFile)
373 self.multiMenuActions.append(act)
374 act = self.multiMenu.addAction(
375 self.tr('Delete'), self.__deleteLanguageFile)
376 self.multiMenuActions.append(act)
377 self.multiMenu.addSeparator()
378 self.multiMenu.addAction(self.tr('Configure...'), self._configure)
379
380 self.dirMenu = QMenu(self)
381 if self.project.getProjectType() in [
382 "PyQt5", "PyQt5C", "PyQt6", "PyQt6C", "E6Plugin",
383 "PySide2", "PySide2C", "PySide6", "PySide6C"
384 ]:
385 act = self.dirMenu.addAction(
386 self.tr('Generate all translations'),
387 self.__generateAll)
388 self.tsprocDirMenuActions.append(act)
389 act = self.dirMenu.addAction(
390 self.tr('Generate all translations (with obsolete)'),
391 self.__generateObsoleteAll)
392 self.tsprocDirMenuActions.append(act)
393 act = self.dirMenu.addAction(
394 self.tr('Release all translations'),
395 self.__releaseAll)
396 self.qmprocDirMenuActions.append(act)
397 self.dirMenu.addSeparator()
398 act = self.dirMenu.addAction(
399 self.tr('Preview all translations'),
400 self.__TRPreview)
401 else:
402 if self.hooks["extractMessages"] is not None:
403 act = self.dirMenu.addAction(
404 self.hooksMenuEntries.get(
405 "extractMessages",
406 self.tr('Extract messages')),
407 self.__extractMessages)
408 self.dirMenuActions.append(act)
409 self.dirMenu.addSeparator()
410 if self.hooks["generateAll"] is not None:
411 act = self.dirMenu.addAction(
412 self.hooksMenuEntries.get(
413 "generateAll",
414 self.tr('Generate all translations')),
415 self.__generateAll)
416 self.tsprocDirMenuActions.append(act)
417 if self.hooks["generateAllWithObsolete"] is not None:
418 act = self.dirMenu.addAction(
419 self.hooksMenuEntries.get(
420 "generateAllWithObsolete",
421 self.tr(
422 'Generate all translations (with obsolete)')),
423 self.__generateObsoleteAll)
424 self.tsprocDirMenuActions.append(act)
425 if self.hooks["releaseAll"] is not None:
426 act = self.dirMenu.addAction(
427 self.hooksMenuEntries.get(
428 "releaseAll",
429 self.tr('Release all translations')),
430 self.__releaseAll)
431 self.qmprocDirMenuActions.append(act)
432 self.dirMenu.addSeparator()
433 act = self.dirMenu.addAction(
434 self.tr('Delete'), self._deleteDirectory)
435 self.dirMenuActions.append(act)
436 self.dirMenu.addSeparator()
437 self.__addTranslationDirAct = self.dirMenu.addAction(
438 self.tr('Add translation...'), self.project.addLanguage)
439 self.dirMenu.addAction(
440 self.tr('Add translation files...'),
441 self.__addTranslationFiles)
442 self.dirMenu.addSeparator()
443 self.dirMenu.addAction(
444 self.tr('Copy Path to Clipboard'), self._copyToClipboard)
445 self.dirMenu.addSeparator()
446 self.dirMenu.addAction(self.tr('Configure...'), self._configure)
447
448 self.dirMultiMenu = None
449
450 self.menu.aboutToShow.connect(self.__showContextMenu)
451 self.multiMenu.aboutToShow.connect(self.__showContextMenuMulti)
452 self.dirMenu.aboutToShow.connect(self.__showContextMenuDir)
453 self.backMenu.aboutToShow.connect(self.__showContextMenuBack)
454 self.mainMenu = self.menu
455
456 def _contextMenuRequested(self, coord):
457 """
458 Protected slot to show the context menu.
459
460 @param coord the position of the mouse pointer (QPoint)
461 """
462 if not self.project.isOpen():
463 return
464
465 with contextlib.suppress(Exception):
466 categories = self.getSelectedItemsCountCategorized(
467 [ProjectBrowserFileItem, ProjectBrowserSimpleDirectoryItem])
468 cnt = categories["sum"]
469 if cnt <= 1:
470 index = self.indexAt(coord)
471 if index.isValid():
472 self._selectSingleItem(index)
473 categories = self.getSelectedItemsCountCategorized(
474 [ProjectBrowserFileItem,
475 ProjectBrowserSimpleDirectoryItem])
476 cnt = categories["sum"]
477
478 bfcnt = categories[str(ProjectBrowserFileItem)]
479 sdcnt = categories[str(ProjectBrowserSimpleDirectoryItem)]
480 if cnt > 1 and cnt == bfcnt:
481 self.multiMenu.popup(self.mapToGlobal(coord))
482 else:
483 index = self.indexAt(coord)
484 if cnt == 1 and index.isValid():
485 if bfcnt == 1:
486 self.menu.popup(self.mapToGlobal(coord))
487 elif sdcnt == 1:
488 self.dirMenu.popup(self.mapToGlobal(coord))
489 else:
490 self.backMenu.popup(self.mapToGlobal(coord))
491 else:
492 self.backMenu.popup(self.mapToGlobal(coord))
493
494 def __showContextMenu(self):
495 """
496 Private slot called by the menu aboutToShow signal.
497 """
498 if self.project.getProjectType() in [
499 "PyQt5", "PyQt5C", "PyQt6", "PyQt6C", "E6Plugin",
500 "PySide2", "PySide2C", "PySide6", "PySide6C"
501 ]:
502 tsFiles = 0
503 qmFiles = 0
504 itmList = self.getSelectedItems()
505 for itm in itmList[:]:
506 if itm.fileName().endswith('.ts'):
507 tsFiles += 1
508 elif itm.fileName().endswith('.qm'):
509 qmFiles += 1
510 if (
511 (tsFiles > 0 and qmFiles > 0) or
512 (tsFiles == 0 and qmFiles == 0)
513 ):
514 for act in self.tsMenuActions + self.qmMenuActions:
515 act.setEnabled(False)
516 elif tsFiles > 0:
517 for act in self.tsMenuActions:
518 act.setEnabled(True)
519 for act in self.qmMenuActions:
520 act.setEnabled(False)
521 elif qmFiles > 0:
522 for act in self.tsMenuActions:
523 act.setEnabled(False)
524 for act in self.qmMenuActions:
525 act.setEnabled(True)
526 if self.pylupdateProcRunning:
527 for act in self.tsprocMenuActions:
528 act.setEnabled(False)
529 if self.lreleaseProcRunning:
530 for act in self.qmprocMenuActions:
531 act.setEnabled(True)
532 self.__addTranslationAct.setEnabled(
533 self.project.getTranslationPattern() != "")
534
535 ProjectBaseBrowser._showContextMenu(self, self.menu)
536
537 self.showMenu.emit("Main", self.menu)
538
539 def __showContextMenuMulti(self):
540 """
541 Private slot called by the multiMenu aboutToShow signal.
542 """
543 if self.project.getProjectType() in [
544 "PyQt5", "PyQt5C", "PyQt6", "PyQt6C", "E6Plugin",
545 "PySide2", "PySide2C", "PySide6", "PySide6C"
546 ]:
547 tsFiles = 0
548 qmFiles = 0
549 itmList = self.getSelectedItems()
550 for itm in itmList[:]:
551 if itm.fileName().endswith('.ts'):
552 tsFiles += 1
553 elif itm.fileName().endswith('.qm'):
554 qmFiles += 1
555 if (
556 (tsFiles > 0 and qmFiles > 0) or
557 (tsFiles == 0 and qmFiles == 0)
558 ):
559 for act in self.tsMultiMenuActions + self.qmMultiMenuActions:
560 act.setEnabled(False)
561 elif tsFiles > 0:
562 for act in self.tsMultiMenuActions:
563 act.setEnabled(True)
564 for act in self.qmMultiMenuActions:
565 act.setEnabled(False)
566 elif qmFiles > 0:
567 for act in self.tsMultiMenuActions:
568 act.setEnabled(False)
569 for act in self.qmMultiMenuActions:
570 act.setEnabled(True)
571 if self.pylupdateProcRunning:
572 for act in self.tsprocMultiMenuActions:
573 act.setEnabled(False)
574 if self.lreleaseProcRunning:
575 for act in self.qmprocMultiMenuActions:
576 act.setEnabled(True)
577
578 ProjectBaseBrowser._showContextMenuMulti(self, self.multiMenu)
579
580 self.showMenu.emit("MainMulti", self.multiMenu)
581
582 def __showContextMenuDir(self):
583 """
584 Private slot called by the dirMenu aboutToShow signal.
585 """
586 if self.project.getProjectType() in [
587 "PyQt5", "PyQt5C", "PyQt6", "PyQt6C", "E6Plugin",
588 "PySide2", "PySide2C", "PySide6", "PySide6C"
589 ]:
590 if self.pylupdateProcRunning:
591 for act in self.tsprocDirMenuActions:
592 act.setEnabled(False)
593 if self.lreleaseProcRunning:
594 for act in self.qmprocDirMenuActions:
595 act.setEnabled(True)
596 self.__addTranslationDirAct.setEnabled(
597 self.project.getTranslationPattern() != "")
598
599 ProjectBaseBrowser._showContextMenuDir(self, self.dirMenu)
600
601 self.showMenu.emit("MainDir", self.dirMenu)
602
603 def __showContextMenuBack(self):
604 """
605 Private slot called by the backMenu aboutToShow signal.
606 """
607 if self.project.getProjectType() in [
608 "PyQt5", "PyQt5C", "PyQt6", "PyQt6C", "E6Plugin",
609 "PySide2", "PySide2C", "PySide6", "PySide6C"
610 ]:
611 if self.pylupdateProcRunning:
612 for act in self.tsprocBackMenuActions:
613 act.setEnabled(False)
614 if self.lreleaseProcRunning:
615 for act in self.qmprocBackMenuActions:
616 act.setEnabled(True)
617 self.__addTranslationBackAct.setEnabled(
618 self.project.getTranslationPattern() != "")
619
620 self.showMenu.emit("MainBack", self.backMenu)
621
622 def __addTranslationFiles(self):
623 """
624 Private method to add translation files to the project.
625 """
626 itm = self.model().item(self.currentIndex())
627 if isinstance(itm, ProjectBrowserFileItem):
628 dn = os.path.dirname(itm.fileName())
629 elif isinstance(
630 itm,
631 (ProjectBrowserSimpleDirectoryItem, ProjectBrowserDirectoryItem)
632 ):
633 dn = itm.dirName()
634 else:
635 dn = None
636 self.project.addFiles('translation', dn)
637
638 def _openItem(self):
639 """
640 Protected slot to handle the open popup menu entry.
641 """
642 itmList = self.getSelectedItems()
643 for itm in itmList:
644 if isinstance(itm, ProjectBrowserFileItem):
645 # hook support
646 if self.hooks["open"] is not None:
647 self.hooks["open"](itm.fileName())
648 elif itm.isLinguistFile():
649 if itm.fileExt() == '.ts':
650 self.linguistFile.emit(itm.fileName())
651 else:
652 self.trpreview.emit([itm.fileName()])
653 else:
654 self.sourceFile.emit(itm.fileName())
655
656 def __openFileInEditor(self):
657 """
658 Private slot to handle the Open in Editor menu action.
659 """
660 itmList = self.getSelectedItems()
661 for itm in itmList[:]:
662 self.sourceFile.emit(itm.fileName())
663
664 def __removeLanguageFile(self):
665 """
666 Private method to remove a translation from the project.
667 """
668 itmList = self.getSelectedItems()
669
670 for itm in itmList[:]:
671 fn = itm.fileName()
672 self.closeSourceWindow.emit(fn)
673 self.project.removeLanguageFile(fn)
674
675 def __deleteLanguageFile(self):
676 """
677 Private method to delete a translation file from the project.
678 """
679 itmList = self.getSelectedItems()
680
681 translationFiles = [itm.fileName() for itm in itmList]
682
683 from UI.DeleteFilesConfirmationDialog import (
684 DeleteFilesConfirmationDialog
685 )
686 dlg = DeleteFilesConfirmationDialog(
687 self.parent(),
688 self.tr("Delete translation files"),
689 self.tr("Do you really want to delete these translation files"
690 " from the project?"),
691 translationFiles)
692
693 if dlg.exec() == QDialog.DialogCode.Accepted:
694 for fn in translationFiles:
695 self.closeSourceWindow.emit(fn)
696 self.project.deleteLanguageFile(fn)
697
698 def __TRPreview(self, previewAll=False):
699 """
700 Private slot to handle the Preview translations action.
701
702 @param previewAll flag indicating, that all translations
703 should be previewed (boolean)
704 """
705 fileNames = []
706 itmList = self.getSelectedItems()
707 if itmList and not previewAll:
708 for itm in itmList:
709 if isinstance(itm, ProjectBrowserSimpleDirectoryItem):
710 dname = self.project.getRelativePath(itm.dirName())
711 trfiles = sorted(self.project.pdata["TRANSLATIONS"][:])
712 for trfile in trfiles:
713 if (
714 trfile.startswith(dname) and
715 trfile not in fileNames
716 ):
717 fileNames.append(
718 os.path.join(self.project.ppath, trfile))
719 else:
720 fn = itm.fileName()
721 if fn not in fileNames:
722 fileNames.append(os.path.join(self.project.ppath, fn))
723 else:
724 trfiles = sorted(self.project.pdata["TRANSLATIONS"][:])
725 fileNames.extend([os.path.join(self.project.ppath, trfile)
726 for trfile in trfiles
727 if trfile.endswith('.qm')])
728 self.trpreview[list, bool].emit(fileNames, True)
729
730 def __TRPreviewAll(self):
731 """
732 Private slot to handle the Preview all translations action.
733 """
734 self.__TRPreview(True)
735
736 ###########################################################################
737 ## Methods to support the generation and release commands
738 ###########################################################################
739
740 def __writeTempProjectFile(self, langs, filterList):
741 """
742 Private method to write a temporary project file suitable for
743 pylupdate and lrelease.
744
745 @param langs list of languages to include in the process. An empty
746 list (default) means that all translations should be included.
747 (list of ProjectBrowserFileItem)
748 @param filterList list of source file extension that should be
749 considered (list of strings)
750 @return flag indicating success
751 """
752 path, ext = os.path.splitext(self.project.pfile)
753 pfile = '{0}_e4x.pro'.format(path)
754
755 # only consider files satisfying the filter criteria
756 _sources = [s for s in self.project.pdata["SOURCES"]
757 if os.path.splitext(s)[1] in filterList]
758 sources = []
759 for s in _sources:
760 addIt = True
761 for transExcept in self.project.pdata["TRANSLATIONEXCEPTIONS"]:
762 if s.startswith(transExcept):
763 addIt = False
764 break
765 if addIt:
766 sources.append(s)
767
768 _forms = [f for f in self.project.pdata["FORMS"] if f.endswith('.ui')]
769 forms = []
770 for f in _forms:
771 addIt = True
772 for transExcept in self.project.pdata["TRANSLATIONEXCEPTIONS"]:
773 if f.startswith(transExcept):
774 addIt = False
775 break
776 if addIt:
777 forms.append(f)
778
779 if langs:
780 langs = [self.project.getRelativePath(lang.fileName())
781 for lang in langs if lang.fileName().endswith('.ts')]
782 else:
783 try:
784 pattern = self.project.pdata["TRANSLATIONPATTERN"].replace(
785 "%language%", "*")
786 langs = [lang for lang in self.project.pdata["TRANSLATIONS"]
787 if fnmatch.fnmatch(lang, pattern)]
788 except IndexError:
789 langs = []
790 if not langs:
791 E5MessageBox.warning(
792 self,
793 self.tr("Write temporary project file"),
794 self.tr("""No translation files (*.ts) selected."""))
795 return False
796
797 # create a prefix relative from the *.ts down to the project path
798 langLevel = {}
799 for lang in langs:
800 level = lang.count(os.sep)
801 lst = langLevel.get(level, [])
802 lst.append(lang)
803 langLevel[level] = lst
804
805 for level, langs in langLevel.items():
806 prefix = '../' * level
807 sections = [
808 ("SOURCES",
809 [prefix + src for src in sources])]
810 sections.append(
811 ("FORMS",
812 [prefix + form for form in forms]))
813 sections.append(
814 ("TRANSLATIONS",
815 [prefix + lang for lang in langs]))
816
817 directory, name = os.path.split(pfile)
818 outFile = os.path.join(directory, os.path.dirname(langs[0]), name)
819 outDir = os.path.dirname(outFile)
820 if not os.path.exists(outDir):
821 os.makedirs(outDir)
822 try:
823 with open(outFile, "w", encoding="utf-8") as pf:
824 for key, fileList in sections:
825 if len(fileList) > 0:
826 pf.write('{0} = '.format(key))
827 pf.write(' \\\n\t'.join(
828 [f.replace(os.sep, '/') for f in fileList]))
829 pf.write('\n\n')
830
831 self.__tmpProjects.append(outFile)
832 except OSError:
833 E5MessageBox.critical(
834 self,
835 self.tr("Write temporary project file"),
836 self.tr(
837 "<p>The temporary project file <b>{0}</b> could not"
838 " be written.</p>").format(outFile))
839
840 if len(self.__tmpProjects) == 0:
841 return False
842
843 return True
844
845 def __readStdoutLupdate(self, proc):
846 """
847 Private slot to handle the readyReadStandardOutput signal of the
848 pylupdate process.
849
850 @param proc reference to the process
851 @type QProcess
852 """
853 self.__readStdout(proc, '{0}: '.format(self.pylupdate))
854
855 def __readStdoutLrelease(self, proc):
856 """
857 Private slot to handle the readyReadStandardOutput signal of the
858 lrelease process.
859
860 @param proc reference to the process
861 @type QProcess
862 """
863 self.__readStdout(proc, 'lrelease: ')
864
865 def __readStdout(self, proc, ps):
866 """
867 Private method to read from a process' stdout channel.
868
869 @param proc process to read from (QProcess)
870 @param ps prompt string (string)
871 """
872 ioEncoding = Preferences.getSystem("IOEncoding")
873
874 proc.setReadChannel(QProcess.ProcessChannel.StandardOutput)
875 while proc and proc.canReadLine():
876 s = ps
877 output = str(proc.readLine(), ioEncoding, 'replace')
878 s += output
879 self.appendStdout.emit(s)
880
881 def __readStderrLupdate(self, proc):
882 """
883 Private slot to handle the readyReadStandardError signal of the
884 pylupdate5 / pylupdate6 / pyside2-lupdate / pyside6-lupdate process.
885
886 @param proc reference to the process
887 @type QProcess
888 """
889 self.__readStderr(proc, '{0}: '.format(self.pylupdate))
890
891 def __readStderrLrelease(self, proc):
892 """
893 Private slot to handle the readyReadStandardError signal of the
894 lrelease process.
895
896 @param proc reference to the process
897 @type QProcess
898 """
899 self.__readStderr(proc, 'lrelease: ')
900
901 def __readStderr(self, proc, ps):
902 """
903 Private method to read from a process' stderr channel.
904
905 @param proc process to read from (QProcess)
906 @param ps propmt string (string)
907 """
908 ioEncoding = Preferences.getSystem("IOEncoding")
909
910 proc.setReadChannel(QProcess.ProcessChannel.StandardError)
911 while proc and proc.canReadLine():
912 s = ps
913 error = str(proc.readLine(), ioEncoding, 'replace')
914 s += error
915 self.appendStderr.emit(s)
916
917 ###########################################################################
918 ## Methods for the generation commands
919 ###########################################################################
920
921 def __extractMessages(self):
922 """
923 Private slot to extract the messages to form a messages template file.
924 """
925 if self.hooks["extractMessages"] is not None:
926 self.hooks["extractMessages"]()
927
928 def __generateTSFileDone(self, proc, exitCode, exitStatus):
929 """
930 Private slot to handle the finished signal of the pylupdate process.
931
932 @param proc reference to the process
933 @type QProcess
934 @param exitCode exit code of the process
935 @type int
936 @param exitStatus exit status of the process
937 @type QProcess.ExitStatus
938 """
939 ui = e5App().getObject("UserInterface")
940 if exitStatus == QProcess.ExitStatus.NormalExit and exitCode == 0:
941 ui.showNotification(
942 UI.PixmapCache.getPixmap("linguist48"),
943 self.tr("Translation file generation"),
944 self.tr(
945 "The generation of the translation files (*.ts)"
946 " was successful."))
947 else:
948 if exitStatus == QProcess.ExitStatus.CrashExit:
949 info = self.tr(" The process has crashed.")
950 else:
951 info = ""
952 ui.showNotification(
953 UI.PixmapCache.getPixmap("linguist48"),
954 self.tr("Translation file generation"),
955 self.tr(
956 "The generation of the translation files (*.ts) has"
957 " failed.{0}").format(info),
958 kind=NotificationTypes.CRITICAL,
959 timeout=0)
960
961 for index in range(len(self.__pylupdateProcesses)):
962 if proc == self.__pylupdateProcesses[index][0]:
963 tmpProjectFile = self.__pylupdateProcesses[index][1]
964 if tmpProjectFile:
965 with contextlib.suppress(OSError):
966 self.__tmpProjects.remove(tmpProjectFile)
967 os.remove(tmpProjectFile)
968 del self.__pylupdateProcesses[index]
969 break
970
971 if not self.__pylupdateProcesses:
972 # all done
973 self.pylupdateProcRunning = False
974
975 def __generateTSFile(self, noobsolete=False, generateAll=True):
976 """
977 Private method used to run pylupdate5 / pylupdate6 / pyside2-lupdate /
978 pyside6-lupdate to generate the .ts files.
979
980 @param noobsolete flag indicating whether obsolete entries should be
981 kept (boolean)
982 @param generateAll flag indicating whether all translations should be
983 generated (boolean)
984 """
985 langs = [] if generateAll else self.getSelectedItems()
986
987 # Hook support
988 if generateAll:
989 if noobsolete:
990 if self.hooks["generateAll"] is not None:
991 self.hooks["generateAll"](
992 self.project.pdata["TRANSLATIONS"])
993 return
994 else:
995 if self.hooks["generateAllWithObsolete"] is not None:
996 self.hooks["generateAllWithObsolete"](
997 self.project.pdata["TRANSLATIONS"])
998 return
999 else:
1000 if noobsolete:
1001 if self.hooks["generateSelected"] is not None:
1002 li = [self.project.getRelativePath(lang.fileName())
1003 for lang in langs]
1004 self.hooks["generateSelected"](li)
1005 return
1006 else:
1007 if self.hooks["generateSelectedWithObsolete"] is not None:
1008 li = [self.project.getRelativePath(lang.fileName())
1009 for lang in langs]
1010 self.hooks["generateSelectedWithObsolete"](li)
1011 return
1012
1013 # generate a minimal temporary project file suitable for pylupdate
1014 self.__tmpProjects = []
1015 if self.project.getProjectLanguage() in [
1016 "Python", "Python3"
1017 ]:
1018 if self.project.getProjectType() not in ["PyQt6", "PyQt6C"]:
1019 ok = self.__writeTempProjectFile(langs, [".py"])
1020 if not ok:
1021 return
1022 else:
1023 return
1024
1025 if self.project.getProjectType() in ["PyQt5", "PyQt5C", "E6Plugin"]:
1026 self.pylupdate = Utilities.generatePyQtToolPath('pylupdate5')
1027 elif self.project.getProjectType() in ["PyQt6", "PyQt6C"]:
1028 self.pylupdate = Utilities.generatePyQtToolPath('pylupdate6')
1029 elif self.project.getProjectType() in ["PySide2", "PySide2C"]:
1030 self.pylupdate = Utilities.generatePySideToolPath(
1031 'pyside2-lupdate', variant=2)
1032 elif self.project.getProjectType() in ["PySide6", "PySide6C"]:
1033 self.pylupdate = Utilities.generatePySideToolPath(
1034 'pyside6-lupdate', variant=6)
1035 else:
1036 return
1037
1038 self.__pylupdateProcesses = []
1039 if self.project.getProjectType() in ["PyQt6", "PyQt6C"]:
1040 if langs:
1041 langs = [self.project.getRelativePath(lang.fileName())
1042 for lang in langs if lang.fileName().endswith('.ts')]
1043 else:
1044 try:
1045 pattern = self.project.pdata["TRANSLATIONPATTERN"].replace(
1046 "%language%", "*")
1047 langs = [
1048 lang for lang in self.project.pdata["TRANSLATIONS"]
1049 if fnmatch.fnmatch(lang, pattern)
1050 ]
1051 except IndexError:
1052 langs = []
1053 if not langs:
1054 E5MessageBox.warning(
1055 self,
1056 self.tr("Translation file generation"),
1057 self.tr("""No translation files (*.ts) selected."""))
1058 return
1059 for lang in langs:
1060 proc = QProcess()
1061 args = []
1062
1063 if noobsolete:
1064 args.append('--no-obsolete')
1065
1066 args += ["--ts", lang]
1067 args.append(".")
1068
1069 proc.setWorkingDirectory(self.project.ppath)
1070 proc.finished.connect(
1071 functools.partial(self.__generateTSFileDone, proc)
1072 )
1073 proc.readyReadStandardOutput.connect(
1074 functools.partial(self.__readStdoutLupdate, proc)
1075 )
1076 proc.readyReadStandardError.connect(
1077 functools.partial(self.__readStderrLupdate, proc)
1078 )
1079
1080 proc.start(self.pylupdate, args)
1081 procStarted = proc.waitForStarted()
1082 if procStarted:
1083 self.pylupdateProcRunning = True
1084 self.__pylupdateProcesses.append((proc, ""))
1085 else:
1086 E5MessageBox.critical(
1087 self,
1088 self.tr('Process Generation Error'),
1089 self.tr(
1090 'Could not start {0}.<br>'
1091 'Ensure that it is in the search path.'
1092 ).format(self.pylupdate))
1093 else:
1094 for tempProjectFile in self.__tmpProjects[:]:
1095 proc = QProcess()
1096 args = []
1097
1098 if noobsolete:
1099 args.append('-noobsolete')
1100
1101 args.append('-verbose')
1102 path, filename = os.path.split(tempProjectFile)
1103 args.append(filename)
1104 proc.setWorkingDirectory(
1105 os.path.join(self.project.ppath, path))
1106 proc.finished.connect(
1107 functools.partial(self.__generateTSFileDone, proc)
1108 )
1109 proc.readyReadStandardOutput.connect(
1110 functools.partial(self.__readStdoutLupdate, proc)
1111 )
1112 proc.readyReadStandardError.connect(
1113 functools.partial(self.__readStderrLupdate, proc)
1114 )
1115
1116 proc.start(self.pylupdate, args)
1117 procStarted = proc.waitForStarted()
1118 if procStarted:
1119 self.pylupdateProcRunning = True
1120 self.__pylupdateProcesses.append((proc, tempProjectFile))
1121 else:
1122 E5MessageBox.critical(
1123 self,
1124 self.tr('Process Generation Error'),
1125 self.tr(
1126 'Could not start {0}.<br>'
1127 'Ensure that it is in the search path.'
1128 ).format(self.pylupdate))
1129 # cleanup
1130 with contextlib.suppress(OSError):
1131 self.__tmpProjects.remove(tempProjectFile)
1132 os.remove(tempProjectFile)
1133
1134 def __generateAll(self):
1135 """
1136 Private method to generate all translation files (.ts) for Qt Linguist.
1137
1138 All obsolete strings are removed from the .ts file.
1139 """
1140 self.__generateTSFile(noobsolete=True, generateAll=True)
1141
1142 def __generateObsoleteAll(self):
1143 """
1144 Private method to generate all translation files (.ts) for Qt Linguist.
1145
1146 Obsolete strings are kept.
1147 """
1148 self.__generateTSFile(noobsolete=False, generateAll=True)
1149
1150 def __generateSelected(self):
1151 """
1152 Private method to generate selected translation files (.ts) for
1153 Qt Linguist.
1154
1155 All obsolete strings are removed from the .ts file.
1156 """
1157 self.__generateTSFile(noobsolete=True, generateAll=False)
1158
1159 def __generateObsoleteSelected(self):
1160 """
1161 Private method to generate selected translation files (.ts) for
1162 Qt Linguist.
1163
1164 Obsolete strings are kept.
1165 """
1166 self.__generateTSFile(noobsolete=False, generateAll=False)
1167
1168 ###########################################################################
1169 ## Methods for the release commands
1170 ###########################################################################
1171
1172 def __releaseTSFileDone(self, proc, exitCode, exitStatus):
1173 """
1174 Private slot to handle the finished signal of the lrelease process.
1175
1176 @param proc reference to the process
1177 @type QProcess
1178 @param exitCode exit code of the process
1179 @type int
1180 @param exitStatus exit status of the process
1181 @type QProcess.ExitStatus
1182 """
1183 ui = e5App().getObject("UserInterface")
1184 if exitStatus == QProcess.ExitStatus.NormalExit and exitCode == 0:
1185 ui.showNotification(
1186 UI.PixmapCache.getPixmap("linguist48"),
1187 self.tr("Translation file release"),
1188 self.tr("The release of the translation files (*.qm)"
1189 " was successful."))
1190 if self.project.pdata["TRANSLATIONSBINPATH"]:
1191 target = os.path.join(
1192 self.project.ppath,
1193 self.project.pdata["TRANSLATIONSBINPATH"])
1194 for langFile in self.project.pdata["TRANSLATIONS"][:]:
1195 if langFile.endswith('.ts'):
1196 qmFile = os.path.join(self.project.ppath,
1197 langFile.replace('.ts', '.qm'))
1198 if os.path.exists(qmFile):
1199 shutil.move(qmFile, target)
1200 else:
1201 ui.showNotification(
1202 UI.PixmapCache.getPixmap("linguist48"),
1203 self.tr("Translation file release"),
1204 self.tr(
1205 "The release of the translation files (*.qm) has failed."),
1206 kind=NotificationTypes.CRITICAL,
1207 timeout=0)
1208
1209 for index in range(len(self.__lreleaseProcesses)):
1210 if proc == self.__lreleaseProcesses[index]:
1211 del self.__lreleaseProcesses[index]
1212 break
1213 if not self.__lreleaseProcesses:
1214 # all done
1215 self.lreleaseProcRunning = False
1216 self.project.checkLanguageFiles()
1217
1218 def __releaseTSFile(self, generateAll=False):
1219 """
1220 Private method to run lrelease to release the translation files (.qm).
1221
1222 @param generateAll flag indicating whether all translations should be
1223 released (boolean)
1224 """
1225 langs = [] if generateAll else self.getSelectedItems()
1226
1227 # Hooks support
1228 if generateAll:
1229 if self.hooks["releaseAll"] is not None:
1230 self.hooks["releaseAll"](self.project.pdata["TRANSLATIONS"])
1231 return
1232 else:
1233 if self.hooks["releaseSelected"] is not None:
1234 li = [self.project.getRelativePath(lang.fileName())
1235 for lang in langs]
1236 self.hooks["releaseSelected"](li)
1237 return
1238
1239 if self.project.getProjectType() in [
1240 "PyQt5", "PyQt5C", "PyQt6", "PyQt6C", "E6Plugin",
1241 "PySide2", "PySide2C", "PySide6", "PySide6C"
1242 ]:
1243 lrelease = os.path.join(
1244 Utilities.getQtBinariesPath(),
1245 Utilities.generateQtToolName("lrelease"))
1246 else:
1247 return
1248 if Utilities.isWindowsPlatform():
1249 lrelease += '.exe'
1250
1251 if langs:
1252 langs = [self.project.getRelativePath(lang.fileName())
1253 for lang in langs if lang.fileName().endswith('.ts')]
1254 else:
1255 try:
1256 pattern = self.project.pdata["TRANSLATIONPATTERN"].replace(
1257 "%language%", "*")
1258 langs = [lang for lang in self.project.pdata["TRANSLATIONS"]
1259 if fnmatch.fnmatch(lang, pattern)]
1260 except IndexError:
1261 langs = []
1262 if not langs:
1263 E5MessageBox.warning(
1264 self,
1265 self.tr("Write temporary project file"),
1266 self.tr("""No translation files (*.ts) selected."""))
1267 return
1268
1269 self.__lreleaseProcesses = []
1270 args = []
1271 args.append('-verbose')
1272 for langFile in langs:
1273 path, filename = os.path.split(langFile)
1274 args.append(filename)
1275
1276 proc = QProcess()
1277 proc.setWorkingDirectory(os.path.join(self.project.ppath, path))
1278 proc.finished.connect(
1279 functools.partial(self.__releaseTSFileDone, proc)
1280 )
1281 proc.readyReadStandardOutput.connect(
1282 functools.partial(self.__readStdoutLrelease, proc)
1283 )
1284 proc.readyReadStandardError.connect(
1285 functools.partial(self.__readStderrLrelease, proc)
1286 )
1287
1288 proc.start(lrelease, args)
1289 procStarted = proc.waitForStarted()
1290 if procStarted:
1291 self.lreleaseProcRunning = True
1292 self.__lreleaseProcesses.append(proc)
1293 else:
1294 E5MessageBox.critical(
1295 self,
1296 self.tr('Process Generation Error'),
1297 self.tr(
1298 '<p>Could not start lrelease.<br>'
1299 'Ensure that it is available as <b>{0}</b>.</p>'
1300 ).format(lrelease))
1301
1302 def __releaseSelected(self):
1303 """
1304 Private method to release the translation files (.qm).
1305 """
1306 self.__releaseTSFile(generateAll=False)
1307
1308 def __releaseAll(self):
1309 """
1310 Private method to release the translation files (.qm).
1311 """
1312 self.__releaseTSFile(generateAll=True)
1313
1314 ###########################################################################
1315 ## Support for hooks below
1316 ###########################################################################
1317
1318 def _initHookMethods(self):
1319 """
1320 Protected method to initialize the hooks dictionary.
1321
1322 Supported hook methods are:
1323 <ul>
1324 <li>extractMessages: takes no parameters</li>
1325 <li>generateAll: takes list of filenames as parameter</li>
1326 <li>generateAllWithObsolete: takes list of filenames as parameter</li>
1327 <li>generateSelected: takes list of filenames as parameter</li>
1328 <li>generateSelectedWithObsolete: takes list of filenames as
1329 parameter</li>
1330 <li>releaseAll: takes list of filenames as parameter</li>
1331 <li>releaseSelected: takes list of filenames as parameter</li>
1332 <li>open: takes a filename as parameter</li>
1333 </ul>
1334
1335 <b>Note</b>: Filenames are relative to the project directory.
1336 """
1337 self.hooks = {
1338 "extractMessages": None,
1339 "generateAll": None,
1340 "generateAllWithObsolete": None,
1341 "generateSelected": None,
1342 "generateSelectedWithObsolete": None,
1343 "releaseAll": None,
1344 "releaseSelected": None,
1345 "open": None,
1346 }

eric ide

mercurial