mirror of
https://github.com/RetroDECK/ES-DE.git
synced 2024-11-21 21:55:38 +00:00
Merge branch 'master' into unstable
Conflicts: src/components/ImageComponent.h
This commit is contained in:
commit
fe8c592623
|
@ -135,6 +135,7 @@ set(ES_HEADERS
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/AnimationComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/ComponentListComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/ImageComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/ScrollableContainer.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/SliderComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/SwitchComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextComponent.h
|
||||
|
@ -178,6 +179,7 @@ set(ES_SOURCES
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/AnimationComponent.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/ComponentListComponent.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/ImageComponent.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/ScrollableContainer.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/SliderComponent.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/SwitchComponent.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextComponent.cpp
|
||||
|
|
|
@ -120,7 +120,7 @@ The gamelist.xml for a system defines metadata for a system's games. This metada
|
|||
|
||||
**Making a gamelist.xml by hand sucks, so a cool guy named Pendor made a python script which automatically generates a gamelist.xml for you, with boxart automatically downloaded. It can be found here:** https://github.com/elpendor/ES-scraper
|
||||
|
||||
If a file named gamelist.xml is found in the root of a system's search directory OR within `~/.emulationstation/%NAME%/`, it will be parsed and the detailed GuiGameList will be used. This means you can define images, descriptions, and different names for files. Note that only standard ASCII characters are supported (if you see a weird [X] symbol, you're probably using unicode!).
|
||||
If a file named gamelist.xml is found in the root of a system's search directory OR within `~/.emulationstation/%NAME%/`, game metadata will be loaded from it. This allows you to define images, descriptions, and different names for files. Note that only standard ASCII characters are supported for text (if you see a weird [X] symbol, you're probably using unicode!).
|
||||
Images will be automatically resized to fit within the left column of the screen. Smaller images will load faster, so try to keep your resolution low.
|
||||
An example gamelist.xml:
|
||||
```
|
||||
|
|
|
@ -9,9 +9,9 @@ class FileData
|
|||
{
|
||||
public:
|
||||
virtual ~FileData() { };
|
||||
virtual bool isFolder() = 0;
|
||||
virtual std::string getName() = 0;
|
||||
virtual std::string getPath() = 0;
|
||||
virtual bool isFolder() const = 0;
|
||||
virtual const std::string & getName() const = 0;
|
||||
virtual const std::string & getPath() const = 0;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
@ -1,19 +1,29 @@
|
|||
#include "FolderData.h"
|
||||
#include "SystemData.h"
|
||||
#include "GameData.h"
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
|
||||
bool FolderData::isFolder() { return true; }
|
||||
std::string FolderData::getName() { return mName; }
|
||||
std::string FolderData::getPath() { return mPath; }
|
||||
|
||||
std::map<FolderData::ComparisonFunction*, std::string> FolderData::sortStateNameMap;
|
||||
|
||||
bool FolderData::isFolder() const { return true; }
|
||||
const std::string & FolderData::getName() const { return mName; }
|
||||
const std::string & FolderData::getPath() const { return mPath; }
|
||||
unsigned int FolderData::getFileCount() { return mFileVector.size(); }
|
||||
FileData* FolderData::getFile(unsigned int i) { return mFileVector.at(i); }
|
||||
|
||||
|
||||
FolderData::FolderData(SystemData* system, std::string path, std::string name)
|
||||
: mSystem(system), mPath(path), mName(name)
|
||||
{
|
||||
mSystem = system;
|
||||
mPath = path;
|
||||
mName = name;
|
||||
//first created folder data initializes the list
|
||||
if (sortStateNameMap.empty()) {
|
||||
sortStateNameMap[compareFileName] = "file name";
|
||||
sortStateNameMap[compareRating] = "rating";
|
||||
sortStateNameMap[compareUserRating] = "user rating";
|
||||
sortStateNameMap[compareTimesPlayed] = "times played";
|
||||
sortStateNameMap[compareLastPlayed] = "last time played";
|
||||
}
|
||||
}
|
||||
|
||||
FolderData::~FolderData()
|
||||
|
@ -31,8 +41,24 @@ void FolderData::pushFileData(FileData* file)
|
|||
mFileVector.push_back(file);
|
||||
}
|
||||
|
||||
//sort this folder and any subfolders
|
||||
void FolderData::sort(ComparisonFunction & comparisonFunction, bool ascending)
|
||||
{
|
||||
std::sort(mFileVector.begin(), mFileVector.end(), comparisonFunction);
|
||||
|
||||
for(unsigned int i = 0; i < mFileVector.size(); i++)
|
||||
{
|
||||
if(mFileVector.at(i)->isFolder())
|
||||
((FolderData*)mFileVector.at(i))->sort(comparisonFunction, ascending);
|
||||
}
|
||||
|
||||
if (!ascending) {
|
||||
std::reverse(mFileVector.begin(), mFileVector.end());
|
||||
}
|
||||
}
|
||||
|
||||
//returns if file1 should come before file2
|
||||
bool filesort(FileData* file1, FileData* file2)
|
||||
bool FolderData::compareFileName(const FileData* file1, const FileData* file2)
|
||||
{
|
||||
std::string name1 = file1->getName();
|
||||
std::string name2 = file2->getName();
|
||||
|
@ -43,29 +69,118 @@ bool filesort(FileData* file1, FileData* file2)
|
|||
{
|
||||
if(toupper(name1[i]) != toupper(name2[i]))
|
||||
{
|
||||
if(toupper(name1[i]) < toupper(name2[i]))
|
||||
{
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
return toupper(name1[i]) < toupper(name2[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if(name1.length() < name2.length())
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
return name1.length() < name2.length();
|
||||
}
|
||||
|
||||
//sort this folder and any subfolders
|
||||
void FolderData::sort()
|
||||
bool FolderData::compareRating(const FileData* file1, const FileData* file2)
|
||||
{
|
||||
std::sort(mFileVector.begin(), mFileVector.end(), filesort);
|
||||
|
||||
for(unsigned int i = 0; i < mFileVector.size(); i++)
|
||||
{
|
||||
if(mFileVector.at(i)->isFolder())
|
||||
((FolderData*)mFileVector.at(i))->sort();
|
||||
//we need game data. try to cast
|
||||
const GameData * game1 = dynamic_cast<const GameData*>(file1);
|
||||
const GameData * game2 = dynamic_cast<const GameData*>(file2);
|
||||
if (game1 != nullptr && game2 != nullptr) {
|
||||
return game1->getRating() < game2->getRating();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FolderData::compareUserRating(const FileData* file1, const FileData* file2)
|
||||
{
|
||||
//we need game data. try to cast
|
||||
const GameData * game1 = dynamic_cast<const GameData*>(file1);
|
||||
const GameData * game2 = dynamic_cast<const GameData*>(file2);
|
||||
if (game1 != nullptr && game2 != nullptr) {
|
||||
return game1->getUserRating() < game2->getUserRating();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FolderData::compareTimesPlayed(const FileData* file1, const FileData* file2)
|
||||
{
|
||||
//we need game data. try to cast
|
||||
const GameData * game1 = dynamic_cast<const GameData*>(file1);
|
||||
const GameData * game2 = dynamic_cast<const GameData*>(file2);
|
||||
if (game1 != nullptr && game2 != nullptr) {
|
||||
return game1->getTimesPlayed() < game2->getTimesPlayed();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FolderData::compareLastPlayed(const FileData* file1, const FileData* file2)
|
||||
{
|
||||
//we need game data. try to cast
|
||||
const GameData * game1 = dynamic_cast<const GameData*>(file1);
|
||||
const GameData * game2 = dynamic_cast<const GameData*>(file2);
|
||||
if (game1 != nullptr && game2 != nullptr) {
|
||||
return game1->getLastPlayed() < game2->getLastPlayed();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string FolderData::getSortStateName(ComparisonFunction & comparisonFunction, bool ascending)
|
||||
{
|
||||
std::string temp = sortStateNameMap[comparisonFunction];
|
||||
if (ascending) {
|
||||
temp.append(" (ascending)");
|
||||
}
|
||||
else {
|
||||
temp.append(" (descending)");
|
||||
}
|
||||
return temp;
|
||||
}
|
||||
|
||||
FileData* FolderData::getFile(unsigned int i) const
|
||||
{
|
||||
return mFileVector.at(i);
|
||||
}
|
||||
|
||||
std::vector<FileData*> FolderData::getFiles(bool onlyFiles) const
|
||||
{
|
||||
std::vector<FileData*> temp;
|
||||
//now check if a child is a folder and get those children in turn
|
||||
std::vector<FileData*>::const_iterator fdit = mFileVector.cbegin();
|
||||
while(fdit != mFileVector.cend()) {
|
||||
//dynamically try to cast to FolderData type
|
||||
FolderData * folder = dynamic_cast<FolderData*>(*fdit);
|
||||
if (folder != nullptr) {
|
||||
//add this only when user wanted it
|
||||
if (!onlyFiles) {
|
||||
temp.push_back(*fdit);
|
||||
}
|
||||
}
|
||||
else {
|
||||
temp.push_back(*fdit);
|
||||
}
|
||||
++fdit;
|
||||
}
|
||||
return temp;
|
||||
}
|
||||
|
||||
std::vector<FileData*> FolderData::getFilesRecursive(bool onlyFiles) const
|
||||
{
|
||||
std::vector<FileData*> temp;
|
||||
//now check if a child is a folder and get those children in turn
|
||||
std::vector<FileData*>::const_iterator fdit = mFileVector.cbegin();
|
||||
while(fdit != mFileVector.cend()) {
|
||||
//dynamically try to cast to FolderData type
|
||||
FolderData * folder = dynamic_cast<FolderData*>(*fdit);
|
||||
if (folder != nullptr) {
|
||||
//add this onyl when user wanted it
|
||||
if (!onlyFiles) {
|
||||
temp.push_back(*fdit);
|
||||
}
|
||||
//worked. Is actual folder data. recurse
|
||||
std::vector<FileData*> children = folder->getFilesRecursive(onlyFiles);
|
||||
//insert children into return vector
|
||||
temp.insert(temp.end(), children.cbegin(), children.cend());
|
||||
}
|
||||
else {
|
||||
temp.push_back(*fdit);
|
||||
}
|
||||
++fdit;
|
||||
}
|
||||
return temp;
|
||||
}
|
|
@ -1,28 +1,54 @@
|
|||
#ifndef _FOLDER_H_
|
||||
#define _FOLDER_H_
|
||||
|
||||
#include "FileData.h"
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
#include "FileData.h"
|
||||
|
||||
|
||||
class SystemData;
|
||||
|
||||
//This class lets us hold a vector of FileDatas within under a common name.
|
||||
class FolderData : public FileData
|
||||
{
|
||||
public:
|
||||
typedef bool ComparisonFunction(const FileData* a, const FileData* b);
|
||||
struct SortState
|
||||
{
|
||||
ComparisonFunction & comparisonFunction;
|
||||
bool ascending;
|
||||
std::string description;
|
||||
|
||||
SortState(ComparisonFunction & sortFunction, bool sortAscending, const std::string & sortDescription) : comparisonFunction(sortFunction), ascending(sortAscending), description(sortDescription) {}
|
||||
};
|
||||
|
||||
private:
|
||||
static std::map<ComparisonFunction*, std::string> sortStateNameMap;
|
||||
|
||||
public:
|
||||
FolderData(SystemData* system, std::string path, std::string name);
|
||||
~FolderData();
|
||||
|
||||
bool isFolder();
|
||||
std::string getName();
|
||||
std::string getPath();
|
||||
bool isFolder() const;
|
||||
const std::string & getName() const;
|
||||
const std::string & getPath() const;
|
||||
|
||||
unsigned int getFileCount();
|
||||
FileData* getFile(unsigned int i);
|
||||
FileData* getFile(unsigned int i) const;
|
||||
std::vector<FileData*> getFiles(bool onlyFiles = false) const;
|
||||
std::vector<FileData*> getFilesRecursive(bool onlyFiles = false) const;
|
||||
|
||||
void pushFileData(FileData* file);
|
||||
|
||||
void sort();
|
||||
void sort(ComparisonFunction & comparisonFunction = compareFileName, bool ascending = true);
|
||||
static bool compareFileName(const FileData* file1, const FileData* file2);
|
||||
static bool compareRating(const FileData* file1, const FileData* file2);
|
||||
static bool compareUserRating(const FileData* file1, const FileData* file2);
|
||||
static bool compareTimesPlayed(const FileData* file1, const FileData* file2);
|
||||
static bool compareLastPlayed(const FileData* file1, const FileData* file2);
|
||||
static std::string getSortStateName(ComparisonFunction & comparisonFunction = compareFileName, bool ascending = true);
|
||||
|
||||
private:
|
||||
SystemData* mSystem;
|
||||
std::string mPath;
|
||||
|
|
145
src/Font.cpp
145
src/Font.cpp
|
@ -5,7 +5,6 @@
|
|||
#include "Renderer.h"
|
||||
#include <boost/filesystem.hpp>
|
||||
#include "Log.h"
|
||||
#include "Vector2.h"
|
||||
|
||||
FT_Library Font::sLibrary;
|
||||
bool Font::libraryInitialized = false;
|
||||
|
@ -220,13 +219,14 @@ Font::~Font()
|
|||
}
|
||||
|
||||
|
||||
struct Vertex
|
||||
{
|
||||
Vector2<GLfloat> pos;
|
||||
Vector2<GLfloat> tex;
|
||||
};
|
||||
|
||||
void Font::drawText(std::string text, int startx, int starty, int color)
|
||||
{
|
||||
TextCache* cache = buildTextCache(text, startx, starty, color);
|
||||
renderTextCache(cache);
|
||||
delete cache;
|
||||
}
|
||||
|
||||
void Font::renderTextCache(TextCache* cache)
|
||||
{
|
||||
if(!textureID)
|
||||
{
|
||||
|
@ -234,24 +234,92 @@ void Font::drawText(std::string text, int startx, int starty, int color)
|
|||
return;
|
||||
}
|
||||
|
||||
const int triCount = text.length() * 2;
|
||||
Vertex* vert = new Vertex[triCount * 3];
|
||||
GLubyte* colors = new GLubyte[triCount * 3 * 4];
|
||||
if(cache == NULL)
|
||||
{
|
||||
LOG(LogError) << "Attempted to draw NULL TextCache!";
|
||||
return;
|
||||
}
|
||||
|
||||
if(cache->sourceFont != this)
|
||||
{
|
||||
LOG(LogError) << "Attempted to draw TextCache with font other than its source!";
|
||||
return;
|
||||
}
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, textureID);
|
||||
glEnable(GL_TEXTURE_2D);
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
glEnableClientState(GL_VERTEX_ARRAY);
|
||||
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
|
||||
glEnableClientState(GL_COLOR_ARRAY);
|
||||
|
||||
glVertexPointer(2, GL_FLOAT, sizeof(TextCache::Vertex), &cache->verts[0].pos);
|
||||
glTexCoordPointer(2, GL_FLOAT, sizeof(TextCache::Vertex), &cache->verts[0].tex);
|
||||
glColorPointer(4, GL_UNSIGNED_BYTE, 0, cache->colors);
|
||||
|
||||
glDrawArrays(GL_TRIANGLES, 0, cache->vertCount);
|
||||
|
||||
glDisableClientState(GL_VERTEX_ARRAY);
|
||||
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
|
||||
glDisableClientState(GL_COLOR_ARRAY);
|
||||
|
||||
glDisable(GL_TEXTURE_2D);
|
||||
glDisable(GL_BLEND);
|
||||
}
|
||||
|
||||
void Font::sizeText(std::string text, int* w, int* h)
|
||||
{
|
||||
float cwidth = 0.0f;
|
||||
for(unsigned int i = 0; i < text.length(); i++)
|
||||
{
|
||||
unsigned char letter = text[i];
|
||||
if(letter < 32 || letter >= 128)
|
||||
letter = 127;
|
||||
|
||||
cwidth += charData[letter].advX * fontScale;
|
||||
}
|
||||
|
||||
if(w != NULL)
|
||||
*w = (int)cwidth;
|
||||
|
||||
if(h != NULL)
|
||||
*h = getHeight();
|
||||
}
|
||||
|
||||
int Font::getHeight()
|
||||
{
|
||||
return (int)(mMaxGlyphHeight * 1.5f * fontScale);
|
||||
}
|
||||
|
||||
|
||||
//=============================================================================================================
|
||||
//TextCache
|
||||
//=============================================================================================================
|
||||
|
||||
TextCache* Font::buildTextCache(const std::string& text, int offsetX, int offsetY, unsigned int color)
|
||||
{
|
||||
if(!textureID)
|
||||
{
|
||||
LOG(LogError) << "Error - tried to build TextCache with Font that has no texture loaded!";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const int triCount = text.length() * 2;
|
||||
const int vertCount = triCount * 3;
|
||||
TextCache::Vertex* vert = new TextCache::Vertex[vertCount];
|
||||
GLubyte* colors = new GLubyte[vertCount * 4];
|
||||
|
||||
//texture atlas width/height
|
||||
float tw = (float)textureWidth;
|
||||
float th = (float)textureHeight;
|
||||
|
||||
float x = (float)startx;
|
||||
float y = starty + mMaxGlyphHeight * 1.1f * fontScale; //padding (another 0.5% is added to the bottom through the sizeText function)
|
||||
float x = (float)offsetX;
|
||||
float y = offsetY + mMaxGlyphHeight * 1.1f * fontScale; //padding (another 0.5% is added to the bottom through the sizeText function)
|
||||
|
||||
int charNum = 0;
|
||||
for(int i = 0; i < triCount * 3; i += 6, charNum++)
|
||||
for(int i = 0; i < vertCount; i += 6, charNum++)
|
||||
{
|
||||
unsigned char letter = text[charNum];
|
||||
|
||||
|
@ -286,49 +354,24 @@ void Font::drawText(std::string text, int startx, int starty, int color)
|
|||
x += charData[letter].advX * fontScale;
|
||||
}
|
||||
|
||||
Renderer::buildGLColorArray(colors, color, triCount * 3);
|
||||
TextCache* cache = new TextCache(vertCount, vert, colors, this);
|
||||
if(color != 0x00000000)
|
||||
cache->setColor(color);
|
||||
|
||||
glEnableClientState(GL_VERTEX_ARRAY);
|
||||
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
|
||||
glEnableClientState(GL_COLOR_ARRAY);
|
||||
return cache;
|
||||
}
|
||||
|
||||
glVertexPointer(2, GL_FLOAT, sizeof(Vertex), &vert[0].pos);
|
||||
glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &vert[0].tex);
|
||||
glColorPointer(4, GL_UNSIGNED_BYTE, 0, colors);
|
||||
TextCache::TextCache(int verts, Vertex* v, GLubyte* c, Font* f) : vertCount(verts), verts(v), colors(c), sourceFont(f)
|
||||
{
|
||||
}
|
||||
|
||||
glDrawArrays(GL_TRIANGLES, 0, triCount * 3);
|
||||
|
||||
glDisableClientState(GL_VERTEX_ARRAY);
|
||||
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
|
||||
glDisableClientState(GL_COLOR_ARRAY);
|
||||
|
||||
glDisable(GL_TEXTURE_2D);
|
||||
glDisable(GL_BLEND);
|
||||
|
||||
delete[] vert;
|
||||
TextCache::~TextCache()
|
||||
{
|
||||
delete[] verts;
|
||||
delete[] colors;
|
||||
}
|
||||
|
||||
void Font::sizeText(std::string text, int* w, int* h)
|
||||
void TextCache::setColor(unsigned int color)
|
||||
{
|
||||
float cwidth = 0.0f;
|
||||
for(unsigned int i = 0; i < text.length(); i++)
|
||||
{
|
||||
unsigned char letter = text[i];
|
||||
if(letter < 32 || letter >= 128)
|
||||
letter = 127;
|
||||
|
||||
cwidth += charData[letter].advX * fontScale;
|
||||
}
|
||||
|
||||
if(w != NULL)
|
||||
*w = (int)cwidth;
|
||||
|
||||
if(h != NULL)
|
||||
*h = getHeight();
|
||||
}
|
||||
|
||||
int Font::getHeight()
|
||||
{
|
||||
return (int)(mMaxGlyphHeight * 1.5f * fontScale);
|
||||
Renderer::buildGLColorArray(const_cast<GLubyte*>(colors), color, vertCount);
|
||||
}
|
||||
|
|
31
src/Font.h
31
src/Font.h
|
@ -6,6 +6,9 @@
|
|||
#include GLHEADER
|
||||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H
|
||||
#include "Vector2.h"
|
||||
|
||||
class TextCache;
|
||||
|
||||
//A TrueType Font renderer that uses FreeType and OpenGL.
|
||||
//The library is automatically initialized when it's needed.
|
||||
|
@ -37,8 +40,12 @@ public:
|
|||
|
||||
GLuint textureID;
|
||||
|
||||
void drawText(std::string text, int startx, int starty, int color); //Render some text using this font.
|
||||
void sizeText(std::string text, int* w, int* h); //Sets the width and height of a given string to given pointers. Skipped if pointer is NULL.
|
||||
TextCache* buildTextCache(const std::string& text, int offsetX, int offsetY, unsigned int color);
|
||||
void renderTextCache(TextCache* cache);
|
||||
|
||||
//Create a TextCache, render with it, then delete it. Best used for short text or text that changes frequently.
|
||||
void drawText(std::string text, int startx, int starty, int color);
|
||||
void sizeText(std::string text, int* w, int* h); //Sets the width and height of a given string to supplied pointers. A dimension is skipped if its pointer is NULL.
|
||||
int getHeight();
|
||||
|
||||
void init();
|
||||
|
@ -65,4 +72,24 @@ private:
|
|||
int mSize;
|
||||
};
|
||||
|
||||
class TextCache
|
||||
{
|
||||
public:
|
||||
struct Vertex
|
||||
{
|
||||
Vector2<GLfloat> pos;
|
||||
Vector2<GLfloat> tex;
|
||||
};
|
||||
|
||||
void setColor(unsigned int color);
|
||||
|
||||
TextCache(int verts, Vertex* v, GLubyte* c, Font* f);
|
||||
~TextCache();
|
||||
|
||||
const int vertCount;
|
||||
const Vertex* verts;
|
||||
const GLubyte* colors;
|
||||
const Font* sourceFont;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
123
src/GameData.cpp
123
src/GameData.cpp
|
@ -2,23 +2,110 @@
|
|||
#include <boost/filesystem.hpp>
|
||||
#include <iostream>
|
||||
|
||||
bool GameData::isFolder() { return false; }
|
||||
std::string GameData::getName() { return mName; }
|
||||
std::string GameData::getPath() { return mPath; }
|
||||
std::string GameData::getDescription() { return mDescription; }
|
||||
std::string GameData::getImagePath() { return mImagePath; }
|
||||
|
||||
const std::string GameData::xmlTagGameList = "gameList";
|
||||
const std::string GameData::xmlTagGame = "game";
|
||||
const std::string GameData::xmlTagName = "name";
|
||||
const std::string GameData::xmlTagPath = "path";
|
||||
const std::string GameData::xmlTagDescription = "desc";
|
||||
const std::string GameData::xmlTagImagePath = "image";
|
||||
const std::string GameData::xmlTagRating = "rating";
|
||||
const std::string GameData::xmlTagUserRating = "userrating";
|
||||
const std::string GameData::xmlTagTimesPlayed = "timesplayed";
|
||||
const std::string GameData::xmlTagLastPlayed = "lastplayed";
|
||||
|
||||
|
||||
GameData::GameData(SystemData* system, std::string path, std::string name)
|
||||
: mSystem(system), mPath(path), mName(name), mRating(0.0f), mUserRating(0.0f), mTimesPlayed(0), mLastPlayed(0)
|
||||
{
|
||||
mSystem = system;
|
||||
mPath = path;
|
||||
mName = name;
|
||||
|
||||
mDescription = "";
|
||||
mImagePath = "";
|
||||
}
|
||||
|
||||
std::string GameData::getBashPath()
|
||||
bool GameData::isFolder() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string & GameData::getName() const
|
||||
{
|
||||
return mName;
|
||||
}
|
||||
|
||||
void GameData::setName(const std::string & name)
|
||||
{
|
||||
mName = name;
|
||||
}
|
||||
|
||||
const std::string & GameData::getPath() const
|
||||
{
|
||||
return mPath;
|
||||
}
|
||||
|
||||
void GameData::setPath(const std::string & path)
|
||||
{
|
||||
mPath = path;
|
||||
}
|
||||
|
||||
const std::string & GameData::getDescription() const
|
||||
{
|
||||
return mDescription;
|
||||
}
|
||||
|
||||
void GameData::setDescription(const std::string & description)
|
||||
{
|
||||
mDescription = description;
|
||||
}
|
||||
|
||||
const std::string & GameData::getImagePath() const
|
||||
{
|
||||
return mImagePath;
|
||||
}
|
||||
|
||||
void GameData::setImagePath(const std::string & imagePath)
|
||||
{
|
||||
mImagePath = imagePath;
|
||||
}
|
||||
|
||||
float GameData::getRating() const
|
||||
{
|
||||
return mRating;
|
||||
}
|
||||
|
||||
void GameData::setRating(float rating)
|
||||
{
|
||||
mRating = rating;
|
||||
}
|
||||
|
||||
float GameData::getUserRating() const
|
||||
{
|
||||
return mUserRating;
|
||||
}
|
||||
|
||||
void GameData::setUserRating(float rating)
|
||||
{
|
||||
mUserRating = rating;
|
||||
}
|
||||
|
||||
size_t GameData::getTimesPlayed() const
|
||||
{
|
||||
return mTimesPlayed;
|
||||
}
|
||||
|
||||
void GameData::setTimesPlayed(size_t timesPlayed)
|
||||
{
|
||||
mTimesPlayed = timesPlayed;
|
||||
}
|
||||
|
||||
std::time_t GameData::getLastPlayed() const
|
||||
{
|
||||
return mLastPlayed;
|
||||
}
|
||||
|
||||
void GameData::setLastPlayed(std::time_t lastPlayed)
|
||||
{
|
||||
mLastPlayed = lastPlayed;
|
||||
}
|
||||
|
||||
std::string GameData::getBashPath() const
|
||||
{
|
||||
//a quick and dirty way to insert a backslash before most characters that would mess up a bash path
|
||||
std::string path = mPath;
|
||||
|
@ -44,18 +131,8 @@ std::string GameData::getBashPath()
|
|||
}
|
||||
|
||||
//returns the boost::filesystem stem of our path - e.g. for "/foo/bar.rom" returns "bar"
|
||||
std::string GameData::getBaseName()
|
||||
std::string GameData::getBaseName() const
|
||||
{
|
||||
boost::filesystem::path path(mPath);
|
||||
return path.stem().string();
|
||||
}
|
||||
|
||||
void GameData::set(std::string name, std::string description, std::string imagePath)
|
||||
{
|
||||
if(!name.empty())
|
||||
mName = name;
|
||||
if(!description.empty())
|
||||
mDescription = description;
|
||||
if(!imagePath.empty() && boost::filesystem::exists(imagePath))
|
||||
mImagePath = imagePath;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
#define _GAMEDATA_H_
|
||||
|
||||
#include <string>
|
||||
#include <ctime>
|
||||
|
||||
#include "FileData.h"
|
||||
#include "SystemData.h"
|
||||
|
||||
|
@ -9,19 +11,49 @@
|
|||
class GameData : public FileData
|
||||
{
|
||||
public:
|
||||
//static tag names for reading/writing XML documents. This might fail in PUGIXML_WCHAR_MODE
|
||||
//TODO: The class should have member to read fromXML() and write toXML() probably...
|
||||
static const std::string xmlTagGameList;
|
||||
static const std::string xmlTagGame;
|
||||
static const std::string xmlTagName;
|
||||
static const std::string xmlTagPath;
|
||||
static const std::string xmlTagDescription;
|
||||
static const std::string xmlTagImagePath;
|
||||
static const std::string xmlTagRating;
|
||||
static const std::string xmlTagUserRating;
|
||||
static const std::string xmlTagTimesPlayed;
|
||||
static const std::string xmlTagLastPlayed;
|
||||
|
||||
GameData(SystemData* system, std::string path, std::string name);
|
||||
|
||||
void set(std::string name = "", std::string description = "", std::string imagePath = "");
|
||||
const std::string & getName() const;
|
||||
void setName(const std::string & name);
|
||||
|
||||
std::string getName();
|
||||
std::string getPath();
|
||||
std::string getBashPath();
|
||||
std::string getBaseName();
|
||||
const std::string & getPath() const;
|
||||
void setPath(const std::string & path);
|
||||
|
||||
std::string getDescription();
|
||||
std::string getImagePath();
|
||||
const std::string & getDescription() const;
|
||||
void setDescription(const std::string & description);
|
||||
|
||||
bool isFolder();
|
||||
const std::string & getImagePath() const;
|
||||
void setImagePath(const std::string & imagePath);
|
||||
|
||||
float getRating() const;
|
||||
void setRating(float rating);
|
||||
|
||||
float getUserRating() const;
|
||||
void setUserRating(float rating);
|
||||
|
||||
size_t getTimesPlayed() const;
|
||||
void setTimesPlayed(size_t timesPlayed);
|
||||
|
||||
std::time_t getLastPlayed() const;
|
||||
void setLastPlayed(std::time_t lastPlayed);
|
||||
|
||||
std::string getBashPath() const;
|
||||
std::string getBaseName() const;
|
||||
|
||||
bool isFolder() const;
|
||||
private:
|
||||
SystemData* mSystem;
|
||||
std::string mPath;
|
||||
|
@ -30,6 +62,10 @@ private:
|
|||
//extra data
|
||||
std::string mDescription;
|
||||
std::string mImagePath;
|
||||
float mRating;
|
||||
float mUserRating;
|
||||
size_t mTimesPlayed;
|
||||
std::time_t mLastPlayed;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
@ -102,6 +102,19 @@ Vector2u GuiComponent::getSize()
|
|||
return mSize;
|
||||
}
|
||||
|
||||
void GuiComponent::setSize(Vector2u size)
|
||||
{
|
||||
mSize = size;
|
||||
onSizeChanged();
|
||||
}
|
||||
|
||||
void GuiComponent::setSize(unsigned int w, unsigned int h)
|
||||
{
|
||||
mSize.x = w;
|
||||
mSize.y = h;
|
||||
onSizeChanged();
|
||||
}
|
||||
|
||||
//Children stuff.
|
||||
void GuiComponent::addChild(GuiComponent* cmp)
|
||||
{
|
||||
|
@ -156,3 +169,14 @@ GuiComponent* GuiComponent::getParent()
|
|||
{
|
||||
return mParent;
|
||||
}
|
||||
|
||||
|
||||
unsigned char GuiComponent::getOpacity()
|
||||
{
|
||||
return mOpacity;
|
||||
}
|
||||
|
||||
void GuiComponent::setOpacity(unsigned char opacity)
|
||||
{
|
||||
mOpacity = opacity;
|
||||
}
|
||||
|
|
|
@ -36,6 +36,9 @@ public:
|
|||
virtual void onOffsetChanged() {};
|
||||
|
||||
Vector2u getSize();
|
||||
void setSize(Vector2u size);
|
||||
void setSize(unsigned int w, unsigned int h);
|
||||
virtual void onSizeChanged() {};
|
||||
|
||||
void setParent(GuiComponent* parent);
|
||||
GuiComponent* getParent();
|
||||
|
@ -45,11 +48,14 @@ public:
|
|||
void clearChildren();
|
||||
unsigned int getChildCount();
|
||||
GuiComponent* getChild(unsigned int i);
|
||||
unsigned char getOpacity();
|
||||
void setOpacity(unsigned char opacity);
|
||||
|
||||
protected:
|
||||
//Default implementation just renders children - you should probably always call GuiComponent::onRender at some point in your custom onRender.
|
||||
virtual void onRender();
|
||||
|
||||
unsigned char mOpacity;
|
||||
Window* mWindow;
|
||||
GuiComponent* mParent;
|
||||
Vector2i mOffset;
|
||||
|
|
|
@ -423,6 +423,9 @@ void InputManager::loadDefaultConfig()
|
|||
|
||||
cfg->mapInput("mastervolup", Input(DEVICE_KEYBOARD, TYPE_KEY, SDLK_PLUS, 1, true));
|
||||
cfg->mapInput("mastervoldown", Input(DEVICE_KEYBOARD, TYPE_KEY, SDLK_MINUS, 1, true));
|
||||
|
||||
cfg->mapInput("sortordernext", Input(DEVICE_KEYBOARD, TYPE_KEY, SDLK_F7, 1, true));
|
||||
cfg->mapInput("sortorderprevious", Input(DEVICE_KEYBOARD, TYPE_KEY, SDLK_F8, 1, true));
|
||||
}
|
||||
|
||||
void InputManager::writeConfig()
|
||||
|
|
|
@ -43,6 +43,7 @@ namespace Renderer
|
|||
void drawText(std::string text, int x, int y, unsigned int color, Font* font);
|
||||
void drawCenteredText(std::string text, int xOffset, int y, unsigned int color, Font* font);
|
||||
void drawWrappedText(std::string text, int xStart, int yStart, int xLen, unsigned int color, Font* font);
|
||||
void sizeWrappedText(std::string text, int xLen, Font* font, int* xOut, int* yOut);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -252,4 +252,63 @@ namespace Renderer {
|
|||
}
|
||||
}
|
||||
|
||||
void sizeWrappedText(std::string text, int xLen, Font* font, int* xOut, int* yOut)
|
||||
{
|
||||
if(xOut != NULL)
|
||||
*xOut = xLen;
|
||||
|
||||
int y = 0;
|
||||
|
||||
std::string line, word, temp;
|
||||
int w, h;
|
||||
size_t space, newline;
|
||||
|
||||
while(text.length() > 0 || !line.empty()) //while there's text or we still have text to render
|
||||
{
|
||||
space = text.find(' ', 0);
|
||||
if(space == std::string::npos)
|
||||
space = text.length() - 1;
|
||||
|
||||
word = text.substr(0, space + 1);
|
||||
|
||||
//check if the next word contains a newline
|
||||
newline = word.find('\n', 0);
|
||||
if(newline != std::string::npos)
|
||||
{
|
||||
word = word.substr(0, newline);
|
||||
text.erase(0, newline + 1);
|
||||
}else{
|
||||
text.erase(0, space + 1);
|
||||
}
|
||||
|
||||
temp = line + word;
|
||||
|
||||
font->sizeText(temp, &w, &h);
|
||||
|
||||
//if we're on the last word and it'll fit on the line, just add it to the line
|
||||
if((w <= xLen && text.length() == 0) || newline != std::string::npos)
|
||||
{
|
||||
line = temp;
|
||||
word = "";
|
||||
}
|
||||
|
||||
//if the next line will be too long or we're on the last of the text, render it
|
||||
if(w > xLen || text.length() == 0 || newline != std::string::npos)
|
||||
{
|
||||
//increment y by height and some extra padding for the next line
|
||||
y += h + 4;
|
||||
|
||||
//move the word we skipped to the next line
|
||||
line = word;
|
||||
}else{
|
||||
//there's still space, continue building the line
|
||||
line = temp;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if(yOut != NULL)
|
||||
*yOut = y;
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -33,6 +33,8 @@ void Settings::setDefaults()
|
|||
mBoolMap["WINDOWED"] = false;
|
||||
|
||||
mIntMap["DIMTIME"] = 30*1000;
|
||||
|
||||
mIntMap["GameListSortIndex"] = 0;
|
||||
}
|
||||
|
||||
template <typename K, typename V>
|
||||
|
|
|
@ -50,6 +50,10 @@ SystemData::SystemData(std::string name, std::string descName, std::string start
|
|||
|
||||
SystemData::~SystemData()
|
||||
{
|
||||
//save changed game data back to xml
|
||||
if(!Settings::getInstance()->getBool("IGNOREGAMELIST")) {
|
||||
updateGamelist(this);
|
||||
}
|
||||
delete mRootFolder;
|
||||
}
|
||||
|
||||
|
@ -90,6 +94,10 @@ void SystemData::launchGame(Window* window, GameData* game)
|
|||
window->init();
|
||||
VolumeControl::getInstance()->init();
|
||||
AudioManager::getInstance()->init();
|
||||
|
||||
//update number of times the game has been launched and the time
|
||||
game->setTimesPlayed(game->getTimesPlayed() + 1);
|
||||
game->setLastPlayed(std::time(nullptr));
|
||||
}
|
||||
|
||||
void SystemData::populateFolder(FolderData* folder)
|
||||
|
@ -137,7 +145,7 @@ void SystemData::populateFolder(FolderData* folder)
|
|||
//if it matches, add it
|
||||
if(chkExt == extension)
|
||||
{
|
||||
GameData* newGame = new GameData(this, filePath.string(), filePath.stem().string());
|
||||
GameData* newGame = new GameData(this, filePath.generic_string(), filePath.stem().string());
|
||||
folder->pushFileData(newGame);
|
||||
isGame = true;
|
||||
break;
|
||||
|
|
|
@ -93,6 +93,7 @@ bool operator !=(const Vector2<T>& left, const Vector2<T>& right)
|
|||
typedef Vector2<int> Vector2i;
|
||||
typedef Vector2<unsigned int> Vector2u;
|
||||
typedef Vector2<float> Vector2f;
|
||||
typedef Vector2<double> Vector2d;
|
||||
|
||||
class Rect
|
||||
{
|
||||
|
|
|
@ -117,19 +117,19 @@ void parseGamelist(SystemData* system)
|
|||
return;
|
||||
}
|
||||
|
||||
pugi::xml_node root = doc.child("gameList");
|
||||
pugi::xml_node root = doc.child(GameData::xmlTagGameList.c_str());
|
||||
if(!root)
|
||||
{
|
||||
LOG(LogError) << "Could not find <gameList> node in gamelist \"" << xmlpath << "\"!";
|
||||
LOG(LogError) << "Could not find <" << GameData::xmlTagGameList << "> node in gamelist \"" << xmlpath << "\"!";
|
||||
return;
|
||||
}
|
||||
|
||||
for(pugi::xml_node gameNode = root.child("game"); gameNode; gameNode = gameNode.next_sibling("game"))
|
||||
for(pugi::xml_node gameNode = root.child(GameData::xmlTagGame.c_str()); gameNode; gameNode = gameNode.next_sibling(GameData::xmlTagGame.c_str()))
|
||||
{
|
||||
pugi::xml_node pathNode = gameNode.child("path");
|
||||
pugi::xml_node pathNode = gameNode.child(GameData::xmlTagPath.c_str());
|
||||
if(!pathNode)
|
||||
{
|
||||
LOG(LogError) << "<game> node contains no <path> child!";
|
||||
LOG(LogError) << "<" << GameData::xmlTagGame << "> node contains no <" << GameData::xmlTagPath << "> child!";
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -152,13 +152,17 @@ void parseGamelist(SystemData* system)
|
|||
//actually gather the information in the XML doc, then pass it to the game's set method
|
||||
std::string newName, newDesc, newImage;
|
||||
|
||||
if(gameNode.child("name"))
|
||||
newName = gameNode.child("name").text().get();
|
||||
if(gameNode.child("desc"))
|
||||
newDesc = gameNode.child("desc").text().get();
|
||||
if(gameNode.child("image"))
|
||||
if(gameNode.child(GameData::xmlTagName.c_str()))
|
||||
{
|
||||
newImage = gameNode.child("image").text().get();
|
||||
game->setName(gameNode.child(GameData::xmlTagName.c_str()).text().get());
|
||||
}
|
||||
if(gameNode.child(GameData::xmlTagDescription.c_str()))
|
||||
{
|
||||
game->setDescription(gameNode.child(GameData::xmlTagDescription.c_str()).text().get());
|
||||
}
|
||||
if(gameNode.child(GameData::xmlTagImagePath.c_str()))
|
||||
{
|
||||
newImage = gameNode.child(GameData::xmlTagImagePath.c_str()).text().get();
|
||||
|
||||
//expand "."
|
||||
if(newImage[0] == '.')
|
||||
|
@ -168,15 +172,148 @@ void parseGamelist(SystemData* system)
|
|||
newImage.insert(0, pathname.parent_path().string() );
|
||||
}
|
||||
|
||||
//if the image doesn't exist, forget it
|
||||
if(!boost::filesystem::exists(newImage))
|
||||
newImage = "";
|
||||
//if the image exist, set it
|
||||
if(boost::filesystem::exists(newImage))
|
||||
{
|
||||
game->setImagePath(newImage);
|
||||
}
|
||||
}
|
||||
|
||||
game->set(newName, newDesc, newImage);
|
||||
|
||||
}else{
|
||||
//get rating and the times played from the XML doc
|
||||
if(gameNode.child(GameData::xmlTagRating.c_str()))
|
||||
{
|
||||
float rating;
|
||||
std::istringstream(gameNode.child(GameData::xmlTagRating.c_str()).text().get()) >> rating;
|
||||
game->setRating(rating);
|
||||
}
|
||||
if(gameNode.child(GameData::xmlTagUserRating.c_str()))
|
||||
{
|
||||
float userRating;
|
||||
std::istringstream(gameNode.child(GameData::xmlTagUserRating.c_str()).text().get()) >> userRating;
|
||||
game->setUserRating(userRating);
|
||||
}
|
||||
if(gameNode.child(GameData::xmlTagTimesPlayed.c_str()))
|
||||
{
|
||||
size_t timesPlayed;
|
||||
std::istringstream(gameNode.child(GameData::xmlTagTimesPlayed.c_str()).text().get()) >> timesPlayed;
|
||||
game->setTimesPlayed(timesPlayed);
|
||||
}
|
||||
if(gameNode.child(GameData::xmlTagLastPlayed.c_str()))
|
||||
{
|
||||
std::time_t lastPlayed;
|
||||
std::istringstream(gameNode.child(GameData::xmlTagLastPlayed.c_str()).text().get()) >> lastPlayed;
|
||||
game->setLastPlayed(lastPlayed);
|
||||
}
|
||||
}
|
||||
else{
|
||||
LOG(LogWarning) << "Game at \"" << path << "\" does not exist!";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void addGameDataNode(pugi::xml_node & parent, const GameData * game)
|
||||
{
|
||||
//create game and add to parent node
|
||||
pugi::xml_node newGame = parent.append_child(GameData::xmlTagGame.c_str());
|
||||
//add values
|
||||
if (!game->getPath().empty()) {
|
||||
pugi::xml_node pathNode = newGame.append_child(GameData::xmlTagPath.c_str());
|
||||
pathNode.text().set(game->getPath().c_str());
|
||||
}
|
||||
if (!game->getName().empty()) {
|
||||
pugi::xml_node nameNode = newGame.append_child(GameData::xmlTagName.c_str());
|
||||
nameNode.text().set(game->getName().c_str());
|
||||
}
|
||||
if (!game->getDescription().empty()) {
|
||||
pugi::xml_node descriptionNode = newGame.append_child(GameData::xmlTagDescription.c_str());
|
||||
descriptionNode.text().set(game->getDescription().c_str());
|
||||
}
|
||||
if (!game->getImagePath().empty()) {
|
||||
pugi::xml_node imagePathNode = newGame.append_child(GameData::xmlTagImagePath.c_str());
|
||||
imagePathNode.text().set(game->getImagePath().c_str());
|
||||
}
|
||||
//all other values are added regardless of their value
|
||||
pugi::xml_node ratingNode = newGame.append_child(GameData::xmlTagRating.c_str());
|
||||
ratingNode.text().set(std::to_string((long double)game->getRating()).c_str());
|
||||
|
||||
pugi::xml_node userRatingNode = newGame.append_child(GameData::xmlTagUserRating.c_str());
|
||||
userRatingNode.text().set(std::to_string((long double)game->getUserRating()).c_str());
|
||||
|
||||
pugi::xml_node timesPlayedNode = newGame.append_child(GameData::xmlTagTimesPlayed.c_str());
|
||||
timesPlayedNode.text().set(std::to_string((unsigned long long)game->getTimesPlayed()).c_str());
|
||||
|
||||
pugi::xml_node lastPlayedNode = newGame.append_child(GameData::xmlTagLastPlayed.c_str());
|
||||
lastPlayedNode.text().set(std::to_string((unsigned long long)game->getLastPlayed()).c_str());
|
||||
}
|
||||
|
||||
void updateGamelist(SystemData* system)
|
||||
{
|
||||
//We do this by reading the XML again, adding changes and then writing it back,
|
||||
//because there might be information missing in our systemdata which would then miss in the new XML.
|
||||
//We have the complete information for every game though, so we can simply remove a game
|
||||
//we already have in the system from the XML, and then add it back from its GameData information...
|
||||
|
||||
std::string xmlpath = system->getGamelistPath();
|
||||
if(xmlpath.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG(LogInfo) << "Parsing XML file \"" << xmlpath << "\" before writing...";
|
||||
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result result = doc.load_file(xmlpath.c_str());
|
||||
|
||||
if(!result) {
|
||||
LOG(LogError) << "Error parsing XML file \"" << xmlpath << "\"!\n " << result.description();
|
||||
return;
|
||||
}
|
||||
|
||||
pugi::xml_node root = doc.child(GameData::xmlTagGameList.c_str());
|
||||
if(!root) {
|
||||
LOG(LogError) << "Could not find <" << GameData::xmlTagGameList << "> node in gamelist \"" << xmlpath << "\"!";
|
||||
return;
|
||||
}
|
||||
|
||||
//now we have all the information from the XML. now iterate through all our games and add information from there
|
||||
FolderData * rootFolder = system->getRootFolder();
|
||||
if (rootFolder != nullptr) {
|
||||
//get only files, no folders
|
||||
std::vector<FileData*> files = rootFolder->getFilesRecursive(true);
|
||||
//iterate through all files, checking if they're already in the XML
|
||||
std::vector<FileData*>::const_iterator fit = files.cbegin();
|
||||
while(fit != files.cend()) {
|
||||
//try to cast to gamedata
|
||||
const GameData * game = dynamic_cast<const GameData*>(*fit);
|
||||
if (game != nullptr) {
|
||||
//worked. check if this games' path can be found somewhere in the XML
|
||||
for(pugi::xml_node gameNode = root.child(GameData::xmlTagGame.c_str()); gameNode; gameNode = gameNode.next_sibling(GameData::xmlTagGame.c_str())) {
|
||||
//get path from game node
|
||||
pugi::xml_node pathNode = gameNode.child(GameData::xmlTagPath.c_str());
|
||||
if(!pathNode)
|
||||
{
|
||||
LOG(LogError) << "<" << GameData::xmlTagGame << "> node contains no <" << GameData::xmlTagPath << "> child!";
|
||||
continue;
|
||||
}
|
||||
//check path
|
||||
if (pathNode.text().get() == game->getPath()) {
|
||||
//found the game. remove it. it will be added again later with updated values
|
||||
root.remove_child(gameNode);
|
||||
//break node search loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
//either the game content was removed, because it needs to be updated,
|
||||
//or didn't exist in the first place, so just add it
|
||||
addGameDataNode(root, game);
|
||||
}
|
||||
++fit;
|
||||
}
|
||||
//now write the file
|
||||
if (!doc.save_file(xmlpath.c_str())) {
|
||||
LOG(LogError) << "Error saving XML file \"" << xmlpath << "\"!";
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOG(LogError) << "Found no root folder for system \"" << system->getName() << "\"!";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,4 +7,7 @@ class SystemData;
|
|||
//Loads gamelist.xml data into a SystemData.
|
||||
void parseGamelist(SystemData* system);
|
||||
|
||||
//Writes changes to SystemData back to a previously loaded gamelist.xml.
|
||||
void updateGamelist(SystemData* system);
|
||||
|
||||
#endif
|
||||
|
|
|
@ -76,7 +76,7 @@ void AnimationComponent::update(int deltaTime)
|
|||
}
|
||||
}
|
||||
|
||||
void AnimationComponent::addChild(ImageComponent* gui)
|
||||
void AnimationComponent::addChild(GuiComponent* gui)
|
||||
{
|
||||
mChildren.push_back(gui);
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ void AnimationComponent::moveChildren(int offsetx, int offsety)
|
|||
Vector2i move(offsetx, offsety);
|
||||
for(unsigned int i = 0; i < mChildren.size(); i++)
|
||||
{
|
||||
ImageComponent* comp = mChildren.at(i);
|
||||
GuiComponent* comp = mChildren.at(i);
|
||||
comp->setOffset(comp->getOffset() + move);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
#define _ANIMATIONCOMPONENT_H_
|
||||
|
||||
#include "../GuiComponent.h"
|
||||
#include "ImageComponent.h"
|
||||
#include <vector>
|
||||
|
||||
#define ANIMATION_TICK_SPEED 16
|
||||
|
@ -18,12 +17,12 @@ public:
|
|||
|
||||
void update(int deltaTime);
|
||||
|
||||
void addChild(ImageComponent* gui);
|
||||
void addChild(GuiComponent* gui);
|
||||
|
||||
private:
|
||||
unsigned char mOpacity;
|
||||
|
||||
std::vector<ImageComponent*> mChildren;
|
||||
std::vector<GuiComponent*> mChildren;
|
||||
|
||||
void moveChildren(int offsetx, int offsety);
|
||||
void setChildrenOpacity(unsigned char opacity);
|
||||
|
|
|
@ -7,17 +7,15 @@ const std::string GuiFastSelect::LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|||
const int GuiFastSelect::SCROLLSPEED = 100;
|
||||
const int GuiFastSelect::SCROLLDELAY = 507;
|
||||
|
||||
GuiFastSelect::GuiFastSelect(Window* window, GuiGameList* parent, TextListComponent<FileData*>* list, char startLetter, GuiBoxData data,
|
||||
int textcolor, std::shared_ptr<Sound> & scrollsound, Font* font) : GuiComponent(window)
|
||||
GuiFastSelect::GuiFastSelect(Window* window, GuiGameList* parent, TextListComponent<FileData*>* list, char startLetter, ThemeComponent * theme)
|
||||
: GuiComponent(window), mParent(parent), mList(list), mTheme(theme)
|
||||
{
|
||||
mLetterID = LETTERS.find(toupper(startLetter));
|
||||
if(mLetterID == std::string::npos)
|
||||
mLetterID = 0;
|
||||
|
||||
mParent = parent;
|
||||
mList = list;
|
||||
mScrollSound = scrollsound;
|
||||
mFont = font;
|
||||
mScrollSound = mTheme->getSound("menuScroll");
|
||||
mTextColor = mTheme->getColor("fastSelect");
|
||||
|
||||
mScrolling = false;
|
||||
mScrollTimer = 0;
|
||||
|
@ -25,9 +23,7 @@ GuiFastSelect::GuiFastSelect(Window* window, GuiGameList* parent, TextListCompon
|
|||
|
||||
unsigned int sw = Renderer::getScreenWidth(), sh = Renderer::getScreenHeight();
|
||||
mBox = new GuiBox(window, (int)(sw * 0.2f), (int)(sh * 0.2f), (int)(sw * 0.6f), (int)(sh * 0.6f));
|
||||
mBox->setData(data);
|
||||
|
||||
mTextColor = textcolor;
|
||||
mBox->setData(mTheme->getBoxData());
|
||||
}
|
||||
|
||||
GuiFastSelect::~GuiFastSelect()
|
||||
|
@ -41,11 +37,14 @@ void GuiFastSelect::render()
|
|||
unsigned int sw = Renderer::getScreenWidth(), sh = Renderer::getScreenHeight();
|
||||
|
||||
if(!mBox->hasBackground())
|
||||
Renderer::drawRect((int)(sw * 0.2f), (int)(sh * 0.2f), (int)(sw * 0.6f), (int)(sh * 0.6f), 0x000FF0FF);
|
||||
Renderer::drawRect((int)(sw * 0.3f), (int)(sh * 0.3f), (int)(sw * 0.4f), (int)(sh * 0.4f), 0x000FF0AA);
|
||||
|
||||
mBox->render();
|
||||
|
||||
Renderer::drawCenteredText(LETTERS.substr(mLetterID, 1), 0, (int)(sh * 0.5f - (mFont->getHeight() * 0.5f)), mTextColor, mFont);
|
||||
Renderer::drawCenteredText(LETTERS.substr(mLetterID, 1), 0, (int)(sh * 0.5f - (mTheme->getFastSelectFont()->getHeight() * 0.5f)), mTextColor, mTheme->getFastSelectFont());
|
||||
Renderer::drawCenteredText("Sort order:", 0, (int)(sh * 0.6f - (mTheme->getDescriptionFont()->getHeight() * 0.5f)), mTextColor, mTheme->getDescriptionFont());
|
||||
std::string sortString = "<- " + mParent->getSortState().description + " ->";
|
||||
Renderer::drawCenteredText(sortString, 0, (int)(sh * 0.6f + (mTheme->getDescriptionFont()->getHeight() * 0.5f)), mTextColor, mTheme->getDescriptionFont());
|
||||
}
|
||||
|
||||
bool GuiFastSelect::input(InputConfig* config, Input input)
|
||||
|
@ -64,6 +63,19 @@ bool GuiFastSelect::input(InputConfig* config, Input input)
|
|||
return true;
|
||||
}
|
||||
|
||||
if(config->isMappedTo("left", input) && input.value != 0)
|
||||
{
|
||||
mParent->setPreviousSortIndex();
|
||||
mScrollSound->play();
|
||||
return true;
|
||||
}
|
||||
else if(config->isMappedTo("right", input) && input.value != 0)
|
||||
{
|
||||
mParent->setNextSortIndex();
|
||||
mScrollSound->play();
|
||||
return true;
|
||||
}
|
||||
|
||||
if((config->isMappedTo("up", input) || config->isMappedTo("down", input)) && input.value == 0)
|
||||
{
|
||||
mScrolling = false;
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#include "../SystemData.h"
|
||||
#include "../FolderData.h"
|
||||
#include "../Sound.h"
|
||||
#include "ThemeComponent.h"
|
||||
#include "TextListComponent.h"
|
||||
#include "GuiBox.h"
|
||||
|
||||
|
@ -13,8 +14,7 @@ class GuiGameList;
|
|||
class GuiFastSelect : public GuiComponent
|
||||
{
|
||||
public:
|
||||
GuiFastSelect(Window* window, GuiGameList* parent, TextListComponent<FileData*>* list, char startLetter, GuiBoxData data,
|
||||
int textcolor, std::shared_ptr<Sound> & scrollsound, Font* font);
|
||||
GuiFastSelect(Window* window, GuiGameList* parent, TextListComponent<FileData*>* list, char startLetter, ThemeComponent * theme);
|
||||
~GuiFastSelect();
|
||||
|
||||
bool input(InputConfig* config, Input input);
|
||||
|
@ -41,7 +41,7 @@ private:
|
|||
bool mScrolling;
|
||||
|
||||
std::shared_ptr<Sound> mScrollSound;
|
||||
Font* mFont;
|
||||
ThemeComponent * mTheme;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
@ -7,38 +7,53 @@
|
|||
#include "../Log.h"
|
||||
#include "../Settings.h"
|
||||
|
||||
|
||||
std::vector<FolderData::SortState> GuiGameList::sortStates;
|
||||
|
||||
|
||||
Vector2i GuiGameList::getImagePos()
|
||||
{
|
||||
return Vector2i((int)(Renderer::getScreenWidth() * mTheme->getFloat("gameImageOffsetX")), (int)(Renderer::getScreenHeight() * mTheme->getFloat("gameImageOffsetY")));
|
||||
}
|
||||
|
||||
GuiGameList::GuiGameList(Window* window, bool useDetail) : GuiComponent(window),
|
||||
mDescription(window), mTransitionImage(window, 0, 0, "", Renderer::getScreenWidth(), Renderer::getScreenHeight(), true)
|
||||
bool GuiGameList::isDetailed() const
|
||||
{
|
||||
mDetailed = useDetail;
|
||||
if(mSystem == NULL)
|
||||
return false;
|
||||
|
||||
mTheme = new ThemeComponent(mWindow, mDetailed);
|
||||
return mSystem->hasGamelist();
|
||||
}
|
||||
|
||||
mScreenshot = new ImageComponent(mWindow, getImagePos().x, getImagePos().y, "", (unsigned int)mTheme->getFloat("gameImageWidth"), (unsigned int)mTheme->getFloat("gameImageHeight"), false);
|
||||
|
||||
//The GuiGameList can use the older, simple game list if so desired.
|
||||
//The old view only shows a list in the center of the screen; the new view can display an image and description.
|
||||
//Those with smaller displays may prefer the older view.
|
||||
if(mDetailed)
|
||||
{
|
||||
mList = new TextListComponent<FileData*>(mWindow, (int)(Renderer::getScreenWidth() * mTheme->getFloat("listOffsetX")), Renderer::getDefaultFont(Renderer::LARGE)->getHeight() + 2, Renderer::getDefaultFont(Renderer::MEDIUM));
|
||||
|
||||
mImageAnimation = new AnimationComponent();
|
||||
mImageAnimation->addChild(mScreenshot);
|
||||
}else{
|
||||
mList = new TextListComponent<FileData*>(mWindow, 0, Renderer::getDefaultFont(Renderer::LARGE)->getHeight() + 2, Renderer::getDefaultFont(Renderer::MEDIUM));
|
||||
GuiGameList::GuiGameList(Window* window) : GuiComponent(window),
|
||||
mTheme(new ThemeComponent(mWindow)),
|
||||
mList(window, 0, 0, Renderer::getDefaultFont(Renderer::MEDIUM)),
|
||||
mScreenshot(window),
|
||||
mDescription(window),
|
||||
mDescContainer(window),
|
||||
mTransitionImage(window, 0, 0, "", Renderer::getScreenWidth(), Renderer::getScreenHeight(), true),
|
||||
sortStateIndex(Settings::getInstance()->getInt("GameListSortIndex"))
|
||||
{
|
||||
//first object initializes the vector
|
||||
if (sortStates.empty()) {
|
||||
sortStates.push_back(FolderData::SortState(FolderData::compareFileName, true, "file name, ascending"));
|
||||
sortStates.push_back(FolderData::SortState(FolderData::compareFileName, false, "file name, descending"));
|
||||
sortStates.push_back(FolderData::SortState(FolderData::compareRating, true, "database rating, ascending"));
|
||||
sortStates.push_back(FolderData::SortState(FolderData::compareRating, false, "database rating, descending"));
|
||||
sortStates.push_back(FolderData::SortState(FolderData::compareUserRating, true, "your rating, ascending"));
|
||||
sortStates.push_back(FolderData::SortState(FolderData::compareUserRating, false, "your rating, descending"));
|
||||
sortStates.push_back(FolderData::SortState(FolderData::compareTimesPlayed, true, "played least often"));
|
||||
sortStates.push_back(FolderData::SortState(FolderData::compareTimesPlayed, false, "played most often"));
|
||||
sortStates.push_back(FolderData::SortState(FolderData::compareLastPlayed, true, "played least recently"));
|
||||
sortStates.push_back(FolderData::SortState(FolderData::compareLastPlayed, false, "played most recently"));
|
||||
}
|
||||
|
||||
mScreenshot->setOrigin(mTheme->getFloat("gameImageOriginX"), mTheme->getFloat("gameImageOriginY"));
|
||||
mImageAnimation.addChild(&mScreenshot);
|
||||
mDescContainer.addChild(&mDescription);
|
||||
|
||||
//scale delay with screen width (higher width = more text per line)
|
||||
//the scroll speed is automatically scaled by component size
|
||||
mDescContainer.setAutoScroll((int)(1500 + (Renderer::getScreenWidth() * 0.5)), 0.025f);
|
||||
|
||||
mDescription.setOffset(Vector2i((int)(Renderer::getScreenWidth() * 0.03), mScreenshot->getOffset().y + mScreenshot->getSize().y + 12));
|
||||
mDescription.setExtent(Vector2u((int)(Renderer::getScreenWidth() * (mTheme->getFloat("listOffsetX") - 0.03)), 0));
|
||||
|
||||
mTransitionImage.setOffset(Renderer::getScreenWidth(), 0);
|
||||
mTransitionImage.setOrigin(0, 0);
|
||||
mTransitionAnimation.addChild(&mTransitionImage);
|
||||
|
@ -47,7 +62,7 @@ GuiGameList::GuiGameList(Window* window, bool useDetail) : GuiComponent(window),
|
|||
//the list depends on knowing it's final window coordinates (getGlobalOffset), which requires knowing the where the GuiGameList is.
|
||||
//the GuiGameList now moves during screen transitions, so we have to let it know somehow.
|
||||
//this should be removed in favor of using real children soon.
|
||||
mList->setParent(this);
|
||||
mList.setParent(this);
|
||||
|
||||
setSystemId(0);
|
||||
}
|
||||
|
@ -55,15 +70,8 @@ GuiGameList::GuiGameList(Window* window, bool useDetail) : GuiComponent(window),
|
|||
GuiGameList::~GuiGameList()
|
||||
{
|
||||
//undo the parenting hack because otherwise it's not really a child and will try to remove itself on delete
|
||||
mList->setParent(NULL);
|
||||
delete mList;
|
||||
delete mScreenshot;
|
||||
|
||||
if(mDetailed)
|
||||
{
|
||||
delete mImageAnimation;
|
||||
}
|
||||
|
||||
mList.setParent(NULL);
|
||||
|
||||
delete mTheme;
|
||||
}
|
||||
|
||||
|
@ -110,22 +118,17 @@ void GuiGameList::render()
|
|||
if(!mTheme->getBool("hideHeader"))
|
||||
Renderer::drawCenteredText(mSystem->getDescName(), 0, 1, 0xFF0000FF, Renderer::getDefaultFont(Renderer::LARGE));
|
||||
|
||||
if(mDetailed)
|
||||
if(isDetailed())
|
||||
{
|
||||
//divider
|
||||
if(!mTheme->getBool("hideDividers"))
|
||||
Renderer::drawRect((int)(Renderer::getScreenWidth() * mTheme->getFloat("listOffsetX")) - 4, Renderer::getDefaultFont(Renderer::LARGE)->getHeight() + 2, 8, Renderer::getScreenHeight(), 0x0000FFFF);
|
||||
|
||||
mScreenshot->render();
|
||||
|
||||
//if we're not scrolling and we have selected a non-folder
|
||||
if(!mList->isScrolling() && !mList->getSelectedObject()->isFolder())
|
||||
{
|
||||
mDescription.render();
|
||||
}
|
||||
mScreenshot.render();
|
||||
mDescContainer.render();
|
||||
}
|
||||
|
||||
mList->render();
|
||||
mList.render();
|
||||
|
||||
Renderer::translate(-mOffset);
|
||||
|
||||
|
@ -134,14 +137,14 @@ void GuiGameList::render()
|
|||
|
||||
bool GuiGameList::input(InputConfig* config, Input input)
|
||||
{
|
||||
mList->input(config, input);
|
||||
mList.input(config, input);
|
||||
|
||||
if(config->isMappedTo("a", input) && mFolder->getFileCount() > 0 && input.value != 0)
|
||||
{
|
||||
//play select sound
|
||||
mTheme->getSound("menuSelect")->play();
|
||||
|
||||
FileData* file = mList->getSelectedObject();
|
||||
FileData* file = mList.getSelectedObject();
|
||||
if(file->isFolder()) //if you selected a folder, add this directory to the stack, and use the selected one
|
||||
{
|
||||
mFolderStack.push(mFolder);
|
||||
|
@ -150,7 +153,7 @@ bool GuiGameList::input(InputConfig* config, Input input)
|
|||
updateDetailData();
|
||||
return true;
|
||||
}else{
|
||||
mList->stopScrolling();
|
||||
mList.stopScrolling();
|
||||
|
||||
//wait for the sound to finish or we'll never hear it...
|
||||
while(mTheme->getSound("menuSelect")->isPlaying());
|
||||
|
@ -191,6 +194,16 @@ bool GuiGameList::input(InputConfig* config, Input input)
|
|||
}
|
||||
}
|
||||
|
||||
//change sort order
|
||||
if(config->isMappedTo("sortordernext", input) && input.value != 0) {
|
||||
setNextSortIndex();
|
||||
//std::cout << "Sort order is " << FolderData::getSortStateName(sortStates.at(sortStateIndex).comparisonFunction, sortStates.at(sortStateIndex).ascending) << std::endl;
|
||||
}
|
||||
else if(config->isMappedTo("sortorderprevious", input) && input.value != 0) {
|
||||
setPreviousSortIndex();
|
||||
//std::cout << "Sort order is " << FolderData::getSortStateName(sortStates.at(sortStateIndex).comparisonFunction, sortStates.at(sortStateIndex).ascending) << std::endl;
|
||||
}
|
||||
|
||||
//open the "start menu"
|
||||
if(config->isMappedTo("menu", input) && input.value != 0)
|
||||
{
|
||||
|
@ -201,11 +214,11 @@ bool GuiGameList::input(InputConfig* config, Input input)
|
|||
//open the fast select menu
|
||||
if(config->isMappedTo("select", input) && input.value != 0)
|
||||
{
|
||||
mWindow->pushGui(new GuiFastSelect(mWindow, this, mList, mList->getSelectedObject()->getName()[0], mTheme->getBoxData(), mTheme->getColor("fastSelect"), mTheme->getSound("menuScroll"), mTheme->getFastSelectFont()));
|
||||
mWindow->pushGui(new GuiFastSelect(mWindow, this, &mList, mList.getSelectedObject()->getName()[0], mTheme));
|
||||
return true;
|
||||
}
|
||||
|
||||
if(mDetailed)
|
||||
if(isDetailed())
|
||||
{
|
||||
if(config->isMappedTo("up", input) || config->isMappedTo("down", input) || config->isMappedTo("pageup", input) || config->isMappedTo("pagedown", input))
|
||||
{
|
||||
|
@ -220,21 +233,64 @@ bool GuiGameList::input(InputConfig* config, Input input)
|
|||
return false;
|
||||
}
|
||||
|
||||
const FolderData::SortState & GuiGameList::getSortState() const
|
||||
{
|
||||
return sortStates.at(sortStateIndex);
|
||||
}
|
||||
|
||||
void GuiGameList::setSortIndex(size_t index)
|
||||
{
|
||||
//make the index valid
|
||||
if (index >= sortStates.size()) {
|
||||
index = 0;
|
||||
}
|
||||
if (index != sortStateIndex) {
|
||||
//get sort state from vector and sort list
|
||||
sortStateIndex = index;
|
||||
sort(sortStates.at(sortStateIndex).comparisonFunction, sortStates.at(sortStateIndex).ascending);
|
||||
}
|
||||
//save new index to settings
|
||||
Settings::getInstance()->setInt("GameListSortIndex", sortStateIndex);
|
||||
}
|
||||
|
||||
void GuiGameList::setNextSortIndex()
|
||||
{
|
||||
//make the index wrap around
|
||||
if ((sortStateIndex - 1) >= sortStates.size()) {
|
||||
setSortIndex(0);
|
||||
}
|
||||
setSortIndex(sortStateIndex + 1);
|
||||
}
|
||||
|
||||
void GuiGameList::setPreviousSortIndex()
|
||||
{
|
||||
//make the index wrap around
|
||||
if (((int)sortStateIndex - 1) < 0) {
|
||||
setSortIndex(sortStates.size() - 1);
|
||||
}
|
||||
setSortIndex(sortStateIndex - 1);
|
||||
}
|
||||
|
||||
void GuiGameList::sort(FolderData::ComparisonFunction & comparisonFunction, bool ascending)
|
||||
{
|
||||
//resort list and update it
|
||||
mFolder->sort(comparisonFunction, ascending);
|
||||
updateList();
|
||||
updateDetailData();
|
||||
}
|
||||
|
||||
void GuiGameList::updateList()
|
||||
{
|
||||
if(mDetailed)
|
||||
mScreenshot->setImage("");
|
||||
|
||||
mList->clear();
|
||||
mList.clear();
|
||||
|
||||
for(unsigned int i = 0; i < mFolder->getFileCount(); i++)
|
||||
{
|
||||
FileData* file = mFolder->getFile(i);
|
||||
|
||||
if(file->isFolder())
|
||||
mList->addObject(file->getName(), file, mTheme->getColor("secondary"));
|
||||
mList.addObject(file->getName(), file, mTheme->getColor("secondary"));
|
||||
else
|
||||
mList->addObject(file->getName(), file, mTheme->getColor("primary"));
|
||||
mList.addObject(file->getName(), file, mTheme->getColor("primary"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -261,64 +317,77 @@ std::string GuiGameList::getThemeFile()
|
|||
|
||||
void GuiGameList::updateTheme()
|
||||
{
|
||||
if(!mTheme)
|
||||
return;
|
||||
mTheme->readXML(getThemeFile(), isDetailed());
|
||||
|
||||
mTheme->readXML( getThemeFile() );
|
||||
mList.setSelectorColor(mTheme->getColor("selector"));
|
||||
mList.setSelectedTextColor(mTheme->getColor("selected"));
|
||||
mList.setScrollSound(mTheme->getSound("menuScroll"));
|
||||
|
||||
mList->setSelectorColor(mTheme->getColor("selector"));
|
||||
mList->setSelectedTextColor(mTheme->getColor("selected"));
|
||||
mList->setScrollSound(mTheme->getSound("menuScroll"));
|
||||
mList.setFont(mTheme->getListFont());
|
||||
mList.setOffset(0, Renderer::getDefaultFont(Renderer::LARGE)->getHeight() + 2);
|
||||
|
||||
//fonts
|
||||
mList->setFont(mTheme->getListFont());
|
||||
|
||||
if(mDetailed)
|
||||
if(isDetailed())
|
||||
{
|
||||
mList->setCentered(mTheme->getBool("listCentered"));
|
||||
mList.setCentered(mTheme->getBool("listCentered"));
|
||||
|
||||
mList->setOffset((int)(mTheme->getFloat("listOffsetX") * Renderer::getScreenWidth()), mList->getOffset().y);
|
||||
mList->setTextOffsetX((int)(mTheme->getFloat("listTextOffsetX") * Renderer::getScreenWidth()));
|
||||
mList.setOffset((int)(mTheme->getFloat("listOffsetX") * Renderer::getScreenWidth()), mList.getOffset().y);
|
||||
mList.setTextOffsetX((int)(mTheme->getFloat("listTextOffsetX") * Renderer::getScreenWidth()));
|
||||
|
||||
mScreenshot->setOffset((int)(mTheme->getFloat("gameImageOffsetX") * Renderer::getScreenWidth()), (int)(mTheme->getFloat("gameImageOffsetY") * Renderer::getScreenHeight()));
|
||||
mScreenshot->setOrigin(mTheme->getFloat("gameImageOriginX"), mTheme->getFloat("gameImageOriginY"));
|
||||
mScreenshot->setResize((int)mTheme->getFloat("gameImageWidth"), (int)mTheme->getFloat("gameImageHeight"), false);
|
||||
mScreenshot.setOffset((int)(mTheme->getFloat("gameImageOffsetX") * Renderer::getScreenWidth()), (int)(mTheme->getFloat("gameImageOffsetY") * Renderer::getScreenHeight()));
|
||||
mScreenshot.setOrigin(mTheme->getFloat("gameImageOriginX"), mTheme->getFloat("gameImageOriginY"));
|
||||
mScreenshot.setResize((int)mTheme->getFloat("gameImageWidth"), (int)mTheme->getFloat("gameImageHeight"), false);
|
||||
|
||||
mDescription.setColor(mTheme->getColor("description"));
|
||||
mDescription.setFont(mTheme->getDescriptionFont());
|
||||
}else{
|
||||
mList.setCentered(true);
|
||||
mList.setOffset(0, mList.getOffset().y);
|
||||
}
|
||||
}
|
||||
|
||||
void GuiGameList::updateDetailData()
|
||||
{
|
||||
if(!mDetailed)
|
||||
return;
|
||||
|
||||
if(mList->getSelectedObject() && !mList->getSelectedObject()->isFolder())
|
||||
if(!isDetailed())
|
||||
{
|
||||
if(((GameData*)mList->getSelectedObject())->getImagePath().empty())
|
||||
mScreenshot->setImage(mTheme->getString("imageNotFoundPath"));
|
||||
else
|
||||
mScreenshot->setImage(((GameData*)mList->getSelectedObject())->getImagePath());
|
||||
|
||||
Vector2i imgOffset = Vector2i((int)(Renderer::getScreenWidth() * 0.10f), 0);
|
||||
mScreenshot->setOffset(getImagePos() - imgOffset);
|
||||
|
||||
mImageAnimation->fadeIn(35);
|
||||
mImageAnimation->move(imgOffset.x, imgOffset.y, 20);
|
||||
|
||||
mDescription.setOffset(Vector2i((int)(Renderer::getScreenWidth() * 0.03), mScreenshot->getOffset().y + mScreenshot->getSize().y + 12));
|
||||
mDescription.setText(((GameData*)mList->getSelectedObject())->getDescription());
|
||||
mScreenshot.setImage("");
|
||||
mDescription.setText("");
|
||||
}else{
|
||||
mScreenshot->setImage("");
|
||||
//if we've selected a game
|
||||
if(mList.getSelectedObject() && !mList.getSelectedObject()->isFolder())
|
||||
{
|
||||
//set image to either "not found" image or metadata image
|
||||
if(((GameData*)mList.getSelectedObject())->getImagePath().empty())
|
||||
mScreenshot.setImage(mTheme->getString("imageNotFoundPath"));
|
||||
else
|
||||
mScreenshot.setImage(((GameData*)mList.getSelectedObject())->getImagePath());
|
||||
|
||||
Vector2i imgOffset = Vector2i((int)(Renderer::getScreenWidth() * 0.10f), 0);
|
||||
mScreenshot.setOffset(getImagePos() - imgOffset);
|
||||
|
||||
mImageAnimation.fadeIn(35);
|
||||
mImageAnimation.move(imgOffset.x, imgOffset.y, 20);
|
||||
|
||||
mDescContainer.setOffset(Vector2i((int)(Renderer::getScreenWidth() * 0.03), getImagePos().y + mScreenshot.getSize().y + 12));
|
||||
mDescContainer.setSize(Vector2u((int)(Renderer::getScreenWidth() * (mTheme->getFloat("listOffsetX") - 0.03)), Renderer::getScreenHeight() - mDescContainer.getOffset().y));
|
||||
mDescContainer.setScrollPos(Vector2d(0, 0));
|
||||
mDescContainer.resetAutoScrollTimer();
|
||||
|
||||
mDescription.setOffset(0, 0);
|
||||
mDescription.setExtent(Vector2u((int)(Renderer::getScreenWidth() * (mTheme->getFloat("listOffsetX") - 0.03)), 0));
|
||||
mDescription.setText(((GameData*)mList.getSelectedObject())->getDescription());
|
||||
}else{
|
||||
mScreenshot.setImage("");
|
||||
mDescription.setText("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GuiGameList::clearDetailData()
|
||||
{
|
||||
if(mDetailed)
|
||||
if(isDetailed())
|
||||
{
|
||||
mImageAnimation->fadeOut(35);
|
||||
mImageAnimation.fadeOut(35);
|
||||
mDescription.setText("");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -326,11 +395,8 @@ void GuiGameList::clearDetailData()
|
|||
//we have to manually call init/deinit on mTheme because it is not our child
|
||||
void GuiGameList::deinit()
|
||||
{
|
||||
if(mDetailed)
|
||||
{
|
||||
mScreenshot->deinit();
|
||||
}
|
||||
|
||||
mScreenshot.deinit();
|
||||
|
||||
mTheme->deinit();
|
||||
}
|
||||
|
||||
|
@ -338,41 +404,25 @@ void GuiGameList::init()
|
|||
{
|
||||
mTheme->init();
|
||||
|
||||
if(mDetailed)
|
||||
{
|
||||
mScreenshot->init();
|
||||
}
|
||||
mScreenshot.init();
|
||||
}
|
||||
|
||||
GuiGameList* GuiGameList::create(Window* window)
|
||||
{
|
||||
bool detailed = false;
|
||||
|
||||
if(!Settings::getInstance()->getBool("IGNOREGAMELIST"))
|
||||
{
|
||||
for(unsigned int i = 0; i < SystemData::sSystemVector.size(); i++)
|
||||
{
|
||||
if(SystemData::sSystemVector.at(i)->hasGamelist())
|
||||
{
|
||||
detailed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GuiGameList* list = new GuiGameList(window, detailed);
|
||||
GuiGameList* list = new GuiGameList(window);
|
||||
window->pushGui(list);
|
||||
return list;
|
||||
}
|
||||
|
||||
void GuiGameList::update(int deltaTime)
|
||||
{
|
||||
if(mDetailed)
|
||||
mImageAnimation->update(deltaTime);
|
||||
mImageAnimation.update(deltaTime);
|
||||
|
||||
mTransitionAnimation.update(deltaTime);
|
||||
|
||||
mList->update(deltaTime);
|
||||
mList.update(deltaTime);
|
||||
|
||||
mDescContainer.update(deltaTime);
|
||||
}
|
||||
|
||||
void GuiGameList::doTransition(int dir)
|
||||
|
|
|
@ -12,19 +12,23 @@
|
|||
#include "../SystemData.h"
|
||||
#include "../GameData.h"
|
||||
#include "../FolderData.h"
|
||||
#include "ScrollableContainer.h"
|
||||
|
||||
//This is where the magic happens - GuiGameList is the parent of almost every graphical element in ES at the moment.
|
||||
//It has a TextListComponent child that handles the game list, a ThemeComponent that handles the theming system, and an ImageComponent for game images.
|
||||
class GuiGameList : public GuiComponent
|
||||
{
|
||||
static std::vector<FolderData::SortState> sortStates;
|
||||
size_t sortStateIndex;
|
||||
|
||||
public:
|
||||
GuiGameList(Window* window, bool useDetail = false);
|
||||
GuiGameList(Window* window);
|
||||
virtual ~GuiGameList();
|
||||
|
||||
void setSystemId(int id);
|
||||
|
||||
bool input(InputConfig* config, Input input);
|
||||
void update(int deltaTime);
|
||||
bool input(InputConfig* config, Input input) override;
|
||||
void update(int deltaTime) override;
|
||||
void render();
|
||||
|
||||
void init();
|
||||
|
@ -32,8 +36,16 @@ public:
|
|||
|
||||
void updateDetailData();
|
||||
|
||||
const FolderData::SortState & getSortState() const;
|
||||
void setSortIndex(size_t index);
|
||||
void setNextSortIndex();
|
||||
void setPreviousSortIndex();
|
||||
void sort(FolderData::ComparisonFunction & comparisonFunction = FolderData::compareFileName, bool ascending = true);
|
||||
|
||||
static GuiGameList* create(Window* window);
|
||||
|
||||
bool isDetailed() const;
|
||||
|
||||
static const float sInfoWidth;
|
||||
private:
|
||||
void updateList();
|
||||
|
@ -47,12 +59,12 @@ private:
|
|||
FolderData* mFolder;
|
||||
std::stack<FolderData*> mFolderStack;
|
||||
int mSystemId;
|
||||
bool mDetailed;
|
||||
|
||||
TextListComponent<FileData*>* mList;
|
||||
ImageComponent* mScreenshot;
|
||||
TextListComponent<FileData*> mList;
|
||||
ImageComponent mScreenshot;
|
||||
TextComponent mDescription;
|
||||
AnimationComponent* mImageAnimation;
|
||||
ScrollableContainer mDescContainer;
|
||||
AnimationComponent mImageAnimation;
|
||||
ThemeComponent* mTheme;
|
||||
|
||||
ImageComponent mTransitionImage;
|
||||
|
|
|
@ -224,8 +224,6 @@ bool ImageComponent::hasImage()
|
|||
return !mPath.empty();
|
||||
}
|
||||
|
||||
unsigned char ImageComponent::getOpacity() { return mOpacity; }
|
||||
void ImageComponent::setOpacity(unsigned char opacity) { mOpacity = opacity; }
|
||||
|
||||
void ImageComponent::copyScreen()
|
||||
{
|
||||
|
|
|
@ -32,9 +32,6 @@ public:
|
|||
|
||||
bool hasImage();
|
||||
|
||||
unsigned char getOpacity();
|
||||
void setOpacity(unsigned char opacity);
|
||||
|
||||
protected:
|
||||
void onRender();
|
||||
|
||||
|
@ -44,8 +41,6 @@ private:
|
|||
|
||||
bool mAllowUpscale, mTiled, mFlipX, mFlipY;
|
||||
|
||||
unsigned char mOpacity;
|
||||
|
||||
void resize();
|
||||
void buildImageArray(int x, int y, GLfloat* points, GLfloat* texs, float percentageX = 1, float percentageY = 1); //writes 12 GLfloat points and 12 GLfloat texture coordinates to a given array at a given position
|
||||
void drawImageArray(GLfloat* points, GLfloat* texs, GLubyte* colors, unsigned int count = 6); //draws the given set of points and texture coordinates, number of coordinate pairs may be specified (default 6)
|
||||
|
|
109
src/components/ScrollableContainer.cpp
Normal file
109
src/components/ScrollableContainer.cpp
Normal file
|
@ -0,0 +1,109 @@
|
|||
#include "ScrollableContainer.h"
|
||||
#include "../Renderer.h"
|
||||
#include "../Log.h"
|
||||
|
||||
ScrollableContainer::ScrollableContainer(Window* window) : GuiComponent(window),
|
||||
mAutoScrollDelay(0), mAutoScrollSpeed(0), mAutoScrollTimer(0)
|
||||
{
|
||||
}
|
||||
|
||||
void ScrollableContainer::render()
|
||||
{
|
||||
Renderer::pushClipRect(getGlobalOffset(), getSize());
|
||||
|
||||
Vector2f translate = (Vector2f)mOffset - (Vector2f)mScrollPos;
|
||||
|
||||
Renderer::translatef(translate.x, translate.y);
|
||||
|
||||
GuiComponent::onRender();
|
||||
|
||||
Renderer::translatef(-translate.x, -translate.y);
|
||||
|
||||
Renderer::popClipRect();
|
||||
}
|
||||
|
||||
void ScrollableContainer::setAutoScroll(int delay, double speed)
|
||||
{
|
||||
mAutoScrollDelay = delay;
|
||||
mAutoScrollSpeed = speed;
|
||||
mAutoScrollTimer = 0;
|
||||
}
|
||||
|
||||
Vector2d ScrollableContainer::getScrollPos() const
|
||||
{
|
||||
return mScrollPos;
|
||||
}
|
||||
|
||||
void ScrollableContainer::setScrollPos(const Vector2d& pos)
|
||||
{
|
||||
mScrollPos = pos;
|
||||
}
|
||||
|
||||
void ScrollableContainer::update(int deltaTime)
|
||||
{
|
||||
double scrollAmt = (double)deltaTime;
|
||||
|
||||
if(mAutoScrollSpeed != 0)
|
||||
{
|
||||
mAutoScrollTimer += deltaTime;
|
||||
|
||||
scrollAmt = (float)(mAutoScrollTimer - mAutoScrollDelay);
|
||||
|
||||
if(scrollAmt > 0)
|
||||
{
|
||||
//scroll the amount of time left over from the delay
|
||||
mAutoScrollTimer = mAutoScrollDelay;
|
||||
|
||||
//scale speed by our width! more text per line = slower scrolling
|
||||
const double widthMod = (680.0 / getSize().x);
|
||||
mScrollDir = Vector2d(0, mAutoScrollSpeed * widthMod);
|
||||
}else{
|
||||
//not enough to pass the delay, do nothing
|
||||
scrollAmt = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Vector2d scroll = mScrollDir * scrollAmt;
|
||||
mScrollPos += scroll;
|
||||
|
||||
//clip scrolling within bounds
|
||||
if(mScrollPos.x < 0)
|
||||
mScrollPos.x = 0;
|
||||
if(mScrollPos.y < 0)
|
||||
mScrollPos.y = 0;
|
||||
|
||||
|
||||
Vector2i contentSize = getContentSize();
|
||||
if(mScrollPos.x + getSize().x > contentSize.x)
|
||||
mScrollPos.x = (double)contentSize.x - getSize().x;
|
||||
if(mScrollPos.y + getSize().y > contentSize.y)
|
||||
mScrollPos.y = (double)contentSize.y - getSize().y;
|
||||
|
||||
GuiComponent::update(deltaTime);
|
||||
}
|
||||
|
||||
//this should probably return a box to allow for when controls don't start at 0,0
|
||||
Vector2i ScrollableContainer::getContentSize()
|
||||
{
|
||||
Vector2i max;
|
||||
for(unsigned int i = 0; i < mChildren.size(); i++)
|
||||
{
|
||||
Vector2i bottomRight = (Vector2i)mChildren.at(i)->getSize() + mChildren.at(i)->getOffset();
|
||||
if(bottomRight.x > max.x)
|
||||
max.x = bottomRight.x;
|
||||
if(bottomRight.y > max.y)
|
||||
max.y = bottomRight.y;
|
||||
}
|
||||
|
||||
return max;
|
||||
}
|
||||
|
||||
void ScrollableContainer::setSize(Vector2u size)
|
||||
{
|
||||
mSize = size;
|
||||
}
|
||||
|
||||
void ScrollableContainer::resetAutoScrollTimer()
|
||||
{
|
||||
mAutoScrollTimer = 0;
|
||||
}
|
29
src/components/ScrollableContainer.h
Normal file
29
src/components/ScrollableContainer.h
Normal file
|
@ -0,0 +1,29 @@
|
|||
#pragma once
|
||||
|
||||
#include "../GuiComponent.h"
|
||||
|
||||
class ScrollableContainer : public GuiComponent
|
||||
{
|
||||
public:
|
||||
ScrollableContainer(Window* window);
|
||||
|
||||
void setSize(Vector2u size);
|
||||
|
||||
Vector2d getScrollPos() const;
|
||||
void setScrollPos(const Vector2d& pos);
|
||||
void setAutoScroll(int delay, double speed); //Use 0 for speed to disable.
|
||||
void resetAutoScrollTimer();
|
||||
|
||||
void update(int deltaTime) override;
|
||||
void render() override;
|
||||
|
||||
//Vector2i getGlobalOffset() override;
|
||||
private:
|
||||
Vector2i getContentSize();
|
||||
|
||||
Vector2d mScrollPos;
|
||||
Vector2d mScrollDir;
|
||||
int mAutoScrollDelay;
|
||||
double mAutoScrollSpeed;
|
||||
int mAutoScrollTimer;
|
||||
};
|
|
@ -2,12 +2,13 @@
|
|||
#include "../Renderer.h"
|
||||
#include "../Log.h"
|
||||
|
||||
TextComponent::TextComponent(Window* window) : GuiComponent(window), mFont(NULL), mColor(0x000000FF), mAutoCalcExtent(true)
|
||||
TextComponent::TextComponent(Window* window) : GuiComponent(window),
|
||||
mFont(NULL), mColor(0x000000FF), mAutoCalcExtent(true, true)
|
||||
{
|
||||
}
|
||||
|
||||
TextComponent::TextComponent(Window* window, const std::string& text, Font* font, Vector2i pos, Vector2u size) : GuiComponent(window),
|
||||
mFont(NULL), mColor(0x000000FF), mAutoCalcExtent(true)
|
||||
mFont(NULL), mColor(0x000000FF), mAutoCalcExtent(true, true)
|
||||
{
|
||||
setText(text);
|
||||
setFont(font);
|
||||
|
@ -22,22 +23,16 @@ void TextComponent::setBox(Vector2i pos, Vector2u size)
|
|||
|
||||
void TextComponent::setExtent(Vector2u size)
|
||||
{
|
||||
if(size == Vector2u(0, 0))
|
||||
{
|
||||
mAutoCalcExtent = true;
|
||||
calculateExtent();
|
||||
}else{
|
||||
mAutoCalcExtent = false;
|
||||
mSize = size;
|
||||
}
|
||||
mSize = size;
|
||||
mAutoCalcExtent = Vector2<bool>(size.x == 0, size.y == 0);
|
||||
calculateExtent();
|
||||
}
|
||||
|
||||
void TextComponent::setFont(Font* font)
|
||||
{
|
||||
mFont = font;
|
||||
|
||||
if(mAutoCalcExtent)
|
||||
calculateExtent();
|
||||
calculateExtent();
|
||||
}
|
||||
|
||||
void TextComponent::setColor(unsigned int color)
|
||||
|
@ -49,36 +44,44 @@ void TextComponent::setText(const std::string& text)
|
|||
{
|
||||
mText = text;
|
||||
|
||||
if(mAutoCalcExtent)
|
||||
calculateExtent();
|
||||
calculateExtent();
|
||||
}
|
||||
|
||||
Font* TextComponent::getFont() const
|
||||
{
|
||||
return (mFont ? mFont : Renderer::getDefaultFont(Renderer::MEDIUM));;
|
||||
}
|
||||
|
||||
void TextComponent::onRender()
|
||||
{
|
||||
Font* font = (mFont ? mFont : Renderer::getDefaultFont(Renderer::MEDIUM));
|
||||
Font* font = getFont();
|
||||
if(font == NULL)
|
||||
{
|
||||
LOG(LogError) << "TextComponent can't get a valid font!";
|
||||
return;
|
||||
}
|
||||
|
||||
Renderer::pushClipRect(getGlobalOffset(), getSize());
|
||||
//Renderer::pushClipRect(getGlobalOffset(), getSize());
|
||||
|
||||
Renderer::drawWrappedText(mText, 0, 0, mSize.x, mColor, font);
|
||||
Renderer::drawWrappedText(mText, 0, 0, mSize.x, mColor >> 8 << 8 | getOpacity(), font);
|
||||
|
||||
Renderer::popClipRect();
|
||||
//Renderer::popClipRect();
|
||||
|
||||
GuiComponent::onRender();
|
||||
}
|
||||
|
||||
void TextComponent::calculateExtent()
|
||||
{
|
||||
Font* font = (mFont ? mFont : Renderer::getDefaultFont(Renderer::MEDIUM));
|
||||
Font* font = getFont();
|
||||
if(font == NULL)
|
||||
{
|
||||
LOG(LogError) << "TextComponent can't get a valid font!";
|
||||
return;
|
||||
}
|
||||
|
||||
font->sizeText(mText, (int*)&mSize.x, (int*)&mSize.y);
|
||||
if(mAutoCalcExtent.x)
|
||||
font->sizeText(mText, (int*)&mSize.x, (int*)&mSize.y);
|
||||
else
|
||||
if(mAutoCalcExtent.y)
|
||||
Renderer::sizeWrappedText(mText, getSize().x, mFont, NULL, (int*)&mSize.y);
|
||||
}
|
||||
|
|
|
@ -12,18 +12,20 @@ public:
|
|||
|
||||
void setFont(Font* font);
|
||||
void setBox(Vector2i pos, Vector2u size);
|
||||
void setExtent(Vector2u size); //Use Vector2u(0, 0) to automatically generate extent.
|
||||
void setExtent(Vector2u size); //Use Vector2u(0, 0) to automatically generate extent on a single line. Use Vector2(value, 0) to automatically generate extent for wrapped text.
|
||||
void setText(const std::string& text);
|
||||
void setColor(unsigned int color);
|
||||
|
||||
void onRender();
|
||||
void onRender() override;
|
||||
|
||||
private:
|
||||
Font* getFont() const;
|
||||
|
||||
void calculateExtent();
|
||||
|
||||
unsigned int mColor;
|
||||
Font* mFont;
|
||||
bool mAutoCalcExtent;
|
||||
Vector2<bool> mAutoCalcExtent;
|
||||
std::string mText;
|
||||
};
|
||||
|
||||
|
|
|
@ -117,7 +117,7 @@ void TextListComponent<T>::onRender()
|
|||
|
||||
//number of entries that can fit on the screen simultaniously
|
||||
int screenCount = (Renderer::getScreenHeight() - cutoff) / entrySize;
|
||||
//screenCount -= 1;
|
||||
screenCount -= 1;
|
||||
|
||||
if((int)mRowVector.size() >= screenCount)
|
||||
{
|
||||
|
|
|
@ -59,10 +59,8 @@ Font* ThemeComponent::getFastSelectFont()
|
|||
return mFastSelectFont;
|
||||
}
|
||||
|
||||
ThemeComponent::ThemeComponent(Window* window, bool detailed, std::string path) : GuiComponent(window)
|
||||
ThemeComponent::ThemeComponent(Window* window) : GuiComponent(window)
|
||||
{
|
||||
mDetailed = detailed;
|
||||
|
||||
mSoundMap["menuScroll"] = std::shared_ptr<Sound>(new Sound);
|
||||
mSoundMap["menuSelect"] = std::shared_ptr<Sound>(new Sound);
|
||||
mSoundMap["menuBack"] = std::shared_ptr<Sound>(new Sound);
|
||||
|
@ -79,9 +77,6 @@ ThemeComponent::ThemeComponent(Window* window, bool detailed, std::string path)
|
|||
mFastSelectFont = NULL;
|
||||
|
||||
setDefaults();
|
||||
|
||||
if(!path.empty())
|
||||
readXML(path);
|
||||
}
|
||||
|
||||
ThemeComponent::~ThemeComponent()
|
||||
|
@ -159,7 +154,7 @@ void ThemeComponent::deleteComponents()
|
|||
|
||||
|
||||
|
||||
void ThemeComponent::readXML(std::string path)
|
||||
void ThemeComponent::readXML(std::string path, bool detailed)
|
||||
{
|
||||
if(mPath == path)
|
||||
return;
|
||||
|
@ -185,7 +180,7 @@ void ThemeComponent::readXML(std::string path)
|
|||
|
||||
pugi::xml_node root;
|
||||
|
||||
if(!mDetailed)
|
||||
if(!detailed)
|
||||
{
|
||||
//if we're using the basic view, check if there's a basic version of the theme
|
||||
root = doc.child("basicTheme");
|
||||
|
|
|
@ -13,10 +13,10 @@
|
|||
class ThemeComponent : public GuiComponent
|
||||
{
|
||||
public:
|
||||
ThemeComponent(Window* window, bool detailed, std::string path = "");
|
||||
ThemeComponent(Window* window);
|
||||
virtual ~ThemeComponent();
|
||||
|
||||
void readXML(std::string path);
|
||||
void readXML(std::string path, bool detailed);
|
||||
|
||||
GuiBoxData getBoxData();
|
||||
|
||||
|
@ -48,7 +48,6 @@ private:
|
|||
Font* resolveFont(pugi::xml_node node, std::string defaultPath, unsigned int defaultSize);
|
||||
|
||||
std::string mPath;
|
||||
bool mDetailed;
|
||||
|
||||
std::map<std::string, unsigned int> mColorMap;
|
||||
std::map<std::string, bool> mBoolMap;
|
||||
|
|
Loading…
Reference in a new issue