1/*
2 Copyright (c) 2003 Trolltech AS
3 Copyright (c) 2003 Scott Wheeler <wheeler@kde.org>
4
5 This file is part of the KDE libraries
6
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Library General Public
9 License version 2 as published by the Free Software Foundation.
10
11 This library is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 Library General Public License for more details.
15
16 You should have received a copy of the GNU Library General Public License
17 along with this library; see the file COPYING.LIB. If not, write to
18 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 Boston, MA 02110-1301, USA.
20*/
21
22#include "k3syntaxhighlighter.h"
23
24#include <QtGui/QColor>
25#include <QtCore/QRegExp>
26#include <Qt3Support/Q3SyntaxHighlighter>
27#include <QtCore/QTimer>
28
29#include <klocale.h>
30#include <kconfig.h>
31#include <kdebug.h>
32#include <kglobal.h>
33#include <k3sconfig.h>
34#include <k3spell.h>
35#include <Qt3Support/Q3Dict>
36#include <QKeyEvent>
37
38#include <kconfiggroup.h>
39#include <fixx11h.h>
40
41static int dummy, dummy2, dummy3, dummy4;
42static int *Okay = &dummy;
43static int *NotOkay = &dummy2;
44static int *Ignore = &dummy3;
45static int *Unknown = &dummy4;
46static const int tenSeconds = 10*1000;
47
48class K3SyntaxHighlighter::K3SyntaxHighlighterPrivate
49{
50public:
51 QColor col1, col2, col3, col4, col5;
52 SyntaxMode mode;
53 bool enabled;
54};
55
56class K3SpellingHighlighter::K3SpellingHighlighterPrivate
57{
58public:
59
60 K3SpellingHighlighterPrivate() :
61 alwaysEndsWithSpace( true ),
62 intraWordEditing( false ) {}
63
64 QString currentWord;
65 int currentPos;
66 bool alwaysEndsWithSpace;
67 QColor color;
68 bool intraWordEditing;
69};
70
71class K3DictSpellingHighlighter::K3DictSpellingHighlighterPrivate
72{
73public:
74 K3DictSpellingHighlighterPrivate() :
75 mDict( 0 ),
76 spell( 0 ),
77 mSpellConfig( 0 ),
78 rehighlightRequest( 0 ),
79 wordCount( 0 ),
80 errorCount( 0 ),
81 autoReady( false ),
82 globalConfig( true ),
83 spellReady( false ) {}
84
85 ~K3DictSpellingHighlighterPrivate() {
86 delete rehighlightRequest;
87 delete spell;
88 }
89
90 static Q3Dict<int>* sDict()
91 {
92 if (!statDict)
93 statDict = new Q3Dict<int>(50021);
94 return statDict;
95 }
96
97 Q3Dict<int>* mDict;
98 Q3Dict<int> autoDict;
99 Q3Dict<int> autoIgnoreDict;
100 static QObject *sDictionaryMonitor;
101 K3Spell *spell;
102 K3SpellConfig *mSpellConfig;
103 QTimer *rehighlightRequest, *spellTimeout;
104 QString spellKey;
105 int wordCount, errorCount;
106 int checksRequested, checksDone;
107 int disablePercentage;
108 int disableWordCount;
109 bool completeRehighlightRequired;
110 bool active, automatic, autoReady;
111 bool globalConfig, spellReady;
112private:
113 static Q3Dict<int>* statDict;
114
115};
116
117Q3Dict<int>* K3DictSpellingHighlighter::K3DictSpellingHighlighterPrivate::statDict = 0;
118
119
120K3SyntaxHighlighter::K3SyntaxHighlighter( Q3TextEdit *textEdit,
121 bool colorQuoting,
122 const QColor& depth0,
123 const QColor& depth1,
124 const QColor& depth2,
125 const QColor& depth3,
126 SyntaxMode mode )
127 : Q3SyntaxHighlighter( textEdit ),d(new K3SyntaxHighlighterPrivate())
128{
129
130 d->enabled = colorQuoting;
131 d->col1 = depth0;
132 d->col2 = depth1;
133 d->col3 = depth2;
134 d->col4 = depth3;
135 d->col5 = depth0;
136
137 d->mode = mode;
138}
139
140K3SyntaxHighlighter::~K3SyntaxHighlighter()
141{
142 delete d;
143}
144
145int K3SyntaxHighlighter::highlightParagraph( const QString &text, int )
146{
147 if (!d->enabled) {
148 setFormat( 0, text.length(), textEdit()->viewport()->paletteForegroundColor() );
149 return 0;
150 }
151
152 QString simplified = text;
153 simplified = simplified.replace( QRegExp( "\\s" ), QString() ).replace( '|', QLatin1String(">") );
154 while ( simplified.startsWith( QLatin1String(">>>>") ) )
155 simplified = simplified.mid(3);
156 if ( simplified.startsWith( QLatin1String(">>>") ) || simplified.startsWith( QString::fromLatin1("> > >") ) )
157 setFormat( 0, text.length(), d->col2 );
158 else if ( simplified.startsWith( QLatin1String(">>") ) || simplified.startsWith( QString::fromLatin1("> >") ) )
159 setFormat( 0, text.length(), d->col3 );
160 else if ( simplified.startsWith( QLatin1String(">") ) )
161 setFormat( 0, text.length(), d->col4 );
162 else
163 setFormat( 0, text.length(), d->col5 );
164 return 0;
165}
166
167K3SpellingHighlighter::K3SpellingHighlighter( Q3TextEdit *textEdit,
168 const QColor& spellColor,
169 bool colorQuoting,
170 const QColor& depth0,
171 const QColor& depth1,
172 const QColor& depth2,
173 const QColor& depth3 )
174 : K3SyntaxHighlighter( textEdit, colorQuoting, depth0, depth1, depth2, depth3 ),d(new K3SpellingHighlighterPrivate())
175{
176
177 d->color = spellColor;
178}
179
180K3SpellingHighlighter::~K3SpellingHighlighter()
181{
182 delete d;
183}
184
185int K3SpellingHighlighter::highlightParagraph( const QString &text,
186 int paraNo )
187{
188 if ( paraNo == -2 )
189 paraNo = 0;
190 // leave #includes, diffs, and quoted replies alone
191 QString diffAndCo( ">|" );
192
193 bool isCode = diffAndCo.contains(text[0]);
194
195 if ( !text.endsWith(' ') )
196 d->alwaysEndsWithSpace = false;
197
198 K3SyntaxHighlighter::highlightParagraph( text, -2 );
199
200 if ( !isCode ) {
201 int para, index;
202 textEdit()->getCursorPosition( &para, &index );
203 int len = text.length();
204 if ( d->alwaysEndsWithSpace )
205 len--;
206
207 d->currentPos = 0;
208 d->currentWord = "";
209 for ( int i = 0; i < len; i++ ) {
210 if ( !text[i].isLetter() && (!(text[i] == '\'')) ) {
211 if ( ( para != paraNo ) ||
212 !intraWordEditing() ||
213 ( i - d->currentWord.length() > index ) ||
214 ( i < index ) ) {
215 flushCurrentWord();
216 } else {
217 d->currentWord = "";
218 }
219 d->currentPos = i + 1;
220 } else {
221 d->currentWord += text[i];
222 }
223 }
224 if ( ( len > 0 && !text[len - 1].isLetter() ) ||
225 ( index + 1 ) != text.length() ||
226 para != paraNo )
227 flushCurrentWord();
228 }
229 return ++paraNo;
230}
231
232QStringList K3SpellingHighlighter::personalWords()
233{
234 QStringList l;
235 l.append( "KMail" );
236 l.append( "KOrganizer" );
237 l.append( "KAddressBook" );
238 l.append( "KHTML" );
239 l.append( "KIO" );
240 l.append( "KJS" );
241 l.append( "Konqueror" );
242 l.append( "K3Spell" );
243 l.append( "Kontact" );
244 l.append( "Qt" );
245 return l;
246}
247
248void K3SpellingHighlighter::flushCurrentWord()
249{
250 while ( d->currentWord[0].isPunct() ) {
251 d->currentWord = d->currentWord.mid( 1 );
252 d->currentPos++;
253 }
254
255 QChar ch;
256 while ( !d->currentWord.isEmpty() && ( ch = d->currentWord[(int) d->currentWord.length() - 1] ).isPunct() &&
257 ch != '(' && ch != '@' )
258 d->currentWord.truncate( d->currentWord.length() - 1 );
259
260 if ( !d->currentWord.isEmpty() ) {
261 if ( isMisspelled( d->currentWord ) ) {
262 setFormat( d->currentPos, d->currentWord.length(), d->color );
263// setMisspelled( d->currentPos, d->currentWord.length(), true );
264 }
265 }
266 d->currentWord = "";
267}
268
269QObject *K3DictSpellingHighlighter::K3DictSpellingHighlighterPrivate::sDictionaryMonitor = 0;
270
271K3DictSpellingHighlighter::K3DictSpellingHighlighter( Q3TextEdit *textEdit,
272 bool spellCheckingActive ,
273 bool autoEnable,
274 const QColor& spellColor,
275 bool colorQuoting,
276 const QColor& depth0,
277 const QColor& depth1,
278 const QColor& depth2,
279 const QColor& depth3,
280 K3SpellConfig *spellConfig )
281 : K3SpellingHighlighter( textEdit, spellColor,
282 colorQuoting, depth0, depth1, depth2, depth3 ),d(new K3DictSpellingHighlighterPrivate())
283{
284
285 d->mSpellConfig = spellConfig;
286 d->globalConfig = ( !spellConfig );
287 d->automatic = autoEnable;
288 d->active = spellCheckingActive;
289 d->checksRequested = 0;
290 d->checksDone = 0;
291 d->completeRehighlightRequired = false;
292
293 KConfigGroup cg( KGlobal::config(), "K3Spell" );
294 d->disablePercentage = cg.readEntry( "K3Spell_AsYouTypeDisablePercentage", QVariant(42 )).toInt();
295 d->disablePercentage = qMin( d->disablePercentage, 101 );
296 d->disableWordCount = cg.readEntry( "K3Spell_AsYouTypeDisableWordCount", QVariant(100 )).toInt();
297
298 textEdit->installEventFilter( this );
299 textEdit->viewport()->installEventFilter( this );
300
301 d->rehighlightRequest = new QTimer(this);
302 connect( d->rehighlightRequest, SIGNAL(timeout()),
303 this, SLOT(slotRehighlight()));
304 d->spellTimeout = new QTimer(this);
305 connect( d->spellTimeout, SIGNAL(timeout()),
306 this, SLOT(slotK3SpellNotResponding()));
307
308 if ( d->globalConfig ) {
309 d->spellKey = spellKey();
310
311 if ( !d->sDictionaryMonitor )
312 d->sDictionaryMonitor = new QObject();
313 }
314 else {
315 d->mDict = new Q3Dict<int>(4001);
316 connect( d->mSpellConfig, SIGNAL(configChanged()),
317 this, SLOT(slotLocalSpellConfigChanged()) );
318 }
319
320 slotDictionaryChanged();
321}
322
323K3DictSpellingHighlighter::~K3DictSpellingHighlighter()
324{
325 delete d->spell;
326 d->spell = 0;
327 delete d->mDict;
328 d->mDict = 0;
329 delete d;
330}
331
332void K3DictSpellingHighlighter::slotSpellReady( K3Spell *spell )
333{
334 kDebug(0) << "KDictSpellingHighlighter::slotSpellReady( " << spell << " )";
335 if ( d->globalConfig ) {
336 connect( d->sDictionaryMonitor, SIGNAL(destroyed()),
337 this, SLOT(slotDictionaryChanged()));
338 }
339 if ( spell != d->spell )
340 {
341 delete d->spell;
342 d->spell = spell;
343 }
344 d->spellReady = true;
345 const QStringList l = K3SpellingHighlighter::personalWords();
346 for ( QStringList::ConstIterator it = l.begin(); it != l.end(); ++it ) {
347 d->spell->addPersonal( *it );
348 }
349 connect( spell, SIGNAL(misspelling(QString,QStringList,uint)),
350 this, SLOT(slotMisspelling(QString,QStringList,uint)));
351 connect( spell, SIGNAL(corrected(QString,QString,uint)),
352 this, SLOT(slotCorrected(QString,QString,uint)));
353 d->checksRequested = 0;
354 d->checksDone = 0;
355 d->completeRehighlightRequired = true;
356 d->rehighlightRequest->start( 0, true );
357}
358
359bool K3DictSpellingHighlighter::isMisspelled( const QString &word )
360{
361 if (!d->spellReady)
362 return false;
363
364 // This debug is expensive, only enable it locally
365 //kDebug(0) << "KDictSpellingHighlighter::isMisspelled( \"" << word << "\" )";
366 // Normally isMisspelled would look up a dictionary and return
367 // true or false, but kspell is asynchronous and slow so things
368 // get tricky...
369 // For auto detection ignore signature and reply prefix
370 if ( !d->autoReady )
371 d->autoIgnoreDict.replace( word, Ignore );
372
373 // "dict" is used as a cache to store the results of K3Spell
374 Q3Dict<int>* dict = ( d->globalConfig ? d->sDict() : d->mDict );
375 if ( !dict->isEmpty() && (*dict)[word] == NotOkay ) {
376 if ( d->autoReady && ( d->autoDict[word] != NotOkay )) {
377 if ( !d->autoIgnoreDict[word] )
378 ++d->errorCount;
379 d->autoDict.replace( word, NotOkay );
380 }
381
382 return d->active;
383 }
384 if ( !dict->isEmpty() && (*dict)[word] == Okay ) {
385 if ( d->autoReady && !d->autoDict[word] ) {
386 d->autoDict.replace( word, Okay );
387 }
388 return false;
389 }
390
391 if ((dict->isEmpty() || !((*dict)[word])) && d->spell ) {
392 int para, index;
393 textEdit()->getCursorPosition( &para, &index );
394 ++d->wordCount;
395 dict->replace( word, Unknown );
396 ++d->checksRequested;
397 if (currentParagraph() != para)
398 d->completeRehighlightRequired = true;
399 d->spellTimeout->start( tenSeconds, true );
400 d->spell->checkWord( word, false );
401 }
402 return false;
403}
404
405bool K3SpellingHighlighter::intraWordEditing() const
406{
407 return d->intraWordEditing;
408}
409
410void K3SpellingHighlighter::setIntraWordEditing( bool editing )
411{
412 d->intraWordEditing = editing;
413}
414
415void K3DictSpellingHighlighter::slotMisspelling (const QString &originalWord, const QStringList &suggestions,
416 unsigned int pos)
417{
418 Q_UNUSED( suggestions );
419 // kDebug() << suggestions.join( " " ).toLatin1();
420 if ( d->globalConfig )
421 d->sDict()->replace( originalWord, NotOkay );
422 else
423 d->mDict->replace( originalWord, NotOkay );
424
425 //Emit this baby so that apps that want to have suggestions in a popup over
426 //the misspelled word can catch them.
427 emit newSuggestions( originalWord, suggestions, pos );
428}
429
430void K3DictSpellingHighlighter::slotCorrected(const QString &word,
431 const QString &,
432 unsigned int)
433
434{
435 Q3Dict<int>* dict = ( d->globalConfig ? d->sDict() : d->mDict );
436 if ( !dict->isEmpty() && (*dict)[word] == Unknown ) {
437 dict->replace( word, Okay );
438 }
439 ++d->checksDone;
440 if (d->checksDone == d->checksRequested) {
441 d->spellTimeout->stop();
442 slotRehighlight();
443 } else {
444 d->spellTimeout->start( tenSeconds, true );
445 }
446}
447
448void K3DictSpellingHighlighter::dictionaryChanged()
449{
450 QObject *oldMonitor = K3DictSpellingHighlighterPrivate::sDictionaryMonitor;
451 K3DictSpellingHighlighterPrivate::sDictionaryMonitor = new QObject();
452 K3DictSpellingHighlighterPrivate::sDict()->clear();
453 delete oldMonitor;
454}
455
456void K3DictSpellingHighlighter::restartBackgroundSpellCheck()
457{
458 kDebug(0) << "KDictSpellingHighlighter::restartBackgroundSpellCheck()";
459 slotDictionaryChanged();
460}
461
462void K3DictSpellingHighlighter::setActive( bool active )
463{
464 if ( active == d->active )
465 return;
466
467 d->active = active;
468 rehighlight();
469 if ( d->active )
470 emit activeChanged( i18n("As-you-type spell checking enabled.") );
471 else
472 emit activeChanged( i18n("As-you-type spell checking disabled.") );
473}
474
475bool K3DictSpellingHighlighter::isActive() const
476{
477 return d->active;
478}
479
480void K3DictSpellingHighlighter::setAutomatic( bool automatic )
481{
482 if ( automatic == d->automatic )
483 return;
484
485 d->automatic = automatic;
486 if ( d->automatic )
487 slotAutoDetection();
488}
489
490bool K3DictSpellingHighlighter::automatic() const
491{
492 return d->automatic;
493}
494
495void K3DictSpellingHighlighter::slotRehighlight()
496{
497 kDebug(0) << "KDictSpellingHighlighter::slotRehighlight()";
498 if (d->completeRehighlightRequired) {
499 rehighlight();
500 } else {
501 int para, index;
502 textEdit()->getCursorPosition( &para, &index );
503 //rehighlight the current para only (undo/redo safe)
504 textEdit()->insertAt( "", para, index );
505 }
506 if (d->checksDone == d->checksRequested)
507 d->completeRehighlightRequired = false;
508 QTimer::singleShot( 0, this, SLOT(slotAutoDetection()));
509}
510
511void K3DictSpellingHighlighter::slotDictionaryChanged()
512{
513 delete d->spell;
514 d->spellReady = false;
515 d->wordCount = 0;
516 d->errorCount = 0;
517 d->autoDict.clear();
518
519 d->spell = new K3Spell( 0, i18n( "Incremental Spellcheck" ), this,
520 SLOT(slotSpellReady(K3Spell*)), d->mSpellConfig );
521}
522
523void K3DictSpellingHighlighter::slotLocalSpellConfigChanged()
524{
525 kDebug(0) << "KDictSpellingHighlighter::slotSpellConfigChanged()";
526 // the spell config has been changed, so we have to restart from scratch
527 d->mDict->clear();
528 slotDictionaryChanged();
529}
530
531QString K3DictSpellingHighlighter::spellKey()
532{
533 KGlobal::config()->reparseConfiguration();
534 KConfigGroup cg( KGlobal::config(), "K3Spell" );
535 QString key;
536 key += QString::number( cg.readEntry( "K3Spell_NoRootAffix", QVariant(0 )).toInt());
537 key += '/';
538 key += QString::number( cg.readEntry( "K3Spell_RunTogether", QVariant(0 )).toInt());
539 key += '/';
540 key += cg.readEntry( "K3Spell_Dictionary", "" );
541 key += '/';
542 key += QString::number( cg.readEntry( "K3Spell_DictFromList", QVariant(false )).toInt());
543 key += '/';
544 key += QString::number( cg.readEntry( "K3Spell_Encoding", QVariant(KS_E_ASCII )).toInt());
545 key += '/';
546 key += QString::number( cg.readEntry( "K3Spell_Client", QVariant(KS_CLIENT_ISPELL )).toInt());
547 return key;
548}
549
550
551// Automatic spell checking support
552// In auto spell checking mode disable as-you-type spell checking
553// iff more than one third of words are spelt incorrectly.
554//
555// Words in the signature and reply prefix are ignored.
556// Only unique words are counted.
557
558void K3DictSpellingHighlighter::slotAutoDetection()
559{
560 if ( !d->autoReady )
561 return;
562
563 bool savedActive = d->active;
564
565 if ( d->automatic ) {
566 // tme = Too many errors
567 bool tme = ( d->wordCount >= d->disableWordCount ) && ( d->errorCount * 100 >= d->disablePercentage * d->wordCount );
568 if ( d->active && tme )
569 d->active = false;
570 else if ( !d->active && !tme )
571 d->active = true;
572 }
573 if ( d->active != savedActive ) {
574 if ( d->wordCount > 1 ) {
575 if ( d->active )
576 emit activeChanged( i18n("As-you-type spell checking enabled.") );
577 else
578 emit activeChanged( i18n( "Too many misspelled words. "
579 "As-you-type spell checking disabled." ) );
580 }
581 d->completeRehighlightRequired = true;
582 d->rehighlightRequest->start( 100, true );
583 }
584}
585
586void K3DictSpellingHighlighter::slotK3SpellNotResponding()
587{
588 static int retries = 0;
589 if (retries < 10) {
590 if ( d->globalConfig )
591 K3DictSpellingHighlighter::dictionaryChanged();
592 else
593 slotLocalSpellConfigChanged();
594 } else {
595 setAutomatic( false );
596 setActive( false );
597 }
598 ++retries;
599}
600
601bool K3DictSpellingHighlighter::eventFilter( QObject *o, QEvent *e)
602{
603 if (o == textEdit() && (e->type() == QEvent::FocusIn)) {
604 if ( d->globalConfig ) {
605 QString skey = spellKey();
606 if ( d->spell && d->spellKey != skey ) {
607 d->spellKey = skey;
608 K3DictSpellingHighlighter::dictionaryChanged();
609 }
610 }
611 }
612
613 if (o == textEdit() && (e->type() == QEvent::KeyPress)) {
614 QKeyEvent *k = static_cast<QKeyEvent *>(e);
615 d->autoReady = true;
616 if (d->rehighlightRequest->isActive()) // try to stay out of the users way
617 d->rehighlightRequest->start( 500 );
618 if ( k->key() == Qt::Key_Enter ||
619 k->key() == Qt::Key_Return ||
620 k->key() == Qt::Key_Up ||
621 k->key() == Qt::Key_Down ||
622 k->key() == Qt::Key_Left ||
623 k->key() == Qt::Key_Right ||
624 k->key() == Qt::Key_PageUp ||
625 k->key() == Qt::Key_PageDown ||
626 k->key() == Qt::Key_Home ||
627 k->key() == Qt::Key_End ||
628 (( k->state() & Qt::ControlModifier ) &&
629 ((k->key() == Qt::Key_A) ||
630 (k->key() == Qt::Key_B) ||
631 (k->key() == Qt::Key_E) ||
632 (k->key() == Qt::Key_N) ||
633 (k->key() == Qt::Key_P))) ) {
634 if ( intraWordEditing() ) {
635 setIntraWordEditing( false );
636 d->completeRehighlightRequired = true;
637 d->rehighlightRequest->start( 500, true );
638 }
639 if (d->checksDone != d->checksRequested) {
640 // Handle possible change of paragraph while
641 // words are pending spell checking
642 d->completeRehighlightRequired = true;
643 d->rehighlightRequest->start( 500, true );
644 }
645 } else {
646 setIntraWordEditing( true );
647 }
648 if ( k->key() == Qt::Key_Space ||
649 k->key() == Qt::Key_Enter ||
650 k->key() == Qt::Key_Return ) {
651 QTimer::singleShot( 0, this, SLOT(slotAutoDetection()));
652 }
653 }
654
655 else if ( o == textEdit()->viewport() &&
656 ( e->type() == QEvent::MouseButtonPress )) {
657 d->autoReady = true;
658 if ( intraWordEditing() ) {
659 setIntraWordEditing( false );
660 d->completeRehighlightRequired = true;
661 d->rehighlightRequest->start( 0, true );
662 }
663 }
664
665 return false;
666}
667
668#include "k3syntaxhighlighter.moc"
669