]> git.lyx.org Git - lyx.git/blob - src/frontends/qt/GuiSearch.cpp
169c7aa8b63d26073d23acc619c4f7fcf328d764
[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         if (!search.empty()) {
421                 LYXERR(Debug::CLIPBOARD, "from findbuffer: " << search);
422                 findCO->lineEdit()->selectAll();
423                 findCO->lineEdit()->insert(toqstr(search));
424                 findCO->lineEdit()->selectAll();
425         }
426 }
427
428
429 void GuiSearchWidget::findChanged()
430 {
431         bool const emptytext = findCO->currentText().isEmpty();
432         findPB->setEnabled(!emptytext);
433         findPrevPB->setEnabled(!emptytext);
434         bool const replace = !emptytext && bv_ && !bv_->buffer().isReadonly();
435         replacePB->setEnabled(replace);
436         replacePrevPB->setEnabled(replace);
437         replaceallPB->setEnabled(replace);
438         if (instantSearchCB->isChecked())
439                 doFind(false, true);
440 }
441
442
443 void GuiSearchWidget::findClicked()
444 {
445         doFind();
446 }
447
448
449 void GuiSearchWidget::findPrevClicked()
450 {
451         doFind(true);
452 }
453
454
455 void GuiSearchWidget::replaceClicked()
456 {
457         doReplace();
458 }
459
460
461 void GuiSearchWidget::replacePrevClicked()
462 {
463         doReplace(true);
464 }
465
466
467 void GuiSearchWidget::replaceallClicked()
468 {
469         replace(qstring_to_ucs4(findCO->currentText()),
470                 qstring_to_ucs4(replaceCO->currentText()),
471                 caseCB->isChecked(), wordsCB->isChecked(),
472                 true, true, true, selectionCB->isChecked());
473         uniqueInsert(findCO, findCO->currentText());
474         uniqueInsert(replaceCO, replaceCO->currentText());
475 }
476
477
478 void GuiSearchWidget::doFind(bool const backwards, bool const instant)
479 {
480         docstring const needle = qstring_to_ucs4(findCO->currentText());
481         find(needle, caseCB->isChecked(), wordsCB->isChecked(), !backwards,
482              instant, wrapCB->isChecked(), selectionCB->isChecked());
483         uniqueInsert(findCO, findCO->currentText());
484         if (!instant)
485                 findCO->lineEdit()->selectAll();
486 }
487
488
489 void GuiSearchWidget::find(docstring const & search, bool casesensitive,
490                            bool matchword, bool forward, bool instant,
491                            bool wrap, bool onlysel)
492 {
493         docstring const sdata =
494                 find2string(search, casesensitive, matchword,
495                             forward, wrap, instant, onlysel);
496
497         dispatch(FuncRequest(LFUN_WORD_FIND, sdata));
498 }
499
500
501 void GuiSearchWidget::doReplace(bool const backwards)
502 {
503         docstring const needle = qstring_to_ucs4(findCO->currentText());
504         docstring const repl = qstring_to_ucs4(replaceCO->currentText());
505         replace(needle, repl, caseCB->isChecked(), wordsCB->isChecked(),
506                 !backwards, false, wrapCB->isChecked(), selectionCB->isChecked());
507         uniqueInsert(findCO, findCO->currentText());
508         uniqueInsert(replaceCO, replaceCO->currentText());
509 }
510
511
512 void GuiSearchWidget::replace(docstring const & search, docstring const & replace,
513                             bool casesensitive, bool matchword,
514                             bool forward, bool all, bool wrap, bool onlysel)
515 {
516         docstring const sdata =
517                 replace2string(replace, search, casesensitive,
518                                matchword, all, forward, true, wrap, onlysel);
519
520         dispatch(FuncRequest(LFUN_WORD_REPLACE, sdata));
521 }
522
523 void GuiSearchWidget::saveSession(QSettings & settings, QString const & session_key) const
524 {
525         settings.setValue(session_key + "/casesensitive", caseCB->isChecked());
526         settings.setValue(session_key + "/words", wordsCB->isChecked());
527         settings.setValue(session_key + "/instant", instantSearchCB->isChecked());
528         settings.setValue(session_key + "/wrap", wrapCB->isChecked());
529         settings.setValue(session_key + "/selection", selectionCB->isChecked());
530         settings.setValue(session_key + "/minimized", minimized_);
531 }
532
533
534 void GuiSearchWidget::restoreSession(QString const & session_key)
535 {
536         QSettings settings;
537         caseCB->setChecked(settings.value(session_key + "/casesensitive", false).toBool());
538         act_casesense_->setChecked(settings.value(session_key + "/casesensitive", false).toBool());
539         wordsCB->setChecked(settings.value(session_key + "/words", false).toBool());
540         act_wholewords_->setChecked(settings.value(session_key + "/words", false).toBool());
541         instantSearchCB->setChecked(settings.value(session_key + "/instant", false).toBool());
542         act_immediate_->setChecked(settings.value(session_key + "/instant", false).toBool());
543         wrapCB->setChecked(settings.value(session_key + "/wrap", false).toBool());
544         act_wrap_->setChecked(settings.value(session_key + "/wrap", false).toBool());
545         selectionCB->setChecked(settings.value(session_key + "/selection", false).toBool());
546         act_selection_->setChecked(settings.value(session_key + "/selection", false).toBool());
547         minimized_ = settings.value(session_key + "/minimized", false).toBool();
548         // initialize hidings
549         minimizeClicked(false);
550 }
551
552
553 GuiSearch::GuiSearch(GuiView & parent, Qt::DockWidgetArea area, Qt::WindowFlags flags)
554         : DockView(parent, "findreplace", qt_("Search and Replace"), area, flags),
555           widget_(new GuiSearchWidget(this))
556 {
557         setWidget(widget_);
558         widget_->setBufferView(bufferview());
559         setFocusProxy(widget_);
560
561         connect(widget_, SIGNAL(needTitleBarUpdate()), this, SLOT(updateTitle()));
562         connect(widget_, SIGNAL(needSizeUpdate()), this, SLOT(updateSize()));
563 }
564
565 void GuiSearch::onBufferViewChanged()
566 {
567         widget_->setEnabled(static_cast<bool>(bufferview()));
568         widget_->setBufferView(bufferview());
569 }
570
571
572 void GuiSearch::updateView()
573 {
574         updateTitle();
575         updateSize();
576 }
577
578
579 void GuiSearch::saveSession(QSettings & settings) const
580 {
581         Dialog::saveSession(settings);
582         widget_->saveSession(settings, sessionKey());
583 }
584
585
586 void GuiSearch::restoreSession()
587 {
588         DockView::restoreSession();
589         widget_->restoreSession(sessionKey());
590 }
591
592
593 void GuiSearch::updateTitle()
594 {
595         if (widget_->isMinimized()) {
596                 // remove title bar
597                 setTitleBarWidget(new QWidget());
598                 titleBarWidget()->hide();
599         } else
600                 // restore title bar
601                 setTitleBarWidget(nullptr);
602 }
603
604
605 void GuiSearch::updateSize()
606 {
607         widget_->setFixedHeight(widget_->sizeHint().height());
608         if (widget_->isMinimized())
609                 setFixedHeight(widget_->sizeHint().height());
610         else {
611                 // undo setFixedHeight
612                 setMaximumHeight(QWIDGETSIZE_MAX);
613                 setMinimumHeight(0);
614         }
615         update();
616 }
617
618
619 } // namespace frontend
620 } // namespace lyx
621
622
623 #include "moc_GuiSearch.cpp"