]> git.lyx.org Git - lyx.git/blob - src/frontends/qt4/FindAndReplace.cpp
QDialogButtonBox for the remaining dialogs.
[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.h"
21 #include "BufferList.h"
22 #include "BufferParams.h"
23 #include "BufferView.h"
24 #include "Cursor.h"
25 #include "FuncRequest.h"
26 #include "Language.h"
27 #include "Lexer.h"
28 #include "LyX.h"
29 #include "lyxfind.h"
30 #include "Text.h"
31 #include "TextClass.h"
32
33 #include "frontends/alert.h"
34
35 #include "support/debug.h"
36 #include "support/docstream.h"
37 #include "support/filetools.h"
38 #include "support/FileName.h"
39 #include "support/gettext.h"
40 #include "support/lassert.h"
41 #include "support/lstrings.h"
42
43 #include <QCloseEvent>
44 #include <QLineEdit>
45 #include <QMenu>
46
47 using namespace std;
48 using namespace lyx::support;
49
50 namespace lyx {
51 namespace frontend {
52
53
54 FindAndReplaceWidget::FindAndReplaceWidget(GuiView & view)
55         : QTabWidget(&view), view_(view)
56 {
57         setupUi(this);
58         find_work_area_->setGuiView(view_);
59         find_work_area_->init();
60         find_work_area_->setFrameStyle(QFrame::StyledPanel);
61
62         setFocusProxy(find_work_area_);
63         replace_work_area_->setGuiView(view_);
64         replace_work_area_->init();
65         replace_work_area_->setFrameStyle(QFrame::StyledPanel);
66
67         // We don't want two cursors blinking.
68         find_work_area_->stopBlinkingCaret();
69         replace_work_area_->stopBlinkingCaret();
70 }
71
72
73 void FindAndReplaceWidget::dockLocationChanged(Qt::DockWidgetArea area)
74 {
75         if (area == Qt::RightDockWidgetArea || area == Qt::LeftDockWidgetArea) {
76                 dynamicLayoutBasic_->setDirection(QBoxLayout::TopToBottom);
77                 dynamicLayoutAdvanced_->setDirection(QBoxLayout::TopToBottom);
78         } else {
79                 dynamicLayoutBasic_->setDirection(QBoxLayout::LeftToRight);
80                 dynamicLayoutAdvanced_->setDirection(QBoxLayout::LeftToRight);
81         }
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                 // with shift we (temporarily) change search/replace direction
103                 bool const searchback = searchbackCB->isChecked();
104                 if (e->modifiers() == Qt::ShiftModifier && !searchback)
105                         searchbackCB->setChecked(!searchback);
106
107                 if (obj == find_work_area_)
108                         on_findNextPB_clicked();
109                 else
110                         on_replacePB_clicked();
111                 // back to how it was
112                 searchbackCB->setChecked(searchback);
113                 return true;
114         }
115
116         case Qt::Key_Tab:
117                 if (e->modifiers() == Qt::NoModifier) {
118                         if (obj == find_work_area_){
119                                 LYXERR(Debug::FIND, "Focusing replace WA");
120                                 replace_work_area_->setFocus();
121                                 LYXERR(Debug::FIND, "Selecting entire replace buffer");
122                                 dispatch(FuncRequest(LFUN_BUFFER_BEGIN));
123                                 dispatch(FuncRequest(LFUN_BUFFER_END_SELECT));
124                                 return true;
125                         }
126                 }
127                 break;
128
129         case Qt::Key_Backtab:
130                 if (obj == replace_work_area_) {
131                         LYXERR(Debug::FIND, "Focusing find WA");
132                         find_work_area_->setFocus();
133                         LYXERR(Debug::FIND, "Selecting entire find buffer");
134                         dispatch(FuncRequest(LFUN_BUFFER_BEGIN));
135                         dispatch(FuncRequest(LFUN_BUFFER_END_SELECT));
136                         return true;
137                 }
138                 break;
139
140         default:
141                 break;
142         }
143         // standard event processing
144         return QWidget::eventFilter(obj, event);
145 }
146
147
148 static vector<string> const & allManualsFiles()
149 {
150         static const char * files[] = {
151                 "Intro", "UserGuide", "Tutorial", "Additional",
152                 "EmbeddedObjects", "Math", "Customization", "Shortcuts",
153                 "LFUNs", "LaTeXConfig"
154         };
155
156         static vector<string> v;
157         if (v.empty()) {
158                 FileName fname;
159                 for (size_t i = 0; i < sizeof(files) / sizeof(files[0]); ++i) {
160                         fname = i18nLibFileSearch("doc", files[i], "lyx");
161                         v.push_back(fname.absFileName());
162                 }
163         }
164
165         return v;
166 }
167
168
169 /** Switch buf to point to next document buffer.
170  **
171  ** Return true if restarted from master-document buffer.
172  **/
173 static bool nextDocumentBuffer(Buffer * & buf)
174 {
175         ListOfBuffers const children = buf->allRelatives();
176         LYXERR(Debug::FIND, "children.size()=" << children.size());
177         ListOfBuffers::const_iterator it =
178                 find(children.begin(), children.end(), buf);
179         LASSERT(it != children.end(), return false);
180         ++it;
181         if (it == children.end()) {
182                 buf = *children.begin();
183                 return true;
184         }
185         buf = *it;
186         return false;
187 }
188
189
190 /** Switch p_buf to point to previous document buffer.
191  **
192  ** Return true if restarted from last child buffer.
193  **/
194 static bool prevDocumentBuffer(Buffer * & buf)
195 {
196         ListOfBuffers const children = buf->allRelatives();
197         LYXERR(Debug::FIND, "children.size()=" << children.size());
198         ListOfBuffers::const_iterator it =
199                 find(children.begin(), children.end(), buf);
200         LASSERT(it != children.end(), return false)
201         if (it == children.begin()) {
202                 it = children.end();
203                 --it;
204                 buf = *it;
205                 return true;
206         }
207         --it;
208         buf = *it;
209         return false;
210 }
211
212
213 /** Switch buf to point to next or previous buffer in search scope.
214  **
215  ** Return true if restarted from scratch.
216  **/
217 static bool nextPrevBuffer(Buffer * & buf,
218                              FindAndReplaceOptions const & opt)
219 {
220         bool restarted = false;
221         switch (opt.scope) {
222         case FindAndReplaceOptions::S_BUFFER:
223                 restarted = true;
224                 break;
225         case FindAndReplaceOptions::S_DOCUMENT:
226                 if (opt.forward)
227                         restarted = nextDocumentBuffer(buf);
228                 else
229                         restarted = prevDocumentBuffer(buf);
230                 break;
231         case FindAndReplaceOptions::S_OPEN_BUFFERS:
232                 if (opt.forward) {
233                         buf = theBufferList().next(buf);
234                         restarted = (buf == *theBufferList().begin());
235                 } else {
236                         buf = theBufferList().previous(buf);
237                         restarted = (buf == *(theBufferList().end() - 1));
238                 }
239                 break;
240         case FindAndReplaceOptions::S_ALL_MANUALS:
241                 vector<string> const & manuals = allManualsFiles();
242                 vector<string>::const_iterator it =
243                         find(manuals.begin(), manuals.end(), buf->absFileName());
244                 if (it == manuals.end())
245                         it = manuals.begin();
246                 else if (opt.forward) {
247                         ++it;
248                         if (it == manuals.end()) {
249                                 it = manuals.begin();
250                                 restarted = true;
251                         }
252                 } else {
253                         if (it == manuals.begin()) {
254                                 it = manuals.end();
255                                 restarted = true;
256                         }
257                         --it;
258                 }
259                 FileName const & fname = FileName(*it);
260                 if (!theBufferList().exists(fname)) {
261                         guiApp->currentView()->setBusy(false);
262                         guiApp->currentView()->loadDocument(fname, false);
263                         guiApp->currentView()->setBusy(true);
264                 }
265                 buf = theBufferList().getBuffer(fname);
266                 break;
267         }
268         return restarted;
269 }
270
271
272 /** Find the finest question message to post to the user */
273 docstring getQuestionString(FindAndReplaceOptions const & opt)
274 {
275         docstring scope;
276         switch (opt.scope) {
277         case FindAndReplaceOptions::S_BUFFER:
278                 scope = _("File");
279                 break;
280         case FindAndReplaceOptions::S_DOCUMENT:
281                 scope = _("Master document");
282                 break;
283         case FindAndReplaceOptions::S_OPEN_BUFFERS:
284                 scope = _("Open files");
285                 break;
286         case FindAndReplaceOptions::S_ALL_MANUALS:
287                 scope = _("Manuals");
288                 break;
289         }
290         docstring message = opt.forward ?
291                 bformat(_("%1$s: the end was reached while searching forward.\n"
292                           "Continue searching from the beginning?"),
293                         scope) :
294                 bformat(_("%1$s: the beginning was reached while searching backward.\n"
295                           "Continue searching from the end?"),
296                         scope);
297
298         return message;
299 }
300
301
302 /// Return true if a match was found
303 bool FindAndReplaceWidget::findAndReplaceScope(FindAndReplaceOptions & opt, bool replace_all)
304 {
305         BufferView * bv = view_.documentBufferView();
306         if (!bv)
307                 return false;
308         Buffer * buf = &bv->buffer();
309         Buffer * buf_orig = &bv->buffer();
310         DocIterator cur_orig(bv->cursor());
311         int wrap_answer = -1;
312         ostringstream oss;
313         oss << opt;
314         FuncRequest cmd(LFUN_WORD_FINDADV, from_utf8(oss.str()));
315
316         view_.message(_("Advanced search in progress (press ESC to cancel) . . ."));
317         theApp()->startLongOperation();
318         view_.setBusy(true);
319         if (opt.scope == FindAndReplaceOptions::S_ALL_MANUALS) {
320                 vector<string> const & v = allManualsFiles();
321                 if (std::find(v.begin(), v.end(), buf->absFileName()) == v.end()) {
322                         FileName const & fname = FileName(*v.begin());
323                         if (!theBufferList().exists(fname)) {
324                                 guiApp->currentView()->setBusy(false);
325                                 theApp()->stopLongOperation();
326                                 guiApp->currentView()->loadDocument(fname, false);
327                                 theApp()->startLongOperation();
328                                 guiApp->currentView()->setBusy(true);
329                         }
330                         buf = theBufferList().getBuffer(fname);
331                         if (!buf) {
332                                 view_.setBusy(false);
333                                 return false;
334                         }
335
336                         lyx::dispatch(FuncRequest(LFUN_BUFFER_SWITCH,
337                                                   buf->absFileName()));
338                         bv = view_.documentBufferView();
339                         bv->cursor().clear();
340                         bv->cursor().push_back(CursorSlice(buf->inset()));
341                 }
342         }
343
344         UndoGroupHelper helper(buf);
345
346         do {
347                 LYXERR(Debug::FIND, "Dispatching LFUN_WORD_FINDADV");
348                 dispatch(cmd);
349                 LYXERR(Debug::FIND, "dispatched");
350                 if (bv->cursor().result().dispatched()) {
351                         // New match found and selected (old selection replaced if needed)
352                         if (replace_all)
353                                 continue;
354                         view_.setBusy(false);
355                         theApp()->stopLongOperation();
356                         return true;
357                 } else if (replace_all)
358                         bv->clearSelection();
359
360                 if (theApp()->longOperationCancelled()) {
361                         // Search aborted by user
362                         view_.message(_("Advanced search cancelled by user"));
363                         view_.setBusy(false);
364                         theApp()->stopLongOperation();
365                         return false;
366                 }
367
368                 // No match found in current buffer (however old selection might have been replaced)
369                 // select next buffer in scope, if any
370                 bool const prompt = nextPrevBuffer(buf, opt);
371                 if (!buf)
372                         break;
373                 if (prompt) {
374                         if (wrap_answer != -1)
375                                 break;
376                         docstring q = getQuestionString(opt);
377                         view_.setBusy(false);
378                         theApp()->stopLongOperation();
379                         wrap_answer = frontend::Alert::prompt(
380                                 _("Wrap search?"), q,
381                                 0, 1, _("&Yes"), _("&No"));
382                         theApp()->startLongOperation();
383                         view_.setBusy(true);
384                         if (wrap_answer == 1)
385                                 break;
386                 }
387                 if (buf != &view_.documentBufferView()->buffer())
388                         lyx::dispatch(FuncRequest(LFUN_BUFFER_SWITCH,
389                                                   buf->absFileName()));
390
391                 helper.resetBuffer(buf);
392
393                 bv = view_.documentBufferView();
394                 if (opt.forward) {
395                         bv->cursor().clear();
396                         bv->cursor().push_back(CursorSlice(buf->inset()));
397                 } else {
398                         //lyx::dispatch(FuncRequest(LFUN_BUFFER_END));
399                         bv->cursor().setCursor(doc_iterator_end(buf));
400                         bv->cursor().backwardPos();
401                         LYXERR(Debug::FIND, "findBackAdv5: cur: "
402                                 << bv->cursor());
403                 }
404                 bv->clearSelection();
405         } while (wrap_answer != 1);
406
407         if (buf_orig != &view_.documentBufferView()->buffer())
408                 lyx::dispatch(FuncRequest(LFUN_BUFFER_SWITCH,
409                                           buf_orig->absFileName()));
410         bv = view_.documentBufferView();
411         // This may happen after a replace occurred
412         if (cur_orig.pos() > cur_orig.lastpos())
413                 cur_orig.pos() = cur_orig.lastpos();
414         bv->cursor().setCursor(cur_orig);
415         view_.setBusy(false);
416         theApp()->stopLongOperation();
417         return false;
418 }
419
420
421 /// Return true if a match was found
422 bool FindAndReplaceWidget::findAndReplace(
423         bool casesensitive, bool matchword, bool backwards,
424         bool expandmacros, bool ignoreformat, bool replace,
425         bool keep_case, bool replace_all)
426 {
427         Buffer & find_buf = find_work_area_->bufferView().buffer();
428         docstring const & find_buf_name = find_buf.fileName().absoluteFilePath();
429
430         if (find_buf.text().empty()) {
431                 view_.message(_("Nothing to search"));
432                 return false;
433         }
434
435         Buffer & repl_buf = replace_work_area_->bufferView().buffer();
436         docstring const & repl_buf_name = replace ?
437                 repl_buf.fileName().absoluteFilePath() : docstring();
438
439         FindAndReplaceOptions::SearchScope scope =
440                 FindAndReplaceOptions::S_BUFFER;
441         if (CurrentDocument->isChecked())
442                 scope = FindAndReplaceOptions::S_BUFFER;
443         else if (MasterDocument->isChecked())
444                 scope = FindAndReplaceOptions::S_DOCUMENT;
445         else if (OpenDocuments->isChecked())
446                 scope = FindAndReplaceOptions::S_OPEN_BUFFERS;
447         else if (AllManualsRB->isChecked())
448                 scope = FindAndReplaceOptions::S_ALL_MANUALS;
449         else
450                 LATTEST(false);
451
452         FindAndReplaceOptions::SearchRestriction restr =
453                 OnlyMaths->isChecked()
454                         ? FindAndReplaceOptions::R_ONLY_MATHS
455                         : FindAndReplaceOptions::R_EVERYTHING;
456
457         LYXERR(Debug::FIND, "FindAndReplaceOptions: "
458                << "find_buf_name=" << find_buf_name
459                << ", casesensitiv=" << casesensitive
460                << ", matchword=" << matchword
461                << ", backwards=" << backwards
462                << ", expandmacros=" << expandmacros
463                << ", ignoreformat=" << ignoreformat
464                << ", repl_buf_name" << repl_buf_name
465                << ", keep_case=" << keep_case
466                << ", scope=" << scope
467                << ", restr=" << restr);
468
469         FindAndReplaceOptions opt(find_buf_name, casesensitive, matchword,
470                                   !backwards, expandmacros, ignoreformat,
471                                   repl_buf_name, keep_case, scope, restr);
472         return findAndReplaceScope(opt, replace_all);
473 }
474
475
476 bool FindAndReplaceWidget::findAndReplace(bool backwards, bool replace, bool replace_all)
477 {
478         if (! view_.currentMainWorkArea()) {
479                 view_.message(_("No open document(s) in which to search"));
480                 return false;
481         }
482         // Finalize macros that are being typed, both in main document and in search or replacement WAs
483         if (view_.currentWorkArea()->bufferView().cursor().macroModeClose())
484                 view_.currentWorkArea()->bufferView().processUpdateFlags(Update::Force);
485         if (view_.currentMainWorkArea()->bufferView().cursor().macroModeClose())
486                 view_.currentMainWorkArea()->bufferView().processUpdateFlags(Update::Force);
487
488         // FIXME: create a Dialog::returnFocus()
489         // or something instead of this:
490         view_.setCurrentWorkArea(view_.currentMainWorkArea());
491         return findAndReplace(caseCB->isChecked(),
492                 wordsCB->isChecked(),
493                 backwards,
494                 expandMacrosCB->isChecked(),
495                 ignoreFormatCB->isChecked(),
496                 replace,
497                 keepCaseCB->isChecked(),
498                 replace_all);
499 }
500
501
502 void FindAndReplaceWidget::hideDialog()
503 {
504         dispatch(FuncRequest(LFUN_DIALOG_TOGGLE, "findreplaceadv"));
505 }
506
507
508 void FindAndReplaceWidget::on_findNextPB_clicked()
509 {
510         findAndReplace(searchbackCB->isChecked(), false);
511         find_work_area_->setFocus();
512 }
513
514
515 void FindAndReplaceWidget::on_replacePB_clicked()
516 {
517         findAndReplace(searchbackCB->isChecked(), true);
518         replace_work_area_->setFocus();
519 }
520
521
522 void FindAndReplaceWidget::on_replaceallPB_clicked()
523 {
524         findAndReplace(searchbackCB->isChecked(), true, true);
525         replace_work_area_->setFocus();
526 }
527
528
529 /** Copy selected elements from bv's BufferParams to the dest_bv's one
530  ** We don't want to copy'em all, e.g., not the default master **/
531 static void copy_params(BufferView const & bv, BufferView & dest_bv) {
532         Buffer const & doc_buf = bv.buffer();
533         BufferParams const & doc_bp = doc_buf.params();
534         string const & lang = doc_bp.language->lang();
535         string const & doc_class = doc_bp.documentClass().name();
536         Buffer & dest_buf = dest_bv.buffer();
537         dest_buf.params().setLanguage(lang);
538         dest_buf.params().setBaseClass(doc_class);
539         dest_bv.makeDocumentClass();
540         dest_bv.cursor().current_font.setLanguage(doc_bp.language);
541 }
542
543
544 void FindAndReplaceWidget::showEvent(QShowEvent * /* ev */)
545 {
546         LYXERR(Debug::DEBUG, "showEvent()" << endl);
547         BufferView * bv = view_.documentBufferView();
548         if (bv) {
549                 copy_params(*bv, find_work_area_->bufferView());
550                 copy_params(*bv, replace_work_area_->bufferView());
551         }
552
553         find_work_area_->installEventFilter(this);
554         replace_work_area_->installEventFilter(this);
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 }
561
562
563 void FindAndReplaceWidget::hideEvent(QHideEvent *ev)
564 {
565         replace_work_area_->removeEventFilter(this);
566         find_work_area_->removeEventFilter(this);
567         this->QWidget::hideEvent(ev);
568 }
569
570
571 bool FindAndReplaceWidget::initialiseParams(std::string const & /*params*/)
572 {
573         return true;
574 }
575
576
577 void FindAndReplace::updateView()
578 {
579         widget_->updateGUI();
580 }
581
582
583 FindAndReplace::FindAndReplace(GuiView & parent,
584                 Qt::DockWidgetArea area, Qt::WindowFlags flags)
585         : DockView(parent, "findreplaceadv", qt_("Advanced Find and Replace"),
586                    area, flags)
587 {
588         widget_ = new FindAndReplaceWidget(parent);
589         setWidget(widget_);
590         setFocusProxy(widget_);
591
592         connect(this, SIGNAL(dockLocationChanged(Qt::DockWidgetArea)),
593                 widget_, SLOT(dockLocationChanged(Qt::DockWidgetArea)));
594 }
595
596
597 FindAndReplace::~FindAndReplace()
598 {
599         setFocusProxy(0);
600         delete widget_;
601 }
602
603
604 bool FindAndReplace::initialiseParams(std::string const & params)
605 {
606         return widget_->initialiseParams(params);
607 }
608
609
610 void FindAndReplaceWidget::updateGUI()
611 {
612         bool replace_enabled = view_.documentBufferView()
613                 && !view_.documentBufferView()->buffer().isReadonly();
614         replace_work_area_->setEnabled(replace_enabled);
615         replacePB->setEnabled(replace_enabled);
616         replaceallPB->setEnabled(replace_enabled);
617 }
618
619
620 Dialog * createGuiSearchAdv(GuiView & lv)
621 {
622         FindAndReplace * gui = new FindAndReplace(lv, Qt::RightDockWidgetArea);
623 #ifdef Q_OS_MAC
624         // On Mac show and floating
625         gui->setFloating(true);
626 #endif
627         return gui;
628 }
629
630
631 } // namespace frontend
632 } // namespace lyx
633
634
635 #include "moc_FindAndReplace.cpp"