]> git.lyx.org Git - features.git/blob - src/frontends/qt4/FindAndReplace.cpp
85c2ddea867101fdd9597a1e270ae5fa8910e36d
[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         find_work_area_->setGuiView(view_);
57         find_work_area_->init();
58         find_work_area_->setFrameStyle(QFrame::StyledPanel);
59         setFocusProxy(find_work_area_);
60         replace_work_area_->setGuiView(view_);
61         replace_work_area_->init();
62         replace_work_area_->setFrameStyle(QFrame::StyledPanel);
63         // We don't want two cursors blinking.
64         replace_work_area_->stopBlinkingCursor();
65         QMenu * menu = new QMenu();
66         QAction * regAny = menu->addAction(qt_("&Anything"));
67         regAny->setData(".*");
68         QAction * regAnyNonEmpty = menu->addAction(qt_("Any non-&empty"));
69         regAnyNonEmpty->setData(".+");
70         QAction * regAnyWord = menu->addAction(qt_("Any &word"));
71         regAnyWord->setData("[a-z]+");
72         QAction * regAnyNumber = menu->addAction(qt_("Any &number"));
73         regAnyNumber->setData("[0-9]+");
74         QAction * regCustom = menu->addAction(qt_("&User-defined"));
75         regCustom->setData("");
76         regexpInsertPB->setMenu(menu);
77
78         connect(menu, SIGNAL(triggered(QAction *)),
79                 this, SLOT(insertRegexp(QAction *)));
80 }
81
82
83 bool FindAndReplaceWidget::eventFilter(QObject * obj, QEvent * event)
84 {
85         if (event->type() != QEvent::KeyPress
86                   || (obj != find_work_area_ && obj != replace_work_area_))
87                 return QWidget::eventFilter(obj, event);
88
89         QKeyEvent * e = static_cast<QKeyEvent *> (event);
90         switch (e->key()) {
91         case Qt::Key_Escape:
92                 if (e->modifiers() == Qt::NoModifier) {
93                         hideDialog();
94                         return true;
95                 }
96                 break;
97
98         case Qt::Key_Enter:
99         case Qt::Key_Return:
100                 if (e->modifiers() == Qt::ShiftModifier) {
101                         if (obj == find_work_area_)
102                                 on_findPrevPB_clicked();
103                         else
104                                 on_replacePrevPB_clicked();
105                         return true;
106                 } else if (e->modifiers() == Qt::NoModifier) {
107                         if (obj == find_work_area_)
108                                 on_findNextPB_clicked();
109                         else
110                                 on_replaceNextPB_clicked();
111                         return true;
112                 }
113                 break;
114
115         case Qt::Key_Tab:
116                 if (e->modifiers() == Qt::NoModifier) {
117                         if (obj == find_work_area_){
118                                 LYXERR(Debug::FIND, "Focusing replace WA");
119                                 replace_work_area_->setFocus();
120                                 return true;
121                         }
122                 }
123                 break;
124
125         case Qt::Key_Backtab:
126                 if (obj == replace_work_area_) {
127                         LYXERR(Debug::FIND, "Focusing find WA");
128                         find_work_area_->setFocus();
129                         return true;
130                 }
131                 break;
132
133         default:
134                 break;
135         }
136         // standard event processing
137         return QWidget::eventFilter(obj, event);
138 }
139
140
141 static docstring buffer_to_latex(Buffer & buffer) {
142         OutputParams runparams(&buffer.params().encoding());
143         odocstringstream os;
144         runparams.nice = true;
145         runparams.flavor = OutputParams::LATEX;
146         runparams.linelen = 80; //lyxrc.plaintext_linelen;
147         // No side effect of file copying and image conversion
148         runparams.dryrun = true;
149         buffer.texrow().reset();
150         ParagraphList::const_iterator pit = buffer.paragraphs().begin();
151         ParagraphList::const_iterator const end = buffer.paragraphs().end();
152         for (; pit != end; ++pit) {
153                 TeXOnePar(buffer, buffer.text(),
154                           pit, os, buffer.texrow(), runparams);
155                 LYXERR(Debug::FIND, "searchString up to here: "
156                         << os.str());
157         }
158         return os.str();
159 }
160
161
162 static vector<string> const & allManualsFiles() {
163         static vector<string> v;
164         static const char * files[] = {
165                 "Intro", "UserGuide", "Tutorial", "Additional",
166                 "EmbeddedObjects", "Math", "Customization", "Shortcuts",
167                 "LFUNs", "LaTeXConfig"
168         };
169         if (v.empty()) {
170                 FileName fname;
171                 for (size_t i = 0; i < sizeof(files) / sizeof(files[0]); ++i) {
172                         fname = i18nLibFileSearch("doc", files[i], "lyx");
173                         v.push_back(fname.absFilename());
174                 }
175         }
176         return v;
177 }
178
179
180 /** Switch p_buf to point to next document buffer.
181  **
182  ** Return true if restarted from master-document buffer.
183  **
184  ** @note
185  ** Not using p_buf->allRelatives() here, because I'm not sure
186  ** whether or not the returned order is independent of p_buf.
187  **/
188 static bool next_document_buffer(Buffer * & p_buf) {
189         Buffer *p_master = p_buf;
190         Buffer *p_old;
191         do {
192                 p_old = p_master;
193                 p_master = const_cast<Buffer *>(p_master->masterBuffer());
194                 LYXERR(Debug::FIND, "p_old="
195                         << p_old 
196                         << ", p_master=" 
197                         << p_master);
198         } while (p_master != p_old);
199         LASSERT(p_master != NULL, /**/);
200         vector<Buffer *> v_children;
201         /* Root master added as first buffer in the vector */
202         v_children.push_back(p_master);
203         p_master->getChildren(v_children, true);
204         LYXERR(Debug::FIND, "v_children.size()=" << v_children.size());
205         vector<Buffer *>::const_iterator it =
206                 find(v_children.begin(), v_children.end(), p_buf);
207         LASSERT(it != v_children.end(), /**/)
208         ++it;
209         if (it == v_children.end()) {
210                 p_buf = *v_children.begin();
211                 return true;
212         }
213         p_buf = *it;
214         return false;
215 }
216
217
218 /** Switch p_buf to point to previous document buffer.
219  **
220  ** Return true if restarted from last child buffer.
221  **
222  ** @note
223  ** Not using p_buf->allRelatives() here, because I'm not sure
224  ** whether or not the returned order is independent of p_buf.
225  **/
226 static bool prev_document_buffer(Buffer * & p_buf) {
227         Buffer *p_master = p_buf;
228         Buffer *p_old;
229         do {
230                 p_old = p_master;
231                 p_master = const_cast<Buffer *>(p_master->masterBuffer());
232                 LYXERR(Debug::FIND, 
233                         "p_old=" << p_old 
234                         << ", p_master=" << p_master);
235         } while (p_master != p_old);
236         LASSERT(p_master != NULL, /**/);
237         vector<Buffer *> v_children;
238         /* Root master added as first buffer in the vector */
239         v_children.push_back(p_master);
240         p_master->getChildren(v_children, true);
241         LYXERR(Debug::FIND, "v_children.size()=" << v_children.size());
242         vector<Buffer *>::const_iterator it =
243                 find(v_children.begin(), v_children.end(), p_buf);
244         LASSERT(it != v_children.end(), /**/)
245         if (it == v_children.begin()) {
246                 it = v_children.end();
247                 --it;
248                 p_buf = *it;
249                 return true;
250         }
251         --it;
252         p_buf = *it;
253         return false;
254 }
255
256
257 /** Switch buf to point to next or previous buffer in search scope.
258  **
259  ** Return true if restarted from scratch.
260  **/
261 static bool next_prev_buffer(Buffer * & buf,
262                              FindAndReplaceOptions const & opt)
263 {
264         bool restarted = false;
265         switch (opt.scope) {
266         case FindAndReplaceOptions::S_BUFFER:
267                 restarted = true;
268                 break;
269         case FindAndReplaceOptions::S_DOCUMENT:
270                 if (opt.forward)
271                         restarted = next_document_buffer(buf);
272                 else
273                         restarted = prev_document_buffer(buf);
274                 break;
275         case FindAndReplaceOptions::S_OPEN_BUFFERS:
276                 if (opt.forward) {
277                         buf = theBufferList().next(buf);
278                         restarted = buf == *theBufferList().begin();
279                 } else {
280                         buf = theBufferList().previous(buf);
281                         restarted = buf == *(theBufferList().end() - 1);
282                 }
283                 break;
284         case FindAndReplaceOptions::S_ALL_MANUALS:
285                 vector<string> const & v = allManualsFiles();
286                 vector<string>::const_iterator it =
287                         find(v.begin(), v.end(), buf->absFileName());
288                 if (it == v.end()) {
289                         it = v.begin();
290                 } else if (opt.forward) {
291                         ++it;
292                         if (it == v.end()) {
293                                 it = v.begin();
294                                 restarted = true;
295                         }
296                 } else {
297                         if (it == v.begin()) {
298                                 it = v.end();
299                                 restarted = true;
300                         }
301                         --it;
302                 }
303                 FileName const & fname = FileName(*it);
304                 if (!theBufferList().exists(fname)) {
305                         guiApp->currentView()->setBusy(false);
306                         guiApp->currentView()->loadDocument(fname, false);
307                         guiApp->currentView()->setBusy(true);
308                 }
309                 buf = theBufferList().getBuffer(fname);
310                 break;
311         }
312         return restarted;
313 }
314
315
316 /** Find the finest question message to post to the user */
317 docstring question_string(FindAndReplaceOptions const & opt)
318 {
319         docstring scope;
320         switch (opt.scope) {
321         case FindAndReplaceOptions::S_BUFFER:
322                 scope = _("file[[scope]]");
323                 break;
324         case FindAndReplaceOptions::S_DOCUMENT:
325                 scope = _("master document[[scope]]");
326                 break;
327         case FindAndReplaceOptions::S_OPEN_BUFFERS:
328                 scope = _("open files[[scope]]");
329                 break;
330         case FindAndReplaceOptions::S_ALL_MANUALS:
331                 scope = _("manuals[[scope]]");
332                 break;
333         }
334         docstring message = opt.forward ?
335                 bformat(_("End of %1$s reached while searching forward.\n"
336                           "Continue searching from the beginning?"),
337                         scope) : 
338                 bformat(_("Beginning of %1$s reached while searching backward.\n"
339                           "Continue searching from the end?"),
340                         scope);
341
342         return message;
343 }
344
345
346 void FindAndReplaceWidget::findAndReplaceScope(FindAndReplaceOptions & opt)
347 {
348         int wrap_answer = -1;
349         ostringstream oss;
350         oss << opt;
351         FuncRequest cmd(LFUN_WORD_FINDADV, from_utf8(oss.str()));
352         BufferView * bv = view_.documentBufferView();
353         Buffer * buf = &bv->buffer();
354
355         Buffer * buf_orig = &bv->buffer();
356         DocIterator cur_orig(bv->cursor());
357
358         if (opt.scope == FindAndReplaceOptions::S_ALL_MANUALS) {
359                 vector<string> const & v = allManualsFiles();
360                 if (std::find(v.begin(), v.end(), buf->absFileName()) == v.end()) {
361                         FileName const & fname = FileName(*v.begin());
362                         if (!theBufferList().exists(fname)) {
363                                 guiApp->currentView()->setBusy(false);
364                                 guiApp->currentView()->loadDocument(fname, false);
365                                 guiApp->currentView()->setBusy(true);
366                         }
367                         buf = theBufferList().getBuffer(fname);
368                         lyx::dispatch(FuncRequest(LFUN_BUFFER_SWITCH,
369                                                   buf->absFileName()));
370                         bv = view_.documentBufferView();
371                         bv->cursor().clear();
372                         bv->cursor().push_back(CursorSlice(buf->inset()));
373                 }
374         }
375
376         do {
377                 LYXERR(Debug::FIND, "Dispatching LFUN_WORD_FINDADV");
378                 dispatch(cmd);
379                 LYXERR(Debug::FIND, "dispatched");
380                 if (bv->cursor().result().dispatched()) {
381                         // Match found, selected and replaced if needed
382                         return;
383                 }
384
385                 // No match found in current buffer:
386                 // select next buffer in scope, if any
387                 bool prompt = next_prev_buffer(buf, opt);
388                 if (prompt) {
389                         if (wrap_answer != -1)
390                                 break;
391                         docstring q = question_string(opt);
392                         wrap_answer = frontend::Alert::prompt(
393                                 _("Wrap search?"), q,
394                                 0, 1, _("&Yes"), _("&No"));
395                         if (wrap_answer == 1)
396                                 break;
397                 }
398                 if (buf != &view_.documentBufferView()->buffer())
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_orig != &view_.documentBufferView()->buffer())
415                 lyx::dispatch(FuncRequest(LFUN_BUFFER_SWITCH,
416                                           buf_orig->absFileName()));
417         bv = view_.documentBufferView();
418         bv->cursor().setCursor(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         FindAndReplace * gui = new FindAndReplace(lv, Qt::RightDockWidgetArea);
619 #ifdef Q_WS_MACX
620         // On Mac show and floating
621         gui->setFloating(true);
622 #endif
623         return gui;
624 }
625
626
627 } // namespace frontend
628 } // namespace lyx
629
630
631 #include "moc_FindAndReplace.cpp"