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 <KEncodingFileDialog>
27 #include <KMessageBox>
29 #include <KPushButton>
30 #include <KTemporaryFile>
32 #include <QApplication>
39 using namespace KPIMTextEdit
;
46 KMeditorPrivate( KMeditor
*parent
)
48 useExtEditor( false ),
49 mExtEditorProcess( 0 ),
50 mExtEditorTempFile( 0 ) {
61 // Just calls KTextEdit::ensureCursorVisible(), workaround for some bug.
62 void ensureCursorVisibleDelayed();
69 QString
addQuotesToText( const QString
&inputText
);
71 void startExternalEditor();
72 void slotEditorFinished( int, QProcess::ExitStatus exitStatus
);
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
®Exp
, const QString
&newText
,
79 const KPIMIdentities::Signature
&sig
);
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;
92 QString extEditorPath
;
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
112 mExtEditorTempFile
= new KTemporaryFile();
113 if ( !mExtEditorTempFile
->open() ) {
114 delete mExtEditorTempFile
;
115 mExtEditorTempFile
= 0;
116 q
->setUseExternalEditor( false );
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() ) {
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 );
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();
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();
198 } else if ( e
->key() == Qt::Key_Backtab
&& e
->modifiers() == Qt::ShiftModifier
) {
199 textCursor().clearSelection();
202 TextEdit::keyPressEvent( e
);
206 KMeditor::KMeditor( const QString
&text
, QWidget
*parent
)
207 : TextEdit( text
, parent
), d( new KMeditorPrivate( this ) )
212 KMeditor::KMeditor( QWidget
*parent
)
213 : TextEdit( parent
), d( new KMeditorPrivate( this ) )
218 KMeditor::~KMeditor()
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
)
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" ) );
265 if ( fdlg
->exec() ) {
266 url
= fdlg
->selectedUrl();
267 url
.setFileEncoding( fdlg
->selectedEncoding() );
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()
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
);
348 return q
->smartQuote( answer
);
351 QString
KMeditor::smartQuote( const QString
&msg
)
356 bool KMeditor::checkExternalEditorFinished()
358 if ( !d
->mExtEditorProcess
) {
362 int ret
= KMessageBox::warningYesNoCancel(
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" ) ) );
374 case KMessageBox::Yes
:
375 killExternalEditor();
377 case KMessageBox::No
:
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
)
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();
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
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
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() ) {
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() ) {
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
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
471 QTimer::singleShot( 500, this, SLOT( ensureCursorVisibleDelayed() ) );
474 void KMeditorPrivate::cleanWhitespaceHelper( const QRegExp
®Exp
,
475 const QString
&newText
,
476 const KPIMIdentities::Signature
&sig
)
478 int currentSearchPosition
= 0;
483 QString text
= q
->document()->toPlainText();
484 int currentMatch
= regExp
.indexIn( text
, currentSearchPosition
);
485 currentSearchPosition
= currentMatch
;
486 if ( currentMatch
== -1 ) {
491 QTextCursor
cursor( q
->document() );
492 cursor
.setPosition( currentMatch
);
493 cursor
.movePosition( QTextCursor::NextCharacter
, QTextCursor::KeepAnchor
,
494 regExp
.matchedLength() );
497 if ( q
->isLineQuoted( cursor
.block().text() ) ) {
498 currentSearchPosition
+= regExp
.matchedLength();
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();
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;
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 ) {
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;
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 ) {
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 );
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();
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"