]> git.lyx.org Git - lyx.git/blob - src/frontends/qt4/FindAndReplace.cpp
b01e46d9970d900a071581416f4c314adb637595
[lyx.git] / src / frontends / qt4 / 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_funcs.h"
21 #include "BufferParams.h"
22 #include "BufferList.h"
23 #include "Cursor.h"
24 #include "FuncRequest.h"
25 #include "lyxfind.h"
26 #include "output_latex.h"
27 #include "OutputParams.h"
28 #include "TexRow.h"
29
30 #include "frontends/alert.h"
31
32 #include "support/debug.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 #include <iostream>
44
45 using namespace std;
46 using namespace lyx::support;
47
48 namespace lyx {
49 namespace frontend {
50
51
52 FindAndReplaceWidget::FindAndReplaceWidget(GuiView & view)
53         :       view_(view)
54 {
55         setupUi(this);
56 #if QT_VERSION < 0x040400
57         scrollArea->setWidget(scrollAreaWidgetContents);
58 #endif
59         find_work_area_->setGuiView(view_);
60         find_work_area_->init();
61         setFocusProxy(find_work_area_);
62         replace_work_area_->setGuiView(view_);
63         replace_work_area_->init();
64         // We don't want two cursors blinking.
65         replace_work_area_->stopBlinkingCursor();
66
67         QMenu * menu = new QMenu();
68         QAction * regAny = menu->addAction("&Anything");
69         regAny->setData(".*");
70         QAction * regAnyNonEmpty = menu->addAction("Any non-&empty");
71         regAnyNonEmpty->setData(".+");
72         QAction * regAnyWord = menu->addAction("Any &word");
73         regAnyWord->setData("[a-z]+");
74         QAction * regAnyNumber = menu->addAction("Any &number");
75         regAnyNumber->setData("[0-9]+");
76         QAction * regCustom = menu->addAction("&User-defined");
77         regCustom->setData("");
78         regexpInsertPB->setMenu(menu);
79
80         connect(menu, SIGNAL(triggered(QAction *)), this, SLOT(insertRegexp(QAction *)));
81 }
82
83
84 bool FindAndReplaceWidget::eventFilter(QObject *obj, QEvent *event)
85 {
86         if (obj == find_work_area_ && event->type() == QEvent::KeyPress) {
87                 QKeyEvent *e = static_cast<QKeyEvent *> (event);
88                 if (e->key() == Qt::Key_Escape && e->modifiers() == Qt::NoModifier) {
89                         on_closePB_clicked();
90                         return true;
91                 } else if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
92                         if (e->modifiers() == Qt::ShiftModifier) {
93                                 on_findPrevPB_clicked();
94                                 return true;
95                         } else if (e->modifiers() == Qt::NoModifier) {
96                                 on_findNextPB_clicked();
97                                 return true;
98                         }
99                 } else if (e->key() == Qt::Key_Tab && e->modifiers() == Qt::NoModifier) {
100                         LYXERR(Debug::FIND, "Focusing replace WA");
101                         replace_work_area_->setFocus();
102                         return true;
103                 }
104         }
105         if (obj == replace_work_area_ && event->type() == QEvent::KeyPress) {
106                 QKeyEvent *e = static_cast<QKeyEvent *> (event);
107                 if (e->key() == Qt::Key_Escape && e->modifiers() == Qt::NoModifier) {
108                         on_closePB_clicked();
109                         return true;
110                 } else if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
111                         if (e->modifiers() == Qt::ShiftModifier) {
112                                 on_replacePrevPB_clicked();
113                                 return true;
114                         } else if (e->modifiers() == Qt::NoModifier) {
115                                 on_replaceNextPB_clicked();
116                                 return true;
117                         }
118                 } else if (e->key() == Qt::Key_Backtab) {
119                         LYXERR(Debug::FIND, "Focusing find WA");
120                         find_work_area_->setFocus();
121                         return true;
122                 }
123         }
124         // standard event processing
125         return QWidget::eventFilter(obj, event);
126 }
127
128 static docstring buffer_to_latex(Buffer & buffer) {
129         OutputParams runparams(&buffer.params().encoding());
130         odocstringstream os;
131         runparams.nice = true;
132         runparams.flavor = OutputParams::LATEX;
133         runparams.linelen = 80; //lyxrc.plaintext_linelen;
134         // No side effect of file copying and image conversion
135         runparams.dryrun = true;
136         buffer.texrow().reset();
137         ParagraphList::const_iterator pit = buffer.paragraphs().begin();
138         ParagraphList::const_iterator const end = buffer.paragraphs().end();
139         for (; pit != end; ++pit) {
140                 TeXOnePar(buffer, buffer.text(), pit, os, buffer.texrow(), runparams);
141                 LYXERR(Debug::FIND, "searchString up to here: " << os.str());
142         }
143         return os.str();
144 }
145
146
147 static vector<string> const & allManualsFiles() {
148         static vector<string> v;
149         static const char * files[] = {
150                 "Intro", "UserGuide", "Tutorial", "Additional", "EmbeddedObjects",
151                 "Math", "Customization", "Shortcuts", "LFUNs", "LaTeXConfig"
152         };
153         if (v.empty()) {
154                 FileName fname;
155                 for (size_t i = 0; i < sizeof(files) / sizeof(files[0]); ++i) {
156                         fname = i18nLibFileSearch("doc", files[i], "lyx");
157                         v.push_back(fname.absFilename());
158                 }
159         }
160         return v;
161 }
162
163
164 /** Switch p_buf to point to next document buffer.
165  **
166  ** Return true if restarted from master-document buffer.
167  **
168  ** @note
169  ** Not using p_buf->allRelatives() here, because I'm not sure
170  ** whether or not the returned order is independent of p_buf.
171  **/
172 static bool next_document_buffer(Buffer * & p_buf) {
173         Buffer *p_master = p_buf;
174         Buffer *p_old;
175         do {
176                 p_old = p_master;
177                 p_master = const_cast<Buffer *>(p_master->masterBuffer());
178                 LYXERR(Debug::FIND, "p_old=" << p_old << ", p_master=" << p_master);
179         } while (p_master != p_old);
180         LASSERT(p_master != NULL, /**/);
181         vector<Buffer *> v_children;
182         /* Root master added as first buffer in the vector */
183         v_children.push_back(p_master);
184         p_master->getChildren(v_children, true);
185         LYXERR(Debug::FIND, "v_children.size()=" << v_children.size());
186         vector<Buffer *>::const_iterator it = find(v_children.begin(), v_children.end(), p_buf);
187         LASSERT(it != v_children.end(), /**/)
188         ++it;
189         if (it == v_children.end()) {
190                 p_buf = *v_children.begin();
191                 return true;
192         }
193         p_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  ** @note
203  ** Not using p_buf->allRelatives() here, because I'm not sure
204  ** whether or not the returned order is independent of p_buf.
205  **/
206 static bool prev_document_buffer(Buffer * & p_buf) {
207         Buffer *p_master = p_buf;
208         Buffer *p_old;
209         do {
210                 p_old = p_master;
211                 p_master = const_cast<Buffer *>(p_master->masterBuffer());
212                 LYXERR(Debug::FIND, "p_old=" << p_old << ", p_master=" << p_master);
213         } while (p_master != p_old);
214         LASSERT(p_master != NULL, /**/);
215         vector<Buffer *> v_children;
216         /* Root master added as first buffer in the vector */
217         v_children.push_back(p_master);
218         p_master->getChildren(v_children, true);
219         LYXERR(Debug::FIND, "v_children.size()=" << v_children.size());
220         vector<Buffer *>::const_iterator it = find(v_children.begin(), v_children.end(), p_buf);
221         LASSERT(it != v_children.end(), /**/)
222         if (it == v_children.begin()) {
223                 it = v_children.end();
224                 --it;
225                 p_buf = *it;
226                 return true;
227         }
228         --it;
229         p_buf = *it;
230         return false;
231 }
232
233
234 /** Switch buf to point to next or previous buffer in search scope.
235  **
236  ** Return true if restarted from scratch.
237  **/
238 static bool next_prev_buffer(Buffer * & buf, FindAndReplaceOptions const & opt) {
239         bool restarted = false;
240         switch (opt.scope) {
241         case FindAndReplaceOptions::S_BUFFER:
242                 restarted = true;
243                 break;
244         case FindAndReplaceOptions::S_DOCUMENT:
245                 if (opt.forward)
246                         restarted = next_document_buffer(buf);
247                 else
248                         restarted = prev_document_buffer(buf);
249                 break;
250         case FindAndReplaceOptions::S_OPEN_BUFFERS:
251                 if (opt.forward) {
252                         buf = theBufferList().next(buf);
253                         restarted = buf == *theBufferList().begin();
254                 } else {
255                         buf = theBufferList().previous(buf);
256                         restarted = buf == *(theBufferList().end() - 1);
257                 }
258                 break;
259         case FindAndReplaceOptions::S_ALL_MANUALS:
260                 vector<string> const & v = allManualsFiles();
261                 vector<string>::const_iterator it = find(v.begin(), v.end(), buf->absFileName());
262                 if (it == v.end()) {
263                         it = v.begin();
264                 } else if (opt.forward) {
265                         ++it;
266                         if (it == v.end()) {
267                                 it = v.begin();
268                                 restarted = true;
269                         }
270                 } else {
271                         if (it == v.begin()) {
272                                 it = v.end();
273                                 restarted = true;
274                         }
275                         --it;
276                 }
277                 FileName const & fname = FileName(*it);
278                 if (!theBufferList().exists(fname)) {
279                         guiApp->currentView()->setBusy(false);
280                         guiApp->currentView()->loadDocument(fname, false);
281                         guiApp->currentView()->setBusy(true);
282                 }
283                 buf = theBufferList().getBuffer(fname);
284                 break;
285         }
286         return restarted;
287 }
288
289
290 /** Find the finest question message to post to the user */
291 docstring question_string(FindAndReplaceOptions const & opt)
292 {
293         docstring scope;
294         switch (opt.scope) {
295         case FindAndReplaceOptions::S_BUFFER:
296                 scope = _("file[[scope]]");
297                 break;
298         case FindAndReplaceOptions::S_DOCUMENT:
299                 scope = _("master document[[scope]]");
300                 break;
301         case FindAndReplaceOptions::S_OPEN_BUFFERS:
302                 scope = _("open files[[scope]]");
303                 break;
304         case FindAndReplaceOptions::S_ALL_MANUALS:
305                 scope = _("manuals[[scope]]");
306                 break;
307         }
308         docstring message = opt.forward ?
309                 bformat(_("End of %1$s reached while searching forward.\n"
310                           "Continue searching from begin?"),
311                         scope) : 
312                 bformat(_("Beginning of %1$s reached while searching backwards.\n"
313                           "Continue searching from end?"),
314                         scope);
315
316         return message;
317 }
318
319
320 void FindAndReplaceWidget::findAndReplaceScope(FindAndReplaceOptions & opt) {
321         int wrap_answer = -1;
322         ostringstream oss;
323         oss << opt;
324         FuncRequest cmd(LFUN_WORD_FINDADV, from_utf8(oss.str()));
325         BufferView * bv = view_.documentBufferView();
326         Buffer * buf = &bv->buffer();
327
328         Buffer * buf_orig = &bv->buffer();
329         Cursor cur_orig(bv->cursor());
330
331         if (opt.scope == FindAndReplaceOptions::S_ALL_MANUALS) {
332                 vector<string> const & v = allManualsFiles();
333                 if (std::find(v.begin(), v.end(), buf->absFileName()) == v.end()) {
334                         FileName const & fname = FileName(*v.begin());
335                         if (!theBufferList().exists(fname)) {
336                                 guiApp->currentView()->setBusy(false);
337                                 guiApp->currentView()->loadDocument(fname, false);
338                                 guiApp->currentView()->setBusy(true);
339                         }
340                         buf = theBufferList().getBuffer(fname);
341                         lyx::dispatch(FuncRequest(LFUN_BUFFER_SWITCH,
342                                                   buf->absFileName()));
343                         bv = view_.documentBufferView();
344                         bv->cursor().clear();
345                         bv->cursor().push_back(CursorSlice(buf->inset()));
346                 }
347         }
348
349         do {
350                 LYXERR(Debug::FIND, "Dispatching LFUN_WORD_FINDADV");
351                 dispatch(cmd);
352                 if (bv->cursor().result().dispatched()) {
353                         // Match found, selected and replaced if needed
354                         return;
355                 }
356
357                 // No match found in current buffer: select next buffer in scope, if any
358                 bool prompt = next_prev_buffer(buf, opt);
359                 if (prompt) {
360                         if (wrap_answer != -1)
361                                 break;
362                         docstring q = question_string(opt);
363                         wrap_answer = frontend::Alert::prompt(
364                                 _("Wrap search?"), q,
365                                 0, 1, _("&Yes"), _("&No"));
366                         if (wrap_answer == 1)
367                                 break;
368                 }
369                 lyx::dispatch(FuncRequest(LFUN_BUFFER_SWITCH,
370                                           buf->absFileName()));
371                 bv = view_.documentBufferView();
372                 if (opt.forward) {
373                         bv->cursor().clear();
374                         bv->cursor().push_back(CursorSlice(buf->inset()));
375                 } else {
376                         lyx::dispatch(FuncRequest(LFUN_BUFFER_END));
377                         bv->cursor().setCursor(doc_iterator_end(buf));
378                         bv->cursor().backwardPos();
379                         LYXERR(Debug::FIND, "findBackAdv5: cur: " << bv->cursor());
380                 }
381                 bv->clearSelection();
382         } while (wrap_answer != 1);
383         if (buf != buf_orig)
384                 lyx::dispatch(FuncRequest(LFUN_BUFFER_SWITCH,
385                                           buf_orig->absFileName()));
386         bv = view_.documentBufferView();
387         bv->cursor() = cur_orig;
388 }
389
390
391 void FindAndReplaceWidget::findAndReplace(
392         bool casesensitive, bool matchword, bool backwards,
393         bool expandmacros, bool ignoreformat, bool replace,
394         bool keep_case)
395 {
396         Buffer & buffer = find_work_area_->bufferView().buffer();
397         docstring searchString;
398         if (!ignoreformat) {
399                 searchString = buffer_to_latex(buffer);
400         } else {
401                 ParIterator it = buffer.par_iterator_begin();
402                 ParIterator end = buffer.par_iterator_end();
403                 OutputParams runparams(&buffer.params().encoding());
404                 odocstringstream os;
405                 runparams.nice = true;
406                 runparams.flavor = OutputParams::LATEX;
407                 runparams.linelen = 100000; //lyxrc.plaintext_linelen;
408                 runparams.dryrun = true;
409                 for (; it != end; ++it) {
410                         LYXERR(Debug::FIND, "Adding to search string: '" << it->asString(false) << "'");
411                         searchString += it->stringify(pos_type(0), it->size(), AS_STR_INSETS, runparams);
412                 }
413         }
414         if (to_utf8(searchString).empty()) {
415                 buffer.message(_("Nothing to search"));
416                 return;
417         }
418         bool const regexp = to_utf8(searchString).find("\\regexp") != std::string::npos;
419         docstring replaceString;
420         if (replace) {
421                 Buffer & repl_buffer = replace_work_area_->bufferView().buffer();
422                 ostringstream oss;
423                 repl_buffer.write(oss);
424                 replaceString = from_utf8(oss.str()); //buffer_to_latex(replace_buffer);
425         } else {
426                 replaceString = from_utf8(LYX_FR_NULL_STRING);
427         }
428         FindAndReplaceOptions::SearchScope scope = FindAndReplaceOptions::S_BUFFER;
429         if (CurrentDocument->isChecked())
430                 scope = FindAndReplaceOptions::S_BUFFER;
431         else if (MasterDocument->isChecked())
432                 scope = FindAndReplaceOptions::S_DOCUMENT;
433         else if (OpenDocuments->isChecked())
434                 scope = FindAndReplaceOptions::S_OPEN_BUFFERS;
435         else if (AllManualsRB->isChecked())
436                 scope = FindAndReplaceOptions::S_ALL_MANUALS;
437         else
438                 LASSERT(false, /**/);
439         LYXERR(Debug::FIND, "FindAndReplaceOptions: "
440                << "searchstring=" << searchString
441                << ", casesensitiv=" << casesensitive
442                << ", matchword=" << matchword
443                << ", backwards=" << backwards
444                << ", expandmacros=" << expandmacros
445                << ", ignoreformat=" << ignoreformat
446                << ", regexp=" << regexp
447                << ", replaceString" << replaceString
448                << ", keep_case=" << keep_case
449                << ", scope=" << scope);
450         FindAndReplaceOptions opt(searchString, casesensitive, matchword, ! backwards,
451                 expandmacros, ignoreformat, regexp, replaceString, keep_case, scope);
452         view_.setBusy(true);
453         findAndReplaceScope(opt);
454         view_.setBusy(false);
455 }
456
457
458 void FindAndReplaceWidget::findAndReplace(bool backwards, bool replace)
459 {
460         if (! view_.currentMainWorkArea()) {
461                 view_.message(_("No open document(s) in which to search"));
462                 return;
463         }
464         // FIXME: create a Dialog::returnFocus() or something instead of this:
465         view_.setCurrentWorkArea(view_.currentMainWorkArea());
466         findAndReplace(caseCB->isChecked(),
467                 wordsCB->isChecked(),
468                 backwards,
469                 expandMacrosCB->isChecked(),
470                 ignoreFormatCB->isChecked(),
471                 replace,
472                 keepCaseCB->isChecked());
473 }
474
475
476 void FindAndReplaceWidget::insertRegexp(QAction * action)
477 {
478         string const regexp = fromqstr(action->data().toString());
479         LYXERR(Debug::FIND, "Regexp: " << regexp);
480         find_work_area_->setFocus();
481         Cursor & cur = find_work_area_->bufferView().cursor();
482         if (!cur.inRegexped())
483                 dispatch(FuncRequest(LFUN_REGEXP_MODE));
484         dispatch(FuncRequest(LFUN_SELF_INSERT, regexp));
485 }
486
487
488 void FindAndReplaceWidget::on_closePB_clicked()
489 {
490         dispatch(FuncRequest(LFUN_DIALOG_TOGGLE, "findreplaceadv"));
491 }
492
493
494 void FindAndReplaceWidget::on_findNextPB_clicked() {
495         findAndReplace(false, false);
496         find_work_area_->setFocus();
497 }
498
499
500 void FindAndReplaceWidget::on_findPrevPB_clicked() {
501         findAndReplace(true, false);
502         find_work_area_->setFocus();
503 }
504
505
506 void FindAndReplaceWidget::on_replaceNextPB_clicked()
507 {
508         findAndReplace(false, true);
509         replace_work_area_->setFocus();
510 }
511
512
513 void FindAndReplaceWidget::on_replacePrevPB_clicked()
514 {
515         findAndReplace(true, true);
516         replace_work_area_->setFocus();
517 }
518
519
520 void FindAndReplaceWidget::on_replaceallPB_clicked()
521 {
522         replace_work_area_->setFocus();
523 }
524
525
526 void FindAndReplaceWidget::showEvent(QShowEvent * /* ev */)
527 {
528         view_.setCurrentWorkArea(find_work_area_);
529         LYXERR(Debug::FIND, "Selecting entire find buffer");
530         dispatch(FuncRequest(LFUN_BUFFER_BEGIN));
531         dispatch(FuncRequest(LFUN_BUFFER_END_SELECT));
532         find_work_area_->installEventFilter(this);
533         replace_work_area_->installEventFilter(this);
534 }
535
536
537 void FindAndReplaceWidget::hideEvent(QHideEvent *ev)
538 {
539         replace_work_area_->removeEventFilter(this);
540         find_work_area_->removeEventFilter(this);
541         this->QWidget::hideEvent(ev);
542 }
543
544
545 bool FindAndReplaceWidget::initialiseParams(std::string const & /* params */)
546 {
547         return true;
548 }
549
550
551 FindAndReplace::FindAndReplace(GuiView & parent,
552                 Qt::DockWidgetArea area, Qt::WindowFlags flags)
553         : DockView(parent, "Find LyX", qt_("Advanced Find and Replace"), area, flags)
554 {
555         widget_ = new FindAndReplaceWidget(parent);
556         setWidget(widget_);
557         setFocusProxy(widget_);
558 }
559
560
561 FindAndReplace::~FindAndReplace()
562 {
563         setFocusProxy(0);
564         delete widget_;
565 }
566
567
568 bool FindAndReplace::initialiseParams(std::string const & params)
569 {
570         return widget_->initialiseParams(params);
571 }
572
573
574 Dialog * createGuiSearchAdv(GuiView & lv)
575 {
576         return new FindAndReplace(lv, Qt::RightDockWidgetArea);
577 }
578
579
580 } // namespace frontend
581 } // namespace lyx
582
583
584 #include "moc_FindAndReplace.cpp"