/* chattexteditpart.cpp - Chat Text Edit Part Copyright (c) 2004 by Richard Smith Kopete (c) 2002-2004 by the Kopete developers ************************************************************************* * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ************************************************************************* */ #include "chattexteditpart.h" #include "kopetechatsession.h" #include "kopeteonlinestatus.h" #include "kopeteprotocol.h" #include "kopeteglobal.h" #include "kopeteprefs.h" #include #include #include #include #include #include ChatTextEditPart::ChatTextEditPart( Kopete::ChatSession *session, TQWidget *parent, const char *name ) : KopeteRichTextEditPart( parent, name, session->protocol()->capabilities() ), m_session(session) { historyPos = -1; toggleAutoSpellCheck(KopetePrefs::prefs()->spellCheck()); mComplete = new TDECompletion(); mComplete->setIgnoreCase( true ); mComplete->setOrder( TDECompletion::Weighted ); // set params on the edit widget edit()->setMinimumSize( TQSize( 75, 20 ) ); edit()->setWordWrap( TQTextEdit::WidgetWidth ); edit()->setWrapPolicy( TQTextEdit::AtWhiteSpace ); edit()->setAutoFormatting( TQTextEdit::AutoNone ); // some signals and slots connections connect( edit(), TQT_SIGNAL( textChanged()), this, TQT_SLOT( slotTextChanged() ) ); // timers for typing notifications m_typingRepeatTimer = new TQTimer(this, "m_typingRepeatTimer"); m_typingStopTimer = new TQTimer(this, "m_typingStopTimer"); connect( m_typingRepeatTimer, TQT_SIGNAL( timeout() ), this, TQT_SLOT( slotRepeatTypingTimer() ) ); connect( m_typingStopTimer, TQT_SIGNAL( timeout() ), this, TQT_SLOT( slotStoppedTypingTimer() ) ); connect( session, TQT_SIGNAL( contactAdded(const Kopete::Contact*, bool) ), this, TQT_SLOT( slotContactAdded(const Kopete::Contact*) ) ); connect( session, TQT_SIGNAL( contactRemoved(const Kopete::Contact*, const TQString&, Kopete::Message::MessageFormat, bool) ), this, TQT_SLOT( slotContactRemoved(const Kopete::Contact*) ) ); connect( session, TQT_SIGNAL( onlineStatusChanged( Kopete::Contact *, const Kopete::OnlineStatus & , const Kopete::OnlineStatus &) ), this, TQT_SLOT( slotContactStatusChanged( Kopete::Contact *, const Kopete::OnlineStatus &, const Kopete::OnlineStatus & ) ) ); slotContactAdded( session->myself() ); for ( TQPtrListIterator it( session->members() ); it.current(); ++it ) slotContactAdded( *it ); } ChatTextEditPart::~ChatTextEditPart() { delete mComplete; } KTextEdit *ChatTextEditPart::edit() { return static_cast(widget()); } void ChatTextEditPart::toggleAutoSpellCheck( bool enabled ) { if ( richTextEnabled() ) enabled = false; m_autoSpellCheckEnabled = enabled; if ( spellHighlighter() ) { spellHighlighter()->setAutomatic( enabled ); spellHighlighter()->setActive( enabled ); } edit()->setCheckSpellingEnabled( enabled ); } bool ChatTextEditPart::autoSpellCheckEnabled() const { return m_autoSpellCheckEnabled; } KDictSpellingHighlighter* ChatTextEditPart::spellHighlighter() { TQSyntaxHighlighter *qsh = edit()->syntaxHighlighter(); KDictSpellingHighlighter* kdsh = dynamic_cast( qsh ); return kdsh; } // NAUGHTY, BAD AND WRONG! (but needed to fix nick complete bugs) #include class EvilTextEdit : public KTextEdit { public: // grab the paragraph as plain text - very very evil. TQString plainText( int para ) { TQString str = document()->paragAt( para )->string()->toString(); // str includes an extra space on the end (from the newline character?) - remove it return str.left( str.length() - 1 ); } }; void ChatTextEditPart::complete() { int para = 1, parIdx = 1; edit()->getCursorPosition( ¶, &parIdx); // FIXME: strips out all formatting TQString txt = static_cast(edit())->plainText( para ); if ( parIdx > 0 ) { int firstSpace = txt.findRev( TQRegExp( TQString::fromLatin1("\\s\\S+") ), parIdx - 1 ) + 1; int lastSpace = txt.find( TQRegExp( TQString::fromLatin1("[\\s\\:]") ), firstSpace ); if( lastSpace == -1 ) lastSpace = txt.length(); TQString word = txt.mid( firstSpace, lastSpace - firstSpace ); TQString match; kdDebug(14000) << k_funcinfo << word << " from '" << txt << "'" << endl; if ( word != m_lastMatch ) { match = mComplete->makeCompletion( word ); m_lastMatch = TQString(); parIdx -= word.length(); } else { match = mComplete->nextMatch(); parIdx -= m_lastMatch.length(); } if ( !match.isNull() && !match.isEmpty() ) { TQString rightText = txt.right( txt.length() - lastSpace ); if ( para == 0 && firstSpace == 0 && rightText[0] != TQChar(':') ) { rightText = match + TQString::fromLatin1(": ") + rightText; parIdx += 2; } else rightText = match + rightText; // insert *before* remove. this is becase TQt adds an extra blank line // if the rich text control becomes empty (if you remove the only para). // disable updates while we change the contents to eliminate flicker. edit()->setUpdatesEnabled( false ); edit()->insertParagraph( txt.left(firstSpace) + rightText, para ); edit()->removeParagraph( para + 1 ); edit()->setCursorPosition( para, parIdx + match.length() ); edit()->setUpdatesEnabled( true ); // must call this rather than update because TQTextEdit is broken :( edit()->updateContents(); m_lastMatch = match; } else { kdDebug(14000) << k_funcinfo << "No completions! Tried " << mComplete->items() << endl; } } } void ChatTextEditPart::slotPropertyChanged( Kopete::Contact*, const TQString &key, const TQVariant& oldValue, const TQVariant &newValue ) { if ( key == Kopete::Global::Properties::self()->nickName().key() ) { mComplete->removeItem( oldValue.toString() ); mComplete->addItem( newValue.toString() ); } } void ChatTextEditPart::slotContactAdded( const Kopete::Contact *contact ) { connect( contact, TQT_SIGNAL( propertyChanged( Kopete::Contact *, const TQString &, const TQVariant &, const TQVariant & ) ), this, TQT_SLOT( slotPropertyChanged( Kopete::Contact *, const TQString &, const TQVariant &, const TQVariant & ) ) ) ; TQString contactName = contact->property(Kopete::Global::Properties::self()->nickName()).value().toString(); mComplete->addItem( contactName ); } void ChatTextEditPart::slotContactRemoved( const Kopete::Contact *contact ) { disconnect( contact, TQT_SIGNAL( propertyChanged( Kopete::Contact *, const TQString &, const TQVariant &, const TQVariant & ) ), this, TQT_SLOT( slotPropertyChanged( Kopete::Contact *, const TQString &, const TQVariant &, const TQVariant & ) ) ) ; TQString contactName = contact->property(Kopete::Global::Properties::self()->nickName()).value().toString(); mComplete->removeItem( contactName ); } bool ChatTextEditPart::canSend() { if ( !m_session ) return false; // can't send if there's nothing *to* send... if ( edit()->text().isEmpty() ) return false; Kopete::ContactPtrList members = m_session->members(); // if we can't send offline, make sure we have a reachable contact... if ( !( m_session->protocol()->capabilities() & Kopete::Protocol::CanSendOffline ) ) { bool reachableContactFound = false; //TODO: does this perform badly in large / busy IRC channels? - no, doesn't seem to TQPtrListIterator it ( members ); for( ; it.current(); ++it ) { if ( (*it)->isReachable() ) { reachableContactFound = true; break; } } // no online contact found and can't send offline? can't send. if ( !reachableContactFound ) return false; } return true; } void ChatTextEditPart::slotContactStatusChanged( Kopete::Contact *, const Kopete::OnlineStatus &newStatus, const Kopete::OnlineStatus &oldStatus ) { //FIXME: should use signal contact->isReachableChanged, but it doesn't exist ;( if ( ( oldStatus.status() == Kopete::OnlineStatus::Offline ) != ( newStatus.status() == Kopete::OnlineStatus::Offline ) ) { emit canSendChanged( canSend() ); } } void ChatTextEditPart::sendMessage() { TQString txt = text( TQt::PlainText ); // avoid sending emtpy messages or enter keys (see bug 100334) if ( txt.isEmpty() || txt == "\n" ) return; if ( m_lastMatch.isNull() && ( txt.find( TQRegExp( TQString::fromLatin1("^\\w+:\\s") ) ) > -1 ) ) { //no last match and it finds something of the form of "word:" at the start of a line TQString search = txt.left( txt.find(':') ); if( !search.isEmpty() ) { TQString match = mComplete->makeCompletion( search ); if( !match.isNull() ) edit()->setText( txt.replace(0,search.length(),match) ); } } if ( !m_lastMatch.isNull() ) { //FIXME: what is the next line for? mComplete->addItem( m_lastMatch ); m_lastMatch = TQString(); } slotStoppedTypingTimer(); Kopete::Message sentMessage = contents(); emit messageSent( sentMessage ); historyList.prepend( edit()->text() ); historyPos = -1; clear(); emit canSendChanged( false ); } bool ChatTextEditPart::isTyping() { TQString txt = text( TQt::PlainText ); //Make sure the message is empty. TQString::isEmpty() //returns false if a message contains just whitespace //which is the reason why we strip the whitespace return !txt.stripWhiteSpace().isEmpty(); } void ChatTextEditPart::slotTextChanged() { if ( isTyping() ) { // And they were previously typing if( !m_typingRepeatTimer->isActive() ) { m_typingRepeatTimer->start( 4000, false ); slotRepeatTypingTimer(); } // Reset the stop timer again, regardless of status m_typingStopTimer->start( 4500, true ); } emit canSendChanged( canSend() ); } void ChatTextEditPart::historyUp() { if ( historyList.empty() || historyPos == historyList.count() - 1 ) return; TQString text = edit()->text(); bool empty = text.stripWhiteSpace().isEmpty(); // got text? save it if ( !empty ) { if ( historyPos == -1 ) { historyList.prepend( text ); historyPos = 0; } else { historyList[historyPos] = text; } } historyPos++; TQString newText = historyList[historyPos]; TextFormat format=edit()->textFormat(); edit()->setTextFormat(AutoText); //workaround bug 115690 edit()->setText( newText ); edit()->setTextFormat(format); edit()->moveCursor( TQTextEdit::MoveEnd, false ); } void ChatTextEditPart::historyDown() { if ( historyList.empty() || historyPos == -1 ) return; TQString text = edit()->text(); bool empty = text.stripWhiteSpace().isEmpty(); // got text? save it if ( !empty ) { historyList[historyPos] = text; } historyPos--; TQString newText = ( historyPos >= 0 ? historyList[historyPos] : TQString() ); TextFormat format=edit()->textFormat(); edit()->setTextFormat(AutoText); //workaround bug 115690 edit()->setText( newText ); edit()->setTextFormat(format); edit()->moveCursor( TQTextEdit::MoveEnd, false ); } void ChatTextEditPart::addText( const TQString &text ) { edit()->insert( text ); } void ChatTextEditPart::setContents( const Kopete::Message &message ) { edit()->setText( richTextEnabled() ? message.escapedBody() : message.plainBody() ); setFont( message.font() ); setFgColor( message.fg() ); setBgColor( message.bg() ); } Kopete::Message ChatTextEditPart::contents() { Kopete::Message currentMsg( m_session->myself(), m_session->members(), edit()->text(), Kopete::Message::Outbound, richTextEnabled() ? Kopete::Message::RichText : Kopete::Message::PlainText ); currentMsg.setBg( bgColor() ); currentMsg.setFg( fgColor() ); currentMsg.setFont( font() ); return currentMsg; } void ChatTextEditPart::slotRepeatTypingTimer() { emit typing( true ); } void ChatTextEditPart::slotStoppedTypingTimer() { m_typingRepeatTimer->stop(); m_typingStopTimer->stop(); emit typing( false ); } #include "chattexteditpart.moc"