Commit e66371ca authored by Dries's avatar Dries

- Patch #137211 by merlinofchaos: move theme information to .info files and...

- Patch #137211 by merlinofchaos: move theme information to .info files and improved theme inheritance.
parent 644657c5
......@@ -975,6 +975,7 @@ function drupal_maintenance_theme() {
foreach ($themes as $hook => $info) {
if (!isset($info['file']) && !isset($info['function'])) {
$themes[$hook]['function'] = 'theme_'. $hook;
$themes[$hook]['theme path'] = 'misc';
}
}
_theme_set_registry($themes);
......
......@@ -1821,7 +1821,7 @@ function batch_set($batch_definition) {
/**
* Process the batch.
*
*
* Unless the batch has been markes with 'progressive' = FALSE, the function
* isses a drupal_goto and thus ends page execution.
*
......
......@@ -1055,3 +1055,22 @@ function menu_get_item_by_mid($mid) {
}
return FALSE;
}
/**
* Returns the rendered local tasks. The default implementation renders
* them as tabs.
*
* @ingroup themeable
*/
function theme_menu_local_tasks() {
$output = '';
if ($primary = menu_primary_local_tasks()) {
$output .= "<ul class=\"tabs primary\">\n". $primary ."</ul>\n";
}
if ($secondary = menu_secondary_local_tasks()) {
$output .= "<ul class=\"tabs secondary\">\n". $secondary ."</ul>\n";
}
return $output;
}
......@@ -27,7 +27,6 @@
/**
* Initialize the theme system by loading the theme.
*
*/
function init_theme() {
global $theme, $user, $custom_theme, $theme_engine, $theme_key;
......@@ -51,38 +50,100 @@ function init_theme() {
// Store the identifier for retrieving theme settings with.
$theme_key = $theme;
// If we're using a style, load its appropriate theme,
// which is stored in the style's description field.
// Also add the stylesheet using drupal_add_css().
// Otherwise, load the theme.
if (strpos($themes[$theme]->filename, '.css')) {
// File is a style; loads its CSS.
// Set theme to its template/theme
drupal_add_css($themes[$theme]->filename, 'theme');
$theme = basename(dirname($themes[$theme]->owner));
// Find all our ancestor themes and put them in an array.
$base_theme = array();
$ancestor = $theme;
while ($ancestor && isset($themes[$ancestor]->base_theme)) {
if (isset($themes[$themes[$ancestor]->base_theme])) {
$base_theme[] = $new_base_theme = $themes[$themes[$ancestor]->base_theme];
// stop if this is final ancestor.
if (!isset($new_base_theme->base_theme)) {
break;
}
$ancestor = $new_base_theme->base_theme;
}
// stop if ancestor didn't exist.
else {
break;
}
}
_init_theme($themes[$theme], array_reverse($base_theme));
}
/**
* Initialize the theme system given already loaded information. This
* function is useful to initialize a theme when no database is present.
*
* @param $theme
* An object with the following information:
* filename
* The .info file for this theme. The 'path' to
* the theme will be in this file's directory. (Required)
* owner
* The path to the .theme file or the .engine file to load for
* the theme. (Required)
* stylesheet
* The primary stylesheet for the theme. (Optional)
* engine
* The name of theme engine to use. (Optional)
* @param $base_theme
* An optional array of objects that represent the 'base theme' if the
* theme is meant to be derivative of another theme. It requires
* the same information as the $theme object. It should be in
* 'oldest first' order, meaning the top level of the chain will
* be first.
*/
function _init_theme($theme, $base_theme = array()) {
global $theme_info, $base_theme_info, $theme_engine, $theme_path;
$theme_info = $theme;
$base_theme_info = $base_theme;
$theme_path = dirname($theme->filename);
// Add the default stylesheet
if (!empty($theme->stylesheet)) {
drupal_add_css($theme->stylesheet, 'theme');
}
else {
// File is a template/theme
// Load its CSS, if it exists
if (file_exists($stylesheet = dirname($themes[$theme]->filename) .'/style.css')) {
drupal_add_css($stylesheet, 'theme');
// If we don't have a stylesheet of our own, look for the first
// base to have one and use its.
foreach ($base_theme as $base) {
if (!empty($base->stylesheet)) {
drupal_add_css($base->stylesheet, 'theme');
break;
}
}
}
if (strpos($themes[$theme]->filename, '.theme')) {
// file is a theme; include it
include_once './'. $themes[$theme]->filename;
_theme_load_registry($theme);
}
elseif (strpos($themes[$theme]->owner, '.engine')) {
// file is a template; include its engine
include_once './'. $themes[$theme]->owner;
$theme_engine = basename($themes[$theme]->owner, '.engine');
$theme_engine = NULL;
// Initialize the theme.
if (isset($theme->engine)) {
// Include the engine.
include_once './'. $theme->owner;
$theme_engine = $theme->engine;
if (function_exists($theme_engine .'_init')) {
call_user_func($theme_engine .'_init', $themes[$theme]);
foreach ($base_theme as $base) {
call_user_func($theme_engine .'_init', $base);
}
call_user_func($theme_engine .'_init', $theme);
}
}
else {
// include non-engine theme files
foreach ($base_theme as $base) {
// Include the theme file or the engine.
if (!empty($base->owner)) {
include_once './'. $base->owner;
}
}
// and our theme gets one too.
if (!empty($theme->owner)) {
include_once './'. $theme->owner;
}
_theme_load_registry($theme, $theme_engine);
}
_theme_load_registry($theme, $base_theme, $theme_engine);
}
/**
......@@ -111,14 +172,24 @@ function _theme_set_registry($registry) {
/**
* Get the theme_registry cache from the database; if it doesn't exist, build
* it.
*
* @param $theme
* The loaded $theme object.
* @param $base_theme
* An array of loaded $theme objects representing the ancestor themes in
* oldest first order.
* @param theme_engine
* The name of the theme engine.
*/
function _theme_load_registry($theme, $theme_engine = NULL) {
$cache = cache_get("theme_registry:$theme", 'cache');
function _theme_load_registry($theme, $base_theme = NULL, $theme_engine = NULL) {
// Check the theme registry cache; if it exists, use it.
$cache = cache_get("theme_registry:$theme->name", 'cache');
if (isset($cache->data)) {
$registry = $cache->data;
}
else {
$registry = _theme_build_registry($theme, $theme_engine);
// If not, build one and cache it.
$registry = _theme_build_registry($theme, $base_theme, $theme_engine);
_theme_save_registry($theme, $registry);
}
_theme_set_registry($registry);
......@@ -128,7 +199,7 @@ function _theme_load_registry($theme, $theme_engine = NULL) {
* Write the theme_registry cache into the database.
*/
function _theme_save_registry($theme, $registry) {
cache_set("theme_registry:$theme", $registry);
cache_set("theme_registry:$theme->name", $registry);
}
/**
......@@ -141,22 +212,31 @@ function drupal_rebuild_theme_registry() {
}
/**
* Process a single invocation of the theme hook.
* Process a single invocation of the theme hook. $type will be one
* of 'module', 'theme_engine' or 'theme' and it tells us some
* important information.
*
* Because $cache is a reference, the cache will be continually
* expanded upon; new entries will replace old entries in the
* array_merge, but we are careful to ensure some data is carried
* forward, such as the arguments a theme hook needs.
*/
function _theme_process_registry(&$cache, $name, $type) {
function _theme_process_registry(&$cache, $name, $type, $theme, $path) {
$function = $name .'_theme';
if (function_exists($function)) {
$result = $function($cache);
$result = $function($cache, $type, $theme, $path);
// Automatically find paths
$path = drupal_get_path($type, $name);
foreach ($result as $hook => $info) {
$result[$hook]['type'] = $type;
$result[$hook]['theme path'] = $path;
// if function and file are left out, default to standard naming
// conventions.
if (!isset($info['file']) && !isset($info['function'])) {
$result[$hook]['function'] = ($type == 'module' ? 'theme_' : $name .'_') . $hook;
}
// If a path is set in the info, use what was set. Otherwise use the
// default path. This is mostly so system.module can declare theme
// functions on behalf of core .include files.
if (isset($info['file']) && !isset($info['path'])) {
$result[$hook]['file'] = $path .'/'. $info['file'];
}
......@@ -170,15 +250,7 @@ function _theme_process_registry(&$cache, $name, $type) {
if (!isset($info['preprocess functions']) || !is_array($info['preprocess functions'])) {
$info['preprocess functions'] = array();
$prefix = ($type == 'module' ? 'template' : $name);
// It would be too much of a performance hit to let every module have
// a generic preprocess function; themes and theme engines can do that.
if ($type != 'module' && function_exists($prefix .'_preprocess')) {
$info['preprocess functions'][] = $prefix .'_preprocess';
}
if (function_exists($prefix .'_preprocess_'. $hook)) {
$info['preprocess functions'][] = $prefix .'_preprocess_'. $hook;
}
// theme engines get an extra set.
// theme engines get an extra set that come before the normally named preprocess.
if ($type == 'theme_engine') {
if (function_exists($prefix .'_engine_preprocess')) {
$info['preprocess functions'][] = $prefix .'_engine_preprocess';
......@@ -187,6 +259,15 @@ function _theme_process_registry(&$cache, $name, $type) {
$info['preprocess functions'][] = $prefix .'_engine_preprocess_'. $hook;
}
}
// It would be too much of a performance hit to let every module have
// a generic preprocess function; themes and theme engines can do that.
if ($type != 'module' && function_exists($prefix .'_preprocess')) {
$info['preprocess functions'][] = $prefix .'_preprocess';
}
if (function_exists($prefix .'_preprocess_'. $hook)) {
$info['preprocess functions'][] = $prefix .'_preprocess_'. $hook;
}
}
if (isset($cache[$hook]['preprocess functions']) && is_array($cache[$hook]['preprocess functions']) && empty($cache[$hook]['override preprocess functions'])) {
$info['preprocess functions'] = array_merge($cache[$hook]['preprocess functions'], $info['preprocess functions']);
......@@ -194,24 +275,47 @@ function _theme_process_registry(&$cache, $name, $type) {
$result[$hook]['preprocess functions'] = $info['preprocess functions'];
}
// Merge the newly created theme hooks into the existing cache.
$cache = array_merge($cache, $result);
}
}
/**
* Rebuild the hook theme_registry cache.
*
* @param $theme
* The loaded $theme object.
* @param $base_theme
* An array of loaded $theme objects representing the ancestor themes in
* oldest first order.
* @param theme_engine
* The name of the theme engine.
*/
function _theme_build_registry($theme, $theme_engine) {
function _theme_build_registry($theme, $base_theme, $theme_engine) {
$cache = array();
// First, process the theme hooks advertised by modules. This will
// serve as the basic registry.
foreach (module_implements('theme') as $module) {
_theme_process_registry($cache, $module, 'module');
_theme_process_registry($cache, $module, 'module', $module, drupal_get_path('module', $module));
}
// Process each base theme.
foreach ($base_theme as $base) {
// If the theme uses a theme engine, process its hooks.
$base_path = dirname($base->filename);
if ($theme_engine) {
_theme_process_registry($cache, $theme_engine, 'base_theme_engine', $base->name, $base_path);
}
_theme_process_registry($cache, $base->name, 'base_theme', $base->name, $base_path);
}
// And then the same thing, but for the theme.
if ($theme_engine) {
_theme_process_registry($cache, $theme_engine, 'theme_engine');
_theme_process_registry($cache, $theme_engine, 'theme_engine', $theme->name, dirname($theme->filename));
}
_theme_process_registry($cache, $theme, 'theme');
// Finally, hooks provided by the theme itself.
_theme_process_registry($cache, $theme->name, 'theme', $theme->name, dirname($theme->filename));
return $cache;
}
......@@ -237,6 +341,15 @@ function list_themes($refresh = FALSE) {
while ($theme = db_fetch_object($result)) {
if (file_exists($theme->filename)) {
$theme->info = unserialize($theme->info);
if (isset($theme->info['stylesheet'])) {
$theme->stylesheet = $theme->info['stylesheet'];
}
if (isset($theme->info['engine'])) {
$theme->engine = $theme->info['engine'];
}
if (isset($theme->info['base theme'])) {
$theme->base_theme = $theme->info['base theme'];
}
$list[$theme->name] = $theme;
}
}
......@@ -344,6 +457,10 @@ function theme() {
}
$info = $hooks[$hook];
global $theme_path;
$temp = $theme_path;
// point path_to_theme() to the currently used theme path:
$theme_path = $hooks[$hook]['theme path'];
if (isset($info['function'])) {
// The theme call is a function.
......@@ -355,7 +472,7 @@ function theme() {
}
include_once($function_file);
}
return call_user_func_array($info['function'], $args);
$output = call_user_func_array($info['function'], $args);
}
else {
// The theme call is a template.
......@@ -424,8 +541,11 @@ function theme() {
$template_file = $hooks[$hook]['path'] .'/'. $template_file;
}
}
return $render_function($template_file, $variables);
$output = $render_function($template_file, $variables);
}
// restore path_to_theme()
$theme_path = $temp;
return $output;
}
/**
......@@ -448,30 +568,13 @@ function drupal_discover_template($suggestions, $extension = '.tpl.php') {
* Return the path to the currently selected theme.
*/
function path_to_theme() {
global $theme;
global $theme_path;
if (!isset($theme)) {
if (!isset($theme_path)) {
init_theme();
}
$themes = list_themes();
return dirname($themes[$theme]->filename);
}
/**
* Return the path to the currently selected engine.
*/
function path_to_engine() {
global $theme, $theme_engine;
if (!isset($theme)) {
init_theme();
}
$engines = list_theme_engines();
return dirname($engines[$theme_engine]->filename);
return $theme_path;
}
/**
......
......@@ -279,7 +279,8 @@ function color_scheme_form_submit($form_id, $values) {
*/
function _color_rewrite_stylesheet($theme, &$info, &$paths, $palette) {
// Load stylesheet
$style = file_get_contents($paths['source'] .'style.css');
$themes = list_themes();
$style = file_get_contents($themes[$theme]->stylesheet);
// Prepare color conversion table
$conversion = $palette;
......
......@@ -1109,8 +1109,8 @@ function system_install() {
break;
}
db_query("INSERT INTO {system} (filename, name, type, owner, status, throttle, bootstrap, schema_version) VALUES ('themes/engines/phptemplate/phptemplate.engine', 'phptemplate', 'theme_engine', '', 1, 0, 0, 0)");
db_query("INSERT INTO {system} (filename, name, type, owner, status, throttle, bootstrap, schema_version, info) VALUES ('themes/garland/page.tpl.php', 'garland', 'theme', 'themes/engines/phptemplate/phptemplate.engine', 1, 0, 0, 0, '%s')", serialize(drupal_parse_info_file('themes/garland/garland.info') + system_theme_default()));
// Load system theme data appropriately.
system_theme_data();
db_query("INSERT INTO {users} (uid,name,mail) VALUES(0,'','')");
......@@ -3864,6 +3864,16 @@ function system_update_6012() {
return $ret;
}
/**
* Rebuild cache data for theme system changes
*/
function system_update_6013() {
// Rebuild system table contents.
module_rebuild_cache();
system_theme_data();
}
/**
* @} End of "defgroup updates-5.x-to-6.x"
* The next series of updates should start at 7000.
......
......@@ -505,7 +505,9 @@ function system_admin_theme_submit($form_id, $form_values) {
function system_theme_select_form($description = '', $default_value = '', $weight = 0) {
if (user_access('select different theme')) {
$enabled = array();
foreach (list_themes() as $theme) {
$themes = list_themes();
foreach ($themes as $theme) {
if ($theme->status) {
$enabled[] = $theme;
}
......@@ -526,8 +528,17 @@ function system_theme_select_form($description = '', $default_value = '', $weigh
// For the default theme, revert to an empty string so the user's theme updates when the site theme is changed.
$info->key = $info->name == variable_get('theme_default', 'garland') ? '' : $info->name;
$info->screenshot = dirname($info->filename) .'/screenshot.png';
$screenshot = file_exists($info->screenshot) ? theme('image', $info->screenshot, t('Screenshot for %theme theme', array('%theme' => $info->name)), '', array('class' => 'screenshot'), FALSE) : t('no screenshot');
$screenshot = NULL;
$theme_key = $info->name;
while ($theme_key) {
if (file_exists($themes[$theme_key]->info['screenshot'])) {
$screenshot = $themes[$theme_key]->info['screenshot'];
break;
}
$theme_key = isset($themes[$theme_key]->info['base theme']) ? $themes[$theme_key]->info['base theme'] : NULL;
}
$screenshot = $screenshot ? theme('image', $screenshot, t('Screenshot for %theme theme', array('%theme' => $info->name)), '', array('class' => 'screenshot'), FALSE) : t('no screenshot');
$form['themes'][$info->key]['screenshot'] = array('#value' => $screenshot);
$form['themes'][$info->key]['description'] = array('#type' => 'item', '#title' => $info->name, '#value' => dirname($info->filename) . ($info->name == variable_get('theme_default', 'garland') ? '<br /> <em>'. t('(site default theme)') .'</em>' : ''));
......@@ -964,6 +975,8 @@ function system_theme_default() {
'search',
'slogan'
),
'stylesheet' => 'style.css',
'screenshot' => 'screenshot.png',
);
}
......@@ -971,22 +984,12 @@ function system_theme_default() {
* Collect data about all currently available themes
*/
function system_theme_data() {
include_once './includes/install.inc';
// Find themes
$themes = drupal_system_listing('\.theme$', 'themes');
$themes = drupal_system_listing('\.info$', 'themes');
// Find theme engines
$engines = drupal_system_listing('\.engine$', 'themes/engines');
// can't iterate over array itself as it uses a copy of the array items
foreach (array_keys($themes) as $key) {
drupal_get_filename('theme', $themes[$key]->name, $themes[$key]->filename);
drupal_load('theme', $themes[$key]->name);
$themes[$key]->owner = $themes[$key]->filename;
$themes[$key]->prefix = $key;
}
// Remove all theme engines from the system table
db_query("DELETE FROM {system} WHERE type = 'theme_engine'");
......@@ -995,61 +998,106 @@ function system_theme_data() {
drupal_get_filename('theme_engine', $engine->name, $engine->filename);
drupal_load('theme_engine', $engine->name);
db_query("INSERT INTO {system} (name, type, filename, status, throttle, bootstrap) VALUES ('%s', '%s', '%s', %d, %d, %d)", $engine->name, 'theme_engine', $engine->filename, 1, 0, 0);
}
// Add templates to the site listing
foreach (call_user_func($engine->name .'_templates') as $template) {
// Do not double-insert templates with theme files in their directory,
// but do register their engine data.
if (array_key_exists($template->name, $themes)) {
$themes[$template->name]->template = TRUE;
$themes[$template->name]->owner = $engine->filename;
$themes[$template->name]->prefix = $engine->name;
}
else {
$template->template = TRUE;
$template->name = basename(dirname($template->filename));
$template->owner = $engine->filename;
$template->prefix = $engine->name;
$defaults = system_theme_default();
$themes[$template->name] = $template;
$sub_themes = array();
// Read info files for each theme
foreach ($themes as $key => $theme) {
$themes[$key]->info = drupal_parse_info_file($theme->filename) + $defaults;
if (!empty($themes[$key]->info['base theme'])) {
$sub_themes[] = $key;
}
if (empty($themes[$key]->info['engine'])) {
$filename = dirname($themes[$key]->filename) .'/'. $themes[$key]->name .'.theme';
if (file_exists($filename)) {
$themes[$key]->owner = $filename;
$themes[$key]->prefix = $key;
}
}
else {
$engine = $themes[$key]->info['engine'];
if (isset($engines[$engine])) {
$themes[$key]->owner = $engines[$engine]->filename;
$themes[$key]->prefix = $engines[$engine]->name;
$themes[$key]->template = TRUE;
}
}
// Give the stylesheet proper path information.
if (!empty($themes[$key]->info['stylesheet'])) {
$themes[$key]->info['stylesheet'] = dirname($themes[$key]->filename) .'/'. $themes[$key]->info['stylesheet'];
}
// Give the screenshot proper path information.
if (!empty($themes[$key]->info['screenshot'])) {
$themes[$key]->info['screenshot'] = dirname($themes[$key]->filename) .'/'. $themes[$key]->info['screenshot'];
}
}
// Find styles in each theme's directory.
foreach ($themes as $theme) {
foreach (file_scan_directory(dirname($theme->filename), 'style.css$') as $style) {
$style->style = TRUE;
$style->template = isset($theme->template) ? $theme->template : FALSE;
$style->name = basename(dirname($style->filename));
$style->owner = $theme->filename;
$style->prefix = !empty($theme->template) ? $theme->prefix : $theme->name;
// do not double-insert styles with theme files in their directory
if (array_key_exists($style->name, $themes)) {
continue;
// Now that we've established all our master themes, go back and fill in
// data for subthemes.
foreach ($sub_themes as $key) {
$base_key = system_find_base_theme($themes, $key);
if (!$base_key) {
continue;
}
// Copy the 'owner' and 'engine' over if the top level theme uses a
// theme engine.
if (isset($themes[$base_key]->owner)) {
if (isset($themes[$base_key]->info['engine'])) {
$themes[$key]->info['engine'] = $themes[$base_key]->info['engine'];
$themes[$key]->owner = $themes[$base_key]->owner;
$themes[$key]->prefix = $themes[$base_key]->prefix;
}
else {
$themes[$key]->prefix = $key;
}
$themes[$style->name] = $style;
}
}
// Extract current files from database.
system_get_files_database($themes, 'theme');
$defaults = system_theme_default();
// Read info files for the owner
foreach (array_keys($themes) as $key) {
$themes[$key]->info = drupal_parse_info_file(dirname($themes[$key]->filename) .'/'. $themes[$key]->name .'.info') + $defaults;
}
db_query("DELETE FROM {system} WHERE type = 'theme'");
foreach ($themes as $theme) {
if (!isset($theme->owner)) {
$theme->owner = '';
}
db_query("INSERT INTO {system} (name, owner, info, type, filename, status, throttle, bootstrap) VALUES ('%s', '%s', '%s', '%s', '%s', %d, %d, %d)", $theme->name, $theme->owner, serialize($theme->info), 'theme', $theme->filename, isset($theme->status) ? $theme->status : 0, 0, 0);
}
return $themes;
}
/**
* Recursive function to find the top level base theme. Themes can inherit
* templates and function implementations from earlier themes; this function
* finds the top level parent that has no ancestor, or returns NULL if there
* isn't a valid parent.
*/
function system_find_base_theme($themes, $key, $used_keys = array()) {
$base_key = $themes[$key]->info['base theme'];
// Does the base theme exist?
if (!isset($themes[$base_key])) {