2013-11-12 23:28:15 +00:00
# include "ThemeData.h"
# include "Renderer.h"
# include "resources/Font.h"
# include "Sound.h"
# include "resources/TextureResource.h"
# include "Log.h"
2014-05-01 02:12:45 +00:00
# include "Settings.h"
2013-11-12 23:28:15 +00:00
# include "pugiXML/pugixml.hpp"
2013-12-30 23:23:34 +00:00
# include <boost/assign.hpp>
2013-11-12 23:28:15 +00:00
2014-01-03 14:26:39 +00:00
# include "components/ImageComponent.h"
# include "components/TextComponent.h"
2014-01-26 22:20:21 +00:00
// This is a work around for some ambiguity that is introduced in C++11 that boost::assign::map_list_of leave open.
// We use makeMap(actualmap) to implicitly convert the boost::assign::map_list_of's return type to ElementMapType.
// Problem exists with gcc 4.7 and Boost 1.51. Works fine with MSVC2010 without this hack.
typedef std : : map < std : : string , ThemeData : : ElementPropertyType > ElementMapType ;
template < typename T >
ElementMapType makeMap ( const T & mapInit )
{
ElementMapType m = mapInit ;
return m ;
}
std : : map < std : : string , ElementMapType > ThemeData : : sElementMap = boost : : assign : : map_list_of
( " image " , makeMap ( boost : : assign : : map_list_of
2013-12-30 23:23:34 +00:00
( " pos " , NORMALIZED_PAIR )
( " size " , NORMALIZED_PAIR )
2014-01-10 23:45:47 +00:00
( " maxSize " , NORMALIZED_PAIR )
2013-12-30 23:23:34 +00:00
( " origin " , NORMALIZED_PAIR )
( " path " , PATH )
2014-01-23 21:30:32 +00:00
( " tile " , BOOLEAN )
2014-01-26 22:20:21 +00:00
( " color " , COLOR ) ) )
( " text " , makeMap ( boost : : assign : : map_list_of
2013-12-30 23:23:34 +00:00
( " pos " , NORMALIZED_PAIR )
( " size " , NORMALIZED_PAIR )
( " text " , STRING )
( " color " , COLOR )
( " fontPath " , PATH )
( " fontSize " , FLOAT )
2014-05-03 19:51:50 +00:00
( " alignment " , STRING )
2014-05-14 22:01:40 +00:00
( " forceUppercase " , BOOLEAN )
( " lineSpacing " , FLOAT ) ) )
2014-01-26 22:20:21 +00:00
( " textlist " , makeMap ( boost : : assign : : map_list_of
2013-12-30 23:23:34 +00:00
( " pos " , NORMALIZED_PAIR )
( " size " , NORMALIZED_PAIR )
( " selectorColor " , COLOR )
( " selectedColor " , COLOR )
( " primaryColor " , COLOR )
( " secondaryColor " , COLOR )
( " fontPath " , PATH )
2014-01-03 16:40:36 +00:00
( " fontSize " , FLOAT )
2014-01-10 20:24:07 +00:00
( " scrollSound " , PATH )
2014-01-22 03:10:49 +00:00
( " alignment " , STRING )
2014-05-03 19:51:50 +00:00
( " horizontalMargin " , FLOAT )
2014-05-14 23:27:22 +00:00
( " forceUppercase " , BOOLEAN )
( " lineSpacing " , FLOAT ) ) )
2014-01-26 22:20:21 +00:00
( " container " , makeMap ( boost : : assign : : map_list_of
2014-01-03 14:26:39 +00:00
( " pos " , NORMALIZED_PAIR )
2014-01-26 22:20:21 +00:00
( " size " , NORMALIZED_PAIR ) ) )
( " ninepatch " , makeMap ( boost : : assign : : map_list_of
2014-01-06 19:27:34 +00:00
( " pos " , NORMALIZED_PAIR )
( " size " , NORMALIZED_PAIR )
2014-01-26 22:20:21 +00:00
( " path " , PATH ) ) )
( " datetime " , makeMap ( boost : : assign : : map_list_of
2014-01-19 22:06:13 +00:00
( " pos " , NORMALIZED_PAIR )
( " size " , NORMALIZED_PAIR )
( " color " , COLOR )
( " fontPath " , PATH )
2014-05-03 19:51:50 +00:00
( " fontSize " , FLOAT )
( " forceUppercase " , BOOLEAN ) ) )
2014-01-26 22:20:21 +00:00
( " rating " , makeMap ( boost : : assign : : map_list_of
2014-01-19 23:37:08 +00:00
( " pos " , NORMALIZED_PAIR )
( " size " , NORMALIZED_PAIR )
( " filledPath " , PATH )
2014-01-26 22:20:21 +00:00
( " unfilledPath " , PATH ) ) )
( " sound " , makeMap ( boost : : assign : : map_list_of
2014-05-29 20:41:47 +00:00
( " path " , PATH ) ) )
( " helpsystem " , makeMap ( boost : : assign : : map_list_of
( " pos " , NORMALIZED_PAIR )
( " textColor " , COLOR )
2014-05-29 21:27:18 +00:00
( " iconColor " , COLOR )
2014-05-29 20:41:47 +00:00
( " fontPath " , PATH )
( " fontSize " , FLOAT ) ) ) ;
2013-12-30 23:23:34 +00:00
namespace fs = boost : : filesystem ;
# define MINIMUM_THEME_VERSION 3
# define CURRENT_THEME_VERSION 3
2013-12-31 03:48:28 +00:00
// helper
unsigned int getHexColor ( const char * str )
{
ThemeException error ;
if ( ! str )
throw error < < " Empty color " ;
size_t len = strlen ( str ) ;
if ( len ! = 6 & & len ! = 8 )
throw error < < " Invalid color (bad length, \" " < < str < < " \" - must be 6 or 8) " ;
unsigned int val ;
std : : stringstream ss ;
ss < < str ;
ss > > std : : hex > > val ;
if ( len = = 6 )
val = ( val < < 8 ) | 0xFF ;
return val ;
}
// helper
std : : string resolvePath ( const char * in , const fs : : path & relative )
{
if ( ! in | | in [ 0 ] = = ' \0 ' )
return in ;
fs : : path relPath = relative . parent_path ( ) ;
boost : : filesystem : : path path ( in ) ;
// we use boost filesystem here instead of just string checks because
// some directories could theoretically start with ~ or .
if ( * path . begin ( ) = = " ~ " )
{
path = getHomePath ( ) + ( in + 1 ) ;
} else if ( * path . begin ( ) = = " . " )
{
path = relPath / ( in + 1 ) ;
}
return path . generic_string ( ) ;
}
2013-12-30 23:23:34 +00:00
ThemeData : : ThemeData ( )
2013-11-12 23:28:15 +00:00
{
2013-12-30 23:23:34 +00:00
mVersion = 0 ;
2013-11-12 23:28:15 +00:00
}
2013-12-30 23:23:34 +00:00
void ThemeData : : loadFile ( const std : : string & path )
2013-11-12 23:28:15 +00:00
{
2013-12-31 03:48:28 +00:00
mPaths . push_back ( path ) ;
2013-12-30 23:23:34 +00:00
2013-12-31 03:48:28 +00:00
ThemeException error ;
error . setFiles ( mPaths ) ;
2013-12-30 23:23:34 +00:00
if ( ! fs : : exists ( path ) )
2013-12-31 03:48:28 +00:00
throw error < < " File does not exist! " ;
2013-12-30 23:23:34 +00:00
mVersion = 0 ;
mViews . clear ( ) ;
pugi : : xml_document doc ;
pugi : : xml_parse_result res = doc . load_file ( path . c_str ( ) ) ;
if ( ! res )
throw error < < " XML parsing error: \n " < < res . description ( ) ;
pugi : : xml_node root = doc . child ( " theme " ) ;
if ( ! root )
throw error < < " Missing <theme> tag! " ;
// parse version
mVersion = root . child ( " version " ) . text ( ) . as_float ( - 404 ) ;
if ( mVersion = = - 404 )
throw error < < " <version> tag missing! \n It's either out of date or you need to add <version> " < < CURRENT_THEME_VERSION < < " </version> inside your <theme> tag. " ;
if ( mVersion < MINIMUM_THEME_VERSION )
throw error < < " Theme is version " < < mVersion < < " . Minimum supported version is " < < MINIMUM_THEME_VERSION < < " . " ;
2013-12-31 03:48:28 +00:00
parseIncludes ( root ) ;
parseViews ( root ) ;
2013-11-12 23:28:15 +00:00
}
2013-12-31 03:48:28 +00:00
void ThemeData : : parseIncludes ( const pugi : : xml_node & root )
2013-11-12 23:28:15 +00:00
{
2013-12-30 23:23:34 +00:00
ThemeException error ;
2013-12-31 03:48:28 +00:00
error . setFiles ( mPaths ) ;
2013-12-30 23:23:34 +00:00
2013-12-31 03:48:28 +00:00
for ( pugi : : xml_node node = root . child ( " include " ) ; node ; node = node . next_sibling ( " include " ) )
2013-12-30 23:23:34 +00:00
{
2013-12-31 03:48:28 +00:00
const char * relPath = node . text ( ) . get ( ) ;
std : : string path = resolvePath ( relPath , mPaths . back ( ) ) ;
if ( ! ResourceManager : : getInstance ( ) - > fileExists ( path ) )
throw error < < " Included file \" " < < relPath < < " \" not found! (resolved to \" " < < path < < " \" ) " ;
2013-12-30 23:23:34 +00:00
2013-12-31 03:48:28 +00:00
error < < " from included file \" " < < relPath < < " \" : \n " ;
2013-12-30 23:23:34 +00:00
2013-12-31 03:48:28 +00:00
mPaths . push_back ( path ) ;
2013-12-30 23:23:34 +00:00
2013-12-31 03:48:28 +00:00
pugi : : xml_document includeDoc ;
pugi : : xml_parse_result result = includeDoc . load_file ( path . c_str ( ) ) ;
if ( ! result )
throw error < < " Error parsing file: \n " < < result . description ( ) ;
pugi : : xml_node root = includeDoc . child ( " theme " ) ;
if ( ! root )
throw error < < " Missing <theme> tag! " ;
2013-12-30 23:23:34 +00:00
2013-12-31 03:48:28 +00:00
parseIncludes ( root ) ;
parseViews ( root ) ;
mPaths . pop_back ( ) ;
}
2013-11-12 23:28:15 +00:00
}
2013-12-31 03:48:28 +00:00
void ThemeData : : parseViews ( const pugi : : xml_node & root )
2013-11-12 23:28:15 +00:00
{
2013-12-30 23:23:34 +00:00
ThemeException error ;
2013-12-31 03:48:28 +00:00
error . setFiles ( mPaths ) ;
2013-11-12 23:28:15 +00:00
2013-12-31 03:48:28 +00:00
// parse views
for ( pugi : : xml_node node = root . child ( " view " ) ; node ; node = node . next_sibling ( " view " ) )
{
if ( ! node . attribute ( " name " ) )
throw error < < " View missing \" name \" attribute! " ;
2013-11-12 23:28:15 +00:00
2014-03-07 03:35:13 +00:00
const char * delim = " \t \r \n , " ;
const std : : string nameAttr = node . attribute ( " name " ) . as_string ( ) ;
size_t prevOff = nameAttr . find_first_not_of ( delim , 0 ) ;
size_t off = nameAttr . find_first_of ( delim , prevOff ) ;
std : : string viewKey ;
while ( off ! = std : : string : : npos | | prevOff ! = std : : string : : npos )
{
viewKey = nameAttr . substr ( prevOff , off - prevOff ) ;
prevOff = nameAttr . find_first_not_of ( delim , off ) ;
off = nameAttr . find_first_of ( delim , prevOff ) ;
ThemeView & view = mViews . insert ( std : : pair < std : : string , ThemeView > ( viewKey , ThemeView ( ) ) ) . first - > second ;
parseView ( node , view ) ;
}
2013-12-31 03:48:28 +00:00
}
2013-11-12 23:28:15 +00:00
}
2013-12-31 03:48:28 +00:00
void ThemeData : : parseView ( const pugi : : xml_node & root , ThemeView & view )
2013-11-12 23:28:15 +00:00
{
2013-12-31 03:48:28 +00:00
ThemeException error ;
error . setFiles ( mPaths ) ;
2013-11-12 23:28:15 +00:00
2013-12-31 03:48:28 +00:00
for ( pugi : : xml_node node = root . first_child ( ) ; node ; node = node . next_sibling ( ) )
2013-11-12 23:28:15 +00:00
{
2013-12-31 03:48:28 +00:00
if ( ! node . attribute ( " name " ) )
throw error < < " Element of type \" " < < node . name ( ) < < " \" missing \" name \" attribute! " ;
2013-11-12 23:28:15 +00:00
2013-12-31 03:48:28 +00:00
auto elemTypeIt = sElementMap . find ( node . name ( ) ) ;
if ( elemTypeIt = = sElementMap . end ( ) )
throw error < < " Unknown element of type \" " < < node . name ( ) < < " \" ! " ;
2014-03-07 03:35:13 +00:00
const char * delim = " \t \r \n , " ;
2014-01-25 01:25:15 +00:00
const std : : string nameAttr = node . attribute ( " name " ) . as_string ( ) ;
size_t prevOff = nameAttr . find_first_not_of ( delim , 0 ) ;
size_t off = nameAttr . find_first_of ( delim , prevOff ) ;
while ( off ! = std : : string : : npos | | prevOff ! = std : : string : : npos )
{
std : : string elemKey = nameAttr . substr ( prevOff , off - prevOff ) ;
prevOff = nameAttr . find_first_not_of ( delim , off ) ;
off = nameAttr . find_first_of ( delim , prevOff ) ;
parseElement ( node , elemTypeIt - > second ,
2014-01-26 22:20:21 +00:00
view . elements . insert ( std : : pair < std : : string , ThemeElement > ( elemKey , ThemeElement ( ) ) ) . first - > second ) ;
2014-01-25 01:25:15 +00:00
if ( std : : find ( view . orderedKeys . begin ( ) , view . orderedKeys . end ( ) , elemKey ) = = view . orderedKeys . end ( ) )
view . orderedKeys . push_back ( elemKey ) ;
}
2013-12-31 03:48:28 +00:00
}
2013-11-12 23:28:15 +00:00
}
2013-12-31 03:48:28 +00:00
void ThemeData : : parseElement ( const pugi : : xml_node & root , const std : : map < std : : string , ElementPropertyType > & typeMap , ThemeElement & element )
2013-12-30 23:23:34 +00:00
{
ThemeException error ;
2013-12-31 03:48:28 +00:00
error . setFiles ( mPaths ) ;
2013-11-12 23:28:15 +00:00
2014-01-01 05:39:22 +00:00
element . type = root . name ( ) ;
2013-12-30 23:23:34 +00:00
element . extra = root . attribute ( " extra " ) . as_bool ( false ) ;
2014-01-03 14:26:39 +00:00
2013-12-30 23:23:34 +00:00
for ( pugi : : xml_node node = root . first_child ( ) ; node ; node = node . next_sibling ( ) )
2013-11-12 23:28:15 +00:00
{
2013-12-30 23:23:34 +00:00
auto typeIt = typeMap . find ( node . name ( ) ) ;
if ( typeIt = = typeMap . end ( ) )
throw error < < " Unknown property type \" " < < node . name ( ) < < " \" (for element of type " < < root . name ( ) < < " ). " ;
2013-11-12 23:28:15 +00:00
2013-12-30 23:23:34 +00:00
switch ( typeIt - > second )
2013-11-12 23:28:15 +00:00
{
2013-12-30 23:23:34 +00:00
case NORMALIZED_PAIR :
2013-11-12 23:28:15 +00:00
{
2013-12-30 23:23:34 +00:00
std : : string str = std : : string ( node . text ( ) . as_string ( ) ) ;
2013-11-12 23:28:15 +00:00
2013-12-30 23:23:34 +00:00
size_t divider = str . find ( ' ' ) ;
if ( divider = = std : : string : : npos )
2014-01-25 01:25:15 +00:00
throw error < < " invalid normalized pair (property \" " < < node . name ( ) < < " \" , value \" " < < str . c_str ( ) < < " \" ) " ;
2013-11-12 23:28:15 +00:00
2013-12-30 23:23:34 +00:00
std : : string first = str . substr ( 0 , divider ) ;
std : : string second = str . substr ( divider , std : : string : : npos ) ;
Eigen : : Vector2f val ( atof ( first . c_str ( ) ) , atof ( second . c_str ( ) ) ) ;
element . properties [ node . name ( ) ] = val ;
break ;
}
case STRING :
element . properties [ node . name ( ) ] = std : : string ( node . text ( ) . as_string ( ) ) ;
break ;
case PATH :
2013-11-12 23:28:15 +00:00
{
2013-12-31 03:48:28 +00:00
std : : string path = resolvePath ( node . text ( ) . as_string ( ) , mPaths . back ( ) . string ( ) ) ;
if ( ! ResourceManager : : getInstance ( ) - > fileExists ( path ) )
{
std : : stringstream ss ;
ss < < " Warning " < < error . msg ; // "from theme yadda yadda, included file yadda yadda
ss < < " could not find file \" " < < node . text ( ) . get ( ) < < " \" " ;
if ( node . text ( ) . get ( ) ! = path )
ss < < " (which resolved to \" " < < path < < " \" ) " ;
LOG ( LogWarning ) < < ss . str ( ) ;
}
2013-12-30 23:23:34 +00:00
element . properties [ node . name ( ) ] = path ;
break ;
}
case COLOR :
element . properties [ node . name ( ) ] = getHexColor ( node . text ( ) . as_string ( ) ) ;
break ;
case FLOAT :
element . properties [ node . name ( ) ] = node . text ( ) . as_float ( ) ;
break ;
case BOOLEAN :
element . properties [ node . name ( ) ] = node . text ( ) . as_bool ( ) ;
break ;
default :
2014-01-25 01:25:15 +00:00
throw error < < " Unknown ElementPropertyType for \" " < < root . attribute ( " name " ) . as_string ( ) < < " \" , property " < < node . name ( ) ;
2013-11-12 23:28:15 +00:00
}
}
}
2013-12-31 03:48:28 +00:00
2014-01-01 05:39:22 +00:00
const ThemeData : : ThemeElement * ThemeData : : getElement ( const std : : string & view , const std : : string & element , const std : : string & expectedType ) const
{
auto viewIt = mViews . find ( view ) ;
if ( viewIt = = mViews . end ( ) )
2014-03-07 03:35:13 +00:00
return NULL ; // not found
2014-01-01 05:39:22 +00:00
auto elemIt = viewIt - > second . elements . find ( element ) ;
if ( elemIt = = viewIt - > second . elements . end ( ) ) return NULL ;
if ( elemIt - > second . type ! = expectedType & & ! expectedType . empty ( ) )
{
LOG ( LogWarning ) < < " requested mismatched theme type for [ " < < view < < " . " < < element < < " ] - expected \" "
< < expectedType < < " \" , got \" " < < elemIt - > second . type < < " \" " ;
return NULL ;
}
return & elemIt - > second ;
}
2014-01-03 16:40:36 +00:00
const std : : shared_ptr < ThemeData > & ThemeData : : getDefault ( )
2014-01-01 05:39:22 +00:00
{
2014-01-03 16:40:36 +00:00
static std : : shared_ptr < ThemeData > theme = nullptr ;
if ( theme = = nullptr )
2014-01-01 05:39:22 +00:00
{
2014-01-03 16:40:36 +00:00
theme = std : : shared_ptr < ThemeData > ( new ThemeData ( ) ) ;
2014-01-10 20:58:03 +00:00
const std : : string path = getHomePath ( ) + " /.emulationstation/es_theme_default.xml " ;
if ( fs : : exists ( path ) )
{
try
{
theme - > loadFile ( path ) ;
} catch ( ThemeException & e )
{
LOG ( LogError ) < < e . what ( ) ;
theme = std : : shared_ptr < ThemeData > ( new ThemeData ( ) ) ; //reset to empty
}
}
2014-01-01 05:39:22 +00:00
}
2014-01-03 14:26:39 +00:00
2014-01-03 16:40:36 +00:00
return theme ;
}
2014-01-03 14:26:39 +00:00
std : : vector < GuiComponent * > ThemeData : : makeExtras ( const std : : shared_ptr < ThemeData > & theme , const std : : string & view , Window * window )
{
std : : vector < GuiComponent * > comps ;
auto viewIt = theme - > mViews . find ( view ) ;
if ( viewIt = = theme - > mViews . end ( ) )
return comps ;
2014-01-09 23:13:52 +00:00
for ( auto it = viewIt - > second . orderedKeys . begin ( ) ; it ! = viewIt - > second . orderedKeys . end ( ) ; it + + )
2014-01-03 14:26:39 +00:00
{
2014-01-09 23:13:52 +00:00
ThemeElement & elem = viewIt - > second . elements . at ( * it ) ;
if ( elem . extra )
2014-01-03 14:26:39 +00:00
{
GuiComponent * comp = NULL ;
2014-01-09 23:13:52 +00:00
const std : : string & t = elem . type ;
2014-01-03 14:26:39 +00:00
if ( t = = " image " )
comp = new ImageComponent ( window ) ;
else if ( t = = " text " )
comp = new TextComponent ( window ) ;
2014-01-09 23:13:52 +00:00
comp - > applyTheme ( theme , view , * it , ThemeFlags : : ALL ) ;
2014-01-03 14:26:39 +00:00
comps . push_back ( comp ) ;
}
}
return comps ;
}
void ThemeExtras : : setExtras ( const std : : vector < GuiComponent * > & extras )
{
// delete old extras (if any)
for ( auto it = mExtras . begin ( ) ; it ! = mExtras . end ( ) ; it + + )
delete * it ;
mExtras = extras ;
for ( auto it = mExtras . begin ( ) ; it ! = mExtras . end ( ) ; it + + )
addChild ( * it ) ;
}
ThemeExtras : : ~ ThemeExtras ( )
{
for ( auto it = mExtras . begin ( ) ; it ! = mExtras . end ( ) ; it + + )
delete * it ;
}
2014-05-01 02:12:45 +00:00
std : : map < std : : string , ThemeSet > ThemeData : : getThemeSets ( )
{
std : : map < std : : string , ThemeSet > sets ;
static const size_t pathCount = 2 ;
fs : : path paths [ pathCount ] = {
" /etc/emulationstation/themes " ,
getHomePath ( ) + " /.emulationstation/themes "
} ;
fs : : directory_iterator end ;
for ( size_t i = 0 ; i < pathCount ; i + + )
{
if ( ! fs : : is_directory ( paths [ i ] ) )
continue ;
for ( fs : : directory_iterator it ( paths [ i ] ) ; it ! = end ; + + it )
{
if ( fs : : is_directory ( * it ) )
{
ThemeSet set = { * it } ;
sets [ set . getName ( ) ] = set ;
}
}
}
return sets ;
}
fs : : path ThemeData : : getThemeFromCurrentSet ( const std : : string & system )
{
auto themeSets = ThemeData : : getThemeSets ( ) ;
if ( themeSets . empty ( ) )
{
// no theme sets available
return " " ;
}
auto set = themeSets . find ( Settings : : getInstance ( ) - > getString ( " ThemeSet " ) ) ;
if ( set = = themeSets . end ( ) )
{
// currently selected theme set is missing, so just pick the first available set
set = themeSets . begin ( ) ;
Settings : : getInstance ( ) - > setString ( " ThemeSet " , set - > first ) ;
}
return set - > second . getThemePath ( system ) ;
}