1 /*
2  * Copyright (C) Pedram Pourang (aka Tsu Jan) 2014-2019 <tsujan2000@gmail.com>
3  *
4  * FeatherPad is free software: you can redistribute it and/or modify it
5  * under the terms of the GNU General Public License as published by the
6  * Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * FeatherPad is distributed in the hope that it will be useful, but
10  * WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12  * See the GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License along
15  * with this program.  If not, see <http://www.gnu.org/licenses/>.
16  *
17  * @license GPL-3.0+ <https://spdx.org/licenses/GPL-3.0+.html>
18  */
19 
20 #include "fpwin.h"
21 #include "ui_fp.h"
22 #include <QTextDocumentFragment>
23 
24 namespace FeatherPad {
25 
26 /* This order is preserved everywhere for selections:
27    current line -> replacement -> found matches -> selection highlights -> bracket matches */
28 
find(bool forward)29 void FPwin::find (bool forward)
30 {
31     if (!isReady()) return;
32 
33     TabPage *tabPage = qobject_cast< TabPage *>(ui->tabWidget->currentWidget());
34     if (tabPage == nullptr) return;
35 
36     TextEdit *textEdit = tabPage->textEdit();
37     QString txt = tabPage->searchEntry();
38     bool newSrch = false;
39     if (textEdit->getSearchedText() != txt)
40     {
41         textEdit->setSearchedText (txt);
42         newSrch = true;
43     }
44 
45     disconnect (textEdit, &TextEdit::resized, this, &FPwin::hlight);
46     disconnect (textEdit, &TextEdit::updateRect, this, &FPwin::hlight);
47     disconnect (textEdit, &QPlainTextEdit::textChanged, this, &FPwin::hlight);
48 
49     if (txt.isEmpty())
50     {
51         /* remove all yellow and green highlights */
52         QList<QTextEdit::ExtraSelection> es;
53         textEdit->setGreenSel (es); // not needed
54         if (ui->actionLineNumbers->isChecked() || ui->spinBox->isVisible())
55             es.prepend (textEdit->currentLineSelection());
56         es.append (textEdit->getBlueSel());
57         es.append (textEdit->getRedSel());
58         textEdit->setExtraSelections (es);
59         return;
60     }
61 
62     QTextDocument::FindFlags searchFlags = getSearchFlags();
63     QTextDocument::FindFlags newFlags = searchFlags;
64     if (!forward)
65         newFlags = searchFlags | QTextDocument::FindBackward;
66     QTextCursor start = textEdit->textCursor();
67     QTextCursor found = textEdit->finding (txt, start, newFlags, tabPage->matchRegex());
68 
69     if (found.isNull())
70     {
71         if (!forward)
72             start.movePosition (QTextCursor::End, QTextCursor::MoveAnchor);
73         else
74             start.movePosition (QTextCursor::Start, QTextCursor::MoveAnchor);
75         found = textEdit->finding (txt, start, newFlags, tabPage->matchRegex());
76     }
77 
78     if (!found.isNull())
79     {
80         start.setPosition (found.anchor());
81         /* this is needed for selectionChanged() to be emitted */
82         if (newSrch) textEdit->setTextCursor (start);
83         start.setPosition (found.position(), QTextCursor::KeepAnchor);
84         textEdit->skipSelectionHighlighting();
85         textEdit->setTextCursor (start);
86     }
87     /* matches highlights should come here, after the text area is
88        scrolled and even when no match is found (it may be added later) */
89     hlight();
90     connect (textEdit, &QPlainTextEdit::textChanged, this, &FPwin::hlight);
91     connect (textEdit, &TextEdit::updateRect, this, &FPwin::hlight);
92     connect (textEdit, &TextEdit::resized, this, &FPwin::hlight);
93 }
94 /*************************/
95 // Highlight found matches in the visible part of the text.
hlight() const96 void FPwin::hlight() const
97 {
98     /* When FeatherPad's window is being closed, it's possible that, in a moment,
99        the current index is positive but the current widget is null. So, the latter
100        should be checked, not the former. */
101     TabPage *tabPage = qobject_cast< TabPage *>(ui->tabWidget->currentWidget());
102     if (tabPage == nullptr) return;
103 
104     TextEdit *textEdit = tabPage->textEdit();
105 
106     const QString txt = textEdit->getSearchedText();
107     if (txt.isEmpty()) return;
108 
109     QTextDocument::FindFlags searchFlags = getSearchFlags();
110 
111     /* prepend green highlights */
112     QList<QTextEdit::ExtraSelection> es = textEdit->getGreenSel();
113     QColor color = QColor (textEdit->hasDarkScheme() ? QColor (255, 255, 0,
114                                                                /* a quadratic equation for darkValue -> opacity: 0 -> 90,  27 -> 75, 50 -> 65 */
115                                                                static_cast<int>(static_cast<qreal>(textEdit->getDarkValue() * (textEdit->getDarkValue() - 257)) / static_cast<qreal>(414)) + 90)
116                                                      : Qt::yellow);
117     QTextCursor found;
118     /* first put a start cursor at the top left edge... */
119     QPoint Point (0, 0);
120     QTextCursor start = textEdit->cursorForPosition (Point);
121     /* ... then move it backward by the search text length */
122     int startPos = start.position() - (!tabPage->matchRegex() ? txt.length() : 0);
123     if (startPos >= 0)
124         start.setPosition (startPos);
125     else
126         start.setPosition (0);
127     /* get the visible text to check if the search string is inside it */
128     Point = QPoint (textEdit->geometry().width(), textEdit->geometry().height());
129     QTextCursor end = textEdit->cursorForPosition (Point);
130     int endLimit = end.anchor();
131     int endPos = end.position() + (!tabPage->matchRegex() ? txt.length() : 0);
132     end.movePosition (QTextCursor::End);
133     if (endPos <= end.position())
134         end.setPosition (endPos);
135     QTextCursor visCur = start;
136     visCur.setPosition (end.position(), QTextCursor::KeepAnchor);
137     const QString str = visCur.selection().toPlainText(); // '\n' is included in this way
138     Qt::CaseSensitivity cs = tabPage->matchCase() ? Qt::CaseSensitive : Qt::CaseInsensitive;
139     if (tabPage->matchRegex() || str.contains (txt, cs)) // don't waste time if the searched text isn't visible
140     {
141         while (!(found = textEdit->finding (txt, start, searchFlags,  tabPage->matchRegex(), endLimit)).isNull())
142         {
143             QTextEdit::ExtraSelection extra;
144             extra.format.setBackground (color);
145             extra.cursor = found;
146             es.append (extra);
147             start.setPosition (found.position());
148         }
149     }
150 
151     /* also prepend the current line highlight,
152        so that it always comes first when it exists */
153     if (ui->actionLineNumbers->isChecked() || ui->spinBox->isVisible())
154         es.prepend (textEdit->currentLineSelection());
155     /* append blue and red highlights */
156     es.append (textEdit->getBlueSel());
157     es.append (textEdit->getRedSel());
158     textEdit->setExtraSelections (es);
159 }
160 /*************************/
searchFlagChanged()161 void FPwin::searchFlagChanged()
162 {
163     if (!isReady()) return;
164 
165     TabPage *tabPage = qobject_cast< TabPage *>(ui->tabWidget->currentWidget());
166     if (tabPage == nullptr) return;
167 
168     /* deselect text for consistency */
169     TextEdit *textEdit = tabPage->textEdit();
170     QTextCursor start = textEdit->textCursor();
171     if (start.hasSelection())
172     {
173         start.setPosition (start.anchor());
174         textEdit->setTextCursor (start);
175     }
176 
177     hlight();
178 }
179 /*************************/
getSearchFlags() const180 QTextDocument::FindFlags FPwin::getSearchFlags() const
181 {
182     TabPage *tabPage = qobject_cast< TabPage *>(ui->tabWidget->currentWidget());
183     QTextDocument::FindFlags searchFlags = QTextDocument::FindFlags();
184     if (tabPage != nullptr)
185     {
186         if (tabPage->matchWhole())
187             searchFlags = QTextDocument::FindWholeWords;
188         if (tabPage->matchCase())
189             searchFlags |= QTextDocument::FindCaseSensitively;
190     }
191     return searchFlags;
192 }
193 
194 }
195