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