Mercurial > minori
diff src/gui/pages/anime_list.cc @ 81:9b2b41f83a5e
boring: mass rename to cc
because this is a very unix-y project, it makes more sense to use the
'cc' extension
author | Paper <mrpapersonic@gmail.com> |
---|---|
date | Mon, 23 Oct 2023 12:07:27 -0400 |
parents | src/gui/pages/anime_list.cpp@6f7385bd334c |
children | d02fdf1d6708 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/gui/pages/anime_list.cc Mon Oct 23 12:07:27 2023 -0400 @@ -0,0 +1,504 @@ +/** + * anime_list.cpp: defines the anime list page + * and widgets. + * + * much of this file is based around + * Qt's original QTabWidget implementation, because + * I needed a somewhat native way to create a tabbed + * widget with only one subwidget that worked exactly + * like a native tabbed widget. + **/ +#include "gui/pages/anime_list.h" +#include "core/anime.h" +#include "core/anime_db.h" +#include "core/array.h" +#include "core/session.h" +#include "core/strings.h" +#include "core/time.h" +#include "gui/dialog/information.h" +#include "gui/translate/anime.h" +#include "services/services.h" +#include <QDebug> +#include <QHBoxLayout> +#include <QHeaderView> +#include <QMenu> +#include <QProgressBar> +#include <QShortcut> +#include <QStylePainter> +#include <QStyledItemDelegate> +#include <QThreadPool> +#include <set> + +AnimeListPageDelegate::AnimeListPageDelegate(QObject* parent) : QStyledItemDelegate(parent) { +} + +QWidget* AnimeListPageDelegate::createEditor(QWidget*, const QStyleOptionViewItem&, const QModelIndex&) const { + // no edit 4 u + return nullptr; +} + +void AnimeListPageDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const { + switch (index.column()) { +#if 0 + case AnimeListPageModel::AL_PROGRESS: { + const int progress = static_cast<int>(index.data(Qt::UserRole).toReal()); + const int episodes = + static_cast<int>(index.siblingAtColumn(AnimeListPageModel::AL_EPISODES).data(Qt::UserRole).toReal()); + + int text_width = 59; + QRectF text_rect(option.rect.x() + text_width, option.rect.y(), text_width, option.decorationSize.height()); + painter->save(); + painter->drawText(text_rect, tr("/"), QTextOption(Qt::AlignCenter | Qt::AlignVCenter)); + drawText(const QRectF &rectangle, const QString &text, const QTextOption &option = + QTextOption()) painter->drawText(QRectF(text_rect.x(), text_rect.y(), text_width / 2 - 2, + text_rect.height()), QString::number(progress), QTextOption(Qt::AlignRight | Qt::AlignVCenter)); + painter->drawText( + QRectF(text_rect.x() + text_width / 2 + 2, text_rect.y(), text_width / 2 - 2, text_rect.height()), + QString::number(episodes), QTextOption(Qt::AlignLeft | Qt::AlignVCenter)); + painter->restore(); + QStyledItemDelegate::paint(painter, option, index); + break; + } +#endif + default: QStyledItemDelegate::paint(painter, option, index); break; + } +} + +AnimeListPageSortFilter::AnimeListPageSortFilter(QObject* parent) : QSortFilterProxyModel(parent) { +} + +bool AnimeListPageSortFilter::lessThan(const QModelIndex& l, const QModelIndex& r) const { + QVariant left = sourceModel()->data(l, sortRole()); + QVariant right = sourceModel()->data(r, sortRole()); + + switch (left.userType()) { + case QMetaType::Int: + case QMetaType::UInt: + case QMetaType::LongLong: + case QMetaType::ULongLong: return left.toInt() < right.toInt(); + case QMetaType::QDate: return left.toDate() < right.toDate(); + case QMetaType::QString: + default: return QString::compare(left.toString(), right.toString(), Qt::CaseInsensitive) < 0; + } +} + +AnimeListPageModel::AnimeListPageModel(QWidget* parent, Anime::ListStatus _status) : QAbstractListModel(parent) { + status = _status; + return; +} + +int AnimeListPageModel::rowCount(const QModelIndex& parent) const { + return list.size(); + (void)(parent); +} + +int AnimeListPageModel::columnCount(const QModelIndex& parent) const { + return NB_COLUMNS; + (void)(parent); +} + +QVariant AnimeListPageModel::headerData(const int section, const Qt::Orientation orientation, const int role) const { + if (role == Qt::DisplayRole) { + switch (section) { + case AL_TITLE: return tr("Anime title"); + case AL_PROGRESS: return tr("Progress"); + case AL_EPISODES: return tr("Episodes"); + case AL_TYPE: return tr("Type"); + case AL_SCORE: return tr("Score"); + case AL_SEASON: return tr("Season"); + case AL_STARTED: return tr("Date started"); + case AL_COMPLETED: return tr("Date completed"); + case AL_NOTES: return tr("Notes"); + case AL_AVG_SCORE: return tr("Average score"); + case AL_UPDATED: return tr("Last updated"); + default: return {}; + } + } else if (role == Qt::TextAlignmentRole) { + switch (section) { + case AL_TITLE: + case AL_NOTES: return QVariant(Qt::AlignLeft | Qt::AlignVCenter); + case AL_PROGRESS: + case AL_EPISODES: + case AL_TYPE: + case AL_SCORE: + case AL_AVG_SCORE: return QVariant(Qt::AlignCenter | Qt::AlignVCenter); + case AL_SEASON: + case AL_STARTED: + case AL_COMPLETED: + case AL_UPDATED: return QVariant(Qt::AlignRight | Qt::AlignVCenter); + default: return QAbstractListModel::headerData(section, orientation, role); + } + } + return QAbstractListModel::headerData(section, orientation, role); +} + +QVariant AnimeListPageModel::data(const QModelIndex& index, int role) const { + if (!index.isValid()) + return QVariant(); + switch (role) { + case Qt::DisplayRole: + switch (index.column()) { + case AL_TITLE: return QString::fromUtf8(list[index.row()].GetUserPreferredTitle().c_str()); + case AL_PROGRESS: + return QString::number(list[index.row()].GetUserProgress()) + "/" + + QString::number(list[index.row()].GetEpisodes()); + case AL_EPISODES: return list[index.row()].GetEpisodes(); + case AL_SCORE: return list[index.row()].GetUserScore(); + case AL_TYPE: return Strings::ToQString(Translate::ToString(list[index.row()].GetFormat())); + case AL_SEASON: + return Strings::ToQString(Translate::ToString(list[index.row()].GetSeason())) + " " + + QString::number(list[index.row()].GetAirDate().GetYear()); + case AL_AVG_SCORE: return QString::number(list[index.row()].GetAudienceScore()) + "%"; + case AL_STARTED: return list[index.row()].GetUserDateStarted().GetAsQDate(); + case AL_COMPLETED: return list[index.row()].GetUserDateCompleted().GetAsQDate(); + case AL_UPDATED: { + if (list[index.row()].GetUserTimeUpdated() == 0) + return QString("-"); + Time::Duration duration(Time::GetSystemTime() - list[index.row()].GetUserTimeUpdated()); + return QString::fromUtf8(duration.AsRelativeString().c_str()); + } + case AL_NOTES: return QString::fromUtf8(list[index.row()].GetUserNotes().c_str()); + default: return ""; + } + break; + case Qt::UserRole: + switch (index.column()) { + case AL_PROGRESS: return list[index.row()].GetUserProgress(); + case AL_TYPE: return static_cast<int>(list[index.row()].GetFormat()); + case AL_SEASON: return list[index.row()].GetAirDate().GetAsQDate(); + case AL_AVG_SCORE: return list[index.row()].GetAudienceScore(); + case AL_UPDATED: return QVariant::fromValue(list[index.row()].GetUserTimeUpdated()); + default: return data(index, Qt::DisplayRole); + } + break; + case Qt::TextAlignmentRole: + switch (index.column()) { + case AL_TITLE: + case AL_NOTES: return QVariant(Qt::AlignLeft | Qt::AlignVCenter); + case AL_PROGRESS: + case AL_EPISODES: + case AL_TYPE: + case AL_SCORE: + case AL_AVG_SCORE: return QVariant(Qt::AlignCenter | Qt::AlignVCenter); + case AL_SEASON: + case AL_STARTED: + case AL_COMPLETED: + case AL_UPDATED: return QVariant(Qt::AlignRight | Qt::AlignVCenter); + default: break; + } + break; + } + return QVariant(); +} + +Anime::Anime* AnimeListPageModel::GetAnimeFromIndex(QModelIndex index) { + return &list.at(index.row()); +} + +void AnimeListPageModel::RefreshList() { + bool has_children = !!rowCount(index(0)); + if (!has_children) { + beginInsertRows(QModelIndex(), 0, 0); + endInsertRows(); + } + + beginResetModel(); + + list.clear(); + + for (const auto& a : Anime::db.items) { + if (a.second.IsInUserList() && a.second.GetUserStatus() == status) { + list.push_back(a.second); + } + } + + endResetModel(); +} + +int AnimeListPage::VisibleColumnsCount() const { + int count = 0; + + for (int i = 0, end = tree_view->header()->count(); i < end; i++) { + if (!tree_view->isColumnHidden(i)) + count++; + } + + return count; +} + +void AnimeListPage::SetColumnDefaults() { + tree_view->setColumnHidden(AnimeListPageModel::AL_SEASON, false); + tree_view->setColumnHidden(AnimeListPageModel::AL_TYPE, false); + tree_view->setColumnHidden(AnimeListPageModel::AL_UPDATED, false); + tree_view->setColumnHidden(AnimeListPageModel::AL_PROGRESS, false); + tree_view->setColumnHidden(AnimeListPageModel::AL_SCORE, false); + tree_view->setColumnHidden(AnimeListPageModel::AL_TITLE, false); + tree_view->setColumnHidden(AnimeListPageModel::AL_EPISODES, true); + tree_view->setColumnHidden(AnimeListPageModel::AL_AVG_SCORE, true); + tree_view->setColumnHidden(AnimeListPageModel::AL_STARTED, true); + tree_view->setColumnHidden(AnimeListPageModel::AL_COMPLETED, true); + tree_view->setColumnHidden(AnimeListPageModel::AL_UPDATED, true); + tree_view->setColumnHidden(AnimeListPageModel::AL_NOTES, true); +} + +void AnimeListPage::UpdateAnime(int id) { + QThreadPool::globalInstance()->start([this, id] { + Services::UpdateAnimeEntry(id); + Refresh(); + }); +} + +void AnimeListPage::RemoveAnime(int id) { + Anime::Anime& anime = Anime::db.items[id]; + anime.RemoveFromUserList(); + Refresh(); +} + +void AnimeListPage::DisplayColumnHeaderMenu() { + QMenu* menu = new QMenu(this); + menu->setAttribute(Qt::WA_DeleteOnClose); + menu->setTitle(tr("Column visibility")); + menu->setToolTipsVisible(true); + + for (int i = 0; i < AnimeListPageModel::NB_COLUMNS; i++) { + if (i == AnimeListPageModel::AL_TITLE) + continue; + const auto column_name = + sort_models[tab_bar->currentIndex()]->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString(); + QAction* action = menu->addAction(column_name, this, [this, i](const bool checked) { + if (!checked && (VisibleColumnsCount() <= 1)) + return; + + tree_view->setColumnHidden(i, !checked); + + if (checked && (tree_view->columnWidth(i) <= 5)) + tree_view->resizeColumnToContents(i); + + // SaveSettings(); + }); + action->setCheckable(true); + action->setChecked(!tree_view->isColumnHidden(i)); + } + + menu->addSeparator(); + QAction* resetAction = menu->addAction(tr("Reset to defaults"), this, [this]() { + for (int i = 0, count = tree_view->header()->count(); i < count; ++i) { + SetColumnDefaults(); + } + // SaveSettings(); + }); + menu->popup(QCursor::pos()); + (void)(resetAction); +} + +void AnimeListPage::DisplayListMenu() { + QMenu* menu = new QMenu(this); + menu->setAttribute(Qt::WA_DeleteOnClose); + menu->setTitle(tr("Column visibility")); + menu->setToolTipsVisible(true); + + AnimeListPageModel* source_model = + reinterpret_cast<AnimeListPageModel*>(sort_models[tab_bar->currentIndex()]->sourceModel()); + const QItemSelection selection = + sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection()); + + std::set<Anime::Anime*> animes; + for (const auto& index : selection.indexes()) { + if (!index.isValid()) + continue; + Anime::Anime* anime = source_model->GetAnimeFromIndex(index); + if (anime) + animes.insert(anime); + } + + QAction* action = menu->addAction(tr("Information"), [this, animes] { + for (auto& anime : animes) { + InformationDialog* dialog = new InformationDialog( + *anime, [this, anime] { UpdateAnime(anime->GetId()); }, this); + + dialog->show(); + dialog->raise(); + dialog->activateWindow(); + } + }); + menu->addSeparator(); + action = menu->addAction(tr("Delete from list..."), [this, animes] { + for (auto& anime : animes) { + RemoveAnime(anime->GetId()); + } + }); + menu->popup(QCursor::pos()); +} + +void AnimeListPage::ItemDoubleClicked() { + /* throw out any other garbage */ + const QItemSelection selection = + sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection()); + if (!selection.indexes().first().isValid()) { + return; + } + + AnimeListPageModel* source_model = + reinterpret_cast<AnimeListPageModel*>(sort_models[tab_bar->currentIndex()]->sourceModel()); + + const QModelIndex index = source_model->index(selection.indexes().first().row()); + Anime::Anime* anime = source_model->GetAnimeFromIndex(index); + + InformationDialog* dialog = new InformationDialog( + *anime, [this, anime] { UpdateAnime(anime->GetId()); }, this); + + dialog->show(); + dialog->raise(); + dialog->activateWindow(); +} + +void AnimeListPage::paintEvent(QPaintEvent*) { + QStylePainter p(this); + + QStyleOptionTabWidgetFrame opt; + InitStyle(&opt); + opt.rect = panelRect; + p.drawPrimitive(QStyle::PE_FrameTabWidget, opt); +} + +void AnimeListPage::resizeEvent(QResizeEvent* e) { + QWidget::resizeEvent(e); + SetupLayout(); +} + +void AnimeListPage::showEvent(QShowEvent*) { + SetupLayout(); +} + +void AnimeListPage::InitBasicStyle(QStyleOptionTabWidgetFrame* option) const { + if (!option) + return; + + option->initFrom(this); + option->lineWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth, nullptr, this); + option->shape = QTabBar::RoundedNorth; + option->tabBarRect = tab_bar->geometry(); +} + +void AnimeListPage::InitStyle(QStyleOptionTabWidgetFrame* option) const { + if (!option) + return; + + InitBasicStyle(option); + + // int exth = style()->pixelMetric(QStyle::PM_TabBarBaseHeight, nullptr, this); + QSize t(0, tree_view->frameWidth()); + if (tab_bar->isVisibleTo(this)) { + t = tab_bar->sizeHint(); + t.setWidth(width()); + } + + option->tabBarSize = t; + + QRect selected_tab_rect = tab_bar->tabRect(tab_bar->currentIndex()); + selected_tab_rect.moveTopLeft(selected_tab_rect.topLeft() + option->tabBarRect.topLeft()); + option->selectedTabRect = selected_tab_rect; + + option->lineWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth, nullptr, this); +} + +void AnimeListPage::SetupLayout() { + QStyleOptionTabWidgetFrame option; + InitStyle(&option); + + QRect tabRect = style()->subElementRect(QStyle::SE_TabWidgetTabBar, &option, this); + tabRect.setLeft(tabRect.left() + 1); + panelRect = style()->subElementRect(QStyle::SE_TabWidgetTabPane, &option, this); + QRect contentsRect = style()->subElementRect(QStyle::SE_TabWidgetTabContents, &option, this); + + tab_bar->setGeometry(tabRect); + tree_view->parentWidget()->setGeometry(contentsRect); +} + +AnimeListPage::AnimeListPage(QWidget* parent) : QWidget(parent) { + /* Tab bar */ + tab_bar = new QTabBar(this); + tab_bar->setExpanding(false); + tab_bar->setDrawBase(false); + + /* Tree view... */ + QWidget* tree_widget = new QWidget(this); + tree_view = new QTreeView(tree_widget); + tree_view->setItemDelegate(new AnimeListPageDelegate(tree_view)); + tree_view->setUniformRowHeights(true); + tree_view->setAllColumnsShowFocus(false); + tree_view->setAlternatingRowColors(true); + tree_view->setSortingEnabled(true); + tree_view->setSelectionMode(QAbstractItemView::ExtendedSelection); + tree_view->setItemsExpandable(false); + tree_view->setRootIsDecorated(false); + tree_view->setContextMenuPolicy(Qt::CustomContextMenu); + tree_view->setFrameShape(QFrame::NoFrame); + + for (unsigned int i = 0; i < ARRAYSIZE(sort_models); i++) { + tab_bar->addTab(Strings::ToQString(Translate::ToString(Anime::ListStatuses[i])) + " (" + + QString::number(Anime::db.GetListsAnimeAmount(Anime::ListStatuses[i])) + ")"); + sort_models[i] = new AnimeListPageSortFilter(tree_view); + sort_models[i]->setSourceModel(new AnimeListPageModel(this, Anime::ListStatuses[i])); + sort_models[i]->setSortRole(Qt::UserRole); + sort_models[i]->setSortCaseSensitivity(Qt::CaseInsensitive); + } + tree_view->setModel(sort_models[0]); + + QHBoxLayout* layout = new QHBoxLayout(tree_widget); + layout->addWidget(tree_view); + layout->setContentsMargins(0, 0, 0, 0); + + /* Double click stuff */ + connect(tree_view, &QAbstractItemView::doubleClicked, this, &AnimeListPage::ItemDoubleClicked); + connect(tree_view, &QWidget::customContextMenuRequested, this, &AnimeListPage::DisplayListMenu); + + /* Enter & return keys */ + connect(new QShortcut(Qt::Key_Return, tree_view, nullptr, nullptr, Qt::WidgetShortcut), &QShortcut::activated, this, + &AnimeListPage::ItemDoubleClicked); + + connect(new QShortcut(Qt::Key_Enter, tree_view, nullptr, nullptr, Qt::WidgetShortcut), &QShortcut::activated, this, + &AnimeListPage::ItemDoubleClicked); + + tree_view->header()->setStretchLastSection(false); + tree_view->header()->setContextMenuPolicy(Qt::CustomContextMenu); + connect(tree_view->header(), &QWidget::customContextMenuRequested, this, &AnimeListPage::DisplayColumnHeaderMenu); + + connect(tab_bar, &QTabBar::currentChanged, this, [this](int index) { + if (sort_models[index]) + tree_view->setModel(sort_models[index]); + }); + + SetColumnDefaults(); + setFocusPolicy(Qt::TabFocus); + setFocusProxy(tab_bar); +} + +void AnimeListPage::RefreshList() { + for (unsigned int i = 0; i < ARRAYSIZE(sort_models); i++) + reinterpret_cast<AnimeListPageModel*>(sort_models[i]->sourceModel())->RefreshList(); +} + +void AnimeListPage::RefreshTabs() { + for (unsigned int i = 0; i < ARRAYSIZE(sort_models); i++) + tab_bar->setTabText(i, Strings::ToQString(Translate::ToString(Anime::ListStatuses[i])) + " (" + + QString::number(Anime::db.GetListsAnimeAmount(Anime::ListStatuses[i])) + ")"); +} + +void AnimeListPage::Refresh() { + RefreshList(); + RefreshTabs(); +} + +/* This function, really, really should not be called. + Ever. Why would you ever need to clear the anime list? + Also, this sucks. */ +void AnimeListPage::Reset() { + while (tab_bar->count()) + tab_bar->removeTab(0); + for (unsigned int i = 0; i < ARRAYSIZE(sort_models); i++) + delete sort_models[i]; +} + +#include "gui/pages/moc_anime_list.cpp"