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