Add search and replace features
offlate/ui/editor.py
21 | 21 | from .spellcheckedit import SpellCheckEdit | |
22 | 22 | from .tagclickedit import TagClickEdit | |
23 | 23 | from .parallel import RunnableCallback, RunnableSignals | |
24 | + | from .search import SearchWindow | |
24 | 25 | from ..systems.callback import SystemCallback | |
25 | 26 | ||
26 | 27 | import math | |
… | |||
32 | 33 | from watchdog.events import FileSystemEventHandler | |
33 | 34 | from pathlib import Path | |
34 | 35 | from time import sleep | |
36 | + | import re | |
35 | 37 | ||
36 | 38 | class ProjectTab(QTabWidget): | |
37 | 39 | def __init__(self, parent = None): | |
… | |||
364 | 366 | item.setBackground(1, self.emptyColor) | |
365 | 367 | self.translationModified.emit() | |
366 | 368 | ||
367 | - | ||
368 | 369 | def save(self): | |
369 | 370 | self.project.save() | |
370 | 371 | ||
… | |||
384 | 385 | self.showFuzzy = showFuzzy | |
385 | 386 | self.updateContent() | |
386 | 387 | ||
388 | + | def searchNext(self, text, options): | |
389 | + | regexp = self._getRegexp(text, options) | |
390 | + | item = self.treeWidget.currentItem() | |
391 | + | index = self.treeWidget.indexOfTopLevelItem(item) | |
392 | + | nextItem = item | |
393 | + | while True: | |
394 | + | item = nextItem | |
395 | + | nextItem = self.treeWidget.itemBelow(item) | |
396 | + | if nextItem is None: | |
397 | + | if not options.wrap: | |
398 | + | return | |
399 | + | nextItem = self.treeWidget.topLevelItem(0) | |
400 | + | if self.treeWidget.indexOfTopLevelItem(nextItem) == index: | |
401 | + | return | |
402 | + | if index == -1 and self.treeWidget.indexOfTopLevelItem(nextItem) == 0: | |
403 | + | index = 0 | |
404 | + | if self._match(nextItem, regexp, options): | |
405 | + | self.treeWidget.setCurrentItem(nextItem) | |
406 | + | return | |
407 | + | ||
408 | + | def searchPrevious(self, text, options): | |
409 | + | regexp = self._getRegexp(text, options) | |
410 | + | item = self.treeWidget.currentItem() | |
411 | + | index = self.treeWidget.indexOfTopLevelItem(item) | |
412 | + | if index == -1: | |
413 | + | index = 0 | |
414 | + | previous = item | |
415 | + | while True: | |
416 | + | item = previous | |
417 | + | previous = self.treeWidget.itemAbove(item) | |
418 | + | if previous is None: | |
419 | + | if not options.wrap: | |
420 | + | return | |
421 | + | previous = self.treeWidget.topLevelItem(self.treeWidget.topLevelItemCount()-1) | |
422 | + | if self.treeWidget.indexOfTopLevelItem(previous) == index: | |
423 | + | return | |
424 | + | if self._match(previous, regexp, options): | |
425 | + | self.treeWidget.setCurrentItem(previous) | |
426 | + | return | |
427 | + | ||
428 | + | def _getRegexp(self, text, options): | |
429 | + | if options.word: | |
430 | + | regexp = '\b'+text+'\b' | |
431 | + | else: | |
432 | + | regexp = text | |
433 | + | if options.case: | |
434 | + | regexp = re.compile(regexp, re.IGNORECASE) | |
435 | + | else: | |
436 | + | regexp = re.compile(regexp) | |
437 | + | return regexp | |
438 | + | ||
439 | + | def _match(self, item, regexp, options): | |
440 | + | data = item.data(0, Qt.UserRole) | |
441 | + | ||
442 | + | if options.trans: | |
443 | + | if isinstance(data.msgstrs, list): | |
444 | + | for i in range(0, len(data.msgstrs)): | |
445 | + | if regexp.search(data.msgstrs[i]) is not None: | |
446 | + | return True | |
447 | + | else: # dict | |
448 | + | if regexp.search(data.msgstrs) is not None: | |
449 | + | return True | |
450 | + | ||
451 | + | if options.orig: | |
452 | + | if isinstance(data.msgids, list): | |
453 | + | for i in range(0, len(data.msgids)): | |
454 | + | if regexp.search(data.msgids[i]) is not None: | |
455 | + | return True | |
456 | + | else: #dict | |
457 | + | for k in data.msgids: | |
458 | + | if regexp.search(data.msgids[k]) is not None: | |
459 | + | return True | |
460 | + | return False | |
461 | + | ||
462 | + | def replaceAll(self, search, replace, options): | |
463 | + | regexp = self._getRegexp(search, options) | |
464 | + | item = self.treeWidget.currentItem() | |
465 | + | index = self.treeWidget.indexOfTopLevelItem(item) | |
466 | + | for i in range(0 if options.wrap else index, self.treeWidget.topLevelItemCount()): | |
467 | + | item = self.treeWidget.topLevelItem(i) | |
468 | + | self._replaceItem(regexp, replace, options, item) | |
469 | + | ||
470 | + | def replaceHere(self, search, replace, options): | |
471 | + | regexp = self._getRegexp(search, options) | |
472 | + | item = self.treeWidget.currentItem() | |
473 | + | self._replaceItem(regexp, replace, options, item) | |
474 | + | ||
475 | + | def _replaceItem(self, regexp, replace, options, item): | |
476 | + | data = item.data(0, Qt.UserRole) | |
477 | + | if isinstance(data.msgstrs, list): | |
478 | + | for i in range(0, len(data.msgstrs)): | |
479 | + | msgstr = data.get(i) | |
480 | + | msgstr = regexp.sub(replace, msgstr) | |
481 | + | data.update(i, msgstr) | |
482 | + | msgstr = data.get(0) | |
483 | + | item.setText(1, msgstr.replace('\n', ' ')) | |
484 | + | else: # dict | |
485 | + | for k in data.msgstrs: | |
486 | + | msgstr = data.msgstrs[k] | |
487 | + | msgstr = regexp.sub(replace, msgstr) | |
488 | + | data.update(i, msgstr) | |
489 | + | msgstr = data.get(0) | |
490 | + | item.setText(1, msgstr.replace('\n', ' ')) | |
491 | + | ||
492 | + | item.setForeground(1, QBrush()) | |
493 | + | if data.isTranslated(): | |
494 | + | item.setBackground(1, QBrush()) | |
495 | + | else: | |
496 | + | item.setBackground(1, self.emptyColor) | |
497 | + | self.translationModified.emit() | |
498 | + | ||
387 | 499 | def setFont(self, monospace): | |
388 | 500 | self.monospace = monospace | |
389 | 501 | current = self.treeWidget.currentItem() | |
… | |||
492 | 604 | def settings(self): | |
493 | 605 | self.projectManagerWindow.settings() | |
494 | 606 | ||
607 | + | def search(self): | |
608 | + | w = SearchWindow(self) | |
609 | + | w.exec() | |
610 | + | ||
611 | + | def replace(self): | |
612 | + | w = SearchWindow(self, True) | |
613 | + | w.exec() | |
614 | + | ||
615 | + | def searchNext(self, text, options): | |
616 | + | self.tabs.currentWidget().searchNext(text, options) | |
617 | + | ||
618 | + | def searchPrevious(self, text, options): | |
619 | + | self.tabs.currentWidget().searchPrevious(text, options) | |
620 | + | ||
621 | + | def replaceAll(self, search, replace, options): | |
622 | + | self.tabs.currentWidget().replaceAll(search, replace, options) | |
623 | + | ||
624 | + | def replaceHere(self, search, replace, options): | |
625 | + | self.tabs.currentWidget().replaceHere(search, replace, options) | |
626 | + | ||
495 | 627 | def filter(self): | |
496 | 628 | for i in range(0, self.tabs.count()): | |
497 | 629 | self.tabs.widget(i).filter( | |
… | |||
551 | 683 | settingsAct.setStatusTip(self.tr('Set parameters')) | |
552 | 684 | settingsAct.triggered.connect(self.settings) | |
553 | 685 | ||
686 | + | searchAct = QAction(QIcon('search.png'), self.tr('Search'), self) | |
687 | + | searchAct.setShortcut('Ctrl+F') | |
688 | + | searchAct.setStatusTip(self.tr('Search in the document')) | |
689 | + | searchAct.triggered.connect(self.search) | |
690 | + | ||
691 | + | replaceAct = QAction(QIcon('search.png'), self.tr('Replace'), self) | |
692 | + | replaceAct.setShortcut('Ctrl+R') | |
693 | + | replaceAct.setStatusTip(self.tr('Replace content in the document')) | |
694 | + | replaceAct.triggered.connect(self.replace) | |
695 | + | ||
554 | 696 | self.showTranslatedAct = QAction(self.tr('Show Translated'), self, checkable=True) | |
555 | 697 | self.showTranslatedAct.setChecked(True) | |
556 | 698 | self.showTranslatedAct.triggered.connect(self.filter) | |
… | |||
606 | 748 | ||
607 | 749 | editMenu = menubar.addMenu(self.tr('&Edit')) | |
608 | 750 | editMenu.addAction(settingsAct) | |
751 | + | editMenu.addAction(searchAct) | |
752 | + | editMenu.addAction(replaceAct) | |
609 | 753 | ||
610 | 754 | viewMenu = menubar.addMenu(self.tr('&View')) | |
611 | 755 | viewMenu.addAction(self.showTranslatedAct) |
offlate/ui/search.py unknown status 1
1 | + | # Copyright (c) 2019 Julien Lepiller <julien@lepiller.eu> | |
2 | + | # | |
3 | + | # This program is free software: you can redistribute it and/or modify | |
4 | + | # it under the terms of the GNU Affero General Public License as | |
5 | + | # published by the Free Software Foundation, either version 3 of the | |
6 | + | # License, or (at your option) any later version. | |
7 | + | # | |
8 | + | # This program is distributed in the hope that it will be useful, | |
9 | + | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
10 | + | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
11 | + | # GNU Affero General Public License for more details. | |
12 | + | # | |
13 | + | # You should have received a copy of the GNU Affero General Public License | |
14 | + | # along with this program. If not, see <https://www.gnu.org/licenses/>. | |
15 | + | #### | |
16 | + | ||
17 | + | from PyQt5.QtWidgets import * | |
18 | + | from PyQt5.QtGui import * | |
19 | + | from PyQt5.QtCore import * | |
20 | + | ||
21 | + | class SearchOptions: | |
22 | + | def __init__(self, orig, trans, comm, case, wrap, word): | |
23 | + | self.orig = orig | |
24 | + | self.trans = trans | |
25 | + | self.comm = comm | |
26 | + | self.case = case | |
27 | + | self.wrap = wrap | |
28 | + | self.word = word | |
29 | + | ||
30 | + | class SearchWindow(QDialog): | |
31 | + | def __init__(self, parent = None, replace = False): | |
32 | + | super().__init__(parent) | |
33 | + | self.editor = parent | |
34 | + | self.replace = replace | |
35 | + | self.initUI() | |
36 | + | ||
37 | + | def showOrHideReplace(self): | |
38 | + | self.replaceField.setVisible(self.replace) | |
39 | + | self.searchRadio.setChecked(not self.replace) | |
40 | + | self.replaceRadio.setChecked(self.replace) | |
41 | + | self.replaceAllButton.setVisible(self.replace) | |
42 | + | self.replaceButton.setVisible(self.replace) | |
43 | + | self.origCheckBox.setChecked(self.origCheckBox.isChecked() and not self.replace) | |
44 | + | self.commCheckBox.setChecked(self.origCheckBox.isChecked() and not self.replace) | |
45 | + | self.origCheckBox.setEnabled(not self.replace) | |
46 | + | self.commCheckBox.setEnabled(not self.replace) | |
47 | + | ||
48 | + | def setReplace(self, check): | |
49 | + | if not check: | |
50 | + | return | |
51 | + | self.replace = True | |
52 | + | self.showOrHideReplace() | |
53 | + | ||
54 | + | def setSearch(self, check): | |
55 | + | if not check: | |
56 | + | return | |
57 | + | self.replace = False | |
58 | + | self.showOrHideReplace() | |
59 | + | ||
60 | + | def _getOptionObject(self): | |
61 | + | return SearchOptions(self.origCheckBox.isChecked(), | |
62 | + | self.transCheckBox.isChecked(), self.commCheckBox.isChecked(), | |
63 | + | self.caseCheckBox.isChecked(), self.wrapCheckBox.isChecked(), | |
64 | + | self.wordCheckBox.isChecked()) | |
65 | + | ||
66 | + | def searchNext(self): | |
67 | + | self.editor.searchNext(self.searchField.text(), self._getOptionObject()) | |
68 | + | ||
69 | + | def searchPrevious(self): | |
70 | + | self.editor.searchPrevious(self.searchField.text(), | |
71 | + | self._getOptionObject()) | |
72 | + | ||
73 | + | def replaceAll(self): | |
74 | + | self.editor.replaceAll(self.searchField.text(), self.replaceField.text(), | |
75 | + | self._getOptionObject()) | |
76 | + | ||
77 | + | def replaceHere(self): | |
78 | + | self.editor.replaceHere(self.searchField.text(), self.replaceField.text(), | |
79 | + | self._getOptionObject()) | |
80 | + | ||
81 | + | def initUI(self): | |
82 | + | vbox = QVBoxLayout() | |
83 | + | self.searchField = QLineEdit() | |
84 | + | self.searchField.setPlaceholderText(self.tr('String to search for')) | |
85 | + | vbox.addWidget(self.searchField) | |
86 | + | self.replaceField = QLineEdit() | |
87 | + | self.replaceField.setPlaceholderText(self.tr('String to replace into')) | |
88 | + | vbox.addWidget(self.replaceField) | |
89 | + | ||
90 | + | typeBox = QHBoxLayout() | |
91 | + | group = QGroupBox() | |
92 | + | self.searchRadio = QRadioButton(self.tr('Search')) | |
93 | + | self.replaceRadio = QRadioButton(self.tr('Replace')) | |
94 | + | self.searchRadio.clicked.connect(self.setSearch) | |
95 | + | self.replaceRadio.clicked.connect(self.setReplace) | |
96 | + | typeBox.addWidget(self.searchRadio) | |
97 | + | typeBox.addWidget(self.replaceRadio) | |
98 | + | group.setLayout(typeBox) | |
99 | + | vbox.addWidget(group) | |
100 | + | ||
101 | + | options = QGridLayout() | |
102 | + | self.caseCheckBox = QCheckBox(self.tr('Case sensitive')) | |
103 | + | self.wrapCheckBox = QCheckBox(self.tr('Wrap around')) | |
104 | + | self.wrapCheckBox.setChecked(True) | |
105 | + | self.wordCheckBox = QCheckBox(self.tr('Match whole word only')) | |
106 | + | self.origCheckBox = QCheckBox(self.tr('Search in original text')) | |
107 | + | self.origCheckBox.setChecked(True) | |
108 | + | self.transCheckBox = QCheckBox(self.tr('Search in translated text')) | |
109 | + | self.transCheckBox.setChecked(True) | |
110 | + | self.commCheckBox = QCheckBox(self.tr('Search in comments')) | |
111 | + | options.addWidget(self.caseCheckBox, 0, 0) | |
112 | + | options.addWidget(self.wrapCheckBox, 1, 0) | |
113 | + | options.addWidget(self.wordCheckBox, 2, 0) | |
114 | + | options.addWidget(self.origCheckBox, 0, 1) | |
115 | + | options.addWidget(self.transCheckBox, 1, 1) | |
116 | + | options.addWidget(self.commCheckBox, 2, 1) | |
117 | + | vbox.addLayout(options) | |
118 | + | ||
119 | + | vbox.addStretch(1) | |
120 | + | ||
121 | + | buttons = QHBoxLayout() | |
122 | + | closeButton = QPushButton(self.tr('Close')) | |
123 | + | closeButton.clicked.connect(self.close) | |
124 | + | buttons.addWidget(closeButton) | |
125 | + | buttons.addStretch(1) | |
126 | + | self.replaceAllButton = QPushButton(self.tr('Replace all')) | |
127 | + | self.replaceAllButton.clicked.connect(self.replaceAll) | |
128 | + | self.replaceButton = QPushButton(self.tr('Replace one')) | |
129 | + | self.replaceButton.clicked.connect(self.replaceHere) | |
130 | + | previousButton = QPushButton(self.tr('< Previous')) | |
131 | + | previousButton.clicked.connect(self.searchPrevious) | |
132 | + | nextButton = QPushButton(self.tr('Next >')) | |
133 | + | nextButton.setDefault(True) | |
134 | + | nextButton.setAutoDefault(True) | |
135 | + | nextButton.clicked.connect(self.searchNext) | |
136 | + | buttons.addWidget(self.replaceAllButton) | |
137 | + | buttons.addWidget(self.replaceButton) | |
138 | + | buttons.addWidget(previousButton) | |
139 | + | buttons.addWidget(nextButton) | |
140 | + | vbox.addLayout(buttons) | |
141 | + | ||
142 | + | self.setLayout(vbox) | |
143 | + | self.showOrHideReplace() |