1/*
2 chattexteditpart.cpp - Chat Text Edit Part
3
4 Copyright (c) 2008 by Benson Tsai <btsai@vrwarp.com>
5 Copyright (c) 2004 by Richard Smith <kde@metafoo.co.uk>
6
7 Kopete (c) 2002-2004 by the Kopete developers <kopete-devel@kde.org>
8
9 *************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 *************************************************************************
17*/
18
19#include "chattexteditpart.h"
20
21#include "kopetecontact.h"
22#include "kopetechatsession.h"
23#include "kopeteonlinestatus.h"
24#include "kopeteprotocol.h"
25#include "kopeteglobal.h"
26#include "kopeteappearancesettings.h"
27#include "kopetechatwindowsettings.h"
28
29#include <kaction.h>
30#include <kactioncollection.h>
31#include <kcolordialog.h>
32#include <kconfig.h>
33#include <kcompletion.h>
34#include <kdebug.h>
35#include <kfontaction.h>
36#include <kfontdialog.h>
37#include <kfontsizeaction.h>
38#include <kglobalsettings.h>
39#include <kcolorscheme.h>
40#include <kicon.h>
41#include <kparts/genericfactory.h>
42#include <kstandardaction.h>
43#include <ktoggleaction.h>
44#include <kxmlguifactory.h>
45
46
47// Qt includes
48#include <QtCore/QTimer>
49#include <QtCore/QRegExp>
50#include <QtCore/QEvent>
51#include <QKeyEvent>
52#include <QtGui/QTextCursor>
53#include <QtGui/QTextCharFormat>
54
55
56typedef KParts::GenericFactory<ChatTextEditPart> ChatTextEditPartFactory;
57K_EXPORT_COMPONENT_FACTORY( librichtexteditpart, ChatTextEditPartFactory )
58
59ChatTextEditPart::ChatTextEditPart( Kopete::ChatSession *session, QWidget *parent)
60 : KParts::ReadOnlyPart( parent ), m_session(session)
61{
62 init(session, parent);
63}
64
65ChatTextEditPart::ChatTextEditPart(QWidget *parent, QObject*, const QStringList&)
66 : KParts::ReadOnlyPart( parent ), m_session()
67{
68 init(m_session, parent);
69}
70
71void ChatTextEditPart::init( Kopete::ChatSession *session, QWidget *parent)
72{
73 // we need an instance
74 setComponentData( ChatTextEditPartFactory::componentData() );
75
76 editor = new KopeteRichTextWidget(parent, m_session->protocol()->capabilities(), actionCollection());
77 setWidget( editor );
78
79 // TODO: Rename rc file
80 setXMLFile( "kopeterichtexteditpart/kopeterichtexteditpartfull.rc" );
81
82 historyPos = -1;
83
84 mComplete = new KCompletion();
85 mComplete->setIgnoreCase( true );
86 mComplete->setOrder( KCompletion::Weighted );
87
88 // set params on the edit widget
89 textEdit()->setMinimumSize( QSize( 75, 20 ) );
90
91 // some signals and slots connections
92 connect( textEdit(), SIGNAL(textChanged()), this, SLOT(slotTextChanged()) );
93
94 // timers for typing notifications
95 m_typingRepeatTimer = new QTimer(this);
96 m_typingRepeatTimer->setObjectName("m_typingRepeatTimer");
97 m_typingStopTimer = new QTimer(this);
98 m_typingStopTimer->setObjectName("m_typingStopTimer");
99
100 connect( m_typingRepeatTimer, SIGNAL(timeout()), this, SLOT(slotRepeatTypingTimer()) );
101 connect( m_typingStopTimer, SIGNAL(timeout()), this, SLOT(slotStoppedTypingTimer()) );
102
103 connect( session, SIGNAL(contactAdded(const Kopete::Contact*,bool)),
104 this, SLOT(slotContactAdded(const Kopete::Contact*)) );
105 connect( session, SIGNAL(contactRemoved(const Kopete::Contact*,QString,Qt::TextFormat,bool)),
106 this, SLOT(slotContactRemoved(const Kopete::Contact*)) );
107 connect( session, SIGNAL(onlineStatusChanged(Kopete::Contact*,Kopete::OnlineStatus,Kopete::OnlineStatus)),
108 this, SLOT(slotContactStatusChanged(Kopete::Contact*,Kopete::OnlineStatus,Kopete::OnlineStatus)) );
109
110 connect( Kopete::AppearanceSettings::self(), SIGNAL(appearanceChanged()),
111 this, SLOT(slotAppearanceChanged()) );
112
113 connect( KGlobalSettings::self(), SIGNAL(kdisplayFontChanged()),
114 this, SLOT(slotAppearanceChanged()) );
115
116 connect( editor, SIGNAL(richTextSupportChanged()), this, SLOT (slotRichTextSupportChanged()) );
117
118 slotAppearanceChanged();
119
120 slotContactAdded( session->myself() );
121
122 foreach( Kopete::Contact *contact, session->members() )
123 slotContactAdded( contact );
124}
125
126ChatTextEditPart::~ChatTextEditPart()
127{
128 delete mComplete;
129}
130
131void ChatTextEditPart::complete()
132{
133 QTextCursor textCursor = textEdit()->textCursor();
134 QTextBlock block = textCursor.block();
135
136 QString txt = block.text();
137 const int blockLength = block.length() - 1; // block.length includes the '\n'
138 const int blockPosition = block.position();
139 int cursorPos = textCursor.position() - blockPosition;
140
141 // TODO replace with textCursor.movePosition(QTextCursor::PreviousWord)?
142 const int startPos = txt.lastIndexOf( QRegExp( QLatin1String("\\s\\S+") ), cursorPos - 1 ) + 1;
143 int endPos = txt.indexOf( QRegExp( QLatin1String("[\\s\\:]") ), startPos );
144 if( endPos == -1 )
145 {
146 endPos = blockLength;
147 }
148 const QString word = txt.mid( startPos, endPos - startPos );
149
150 if ( endPos < txt.length() && txt[endPos] == ':') {
151 // Eat ':' and ' ' too, if they are there, so that after pressing Tab
152 // we are on the right side of them again.
153 ++endPos;
154 if ( endPos < txt.length() && txt[endPos] == ' ') {
155 ++endPos;
156 }
157 }
158
159 //kDebug(14000) << word << "from" << txt
160 // << "cursor pos=" << cursorPos
161 // << "start pos=" << startPos << "end pos=" << endPos;
162
163 QString match;
164 if ( word != m_lastMatch )
165 {
166 match = mComplete->makeCompletion( word );
167 m_lastMatch.clear();
168 }
169 else
170 {
171 match = mComplete->nextMatch();
172 }
173
174 if ( !match.isEmpty() )
175 {
176 m_lastMatch = match;
177
178 if ( textCursor.blockNumber() == 0 && startPos == 0 )
179 {
180 match += QLatin1String(": ");
181 }
182
183 //kDebug(14000) << "Selecting from position" << cursorPos << "to position" << endPos;
184 // Select the text to remove
185 textCursor.setPosition( startPos + blockPosition );
186 textCursor.setPosition( endPos + blockPosition, QTextCursor::KeepAnchor );
187 //kDebug(14000) << "replacing selection:" << textCursor.selectedText() << "with match:" << match;
188 // Type the text to replace it
189 textCursor.insertText( match );
190 textEdit()->setTextCursor( textCursor );
191 }
192 else
193 {
194 //kDebug(14000) << "No completions! Tried" << mComplete->items();
195 }
196}
197
198void ChatTextEditPart::slotDisplayNameChanged( const QString &oldName, const QString &newName )
199{
200 mComplete->removeItem( oldName );
201 mComplete->addItem( newName );
202}
203
204void ChatTextEditPart::slotContactAdded( const Kopete::Contact *contact )
205{
206 connect( contact, SIGNAL(displayNameChanged(QString,QString)),
207 this, SLOT(slotDisplayNameChanged(QString,QString)) );
208
209 mComplete->addItem( contact->displayName() );
210}
211
212void ChatTextEditPart::slotContactRemoved( const Kopete::Contact *contact )
213{
214 disconnect( contact, SIGNAL(displayNameChanged(QString,QString)),
215 this, SLOT(slotDisplayNameChanged(QString,QString)) );
216
217 mComplete->removeItem( contact->displayName() );
218}
219
220bool ChatTextEditPart::canSend()
221{
222 if ( !m_session ) return false;
223
224 // can't send if there's nothing *to* send...
225 if ( text(Qt::PlainText).isEmpty() )
226 return false;
227
228 Kopete::ContactPtrList members = m_session->members();
229
230 // if we can't send offline, make sure we have a reachable contact...
231 if ( !( m_session->protocol()->capabilities() & Kopete::Protocol::CanSendOffline ) )
232 {
233 bool reachableContactFound = false;
234
235 //TODO: does this perform badly in large / busy IRC channels? - no, doesn't seem to
236 for( int i = 0; i != members.size(); ++i )
237 {
238 if ( members[i]->isReachable() )
239 {
240 reachableContactFound = true;
241 break;
242 }
243 }
244
245 // no online contact found and can't send offline? can't send.
246 if ( !reachableContactFound )
247 return false;
248 }
249
250 return true;
251}
252
253void ChatTextEditPart::slotContactStatusChanged( Kopete::Contact *, const Kopete::OnlineStatus &newStatus, const Kopete::OnlineStatus &oldStatus )
254{
255 //FIXME: should use signal contact->isReachableChanged, but it doesn't exist ;(
256 if ( ( oldStatus.status() == Kopete::OnlineStatus::Offline )
257 != ( newStatus.status() == Kopete::OnlineStatus::Offline ) )
258 {
259 emit canSendChanged( canSend() );
260 }
261}
262
263void ChatTextEditPart::sendMessage()
264{
265 QString txt = this->text( Qt::PlainText );
266 // avoid sending emtpy messages or enter keys (see bug 100334)
267 if ( txt.isEmpty() || txt == "\n" )
268 return;
269
270 if ( m_lastMatch.isNull() && ( txt.indexOf( QRegExp( QLatin1String("^\\w+:\\s") ) ) > -1 ) )
271 { //no last match and it finds something of the form of "word:" at the start of a line
272 QString search = txt.left( txt.indexOf(':') );
273 if( !search.isEmpty() )
274 {
275 QString match = mComplete->makeCompletion( search );
276 if( !match.isNull() )
277 textEdit()->setText( txt.replace(0,search.length(),match) );
278 }
279 }
280
281 if ( !m_lastMatch.isNull() )
282 {
283 //FIXME: what is the next line for?
284 mComplete->addItem( m_lastMatch );
285 m_lastMatch.clear();
286 }
287
288 slotStoppedTypingTimer();
289 Kopete::Message sentMessage = contents();
290 emit messageSent( sentMessage );
291 historyList.prepend( this->text( Qt::AutoText) );
292 historyPos = -1;
293 textEdit()->moveCursor(QTextCursor::End);
294 textEdit()->clear();
295 emit canSendChanged( false );
296}
297
298bool ChatTextEditPart::isTyping()
299{
300 QString txt = text( Qt::PlainText );
301
302 //Make sure the message is empty. QString::isEmpty()
303 //returns false if a message contains just whitespace
304 //which is the reason why we strip the whitespace
305 return !txt.trimmed().isEmpty();
306}
307
308void ChatTextEditPart::slotTextChanged()
309{
310 if ( isTyping() )
311 {
312 // And they were previously typing
313 if( !m_typingRepeatTimer->isActive() )
314 {
315 m_typingRepeatTimer->setSingleShot( false );
316 m_typingRepeatTimer->start( 4000 );
317 slotRepeatTypingTimer();
318 }
319
320 // Reset the stop timer again, regardless of status
321 m_typingStopTimer->setSingleShot( true );
322 m_typingStopTimer->start( 4500 );
323 }
324
325 emit canSendChanged( canSend() );
326}
327
328void ChatTextEditPart::historyUp()
329{
330 if ( historyList.empty() || historyPos == historyList.count() - 1 )
331 return;
332
333 QString text = this->text(Qt::PlainText);
334 bool empty = text.trimmed().isEmpty();
335
336 // got text? save it
337 if ( !empty )
338 {
339 text = this->text(Qt::AutoText);
340 if ( historyPos == -1 )
341 {
342 historyList.prepend( text );
343 historyPos = 0;
344 }
345 else
346 {
347 historyList[historyPos] = text;
348 }
349 }
350
351 historyPos++;
352
353 QString newText = historyList[historyPos];
354 textEdit()->setTextOrHtml( newText );
355 textEdit()->moveCursor( QTextCursor::End );
356}
357
358void ChatTextEditPart::historyDown()
359{
360 if ( historyList.empty() || historyPos == -1 )
361 return;
362
363 QString text = this->text(Qt::PlainText);
364 bool empty = text.trimmed().isEmpty();
365
366 // got text? save it
367 if ( !empty )
368 {
369 text = this->text(Qt::AutoText);
370 historyList[historyPos] = text;
371 }
372
373 historyPos--;
374
375 QString newText = ( historyPos >= 0 ? historyList[historyPos] : QString() );
376
377
378 textEdit()->setTextOrHtml( newText );
379 textEdit()->moveCursor( QTextCursor::End );
380}
381
382void ChatTextEditPart::addText( const QString &text )
383{
384 if( Qt::mightBeRichText(text) )
385 {
386 if ( textEdit()->isRichTextEnabled() )
387 {
388 textEdit()->insertHtml( text );
389 }
390 else
391 {
392 QTextDocument doc;
393 doc.setHtml( text );
394 textEdit()->insertPlainText( doc.toPlainText() );
395 }
396 }
397 else
398 {
399 textEdit()->insertPlainText( text );
400 }
401}
402
403void ChatTextEditPart::setContents( const Kopete::Message &message )
404{
405 if ( isRichTextEnabled() )
406 textEdit()->setHtml ( message.escapedBody() );
407 else
408 textEdit()->setPlainText ( message.plainBody() );
409 textEdit()->moveCursor ( QTextCursor::End );
410}
411
412Kopete::Message ChatTextEditPart::contents()
413{
414 Kopete::Message currentMsg( m_session->myself(), m_session->members() );
415 currentMsg.setDirection( Kopete::Message::Outbound );
416
417 if (isRichTextEnabled())
418 {
419 currentMsg.setHtmlBody(text());
420
421 Kopete::Protocol::Capabilities protocolCaps = m_session->protocol()->capabilities();
422
423 // I hate base *only* support, *waves fist at MSN*
424 if (protocolCaps & Kopete::Protocol::BaseFormatting)
425 {
426 currentMsg.setFont(textEdit()->currentRichFormat().font());
427 }
428
429 if (protocolCaps & Kopete::Protocol::BaseFgColor)
430 {
431 currentMsg.setForegroundColor(textEdit()->currentRichFormat().foreground().color());
432 }
433
434 if (protocolCaps & Kopete::Protocol::BaseBgColor)
435 {
436 currentMsg.setBackgroundColor(textEdit()->currentRichFormat().background().color());
437 }
438 }
439 else
440 {
441 currentMsg.setPlainBody(text());
442 }
443
444 return currentMsg;
445}
446
447void ChatTextEditPart::slotRepeatTypingTimer()
448{
449 emit typing( true );
450}
451
452void ChatTextEditPart::slotStoppedTypingTimer()
453{
454 m_typingRepeatTimer->stop();
455 m_typingStopTimer->stop();
456 emit typing( false );
457}
458
459void ChatTextEditPart::slotAppearanceChanged()
460{
461 Kopete::AppearanceSettings *settings = Kopete::AppearanceSettings::self();
462
463 QFont font = ( settings->chatFontSelection() == 1 ) ? settings->chatFont() : KGlobalSettings::generalFont();
464 QTextCharFormat format;
465 format.setFont(font);
466 format.setBackground(settings->chatBackgroundColor());
467 format.setForeground(settings->chatTextColor());
468
469 editor->setDefaultPlainCharFormat(format);
470 editor->setDefaultRichCharFormat(format);
471}
472
473void ChatTextEditPart::slotRichTextSupportChanged()
474{
475 KXMLGUIFactory * f = factory();
476 if (f)
477 {
478 f->removeClient(this);
479 f->addClient(this);
480 }
481}
482
483KopeteRichTextWidget *ChatTextEditPart::textEdit()
484{
485 return editor;
486}
487
488void ChatTextEditPart::setCheckSpellingEnabled( bool enabled )
489{
490 editor->setCheckSpellingEnabled( enabled );
491}
492
493bool ChatTextEditPart::checkSpellingEnabled() const
494{
495 return editor->checkSpellingEnabled();
496}
497
498void ChatTextEditPart::checkToolbarEnabled()
499{
500 emit toolbarToggled( isRichTextEnabled() );
501}
502
503KAboutData *ChatTextEditPart::createAboutData()
504{
505 KAboutData *aboutData = new KAboutData("krichtexteditpart", 0, ki18n("Chat Text Edit Part"), "0.1",
506 ki18n("A simple rich text editor part"),
507 KAboutData::License_LGPL );
508 aboutData->addAuthor(ki18n("Richard J. Moore"), KLocalizedString(), "rich@kde.org", "http://xmelegance.org/" );
509 aboutData->addAuthor(ki18n("Jason Keirstead"), KLocalizedString(), "jason@keirstead.org", "http://www.keirstead.org/" );
510 aboutData->addAuthor(ki18n("Michaƫl Larouche"), KLocalizedString(), "larouche@kde.org" "http://www.tehbisnatch.org/" );
511 aboutData->addAuthor(ki18n("Benson Tsai"), KLocalizedString(), "btsai@vrwarp.com" "http://www.vrwarp.com/" );
512
513 return aboutData;
514}
515
516void ChatTextEditPart::readConfig( KConfigGroup& config )
517{
518 kDebug() << "Loading config";
519
520 QTextCharFormat format = editor->defaultRichFormat();
521
522 QFont font = config.readEntry( "TextFont", format.font() );
523 QColor fg = config.readEntry( "TextFgColor", format.foreground().color() );
524 QColor bg = config.readEntry( "TextBgColor", format.background().color() );
525
526 QTextCharFormat desiredFormat = editor->currentRichFormat();
527 desiredFormat.setFont(font);
528 desiredFormat.setForeground(fg);
529 desiredFormat.setBackground(bg);
530 editor->setCurrentRichCharFormat(desiredFormat);
531
532 textEdit()->setAlignment(static_cast<Qt::AlignmentFlag>(config.readEntry( "EditAlignment", int(Qt::AlignLeft) )));
533}
534
535void ChatTextEditPart::writeConfig( KConfigGroup& config )
536{
537 kDebug() << "Saving config";
538
539 config.writeEntry( "TextFont", editor->currentRichFormat().font() );
540 config.writeEntry( "TextFgColor", editor->currentRichFormat().foreground().color() );
541 config.writeEntry( "TextBgColor", editor->currentRichFormat().background().color() );
542 config.writeEntry( "EditAlignment", int(editor->alignment()) );
543}
544
545void ChatTextEditPart::resetConfig( KConfigGroup& config )
546{
547 kDebug() << "Setting default font style";
548
549 editor->slotResetFontAndColor();
550
551 //action_align_left->trigger();
552
553 config.deleteEntry( "TextFont" );
554 config.deleteEntry( "TextFg" );
555 config.deleteEntry( "TextBg" );
556 config.deleteEntry( "EditAlignment" );
557}
558
559QString ChatTextEditPart::text( Qt::TextFormat format ) const
560{
561 if( (format == Qt::RichText || format == Qt::AutoText) && isRichTextEnabled() )
562 return editor->toHtml();
563 else
564 return editor->toPlainText();
565}
566
567bool ChatTextEditPart::isRichTextEnabled() const
568{
569 return editor->isRichTextEnabled();
570}
571
572#include "chattexteditpart.moc"
573
574// vim: set noet ts=4 sts=4 sw=4:
575