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 #include <QSvgRenderer>
48 using namespace lyx::support;
55 static void uniqueInsert(QComboBox * box, QString const & text)
57 for (int i = box->count(); --i >= 0; )
58 if (box->itemText(i) == text)
61 box->insertItem(0, text);
65 GuiSearchWidget::GuiSearchWidget(QWidget * parent, GuiView & view)
66 : QWidget(parent), view_(view)
70 // fix height to minimum
71 setFixedHeight(sizeHint().height());
73 // align items in grid on top
74 gridLayout->setAlignment(Qt::AlignTop);
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(instantSearchCB, SIGNAL(clicked()), this, SLOT(immediateClicked()));
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()));
91 setFocusProxy(findCO);
93 // Use a FancyLineEdit due to the indicator icons
94 findLE_ = new FancyLineEdit(this);
95 findCO->setLineEdit(findLE_);
97 // And a menu in minimal mode
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);
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_);
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()));
123 findCO->setCompleter(nullptr);
124 replaceCO->setCompleter(nullptr);
126 replacePB->setEnabled(false);
127 replacePrevPB->setEnabled(false);
128 replaceallPB->setEnabled(false);
132 bool GuiSearchWidget::initialiseParams(std::string const & str)
135 // selectAll & insert rather than setText in order to keep undo stack
136 findCO->lineEdit()->selectAll();
137 findCO->lineEdit()->insert(toqstr(str));
140 findCO->lineEdit()->selectAll();
145 void GuiSearchWidget::keyPressEvent(QKeyEvent * ev)
148 setKeySymbol(&sym, ev);
150 // catch Return and Shift-Return
151 if (ev->key() == Qt::Key_Return || ev->key() == Qt::Key_Enter) {
152 doFind(ev->modifiers() == Qt::ShiftModifier);
155 if (ev->key() == Qt::Key_Escape) {
156 Qt::KeyboardModifiers mod = ev->modifiers();
157 if (mod & Qt::AltModifier) {
158 QWidget::keyPressEvent(ev);
162 dispatch(FuncRequest(LFUN_DIALOG_HIDE, "findreplace"));
164 bv_->buffer().updateBuffer();
168 // we catch the key sequences for forward and backwards search
170 KeyModifier mod = lyx::q_key_state(ev->modifiers());
171 KeySequence keyseq(&theTopLevelKeymap(), &theTopLevelKeymap());
172 FuncRequest fr = keyseq.addkey(sym, mod);
173 if (fr == FuncRequest(LFUN_WORD_FIND_FORWARD)
174 || fr == FuncRequest(LFUN_WORD_FIND)) {
178 if (fr == FuncRequest(LFUN_WORD_FIND_BACKWARD)) {
182 if (fr == FuncRequest(LFUN_DIALOG_TOGGLE, "findreplace")) {
187 QWidget::keyPressEvent(ev);
191 void GuiSearchWidget::minimizeClicked(bool const toggle)
194 minimized_ = !minimized_;
196 replaceLA->setHidden(minimized_);
197 replaceCO->setHidden(minimized_);
198 replacePB->setHidden(minimized_);
199 replacePrevPB->setHidden(minimized_);
200 replaceallPB->setHidden(minimized_);
201 CBFrame->setHidden(minimized_);
204 minimizePB->setText(qt_("Ex&pand"));
205 minimizePB->setToolTip(qt_("Show replace and option widgets"));
208 act_casesense_->setChecked(caseCB->isChecked());
209 act_immediate_->setChecked(instantSearchCB->isChecked());
210 act_selection_->setChecked(selectionCB->isChecked());
211 act_selection_->setEnabled(!instantSearchCB->isChecked());
212 act_wholewords_->setChecked(wordsCB->isChecked());
213 act_wrap_->setChecked(wrapCB->isChecked());
216 minimizePB->setText(qt_("&Minimize"));
217 minimizePB->setToolTip(qt_("Hide replace and option widgets"));
220 Q_EMIT needSizeUpdate();
221 Q_EMIT needTitleBarUpdate();
226 void GuiSearchWidget::handleIndicators()
228 findLE_->setButtonVisible(FancyLineEdit::Right, minimized_);
234 if (caseCB->isChecked())
236 if (wordsCB->isChecked())
238 if (selectionCB->isChecked())
240 if (instantSearchCB->isChecked())
242 if (wrapCB->isChecked())
245 bool const dark_mode = guiApp && guiApp->isInDarkMode();
247 // Consider device/pixel ratio (HiDPI)
248 if (guiApp && guiApp->currentView())
249 dpr = guiApp->currentView()->devicePixelRatio();
250 QString imagedir = "images/";
251 QPixmap bpixmap = getPixmap("images/", "search-options", "svgz,png");
252 QPixmap pm = bpixmap;
256 QPixmap scaled_pm = QPixmap(bpixmap.size() * dpr);
257 pm = QPixmap(pms * scaled_pm.width() + ((pms - 1) * gap),
259 pm.fill(Qt::transparent);
260 QPainter painter(&pm);
263 tip = qt_("Active options:");
265 if (caseCB->isChecked()) {
266 tip += "<li>" + qt_("Case sensitive search");
267 QPixmap spixmap = getPixmap("images/", "search-case-sensitive", "svgz,png");
268 // We render SVG directly for HiDPI scalability
269 FileName fname = imageLibFileSearch(imagedir, "search-case-sensitive", "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(0, 0, spixmap.width() * dpr,
275 spixmap.height() * dpr));
277 x += (spixmap.width() * dpr) + gap;
279 if (wordsCB->isChecked()) {
280 tip += "<li>" + qt_("Whole words only");
281 QPixmap spixmap = getPixmap("images/", "search-whole-words", "svgz,png");
282 FileName fname = imageLibFileSearch(imagedir, "search-whole-words", "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));
290 x += (spixmap.width() * dpr) + gap;
292 if (selectionCB->isChecked()) {
293 tip += "<li>" + qt_("Search only in selection");
294 QPixmap spixmap = getPixmap("images/", "search-selection", "svgz,png");
295 FileName fname = imageLibFileSearch(imagedir, "search-selection", "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));
303 x += (spixmap.width() * dpr) + gap;
305 if (instantSearchCB->isChecked()) {
306 tip += "<li>" + qt_("Search as you type");
307 QPixmap spixmap = getPixmap("images/", "search-instant", "svgz,png");
308 FileName fname = imageLibFileSearch(imagedir, "search-instant", "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));
316 x += (spixmap.width() * dpr) + gap;
318 if (wrapCB->isChecked()) {
319 tip += "<li>" + qt_("Wrap search");
320 QPixmap spixmap = getPixmap("images/", "search-wrap", "svgz,png");
321 FileName fname = imageLibFileSearch(imagedir, "search-wrap", "svgz,png");
322 QString fpath = toqstr(fname.absFileName());
323 if (!fpath.isEmpty()) {
324 QSvgRenderer svgRenderer(fpath);
325 if (svgRenderer.isValid())
326 svgRenderer.render(&painter, QRectF(x, 0, spixmap.width() * dpr,
327 spixmap.height() * dpr));
329 x += (spixmap.width() * dpr) + gap;
332 pm.setDevicePixelRatio(dpr);
335 tip = qt_("Click here to change search options");
336 // We render SVG directly for HiDPI scalability
337 FileName fname = imageLibFileSearch(imagedir, "search-options", "svgz,png");
338 QString fpath = toqstr(fname.absFileName());
339 if (!fpath.isEmpty()) {
340 QSvgRenderer svgRenderer(fpath);
341 if (svgRenderer.isValid()) {
342 pm = QPixmap(bpixmap.size() * dpr);
343 pm.fill(Qt::transparent);
344 QPainter painter(&pm);
345 svgRenderer.render(&painter);
346 pm.setDevicePixelRatio(dpr);
351 QImage img = pm.toImage();
353 pm.convertFromImage(img);
355 findLE_->setButtonPixmap(FancyLineEdit::Right, pm);
357 findLE_->setButtonToolTip(FancyLineEdit::Right, tip);
361 void GuiSearchWidget::caseSenseActTriggered()
363 caseCB->setChecked(act_casesense_->isChecked());
368 void GuiSearchWidget::wholeWordsActTriggered()
370 wordsCB->setChecked(act_wholewords_->isChecked());
375 void GuiSearchWidget::searchSelActTriggered()
377 selectionCB->setChecked(act_selection_->isChecked());
382 void GuiSearchWidget::immediateActTriggered()
384 bool const immediate = act_immediate_->isChecked();
385 instantSearchCB->setChecked(immediate);
386 // FIXME: make these two work together eventually.
387 selectionCB->setEnabled(!immediate);
388 act_selection_->setEnabled(!immediate);
390 selectionCB->setChecked(false);
391 act_selection_->setChecked(false);
397 void GuiSearchWidget::immediateClicked()
399 // FIXME: make these two work together eventually.
400 bool const immediate = instantSearchCB->isChecked();
401 selectionCB->setEnabled(!immediate);
403 selectionCB->setChecked(false);
407 void GuiSearchWidget::wrapActTriggered()
409 wrapCB->setChecked(act_wrap_->isChecked());
414 void GuiSearchWidget::showEvent(QShowEvent * e)
417 QWidget::showEvent(e);
421 void GuiSearchWidget::hideEvent(QHideEvent *)
423 dispatch(FuncRequest(LFUN_DIALOG_HIDE, "findreplace"));
425 // update toolbar status
427 bv_->buffer().updateBuffer();
431 void GuiSearchWidget::findBufferChanged()
433 docstring search = theClipboard().getFindBuffer();
434 // update from find buffer, but only if the strings differ (else we
435 // might end up in loops with search as you type)
436 if (!search.empty() && toqstr(search) != findCO->lineEdit()->text()) {
437 LYXERR(Debug::CLIPBOARD, "from findbuffer: " << search);
438 // selectAll & insert rather than setText in order to keep undo stack
439 findCO->lineEdit()->selectAll();
440 findCO->lineEdit()->insert(toqstr(search));
445 void GuiSearchWidget::findChanged()
447 bool const emptytext = findCO->currentText().isEmpty();
448 findPB->setEnabled(!emptytext);
449 findPrevPB->setEnabled(!emptytext);
450 bool const replace = !emptytext && bv_ && !bv_->buffer().isReadonly();
451 replacePB->setEnabled(replace);
452 replacePrevPB->setEnabled(replace);
453 replaceallPB->setEnabled(replace);
454 if (instantSearchCB->isChecked())
459 void GuiSearchWidget::findClicked()
465 void GuiSearchWidget::findPrevClicked()
471 void GuiSearchWidget::replaceClicked()
477 void GuiSearchWidget::replacePrevClicked()
483 void GuiSearchWidget::replaceallClicked()
485 replace(qstring_to_ucs4(findCO->currentText()),
486 qstring_to_ucs4(replaceCO->currentText()),
487 caseCB->isChecked(), wordsCB->isChecked(),
488 true, true, true, selectionCB->isChecked());
489 uniqueInsert(findCO, findCO->currentText());
490 uniqueInsert(replaceCO, replaceCO->currentText());
494 void GuiSearchWidget::doFind(bool const backwards, bool const instant)
496 docstring const needle = qstring_to_ucs4(findCO->currentText());
497 find(needle, caseCB->isChecked(), wordsCB->isChecked(), !backwards,
498 instant, wrapCB->isChecked(), selectionCB->isChecked());
499 uniqueInsert(findCO, findCO->currentText());
501 findCO->lineEdit()->selectAll();
505 void GuiSearchWidget::find(docstring const & search, bool casesensitive,
506 bool matchword, bool forward, bool instant,
507 bool wrap, bool onlysel)
509 docstring const sdata =
510 find2string(search, casesensitive, matchword,
511 forward, wrap, instant, onlysel);
513 dispatch(FuncRequest(LFUN_WORD_FIND, sdata));
517 void GuiSearchWidget::doReplace(bool const backwards)
519 docstring const needle = qstring_to_ucs4(findCO->currentText());
520 docstring const repl = qstring_to_ucs4(replaceCO->currentText());
521 replace(needle, repl, caseCB->isChecked(), wordsCB->isChecked(),
522 !backwards, false, wrapCB->isChecked(), selectionCB->isChecked());
523 uniqueInsert(findCO, findCO->currentText());
524 uniqueInsert(replaceCO, replaceCO->currentText());
528 void GuiSearchWidget::replace(docstring const & search, docstring const & replace,
529 bool casesensitive, bool matchword,
530 bool forward, bool all, bool wrap, bool onlysel)
532 docstring const sdata =
533 replace2string(replace, search, casesensitive,
534 matchword, all, forward, true, wrap, onlysel);
536 dispatch(FuncRequest(LFUN_WORD_REPLACE, sdata));
539 void GuiSearchWidget::saveSession(QSettings & settings, QString const & session_key) const
541 settings.setValue(session_key + "/casesensitive", caseCB->isChecked());
542 settings.setValue(session_key + "/words", wordsCB->isChecked());
543 settings.setValue(session_key + "/instant", instantSearchCB->isChecked());
544 settings.setValue(session_key + "/wrap", wrapCB->isChecked());
545 settings.setValue(session_key + "/selection", selectionCB->isChecked());
546 settings.setValue(session_key + "/minimized", minimized_);
550 void GuiSearchWidget::restoreSession(QString const & session_key)
553 caseCB->setChecked(settings.value(session_key + "/casesensitive", false).toBool());
554 act_casesense_->setChecked(settings.value(session_key + "/casesensitive", false).toBool());
555 wordsCB->setChecked(settings.value(session_key + "/words", false).toBool());
556 act_wholewords_->setChecked(settings.value(session_key + "/words", false).toBool());
557 bool const immediate = settings.value(session_key + "/instant", false).toBool();
558 instantSearchCB->setChecked(immediate);
559 act_immediate_->setChecked(immediate);
560 wrapCB->setChecked(settings.value(session_key + "/wrap", true).toBool());
561 act_wrap_->setChecked(settings.value(session_key + "/wrap", true).toBool());
562 selectionCB->setChecked(settings.value(session_key + "/selection", false).toBool() && !immediate);
563 selectionCB->setEnabled(!immediate);
564 act_selection_->setChecked(settings.value(session_key + "/selection", false).toBool());
565 minimized_ = settings.value(session_key + "/minimized", false).toBool();
566 // initialize hidings
567 minimizeClicked(false);
571 GuiSearch::GuiSearch(GuiView & parent, Qt::DockWidgetArea area, Qt::WindowFlags flags)
572 : DockView(parent, "findreplace", qt_("Search and Replace"), area, flags),
573 widget_(new GuiSearchWidget(this, parent))
576 widget_->setBufferView(bufferview());
577 setFocusProxy(widget_);
579 connect(widget_, SIGNAL(needTitleBarUpdate()), this, SLOT(updateTitle()));
580 connect(widget_, SIGNAL(needSizeUpdate()), this, SLOT(updateSize()));
584 void GuiSearch::mousePressEvent(QMouseEvent * event)
586 if (isFloating() && event->button() == Qt::LeftButton) {
587 #if QT_VERSION >= 0x060000
588 dragPosition = event->globalPosition().toPoint() - frameGeometry().topLeft();
590 dragPosition = event->globalPos() - frameGeometry().topLeft();
594 DockView::mousePressEvent(event);
598 void GuiSearch::mouseMoveEvent(QMouseEvent * event)
600 if (isFloating() && event->buttons() & Qt::LeftButton) {
601 #if QT_VERSION >= 0x060000
602 move(event->globalPosition().toPoint() - dragPosition);
604 move(event->globalPos() - dragPosition);
608 DockView::mouseMoveEvent(event);
612 void GuiSearch::mouseDoubleClickEvent(QMouseEvent * event)
614 if (event->button() == Qt::LeftButton)
615 setFloating(!isFloating());
617 DockView::mouseDoubleClickEvent(event);
621 void GuiSearch::onBufferViewChanged()
623 widget_->setEnabled(static_cast<bool>(bufferview()));
624 widget_->setBufferView(bufferview());
628 void GuiSearch::updateView()
635 void GuiSearch::saveSession(QSettings & settings) const
637 Dialog::saveSession(settings);
638 widget_->saveSession(settings, sessionKey());
642 void GuiSearch::restoreSession()
644 DockView::restoreSession();
645 widget_->restoreSession(sessionKey());
649 void GuiSearch::updateTitle()
651 if (widget_->isMinimized()) {
653 setTitleBarWidget(new QWidget());
654 titleBarWidget()->hide();
655 } else if (titleBarWidget()) {
657 setTitleBarWidget(nullptr);
662 void GuiSearch::updateSize()
664 widget_->setFixedHeight(widget_->sizeHint().height());
665 if (widget_->isMinimized())
666 setFixedHeight(widget_->sizeHint().height());
668 // undo setFixedHeight
669 setMaximumHeight(QWIDGETSIZE_MAX);
676 } // namespace frontend
680 #include "moc_GuiSearch.cpp"