Qt: lazy load game list icons and dir size

This commit is contained in:
Megamouse 2023-04-21 23:31:38 +02:00
parent e9df63119b
commit b401ad85d1
10 changed files with 418 additions and 89 deletions

View file

@ -671,6 +671,7 @@
<ClCompile Include="rpcs3qt\localized.cpp" />
<ClCompile Include="rpcs3qt\log_viewer.cpp" />
<ClCompile Include="rpcs3qt\microphone_creator.cpp" />
<ClCompile Include="rpcs3qt\movie_item.cpp" />
<ClCompile Include="rpcs3qt\osk_dialog_frame.cpp" />
<ClCompile Include="rpcs3qt\pad_led_settings_dialog.cpp" />
<ClCompile Include="rpcs3qt\pad_motion_settings_dialog.cpp" />
@ -696,6 +697,7 @@
<ClCompile Include="rpcs3qt\shortcut_utils.cpp" />
<ClCompile Include="rpcs3qt\skylander_dialog.cpp" />
<ClCompile Include="rpcs3qt\system_cmd_dialog.cpp" />
<ClCompile Include="rpcs3qt\table_item_delegate.cpp" />
<ClCompile Include="rpcs3qt\tooltips.cpp" />
<ClCompile Include="rpcs3qt\update_manager.cpp" />
<ClCompile Include="rpcs3qt\qt_camera_video_surface.cpp" />

View file

@ -936,6 +936,12 @@
<ClCompile Include="QTGeneratedFiles\Release\moc_system_cmd_dialog.cpp">
<Filter>Generated Files\Release</Filter>
</ClCompile>
<ClCompile Include="rpcs3qt\table_item_delegate.cpp">
<Filter>Gui\custom items</Filter>
</ClCompile>
<ClCompile Include="rpcs3qt\movie_item.cpp">
<Filter>Gui\custom items</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Input\ds4_pad_handler.h">

View file

@ -43,6 +43,7 @@ set(SRC_FILES
memory_string_searcher.cpp
memory_viewer_panel.cpp
microphone_creator.cpp
movie_item.cpp
msg_dialog_frame.cpp
osk_dialog_frame.cpp
pad_led_settings_dialog.cpp
@ -81,6 +82,7 @@ set(SRC_FILES
skylander_dialog.cpp
syntax_highlighter.cpp
system_cmd_dialog.cpp
table_item_delegate.cpp
tooltips.cpp
trophy_manager_dialog.cpp
trophy_notification_frame.cpp

View file

@ -81,7 +81,7 @@ game_list_frame::game_list_frame(std::shared_ptr<gui_settings> gui_settings, std
m_game_list = new game_list();
m_game_list->setShowGrid(false);
m_game_list->setItemDelegate(new table_item_delegate(this, true));
m_game_list->setItemDelegate(new table_item_delegate(m_game_list, true));
m_game_list->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_game_list->setSelectionBehavior(QAbstractItemView::SelectRows);
m_game_list->setSelectionMode(QAbstractItemView::SingleSelection);
@ -140,8 +140,8 @@ game_list_frame::game_list_frame(std::shared_ptr<gui_settings> gui_settings, std
connect(&m_refresh_watcher, &QFutureWatcher<void>::finished, this, &game_list_frame::OnRefreshFinished);
connect(&m_refresh_watcher, &QFutureWatcher<void>::canceled, this, [this]()
{
gui::utils::stop_future_watcher(m_size_watcher, true, m_size_watcher_cancel);
gui::utils::stop_future_watcher(m_repaint_watcher, true);
WaitAndAbortSizeCalcThreads();
WaitAndAbortRepaintThreads();
m_path_entries.clear();
m_path_list.clear();
@ -158,6 +158,21 @@ game_list_frame::game_list_frame(std::shared_ptr<gui_settings> gui_settings, std
item->call_icon_func();
}
});
connect(this, &game_list_frame::IconReady, this, [this](movie_item* item)
{
if (!m_is_list_layout || !item) return;
item->call_icon_func();
});
connect(this, &game_list_frame::SizeOnDiskReady, this, [this](const game_info& game)
{
if (!m_is_list_layout || !game || !game->item) return;
if (QTableWidgetItem* size_item = m_game_list->item(game->item->row(), gui::column_dir_size))
{
const u64& game_size = game->info.size_on_disk;
size_item->setText(game_size != umax ? gui::utils::format_byte_size(game_size) : tr("Unknown"));
size_item->setData(Qt::UserRole, QVariant::fromValue<qulonglong>(game_size));
}
});
connect(&m_size_watcher, &QFutureWatcher<void>::canceled, this, [this]()
{
if (m_size_watcher_cancel)
@ -267,8 +282,8 @@ void game_list_frame::LoadSettings()
game_list_frame::~game_list_frame()
{
gui::utils::stop_future_watcher(m_size_watcher, true);
gui::utils::stop_future_watcher(m_repaint_watcher, true);
WaitAndAbortSizeCalcThreads();
WaitAndAbortRepaintThreads();
gui::utils::stop_future_watcher(m_refresh_watcher, true);
SaveSettings();
@ -440,9 +455,9 @@ void game_list_frame::Refresh(const bool from_drive, const bool scroll_after)
{
if (from_drive)
{
gui::utils::stop_future_watcher(m_size_watcher, true, m_size_watcher_cancel);
WaitAndAbortSizeCalcThreads();
}
gui::utils::stop_future_watcher(m_repaint_watcher, true);
WaitAndAbortRepaintThreads();
gui::utils::stop_future_watcher(m_refresh_watcher, from_drive);
if (from_drive)
@ -781,8 +796,8 @@ void game_list_frame::Refresh(const bool from_drive, const bool scroll_after)
void game_list_frame::OnRefreshFinished()
{
gui::utils::stop_future_watcher(m_size_watcher, true, m_size_watcher_cancel);
gui::utils::stop_future_watcher(m_repaint_watcher, true);
WaitAndAbortSizeCalcThreads();
WaitAndAbortRepaintThreads();
for (auto&& g : m_games.pop_all())
{
@ -856,6 +871,38 @@ void game_list_frame::OnRefreshFinished()
m_size_watcher_cancel = std::make_shared<atomic_t<bool>>(false);
if (m_is_list_layout)
{
for (auto& game : m_game_data)
{
if (movie_item* item = game->item)
{
item->set_size_calc_func([this, game, cancel = item->size_on_disk_loading_aborted(), dev_flash = g_cfg_vfs.get_dev_flash()]()
{
if (game && game->info.size_on_disk == umax && (!cancel || !cancel->load()))
{
if (game->info.path.starts_with(dev_flash))
{
// Do not report size of apps inside /dev_flash (it does not make sense to do so)
game->info.size_on_disk = 0;
}
else
{
game->info.size_on_disk = fs::get_dir_size(game->info.path, 1, cancel.get());
}
if (!cancel || !cancel->load())
{
Q_EMIT SizeOnDiskReady(game);
}
}
});
}
}
return;
}
m_size_watcher.setFuture(QtConcurrent::map(m_game_data, [this, cancel = m_size_watcher_cancel, dev_flash = g_cfg_vfs.get_dev_flash()](const game_info& game) -> void
{
if (game)
@ -2393,7 +2440,7 @@ void game_list_frame::ResizeIcons(const int& slider_pos)
void game_list_frame::RepaintIcons(const bool& from_settings)
{
gui::utils::stop_future_watcher(m_repaint_watcher, true);
WaitAndAbortRepaintThreads();
if (from_settings)
{
@ -2415,8 +2462,52 @@ void game_list_frame::RepaintIcons(const bool& from_settings)
for (auto& game : m_game_data)
{
game->pxmap = placeholder;
if (movie_item* item = game->item)
{
item->set_icon_load_func([this, game, cancel = item->icon_loading_aborted()]()
{
if (cancel && cancel->load())
{
return;
}
static std::unordered_set<std::string> warn_once_list;
static shared_mutex s_mtx;
if (game->icon.isNull() && (game->info.icon_path.empty() || !game->icon.load(qstr(game->info.icon_path))))
{
if (game_list_log.warning)
{
bool logged = false;
{
std::lock_guard lock(s_mtx);
logged = !warn_once_list.emplace(game->info.icon_path).second;
}
if (!logged)
{
game_list_log.warning("Could not load image from path %s", sstr(QDir(qstr(game->info.icon_path)).absolutePath()));
}
}
}
if (!game->item || (cancel && cancel->load()))
{
return;
}
const QColor color = getGridCompatibilityColor(game->compat.color);
{
std::lock_guard lock(game->item->pixmap_mutex);
game->pxmap = PaintedPixmap(game->icon, game->hasCustomConfig, game->hasCustomPadConfig, color);
}
if (!cancel || !cancel->load())
{
Q_EMIT IconReady(game->item);
}
});
item->call_icon_func();
}
}
@ -2430,6 +2521,8 @@ void game_list_frame::RepaintIcons(const bool& from_settings)
// Shorten the last section to remove horizontal scrollbar if possible
m_game_list->resizeColumnToContents(gui::column_count - 1);
return;
}
const std::function func = [this](const game_info& game) -> movie_item*
@ -2606,12 +2699,14 @@ void game_list_frame::PopulateGameList()
{
ensure(icon_item && game);
if (QMovie* movie = icon_item->movie(); movie && icon_item->get_active())
if (std::shared_ptr<QMovie> movie = icon_item->movie(); movie && icon_item->get_active())
{
icon_item->setData(Qt::DecorationRole, movie->currentPixmap().scaled(m_icon_size, Qt::KeepAspectRatio));
}
else
{
std::lock_guard lock(icon_item->pixmap_mutex);
icon_item->setData(Qt::DecorationRole, game->pxmap);
if (!game->has_hover_gif)
@ -2986,3 +3081,29 @@ std::string game_list_frame::GetGameVersion(const game_info& game)
return game->info.app_ver;
}
void game_list_frame::WaitAndAbortRepaintThreads()
{
gui::utils::stop_future_watcher(m_repaint_watcher, true);
for (const game_info& game : m_game_data)
{
if (game && game->item)
{
game->item->wait_for_icon_loading(true);
}
}
}
void game_list_frame::WaitAndAbortSizeCalcThreads()
{
gui::utils::stop_future_watcher(m_size_watcher, true, m_size_watcher_cancel);
for (const game_info& game : m_game_data)
{
if (game && game->item)
{
game->item->wait_for_size_on_disk_loading(true);
}
}
}

View file

@ -92,6 +92,8 @@ Q_SIGNALS:
void RequestBoot(const game_info& game, cfg_mode config_mode = cfg_mode::custom, const std::string& config_path = "", const std::string& savestate = "");
void RequestIconSizeChange(const int& val);
void NotifyEmuSettingsChange();
void IconReady(movie_item* item);
void SizeOnDiskReady(const game_info& game);
protected:
/** Override inherited method from Qt to allow signalling when close happened.*/
void closeEvent(QCloseEvent* event) override;
@ -127,6 +129,9 @@ private:
game_info GetGameInfoByMode(const QTableWidgetItem* item) const;
static game_info GetGameInfoFromItem(const QTableWidgetItem* item);
void WaitAndAbortRepaintThreads();
void WaitAndAbortSizeCalcThreads();
// Which widget we are displaying depends on if we are in grid or list mode.
QMainWindow* m_game_dock = nullptr;
QStackedWidget* m_central_widget = nullptr;

View file

@ -81,7 +81,7 @@ movie_item* game_list_grid::addItem(const game_info& app, const QString& name, c
exp_size_f = m_icon_size + m_icon_size * m_margin_factor * 2;
}
QMovie* movie = item->movie();
std::shared_ptr<QMovie> movie = item->movie();
const bool draw_movie_frame = movie && movie->isValid() && item->get_active();
const QSize exp_size = (exp_size_f * device_pixel_ratio).toSize();

View file

@ -0,0 +1,148 @@
#include "stdafx.h"
#include "movie_item.h"
movie_item::movie_item() : QTableWidgetItem()
{
init_pointers();
}
movie_item::movie_item(const QString& text, int type) : QTableWidgetItem(text, type)
{
init_pointers();
}
movie_item::movie_item(const QIcon& icon, const QString& text, int type) : QTableWidgetItem(icon, text, type)
{
init_pointers();
}
movie_item::~movie_item()
{
if (m_movie)
{
m_movie->stop();
}
wait_for_icon_loading(true);
wait_for_size_on_disk_loading(true);
}
void movie_item::init_pointers()
{
m_icon_loading_aborted.reset(new atomic_t<bool>(false));
m_size_on_disk_loading_aborted.reset(new atomic_t<bool>(false));
}
void movie_item::set_active(bool active)
{
if (!std::exchange(m_active, active) && active && m_movie)
{
m_movie->jumpToFrame(1);
m_movie->start();
}
}
void movie_item::init_movie(const QString& path)
{
if (path.isEmpty() || !m_icon_callback) return;
m_movie.reset(new QMovie(path));
if (!m_movie->isValid())
{
m_movie.reset();
return;
}
QObject::connect(m_movie.get(), &QMovie::frameChanged, m_movie.get(), m_icon_callback);
}
void movie_item::call_icon_func() const
{
if (m_icon_callback)
{
m_icon_callback(0);
}
}
void movie_item::set_icon_func(const icon_callback_t& func)
{
m_icon_callback = func;
call_icon_func();
}
void movie_item::call_icon_load_func()
{
wait_for_icon_loading(true);
if (!m_icon_load_callback || m_icon_loading)
{
return;
}
*m_icon_loading_aborted = false;
m_icon_loading = true;
m_icon_load_thread.reset(QThread::create([this]()
{
if (m_icon_load_callback)
{
m_icon_load_callback();
}
}));
m_icon_load_thread->start();
}
void movie_item::set_icon_load_func(const icon_load_callback_t& func)
{
wait_for_icon_loading(true);
m_icon_loading = false;
m_icon_load_callback = func;
}
void movie_item::call_size_calc_func()
{
wait_for_size_on_disk_loading(true);
if (!m_size_calc_callback || m_size_on_disk_loading)
{
return;
}
*m_size_on_disk_loading_aborted = false;
m_size_on_disk_loading = true;
m_size_calc_thread.reset(QThread::create([this]()
{
if (m_size_calc_callback)
{
m_size_calc_callback();
}
}));
m_size_calc_thread->start();
}
void movie_item::set_size_calc_func(const size_calc_callback_t& func)
{
m_size_on_disk_loading = false;
m_size_calc_callback = func;
}
void movie_item::wait_for_icon_loading(bool abort)
{
if (m_icon_load_thread)
{
*m_icon_loading_aborted = abort;
m_icon_load_thread->wait();
m_icon_load_thread.reset();
}
}
void movie_item::wait_for_size_on_disk_loading(bool abort)
{
if (m_size_calc_thread)
{
*m_size_on_disk_loading_aborted = abort;
m_size_calc_thread->wait();
m_size_calc_thread.reset();
}
}

View file

@ -1,87 +1,89 @@
#pragma once
#include "util/atomic.hpp"
#include <QTableWidgetItem>
#include <QMovie>
#include <QObject>
#include <QThread>
#include <mutex>
#include <memory>
#include <functional>
using icon_callback_t = std::function<void(int)>;
using icon_load_callback_t = std::function<void()>;
using size_calc_callback_t = std::function<void()>;
class movie_item : public QTableWidgetItem
{
public:
movie_item() : QTableWidgetItem()
{
}
movie_item(const QString& text, int type = Type) : QTableWidgetItem(text, type)
{
}
movie_item(const QIcon& icon, const QString& text, int type = Type) : QTableWidgetItem(icon, text, type)
{
}
movie_item();
movie_item(const QString& text, int type = Type);
movie_item(const QIcon& icon, const QString& text, int type = Type);
~movie_item();
~movie_item()
{
if (m_movie)
{
m_movie->stop();
delete m_movie;
}
}
void init_pointers();
void set_active(bool active)
{
if (!std::exchange(m_active, active) && active && m_movie)
{
m_movie->jumpToFrame(1);
m_movie->start();
}
}
void set_active(bool active);
[[nodiscard]] bool get_active() const
{
return m_active;
}
[[nodiscard]] QMovie* movie() const
[[nodiscard]] std::shared_ptr<QMovie> movie() const
{
return m_movie;
}
void init_movie(const QString& path)
void init_movie(const QString& path);
void call_icon_func() const;
void set_icon_func(const icon_callback_t& func);
void call_icon_load_func();
void set_icon_load_func(const icon_load_callback_t& func);
void call_size_calc_func();
void set_size_calc_func(const size_calc_callback_t& func);
void wait_for_icon_loading(bool abort);
void wait_for_size_on_disk_loading(bool abort);
bool icon_loading() const
{
if (path.isEmpty() || !m_icon_callback) return;
if (QMovie* movie = new QMovie(path); movie->isValid())
{
m_movie = movie;
}
else
{
delete movie;
return;
}
QObject::connect(m_movie, &QMovie::frameChanged, m_movie, m_icon_callback);
return m_icon_loading;
}
void call_icon_func() const
bool size_on_disk_loading() const
{
if (m_icon_callback)
{
m_icon_callback(0);
}
return m_size_on_disk_loading;
}
void set_icon_func(const icon_callback_t& func)
std::shared_ptr<atomic_t<bool>> icon_loading_aborted() const
{
m_icon_callback = func;
call_icon_func();
return m_icon_loading_aborted;
}
std::shared_ptr<atomic_t<bool>> size_on_disk_loading_aborted() const
{
return m_size_on_disk_loading_aborted;
}
std::mutex pixmap_mutex;
private:
QMovie* m_movie = nullptr;
std::shared_ptr<QMovie> m_movie;
std::unique_ptr<QThread> m_icon_load_thread;
std::unique_ptr<QThread> m_size_calc_thread;
bool m_active = false;
atomic_t<bool> m_size_on_disk_loading = false;
atomic_t<bool> m_icon_loading = false;
size_calc_callback_t m_size_calc_callback = nullptr;
icon_load_callback_t m_icon_load_callback = nullptr;
icon_callback_t m_icon_callback = nullptr;
std::shared_ptr<atomic_t<bool>> m_icon_loading_aborted;
std::shared_ptr<atomic_t<bool>> m_size_on_disk_loading_aborted;
};

View file

@ -0,0 +1,68 @@
#include "table_item_delegate.h"
#include <QTableWidget>
#include "movie_item.h"
#include "gui_settings.h"
table_item_delegate::table_item_delegate(QObject* parent, bool has_icons)
: QStyledItemDelegate(parent), m_has_icons(has_icons)
{
}
void table_item_delegate::initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const
{
// Remove the focus frame around selected items
option->state &= ~QStyle::State_HasFocus;
if (m_has_icons && index.column() == 0)
{
// Don't highlight icons
option->state &= ~QStyle::State_Selected;
// Center icons
option->decorationAlignment = Qt::AlignCenter;
option->decorationPosition = QStyleOptionViewItem::Top;
}
QStyledItemDelegate::initStyleOption(option, index);
}
void table_item_delegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
if (index.column() == gui::game_list_columns::column_icon && option.state & QStyle::State_Selected)
{
// Add background highlight color to icons
painter->fillRect(option.rect, option.palette.color(QPalette::Highlight));
}
QStyledItemDelegate::paint(painter, option, index);
// Find out if the icon or size items are visible
if (index.column() == gui::game_list_columns::column_dir_size || (m_has_icons && index.column() == gui::game_list_columns::column_icon))
{
if (const QTableWidget* table = static_cast<const QTableWidget*>(parent()))
{
if (const QTableWidgetItem* current_item = table->item(index.row(), index.column());
current_item && table->visibleRegion().intersects(table->visualItemRect(current_item)))
{
if (movie_item* item = static_cast<movie_item*>(table->item(index.row(), gui::game_list_columns::column_icon)))
{
if (index.column() == gui::game_list_columns::column_dir_size)
{
if (!item->size_on_disk_loading())
{
item->call_size_calc_func();
}
}
else if (m_has_icons && index.column() == gui::game_list_columns::column_icon)
{
if (!item->icon_loading())
{
item->call_icon_load_func();
}
}
}
}
}
}
}

View file

@ -10,34 +10,9 @@ private:
bool m_has_icons;
public:
explicit table_item_delegate(QObject *parent = nullptr, bool has_icons = false) : QStyledItemDelegate(parent), m_has_icons(has_icons) {}
explicit table_item_delegate(QObject *parent = nullptr, bool has_icons = false);
void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const override
{
// Remove the focus frame around selected items
option->state &= ~QStyle::State_HasFocus;
void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const override;
if (m_has_icons && index.column() == 0)
{
// Don't highlight icons
option->state &= ~QStyle::State_Selected;
// Center icons
option->decorationAlignment = Qt::AlignCenter;
option->decorationPosition = QStyleOptionViewItem::Top;
}
QStyledItemDelegate::initStyleOption(option, index);
}
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
{
if (index.column() == 0 && option.state & QStyle::State_Selected)
{
// Add background highlight color to icons
painter->fillRect(option.rect, option.palette.color(QPalette::Highlight));
}
QStyledItemDelegate::paint(painter, option, index);
}
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
};