]> git.lyx.org Git - lyx.git/blob - src/frontends/qt/GuiSearch.cpp
GuiSearch: prevent Tab key to switch to work area (part of #12170)
[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         // Make this a sub window to prevent focusNextPrevChild (Tab)
131         // switching to the parent (#12170)
132         setWindowFlags(Qt::SubWindow);
133 }
134
135
136 bool GuiSearchWidget::initialiseParams(std::string const & str)
137 {
138         if (!str.empty())
139                 findCO->lineEdit()->setText(toqstr(str));
140         return true;
141 }
142
143
144 void GuiSearchWidget::keyPressEvent(QKeyEvent * ev)
145 {
146         KeySymbol sym;
147         setKeySymbol(&sym, ev);
148
149         // catch Return and Shift-Return
150         if (ev->key() == Qt::Key_Return || ev->key() == Qt::Key_Enter) {
151                 doFind(ev->modifiers() == Qt::ShiftModifier);
152                 return;
153         }
154         if (ev->key() == Qt::Key_Escape) {
155                 dispatch(FuncRequest(LFUN_DIALOG_HIDE, "findreplace"));
156                 return;
157         }
158
159         // we catch the key sequences for forward and backwards search
160         if (sym.isOK()) {
161                 KeyModifier mod = lyx::q_key_state(ev->modifiers());
162                 KeySequence keyseq(&theTopLevelKeymap(), &theTopLevelKeymap());
163                 FuncRequest fr = keyseq.addkey(sym, mod);
164                 if (fr == FuncRequest(LFUN_WORD_FIND_FORWARD)
165                     || fr == FuncRequest(LFUN_WORD_FIND)) {
166                         doFind();
167                         return;
168                 }
169                 if (fr == FuncRequest(LFUN_WORD_FIND_BACKWARD)) {
170                         doFind(true);
171                         return;
172                 }
173                 if (fr == FuncRequest(LFUN_DIALOG_TOGGLE, "findreplace")) {
174                         dispatch(fr);
175                         return;
176                 }
177         }
178         QWidget::keyPressEvent(ev);
179 }
180
181
182 void GuiSearchWidget::minimizeClicked(bool const toggle)
183 {
184         if (toggle)
185                 minimized_ = !minimized_;
186
187         replaceLA->setHidden(minimized_);
188         replaceCO->setHidden(minimized_);
189         replacePB->setHidden(minimized_);
190         replacePrevPB->setHidden(minimized_);
191         replaceallPB->setHidden(minimized_);
192         CBFrame->setHidden(minimized_);
193
194         if (minimized_) {
195                 minimizePB->setText(qt_("Ex&pand"));
196                 minimizePB->setToolTip(qt_("Show replace and option widgets"));
197                 // update menu items
198                 blockSignals(true);
199                 act_casesense_->setChecked(caseCB->isChecked());
200                 act_immediate_->setChecked(instantSearchCB->isChecked());
201                 act_selection_->setChecked(selectionCB->isChecked());
202                 act_wholewords_->setChecked(wordsCB->isChecked());
203                 act_wrap_->setChecked(wrapCB->isChecked());
204                 blockSignals(false);
205         } else {
206                 minimizePB->setText(qt_("&Minimize"));
207                 minimizePB->setToolTip(qt_("Hide replace and option widgets"));
208         }
209
210         Q_EMIT needSizeUpdate();
211         Q_EMIT needTitleBarUpdate();
212         handleIndicators();
213 }
214
215
216 void GuiSearchWidget::handleIndicators()
217 {
218         findLE_->setButtonVisible(FancyLineEdit::Right, minimized_);
219
220         QString tip;
221
222         if (minimized_) {
223                 int pms = 0;
224                 if (caseCB->isChecked())
225                         ++pms;
226                 if (wordsCB->isChecked())
227                         ++pms;
228                 if (selectionCB->isChecked())
229                         ++pms;
230                 if (instantSearchCB->isChecked())
231                         ++pms;
232                 if (wrapCB->isChecked())
233                         ++pms;
234
235                 bool const dark_mode = guiApp && guiApp->isInDarkMode();
236                 qreal dpr = 1.0;
237 #if QT_VERSION >= 0x050000
238                 // Consider device/pixel ratio (HiDPI)
239                 if (guiApp && guiApp->currentView())
240                         dpr = guiApp->currentView()->devicePixelRatio();
241 #endif
242                 QString imagedir = "images/";
243                 QPixmap bpixmap = getPixmap("images/", "search-options", "svgz,png");
244                 QPixmap pm = bpixmap;
245
246                 if (pms > 0) {
247                         int const gap = 3;
248                         QPixmap scaled_pm = QPixmap(bpixmap.size() * dpr);
249                         pm = QPixmap(pms * scaled_pm.width() + ((pms - 1) * gap),
250                                      scaled_pm.height());
251                         pm.fill(Qt::transparent);
252                         QPainter painter(&pm);
253                         int x = 0;
254                         
255                         tip = qt_("Active options:");
256                         tip += "<ul>";
257                         if (caseCB->isChecked()) {
258                                 tip += "<li>" + qt_("Case sensitive search");
259                                 QPixmap spixmap = getPixmap("images/", "search-case-sensitive", "svgz,png");
260 #if QT_VERSION < 0x050000
261                                 painter.drawPixmap(x, 0, spixmap);
262 #else
263                                 // With Qt5, we render SVG directly for HiDPI scalability
264                                 FileName fname = imageLibFileSearch(imagedir, "search-case-sensitive", "svgz,png");
265                                 QString fpath = toqstr(fname.absFileName());
266                                 if (!fpath.isEmpty()) {
267                                         QSvgRenderer svgRenderer(fpath);
268                                         if (svgRenderer.isValid())
269                                                 svgRenderer.render(&painter, QRectF(0, 0, spixmap.width() * dpr,
270                                                                                     spixmap.height() * dpr));
271                                 }
272 #endif
273                                 x += (spixmap.width() * dpr) + gap;
274                         }
275                         if (wordsCB->isChecked()) {
276                                 tip += "<li>" + qt_("Whole words only");
277                                 QPixmap spixmap = getPixmap("images/", "search-whole-words", "svgz,png");
278 #if QT_VERSION < 0x050000
279                                 painter.drawPixmap(x, 0, spixmap);
280 #else
281                                 FileName fname = imageLibFileSearch(imagedir, "search-whole-words", "svgz,png");
282                                 QString fpath = toqstr(fname.absFileName());
283                                 if (!fpath.isEmpty()) {
284                                         QSvgRenderer svgRenderer(fpath);
285                                         if (svgRenderer.isValid())
286                                                 svgRenderer.render(&painter, QRectF(x, 0, spixmap.width() * dpr,
287                                                                                     spixmap.height() * dpr));
288                                 }
289 #endif
290                                 x += (spixmap.width() * dpr) + gap;
291                         }
292                         if (selectionCB->isChecked()) {
293                                 tip += "<li>" + qt_("Search only in selection");
294                                 QPixmap spixmap = getPixmap("images/", "search-selection", "svgz,png");
295 #if QT_VERSION < 0x050000
296                                 painter.drawPixmap(x, 0, spixmap);
297 #else
298                                 FileName fname = imageLibFileSearch(imagedir, "search-selection", "svgz,png");
299                                 QString fpath = toqstr(fname.absFileName());
300                                 if (!fpath.isEmpty()) {
301                                         QSvgRenderer svgRenderer(fpath);
302                                         if (svgRenderer.isValid())
303                                                 svgRenderer.render(&painter, QRectF(x, 0, spixmap.width() * dpr,
304                                                                                     spixmap.height() * dpr));
305                                 }
306 #endif
307                                 x += (spixmap.width() * dpr) + gap;
308                         }
309                         if (instantSearchCB->isChecked()) {
310                                 tip += "<li>" + qt_("Search as you type");
311                                 QPixmap spixmap = getPixmap("images/", "search-instant", "svgz,png");
312 #if QT_VERSION < 0x050000
313                                 painter.drawPixmap(x, 0, spixmap);
314 #else
315                                 FileName fname = imageLibFileSearch(imagedir, "search-instant", "svgz,png");
316                                 QString fpath = toqstr(fname.absFileName());
317                                 if (!fpath.isEmpty()) {
318                                         QSvgRenderer svgRenderer(fpath);
319                                         if (svgRenderer.isValid())
320                                                 svgRenderer.render(&painter, QRectF(x, 0, spixmap.width() * dpr,
321                                                                                     spixmap.height() * dpr));
322                                 }
323 #endif
324                                 x += (spixmap.width() * dpr) + gap;
325                         }
326                         if (wrapCB->isChecked()) {
327                                 tip += "<li>" + qt_("Wrap search");
328                                 QPixmap spixmap = getPixmap("images/", "search-wrap", "svgz,png");
329 #if QT_VERSION < 0x050000
330                                 painter.drawPixmap(x, 0, spixmap);
331 #else
332                                 FileName fname = imageLibFileSearch(imagedir, "search-wrap", "svgz,png");
333                                 QString fpath = toqstr(fname.absFileName());
334                                 if (!fpath.isEmpty()) {
335                                         QSvgRenderer svgRenderer(fpath);
336                                         if (svgRenderer.isValid())
337                                                 svgRenderer.render(&painter, QRectF(x, 0, spixmap.width() * dpr,
338                                                                                     spixmap.height() * dpr));
339                                 }
340 #endif
341                                 x += (spixmap.width() * dpr) + gap;
342                         }
343                         tip += "</ul>";
344 #if QT_VERSION >= 0x050000
345                         pm.setDevicePixelRatio(dpr);
346 #endif
347                         painter.end();
348                 } else {
349                         tip = qt_("Click here to change search options");
350 #if QT_VERSION >= 0x050000
351                         // With Qt5, we render SVG directly for HiDPI scalability
352                         FileName fname = imageLibFileSearch(imagedir, "search-options", "svgz,png");
353                         QString fpath = toqstr(fname.absFileName());
354                         if (!fpath.isEmpty()) {
355                                 QSvgRenderer svgRenderer(fpath);
356                                 if (svgRenderer.isValid()) {
357                                         pm = QPixmap(bpixmap.size() * dpr);
358                                         pm.fill(Qt::transparent);
359                                         QPainter painter(&pm);
360                                         svgRenderer.render(&painter);
361                                         pm.setDevicePixelRatio(dpr);
362                                 }
363                         }
364 #endif
365                 }
366                 if (dark_mode) {
367                         QImage img = pm.toImage();
368                         img.invertPixels();
369                         pm.convertFromImage(img);
370                 }
371                 findLE_->setButtonPixmap(FancyLineEdit::Right, pm);
372         }
373         findLE_->setButtonToolTip(FancyLineEdit::Right, tip);
374 }
375
376
377 void GuiSearchWidget::caseSenseActTriggered()
378 {
379         caseCB->setChecked(act_casesense_->isChecked());
380         handleIndicators();
381 }
382
383
384 void GuiSearchWidget::wholeWordsActTriggered()
385 {
386         wordsCB->setChecked(act_wholewords_->isChecked());
387         handleIndicators();
388 }
389
390
391 void GuiSearchWidget::searchSelActTriggered()
392 {
393         selectionCB->setChecked(act_selection_->isChecked());
394         handleIndicators();
395 }
396
397
398 void GuiSearchWidget::immediateActTriggered()
399 {
400         instantSearchCB->setChecked(act_immediate_->isChecked());
401         handleIndicators();
402 }
403
404
405 void GuiSearchWidget::wrapActTriggered()
406 {
407         wrapCB->setChecked(act_wrap_->isChecked());
408         handleIndicators();
409 }
410
411
412 void GuiSearchWidget::showEvent(QShowEvent * e)
413 {
414         findChanged();
415         findPB->setFocus();
416         findCO->lineEdit()->selectAll();
417         QWidget::showEvent(e);
418 }
419
420
421 void GuiSearchWidget::findBufferChanged()
422 {
423         docstring search = theClipboard().getFindBuffer();
424         // update from find buffer, but only if the strings differ (else we
425         // might end up in loops with search as you type)
426         if (!search.empty() && toqstr(search) != findCO->lineEdit()->text()) {
427                 LYXERR(Debug::CLIPBOARD, "from findbuffer: " << search);
428                 findCO->lineEdit()->selectAll();
429                 findCO->lineEdit()->insert(toqstr(search));
430                 findCO->lineEdit()->selectAll();
431         }
432 }
433
434
435 void GuiSearchWidget::findChanged()
436 {
437         bool const emptytext = findCO->currentText().isEmpty();
438         findPB->setEnabled(!emptytext);
439         findPrevPB->setEnabled(!emptytext);
440         bool const replace = !emptytext && bv_ && !bv_->buffer().isReadonly();
441         replacePB->setEnabled(replace);
442         replacePrevPB->setEnabled(replace);
443         replaceallPB->setEnabled(replace);
444         if (instantSearchCB->isChecked())
445                 doFind(false, true);
446 }
447
448
449 void GuiSearchWidget::findClicked()
450 {
451         doFind();
452 }
453
454
455 void GuiSearchWidget::findPrevClicked()
456 {
457         doFind(true);
458 }
459
460
461 void GuiSearchWidget::replaceClicked()
462 {
463         doReplace();
464 }
465
466
467 void GuiSearchWidget::replacePrevClicked()
468 {
469         doReplace(true);
470 }
471
472
473 void GuiSearchWidget::replaceallClicked()
474 {
475         replace(qstring_to_ucs4(findCO->currentText()),
476                 qstring_to_ucs4(replaceCO->currentText()),
477                 caseCB->isChecked(), wordsCB->isChecked(),
478                 true, true, true, selectionCB->isChecked());
479         uniqueInsert(findCO, findCO->currentText());
480         uniqueInsert(replaceCO, replaceCO->currentText());
481 }
482
483
484 void GuiSearchWidget::doFind(bool const backwards, bool const instant)
485 {
486         docstring const needle = qstring_to_ucs4(findCO->currentText());
487         find(needle, caseCB->isChecked(), wordsCB->isChecked(), !backwards,
488              instant, wrapCB->isChecked(), selectionCB->isChecked());
489         uniqueInsert(findCO, findCO->currentText());
490         if (!instant)
491                 findCO->lineEdit()->selectAll();
492 }
493
494
495 void GuiSearchWidget::find(docstring const & search, bool casesensitive,
496                            bool matchword, bool forward, bool instant,
497                            bool wrap, bool onlysel)
498 {
499         docstring const sdata =
500                 find2string(search, casesensitive, matchword,
501                             forward, wrap, instant, onlysel);
502
503         dispatch(FuncRequest(LFUN_WORD_FIND, sdata));
504 }
505
506
507 void GuiSearchWidget::doReplace(bool const backwards)
508 {
509         docstring const needle = qstring_to_ucs4(findCO->currentText());
510         docstring const repl = qstring_to_ucs4(replaceCO->currentText());
511         replace(needle, repl, caseCB->isChecked(), wordsCB->isChecked(),
512                 !backwards, false, wrapCB->isChecked(), selectionCB->isChecked());
513         uniqueInsert(findCO, findCO->currentText());
514         uniqueInsert(replaceCO, replaceCO->currentText());
515 }
516
517
518 void GuiSearchWidget::replace(docstring const & search, docstring const & replace,
519                             bool casesensitive, bool matchword,
520                             bool forward, bool all, bool wrap, bool onlysel)
521 {
522         docstring const sdata =
523                 replace2string(replace, search, casesensitive,
524                                matchword, all, forward, true, wrap, onlysel);
525
526         dispatch(FuncRequest(LFUN_WORD_REPLACE, sdata));
527 }
528
529 void GuiSearchWidget::saveSession(QSettings & settings, QString const & session_key) const
530 {
531         settings.setValue(session_key + "/casesensitive", caseCB->isChecked());
532         settings.setValue(session_key + "/words", wordsCB->isChecked());
533         settings.setValue(session_key + "/instant", instantSearchCB->isChecked());
534         settings.setValue(session_key + "/wrap", wrapCB->isChecked());
535         settings.setValue(session_key + "/selection", selectionCB->isChecked());
536         settings.setValue(session_key + "/minimized", minimized_);
537 }
538
539
540 void GuiSearchWidget::restoreSession(QString const & session_key)
541 {
542         QSettings settings;
543         caseCB->setChecked(settings.value(session_key + "/casesensitive", false).toBool());
544         act_casesense_->setChecked(settings.value(session_key + "/casesensitive", false).toBool());
545         wordsCB->setChecked(settings.value(session_key + "/words", false).toBool());
546         act_wholewords_->setChecked(settings.value(session_key + "/words", false).toBool());
547         instantSearchCB->setChecked(settings.value(session_key + "/instant", false).toBool());
548         act_immediate_->setChecked(settings.value(session_key + "/instant", false).toBool());
549         wrapCB->setChecked(settings.value(session_key + "/wrap", false).toBool());
550         act_wrap_->setChecked(settings.value(session_key + "/wrap", false).toBool());
551         selectionCB->setChecked(settings.value(session_key + "/selection", false).toBool());
552         act_selection_->setChecked(settings.value(session_key + "/selection", false).toBool());
553         minimized_ = settings.value(session_key + "/minimized", false).toBool();
554         // initialize hidings
555         minimizeClicked(false);
556 }
557
558
559 GuiSearch::GuiSearch(GuiView & parent, Qt::DockWidgetArea area, Qt::WindowFlags flags)
560         : DockView(parent, "findreplace", qt_("Search and Replace"), area, flags),
561           widget_(new GuiSearchWidget(this))
562 {
563         setWidget(widget_);
564         widget_->setBufferView(bufferview());
565         setFocusProxy(widget_);
566
567         connect(widget_, SIGNAL(needTitleBarUpdate()), this, SLOT(updateTitle()));
568         connect(widget_, SIGNAL(needSizeUpdate()), this, SLOT(updateSize()));
569 }
570
571 void GuiSearch::onBufferViewChanged()
572 {
573         widget_->setEnabled(static_cast<bool>(bufferview()));
574         widget_->setBufferView(bufferview());
575 }
576
577
578 void GuiSearch::updateView()
579 {
580         updateTitle();
581         updateSize();
582 }
583
584
585 void GuiSearch::saveSession(QSettings & settings) const
586 {
587         Dialog::saveSession(settings);
588         widget_->saveSession(settings, sessionKey());
589 }
590
591
592 void GuiSearch::restoreSession()
593 {
594         DockView::restoreSession();
595         widget_->restoreSession(sessionKey());
596 }
597
598
599 void GuiSearch::updateTitle()
600 {
601         if (widget_->isMinimized()) {
602                 // remove title bar
603                 setTitleBarWidget(new QWidget());
604                 titleBarWidget()->hide();
605         } else
606                 // restore title bar
607                 setTitleBarWidget(nullptr);
608 }
609
610
611 void GuiSearch::updateSize()
612 {
613         widget_->setFixedHeight(widget_->sizeHint().height());
614         if (widget_->isMinimized())
615                 setFixedHeight(widget_->sizeHint().height());
616         else {
617                 // undo setFixedHeight
618                 setMaximumHeight(QWIDGETSIZE_MAX);
619                 setMinimumHeight(0);
620         }
621         update();
622 }
623
624
625 } // namespace frontend
626 } // namespace lyx
627
628
629 #include "moc_GuiSearch.cpp"