Index: includes/menu.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/menu.inc,v
retrieving revision 1.369
diff -u -p -r1.369 menu.inc
--- includes/menu.inc	17 Dec 2009 13:10:18 -0000	1.369
+++ includes/menu.inc	29 Dec 2009 04:53:31 -0000
@@ -1504,8 +1504,8 @@ function menu_get_active_help() {
  *
  * @param $initialize
  *   This parameter should only be used internally; it is set to TRUE in order
- *   to force the custom theme to be initialized from the menu router item for
- *   the current page.
+ *   to force the custom theme to be initialized for the current page request.
+ *
  * @return
  *   The machine-readable name of the custom theme, if there is one.
  *
@@ -1516,9 +1516,23 @@ function menu_get_custom_theme($initiali
   // Skip this if the site is offline or being installed or updated, since the
   // menu system may not be correctly initialized then.
   if ($initialize && !_menu_site_is_offline(TRUE) && (!defined('MAINTENANCE_MODE') || (MAINTENANCE_MODE != 'update' && MAINTENANCE_MODE != 'install'))) {
-    $router_item = menu_get_item();
-    if (!empty($router_item['access']) && !empty($router_item['theme_callback']) && function_exists($router_item['theme_callback'])) {
-      $custom_theme = call_user_func_array($router_item['theme_callback'], $router_item['theme_arguments']);
+    // First allow modules to dynamically set a custom theme for the current
+    // page. Since we can only have one, the last module to return a valid
+    // theme takes precedence.
+    $custom_themes = array_filter(module_invoke_all('custom_theme'), 'drupal_theme_access');
+    if (!empty($custom_themes)) {
+      $custom_theme = array_pop($custom_themes);
+    }
+    // Otherwise, execute the theme callback function for the current page, if
+    // there is one, in order to determine the custom theme to set.
+    if (!isset($custom_theme)) {
+      $router_item = menu_get_item();
+      if (!empty($router_item['access']) && !empty($router_item['theme_callback']) && function_exists($router_item['theme_callback'])) {
+        $theme_name = call_user_func_array($router_item['theme_callback'], $router_item['theme_arguments']);
+        if (drupal_theme_access($theme_name)) {
+          $custom_theme = $theme_name;
+        }
+      }
     }
   }
   return $custom_theme;
Index: includes/theme.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/theme.inc,v
retrieving revision 1.559
diff -u -p -r1.559 theme.inc
--- includes/theme.inc	28 Dec 2009 20:58:08 -0000	1.559
+++ includes/theme.inc	29 Dec 2009 04:53:31 -0000
@@ -41,12 +41,28 @@ define('MARK_UPDATED', 2);
  * Determines if a theme is available to use.
  *
  * @param $theme
- *   An object representing the theme to check.
+ *   Either the name of a theme or a full theme object.
+ *
  * @return
  *   Boolean TRUE if the theme is enabled or is the site administration theme;
  *   FALSE otherwise.
  */
 function drupal_theme_access($theme) {
+  if (is_object($theme)) {
+    return _drupal_theme_access($theme);
+  }
+  else {
+    $themes = list_themes();
+    return isset($themes[$theme]) && _drupal_theme_access($themes[$theme]);
+  }
+}
+
+/**
+ * Helper function for determining access to a theme.
+ *
+ * @see drupal_theme_access()
+ */
+function _drupal_theme_access($theme) {
   $admin_theme = variable_get('admin_theme');
   return !empty($theme->status) || ($admin_theme && $theme->name == $admin_theme);
 }
@@ -67,12 +83,12 @@ function drupal_theme_initialize() {
 
   // Only select the user selected theme if it is available in the
   // list of themes that can be accessed.
-  $theme = !empty($user->theme) && isset($themes[$user->theme]) && drupal_theme_access($themes[$user->theme]) ? $user->theme : variable_get('theme_default', 'garland');
+  $theme = !empty($user->theme) && drupal_theme_access($user->theme) ? $user->theme : variable_get('theme_default', 'garland');
 
-  // Allow modules to override the present theme... only select custom theme
-  // if it is available in the list of themes that can be accessed.
+  // Allow modules to override the theme. Validation has already been performed
+  // inside menu_get_custom_theme(), so we do not need to check it again here.
   $custom_theme = menu_get_custom_theme();
-  $theme = $custom_theme && isset($themes[$custom_theme]) && drupal_theme_access($themes[$custom_theme]) ? $custom_theme : $theme;
+  $theme = !empty($custom_theme) ? $custom_theme : $theme;
 
   // Store the identifier for retrieving theme settings with.
   $theme_key = $theme;
Index: modules/menu/menu.api.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/menu/menu.api.php,v
retrieving revision 1.21
diff -u -p -r1.21 menu.api.php
--- modules/menu/menu.api.php	9 Dec 2009 17:44:44 -0000	1.21
+++ modules/menu/menu.api.php	29 Dec 2009 04:53:31 -0000
@@ -175,10 +175,13 @@
  *   - "access arguments": An array of arguments to pass to the access callback
  *     function, with path component substitution as described above.
  *   - "theme callback": Optional. A function returning the machine-readable
- *     name of the theme that will be used to render the page. If the function
- *     returns nothing, the main site theme will be used. If no function is
- *     provided, the main site theme will also be used, unless a value is
- *     inherited from a parent menu item.
+ *     name of the default theme that will be used to render the page. If this
+ *     function is provided, it is expected to return a currently-active theme
+ *     on the site (otherwise, the main site theme will be used instead). If no
+ *     function is provided, the main site theme will also be used, unless a
+ *     value is inherited from a parent menu item. In all cases, the results of
+ *     this function can be dynamically overridden for a particular page
+ *     request by modules which implement hook_custom_theme().
  *   - "theme arguments": An array of arguments to pass to the theme callback
  *     function, with path component substitution as described above.
  *   - "file": A file that will be included before the page callback is called;
Index: modules/simpletest/tests/menu.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/menu.test,v
retrieving revision 1.26
diff -u -p -r1.26 menu.test
--- modules/simpletest/tests/menu.test	14 Dec 2009 20:23:01 -0000	1.26
+++ modules/simpletest/tests/menu.test	29 Dec 2009 04:53:31 -0000
@@ -38,7 +38,7 @@ class MenuRouterTestCase extends DrupalW
    */
   function testThemeCallbackAdministrative() {
     $this->drupalGet('menu-test/theme-callback/use-admin-theme');
-    $this->assertText('Requested theme: seven. Actual theme: seven.', t('The administrative theme can be correctly set in a theme callback.'));
+    $this->assertText('Custom theme: seven. Actual theme: seven.', t('The administrative theme can be correctly set in a theme callback.'));
     $this->assertRaw('seven/style.css', t("The administrative theme's CSS appears on the page."));
   }
 
@@ -47,7 +47,7 @@ class MenuRouterTestCase extends DrupalW
    */
   function testThemeCallbackInheritance() {
     $this->drupalGet('menu-test/theme-callback/use-admin-theme/inheritance');
-    $this->assertText('Requested theme: seven. Actual theme: seven. Theme callback inheritance is being tested.', t('Theme callback inheritance correctly uses the administrative theme.'));
+    $this->assertText('Custom theme: seven. Actual theme: seven. Theme callback inheritance is being tested.', t('Theme callback inheritance correctly uses the administrative theme.'));
     $this->assertRaw('seven/style.css', t("The administrative theme's CSS appears on the page."));
   }
 
@@ -66,7 +66,7 @@ class MenuRouterTestCase extends DrupalW
     $admin_user = $this->drupalCreateUser(array('access site in maintenance mode'));
     $this->drupalLogin($admin_user);
     $this->drupalGet('menu-test/theme-callback/use-admin-theme');
-    $this->assertText('Requested theme: seven. Actual theme: seven.', t('The theme callback system is correctly triggered for an administrator when the site is in maintenance mode.'));
+    $this->assertText('Custom theme: seven. Actual theme: seven.', t('The theme callback system is correctly triggered for an administrator when the site is in maintenance mode.'));
     $this->assertRaw('seven/style.css', t("The administrative theme's CSS appears on the page."));
   }
 
@@ -76,13 +76,13 @@ class MenuRouterTestCase extends DrupalW
   function testThemeCallbackOptionalTheme() {
     // Request a theme that is not enabled.
     $this->drupalGet('menu-test/theme-callback/use-stark-theme');
-    $this->assertText('Requested theme: stark. Actual theme: garland.', t('The theme callback system falls back on the default theme when a theme that is not enabled is requested.'));
+    $this->assertText('Custom theme: NONE. Actual theme: garland.', t('The theme callback system falls back on the default theme when a theme that is not enabled is requested.'));
     $this->assertRaw('garland/style.css', t("The default theme's CSS appears on the page."));
 
     // Now enable the theme and request it again.
     theme_enable(array('stark'));
     $this->drupalGet('menu-test/theme-callback/use-stark-theme');
-    $this->assertText('Requested theme: stark. Actual theme: stark.', t('The theme callback system uses an optional theme once it has been enabled.'));
+    $this->assertText('Custom theme: stark. Actual theme: stark.', t('The theme callback system uses an optional theme once it has been enabled.'));
     $this->assertRaw('stark/layout.css', t("The optional theme's CSS appears on the page."));
   }
 
@@ -91,7 +91,7 @@ class MenuRouterTestCase extends DrupalW
    */
   function testThemeCallbackFakeTheme() {
     $this->drupalGet('menu-test/theme-callback/use-fake-theme');
-    $this->assertText('Requested theme: fake_theme. Actual theme: garland.', t('The theme callback system falls back on the default theme when a theme that does not exist is requested.'));
+    $this->assertText('Custom theme: NONE. Actual theme: garland.', t('The theme callback system falls back on the default theme when a theme that does not exist is requested.'));
     $this->assertRaw('garland/style.css', t("The default theme's CSS appears on the page."));
   }
 
@@ -100,11 +100,34 @@ class MenuRouterTestCase extends DrupalW
    */
   function testThemeCallbackNoThemeRequested() {
     $this->drupalGet('menu-test/theme-callback/no-theme-requested');
-    $this->assertText('Requested theme: NONE. Actual theme: garland.', t('The theme callback system falls back on the default theme when no theme is requested.'));
+    $this->assertText('Custom theme: NONE. Actual theme: garland.', t('The theme callback system falls back on the default theme when no theme is requested.'));
     $this->assertRaw('garland/style.css', t("The default theme's CSS appears on the page."));
   }
 
   /**
+   * Test that the result of hook_custom_theme() overrides the theme callback.
+   */
+  function testHookCustomTheme() {
+    // Trigger hook_custom_theme() to dynamically request the Stark theme for
+    // the requested page.
+    variable_set('menu_test_hook_custom_theme_name', 'stark');
+
+    // Request a page whose theme callback returns the Seven theme. Since Stark
+    // is not a currently enabled theme, our above request should be ignored,
+    // and Seven should still be used.
+    $this->drupalGet('menu-test/theme-callback/use-admin-theme');
+    $this->assertText('Custom theme: seven. Actual theme: seven.', t('The result of hook_custom_theme() does not override a theme callback when it returns a theme that is not enabled.'));
+    $this->assertRaw('seven/style.css', t("The Seven theme's CSS appears on the page."));
+
+    // Now enable the Stark theme and request the same page as above. This
+    // time, we expect hook_custom_theme() to prevail.
+    theme_enable(array('stark'));
+    $this->drupalGet('menu-test/theme-callback/use-admin-theme');
+    $this->assertText('Custom theme: stark. Actual theme: stark.', t('The result of hook_custom_theme() overrides what was set in a theme callback.'));
+    $this->assertRaw('stark/layout.css', t("The Stark theme's CSS appears on the page."));
+  }
+
+  /**
    * Tests for menu_link_maintain().
    */
   function testMenuLinkMaintain() {
Index: modules/simpletest/tests/menu_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/menu_test.module,v
retrieving revision 1.11
diff -u -p -r1.11 menu_test.module
--- modules/simpletest/tests/menu_test.module	14 Dec 2009 20:23:01 -0000	1.11
+++ modules/simpletest/tests/menu_test.module	29 Dec 2009 04:53:31 -0000
@@ -194,7 +194,7 @@ function menu_test_theme_page_callback($
   // Now check both the requested custom theme and the actual theme being used.
   $custom_theme = menu_get_custom_theme();
   $requested_theme = empty($custom_theme) ? 'NONE' : $custom_theme;
-  $output = "Requested theme: $requested_theme. Actual theme: $theme_key.";
+  $output = "Custom theme: $requested_theme. Actual theme: $theme_key.";
   if ($inherited) {
     $output .= ' Theme callback inheritance is being tested.';
   }
@@ -228,6 +228,21 @@ function menu_test_theme_callback($argum
 }
 
 /**
+ * Implement hook_custom_theme().
+ *
+ * @return
+ *   The name of the custom theme to use for the current page.
+ */
+function menu_test_custom_theme() {
+  // If an appropriate variable has been set in the database, request the theme
+  // that is stored there. Otherwise, do not attempt to dynamically set the
+  // theme.
+  if ($theme = variable_get('menu_test_hook_custom_theme_name', FALSE)) {
+    return $theme;
+  }
+}
+
+/**
  * Helper function for the testMenuName() test. Used to change the menu_name
  * parameter of a menu.
  *
Index: modules/system/system.api.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.api.php,v
retrieving revision 1.112
diff -u -p -r1.112 system.api.php
--- modules/system/system.api.php	28 Dec 2009 10:48:51 -0000	1.112
+++ modules/system/system.api.php	29 Dec 2009 04:53:31 -0000
@@ -1094,6 +1094,31 @@ function hook_theme_registry_alter(&$the
 }
 
 /**
+ * Return the machine-readable name of the theme to use for the current page.
+ *
+ * This hook can be used to dynamically set the theme for the current page
+ * request. It overrides the default theme as well as any per-page or
+ * per-section theme set by the theme callback function in hook_menu(). This
+ * should be used by modules which need to override the theme based on dynamic
+ * conditions.
+ *
+ * Since only one theme can be used at a time, the last (i.e., highest
+ * weighted) module which returns a valid theme name from this hook will
+ * prevail.
+ *
+ * @return
+ *   The machine-readable name of the theme that should be used for the current
+ *   page request. The value returned from this function will only have an
+ *   effect if it corresponds to a currently-active theme on the site.
+ */
+function hook_custom_theme() {
+  // Allow the user to request a particular theme via a query parameter.
+  if (isset($_GET['theme'])) {
+    return $_GET['theme'];
+  }
+}
+
+/**
  * Register XML-RPC callbacks.
  *
  * This hook lets a module register callback functions to be called when
