diff --git a/core/core.services.yml b/core/core.services.yml
index f3715856ba6628a2adb3560cd49d6e80f658bd09..6104db91d26f1ac3aff6b6678dffe3fd4d43fdb5 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -165,21 +165,6 @@ services:
     calls:
       - [addSubscriber, ['@http_client_simpletest_subscriber']]
       - [setUserAgent, ['Drupal (+http://drupal.org/)']]
-  theme.negotiator:
-    class: Drupal\Core\Theme\ThemeNegotiator
-    arguments: ['@access_check.theme']
-    calls:
-      - [setRequest, ['@request']]
-  theme.negotiator.default:
-    class: Drupal\Core\Theme\DefaultNegotiator
-    arguments: ['@config.factory']
-    tags:
-      - { name: theme_negotiator, priority: -100 }
-  theme.negotiator.ajax_base_page:
-    class: Drupal\Core\Theme\AjaxBasePageNegotiator
-    arguments: ['@csrf_token', '@config.factory']
-    tags:
-      - { name: theme_negotiator, priority: 1000 }
   container.namespaces:
     class: ArrayObject
     arguments: [ '%container.namespaces%' ]
diff --git a/core/includes/ajax.inc b/core/includes/ajax.inc
index 3ff6c176751068b4b9734a68f1a261c2cea2ebd2..3f2389f57e9591de612230c2f74359a01f7ccd5c 100644
--- a/core/includes/ajax.inc
+++ b/core/includes/ajax.inc
@@ -302,6 +302,42 @@ function ajax_render($commands = array()) {
   return drupal_json_encode($commands);
 }
 
+/**
+ * Theme callback: Returns the correct theme for an Ajax request.
+ *
+ * Many different pages can invoke an Ajax request to system/ajax or another
+ * generic Ajax path. It is almost always desired for an Ajax response to be
+ * rendered using the same theme as the base page, because most themes are built
+ * with the assumption that they control the entire page, so if the CSS for two
+ * themes are both loaded for a given page, they may conflict with each other.
+ * For example, Bartik is Drupal's default theme, and Seven is Drupal's default
+ * administration theme. Depending on whether the "Use the administration theme
+ * when editing or creating content" checkbox is checked, the node edit form may
+ * be displayed in either theme, but the Ajax response to the Field module's
+ * "Add another item" button should be rendered using the same theme as the rest
+ * of the page. Therefore, system_menu() sets the 'theme callback' for
+ * 'system/ajax' to this function, and it is recommended that modules
+ * implementing other generic Ajax paths do the same.
+ *
+ * @see system_menu()
+ * @see file_menu()
+ */
+function ajax_base_page_theme() {
+  if (!empty($_POST['ajax_page_state']['theme']) && !empty($_POST['ajax_page_state']['theme_token'])) {
+    $theme = $_POST['ajax_page_state']['theme'];
+    $token = $_POST['ajax_page_state']['theme_token'];
+
+    // Prevent a request forgery from giving a person access to a theme they
+    // shouldn't be otherwise allowed to see. However, since everyone is allowed
+    // to see the default theme, token validation isn't required for that, and
+    // bypassing it allows most use-cases to work even when accessed from the
+    // page cache.
+    if ($theme === \Drupal::config('system.theme')->get('default') || drupal_valid_token($token, $theme)) {
+      return $theme;
+    }
+  }
+}
+
 /**
  * Converts the return value of a page callback into an Ajax commands array.
  *
diff --git a/core/includes/common.inc b/core/includes/common.inc
index fc46b9b389e548b11f32b25370a5be5a8389f6bb..36e069db66558aa49b2b3816bde4022bc3c09767 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -2331,7 +2331,7 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS
       global $theme_key;
       // Provide the page with information about the theme that's used, so that
       // a later AJAX request can be rendered using the same theme.
-      // @see \Drupal\Core\Theme\AjaxBasePageNegotiator
+      // @see ajax_base_page_theme()
       $setting['ajaxPageState']['theme'] = $theme_key;
       // Checks that the DB is available before filling theme_token.
       if (!defined('MAINTENANCE_MODE')) {
@@ -3148,6 +3148,7 @@ function _drupal_bootstrap_full($skip = FALSE) {
   // Let all modules take action before the menu system handles the request.
   // We do not want this while running update.php.
   if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') {
+    menu_set_custom_theme();
     drupal_theme_initialize();
   }
 }
diff --git a/core/includes/menu.inc b/core/includes/menu.inc
index 2e2c50d2ea674e8d5613217c7bab45578ef8cb45..a8a30b6c9a3fbc53453473289b35faaa1057c0e1 100644
--- a/core/includes/menu.inc
+++ b/core/includes/menu.inc
@@ -463,8 +463,8 @@ function menu_set_item($path, $router_item) {
  *   menu_router table. The value corresponding to the key 'map' holds the
  *   loaded objects. The value corresponding to the key 'access' is TRUE if the
  *   current user can access this page. The values corresponding to the keys
- *   'title', 'page_arguments', and 'access_arguments', will be filled in based
- *   on the database values and the objects loaded.
+ *   'title', 'page_arguments', 'access_arguments', and 'theme_arguments' will
+ *   be filled in based on the database values and the objects loaded.
  */
 function menu_get_item($path = NULL, $router_item = NULL) {
   $router_items = &drupal_static(__FUNCTION__);
@@ -501,6 +501,7 @@ function menu_get_item($path = NULL, $router_item = NULL) {
       if ($router_item['access']) {
         $router_item['map'] = $map;
         $router_item['page_arguments'] = array_merge(menu_unserialize($router_item['page_arguments'], $map), array_slice($map, $router_item['number_parts']));
+        $router_item['theme_arguments'] = array_merge(menu_unserialize($router_item['theme_arguments'], $map), array_slice($map, $router_item['number_parts']));
       }
     }
     $router_items[$path] = $router_item;
@@ -1794,6 +1795,51 @@ function drupal_help_arg($arg = array()) {
   return $arg + array('', '', '', '', '', '', '', '', '', '', '', '');
 }
 
+/**
+ * Gets the custom theme for the current page, if there is one.
+ *
+ * @param $initialize
+ *   This parameter should only be used internally; it is set to TRUE in order
+ *   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.
+ *
+ * @see menu_set_custom_theme()
+ */
+function menu_get_custom_theme($initialize = FALSE) {
+  $custom_theme = &drupal_static(__FUNCTION__);
+  // 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'))) {
+    // 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(\Drupal::moduleHandler()->invokeAll('custom_theme'), 'drupal_theme_access');
+    if (!empty($custom_themes)) {
+      $custom_theme = array_pop($custom_themes);
+    }
+    // If there is a theme callback function for the current page, execute it.
+    // If this returns a valid theme, it will override any theme that was set
+    // by a hook_custom_theme() implementation above.
+    $router_item = menu_get_item();
+    if (!empty($router_item['access']) && !empty($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;
+}
+
+/**
+ * Sets a custom theme for the current page, if there is one.
+ */
+function menu_set_custom_theme() {
+  menu_get_custom_theme(TRUE);
+}
+
 /**
  * Returns an array containing the names of system-defined (default) menus.
  */
@@ -3020,6 +3066,13 @@ function _menu_router_build($callbacks, $save = FALSE) {
             }
           }
         }
+        // Same for theme callbacks.
+        if (!isset($item['theme callback']) && isset($parent['theme callback'])) {
+          $item['theme callback'] = $parent['theme callback'];
+          if (!isset($item['theme arguments']) && isset($parent['theme arguments'])) {
+            $item['theme arguments'] = $parent['theme arguments'];
+          }
+        }
         // Same for load arguments: if a loader doesn't have any explict
         // arguments, try to find arguments in the parent.
         if (!isset($item['load arguments'])) {
@@ -3056,6 +3109,8 @@ function _menu_router_build($callbacks, $save = FALSE) {
       'page callback' => '',
       'title arguments' => array(),
       'title callback' => 't',
+      'theme arguments' => array(),
+      'theme callback' => '',
       'description' => '',
       'description arguments' => array(),
       'description callback' => 't',
@@ -3120,6 +3175,8 @@ function _menu_router_save($menu, $masks) {
       'title',
       'title_callback',
       'title_arguments',
+      'theme_callback',
+      'theme_arguments',
       'type',
       'description',
       'description_callback',
@@ -3150,6 +3207,8 @@ function _menu_router_save($menu, $masks) {
       'title' => $item['title'],
       'title_callback' => $item['title callback'],
       'title_arguments' => ($item['title arguments'] ? serialize($item['title arguments']) : ''),
+      'theme_callback' => $item['theme callback'],
+      'theme_arguments' => serialize($item['theme arguments']),
       'type' => $item['type'],
       'description' => $item['description'],
       'description_callback' => $item['description callback'],
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 98be6deb80e43c1323181df780b0b2e76828e4fb..a6d9362165742850860452ad582d062f65b4c98f 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -92,14 +92,16 @@ function drupal_theme_initialize() {
   }
 
   drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE);
-
   $themes = list_themes();
 
-  // @todo Let the theme.negotiator listen to the kernel request event.
-  // Determine the active theme for the theme negotiator service. This includes
-  // the default theme as well as really specific ones like the ajax base theme.
-  $request = \Drupal::request();
-  $theme = \Drupal::service('theme.negotiator')->determineActiveTheme($request) ?: 'stark';
+  // Only select the user selected theme if it is available in the
+  // list of themes that can be accessed.
+  $theme = !empty($user->theme) && drupal_theme_access($user->theme) ? $user->theme : \Drupal::config('system.theme')->get('default');
+
+  // 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 = !empty($custom_theme) ? $custom_theme : $theme;
 
   // Store the identifier for retrieving theme settings with.
   $theme_key = $theme;
@@ -112,6 +114,9 @@ function drupal_theme_initialize() {
     $base_theme[] = $themes[$ancestor];
   }
   _drupal_theme_initialize($themes[$theme], array_reverse($base_theme));
+
+  // Themes can have alter functions, so reset the drupal_alter() cache.
+  drupal_static_reset('drupal_alter');
 }
 
 /**
diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php
index 098b1653aa770df172baf485241ec29f52149a1c..8a525c9abdbcd96eaa0d80cfe5642ba41408399c 100644
--- a/core/lib/Drupal/Core/CoreServiceProvider.php
+++ b/core/lib/Drupal/Core/CoreServiceProvider.php
@@ -23,7 +23,6 @@
 use Drupal\Core\DependencyInjection\Compiler\RegisterBreadcrumbBuilderPass;
 use Drupal\Core\DependencyInjection\Compiler\RegisterAuthenticationPass;
 use Drupal\Core\DependencyInjection\Compiler\RegisterTwigExtensionsPass;
-use Drupal\Core\Theme\ThemeNegotiatorPass;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\DependencyInjection\Reference;
 use Symfony\Component\DependencyInjection\Definition;
@@ -72,9 +71,6 @@ public function register(ContainerBuilder $container) {
     // Add the compiler pass that will process the tagged breadcrumb builder
     // services.
     $container->addCompilerPass(new RegisterBreadcrumbBuilderPass());
-    // Add the compiler pass that will process the tagged theme negotiator
-    // service.
-    $container->addCompilerPass(new ThemeNegotiatorPass());
     // Add the compiler pass that lets service providers modify existing
     // service definitions.
     $container->addCompilerPass(new ModifyServiceDefinitionsPass());
diff --git a/core/lib/Drupal/Core/EventSubscriber/LegacyRequestSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/LegacyRequestSubscriber.php
index 2a495f4dd19405fb47aa8f31ca36ab113e343a0d..39828ef352f73e0c4b44e5ca476d9bddbd858374 100644
--- a/core/lib/Drupal/Core/EventSubscriber/LegacyRequestSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/LegacyRequestSubscriber.php
@@ -27,6 +27,9 @@ class LegacyRequestSubscriber implements EventSubscriberInterface {
    */
   public function onKernelRequestLegacy(GetResponseEvent $event) {
     if ($event->getRequestType() == HttpKernelInterface::MASTER_REQUEST) {
+      menu_set_custom_theme();
+      drupal_theme_initialize();
+
       // Tell Drupal it is now fully bootstrapped (for the benefit of code that
       // calls drupal_get_bootstrap_phase()), but without having
       // _drupal_bootstrap_full() do anything, since we've already done the
@@ -36,16 +39,6 @@ public function onKernelRequestLegacy(GetResponseEvent $event) {
     }
   }
 
-  /**
-   * Initializes the theme system after the routing system.
-   *
-   * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
-   *   The Event to process.
-   */
-  public function onKernelRequestLegacyAfterRouting(GetResponseEvent $event) {
-    drupal_theme_initialize();
-  }
-
   /**
    * Registers the methods in this class that should be listeners.
    *
@@ -54,8 +47,6 @@ public function onKernelRequestLegacyAfterRouting(GetResponseEvent $event) {
    */
   static function getSubscribedEvents() {
     $events[KernelEvents::REQUEST][] = array('onKernelRequestLegacy', 90);
-    // Initialize the theme system after the routing system.
-    $events[KernelEvents::REQUEST][] = array('onKernelRequestLegacyAfterRouting', 30);
 
     return $events;
   }
diff --git a/core/lib/Drupal/Core/Theme/AjaxBasePageNegotiator.php b/core/lib/Drupal/Core/Theme/AjaxBasePageNegotiator.php
deleted file mode 100644
index d2d2004f2e0a53411bc384454d62efd56c86b841..0000000000000000000000000000000000000000
--- a/core/lib/Drupal/Core/Theme/AjaxBasePageNegotiator.php
+++ /dev/null
@@ -1,84 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Core\Theme\AjaxBasePageNegotiator.
- */
-
-namespace Drupal\Core\Theme;
-
-use Drupal\Core\Access\CsrfTokenGenerator;
-use Drupal\Core\Config\ConfigFactory;
-use Symfony\Cmf\Component\Routing\RouteObjectInterface;
-use Symfony\Component\HttpFoundation\Request;
-
-/**
- * Defines a theme negotiator that deals with the active theme on ajax requests.
- *
- * Many different pages can invoke an Ajax request to system/ajax or another
- * generic Ajax path. It is almost always desired for an Ajax response to be
- * rendered using the same theme as the base page, because most themes are built
- * with the assumption that they control the entire page, so if the CSS for two
- * themes are both loaded for a given page, they may conflict with each other.
- * For example, Bartik is Drupal's default theme, and Seven is Drupal's default
- * administration theme. Depending on whether the "Use the administration theme
- * when editing or creating content" checkbox is checked, the node edit form may
- * be displayed in either theme, but the Ajax response to the Field module's
- * "Add another item" button should be rendered using the same theme as the rest
- * of the page.
- *
- * Therefore specify '_theme: ajax_base_page' as part of the router options.
- */
-class AjaxBasePageNegotiator implements ThemeNegotiatorInterface {
-
-  /**
-   * The CSRF token generator.
-   *
-   * @var \Drupal\Core\Access\CsrfTokenGenerator
-   */
-  protected $csrfGenerator;
-
-  /**
-   * The config factory.
-   *
-   * @var \Drupal\Core\Config\ConfigFactory
-   */
-  protected $configFactory;
-
-  /**
-   * Constructs a new AjaxBasePageNegotiator.
-   *
-   * @param \Drupal\Core\Access\CsrfTokenGenerator $token_generator
-   *   The CSRF token generator.
-   * @param \Drupal\Core\Config\ConfigFactory $config_factory
-   *   The config factory.
-   */
-  public function __construct(CsrfTokenGenerator $token_generator, ConfigFactory $config_factory) {
-    $this->csrfGenerator = $token_generator;
-    $this->configFactory = $config_factory;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function determineActiveTheme(Request $request) {
-    // Check whether the route was configured to use the base page theme.
-    if (!(($route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)) && $route->hasOption('_theme') && $route->getOption('_theme') == 'ajax_base_page')) {
-      return NULL;
-    }
-    if (($ajax_page_state = $request->request->get('ajax_page_state'))  && !empty($ajax_page_state['theme']) && !empty($ajax_page_state['theme_token'])) {
-      $theme = $ajax_page_state['theme'];
-      $token = $ajax_page_state['theme_token'];
-
-      // Prevent a request forgery from giving a person access to a theme they
-      // shouldn't be otherwise allowed to see. However, since everyone is allowed
-      // to see the default theme, token validation isn't required for that, and
-      // bypassing it allows most use-cases to work even when accessed from the
-      // page cache.
-      if ($theme === $this->configFactory->get('system.theme')->get('default') || $this->csrfGenerator->validate($token, $theme)) {
-        return $theme;
-      }
-    }
-  }
-
-}
diff --git a/core/lib/Drupal/Core/Theme/DefaultNegotiator.php b/core/lib/Drupal/Core/Theme/DefaultNegotiator.php
deleted file mode 100644
index d59667806acca18b6a07ce81c3f878c81f9d21b9..0000000000000000000000000000000000000000
--- a/core/lib/Drupal/Core/Theme/DefaultNegotiator.php
+++ /dev/null
@@ -1,42 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Core\Theme\DefaultNegotiator.
- */
-
-namespace Drupal\Core\Theme;
-
-use Drupal\Core\Config\ConfigFactory;
-use Symfony\Component\HttpFoundation\Request;
-
-/**
- * Determines the default theme of the site.
- */
-class DefaultNegotiator implements ThemeNegotiatorInterface {
-
-  /**
-   * The system theme config object.
-   *
-   * @var \Drupal\Core\Config\Config
-   */
-  protected $config;
-
-  /**
-   * Constructs a DefaultNegotiator object.
-   *
-   * @param \Drupal\Core\Config\ConfigFactory $config_factory
-   *   The config factory.
-   */
-  public function __construct(ConfigFactory $config_factory) {
-    $this->config = $config_factory->get('system.theme');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function determineActiveTheme(Request $request) {
-    return $this->config->get('default');
-  }
-
-}
diff --git a/core/lib/Drupal/Core/Theme/ThemeNegotiator.php b/core/lib/Drupal/Core/Theme/ThemeNegotiator.php
deleted file mode 100644
index 96216ca159d87788215f2dfdbbf3628c53be93da..0000000000000000000000000000000000000000
--- a/core/lib/Drupal/Core/Theme/ThemeNegotiator.php
+++ /dev/null
@@ -1,133 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Core\Theme\ThemeNegotiator.
- */
-
-namespace Drupal\Core\Theme;
-
-use Symfony\Component\HttpFoundation\Request;
-
-/**
- * Provides a class which determines the active theme of the page.
- *
- * It therefore uses ThemeNegotiatorInterface objects which are passed in
- * using the 'theme_negotiator' tag.
- *
- * @see \Drupal\Core\Theme\ThemeNegotiatorPass
- * @see \Drupal\Core\Theme\ThemeNegotiatorInterface
- */
-class ThemeNegotiator implements ThemeNegotiatorInterface {
-
-  /**
-   * Holds arrays of theme negotiators, keyed by priority.
-   *
-   * @var array
-   */
-  protected $negotiators = array();
-
-  /**
-   * Holds the array of theme negotiators sorted by priority.
-   *
-   * Set to NULL if the array needs to be re-calculated.
-   *
-   * @var array|NULL
-   */
-  protected $sortedNegotiators;
-
-  /**
-   * The current request.
-   *
-   * @var \Symfony\Component\HttpFoundation\Request
-   */
-  protected $request;
-
-  /**
-   * The access checker for themes.
-   *
-   * @var \Drupal\Core\Theme\ThemeAccessCheck
-   */
-  protected $themeAccess;
-
-  /**
-   * Constructs a new ThemeNegotiator.
-   *
-   * @param \Drupal\Core\Theme\ThemeAccessCheck $theme_access
-   *   The access checker for themes.
-   */
-  public function __construct(ThemeAccessCheck $theme_access) {
-    $this->themeAccess = $theme_access;
-  }
-
-  /**
-   * Sets the request object to use.
-   *
-   * @param \Symfony\Component\HttpFoundation\Request $request
-   *   The request object.
-   */
-  public function setRequest(Request $request) {
-    $this->request = $request;
-  }
-
-  /**
-   * Adds a active theme negotiation service.
-   *
-   * @param \Drupal\Core\Theme\ThemeNegotiatorInterface $negotiator
-   *   The theme negotiator to add.
-   * @param int $priority
-   *   Priority of the breadcrumb builder.
-   */
-  public function addNegotiator(ThemeNegotiatorInterface $negotiator, $priority) {
-    $this->negotiators[$priority][] = $negotiator;
-    // Force the negotiators to be re-sorted.
-    $this->sortedNegotiators = NULL;
-  }
-
-  /**
-   * Returns the sorted array of theme negotiators.
-   *
-   * @return array|\Drupal\Core\Theme\ThemeNegotiatorInterface[]
-   *   An array of breadcrumb builder objects.
-   */
-  protected function getSortedNegotiators() {
-    if (!isset($this->sortedNegotiators)) {
-      // Sort the negotiators according to priority.
-      krsort($this->negotiators);
-      // Merge nested negotiators from $this->negotiators into
-      // $this->sortedNegotiators.
-      $this->sortedNegotiators = array();
-      foreach ($this->negotiators as $builders) {
-        $this->sortedNegotiators = array_merge($this->sortedNegotiators, $builders);
-      }
-    }
-    return $this->sortedNegotiators;
-  }
-
-  /**
-   * Get the current active theme.
-   *
-   * @return string
-   *   The current active string.
-   */
-  public function getActiveTheme() {
-    if (!$this->request->attributes->has('_theme_active')) {
-      $this->determineActiveTheme($this->request);
-    }
-    return $this->request->attributes->get('_theme_active');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function determineActiveTheme(Request $request) {
-    foreach ($this->getSortedNegotiators() as $negotiator) {
-      $theme = $negotiator->determineActiveTheme($request);
-      if ($theme !== NULL && $this->themeAccess->checkAccess($theme)) {
-        $request->attributes->set('_theme_active', $theme);
-        return $request->attributes->get('_theme_active');
-      }
-    }
-  }
-
-}
diff --git a/core/lib/Drupal/Core/Theme/ThemeNegotiatorInterface.php b/core/lib/Drupal/Core/Theme/ThemeNegotiatorInterface.php
deleted file mode 100644
index 55deaebf838d983470fb4aab02be5e08b6e2f531..0000000000000000000000000000000000000000
--- a/core/lib/Drupal/Core/Theme/ThemeNegotiatorInterface.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Core\Theme\ThemeNegotiatorInterface.
- */
-
-namespace Drupal\Core\Theme;
-
-use Symfony\Component\HttpFoundation\Request;
-
-/**
- * Defines an interface for classes which determine the active theme.
- *
- * To set the active theme, create a new service tagged with 'theme_negotiator'
- * (see user.services.yml for an example). The only method this service needs
- * to implement is determineActiveTheme. Return the name of the theme, or NULL
- * if other negotiators like the configured default one should kick in instead.
- *
- * If you are setting a theme which is closely tied to the functionality of a
- * particular page or set of pages (such that the page might not function
- * correctly if a different theme is used), make sure to set the priority on
- * the service to a high number so that it is not accidentally overridden by
- * other theme negotiators. By convention, a priority of "1000" is used in
- * these cases; see \Drupal\Core\Theme\AjaxBasePageNegotiator and
- * core.services.yml for an example.
- */
-interface ThemeNegotiatorInterface {
-
-  /**
-   * Determine the active theme for the request.
-   *
-   * @param \Symfony\Component\HttpFoundation\Request $request
-   *   The active request of the site.
-   *
-   * @return string|null
-   *   Returns the active theme name, else return NULL.
-   */
-  public function determineActiveTheme(Request $request);
-
-}
diff --git a/core/lib/Drupal/Core/Theme/ThemeNegotiatorPass.php b/core/lib/Drupal/Core/Theme/ThemeNegotiatorPass.php
deleted file mode 100644
index 7f1469295944498305c77fb8be13f94cc34088a4..0000000000000000000000000000000000000000
--- a/core/lib/Drupal/Core/Theme/ThemeNegotiatorPass.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Core\Theme\ThemeNegotiatorPass.
- */
-
-namespace Drupal\Core\Theme;
-
-use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
-use Symfony\Component\DependencyInjection\ContainerBuilder;
-use Symfony\Component\DependencyInjection\Reference;
-
-/**
- * Adds services to the theme negotiator service.
- *
- * @see \Drupal\Core\Theme\ThemeNegotiator
- * @see \Drupal\Core\Theme\ThemeNegotiatorInterfa
- */
-class ThemeNegotiatorPass implements CompilerPassInterface {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function process(ContainerBuilder $container) {
-    if (!$container->hasDefinition('theme.negotiator')) {
-      return;
-    }
-    $manager = $container->getDefinition('theme.negotiator');
-    foreach ($container->findTaggedServiceIds('theme_negotiator') as $id => $attributes) {
-      $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
-      $manager->addMethodCall('addNegotiator', array(new Reference($id), $priority));
-    }
-  }
-
-}
diff --git a/core/misc/ajax.js b/core/misc/ajax.js
index e9baf942c5afb995e0d2c57b7b8b8d82a2b69519..a586945fddea762a139bb289b9fefa7499d280b3 100644
--- a/core/misc/ajax.js
+++ b/core/misc/ajax.js
@@ -382,7 +382,7 @@ Drupal.ajax.prototype.beforeSerialize = function (element, options) {
 
   // Allow Drupal to return new JavaScript and CSS files to load without
   // returning the ones already loaded.
-  // @see \Drupal\Core\Theme\AjaxBasePageNegotiator
+  // @see ajax_base_page_theme()
   // @see drupal_get_css()
   // @see drupal_get_js()
   var pageState = drupalSettings.ajaxPageState;
diff --git a/core/modules/block/block.module b/core/modules/block/block.module
index afe58625a6c2417b24dabc0138dc6afee547ffdb..e1bc631887adf88467a80faadf7d3d9d4d0b5031 100644
--- a/core/modules/block/block.module
+++ b/core/modules/block/block.module
@@ -109,9 +109,41 @@ function block_menu() {
     'type' => MENU_VISIBLE_IN_BREADCRUMB,
     'route_name' => 'block.admin_add',
   );
+  // Block administration is tied to the theme and plugin definition so
+  // that the plugin can appropriately attach to this URL structure.
+  // @todo D8: Use dynamic % arguments instead of static, hard-coded theme names
+  //   and plugin IDs to decouple the routes from these dependencies.
+  // @see http://drupal.org/node/1067408
+  foreach (list_themes() as $key => $theme) {
+    $items["admin/structure/block/demo/$key"] = array(
+      'route_name' => 'block.admin_demo',
+      'type' => MENU_CALLBACK,
+      'theme callback' => '_block_custom_theme',
+      'theme arguments' => array($key),
+    );
+  }
   return $items;
 }
 
+/**
+ * Theme callback: Uses the theme specified in the parameter.
+ *
+ * @param $theme
+ *   The theme whose blocks are being configured. If not set, the default theme
+ *   is assumed.
+ *
+ * @return
+ *   The theme that should be used for the block configuration page, or NULL
+ *   to indicate that the default theme should be used.
+ *
+ * @see block_menu()
+ */
+function _block_custom_theme($theme = NULL) {
+  // We return exactly what was passed in, to guarantee that the page will
+  // always be displayed using the theme whose blocks are being configured.
+  return $theme;
+}
+
 /**
  * Implements hook_page_build().
  *
diff --git a/core/modules/block/block.services.yml b/core/modules/block/block.services.yml
index 29a31c26d3b3d0e3571d3417c0a88b0f435a506a..b6bf1f41d373427fe61239426e9ceb29a5956aeb 100644
--- a/core/modules/block/block.services.yml
+++ b/core/modules/block/block.services.yml
@@ -9,8 +9,3 @@ services:
     factory_method: get
     factory_service: cache_factory
     arguments: [block]
-  theme.negotiator.block.admin_demo:
-    class: Drupal\block\Theme\AdminDemoNegotiator
-    tags:
-      - { name: theme_negotiator, priority: 1000 }
-
diff --git a/core/modules/block/lib/Drupal/block/Theme/AdminDemoNegotiator.php b/core/modules/block/lib/Drupal/block/Theme/AdminDemoNegotiator.php
deleted file mode 100644
index d3dcb337605b10cdbc49faf15102e324d0d7a074..0000000000000000000000000000000000000000
--- a/core/modules/block/lib/Drupal/block/Theme/AdminDemoNegotiator.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\block\Theme\AdminDemoNegotiator.
- */
-
-namespace Drupal\block\Theme;
-
-use Drupal\Core\Theme\ThemeNegotiatorInterface;
-use Symfony\Cmf\Component\Routing\RouteObjectInterface;
-use Symfony\Component\HttpFoundation\Request;
-
-/**
- * Negotiates the theme for the block admin demo page via the URL.
- */
-class AdminDemoNegotiator implements ThemeNegotiatorInterface {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function determineActiveTheme(Request $request) {
-    // We return exactly what was passed in, to guarantee that the page will
-    // always be displayed using the theme whose blocks are being configured.
-    if ($request->attributes->get(RouteObjectInterface::ROUTE_NAME) == 'block.admin_demo') {
-      return $request->attributes->get('theme');
-    }
-  }
-
-}
diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module
index 46ed263bad3e101b5258f85a8f3b0154d4971d76..43127b0d7a37adc05dc8d1acf32dc649a2e6c63a 100644
--- a/core/modules/content_translation/content_translation.module
+++ b/core/modules/content_translation/content_translation.module
@@ -156,7 +156,7 @@ function content_translation_menu() {
     if (content_translation_enabled($entity_type)) {
       $path = _content_translation_link_to_router_path($entity_type, $info['links']['canonical']);
       $entity_position = count(explode('/', $path)) - 1;
-      $keys = array_flip(array('load_arguments'));
+      $keys = array_flip(array('theme_callback', 'theme_arguments', 'load_arguments'));
       $menu_info = array_intersect_key($info['translation']['content_translation'], $keys) + array('file' => 'content_translation.pages.inc');
       $item = array();
 
diff --git a/core/modules/contextual/contextual.module b/core/modules/contextual/contextual.module
index 9f793cb61b02ca52716158dce878b18d6c6c9434..e405f206f40cf79cd1db2ae50ed578f3cd290d4b 100644
--- a/core/modules/contextual/contextual.module
+++ b/core/modules/contextual/contextual.module
@@ -6,6 +6,21 @@
  * Adds contextual links to perform actions related to elements on a page.
  */
 
+/**
+ * Implements hook_menu().
+ */
+function contextual_menu() {
+  // @todo Remove this menu item in http://drupal.org/node/1954892 when theme
+  //   callbacks are replaced with something else.
+  $items['contextual/render'] = array(
+    'route_name' => 'contextual.render',
+    'theme callback' => 'ajax_base_page_theme',
+    'type' => MENU_CALLBACK,
+  );
+
+  return $items;
+}
+
 /**
  * Implements hook_toolbar().
  */
diff --git a/core/modules/contextual/contextual.routing.yml b/core/modules/contextual/contextual.routing.yml
index b44d61920ea8f9c15370038d305fce1a6695c601..8ab2f2887bf5eeff1e15d5b05967bd7af585e845 100644
--- a/core/modules/contextual/contextual.routing.yml
+++ b/core/modules/contextual/contextual.routing.yml
@@ -2,7 +2,5 @@ contextual.render:
   path: '/contextual/render'
   defaults:
     _controller: '\Drupal\contextual\ContextualController::render'
-  options:
-    _theme: ajax_base_page
   requirements:
     _permission: 'access contextual links'
diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module
index 291c4be6e8422880b98b6290305e8e0517367f85..9e4d1326203239e950232f8414f46d958b479a70 100644
--- a/core/modules/edit/edit.module
+++ b/core/modules/edit/edit.module
@@ -17,6 +17,26 @@
 use Drupal\entity\Entity\EntityDisplay;
 use Drupal\user\TempStoreFactory;
 
+/**
+ * Implements hook_menu().
+ */
+function edit_menu() {
+  // @todo Remove these menu items in http://drupal.org/node/1954892 when theme
+  //   callbacks are replaced with something else.
+  $items['edit/metadata'] = array(
+    'route_name' => 'edit.metadata',
+    'theme callback' => 'ajax_base_page_theme',
+    'type' => MENU_CALLBACK,
+  );
+  $items['edit/form/%/%/%/%/%'] = array(
+    'route_name' => 'edit.field_form',
+    'theme callback' => 'ajax_base_page_theme',
+    'type' => MENU_CALLBACK,
+  );
+
+  return $items;
+}
+
 /**
  * Implements hook_permission().
  */
diff --git a/core/modules/edit/edit.routing.yml b/core/modules/edit/edit.routing.yml
index 80fbbf8e4f3eca843e81183f35fede728b1d5bf7..41acaacc090a3d4d00b32dbb5ba3282c7574815c 100644
--- a/core/modules/edit/edit.routing.yml
+++ b/core/modules/edit/edit.routing.yml
@@ -2,10 +2,9 @@ edit.metadata:
   path: '/edit/metadata'
   defaults:
     _controller: '\Drupal\edit\EditController::metadata'
-  options:
-    _theme: ajax_base_page
   requirements:
     _permission: 'access in-place editing'
+
 edit.attachments:
   path: '/edit/attachments'
   defaults:
@@ -17,9 +16,6 @@ edit.field_form:
   path: '/edit/form/{entity_type}/{entity}/{field_name}/{langcode}/{view_mode_id}'
   defaults:
     _controller: '\Drupal\edit\EditController::fieldForm'
-  options:
-    _access_mode: 'ALL'
-    _theme: ajax_base_page
   requirements:
     _permission: 'access in-place editing'
     _access_edit_entity_field: 'TRUE'
diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module
index 07fc1b30b04d7695fa6071224a8b667d7940760c..4a560ee7c22d6a640b3a92a8e274781deb3229a2 100644
--- a/core/modules/editor/editor.module
+++ b/core/modules/editor/editor.module
@@ -139,6 +139,21 @@ function editor_library_info() {
   return $libraries;
 }
 
+/**
+ * Implements hook_menu().
+ */
+function editor_menu() {
+  // @todo Remove this menu item in http://drupal.org/node/1954892 when theme
+  //   callbacks are replaced with something else.
+  $items['editor/%/%/%/%/%'] = array(
+    'route_name' => 'editor.field_untransformed_text',
+    'theme callback' => 'ajax_base_page_theme',
+    'type' => MENU_CALLBACK,
+  );
+
+  return $items;
+}
+
 /**
  * Implements hook_form_FORM_ID_alter().
  */
diff --git a/core/modules/editor/editor.routing.yml b/core/modules/editor/editor.routing.yml
index bf9d3607c52d26dce28d6bc5579932dbc51ad49e..3308dd07efc44e94d49379a14c2f23b8ff195272 100644
--- a/core/modules/editor/editor.routing.yml
+++ b/core/modules/editor/editor.routing.yml
@@ -2,8 +2,6 @@ editor.field_untransformed_text:
   path: '/editor/{entity_type}/{entity}/{field_name}/{langcode}/{view_mode_id}'
   defaults:
     _controller: '\Drupal\editor\EditorController::getUntransformedText'
-  options:
-    _theme: ajax_base_page
   requirements:
     _permission: 'access in-place editing'
     _access_edit_entity_field: 'TRUE'
diff --git a/core/modules/file/file.module b/core/modules/file/file.module
index e96a4c1f71a9e5b4bb1f6c9b6a222572dee50837..9bc1d858684572df262241da751310ad62021763 100644
--- a/core/modules/file/file.module
+++ b/core/modules/file/file.module
@@ -37,6 +37,21 @@ function file_help($path, $arg) {
   }
 }
 
+/**
+ * Implements hook_menu().
+ */
+function file_menu() {
+  $items = array();
+
+  $items['file/ajax'] = array(
+    'route_name' => 'file.ajax_upload',
+    'theme callback' => 'ajax_base_page_theme',
+    'type' => MENU_CALLBACK,
+  );
+
+  return $items;
+}
+
 /**
  * Implements hook_element_info().
  *
diff --git a/core/modules/file/file.routing.yml b/core/modules/file/file.routing.yml
index d9d4efa0aff61727f0fea37e414b384e606cba06..8bf971c9a21e2ebacc36c54b399e118fbd0da60f 100644
--- a/core/modules/file/file.routing.yml
+++ b/core/modules/file/file.routing.yml
@@ -2,8 +2,6 @@ file.ajax_upload:
   path: '/file/ajax'
   defaults:
     _controller: '\Drupal\file\Controller\FileWidgetAjaxController::upload'
-  options:
-    _theme: ajax_base_page
   requirements:
     _permission: 'access content'
 
diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchPageOverrideTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchPageOverrideTest.php
index 101b745395f7d35e4ae9cd439136cf1c5cf36248..ab3f9135c86638a241f14d2e937c103dc113fdaa 100644
--- a/core/modules/search/lib/Drupal/search/Tests/SearchPageOverrideTest.php
+++ b/core/modules/search/lib/Drupal/search/Tests/SearchPageOverrideTest.php
@@ -38,7 +38,7 @@ function setUp() {
 
     // Enable the extra type module for searching.
     \Drupal::config('search.settings')->set('active_plugins', array('node_search', 'user_search', 'search_extra_type_search'))->save();
-    \Drupal::service('router.builder')->rebuild();
+    \Drupal::state()->set('menu_rebuild_needed', TRUE);
   }
 
   function testSearchPageHook() {
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutLinksTest.php b/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutLinksTest.php
index b747d793b062b7171a66c2876828ede99e261ba7..bd2d194c2503ef5899e18d3a6e37e7760ed69ec9 100644
--- a/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutLinksTest.php
+++ b/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutLinksTest.php
@@ -145,7 +145,6 @@ function testShortcutLinkDelete() {
    */
   function testNoShortcutLink() {
     // Change to a theme that displays shortcuts.
-    theme_enable(array('seven'));
     \Drupal::config('system.theme')
       ->set('default', 'seven')
       ->save();
@@ -158,9 +157,8 @@ function testNoShortcutLink() {
     $this->assertNoRaw('add-shortcut', 'Add to shortcuts link was not shown on a page the user does not have access to.');
 
     // Verify that the testing mechanism works by verifying the shortcut
-    // link appears on admin/people.
-    $this->drupalGet('admin/people');
+    // link appears on admin/content/node.
+    $this->drupalGet('admin/content/node');
     $this->assertRaw('add-shortcut', 'Add to shortcuts link was shown on a page the user does have access to.');
   }
-
 }
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutTestBase.php b/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutTestBase.php
index e0bdb31cf5af4022beb18b163ae11ece0b30af49..06088ef4b68c1b935a0f0760dd6723f5ed139ea7 100644
--- a/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutTestBase.php
+++ b/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutTestBase.php
@@ -69,7 +69,7 @@ function setUp() {
     }
 
     // Create users.
-    $this->admin_user = $this->drupalCreateUser(array('access toolbar', 'administer shortcuts', 'view the administration theme', 'create article content', 'create page content', 'access content overview', 'administer users'));
+    $this->admin_user = $this->drupalCreateUser(array('access toolbar', 'administer shortcuts', 'view the administration theme', 'create article content', 'create page content', 'access content overview'));
     $this->shortcut_user = $this->drupalCreateUser(array('customize shortcut links', 'switch shortcut sets'));
 
     // Create a node.
diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php
index ccc327dcba383bfdf82f4b525909a17b6adf1c71..71cf0c4d771354404724cdee79ec788b45d08c78 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php
@@ -481,9 +481,18 @@ public function testThemeIntegration() {
     $this->initializeTestThemeConfiguration();
     $this->doTestThemeCallbackFakeTheme();
 
+    $this->initializeTestThemeConfiguration();
+    $this->doTestHookCustomTheme();
+
+    $this->initializeTestThemeConfiguration();
+    $this->doTestThemeCallbackHookCustomTheme();
+
     $this->initializeTestThemeConfiguration();
     $this->doTestThemeCallbackAdministrative();
 
+    $this->initializeTestThemeConfiguration();
+    $this->doTestThemeCallbackInheritance();
+
     $this->initializeTestThemeConfiguration();
     $this->doTestThemeCallbackNoThemeRequested();
 
@@ -507,17 +516,27 @@ protected function initializeTestThemeConfiguration() {
   }
 
   /**
-   * Test the theme negotiation when it is set to use an administrative theme.
+   * Test the theme callback when it is set to use an administrative theme.
    */
   protected function doTestThemeCallbackAdministrative() {
     theme_enable(array($this->admin_theme));
     $this->drupalGet('menu-test/theme-callback/use-admin-theme');
-    $this->assertText('Active theme: seven. Actual theme: seven.', 'The administrative theme can be correctly set in a theme negotiation.');
+    $this->assertText('Custom theme: seven. Actual theme: seven.', 'The administrative theme can be correctly set in a theme callback.');
+    $this->assertRaw('seven/style.css', "The administrative theme's CSS appears on the page.");
+  }
+
+  /**
+   * Test that the theme callback is properly inherited.
+   */
+  protected function doTestThemeCallbackInheritance() {
+    theme_enable(array($this->admin_theme));
+    $this->drupalGet('menu-test/theme-callback/use-admin-theme/inheritance');
+    $this->assertText('Custom theme: seven. Actual theme: seven. Theme callback inheritance is being tested.', 'Theme callback inheritance correctly uses the administrative theme.');
     $this->assertRaw('seven/style.css', "The administrative theme's CSS appears on the page.");
   }
 
   /**
-   * Test the theme negotiation when the site is in maintenance mode.
+   * Test the theme callback when the site is in maintenance mode.
    */
   protected function doTestThemeCallbackMaintenanceMode() {
     $this->container->get('state')->set('system.maintenance_mode', TRUE);
@@ -532,44 +551,76 @@ protected function doTestThemeCallbackMaintenanceMode() {
     $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('Active theme: seven. Actual theme: seven.', 'The theme negotiation system is correctly triggered for an administrator when the site is in maintenance mode.');
+    $this->assertText('Custom theme: seven. Actual theme: seven.', 'The theme callback system is correctly triggered for an administrator when the site is in maintenance mode.');
     $this->assertRaw('seven/style.css', "The administrative theme's CSS appears on the page.");
 
     $this->container->get('state')->set('system.maintenance_mode', FALSE);
   }
 
   /**
-   * Test the theme negotiation when it is set to use an optional theme.
+   * Test the theme callback when it is set to use an optional theme.
    */
   protected function doTestThemeCallbackOptionalTheme() {
     // Request a theme that is not enabled.
     $this->drupalGet('menu-test/theme-callback/use-stark-theme');
-    $this->assertText('Active theme: bartik. Actual theme: bartik.', 'The theme negotiation system falls back on the default theme when a theme that is not enabled is requested.');
+    $this->assertText('Custom theme: NONE. Actual theme: bartik.', 'The theme callback system falls back on the default theme when a theme that is not enabled is requested.');
     $this->assertRaw('bartik/css/style.css', "The default theme's CSS appears on the page.");
 
     // Now enable the theme and request it again.
     theme_enable(array($this->alternate_theme));
     $this->drupalGet('menu-test/theme-callback/use-stark-theme');
-    $this->assertText('Active theme: stark. Actual theme: stark.', 'The theme negotiation system uses an optional theme once it has been enabled.');
+    $this->assertText('Custom theme: stark. Actual theme: stark.', 'The theme callback system uses an optional theme once it has been enabled.');
     $this->assertRaw('stark/css/layout.css', "The optional theme's CSS appears on the page.");
   }
 
   /**
-   * Test the theme negotiation when it is set to use a theme that does not exist.
+   * Test the theme callback when it is set to use a theme that does not exist.
    */
   protected function doTestThemeCallbackFakeTheme() {
     $this->drupalGet('menu-test/theme-callback/use-fake-theme');
-    $this->assertText('Active theme: bartik. Actual theme: bartik.', 'The theme negotiation system falls back on the default theme when a theme that does not exist is requested.');
+    $this->assertText('Custom theme: NONE. Actual theme: bartik.', 'The theme callback system falls back on the default theme when a theme that does not exist is requested.');
     $this->assertRaw('bartik/css/style.css', "The default theme's CSS appears on the page.");
   }
 
   /**
-   * Test the theme negotiation when no theme is requested.
+   * Test the theme callback when no theme is requested.
    */
   protected function doTestThemeCallbackNoThemeRequested() {
     $this->drupalGet('menu-test/theme-callback/no-theme-requested');
-    $this->assertText('Active theme: bartik. Actual theme: bartik.', 'The theme negotiation system falls back on the default theme when no theme is requested.');
+    $this->assertText('Custom theme: NONE. Actual theme: bartik.', 'The theme callback system falls back on the default theme when no theme is requested.');
     $this->assertRaw('bartik/css/style.css', "The default theme's CSS appears on the page.");
   }
 
+  /**
+   * Test that hook_custom_theme() can control the theme of a page.
+   */
+  protected function doTestHookCustomTheme() {
+    // Trigger hook_custom_theme() to dynamically request the Stark theme for
+    // the requested page.
+    \Drupal::state()->set('menu_test.hook_custom_theme_name', $this->alternate_theme);
+    theme_enable(array($this->alternate_theme, $this->admin_theme));
+
+    // Visit a page that does not implement a theme callback. The above request
+    // should be honored.
+    $this->drupalGet('menu-test/no-theme-callback');
+    $this->assertText('Custom theme: stark. Actual theme: stark.', 'The result of hook_custom_theme() is used as the theme for the current page.');
+    $this->assertRaw('stark/css/layout.css', "The Stark theme's CSS appears on the page.");
+  }
+
+  /**
+   * Test that the theme callback wins out over hook_custom_theme().
+   */
+  protected function doTestThemeCallbackHookCustomTheme() {
+    // Trigger hook_custom_theme() to dynamically request the Stark theme for
+    // the requested page.
+    \Drupal::state()->set('menu_test.hook_custom_theme_name', $this->alternate_theme);
+    theme_enable(array($this->alternate_theme, $this->admin_theme));
+
+    // The menu "theme callback" should take precedence over a value set in
+    // hook_custom_theme().
+    $this->drupalGet('menu-test/theme-callback/use-admin-theme');
+    $this->assertText('Custom theme: seven. Actual theme: seven.', 'The result of hook_custom_theme() does not override what was set in a theme callback.');
+    $this->assertRaw('seven/style.css', "The Seven theme's CSS appears on the page.");
+  }
+
 }
diff --git a/core/modules/system/lib/Drupal/system/Tests/System/ThemeTest.php b/core/modules/system/lib/Drupal/system/Tests/System/ThemeTest.php
index 554450accbc60ec837543b70b74e90294e675f4e..9a13acbc0e4d8a1e0c0ebc40a36b29da76e3d5d9 100644
--- a/core/modules/system/lib/Drupal/system/Tests/System/ThemeTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/System/ThemeTest.php
@@ -178,7 +178,7 @@ function testThemeSettings() {
    * Test the administration theme functionality.
    */
   function testAdministrationTheme() {
-    theme_enable(array('bartik', 'seven'));
+    theme_enable(array('seven'));
 
     // Enable an administration theme and show it on the node admin pages.
     $edit = array(
diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php
index 7263a022939a2d75d676c6b12fb81210eef7e691..069960c943d9ea152cfc857241156cc6dbff82c4 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php
@@ -118,17 +118,6 @@ function testPreprocessForSuggestions() {
     }
   }
 
-  /**
-   * Tests the priority of some theme negotiators.
-   */
-  public function testNegotiatorPriorities() {
-    $this->drupalGet('theme-test/priority');
-
-    // Ensure that the custom theme negotiator was not able to set the theme.
-
-    $this->assertNoText('Theme hook implementor=test_theme_theme_test__suggestion(). Foo=template_preprocess_theme_test', 'Theme hook suggestion ran with data available from a preprocess function for the base hook.');
-  }
-
   /**
    * Ensure page-front template suggestion is added when on front page.
    */
@@ -279,4 +268,5 @@ function testPreprocessHtml() {
     $this->assertText('theme test page bottom markup', 'Modules are able to set the page bottom region.');
   }
 
+
 }
diff --git a/core/modules/system/lib/Drupal/system/Theme/BatchNegotiator.php b/core/modules/system/lib/Drupal/system/Theme/BatchNegotiator.php
deleted file mode 100644
index a10374d5b8e06fdbdd030910d41e89dbde6a9d01..0000000000000000000000000000000000000000
--- a/core/modules/system/lib/Drupal/system/Theme/BatchNegotiator.php
+++ /dev/null
@@ -1,54 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\system\Theme\BatchNegotiator.
- */
-
-namespace Drupal\system\Theme;
-
-use Drupal\Core\Batch\BatchStorageInterface;
-use Drupal\Core\Theme\ThemeNegotiatorInterface;
-use Symfony\Cmf\Component\Routing\RouteObjectInterface;
-use Symfony\Component\HttpFoundation\Request;
-
-/**
- * Sets the active theme for the batch page.
- */
-class BatchNegotiator implements ThemeNegotiatorInterface {
-
-  /**
-   * The batch storage.
-   *
-   * @var \Drupal\Core\Batch\BatchStorageInterface
-   */
-  protected $batchStorage;
-
-  /**
-   * Constructs a BatchNegotiator.
-   *
-   * @param \Drupal\Core\Batch\BatchStorageInterface $batch_storage
-   *   The batch storage.
-   */
-  public function __construct(BatchStorageInterface $batch_storage) {
-    $this->batchStorage = $batch_storage;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function determineActiveTheme(Request $request) {
-    if ($request->attributes->get(RouteObjectInterface::ROUTE_NAME) == 'system.batch_page') {
-      // Retrieve the current state of the batch.
-      $batch = &batch_get();
-      if (!$batch && $request->request->has('id')) {
-        $batch = $this->batchStorage->load($request->request->get('id'));
-      }
-      // Use the same theme as the page that started the batch.
-      if (!empty($batch['theme'])) {
-        return $batch['theme'];
-      }
-    }
-  }
-
-}
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index 67c48f356313ed649d591dd375fd371e6c44e91f..2bff21fd560443e862d2df476dfc97e14dfd86ea 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -1425,6 +1425,36 @@ function hook_template_preprocess_default_variables_alter(&$variables) {
   $variables['is_admin'] = user_access('access administration pages');
 }
 
+/**
+ * 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 should be used by modules which need to override the theme
+ * based on dynamic conditions (for example, a module which allows the theme to
+ * be set based on the current user's role). The return value of this hook will
+ * be used on all pages except those which have a valid per-page or per-section
+ * theme set via a theme callback function in hook_menu(); the themes on those
+ * pages can only be overridden using hook_menu_alter().
+ *
+ * Note that returning different themes for the same path may not work with page
+ * caching. This is most likely to be a problem if an anonymous user on a given
+ * path could have different themes returned under different 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. Do not
+ *   return a value if you do not wish to set a custom theme.
+ */
+function hook_custom_theme() {
+  // Allow the user to request a particular theme via a query parameter.
+  return \Drupal::request()->query->get('theme');
+}
+
 /**
  * Log an event message.
  *
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index f0cc453cc9083b9ae8e2737503325045e0d2c2c3..4f848ce9b19019de9b9f3397f330c5abaed64acd 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -824,6 +824,20 @@ function system_schema() {
         'not null' => TRUE,
         'default' => '',
       ),
+      'theme_callback' => array(
+        'description' => 'A function which returns the name of the theme that will be used to render this page. If left empty, the default theme will be used.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'theme_arguments' => array(
+        'description' => 'A serialized array of arguments for the theme callback.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
       'type' => array(
         'description' => 'Numeric representation of the type of the menu item, like MENU_LOCAL_TASK.',
         'type' => 'int',
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index fdf17374bb3b01b6aa8103a2e0700aa647e03746..40c3234681028ca9a903c837d30fb8fb6321b1cd 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -613,6 +613,12 @@ function system_element_info() {
  * Implements hook_menu().
  */
 function system_menu() {
+  $items['system/ajax'] = array(
+    'title' => 'AHAH callback',
+    'route_name' => 'system.ajax',
+    'theme callback' => 'ajax_base_page_theme',
+    'type' => MENU_CALLBACK,
+  );
   $items['admin'] = array(
     'title' => 'Administration',
     'route_name' => 'system.admin',
@@ -806,6 +812,13 @@ function system_menu() {
     'route_name' => 'system.status',
   );
 
+  // Default page for batch operations.
+  $items['batch'] = array(
+    'route_name' => 'system.batch_page',
+    'theme callback' => '_system_batch_theme',
+    'type' => MENU_CALLBACK,
+  );
+
   return $items;
 }
 
@@ -857,6 +870,21 @@ function system_theme_suggestions_region(array $variables) {
   return $suggestions;
 }
 
+/**
+ * Theme callback for the default batch page.
+ */
+function _system_batch_theme() {
+  // Retrieve the current state of the batch.
+  $batch = &batch_get();
+  if (!$batch && isset($_REQUEST['id'])) {
+    $batch = \Drupal::service('batch.storage')->load($_REQUEST['id']);
+  }
+  // Use the same theme as the page that started the batch.
+  if (!empty($batch['theme'])) {
+    return $batch['theme'];
+  }
+}
+
 /**
  * Implements hook_library_info().
  */
@@ -2103,6 +2131,19 @@ function system_page_build(&$page) {
   }
 }
 
+/**
+ * Implements hook_custom_theme().
+ */
+function system_custom_theme() {
+  if (drupal_container()->isScopeActive('request')) {
+    $request = \Drupal::request();
+    $path = $request->attributes->get('_system_path');
+    if (user_access('view the administration theme') && path_is_admin($path)) {
+      return \Drupal::config('system.theme')->get('admin');
+    }
+  }
+}
+
 /**
  * Implements hook_form_FORM_ID_alter().
  */
diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml
index a0aadb6e5b2fe0ce7a90230db78d51ceff91e229..77a741943c8e54583a8663956d4c5aa7902e021b 100644
--- a/core/modules/system/system.routing.yml
+++ b/core/modules/system/system.routing.yml
@@ -2,8 +2,6 @@ system.ajax:
   path: '/system/ajax'
   defaults:
     _controller: '\Drupal\system\Controller\FormAjaxController::content'
-  options:
-    _theme: ajax_base_page
   requirements:
     _access: 'TRUE'
 
diff --git a/core/modules/system/system.services.yml b/core/modules/system/system.services.yml
index 32210489ed284fa23161dcc933d76c493d5d0f40..3371dad2c684f60932e6919f4474afd2689e77ca 100644
--- a/core/modules/system/system.services.yml
+++ b/core/modules/system/system.services.yml
@@ -19,8 +19,3 @@ services:
     class: Drupal\system\PathProcessor\PathProcessorFiles
     tags:
       - { name: path_processor_inbound, priority: 200 }
-  theme.negotiator.system.batch:
-    class: Drupal\system\Theme\BatchNegotiator
-    arguments: ['@batch.storage']
-    tags:
-      - { name: theme_negotiator, priority: 1000 }
diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.module b/core/modules/system/tests/modules/ajax_test/ajax_test.module
index 5fb833805f8ddb0559f024c013d0f3488e0e2715..f42844fb7f0b2c9226b16964188e1db44feb7096 100644
--- a/core/modules/system/tests/modules/ajax_test/ajax_test.module
+++ b/core/modules/system/tests/modules/ajax_test/ajax_test.module
@@ -12,6 +12,19 @@
 use Drupal\Core\Ajax\CloseDialogCommand;
 use Drupal\Core\Ajax\HtmlCommand;
 
+/**
+ * Implements hook_menu().
+ */
+function ajax_test_menu() {
+  $items['ajax-test/order'] = array(
+    'title' => 'AJAX commands order',
+    'route_name' => 'ajax_test.order',
+    'theme callback' => 'ajax_base_page_theme',
+    'type' => MENU_CALLBACK,
+  );
+  return $items;
+}
+
 /**
  * Implements hook_system_theme_info().
  */
diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml b/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml
index 270ef51a754d845e05437e767f1e30642cea980e..9453bef47a9c82efb53ee3175f7a574609afb93d 100644
--- a/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml
+++ b/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml
@@ -37,8 +37,6 @@ ajax_test.order:
   path: '/ajax-test/order'
   defaults:
     _controller: '\Drupal\ajax_test\Controller\AjaxTestController::order'
-  options:
-    _theme: ajax_base_page
   requirements:
     _access: 'TRUE'
 
diff --git a/core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/EventSubscriber/ActiveTrailSubscriber.php b/core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/EventSubscriber/ActiveTrailSubscriber.php
deleted file mode 100644
index 4bb35956e72817fc66350592e5bb46e64a2b43e0..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/EventSubscriber/ActiveTrailSubscriber.php
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\menu_test\EventSubscriber\ActiveTrailSubscriber.
- */
-
-namespace Drupal\menu_test\EventSubscriber;
-
-use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
-use Symfony\Component\EventDispatcher\EventSubscriberInterface;
-use Symfony\Component\HttpKernel\Event\GetResponseEvent;
-use Symfony\Component\HttpKernel\KernelEvents;
-
-/**
- * Tracks the active trail.
- */
-class ActiveTrailSubscriber implements EventSubscriberInterface {
-
-  /**
-   * The active trail before redirect.
-   *
-   * @var array
-   */
-  protected $trail = array();
-
-  /**
-   * The state service.
-   *
-   * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
-   */
-  protected $state;
-
-  /**
-   * Constructs a new ActiveTrailSubscriber.
-   *
-   * @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $state
-   *   The state service.
-   */
-  public function __construct(KeyValueStoreInterface $state) {
-    $this->state = $state;
-  }
-
-  /**
-   * Tracks the active trail.
-   *
-   * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
-   *   The event to process.
-   */
-  public function onKernelRequest(GetResponseEvent $event) {
-    // When requested by one of the MenuTrailTestCase tests, record the initial
-    // active trail during Drupal's bootstrap (before the user is redirected to
-    // a custom 403 or 404 page).
-    if (!$this->trail && $this->state->get('menu_test.record_active_trail') ?: FALSE) {
-      $this->trail = menu_get_active_trail();
-      $this->state->set('menu_test.active_trail_initial', $this->trail);
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function getSubscribedEvents() {
-    $events[KernelEvents::REQUEST][] = array('onKernelRequest');
-    return $events;
-  }
-
-}
diff --git a/core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/Theme/TestThemeNegotiator.php b/core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/Theme/TestThemeNegotiator.php
deleted file mode 100644
index b324d8d48b212d93aba4439aba8301437e64ccbd..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/Theme/TestThemeNegotiator.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\menu_test\Theme\TestThemeNegotiator.
- */
-
-namespace Drupal\menu_test\Theme;
-
-use Drupal\Core\Theme\ThemeNegotiatorInterface;
-use Symfony\Component\HttpFoundation\Request;
-
-/**
- * Tests the theme negotiation functionality.
- *
- * Retrieves the theme key of the theme to use for the current request based on
- * the theme name provided in the URL.
- */
-class TestThemeNegotiator implements ThemeNegotiatorInterface {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function determineActiveTheme(Request $request) {
-    $argument = $request->attributes->get('inherited');
-    // Test using the variable administrative theme.
-    if ($argument == 'use-admin-theme') {
-      return \Drupal::config('system.theme')->get('admin');
-    }
-    // Test using a theme that exists, but may or may not be enabled.
-    elseif ($argument == 'use-stark-theme') {
-      return 'stark';
-    }
-    // Test using a theme that does not exist.
-    elseif ($argument == 'use-fake-theme') {
-      return 'fake_theme';
-    }
-    // For any other value of the URL argument, do not return anything. This
-    // allows us to test that returning nothing from a theme negotiation
-    // causes the page to correctly fall back on using the main site theme.
-  }
-
-}
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.module b/core/modules/system/tests/modules/menu_test/menu_test.module
index df5b4904e0d98ade288de3f07ffcb68c2f77efa9..ab2ef302894bf941f5c1ee699acd89784dc26adb 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.module
+++ b/core/modules/system/tests/modules/menu_test/menu_test.module
@@ -64,12 +64,18 @@ function menu_test_menu() {
     'route_name' => 'menu_test.hierarchy_parent_child2',
   );
   // Theme callback tests.
+  $items['menu-test/theme-callback/%'] = array(
+    'title' => 'Page that displays different themes',
+    'route_name' => 'menu_test.theme_callback',
+    'theme callback' => 'menu_test_theme_callback',
+    'theme arguments' => array(2),
+  );
   $items['menu-test/theme-callback/%/inheritance'] = array(
-    'title' => 'Page that tests theme negotiation inheritance.',
+    'title' => 'Page that tests theme callback inheritance.',
     'route_name' => 'menu_test.theme_callback_inheritance',
   );
   $items['menu-test/no-theme-callback'] = array(
-    'title' => 'Page that displays different themes without using a theme negotiation.',
+    'title' => 'Page that displays different themes without using a theme callback.',
     'route_name' => 'menu_test.no_theme_callback',
   );
   // Path containing "exotic" characters.
@@ -433,7 +439,7 @@ function menu_test_custom_403_404_callback() {
 }
 
 /**
- * Page callback: Tests the theme negotiation functionality.
+ * Page callback: Tests the theme callback functionality.
  *
  * @param bool $inherited
  *   (optional) TRUE when the requested page is intended to inherit
@@ -451,15 +457,66 @@ function menu_test_theme_page_callback($inherited = FALSE) {
   global $theme_key;
   // Initialize the theme system so that $theme_key will be populated.
   drupal_theme_initialize();
-  // Now we check what the theme negotiator service returns.
-  $active_theme = \Drupal::service('theme.negotiator')->getActiveTheme('getActiveTheme');
-  $output = "Active theme: $active_theme. Actual theme: $theme_key.";
+  // 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 = "Custom theme: $requested_theme. Actual theme: $theme_key.";
   if ($inherited) {
-    $output .= ' Theme negotiation inheritance is being tested.';
+    $output .= ' Theme callback inheritance is being tested.';
   }
   return $output;
 }
 
+/**
+ * Theme callback: Tests the theme callback functionality.
+ *
+ * Retrieves the theme key of the theme to use for the current request based on
+ * the theme name provided in the URL.
+ *
+ * @param string $argument
+ *   The argument passed in from the URL.
+ *
+ * @return string
+ *   The name of the custom theme to request for the current page.
+ *
+ * @see menu_test_menu().
+ */
+function menu_test_theme_callback($argument) {
+  // Test using the variable administrative theme.
+  if ($argument == 'use-admin-theme') {
+    return \Drupal::config('system.theme')->get('admin');
+  }
+  // Test using a theme that exists, but may or may not be enabled.
+  elseif ($argument == 'use-stark-theme') {
+    return 'stark';
+  }
+  // Test using a theme that does not exist.
+  elseif ($argument == 'use-fake-theme') {
+    return 'fake_theme';
+  }
+  // For any other value of the URL argument, do not return anything. This
+  // allows us to test that returning nothing from a theme callback function
+  // causes the page to correctly fall back on using the main site theme.
+}
+
+/**
+ * Implements hook_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.
+ */
+function menu_test_custom_theme() {
+  // When requested by one of the MenuTrailTestCase tests, record the initial
+  // active trail during Drupal's bootstrap (before the user is redirected to a
+  // custom 403 or 404 page). See menu_test_custom_403_404_callback().
+  if (\Drupal::state()->get('menu_test.record_active_trail') ?: FALSE) {
+    \Drupal::state()->set('menu_test.active_trail_initial', menu_get_active_trail());
+  }
+  if ($theme = \Drupal::state()->get('menu_test.hook_custom_theme_name') ?: FALSE) {
+    return $theme;
+  }
+}
+
 /**
  * Sets a static variable for the testMenuName() test.
  *
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.routing.yml b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml
index d37f47b33cdf5d5fb177a9b4a45fe11923a0769d..5ab1a945cf2d57c61755b10bedc3eb6d2a426835 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.routing.yml
+++ b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml
@@ -490,7 +490,7 @@ menu_test.theme_callback:
 menu_test.no_theme_callback:
   path: '/menu-test/no-theme-callback'
   defaults:
-    _title: 'Page that displays different themes without using a theme negotiation.'
+    _title: 'Page that displays different themes without using a theme callback.'
     _content: '\Drupal\menu_test\Controller\MenuTestController::themePage'
     inherited: false
   requirements:
@@ -511,7 +511,7 @@ menu_test.exotic_path:
 menu_test.theme_callback_inheritance:
   path: '/menu-test/theme-callback/{inherited}/inheritance'
   defaults:
-    _title: 'Page that tests theme negotiation inheritance.'
+    _title: 'Page that tests theme callback inheritance.'
     _content: '\Drupal\menu_test\Controller\MenuTestController::themePage'
   requirements:
     _permission: 'access content'
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.services.yml b/core/modules/system/tests/modules/menu_test/menu_test.services.yml
index 3de0169ad1071675d4c9ff2675a8f3477a353113..097ddf201b9275a84ca6545d648f701d3730cae9 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.services.yml
+++ b/core/modules/system/tests/modules/menu_test/menu_test.services.yml
@@ -3,14 +3,3 @@ services:
     class: Drupal\menu_test\EventSubscriber\MaintenanceModeSubscriber
     tags:
       - { name: event_subscriber }
-
-  menu_test.active_trail_subscriber:
-    class: Drupal\menu_test\EventSubscriber\ActiveTrailSubscriber
-    arguments: ['@state']
-    tags:
-      - { name: event_subscriber }
-
-  theme.negotiator.test_theme:
-    class: Drupal\menu_test\Theme\TestThemeNegotiator
-    tags:
-      - { name: theme_negotiator }
diff --git a/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/Theme/CustomThemeNegotiator.php b/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/Theme/CustomThemeNegotiator.php
deleted file mode 100644
index 9c0396eedcd02e130c52d4ec5ad4565a58346413..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/Theme/CustomThemeNegotiator.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\theme_test\Theme\CustomThemeNegotiator.
- */
-
-namespace Drupal\theme_test\Theme;
-
-use Drupal\Core\Theme\ThemeNegotiatorInterface;
-use Symfony\Cmf\Component\Routing\RouteObjectInterface;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\Routing\Route;
-
-/**
- * Just forces the 'test_theme' theme.
- */
-class CustomThemeNegotiator implements ThemeNegotiatorInterface {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function determineActiveTheme(Request $request) {
-    if (($route_object = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)) && $route_object instanceof Route && $route_object->hasOption('_custom_theme')) {
-      return $route_object->getOption('_custom_theme');
-    }
-  }
-
-}
diff --git a/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/Theme/HighPriorityThemeNegotiator.php b/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/Theme/HighPriorityThemeNegotiator.php
deleted file mode 100644
index 2bb5cf6dd4143af88ac413988aabbd1dd504cf38..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/Theme/HighPriorityThemeNegotiator.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\theme_test\Theme\HighPriorityThemeNegotiator.
- */
-
-namespace Drupal\theme_test\Theme;
-
-use Drupal\Core\Theme\ThemeNegotiatorInterface;
-use Symfony\Cmf\Component\Routing\RouteObjectInterface;
-use Symfony\Component\HttpFoundation\Request;
-
-/**
- * Implements a test theme negotiator which was configured with a high priority.
- */
-class HighPriorityThemeNegotiator implements ThemeNegotiatorInterface {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function determineActiveTheme(Request $request) {
-    if (($route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME)) && $route_name == 'theme_test.priority') {
-      return 'stark';
-    }
-  }
-
-}
diff --git a/core/modules/system/tests/modules/theme_test/theme_test.module b/core/modules/system/tests/modules/theme_test/theme_test.module
index 52bff218b40d34e6e2d9b72c4ae393f597ec06a0..87e26b8922836ee8eb3d44e01e835903db091629 100644
--- a/core/modules/system/tests/modules/theme_test/theme_test.module
+++ b/core/modules/system/tests/modules/theme_test/theme_test.module
@@ -80,6 +80,13 @@ function theme_test_menu() {
   return $items;
 }
 
+/**
+ * Custom theme callback.
+ */
+function _theme_custom_theme() {
+  return 'test_theme';
+}
+
 /**
  * Implements hook_preprocess_HOOK() for HTML document templates.
  */
diff --git a/core/modules/system/tests/modules/theme_test/theme_test.routing.yml b/core/modules/system/tests/modules/theme_test/theme_test.routing.yml
index 9f08d5b0c56ae2ded56233dcf65b5eb8f13d57c5..b4a7fd64fe98576a8308ac7eafc5f8cdd868eb13 100644
--- a/core/modules/system/tests/modules/theme_test/theme_test.routing.yml
+++ b/core/modules/system/tests/modules/theme_test/theme_test.routing.yml
@@ -1,7 +1,5 @@
 theme_test.function_template_override:
   path: '/theme-test/function-template-overridden'
-  options:
-    _custom_theme: 'test_theme'
   defaults:
     _content: '\Drupal\theme_test\ThemeTestController::functionTemplateOverridden'
   requirements:
@@ -23,18 +21,6 @@ theme_test.template_test:
 
 theme_test.suggestion:
   path: '/theme-test/suggestion'
-  options:
-    _custom_theme: 'test_theme'
-  defaults:
-    _content: '\Drupal\theme_test\ThemeTestController::testSuggestion'
-    _title: 'Suggestion'
-  requirements:
-    _access: 'TRUE'
-
-theme_test.priority:
-  path: '/theme-test/priority'
-  options:
-    _custom_theme: 'test_theme'
   defaults:
     _content: '\Drupal\theme_test\ThemeTestController::testSuggestion'
     _title: 'Suggestion'
@@ -43,8 +29,6 @@ theme_test.priority:
 
 theme_test.alter:
   path: '/theme-test/alter'
-  options:
-    _custom_theme: 'test_theme'
   defaults:
     _content: '\Drupal\theme_test\ThemeTestController::testAlter'
     _title: 'Suggestion'
diff --git a/core/modules/system/tests/modules/theme_test/theme_test.services.yml b/core/modules/system/tests/modules/theme_test/theme_test.services.yml
index 69fd3ca7005764b3b11c653eb88a2b145ec238c8..8e442df969599c7cdc56bd5db1eb7c6de1c9f4dc 100644
--- a/core/modules/system/tests/modules/theme_test/theme_test.services.yml
+++ b/core/modules/system/tests/modules/theme_test/theme_test.services.yml
@@ -3,13 +3,3 @@ services:
     class: Drupal\theme_test\EventSubscriber\ThemeTestSubscriber
     tags:
       - { name: event_subscriber }
-
-  theme.negotiator.test_custom_theme:
-    class: Drupal\theme_test\Theme\CustomThemeNegotiator
-    tags:
-      - { name: theme_negotiator }
-
-  theme.negotiator.high_priority:
-    class: Drupal\theme_test\Theme\HighPriorityThemeNegotiator
-    tags:
-      - { name: theme_negotiator, priority: 1000 }
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/ThemeTest.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/ThemeTest.php
index ee8cc38b3f38a91e144ac88ede73d32f7efcdaa1..452e33ff6242c32d81e24614077816429967ba0d 100644
--- a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/ThemeTest.php
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/ThemeTest.php
@@ -25,11 +25,11 @@ function setUp() {
 
     // Make sure we are using distinct default and administrative themes for
     // the duration of these tests.
-    theme_enable(array('bartik', 'seven'));
     \Drupal::config('system.theme')
       ->set('default', 'bartik')
-      ->set('admin', 'seven')
       ->save();
+    theme_enable(array('seven'));
+    \Drupal::config('system.theme')->set('admin', 'seven')->save();
 
     // Create and log in as a user who has permission to add and edit taxonomy
     // terms and view the administrative theme.
diff --git a/core/modules/text/lib/Drupal/text/Tests/Formatter/TextPlainUnitTest.php b/core/modules/text/lib/Drupal/text/Tests/Formatter/TextPlainUnitTest.php
index 3cd5dcadd051f2179b98d7fec68390acda04b218..72c0fe9769ba6cea8846548bb5ae1d1ff4858eb2 100644
--- a/core/modules/text/lib/Drupal/text/Tests/Formatter/TextPlainUnitTest.php
+++ b/core/modules/text/lib/Drupal/text/Tests/Formatter/TextPlainUnitTest.php
@@ -49,7 +49,6 @@ function setUp() {
     // Configure the theme system.
     $this->installConfig(array('system', 'field'));
     $this->installSchema('entity_test', 'entity_test');
-    $this->installSchema('user', 'users');
 
     // @todo Add helper methods for all of the following.
 
diff --git a/core/modules/user/lib/Drupal/user/Theme/AdminNegotiator.php b/core/modules/user/lib/Drupal/user/Theme/AdminNegotiator.php
deleted file mode 100644
index 4658713303b0688b2fa757ceff78b2f06dd2a4a8..0000000000000000000000000000000000000000
--- a/core/modules/user/lib/Drupal/user/Theme/AdminNegotiator.php
+++ /dev/null
@@ -1,71 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\user\Theme\AdminNegotiator.
- */
-
-namespace Drupal\user\Theme;
-
-use Drupal\Core\Config\ConfigFactory;
-use Drupal\Core\Entity\EntityManager;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\Core\Theme\ThemeNegotiatorInterface;
-use Symfony\Component\HttpFoundation\Request;
-
-/**
- * Sets the active theme on admin pages.
- */
-class AdminNegotiator implements ThemeNegotiatorInterface {
-
-  /**
-   * The current user.
-   *
-   * @var \Drupal\Core\Session\AccountInterface
-   */
-  protected $user;
-
-  /**
-   * The config factory.
-   *
-   * @var \Drupal\Core\Config\ConfigFactory
-   */
-  protected $configFactory;
-
-  /**
-   * The entity manager.
-   *
-   * @var \Drupal\Core\Entity\EntityManager
-   */
-  protected $entityManager;
-
-  /**
-   * Creates a new AdminNegotiator instance.
-   *
-   * @param \Drupal\Core\Session\AccountInterface $user
-   *   The current user.
-   * @param \Drupal\Core\Config\ConfigFactory $config_factory
-   *   The config factory.
-   * @param \Drupal\Core\Entity\EntityManager $entity_manager
-   *   The entity manager.
-   */
-  public function __construct(AccountInterface $user, ConfigFactory $config_factory, EntityManager $entity_manager) {
-    $this->user = $user;
-    $this->configFactory = $config_factory;
-    $this->entityManager = $entity_manager;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function determineActiveTheme(Request $request) {
-    $path = $request->attributes->get('_system_path');
-
-    // Don't break if the user_role entity is not available in order to decouple
-    // system and user module.
-    if ($this->entityManager->hasController('user_role', 'storage') && $this->user->hasPermission('view the administration theme') && path_is_admin($path)) {
-      return $this->configFactory->get('system.theme')->get('admin');
-    }
-  }
-
-}
diff --git a/core/modules/user/lib/Drupal/user/Theme/UserNegotiator.php b/core/modules/user/lib/Drupal/user/Theme/UserNegotiator.php
deleted file mode 100644
index 17c1e33bebcd593f97a89c7c3ac92f8b4d9e75bb..0000000000000000000000000000000000000000
--- a/core/modules/user/lib/Drupal/user/Theme/UserNegotiator.php
+++ /dev/null
@@ -1,61 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\user\Theme\UserNegotiator.
- */
-
-namespace Drupal\user\Theme;
-
-use Drupal\Core\Entity\EntityManager;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\Core\Theme\ActiveTheme;
-use Drupal\Core\Theme\ThemeNegotiatorInterface;
-use Symfony\Component\HttpFoundation\Request;
-
-/**
- * Defines the theme negotiator service for theme configured per user.
- */
-class UserNegotiator implements  ThemeNegotiatorInterface {
-
-  /**
-   * The user storage controller.
-   *
-   * @var \Drupal\user\UserStorageControllerInterface
-   */
-  protected $userStorageController;
-
-  /**
-   * The current user.
-   *
-   * @var \Drupal\Core\Session\AccountInterface
-   */
-  protected $currentUser;
-
-  /**
-   * Constructs a UserNegotiator object.
-   *
-   * @param \Drupal\Core\Entity\EntityManager $entity_manager
-   *   The entity manager
-   * @param \Drupal\Core\Session\AccountInterface $current_user
-   *   The current user.
-   */
-  public function __construct(EntityManager $entity_manager, AccountInterface $current_user) {
-    $this->userStorageController = $entity_manager->getStorageController('user');
-    $this->currentUser = $current_user;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function determineActiveTheme(Request $request) {
-    if ($user = $this->userStorageController->load($this->currentUser->id())) {;
-      // Only select the user selected theme if it is available in the
-      // list of themes that can be accessed.
-      if (!empty($user->theme) && drupal_theme_access($user->theme)) {
-        return $user->theme;
-      }
-    }
-  }
-
-}
diff --git a/core/modules/user/user.services.yml b/core/modules/user/user.services.yml
index bc348fa302f5613157428aac0325d7455ee0b59b..6fb7d47eeabff9fe3c4f9858ab936f107490b8e2 100644
--- a/core/modules/user/user.services.yml
+++ b/core/modules/user/user.services.yml
@@ -25,13 +25,3 @@ services:
     class: Drupal\user\EventSubscriber\MaintenanceModeSubscriber
     tags:
       - { name: event_subscriber }
-  theme.negotiator.user:
-    class: Drupal\user\Theme\UserNegotiator
-    arguments: ['@plugin.manager.entity', '@current_user']
-    tags:
-      - { name: theme_negotiator, priority: -50 }
-  theme.negotiator.admin_theme:
-    class: Drupal\user\Theme\AdminNegotiator
-    arguments: ['@current_user', '@config.factory', '@entity.manager']
-    tags:
-      - { name: theme_negotiator, priority: -40 }
diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/FieldCounterTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldCounterTest.php
index 897bfd923de07ad43cad144d215b13183a0fa770..87732485d4588b5eb293fdda1902cac2cf1184e4 100644
--- a/core/modules/views/lib/Drupal/views/Tests/Handler/FieldCounterTest.php
+++ b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldCounterTest.php
@@ -19,7 +19,7 @@ class FieldCounterTest extends ViewUnitTestBase {
    *
    * @var array
    */
-  public static $modules = array('user', 'field');
+  public static $modules = array('user');
 
   /**
    * Views used by this test.
@@ -36,12 +36,6 @@ public static function getInfo() {
     );
   }
 
-  protected function setUp() {
-    parent::setUp();
-
-    $this->installSchema('user', 'users');
-  }
-
   function testSimple() {
     $view = views_get_view('test_view');
     $view->setDisplay();
diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/FieldUnitTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldUnitTest.php
index 9c500021959884b40a29f058a64d199580e4ecde..eff89294d3dc191a2a101b9fbc072d33aa9e321a 100644
--- a/core/modules/views/lib/Drupal/views/Tests/Handler/FieldUnitTest.php
+++ b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldUnitTest.php
@@ -17,7 +17,7 @@
  */
 class FieldUnitTest extends ViewUnitTestBase {
 
-  public static $modules = array('user', 'field');
+  public static $modules = array('user');
 
   /**
    * Views used by this test.
@@ -38,12 +38,6 @@ public static function getInfo() {
     );
   }
 
-  protected function setUp() {
-    parent::setUp();
-
-    $this->installSchema('user', 'users');
-  }
-
   /**
    * Overrides Drupal\views\Tests\ViewTestBase::viewsData().
    */
diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayPageTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayPageTest.php
index b4e343d407208b1f1c066f9ca94153d03765205f..48c116e201dc9e4ffd7a5b4b310d3fa3ff73505e 100644
--- a/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayPageTest.php
+++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayPageTest.php
@@ -59,7 +59,6 @@ protected function setUp() {
     // Setup the needed tables in order to make the drupal router working.
     $this->installSchema('system', array('router', 'menu_router', 'url_alias'));
     $this->installSchema('menu_link', 'menu_links');
-    $this->installSchema('user', 'users');
   }
 
   /**
diff --git a/core/modules/views/lib/Drupal/views/ViewExecutable.php b/core/modules/views/lib/Drupal/views/ViewExecutable.php
index 413962bd2beb781150f9748c2f87ae59c02cc1fb..435da780cd607391b10d6e40740ab37a714e08f4 100644
--- a/core/modules/views/lib/Drupal/views/ViewExecutable.php
+++ b/core/modules/views/lib/Drupal/views/ViewExecutable.php
@@ -1302,6 +1302,8 @@ public function render($display_id = NULL) {
       return;
     }
 
+    drupal_theme_initialize();
+
     $exposed_form = $this->display_handler->getPlugin('exposed_form');
     $exposed_form->preRender($this->result);
 
@@ -1363,14 +1365,12 @@ public function render($display_id = NULL) {
       $module_handler->invokeAll('views_pre_render', array($this));
 
       // Let the themes play too, because pre render is a very themey thing.
-      if (isset($GLOBALS['base_theme_info']) && isset($GLOBALS['theme'])) {
-        foreach ($GLOBALS['base_theme_info'] as $base) {
-          $module_handler->invoke($base, 'views_pre_render', array($this));
-        }
-
-        $module_handler->invoke($GLOBALS['theme'], 'views_pre_render', array($this));
+      foreach ($GLOBALS['base_theme_info'] as $base) {
+        $module_handler->invoke($base, 'views_pre_render', array($this));
       }
 
+      $module_handler->invoke($GLOBALS['theme'], 'views_pre_render', array($this));
+
       $this->display_handler->output = $this->display_handler->render();
       if ($cache) {
         $cache->cacheSet('output');
@@ -1387,14 +1387,12 @@ public function render($display_id = NULL) {
     $module_handler->invokeAll('views_post_render', array($this, &$this->display_handler->output, $cache));
 
     // Let the themes play too, because post render is a very themey thing.
-    if (isset($GLOBALS['base_theme_info']) && isset($GLOBALS['theme'])) {
-      foreach ($GLOBALS['base_theme_info'] as $base) {
-        $module_handler->invoke($base, 'views_post_render', array($this));
-      }
-
-      $module_handler->invoke($GLOBALS['theme'], 'views_post_render', array($this));
+    foreach ($GLOBALS['base_theme_info'] as $base) {
+      $module_handler->invoke($base, 'views_post_render', array($this));
     }
 
+    $module_handler->invoke($GLOBALS['theme'], 'views_post_render', array($this));
+
     return $this->display_handler->output;
   }
 
diff --git a/core/modules/views/views.module b/core/modules/views/views.module
index 11dbebba0405d1312e51990739f1695672c737b6..5bea350b132873ed8aba0d65ab5b1312096ed2c6 100644
--- a/core/modules/views/views.module
+++ b/core/modules/views/views.module
@@ -246,6 +246,20 @@ function views_permission() {
   );
 }
 
+/**
+ * Implement hook_menu().
+ */
+function views_menu() {
+  $items = array();
+  $items['views/ajax'] = array(
+    'title' => 'Views',
+    'theme callback' => 'ajax_base_page_theme',
+    'route_name' => 'views.ajax',
+    'type' => MENU_CALLBACK,
+  );
+  return $items;
+}
+
 /**
  * Implement hook_menu_alter().
  */
diff --git a/core/modules/views/views.routing.yml b/core/modules/views/views.routing.yml
index 691640d00ea0a7076cd76902288d3e948d97a0de..9abe5d59142a7dfc22bc93e83289765f9ed94583 100644
--- a/core/modules/views/views.routing.yml
+++ b/core/modules/views/views.routing.yml
@@ -2,7 +2,5 @@ views.ajax:
   path: '/views/ajax'
   defaults:
     _controller: '\Drupal\views\Controller\ViewAjaxController::ajaxView'
-  options:
-    _theme: ajax_base_page
   requirements:
     _access: 'TRUE'
diff --git a/core/tests/Drupal/Tests/Core/Theme/ThemeNegotiatorTest.php b/core/tests/Drupal/Tests/Core/Theme/ThemeNegotiatorTest.php
deleted file mode 100644
index 9779e63822363f620ffd604e915fbbd4342191b5..0000000000000000000000000000000000000000
--- a/core/tests/Drupal/Tests/Core/Theme/ThemeNegotiatorTest.php
+++ /dev/null
@@ -1,141 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Core\Theme\ThemeNegotiatorTest.
- */
-
-namespace Drupal\Tests\Core\Theme;
-
-use Drupal\Core\Theme\ThemeNegotiator;
-use Drupal\Tests\UnitTestCase;
-use Symfony\Component\HttpFoundation\Request;
-
-/**
- * Tests the theme negotiator.
- *
- * @see \Drupal\Core\Theme\ThemeNegotiator
- */
-class ThemeNegotiatorTest extends UnitTestCase {
-
-  /**
-   * The mocked theme access checker.
-   *
-   * @var \Drupal\Core\Theme\ThemeAccessCheck|\PHPUnit_Framework_MockObject_MockObject
-   */
-  protected $themeAccessCheck;
-
-  /**
-   * The actual tested theme negotiator.
-   *
-   * @var \Drupal\Core\Theme\ThemeNegotiator
-   */
-  protected $themeNegotiator;
-
-  public static function getInfo() {
-    return array(
-      'name' => 'Theme negotiator',
-      'description' => 'Tests the theme negotiator.',
-      'group' => 'Theme',
-    );
-  }
-
-  protected function setUp() {
-    $this->themeAccessCheck = $this->getMockBuilder('\Drupal\Core\Theme\ThemeAccessCheck')
-      ->disableOriginalConstructor()
-      ->getMock();
-    $this->themeNegotiator = new ThemeNegotiator($this->themeAccessCheck);
-  }
-
-  /**
-   * Tests determining the theme.
-   *
-   * @see \Drupal\Core\Theme\ThemeNegotiator::determineActiveTheme()
-   */
-  public function testDetermineActiveTheme() {
-    $negotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface');
-    $negotiator->expects($this->once())
-      ->method('determineActiveTheme')
-      ->will($this->returnValue('example_test'));
-
-    $this->themeNegotiator->addNegotiator($negotiator, 0);
-
-    $this->themeAccessCheck->expects($this->any())
-      ->method('checkAccess')
-      ->will($this->returnValue(TRUE));
-
-    $request = Request::create('/test-route');
-    $theme = $this->themeNegotiator->determineActiveTheme($request);
-
-    $this->assertEquals('example_test', $theme);
-    $this->assertEquals('example_test', $request->attributes->get('_theme_active'));
-  }
-
-  /**
-   * Tests determining with two negotiators checking the priority.
-   *
-   * @see \Drupal\Core\Theme\ThemeNegotiator::determineActiveTheme()
-   */
-  public function testDetermineActiveThemeWithPriority() {
-    $negotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface');
-    $negotiator->expects($this->once())
-      ->method('determineActiveTheme')
-      ->will($this->returnValue('example_test'));
-
-    $this->themeNegotiator->addNegotiator($negotiator, 10);
-
-    $negotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface');
-    $negotiator->expects($this->never())
-      ->method('determineActiveTheme');
-
-    $this->themeNegotiator->addNegotiator($negotiator, 0);
-
-    $this->themeAccessCheck->expects($this->any())
-      ->method('checkAccess')
-      ->will($this->returnValue(TRUE));
-
-    $request = Request::create('/test-route');
-    $theme = $this->themeNegotiator->determineActiveTheme($request);
-
-    $this->assertEquals('example_test', $theme);
-    $this->assertEquals('example_test', $request->attributes->get('_theme_active'));
-  }
-
-  /**
-   * Tests determining with two negotiators of which just one returns access.
-   *
-   * @see \Drupal\Core\Theme\ThemeNegotiator::determineActiveTheme()
-   */
-  public function testDetermineActiveThemeWithAccessCheck() {
-    $negotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface');
-    $negotiator->expects($this->once())
-      ->method('determineActiveTheme')
-      ->will($this->returnValue('example_test'));
-
-    $this->themeNegotiator->addNegotiator($negotiator, 10);
-
-    $negotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface');
-    $negotiator->expects($this->once())
-      ->method('determineActiveTheme')
-      ->will($this->returnValue('example_test2'));
-
-    $this->themeNegotiator->addNegotiator($negotiator, 0);
-
-    $this->themeAccessCheck->expects($this->at(0))
-      ->method('checkAccess')
-      ->with('example_test')
-      ->will($this->returnValue(FALSE));
-
-    $this->themeAccessCheck->expects($this->at(1))
-      ->method('checkAccess')
-      ->with('example_test2')
-      ->will($this->returnValue(TRUE));
-
-    $request = Request::create('/test-route');
-    $theme = $this->themeNegotiator->determineActiveTheme($request);
-
-    $this->assertEquals('example_test2', $theme);
-    $this->assertEquals('example_test2', $request->attributes->get('_theme_active'));
-  }
-
-}