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; };