1 #include "widgets/splits/Split.hpp"
2
3 #include "Application.hpp"
4 #include "common/Common.hpp"
5 #include "common/Env.hpp"
6 #include "common/NetworkRequest.hpp"
7 #include "common/QLogging.hpp"
8 #include "controllers/accounts/AccountController.hpp"
9 #include "providers/twitch/EmoteValue.hpp"
10 #include "providers/twitch/TwitchChannel.hpp"
11 #include "providers/twitch/TwitchIrcServer.hpp"
12 #include "providers/twitch/TwitchMessageBuilder.hpp"
13 #include "singletons/Fonts.hpp"
14 #include "singletons/Settings.hpp"
15 #include "singletons/Theme.hpp"
16 #include "singletons/WindowManager.hpp"
17 #include "util/Clipboard.hpp"
18 #include "util/NuulsUploader.hpp"
19 #include "util/Shortcut.hpp"
20 #include "util/StreamLink.hpp"
21 #include "widgets/Notebook.hpp"
22 #include "widgets/TooltipWidget.hpp"
23 #include "widgets/Window.hpp"
24 #include "widgets/dialogs/QualityPopup.hpp"
25 #include "widgets/dialogs/SelectChannelDialog.hpp"
26 #include "widgets/dialogs/SelectChannelFiltersDialog.hpp"
27 #include "widgets/dialogs/UserInfoPopup.hpp"
28 #include "widgets/helper/ChannelView.hpp"
29 #include "widgets/helper/DebugPopup.hpp"
30 #include "widgets/helper/NotebookTab.hpp"
31 #include "widgets/helper/ResizingTextEdit.hpp"
32 #include "widgets/helper/SearchPopup.hpp"
33 #include "widgets/splits/SplitContainer.hpp"
34 #include "widgets/splits/SplitHeader.hpp"
35 #include "widgets/splits/SplitInput.hpp"
36 #include "widgets/splits/SplitOverlay.hpp"
37
38 #include <QApplication>
39 #include <QClipboard>
40 #include <QDesktopServices>
41 #include <QDockWidget>
42 #include <QDrag>
43 #include <QJsonArray>
44 #include <QLabel>
45 #include <QListWidget>
46 #include <QMimeData>
47 #include <QMovie>
48 #include <QPainter>
49 #include <QVBoxLayout>
50
51 #include <functional>
52 #include <random>
53
54 namespace chatterino {
55 namespace {
showTutorialVideo(QWidget * parent,const QString & source,const QString & title,const QString & description)56 void showTutorialVideo(QWidget *parent, const QString &source,
57 const QString &title, const QString &description)
58 {
59 auto window =
60 new BasePopup(BaseWindow::Flags::EnableCustomFrame, parent);
61 window->setWindowTitle("Chatterino - " + title);
62 window->setAttribute(Qt::WA_DeleteOnClose);
63 auto layout = new QVBoxLayout();
64 layout->addWidget(new QLabel(description));
65 auto label = new QLabel(window);
66 layout->addWidget(label);
67 auto movie = new QMovie(label);
68 movie->setFileName(source);
69 label->setMovie(movie);
70 movie->start();
71 window->getLayoutContainer()->setLayout(layout);
72 window->show();
73 }
74 } // namespace
75
76 pajlada::Signals::Signal<Qt::KeyboardModifiers> Split::modifierStatusChanged;
77 Qt::KeyboardModifiers Split::modifierStatus = Qt::NoModifier;
78
Split(QWidget * parent)79 Split::Split(QWidget *parent)
80 : BaseWidget(parent)
81 , channel_(Channel::getEmpty())
82 , vbox_(new QVBoxLayout(this))
83 , header_(new SplitHeader(this))
84 , view_(new ChannelView(this))
85 , input_(new SplitInput(this))
86 , overlay_(new SplitOverlay(this))
87 {
88 this->setMouseTracking(true);
89 this->view_->setPausable(true);
90 this->view_->setFocusPolicy(Qt::FocusPolicy::NoFocus);
91
92 this->vbox_->setSpacing(0);
93 this->vbox_->setMargin(1);
94
95 this->vbox_->addWidget(this->header_);
96 this->vbox_->addWidget(this->view_, 1);
97 this->vbox_->addWidget(this->input_);
98
99 // Initialize chat widget-wide hotkeys
100 // CTRL+W: Close Split
101 createShortcut(this, "CTRL+W", &Split::deleteFromContainer);
102
103 // CTRL+R: Change Channel
104 createShortcut(this, "CTRL+R", &Split::changeChannel);
105
106 // CTRL+F: Search
107 createShortcut(this, "CTRL+F", &Split::showSearch);
108
109 // F5: reload emotes
110 createShortcut(this, "F5", &Split::reloadChannelAndSubscriberEmotes);
111
112 // CTRL+F5: reconnect
113 createShortcut(this, "CTRL+F5", &Split::reconnect);
114
115 // Alt+X: create clip LUL
116 createShortcut(this, "Alt+X", [this] {
117 if (const auto type = this->getChannel()->getType();
118 type != Channel::Type::Twitch &&
119 type != Channel::Type::TwitchWatching)
120 {
121 return;
122 }
123
124 auto *twitchChannel =
125 dynamic_cast<TwitchChannel *>(this->getChannel().get());
126
127 twitchChannel->createClip();
128 });
129
130 // F10
131 createShortcut(this, "F10", [] {
132 auto *popup = new DebugPopup;
133 popup->setAttribute(Qt::WA_DeleteOnClose);
134 popup->setWindowTitle("Chatterino - Debug popup");
135 popup->show();
136 });
137
138 // xd
139 // CreateShortcut(this, "ALT+SHIFT+RIGHT", &Split::doIncFlexX);
140 // CreateShortcut(this, "ALT+SHIFT+LEFT", &Split::doDecFlexX);
141 // CreateShortcut(this, "ALT+SHIFT+UP", &Split::doIncFlexY);
142 // CreateShortcut(this, "ALT+SHIFT+DOWN", &Split::doDecFlexY);
143
144 this->input_->ui_.textEdit->installEventFilter(parent);
145
146 // update placeheolder text on Twitch account change and channel change
147 this->signalHolder_.managedConnect(
148 getApp()->accounts->twitch.currentUserChanged, [this] {
149 this->updateInputPlaceholder();
150 });
151 this->signalHolder_.managedConnect(channelChanged, [this] {
152 this->updateInputPlaceholder();
153 });
154 this->updateInputPlaceholder();
155
156 this->view_->mouseDown.connect([this](QMouseEvent *) {
157 this->giveFocus(Qt::MouseFocusReason);
158 });
159 this->view_->selectionChanged.connect([this]() {
160 if (view_->hasSelection())
161 {
162 this->input_->clearSelection();
163 }
164 });
165
166 this->view_->openChannelIn.connect([this](
167 QString twitchChannel,
168 FromTwitchLinkOpenChannelIn openIn) {
169 ChannelPtr channel =
170 getApp()->twitch.server->getOrAddChannel(twitchChannel);
171 switch (openIn)
172 {
173 case FromTwitchLinkOpenChannelIn::Split:
174 this->openSplitRequested.invoke(channel);
175 break;
176 case FromTwitchLinkOpenChannelIn::Tab:
177 this->joinChannelInNewTab(channel);
178 break;
179 case FromTwitchLinkOpenChannelIn::BrowserPlayer:
180 this->openChannelInBrowserPlayer(channel);
181 break;
182 case FromTwitchLinkOpenChannelIn::Streamlink:
183 this->openChannelInStreamlink(twitchChannel);
184 break;
185 default:
186 qCWarning(chatterinoWidget)
187 << "Unhandled \"FromTwitchLinkOpenChannelIn\" enum value: "
188 << static_cast<int>(openIn);
189 }
190 });
191
192 this->input_->textChanged.connect([=](const QString &newText) {
193 if (getSettings()->showEmptyInput)
194 {
195 return;
196 }
197
198 if (newText.length() == 0)
199 {
200 this->input_->hide();
201 }
202 else if (this->input_->isHidden())
203 {
204 this->input_->show();
205 }
206 });
207
208 getSettings()->showEmptyInput.connect(
209 [this](const bool &showEmptyInput, auto) {
210 if (!showEmptyInput && this->input_->getInputText().length() == 0)
211 {
212 this->input_->hide();
213 }
214 else
215 {
216 this->input_->show();
217 }
218 },
219 this->managedConnections_);
220
221 this->header_->updateModerationModeIcon();
222 this->overlay_->hide();
223
224 this->setSizePolicy(QSizePolicy::MinimumExpanding,
225 QSizePolicy::MinimumExpanding);
226
227 this->managedConnect(modifierStatusChanged, [this](Qt::KeyboardModifiers
228 status) {
229 if ((status ==
230 showSplitOverlayModifiers /*|| status == showAddSplitRegions*/) &&
231 this->isMouseOver_)
232 {
233 this->overlay_->show();
234 }
235 else
236 {
237 this->overlay_->hide();
238 }
239
240 if (getSettings()->pauseChatModifier.getEnum() != Qt::NoModifier &&
241 status == getSettings()->pauseChatModifier.getEnum())
242 {
243 this->view_->pause(PauseReason::KeyboardModifier);
244 }
245 else
246 {
247 this->view_->unpause(PauseReason::KeyboardModifier);
248 }
249 });
250
251 this->input_->ui_.textEdit->focused.connect([this] {
252 this->focused.invoke();
253 });
254 this->input_->ui_.textEdit->focusLost.connect([this] {
255 this->focusLost.invoke();
256 });
257 this->input_->ui_.textEdit->imagePasted.connect(
258 [this](const QMimeData *source) {
259 if (!getSettings()->imageUploaderEnabled)
260 return;
261
262 if (getSettings()->askOnImageUpload.getValue())
263 {
264 QMessageBox msgBox;
265 msgBox.setText("Image upload");
266 msgBox.setInformativeText(
267 "You are uploading an image to a 3rd party service not in "
268 "control of the chatterino team. You may not be able to "
269 "remove the image from the site. Are you okay with this?");
270 msgBox.addButton(QMessageBox::Cancel);
271 msgBox.addButton(QMessageBox::Yes);
272 msgBox.addButton("Yes, don't ask again", QMessageBox::YesRole);
273
274 msgBox.setDefaultButton(QMessageBox::Yes);
275
276 auto picked = msgBox.exec();
277 if (picked == QMessageBox::Cancel)
278 {
279 return;
280 }
281 else if (picked == 0) // don't ask again button
282 {
283 getSettings()->askOnImageUpload.setValue(false);
284 }
285 }
286 upload(source, this->getChannel(), *this->input_->ui_.textEdit);
287 });
288
289 getSettings()->imageUploaderEnabled.connect(
290 [this](const bool &val) {
291 this->setAcceptDrops(val);
292 },
293 this->managedConnections_);
294 }
295
~Split()296 Split::~Split()
297 {
298 this->usermodeChangedConnection_.disconnect();
299 this->roomModeChangedConnection_.disconnect();
300 this->channelIDChangedConnection_.disconnect();
301 this->indirectChannelChangedConnection_.disconnect();
302 }
303
getChannelView()304 ChannelView &Split::getChannelView()
305 {
306 return *this->view_;
307 }
308
getInput()309 SplitInput &Split::getInput()
310 {
311 return *this->input_;
312 }
313
updateInputPlaceholder()314 void Split::updateInputPlaceholder()
315 {
316 if (!this->getChannel()->isTwitchChannel())
317 {
318 return;
319 }
320
321 auto user = getApp()->accounts->twitch.getCurrent();
322 QString placeholderText;
323
324 if (user->isAnon())
325 {
326 placeholderText = "Log in to send messages...";
327 }
328 else
329 {
330 placeholderText =
331 QString("Send message as %1...")
332 .arg(getApp()->accounts->twitch.getCurrent()->getUserName());
333 }
334
335 this->input_->ui_.textEdit->setPlaceholderText(placeholderText);
336 }
337
joinChannelInNewTab(ChannelPtr channel)338 void Split::joinChannelInNewTab(ChannelPtr channel)
339 {
340 auto &nb = getApp()->windows->getMainWindow().getNotebook();
341 SplitContainer *container = nb.addPage(true);
342
343 Split *split = new Split(container);
344 split->setChannel(channel);
345 container->appendSplit(split);
346 }
347
openChannelInBrowserPlayer(ChannelPtr channel)348 void Split::openChannelInBrowserPlayer(ChannelPtr channel)
349 {
350 if (auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
351 {
352 QDesktopServices::openUrl(
353 "https://player.twitch.tv/?parent=twitch.tv&channel=" +
354 twitchChannel->getName());
355 }
356 }
357
openChannelInStreamlink(QString channelName)358 void Split::openChannelInStreamlink(QString channelName)
359 {
360 try
361 {
362 openStreamlinkForChannel(channelName);
363 }
364 catch (const Exception &ex)
365 {
366 qCWarning(chatterinoWidget)
367 << "Error in doOpenStreamlink:" << ex.what();
368 }
369 }
370
getIndirectChannel()371 IndirectChannel Split::getIndirectChannel()
372 {
373 return this->channel_;
374 }
375
getChannel()376 ChannelPtr Split::getChannel()
377 {
378 return this->channel_.get();
379 }
380
setChannel(IndirectChannel newChannel)381 void Split::setChannel(IndirectChannel newChannel)
382 {
383 this->channel_ = newChannel;
384
385 this->view_->setChannel(newChannel.get());
386
387 this->usermodeChangedConnection_.disconnect();
388 this->roomModeChangedConnection_.disconnect();
389 this->indirectChannelChangedConnection_.disconnect();
390
391 TwitchChannel *tc = dynamic_cast<TwitchChannel *>(newChannel.get().get());
392
393 if (tc != nullptr)
394 {
395 this->usermodeChangedConnection_ = tc->userStateChanged.connect([this] {
396 this->header_->updateModerationModeIcon();
397 this->header_->updateRoomModes();
398 });
399
400 this->roomModeChangedConnection_ = tc->roomModesChanged.connect([this] {
401 this->header_->updateRoomModes();
402 });
403 }
404
405 this->indirectChannelChangedConnection_ =
406 newChannel.getChannelChanged().connect([this] {
407 QTimer::singleShot(0, [this] {
408 this->setChannel(this->channel_);
409 });
410 });
411
412 this->header_->updateModerationModeIcon();
413 this->header_->updateChannelText();
414 this->header_->updateRoomModes();
415
416 if (newChannel.getType() == Channel::Type::Twitch)
417 {
418 this->header_->setViewersButtonVisible(true);
419 }
420 else
421 {
422 this->header_->setViewersButtonVisible(false);
423 }
424
425 this->channel_.get()->displayNameChanged.connect([this] {
426 this->actionRequested.invoke(Action::RefreshTab);
427 });
428
429 this->channelChanged.invoke();
430
431 // Queue up save because: Split channel changed
432 getApp()->windows->queueSave();
433 }
434
setModerationMode(bool value)435 void Split::setModerationMode(bool value)
436 {
437 this->moderationMode_ = value;
438 this->header_->updateModerationModeIcon();
439 this->view_->queueLayout();
440 }
441
getModerationMode() const442 bool Split::getModerationMode() const
443 {
444 return this->moderationMode_;
445 }
446
insertTextToInput(const QString & text)447 void Split::insertTextToInput(const QString &text)
448 {
449 this->input_->insertText(text);
450 }
451
showChangeChannelPopup(const char * dialogTitle,bool empty,std::function<void (bool)> callback)452 void Split::showChangeChannelPopup(const char *dialogTitle, bool empty,
453 std::function<void(bool)> callback)
454 {
455 if (this->selectChannelDialog_.hasElement())
456 {
457 this->selectChannelDialog_->raise();
458
459 return;
460 }
461
462 auto dialog = new SelectChannelDialog(this);
463 if (!empty)
464 {
465 dialog->setSelectedChannel(this->getIndirectChannel());
466 }
467 dialog->setAttribute(Qt::WA_DeleteOnClose);
468 dialog->show();
469 dialog->closed.connect([=] {
470 if (dialog->hasSeletedChannel())
471 {
472 this->setChannel(dialog->getSelectedChannel());
473 this->actionRequested.invoke(Action::RefreshTab);
474 }
475
476 callback(dialog->hasSeletedChannel());
477 this->selectChannelDialog_ = nullptr;
478 });
479 this->selectChannelDialog_ = dialog;
480 }
481
updateGifEmotes()482 void Split::updateGifEmotes()
483 {
484 this->view_->queueUpdate();
485 }
486
updateLastReadMessage()487 void Split::updateLastReadMessage()
488 {
489 this->view_->updateLastReadMessage();
490 }
491
giveFocus(Qt::FocusReason reason)492 void Split::giveFocus(Qt::FocusReason reason)
493 {
494 this->input_->ui_.textEdit->setFocus(reason);
495 }
496
hasFocus() const497 bool Split::hasFocus() const
498 {
499 return this->input_->ui_.textEdit->hasFocus();
500 }
501
paintEvent(QPaintEvent *)502 void Split::paintEvent(QPaintEvent *)
503 {
504 // color the background of the chat
505 QPainter painter(this);
506
507 painter.fillRect(this->rect(), this->theme->splits.background);
508 }
509
mouseMoveEvent(QMouseEvent * event)510 void Split::mouseMoveEvent(QMouseEvent *event)
511 {
512 this->handleModifiers(QGuiApplication::queryKeyboardModifiers());
513 }
514
keyPressEvent(QKeyEvent * event)515 void Split::keyPressEvent(QKeyEvent *event)
516 {
517 this->view_->unsetCursor();
518 this->handleModifiers(QGuiApplication::queryKeyboardModifiers());
519 }
520
keyReleaseEvent(QKeyEvent * event)521 void Split::keyReleaseEvent(QKeyEvent *event)
522 {
523 this->view_->unsetCursor();
524 this->handleModifiers(QGuiApplication::queryKeyboardModifiers());
525 }
526
resizeEvent(QResizeEvent * event)527 void Split::resizeEvent(QResizeEvent *event)
528 {
529 // Queue up save because: Split resized
530 getApp()->windows->queueSave();
531
532 BaseWidget::resizeEvent(event);
533
534 this->overlay_->setGeometry(this->rect());
535 }
536
enterEvent(QEvent * event)537 void Split::enterEvent(QEvent *event)
538 {
539 this->isMouseOver_ = true;
540
541 this->handleModifiers(QGuiApplication::queryKeyboardModifiers());
542
543 if (modifierStatus ==
544 showSplitOverlayModifiers /*|| modifierStatus == showAddSplitRegions*/)
545 {
546 this->overlay_->show();
547 }
548
549 this->actionRequested.invoke(Action::ResetMouseStatus);
550 }
551
leaveEvent(QEvent * event)552 void Split::leaveEvent(QEvent *event)
553 {
554 this->isMouseOver_ = false;
555
556 this->overlay_->hide();
557
558 TooltipWidget::instance()->hide();
559
560 this->handleModifiers(QGuiApplication::queryKeyboardModifiers());
561 }
562
focusInEvent(QFocusEvent * event)563 void Split::focusInEvent(QFocusEvent *event)
564 {
565 this->giveFocus(event->reason());
566 }
567
handleModifiers(Qt::KeyboardModifiers modifiers)568 void Split::handleModifiers(Qt::KeyboardModifiers modifiers)
569 {
570 if (modifierStatus != modifiers)
571 {
572 modifierStatus = modifiers;
573 modifierStatusChanged.invoke(modifiers);
574 }
575 }
576
setIsTopRightSplit(bool value)577 void Split::setIsTopRightSplit(bool value)
578 {
579 this->isTopRightSplit_ = value;
580 this->header_->setAddButtonVisible(value);
581 }
582
583 /// Slots
addSibling()584 void Split::addSibling()
585 {
586 this->actionRequested.invoke(Action::AppendNewSplit);
587 }
588
deleteFromContainer()589 void Split::deleteFromContainer()
590 {
591 this->actionRequested.invoke(Action::Delete);
592 }
593
changeChannel()594 void Split::changeChannel()
595 {
596 this->showChangeChannelPopup("Change channel", false, [](bool) {});
597
598 auto popup = this->findChildren<QDockWidget *>();
599 if (popup.size() && popup.at(0)->isVisible() && !popup.at(0)->isFloating())
600 {
601 popup.at(0)->hide();
602 showViewerList();
603 }
604 }
605
explainMoving()606 void Split::explainMoving()
607 {
608 showTutorialVideo(this, ":/examples/moving.gif", "Moving",
609 "Hold <Ctrl+Alt> to move splits.\n\nExample:");
610 }
611
explainSplitting()612 void Split::explainSplitting()
613 {
614 showTutorialVideo(this, ":/examples/splitting.gif", "Splitting",
615 "Hold <Ctrl+Alt> to add new splits.\n\nExample:");
616 }
617
popup()618 void Split::popup()
619 {
620 auto app = getApp();
621 Window &window = app->windows->createWindow(WindowType::Popup);
622
623 Split *split = new Split(static_cast<SplitContainer *>(
624 window.getNotebook().getOrAddSelectedPage()));
625
626 split->setChannel(this->getIndirectChannel());
627 window.getNotebook().getOrAddSelectedPage()->appendSplit(split);
628
629 window.show();
630 }
631
clear()632 void Split::clear()
633 {
634 this->view_->clearMessages();
635 }
636
openInBrowser()637 void Split::openInBrowser()
638 {
639 auto channel = this->getChannel();
640
641 if (auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
642 {
643 QDesktopServices::openUrl("https://twitch.tv/" +
644 twitchChannel->getName());
645 }
646 }
647
openWhispersInBrowser()648 void Split::openWhispersInBrowser()
649 {
650 auto userName = getApp()->accounts->twitch.getCurrent()->getUserName();
651 QDesktopServices::openUrl("https://twitch.tv/popout/moderator/" + userName +
652 "/whispers");
653 }
654
openBrowserPlayer()655 void Split::openBrowserPlayer()
656 {
657 this->openChannelInBrowserPlayer(this->getChannel());
658 }
659
openModViewInBrowser()660 void Split::openModViewInBrowser()
661 {
662 auto channel = this->getChannel();
663
664 if (auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
665 {
666 QDesktopServices::openUrl("https://twitch.tv/moderator/" +
667 twitchChannel->getName());
668 }
669 }
670
openInStreamlink()671 void Split::openInStreamlink()
672 {
673 this->openChannelInStreamlink(this->getChannel()->getName());
674 }
675
openWithCustomScheme()676 void Split::openWithCustomScheme()
677 {
678 QString scheme = getSettings()->customURIScheme.getValue();
679 if (scheme.isEmpty())
680 {
681 return;
682 }
683
684 const auto channel = this->getChannel().get();
685
686 if (const auto twitchChannel = dynamic_cast<TwitchChannel *>(channel))
687 {
688 QDesktopServices::openUrl(QString("%1https://twitch.tv/%2")
689 .arg(scheme)
690 .arg(twitchChannel->getName()));
691 }
692 }
693
showViewerList()694 void Split::showViewerList()
695 {
696 auto viewerDock =
697 new QDockWidget("Viewer List - " + this->getChannel()->getName(), this);
698 viewerDock->setAllowedAreas(Qt::LeftDockWidgetArea);
699 viewerDock->setFeatures(QDockWidget::DockWidgetVerticalTitleBar |
700 QDockWidget::DockWidgetClosable |
701 QDockWidget::DockWidgetFloatable);
702 viewerDock->resize(
703 0.5 * this->width(),
704 this->height() - this->header_->height() - this->input_->height());
705 viewerDock->move(0, this->header_->height());
706
707 auto multiWidget = new QWidget(viewerDock);
708 auto dockVbox = new QVBoxLayout(viewerDock);
709 auto searchBar = new QLineEdit(viewerDock);
710
711 auto chattersList = new QListWidget();
712 auto resultList = new QListWidget();
713
714 auto formatListItemText = [](QString text) {
715 auto item = new QListWidgetItem();
716 item->setText(text);
717 item->setFont(getApp()->fonts->getFont(FontStyle::ChatMedium, 1.0));
718 return item;
719 };
720
721 static QStringList labels = {
722 "Broadcaster", "Moderators", "VIPs", "Staff",
723 "Admins", "Global Moderators", "Viewers"};
724 static QStringList jsonLabels = {"broadcaster", "moderators", "vips",
725 "staff", "admins", "global_mods",
726 "viewers"};
727 QList<QListWidgetItem *> labelList;
728 for (auto &x : labels)
729 {
730 auto label = formatListItemText(x);
731 label->setForeground(this->theme->accent);
732 labelList.append(label);
733 }
734 auto loadingLabel = new QLabel("Loading...");
735
736 NetworkRequest::twitchRequest("https://tmi.twitch.tv/group/user/" +
737 this->getChannel()->getName() + "/chatters")
738 .caller(this)
739 .onSuccess([=](auto result) -> Outcome {
740 auto obj = result.parseJson();
741 QJsonObject chattersObj = obj.value("chatters").toObject();
742
743 loadingLabel->hide();
744 for (int i = 0; i < jsonLabels.size(); i++)
745 {
746 auto currentCategory =
747 chattersObj.value(jsonLabels.at(i)).toArray();
748 // If current category of chatters is empty, dont show this
749 // category.
750 if (currentCategory.empty())
751 continue;
752
753 chattersList->addItem(labelList.at(i));
754 foreach (const QJsonValue &v, currentCategory)
755 {
756 chattersList->addItem(formatListItemText(v.toString()));
757 }
758 chattersList->addItem(new QListWidgetItem());
759 }
760
761 return Success;
762 })
763 .execute();
764
765 searchBar->setPlaceholderText("Search User...");
766 QObject::connect(searchBar, &QLineEdit::textEdited, this, [=]() {
767 auto query = searchBar->text();
768 if (!query.isEmpty())
769 {
770 auto results = chattersList->findItems(query, Qt::MatchContains);
771 chattersList->hide();
772 resultList->clear();
773 for (auto &item : results)
774 {
775 if (!labels.contains(item->text()))
776 {
777 resultList->addItem(formatListItemText(item->text()));
778 }
779 }
780 resultList->show();
781 }
782 else
783 {
784 resultList->hide();
785 chattersList->show();
786 }
787 });
788
789 QObject::connect(viewerDock, &QDockWidget::topLevelChanged, this, [=]() {
790 viewerDock->setMinimumWidth(300);
791 });
792
793 auto listDoubleClick = [=](QString userName) {
794 if (!labels.contains(userName) && !userName.isEmpty())
795 {
796 this->view_->showUserInfoPopup(userName);
797 }
798 };
799
800 QObject::connect(chattersList, &QListWidget::doubleClicked, this, [=]() {
801 listDoubleClick(chattersList->currentItem()->text());
802 });
803
804 QObject::connect(resultList, &QListWidget::doubleClicked, this, [=]() {
805 listDoubleClick(resultList->currentItem()->text());
806 });
807
808 dockVbox->addWidget(searchBar);
809 dockVbox->addWidget(loadingLabel);
810 dockVbox->addWidget(chattersList);
811 dockVbox->addWidget(resultList);
812 resultList->hide();
813
814 multiWidget->setStyleSheet(this->theme->splits.input.styleSheet);
815 multiWidget->setLayout(dockVbox);
816 viewerDock->setWidget(multiWidget);
817 viewerDock->setFloating(true);
818 viewerDock->show();
819 viewerDock->activateWindow();
820 }
821
openSubPage()822 void Split::openSubPage()
823 {
824 ChannelPtr channel = this->getChannel();
825
826 if (auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
827 {
828 QDesktopServices::openUrl(twitchChannel->subscriptionUrl());
829 }
830 }
831
copyToClipboard()832 void Split::copyToClipboard()
833 {
834 crossPlatformCopy(this->view_->getSelectedText());
835 }
836
setFiltersDialog()837 void Split::setFiltersDialog()
838 {
839 SelectChannelFiltersDialog d(this->getFilters(), this);
840 d.setWindowTitle("Select filters");
841
842 if (d.exec() == QDialog::Accepted)
843 {
844 this->setFilters(d.getSelection());
845 }
846 }
847
setFilters(const QList<QUuid> ids)848 void Split::setFilters(const QList<QUuid> ids)
849 {
850 this->view_->setFilters(ids);
851 this->header_->updateChannelText();
852 }
853
getFilters() const854 const QList<QUuid> Split::getFilters() const
855 {
856 return this->view_->getFilterIds();
857 }
858
showSearch()859 void Split::showSearch()
860 {
861 SearchPopup *popup = new SearchPopup(this);
862
863 popup->setChannelFilters(this->view_->getFilterSet());
864 popup->setAttribute(Qt::WA_DeleteOnClose);
865 popup->setChannel(this->getChannel());
866 popup->show();
867 }
868
reloadChannelAndSubscriberEmotes()869 void Split::reloadChannelAndSubscriberEmotes()
870 {
871 getApp()->accounts->twitch.getCurrent()->loadEmotes();
872 auto channel = this->getChannel();
873
874 if (auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
875 {
876 twitchChannel->refreshBTTVChannelEmotes(true);
877 twitchChannel->refreshFFZChannelEmotes(true);
878 }
879 }
880
reconnect()881 void Split::reconnect()
882 {
883 this->getChannel()->reconnect();
884 }
885
dragEnterEvent(QDragEnterEvent * event)886 void Split::dragEnterEvent(QDragEnterEvent *event)
887 {
888 if (getSettings()->imageUploaderEnabled &&
889 (event->mimeData()->hasImage() || event->mimeData()->hasUrls()))
890 {
891 event->acceptProposedAction();
892 }
893 else
894 {
895 BaseWidget::dragEnterEvent(event);
896 }
897 }
898
dropEvent(QDropEvent * event)899 void Split::dropEvent(QDropEvent *event)
900 {
901 if (getSettings()->imageUploaderEnabled &&
902 (event->mimeData()->hasImage() || event->mimeData()->hasUrls()))
903 {
904 this->input_->ui_.textEdit->imagePasted.invoke(event->mimeData());
905 }
906 else
907 {
908 BaseWidget::dropEvent(event);
909 }
910 }
911 template <typename Iter, typename RandomGenerator>
select_randomly(Iter start,Iter end,RandomGenerator & g)912 static Iter select_randomly(Iter start, Iter end, RandomGenerator &g)
913 {
914 std::uniform_int_distribution<> dis(0, std::distance(start, end) - 1);
915 std::advance(start, dis(g));
916 return start;
917 }
918
919 template <typename Iter>
select_randomly(Iter start,Iter end)920 static Iter select_randomly(Iter start, Iter end)
921 {
922 static std::random_device rd;
923 static std::mt19937 gen(rd());
924 return select_randomly(start, end, gen);
925 }
926
drag()927 void Split::drag()
928 {
929 if (auto container = dynamic_cast<SplitContainer *>(this->parentWidget()))
930 {
931 SplitContainer::isDraggingSplit = true;
932 SplitContainer::draggingSplit = this;
933
934 auto originalLocation = container->releaseSplit(this);
935 auto drag = new QDrag(this);
936 auto mimeData = new QMimeData;
937
938 mimeData->setData("chatterino/split", "xD");
939 drag->setMimeData(mimeData);
940
941 if (drag->exec(Qt::MoveAction) == Qt::IgnoreAction)
942 {
943 container->insertSplit(this, originalLocation);
944 }
945
946 SplitContainer::isDraggingSplit = false;
947 }
948 }
949
950 } // namespace chatterino
951