added colon emote popup for ffz and bttv

This commit is contained in:
fourtf 2020-08-15 18:59:17 +02:00
parent 6781482485
commit f7237dccdd
26 changed files with 700 additions and 251 deletions

View file

@ -239,11 +239,9 @@ SOURCES += \
src/widgets/dialogs/QualityPopup.cpp \
src/widgets/dialogs/SelectChannelDialog.cpp \
src/widgets/dialogs/SettingsDialog.cpp \
src/widgets/dialogs/switcher/AbstractSwitcherItem.cpp \
src/widgets/listview/GenericItemDelegate.cpp \
src/widgets/dialogs/switcher/NewTabItem.cpp \
src/widgets/dialogs/switcher/QuickSwitcherModel.cpp \
src/widgets/dialogs/switcher/QuickSwitcherPopup.cpp \
src/widgets/dialogs/switcher/SwitcherItemDelegate.cpp \
src/widgets/dialogs/switcher/SwitchSplitItem.cpp \
src/widgets/dialogs/TextInputDialog.cpp \
src/widgets/dialogs/UpdateDialog.cpp \
@ -268,6 +266,9 @@ SOURCES += \
src/widgets/Label.cpp \
src/widgets/Notebook.cpp \
src/widgets/Scrollbar.cpp \
src/widgets/listview/GenericListItem.cpp \
src/widgets/listview/GenericListModel.cpp \
src/widgets/listview/GenericListView.cpp \
src/widgets/settingspages/AboutPage.cpp \
src/widgets/settingspages/AccountsPage.cpp \
src/widgets/settingspages/CommandPage.cpp \
@ -280,6 +281,8 @@ SOURCES += \
src/widgets/settingspages/NotificationPage.cpp \
src/widgets/settingspages/SettingsPage.cpp \
src/widgets/splits/ClosedSplits.cpp \
src/widgets/splits/EmoteInputItem.cpp \
src/widgets/splits/EmoteInputPopup.cpp \
src/widgets/splits/Split.cpp \
src/widgets/splits/SplitContainer.cpp \
src/widgets/splits/SplitHeader.cpp \
@ -474,10 +477,10 @@ HEADERS += \
src/widgets/dialogs/SelectChannelDialog.hpp \
src/widgets/dialogs/SettingsDialog.hpp \
src/widgets/dialogs/switcher/AbstractSwitcherItem.hpp \
src/widgets/listview/GenericItemDelegate.hpp \
src/widgets/dialogs/switcher/NewTabItem.hpp \
src/widgets/dialogs/switcher/QuickSwitcherModel.hpp \
src/widgets/dialogs/switcher/QuickSwitcherPopup.hpp \
src/widgets/dialogs/switcher/SwitcherItemDelegate.hpp \
src/widgets/dialogs/switcher/SwitchSplitItem.hpp \
src/widgets/dialogs/TextInputDialog.hpp \
src/widgets/dialogs/UpdateDialog.hpp \
@ -504,6 +507,9 @@ HEADERS += \
src/widgets/Label.hpp \
src/widgets/Notebook.hpp \
src/widgets/Scrollbar.hpp \
src/widgets/listview/GenericListItem.hpp \
src/widgets/listview/GenericListModel.hpp \
src/widgets/listview/GenericListView.hpp \
src/widgets/settingspages/AboutPage.hpp \
src/widgets/settingspages/AccountsPage.hpp \
src/widgets/settingspages/CommandPage.hpp \
@ -516,6 +522,8 @@ HEADERS += \
src/widgets/settingspages/NotificationPage.hpp \
src/widgets/settingspages/SettingsPage.hpp \
src/widgets/splits/ClosedSplits.hpp \
src/widgets/splits/EmoteInputItem.hpp \
src/widgets/splits/EmoteInputPopup.hpp \
src/widgets/splits/Split.hpp \
src/widgets/splits/SplitContainer.hpp \
src/widgets/splits/SplitHeader.hpp \

View file

@ -22,6 +22,11 @@ public:
this->set(t);
}
QObjectRef(const QObjectRef &other)
{
this->set(other.t_);
}
~QObjectRef()
{
this->set(nullptr);

View file

@ -11,8 +11,6 @@ public:
explicit BasePopup(FlagsEnum<BaseWindow::Flags> flags_ = None,
QWidget *parent = nullptr);
virtual ~BasePopup() = default;
protected:
void keyPressEvent(QKeyEvent *e) override;
};

View file

@ -57,6 +57,18 @@ BaseWindow::BaseWindow(FlagsEnum<Flags> _flags, QWidget *parent)
this->setWindowFlag(Qt::FramelessWindowHint);
}
if (_flags.has(DontFocus))
{
this->setAttribute(Qt::WA_ShowWithoutActivating);
#ifdef Q_OS_LINUX
this->setWindowFlags(Qt::ToolTip);
#else
this->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint |
Qt::X11BypassWindowManagerHint |
Qt::BypassWindowManagerHint);
#endif
}
this->init();
getSettings()->uiScale.connect(

View file

@ -29,6 +29,7 @@ public:
TopMost = 4,
DisableCustomScaling = 8,
FramelessDraggable = 16,
DontFocus = 32,
};
enum ActionOnFocusLoss { Nothing, Delete, Close, Hide };

View file

@ -21,7 +21,7 @@ TooltipWidget *TooltipWidget::instance()
}
TooltipWidget::TooltipWidget(BaseWidget *parent)
: BaseWindow(BaseWindow::TopMost, parent)
: BaseWindow({BaseWindow::TopMost, BaseWindow::DontFocus}, parent)
, displayImage_(new QLabel())
, displayText_(new QLabel())
{
@ -31,15 +31,6 @@ TooltipWidget::TooltipWidget(BaseWidget *parent)
this->updateFont();
this->setStayInScreenRect(true);
this->setAttribute(Qt::WA_ShowWithoutActivating);
#ifdef Q_OS_LINUX
this->setWindowFlags(Qt::ToolTip);
#else
this->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint |
Qt::X11BypassWindowManagerHint |
Qt::BypassWindowManagerHint);
#endif
displayImage_->hide();
displayImage_->setAlignment(Qt::AlignHCenter);
displayImage_->setStyleSheet("background: transparent");

View file

@ -1,20 +0,0 @@
#include "widgets/dialogs/switcher/AbstractSwitcherItem.hpp"
#include "Application.hpp"
namespace chatterino {
const QSize AbstractSwitcherItem::ICON_SIZE(32, 32);
AbstractSwitcherItem *AbstractSwitcherItem::fromVariant(const QVariant &variant)
{
// See https://stackoverflow.com/a/44503822 .
return static_cast<AbstractSwitcherItem *>(variant.value<void *>());
}
AbstractSwitcherItem::AbstractSwitcherItem(const QIcon &icon)
: icon_(icon)
{
}
} // namespace chatterino

View file

@ -1,45 +1,9 @@
#pragma once
#include "widgets/listview/GenericListItem.hpp"
namespace chatterino {
class AbstractSwitcherItem
{
public:
/**
* @brief Attempt to obtain an AbstractSwitcherItem * from the passed QVariant.
*
* @param variant variant to try to convert to AbstractSwitcherItem *
*
* @return an AbstractSwitcherItem * if the QVariant could be converted,
* or nullptr if the variant did not contain AbstractSwitcherItem *
*/
static AbstractSwitcherItem *fromVariant(const QVariant &variant);
virtual ~AbstractSwitcherItem() = default;
/**
* @brief Since all switcher items are required to have an icon, we require it
* in the base class constructor.
*
* @param icon icon to be displayed in the switcher list
*/
AbstractSwitcherItem(const QIcon &icon);
/**
* @brief Action to perform when this item is activated. Must be implemented in
* subclasses.
*/
virtual void action() = 0;
virtual void paint(QPainter *painter, const QRect &rect) const = 0;
virtual QSize sizeHint(const QRect &rect) const = 0;
protected:
QIcon icon_;
static const QSize ICON_SIZE;
};
using AbstractSwitcherItem = GenericListItem;
} // namespace chatterino
// This allows us to store AbstractSwitcherItem * as a QVariant
Q_DECLARE_METATYPE(chatterino::AbstractSwitcherItem *);

View file

@ -2,51 +2,4 @@
namespace chatterino {
QuickSwitcherModel::QuickSwitcherModel(QWidget *parent)
: QAbstractListModel(parent)
{
}
int QuickSwitcherModel::rowCount(const QModelIndex &parent) const
{
return this->items_.size();
}
QVariant QuickSwitcherModel::data(const QModelIndex &index,
int /* role */) const
{
if (!index.isValid())
return QVariant();
if (index.row() >= this->items_.size())
return QVariant();
auto item = this->items_[index.row()].get();
// See https://stackoverflow.com/a/44503822 .
return QVariant::fromValue(static_cast<void *>(item));
}
void QuickSwitcherModel::addItem(std::unique_ptr<AbstractSwitcherItem> item)
{
// {begin,end}InsertRows needs to be called to notify attached views
this->beginInsertRows(QModelIndex(), this->items_.size(),
this->items_.size());
this->items_.push_back(std::move(item));
this->endInsertRows();
}
void QuickSwitcherModel::clear()
{
if (this->items_.empty())
return;
// {begin,end}RemoveRows needs to be called to notify attached views
this->beginRemoveRows(QModelIndex(), 0, this->items_.size() - 1);
// clear
this->items_.clear();
this->endRemoveRows();
}
} // namespace chatterino

View file

@ -1,52 +1,10 @@
#pragma once
#include <memory>
#include "widgets/dialogs/switcher/AbstractSwitcherItem.hpp"
#include "widgets/listview/GenericListModel.hpp"
namespace chatterino {
class QuickSwitcherModel : public QAbstractListModel
{
public:
QuickSwitcherModel(QWidget *parent = nullptr);
using QuickSwitcherModel = GenericListModel;
/**
* @brief Reimplements QAbstractItemModel::rowCount.
*
* @return number of items currrently present in this model
*/
int rowCount(const QModelIndex &parent = QModelIndex()) const;
/**
* @brief Reimplements QAbstractItemModel::data. Currently, the role parameter
* is not used and an AbstractSwitcherItem * is always returned.
*
* @param index index of item to fetch data from
* @param role (not used)
*
* @return AbstractSwitcherItem * (wrapped as QVariant) at index
*/
QVariant data(const QModelIndex &index, int role) const;
/**
* @brief Add an item to this QuickSwitcherModel. It will be displayed in
* attached views.
*
* NOTE: The model will take ownership of the pointer. In particular,
* the same item should not be passed to multiple QuickSwitcherModels.
*
* @param item item to add to the model
*/
void addItem(std::unique_ptr<AbstractSwitcherItem> item);
/**
* @brief Clears this QuickSwitcherModel of all items. This will delete all
* AbstractSwitcherItems added after the last invokation of
* QuickSwitcherModel::clear (and invalidate their pointers).
*/
void clear();
private:
std::vector<std::unique_ptr<AbstractSwitcherItem>> items_;
};
} // namespace chatterino
}

View file

@ -9,6 +9,7 @@
#include "widgets/dialogs/switcher/NewTabItem.hpp"
#include "widgets/dialogs/switcher/SwitchSplitItem.hpp"
#include "widgets/helper/NotebookTab.hpp"
#include "widgets/listview/GenericListView.hpp"
namespace chatterino {
@ -36,7 +37,6 @@ QuickSwitcherPopup::QuickSwitcherPopup(QWidget *parent)
BaseWindow::Flags::TopMost},
parent)
, switcherModel_(this)
, switcherItemDelegate_(this)
{
this->setWindowFlag(Qt::Dialog);
this->setActionOnFocusLoss(BaseWindow::ActionOnFocusLoss::Delete);
@ -51,10 +51,8 @@ QuickSwitcherPopup::QuickSwitcherPopup(QWidget *parent)
this->size(), geom));
this->themeChangedEvent();
}
QuickSwitcherPopup::~QuickSwitcherPopup()
{
this->installEventFilter(this->ui_.list);
}
void QuickSwitcherPopup::initWidgets()
@ -72,24 +70,12 @@ void QuickSwitcherPopup::initWidgets()
}
{
vbox.emplace<QListView>().assign(&this->ui_.list);
this->ui_.list->setSelectionMode(QAbstractItemView::SingleSelection);
this->ui_.list->setSelectionBehavior(QAbstractItemView::SelectItems);
this->ui_.list->setModel(&this->switcherModel_);
this->ui_.list->setItemDelegate(&this->switcherItemDelegate_);
auto listView = vbox.emplace<GenericListView>().assign(&this->ui_.list);
listView->setModel(&this->switcherModel_);
/*
* I also tried handling key events using the according slots but
* it lead to all kind of problems that did not occur with the
* eventFilter approach.
*/
QObject::connect(
this->ui_.list, &QListView::clicked, this,
[this](const QModelIndex &index) {
auto *item = AbstractSwitcherItem::fromVariant(index.data());
item->action();
this->close();
});
QObject::connect(listView.getElement(),
&GenericListView::closeRequested, this,
[this] { this->close(); });
}
}
@ -145,61 +131,6 @@ void QuickSwitcherPopup::updateSuggestions(const QString &text)
QTimer::singleShot(0, [this] { this->adjustSize(); });
}
bool QuickSwitcherPopup::eventFilter(QObject *watched, QEvent *event)
{
if (event->type() == QEvent::KeyPress)
{
auto *keyEvent = static_cast<QKeyEvent *>(event);
int key = keyEvent->key();
const QModelIndex &curIdx = this->ui_.list->currentIndex();
const int curRow = curIdx.row();
const int count = this->switcherModel_.rowCount(curIdx);
if (key == Qt::Key_Down || key == Qt::Key_Tab)
{
if (count <= 0)
return true;
const int newRow = (curRow + 1) % count;
this->ui_.list->setCurrentIndex(curIdx.siblingAtRow(newRow));
return true;
}
else if (key == Qt::Key_Up || key == Qt::Key_Backtab)
{
if (count <= 0)
return true;
int newRow = curRow - 1;
if (newRow < 0)
newRow += count;
this->ui_.list->setCurrentIndex(curIdx.siblingAtRow(newRow));
return true;
}
else if (key == Qt::Key_Enter || key == Qt::Key_Return)
{
if (count <= 0)
return true;
const auto index = this->ui_.list->currentIndex();
auto *item = AbstractSwitcherItem::fromVariant(index.data());
item->action();
this->close();
return true;
}
else
{
return false;
}
}
return false;
}
void QuickSwitcherPopup::themeChangedEvent()
{
BasePopup::themeChangedEvent();
@ -220,7 +151,7 @@ void QuickSwitcherPopup::themeChangedEvent()
.arg(selCol);
this->ui_.searchEdit->setStyleSheet(this->theme->splits.input.styleSheet);
this->ui_.list->setStyleSheet(listStyle);
this->ui_.list->refreshTheme(*this->theme);
}
} // namespace chatterino

View file

@ -3,7 +3,6 @@
#include "common/Channel.hpp"
#include "widgets/BasePopup.hpp"
#include "widgets/dialogs/switcher/QuickSwitcherModel.hpp"
#include "widgets/dialogs/switcher/SwitcherItemDelegate.hpp"
#include "widgets/splits/Split.hpp"
#include "widgets/splits/SplitContainer.hpp"
@ -11,6 +10,8 @@
namespace chatterino {
class GenericListView;
class QuickSwitcherPopup : public BasePopup
{
public:
@ -22,10 +23,7 @@ public:
*/
explicit QuickSwitcherPopup(QWidget *parent = nullptr);
~QuickSwitcherPopup();
protected:
virtual bool eventFilter(QObject *watched, QEvent *event) override;
virtual void themeChangedEvent() override;
public slots:
@ -36,11 +34,10 @@ private:
struct {
QLineEdit *searchEdit{};
QListView *list{};
GenericListView *list{};
} ui_;
QuickSwitcherModel switcherModel_;
SwitcherItemDelegate switcherItemDelegate_;
void initWidgets();
};

View file

@ -1,4 +1,4 @@
#include "widgets/dialogs/switcher/SwitcherItemDelegate.hpp"
#include "widgets/dialogs/switcher/GenericItemDelegate.hpp"
#include "widgets/dialogs/switcher/AbstractSwitcherItem.hpp"

View file

@ -0,0 +1,22 @@
#include "GenericListItem.hpp"
namespace chatterino {
const QSize GenericListItem::ICON_SIZE(32, 32);
GenericListItem *GenericListItem::fromVariant(const QVariant &variant)
{
// See https://stackoverflow.com/a/44503822 .
return static_cast<GenericListItem *>(variant.value<void *>());
}
GenericListItem::GenericListItem()
{
}
GenericListItem::GenericListItem(const QIcon &icon)
: icon_(icon)
{
}
} // namespace chatterino

View file

@ -0,0 +1,44 @@
#pragma once
namespace chatterino {
class GenericListItem
{
public:
/**
* @brief Attempt to obtain an GenericListItem * from the passed QVariant.
*
* @param variant variant to try to convert to GenericListItem *
*
* @return an GenericListItem * if the QVariant could be converted,
* or nullptr if the variant did not contain GenericListItem *
*/
static GenericListItem *fromVariant(const QVariant &variant);
virtual ~GenericListItem() = default;
GenericListItem();
/**
* @param icon icon to be displayed in the switcher list
*/
GenericListItem(const QIcon &icon);
/**
* @brief Action to perform when this item is activated. Must be implemented in
* subclasses.
*/
virtual void action() = 0;
virtual void paint(QPainter *painter, const QRect &rect) const = 0;
virtual QSize sizeHint(const QRect &rect) const = 0;
protected:
QIcon icon_;
static const QSize ICON_SIZE;
};
} // namespace chatterino
// This allows us to store GenericListItem * as a QVariant
Q_DECLARE_METATYPE(chatterino::GenericListItem *);

View file

@ -0,0 +1,51 @@
#include "GenericListModel.hpp"
namespace chatterino {
GenericListModel::GenericListModel(QWidget *parent)
: QAbstractListModel(parent)
{
}
int GenericListModel::rowCount(const QModelIndex &parent) const
{
return this->items_.size();
}
QVariant GenericListModel::data(const QModelIndex &index, int /* role */) const
{
if (!index.isValid())
return QVariant();
if (index.row() >= this->items_.size())
return QVariant();
auto item = this->items_[index.row()].get();
// See https://stackoverflow.com/a/44503822 .
return QVariant::fromValue(static_cast<void *>(item));
}
void GenericListModel::addItem(std::unique_ptr<GenericListItem> item)
{
// {begin,end}InsertRows needs to be called to notify attached views
this->beginInsertRows(QModelIndex(), this->items_.size(),
this->items_.size());
this->items_.push_back(std::move(item));
this->endInsertRows();
}
void GenericListModel::clear()
{
if (this->items_.empty())
return;
// {begin,end}RemoveRows needs to be called to notify attached views
this->beginRemoveRows(QModelIndex(), 0, this->items_.size() - 1);
// clear
this->items_.clear();
this->endRemoveRows();
}
} // namespace chatterino

View file

@ -0,0 +1,49 @@
#include "widgets/listview/GenericListItem.hpp"
namespace chatterino {
class GenericListModel : public QAbstractListModel
{
public:
GenericListModel(QWidget *parent = nullptr);
/**
* @brief Reimplements QAbstractItemModel::rowCount.
*
* @return number of items currrently present in this model
*/
int rowCount(const QModelIndex &parent = QModelIndex()) const;
/**
* @brief Reimplements QAbstractItemModel::data. Currently, the role parameter
* is not used and an GenericListItem * is always returned.
*
* @param index index of item to fetch data from
* @param role (not used)
*
* @return GenericListItem * (wrapped as QVariant) at index
*/
QVariant data(const QModelIndex &index, int role) const;
/**
* @brief Add an item to this QuickSwitcherModel. It will be displayed in
* attached views.
*
* NOTE: The model will take ownership of the pointer. In particular,
* the same item should not be passed to multiple QuickSwitcherModels.
*
* @param item item to add to the model
*/
void addItem(std::unique_ptr<GenericListItem> item);
/**
* @brief Clears this QuickSwitcherModel of all items. This will delete all
* GenericListItems added after the last invokation of
* QuickSwitcherModel::clear (and invalidate their pointers).
*/
void clear();
private:
std::vector<std::unique_ptr<GenericListItem>> items_;
};
} // namespace chatterino

View file

@ -0,0 +1,114 @@
#include "GenericListView.hpp"
#include "singletons/Theme.hpp"
#include "widgets/listview/GenericListModel.hpp"
namespace chatterino {
GenericListView::GenericListView()
: itemDelegate_(this)
{
this->setSelectionMode(QAbstractItemView::SingleSelection);
this->setSelectionBehavior(QAbstractItemView::SelectItems);
this->setItemDelegate(&this->itemDelegate_);
QObject::connect(
this, &QListView::clicked, this, [this](const QModelIndex &index) {
auto *item = GenericListItem::fromVariant(index.data());
item->action();
emit this->closeRequested();
});
}
void GenericListView::setModel(QAbstractItemModel *model)
{
auto casted = dynamic_cast<GenericListModel *>(model);
assert(casted);
this->setModel(casted);
}
void GenericListView::setModel(GenericListModel *model)
{
this->model_ = model;
QListView::setModel(model);
}
bool GenericListView::eventFilter(QObject * /*watched*/, QEvent *event)
{
if (!this->model_)
return false;
if (event->type() == QEvent::KeyPress)
{
auto *keyEvent = static_cast<QKeyEvent *>(event);
int key = keyEvent->key();
const QModelIndex &curIdx = this->currentIndex();
const int curRow = curIdx.row();
const int count = this->model_->rowCount(curIdx);
if (key == Qt::Key_Down || key == Qt::Key_Tab)
{
if (count <= 0)
return true;
const int newRow = (curRow + 1) % count;
this->setCurrentIndex(curIdx.siblingAtRow(newRow));
return true;
}
else if (key == Qt::Key_Up || key == Qt::Key_Backtab)
{
if (count <= 0)
return true;
int newRow = curRow - 1;
if (newRow < 0)
newRow += count;
this->setCurrentIndex(curIdx.siblingAtRow(newRow));
return true;
}
else if (key == Qt::Key_Enter || key == Qt::Key_Return)
{
if (count <= 0)
return true;
const auto index = this->currentIndex();
auto *item = GenericListItem::fromVariant(index.data());
item->action();
emit this->closeRequested();
return true;
}
else
{
return false;
}
}
return false;
}
void GenericListView::refreshTheme(const Theme &theme)
{
const QString textCol = theme.window.text.name();
const QString bgCol = theme.window.background.name();
const QString selCol =
(theme.isLightTheme()
? "#68B1FF" // Copied from Theme::splits.input.styleSheet
: theme.tabs.selected.backgrounds.regular.color().name());
const QString listStyle =
QString(
"color: %1; background-color: %2; selection-background-color: %3")
.arg(textCol)
.arg(bgCol)
.arg(selCol);
this->setStyleSheet(listStyle);
}
} // namespace chatterino

View file

@ -0,0 +1,32 @@
#pragma once
#include <QListView>
#include "widgets/listview/GenericItemDelegate.hpp"
#include "widgets/listview/GenericListItem.hpp"
namespace chatterino {
class GenericListModel;
class Theme;
class GenericListView : public QListView
{
Q_OBJECT
public:
GenericListView();
virtual void setModel(QAbstractItemModel *model) override;
void setModel(GenericListModel *);
bool eventFilter(QObject *watched, QEvent *event) override;
GenericListModel *model_{};
SwitcherItemDelegate itemDelegate_;
void refreshTheme(const Theme &theme);
signals:
void closeRequested();
};
} // namespace chatterino

View file

@ -0,0 +1,46 @@
#include "EmoteInputItem.hpp"
namespace chatterino {
EmoteInputItem::EmoteInputItem(const EmotePtr &emote, const QString &text,
ActionCallback action)
: emote_(emote)
, text_(text)
, action_(action)
{
}
void EmoteInputItem::action()
{
if (this->action_ && this->emote_)
this->action_(this->emote_->name.string);
}
void EmoteInputItem::paint(QPainter *painter, const QRect &rect) const
{
if (this->emote_)
{
if (auto image = this->emote_->images.getImage(4))
{
if (auto pixmap = image->pixmapOrLoad())
{
painter->drawPixmap(QRect(rect.x(), rect.y(), ICON_SIZE.width(),
ICON_SIZE.height()),
*pixmap);
}
}
}
QRect iconRect(rect.topLeft(), ICON_SIZE);
QRect textRect =
QRect(iconRect.topRight(),
QSize(rect.width() - iconRect.width(), iconRect.height()));
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, this->text_);
}
QSize EmoteInputItem::sizeHint(const QRect &rect) const
{
return QSize(rect.width(), ICON_SIZE.height());
}
} // namespace chatterino

View file

@ -0,0 +1,29 @@
#pragma once
#include <functional>
#include "messages/Emote.hpp"
#include "widgets/listview/GenericListItem.hpp"
namespace chatterino {
class EmoteInputItem : public GenericListItem
{
using ActionCallback = std::function<void(const QString &)>;
public:
EmoteInputItem(const EmotePtr &emote, const QString &text,
ActionCallback action);
// GenericListItem interface
public:
virtual void action() override;
virtual void paint(QPainter *painter, const QRect &rect) const override;
virtual QSize sizeHint(const QRect &rect) const override;
private:
EmotePtr emote_;
QString text_;
ActionCallback action_;
};
} // namespace chatterino

View file

@ -0,0 +1,106 @@
#include "EmoteInputPopup.hpp"
#include "Application.hpp"
#include "messages/Emote.hpp"
#include "providers/bttv/BttvEmotes.hpp"
#include "providers/ffz/FfzEmotes.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "singletons/Emotes.hpp"
#include "util/LayoutCreator.hpp"
#include "widgets/listview/GenericListView.hpp"
#include "widgets/splits/EmoteInputItem.hpp"
namespace chatterino {
namespace {
struct _Emote {
EmotePtr emote;
QString providerName;
};
void addEmotes(std::vector<_Emote> &out, const EmoteMap &map,
const QString &text, const QString &providerName)
{
for (auto &&emote : map)
if (emote.first.string.contains(text, Qt::CaseInsensitive))
out.push_back({emote.second, providerName});
}
} // namespace
EmoteInputPopup::EmoteInputPopup(QWidget *parent)
: BasePopup({BasePopup::EnableCustomFrame, BasePopup::Frameless,
BasePopup::DontFocus},
parent)
, model_(this)
{
this->initLayout();
// this->connections_.addConnection(
// getApp()->emotes->gifTimer.signal.connect([this] {
// if (this->isVisible())
// {
// // redraw listview somehow
// }
// }));
}
void EmoteInputPopup::initLayout()
{
LayoutCreator creator = {this};
auto listView =
creator.emplace<GenericListView>().assign(&this->ui_.listView);
listView->setModel(&this->model_);
QObject::connect(listView.getElement(), &GenericListView::closeRequested,
this, [this] { this->close(); });
}
void EmoteInputPopup::updateEmotes(const QString &text, ChannelPtr channel)
{
std::vector<_Emote> emotes;
if (auto tc = dynamic_cast<TwitchChannel *>(channel.get()))
{
// TODO extract "Channel BetterTTV" text into a #define.
if (auto bttv = tc->bttvEmotes())
addEmotes(emotes, *bttv, text, "Channel BetterTTV");
if (auto ffz = tc->ffzEmotes())
addEmotes(emotes, *ffz, text, "Channel FrankerFaceZ");
if (auto bttvG = tc->globalBttv().emotes())
addEmotes(emotes, *bttvG, text, "Global BetterTTV");
if (auto ffzG = tc->globalFfz().emotes())
addEmotes(emotes, *ffzG, text, "Global FrankerFaceZ");
}
this->model_.clear();
int count = 0;
for (auto &&emote : emotes)
{
this->model_.addItem(std::make_unique<EmoteInputItem>(
emote.emote, emote.emote->name.string + " - " + emote.providerName,
this->callback_));
if (count++ == maxLineCount)
break;
}
if (!emotes.empty())
{
this->ui_.listView->setCurrentIndex(this->model_.index(0));
}
}
bool EmoteInputPopup::eventFilter(QObject *watched, QEvent *event)
{
return this->ui_.listView->eventFilter(watched, event);
}
void EmoteInputPopup::setInputAction(ActionCallback callback)
{
this->callback_ = std::move(callback);
}
} // namespace chatterino

View file

@ -0,0 +1,38 @@
#pragma once
#include <functional>
#include "common/Channel.hpp"
#include "widgets/BasePopup.hpp"
#include "widgets/listview/GenericListModel.hpp"
namespace chatterino {
class GenericListView;
class EmoteInputPopup : public BasePopup
{
using ActionCallback = std::function<void(const QString &)>;
constexpr static int maxLineCount = 10;
public:
EmoteInputPopup(QWidget *parent = nullptr);
void updateEmotes(const QString &text, ChannelPtr channel);
virtual bool eventFilter(QObject *, QEvent *event) override;
void setInputAction(ActionCallback callback);
private:
void initLayout();
struct {
GenericListView *listView;
} ui_;
GenericListModel model_;
ActionCallback callback_;
// pajlada::Signals::SignalHolder connections_;
};
} // namespace chatterino

View file

@ -7,6 +7,7 @@
#include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Settings.hpp"
#include "singletons/Theme.hpp"
#include "util/Clamp.hpp"
#include "util/LayoutCreator.hpp"
#include "widgets/Notebook.hpp"
#include "widgets/Scrollbar.hpp"
@ -14,6 +15,7 @@
#include "widgets/helper/ChannelView.hpp"
#include "widgets/helper/EffectLabel.hpp"
#include "widgets/helper/ResizingTextEdit.hpp"
#include "widgets/splits/EmoteInputPopup.hpp"
#include "widgets/splits/Split.hpp"
#include "widgets/splits/SplitContainer.hpp"
#include "widgets/splits/SplitInput.hpp"
@ -41,6 +43,7 @@ SplitInput::SplitInput(Split *_chatWidget)
// misc
this->installKeyPressedEvent();
this->ui_.textEdit->focusLost.connect([this] { this->hideColonMenu(); });
this->scaleChangedEvent(this->scale());
}
@ -78,6 +81,8 @@ void SplitInput::initLayout()
// set edit font
this->ui_.textEdit->setFont(
app->fonts->getFont(FontStyle::ChatMedium, this->scale()));
QObject::connect(this->ui_.textEdit, &QTextEdit::cursorPositionChanged,
this, &SplitInput::onCursorPositionChanged);
this->managedConnections_.push_back(app->fonts->fontChanged.connect([=]() {
this->ui_.textEdit->setFont(
@ -187,6 +192,18 @@ void SplitInput::installKeyPressedEvent()
auto app = getApp();
this->ui_.textEdit->keyPressed.connect([this, app](QKeyEvent *event) {
if (auto popup = this->emoteInputPopup_.get())
{
if (popup->isVisible())
{
if (popup->eventFilter(nullptr, event))
{
event->accept();
return;
}
}
}
if (event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return)
{
auto c = this->split_->getChannel();
@ -435,6 +452,102 @@ void SplitInput::installKeyPressedEvent()
});
}
void SplitInput::onCursorPositionChanged()
{
this->updateColonMenu();
}
void SplitInput::updateColonMenu()
{
if (!dynamic_cast<TwitchChannel *>(this->split_->getChannel().get()))
{
this->hideColonMenu();
}
// check if in :
auto &edit = *this->ui_.textEdit;
auto text = edit.toPlainText();
auto position = edit.textCursor().position();
if (text.length() == 0)
{
this->hideColonMenu();
return;
}
for (int i = clamp(position, 0, text.length() - 1); i >= 0; i--)
{
if (text[i] == ' ')
{
this->hideColonMenu();
return;
}
else if (text[i] == ':')
{
this->showColonMenu(text.mid(i, position - i).mid(1));
return;
}
}
this->hideColonMenu();
}
void SplitInput::showColonMenu(const QString &text)
{
if (!this->emoteInputPopup_.get())
{
this->emoteInputPopup_ = new EmoteInputPopup(this);
this->emoteInputPopup_->setInputAction(
[that = QObjectRef(this)](const QString &text) mutable {
if (auto this2 = that.get())
{
this2->insertColonText(text);
this2->hideColonMenu();
}
});
}
auto popup = this->emoteInputPopup_.get();
assert(popup);
popup->updateEmotes(text, this->split_->getChannel());
auto pos = this->mapToGlobal({0, 0}) - QPoint(0, popup->height()) +
QPoint((this->width() - popup->width()) / 2, 0);
popup->move(pos);
popup->show();
}
void SplitInput::hideColonMenu()
{
if (auto popup = this->emoteInputPopup_.get())
popup->hide();
}
void SplitInput::insertColonText(const QString &input)
{
auto &edit = *this->ui_.textEdit;
auto text = edit.toPlainText();
auto position = edit.textCursor().position();
for (int i = clamp(position, 0, text.length() - 1); i >= 0; i--)
{
if (text[i] == ':')
{
auto cursor = edit.textCursor();
edit.setText(text.remove(i, position - i).insert(i, input));
cursor.setPosition(i + input.size());
edit.setTextCursor(cursor);
break;
}
}
}
void SplitInput::clearSelection()
{
QTextCursor c = this->ui_.textEdit->textCursor();

View file

@ -16,6 +16,7 @@ namespace chatterino {
class Split;
class EmotePopup;
class EmoteInputPopup;
class EffectLabel;
class ResizingTextEdit;
@ -44,11 +45,17 @@ protected:
private:
void initLayout();
void installKeyPressedEvent();
void onCursorPositionChanged();
void updateEmoteButton();
void updateColonMenu();
void showColonMenu(const QString &text);
void hideColonMenu();
void insertColonText(const QString &text);
void openEmotePopup();
Split *const split_;
QObjectRef<EmotePopup> emotePopup_;
QObjectRef<EmoteInputPopup> emoteInputPopup_;
struct {
ResizingTextEdit *textEdit;