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