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"
43 #include "QSizePolicy"
44 #if QT_VERSION >= 0x050000
45 #include <QSvgRenderer>
49 using namespace lyx::support;
56 static void uniqueInsert(QComboBox * box, QString const & text)
58 for (int i = box->count(); --i >= 0; )
59 if (box->itemText(i) == text)
62 box->insertItem(0, text);
66 GuiSearchWidget::GuiSearchWidget(QWidget * parent)
71 // fix height to minimum
72 setFixedHeight(sizeHint().height());
74 // align items in grid on top
75 gridLayout->setAlignment(Qt::AlignTop);
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()));
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);
130 // Make this a sub window to prevent focusNextPrevChild (Tab)
131 // switching to the parent (#12170)
132 setWindowFlags(Qt::SubWindow);
136 bool GuiSearchWidget::initialiseParams(std::string const & str)
139 findCO->lineEdit()->setText(toqstr(str));
144 void GuiSearchWidget::keyPressEvent(QKeyEvent * ev)
147 setKeySymbol(&sym, ev);
149 // catch Return and Shift-Return
150 if (ev->key() == Qt::Key_Return || ev->key() == Qt::Key_Enter) {
151 doFind(ev->modifiers() == Qt::ShiftModifier);
154 if (ev->key() == Qt::Key_Escape) {
155 dispatch(FuncRequest(LFUN_DIALOG_HIDE, "findreplace"));
159 // we catch the key sequences for forward and backwards search
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)) {
169 if (fr == FuncRequest(LFUN_WORD_FIND_BACKWARD)) {
173 if (fr == FuncRequest(LFUN_DIALOG_TOGGLE, "findreplace")) {
178 QWidget::keyPressEvent(ev);
182 void GuiSearchWidget::minimizeClicked(bool const toggle)
185 minimized_ = !minimized_;
187 replaceLA->setHidden(minimized_);
188 replaceCO->setHidden(minimized_);
189 replacePB->setHidden(minimized_);
190 replacePrevPB->setHidden(minimized_);
191 replaceallPB->setHidden(minimized_);
192 CBFrame->setHidden(minimized_);
195 minimizePB->setText(qt_("Ex&pand"));
196 minimizePB->setToolTip(qt_("Show replace and option widgets"));
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());
206 minimizePB->setText(qt_("&Minimize"));
207 minimizePB->setToolTip(qt_("Hide replace and option widgets"));
210 Q_EMIT needSizeUpdate();
211 Q_EMIT needTitleBarUpdate();
216 void GuiSearchWidget::handleIndicators()
218 findLE_->setButtonVisible(FancyLineEdit::Right, minimized_);
224 if (caseCB->isChecked())
226 if (wordsCB->isChecked())
228 if (selectionCB->isChecked())
230 if (instantSearchCB->isChecked())
232 if (wrapCB->isChecked())
235 bool const dark_mode = guiApp && guiApp->isInDarkMode();
237 #if QT_VERSION >= 0x050000
238 // Consider device/pixel ratio (HiDPI)
239 if (guiApp && guiApp->currentView())
240 dpr = guiApp->currentView()->devicePixelRatio();
242 QString imagedir = "images/";
243 QPixmap bpixmap = getPixmap("images/", "search-options", "svgz,png");
244 QPixmap pm = bpixmap;
248 QPixmap scaled_pm = QPixmap(bpixmap.size() * dpr);
249 pm = QPixmap(pms * scaled_pm.width() + ((pms - 1) * gap),
251 pm.fill(Qt::transparent);
252 QPainter painter(&pm);
255 tip = qt_("Active options:");
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);
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));
273 x += (spixmap.width() * dpr) + gap;
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);
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));
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 #if QT_VERSION < 0x050000
296 painter.drawPixmap(x, 0, spixmap);
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));
307 x += (spixmap.width() * dpr) + gap;
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);
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));
324 x += (spixmap.width() * dpr) + gap;
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);
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));
341 x += (spixmap.width() * dpr) + gap;
344 #if QT_VERSION >= 0x050000
345 pm.setDevicePixelRatio(dpr);
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);
367 QImage img = pm.toImage();
369 pm.convertFromImage(img);
371 findLE_->setButtonPixmap(FancyLineEdit::Right, pm);
373 findLE_->setButtonToolTip(FancyLineEdit::Right, tip);
377 void GuiSearchWidget::caseSenseActTriggered()
379 caseCB->setChecked(act_casesense_->isChecked());
384 void GuiSearchWidget::wholeWordsActTriggered()
386 wordsCB->setChecked(act_wholewords_->isChecked());
391 void GuiSearchWidget::searchSelActTriggered()
393 selectionCB->setChecked(act_selection_->isChecked());
398 void GuiSearchWidget::immediateActTriggered()
400 instantSearchCB->setChecked(act_immediate_->isChecked());
405 void GuiSearchWidget::wrapActTriggered()
407 wrapCB->setChecked(act_wrap_->isChecked());
412 void GuiSearchWidget::showEvent(QShowEvent * e)
416 findCO->lineEdit()->selectAll();
417 QWidget::showEvent(e);
421 void GuiSearchWidget::findBufferChanged()
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();
435 void GuiSearchWidget::findChanged()
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())
449 void GuiSearchWidget::findClicked()
455 void GuiSearchWidget::findPrevClicked()
461 void GuiSearchWidget::replaceClicked()
467 void GuiSearchWidget::replacePrevClicked()
473 void GuiSearchWidget::replaceallClicked()
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());
484 void GuiSearchWidget::doFind(bool const backwards, bool const instant)
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());
491 findCO->lineEdit()->selectAll();
495 void GuiSearchWidget::find(docstring const & search, bool casesensitive,
496 bool matchword, bool forward, bool instant,
497 bool wrap, bool onlysel)
499 docstring const sdata =
500 find2string(search, casesensitive, matchword,
501 forward, wrap, instant, onlysel);
503 dispatch(FuncRequest(LFUN_WORD_FIND, sdata));
507 void GuiSearchWidget::doReplace(bool const backwards)
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());
518 void GuiSearchWidget::replace(docstring const & search, docstring const & replace,
519 bool casesensitive, bool matchword,
520 bool forward, bool all, bool wrap, bool onlysel)
522 docstring const sdata =
523 replace2string(replace, search, casesensitive,
524 matchword, all, forward, true, wrap, onlysel);
526 dispatch(FuncRequest(LFUN_WORD_REPLACE, sdata));
529 void GuiSearchWidget::saveSession(QSettings & settings, QString const & session_key) const
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_);
540 void GuiSearchWidget::restoreSession(QString const & session_key)
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);
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))
564 widget_->setBufferView(bufferview());
565 setFocusProxy(widget_);
567 connect(widget_, SIGNAL(needTitleBarUpdate()), this, SLOT(updateTitle()));
568 connect(widget_, SIGNAL(needSizeUpdate()), this, SLOT(updateSize()));
571 void GuiSearch::onBufferViewChanged()
573 widget_->setEnabled(static_cast<bool>(bufferview()));
574 widget_->setBufferView(bufferview());
578 void GuiSearch::updateView()
585 void GuiSearch::saveSession(QSettings & settings) const
587 Dialog::saveSession(settings);
588 widget_->saveSession(settings, sessionKey());
592 void GuiSearch::restoreSession()
594 DockView::restoreSession();
595 widget_->restoreSession(sessionKey());
599 void GuiSearch::updateTitle()
601 if (widget_->isMinimized()) {
603 setTitleBarWidget(new QWidget());
604 titleBarWidget()->hide();
607 setTitleBarWidget(nullptr);
611 void GuiSearch::updateSize()
613 widget_->setFixedHeight(widget_->sizeHint().height());
614 if (widget_->isMinimized())
615 setFixedHeight(widget_->sizeHint().height());
617 // undo setFixedHeight
618 setMaximumHeight(QWIDGETSIZE_MAX);
625 } // namespace frontend
629 #include "moc_GuiSearch.cpp"