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
25 #include "maillistdrag.h"
27 #include <kpimidentities/signature.h>
34 #include <KEncodingFileDialog>
37 #include <KMessageBox>
38 #include <KPushButton>
40 #include <KTemporaryFile>
42 #include <KWindowSystem>
44 #include <QApplication>
54 using namespace KPIMTextEdit
;
61 KMeditorPrivate( KMeditor
*parent
)
63 useExtEditor( false ),
64 mExtEditorProcess( 0 ),
65 mExtEditorTempFile( 0 ) {
76 // Just calls KTextEdit::ensureCursorVisible(), workaround for some bug.
77 void ensureCursorVisibleDelayed();
84 QString
addQuotesToText( const QString
&inputText
);
86 void startExternalEditor();
87 void slotEditorFinished( int, QProcess::ExitStatus exitStatus
);
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
®Exp
, const QString
&newText
,
94 const KPIMIdentities::Signature
&sig
);
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
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;
113 QString extEditorPath
;
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
133 mExtEditorTempFile
= new KTemporaryFile();
134 if ( !mExtEditorTempFile
->open() ) {
135 delete mExtEditorTempFile
;
136 mExtEditorTempFile
= 0;
137 q
->setUseExternalEditor( false );
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() );
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() ) {
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();
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();
205 else if ( e
->key() == Qt::Key_Backtab
&& e
->modifiers() == Qt::ShiftModifier
)
207 textCursor().clearSelection();
212 TextEdit::keyPressEvent( e
);
216 KMeditor::KMeditor( const QString
& text
, QWidget
*parent
)
217 : TextEdit( text
, parent
), d( new KMeditorPrivate( this ) )
222 KMeditor::KMeditor( QWidget
*parent
)
223 : TextEdit( parent
), d( new KMeditorPrivate( this ) )
228 KMeditor::~KMeditor()
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
)
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" ) );
275 KUrl url
= fdlg
.selectedUrl();
276 url
.setFileEncoding( fdlg
.selectedEncoding() );
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()
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
);
354 return q
->smartQuote( answer
);
357 QString
KMeditor::smartQuote( const QString
& msg
)
362 bool KMeditor::checkExternalEditorFinished()
364 if ( !d
->mExtEditorProcess
)
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();
375 case KMessageBox::No
:
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
)
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();
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
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() )
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() )
438 // No, cursor is not in the current block
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
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
467 QTimer::singleShot( 500, this, SLOT( ensureCursorVisibleDelayed() ) );
470 void KMeditorPrivate::cleanWhitespaceHelper( const QRegExp
®Exp
,
471 const QString
&newText
,
472 const KPIMIdentities::Signature
&sig
)
474 int currentSearchPosition
= 0;
479 QString text
= q
->document()->toPlainText();
480 int currentMatch
= regExp
.indexIn( text
, currentSearchPosition
);
481 currentSearchPosition
= currentMatch
;
482 if ( currentMatch
== -1 )
486 QTextCursor
cursor( q
->document() );
487 cursor
.setPosition( currentMatch
);
488 cursor
.movePosition( QTextCursor::NextCharacter
, QTextCursor::KeepAnchor
,
489 regExp
.matchedLength() );
492 if ( q
->isLineQuoted( cursor
.block().text() ) ) {
493 currentSearchPosition
+= regExp
.matchedLength();
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();
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;
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 )
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;
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 )
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 );
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();
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();
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();
641 #include "kmeditor.moc"