krop's commit fixes my problem in a better way, reverting
[kdepim.git] / libkdepim / kmeditor.cpp
blob7fcd874a4f5e1b8b6f0325dde1eb1791da710cd0
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"
25 #include <KEncodingFileDialog>
26 #include <KLocale>
27 #include <KMessageBox>
28 #include <KProcess>
29 #include <KPushButton>
30 #include <KTemporaryFile>
32 #include <QApplication>
33 #include <QClipboard>
34 #include <QPointer>
35 #include <QProcess>
36 #include <QShortcut>
37 #include <QTimer>
39 using namespace KPIMTextEdit;
41 namespace KPIM {
43 class KMeditorPrivate
45 public:
46 KMeditorPrivate( KMeditor *parent )
47 : q( parent ),
48 useExtEditor( false ),
49 mExtEditorProcess( 0 ),
50 mExtEditorTempFile( 0 ) {
53 ~KMeditorPrivate()
58 // Slots
61 // Just calls KTextEdit::ensureCursorVisible(), workaround for some bug.
62 void ensureCursorVisibleDelayed();
65 // Normal functions
68 void init();
69 QString addQuotesToText( const QString &inputText );
71 void startExternalEditor();
72 void slotEditorFinished( int, QProcess::ExitStatus exitStatus );
74 /**
75 * Replaces each text which matches the regular expression with another text.
76 * Text inside quotes or the given signature will be ignored.
78 void cleanWhitespaceHelper( const QRegExp &regExp, const QString &newText,
79 const KPIMIdentities::Signature &sig );
81 /**
82 * Returns a list of all occurrences of the given signature.
83 * The list contains pairs which consists of the starting position and
84 * the end of the signature.
86 * @param sig this signature will be searched for
87 * @return a list of pairs of start and end positions of the signature
89 QList< QPair<int,int> > signaturePositions( const KPIMIdentities::Signature &sig ) const;
91 // Data members
92 QString extEditorPath;
93 KMeditor *q;
94 bool useExtEditor;
96 KProcess *mExtEditorProcess;
97 KTemporaryFile *mExtEditorTempFile;
102 using namespace KPIM;
104 void KMeditorPrivate::startExternalEditor()
106 if ( extEditorPath.isEmpty() ) {
107 q->setUseExternalEditor( false );
108 //TODO: show messagebox
109 return;
112 mExtEditorTempFile = new KTemporaryFile();
113 if ( !mExtEditorTempFile->open() ) {
114 delete mExtEditorTempFile;
115 mExtEditorTempFile = 0;
116 q->setUseExternalEditor( false );
117 return;
120 mExtEditorTempFile->write( q->textOrHtml().toUtf8() );
121 mExtEditorTempFile->close();
123 mExtEditorProcess = new KProcess();
124 // construct command line...
125 QStringList command = extEditorPath.split( ' ', QString::SkipEmptyParts );
126 bool filenameAdded = false;
127 for ( QStringList::Iterator it = command.begin(); it != command.end(); ++it ) {
128 if ( ( *it ).contains( "%f" ) ) {
129 ( *it ).replace( QRegExp( "%f" ), mExtEditorTempFile->fileName() );
130 filenameAdded = true;
132 else if ( ( *it ).contains( "%l" ) ) {
133 ( *it ).replace( QRegExp( "%l" ), QString::number(q->textCursor().blockNumber() + 1) ); // line number
135 ( *mExtEditorProcess ) << ( *it );
137 if ( !filenameAdded ) { // no %f in the editor command
138 ( *mExtEditorProcess ) << mExtEditorTempFile->fileName();
141 QObject::connect( mExtEditorProcess, SIGNAL(finished(int,QProcess::ExitStatus)),
142 q, SLOT(slotEditorFinished(int,QProcess::ExitStatus)) );
143 mExtEditorProcess->start();
144 if ( !mExtEditorProcess->waitForStarted() ) {
145 //TODO: messagebox
146 mExtEditorProcess->deleteLater();
147 mExtEditorProcess = 0;
148 delete mExtEditorTempFile;
149 mExtEditorTempFile = 0;
150 q->setUseExternalEditor( false );
154 void KMeditorPrivate::slotEditorFinished( int, QProcess::ExitStatus exitStatus )
156 if ( exitStatus == QProcess::NormalExit ) {
157 // the external editor could have renamed the original file and recreated a new file
158 // with the given filename, so we need to reopen the file after the editor exited
159 QFile localFile(mExtEditorTempFile->fileName());
160 if ( localFile.open(QIODevice::ReadOnly | QIODevice::Text) ) {
161 QByteArray f = localFile.readAll();
162 q->setTextOrHtml( QString::fromUtf8( f.data(), f.size() ) );
163 q->document()->setModified( true );
164 localFile.close();
168 q->killExternalEditor(); // cleanup...
171 void KMeditorPrivate::ensureCursorVisibleDelayed()
173 static_cast<KPIMTextEdit::TextEdit*>( q )->ensureCursorVisible();
176 void KMeditor::keyPressEvent ( QKeyEvent *e )
178 if ( d->useExtEditor &&
179 ( e->key() != Qt::Key_Shift ) &&
180 ( e->key() != Qt::Key_Control ) &&
181 ( e->key() != Qt::Key_Meta ) &&
182 ( e->key() != Qt::Key_CapsLock ) &&
183 ( e->key() != Qt::Key_NumLock ) &&
184 ( e->key() != Qt::Key_ScrollLock ) &&
185 ( e->key() != Qt::Key_Alt ) &&
186 ( e->key() != Qt::Key_AltGr ) ) {
187 if ( !d->mExtEditorProcess ) {
188 d->startExternalEditor();
190 return;
193 if ( e->key() == Qt::Key_Up && e->modifiers() != Qt::ShiftModifier &&
194 textCursor().block().position() == 0 &&
195 textCursor().block().layout()->lineForTextPosition( textCursor().position() ).lineNumber() == 0 ) {
196 textCursor().clearSelection();
197 emit focusUp();
198 } else if ( e->key() == Qt::Key_Backtab && e->modifiers() == Qt::ShiftModifier ) {
199 textCursor().clearSelection();
200 emit focusUp();
201 } else {
202 TextEdit::keyPressEvent( e );
206 KMeditor::KMeditor( const QString &text, QWidget *parent )
207 : TextEdit( text, parent ), d( new KMeditorPrivate( this ) )
209 d->init();
212 KMeditor::KMeditor( QWidget *parent )
213 : TextEdit( parent ), d( new KMeditorPrivate( this ) )
215 d->init();
218 KMeditor::~KMeditor()
220 delete d;
223 void KMeditorPrivate::init()
225 QShortcut * insertMode = new QShortcut( QKeySequence( Qt::Key_Insert ), q );
226 q->connect( insertMode, SIGNAL(activated()),
227 q, SLOT(slotChangeInsertMode()) );
230 void KMeditor::slotChangeInsertMode()
232 setOverwriteMode( !overwriteMode() );
233 emit insertModeChanged();
236 void KMeditor::setUseExternalEditor( bool use )
238 d->useExtEditor = use;
241 void KMeditor::setExternalEditorPath( const QString &path )
243 d->extEditorPath = path;
246 void KMeditor::setFontForWholeText( const QFont &font )
248 QTextCharFormat fmt;
249 fmt.setFont( font );
250 QTextCursor cursor( document() );
251 cursor.movePosition( QTextCursor::End, QTextCursor::KeepAnchor );
252 cursor.mergeCharFormat( fmt );
253 document()->setDefaultFont( font );
256 KUrl KMeditor::insertFile()
258 QPointer<KEncodingFileDialog> fdlg =
259 new KEncodingFileDialog( QString(), QString(), QString(), QString(),
260 KFileDialog::Opening, this );
261 fdlg->okButton()->setText( i18nc( "@action:button", "&Insert" ) );
262 fdlg->setCaption( i18nc( "@title:window", "Insert File" ) );
264 KUrl url;
265 if ( fdlg->exec() ) {
266 url = fdlg->selectedUrl();
267 url.setFileEncoding( fdlg->selectedEncoding() );
269 delete fdlg;
270 return url;
273 void KMeditor::enableWordWrap( int wrapColumn )
275 setWordWrapMode( QTextOption::WordWrap );
276 setLineWrapMode( QTextEdit::FixedColumnWidth );
277 setLineWrapColumnOrWidth( wrapColumn );
280 void KMeditor::disableWordWrap()
282 setLineWrapMode( QTextEdit::WidgetWidth );
285 void KMeditor::slotPasteAsQuotation()
287 if ( hasFocus() ) {
288 QString s = QApplication::clipboard()->text();
289 if ( !s.isEmpty() ) {
290 insertPlainText( d->addQuotesToText( s ) );
295 void KMeditor::slotRemoveQuotes()
297 QTextCursor cursor = textCursor();
298 cursor.beginEditBlock();
299 if ( !cursor.hasSelection() ) {
300 cursor.select( QTextCursor::Document );
303 QTextBlock block = document()->findBlock( cursor.selectionStart() );
304 int selectionEnd = cursor.selectionEnd();
305 while ( block.isValid() && block.position() <= selectionEnd ) {
306 cursor.setPosition( block.position() );
307 if ( isLineQuoted( block.text() ) ) {
308 int length = quoteLength( block.text() );
309 cursor.movePosition( QTextCursor::NextCharacter, QTextCursor::KeepAnchor, length );
310 cursor.removeSelectedText();
311 selectionEnd -= length;
313 block = block.next();
315 cursor.clearSelection();
316 cursor.endEditBlock();
319 void KMeditor::slotAddQuotes()
321 QTextCursor cursor = textCursor();
322 cursor.beginEditBlock();
323 if ( !cursor.hasSelection() ) {
324 cursor.select( QTextCursor::Document );
327 QTextBlock block = document()->findBlock( cursor.selectionStart() );
328 int selectionEnd = cursor.selectionEnd();
329 while ( block.isValid() && block.position() <= selectionEnd ) {
330 cursor.setPosition( block.position() );
331 cursor.insertText( defaultQuoteSign() );
332 selectionEnd += defaultQuoteSign().length();
333 block = block.next();
335 cursor.clearSelection();
336 cursor.endEditBlock();
339 QString KMeditorPrivate::addQuotesToText( const QString &inputText )
341 QString answer = QString( inputText );
342 QString indentStr = q->defaultQuoteSign();
343 answer.replace( '\n', '\n' + indentStr );
344 //cursor.selectText() as QChar::ParagraphSeparator as paragraph separator.
345 answer.replace( QChar::ParagraphSeparator, '\n' + indentStr );
346 answer.prepend( indentStr );
347 answer += '\n';
348 return q->smartQuote( answer );
351 QString KMeditor::smartQuote( const QString &msg )
353 return msg;
356 bool KMeditor::checkExternalEditorFinished()
358 if ( !d->mExtEditorProcess ) {
359 return true;
362 int ret = KMessageBox::warningYesNoCancel(
363 topLevelWidget(),
364 i18nc( "@info",
365 "The external editor is still running.<nl>"
366 "Do you want to stop the editor or keep it running?</nl>"
367 "<warning>Stopping the editor will cause all your "
368 "unsaved changes to be lost!</warning>" ),
369 i18nc( "@title:window", "External Editor Running" ),
370 KGuiItem( i18nc( "@action:button", "Stop Editor" ) ),
371 KGuiItem( i18nc( "@action:button", "Keep Editor Running" ) ) );
373 switch( ret ) {
374 case KMessageBox::Yes:
375 killExternalEditor();
376 return true;
377 case KMessageBox::No:
378 return true;
379 default:
380 return false;
384 void KMeditor::killExternalEditor()
386 if ( d->mExtEditorProcess ) {
387 d->mExtEditorProcess->deleteLater();
389 d->mExtEditorProcess = 0;
390 delete d->mExtEditorTempFile;
391 d->mExtEditorTempFile = 0;
394 void KMeditor::setCursorPositionFromStart( unsigned int pos )
396 if ( pos > 0 ) {
397 QTextCursor cursor = textCursor();
398 cursor.setPosition( pos );
399 setTextCursor( cursor );
400 ensureCursorVisible();
404 int KMeditor::linePosition()
406 const QTextCursor cursor = textCursor();
407 const QTextDocument *doc = document();
408 QTextBlock block = doc->begin();
409 int lineCount = 0;
411 // Simply using cursor.block.blockNumber() would not work since that does not
412 // take word-wrapping into account, i.e. it is possible to have more than one
413 // line in a block.
415 // What we have to do therefore is to iterate over the blocks and count the
416 // lines in them. Once we have reached the block where the cursor is, we have
417 // to iterate over each line in it, to find the exact line in the block where
418 // the cursor is.
419 while ( block.isValid() ) {
420 const QTextLayout *layout = block.layout();
422 // If the current block has the cursor in it, iterate over all its lines
423 if ( block == cursor.block() ) {
425 // Special case: Cursor at end of single non-wrapped line, exit early
426 // in this case as the logic below can't handle it
427 if ( block.lineCount() == layout->lineCount() ) {
428 return lineCount;
431 const int cursorBasePosition = cursor.position() - block.position();
432 for ( int i = 0; i < layout->lineCount(); i++ ) {
433 QTextLine line = layout->lineAt( i );
434 if ( cursorBasePosition >= line.textStart() &&
435 cursorBasePosition < line.textStart() + line.textLength() ) {
436 break;
438 lineCount++;
440 return lineCount;
441 } else {
442 // No, cursor is not in the current block
443 lineCount += layout->lineCount();
446 block = block.next();
449 // Only gets here if the cursor block can't be found, shouldn't happen except
450 // for an empty document maybe
451 return lineCount;
454 int KMeditor::columnNumber()
456 QTextCursor cursor = textCursor();
457 return cursor.columnNumber();
460 void KMeditor::ensureCursorVisible()
462 QCoreApplication::processEvents();
464 // Hack: In KMail, the layout of the composer changes again after
465 // creating the editor (the toolbar/menubar creation is delayed), so
466 // the size of the editor changes as well, possibly hiding the cursor
467 // even though we called ensureCursorVisible() before the layout phase.
469 // Delay the actual call to ensureCursorVisible() a bit to work around
470 // the problem.
471 QTimer::singleShot( 500, this, SLOT( ensureCursorVisibleDelayed() ) );
474 void KMeditorPrivate::cleanWhitespaceHelper( const QRegExp &regExp,
475 const QString &newText,
476 const KPIMIdentities::Signature &sig )
478 int currentSearchPosition = 0;
480 forever {
482 // Find the text
483 QString text = q->document()->toPlainText();
484 int currentMatch = regExp.indexIn( text, currentSearchPosition );
485 currentSearchPosition = currentMatch;
486 if ( currentMatch == -1 ) {
487 break;
490 // Select the text
491 QTextCursor cursor( q->document() );
492 cursor.setPosition( currentMatch );
493 cursor.movePosition( QTextCursor::NextCharacter, QTextCursor::KeepAnchor,
494 regExp.matchedLength() );
496 // Skip quoted text
497 if ( q->isLineQuoted( cursor.block().text() ) ) {
498 currentSearchPosition += regExp.matchedLength();
499 continue;
502 // Skip text inside signatures
503 bool insideSignature = false;
504 QList< QPair<int,int> > sigPositions = signaturePositions( sig );
505 QPair<int,int> position;
506 foreach ( position, sigPositions ) { //krazy:exclude=foreach
507 if ( cursor.position() >= position.first &&
508 cursor.position() <= position.second ) {
509 insideSignature = true;
512 if ( insideSignature ) {
513 currentSearchPosition += regExp.matchedLength();
514 continue;
517 // Replace the text
518 cursor.removeSelectedText();
519 cursor.insertText( newText );
520 currentSearchPosition += newText.length();
524 void KMeditor::cleanWhitespace( const KPIMIdentities::Signature &sig )
526 QTextCursor cursor( document() );
527 cursor.beginEditBlock();
529 // Squeeze tabs and spaces
530 d->cleanWhitespaceHelper( QRegExp( QLatin1String( "[\t ]+" ) ),
531 QString( QLatin1Char( ' ' ) ), sig );
533 // Remove trailing whitespace
534 d->cleanWhitespaceHelper( QRegExp( QLatin1String( "[\t ][\n]" ) ),
535 QString( QLatin1Char( '\n' ) ), sig );
537 // Single space lines
538 d->cleanWhitespaceHelper( QRegExp( QLatin1String( "[\n]{3,}" ) ),
539 QLatin1String( "\n\n" ), sig );
541 if ( !textCursor().hasSelection() ) {
542 textCursor().clearSelection();
545 cursor.endEditBlock();
548 QList< QPair<int,int> >
549 KMeditorPrivate::signaturePositions( const KPIMIdentities::Signature &sig ) const
551 QList< QPair<int,int> > signaturePositions;
552 if ( !sig.rawText().isEmpty() ) {
554 QString sigText = sig.plainText();
556 int currentSearchPosition = 0;
557 forever {
559 // Find the next occurrence of the signature text
560 QString text = q->document()->toPlainText();
561 int currentMatch = text.indexOf( sigText, currentSearchPosition );
562 currentSearchPosition = currentMatch + sigText.length();
563 if ( currentMatch == -1 ) {
564 break;
567 signaturePositions.append( QPair<int,int>( currentMatch,
568 currentMatch + sigText.length() ) );
571 return signaturePositions;
574 void KMeditor::replaceSignature( const KPIMIdentities::Signature &oldSig,
575 const KPIMIdentities::Signature &newSig )
577 QTextCursor cursor( document() );
578 cursor.beginEditBlock();
580 QString oldSigText = oldSig.plainText();
582 int currentSearchPosition = 0;
583 forever {
585 // Find the next occurrence of the signature text
586 QString text = document()->toPlainText();
587 int currentMatch = text.indexOf( oldSigText, currentSearchPosition );
588 currentSearchPosition = currentMatch;
589 if ( currentMatch == -1 ) {
590 break;
593 // Select the signature
594 QTextCursor cursor( document() );
595 cursor.setPosition( currentMatch );
597 // If the new signature is completely empty, we also want to remove the
598 // signature separator, so include it in the selection
599 int additionalMove = 0;
600 if ( newSig.rawText().isEmpty() &&
601 text.mid( currentMatch - 4, 4 ) == QLatin1String( "-- \n" ) ) {
602 cursor.movePosition( QTextCursor::PreviousCharacter,
603 QTextCursor::MoveAnchor, 4 );
604 additionalMove = 4;
606 cursor.movePosition( QTextCursor::NextCharacter, QTextCursor::KeepAnchor,
607 oldSigText.length() + additionalMove );
609 // Skip quoted signatures
610 if ( isLineQuoted( cursor.block().text() ) ) {
611 currentSearchPosition += oldSig.plainText().length();
612 continue;
615 // Remove the old and instert the new signature
616 cursor.removeSelectedText();
617 setTextCursor( cursor );
618 newSig.insertIntoTextEdit( this, KPIMIdentities::Signature::AtCursor, false );
620 currentSearchPosition += newSig.plainText().length();
623 cursor.endEditBlock();
626 #include "kmeditor.moc"