Tons of new theming features!

Check out THEMES.md for more info.
This commit is contained in:
root 2012-08-13 13:32:53 -05:00
parent c7349cd99a
commit d842d67557
13 changed files with 364 additions and 71 deletions

24
CREDITS.md Normal file
View file

@ -0,0 +1,24 @@
Programming
Alec "Aloshi" Lofquist - http://www.aloshi.com
Resources
=========
LinLibertine.ttf
The Libertine Font Project - http://www.linuxlibertine.org/
PugiXML
http://pugixml.org/
SDL 1.2
http://www.libsdl.org/
SDL TTF
http://www.libsdl.org/projects/SDL_ttf/
SDL_image
http://www.libsdl.org/projects/SDL_image/
SDL_gfx
http://sourceforge.net/projects/sdlgfx/

View file

@ -55,23 +55,8 @@ The switch `--gamelist-only` can be used to skip automatic searching, and only d
Themes
======
At the moment, theming is still in flux. But if you want to play around with what's here, feel free. ES will first check a system's search directory for a file named theme.xml. If that's not found, it'll check $HOME/.emulationstation/es_theme.xml.
Themes are drawn before the rest of the game list. Here's the example I've been using to test a background:
If you want to know more about themes, read THEMES.md!
```
<theme>
<component>
<type>image</type>
<path>/home/aloshi/EmulationStation/theme/background.png</path>
<pos>0 0</pos>
<dim>1 1</dim>
</component>
</theme>
```
You can add more than one component. You can use more than one component and components can be nested for your own personal use (but they won't inherit positions or anything). The only type thus far is image. Pos is short for position and dim is short for dimensions. Both work in screen percentages - a decimal from 0 to 1. A single space separates X/Y or width/height.
At the moment the X position is the horizontal center point for the image and Y is the top of the image.
Variable support is present, but the only variable defined right now is $headerHeight. You should be able to use addition/subtraction/multiplication/division.
-Aloshi
http://www.aloshi.com

74
THEMES.md Normal file
View file

@ -0,0 +1,74 @@
Themes
======
EmulationStation allows each system to have its own "theme." A theme is a collection of display settings and images defined in an XML document.
ES will check two places for a theme: first, the system's search directory for theme.xml. Then, if that's not found, $HOME/.emulationstation/es_theme.xml.
Almost all positions, dimensions, etc. work in percentages - that is, they are a decimal between 0 and 1, representing the percentage of the screen on that axis to use.
This ensures that themes look similar at every resolution.
Example
=======
Here's a simple theme that defines some colors and displays a background:
```
<theme>
<listPrimaryColor>0000FF</listPrimaryColor>
<listSecondaryColor>00FF00</listSecondaryColor>
<component>
<type>image</type>
<path>./theme/background.png</path>
<pos>0 0</pos>
<dim>1 1</dim>
<origin>0 0</origin>
</component>
</theme>
```
All themes must be enclosed in a `<theme>` tag.
Components
==========
A theme is made up of components, which have various types. At the moment, the only type is `image`. Components can be nested for your own organization. In the future, you may be able to get data from a parent.
The "image" component
=====================
Used to display an image.
`<path>` - path to the image file. Most common file types are supported, and . and ~ are properly expanded.
`<pos>` - the position, as two screen percentage, at which to display the image.
`<dim>` - the dimensions, as two screen percentages, that the image will be resized to. Leave one percentage 0 to keep the aspect ratio.
`<origin>` - the point on the image that <pos> defines, as an image percentage. "0.5 0.5", the center of the image, by default.
`<tiled />` - if present, the image is tiled instead of resized. Tiling isn't exact at the moment, but good enough for backgrounds.
`<useAlpha />` - if present, the image will not be stripped of its alpha channel. It will render much slower, but should have transparency.
Display tags
============
Display tags must be at the root of the <theme> tree - for example, they can't be inside a component tag. They are not required.
`<listPrimaryColor>` - the hex font color to use for games on the GuiGameList.
`<listSecondaryColor>` - the hex font color to use for folders on the GuiGameList.
`<descColor>` - the hex font color to use for the description on the GuiGameList.
`<listSelectorColor>` - the hex color to use for the "selector bar" on the GuiGameList.
`<hideHeader />` - if present, the system name header won't be displayed (useful for replacing it with an image).
`<hideDividers />` - if present, the divider between games on the detailed GuiGameList won't be displayed.
List of variables
=================
Variables can be used in position and dimension definitions. They can be added, subtracted, multiplied, and divided. Parenthesis are valid.
Currently, there's only one variable defined:
`$headerHeight` - height of the system name header.
-Aloshi
http://www.aloshi.com

View file

@ -1,3 +1,8 @@
August 13
-Tons of new theming features!
-I've added a THEMES.md file for documentation on theming.
-A CREDITS.md file.
August 12
-If a theme.xml is not found in a system's directory, ES will now check for $HOME/.emulationstation/es_theme.xml. If present, it will load that.
-Themes can now be used without the detailed GuiGameList.

View file

@ -10,6 +10,21 @@ bool Renderer::loadedFonts = false;
TTF_Font* Renderer::fonts[3];
int Renderer::fontHeight[3];
SDL_Color getSDLColor(int& color)
{
char* c = (char*)(&color);
SDL_Color ret;
ret.r = *(c + 2);
ret.g = *(c + 1);
ret.b = *(c + 0);
return ret;
}
void Renderer::drawRect(int x, int y, int h, int w, int color)
{
SDL_Rect rect = {x, y, h, w};
@ -67,9 +82,10 @@ void Renderer::drawText(std::string text, int x, int y, int color, FontSize font
//SDL_Color is a struct of four bytes, with the first three being colors. An int is four bytes.
//So, we can just pretend the int is an SDL_Color.
SDL_Color* sdlcolor = (SDL_Color*)&color;
//SDL_Color* sdlcolor = (SDL_Color*)&color;
SDL_Color sdlcolor = getSDLColor(color);
SDL_Surface* textSurf = TTF_RenderText_Blended(font, text.c_str(), *sdlcolor);
SDL_Surface* textSurf = TTF_RenderText_Blended(font, text.c_str(), sdlcolor);
if(textSurf == NULL)
{
std::cerr << "Error - could not render text \"" << text << "\" to surface!\n";

View file

@ -24,6 +24,7 @@ GuiGameList::GuiGameList(bool useDetail)
mList = new GuiList<FileData*>(Renderer::getScreenWidth() * 0.4, Renderer::getFontHeight(Renderer::LARGE) + 2);
mScreenshot = new GuiImage(Renderer::getScreenWidth() * 0.2, Renderer::getFontHeight(Renderer::LARGE) + 2, "", Renderer::getScreenWidth() * 0.3);
mScreenshot->setOrigin(0.5, 0.0);
addChild(mScreenshot);
}else{
mList = new GuiList<FileData*>(0, Renderer::getFontHeight(Renderer::LARGE) + 2);
@ -96,12 +97,14 @@ void GuiGameList::onRender()
Renderer::drawText(fps, 0, 0, 0x00FF00);
#endif
Renderer::drawCenteredText(mSystem->getName(), 0, 1, 0x0000FF, Renderer::LARGE);
//header
if(!mTheme->getHeaderHidden())
Renderer::drawCenteredText(mSystem->getName(), 0, 1, 0xFF0000, Renderer::LARGE);
if(mDetailed)
{
Renderer::drawRect(Renderer::getScreenWidth() * 0.4, Renderer::getFontHeight(Renderer::LARGE) + 2, 8, Renderer::getScreenHeight(), 0x0000FF);
if(!mTheme->getDividersHidden())
Renderer::drawRect(Renderer::getScreenWidth() * 0.4, Renderer::getFontHeight(Renderer::LARGE) + 2, 8, Renderer::getScreenHeight(), 0x0000FF);
//if we have selected a non-folder
if(mList->getSelectedObject() && !mList->getSelectedObject()->isFolder())
@ -111,7 +114,7 @@ void GuiGameList::onRender()
//todo: cache this
std::string desc = game->getDescription();
if(!desc.empty())
Renderer::drawWrappedText(desc, 2, Renderer::getFontHeight(Renderer::LARGE) + mScreenshot->getHeight() + 10, Renderer::getScreenWidth() * 0.4, 0xFF0000, Renderer::SMALL);
Renderer::drawWrappedText(desc, 2, Renderer::getFontHeight(Renderer::LARGE) + mScreenshot->getHeight() + 10, Renderer::getScreenWidth() * 0.4, mTheme->getDescColor(), Renderer::SMALL);
}
}
}
@ -178,9 +181,9 @@ void GuiGameList::updateList()
FileData* file = mFolder->getFile(i);
if(file->isFolder())
mList->addObject(file->getName(), file, 0x00C000);
mList->addObject(file->getName(), file, mTheme->getSecondaryColor());
else
mList->addObject(file->getName(), file);
mList->addObject(file->getName(), file, mTheme->getPrimaryColor());
}
}
@ -199,6 +202,8 @@ void GuiGameList::updateTheme()
mTheme->readXML(defaultPath);
else
mTheme->readXML(""); //clears any current theme
mList->setSelectorColor(mTheme->getSelectorColor());
}
void GuiGameList::updateDetailData()

View file

@ -14,9 +14,17 @@ GuiImage::GuiImage(int offsetX, int offsetY, std::string path, unsigned int maxW
mOffsetX = offsetX;
mOffsetY = offsetY;
//default origin (center of image)
mOriginX = 0;
mOriginY = 0;
mTiled = false;
mMaxWidth = maxWidth;
mMaxHeight = maxHeight;
mResizeExact = resizeExact;
mUseAlpha = false;
if(!path.empty())
setImage(path);
@ -43,38 +51,15 @@ void GuiImage::loadImage(std::string path)
}
//resize it
if(mResizeExact)
{
double scaleX = (double)mMaxWidth / (double)newSurf->w;
double scaleY = (double)mMaxHeight / (double)newSurf->h;
SDL_Surface* resSurf = zoomSurface(newSurf, scaleX, scaleY, SMOOTHING_OFF);
SDL_FreeSurface(newSurf);
newSurf = resSurf;
}else{
if(mMaxWidth && newSurf->w > mMaxWidth)
{
double scale = (double)mMaxWidth / (double)newSurf->w;
SDL_Surface* resSurf = zoomSurface(newSurf, scale, scale, SMOOTHING_OFF);
SDL_FreeSurface(newSurf);
newSurf = resSurf;
}
if(mMaxHeight && newSurf->h > mMaxHeight)
{
double scale = (double)mMaxHeight / (double)newSurf->h;
SDL_Surface* resSurf = zoomSurface(newSurf, scale, scale, SMOOTHING_OFF);
SDL_FreeSurface(newSurf);
newSurf = resSurf;
}
}
resizeSurface(&newSurf);
//convert it into display format for faster rendering
SDL_Surface* dispSurf = SDL_DisplayFormat(newSurf);
SDL_Surface* dispSurf;
if(mUseAlpha)
dispSurf = SDL_DisplayFormatAlpha(newSurf);
else
dispSurf = SDL_DisplayFormat(newSurf);
SDL_FreeSurface(newSurf);
newSurf = dispSurf;
@ -85,16 +70,56 @@ void GuiImage::loadImage(std::string path)
mSurface = newSurf;
//Also update the rect
mRect.x = mOffsetX - (mSurface->w / 2);
mRect.y = mOffsetY;
mRect.w = mSurface->w;
mRect.h = mSurface->h;
updateRect();
}else{
std::cerr << "File \"" << path << "\" not found!\n";
}
}
//enjoy this overly-complicated pointer stuff that results from splitting a function too late
void GuiImage::resizeSurface(SDL_Surface** surfRef)
{
if(mTiled)
return;
SDL_Surface* newSurf = *surfRef;
if(mResizeExact)
{
double scaleX = (double)mMaxWidth / (double)newSurf->w;
double scaleY = (double)mMaxHeight / (double)newSurf->h;
if(scaleX == 0)
scaleX = scaleY;
if(scaleY == 0)
scaleY = scaleX;
SDL_Surface* resSurf = zoomSurface(newSurf, scaleX, scaleY, SMOOTHING_OFF);
SDL_FreeSurface(newSurf);
newSurf = resSurf;
}else{
if(mMaxWidth && newSurf->w > mMaxWidth)
{
double scale = (double)mMaxWidth / (double)newSurf->w;
SDL_Surface* resSurf = zoomSurface(newSurf, scale, scale, SMOOTHING_OFF);
SDL_FreeSurface(newSurf);
newSurf = resSurf;
}
if(mMaxHeight && newSurf->h > mMaxHeight)
{
double scale = (double)mMaxHeight / (double)newSurf->h;
SDL_Surface* resSurf = zoomSurface(newSurf, scale, scale, SMOOTHING_OFF);
SDL_FreeSurface(newSurf);
newSurf = resSurf;
}
}
*surfRef = newSurf;
}
void GuiImage::setImage(std::string path)
{
if(mPath == path)
@ -113,8 +138,64 @@ void GuiImage::setImage(std::string path)
}
void GuiImage::updateRect()
{
mRect.x = mOffsetX /*- mSurface->w*/ - (mSurface->w * mOriginX);
mRect.y = mOffsetY + (mSurface->h * mOriginY);
mRect.w = mSurface->w;
mRect.h = mSurface->h;
}
void GuiImage::setOrigin(float originX, float originY)
{
mOriginX = originX;
mOriginY = originY;
if(mSurface)
updateRect();
}
void GuiImage::setTiling(bool tile)
{
mTiled = tile;
if(mTiled)
mResizeExact = false;
}
void GuiImage::setAlpha(bool useAlpha)
{
mUseAlpha = useAlpha;
if(mSurface)
{
SDL_FreeSurface(mSurface);
mSurface = NULL;
loadImage(mPath);
}
}
bool dbg = false;
void GuiImage::onRender()
{
if(mSurface)
SDL_BlitSurface(mSurface, NULL, Renderer::screen, &mRect);
{
if(mTiled)
{
SDL_Rect rect = mRect;
for(int x = 0; x < mMaxWidth / mSurface->w + 0.5; x++)
{
for(int y = 0; y < mMaxHeight / mSurface->h + 0.5; y++)
{
SDL_BlitSurface(mSurface, NULL, Renderer::screen, &rect);
rect.y += mSurface->h;
}
rect.x += mSurface->w;
rect.y = mRect.y;
}
}else{
SDL_BlitSurface(mSurface, NULL, Renderer::screen, &mRect);
}
}
}

View file

@ -13,6 +13,9 @@ public:
~GuiImage();
void setImage(std::string path);
void setOrigin(float originX, float originY);
void setTiling(bool tile);
void setAlpha(bool useAlpha);
int getWidth();
int getHeight();
@ -21,9 +24,12 @@ public:
private:
int mMaxWidth, mMaxHeight;
bool mResizeExact;
float mOriginX, mOriginY;
bool mResizeExact, mTiled, mUseAlpha;
void loadImage(std::string path);
void resizeSurface(SDL_Surface** surfRef);
void updateRect();
std::string mPath;

View file

@ -13,6 +13,7 @@ GuiList<listType>::GuiList(int offsetX, int offsetY, Renderer::FontSize fontsize
mOffsetY = offsetY;
mFont = fontsize;
mSelectorColor = 0x000000;
InputManager::registerComponent(this);
}
@ -48,7 +49,7 @@ void GuiList<listType>::onRender()
if(mRowVector.size() == 0)
{
Renderer::drawCenteredText("The list is empty.", 0, y, 0xFF0000, mFont);
Renderer::drawCenteredText("The list is empty.", 0, y, 0x0000FF, mFont);
return;
}
@ -60,7 +61,7 @@ void GuiList<listType>::onRender()
{
if(mSelection == i)
{
Renderer::drawRect(mOffsetX, y, Renderer::getScreenWidth(), Renderer::getFontHeight(mFont), 0x000000);
Renderer::drawRect(mOffsetX, y, Renderer::getScreenWidth(), Renderer::getFontHeight(mFont), mSelectorColor);
}
ListRow row = mRowVector.at((unsigned int)i);
@ -188,3 +189,9 @@ void GuiList<listType>::onResume()
{
InputManager::registerComponent(this);
}
template <typename listType>
void GuiList<listType>::setSelectorColor(int selectorColor)
{
mSelectorColor = selectorColor;
}

View file

@ -31,11 +31,14 @@ public:
std::string getSelectedName();
listType getSelectedObject();
int getSelection();
void setSelectorColor(int selectorColor);
private:
int mScrollDir, mScrollAccumulator;
bool mScrolling;
Renderer::FontSize mFont;
int mSelectorColor;
int mOffsetX, mOffsetY;

View file

@ -3,9 +3,24 @@
#include <iostream>
#include "GuiImage.h"
#include <boost/filesystem.hpp>
#include <sstream>
int GuiTheme::getPrimaryColor() { return mListPrimaryColor; }
int GuiTheme::getSecondaryColor() { return mListSecondaryColor; }
int GuiTheme::getSelectorColor() { return mListSelectorColor; }
int GuiTheme::getDescColor() { return mDescColor; }
bool GuiTheme::getHeaderHidden() { return mHideHeader; }
bool GuiTheme::getDividersHidden() { return mHideDividers; }
GuiTheme::GuiTheme(std::string path)
{
mListPrimaryColor = 0x0000FF;
mListSecondaryColor = 0x00FF00;
mListSelectorColor = 0x000000;
mDescColor = 0x0000FF;
mHideHeader = false;
mHideDividers = false;
if(!path.empty())
readXML(path);
}
@ -55,6 +70,14 @@ void GuiTheme::readXML(std::string path)
pugi::xml_node root = doc.child("theme");
//load non-component theme stuff
mListPrimaryColor = resolveColor(root.child("listPrimaryColor").text().get(), 0x0000FF);
mListSecondaryColor = resolveColor(root.child("listSecondaryColor").text().get(), 0x00FF00);
mListSelectorColor = resolveColor(root.child("listSelectorColor").text().get(), 0x000000);
mDescColor = resolveColor(root.child("descColor").text().get(), 0x0000FF);
mHideHeader = root.child("hideHeader");
mHideDividers = root.child("hideDividers");
for(pugi::xml_node data = root.child("component"); data; data = data.next_sibling("component"))
{
createElement(data, this);
@ -79,17 +102,22 @@ GuiComponent* GuiTheme::createElement(pugi::xml_node data, GuiComponent* parent)
std::string pos = data.child("pos").text().get();
std::string dim = data.child("dim").text().get();
std::string origin = data.child("origin").text().get();
bool useAlpha = data.child("useAlpha");
bool tiled = data.child("tiled");
//split position and dimension information
size_t posSplit = pos.find(' ');
std::string posX = pos.substr(0, posSplit);
std::string posY = pos.substr(posSplit + 1, pos.length() - posSplit - 1);
std::string posX, posY;
splitString(pos, ' ', &posX, &posY);
size_t dimSplit = dim.find(' ');
std::string dimW = dim.substr(0, dimSplit);
std::string dimH = dim.substr(dimSplit + 1, dim.length() - dimSplit - 1);
std::string dimW, dimH;
splitString(dim, ' ', &dimW, &dimH);
std::cout << "image, x: " << posX << " y: " << posY << " w: " << dimW << " h: " << dimH << "\n";
std::string originX, originY;
splitString(origin, ' ', &originX, &originY);
std::cout << "image, x: " << posX << " y: " << posY << " w: " << dimW << " h: " << dimH << " ox: " << originX << " oy: " << originY << " alpha: " << useAlpha << " tiled: " << tiled << "\n";
//resolve to pixels from percentages/variables
int x = resolveExp(posX) * Renderer::getScreenWidth();
@ -97,9 +125,17 @@ GuiComponent* GuiTheme::createElement(pugi::xml_node data, GuiComponent* parent)
int w = resolveExp(dimW) * Renderer::getScreenWidth();
int h = resolveExp(dimH) * Renderer::getScreenHeight();
int ox = strToInt(originX);
int oy = strToInt(originY);
std::cout << "w: " << w << "px, h: " << h << "px\n";
GuiComponent* comp = new GuiImage(x, y, path, w, h, true);
GuiImage* comp = new GuiImage(x, y, "", w, h, true);
comp->setOrigin(ox, oy);
comp->setAlpha(useAlpha);
comp->setTiling(tiled);
comp->setImage(path);
parent->addChild(comp);
mComponentVector.push_back(comp);
return comp;
@ -130,3 +166,39 @@ float GuiTheme::resolveExp(std::string str)
return exp.eval();
}
int GuiTheme::resolveColor(std::string str, int defaultColor)
{
if(str.empty())
return defaultColor;
int ret;
std::stringstream ss;
ss << std::hex << str;
ss >> ret;
std::cout << "resolved " << str << " to " << ret << "\n";
return ret;
}
void GuiTheme::splitString(std::string str, char delim, std::string* before, std::string* after)
{
if(str.empty())
return;
size_t split = str.find(delim);
*before = str.substr(0, split);
*after = str.substr(split + 1, str.length() - split - 1);
}
int GuiTheme::strToInt(std::string str)
{
if(str.empty())
return 0;
int ret;
std::stringstream ss;
ss << str;
ss >> ret;
return ret;
}

View file

@ -12,14 +12,27 @@ public:
void readXML(std::string path);
int getPrimaryColor();
int getSecondaryColor();
int getSelectorColor();
int getDescColor();
bool getHeaderHidden();
bool getDividersHidden();
private:
void deleteComponents();
GuiComponent* createElement(pugi::xml_node data, GuiComponent* parent);
//utility functions
std::string expandPath(std::string path);
float resolveExp(std::string str);
int resolveColor(std::string str, int defaultColor = 0x000000);
void splitString(std::string str, char delim, std::string* before, std::string* after);
int strToInt(std::string str);
std::vector<GuiComponent*> mComponentVector;
std::string mPath;
int mListPrimaryColor, mListSecondaryColor, mListSelectorColor, mDescColor;
bool mHideHeader, mHideDividers;
};
#endif

View file

@ -17,6 +17,8 @@ int main(int argc, char* argv[])
{
bool running = true;
//by the way, if anyone ever tries to port this to a different renderer but leave SDL as input -
//KEEP INITIALIZING VIDEO. It starts SDL's event system, and without it, input won't work.
if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK) != 0)
{
std::cerr << "Error - could not initialize SDL!\n";