// kmcomposewin.cpp
// Author: Markus Wuebben <markus.wuebben@kde.org>
// This code is published under the GPL.

#include <config.h>

#include "kmedit.h"
#include "kmlineeditspell.h"

#define REALLY_WANT_KMCOMPOSEWIN_H
#include "kmcomposewin.h"
#undef REALLY_WANT_KMCOMPOSEWIN_H
#include "kmmsgdict.h"
#include "kmfolder.h"
#include "kmcommands.h"

#include <maillistdrag.h>
using KPIM::MailListDrag;

#include <libtdepim/tdefileio.h>
#include <libemailfunctions/email.h>

#include <kcursor.h>
#include <kprocess.h>

#include <tdepopupmenu.h>
#include <kdebug.h>
#include <tdemessagebox.h>
#include <kurldrag.h>

#include <tdetempfile.h>
#include <tdelocale.h>
#include <tdeapplication.h>
#include <kdirwatch.h>
#include <kiconloader.h>

#include "globalsettings.h"
#include "replyphrases.h"

#include <tdespell.h>
#include <tdespelldlg.h>
#include <spellingfilter.h>
#include <ksyntaxhighlighter.h>

#include <tqregexp.h>
#include <tqbuffer.h>
#include <tqevent.h>

#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <assert.h>


void KMEdit::contentsDragEnterEvent(TQDragEnterEvent *e)
{
    if (e->provides(MailListDrag::format()))
        e->accept(true);
    else if (e->provides("image/png"))
        e->accept();
    else
        return KEdit::contentsDragEnterEvent(e);
}

void KMEdit::contentsDragMoveEvent(TQDragMoveEvent *e)
{
    if (e->provides(MailListDrag::format()))
        e->accept();
    else if (e->provides("image/png"))
        e->accept();
    else
        return KEdit::contentsDragMoveEvent(e);
}

void KMEdit::keyPressEvent( TQKeyEvent* e )
{
    if( e->key() == Key_Return ) {
        int line, col;
        getCursorPosition( &line, &col );
        TQString lineText = text( line );
        // returns line with additional trailing space (bug in TQt?), cut it off
        lineText.truncate( lineText.length() - 1 );
        // special treatment of quoted lines only if the cursor is neither at
        // the begin nor at the end of the line
        if( ( col > 0 ) && ( col < int( lineText.length() ) ) ) {
            bool isQuotedLine = false;
            uint bot = 0; // bot = begin of text after quote indicators
            while( bot < lineText.length() ) {
                if( ( lineText[bot] == '>' ) || ( lineText[bot] == '|' ) ) {
                    isQuotedLine = true;
                    ++bot;
                }
                else if( lineText[bot].isSpace() ) {
                    ++bot;
                }
                else {
                    break;
                }
            }

            KEdit::keyPressEvent( e );

            // duplicate quote indicators of the previous line before the new
            // line if the line actually contained text (apart from the quote
            // indicators) and the cursor is behind the quote indicators
            if( isQuotedLine
                && ( bot != lineText.length() )
                && ( col >= int( bot ) ) ) {

		// The cursor position might have changed unpredictably if there was selected
		// text which got replaced by a new line, so we query it again:
		getCursorPosition( &line, &col );
                TQString newLine = text( line );
                // remove leading white space from the new line and instead
                // add the quote indicators of the previous line
                unsigned int leadingWhiteSpaceCount = 0;
                while( ( leadingWhiteSpaceCount < newLine.length() )
                       && newLine[leadingWhiteSpaceCount].isSpace() ) {
                    ++leadingWhiteSpaceCount;
                }
                newLine = newLine.replace( 0, leadingWhiteSpaceCount,
                                           lineText.left( bot ) );
                removeParagraph( line );
                insertParagraph( newLine, line );
                // place the cursor at the begin of the new line since
                // we assume that the user split the quoted line in order
                // to add a comment to the first part of the quoted line
                setCursorPosition( line, 0 );
            }
        }
        else
            KEdit::keyPressEvent( e );
    }
    else
        KEdit::keyPressEvent( e );
}

void KMEdit::contentsDropEvent(TQDropEvent *e)
{
    if (e->provides(MailListDrag::format())) {
        // Decode the list of serial numbers stored as the drag data
        TQByteArray serNums;
        MailListDrag::decode( e, serNums );
        TQBuffer serNumBuffer(serNums);
        serNumBuffer.open(IO_ReadOnly);
        TQDataStream serNumStream(&serNumBuffer);
        TQ_UINT32 serNum;
        KMFolder *folder = 0;
        int idx;
        TQPtrList<KMMsgBase> messageList;
        while (!serNumStream.atEnd()) {
            KMMsgBase *msgBase = 0;
            serNumStream >> serNum;
            KMMsgDict::instance()->getLocation(serNum, &folder, &idx);
            if (folder)
                msgBase = folder->getMsgBase(idx);
            if (msgBase)
                messageList.append( msgBase );
        }
        serNumBuffer.close();
        uint identity = folder ? folder->identity() : 0;
        KMCommand *command =
            new KMForwardAttachedCommand(mComposer, messageList,
                                         identity, mComposer);
        command->start();
    }
    else if( e->provides("image/png") ) {
        emit attachPNGImageData(e->encodedData("image/png"));
    }
    else if( KURLDrag::canDecode( e ) ) {
        KURL::List urlList;
        if( KURLDrag::decode( e, urlList ) ) {
            TDEPopupMenu p;
            p.insertItem( i18n("Add as Text"), 0 );
            p.insertItem( i18n("Add as Attachment"), 1 );
            int id = p.exec( mapToGlobal( e->pos() ) );
            switch ( id) {
              case 0:
                for ( KURL::List::Iterator it = urlList.begin();
                     it != urlList.end(); ++it ) {
                  insert( (*it).url() );
                }
                break;
              case 1:
                for ( KURL::List::Iterator it = urlList.begin();
                     it != urlList.end(); ++it ) {
                  mComposer->addAttach( *it );
                }
                break;
            }
        }
        else if ( TQTextDrag::canDecode( e ) ) {
          TQString s;
          if ( TQTextDrag::decode( e, s ) )
            insert( s );
        }
        else
          kdDebug(5006) << "KMEdit::contentsDropEvent, unable to add dropped object" << endl;
    }
    else if( e->provides("text/x-textsnippet") ) {
	emit insertSnippet();
    }
    else {
        KEdit::contentsDropEvent(e);
    }
}

KMEdit::KMEdit(TQWidget *parent, KMComposeWin* composer,
               KSpellConfig* autoSpellConfig,
               const char *name)
  : KEdit( parent, name ),
    mComposer( composer ),
    mKSpellForDialog( 0 ),
    mSpeller( 0 ),
    mSpellConfig( autoSpellConfig ),
    mSpellingFilter( 0 ),
    mExtEditorTempFile( 0 ),
    mExtEditorTempFileWatcher( 0 ),
    mExtEditorProcess( 0 ),
    mUseExtEditor( false ),
    mWasModifiedBeforeSpellCheck( false ),
    mHighlighter( 0 ),
    mSpellLineEdit( false ),
    mPasteMode( TQClipboard::Clipboard )
{
  connect( this, TQ_SIGNAL(selectionChanged()), this, TQ_SLOT(slotSelectionChanged()) );
  installEventFilter(this);
  KCursor::setAutoHideCursor( this, true, true );
  setOverwriteEnabled( true );
  createSpellers();
  connect( mSpellConfig, TQ_SIGNAL( configChanged() ),
           this, TQ_SLOT( createSpellers() ) );
  connect( mSpeller, TQ_SIGNAL( death() ),
           this, TQ_SLOT( spellerDied() ) );
}

void KMEdit::createSpellers()
{
  delete mSpeller;
  mSpeller = new KMSpell( this, TQ_SLOT( spellerReady( KSpell * ) ), mSpellConfig );
}

void KMEdit::initializeAutoSpellChecking()
{
  if ( mHighlighter )
    return; // already initialized
  TQColor defaultColor1( 0x00, 0x80, 0x00 ); // defaults from kmreaderwin.cpp
  TQColor defaultColor2( 0x00, 0x70, 0x00 );
  TQColor defaultColor3( 0x00, 0x60, 0x00 );
  TQColor defaultForeground( kapp->palette().active().text() );

  TQColor c = TQt::red;
  TDEConfigGroup readerConfig( KMKernel::config(), "Reader" );
  TQColor col1;
  if ( !readerConfig.readBoolEntry(  "defaultColors", true ) )
      col1 = readerConfig.readColorEntry( "ForegroundColor", &defaultForeground );
  else
      col1 = defaultForeground;
  TQColor col2 = readerConfig.readColorEntry( "QuotedText3", &defaultColor3 );
  TQColor col3 = readerConfig.readColorEntry( "QuotedText2", &defaultColor2 );
  TQColor col4 = readerConfig.readColorEntry( "QuotedText1", &defaultColor1 );
  TQColor misspelled = readerConfig.readColorEntry( "MisspelledColor", &c );
  mHighlighter = new KMSyntaxHighter( this, /*active*/ true,
                                       /*autoEnabled*/ false,
                                       /*spellColor*/ misspelled,
                                       /*colorQuoting*/ true,
                                       col1, col2, col3, col4,
                                       mSpellConfig );

  connect( mHighlighter, TQ_SIGNAL(newSuggestions(const TQString&, const TQStringList&, unsigned int)),
           this, TQ_SLOT(addSuggestion(const TQString&, const TQStringList&, unsigned int)) );
}


TQPopupMenu *KMEdit::createPopupMenu( const TQPoint& pos )
{
  enum { IdUndo, IdRedo, IdSep1, IdCut, IdCopy, IdPaste, IdClear, IdSep2, IdSelectAll };

  TQPopupMenu *menu = KEdit::createPopupMenu( pos );
  if ( !TQApplication::clipboard()->image().isNull() ) {
    int id = menu->idAt(0);
    menu->setItemEnabled( id - IdPaste, true);
  }

  return menu;
}

void KMEdit::deleteAutoSpellChecking()
{ // because the highlighter doesn't support RichText, delete its instance.
  delete mHighlighter;
  mHighlighter =0;
}

void KMEdit::addSuggestion(const TQString& text, const TQStringList& lst, unsigned int )
{
  mReplacements[text] = lst;
}

void KMEdit::setSpellCheckingActive(bool spellCheckingActive)
{
  if ( mHighlighter ) {
    mHighlighter->setActive(spellCheckingActive);
  }
}


KMEdit::~KMEdit()
{
  removeEventFilter(this);

  if ( mSpeller ) {
    // The speller needs some time to clean up, so trigger cleanup and let it delete itself
    mSpeller->setAutoDelete( true );
    mSpeller->cleanUp();
    mSpeller = 0;
  }

  delete mKSpellForDialog;
  delete mHighlighter;
  mHighlighter = 0;
}



TQString KMEdit::brokenText()
{
  TQString temp, line;

  int num_lines = numLines();
  for (int i = 0; i < num_lines; ++i)
  {
    int lastLine = 0;
    line = textLine(i);
    for (int j = 0; j < (int)line.length(); ++j)
    {
      if (lineOfChar(i, j) > lastLine)
      {
        lastLine = lineOfChar(i, j);
        temp += '\n';
      }
      temp += line[j];
    }
    if (i + 1 < num_lines) temp += '\n';
  }

  return temp;
}


unsigned int KMEdit::lineBreakColumn() const
{
  unsigned int lineBreakColumn = 0;
  unsigned int numlines = numLines();
  while ( numlines-- ) {
    lineBreakColumn = TQMAX( lineBreakColumn, textLine( numlines ).length() );
  }
  return lineBreakColumn;
}

KMSpell::KMSpell( TQObject *receiver, const char *slot, KSpellConfig *spellConfig )
  : KSpell( 0, TQString(), receiver, slot, spellConfig )
{
}

KMSyntaxHighter::KMSyntaxHighter( TQTextEdit *textEdit,
                                  bool spellCheckingActive,
                                  bool autoEnable,
                                  const TQColor& spellColor,
                                  bool colorQuoting,
                                  const TQColor& QuoteColor0,
                                  const TQColor& QuoteColor1,
                                  const TQColor& QuoteColor2,
                                  const TQColor& QuoteColor3,
                                  KSpellConfig *spellConfig )
  : KDictSpellingHighlighter( textEdit, spellCheckingActive, autoEnable, spellColor, colorQuoting,
                              QuoteColor0, QuoteColor1, QuoteColor2, QuoteColor3, spellConfig )
{
}

bool KMSyntaxHighter::isMisspelled( const TQString &word )
{
  if ( mIgnoredWords.contains( word ) ) {
    return false;
  }
  else {
    return KDictSpellingHighlighter::isMisspelled( word );
  }
}

void KMSyntaxHighter::ignoreWord( const TQString &word )
{
  mIgnoredWords << word;
}

TQStringList KMSyntaxHighter::ignoredWords() const
{
  return mIgnoredWords;
}

void KMEdit::spellerDied()
{
  mSpeller = 0;
}

void KMEdit::spellerReady( KSpell *spell )
{
  Q_ASSERT( mSpeller == spell );
}

bool KMEdit::eventFilter(TQObject*o, TQEvent* e)
{
  if (o == this)
    KCursor::autoHideEventFilter(o, e);

  if (e->type() == TQEvent::KeyPress)
  {
    TQKeyEvent *k = (TQKeyEvent*)e;

    if (mUseExtEditor) {
      if (k->key() == Key_Up)
      {
        emit focusUp();
        return true;
      }

      // ignore modifier keys (cf. bug 48841)
      if ( (k->key() == Key_Shift) || (k->key() == Key_Control) ||
           (k->key() == Key_Meta) || (k->key() == Key_Alt) )
        return true;
      if (mExtEditorTempFile) return true;
      TQString sysLine = mExtEditor;
      mExtEditorTempFile = new KTempFile();

      mExtEditorTempFile->setAutoDelete(true);

      (*mExtEditorTempFile->textStream()) << text();

      mExtEditorTempFile->close();
      // replace %f in the system line
      sysLine.replace( "%f", mExtEditorTempFile->name() );
      mExtEditorProcess = new TDEProcess();
      mExtEditorProcess->setUseShell( true );
      sysLine += " ";
      while (!sysLine.isEmpty())
      {
        *mExtEditorProcess << sysLine.left(sysLine.find(" ")).local8Bit();
        sysLine.remove(0, sysLine.find(" ") + 1);
      }
      connect(mExtEditorProcess, TQ_SIGNAL(processExited(TDEProcess*)),
              TQ_SLOT(slotExternalEditorDone(TDEProcess*)));
      if (!mExtEditorProcess->start())
      {
        KMessageBox::error( topLevelWidget(),
                            i18n("Unable to start external editor.") );
        killExternalEditor();
      } else {
        mExtEditorTempFileWatcher = new KDirWatch( this, "mExtEditorTempFileWatcher" );
        connect( mExtEditorTempFileWatcher, TQ_SIGNAL(dirty(const TQString&)),
                 TQ_SLOT(slotExternalEditorTempFileChanged(const TQString&)) );
        mExtEditorTempFileWatcher->addFile( mExtEditorTempFile->name() );
      }
      return true;
    } else {
    // ---sven's Arrow key navigation start ---
    // Key Up in first line takes you to Subject line.
    if (k->key() == Key_Up && k->state() != ShiftButton && currentLine() == 0
      && lineOfChar(0, currentColumn()) == 0)
    {
      deselect();
      emit focusUp();
      return true;
    }
    // ---sven's Arrow key navigation end ---

    if (k->key() == Key_Backtab && k->state() == ShiftButton)
    {
      deselect();
      emit focusUp();
      return true;
    }

    }
  } else if ( e->type() == TQEvent::ContextMenu ) {
    TQContextMenuEvent *event = (TQContextMenuEvent*) e;

    int para = 1, charPos, firstSpace, lastSpace;

    //Get the character at the position of the click
    charPos = charAt( viewportToContents(event->pos()), &para );
    TQString paraText = text( para );

    if( !paraText.at(charPos).isSpace() )
    {
      //Get word right clicked on
      const TQRegExp wordBoundary( "[\\s\\W]" );
      firstSpace = paraText.findRev( wordBoundary, charPos ) + 1;
      lastSpace = paraText.find( wordBoundary, charPos );
      if( lastSpace == -1 )
        lastSpace = paraText.length();
      TQString word = paraText.mid( firstSpace, lastSpace - firstSpace );
      //Continue if this word was misspelled
      if( !word.isEmpty() && mReplacements.contains( word ) )
      {
        TDEPopupMenu p;

        //Add the suggestions to the popup menu
        TQStringList reps = mReplacements[word];
        if( reps.count() > 0 )
        {
          int listPos = 0;
          for ( TQStringList::Iterator it = reps.begin(); it != reps.end(); ++it ) {
            p.insertItem( *it, listPos );
            listPos++;
          }
        }
        else
        {
          p.setItemEnabled( p.insertItem( i18n( "No Suggestions" ), -2 ), false );
        }

        int addToDictionaryId = -42;
        int ignoreId = -43;
        if ( mSpeller && mSpeller->status() == KSpell::Running ) {
          p.insertSeparator();
          addToDictionaryId = p.insertItem( i18n( "Add to Dictionary" ) );
          ignoreId = p.insertItem( i18n( "Ignore All" ) );
        }

        //Execute the popup inline
        const int id = p.exec( mapToGlobal( event->pos() ) );

        if ( id == ignoreId ) {
          mHighlighter->ignoreWord( word );
          mHighlighter->rehighlight();
        }
        if ( id == addToDictionaryId ) {
          mSpeller->addPersonal( word );
          mSpeller->writePersonalDictionary();
          if ( mHighlighter ) {
            // Wait a bit until reloading the highlighter, the mSpeller first needs to finish saving
            // the personal word list.
            TQTimer::singleShot( 200, mHighlighter, TQ_SLOT( slotLocalSpellConfigChanged() ) );
          }
        }
        else if( id > -1 )
        {
          //Save the cursor position
          int parIdx = 1, txtIdx = 1;
          getCursorPosition(&parIdx, &txtIdx);
          setSelection(para, firstSpace, para, lastSpace);
          insert(mReplacements[word][id]);
          // Restore the cursor position; if the cursor was behind the
          // misspelled word then adjust the cursor position
          if ( para == parIdx && txtIdx >= lastSpace )
            txtIdx += mReplacements[word][id].length() - word.length();
          setCursorPosition(parIdx, txtIdx);
        }

        if ( id == addToDictionaryId || id == ignoreId ) {
          // No longer misspelled: Either added to dictionary or ignored
          mReplacements.remove( word );
        }

        //Cancel original event
        return true;
      }
    }
  } else if ( e->type() == TQEvent::FocusIn || e->type() == TQEvent::FocusOut ) {
    TQFocusEvent *fe = static_cast<TQFocusEvent*>(e);
    if(! (fe->reason() == TQFocusEvent::ActiveWindow || fe->reason() == TQFocusEvent::Popup) )
      emit focusChanged( fe->gotFocus() );
  }

  return KEdit::eventFilter(o, e);
}


int KMEdit::autoSpellChecking( bool on )
{
  if ( textFormat() == TQt::RichText ) {
     // syntax highlighter doesn't support extended text properties
     if ( on )
       KMessageBox::sorry(this, i18n("Automatic spellchecking is not possible on text with markup."));
     return -1;
  }
  if ( mHighlighter ) {
    // don't autoEnable spell checking if the user turned spell checking off
    mHighlighter->setAutomatic( on );
    mHighlighter->setActive( on );
  }
  return 1;
}


void KMEdit::slotExternalEditorTempFileChanged( const TQString & fileName ) {
  if ( !mExtEditorTempFile )
    return;
  if ( fileName != mExtEditorTempFile->name() )
    return;
  // read data back in from file
  setAutoUpdate(false);
  clear();

  insertLine(TQString::fromLocal8Bit(KPIM::kFileToString( fileName, true, false )), -1);
  setAutoUpdate(true);
  repaint();
}

void KMEdit::slotExternalEditorDone( TDEProcess * proc ) {
  assert(proc == mExtEditorProcess);
  // make sure, we update even when KDirWatcher is too slow:
  slotExternalEditorTempFileChanged( mExtEditorTempFile->name() );
  killExternalEditor();
}

void KMEdit::killExternalEditor() {
  delete mExtEditorTempFileWatcher; mExtEditorTempFileWatcher = 0;
  delete mExtEditorTempFile; mExtEditorTempFile = 0;
  delete mExtEditorProcess; mExtEditorProcess = 0;
}


bool KMEdit::checkExternalEditorFinished() {
  if ( !mExtEditorProcess )
    return true;
  switch ( KMessageBox::warningYesNoCancel( topLevelWidget(),
           i18n("The external editor is still running.\n"
                "Abort the external editor or leave it open?"),
           i18n("External Editor"),
           i18n("Abort Editor"), i18n("Leave Editor Open") ) ) {
  case KMessageBox::Yes:
    killExternalEditor();
    return true;
  case KMessageBox::No:
    return true;
  default:
    return false;
  }
}

void KMEdit::spellcheck()
{
  if ( mKSpellForDialog )
    return;
  mWasModifiedBeforeSpellCheck = isModified();
  mSpellLineEdit = !mSpellLineEdit;
//  maybe for later, for now plaintext is given to KSpell
//  if (textFormat() == TQt::RichText ) {
//    kdDebug(5006) << "KMEdit::spellcheck, spellchecking for RichText" << endl;
//    mKSpellForDialog = new KSpell(this, i18n("Spellcheck - KMail"), this,
//                    TQ_SLOT(slotSpellcheck2(KSpell*)),0,true,false,KSpell::HTML);
//  }
//  else {

    // Don't use mSpellConfig here. Reason is that the spell dialog, KSpellDlg, uses its own
    // spell config, and therefore the two wouldn't be in sync.
    mKSpellForDialog = new KSpell( this, i18n("Spellcheck - KMail"), this,
                                   TQ_SLOT(slotSpellcheck2(KSpell*))/*, mSpellConfig*/ );
//  }

  TQStringList l = KSpellingHighlighter::personalWords();
  for ( TQStringList::Iterator it = l.begin(); it != l.end(); ++it ) {
      mKSpellForDialog->addPersonal( *it );
  }
  connect (mKSpellForDialog, TQ_SIGNAL( death()),
          this, TQ_SLOT (slotSpellDone()));
  connect (mKSpellForDialog, TQ_SIGNAL (misspelling (const TQString &, const TQStringList &, unsigned int)),
          this, TQ_SLOT (slotMisspelling (const TQString &, const TQStringList &, unsigned int)));
  connect (mKSpellForDialog, TQ_SIGNAL (corrected (const TQString &, const TQString &, unsigned int)),
          this, TQ_SLOT (slotCorrected (const TQString &, const TQString &, unsigned int)));
  connect (mKSpellForDialog, TQ_SIGNAL (done(const TQString &)),
          this, TQ_SLOT (slotSpellResult (const TQString&)));
}

void KMEdit::cut()
{
  KEdit::cut();
  if ( textFormat() != TQt::RichText && mHighlighter )
    mHighlighter->restartBackgroundSpellCheck();
}

void KMEdit::clear()
{
  KEdit::clear();
  if ( textFormat() != TQt::RichText && mHighlighter )
    mHighlighter->restartBackgroundSpellCheck();
}

void KMEdit::del()
{
  KEdit::del();
  if ( textFormat() != TQt::RichText && mHighlighter )
    mHighlighter->restartBackgroundSpellCheck();
}

void KMEdit::paste()
{
  mComposer->paste( mPasteMode );
}

// KMEdit indirectly inherits from TQTextEdit, which has virtual paste() method,
// but it controls whether it pastes clipboard or selection by an internal
// flag that is not accessible in any way, so paste() being virtual is actually
// useless, because reimplementations can't known where to paste from anyway.
// Roll our own internal flag.
void KMEdit::contentsMouseReleaseEvent( TQMouseEvent * e )
{
  if( e->button() != TQt::MidButton )
    return KEdit::contentsMouseReleaseEvent( e );
  mPasteMode = TQClipboard::Selection;
  KEdit::contentsMouseReleaseEvent( e );
  mPasteMode = TQClipboard::Clipboard;
}

void KMEdit::contentsMouseDoubleClickEvent( TQMouseEvent *e )
{
  bool handled = false;
  if ( e->button() == TQt::LeftButton ) {

    // Get the cursor position for the place where the user clicked to
    int paragraphPos;
    int charPos = charAt ( e->pos(), &paragraphPos );
    TQString paraText = text( paragraphPos );

    // Now select the word under the cursor
    if ( charPos >= 0 && static_cast<unsigned int>( charPos ) <= paraText.length() ) {

      // Start the selection where the user clicked
      int start = charPos;
      unsigned int end = charPos;

      // Extend the selection to the left, until we reach a non-letter and non-digit char
      for (;;) {
        if ( ( start - 1 ) < 0 )
          break;
        TQChar charToTheLeft = paraText.at( start - 1 );
        if ( charToTheLeft.isLetter() || charToTheLeft.isDigit() )
          start--;
        else
          break;
      }

      // Extend the selection to the left, until we reach a non-letter and non-digit char
      for (;;) {
        if ( ( end + 1 ) >= paraText.length() )
          break;
        TQChar charToTheRight = paraText.at( end + 1 );
        if ( charToTheRight.isLetter() || charToTheRight.isDigit() )
          end++;
        else
          break;
      }

      setSelection( paragraphPos, start, paragraphPos, end + 1 );
      handled = true;
    }
  }

  if ( !handled )
    return KEdit::contentsMouseDoubleClickEvent( e );
}

void KMEdit::slotMisspelling(const TQString &text, const TQStringList &lst, unsigned int pos)
{
    kdDebug(5006)<<"void KMEdit::slotMisspelling(const TQString &text, const TQStringList &lst, unsigned int pos) : "<<text <<endl;
    if( mSpellLineEdit )
        mComposer->sujectLineWidget()->spellCheckerMisspelling( text, lst, pos);
    else
        misspelling(text, lst, pos);
}

void KMEdit::slotCorrected (const TQString &oldWord, const TQString &newWord, unsigned int pos)
{
    kdDebug(5006)<<"slotCorrected (const TQString &oldWord, const TQString &newWord, unsigned int pos) : "<<oldWord<<endl;
    if( mSpellLineEdit )
        mComposer->sujectLineWidget()->spellCheckerCorrected( oldWord, newWord, pos);
    else {
        unsigned int l = 0;
        unsigned int cnt = 0;
        bool _bold,_underline,_italic;
        TQColor _color;
        TQFont _font;
        posToRowCol (pos, l, cnt);
        setCursorPosition(l, cnt+1); // the new word will get the same markup now as the first character of the word
        _bold = bold();
        _underline = underline();
        _italic = italic();
        _color = color();
        _font = currentFont();
        corrected(oldWord, newWord, pos);
        setSelection (l, cnt, l, cnt+newWord.length());
        setBold(_bold);
        setItalic(_italic);
        setUnderline(_underline);
        setColor(_color);
        setCurrentFont(_font);
    }

}

void KMEdit::slotSpellcheck2(KSpell*)
{
  // Make sure words ignored by the highlighter are ignored by KSpell as well
  if ( mHighlighter ) {
    for ( uint i = 0; i < mHighlighter->ignoredWords().size(); i++ )
      mKSpellForDialog->ignore( mHighlighter->ignoredWords()[i] );
  }

    if( !mSpellLineEdit)
    {
        spellcheck_start();

        TQString quotePrefix;
        if(mComposer && mComposer->msg())
        {
            int languageNr = GlobalSettings::self()->replyCurrentLanguage();
            ReplyPhrases replyPhrases( TQString::number(languageNr) );
            replyPhrases.readConfig();

            quotePrefix = mComposer->msg()->formatString(
                 replyPhrases.indentPrefix() );
        }

        kdDebug(5006) << "spelling: new SpellingFilter with prefix=\"" << quotePrefix << "\"" << endl;
        TQTextEdit plaintext;
        plaintext.setText(text());
        plaintext.setTextFormat(TQt::PlainText);
        mSpellingFilter = new SpellingFilter(plaintext.text(), quotePrefix, SpellingFilter::FilterUrls,
                                             SpellingFilter::FilterEmailAddresses);

        mKSpellForDialog->check(mSpellingFilter->filteredText());
    }
    else if( mComposer )
        mKSpellForDialog->check( mComposer->sujectLineWidget()->text());
}

void KMEdit::slotSpellResult(const TQString &s)
{
    if( !mSpellLineEdit)
        spellcheck_stop();

  int dlgResult = mKSpellForDialog->dlgResult();
  if ( dlgResult == KS_CANCEL )
  {
      if( mSpellLineEdit)
      {
          //stop spell check
          mSpellLineEdit = false;
          TQString tmpText( s );
          tmpText =  tmpText.remove('\n');

          if( tmpText != mComposer->sujectLineWidget()->text() )
              mComposer->sujectLineWidget()->setText( tmpText );
      }
      else
      {
          setModified(true);
      }
  }
  mKSpellForDialog->cleanUp();
  KDictSpellingHighlighter::dictionaryChanged();

  emit spellcheck_done( dlgResult );
}

void KMEdit::slotSpellDone()
{
  kdDebug(5006)<<" void KMEdit::slotSpellDone()\n";
  KSpell::spellStatus status = mKSpellForDialog->status();
  delete mKSpellForDialog;
  mKSpellForDialog = 0;

  kdDebug(5006) << "spelling: delete SpellingFilter" << endl;
  delete mSpellingFilter;
  mSpellingFilter = 0;
  mComposer->sujectLineWidget()->deselect();
  if (status == KSpell::Error)
  {
     KMessageBox::sorry( topLevelWidget(),
                         i18n("ISpell/Aspell could not be started. Please "
                              "make sure you have ISpell or Aspell properly "
                              "configured and in your PATH.") );
     emit spellcheck_done( KS_CANCEL );
  }
  else if (status == KSpell::Crashed)
  {
     spellcheck_stop();
     KMessageBox::sorry( topLevelWidget(),
                         i18n("ISpell/Aspell seems to have crashed.") );
     emit spellcheck_done( KS_CANCEL );
  }
  else
  {
      if( mSpellLineEdit )
          spellcheck();
      else if( !mComposer->subjectTextWasSpellChecked() && status == KSpell::FinishedNoMisspellingsEncountered )
          KMessageBox::information( topLevelWidget(),
                                    i18n("No misspellings encountered.") );
  }
}

void KMEdit::setCursorPositionFromStart( unsigned int pos ) {
  unsigned int l = 0;
  unsigned int c = 0;
  posToRowCol( pos, l, c );
  // kdDebug() << "Num lines: " << numLines() << endl;
  // kdDebug() << "Position " << pos << " converted to " << l << ":" << c << endl;
  setCursorPosition( l, c );
  ensureCursorVisible();
}

int KMEdit::indexOfCurrentLineStart( int paragraph, int index )
{
  Q_ASSERT( paragraph >= 0 && paragraph < paragraphs() );
  Q_ASSERT( index >= 0 && ( index == 0 || index < paragraphLength( paragraph ) ) );

  const int startLine = lineOfChar( paragraph, index );
  Q_ASSERT( startLine >= 0 && startLine < linesOfParagraph( paragraph ) );
  for ( int curIndex = index; curIndex >= 0; curIndex-- ) {
    const int line = lineOfChar( paragraph, curIndex );
    if ( line != startLine ) {
      return curIndex + 1;
    }
  }
  return 0;
}

#include "kmedit.moc"
