2 * \file FindAndReplace.cpp
3 * This file is part of LyX, the document processor.
4 * Licence details can be found in the file COPYING.
6 * \author Tommaso Cucinotta
8 * Full author contact details are available in file CREDITS.
13 #include "FindAndReplace.h"
15 #include "GuiApplication.h"
19 #include "BufferList.h"
20 #include "BufferParams.h"
21 #include "BufferView.h"
23 #include "FuncRequest.h"
29 #include "frontends/alert.h"
31 #include "support/debug.h"
32 #include "support/docstream.h"
33 #include "support/filetools.h"
34 #include "support/FileName.h"
35 #include "support/gettext.h"
36 #include "support/lassert.h"
37 #include "support/lstrings.h"
39 #include <QCloseEvent>
44 using namespace lyx::support;
50 FindAndReplaceWidget::FindAndReplaceWidget(GuiView & view)
51 : QTabWidget(&view), view_(view)
54 find_work_area_->setGuiView(view_);
55 find_work_area_->init();
56 find_work_area_->setFrameStyle(QFrame::StyledPanel);
58 setFocusProxy(find_work_area_);
59 replace_work_area_->setGuiView(view_);
60 replace_work_area_->init();
61 replace_work_area_->setFrameStyle(QFrame::StyledPanel);
63 // We don't want two cursors blinking.
64 find_work_area_->stopBlinkingCaret();
65 replace_work_area_->stopBlinkingCaret();
66 old_buffer_ = view_.documentBufferView() ?
67 &(view_.documentBufferView()->buffer()) : 0;
70 cbVerticalLayout->setAlignment(Qt::AlignTop);
71 pbVerticalLayout->setAlignment(Qt::AlignTop);
75 void FindAndReplaceWidget::dockLocationChanged(Qt::DockWidgetArea area)
77 if (area == Qt::RightDockWidgetArea || area == Qt::LeftDockWidgetArea) {
78 dynamicLayoutBasic_->setDirection(QBoxLayout::TopToBottom);
79 dynamicLayoutAdvanced_->setDirection(QBoxLayout::TopToBottom);
81 dynamicLayoutBasic_->setDirection(QBoxLayout::LeftToRight);
82 dynamicLayoutAdvanced_->setDirection(QBoxLayout::LeftToRight);
87 bool FindAndReplaceWidget::eventFilter(QObject * obj, QEvent * event)
90 if (event->type() != QEvent::KeyPress
91 || (obj != find_work_area_ && obj != replace_work_area_))
92 return QWidget::eventFilter(obj, event);
94 QKeyEvent * e = static_cast<QKeyEvent *> (event);
97 if (e->modifiers() == Qt::NoModifier) {
104 case Qt::Key_Return: {
105 // with shift we (temporarily) change search/replace direction
106 bool const searchback = searchbackCB->isChecked();
107 if (e->modifiers() == Qt::ShiftModifier && !searchback)
108 searchbackCB->setChecked(true);
110 if (obj == find_work_area_)
111 on_findNextPB_clicked();
113 on_replacePB_clicked();
114 // back to how it was
115 searchbackCB->setChecked(searchback);
120 if (e->modifiers() == Qt::NoModifier) {
121 if (obj == find_work_area_){
122 LYXERR(Debug::FIND, "Focusing replace WA");
123 replace_work_area_->setFocus();
124 LYXERR(Debug::FIND, "Selecting entire replace buffer");
125 dispatch(FuncRequest(LFUN_BUFFER_BEGIN));
126 dispatch(FuncRequest(LFUN_BUFFER_END_SELECT));
132 case Qt::Key_Backtab:
133 if (obj == replace_work_area_) {
134 LYXERR(Debug::FIND, "Focusing find WA");
135 find_work_area_->setFocus();
136 LYXERR(Debug::FIND, "Selecting entire find buffer");
137 dispatch(FuncRequest(LFUN_BUFFER_BEGIN));
138 dispatch(FuncRequest(LFUN_BUFFER_END_SELECT));
146 // standard event processing
147 return QWidget::eventFilter(obj, event);
151 static vector<string> const & allManualsFiles()
153 static const char * files[] = {
154 "Intro", "UserGuide", "Tutorial", "Additional",
155 "EmbeddedObjects", "Math", "Customization", "Shortcuts",
156 "LFUNs", "LaTeXConfig"
159 static vector<string> v;
162 for (size_t i = 0; i < sizeof(files) / sizeof(files[0]); ++i) {
163 fname = i18nLibFileSearch("doc", files[i], "lyx");
164 v.push_back(fname.absFileName());
172 /** Switch buf to point to next document buffer.
174 ** Return true if restarted from master-document buffer.
176 static bool nextDocumentBuffer(Buffer * & buf)
178 ListOfBuffers const children = buf->allRelatives();
179 LYXERR(Debug::FIND, "children.size()=" << children.size());
180 ListOfBuffers::const_iterator it =
181 find(children.begin(), children.end(), buf);
182 LASSERT(it != children.end(), return false);
184 if (it == children.end()) {
185 buf = *children.begin();
193 /** Switch p_buf to point to previous document buffer.
195 ** Return true if restarted from last child buffer.
197 static bool prevDocumentBuffer(Buffer * & buf)
199 ListOfBuffers const children = buf->allRelatives();
200 LYXERR(Debug::FIND, "children.size()=" << children.size());
201 ListOfBuffers::const_iterator it =
202 find(children.begin(), children.end(), buf);
203 LASSERT(it != children.end(), return false)
204 if (it == children.begin()) {
216 /** Switch buf to point to next or previous buffer in search scope.
218 ** Return true if restarted from scratch.
220 static bool nextPrevBuffer(Buffer * & buf,
221 FindAndReplaceOptions const & opt)
223 bool restarted = false;
225 case FindAndReplaceOptions::S_BUFFER:
228 case FindAndReplaceOptions::S_DOCUMENT:
230 restarted = nextDocumentBuffer(buf);
232 restarted = prevDocumentBuffer(buf);
234 case FindAndReplaceOptions::S_OPEN_BUFFERS:
236 buf = theBufferList().next(buf);
237 restarted = (buf == *theBufferList().begin());
239 buf = theBufferList().previous(buf);
240 restarted = (buf == *(theBufferList().end() - 1));
243 case FindAndReplaceOptions::S_ALL_MANUALS:
244 vector<string> const & manuals = allManualsFiles();
245 vector<string>::const_iterator it =
246 find(manuals.begin(), manuals.end(), buf->absFileName());
247 if (it == manuals.end())
248 it = manuals.begin();
249 else if (opt.forward) {
251 if (it == manuals.end()) {
252 it = manuals.begin();
256 if (it == manuals.begin()) {
262 FileName const & fname = FileName(*it);
263 if (!theBufferList().exists(fname)) {
264 guiApp->currentView()->setBusy(false);
265 guiApp->currentView()->loadDocument(fname, false);
266 guiApp->currentView()->setBusy(true);
268 buf = theBufferList().getBuffer(fname);
275 /** Find the finest question message to post to the user */
276 docstring getQuestionString(FindAndReplaceOptions const & opt)
280 case FindAndReplaceOptions::S_BUFFER:
283 case FindAndReplaceOptions::S_DOCUMENT:
284 scope = _("Master document");
286 case FindAndReplaceOptions::S_OPEN_BUFFERS:
287 scope = _("Open files");
289 case FindAndReplaceOptions::S_ALL_MANUALS:
290 scope = _("Manuals");
293 docstring message = opt.forward ?
294 bformat(_("%1$s: the end was reached while searching forward.\n"
295 "Continue searching from the beginning?"),
297 bformat(_("%1$s: the beginning was reached while searching backward.\n"
298 "Continue searching from the end?"),
305 /// Return true if a match was found
306 bool FindAndReplaceWidget::findAndReplaceScope(FindAndReplaceOptions & opt, bool replace_all)
308 BufferView * bv = view_.documentBufferView();
311 Buffer * buf = &bv->buffer();
312 Buffer * buf_orig = &bv->buffer();
313 DocIterator cur_orig(bv->cursor());
314 int wrap_answer = -1;
315 opt.replace_all = replace_all;
318 FuncRequest cmd(LFUN_WORD_FINDADV, from_utf8(oss.str()));
320 view_.message(_("Advanced search in progress (press ESC to cancel) . . ."));
321 theApp()->startLongOperation();
323 if (opt.scope == FindAndReplaceOptions::S_ALL_MANUALS) {
324 vector<string> const & v = allManualsFiles();
325 if (std::find(v.begin(), v.end(), buf->absFileName()) == v.end()) {
326 FileName const & fname = FileName(*v.begin());
327 if (!theBufferList().exists(fname)) {
328 guiApp->currentView()->setBusy(false);
329 theApp()->stopLongOperation();
330 guiApp->currentView()->loadDocument(fname, false);
331 theApp()->startLongOperation();
332 guiApp->currentView()->setBusy(true);
334 buf = theBufferList().getBuffer(fname);
336 view_.setBusy(false);
340 lyx::dispatch(FuncRequest(LFUN_BUFFER_SWITCH,
341 buf->absFileName()));
342 bv = view_.documentBufferView();
343 bv->cursor().clear();
344 bv->cursor().push_back(CursorSlice(buf->inset()));
348 UndoGroupHelper helper(buf);
351 LYXERR(Debug::FIND, "Dispatching LFUN_WORD_FINDADV");
353 LYXERR(Debug::FIND, "dispatched");
354 if (bv->cursor().result().dispatched()) {
355 // New match found and selected (old selection replaced if needed)
358 view_.setBusy(false);
359 theApp()->stopLongOperation();
361 } else if (replace_all)
362 bv->clearSelection();
364 if (theApp()->longOperationCancelled()) {
365 // Search aborted by user
366 view_.message(_("Advanced search cancelled by user"));
367 view_.setBusy(false);
368 theApp()->stopLongOperation();
372 // No match found in current buffer (however old selection might have been replaced)
373 // select next buffer in scope, if any
374 bool const prompt = nextPrevBuffer(buf, opt);
378 if (wrap_answer != -1)
380 docstring q = getQuestionString(opt);
381 view_.setBusy(false);
382 theApp()->stopLongOperation();
383 wrap_answer = frontend::Alert::prompt(
384 _("Wrap search?"), q,
385 0, 1, _("&Yes"), _("&No"));
386 theApp()->startLongOperation();
388 if (wrap_answer == 1)
391 if (buf != &view_.documentBufferView()->buffer())
392 lyx::dispatch(FuncRequest(LFUN_BUFFER_SWITCH,
393 buf->absFileName()));
395 helper.resetBuffer(buf);
397 bv = view_.documentBufferView();
399 bv->cursor().clear();
400 bv->cursor().push_back(CursorSlice(buf->inset()));
402 //lyx::dispatch(FuncRequest(LFUN_BUFFER_END));
403 bv->cursor().setCursor(doc_iterator_end(buf));
404 bv->cursor().backwardPos();
405 LYXERR(Debug::FIND, "findBackAdv5: cur: "
408 bv->clearSelection();
409 } while (wrap_answer != 1);
411 if (buf_orig != &view_.documentBufferView()->buffer())
412 lyx::dispatch(FuncRequest(LFUN_BUFFER_SWITCH,
413 buf_orig->absFileName()));
414 bv = view_.documentBufferView();
415 // This may happen after a replace occurred
416 if (cur_orig.pos() > cur_orig.lastpos())
417 cur_orig.pos() = cur_orig.lastpos();
418 bv->cursor().setCursor(cur_orig);
419 view_.setBusy(false);
420 theApp()->stopLongOperation();
425 /// Return true if a match was found
426 bool FindAndReplaceWidget::findAndReplace(
427 bool casesensitive, bool matchword, bool backwards,
428 bool expandmacros, bool ignoreformat, bool replace,
429 bool keep_case, bool replace_all)
431 Buffer & find_buf = find_work_area_->bufferView().buffer();
432 docstring const & find_buf_name = find_buf.fileName().absoluteFilePath();
434 if (find_buf.text().empty()) {
435 view_.message(_("Nothing to search"));
439 Buffer & repl_buf = replace_work_area_->bufferView().buffer();
440 docstring const & repl_buf_name = replace ?
441 repl_buf.fileName().absoluteFilePath() : docstring();
443 FindAndReplaceOptions::SearchScope scope =
444 FindAndReplaceOptions::S_BUFFER;
445 if (CurrentDocument->isChecked())
446 scope = FindAndReplaceOptions::S_BUFFER;
447 else if (MasterDocument->isChecked())
448 scope = FindAndReplaceOptions::S_DOCUMENT;
449 else if (OpenDocuments->isChecked())
450 scope = FindAndReplaceOptions::S_OPEN_BUFFERS;
451 else if (AllManualsRB->isChecked())
452 scope = FindAndReplaceOptions::S_ALL_MANUALS;
456 FindAndReplaceOptions::SearchRestriction restr =
457 OnlyMaths->isChecked()
458 ? FindAndReplaceOptions::R_ONLY_MATHS
459 : FindAndReplaceOptions::R_EVERYTHING;
461 LYXERR(Debug::FIND, "FindAndReplaceOptions: "
462 << "find_buf_name=" << find_buf_name
463 << ", casesensitiv=" << casesensitive
464 << ", matchword=" << matchword
465 << ", backwards=" << backwards
466 << ", expandmacros=" << expandmacros
467 << ", ignoreformat=" << ignoreformat
468 << ", repl_buf_name" << repl_buf_name
469 << ", keep_case=" << keep_case
470 << ", scope=" << scope
471 << ", restr=" << restr);
473 FindAndReplaceOptions opt(find_buf_name, casesensitive, matchword,
474 !backwards, expandmacros, ignoreformat,
475 repl_buf_name, keep_case, scope, restr);
476 return findAndReplaceScope(opt, replace_all);
480 bool FindAndReplaceWidget::findAndReplace(bool backwards, bool replace, bool replace_all)
482 if (! view_.currentMainWorkArea()) {
483 view_.message(_("No open document(s) in which to search"));
486 // Finalize macros that are being typed, both in main document and in search or replacement WAs
487 if (view_.currentWorkArea()->bufferView().cursor().macroModeClose())
488 view_.currentWorkArea()->bufferView().processUpdateFlags(Update::Force);
489 if (view_.currentMainWorkArea()->bufferView().cursor().macroModeClose())
490 view_.currentMainWorkArea()->bufferView().processUpdateFlags(Update::Force);
492 // FIXME: create a Dialog::returnFocus()
493 // or something instead of this:
494 view_.setCurrentWorkArea(view_.currentMainWorkArea());
495 return findAndReplace(caseCB->isChecked(),
496 wordsCB->isChecked(),
498 expandMacrosCB->isChecked(),
499 ignoreFormatCB->isChecked(),
501 keepCaseCB->isChecked(),
506 void FindAndReplaceWidget::hideDialog()
508 dispatch(FuncRequest(LFUN_DIALOG_TOGGLE, "findreplaceadv"));
512 void FindAndReplaceWidget::on_findNextPB_clicked()
514 findAndReplace(searchbackCB->isChecked(), false);
515 find_work_area_->setFocus();
519 void FindAndReplaceWidget::on_replacePB_clicked()
521 findAndReplace(searchbackCB->isChecked(), true);
522 replace_work_area_->setFocus();
526 void FindAndReplaceWidget::on_replaceallPB_clicked()
528 findAndReplace(searchbackCB->isChecked(), true, true);
529 replace_work_area_->setFocus();
533 void FindAndReplaceWidget::on_searchbackCB_clicked()
539 // Copy selected elements from bv's BufferParams to the dest_bv's
540 static void copy_params(BufferView const & bv, BufferView & dest_bv) {
541 Buffer const & doc_buf = bv.buffer();
542 BufferParams const & doc_bp = doc_buf.params();
543 Buffer & dest_buf = dest_bv.buffer();
544 dest_buf.params().copyForAdvFR(doc_bp);
545 dest_bv.makeDocumentClass();
546 dest_bv.cursor().current_font.setLanguage(doc_bp.language);
550 void FindAndReplaceWidget::showEvent(QShowEvent * /* ev */)
552 LYXERR(Debug::DEBUG, "showEvent()" << endl);
553 BufferView * bv = view_.documentBufferView();
555 copy_params(*bv, find_work_area_->bufferView());
556 copy_params(*bv, replace_work_area_->bufferView());
559 find_work_area_->installEventFilter(this);
560 replace_work_area_->installEventFilter(this);
562 view_.setCurrentWorkArea(find_work_area_);
563 LYXERR(Debug::FIND, "Selecting entire find buffer");
564 dispatch(FuncRequest(LFUN_BUFFER_BEGIN));
565 dispatch(FuncRequest(LFUN_BUFFER_END_SELECT));
569 void FindAndReplaceWidget::hideEvent(QHideEvent *ev)
571 replace_work_area_->removeEventFilter(this);
572 find_work_area_->removeEventFilter(this);
573 this->QWidget::hideEvent(ev);
577 bool FindAndReplaceWidget::initialiseParams(std::string const & /*params*/)
583 void FindAndReplace::updateView()
585 widget_->updateGUI();
586 widget_->updateButtons();
590 FindAndReplace::FindAndReplace(GuiView & parent,
591 Qt::DockWidgetArea area, Qt::WindowFlags flags)
592 : DockView(parent, "findreplaceadv", qt_("Advanced Find and Replace"),
595 widget_ = new FindAndReplaceWidget(parent);
597 setFocusProxy(widget_);
599 // On Mac show and floating
603 connect(this, SIGNAL(dockLocationChanged(Qt::DockWidgetArea)),
604 widget_, SLOT(dockLocationChanged(Qt::DockWidgetArea)));
608 FindAndReplace::~FindAndReplace()
610 setFocusProxy(nullptr);
615 bool FindAndReplace::initialiseParams(std::string const & params)
617 return widget_->initialiseParams(params);
621 void FindAndReplaceWidget::updateGUI()
623 BufferView * bv = view_.documentBufferView();
625 if (old_buffer_ != &bv->buffer()) {
626 copy_params(*bv, find_work_area_->bufferView());
627 copy_params(*bv, replace_work_area_->bufferView());
628 old_buffer_ = &bv->buffer();
631 old_buffer_ = nullptr;
633 bool const find_enabled = !find_work_area_->bufferView().buffer().empty();
634 findNextPB->setEnabled(find_enabled);
635 bool const replace_enabled = find_enabled && bv && !bv->buffer().isReadonly();
636 replaceLabel->setEnabled(replace_enabled);
637 replace_work_area_->setEnabled(replace_enabled);
638 replacePB->setEnabled(replace_enabled);
639 replaceallPB->setEnabled(replace_enabled);
643 void FindAndReplaceWidget::updateButtons()
645 if (searchbackCB->isChecked()) {
646 findNextPB->setText(qt_("&< Find"));
647 findNextPB->setToolTip(qt_("Find previous occurrence (Shift+Enter, forwards: Enter)"));
648 replacePB->setText(qt_("< Rep&lace"));
649 replacePB->setToolTip(qt_("Replace and find previous occurrence (Shift+Enter, forwards: Enter)"));
651 findNextPB->setText(qt_("Find &>"));
652 findNextPB->setToolTip(qt_("Find next occurrence (Enter, backwards: Shift+Enter)"));
653 replacePB->setText(qt_("Rep&lace >"));
654 replacePB->setToolTip(qt_("Replace and find next occurrence (Enter, backwards: Shift+Enter)"));
659 } // namespace frontend
663 #include "moc_FindAndReplace.cpp"