]> git.lyx.org Git - lyx.git/blob - src/frontends/qt/FindAndReplace.cpp
ab61e1ab7347c325b1f0cc2e2cd853b1d2c80698
[lyx.git] / src / frontends / qt / FindAndReplace.cpp
1 /**
2  * \file FindAndReplace.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author Tommaso Cucinotta
7  *
8  * Full author contact details are available in file CREDITS.
9  */
10
11 #include <config.h>
12
13 #include "FindAndReplace.h"
14
15 #include "GuiApplication.h"
16 #include "GuiView.h"
17
18 #include "Buffer.h"
19 #include "BufferList.h"
20 #include "BufferParams.h"
21 #include "BufferView.h"
22 #include "Cursor.h"
23 #include "FuncRequest.h"
24 #include "Language.h"
25 #include "LyX.h"
26 #include "lyxfind.h"
27 #include "Text.h"
28
29 #include "frontends/alert.h"
30
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"
38
39 #include <QCloseEvent>
40 #include <QLineEdit>
41 #include <QMenu>
42
43 using namespace std;
44 using namespace lyx::support;
45
46 namespace lyx {
47 namespace frontend {
48
49
50 FindAndReplaceWidget::FindAndReplaceWidget(GuiView & view)
51         : QTabWidget(&view), view_(view)
52 {
53         setupUi(this);
54         find_work_area_->setGuiView(view_);
55         find_work_area_->init();
56         find_work_area_->setFrameStyle(QFrame::StyledPanel);
57
58         setFocusProxy(find_work_area_);
59         replace_work_area_->setGuiView(view_);
60         replace_work_area_->init();
61         replace_work_area_->setFrameStyle(QFrame::StyledPanel);
62
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;
68
69         // align items on top
70         cbVerticalLayout->setAlignment(Qt::AlignTop);
71         pbVerticalLayout->setAlignment(Qt::AlignTop);
72 }
73
74
75 void FindAndReplaceWidget::dockLocationChanged(Qt::DockWidgetArea area)
76 {
77         if (area == Qt::RightDockWidgetArea || area == Qt::LeftDockWidgetArea) {
78                 dynamicLayoutBasic_->setDirection(QBoxLayout::TopToBottom);
79                 dynamicLayoutAdvanced_->setDirection(QBoxLayout::TopToBottom);
80         } else {
81                 dynamicLayoutBasic_->setDirection(QBoxLayout::LeftToRight);
82                 dynamicLayoutAdvanced_->setDirection(QBoxLayout::LeftToRight);
83         }
84 }
85
86
87 bool FindAndReplaceWidget::eventFilter(QObject * obj, QEvent * event)
88 {
89         updateGUI();
90         if (event->type() != QEvent::KeyPress
91                   || (obj != find_work_area_ && obj != replace_work_area_))
92                 return QWidget::eventFilter(obj, event);
93
94         QKeyEvent * e = static_cast<QKeyEvent *> (event);
95         switch (e->key()) {
96         case Qt::Key_Escape:
97                 if (e->modifiers() == Qt::NoModifier) {
98                         hideDialog();
99                         return true;
100                 }
101                 break;
102
103         case Qt::Key_Enter:
104         case Qt::Key_Return: {
105                 bool const searchback = (e->modifiers() == Qt::ShiftModifier);
106                 bool const replace = (obj == replace_work_area_);
107                 findAndReplace(searchback, replace);
108                 if (replace)
109                         replace_work_area_->setFocus();
110                 else
111                         find_work_area_->setFocus();
112                 return true;
113         }
114
115         case Qt::Key_Tab:
116                 if (e->modifiers() == Qt::NoModifier) {
117                         if (obj == find_work_area_){
118                                 LYXERR(Debug::FINDVERBOSE, "Focusing replace WA");
119                                 replace_work_area_->setFocus();
120                                 LYXERR(Debug::FINDVERBOSE, "Selecting entire replace buffer");
121                                 dispatch(FuncRequest(LFUN_BUFFER_BEGIN));
122                                 dispatch(FuncRequest(LFUN_BUFFER_END_SELECT));
123                                 return true;
124                         }
125                 }
126                 break;
127
128         case Qt::Key_Backtab:
129                 if (obj == replace_work_area_) {
130                         LYXERR(Debug::FINDVERBOSE, "Focusing find WA");
131                         find_work_area_->setFocus();
132                         LYXERR(Debug::FINDVERBOSE, "Selecting entire find buffer");
133                         dispatch(FuncRequest(LFUN_BUFFER_BEGIN));
134                         dispatch(FuncRequest(LFUN_BUFFER_END_SELECT));
135                         return true;
136                 }
137                 break;
138
139         default:
140                 break;
141         }
142         // standard event processing
143         return QWidget::eventFilter(obj, event);
144 }
145
146
147 static vector<string> const & allManualsFiles()
148 {
149         static const char * files[] = {
150                 "Intro", "UserGuide", "Tutorial", "Additional",
151                 "EmbeddedObjects", "Math", "Customization", "Shortcuts",
152                 "LFUNs", "LaTeXConfig"
153         };
154
155         static vector<string> v;
156         if (v.empty()) {
157                 FileName fname;
158                 for (size_t i = 0; i < sizeof(files) / sizeof(files[0]); ++i) {
159                         fname = i18nLibFileSearch("doc", files[i], "lyx");
160                         v.push_back(fname.absFileName());
161                 }
162         }
163
164         return v;
165 }
166
167
168 /** Switch buf to point to next document buffer.
169  **
170  ** Return true if restarted from master-document buffer.
171  **/
172 static bool nextDocumentBuffer(Buffer * & buf)
173 {
174         ListOfBuffers const children = buf->allRelatives();
175         LYXERR(Debug::FINDVERBOSE, "children.size()=" << children.size());
176         ListOfBuffers::const_iterator it =
177                 find(children.begin(), children.end(), buf);
178         LASSERT(it != children.end(), return false);
179         ++it;
180         if (it == children.end()) {
181                 buf = *children.begin();
182                 return true;
183         }
184         buf = *it;
185         return false;
186 }
187
188
189 /** Switch p_buf to point to previous document buffer.
190  **
191  ** Return true if restarted from last child buffer.
192  **/
193 static bool prevDocumentBuffer(Buffer * & buf)
194 {
195         ListOfBuffers const children = buf->allRelatives();
196         LYXERR(Debug::FINDVERBOSE, "children.size()=" << children.size());
197         ListOfBuffers::const_iterator it =
198                 find(children.begin(), children.end(), buf);
199         LASSERT(it != children.end(), return false)
200         if (it == children.begin()) {
201                 it = children.end();
202                 --it;
203                 buf = *it;
204                 return true;
205         }
206         --it;
207         buf = *it;
208         return false;
209 }
210
211
212 /** Switch buf to point to next or previous buffer in search scope.
213  **
214  ** Return true if restarted from scratch.
215  **/
216 static bool nextPrevBuffer(Buffer * & buf,
217                              FindAndReplaceOptions const & opt)
218 {
219         bool restarted = false;
220         switch (opt.scope) {
221         case FindAndReplaceOptions::S_BUFFER:
222                 restarted = true;
223                 break;
224         case FindAndReplaceOptions::S_DOCUMENT:
225                 if (opt.forward)
226                         restarted = nextDocumentBuffer(buf);
227                 else
228                         restarted = prevDocumentBuffer(buf);
229                 break;
230         case FindAndReplaceOptions::S_OPEN_BUFFERS:
231                 if (opt.forward) {
232                         buf = theBufferList().next(buf);
233                         restarted = (buf == *theBufferList().begin());
234                 } else {
235                         buf = theBufferList().previous(buf);
236                         restarted = (buf == *(theBufferList().end() - 1));
237                 }
238                 break;
239         case FindAndReplaceOptions::S_ALL_MANUALS:
240                 vector<string> const & manuals = allManualsFiles();
241                 vector<string>::const_iterator it =
242                         find(manuals.begin(), manuals.end(), buf->absFileName());
243                 if (it == manuals.end())
244                         it = manuals.begin();
245                 else if (opt.forward) {
246                         ++it;
247                         if (it == manuals.end()) {
248                                 it = manuals.begin();
249                                 restarted = true;
250                         }
251                 } else {
252                         if (it == manuals.begin()) {
253                                 it = manuals.end();
254                                 restarted = true;
255                         }
256                         --it;
257                 }
258                 FileName const & fname = FileName(*it);
259                 if (!theBufferList().exists(fname)) {
260                         guiApp->currentView()->setBusy(false);
261                         guiApp->currentView()->loadDocument(fname, false);
262                         guiApp->currentView()->setBusy(true);
263                 }
264                 buf = theBufferList().getBuffer(fname);
265                 break;
266         }
267         return restarted;
268 }
269
270
271 /** Find the finest question message to post to the user */
272 docstring getQuestionString(FindAndReplaceOptions const & opt)
273 {
274         docstring scope;
275         switch (opt.scope) {
276         case FindAndReplaceOptions::S_BUFFER:
277                 scope = _("File");
278                 break;
279         case FindAndReplaceOptions::S_DOCUMENT:
280                 scope = _("Master document");
281                 break;
282         case FindAndReplaceOptions::S_OPEN_BUFFERS:
283                 scope = _("Open files");
284                 break;
285         case FindAndReplaceOptions::S_ALL_MANUALS:
286                 scope = _("Manuals");
287                 break;
288         }
289         docstring message = opt.forward ?
290                 bformat(_("%1$s: the end was reached while searching forward.\n"
291                           "Continue searching from the beginning?"),
292                         scope) :
293                 bformat(_("%1$s: the beginning was reached while searching backward.\n"
294                           "Continue searching from the end?"),
295                         scope);
296
297         return message;
298 }
299
300
301 /// Return true if a match was found
302 bool FindAndReplaceWidget::findAndReplaceScope(FindAndReplaceOptions & opt, bool replace_all)
303 {
304         BufferView * bv = view_.documentBufferView();
305         if (!bv)
306                 return false;
307         Buffer * buf = &bv->buffer();
308         Buffer * buf_orig = &bv->buffer();
309         DocIterator cur_orig(bv->cursor());
310         int wrap_answer = -1;
311         opt.replace_all = replace_all;
312         ostringstream oss;
313         oss << opt;
314         FuncRequest cmd(LFUN_WORD_FINDADV, from_utf8(oss.str()));
315
316         view_.message(_("Advanced search in progress (press ESC to cancel) . . ."));
317         theApp()->startLongOperation();
318         view_.setBusy(true);
319         if (opt.scope == FindAndReplaceOptions::S_ALL_MANUALS) {
320                 vector<string> const & v = allManualsFiles();
321                 if (std::find(v.begin(), v.end(), buf->absFileName()) == v.end()) {
322                         FileName const & fname = FileName(*v.begin());
323                         if (!theBufferList().exists(fname)) {
324                                 guiApp->currentView()->setBusy(false);
325                                 theApp()->stopLongOperation();
326                                 guiApp->currentView()->loadDocument(fname, false);
327                                 theApp()->startLongOperation();
328                                 guiApp->currentView()->setBusy(true);
329                         }
330                         buf = theBufferList().getBuffer(fname);
331                         if (!buf) {
332                                 view_.setBusy(false);
333                                 return false;
334                         }
335
336                         lyx::dispatch(FuncRequest(LFUN_BUFFER_SWITCH,
337                                                   buf->absFileName()));
338                         bv = view_.documentBufferView();
339                         bv->cursor().clear();
340                         bv->cursor().push_back(CursorSlice(buf->inset()));
341                 }
342         }
343
344         UndoGroupHelper helper(buf);
345
346         do {
347                 LYXERR(Debug::FINDVERBOSE, "Dispatching LFUN_WORD_FINDADV");
348                 dispatch(cmd);
349                 LYXERR(Debug::FINDVERBOSE, "dispatched");
350                 if (bv->cursor().result().dispatched()) {
351                         // New match found and selected (old selection replaced if needed)
352                         if (replace_all)
353                                 continue;
354                         view_.setBusy(false);
355                         theApp()->stopLongOperation();
356                         return true;
357                 } else if (replace_all)
358                         bv->clearSelection();
359
360                 if (theApp()->longOperationCancelled()) {
361                         // Search aborted by user
362                         view_.message(_("Advanced search cancelled by user"));
363                         view_.setBusy(false);
364                         theApp()->stopLongOperation();
365                         return false;
366                 }
367
368                 // No match found in current buffer (however old selection might have been replaced)
369                 // select next buffer in scope, if any
370                 bool const prompt = nextPrevBuffer(buf, opt);
371                 if (!buf)
372                         break;
373                 if (prompt) {
374                         if (wrap_answer != -1)
375                                 break;
376                         docstring q = getQuestionString(opt);
377                         view_.setBusy(false);
378                         theApp()->stopLongOperation();
379                         wrap_answer = frontend::Alert::prompt(
380                                 _("Wrap search?"), q,
381                                 0, 1, _("&Yes"), _("&No"));
382                         theApp()->startLongOperation();
383                         view_.setBusy(true);
384                         if (wrap_answer == 1)
385                                 break;
386                 }
387                 if (buf != &view_.documentBufferView()->buffer())
388                         lyx::dispatch(FuncRequest(LFUN_BUFFER_SWITCH,
389                                                   buf->absFileName()));
390
391                 helper.resetBuffer(buf);
392
393                 bv = view_.documentBufferView();
394                 if (opt.forward) {
395                         bv->cursor().clear();
396                         bv->cursor().push_back(CursorSlice(buf->inset()));
397                 } else {
398                         //lyx::dispatch(FuncRequest(LFUN_BUFFER_END));
399                         bv->cursor().setCursor(doc_iterator_end(buf));
400                         bv->cursor().backwardPos();
401                         LYXERR(Debug::FINDVERBOSE, "findBackAdv5: cur: "
402                                 << bv->cursor());
403                 }
404                 bv->clearSelection();
405         } while (wrap_answer != 1);
406
407         if (buf_orig != &view_.documentBufferView()->buffer())
408                 lyx::dispatch(FuncRequest(LFUN_BUFFER_SWITCH,
409                                           buf_orig->absFileName()));
410         bv = view_.documentBufferView();
411         // This may happen after a replace occurred
412         if (cur_orig.pos() > cur_orig.lastpos())
413                 cur_orig.pos() = cur_orig.lastpos();
414         bv->cursor().setCursor(cur_orig);
415         view_.setBusy(false);
416         theApp()->stopLongOperation();
417         return false;
418 }
419
420
421 /// Return true if a match was found
422 bool FindAndReplaceWidget::findAndReplace(
423         bool casesensitive, bool matchword, bool backwards,
424         bool expandmacros, bool ignoreformat, bool replace,
425         bool keep_case, bool replace_all)
426 {
427         Buffer & find_buf = find_work_area_->bufferView().buffer();
428         docstring const & find_buf_name = find_buf.fileName().absoluteFilePath();
429
430         if (find_buf.text().empty()) {
431                 view_.message(_("Nothing to search"));
432                 return false;
433         }
434
435         Buffer & repl_buf = replace_work_area_->bufferView().buffer();
436         docstring const & repl_buf_name = replace ?
437                 repl_buf.fileName().absoluteFilePath() : docstring();
438
439         FindAndReplaceOptions::SearchScope scope =
440                 FindAndReplaceOptions::S_BUFFER;
441         if (CurrentDocument->isChecked())
442                 scope = FindAndReplaceOptions::S_BUFFER;
443         else if (MasterDocument->isChecked())
444                 scope = FindAndReplaceOptions::S_DOCUMENT;
445         else if (OpenDocuments->isChecked())
446                 scope = FindAndReplaceOptions::S_OPEN_BUFFERS;
447         else if (AllManualsRB->isChecked())
448                 scope = FindAndReplaceOptions::S_ALL_MANUALS;
449         else
450                 LATTEST(false);
451
452         FindAndReplaceOptions::SearchRestriction restr =
453                 OnlyMaths->isChecked()
454                         ? FindAndReplaceOptions::R_ONLY_MATHS
455                         : FindAndReplaceOptions::R_EVERYTHING;
456
457         LYXERR(Debug::FINDVERBOSE, "FindAndReplaceOptions: "
458                << "find_buf_name=" << find_buf_name
459                << ", casesensitiv=" << casesensitive
460                << ", matchword=" << matchword
461                << ", backwards=" << backwards
462                << ", expandmacros=" << expandmacros
463                << ", ignoreformat=" << ignoreformat
464                << ", repl_buf_name" << repl_buf_name
465                << ", keep_case=" << keep_case
466                << ", scope=" << scope
467                << ", restr=" << restr);
468
469         FindAndReplaceOptions opt(find_buf_name, casesensitive, matchword,
470                                   !backwards, expandmacros, ignoreformat,
471                                   repl_buf_name, keep_case, scope, restr);
472         return findAndReplaceScope(opt, replace_all);
473 }
474
475
476 bool FindAndReplaceWidget::findAndReplace(bool backwards, bool replace, bool replace_all)
477 {
478         if (! view_.currentMainWorkArea()) {
479                 view_.message(_("No open document(s) in which to search"));
480                 return false;
481         }
482         // Finalize macros that are being typed, both in main document and in search or replacement WAs
483         if (view_.currentWorkArea()->bufferView().cursor().macroModeClose())
484                 view_.currentWorkArea()->bufferView().processUpdateFlags(Update::Force);
485         if (view_.currentMainWorkArea()->bufferView().cursor().macroModeClose())
486                 view_.currentMainWorkArea()->bufferView().processUpdateFlags(Update::Force);
487
488         // FIXME: create a Dialog::returnFocus()
489         // or something instead of this:
490         view_.setCurrentWorkArea(view_.currentMainWorkArea());
491         return findAndReplace(caseCB->isChecked(),
492                 wordsCB->isChecked(),
493                 backwards,
494                 expandMacrosCB->isChecked(),
495                 ignoreFormatCB->isChecked(),
496                 replace,
497                 keepCaseCB->isChecked(),
498                 replace_all);
499 }
500
501
502 void FindAndReplaceWidget::hideDialog()
503 {
504         dispatch(FuncRequest(LFUN_DIALOG_TOGGLE, "findreplaceadv"));
505 }
506
507
508 void FindAndReplaceWidget::on_findNextPB_clicked()
509 {
510         findAndReplace(searchbackCB->isChecked(), false);
511         find_work_area_->setFocus();
512 }
513
514
515 void FindAndReplaceWidget::on_replacePB_clicked()
516 {
517         findAndReplace(searchbackCB->isChecked(), true);
518         replace_work_area_->setFocus();
519 }
520
521
522 void FindAndReplaceWidget::on_replaceallPB_clicked()
523 {
524         findAndReplace(searchbackCB->isChecked(), true, true);
525         replace_work_area_->setFocus();
526 }
527
528
529 void FindAndReplaceWidget::on_searchbackCB_clicked()
530 {
531         updateButtons();
532 }
533
534
535 // Copy selected elements from bv's BufferParams to the dest_bv's
536 static void copy_params(BufferView const & bv, BufferView & dest_bv) {
537         Buffer const & doc_buf = bv.buffer();
538         BufferParams const & doc_bp = doc_buf.params();
539         Buffer & dest_buf = dest_bv.buffer();
540         dest_buf.params().copyForAdvFR(doc_bp);
541         dest_bv.makeDocumentClass();
542         dest_bv.cursor().current_font.setLanguage(doc_bp.language);
543 }
544
545
546 void FindAndReplaceWidget::showEvent(QShowEvent * /* ev */)
547 {
548         LYXERR(Debug::DEBUG, "showEvent()" << endl);
549         BufferView * bv = view_.documentBufferView();
550         if (bv) {
551                 copy_params(*bv, find_work_area_->bufferView());
552                 copy_params(*bv, replace_work_area_->bufferView());
553         }
554
555         find_work_area_->installEventFilter(this);
556         replace_work_area_->installEventFilter(this);
557
558         view_.setCurrentWorkArea(find_work_area_);
559         LYXERR(Debug::FINDVERBOSE, "Selecting entire find buffer");
560         dispatch(FuncRequest(LFUN_BUFFER_BEGIN));
561         dispatch(FuncRequest(LFUN_BUFFER_END_SELECT));
562 }
563
564
565 void FindAndReplaceWidget::hideEvent(QHideEvent *ev)
566 {
567         replace_work_area_->removeEventFilter(this);
568         find_work_area_->removeEventFilter(this);
569         this->QWidget::hideEvent(ev);
570 }
571
572
573 bool FindAndReplaceWidget::initialiseParams(std::string const & /*params*/)
574 {
575         return true;
576 }
577
578
579 void FindAndReplace::updateView()
580 {
581         widget_->updateGUI();
582         widget_->updateButtons();
583 }
584
585
586 FindAndReplace::FindAndReplace(GuiView & parent,
587                 Qt::DockWidgetArea area, Qt::WindowFlags flags)
588         : DockView(parent, "findreplaceadv", qt_("Advanced Find and Replace"),
589                    area, flags)
590 {
591         widget_ = new FindAndReplaceWidget(parent);
592         setWidget(widget_);
593         setFocusProxy(widget_);
594 #ifdef Q_OS_MAC
595         // On Mac show and floating
596         setFloating(true);
597 #endif
598
599         connect(this, SIGNAL(dockLocationChanged(Qt::DockWidgetArea)),
600                 widget_, SLOT(dockLocationChanged(Qt::DockWidgetArea)));
601 }
602
603
604 FindAndReplace::~FindAndReplace()
605 {
606         setFocusProxy(nullptr);
607         delete widget_;
608 }
609
610
611 bool FindAndReplace::initialiseParams(std::string const & params)
612 {
613         return widget_->initialiseParams(params);
614 }
615
616
617 void FindAndReplaceWidget::updateGUI()
618 {
619         BufferView * bv = view_.documentBufferView();
620         if (bv) {
621                 if (old_buffer_ != &bv->buffer()) {
622                                 copy_params(*bv, find_work_area_->bufferView());
623                                 copy_params(*bv, replace_work_area_->bufferView());
624                                 old_buffer_ = &bv->buffer();
625                 }
626         } else
627                 old_buffer_ = nullptr;
628
629         bool const find_enabled = !find_work_area_->bufferView().buffer().empty();
630         findNextPB->setEnabled(find_enabled);
631         bool const replace_enabled = find_enabled && bv && !bv->buffer().isReadonly();
632         replaceLabel->setEnabled(replace_enabled);
633         replace_work_area_->setEnabled(replace_enabled);
634         replacePB->setEnabled(replace_enabled);
635         replaceallPB->setEnabled(replace_enabled);
636 }
637
638
639 void FindAndReplaceWidget::updateButtons()
640 {
641         if (searchbackCB->isChecked()) {
642                 findNextPB->setText(qt_("&< Find"));
643                 findNextPB->setToolTip(qt_("Find previous occurrence (Shift+Enter, forwards: Enter)"));
644                 replacePB->setText(qt_("< Rep&lace"));
645                 replacePB->setToolTip(qt_("Replace and find previous occurrence (Shift+Enter, forwards: Enter)"));
646         } else {
647                 findNextPB->setText(qt_("Find &>"));
648                 findNextPB->setToolTip(qt_("Find next occurrence (Enter, backwards: Shift+Enter)"));
649                 replacePB->setText(qt_("Rep&lace >"));
650                 replacePB->setToolTip(qt_("Replace and find next occurrence (Enter, backwards: Shift+Enter)"));
651         }
652 }
653
654
655 } // namespace frontend
656 } // namespace lyx
657
658
659 #include "moc_FindAndReplace.cpp"