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