Add an entry point to the Web Shortcuts KCM at the bottom of the submenu.
[konversation:agateau-konversation.git] / src / viewer / ircview.cpp
1 // -*- mode: c++; c-file-style: "bsd"; c-basic-offset: 4; tabs-width: 4; indent-tabs-mode: nil -*-
2
3 /*
4   This program is free software; you can redistribute it and/or modify
5   it under the terms of the GNU General Public License as published by
6   the Free Software Foundation; either version 2 of the License, or
7   (at your option) any later version.
8 */
9
10 /*
11   Copyright (C) 2002 Dario Abatianni <eisfuchs@tigress.com>
12   Copyright (C) 2005-2007 Peter Simonsson <psn@linux.se>
13   Copyright (C) 2006-2010 Eike Hein <hein@kde.org>
14   Copyright (C) 2004-2009 Eli Mackenzie <argonel@gmail.com>
15 */
16
17 #include "ircview.h"
18 #include "channel.h"
19 #include "dcc/chatcontainer.h"
20 #include "application.h"
21 #include "mainwindow.h"
22 #include "viewcontainer.h"
23 #include "connectionmanager.h"
24 #include "highlight.h"
25 #include "server.h"
26 #include "sound.h"
27 #include "common.h"
28 #include "emoticons.h"
29 #include "notificationhandler.h"
30
31 #include <QStringList>
32 #include <QRegExp>
33 #include <QClipboard>
34 #include <QBrush>
35 #include <QEvent>
36 #include <QColor>
37 #include <QMouseEvent>
38 #include <QScrollBar>
39 #include <QTextBlock>
40 #include <QAbstractTextDocumentLayout>
41 #include <QPainter>
42 #include <QTextObjectInterface>
43 #include <QTextDocumentFragment>
44 #include <QTextCodec>
45
46 #include <KUrl>
47 #include <KBookmarkManager>
48 #include <kbookmarkdialog.h>
49 #include <KMenu>
50 #include <KGlobalSettings>
51 #include <KFileDialog>
52 #include <KAuthorized>
53 #include <KActionCollection>
54 #include <KToggleAction>
55 #include <KIO/CopyJob>
56
57 // For the Web Shortcuts context menu sub-menu.
58 #if KDE_IS_VERSION(4,5,0)
59 #include <KStringHandler>
60 #include <KToolInvocation>
61 #include <KUriFilter>
62 #endif
63
64 class Server;
65 class ChatWindow;
66 class SearchBar;
67
68 class QPixmap;
69 class QDropEvent;
70 class QDragEnterEvent;
71 class QEvent;
72
73 class KMenu;
74
75 using namespace Konversation;
76
77 class ScrollBarPin
78 {
79         QPointer<QScrollBar> m_bar;
80     public:
81         ScrollBarPin(QScrollBar *scrollBar) : m_bar(scrollBar)
82         {
83             if (m_bar)
84                 m_bar = m_bar->value() == m_bar->maximum()? m_bar : 0;
85         }
86         ~ScrollBarPin()
87         {
88             if (m_bar)
89                 m_bar->setValue(m_bar->maximum());
90         }
91 };
92
93 // Scribe bug - if the cursor position or anchor points to the last character in the document,
94 // the cursor becomes glued to the end of the document instead of retaining the actual position.
95 // This causes the selection to expand when something is appended to the document.
96 class SelectionPin
97 {
98     int pos, anc;
99     QPointer<IRCView> d;
100     public:
101         SelectionPin(IRCView *doc) : pos(0), anc(0), d(doc)
102         {
103             if (d->textCursor().hasSelection())
104             {
105                 int end = d->document()->rootFrame()->lastPosition();
106                 QTextBlock b = d->document()->lastBlock();
107                 pos = d->textCursor().position();
108                 anc = d->textCursor().anchor();
109                 if (pos != end && anc != end)
110                     anc = pos = 0;
111             }
112         }
113
114         ~SelectionPin()
115         {
116             if (d && (pos || anc))
117             {
118                 QTextCursor mv(d->textCursor());
119                 mv.setPosition(anc);
120                 mv.setPosition(pos, QTextCursor::KeepAnchor);
121                 d->setTextCursor(mv);
122             }
123         }
124 };
125
126
127 IRCView::IRCView(QWidget* parent, Server* newServer) : KTextBrowser(parent), m_nextCullIsMarker(false), m_rememberLinePosition(-1), m_rememberLineDirtyBit(false), markerFormatObject(this)
128 {
129     m_copyUrlMenu = false;
130     m_resetScrollbar = true;
131     m_offset = 0;
132     m_mousePressed = false;
133     m_isOnNick = false;
134     m_isOnChannel = false;
135     m_chatWin = 0;
136     m_nickPopup = 0;
137     m_channelPopup = 0;
138
139     setAcceptDrops(false);
140
141     // Marker lines
142     connect(document(), SIGNAL(contentsChange(int, int, int)), SLOT(cullMarkedLine(int, int, int)));
143
144     //This assert is here because a bad build environment can cause this to fail. There is a note
145     // in the Qt source that indicates an error should be output, but there is no such output.
146     QTextObjectInterface *iface = qobject_cast<QTextObjectInterface *>(&markerFormatObject);
147     if (!iface)
148     {
149         Q_ASSERT(iface);
150     }
151
152     document()->documentLayout()->registerHandler(IRCView::MarkerLine, &markerFormatObject);
153     document()->documentLayout()->registerHandler(IRCView::RememberLine, &markerFormatObject);
154
155
156     connect(this, SIGNAL(anchorClicked(QUrl)), this, SLOT(anchorClicked(QUrl)));
157     connect( this, SIGNAL( highlighted ( const QString &) ), this, SLOT( highlightedSlot( const QString &) ) );
158     setOpenLinks(false);
159     setUndoRedoEnabled(0);
160     document()->setDefaultStyleSheet("a.nick:link {text-decoration: none}");
161     setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
162     setFocusPolicy(Qt::ClickFocus);
163     setReadOnly(true);
164     viewport()->setCursor(Qt::ArrowCursor);
165     setTextInteractionFlags(Qt::TextBrowserInteraction);
166     viewport()->setMouseTracking(true);
167
168     setupContextMenu();
169
170     setServer(newServer);
171
172     if (Preferences::self()->useParagraphSpacing()) enableParagraphSpacing();
173
174     //HACK to workaround an issue with the QTextDocument
175     //doing a relayout/scrollbar over and over resulting in 100%
176     //proc usage. See bug 215256
177     setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
178 }
179
180 IRCView::~IRCView()
181 {
182 }
183
184 void IRCView::setServer(Server* newServer)
185 {
186     m_server = newServer;
187
188     if (newServer)
189     {
190         QAction *action = newServer->getViewContainer()->actionCollection()->action("open_logfile");
191         if(action)
192         {
193                 m_popup->addSeparator();
194                 m_popup->addAction( action );
195                 action = newServer->getViewContainer()->actionCollection()->action("channel_settings");
196                 if ( action )
197                         m_popup->addAction( action );
198         }
199     }
200
201 }
202
203 void IRCView::setChatWin(ChatWindow* chatWin)
204 {
205     m_chatWin = chatWin;
206
207     if(m_chatWin->getType()==ChatWindow::Channel)
208         setupNickPopupMenu(false);
209     else
210         setupNickPopupMenu(true);
211
212     setupChannelPopupMenu();
213 }
214
215 void IRCView::findText()
216 {
217     emit doSearch();
218 }
219
220 void IRCView::findNextText()
221 {
222     emit doSearchNext();
223 }
224
225 void IRCView::findPreviousText()
226 {
227     emit doSearchPrevious();
228 }
229
230 bool IRCView::search(const QString& pattern, bool caseSensitive, bool wholeWords, bool forward, bool fromCursor)
231 {
232     if (pattern.isEmpty())
233         return true;
234
235     m_pattern       = pattern;
236     m_forward       = forward;
237     m_searchFlags = 0;
238     if (caseSensitive)
239         m_searchFlags |= QTextDocument::FindCaseSensitively;
240     if (wholeWords)
241         m_searchFlags |= QTextDocument::FindWholeWords;
242     if (!fromCursor)
243         m_forward ? moveCursor(QTextCursor::Start) : moveCursor(QTextCursor::End);
244
245     return searchNext();
246 }
247
248 bool IRCView::searchNext(bool reversed)
249 {
250     bool fwd = (reversed ? !m_forward : m_forward);
251     if (fwd) {
252         m_searchFlags &= ~QTextDocument::FindBackward;
253     }
254     else {
255         m_searchFlags |= QTextDocument::FindBackward;
256     }
257     return find(m_pattern, m_searchFlags);
258 }
259
260 // Marker lines
261
262 #define _S(x) #x << (x)
263 void dump_doc(QTextDocument* document)
264 {
265     QTextBlock b(document->firstBlock());
266     while (b.isValid())
267     {
268         kDebug()    << _S(b.position())
269                     << _S(b.length())
270                     << _S(b.userState())
271                     ;
272                     b=b.next();
273     };
274 }
275
276 QDebug operator<<(QDebug dbg, QList<QTextBlock> &l)
277 {
278     dbg.space() << _S(l.count()) << endl;
279         for (int i=0; i< l.count(); ++i)
280         {
281             QTextBlock b=l[i];
282             dbg.space() << _S(i) << _S(b.blockNumber()) << _S(b.length()) << _S(b.userState()) << endl;
283         }
284
285     return dbg.space();
286 }
287
288 class IrcViewMimeData : public QMimeData
289 {
290 public:
291     IrcViewMimeData(const QTextDocumentFragment& _fragment): fragment(_fragment) {}
292     virtual QStringList formats() const;
293
294 protected:
295     virtual QVariant retrieveData(const QString &mimeType, QVariant::Type type) const;
296
297 private:
298     mutable QTextDocumentFragment fragment;
299 };
300
301 QStringList IrcViewMimeData::formats() const
302 {
303     if (!fragment.isEmpty())
304         return QStringList() << QString::fromLatin1("text/plain");
305     else
306         return QMimeData::formats();
307 }
308
309 QVariant IrcViewMimeData::retrieveData(const QString &mimeType, QVariant::Type type) const
310 {
311     if (!fragment.isEmpty())
312     {
313         IrcViewMimeData *that = const_cast<IrcViewMimeData *>(this);
314
315         //Copy the text, skipping any QChar::ObjectReplacementCharacter
316         QRegExp needle(QString("\\xFFFC\\n?"));
317
318         that->setText(fragment.toPlainText().remove(needle));
319         fragment = QTextDocumentFragment();
320     }
321     return QMimeData::retrieveData(mimeType, type);
322 }
323
324 QMimeData *IRCView::createMimeDataFromSelection() const
325 {
326     const QTextDocumentFragment fragment(textCursor());
327     return new IrcViewMimeData(fragment);
328 }
329
330 void IRCView::dragEnterEvent(QDragEnterEvent* e)
331 {
332     if (e->mimeData()->hasUrls())
333         e->acceptProposedAction();
334     else
335         e->ignore();
336 }
337
338 void IRCView::dragMoveEvent(QDragMoveEvent* e)
339 {
340     if (e->mimeData()->hasUrls())
341         e->accept();
342     else
343         e->ignore();
344 }
345
346 void IRCView::dropEvent(QDropEvent* e)
347 {
348     if (e->mimeData() && e->mimeData()->hasUrls())
349         emit urlsDropped(KUrl::List::fromMimeData(e->mimeData(), KUrl::List::PreferLocalUrls));
350 }
351
352 void IrcViewMarkerLine::drawObject(QPainter *painter, const QRectF &r, QTextDocument *doc, int posInDocument, const QTextFormat &format)
353 {
354     Q_UNUSED(format);
355
356     QTextBlock block=doc->findBlock(posInDocument);
357     QPen pen;
358     switch (block.userState())
359     {
360         case IRCView::BlockIsMarker:
361             pen.setColor(Preferences::self()->color(Preferences::ActionMessage));
362             break;
363
364         case IRCView::BlockIsRemember:
365             pen.setColor(Preferences::self()->color(Preferences::CommandMessage));
366             // pen.setStyle(Qt::DashDotDotLine);
367             break;
368
369         default:
370             //nice color, eh?
371             pen.setColor(Qt::cyan);
372     }
373
374     pen.setWidth(2); // FIXME this is a hardcoded value...
375     painter->setPen(pen);
376
377     qreal y = (r.top() + r.height() / 2);
378     QLineF line(r.left(), y, r.right(), y);
379     painter->drawLine(line);
380 }
381
382 QSizeF IrcViewMarkerLine::intrinsicSize(QTextDocument *doc, int posInDocument, const QTextFormat &format)
383 {
384     Q_UNUSED(posInDocument); Q_UNUSED(format);
385
386     QTextFrameFormat f=doc->rootFrame()->frameFormat();
387     qreal width = doc->pageSize().width()-(f.leftMargin()+f.rightMargin());
388     return QSizeF(width, 6); // FIXME this is a hardcoded value...
389 }
390
391 void IRCView::cullMarkedLine(int where, int rem, int add) //slot
392 {
393     if (where == 0 && add == 0 && rem !=0)
394     {
395         if (document()->blockCount() == 1 && document()->firstBlock().length() == 1)
396         {
397             wipeLineParagraphs();
398         }
399         else
400         {
401             if (m_nextCullIsMarker)
402             {
403                 //move the remember line up.. if the cull removed it, this will forget its position
404                 if (m_rememberLinePosition >= 0)
405                     --m_rememberLinePosition;
406                 m_markers.takeFirst();
407             }
408             int s = document()->firstBlock().userState();
409             m_nextCullIsMarker = (s == BlockIsMarker || s == BlockIsRemember);
410         }
411     }
412 }
413
414 void IRCView::insertMarkerLine() //slot
415 {
416     //if the last line is already a marker of any kind, skip out
417     if (lastBlockIsLine())
418         return;
419
420     //the code used to preserve the dirty bit status, but that was never affected by appendLine...
421     //maybe i missed something
422     appendLine(IRCView::MarkerLine);
423 }
424
425 void IRCView::insertRememberLine() //slot
426 {
427     m_rememberLineDirtyBit = true; // means we're going to append a remember line if some text gets inserted
428
429     if (!Preferences::self()->automaticRememberLineOnlyOnTextChange())
430         appendRememberLine();
431 }
432
433 void IRCView::cancelRememberLine() //slot
434 {
435     m_rememberLineDirtyBit = false;
436 }
437
438 bool IRCView::lastBlockIsLine(int select)
439 {
440     int state = document()->lastBlock().userState();
441
442     if (select == -1)
443         return (state == BlockIsRemember || state == BlockIsMarker);
444
445     return state == select;
446 }
447
448 void IRCView::appendRememberLine()
449 {
450     //clear this now, so that doAppend doesn't double insert
451     m_rememberLineDirtyBit = false;
452
453     //if the last line is already the remember line, do nothing
454     if (lastBlockIsLine(BlockIsRemember))
455         return;
456
457     // if we already have a rememberline, remove the previous one
458     if (m_rememberLinePosition > -1)
459     {
460         //get the block that is the remember line
461         QTextBlock rem = m_markers[m_rememberLinePosition];
462         m_markers.removeAt(m_rememberLinePosition); //probably will be in there only once
463         m_rememberLinePosition=-1;
464         voidLineBlock(rem);
465     }
466
467     //tell the control we did stuff
468     //FIXME do we still do something like this?
469     //repaintChanged();
470
471     //actually insert a line
472     appendLine(IRCView::RememberLine);
473
474     //store the index of the remember line
475     m_rememberLinePosition = m_markers.count() - 1;
476 }
477
478 void IRCView::voidLineBlock(QTextBlock rem)
479 {
480     if (rem.blockNumber() == 0)
481     {
482         Q_ASSERT(m_nextCullIsMarker);
483         m_nextCullIsMarker = false;
484     }
485     QTextCursor c(rem);
486     //FIXME make sure this doesn't flicker
487     c.select(QTextCursor::BlockUnderCursor);
488     c.removeSelectedText();
489 }
490
491 void IRCView::clearLines()
492 {
493     //if we have a remember line, put it in the list
494         //its already in the list
495
496     kDebug() << _S(m_nextCullIsMarker) << _S(m_rememberLinePosition) << _S(textCursor().position()) << m_markers;
497     dump_doc(document());
498
499     //are there any markers?
500     if (hasLines())
501     {
502         for (int i=0; i < m_markers.count(); ++i)
503             voidLineBlock(m_markers[i]);
504
505         wipeLineParagraphs();
506
507         //FIXME do we have this? //repaintChanged();
508     }
509
510 }
511
512 void IRCView::wipeLineParagraphs()
513 {
514     m_nextCullIsMarker = false;
515     m_rememberLinePosition = -1;
516     m_markers.clear();
517 }
518
519 bool IRCView::hasLines()
520 {
521     return m_markers.count() > 0;
522 }
523
524 QTextCharFormat IRCView::getFormat(ObjectFormats x)
525 {
526     QTextCharFormat f;
527     f.setObjectType(x);
528     return f;
529 }
530
531 void IRCView::appendLine(IRCView::ObjectFormats type)
532 {
533     ScrollBarPin barpin(verticalScrollBar());
534     SelectionPin selpin(this);
535
536     QTextCursor cursor(document());
537     cursor.movePosition(QTextCursor::End);
538
539     cursor.insertBlock();
540     cursor.insertText(QString(QChar::ObjectReplacementCharacter), getFormat(type));
541     cursor.block().setUserState(type == MarkerLine? BlockIsMarker : BlockIsRemember);
542
543     m_markers.append(cursor.block());
544 }
545
546
547 // Other stuff
548
549 void IRCView::enableParagraphSpacing() {}
550
551 void IRCView::updateAppearance()
552 {
553     if (Preferences::self()->customTextFont())
554         setFont(Preferences::self()->textFont());
555     else
556         setFont(KGlobalSettings::generalFont());
557
558     setVerticalScrollBarPolicy(Preferences::self()->showIRCViewScrollBar() ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff);
559
560     QPalette p;
561
562     p.setColor(QPalette::Base, Preferences::self()->color(Preferences::TextViewBackground));
563
564     if (Preferences::self()->showBackgroundImage())
565     {
566         KUrl url = Preferences::self()->backgroundImage();
567
568         if (!url.isEmpty())
569         {
570             QBrush brush;
571
572             brush.setTexture(QPixmap(url.path()));
573
574             p.setBrush(QPalette::Base, brush);
575         }
576     }
577
578     setPalette(p);
579 }
580
581 // Data insertion
582
583 void IRCView::append(const QString& nick, const QString& message)
584 {
585     QString channelColor = Preferences::self()->color(Preferences::ChannelMessage).name();
586
587     m_tabNotification = Konversation::tnfNormal;
588
589     QString nickLine = createNickLine(nick, channelColor);
590
591     QString line;
592     bool rtl = (basicDirection(message) == QChar::DirR);
593
594     if(rtl)
595     {
596         line = RLE;
597         line += LRE;
598         line += "<font color=\"" + channelColor + "\">" + nickLine +" %1" + PDF + RLM + " %3</font>";
599     }
600     else
601     {
602         if (!QApplication::isLeftToRight())
603             line += LRE;
604
605         line += "<font color=\"" + channelColor + "\">%1" + nickLine + " %3</font>";
606     }
607
608     line = line.arg(timeStamp(), nick, filter(message, channelColor, nick, true));
609
610     emit textToLog(QString("<%1>\t%2").arg(nick).arg(message));
611
612     doAppend(line, rtl);
613 }
614
615 void IRCView::appendRaw(const QString& message, bool suppressTimestamps, bool self)
616 {
617     QColor channelColor=Preferences::self()->color(Preferences::ChannelMessage);
618     m_tabNotification = Konversation::tnfNone;
619
620     QString line;
621     if (suppressTimestamps)
622         line = QString("<font color=\"" + channelColor.name() + "\">" + message + "</font>");
623     else
624         line = QString(timeStamp() + " <font color=\"" + channelColor.name() + "\">" + message + "</font>");
625
626     doAppend(line, false, self);
627 }
628
629 void IRCView::appendLog(const QString & message)
630 {
631     QColor channelColor = Preferences::self()->color(Preferences::ChannelMessage);
632     m_tabNotification = Konversation::tnfNone;
633
634     QString line("<font color=\"" + channelColor.name() + "\">" + message + "</font>");
635
636     doRawAppend(line, !QApplication::isLeftToRight());
637 }
638
639 void IRCView::appendQuery(const QString& nick, const QString& message, bool inChannel)
640 {
641     QString queryColor=Preferences::self()->color(Preferences::QueryMessage).name();
642
643     m_tabNotification = Konversation::tnfPrivate;
644
645     QString nickLine = createNickLine(nick, queryColor, true, inChannel);
646
647     QString line;
648     bool rtl = (basicDirection(message) == QChar::DirR);
649
650     if(rtl)
651     {
652         line = RLE;
653         line += LRE;
654         line += "<font color=\"" + queryColor + "\">" + nickLine + " %1" + PDF + RLM + " %3</font>";
655     }
656     else
657     {
658         if (!QApplication::isLeftToRight())
659             line += LRE;
660
661         line += "<font color=\"" + queryColor + "\">%1 " + nickLine + " %3</font>";
662     }
663
664     line = line.arg(timeStamp(), nick, filter(message, queryColor, nick, true));
665
666     emit textToLog(QString("<%1>\t%2").arg(nick).arg(message));
667
668     doAppend(line, rtl);
669 }
670
671 void IRCView::appendChannelAction(const QString& nick, const QString& message)
672 {
673     m_tabNotification = Konversation::tnfNormal;
674     appendAction(nick, message);
675 }
676
677 void IRCView::appendQueryAction(const QString& nick, const QString& message)
678 {
679     m_tabNotification = Konversation::tnfPrivate;
680     appendAction(nick, message);
681 }
682
683 void IRCView::appendAction(const QString& nick, const QString& message)
684 {
685     QString actionColor = Preferences::self()->color(Preferences::ActionMessage).name();
686
687     QString line;
688
689     QString nickLine = createNickLine(nick, actionColor, false);
690
691     if (message.isEmpty())
692     {
693         if (!QApplication::isLeftToRight())
694             line += LRE;
695
696         line += "<font color=\"" + actionColor + "\">%1 * " + nickLine + "</font>";
697
698         line = line.arg(timeStamp(), nick);
699
700         emit textToLog(QString("\t * %1").arg(nick));
701
702         doAppend(line, false);
703     }
704     else
705     {
706         bool rtl = (basicDirection(message) == QChar::DirR);
707
708         if (rtl)
709         {
710             line = RLE;
711             line += LRE;
712             line += "<font color=\"" + actionColor + "\">" + nickLine + " * %1" + PDF + " %3</font>";
713         }
714         else
715         {
716             if (!QApplication::isLeftToRight())
717                 line += LRE;
718
719             line += "<font color=\"" + actionColor + "\">%1 * " + nickLine + " %3</font>";
720         }
721
722         line = line.arg(timeStamp(), nick, filter(message, actionColor, nick, true));
723
724         emit textToLog(QString("\t * %1 %2").arg(nick).arg(message));
725
726         doAppend(line, rtl);
727     }
728 }
729
730 void IRCView::appendServerMessage(const QString& type, const QString& message, bool parseURL)
731 {
732     QString serverColor = Preferences::self()->color(Preferences::ServerMessage).name();
733     m_tabNotification = Konversation::tnfControl;
734
735     // Fixed width font option for MOTD
736     QString fixed;
737     if(Preferences::self()->fixedMOTD() && !m_fontDataBase.isFixedPitch(font().family()))
738     {
739         if(type == i18n("MOTD"))
740             fixed=" face=\"" + KGlobalSettings::fixedFont().family() + "\"";
741     }
742
743     QString line;
744     bool rtl = (basicDirection(message) == QChar::DirR);
745
746     if(rtl)
747     {
748         line = RLE;
749         line += LRE;
750         line += "<font color=\"" + serverColor + "\"" + fixed + "><b>[</b>%2<b>]</b> %1" + PDF + " %3</font>";
751     }
752     else
753     {
754         if (!QApplication::isLeftToRight())
755             line += LRE;
756
757         line += "<font color=\"" + serverColor + "\"" + fixed + ">%1 <b>[</b>%2<b>]</b> %3</font>";
758     }
759
760     if(type != i18n("Notify"))
761         line = line.arg(timeStamp(), type, filter(message, serverColor, 0 , true, parseURL));
762     else
763     {
764         // See Server::setWatchedNickOnline() for the originating call site.
765         line = "<font color=\"" + serverColor + "\"><a class=\"nick\" href=\"#"+message.section(' ', 0, 0)+"\">"
766             +line.arg(timeStamp(), type, message.section(' ', 1))+"</a></font>";
767     }
768
769     emit textToLog(QString("%1\t%2").arg(type).arg(message));
770
771     doAppend(line, rtl);
772 }
773
774 void IRCView::appendCommandMessage(const QString& type,const QString& message, bool important, bool parseURL, bool self)
775 {
776     if (Preferences::self()->hideUnimportantEvents() && !important)
777         return;
778
779     QString commandColor = Preferences::self()->color(Preferences::CommandMessage).name();
780     QString prefix="***";
781     m_tabNotification = Konversation::tnfControl;
782
783     if(type == i18n("Join"))
784     {
785         prefix="-->";
786         parseURL=false;
787     }
788     else if(type == i18n("Part") || type == i18n("Quit"))
789     {
790         prefix="<--";
791     }
792
793     prefix=Qt::escape(prefix);
794
795     QString line;
796     bool rtl = (basicDirection(message) == QChar::DirR);
797
798     if(rtl)
799     {
800         line = RLE;
801         line += LRE;
802         line += "<font color=\"" + commandColor + "\">%2 %1" + PDF + " %3</font>";
803     }
804     else
805     {
806         if (!QApplication::isLeftToRight())
807             line += LRE;
808
809         line += "<font color=\"" + commandColor + "\">%1 %2 %3</font>";
810     }
811
812     line = line.arg(timeStamp(), prefix, filter(message, commandColor, 0, true, parseURL, self));
813
814     emit textToLog(QString("%1\t%2").arg(type).arg(message));
815
816     doAppend(line, rtl, self);
817 }
818
819 void IRCView::appendBacklogMessage(const QString& firstColumn,const QString& rawMessage)
820 {
821     QString time;
822     QString message = rawMessage;
823     QString nick = firstColumn;
824     QString backlogColor = Preferences::self()->color(Preferences::BacklogMessage).name();
825     m_tabNotification = Konversation::tnfNone;
826
827     //The format in Chatwindow::logText is not configurable, so as long as nobody allows square brackets in a date/time format....
828     int eot = nick.lastIndexOf(' ');
829     time = nick.left(eot);
830     nick = nick.mid(eot+1);
831
832     if(!nick.isEmpty() && !nick.startsWith('<') && !nick.startsWith('*'))
833     {
834         nick = '|' + nick + '|';
835     }
836
837     // Nicks are in "<nick>" format so replace the "<>"
838     nick.replace('<',"&lt;");
839     nick.replace('>',"&gt;");
840
841     QString line;
842     bool rtl = (basicDirection(message) == QChar::DirR);
843
844     if(rtl)
845     {
846         line = RLE;
847         line += LRE;
848         line += "<font color=\"" + backlogColor + "\">%2 %1" + PDF + " %3</font>";
849     }
850     else
851     {
852         if (!QApplication::isLeftToRight())
853             line += LRE;
854
855         line += "<font color=\"" + backlogColor + "\">%1 %2 %3</font>";
856     }
857
858     line = line.arg(time, nick, filter(message, backlogColor, NULL, false, false));
859
860     doAppend(line, rtl);
861 }
862
863 void IRCView::doAppend(const QString& newLine, bool rtl, bool self)
864 {
865     if (m_rememberLineDirtyBit)
866         appendRememberLine();
867
868     if (!self && m_chatWin)
869         m_chatWin->activateTabNotification(m_tabNotification);
870
871     int scrollMax = Preferences::self()->scrollbackMax();
872     if (scrollMax != 0)
873     {
874         //don't remove lines if the user has scrolled up to read old lines
875         bool atBottom = (verticalScrollBar()->value() == verticalScrollBar()->maximum());
876         document()->setMaximumBlockCount(atBottom ? scrollMax : document()->maximumBlockCount() + 1);
877     }
878
879     doRawAppend(newLine, rtl);
880
881     //FIXME: Disable auto-text for DCC Chats since we don't have a server to parse wildcards.
882     if (!m_autoTextToSend.isEmpty() && m_server)
883     {
884         // replace placeholders in autoText
885         QString sendText = m_server->parseWildcards(m_autoTextToSend,m_server->getNickname(),
886             QString(), QString(), QString(), QString());
887         // avoid recursion due to signalling
888         m_autoTextToSend.clear();
889         // send signal only now
890         emit autoText(sendText);
891     }
892     else
893     {
894         m_autoTextToSend.clear();
895     }
896
897     if (!m_lastStatusText.isEmpty())
898         emit clearStatusBarTempText();
899 }
900
901 void IRCView::doRawAppend(const QString& newLine, bool rtl)
902 {
903     SelectionPin selpin(this); // HACK stop selection at end from growing
904     QString line(newLine);
905
906     line.remove('\n');
907
908     KTextBrowser::append(line);
909
910     QTextCursor formatCursor(document()->lastBlock());
911     QTextBlockFormat format = formatCursor.blockFormat();
912
913     if (!QApplication::isLeftToRight())
914         rtl = !rtl;
915
916     format.setAlignment(rtl ? Qt::AlignRight : Qt::AlignLeft);
917     formatCursor.setBlockFormat(format);
918 }
919
920 QString IRCView::timeStamp()
921 {
922     if(Preferences::self()->timestamping())
923     {
924         QTime time = QTime::currentTime();
925         QString timeColor = Preferences::self()->color(Preferences::Time).name();
926         QString timeFormat = Preferences::self()->timestampFormat();
927         QString timeString;
928
929         if(!Preferences::self()->showDate())
930         {
931             timeString = QString("<font color=\"" + timeColor + "\">[%1]</font> ").arg(time.toString(timeFormat));
932         }
933         else
934         {
935             QDate date = QDate::currentDate();
936             timeString = QString("<font color=\"" +
937                 timeColor + "\">[%1 %2]</font> ")
938                     .arg(KGlobal::locale()->formatDate(date, KLocale::ShortDate),
939                          time.toString(timeFormat));
940         }
941
942         return timeString;
943     }
944
945     return QString();
946 }
947
948 QString IRCView::createNickLine(const QString& nick, const QString& defaultColor, bool encapsulateNick, bool privMsg)
949 {
950     QString nickLine = "%2";
951     QString nickColor;
952
953     if (Preferences::self()->useColoredNicks())
954     {
955         if (m_server)
956         {
957             if (nick != m_server->getNickname())
958                 nickColor = Preferences::self()->nickColor(m_server->obtainNickInfo(nick)->getNickColor()).name();
959             else
960                 nickColor =  Preferences::self()->nickColor(8).name();
961         }
962         else if (m_chatWin->getType() == ChatWindow::DccChat)
963         {
964             QString ownNick = static_cast<DCC::ChatContainer*>(m_chatWin)->ownNick();
965
966             if (nick != ownNick)
967                 nickColor = Preferences::self()->nickColor(Konversation::colorForNick(ownNick)).name();
968             else
969                 nickColor = Preferences::self()->nickColor(8).name();
970         }
971     }
972     else
973         nickColor = defaultColor;
974
975     nickLine = "<font color=\"" + nickColor + "\">"+nickLine+"</font>";
976
977     if (Preferences::self()->useClickableNicks())
978         nickLine = "<a class=\"nick\" href=\"#" + nick + "\">" + nickLine + "</a>";
979
980     if (privMsg)
981         nickLine.prepend ("-&gt; ");
982
983     if(encapsulateNick)
984         nickLine = "&lt;" + nickLine + "&gt;";
985
986     if(Preferences::self()->useBoldNicks())
987         nickLine = "<b>" + nickLine + "</b>";
988
989     return nickLine;
990 }
991
992 void IRCView::replaceDecoration(QString& line, char decoration, char replacement)
993 {
994     int pos;
995     bool decorated = false;
996
997     while((pos=line.indexOf(decoration))!=-1)
998     {
999         line.replace(pos,1,(decorated) ? QString("</%1>").arg(replacement) : QString("<%1>").arg(replacement));
1000         decorated = !decorated;
1001     }
1002 }
1003
1004 QString IRCView::filter(const QString& line, const QString& defaultColor, const QString& whoSent,
1005 bool doHighlight, bool parseURL, bool self)
1006 {
1007     QString filteredLine(line);
1008     Application* konvApp = static_cast<Application*>(kapp);
1009
1010     //Since we can't turn off whitespace simplification withouteliminating text wrapping,
1011     //  if the line starts with a space turn it into a non-breaking space.
1012     //    (which magically turns back into a space on copy)
1013
1014     if (filteredLine[0]==' ')
1015         filteredLine[0]='\xA0';
1016
1017     // TODO: Use QStyleSheet::escape() here
1018     // Replace all < with &lt;
1019     filteredLine.replace('<',"\x0blt;");
1020     // Replace all > with &gt;
1021     filteredLine.replace('>', "\x0bgt;");
1022
1023     if(filteredLine.contains('\x07'))
1024     {
1025         if(Preferences::self()->beep())
1026         {
1027             kapp->beep();
1028         }
1029     }
1030
1031     // replace \003 and \017 codes with rich text color codes
1032     // captures          1    2                   23 4                   4 3     1
1033     QRegExp colorRegExp("(\003([0-9]|0[0-9]|1[0-5]|)(,([0-9]|0[0-9]|1[0-5])|,|)|\017)");
1034
1035     int pos;
1036     bool allowColors = Preferences::self()->allowColorCodes();
1037     bool firstColor = true;
1038     QString colorString;
1039
1040     while((pos=colorRegExp.indexIn(filteredLine))!=-1)
1041     {
1042         if(!allowColors)
1043         {
1044             colorString.clear();
1045         }
1046         else
1047         {
1048             colorString = (firstColor) ? QString() : QString("</font>");
1049
1050             // reset colors on \017 to default value
1051             if(colorRegExp.cap(1) == "\017")
1052                 colorString += "<font color=\""+defaultColor+"\">";
1053             else
1054             {
1055                 if(!colorRegExp.cap(2).isEmpty())
1056                 {
1057                     int foregroundColor = colorRegExp.cap(2).toInt();
1058                     colorString += "<font color=\"" + Preferences::self()->ircColorCode(foregroundColor).name() + "\">";
1059                 }
1060                 else
1061                 {
1062                     colorString += "<font color=\""+defaultColor+"\">";
1063                 }
1064             }
1065
1066             firstColor = false;
1067         }
1068
1069         filteredLine.replace(pos, colorRegExp.cap(0).length(), colorString);
1070     }
1071
1072     if(!firstColor)
1073         filteredLine+="</font>";
1074
1075     // Replace all text decorations
1076     // TODO: \017 should reset all text decorations to plain text
1077     replaceDecoration(filteredLine,'\x02','b');
1078     replaceDecoration(filteredLine,'\x1d','i');
1079     replaceDecoration(filteredLine,'\x13','s');
1080     replaceDecoration(filteredLine,'\x15','u');
1081     replaceDecoration(filteredLine,'\x16','b');   // should be inverse
1082     replaceDecoration(filteredLine,'\x1f','u');
1083
1084     if(parseURL)
1085     {
1086         if(whoSent.isEmpty())
1087             filteredLine = Konversation::tagUrls(filteredLine, m_chatWin->getName());
1088         else
1089             filteredLine = Konversation::tagUrls(filteredLine, whoSent);
1090     }
1091     else
1092     {
1093         // Change & to &amp; to prevent html entities to do strange things to the text
1094         filteredLine.replace('&', "&amp;");
1095         filteredLine.replace("\x0b", "&");
1096     }
1097
1098     // Highlight
1099     QString ownNick;
1100
1101     if (m_server)
1102     {
1103         ownNick = m_server->getNickname();
1104     }
1105     else if (m_chatWin->getType() == ChatWindow::DccChat)
1106     {
1107         ownNick = static_cast<DCC::ChatContainer*>(m_chatWin)->ownNick();
1108     }
1109
1110     if(doHighlight && (whoSent != ownNick) && !self)
1111     {
1112         QString highlightColor;
1113
1114         if(Preferences::self()->highlightNick() &&
1115             filteredLine.toLower().contains(QRegExp("(^|[^\\d\\w])" +
1116             QRegExp::escape(ownNick.toLower()) +
1117             "([^\\d\\w]|$)")))
1118         {
1119             // highlight current nickname
1120             highlightColor = Preferences::self()->highlightNickColor().name();
1121             m_tabNotification = Konversation::tnfNick;
1122         }
1123         else
1124         {
1125             QList<Highlight*> highlightList = Preferences::highlightList();
1126             QListIterator<Highlight*> it(highlightList);
1127             Highlight* highlight;
1128             bool patternFound = false;
1129
1130             QStringList captures;
1131             while (it.hasNext())
1132             {
1133                 highlight = it.next();
1134                 if(highlight->getRegExp())
1135                 {
1136                     QRegExp needleReg(highlight->getPattern());
1137                     needleReg.setCaseSensitivity(Qt::CaseInsensitive);
1138                                                   // highlight regexp in text
1139                     patternFound = ((filteredLine.contains(needleReg)) ||
1140                                                   // highlight regexp in nickname
1141                         (whoSent.contains(needleReg)));
1142
1143                     // remember captured patterns for later
1144                     captures=needleReg.capturedTexts();
1145
1146                 }
1147                 else
1148                 {
1149                     QString needle=highlight->getPattern();
1150                                                   // highlight patterns in text
1151                     patternFound = ((filteredLine.contains(needle, Qt::CaseInsensitive)) ||
1152                                                   // highlight patterns in nickname
1153                         (whoSent.contains(needle, Qt::CaseInsensitive)));
1154                 }
1155
1156                 if (patternFound)
1157                     break;
1158             }
1159
1160             if(patternFound)
1161             {
1162                 highlightColor = highlight->getColor().name();
1163                 m_highlightColor = highlightColor;
1164                 m_tabNotification = Konversation::tnfHighlight;
1165
1166                 if(Preferences::self()->highlightSoundsEnabled() && m_chatWin->notificationsEnabled())
1167                 {
1168                     konvApp->sound()->play(highlight->getSoundURL());
1169                 }
1170
1171                 konvApp->notificationHandler()->highlight(m_chatWin, whoSent, line);
1172                 m_autoTextToSend = highlight->getAutoText();
1173
1174                 // replace %0 - %9 in regex groups
1175                 for(int capture=0;capture<captures.count();capture++)
1176                 {
1177                   m_autoTextToSend.replace(QString("%%1").arg(capture),captures[capture]);
1178                 }
1179                 m_autoTextToSend.remove(QRegExp("%[0-9]"));
1180             }
1181         }
1182
1183         // apply found highlight color to line
1184         if(!highlightColor.isEmpty())
1185         {
1186             filteredLine = "<font color=\"" + highlightColor + "\">" + filteredLine + "</font>";
1187         }
1188     }
1189     else if(doHighlight && (whoSent == ownNick) && Preferences::self()->highlightOwnLines())
1190     {
1191         // highlight own lines
1192         filteredLine = "<font color=\"" + Preferences::self()->highlightOwnLinesColor().name() +
1193             "\">" + filteredLine + "</font>";
1194     }
1195
1196     filteredLine = Konversation::Emoticons::parseEmoticons(filteredLine);
1197
1198     // Replace pairs of spaces with "<space>&nbsp;" to preserve some semblance of text wrapping
1199     filteredLine.replace("  "," \xA0");
1200     return filteredLine;
1201 }
1202
1203 void IRCView::resizeEvent(QResizeEvent *event)
1204 {
1205     ScrollBarPin b(verticalScrollBar());
1206     KTextBrowser::resizeEvent(event);
1207 }
1208
1209 void IRCView::mouseMoveEvent(QMouseEvent* ev)
1210 {
1211     if (m_mousePressed && (m_pressPosition - ev->pos()).manhattanLength() > KApplication::startDragDistance())
1212     {
1213         m_mousePressed = false;
1214
1215         QTextCursor textCursor = this->textCursor();
1216         textCursor.clearSelection();
1217         setTextCursor(textCursor);
1218
1219
1220         QPointer<QDrag> drag = new QDrag(this);
1221         QMimeData* mimeData = new QMimeData;
1222
1223         KUrl url(m_urlToDrag);
1224         url.populateMimeData(mimeData);
1225
1226         drag->setMimeData(mimeData);
1227
1228         QPixmap pixmap = KIO::pixmapForUrl(url, 0, KIconLoader::Desktop, KIconLoader::SizeMedium);
1229         drag->setPixmap(pixmap);
1230
1231         drag->exec();
1232
1233         return;
1234     }
1235     else
1236     {
1237         // Store the url here instead of in highlightedSlot as the link given there is decoded.
1238         m_urlToCopy = anchorAt(ev->pos());
1239     }
1240
1241     KTextBrowser::mouseMoveEvent(ev);
1242 }
1243
1244 void IRCView::mousePressEvent(QMouseEvent* ev)
1245 {
1246     if (ev->button() == Qt::LeftButton)
1247     {
1248         m_urlToDrag = anchorAt(ev->pos());
1249
1250         if (!m_urlToDrag.isEmpty() && Konversation::isUrl(m_urlToDrag))
1251         {
1252             m_mousePressed = true;
1253             m_pressPosition = ev->pos();
1254         }
1255     }
1256
1257     KTextBrowser::mousePressEvent(ev);
1258 }
1259
1260 void IRCView::mouseReleaseEvent(QMouseEvent *ev)
1261 {
1262     if (ev->button() == Qt::LeftButton)
1263     {
1264         m_mousePressed = false;
1265     }
1266     else if (ev->button() == Qt::MidButton)
1267     {
1268         if (m_copyUrlMenu)
1269         {
1270             openLink(QUrl (m_urlToCopy));
1271             return;
1272         }
1273         else
1274         {
1275             emit textPasted(true);
1276             return;
1277         }
1278     }
1279
1280     KTextBrowser::mouseReleaseEvent(ev);
1281 }
1282
1283 void IRCView::keyPressEvent(QKeyEvent* ev)
1284 {
1285     const int key = ev->key() | ev->modifiers();
1286
1287     if (KStandardShortcut::paste().contains(key))
1288     {
1289         emit textPasted(false);
1290         ev->accept();
1291         return;
1292     }
1293
1294     KTextBrowser::keyPressEvent(ev);
1295 }
1296
1297 void IRCView::anchorClicked(const QUrl& url)
1298 {
1299     openLink(url);
1300 }
1301
1302 // FIXME do we still care about newtab? looks like konqi has lots of config now..
1303 void IRCView::openLink(const QUrl& url)
1304 {
1305     QString link(url.toString());
1306     // HACK Replace " " with %20 for channelnames, NOTE there can't be 2 channelnames in one link
1307     link = link.replace (' ', "%20");
1308
1309     if (!link.isEmpty() && !link.startsWith('#'))
1310     {
1311         if (link.startsWith(QLatin1String("irc://")) || link.startsWith(QLatin1String("ircs://")))
1312         {
1313             Application* konvApp = Application::instance();
1314             konvApp->getConnectionManager()->connectTo(Konversation::SilentlyReuseConnection, link);
1315         }
1316         else
1317             Application::openUrl(url.toEncoded());
1318     }
1319     //FIXME: Don't do channel links in DCC Chats to begin with since they don't have a server.
1320     else if (link.startsWith(QLatin1String("##")) && m_server && m_server->isConnected())
1321     {
1322         QString channel(link);
1323         channel.replace("##", "#");
1324         m_server->sendJoinCommand(channel);
1325     }
1326     //FIXME: Don't do user links in DCC Chats to begin with since they don't have a server.
1327     else if (link.startsWith('#') && m_server && m_server->isConnected())
1328     {
1329         QString recipient(link);
1330         recipient.remove('#');
1331         NickInfoPtr nickInfo = m_server->obtainNickInfo(recipient);
1332         m_server->addQuery(nickInfo, true /*we initiated*/);
1333     }
1334 }
1335
1336 void IRCView::saveLinkAs()
1337 {
1338     if(m_urlToCopy.isEmpty())
1339         return;
1340
1341     KUrl srcUrl (m_urlToCopy);
1342     KUrl saveUrl = KFileDialog::getSaveUrl(srcUrl.fileName(KUrl::ObeyTrailingSlash), QString(), this, i18n("Save link as"));
1343
1344     if (saveUrl.isEmpty() || !saveUrl.isValid())
1345         return;
1346
1347     KIO::copy(srcUrl, saveUrl);
1348 }
1349
1350 void IRCView::highlightedSlot(const QString& /*_link*/)
1351 {
1352     QString link = m_urlToCopy;
1353     // HACK Replace " " with %20 for channelnames, NOTE there can't be 2 channelnames in one link
1354     link = link.replace (' ', "%20");
1355
1356     //we just saw this a second ago.  no need to reemit.
1357     if (link == m_lastStatusText && !link.isEmpty())
1358         return;
1359
1360     // remember current URL to overcome link clicking problems in KTextBrowser
1361     //m_highlightedURL = link;
1362
1363     if (link.isEmpty())
1364     {
1365         if (!m_lastStatusText.isEmpty())
1366         {
1367             emit clearStatusBarTempText();
1368             m_lastStatusText.clear();
1369         }
1370     }
1371     else
1372     {
1373         m_lastStatusText = link;
1374     }
1375
1376     if (!link.startsWith('#'))
1377     {
1378         m_isOnNick = false;
1379         m_isOnChannel = false;
1380
1381         if (!link.isEmpty()) {
1382             //link therefore != m_lastStatusText  so emit with this new text
1383             emit setStatusBarTempText(link);
1384         }
1385
1386         if (link.isEmpty() && m_copyUrlMenu)
1387         {
1388             m_copyUrlClipBoard->setVisible(false);
1389             m_bookmark->setVisible(false);
1390             m_saveUrl->setVisible(false);
1391             copyUrlMenuSeparator->setVisible(false);
1392             m_copyUrlMenu = false;
1393
1394         }
1395         else if (!link.isEmpty() && !m_copyUrlMenu)
1396         {
1397            copyUrlMenuSeparator->setVisible(true);
1398            m_copyUrlClipBoard->setVisible(true);
1399            m_bookmark->setVisible(true);
1400            m_saveUrl->setVisible(true);
1401            m_copyUrlMenu = true;
1402         }
1403     }
1404     else if (link.startsWith('#') && !link.startsWith(QLatin1String("##")))
1405     {
1406         m_currentNick = link.mid(1);
1407
1408         if (m_nickPopup)
1409             m_nickPopup->setTitle(m_currentNick);
1410
1411         m_isOnNick = true;
1412         emit setStatusBarTempText(i18n("Open a query with %1", m_currentNick));
1413     }
1414     else
1415     {
1416         // link.startsWith("##")
1417         m_currentChannel = link.mid(1);
1418
1419         if(m_channelPopup)
1420         {
1421             QString prettyId = m_currentChannel;
1422
1423             if (prettyId.length()>15)
1424             {
1425                 prettyId.truncate(15);
1426                 prettyId.append("...");
1427             }
1428
1429             m_channelPopup->setTitle(prettyId);
1430         }
1431
1432         m_isOnChannel = true;
1433         emit setStatusBarTempText(i18n("Join the channel %1", m_currentChannel));
1434     }
1435 }
1436
1437 void IRCView::copyUrl()
1438 {
1439         if ( !m_urlToCopy.isEmpty() )
1440         {
1441                 QClipboard *cb = qApp->clipboard();
1442                 cb->setText(m_urlToCopy,QClipboard::Selection);
1443                 cb->setText(m_urlToCopy,QClipboard::Clipboard);
1444         }
1445
1446 }
1447
1448 void IRCView::slotBookmark()
1449 {
1450     if (m_urlToCopy.isEmpty())
1451         return;
1452
1453     KBookmarkManager* bm = KBookmarkManager::userBookmarksManager();
1454     KBookmarkDialog* dialog = new KBookmarkDialog(bm, this);
1455     dialog->addBookmark(m_urlToCopy, m_urlToCopy);
1456     delete dialog;
1457 }
1458
1459 // Context Menu
1460
1461 KMenu* IRCView::getPopup() const
1462 {
1463     return m_popup;
1464 }
1465
1466 void IRCView::setupContextMenu()
1467 {
1468     m_popup = new KMenu(this);
1469     m_popup->setObjectName("ircview_context_menu");
1470
1471     m_popup->addSeparator();
1472
1473     m_copyUrlClipBoard = new KAction(this);
1474     m_copyUrlClipBoard->setIcon(KIcon("edit-copy"));
1475     m_copyUrlClipBoard->setText(i18n("Copy Link Address"));
1476     connect(m_copyUrlClipBoard, SIGNAL(triggered()), SLOT(copyUrl()));
1477     m_popup->addAction(m_copyUrlClipBoard);
1478     m_copyUrlClipBoard->setVisible( false );
1479
1480     m_bookmark = new KAction(this);
1481     m_bookmark->setIcon(KIcon("bookmark-new"));
1482     m_bookmark->setText(i18n("Add to Bookmarks"));
1483     connect(m_bookmark, SIGNAL(triggered()), SLOT(slotBookmark()));
1484     m_popup->addAction(m_bookmark);
1485     m_bookmark->setVisible( false );
1486
1487     m_saveUrl = new KAction(this);
1488     m_saveUrl->setIcon(KIcon("document-save"));
1489     m_saveUrl->setText(i18n("Save Link As..."));
1490     connect(m_saveUrl, SIGNAL(triggered()), SLOT(saveLinkAs()));
1491     m_popup->addAction(m_saveUrl);
1492     m_saveUrl->setVisible( false );
1493
1494     QAction* toggleMenuBarSeparator = m_popup->addSeparator();
1495     toggleMenuBarSeparator->setVisible(false);
1496     copyUrlMenuSeparator = m_popup->addSeparator();
1497     copyUrlMenuSeparator->setVisible( false );
1498
1499     QAction* copyAct = new KAction(this);
1500     copyAct->setIcon(KIcon("edit-copy"));
1501     copyAct->setText(i18n("&Copy"));
1502     connect(copyAct, SIGNAL(triggered()), SLOT(copy()));
1503     m_popup->addAction(copyAct);
1504     connect(this, SIGNAL(copyAvailable(bool)), copyAct, SLOT( setEnabled( bool ) ));
1505     copyAct->setEnabled( false );
1506
1507 #if KDE_IS_VERSION(4,5,0)
1508     m_webShortcutMenu = new KMenu(this);
1509     m_popup->addMenu(m_webShortcutMenu);
1510     m_webShortcutMenu->menuAction()->setIcon(KIcon("preferences-web-browser-shortcuts"));
1511     m_webShortcutMenu->menuAction()->setVisible(false);
1512 #endif
1513
1514     QAction* selectAllAct = new KAction(this);
1515     selectAllAct->setText(i18n("Select All"));
1516     connect(selectAllAct, SIGNAL(triggered()), SLOT(selectAll()));
1517     m_popup->addAction(selectAllAct);
1518
1519     QAction* findTextAct = new KAction(this);
1520     findTextAct->setIcon(KIcon("edit-find"));
1521     findTextAct->setText(i18n("Find Text..."));
1522     connect(findTextAct, SIGNAL(triggered()), SLOT(findText()));
1523     m_popup->addAction(findTextAct);
1524 }
1525 void IRCView::setupNickPopupMenu(bool isQuery)
1526 {
1527     m_nickPopup = new KMenu(this);
1528     m_nickPopup->setObjectName("nicklist_context_menu");
1529     m_nickPopup->setTitle(m_currentNick);
1530
1531     QAction* action = m_nickPopup->addAction(i18n("&Whois"), this, SLOT(handleContextActions()));
1532     action->setData(Konversation::Whois);
1533     action = m_nickPopup->addAction(i18n("&Version"), this, SLOT(handleContextActions()));
1534     action->setData(Konversation::Version);
1535     action = m_nickPopup->addAction(i18n("&Ping"), this, SLOT(handleContextActions()));
1536     action->setData(Konversation::Ping);
1537
1538     m_nickPopup->addSeparator();
1539
1540     if(!isQuery)
1541     {
1542         QMenu* modes = m_nickPopup->addMenu(i18n("Modes"));
1543         action = modes->addAction(i18n("Give Op"), this, SLOT(handleContextActions()));
1544         action->setData(Konversation::GiveOp);
1545         action->setIcon(KIcon("irc-operator"));
1546         action = modes->addAction(i18n("Take Op"), this, SLOT(handleContextActions()));
1547         action->setData(Konversation::TakeOp);
1548         action->setIcon(KIcon("irc-remove-operator"));
1549         action = modes->addAction(i18n("Give Voice"), this, SLOT(handleContextActions()));
1550         action->setData(Konversation::GiveVoice);
1551         action->setIcon(KIcon("irc-voice"));
1552         action = modes->addAction(i18n("Take Voice"), this, SLOT(handleContextActions()));
1553         action->setData(Konversation::TakeVoice);
1554         action->setIcon(KIcon("irc-unvoice"));
1555
1556         QMenu* kickban = m_nickPopup->addMenu(i18n("Kick / Ban"));
1557         action = kickban->addAction(i18n("Kick"), this, SLOT(handleContextActions()));
1558         action->setData(Konversation::Kick);
1559         action = kickban->addAction(i18n("Kickban"), this, SLOT(handleContextActions()));
1560         action->setData(Konversation::KickBan);
1561         action = kickban->addAction(i18n("Ban Nickname"), this, SLOT(handleContextActions()));
1562         action->setData(Konversation::BanNick);
1563         kickban->addSeparator();
1564         action = kickban->addAction(i18n("Ban *!*@*.host"), this, SLOT(handleContextActions()));
1565         action->setData(Konversation::BanHost);
1566         action = kickban->addAction(i18n("Ban *!*@domain"), this, SLOT(handleContextActions()));
1567         action->setData(Konversation::BanDomain);
1568         action = kickban->addAction(i18n("Ban *!user@*.host"), this, SLOT(handleContextActions()));
1569         action->setData(Konversation::BanUserHost);
1570         action = kickban->addAction(i18n("Ban *!user@domain"), this, SLOT(handleContextActions()));
1571         action->setData(Konversation::BanUserDomain);
1572         kickban->addSeparator();
1573         action = kickban->addAction(i18n("Kickban *!*@*.host"), this, SLOT(handleContextActions()));
1574         action->setData(Konversation::KickBanHost);
1575         action = kickban->addAction(i18n("Kickban *!*@domain"), this, SLOT(handleContextActions()));
1576         action->setData(Konversation::KickBanDomain);
1577         action = kickban->addAction(i18n("Kickban *!user@*.host"), this, SLOT(handleContextActions()));
1578         action->setData(Konversation::KickBanUserHost);
1579         action = kickban->addAction(i18n("Kickban *!user@domain"), this, SLOT(handleContextActions()));
1580         action->setData(Konversation::KickBanUserDomain);
1581     }
1582
1583     m_ignoreAction = new KToggleAction(i18n("Ignore"), this);
1584     m_ignoreAction->setCheckedState(KGuiItem(i18n("Unignore")));
1585     m_ignoreAction->setData(Konversation::IgnoreNick);
1586     m_nickPopup->addAction(m_ignoreAction);
1587     connect(m_ignoreAction, SIGNAL(triggered()), this, SLOT(handleContextActions()));
1588
1589     m_nickPopup->addSeparator();
1590
1591     action = m_nickPopup->addAction(i18n("Open Query"), this, SLOT(handleContextActions()));
1592     action->setData(Konversation::OpenQuery);
1593
1594     KConfigGroup config = KGlobal::config()->group("KDE Action Restrictions");
1595
1596     if(config.readEntry<bool>("allow_downloading", true))
1597     {
1598         action = m_nickPopup->addAction(SmallIcon("arrow-right-double"),i18n("Send &File..."), this, SLOT(handleContextActions()));
1599         action->setData(Konversation::DccSend);
1600     }
1601
1602     m_nickPopup->addSeparator();
1603
1604     m_addNotifyAction = m_nickPopup->addAction(i18n("Add to Watched Nicks"), this, SLOT(handleContextActions()));
1605     m_addNotifyAction->setData(Konversation::AddNotify);
1606 }
1607
1608 void IRCView::setNickAndChannelContextMenusEnabled(bool enable)
1609 {
1610     if (m_nickPopup) m_nickPopup->setEnabled(enable);
1611     if (m_channelPopup) m_channelPopup->setEnabled(enable);
1612 }
1613
1614 const QString& IRCView::getContextNick() const
1615 {
1616     return m_currentNick;
1617 }
1618
1619 void IRCView::clearContextNick()
1620 {
1621     m_currentNick.clear();
1622 }
1623
1624 void IRCView::updateNickMenuEntries(const QString& nickname)
1625 {
1626     if (Preferences::isIgnored(nickname))
1627     {
1628         m_ignoreAction->setChecked(true);
1629         m_ignoreAction->setData(Konversation::UnignoreNick);
1630     }
1631     else
1632     {
1633         m_ignoreAction->setChecked(false);
1634         m_ignoreAction->setData(Konversation::IgnoreNick);
1635     }
1636
1637     if (!m_server || !m_server->getServerGroup() || !m_server->isConnected() || !Preferences::hasNotifyList(m_server->getServerGroup()->id())
1638         || Preferences::isNotify(m_server->getServerGroup()->id(), nickname))
1639     {
1640         m_addNotifyAction->setEnabled(false);
1641     }
1642     else
1643     {
1644         m_addNotifyAction->setEnabled(true);
1645     }
1646 }
1647
1648 void IRCView::setupChannelPopupMenu()
1649 {
1650     m_channelPopup = new KMenu(this);
1651     m_channelPopup->setObjectName("channel_context_menu");
1652     m_channelPopup->setTitle(m_currentChannel);
1653
1654     QAction* action = m_channelPopup->addAction(i18n("&Join Channel..."), this, SLOT(handleContextActions()));
1655     action->setData(Konversation::Join);
1656     action->setIcon(KIcon("irc-join-channel"));
1657     action = m_channelPopup->addAction(i18n("Get &user list"), this, SLOT(handleContextActions()));
1658     action->setData(Konversation::Names);
1659     action = m_channelPopup->addAction(i18n("Get &topic"), this, SLOT(handleContextActions()));
1660     action->setData(Konversation::Topic);
1661 }
1662
1663 void IRCView::contextMenuEvent(QContextMenuEvent* ev)
1664 {
1665     if (m_nickPopup && m_server && m_isOnNick && m_nickPopup->isEnabled())
1666     {
1667         updateNickMenuEntries(getContextNick());
1668
1669         if(m_nickPopup->exec(ev->globalPos()) == 0)
1670             clearContextNick();
1671
1672         m_isOnNick = false;
1673     }
1674     else if(m_channelPopup && m_server && m_isOnChannel && m_channelPopup->isEnabled())
1675     {
1676         m_channelPopup->exec(ev->globalPos());
1677         m_isOnChannel = false;
1678     }
1679     else
1680     {
1681         KActionCollection* actionCollection = Application::instance()->getMainWindow()->actionCollection();
1682         KToggleAction* toggleMenuBarAction = static_cast<KToggleAction*>(actionCollection->action("options_show_menubar"));
1683         QAction* separator = NULL;
1684
1685         if(toggleMenuBarAction && !toggleMenuBarAction->isChecked())
1686         {
1687             m_popup->insertAction(m_copyUrlClipBoard, toggleMenuBarAction);
1688             separator = m_popup->insertSeparator(m_copyUrlClipBoard);
1689         }
1690
1691 #if KDE_IS_VERSION(4,5,0)
1692         updateWebShortcutMenu();
1693 #endif
1694
1695         m_popup->exec(ev->globalPos());
1696
1697         if(separator)
1698         {
1699             m_popup->removeAction(toggleMenuBarAction);
1700             m_popup->removeAction(separator);
1701         }
1702     }
1703 }
1704
1705 void IRCView::handleContextActions()
1706 {
1707     QAction* action = qobject_cast<QAction*>(sender());
1708
1709     emit popupCommand(action->data().toInt());
1710 }
1711
1712 void IRCView::updateWebShortcutMenu()
1713 {
1714 #if KDE_IS_VERSION(4,5,0)
1715     QString selectedText = textCursor().selectedText();
1716
1717     if (selectedText.isEmpty())
1718     {
1719         m_webShortcutMenu->menuAction()->setVisible(false);
1720
1721         return;
1722     }
1723
1724     m_webShortcutMenu->clear();
1725
1726     KUriFilterData filterData(selectedText.remove('\n').remove('\r'));
1727
1728     // Unfortunately if we don't do this here, then KUriFilterData::preferredSearchProviders()
1729     // will later return an empty list when the user has his default search engine set to
1730     // "None" in the Web Shortcuts configuration. I consider this nonsensical coupling between
1731     // the default search engine setting and the list of enabled web shortcuts a bug in
1732     // the kuriikwsfilter plugin, considering we don't actually have any interest in the default
1733     // search engine. I picked Google because I expect that web shortcut to be around for some
1734     // time to come.
1735     filterData.setAlternateDefaultSearchProvider("google");
1736
1737     if (KUriFilter::self()->filterUri(filterData, QStringList() << "kuriikwsfilter"))
1738     {
1739         QStringList searchProviders = filterData.preferredSearchProviders();
1740
1741         if (!searchProviders.isEmpty())
1742         {
1743             const QString squeezedText = KStringHandler::rsqueeze(selectedText, 21);
1744             m_webShortcutMenu->setTitle(i18n("Search for '%1' with", squeezedText));
1745
1746             m_webShortcutMenu->menuAction()->setVisible(true);
1747
1748             KAction* action = 0;
1749
1750             foreach(const QString& searchProvider, searchProviders)
1751             {
1752                 action = new KAction(searchProvider, m_webShortcutMenu);
1753                 action->setIcon(KIcon(filterData.iconNameForPreferredSearchProvider(searchProvider)));
1754                 action->setData(filterData.queryForPreferredSearchProvider(searchProvider));
1755                 connect(action, SIGNAL(triggered()), this, SLOT(handleWebShortcutAction()));
1756                 m_webShortcutMenu->addAction(action);
1757             }
1758
1759             m_webShortcutMenu->addSeparator();
1760
1761             action = new KAction(i18n("Configure Web Shortcuts..."), m_webShortcutMenu);
1762             action->setIcon(KIcon("configure"));
1763             connect(action, SIGNAL(triggered()), this, SLOT(configureWebShortcuts()));
1764             m_webShortcutMenu->addAction(action);
1765
1766             return;
1767         }
1768     }
1769
1770     m_webShortcutMenu->menuAction()->setVisible(false);
1771 #endif
1772 }
1773
1774 void IRCView::handleWebShortcutAction()
1775 {
1776 #if KDE_IS_VERSION(4,5,0)
1777     KAction* action = qobject_cast<KAction*>(sender());
1778
1779     if (action)
1780     {
1781         KUriFilterData filterData(action->data().toString());
1782
1783         if (KUriFilter::self()->filterUri(filterData, QStringList() << "kurisearchfilter"))
1784             Application::instance()->openUrl(filterData.uri().url());
1785     }
1786 #endif
1787 }
1788
1789 void IRCView::configureWebShortcuts()
1790 {
1791     KToolInvocation::kdeinitExec("kcmshell4", QStringList() << "ebrowsing");
1792 }
1793
1794 // For more information about these RTFM
1795 // http://www.unicode.org/reports/tr9/
1796 // http://www.w3.org/TR/unicode-xml/
1797 QChar IRCView::LRM = (ushort)0x200e; // Right-to-Left Mark
1798 QChar IRCView::RLM = (ushort)0x200f; // Left-to-Right Mark
1799 QChar IRCView::LRE = (ushort)0x202a; // Left-to-Right Embedding
1800 QChar IRCView::RLE = (ushort)0x202b; // Right-to-Left Embedding
1801 QChar IRCView::RLO = (ushort)0x202e; // Right-to-Left Override
1802 QChar IRCView::LRO = (ushort)0x202d; // Left-to-Right Override
1803 QChar IRCView::PDF = (ushort)0x202c; // Previously Defined Format
1804
1805 QChar::Direction IRCView::basicDirection(const QString& string)
1806 {
1807     // The following code decides between LTR or RTL direction for
1808     // a line based on the amount of each type of characters pre-
1809     // sent. It does so by counting, but stops when one of the two
1810     // counters becomes higher than half of the string length to
1811     // avoid unnecessary work.
1812
1813     unsigned int pos = 0;
1814     unsigned int rtl_chars = 0;
1815     unsigned int ltr_chars = 0;
1816     unsigned int str_len = string.length();
1817     unsigned int str_half_len = str_len/2;
1818
1819     for(pos=0; pos < str_len; ++pos)
1820     {
1821         if (!(string[pos].isNumber() || string[pos].isSymbol() ||
1822             string[pos].isSpace()  || string[pos].isPunct()  ||
1823             string[pos].isMark()))
1824         {
1825             switch(string[pos].direction())
1826             {
1827                 case QChar::DirL:
1828                 case QChar::DirLRO:
1829                 case QChar::DirLRE:
1830                     ltr_chars++;
1831                     break;
1832                 case QChar::DirR:
1833                 case QChar::DirAL:
1834                 case QChar::DirRLO:
1835                 case QChar::DirRLE:
1836                     rtl_chars++;
1837                     break;
1838                 default:
1839                     break;
1840             }
1841         }
1842
1843         if (ltr_chars > str_half_len)
1844             return QChar::DirL;
1845         else if (rtl_chars > str_half_len)
1846             return QChar::DirR;
1847     }
1848
1849     if (rtl_chars > ltr_chars)
1850         return QChar::DirR;
1851     else
1852         return QChar::DirL;
1853 }