2020-09-17 20:00:07 +00:00
|
|
|
// SPDX-License-Identifier: MIT
|
2020-06-28 16:39:18 +00:00
|
|
|
//
|
2020-09-17 20:00:07 +00:00
|
|
|
// EmulationStation Desktop Edition
|
2020-06-28 16:39:18 +00:00
|
|
|
// IList.h
|
|
|
|
//
|
2021-01-12 22:10:39 +00:00
|
|
|
// List base class, used by both the gamelist views and the menu.
|
2020-06-28 16:39:18 +00:00
|
|
|
//
|
|
|
|
|
2017-10-31 17:12:50 +00:00
|
|
|
#ifndef ES_CORE_COMPONENTS_ILIST_H
|
|
|
|
#define ES_CORE_COMPONENTS_ILIST_H
|
2014-02-08 02:15:48 +00:00
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
#include "Window.h"
|
2014-06-20 01:30:09 +00:00
|
|
|
#include "components/ImageComponent.h"
|
2021-01-12 21:41:28 +00:00
|
|
|
#include "utils/StringUtil.h"
|
2014-02-08 02:15:48 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
enum CursorState {
|
2021-07-07 18:31:46 +00:00
|
|
|
CURSOR_STOPPED, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0).
|
2020-06-28 16:39:18 +00:00
|
|
|
CURSOR_SCROLLING
|
2014-02-08 03:45:28 +00:00
|
|
|
};
|
2014-02-08 02:15:48 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
enum ListLoopType {
|
2021-07-07 18:31:46 +00:00
|
|
|
LIST_ALWAYS_LOOP, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0).
|
2020-06-28 16:39:18 +00:00
|
|
|
LIST_PAUSE_AT_END,
|
|
|
|
LIST_NEVER_LOOP
|
2014-03-01 21:02:44 +00:00
|
|
|
};
|
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
struct ScrollTier {
|
|
|
|
int length; // How long we stay on this tier before going to the next.
|
|
|
|
int scrollDelay; // How long between scrolls.
|
2014-02-08 03:45:28 +00:00
|
|
|
};
|
2014-02-08 02:15:48 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
struct ScrollTierList {
|
|
|
|
const int count;
|
|
|
|
const ScrollTier* tiers;
|
2014-03-01 21:02:44 +00:00
|
|
|
};
|
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
// Default scroll tiers.
|
2021-07-07 18:31:46 +00:00
|
|
|
// clang-format off
|
2014-03-01 21:02:44 +00:00
|
|
|
const ScrollTier QUICK_SCROLL_TIERS[] = {
|
2021-07-07 18:31:46 +00:00
|
|
|
{ 500, 500 },
|
|
|
|
{ 1200, 114 },
|
|
|
|
{ 0, 16 }
|
2020-06-28 16:39:18 +00:00
|
|
|
};
|
|
|
|
const ScrollTierList LIST_SCROLL_STYLE_QUICK = {
|
2021-01-12 21:41:28 +00:00
|
|
|
3,
|
2020-06-28 16:39:18 +00:00
|
|
|
QUICK_SCROLL_TIERS
|
2014-02-08 03:45:28 +00:00
|
|
|
};
|
2014-03-01 21:02:44 +00:00
|
|
|
|
|
|
|
const ScrollTier SLOW_SCROLL_TIERS[] = {
|
2021-07-07 18:31:46 +00:00
|
|
|
{ 500, 500 },
|
|
|
|
{ 0, 200 }
|
2014-03-01 21:02:44 +00:00
|
|
|
};
|
2020-06-28 16:39:18 +00:00
|
|
|
|
2021-01-12 21:41:28 +00:00
|
|
|
const ScrollTierList LIST_SCROLL_STYLE_SLOW = {
|
|
|
|
2,
|
|
|
|
SLOW_SCROLL_TIERS
|
|
|
|
};
|
2021-07-07 18:31:46 +00:00
|
|
|
// clang-format on
|
2014-02-08 02:15:48 +00:00
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
template <typename EntryData, typename UserData> class IList : public GuiComponent
|
2014-02-08 03:45:28 +00:00
|
|
|
{
|
|
|
|
public:
|
2020-06-28 16:39:18 +00:00
|
|
|
struct Entry {
|
|
|
|
std::string name;
|
|
|
|
UserData object;
|
|
|
|
EntryData data;
|
|
|
|
};
|
2014-02-08 02:15:48 +00:00
|
|
|
|
2014-02-08 03:45:28 +00:00
|
|
|
protected:
|
2020-06-28 16:39:18 +00:00
|
|
|
int mCursor;
|
|
|
|
int mScrollTier;
|
|
|
|
int mScrollVelocity;
|
|
|
|
int mScrollTierAccumulator;
|
|
|
|
int mScrollCursorAccumulator;
|
2014-02-08 03:45:28 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
unsigned char mTitleOverlayOpacity;
|
|
|
|
unsigned int mTitleOverlayColor;
|
2014-02-13 23:10:28 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
const ScrollTierList& mTierList;
|
|
|
|
const ListLoopType mLoopType;
|
2014-03-01 21:02:44 +00:00
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
std::vector<Entry> mEntries;
|
2020-09-17 20:00:07 +00:00
|
|
|
Window* mWindow;
|
2019-08-25 15:23:02 +00:00
|
|
|
|
2014-02-08 03:45:28 +00:00
|
|
|
public:
|
2021-07-07 18:31:46 +00:00
|
|
|
IList(Window* window,
|
|
|
|
const ScrollTierList& tierList = LIST_SCROLL_STYLE_QUICK,
|
|
|
|
const ListLoopType& loopType = LIST_PAUSE_AT_END)
|
|
|
|
: GuiComponent(window)
|
|
|
|
, mTierList(tierList)
|
|
|
|
, mLoopType(loopType)
|
|
|
|
, mWindow(window)
|
2020-06-28 16:39:18 +00:00
|
|
|
{
|
|
|
|
mCursor = 0;
|
|
|
|
mScrollTier = 0;
|
|
|
|
mScrollVelocity = 0;
|
|
|
|
mScrollTierAccumulator = 0;
|
|
|
|
mScrollCursorAccumulator = 0;
|
|
|
|
|
|
|
|
mTitleOverlayOpacity = 0x00;
|
|
|
|
mTitleOverlayColor = 0xFFFFFF00;
|
|
|
|
}
|
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
bool isScrolling() const { return (mScrollVelocity != 0 && mScrollTier > 0); }
|
2020-06-28 16:39:18 +00:00
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
int getScrollingVelocity() { return mScrollVelocity; }
|
2020-06-28 16:39:18 +00:00
|
|
|
|
|
|
|
void stopScrolling()
|
|
|
|
{
|
2021-01-12 21:41:28 +00:00
|
|
|
mTitleOverlayOpacity = 0;
|
|
|
|
|
2020-06-28 16:39:18 +00:00
|
|
|
listInput(0);
|
2020-09-13 17:08:17 +00:00
|
|
|
if (mScrollVelocity == 0)
|
|
|
|
onCursorChanged(CURSOR_STOPPED);
|
2020-06-28 16:39:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void clear()
|
|
|
|
{
|
|
|
|
mEntries.clear();
|
|
|
|
mCursor = 0;
|
|
|
|
listInput(0);
|
|
|
|
onCursorChanged(CURSOR_STOPPED);
|
|
|
|
}
|
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
const std::string& getSelectedName()
|
2020-06-28 16:39:18 +00:00
|
|
|
{
|
|
|
|
assert(size() > 0);
|
|
|
|
return mEntries.at(mCursor).name;
|
|
|
|
}
|
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
const UserData& getSelected() const
|
2020-06-28 16:39:18 +00:00
|
|
|
{
|
|
|
|
assert(size() > 0);
|
|
|
|
return mEntries.at(mCursor).object;
|
|
|
|
}
|
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
const UserData& getNext() const
|
2020-09-20 08:05:03 +00:00
|
|
|
{
|
|
|
|
// If there is a next entry, then return it, otherwise return the current entry.
|
|
|
|
if (mCursor + 1 < mEntries.size())
|
2021-07-07 18:31:46 +00:00
|
|
|
return mEntries.at(mCursor + 1).object;
|
2020-09-20 08:05:03 +00:00
|
|
|
else
|
|
|
|
return mEntries.at(mCursor).object;
|
|
|
|
}
|
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
const UserData& getPrevious() const
|
2020-09-20 08:05:03 +00:00
|
|
|
{
|
|
|
|
// If there is a previous entry, then return it, otherwise return the current entry.
|
|
|
|
if (mCursor != 0)
|
2021-07-07 18:31:46 +00:00
|
|
|
return mEntries.at(mCursor - 1).object;
|
2020-09-20 08:05:03 +00:00
|
|
|
else
|
|
|
|
return mEntries.at(mCursor).object;
|
|
|
|
}
|
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
const UserData& getFirst() const
|
2020-06-28 16:39:18 +00:00
|
|
|
{
|
|
|
|
assert(size() > 0);
|
|
|
|
return mEntries.front().object;
|
|
|
|
}
|
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
const UserData& getLast() const
|
2020-06-28 16:39:18 +00:00
|
|
|
{
|
|
|
|
assert(size() > 0);
|
|
|
|
return mEntries.back().object;
|
|
|
|
}
|
|
|
|
|
|
|
|
void setCursor(typename std::vector<Entry>::const_iterator& it)
|
|
|
|
{
|
|
|
|
assert(it != mEntries.cend());
|
|
|
|
mCursor = it - mEntries.cbegin();
|
|
|
|
onCursorChanged(CURSOR_STOPPED);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns true if successful (select is in our list), false if not.
|
|
|
|
bool setCursor(const UserData& obj)
|
|
|
|
{
|
|
|
|
for (auto it = mEntries.cbegin(); it != mEntries.cend(); it++) {
|
|
|
|
if ((*it).object == obj) {
|
2020-09-17 20:00:07 +00:00
|
|
|
mCursor = static_cast<int>(it - mEntries.cbegin());
|
2020-06-28 16:39:18 +00:00
|
|
|
onCursorChanged(CURSOR_STOPPED);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Entry management.
|
2021-07-07 18:31:46 +00:00
|
|
|
void add(const Entry& e) { mEntries.push_back(e); }
|
2020-06-28 16:39:18 +00:00
|
|
|
|
|
|
|
bool remove(const UserData& obj)
|
|
|
|
{
|
|
|
|
for (auto it = mEntries.cbegin(); it != mEntries.cend(); it++) {
|
|
|
|
if ((*it).object == obj) {
|
|
|
|
remove(it);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
int size() const { return static_cast<int>(mEntries.size()); }
|
2014-02-08 03:45:28 +00:00
|
|
|
|
|
|
|
protected:
|
2020-06-28 16:39:18 +00:00
|
|
|
void remove(typename std::vector<Entry>::const_iterator& it)
|
|
|
|
{
|
|
|
|
if (mCursor > 0 && it - mEntries.cbegin() <= mCursor) {
|
|
|
|
mCursor--;
|
|
|
|
onCursorChanged(CURSOR_STOPPED);
|
|
|
|
}
|
|
|
|
|
|
|
|
mEntries.erase(it);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool listFirstRow()
|
|
|
|
{
|
|
|
|
mCursor = 0;
|
|
|
|
onCursorChanged(CURSOR_STOPPED);
|
|
|
|
onScroll();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool listLastRow()
|
|
|
|
{
|
2020-12-29 11:54:24 +00:00
|
|
|
mCursor = static_cast<int>(mEntries.size()) - 1;
|
2020-06-28 16:39:18 +00:00
|
|
|
onCursorChanged(CURSOR_STOPPED);
|
|
|
|
onScroll();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool listInput(int velocity) // A velocity of 0 = stop scrolling.
|
|
|
|
{
|
|
|
|
mScrollVelocity = velocity;
|
|
|
|
mScrollTier = 0;
|
|
|
|
mScrollTierAccumulator = 0;
|
|
|
|
mScrollCursorAccumulator = 0;
|
|
|
|
|
|
|
|
int prevCursor = mCursor;
|
|
|
|
scroll(mScrollVelocity);
|
|
|
|
return (prevCursor != mCursor);
|
|
|
|
}
|
|
|
|
|
|
|
|
void listUpdate(int deltaTime)
|
|
|
|
{
|
|
|
|
// Update the title overlay opacity.
|
|
|
|
// Fade in if scroll tier is >= 1, otherwise fade out.
|
|
|
|
const int dir = (mScrollTier >= mTierList.count - 1) ? 1 : -1;
|
2021-01-12 21:41:28 +00:00
|
|
|
// We simply translate the time directly to opacity, i.e. no scaling is performed.
|
|
|
|
int op = mTitleOverlayOpacity + deltaTime * dir;
|
2020-06-28 16:39:18 +00:00
|
|
|
if (op >= 255)
|
|
|
|
mTitleOverlayOpacity = 255;
|
|
|
|
else if (op <= 0)
|
|
|
|
mTitleOverlayOpacity = 0;
|
|
|
|
else
|
2020-09-17 20:00:07 +00:00
|
|
|
mTitleOverlayOpacity = static_cast<unsigned char>(op);
|
2020-06-28 16:39:18 +00:00
|
|
|
|
|
|
|
if (mScrollVelocity == 0 || size() < 2)
|
|
|
|
return;
|
|
|
|
|
|
|
|
mScrollCursorAccumulator += deltaTime;
|
|
|
|
mScrollTierAccumulator += deltaTime;
|
|
|
|
|
|
|
|
// We delay scrolling until after scroll tier has updated so isScrolling() returns
|
|
|
|
// accurately during onCursorChanged callbacks. We don't just do scroll tier first
|
|
|
|
// because it would not catch the scrollDelay == tier length case.
|
|
|
|
int scrollCount = 0;
|
|
|
|
while (mScrollCursorAccumulator >= mTierList.tiers[mScrollTier].scrollDelay) {
|
|
|
|
mScrollCursorAccumulator -= mTierList.tiers[mScrollTier].scrollDelay;
|
|
|
|
scrollCount++;
|
|
|
|
}
|
|
|
|
|
2021-01-12 21:41:28 +00:00
|
|
|
// Should we go to the next scrolling tier?
|
2021-07-07 18:31:46 +00:00
|
|
|
while (mScrollTier < mTierList.count - 1 &&
|
|
|
|
mScrollTierAccumulator >= mTierList.tiers[mScrollTier].length) {
|
2020-06-28 16:39:18 +00:00
|
|
|
mScrollTierAccumulator -= mTierList.tiers[mScrollTier].length;
|
|
|
|
mScrollTier++;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Actually perform the scrolling.
|
|
|
|
for (int i = 0; i < scrollCount; i++)
|
|
|
|
scroll(mScrollVelocity);
|
|
|
|
}
|
|
|
|
|
|
|
|
void listRenderTitleOverlay(const Transform4x4f& /*trans*/)
|
|
|
|
{
|
2021-01-12 21:41:28 +00:00
|
|
|
if (!Settings::getInstance()->getBool("ListScrollOverlay"))
|
2020-06-28 16:39:18 +00:00
|
|
|
return;
|
|
|
|
|
2021-01-12 21:41:28 +00:00
|
|
|
if (size() == 0 || mTitleOverlayOpacity == 0) {
|
|
|
|
mWindow->renderListScrollOverlay(0, "");
|
|
|
|
return;
|
|
|
|
}
|
2020-06-28 16:39:18 +00:00
|
|
|
|
2021-01-12 21:41:28 +00:00
|
|
|
std::string titleIndex;
|
|
|
|
bool favoritesSorting;
|
2020-06-28 16:39:18 +00:00
|
|
|
|
2021-01-12 21:41:28 +00:00
|
|
|
if (getSelected()->getSystem()->isCustomCollection())
|
|
|
|
favoritesSorting = Settings::getInstance()->getBool("FavFirstCustom");
|
|
|
|
else
|
|
|
|
favoritesSorting = Settings::getInstance()->getBool("FavoritesFirst");
|
|
|
|
|
|
|
|
if (favoritesSorting && getSelected()->getFavorite()) {
|
2021-07-07 18:31:46 +00:00
|
|
|
#if defined(_MSC_VER) // MSVC compiler.
|
2021-01-12 21:41:28 +00:00
|
|
|
titleIndex = Utils::String::wideStringToString(L"\uF005");
|
2021-07-07 18:31:46 +00:00
|
|
|
#else
|
2021-01-12 21:41:28 +00:00
|
|
|
titleIndex = "\uF005";
|
2021-07-07 18:31:46 +00:00
|
|
|
#endif
|
2021-01-12 21:41:28 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
titleIndex = getSelected()->getName();
|
|
|
|
if (titleIndex.size()) {
|
|
|
|
titleIndex[0] = toupper(titleIndex[0]);
|
|
|
|
if (titleIndex.size() > 1) {
|
|
|
|
titleIndex = titleIndex.substr(0, 2);
|
|
|
|
titleIndex[1] = tolower(titleIndex[1]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-06-28 16:39:18 +00:00
|
|
|
|
2021-01-12 21:41:28 +00:00
|
|
|
// The actual rendering takes place in Window to make sure that the overlay is placed on
|
|
|
|
// top of all GUI elements but below the info popups and GPU statistics overlay.
|
|
|
|
mWindow->renderListScrollOverlay(mTitleOverlayOpacity, titleIndex);
|
2020-06-28 16:39:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void scroll(int amt)
|
|
|
|
{
|
|
|
|
if (mScrollVelocity == 0 || size() < 2)
|
|
|
|
return;
|
|
|
|
|
|
|
|
int cursor = mCursor + amt;
|
|
|
|
int absAmt = amt < 0 ? -amt : amt;
|
|
|
|
|
|
|
|
// Stop at the end if we've been holding down the button for a long time or
|
|
|
|
// we're scrolling faster than one item at a time (e.g. page up/down).
|
|
|
|
// Otherwise, loop around.
|
|
|
|
if ((mLoopType == LIST_PAUSE_AT_END && (mScrollTier > 0 || absAmt > 1)) ||
|
|
|
|
mLoopType == LIST_NEVER_LOOP) {
|
|
|
|
if (cursor < 0) {
|
|
|
|
cursor = 0;
|
|
|
|
mScrollVelocity = 0;
|
|
|
|
mScrollTier = 0;
|
|
|
|
}
|
|
|
|
else if (cursor >= size()) {
|
|
|
|
cursor = size() - 1;
|
|
|
|
mScrollVelocity = 0;
|
|
|
|
mScrollTier = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
while (cursor < 0)
|
|
|
|
cursor += size();
|
|
|
|
while (cursor >= size())
|
|
|
|
cursor -= size();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cursor != mCursor)
|
|
|
|
onScroll();
|
|
|
|
|
|
|
|
mCursor = cursor;
|
|
|
|
onCursorChanged((mScrollTier > 0) ? CURSOR_SCROLLING : CURSOR_STOPPED);
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual void onCursorChanged(const CursorState& /*state*/) {}
|
|
|
|
virtual void onScroll() {}
|
2014-02-08 02:15:48 +00:00
|
|
|
};
|
2017-10-31 17:12:50 +00:00
|
|
|
|
|
|
|
#endif // ES_CORE_COMPONENTS_ILIST_H
|