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