1 #include "ChannelView.hpp"
2 
3 #include <QClipboard>
4 #include <QDate>
5 #include <QDebug>
6 #include <QDesktopServices>
7 #include <QGraphicsBlurEffect>
8 #include <QMessageBox>
9 #include <QPainter>
10 #include <QScreen>
11 #include <algorithm>
12 #include <chrono>
13 #include <cmath>
14 #include <functional>
15 #include <memory>
16 
17 #include "Application.hpp"
18 #include "common/Common.hpp"
19 #include "common/QLogging.hpp"
20 #include "controllers/accounts/AccountController.hpp"
21 #include "controllers/commands/CommandController.hpp"
22 #include "debug/Benchmark.hpp"
23 #include "messages/Emote.hpp"
24 #include "messages/LimitedQueueSnapshot.hpp"
25 #include "messages/Message.hpp"
26 #include "messages/MessageBuilder.hpp"
27 #include "messages/MessageElement.hpp"
28 #include "messages/layouts/MessageLayout.hpp"
29 #include "messages/layouts/MessageLayoutElement.hpp"
30 #include "providers/LinkResolver.hpp"
31 #include "providers/twitch/TwitchChannel.hpp"
32 #include "providers/twitch/TwitchIrcServer.hpp"
33 #include "singletons/Resources.hpp"
34 #include "singletons/Settings.hpp"
35 #include "singletons/Theme.hpp"
36 #include "singletons/TooltipPreviewImage.hpp"
37 #include "singletons/WindowManager.hpp"
38 #include "util/Clipboard.hpp"
39 #include "util/DistanceBetweenPoints.hpp"
40 #include "util/Helpers.hpp"
41 #include "util/IncognitoBrowser.hpp"
42 #include "util/StreamerMode.hpp"
43 #include "util/Twitch.hpp"
44 #include "widgets/Scrollbar.hpp"
45 #include "widgets/TooltipWidget.hpp"
46 #include "widgets/Window.hpp"
47 #include "widgets/dialogs/SettingsDialog.hpp"
48 #include "widgets/dialogs/UserInfoPopup.hpp"
49 #include "widgets/helper/EffectLabel.hpp"
50 #include "widgets/helper/SearchPopup.hpp"
51 #include "widgets/splits/Split.hpp"
52 #include "widgets/splits/SplitInput.hpp"
53 
54 #define DRAW_WIDTH (this->width())
55 #define SELECTION_RESUME_SCROLLING_MSG_THRESHOLD 3
56 #define CHAT_HOVER_PAUSE_DURATION 1000
57 
58 namespace chatterino {
59 namespace {
addEmoteContextMenuItems(const Emote & emote,MessageElementFlags creatorFlags,QMenu & menu)60     void addEmoteContextMenuItems(const Emote &emote,
61                                   MessageElementFlags creatorFlags, QMenu &menu)
62     {
63         auto openAction = menu.addAction("Open");
64         auto openMenu = new QMenu;
65         openAction->setMenu(openMenu);
66 
67         auto copyAction = menu.addAction("Copy");
68         auto copyMenu = new QMenu;
69         copyAction->setMenu(copyMenu);
70 
71         // see if the QMenu actually gets destroyed
72         QObject::connect(openMenu, &QMenu::destroyed, [] {
73             QMessageBox(QMessageBox::Information, "xD", "the menu got deleted")
74                 .exec();
75         });
76 
77         // Add copy and open links for 1x, 2x, 3x
78         auto addImageLink = [&](const ImagePtr &image, char scale) {
79             if (!image->isEmpty())
80             {
81                 copyMenu->addAction(QString(scale) + "x link",
82                                     [url = image->url()] {
83                                         crossPlatformCopy(url.string);
84                                     });
85                 openMenu->addAction(
86                     QString(scale) + "x link", [url = image->url()] {
87                         QDesktopServices::openUrl(QUrl(url.string));
88                     });
89             }
90         };
91 
92         addImageLink(emote.images.getImage1(), '1');
93         addImageLink(emote.images.getImage2(), '2');
94         addImageLink(emote.images.getImage3(), '3');
95 
96         // Copy and open emote page link
97         auto addPageLink = [&](const QString &name) {
98             copyMenu->addSeparator();
99             openMenu->addSeparator();
100 
101             copyMenu->addAction("Copy " + name + " emote link",
102                                 [url = emote.homePage] {
103                                     crossPlatformCopy(url.string);
104                                 });
105             openMenu->addAction("Open " + name + " emote link",
106                                 [url = emote.homePage] {
107                                     QDesktopServices::openUrl(QUrl(url.string));
108                                 });
109         };
110 
111         if (creatorFlags.has(MessageElementFlag::TwitchEmote))
112         {
113             addPageLink("TwitchEmotes");
114         }
115         else if (creatorFlags.has(MessageElementFlag::BttvEmote))
116         {
117             addPageLink("BTTV");
118         }
119         else if (creatorFlags.has(MessageElementFlag::FfzEmote))
120         {
121             addPageLink("FFZ");
122         }
123     }
124 }  // namespace
125 
ChannelView(BaseWidget * parent)126 ChannelView::ChannelView(BaseWidget *parent)
127     : BaseWidget(parent)
128     , scrollBar_(new Scrollbar(this))
129 {
130     this->setMouseTracking(true);
131 
132     this->initializeLayout();
133     this->initializeScrollbar();
134     this->initializeSignals();
135 
136     this->cursors_.neutral = QCursor(getResources().scrolling.neutralScroll);
137     this->cursors_.up = QCursor(getResources().scrolling.upScroll);
138     this->cursors_.down = QCursor(getResources().scrolling.downScroll);
139 
140     this->pauseTimer_.setSingleShot(true);
141     QObject::connect(&this->pauseTimer_, &QTimer::timeout, this, [this] {
142         /// remove elements that are finite
143         for (auto it = this->pauses_.begin(); it != this->pauses_.end();)
144             it = it->second ? this->pauses_.erase(it) : ++it;
145 
146         this->updatePauses();
147     });
148 
149     auto shortcut = new QShortcut(QKeySequence::StandardKey::Copy, this);
150     QObject::connect(shortcut, &QShortcut::activated, [this] {
151         crossPlatformCopy(this->getSelectedText());
152     });
153 
154     this->clickTimer_ = new QTimer(this);
155     this->clickTimer_->setSingleShot(true);
156     this->clickTimer_->setInterval(500);
157 
158     this->scrollTimer_.setInterval(20);
159     QObject::connect(&this->scrollTimer_, &QTimer::timeout, this,
160                      &ChannelView::scrollUpdateRequested);
161 
162     this->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
163 }
164 
initializeLayout()165 void ChannelView::initializeLayout()
166 {
167     this->goToBottom_ = new EffectLabel(this, 0);
168     this->goToBottom_->setStyleSheet(
169         "background-color: rgba(0,0,0,0.66); color: #FFF;");
170     this->goToBottom_->getLabel().setText("More messages below");
171     this->goToBottom_->setVisible(false);
172 
173     QObject::connect(this->goToBottom_, &EffectLabel::leftClicked, this, [=] {
174         QTimer::singleShot(180, [=] {
175             this->scrollBar_->scrollToBottom(
176                 getSettings()->enableSmoothScrollingNewMessages.getValue());
177         });
178     });
179 }
180 
initializeScrollbar()181 void ChannelView::initializeScrollbar()
182 {
183     this->scrollBar_->getCurrentValueChanged().connect([this] {
184         this->performLayout(true);
185         this->queueUpdate();
186     });
187 }
188 
initializeSignals()189 void ChannelView::initializeSignals()
190 {
191     this->connections_.push_back(
192         getApp()->windows->wordFlagsChanged.connect([this] {
193             this->queueLayout();
194             this->update();
195         }));
196 
197     getSettings()->showLastMessageIndicator.connect(
198         [this](auto, auto) {
199             this->update();
200         },
201         this->connections_);
202 
203     connections_.push_back(getApp()->windows->gifRepaintRequested.connect([&] {
204         this->queueUpdate();
205     }));
206 
207     connections_.push_back(
208         getApp()->windows->layoutRequested.connect([&](Channel *channel) {
209             if (this->isVisible() &&
210                 (channel == nullptr || this->channel_.get() == channel))
211             {
212                 this->queueLayout();
213             }
214         }));
215 
216     connections_.push_back(getApp()->fonts->fontChanged.connect([this] {
217         this->queueLayout();
218     }));
219 }
220 
pausable() const221 bool ChannelView::pausable() const
222 {
223     return pausable_;
224 }
225 
setPausable(bool value)226 void ChannelView::setPausable(bool value)
227 {
228     this->pausable_ = value;
229 }
230 
paused() const231 bool ChannelView::paused() const
232 {
233     /// No elements in the map -> not paused
234     return this->pausable() && !this->pauses_.empty();
235 }
236 
pause(PauseReason reason,boost::optional<uint> msecs)237 void ChannelView::pause(PauseReason reason, boost::optional<uint> msecs)
238 {
239     if (msecs)
240     {
241         /// Msecs has a value
242         auto timePoint =
243             SteadyClock::now() + std::chrono::milliseconds(msecs.get());
244         auto it = this->pauses_.find(reason);
245 
246         if (it == this->pauses_.end())
247         {
248             /// No value found so we insert a new one.
249             this->pauses_[reason] = timePoint;
250         }
251         else
252         {
253             /// If the new time point is newer then we override.
254             if (it->second && it->second.get() < timePoint)
255                 it->second = timePoint;
256         }
257     }
258     else
259     {
260         /// Msecs is none -> pause is infinite.
261         /// We just override the value.
262         this->pauses_[reason] = boost::none;
263     }
264 
265     this->updatePauses();
266 }
267 
unpause(PauseReason reason)268 void ChannelView::unpause(PauseReason reason)
269 {
270     /// Remove the value from the map
271     this->pauses_.erase(reason);
272 
273     this->updatePauses();
274 }
275 
updatePauses()276 void ChannelView::updatePauses()
277 {
278     using namespace std::chrono;
279 
280     if (this->pauses_.empty())
281     {
282         this->unpaused();
283 
284         /// No pauses so we can stop the timer
285         this->pauseEnd_ = boost::none;
286         this->pauseTimer_.stop();
287 
288         this->scrollBar_->offset(this->pauseScrollOffset_);
289         this->pauseScrollOffset_ = 0;
290 
291         this->queueLayout();
292     }
293     else if (std::any_of(this->pauses_.begin(), this->pauses_.end(),
294                          [](auto &&value) {
295                              return !value.second;
296                          }))
297     {
298         /// Some of the pauses are infinite
299         this->pauseEnd_ = boost::none;
300         this->pauseTimer_.stop();
301     }
302     else
303     {
304         /// Get the maximum pause
305         auto pauseEnd =
306             std::max_element(this->pauses_.begin(), this->pauses_.end(),
307                              [](auto &&a, auto &&b) {
308                                  return a.second > b.second;
309                              })
310                 ->second.get();
311 
312         if (pauseEnd != this->pauseEnd_)
313         {
314             /// Start the timer
315             this->pauseEnd_ = pauseEnd;
316             this->pauseTimer_.start(
317                 duration_cast<milliseconds>(pauseEnd - SteadyClock::now()));
318         }
319     }
320 }
321 
unpaused()322 void ChannelView::unpaused()
323 {
324     /// Move selection
325     this->selection_.selectionMin.messageIndex -= this->pauseSelectionOffset_;
326     this->selection_.selectionMax.messageIndex -= this->pauseSelectionOffset_;
327     this->selection_.start.messageIndex -= this->pauseSelectionOffset_;
328     this->selection_.end.messageIndex -= this->pauseSelectionOffset_;
329 
330     this->pauseSelectionOffset_ = 0;
331 }
332 
themeChangedEvent()333 void ChannelView::themeChangedEvent()
334 {
335     BaseWidget::themeChangedEvent();
336 
337     this->queueLayout();
338 }
339 
scaleChangedEvent(float scale)340 void ChannelView::scaleChangedEvent(float scale)
341 {
342     BaseWidget::scaleChangedEvent(scale);
343 
344     if (this->goToBottom_)
345     {
346         auto factor = this->qtFontScale();
347 #ifdef Q_OS_MACOS
348         factor = scale * 80.f /
349                  std::max<float>(
350                      0.01, this->logicalDpiX() * this->devicePixelRatioF());
351 #endif
352         this->goToBottom_->getLabel().setFont(
353             getFonts()->getFont(FontStyle::UiMedium, factor));
354     }
355 }
356 
queueUpdate()357 void ChannelView::queueUpdate()
358 {
359     //    if (this->updateTimer.isActive()) {
360     //        this->updateQueued = true;
361     //        return;
362     //    }
363 
364     //    this->repaint();
365 
366     this->update();
367 
368     //    this->updateTimer.start();
369 }
370 
queueLayout()371 void ChannelView::queueLayout()
372 {
373     //    if (!this->layoutCooldown->isActive()) {
374     this->performLayout();
375 
376     //        this->layoutCooldown->start();
377     //    } else {
378     //        this->layoutQueued = true;
379     //    }
380 }
381 
performLayout(bool causedByScrollbar)382 void ChannelView::performLayout(bool causedByScrollbar)
383 {
384     // BenchmarkGuard benchmark("layout");
385 
386     /// Get messages and check if there are at least 1
387     auto messages = this->getMessagesSnapshot();
388 
389     this->showingLatestMessages_ =
390         this->scrollBar_->isAtBottom() || !this->scrollBar_->isVisible();
391 
392     /// Layout visible messages
393     this->layoutVisibleMessages(messages);
394 
395     /// Update scrollbar
396     this->updateScrollbar(messages, causedByScrollbar);
397 
398     this->goToBottom_->setVisible(this->enableScrollingToBottom_ &&
399                                   this->scrollBar_->isVisible() &&
400                                   !this->scrollBar_->isAtBottom());
401 }
402 
layoutVisibleMessages(LimitedQueueSnapshot<MessageLayoutPtr> & messages)403 void ChannelView::layoutVisibleMessages(
404     LimitedQueueSnapshot<MessageLayoutPtr> &messages)
405 {
406     const auto start = size_t(this->scrollBar_->getCurrentValue());
407     const auto layoutWidth = this->getLayoutWidth();
408     const auto flags = this->getFlags();
409     auto redrawRequired = false;
410 
411     if (messages.size() > start)
412     {
413         auto y = int(-(messages[start]->getHeight() *
414                        (fmod(this->scrollBar_->getCurrentValue(), 1))));
415 
416         for (auto i = start; i < messages.size() && y <= this->height(); i++)
417         {
418             auto message = messages[i];
419 
420             redrawRequired |=
421                 message->layout(layoutWidth, this->scale(), flags);
422 
423             y += message->getHeight();
424         }
425     }
426 
427     if (redrawRequired)
428         this->queueUpdate();
429 }
430 
updateScrollbar(LimitedQueueSnapshot<MessageLayoutPtr> & messages,bool causedByScrollbar)431 void ChannelView::updateScrollbar(
432     LimitedQueueSnapshot<MessageLayoutPtr> &messages, bool causedByScrollbar)
433 {
434     if (messages.size() == 0)
435     {
436         this->scrollBar_->setVisible(false);
437         return;
438     }
439 
440     /// Layout the messages at the bottom
441     auto h = this->height() - 8;
442     auto flags = this->getFlags();
443     auto layoutWidth = this->getLayoutWidth();
444     auto showScrollbar = false;
445 
446     // convert i to int since it checks >= 0
447     for (auto i = int(messages.size()) - 1; i >= 0; i--)
448     {
449         auto *message = messages[i].get();
450 
451         message->layout(layoutWidth, this->scale(), flags);
452 
453         h -= message->getHeight();
454 
455         if (h < 0)  // break condition
456         {
457             this->scrollBar_->setLargeChange(
458                 (messages.size() - i) +
459                 qreal(h) / std::max<int>(1, message->getHeight()));
460 
461             showScrollbar = true;
462             break;
463         }
464     }
465 
466     /// Update scrollbar values
467     this->scrollBar_->setVisible(showScrollbar);
468 
469     if (!showScrollbar && !causedByScrollbar)
470     {
471         this->scrollBar_->setDesiredValue(0);
472     }
473 
474     this->scrollBar_->setMaximum(messages.size());
475 
476     // If we were showing the latest messages and the scrollbar now wants to be
477     // rendered, scroll to bottom
478     if (this->enableScrollingToBottom_ && this->showingLatestMessages_ &&
479         showScrollbar)
480     {
481         this->scrollBar_->scrollToBottom(
482             // this->messageWasAdded &&
483             getSettings()->enableSmoothScrollingNewMessages.getValue());
484         this->messageWasAdded_ = false;
485     }
486 }
487 
clearMessages()488 void ChannelView::clearMessages()
489 {
490     // Clear all stored messages in this chat widget
491     this->messages_.clear();
492     this->scrollBar_->clearHighlights();
493     this->queueLayout();
494 
495     this->lastMessageHasAlternateBackground_ = false;
496     this->lastMessageHasAlternateBackgroundReverse_ = true;
497 }
498 
getScrollBar()499 Scrollbar &ChannelView::getScrollBar()
500 {
501     return *this->scrollBar_;
502 }
503 
getSelectedText()504 QString ChannelView::getSelectedText()
505 {
506     QString result = "";
507 
508     LimitedQueueSnapshot<MessageLayoutPtr> messagesSnapshot =
509         this->getMessagesSnapshot();
510 
511     Selection _selection = this->selection_;
512 
513     if (_selection.isEmpty())
514     {
515         return result;
516     }
517 
518     for (int msg = _selection.selectionMin.messageIndex;
519          msg <= _selection.selectionMax.messageIndex; msg++)
520     {
521         MessageLayoutPtr layout = messagesSnapshot[msg];
522         int from = msg == _selection.selectionMin.messageIndex
523                        ? _selection.selectionMin.charIndex
524                        : 0;
525         int to = msg == _selection.selectionMax.messageIndex
526                      ? _selection.selectionMax.charIndex
527                      : layout->getLastCharacterIndex() + 1;
528 
529         layout->addSelectionText(result, from, to);
530     }
531 
532     return result;
533 }
534 
hasSelection()535 bool ChannelView::hasSelection()
536 {
537     return !this->selection_.isEmpty();
538 }
539 
clearSelection()540 void ChannelView::clearSelection()
541 {
542     this->selection_ = Selection();
543     queueLayout();
544 }
545 
setEnableScrollingToBottom(bool value)546 void ChannelView::setEnableScrollingToBottom(bool value)
547 {
548     this->enableScrollingToBottom_ = value;
549 }
550 
getEnableScrollingToBottom() const551 bool ChannelView::getEnableScrollingToBottom() const
552 {
553     return this->enableScrollingToBottom_;
554 }
555 
setOverrideFlags(boost::optional<MessageElementFlags> value)556 void ChannelView::setOverrideFlags(boost::optional<MessageElementFlags> value)
557 {
558     this->overrideFlags_ = std::move(value);
559 }
560 
getOverrideFlags() const561 const boost::optional<MessageElementFlags> &ChannelView::getOverrideFlags()
562     const
563 {
564     return this->overrideFlags_;
565 }
566 
getMessagesSnapshot()567 LimitedQueueSnapshot<MessageLayoutPtr> ChannelView::getMessagesSnapshot()
568 {
569     if (!this->paused() /*|| this->scrollBar_->isVisible()*/)
570     {
571         this->snapshot_ = this->messages_.getSnapshot();
572     }
573 
574     return this->snapshot_;
575 }
576 
channel()577 ChannelPtr ChannelView::channel()
578 {
579     return this->channel_;
580 }
581 
showScrollbarHighlights() const582 bool ChannelView::showScrollbarHighlights() const
583 {
584     return this->channel_->getType() != Channel::Type::TwitchMentions;
585 }
586 
setChannel(ChannelPtr underlyingChannel)587 void ChannelView::setChannel(ChannelPtr underlyingChannel)
588 {
589     /// Clear connections from the last channel
590     this->channelConnections_.clear();
591 
592     this->clearMessages();
593     this->scrollBar_->clearHighlights();
594 
595     /// make copy of channel and expose
596     this->channel_ = std::make_unique<Channel>(underlyingChannel->getName(),
597                                                underlyingChannel->getType());
598 
599     //
600     // Proxy channel connections
601     // Use a proxy channel to keep filtered messages past the time they are removed from their origin channel
602     //
603 
604     this->channelConnections_.push_back(
605         underlyingChannel->messageAppended.connect(
606             [this](MessagePtr &message,
607                    boost::optional<MessageFlags> overridingFlags) {
608                 if (this->shouldIncludeMessage(message))
609                 {
610                     if (this->channel_->lastDate_ != QDate::currentDate())
611                     {
612                         this->channel_->lastDate_ = QDate::currentDate();
613                         auto msg = makeSystemMessage(
614                             QLocale().toString(QDate::currentDate(),
615                                                QLocale::LongFormat),
616                             QTime(0, 0));
617                         this->channel_->addMessage(msg);
618                     }
619                     // When the message was received in the underlyingChannel,
620                     // logging will be handled. Prevent duplications.
621                     if (overridingFlags)
622                     {
623                         overridingFlags.get().set(MessageFlag::DoNotLog);
624                     }
625                     else
626                     {
627                         overridingFlags = MessageFlags(message->flags);
628                         overridingFlags.get().set(MessageFlag::DoNotLog);
629                     }
630 
631                     this->channel_->addMessage(message, overridingFlags);
632                 }
633             }));
634 
635     this->channelConnections_.push_back(
636         underlyingChannel->messagesAddedAtStart.connect(
637             [this](std::vector<MessagePtr> &messages) {
638                 std::vector<MessagePtr> filtered;
639                 std::copy_if(messages.begin(), messages.end(),
640                              std::back_inserter(filtered),
641                              [this](MessagePtr msg) {
642                                  return this->shouldIncludeMessage(msg);
643                              });
644 
645                 if (!filtered.empty())
646                     this->channel_->addMessagesAtStart(filtered);
647             }));
648 
649     this->channelConnections_.push_back(
650         underlyingChannel->messageReplaced.connect(
651             [this](size_t index, MessagePtr replacement) {
652                 if (this->shouldIncludeMessage(replacement))
653                     this->channel_->replaceMessage(index, replacement);
654             }));
655 
656     //
657     // Standard channel connections
658     //
659 
660     // on new message
661     this->channelConnections_.push_back(this->channel_->messageAppended.connect(
662         [this](MessagePtr &message,
663                boost::optional<MessageFlags> overridingFlags) {
664             this->messageAppended(message, std::move(overridingFlags));
665         }));
666 
667     this->channelConnections_.push_back(
668         this->channel_->messagesAddedAtStart.connect(
669             [this](std::vector<MessagePtr> &messages) {
670                 this->messageAddedAtStart(messages);
671             }));
672 
673     // on message removed
674     this->channelConnections_.push_back(
675         this->channel_->messageRemovedFromStart.connect(
676             [this](MessagePtr &message) {
677                 this->messageRemoveFromStart(message);
678             }));
679 
680     // on message replaced
681     this->channelConnections_.push_back(this->channel_->messageReplaced.connect(
682         [this](size_t index, MessagePtr replacement) {
683             this->messageReplaced(index, replacement);
684         }));
685 
686     auto snapshot = underlyingChannel->getMessageSnapshot();
687 
688     for (size_t i = 0; i < snapshot.size(); i++)
689     {
690         MessageLayoutPtr deleted;
691 
692         auto messageLayout = new MessageLayout(snapshot[i]);
693 
694         if (this->lastMessageHasAlternateBackground_)
695         {
696             messageLayout->flags.set(MessageLayoutFlag::AlternateBackground);
697         }
698         this->lastMessageHasAlternateBackground_ =
699             !this->lastMessageHasAlternateBackground_;
700 
701         if (underlyingChannel->shouldIgnoreHighlights())
702         {
703             messageLayout->flags.set(MessageLayoutFlag::IgnoreHighlights);
704         }
705 
706         this->messages_.pushBack(MessageLayoutPtr(messageLayout), deleted);
707         if (this->showScrollbarHighlights())
708         {
709             this->scrollBar_->addHighlight(
710                 snapshot[i]->getScrollBarHighlight());
711         }
712     }
713 
714     this->underlyingChannel_ = underlyingChannel;
715 
716     this->queueLayout();
717     this->queueUpdate();
718 
719     // Notifications
720     if (auto tc = dynamic_cast<TwitchChannel *>(underlyingChannel.get()))
721     {
722         this->connections_.push_back(tc->liveStatusChanged.connect([this]() {
723             this->liveStatusChanged.invoke();
724         }));
725     }
726 }
727 
setFilters(const QList<QUuid> & ids)728 void ChannelView::setFilters(const QList<QUuid> &ids)
729 {
730     this->channelFilters_ = std::make_shared<FilterSet>(ids);
731 }
732 
getFilterIds() const733 const QList<QUuid> ChannelView::getFilterIds() const
734 {
735     if (!this->channelFilters_)
736     {
737         return QList<QUuid>();
738     }
739 
740     return this->channelFilters_->filterIds();
741 }
742 
getFilterSet() const743 FilterSetPtr ChannelView::getFilterSet() const
744 {
745     return this->channelFilters_;
746 }
747 
shouldIncludeMessage(const MessagePtr & m) const748 bool ChannelView::shouldIncludeMessage(const MessagePtr &m) const
749 {
750     if (this->channelFilters_)
751     {
752         if (getSettings()->excludeUserMessagesFromFilter &&
753             getApp()->accounts->twitch.getCurrent()->getUserName().compare(
754                 m->loginName, Qt::CaseInsensitive) == 0)
755             return true;
756 
757         return this->channelFilters_->filter(m, this->channel_);
758     }
759 
760     return true;
761 }
762 
sourceChannel() const763 ChannelPtr ChannelView::sourceChannel() const
764 {
765     return this->sourceChannel_;
766 }
767 
setSourceChannel(ChannelPtr sourceChannel)768 void ChannelView::setSourceChannel(ChannelPtr sourceChannel)
769 {
770     this->sourceChannel_ = std::move(sourceChannel);
771 }
772 
hasSourceChannel() const773 bool ChannelView::hasSourceChannel() const
774 {
775     return this->sourceChannel_ != nullptr;
776 }
777 
messageAppended(MessagePtr & message,boost::optional<MessageFlags> overridingFlags)778 void ChannelView::messageAppended(MessagePtr &message,
779                                   boost::optional<MessageFlags> overridingFlags)
780 {
781     MessageLayoutPtr deleted;
782 
783     auto *messageFlags = &message->flags;
784     if (overridingFlags)
785     {
786         messageFlags = overridingFlags.get_ptr();
787     }
788 
789     auto messageRef = new MessageLayout(message);
790 
791     if (this->lastMessageHasAlternateBackground_)
792     {
793         messageRef->flags.set(MessageLayoutFlag::AlternateBackground);
794     }
795     if (this->channel_->shouldIgnoreHighlights())
796     {
797         messageRef->flags.set(MessageLayoutFlag::IgnoreHighlights);
798     }
799     this->lastMessageHasAlternateBackground_ =
800         !this->lastMessageHasAlternateBackground_;
801 
802     if (!this->scrollBar_->isAtBottom() &&
803         this->scrollBar_->getCurrentValueAnimation().state() ==
804             QPropertyAnimation::Running)
805     {
806         QEventLoop loop;
807 
808         connect(&this->scrollBar_->getCurrentValueAnimation(),
809                 &QAbstractAnimation::stateChanged, &loop, &QEventLoop::quit);
810 
811         loop.exec();
812     }
813 
814     if (this->messages_.pushBack(MessageLayoutPtr(messageRef), deleted))
815     {
816         if (this->paused())
817         {
818             if (!this->scrollBar_->isAtBottom())
819                 this->pauseScrollOffset_--;
820         }
821         else
822         {
823             if (this->scrollBar_->isAtBottom())
824                 this->scrollBar_->scrollToBottom();
825             else
826                 this->scrollBar_->offset(-1);
827         }
828     }
829 
830     if (!messageFlags->has(MessageFlag::DoNotTriggerNotification))
831     {
832         if (messageFlags->has(MessageFlag::Highlighted) &&
833             messageFlags->has(MessageFlag::ShowInMentions) &&
834             !messageFlags->has(MessageFlag::Subscription) &&
835             (getSettings()->highlightMentions ||
836              this->channel_->getType() != Channel::Type::TwitchMentions))
837 
838         {
839             this->tabHighlightRequested.invoke(HighlightState::Highlighted);
840         }
841         else
842         {
843             this->tabHighlightRequested.invoke(HighlightState::NewMessage);
844         }
845     }
846 
847     if (this->showScrollbarHighlights())
848     {
849         this->scrollBar_->addHighlight(message->getScrollBarHighlight());
850     }
851 
852     this->messageWasAdded_ = true;
853     this->queueLayout();
854 }
855 
messageAddedAtStart(std::vector<MessagePtr> & messages)856 void ChannelView::messageAddedAtStart(std::vector<MessagePtr> &messages)
857 {
858     std::vector<MessageLayoutPtr> messageRefs;
859     messageRefs.resize(messages.size());
860 
861     /// Create message layouts
862     for (size_t i = 0; i < messages.size(); i++)
863     {
864         auto message = messages.at(i);
865         auto layout = new MessageLayout(message);
866 
867         // alternate color
868         if (!this->lastMessageHasAlternateBackgroundReverse_)
869             layout->flags.set(MessageLayoutFlag::AlternateBackground);
870         this->lastMessageHasAlternateBackgroundReverse_ =
871             !this->lastMessageHasAlternateBackgroundReverse_;
872 
873         messageRefs.at(i) = MessageLayoutPtr(layout);
874     }
875 
876     /// Add the messages at the start
877     if (this->messages_.pushFront(messageRefs).size() > 0)
878     {
879         if (this->scrollBar_->isAtBottom())
880             this->scrollBar_->scrollToBottom();
881         else
882             this->scrollBar_->offset(qreal(messages.size()));
883     }
884 
885     if (this->showScrollbarHighlights())
886     {
887         std::vector<ScrollbarHighlight> highlights;
888         highlights.reserve(messages.size());
889         for (const auto &message : messages)
890         {
891             highlights.push_back(message->getScrollBarHighlight());
892         }
893 
894         this->scrollBar_->addHighlightsAtStart(highlights);
895     }
896 
897     this->messageWasAdded_ = true;
898     this->queueLayout();
899 }
900 
messageRemoveFromStart(MessagePtr & message)901 void ChannelView::messageRemoveFromStart(MessagePtr &message)
902 {
903     if (this->paused())
904     {
905         this->pauseSelectionOffset_ += 1;
906     }
907     else
908     {
909         this->selection_.selectionMin.messageIndex--;
910         this->selection_.selectionMax.messageIndex--;
911         this->selection_.start.messageIndex--;
912         this->selection_.end.messageIndex--;
913     }
914 
915     this->queueLayout();
916 }
917 
messageReplaced(size_t index,MessagePtr & replacement)918 void ChannelView::messageReplaced(size_t index, MessagePtr &replacement)
919 {
920     if (index >= this->messages_.getSnapshot().size())
921     {
922         return;
923     }
924 
925     MessageLayoutPtr newItem(new MessageLayout(replacement));
926     auto snapshot = this->messages_.getSnapshot();
927     if (index >= snapshot.size())
928     {
929         qCDebug(chatterinoWidget)
930             << "Tried to replace out of bounds message. Index:" << index
931             << ". Length:" << snapshot.size();
932         return;
933     }
934 
935     const auto &message = snapshot[index];
936     if (message->flags.has(MessageLayoutFlag::AlternateBackground))
937     {
938         newItem->flags.set(MessageLayoutFlag::AlternateBackground);
939     }
940 
941     this->scrollBar_->replaceHighlight(index,
942                                        replacement->getScrollBarHighlight());
943 
944     this->messages_.replaceItem(message, newItem);
945     this->queueLayout();
946 }
947 
updateLastReadMessage()948 void ChannelView::updateLastReadMessage()
949 {
950     auto _snapshot = this->getMessagesSnapshot();
951 
952     if (_snapshot.size() > 0)
953     {
954         this->lastReadMessage_ = _snapshot[_snapshot.size() - 1];
955     }
956 
957     this->update();
958 }
959 
resizeEvent(QResizeEvent *)960 void ChannelView::resizeEvent(QResizeEvent *)
961 {
962     this->scrollBar_->setGeometry(this->width() - this->scrollBar_->width(), 0,
963                                   this->scrollBar_->width(), this->height());
964 
965     this->goToBottom_->setGeometry(0, this->height() - int(this->scale() * 26),
966                                    this->width(), int(this->scale() * 26));
967 
968     this->scrollBar_->raise();
969 
970     this->queueLayout();
971 
972     this->update();
973 }
974 
setSelection(const SelectionItem & start,const SelectionItem & end)975 void ChannelView::setSelection(const SelectionItem &start,
976                                const SelectionItem &end)
977 {
978     // selections
979     if (!this->selecting_ && start != end)
980     {
981         // this->messagesAddedSinceSelectionPause_ = 0;
982 
983         this->selecting_ = true;
984         // this->pausedBySelection_ = true;
985     }
986 
987     this->selection_ = Selection(start, end);
988 
989     this->selectionChanged.invoke();
990 }
991 
getFlags() const992 MessageElementFlags ChannelView::getFlags() const
993 {
994     auto app = getApp();
995 
996     if (this->overrideFlags_)
997     {
998         return this->overrideFlags_.get();
999     }
1000 
1001     MessageElementFlags flags = app->windows->getWordFlags();
1002 
1003     Split *split = dynamic_cast<Split *>(this->parentWidget());
1004 
1005     if (split == nullptr)
1006     {
1007         SearchPopup *searchPopup =
1008             dynamic_cast<SearchPopup *>(this->parentWidget());
1009         if (searchPopup != nullptr)
1010         {
1011             split = dynamic_cast<Split *>(searchPopup->parentWidget());
1012         }
1013     }
1014 
1015     if (split != nullptr)
1016     {
1017         if (split->getModerationMode())
1018         {
1019             flags.set(MessageElementFlag::ModeratorTools);
1020         }
1021         if (this->underlyingChannel_ == app->twitch.server->mentionsChannel ||
1022             this->underlyingChannel_ == app->twitch.server->liveChannel)
1023         {
1024             flags.set(MessageElementFlag::ChannelName);
1025             flags.unset(MessageElementFlag::ChannelPointReward);
1026         }
1027     }
1028 
1029     if (this->sourceChannel_ == app->twitch.server->mentionsChannel)
1030         flags.set(MessageElementFlag::ChannelName);
1031 
1032     return flags;
1033 }
1034 
paintEvent(QPaintEvent *)1035 void ChannelView::paintEvent(QPaintEvent * /*event*/)
1036 {
1037     //    BenchmarkGuard benchmark("paint");
1038 
1039     QPainter painter(this);
1040 
1041     painter.fillRect(rect(), this->theme->splits.background);
1042 
1043     // draw messages
1044     this->drawMessages(painter);
1045 
1046     // draw paused sign
1047     if (this->paused())
1048     {
1049         auto a = this->scale() * 20;
1050         auto brush = QBrush(QColor(127, 127, 127, 255));
1051         painter.fillRect(QRectF(5, a / 4, a / 4, a), brush);
1052         painter.fillRect(QRectF(15, a / 4, a / 4, a), brush);
1053     }
1054 }
1055 
1056 // if overlays is false then it draws the message, if true then it draws things
1057 // such as the grey overlay when a message is disabled
drawMessages(QPainter & painter)1058 void ChannelView::drawMessages(QPainter &painter)
1059 {
1060     auto messagesSnapshot = this->getMessagesSnapshot();
1061 
1062     size_t start = size_t(this->scrollBar_->getCurrentValue());
1063 
1064     if (start >= messagesSnapshot.size())
1065     {
1066         return;
1067     }
1068 
1069     int y = int(-(messagesSnapshot[start].get()->getHeight() *
1070                   (fmod(this->scrollBar_->getCurrentValue(), 1))));
1071 
1072     MessageLayout *end = nullptr;
1073     bool windowFocused = this->window() == QApplication::activeWindow();
1074 
1075     auto app = getApp();
1076     bool isMentions =
1077         this->underlyingChannel_ == app->twitch.server->mentionsChannel;
1078 
1079     for (size_t i = start; i < messagesSnapshot.size(); ++i)
1080     {
1081         MessageLayout *layout = messagesSnapshot[i].get();
1082 
1083         bool isLastMessage = false;
1084         if (getSettings()->showLastMessageIndicator)
1085         {
1086             isLastMessage = this->lastReadMessage_.get() == layout;
1087         }
1088 
1089         layout->paint(painter, DRAW_WIDTH, y, i, this->selection_,
1090                       isLastMessage, windowFocused, isMentions);
1091 
1092         y += layout->getHeight();
1093 
1094         end = layout;
1095         if (y > this->height())
1096         {
1097             break;
1098         }
1099     }
1100 
1101     if (end == nullptr)
1102     {
1103         return;
1104     }
1105 
1106     // remove messages that are on screen
1107     // the messages that are left at the end get their buffers reset
1108     for (size_t i = start; i < messagesSnapshot.size(); ++i)
1109     {
1110         auto it = this->messagesOnScreen_.find(messagesSnapshot[i]);
1111         if (it != this->messagesOnScreen_.end())
1112         {
1113             this->messagesOnScreen_.erase(it);
1114         }
1115     }
1116 
1117     // delete the message buffers that aren't on screen
1118     for (const std::shared_ptr<MessageLayout> &item : this->messagesOnScreen_)
1119     {
1120         item->deleteBuffer();
1121     }
1122 
1123     this->messagesOnScreen_.clear();
1124 
1125     // add all messages on screen to the map
1126     for (size_t i = start; i < messagesSnapshot.size(); ++i)
1127     {
1128         std::shared_ptr<MessageLayout> layout = messagesSnapshot[i];
1129 
1130         this->messagesOnScreen_.insert(layout);
1131 
1132         if (layout.get() == end)
1133         {
1134             break;
1135         }
1136     }
1137 }
1138 
wheelEvent(QWheelEvent * event)1139 void ChannelView::wheelEvent(QWheelEvent *event)
1140 {
1141     if (!event->angleDelta().y())
1142     {
1143         return;
1144     }
1145 
1146     if (event->modifiers() & Qt::ControlModifier)
1147     {
1148         event->ignore();
1149         return;
1150     }
1151 
1152     if (this->scrollBar_->isVisible())
1153     {
1154         float mouseMultiplier = getSettings()->mouseScrollMultiplier;
1155 
1156         qreal desired = this->scrollBar_->getDesiredValue();
1157         qreal delta = event->angleDelta().y() * qreal(1.5) * mouseMultiplier;
1158 
1159         auto snapshot = this->getMessagesSnapshot();
1160         int snapshotLength = int(snapshot.size());
1161         int i = std::min<int>(int(desired), snapshotLength);
1162 
1163         if (delta > 0)
1164         {
1165             qreal scrollFactor = fmod(desired, 1);
1166             qreal currentScrollLeft = std::max<qreal>(
1167                 0.01, int(scrollFactor * snapshot[i]->getHeight()));
1168 
1169             for (; i >= 0; i--)
1170             {
1171                 if (delta < currentScrollLeft)
1172                 {
1173                     desired -= scrollFactor * (delta / currentScrollLeft);
1174                     break;
1175                 }
1176                 else
1177                 {
1178                     delta -= currentScrollLeft;
1179                     desired -= scrollFactor;
1180                 }
1181 
1182                 if (i == 0)
1183                 {
1184                     desired = 0;
1185                 }
1186                 else
1187                 {
1188                     snapshot[i - 1]->layout(this->getLayoutWidth(),
1189                                             this->scale(), this->getFlags());
1190                     scrollFactor = 1;
1191                     currentScrollLeft = snapshot[i - 1]->getHeight();
1192                 }
1193             }
1194         }
1195         else
1196         {
1197             delta = -delta;
1198             qreal scrollFactor = 1 - fmod(desired, 1);
1199             qreal currentScrollLeft = std::max<qreal>(
1200                 0.01, int(scrollFactor * snapshot[i]->getHeight()));
1201 
1202             for (; i < snapshotLength; i++)
1203             {
1204                 if (delta < currentScrollLeft)
1205                 {
1206                     desired +=
1207                         scrollFactor * (qreal(delta) / currentScrollLeft);
1208                     break;
1209                 }
1210                 else
1211                 {
1212                     delta -= currentScrollLeft;
1213                     desired += scrollFactor;
1214                 }
1215 
1216                 if (i == snapshotLength - 1)
1217                 {
1218                     desired = snapshot.size();
1219                 }
1220                 else
1221                 {
1222                     snapshot[i + 1]->layout(this->getLayoutWidth(),
1223                                             this->scale(), this->getFlags());
1224 
1225                     scrollFactor = 1;
1226                     currentScrollLeft = snapshot[i + 1]->getHeight();
1227                 }
1228             }
1229         }
1230 
1231         this->scrollBar_->setDesiredValue(desired, true);
1232     }
1233 }
1234 
enterEvent(QEvent *)1235 void ChannelView::enterEvent(QEvent *)
1236 {
1237 }
1238 
leaveEvent(QEvent *)1239 void ChannelView::leaveEvent(QEvent *)
1240 {
1241     this->unpause(PauseReason::Mouse);
1242 
1243     this->queueLayout();
1244 }
1245 
mouseMoveEvent(QMouseEvent * event)1246 void ChannelView::mouseMoveEvent(QMouseEvent *event)
1247 {
1248     /// Pause on hover
1249     if (float pauseTime = getSettings()->pauseOnHoverDuration;
1250         pauseTime > 0.001f)
1251     {
1252         this->pause(PauseReason::Mouse, uint(pauseTime * 1000.f));
1253     }
1254     else if (pauseTime < -0.5f)
1255     {
1256         this->pause(PauseReason::Mouse);
1257     }
1258 
1259     auto tooltipWidget = TooltipWidget::instance();
1260     std::shared_ptr<MessageLayout> layout;
1261     QPoint relativePos;
1262     int messageIndex;
1263 
1264     // no message under cursor
1265     if (!tryGetMessageAt(event->pos(), layout, relativePos, messageIndex))
1266     {
1267         this->setCursor(Qt::ArrowCursor);
1268         tooltipWidget->hide();
1269         return;
1270     }
1271 
1272     if (this->isScrolling_)
1273     {
1274         this->currentMousePosition_ = event->screenPos();
1275     }
1276 
1277     // is selecting
1278     if (this->isLeftMouseDown_)
1279     {
1280         // this->pause(PauseReason::Selecting, 300);
1281         int index = layout->getSelectionIndex(relativePos);
1282 
1283         this->setSelection(this->selection_.start,
1284                            SelectionItem(messageIndex, index));
1285 
1286         this->queueUpdate();
1287     }
1288 
1289     // message under cursor is collapsed
1290     if (layout->flags.has(MessageLayoutFlag::Collapsed))
1291     {
1292         this->setCursor(Qt::PointingHandCursor);
1293         tooltipWidget->hide();
1294         return;
1295     }
1296 
1297     // check if word underneath cursor
1298     const MessageLayoutElement *hoverLayoutElement =
1299         layout->getElementAt(relativePos);
1300 
1301     if (hoverLayoutElement == nullptr)
1302     {
1303         this->setCursor(Qt::ArrowCursor);
1304         tooltipWidget->hide();
1305         return;
1306     }
1307 
1308     if (this->isDoubleClick_)
1309     {
1310         int wordStart;
1311         int wordEnd;
1312         this->getWordBounds(layout.get(), hoverLayoutElement, relativePos,
1313                             wordStart, wordEnd);
1314         SelectionItem newStart(messageIndex, wordStart);
1315         SelectionItem newEnd(messageIndex, wordEnd);
1316 
1317         // Selection changed in same message
1318         if (messageIndex == this->doubleClickSelection_.origMessageIndex)
1319         {
1320             // Selecting to the left
1321             if (wordStart < this->selection_.start.charIndex &&
1322                 !this->doubleClickSelection_.selectingRight)
1323             {
1324                 this->doubleClickSelection_.selectingLeft = true;
1325                 // Ensure that the original word stays selected(Edge case)
1326                 if (wordStart > this->doubleClickSelection_.originalEnd)
1327                 {
1328                     this->setSelection(
1329                         this->doubleClickSelection_.origStartItem, newEnd);
1330                 }
1331                 else
1332                 {
1333                     this->setSelection(newStart, this->selection_.end);
1334                 }
1335                 // Selecting to the right
1336             }
1337             else if (wordEnd > this->selection_.end.charIndex &&
1338                      !this->doubleClickSelection_.selectingLeft)
1339             {
1340                 this->doubleClickSelection_.selectingRight = true;
1341                 // Ensure that the original word stays selected(Edge case)
1342                 if (wordEnd < this->doubleClickSelection_.originalStart)
1343                 {
1344                     this->setSelection(newStart,
1345                                        this->doubleClickSelection_.origEndItem);
1346                 }
1347                 else
1348                 {
1349                     this->setSelection(this->selection_.start, newEnd);
1350                 }
1351             }
1352             // Swapping from selecting left to selecting right
1353             if (wordStart > this->selection_.start.charIndex &&
1354                 !this->doubleClickSelection_.selectingRight)
1355             {
1356                 if (wordStart > this->doubleClickSelection_.originalEnd)
1357                 {
1358                     this->doubleClickSelection_.selectingLeft = false;
1359                     this->doubleClickSelection_.selectingRight = true;
1360                     this->setSelection(
1361                         this->doubleClickSelection_.origStartItem, newEnd);
1362                 }
1363                 else
1364                 {
1365                     this->setSelection(newStart, this->selection_.end);
1366                 }
1367                 // Swapping from selecting right to selecting left
1368             }
1369             else if (wordEnd < this->selection_.end.charIndex &&
1370                      !this->doubleClickSelection_.selectingLeft)
1371             {
1372                 if (wordEnd < this->doubleClickSelection_.originalStart)
1373                 {
1374                     this->doubleClickSelection_.selectingLeft = true;
1375                     this->doubleClickSelection_.selectingRight = false;
1376                     this->setSelection(newStart,
1377                                        this->doubleClickSelection_.origEndItem);
1378                 }
1379                 else
1380                 {
1381                     this->setSelection(this->selection_.start, newEnd);
1382                 }
1383             }
1384             // Selection changed in a different message
1385         }
1386         else
1387         {
1388             // Message over the original
1389             if (messageIndex < this->selection_.start.messageIndex)
1390             {
1391                 // Swapping from left to right selecting
1392                 if (!this->doubleClickSelection_.selectingLeft)
1393                 {
1394                     this->doubleClickSelection_.selectingLeft = true;
1395                     this->doubleClickSelection_.selectingRight = false;
1396                 }
1397                 if (wordStart < this->selection_.start.charIndex &&
1398                     !this->doubleClickSelection_.selectingRight)
1399                 {
1400                     this->doubleClickSelection_.selectingLeft = true;
1401                 }
1402                 this->setSelection(newStart,
1403                                    this->doubleClickSelection_.origEndItem);
1404                 // Message under the original
1405             }
1406             else if (messageIndex > this->selection_.end.messageIndex)
1407             {
1408                 // Swapping from right to left selecting
1409                 if (!this->doubleClickSelection_.selectingRight)
1410                 {
1411                     this->doubleClickSelection_.selectingLeft = false;
1412                     this->doubleClickSelection_.selectingRight = true;
1413                 }
1414                 if (wordEnd > this->selection_.end.charIndex &&
1415                     !this->doubleClickSelection_.selectingLeft)
1416                 {
1417                     this->doubleClickSelection_.selectingRight = true;
1418                 }
1419                 this->setSelection(this->doubleClickSelection_.origStartItem,
1420                                    newEnd);
1421                 // Selection changed in non original message
1422             }
1423             else
1424             {
1425                 if (this->doubleClickSelection_.selectingLeft)
1426                 {
1427                     this->setSelection(newStart, this->selection_.end);
1428                 }
1429                 else
1430                 {
1431                     this->setSelection(this->selection_.start, newEnd);
1432                 }
1433             }
1434         }
1435         // Reset direction of selection
1436         if (wordStart == this->doubleClickSelection_.originalStart &&
1437             wordEnd == this->doubleClickSelection_.originalEnd)
1438         {
1439             this->doubleClickSelection_.selectingLeft =
1440                 this->doubleClickSelection_.selectingRight = false;
1441         }
1442     }
1443 
1444     auto element = &hoverLayoutElement->getCreator();
1445     bool isLinkValid = hoverLayoutElement->getLink().isValid();
1446     auto emoteElement = dynamic_cast<const EmoteElement *>(element);
1447 
1448     if (element->getTooltip().isEmpty() ||
1449         (isLinkValid && emoteElement == nullptr &&
1450          !getSettings()->linkInfoTooltip))
1451     {
1452         tooltipWidget->hide();
1453     }
1454     else
1455     {
1456         auto &tooltipPreviewImage = TooltipPreviewImage::instance();
1457         tooltipPreviewImage.setImageScale(0, 0);
1458         auto badgeElement = dynamic_cast<const BadgeElement *>(element);
1459 
1460         if ((badgeElement || emoteElement) &&
1461             getSettings()->emotesTooltipPreview.getValue())
1462         {
1463             if (event->modifiers() == Qt::ShiftModifier ||
1464                 getSettings()->emotesTooltipPreview.getValue() == 1)
1465             {
1466                 if (emoteElement)
1467                 {
1468                     tooltipPreviewImage.setImage(
1469                         emoteElement->getEmote()->images.getImage(3.0));
1470                 }
1471                 else if (badgeElement)
1472                 {
1473                     tooltipPreviewImage.setImage(
1474                         badgeElement->getEmote()->images.getImage(3.0));
1475                 }
1476             }
1477             else
1478             {
1479                 tooltipPreviewImage.setImage(nullptr);
1480             }
1481         }
1482         else
1483         {
1484             if (element->getTooltip() == "No link info loaded")
1485             {
1486                 std::weak_ptr<MessageLayout> weakLayout = layout;
1487                 LinkResolver::getLinkInfo(
1488                     element->getLink().value, nullptr,
1489                     [weakLayout, element](QString tooltipText,
1490                                           Link originalLink,
1491                                           ImagePtr thumbnail) {
1492                         auto shared = weakLayout.lock();
1493                         if (!shared)
1494                             return;
1495                         element->setTooltip(tooltipText);
1496                         element->setThumbnail(thumbnail);
1497                     });
1498             }
1499             auto thumbnailSize = getSettings()->thumbnailSize;
1500             if (!thumbnailSize)
1501             {
1502                 tooltipPreviewImage.setImage(nullptr);
1503             }
1504             else
1505             {
1506                 const auto shouldHideThumbnail =
1507                     isInStreamerMode() &&
1508                     getSettings()->streamerModeHideLinkThumbnails &&
1509                     element->getThumbnail() != nullptr &&
1510                     !element->getThumbnail()->url().string.isEmpty();
1511                 auto thumb =
1512                     shouldHideThumbnail
1513                         ? Image::fromPixmap(getResources().streamerMode)
1514                         : element->getThumbnail();
1515                 tooltipPreviewImage.setImage(std::move(thumb));
1516 
1517                 if (element->getThumbnailType() ==
1518                     MessageElement::ThumbnailType::Link_Thumbnail)
1519                 {
1520                     tooltipPreviewImage.setImageScale(thumbnailSize,
1521                                                       thumbnailSize);
1522                 }
1523             }
1524         }
1525 
1526         tooltipWidget->moveTo(this, event->globalPos());
1527         tooltipWidget->setWordWrap(isLinkValid);
1528         tooltipWidget->setText(element->getTooltip());
1529         tooltipWidget->adjustSize();
1530         tooltipWidget->setWindowFlag(Qt::WindowStaysOnTopHint, true);
1531         tooltipWidget->show();
1532         tooltipWidget->raise();
1533     }
1534 
1535     // check if word has a link
1536     if (isLinkValid)
1537     {
1538         this->setCursor(Qt::PointingHandCursor);
1539     }
1540     else
1541     {
1542         this->setCursor(Qt::ArrowCursor);
1543     }
1544 }
1545 
mousePressEvent(QMouseEvent * event)1546 void ChannelView::mousePressEvent(QMouseEvent *event)
1547 {
1548     this->mouseDown.invoke(event);
1549 
1550     std::shared_ptr<MessageLayout> layout;
1551     QPoint relativePos;
1552     int messageIndex;
1553 
1554     if (!tryGetMessageAt(event->pos(), layout, relativePos, messageIndex))
1555     {
1556         setCursor(Qt::ArrowCursor);
1557         auto messagesSnapshot = this->getMessagesSnapshot();
1558         if (messagesSnapshot.size() == 0)
1559         {
1560             return;
1561         }
1562 
1563         // Start selection at the last message at its last index
1564         if (event->button() == Qt::LeftButton)
1565         {
1566             auto lastMessageIndex = messagesSnapshot.size() - 1;
1567             auto lastMessage = messagesSnapshot[lastMessageIndex];
1568             auto lastCharacterIndex = lastMessage->getLastCharacterIndex();
1569 
1570             SelectionItem selectionItem(lastMessageIndex, lastCharacterIndex);
1571             this->setSelection(selectionItem, selectionItem);
1572         }
1573         return;
1574     }
1575 
1576     // check if message is collapsed
1577     switch (event->button())
1578     {
1579         case Qt::LeftButton: {
1580             if (this->isScrolling_)
1581                 this->disableScrolling();
1582 
1583             this->lastLeftPressPosition_ = event->screenPos();
1584             this->isLeftMouseDown_ = true;
1585 
1586             if (layout->flags.has(MessageLayoutFlag::Collapsed))
1587                 return;
1588 
1589             if (getSettings()->linksDoubleClickOnly.getValue())
1590             {
1591                 this->pause(PauseReason::DoubleClick, 200);
1592             }
1593 
1594             int index = layout->getSelectionIndex(relativePos);
1595             auto selectionItem = SelectionItem(messageIndex, index);
1596             this->setSelection(selectionItem, selectionItem);
1597         }
1598         break;
1599 
1600         case Qt::RightButton: {
1601             if (this->isScrolling_)
1602                 this->disableScrolling();
1603 
1604             this->lastRightPressPosition_ = event->screenPos();
1605             this->isRightMouseDown_ = true;
1606         }
1607         break;
1608 
1609         case Qt::MiddleButton: {
1610             const MessageLayoutElement *hoverLayoutElement =
1611                 layout->getElementAt(relativePos);
1612 
1613             if (hoverLayoutElement != nullptr &&
1614                 hoverLayoutElement->getLink().isUrl() &&
1615                 this->isScrolling_ == false)
1616             {
1617                 break;
1618             }
1619             else
1620             {
1621                 if (this->isScrolling_)
1622                     this->disableScrolling();
1623                 else if (hoverLayoutElement != nullptr &&
1624                          hoverLayoutElement->getFlags().has(
1625                              MessageElementFlag::Username))
1626                     break;
1627                 else if (this->scrollBar_->isVisible())
1628                     this->enableScrolling(event->screenPos());
1629             }
1630         }
1631         break;
1632 
1633         default:;
1634     }
1635 
1636     this->update();
1637 }
1638 
mouseReleaseEvent(QMouseEvent * event)1639 void ChannelView::mouseReleaseEvent(QMouseEvent *event)
1640 {
1641     // find message
1642     this->queueLayout();
1643 
1644     std::shared_ptr<MessageLayout> layout;
1645     QPoint relativePos;
1646     int messageIndex;
1647 
1648     bool foundElement =
1649         tryGetMessageAt(event->pos(), layout, relativePos, messageIndex);
1650 
1651     // check if mouse was pressed
1652     if (event->button() == Qt::LeftButton)
1653     {
1654         this->doubleClickSelection_.selectingLeft =
1655             this->doubleClickSelection_.selectingRight = false;
1656         if (this->isDoubleClick_)
1657         {
1658             this->isDoubleClick_ = false;
1659             // Was actually not a wanted triple-click
1660             if (fabsf(distanceBetweenPoints(this->lastDClickPosition_,
1661                                             event->screenPos())) > 10.f)
1662             {
1663                 this->clickTimer_->stop();
1664                 return;
1665             }
1666         }
1667         else if (this->isLeftMouseDown_)
1668         {
1669             this->isLeftMouseDown_ = false;
1670 
1671             if (fabsf(distanceBetweenPoints(this->lastLeftPressPosition_,
1672                                             event->screenPos())) > 15.f)
1673             {
1674                 return;
1675             }
1676         }
1677         else
1678         {
1679             return;
1680         }
1681     }
1682     else if (event->button() == Qt::RightButton)
1683     {
1684         if (this->isRightMouseDown_)
1685         {
1686             this->isRightMouseDown_ = false;
1687 
1688             if (fabsf(distanceBetweenPoints(this->lastRightPressPosition_,
1689                                             event->screenPos())) > 15.f)
1690             {
1691                 return;
1692             }
1693         }
1694         else
1695         {
1696             return;
1697         }
1698     }
1699     else if (event->button() == Qt::MiddleButton)
1700     {
1701         if (this->isScrolling_ && this->scrollBar_->isVisible())
1702         {
1703             if (event->screenPos() == this->lastMiddlePressPosition_)
1704                 this->enableScrolling(event->screenPos());
1705             else
1706                 this->disableScrolling();
1707 
1708             return;
1709         }
1710         else if (foundElement)
1711         {
1712             const MessageLayoutElement *hoverLayoutElement =
1713                 layout->getElementAt(relativePos);
1714 
1715             if (hoverLayoutElement == nullptr)
1716             {
1717                 return;
1718             }
1719             else if (hoverLayoutElement->getFlags().has(
1720                          MessageElementFlag::Username))
1721             {
1722                 openTwitchUsercard(this->channel_->getName(),
1723                                    hoverLayoutElement->getLink().value);
1724                 return;
1725             }
1726             else if (hoverLayoutElement->getLink().isUrl() == false)
1727             {
1728                 return;
1729             }
1730         }
1731     }
1732     else
1733     {
1734         // not left or right button
1735         return;
1736     }
1737 
1738     // no message found
1739     if (!foundElement)
1740     {
1741         // No message at clicked position
1742         return;
1743     }
1744 
1745     // message under cursor is collapsed
1746     if (layout->flags.has(MessageLayoutFlag::Collapsed))
1747     {
1748         layout->flags.set(MessageLayoutFlag::Expanded);
1749         layout->flags.set(MessageLayoutFlag::RequiresLayout);
1750 
1751         this->queueLayout();
1752         return;
1753     }
1754 
1755     const MessageLayoutElement *hoverLayoutElement =
1756         layout->getElementAt(relativePos);
1757     // Triple-clicking a message selects the whole message
1758     if (this->clickTimer_->isActive() && this->selecting_)
1759     {
1760         if (fabsf(distanceBetweenPoints(this->lastDClickPosition_,
1761                                         event->screenPos())) < 10.f)
1762         {
1763             this->selectWholeMessage(layout.get(), messageIndex);
1764         }
1765     }
1766 
1767     if (hoverLayoutElement == nullptr)
1768     {
1769         return;
1770     }
1771 
1772     // handle the click
1773     this->handleMouseClick(event, hoverLayoutElement, layout);
1774 
1775     this->update();
1776 }
1777 
handleMouseClick(QMouseEvent * event,const MessageLayoutElement * hoveredElement,MessageLayoutPtr layout)1778 void ChannelView::handleMouseClick(QMouseEvent *event,
1779                                    const MessageLayoutElement *hoveredElement,
1780                                    MessageLayoutPtr layout)
1781 {
1782     switch (event->button())
1783     {
1784         case Qt::LeftButton: {
1785             if (this->selecting_)
1786             {
1787                 // this->pausedBySelection = false;
1788                 this->selecting_ = false;
1789                 // this->pauseTimeout.stop();
1790                 // this->pausedTemporarily = false;
1791 
1792                 this->queueLayout();
1793             }
1794 
1795             auto &link = hoveredElement->getLink();
1796             if (!getSettings()->linksDoubleClickOnly)
1797             {
1798                 this->handleLinkClick(event, link, layout.get());
1799             }
1800 
1801             // Invoke to signal from EmotePopup.
1802             if (link.type == Link::InsertText)
1803             {
1804                 this->linkClicked.invoke(link);
1805             }
1806         }
1807         break;
1808         case Qt::RightButton: {
1809             auto split = dynamic_cast<Split *>(this->parentWidget());
1810             auto insertText = [=](QString text) {
1811                 if (split)
1812                 {
1813                     split->insertTextToInput(text);
1814                 }
1815             };
1816 
1817             auto &link = hoveredElement->getLink();
1818             if (link.type == Link::UserInfo)
1819             {
1820                 const bool commaMention = getSettings()->mentionUsersWithComma;
1821                 const bool isFirstWord =
1822                     split && split->getInput().isEditFirstWord();
1823                 auto userMention =
1824                     formatUserMention(link.value, isFirstWord, commaMention);
1825                 insertText("@" + userMention + " ");
1826             }
1827             else if (link.type == Link::UserWhisper)
1828             {
1829                 insertText("/w " + link.value + " ");
1830             }
1831             else
1832             {
1833                 this->addContextMenuItems(hoveredElement, layout);
1834             }
1835         }
1836         break;
1837         case Qt::MiddleButton: {
1838             auto &link = hoveredElement->getLink();
1839             if (!getSettings()->linksDoubleClickOnly)
1840             {
1841                 this->handleLinkClick(event, link, layout.get());
1842             }
1843         }
1844         break;
1845         default:;
1846     }
1847 }
1848 
addContextMenuItems(const MessageLayoutElement * hoveredElement,MessageLayoutPtr layout)1849 void ChannelView::addContextMenuItems(
1850     const MessageLayoutElement *hoveredElement, MessageLayoutPtr layout)
1851 {
1852     const auto &creator = hoveredElement->getCreator();
1853     auto creatorFlags = creator.getFlags();
1854 
1855     static QMenu *previousMenu = nullptr;
1856     if (previousMenu != nullptr)
1857     {
1858         previousMenu->deleteLater();
1859         previousMenu = nullptr;
1860     }
1861 
1862     auto menu = new QMenu;
1863     previousMenu = menu;
1864 
1865     if (creatorFlags.hasAny({MessageElementFlag::Badges}))
1866     {
1867         auto badgeElement = dynamic_cast<const BadgeElement *>(&creator);
1868         addEmoteContextMenuItems(*badgeElement->getEmote(), creatorFlags,
1869                                  *menu);
1870     }
1871 
1872     // Emote actions
1873     if (creatorFlags.hasAny(
1874             {MessageElementFlag::EmoteImages, MessageElementFlag::EmojiImage}))
1875     {
1876         const auto emoteElement = dynamic_cast<const EmoteElement *>(&creator);
1877         if (emoteElement)
1878             addEmoteContextMenuItems(*emoteElement->getEmote(), creatorFlags,
1879                                      *menu);
1880     }
1881 
1882     // add seperator
1883     if (!menu->actions().empty())
1884     {
1885         menu->addSeparator();
1886     }
1887 
1888     // Link copy
1889     if (hoveredElement->getLink().type == Link::Url)
1890     {
1891         QString url = hoveredElement->getLink().value;
1892 
1893         // open link
1894         menu->addAction("Open link", [url] {
1895             QDesktopServices::openUrl(QUrl(url));
1896         });
1897         // open link default
1898         if (supportsIncognitoLinks())
1899         {
1900             menu->addAction("Open link incognito", [url] {
1901                 openLinkIncognito(url);
1902             });
1903         }
1904         menu->addAction("Copy link", [url] {
1905             crossPlatformCopy(url);
1906         });
1907 
1908         menu->addSeparator();
1909     }
1910 
1911     // Copy actions
1912     if (!this->selection_.isEmpty())
1913     {
1914         menu->addAction("Copy selection", [this] {
1915             crossPlatformCopy(this->getSelectedText());
1916         });
1917     }
1918 
1919     menu->addAction("Copy message", [layout] {
1920         QString copyString;
1921         layout->addSelectionText(copyString, 0, INT_MAX,
1922                                  CopyMode::OnlyTextAndEmotes);
1923 
1924         crossPlatformCopy(copyString);
1925     });
1926 
1927     menu->addAction("Copy full message", [layout] {
1928         QString copyString;
1929         layout->addSelectionText(copyString);
1930 
1931         crossPlatformCopy(copyString);
1932     });
1933 
1934     // If is a link to a twitch user/stream
1935     if (hoveredElement->getLink().type == Link::Url)
1936     {
1937         static QRegularExpression twitchChannelRegex(
1938             R"(^(?:https?:\/\/)?(?:www\.|go\.)?twitch\.tv\/(?<username>[a-z0-9_]{3,}))",
1939             QRegularExpression::CaseInsensitiveOption);
1940         static QSet<QString> ignoredUsernames{
1941             "videos",    "settings", "directory",     "jobs",     "friends",
1942             "inventory", "payments", "subscriptions", "messages",
1943         };
1944 
1945         auto twitchMatch =
1946             twitchChannelRegex.match(hoveredElement->getLink().value);
1947         auto twitchUsername = twitchMatch.captured("username");
1948         if (!twitchUsername.isEmpty() &&
1949             !ignoredUsernames.contains(twitchUsername))
1950         {
1951             menu->addSeparator();
1952             menu->addAction("Open in new split", [twitchUsername, this] {
1953                 this->openChannelIn.invoke(twitchUsername,
1954                                            FromTwitchLinkOpenChannelIn::Split);
1955             });
1956             menu->addAction("Open in new tab", [twitchUsername, this] {
1957                 this->openChannelIn.invoke(twitchUsername,
1958                                            FromTwitchLinkOpenChannelIn::Tab);
1959             });
1960 
1961             menu->addSeparator();
1962             menu->addAction("Open player in browser", [twitchUsername, this] {
1963                 this->openChannelIn.invoke(
1964                     twitchUsername, FromTwitchLinkOpenChannelIn::BrowserPlayer);
1965             });
1966             menu->addAction("Open in streamlink", [twitchUsername, this] {
1967                 this->openChannelIn.invoke(
1968                     twitchUsername, FromTwitchLinkOpenChannelIn::Streamlink);
1969             });
1970         }
1971     }
1972 
1973     menu->popup(QCursor::pos());
1974     menu->raise();
1975 
1976     return;
1977 }
1978 
mouseDoubleClickEvent(QMouseEvent * event)1979 void ChannelView::mouseDoubleClickEvent(QMouseEvent *event)
1980 {
1981     std::shared_ptr<MessageLayout> layout;
1982     QPoint relativePos;
1983     int messageIndex;
1984 
1985     if (!tryGetMessageAt(event->pos(), layout, relativePos, messageIndex))
1986     {
1987         return;
1988     }
1989 
1990     // message under cursor is collapsed
1991     if (layout->flags.has(MessageLayoutFlag::Collapsed))
1992     {
1993         return;
1994     }
1995 
1996     const MessageLayoutElement *hoverLayoutElement =
1997         layout->getElementAt(relativePos);
1998     this->lastDClickPosition_ = event->screenPos();
1999 
2000     if (hoverLayoutElement == nullptr)
2001     {
2002         // Possibility for triple click which doesn't have to be over an
2003         // existing layout element
2004         this->clickTimer_->start();
2005         return;
2006     }
2007 
2008     if (!this->isLeftMouseDown_)
2009     {
2010         this->isDoubleClick_ = true;
2011 
2012         int wordStart;
2013         int wordEnd;
2014         this->getWordBounds(layout.get(), hoverLayoutElement, relativePos,
2015                             wordStart, wordEnd);
2016 
2017         this->clickTimer_->start();
2018 
2019         SelectionItem wordMin(messageIndex, wordStart);
2020         SelectionItem wordMax(messageIndex, wordEnd);
2021 
2022         this->doubleClickSelection_.originalStart = wordStart;
2023         this->doubleClickSelection_.originalEnd = wordEnd;
2024         this->doubleClickSelection_.origMessageIndex = messageIndex;
2025         this->doubleClickSelection_.origStartItem = wordMin;
2026         this->doubleClickSelection_.origEndItem = wordMax;
2027 
2028         this->setSelection(wordMin, wordMax);
2029     }
2030 
2031     if (getSettings()->linksDoubleClickOnly)
2032     {
2033         auto &link = hoverLayoutElement->getLink();
2034         this->handleLinkClick(event, link, layout.get());
2035     }
2036 }
2037 
hideEvent(QHideEvent *)2038 void ChannelView::hideEvent(QHideEvent *)
2039 {
2040     for (auto &layout : this->messagesOnScreen_)
2041     {
2042         layout->deleteBuffer();
2043     }
2044 
2045     this->messagesOnScreen_.clear();
2046 }
2047 
showUserInfoPopup(const QString & userName)2048 void ChannelView::showUserInfoPopup(const QString &userName)
2049 {
2050     QWidget *userCardParent = this;
2051 #ifdef Q_OS_MACOS
2052     // Order of closing/opening/killing widgets when the "Automatically close user info popups" setting is enabled is special on macOS, so user info popups should always use the main window as its parent
2053     userCardParent =
2054         static_cast<QWidget *>(&(getApp()->windows->getMainWindow()));
2055 #endif
2056     auto *userPopup =
2057         new UserInfoPopup(getSettings()->autoCloseUserPopup, userCardParent);
2058     userPopup->setData(userName, this->hasSourceChannel()
2059                                      ? this->sourceChannel_
2060                                      : this->underlyingChannel_);
2061     QPoint offset(int(150 * this->scale()), int(70 * this->scale()));
2062     userPopup->move(QCursor::pos() - offset);
2063     userPopup->show();
2064 }
2065 
handleLinkClick(QMouseEvent * event,const Link & link,MessageLayout * layout)2066 void ChannelView::handleLinkClick(QMouseEvent *event, const Link &link,
2067                                   MessageLayout *layout)
2068 {
2069     if (event->button() != Qt::LeftButton &&
2070         event->button() != Qt::MiddleButton)
2071     {
2072         return;
2073     }
2074 
2075     switch (link.type)
2076     {
2077         case Link::UserWhisper:
2078         case Link::UserInfo: {
2079             auto user = link.value;
2080             this->showUserInfoPopup(user);
2081         }
2082         break;
2083 
2084         case Link::Url: {
2085             if (getSettings()->openLinksIncognito && supportsIncognitoLinks())
2086                 openLinkIncognito(link.value);
2087             else
2088                 QDesktopServices::openUrl(QUrl(link.value));
2089         }
2090         break;
2091 
2092         case Link::UserAction: {
2093             QString value = link.value;
2094 
2095             ChannelPtr channel = this->underlyingChannel_;
2096             SearchPopup *searchPopup =
2097                 dynamic_cast<SearchPopup *>(this->parentWidget());
2098             if (searchPopup != nullptr)
2099             {
2100                 Split *split =
2101                     dynamic_cast<Split *>(searchPopup->parentWidget());
2102                 if (split != nullptr)
2103                 {
2104                     channel = split->getChannel();
2105                 }
2106             }
2107 
2108             value.replace("{user}", layout->getMessage()->loginName)
2109                 .replace("{channel}", this->channel_->getName())
2110                 .replace("{msg-id}", layout->getMessage()->id)
2111                 .replace("{message}", layout->getMessage()->messageText);
2112 
2113             value = getApp()->commands->execCommand(value, channel, false);
2114             channel->sendMessage(value);
2115         }
2116         break;
2117 
2118         case Link::AutoModAllow: {
2119             getApp()->accounts->twitch.getCurrent()->autoModAllow(
2120                 link.value, this->channel());
2121         }
2122         break;
2123 
2124         case Link::AutoModDeny: {
2125             getApp()->accounts->twitch.getCurrent()->autoModDeny(
2126                 link.value, this->channel());
2127         }
2128         break;
2129 
2130         case Link::OpenAccountsPage: {
2131             SettingsDialog::showDialog(this,
2132                                        SettingsDialogPreference::Accounts);
2133         }
2134         break;
2135         case Link::JumpToChannel: {
2136             // Get all currently open pages
2137             QList<SplitContainer *> openPages;
2138 
2139             auto &nb = getApp()->windows->getMainWindow().getNotebook();
2140             for (int i = 0; i < nb.getPageCount(); ++i)
2141             {
2142                 openPages.push_back(
2143                     static_cast<SplitContainer *>(nb.getPageAt(i)));
2144             }
2145 
2146             for (auto *page : openPages)
2147             {
2148                 auto splits = page->getSplits();
2149 
2150                 // Search for channel matching link in page/split container
2151                 // TODO(zneix): Consider opening a channel if it's closed (?)
2152                 auto it = std::find_if(
2153                     splits.begin(), splits.end(), [link](Split *split) {
2154                         return split->getChannel()->getName() == link.value;
2155                     });
2156 
2157                 if (it != splits.end())
2158                 {
2159                     // Select SplitContainer and Split itself where mention message was sent
2160                     // TODO(zneix): Try exploring ways of scrolling to a certain message as well
2161                     nb.select(page);
2162 
2163                     Split *split = *it;
2164                     page->setSelected(split);
2165                     break;
2166                 }
2167             }
2168         }
2169         break;
2170         case Link::CopyToClipboard: {
2171             crossPlatformCopy(link.value);
2172         }
2173         break;
2174         case Link::Reconnect: {
2175             this->underlyingChannel_.get()->reconnect();
2176         }
2177         break;
2178 
2179         default:;
2180     }
2181 }
2182 
tryGetMessageAt(QPoint p,std::shared_ptr<MessageLayout> & _message,QPoint & relativePos,int & index)2183 bool ChannelView::tryGetMessageAt(QPoint p,
2184                                   std::shared_ptr<MessageLayout> &_message,
2185                                   QPoint &relativePos, int &index)
2186 {
2187     auto messagesSnapshot = this->getMessagesSnapshot();
2188 
2189     size_t start = this->scrollBar_->getCurrentValue();
2190 
2191     if (start >= messagesSnapshot.size())
2192     {
2193         return false;
2194     }
2195 
2196     int y = -(messagesSnapshot[start]->getHeight() *
2197               (fmod(this->scrollBar_->getCurrentValue(), 1)));
2198 
2199     for (size_t i = start; i < messagesSnapshot.size(); ++i)
2200     {
2201         auto message = messagesSnapshot[i];
2202 
2203         if (p.y() < y + message->getHeight())
2204         {
2205             relativePos = QPoint(p.x(), p.y() - y);
2206             _message = message;
2207             index = i;
2208             return true;
2209         }
2210 
2211         y += message->getHeight();
2212     }
2213 
2214     return false;
2215 }
2216 
getLayoutWidth() const2217 int ChannelView::getLayoutWidth() const
2218 {
2219     if (this->scrollBar_->isVisible())
2220         return int(this->width() - scrollbarPadding * this->scale());
2221 
2222     return this->width();
2223 }
2224 
selectWholeMessage(MessageLayout * layout,int & messageIndex)2225 void ChannelView::selectWholeMessage(MessageLayout *layout, int &messageIndex)
2226 {
2227     SelectionItem msgStart(messageIndex,
2228                            layout->getFirstMessageCharacterIndex());
2229     SelectionItem msgEnd(messageIndex, layout->getLastCharacterIndex());
2230     this->setSelection(msgStart, msgEnd);
2231 }
2232 
getWordBounds(MessageLayout * layout,const MessageLayoutElement * element,const QPoint & relativePos,int & wordStart,int & wordEnd)2233 void ChannelView::getWordBounds(MessageLayout *layout,
2234                                 const MessageLayoutElement *element,
2235                                 const QPoint &relativePos, int &wordStart,
2236                                 int &wordEnd)
2237 {
2238     const int mouseInWordIndex = element->getMouseOverIndex(relativePos);
2239     wordStart = layout->getSelectionIndex(relativePos) - mouseInWordIndex;
2240     const int selectionLength = element->getSelectionIndexCount();
2241     const int length =
2242         element->hasTrailingSpace() ? selectionLength - 1 : selectionLength;
2243     wordEnd = wordStart + length;
2244 }
2245 
enableScrolling(const QPointF & scrollStart)2246 void ChannelView::enableScrolling(const QPointF &scrollStart)
2247 {
2248     this->isScrolling_ = true;
2249     this->lastMiddlePressPosition_ = scrollStart;
2250     // The line below prevents a sudden jerk at the beginning
2251     this->currentMousePosition_ = scrollStart;
2252 
2253     this->scrollTimer_.start();
2254 
2255     if (!QGuiApplication::overrideCursor())
2256         QGuiApplication::setOverrideCursor(this->cursors_.neutral);
2257 }
2258 
disableScrolling()2259 void ChannelView::disableScrolling()
2260 {
2261     this->isScrolling_ = false;
2262     this->scrollTimer_.stop();
2263     QGuiApplication::restoreOverrideCursor();
2264 }
2265 
scrollUpdateRequested()2266 void ChannelView::scrollUpdateRequested()
2267 {
2268     const qreal dpi = this->devicePixelRatioF();
2269     const qreal delta = dpi * (this->currentMousePosition_.y() -
2270                                this->lastMiddlePressPosition_.y());
2271     const int cursorHeight = this->cursors_.neutral.pixmap().height();
2272 
2273     if (fabs(delta) <= cursorHeight * dpi)
2274     {
2275         /*
2276          * If within an area close to the initial position, don't do any
2277          * scrolling at all.
2278          */
2279         QGuiApplication::changeOverrideCursor(this->cursors_.neutral);
2280         return;
2281     }
2282 
2283     qreal offset;
2284     if (delta > 0)
2285     {
2286         QGuiApplication::changeOverrideCursor(this->cursors_.down);
2287         offset = delta - cursorHeight;
2288     }
2289     else
2290     {
2291         QGuiApplication::changeOverrideCursor(this->cursors_.up);
2292         offset = delta + cursorHeight;
2293     }
2294 
2295     // "Good" feeling multiplier found by trial-and-error
2296     const qreal multiplier = qreal(0.02);
2297     this->scrollBar_->offset(multiplier * offset);
2298 }
2299 
2300 }  // namespace chatterino
2301