1/* ****************************************************************************
2 This file is part of Lokalize
3
4 Copyright (C) 2007-2009 by Nick Shaforostoff <shafff@ukr.net>
5
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License as
8 published by the Free Software Foundation; either version 2 of
9 the License or (at your option) version 3 or any later version
10 accepted by the membership of KDE e.V. (or its successor approved
11 by the membership of KDE e.V.), which shall act as a proxy
12 defined in Section 14 of version 3 of the license.
13
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
18
19 You should have received a copy of the GNU General Public License
20 along with this program. If not, see <http://www.gnu.org/licenses/>.
21
22**************************************************************************** */
23
24#include "editortab.h"
25#include "editorview.h"
26#include "catalog.h"
27#include "pos.h"
28#include "cmd.h"
29#include "project.h"
30#include "prefs_lokalize.h"
31#include "ui_kaider_findextension.h"
32#include "stemming.h"
33
34
35#include <kglobal.h>
36#include <kmessagebox.h>
37#include <klocale.h>
38#include <kdebug.h>
39#include <kurl.h>
40
41#include <kprogressdialog.h>
42
43#include <kreplacedialog.h>
44#include <kreplace.h>
45
46#include <sonnet/backgroundchecker.h>
47#include <sonnet/dialog.h>
48
49#include <QTimer>
50#include <QPointer>
51
52
53#define IGNOREACCELS KFind::MinimumUserOption
54#define INCLUDENOTES KFind::MinimumUserOption*2
55
56static long makeOptions(long options, const Ui_findExtension* ui_findExtension)
57{
58 return options
59 +IGNOREACCELS*ui_findExtension->m_ignoreAccelMarks->isChecked()
60 +INCLUDENOTES*ui_findExtension->m_notes->isChecked();
61 //bool skipMarkup(){return ui_findExtension->m_skipTags->isChecked();}
62}
63
64class EntryFindDialog: public KFindDialog
65{
66public:
67 EntryFindDialog(QWidget* parent);
68 ~EntryFindDialog();
69 long options() const{return makeOptions(KFindDialog::options(),ui_findExtension);}
70 static EntryFindDialog* instance(QWidget* parent=0);
71private:
72 static QPointer<EntryFindDialog> _instance;
73 static void cleanup(){delete EntryFindDialog::_instance;}
74private:
75 Ui_findExtension* ui_findExtension;
76};
77
78QPointer<EntryFindDialog> EntryFindDialog::_instance=0;
79EntryFindDialog* EntryFindDialog::instance(QWidget* parent)
80{
81 if (_instance==0 )
82 {
83 _instance=new EntryFindDialog(parent);
84 qAddPostRoutine(EntryFindDialog::cleanup);
85 }
86 return _instance;
87}
88
89EntryFindDialog::EntryFindDialog(QWidget* parent)
90 : KFindDialog(parent)
91 , ui_findExtension(new Ui_findExtension)
92{
93 ui_findExtension->setupUi(findExtension());
94 setHasSelection(false);
95
96 KConfig config;
97 KConfigGroup stateGroup(&config,"FindReplace");
98 setOptions(stateGroup.readEntry("FindOptions",(qlonglong)0));
99 setFindHistory(stateGroup.readEntry("FindHistory",QStringList()));
100}
101
102EntryFindDialog::~EntryFindDialog()
103{
104 KConfig config;
105 KConfigGroup stateGroup(&config,"FindReplace");
106 stateGroup.writeEntry("FindOptions",(qlonglong)options());
107 stateGroup.writeEntry("FindHistory",findHistory());
108
109 delete ui_findExtension;
110}
111
112//BEGIN EntryReplaceDialog
113class EntryReplaceDialog: public KReplaceDialog
114{
115public:
116 EntryReplaceDialog(QWidget* parent);
117 ~EntryReplaceDialog();
118 long options() const{return makeOptions(KReplaceDialog::options(),ui_findExtension);}
119 static EntryReplaceDialog* instance(QWidget* parent=0);
120private:
121 static QPointer<EntryReplaceDialog> _instance;
122 static void cleanup(){delete EntryReplaceDialog::_instance;}
123private:
124 Ui_findExtension* ui_findExtension;
125};
126
127QPointer<EntryReplaceDialog> EntryReplaceDialog::_instance=0;
128EntryReplaceDialog* EntryReplaceDialog::instance(QWidget* parent)
129{
130 if (_instance==0 )
131 {
132 _instance=new EntryReplaceDialog(parent);
133 qAddPostRoutine(EntryReplaceDialog::cleanup);
134 }
135 return _instance;
136}
137
138EntryReplaceDialog::EntryReplaceDialog(QWidget* parent)
139 : KReplaceDialog(parent)
140 , ui_findExtension(new Ui_findExtension)
141{
142 ui_findExtension->setupUi(findExtension());
143 //ui_findExtension->m_notes->hide();
144 setHasSelection(false);
145
146 KConfig config;
147 KConfigGroup stateGroup(&config,"FindReplace");
148 setOptions(stateGroup.readEntry("ReplaceOptions",(qlonglong)0));
149 setFindHistory(stateGroup.readEntry("ReplacePatternHistory",QStringList()));
150 setReplacementHistory(stateGroup.readEntry("ReplacementHistory",QStringList()));
151}
152
153EntryReplaceDialog::~EntryReplaceDialog()
154{
155 KConfig config;
156 KConfigGroup stateGroup(&config,"FindReplace");
157 stateGroup.writeEntry("ReplaceOptions",(qlonglong)options());
158 stateGroup.writeEntry("ReplacePatternHistory",findHistory());
159 stateGroup.writeEntry("ReplacementHistory",replacementHistory());
160
161 delete ui_findExtension;
162}
163//END EntryReplaceDialog
164
165//TODO &amp;, &nbsp;
166static void calcOffsetWithAccels(const QString& data, int& offset, int& length)
167{
168 int i=0;
169 for (;i<offset;++i)
170 if (KDE_ISUNLIKELY( data.at(i)=='&' ))
171 ++offset;
172
173 //if & is inside highlighted word
174 int limit=offset+length;
175 for (i=offset;i<limit;++i)
176 if (KDE_ISUNLIKELY( data.at(i)=='&' ))
177 {
178 ++length;
179 limit=qMin(data.size(),offset+length);//just safety
180 }
181}
182
183void EditorTab::find()
184{
185 //QWidget* p=0; QWidget* next=qobject_cast<QWidget*>(parent()); while(next) { p=next; next=qobject_cast<QWidget*>(next->parent()); }
186 EntryFindDialog::instance(nativeParentWidget());
187
188 QString sel=selectionInTarget();
189 if (!(sel.isEmpty()&&selectionInSource().isEmpty()))
190 {
191 if (sel.isEmpty())
192 sel=selectionInSource();
193 if (_find&&_find->options()&IGNOREACCELS)
194 sel.remove('&');
195 EntryFindDialog::instance()->setPattern(sel);
196 }
197
198 if ( EntryFindDialog::instance()->exec() != QDialog::Accepted )
199 return;
200
201 if (_find)
202 {
203 _find->resetCounts();
204 _find->setPattern(EntryFindDialog::instance()->pattern());
205 _find->setOptions(EntryFindDialog::instance()->options());
206
207 }
208 else // This creates a find-next-prompt dialog if needed.
209 {
210 _find = new KFind(EntryFindDialog::instance()->pattern(),EntryFindDialog::instance()->options(),this,EntryFindDialog::instance());
211 connect(_find,SIGNAL(highlight(QString,int,int)),this, SLOT(highlightFound(QString,int,int)) );
212 connect(_find,SIGNAL(findNext()),this,SLOT(findNext()));
213 _find->closeFindNextDialog();
214 }
215
216 DocPosition pos;
217 if (_find->options() & KFind::FromCursor)
218 pos=m_currentPos;
219 else if (!determineStartingPos(_find,pos))
220 return;
221
222
223 findNext(pos);
224}
225
226bool EditorTab::determineStartingPos(KFind* find,
227 DocPosition& pos)
228{
229 if (find->options() & KFind::FindBackwards)
230 {
231 pos.entry=m_catalog->numberOfEntries()-1;
232 pos.form=(m_catalog->isPlural(pos.entry))?
233 m_catalog->numberOfPluralForms()-1:0;
234 }
235 else
236 {
237 pos.entry=0;
238 pos.form=0;
239 }
240 return true;
241}
242
243void EditorTab::findNext(const DocPosition& startingPos)
244{
245 Catalog& catalog=*m_catalog;
246 KFind& find=*_find;
247
248 if (KDE_ISUNLIKELY( catalog.numberOfEntries()<=startingPos.entry ))
249 return;//for the case when app wasn't able to process event before file close
250
251 bool anotherEntry=_searchingPos.entry!=m_currentPos.entry;
252 _searchingPos=startingPos;
253
254 if (anotherEntry)
255 _searchingPos.offset=0;
256
257
258 QRegExp rx("[^(\\\\n)>]\n");
259 QTime a;a.start();
260 //_searchingPos.part=DocPosition::Source;
261 bool ignoreaccels=_find->options()&IGNOREACCELS;
262 bool includenotes=_find->options()&INCLUDENOTES;
263 int switchOptions=DocPosition::Source|DocPosition::Target|(includenotes*DocPosition::Comment);
264 int flag=1;
265 while (flag)
266 {
267
268 flag=0;
269 KFind::Result res = KFind::NoMatch;
270 while (true)
271 {
272 if (find.needData()||anotherEntry||m_view->m_modifiedAfterFind)
273 {
274 anotherEntry=false;
275 m_view->m_modifiedAfterFind=false;
276
277 QString data;
278 if (_searchingPos.part==DocPosition::Comment)
279 data=catalog.notes(_searchingPos).at(_searchingPos.form).content;
280 else
281 data=catalog.catalogString(_searchingPos).string;
282
283 if (ignoreaccels)
284 data.remove('&');
285 find.setData(data);
286 }
287
288 res = find.find();
289 //offset=-1;
290 if (res!=KFind::NoMatch)
291 break;
292
293 if (!(
294 (find.options()&KFind::FindBackwards)?
295 switchPrev(m_catalog,_searchingPos,switchOptions):
296 switchNext(m_catalog,_searchingPos,switchOptions)
297 ))
298 break;
299 }
300
301 if (res==KFind::NoMatch)
302 {
303 //file-wide search
304 if(find.shouldRestart(true,true))
305 {
306 flag=1;
307 determineStartingPos(_find,_searchingPos);
308 }
309 find.resetCounts();
310 }
311 }
312}
313
314void EditorTab::findNext()
315{
316 if (_find)
317 {
318 findNext((m_currentPos.entry==_searchingPos.entry&&_searchingPos.part==DocPosition::Comment)?
319 _searchingPos:m_currentPos);
320 }
321 else
322 find();
323
324}
325
326void EditorTab::findPrev()
327{
328
329 if (_find)
330 {
331 _find->setOptions(_find->options() ^ KFind::FindBackwards);
332 findNext(m_currentPos);
333 }
334 else
335 {
336 find();
337 }
338
339}
340
341void EditorTab::highlightFound(const QString &,int matchingIndex,int matchedLength)
342{
343 if (_find->options()&IGNOREACCELS && _searchingPos.part!=DocPosition::Comment)
344 {
345 QString data=m_catalog->catalogString(_searchingPos).string;
346 calcOffsetWithAccels(data, matchingIndex, matchedLength);
347 }
348
349 _searchingPos.offset=matchingIndex;
350 gotoEntry(_searchingPos,matchedLength);
351}
352
353void EditorTab::replace()
354{
355 EntryReplaceDialog::instance(nativeParentWidget());
356
357 if (!m_view->selectionInTarget().isEmpty())
358 {
359 if (_replace&&_replace->options()&IGNOREACCELS)
360 {
361 QString tmp(m_view->selectionInTarget());
362 tmp.remove('&');
363 EntryReplaceDialog::instance()->setPattern(tmp);
364 }
365 else
366 EntryReplaceDialog::instance()->setPattern(m_view->selectionInTarget());
367 }
368
369
370 if ( EntryReplaceDialog::instance()->exec() != QDialog::Accepted )
371 return;
372
373
374 if (_replace) _replace->deleteLater();// _replace=0;
375
376 // This creates a find-next-prompt dialog if needed.
377 {
378 _replace = new KReplace(EntryReplaceDialog::instance()->pattern(),EntryReplaceDialog::instance()->replacement(),EntryReplaceDialog::instance()->options(),this,EntryReplaceDialog::instance());
379 connect(_replace,SIGNAL(highlight(QString,int,int)), this,SLOT(highlightFound_(QString,int,int)));
380 connect(_replace,SIGNAL(findNext()), this,SLOT(replaceNext()));
381 connect(_replace,SIGNAL(replace(QString,int,int,int)), this,SLOT(doReplace(QString,int,int,int)));
382 connect(_replace,SIGNAL(dialogClosed()), this,SLOT(cleanupReplace()));
383// _replace->closeReplaceNextDialog();
384 }
385// else
386// {
387// _replace->resetCounts();
388// _replace->setPattern(EntryReplaceDialog::instance()->pattern());
389// _replace->setOptions(EntryReplaceDialog::instance()->options());
390// }
391
392 //m_catalog->beginMacro(i18nc("@item Undo action item","Replace"));
393 m_doReplaceCalled=false;
394
395 if (_replace->options() & KFind::FromCursor)
396 replaceNext(m_currentPos);
397 else
398 {
399 DocPosition pos;
400 if (!determineStartingPos(_replace,pos)) return;
401 replaceNext(pos);
402 }
403
404}
405
406
407void EditorTab::replaceNext(const DocPosition& startingPos)
408{
409 bool anotherEntry=_replacingPos.entry!=_replacingPos.entry;
410 _replacingPos=startingPos;
411
412 if (anotherEntry)
413 _replacingPos.offset=0;
414
415
416 int flag=1;
417 bool ignoreaccels=_replace->options()&IGNOREACCELS;
418 bool includenotes=_replace->options()&INCLUDENOTES;
419 kWarning()<<"includenotes"<<includenotes;
420 int switchOptions=DocPosition::Target|(includenotes*DocPosition::Comment);
421 while (flag)
422 {
423 flag=0;
424 KFind::Result res=KFind::NoMatch;
425 while (1)
426 {
427 if (_replace->needData()||anotherEntry/*||m_view->m_modifiedAfterFind*/)
428 {
429 anotherEntry=false;
430 //m_view->m_modifiedAfterFind=false;//NOTE TEST THIS
431
432 QString data;
433 if (_replacingPos.part==DocPosition::Comment)
434 data=m_catalog->notes(_replacingPos).at(_replacingPos.form).content;
435 else
436 {
437 data=m_catalog->targetWithTags(_replacingPos).string;
438 if (ignoreaccels) data.remove('&');
439 }
440 _replace->setData(data);
441 }
442 res = _replace->replace();
443 if (res!=KFind::NoMatch)
444 break;
445
446 if (!(
447 (_replace->options()&KFind::FindBackwards)?
448 switchPrev(m_catalog,_replacingPos,switchOptions):
449 switchNext(m_catalog,_replacingPos,switchOptions)
450 ))
451 break;
452 }
453
454 if (res==KFind::NoMatch)
455 {
456 if((_replace->options()&KFind::FromCursor)
457 &&_replace->shouldRestart(true))
458 {
459 flag=1;
460 determineStartingPos(_replace,_replacingPos);
461 }
462 else
463 {
464 if(!(_replace->options() & KFind::FromCursor))
465 _replace->displayFinalDialog();
466
467 _replace->closeReplaceNextDialog();
468 cleanupReplace();
469 }
470 _replace->resetCounts();
471 }
472 }
473}
474
475void EditorTab::cleanupReplace()
476{
477 if(m_doReplaceCalled)
478 {
479 m_doReplaceCalled=false;
480 m_catalog->endMacro();
481 }
482}
483
484void EditorTab::replaceNext()
485{
486 replaceNext(m_currentPos);
487}
488
489void EditorTab::highlightFound_(const QString &,int matchingIndex,int matchedLength)
490{
491 if (_replace->options()&IGNOREACCELS)
492 {
493 QString data=m_catalog->targetWithTags(_replacingPos).string;
494 calcOffsetWithAccels(data,matchingIndex,matchedLength);
495 }
496
497 _replacingPos.offset=matchingIndex;
498 gotoEntry(_replacingPos,matchedLength);
499}
500
501
502void EditorTab::doReplace(const QString &newStr,int offset,int newLen,int remLen)
503{
504 if(!m_doReplaceCalled)
505 {
506 m_doReplaceCalled=true;
507 m_catalog->beginMacro(i18nc("@item Undo action item","Replace"));
508 }
509 DocPosition pos=_replacingPos;
510 if (_replacingPos.part==DocPosition::Comment)
511 m_catalog->push(new SetNoteCmd(m_catalog,pos,newStr));
512 else
513 {
514 QString oldStr=m_catalog->target(_replacingPos);
515
516 if (_replace->options()&IGNOREACCELS)
517 calcOffsetWithAccels(oldStr,offset,remLen);
518
519 pos.offset=offset;
520 m_catalog->push(new DelTextCmd(m_catalog,pos,oldStr.mid(offset,remLen)));
521
522 if (newLen)
523 m_catalog->push(new InsTextCmd(m_catalog,pos,newStr.mid(offset,newLen)));
524 }
525 if (pos.entry==m_currentPos.entry)
526 {
527 pos.offset+=newLen;
528 m_view->gotoEntry(pos);
529 }
530}
531
532
533
534
535
536
537
538
539
540
541void EditorTab::spellcheck()
542{
543 if (!m_sonnetDialog)
544 {
545 m_sonnetChecker=new Sonnet::BackgroundChecker(this);
546 m_sonnetChecker->changeLanguage(enhanceLangCode(Project::instance()->langCode()));
547 m_sonnetDialog=new Sonnet::Dialog(m_sonnetChecker,this);
548 connect(m_sonnetDialog,SIGNAL(done(QString)),this,SLOT(spellcheckNext()));
549 connect(m_sonnetDialog,SIGNAL(replace(QString,int,QString)),
550 this,SLOT(spellcheckReplace(QString,int,QString)));
551 connect(m_sonnetDialog,SIGNAL(stop()),this,SLOT(spellcheckStop()));
552 connect(m_sonnetDialog,SIGNAL(cancel()),this,SLOT(spellcheckCancel()));
553
554 connect(m_sonnetDialog/*m_sonnetChecker*/,SIGNAL(misspelling(QString,int)),
555 this,SLOT(spellcheckShow(QString,int)));
556// disconnect(/*m_sonnetDialog*/m_sonnetChecker,SIGNAL(misspelling(QString,int)),
557// m_sonnetDialog,SLOT(slotMisspelling(QString,int)));
558//
559// connect( d->checker, SIGNAL(misspelling(const QString&, int)),
560// SLOT(slotMisspelling(const QString&, int)) );
561 }
562
563 QString text=m_catalog->msgstr(m_currentPos);
564 if (!m_view->selectionInTarget().isEmpty())
565 text=m_view->selectionInTarget();
566 text.remove('&');
567 m_sonnetDialog->setBuffer(text);
568
569 _spellcheckPos=m_currentPos;
570 _spellcheckStartPos=m_currentPos;
571 _spellcheckStop=false;
572 //m_catalog->beginMacro(i18n("Spellcheck"));
573 _spellcheckStartUndoIndex=m_catalog->index();
574 m_sonnetDialog->show();
575
576}
577
578
579void EditorTab::spellcheckNext()
580{
581 if (_spellcheckStop)
582 return;
583
584 do
585 {
586 if (!switchNext(m_catalog,_spellcheckPos))
587 {
588 kWarning()<<_spellcheckStartPos.entry;
589 kWarning()<<_spellcheckStartPos.form;
590 bool continueFromStart=
591 !(_spellcheckStartPos.entry==0 && _spellcheckStartPos.form==0)
592 && KMessageBox::questionYesNo(this,i18n("Lokalize has reached end of document. Do you want to continue from start?"), i18nc("@title", "Spellcheck"))==KMessageBox::Yes;
593 if (continueFromStart)
594 {
595 _spellcheckStartPos.entry=0;
596 _spellcheckStartPos.form=0;
597 _spellcheckPos=_spellcheckStartPos;
598 }
599 else
600 {
601 KMessageBox::information(this,i18n("Lokalize has finished spellchecking"), i18nc("@title", "Spellcheck"));
602 return;
603 }
604 }
605 }
606 while (m_catalog->msgstr(_spellcheckPos).isEmpty() || !m_catalog->isApproved(_spellcheckPos.entry));
607
608 QString text=m_catalog->msgstr(_spellcheckPos);
609 text.remove('&');
610 m_sonnetDialog->setBuffer(text);
611}
612
613void EditorTab::spellcheckStop()
614{
615 _spellcheckStop=true;
616}
617
618void EditorTab::spellcheckCancel()
619{
620 m_catalog->setIndex(_spellcheckStartUndoIndex);
621 gotoEntry(_spellcheckPos);
622}
623
624void EditorTab::spellcheckShow(const QString &word, int offset)
625{
626 QString source=m_catalog->source(_spellcheckPos);
627 source.remove('&');
628 if (source.contains(word))
629 {
630 m_sonnetDialog->setUpdatesEnabled(false);
631 m_sonnetChecker->continueChecking();
632 return;
633 }
634 m_sonnetDialog->setUpdatesEnabled(true);
635
636 show();
637
638 DocPosition pos=_spellcheckPos;
639 int length=word.length();
640 calcOffsetWithAccels(m_catalog->target(pos),offset,length);
641 pos.offset=offset;
642
643 gotoEntry(pos,length);
644}
645
646void EditorTab::spellcheckReplace(QString oldWord, int offset, const QString &newWord)
647{
648 DocPosition pos=_spellcheckPos;
649 int length=oldWord.length();
650 calcOffsetWithAccels(m_catalog->target(pos),offset,length);
651 pos.offset=offset;
652 if (length>oldWord.length())//replaced word contains accel mark
653 oldWord=m_catalog->target(pos).mid(offset,length);
654
655 m_catalog->push(new DelTextCmd(m_catalog,pos,oldWord));
656 m_catalog->push(new InsTextCmd(m_catalog,pos,newWord));
657
658
659 gotoEntry(pos,newWord.length());
660}
661
662
663
664
665
666
667
668
669
670
671
672bool EditorTab::findEntryBySourceContext(const QString& source, const QString& ctxt)
673{
674 DocPosition pos(0);
675 do
676 {
677 if (m_catalog->source(pos)==source && m_catalog->context(pos.entry)==QStringList(ctxt))
678 {
679 gotoEntry(pos);
680 return true;
681 }
682 }
683 while (switchNext(m_catalog,pos));
684 return false;
685}
686
687
688void EditorTab::displayWordCount()
689{
690 //TODO in trans and fuzzy separately
691 int sourceCount=0;
692 int targetCount=0;
693 QRegExp rxClean(Project::instance()->markup()+'|'+Project::instance()->accel());//cleaning regexp; NOTE isEmpty()?
694 QRegExp rxSplit("\\W|\\d");//splitting regexp
695 DocPosition pos(0);
696 do
697 {
698 QString msg=m_catalog->source(pos);
699 msg.remove(rxClean);
700 QStringList words=msg.split(rxSplit,QString::SkipEmptyParts);
701 sourceCount+=words.size();
702
703 msg=m_catalog->target(pos);
704 msg.remove(rxClean);
705 words=msg.split(rxSplit,QString::SkipEmptyParts);
706 targetCount+=words.size();
707 }
708 while (switchNext(m_catalog,pos));
709
710 KMessageBox::information(this, i18nc("@info words count",
711 "Source text words: %1<br/>Target text words: %2",
712 sourceCount,targetCount),i18nc("@title","Word Count"));
713}
714
715
716