mirror of
https://github.com/RetroDECK/Duckstation.git
synced 2024-11-29 09:05:41 +00:00
Qt: Reduce game list jank after shutting down VM
Prevents progress bar briefly appearing, and the list scrolling to the top when you exit a game.
This commit is contained in:
parent
3a83c4265c
commit
9a626caad9
|
@ -119,15 +119,15 @@ static std::string GetCustomPropertiesFile();
|
||||||
static FileSystem::ManagedCFilePtr OpenMemoryCardTimestampCache(bool for_write);
|
static FileSystem::ManagedCFilePtr OpenMemoryCardTimestampCache(bool for_write);
|
||||||
static bool UpdateMemcardTimestampCache(const MemcardTimestampCacheEntry& entry);
|
static bool UpdateMemcardTimestampCache(const MemcardTimestampCacheEntry& entry);
|
||||||
|
|
||||||
} // namespace GameList
|
static EntryList s_entries;
|
||||||
|
|
||||||
static std::vector<GameList::Entry> s_entries;
|
|
||||||
static std::recursive_mutex s_mutex;
|
static std::recursive_mutex s_mutex;
|
||||||
static GameList::CacheMap s_cache_map;
|
static CacheMap s_cache_map;
|
||||||
static std::vector<GameList::MemcardTimestampCacheEntry> s_memcard_timestamp_cache_entries;
|
static std::vector<MemcardTimestampCacheEntry> s_memcard_timestamp_cache_entries;
|
||||||
|
|
||||||
static bool s_game_list_loaded = false;
|
static bool s_game_list_loaded = false;
|
||||||
|
|
||||||
|
} // namespace GameList
|
||||||
|
|
||||||
const char* GameList::GetEntryTypeName(EntryType type)
|
const char* GameList::GetEntryTypeName(EntryType type)
|
||||||
{
|
{
|
||||||
static std::array<const char*, static_cast<int>(EntryType::Count)> names = {
|
static std::array<const char*, static_cast<int>(EntryType::Count)> names = {
|
||||||
|
@ -823,6 +823,13 @@ void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback*
|
||||||
CreateDiscSetEntries(played_time);
|
CreateDiscSetEntries(played_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GameList::EntryList GameList::TakeEntryList()
|
||||||
|
{
|
||||||
|
EntryList ret = std::move(s_entries);
|
||||||
|
s_entries = {};
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
void GameList::CreateDiscSetEntries(const PlayedTimeMap& played_time_map)
|
void GameList::CreateDiscSetEntries(const PlayedTimeMap& played_time_map)
|
||||||
{
|
{
|
||||||
std::unique_lock lock(s_mutex);
|
std::unique_lock lock(s_mutex);
|
||||||
|
|
|
@ -71,6 +71,8 @@ struct Entry
|
||||||
ALWAYS_INLINE EntryType GetSortType() const { return (type == EntryType::DiscSet) ? EntryType::Disc : type; }
|
ALWAYS_INLINE EntryType GetSortType() const { return (type == EntryType::DiscSet) ? EntryType::Disc : type; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
using EntryList = std::vector<Entry>;
|
||||||
|
|
||||||
const char* GetEntryTypeName(EntryType type);
|
const char* GetEntryTypeName(EntryType type);
|
||||||
const char* GetEntryTypeDisplayName(EntryType type);
|
const char* GetEntryTypeDisplayName(EntryType type);
|
||||||
|
|
||||||
|
@ -97,6 +99,10 @@ bool IsGameListLoaded();
|
||||||
/// If only_cache is set, no new files will be scanned, only those present in the cache.
|
/// If only_cache is set, no new files will be scanned, only those present in the cache.
|
||||||
void Refresh(bool invalidate_cache, bool only_cache = false, ProgressCallback* progress = nullptr);
|
void Refresh(bool invalidate_cache, bool only_cache = false, ProgressCallback* progress = nullptr);
|
||||||
|
|
||||||
|
/// Moves the current game list, which can be temporarily displayed in the UI until refresh completes.
|
||||||
|
/// The caller **must** call Refresh() afterward, otherwise it will be permanently lost.
|
||||||
|
EntryList TakeEntryList();
|
||||||
|
|
||||||
/// Add played time for the specified serial.
|
/// Add played time for the specified serial.
|
||||||
void AddPlayedTimeForSerial(const std::string& serial, std::time_t last_time, std::time_t add_time);
|
void AddPlayedTimeForSerial(const std::string& serial, std::time_t last_time, std::time_t add_time);
|
||||||
void ClearPlayedTimeForSerial(const std::string& serial);
|
void ClearPlayedTimeForSerial(const std::string& serial);
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
#include "core/system.h"
|
#include "core/system.h"
|
||||||
|
|
||||||
#include "common/file_system.h"
|
#include "common/file_system.h"
|
||||||
#include "common/log.h"
|
|
||||||
#include "common/path.h"
|
#include "common/path.h"
|
||||||
#include "common/string_util.h"
|
#include "common/string_util.h"
|
||||||
|
|
||||||
|
@ -21,8 +20,6 @@
|
||||||
#include <QtGui/QIcon>
|
#include <QtGui/QIcon>
|
||||||
#include <QtGui/QPainter>
|
#include <QtGui/QPainter>
|
||||||
|
|
||||||
Log_SetChannel(GameList);
|
|
||||||
|
|
||||||
static constexpr std::array<const char*, GameListModel::Column_Count> s_column_names = {
|
static constexpr std::array<const char*, GameListModel::Column_Count> s_column_names = {
|
||||||
{"Icon", "Serial", "Title", "File Title", "Developer", "Publisher", "Genre", "Year", "Players", "Time Played",
|
{"Icon", "Serial", "Title", "File Title", "Developer", "Publisher", "Genre", "Year", "Players", "Time Played",
|
||||||
"Last Played", "Size", "File Size", "Region", "Compatibility", "Cover"}};
|
"Last Played", "Size", "File Size", "Region", "Compatibility", "Cover"}};
|
||||||
|
@ -334,9 +331,13 @@ int GameListModel::getCoverArtSpacing() const
|
||||||
|
|
||||||
int GameListModel::rowCount(const QModelIndex& parent) const
|
int GameListModel::rowCount(const QModelIndex& parent) const
|
||||||
{
|
{
|
||||||
if (parent.isValid())
|
if (parent.isValid()) [[unlikely]]
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
|
if (m_taken_entries.has_value())
|
||||||
|
return static_cast<int>(m_taken_entries->size());
|
||||||
|
|
||||||
|
const auto lock = GameList::GetLock();
|
||||||
return static_cast<int>(GameList::GetEntryCount());
|
return static_cast<int>(GameList::GetEntryCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -350,18 +351,32 @@ int GameListModel::columnCount(const QModelIndex& parent) const
|
||||||
|
|
||||||
QVariant GameListModel::data(const QModelIndex& index, int role) const
|
QVariant GameListModel::data(const QModelIndex& index, int role) const
|
||||||
{
|
{
|
||||||
if (!index.isValid())
|
if (!index.isValid()) [[unlikely]]
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
const int row = index.row();
|
const int row = index.row();
|
||||||
if (row < 0 || row >= static_cast<int>(GameList::GetEntryCount()))
|
DebugAssert(row >= 0);
|
||||||
return {};
|
|
||||||
|
|
||||||
const auto lock = GameList::GetLock();
|
if (m_taken_entries.has_value())
|
||||||
const GameList::Entry* ge = GameList::GetEntryByIndex(row);
|
{
|
||||||
if (!ge)
|
if (static_cast<u32>(row) >= m_taken_entries->size())
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
|
return data(index, role, &m_taken_entries.value()[row]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const auto lock = GameList::GetLock();
|
||||||
|
const GameList::Entry* ge = GameList::GetEntryByIndex(static_cast<u32>(row));
|
||||||
|
if (!ge)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
return data(index, role, ge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant GameListModel::data(const QModelIndex& index, int role, const GameList::Entry* ge) const
|
||||||
|
{
|
||||||
switch (role)
|
switch (role)
|
||||||
{
|
{
|
||||||
case Qt::DisplayRole:
|
case Qt::DisplayRole:
|
||||||
|
@ -544,10 +559,27 @@ QVariant GameListModel::headerData(int section, Qt::Orientation orientation, int
|
||||||
return m_column_display_names[section];
|
return m_column_display_names[section];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool GameListModel::hasTakenGameList() const
|
||||||
|
{
|
||||||
|
return m_taken_entries.has_value();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameListModel::takeGameList()
|
||||||
|
{
|
||||||
|
const auto lock = GameList::GetLock();
|
||||||
|
m_taken_entries = GameList::TakeEntryList();
|
||||||
|
|
||||||
|
// If it's empty (e.g. first boot), don't use it.
|
||||||
|
if (m_taken_entries->empty())
|
||||||
|
m_taken_entries.reset();
|
||||||
|
}
|
||||||
|
|
||||||
void GameListModel::refresh()
|
void GameListModel::refresh()
|
||||||
{
|
{
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
|
|
||||||
|
m_taken_entries.reset();
|
||||||
|
|
||||||
// Invalidate memcard LRU cache, forcing a re-query of the memcard timestamps.
|
// Invalidate memcard LRU cache, forcing a re-query of the memcard timestamps.
|
||||||
m_memcard_pixmap_cache.Clear();
|
m_memcard_pixmap_cache.Clear();
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,9 @@ public:
|
||||||
|
|
||||||
ALWAYS_INLINE const QString& getColumnDisplayName(int column) { return m_column_display_names[column]; }
|
ALWAYS_INLINE const QString& getColumnDisplayName(int column) { return m_column_display_names[column]; }
|
||||||
|
|
||||||
|
bool hasTakenGameList() const;
|
||||||
|
void takeGameList();
|
||||||
|
|
||||||
void refresh();
|
void refresh();
|
||||||
void reloadThemeSpecificImages();
|
void reloadThemeSpecificImages();
|
||||||
|
|
||||||
|
@ -98,6 +101,8 @@ private:
|
||||||
};
|
};
|
||||||
#pragma pack(pop)
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
QVariant data(const QModelIndex& index, int role, const GameList::Entry* ge) const;
|
||||||
|
|
||||||
void loadCommonImages();
|
void loadCommonImages();
|
||||||
void loadThemeSpecificImages();
|
void loadThemeSpecificImages();
|
||||||
void setColumnDisplayNames();
|
void setColumnDisplayNames();
|
||||||
|
@ -109,6 +114,8 @@ private:
|
||||||
|
|
||||||
static QString formatTimespan(time_t timespan);
|
static QString formatTimespan(time_t timespan);
|
||||||
|
|
||||||
|
std::optional<GameList::EntryList> m_taken_entries;
|
||||||
|
|
||||||
float m_cover_scale = 0.0f;
|
float m_cover_scale = 0.0f;
|
||||||
bool m_show_titles_for_covers = false;
|
bool m_show_titles_for_covers = false;
|
||||||
bool m_show_game_icons = false;
|
bool m_show_game_icons = false;
|
||||||
|
|
|
@ -16,6 +16,11 @@ AsyncRefreshProgressCallback::AsyncRefreshProgressCallback(GameListRefreshThread
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float AsyncRefreshProgressCallback::timeSinceStart() const
|
||||||
|
{
|
||||||
|
return m_start_time.GetTimeSeconds();
|
||||||
|
}
|
||||||
|
|
||||||
void AsyncRefreshProgressCallback::Cancel()
|
void AsyncRefreshProgressCallback::Cancel()
|
||||||
{
|
{
|
||||||
// Not atomic, but we don't need to cancel immediately.
|
// Not atomic, but we don't need to cancel immediately.
|
||||||
|
@ -87,7 +92,7 @@ void AsyncRefreshProgressCallback::ModalInformation(const std::string_view messa
|
||||||
|
|
||||||
void AsyncRefreshProgressCallback::fireUpdate()
|
void AsyncRefreshProgressCallback::fireUpdate()
|
||||||
{
|
{
|
||||||
m_parent->refreshProgress(m_status_text, m_last_value, m_last_range);
|
m_parent->refreshProgress(m_status_text, m_last_value, m_last_range, m_start_time.GetTimeSeconds());
|
||||||
}
|
}
|
||||||
|
|
||||||
GameListRefreshThread::GameListRefreshThread(bool invalidate_cache)
|
GameListRefreshThread::GameListRefreshThread(bool invalidate_cache)
|
||||||
|
@ -97,6 +102,11 @@ GameListRefreshThread::GameListRefreshThread(bool invalidate_cache)
|
||||||
|
|
||||||
GameListRefreshThread::~GameListRefreshThread() = default;
|
GameListRefreshThread::~GameListRefreshThread() = default;
|
||||||
|
|
||||||
|
float GameListRefreshThread::timeSinceStart() const
|
||||||
|
{
|
||||||
|
return m_progress.timeSinceStart();
|
||||||
|
}
|
||||||
|
|
||||||
void GameListRefreshThread::cancel()
|
void GameListRefreshThread::cancel()
|
||||||
{
|
{
|
||||||
m_progress.Cancel();
|
m_progress.Cancel();
|
||||||
|
|
|
@ -16,6 +16,8 @@ class AsyncRefreshProgressCallback : public ProgressCallback
|
||||||
public:
|
public:
|
||||||
AsyncRefreshProgressCallback(GameListRefreshThread* parent);
|
AsyncRefreshProgressCallback(GameListRefreshThread* parent);
|
||||||
|
|
||||||
|
float timeSinceStart() const;
|
||||||
|
|
||||||
void Cancel();
|
void Cancel();
|
||||||
|
|
||||||
void PushState() override;
|
void PushState() override;
|
||||||
|
@ -33,7 +35,7 @@ private:
|
||||||
void fireUpdate();
|
void fireUpdate();
|
||||||
|
|
||||||
GameListRefreshThread* m_parent;
|
GameListRefreshThread* m_parent;
|
||||||
Common::Timer m_last_update_time;
|
Common::Timer m_start_time;
|
||||||
QString m_status_text;
|
QString m_status_text;
|
||||||
int m_last_range = 1;
|
int m_last_range = 1;
|
||||||
int m_last_value = 0;
|
int m_last_value = 0;
|
||||||
|
@ -47,10 +49,12 @@ public:
|
||||||
GameListRefreshThread(bool invalidate_cache);
|
GameListRefreshThread(bool invalidate_cache);
|
||||||
~GameListRefreshThread();
|
~GameListRefreshThread();
|
||||||
|
|
||||||
|
float timeSinceStart() const;
|
||||||
|
|
||||||
void cancel();
|
void cancel();
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void refreshProgress(const QString& status, int current, int total);
|
void refreshProgress(const QString& status, int current, int total, float time);
|
||||||
void refreshComplete();
|
void refreshComplete();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
|
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
|
||||||
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
|
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
|
||||||
|
|
||||||
#include "gamelistwidget.h"
|
#include "gamelistwidget.h"
|
||||||
|
@ -182,9 +182,6 @@ void GameListWidget::initialize()
|
||||||
connect(m_ui.searchText, &QLineEdit::textChanged, this,
|
connect(m_ui.searchText, &QLineEdit::textChanged, this,
|
||||||
[this](const QString& text) { m_sort_model->setFilterName(text); });
|
[this](const QString& text) { m_sort_model->setFilterName(text); });
|
||||||
|
|
||||||
// Works around a strange bug where after hiding the game list, the cursor for the whole window changes to a beam..
|
|
||||||
// m_ui.searchText->setCursor(QCursor(Qt::ArrowCursor));
|
|
||||||
|
|
||||||
m_table_view = new QTableView(m_ui.stack);
|
m_table_view = new QTableView(m_ui.stack);
|
||||||
m_table_view->setModel(m_sort_model);
|
m_table_view->setModel(m_sort_model);
|
||||||
m_table_view->setSortingEnabled(true);
|
m_table_view->setSortingEnabled(true);
|
||||||
|
@ -285,6 +282,9 @@ void GameListWidget::refresh(bool invalidate_cache)
|
||||||
{
|
{
|
||||||
cancelRefresh();
|
cancelRefresh();
|
||||||
|
|
||||||
|
if (!invalidate_cache)
|
||||||
|
m_model->takeGameList();
|
||||||
|
|
||||||
m_refresh_thread = new GameListRefreshThread(invalidate_cache);
|
m_refresh_thread = new GameListRefreshThread(invalidate_cache);
|
||||||
connect(m_refresh_thread, &GameListRefreshThread::refreshProgress, this, &GameListWidget::onRefreshProgress,
|
connect(m_refresh_thread, &GameListRefreshThread::refreshProgress, this, &GameListWidget::onRefreshProgress,
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
|
@ -314,14 +314,19 @@ void GameListWidget::reloadThemeSpecificImages()
|
||||||
m_model->reloadThemeSpecificImages();
|
m_model->reloadThemeSpecificImages();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameListWidget::onRefreshProgress(const QString& status, int current, int total)
|
void GameListWidget::onRefreshProgress(const QString& status, int current, int total, float time)
|
||||||
{
|
{
|
||||||
|
// Avoid spamming the UI on very short refresh (e.g. game exit).
|
||||||
|
static constexpr float SHORT_REFRESH_TIME = 0.5f;
|
||||||
|
if (!m_model->hasTakenGameList())
|
||||||
|
m_model->refresh();
|
||||||
|
|
||||||
// switch away from the placeholder while we scan, in case we find anything
|
// switch away from the placeholder while we scan, in case we find anything
|
||||||
if (m_ui.stack->currentIndex() == 2)
|
if (m_ui.stack->currentIndex() == 2)
|
||||||
m_ui.stack->setCurrentIndex(Host::GetBaseBoolSettingValue("UI", "GameListGridView", false) ? 1 : 0);
|
m_ui.stack->setCurrentIndex(Host::GetBaseBoolSettingValue("UI", "GameListGridView", false) ? 1 : 0);
|
||||||
|
|
||||||
m_model->refresh();
|
if (!m_model->hasTakenGameList() || time >= SHORT_REFRESH_TIME)
|
||||||
emit refreshProgress(status, current, total);
|
emit refreshProgress(status, current, total);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameListWidget::onRefreshComplete()
|
void GameListWidget::onRefreshComplete()
|
||||||
|
|
|
@ -69,7 +69,7 @@ Q_SIGNALS:
|
||||||
void layoutChange();
|
void layoutChange();
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void onRefreshProgress(const QString& status, int current, int total);
|
void onRefreshProgress(const QString& status, int current, int total, float time);
|
||||||
void onRefreshComplete();
|
void onRefreshComplete();
|
||||||
|
|
||||||
void onSelectionModelCurrentChanged(const QModelIndex& current, const QModelIndex& previous);
|
void onSelectionModelCurrentChanged(const QModelIndex& current, const QModelIndex& previous);
|
||||||
|
|
Loading…
Reference in a new issue