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