Added support for Twitch's Chat Replies (#3722)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
#include "DraggablePopup.hpp"
|
||||
|
||||
#include <QMouseEvent>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
namespace {
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
FlagsEnum<BaseWindow::Flags> popupFlags{BaseWindow::Dialog,
|
||||
BaseWindow::EnableCustomFrame};
|
||||
FlagsEnum<BaseWindow::Flags> popupFlagsCloseAutomatically{
|
||||
BaseWindow::EnableCustomFrame};
|
||||
#else
|
||||
FlagsEnum<BaseWindow::Flags> popupFlags{BaseWindow::EnableCustomFrame};
|
||||
FlagsEnum<BaseWindow::Flags> popupFlagsCloseAutomatically{
|
||||
BaseWindow::EnableCustomFrame, BaseWindow::Frameless,
|
||||
BaseWindow::FramelessDraggable};
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
DraggablePopup::DraggablePopup(bool closeAutomatically, QWidget *parent)
|
||||
: BaseWindow(closeAutomatically ? popupFlagsCloseAutomatically : popupFlags,
|
||||
parent)
|
||||
, lifetimeHack_(std::make_shared<bool>(false))
|
||||
, dragTimer_(this)
|
||||
|
||||
{
|
||||
if (closeAutomatically)
|
||||
{
|
||||
this->setActionOnFocusLoss(BaseWindow::Delete);
|
||||
}
|
||||
else
|
||||
{
|
||||
this->setAttribute(Qt::WA_DeleteOnClose);
|
||||
}
|
||||
|
||||
// Update the window position according to this->requestedDragPos_ on every trigger
|
||||
this->dragTimer_.callOnTimeout(
|
||||
[this, hack = std::weak_ptr<bool>(this->lifetimeHack_)] {
|
||||
if (!hack.lock())
|
||||
{
|
||||
// Ensure this timer is never called after the object has been destroyed
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this->isMoving_)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this->move(this->requestedDragPos_);
|
||||
});
|
||||
}
|
||||
|
||||
void DraggablePopup::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::MouseButton::LeftButton)
|
||||
{
|
||||
this->dragTimer_.start(std::chrono::milliseconds(17));
|
||||
this->startPosDrag_ = event->pos();
|
||||
this->movingRelativePos = event->localPos();
|
||||
}
|
||||
}
|
||||
|
||||
void DraggablePopup::mouseReleaseEvent(QMouseEvent *event)
|
||||
{
|
||||
this->dragTimer_.stop();
|
||||
this->isMoving_ = false;
|
||||
}
|
||||
|
||||
void DraggablePopup::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
// Drag the window by the amount changed from inital position
|
||||
// Note that we provide a few *units* of deadzone so people don't
|
||||
// start dragging the window if they are slow at clicking.
|
||||
|
||||
auto movePos = event->pos() - this->startPosDrag_;
|
||||
if (this->isMoving_ || movePos.manhattanLength() > 10.0)
|
||||
{
|
||||
this->requestedDragPos_ =
|
||||
(event->screenPos() - this->movingRelativePos).toPoint();
|
||||
this->isMoving_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "widgets/BaseWindow.hpp"
|
||||
|
||||
#include <QPoint>
|
||||
#include <QTimer>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class DraggablePopup : public BaseWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/// DraggablePopup implements the automatic dragging behavior when clicking
|
||||
/// anywhere in the window (that doesn't have some other widget).
|
||||
///
|
||||
/// If closeAutomatically is set, the window will close when losing focus,
|
||||
/// and the window will be frameless.
|
||||
DraggablePopup(bool closeAutomatically, QWidget *parent);
|
||||
|
||||
protected:
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
|
||||
// lifetimeHack_ is used to check that the window hasn't been destroyed yet
|
||||
std::shared_ptr<bool> lifetimeHack_;
|
||||
|
||||
private:
|
||||
// isMoving_ is set to true if the user is holding the left mouse button down and has moved the mouse a small amount away from the original click point (startPosDrag_)
|
||||
bool isMoving_ = false;
|
||||
|
||||
// startPosDrag_ is the coordinates where the user originally pressed the mouse button down to start dragging
|
||||
QPoint startPosDrag_;
|
||||
|
||||
// requestDragPos_ is the final screen coordinates where the widget should be moved to.
|
||||
// Takes the relative position of where the user originally clicked the widget into account
|
||||
QPoint requestedDragPos_;
|
||||
|
||||
// dragTimer_ is called ~60 times per second once the user has initiated dragging
|
||||
QTimer dragTimer_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,193 @@
|
||||
#include "ReplyThreadPopup.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/Channel.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
#include "controllers/hotkeys/HotkeyController.hpp"
|
||||
#include "messages/MessageThread.hpp"
|
||||
#include "util/LayoutCreator.hpp"
|
||||
#include "widgets/Scrollbar.hpp"
|
||||
#include "widgets/helper/ChannelView.hpp"
|
||||
#include "widgets/helper/ResizingTextEdit.hpp"
|
||||
#include "widgets/splits/Split.hpp"
|
||||
#include "widgets/splits/SplitInput.hpp"
|
||||
|
||||
const QString TEXT_TITLE("Reply Thread - @%1 in #%2");
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
ReplyThreadPopup::ReplyThreadPopup(bool closeAutomatically, QWidget *parent,
|
||||
Split *split)
|
||||
: DraggablePopup(closeAutomatically, parent)
|
||||
, split_(split)
|
||||
{
|
||||
this->setWindowTitle(QStringLiteral("Reply Thread"));
|
||||
this->setStayInScreenRect(true);
|
||||
|
||||
HotkeyController::HotkeyMap actions{
|
||||
{"delete",
|
||||
[this](std::vector<QString>) -> QString {
|
||||
this->deleteLater();
|
||||
return "";
|
||||
}},
|
||||
{"scrollPage",
|
||||
[this](std::vector<QString> arguments) -> QString {
|
||||
if (arguments.empty())
|
||||
{
|
||||
qCWarning(chatterinoHotkeys)
|
||||
<< "scrollPage hotkey called without arguments!";
|
||||
return "scrollPage hotkey called without arguments!";
|
||||
}
|
||||
auto direction = arguments.at(0);
|
||||
|
||||
auto &scrollbar = this->ui_.threadView->getScrollBar();
|
||||
if (direction == "up")
|
||||
{
|
||||
scrollbar.offset(-scrollbar.getLargeChange());
|
||||
}
|
||||
else if (direction == "down")
|
||||
{
|
||||
scrollbar.offset(scrollbar.getLargeChange());
|
||||
}
|
||||
else
|
||||
{
|
||||
qCWarning(chatterinoHotkeys) << "Unknown scroll direction";
|
||||
}
|
||||
return "";
|
||||
}},
|
||||
|
||||
// these actions make no sense in the context of a reply thread, so they aren't implemented
|
||||
{"execModeratorAction", nullptr},
|
||||
{"reject", nullptr},
|
||||
{"accept", nullptr},
|
||||
{"openTab", nullptr},
|
||||
{"search", nullptr},
|
||||
};
|
||||
|
||||
this->shortcuts_ = getApp()->hotkeys->shortcutsForCategory(
|
||||
HotkeyCategory::PopupWindow, actions, this);
|
||||
|
||||
auto layout = LayoutCreator<QWidget>(this->getLayoutContainer())
|
||||
.setLayoutType<QVBoxLayout>();
|
||||
|
||||
// initialize UI
|
||||
this->ui_.threadView =
|
||||
new ChannelView(this, this->split_, ChannelView::Context::ReplyThread);
|
||||
this->ui_.threadView->setMinimumSize(400, 100);
|
||||
this->ui_.threadView->setSizePolicy(QSizePolicy::Expanding,
|
||||
QSizePolicy::Expanding);
|
||||
this->ui_.threadView->mouseDown.connect([this](QMouseEvent *) {
|
||||
this->giveFocus(Qt::MouseFocusReason);
|
||||
});
|
||||
|
||||
// Create SplitInput with inline replying disabled
|
||||
this->ui_.replyInput = new SplitInput(this, this->split_, false);
|
||||
|
||||
this->bSignals_.emplace_back(
|
||||
getApp()->accounts->twitch.currentUserChanged.connect([this] {
|
||||
this->updateInputUI();
|
||||
}));
|
||||
|
||||
layout->setSpacing(0);
|
||||
// provide draggable margin if frameless
|
||||
layout->setMargin(closeAutomatically ? 15 : 1);
|
||||
layout->addWidget(this->ui_.threadView, 1);
|
||||
layout->addWidget(this->ui_.replyInput);
|
||||
}
|
||||
|
||||
void ReplyThreadPopup::setThread(std::shared_ptr<MessageThread> thread)
|
||||
{
|
||||
this->thread_ = std::move(thread);
|
||||
this->ui_.replyInput->setReply(this->thread_);
|
||||
this->addMessagesFromThread();
|
||||
this->updateInputUI();
|
||||
}
|
||||
|
||||
void ReplyThreadPopup::addMessagesFromThread()
|
||||
{
|
||||
this->ui_.threadView->clearMessages();
|
||||
if (!this->thread_)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &sourceChannel = this->split_->getChannel();
|
||||
this->setWindowTitle(TEXT_TITLE.arg(this->thread_->root()->loginName,
|
||||
sourceChannel->getName()));
|
||||
|
||||
ChannelPtr virtualChannel;
|
||||
if (sourceChannel->isTwitchChannel())
|
||||
{
|
||||
virtualChannel =
|
||||
std::make_shared<TwitchChannel>(sourceChannel->getName());
|
||||
}
|
||||
else
|
||||
{
|
||||
virtualChannel = std::make_shared<Channel>(sourceChannel->getName(),
|
||||
Channel::Type::None);
|
||||
}
|
||||
|
||||
this->ui_.threadView->setChannel(virtualChannel);
|
||||
this->ui_.threadView->setSourceChannel(sourceChannel);
|
||||
|
||||
virtualChannel->addMessage(this->thread_->root());
|
||||
for (const auto &msgRef : this->thread_->replies())
|
||||
{
|
||||
if (auto msg = msgRef.lock())
|
||||
{
|
||||
virtualChannel->addMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
this->messageConnection_ =
|
||||
std::make_unique<pajlada::Signals::ScopedConnection>(
|
||||
sourceChannel->messageAppended.connect(
|
||||
[this, virtualChannel](MessagePtr &message, auto) {
|
||||
if (message->replyThread == this->thread_)
|
||||
{
|
||||
// same reply thread, add message
|
||||
virtualChannel->addMessage(message);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
void ReplyThreadPopup::updateInputUI()
|
||||
{
|
||||
auto channel = this->split_->getChannel();
|
||||
// Bail out if not a twitch channel.
|
||||
// Special twitch channels will hide their reply input box.
|
||||
if (!channel || !channel->isTwitchChannel())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this->ui_.replyInput->setVisible(channel->isWritable());
|
||||
|
||||
auto user = getApp()->accounts->twitch.getCurrent();
|
||||
QString placeholderText;
|
||||
|
||||
if (user->isAnon())
|
||||
{
|
||||
placeholderText = QStringLiteral("Log in to send messages...");
|
||||
}
|
||||
else
|
||||
{
|
||||
placeholderText =
|
||||
QStringLiteral("Reply as %1...")
|
||||
.arg(getApp()->accounts->twitch.getCurrent()->getUserName());
|
||||
}
|
||||
|
||||
this->ui_.replyInput->setPlaceholderText(placeholderText);
|
||||
}
|
||||
|
||||
void ReplyThreadPopup::giveFocus(Qt::FocusReason reason)
|
||||
{
|
||||
this->ui_.replyInput->giveFocus(reason);
|
||||
}
|
||||
|
||||
void ReplyThreadPopup::focusInEvent(QFocusEvent *event)
|
||||
{
|
||||
this->giveFocus(event->reason());
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "ForwardDecl.hpp"
|
||||
#include "widgets/DraggablePopup.hpp"
|
||||
|
||||
#include <boost/signals2.hpp>
|
||||
#include <pajlada/signals/scoped-connection.hpp>
|
||||
#include <pajlada/signals/signal.hpp>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class MessageThread;
|
||||
class Split;
|
||||
class SplitInput;
|
||||
|
||||
class ReplyThreadPopup final : public DraggablePopup
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ReplyThreadPopup(bool closeAutomatically, QWidget *parent, Split *split);
|
||||
|
||||
void setThread(std::shared_ptr<MessageThread> thread);
|
||||
void giveFocus(Qt::FocusReason reason);
|
||||
|
||||
protected:
|
||||
void focusInEvent(QFocusEvent *event) override;
|
||||
|
||||
private:
|
||||
void addMessagesFromThread();
|
||||
void updateInputUI();
|
||||
|
||||
// The message reply thread
|
||||
std::shared_ptr<MessageThread> thread_;
|
||||
// The channel that the reply thread is in
|
||||
ChannelPtr channel_;
|
||||
Split *split_;
|
||||
|
||||
struct {
|
||||
ChannelView *threadView = nullptr;
|
||||
SplitInput *replyInput = nullptr;
|
||||
} ui_;
|
||||
|
||||
std::unique_ptr<pajlada::Signals::ScopedConnection> messageConnection_;
|
||||
std::vector<boost::signals2::scoped_connection> bSignals_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -91,8 +91,16 @@ namespace {
|
||||
LimitedQueueSnapshot<MessagePtr> snapshot =
|
||||
channel->getMessageSnapshot();
|
||||
|
||||
ChannelPtr channelPtr(
|
||||
new Channel(channel->getName(), Channel::Type::None));
|
||||
ChannelPtr channelPtr;
|
||||
if (channel->isTwitchChannel())
|
||||
{
|
||||
channelPtr = std::make_shared<TwitchChannel>(channel->getName());
|
||||
}
|
||||
else
|
||||
{
|
||||
channelPtr = std::make_shared<Channel>(channel->getName(),
|
||||
Channel::Type::None);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < snapshot.size(); i++)
|
||||
{
|
||||
@@ -118,33 +126,14 @@ namespace {
|
||||
|
||||
} // namespace
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
FlagsEnum<BaseWindow::Flags> userInfoPopupFlags{BaseWindow::Dialog,
|
||||
BaseWindow::EnableCustomFrame};
|
||||
FlagsEnum<BaseWindow::Flags> userInfoPopupFlagsCloseAutomatically{
|
||||
BaseWindow::EnableCustomFrame};
|
||||
#else
|
||||
FlagsEnum<BaseWindow::Flags> userInfoPopupFlags{BaseWindow::EnableCustomFrame};
|
||||
FlagsEnum<BaseWindow::Flags> userInfoPopupFlagsCloseAutomatically{
|
||||
BaseWindow::EnableCustomFrame, BaseWindow::Frameless,
|
||||
BaseWindow::FramelessDraggable};
|
||||
#endif
|
||||
|
||||
UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent)
|
||||
: BaseWindow(closeAutomatically ? userInfoPopupFlagsCloseAutomatically
|
||||
: userInfoPopupFlags,
|
||||
parent)
|
||||
, hack_(new bool)
|
||||
, dragTimer_(this)
|
||||
UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent,
|
||||
Split *split)
|
||||
: DraggablePopup(closeAutomatically, parent)
|
||||
, split_(split)
|
||||
{
|
||||
this->setWindowTitle("Usercard");
|
||||
this->setStayInScreenRect(true);
|
||||
|
||||
if (closeAutomatically)
|
||||
this->setActionOnFocusLoss(BaseWindow::Delete);
|
||||
else
|
||||
this->setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
HotkeyController::HotkeyMap actions{
|
||||
{"delete",
|
||||
[this](std::vector<QString>) -> QString {
|
||||
@@ -498,7 +487,8 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent)
|
||||
this->ui_.noMessagesLabel = new Label("No recent messages");
|
||||
this->ui_.noMessagesLabel->setVisible(false);
|
||||
|
||||
this->ui_.latestMessages = new ChannelView(this);
|
||||
this->ui_.latestMessages =
|
||||
new ChannelView(this, this->split_, ChannelView::Context::UserCard);
|
||||
this->ui_.latestMessages->setMinimumSize(400, 275);
|
||||
this->ui_.latestMessages->setSizePolicy(QSizePolicy::Expanding,
|
||||
QSizePolicy::Expanding);
|
||||
@@ -510,21 +500,6 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent)
|
||||
|
||||
this->installEvents();
|
||||
this->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Policy::Ignored);
|
||||
|
||||
this->dragTimer_.callOnTimeout(
|
||||
[this, hack = std::weak_ptr<bool>(this->hack_)] {
|
||||
if (!hack.lock())
|
||||
{
|
||||
// Ensure this timer is never called after the object has been destroyed
|
||||
return;
|
||||
}
|
||||
if (!this->isMoving_)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this->move(this->requestedDragPos_);
|
||||
});
|
||||
}
|
||||
|
||||
void UserInfoPopup::themeChangedEvent()
|
||||
@@ -550,36 +525,6 @@ void UserInfoPopup::scaleChangedEvent(float /*scale*/)
|
||||
});
|
||||
}
|
||||
|
||||
void UserInfoPopup::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::MouseButton::LeftButton)
|
||||
{
|
||||
this->dragTimer_.start(std::chrono::milliseconds(17));
|
||||
this->startPosDrag_ = event->pos();
|
||||
this->movingRelativePos = event->localPos();
|
||||
}
|
||||
}
|
||||
|
||||
void UserInfoPopup::mouseReleaseEvent(QMouseEvent *event)
|
||||
{
|
||||
this->dragTimer_.stop();
|
||||
this->isMoving_ = false;
|
||||
}
|
||||
|
||||
void UserInfoPopup::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
// Drag the window by the amount changed from inital position
|
||||
// Note that we provide a few *units* of deadzone so people don't
|
||||
// start dragging the window if they are slow at clicking.
|
||||
auto movePos = event->pos() - this->startPosDrag_;
|
||||
if (this->isMoving_ || movePos.manhattanLength() > 10.0)
|
||||
{
|
||||
this->requestedDragPos_ =
|
||||
(event->screenPos() - this->movingRelativePos).toPoint();
|
||||
this->isMoving_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void UserInfoPopup::installEvents()
|
||||
{
|
||||
std::shared_ptr<bool> ignoreNext = std::make_shared<bool>(false);
|
||||
@@ -765,7 +710,7 @@ void UserInfoPopup::updateLatestMessages()
|
||||
|
||||
void UserInfoPopup::updateUserData()
|
||||
{
|
||||
std::weak_ptr<bool> hack = this->hack_;
|
||||
std::weak_ptr<bool> hack = this->lifetimeHack_;
|
||||
auto currentUser = getApp()->accounts->twitch.getCurrent();
|
||||
|
||||
const auto onUserFetchFailed = [this, hack] {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "widgets/BaseWindow.hpp"
|
||||
#include "widgets/DraggablePopup.hpp"
|
||||
#include "widgets/helper/ChannelView.hpp"
|
||||
|
||||
#include <pajlada/signals/scoped-connection.hpp>
|
||||
@@ -16,12 +16,13 @@ class Channel;
|
||||
using ChannelPtr = std::shared_ptr<Channel>;
|
||||
class Label;
|
||||
|
||||
class UserInfoPopup final : public BaseWindow
|
||||
class UserInfoPopup final : public DraggablePopup
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
UserInfoPopup(bool closeAutomatically, QWidget *parent);
|
||||
UserInfoPopup(bool closeAutomatically, QWidget *parent,
|
||||
Split *split = nullptr);
|
||||
|
||||
void setData(const QString &name, const ChannelPtr &channel);
|
||||
void setData(const QString &name, const ChannelPtr &contextChannel,
|
||||
@@ -30,9 +31,6 @@ public:
|
||||
protected:
|
||||
virtual void themeChangedEvent() override;
|
||||
virtual void scaleChangedEvent(float scale) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
|
||||
private:
|
||||
void installEvents();
|
||||
@@ -43,6 +41,8 @@ private:
|
||||
bool isMod_;
|
||||
bool isBroadcaster_;
|
||||
|
||||
Split *split_;
|
||||
|
||||
QString userName_;
|
||||
QString userId_;
|
||||
QString avatarUrl_;
|
||||
@@ -51,25 +51,10 @@ private:
|
||||
// The channel the messages are rendered from (e.g. #forsen). Can be a special channel, but will try to not be where possible.
|
||||
ChannelPtr underlyingChannel_;
|
||||
|
||||
// isMoving_ is set to true if the user is holding the left mouse button down and has moved the mouse a small amount away from the original click point (startPosDrag_)
|
||||
bool isMoving_ = false;
|
||||
|
||||
// startPosDrag_ is the coordinates where the user originally pressed the mouse button down to start dragging
|
||||
QPoint startPosDrag_;
|
||||
|
||||
// requestDragPos_ is the final screen coordinates where the widget should be moved to.
|
||||
// Takes the relative position of where the user originally clicked the widget into account
|
||||
QPoint requestedDragPos_;
|
||||
|
||||
// dragTimer_ is called ~60 times per second once the user has initiated dragging
|
||||
QTimer dragTimer_;
|
||||
|
||||
pajlada::Signals::NoArgSignal userStateChanged_;
|
||||
|
||||
std::unique_ptr<pajlada::Signals::ScopedConnection> refreshConnection_;
|
||||
|
||||
std::shared_ptr<bool> hack_;
|
||||
|
||||
struct {
|
||||
Button *avatarButton = nullptr;
|
||||
Button *localizedNameCopyButton = nullptr;
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
#include "widgets/Scrollbar.hpp"
|
||||
#include "widgets/TooltipWidget.hpp"
|
||||
#include "widgets/Window.hpp"
|
||||
#include "widgets/dialogs/ReplyThreadPopup.hpp"
|
||||
#include "widgets/dialogs/SettingsDialog.hpp"
|
||||
#include "widgets/dialogs/UserInfoPopup.hpp"
|
||||
#include "widgets/helper/EffectLabel.hpp"
|
||||
@@ -119,9 +120,11 @@ namespace {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ChannelView::ChannelView(BaseWidget *parent)
|
||||
ChannelView::ChannelView(BaseWidget *parent, Split *split, Context context)
|
||||
: BaseWidget(parent)
|
||||
, scrollBar_(new Scrollbar(this))
|
||||
, split_(split)
|
||||
, context_(context)
|
||||
{
|
||||
this->setMouseTracking(true);
|
||||
|
||||
@@ -160,7 +163,7 @@ ChannelView::ChannelView(BaseWidget *parent)
|
||||
// and tabbing to it from another widget. I don't currently know
|
||||
// of any place where you can, or where it would make sense,
|
||||
// to tab to a ChannelVieChannelView
|
||||
this->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
|
||||
this->setFocusPolicy(Qt::FocusPolicy::ClickFocus);
|
||||
}
|
||||
|
||||
void ChannelView::initializeLayout()
|
||||
@@ -1019,6 +1022,17 @@ MessageElementFlags ChannelView::getFlags() const
|
||||
if (this->sourceChannel_ == app->twitch->mentionsChannel)
|
||||
flags.set(MessageElementFlag::ChannelName);
|
||||
|
||||
if (this->context_ == Context::ReplyThread)
|
||||
{
|
||||
// Don't show inline replies within the ReplyThreadPopup
|
||||
flags.unset(MessageElementFlag::RepliedMessage);
|
||||
}
|
||||
|
||||
if (!this->canReplyToMessages())
|
||||
{
|
||||
flags.unset(MessageElementFlag::ReplyButton);
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
@@ -1983,10 +1997,27 @@ void ChannelView::addMessageContextMenuItems(
|
||||
|
||||
menu.addAction("Copy full message", [layout] {
|
||||
QString copyString;
|
||||
layout->addSelectionText(copyString);
|
||||
layout->addSelectionText(copyString, 0, INT_MAX,
|
||||
CopyMode::EverythingButReplies);
|
||||
|
||||
crossPlatformCopy(copyString);
|
||||
});
|
||||
|
||||
// Only display reply option where it makes sense
|
||||
if (this->canReplyToMessages() && layout->isReplyable())
|
||||
{
|
||||
const auto &messagePtr = layout->getMessagePtr();
|
||||
menu.addAction("Reply to message", [this, &messagePtr] {
|
||||
this->setInputReply(messagePtr);
|
||||
});
|
||||
|
||||
if (messagePtr->replyThread != nullptr)
|
||||
{
|
||||
menu.addAction("View thread", [this, &messagePtr] {
|
||||
this->showReplyThreadPopup(messagePtr);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChannelView::addTwitchLinkContextMenuItems(
|
||||
@@ -2224,8 +2255,8 @@ void ChannelView::showUserInfoPopup(const QString &userName,
|
||||
{
|
||||
auto *userCardParent =
|
||||
static_cast<QWidget *>(&(getApp()->windows->getMainWindow()));
|
||||
auto *userPopup =
|
||||
new UserInfoPopup(getSettings()->autoCloseUserPopup, userCardParent);
|
||||
auto *userPopup = new UserInfoPopup(getSettings()->autoCloseUserPopup,
|
||||
userCardParent, this->split_);
|
||||
|
||||
auto contextChannel =
|
||||
getApp()->twitch->getChannelOrEmpty(alternativePopoutChannel);
|
||||
@@ -2351,6 +2382,14 @@ void ChannelView::handleLinkClick(QMouseEvent *event, const Link &link,
|
||||
this->underlyingChannel_.get()->reconnect();
|
||||
}
|
||||
break;
|
||||
case Link::ReplyToMessage: {
|
||||
this->setInputReply(layout->getMessagePtr());
|
||||
}
|
||||
break;
|
||||
case Link::ViewThread: {
|
||||
this->showReplyThreadPopup(layout->getMessagePtr());
|
||||
}
|
||||
break;
|
||||
|
||||
default:;
|
||||
}
|
||||
@@ -2473,4 +2512,106 @@ void ChannelView::scrollUpdateRequested()
|
||||
this->scrollBar_->offset(multiplier * offset);
|
||||
}
|
||||
|
||||
void ChannelView::setInputReply(const MessagePtr &message)
|
||||
{
|
||||
if (message == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::shared_ptr<MessageThread> thread;
|
||||
|
||||
if (message->replyThread == nullptr)
|
||||
{
|
||||
auto getThread = [&](TwitchChannel *tc) {
|
||||
auto threadIt = tc->threads().find(message->id);
|
||||
if (threadIt != tc->threads().end() && !threadIt->second.expired())
|
||||
{
|
||||
return threadIt->second.lock();
|
||||
}
|
||||
else
|
||||
{
|
||||
auto thread = std::make_shared<MessageThread>(message);
|
||||
tc->addReplyThread(thread);
|
||||
return thread;
|
||||
}
|
||||
};
|
||||
|
||||
if (auto tc =
|
||||
dynamic_cast<TwitchChannel *>(this->underlyingChannel_.get()))
|
||||
{
|
||||
thread = getThread(tc);
|
||||
}
|
||||
else if (auto tc = dynamic_cast<TwitchChannel *>(this->channel_.get()))
|
||||
{
|
||||
thread = getThread(tc);
|
||||
}
|
||||
else
|
||||
{
|
||||
qCWarning(chatterinoCommon) << "Failed to create new reply thread";
|
||||
// Unable to create new reply thread.
|
||||
// TODO(dnsge): Should probably notify user?
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
thread = message->replyThread;
|
||||
}
|
||||
|
||||
this->split_->setInputReply(thread);
|
||||
}
|
||||
|
||||
void ChannelView::showReplyThreadPopup(const MessagePtr &message)
|
||||
{
|
||||
if (message == nullptr || message->replyThread == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto popupParent =
|
||||
static_cast<QWidget *>(&(getApp()->windows->getMainWindow()));
|
||||
auto popup = new ReplyThreadPopup(getSettings()->autoCloseThreadPopup,
|
||||
popupParent, this->split_);
|
||||
|
||||
popup->setThread(message->replyThread);
|
||||
|
||||
QPoint offset(int(150 * this->scale()), int(70 * this->scale()));
|
||||
popup->move(QCursor::pos() - offset);
|
||||
popup->show();
|
||||
popup->giveFocus(Qt::MouseFocusReason);
|
||||
}
|
||||
|
||||
ChannelView::Context ChannelView::getContext() const
|
||||
{
|
||||
return this->context_;
|
||||
}
|
||||
|
||||
bool ChannelView::canReplyToMessages() const
|
||||
{
|
||||
if (this->context_ == ChannelView::Context::ReplyThread ||
|
||||
this->context_ == ChannelView::Context::Search)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this->channel_ == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this->channel_->isTwitchChannel())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this->channel_->getType() == Channel::Type::TwitchWhispers ||
|
||||
this->channel_->getType() == Channel::Type::TwitchLive)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -39,6 +39,7 @@ class Scrollbar;
|
||||
class EffectLabel;
|
||||
struct Link;
|
||||
class MessageLayoutElement;
|
||||
class Split;
|
||||
|
||||
enum class PauseReason {
|
||||
Mouse,
|
||||
@@ -61,7 +62,15 @@ class ChannelView final : public BaseWidget
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ChannelView(BaseWidget *parent = nullptr);
|
||||
enum class Context {
|
||||
None,
|
||||
UserCard,
|
||||
ReplyThread,
|
||||
Search,
|
||||
};
|
||||
|
||||
explicit ChannelView(BaseWidget *parent = nullptr, Split *split = nullptr,
|
||||
Context context = Context::None);
|
||||
|
||||
void queueUpdate();
|
||||
Scrollbar &getScrollBar();
|
||||
@@ -99,6 +108,8 @@ public:
|
||||
|
||||
void clearMessages();
|
||||
|
||||
Context getContext() const;
|
||||
|
||||
/**
|
||||
* @brief Creates and shows a UserInfoPopup dialog
|
||||
*
|
||||
@@ -196,6 +207,10 @@ private:
|
||||
void enableScrolling(const QPointF &scrollStart);
|
||||
void disableScrolling();
|
||||
|
||||
void setInputReply(const MessagePtr &message);
|
||||
void showReplyThreadPopup(const MessagePtr &message);
|
||||
bool canReplyToMessages() const;
|
||||
|
||||
QTimer *layoutCooldown_;
|
||||
bool layoutQueued_;
|
||||
|
||||
@@ -221,6 +236,7 @@ private:
|
||||
ChannelPtr channel_ = nullptr;
|
||||
ChannelPtr underlyingChannel_ = nullptr;
|
||||
ChannelPtr sourceChannel_ = nullptr;
|
||||
Split *split_ = nullptr;
|
||||
|
||||
Scrollbar *scrollBar_;
|
||||
EffectLabel *goToBottom_;
|
||||
@@ -264,6 +280,8 @@ private:
|
||||
Selection selection_;
|
||||
bool selecting_ = false;
|
||||
|
||||
const Context context_;
|
||||
|
||||
LimitedQueue<MessageLayoutPtr> messages_;
|
||||
|
||||
pajlada::Signals::SignalHolder signalHolder_;
|
||||
|
||||
@@ -49,8 +49,9 @@ ChannelPtr SearchPopup::filter(const QString &text, const QString &channelName,
|
||||
return channel;
|
||||
}
|
||||
|
||||
SearchPopup::SearchPopup(QWidget *parent)
|
||||
SearchPopup::SearchPopup(QWidget *parent, Split *split)
|
||||
: BasePopup({}, parent)
|
||||
, split_(split)
|
||||
{
|
||||
this->initLayout();
|
||||
this->resize(400, 600);
|
||||
@@ -238,7 +239,8 @@ void SearchPopup::initLayout()
|
||||
|
||||
// CHANNELVIEW
|
||||
{
|
||||
this->channelView_ = new ChannelView(this);
|
||||
this->channelView_ = new ChannelView(this, this->split_,
|
||||
ChannelView::Context::Search);
|
||||
|
||||
layout1->addWidget(this->channelView_);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "messages/LimitedQueueSnapshot.hpp"
|
||||
#include "messages/search/MessagePredicate.hpp"
|
||||
#include "widgets/BasePopup.hpp"
|
||||
#include "widgets/splits/Split.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
@@ -15,7 +16,7 @@ namespace chatterino {
|
||||
class SearchPopup : public BasePopup
|
||||
{
|
||||
public:
|
||||
SearchPopup(QWidget *parent);
|
||||
SearchPopup(QWidget *parent, Split *split = nullptr);
|
||||
|
||||
virtual void addChannel(ChannelView &channel);
|
||||
|
||||
@@ -58,6 +59,7 @@ private:
|
||||
QLineEdit *searchInput_{};
|
||||
ChannelView *channelView_{};
|
||||
QString channelName_{};
|
||||
Split *split_ = nullptr;
|
||||
QList<std::reference_wrapper<ChannelView>> searchChannels_;
|
||||
};
|
||||
|
||||
|
||||
@@ -185,6 +185,7 @@ void GeneralPage::initLayout(GeneralPageView &layout)
|
||||
tabDirectionDropdown->setMinimumWidth(
|
||||
tabDirectionDropdown->minimumSizeHint().width());
|
||||
|
||||
layout.addCheckbox("Show message reply button", s.showReplyButton);
|
||||
layout.addCheckbox("Show tab close button", s.showTabCloseButton);
|
||||
layout.addCheckbox("Always on top", s.windowTopMost);
|
||||
#ifdef USEWINSDK
|
||||
@@ -641,6 +642,9 @@ void GeneralPage::initLayout(GeneralPageView &layout)
|
||||
layout.addCheckbox("Show parted users (< 1000 chatters)", s.showParts);
|
||||
layout.addCheckbox("Automatically close user popup when it loses focus",
|
||||
s.autoCloseUserPopup);
|
||||
layout.addCheckbox(
|
||||
"Automatically close reply thread popup when it loses focus",
|
||||
s.autoCloseThreadPopup);
|
||||
layout.addCheckbox("Lowercase domains (anti-phishing)", s.lowercaseDomains);
|
||||
layout.addCheckbox("Bold @usernames", s.boldUsernames);
|
||||
layout.addCheckbox("Color @usernames", s.colorUsernames);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "controllers/commands/CommandController.hpp"
|
||||
#include "controllers/hotkeys/HotkeyController.hpp"
|
||||
#include "controllers/notifications/NotificationController.hpp"
|
||||
#include "messages/MessageThread.hpp"
|
||||
#include "providers/twitch/EmoteValue.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||
@@ -85,7 +86,7 @@ Split::Split(QWidget *parent)
|
||||
, channel_(Channel::getEmpty())
|
||||
, vbox_(new QVBoxLayout(this))
|
||||
, header_(new SplitHeader(this))
|
||||
, view_(new ChannelView(this))
|
||||
, view_(new ChannelView(this, this))
|
||||
, input_(new SplitInput(this))
|
||||
, overlay_(new SplitOverlay(this))
|
||||
{
|
||||
@@ -1164,7 +1165,7 @@ const QList<QUuid> Split::getFilters() const
|
||||
|
||||
void Split::showSearch(bool singleChannel)
|
||||
{
|
||||
auto *popup = new SearchPopup(this);
|
||||
auto *popup = new SearchPopup(this, this);
|
||||
popup->setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
if (singleChannel)
|
||||
@@ -1269,6 +1270,11 @@ void Split::drag()
|
||||
}
|
||||
}
|
||||
|
||||
void Split::setInputReply(const std::shared_ptr<MessageThread> &reply)
|
||||
{
|
||||
this->input_->setReply(reply);
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
QDebug operator<<(QDebug dbg, const chatterino::Split &split)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
namespace chatterino {
|
||||
|
||||
class ChannelView;
|
||||
class MessageThread;
|
||||
class SplitHeader;
|
||||
class SplitInput;
|
||||
class SplitContainer;
|
||||
@@ -74,6 +75,8 @@ public:
|
||||
|
||||
void setContainer(SplitContainer *container);
|
||||
|
||||
void setInputReply(const std::shared_ptr<MessageThread> &reply);
|
||||
|
||||
static pajlada::Signals::Signal<Qt::KeyboardModifiers>
|
||||
modifierStatusChanged;
|
||||
static Qt::KeyboardModifiers modifierStatus;
|
||||
|
||||
@@ -23,15 +23,24 @@
|
||||
#include "widgets/splits/SplitContainer.hpp"
|
||||
#include "widgets/splits/SplitInput.hpp"
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <QCompleter>
|
||||
#include <QPainter>
|
||||
|
||||
namespace chatterino {
|
||||
const int TWITCH_MESSAGE_LIMIT = 500;
|
||||
|
||||
SplitInput::SplitInput(Split *_chatWidget)
|
||||
: BaseWidget(_chatWidget)
|
||||
SplitInput::SplitInput(Split *_chatWidget, bool enableInlineReplying)
|
||||
: SplitInput(_chatWidget, _chatWidget, enableInlineReplying)
|
||||
{
|
||||
}
|
||||
|
||||
SplitInput::SplitInput(QWidget *parent, Split *_chatWidget,
|
||||
bool enableInlineReplying)
|
||||
: BaseWidget(parent)
|
||||
, split_(_chatWidget)
|
||||
, enableInlineReplying_(enableInlineReplying)
|
||||
{
|
||||
this->installEventFilter(this);
|
||||
this->initLayout();
|
||||
@@ -66,17 +75,43 @@ void SplitInput::initLayout()
|
||||
LayoutCreator<SplitInput> layoutCreator(this);
|
||||
|
||||
auto layout =
|
||||
layoutCreator.setLayoutType<QHBoxLayout>().withoutMargin().assign(
|
||||
&this->ui_.hbox);
|
||||
layoutCreator.setLayoutType<QVBoxLayout>().withoutMargin().assign(
|
||||
&this->ui_.vbox);
|
||||
|
||||
// reply label stuff
|
||||
auto replyWrapper =
|
||||
layout.emplace<QWidget>().assign(&this->ui_.replyWrapper);
|
||||
this->ui_.replyWrapper->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
auto replyHbox = replyWrapper.emplace<QHBoxLayout>().withoutMargin().assign(
|
||||
&this->ui_.replyHbox);
|
||||
|
||||
auto replyLabel = replyHbox.emplace<QLabel>().assign(&this->ui_.replyLabel);
|
||||
replyLabel->setAlignment(Qt::AlignLeft);
|
||||
replyLabel->setFont(
|
||||
app->fonts->getFont(FontStyle::ChatMedium, this->scale()));
|
||||
|
||||
replyHbox->addStretch(1);
|
||||
|
||||
auto replyCancelButton = replyHbox.emplace<EffectLabel>(nullptr, 4)
|
||||
.assign(&this->ui_.cancelReplyButton);
|
||||
replyCancelButton->getLabel().setTextFormat(Qt::RichText);
|
||||
|
||||
replyCancelButton->hide();
|
||||
replyLabel->hide();
|
||||
|
||||
// hbox for input, right box
|
||||
auto hboxLayout =
|
||||
layout.emplace<QHBoxLayout>().withoutMargin().assign(&this->ui_.hbox);
|
||||
|
||||
// input
|
||||
auto textEdit =
|
||||
layout.emplace<ResizingTextEdit>().assign(&this->ui_.textEdit);
|
||||
hboxLayout.emplace<ResizingTextEdit>().assign(&this->ui_.textEdit);
|
||||
connect(textEdit.getElement(), &ResizingTextEdit::textChanged, this,
|
||||
&SplitInput::editTextChanged);
|
||||
|
||||
// right box
|
||||
auto box = layout.emplace<QVBoxLayout>().withoutMargin();
|
||||
auto box = hboxLayout.emplace<QVBoxLayout>().withoutMargin();
|
||||
box->setSpacing(0);
|
||||
{
|
||||
auto textEditLength =
|
||||
@@ -102,6 +137,8 @@ void SplitInput::initLayout()
|
||||
this->managedConnections_.managedConnect(app->fonts->fontChanged, [=]() {
|
||||
this->ui_.textEdit->setFont(
|
||||
app->fonts->getFont(FontStyle::ChatMedium, this->scale()));
|
||||
this->ui_.replyLabel->setFont(
|
||||
app->fonts->getFont(FontStyle::ChatMediumBold, this->scale()));
|
||||
});
|
||||
|
||||
// open emote popup
|
||||
@@ -109,6 +146,12 @@ void SplitInput::initLayout()
|
||||
this->openEmotePopup();
|
||||
});
|
||||
|
||||
// clear input and remove reply thread
|
||||
QObject::connect(this->ui_.cancelReplyButton, &EffectLabel::leftClicked,
|
||||
[=] {
|
||||
this->clearInput();
|
||||
});
|
||||
|
||||
// clear channelview selection when selecting in the input
|
||||
QObject::connect(this->ui_.textEdit, &QTextEdit::copyAvailable,
|
||||
[this](bool available) {
|
||||
@@ -129,8 +172,10 @@ void SplitInput::initLayout()
|
||||
|
||||
void SplitInput::scaleChangedEvent(float scale)
|
||||
{
|
||||
// update the icon size of the emote button
|
||||
auto app = getApp();
|
||||
// update the icon size of the buttons
|
||||
this->updateEmoteButton();
|
||||
this->updateCancelReplyButton();
|
||||
|
||||
// set maximum height
|
||||
if (!this->hidden)
|
||||
@@ -138,9 +183,11 @@ void SplitInput::scaleChangedEvent(float scale)
|
||||
this->setMaximumHeight(this->scaledMaxHeight());
|
||||
}
|
||||
this->ui_.textEdit->setFont(
|
||||
getApp()->fonts->getFont(FontStyle::ChatMedium, this->scale()));
|
||||
app->fonts->getFont(FontStyle::ChatMedium, this->scale()));
|
||||
this->ui_.textEditLength->setFont(
|
||||
getApp()->fonts->getFont(FontStyle::ChatMedium, this->scale()));
|
||||
app->fonts->getFont(FontStyle::ChatMedium, this->scale()));
|
||||
this->ui_.replyLabel->setFont(
|
||||
app->fonts->getFont(FontStyle::ChatMediumBold, this->scale()));
|
||||
}
|
||||
|
||||
void SplitInput::themeChangedEvent()
|
||||
@@ -155,6 +202,7 @@ void SplitInput::themeChangedEvent()
|
||||
#endif
|
||||
|
||||
this->updateEmoteButton();
|
||||
this->updateCancelReplyButton();
|
||||
this->ui_.textEditLength->setPalette(palette);
|
||||
|
||||
this->ui_.textEdit->setStyleSheet(this->theme->splits.input.styleSheet);
|
||||
@@ -162,10 +210,19 @@ void SplitInput::themeChangedEvent()
|
||||
this->ui_.textEdit->setPalette(placeholderPalette);
|
||||
#endif
|
||||
|
||||
this->ui_.hbox->setMargin(
|
||||
this->ui_.vbox->setMargin(
|
||||
int((this->theme->isLightTheme() ? 4 : 2) * this->scale()));
|
||||
|
||||
this->ui_.emoteButton->getLabel().setStyleSheet("color: #000");
|
||||
|
||||
if (this->theme->isLightTheme())
|
||||
{
|
||||
this->ui_.replyLabel->setStyleSheet("color: #333");
|
||||
}
|
||||
else
|
||||
{
|
||||
this->ui_.replyLabel->setStyleSheet("color: #ccc");
|
||||
}
|
||||
}
|
||||
|
||||
void SplitInput::updateEmoteButton()
|
||||
@@ -184,6 +241,24 @@ void SplitInput::updateEmoteButton()
|
||||
this->ui_.emoteButton->setFixedHeight(int(18 * scale));
|
||||
}
|
||||
|
||||
void SplitInput::updateCancelReplyButton()
|
||||
{
|
||||
float scale = this->scale();
|
||||
|
||||
QString text =
|
||||
QStringLiteral(
|
||||
"<img src=':/buttons/cancel.svg' width='%1' height='%1' />")
|
||||
.arg(QString::number(int(12 * scale)));
|
||||
|
||||
if (this->theme->isLightTheme())
|
||||
{
|
||||
text.replace("cancel", "cancelDark");
|
||||
}
|
||||
|
||||
this->ui_.cancelReplyButton->getLabel().setText(text);
|
||||
this->ui_.cancelReplyButton->setFixedHeight(int(12 * scale));
|
||||
}
|
||||
|
||||
void SplitInput::openEmotePopup()
|
||||
{
|
||||
if (!this->emotePopup_)
|
||||
@@ -217,6 +292,79 @@ void SplitInput::openEmotePopup()
|
||||
this->emotePopup_->activateWindow();
|
||||
}
|
||||
|
||||
QString SplitInput::handleSendMessage(std::vector<QString> &arguments)
|
||||
{
|
||||
auto c = this->split_->getChannel();
|
||||
if (c == nullptr)
|
||||
return "";
|
||||
|
||||
if (!c->isTwitchChannel() || this->replyThread_ == nullptr)
|
||||
{
|
||||
// standard message send behavior
|
||||
QString message = ui_.textEdit->toPlainText();
|
||||
|
||||
message = message.replace('\n', ' ');
|
||||
QString sendMessage =
|
||||
getApp()->commands->execCommand(message, c, false);
|
||||
|
||||
c->sendMessage(sendMessage);
|
||||
|
||||
this->postMessageSend(message, arguments);
|
||||
return "";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Reply to message
|
||||
auto tc = dynamic_cast<TwitchChannel *>(c.get());
|
||||
if (!tc)
|
||||
{
|
||||
// this should not fail
|
||||
return "";
|
||||
}
|
||||
|
||||
QString message = this->ui_.textEdit->toPlainText();
|
||||
|
||||
if (this->enableInlineReplying_)
|
||||
{
|
||||
// Remove @username prefix that is inserted when doing inline replies
|
||||
message.remove(0, this->replyThread_->root()->displayName.length() +
|
||||
1); // remove "@username"
|
||||
|
||||
if (!message.isEmpty() && message.at(0) == ' ')
|
||||
{
|
||||
message.remove(0, 1); // remove possible space
|
||||
}
|
||||
}
|
||||
|
||||
message = message.replace('\n', ' ');
|
||||
QString sendMessage =
|
||||
getApp()->commands->execCommand(message, c, false);
|
||||
|
||||
// Reply within TwitchChannel
|
||||
tc->sendReply(sendMessage, this->replyThread_->rootId());
|
||||
|
||||
this->postMessageSend(message, arguments);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
void SplitInput::postMessageSend(const QString &message,
|
||||
const std::vector<QString> &arguments)
|
||||
{
|
||||
// don't add duplicate messages and empty message to message history
|
||||
if ((this->prevMsg_.isEmpty() || !this->prevMsg_.endsWith(message)) &&
|
||||
!message.trimmed().isEmpty())
|
||||
{
|
||||
this->prevMsg_.append(message);
|
||||
}
|
||||
|
||||
if (arguments.empty() || arguments.at(0) != "keepInput")
|
||||
{
|
||||
this->clearInput();
|
||||
}
|
||||
this->prevIndex_ = this->prevMsg_.size();
|
||||
}
|
||||
|
||||
int SplitInput::scaledMaxHeight() const
|
||||
{
|
||||
return int(150 * this->scale());
|
||||
@@ -302,37 +450,7 @@ void SplitInput::addShortcuts()
|
||||
}},
|
||||
{"sendMessage",
|
||||
[this](std::vector<QString> arguments) -> QString {
|
||||
auto c = this->split_->getChannel();
|
||||
if (c == nullptr)
|
||||
return "";
|
||||
|
||||
QString message = ui_.textEdit->toPlainText();
|
||||
|
||||
message = message.replace('\n', ' ');
|
||||
QString sendMessage =
|
||||
getApp()->commands->execCommand(message, c, false);
|
||||
|
||||
c->sendMessage(sendMessage);
|
||||
// don't add duplicate messages and empty message to message history
|
||||
if ((this->prevMsg_.isEmpty() ||
|
||||
!this->prevMsg_.endsWith(message)) &&
|
||||
!message.trimmed().isEmpty())
|
||||
{
|
||||
this->prevMsg_.append(message);
|
||||
}
|
||||
bool shouldClearInput = true;
|
||||
if (arguments.size() != 0 && arguments.at(0) == "keepInput")
|
||||
{
|
||||
shouldClearInput = false;
|
||||
}
|
||||
|
||||
if (shouldClearInput)
|
||||
{
|
||||
this->currMsg_ = QString();
|
||||
this->ui_.textEdit->setPlainText(QString());
|
||||
}
|
||||
this->prevIndex_ = this->prevMsg_.size();
|
||||
return "";
|
||||
return this->handleSendMessage(arguments);
|
||||
}},
|
||||
{"previousMessage",
|
||||
[this](std::vector<QString>) -> QString {
|
||||
@@ -456,8 +574,7 @@ void SplitInput::addShortcuts()
|
||||
}},
|
||||
{"clear",
|
||||
[this](std::vector<QString>) -> QString {
|
||||
this->ui_.textEdit->setText("");
|
||||
this->ui_.textEdit->moveCursor(QTextCursor::Start);
|
||||
this->clearInput();
|
||||
return "";
|
||||
}},
|
||||
{"selectAll",
|
||||
@@ -769,37 +886,71 @@ void SplitInput::editTextChanged()
|
||||
}
|
||||
|
||||
this->ui_.textEditLength->setText(labelText);
|
||||
|
||||
bool hasReply = false;
|
||||
if (this->enableInlineReplying_)
|
||||
{
|
||||
if (this->replyThread_ != nullptr)
|
||||
{
|
||||
// Check if the input still starts with @username. If not, don't reply.
|
||||
//
|
||||
// We need to verify that
|
||||
// 1. the @username prefix exists and
|
||||
// 2. if a character exists after the @username, it is a space
|
||||
QString replyPrefix = "@" + this->replyThread_->root()->displayName;
|
||||
if (!text.startsWith(replyPrefix) ||
|
||||
(text.length() > replyPrefix.length() &&
|
||||
text.at(replyPrefix.length()) != ' '))
|
||||
{
|
||||
this->replyThread_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide reply label if inline replies are possible
|
||||
hasReply = this->replyThread_ != nullptr;
|
||||
}
|
||||
|
||||
this->ui_.replyWrapper->setVisible(hasReply);
|
||||
this->ui_.replyLabel->setVisible(hasReply);
|
||||
this->ui_.cancelReplyButton->setVisible(hasReply);
|
||||
}
|
||||
|
||||
void SplitInput::paintEvent(QPaintEvent * /*event*/)
|
||||
{
|
||||
QPainter painter(this);
|
||||
|
||||
int s;
|
||||
QColor borderColor;
|
||||
|
||||
if (this->theme->isLightTheme())
|
||||
{
|
||||
int s = int(3 * this->scale());
|
||||
QRect rect = this->rect().marginsRemoved(QMargins(s - 1, s - 1, s, s));
|
||||
|
||||
painter.fillRect(rect, this->theme->splits.input.background);
|
||||
|
||||
painter.setPen(QColor("#ccc"));
|
||||
painter.drawRect(rect);
|
||||
s = int(3 * this->scale());
|
||||
borderColor = QColor("#ccc");
|
||||
}
|
||||
else
|
||||
{
|
||||
int s = int(1 * this->scale());
|
||||
QRect rect = this->rect().marginsRemoved(QMargins(s - 1, s - 1, s, s));
|
||||
|
||||
painter.fillRect(rect, this->theme->splits.input.background);
|
||||
|
||||
painter.setPen(QColor("#333"));
|
||||
painter.drawRect(rect);
|
||||
s = int(1 * this->scale());
|
||||
borderColor = QColor("#333");
|
||||
}
|
||||
|
||||
// int offset = 2;
|
||||
// painter.fillRect(offset, this->height() - offset, this->width() - 2 *
|
||||
// offset, 1,
|
||||
// getApp()->themes->splits.input.focusedLine);
|
||||
QMargins removeMargins(s - 1, s - 1, s, s);
|
||||
QRect baseRect = this->rect();
|
||||
|
||||
// completeAreaRect includes the reply label
|
||||
QRect completeAreaRect = baseRect.marginsRemoved(removeMargins);
|
||||
painter.fillRect(completeAreaRect, this->theme->splits.input.background);
|
||||
painter.setPen(borderColor);
|
||||
painter.drawRect(completeAreaRect);
|
||||
|
||||
if (this->enableInlineReplying_ && this->replyThread_ != nullptr)
|
||||
{
|
||||
// Move top of rect down to not include reply label
|
||||
baseRect.setTop(baseRect.top() + this->ui_.replyWrapper->height());
|
||||
|
||||
QRect onlyInputRect = baseRect.marginsRemoved(removeMargins);
|
||||
painter.setPen(borderColor);
|
||||
painter.drawRect(onlyInputRect);
|
||||
}
|
||||
}
|
||||
|
||||
void SplitInput::resizeEvent(QResizeEvent *)
|
||||
@@ -814,4 +965,41 @@ void SplitInput::resizeEvent(QResizeEvent *)
|
||||
}
|
||||
}
|
||||
|
||||
void SplitInput::giveFocus(Qt::FocusReason reason)
|
||||
{
|
||||
this->ui_.textEdit->setFocus(reason);
|
||||
}
|
||||
|
||||
void SplitInput::setReply(std::shared_ptr<MessageThread> reply,
|
||||
bool showReplyingLabel)
|
||||
{
|
||||
this->replyThread_ = std::move(reply);
|
||||
|
||||
if (this->enableInlineReplying_)
|
||||
{
|
||||
// Only enable reply label if inline replying
|
||||
this->ui_.textEdit->setPlainText(
|
||||
"@" + this->replyThread_->root()->displayName + " ");
|
||||
this->ui_.textEdit->moveCursor(QTextCursor::EndOfBlock);
|
||||
this->ui_.replyLabel->setText("Replying to @" +
|
||||
this->replyThread_->root()->displayName);
|
||||
}
|
||||
}
|
||||
|
||||
void SplitInput::setPlaceholderText(const QString &text)
|
||||
{
|
||||
this->ui_.textEdit->setPlaceholderText(text);
|
||||
}
|
||||
|
||||
void SplitInput::clearInput()
|
||||
{
|
||||
this->currMsg_ = "";
|
||||
this->ui_.textEdit->setText("");
|
||||
this->ui_.textEdit->moveCursor(QTextCursor::Start);
|
||||
if (this->enableInlineReplying_)
|
||||
{
|
||||
this->replyThread_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <QTextEdit>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
#include <memory>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
@@ -18,6 +19,7 @@ class Split;
|
||||
class EmotePopup;
|
||||
class InputCompletionPopup;
|
||||
class EffectLabel;
|
||||
class MessageThread;
|
||||
class ResizingTextEdit;
|
||||
|
||||
class SplitInput : public BaseWidget
|
||||
@@ -25,13 +27,19 @@ class SplitInput : public BaseWidget
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SplitInput(Split *_chatWidget);
|
||||
SplitInput(Split *_chatWidget, bool enableInlineReplying = true);
|
||||
SplitInput(QWidget *parent, Split *_chatWidget,
|
||||
bool enableInlineReplying = true);
|
||||
|
||||
void clearSelection();
|
||||
bool isEditFirstWord() const;
|
||||
QString getInputText() const;
|
||||
void insertText(const QString &text);
|
||||
|
||||
void setReply(std::shared_ptr<MessageThread> reply,
|
||||
bool showInlineReplying = true);
|
||||
void setPlaceholderText(const QString &text);
|
||||
|
||||
/**
|
||||
* @brief Hide the widget
|
||||
*
|
||||
@@ -64,7 +72,16 @@ protected:
|
||||
void paintEvent(QPaintEvent * /*event*/) override;
|
||||
void resizeEvent(QResizeEvent * /*event*/) override;
|
||||
|
||||
private:
|
||||
virtual void giveFocus(Qt::FocusReason reason);
|
||||
|
||||
QString handleSendMessage(std::vector<QString> &arguments);
|
||||
void postMessageSend(const QString &message,
|
||||
const std::vector<QString> &arguments);
|
||||
|
||||
/// Clears the input box, clears reply thread if inline replies are enabled
|
||||
void clearInput();
|
||||
|
||||
protected:
|
||||
void addShortcuts() override;
|
||||
void initLayout();
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
@@ -78,6 +95,8 @@ private:
|
||||
void insertCompletionText(const QString &text);
|
||||
void openEmotePopup();
|
||||
|
||||
void updateCancelReplyButton();
|
||||
|
||||
// scaledMaxHeight returns the height in pixels that this widget can grow to
|
||||
// This does not take hidden into account, so callers must take hidden into account themselves
|
||||
int scaledMaxHeight() const;
|
||||
@@ -92,8 +111,17 @@ private:
|
||||
EffectLabel *emoteButton;
|
||||
|
||||
QHBoxLayout *hbox;
|
||||
QVBoxLayout *vbox;
|
||||
|
||||
QWidget *replyWrapper;
|
||||
QHBoxLayout *replyHbox;
|
||||
QLabel *replyLabel;
|
||||
EffectLabel *cancelReplyButton;
|
||||
} ui_;
|
||||
|
||||
std::shared_ptr<MessageThread> replyThread_ = nullptr;
|
||||
bool enableInlineReplying_;
|
||||
|
||||
pajlada::Signals::SignalHolder managedConnections_;
|
||||
QStringList prevMsg_;
|
||||
QString currMsg_;
|
||||
@@ -109,6 +137,7 @@ private slots:
|
||||
void editTextChanged();
|
||||
|
||||
friend class Split;
|
||||
friend class ReplyThreadPopup;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
Reference in New Issue
Block a user