]> git.lyx.org Git - lyx.git/blob - src/frontends/qt/FindAndReplace.cpp
More enums & includes refactoring
[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                 // 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);
109
110                 if (obj == find_work_area_)
111                         on_findNextPB_clicked();
112                 else
113                         on_replacePB_clicked();
114                 // back to how it was
115                 searchbackCB->setChecked(searchback);
116                 return true;
117         }
118
119         case Qt::Key_Tab:
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));
127                                 return true;
128                         }
129                 }
130                 break;
131
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));
139                         return true;
140                 }
141                 break;
142
143         default:
144                 break;
145         }
146         // standard event processing
147         return QWidget::eventFilter(obj, event);
148 }
149
150
151 static vector<string> const & allManualsFiles()
152 {
153         static const char * files[] = {
154                 "Intro", "UserGuide", "Tutorial", "Additional",
155                 "EmbeddedObjects", "Math", "Customization", "Shortcuts",
156                 "LFUNs", "LaTeXConfig"
157         };
158
159         static vector<string> v;
160         if (v.empty()) {
161                 FileName fname;
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());
165                 }
166         }
167
168         return v;
169 }
170
171
172 /** Switch buf to point to next document buffer.
173  **
174  ** Return true if restarted from master-document buffer.
175  **/
176 static bool nextDocumentBuffer(Buffer * & buf)
177 {
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);
183         ++it;
184         if (it == children.end()) {
185                 buf = *children.begin();
186                 return true;
187         }
188         buf = *it;
189         return false;
190 }
191
192
193 /** Switch p_buf to point to previous document buffer.
194  **
195  ** Return true if restarted from last child buffer.
196  **/
197 static bool prevDocumentBuffer(Buffer * & buf)
198 {
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()) {
205                 it = children.end();
206                 --it;
207                 buf = *it;
208                 return true;
209         }
210         --it;
211         buf = *it;
212         return false;
213 }
214
215
216 /** Switch buf to point to next or previous buffer in search scope.
217  **
218  ** Return true if restarted from scratch.
219  **/
220 static bool nextPrevBuffer(Buffer * & buf,
221                              FindAndReplaceOptions const & opt)
222 {
223         bool restarted = false;
224         switch (opt.scope) {
225         case FindAndReplaceOptions::S_BUFFER:
226                 restarted = true;
227                 break;
228         case FindAndReplaceOptions::S_DOCUMENT:
229                 if (opt.forward)
230                         restarted = nextDocumentBuffer(buf);
231                 else
232                         restarted = prevDocumentBuffer(buf);
233                 break;
234         case FindAndReplaceOptions::S_OPEN_BUFFERS:
235                 if (opt.forward) {
236                         buf = theBufferList().next(buf);
237                         restarted = (buf == *theBufferList().begin());
238                 } else {
239                         buf = theBufferList().previous(buf);
240                         restarted = (buf == *(theBufferList().end() - 1));
241                 }
242                 break;
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) {
250                         ++it;
251                         if (it == manuals.end()) {
252                                 it = manuals.begin();
253                                 restarted = true;
254                         }
255                 } else {
256                         if (it == manuals.begin()) {
257                                 it = manuals.end();
258                                 restarted = true;
259                         }
260                         --it;
261                 }
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);
267                 }
268                 buf = theBufferList().getBuffer(fname);
269                 break;
270         }
271         return restarted;
272 }
273
274
275 /** Find the finest question message to post to the user */
276 docstring getQuestionString(FindAndReplaceOptions const & opt)
277 {
278         docstring scope;
279         switch (opt.scope) {
280         case FindAndReplaceOptions::S_BUFFER:
281                 scope = _("File");
282                 break;
283         case FindAndReplaceOptions::S_DOCUMENT:
284                 scope = _("Master document");
285                 break;
286         case FindAndReplaceOptions::S_OPEN_BUFFERS:
287                 scope = _("Open files");
288                 break;
289         case FindAndReplaceOptions::S_ALL_MANUALS:
290                 scope = _("Manuals");
291                 break;
292         }
293         docstring message = opt.forward ?
294                 bformat(_("%1$s: the end was reached while searching forward.\n"
295                           "Continue searching from the beginning?"),
296                         scope) :
297                 bformat(_("%1$s: the beginning was reached while searching backward.\n"
298                           "Continue searching from the end?"),
299                         scope);
300
301         return message;
302 }
303
304
305 /// Return true if a match was found
306 bool FindAndReplaceWidget::findAndReplaceScope(FindAndReplaceOptions & opt, bool replace_all)
307 {
308         BufferView * bv = view_.documentBufferView();
309         if (!bv)
310                 return false;
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;
316         ostringstream oss;
317         oss << opt;
318         FuncRequest cmd(LFUN_WORD_FINDADV, from_utf8(oss.str()));
319
320         view_.message(_("Advanced search in progress (press ESC to cancel) . . ."));
321         theApp()->startLongOperation();
322         view_.setBusy(true);
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);
333                         }
334                         buf = theBufferList().getBuffer(fname);
335                         if (!buf) {
336                                 view_.setBusy(false);
337                                 return false;
338                         }
339
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()));
345                 }
346         }
347
348         UndoGroupHelper helper(buf);
349
350         do {
351                 LYXERR(Debug::FIND, "Dispatching LFUN_WORD_FINDADV");
352                 dispatch(cmd);
353                 LYXERR(Debug::FIND, "dispatched");
354                 if (bv->cursor().result().dispatched()) {
355                         // New match found and selected (old selection replaced if needed)
356                         if (replace_all)
357                                 continue;
358                         view_.setBusy(false);
359                         theApp()->stopLongOperation();
360                         return true;
361                 } else if (replace_all)
362                         bv->clearSelection();
363
364                 if (theApp()->longOperationCancelled()) {
365                         // Search aborted by user
366                         view_.message(_("Advanced search cancelled by user"));
367                         view_.setBusy(false);
368                         theApp()->stopLongOperation();
369                         return false;
370                 }
371
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);
375                 if (!buf)
376                         break;
377                 if (prompt) {
378                         if (wrap_answer != -1)
379                                 break;
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();
387                         view_.setBusy(true);
388                         if (wrap_answer == 1)
389                                 break;
390                 }
391                 if (buf != &view_.documentBufferView()->buffer())
392                         lyx::dispatch(FuncRequest(LFUN_BUFFER_SWITCH,
393                                                   buf->absFileName()));
394
395                 helper.resetBuffer(buf);
396
397                 bv = view_.documentBufferView();
398                 if (opt.forward) {
399                         bv->cursor().clear();
400                         bv->cursor().push_back(CursorSlice(buf->inset()));
401                 } else {
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: "
406                                 << bv->cursor());
407                 }
408                 bv->clearSelection();
409         } while (wrap_answer != 1);
410
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();
421         return false;
422 }
423
424
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)
430 {
431         Buffer & find_buf = find_work_area_->bufferView().buffer();
432         docstring const & find_buf_name = find_buf.fileName().absoluteFilePath();
433
434         if (find_buf.text().empty()) {
435                 view_.message(_("Nothing to search"));
436                 return false;
437         }
438
439         Buffer & repl_buf = replace_work_area_->bufferView().buffer();
440         docstring const & repl_buf_name = replace ?
441                 repl_buf.fileName().absoluteFilePath() : docstring();
442
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;
453         else
454                 LATTEST(false);
455
456         FindAndReplaceOptions::SearchRestriction restr =
457                 OnlyMaths->isChecked()
458                         ? FindAndReplaceOptions::R_ONLY_MATHS
459                         : FindAndReplaceOptions::R_EVERYTHING;
460
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);
472
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);
477 }
478
479
480 bool FindAndReplaceWidget::findAndReplace(bool backwards, bool replace, bool replace_all)
481 {
482         if (! view_.currentMainWorkArea()) {
483                 view_.message(_("No open document(s) in which to search"));
484                 return false;
485         }
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);
491
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(),
497                 backwards,
498                 expandMacrosCB->isChecked(),
499                 ignoreFormatCB->isChecked(),
500                 replace,
501                 keepCaseCB->isChecked(),
502                 replace_all);
503 }
504
505
506 void FindAndReplaceWidget::hideDialog()
507 {
508         dispatch(FuncRequest(LFUN_DIALOG_TOGGLE, "findreplaceadv"));
509 }
510
511
512 void FindAndReplaceWidget::on_findNextPB_clicked()
513 {
514         findAndReplace(searchbackCB->isChecked(), false);
515         find_work_area_->setFocus();
516 }
517
518
519 void FindAndReplaceWidget::on_replacePB_clicked()
520 {
521         findAndReplace(searchbackCB->isChecked(), true);
522         replace_work_area_->setFocus();
523 }
524
525
526 void FindAndReplaceWidget::on_replaceallPB_clicked()
527 {
528         findAndReplace(searchbackCB->isChecked(), true, true);
529         replace_work_area_->setFocus();
530 }
531
532
533 // Copy selected elements from bv's BufferParams to the dest_bv's
534 static void copy_params(BufferView const & bv, BufferView & dest_bv) {
535         Buffer const & doc_buf = bv.buffer();
536         BufferParams const & doc_bp = doc_buf.params();
537         Buffer & dest_buf = dest_bv.buffer();
538         dest_buf.params().copyForAdvFR(doc_bp);
539         dest_bv.makeDocumentClass();
540         dest_bv.cursor().current_font.setLanguage(doc_bp.language);
541 }
542
543
544 void FindAndReplaceWidget::showEvent(QShowEvent * /* ev */)
545 {
546         LYXERR(Debug::DEBUG, "showEvent()" << endl);
547         BufferView * bv = view_.documentBufferView();
548         if (bv) {
549                 copy_params(*bv, find_work_area_->bufferView());
550                 copy_params(*bv, replace_work_area_->bufferView());
551         }
552
553         find_work_area_->installEventFilter(this);
554         replace_work_area_->installEventFilter(this);
555
556         view_.setCurrentWorkArea(find_work_area_);
557         LYXERR(Debug::FIND, "Selecting entire find buffer");
558         dispatch(FuncRequest(LFUN_BUFFER_BEGIN));
559         dispatch(FuncRequest(LFUN_BUFFER_END_SELECT));
560 }
561
562
563 void FindAndReplaceWidget::hideEvent(QHideEvent *ev)
564 {
565         replace_work_area_->removeEventFilter(this);
566         find_work_area_->removeEventFilter(this);
567         this->QWidget::hideEvent(ev);
568 }
569
570
571 bool FindAndReplaceWidget::initialiseParams(std::string const & /*params*/)
572 {
573         return true;
574 }
575
576
577 void FindAndReplace::updateView()
578 {
579         widget_->updateGUI();
580 }
581
582
583 FindAndReplace::FindAndReplace(GuiView & parent,
584                 Qt::DockWidgetArea area, Qt::WindowFlags flags)
585         : DockView(parent, "findreplaceadv", qt_("Advanced Find and Replace"),
586                    area, flags)
587 {
588         widget_ = new FindAndReplaceWidget(parent);
589         setWidget(widget_);
590         setFocusProxy(widget_);
591
592         connect(this, SIGNAL(dockLocationChanged(Qt::DockWidgetArea)),
593                 widget_, SLOT(dockLocationChanged(Qt::DockWidgetArea)));
594 }
595
596
597 FindAndReplace::~FindAndReplace()
598 {
599         setFocusProxy(nullptr);
600         delete widget_;
601 }
602
603
604 bool FindAndReplace::initialiseParams(std::string const & params)
605 {
606         return widget_->initialiseParams(params);
607 }
608
609
610 void FindAndReplaceWidget::updateGUI()
611 {
612         BufferView * bv = view_.documentBufferView();
613         if (bv) {
614                 if (old_buffer_ != &bv->buffer()) {
615                                 copy_params(*bv, find_work_area_->bufferView());
616                                 copy_params(*bv, replace_work_area_->bufferView());
617                                 old_buffer_ = &bv->buffer();
618                 }
619         } else
620                 old_buffer_ = nullptr;
621
622         bool const find_enabled = !find_work_area_->bufferView().buffer().empty();
623         findNextPB->setEnabled(find_enabled);
624         bool const replace_enabled = find_enabled && bv && !bv->buffer().isReadonly();
625         replaceLabel->setEnabled(replace_enabled);
626         replace_work_area_->setEnabled(replace_enabled);
627         replacePB->setEnabled(replace_enabled);
628         replaceallPB->setEnabled(replace_enabled);
629 }
630
631
632 } // namespace frontend
633 } // namespace lyx
634
635
636 #include "moc_FindAndReplace.cpp"