Mercurial > minori
diff src/gui/pages/anime_list.cpp @ 9:5c0397762b53
INCOMPLETE: megacommit :)
author | Paper <mrpapersonic@gmail.com> |
---|---|
date | Sun, 10 Sep 2023 03:59:16 -0400 |
parents | |
children | 4b198a111713 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/gui/pages/anime_list.cpp Sun Sep 10 03:59:16 2023 -0400 @@ -0,0 +1,468 @@ +/** + * 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/session.h" +#include "core/time.h" +#include "gui/dialog/information.h" +#include "gui/translate/anime.h" +#include "services/anilist.h" +#include <QHBoxLayout> +#include <QHeaderView> +#include <QMenu> +#include <QProgressBar> +#include <QShortcut> +#include <QStylePainter> +#include <QStyledItemDelegate> +#include <cmath> + +#if 0 +AnimeListWidgetDelegate::AnimeListWidgetDelegate(QObject* parent) : QStyledItemDelegate(parent) { +} + +QWidget* AnimeListWidgetDelegate::createEditor(QWidget*, const QStyleOptionViewItem&, const QModelIndex&) const { + // no edit 4 u + return nullptr; +} + +void AnimeListWidgetDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const { + switch (index.column()) { +/* + case AnimeListWidgetModel::AL_PROGRESS: { + const int progress = static_cast<int>(index.data(Qt::UserRole).toReal()); + const int episodes = + static_cast<int>(index.siblingAtColumn(AnimeListWidgetModel::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, "/", 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; + } +*/ + default: QStyledItemDelegate::paint(painter, option, index); break; + } +} + +AnimeListWidgetSortFilter::AnimeListWidgetSortFilter(QObject* parent) : QSortFilterProxyModel(parent) { +} + +bool AnimeListWidgetSortFilter::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; + } +} + +AnimeListWidgetModel::AnimeListWidgetModel(QWidget* parent) : QAbstractListModel(parent) { + return; +} + +int AnimeListWidgetModel::rowCount(const QModelIndex& parent) const { + int count = 0; + for (const auto& [id, anime] : Anime::db.items) { + if (anime.IsInUserList()) + count++; + } + return count; + (void)(parent); +} + +int AnimeListWidgetModel::columnCount(const QModelIndex& parent) const { + return NB_COLUMNS; + (void)(parent); +} + +QVariant AnimeListWidgetModel::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 AnimeListWidgetModel::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()].progress) + "/" + QString::number(list[index.row()].episodes); + case AL_EPISODES: return list[index.row()].episodes; + case AL_SCORE: return list[index.row()].score; + case AL_TYPE: return QString::fromStdString(Translate::TranslateSeriesFormat(list[index.row()].type)); + case AL_SEASON: + return QString::fromStdString(Translate::TranslateSeriesSeason(list[index.row()].season)) + " " + + QString::number(list[index.row()].air_date.GetYear()); + case AL_AVG_SCORE: return QString::number(list[index.row()].audience_score) + "%"; + case AL_STARTED: return list[index.row()].started.GetAsQDate(); + case AL_COMPLETED: return list[index.row()].completed.GetAsQDate(); + case AL_UPDATED: { + if (list[index.row()].updated == 0) + return QString("-"); + Time::Duration duration(Time::GetSystemTime() - list[index.row()].updated); + return QString::fromUtf8(duration.AsRelativeString().c_str()); + } + case AL_NOTES: return QString::fromUtf8(list[index.row()].notes.c_str()); + default: return ""; + } + break; + case Qt::UserRole: + switch (index.column()) { + case AL_ID: return + case AL_PROGRESS: return list[index.row()].progress; + case AL_TYPE: return list[index.row()].type; + case AL_SEASON: return list[index.row()].air_date.GetAsQDate(); + case AL_AVG_SCORE: return list[index.row()].audience_score; + case AL_UPDATED: return list[index.row()].updated; + 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(); +} + +void AnimeListWidgetModel::UpdateAnime(int id) { + /* meh... it might be better to just redraw the entire list */ + int i = 0; + for (const auto& [a_id, anime] : Anime:db.items) { + if (anime.IsInUserList() && a_id == id && anime.GetUserStatus() == Anime::ListStatus::WATCHING) { + emit dataChanged(index(i), index(i)); + } + i++; + } +} +#endif + +int AnimeListWidget::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 AnimeListWidget::SetColumnDefaults() { + tree_view->setColumnHidden(AnimeListWidgetModel::AL_SEASON, false); + tree_view->setColumnHidden(AnimeListWidgetModel::AL_TYPE, false); + tree_view->setColumnHidden(AnimeListWidgetModel::AL_UPDATED, false); + tree_view->setColumnHidden(AnimeListWidgetModel::AL_PROGRESS, false); + tree_view->setColumnHidden(AnimeListWidgetModel::AL_SCORE, false); + tree_view->setColumnHidden(AnimeListWidgetModel::AL_TITLE, false); + tree_view->setColumnHidden(AnimeListWidgetModel::AL_EPISODES, true); + tree_view->setColumnHidden(AnimeListWidgetModel::AL_AVG_SCORE, true); + tree_view->setColumnHidden(AnimeListWidgetModel::AL_STARTED, true); + tree_view->setColumnHidden(AnimeListWidgetModel::AL_COMPLETED, true); + tree_view->setColumnHidden(AnimeListWidgetModel::AL_UPDATED, true); + tree_view->setColumnHidden(AnimeListWidgetModel::AL_NOTES, true); +} + +void AnimeListWidget::DisplayColumnHeaderMenu() { + QMenu* menu = new QMenu(this); + menu->setAttribute(Qt::WA_DeleteOnClose); + menu->setTitle(tr("Column visibility")); + menu->setToolTipsVisible(true); + + for (int i = 0; i < AnimeListWidgetModel::NB_COLUMNS; i++) { + if (i == AnimeListWidgetModel::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 AnimeListWidget::DisplayListMenu() { + QMenu* menu = new QMenu(this); + menu->setAttribute(Qt::WA_DeleteOnClose); + menu->setTitle(tr("Column visibility")); + menu->setToolTipsVisible(true); + + const QItemSelection selection = sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection()); + if (!selection.indexes().first().isValid()) { + return; + } + +/* + QAction* action = menu->addAction("Information", [this, selection] { + const QModelIndex index = ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel()) + ->index(selection.indexes().first().row()); + Anime::Anime* anime = + ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->GetAnimeFromIndex(index); + if (!anime) { + return; + } + + InformationDialog* dialog = new InformationDialog( + *anime, + [this, anime] { + ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->UpdateAnime(*anime); + }, + this); + + dialog->show(); + dialog->raise(); + dialog->activateWindow(); + }); +*/ + menu->popup(QCursor::pos()); +} + +void AnimeListWidget::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; + } + +/* + const QModelIndex index = ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel()) + ->index(selection.indexes().first().row()); + Anime::Anime* anime = + ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->GetAnimeFromIndex(index); + if (!anime) { + return; + } + + InformationDialog* dialog = new InformationDialog(*anime, [this, anime] { + ((AnimeListWidgetModel*)sort_models[tab_bar->currentIndex()]->sourceModel())->UpdateAnime(*anime); + }, this); + + dialog->show(); + dialog->raise(); + dialog->activateWindow(); +*/ +} + +void AnimeListWidget::paintEvent(QPaintEvent*) { + QStylePainter p(this); + + QStyleOptionTabWidgetFrame opt; + InitStyle(&opt); + opt.rect = panelRect; + p.drawPrimitive(QStyle::PE_FrameTabWidget, opt); +} + +void AnimeListWidget::resizeEvent(QResizeEvent* e) { + QWidget::resizeEvent(e); + SetupLayout(); +} + +void AnimeListWidget::showEvent(QShowEvent*) { + SetupLayout(); +} + +void AnimeListWidget::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 AnimeListWidget::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 AnimeListWidget::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); +} + +AnimeListWidget::AnimeListWidget(QWidget* parent) : QWidget(parent) { + /* Tab bar */ + tab_bar = new QTabBar(this); + tab_bar->setExpanding(false); + tab_bar->setDrawBase(false); + for (int i = 0; i < ARRAYSIZE(sort_models); i++) { + tab_bar->addTab(QString::fromStdString(Translate::TranslateListStatus(Anime::ListStatuses[i]))); + + /* Tree view... */ + QWidget* tree_widget = new QWidget(this); + tree_view = new QTreeView(tree_widget); + tree_view->setItemDelegate(new AnimeListWidgetDelegate(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); + + QHBoxLayout* layout = new QHBoxLayout; + layout->addWidget(tree_view); + layout->setMargin(0); + tree_widget->setLayout(layout); + + /* Double click stuff */ + connect(tree_view, &QAbstractItemView::doubleClicked, this, &AnimeListWidget::ItemDoubleClicked); + connect(tree_view, &QWidget::customContextMenuRequested, this, &AnimeListWidget::DisplayListMenu); + + /* Enter & return keys */ + connect(new QShortcut(Qt::Key_Return, tree_view, nullptr, nullptr, Qt::WidgetShortcut), &QShortcut::activated, + this, &AnimeListWidget::ItemDoubleClicked); + + connect(new QShortcut(Qt::Key_Enter, tree_view, nullptr, nullptr, Qt::WidgetShortcut), &QShortcut::activated, + this, &AnimeListWidget::ItemDoubleClicked); + + tree_view->header()->setStretchLastSection(false); + tree_view->header()->setContextMenuPolicy(Qt::CustomContextMenu); + connect(tree_view->header(), &QWidget::customContextMenuRequested, this, + &AnimeListWidget::DisplayColumnHeaderMenu); + + connect(tab_bar, &QTabBar::currentChanged, this, [this](int index) { + if (sort_models[index]) + tree_view->setModel(sort_models[index]); + }); + + setFocusPolicy(Qt::TabFocus); + setFocusProxy(tab_bar); + } + + void AnimeListWidget::UpdateAnimeList() { + for (unsigned int i = 0; i < ARRAYSIZE(sort_models); i++) { + sort_models[i] = new AnimeListWidgetSortFilter(tree_view); + sort_models[i]->setSourceModel(new AnimeListWidgetModel(this, &anime_lists[i])); + sort_models[i]->setSortRole(Qt::UserRole); + sort_models[i]->setSortCaseSensitivity(Qt::CaseInsensitive); + } + if (ARRAYSIZE(sort_models) > 0) + tree_view->setModel(sort_models[0]); + SetColumnDefaults(); + SetupLayout(); + } + + void AnimeListWidget::Reset() { + while (tab_bar->count()) + tab_bar->removeTab(0); + for (int i = 0; i < ARRAYSIZE(sort_models); i++) + delete sort_models[i]; + } + +#include "gui/pages/moc_anime_list.cpp"