]> git.lyx.org Git - lyx.git/blob - src/frontends/qt/GuiSearch.cpp
Prevent infinite loop with instant search on Mac (#12161)
[lyx.git] / src / frontends / qt / GuiSearch.cpp
1 /**
2  * \file GuiSearch.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author John Levon
7  * \author Edwin Leuven
8  * \author Angus Leeming
9  *
10  * Full author contact details are available in file CREDITS.
11  */
12
13 #include <config.h>
14
15 #include "GuiApplication.h"
16 #include "GuiSearch.h"
17
18 #include "lyxfind.h"
19 #include "qt_helpers.h"
20 #include "FuncRequest.h"
21 #include "LyX.h"
22 #include "BufferView.h"
23 #include "Buffer.h"
24 #include "Cursor.h"
25 #include "FuncRequest.h"
26 #include "KeyMap.h"
27 #include "GuiKeySymbol.h"
28 #include "GuiView.h"
29
30 #include "qt_helpers.h"
31 #include "support/filetools.h"
32 #include "support/debug.h"
33 #include "support/gettext.h"
34 #include "support/FileName.h"
35 #include "frontends/alert.h"
36 #include "frontends/Clipboard.h"
37
38 #include <QClipboard>
39 #include <QPainter>
40 #include <QLineEdit>
41 #include <QSettings>
42 #include <QShowEvent>
43 #include "QSizePolicy"
44 #if QT_VERSION >= 0x050000
45 #include <QSvgRenderer>
46 #endif
47
48 using namespace std;
49 using namespace lyx::support;
50
51 using lyx::KeySymbol;
52
53 namespace lyx {
54 namespace frontend {
55
56 static void uniqueInsert(QComboBox * box, QString const & text)
57 {
58         for (int i = box->count(); --i >= 0; )
59                 if (box->itemText(i) == text)
60                         return;
61
62         box->insertItem(0, text);
63 }
64
65
66 GuiSearchWidget::GuiSearchWidget(QWidget * parent)
67         :       QWidget(parent)
68 {
69         setupUi(this);
70
71         // fix height to minimum
72         setFixedHeight(sizeHint().height());
73
74         // align items in grid on top
75         gridLayout->setAlignment(Qt::AlignTop);
76
77         connect(findPB, SIGNAL(clicked()), this, SLOT(findClicked()));
78         connect(findPrevPB, SIGNAL(clicked()), this, SLOT(findPrevClicked()));
79         connect(minimizePB, SIGNAL(clicked()), this, SLOT(minimizeClicked()));
80         connect(replacePB, SIGNAL(clicked()), this, SLOT(replaceClicked()));
81         connect(replacePrevPB, SIGNAL(clicked()), this, SLOT(replacePrevClicked()));
82         connect(replaceallPB, SIGNAL(clicked()), this, SLOT(replaceallClicked()));
83         connect(findCO, SIGNAL(editTextChanged(QString)),
84                 this, SLOT(findChanged()));
85         if(qApp->clipboard()->supportsFindBuffer()) {
86                 connect(qApp->clipboard(), SIGNAL(findBufferChanged()),
87                         this, SLOT(findBufferChanged()));
88                 findBufferChanged();
89         }
90
91         setFocusProxy(findCO);
92
93         // Use a FancyLineEdit due to the indicator icons
94         findLE_ = new FancyLineEdit(this);
95         findCO->setLineEdit(findLE_);
96
97         // And a menu in minimal mode
98         menu_ = new QMenu();
99         act_casesense_ = new QAction(qt_("&Case sensitive[[search]]"), this);
100         act_casesense_->setCheckable(true);
101         act_wholewords_ = new QAction(qt_("Wh&ole words"), this);
102         act_wholewords_->setCheckable(true);
103         act_selection_ = new QAction(qt_("Selection onl&y"), this);
104         act_selection_->setCheckable(true);
105         act_immediate_ = new QAction(qt_("Search as yo&u type"), this);
106         act_immediate_->setCheckable(true);
107         act_wrap_ = new QAction(qt_("&Wrap"), this);
108         act_wrap_->setCheckable(true);
109
110         menu_->addAction(act_casesense_);
111         menu_->addAction(act_wholewords_);
112         menu_->addAction(act_selection_);
113         menu_->addAction(act_immediate_);
114         menu_->addAction(act_wrap_);
115         findLE_->setButtonMenu(FancyLineEdit::Right, menu_);
116
117         connect(act_casesense_, SIGNAL(triggered()), this, SLOT(caseSenseActTriggered()));
118         connect(act_wholewords_, SIGNAL(triggered()), this, SLOT(wholeWordsActTriggered()));
119         connect(act_selection_, SIGNAL(triggered()), this, SLOT(searchSelActTriggered()));
120         connect(act_immediate_, SIGNAL(triggered()), this, SLOT(immediateActTriggered()));
121         connect(act_wrap_, SIGNAL(triggered()), this, SLOT(wrapActTriggered()));
122
123         findCO->setCompleter(nullptr);
124         replaceCO->setCompleter(nullptr);
125
126         replacePB->setEnabled(false);
127         replacePrevPB->setEnabled(false);
128         replaceallPB->setEnabled(false);
129 }
130
131
132 bool GuiSearchWidget::initialiseParams(std::string const & str)
133 {
134         if (!str.empty())
135                 findCO->lineEdit()->setText(toqstr(str));
136         return true;
137 }
138
139
140 void GuiSearchWidget::keyPressEvent(QKeyEvent * ev)
141 {
142         KeySymbol sym;
143         setKeySymbol(&sym, ev);
144
145         // catch Return and Shift-Return
146         if (ev->key() == Qt::Key_Return || ev->key() == Qt::Key_Enter) {
147                 doFind(ev->modifiers() == Qt::ShiftModifier);
148                 return;
149         }
150         if (ev->key() == Qt::Key_Escape) {
151                 dispatch(FuncRequest(LFUN_DIALOG_HIDE, "findreplace"));
152                 return;
153         }
154
155         // we catch the key sequences for forward and backwards search
156         if (sym.isOK()) {
157                 KeyModifier mod = lyx::q_key_state(ev->modifiers());
158                 KeySequence keyseq(&theTopLevelKeymap(), &theTopLevelKeymap());
159                 FuncRequest fr = keyseq.addkey(sym, mod);
160                 if (fr == FuncRequest(LFUN_WORD_FIND_FORWARD)
161                     || fr == FuncRequest(LFUN_WORD_FIND)) {
162                         doFind();
163                         return;
164                 }
165                 if (fr == FuncRequest(LFUN_WORD_FIND_BACKWARD)) {
166                         doFind(true);
167                         return;
168                 }
169                 if (fr == FuncRequest(LFUN_DIALOG_TOGGLE, "findreplace")) {
170                         dispatch(fr);
171                         return;
172                 }
173         }
174         QWidget::keyPressEvent(ev);
175 }
176
177
178 void GuiSearchWidget::minimizeClicked(bool const toggle)
179 {
180         if (toggle)
181                 minimized_ = !minimized_;
182
183         replaceLA->setHidden(minimized_);
184         replaceCO->setHidden(minimized_);
185         replacePB->setHidden(minimized_);
186         replacePrevPB->setHidden(minimized_);
187         replaceallPB->setHidden(minimized_);
188         CBFrame->setHidden(minimized_);
189
190         if (minimized_) {
191                 minimizePB->setText(qt_("Ex&pand"));
192                 minimizePB->setToolTip(qt_("Show replace and option widgets"));
193                 // update menu items
194                 blockSignals(true);
195                 act_casesense_->setChecked(caseCB->isChecked());
196                 act_immediate_->setChecked(instantSearchCB->isChecked());
197                 act_selection_->setChecked(selectionCB->isChecked());
198                 act_wholewords_->setChecked(wordsCB->isChecked());
199                 act_wrap_->setChecked(wrapCB->isChecked());
200                 blockSignals(false);
201         } else {
202                 minimizePB->setText(qt_("&Minimize"));
203                 minimizePB->setToolTip(qt_("Hide replace and option widgets"));
204         }
205
206         Q_EMIT needSizeUpdate();
207         Q_EMIT needTitleBarUpdate();
208         handleIndicators();
209 }
210
211
212 void GuiSearchWidget::handleIndicators()
213 {
214         findLE_->setButtonVisible(FancyLineEdit::Right, minimized_);
215
216         QString tip;
217
218         if (minimized_) {
219                 int pms = 0;
220                 if (caseCB->isChecked())
221                         ++pms;
222                 if (wordsCB->isChecked())
223                         ++pms;
224                 if (selectionCB->isChecked())
225                         ++pms;
226                 if (instantSearchCB->isChecked())
227                         ++pms;
228                 if (wrapCB->isChecked())
229                         ++pms;
230
231                 bool const dark_mode = guiApp && guiApp->isInDarkMode();
232                 qreal dpr = 1.0;
233 #if QT_VERSION >= 0x050000
234                 // Consider device/pixel ratio (HiDPI)
235                 if (guiApp && guiApp->currentView())
236                         dpr = guiApp->currentView()->devicePixelRatio();
237 #endif
238                 QString imagedir = "images/";
239                 QPixmap bpixmap = getPixmap("images/", "search-options", "svgz,png");
240                 QPixmap pm = bpixmap;
241
242                 if (pms > 0) {
243                         int const gap = 3;
244                         QPixmap scaled_pm = QPixmap(bpixmap.size() * dpr);
245                         pm = QPixmap(pms * scaled_pm.width() + ((pms - 1) * gap),
246                                      scaled_pm.height());
247                         pm.fill(Qt::transparent);
248                         QPainter painter(&pm);
249                         int x = 0;
250                         
251                         tip = qt_("Active options:");
252                         tip += "<ul>";
253                         if (caseCB->isChecked()) {
254                                 tip += "<li>" + qt_("Case sensitive search");
255                                 QPixmap spixmap = getPixmap("images/", "search-case-sensitive", "svgz,png");
256 #if QT_VERSION < 0x050000
257                                 painter.drawPixmap(x, 0, spixmap);
258 #else
259                                 // With Qt5, we render SVG directly for HiDPI scalability
260                                 FileName fname = imageLibFileSearch(imagedir, "search-case-sensitive", "svgz,png");
261                                 QString fpath = toqstr(fname.absFileName());
262                                 if (!fpath.isEmpty()) {
263                                         QSvgRenderer svgRenderer(fpath);
264                                         if (svgRenderer.isValid())
265                                                 svgRenderer.render(&painter, QRectF(0, 0, spixmap.width() * dpr,
266                                                                                     spixmap.height() * dpr));
267                                 }
268 #endif
269                                 x += (spixmap.width() * dpr) + gap;
270                         }
271                         if (wordsCB->isChecked()) {
272                                 tip += "<li>" + qt_("Whole words only");
273                                 QPixmap spixmap = getPixmap("images/", "search-whole-words", "svgz,png");
274 #if QT_VERSION < 0x050000
275                                 painter.drawPixmap(x, 0, spixmap);
276 #else
277                                 FileName fname = imageLibFileSearch(imagedir, "search-whole-words", "svgz,png");
278                                 QString fpath = toqstr(fname.absFileName());
279                                 if (!fpath.isEmpty()) {
280                                         QSvgRenderer svgRenderer(fpath);
281                                         if (svgRenderer.isValid())
282                                                 svgRenderer.render(&painter, QRectF(x, 0, spixmap.width() * dpr,
283                                                                                     spixmap.height() * dpr));
284                                 }
285 #endif
286                                 x += (spixmap.width() * dpr) + gap;
287                         }
288                         if (selectionCB->isChecked()) {
289                                 tip += "<li>" + qt_("Search only in selection");
290                                 QPixmap spixmap = getPixmap("images/", "search-selection", "svgz,png");
291 #if QT_VERSION < 0x050000
292                                 painter.drawPixmap(x, 0, spixmap);
293 #else
294                                 FileName fname = imageLibFileSearch(imagedir, "search-selection", "svgz,png");
295                                 QString fpath = toqstr(fname.absFileName());
296                                 if (!fpath.isEmpty()) {
297                                         QSvgRenderer svgRenderer(fpath);
298                                         if (svgRenderer.isValid())
299                                                 svgRenderer.render(&painter, QRectF(x, 0, spixmap.width() * dpr,
300                                                                                     spixmap.height() * dpr));
301                                 }
302 #endif
303                                 x += (spixmap.width() * dpr) + gap;
304                         }
305                         if (instantSearchCB->isChecked()) {
306                                 tip += "<li>" + qt_("Search as you type");
307                                 QPixmap spixmap = getPixmap("images/", "search-instant", "svgz,png");
308 #if QT_VERSION < 0x050000
309                                 painter.drawPixmap(x, 0, spixmap);
310 #else
311                                 FileName fname = imageLibFileSearch(imagedir, "search-instant", "svgz,png");
312                                 QString fpath = toqstr(fname.absFileName());
313                                 if (!fpath.isEmpty()) {
314                                         QSvgRenderer svgRenderer(fpath);
315                                         if (svgRenderer.isValid())
316                                                 svgRenderer.render(&painter, QRectF(x, 0, spixmap.width() * dpr,
317                                                                                     spixmap.height() * dpr));
318                                 }
319 #endif
320                                 x += (spixmap.width() * dpr) + gap;
321                         }
322                         if (wrapCB->isChecked()) {
323                                 tip += "<li>" + qt_("Wrap search");
324                                 QPixmap spixmap = getPixmap("images/", "search-wrap", "svgz,png");
325 #if QT_VERSION < 0x050000
326                                 painter.drawPixmap(x, 0, spixmap);
327 #else
328                                 FileName fname = imageLibFileSearch(imagedir, "search-wrap", "svgz,png");
329                                 QString fpath = toqstr(fname.absFileName());
330                                 if (!fpath.isEmpty()) {
331                                         QSvgRenderer svgRenderer(fpath);
332                                         if (svgRenderer.isValid())
333                                                 svgRenderer.render(&painter, QRectF(x, 0, spixmap.width() * dpr,
334                                                                                     spixmap.height() * dpr));
335                                 }
336 #endif
337                                 x += (spixmap.width() * dpr) + gap;
338                         }
339                         tip += "</ul>";
340 #if QT_VERSION >= 0x050000
341                         pm.setDevicePixelRatio(dpr);
342 #endif
343                         painter.end();
344                 } else {
345                         tip = qt_("Click here to change search options");
346 #if QT_VERSION >= 0x050000
347                         // With Qt5, we render SVG directly for HiDPI scalability
348                         FileName fname = imageLibFileSearch(imagedir, "search-options", "svgz,png");
349                         QString fpath = toqstr(fname.absFileName());
350                         if (!fpath.isEmpty()) {
351                                 QSvgRenderer svgRenderer(fpath);
352                                 if (svgRenderer.isValid()) {
353                                         pm = QPixmap(bpixmap.size() * dpr);
354                                         pm.fill(Qt::transparent);
355                                         QPainter painter(&pm);
356                                         svgRenderer.render(&painter);
357                                         pm.setDevicePixelRatio(dpr);
358                                 }
359                         }
360 #endif
361                 }
362                 if (dark_mode) {
363                         QImage img = pm.toImage();
364                         img.invertPixels();
365                         pm.convertFromImage(img);
366                 }
367                 findLE_->setButtonPixmap(FancyLineEdit::Right, pm);
368         }
369         findLE_->setButtonToolTip(FancyLineEdit::Right, tip);
370 }
371
372
373 void GuiSearchWidget::caseSenseActTriggered()
374 {
375         caseCB->setChecked(act_casesense_->isChecked());
376         handleIndicators();
377 }
378
379
380 void GuiSearchWidget::wholeWordsActTriggered()
381 {
382         wordsCB->setChecked(act_wholewords_->isChecked());
383         handleIndicators();
384 }
385
386
387 void GuiSearchWidget::searchSelActTriggered()
388 {
389         selectionCB->setChecked(act_selection_->isChecked());
390         handleIndicators();
391 }
392
393
394 void GuiSearchWidget::immediateActTriggered()
395 {
396         instantSearchCB->setChecked(act_immediate_->isChecked());
397         handleIndicators();
398 }
399
400
401 void GuiSearchWidget::wrapActTriggered()
402 {
403         wrapCB->setChecked(act_wrap_->isChecked());
404         handleIndicators();
405 }
406
407
408 void GuiSearchWidget::showEvent(QShowEvent * e)
409 {
410         findChanged();
411         findPB->setFocus();
412         findCO->lineEdit()->selectAll();
413         QWidget::showEvent(e);
414 }
415
416
417 void GuiSearchWidget::findBufferChanged()
418 {
419         docstring search = theClipboard().getFindBuffer();
420         // update from find buffer, but only if the strings differs (else we
421         // might end up in loops with search as you type)
422         if (!search.empty() && toqstr(search) != findCO->lineEdit()->text()) {
423                 LYXERR(Debug::CLIPBOARD, "from findbuffer: " << search);
424                 findCO->lineEdit()->selectAll();
425                 findCO->lineEdit()->insert(toqstr(search));
426                 findCO->lineEdit()->selectAll();
427         }
428 }
429
430
431 void GuiSearchWidget::findChanged()
432 {
433         bool const emptytext = findCO->currentText().isEmpty();
434         findPB->setEnabled(!emptytext);
435         findPrevPB->setEnabled(!emptytext);
436         bool const replace = !emptytext && bv_ && !bv_->buffer().isReadonly();
437         replacePB->setEnabled(replace);
438         replacePrevPB->setEnabled(replace);
439         replaceallPB->setEnabled(replace);
440         if (instantSearchCB->isChecked())
441                 doFind(false, true);
442 }
443
444
445 void GuiSearchWidget::findClicked()
446 {
447         doFind();
448 }
449
450
451 void GuiSearchWidget::findPrevClicked()
452 {
453         doFind(true);
454 }
455
456
457 void GuiSearchWidget::replaceClicked()
458 {
459         doReplace();
460 }
461
462
463 void GuiSearchWidget::replacePrevClicked()
464 {
465         doReplace(true);
466 }
467
468
469 void GuiSearchWidget::replaceallClicked()
470 {
471         replace(qstring_to_ucs4(findCO->currentText()),
472                 qstring_to_ucs4(replaceCO->currentText()),
473                 caseCB->isChecked(), wordsCB->isChecked(),
474                 true, true, true, selectionCB->isChecked());
475         uniqueInsert(findCO, findCO->currentText());
476         uniqueInsert(replaceCO, replaceCO->currentText());
477 }
478
479
480 void GuiSearchWidget::doFind(bool const backwards, bool const instant)
481 {
482         docstring const needle = qstring_to_ucs4(findCO->currentText());
483         find(needle, caseCB->isChecked(), wordsCB->isChecked(), !backwards,
484              instant, wrapCB->isChecked(), selectionCB->isChecked());
485         uniqueInsert(findCO, findCO->currentText());
486         if (!instant)
487                 findCO->lineEdit()->selectAll();
488 }
489
490
491 void GuiSearchWidget::find(docstring const & search, bool casesensitive,
492                            bool matchword, bool forward, bool instant,
493                            bool wrap, bool onlysel)
494 {
495         docstring const sdata =
496                 find2string(search, casesensitive, matchword,
497                             forward, wrap, instant, onlysel);
498
499         dispatch(FuncRequest(LFUN_WORD_FIND, sdata));
500 }
501
502
503 void GuiSearchWidget::doReplace(bool const backwards)
504 {
505         docstring const needle = qstring_to_ucs4(findCO->currentText());
506         docstring const repl = qstring_to_ucs4(replaceCO->currentText());
507         replace(needle, repl, caseCB->isChecked(), wordsCB->isChecked(),
508                 !backwards, false, wrapCB->isChecked(), selectionCB->isChecked());
509         uniqueInsert(findCO, findCO->currentText());
510         uniqueInsert(replaceCO, replaceCO->currentText());
511 }
512
513
514 void GuiSearchWidget::replace(docstring const & search, docstring const & replace,
515                             bool casesensitive, bool matchword,
516                             bool forward, bool all, bool wrap, bool onlysel)
517 {
518         docstring const sdata =
519                 replace2string(replace, search, casesensitive,
520                                matchword, all, forward, true, wrap, onlysel);
521
522         dispatch(FuncRequest(LFUN_WORD_REPLACE, sdata));
523 }
524
525 void GuiSearchWidget::saveSession(QSettings & settings, QString const & session_key) const
526 {
527         settings.setValue(session_key + "/casesensitive", caseCB->isChecked());
528         settings.setValue(session_key + "/words", wordsCB->isChecked());
529         settings.setValue(session_key + "/instant", instantSearchCB->isChecked());
530         settings.setValue(session_key + "/wrap", wrapCB->isChecked());
531         settings.setValue(session_key + "/selection", selectionCB->isChecked());
532         settings.setValue(session_key + "/minimized", minimized_);
533 }
534
535
536 void GuiSearchWidget::restoreSession(QString const & session_key)
537 {
538         QSettings settings;
539         caseCB->setChecked(settings.value(session_key + "/casesensitive", false).toBool());
540         act_casesense_->setChecked(settings.value(session_key + "/casesensitive", false).toBool());
541         wordsCB->setChecked(settings.value(session_key + "/words", false).toBool());
542         act_wholewords_->setChecked(settings.value(session_key + "/words", false).toBool());
543         instantSearchCB->setChecked(settings.value(session_key + "/instant", false).toBool());
544         act_immediate_->setChecked(settings.value(session_key + "/instant", false).toBool());
545         wrapCB->setChecked(settings.value(session_key + "/wrap", false).toBool());
546         act_wrap_->setChecked(settings.value(session_key + "/wrap", false).toBool());
547         selectionCB->setChecked(settings.value(session_key + "/selection", false).toBool());
548         act_selection_->setChecked(settings.value(session_key + "/selection", false).toBool());
549         minimized_ = settings.value(session_key + "/minimized", false).toBool();
550         // initialize hidings
551         minimizeClicked(false);
552 }
553
554
555 GuiSearch::GuiSearch(GuiView & parent, Qt::DockWidgetArea area, Qt::WindowFlags flags)
556         : DockView(parent, "findreplace", qt_("Search and Replace"), area, flags),
557           widget_(new GuiSearchWidget(this))
558 {
559         setWidget(widget_);
560         widget_->setBufferView(bufferview());
561         setFocusProxy(widget_);
562
563         connect(widget_, SIGNAL(needTitleBarUpdate()), this, SLOT(updateTitle()));
564         connect(widget_, SIGNAL(needSizeUpdate()), this, SLOT(updateSize()));
565 }
566
567 void GuiSearch::onBufferViewChanged()
568 {
569         widget_->setEnabled(static_cast<bool>(bufferview()));
570         widget_->setBufferView(bufferview());
571 }
572
573
574 void GuiSearch::updateView()
575 {
576         updateTitle();
577         updateSize();
578 }
579
580
581 void GuiSearch::saveSession(QSettings & settings) const
582 {
583         Dialog::saveSession(settings);
584         widget_->saveSession(settings, sessionKey());
585 }
586
587
588 void GuiSearch::restoreSession()
589 {
590         DockView::restoreSession();
591         widget_->restoreSession(sessionKey());
592 }
593
594
595 void GuiSearch::updateTitle()
596 {
597         if (widget_->isMinimized()) {
598                 // remove title bar
599                 setTitleBarWidget(new QWidget());
600                 titleBarWidget()->hide();
601         } else
602                 // restore title bar
603                 setTitleBarWidget(nullptr);
604 }
605
606
607 void GuiSearch::updateSize()
608 {
609         widget_->setFixedHeight(widget_->sizeHint().height());
610         if (widget_->isMinimized())
611                 setFixedHeight(widget_->sizeHint().height());
612         else {
613                 // undo setFixedHeight
614                 setMaximumHeight(QWIDGETSIZE_MAX);
615                 setMinimumHeight(0);
616         }
617         update();
618 }
619
620
621 } // namespace frontend
622 } // namespace lyx
623
624
625 #include "moc_GuiSearch.cpp"