From f7237dccdd5a4994b057967c3b3cdd7fa5166d55 Mon Sep 17 00:00:00 2001 From: fourtf Date: Sat, 15 Aug 2020 18:59:17 +0200 Subject: [PATCH] added colon emote popup for ffz and bttv --- chatterino.pro | 16 ++- src/util/QObjectRef.hpp | 5 + src/widgets/BasePopup.hpp | 2 - src/widgets/BaseWindow.cpp | 12 ++ src/widgets/BaseWindow.hpp | 1 + src/widgets/TooltipWidget.cpp | 11 +- .../dialogs/switcher/AbstractSwitcherItem.cpp | 20 --- .../dialogs/switcher/AbstractSwitcherItem.hpp | 42 +------ .../dialogs/switcher/QuickSwitcherModel.cpp | 47 -------- .../dialogs/switcher/QuickSwitcherModel.hpp | 48 +------- .../dialogs/switcher/QuickSwitcherPopup.cpp | 85 ++----------- .../dialogs/switcher/QuickSwitcherPopup.hpp | 9 +- .../GenericItemDelegate.cpp} | 2 +- .../GenericItemDelegate.hpp} | 0 src/widgets/listview/GenericListItem.cpp | 22 ++++ src/widgets/listview/GenericListItem.hpp | 44 +++++++ src/widgets/listview/GenericListModel.cpp | 51 ++++++++ src/widgets/listview/GenericListModel.hpp | 49 ++++++++ src/widgets/listview/GenericListView.cpp | 114 ++++++++++++++++++ src/widgets/listview/GenericListView.hpp | 32 +++++ src/widgets/splits/EmoteInputItem.cpp | 46 +++++++ src/widgets/splits/EmoteInputItem.hpp | 29 +++++ src/widgets/splits/EmoteInputPopup.cpp | 106 ++++++++++++++++ src/widgets/splits/EmoteInputPopup.hpp | 38 ++++++ src/widgets/splits/SplitInput.cpp | 113 +++++++++++++++++ src/widgets/splits/SplitInput.hpp | 7 ++ 26 files changed, 700 insertions(+), 251 deletions(-) delete mode 100644 src/widgets/dialogs/switcher/AbstractSwitcherItem.cpp rename src/widgets/{dialogs/switcher/SwitcherItemDelegate.cpp => listview/GenericItemDelegate.cpp} (94%) rename src/widgets/{dialogs/switcher/SwitcherItemDelegate.hpp => listview/GenericItemDelegate.hpp} (100%) create mode 100644 src/widgets/listview/GenericListItem.cpp create mode 100644 src/widgets/listview/GenericListItem.hpp create mode 100644 src/widgets/listview/GenericListModel.cpp create mode 100644 src/widgets/listview/GenericListModel.hpp create mode 100644 src/widgets/listview/GenericListView.cpp create mode 100644 src/widgets/listview/GenericListView.hpp create mode 100644 src/widgets/splits/EmoteInputItem.cpp create mode 100644 src/widgets/splits/EmoteInputItem.hpp create mode 100644 src/widgets/splits/EmoteInputPopup.cpp create mode 100644 src/widgets/splits/EmoteInputPopup.hpp diff --git a/chatterino.pro b/chatterino.pro index 9179f973..56228b39 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -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 \ diff --git a/src/util/QObjectRef.hpp b/src/util/QObjectRef.hpp index bd469d66..ec425939 100644 --- a/src/util/QObjectRef.hpp +++ b/src/util/QObjectRef.hpp @@ -22,6 +22,11 @@ public: this->set(t); } + QObjectRef(const QObjectRef &other) + { + this->set(other.t_); + } + ~QObjectRef() { this->set(nullptr); diff --git a/src/widgets/BasePopup.hpp b/src/widgets/BasePopup.hpp index 6d7ab7d6..5741b000 100644 --- a/src/widgets/BasePopup.hpp +++ b/src/widgets/BasePopup.hpp @@ -11,8 +11,6 @@ public: explicit BasePopup(FlagsEnum flags_ = None, QWidget *parent = nullptr); - virtual ~BasePopup() = default; - protected: void keyPressEvent(QKeyEvent *e) override; }; diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index 58df75bb..9cec59af 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -57,6 +57,18 @@ BaseWindow::BaseWindow(FlagsEnum _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( diff --git a/src/widgets/BaseWindow.hpp b/src/widgets/BaseWindow.hpp index 5d0fe6bd..1a63e4d7 100644 --- a/src/widgets/BaseWindow.hpp +++ b/src/widgets/BaseWindow.hpp @@ -29,6 +29,7 @@ public: TopMost = 4, DisableCustomScaling = 8, FramelessDraggable = 16, + DontFocus = 32, }; enum ActionOnFocusLoss { Nothing, Delete, Close, Hide }; diff --git a/src/widgets/TooltipWidget.cpp b/src/widgets/TooltipWidget.cpp index b1c4385a..8f651109 100644 --- a/src/widgets/TooltipWidget.cpp +++ b/src/widgets/TooltipWidget.cpp @@ -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"); diff --git a/src/widgets/dialogs/switcher/AbstractSwitcherItem.cpp b/src/widgets/dialogs/switcher/AbstractSwitcherItem.cpp deleted file mode 100644 index 2ebd3a51..00000000 --- a/src/widgets/dialogs/switcher/AbstractSwitcherItem.cpp +++ /dev/null @@ -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(variant.value()); -} - -AbstractSwitcherItem::AbstractSwitcherItem(const QIcon &icon) - : icon_(icon) -{ -} - -} // namespace chatterino diff --git a/src/widgets/dialogs/switcher/AbstractSwitcherItem.hpp b/src/widgets/dialogs/switcher/AbstractSwitcherItem.hpp index f560c13c..ab117117 100644 --- a/src/widgets/dialogs/switcher/AbstractSwitcherItem.hpp +++ b/src/widgets/dialogs/switcher/AbstractSwitcherItem.hpp @@ -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 *); diff --git a/src/widgets/dialogs/switcher/QuickSwitcherModel.cpp b/src/widgets/dialogs/switcher/QuickSwitcherModel.cpp index 03bf26e3..43e6ecc1 100644 --- a/src/widgets/dialogs/switcher/QuickSwitcherModel.cpp +++ b/src/widgets/dialogs/switcher/QuickSwitcherModel.cpp @@ -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(item)); -} - -void QuickSwitcherModel::addItem(std::unique_ptr 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 diff --git a/src/widgets/dialogs/switcher/QuickSwitcherModel.hpp b/src/widgets/dialogs/switcher/QuickSwitcherModel.hpp index 70e08d7a..86fc9f9c 100644 --- a/src/widgets/dialogs/switcher/QuickSwitcherModel.hpp +++ b/src/widgets/dialogs/switcher/QuickSwitcherModel.hpp @@ -1,52 +1,10 @@ #pragma once #include -#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 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> items_; -}; -} // namespace chatterino +} diff --git a/src/widgets/dialogs/switcher/QuickSwitcherPopup.cpp b/src/widgets/dialogs/switcher/QuickSwitcherPopup.cpp index 22555e2d..09a968e2 100644 --- a/src/widgets/dialogs/switcher/QuickSwitcherPopup.cpp +++ b/src/widgets/dialogs/switcher/QuickSwitcherPopup.cpp @@ -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().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().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(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 diff --git a/src/widgets/dialogs/switcher/QuickSwitcherPopup.hpp b/src/widgets/dialogs/switcher/QuickSwitcherPopup.hpp index 7f2baade..e92848ec 100644 --- a/src/widgets/dialogs/switcher/QuickSwitcherPopup.hpp +++ b/src/widgets/dialogs/switcher/QuickSwitcherPopup.hpp @@ -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(); }; diff --git a/src/widgets/dialogs/switcher/SwitcherItemDelegate.cpp b/src/widgets/listview/GenericItemDelegate.cpp similarity index 94% rename from src/widgets/dialogs/switcher/SwitcherItemDelegate.cpp rename to src/widgets/listview/GenericItemDelegate.cpp index 72bde7e2..467de27d 100644 --- a/src/widgets/dialogs/switcher/SwitcherItemDelegate.cpp +++ b/src/widgets/listview/GenericItemDelegate.cpp @@ -1,4 +1,4 @@ -#include "widgets/dialogs/switcher/SwitcherItemDelegate.hpp" +#include "widgets/dialogs/switcher/GenericItemDelegate.hpp" #include "widgets/dialogs/switcher/AbstractSwitcherItem.hpp" diff --git a/src/widgets/dialogs/switcher/SwitcherItemDelegate.hpp b/src/widgets/listview/GenericItemDelegate.hpp similarity index 100% rename from src/widgets/dialogs/switcher/SwitcherItemDelegate.hpp rename to src/widgets/listview/GenericItemDelegate.hpp diff --git a/src/widgets/listview/GenericListItem.cpp b/src/widgets/listview/GenericListItem.cpp new file mode 100644 index 00000000..6c679b30 --- /dev/null +++ b/src/widgets/listview/GenericListItem.cpp @@ -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(variant.value()); +} + +GenericListItem::GenericListItem() +{ +} + +GenericListItem::GenericListItem(const QIcon &icon) + : icon_(icon) +{ +} + +} // namespace chatterino diff --git a/src/widgets/listview/GenericListItem.hpp b/src/widgets/listview/GenericListItem.hpp new file mode 100644 index 00000000..ce78d1db --- /dev/null +++ b/src/widgets/listview/GenericListItem.hpp @@ -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 *); diff --git a/src/widgets/listview/GenericListModel.cpp b/src/widgets/listview/GenericListModel.cpp new file mode 100644 index 00000000..9163b2b3 --- /dev/null +++ b/src/widgets/listview/GenericListModel.cpp @@ -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(item)); +} + +void GenericListModel::addItem(std::unique_ptr 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 diff --git a/src/widgets/listview/GenericListModel.hpp b/src/widgets/listview/GenericListModel.hpp new file mode 100644 index 00000000..a803c0ff --- /dev/null +++ b/src/widgets/listview/GenericListModel.hpp @@ -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 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> items_; +}; +} // namespace chatterino diff --git a/src/widgets/listview/GenericListView.cpp b/src/widgets/listview/GenericListView.cpp new file mode 100644 index 00000000..b5a0cdde --- /dev/null +++ b/src/widgets/listview/GenericListView.cpp @@ -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(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(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 diff --git a/src/widgets/listview/GenericListView.hpp b/src/widgets/listview/GenericListView.hpp new file mode 100644 index 00000000..3baebf78 --- /dev/null +++ b/src/widgets/listview/GenericListView.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#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 diff --git a/src/widgets/splits/EmoteInputItem.cpp b/src/widgets/splits/EmoteInputItem.cpp new file mode 100644 index 00000000..0e7d0556 --- /dev/null +++ b/src/widgets/splits/EmoteInputItem.cpp @@ -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 diff --git a/src/widgets/splits/EmoteInputItem.hpp b/src/widgets/splits/EmoteInputItem.hpp new file mode 100644 index 00000000..d0798c2e --- /dev/null +++ b/src/widgets/splits/EmoteInputItem.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include "messages/Emote.hpp" +#include "widgets/listview/GenericListItem.hpp" + +namespace chatterino { + +class EmoteInputItem : public GenericListItem +{ + using ActionCallback = std::function; + +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 diff --git a/src/widgets/splits/EmoteInputPopup.cpp b/src/widgets/splits/EmoteInputPopup.cpp new file mode 100644 index 00000000..fce3b623 --- /dev/null +++ b/src/widgets/splits/EmoteInputPopup.cpp @@ -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().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(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( + 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 diff --git a/src/widgets/splits/EmoteInputPopup.hpp b/src/widgets/splits/EmoteInputPopup.hpp new file mode 100644 index 00000000..ced9c8f1 --- /dev/null +++ b/src/widgets/splits/EmoteInputPopup.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#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; + + 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 diff --git a/src/widgets/splits/SplitInput.cpp b/src/widgets/splits/SplitInput.cpp index ecba0edf..99b7967f 100644 --- a/src/widgets/splits/SplitInput.cpp +++ b/src/widgets/splits/SplitInput.cpp @@ -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(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(); diff --git a/src/widgets/splits/SplitInput.hpp b/src/widgets/splits/SplitInput.hpp index 53ecf9d7..00b22ebc 100644 --- a/src/widgets/splits/SplitInput.hpp +++ b/src/widgets/splits/SplitInput.hpp @@ -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_; + QObjectRef emoteInputPopup_; struct { ResizingTextEdit *textEdit;