From a9080ceb3ca71f0ed1125da1ea866eb926e668c7 Mon Sep 17 00:00:00 2001 From: Leon Richardt Date: Thu, 13 Aug 2020 19:25:51 +0200 Subject: [PATCH] Discord-like Quick Switcher (#1588) * Proof of Concept for Quick Switcher * Fix crash when suggestions are empty * QuickSwitcher: Use tab name instead of a single channel * Rebase later * Add missing include for * Move QuickSwitcher related classes into own subfolder * Refactor switcher list items Now, items are responsible for taking the right action when selected in the switcher list. This should allow for more focused code and responsibilities. * Add note about memory management * Add option to open channel in a new tab * Add support for using the mouse * Spawn switcher popup in the middle of the window Works reliably on i3 at least. Might need some additional testing on other WMs (and especially on Windows!). * Add some icons for switcher items Note that the final design of the list is not final but I do plan to incorporate these in the future. * Set Qt::Dialog window flag on switcher popup Prevents tiling window managers like i3 from trying to tile the window. * Rename "SwitcherItem" to "AbstractSwitcherItem" * Add comments about what items are inserted * Use custom model and view Still missing: Currently selected item is not highlighted yet. You can move between selected items with tab and arrow keys though. * Add helper function to convert QVariant to AbstractSwitcherItem * * Remove useless constant * Highlight currently selected switcher item * Use a different method for centering QuickSwitcherPopup window * QuickSwitcherModel: Add documentation * Add default parameter to QuickSwitcherModel::rowCount * QuickSwitcherPopup: Add comments * Remove outdated TODO * QuickSwitcherModel: Init vector with default capacity * Remove outdated comment * Add comment about 0 ms timeout interval * NewTabItem: Simplify interface * Only fetch opened splits once This is better than the prior approach since opened splits cannot change anyways while the switcher is open. * Use SplitContainer to pass information instead of custom type * Allow searching for tab titles as well Before this commit, only channel names could be searched. * Refactor switcher item interface to be more flexible Also show tab name and channel name in the switcher list. * Add documentation for AbstractSwitcherItem * Add documentation for NewTabItem * Add comments about {begin,end}{Insert,Remove}Rows * Remove unused method * Replace magic size with named constant * Add change log entry Co-authored-by: fourtf --- CHANGELOG.md | 1 + chatterino.pro | 30 ++- lib/libcommuni | 2 +- resources/resources_autogenerated.qrc | 2 + resources/switcher/plus.svg | 75 +++++++ resources/switcher/switch.svg | 104 +++++++++ src/widgets/Window.cpp | 7 + .../dialogs/switcher/AbstractSwitcherItem.cpp | 20 ++ .../dialogs/switcher/AbstractSwitcherItem.hpp | 45 ++++ src/widgets/dialogs/switcher/NewTabItem.cpp | 60 ++++++ src/widgets/dialogs/switcher/NewTabItem.hpp | 32 +++ .../dialogs/switcher/QuickSwitcherModel.cpp | 61 ++++++ .../dialogs/switcher/QuickSwitcherModel.hpp | 59 ++++++ .../dialogs/switcher/QuickSwitcherPopup.cpp | 200 ++++++++++++++++++ .../dialogs/switcher/QuickSwitcherPopup.hpp | 49 +++++ .../dialogs/switcher/SwitchSplitItem.cpp | 90 ++++++++ .../dialogs/switcher/SwitchSplitItem.hpp | 29 +++ .../dialogs/switcher/SwitcherItemDelegate.cpp | 48 +++++ .../dialogs/switcher/SwitcherItemDelegate.hpp | 22 ++ src/widgets/splits/SplitContainer.hpp | 2 +- 20 files changed, 927 insertions(+), 11 deletions(-) create mode 100644 resources/switcher/plus.svg create mode 100644 resources/switcher/switch.svg create mode 100644 src/widgets/dialogs/switcher/AbstractSwitcherItem.cpp create mode 100644 src/widgets/dialogs/switcher/AbstractSwitcherItem.hpp create mode 100644 src/widgets/dialogs/switcher/NewTabItem.cpp create mode 100644 src/widgets/dialogs/switcher/NewTabItem.hpp create mode 100644 src/widgets/dialogs/switcher/QuickSwitcherModel.cpp create mode 100644 src/widgets/dialogs/switcher/QuickSwitcherModel.hpp create mode 100644 src/widgets/dialogs/switcher/QuickSwitcherPopup.cpp create mode 100644 src/widgets/dialogs/switcher/QuickSwitcherPopup.hpp create mode 100644 src/widgets/dialogs/switcher/SwitchSplitItem.cpp create mode 100644 src/widgets/dialogs/switcher/SwitchSplitItem.hpp create mode 100644 src/widgets/dialogs/switcher/SwitcherItemDelegate.cpp create mode 100644 src/widgets/dialogs/switcher/SwitcherItemDelegate.hpp 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);