]> git.lyx.org Git - features.git/blob - src/frontends/qt4/FindAndReplace.cpp
a172ee147e60186ab3ed1372dbe71e4b67ddc8ab
[features.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         DocIterator 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                 LYXERR(Debug::FIND, "dispatched");
382                 if (bv->cursor().result().dispatched()) {
383                         // Match found, selected and replaced if needed
384                         return;
385                 }
386
387                 // No match found in current buffer:
388                 // select next buffer in scope, if any
389                 bool prompt = next_prev_buffer(buf, opt);
390                 if (prompt) {
391                         if (wrap_answer != -1)
392                                 break;
393                         docstring q = question_string(opt);
394                         wrap_answer = frontend::Alert::prompt(
395                                 _("Wrap search?"), q,
396                                 0, 1, _("&Yes"), _("&No"));
397                         if (wrap_answer == 1)
398                                 break;
399                 }
400                 if (buf != &view_.documentBufferView()->buffer())
401                         lyx::dispatch(FuncRequest(LFUN_BUFFER_SWITCH,
402                                                   buf->absFileName()));
403                 bv = view_.documentBufferView();
404                 if (opt.forward) {
405                         bv->cursor().clear();
406                         bv->cursor().push_back(CursorSlice(buf->inset()));
407                 } else {
408                         //lyx::dispatch(FuncRequest(LFUN_BUFFER_END));
409                         bv->cursor().setCursor(doc_iterator_end(buf));
410                         bv->cursor().backwardPos();
411                         LYXERR(Debug::FIND, "findBackAdv5: cur: "
412                                 << bv->cursor());
413                 }
414                 bv->clearSelection();
415         } while (wrap_answer != 1);
416         if (buf_orig != &view_.documentBufferView()->buffer())
417                 lyx::dispatch(FuncRequest(LFUN_BUFFER_SWITCH,
418                                           buf_orig->absFileName()));
419         bv = view_.documentBufferView();
420         bv->cursor().setCursor(cur_orig);
421 }
422
423
424 void FindAndReplaceWidget::findAndReplace(
425         bool casesensitive, bool matchword, bool backwards,
426         bool expandmacros, bool ignoreformat, bool replace,
427         bool keep_case)
428 {
429         Buffer & buffer = find_work_area_->bufferView().buffer();
430         docstring searchString;
431         if (!ignoreformat) {
432                 searchString = buffer_to_latex(buffer);
433         } else {
434                 ParIterator it = buffer.par_iterator_begin();
435                 ParIterator end = buffer.par_iterator_end();
436                 OutputParams runparams(&buffer.params().encoding());
437                 odocstringstream os;
438                 runparams.nice = true;
439                 runparams.flavor = OutputParams::LATEX;
440                 runparams.linelen = 100000; //lyxrc.plaintext_linelen;
441                 runparams.dryrun = true;
442                 for (; it != end; ++it) {
443                         LYXERR(Debug::FIND, "Adding to search string: '"
444                                 << it->asString(false)
445                                 << "'");
446                         searchString +=
447                                 it->stringify(pos_type(0), it->size(),
448                                               AS_STR_INSETS, runparams);
449                 }
450         }
451         if (to_utf8(searchString).empty()) {
452                 buffer.message(_("Nothing to search"));
453                 return;
454         }
455         bool const regexp =
456                 to_utf8(searchString).find("\\regexp") != std::string::npos;
457         docstring replaceString;
458         if (replace) {
459                 Buffer & repl_buffer =
460                         replace_work_area_->bufferView().buffer();
461                 ostringstream oss;
462                 repl_buffer.write(oss);
463                 //buffer_to_latex(replace_buffer);
464                 replaceString = from_utf8(oss.str());
465         } else {
466                 replaceString = from_utf8(LYX_FR_NULL_STRING);
467         }
468         FindAndReplaceOptions::SearchScope scope =
469                 FindAndReplaceOptions::S_BUFFER;
470         if (CurrentDocument->isChecked())
471                 scope = FindAndReplaceOptions::S_BUFFER;
472         else if (MasterDocument->isChecked())
473                 scope = FindAndReplaceOptions::S_DOCUMENT;
474         else if (OpenDocuments->isChecked())
475                 scope = FindAndReplaceOptions::S_OPEN_BUFFERS;
476         else if (AllManualsRB->isChecked())
477                 scope = FindAndReplaceOptions::S_ALL_MANUALS;
478         else
479                 LASSERT(false, /**/);
480         LYXERR(Debug::FIND, "FindAndReplaceOptions: "
481                << "searchstring=" << searchString
482                << ", casesensitiv=" << casesensitive
483                << ", matchword=" << matchword
484                << ", backwards=" << backwards
485                << ", expandmacros=" << expandmacros
486                << ", ignoreformat=" << ignoreformat
487                << ", regexp=" << regexp
488                << ", replaceString" << replaceString
489                << ", keep_case=" << keep_case
490                << ", scope=" << scope);
491         FindAndReplaceOptions opt(searchString, casesensitive, matchword,
492                                   !backwards, expandmacros, ignoreformat,
493                                   regexp, replaceString, keep_case, scope);
494         view_.setBusy(true);
495         findAndReplaceScope(opt);
496         view_.setBusy(false);
497 }
498
499
500 void FindAndReplaceWidget::findAndReplace(bool backwards, bool replace)
501 {
502         if (! view_.currentMainWorkArea()) {
503                 view_.message(_("No open document(s) in which to search"));
504                 return;
505         }
506         // FIXME: create a Dialog::returnFocus()
507         // or something instead of this:
508         view_.setCurrentWorkArea(view_.currentMainWorkArea());
509         findAndReplace(caseCB->isChecked(),
510                 wordsCB->isChecked(),
511                 backwards,
512                 expandMacrosCB->isChecked(),
513                 ignoreFormatCB->isChecked(),
514                 replace,
515                 keepCaseCB->isChecked());
516 }
517
518
519 void FindAndReplaceWidget::insertRegexp(QAction * action)
520 {
521         string const regexp = fromqstr(action->data().toString());
522         LYXERR(Debug::FIND, "Regexp: " << regexp);
523         find_work_area_->setFocus();
524         Cursor & cur = find_work_area_->bufferView().cursor();
525         if (!cur.inRegexped())
526                 dispatch(FuncRequest(LFUN_REGEXP_MODE));
527         dispatch(FuncRequest(LFUN_SELF_INSERT, regexp));
528 }
529
530
531 void FindAndReplaceWidget::hideDialog()
532 {
533         dispatch(FuncRequest(LFUN_DIALOG_TOGGLE, "findreplaceadv"));
534 }
535
536
537 void FindAndReplaceWidget::on_findNextPB_clicked() {
538         findAndReplace(false, false);
539         find_work_area_->setFocus();
540 }
541
542
543 void FindAndReplaceWidget::on_findPrevPB_clicked() {
544         findAndReplace(true, false);
545         find_work_area_->setFocus();
546 }
547
548
549 void FindAndReplaceWidget::on_replaceNextPB_clicked()
550 {
551         findAndReplace(false, true);
552         replace_work_area_->setFocus();
553 }
554
555
556 void FindAndReplaceWidget::on_replacePrevPB_clicked()
557 {
558         findAndReplace(true, true);
559         replace_work_area_->setFocus();
560 }
561
562
563 void FindAndReplaceWidget::on_replaceallPB_clicked()
564 {
565         replace_work_area_->setFocus();
566 }
567
568
569 void FindAndReplaceWidget::showEvent(QShowEvent * /* ev */)
570 {
571         view_.setCurrentWorkArea(find_work_area_);
572         LYXERR(Debug::FIND, "Selecting entire find buffer");
573         dispatch(FuncRequest(LFUN_BUFFER_BEGIN));
574         dispatch(FuncRequest(LFUN_BUFFER_END_SELECT));
575         find_work_area_->installEventFilter(this);
576         replace_work_area_->installEventFilter(this);
577 }
578
579
580 void FindAndReplaceWidget::hideEvent(QHideEvent *ev)
581 {
582         replace_work_area_->removeEventFilter(this);
583         find_work_area_->removeEventFilter(this);
584         this->QWidget::hideEvent(ev);
585 }
586
587
588 bool FindAndReplaceWidget::initialiseParams(std::string const & /*params*/)
589 {
590         return true;
591 }
592
593
594 FindAndReplace::FindAndReplace(GuiView & parent,
595                 Qt::DockWidgetArea area, Qt::WindowFlags flags)
596         : DockView(parent, "Find LyX", qt_("Advanced Find and Replace"),
597                    area, flags)
598 {
599         widget_ = new FindAndReplaceWidget(parent);
600         setWidget(widget_);
601         setFocusProxy(widget_);
602 }
603
604
605 FindAndReplace::~FindAndReplace()
606 {
607         setFocusProxy(0);
608         delete widget_;
609 }
610
611
612 bool FindAndReplace::initialiseParams(std::string const & params)
613 {
614         return widget_->initialiseParams(params);
615 }
616
617
618 Dialog * createGuiSearchAdv(GuiView & lv)
619 {
620         FindAndReplace * gui = new FindAndReplace(lv, Qt::RightDockWidgetArea);
621 #ifdef Q_WS_MACX
622         // On Mac show and floating
623         gui->setFloating(true);
624 #endif
625         return gui;
626 }
627
628
629 } // namespace frontend
630 } // namespace lyx
631
632
633 #include "moc_FindAndReplace.cpp"