ircdocktabcontents.cpp
1 //------------------------------------------------------------------------------
2 // ircdocktabcontents.cpp
3 //------------------------------------------------------------------------------
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 // 02110-1301 USA
19 //
20 //------------------------------------------------------------------------------
21 // Copyright (C) 2010 "Zalewa" <zalewapl@gmail.com>
22 //------------------------------------------------------------------------------
23 #include "application.h"
24 #include "gui/commongui.h"
25 #include "gui/irc/ircignoresmanager.h"
26 #include "gui/irc/ircsounds.h"
27 #include "gui/irc/ircuserlistmodel.h"
28 #include "irc/chatlogrotate.h"
29 #include "irc/chatlogs.h"
30 #include "irc/configuration/chatlogscfg.h"
31 #include "irc/configuration/ircconfig.h"
32 #include "irc/entities/ircnetworkentity.h"
33 #include "irc/ircchanneladapter.h"
34 #include "irc/ircdock.h"
35 #include "irc/ircglobal.h"
36 #include "irc/ircmessageclass.h"
37 #include "irc/ircnetworkadapter.h"
38 #include "irc/ircnicknamecompleter.h"
39 #include "irc/ircuserinfo.h"
40 #include "irc/ircuserlist.h"
41 #include "irc/ops/ircdelayedoperationignore.h"
42 #include "ircdocktabcontents.h"
43 #include "log.h"
44 #include "ui_ircdocktabcontents.h"
45 #include <cassert>
46 #include <QDateTime>
47 #include <QFile>
48 #include <QKeyEvent>
49 #include <QMenu>
50 #include <QScrollBar>
51 #include <QStandardItemModel>
52 #include <QTimer>
53 
54 
55 const int IRCDockTabContents::BLINK_TIMER_DELAY_MS = 650;
56 
57 DClass<IRCDockTabContents> : public Ui::IRCDockTabContents
58 {
59 public:
60  QFile log;
61  QDateTime lastMessageDate;
62 
70  bool bBlinkTitle;
71  bool bIsDestroying;
72 
73  QTimer blinkTimer;
74 
75  IRCMessageClass *lastMessageClass;
76  IRCNicknameCompleter *nicknameCompleter;
81  QStringList textOutputContents;
82  ::IRCDockTabContents::UserListMenu *userListContextMenu;
83 };
84 
85 DPointeredNoCopy(IRCDockTabContents)
86 
87 class IRCDockTabContents::UserListMenu : public QMenu
88 {
89 public:
90  UserListMenu();
91 
92  QAction *ban;
93  QAction *whois;
94  QAction *ctcpTime;
95  QAction *ctcpPing;
96  QAction *ctcpVersion;
97  QAction *dehalfOp;
98  QAction *deop;
99  QAction *devoice;
100  QAction *halfOp;
101  QAction *kick;
102  QAction *ignore;
103  QAction *op;
104  QAction *openChatWindow;
105  QAction *voice;
106 
107 private:
108  bool bIsOperator;
109 };
110 
111 IRCDockTabContents::IRCDockTabContents(IRCDock *pParentIRCDock)
112 {
113  d->setupUi(this);
114  d->lastMessageDate = QDateTime::currentDateTime();
115 
116  d->bBlinkTitle = false;
117  d->bIsDestroying = false;
118  d->lastMessageClass = nullptr;
119  d->userListContextMenu = nullptr;
120  this->pIrcAdapter = nullptr;
121 
122  this->pParentIRCDock = pParentIRCDock;
123  d->nicknameCompleter = new IRCNicknameCompleter();
124 
125  d->txtOutputWidget->setContextMenuPolicy(Qt::CustomContextMenu);
126  setupNewUserListModel();
127 
128  // There is only one case in which we want this to be visible:
129  // if we are in a channel.
130  d->lvUserList->setVisible(false);
131 
132  this->connect(d->btnSend, SIGNAL(clicked()), SLOT(sendMessage()));
133  this->connect(d->leCommandLine, SIGNAL(returnPressed()), SLOT(sendMessage()));
134  this->connect(gApp, SIGNAL(focusChanged(QWidget*,QWidget*)),
135  SLOT(onFocusChanged(QWidget*,QWidget*)));
136 
138 
139  d->blinkTimer.setSingleShot(false);
140  this->connect(&d->blinkTimer, SIGNAL(timeout()),
141  SLOT(blinkTimerSlot()));
142 
143  // Performance check line, keep commented for non-testing builds:
144  //receiveMessage(Strings::createRandomAlphaNumericStringWithNewLines(80, 5000));
145 }
146 
147 IRCDockTabContents::~IRCDockTabContents()
148 {
149  d->bIsDestroying = true;
150 
151  if (d->lastMessageClass != nullptr)
152  delete d->lastMessageClass;
153 
154  if (d->userListContextMenu != nullptr)
155  delete d->userListContextMenu;
156 
157  if (pIrcAdapter != nullptr)
158  {
159  disconnect(pIrcAdapter, nullptr, nullptr, nullptr);
160  IRCAdapterBase *pTmpAdapter = pIrcAdapter;
161  pIrcAdapter = nullptr;
162  delete pTmpAdapter;
163  }
164 }
165 
166 void IRCDockTabContents::adapterFocusRequest()
167 {
168  emit focusRequest(this);
169 }
170 
171 void IRCDockTabContents::adapterTerminating()
172 {
173  if (pIrcAdapter != nullptr && !d->bIsDestroying)
174  {
175  // Disconnect the adapter from this tab.
176  disconnect(pIrcAdapter, nullptr, nullptr, nullptr);
177  pIrcAdapter = nullptr;
178 
179  emit chatWindowCloseRequest(this);
180  }
181 }
182 
183 void IRCDockTabContents::alertIfConfigured()
184 {
185  if (gIRCConfig.appearance.windowAlertOnImportantChatEvent)
186  QApplication::alert(gApp->mainWindowAsQWidget());
187 }
188 
190 {
191  const static QString STYLE_SHEET_BASE_TEMPLATE =
192  "QListView, QTextEdit, QLineEdit { background: %1; color: %2; } ";
193 
194  const IRCConfig::AppearanceCfg &appearance = gIRCConfig.appearance;
195 
196  QString qtStyleSheet = STYLE_SHEET_BASE_TEMPLATE
197  .arg(appearance.backgroundColor)
198  .arg(appearance.defaultTextColor);
199 
200  QColor colorSelectedText(appearance.userListSelectedTextColor);
201  QColor colorSelectedBackground(appearance.userListSelectedBackgroundColor);
202  qtStyleSheet += QString("QListView::item:selected { color: %1; background: %2; } ").arg(colorSelectedText.name(), colorSelectedBackground.name());
203 
204  QColor colorHoverText = colorSelectedText.lighter();
205  QColor colorHoverBackground = colorSelectedBackground.lighter();
206  qtStyleSheet += QString("QListView::item:hover { color: %1; background: %2; } ").arg(colorHoverText.name(), colorHoverBackground.name());
207 
208  QString channelActionClassName = IRCMessageClass::toStyleSheetClassName(IRCMessageClass::ChannelAction);
209  QString ctcpClassName = IRCMessageClass::toStyleSheetClassName(IRCMessageClass::Ctcp);
210  QString errorClassName = IRCMessageClass::toStyleSheetClassName(IRCMessageClass::Error);
211  QString networkActionClassName = IRCMessageClass::toStyleSheetClassName(IRCMessageClass::NetworkAction);
212 
213  QString htmlStyleSheetMessageArea = "";
214  htmlStyleSheetMessageArea += "span { white-space: pre; }";
215  htmlStyleSheetMessageArea += QString("a { color: %1; white-space: pre; } ").arg(appearance.urlColor);
216  htmlStyleSheetMessageArea += QString("." + channelActionClassName + " { color: %1; } ").arg(appearance.channelActionColor);
217  htmlStyleSheetMessageArea += QString("." + ctcpClassName + " { color: %1; } ").arg(appearance.ctcpColor);
218  htmlStyleSheetMessageArea += QString("." + errorClassName + " { color: %1; } ").arg(appearance.errorColor);
219  htmlStyleSheetMessageArea += QString("." + networkActionClassName + " { color: %1; } ").arg(appearance.networkActionColor);
220 
221  d->lvUserList->setStyleSheet(qtStyleSheet);
222  d->lvUserList->setFont(appearance.userListFont);
223 
224  d->leCommandLine->installEventFilter(this);
225  d->leCommandLine->setStyleSheet(qtStyleSheet);
226  d->leCommandLine->setFont(appearance.mainFont);
227 
228  d->txtOutputWidget->setStyleSheet(qtStyleSheet);
229  d->txtOutputWidget->setFont(appearance.mainFont);
230 
231  d->txtOutputWidget->document()->setDefaultStyleSheet(htmlStyleSheetMessageArea);
232  d->txtOutputWidget->clear();
233  d->txtOutputWidget->insertHtml(d->textOutputContents.join(""));
234  d->txtOutputWidget->moveCursor(QTextCursor::End);
235 }
236 
237 void IRCDockTabContents::blinkTimerSlot()
238 {
239  setBlinkTitle(!d->bBlinkTitle);
240 }
241 
242 void IRCDockTabContents::completeNickname()
243 {
244  IRCCompletionResult result;
245  if (d->nicknameCompleter->isReset())
246  result = d->nicknameCompleter->complete(d->leCommandLine->text(), d->leCommandLine->cursorPosition());
247  else
248  result = d->nicknameCompleter->cycleNext();
249  if (result.isValid())
250  {
251  // Prevent reset due to cursor position change.
252  d->leCommandLine->blockSignals(true);
253  d->leCommandLine->setText(result.textLine);
254  d->leCommandLine->setCursorPosition(result.cursorPos);
255  d->leCommandLine->blockSignals(false);
256  }
257 }
258 
259 bool IRCDockTabContents::eventFilter(QObject *watched, QEvent *event)
260 {
261  if (watched == d->leCommandLine && event->type() == QEvent::KeyPress)
262  {
263  auto keyEvent = static_cast<QKeyEvent *>(event);
264  if (keyEvent->key() == Qt::Key_Tab)
265  {
266  completeNickname();
267  return true;
268  }
269  }
270  return false;
271 }
272 
273 QStandardItem *IRCDockTabContents::findUserListItem(const QString &nickname)
274 {
275  auto pModel = (QStandardItemModel *)d->lvUserList->model();
276  IRCUserInfo userInfo(nickname, network());
277 
278  for (int i = 0; i < pModel->rowCount(); ++i)
279  {
280  QStandardItem *pItem = pModel->item(i);
281  if (userInfo == IRCUserInfo(pItem->text(), network()))
282  return pItem;
283  }
284 
285  return nullptr;
286 }
287 
288 IRCDockTabContents::UserListMenu &IRCDockTabContents::getUserListContextMenu()
289 {
290  if (d->userListContextMenu == nullptr)
291  d->userListContextMenu = new UserListMenu();
292 
293  return *d->userListContextMenu;
294 }
295 
297 {
298  // Make sure the tab title is not "d->blinkTimered out" anymore.
299  d->blinkTimer.stop();
300  setBlinkTitle(false);
301 
302  d->leCommandLine->setFocus();
303 }
304 
305 bool IRCDockTabContents::hasTabFocus() const
306 {
307  return this->pParentIRCDock->hasTabFocus(this);
308 }
309 
310 QIcon IRCDockTabContents::icon() const
311 {
312  if (pIrcAdapter == nullptr)
313  return QIcon();
314 
315  switch (pIrcAdapter->adapterType())
316  {
317  case IRCAdapterBase::ChannelAdapter:
318  return QIcon(":/icons/irc_channel.png");
319 
320  case IRCAdapterBase::NetworkAdapter:
321  return QIcon(":/flags/lan-small");
322 
323  case IRCAdapterBase::PrivAdapter:
324  return QIcon(":/icons/person.png");
325 
326  default:
327  return QIcon();
328  }
329 }
330 
331 void IRCDockTabContents::insertMessage(const IRCMessageClass &messageClass, const QString &htmlString)
332 {
333  if (d->lastMessageClass == nullptr)
334  d->lastMessageClass = new IRCMessageClass();
335  *d->lastMessageClass = messageClass;
336 
337  d->textOutputContents << htmlString;
338 
339  // Text insertion must be done this way to allow proper
340  // handling of "Pause" button. Note that the cursor
341  // in the widget is not affected as textCursor() creates a copy
342  // of cursor object.
343  QTextCursor cursor = d->txtOutputWidget->textCursor();
344  cursor.movePosition(QTextCursor::End);
345  cursor.insertHtml(htmlString);
346 
347  if (!d->btnPauseTextArea->isChecked())
348  d->txtOutputWidget->moveCursor(QTextCursor::End);
349 
350  emit newMessagePrinted();
351 }
352 
353 void IRCDockTabContents::markDate()
354 {
355  QDateTime previousMessageDate = d->lastMessageDate;
356  QDateTime nowDate = QDateTime::currentDateTime();
357  d->lastMessageDate = nowDate;
358  if (previousMessageDate.daysTo(nowDate) != 0)
359  {
360  receiveMessageWithClass(tr("<<<DATE>>> Date on this computer changes to %1").arg(
361  nowDate.toString()), IRCMessageClass::NetworkAction);
362  }
363 }
364 
365 void IRCDockTabContents::myNicknameUsedSlot()
366 {
367  alertIfConfigured();
368  pParentIRCDock->sounds().playIfAvailable(IRCSounds::NicknameUsed);
369  if (!hasTabFocus())
370  d->blinkTimer.start(BLINK_TIMER_DELAY_MS);
371 }
372 
373 void IRCDockTabContents::nameAdded(const IRCUserInfo &userInfo)
374 {
375  auto pModel = (QStandardItemModel *)d->lvUserList->model();
376  QStandardItem *pItem = new QStandardItem(userInfo.prefixedName());
377  pItem->setData(userInfo.cleanNickname(), IRCUserListModel::RoleCleanNickname);
378 
379  // Try to append the nickname at the proper place in the list.
380  for (int i = 0; i < pModel->rowCount(); ++i)
381  {
382  QStandardItem *pExistingItem = pModel->item(i);
383  QString existingNickname = pExistingItem->text();
384 
385  if (userInfo <= IRCUserInfo(existingNickname, network()))
386  {
387  pModel->insertRow(i, pItem);
388  return;
389  }
390  }
391 
392  // If above code didn't return then
393  // this nickname should be appended to the end of the list.
394  pModel->appendRow(pItem);
395 }
396 
397 void IRCDockTabContents::nameListUpdated(const IRCUserList &userList)
398 {
399  setupNewUserListModel();
400 
401  for (unsigned i = 0; i < userList.size(); ++i)
402  nameAdded(*userList[i]);
403 }
404 
405 void IRCDockTabContents::nameRemoved(const IRCUserInfo &userInfo)
406 {
407  auto pModel = (QStandardItemModel *)d->lvUserList->model();
408  for (int i = 0; i < pModel->rowCount(); ++i)
409  {
410  QStandardItem *pItem = pModel->item(i);
411  if (userInfo.isSameNickname(pItem->text()))
412  {
413  pModel->removeRow(i);
414  break;
415  }
416  }
417 }
418 
419 void IRCDockTabContents::nameUpdated(const IRCUserInfo &userInfo)
420 {
421  nameRemoved(userInfo);
422  nameAdded(userInfo);
423 }
424 
425 IRCNetworkAdapter *IRCDockTabContents::network()
426 {
427  return ircAdapter()->network();
428 }
429 
430 const IRCNetworkEntity &IRCDockTabContents::networkEntity() const
431 {
432  return ircAdapter()->networkEntity();
433 }
434 
436 {
437  // Once a new chat adapter is opened we need to add it to the master
438  // dock widget.
439  pParentIRCDock->addIRCAdapter(pAdapter);
440 }
441 
442 void IRCDockTabContents::onFocusChanged(QWidget *old, QWidget *now)
443 {
444  if (old == d->lvUserList && now != d->userListContextMenu)
445  d->lvUserList->clearSelection();
446 }
447 
448 bool IRCDockTabContents::openLog()
449 {
450  rotateOldLog();
451  ChatLogs logs;
452  if (!logs.mkLogDir(networkEntity()))
453  {
454  receiveMessageWithClass(tr("Failed to create chat log directory:\n'%1'").arg(
455  logs.networkDirPath(networkEntity())), IRCMessageClass::Error);
456  return false;
457  }
458  d->log.setFileName(ChatLogs().logFilePath(networkEntity(), recipient()));
459  d->log.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text);
460  d->log.write(tr("<<<DATE>>> Chat log started on %1\n\n").arg(QDateTime::currentDateTime().toString()).toUtf8());
461  return true;
462 }
463 
464 void IRCDockTabContents::rotateOldLog()
465 {
466  assert(!d->log.isOpen());
467  ChatLogsCfg cfg;
468 
469  ChatLogRotate logRotate;
470  logRotate.setRemovalAgeDaysThreshold(
471  cfg.isRestoreChatFromLogs() ? cfg.oldLogsRemovalDaysThreshold() : -1);
472  logRotate.rotate(networkEntity(), recipient());
473 }
474 
475 void IRCDockTabContents::printToSendersNetworksCurrentChatBox(const QString &text, const IRCMessageClass &msgClass)
476 {
477  auto adapter = static_cast<IRCAdapterBase *>(sender());
478  IRCDockTabContents *tab = pParentIRCDock->tabWithFocus();
479  if (tab != nullptr && tab->ircAdapter()->network()->isAdapterRelated(adapter))
480  tab->ircAdapter()->emitMessageWithClass(text, msgClass);
481  else
482  adapter->emitMessageWithClass(text, msgClass);
483 }
484 
485 void IRCDockTabContents::receiveError(const QString &error)
486 {
487  receiveMessageWithClass(tr("Error: %1").arg(error), IRCMessageClass::Error);
488 }
489 
490 void IRCDockTabContents::receiveMessage(const QString &message)
491 {
492  receiveMessageWithClass(message, IRCMessageClass::Normal);
493 }
494 
495 void IRCDockTabContents::receiveMessageWithClass(const QString &message, const IRCMessageClass &messageClass)
496 {
497  markDate();
498 
499  QString messageHtmlEscaped = message;
500 
501  if (gIRCConfig.appearance.timestamps)
502  {
503  QString timestamp = Strings::timestamp("[hh:mm:ss] ");
504 
505  messageHtmlEscaped = timestamp + messageHtmlEscaped;
506 
507  // It is also required to replace all '\n' characters with timestamp
508  // markers to ensure that timestamp is in every line of text.
509  messageHtmlEscaped = messageHtmlEscaped.replace("\n", "\n" + timestamp);
510  }
511 
512  // As the new-line character is stripped by the lower levels we should
513  // assume that each message ends with a new-line char, as specified
514  // by RFC 1459.
515  messageHtmlEscaped += "\n";
516 
517  writeLog(messageHtmlEscaped);
518 
519  messageHtmlEscaped = wrapTextWithMetaTags(messageHtmlEscaped, messageClass);
520 
521  // Play sound if this is Priv adapter.
522  if (pIrcAdapter->adapterType() == IRCAdapterBase::PrivAdapter)
523  {
524  alertIfConfigured();
525  pParentIRCDock->sounds().playIfAvailable(IRCSounds::PrivateMessageReceived);
526 
527  // If this tab doesn't have focus, also start d->blinkTimering the title.
528  if (!hasTabFocus())
529  d->blinkTimer.start(BLINK_TIMER_DELAY_MS);
530  }
531 
532  this->insertMessage(messageClass, messageHtmlEscaped);
533 }
534 
535 QString IRCDockTabContents::recipient() const
536 {
537  return pIrcAdapter->recipient();
538 }
539 
540 void IRCDockTabContents::resetNicknameCompletion()
541 {
542  d->nicknameCompleter->reset();
543 }
544 
545 bool IRCDockTabContents::restoreLog()
546 {
547  ChatLogs logs;
548  QFile file(logs.logFilePath(networkEntity(), recipient()));
549  if (file.open(QIODevice::ReadOnly | QIODevice::Text))
550  {
551  QByteArray contents = file.readAll();
552  QStringList lines = QString::fromUtf8(contents, contents.size()).split("\n");
553  int line = lines.size() - 1000;
554  lines = lines.mid((line > 0) ? line : 0);
555 
556  insertMessage(IRCMessageClass::Normal,
557  wrapTextWithMetaTags(lines.join("\n"), IRCMessageClass::Normal));
558 
559  receiveMessageWithClass(tr("---- All lines above were loaded from log ----"),
560  IRCMessageClass::NetworkAction);
561  return true;
562  }
563  return false;
564 }
565 
566 QString IRCDockTabContents::selectedNickname()
567 {
568  QModelIndexList selectedIndexes = d->lvUserList->selectionModel()->selectedRows();
569  // There can be only one.
570  if (!selectedIndexes.isEmpty())
571  {
572  int row = selectedIndexes[0].row();
573  auto pModel = (QStandardItemModel *)d->lvUserList->model();
574  QStandardItem *pItem = pModel->item(row);
575 
576  return pItem->text();
577  }
578 
579  return "";
580 }
581 
582 void IRCDockTabContents::sendCtcpPing(const QString &nickname)
583 {
584  network()->sendCtcp(nickname, QString("PING %1").arg(QDateTime::currentMSecsSinceEpoch()));
585 }
586 
587 void IRCDockTabContents::sendCtcpTime(const QString &nickname)
588 {
589  network()->sendCtcp(nickname, QString("TIME"));
590 }
591 
592 void IRCDockTabContents::sendCtcpVersion(const QString &nickname)
593 {
594  network()->sendCtcp(nickname, QString("VERSION"));
595 }
596 
597 void IRCDockTabContents::sendMessage()
598 {
599  QString message = d->leCommandLine->text();
600  d->leCommandLine->setText("");
601 
602  if (!message.trimmed().isEmpty())
603  pIrcAdapter->sendMessage(message);
604 }
605 
606 void IRCDockTabContents::sendWhois(const QString &nickname)
607 {
608  network()->sendMessage(QString("/WHOIS %1").arg(nickname));
609 }
610 
611 void IRCDockTabContents::setBlinkTitle(bool b)
612 {
613  bool bEmit = false;
614  if (d->bBlinkTitle != b)
615  {
616  // Delay signal emit until after we change the variable.
617  bEmit = true;
618  }
619 
620  d->bBlinkTitle = b;
621 
622  if (bEmit)
623  emit titleBlinkRequested();
624 }
625 
627 {
628  assert(pIrcAdapter == nullptr);
629  pIrcAdapter = pAdapter;
630 
631  ChatLogsCfg cfg;
632  if (cfg.isRestoreChatFromLogs())
633  restoreLog();
634  if (cfg.isStoreLogs())
635  openLog();
636 
637  connect(pIrcAdapter, SIGNAL(error(const QString&)), SLOT(receiveError(const QString&)));
638  connect(pIrcAdapter, SIGNAL(focusRequest()), SLOT(adapterFocusRequest()));
639  connect(pIrcAdapter, SIGNAL(message(const QString&)), SLOT(receiveMessage(const QString&)));
640  connect(pIrcAdapter, SIGNAL(messageWithClass(const QString&,const IRCMessageClass&)), SLOT(receiveMessageWithClass(const QString&,const IRCMessageClass&)));
641  connect(pIrcAdapter, SIGNAL(terminating()), SLOT(adapterTerminating()));
642  connect(pIrcAdapter, SIGNAL(titleChange()), SLOT(adapterTitleChange()));
643  connect(pIrcAdapter, SIGNAL(messageToNetworksCurrentChatBox(QString,IRCMessageClass)),
644  SLOT(printToSendersNetworksCurrentChatBox(QString,IRCMessageClass)));
645 
646  switch (pIrcAdapter->adapterType())
647  {
648  case IRCAdapterBase::NetworkAdapter:
649  {
650  auto pNetworkAdapter = (IRCNetworkAdapter *)pAdapter;
651  connect(pNetworkAdapter, SIGNAL(newChatWindowIsOpened(IRCChatAdapter*)), SLOT(newChatWindowIsOpened(IRCChatAdapter*)));
652  break;
653  }
654 
655  case IRCAdapterBase::ChannelAdapter:
656  {
657  auto pChannelAdapter = (IRCChannelAdapter *)pAdapter;
658  connect(pChannelAdapter, SIGNAL(myNicknameUsed()), SLOT(myNicknameUsedSlot()));
659  connect(pChannelAdapter, SIGNAL(nameAdded(const IRCUserInfo&)), SLOT(nameAdded(const IRCUserInfo&)));
660  connect(pChannelAdapter, SIGNAL(nameListUpdated(const IRCUserList&)), SLOT(nameListUpdated(const IRCUserList&)));
661  connect(pChannelAdapter, SIGNAL(nameRemoved(const IRCUserInfo&)), SLOT(nameRemoved(const IRCUserInfo&)));
662  connect(pChannelAdapter, SIGNAL(nameUpdated(const IRCUserInfo&)), SLOT(nameUpdated(const IRCUserInfo&)));
663 
664  d->lvUserList->setVisible(true);
665  connect(d->lvUserList, SIGNAL(customContextMenuRequested(const QPoint&)),
666  SLOT(userListCustomContextMenuRequested(const QPoint&)));
667 
668  connect(d->lvUserList, SIGNAL(doubleClicked(const QModelIndex&)),
669  SLOT(userListDoubleClicked()));
670 
671  d->lvUserList->setContextMenuPolicy(Qt::CustomContextMenu);
672 
673  break;
674  }
675 
676  case IRCAdapterBase::PrivAdapter:
677  {
678  break;
679  }
680 
681  default:
682  {
683  receiveError("Doomseeker error: Unknown IRCAdapterBase*");
684  break;
685  }
686  }
687 }
688 
689 void IRCDockTabContents::setupNewUserListModel()
690 {
691  d->lvUserList->setModel(new QStandardItemModel(d->lvUserList));
692  d->nicknameCompleter->setModel(d->lvUserList->model());
693 }
694 
695 void IRCDockTabContents::showChatContextMenu(const QPoint &pos)
696 {
697  QMenu *menu = d->txtOutputWidget->createStandardContextMenu(pos);
698  if (ircAdapter()->adapterType() == IRCAdapterBase::PrivAdapter)
699  {
700  menu->addSeparator();
701  appendPrivChatContextMenuOptions(menu);
702  }
703  menu->addSeparator();
704  appendGeneralChatContextMenuOptions(menu);
705  menu->exec(d->txtOutputWidget->mapToGlobal(pos));
706  delete menu;
707 }
708 
709 void IRCDockTabContents::appendGeneralChatContextMenuOptions(QMenu *menu)
710 {
711  QAction *manageIgnores = menu->addAction(tr("Manage ignores"));
712  this->connect(manageIgnores, SIGNAL(triggered()), SLOT(showIgnoresManager()));
713 }
714 
715 void IRCDockTabContents::appendPrivChatContextMenuOptions(QMenu *menu)
716 {
717  appendPrivChatContextMenuAction(menu, tr("Whois"), PrivWhois);
718  appendPrivChatContextMenuAction(menu, tr("CTCP Ping"), PrivCtcpPing);
719  appendPrivChatContextMenuAction(menu, tr("CTCP Time"), PrivCtcpTime);
720  appendPrivChatContextMenuAction(menu, tr("CTCP Version"), PrivCtcpVersion);
721  appendPrivChatContextMenuAction(menu, tr("Ignore"), PrivIgnore);
722 }
723 
724 void IRCDockTabContents::appendPrivChatContextMenuAction(QMenu *menu,
725  const QString &text, PrivChatMenu type)
726 {
727  QAction *action = menu->addAction(text);
728  action->setData(type);
729  this->connect(action, SIGNAL(triggered()), SLOT(onPrivChatActionTriggered()));
730 }
731 
732 void IRCDockTabContents::onPrivChatActionTriggered()
733 {
734  QString nickname = ircAdapter()->recipient();
735  QString cleanNickname = IRCUserInfo(nickname, network()).cleanNickname();
736  auto action = static_cast<QAction *>(sender());
737  switch (action->data().toInt())
738  {
739  case PrivWhois:
740  sendWhois(cleanNickname);
741  break;
742  case PrivCtcpPing:
743  sendCtcpPing(cleanNickname);
744  break;
745  case PrivCtcpTime:
746  sendCtcpTime(cleanNickname);
747  break;
748  case PrivCtcpVersion:
749  sendCtcpVersion(cleanNickname);
750  break;
751  case PrivIgnore:
752  startIgnoreOperation(cleanNickname);
753  break;
754  default:
755  assert(0 && "Unsupported priv chat action");
756  qDebug() << "Unsupported priv chat action: " << action->data();
757  break;
758  }
759 }
760 
761 void IRCDockTabContents::showIgnoresManager()
762 {
763  auto dialog = new IRCIgnoresManager(this, networkEntity().description());
764  connect(dialog, SIGNAL(accepted()), network(), SLOT(reloadNetworkEntityFromConfig()));
765  dialog->setAttribute(Qt::WA_DeleteOnClose);
766  dialog->show();
767 }
768 
769 void IRCDockTabContents::startIgnoreOperation(const QString &nickname)
770 {
771  auto op = new IRCDelayedOperationIgnore(this, network(), nickname);
772  op->setShowPatternPopup(true);
773  op->start();
774 }
775 
776 QString IRCDockTabContents::title() const
777 {
778  return pIrcAdapter->title();
779 }
780 
781 QString IRCDockTabContents::titleColor() const
782 {
783  if (d->lastMessageClass != nullptr && !this->hasTabFocus())
784  {
785  QString color;
786 
787  if (*d->lastMessageClass == IRCMessageClass::Normal)
788  color = "#ff0000";
789  else
790  color = d->lastMessageClass->colorFromConfig();
791 
792  if (d->bBlinkTitle)
793  {
794  QColor c(color);
795 
796  int rInverted = 0xff - c.red();
797  int gInverted = 0xff - c.green();
798  int bInverted = 0xff - c.blue();
799 
800  QColor inverted(rInverted, gInverted, bInverted);
801 
802  return inverted.name();
803  }
804  else
805  return color;
806  }
807 
808  return "";
809 }
810 
811 void IRCDockTabContents::userListCustomContextMenuRequested(const QPoint &pos)
812 {
813  if (this->pIrcAdapter->adapterType() != IRCAdapterBase::ChannelAdapter)
814  {
815  // Prevent illegal calls.
816  return;
817  }
818 
819  QString nickname = this->selectedNickname();
820  if (nickname.isEmpty())
821  {
822  // Prevent calls if there is no one selected.
823  return;
824  }
825  QString cleanNickname = IRCUserInfo(nickname, network()).cleanNickname();
826 
827  auto pAdapter = (IRCChannelAdapter *) this->pIrcAdapter;
828  const QString &channel = pAdapter->recipient();
829 
830  UserListMenu &menu = this->getUserListContextMenu();
831  QPoint posGlobal = d->lvUserList->mapToGlobal(pos);
832 
833  QAction *pAction = menu.exec(posGlobal);
834 
835  if (pAction == nullptr)
836  return;
837 
838  if (pAction == menu.ban)
839  {
840  bool bOk = false;
841 
842  QString reason = CommonGUI::askString(tr("Ban user"), tr("Input reason for banning user %1 from channel %2").arg(nickname, channel), &bOk);
843  if (bOk)
844  pAdapter->banUser(cleanNickname, reason);
845  }
846  else if (pAction == menu.ctcpTime)
847  sendCtcpTime(cleanNickname);
848  else if (pAction == menu.ctcpPing)
849  sendCtcpPing(cleanNickname);
850  else if (pAction == menu.ctcpVersion)
851  sendCtcpVersion(cleanNickname);
852  else if (pAction == menu.deop)
853  pAdapter->setOp(cleanNickname, false);
854  else if (pAction == menu.dehalfOp)
855  pAdapter->setHalfOp(cleanNickname, false);
856  else if (pAction == menu.devoice)
857  pAdapter->setVoiced(cleanNickname, false);
858  else if (pAction == menu.halfOp)
859  pAdapter->setHalfOp(cleanNickname, true);
860  else if (pAction == menu.ignore)
861  startIgnoreOperation(cleanNickname);
862  else if (pAction == menu.kick)
863  {
864  bool bOk = false;
865 
866  QString reason = CommonGUI::askString(tr("Kick user"), tr("Input reason for kicking user %1 from channel %2").arg(nickname, channel), &bOk);
867  if (bOk)
868  pAdapter->kickUser(cleanNickname, reason);
869  }
870  else if (pAction == menu.op)
871  pAdapter->setOp(cleanNickname, true);
872  else if (pAction == menu.openChatWindow)
873  pAdapter->network()->openNewAdapter(cleanNickname);
874  else if (pAction == menu.voice)
875  pAdapter->setVoiced(cleanNickname, true);
876  else if (pAction == menu.whois)
877  sendWhois(cleanNickname);
878 }
879 
880 void IRCDockTabContents::userListDoubleClicked()
881 {
882  if (this->pIrcAdapter->adapterType() != IRCAdapterBase::ChannelAdapter)
883  {
884  // Prevent illegal calls.
885  return;
886  }
887 
888  QString nickname = this->selectedNickname();
889  if (nickname.isEmpty())
890  {
891  // Prevent calls if there is no one selected.
892  return;
893  }
894  QString cleanNickname = IRCUserInfo(nickname, network()).cleanNickname();
895 
896  this->pIrcAdapter->network()->openNewAdapter(cleanNickname);
897 }
898 
899 QString IRCDockTabContents::wrapTextWithMetaTags(const QString &text,
900  const IRCMessageClass &messageClass) const
901 {
902  QString result = text;
903  result.replace("<", "&lt;").replace(">", "&gt;");
904  result = Strings::wrapUrlsWithHtmlATags(result);
905 
906  QString className = messageClass.toStyleSheetClassName();
907  if (className.isEmpty())
908  result = "<span>" + result + "</span>";
909  else
910  result = ("<span class='" + className + "'>" + result + "</span>");
911  return result;
912 }
913 
914 bool IRCDockTabContents::writeLog(const QString &text)
915 {
916  ChatLogsCfg cfg;
917  if (d->log.isOpen() && cfg.isStoreLogs())
918  {
919  d->log.write(text.toUtf8());
920  d->log.flush();
921  return true;
922  }
923  return false;
924 }
925 
927 IRCDockTabContents::UserListMenu::UserListMenu()
928 {
929  this->openChatWindow = this->addAction(tr("Open chat window"));
930  this->addSeparator();
931  this->whois = this->addAction(tr("Whois"));
932  this->ctcpTime = this->addAction(tr("CTCP Time"));
933  this->ctcpPing = this->addAction(tr("CTCP Ping"));
934  this->ctcpVersion = this->addAction(tr("CTCP Version"));
935  this->addSeparator();
936  this->op = this->addAction(tr("Op"));
937  this->deop = this->addAction(tr("Deop"));
938  this->halfOp = this->addAction(tr("Half op"));
939  this->dehalfOp = this->addAction(tr("De half op"));
940  this->voice = this->addAction(tr("Voice"));
941  this->devoice = this->addAction(tr("Devoice"));
942  this->addSeparator();
943  this->ignore = this->addAction(tr("Ignore"));
944  this->kick = this->addAction(tr("Kick"));
945  this->ban = this->addAction(tr("Ban"));
946 
947  this->bIsOperator = false;
948 }