1 /*
2 SPDX-License-Identifier: GPL-2.0-or-later
3
4 SPDX-FileCopyrightText: 2002 Dario Abatianni <eisfuchs@tigress.com>
5 SPDX-FileCopyrightText: 2006-2008 Eike Hein <hein@kde.org>
6 */
7
8 #include "chatwindow.h"
9
10 #include "channel.h"
11 #include "query.h"
12 #include "ircview.h"
13 #include "ircinput.h"
14 #include "server.h"
15 #include "application.h"
16 #include "logfilereader.h"
17 #include "viewcontainer.h"
18 #include "konversation_log.h"
19
20 #include <KUser>
21
22 #include <QDateTime>
23 #include <QDir>
24 #include <QTextCodec>
25 #include <QKeyEvent>
26 #include <QScrollBar>
27 #include <QLocale>
28
29
ChatWindow(QWidget * parent)30 ChatWindow::ChatWindow(QWidget* parent) : QWidget(parent)
31 {
32 auto* mainLayout = new QVBoxLayout(this);
33 mainLayout->setContentsMargins(margin(), margin(), margin(), margin());
34 mainLayout->setSpacing(spacing());
35
36 setName(QStringLiteral("ChatWindowObject"));
37 setTextView(nullptr);
38 setInputBar(nullptr);
39 firstLog = true;
40 m_server = nullptr;
41 m_recreationScheduled = false;
42 m_isTopLevelView = true;
43 m_notificationsEnabled = true;
44 m_channelEncodingSupported = false;
45 m_currentTabNotify = Konversation::tnfNone;
46 }
47
~ChatWindow()48 ChatWindow::~ChatWindow()
49 {
50 if (getInputBar() && getServer())
51 {
52 const QString& language = getInputBar()->spellCheckingLanguage();
53
54 if (!language.isEmpty())
55 {
56 Konversation::ServerGroupSettingsPtr serverGroup = getServer()->getConnectionSettings().serverGroup();
57
58 if (serverGroup)
59 Preferences::setSpellCheckingLanguage(serverGroup, getName(), language);
60 else
61 Preferences::setSpellCheckingLanguage(getServer()->getDisplayName(), getName(), language);
62 }
63 }
64
65 Q_EMIT closing(this);
66 m_server=nullptr;
67 }
68
childEvent(QChildEvent * event)69 void ChatWindow::childEvent(QChildEvent* event)
70 {
71 if(event->type() == QChildEvent::ChildAdded)
72 {
73 if(event->child()->isWidgetType())
74 {
75 layout()->addWidget(qobject_cast< QWidget* >(event->child()));
76 }
77 }
78 else if(event->type() == QChildEvent::ChildRemoved)
79 {
80 if(event->child()->isWidgetType())
81 {
82 layout()->removeWidget(qobject_cast<QWidget*>(event->child()));
83 }
84 }
85 }
86
87 // reimplement this if your window needs special close treatment
closeYourself(bool)88 bool ChatWindow::closeYourself(bool /* askForConfirmation */)
89 {
90 deleteLater();
91
92 return true;
93 }
94
cycle()95 void ChatWindow::cycle()
96 {
97 m_recreationScheduled = true;
98
99 closeYourself(false);
100 }
101
updateAppearance()102 void ChatWindow::updateAppearance()
103 {
104 if (getTextView()) getTextView()->updateAppearance();
105
106 // The font size of the KTabWidget container may be inappropriately
107 // small due to the "Tab bar" font size setting.
108 setFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
109 }
110
setName(const QString & newName)111 void ChatWindow::setName(const QString& newName)
112 {
113 name=newName;
114 Q_EMIT nameChanged(this,newName);
115 }
116
getName() const117 QString ChatWindow::getName() const
118 {
119 return name;
120 }
121
getTitle() const122 QString ChatWindow::getTitle() const
123 {
124 QString title;
125 if (getType() == Channel)
126 {
127 title = QStringLiteral("%1 (%2)")
128 .arg(getName(), getServer()->getDisplayName());
129 }
130 else
131 {
132 title = getName();
133 }
134
135 return title;
136 }
137
getURI(bool passNetwork)138 QString ChatWindow::getURI(bool passNetwork)
139 {
140 QString protocol;
141 QString url;
142 QString port;
143 QString server;
144 QString channel;
145
146 if (getServer()->getUseSSL())
147 protocol = QStringLiteral("ircs://");
148 else
149 protocol = QStringLiteral("irc://");
150
151 if (getType() == Channel)
152 {
153 channel = getName();
154 if (channel.startsWith(QLatin1Char('#'))) {
155 channel.remove(0, 1);
156 }
157
158 // must protect second #, but might as well protect all of them
159 channel.replace(QLatin1String("#"), QLatin1String("%23"));
160 }
161
162 if (passNetwork)
163 {
164 server = getServer()->getDisplayName();
165
166 QUrl test(protocol+server);
167
168 // QUrl (ultimately used by the bookmark system, which is the
169 // primary consumer here) doesn't like spaces in hostnames as
170 // well as other things which are possible in user-chosen net-
171 // work names, so let's fall back to the hostname if we can't
172 // get the network name by it.
173 if (!test.isValid())
174 passNetwork = false;
175 }
176
177 if (!passNetwork)
178 {
179 server = getServer()->getServerName();
180 port = QLatin1Char(':') + QString::number(getServer()->getPort());
181 }
182
183 if (server.contains(QLatin1Char(':'))) // IPv6
184 server = QLatin1Char('[') + server + QLatin1Char(']');
185
186 url = protocol + server + port + QLatin1Char('/') + channel;
187
188 return url;
189 }
190
setType(WindowType newType)191 void ChatWindow::setType(WindowType newType)
192 {
193 type=newType;
194 }
195
getType() const196 ChatWindow::WindowType ChatWindow::getType() const
197 {
198 return type;
199 }
200
isTopLevelView() const201 bool ChatWindow::isTopLevelView() const
202 {
203 return m_isTopLevelView;
204 }
205
setServer(Server * newServer)206 void ChatWindow::setServer(Server* newServer)
207 {
208 if (!newServer)
209 {
210 qCDebug(KONVERSATION_LOG) << "ChatWindow::setServer(0)!";
211 }
212 else
213 {
214 m_server=newServer;
215 connect(m_server, &Server::serverOnline, this, &ChatWindow::serverOnline);
216
217 // check if we need to set up the signals
218 if(getType() != ChannelList)
219 {
220 if(textView) textView->setServer(newServer);
221 else qCDebug(KONVERSATION_LOG) << "textView==0!";
222 }
223
224 serverOnline(m_server->isConnected());
225 }
226
227 if (getInputBar())
228 {
229 QString language;
230
231 Konversation::ServerGroupSettingsPtr serverGroup = newServer->getConnectionSettings().serverGroup();
232
233 if (serverGroup)
234 language = Preferences::spellCheckingLanguage(serverGroup, getName());
235 else
236 language = Preferences::spellCheckingLanguage(newServer->getDisplayName(), getName());
237
238 if (!language.isEmpty())
239 getInputBar()->setSpellCheckingLanguage(language);
240 }
241 }
242
getServer() const243 Server* ChatWindow::getServer() const
244 {
245 return m_server;
246 }
247
serverOnline(bool)248 void ChatWindow::serverOnline(bool /* state */)
249 {
250 //Q_EMIT online(this,state);
251 }
252
setTextView(IRCView * newView)253 void ChatWindow::setTextView(IRCView* newView)
254 {
255 textView = newView;
256
257 if(!textView)
258 {
259 return;
260 }
261
262 textView->setVerticalScrollBarPolicy(Preferences::self()->showIRCViewScrollBar() ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff);
263
264 textView->setChatWin(this);
265 connect(textView, &IRCView::textToLog, this, &ChatWindow::logText);
266 connect(textView, &IRCView::setStatusBarTempText, this, &ChatWindow::setStatusBarTempText);
267 connect(textView, &IRCView::clearStatusBarTempText, this, &ChatWindow::clearStatusBarTempText);
268 }
269
appendRaw(const QString & message,bool self)270 void ChatWindow::appendRaw(const QString& message, bool self)
271 {
272 if(!textView) return;
273 textView->appendRaw(message, self);
274 }
275
appendLog(const QString & message)276 void ChatWindow::appendLog(const QString& message)
277 {
278 if(!textView) return;
279 textView->appendLog(message);
280 }
281
append(const QString & nickname,const QString & message,const QHash<QString,QString> & messageTags,const QString & label)282 void ChatWindow::append(const QString& nickname, const QString& message, const QHash<QString, QString> &messageTags, const QString& label)
283 {
284 if(!textView) return;
285 textView->append(nickname, message, messageTags, label);
286 }
287
appendQuery(const QString & nickname,const QString & message,const QHash<QString,QString> & messageTags,bool inChannel)288 void ChatWindow::appendQuery(const QString& nickname, const QString& message, const QHash<QString, QString> &messageTags, bool inChannel)
289 {
290 if(!textView) return ;
291 textView->appendQuery(nickname, message, messageTags, inChannel);
292 }
293
appendAction(const QString & nickname,const QString & message,const QHash<QString,QString> & messageTags)294 void ChatWindow::appendAction(const QString& nickname, const QString& message, const QHash<QString, QString> &messageTags)
295 {
296 if(!textView) return;
297
298 if (getType() == Query || getType() == DccChat)
299 textView->appendQueryAction(nickname, message, messageTags);
300 else
301 textView->appendChannelAction(nickname, message, messageTags);
302 }
303
appendServerMessage(const QString & type,const QString & message,const QHash<QString,QString> & messageTags,bool parseURL)304 void ChatWindow::appendServerMessage(const QString& type, const QString& message, const QHash<QString, QString> &messageTags, bool parseURL)
305 {
306 if(!textView) return ;
307 textView->appendServerMessage(type, message, messageTags, parseURL);
308 }
309
appendCommandMessage(const QString & command,const QString & message,const QHash<QString,QString> & messageTags,bool parseURL,bool self)310 void ChatWindow::appendCommandMessage(const QString& command, const QString& message, const QHash<QString, QString> &messageTags, bool parseURL, bool self)
311 {
312 if(!textView) return ;
313 textView->appendCommandMessage(command, message, messageTags, parseURL, self);
314 }
315
appendBacklogMessage(const QString & firstColumn,const QString & message)316 void ChatWindow::appendBacklogMessage(const QString& firstColumn,const QString& message)
317 {
318 if(!textView) return ;
319 textView->appendBacklogMessage(firstColumn,Konversation::sterilizeUnicode(message));
320 }
321
clear()322 void ChatWindow::clear()
323 {
324 if (!textView) return;
325
326 textView->clear();
327
328 resetTabNotification();
329
330 if (m_server)
331 m_server->getViewContainer()->unsetViewNotification(this);
332 }
333
cdIntoLogPath()334 void ChatWindow::cdIntoLogPath()
335 {
336 QString home = KUser(KUser::UseRealUserID).homeDir();
337 QUrl logUrl = Preferences::self()->logfilePath();
338
339 if(!logUrl.isLocalFile())
340 {
341 return;
342 }
343
344 QString logPath = logUrl.toLocalFile();
345
346 QDir logDir(home);
347
348 // Try to "cd" into the logfile path.
349 if (!logDir.cd(logPath))
350 {
351 // Only create log path if logging is enabled.
352 if (log())
353 {
354 // Try to create the logfile path and "cd" into it again.
355 logDir.mkpath(logPath);
356 logDir.cd(logPath);
357 }
358 }
359
360 // Add the logfile name to the path.
361 logfile.setFileName(logDir.path() + QLatin1Char('/') + logName);
362 }
363
setLogfileName(const QString & name)364 void ChatWindow::setLogfileName(const QString& name)
365 {
366 // Only change name of logfile if the window was new.
367 if(firstLog)
368 {
369 if (getTextView())
370 getTextView()->setContextMenuOptions(IrcContextMenus::ShowLogAction, true);
371
372 // status panels get special treatment here, since they have no server at the beginning
373 if (getType() == Status || getType() == DccChat)
374 {
375 logName = name + QLatin1String(".log");
376 }
377 else if (m_server)
378 {
379 // make sure that no path delimiters are in the name
380 logName = QString(m_server->getDisplayName().toLower() + QLatin1Char('_') + name + QLatin1String(".log")).replace(QLatin1Char('/'), QLatin1Char('_'));
381 }
382
383 // load backlog to show
384 if(Preferences::self()->showBacklog())
385 {
386 // "cd" into log path or create path, if it's not there
387 cdIntoLogPath();
388 // Show last log lines. This idea was stole ... um ... inspired by PMP :)
389 // Don't do this for the server status windows, though
390 if((getType() != Status) && logfile.open(QIODevice::ReadOnly))
391 {
392 qint64 filePosition;
393
394 QTextStream backlog(&logfile);
395 backlog.setCodec(QTextCodec::codecForName("UTF-8"));
396 backlog.setAutoDetectUnicode(true);
397
398 QStringList firstColumns;
399 QStringList messages;
400 int offset = 0;
401 qint64 lastPacketHeadPosition = backlog.device()->size();
402 const unsigned int packetSize = 4096;
403 while(messages.count() < Preferences::self()->backlogLines() && backlog.device()->size() > packetSize * offset)
404 {
405 QStringList firstColumnsInPacket;
406 QStringList messagesInPacket;
407
408 // packetSize * offset < size <= packetSize * ( offset + 1 )
409
410 // Check if the log is bigger than packetSize * ( offset + 1 )
411 if(backlog.device()->size() > packetSize * ( offset + 1 ))
412 {
413 // Set file pointer to the packet size above the offset
414 backlog.seek(backlog.device()->size() - packetSize * ( offset + 1 ));
415 // Skip first line, since it may be incomplete
416 backlog.readLine();
417 }
418 else
419 {
420 // Set file pointer to the head
421
422 // Qt 4.5 Doc: Note that when using a QTextStream on a
423 // QFile, calling reset() on the QFile will not have the
424 // expected result because QTextStream buffers the file.
425 // Use the QTextStream::seek() function instead.
426 // backlog.device()->reset();
427 backlog.seek( 0 );
428 }
429
430 // remember actual file position to check for deadlocks
431 filePosition = backlog.pos();
432
433 qint64 currentPacketHeadPosition = filePosition;
434
435 // Loop until end of file reached
436 while(!backlog.atEnd() && filePosition < lastPacketHeadPosition)
437 {
438 const QString backlogFileLine = backlog.readLine();
439
440 // check for deadlocks
441 if(backlog.pos() == filePosition)
442 {
443 backlog.seek(filePosition + 1);
444 }
445
446 // if a tab character is present in the line, meaning it is a valid chatline
447 const int tabIndex = backlogFileLine.indexOf(QLatin1Char('\t'));
448 if (tabIndex != -1) {
449 // extract first column from log
450 const QString backlogFirst = backlogFileLine.left(tabIndex);
451 // cut first column from line
452 const QString backlogLine = backlogFileLine.mid(tabIndex + 1);
453 // Logfile is in utf8 so we don't need to do encoding stuff here
454 // append backlog with time and first column to text view
455 firstColumnsInPacket << backlogFirst;
456 messagesInPacket << backlogLine;
457 }
458
459 // remember actual file position to check for deadlocks
460 filePosition = backlog.pos();
461 } // while
462
463 // remember the position not to read the same lines again
464 lastPacketHeadPosition = currentPacketHeadPosition;
465 ++offset;
466
467 firstColumns = firstColumnsInPacket + firstColumns;
468 messages = messagesInPacket + messages;
469 }
470 backlog.setDevice(nullptr);
471 logfile.close();
472
473 // trim
474 int surplus = messages.count() - Preferences::self()->backlogLines();
475 // "surplus" can be a minus value. (when the backlog is too short)
476 if(surplus > 0)
477 {
478 for(int i = 0 ; i < surplus ; ++i)
479 {
480 firstColumns.pop_front();
481 messages.pop_front();
482 }
483 }
484
485 QStringList::Iterator itFirstColumn = firstColumns.begin();
486 QStringList::Iterator itMessage = messages.begin();
487 for( ; itFirstColumn != firstColumns.end() ; ++itFirstColumn, ++itMessage )
488 appendBacklogMessage(*itFirstColumn, *itMessage);
489 }
490 } // if(Preferences::showBacklog())
491 }
492 }
493
logText(const QString & text)494 void ChatWindow::logText(const QString& text)
495 {
496 if(log())
497 {
498 // "cd" into log path or create path, if it's not there
499 cdIntoLogPath();
500
501 if(logfile.open(QIODevice::WriteOnly | QIODevice::Append))
502 {
503 // wrap the file into a stream
504 QTextStream logStream(&logfile);
505 // write log in utf8 to help i18n
506 logStream.setCodec(QTextCodec::codecForName("UTF-8"));
507 logStream.setAutoDetectUnicode(true);
508
509 if(firstLog)
510 {
511 QString intro(i18n("\n*** Logfile started\n*** on %1\n\n", QDateTime::currentDateTime().toString()));
512 logStream << intro;
513 firstLog=false;
514 }
515
516 QDateTime dateTime = QDateTime::currentDateTime();
517 QString logLine = QStringLiteral("[%1] [%2] %3\n").arg(QLocale().toString(dateTime.date(), QLocale::LongFormat), QLocale().toString(dateTime.time(), QLocale::LongFormat), text);
518 logStream << logLine;
519
520 // detach stream from file
521 logStream.setDevice(nullptr);
522
523 // close file
524 logfile.close();
525 }
526 else qCWarning(KONVERSATION_LOG) << "open(QIODevice::Append) for " << logfile.fileName() << " failed!";
527 }
528 }
529
setChannelEncodingSupported(bool enabled)530 void ChatWindow::setChannelEncodingSupported(bool enabled)
531 {
532 m_channelEncodingSupported = enabled;
533 }
534
isChannelEncodingSupported() const535 bool ChatWindow::isChannelEncodingSupported() const
536 {
537 return m_channelEncodingSupported;
538 }
539
spacing()540 int ChatWindow::spacing()
541 {
542 if(Preferences::self()->useSpacing())
543 return Preferences::self()->spacing();
544 else
545 return style()->layoutSpacing(QSizePolicy::DefaultType, QSizePolicy::DefaultType, Qt::Vertical);
546 }
547
margin()548 int ChatWindow::margin()
549 {
550 if(Preferences::self()->useSpacing())
551 return Preferences::self()->margin();
552 else
553 return 0;
554 }
555
556 // Accessors
getTextView() const557 IRCView* ChatWindow::getTextView() const
558 {
559 return textView;
560 }
561
log() const562 bool ChatWindow::log() const
563 {
564 return Preferences::self()->log();
565 }
566
567 // reimplement this in all panels that have user input
getTextInLine() const568 QString ChatWindow::getTextInLine() const
569 {
570 if (m_inputBar)
571 return m_inputBar->toPlainText();
572 else
573 return QString();
574 }
575
canBeFrontView() const576 bool ChatWindow::canBeFrontView() const
577 {
578 return false;
579 }
580
searchView() const581 bool ChatWindow::searchView() const
582 {
583 return false;
584 }
585
586 // reimplement this in all panels that have user input
indicateAway(bool)587 void ChatWindow::indicateAway(bool)
588 {
589 }
590
591 // reimplement this in all panels that have user input
appendInputText(const QString & text,bool fromCursor)592 void ChatWindow::appendInputText(const QString& text, bool fromCursor)
593 {
594 if (!fromCursor)
595 m_inputBar->append(text);
596 else
597 {
598 const int position = m_inputBar->textCursor().position();
599 m_inputBar->textCursor().insertText(text);
600 QTextCursor cursor = m_inputBar->textCursor();
601 cursor.setPosition(position + text.length());
602 m_inputBar->setTextCursor(cursor);
603 }
604 }
605
eventFilter(QObject * watched,QEvent * e)606 bool ChatWindow::eventFilter(QObject* watched, QEvent* e)
607 {
608 if(e->type() == QEvent::KeyPress)
609 {
610 auto* ke = static_cast<QKeyEvent*>(e);
611
612 bool scrollMod = (Preferences::self()->useMultiRowInputBox() ? false : (ke->modifiers() == Qt::ShiftModifier));
613
614 if(ke->key() == Qt::Key_Up && scrollMod)
615 {
616 if(textView)
617 {
618 QScrollBar* sbar = textView->verticalScrollBar();
619 sbar->setValue(sbar->value() - sbar->singleStep());
620 }
621
622 return true;
623 }
624 else if(ke->key() == Qt::Key_Down && scrollMod)
625 {
626 if(textView)
627 {
628 QScrollBar* sbar = textView->verticalScrollBar();
629 sbar->setValue(sbar->value() + sbar->singleStep());
630 }
631
632 return true;
633 }
634 else if(ke->modifiers() == Qt::NoModifier && ke->key() == Qt::Key_PageUp)
635 {
636 if(textView)
637 {
638 QScrollBar* sbar = textView->verticalScrollBar();
639 sbar->setValue(sbar->value() - sbar->pageStep());
640 }
641
642 return true;
643 }
644 else if(ke->modifiers() == Qt::NoModifier && ke->key() == Qt::Key_PageDown)
645 {
646 if(textView)
647 {
648 QScrollBar* sbar = textView->verticalScrollBar();
649 sbar->setValue(sbar->value() + sbar->pageStep());
650 }
651
652 return true;
653 }
654
655 }
656
657 return QWidget::eventFilter(watched, e);
658 }
659
adjustFocus()660 void ChatWindow::adjustFocus()
661 {
662 childAdjustFocus();
663 }
664
emitUpdateInfo()665 void ChatWindow::emitUpdateInfo()
666 {
667 QString info = getName();
668 Q_EMIT updateInfo(info);
669 }
670
highlightColor()671 QColor ChatWindow::highlightColor()
672 {
673 return getTextView()->highlightColor();
674 }
675
activateTabNotification(Konversation::TabNotifyType type)676 void ChatWindow::activateTabNotification(Konversation::TabNotifyType type)
677 {
678 if (!notificationsEnabled())
679 return;
680
681 if(type > m_currentTabNotify)
682 return;
683
684 m_currentTabNotify = type;
685
686 Q_EMIT updateTabNotification(this,type);
687 }
688
resetTabNotification()689 void ChatWindow::resetTabNotification()
690 {
691 m_currentTabNotify = Konversation::tnfNone;
692 }
693
msgHelper(const QString & recipient,const QString & message)694 void ChatWindow::msgHelper(const QString& recipient, const QString& message)
695 {
696 // A helper method for handling the 'msg' and 'query' (with a message
697 // payload) commands. When the user uses either, we show a visualiza-
698 // tion of what he/she has sent in the form of '<-> target> message>'
699 // in the chat view of the tab the command was issued in, as well as
700 // add the resulting message to the target view (if present), in that
701 // order. The order is especially important as the origin and target
702 // views may be the same, and the two messages may thus appear toge-
703 // ther and should be sensibly ordered.
704
705 if (recipient.isEmpty() || message.isEmpty())
706 return;
707
708 bool isAction = false;
709 QString result = message;
710 QString visualization;
711
712 if (result.startsWith(Preferences::self()->commandChar() + QLatin1String("me")))
713 {
714 isAction = true;
715
716 result.remove(0, 4);
717 visualization = QStringLiteral("* %1 %2").arg(m_server->getNickname(), result);
718 }
719 else
720 visualization = result;
721
722 appendQuery(recipient, visualization, QHash<QString, QString>(), true);
723
724 if (!getServer())
725 return;
726
727 ::Query* query = m_server->getQueryByName(recipient);
728
729 if (query)
730 {
731 if (isAction)
732 query->appendAction(m_server->getNickname(), result);
733 else
734 query->appendQuery(m_server->getNickname(), result);
735
736 return;
737 }
738
739 ::Channel* channel = m_server->getChannelByName(recipient);
740
741 if (channel)
742 {
743 if (isAction)
744 channel->appendAction(m_server->getNickname(), result);
745 else
746 channel->append(m_server->getNickname(), result);
747 }
748 }
749
activateView()750 void ChatWindow::activateView()
751 {
752 Q_EMIT windowActivationRequested();
753 Q_EMIT showView(this);
754 }
755