Qt: Add optional animation of icons in game list

Disabled by default.
This commit is contained in:
Stenzek
2025-09-27 17:45:21 +10:00
parent da471120c4
commit b40ff8b0bd
7 changed files with 373 additions and 48 deletions

View File

@@ -14,7 +14,10 @@
#include "core/settings.h"
#include "core/system.h"
#include "util/animated_image.h"
#include "common/assert.h"
#include "common/error.h"
#include "common/file_system.h"
#include "common/log.h"
#include "common/path.h"
@@ -76,8 +79,9 @@ static constexpr int COVER_ART_SIZE = 512;
static constexpr int COVER_ART_SPACING = 32;
static constexpr int MIN_COVER_CACHE_SIZE = 256;
static constexpr int MIN_COVER_CACHE_ROW_BUFFER = 4;
static constexpr int MEMORY_CARD_ICON_SIZE = 16;
static constexpr int MEMORY_CARD_ICON_PADDING = 12;
static constexpr int GAME_ICON_SIZE = 16;
static constexpr int GAME_ICON_PADDING = 12;
static constexpr int GAME_ICON_ANIMATION_LOOPS = 5;
static void resizeAndPadImage(QImage* image, int expected_width, int expected_height, bool fill_with_top_left,
bool expand_to_fill)
@@ -138,6 +142,21 @@ static void resizeAndPadImage(QImage* image, int expected_width, int expected_he
*image = std::move(padded_image);
}
static void resizeGameIcon(QPixmap& pm, int icon_size, qreal device_pixel_ratio)
{
const int pm_width = pm.width();
const int pm_height = pm.height();
const qreal scale = (static_cast<qreal>(icon_size) / static_cast<qreal>(pm_width)) * device_pixel_ratio;
const int scaled_pm_width = static_cast<int>(static_cast<qreal>(pm_width) * scale);
const int scaled_pm_height = static_cast<int>(static_cast<qreal>(pm_height) * scale);
if (pm_width != scaled_pm_width || pm_height != scaled_pm_height)
QtUtils::ResizeSharpBilinear(pm, std::max(scaled_pm_width, scaled_pm_height), pm_width);
pm.setDevicePixelRatio(device_pixel_ratio);
}
static QString sizeToString(s64 size)
{
static constexpr s64 one_mb = 1024 * 1024;
@@ -201,8 +220,8 @@ void GameListModel::setShowCoverTitles(bool enabled)
void GameListModel::updateRowHeight(const QWidget* const widget)
{
m_row_height = m_icon_size + MEMORY_CARD_ICON_PADDING +
widget->style()->pixelMetric(QStyle::PM_FocusFrameVMargin, nullptr, widget);
m_row_height =
m_icon_size + GAME_ICON_PADDING + widget->style()->pixelMetric(QStyle::PM_FocusFrameVMargin, nullptr, widget);
}
void GameListModel::setShowGameIcons(bool enabled)
@@ -242,7 +261,7 @@ void GameListModel::setIconSize(int size)
int GameListModel::getIconColumnWidth() const
{
return m_icon_size + MEMORY_CARD_ICON_PADDING * 2;
return m_icon_size + GAME_ICON_PADDING * 2;
}
void GameListModel::setCoverScale(float scale)
@@ -468,7 +487,7 @@ void GameListModel::invalidateCoverForPath(const std::string& path)
emit dataChanged(mi, mi, {Qt::DecorationRole});
}
const QPixmap& GameListModel::getIconPixmapForEntry(const GameList::Entry* ge) const
const QPixmap* GameListModel::lookupIconPixmapForEntry(const GameList::Entry* ge) const
{
// We only do this for discs/disc sets for now.
if (m_show_game_icons && (!ge->serial.empty() && (ge->IsDisc() || ge->IsDiscSet())))
@@ -477,7 +496,7 @@ const QPixmap& GameListModel::getIconPixmapForEntry(const GameList::Entry* ge) c
if (item)
{
if (!item->isNull())
return *item;
return item;
}
else
{
@@ -486,19 +505,8 @@ const QPixmap& GameListModel::getIconPixmapForEntry(const GameList::Entry* ge) c
QPixmap pm;
if (!path.empty() && pm.load(QString::fromStdString(path)))
{
const int pm_width = pm.width();
const int pm_height = pm.height();
const qreal scale = (static_cast<qreal>(m_icon_size) / static_cast<qreal>(pm_width)) * m_device_pixel_ratio;
const int scaled_pm_width = static_cast<int>(static_cast<qreal>(pm_width) * scale);
const int scaled_pm_height = static_cast<int>(static_cast<qreal>(pm_height) * scale);
if (pm_width != scaled_pm_width || pm_height != scaled_pm_height)
QtUtils::ResizeSharpBilinear(pm, std::max(scaled_pm_width, scaled_pm_height), pm_width);
pm.setDevicePixelRatio(m_device_pixel_ratio);
return *m_icon_pixmap_cache.Insert(ge->serial, std::move(pm));
resizeGameIcon(pm, m_icon_size, m_device_pixel_ratio);
return m_icon_pixmap_cache.Insert(ge->serial, std::move(pm));
}
// Stop it trying again in the future.
@@ -506,6 +514,14 @@ const QPixmap& GameListModel::getIconPixmapForEntry(const GameList::Entry* ge) c
}
}
return nullptr;
}
const QPixmap& GameListModel::getIconPixmapForEntry(const GameList::Entry* ge) const
{
if (const QPixmap* pm = lookupIconPixmapForEntry(ge))
return *pm;
// If we don't have a pixmap, we return the type pixmap.
return m_type_pixmaps[static_cast<u32>(ge->type)];
}
@@ -539,7 +555,7 @@ QIcon GameListModel::getIconForGame(const QString& path)
// Only use the cache if we're not using larger icons. Otherwise they'll get double scaled.
// Provides a small performance boost when using default size icons.
if (m_icon_size == MEMORY_CARD_ICON_SIZE)
if (m_icon_size == GAME_ICON_SIZE)
{
if (const QPixmap* pm = m_icon_pixmap_cache.Lookup(entry->serial))
{
@@ -1323,15 +1339,145 @@ private:
} // namespace
GameListWidget::GameListWidget(QWidget* parent /* = nullptr */) : QWidget(parent)
class GameListAnimatedIconDelegate final : public QStyledItemDelegate
{
}
public:
GameListAnimatedIconDelegate(QObject* parent, GameListModel* model, GameListCenterIconStyleDelegate* center_delegate,
GameListAchievementsStyleDelegate* achievements_delegate)
: QStyledItemDelegate(parent), m_model(model), m_center_delegate(center_delegate),
m_achievements_delegate(achievements_delegate)
{
connect(&m_animation_timer, &QTimer::timeout, this, &GameListAnimatedIconDelegate::nextAnimationFrame);
}
GameListWidget::~GameListWidget() = default;
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override
{
const int column = index.column();
if (column == GameListModel::Column_Region)
{
m_center_delegate->paint(painter, option, index);
return;
}
else if (column == GameListModel::Column_Achievements)
{
m_achievements_delegate->paint(painter, option, index);
return;
}
else if (column != GameListModel::Column_Icon || m_frame_pixmaps.empty())
{
QStyledItemDelegate::paint(painter, option, index);
return;
}
void GameListWidget::initialize(QAction* actionGameList, QAction* actionGameGrid, QAction* actionMergeDiscSets,
QAction* actionListShowIcons, QAction* actionGridShowTitles,
QAction* actionShowLocalizedTitles)
const QRect& r = option.rect;
const QPixmap pix = m_frame_pixmaps[m_current_frame];
const int pix_width = static_cast<int>(pix.width() / pix.devicePixelRatio());
const int pix_height = static_cast<int>(pix.height() / pix.devicePixelRatio());
// draw pixmap at center of item
const QPoint p = QPoint((r.width() - pix_width) / 2, (r.height() - pix_height) / 2);
painter->drawPixmap(r.topLeft() + p, pix);
}
bool setEntry(const GameList::Entry* entry, int source_row)
{
DebugAssert(source_row >= 0);
const std::string icon_path = GameList::GetGameIconPath(entry->serial, entry->path);
if (icon_path.empty())
{
clearEntry();
return false;
}
AnimatedImage image;
Error error;
if (!image.LoadFromFile(icon_path.c_str(), &error))
{
ERROR_LOG("Failed to load animated icon '{}': {}", Path::GetFileName(icon_path), error.GetDescription());
clearEntry();
return false;
}
// don't use animated delegate if there's only one frame
if (image.GetFrames() <= 1)
{
clearEntry();
return false;
}
m_frame_pixmaps.clear();
m_frame_pixmaps.reserve(image.GetFrames());
for (u32 i = 0; i < image.GetFrames(); i++)
{
QPixmap pm = QPixmap::fromImage(QImage(reinterpret_cast<uchar*>(image.GetPixels(i)), image.GetWidth(),
image.GetHeight(), QImage::Format::Format_RGBA8888));
resizeGameIcon(pm, m_model->getIconSize(), m_model->getDevicePixelRatio());
m_frame_pixmaps.push_back(std::move(pm));
}
m_current_frame = 0;
m_loops_remaining = GAME_ICON_ANIMATION_LOOPS;
m_source_row = source_row;
const AnimatedImage::FrameDelay& delay = image.GetFrameDelay(0);
m_animation_timer.start(std::max((1000 * delay.numerator) / delay.denominator, 100));
return true;
}
void clearEntry()
{
m_source_row = -1;
m_loops_remaining = 0;
m_current_frame = 0;
m_frame_pixmaps.clear();
m_animation_timer.stop();
}
void nextAnimationFrame()
{
m_current_frame = (m_current_frame + 1) % static_cast<u32>(m_frame_pixmaps.size());
if (m_current_frame == 0)
{
m_loops_remaining--;
if (m_loops_remaining == 0)
m_animation_timer.stop();
}
emit m_model->dataChanged(m_model->index(m_source_row, GameListModel::Column_Icon),
m_model->index(m_source_row, GameListModel::Column_Icon), {Qt::DecorationRole});
}
void pauseAnimation()
{
if (!m_frame_pixmaps.empty() && m_loops_remaining > 0)
m_animation_timer.stop();
}
void resumeAnimation()
{
if (!m_frame_pixmaps.empty() && m_loops_remaining > 0)
m_animation_timer.start();
}
private:
GameListModel* m_model;
GameListCenterIconStyleDelegate* m_center_delegate;
GameListAchievementsStyleDelegate* m_achievements_delegate;
std::vector<QPixmap> m_frame_pixmaps;
u32 m_current_frame = 0;
int m_loops_remaining = 0;
int m_source_row = -1;
QTimer m_animation_timer;
};
GameListWidget::GameListWidget(QWidget* parent, QAction* action_view_list, QAction* action_view_grid,
QAction* action_merge_disc_sets, QAction* action_show_list_icons,
QAction* action_animate_list_icons, QAction* action_show_grid_titles,
QAction* action_show_localized_titles)
: QWidget(parent)
{
m_model = new GameListModel(this);
connect(m_model, &GameListModel::coverScaleChanged, this, &GameListWidget::onScaleChanged);
@@ -1354,6 +1500,7 @@ void GameListWidget::initialize(QAction* actionGameList, QAction* actionGameGrid
}
m_list_view = new GameListListView(m_model, m_sort_model, m_ui.stack);
m_list_view->setAnimateGameIcons(Host::GetBaseBoolSettingValue("UI", "GameListAnimateGameIcons", false));
m_ui.stack->insertWidget(0, m_list_view);
m_grid_view = new GameListGridView(m_model, m_sort_model, m_ui.stack);
@@ -1364,12 +1511,12 @@ void GameListWidget::initialize(QAction* actionGameList, QAction* actionGameGrid
m_empty_ui.supportedFormats->setText(qApp->translate("GameListWidget", SUPPORTED_FORMATS_STRING));
m_ui.stack->insertWidget(2, m_empty_widget);
m_ui.viewGameList->setDefaultAction(actionGameList);
m_ui.viewGameGrid->setDefaultAction(actionGameGrid);
m_ui.mergeDiscSets->setDefaultAction(actionMergeDiscSets);
m_ui.showGameIcons->setDefaultAction(actionListShowIcons);
m_ui.showGridTitles->setDefaultAction(actionGridShowTitles);
m_ui.showLocalizedTitles->setDefaultAction(actionShowLocalizedTitles);
m_ui.viewGameList->setDefaultAction(action_view_list);
m_ui.viewGameGrid->setDefaultAction(action_view_grid);
m_ui.mergeDiscSets->setDefaultAction(action_merge_disc_sets);
m_ui.showGameIcons->setDefaultAction(action_show_list_icons);
m_ui.showGridTitles->setDefaultAction(action_show_grid_titles);
m_ui.showLocalizedTitles->setDefaultAction(action_show_localized_titles);
connect(m_ui.scale, &QSlider::sliderPressed, this, &GameListWidget::showScaleToolTip);
connect(m_ui.scale, &QSlider::sliderReleased, this, &QToolTip::hideText);
@@ -1402,19 +1549,22 @@ void GameListWidget::initialize(QAction* actionGameList, QAction* actionGameGrid
const bool grid_view = Host::GetBaseBoolSettingValue("UI", "GameListGridView", false);
if (grid_view)
actionGameGrid->setChecked(true);
action_view_grid->setChecked(true);
else
actionGameList->setChecked(true);
actionMergeDiscSets->setChecked(m_sort_model->isMergingDiscSets());
actionShowLocalizedTitles->setChecked(m_model->getShowLocalizedTitles());
actionListShowIcons->setChecked(m_model->getShowGameIcons());
actionGridShowTitles->setChecked(m_model->getShowCoverTitles());
action_view_list->setChecked(true);
action_merge_disc_sets->setChecked(m_sort_model->isMergingDiscSets());
action_show_localized_titles->setChecked(m_model->getShowLocalizedTitles());
action_show_list_icons->setChecked(m_model->getShowGameIcons());
action_animate_list_icons->setChecked(m_list_view->isAnimatingGameIcons());
action_show_grid_titles->setChecked(m_model->getShowCoverTitles());
onIconSizeChanged(m_model->getIconSize());
setViewMode(grid_view ? VIEW_MODE_GRID : VIEW_MODE_LIST);
updateBackground(true);
}
GameListWidget::~GameListWidget() = default;
bool GameListWidget::isShowingGameList() const
{
return (m_ui.stack->currentIndex() == VIEW_MODE_LIST);
@@ -1591,7 +1741,13 @@ void GameListWidget::onSelectionModelCurrentChanged(const QModelIndex& current,
{
const QModelIndex source_index = m_sort_model->mapToSource(current);
if (!source_index.isValid() || source_index.row() >= static_cast<int>(GameList::GetEntryCount()))
{
m_list_view->clearAnimatedGameIconDelegate();
return;
}
// selection model hasn't updated yet, so this has to be queued... ugh.
QMetaObject::invokeMethod(m_list_view, &GameListListView::updateAnimatedGameIconDelegate, Qt::QueuedConnection);
emit selectionChanged();
}
@@ -1712,6 +1868,25 @@ void GameListWidget::setShowGameIcons(bool enabled)
Host::SetBaseBoolSettingValue("UI", "GameListShowGameIcons", enabled);
Host::CommitBaseSettingChanges();
m_model->setShowGameIcons(enabled);
if (isShowingGameList() && m_list_view->isAnimatingGameIcons())
{
if (enabled)
m_list_view->updateAnimatedGameIconDelegate();
else
m_list_view->clearAnimatedGameIconDelegate();
}
}
void GameListWidget::setAnimateGameIcons(bool enabled)
{
if (m_list_view->isAnimatingGameIcons() == enabled)
return;
Host::SetBaseBoolSettingValue("UI", "GameListAnimateGameIcons", enabled);
Host::CommitBaseSettingChanges();
m_list_view->setAnimateGameIcons(enabled);
if (isShowingGameList())
m_list_view->updateAnimatedGameIconDelegate();
}
void GameListWidget::setShowCoverTitles(bool enabled)
@@ -1727,6 +1902,7 @@ void GameListWidget::setShowCoverTitles(bool enabled)
void GameListWidget::setViewMode(int stack_index)
{
const int prev_stack_index = m_ui.stack->currentIndex();
m_ui.stack->setCurrentIndex(stack_index);
setFocusProxy(m_ui.stack->currentWidget());
@@ -1760,6 +1936,12 @@ void GameListWidget::setViewMode(int stack_index)
m_ui.scale->setMaximum(MAX_ICON_SIZE / ICON_SIZE_STEP);
m_ui.scale->setValue(m_model->getIconSize() / ICON_SIZE_STEP);
}
// pause animation when list is not visible
if (stack_index == VIEW_MODE_LIST)
m_list_view->updateAnimatedGameIconDelegate();
else if (prev_stack_index == VIEW_MODE_LIST)
m_list_view->clearAnimatedGameIconDelegate();
}
void GameListWidget::showScaleToolTip()
@@ -1774,9 +1956,14 @@ void GameListWidget::showScaleToolTip()
void GameListWidget::onScaleSliderChanged(int value)
{
if (isShowingGameGrid())
{
m_model->setCoverScale(static_cast<float>(value) / 100.0f);
}
else if (isShowingGameList())
{
m_model->setIconSize(value * ICON_SIZE_STEP);
m_list_view->updateAnimatedGameIconDelegate();
}
if (m_ui.scale->isSliderDown())
showScaleToolTip();
@@ -1871,7 +2058,7 @@ GameListListView::GameListListView(GameListModel* model, GameListSortModel* sort
setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
setVerticalScrollMode(QAbstractItemView::ScrollMode::ScrollPerPixel);
GameListCenterIconStyleDelegate* center_icon_delegate = new GameListCenterIconStyleDelegate(this);
GameListCenterIconStyleDelegate* const center_icon_delegate = new GameListCenterIconStyleDelegate(this);
setItemDelegateForColumn(GameListModel::Column_Icon, center_icon_delegate);
setItemDelegateForColumn(GameListModel::Column_Region, center_icon_delegate);
setItemDelegateForColumn(GameListModel::Column_Achievements,
@@ -2064,6 +2251,77 @@ void GameListListView::adjustIconSize(int delta)
{
const int new_size = std::clamp(m_model->getIconSize() + delta, MIN_ICON_SIZE, MAX_ICON_SIZE);
m_model->setIconSize(new_size);
updateAnimatedGameIconDelegate();
}
bool GameListListView::isAnimatingGameIcons() const
{
return (m_animated_game_icon_delegate != nullptr);
}
void GameListListView::setAnimateGameIcons(bool enabled)
{
if (!enabled)
{
clearAnimatedGameIconDelegate();
delete m_animated_game_icon_delegate;
m_animated_game_icon_delegate = nullptr;
return;
}
if (m_animated_game_icon_delegate)
return;
m_animated_game_icon_delegate = new GameListAnimatedIconDelegate(
this, m_model, static_cast<GameListCenterIconStyleDelegate*>(itemDelegateForColumn(GameListModel::Column_Icon)),
static_cast<GameListAchievementsStyleDelegate*>(itemDelegateForColumn(GameListModel::Column_Achievements)));
}
void GameListListView::updateAnimatedGameIconDelegate()
{
if (!m_animated_game_icon_delegate || !m_model->getShowGameIcons())
return;
const QModelIndexList selected = selectionModel()->selectedIndexes();
if (selected.isEmpty())
{
clearAnimatedGameIconDelegate();
return;
}
// clear previous
const int visible_row = selected.first().row();
if (m_animated_icon_row >= 0)
{
setItemDelegateForRow(m_animated_icon_row, nullptr);
m_animated_icon_row = -1;
}
const auto lock = GameList::GetLock();
const QModelIndex source_index = m_sort_model->mapToSource(selected.first());
const GameList::Entry* entry = m_model->hasTakenGameList() ?
m_model->getTakenGameListEntry(static_cast<u32>(source_index.row())) :
GameList::GetEntryByIndex(static_cast<u32>(source_index.row()));
// don't try to load an animated icon if there is no icon
if (!entry || !m_model->lookupIconPixmapForEntry(entry))
return;
if (static_cast<GameListAnimatedIconDelegate*>(m_animated_game_icon_delegate)->setEntry(entry, source_index.row()))
{
m_animated_icon_row = visible_row;
setItemDelegateForRow(visible_row, m_animated_game_icon_delegate);
}
}
void GameListListView::clearAnimatedGameIconDelegate()
{
if (m_animated_icon_row < 0)
return;
static_cast<GameListAnimatedIconDelegate*>(m_animated_game_icon_delegate)->clearEntry();
setItemDelegateForRow(m_animated_icon_row, nullptr);
m_animated_icon_row = -1;
}
GameListGridView::GameListGridView(GameListModel* model, GameListSortModel* sort_model, QWidget* parent)

View File

@@ -25,6 +25,8 @@
Q_DECLARE_METATYPE(const GameList::Entry*);
class QStyledItemDelegate;
class GameListSortModel;
class GameListRefreshThread;
class GameListWidget;
@@ -111,8 +113,11 @@ public:
void refreshCovers();
void updateCacheSize(int num_rows, int num_columns);
qreal getDevicePixelRatio() const { return m_device_pixel_ratio; }
void setDevicePixelRatio(qreal dpr);
const QPixmap* lookupIconPixmapForEntry(const GameList::Entry* ge) const;
Q_SIGNALS:
void coverScaleChanged(float scale);
void iconSizeChanged(int size);
@@ -180,6 +185,11 @@ public:
void adjustIconSize(int delta);
bool isAnimatingGameIcons() const;
void setAnimateGameIcons(bool enabled);
void updateAnimatedGameIconDelegate();
void clearAnimatedGameIconDelegate();
protected:
void wheelEvent(QWheelEvent* e) override;
@@ -194,6 +204,9 @@ private:
GameListModel* m_model = nullptr;
GameListSortModel* m_sort_model = nullptr;
QStyledItemDelegate* m_animated_game_icon_delegate = nullptr;
int m_animated_icon_row = -1;
};
class GameListGridView final : public QListView
@@ -226,16 +239,16 @@ class GameListWidget final : public QWidget
Q_OBJECT
public:
explicit GameListWidget(QWidget* parent = nullptr);
explicit GameListWidget(QWidget* parent, QAction* action_view_list, QAction* action_view_grid,
QAction* action_merge_disc_sets, QAction* action_show_list_icons,
QAction* action_animate_list_icons, QAction* action_show_grid_titles,
QAction* action_show_localized_titles);
~GameListWidget();
ALWAYS_INLINE GameListModel* getModel() const { return m_model; }
ALWAYS_INLINE GameListListView* getListView() const { return m_list_view; }
ALWAYS_INLINE GameListGridView* getGridView() const { return m_grid_view; }
void initialize(QAction* actionGameList, QAction* actionGameGrid, QAction* actionMergeDiscSets,
QAction* actionListShowIcons, QAction* actionGridShowTitles, QAction* actionLocalizedTitles);
void refresh(bool invalidate_cache);
void cancelRefresh();
void setBackgroundPath(const std::string_view path);
@@ -254,6 +267,7 @@ public:
void setMergeDiscSets(bool enabled);
void setShowLocalizedTitles(bool enabled);
void setShowGameIcons(bool enabled);
void setAnimateGameIcons(bool enabled);
void setShowCoverTitles(bool enabled);
void refreshGridCovers();
void focusSearchWidget();

View File

@@ -546,6 +546,7 @@ void MainWindow::updateGameListRelatedActions()
m_ui.actionMergeDiscSets->setDisabled(disable);
m_ui.actionShowLocalizedTitles->setDisabled(disable);
m_ui.actionShowGameIcons->setDisabled(disable || !game_list);
m_ui.actionAnimateGameIcons->setDisabled(disable || !game_list || !m_ui.actionShowGameIcons->isChecked());
m_ui.actionGridViewShowTitles->setDisabled(disable || !game_grid);
m_ui.actionViewZoomIn->setDisabled(disable);
m_ui.actionViewZoomOut->setDisabled(disable);
@@ -1782,10 +1783,10 @@ void MainWindow::setupAdditionalUi()
group->addAction(m_ui.actionViewGameGrid);
group->addAction(m_ui.actionViewSystemDisplay);
m_game_list_widget = new GameListWidget(m_ui.mainContainer);
m_game_list_widget->initialize(m_ui.actionViewGameList, m_ui.actionViewGameGrid, m_ui.actionMergeDiscSets,
m_ui.actionShowGameIcons, m_ui.actionGridViewShowTitles,
m_ui.actionShowLocalizedTitles);
m_game_list_widget =
new GameListWidget(m_ui.mainContainer, m_ui.actionViewGameList, m_ui.actionViewGameGrid, m_ui.actionMergeDiscSets,
m_ui.actionShowGameIcons, m_ui.actionAnimateGameIcons, m_ui.actionGridViewShowTitles,
m_ui.actionShowLocalizedTitles);
m_ui.mainContainer->addWidget(m_game_list_widget);
m_status_progress_widget = new QProgressBar(m_ui.statusBar);
@@ -2463,6 +2464,7 @@ void MainWindow::connectSignals()
connect(m_ui.actionShowLocalizedTitles, &QAction::triggered, m_game_list_widget,
&GameListWidget::setShowLocalizedTitles);
connect(m_ui.actionShowGameIcons, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowGameIcons);
connect(m_ui.actionAnimateGameIcons, &QAction::triggered, m_game_list_widget, &GameListWidget::setAnimateGameIcons);
connect(m_ui.actionGridViewShowTitles, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowCoverTitles);
connect(m_ui.actionViewZoomIn, &QAction::triggered, this, &MainWindow::onViewZoomInActionTriggered);
connect(m_ui.actionViewZoomOut, &QAction::triggered, this, &MainWindow::onViewZoomOutActionTriggered);

View File

@@ -221,6 +221,7 @@
<addaction name="actionMergeDiscSets"/>
<addaction name="actionShowLocalizedTitles"/>
<addaction name="actionShowGameIcons"/>
<addaction name="actionAnimateGameIcons"/>
<addaction name="actionGridViewShowTitles"/>
<addaction name="actionViewZoomIn"/>
<addaction name="actionViewZoomOut"/>
@@ -1301,6 +1302,20 @@
<string>Opens the memory editor window.</string>
</property>
</action>
<action name="actionAnimateGameIcons">
<property name="checkable">
<bool>true</bool>
</property>
<property name="icon">
<iconset theme="add-animation"/>
</property>
<property name="text">
<string>Animate Game Icons (List View)</string>
</property>
<property name="toolTip">
<string>Animates icons in the list view when selected.</string>
</property>
</action>
</widget>
<resources>
<include location="resources/duckstation-qt.qrc"/>

View File

@@ -25,6 +25,7 @@
<file>icons/audio-card@2x.png</file>
<file>icons/audio-card.png</file>
<file>icons/black/index.theme</file>
<file>icons/black/svg/add-animation.svg</file>
<file>icons/black/svg/add-line.svg</file>
<file>icons/black/svg/alert-line.svg</file>
<file>icons/black/svg/arrow-down-line.svg</file>
@@ -246,6 +247,7 @@
<file>icons/view-refresh@2x.png</file>
<file>icons/view-refresh.png</file>
<file>icons/white/index.theme</file>
<file>icons/white/svg/add-animation.svg</file>
<file>icons/white/svg/add-line.svg</file>
<file>icons/white/svg/alert-line.svg</file>
<file>icons/white/svg/arrow-down-line.svg</file>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- License: MIT. Made by vmware: https://github.com/vmware/clarity-assets -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
<title>animation_solid</title>
<g id="bb9f4e02-7318-4dbb-9960-e0435cf0cad7" data-name="Layer 3">
<path d="M3.5,23.77a6.41,6.41,0,0,0,9.33,8.67A11.65,11.65,0,0,1,3.5,23.77Z"/>
<path d="M7.68,14.53a9.6,9.6,0,0,0,13.4,13.7A14.11,14.11,0,0,1,7.68,14.53Z"/>
<path d="M21.78,2A12.12,12.12,0,1,1,9.66,14.15,12.12,12.12,0,0,1,21.78,2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 571 B

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- License: MIT. Made by vmware: https://github.com/vmware/clarity-assets -->
<svg fill="#ffffff" width="800px" height="800px" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
<title>animation_solid</title>
<g id="bb9f4e02-7318-4dbb-9960-e0435cf0cad7" data-name="Layer 3">
<path d="M3.5,23.77a6.41,6.41,0,0,0,9.33,8.67A11.65,11.65,0,0,1,3.5,23.77Z"/>
<path d="M7.68,14.53a9.6,9.6,0,0,0,13.4,13.7A14.11,14.11,0,0,1,7.68,14.53Z"/>
<path d="M21.78,2A12.12,12.12,0,1,1,9.66,14.15,12.12,12.12,0,0,1,21.78,2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 571 B