3 * This file is part of LyX, the document processor.
4 * Licence details can be found in the file COPYING.
8 * \author Angus Leeming
10 * Full author contact details are available in file CREDITS.
15 #include "GuiApplication.h"
16 #include "GuiSearch.h"
19 #include "qt_helpers.h"
20 #include "FuncRequest.h"
22 #include "BufferView.h"
25 #include "FuncRequest.h"
27 #include "GuiKeySymbol.h"
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"
44 #include "QSizePolicy"
45 #if QT_VERSION >= 0x050000
46 #include <QSvgRenderer>
50 using namespace lyx::support;
57 static void uniqueInsert(QComboBox * box, QString const & text)
59 for (int i = box->count(); --i >= 0; )
60 if (box->itemText(i) == text)
63 box->insertItem(0, text);
67 GuiSearchWidget::GuiSearchWidget(QWidget * parent, GuiView & view)
68 : QWidget(parent), view_(view)
72 // fix height to minimum
73 setFixedHeight(sizeHint().height());
75 // align items in grid on top
76 gridLayout->setAlignment(Qt::AlignTop);
78 connect(findPB, SIGNAL(clicked()), this, SLOT(findClicked()));
79 connect(findPrevPB, SIGNAL(clicked()), this, SLOT(findPrevClicked()));
80 connect(minimizePB, SIGNAL(clicked()), this, SLOT(minimizeClicked()));
81 connect(replacePB, SIGNAL(clicked()), this, SLOT(replaceClicked()));
82 connect(replacePrevPB, SIGNAL(clicked()), this, SLOT(replacePrevClicked()));
83 connect(replaceallPB, SIGNAL(clicked()), this, SLOT(replaceallClicked()));
84 connect(findCO, SIGNAL(editTextChanged(QString)),
85 this, SLOT(findChanged()));
86 if(qApp->clipboard()->supportsFindBuffer()) {
87 connect(qApp->clipboard(), SIGNAL(findBufferChanged()),
88 this, SLOT(findBufferChanged()));
92 setFocusProxy(findCO);
94 // Use a FancyLineEdit due to the indicator icons
95 findLE_ = new FancyLineEdit(this);
96 findCO->setLineEdit(findLE_);
98 // And a menu in minimal mode
100 act_casesense_ = new QAction(qt_("&Case sensitive[[search]]"), this);
101 act_casesense_->setCheckable(true);
102 act_wholewords_ = new QAction(qt_("Wh&ole words"), this);
103 act_wholewords_->setCheckable(true);
104 act_selection_ = new QAction(qt_("Selection onl&y"), this);
105 act_selection_->setCheckable(true);
106 act_immediate_ = new QAction(qt_("Search as yo&u type"), this);
107 act_immediate_->setCheckable(true);
108 act_wrap_ = new QAction(qt_("&Wrap"), this);
109 act_wrap_->setCheckable(true);
111 menu_->addAction(act_casesense_);
112 menu_->addAction(act_wholewords_);
113 menu_->addAction(act_selection_);
114 menu_->addAction(act_immediate_);
115 menu_->addAction(act_wrap_);
116 findLE_->setButtonMenu(FancyLineEdit::Right, menu_);
118 connect(act_casesense_, SIGNAL(triggered()), this, SLOT(caseSenseActTriggered()));
119 connect(act_wholewords_, SIGNAL(triggered()), this, SLOT(wholeWordsActTriggered()));
120 connect(act_selection_, SIGNAL(triggered()), this, SLOT(searchSelActTriggered()));
121 connect(act_immediate_, SIGNAL(triggered()), this, SLOT(immediateActTriggered()));
122 connect(act_wrap_, SIGNAL(triggered()), this, SLOT(wrapActTriggered()));
124 findCO->setCompleter(nullptr);
125 replaceCO->setCompleter(nullptr);
127 replacePB->setEnabled(false);
128 replacePrevPB->setEnabled(false);
129 replaceallPB->setEnabled(false);
133 bool GuiSearchWidget::initialiseParams(std::string const & str)
136 findCO->lineEdit()->setText(toqstr(str));
141 void GuiSearchWidget::keyPressEvent(QKeyEvent * ev)
144 setKeySymbol(&sym, ev);
146 // catch Return and Shift-Return
147 if (ev->key() == Qt::Key_Return || ev->key() == Qt::Key_Enter) {
148 doFind(ev->modifiers() == Qt::ShiftModifier);
151 if (ev->key() == Qt::Key_Escape) {
152 dispatch(FuncRequest(LFUN_DIALOG_HIDE, "findreplace"));
154 bv_->buffer().updateBuffer();
158 // we catch the key sequences for forward and backwards search
160 KeyModifier mod = lyx::q_key_state(ev->modifiers());
161 KeySequence keyseq(&theTopLevelKeymap(), &theTopLevelKeymap());
162 FuncRequest fr = keyseq.addkey(sym, mod);
163 if (fr == FuncRequest(LFUN_WORD_FIND_FORWARD)
164 || fr == FuncRequest(LFUN_WORD_FIND)) {
168 if (fr == FuncRequest(LFUN_WORD_FIND_BACKWARD)) {
172 if (fr == FuncRequest(LFUN_DIALOG_TOGGLE, "findreplace")) {
177 QWidget::keyPressEvent(ev);
181 void GuiSearchWidget::minimizeClicked(bool const toggle)
184 minimized_ = !minimized_;
186 replaceLA->setHidden(minimized_);
187 replaceCO->setHidden(minimized_);
188 replacePB->setHidden(minimized_);
189 replacePrevPB->setHidden(minimized_);
190 replaceallPB->setHidden(minimized_);
191 CBFrame->setHidden(minimized_);
194 minimizePB->setText(qt_("Ex&pand"));
195 minimizePB->setToolTip(qt_("Show replace and option widgets"));
198 act_casesense_->setChecked(caseCB->isChecked());
199 act_immediate_->setChecked(instantSearchCB->isChecked());
200 act_selection_->setChecked(selectionCB->isChecked());
201 act_wholewords_->setChecked(wordsCB->isChecked());
202 act_wrap_->setChecked(wrapCB->isChecked());
205 minimizePB->setText(qt_("&Minimize"));
206 minimizePB->setToolTip(qt_("Hide replace and option widgets"));
209 Q_EMIT needSizeUpdate();
210 Q_EMIT needTitleBarUpdate();
215 void GuiSearchWidget::handleIndicators()
217 findLE_->setButtonVisible(FancyLineEdit::Right, minimized_);
223 if (caseCB->isChecked())
225 if (wordsCB->isChecked())
227 if (selectionCB->isChecked())
229 if (instantSearchCB->isChecked())
231 if (wrapCB->isChecked())
234 bool const dark_mode = guiApp && guiApp->isInDarkMode();
236 #if QT_VERSION >= 0x050000
237 // Consider device/pixel ratio (HiDPI)
238 if (guiApp && guiApp->currentView())
239 dpr = guiApp->currentView()->devicePixelRatio();
241 QString imagedir = "images/";
242 QPixmap bpixmap = getPixmap("images/", "search-options", "svgz,png");
243 QPixmap pm = bpixmap;
247 QPixmap scaled_pm = QPixmap(bpixmap.size() * dpr);
248 pm = QPixmap(pms * scaled_pm.width() + ((pms - 1) * gap),
250 pm.fill(Qt::transparent);
251 QPainter painter(&pm);
254 tip = qt_("Active options:");
256 if (caseCB->isChecked()) {
257 tip += "<li>" + qt_("Case sensitive search");
258 QPixmap spixmap = getPixmap("images/", "search-case-sensitive", "svgz,png");
259 #if QT_VERSION < 0x050000
260 painter.drawPixmap(x, 0, spixmap);
262 // With Qt5, we render SVG directly for HiDPI scalability
263 FileName fname = imageLibFileSearch(imagedir, "search-case-sensitive", "svgz,png");
264 QString fpath = toqstr(fname.absFileName());
265 if (!fpath.isEmpty()) {
266 QSvgRenderer svgRenderer(fpath);
267 if (svgRenderer.isValid())
268 svgRenderer.render(&painter, QRectF(0, 0, spixmap.width() * dpr,
269 spixmap.height() * dpr));
272 x += (spixmap.width() * dpr) + gap;
274 if (wordsCB->isChecked()) {
275 tip += "<li>" + qt_("Whole words only");
276 QPixmap spixmap = getPixmap("images/", "search-whole-words", "svgz,png");
277 #if QT_VERSION < 0x050000
278 painter.drawPixmap(x, 0, spixmap);
280 FileName fname = imageLibFileSearch(imagedir, "search-whole-words", "svgz,png");
281 QString fpath = toqstr(fname.absFileName());
282 if (!fpath.isEmpty()) {
283 QSvgRenderer svgRenderer(fpath);
284 if (svgRenderer.isValid())
285 svgRenderer.render(&painter, QRectF(x, 0, spixmap.width() * dpr,
286 spixmap.height() * dpr));
289 x += (spixmap.width() * dpr) + gap;
291 if (selectionCB->isChecked()) {
292 tip += "<li>" + qt_("Search only in selection");
293 QPixmap spixmap = getPixmap("images/", "search-selection", "svgz,png");
294 #if QT_VERSION < 0x050000
295 painter.drawPixmap(x, 0, spixmap);
297 FileName fname = imageLibFileSearch(imagedir, "search-selection", "svgz,png");
298 QString fpath = toqstr(fname.absFileName());
299 if (!fpath.isEmpty()) {
300 QSvgRenderer svgRenderer(fpath);
301 if (svgRenderer.isValid())
302 svgRenderer.render(&painter, QRectF(x, 0, spixmap.width() * dpr,
303 spixmap.height() * dpr));
306 x += (spixmap.width() * dpr) + gap;
308 if (instantSearchCB->isChecked()) {
309 tip += "<li>" + qt_("Search as you type");
310 QPixmap spixmap = getPixmap("images/", "search-instant", "svgz,png");
311 #if QT_VERSION < 0x050000
312 painter.drawPixmap(x, 0, spixmap);
314 FileName fname = imageLibFileSearch(imagedir, "search-instant", "svgz,png");
315 QString fpath = toqstr(fname.absFileName());
316 if (!fpath.isEmpty()) {
317 QSvgRenderer svgRenderer(fpath);
318 if (svgRenderer.isValid())
319 svgRenderer.render(&painter, QRectF(x, 0, spixmap.width() * dpr,
320 spixmap.height() * dpr));
323 x += (spixmap.width() * dpr) + gap;
325 if (wrapCB->isChecked()) {
326 tip += "<li>" + qt_("Wrap search");
327 QPixmap spixmap = getPixmap("images/", "search-wrap", "svgz,png");
328 #if QT_VERSION < 0x050000
329 painter.drawPixmap(x, 0, spixmap);
331 FileName fname = imageLibFileSearch(imagedir, "search-wrap", "svgz,png");
332 QString fpath = toqstr(fname.absFileName());
333 if (!fpath.isEmpty()) {
334 QSvgRenderer svgRenderer(fpath);
335 if (svgRenderer.isValid())
336 svgRenderer.render(&painter, QRectF(x, 0, spixmap.width() * dpr,
337 spixmap.height() * dpr));
340 x += (spixmap.width() * dpr) + gap;
343 #if QT_VERSION >= 0x050000
344 pm.setDevicePixelRatio(dpr);
348 tip = qt_("Click here to change search options");
349 #if QT_VERSION >= 0x050000
350 // With Qt5, we render SVG directly for HiDPI scalability
351 FileName fname = imageLibFileSearch(imagedir, "search-options", "svgz,png");
352 QString fpath = toqstr(fname.absFileName());
353 if (!fpath.isEmpty()) {
354 QSvgRenderer svgRenderer(fpath);
355 if (svgRenderer.isValid()) {
356 pm = QPixmap(bpixmap.size() * dpr);
357 pm.fill(Qt::transparent);
358 QPainter painter(&pm);
359 svgRenderer.render(&painter);
360 pm.setDevicePixelRatio(dpr);
366 QImage img = pm.toImage();
368 pm.convertFromImage(img);
370 findLE_->setButtonPixmap(FancyLineEdit::Right, pm);
372 findLE_->setButtonToolTip(FancyLineEdit::Right, tip);
376 void GuiSearchWidget::caseSenseActTriggered()
378 caseCB->setChecked(act_casesense_->isChecked());
383 void GuiSearchWidget::wholeWordsActTriggered()
385 wordsCB->setChecked(act_wholewords_->isChecked());
390 void GuiSearchWidget::searchSelActTriggered()
392 selectionCB->setChecked(act_selection_->isChecked());
397 void GuiSearchWidget::immediateActTriggered()
399 instantSearchCB->setChecked(act_immediate_->isChecked());
404 void GuiSearchWidget::wrapActTriggered()
406 wrapCB->setChecked(act_wrap_->isChecked());
411 void GuiSearchWidget::showEvent(QShowEvent * e)
415 findCO->lineEdit()->selectAll();
416 QWidget::showEvent(e);
420 void GuiSearchWidget::hideEvent(QHideEvent *)
422 dispatch(FuncRequest(LFUN_DIALOG_HIDE, "findreplace"));
424 // update toolbar status
426 bv_->buffer().updateBuffer();
430 void GuiSearchWidget::findBufferChanged()
432 docstring search = theClipboard().getFindBuffer();
433 // update from find buffer, but only if the strings differ (else we
434 // might end up in loops with search as you type)
435 if (!search.empty() && toqstr(search) != findCO->lineEdit()->text()) {
436 LYXERR(Debug::CLIPBOARD, "from findbuffer: " << search);
437 findCO->lineEdit()->selectAll();
438 findCO->lineEdit()->insert(toqstr(search));
439 findCO->lineEdit()->selectAll();
444 void GuiSearchWidget::findChanged()
446 bool const emptytext = findCO->currentText().isEmpty();
447 findPB->setEnabled(!emptytext);
448 findPrevPB->setEnabled(!emptytext);
449 bool const replace = !emptytext && bv_ && !bv_->buffer().isReadonly();
450 replacePB->setEnabled(replace);
451 replacePrevPB->setEnabled(replace);
452 replaceallPB->setEnabled(replace);
453 if (instantSearchCB->isChecked())
458 void GuiSearchWidget::findClicked()
464 void GuiSearchWidget::findPrevClicked()
470 void GuiSearchWidget::replaceClicked()
476 void GuiSearchWidget::replacePrevClicked()
482 void GuiSearchWidget::replaceallClicked()
484 replace(qstring_to_ucs4(findCO->currentText()),
485 qstring_to_ucs4(replaceCO->currentText()),
486 caseCB->isChecked(), wordsCB->isChecked(),
487 true, true, true, selectionCB->isChecked());
488 uniqueInsert(findCO, findCO->currentText());
489 uniqueInsert(replaceCO, replaceCO->currentText());
493 void GuiSearchWidget::doFind(bool const backwards, bool const instant)
495 docstring const needle = qstring_to_ucs4(findCO->currentText());
496 find(needle, caseCB->isChecked(), wordsCB->isChecked(), !backwards,
497 instant, wrapCB->isChecked(), selectionCB->isChecked());
498 uniqueInsert(findCO, findCO->currentText());
500 findCO->lineEdit()->selectAll();
504 void GuiSearchWidget::find(docstring const & search, bool casesensitive,
505 bool matchword, bool forward, bool instant,
506 bool wrap, bool onlysel)
508 docstring const sdata =
509 find2string(search, casesensitive, matchword,
510 forward, wrap, instant, onlysel);
512 dispatch(FuncRequest(LFUN_WORD_FIND, sdata));
516 void GuiSearchWidget::doReplace(bool const backwards)
518 docstring const needle = qstring_to_ucs4(findCO->currentText());
519 docstring const repl = qstring_to_ucs4(replaceCO->currentText());
520 replace(needle, repl, caseCB->isChecked(), wordsCB->isChecked(),
521 !backwards, false, wrapCB->isChecked(), selectionCB->isChecked());
522 uniqueInsert(findCO, findCO->currentText());
523 uniqueInsert(replaceCO, replaceCO->currentText());
527 void GuiSearchWidget::replace(docstring const & search, docstring const & replace,
528 bool casesensitive, bool matchword,
529 bool forward, bool all, bool wrap, bool onlysel)
531 docstring const sdata =
532 replace2string(replace, search, casesensitive,
533 matchword, all, forward, true, wrap, onlysel);
535 dispatch(FuncRequest(LFUN_WORD_REPLACE, sdata));
538 void GuiSearchWidget::saveSession(QSettings & settings, QString const & session_key) const
540 settings.setValue(session_key + "/casesensitive", caseCB->isChecked());
541 settings.setValue(session_key + "/words", wordsCB->isChecked());
542 settings.setValue(session_key + "/instant", instantSearchCB->isChecked());
543 settings.setValue(session_key + "/wrap", wrapCB->isChecked());
544 settings.setValue(session_key + "/selection", selectionCB->isChecked());
545 settings.setValue(session_key + "/minimized", minimized_);
549 void GuiSearchWidget::restoreSession(QString const & session_key)
552 caseCB->setChecked(settings.value(session_key + "/casesensitive", false).toBool());
553 act_casesense_->setChecked(settings.value(session_key + "/casesensitive", false).toBool());
554 wordsCB->setChecked(settings.value(session_key + "/words", false).toBool());
555 act_wholewords_->setChecked(settings.value(session_key + "/words", false).toBool());
556 instantSearchCB->setChecked(settings.value(session_key + "/instant", false).toBool());
557 act_immediate_->setChecked(settings.value(session_key + "/instant", false).toBool());
558 wrapCB->setChecked(settings.value(session_key + "/wrap", true).toBool());
559 act_wrap_->setChecked(settings.value(session_key + "/wrap", true).toBool());
560 selectionCB->setChecked(settings.value(session_key + "/selection", false).toBool());
561 act_selection_->setChecked(settings.value(session_key + "/selection", false).toBool());
562 minimized_ = settings.value(session_key + "/minimized", false).toBool();
563 // initialize hidings
564 minimizeClicked(false);
568 GuiSearch::GuiSearch(GuiView & parent, Qt::DockWidgetArea area, Qt::WindowFlags flags)
569 : DockView(parent, "findreplace", qt_("Search and Replace"), area, flags),
570 widget_(new GuiSearchWidget(this, parent))
573 widget_->setBufferView(bufferview());
574 setFocusProxy(widget_);
576 connect(widget_, SIGNAL(needTitleBarUpdate()), this, SLOT(updateTitle()));
577 connect(widget_, SIGNAL(needSizeUpdate()), this, SLOT(updateSize()));
581 void GuiSearch::mousePressEvent(QMouseEvent * event)
583 if (isFloating() && event->button() == Qt::LeftButton) {
584 #if QT_VERSION >= 0x060000
585 dragPosition = event->globalPosition().toPoint() - frameGeometry().topLeft();
587 dragPosition = event->globalPos() - frameGeometry().topLeft();
591 DockView::mousePressEvent(event);
595 void GuiSearch::mouseMoveEvent(QMouseEvent * event)
597 if (isFloating() && event->buttons() & Qt::LeftButton) {
598 #if QT_VERSION >= 0x060000
599 move(event->globalPosition().toPoint() - dragPosition);
601 move(event->globalPos() - dragPosition);
605 DockView::mouseMoveEvent(event);
609 void GuiSearch::mouseDoubleClickEvent(QMouseEvent * event)
611 if (event->button() == Qt::LeftButton)
612 setFloating(!isFloating());
614 DockView::mouseDoubleClickEvent(event);
618 void GuiSearch::onBufferViewChanged()
620 widget_->setEnabled(static_cast<bool>(bufferview()));
621 widget_->setBufferView(bufferview());
625 void GuiSearch::updateView()
632 void GuiSearch::saveSession(QSettings & settings) const
634 Dialog::saveSession(settings);
635 widget_->saveSession(settings, sessionKey());
639 void GuiSearch::restoreSession()
641 DockView::restoreSession();
642 widget_->restoreSession(sessionKey());
646 void GuiSearch::updateTitle()
648 if (widget_->isMinimized()) {
650 setTitleBarWidget(new QWidget());
651 titleBarWidget()->hide();
652 } else if (titleBarWidget()) {
654 setTitleBarWidget(nullptr);
659 void GuiSearch::updateSize()
661 widget_->setFixedHeight(widget_->sizeHint().height());
662 if (widget_->isMinimized())
663 setFixedHeight(widget_->sizeHint().height());
665 // undo setFixedHeight
666 setMaximumHeight(QWIDGETSIZE_MAX);
673 } // namespace frontend
677 #include "moc_GuiSearch.cpp"