Fix typo found by Yuri Chornoivan
[kdepim.git] / libkdepim / kmeditor.cpp
blobaacf5e81ecc1a37e162f45ea6d5d2988961adaf5
1 /**
2 * kmeditor.cpp
4 * Copyright 2007 Laurent Montel <montel@kde.org>
5 * Copyright 2008 Thomas McGuire <mcguire@kde.org>
7 * This library is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU Lesser General Public
9 * License as published by the Free Software Foundation; either
10 * version 2.1 of the License, or (at your option) any later version.
12 * This library is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Lesser General Public License for more details.
17 * You should have received a copy of the GNU Lesser General Public
18 * License along with this library; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
20 * 02110-1301 USA
23 #include "kmeditor.h"
24 #include "utils.h"
25 #include "maillistdrag.h"
27 #include <kpimidentities/signature.h>
29 #include <KCharsets>
30 #include <KComboBox>
31 #include <KCursor>
32 #include <KDebug>
33 #include <KDirWatch>
34 #include <KEncodingFileDialog>
35 #include <KLocale>
36 #include <KMenu>
37 #include <KMessageBox>
38 #include <KPushButton>
39 #include <KProcess>
40 #include <KTemporaryFile>
41 #include <KToolBar>
42 #include <KWindowSystem>
44 #include <QApplication>
45 #include <QClipboard>
46 #include <QShortcut>
47 #include <QTextCodec>
48 #include <QAction>
49 #include <QProcess>
50 #include <QTimer>
52 #include <assert.h>
54 using namespace KPIMTextEdit;
56 namespace KPIM {
58 class KMeditorPrivate
60 public:
61 KMeditorPrivate( KMeditor *parent )
62 : q( parent ),
63 useExtEditor( false ),
64 mExtEditorProcess( 0 ),
65 mExtEditorTempFile( 0 ) {
68 ~KMeditorPrivate()
73 // Slots
76 // Just calls KTextEdit::ensureCursorVisible(), workaround for some bug.
77 void ensureCursorVisibleDelayed();
80 // Normal functions
83 void init();
84 QString addQuotesToText( const QString &inputText );
86 void startExternalEditor();
87 void slotEditorFinished( int, QProcess::ExitStatus exitStatus );
89 /**
90 * Replaces each text which matches the regular expression with another text.
91 * Text inside quotes or the given signature will be ignored.
93 void cleanWhitespaceHelper( const QRegExp &regExp, const QString &newText,
94 const KPIMIdentities::Signature &sig );
96 /**
97 * Returns a list of all occurences of the given signature.
98 * The list contains pairs which consists of the starting position and the end
99 * of the signature.
101 * @param sig this signature will be searched for
102 * @return a list of pairs of start and end positions of the signature
104 QList< QPair<int,int> > signaturePositions( const KPIMIdentities::Signature &sig ) const;
107 * Returns the text of the signature. If the signature is HTML, the HTML
108 * tags will be stripped.
110 QString plainSignatureText( const KPIMIdentities::Signature &signature ) const;
112 // Data members
113 QString extEditorPath;
114 KMeditor *q;
115 bool useExtEditor;
117 KProcess *mExtEditorProcess;
118 KTemporaryFile *mExtEditorTempFile;
123 using namespace KPIM;
125 void KMeditorPrivate::startExternalEditor()
127 if ( extEditorPath.isEmpty() ) {
128 q->setUseExternalEditor( false );
129 //TODO: show messagebox
130 return;
133 mExtEditorTempFile = new KTemporaryFile();
134 if ( !mExtEditorTempFile->open() ) {
135 delete mExtEditorTempFile;
136 mExtEditorTempFile = 0;
137 q->setUseExternalEditor( false );
138 return;
141 mExtEditorTempFile->write( q->textOrHtml().toUtf8() );
142 mExtEditorTempFile->flush();
144 mExtEditorProcess = new KProcess();
145 // construct command line...
146 QStringList command = extEditorPath.split( ' ', QString::SkipEmptyParts );
147 bool filenameAdded = false;
148 for ( QStringList::Iterator it = command.begin(); it != command.end(); ++it ) {
149 if ( ( *it ).contains( "%f" ) ) {
150 ( *it ).replace( QRegExp( "%f" ), mExtEditorTempFile->fileName() );
151 filenameAdded=true;
153 ( *mExtEditorProcess ) << ( *it );
155 if ( !filenameAdded ) // no %f in the editor command
156 ( *mExtEditorProcess ) << mExtEditorTempFile->fileName();
158 QObject::connect( mExtEditorProcess, SIGNAL( finished ( int, QProcess::ExitStatus ) ),
159 q, SLOT( slotEditorFinished( int, QProcess::ExitStatus ) ) );
160 mExtEditorProcess->start();
161 if ( !mExtEditorProcess->waitForStarted() ) {
162 //TODO: messagebox
163 mExtEditorProcess->deleteLater();
164 mExtEditorProcess = 0;
165 delete mExtEditorTempFile;
166 mExtEditorTempFile = 0;
167 q->setUseExternalEditor( false );
171 void KMeditorPrivate::slotEditorFinished(int, QProcess::ExitStatus exitStatus)
173 if ( exitStatus == QProcess::NormalExit ) {
174 mExtEditorTempFile->flush();
175 mExtEditorTempFile->seek( 0 );
176 QByteArray f = mExtEditorTempFile->readAll();
177 q->setTextOrHtml( QString::fromUtf8( f.data(), f.size() ) );
180 q->killExternalEditor(); // cleanup...
184 void KMeditorPrivate::ensureCursorVisibleDelayed()
186 static_cast<KPIMTextEdit::TextEdit*>( q )->ensureCursorVisible();
189 void KMeditor::keyPressEvent ( QKeyEvent * e )
191 if ( d->useExtEditor ) {
192 if ( !d->mExtEditorProcess ) {
193 d->startExternalEditor();
195 return;
198 if ( e->key() == Qt::Key_Up && e->modifiers() != Qt::ShiftModifier &&
199 textCursor().block().position() == 0 &&
200 textCursor().position() < textCursor().block().layout()->lineAt( 0 ).textLength() )
202 textCursor().clearSelection();
203 emit focusUp();
205 else if ( e->key() == Qt::Key_Backtab && e->modifiers() == Qt::ShiftModifier )
207 textCursor().clearSelection();
208 emit focusUp();
210 else
212 TextEdit::keyPressEvent( e );
216 KMeditor::KMeditor( const QString& text, QWidget *parent )
217 : TextEdit( text, parent ), d( new KMeditorPrivate( this ) )
219 d->init();
222 KMeditor::KMeditor( QWidget *parent )
223 : TextEdit( parent ), d( new KMeditorPrivate( this ) )
225 d->init();
228 KMeditor::~KMeditor()
230 delete d;
233 void KMeditorPrivate::init()
235 QShortcut * insertMode = new QShortcut( QKeySequence( Qt::Key_Insert ), q );
236 q->connect( insertMode, SIGNAL( activated() ),
237 q, SLOT( slotChangeInsertMode() ) );
240 void KMeditor::slotChangeInsertMode()
242 setOverwriteMode( !overwriteMode() );
243 emit insertModeChanged();
246 void KMeditor::setUseExternalEditor( bool use )
248 d->useExtEditor = use;
251 void KMeditor::setExternalEditorPath( const QString & path )
253 d->extEditorPath = path;
256 void KMeditor::setFontForWholeText( const QFont &font )
258 QTextCharFormat fmt;
259 fmt.setFont( font );
260 QTextCursor cursor( document() );
261 cursor.movePosition( QTextCursor::End, QTextCursor::KeepAnchor );
262 cursor.mergeCharFormat( fmt );
263 document()->setDefaultFont( font );
266 KUrl KMeditor::insertFile()
268 KEncodingFileDialog fdlg( QString(), QString(), QString(), QString(),
269 KFileDialog::Opening, this );
270 fdlg.okButton()->setText( i18n( "&Insert" ) );
271 fdlg.setCaption( i18n( "Insert File" ) );
272 if ( !fdlg.exec() )
273 return KUrl();
274 else {
275 KUrl url = fdlg.selectedUrl();
276 url.setFileEncoding( fdlg.selectedEncoding() );
277 return url;
281 void KMeditor::enableWordWrap( int wrapColumn )
283 setWordWrapMode( QTextOption::WordWrap );
284 setLineWrapMode( QTextEdit::FixedColumnWidth );
285 setLineWrapColumnOrWidth( wrapColumn );
288 void KMeditor::disableWordWrap()
290 setLineWrapMode( QTextEdit::WidgetWidth );
293 void KMeditor::slotPasteAsQuotation()
295 if ( hasFocus() ) {
296 QString s = QApplication::clipboard()->text();
297 if ( !s.isEmpty() ) {
298 insertPlainText( d->addQuotesToText( s ) );
303 void KMeditor::slotRemoveQuotes()
305 QTextCursor cursor = textCursor();
306 cursor.beginEditBlock();
307 if ( !cursor.hasSelection() )
308 cursor.select( QTextCursor::Document );
310 QTextBlock block = document()->findBlock( cursor.selectionStart() );
311 int selectionEnd = cursor.selectionEnd();
312 while ( block.isValid() && block.position() <= selectionEnd ) {
313 cursor.setPosition( block.position() );
314 if ( isLineQuoted( block.text() ) ) {
315 int length = quoteLength( block.text() );
316 cursor.movePosition( QTextCursor::NextCharacter, QTextCursor::KeepAnchor, length );
317 cursor.removeSelectedText();
318 selectionEnd -= length;
320 block = block.next();
322 cursor.clearSelection();
323 cursor.endEditBlock();
326 void KMeditor::slotAddQuotes()
328 QTextCursor cursor = textCursor();
329 cursor.beginEditBlock();
330 if ( !cursor.hasSelection() )
331 cursor.select( QTextCursor::Document );
333 QTextBlock block = document()->findBlock( cursor.selectionStart() );
334 int selectionEnd = cursor.selectionEnd();
335 while ( block.isValid() && block.position() <= selectionEnd ) {
336 cursor.setPosition( block.position() );
337 cursor.insertText( defaultQuoteSign() );
338 selectionEnd += defaultQuoteSign().length();
339 block = block.next();
341 cursor.clearSelection();
342 cursor.endEditBlock();
345 QString KMeditorPrivate::addQuotesToText( const QString &inputText )
347 QString answer = QString( inputText );
348 QString indentStr = q->defaultQuoteSign();
349 answer.replace( '\n', '\n' + indentStr );
350 //cursor.selectText() as QChar::ParagraphSeparator as paragraph separator.
351 answer.replace( QChar::ParagraphSeparator, '\n' + indentStr );
352 answer.prepend( indentStr );
353 answer += '\n';
354 return q->smartQuote( answer );
357 QString KMeditor::smartQuote( const QString & msg )
359 return msg;
362 bool KMeditor::checkExternalEditorFinished()
364 if ( !d->mExtEditorProcess )
365 return true;
366 switch ( KMessageBox::warningYesNoCancel( topLevelWidget(),
367 i18n("The external editor is still running.\n"
368 "Abort the external editor or leave it open?"),
369 i18n("External Editor"),
370 KGuiItem( i18n("Abort Editor") ),
371 KGuiItem( i18n("Leave Editor Open") ) ) ) {
372 case KMessageBox::Yes:
373 killExternalEditor();
374 return true;
375 case KMessageBox::No:
376 return true;
377 default:
378 return false;
382 void KMeditor::killExternalEditor()
384 if ( d->mExtEditorProcess )
385 d->mExtEditorProcess->deleteLater();
386 d->mExtEditorProcess = 0;
387 delete d->mExtEditorTempFile;
388 d->mExtEditorTempFile = 0;
391 void KMeditor::setCursorPositionFromStart( unsigned int pos )
393 if ( pos > 0 )
395 QTextCursor cursor = textCursor();
396 cursor.setPosition( pos );
397 setTextCursor( cursor );
398 ensureCursorVisible();
402 int KMeditor::linePosition()
404 const QTextCursor cursor = textCursor();
405 const QTextDocument* doc = document();
406 QTextBlock block = doc->begin();
407 int lineCount = 0;
409 // Simply using cursor.block.blockNumber() would not work since that does not
410 // take word-wrapping into account, i.e. it is possible to have more than one line
411 // in a block.
413 // What we have to do therefore is to iterate over the blocks and count the lines
414 // in them. Once we have reached the block where the cursor is, we have to iterate
415 // over each line in it, to find the exact line in the block where the cursor is.
416 while ( block.isValid() ) {
417 const QTextLayout *layout = block.layout();
419 // If the current block has the cursor in it, iterate over all its lines
420 if ( block == cursor.block() ) {
422 // Special case: Cursor at end of single non-wrapped line, exit early
423 // in this case as the logic below can't handle it
424 if ( block.lineCount() == layout->lineCount() )
425 return lineCount;
427 const int cursorBasePosition = cursor.position() - block.position();
428 for ( int i = 0; i < layout->lineCount(); i++ ) {
429 QTextLine line = layout->lineAt( i );
430 if ( cursorBasePosition >= line.textStart() &&
431 cursorBasePosition < line.textStart() + line.textLength() )
432 break;
433 lineCount++;
435 return lineCount;
438 // No, cursor is not in the current block
439 else
440 lineCount += layout->lineCount();
442 block = block.next();
445 // Only gets here if the cursor block can't be found, shouldn't happen except
446 // for an empty document maybe
447 return lineCount;
450 int KMeditor::columnNumber()
452 QTextCursor cursor = textCursor();
453 return cursor.columnNumber();
456 void KMeditor::ensureCursorVisible()
458 QCoreApplication::processEvents();
460 // Hack: In KMail, the layout of the composer changes again after
461 // creating the editor (the toolbar/menubar creation is delayed), so
462 // the size of the editor changes as well, possibly hiding the cursor
463 // even though we called ensureCursorVisible() before the layout phase.
465 // Delay the actual call to ensureCursorVisible() a bit to work around
466 // the problem.
467 QTimer::singleShot( 500, this, SLOT( ensureCursorVisibleDelayed() ) );
470 void KMeditorPrivate::cleanWhitespaceHelper( const QRegExp &regExp,
471 const QString &newText,
472 const KPIMIdentities::Signature &sig )
474 int currentSearchPosition = 0;
476 forever {
478 // Find the text
479 QString text = q->document()->toPlainText();
480 int currentMatch = regExp.indexIn( text, currentSearchPosition );
481 currentSearchPosition = currentMatch;
482 if ( currentMatch == -1 )
483 break;
485 // Select the text
486 QTextCursor cursor( q->document() );
487 cursor.setPosition( currentMatch );
488 cursor.movePosition( QTextCursor::NextCharacter, QTextCursor::KeepAnchor,
489 regExp.matchedLength() );
491 // Skip quoted text
492 if ( q->isLineQuoted( cursor.block().text() ) ) {
493 currentSearchPosition += regExp.matchedLength();
494 continue;
497 // Skip text inside signatures
498 bool insideSignature = false;
499 QList< QPair<int,int> > sigPositions = signaturePositions( sig );
500 QPair<int,int> position;
501 foreach( position, sigPositions ) { //krazy:exclude=foreach
502 if ( cursor.position() >= position.first &&
503 cursor.position() <= position.second )
504 insideSignature = true;
506 if ( insideSignature ) {
507 currentSearchPosition += regExp.matchedLength();
508 continue;
511 // Replace the text
512 cursor.removeSelectedText();
513 cursor.insertText( newText );
514 currentSearchPosition += newText.length();
518 void KMeditor::cleanWhitespace( const KPIMIdentities::Signature &sig )
520 QTextCursor cursor( document() );
521 cursor.beginEditBlock();
523 // Squeeze tabs and spaces
524 d->cleanWhitespaceHelper( QRegExp( QLatin1String( "[\t ]+" ) ),
525 QString( QLatin1Char( ' ' ) ), sig );
527 // Remove trailing whitespace
528 d->cleanWhitespaceHelper( QRegExp( QLatin1String( "[\t ][\n]" ) ),
529 QString( QLatin1Char( '\n' ) ), sig );
531 // Single space lines
532 d->cleanWhitespaceHelper( QRegExp( QLatin1String( "[\n]{3,}" ) ),
533 QLatin1String( "\n\n" ), sig );
535 if ( !textCursor().hasSelection() ) {
536 textCursor().clearSelection();
539 cursor.endEditBlock();
542 QList< QPair<int,int> > KMeditorPrivate::signaturePositions( const KPIMIdentities::Signature &sig ) const
544 QList< QPair<int,int> > signaturePositions;
545 if ( !sig.rawText().isEmpty() ) {
547 QString sigText = plainSignatureText( sig );
549 int currentSearchPosition = 0;
550 forever {
552 // Find the next occurrence of the signature text
553 QString text = q->document()->toPlainText();
554 int currentMatch = text.indexOf( sigText, currentSearchPosition );
555 currentSearchPosition = currentMatch + sigText.length();
556 if ( currentMatch == -1 )
557 break;
559 signaturePositions.append( QPair<int,int>( currentMatch,
560 currentMatch + sigText.length() ) );
563 return signaturePositions;
567 void KMeditor::replaceSignature( const KPIMIdentities::Signature &oldSig,
568 const KPIMIdentities::Signature &newSig )
570 QTextCursor cursor( document() );
571 cursor.beginEditBlock();
573 QString oldSigText = d->plainSignatureText( oldSig );
575 int currentSearchPosition = 0;
576 forever {
578 // Find the next occurrence of the signature text
579 QString text = document()->toPlainText();
580 int currentMatch = text.indexOf( oldSigText, currentSearchPosition );
581 currentSearchPosition = currentMatch;
582 if ( currentMatch == -1 )
583 break;
585 // Select the signature
586 QTextCursor cursor( document() );
587 cursor.setPosition( currentMatch );
589 // If the new signature is completely empty, we also want to remove the
590 // signature separator, so include it in the selection
591 int additionalMove = 0;
592 if ( newSig.rawText().isEmpty() &&
593 text.mid( currentMatch - 4, 4 ) == QLatin1String( "-- \n" ) ) {
594 cursor.movePosition( QTextCursor::PreviousCharacter,
595 QTextCursor::MoveAnchor, 4 );
596 additionalMove = 4;
598 cursor.movePosition( QTextCursor::NextCharacter, QTextCursor::KeepAnchor,
599 oldSigText.length() + additionalMove );
602 // Skip quoted signatures
603 if ( isLineQuoted( cursor.block().text() ) ) {
604 currentSearchPosition += d->plainSignatureText( oldSig ).length();
605 continue;
608 // Remove the old and instert the new signature
609 cursor.removeSelectedText();
610 if ( newSig.isInlinedHtml() &&
611 newSig.type() == KPIMIdentities::Signature::Inlined ) {
612 cursor.insertHtml( newSig.rawText() );
613 enableRichTextMode();
615 else
616 cursor.insertText( newSig.rawText() );
618 currentSearchPosition += d->plainSignatureText( newSig ).length();
621 cursor.endEditBlock();
625 QString KMeditorPrivate::plainSignatureText( const KPIMIdentities::Signature &signature ) const
627 QString sigText = signature.rawText();
628 if ( signature.isInlinedHtml() &&
629 signature.type() == KPIMIdentities::Signature::Inlined ) {
631 // Use a QTextDocument as a helper, it does all the work for us and
632 // strips all HTML tags.
633 QTextDocument helper;
634 QTextCursor helperCursor( &helper );
635 helperCursor.insertHtml( sigText );
636 sigText = helper.toPlainText();
638 return sigText;
641 #include "kmeditor.moc"