root/trunk/framework/Web/UI/TThemeManager.php

Revision 2497, 13.7 kB (checked in by knut, 3 months ago)

fixed #896

  • Property svn:keywords set to Id
Line 
1<?php
2/**
3 * TThemeManager class
4 *
5 * @author Qiang Xue <qiang.xue@gmail.com>
6 * @link http://www.pradosoft.com/
7 * @copyright Copyright &copy; 2005-2008 PradoSoft
8 * @license http://www.pradosoft.com/license/
9 * @version $Id$
10 * @package System.Web.UI
11 */
12
13Prado::using('System.Web.Services.TPageService');
14
15/**
16 * TThemeManager class
17 *
18 * TThemeManager manages the themes used in a Prado application.
19 *
20 * Themes are stored under the directory specified by the
21 * {@link setBasePath BasePath} property. The themes can be accessed
22 * via URL {@link setBaseUrl BaseUrl}. Each theme is represented by a subdirectory
23 * and all the files under that directory. The name of a theme is the name
24 * of the corresponding subdirectory.
25 * By default, the base path of all themes is a directory named "themes"
26 * under the directory containing the application entry script.
27 * To get a theme (normally you do not need to), call {@link getTheme}.
28 *
29 * TThemeManager may be configured within page service tag in application
30 * configuration file as follows,
31 * <module id="themes" class="System.Web.UI.TThemeManager"
32 *         BasePath="Application.themes" BaseUrl="/themes" />
33 * where {@link getCacheExpire CacheExpire}, {@link getCacheControl CacheControl}
34 * and {@link getBufferOutput BufferOutput} are configurable properties of THttpResponse.
35 *
36 * @author Qiang Xue <qiang.xue@gmail.com>
37 * @version $Id$
38 * @package System.Web.UI
39 * @since 3.0
40 */
41class TThemeManager extends TModule
42{
43    /**
44     * default themes base path
45     */
46    const DEFAULT_BASEPATH='themes';
47    /**
48     * @var boolean whether this module has been initialized
49     */
50    private $_initialized=false;
51    /**
52     * @var string the directory containing all themes
53     */
54    private $_basePath=null;
55    /**
56     * @var string the base URL for all themes
57     */
58    private $_baseUrl=null;
59
60    /**
61     * Initializes the module.
62     * This method is required by IModule and is invoked by application.
63     * @param TXmlElement module configuration
64     */
65    public function init($config)
66    {
67        $this->_initialized=true;
68        $service=$this->getService();
69        if($service instanceof TPageService)
70            $service->setThemeManager($this);
71        else
72            throw new TConfigurationException('thememanager_service_unavailable');
73    }
74
75    /**
76     * @param string name of the theme to be retrieved
77     * @return TTheme the theme retrieved
78     */
79    public function getTheme($name)
80    {
81        $themePath=$this->getBasePath().DIRECTORY_SEPARATOR.$name;
82        $themeUrl=rtrim($this->getBaseUrl(),'/').'/'.$name;
83        return new TTheme($themePath,$themeUrl);
84
85    }
86
87    /**
88     * @return array list of available theme names
89     */
90    public function getAvailableThemes()
91    {
92        $themes=array();
93        $basePath=$this->getBasePath();
94        $folder=@opendir($basePath);
95        while($file=@readdir($folder))
96        {
97            if($file!=='.' && $file!=='..' && $file!=='.svn' && is_dir($basePath.DIRECTORY_SEPARATOR.$file))
98                $themes[]=$file;
99        }
100        closedir($folder);
101        return $themes;
102    }
103
104    /**
105     * @return string the base path for all themes. It is returned as an absolute path.
106     * @throws TConfigurationException if base path is not set and "themes" directory does not exist.
107     */
108    public function getBasePath()
109    {
110        if($this->_basePath===null)
111        {
112            $this->_basePath=dirname($this->getRequest()->getApplicationFilePath()).DIRECTORY_SEPARATOR.self::DEFAULT_BASEPATH;
113            if(($basePath=realpath($this->_basePath))===false || !is_dir($basePath))
114                throw new TConfigurationException('thememanager_basepath_invalid2',$this->_basePath);
115            $this->_basePath=$basePath;
116        }
117        return $this->_basePath;
118    }
119
120    /**
121     * @param string the base path for all themes. It must be in the format of a namespace.
122     * @throws TInvalidDataValueException if the base path is not a proper namespace.
123     */
124    public function setBasePath($value)
125    {
126        if($this->_initialized)
127            throw new TInvalidOperationException('thememanager_basepath_unchangeable');
128        else
129        {
130            $this->_basePath=Prado::getPathOfNamespace($value);
131            if($this->_basePath===null || !is_dir($this->_basePath))
132                throw new TInvalidDataValueException('thememanager_basepath_invalid',$value);
133        }
134    }
135
136    /**
137     * @return string the base URL for all themes.
138     * @throws TConfigurationException If base URL is not set and a correct one cannot be determined by Prado.
139     */
140    public function getBaseUrl()
141    {
142        if($this->_baseUrl===null)
143        {
144            $appPath=dirname($this->getRequest()->getApplicationFilePath());
145            $basePath=$this->getBasePath();
146            if(strpos($basePath,$appPath)===false)
147                throw new TConfigurationException('thememanager_baseurl_required');
148            $appUrl=rtrim(dirname($this->getRequest()->getApplicationUrl()),'/\\');
149            $this->_baseUrl=$appUrl.strtr(substr($basePath,strlen($appPath)),'\\','/');
150        }
151        return $this->_baseUrl;
152    }
153
154    /**
155     * @param string the base URL for all themes.
156     */
157    public function setBaseUrl($value)
158    {
159        $this->_baseUrl=rtrim($value,'/');
160    }
161}
162
163/**
164 * TTheme class
165 *
166 * TTheme represents a particular theme. It is merely a collection of skins
167 * that are applicable to the corresponding controls.
168 *
169 * Each theme is stored as a directory and files under that directory.
170 * The theme name is the directory name. When TTheme is created, the files
171 * whose name has the extension ".skin" are parsed and saved as controls skins.
172 *
173 * A skin is essentially a list of initial property values that are to be applied
174 * to a control when the skin is applied.
175 * Each type of control can have multiple skins identified by the SkinID.
176 * If a skin does not have SkinID, it is the default skin that will be applied
177 * to controls that do not specify particular SkinID.
178 *
179 * Whenever possible, TTheme will try to make use of available cache to save
180 * the parsing time.
181 *
182 * To apply a theme to a particular control, call {@link applySkin}.
183 *
184 * @author Qiang Xue <qiang.xue@gmail.com>
185 * @version $Id$
186 * @package System.Web.UI
187 * @since 3.0
188 */
189class TTheme extends TApplicationComponent implements ITheme
190{
191    /**
192     * prefix for cache variable name used to store parsed themes
193     */
194    const THEME_CACHE_PREFIX='prado:theme:';
195    /**
196     * Extension name of skin files
197     */
198    const SKIN_FILE_EXT='.skin';
199    /**
200     * @var string theme path
201     */
202    private $_themePath;
203    /**
204     * @var string theme url
205     */
206    private $_themeUrl;
207    /**
208     * @var array list of skins for the theme
209     */
210    private $_skins=null;
211    /**
212     * @var string theme name
213     */
214    private $_name='';
215    /**
216     * @var array list of css files
217     */
218    private $_cssFiles=array();
219    /**
220     * @var array list of js files
221     */
222    private $_jsFiles=array();
223
224    /**
225     * Constructor.
226     * @param string theme path
227     * @param string theme URL
228     * @throws TConfigurationException if theme path does not exist or any parsing error of the skin files
229     */
230    public function __construct($themePath,$themeUrl)
231    {
232        $this->_themeUrl=$themeUrl;
233        $this->_themePath=realpath($themePath);
234        $this->_name=basename($themePath);
235        $cacheValid=false;
236        // TODO: the following needs to be cleaned up (Qiang)
237        if(($cache=$this->getApplication()->getCache())!==null)
238        {
239            $array=$cache->get(self::THEME_CACHE_PREFIX.$themePath);
240            if(is_array($array))
241            {
242                list($skins,$cssFiles,$jsFiles,$timestamp)=$array;
243                if($this->getApplication()->getMode()!==TApplicationMode::Performance)
244                {
245                    if(($dir=opendir($themePath))===false)
246                        throw new TIOException('theme_path_inexistent',$themePath);
247                    $cacheValid=true;
248                    while(($file=readdir($dir))!==false)
249                    {
250                        if($file==='.' || $file==='..')
251                            continue;
252                        else if(basename($file,'.css')!==$file)
253                            $this->_cssFiles[]=$themeUrl.'/'.$file;
254                        else if(basename($file,'.js')!==$file)
255                            $this->_jsFiles[]=$themeUrl.'/'.$file;
256                        else if(basename($file,self::SKIN_FILE_EXT)!==$file && filemtime($themePath.DIRECTORY_SEPARATOR.$file)>$timestamp)
257                        {
258                            $cacheValid=false;
259                            break;
260                        }
261                    }
262                    closedir($dir);
263                    if($cacheValid)
264                        $this->_skins=$skins;
265                }
266                else
267                {
268                    $cacheValid=true;
269                    $this->_cssFiles=$cssFiles;
270                    $this->_jsFiles=$jsFiles;
271                    $this->_skins=$skins;
272                }
273            }
274        }
275        if(!$cacheValid)
276        {
277            $this->_cssFiles=array();
278            $this->_jsFiles=array();
279            $this->_skins=array();
280            if(($dir=opendir($themePath))===false)
281                throw new TIOException('theme_path_inexistent',$themePath);
282            while(($file=readdir($dir))!==false)
283            {
284                if($file==='.' || $file==='..')
285                    continue;
286                else if(basename($file,'.css')!==$file)
287                    $this->_cssFiles[]=$themeUrl.'/'.$file;
288                else if(basename($file,'.js')!==$file)
289                    $this->_jsFiles[]=$themeUrl.'/'.$file;
290                else if(basename($file,self::SKIN_FILE_EXT)!==$file)
291                {
292                    $template=new TTemplate(file_get_contents($themePath.'/'.$file),$themePath,$themePath.'/'.$file);
293                    foreach($template->getItems() as $skin)
294                    {
295                        if(!isset($skin[2]))  // a text string, ignored
296                            continue;
297                        else if($skin[0]!==-1)
298                            throw new TConfigurationException('theme_control_nested',$skin[1],dirname($themePath));
299                        $type=$skin[1];
300                        $id=isset($skin[2]['skinid'])?$skin[2]['skinid']:0;
301                        unset($skin[2]['skinid']);
302                        if(isset($this->_skins[$type][$id]))
303                            throw new TConfigurationException('theme_skinid_duplicated',$type,$id,dirname($themePath));
304                        /*
305                        foreach($skin[2] as $name=>$value)
306                        {
307                            if(is_array($value) && ($value[0]===TTemplate::CONFIG_DATABIND || $value[0]===TTemplate::CONFIG_PARAMETER))
308                                throw new TConfigurationException('theme_databind_forbidden',dirname($themePath),$type,$id);
309                        }
310                        */
311                        $this->_skins[$type][$id]=$skin[2];
312                    }
313                }
314            }
315            closedir($dir);
316            sort($this->_cssFiles);
317            sort($this->_jsFiles);
318            if($cache!==null)
319                $cache->set(self::THEME_CACHE_PREFIX.$themePath,array($this->_skins,$this->_cssFiles,$this->_jsFiles,time()));
320        }
321    }
322
323    /**
324     * @return string theme name
325     */
326    public function getName()
327    {
328        return $this->_name;
329    }
330
331     /**
332     * @param string theme name
333     */
334    protected function setName($value)
335    {
336        $this->_name = $value;
337    }
338
339    /**
340     * @return string the URL to the theme folder (without ending slash)
341     */
342    public function getBaseUrl()
343    {
344        return $this->_themeUrl;
345    }
346
347     /**
348     * @param string the URL to the theme folder
349     */
350    protected function setBaseUrl($value)
351    {
352        $this->_themeUrl=rtrim($value,'/');
353    }
354
355    /**
356     * @return string the file path to the theme folder
357     */
358    public function getBasePath()
359    {
360        return $this->_themePath;
361    }
362
363     /**
364     * @param string tthe file path to the theme folder
365     */
366    protected function setBasePath($value)
367    {
368        $this->_themePath=$value;
369    }
370   
371    /**
372     * @return array list of skins for the theme
373     */
374    public function getSkins()
375    {
376        return $this->_skins;
377    }
378
379     /**
380     * @param array list of skins for the theme
381     */
382    protected function setSkins($value)
383    {
384        $this->_skins = $value;
385    }
386
387    /**
388     * Applies the theme to a particular control.
389     * The control's class name and SkinID value will be used to
390     * identify which skin to be applied. If the control's SkinID is empty,
391     * the default skin will be applied.
392     * @param TControl the control to be applied with a skin
393     * @return boolean if a skin is successfully applied
394     * @throws TConfigurationException if any error happened during the skin application
395     */
396    public function applySkin($control)
397    {
398        $type=get_class($control);
399        if(($id=$control->getSkinID())==='')
400            $id=0;
401        if(isset($this->_skins[$type][$id]))
402        {
403            foreach($this->_skins[$type][$id] as $name=>$value)
404            {
405                Prado::trace("Applying skin $name to $type",'System.Web.UI.TThemeManager');
406                if(is_array($value))
407                {
408                    switch($value[0])
409                    {
410                        case TTemplate::CONFIG_EXPRESSION:
411                            $value=$this->evaluateExpression($value[1]);
412                            break;
413                        case TTemplate::CONFIG_ASSET:
414                            $value=$this->_themeUrl.'/'.ltrim($value[1],'/');
415                            break;
416                        case TTemplate::CONFIG_DATABIND:
417                            $control->bindProperty($name,$value[1]);
418                            break;
419                        case TTemplate::CONFIG_PARAMETER:
420                            $control->setSubProperty($name,$this->getApplication()->getParameters()->itemAt($value[1]));
421                            break;
422                        case TTemplate::CONFIG_TEMPLATE:
423                            $control->setSubProperty($name,$value[1]);
424                            break;
425                        case TTemplate::CONFIG_LOCALIZATION:
426                            $control->setSubProperty($name,Prado::localize($value[1]));
427                            break;
428                        default:
429                            throw new TConfigurationException('theme_tag_unexpected',$name,$value[0]);
430                            break;
431                    }
432                }
433                if(!is_array($value))
434                {
435                    if(strpos($name,'.')===false)    // is simple property or custom attribute
436                    {
437                        if($control->hasProperty($name))
438                        {
439                            if($control->canSetProperty($name))
440                            {
441                                $setter='set'.$name;
442                                $control->$setter($value);
443                            }
444                            else
445                                throw new TConfigurationException('theme_property_readonly',$type,$name);
446                        }
447                        else
448                            throw new TConfigurationException('theme_property_undefined',$type,$name);
449                    }
450                    else    // complex property
451                        $control->setSubProperty($name,$value);
452                }
453            }
454            return true;
455        }
456        else
457            return false;
458    }
459
460    /**
461     * @return array list of CSS files (URL) in the theme
462     */
463    public function getStyleSheetFiles()
464    {
465        return $this->_cssFiles;
466    }
467
468     /**
469     * @param array list of CSS files (URL) in the theme
470     */
471    protected function setStyleSheetFiles($value)
472    {
473        $this->_cssFiles=$value;
474    }
475
476    /**
477     * @return array list of Javascript files (URL) in the theme
478     */
479    public function getJavaScriptFiles()
480    {
481        return $this->_jsFiles;
482    }
483
484    /**
485     * @param array list of Javascript files (URL) in the theme
486     */
487    protected function setJavaScriptFiles($value)
488    {
489        $this->_jsFiles=$value;
490    }
491}
492
493?>
Note: See TracBrowser for help on using the browser.