diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj
index c87940b4cc..42ea0f80f7 100644
--- a/rpcs3/rpcs3.vcxproj
+++ b/rpcs3/rpcs3.vcxproj
@@ -671,6 +671,7 @@
+
@@ -696,6 +697,7 @@
+
diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters
index de35dad43e..5f76651e2c 100644
--- a/rpcs3/rpcs3.vcxproj.filters
+++ b/rpcs3/rpcs3.vcxproj.filters
@@ -936,6 +936,12 @@
Generated Files\Release
+
+ Gui\custom items
+
+
+ Gui\custom items
+
diff --git a/rpcs3/rpcs3qt/CMakeLists.txt b/rpcs3/rpcs3qt/CMakeLists.txt
index 6813c1f9fd..09d1a4fa49 100644
--- a/rpcs3/rpcs3qt/CMakeLists.txt
+++ b/rpcs3/rpcs3qt/CMakeLists.txt
@@ -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
diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp
index caea81b70e..06c62edb49 100644
--- a/rpcs3/rpcs3qt/game_list_frame.cpp
+++ b/rpcs3/rpcs3qt/game_list_frame.cpp
@@ -81,7 +81,7 @@ game_list_frame::game_list_frame(std::shared_ptr 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, std
connect(&m_refresh_watcher, &QFutureWatcher::finished, this, &game_list_frame::OnRefreshFinished);
connect(&m_refresh_watcher, &QFutureWatcher::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, 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(game_size));
+ }
+ });
connect(&m_size_watcher, &QFutureWatcher::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>(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 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 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);
+ }
+ }
+}
diff --git a/rpcs3/rpcs3qt/game_list_frame.h b/rpcs3/rpcs3qt/game_list_frame.h
index 4eb1e919e8..843eba5249 100644
--- a/rpcs3/rpcs3qt/game_list_frame.h
+++ b/rpcs3/rpcs3qt/game_list_frame.h
@@ -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;
diff --git a/rpcs3/rpcs3qt/game_list_grid.cpp b/rpcs3/rpcs3qt/game_list_grid.cpp
index e3a4d05cbc..bf581d23ce 100644
--- a/rpcs3/rpcs3qt/game_list_grid.cpp
+++ b/rpcs3/rpcs3qt/game_list_grid.cpp
@@ -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 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();
diff --git a/rpcs3/rpcs3qt/movie_item.cpp b/rpcs3/rpcs3qt/movie_item.cpp
new file mode 100644
index 0000000000..bbb01a7f31
--- /dev/null
+++ b/rpcs3/rpcs3qt/movie_item.cpp
@@ -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(false));
+ m_size_on_disk_loading_aborted.reset(new atomic_t(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();
+ }
+}
diff --git a/rpcs3/rpcs3qt/movie_item.h b/rpcs3/rpcs3qt/movie_item.h
index 83a79d2a5d..8c73cba4a3 100644
--- a/rpcs3/rpcs3qt/movie_item.h
+++ b/rpcs3/rpcs3qt/movie_item.h
@@ -1,87 +1,89 @@
#pragma once
+#include "util/atomic.hpp"
+
#include
#include
#include
+#include
+#include
+#include
#include
using icon_callback_t = std::function;
+using icon_load_callback_t = std::function;
+using size_calc_callback_t = std::function;
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 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> icon_loading_aborted() const
{
- m_icon_callback = func;
- call_icon_func();
+ return m_icon_loading_aborted;
}
+ std::shared_ptr> 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 m_movie;
+ std::unique_ptr m_icon_load_thread;
+ std::unique_ptr m_size_calc_thread;
bool m_active = false;
+ atomic_t m_size_on_disk_loading = false;
+ atomic_t 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> m_icon_loading_aborted;
+ std::shared_ptr> m_size_on_disk_loading_aborted;
};
diff --git a/rpcs3/rpcs3qt/table_item_delegate.cpp b/rpcs3/rpcs3qt/table_item_delegate.cpp
new file mode 100644
index 0000000000..59e37bf7aa
--- /dev/null
+++ b/rpcs3/rpcs3qt/table_item_delegate.cpp
@@ -0,0 +1,68 @@
+#include "table_item_delegate.h"
+
+#include
+#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(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(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();
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/rpcs3/rpcs3qt/table_item_delegate.h b/rpcs3/rpcs3qt/table_item_delegate.h
index 88ecdff73b..d6a29484b4 100644
--- a/rpcs3/rpcs3qt/table_item_delegate.h
+++ b/rpcs3/rpcs3qt/table_item_delegate.h
@@ -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;
};