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