#pragma once #include "common/FlagsEnum.hpp" #include "messages/layouts/MessageLayoutContext.hpp" #include "messages/LimitedQueue.hpp" #include "messages/LimitedQueueSnapshot.hpp" #include "messages/Selection.hpp" #include "util/ThreadGuard.hpp" #include "widgets/BaseWidget.hpp" #include "widgets/TooltipWidget.hpp" #include #include #include #include #include #include #include #include #include #include namespace chatterino { enum class HighlightState; class Channel; using ChannelPtr = std::shared_ptr; struct Message; using MessagePtr = std::shared_ptr; enum class MessageFlag : int64_t; using MessageFlags = FlagsEnum; class MessageLayout; using MessageLayoutPtr = std::shared_ptr; enum class MessageElementFlag : int64_t; using MessageElementFlags = FlagsEnum; class Scrollbar; class EffectLabel; struct Link; class MessageLayoutElement; class Split; class FilterSet; using FilterSetPtr = std::shared_ptr; enum class PauseReason { Mouse, Selection, DoubleClick, KeyboardModifier, }; enum class FromTwitchLinkOpenChannelIn { Split, Tab, BrowserPlayer, Streamlink, }; using SteadyClock = std::chrono::steady_clock; class ChannelView final : public BaseWidget { Q_OBJECT public: enum class Context { None, UserCard, ReplyThread, Search, }; /// Creates a channel view without a split. /// In such a view, usercards and reply-threads can't be opened. /// /// @param parent The parent of this widget. Can be `nullptr`. /// @param context The context in which this view is shown (e.g. as a usercard). /// @param messagesLimit The maximum amount of messages this view will display. explicit ChannelView(QWidget *parent, Context context = Context::None, size_t messagesLimit = 1000); /// Creates a channel view in a split. /// /// @param parent The parent of this widget. /// @param split The split containing this widget. /// @a split must be in the widget tree of @a parent. /// @param context The context in which this view is shown (e.g. as a usercard). /// @param messagesLimit The maximum amount of messages this view will display. explicit ChannelView(QWidget *parent, Split *split, Context context = Context::None, size_t messagesLimit = 1000); void queueUpdate(); void queueUpdate(const QRect &area); Scrollbar &getScrollBar(); QString getSelectedText(); bool hasSelection(); void clearSelection(); /** * Copies the currently selected text to the users clipboard. * * @see ::getSelectedText() */ void copySelectedText(); void setEnableScrollingToBottom(bool); bool getEnableScrollingToBottom() const; void setOverrideFlags(std::optional value); const std::optional &getOverrideFlags() const; void updateLastReadMessage(); /** * Attempts to scroll to a message in this channel. * @return true if the message was found and highlighted. */ bool scrollToMessage(const MessagePtr &message); /** * Attempts to scroll to a message id in this channel. * @return true if the message was found and highlighted. */ bool scrollToMessageId(const QString &id); /// Pausing bool pausable() const; void setPausable(bool value); bool paused() const; void pause(PauseReason reason, std::optional msecs = std::nullopt); void unpause(PauseReason reason); MessageElementFlags getFlags() const; ChannelPtr channel(); void setChannel(const ChannelPtr &underlyingChannel); void setFilters(const QList &ids); QList getFilterIds() const; FilterSetPtr getFilterSet() const; ChannelPtr sourceChannel() const; void setSourceChannel(ChannelPtr sourceChannel); bool hasSourceChannel() const; LimitedQueueSnapshot &getMessagesSnapshot(); void queueLayout(); void invalidateBuffers(); void clearMessages(); Context getContext() const; /** * @brief Creates and shows a UserInfoPopup dialog * * @param userName The login name of the user * @param alternativePopoutChannel Optional parameter containing the channel name to use for context **/ void showUserInfoPopup(const QString &userName, QString alternativePopoutChannel = QString()); /** * @brief This method is meant to be used when filtering out channels. * It must return true if a message belongs in this channel. * It might return true if a message doesn't belong in this channel. */ bool mayContainMessage(const MessagePtr &message); pajlada::Signals::Signal mouseDown; pajlada::Signals::NoArgSignal selectionChanged; pajlada::Signals::Signal tabHighlightRequested; pajlada::Signals::NoArgSignal liveStatusChanged; pajlada::Signals::Signal linkClicked; pajlada::Signals::Signal openChannelIn; protected: void themeChangedEvent() override; void scaleChangedEvent(float scale) override; void resizeEvent(QResizeEvent * /*event*/) override; void paintEvent(QPaintEvent * /*event*/) override; void wheelEvent(QWheelEvent *event) override; #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) void enterEvent(QEnterEvent * /*event*/) override; #else void enterEvent(QEvent * /*event*/) override; #endif void leaveEvent(QEvent * /*event*/) override; void mouseMoveEvent(QMouseEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void mouseDoubleClickEvent(QMouseEvent *event) override; void hideEvent(QHideEvent * /*event*/) override; void showEvent(QShowEvent *event) override; void handleLinkClick(QMouseEvent *event, const Link &link, MessageLayout *layout); bool tryGetMessageAt(QPoint p, std::shared_ptr &message, QPoint &relativePos, int &index); private: struct InternalCtor { }; ChannelView(InternalCtor tag, QWidget *parent, Split *split, Context context, size_t messagesLimit); void initializeLayout(); void initializeScrollbar(); void initializeSignals(); void messageAppended(MessagePtr &message, std::optional overridingFlags); void messageAddedAtStart(std::vector &messages); void messageRemoveFromStart(MessagePtr &message); void messageReplaced(size_t index, MessagePtr &replacement); void messagesUpdated(); void performLayout(bool causedByScrollbar = false, bool causedByShow = false); void layoutVisibleMessages( const LimitedQueueSnapshot &messages); void updateScrollbar(const LimitedQueueSnapshot &messages, bool causedByScrollbar, bool causedByShow); void drawMessages(QPainter &painter, const QRect &area); void setSelection(const SelectionItem &start, const SelectionItem &end); void setSelection(const Selection &newSelection); void selectWholeMessage(MessageLayout *layout, int &messageIndex); void handleMouseClick(QMouseEvent *event, const MessageLayoutElement *hoveredElement, MessageLayoutPtr layout); void addContextMenuItems(const MessageLayoutElement *hoveredElement, MessageLayoutPtr layout, QMouseEvent *event); void addMessageContextMenuItems(QMenu *menu, const MessageLayoutPtr &layout); void addTwitchLinkContextMenuItems( QMenu *menu, const MessageLayoutElement *hoveredElement); void addCommandExecutionContextMenuItems(QMenu *menu, const MessageLayoutPtr &layout); int getLayoutWidth() const; void updatePauses(); void unpaused(); void enableScrolling(const QPointF &scrollStart); void disableScrolling(); /** * Scrolls to a message layout that must be from this view. * * @param layout Must be from this channel. * @param messageIdx Must be an index into this channel. */ void scrollToMessageLayout(MessageLayout *layout, size_t messageIdx); void setInputReply(const MessagePtr &message); void showReplyThreadPopup(const MessagePtr &message); bool canReplyToMessages() const; bool layoutQueued_ = false; bool bufferInvalidationQueued_ = false; bool lastMessageHasAlternateBackground_ = false; bool lastMessageHasAlternateBackgroundReverse_ = true; /// Tracks the area of animated elements in the last full repaint. /// If this is empty (QRect::isEmpty()), no animated element is shown. QRect animationArea_; bool pausable_ = false; QTimer pauseTimer_; std::unordered_map> pauses_; std::optional pauseEnd_; int pauseScrollMinimumOffset_ = 0; int pauseScrollMaximumOffset_ = 0; // Keeps track how many message indices we need to offset the selection when we resume scrolling uint32_t pauseSelectionOffset_ = 0; std::optional overrideFlags_; MessageLayoutPtr lastReadMessage_; ThreadGuard snapshotGuard_; LimitedQueueSnapshot snapshot_; ChannelPtr channel_ = nullptr; ChannelPtr underlyingChannel_ = nullptr; ChannelPtr sourceChannel_ = nullptr; Split *split_; Scrollbar *scrollBar_; EffectLabel *goToBottom_{}; bool showScrollBar_ = false; FilterSetPtr channelFilters_; // Returns true if message should be included bool shouldIncludeMessage(const MessagePtr &m) const; // Returns whether the scrollbar should have highlights bool showScrollbarHighlights() const; // This variable can be used to decide whether or not we should render the // "Show latest messages" button bool showingLatestMessages_ = true; bool enableScrollingToBottom_ = true; bool onlyUpdateEmotes_ = false; // Mouse event variables bool isLeftMouseDown_ = false; bool isRightMouseDown_ = false; bool isDoubleClick_ = false; QPointF lastLeftPressPosition_; QPointF lastRightPressPosition_; QPointF lastDoubleClickPosition_; QTimer clickTimer_; bool isScrolling_ = false; QPointF lastMiddlePressPosition_; QPointF currentMousePosition_; QTimer scrollTimer_; // We're only interested in the pointer, not the contents MessageLayout *highlightedMessage_ = nullptr; QVariantAnimation highlightAnimation_; void setupHighlightAnimationColors(); struct { QCursor neutral; QCursor up; QCursor down; } cursors_; Selection selection_; Selection doubleClickSelection_; const Context context_; LimitedQueue messages_; pajlada::Signals::SignalHolder signalHolder_; // channelConnections_ will be cleared when the underlying channel of the channelview changes pajlada::Signals::SignalHolder channelConnections_; std::unordered_set> messagesOnScreen_; MessageColors messageColors_; MessagePreferences messagePreferences_; void scrollUpdateRequested(); TooltipWidget *const tooltipWidget_{}; }; } // namespace chatterino