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