text: // Copyright 2009 The Closure Library Authors. All Rights Reserved.
  //
  // Licensed under the Apache License, Version 2.0 (the "License");
  // you may not use this file except in compliance with the License.
  // You may obtain a copy of the License at
  //
  //      http://www.apache.org/licenses/LICENSE-2.0
  //
  // Unless required by applicable law or agreed to in writing, software
  // distributed under the License is distributed on an "AS-IS" BASIS,
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  // See the License for the specific language governing permissions and
  // limitations under the License.
  
  /**
   * @fileoverview A UI for editing tweak settings / clicking tweak actions.
   *
   * @author agrieve@google.com (Andrew Grieve)
   */
  
  goog.provide('goog.tweak.EntriesPanel');
  goog.provide('goog.tweak.TweakUi');
  
  goog.require('goog.array');
  goog.require('goog.asserts');
  goog.require('goog.dom');
  goog.require('goog.dom.TagName');
  goog.require('goog.dom.safe');
  goog.require('goog.html.SafeHtml');
  goog.require('goog.object');
  goog.require('goog.string.Const');
  goog.require('goog.style');
  goog.require('goog.tweak');
  goog.require('goog.tweak.BaseEntry');
  goog.require('goog.tweak.BooleanGroup');
  goog.require('goog.tweak.BooleanInGroupSetting');
  goog.require('goog.tweak.BooleanSetting');
  goog.require('goog.tweak.ButtonAction');
  goog.require('goog.tweak.NumericSetting');
  goog.require('goog.tweak.StringSetting');
  goog.require('goog.ui.Zippy');
  goog.require('goog.userAgent');
  
  
  
  /**
   * A UI for editing tweak settings / clicking tweak actions.
   * @param {!goog.tweak.Registry} registry The registry to render.
   * @param {goog.dom.DomHelper=} opt_domHelper The DomHelper to render with.
   * @constructor
   * @final
   */
  goog.tweak.TweakUi = function(registry, opt_domHelper) {
    /**
     * The registry to create a UI from.
     * @type {!goog.tweak.Registry}
     * @private
     */
    this.registry_ = registry;
  
    /**
     * The element to display when the UI is visible.
     * @type {goog.tweak.EntriesPanel|undefined}
     * @private
     */
    this.entriesPanel_;
  
    /**
     * The DomHelper to render with.
     * @type {!goog.dom.DomHelper}
     * @private
     */
    this.domHelper_ = opt_domHelper || goog.dom.getDomHelper();
  
    // Listen for newly registered entries (happens with lazy-loaded modules).
    registry.addOnRegisterListener(goog.bind(this.onNewRegisteredEntry_, this));
  };
  
  
  /**
   * The CSS class name unique to the root tweak panel div.
   * @type {string}
   * @private
   */
  goog.tweak.TweakUi.ROOT_PANEL_CLASS_ = goog.getCssName('goog-tweak-root');
  
  
  /**
   * The CSS class name unique to the tweak entry div.
   * @type {string}
   * @private
   */
  goog.tweak.TweakUi.ENTRY_CSS_CLASS_ = goog.getCssName('goog-tweak-entry');
  
  
  /**
   * The CSS classes for each tweak entry div.
   * @type {string}
   * @private
   */
  goog.tweak.TweakUi.ENTRY_CSS_CLASSES_ = goog.tweak.TweakUi.ENTRY_CSS_CLASS_ +
      ' ' + goog.getCssName('goog-inline-block');
  
  
  /**
   * The CSS classes for each namespace tweak entry div.
   * @type {string}
   * @private
   */
  goog.tweak.TweakUi.ENTRY_GROUP_CSS_CLASSES_ =
      goog.tweak.TweakUi.ENTRY_CSS_CLASS_;
  
  
  /**
   * Marker that the style sheet has already been installed.
   * @type {string}
   * @private
   */
  goog.tweak.TweakUi.STYLE_SHEET_INSTALLED_MARKER_ = '__closure_tweak_installed_';
  
  
  /**
   * CSS used by TweakUI.
   * @type {string}
   * @private
   */
  goog.tweak.TweakUi.CSS_STYLES_ = (function() {
    var MOBILE = goog.userAgent.MOBILE;
    var IE = goog.userAgent.IE;
    var ROOT_PANEL_CLASS = '.' + goog.tweak.TweakUi.ROOT_PANEL_CLASS_;
    var GOOG_INLINE_BLOCK_CLASS = '.' + goog.getCssName('goog-inline-block');
    var ret = ROOT_PANEL_CLASS + '{background:#ffc; padding:0 4px}';
    // Make this work even if the user hasn't included common.css.
    if (!IE) {
      ret += GOOG_INLINE_BLOCK_CLASS + '{display:inline-block}';
    }
    // Space things out vertically for touch UIs.
    if (MOBILE) {
      ret += ROOT_PANEL_CLASS + ',' + ROOT_PANEL_CLASS + ' fieldset{' +
          'line-height:2em;' +
          '}';
    }
    return ret;
  })();
  
  
  /**
   * Creates a TweakUi if tweaks are enabled.
   * @param {goog.dom.DomHelper=} opt_domHelper The DomHelper to render with.
   * @return {!Element|undefined} The root UI element or undefined if tweaks are
   *     not enabled.
   */
  goog.tweak.TweakUi.create = function(opt_domHelper) {
    var registry = goog.tweak.getRegistry();
    if (registry) {
      var ui = new goog.tweak.TweakUi(registry, opt_domHelper);
      ui.render();
      return ui.getRootElement();
    }
  };
  
  
  /**
   * Creates a TweakUi inside of a show/hide link.
   * @param {goog.dom.DomHelper=} opt_domHelper The DomHelper to render with.
   * @return {!Element|undefined} The root UI element or undefined if tweaks are
   *     not enabled.
   */
  goog.tweak.TweakUi.createCollapsible = function(opt_domHelper) {
    var registry = goog.tweak.getRegistry();
    if (registry) {
      var dh = opt_domHelper || goog.dom.getDomHelper();
  
      // The following strings are for internal debugging only.  No translation
      // necessary.  Do NOT wrap goog.getMsg() around these strings.
      var showLink =
          dh.createDom(goog.dom.TagName.A, {href: 'javascript:;'}, 'Show Tweaks');
      var hideLink =
          dh.createDom(goog.dom.TagName.A, {href: 'javascript:;'}, 'Hide Tweaks');
      var ret = dh.createDom(goog.dom.TagName.DIV, null, showLink);
  
      var lazyCreate = function() {
        // Lazily render the UI.
        var ui = new goog.tweak.TweakUi(
            /** @type {!goog.tweak.Registry} */ (registry), dh);
        ui.render();
        // Put the hide link on the same line as the "Show Descriptions" link.
        // Set the style lazily because we can.
        hideLink.style.marginRight = '10px';
        var tweakElem = ui.getRootElement();
        tweakElem.insertBefore(hideLink, tweakElem.firstChild);
        ret.appendChild(tweakElem);
        return tweakElem;
      };
      new goog.ui.Zippy(showLink, lazyCreate, false /* expanded */, hideLink);
      return ret;
    }
  };
  
  
  /**
   * Compares the given entries. Orders alphabetically and groups buttons and
   * expandable groups.
   * @param {!goog.tweak.BaseEntry} a The first entry to compare.
   * @param {!goog.tweak.BaseEntry} b The second entry to compare.
   * @return {number} Refer to goog.array.defaultCompare.
   * @private
   */
  goog.tweak.TweakUi.entryCompare_ = function(a, b) {
    return (
        goog.array.defaultCompare(
            a instanceof goog.tweak.NamespaceEntry_,
            b instanceof goog.tweak.NamespaceEntry_) ||
        goog.array.defaultCompare(
            a instanceof goog.tweak.BooleanGroup,
            b instanceof goog.tweak.BooleanGroup) ||
        goog.array.defaultCompare(
            a instanceof goog.tweak.ButtonAction,
            b instanceof goog.tweak.ButtonAction) ||
        goog.array.defaultCompare(a.label, b.label) ||
        goog.array.defaultCompare(a.getId(), b.getId()));
  };
  
  
  /**
   * @param {!goog.tweak.BaseEntry} entry The entry.
   * @return {boolean} Returns whether the given entry contains sub-entries.
   * @private
   */
  goog.tweak.TweakUi.isGroupEntry_ = function(entry) {
    return entry instanceof goog.tweak.NamespaceEntry_ ||
        entry instanceof goog.tweak.BooleanGroup;
  };
  
  
  /**
   * Returns the list of entries from the given boolean group.
   * @param {!goog.tweak.BooleanGroup} group The group to get the entries from.
   * @return {!Array<!goog.tweak.BaseEntry>} The sorted entries.
   * @private
   */
  goog.tweak.TweakUi.extractBooleanGroupEntries_ = function(group) {
    var ret = goog.object.getValues(group.getChildEntries());
    ret.sort(goog.tweak.TweakUi.entryCompare_);
    return ret;
  };
  
  
  /**
   * @param {!goog.tweak.BaseEntry} entry The entry.
   * @return {string} Returns the namespace for the entry, or '' if it is not
   *     namespaced.
   * @private
   */
  goog.tweak.TweakUi.extractNamespace_ = function(entry) {
    var namespaceMatch = /.+(?=\.)/.exec(entry.getId());
    return namespaceMatch ? namespaceMatch[0] : '';
  };
  
  
  /**
   * @param {!goog.tweak.BaseEntry} entry The entry.
   * @return {string} Returns the part of the label after the last period, unless
   *     the label has been explicly set (it is different from the ID).
   * @private
   */
  goog.tweak.TweakUi.getNamespacedLabel_ = function(entry) {
    var label = entry.label;
    if (label == entry.getId()) {
      label = label.substr(label.lastIndexOf('.') + 1);
    }
    return label;
  };
  
  
  /**
   * @return {!Element} The root element. Must not be called before render().
   */
  goog.tweak.TweakUi.prototype.getRootElement = function() {
    goog.asserts.assert(
        this.entriesPanel_, 'TweakUi.getRootElement called before render().');
    return this.entriesPanel_.getRootElement();
  };
  
  
  /**
   * Reloads the page with query parameters set by the UI.
   * @private
   */
  goog.tweak.TweakUi.prototype.restartWithAppliedTweaks_ = function() {
    var queryString = this.registry_.makeUrlQuery();
    var wnd = this.domHelper_.getWindow();
    if (queryString != wnd.location.search) {
      wnd.location.search = queryString;
    } else {
      wnd.location.reload();
    }
  };
  
  
  /**
   * Installs the required CSS styles.
   * @private
   */
  goog.tweak.TweakUi.prototype.installStyles_ = function() {
    // Use an marker to install the styles only once per document.
    // Styles are injected via JS instead of in a separate style sheet so that
    // they are automatically excluded when tweaks are stripped out.
    var doc = this.domHelper_.getDocument();
    if (!(goog.tweak.TweakUi.STYLE_SHEET_INSTALLED_MARKER_ in doc)) {
      goog.style.installStyles(goog.tweak.TweakUi.CSS_STYLES_, doc);
      doc[goog.tweak.TweakUi.STYLE_SHEET_INSTALLED_MARKER_] = true;
    }
  };
  
  
  /**
   * Creates the element to display when the UI is visible.
   * @return {!Element} The root element.
   */
  goog.tweak.TweakUi.prototype.render = function() {
    this.installStyles_();
    var dh = this.domHelper_;
    // The submit button
    var submitButton = dh.createDom(
        goog.dom.TagName.BUTTON, {style: 'font-weight:bold'}, 'Apply Tweaks');
    submitButton.onclick = goog.bind(this.restartWithAppliedTweaks_, this);
  
    var rootPanel = new goog.tweak.EntriesPanel([], dh);
    var rootPanelDiv = rootPanel.render(submitButton);
    rootPanelDiv.className += ' ' + goog.tweak.TweakUi.ROOT_PANEL_CLASS_;
    this.entriesPanel_ = rootPanel;
  
    var entries = this.registry_.extractEntries(
        true /* excludeChildEntries */, false /* excludeNonSettings */);
    for (var i = 0, entry; entry = entries[i]; i++) {
      this.insertEntry_(entry);
    }
  
    return rootPanelDiv;
  };
  
  
  /**
   * Updates the UI with the given entry.
   * @param {!goog.tweak.BaseEntry} entry The newly registered entry.
   * @private
   */
  goog.tweak.TweakUi.prototype.onNewRegisteredEntry_ = function(entry) {
    if (this.entriesPanel_) {
      this.insertEntry_(entry);
    }
  };
  
  
  /**
   * Updates the UI with the given entry.
   * @param {!goog.tweak.BaseEntry} entry The newly registered entry.
   * @private
   */
  goog.tweak.TweakUi.prototype.insertEntry_ = function(entry) {
    var panel = this.entriesPanel_;
    var namespace = goog.tweak.TweakUi.extractNamespace_(entry);
  
    if (namespace) {
      // Find the NamespaceEntry that the entry belongs to.
      var namespaceEntryId = goog.tweak.NamespaceEntry_.ID_PREFIX + namespace;
      var nsPanel = panel.childPanels[namespaceEntryId];
      if (nsPanel) {
        panel = nsPanel;
      } else {
        entry = new goog.tweak.NamespaceEntry_(namespace, [entry]);
      }
    }
    if (entry instanceof goog.tweak.BooleanInGroupSetting) {
      var group = entry.getGroup();
      // BooleanGroup entries are always registered before their
      // BooleanInGroupSettings.
      panel = panel.childPanels[group.getId()];
    }
    goog.asserts.assert(panel, 'Missing panel for entry %s', entry.getId());
    panel.insertEntry(entry);
  };
  
  
  
  /**
   * The body of the tweaks UI and also used for BooleanGroup.
   * @param {!Array<!goog.tweak.BaseEntry>} entries The entries to show in the
   *     panel.
   * @param {goog.dom.DomHelper=} opt_domHelper The DomHelper to render with.
   * @constructor
   * @final
   */
  goog.tweak.EntriesPanel = function(entries, opt_domHelper) {
    /**
     * The entries to show in the panel.
     * @type {!Array<!goog.tweak.BaseEntry>} entries
     * @private
     */
    this.entries_ = entries;
  
    var self = this;
    /**
     * The bound onclick handler for the help question marks.
     * @this {Element}
     * @private
     */
    this.boundHelpOnClickHandler_ = function() {
      self.onHelpClick_(this.parentNode);
    };
  
    /**
     * The element that contains the UI.
     * @type {Element}
     * @private
     */
    this.rootElem_;
  
    /**
     * The element that contains all of the settings and the endElement.
     * @type {Element}
     * @private
     */
    this.mainPanel_;
  
    /**
     * Flips between true/false each time the "Toggle Descriptions" link is
     * clicked.
     * @type {boolean}
     * @private
     */
    this.showAllDescriptionsState_;
  
    /**
     * The DomHelper to render with.
     * @type {!goog.dom.DomHelper}
     * @private
     */
    this.domHelper_ = opt_domHelper || goog.dom.getDomHelper();
  
    /**
     * Map of tweak ID -> EntriesPanel for child panels (BooleanGroups).
     * @type {!Object<!goog.tweak.EntriesPanel>}
     */
    this.childPanels = {};
  };
  
  
  /**
   * @return {!Element} Returns the expanded element. Must not be called before
   *     render().
   */
  goog.tweak.EntriesPanel.prototype.getRootElement = function() {
    goog.asserts.assert(
        this.rootElem_, 'EntriesPanel.getRootElement called before render().');
    return /** @type {!Element} */ (this.rootElem_);
  };
  
  
  /**
   * Creates and returns the expanded element.
   * The markup looks like:
   *
   *    <div>
   *      <a>Show Descriptions</a>
   *      <div>
   *         ...
   *         {endElement}
   *      </div>
   *    </div>
   *
   * @param {Element|DocumentFragment=} opt_endElement Element to insert after all
   *     tweak entries.
   * @return {!Element} The root element for the panel.
   */
  goog.tweak.EntriesPanel.prototype.render = function(opt_endElement) {
    var dh = this.domHelper_;
    var entries = this.entries_;
    var ret = dh.createDom(goog.dom.TagName.DIV);
  
    var showAllDescriptionsLink = dh.createDom(
        goog.dom.TagName.A, {
          href: 'javascript:;',
          onclick: goog.bind(this.toggleAllDescriptions, this)
        },
        'Toggle all Descriptions');
    ret.appendChild(showAllDescriptionsLink);
  
    // Add all of the entries.
    var mainPanel = dh.createElement(goog.dom.TagName.DIV);
    this.mainPanel_ = mainPanel;
    for (var i = 0, entry; entry = entries[i]; i++) {
      mainPanel.appendChild(this.createEntryElem_(entry));
    }
  
    if (opt_endElement) {
      mainPanel.appendChild(opt_endElement);
    }
    ret.appendChild(mainPanel);
    this.rootElem_ = ret;
    return /** @type {!Element} */ (ret);
  };
  
  
  /**
   * Inserts the given entry into the panel.
   * @param {!goog.tweak.BaseEntry} entry The entry to insert.
   */
  goog.tweak.EntriesPanel.prototype.insertEntry = function(entry) {
    var insertIndex =
        -goog.array.binarySearch(
            this.entries_, entry, goog.tweak.TweakUi.entryCompare_) -
        1;
    goog.asserts.assert(
        insertIndex >= 0, 'insertEntry failed for %s', entry.getId());
    goog.array.insertAt(this.entries_, entry, insertIndex);
    this.mainPanel_.insertBefore(
        this.createEntryElem_(entry),
        // IE doesn't like 'undefined' here.
        this.mainPanel_.childNodes[insertIndex] || null);
  };
  
  
  /**
   * Creates and returns a form element for the given entry.
   * @param {!goog.tweak.BaseEntry} entry The entry.
   * @return {!Element} The root DOM element for the entry.
   * @private
   */
  goog.tweak.EntriesPanel.prototype.createEntryElem_ = function(entry) {
    var dh = this.domHelper_;
    var isGroupEntry = goog.tweak.TweakUi.isGroupEntry_(entry);
    var classes = isGroupEntry ? goog.tweak.TweakUi.ENTRY_GROUP_CSS_CLASSES_ :
                                 goog.tweak.TweakUi.ENTRY_CSS_CLASSES_;
    // Containers should not use label tags or else all descendent inputs will be
    // connected on desktop browsers.
    var containerNodeName =
        isGroupEntry ? goog.dom.TagName.SPAN : goog.dom.TagName.LABEL;
    var ret = dh.createDom(
        goog.dom.TagName.DIV, classes,
        dh.createDom(
            containerNodeName, {
              // Make the hover text the description.
              title: entry.description,
              style: 'color:' + (entry.isRestartRequired() ? '' : 'blue')
            },
            this.createTweakEntryDom_(entry)),
        // Add the expandable help question mark.
        this.createHelpElem_(entry));
    return ret;
  };
  
  
  /**
   * Click handler for the help link.
   * @param {Node} entryDiv The div that contains the tweak.
   * @private
   */
  goog.tweak.EntriesPanel.prototype.onHelpClick_ = function(entryDiv) {
    this.showDescription_(entryDiv, !entryDiv.style.display);
  };
  
  
  /**
   * Twiddle the DOM so that the entry within the given span is shown/hidden.
   * @param {Node} entryDiv The div that contains the tweak.
   * @param {boolean} show True to show, false to hide.
   * @private
   */
  goog.tweak.EntriesPanel.prototype.showDescription_ = function(entryDiv, show) {
    var descriptionElem = entryDiv.lastChild.lastChild;
    goog.style.setElementShown(/** @type {Element} */ (descriptionElem), show);
    entryDiv.style.display = show ? 'block' : '';
  };
  
  
  /**
   * Creates and returns a help element for the given entry.
   * @param {goog.tweak.BaseEntry} entry The entry.
   * @return {!Element} The root element of the created DOM.
   * @private
   */
  goog.tweak.EntriesPanel.prototype.createHelpElem_ = function(entry) {
    // The markup looks like:
    // <span onclick=...><b>?</b><span>{description}</span></span>
    var ret = this.domHelper_.createElement(goog.dom.TagName.SPAN);
    goog.dom.safe.setInnerHtml(
        ret,
        goog.html.SafeHtml.concat(
            goog.html.SafeHtml.create(
                'b', {'style': goog.string.Const.from('padding:0 1em 0 .5em')},
                '?'),
            goog.html.SafeHtml.create(
                'span',
                {'style': goog.string.Const.from('display:none;color:#666')})));
    ret.onclick = this.boundHelpOnClickHandler_;
    // IE<9 doesn't support lastElementChild.
    var descriptionElem = /** @type {!Element} */ (ret.lastChild);
    if (entry.isRestartRequired()) {
      goog.dom.setTextContent(descriptionElem, entry.description);
    } else {
      goog.dom.safe.setInnerHtml(
          descriptionElem,
          goog.html.SafeHtml.concat(
              goog.html.SafeHtml.htmlEscape(entry.description),
              goog.html.SafeHtml.create(
                  'span', {'style': goog.string.Const.from('color: blue')},
                  '(no restart required)')));
    }
    return ret;
  };
  
  
  /**
   * Show all entry descriptions (has the same effect as clicking on all ?'s).
   */
  goog.tweak.EntriesPanel.prototype.toggleAllDescriptions = function() {
    var show = !this.showAllDescriptionsState_;
    this.showAllDescriptionsState_ = show;
    var entryDivs = this.domHelper_.getElementsByTagNameAndClass(
        goog.dom.TagName.DIV, goog.tweak.TweakUi.ENTRY_CSS_CLASS_,
        this.rootElem_);
    for (var i = 0, div; div = entryDivs[i]; i++) {
      this.showDescription_(div, show);
    }
  };
  
  
  /**
   * Creates the DOM element to control the given enum setting.
   * @param {!goog.tweak.StringSetting|!goog.tweak.NumericSetting} tweak The
   *     setting.
   * @param {string} label The label for the entry.
   * @param {!Function} onchangeFunc onchange event handler.
   * @return {!DocumentFragment} The DOM element.
   * @private
   */
  goog.tweak.EntriesPanel.prototype.createComboBoxDom_ = function(
      tweak, label, onchangeFunc) {
    // The markup looks like:
    // Label: <select><option></option></select>
    var dh = this.domHelper_;
    var ret = dh.getDocument().createDocumentFragment();
    ret.appendChild(dh.createTextNode(label + ': '));
    var selectElem = dh.createElement(goog.dom.TagName.SELECT);
    var values = tweak.getValidValues();
    for (var i = 0, il = values.length; i < il; ++i) {
      var optionElem = dh.createElement(goog.dom.TagName.OPTION);
      optionElem.text = String(values[i]);
      // Setting the option tag's value is required for selectElem.value to work
      // properly.
      optionElem.value = String(values[i]);
      selectElem.appendChild(optionElem);
    }
    ret.appendChild(selectElem);
  
    // Set the value and add a callback.
    selectElem.value = String(tweak.getNewValue());
    selectElem.onchange = onchangeFunc;
    tweak.addCallback(function() {
      selectElem.value = String(tweak.getNewValue());
    });
    return ret;
  };
  
  
  /**
   * Creates the DOM element to control the given boolean setting.
   * @param {!goog.tweak.BooleanSetting} tweak The setting.
   * @param {string} label The label for the entry.
   * @return {!DocumentFragment} The DOM elements.
   * @private
   */
  goog.tweak.EntriesPanel.prototype.createBooleanSettingDom_ = function(
      tweak, label) {
    var dh = this.domHelper_;
    var ret = dh.getDocument().createDocumentFragment();
    var checkbox = dh.createDom(goog.dom.TagName.INPUT, {type: 'checkbox'});
    ret.appendChild(checkbox);
    ret.appendChild(dh.createTextNode(label));
  
    // Needed on IE6 to ensure the textbox doesn't get cleared
    // when added to the DOM.
    checkbox.defaultChecked = tweak.getNewValue();
  
    checkbox.checked = tweak.getNewValue();
    checkbox.onchange = function() { tweak.setValue(checkbox.checked); };
    tweak.addCallback(function() { checkbox.checked = tweak.getNewValue(); });
    return ret;
  };
  
  
  /**
   * Creates the DOM for a BooleanGroup or NamespaceEntry.
   * @param {!goog.tweak.BooleanGroup|!goog.tweak.NamespaceEntry_} entry The
   *     entry.
   * @param {string} label The label for the entry.
   * @param {!Array<goog.tweak.BaseEntry>} childEntries The child entries.
   * @return {!DocumentFragment} The DOM element.
   * @private
   */
  goog.tweak.EntriesPanel.prototype.createSubPanelDom_ = function(
      entry, label, childEntries) {
    var dh = this.domHelper_;
    var toggleLink =
        dh.createDom(goog.dom.TagName.A, {href: 'javascript:;'}, label + ' \xBB');
    var toggleLink2 =
        dh.createDom(goog.dom.TagName.A, {href: 'javascript:;'}, '\xAB ' + label);
    toggleLink2.style.marginRight = '10px';
  
    var innerUi = new goog.tweak.EntriesPanel(childEntries, dh);
    this.childPanels[entry.getId()] = innerUi;
  
    var elem = innerUi.render();
    // Move the toggle descriptions link into the legend.
    var descriptionsLink = elem.firstChild;
    var childrenElem = dh.createDom(
        goog.dom.TagName.FIELDSET, goog.getCssName('goog-inline-block'),
        dh.createDom(
            goog.dom.TagName.LEGEND, null, toggleLink2, descriptionsLink),
        elem);
  
    new goog.ui.Zippy(
        toggleLink, childrenElem, false /* expanded */, toggleLink2);
  
    var ret = dh.getDocument().createDocumentFragment();
    ret.appendChild(toggleLink);
    ret.appendChild(childrenElem);
    return ret;
  };
  
  
  /**
   * Creates the DOM element to control the given string setting.
   * @param {!goog.tweak.StringSetting|!goog.tweak.NumericSetting} tweak The
   *     setting.
   * @param {string} label The label for the entry.
   * @param {!Function} onchangeFunc onchange event handler.
   * @return {!DocumentFragment} The DOM element.
   * @private
   */
  goog.tweak.EntriesPanel.prototype.createTextBoxDom_ = function(
      tweak, label, onchangeFunc) {
    var dh = this.domHelper_;
    var ret = dh.getDocument().createDocumentFragment();
    ret.appendChild(dh.createTextNode(label + ': '));
    var textBox = dh.createDom(goog.dom.TagName.INPUT, {
      value: String(tweak.getNewValue()),
      // TODO(agrieve): Make size configurable or autogrow.
      size: 5,
      onblur: onchangeFunc
    });
    ret.appendChild(textBox);
    tweak.addCallback(function() {
      textBox.value = String(tweak.getNewValue());
    });
    return ret;
  };
  
  
  /**
   * Creates the DOM element to control the given button action.
   * @param {!goog.tweak.ButtonAction} tweak The action.
   * @param {string} label The label for the entry.
   * @return {!Element} The DOM element.
   * @private
   */
  goog.tweak.EntriesPanel.prototype.createButtonActionDom_ = function(
      tweak, label) {
    return this.domHelper_.createDom(
        goog.dom.TagName.BUTTON, {onclick: goog.bind(tweak.fireCallbacks, tweak)},
        label);
  };
  
  
  /**
   * Creates the DOM element to control the given entry.
   * @param {!goog.tweak.BaseEntry} entry The entry.
   * @return {!Element|!DocumentFragment} The DOM element.
   * @private
   */
  goog.tweak.EntriesPanel.prototype.createTweakEntryDom_ = function(entry) {
    var label = goog.tweak.TweakUi.getNamespacedLabel_(entry);
    if (entry instanceof goog.tweak.BooleanSetting) {
      return this.createBooleanSettingDom_(entry, label);
    } else if (entry instanceof goog.tweak.BooleanGroup) {
      var childEntries = goog.tweak.TweakUi.extractBooleanGroupEntries_(entry);
      return this.createSubPanelDom_(entry, label, childEntries);
    } else if (entry instanceof goog.tweak.StringSetting) {
      /** @this {Element} */
      var setValueFunc = function() { entry.setValue(this.value); };
      return entry.getValidValues() ?
          this.createComboBoxDom_(entry, label, setValueFunc) :
          this.createTextBoxDom_(entry, label, setValueFunc);
    } else if (entry instanceof goog.tweak.NumericSetting) {
      setValueFunc = function() {
        // Reset the value if it's not a number.
        if (isNaN(this.value)) {
          this.value = entry.getNewValue();
        } else {
          entry.setValue(+this.value);
        }
      };
      return entry.getValidValues() ?
          this.createComboBoxDom_(entry, label, setValueFunc) :
          this.createTextBoxDom_(entry, label, setValueFunc);
    } else if (entry instanceof goog.tweak.NamespaceEntry_) {
      return this.createSubPanelDom_(entry, entry.label, entry.entries);
    }
    goog.asserts.assertInstanceof(
        entry, goog.tweak.ButtonAction, 'invalid entry: %s', entry);
    return this.createButtonActionDom_(
        /** @type {!goog.tweak.ButtonAction} */ (entry), label);
  };
  
  
  
  /**
   * Entries used to represent the collapsible namespace links. These entries are
   * never registered with the TweakRegistry, but are contained within the
   * collection of entries within TweakPanels.
   * @param {string} namespace The namespace for the entry.
   * @param {!Array<!goog.tweak.BaseEntry>} entries Entries within the namespace.
   * @constructor
   * @extends {goog.tweak.BaseEntry}
   * @private
   */
  goog.tweak.NamespaceEntry_ = function(namespace, entries) {
    goog.tweak.BaseEntry.call(
        this, goog.tweak.NamespaceEntry_.ID_PREFIX + namespace,
        'Tweaks within the ' + namespace + ' namespace.');
  
    /**
     * Entries within this namespace.
     * @type {!Array<!goog.tweak.BaseEntry>}
     */
    this.entries = entries;
  
    this.label = namespace;
  };
  goog.inherits(goog.tweak.NamespaceEntry_, goog.tweak.BaseEntry);
  
  
  /**
   * Prefix for the IDs of namespace entries used to ensure that they do not
   * conflict with regular entries.
   * @type {string}
   */
  goog.tweak.NamespaceEntry_.ID_PREFIX = '!';
  