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