comparison 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
comparison
equal deleted inserted replaced
80:825506f0e221 81:9b2b41f83a5e
1 /**
2 * anime_list.cpp: defines the anime list page
3 * and widgets.
4 *
5 * much of this file is based around
6 * Qt's original QTabWidget implementation, because
7 * I needed a somewhat native way to create a tabbed
8 * widget with only one subwidget that worked exactly
9 * like a native tabbed widget.
10 **/
11 #include "gui/pages/anime_list.h"
12 #include "core/anime.h"
13 #include "core/anime_db.h"
14 #include "core/array.h"
15 #include "core/session.h"
16 #include "core/strings.h"
17 #include "core/time.h"
18 #include "gui/dialog/information.h"
19 #include "gui/translate/anime.h"
20 #include "services/services.h"
21 #include <QDebug>
22 #include <QHBoxLayout>
23 #include <QHeaderView>
24 #include <QMenu>
25 #include <QProgressBar>
26 #include <QShortcut>
27 #include <QStylePainter>
28 #include <QStyledItemDelegate>
29 #include <QThreadPool>
30 #include <set>
31
32 AnimeListPageDelegate::AnimeListPageDelegate(QObject* parent) : QStyledItemDelegate(parent) {
33 }
34
35 QWidget* AnimeListPageDelegate::createEditor(QWidget*, const QStyleOptionViewItem&, const QModelIndex&) const {
36 // no edit 4 u
37 return nullptr;
38 }
39
40 void AnimeListPageDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option,
41 const QModelIndex& index) const {
42 switch (index.column()) {
43 #if 0
44 case AnimeListPageModel::AL_PROGRESS: {
45 const int progress = static_cast<int>(index.data(Qt::UserRole).toReal());
46 const int episodes =
47 static_cast<int>(index.siblingAtColumn(AnimeListPageModel::AL_EPISODES).data(Qt::UserRole).toReal());
48
49 int text_width = 59;
50 QRectF text_rect(option.rect.x() + text_width, option.rect.y(), text_width, option.decorationSize.height());
51 painter->save();
52 painter->drawText(text_rect, tr("/"), QTextOption(Qt::AlignCenter | Qt::AlignVCenter));
53 drawText(const QRectF &rectangle, const QString &text, const QTextOption &option =
54 QTextOption()) painter->drawText(QRectF(text_rect.x(), text_rect.y(), text_width / 2 - 2,
55 text_rect.height()), QString::number(progress), QTextOption(Qt::AlignRight | Qt::AlignVCenter));
56 painter->drawText(
57 QRectF(text_rect.x() + text_width / 2 + 2, text_rect.y(), text_width / 2 - 2, text_rect.height()),
58 QString::number(episodes), QTextOption(Qt::AlignLeft | Qt::AlignVCenter));
59 painter->restore();
60 QStyledItemDelegate::paint(painter, option, index);
61 break;
62 }
63 #endif
64 default: QStyledItemDelegate::paint(painter, option, index); break;
65 }
66 }
67
68 AnimeListPageSortFilter::AnimeListPageSortFilter(QObject* parent) : QSortFilterProxyModel(parent) {
69 }
70
71 bool AnimeListPageSortFilter::lessThan(const QModelIndex& l, const QModelIndex& r) const {
72 QVariant left = sourceModel()->data(l, sortRole());
73 QVariant right = sourceModel()->data(r, sortRole());
74
75 switch (left.userType()) {
76 case QMetaType::Int:
77 case QMetaType::UInt:
78 case QMetaType::LongLong:
79 case QMetaType::ULongLong: return left.toInt() < right.toInt();
80 case QMetaType::QDate: return left.toDate() < right.toDate();
81 case QMetaType::QString:
82 default: return QString::compare(left.toString(), right.toString(), Qt::CaseInsensitive) < 0;
83 }
84 }
85
86 AnimeListPageModel::AnimeListPageModel(QWidget* parent, Anime::ListStatus _status) : QAbstractListModel(parent) {
87 status = _status;
88 return;
89 }
90
91 int AnimeListPageModel::rowCount(const QModelIndex& parent) const {
92 return list.size();
93 (void)(parent);
94 }
95
96 int AnimeListPageModel::columnCount(const QModelIndex& parent) const {
97 return NB_COLUMNS;
98 (void)(parent);
99 }
100
101 QVariant AnimeListPageModel::headerData(const int section, const Qt::Orientation orientation, const int role) const {
102 if (role == Qt::DisplayRole) {
103 switch (section) {
104 case AL_TITLE: return tr("Anime title");
105 case AL_PROGRESS: return tr("Progress");
106 case AL_EPISODES: return tr("Episodes");
107 case AL_TYPE: return tr("Type");
108 case AL_SCORE: return tr("Score");
109 case AL_SEASON: return tr("Season");
110 case AL_STARTED: return tr("Date started");
111 case AL_COMPLETED: return tr("Date completed");
112 case AL_NOTES: return tr("Notes");
113 case AL_AVG_SCORE: return tr("Average score");
114 case AL_UPDATED: return tr("Last updated");
115 default: return {};
116 }
117 } else if (role == Qt::TextAlignmentRole) {
118 switch (section) {
119 case AL_TITLE:
120 case AL_NOTES: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
121 case AL_PROGRESS:
122 case AL_EPISODES:
123 case AL_TYPE:
124 case AL_SCORE:
125 case AL_AVG_SCORE: return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
126 case AL_SEASON:
127 case AL_STARTED:
128 case AL_COMPLETED:
129 case AL_UPDATED: return QVariant(Qt::AlignRight | Qt::AlignVCenter);
130 default: return QAbstractListModel::headerData(section, orientation, role);
131 }
132 }
133 return QAbstractListModel::headerData(section, orientation, role);
134 }
135
136 QVariant AnimeListPageModel::data(const QModelIndex& index, int role) const {
137 if (!index.isValid())
138 return QVariant();
139 switch (role) {
140 case Qt::DisplayRole:
141 switch (index.column()) {
142 case AL_TITLE: return QString::fromUtf8(list[index.row()].GetUserPreferredTitle().c_str());
143 case AL_PROGRESS:
144 return QString::number(list[index.row()].GetUserProgress()) + "/" +
145 QString::number(list[index.row()].GetEpisodes());
146 case AL_EPISODES: return list[index.row()].GetEpisodes();
147 case AL_SCORE: return list[index.row()].GetUserScore();
148 case AL_TYPE: return Strings::ToQString(Translate::ToString(list[index.row()].GetFormat()));
149 case AL_SEASON:
150 return Strings::ToQString(Translate::ToString(list[index.row()].GetSeason())) + " " +
151 QString::number(list[index.row()].GetAirDate().GetYear());
152 case AL_AVG_SCORE: return QString::number(list[index.row()].GetAudienceScore()) + "%";
153 case AL_STARTED: return list[index.row()].GetUserDateStarted().GetAsQDate();
154 case AL_COMPLETED: return list[index.row()].GetUserDateCompleted().GetAsQDate();
155 case AL_UPDATED: {
156 if (list[index.row()].GetUserTimeUpdated() == 0)
157 return QString("-");
158 Time::Duration duration(Time::GetSystemTime() - list[index.row()].GetUserTimeUpdated());
159 return QString::fromUtf8(duration.AsRelativeString().c_str());
160 }
161 case AL_NOTES: return QString::fromUtf8(list[index.row()].GetUserNotes().c_str());
162 default: return "";
163 }
164 break;
165 case Qt::UserRole:
166 switch (index.column()) {
167 case AL_PROGRESS: return list[index.row()].GetUserProgress();
168 case AL_TYPE: return static_cast<int>(list[index.row()].GetFormat());
169 case AL_SEASON: return list[index.row()].GetAirDate().GetAsQDate();
170 case AL_AVG_SCORE: return list[index.row()].GetAudienceScore();
171 case AL_UPDATED: return QVariant::fromValue(list[index.row()].GetUserTimeUpdated());
172 default: return data(index, Qt::DisplayRole);
173 }
174 break;
175 case Qt::TextAlignmentRole:
176 switch (index.column()) {
177 case AL_TITLE:
178 case AL_NOTES: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
179 case AL_PROGRESS:
180 case AL_EPISODES:
181 case AL_TYPE:
182 case AL_SCORE:
183 case AL_AVG_SCORE: return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
184 case AL_SEASON:
185 case AL_STARTED:
186 case AL_COMPLETED:
187 case AL_UPDATED: return QVariant(Qt::AlignRight | Qt::AlignVCenter);
188 default: break;
189 }
190 break;
191 }
192 return QVariant();
193 }
194
195 Anime::Anime* AnimeListPageModel::GetAnimeFromIndex(QModelIndex index) {
196 return &list.at(index.row());
197 }
198
199 void AnimeListPageModel::RefreshList() {
200 bool has_children = !!rowCount(index(0));
201 if (!has_children) {
202 beginInsertRows(QModelIndex(), 0, 0);
203 endInsertRows();
204 }
205
206 beginResetModel();
207
208 list.clear();
209
210 for (const auto& a : Anime::db.items) {
211 if (a.second.IsInUserList() && a.second.GetUserStatus() == status) {
212 list.push_back(a.second);
213 }
214 }
215
216 endResetModel();
217 }
218
219 int AnimeListPage::VisibleColumnsCount() const {
220 int count = 0;
221
222 for (int i = 0, end = tree_view->header()->count(); i < end; i++) {
223 if (!tree_view->isColumnHidden(i))
224 count++;
225 }
226
227 return count;
228 }
229
230 void AnimeListPage::SetColumnDefaults() {
231 tree_view->setColumnHidden(AnimeListPageModel::AL_SEASON, false);
232 tree_view->setColumnHidden(AnimeListPageModel::AL_TYPE, false);
233 tree_view->setColumnHidden(AnimeListPageModel::AL_UPDATED, false);
234 tree_view->setColumnHidden(AnimeListPageModel::AL_PROGRESS, false);
235 tree_view->setColumnHidden(AnimeListPageModel::AL_SCORE, false);
236 tree_view->setColumnHidden(AnimeListPageModel::AL_TITLE, false);
237 tree_view->setColumnHidden(AnimeListPageModel::AL_EPISODES, true);
238 tree_view->setColumnHidden(AnimeListPageModel::AL_AVG_SCORE, true);
239 tree_view->setColumnHidden(AnimeListPageModel::AL_STARTED, true);
240 tree_view->setColumnHidden(AnimeListPageModel::AL_COMPLETED, true);
241 tree_view->setColumnHidden(AnimeListPageModel::AL_UPDATED, true);
242 tree_view->setColumnHidden(AnimeListPageModel::AL_NOTES, true);
243 }
244
245 void AnimeListPage::UpdateAnime(int id) {
246 QThreadPool::globalInstance()->start([this, id] {
247 Services::UpdateAnimeEntry(id);
248 Refresh();
249 });
250 }
251
252 void AnimeListPage::RemoveAnime(int id) {
253 Anime::Anime& anime = Anime::db.items[id];
254 anime.RemoveFromUserList();
255 Refresh();
256 }
257
258 void AnimeListPage::DisplayColumnHeaderMenu() {
259 QMenu* menu = new QMenu(this);
260 menu->setAttribute(Qt::WA_DeleteOnClose);
261 menu->setTitle(tr("Column visibility"));
262 menu->setToolTipsVisible(true);
263
264 for (int i = 0; i < AnimeListPageModel::NB_COLUMNS; i++) {
265 if (i == AnimeListPageModel::AL_TITLE)
266 continue;
267 const auto column_name =
268 sort_models[tab_bar->currentIndex()]->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
269 QAction* action = menu->addAction(column_name, this, [this, i](const bool checked) {
270 if (!checked && (VisibleColumnsCount() <= 1))
271 return;
272
273 tree_view->setColumnHidden(i, !checked);
274
275 if (checked && (tree_view->columnWidth(i) <= 5))
276 tree_view->resizeColumnToContents(i);
277
278 // SaveSettings();
279 });
280 action->setCheckable(true);
281 action->setChecked(!tree_view->isColumnHidden(i));
282 }
283
284 menu->addSeparator();
285 QAction* resetAction = menu->addAction(tr("Reset to defaults"), this, [this]() {
286 for (int i = 0, count = tree_view->header()->count(); i < count; ++i) {
287 SetColumnDefaults();
288 }
289 // SaveSettings();
290 });
291 menu->popup(QCursor::pos());
292 (void)(resetAction);
293 }
294
295 void AnimeListPage::DisplayListMenu() {
296 QMenu* menu = new QMenu(this);
297 menu->setAttribute(Qt::WA_DeleteOnClose);
298 menu->setTitle(tr("Column visibility"));
299 menu->setToolTipsVisible(true);
300
301 AnimeListPageModel* source_model =
302 reinterpret_cast<AnimeListPageModel*>(sort_models[tab_bar->currentIndex()]->sourceModel());
303 const QItemSelection selection =
304 sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection());
305
306 std::set<Anime::Anime*> animes;
307 for (const auto& index : selection.indexes()) {
308 if (!index.isValid())
309 continue;
310 Anime::Anime* anime = source_model->GetAnimeFromIndex(index);
311 if (anime)
312 animes.insert(anime);
313 }
314
315 QAction* action = menu->addAction(tr("Information"), [this, animes] {
316 for (auto& anime : animes) {
317 InformationDialog* dialog = new InformationDialog(
318 *anime, [this, anime] { UpdateAnime(anime->GetId()); }, this);
319
320 dialog->show();
321 dialog->raise();
322 dialog->activateWindow();
323 }
324 });
325 menu->addSeparator();
326 action = menu->addAction(tr("Delete from list..."), [this, animes] {
327 for (auto& anime : animes) {
328 RemoveAnime(anime->GetId());
329 }
330 });
331 menu->popup(QCursor::pos());
332 }
333
334 void AnimeListPage::ItemDoubleClicked() {
335 /* throw out any other garbage */
336 const QItemSelection selection =
337 sort_models[tab_bar->currentIndex()]->mapSelectionToSource(tree_view->selectionModel()->selection());
338 if (!selection.indexes().first().isValid()) {
339 return;
340 }
341
342 AnimeListPageModel* source_model =
343 reinterpret_cast<AnimeListPageModel*>(sort_models[tab_bar->currentIndex()]->sourceModel());
344
345 const QModelIndex index = source_model->index(selection.indexes().first().row());
346 Anime::Anime* anime = source_model->GetAnimeFromIndex(index);
347
348 InformationDialog* dialog = new InformationDialog(
349 *anime, [this, anime] { UpdateAnime(anime->GetId()); }, this);
350
351 dialog->show();
352 dialog->raise();
353 dialog->activateWindow();
354 }
355
356 void AnimeListPage::paintEvent(QPaintEvent*) {
357 QStylePainter p(this);
358
359 QStyleOptionTabWidgetFrame opt;
360 InitStyle(&opt);
361 opt.rect = panelRect;
362 p.drawPrimitive(QStyle::PE_FrameTabWidget, opt);
363 }
364
365 void AnimeListPage::resizeEvent(QResizeEvent* e) {
366 QWidget::resizeEvent(e);
367 SetupLayout();
368 }
369
370 void AnimeListPage::showEvent(QShowEvent*) {
371 SetupLayout();
372 }
373
374 void AnimeListPage::InitBasicStyle(QStyleOptionTabWidgetFrame* option) const {
375 if (!option)
376 return;
377
378 option->initFrom(this);
379 option->lineWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth, nullptr, this);
380 option->shape = QTabBar::RoundedNorth;
381 option->tabBarRect = tab_bar->geometry();
382 }
383
384 void AnimeListPage::InitStyle(QStyleOptionTabWidgetFrame* option) const {
385 if (!option)
386 return;
387
388 InitBasicStyle(option);
389
390 // int exth = style()->pixelMetric(QStyle::PM_TabBarBaseHeight, nullptr, this);
391 QSize t(0, tree_view->frameWidth());
392 if (tab_bar->isVisibleTo(this)) {
393 t = tab_bar->sizeHint();
394 t.setWidth(width());
395 }
396
397 option->tabBarSize = t;
398
399 QRect selected_tab_rect = tab_bar->tabRect(tab_bar->currentIndex());
400 selected_tab_rect.moveTopLeft(selected_tab_rect.topLeft() + option->tabBarRect.topLeft());
401 option->selectedTabRect = selected_tab_rect;
402
403 option->lineWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth, nullptr, this);
404 }
405
406 void AnimeListPage::SetupLayout() {
407 QStyleOptionTabWidgetFrame option;
408 InitStyle(&option);
409
410 QRect tabRect = style()->subElementRect(QStyle::SE_TabWidgetTabBar, &option, this);
411 tabRect.setLeft(tabRect.left() + 1);
412 panelRect = style()->subElementRect(QStyle::SE_TabWidgetTabPane, &option, this);
413 QRect contentsRect = style()->subElementRect(QStyle::SE_TabWidgetTabContents, &option, this);
414
415 tab_bar->setGeometry(tabRect);
416 tree_view->parentWidget()->setGeometry(contentsRect);
417 }
418
419 AnimeListPage::AnimeListPage(QWidget* parent) : QWidget(parent) {
420 /* Tab bar */
421 tab_bar = new QTabBar(this);
422 tab_bar->setExpanding(false);
423 tab_bar->setDrawBase(false);
424
425 /* Tree view... */
426 QWidget* tree_widget = new QWidget(this);
427 tree_view = new QTreeView(tree_widget);
428 tree_view->setItemDelegate(new AnimeListPageDelegate(tree_view));
429 tree_view->setUniformRowHeights(true);
430 tree_view->setAllColumnsShowFocus(false);
431 tree_view->setAlternatingRowColors(true);
432 tree_view->setSortingEnabled(true);
433 tree_view->setSelectionMode(QAbstractItemView::ExtendedSelection);
434 tree_view->setItemsExpandable(false);
435 tree_view->setRootIsDecorated(false);
436 tree_view->setContextMenuPolicy(Qt::CustomContextMenu);
437 tree_view->setFrameShape(QFrame::NoFrame);
438
439 for (unsigned int i = 0; i < ARRAYSIZE(sort_models); i++) {
440 tab_bar->addTab(Strings::ToQString(Translate::ToString(Anime::ListStatuses[i])) + " (" +
441 QString::number(Anime::db.GetListsAnimeAmount(Anime::ListStatuses[i])) + ")");
442 sort_models[i] = new AnimeListPageSortFilter(tree_view);
443 sort_models[i]->setSourceModel(new AnimeListPageModel(this, Anime::ListStatuses[i]));
444 sort_models[i]->setSortRole(Qt::UserRole);
445 sort_models[i]->setSortCaseSensitivity(Qt::CaseInsensitive);
446 }
447 tree_view->setModel(sort_models[0]);
448
449 QHBoxLayout* layout = new QHBoxLayout(tree_widget);
450 layout->addWidget(tree_view);
451 layout->setContentsMargins(0, 0, 0, 0);
452
453 /* Double click stuff */
454 connect(tree_view, &QAbstractItemView::doubleClicked, this, &AnimeListPage::ItemDoubleClicked);
455 connect(tree_view, &QWidget::customContextMenuRequested, this, &AnimeListPage::DisplayListMenu);
456
457 /* Enter & return keys */
458 connect(new QShortcut(Qt::Key_Return, tree_view, nullptr, nullptr, Qt::WidgetShortcut), &QShortcut::activated, this,
459 &AnimeListPage::ItemDoubleClicked);
460
461 connect(new QShortcut(Qt::Key_Enter, tree_view, nullptr, nullptr, Qt::WidgetShortcut), &QShortcut::activated, this,
462 &AnimeListPage::ItemDoubleClicked);
463
464 tree_view->header()->setStretchLastSection(false);
465 tree_view->header()->setContextMenuPolicy(Qt::CustomContextMenu);
466 connect(tree_view->header(), &QWidget::customContextMenuRequested, this, &AnimeListPage::DisplayColumnHeaderMenu);
467
468 connect(tab_bar, &QTabBar::currentChanged, this, [this](int index) {
469 if (sort_models[index])
470 tree_view->setModel(sort_models[index]);
471 });
472
473 SetColumnDefaults();
474 setFocusPolicy(Qt::TabFocus);
475 setFocusProxy(tab_bar);
476 }
477
478 void AnimeListPage::RefreshList() {
479 for (unsigned int i = 0; i < ARRAYSIZE(sort_models); i++)
480 reinterpret_cast<AnimeListPageModel*>(sort_models[i]->sourceModel())->RefreshList();
481 }
482
483 void AnimeListPage::RefreshTabs() {
484 for (unsigned int i = 0; i < ARRAYSIZE(sort_models); i++)
485 tab_bar->setTabText(i, Strings::ToQString(Translate::ToString(Anime::ListStatuses[i])) + " (" +
486 QString::number(Anime::db.GetListsAnimeAmount(Anime::ListStatuses[i])) + ")");
487 }
488
489 void AnimeListPage::Refresh() {
490 RefreshList();
491 RefreshTabs();
492 }
493
494 /* This function, really, really should not be called.
495 Ever. Why would you ever need to clear the anime list?
496 Also, this sucks. */
497 void AnimeListPage::Reset() {
498 while (tab_bar->count())
499 tab_bar->removeTab(0);
500 for (unsigned int i = 0; i < ARRAYSIZE(sort_models); i++)
501 delete sort_models[i];
502 }
503
504 #include "gui/pages/moc_anime_list.cpp"