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(findCO, SIGNAL(editTextChanged(QString)),
83 this, SLOT(findChanged()));
84 if(qApp->clipboard()->supportsFindBuffer()) {
85 connect(qApp->clipboard(), SIGNAL(findBufferChanged()),
86 this, SLOT(findBufferChanged()));
90 setFocusProxy(findCO);
92 // Use a FancyLineEdit due to the indicator icons
93 findLE_ = new FancyLineEdit(this);
94 findCO->setLineEdit(findLE_);
96 // And a menu in minimal mode
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);
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_);
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()));
122 findCO->setCompleter(nullptr);
123 replaceCO->setCompleter(nullptr);
125 replacePB->setEnabled(false);
126 replacePrevPB->setEnabled(false);
127 replaceallPB->setEnabled(false);
131 bool GuiSearchWidget::initialiseParams(std::string const & str)
134 findCO->lineEdit()->setText(toqstr(str));
139 void GuiSearchWidget::keyPressEvent(QKeyEvent * ev)
142 setKeySymbol(&sym, ev);
144 // catch Return and Shift-Return
145 if (ev->key() == Qt::Key_Return || ev->key() == Qt::Key_Enter) {
146 doFind(ev->modifiers() == Qt::ShiftModifier);
149 if (ev->key() == Qt::Key_Escape) {
150 dispatch(FuncRequest(LFUN_DIALOG_HIDE, "findreplace"));
152 bv_->buffer().updateBuffer();
156 // we catch the key sequences for forward and backwards search
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)) {
166 if (fr == FuncRequest(LFUN_WORD_FIND_BACKWARD)) {
170 if (fr == FuncRequest(LFUN_DIALOG_TOGGLE, "findreplace")) {
175 QWidget::keyPressEvent(ev);
179 void GuiSearchWidget::minimizeClicked(bool const toggle)
182 minimized_ = !minimized_;
184 replaceLA->setHidden(minimized_);
185 replaceCO->setHidden(minimized_);
186 replacePB->setHidden(minimized_);
187 replacePrevPB->setHidden(minimized_);
188 replaceallPB->setHidden(minimized_);
189 CBFrame->setHidden(minimized_);
192 minimizePB->setText(qt_("Ex&pand"));
193 minimizePB->setToolTip(qt_("Show replace and option widgets"));
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());
203 minimizePB->setText(qt_("&Minimize"));
204 minimizePB->setToolTip(qt_("Hide replace and option widgets"));
207 Q_EMIT needSizeUpdate();
208 Q_EMIT needTitleBarUpdate();
213 void GuiSearchWidget::handleIndicators()
215 findLE_->setButtonVisible(FancyLineEdit::Right, minimized_);
221 if (caseCB->isChecked())
223 if (wordsCB->isChecked())
225 if (selectionCB->isChecked())
227 if (instantSearchCB->isChecked())
229 if (wrapCB->isChecked())
232 bool const dark_mode = guiApp && guiApp->isInDarkMode();
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;
243 QPixmap scaled_pm = QPixmap(bpixmap.size() * dpr);
244 pm = QPixmap(pms * scaled_pm.width() + ((pms - 1) * gap),
246 pm.fill(Qt::transparent);
247 QPainter painter(&pm);
250 tip = qt_("Active options:");
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));
264 x += (spixmap.width() * dpr) + gap;
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));
277 x += (spixmap.width() * dpr) + gap;
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));
290 x += (spixmap.width() * dpr) + gap;
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));
303 x += (spixmap.width() * dpr) + gap;
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));
316 x += (spixmap.width() * dpr) + gap;
319 pm.setDevicePixelRatio(dpr);
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);
338 QImage img = pm.toImage();
340 pm.convertFromImage(img);
342 findLE_->setButtonPixmap(FancyLineEdit::Right, pm);
344 findLE_->setButtonToolTip(FancyLineEdit::Right, tip);
348 void GuiSearchWidget::caseSenseActTriggered()
350 caseCB->setChecked(act_casesense_->isChecked());
355 void GuiSearchWidget::wholeWordsActTriggered()
357 wordsCB->setChecked(act_wholewords_->isChecked());
362 void GuiSearchWidget::searchSelActTriggered()
364 selectionCB->setChecked(act_selection_->isChecked());
369 void GuiSearchWidget::immediateActTriggered()
371 instantSearchCB->setChecked(act_immediate_->isChecked());
376 void GuiSearchWidget::wrapActTriggered()
378 wrapCB->setChecked(act_wrap_->isChecked());
383 void GuiSearchWidget::showEvent(QShowEvent * e)
387 findCO->lineEdit()->selectAll();
388 QWidget::showEvent(e);
392 void GuiSearchWidget::hideEvent(QHideEvent *)
394 dispatch(FuncRequest(LFUN_DIALOG_HIDE, "findreplace"));
396 // update toolbar status
398 bv_->buffer().updateBuffer();
402 void GuiSearchWidget::findBufferChanged()
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();
416 void GuiSearchWidget::findChanged()
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())
430 void GuiSearchWidget::findClicked()
436 void GuiSearchWidget::findPrevClicked()
442 void GuiSearchWidget::replaceClicked()
448 void GuiSearchWidget::replacePrevClicked()
454 void GuiSearchWidget::replaceallClicked()
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());
465 void GuiSearchWidget::doFind(bool const backwards, bool const instant)
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());
472 findCO->lineEdit()->selectAll();
476 void GuiSearchWidget::find(docstring const & search, bool casesensitive,
477 bool matchword, bool forward, bool instant,
478 bool wrap, bool onlysel)
480 docstring const sdata =
481 find2string(search, casesensitive, matchword,
482 forward, wrap, instant, onlysel);
484 dispatch(FuncRequest(LFUN_WORD_FIND, sdata));
488 void GuiSearchWidget::doReplace(bool const backwards)
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());
499 void GuiSearchWidget::replace(docstring const & search, docstring const & replace,
500 bool casesensitive, bool matchword,
501 bool forward, bool all, bool wrap, bool onlysel)
503 docstring const sdata =
504 replace2string(replace, search, casesensitive,
505 matchword, all, forward, true, wrap, onlysel);
507 dispatch(FuncRequest(LFUN_WORD_REPLACE, sdata));
510 void GuiSearchWidget::saveSession(QSettings & settings, QString const & session_key) const
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_);
521 void GuiSearchWidget::restoreSession(QString const & session_key)
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);
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))
545 widget_->setBufferView(bufferview());
546 setFocusProxy(widget_);
548 connect(widget_, SIGNAL(needTitleBarUpdate()), this, SLOT(updateTitle()));
549 connect(widget_, SIGNAL(needSizeUpdate()), this, SLOT(updateSize()));
553 void GuiSearch::mousePressEvent(QMouseEvent * event)
555 if (isFloating() && event->button() == Qt::LeftButton) {
556 #if QT_VERSION >= 0x060000
557 dragPosition = event->globalPosition().toPoint() - frameGeometry().topLeft();
559 dragPosition = event->globalPos() - frameGeometry().topLeft();
563 DockView::mousePressEvent(event);
567 void GuiSearch::mouseMoveEvent(QMouseEvent * event)
569 if (isFloating() && event->buttons() & Qt::LeftButton) {
570 #if QT_VERSION >= 0x060000
571 move(event->globalPosition().toPoint() - dragPosition);
573 move(event->globalPos() - dragPosition);
577 DockView::mouseMoveEvent(event);
581 void GuiSearch::mouseDoubleClickEvent(QMouseEvent * event)
583 if (event->button() == Qt::LeftButton)
584 setFloating(!isFloating());
586 DockView::mouseDoubleClickEvent(event);
590 void GuiSearch::onBufferViewChanged()
592 widget_->setEnabled(static_cast<bool>(bufferview()));
593 widget_->setBufferView(bufferview());
597 void GuiSearch::updateView()
604 void GuiSearch::saveSession(QSettings & settings) const
606 Dialog::saveSession(settings);
607 widget_->saveSession(settings, sessionKey());
611 void GuiSearch::restoreSession()
613 DockView::restoreSession();
614 widget_->restoreSession(sessionKey());
618 void GuiSearch::updateTitle()
620 if (widget_->isMinimized()) {
622 setTitleBarWidget(new QWidget());
623 titleBarWidget()->hide();
624 } else if (titleBarWidget()) {
626 setTitleBarWidget(nullptr);
631 void GuiSearch::updateSize()
633 widget_->setFixedHeight(widget_->sizeHint().height());
634 if (widget_->isMinimized())
635 setFixedHeight(widget_->sizeHint().height());
637 // undo setFixedHeight
638 setMaximumHeight(QWIDGETSIZE_MAX);
645 } // namespace frontend
649 #include "moc_GuiSearch.cpp"