Reduce uniqueness of docker images to prevent continuous aggregation
Fixes #747

Signed-off-by: Tully Foote <22715502305f5acc3bd5bf22a9c406e70a1e1348@osrfoundation.org>

<project>
  <actions/>
  <description>Generated at @ESCAPE(now_str) from template '@ESCAPE(template_name)'@
@[if disabled]
but disabled since the package is blacklisted (or not whitelisted) in the configuration file@
@[end if]@
@ </description>
  <keepDependencies>false</keepDependencies>
  <properties>
@(SNIPPET(
    'property_log-rotator',
    days_to_keep=730,
    num_to_keep=100,
))@
@[if job_priority is not None]@
@(SNIPPET(
    'property_job-priority',
    priority=job_priority,
))@
@[end if]@
@(SNIPPET(
    'property_requeue-job',
))@
@{
parameters = [
    {
        'type': 'boolean',
        'name': 'skip_cleanup',
        'description': 'Skip cleanup of build artifacts as well as rosdoc index',
    },
    {
        'type': 'string',
        'name': 'install_packages',
        'default_value': ' '.join(install_packages),
        'description': 'Package(s) to be installed prior to any packages detected for installation by rosdep (space-separated)',
    },
    {
        'type': 'string',
        'name': 'repos_file_urls',
        'default_value': ' '.join(repos_file_urls),
        'description': 'URL(s) of repos file(s) containing the list of packages to be built (space-separated)',
    },
    {
        'type': 'string',
        'name': 'repository_names',
        'default_value': ' '.join(repository_names),
        'description': 'Repositories names from the rosdistro to be built (space-separated)',
    },
    {
        'type': 'string',
        'name': 'test_branch',
        'default_value': test_branch or '',
        'description': 'Branch to attempt to checkout before doing batch job',
    },
    {
        'type': 'string',
        'name': 'package_selection_args',
        'default_value': package_selection_args or '',
        'description': 'Package selection arguments passed to colcon to specify which packages should be built and tested, or blank for ALL',
    },
    {
        'type': 'string',
        'name': 'build_tool_args',
        'default_value': build_tool_args or '',
        'description': 'Arbitrary arguments passed to the build tool',
    },
]
}@
@(SNIPPET(
    'property_parameters-definition',
    parameters=parameters,
))@
@(SNIPPET(
    'property_authorization-matrix',
    project_authorization_xml=project_authorization_xml
))@
  </properties>
  <scmCheckoutRetryCount>2</scmCheckoutRetryCount>
  <assignedNode>@(node_label)</assignedNode>
  <canRoam>false</canRoam>
  <disabled>@('true' if disabled else 'false')</disabled>
  <blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
  <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
  <triggers>
@[if trigger_jobs]@
@(SNIPPET(
    'trigger_reverse-build',
    upstream_projects=trigger_jobs,
))@
@[end if]@
@[if trigger_timer is not None]@
@(SNIPPET(
    'trigger_timer',
    spec=trigger_timer,
))@
@[end if]@
  </triggers>
  <concurrentBuild>true</concurrentBuild>
  <builders>
@(SNIPPET(
    'builder_system-groovy_check-free-disk-space',
))@
@(SNIPPET(
    'builder_shell_docker-info',
))@
@(SNIPPET(
    'builder_check-docker',
    os_name=os_name,
    os_code_name=os_code_name,
    arch=arch,
))@
@(SNIPPET(
    'builder_shell_clone-ros-buildfarm',
    ros_buildfarm_repository=ros_buildfarm_repository,
    wrapper_scripts=wrapper_scripts,
))@
@(SNIPPET(
    'builder_shell_key-files',
    script_generating_key_files=script_generating_key_files,
))@
@[if underlay_source_jobs]@
@(SNIPPET(
    'builder_shell',
    script='\n'.join([
        'echo "# BEGIN SECTION: Prepare package underlay"',
    ]),
))@
@[for i, underlay_source_job in enumerate(underlay_source_jobs, 1)]@
@(SNIPPET(
    'copy_artifacts',
    artifacts=[
      '*.tar.bz2',
    ],
    project=underlay_source_job,
    target_directory='$WORKSPACE/underlay%d' % (i),
))@
@[end for]@
@(SNIPPET(
    'builder_shell',
    script='\n'.join([
        'tar -xjf $WORKSPACE/underlay%d/*.tar.bz2 -C $WORKSPACE/underlay%d' %
        (i + 1, i + 1) for i in range(len(underlay_source_jobs))
    ] + [
        'echo "# END SECTION"',
    ]),
))@
@[end if]@
@(SNIPPET(
    'builder_shell',
    script='\n'.join([
        'rm -fr $WORKSPACE/docker_generating_dockers',
        'mkdir -p $WORKSPACE/docker_generating_dockers',
        '',
        '# monitor all subprocesses and enforce termination',
        'python3 -u $WORKSPACE/ros_buildfarm/scripts/subprocess_reaper.py $$ --cid-file $WORKSPACE/docker_generating_dockers/docker.cid > $WORKSPACE/docker_generating_dockers/subprocess_reaper.log 2>&1 &',
        '# sleep to give python time to startup',
        'sleep 1',
        '',
        '# create a unique dockerfile name prefix',
        '[ -z "$EXECUTOR_NUMBER" ] && export EXECUTOR_NUMBER=0',
        'export DOCKER_IMAGE_PREFIX=executor_$EXECUTOR_NUMBER',
        '',
        '# generate Dockerfile, build and run it',
        '# generating the Dockerfiles for the actual CI tasks',
        'echo "# BEGIN SECTION: Generate Dockerfile - CI tasks"',
        'export TZ="%s"' % timezone,
        'export PYTHONPATH=$WORKSPACE/ros_buildfarm:$PYTHONPATH',
        'python3 -u $WORKSPACE/ros_buildfarm/scripts/ci/run_ci_job.py' +
        ' ' + rosdistro_name +
        ' ' + os_name +
        ' ' + os_code_name +
        ' ' + arch +
        ' ' + ' '.join(repository_args) +
        ' --build-tool ' + build_tool +
        ' --ros-version ' + str(ros_version) +
        ' --env-vars ' + ' '.join([v.replace('$', '\\$',) for v in build_environment_variables]) +
        ' --dockerfile-dir $WORKSPACE/docker_generating_dockers' +
        ' --repos-file-urls $repos_file_urls' +
        ' --repository-names $repository_names' +
        ' --test-branch "$test_branch"' +
        ' --skip-rosdep-keys ' + ' '.join(skip_rosdep_keys) +
        ' --install-packages $install_packages' +
        ' --workspace-mount-point /tmp/ws' + ''.join([
            ' /tmp/ws%d' % (i + 2) for i in range(len(underlay_source_paths))
        ]) +
        ' --package-selection-args $package_selection_args' +
        ' --build-tool-args $build_tool_args',
        'echo "# END SECTION"',
        '',
        'echo "# BEGIN SECTION: Build Dockerfile - generating CI tasks"',
        'cd $WORKSPACE/docker_generating_dockers',
        'python3 -u $WORKSPACE/ros_buildfarm/scripts/misc/docker_pull_baseimage.py',
        'docker build --force-rm -t $DOCKER_IMAGE_PREFIX.ci_task_generation.%s .' % (rosdistro_name),
        'echo "# END SECTION"',
        '',
        'echo "# BEGIN SECTION: Run Dockerfile - generating CI tasks"',
        'rm -fr $WORKSPACE/docker_create_workspace',
        'rm -fr $WORKSPACE/docker_build_and_install',
        'rm -fr $WORKSPACE/docker_build_and_test',
        'mkdir -p $WORKSPACE/docker_create_workspace',
        'mkdir -p $WORKSPACE/docker_build_and_install',
        'mkdir -p $WORKSPACE/docker_build_and_test',
        'docker run' +
        ' --rm ' +
        ' --cidfile=$WORKSPACE/docker_generating_dockers/docker.cid' +
        ' -e=HOME=/home/buildfarm' +
        ' -v $WORKSPACE/ros_buildfarm:/tmp/ros_buildfarm:ro' +
        ' -v $WORKSPACE/docker_create_workspace:/tmp/docker_create_workspace' +
        ' -v $WORKSPACE/docker_build_and_install:/tmp/docker_build_and_install' +
        ' -v $WORKSPACE/docker_build_and_test:/tmp/docker_build_and_test' +
        ' $DOCKER_IMAGE_PREFIX.ci_task_generation.%s' % (rosdistro_name),
        'cd -',  # restore pwd when used in scripts
        'echo "# END SECTION"',
    ]),
))@
@(SNIPPET(
    'builder_shell',
    script='\n'.join([
        '# monitor all subprocesses and enforce termination',
        'python3 -u $WORKSPACE/ros_buildfarm/scripts/subprocess_reaper.py $$ --cid-file $WORKSPACE/docker_create_workspace/docker.cid > $WORKSPACE/docker_create_workspace/subprocess_reaper.log 2>&1 &',
        '# sleep to give python time to startup',
        'sleep 1',
        '',
        '# create a unique dockerfile name prefix',
        'export DOCKER_IMAGE_PREFIX=$(date +%s.%N)',
        '',
        'echo "# BEGIN SECTION: Build Dockerfile - create workspace"',
        '# build and run create_workspace Dockerfile',
        'cd $WORKSPACE/docker_create_workspace',
        'python3 -u $WORKSPACE/ros_buildfarm/scripts/misc/docker_pull_baseimage.py',
        'docker build --force-rm -t $DOCKER_IMAGE_PREFIX.ci_create_workspace.%s .' % (rosdistro_name),
        'echo "# END SECTION"',
        '',
        'echo "# BEGIN SECTION: Run Dockerfile - create workspace"',
    ] + [
        'export UNDERLAY%d_JOB_SPACE=$WORKSPACE/underlay%d/ros%d-linux' % (i + 1, i + 1, local_ros_version)
        for i, local_ros_version in zip(range(len(underlay_source_jobs)), [ros_version] * len(underlay_source_jobs))
    ] + [
        'rm -fr $WORKSPACE/ws/src',
        'mkdir -p $WORKSPACE/ws/src',
    ] + [
        'mkdir -p %s' % (dir) for dir in underlay_source_paths
    ] + [
        'docker run' +
        ' --rm ' +
        ' --cidfile=$WORKSPACE/docker_create_workspace/docker.cid' +
        ' -v $WORKSPACE/ros_buildfarm:/tmp/ros_buildfarm:ro' +
        ''.join([
            ' -v %s:/tmp/ws%s/install_isolated:ro' % (space, i if i > 1 else '')
            for i, space in enumerate(underlay_source_paths, 1)
        ]) +
        ' -v $WORKSPACE/ws:/tmp/ws%s' % (len(underlay_source_paths) + 1 if len(underlay_source_paths) else '') +
        ' $DOCKER_IMAGE_PREFIX.ci_create_workspace.%s' % (rosdistro_name),
        'cd -',  # restore pwd when used in scripts
        'echo "# END SECTION"',
    ]),
))@
@(SNIPPET(
    'builder_shell',
    script='\n'.join([
        'echo "# BEGIN SECTION: Copy dependency list"',
        '/bin/cp -f $WORKSPACE/ws/install_list_build.txt $WORKSPACE/docker_build_and_install/install_list_build.txt',
        '/bin/cp -f $WORKSPACE/ws/install_list_build.txt $WORKSPACE/docker_build_and_test/install_list_build.txt',
        '/bin/cp -f $WORKSPACE/ws/install_list_test.txt $WORKSPACE/docker_build_and_test/install_list_test.txt',
        'echo "# END SECTION"',
    ]),
))@
@(SNIPPET(
    'builder_shell',
    script='\n'.join([
        '# monitor all subprocesses and enforce termination',
        'python3 -u $WORKSPACE/ros_buildfarm/scripts/subprocess_reaper.py $$ --cid-file $WORKSPACE/docker_build_and_install/docker.cid > $WORKSPACE/docker_build_and_install/subprocess_reaper.log 2>&1 &',
        '# sleep to give python time to startup',
        'sleep 1',
        '',
        '# create a unique dockerfile name prefix',
        'export DOCKER_IMAGE_PREFIX=$(date +%s.%N)',
        '',
        'echo "# BEGIN SECTION: Build Dockerfile - build and install"',
        '# build and run build and install Dockerfile',
        'cd $WORKSPACE/docker_build_and_install',
        'python3 -u $WORKSPACE/ros_buildfarm/scripts/misc/docker_pull_baseimage.py',
        'docker build --force-rm -t $DOCKER_IMAGE_PREFIX.ci_build_and_install.%s .' % (rosdistro_name),
        'echo "# END SECTION"',
        '',
        'echo "# BEGIN SECTION: ccache stats (before)"',
        'mkdir -p $HOME/.ccache',
        'docker run' +
        ' --rm ' +
        ' --cidfile=$WORKSPACE/docker_build_and_install/docker_ccache_before.cid' +
        ' -e CCACHE_DIR=/home/buildfarm/.ccache' +
        ' -v $HOME/.ccache:/home/buildfarm/.ccache' +
        ' $DOCKER_IMAGE_PREFIX.ci_build_and_install.%s' % (rosdistro_name) +
        ' "ccache -s"',
        'echo "# END SECTION"',
        '',
        'echo "# BEGIN SECTION: Run Dockerfile - build and install"',
    ] + [
        'export UNDERLAY%d_JOB_SPACE=$WORKSPACE/underlay%d/ros%d-linux' % (i + 1, i + 1, local_ros_version)
        for i, local_ros_version in zip(range(len(underlay_source_jobs)), [ros_version] * len(underlay_source_jobs))
    ] + [
        'docker run' +
        ' --rm ' +
        ' --cidfile=$WORKSPACE/docker_build_and_install/docker.cid' +
        ' -e CCACHE_DIR=/home/buildfarm/.ccache' +
        ' -v $HOME/.ccache:/home/buildfarm/.ccache' +
        ' -v $WORKSPACE/ros_buildfarm:/tmp/ros_buildfarm:ro' +
        ''.join([
            ' -v %s:/tmp/ws%s/install_isolated:ro' % (space, i if i > 1 else '')
            for i, space in enumerate(underlay_source_paths, 1)
        ]) +
        ' -v $WORKSPACE/ws:/tmp/ws%s' % (len(underlay_source_paths) + 1 if len(underlay_source_paths) else '') +
        ' $DOCKER_IMAGE_PREFIX.ci_build_and_install.%s' % (rosdistro_name),
        'cd -',  # restore pwd when used in scripts
        'echo "# END SECTION"',
        '',
        'echo "# BEGIN SECTION: ccache stats (after)"',
        'docker run' +
        ' --rm ' +
        ' --cidfile=$WORKSPACE/docker_build_and_install/docker_ccache_after.cid' +
        ' -e CCACHE_DIR=/home/buildfarm/.ccache' +
        ' -v $HOME/.ccache:/home/buildfarm/.ccache' +
        ' $DOCKER_IMAGE_PREFIX.ci_build_and_install.%s' % (rosdistro_name) +
        ' "ccache -s"',
        'echo "# END SECTION"',
    ]),
))@
@(SNIPPET(
    'builder_shell',
    script='\n'.join([
        'echo "# BEGIN SECTION: Compress install space"',
        'tar -cjf $WORKSPACE/ros%d-%s-linux-%s-%s-ci.tar.bz2 ' % (ros_version, rosdistro_name, os_code_name, arch) +
        ' -C $WORKSPACE/ws' +
        ' --transform "s/^install_isolated/ros%d-linux/"' % (ros_version) +
        ' install_isolated',
        'echo "# END SECTION"',
    ]),
))@
@(SNIPPET(
    'builder_shell',
    script='\n'.join([
        '# monitor all subprocesses and enforce termination',
        'python3 -u $WORKSPACE/ros_buildfarm/scripts/subprocess_reaper.py $$ --cid-file $WORKSPACE/docker_build_and_test/docker.cid > $WORKSPACE/docker_build_and_test/subprocess_reaper.log 2>&1 &',
        '# sleep to give python time to startup',
        'sleep 1',
        '',
        '# create a unique dockerfile name prefix',
        'export DOCKER_IMAGE_PREFIX=$(date +%s.%N)',
        '',
        'echo "# BEGIN SECTION: Build Dockerfile - build and test"',
        '# build and run build and test Dockerfile',
        'cd $WORKSPACE/docker_build_and_test',
        'python3 -u $WORKSPACE/ros_buildfarm/scripts/misc/docker_pull_baseimage.py',
        'docker build --force-rm -t $DOCKER_IMAGE_PREFIX.ci_build_and_test.%s .' % (rosdistro_name),
        'echo "# END SECTION"',
        '',
        'echo "# BEGIN SECTION: ccache stats (before)"',
        'mkdir -p $HOME/.ccache',
        'docker run' +
        ' --rm ' +
        ' --cidfile=$WORKSPACE/docker_build_and_test/docker_ccache_before.cid' +
        ' -e CCACHE_DIR=/home/buildfarm/.ccache' +
        ' -v $HOME/.ccache:/home/buildfarm/.ccache' +
        ' $DOCKER_IMAGE_PREFIX.ci_build_and_test.%s' % (rosdistro_name) +
        ' "ccache -s"',
        'echo "# END SECTION"',
        '',
        'echo "# BEGIN SECTION: Run Dockerfile - build and test"',
    ] + [
        'export UNDERLAY%d_JOB_SPACE=$WORKSPACE/underlay%d/ros%d-linux' % (i + 1, i + 1, local_ros_version)
        for i, local_ros_version in zip(range(len(underlay_source_jobs)), [ros_version] * len(underlay_source_jobs))
    ] + [
        'rm -fr $WORKSPACE/ws/test_results',
        'mkdir -p $WORKSPACE/ws/test_results',
        'docker run' +
        ' --rm ' +
        ' --cidfile=$WORKSPACE/docker_build_and_test/docker.cid' +
        ' -e CCACHE_DIR=/home/buildfarm/.ccache' +
        ' -v $HOME/.ccache:/home/buildfarm/.ccache' +
        ' -v $WORKSPACE/ros_buildfarm:/tmp/ros_buildfarm:ro' +
        ''.join([
            ' -v %s:/tmp/ws%s/install_isolated:ro' % (space, i if i > 1 else '')
            for i, space in enumerate(underlay_source_paths, 1)
        ]) +
        ' -v $WORKSPACE/ws:/tmp/ws%s' % (len(underlay_source_paths) + 1 if len(underlay_source_paths) else '') +
        ' $DOCKER_IMAGE_PREFIX.ci_build_and_test.%s' % (rosdistro_name),
        'cd -',  # restore pwd when used in scripts
        'echo "# END SECTION"',
        '',
        'echo "# BEGIN SECTION: ccache stats (after)"',
        'docker run' +
        ' --rm ' +
        ' --cidfile=$WORKSPACE/docker_build_and_test/docker_ccache_after.cid' +
        ' -e CCACHE_DIR=/home/buildfarm/.ccache' +
        ' -v $HOME/.ccache:/home/buildfarm/.ccache' +
        ' $DOCKER_IMAGE_PREFIX.ci_build_and_test.%s' % (rosdistro_name) +
        ' "ccache -s"',
        'echo "# END SECTION"',
    ]),
))@
@(SNIPPET(
    'builder_shell',
    script='\n'.join([
        'if [ "$skip_cleanup" = "false" ]; then',
        'echo "# BEGIN SECTION: Clean up to save disk space on agents"',
        'rm -fr ws/build_isolated',
        'rm -fr ws/devel_isolated',
        'rm -fr ws/install_isolated',
        'echo "# END SECTION"',
        'fi',
    ]),
))@
  </builders>
  <publishers>
@(SNIPPET(
    'publisher_warnings',
    unstable_threshold='',
))@
@(SNIPPET(
    'archive_artifacts',
    artifacts=[
      'ros%d-%s-linux-%s-%s-ci.tar.bz2' % (ros_version, rosdistro_name, os_code_name, arch),
    ] + [image for images in show_images.values() for image in images],
))@
@[for title, artifacts in show_images.items()]@
@(SNIPPET(
    'image_gallery',
    title=title,
    image_width='',
    artifacts=artifacts,
))@
@[end for]@
@[if show_plots]@
@(SNIPPET(
    'plot_plugin',
    plots=show_plots,
))@
@[end if]@
@[if xunit_publisher_types]@
@(SNIPPET(
    'publisher_xunit',
    types=xunit_publisher_types,
))@
@[end if]@
  </publishers>
  <buildWrappers>
@[if timeout_minutes is not None]@
@(SNIPPET(
    'build-wrapper_build-timeout',
    timeout_minutes=timeout_minutes,
))@
@[end if]@
@(SNIPPET(
    'build-wrapper_timestamper',
))@
  </buildWrappers>
</project>

<project>
  <actions/>
  <description>Generated at @ESCAPE(now_str) from template '@ESCAPE(template_name)'@
@[if disabled]
but disabled since the package is blacklisted (or not whitelisted) in the configuration file@
@[end if]@
@ </description>
  <keepDependencies>false</keepDependencies>
  <properties>
@(SNIPPET(
    'property_log-rotator',
    days_to_keep=730,
    num_to_keep=100,
))@
@[if job_priority is not None]@
@(SNIPPET(
    'property_job-priority',
    priority=job_priority,
))@
@[end if]@
@(SNIPPET(
    'property_requeue-job',
))@
@{
parameters = [
    {
        'type': 'boolean',
        'name': 'skip_cleanup',
        'description': 'Skip cleanup of build artifacts as well as rosdoc index',
    },
    {
        'type': 'string',
        'name': 'install_packages',
        'default_value': ' '.join(install_packages),
        'description': 'Package(s) to be installed prior to any packages detected for installation by rosdep (space-separated)',
    },
    {
        'type': 'string',
        'name': 'repos_file_urls',
        'default_value': ' '.join(repos_file_urls),
        'description': 'URL(s) of repos file(s) containing the list of packages to be built (space-separated)',
    },
    {
        'type': 'string',
        'name': 'repository_names',
        'default_value': ' '.join(repository_names),
        'description': 'Repositories names from the rosdistro to be built (space-separated)',
    },
    {
        'type': 'string',
        'name': 'test_branch',
        'default_value': test_branch or '',
        'description': 'Branch to attempt to checkout before doing batch job',
    },
    {
        'type': 'string',
        'name': 'package_selection_args',
        'default_value': package_selection_args or '',
        'description': 'Package selection arguments passed to colcon to specify which packages should be built and tested, or blank for ALL',
    },
    {
        'type': 'string',
        'name': 'build_tool_args',
        'default_value': build_tool_args or '',
        'description': 'Arbitrary arguments passed to the build tool',
    },
]
}@
@(SNIPPET(
    'property_parameters-definition',
    parameters=parameters,
))@
@(SNIPPET(
    'property_authorization-matrix',
    project_authorization_xml=project_authorization_xml
))@
  </properties>
  <scmCheckoutRetryCount>2</scmCheckoutRetryCount>
  <assignedNode>@(node_label)</assignedNode>
  <canRoam>false</canRoam>
  <disabled>@('true' if disabled else 'false')</disabled>
  <blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
  <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
  <triggers>
@[if trigger_jobs]@
@(SNIPPET(
    'trigger_reverse-build',
    upstream_projects=trigger_jobs,
))@
@[end if]@
@[if trigger_timer is not None]@
@(SNIPPET(
    'trigger_timer',
    spec=trigger_timer,
))@
@[end if]@
  </triggers>
  <concurrentBuild>true</concurrentBuild>
  <builders>
@(SNIPPET(
    'builder_system-groovy_check-free-disk-space',
))@
@(SNIPPET(
    'builder_shell_docker-info',
))@
@(SNIPPET(
    'builder_check-docker',
    os_name=os_name,
    os_code_name=os_code_name,
    arch=arch,
))@
@(SNIPPET(
    'builder_shell_clone-ros-buildfarm',
    ros_buildfarm_repository=ros_buildfarm_repository,
    wrapper_scripts=wrapper_scripts,
))@
@(SNIPPET(
    'builder_shell_key-files',
    script_generating_key_files=script_generating_key_files,
))@
@[if underlay_source_jobs]@
@(SNIPPET(
    'builder_shell',
    script='\n'.join([
        'echo "# BEGIN SECTION: Prepare package underlay"',
    ]),
))@
@[for i, underlay_source_job in enumerate(underlay_source_jobs, 1)]@
@(SNIPPET(
    'copy_artifacts',
    artifacts=[
      '*.tar.bz2',
    ],
    project=underlay_source_job,
    target_directory='$WORKSPACE/underlay%d' % (i),
))@
@[end for]@
@(SNIPPET(
    'builder_shell',
    script='\n'.join([
        'tar -xjf $WORKSPACE/underlay%d/*.tar.bz2 -C $WORKSPACE/underlay%d' %
        (i + 1, i + 1) for i in range(len(underlay_source_jobs))
    ] + [
        'echo "# END SECTION"',
    ]),
))@
@[end if]@
@(SNIPPET(
    'builder_shell',
    script='\n'.join([
        'rm -fr $WORKSPACE/docker_generating_dockers',
        'mkdir -p $WORKSPACE/docker_generating_dockers',
        '',
        '# monitor all subprocesses and enforce termination',
        'python3 -u $WORKSPACE/ros_buildfarm/scripts/subprocess_reaper.py $$ --cid-file $WORKSPACE/docker_generating_dockers/docker.cid > $WORKSPACE/docker_generating_dockers/subprocess_reaper.log 2>&1 &',
        '# sleep to give python time to startup',
        'sleep 1',
        '',
        '# create a unique dockerfile name prefix',
        'export DOCKER_IMAGE_PREFIX=$(date +%s.%N)',
        '',
        '# generate Dockerfile, build and run it',
        '# generating the Dockerfiles for the actual CI tasks',
        'echo "# BEGIN SECTION: Generate Dockerfile - CI tasks"',
        'export TZ="%s"' % timezone,
        'export PYTHONPATH=$WORKSPACE/ros_buildfarm:$PYTHONPATH',
        'python3 -u $WORKSPACE/ros_buildfarm/scripts/ci/run_ci_job.py' +
        ' ' + rosdistro_name +
        ' ' + os_name +
        ' ' + os_code_name +
        ' ' + arch +
        ' ' + ' '.join(repository_args) +
        ' --build-tool ' + build_tool +
        ' --ros-version ' + str(ros_version) +
        ' --env-vars ' + ' '.join([v.replace('$', '\\$',) for v in build_environment_variables]) +
        ' --dockerfile-dir $WORKSPACE/docker_generating_dockers' +
        ' --repos-file-urls $repos_file_urls' +
        ' --repository-names $repository_names' +
        ' --test-branch "$test_branch"' +
        ' --skip-rosdep-keys ' + ' '.join(skip_rosdep_keys) +
        ' --install-packages $install_packages' +
        ' --workspace-mount-point /tmp/ws' + ''.join([
            ' /tmp/ws%d' % (i + 2) for i in range(len(underlay_source_paths))
        ]) +
        ' --package-selection-args $package_selection_args' +
        ' --build-tool-args $build_tool_args',
        'echo "# END SECTION"',
        '',
        'echo "# BEGIN SECTION: Build Dockerfile - generating CI tasks"',
        'cd $WORKSPACE/docker_generating_dockers',
        'python3 -u $WORKSPACE/ros_buildfarm/scripts/misc/docker_pull_baseimage.py',
        'docker build --force-rm -t $DOCKER_IMAGE_PREFIX.ci_task_generation.%s .' % (rosdistro_name),
        'echo "# END SECTION"',
        '',
        'echo "# BEGIN SECTION: Run Dockerfile - generating CI tasks"',
        'rm -fr $WORKSPACE/docker_create_workspace',
        'rm -fr $WORKSPACE/docker_build_and_install',
        'rm -fr $WORKSPACE/docker_build_and_test',
        'mkdir -p $WORKSPACE/docker_create_workspace',
        'mkdir -p $WORKSPACE/docker_build_and_install',
        'mkdir -p $WORKSPACE/docker_build_and_test',
        'docker run' +
        ' --rm ' +
        ' --cidfile=$WORKSPACE/docker_generating_dockers/docker.cid' +
        ' -e=HOME=/home/buildfarm' +
        ' -v $WORKSPACE/ros_buildfarm:/tmp/ros_buildfarm:ro' +
        ' -v $WORKSPACE/docker_create_workspace:/tmp/docker_create_workspace' +
        ' -v $WORKSPACE/docker_build_and_install:/tmp/docker_build_and_install' +
        ' -v $WORKSPACE/docker_build_and_test:/tmp/docker_build_and_test' +
        ' $DOCKER_IMAGE_PREFIX.ci_task_generation.%s' % (rosdistro_name),
        'cd -',  # restore pwd when used in scripts
        'echo "# END SECTION"',
    ]),
))@
@(SNIPPET(
    'builder_shell',
    script='\n'.join([
        '# monitor all subprocesses and enforce termination',
        'python3 -u $WORKSPACE/ros_buildfarm/scripts/subprocess_reaper.py $$ --cid-file $WORKSPACE/docker_create_workspace/docker.cid > $WORKSPACE/docker_create_workspace/subprocess_reaper.log 2>&1 &',
        '# sleep to give python time to startup',
        'sleep 1',
        '',
        '# create a unique dockerfile name prefix',
        'export DOCKER_IMAGE_PREFIX=$(date +%s.%N)',
        '',
        'echo "# BEGIN SECTION: Build Dockerfile - create workspace"',
        '# build and run create_workspace Dockerfile',
        'cd $WORKSPACE/docker_create_workspace',
        'python3 -u $WORKSPACE/ros_buildfarm/scripts/misc/docker_pull_baseimage.py',
        'docker build --force-rm -t $DOCKER_IMAGE_PREFIX.ci_create_workspace.%s .' % (rosdistro_name),
        'echo "# END SECTION"',
        '',
        'echo "# BEGIN SECTION: Run Dockerfile - create workspace"',
    ] + [
        'export UNDERLAY%d_JOB_SPACE=$WORKSPACE/underlay%d/ros%d-linux' % (i + 1, i + 1, local_ros_version)
        for i, local_ros_version in zip(range(len(underlay_source_jobs)), [ros_version] * len(underlay_source_jobs))
    ] + [
        'rm -fr $WORKSPACE/ws/src',
        'mkdir -p $WORKSPACE/ws/src',
    ] + [
        'mkdir -p %s' % (dir) for dir in underlay_source_paths
    ] + [
        'docker run' +
        ' --rm ' +
        ' --cidfile=$WORKSPACE/docker_create_workspace/docker.cid' +
        ' -v $WORKSPACE/ros_buildfarm:/tmp/ros_buildfarm:ro' +
        ''.join([
            ' -v %s:/tmp/ws%s/install_isolated:ro' % (space, i if i > 1 else '')
            for i, space in enumerate(underlay_source_paths, 1)
        ]) +
        ' -v $WORKSPACE/ws:/tmp/ws%s' % (len(underlay_source_paths) + 1 if len(underlay_source_paths) else '') +
        ' $DOCKER_IMAGE_PREFIX.ci_create_workspace.%s' % (rosdistro_name),
        'cd -',  # restore pwd when used in scripts
        'echo "# END SECTION"',
    ]),
))@
@(SNIPPET(
    'builder_shell',
    script='\n'.join([
        'echo "# BEGIN SECTION: Copy dependency list"',
        '/bin/cp -f $WORKSPACE/ws/install_list_build.txt $WORKSPACE/docker_build_and_install/install_list_build.txt',
        '/bin/cp -f $WORKSPACE/ws/install_list_build.txt $WORKSPACE/docker_build_and_test/install_list_build.txt',
        '/bin/cp -f $WORKSPACE/ws/install_list_test.txt $WORKSPACE/docker_build_and_test/install_list_test.txt',
        'echo "# END SECTION"',
    ]),
))@
@(SNIPPET(
    'builder_shell',
    script='\n'.join([
        '# monitor all subprocesses and enforce termination',
        'python3 -u $WORKSPACE/ros_buildfarm/scripts/subprocess_reaper.py $$ --cid-file $WORKSPACE/docker_build_and_install/docker.cid > $WORKSPACE/docker_build_and_install/subprocess_reaper.log 2>&1 &',
        '# sleep to give python time to startup',
        'sleep 1',
        '',
        '# create a unique dockerfile name prefix',
        'export DOCKER_IMAGE_PREFIX=$(date +%s.%N)',
        '',
        'echo "# BEGIN SECTION: Build Dockerfile - build and install"',
        '# build and run build and install Dockerfile',
        'cd $WORKSPACE/docker_build_and_install',
        'python3 -u $WORKSPACE/ros_buildfarm/scripts/misc/docker_pull_baseimage.py',
        'docker build --force-rm -t $DOCKER_IMAGE_PREFIX.ci_build_and_install.%s .' % (rosdistro_name),
        'echo "# END SECTION"',
        '',
        'echo "# BEGIN SECTION: ccache stats (before)"',
        'mkdir -p $HOME/.ccache',
        'docker run' +
        ' --rm ' +
        ' --cidfile=$WORKSPACE/docker_build_and_install/docker_ccache_before.cid' +
        ' -e CCACHE_DIR=/home/buildfarm/.ccache' +
        ' -v $HOME/.ccache:/home/buildfarm/.ccache' +
        ' $DOCKER_IMAGE_PREFIX.ci_build_and_install.%s' % (rosdistro_name) +
        ' "ccache -s"',
        'echo "# END SECTION"',
        '',
        'echo "# BEGIN SECTION: Run Dockerfile - build and install"',
    ] + [
        'export UNDERLAY%d_JOB_SPACE=$WORKSPACE/underlay%d/ros%d-linux' % (i + 1, i + 1, local_ros_version)
        for i, local_ros_version in zip(range(len(underlay_source_jobs)), [ros_version] * len(underlay_source_jobs))
    ] + [
        'docker run' +
        ' --rm ' +
        ' --cidfile=$WORKSPACE/docker_build_and_install/docker.cid' +
        ' -e CCACHE_DIR=/home/buildfarm/.ccache' +
        ' -v $HOME/.ccache:/home/buildfarm/.ccache' +
        ' -v $WORKSPACE/ros_buildfarm:/tmp/ros_buildfarm:ro' +
        ''.join([
            ' -v %s:/tmp/ws%s/install_isolated:ro' % (space, i if i > 1 else '')
            for i, space in enumerate(underlay_source_paths, 1)
        ]) +
        ' -v $WORKSPACE/ws:/tmp/ws%s' % (len(underlay_source_paths) + 1 if len(underlay_source_paths) else '') +
        ' $DOCKER_IMAGE_PREFIX.ci_build_and_install.%s' % (rosdistro_name),
        'cd -',  # restore pwd when used in scripts
        'echo "# END SECTION"',
        '',
        'echo "# BEGIN SECTION: ccache stats (after)"',
        'docker run' +
        ' --rm ' +
        ' --cidfile=$WORKSPACE/docker_build_and_install/docker_ccache_after.cid' +
        ' -e CCACHE_DIR=/home/buildfarm/.ccache' +
        ' -v $HOME/.ccache:/home/buildfarm/.ccache' +
        ' $DOCKER_IMAGE_PREFIX.ci_build_and_install.%s' % (rosdistro_name) +
        ' "ccache -s"',
        'echo "# END SECTION"',
    ]),
))@
@(SNIPPET(
    'builder_shell',
    script='\n'.join([
        'echo "# BEGIN SECTION: Compress install space"',
        'tar -cjf $WORKSPACE/ros%d-%s-linux-%s-%s-ci.tar.bz2 ' % (ros_version, rosdistro_name, os_code_name, arch) +
        ' -C $WORKSPACE/ws' +
        ' --transform "s/^install_isolated/ros%d-linux/"' % (ros_version) +
        ' install_isolated',
        'echo "# END SECTION"',
    ]),
))@
@(SNIPPET(
    'builder_shell',
    script='\n'.join([
        '# monitor all subprocesses and enforce termination',
        'python3 -u $WORKSPACE/ros_buildfarm/scripts/subprocess_reaper.py $$ --cid-file $WORKSPACE/docker_build_and_test/docker.cid > $WORKSPACE/docker_build_and_test/subprocess_reaper.log 2>&1 &',
        '# sleep to give python time to startup',
        'sleep 1',
        '',
        '# create a unique dockerfile name prefix',
        'export DOCKER_IMAGE_PREFIX=$(date +%s.%N)',
        '',
        'echo "# BEGIN SECTION: Build Dockerfile - build and test"',
        '# build and run build and test Dockerfile',
        'cd $WORKSPACE/docker_build_and_test',
        'python3 -u $WORKSPACE/ros_buildfarm/scripts/misc/docker_pull_baseimage.py',
        'docker build --force-rm -t $DOCKER_IMAGE_PREFIX.ci_build_and_test.%s .' % (rosdistro_name),
        'echo "# END SECTION"',
        '',
        'echo "# BEGIN SECTION: ccache stats (before)"',
        'mkdir -p $HOME/.ccache',
        'docker run' +
        ' --rm ' +
        ' --cidfile=$WORKSPACE/docker_build_and_test/docker_ccache_before.cid' +
        ' -e CCACHE_DIR=/home/buildfarm/.ccache' +
        ' -v $HOME/.ccache:/home/buildfarm/.ccache' +
        ' $DOCKER_IMAGE_PREFIX.ci_build_and_test.%s' % (rosdistro_name) +
        ' "ccache -s"',
        'echo "# END SECTION"',
        '',
        'echo "# BEGIN SECTION: Run Dockerfile - build and test"',
    ] + [
        'export UNDERLAY%d_JOB_SPACE=$WORKSPACE/underlay%d/ros%d-linux' % (i + 1, i + 1, local_ros_version)
        for i, local_ros_version in zip(range(len(underlay_source_jobs)), [ros_version] * len(underlay_source_jobs))
    ] + [
        'rm -fr $WORKSPACE/ws/test_results',
        'mkdir -p $WORKSPACE/ws/test_results',
        'docker run' +
        ' --rm ' +
        ' --cidfile=$WORKSPACE/docker_build_and_test/docker.cid' +
        ' -e CCACHE_DIR=/home/buildfarm/.ccache' +
        ' -v $HOME/.ccache:/home/buildfarm/.ccache' +
        ' -v $WORKSPACE/ros_buildfarm:/tmp/ros_buildfarm:ro' +
        ''.join([
            ' -v %s:/tmp/ws%s/install_isolated:ro' % (space, i if i > 1 else '')
            for i, space in enumerate(underlay_source_paths, 1)
        ]) +
        ' -v $WORKSPACE/ws:/tmp/ws%s' % (len(underlay_source_paths) + 1 if len(underlay_source_paths) else '') +
        ' $DOCKER_IMAGE_PREFIX.ci_build_and_test.%s' % (rosdistro_name),
        'cd -',  # restore pwd when used in scripts
        'echo "# END SECTION"',
        '',
        'echo "# BEGIN SECTION: ccache stats (after)"',
        'docker run' +
        ' --rm ' +
        ' --cidfile=$WORKSPACE/docker_build_and_test/docker_ccache_after.cid' +
        ' -e CCACHE_DIR=/home/buildfarm/.ccache' +
        ' -v $HOME/.ccache:/home/buildfarm/.ccache' +
        ' $DOCKER_IMAGE_PREFIX.ci_build_and_test.%s' % (rosdistro_name) +
        ' "ccache -s"',
        'echo "# END SECTION"',
    ]),
))@
@(SNIPPET(
    'builder_shell',
    script='\n'.join([
        'if [ "$skip_cleanup" = "false" ]; then',
        'echo "# BEGIN SECTION: Clean up to save disk space on agents"',
        'rm -fr ws/build_isolated',
        'rm -fr ws/devel_isolated',
        'rm -fr ws/install_isolated',
        'echo "# END SECTION"',
        'fi',
    ]),
))@
  </builders>
  <publishers>
@(SNIPPET(
    'publisher_warnings',
    unstable_threshold='',
))@
@(SNIPPET(
    'archive_artifacts',
    artifacts=[
      'ros%d-%s-linux-%s-%s-ci.tar.bz2' % (ros_version, rosdistro_name, os_code_name, arch),
    ] + [image for images in show_images.values() for image in images],
))@
@[for title, artifacts in show_images.items()]@
@(SNIPPET(
    'image_gallery',
    title=title,
    image_width='',
    artifacts=artifacts,
))@
@[end for]@
@[if show_plots]@
@(SNIPPET(
    'plot_plugin',
    plots=show_plots,
))@
@[end if]@
@[if xunit_publisher_types]@
@(SNIPPET(
    'publisher_xunit',
    types=xunit_publisher_types,
))@
@[end if]@
  </publishers>
  <buildWrappers>
@[if timeout_minutes is not None]@
@(SNIPPET(
    'build-wrapper_build-timeout',
    timeout_minutes=timeout_minutes,
))@
@[end if]@
@(SNIPPET(
    'build-wrapper_timestamper',
))@
  </buildWrappers>
</project>


Improvements for std.core.pretty courtesy of Ben Christel.

Accept a hash of options as an optional second parameter, toggling
options such as quoting keys and values and using array notation.

/*  Sirikata
 *  pretty.js
 *
 *  Copyright (c) 2011, Ewen Cheslack-Postava
 *  All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions are
 *  met:
 *  * Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *  * Neither the name of Sirikata nor the names of its contributors may
 *    be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

if (typeof(std) === "undefined") /**@namespace */ std = {};
if (typeof(std.core) === "undefined") /** @namespace */ std.core = {};

system.require('std/escape.em');

std.core.isArray = function(obj) {
    return (obj.constructor.toString().indexOf("Array") != -1);
};

/**
 * Converts an object to a string in a 'pretty' format, i.e. so it is
 *  human-readable, multiple lines, and handles indentation. If the
 *  object is not a tree (i.e. it has circular references in it), then
 *  the normal toString version will be returned.  Generally this
 *  should only be used on small, tree-like objects.
 */
std.core.pretty = function(obj, options) {
    if (typeof(options) !== 'object' || options == null) options = {};

    var valuesInQuotes = options.valuesInQuotes ? true : false;
    var keysInQuotes = options.keysInQuotes ? true : false;
    var arrayNotation = options.arrayNotation ? true : false;

    var visited = [];

    // Fast path -- if its not an object or array, we can just do the normal conversion
    if (typeof(obj) === "string" && valuesInQuotes)
        return Escape.escapeString(obj);
    if (typeof(obj) !== "object" || obj === null)
        return '' + obj;

    var objectPrettyOverride = function(obj) {
        // If the object overrides pretty printing, just hand back whatever it wants.
        if (typeof(obj.__prettyPrintString__) === "string")
            return obj.__prettyPrintString__;
        else if (typeof(obj.__prettyPrintString__) === "function")
            return obj.__prettyPrintString__();
        else
            return undefined;
    };

    {
        var override = objectPrettyOverride(obj);
        if (override) return override;
    }

    var objectFields = function(obj) {
        if (typeof(obj.__prettyPrintFields__) == "function")
            return obj.__prettyPrintFields__();
        else
            return Object.getOwnPropertyNames(obj);
    };
    var longObject = function(obj) {
        var fields = objectFields(obj);
        if (fields.length > 3)
            return true;

        for(var f in fields)
            if(typeof(obj[fields[f]]) === 'object')
                return true;

        return false;
    };

    var checkVisited = function  (obj)
    {
        for (var s in visited)
        {
            if (visited[s][0] === obj)
            {
                return s;
            }
        }
        return null;
    };
    var shouldPrintAsArray = function (obj) {
        return arrayNotation && std.core.isArray(obj);
    };

    var output = '';
    var obj_stack = [ {obj: obj, idx: -1} ];
    var indent = '';
    var numPrint = 1;
    while(obj_stack.length != 0) {

        var cur = obj_stack.pop();

        visited.push([cur.obj,numPrint,output.length]);
        ++numPrint;

        // Check if we need to start this object
        if (cur.idx == -1) {
            // Start the object
            if (shouldPrintAsArray(cur.obj)) {
                output += '[';
            } else {
                output += '{';
            }
            indent += ' ';
            obj_stack.push( {obj: cur.obj, idx: cur.idx+1} );
        }
        else if ((shouldPrintAsArray(cur.obj) && cur.idx < cur.obj.length) ||
                (!shouldPrintAsArray(cur.obj) && cur.idx < objectFields(cur.obj).length)) {
            // Setup processing of next child

            // Add a comma and either space or newline as appropriate
            if (cur.idx != 0)
                output += ',';
            if (longObject(cur.obj))
                output += '\n' + indent;
            else
                output += ' ';

            obj_stack.push( {obj: cur.obj, idx: cur.idx+1} );
            // And process this one, possibly triggering recursion

            var key;
            if (shouldPrintAsArray(cur.obj)) {
                key = cur.idx;
            } else {
                key = objectFields(cur.obj)[cur.idx];
            }

            var child = cur.obj[key];

            if (!shouldPrintAsArray(cur.obj)) {
                var displaykey = keysInQuotes ? Escape.escapeString(key) : key;
                output += displaykey + ': ';
            }

            if (typeof(child) === "object" && child !== null) {

                //check if we've arleady visited this object.
                var visitedIndex = checkVisited(child);
                if (visitedIndex != null)
                {
					var entry = visited[visitedIndex];
					if(entry[2] != -1)
					{
						var tag = '<#' + entry[1].toString() + '>';
						output = output.slice(0, entry[2]) + tag +
								output.slice(entry[2]);
						for(var i in visited)
							if(visited[i][2] > entry[2])
								visited[i][2] += tag.length;
						visited[visitedIndex][2] = -1;
					}

					output += '<#'+ entry[1].toString() + '>';
                    continue;
                }

                var override = objectPrettyOverride(child);
                if (override)
                    output += override;
                else
                    obj_stack.push( {obj: child, idx: -1} );
            }
            else
            {
                if (valuesInQuotes && typeof(child) === 'string')
                    output += Escape.escapeString(child);
                else
                    output += child;
            }

        }
        else {
            // Close this object
            indent = indent.substr(1);

            if (longObject(cur.obj))
                output += '\n' + indent;
            else
                output += ' ';
            if (shouldPrintAsArray(cur.obj)) {
                output += ']';
            } else {
                output += '}';
            }

            // No object stack push, we've finished with this object
        }
    }

    return output;
};

std.core.quotepretty = function(obj) {
    return std.core.pretty(obj,
        {
        valuesInQuotes: true,
        keysInQuotes: true
        }
    );
};

std.core.serialize = function(obj) {
    return std.core.pretty(obj,
        {
        valuesInQuotes: true,
        keysInQuotes: true,
        arrayNotation: true
        }
    );
};

/*  Sirikata
 *  pretty.js
 *
 *  Copyright (c) 2011, Ewen Cheslack-Postava
 *  All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions are
 *  met:
 *  * Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *  * Neither the name of Sirikata nor the names of its contributors may
 *    be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

if (typeof(std) === "undefined") /**@namespace */ std = {};
if (typeof(std.core) === "undefined") /** @namespace */ std.core = {};

system.require('std/escape.em');

/**
 * Converts an object to a string in a 'pretty' format, i.e. so it is
 *  human-readable, multiple lines, and handles indentation. If the
 *  object is not a tree (i.e. it has circular references in it), then
 *  the normal toString version will be returned.  Generally this
 *  should only be used on small, tree-like objects.
 */
std.core.pretty = function(obj,displayQuotes,keysInQuotes) {

    var visited = [];

    // Fast path -- if its not an object or array, we can just do the normal conversion
    if (typeof(obj) !== "object" || obj === null)
        return '' + obj;

    var objectPrettyOverride = function(obj) {
        // If the object overrides pretty printing, just hand back whatever it wants.
        if (typeof(obj.__prettyPrintString__) === "string")
            return obj.__prettyPrintString__;
        else if (typeof(obj.__prettyPrintString__) === "function")
            return obj.__prettyPrintString__();
        else
            return undefined;
    };

    {
        var override = objectPrettyOverride(obj);
        if (override) return override;
    }

    var objectFields = function(obj) {
        if (typeof(obj.__prettyPrintFields__) == "function")
            return obj.__prettyPrintFields__();
        else
            return Object.getOwnPropertyNames(obj);
    };
    var longObject = function(obj) {
        var fields = objectFields(obj);
        if (fields.length > 3)
            return true;

        for(var f in fields)
            if(typeof(obj[fields[f]]) === 'object')
                return true;

        return false;
    };

    var checkVisited = function  (obj)
    {
        for (var s in visited)
        {
            if (visited[s][0] === obj)
            {
                return s;
            }
        }
        return null;
    };

    var output = '';
    var obj_stack = [ {obj: obj, idx: -1} ];
    var indent = '';
    var numPrint = 1;
    while(obj_stack.length != 0) {

        var cur = obj_stack.pop();

        visited.push([cur.obj,numPrint,output.length]);
        ++numPrint;

        // Check if we need to start this object
        if (cur.idx == -1) {
            // Start the object
            output += '{';
            indent += ' ';
            obj_stack.push( {obj: cur.obj, idx: cur.idx+1} );
        }
        else if (cur.idx < objectFields(cur.obj).length) {
            // Setup processing of next child

            // Add a comma and either space or newline as appropriate
            if (cur.idx != 0)
                output += ',';
            if (longObject(cur.obj))
                output += '\n' + indent;
            else
                output += ' ';

            obj_stack.push( {obj: cur.obj, idx: cur.idx+1} );
            // And process this one, possibly triggering recursion
            var key = objectFields(cur.obj)[cur.idx];

            var child = cur.obj[key];
            var displaykey = keysInQuotes ? Escape.escapeString(key) : key;
            output += displaykey + ': ';

            if (typeof(child) === "object" && child !== null) {

                //check if we've arleady visited this object.
                var visitedIndex = checkVisited(child);
                if (visitedIndex != null)
                {
					var entry = visited[visitedIndex];
					if(entry[2] != -1)
					{
						var tag = '<#' + entry[1].toString() + '>';
						output = output.slice(0, entry[2]) + tag +
								output.slice(entry[2]);
						for(var i in visited)
							if(visited[i][2] > entry[2])
								visited[i][2] += tag.length;
						visited[visitedIndex][2] = -1;
					}

					output += '<#'+ entry[1].toString() + '>';
                    continue;
                }

                var override = objectPrettyOverride(child);
                if (override)
                    output += override;
                else
                    obj_stack.push( {obj: child, idx: -1} );
            }
            else
            {
                if (displayQuotes && typeof(child) === 'string')
                    output += Escape.escapeString(child);
                else
                    output += child;
            }

        }
        else {
            // Close this object
            indent = indent.substr(1);

            if (longObject(cur.obj))
                output += '\n' + indent;
            else
                output += ' ';

            output += '}';

            // No object stack push, we've finished with this object
        }
    }

    return output;
};

std.core.quotepretty = function(obj) {
    return std.core.pretty(obj, true, true);
};


Restore space in self-closing tags (#701)

This will match the changes that the Jenkins UI makes to the job, so our diffs are clean later.

    <hudson.plugins.plot.PlotPublisher plugin="plot@@2.1.3">
      <plots>
@[for plot_group, plot_list in plots.items()]@
@[for plot in plot_list]@
        <hudson.plugins.plot.Plot>
          <title>@(plot.title)</title>
          <yaxis>@(plot.y_axis_label)</yaxis>
          <series>
@[for series in plot.data_series]@
            <hudson.plugins.plot.CSVSeries>
              <file>@(series.data_file)</file>
              <label />
              <fileType>@(series.data_type)</fileType>
              <colExclusionSet>
                <int>@(series.selection_value)</int>
              </colExclusionSet>
              <inclusionFlag>@(series.selection_flag)</inclusionFlag>
              <exclusionValues>@(series.selection_value)</exclusionValues>
              <exclusionValuesList class="java.util.Arrays$ArrayList">
                <a class="string-array">
                  <string>@(series.selection_value)</string>
                </a>
              </exclusionValuesList>
              <url>@(series.url)</url>
              <displayTableFlag>false</displayTableFlag>
            </hudson.plugins.plot.CSVSeries>
@[end for]@
          </series>
          <group>@(plot_group)</group>
          <numBuilds />
          <csvFileName>@(plot.master_csv_name)</csvFileName>
          <csvLastModification>0</csvLastModification>
          <style>@(plot.style)</style>
          <useDescr>false</useDescr>
          <keepRecords>false</keepRecords>
          <exclZero>@('true' if plot.y_axis_exclude_zero else 'false')</exclZero>
          <logarithmic>false</logarithmic>
@[if plot.y_axis_minimum is not None]@
          <yaxisMinimum>@(plot.y_axis_minimum)</yaxisMinimum>
@[else]@
          <yaxisMinimum />
@[end if]@
@[if plot.y_axis_maximum is not None]@
          <yaxisMaximum>@(plot.y_axis_maximum)</yaxisMaximum>
@[else]@
          <yaxisMaximum />
@[end if]@
        </hudson.plugins.plot.Plot>
@[end for]@
@[end for]@
      </plots>
    </hudson.plugins.plot.PlotPublisher>

    <hudson.plugins.plot.PlotPublisher plugin="plot@@2.1.3">
      <plots>
@[for plot_group, plot_list in plots.items()]@
@[for plot in plot_list]@
        <hudson.plugins.plot.Plot>
          <title>@(plot.title)</title>
          <yaxis>@(plot.y_axis_label)</yaxis>
          <series>
@[for series in plot.data_series]@
            <hudson.plugins.plot.CSVSeries>
              <file>@(series.data_file)</file>
              <label />
              <fileType>@(series.data_type)</fileType>
              <colExclusionSet>
                <int>@(series.selection_value)</int>
              </colExclusionSet>
              <inclusionFlag>@(series.selection_flag)</inclusionFlag>
              <exclusionValues>@(series.selection_value)</exclusionValues>
              <exclusionValuesList class="java.util.Arrays$ArrayList">
                <a class="string-array">
                  <string>@(series.selection_value)</string>
                </a>
              </exclusionValuesList>
              <url>@(series.url)</url>
              <displayTableFlag>false</displayTableFlag>
            </hudson.plugins.plot.CSVSeries>
@[end for]@
          </series>
          <group>@(plot_group)</group>
          <numBuilds />
          <csvFileName>@(plot.master_csv_name)</csvFileName>
          <csvLastModification>0</csvLastModification>
          <style>@(plot.style)</style>
          <useDescr>false</useDescr>
          <keepRecords>false</keepRecords>
          <exclZero>@('true' if plot.y_axis_exclude_zero else 'false')</exclZero>
          <logarithmic>false</logarithmic>
@[if plot.y_axis_minimum is not None]@
          <yaxisMinimum>@(plot.y_axis_minimum)</yaxisMinimum>
@[else]@
          <yaxisMinimum/>
@[end if]@
@[if plot.y_axis_maximum is not None]@
          <yaxisMaximum>@(plot.y_axis_maximum)</yaxisMaximum>
@[else]@
          <yaxisMaximum/>
@[end if]@
        </hudson.plugins.plot.Plot>
@[end for]@
@[end for]@
      </plots>
    </hudson.plugins.plot.PlotPublisher>


Expose callbacks for simpleStorage.write and simpleStorage.erase.

if (typeof(std) == 'undefined')
    std = {};

if (typeof(std.simpleStorage) != 'undefined')
    throw new Error('Error in simple storage.  Should only import once.');

(function()
{
    system.require('std/shim/restore/persistService.em');
    std.simpleStorage = {  };

    var allKeysKeyname = 'allKeys';
    var prepender      = 'toPrependWith_';

    var scriptKeyName =   'pScriptKeyName';

    //contains the script to execute on restore in its script field.
    var mScript = { };

    //query backend with this index to retrieve mPres object, which lists
    //all presences that we have.
    var presKeyName   =     'pPresKeyName';
    //prevents more than one presence from being stored at a time because
    //we didn't expose batching erases and sets in the same set unfortunately
    var presOperInProgress = false;
    //last elements of the array contain next elements to be processed
    var queuedPresOpers= [];
    //Keys contain all presence sporefs.  Can use these to restore our presences.
    var mPres   = { };

    std.simpleStorage.setField = function(fieldName,fieldVal,cb)
    {
        var keyName = prepender + fieldName;
        std.persist.checkpointPartialPersist(fieldVal,keyName,cb);
    };
    std.simpleStorage.write = std.simpleStorage.setField;

    std.simpleStorage.eraseField = function(fieldName, cb)
    {
        system.storageErase(prepender + fieldName, cb);
    };
    std.simpleStorage.erase = std.simpleStorage.eraseField;

    std.simpleStorage.readField = function(fieldName,cb,def)
    {
        std.persist.restoreFromAsync(prepender + fieldName,
                                     function(success,val)
                                     {
                                         if (success)
                                             cb(val);
                                         else
                                             cb(def);
                                     });
    };
    std.simpleStorage.read = std.simpleStorage.readField;

    std.simpleStorage.setPresence = function (presToRestore)
    {
        //cannot process current presence because already performing a
        //presence operation.
        if (presOperInProgress)
        {
            queuedPresOpers.unshift(['add',presToRestore]);
            return;
        }

        //locks other presence operations from occurring.
        presOperInProgress = true;

        //ensures this presence is added to our list of presences
        var presName = presToRestore.toString();
        mPres[presName] = true;

        // //callback to execute if setPresence works correctly.
        // //essentially all it does is that it begins processing
        // //next presence event.
        var finishedCB = function(success)
        {
            if (!success)
                delete mPres[presName];

            presOperInProgress = false;
            processNextPresenceEvent();
        };

        std.persist.persistMany([[mPres,presKeyName],
                                [presToRestore,presName]], finishedCB);

    };
    std.simpleStorage.writePresence = std.simpleStorage.setPresence;

    std.simpleStorage.erasePresence = function (presToErase)
    {
        //cannot process current presence because already performing a
        //presence operation.
        if (presOperInProgress)
        {
            queuedPresOpers.unshift(['remove',presToErase]);
            return;
        }

        //locks other presence operations from occurring.
        presOperInProgress = true;

        var presName = presToRestore.toString();

        //FIXME: currently cannot batch erase and write operations simultaneously.
        //As a result, mPres and actual presences in backend could get out of sync.
        //When this bug is resolved, the following finished cb will look a lot more
        //like the one in the above func.
        var finishedErasedPres = function(success)
        {
            var finishedUpdatedMPres = function(success)
            {
                if (!success)
                {
                    system.print('\nError in simpleStorage.  Could not delete presence correctly\n');
                }
                presOperInProgress = false;
                processNextPresenceEvent();
            };

            if (success)
            {
                delete mPres[presName];
                std.persist.checkpointPartialPersist(mPres,presKeyName);
                return;
            }
            presOperInProgress = false;
            processNextPresenceEvent();
        };

        system.storageErase(presName,finishedErasePres);
    };

    /**
     Pops the next operation to be processed
     */
    function processNextPresenceEvent()
    {
        if (queuedPresOpers.length == 0)
            return;

        var toProcess = queuedPresOpers.pop();

        if (toProcess[0] == 'add')
            std.simpleStorage.setPresence(toProcess[1]);
        else
            std.simpleStorage.removePresence(toProcess[1]);
    }

    std.simpleStorage.setScript = function(newScriptFunc,executeOnSet)
    {
        var newScript =  ("(" + newScriptFunc + ")();");
        if (typeof(executeOnSet) === 'undefined')
            executeOnSet = false;

        mScript.script = newScript;
        system.setRestoreScript('system.require("std/shim/restore/simpleStorage.em");');

        var cbFunc = function(){ };
        if (executeOnSet)
            cbFunc = function(){ system.eval(newScript);  };

        std.persist.checkpointPartialPersist(mScript,scriptKeyName,cbFunc);
    };

    std.simpleStorage.readScript = function(cb,def)
    {
        std.persist.restoreFromAsync(scriptKeyName,
                                     function(success,val)
                                     {
                                         if ((success) && ('script' in val))
                                             cb(val.script);
                                         else
                                             cb(def);
                                     });
    };

    //evaluates the script once it has been restored.
    function onRestoreScript(scriptToEval)
    {
        system.eval(scriptToEval);
    };

    //restores the script
    function finishOnRestorePresences()
    {
        std.simpleStorage.readScript(onRestoreScript,"");
    }

    /**
     Called when we regain our list of presences.
     Initiates restoring each presence separately.
     */
    function onPresKeyRestored(success,allPres)
    {
        if (success)
            mPres = allPres;

        restorePresences();
    }

    //keeps track of all the presences that we have already restored.
    var allRestored = {};

    //called each time
    function restorePresences()
    {

        // Callback & data to handle synchronizing all the parallel restoration + connection requests
        var num_outstanding = 0;
        var finishRestorePresence = function(presName,success,pres) {
            if (!success) {
                throw new Error('Failed to restore or connect presence ' + presName + ". Bailing on restoration -- this object's script will *not* be executed.");
            }

            num_outstanding--;
            if (num_outstanding == 0) {
                finishOnRestorePresences();
            }
        };

        // Fire off requests for any presences not yet connected
        for (var s in mPres) {
            //if we've already restored this presence ignore it.
            if (s in allRestored)
                continue;
            num_outstanding++;
            allRestored[s] = true;
            std.persist.restoreFromAsync(s,std.core.bind(finishRestorePresence,undefined,s));
        }

        // Special case: no presences to restore.
        if (num_outstanding == 0) finishOnRestorePresences();
    }

    std.persist.restoreFromAsync(presKeyName,onPresKeyRestored);

})();

if (typeof(std) == 'undefined')
    std = {};

if (typeof(std.simpleStorage) != 'undefined')
    throw new Error('Error in simple storage.  Should only import once.');

(function()
{
    system.require('std/shim/restore/persistService.em');
    std.simpleStorage = {  };

    var allKeysKeyname = 'allKeys';
    var prepender      = 'toPrependWith_';

    var scriptKeyName =   'pScriptKeyName';

    //contains the script to execute on restore in its script field.
    var mScript = { };

    //query backend with this index to retrieve mPres object, which lists
    //all presences that we have.
    var presKeyName   =     'pPresKeyName';
    //prevents more than one presence from being stored at a time because
    //we didn't expose batching erases and sets in the same set unfortunately
    var presOperInProgress = false;
    //last elements of the array contain next elements to be processed
    var queuedPresOpers= [];
    //Keys contain all presence sporefs.  Can use these to restore our presences.
    var mPres   = { };

    std.simpleStorage.setField = function(fieldName,fieldVal)
    {
        var keyName = prepender + fieldName;
        std.persist.checkpointPartialPersist(fieldVal,keyName);
    };
    std.simpleStorage.write = std.simpleStorage.setField;

    std.simpleStorage.eraseField = function(fieldName)
    {
        system.storageErase(prepender + fieldName);
    };
    std.simpleStorage.erase = std.simpleStorage.eraseField;

    std.simpleStorage.readField = function(fieldName,cb,def)
    {
        std.persist.restoreFromAsync(prepender + fieldName,
                                     function(success,val)
                                     {
                                         if (success)
                                             cb(val);
                                         else
                                             cb(def);
                                     });
    };
    std.simpleStorage.read = std.simpleStorage.readField;

    std.simpleStorage.setPresence = function (presToRestore)
    {
        //cannot process current presence because already performing a
        //presence operation.
        if (presOperInProgress)
        {
            queuedPresOpers.unshift(['add',presToRestore]);
            return;
        }

        //locks other presence operations from occurring.
        presOperInProgress = true;

        //ensures this presence is added to our list of presences
        var presName = presToRestore.toString();
        mPres[presName] = true;

        // //callback to execute if setPresence works correctly.
        // //essentially all it does is that it begins processing
        // //next presence event.
        var finishedCB = function(success)
        {
            if (!success)
                delete mPres[presName];

            presOperInProgress = false;
            processNextPresenceEvent();
        };

        std.persist.persistMany([[mPres,presKeyName],
                                [presToRestore,presName]], finishedCB);

    };
    std.simpleStorage.writePresence = std.simpleStorage.setPresence;

    std.simpleStorage.erasePresence = function (presToErase)
    {
        //cannot process current presence because already performing a
        //presence operation.
        if (presOperInProgress)
        {
            queuedPresOpers.unshift(['remove',presToErase]);
            return;
        }

        //locks other presence operations from occurring.
        presOperInProgress = true;

        var presName = presToRestore.toString();

        //FIXME: currently cannot batch erase and write operations simultaneously.
        //As a result, mPres and actual presences in backend could get out of sync.
        //When this bug is resolved, the following finished cb will look a lot more
        //like the one in the above func.
        var finishedErasedPres = function(success)
        {
            var finishedUpdatedMPres = function(success)
            {
                if (!success)
                {
                    system.print('\nError in simpleStorage.  Could not delete presence correctly\n');
                }
                presOperInProgress = false;
                processNextPresenceEvent();
            };

            if (success)
            {
                delete mPres[presName];
                std.persist.checkpointPartialPersist(mPres,presKeyName);
                return;
            }
            presOperInProgress = false;
            processNextPresenceEvent();
        };

        system.storageErase(presName,finishedErasePres);
    };

    /**
     Pops the next operation to be processed
     */
    function processNextPresenceEvent()
    {
        if (queuedPresOpers.length == 0)
            return;

        var toProcess = queuedPresOpers.pop();

        if (toProcess[0] == 'add')
            std.simpleStorage.setPresence(toProcess[1]);
        else
            std.simpleStorage.removePresence(toProcess[1]);
    }

    std.simpleStorage.setScript = function(newScriptFunc,executeOnSet)
    {
        var newScript =  ("(" + newScriptFunc + ")();");
        if (typeof(executeOnSet) === 'undefined')
            executeOnSet = false;

        mScript.script = newScript;
        system.setRestoreScript('system.require("std/shim/restore/simpleStorage.em");');

        var cbFunc = function(){ };
        if (executeOnSet)
            cbFunc = function(){ system.eval(newScript);  };

        std.persist.checkpointPartialPersist(mScript,scriptKeyName,cbFunc);
    };

    std.simpleStorage.readScript = function(cb,def)
    {
        std.persist.restoreFromAsync(scriptKeyName,
                                     function(success,val)
                                     {
                                         if ((success) && ('script' in val))
                                             cb(val.script);
                                         else
                                             cb(def);
                                     });
    };

    //evaluates the script once it has been restored.
    function onRestoreScript(scriptToEval)
    {
        system.eval(scriptToEval);
    };

    //restores the script
    function finishOnRestorePresences()
    {
        std.simpleStorage.readScript(onRestoreScript,"");
    }

    /**
     Called when we regain our list of presences.
     Initiates restoring each presence separately.
     */
    function onPresKeyRestored(success,allPres)
    {
        if (success)
            mPres = allPres;

        restorePresences();
    }

    //keeps track of all the presences that we have already restored.
    var allRestored = {};

    //called each time
    function restorePresences()
    {

        // Callback & data to handle synchronizing all the parallel restoration + connection requests
        var num_outstanding = 0;
        var finishRestorePresence = function(presName,success,pres) {
            if (!success) {
                throw new Error('Failed to restore or connect presence ' + presName + ". Bailing on restoration -- this object's script will *not* be executed.");
            }

            num_outstanding--;
            if (num_outstanding == 0) {
                finishOnRestorePresences();
            }
        };

        // Fire off requests for any presences not yet connected
        for (var s in mPres) {
            //if we've already restored this presence ignore it.
            if (s in allRestored)
                continue;
            num_outstanding++;
            allRestored[s] = true;
            std.persist.restoreFromAsync(s,std.core.bind(finishRestorePresence,undefined,s));
        }

        // Special case: no presences to restore.
        if (num_outstanding == 0) finishOnRestorePresences();
    }

    std.persist.restoreFromAsync(presKeyName,onPresKeyRestored);

})();


Make prelight of toolbar buttons black.

Make prelight of toolbar buttons black.

${
import math

def my_floor(num):
    return int(math.floor(num))

def my_ceil(num):
    return int(math.ceil(num))

# Should we set the line width in the engine to 2.25, and draw non pixel aligned lines?
# Are these already the correct sizes for the XO?

# These sizes need to be sanity checked ...
if theme == "sugar-xo":
    xo = True
    line_width = 2.25       # 2.25
    thick_line_width = 3.5  # 3.5
    subcell_size = 15
    bullet_size = 9.5
    font_height = 24
    default_padding = 6
else: # About 50% smaller
    xo = False
    line_width = 1.125
    thick_line_width = 1.75
    subcell_size = 7
    bullet_size = 5
    font_height = 12
    default_padding = 3

radio_size = my_floor(subcell_size + bullet_size + line_width)
scale_slider_width = my_floor(2 * subcell_size + line_width)
thickness = my_ceil(line_width)

# Colors:

black = '"#000000"'
toolbar_grey = '"#404040"'
button_grey = '"#808080"'
selection_grey = '"#A6A6A6"'
panel_grey = '"#C0C0C0"'
text_field_grey = '"#E5E5E5"'
white = '"#FFFFFF"'

}
style "default"
{
    # Will be overriden in some widgets
    color["focus_line"] = $white

    bg[NORMAL] = $button_grey
    bg[PRELIGHT] = $button_grey
    bg[ACTIVE] = $white
    bg[SELECTED] = $text_field_grey

    fg[NORMAL]        = $black
    fg[ACTIVE]        = $black
    fg[SELECTED]      = $white
    fg[PRELIGHT]      = $white

    text[NORMAL] = $black # button_grey? 1. need to patch GTK+ to use ACTIVE when focused, 2. what color should this really be?
    text[ACTIVE] = $black
    text[SELECTED] = $black
    text[PRELIGHT] = $black

    base[NORMAL] = $text_field_grey
    base[ACTIVE] = $white
    base[SELECTED] = $selection_grey
    base[PRELIGHT] = $white

    fg[INSENSITIVE] = $button_grey
    text[INSENSITIVE] = $button_grey

    xthickness = $thickness
    ythickness = $thickness

    # A lot of these will probably need to be changed, but this has to
    # be done when the exact sizes are known
    GtkWidget::interior-focus = 0
    GtkWidget::focus-line-width = 1   # This does not do anything really
    GtkWidget::focus-padding = 0
    # 0.05 works good for both the sugar and sugar-xo themes
    GtkWidget::cursor-aspect-ratio = 0.05

    GtkWidget::wide-separators = 0
    GtkWidget::separator-height = $thickness
    GtkWidget::separator-width = $thickness

    GtkRange::activate-slider = 1
    GtkButton::inner-border = { 6, 6, 6, 6 }           # ??

    GtkButton::default-border = { 1, 1, 1, 1 }         # line_width - focus-line-width
    GtkButton::default-outside-border = { 2, 2, 2, 2 } # focus-line-width

    GtkScrolledWindow::scrollbar-spacing = 0

    GtkExpander::expander-size = 24
    GtkExpander::expander-spacing = 2     # XXX

    GtkTreeView::expander-size = 24

    GtkArrow::arrow-size = 1.0

    GtkToolbar::shadow-type = GTK_SHADOW_NONE

    GtkProgressBar::min-horizontal-bar-height = $subcell_size
    GtkProgressBar::min-vertical-bar-width = $subcell_size

    engine "sugar" {
        line_width = $line_width
        thick_line_width = $thick_line_width

        # Not sure about this one, but it is not that important
        max_radius = $( 2*subcell_size )
    }
}

#############################################################
# Styles that set default background/foreground colors
#
#  We almost always need two styles because of the X windows.
#  For example bg[NORMAL] of a window (or eventbox) needs to
#  be set to be the correct window color so that X fills the
#  window correctly from the beginning, and it does not
#  flash in the wrong color.
#############################################################

style "window"
{
    bg[NORMAL] = $panel_grey
}

style "window-child"
{
    bg[INSENSITIVE] = $panel_grey
    base[INSENSITIVE] = $panel_grey

    fg[NORMAL] = $black

    engine "sugar" {
        label_fg_color = $black
    }
}

style "groupbox-panel"
{
    bg[NORMAL] = $selection_grey
}

style "groupbox-panel-child"
{
    bg[INSENSITIVE] = $selection_grey
    base[INSENSITIVE] = $selection_grey

    fg[NORMAL] = $white

    engine "sugar" {
        label_fg_color = $white
    }
}

style "groupbox-palette"
{
    bg[NORMAL] = $toolbar_grey
}

style "groupbox-palette-child"
{
    bg[INSENSITIVE] = $toolbar_grey
    base[INSENSITIVE] = $toolbar_grey

    fg[NORMAL] = $white

    engine "sugar" {
        label_fg_color = $white
    }
}

style "menu"
{
    bg[NORMAL] = $black
    bg[ACTIVE] = $button_grey

    GtkMenu::horizontal-padding = 0
    GtkMenu::vertical-padding   = 0

    xthickness = 0
    ythickness = 0
}

# Can this all be moved in the menuitem style?
style "menu-child"
{
    color["bg_color"] = $black

    fg[NORMAL]   = $white
    fg[PRELIGHT] = $white

    # Work around bug #382646
    text[NORMAL]   = $white
    text[PRELIGHT] = $white

    bg[NORMAL] = $button_grey

    bg[INSENSITIVE] = $black
    base[INSENSITIVE] = $black

    engine "sugar" {
        label_fg_color = $white
    }
}

#########################################################
#########################################################

style "scrollbar"
{
    GtkScrollbar::slider-width = 16    # ?
    GtkScrollbar::trough-border = 0
    GtkScrollbar::stepper-spacing = 0
    GtkScrollbar::min-slider-length = 30    # ?
    GtkScrollbar::has-forward-stepper = 0
    GtkScrollbar::has-backward-stepper = 0
    GtkScrollbar::has-secondary-forward-stepper = 0
    GtkScrollbar::has-secondary-backward-stepper = 0

    # Hack for mozilla as it does not obey the above style properties ...
    # Really, mozilla should be fixed ...
    GtkScrollbar::stepper-size = 1
}

# hints for the trough padding hacks
# We cannot go trough GTK+, as we don't want a padding on all sides, but only one
# If we wanted padding on all sides, trough-border would work great.
style "hscrollbar" = "scrollbar"
{
    engine "sugar" {
        hint = "hscrollbar"
        # Rounded, not floating point value. maybe change this?
        scrollbar_border = $thickness
    }
}

style "vscrollbar" = "scrollbar"
{
    engine "sugar" {
        hint = "vscrollbar"
        scrollbar_border = $thickness
    }
}

style "scale"
{
    GtkWidget::focus-line-width = 0

    bg[NORMAL] = "#808080"

    # "square" slider (really round of course)
    # Same as indicator-size?
    GtkRange::slider-width = $scale_slider_width
    GtkScale::slider-length = $scale_slider_width
    GtkRange::trough-border = 0
    GtkRange::stepper-spacing = 0
    GtkRange::trough-side-details = 1
}

# hints for the trough sizing hacks
style "hscale" = "scale"
{
    engine "sugar" {
        hint = "hscale"
    }
}

style "vscale" = "scale"
{
    engine "sugar" {
        hint = "vscale"
    }
}

style "spinbutton"
{
    bg[NORMAL]  = $button_grey
    bg[ACTIVE]  = $white
    fg[NORMAL]  = $white
    fg[ACTIVE]  = $black

    engine "sugar" {
        hint = "spinbutton"
    }
}

style "comboboxentry"
{
    # Copied from the "default" style. Part of the workaround for bug #382646.
    text[NORMAL] = $black
    text[ACTIVE] = $black
    text[SELECTED] = $black
    text[PRELIGHT] = $black

    engine "sugar" {
        hint = "comboboxentry"
    }
}

style "frame"
{
    bg[NORMAL] = $toolbar_grey
}

style "notebook"
{
    GtkNotebook::tab-overlap = -2

    xthickness = 0
    ythickness = 0

    bg[ACTIVE] = $button_grey
}

style "toolbutton"
{
    color["focus_line"] = $black

    bg[PRELIGHT] = $black
    bg[ACTIVE] = $button_grey
    bg[NORMAL] = $black

    fg[NORMAL] = $white
    fg[ACTIVE] = $white
    fg[PRELIGHT] = $white

    engine "sugar" {
        max_radius = 5.0
	fake_padding = $default_padding
    }
}

style "toolbox"
{
    fg[NORMAL] = $white
    fg[ACTIVE] = $white

    bg[NORMAL] = $toolbar_grey
    bg[INSENSITIVE] = $toolbar_grey
    base[INSENSITIVE] = $toolbar_grey
}

style "panel"
{
    bg[NORMAL] = $panel_grey
    fg[NORMAL] = $black
}

style "entry"
{
    # small inner border and a large x/ythickness for entries
    # to reduce the number of hacks needed :-)
    xthickness = $(thickness*3)
    ythickness = $(thickness*3)

    # This tries to get a height of exactly 45 pixel for the entry.
    GtkEntry::inner-border = { $(max(subcell_size - thickness*3, 0)), $(max(subcell_size - thickness*3, 0)),
                               $(max(my_floor((3*subcell_size - font_height - thickness*3*2)/2),0)), $(max(my_ceil((3*subcell_size - font_height - thickness*3*2)/2), 0)) }

    GtkWidget::focus-line-width = 0
}

style "button"
{
    fg[NORMAL]  = $white

    bg[NORMAL] = $button_grey
}

style "combobox"
{
    # Work around GTK+ bug #382646
    text[NORMAL]      = $white
    text[ACTIVE]      = $white
    text[PRELIGHT]    = $white
}

style "checkbutton"
{
    base[NORMAL]      = $white
    base[PRELIGHT]    = $white
    base[ACTIVE]      = $text_field_grey

    text[PRELIGHT]    = $toolbar_grey
    text[NORMAL]      = $toolbar_grey
    text[ACTIVE]      = $toolbar_grey

    # I won't get the exact sizing, but this should be pretty close
    GtkCheckButton::indicator-size = $radio_size
    GtkCheckButton::indicator-spacing = 3
    GtkWidget::focus-padding = 3

    engine "sugar" {
        # Reset the colors to the normal color again
        # as they were overriden by the button style
        fg[NORMAL]        = label_fg_color
        fg[PRELIGHT]      = label_fg_color
        fg[ACTIVE]        = label_fg_color
    }
}

style "progressbar"
{
    xthickness = 0
    ythickness = 0
}

style "menuitem"
{
    GtkCheckMenuItem::indicator-size = $radio_size

    bg[PRELIGHT] = $button_grey

    text[PRELIGHT]    = $white
    text[NORMAL]      = $white
    text[ACTIVE]      = $white

    GtkMenuItem::horizontal-padding = $line_width
    xthickness = $(my_ceil(line_width * 2))
    ythickness = $thickness
}

style "checkmenuitem"
{
    # This style is only there because of bug #382646 ...
    base[NORMAL]      = $white
    base[PRELIGHT]    = $white
    base[ACTIVE]      = $text_field_grey

    text[PRELIGHT]    = $toolbar_grey
    text[NORMAL]      = $toolbar_grey
    text[ACTIVE]      = $toolbar_grey
}

style "separatormenuitem"
{
    GtkMenuItem::horizontal-padding = 0
    xthickness = 0
}

####################################################################
# Default style, setting some generic options and style properties
####################################################################
class "GtkWidget" style "default"

####################################################################
# Styles that apply the different background (and foreground) colors
####################################################################

# This one should probably be the default (ie. no window-child style)
widget_class "<GtkWindow>"               style "window"
widget_class "<GtkWindow>*<GtkEventBox>" style "window"
widget_class "<GtkWindow>*"              style "window-child"

# SugarToolbox
#widget_class "*<SugarToolbox>" style "toolbox"
#widget_class "*<SugarToolbox>*" style "toolbox-child"
#widget_class "*<SugarToolbox>*<GtkEventBox>" style "toolbox"
widget_class "*<SugarToolbox>*"        style "toolbox"
widget_class "*<GtkToolbar>*"          style "toolbox"

# SugarPalette
widget_class "<SugarPalette>"          style "menu"
widget_class "<SugarPalette>.*"        style "menu-child"

# SugarFrameWindow
widget_class "*<SugarFrameWindow>*"    style "frame"

# SugarPanel
widget_class "*<SugarPanel>*"          style "panel"

# GroupBoxes (don't set bg[NORMAL] on the widget itself)
widget_class "<GtkWindow>*<SugarGroupBox>*<GtkEventBox>" style "groupbox-panel"
widget_class "<GtkWindow>*<SugarGroupBox>*"   style "groupbox-panel-child"

widget_class "<SugarPalette>*<SugarGroupBox>*<GtkEventBox>" style "groupbox-palette"
widget_class "<SugarPalette>*<SugarGroupBox>*"   style "groupbox-palette-child"

####################################################################
# Normal widget styles, using the above things
####################################################################
# The following only uses widget_class matches to get the priority
# right. An alternative would be to lower the priority of the
# background matches (which need widget_class) to eg. "application"

# Menu
widget_class "*<GtkMenuShell>"            style "menu"               # Why is this menu shell?
widget_class "*<GtkMenuShell>.*"          style "menu-child"         # Why is this menu shell?
widget_class "*<GtkMenuItem>*"            style "menuitem"
widget_class "*<GtkCheckMenuItem>"        style "checkmenuitem"
widget_class "*<GtkSeparatorMenuItem>*"   style "separatormenuitem"

# Buttons and Combos
widget_class "*<GtkComboBox>*"         style "combobox"
widget_class "*<GtkComboBoxEntry>*"    style "comboboxentry"
widget_class "*<GtkCombo>*"            style "comboboxentry"
widget_class "*<GtkButton>*"           style "button"
widget_class "*<GtkCheckButton>*"      style "checkbutton"

# Entries
widget_class "*<GtkEntry>"       style "entry"
widget_class "*<GtkSpinButton>"  style "spinbutton"

# Misc widgets
widget_class "*<GtkNotebook>"    style "notebook"
widget_class "*<GtkHScrollbar>"  style "hscrollbar"
widget_class "*<GtkVScrollbar>"  style "vscrollbar"
widget_class "*<GtkHScale>"      style "hscale"
widget_class "*<GtkVScale>"      style "vscale"
widget_class "*<GtkProgressBar>" style "progressbar"

# Toolbar
widget_class "*<GtkToolButton>*"          style "toolbutton"

${
import math

def my_floor(num):
    return int(math.floor(num))

def my_ceil(num):
    return int(math.ceil(num))

# Should we set the line width in the engine to 2.25, and draw non pixel aligned lines?
# Are these already the correct sizes for the XO?

# These sizes need to be sanity checked ...
if theme == "sugar-xo":
    xo = True
    line_width = 2.25       # 2.25
    thick_line_width = 3.5  # 3.5
    subcell_size = 15
    bullet_size = 9.5
    font_height = 24
    default_padding = 6
else: # About 50% smaller
    xo = False
    line_width = 1.125
    thick_line_width = 1.75
    subcell_size = 7
    bullet_size = 5
    font_height = 12
    default_padding = 3

radio_size = my_floor(subcell_size + bullet_size + line_width)
scale_slider_width = my_floor(2 * subcell_size + line_width)
thickness = my_ceil(line_width)

# Colors:

black = '"#000000"'
toolbar_grey = '"#404040"'
button_grey = '"#808080"'
selection_grey = '"#A6A6A6"'
panel_grey = '"#C0C0C0"'
text_field_grey = '"#E5E5E5"'
white = '"#FFFFFF"'

}
style "default"
{
    # Will be overriden in some widgets
    color["focus_line"] = $white

    bg[NORMAL] = $button_grey
    bg[PRELIGHT] = $button_grey
    bg[ACTIVE] = $white
    bg[SELECTED] = $text_field_grey

    fg[NORMAL]        = $black
    fg[ACTIVE]        = $black
    fg[SELECTED]      = $white
    fg[PRELIGHT]      = $white

    text[NORMAL] = $black # button_grey? 1. need to patch GTK+ to use ACTIVE when focused, 2. what color should this really be?
    text[ACTIVE] = $black
    text[SELECTED] = $black
    text[PRELIGHT] = $black

    base[NORMAL] = $text_field_grey
    base[ACTIVE] = $white
    base[SELECTED] = $selection_grey
    base[PRELIGHT] = $white

    fg[INSENSITIVE] = $button_grey
    text[INSENSITIVE] = $button_grey

    xthickness = $thickness
    ythickness = $thickness

    # A lot of these will probably need to be changed, but this has to
    # be done when the exact sizes are known
    GtkWidget::interior-focus = 0
    GtkWidget::focus-line-width = 1   # This does not do anything really
    GtkWidget::focus-padding = 0
    # 0.05 works good for both the sugar and sugar-xo themes
    GtkWidget::cursor-aspect-ratio = 0.05

    GtkWidget::wide-separators = 0
    GtkWidget::separator-height = $thickness
    GtkWidget::separator-width = $thickness

    GtkRange::activate-slider = 1
    GtkButton::inner-border = { 6, 6, 6, 6 }           # ??

    GtkButton::default-border = { 1, 1, 1, 1 }         # line_width - focus-line-width
    GtkButton::default-outside-border = { 2, 2, 2, 2 } # focus-line-width

    GtkScrolledWindow::scrollbar-spacing = 0

    GtkExpander::expander-size = 24
    GtkExpander::expander-spacing = 2     # XXX

    GtkTreeView::expander-size = 24

    GtkArrow::arrow-size = 1.0

    GtkToolbar::shadow-type = GTK_SHADOW_NONE

    GtkProgressBar::min-horizontal-bar-height = $subcell_size
    GtkProgressBar::min-vertical-bar-width = $subcell_size

    engine "sugar" {
        line_width = $line_width
        thick_line_width = $thick_line_width

        # Not sure about this one, but it is not that important
        max_radius = $( 2*subcell_size )
    }
}

#############################################################
# Styles that set default background/foreground colors
#
#  We almost always need two styles because of the X windows.
#  For example bg[NORMAL] of a window (or eventbox) needs to
#  be set to be the correct window color so that X fills the
#  window correctly from the beginning, and it does not
#  flash in the wrong color.
#############################################################

style "window"
{
    bg[NORMAL] = $panel_grey
}

style "window-child"
{
    bg[INSENSITIVE] = $panel_grey
    base[INSENSITIVE] = $panel_grey

    fg[NORMAL] = $black

    engine "sugar" {
        label_fg_color = $black
    }
}

style "groupbox-panel"
{
    bg[NORMAL] = $selection_grey
}

style "groupbox-panel-child"
{
    bg[INSENSITIVE] = $selection_grey
    base[INSENSITIVE] = $selection_grey

    fg[NORMAL] = $white

    engine "sugar" {
        label_fg_color = $white
    }
}

style "groupbox-palette"
{
    bg[NORMAL] = $toolbar_grey
}

style "groupbox-palette-child"
{
    bg[INSENSITIVE] = $toolbar_grey
    base[INSENSITIVE] = $toolbar_grey

    fg[NORMAL] = $white

    engine "sugar" {
        label_fg_color = $white
    }
}

style "menu"
{
    bg[NORMAL] = $black
    bg[ACTIVE] = $button_grey

    GtkMenu::horizontal-padding = 0
    GtkMenu::vertical-padding   = 0

    xthickness = 0
    ythickness = 0
}

# Can this all be moved in the menuitem style?
style "menu-child"
{
    color["bg_color"] = $black

    fg[NORMAL]   = $white
    fg[PRELIGHT] = $white

    # Work around bug #382646
    text[NORMAL]   = $white
    text[PRELIGHT] = $white

    bg[NORMAL] = $button_grey

    bg[INSENSITIVE] = $black
    base[INSENSITIVE] = $black

    engine "sugar" {
        label_fg_color = $white
    }
}

#########################################################
#########################################################

style "scrollbar"
{
    GtkScrollbar::slider-width = 16    # ?
    GtkScrollbar::trough-border = 0
    GtkScrollbar::stepper-spacing = 0
    GtkScrollbar::min-slider-length = 30    # ?
    GtkScrollbar::has-forward-stepper = 0
    GtkScrollbar::has-backward-stepper = 0
    GtkScrollbar::has-secondary-forward-stepper = 0
    GtkScrollbar::has-secondary-backward-stepper = 0

    # Hack for mozilla as it does not obey the above style properties ...
    # Really, mozilla should be fixed ...
    GtkScrollbar::stepper-size = 1
}

# hints for the trough padding hacks
# We cannot go trough GTK+, as we don't want a padding on all sides, but only one
# If we wanted padding on all sides, trough-border would work great.
style "hscrollbar" = "scrollbar"
{
    engine "sugar" {
        hint = "hscrollbar"
        # Rounded, not floating point value. maybe change this?
        scrollbar_border = $thickness
    }
}

style "vscrollbar" = "scrollbar"
{
    engine "sugar" {
        hint = "vscrollbar"
        scrollbar_border = $thickness
    }
}

style "scale"
{
    GtkWidget::focus-line-width = 0

    bg[NORMAL] = "#808080"

    # "square" slider (really round of course)
    # Same as indicator-size?
    GtkRange::slider-width = $scale_slider_width
    GtkScale::slider-length = $scale_slider_width
    GtkRange::trough-border = 0
    GtkRange::stepper-spacing = 0
    GtkRange::trough-side-details = 1
}

# hints for the trough sizing hacks
style "hscale" = "scale"
{
    engine "sugar" {
        hint = "hscale"
    }
}

style "vscale" = "scale"
{
    engine "sugar" {
        hint = "vscale"
    }
}

style "spinbutton"
{
    bg[NORMAL]  = $button_grey
    bg[ACTIVE]  = $white
    fg[NORMAL]  = $white
    fg[ACTIVE]  = $black

    engine "sugar" {
        hint = "spinbutton"
    }
}

style "comboboxentry"
{
    # Copied from the "default" style. Part of the workaround for bug #382646.
    text[NORMAL] = $black
    text[ACTIVE] = $black
    text[SELECTED] = $black
    text[PRELIGHT] = $black

    engine "sugar" {
        hint = "comboboxentry"
    }
}

style "frame"
{
    bg[NORMAL] = $toolbar_grey
}

style "notebook"
{
    GtkNotebook::tab-overlap = -2

    xthickness = 0
    ythickness = 0

    bg[ACTIVE] = $button_grey
}

style "toolbutton"
{
    color["focus_line"] = $black

    bg[PRELIGHT] = $black
    bg[ACTIVE] = $button_grey

    fg[NORMAL] = $white
    fg[ACTIVE] = $white
    fg[PRELIGHT] = $white

    engine "sugar" {
        max_radius = 5.0
	fake_padding = $default_padding
    }
}

style "toolbox"
{
    fg[NORMAL] = $white
    fg[ACTIVE] = $white

    bg[NORMAL] = $toolbar_grey
    bg[INSENSITIVE] = $toolbar_grey
    base[INSENSITIVE] = $toolbar_grey
}

style "panel"
{
    bg[NORMAL] = $panel_grey
    fg[NORMAL] = $black
}

style "entry"
{
    # small inner border and a large x/ythickness for entries
    # to reduce the number of hacks needed :-)
    xthickness = $(thickness*3)
    ythickness = $(thickness*3)

    # This tries to get a height of exactly 45 pixel for the entry.
    GtkEntry::inner-border = { $(max(subcell_size - thickness*3, 0)), $(max(subcell_size - thickness*3, 0)),
                               $(max(my_floor((3*subcell_size - font_height - thickness*3*2)/2),0)), $(max(my_ceil((3*subcell_size - font_height - thickness*3*2)/2), 0)) }

    GtkWidget::focus-line-width = 0
}

style "button"
{
    fg[NORMAL]  = $white

    bg[NORMAL] = $button_grey
}

style "combobox"
{
    # Work around GTK+ bug #382646
    text[NORMAL]      = $white
    text[ACTIVE]      = $white
    text[PRELIGHT]    = $white
}

style "checkbutton"
{
    base[NORMAL]      = $white
    base[PRELIGHT]    = $white
    base[ACTIVE]      = $text_field_grey

    text[PRELIGHT]    = $toolbar_grey
    text[NORMAL]      = $toolbar_grey
    text[ACTIVE]      = $toolbar_grey

    # I won't get the exact sizing, but this should be pretty close
    GtkCheckButton::indicator-size = $radio_size
    GtkCheckButton::indicator-spacing = 3
    GtkWidget::focus-padding = 3

    engine "sugar" {
        # Reset the colors to the normal color again
        # as they were overriden by the button style
        fg[NORMAL]        = label_fg_color
        fg[PRELIGHT]      = label_fg_color
        fg[ACTIVE]        = label_fg_color
    }
}

style "progressbar"
{
    xthickness = 0
    ythickness = 0
}

style "menuitem"
{
    GtkCheckMenuItem::indicator-size = $radio_size

    bg[PRELIGHT] = $button_grey

    text[PRELIGHT]    = $white
    text[NORMAL]      = $white
    text[ACTIVE]      = $white

    GtkMenuItem::horizontal-padding = $line_width
    xthickness = $(my_ceil(line_width * 2))
    ythickness = $thickness
}

style "checkmenuitem"
{
    # This style is only there because of bug #382646 ...
    base[NORMAL]      = $white
    base[PRELIGHT]    = $white
    base[ACTIVE]      = $text_field_grey

    text[PRELIGHT]    = $toolbar_grey
    text[NORMAL]      = $toolbar_grey
    text[ACTIVE]      = $toolbar_grey
}

style "separatormenuitem"
{
    GtkMenuItem::horizontal-padding = 0
    xthickness = 0
}

####################################################################
# Default style, setting some generic options and style properties
####################################################################
class "GtkWidget" style "default"

####################################################################
# Styles that apply the different background (and foreground) colors
####################################################################

# This one should probably be the default (ie. no window-child style)
widget_class "<GtkWindow>"               style "window"
widget_class "<GtkWindow>*<GtkEventBox>" style "window"
widget_class "<GtkWindow>*"              style "window-child"

# SugarToolbox
#widget_class "*<SugarToolbox>" style "toolbox"
#widget_class "*<SugarToolbox>*" style "toolbox-child"
#widget_class "*<SugarToolbox>*<GtkEventBox>" style "toolbox"
widget_class "*<SugarToolbox>*"        style "toolbox"
widget_class "*<GtkToolbar>*"          style "toolbox"

# SugarPalette
widget_class "<SugarPalette>"          style "menu"
widget_class "<SugarPalette>.*"        style "menu-child"

# SugarFrameWindow
widget_class "*<SugarFrameWindow>*"    style "frame"

# SugarPanel
widget_class "*<SugarPanel>*"          style "panel"

# GroupBoxes (don't set bg[NORMAL] on the widget itself)
widget_class "<GtkWindow>*<SugarGroupBox>*<GtkEventBox>" style "groupbox-panel"
widget_class "<GtkWindow>*<SugarGroupBox>*"   style "groupbox-panel-child"

widget_class "<SugarPalette>*<SugarGroupBox>*<GtkEventBox>" style "groupbox-palette"
widget_class "<SugarPalette>*<SugarGroupBox>*"   style "groupbox-palette-child"

####################################################################
# Normal widget styles, using the above things
####################################################################
# The following only uses widget_class matches to get the priority
# right. An alternative would be to lower the priority of the
# background matches (which need widget_class) to eg. "application"

# Toolbar
widget_class "*<GtkToolButton>*"          style "toolbutton"

# Menu
widget_class "*<GtkMenuShell>"            style "menu"               # Why is this menu shell?
widget_class "*<GtkMenuShell>.*"          style "menu-child"         # Why is this menu shell?
widget_class "*<GtkMenuItem>*"            style "menuitem"
widget_class "*<GtkCheckMenuItem>"        style "checkmenuitem"
widget_class "*<GtkSeparatorMenuItem>*"   style "separatormenuitem"

# Buttons and Combos
widget_class "*<GtkComboBox>*"         style "combobox"
widget_class "*<GtkComboBoxEntry>*"    style "comboboxentry"
widget_class "*<GtkCombo>*"            style "comboboxentry"
widget_class "*<GtkButton>*"           style "button"
widget_class "*<GtkCheckButton>*"      style "checkbutton"

# Entries
widget_class "*<GtkEntry>"       style "entry"
widget_class "*<GtkSpinButton>"  style "spinbutton"

# Misc widgets
widget_class "*<GtkNotebook>"    style "notebook"
widget_class "*<GtkHScrollbar>"  style "hscrollbar"
widget_class "*<GtkVScrollbar>"  style "vscrollbar"
widget_class "*<GtkHScale>"      style "hscale"
widget_class "*<GtkVScale>"      style "vscale"
widget_class "*<GtkProgressBar>" style "progressbar"