renamed a bunch of files and classes

This commit is contained in:
2017-11-12 17:21:50 +01:00
parent ad8ee97de9
commit 7f18cefee4
44 changed files with 699 additions and 505 deletions
+1
View File
@@ -0,0 +1 @@
../.clang-format
+884
View File
@@ -0,0 +1,884 @@
#include "widgets/helper/channelview.hpp"
#include "channelmanager.hpp"
#include "colorscheme.hpp"
#include "debug/log.hpp"
#include "messages/limitedqueuesnapshot.hpp"
#include "messages/message.hpp"
#include "messages/messageref.hpp"
#include "settingsmanager.hpp"
#include "ui_accountpopupform.h"
#include "util/benchmark.hpp"
#include "util/distancebetweenpoints.hpp"
#include "widgets/split.hpp"
#include "windowmanager.hpp"
#include <QDebug>
#include <QDesktopServices>
#include <QGraphicsBlurEffect>
#include <QPainter>
#include <math.h>
#include <algorithm>
#include <chrono>
#include <functional>
#include <memory>
using namespace chatterino::messages;
namespace chatterino {
namespace widgets {
ChannelView::ChannelView(WindowManager &windowManager, BaseWidget *parent)
: BaseWidget(parent)
, windowManager(windowManager)
, scrollBar(this)
, userPopupWidget(std::shared_ptr<twitch::TwitchChannel>())
{
#ifndef Q_OS_MAC
// this->setAttribute(Qt::WA_OpaquePaintEvent);
#endif
this->setMouseTracking(true);
QObject::connect(&SettingsManager::getInstance(), &SettingsManager::wordTypeMaskChanged, this,
&ChannelView::wordTypeMaskChanged);
this->scrollBar.getCurrentValueChanged().connect([this] {
// Whenever the scrollbar value has been changed, re-render the ChatWidgetView
this->layoutMessages();
this->goToBottom->setVisible(this->scrollBar.isVisible() && !this->scrollBar.isAtBottom());
this->queueUpdate();
});
this->repaintGifsConnection =
windowManager.repaintGifs.connect([&] { this->updateGifEmotes(); });
this->layoutConnection = windowManager.layout.connect([&] { this->layoutMessages(); });
this->goToBottom = new RippleEffectLabel(this, 0);
this->goToBottom->setStyleSheet("background-color: rgba(0,0,0,0.5); color: #FFF;");
this->goToBottom->getLabel().setText("Jump to bottom");
this->goToBottom->setVisible(false);
connect(goToBottom, &RippleEffectLabel::clicked, this,
[this] { QTimer::singleShot(180, [this] { this->scrollBar.scrollToBottom(); }); });
this->updateTimer.setInterval(1000 / 60);
this->updateTimer.setSingleShot(true);
connect(&this->updateTimer, &QTimer::timeout, this, [this] {
if (this->updateQueued) {
this->update();
}
this->updateTimer.start();
});
}
ChannelView::~ChannelView()
{
QObject::disconnect(&SettingsManager::getInstance(), &SettingsManager::wordTypeMaskChanged,
this, &ChannelView::wordTypeMaskChanged);
}
void ChannelView::queueUpdate()
{
if (this->updateTimer.isActive()) {
this->updateQueued = true;
}
update();
this->updateTimer.start();
}
void ChannelView::layoutMessages()
{
this->actuallyLayoutMessages();
}
void ChannelView::actuallyLayoutMessages()
{
// BENCH(timer)
auto messages = this->getMessagesSnapshot();
if (messages.getLength() == 0) {
this->scrollBar.setVisible(false);
return;
}
bool redrawRequired = false;
bool showScrollbar = false;
// Bool indicating whether or not we were showing all messages
// True if one of the following statements are true:
// The scrollbar was not visible
// The scrollbar was visible and at the bottom
this->showingLatestMessages = this->scrollBar.isAtBottom() || !this->scrollBar.isVisible();
size_t start = this->scrollBar.getCurrentValue();
int layoutWidth =
(this->scrollBar.isVisible() ? width() - this->scrollBar.width() : width()) - 4;
// layout the visible messages in the view
if (messages.getLength() > start) {
int y = -(messages[start]->getHeight() * (fmod(this->scrollBar.getCurrentValue(), 1)));
for (size_t i = start; i < messages.getLength(); ++i) {
auto message = messages[i];
redrawRequired |= message->layout(layoutWidth);
y += message->getHeight();
if (y >= height()) {
break;
}
}
}
// layout the messages at the bottom to determine the scrollbar thumb size
int h = height() - 8;
for (std::size_t i = messages.getLength() - 1; i > 0; i--) {
auto *message = messages[i].get();
message->layout(layoutWidth);
h -= message->getHeight();
if (h < 0) {
this->scrollBar.setLargeChange((messages.getLength() - i) +
(qreal)h / message->getHeight());
this->scrollBar.setDesiredValue(this->scrollBar.getDesiredValue());
showScrollbar = true;
break;
}
}
this->scrollBar.setVisible(showScrollbar);
if (!showScrollbar) {
this->scrollBar.setDesiredValue(0);
}
this->scrollBar.setMaximum(messages.getLength());
if (this->showingLatestMessages && showScrollbar) {
// If we were showing the latest messages and the scrollbar now wants to be rendered, scroll
// to bottom
// TODO: Do we want to check if the user is currently moving the scrollbar?
// Perhaps also if the user scrolled with the scrollwheel in this ChatWidget in the last 0.2
// seconds or something
this->scrollBar.scrollToBottom();
}
// MARK(timer);
if (redrawRequired) {
this->queueUpdate();
}
}
void ChannelView::clearMessages()
{
// Clear all stored messages in this chat widget
this->messages.clear();
// Layout chat widget messages, and force an update regardless if there are no messages
this->layoutMessages();
this->queueUpdate();
}
void ChannelView::updateGifEmotes()
{
if (!this->gifEmotes.empty()) {
this->onlyUpdateEmotes = true;
this->queueUpdate();
}
}
ScrollBar &ChannelView::getScrollBar()
{
return this->scrollBar;
}
QString ChannelView::getSelectedText()
{
LimitedQueueSnapshot<SharedMessageRef> messages = this->getMessagesSnapshot();
QString text;
bool isSingleMessage = this->selection.isSingleMessage();
size_t i = std::max(0, this->selection.min.messageIndex);
int charIndex = 0;
bool first = true;
auto addPart = [&](const WordPart &part, int from = 0, int to = -1) {
if (part.getCopyText().isEmpty()) {
return;
}
if (part.getWord().isText()) {
text += part.getText().mid(from, to);
} else {
text += part.getCopyText();
}
};
// first line
for (const messages::WordPart &part : messages[i]->getWordParts()) {
int charLength = part.getCharacterLength();
if (charIndex + charLength < this->selection.min.charIndex) {
charIndex += charLength;
continue;
}
if (first) {
first = false;
bool isSingleWord =
isSingleMessage &&
this->selection.max.charIndex - charIndex < part.getCharacterLength();
if (isSingleWord) {
// return single word
addPart(part, this->selection.min.charIndex - charIndex,
this->selection.max.charIndex - this->selection.min.charIndex);
return text;
} else {
// add first word of the selection
addPart(part, this->selection.min.charIndex - charIndex);
}
} else if (isSingleMessage && charIndex + charLength >= selection.max.charIndex) {
addPart(part, 0, this->selection.max.charIndex - charIndex);
return text;
} else {
text += part.getCopyText() + (part.hasTrailingSpace() ? " " : "");
}
charIndex += charLength;
}
text += "\n";
// middle lines
for (i++; i < this->selection.max.messageIndex; i++) {
for (const messages::WordPart &part : messages[i]->getWordParts()) {
if (!part.getCopyText().isEmpty()) {
text += part.getCopyText();
if (part.hasTrailingSpace()) {
text += " ";
}
}
}
text += "\n";
}
// last line
charIndex = 0;
for (const messages::WordPart &part :
messages[this->selection.max.messageIndex]->getWordParts()) {
int charLength = part.getCharacterLength();
if (charIndex + charLength >= this->selection.max.charIndex) {
addPart(part, 0, this->selection.max.charIndex - charIndex);
return text;
}
text += part.getCopyText();
if (part.hasTrailingSpace()) {
text += " ";
}
charIndex += charLength;
}
return text;
}
bool ChannelView::hasSelection()
{
return !this->selection.isEmpty();
}
void ChannelView::clearSelection()
{
this->selection = Selection();
layoutMessages();
}
messages::LimitedQueueSnapshot<SharedMessageRef> ChannelView::getMessagesSnapshot()
{
return this->messages.getSnapshot();
}
void ChannelView::setChannel(std::shared_ptr<Channel> channel)
{
if (this->channel) {
this->detachChannel();
}
this->messages.clear();
// on new message
this->messageAppendedConnection =
channel->messageAppended.connect([this](SharedMessage &message) {
SharedMessageRef deleted;
auto messageRef = new MessageRef(message);
if (this->messages.appendItem(SharedMessageRef(messageRef), deleted)) {
qreal value = std::max(0.0, this->getScrollBar().getDesiredValue() - 1);
this->getScrollBar().setDesiredValue(value, false);
}
layoutMessages();
update();
});
// on message removed
this->messageRemovedConnection =
channel->messageRemovedFromStart.connect([this](SharedMessage &) {
this->selection.min.messageIndex--;
this->selection.max.messageIndex--;
this->selection.start.messageIndex--;
this->selection.end.messageIndex--;
this->layoutMessages();
});
auto snapshot = channel->getMessageSnapshot();
for (size_t i = 0; i < snapshot.getLength(); i++) {
SharedMessageRef deleted;
auto messageRef = new MessageRef(snapshot[i]);
this->messages.appendItem(SharedMessageRef(messageRef), deleted);
}
this->channel = channel;
this->userPopupWidget.setChannel(channel);
}
void ChannelView::detachChannel()
{
// on message added
this->messageAppendedConnection.disconnect();
// on message removed
this->messageRemovedConnection.disconnect();
}
void ChannelView::resizeEvent(QResizeEvent *)
{
this->scrollBar.resize(this->scrollBar.width(), height());
this->scrollBar.move(width() - this->scrollBar.width(), 0);
this->goToBottom->setGeometry(0, this->height() - 32, this->width(), 32);
this->scrollBar.raise();
layoutMessages();
this->update();
}
void ChannelView::setSelection(const SelectionItem &start, const SelectionItem &end)
{
// selections
this->selection = Selection(start, end);
this->selectionChanged();
// qDebug() << min.messageIndex << ":" << min.charIndex << " " << max.messageIndex << ":"
// << max.charIndex;
}
void ChannelView::paintEvent(QPaintEvent * /*event*/)
{
// BENCH(timer);
QPainter painter(this);
// painter.setRenderHint(QPainter::SmoothPixmapTransform);
// only update gif emotes
#ifndef Q_OS_MAC
// if (this->onlyUpdateEmotes) {
// this->onlyUpdateEmotes = false;
// for (const GifEmoteData &item : this->gifEmotes) {
// painter.fillRect(item.rect, this->colorScheme.ChatBackground);
// painter.drawPixmap(item.rect, *item.image->getPixmap());
// }
// return;
// }
#endif
// update all messages
this->gifEmotes.clear();
painter.fillRect(rect(), this->colorScheme.ChatBackground);
// draw messages
this->drawMessages(painter);
// draw gif emotes
for (GifEmoteData &item : this->gifEmotes) {
// painter.fillRect(item.rect, this->colorScheme.ChatBackground);
painter.drawPixmap(item.rect, *item.image->getPixmap());
}
// MARK(timer);
}
void ChannelView::drawMessages(QPainter &painter)
{
auto messages = this->getMessagesSnapshot();
size_t start = this->scrollBar.getCurrentValue();
if (start >= messages.getLength()) {
return;
}
int y = -(messages[start].get()->getHeight() * (fmod(this->scrollBar.getCurrentValue(), 1)));
for (size_t i = start; i < messages.getLength(); ++i) {
messages::MessageRef *messageRef = messages[i].get();
std::shared_ptr<QPixmap> buffer = messageRef->buffer;
// bool updateBuffer = messageRef->updateBuffer;
bool updateBuffer = false;
if (!buffer) {
buffer = std::shared_ptr<QPixmap>(new QPixmap(width(), messageRef->getHeight()));
updateBuffer = true;
}
updateBuffer |= this->selecting;
// update messages that have been changed
if (updateBuffer) {
this->updateMessageBuffer(messageRef, buffer.get(), i);
// qDebug() << "updating buffer xD";
}
// get gif emotes
for (messages::WordPart const &wordPart : messageRef->getWordParts()) {
if (wordPart.getWord().isImage()) {
messages::LazyLoadedImage &lli = wordPart.getWord().getImage();
if (lli.getAnimated()) {
GifEmoteData gifEmoteData;
gifEmoteData.image = &lli;
QRect rect(wordPart.getX(), wordPart.getY() + y, wordPart.getWidth(),
wordPart.getHeight());
gifEmoteData.rect = rect;
this->gifEmotes.push_back(gifEmoteData);
}
}
}
messageRef->buffer = buffer;
if (buffer) {
painter.drawPixmap(0, y, *buffer.get());
}
y += messageRef->getHeight();
if (y > height()) {
break;
}
}
}
void ChannelView::updateMessageBuffer(messages::MessageRef *messageRef, QPixmap *buffer,
int messageIndex)
{
QPainter painter(buffer);
// draw background
// if (this->selectionMin.messageIndex <= messageIndex &&
// this->selectionMax.messageIndex >= messageIndex) {
// painter.fillRect(buffer->rect(), QColor(24, 55, 25));
//} else {
painter.fillRect(buffer->rect(),
(messageRef->getMessage()->getCanHighlightTab())
? this->colorScheme.ChatBackgroundHighlighted
: this->colorScheme.ChatBackground);
//}
// draw selection
if (!selection.isEmpty()) {
drawMessageSelection(painter, messageRef, messageIndex, buffer->height());
}
// draw message
for (messages::WordPart const &wordPart : messageRef->getWordParts()) {
// image
if (wordPart.getWord().isImage()) {
messages::LazyLoadedImage &lli = wordPart.getWord().getImage();
const QPixmap *image = lli.getPixmap();
if (image != nullptr && !lli.getAnimated()) {
painter.drawPixmap(QRect(wordPart.getX(), wordPart.getY(), wordPart.getWidth(),
wordPart.getHeight()),
*image);
}
}
// text
else {
QColor color = wordPart.getWord().getColor().getColor(this->colorScheme);
this->colorScheme.normalizeColor(color);
painter.setPen(color);
painter.setFont(wordPart.getWord().getFont());
painter.drawText(QRectF(wordPart.getX(), wordPart.getY(), 10000, 10000),
wordPart.getText(), QTextOption(Qt::AlignLeft | Qt::AlignTop));
}
}
messageRef->updateBuffer = false;
}
void ChannelView::drawMessageSelection(QPainter &painter, messages::MessageRef *messageRef,
int messageIndex, int bufferHeight)
{
if (this->selection.min.messageIndex > messageIndex ||
this->selection.max.messageIndex < messageIndex) {
return;
}
QColor selectionColor(255, 255, 255, 63);
int charIndex = 0;
size_t i = 0;
auto &parts = messageRef->getWordParts();
int currentLineNumber = 0;
QRect rect;
if (parts.size() > 0) {
if (selection.min.messageIndex == messageIndex) {
rect.setTop(parts.at(0).getY());
}
rect.setLeft(parts.at(0).getX());
}
// skip until selection start
if (this->selection.min.messageIndex == messageIndex && this->selection.min.charIndex != 0) {
for (; i < parts.size(); i++) {
const messages::WordPart &part = parts.at(i);
auto characterLength = part.getCharacterLength();
if (characterLength + charIndex > selection.min.charIndex) {
break;
}
charIndex += characterLength;
currentLineNumber = part.getLineNumber();
}
if (i >= parts.size()) {
return;
}
// handle word that has a cut of selection
const messages::WordPart &part = parts.at(i);
// check if selection if single word
int characterLength = part.getCharacterLength();
bool isSingleWord = charIndex + characterLength > this->selection.max.charIndex &&
this->selection.max.messageIndex == messageIndex;
rect = part.getRect();
currentLineNumber = part.getLineNumber();
if (part.getWord().isText()) {
int offset = this->selection.min.charIndex - charIndex;
for (int j = 0; j < offset; j++) {
rect.setLeft(rect.left() + part.getCharWidth(j));
}
if (isSingleWord) {
int length = (this->selection.max.charIndex - charIndex) - offset;
rect.setRight(part.getX());
for (int j = 0; j < offset + length; j++) {
rect.setRight(rect.right() + part.getCharWidth(j));
}
painter.fillRect(rect, selectionColor);
return;
}
} else {
if (isSingleWord) {
if (charIndex + 1 != this->selection.max.charIndex) {
rect.setRight(part.getX() + part.getWord().getImage().getScaledWidth());
}
painter.fillRect(rect, selectionColor);
return;
}
if (charIndex != this->selection.min.charIndex) {
rect.setLeft(part.getX() + part.getWord().getImage().getScaledWidth());
}
}
i++;
charIndex += characterLength;
}
// go through lines and draw selection
for (; i < parts.size(); i++) {
const messages::WordPart &part = parts.at(i);
int charLength = part.getCharacterLength();
bool isLastSelectedWord = this->selection.max.messageIndex == messageIndex &&
charIndex + charLength > this->selection.max.charIndex;
if (part.getLineNumber() == currentLineNumber) {
rect.setLeft(std::min(rect.left(), part.getX()));
rect.setTop(std::min(rect.top(), part.getY()));
rect.setRight(std::max(rect.right(), part.getRight()));
rect.setBottom(std::max(rect.bottom(), part.getBottom() - 1));
} else {
painter.fillRect(rect, selectionColor);
currentLineNumber = part.getLineNumber();
rect = part.getRect();
}
if (isLastSelectedWord) {
if (part.getWord().isText()) {
int offset = this->selection.min.charIndex - charIndex;
int length = (this->selection.max.charIndex - charIndex) - offset;
rect.setRight(part.getX());
for (int j = 0; j < offset + length; j++) {
rect.setRight(rect.right() + part.getCharWidth(j));
}
} else {
if (this->selection.max.charIndex == charIndex) {
rect.setRight(part.getX());
}
}
painter.fillRect(rect, selectionColor);
return;
}
charIndex += charLength;
}
if (this->selection.max.messageIndex != messageIndex) {
rect.setBottom(bufferHeight);
}
painter.fillRect(rect, selectionColor);
}
void ChannelView::wheelEvent(QWheelEvent *event)
{
if (this->scrollBar.isVisible()) {
auto mouseMultiplier = SettingsManager::getInstance().mouseScrollMultiplier.get();
this->scrollBar.setDesiredValue(
this->scrollBar.getDesiredValue() - event->delta() / 10.0 * mouseMultiplier, true);
}
}
void ChannelView::mouseMoveEvent(QMouseEvent *event)
{
std::shared_ptr<messages::MessageRef> message;
QPoint relativePos;
int messageIndex;
if (!tryGetMessageAt(event->pos(), message, relativePos, messageIndex)) {
setCursor(Qt::ArrowCursor);
return;
}
if (this->selecting) {
int index = message->getSelectionIndex(relativePos);
this->setSelection(this->selection.start, SelectionItem(messageIndex, index));
this->repaint();
}
const messages::Word *hoverWord;
if ((hoverWord = message->tryGetWordPart(relativePos)) == nullptr) {
setCursor(Qt::ArrowCursor);
return;
}
if (hoverWord->getLink().isValid()) {
setCursor(Qt::PointingHandCursor);
} else {
setCursor(Qt::ArrowCursor);
}
}
void ChannelView::mousePressEvent(QMouseEvent *event)
{
this->isMouseDown = true;
this->lastPressPosition = event->screenPos();
std::shared_ptr<messages::MessageRef> message;
QPoint relativePos;
int messageIndex;
this->mouseDown(event);
if (!tryGetMessageAt(event->pos(), message, relativePos, messageIndex)) {
setCursor(Qt::ArrowCursor);
auto messages = this->getMessagesSnapshot();
if (messages.getLength() == 0) {
return;
}
// Start selection at the last message at its last index
auto lastMessageIndex = messages.getLength() - 1;
auto lastMessage = messages[lastMessageIndex];
auto lastCharacterIndex = lastMessage->getLastCharacterIndex();
SelectionItem selectionItem(lastMessageIndex, lastCharacterIndex);
this->setSelection(selectionItem, selectionItem);
this->selecting = true;
return;
}
int index = message->getSelectionIndex(relativePos);
auto selectionItem = SelectionItem(messageIndex, index);
this->setSelection(selectionItem, selectionItem);
this->selecting = true;
this->repaint();
}
void ChannelView::mouseReleaseEvent(QMouseEvent *event)
{
if (!this->isMouseDown) {
// We didn't grab the mouse press, so we shouldn't be handling the mouse
// release
return;
}
this->isMouseDown = false;
this->selecting = false;
float distance = util::distanceBetweenPoints(this->lastPressPosition, event->screenPos());
qDebug() << "Distance: " << distance;
if (fabsf(distance) > 15.f) {
// It wasn't a proper click, so we don't care about that here
return;
}
// If you clicked and released less than X pixels away, it counts
// as a click!
// show user thing pajaW
std::shared_ptr<messages::MessageRef> message;
QPoint relativePos;
int messageIndex;
if (!tryGetMessageAt(event->pos(), message, relativePos, messageIndex)) {
// No message at clicked position
this->userPopupWidget.hide();
return;
}
const messages::Word *hoverWord;
if ((hoverWord = message->tryGetWordPart(relativePos)) == nullptr) {
return;
}
auto &link = hoverWord->getLink();
switch (link.getType()) {
case messages::Link::UserInfo: {
auto user = link.getValue();
this->userPopupWidget.setName(user);
this->userPopupWidget.move(event->screenPos().toPoint());
this->userPopupWidget.updatePermissions();
this->userPopupWidget.show();
this->userPopupWidget.setFocus();
qDebug() << "Clicked " << user << "s message";
break;
}
case messages::Link::Url: {
QDesktopServices::openUrl(QUrl(link.getValue()));
break;
}
}
}
bool ChannelView::tryGetMessageAt(QPoint p, std::shared_ptr<messages::MessageRef> &_message,
QPoint &relativePos, int &index)
{
auto messages = this->getMessagesSnapshot();
size_t start = this->scrollBar.getCurrentValue();
if (start >= messages.getLength()) {
return false;
}
int y = -(messages[start]->getHeight() * (fmod(this->scrollBar.getCurrentValue(), 1)));
for (size_t i = start; i < messages.getLength(); ++i) {
auto message = messages[i];
if (p.y() < y + message->getHeight()) {
relativePos = QPoint(p.x(), p.y() - y);
_message = message;
index = i;
return true;
}
y += message->getHeight();
}
return false;
}
} // namespace widgets
} // namespace chatterino
+176
View File
@@ -0,0 +1,176 @@
#pragma once
#include "channel.hpp"
#include "messages/lazyloadedimage.hpp"
#include "messages/limitedqueuesnapshot.hpp"
#include "messages/messageref.hpp"
#include "messages/word.hpp"
#include "widgets/accountpopup.hpp"
#include "widgets/basewidget.hpp"
#include "widgets/helper/rippleeffectlabel.hpp"
#include "widgets/scrollbar.hpp"
#include <QPaintEvent>
#include <QScroller>
#include <QTimer>
#include <QWheelEvent>
#include <QWidget>
#include <boost/signals2.hpp>
namespace chatterino {
namespace widgets {
struct SelectionItem {
int messageIndex;
int charIndex;
SelectionItem()
{
messageIndex = charIndex = 0;
}
SelectionItem(int _messageIndex, int _charIndex)
{
this->messageIndex = _messageIndex;
this->charIndex = _charIndex;
}
bool isSmallerThan(const SelectionItem &other) const
{
return this->messageIndex < other.messageIndex ||
(this->messageIndex == other.messageIndex && this->charIndex < other.charIndex);
}
bool equals(const SelectionItem &other) const
{
return this->messageIndex == other.messageIndex && this->charIndex == other.charIndex;
}
};
struct Selection {
SelectionItem start;
SelectionItem end;
SelectionItem min;
SelectionItem max;
Selection()
{
}
Selection(const SelectionItem &start, const SelectionItem &end)
: start(start)
, end(end)
, min(start)
, max(end)
{
if (max.isSmallerThan(min)) {
std::swap(this->min, this->max);
}
}
bool isEmpty() const
{
return this->start.equals(this->end);
}
bool isSingleMessage() const
{
return this->min.messageIndex == this->max.messageIndex;
}
};
class ChannelView : public BaseWidget
{
Q_OBJECT
public:
explicit ChannelView(WindowManager &windowManager, BaseWidget *parent = 0);
~ChannelView();
void updateGifEmotes();
void queueUpdate();
ScrollBar &getScrollBar();
QString getSelectedText();
bool hasSelection();
void clearSelection();
void setChannel(std::shared_ptr<Channel> channel);
messages::LimitedQueueSnapshot<messages::SharedMessageRef> getMessagesSnapshot();
void layoutMessages();
void clearMessages();
boost::signals2::signal<void(QMouseEvent *)> mouseDown;
boost::signals2::signal<void()> selectionChanged;
protected:
virtual void resizeEvent(QResizeEvent *) override;
virtual void paintEvent(QPaintEvent *) override;
virtual void wheelEvent(QWheelEvent *event) override;
virtual void mouseMoveEvent(QMouseEvent *event) override;
virtual void mousePressEvent(QMouseEvent *event) override;
virtual void mouseReleaseEvent(QMouseEvent *event) override;
bool tryGetMessageAt(QPoint p, std::shared_ptr<messages::MessageRef> &message,
QPoint &relativePos, int &index);
private:
struct GifEmoteData {
messages::LazyLoadedImage *image;
QRect rect;
};
WindowManager &windowManager;
QTimer updateTimer;
bool updateQueued = false;
void detachChannel();
void actuallyLayoutMessages();
void drawMessages(QPainter &painter);
void updateMessageBuffer(messages::MessageRef *messageRef, QPixmap *buffer, int messageIndex);
void drawMessageSelection(QPainter &painter, messages::MessageRef *messageRef, int messageIndex,
int bufferHeight);
void setSelection(const SelectionItem &start, const SelectionItem &end);
std::shared_ptr<Channel> channel;
std::vector<GifEmoteData> gifEmotes;
ScrollBar scrollBar;
RippleEffectLabel *goToBottom;
// This variable can be used to decide whether or not we should render the "Show latest
// messages" button
bool showingLatestMessages = true;
AccountPopupWidget userPopupWidget;
bool onlyUpdateEmotes = false;
// Mouse event variables
bool isMouseDown = false;
QPointF lastPressPosition;
Selection selection;
bool selecting = false;
messages::LimitedQueue<messages::SharedMessageRef> messages;
boost::signals2::connection messageAppendedConnection;
boost::signals2::connection messageRemovedConnection;
boost::signals2::connection repaintGifsConnection;
boost::signals2::connection layoutConnection;
private slots:
void wordTypeMaskChanged()
{
layoutMessages();
update();
}
};
} // namespace widgets
} // namespace chatterino
+53
View File
@@ -0,0 +1,53 @@
#include "widgets/helper/droppreview.hpp"
#include "colorscheme.hpp"
#include <QDebug>
#include <QPainter>
namespace chatterino {
namespace widgets {
NotebookPageDropPreview::NotebookPageDropPreview(BaseWidget *parent)
: BaseWidget(parent)
, positionAnimation(this, "geometry")
{
this->positionAnimation.setEasingCurve(QEasingCurve(QEasingCurve::InCubic));
this->setHidden(true);
}
void NotebookPageDropPreview::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.fillRect(8, 8, this->width() - 17, this->height() - 17,
this->colorScheme.DropPreviewBackground);
}
void NotebookPageDropPreview::hideEvent(QHideEvent *)
{
this->animate = false;
}
void NotebookPageDropPreview::setBounds(const QRect &rect)
{
if (rect == this->desiredGeometry) {
return;
}
if (this->animate) {
this->positionAnimation.stop();
this->positionAnimation.setDuration(50);
this->positionAnimation.setStartValue(this->geometry());
this->positionAnimation.setEndValue(rect);
this->positionAnimation.start();
} else {
this->setGeometry(rect);
}
this->desiredGeometry = rect;
this->animate = true;
}
} // namespace widgets
} // namespace chatterino
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include "widgets/basewidget.hpp"
#include <QPropertyAnimation>
#include <QWidget>
namespace chatterino {
namespace widgets {
class NotebookPageDropPreview : public BaseWidget
{
public:
NotebookPageDropPreview(BaseWidget *parent);
void setBounds(const QRect &rect);
protected:
virtual void paintEvent(QPaintEvent *) override;
virtual void hideEvent(QHideEvent *) override;
QPropertyAnimation positionAnimation;
QRect desiredGeometry;
bool animate = false;
};
} // namespace widgets
} // namespace chatterino
+106
View File
@@ -0,0 +1,106 @@
#include "widgets/helper/notebookbutton.hpp"
#include "colorscheme.hpp"
#include "widgets/helper/rippleeffectbutton.hpp"
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QRadialGradient>
namespace chatterino {
namespace widgets {
NotebookButton::NotebookButton(BaseWidget *parent)
: RippleEffectButton(parent)
{
setMouseEffectColor(QColor(0, 0, 0));
}
void NotebookButton::paintEvent(QPaintEvent *)
{
QPainter painter(this);
QColor background;
QColor foreground;
background = this->colorScheme.TabBackground;
if (mouseDown) {
// background = this->colorScheme.TabSelectedBackground;
foreground = this->colorScheme.TabHoverText;
} else if (mouseOver) {
// background = this->colorScheme.TabHoverText;
foreground = this->colorScheme.TabHoverText;
} else {
// background = this->colorScheme.TabPanelBackground;
foreground = QColor(70, 80, 80);
}
painter.setPen(Qt::NoPen);
painter.fillRect(this->rect(), background);
float h = height(), w = width();
if (icon == IconPlus) {
painter.fillRect(
QRectF((h / 12) * 2 + 1, (h / 12) * 5 + 1, w - ((h / 12) * 5), (h / 12) * 1),
foreground);
painter.fillRect(
QRectF((h / 12) * 5 + 1, (h / 12) * 2 + 1, (h / 12) * 1, w - ((h / 12) * 5)),
foreground);
} else if (icon == IconUser) {
painter.setRenderHint(QPainter::Antialiasing);
painter.setRenderHint(QPainter::HighQualityAntialiasing);
auto a = w / 8;
QPainterPath path;
path.arcMoveTo(a, 4 * a, 6 * a, 6 * a, 0);
path.arcTo(a, 4 * a, 6 * a, 6 * a, 0, 180);
painter.fillPath(path, foreground);
painter.setBrush(background);
painter.drawEllipse(2 * a, 1 * a, 4 * a, 4 * a);
painter.setBrush(foreground);
painter.drawEllipse(2.5 * a, 1.5 * a, 3 * a + 1, 3 * a);
} else // IconSettings
{
painter.setRenderHint(QPainter::Antialiasing);
painter.setRenderHint(QPainter::HighQualityAntialiasing);
auto a = w / 8;
QPainterPath path;
path.arcMoveTo(a, a, 6 * a, 6 * a, 0 - (360 / 32.0));
for (int i = 0; i < 8; i++) {
path.arcTo(a, a, 6 * a, 6 * a, i * (360 / 8.0) - (360 / 32.0), (360 / 32.0));
path.arcTo(2 * a, 2 * a, 4 * a, 4 * a, i * (360 / 8.0) + (360 / 32.0), (360 / 32.0));
}
painter.fillPath(path, foreground);
painter.setBrush(background);
painter.drawEllipse(3 * a, 3 * a, 2 * a, 2 * a);
}
fancyPaint(painter);
}
void NotebookButton::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
mouseDown = false;
update();
emit clicked();
}
RippleEffectButton::mouseReleaseEvent(event);
}
} // namespace widgets
} // namespace chatterino
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include "rippleeffectbutton.hpp"
#include <QWidget>
namespace chatterino {
namespace widgets {
class NotebookButton : public RippleEffectButton
{
Q_OBJECT
public:
static const int IconPlus = 0;
static const int IconUser = 1;
static const int IconSettings = 2;
int icon = 0;
NotebookButton(BaseWidget *parent);
protected:
void paintEvent(QPaintEvent *) override;
void mouseReleaseEvent(QMouseEvent *event) override;
signals:
void clicked();
private:
QPoint mousePos;
};
} // namespace widgets
} // namespace chatterino
+319
View File
@@ -0,0 +1,319 @@
#include "widgets/helper/notebooktab.hpp"
#include "colorscheme.hpp"
#include "settingsmanager.hpp"
#include "widgets/notebook.hpp"
#include "widgets/textinputdialog.hpp"
#include <QApplication>
#include <QDebug>
#include <QPainter>
namespace chatterino {
namespace widgets {
NotebookTab::NotebookTab(Notebook *_notebook)
: BaseWidget(_notebook)
, positionChangedAnimation(this, "pos")
, notebook(_notebook)
, menu(this)
{
this->calcSize();
this->setAcceptDrops(true);
this->positionChangedAnimation.setEasingCurve(QEasingCurve(QEasingCurve::InCubic));
this->hideXConnection = SettingsManager::getInstance().hideTabX.valueChanged.connect(
boost::bind(&NotebookTab::hideTabXChanged, this, _1));
this->setMouseTracking(true);
this->menu.addAction("Rename", [this]() {
TextInputDialog d(this);
d.setWindowTitle("Change tab title (Leave empty for default behaviour)");
if (this->useDefaultBehaviour) {
d.setText("");
} else {
d.setText(this->getTitle());
}
if (d.exec() == QDialog::Accepted) {
QString newTitle = d.getText();
if (newTitle.isEmpty()) {
this->useDefaultBehaviour = true;
this->page->refreshTitle();
} else {
this->useDefaultBehaviour = false;
this->setTitle(newTitle);
}
}
});
this->menu.addAction("Close", [=]() {
this->notebook->removePage(this->page);
qDebug() << "lmoa";
});
this->menu.addAction("Enable highlights on new message", []() {
qDebug() << "TODO: Implement"; //
});
}
NotebookTab::~NotebookTab()
{
this->hideXConnection.disconnect();
}
void NotebookTab::calcSize()
{
float scale = getDpiMultiplier();
if (SettingsManager::getInstance().hideTabX.get()) {
this->resize(static_cast<int>((fontMetrics().width(title) + 16) * scale),
static_cast<int>(24 * scale));
} else {
this->resize(static_cast<int>((fontMetrics().width(title) + 8 + 24) * scale),
static_cast<int>(24 * scale));
}
if (this->parent() != nullptr) {
(static_cast<Notebook *>(this->parent()))->performLayout(true);
}
}
const QString &NotebookTab::getTitle() const
{
return title;
}
void NotebookTab::setTitle(const QString &newTitle)
{
this->title = newTitle;
this->calcSize();
}
bool NotebookTab::isSelected() const
{
return this->selected;
}
void NotebookTab::setSelected(bool value)
{
this->selected = value;
this->update();
}
NotebookTab::HighlightStyle NotebookTab::getHighlightStyle() const
{
return this->highlightStyle;
}
void NotebookTab::setHighlightStyle(HighlightStyle newHighlightStyle)
{
this->highlightStyle = newHighlightStyle;
this->update();
}
QRect NotebookTab::getDesiredRect() const
{
return QRect(positionAnimationDesiredPoint, size());
}
void NotebookTab::hideTabXChanged(bool)
{
this->calcSize();
this->update();
}
void NotebookTab::moveAnimated(QPoint pos, bool animated)
{
this->positionAnimationDesiredPoint = pos;
QWidget *w = this->window();
if ((w != nullptr && !w->isVisible()) || !animated || !positionChangedAnimationRunning) {
this->move(pos);
this->positionChangedAnimationRunning = true;
return;
}
if (this->positionChangedAnimation.endValue() == pos) {
return;
}
this->positionChangedAnimation.stop();
this->positionChangedAnimation.setDuration(75);
this->positionChangedAnimation.setStartValue(this->pos());
this->positionChangedAnimation.setEndValue(pos);
this->positionChangedAnimation.start();
}
void NotebookTab::paintEvent(QPaintEvent *)
{
QPainter painter(this);
QColor fg = QColor(0, 0, 0);
if (this->selected) {
if (this->window() == QApplication::activeWindow()) {
painter.fillRect(rect(), this->colorScheme.TabSelectedBackground);
fg = this->colorScheme.TabSelectedText;
} else {
painter.fillRect(rect(), this->colorScheme.TabSelectedUnfocusedBackground);
fg = this->colorScheme.TabSelectedUnfocusedText;
}
} else if (this->mouseOver) {
painter.fillRect(rect(), this->colorScheme.TabHoverBackground);
fg = this->colorScheme.TabHoverText;
} else if (this->highlightStyle == HighlightHighlighted) {
painter.fillRect(rect(), this->colorScheme.TabHighlightedBackground);
fg = this->colorScheme.TabHighlightedText;
} else if (this->highlightStyle == HighlightNewMessage) {
painter.fillRect(rect(), this->colorScheme.TabNewMessageBackground);
fg = this->colorScheme.TabHighlightedText;
} else {
painter.fillRect(rect(), this->colorScheme.TabBackground);
fg = this->colorScheme.TabText;
}
painter.setPen(fg);
float scale = this->getDpiMultiplier();
int rectW = (SettingsManager::getInstance().hideTabX.get() ? 0 : static_cast<int>(16) * scale);
QRect rect(0, 0, this->width() - rectW, this->height());
painter.drawText(rect, title, QTextOption(Qt::AlignCenter));
if (!SettingsManager::getInstance().hideTabX.get() && (mouseOver || selected)) {
QRect xRect = this->getXRect();
if (mouseOverX) {
painter.fillRect(xRect, QColor(0, 0, 0, 64));
if (mouseDownX) {
painter.fillRect(xRect, QColor(0, 0, 0, 64));
}
}
int a = static_cast<int>(scale * 4);
painter.drawLine(xRect.topLeft() + QPoint(a, a), xRect.bottomRight() + QPoint(-a, -a));
painter.drawLine(xRect.topRight() + QPoint(-a, a), xRect.bottomLeft() + QPoint(a, -a));
}
}
void NotebookTab::mousePressEvent(QMouseEvent *event)
{
this->mouseDown = true;
this->mouseDownX = this->getXRect().contains(event->pos());
this->update();
this->notebook->select(page);
switch (event->button()) {
case Qt::RightButton: {
this->menu.popup(event->globalPos());
} break;
}
}
void NotebookTab::mouseReleaseEvent(QMouseEvent *event)
{
this->mouseDown = false;
if (event->button() == Qt::MiddleButton) {
if (this->rect().contains(event->pos())) {
this->notebook->removePage(this->page);
}
} else {
if (!SettingsManager::getInstance().hideTabX.get() && this->mouseDownX &&
this->getXRect().contains(event->pos())) {
this->mouseDownX = false;
this->notebook->removePage(this->page);
} else {
this->update();
}
}
}
void NotebookTab::enterEvent(QEvent *)
{
this->mouseOver = true;
this->update();
}
void NotebookTab::leaveEvent(QEvent *)
{
this->mouseOverX = false;
this->mouseOver = false;
this->update();
}
void NotebookTab::dragEnterEvent(QDragEnterEvent *)
{
this->notebook->select(this->page);
}
void NotebookTab::mouseMoveEvent(QMouseEvent *event)
{
if (!SettingsManager::getInstance().hideTabX.get()) {
bool overX = this->getXRect().contains(event->pos());
if (overX != this->mouseOverX) {
// Over X state has been changed (we either left or entered it;
this->mouseOverX = overX;
this->update();
}
}
if (this->mouseDown && !this->getDesiredRect().contains(event->pos())) {
QPoint relPoint = this->mapToParent(event->pos());
int index;
SplitContainer *clickedPage = notebook->tabAt(relPoint, index);
if (clickedPage != nullptr && clickedPage != this->page) {
this->notebook->rearrangePage(clickedPage, index);
}
}
}
void NotebookTab::load(const boost::property_tree::ptree &tree)
{
// Load tab title
try {
QString newTitle = QString::fromStdString(tree.get<std::string>("title"));
if (newTitle.isEmpty()) {
this->useDefaultBehaviour = true;
} else {
this->setTitle(newTitle);
this->useDefaultBehaviour = false;
}
} catch (boost::property_tree::ptree_error) {
}
}
boost::property_tree::ptree NotebookTab::save()
{
boost::property_tree::ptree tree;
if (this->useDefaultBehaviour) {
tree.put("title", "");
} else {
tree.put("title", this->getTitle().toStdString());
}
return tree;
}
} // namespace widgets
} // namespace chatterino
+97
View File
@@ -0,0 +1,97 @@
#pragma once
#include "widgets/basewidget.hpp"
#include <QMenu>
#include <QPropertyAnimation>
#include <boost/property_tree/ptree.hpp>
#include <boost/signals2.hpp>
#include <boost/signals2/connection.hpp>
namespace chatterino {
class ColorScheme;
namespace widgets {
class Notebook;
class SplitContainer;
class NotebookTab : public BaseWidget
{
Q_OBJECT
public:
enum HighlightStyle { HighlightNone, HighlightHighlighted, HighlightNewMessage };
explicit NotebookTab(Notebook *_notebook);
~NotebookTab();
void calcSize();
SplitContainer *page;
const QString &getTitle() const;
void setTitle(const QString &newTitle);
bool isSelected() const;
void setSelected(bool value);
HighlightStyle getHighlightStyle() const;
void setHighlightStyle(HighlightStyle style);
void moveAnimated(QPoint pos, bool animated = true);
QRect getDesiredRect() const;
void hideTabXChanged(bool);
protected:
void paintEvent(QPaintEvent *) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void enterEvent(QEvent *) override;
void leaveEvent(QEvent *) override;
void dragEnterEvent(QDragEnterEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
private:
boost::signals2::connection hideXConnection;
QPropertyAnimation positionChangedAnimation;
bool positionChangedAnimationRunning = false;
QPoint positionAnimationDesiredPoint;
Notebook *notebook;
QString title;
public:
bool useDefaultBehaviour = true;
private:
bool selected = false;
bool mouseOver = false;
bool mouseDown = false;
bool mouseOverX = false;
bool mouseDownX = false;
HighlightStyle highlightStyle = HighlightStyle::HighlightNone;
QMenu menu;
QRect getXRect()
{
float scale = this->getDpiMultiplier();
return QRect(this->width() - static_cast<int>(20 * scale), static_cast<int>(4 * scale),
static_cast<int>(16 * scale), static_cast<int>(16 * scale));
}
public:
void load(const boost::property_tree::ptree &tree);
boost::property_tree::ptree save();
};
} // namespace widgets
} // namespace chatterino
+163
View File
@@ -0,0 +1,163 @@
#include "widgets/helper/resizingtextedit.hpp"
ResizingTextEdit::ResizingTextEdit()
{
auto sizePolicy = this->sizePolicy();
sizePolicy.setHeightForWidth(true);
sizePolicy.setVerticalPolicy(QSizePolicy::Preferred);
this->setSizePolicy(sizePolicy);
this->setAcceptRichText(false);
QObject::connect(this, &QTextEdit::textChanged, this, &QWidget::updateGeometry);
this->setFocusPolicy(Qt::ClickFocus);
}
QSize ResizingTextEdit::sizeHint() const
{
return QSize(this->width(), this->heightForWidth(this->width()));
}
bool ResizingTextEdit::hasHeightForWidth() const
{
return true;
}
int ResizingTextEdit::heightForWidth(int) const
{
auto margins = this->contentsMargins();
return margins.top() + document()->size().height() + margins.bottom() + 5;
}
QString ResizingTextEdit::textUnderCursor(bool *hadSpace) const
{
auto currentText = this->toPlainText();
QTextCursor tc = this->textCursor();
auto textUpToCursor = currentText.left(tc.selectionStart());
auto words = textUpToCursor.splitRef(' ');
if (words.size() == 0) {
return QString();
}
bool first = true;
QString lastWord;
for (auto it = words.crbegin(); it != words.crend(); ++it) {
auto word = *it;
if (first && word.isEmpty()) {
first = false;
if (hadSpace != nullptr) {
*hadSpace = true;
}
continue;
}
lastWord = word.toString();
break;
}
if (lastWord.isEmpty()) {
return QString();
}
return lastWord;
}
void ResizingTextEdit::keyPressEvent(QKeyEvent *event)
{
event->ignore();
this->keyPressed(event);
if (event->key() == Qt::Key_Backtab) {
// Ignore for now. We want to use it for autocomplete later
return;
}
if (event->key() == Qt::Key_Tab &&
(event->modifiers() & Qt::ControlModifier) == Qt::NoModifier) {
QString currentCompletionPrefix = this->textUnderCursor();
if (!currentCompletionPrefix.size()) {
return;
}
if (!this->nextCompletion) {
// first selection
this->completer->setCompletionPrefix(currentCompletionPrefix);
this->nextCompletion = true;
this->completer->complete();
return;
}
// scrolling through selections
if (!this->completer->setCurrentRow(this->completer->currentRow() + 1)) {
// wrap over and start again
this->completer->setCurrentRow(0);
}
this->completer->complete();
return;
}
// (hemirt)
// this resets the selection in the completion list, it should probably only trigger on actual
// chat input (space, character) and not on every key input (pressing alt for example)
this->nextCompletion = false;
if (!event->isAccepted()) {
QTextEdit::keyPressEvent(event);
}
}
void ResizingTextEdit::setCompleter(QCompleter *c)
{
if (this->completer) {
QObject::disconnect(this->completer, 0, this, 0);
}
this->completer = c;
if (!this->completer) {
return;
}
this->completer->setWidget(this);
this->completer->setCompletionMode(QCompleter::InlineCompletion);
this->completer->setCaseSensitivity(Qt::CaseInsensitive);
/*QObject::connect(this->completer, SIGNAL(highlighted(QString)), this,
SLOT(insertCompletion(QString)));
*/
QObject::connect(completer,
static_cast<void (QCompleter::*)(const QString &)>(&QCompleter::highlighted),
this, &ResizingTextEdit::insertCompletion);
}
void ResizingTextEdit::insertCompletion(const QString &completion)
{
if (this->completer->widget() != this) {
return;
}
bool hadSpace = false;
auto prefix = this->textUnderCursor(&hadSpace);
int prefixSize = prefix.size();
if (hadSpace) {
++prefixSize;
}
QTextCursor tc = this->textCursor();
tc.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, prefixSize);
tc.insertText(completion);
this->setTextCursor(tc);
}
QCompleter *ResizingTextEdit::getCompleter() const
{
return this->completer;
}
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <QCompleter>
#include <QKeyEvent>
#include <QTextEdit>
#include <boost/signals2.hpp>
class ResizingTextEdit : public QTextEdit
{
public:
ResizingTextEdit();
QSize sizeHint() const override;
bool hasHeightForWidth() const override;
boost::signals2::signal<void(QKeyEvent *)> keyPressed;
void setCompleter(QCompleter *c);
QCompleter *getCompleter() const;
protected:
virtual int heightForWidth(int) const override;
virtual void keyPressEvent(QKeyEvent *event) override;
private:
QCompleter *completer = nullptr;
// hadSpace is set to true in case the "textUnderCursor" word was after a space
QString textUnderCursor(bool *hadSpace = nullptr) const;
bool nextCompletion = false;
private slots:
void insertCompletion(const QString &completion);
};
+141
View File
@@ -0,0 +1,141 @@
#include "rippleeffectbutton.hpp"
#include <QDebug>
#include <QPainter>
namespace chatterino {
namespace widgets {
RippleEffectButton::RippleEffectButton(BaseWidget *parent)
: BaseWidget(parent)
{
connect(&effectTimer, &QTimer::timeout, this, &RippleEffectButton::onMouseEffectTimeout);
this->effectTimer.setInterval(20);
this->effectTimer.start();
}
void RippleEffectButton::setMouseEffectColor(QColor color)
{
this->mouseEffectColor = color;
}
void RippleEffectButton::paintEvent(QPaintEvent *)
{
QPainter painter(this);
this->fancyPaint(painter);
}
void RippleEffectButton::fancyPaint(QPainter &painter)
{
QColor &c = this->mouseEffectColor;
if (this->hoverMultiplier > 0) {
QRadialGradient gradient(mousePos.x(), mousePos.y(), 50, mousePos.x(), mousePos.y());
gradient.setColorAt(
0, QColor(c.red(), c.green(), c.blue(), (int)(24 * this->hoverMultiplier)));
gradient.setColorAt(
1, QColor(c.red(), c.green(), c.blue(), (int)(12 * this->hoverMultiplier)));
painter.fillRect(this->rect(), gradient);
}
for (auto effect : this->clickEffects) {
QRadialGradient gradient(effect.position.x(), effect.position.y(),
effect.progress * (float)width() * 2, effect.position.x(),
effect.position.y());
gradient.setColorAt(
0, QColor(c.red(), c.green(), c.blue(), (int)((1 - effect.progress) * 95)));
gradient.setColorAt(
0.9999, QColor(c.red(), c.green(), c.blue(), (int)((1 - effect.progress) * 95)));
gradient.setColorAt(1, QColor(c.red(), c.green(), c.blue(), (int)(0)));
painter.fillRect(this->rect(), gradient);
}
}
void RippleEffectButton::enterEvent(QEvent *)
{
this->mouseOver = true;
}
void RippleEffectButton::leaveEvent(QEvent *)
{
this->mouseOver = false;
}
void RippleEffectButton::mousePressEvent(QMouseEvent *event)
{
if (event->button() != Qt::LeftButton) {
return;
}
this->clickEffects.push_back(ClickEffect(event->pos()));
this->mouseDown = true;
}
void RippleEffectButton::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() != Qt::LeftButton) {
return;
}
this->mouseDown = false;
if (this->rect().contains(event->pos())) {
emit clicked();
}
}
void RippleEffectButton::mouseMoveEvent(QMouseEvent *event)
{
this->mousePos = event->pos();
}
void RippleEffectButton::onMouseEffectTimeout()
{
bool performUpdate = false;
if (selected) {
if (this->hoverMultiplier != 0) {
this->hoverMultiplier = std::max(0.0, this->hoverMultiplier - 0.1);
performUpdate = true;
}
} else if (mouseOver) {
if (this->hoverMultiplier != 1) {
this->hoverMultiplier = std::min(1.0, this->hoverMultiplier + 0.5);
performUpdate = true;
}
} else {
if (this->hoverMultiplier != 0) {
this->hoverMultiplier = std::max(0.0, this->hoverMultiplier - 0.3);
performUpdate = true;
}
}
if (this->clickEffects.size() != 0) {
performUpdate = true;
for (auto it = this->clickEffects.begin(); it != this->clickEffects.end();) {
(*it).progress += mouseDown ? 0.02 : 0.07;
if ((*it).progress >= 1.0) {
it = this->clickEffects.erase(it);
} else {
it++;
}
}
}
if (performUpdate) {
update();
}
}
} // namespace widgets
} // namespace chatterino
+61
View File
@@ -0,0 +1,61 @@
#pragma once
#include "widgets/basewidget.hpp"
#include <QMouseEvent>
#include <QPainter>
#include <QPoint>
#include <QTimer>
#include <QWidget>
namespace chatterino {
namespace widgets {
class RippleEffectButton : public BaseWidget
{
Q_OBJECT
struct ClickEffect {
double progress = 0.0;
QPoint position;
ClickEffect(QPoint _position)
: position(_position)
{
}
};
public:
RippleEffectButton(BaseWidget *parent);
void setMouseEffectColor(QColor color);
signals:
void clicked();
protected:
bool selected = false;
bool mouseOver = false;
bool mouseDown = false;
virtual void paintEvent(QPaintEvent *) override;
virtual void enterEvent(QEvent *) override;
virtual void leaveEvent(QEvent *) override;
virtual void mousePressEvent(QMouseEvent *event) override;
virtual void mouseReleaseEvent(QMouseEvent *event) override;
virtual void mouseMoveEvent(QMouseEvent *event) override;
void fancyPaint(QPainter &painter);
private:
QPoint mousePos;
double hoverMultiplier = 0.0;
QTimer effectTimer;
std::vector<ClickEffect> clickEffects;
QColor mouseEffectColor = {255, 255, 255};
void onMouseEffectTimeout();
};
} // namespace widgets
} // namespace chatterino
+28
View File
@@ -0,0 +1,28 @@
#include "widgets/helper/rippleeffectlabel.hpp"
#include "colorscheme.hpp"
#include "widgets/helper/splitheader.hpp"
#include <QBrush>
#include <QPainter>
namespace chatterino {
namespace widgets {
RippleEffectLabel::RippleEffectLabel(BaseWidget *parent, int spacing)
: RippleEffectButton(parent)
, label(this)
{
setLayout(&this->hbox);
this->label.setAlignment(Qt::AlignCenter);
this->hbox.setMargin(0);
this->hbox.addSpacing(spacing);
this->hbox.addWidget(&this->label);
this->hbox.addSpacing(spacing);
this->setMouseEffectColor(QColor(255, 255, 255, 63));
}
} // namespace widgets
} // namespace chatterino
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include "widgets/basewidget.hpp"
#include "widgets/helper/rippleeffectbutton.hpp"
#include "widgets/helper/signallabel.hpp"
#include <QHBoxLayout>
#include <QLabel>
#include <QPaintEvent>
#include <QWidget>
namespace chatterino {
class ColorScheme;
namespace widgets {
class RippleEffectLabel : public RippleEffectButton
{
public:
explicit RippleEffectLabel(BaseWidget *parent, int spacing = 6);
SignalLabel &getLabel()
{
return this->label;
}
private:
QHBoxLayout hbox;
SignalLabel label;
};
} // namespace widgets
} // namespace chatterino
+19
View File
@@ -0,0 +1,19 @@
#include "widgets/helper/scrollbarhighlight.hpp"
#include "colorscheme.hpp"
#include "widgets/scrollbar.hpp"
namespace chatterino {
namespace widgets {
ScrollBarHighlight::ScrollBarHighlight(double _position, int _colorIndex, ScrollBar *parent,
Style _style, QString _tag)
: colorScheme(parent->colorScheme)
, position(_position)
, colorIndex(std::max(0, std::min(this->colorScheme.HighlightColorCount, _colorIndex)))
, style(_style)
, tag(_tag)
{
}
} // namespace widgets
} // namespace chatterino
+53
View File
@@ -0,0 +1,53 @@
#pragma once
#include "QString"
namespace chatterino {
class ColorScheme;
namespace widgets {
class ScrollBar;
class ScrollBarHighlight
{
public:
enum Style { Default, Left, Right, SingleLine };
ScrollBarHighlight(double _position, int _colorIndex, ScrollBar *parent, Style _style = Default,
QString _tag = "");
ColorScheme &colorScheme;
double getPosition()
{
return this->position;
}
int getColorIndex()
{
return this->colorIndex;
}
Style getStyle()
{
return this->style;
}
QString getTag()
{
return this->tag;
}
ScrollBarHighlight *next = nullptr;
private:
double position;
int colorIndex;
Style style;
QString tag;
};
} // namespace widgets
} // namespace chatterino
+74
View File
@@ -0,0 +1,74 @@
#include "widgets/helper/settingsdialogtab.hpp"
#include "widgets/settingsdialog.hpp"
#include <QPainter>
#include <QStyleOption>
namespace chatterino {
namespace widgets {
SettingsDialogTab::SettingsDialogTab(SettingsDialog *_dialog, QString _labelText,
QString imageFileName)
: dialog(_dialog)
{
this->ui.labelText = _labelText;
this->ui.image.load(imageFileName);
// XXX: DPI (not sure if this is auto-adjusted with custom DPI)
this->setFixedHeight(32);
this->setCursor(QCursor(Qt::PointingHandCursor));
this->setStyleSheet("color: #FFF");
}
void SettingsDialogTab::setSelected(bool _selected)
{
if (this->selected == _selected) {
return;
}
this->selected = _selected;
emit selectedChanged(selected);
}
QWidget *SettingsDialogTab::getWidget()
{
return this->ui.widget;
}
void SettingsDialogTab::setWidget(QWidget *widget)
{
this->ui.widget = widget;
}
void SettingsDialogTab::paintEvent(QPaintEvent *)
{
QPainter painter(this);
QStyleOption opt;
opt.init(this);
this->style()->drawPrimitive(QStyle::PE_Widget, &opt, &painter, this);
int a = (this->height() - this->ui.image.width()) / 2;
painter.drawImage(a, a, this->ui.image);
a = a + a + this->ui.image.width();
painter.drawText(QRect(a, 0, width() - a, height()), this->ui.labelText,
QTextOption(Qt::AlignLeft | Qt::AlignVCenter));
}
void SettingsDialogTab::mousePressEvent(QMouseEvent *event)
{
if (event->button() != Qt::LeftButton) {
return;
}
this->dialog->select(this);
}
} // namespace widgets
} // namespace chatterino
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include <QPaintEvent>
#include <QWidget>
namespace chatterino {
namespace widgets {
class SettingsDialog;
class SettingsDialogTab : public QWidget
{
Q_OBJECT
public:
SettingsDialogTab(SettingsDialog *dialog, QString _label, QString imageFileName);
void setSelected(bool selected);
QWidget *getWidget();
void setWidget(QWidget *widget);
signals:
void selectedChanged(bool);
private:
void paintEvent(QPaintEvent *);
void mousePressEvent(QMouseEvent *event);
struct {
QWidget *widget;
QString labelText;
QImage image;
} ui;
// Parent settings dialog
SettingsDialog *dialog;
bool selected = false;
};
} // namespace widgets
} // namespace chatterino
+53
View File
@@ -0,0 +1,53 @@
#pragma once
#include <QFlags>
#include <QLabel>
#include <QMouseEvent>
#include <QWidget>
class SignalLabel : public QLabel
{
Q_OBJECT
public:
explicit SignalLabel(QWidget *parent = 0, Qt::WindowFlags f = 0)
: QLabel(parent, f)
{
}
virtual ~SignalLabel() = default;
signals:
void mouseDoubleClick(QMouseEvent *ev);
void mouseDown();
void mouseUp();
protected:
virtual void mouseDoubleClickEvent(QMouseEvent *ev) override
{
emit this->mouseDoubleClick(ev);
}
virtual void mousePressEvent(QMouseEvent *event) override
{
if (event->button() == Qt::LeftButton) {
emit mouseDown();
}
event->ignore();
}
void mouseReleaseEvent(QMouseEvent *event) override
{
if (event->button() == Qt::LeftButton) {
emit mouseUp();
}
event->ignore();
}
virtual void mouseMoveEvent(QMouseEvent *event) override
{
event->ignore();
}
};
+9
View File
@@ -0,0 +1,9 @@
#include "splitcolumn.hpp"
namespace chatterino {
namespace helper {
SplitColumn::SplitColumn()
{
}
}
}
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <vector>
#include "widgets/split.hpp"
namespace chatterino {
namespace helper {
class SplitColumn
{
public:
SplitColumn();
void insert(widgets::Split *split, int index = -1);
void remove(int index);
double getFlex();
void setFlex(double flex);
private:
std::vector<widgets::Split> items;
};
}
}
+221
View File
@@ -0,0 +1,221 @@
#include "widgets/helper/splitheader.hpp"
#include "colorscheme.hpp"
#include "twitch/twitchchannel.hpp"
#include "util/urlfetch.hpp"
#include "widgets/split.hpp"
#include "widgets/splitcontainer.hpp"
#include <QByteArray>
#include <QDrag>
#include <QMimeData>
#include <QPainter>
namespace chatterino {
namespace widgets {
SplitHeader::SplitHeader(Split *_chatWidget)
: BaseWidget(_chatWidget)
, chatWidget(_chatWidget)
, leftLabel(this)
, leftMenu(this)
, rightLabel(this)
, rightMenu(this)
{
this->refreshTheme();
this->updateChannelText();
this->setLayout(&this->hbox);
this->hbox.setMargin(0);
this->hbox.addWidget(&this->leftLabel);
this->hbox.addWidget(&this->channelNameLabel, 1);
this->hbox.addWidget(&this->rightLabel);
// left
this->leftLabel.getLabel().setTextFormat(Qt::RichText);
this->leftLabel.getLabel().setText("<img src=':/images/tool_moreCollapser_off16.png' />");
connect(&this->leftLabel, &RippleEffectLabel::clicked, this, &SplitHeader::leftButtonClicked);
this->leftMenu.addAction("Add new split", this->chatWidget, &Split::doAddSplit,
QKeySequence(tr("Ctrl+T")));
this->leftMenu.addAction("Close split", this->chatWidget, &Split::doCloseSplit,
QKeySequence(tr("Ctrl+W")));
this->leftMenu.addAction("Move split", this, SLOT(menuMoveSplit()));
this->leftMenu.addAction("Popup", this->chatWidget, &Split::doPopup);
this->leftMenu.addAction("Open viewer list", this->chatWidget, &Split::doOpenViewerList);
this->leftMenu.addSeparator();
this->leftMenu.addAction("Change channel", this->chatWidget, &Split::doChangeChannel,
QKeySequence(tr("Ctrl+R")));
this->leftMenu.addAction("Clear chat", this->chatWidget, &Split::doClearChat);
this->leftMenu.addAction("Open channel", this->chatWidget, &Split::doOpenChannel);
this->leftMenu.addAction("Open popup player", this->chatWidget, &Split::doOpenPopupPlayer);
this->leftMenu.addAction("Open in Streamlink", this->chatWidget, &Split::doOpenStreamlink);
this->leftMenu.addSeparator();
this->leftMenu.addAction("Reload channel emotes", this, SLOT(menuReloadChannelEmotes()));
this->leftMenu.addAction("Manual reconnect", this, SLOT(menuManualReconnect()));
this->leftMenu.addSeparator();
this->leftMenu.addAction("Show changelog", this, SLOT(menuShowChangelog()));
// middle
this->channelNameLabel.setAlignment(Qt::AlignCenter);
connect(&this->channelNameLabel, &SignalLabel::mouseDoubleClick, this,
&SplitHeader::mouseDoubleClickEvent);
// right
this->rightLabel.setMinimumWidth(this->height());
this->rightLabel.getLabel().setTextFormat(Qt::RichText);
this->rightLabel.getLabel().setText("ayy");
this->initializeChannelSignals();
this->chatWidget->channelChanged.connect([this]() {
this->initializeChannelSignals(); //
});
}
void SplitHeader::initializeChannelSignals()
{
// Disconnect any previous signal first
this->onlineStatusChangedConnection.disconnect();
auto channel = this->chatWidget->getChannel();
twitch::TwitchChannel *twitchChannel = dynamic_cast<twitch::TwitchChannel *>(channel.get());
if (twitchChannel) {
twitchChannel->onlineStatusChanged.connect([this]() {
this->updateChannelText(); //
});
}
}
void SplitHeader::resizeEvent(QResizeEvent *event)
{
this->setFixedHeight(static_cast<float>(28 * getDpiMultiplier()));
}
void SplitHeader::updateChannelText()
{
const std::string channelName = this->chatWidget->channelName;
if (channelName.empty()) {
this->channelNameLabel.setText("<no channel>");
} else {
auto channel = this->chatWidget->getChannel();
twitch::TwitchChannel *twitchChannel = dynamic_cast<twitch::TwitchChannel *>(channel.get());
if (twitchChannel != nullptr && twitchChannel->isLive) {
this->channelNameLabel.setText(QString::fromStdString(channelName) + " (live)");
this->setToolTip("<style>.center { text-align: center; }</style>"
"<p class = \"center\">" +
twitchChannel->streamStatus + "<br><br>" + twitchChannel->streamGame +
"<br>"
"Live for " +
twitchChannel->streamUptime + " with " +
twitchChannel->streamViewerCount + " viewers"
"</p>");
} else {
this->channelNameLabel.setText(QString::fromStdString(channelName));
this->setToolTip("");
}
}
}
void SplitHeader::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.fillRect(rect(), this->colorScheme.ChatHeaderBackground);
painter.setPen(this->colorScheme.ChatHeaderBorder);
painter.drawRect(0, 0, width() - 1, height() - 1);
}
void SplitHeader::mousePressEvent(QMouseEvent *event)
{
this->dragging = true;
this->dragStart = event->pos();
}
void SplitHeader::mouseMoveEvent(QMouseEvent *event)
{
if (this->dragging) {
if (std::abs(this->dragStart.x() - event->pos().x()) > 12 ||
std::abs(this->dragStart.y() - event->pos().y()) > 12) {
auto page = static_cast<SplitContainer *>(this->chatWidget->parentWidget());
if (page != nullptr) {
SplitContainer::isDraggingSplit = true;
SplitContainer::draggingSplit = this->chatWidget;
auto originalLocation = page->removeFromLayout(this->chatWidget);
// page->update();
QDrag *drag = new QDrag(this->chatWidget);
QMimeData *mimeData = new QMimeData;
mimeData->setData("chatterino/split", "xD");
drag->setMimeData(mimeData);
Qt::DropAction dropAction = drag->exec(Qt::MoveAction);
if (dropAction == Qt::IgnoreAction) {
page->addToLayout(this->chatWidget, originalLocation);
}
SplitContainer::isDraggingSplit = false;
}
}
}
}
void SplitHeader::mouseDoubleClickEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
this->chatWidget->doChangeChannel();
}
}
void SplitHeader::leftButtonClicked()
{
QTimer::singleShot(80, [&] {
this->leftMenu.move(this->leftLabel.mapToGlobal(QPoint(0, this->leftLabel.height())));
this->leftMenu.show();
});
}
void SplitHeader::rightButtonClicked()
{
}
void SplitHeader::refreshTheme()
{
QPalette palette;
palette.setColor(QPalette::Foreground, this->colorScheme.Text);
this->leftLabel.setPalette(palette);
this->channelNameLabel.setPalette(palette);
this->rightLabel.setPalette(palette);
}
void SplitHeader::menuMoveSplit()
{
}
void SplitHeader::menuReloadChannelEmotes()
{
}
void SplitHeader::menuManualReconnect()
{
}
void SplitHeader::menuShowChangelog()
{
}
} // namespace widgets
} // namespace chatterino
+77
View File
@@ -0,0 +1,77 @@
#pragma once
#include "widgets/basewidget.hpp"
#include "widgets/helper/rippleeffectlabel.hpp"
#include "widgets/helper/signallabel.hpp"
#include <QAction>
#include <QHBoxLayout>
#include <QLabel>
#include <QMenu>
#include <QMouseEvent>
#include <QPaintEvent>
#include <QPoint>
#include <QWidget>
#include <boost/signals2/connection.hpp>
namespace chatterino {
class ColorScheme;
namespace widgets {
class Split;
class SplitHeader : public BaseWidget
{
Q_OBJECT
public:
explicit SplitHeader(Split *_chatWidget);
// Update channel text from chat widget
void updateChannelText();
protected:
virtual void paintEvent(QPaintEvent *) override;
virtual void mousePressEvent(QMouseEvent *event) override;
virtual void mouseMoveEvent(QMouseEvent *event) override;
virtual void mouseDoubleClickEvent(QMouseEvent *event) override;
virtual void resizeEvent(QResizeEvent *event) override;
private:
Split *const chatWidget;
QPoint dragStart;
bool dragging = false;
boost::signals2::connection onlineStatusChangedConnection;
QHBoxLayout hbox;
// top left
RippleEffectLabel leftLabel;
QMenu leftMenu;
// center
SignalLabel channelNameLabel;
// top right
RippleEffectLabel rightLabel;
QMenu rightMenu;
void leftButtonClicked();
void rightButtonClicked();
virtual void refreshTheme() override;
void initializeChannelSignals();
public slots:
void menuMoveSplit();
void menuReloadChannelEmotes();
void menuManualReconnect();
void menuShowChangelog();
};
} // namespace widgets
} // namespace chatterino
+248
View File
@@ -0,0 +1,248 @@
#include "widgets/helper/splitinput.hpp"
#include "colorscheme.hpp"
#include "completionmanager.hpp"
#include "ircmanager.hpp"
#include "settingsmanager.hpp"
#include "widgets/notebook.hpp"
#include "widgets/split.hpp"
#include "widgets/splitcontainer.hpp"
#include <QCompleter>
#include <QPainter>
namespace chatterino {
namespace widgets {
SplitInput::SplitInput(Split *_chatWidget, EmoteManager &emoteManager, WindowManager &windowManager)
: BaseWidget(_chatWidget)
, chatWidget(_chatWidget)
, emoteManager(emoteManager)
, windowManager(windowManager)
, emotesLabel(this)
{
this->setMaximumHeight(150);
this->setLayout(&this->hbox);
this->hbox.setMargin(0);
this->hbox.addLayout(&this->editContainer);
this->hbox.addLayout(&this->vbox);
auto &fontManager = FontManager::getInstance();
this->textInput.setFont(fontManager.getFont(FontManager::Type::Medium));
fontManager.fontChanged.connect([this, &fontManager]() {
this->textInput.setFont(fontManager.getFont(FontManager::Type::Medium));
});
this->editContainer.addWidget(&this->textInput);
this->editContainer.setMargin(4);
this->emotesLabel.setMinimumHeight(24);
this->vbox.addWidget(&this->textLengthLabel);
this->vbox.addStretch(1);
this->vbox.addWidget(&this->emotesLabel);
this->textLengthLabel.setText("100");
this->textLengthLabel.setAlignment(Qt::AlignRight);
this->emotesLabel.getLabel().setTextFormat(Qt::RichText);
this->emotesLabel.getLabel().setText(
"<img src=':/images/Emoji_Color_1F60A_19.png' width='12' height='12' "
"/>");
connect(&this->emotesLabel, &RippleEffectLabel::clicked, [this] {
if (this->emotePopup == nullptr) {
this->emotePopup =
new EmotePopup(this->colorScheme, this->emoteManager, this->windowManager);
}
this->emotePopup->resize(300, 500);
this->emotePopup->loadChannel(this->chatWidget->getChannel());
this->emotePopup->show();
});
connect(&textInput, &ResizingTextEdit::textChanged, this, &SplitInput::editTextChanged);
this->refreshTheme();
textLengthLabel.setHidden(!SettingsManager::getInstance().showMessageLength.get());
auto completer = new QCompleter(
this->chatWidget->completionManager.createModel(this->chatWidget->channelName));
this->textInput.setCompleter(completer);
this->textInput.keyPressed.connect([this](QKeyEvent *event) {
if (event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return) {
auto c = this->chatWidget->getChannel();
if (c == nullptr) {
return;
}
QString message = textInput.toPlainText();
c->sendMessage(message.replace('\n', ' '));
prevMsg.append(message);
event->accept();
if (!(event->modifiers() == Qt::ControlModifier)) {
textInput.setText(QString());
prevIndex = 0;
} else if (textInput.toPlainText() == prevMsg.at(prevMsg.size() - 1)) {
prevMsg.removeLast();
}
prevIndex = prevMsg.size();
} else if (event->key() == Qt::Key_Up) {
if (event->modifiers() == Qt::AltModifier) {
SplitContainer *page =
static_cast<SplitContainer *>(this->chatWidget->parentWidget());
int reqX = page->currentX;
int reqY = page->lastRequestedY[reqX] - 1;
qDebug() << "Alt+Down to" << reqX << "/" << reqY;
page->requestFocus(reqX, reqY);
} else {
if (prevMsg.size() && prevIndex) {
prevIndex--;
textInput.setText(prevMsg.at(prevIndex));
}
}
} else if (event->key() == Qt::Key_Down) {
if (event->modifiers() == Qt::AltModifier) {
SplitContainer *page =
static_cast<SplitContainer *>(this->chatWidget->parentWidget());
int reqX = page->currentX;
int reqY = page->lastRequestedY[reqX] + 1;
qDebug() << "Alt+Down to" << reqX << "/" << reqY;
page->requestFocus(reqX, reqY);
} else {
if (prevIndex != (prevMsg.size() - 1) && prevIndex != prevMsg.size()) {
prevIndex++;
textInput.setText(prevMsg.at(prevIndex));
} else {
prevIndex = prevMsg.size();
textInput.setText(QString());
}
}
} else if (event->key() == Qt::Key_Left) {
if (event->modifiers() == Qt::AltModifier) {
SplitContainer *page =
static_cast<SplitContainer *>(this->chatWidget->parentWidget());
int reqX = page->currentX - 1;
int reqY = page->lastRequestedY[reqX];
qDebug() << "Alt+Left to" << reqX << "/" << reqY;
page->requestFocus(reqX, reqY);
}
} else if (event->key() == Qt::Key_Right) {
if (event->modifiers() == Qt::AltModifier) {
SplitContainer *page =
static_cast<SplitContainer *>(this->chatWidget->parentWidget());
int reqX = page->currentX + 1;
int reqY = page->lastRequestedY[reqX];
qDebug() << "Alt+Right to" << reqX << "/" << reqY;
page->requestFocus(reqX, reqY);
}
} else if (event->key() == Qt::Key_Tab) {
if (event->modifiers() == Qt::ControlModifier) {
SplitContainer *page =
static_cast<SplitContainer *>(this->chatWidget->parentWidget());
Notebook *notebook = static_cast<Notebook *>(page->parentWidget());
notebook->nextTab();
}
} else if (event->key() == Qt::Key_Backtab) {
if (event->modifiers() == (Qt::ControlModifier | Qt::ShiftModifier)) {
SplitContainer *page =
static_cast<SplitContainer *>(this->chatWidget->parentWidget());
Notebook *notebook = static_cast<Notebook *>(page->parentWidget());
notebook->previousTab();
}
} else if (event->key() == Qt::Key_C && event->modifiers() == Qt::ControlModifier) {
if (this->chatWidget->view.hasSelection()) {
this->chatWidget->doCopy();
event->accept();
}
}
});
this->textLengthVisibleChangedConnection =
SettingsManager::getInstance().showMessageLength.valueChanged.connect(
[this](const bool &value) { this->textLengthLabel.setHidden(!value); });
QObject::connect(&this->textInput, &QTextEdit::copyAvailable, [this](bool available) {
if (available) {
this->chatWidget->view.clearSelection();
}
});
}
SplitInput::~SplitInput()
{
this->textLengthVisibleChangedConnection.disconnect();
}
void SplitInput::clearSelection()
{
QTextCursor c = this->textInput.textCursor();
c.setPosition(c.position());
c.setPosition(c.position(), QTextCursor::KeepAnchor);
this->textInput.setTextCursor(c);
}
void SplitInput::refreshTheme()
{
QPalette palette;
palette.setColor(QPalette::Foreground, this->colorScheme.Text);
this->textLengthLabel.setPalette(palette);
this->textInput.setStyleSheet(this->colorScheme.InputStyleSheet);
}
void SplitInput::editTextChanged()
{
}
void SplitInput::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.fillRect(this->rect(), this->colorScheme.ChatInputBackground);
painter.setPen(this->colorScheme.ChatInputBorder);
painter.drawRect(0, 0, this->width() - 1, this->height() - 1);
}
void SplitInput::resizeEvent(QResizeEvent *)
{
if (this->height() == this->maximumHeight()) {
this->textInput.setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
} else {
this->textInput.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
}
}
void SplitInput::mousePressEvent(QMouseEvent *)
{
this->chatWidget->giveFocus(Qt::MouseFocusReason);
}
} // namespace widgets
} // namespace chatterino
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include "emotemanager.hpp"
#include "resizingtextedit.hpp"
#include "widgets/basewidget.hpp"
#include "widgets/emotepopup.hpp"
#include "widgets/helper/rippleeffectlabel.hpp"
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPaintEvent>
#include <QTextEdit>
#include <QVBoxLayout>
#include <QWidget>
#include <boost/signals2.hpp>
namespace chatterino {
namespace widgets {
class Split;
class SplitInput : public BaseWidget
{
Q_OBJECT
public:
SplitInput(Split *_chatWidget, EmoteManager &, WindowManager &);
~SplitInput();
void clearSelection();
protected:
virtual void paintEvent(QPaintEvent *) override;
virtual void resizeEvent(QResizeEvent *) override;
virtual void mousePressEvent(QMouseEvent *event) override;
private:
Split *const chatWidget;
EmotePopup *emotePopup = nullptr;
EmoteManager &emoteManager;
WindowManager &windowManager;
boost::signals2::connection textLengthVisibleChangedConnection;
QHBoxLayout hbox;
QVBoxLayout vbox;
QHBoxLayout editContainer;
ResizingTextEdit textInput;
QLabel textLengthLabel;
RippleEffectLabel emotesLabel;
QStringList prevMsg;
unsigned int prevIndex = 0;
virtual void refreshTheme() override;
private slots:
void editTextChanged();
friend class Split;
};
} // namespace widgets
} // namespace chatterino