* @link https://github.com/pradosoft/prado
* @copyright Copyright © 2005-2015 The PRADO Group
* @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
* @package System.Web.UI
*/
Prado::using('System.Web.Services.TPageService');
/**
* TThemeManager class
*
* TThemeManager manages the themes used in a Prado application.
*
* Themes are stored under the directory specified by the
* {@link setBasePath BasePath} property. The themes can be accessed
* via URL {@link setBaseUrl BaseUrl}. Each theme is represented by a subdirectory
* and all the files under that directory. The name of a theme is the name
* of the corresponding subdirectory.
* By default, the base path of all themes is a directory named "themes"
* under the directory containing the application entry script.
* To get a theme (normally you do not need to), call {@link getTheme}.
*
* TThemeManager may be configured within page service tag in application
* configuration file as follows,
*
* where {@link getCacheExpire CacheExpire}, {@link getCacheControl CacheControl}
* and {@link getBufferOutput BufferOutput} are configurable properties of THttpResponse.
*
* @author Qiang Xue
* @package System.Web.UI
* @since 3.0
*/
class TThemeManager extends TModule
{
/**
* default themes base path
*/
const DEFAULT_BASEPATH='themes';
/**
* default theme class
*/
const DEFAULT_THEMECLASS = 'TTheme';
/**
* @var string
*/
private $_themeClass=self::DEFAULT_THEMECLASS;
/**
* @var boolean whether this module has been initialized
*/
private $_initialized=false;
/**
* @var string the directory containing all themes
*/
private $_basePath=null;
/**
* @var string the base URL for all themes
*/
private $_baseUrl=null;
/**
* Initializes the module.
* This method is required by IModule and is invoked by application.
* @param TXmlElement module configuration
*/
public function init($config)
{
$this->_initialized=true;
$service=$this->getService();
if($service instanceof TPageService)
$service->setThemeManager($this);
else
throw new TConfigurationException('thememanager_service_unavailable');
}
/**
* @param string name of the theme to be retrieved
* @return TTheme the theme retrieved
*/
public function getTheme($name)
{
$themePath=$this->getBasePath().DIRECTORY_SEPARATOR.$name;
$themeUrl=rtrim($this->getBaseUrl(),'/').'/'.$name;
return Prado::createComponent($this->getThemeClass(), $themePath, $themeUrl);
}
/**
* @param string|null $class Theme class name in namespace format
*/
public function setThemeClass($class) {
$this->_themeClass = $class===null ? self::DEFAULT_THEMECLASS : (string)$class;
}
/**
* @return string Theme class name in namespace format. Defaults to {@link TThemeManager::DEFAULT_THEMECLASS DEFAULT_THEMECLASS}.
*/
public function getThemeClass() {
return $this->_themeClass;
}
/**
* @return array list of available theme names
*/
public function getAvailableThemes()
{
$themes=array();
$basePath=$this->getBasePath();
$folder=@opendir($basePath);
while($file=@readdir($folder))
{
if($file!=='.' && $file!=='..' && $file!=='.svn' && is_dir($basePath.DIRECTORY_SEPARATOR.$file))
$themes[]=$file;
}
closedir($folder);
return $themes;
}
/**
* @return string the base path for all themes. It is returned as an absolute path.
* @throws TConfigurationException if base path is not set and "themes" directory does not exist.
*/
public function getBasePath()
{
if($this->_basePath===null)
{
$this->_basePath=dirname($this->getRequest()->getApplicationFilePath()).DIRECTORY_SEPARATOR.self::DEFAULT_BASEPATH;
if(($basePath=realpath($this->_basePath))===false || !is_dir($basePath))
throw new TConfigurationException('thememanager_basepath_invalid2',$this->_basePath);
$this->_basePath=$basePath;
}
return $this->_basePath;
}
/**
* @param string the base path for all themes. It must be in the format of a namespace.
* @throws TInvalidDataValueException if the base path is not a proper namespace.
*/
public function setBasePath($value)
{
if($this->_initialized)
throw new TInvalidOperationException('thememanager_basepath_unchangeable');
else
{
$this->_basePath=Prado::getPathOfNamespace($value);
if($this->_basePath===null || !is_dir($this->_basePath))
throw new TInvalidDataValueException('thememanager_basepath_invalid',$value);
}
}
/**
* @return string the base URL for all themes.
* @throws TConfigurationException If base URL is not set and a correct one cannot be determined by Prado.
*/
public function getBaseUrl()
{
if($this->_baseUrl===null)
{
$appPath=dirname($this->getRequest()->getApplicationFilePath());
$basePath=$this->getBasePath();
if(strpos($basePath,$appPath)===false)
throw new TConfigurationException('thememanager_baseurl_required');
$appUrl=rtrim(dirname($this->getRequest()->getApplicationUrl()),'/\\');
$this->_baseUrl=$appUrl.strtr(substr($basePath,strlen($appPath)),'\\','/');
}
return $this->_baseUrl;
}
/**
* @param string the base URL for all themes.
*/
public function setBaseUrl($value)
{
$this->_baseUrl=rtrim($value,'/');
}
}
/**
* TTheme class
*
* TTheme represents a particular theme. It is merely a collection of skins
* that are applicable to the corresponding controls.
*
* Each theme is stored as a directory and files under that directory.
* The theme name is the directory name. When TTheme is created, the files
* whose name has the extension ".skin" are parsed and saved as controls skins.
*
* A skin is essentially a list of initial property values that are to be applied
* to a control when the skin is applied.
* Each type of control can have multiple skins identified by the SkinID.
* If a skin does not have SkinID, it is the default skin that will be applied
* to controls that do not specify particular SkinID.
*
* Whenever possible, TTheme will try to make use of available cache to save
* the parsing time.
*
* To apply a theme to a particular control, call {@link applySkin}.
*
* @author Qiang Xue
* @package System.Web.UI
* @since 3.0
*/
class TTheme extends TApplicationComponent implements ITheme
{
/**
* prefix for cache variable name used to store parsed themes
*/
const THEME_CACHE_PREFIX='prado:theme:';
/**
* Extension name of skin files
*/
const SKIN_FILE_EXT='.skin';
/**
* @var string theme path
*/
private $_themePath;
/**
* @var string theme url
*/
private $_themeUrl;
/**
* @var array list of skins for the theme
*/
private $_skins=null;
/**
* @var string theme name
*/
private $_name='';
/**
* @var array list of css files
*/
private $_cssFiles=array();
/**
* @var array list of js files
*/
private $_jsFiles=array();
/**
* Constructor.
* @param string theme path
* @param string theme URL
* @throws TConfigurationException if theme path does not exist or any parsing error of the skin files
*/
public function __construct($themePath,$themeUrl)
{
$this->_themeUrl=$themeUrl;
$this->_themePath=realpath($themePath);
$this->_name=basename($themePath);
$cacheValid=false;
// TODO: the following needs to be cleaned up (Qiang)
if(($cache=$this->getApplication()->getCache())!==null)
{
$array=$cache->get(self::THEME_CACHE_PREFIX.$themePath);
if(is_array($array))
{
list($skins,$cssFiles,$jsFiles,$timestamp)=$array;
if($this->getApplication()->getMode()!==TApplicationMode::Performance)
{
if(($dir=opendir($themePath))===false)
throw new TIOException('theme_path_inexistent',$themePath);
$cacheValid=true;
while(($file=readdir($dir))!==false)
{
if($file==='.' || $file==='..')
continue;
else if(basename($file,'.css')!==$file)
$this->_cssFiles[]=$themeUrl.'/'.$file;
else if(basename($file,'.js')!==$file)
$this->_jsFiles[]=$themeUrl.'/'.$file;
else if(basename($file,self::SKIN_FILE_EXT)!==$file && filemtime($themePath.DIRECTORY_SEPARATOR.$file)>$timestamp)
{
$cacheValid=false;
break;
}
}
closedir($dir);
if($cacheValid)
$this->_skins=$skins;
}
else
{
$cacheValid=true;
$this->_cssFiles=$cssFiles;
$this->_jsFiles=$jsFiles;
$this->_skins=$skins;
}
}
}
if(!$cacheValid)
{
$this->_cssFiles=array();
$this->_jsFiles=array();
$this->_skins=array();
if(($dir=opendir($themePath))===false)
throw new TIOException('theme_path_inexistent',$themePath);
while(($file=readdir($dir))!==false)
{
if($file==='.' || $file==='..')
continue;
else if(basename($file,'.css')!==$file)
$this->_cssFiles[]=$themeUrl.'/'.$file;
else if(basename($file,'.js')!==$file)
$this->_jsFiles[]=$themeUrl.'/'.$file;
else if(basename($file,self::SKIN_FILE_EXT)!==$file)
{
$template=new TTemplate(file_get_contents($themePath.'/'.$file),$themePath,$themePath.'/'.$file);
foreach($template->getItems() as $skin)
{
if(!isset($skin[2])) // a text string, ignored
continue;
else if($skin[0]!==-1)
throw new TConfigurationException('theme_control_nested',$skin[1],dirname($themePath));
$type=$skin[1];
$id=isset($skin[2]['skinid'])?$skin[2]['skinid']:0;
unset($skin[2]['skinid']);
if(isset($this->_skins[$type][$id]))
throw new TConfigurationException('theme_skinid_duplicated',$type,$id,dirname($themePath));
/*
foreach($skin[2] as $name=>$value)
{
if(is_array($value) && ($value[0]===TTemplate::CONFIG_DATABIND || $value[0]===TTemplate::CONFIG_PARAMETER))
throw new TConfigurationException('theme_databind_forbidden',dirname($themePath),$type,$id);
}
*/
$this->_skins[$type][$id]=$skin[2];
}
}
}
closedir($dir);
sort($this->_cssFiles);
sort($this->_jsFiles);
if($cache!==null)
$cache->set(self::THEME_CACHE_PREFIX.$themePath,array($this->_skins,$this->_cssFiles,$this->_jsFiles,time()));
}
}
/**
* @return string theme name
*/
public function getName()
{
return $this->_name;
}
/**
* @param string theme name
*/
protected function setName($value)
{
$this->_name = $value;
}
/**
* @return string the URL to the theme folder (without ending slash)
*/
public function getBaseUrl()
{
return $this->_themeUrl;
}
/**
* @param string the URL to the theme folder
*/
protected function setBaseUrl($value)
{
$this->_themeUrl=rtrim($value,'/');
}
/**
* @return string the file path to the theme folder
*/
public function getBasePath()
{
return $this->_themePath;
}
/**
* @param string tthe file path to the theme folder
*/
protected function setBasePath($value)
{
$this->_themePath=$value;
}
/**
* @return array list of skins for the theme
*/
public function getSkins()
{
return $this->_skins;
}
/**
* @param array list of skins for the theme
*/
protected function setSkins($value)
{
$this->_skins = $value;
}
/**
* Applies the theme to a particular control.
* The control's class name and SkinID value will be used to
* identify which skin to be applied. If the control's SkinID is empty,
* the default skin will be applied.
* @param TControl the control to be applied with a skin
* @return boolean if a skin is successfully applied
* @throws TConfigurationException if any error happened during the skin application
*/
public function applySkin($control)
{
$type=get_class($control);
if(($id=$control->getSkinID())==='')
$id=0;
if(isset($this->_skins[$type][$id]))
{
foreach($this->_skins[$type][$id] as $name=>$value)
{
Prado::trace("Applying skin $name to $type",'System.Web.UI.TThemeManager');
if(is_array($value))
{
switch($value[0])
{
case TTemplate::CONFIG_EXPRESSION:
$value=$this->evaluateExpression($value[1]);
break;
case TTemplate::CONFIG_ASSET:
$value=$this->_themeUrl.'/'.ltrim($value[1],'/');
break;
case TTemplate::CONFIG_DATABIND:
$control->bindProperty($name,$value[1]);
break;
case TTemplate::CONFIG_PARAMETER:
$control->setSubProperty($name,$this->getApplication()->getParameters()->itemAt($value[1]));
break;
case TTemplate::CONFIG_TEMPLATE:
$control->setSubProperty($name,$value[1]);
break;
case TTemplate::CONFIG_LOCALIZATION:
$control->setSubProperty($name,Prado::localize($value[1]));
break;
default:
throw new TConfigurationException('theme_tag_unexpected',$name,$value[0]);
break;
}
}
if(!is_array($value))
{
if(strpos($name,'.')===false) // is simple property or custom attribute
{
if($control->hasProperty($name))
{
if($control->canSetProperty($name))
{
$setter='set'.$name;
$control->$setter($value);
}
else
throw new TConfigurationException('theme_property_readonly',$type,$name);
}
else
throw new TConfigurationException('theme_property_undefined',$type,$name);
}
else // complex property
$control->setSubProperty($name,$value);
}
}
return true;
}
else
return false;
}
/**
* @return array list of CSS files (URL) in the theme
*/
public function getStyleSheetFiles()
{
return $this->_cssFiles;
}
/**
* @param array list of CSS files (URL) in the theme
*/
protected function setStyleSheetFiles($value)
{
$this->_cssFiles=$value;
}
/**
* @return array list of Javascript files (URL) in the theme
*/
public function getJavaScriptFiles()
{
return $this->_jsFiles;
}
/**
* @param array list of Javascript files (URL) in the theme
*/
protected function setJavaScriptFiles($value)
{
$this->_jsFiles=$value;
}
}