Add search and replace features

Julien LepillerSat Dec 12 15:40:23+0100 2020

16901b3

Add search and replace features

offlate/ui/editor.py

2121
from .spellcheckedit import SpellCheckEdit
2222
from .tagclickedit import TagClickEdit
2323
from .parallel import RunnableCallback, RunnableSignals
24+
from .search import SearchWindow
2425
from ..systems.callback import SystemCallback
2526
2627
import math

3233
from watchdog.events import FileSystemEventHandler
3334
from pathlib import Path
3435
from time import sleep
36+
import re
3537
3638
class ProjectTab(QTabWidget):
3739
    def __init__(self, parent = None):

364366
            item.setBackground(1, self.emptyColor)
365367
        self.translationModified.emit()
366368
367-
368369
    def save(self):
369370
        self.project.save()
370371

384385
        self.showFuzzy = showFuzzy
385386
        self.updateContent()
386387
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+
387499
    def setFont(self, monospace):
388500
        self.monospace = monospace
389501
        current = self.treeWidget.currentItem()

492604
    def settings(self):
493605
        self.projectManagerWindow.settings()
494606
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+
495627
    def filter(self):
496628
        for i in range(0, self.tabs.count()):
497629
            self.tabs.widget(i).filter(

551683
        settingsAct.setStatusTip(self.tr('Set parameters'))
552684
        settingsAct.triggered.connect(self.settings)
553685
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+
554696
        self.showTranslatedAct = QAction(self.tr('Show Translated'), self, checkable=True)
555697
        self.showTranslatedAct.setChecked(True)
556698
        self.showTranslatedAct.triggered.connect(self.filter)

606748
607749
        editMenu = menubar.addMenu(self.tr('&Edit'))
608750
        editMenu.addAction(settingsAct)
751+
        editMenu.addAction(searchAct)
752+
        editMenu.addAction(replaceAct)
609753
610754
        viewMenu = menubar.addMenu(self.tr('&View'))
611755
        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()