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