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