diff --git a/CHANGELOG.md b/CHANGELOG.md index cc55bf904..98f2221c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Major: We now support image thumbnails coming from the link resolver. This feature is off by default and can be enabled in the settings with the "Show link thumbnail" setting. This feature also requires the "Show link info when hovering" setting to be enabled (#1664) - Major: Added image upload functionality to i.nuuls.com with an ability to change upload destination. This works by dragging and dropping an image into a split, or pasting an image into the text edit field. (#1332, #1741) +- Minor: Add a switcher widget, similar to Discord. It can be opened by pressing Ctrl+K. (#1588) - Major: Added option to display tabs vertically. (#1815) - Minor: Clicking on `Open in browser` in a whisper split will now open your whispers on twitch. (#1828) - Minor: Clicking on @mentions will open the User Popup. (#1674) diff --git a/chatterino.pro b/chatterino.pro index bd34241de..9179f973d 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -156,10 +156,10 @@ SOURCES += \ src/messages/MessageColor.cpp \ src/messages/MessageContainer.cpp \ src/messages/MessageElement.cpp \ - src/messages/SharedMessageBuilder.cpp \ src/messages/search/AuthorPredicate.cpp \ src/messages/search/LinkPredicate.cpp \ src/messages/search/SubstringPredicate.cpp \ + src/messages/SharedMessageBuilder.cpp \ src/providers/bttv/BttvEmotes.cpp \ src/providers/bttv/LoadBttvChannelEmote.cpp \ src/providers/chatterino/ChatterinoBadges.cpp \ @@ -218,11 +218,11 @@ SOURCES += \ src/util/IncognitoBrowser.cpp \ src/util/InitUpdateButton.cpp \ src/util/JsonQuery.cpp \ - src/util/RapidjsonHelpers.cpp \ - src/util/StreamLink.cpp \ - src/util/StreamerMode.cpp \ - src/util/Twitch.cpp \ src/util/NuulsUploader.cpp \ + src/util/RapidjsonHelpers.cpp \ + src/util/StreamerMode.cpp \ + src/util/StreamLink.cpp \ + src/util/Twitch.cpp \ src/util/WindowsHelper.cpp \ src/widgets/AccountSwitchPopup.cpp \ src/widgets/AccountSwitchWidget.cpp \ @@ -239,6 +239,12 @@ SOURCES += \ src/widgets/dialogs/QualityPopup.cpp \ src/widgets/dialogs/SelectChannelDialog.cpp \ src/widgets/dialogs/SettingsDialog.cpp \ + src/widgets/dialogs/switcher/AbstractSwitcherItem.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 \ src/widgets/dialogs/UserInfoPopup.cpp \ @@ -358,12 +364,12 @@ HEADERS += \ src/messages/MessageContainer.hpp \ src/messages/MessageElement.hpp \ src/messages/MessageParseArgs.hpp \ - src/messages/SharedMessageBuilder.hpp \ src/messages/search/AuthorPredicate.hpp \ src/messages/search/LinkPredicate.hpp \ src/messages/search/MessagePredicate.hpp \ src/messages/search/SubstringPredicate.hpp \ src/messages/Selection.hpp \ + src/messages/SharedMessageBuilder.hpp \ src/PrecompiledHeader.hpp \ src/providers/bttv/BttvEmotes.hpp \ src/providers/bttv/LoadBttvChannelEmote.hpp \ @@ -433,13 +439,12 @@ HEADERS += \ src/util/JsonQuery.hpp \ src/util/LayoutCreator.hpp \ src/util/LayoutHelper.hpp \ + src/util/NuulsUploader.hpp \ src/util/Overloaded.hpp \ src/util/PersistSignalVector.hpp \ src/util/PostToThread.hpp \ src/util/QObjectRef.hpp \ src/util/QStringHash.hpp \ - src/util/StreamerMode.hpp \ - src/util/Twitch.hpp \ src/util/rangealgorithm.hpp \ src/util/RapidjsonHelpers.hpp \ src/util/RapidJsonSerializeQString.hpp \ @@ -449,8 +454,9 @@ HEADERS += \ src/util/SharedPtrElementLess.hpp \ src/util/Shortcut.hpp \ src/util/StandardItemHelper.hpp \ + src/util/StreamerMode.hpp \ src/util/StreamLink.hpp \ - src/util/NuulsUploader.hpp \ + src/util/Twitch.hpp \ src/util/WindowsHelper.hpp \ src/widgets/AccountSwitchPopup.hpp \ src/widgets/AccountSwitchWidget.hpp \ @@ -467,6 +473,12 @@ HEADERS += \ src/widgets/dialogs/QualityPopup.hpp \ src/widgets/dialogs/SelectChannelDialog.hpp \ src/widgets/dialogs/SettingsDialog.hpp \ + src/widgets/dialogs/switcher/AbstractSwitcherItem.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 \ src/widgets/dialogs/UserInfoPopup.hpp \ diff --git a/lib/libcommuni b/lib/libcommuni index f3e7f9791..a31ffb037 160000 --- a/lib/libcommuni +++ b/lib/libcommuni @@ -1 +1 @@ -Subproject commit f3e7f97914d9bf1166d349a83d93a2b4f4743c39 +Subproject commit a31ffb037eadac65dba73ad2b2da6dafe31e3bf7 diff --git a/resources/resources_autogenerated.qrc b/resources/resources_autogenerated.qrc index 4fe4cf72e..2497e6f6f 100644 --- a/resources/resources_autogenerated.qrc +++ b/resources/resources_autogenerated.qrc @@ -78,6 +78,8 @@ split/move.png split/right.png split/up.png + switcher/plus.svg + switcher/switch.svg tlds.txt twitch/admin.png twitch/automod.png diff --git a/resources/switcher/plus.svg b/resources/switcher/plus.svg new file mode 100644 index 000000000..bf8ead17e --- /dev/null +++ b/resources/switcher/plus.svg @@ -0,0 +1,75 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/resources/switcher/switch.svg b/resources/switcher/switch.svg new file mode 100644 index 000000000..4797699dc --- /dev/null +++ b/resources/switcher/switch.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/src/widgets/Window.cpp b/src/widgets/Window.cpp index 115902a1a..ae9b68a27 100644 --- a/src/widgets/Window.cpp +++ b/src/widgets/Window.cpp @@ -17,6 +17,7 @@ #include "widgets/dialogs/SettingsDialog.hpp" #include "widgets/dialogs/UpdateDialog.hpp" #include "widgets/dialogs/WelcomeDialog.hpp" +#include "widgets/dialogs/switcher/QuickSwitcherPopup.hpp" #include "widgets/helper/EffectLabel.hpp" #include "widgets/helper/NotebookTab.hpp" #include "widgets/helper/TitlebarButton.hpp" @@ -393,6 +394,12 @@ void Window::addShortcuts() getSettings()->hideSimilar.setValue(!getSettings()->hideSimilar); getApp()->windows->forceLayoutChannelViews(); }); + + createWindowShortcut(this, "CTRL+K", [this] { + auto quickSwitcher = + new QuickSwitcherPopup(&getApp()->windows->getMainWindow()); + quickSwitcher->show(); + }); } void Window::addMenuBar() diff --git a/src/widgets/dialogs/switcher/AbstractSwitcherItem.cpp b/src/widgets/dialogs/switcher/AbstractSwitcherItem.cpp new file mode 100644 index 000000000..2ebd3a517 --- /dev/null +++ b/src/widgets/dialogs/switcher/AbstractSwitcherItem.cpp @@ -0,0 +1,20 @@ +#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 new file mode 100644 index 000000000..f560c13cf --- /dev/null +++ b/src/widgets/dialogs/switcher/AbstractSwitcherItem.hpp @@ -0,0 +1,45 @@ +#pragma once + +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; +}; + +} // namespace chatterino + +// This allows us to store AbstractSwitcherItem * as a QVariant +Q_DECLARE_METATYPE(chatterino::AbstractSwitcherItem *); diff --git a/src/widgets/dialogs/switcher/NewTabItem.cpp b/src/widgets/dialogs/switcher/NewTabItem.cpp new file mode 100644 index 000000000..0713e5f7d --- /dev/null +++ b/src/widgets/dialogs/switcher/NewTabItem.cpp @@ -0,0 +1,60 @@ +#include "widgets/dialogs/switcher/NewTabItem.hpp" + +#include "Application.hpp" +#include "providers/twitch/TwitchIrcServer.hpp" +#include "singletons/Fonts.hpp" +#include "singletons/Theme.hpp" +#include "singletons/WindowManager.hpp" +#include "widgets/Notebook.hpp" +#include "widgets/Window.hpp" +#include "widgets/helper/NotebookTab.hpp" +#include "widgets/splits/Split.hpp" + +namespace chatterino { + +NewTabItem::NewTabItem(const QString &channelName) + : AbstractSwitcherItem(QIcon(":/switcher/plus.svg")) + , channelName_(channelName) + , text_(QString(TEXT_FORMAT).arg(channelName)) +{ +} + +void NewTabItem::action() +{ + auto &nb = getApp()->windows->getMainWindow().getNotebook(); + SplitContainer *container = nb.addPage(true); + + Split *split = new Split(container); + split->setChannel( + getApp()->twitch.server->getOrAddChannel(this->channelName_)); + container->appendSplit(split); +} + +void NewTabItem::paint(QPainter *painter, const QRect &rect) const +{ + painter->save(); + + painter->setRenderHint(QPainter::Antialiasing, true); + + // TODO(leon): Right pen/brush/font settings? + painter->setPen(getApp()->themes->splits.header.text); + painter->setBrush(Qt::SolidPattern); + painter->setFont(getApp()->fonts->getFont(FontStyle::UiMediumBold, 1.0)); + + QRect iconRect(rect.topLeft(), ICON_SIZE); + this->icon_.paint(painter, iconRect, Qt::AlignLeft | Qt::AlignVCenter); + + QRect textRect = + QRect(iconRect.topRight(), + QSize(rect.width() - iconRect.width(), iconRect.height())); + painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, this->text_); + + painter->restore(); +} + +QSize NewTabItem::sizeHint(const QRect &rect) const +{ + return QSize(rect.width(), ICON_SIZE.height()); +} + +} // namespace chatterino diff --git a/src/widgets/dialogs/switcher/NewTabItem.hpp b/src/widgets/dialogs/switcher/NewTabItem.hpp new file mode 100644 index 000000000..152105ade --- /dev/null +++ b/src/widgets/dialogs/switcher/NewTabItem.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "widgets/dialogs/switcher/AbstractSwitcherItem.hpp" + +namespace chatterino { + +class NewTabItem : public AbstractSwitcherItem +{ +public: + /** + * @brief Construct a new NewTabItem that opens a passed channel in a new + * tab. + * + * @param channelName name of channel to open + */ + NewTabItem(const QString &channelName); + + /** + * @brief Open the channel passed in the constructor in a new tab. + */ + virtual void action() override; + + virtual void paint(QPainter *painter, const QRect &rect) const; + virtual QSize sizeHint(const QRect &rect) const; + +private: + static constexpr const char *TEXT_FORMAT = "Open channel \"%1\" in new tab"; + QString channelName_; + QString text_; +}; + +} // namespace chatterino diff --git a/src/widgets/dialogs/switcher/QuickSwitcherModel.cpp b/src/widgets/dialogs/switcher/QuickSwitcherModel.cpp new file mode 100644 index 000000000..4898e15ad --- /dev/null +++ b/src/widgets/dialogs/switcher/QuickSwitcherModel.cpp @@ -0,0 +1,61 @@ +#include "widgets/dialogs/switcher/QuickSwitcherModel.hpp" + +namespace chatterino { + +QuickSwitcherModel::QuickSwitcherModel(QWidget *parent) + : QAbstractListModel(parent) + , items_(INITIAL_ITEMS_SIZE) +{ +} + +QuickSwitcherModel::~QuickSwitcherModel() +{ + for (AbstractSwitcherItem *item : this->items_) + { + delete item; + } +} + +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_.at(index.row()); + // See https://stackoverflow.com/a/44503822 . + return QVariant::fromValue(static_cast(item)); +} + +void QuickSwitcherModel::addItem(AbstractSwitcherItem *item) +{ + // {begin,end}InsertRows needs to be called to notify attached views + this->beginInsertRows(QModelIndex(), this->items_.size(), + this->items_.size()); + this->items_.append(item); + this->endInsertRows(); +} + +void QuickSwitcherModel::clear() +{ + // {begin,end}RemoveRows needs to be called to notify attached views + this->beginRemoveRows(QModelIndex(), 0, this->items_.size() - 1); + + for (AbstractSwitcherItem *item : this->items_) + { + delete item; + } + this->items_.clear(); + + this->endRemoveRows(); +} + +} // namespace chatterino diff --git a/src/widgets/dialogs/switcher/QuickSwitcherModel.hpp b/src/widgets/dialogs/switcher/QuickSwitcherModel.hpp new file mode 100644 index 000000000..92b448925 --- /dev/null +++ b/src/widgets/dialogs/switcher/QuickSwitcherModel.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include "widgets/dialogs/switcher/AbstractSwitcherItem.hpp" + +namespace chatterino { + +class QuickSwitcherModel : public QAbstractListModel +{ +public: + QuickSwitcherModel(QWidget *parent = nullptr); + ~QuickSwitcherModel(); + + /** + * @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(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: + /* + * On my system, the default QVector capacity is 0. 20 is an attempt at preventing + * frequent reallocations. The number is not backed by any user data but rather a + * guess at how many switcher items are probably going to be added. + */ + static constexpr int INITIAL_ITEMS_SIZE = 20; + + QVector items_; +}; +} // namespace chatterino diff --git a/src/widgets/dialogs/switcher/QuickSwitcherPopup.cpp b/src/widgets/dialogs/switcher/QuickSwitcherPopup.cpp new file mode 100644 index 000000000..97ca292a1 --- /dev/null +++ b/src/widgets/dialogs/switcher/QuickSwitcherPopup.cpp @@ -0,0 +1,200 @@ +#include "widgets/dialogs/switcher/QuickSwitcherPopup.hpp" + +#include "Application.hpp" +#include "singletons/WindowManager.hpp" +#include "util/LayoutCreator.hpp" +#include "widgets/Notebook.hpp" +#include "widgets/Window.hpp" +#include "widgets/dialogs/switcher/NewTabItem.hpp" +#include "widgets/dialogs/switcher/SwitchSplitItem.hpp" +#include "widgets/helper/NotebookTab.hpp" + +namespace chatterino { + +namespace { + using namespace chatterino; + + QSet openPages() + { + QSet pages; + + auto &nb = getApp()->windows->getMainWindow().getNotebook(); + for (int i = 0; i < nb.getPageCount(); ++i) + { + pages.insert(static_cast(nb.getPageAt(i))); + } + + return pages; + } +} // namespace + +const QSize QuickSwitcherPopup::MINIMUM_SIZE(500, 300); + +QuickSwitcherPopup::QuickSwitcherPopup(QWidget *parent) + : BasePopup(FlagsEnum{BaseWindow::Flags::Frameless, + BaseWindow::Flags::TopMost}, + parent) + , switcherModel_(this) + , switcherItemDelegate_(this) + , openPages_(openPages()) +{ + this->setWindowFlag(Qt::Dialog); + this->setActionOnFocusLoss(BaseWindow::ActionOnFocusLoss::Delete); + this->setMinimumSize(QuickSwitcherPopup::MINIMUM_SIZE); + + this->initWidgets(); + + this->setStayInScreenRect(true); + const QRect geom = parent->geometry(); + // This places the popup in the middle of the parent widget + this->setGeometry(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, + this->size(), geom)); +} + +QuickSwitcherPopup::~QuickSwitcherPopup() +{ +} + +void QuickSwitcherPopup::initWidgets() +{ + LayoutCreator creator(this->BaseWindow::getLayoutContainer()); + auto vbox = creator.setLayoutType(); + + { + vbox.emplace().assign(&this->ui_.searchEdit); + QObject::connect(this->ui_.searchEdit, &QLineEdit::textChanged, this, + &QuickSwitcherPopup::updateSuggestions); + + this->ui_.searchEdit->installEventFilter(this); + } + + { + 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_); + + /* + * 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(); + }); + } +} + +void QuickSwitcherPopup::updateSuggestions(const QString &text) +{ + this->switcherModel_.clear(); + + // Add items for navigating to different splits + for (auto *sc : this->openPages_) + { + const QString &tabTitle = sc->getTab()->getTitle(); + const auto splits = sc->getSplits(); + + // First, check for splits on this page + for (auto *split : splits) + { + if (split->getChannel()->getName().contains(text, + Qt::CaseInsensitive)) + { + SwitchSplitItem *item = new SwitchSplitItem(split); + this->switcherModel_.addItem(item); + + // We want to continue the outer loop so we need a goto + goto nextPage; + } + } + + // Then check if tab title matches + if (tabTitle.contains(text, Qt::CaseInsensitive)) + { + SwitchSplitItem *item = new SwitchSplitItem(sc); + this->switcherModel_.addItem(item); + continue; + } + + nextPage:; + } + + // Add item for opening a channel in a new tab + if (!text.isEmpty()) + { + NewTabItem *item = new NewTabItem(text); + this->switcherModel_.addItem(item); + } + + const auto &startIdx = this->switcherModel_.index(0); + this->ui_.list->setCurrentIndex(startIdx); + + /* + * Timeout interval 0 means the call will be delayed until all window events + * have been processed (cf. https://doc.qt.io/qt-5/qtimer.html#interval-prop). + */ + 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; +} + +} // namespace chatterino diff --git a/src/widgets/dialogs/switcher/QuickSwitcherPopup.hpp b/src/widgets/dialogs/switcher/QuickSwitcherPopup.hpp new file mode 100644 index 000000000..8bdd8058b --- /dev/null +++ b/src/widgets/dialogs/switcher/QuickSwitcherPopup.hpp @@ -0,0 +1,49 @@ +#pragma once + +#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" + +#include + +namespace chatterino { + +class QuickSwitcherPopup : public BasePopup +{ +public: + /** + * @brief Construct a new QuickSwitcherPopup. + * + * @param parent Parent widget of the popup. The popup will be placed + * in the center of the parent widget. + */ + explicit QuickSwitcherPopup(QWidget *parent = nullptr); + + ~QuickSwitcherPopup(); + +protected: + virtual bool eventFilter(QObject *watched, QEvent *event) override; + +public slots: + void updateSuggestions(const QString &text); + +private: + static const QSize MINIMUM_SIZE; + + struct { + QLineEdit *searchEdit{}; + QListView *list{}; + } ui_; + + QuickSwitcherModel switcherModel_; + SwitcherItemDelegate switcherItemDelegate_; + + QSet openPages_; + + void initWidgets(); +}; + +} // namespace chatterino diff --git a/src/widgets/dialogs/switcher/SwitchSplitItem.cpp b/src/widgets/dialogs/switcher/SwitchSplitItem.cpp new file mode 100644 index 000000000..f110eb55d --- /dev/null +++ b/src/widgets/dialogs/switcher/SwitchSplitItem.cpp @@ -0,0 +1,90 @@ +#include "widgets/dialogs/switcher/SwitchSplitItem.hpp" + +#include "Application.hpp" +#include "singletons/Fonts.hpp" +#include "singletons/Theme.hpp" +#include "widgets/helper/NotebookTab.hpp" + +namespace chatterino { + +SwitchSplitItem::SwitchSplitItem(Split *split) + : AbstractSwitcherItem(QIcon(":switcher/switch.svg")) + , split_(split) + , container_(split->getContainer()) +{ +} + +SwitchSplitItem::SwitchSplitItem(SplitContainer *container) + : AbstractSwitcherItem(QIcon(":switcher/switch.svg")) + , container_(container) +{ +} + +void SwitchSplitItem::action() +{ + auto &nb = getApp()->windows->getMainWindow().getNotebook(); + nb.select(this->container_); + + /* + * If the item is referring to a specific channel, select the + * corresponding split. + */ + if (this->split_) + { + this->container_->setSelected(this->split_); + } +} + +void SwitchSplitItem::paint(QPainter *painter, const QRect &rect) const +{ + painter->save(); + + painter->setRenderHint(QPainter::Antialiasing, true); + + // TODO(leon): Right pen/brush/font settings? + painter->setPen(getApp()->themes->splits.header.text); + painter->setBrush(Qt::SolidPattern); + painter->setFont(getApp()->fonts->getFont(FontStyle::UiMediumBold, 1.0)); + + QRect iconRect(rect.topLeft(), ICON_SIZE); + this->icon_.paint(painter, iconRect, Qt::AlignLeft | Qt::AlignVCenter); + + if (this->split_) + { + // Draw channel name and name of the containing tab + const auto availableTextWidth = rect.width() - iconRect.width(); + QRect leftTextRect = + QRect(iconRect.topRight(), + QSize(0.3 * availableTextWidth, iconRect.height())); + + painter->drawText(leftTextRect, Qt::AlignLeft | Qt::AlignVCenter, + this->split_->getChannel()->getName()); + + QRect rightTextRect = + QRect(leftTextRect.topRight(), + QSize(0.7 * availableTextWidth, iconRect.height())); + + painter->setFont(getApp()->fonts->getFont(FontStyle::UiMedium, 1.0)); + painter->drawText(rightTextRect, Qt::AlignRight | Qt::AlignVCenter, + this->container_->getTab()->getTitle()); + } + else if (!this->split_ && this->container_) + { + // Only draw name of tab + QRect textRect = + QRect(iconRect.topRight(), + QSize(rect.width() - iconRect.width(), iconRect.height())); + + painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, + this->container_->getTab()->getTitle()); + } + + painter->restore(); +} + +QSize SwitchSplitItem::sizeHint(const QRect &rect) const +{ + return QSize(rect.width(), ICON_SIZE.height()); +} + +} // namespace chatterino diff --git a/src/widgets/dialogs/switcher/SwitchSplitItem.hpp b/src/widgets/dialogs/switcher/SwitchSplitItem.hpp new file mode 100644 index 000000000..22e653a7f --- /dev/null +++ b/src/widgets/dialogs/switcher/SwitchSplitItem.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "widgets/dialogs/switcher/AbstractSwitcherItem.hpp" + +#include "singletons/WindowManager.hpp" +#include "widgets/Notebook.hpp" +#include "widgets/Window.hpp" +#include "widgets/helper/NotebookTab.hpp" +#include "widgets/splits/Split.hpp" + +namespace chatterino { + +class SwitchSplitItem : public AbstractSwitcherItem +{ +public: + SwitchSplitItem(Split *split); + SwitchSplitItem(SplitContainer *container); + + virtual void action() override; + + virtual void paint(QPainter *painter, const QRect &rect) const; + virtual QSize sizeHint(const QRect &rect) const; + +private: + Split *split_{}; + SplitContainer *container_{}; +}; + +} // namespace chatterino diff --git a/src/widgets/dialogs/switcher/SwitcherItemDelegate.cpp b/src/widgets/dialogs/switcher/SwitcherItemDelegate.cpp new file mode 100644 index 000000000..72bde7e27 --- /dev/null +++ b/src/widgets/dialogs/switcher/SwitcherItemDelegate.cpp @@ -0,0 +1,48 @@ +#include "widgets/dialogs/switcher/SwitcherItemDelegate.hpp" + +#include "widgets/dialogs/switcher/AbstractSwitcherItem.hpp" + +namespace chatterino { + +SwitcherItemDelegate::SwitcherItemDelegate(QObject *parent) + : QStyledItemDelegate(parent) +{ +} + +SwitcherItemDelegate::~SwitcherItemDelegate() +{ +} + +void SwitcherItemDelegate::paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + auto *item = AbstractSwitcherItem::fromVariant(index.data()); + + if (item) + { + if (option.state & QStyle::State_Selected) + painter->fillRect(option.rect, option.palette.highlight()); + + item->paint(painter, option.rect); + } + else + { + QStyledItemDelegate::paint(painter, option, index); + } +} + +QSize SwitcherItemDelegate::sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + auto *item = AbstractSwitcherItem::fromVariant(index.data()); + + if (item) + { + return item->sizeHint(option.rect); + } + + return QStyledItemDelegate::sizeHint(option, index); +} + +} // namespace chatterino diff --git a/src/widgets/dialogs/switcher/SwitcherItemDelegate.hpp b/src/widgets/dialogs/switcher/SwitcherItemDelegate.hpp new file mode 100644 index 000000000..a13578fc2 --- /dev/null +++ b/src/widgets/dialogs/switcher/SwitcherItemDelegate.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include + +namespace chatterino { + +class SwitcherItemDelegate : public QStyledItemDelegate +{ + Q_OBJECT + +public: + SwitcherItemDelegate(QObject *parent = nullptr); + ~SwitcherItemDelegate(); + + void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + + QSize sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &index) const override; +}; + +} // namespace chatterino diff --git a/src/widgets/splits/SplitContainer.hpp b/src/widgets/splits/SplitContainer.hpp index 59c3e4cbb..69a8e7a3c 100644 --- a/src/widgets/splits/SplitContainer.hpp +++ b/src/widgets/splits/SplitContainer.hpp @@ -182,6 +182,7 @@ public: Position deleteSplit(Split *split); void selectNextSplit(Direction direction); + void setSelected(Split *selected_); void decodeFromJson(QJsonObject &obj); @@ -214,7 +215,6 @@ protected: private: void layout(); - void setSelected(Split *selected_); void selectSplitRecursive(Node *node, Direction direction); void focusSplitRecursive(Node *node, Direction direction); void setPreferedTargetRecursive(Node *node);