[pulseview] 01/06: Import Upstream version 0.2.0

Zoltan Gyarmati zgyarmati-guest at moszumanska.debian.org
Wed Mar 29 21:43:19 UTC 2017


This is an automated email from the git hooks/post-receive script.

zgyarmati-guest pushed a commit to branch master
in repository pulseview.

commit 02adf9a7b0913f54d43d467d0781d319a895400c
Author: Zoltan Gyarmati <Zoltan Gyarmati mr.zoltan.gyarmati at gmail.com>
Date:   Wed Mar 1 02:09:53 2017 +0100

    Import Upstream version 0.2.0
---
 CMake/cotire.cmake                 | 3185 ++++++++++++++++++++++++++++++++++++
 CMake/memaccess.cmake              |   41 +
 CMakeLists.txt                     |  373 +++++
 COPYING                            |  674 ++++++++
 HACKING                            |   79 +
 INSTALL                            |   53 +
 NEWS                               |  101 ++
 README                             |   69 +
 config.h.in                        |   37 +
 contrib/pulseview.desktop          |    9 +
 contrib/pulseview_cross.nsi        |  252 +++
 doc/pulseview.1                    |   59 +
 extdef.h                           |   29 +
 icons/application-exit.png         |  Bin 0 -> 1084 bytes
 icons/configure.png                |  Bin 0 -> 1151 bytes
 icons/decoder-delete.svg           |   17 +
 icons/decoder-hidden.svg           |   33 +
 icons/decoder-shown.svg            |   47 +
 icons/document-open.png            |  Bin 0 -> 919 bytes
 icons/document-save-as.png         |  Bin 0 -> 841 bytes
 icons/probes.svg                   |   72 +
 icons/sigrok-logo-notext.ico       |  Bin 0 -> 9662 bytes
 icons/sigrok-logo-notext.png       |  Bin 0 -> 3782 bytes
 icons/status-green.svg             |   12 +
 icons/status-grey.svg              |   12 +
 icons/status-red.svg               |   12 +
 icons/trigger-change.svg           |   76 +
 icons/trigger-falling.svg          |   36 +
 icons/trigger-high.svg             |   36 +
 icons/trigger-low.svg              |   36 +
 icons/trigger-none.svg             |   35 +
 icons/trigger-rising.svg           |   36 +
 icons/zoom-fit.png                 |  Bin 0 -> 729 bytes
 icons/zoom-in.png                  |  Bin 0 -> 727 bytes
 icons/zoom-original.png            |  Bin 0 -> 766 bytes
 icons/zoom-out.png                 |  Bin 0 -> 651 bytes
 main.cpp                           |  181 ++
 pulseview.qrc                      |   26 +
 pulseviewico.rc                    |    1 +
 pv/data/analog.cpp                 |   64 +
 pv/data/analog.h                   |   56 +
 pv/data/analogsnapshot.cpp         |  215 +++
 pv/data/analogsnapshot.h           |   96 ++
 pv/data/decode/annotation.cpp      |   73 +
 pv/data/decode/annotation.h        |   55 +
 pv/data/decode/decoder.cpp         |  159 ++
 pv/data/decode/decoder.h           |   90 +
 pv/data/decode/row.cpp             |   72 +
 pv/data/decode/row.h               |   59 +
 pv/data/decode/rowdata.cpp         |   58 +
 pv/data/decode/rowdata.h           |   57 +
 pv/data/decoderstack.cpp           |  450 +++++
 pv/data/decoderstack.h             |  165 ++
 pv/data/logic.cpp                  |   72 +
 pv/data/logic.h                    |   59 +
 pv/data/logicsnapshot.cpp          |  468 ++++++
 pv/data/logicsnapshot.h            |  115 ++
 pv/data/signaldata.cpp             |   49 +
 pv/data/signaldata.h               |   52 +
 pv/data/snapshot.cpp               |   95 ++
 pv/data/snapshot.h                 |   83 +
 pv/device/device.cpp               |  104 ++
 pv/device/device.h                 |   51 +
 pv/device/devinst.cpp              |  139 ++
 pv/device/devinst.h                |   95 ++
 pv/device/file.cpp                 |   66 +
 pv/device/file.h                   |   49 +
 pv/device/inputfile.cpp            |  143 ++
 pv/device/inputfile.h              |   68 +
 pv/device/sessionfile.cpp          |   75 +
 pv/device/sessionfile.h            |   47 +
 pv/devicemanager.cpp               |  150 ++
 pv/devicemanager.h                 |   75 +
 pv/dialogs/about.cpp               |  121 ++
 pv/dialogs/about.h                 |   53 +
 pv/dialogs/about.ui                |   96 ++
 pv/dialogs/connect.cpp             |  241 +++
 pv/dialogs/connect.h               |   95 ++
 pv/dialogs/storeprogress.cpp       |   84 +
 pv/dialogs/storeprogress.h         |   65 +
 pv/mainwindow.cpp                  |  456 ++++++
 pv/mainwindow.h                    |  122 ++
 pv/popups/deviceoptions.cpp        |   53 +
 pv/popups/deviceoptions.h          |   54 +
 pv/popups/probes.cpp               |  258 +++
 pv/popups/probes.h                 |  104 ++
 pv/prop/binding/binding.cpp        |   94 ++
 pv/prop/binding/binding.h          |   64 +
 pv/prop/binding/decoderoptions.cpp |  151 ++
 pv/prop/binding/decoderoptions.h   |   66 +
 pv/prop/binding/deviceoptions.cpp  |  192 +++
 pv/prop/binding/deviceoptions.h    |   72 +
 pv/prop/bool.cpp                   |   83 +
 pv/prop/bool.h                     |   55 +
 pv/prop/double.cpp                 |   96 ++
 pv/prop/double.h                   |   67 +
 pv/prop/enum.cpp                   |   98 ++
 pv/prop/enum.h                     |   60 +
 pv/prop/int.cpp                    |  186 +++
 pv/prop/int.h                      |   64 +
 pv/prop/property.cpp               |   44 +
 pv/prop/property.h                 |   67 +
 pv/prop/string.cpp                 |   77 +
 pv/prop/string.h                   |   52 +
 pv/sigsession.cpp                  |  650 ++++++++
 pv/sigsession.h                    |  204 +++
 pv/storesession.cpp                |  196 +++
 pv/storesession.h                  |   83 +
 pv/toolbars/samplingbar.cpp        |  450 +++++
 pv/toolbars/samplingbar.h          |  121 ++
 pv/view/analogsignal.cpp           |  202 +++
 pv/view/analogsignal.h             |   92 ++
 pv/view/cursor.cpp                 |  159 ++
 pv/view/cursor.h                   |   86 +
 pv/view/cursorpair.cpp             |  155 ++
 pv/view/cursorpair.h               |   81 +
 pv/view/decodetrace.cpp            |  725 ++++++++
 pv/view/decodetrace.h              |  191 +++
 pv/view/header.cpp                 |  304 ++++
 pv/view/header.h                   |   94 ++
 pv/view/logicsignal.cpp            |  353 ++++
 pv/view/logicsignal.h              |  119 ++
 pv/view/marginwidget.cpp           |   35 +
 pv/view/marginwidget.h             |   53 +
 pv/view/ruler.cpp                  |  275 ++++
 pv/view/ruler.h                    |   84 +
 pv/view/selectableitem.cpp         |   66 +
 pv/view/selectableitem.h           |   81 +
 pv/view/signal.cpp                 |  136 ++
 pv/view/signal.h                   |   92 ++
 pv/view/timemarker.cpp             |  104 ++
 pv/view/timemarker.h               |  113 ++
 pv/view/trace.cpp                  |  292 ++++
 pv/view/trace.h                    |  203 +++
 pv/view/tracepalette.cpp           |   70 +
 pv/view/tracepalette.h             |   41 +
 pv/view/view.cpp                   |  539 ++++++
 pv/view/view.h                     |  214 +++
 pv/view/viewport.cpp               |  161 ++
 pv/view/viewport.h                 |   68 +
 pv/widgets/colourbutton.cpp        |  109 ++
 pv/widgets/colourbutton.h          |   68 +
 pv/widgets/colourpopup.cpp         |   51 +
 pv/widgets/colourpopup.h           |   55 +
 pv/widgets/decodergroupbox.cpp     |   77 +
 pv/widgets/decodergroupbox.h       |   56 +
 pv/widgets/decodermenu.cpp         |   72 +
 pv/widgets/decodermenu.h           |   56 +
 pv/widgets/popup.cpp               |  269 +++
 pv/widgets/popup.h                 |   93 ++
 pv/widgets/popuptoolbutton.cpp     |   58 +
 pv/widgets/popuptoolbutton.h       |   52 +
 pv/widgets/sweeptimingwidget.cpp   |  184 +++
 pv/widgets/sweeptimingwidget.h     |   75 +
 pv/widgets/wellarray.cpp           |  290 ++++
 pv/widgets/wellarray.h             |  133 ++
 signalhandler.cpp                  |   95 ++
 signalhandler.h                    |   55 +
 test/CMakeLists.txt                |  176 ++
 test/data/analogsnapshot.cpp       |  114 ++
 test/data/decoderstack.cpp         |   86 +
 test/data/logicsnapshot.cpp        |  534 ++++++
 test/test.cpp                      |   22 +
 163 files changed, 22415 insertions(+)

diff --git a/CMake/cotire.cmake b/CMake/cotire.cmake
new file mode 100644
index 0000000..a6e3141
--- /dev/null
+++ b/CMake/cotire.cmake
@@ -0,0 +1,3185 @@
+# - cotire (compile time reducer)
+#
+# See the cotire manual for usage hints.
+#
+#=============================================================================
+# Copyright 2012-2013 Sascha Kratky
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#=============================================================================
+
+if(__COTIRE_INCLUDED)
+	return()
+endif()
+set(__COTIRE_INCLUDED TRUE)
+
+# call cmake_minimum_required, but prevent modification of the CMake policy stack in include mode
+# cmake_minimum_required also sets the policy version as a side effect, which we have to avoid
+if (NOT CMAKE_SCRIPT_MODE_FILE)
+	cmake_policy(PUSH)
+endif()
+# we need the CMake variables CMAKE_SCRIPT_MODE_FILE and CMAKE_ARGV available since 2.8.5
+# we need APPEND_STRING option for set_property available since 2.8.6
+cmake_minimum_required(VERSION 2.8.6)
+if (NOT CMAKE_SCRIPT_MODE_FILE)
+	cmake_policy(POP)
+endif()
+
+set (COTIRE_CMAKE_MODULE_FILE "${CMAKE_CURRENT_LIST_FILE}")
+set (COTIRE_CMAKE_MODULE_VERSION "1.4.1")
+
+include(CMakeParseArguments)
+include(ProcessorCount)
+
+function (cotire_determine_compiler_version _language _versionPrefix)
+	if (NOT ${_versionPrefix}_VERSION)
+		# use CMake's predefined compiler version variable (available since CMake 2.8.8)
+		if (DEFINED CMAKE_${_language}_COMPILER_VERSION)
+			set (${_versionPrefix}_VERSION "${CMAKE_${_language}_COMPILER_VERSION}")
+		elseif (WIN32)
+			# cl.exe messes with the output streams unless the environment variable VS_UNICODE_OUTPUT is cleared
+			unset (ENV{VS_UNICODE_OUTPUT})
+			string (STRIP "${CMAKE_${_language}_COMPILER_ARG1}" _compilerArg1)
+			execute_process (COMMAND ${CMAKE_${_language}_COMPILER} ${_compilerArg1}
+				ERROR_VARIABLE _versionLine OUTPUT_QUIET TIMEOUT 10)
+			string (REGEX REPLACE ".*Version *([0-9]+(\\.[0-9]+)*).*" "\\1" ${_versionPrefix}_VERSION "${_versionLine}")
+		else()
+			# assume GCC like command line interface
+			string (STRIP "${CMAKE_${_language}_COMPILER_ARG1}" _compilerArg1)
+			execute_process (COMMAND ${CMAKE_${_language}_COMPILER} ${_compilerArg1} "-dumpversion"
+				OUTPUT_VARIABLE ${_versionPrefix}_VERSION
+				RESULT_VARIABLE _result
+				OUTPUT_STRIP_TRAILING_WHITESPACE TIMEOUT 10)
+			if (_result)
+				set (${_versionPrefix}_VERSION "")
+			endif()
+		endif()
+		if (${_versionPrefix}_VERSION)
+			set (${_versionPrefix}_VERSION "${${_versionPrefix}_VERSION}" CACHE INTERNAL "${_language} compiler version")
+		endif()
+		set (${_versionPrefix}_VERSION "${${_versionPrefix}_VERSION}" PARENT_SCOPE)
+		if (COTIRE_DEBUG)
+			message (STATUS "${CMAKE_${_language}_COMPILER} version ${${_versionPrefix}_VERSION}")
+		endif()
+	endif()
+endfunction()
+
+function (cotire_get_source_file_extension _sourceFile _extVar)
+	# get_filename_component returns extension from first occurrence of . in file name
+	# this function computes the extension from last occurrence of . in file name
+	string (FIND "${_sourceFile}" "." _index REVERSE)
+	if (_index GREATER -1)
+		math (EXPR _index "${_index} + 1")
+		string (SUBSTRING "${_sourceFile}" ${_index} -1 _sourceExt)
+	else()
+		set (_sourceExt "")
+	endif()
+	set (${_extVar} "${_sourceExt}" PARENT_SCOPE)
+endfunction()
+
+macro (cotire_check_is_path_relative_to _path _isRelativeVar)
+	set (${_isRelativeVar} FALSE)
+	if (IS_ABSOLUTE "${_path}")
+		foreach (_dir ${ARGN})
+			file (RELATIVE_PATH _relPath "${_dir}" "${_path}")
+			if (NOT _relPath OR (NOT IS_ABSOLUTE "${_relPath}" AND NOT "${_relPath}" MATCHES "^\\.\\."))
+				set (${_isRelativeVar} TRUE)
+				break()
+			endif()
+		endforeach()
+	endif()
+endmacro()
+
+function (cotire_filter_language_source_files _language _sourceFilesVar _excludedSourceFilesVar _cotiredSourceFilesVar)
+	set (_sourceFiles "")
+	set (_excludedSourceFiles "")
+	set (_cotiredSourceFiles "")
+	if (CMAKE_${_language}_SOURCE_FILE_EXTENSIONS)
+		set (_languageExtensions "${CMAKE_${_language}_SOURCE_FILE_EXTENSIONS}")
+	else()
+		set (_languageExtensions "")
+	endif()
+	if (CMAKE_${_language}_IGNORE_EXTENSIONS)
+		set (_ignoreExtensions "${CMAKE_${_language}_IGNORE_EXTENSIONS}")
+	else()
+		set (_ignoreExtensions "")
+	endif()
+	if (COTIRE_UNITY_SOURCE_EXCLUDE_EXTENSIONS)
+		set (_excludeExtensions "${COTIRE_UNITY_SOURCE_EXCLUDE_EXTENSIONS}")
+	else()
+		set (_excludeExtensions "")
+	endif()
+	if (COTIRE_DEBUG)
+		message (STATUS "${_language} source file extensions: ${_languageExtensions}")
+		message (STATUS "${_language} ignore extensions: ${_ignoreExtensions}")
+		message (STATUS "${_language} exclude extensions: ${_excludeExtensions}")
+	endif()
+	foreach (_sourceFile ${ARGN})
+		get_source_file_property(_sourceIsHeaderOnly "${_sourceFile}" HEADER_FILE_ONLY)
+		get_source_file_property(_sourceIsExternal "${_sourceFile}" EXTERNAL_OBJECT)
+		get_source_file_property(_sourceIsSymbolic "${_sourceFile}" SYMBOLIC)
+		get_source_file_property(_sourceLanguage "${_sourceFile}" LANGUAGE)
+		set (_sourceIsFiltered FALSE)
+		if (NOT _sourceIsHeaderOnly AND NOT _sourceIsExternal AND NOT _sourceIsSymbolic)
+			cotire_get_source_file_extension("${_sourceFile}" _sourceExt)
+			if (_sourceExt)
+				list (FIND _ignoreExtensions "${_sourceExt}" _ignoreIndex)
+				if (_ignoreIndex LESS 0)
+					list (FIND _excludeExtensions "${_sourceExt}" _excludeIndex)
+					if (_excludeIndex GREATER -1)
+						list (APPEND _excludedSourceFiles "${_sourceFile}")
+					else()
+						list (FIND _languageExtensions "${_sourceExt}" _sourceIndex)
+						if (_sourceIndex GREATER -1)
+							set (_sourceIsFiltered TRUE)
+						elseif ("${_sourceLanguage}" STREQUAL "${_language}")
+							# add to excluded sources, if file is not ignored and has correct language without having the correct extension
+							list (APPEND _excludedSourceFiles "${_sourceFile}")
+						endif()
+					endif()
+				endif()
+			endif()
+		endif()
+		if (COTIRE_DEBUG)
+			message (STATUS "${_sourceFile} filtered=${_sourceIsFiltered} language=${_sourceLanguage} header=${_sourceIsHeaderOnly}")
+		endif()
+		if (_sourceIsFiltered)
+			get_source_file_property(_sourceIsExcluded "${_sourceFile}" COTIRE_EXCLUDED)
+			get_source_file_property(_sourceIsCotired "${_sourceFile}" COTIRE_TARGET)
+			get_source_file_property(_sourceCompileFlags "${_sourceFile}" COMPILE_FLAGS)
+			if (COTIRE_DEBUG)
+				message (STATUS "${_sourceFile} excluded=${_sourceIsExcluded} cotired=${_sourceIsCotired}")
+			endif()
+			if (_sourceIsCotired)
+				list (APPEND _cotiredSourceFiles "${_sourceFile}")
+			elseif (_sourceIsExcluded OR _sourceCompileFlags)
+				list (APPEND _excludedSourceFiles "${_sourceFile}")
+			else()
+				list (APPEND _sourceFiles "${_sourceFile}")
+			endif()
+		endif()
+	endforeach()
+	if (COTIRE_DEBUG)
+		message (STATUS "All: ${ARGN}")
+		message (STATUS "${_language}: ${_sourceFiles}")
+		message (STATUS "Excluded: ${_excludedSourceFiles}")
+		message (STATUS "Cotired: ${_cotiredSourceFiles}")
+	endif()
+	set (${_sourceFilesVar} ${_sourceFiles} PARENT_SCOPE)
+	set (${_excludedSourceFilesVar} ${_excludedSourceFiles} PARENT_SCOPE)
+	set (${_cotiredSourceFilesVar} ${_cotiredSourceFiles} PARENT_SCOPE)
+endfunction()
+
+function (cotire_get_objects_with_property_on _filteredObjectsVar _property _type)
+	set (_filteredObjects "")
+	foreach (_object ${ARGN})
+		get_property(_isSet ${_type} "${_object}" PROPERTY ${_property} SET)
+		if (_isSet)
+			get_property(_propertyValue ${_type} "${_object}" PROPERTY ${_property})
+			if (_propertyValue)
+				list (APPEND _filteredObjects "${_object}")
+			endif()
+		endif()
+	endforeach()
+	set (${_filteredObjectsVar} ${_filteredObjects} PARENT_SCOPE)
+endfunction()
+
+function (cotire_get_objects_with_property_off _filteredObjectsVar _property _type)
+	set (_filteredObjects "")
+	foreach (_object ${ARGN})
+		get_property(_isSet ${_type} "${_object}" PROPERTY ${_property} SET)
+		if (_isSet)
+			get_property(_propertyValue ${_type} "${_object}" PROPERTY ${_property})
+			if (NOT _propertyValue)
+				list (APPEND _filteredObjects "${_object}")
+			endif()
+		endif()
+	endforeach()
+	set (${_filteredObjectsVar} ${_filteredObjects} PARENT_SCOPE)
+endfunction()
+
+function (cotire_get_source_file_property_values _valuesVar _property)
+	set (_values "")
+	foreach (_sourceFile ${ARGN})
+		get_source_file_property(_propertyValue "${_sourceFile}" ${_property})
+		if (_propertyValue)
+			list (APPEND _values "${_propertyValue}")
+		endif()
+	endforeach()
+	set (${_valuesVar} ${_values} PARENT_SCOPE)
+endfunction()
+
+function (cotrie_resolve_config_properites _configurations _propertiesVar)
+	set (_properties "")
+	foreach (_property ${ARGN})
+		if ("${_property}" MATCHES "<CONFIG>")
+			foreach (_config ${_configurations})
+				string (TOUPPER "${_config}" _upperConfig)
+				string (REPLACE "<CONFIG>" "${_upperConfig}" _configProperty "${_property}")
+				list (APPEND _properties ${_configProperty})
+			endforeach()
+		else()
+			list (APPEND _properties ${_property})
+		endif()
+	endforeach()
+	set (${_propertiesVar} ${_properties} PARENT_SCOPE)
+endfunction()
+
+function (cotrie_copy_set_properites _configurations _type _source _target)
+	cotrie_resolve_config_properites("${_configurations}" _properties ${ARGN})
+	foreach (_property ${_properties})
+		get_property(_isSet ${_type} ${_source} PROPERTY ${_property} SET)
+		if (_isSet)
+			get_property(_propertyValue ${_type} ${_source} PROPERTY ${_property})
+			set_property(${_type} ${_target} PROPERTY ${_property} "${_propertyValue}")
+		endif()
+	endforeach()
+endfunction()
+
+function (cotire_filter_compile_flags _language _flagFilter _matchedOptionsVar _unmatchedOptionsVar)
+	if (WIN32 AND CMAKE_${_language}_COMPILER_ID MATCHES "MSVC|Intel")
+		set (_flagPrefix "[/-]")
+	else()
+		set (_flagPrefix "--?")
+	endif()
+	set (_optionFlag "")
+	set (_matchedOptions "")
+	set (_unmatchedOptions "")
+	foreach (_compileFlag ${ARGN})
+		if (_compileFlag)
+			if (_optionFlag AND NOT "${_compileFlag}" MATCHES "^${_flagPrefix}")
+				# option with separate argument
+				list (APPEND _matchedOptions "${_compileFlag}")
+				set (_optionFlag "")
+			elseif ("${_compileFlag}" MATCHES "^(${_flagPrefix})(${_flagFilter})$")
+				# remember option
+				set (_optionFlag "${CMAKE_MATCH_2}")
+			elseif ("${_compileFlag}" MATCHES "^(${_flagPrefix})(${_flagFilter})(.+)$")
+				# option with joined argument
+				list (APPEND _matchedOptions "${CMAKE_MATCH_3}")
+				set (_optionFlag "")
+			else()
+				# flush remembered option
+				if (_optionFlag)
+					list (APPEND _matchedOptions "${_optionFlag}")
+					set (_optionFlag "")
+				endif()
+				# add to unfiltered options
+				list (APPEND _unmatchedOptions "${_compileFlag}")
+			endif()
+		endif()
+	endforeach()
+	if (_optionFlag)
+		list (APPEND _matchedOptions "${_optionFlag}")
+	endif()
+	if (COTIRE_DEBUG)
+		message (STATUS "Filter ${_flagFilter}")
+		if (_matchedOptions)
+			message (STATUS "Matched ${_matchedOptions}")
+		endif()
+		if (_unmatchedOptions)
+			message (STATUS "Unmatched ${_unmatchedOptions}")
+		endif()
+	endif()
+	set (${_matchedOptionsVar} ${_matchedOptions} PARENT_SCOPE)
+	set (${_unmatchedOptionsVar} ${_unmatchedOptions} PARENT_SCOPE)
+endfunction()
+
+function (cotire_get_target_compile_flags _config _language _directory _target _flagsVar)
+	string (TOUPPER "${_config}" _upperConfig)
+	# collect options from CMake language variables
+	set (_compileFlags "")
+	if (CMAKE_${_language}_FLAGS)
+		set (_compileFlags "${_compileFlags} ${CMAKE_${_language}_FLAGS}")
+	endif()
+	if (CMAKE_${_language}_FLAGS_${_upperConfig})
+		set (_compileFlags "${_compileFlags} ${CMAKE_${_language}_FLAGS_${_upperConfig}}")
+	endif()
+	if (_target)
+		# add option from CMake target type variable
+		get_target_property(_targetType ${_target} TYPE)
+		if (POLICY CMP0018)
+			# handle POSITION_INDEPENDENT_CODE property introduced with CMake 2.8.9 if policy CMP0018 is turned on
+			cmake_policy(GET CMP0018 _PIC_Policy)
+		else()
+			# default to old behavior
+			set (_PIC_Policy "OLD")
+		endif()
+		if (COTIRE_DEBUG)
+			message(STATUS "CMP0018=${_PIC_Policy}")
+		endif()
+		if (_PIC_Policy STREQUAL "NEW")
+			# NEW behavior: honor the POSITION_INDEPENDENT_CODE target property
+			get_target_property(_targetPIC ${_target} POSITION_INDEPENDENT_CODE)
+			if (_targetPIC)
+				if (_targetType STREQUAL "EXECUTABLE" AND CMAKE_${_language}_COMPILE_OPTIONS_PIE)
+					set (_compileFlags "${_compileFlags} ${CMAKE_${_language}_COMPILE_OPTIONS_PIE}")
+				elseif (CMAKE_${_language}_COMPILE_OPTIONS_PIC)
+					set (_compileFlags "${_compileFlags} ${CMAKE_${_language}_COMPILE_OPTIONS_PIC}")
+				endif()
+			endif()
+		else()
+			# OLD behavior or policy not set: use the value of CMAKE_SHARED_LIBRARY_<Lang>_FLAGS
+			if (_targetType STREQUAL "MODULE_LIBRARY")
+				# flags variable for module library uses different name SHARED_MODULE
+				# (e.g., CMAKE_SHARED_MODULE_C_FLAGS)
+				set (_targetType SHARED_MODULE)
+			endif()
+			if (CMAKE_${_targetType}_${_language}_FLAGS)
+				set (_compileFlags "${_compileFlags} ${CMAKE_${_targetType}_${_language}_FLAGS}")
+			endif()
+		endif()
+	endif()
+	if (_directory)
+		# add_definitions may have been used to add flags to the compiler command
+		get_directory_property(_dirDefinitions DIRECTORY "${_directory}" DEFINITIONS)
+		if (_dirDefinitions)
+			set (_compileFlags "${_compileFlags} ${_dirDefinitions}")
+		endif()
+	endif()
+	if (_target)
+		# add target compile options
+		get_target_property(_targetflags ${_target} COMPILE_FLAGS)
+		if (_targetflags)
+			set (_compileFlags "${_compileFlags} ${_targetflags}")
+		endif()
+	endif()
+	if (UNIX)
+		separate_arguments(_compileFlags UNIX_COMMAND "${_compileFlags}")
+	elseif(WIN32)
+		separate_arguments(_compileFlags WINDOWS_COMMAND "${_compileFlags}")
+	else()
+		separate_arguments(_compileFlags)
+	endif()
+	# platform specific flags
+	if (APPLE)
+		get_target_property(_architectures ${_target} OSX_ARCHITECTURES_${_upperConfig})
+		if (NOT _architectures)
+			get_target_property(_architectures ${_target} OSX_ARCHITECTURES)
+		endif()
+		foreach (_arch ${_architectures})
+			list (APPEND _compileFlags "-arch" "${_arch}")
+		endforeach()
+		if (CMAKE_OSX_SYSROOT AND CMAKE_OSX_SYSROOT_DEFAULT AND CMAKE_${_language}_HAS_ISYSROOT)
+			if (NOT "${CMAKE_OSX_SYSROOT}" STREQUAL "${CMAKE_OSX_SYSROOT_DEFAULT}")
+				list (APPEND _compileFlags "-isysroot" "${CMAKE_OSX_SYSROOT}")
+			endif()
+		endif()
+		if (CMAKE_OSX_DEPLOYMENT_TARGET AND CMAKE_${_language}_OSX_DEPLOYMENT_TARGET_FLAG)
+			list (APPEND _compileFlags "${CMAKE_${_language}_OSX_DEPLOYMENT_TARGET_FLAG}${CMAKE_OSX_DEPLOYMENT_TARGET}")
+		endif()
+	endif()
+	if (COTIRE_DEBUG AND _compileFlags)
+		message (STATUS "Target ${_target} compile flags ${_compileFlags}")
+	endif()
+	set (${_flagsVar} ${_compileFlags} PARENT_SCOPE)
+endfunction()
+
+function (cotire_get_target_include_directories _config _language _targetSourceDir _targetBinaryDir _target _includeDirsVar)
+	set (_includeDirs "")
+	# default include dirs
+	if (CMAKE_INCLUDE_CURRENT_DIR)
+		list (APPEND _includeDirs "${_targetBinaryDir}")
+		list (APPEND _includeDirs "${_targetSourceDir}")
+	endif()
+	# parse additional include directories from target compile flags
+	set (_targetFlags "")
+	cotire_get_target_compile_flags("${_config}" "${_language}" "${_targetSourceDir}" "${_target}" _targetFlags)
+	cotire_filter_compile_flags("${_language}" "I" _dirs _ignore ${_targetFlags})
+	if (_dirs)
+		list (APPEND _includeDirs ${_dirs})
+	endif()
+	# target include directories
+	get_directory_property(_dirs DIRECTORY "${_targetSourceDir}" INCLUDE_DIRECTORIES)
+	if (_target)
+		get_target_property(_targetDirs ${_target} INCLUDE_DIRECTORIES)
+		if (_targetDirs)
+			list (APPEND _dirs ${_targetDirs})
+			list (REMOVE_DUPLICATES _dirs)
+		endif()
+	endif()
+	list (LENGTH _includeDirs _projectInsertIndex)
+	foreach (_dir ${_dirs})
+		if (CMAKE_INCLUDE_DIRECTORIES_PROJECT_BEFORE)
+			cotire_check_is_path_relative_to("${_dir}" _isRelative "${CMAKE_SOURCE_DIR}" "${CMAKE_BINARY_DIR}")
+			if (_isRelative)
+				list (LENGTH _includeDirs _len)
+				if (_len EQUAL _projectInsertIndex)
+					list (APPEND _includeDirs "${_dir}")
+				else()
+					list (INSERT _includeDirs _projectInsertIndex "${_dir}")
+				endif()
+				math (EXPR _projectInsertIndex "${_projectInsertIndex} + 1")
+			else()
+				list (APPEND _includeDirs "${_dir}")
+			endif()
+		else()
+			list (APPEND _includeDirs "${_dir}")
+		endif()
+	endforeach()
+	list (REMOVE_DUPLICATES _includeDirs)
+	if (CMAKE_${_language}_IMPLICIT_INCLUDE_DIRECTORIES)
+		list (REMOVE_ITEM _includeDirs ${CMAKE_${_language}_IMPLICIT_INCLUDE_DIRECTORIES})
+	endif()
+	if (COTIRE_DEBUG AND _includeDirs)
+		message (STATUS "Target ${_target} include dirs ${_includeDirs}")
+	endif()
+	set (${_includeDirsVar} ${_includeDirs} PARENT_SCOPE)
+endfunction()
+
+macro (cotire_make_C_identifier _identifierVar _str)
+	# mimic CMake SystemTools::MakeCindentifier behavior
+	if ("${_str}" MATCHES "^[0-9].+$")
+		set (_str "_${str}")
+	endif()
+	string (REGEX REPLACE "[^a-zA-Z0-9]" "_" ${_identifierVar} "${_str}")
+endmacro()
+
+function (cotire_get_target_export_symbol _target _exportSymbolVar)
+	set (_exportSymbol "")
+	get_target_property(_targetType ${_target} TYPE)
+	get_target_property(_enableExports ${_target} ENABLE_EXPORTS)
+	if (_targetType MATCHES "(SHARED|MODULE)_LIBRARY" OR
+		(_targetType STREQUAL "EXECUTABLE" AND _enableExports))
+		get_target_property(_exportSymbol ${_target} DEFINE_SYMBOL)
+		if (NOT _exportSymbol)
+			set (_exportSymbol "${_target}_EXPORTS")
+		endif()
+		cotire_make_C_identifier(_exportSymbol "${_exportSymbol}")
+	endif()
+	set (${_exportSymbolVar} ${_exportSymbol} PARENT_SCOPE)
+endfunction()
+
+function (cotire_get_target_compile_definitions _config _language _directory _target _definitionsVar)
+	string (TOUPPER "${_config}" _upperConfig)
+	set (_configDefinitions "")
+	# CMAKE_INTDIR for multi-configuration build systems
+	if (NOT "${CMAKE_CFG_INTDIR}" STREQUAL ".")
+		list (APPEND _configDefinitions "CMAKE_INTDIR=\"${_config}\"")
+	endif()
+	# target export define symbol
+	cotire_get_target_export_symbol("${_target}" _defineSymbol)
+	if (_defineSymbol)
+		list (APPEND _configDefinitions "${_defineSymbol}")
+	endif()
+	# directory compile definitions
+	get_directory_property(_definitions DIRECTORY "${_directory}" COMPILE_DEFINITIONS)
+	if (_definitions)
+		list (APPEND _configDefinitions ${_definitions})
+	endif()
+	get_directory_property(_definitions DIRECTORY "${_directory}" COMPILE_DEFINITIONS_${_upperConfig})
+	if (_definitions)
+		list (APPEND _configDefinitions ${_definitions})
+	endif()
+	# target compile definitions
+	get_target_property(_definitions ${_target} COMPILE_DEFINITIONS)
+	if (_definitions)
+		list (APPEND _configDefinitions ${_definitions})
+	endif()
+	get_target_property(_definitions ${_target} COMPILE_DEFINITIONS_${_upperConfig})
+	if (_definitions)
+		list (APPEND _configDefinitions ${_definitions})
+	endif()
+	# parse additional compile definitions from target compile flags
+	# and don't look at directory compile definitions, which we already handled
+	set (_targetFlags "")
+	cotire_get_target_compile_flags("${_config}" "${_language}" "" "${_target}" _targetFlags)
+	cotire_filter_compile_flags("${_language}" "D" _definitions _ignore ${_targetFlags})
+	if (_definitions)
+		list (APPEND _configDefinitions ${_definitions})
+	endif()
+	list (REMOVE_DUPLICATES _configDefinitions)
+	if (COTIRE_DEBUG AND _configDefinitions)
+		message (STATUS "Target ${_target} compile definitions ${_configDefinitions}")
+	endif()
+	set (${_definitionsVar} ${_configDefinitions} PARENT_SCOPE)
+endfunction()
+
+function (cotire_get_target_compiler_flags _config _language _directory _target _compilerFlagsVar)
+	# parse target compile flags omitting compile definitions and include directives
+	set (_targetFlags "")
+	cotire_get_target_compile_flags("${_config}" "${_language}" "${_directory}" "${_target}" _targetFlags)
+	set (_compilerFlags "")
+	cotire_filter_compile_flags("${_language}" "[ID]" _ignore _compilerFlags ${_targetFlags})
+	if (COTIRE_DEBUG AND _compilerFlags)
+		message (STATUS "Target ${_target} compiler flags ${_compilerFlags}")
+	endif()
+	set (${_compilerFlagsVar} ${_compilerFlags} PARENT_SCOPE)
+endfunction()
+
+function (cotire_add_sys_root_paths _pathsVar)
+	if (APPLE)
+		if (CMAKE_OSX_SYSROOT AND CMAKE_${_language}_HAS_ISYSROOT)
+			foreach (_path IN LISTS ${_pathsVar})
+				if (IS_ABSOLUTE "${_path}")
+					get_filename_component(_path "${CMAKE_OSX_SYSROOT}/${_path}" ABSOLUTE)
+					if (EXISTS "${_path}")
+						list (APPEND ${_pathsVar} "${_path}")
+					endif()
+				endif()
+			endforeach()
+		endif()
+	endif()
+	set (${_pathsVar} ${${_pathsVar}} PARENT_SCOPE)
+	if (COTIRE_DEBUG)
+		message (STATUS "${_pathsVar}=${${_pathsVar}}")
+	endif()
+endfunction()
+
+function (cotire_get_source_extra_properties _sourceFile _pattern _resultVar)
+	set (_extraProperties ${ARGN})
+	set (_result "")
+	if (_extraProperties)
+		list (FIND _extraProperties "${_sourceFile}" _index)
+		if (_index GREATER -1)
+			math (EXPR _index "${_index} + 1")
+			list (LENGTH _extraProperties _len)
+			math (EXPR _len "${_len} - 1")
+			foreach (_index RANGE ${_index} ${_len})
+				list (GET _extraProperties ${_index} _value)
+				if ("${_value}" MATCHES "${_pattern}")
+					list (APPEND _result "${_value}")
+				else()
+					break()
+				endif()
+			endforeach()
+		endif()
+	endif()
+	set (${_resultVar} ${_result} PARENT_SCOPE)
+endfunction()
+
+function (cotire_get_source_compile_definitions _config _language _sourceFile _definitionsVar)
+	set (_compileDefinitions "")
+	if (NOT CMAKE_SCRIPT_MODE_FILE)
+		string (TOUPPER "${_config}" _upperConfig)
+		get_source_file_property(_definitions "${_sourceFile}" COMPILE_DEFINITIONS)
+		if (_definitions)
+			list (APPEND _compileDefinitions ${_definitions})
+		endif()
+		get_source_file_property(_definitions "${_sourceFile}" COMPILE_DEFINITIONS_${_upperConfig})
+		if (_definitions)
+			list (APPEND _compileDefinitions ${_definitions})
+		endif()
+	endif()
+	cotire_get_source_extra_properties("${_sourceFile}" "^[a-zA-Z0-9_]+(=.*)?$" _definitions ${ARGN})
+	if (_definitions)
+		list (APPEND _compileDefinitions ${_definitions})
+	endif()
+	if (COTIRE_DEBUG AND _compileDefinitions)
+		message (STATUS "Source ${_sourceFile} compile definitions ${_compileDefinitions}")
+	endif()
+	set (${_definitionsVar} ${_compileDefinitions} PARENT_SCOPE)
+endfunction()
+
+function (cotire_get_source_files_compile_definitions _config _language _definitionsVar)
+	set (_configDefinitions "")
+	foreach (_sourceFile ${ARGN})
+		cotire_get_source_compile_definitions("${_config}" "${_language}" "${_sourceFile}" _sourceDefinitions)
+		if (_sourceDefinitions)
+			list (APPEND _configDefinitions "${_sourceFile}" ${_sourceDefinitions} "-")
+		endif()
+	endforeach()
+	set (${_definitionsVar} ${_configDefinitions} PARENT_SCOPE)
+endfunction()
+
+function (cotire_get_source_undefs _sourceFile _property _sourceUndefsVar)
+	set (_sourceUndefs "")
+	if (NOT CMAKE_SCRIPT_MODE_FILE)
+		get_source_file_property(_undefs "${_sourceFile}" ${_property})
+		if (_undefs)
+			list (APPEND _sourceUndefs ${_undefs})
+		endif()
+	endif()
+	cotire_get_source_extra_properties("${_sourceFile}" "^[a-zA-Z0-9_]+$" _undefs ${ARGN})
+	if (_undefs)
+		list (APPEND _sourceUndefs ${_undefs})
+	endif()
+	if (COTIRE_DEBUG AND _sourceUndefs)
+		message (STATUS "Source ${_sourceFile} ${_property} undefs ${_sourceUndefs}")
+	endif()
+	set (${_sourceUndefsVar} ${_sourceUndefs} PARENT_SCOPE)
+endfunction()
+
+function (cotire_get_source_files_undefs _property _sourceUndefsVar)
+	set (_sourceUndefs "")
+	foreach (_sourceFile ${ARGN})
+		cotire_get_source_undefs("${_sourceFile}" ${_property} _undefs)
+		if (_undefs)
+			list (APPEND _sourceUndefs "${_sourceFile}" ${_undefs} "-")
+		endif()
+	endforeach()
+	set (${_sourceUndefsVar} ${_sourceUndefs} PARENT_SCOPE)
+endfunction()
+
+macro (cotire_set_cmd_to_prologue _cmdVar)
+	set (${_cmdVar} "${CMAKE_COMMAND}")
+	if (COTIRE_DEBUG)
+		list (APPEND ${_cmdVar} "--warn-uninitialized")
+	endif()
+	list (APPEND ${_cmdVar} "-DCOTIRE_BUILD_TYPE:STRING=$<CONFIGURATION>")
+	if (COTIRE_VERBOSE)
+		list (APPEND ${_cmdVar} "-DCOTIRE_VERBOSE:BOOL=ON")
+	elseif("${CMAKE_GENERATOR}" MATCHES "Makefiles")
+		list (APPEND ${_cmdVar} "-DCOTIRE_VERBOSE:BOOL=$(VERBOSE)")
+	endif()
+endmacro()
+
+function (cotire_init_compile_cmd _cmdVar _language _compilerExe _compilerArg1)
+	if (NOT _compilerExe)
+		set (_compilerExe "${CMAKE_${_language}_COMPILER}")
+	endif()
+	if (NOT _compilerArg1)
+		set (_compilerArg1 ${CMAKE_${_language}_COMPILER_ARG1})
+	endif()
+	string (STRIP "${_compilerArg1}" _compilerArg1)
+	set (${_cmdVar} "${_compilerExe}" ${_compilerArg1} PARENT_SCOPE)
+endfunction()
+
+macro (cotire_add_definitions_to_cmd _cmdVar _language)
+	foreach (_definition ${ARGN})
+		if (WIN32 AND CMAKE_${_language}_COMPILER_ID MATCHES "MSVC|Intel")
+			list (APPEND ${_cmdVar} "/D${_definition}")
+		else()
+			list (APPEND ${_cmdVar} "-D${_definition}")
+		endif()
+	endforeach()
+endmacro()
+
+macro (cotire_add_includes_to_cmd _cmdVar _language)
+	foreach (_include ${ARGN})
+		if (WIN32 AND CMAKE_${_language}_COMPILER_ID MATCHES "MSVC|Intel")
+			file (TO_NATIVE_PATH "${_include}" _include)
+			list (APPEND ${_cmdVar} "/I${_include}")
+		else()
+			list (APPEND ${_cmdVar} "-I${_include}")
+		endif()
+	endforeach()
+endmacro()
+
+macro (cotire_add_compile_flags_to_cmd _cmdVar)
+	foreach (_flag ${ARGN})
+		list (APPEND ${_cmdVar} "${_flag}")
+	endforeach()
+endmacro()
+
+function (cotire_check_file_up_to_date _fileIsUpToDateVar _file)
+	set (${_fileIsUpToDateVar} FALSE PARENT_SCOPE)
+	set (_triggerFile "")
+	foreach (_dependencyFile ${ARGN})
+		if (EXISTS "${_dependencyFile}" AND "${_dependencyFile}" IS_NEWER_THAN "${_file}")
+			set (_triggerFile "${_dependencyFile}")
+			break()
+		endif()
+	endforeach()
+	get_filename_component(_fileName "${_file}" NAME)
+	if (EXISTS "${_file}")
+		if (_triggerFile)
+			if (COTIRE_VERBOSE)
+				message (STATUS "${_fileName} update triggered by ${_triggerFile} change.")
+			endif()
+		else()
+			if (COTIRE_VERBOSE)
+				message (STATUS "${_fileName} is up-to-date.")
+			endif()
+			set (${_fileIsUpToDateVar} TRUE PARENT_SCOPE)
+		endif()
+	else()
+		if (COTIRE_VERBOSE)
+			message (STATUS "${_fileName} does not exist yet.")
+		endif()
+	endif()
+endfunction()
+
+macro (cotire_find_closest_relative_path _headerFile _includeDirs _relPathVar)
+	set (${_relPathVar} "")
+	foreach (_includeDir ${_includeDirs})
+		if (IS_DIRECTORY "${_includeDir}")
+			file (RELATIVE_PATH _relPath "${_includeDir}" "${_headerFile}")
+			if (NOT IS_ABSOLUTE "${_relPath}" AND NOT "${_relPath}" MATCHES "^\\.\\.")
+				string (LENGTH "${${_relPathVar}}" _closestLen)
+				string (LENGTH "${_relPath}" _relLen)
+				if (_closestLen EQUAL 0 OR _relLen LESS _closestLen)
+					set (${_relPathVar} "${_relPath}")
+				endif()
+			endif()
+		elseif ("${_includeDir}" STREQUAL "${_headerFile}")
+			# if path matches exactly, return short non-empty string
+			set (${_relPathVar} "1")
+			break()
+		endif()
+	endforeach()
+endmacro()
+
+macro (cotire_check_header_file_location _headerFile _insideIncudeDirs _outsideIncudeDirs _headerIsInside)
+	# check header path against ignored and honored include directories
+	cotire_find_closest_relative_path("${_headerFile}" "${_insideIncudeDirs}" _insideRelPath)
+	if (_insideRelPath)
+		# header is inside, but could be become outside if there is a shorter outside match
+		cotire_find_closest_relative_path("${_headerFile}" "${_outsideIncudeDirs}" _outsideRelPath)
+		if (_outsideRelPath)
+			string (LENGTH "${_insideRelPath}" _insideRelPathLen)
+			string (LENGTH "${_outsideRelPath}" _outsideRelPathLen)
+			if (_outsideRelPathLen LESS _insideRelPathLen)
+				set (${_headerIsInside} FALSE)
+			else()
+				set (${_headerIsInside} TRUE)
+			endif()
+		else()
+			set (${_headerIsInside} TRUE)
+		endif()
+	else()
+		# header is outside
+		set (${_headerIsInside} FALSE)
+	endif()
+endmacro()
+
+macro (cotire_check_ignore_header_file_path _headerFile _headerIsIgnoredVar)
+	if (NOT EXISTS "${_headerFile}")
+		set (${_headerIsIgnoredVar} TRUE)
+	elseif (IS_DIRECTORY "${_headerFile}")
+		set (${_headerIsIgnoredVar} TRUE)
+	elseif ("${_headerFile}" MATCHES "\\.\\.|[_-]fixed" AND "${_headerFile}" MATCHES "\\.h$")
+		# heuristic: ignore C headers with embedded parent directory references or "-fixed" or "_fixed" in path
+		# these often stem from using GCC #include_next tricks, which may break the precompiled header compilation
+		# with the error message "error: no include path in which to search for header.h"
+		set (${_headerIsIgnoredVar} TRUE)
+	else()
+		set (${_headerIsIgnoredVar} FALSE)
+	endif()
+endmacro()
+
+macro (cotire_check_ignore_header_file_ext _headerFile _ignoreExtensionsVar _headerIsIgnoredVar)
+	# check header file extension
+	cotire_get_source_file_extension("${_headerFile}" _headerFileExt)
+	set (${_headerIsIgnoredVar} FALSE)
+	if (_headerFileExt)
+		list (FIND ${_ignoreExtensionsVar} "${_headerFileExt}" _index)
+		if (_index GREATER -1)
+			set (${_headerIsIgnoredVar} TRUE)
+		endif()
+	endif()
+endmacro()
+
+macro (cotire_parse_line _line _headerFileVar _headerDepthVar)
+	if (MSVC)
+		# cl.exe /showIncludes output looks different depending on the language pack used, e.g.:
+		# English: "Note: including file:   C:\directory\file"
+		# German: "Hinweis: Einlesen der Datei:   C:\directory\file"
+		# We use a very general regular expression, relying on the presence of the : characters
+		if ("${_line}" MATCHES ":( +)([^:]+:[^:]+)$")
+			# Visual Studio compiler output
+			string (LENGTH "${CMAKE_MATCH_1}" ${_headerDepthVar})
+			get_filename_component(${_headerFileVar} "${CMAKE_MATCH_2}" ABSOLUTE)
+		else()
+			set (${_headerFileVar} "")
+			set (${_headerDepthVar} 0)
+		endif()
+	else()
+		if ("${_line}" MATCHES "^(\\.+) (.*)$")
+			# GCC like output
+			string (LENGTH "${CMAKE_MATCH_1}" ${_headerDepthVar})
+			if (IS_ABSOLUTE "${CMAKE_MATCH_2}")
+				set (${_headerFileVar} "${CMAKE_MATCH_2}")
+			else()
+				get_filename_component(${_headerFileVar} "${CMAKE_MATCH_2}" REALPATH)
+			endif()
+		else()
+			set (${_headerFileVar} "")
+			set (${_headerDepthVar} 0)
+		endif()
+	endif()
+endmacro()
+
+function (cotire_parse_includes _language _scanOutput _ignoredIncudeDirs _honoredIncudeDirs _ignoredExtensions _selectedIncludesVar _unparsedLinesVar)
+	if (WIN32)
+		# prevent CMake macro invocation errors due to backslash characters in Windows paths
+		string (REPLACE "\\" "/" _scanOutput "${_scanOutput}")
+	endif()
+	# canonize slashes
+	string (REPLACE "//" "/" _scanOutput "${_scanOutput}")
+	# prevent semicolon from being interpreted as a line separator
+	string (REPLACE ";" "\\;" _scanOutput "${_scanOutput}")
+	# then separate lines
+	string (REGEX REPLACE "\n" ";" _scanOutput "${_scanOutput}")
+	list (LENGTH _scanOutput _len)
+	# remove duplicate lines to speed up parsing
+	list (REMOVE_DUPLICATES _scanOutput)
+	list (LENGTH _scanOutput _uniqueLen)
+	if (COTIRE_VERBOSE)
+		message (STATUS "Scanning ${_uniqueLen} unique lines of ${_len} for includes")
+		if (_ignoredExtensions)
+			message (STATUS "Ignored extensions: ${_ignoredExtensions}")
+		endif()
+		if (_ignoredIncudeDirs)
+			message (STATUS "Ignored paths: ${_ignoredIncudeDirs}")
+		endif()
+		if (_honoredIncudeDirs)
+			message (STATUS "Included paths: ${_honoredIncudeDirs}")
+		endif()
+	endif()
+	set (_sourceFiles ${ARGN})
+	set (_selectedIncludes "")
+	set (_unparsedLines "")
+	# stack keeps track of inside/outside project status of processed header files
+	set (_headerIsInsideStack "")
+	foreach (_line IN LISTS _scanOutput)
+		if (_line)
+			cotire_parse_line("${_line}" _headerFile _headerDepth)
+			if (_headerFile)
+				cotire_check_header_file_location("${_headerFile}" "${_ignoredIncudeDirs}" "${_honoredIncudeDirs}" _headerIsInside)
+				if (COTIRE_DEBUG)
+					message (STATUS "${_headerDepth}: ${_headerFile} ${_headerIsInside}")
+				endif()
+				# update stack
+				list (LENGTH _headerIsInsideStack _stackLen)
+				if (_headerDepth GREATER _stackLen)
+					math (EXPR _stackLen "${_stackLen} + 1")
+					foreach (_index RANGE ${_stackLen} ${_headerDepth})
+						list (APPEND _headerIsInsideStack ${_headerIsInside})
+					endforeach()
+				else()
+					foreach (_index RANGE ${_headerDepth} ${_stackLen})
+						list (REMOVE_AT _headerIsInsideStack -1)
+					endforeach()
+					list (APPEND _headerIsInsideStack ${_headerIsInside})
+				endif()
+				if (COTIRE_DEBUG)
+					message (STATUS "${_headerIsInsideStack}")
+				endif()
+				# header is a candidate if it is outside project
+				if (NOT _headerIsInside)
+					# get parent header file's inside/outside status
+					if (_headerDepth GREATER 1)
+						math (EXPR _index "${_headerDepth} - 2")
+						list (GET _headerIsInsideStack ${_index} _parentHeaderIsInside)
+					else()
+						set (_parentHeaderIsInside TRUE)
+					endif()
+					# select header file if parent header file is inside project
+					# (e.g., a project header file that includes a standard header file)
+					if (_parentHeaderIsInside)
+						cotire_check_ignore_header_file_path("${_headerFile}" _headerIsIgnored)
+						if (NOT _headerIsIgnored)
+							cotire_check_ignore_header_file_ext("${_headerFile}" _ignoredExtensions _headerIsIgnored)
+							if (NOT _headerIsIgnored)
+								list (APPEND _selectedIncludes "${_headerFile}")
+							else()
+								# fix header's inside status on stack, it is ignored by extension now
+								list (REMOVE_AT _headerIsInsideStack -1)
+								list (APPEND _headerIsInsideStack TRUE)
+							endif()
+						endif()
+						if (COTIRE_DEBUG)
+							message (STATUS "${_headerFile} ${_ignoredExtensions} ${_headerIsIgnored}")
+						endif()
+					endif()
+				endif()
+			else()
+				if (MSVC)
+					# for cl.exe do not keep unparsed lines which solely consist of a source file name
+					string (FIND "${_sourceFiles}" "${_line}" _index)
+					if (_index LESS 0)
+						list (APPEND _unparsedLines "${_line}")
+					endif()
+				else()
+					list (APPEND _unparsedLines "${_line}")
+				endif()
+			endif()
+		endif()
+	endforeach()
+	list (REMOVE_DUPLICATES _selectedIncludes)
+	set (${_selectedIncludesVar} ${_selectedIncludes} PARENT_SCOPE)
+	set (${_unparsedLinesVar} ${_unparsedLines} PARENT_SCOPE)
+endfunction()
+
+function (cotire_scan_includes _includesVar)
+	set(_options "")
+	set(_oneValueArgs COMPILER_ID COMPILER_EXECUTABLE COMPILER_VERSION LANGUAGE UNPARSED_LINES)
+	set(_multiValueArgs COMPILE_DEFINITIONS COMPILE_FLAGS INCLUDE_DIRECTORIES IGNORE_PATH INCLUDE_PATH IGNORE_EXTENSIONS)
+	cmake_parse_arguments(_option "${_options}" "${_oneValueArgs}" "${_multiValueArgs}" ${ARGN})
+	set (_sourceFiles ${_option_UNPARSED_ARGUMENTS})
+	if (NOT _option_LANGUAGE)
+		set (_option_LANGUAGE "CXX")
+	endif()
+	if (NOT _option_COMPILER_ID)
+		set (_option_COMPILER_ID "${CMAKE_${_option_LANGUAGE}_ID}")
+	endif()
+	set (_cmd "${_option_COMPILER_EXECUTABLE}" ${_option_COMPILER_ARG1})
+	cotire_init_compile_cmd(_cmd "${_option_LANGUAGE}" "${_option_COMPILER_EXECUTABLE}" "${_option_COMPILER_ARG1}")
+	cotire_add_definitions_to_cmd(_cmd "${_option_LANGUAGE}" ${_option_COMPILE_DEFINITIONS})
+	cotire_add_compile_flags_to_cmd(_cmd ${_option_COMPILE_FLAGS})
+	cotire_add_includes_to_cmd(_cmd "${_option_LANGUAGE}" ${_option_INCLUDE_DIRECTORIES})
+	cotire_add_makedep_flags("${_option_LANGUAGE}" "${_option_COMPILER_ID}" "${_option_COMPILER_VERSION}" _cmd)
+	# only consider existing source files for scanning
+	set (_existingSourceFiles "")
+	foreach (_sourceFile ${_sourceFiles})
+		if (EXISTS "${_sourceFile}")
+			list (APPEND _existingSourceFiles "${_sourceFile}")
+		endif()
+	endforeach()
+	if (NOT _existingSourceFiles)
+		set (${_includesVar} "" PARENT_SCOPE)
+		return()
+	endif()
+	list (APPEND _cmd ${_existingSourceFiles})
+	if (COTIRE_VERBOSE)
+		message (STATUS "execute_process: ${_cmd}")
+	endif()
+	if (_option_COMPILER_ID MATCHES "MSVC")
+		if (COTIRE_DEBUG)
+			message (STATUS "clearing VS_UNICODE_OUTPUT")
+		endif()
+		# cl.exe messes with the output streams unless the environment variable VS_UNICODE_OUTPUT is cleared
+		unset (ENV{VS_UNICODE_OUTPUT})
+	endif()
+	execute_process(COMMAND ${_cmd} WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
+		RESULT_VARIABLE _result OUTPUT_QUIET ERROR_VARIABLE _output)
+	if (_result)
+		message (STATUS "Result ${_result} scanning includes of ${_existingSourceFiles}.")
+	endif()
+	cotire_parse_includes(
+		"${_option_LANGUAGE}" "${_output}"
+		"${_option_IGNORE_PATH}" "${_option_INCLUDE_PATH}"
+		"${_option_IGNORE_EXTENSIONS}"
+		_includes _unparsedLines
+		${_sourceFiles})
+	set (${_includesVar} ${_includes} PARENT_SCOPE)
+	if (_option_UNPARSED_LINES)
+		set (${_option_UNPARSED_LINES} ${_unparsedLines} PARENT_SCOPE)
+	endif()
+endfunction()
+
+macro (cotire_append_undefs _contentsVar)
+	set (_undefs ${ARGN})
+	if (_undefs)
+		list (REMOVE_DUPLICATES _undefs)
+		foreach (_definition ${_undefs})
+			list (APPEND ${_contentsVar} "#undef ${_definition}")
+		endforeach()
+	endif()
+endmacro()
+
+macro (cotire_comment_str _language _commentText _commentVar)
+	if ("${_language}" STREQUAL "CMAKE")
+		set (${_commentVar} "# ${_commentText}")
+	else()
+		set (${_commentVar} "/* ${_commentText} */")
+	endif()
+endmacro()
+
+function (cotire_write_file _language _file _contents _force)
+	get_filename_component(_moduleName "${COTIRE_CMAKE_MODULE_FILE}" NAME)
+	cotire_comment_str("${_language}" "${_moduleName} ${COTIRE_CMAKE_MODULE_VERSION} generated file" _header1)
+	cotire_comment_str("${_language}" "${_file}" _header2)
+	set (_contents "${_header1}\n${_header2}\n${_contents}")
+	if (COTIRE_DEBUG)
+		message (STATUS "${_contents}")
+	endif()
+	if (_force OR NOT EXISTS "${_file}")
+		file (WRITE "${_file}" "${_contents}")
+	else()
+		file (READ "${_file}" _oldContents)
+		if (NOT "${_oldContents}" STREQUAL "${_contents}")
+			file (WRITE "${_file}" "${_contents}")
+		else()
+			if (COTIRE_DEBUG)
+				message (STATUS "${_file} unchanged")
+			endif()
+		endif()
+	endif()
+endfunction()
+
+function (cotire_generate_unity_source _unityFile)
+	set(_options "")
+	set(_oneValueArgs LANGUAGE)
+	set(_multiValueArgs
+		DEPENDS SOURCES_COMPILE_DEFINITIONS
+		PRE_UNDEFS SOURCES_PRE_UNDEFS POST_UNDEFS SOURCES_POST_UNDEFS PROLOGUE EPILOGUE)
+	cmake_parse_arguments(_option "${_options}" "${_oneValueArgs}" "${_multiValueArgs}" ${ARGN})
+	if (_option_DEPENDS)
+		cotire_check_file_up_to_date(_unityFileIsUpToDate "${_unityFile}" ${_option_DEPENDS})
+		if (_unityFileIsUpToDate)
+			return()
+		endif()
+	endif()
+	set (_sourceFiles ${_option_UNPARSED_ARGUMENTS})
+	if (NOT _option_PRE_UNDEFS)
+		set (_option_PRE_UNDEFS "")
+	endif()
+	if (NOT _option_SOURCES_PRE_UNDEFS)
+		set (_option_SOURCES_PRE_UNDEFS "")
+	endif()
+	if (NOT _option_POST_UNDEFS)
+		set (_option_POST_UNDEFS "")
+	endif()
+	if (NOT _option_SOURCES_POST_UNDEFS)
+		set (_option_SOURCES_POST_UNDEFS "")
+	endif()
+	set (_contents "")
+	if (_option_PROLOGUE)
+		list (APPEND _contents ${_option_PROLOGUE})
+	endif()
+	if (_option_LANGUAGE AND _sourceFiles)
+		if ("${_option_LANGUAGE}" STREQUAL "CXX")
+			list (APPEND _contents "#ifdef __cplusplus")
+		elseif ("${_option_LANGUAGE}" STREQUAL "C")
+			list (APPEND _contents "#ifndef __cplusplus")
+		endif()
+	endif()
+	set (_compileUndefinitions "")
+	foreach (_sourceFile ${_sourceFiles})
+		cotire_get_source_compile_definitions(
+			"${_option_CONFIGURATION}" "${_option_LANGUAGE}" "${_sourceFile}" _compileDefinitions
+			${_option_SOURCES_COMPILE_DEFINITIONS})
+		cotire_get_source_undefs("${_sourceFile}" COTIRE_UNITY_SOURCE_PRE_UNDEFS _sourcePreUndefs ${_option_SOURCES_PRE_UNDEFS})
+		cotire_get_source_undefs("${_sourceFile}" COTIRE_UNITY_SOURCE_POST_UNDEFS _sourcePostUndefs ${_option_SOURCES_POST_UNDEFS})
+		if (_option_PRE_UNDEFS)
+			list (APPEND _compileUndefinitions ${_option_PRE_UNDEFS})
+		endif()
+		if (_sourcePreUndefs)
+			list (APPEND _compileUndefinitions ${_sourcePreUndefs})
+		endif()
+		if (_compileUndefinitions)
+			cotire_append_undefs(_contents ${_compileUndefinitions})
+			set (_compileUndefinitions "")
+		endif()
+		if (_sourcePostUndefs)
+			list (APPEND _compileUndefinitions ${_sourcePostUndefs})
+		endif()
+		if (_option_POST_UNDEFS)
+			list (APPEND _compileUndefinitions ${_option_POST_UNDEFS})
+		endif()
+		foreach (_definition ${_compileDefinitions})
+			if ("${_definition}" MATCHES "^([a-zA-Z0-9_]+)=(.+)$")
+				list (APPEND _contents "#define ${CMAKE_MATCH_1} ${CMAKE_MATCH_2}")
+				list (INSERT _compileUndefinitions 0 "${CMAKE_MATCH_1}")
+			else()
+				list (APPEND _contents "#define ${_definition}")
+				list (INSERT _compileUndefinitions 0 "${_definition}")
+			endif()
+		endforeach()
+		get_filename_component(_sourceFile "${_sourceFile}" ABSOLUTE)
+		if (WIN32)
+			file (TO_NATIVE_PATH "${_sourceFile}" _sourceFile)
+		endif()
+		list (APPEND _contents "#include \"${_sourceFile}\"")
+	endforeach()
+	if (_compileUndefinitions)
+		cotire_append_undefs(_contents ${_compileUndefinitions})
+		set (_compileUndefinitions "")
+	endif()
+	if (_option_LANGUAGE AND _sourceFiles)
+		list (APPEND _contents "#endif")
+	endif()
+	if (_option_EPILOGUE)
+		list (APPEND _contents ${_option_EPILOGUE})
+	endif()
+	list (APPEND _contents "")
+	string (REPLACE ";" "\n" _contents "${_contents}")
+	if (COTIRE_VERBOSE)
+		message ("${_contents}")
+	endif()
+	cotire_write_file("${_option_LANGUAGE}" "${_unityFile}" "${_contents}" TRUE)
+endfunction()
+
+function (cotire_generate_prefix_header _prefixFile)
+	set(_options "")
+	set(_oneValueArgs LANGUAGE COMPILER_EXECUTABLE COMPILER_ID COMPILER_VERSION)
+	set(_multiValueArgs DEPENDS COMPILE_DEFINITIONS COMPILE_FLAGS
+		INCLUDE_DIRECTORIES IGNORE_PATH INCLUDE_PATH IGNORE_EXTENSIONS)
+	cmake_parse_arguments(_option "${_options}" "${_oneValueArgs}" "${_multiValueArgs}" ${ARGN})
+	if (_option_DEPENDS)
+		cotire_check_file_up_to_date(_prefixFileIsUpToDate "${_prefixFile}" ${_option_DEPENDS})
+		if (_prefixFileIsUpToDate)
+			return()
+		endif()
+	endif()
+	set (_epilogue "")
+	if (_option_COMPILER_ID MATCHES "Intel")
+		# Intel compiler requires hdrstop pragma to stop generating PCH file
+		set (_epilogue "#pragma hdrstop")
+	endif()
+	set (_sourceFiles ${_option_UNPARSED_ARGUMENTS})
+	cotire_scan_includes(_selectedHeaders ${_sourceFiles}
+		LANGUAGE "${_option_LANGUAGE}"
+		COMPILER_EXECUTABLE "${_option_COMPILER_EXECUTABLE}"
+		COMPILER_ID "${_option_COMPILER_ID}"
+		COMPILER_VERSION "${_option_COMPILER_VERSION}"
+		COMPILE_DEFINITIONS ${_option_COMPILE_DEFINITIONS}
+		COMPILE_FLAGS ${_option_COMPILE_FLAGS}
+		INCLUDE_DIRECTORIES ${_option_INCLUDE_DIRECTORIES}
+		IGNORE_PATH ${_option_IGNORE_PATH}
+		INCLUDE_PATH ${_option_INCLUDE_PATH}
+		IGNORE_EXTENSIONS ${_option_IGNORE_EXTENSIONS}
+		UNPARSED_LINES _unparsedLines)
+	cotire_generate_unity_source("${_prefixFile}" EPILOGUE ${_epilogue} LANGUAGE "${_option_LANGUAGE}" ${_selectedHeaders})
+	set (_unparsedLinesFile "${_prefixFile}.log")
+	if (_unparsedLines)
+		if (COTIRE_VERBOSE OR NOT _selectedHeaders)
+			list (LENGTH _unparsedLines _skippedLineCount)
+			file (RELATIVE_PATH _unparsedLinesFileRelPath "${CMAKE_BINARY_DIR}" "${_unparsedLinesFile}")
+			message (STATUS "${_skippedLineCount} line(s) skipped, see ${_unparsedLinesFileRelPath}")
+		endif()
+		string (REPLACE ";" "\n" _unparsedLines "${_unparsedLines}")
+	endif()
+	file (WRITE "${_unparsedLinesFile}" "${_unparsedLines}\n")
+endfunction()
+
+function (cotire_add_makedep_flags _language _compilerID _compilerVersion _flagsVar)
+	set (_flags ${${_flagsVar}})
+	if (_compilerID MATCHES "MSVC")
+		# cl.exe options used
+		# /nologo suppresses display of sign-on banner
+		# /TC treat all files named on the command line as C source files
+		# /TP treat all files named on the command line as C++ source files
+		# /EP preprocess to stdout without #line directives
+		# /showIncludes list include files
+		set (_sourceFileTypeC "/TC")
+		set (_sourceFileTypeCXX "/TP")
+		if (_flags)
+			# append to list
+			list (APPEND _flags /nologo "${_sourceFileType${_language}}" /EP /showIncludes)
+		else()
+			# return as a flag string
+			set (_flags "${_sourceFileType${_language}} /EP /showIncludes")
+		endif()
+	elseif (_compilerID MATCHES "GNU")
+		# GCC options used
+		# -H print the name of each header file used
+		# -E invoke preprocessor
+		# -fdirectives-only do not expand macros, requires GCC >= 4.3
+		if (_flags)
+			# append to list
+			list (APPEND _flags -H -E)
+			if (NOT "${_compilerVersion}" VERSION_LESS "4.3.0")
+				list (APPEND _flags "-fdirectives-only")
+			endif()
+		else()
+			# return as a flag string
+			set (_flags "-H -E")
+			if (NOT "${_compilerVersion}" VERSION_LESS "4.3.0")
+				set (_flags "${_flags} -fdirectives-only")
+			endif()
+		endif()
+	elseif (_compilerID MATCHES "Clang")
+		# Clang options used
+		# -H print the name of each header file used
+		# -E invoke preprocessor
+		if (_flags)
+			# append to list
+			list (APPEND _flags -H -E)
+		else()
+			# return as a flag string
+			set (_flags "-H -E")
+		endif()
+	elseif (_compilerID MATCHES "Intel")
+		if (WIN32)
+			# Windows Intel options used
+			# /nologo do not display compiler version information
+			# /QH display the include file order
+			# /EP preprocess to stdout, omitting #line directives
+			# /TC process all source or unrecognized file types as C source files
+			# /TP process all source or unrecognized file types as C++ source files
+			set (_sourceFileTypeC "/TC")
+			set (_sourceFileTypeCXX "/TP")
+			if (_flags)
+				# append to list
+				list (APPEND _flags /nologo "${_sourceFileType${_language}}" /EP /QH)
+			else()
+				# return as a flag string
+				set (_flags "${_sourceFileType${_language}} /EP /QH")
+			endif()
+		else()
+			# Linux / Mac OS X Intel options used
+			# -H print the name of each header file used
+			# -EP preprocess to stdout, omitting #line directives
+			# -Kc++ process all source or unrecognized file types as C++ source files
+			if (_flags)
+				# append to list
+				if ("${_language}" STREQUAL "CXX")
+					list (APPEND _flags -Kc++)
+				endif()
+				list (APPEND _flags -H -EP)
+			else()
+				# return as a flag string
+				if ("${_language}" STREQUAL "CXX")
+					set (_flags "-Kc++ ")
+				endif()
+				set (_flags "${_flags}-H -EP")
+			endif()
+		endif()
+	else()
+		message (FATAL_ERROR "Unsupported ${_language} compiler ${_compilerID} version ${_compilerVersion}.")
+	endif()
+	set (${_flagsVar} ${_flags} PARENT_SCOPE)
+endfunction()
+
+function (cotire_add_pch_compilation_flags _language _compilerID _compilerVersion _prefixFile _pchFile _hostFile _flagsVar)
+	set (_flags ${${_flagsVar}})
+	if (_compilerID MATCHES "MSVC")
+		file (TO_NATIVE_PATH "${_prefixFile}" _prefixFileNative)
+		file (TO_NATIVE_PATH "${_pchFile}" _pchFileNative)
+		file (TO_NATIVE_PATH "${_hostFile}" _hostFileNative)
+		# cl.exe options used
+		# /Yc creates a precompiled header file
+		# /Fp specifies precompiled header binary file name
+		# /FI forces inclusion of file
+		# /TC treat all files named on the command line as C source files
+		# /TP treat all files named on the command line as C++ source files
+		# /Zs syntax check only
+		set (_sourceFileTypeC "/TC")
+		set (_sourceFileTypeCXX "/TP")
+		if (_flags)
+			# append to list
+			list (APPEND _flags /nologo "${_sourceFileType${_language}}"
+				"/Yc${_prefixFileNative}" "/Fp${_pchFileNative}" "/FI${_prefixFileNative}" /Zs "${_hostFileNative}")
+		else()
+			# return as a flag string
+			set (_flags "/Yc\"${_prefixFileNative}\" /Fp\"${_pchFileNative}\" /FI\"${_prefixFileNative}\"")
+		endif()
+	elseif (_compilerID MATCHES "GNU|Clang")
+		# GCC / Clang options used
+		# -x specify the source language
+		# -c compile but do not link
+		# -o place output in file
+		set (_xLanguage_C "c-header")
+		set (_xLanguage_CXX "c++-header")
+		if (_flags)
+			# append to list
+			list (APPEND _flags "-x" "${_xLanguage_${_language}}" "-c" "${_prefixFile}" -o "${_pchFile}")
+		else()
+			# return as a flag string
+			set (_flags "-x ${_xLanguage_${_language}} -c \"${_prefixFile}\" -o \"${_pchFile}\"")
+		endif()
+	elseif (_compilerID MATCHES "Intel")
+		if (WIN32)
+			file (TO_NATIVE_PATH "${_prefixFile}" _prefixFileNative)
+			file (TO_NATIVE_PATH "${_pchFile}" _pchFileNative)
+			file (TO_NATIVE_PATH "${_hostFile}" _hostFileNative)
+			# Windows Intel options used
+			# /nologo do not display compiler version information
+			# /Yc create a precompiled header (PCH) file
+			# /Fp specify a path or file name for precompiled header files
+			# /FI tells the preprocessor to include a specified file name as the header file
+			# /TC process all source or unrecognized file types as C source files
+			# /TP process all source or unrecognized file types as C++ source files
+			# /Zs syntax check only
+			# /Wpch-messages enable diagnostics related to pre-compiled headers (requires Intel XE 2013 Update 2)
+			set (_sourceFileTypeC "/TC")
+			set (_sourceFileTypeCXX "/TP")
+			if (_flags)
+				# append to list
+				list (APPEND _flags /nologo "${_sourceFileType${_language}}"
+					"/Yc" "/Fp${_pchFileNative}" "/FI${_prefixFileNative}" /Zs "${_hostFileNative}")
+				if (NOT "${_compilerVersion}" VERSION_LESS "13.1.0")
+					list (APPEND _flags "/Wpch-messages")
+				endif()
+			else()
+				# return as a flag string
+				set (_flags "/Yc /Fp\"${_pchFileNative}\" /FI\"${_prefixFileNative}\"")
+				if (NOT "${_compilerVersion}" VERSION_LESS "13.1.0")
+					set (_flags "${_flags} /Wpch-messages")
+				endif()
+			endif()
+		else()
+			# Linux / Mac OS X Intel options used
+			# -pch-dir location for precompiled header files
+			# -pch-create name of the precompiled header (PCH) to create
+			# -Kc++ process all source or unrecognized file types as C++ source files
+			# -fsyntax-only check only for correct syntax
+			# -Wpch-messages enable diagnostics related to pre-compiled headers (requires Intel XE 2013 Update 2)
+			get_filename_component(_pchDir "${_pchFile}" PATH)
+			get_filename_component(_pchName "${_pchFile}" NAME)
+			set (_xLanguage_C "c-header")
+			set (_xLanguage_CXX "c++-header")
+			if (_flags)
+				# append to list
+				if ("${_language}" STREQUAL "CXX")
+					list (APPEND _flags -Kc++)
+				endif()
+				list (APPEND _flags "-include" "${_prefixFile}" "-pch-dir" "${_pchDir}" "-pch-create" "${_pchName}" "-fsyntax-only" "${_hostFile}")
+				if (NOT "${_compilerVersion}" VERSION_LESS "13.1.0")
+					list (APPEND _flags "-Wpch-messages")
+				endif()
+			else()
+				# return as a flag string
+				set (_flags "-include \"${_prefixFile}\" -pch-dir \"${_pchDir}\" -pch-create \"${_pchName}\"")
+				if (NOT "${_compilerVersion}" VERSION_LESS "13.1.0")
+					set (_flags "${_flags} -Wpch-messages")
+				endif()
+			endif()
+		endif()
+	else()
+		message (FATAL_ERROR "Unsupported ${_language} compiler ${_compilerID} version ${_compilerVersion}.")
+	endif()
+	set (${_flagsVar} ${_flags} PARENT_SCOPE)
+endfunction()
+
+function (cotire_add_pch_inclusion_flags _language _compilerID _compilerVersion _prefixFile _pchFile _flagsVar)
+	set (_flags ${${_flagsVar}})
+	if (_compilerID MATCHES "MSVC")
+		file (TO_NATIVE_PATH "${_prefixFile}" _prefixFileNative)
+		file (TO_NATIVE_PATH "${_pchFile}" _pchFileNative)
+		# cl.exe options used
+		# /Yu uses a precompiled header file during build
+		# /Fp specifies precompiled header binary file name
+		# /FI forces inclusion of file
+		if (_flags)
+			# append to list
+			list (APPEND _flags "/Yu${_prefixFileNative}" "/Fp${_pchFileNative}" "/FI${_prefixFileNative}")
+		else()
+			# return as a flag string
+			set (_flags "/Yu\"${_prefixFileNative}\" /Fp\"${_pchFileNative}\" /FI\"${_prefixFileNative}\"")
+		endif()
+	elseif (_compilerID MATCHES "GNU")
+		# GCC options used
+		# -include process include file as the first line of the primary source file
+		# -Winvalid-pch warns if precompiled header is found but cannot be used
+		if (_flags)
+			# append to list
+			list (APPEND _flags "-include" "${_prefixFile}" "-Winvalid-pch")
+		else()
+			# return as a flag string
+			set (_flags "-include \"${_prefixFile}\" -Winvalid-pch")
+		endif()
+	elseif (_compilerID MATCHES "Clang")
+		# Clang options used
+		# -include process include file as the first line of the primary source file
+		# -Qunused-arguments don't emit warning for unused driver arguments
+		if (_flags)
+			# append to list
+			list (APPEND _flags "-include" "${_prefixFile}" "-Qunused-arguments")
+		else()
+			# return as a flag string
+			set (_flags "-include \"${_prefixFile}\" -Qunused-arguments")
+		endif()
+	elseif (_compilerID MATCHES "Intel")
+		if (WIN32)
+			file (TO_NATIVE_PATH "${_prefixFile}" _prefixFileNative)
+			file (TO_NATIVE_PATH "${_pchFile}" _pchFileNative)
+			# Windows Intel options used
+			# /Yu use a precompiled header (PCH) file
+			# /Fp specify a path or file name for precompiled header files
+			# /FI tells the preprocessor to include a specified file name as the header file
+			# /Wpch-messages enable diagnostics related to pre-compiled headers (requires Intel XE 2013 Update 2)
+			if (_flags)
+				# append to list
+				list (APPEND _flags "/Yu" "/Fp${_pchFileNative}" "/FI${_prefixFileNative}")
+				if (NOT "${_compilerVersion}" VERSION_LESS "13.1.0")
+					list (APPEND _flags "/Wpch-messages")
+				endif()
+			else()
+				# return as a flag string
+				set (_flags "/Yu /Fp\"${_pchFileNative}\" /FI\"${_prefixFileNative}\"")
+				if (NOT "${_compilerVersion}" VERSION_LESS "13.1.0")
+					set (_flags "${_flags} /Wpch-messages")
+				endif()
+			endif()
+		else()
+			# Linux / Mac OS X Intel options used
+			# -pch-dir location for precompiled header files
+			# -pch-use name of the precompiled header (PCH) to use
+			# -include process include file as the first line of the primary source file
+			# -Wpch-messages enable diagnostics related to pre-compiled headers (requires Intel XE 2013 Update 2)
+			get_filename_component(_pchDir "${_pchFile}" PATH)
+			get_filename_component(_pchName "${_pchFile}" NAME)
+			if (_flags)
+				# append to list
+				list (APPEND _flags "-include" "${_prefixFile}" "-pch-dir" "${_pchDir}" "-pch-use" "${_pchName}")
+				if (NOT "${_compilerVersion}" VERSION_LESS "13.1.0")
+					list (APPEND _flags "-Wpch-messages")
+				endif()
+			else()
+				# return as a flag string
+				set (_flags "-include \"${_prefixFile}\" -pch-dir \"${_pchDir}\" -pch-use \"${_pchName}\"")
+				if (NOT "${_compilerVersion}" VERSION_LESS "13.1.0")
+					set (_flags "${_flags} -Wpch-messages")
+				endif()
+			endif()
+		endif()
+	else()
+		message (FATAL_ERROR "Unsupported ${_language} compiler ${_compilerID} version ${_compilerVersion}.")
+	endif()
+	set (${_flagsVar} ${_flags} PARENT_SCOPE)
+endfunction()
+
+function (cotire_precompile_prefix_header _prefixFile _pchFile _hostFile)
+	set(_options "")
+	set(_oneValueArgs COMPILER_EXECUTABLE COMPILER_ID COMPILER_VERSION LANGUAGE)
+	set(_multiValueArgs COMPILE_DEFINITIONS COMPILE_FLAGS INCLUDE_DIRECTORIES)
+	cmake_parse_arguments(_option "${_options}" "${_oneValueArgs}" "${_multiValueArgs}" ${ARGN})
+	if (NOT _option_LANGUAGE)
+		set (_option_LANGUAGE "CXX")
+	endif()
+	if (NOT _option_COMPILER_ID)
+		set (_option_COMPILER_ID "${CMAKE_${_option_LANGUAGE}_ID}")
+	endif()
+	cotire_init_compile_cmd(_cmd "${_option_LANGUAGE}" "${_option_COMPILER_EXECUTABLE}" "${_option_COMPILER_ARG1}")
+	cotire_add_definitions_to_cmd(_cmd "${_option_LANGUAGE}" ${_option_COMPILE_DEFINITIONS})
+	cotire_add_compile_flags_to_cmd(_cmd ${_option_COMPILE_FLAGS})
+	cotire_add_includes_to_cmd(_cmd "${_option_LANGUAGE}" ${_option_INCLUDE_DIRECTORIES})
+	cotire_add_pch_compilation_flags(
+		"${_option_LANGUAGE}" "${_option_COMPILER_ID}" "${_option_COMPILER_VERSION}"
+		"${_prefixFile}" "${_pchFile}" "${_hostFile}" _cmd)
+	if (COTIRE_VERBOSE)
+		message (STATUS "execute_process: ${_cmd}")
+	endif()
+	if (_option_COMPILER_ID MATCHES "MSVC")
+		if (COTIRE_DEBUG)
+			message (STATUS "clearing VS_UNICODE_OUTPUT")
+		endif()
+		# cl.exe messes with the output streams unless the environment variable VS_UNICODE_OUTPUT is cleared
+		unset (ENV{VS_UNICODE_OUTPUT})
+	endif()
+	execute_process(COMMAND ${_cmd} WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" RESULT_VARIABLE _result)
+	if (_result)
+		message (FATAL_ERROR "Error ${_result} precompiling ${_prefixFile}.")
+	endif()
+endfunction()
+
+function (cotire_check_precompiled_header_support _language _targetSourceDir _target _msgVar)
+	set (_unsupportedCompiler
+		"Precompiled headers not supported for ${_language} compiler ${CMAKE_${_language}_COMPILER_ID}")
+	if (CMAKE_${_language}_COMPILER_ID MATCHES "MSVC")
+		# supported since Visual Studio C++ 6.0
+		# and CMake does not support an earlier version
+		set (${_msgVar} "" PARENT_SCOPE)
+	elseif (CMAKE_${_language}_COMPILER_ID MATCHES "GNU")
+		# GCC PCH support requires version >= 3.4
+		cotire_determine_compiler_version("${_language}" COTIRE_${_language}_COMPILER)
+		if ("${COTIRE_${_language}_COMPILER_VERSION}" MATCHES ".+" AND
+			"${COTIRE_${_language}_COMPILER_VERSION}" VERSION_LESS "3.4.0")
+			set (${_msgVar} "${_unsupportedCompiler} version ${COTIRE_${_language}_COMPILER_VERSION}." PARENT_SCOPE)
+		else()
+			set (${_msgVar} "" PARENT_SCOPE)
+		endif()
+	elseif (CMAKE_${_language}_COMPILER_ID MATCHES "Clang")
+		# all Clang versions have PCH support
+		set (${_msgVar} "" PARENT_SCOPE)
+	elseif (CMAKE_${_language}_COMPILER_ID MATCHES "Intel")
+		# Intel PCH support requires version >= 8.0.0
+		cotire_determine_compiler_version("${_language}" COTIRE_${_language}_COMPILER)
+		if ("${COTIRE_${_language}_COMPILER_VERSION}" MATCHES ".+" AND
+			"${COTIRE_${_language}_COMPILER_VERSION}" VERSION_LESS "8.0.0")
+			set (${_msgVar} "${_unsupportedCompiler} version ${COTIRE_${_language}_COMPILER_VERSION}." PARENT_SCOPE)
+		else()
+			set (${_msgVar} "" PARENT_SCOPE)
+		endif()
+	else()
+		set (${_msgVar} "${_unsupportedCompiler}." PARENT_SCOPE)
+	endif()
+	if (APPLE)
+		# PCH compilation not supported by GCC / Clang for multi-architecture builds (e.g., i386, x86_64)
+		if (CMAKE_CONFIGURATION_TYPES)
+			set (_configs ${CMAKE_CONFIGURATION_TYPES})
+		elseif (CMAKE_BUILD_TYPE)
+			set (_configs ${CMAKE_BUILD_TYPE})
+		else()
+			set (_configs "None")
+		endif()
+		foreach (_config ${_configs})
+			set (_targetFlags "")
+			cotire_get_target_compile_flags("${_config}" "${_language}" "${_targetSourceDir}" "${_target}" _targetFlags)
+			cotire_filter_compile_flags("${_language}" "arch" _architectures _ignore ${_targetFlags})
+			list (LENGTH _architectures _numberOfArchitectures)
+			if (_numberOfArchitectures GREATER 1)
+				string (REPLACE ";" ", " _architectureStr "${_architectures}")
+				set (${_msgVar}
+					"Precompiled headers not supported on Darwin for multi-architecture builds (${_architectureStr})."
+					PARENT_SCOPE)
+				break()
+			endif()
+		endforeach()
+	endif()
+endfunction()
+
+macro (cotire_get_intermediate_dir _cotireDir)
+	get_filename_component(${_cotireDir} "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/${COTIRE_INTDIR}" ABSOLUTE)
+endmacro()
+
+macro (cotire_setup_file_extension_variables)
+	set (_unityFileExt_C ".c")
+	set (_unityFileExt_CXX ".cxx")
+	set (_prefixFileExt_C ".h")
+	set (_prefixFileExt_CXX ".hxx")
+endmacro()
+
+function (cotire_make_single_unity_source_file_path _language _target _unityFileVar)
+	cotire_setup_file_extension_variables()
+	if (NOT DEFINED _unityFileExt_${_language})
+		set (${_unityFileVar} "" PARENT_SCOPE)
+		return()
+	endif()
+	set (_unityFileBaseName "${_target}_${_language}${COTIRE_UNITY_SOURCE_FILENAME_SUFFIX}")
+	set (_unityFileName "${_unityFileBaseName}${_unityFileExt_${_language}}")
+	cotire_get_intermediate_dir(_baseDir)
+	set (_unityFile "${_baseDir}/${_unityFileName}")
+	set (${_unityFileVar} "${_unityFile}" PARENT_SCOPE)
+	if (COTIRE_DEBUG)
+		message(STATUS "${_unityFile}")
+	endif()
+endfunction()
+
+function (cotire_make_unity_source_file_paths _language _target _maxIncludes _unityFilesVar)
+	cotire_setup_file_extension_variables()
+	if (NOT DEFINED _unityFileExt_${_language})
+		set (${_unityFileVar} "" PARENT_SCOPE)
+		return()
+	endif()
+	set (_unityFileBaseName "${_target}_${_language}${COTIRE_UNITY_SOURCE_FILENAME_SUFFIX}")
+	cotire_get_intermediate_dir(_baseDir)
+	set (_startIndex 0)
+	set (_index 0)
+	set (_unityFiles "")
+	set (_sourceFiles ${ARGN})
+	foreach (_sourceFile ${_sourceFiles})
+		get_source_file_property(_startNew "${_sourceFile}" COTIRE_START_NEW_UNITY_SOURCE)
+		math (EXPR _unityFileCount "${_index} - ${_startIndex}")
+		if (_startNew OR (_maxIncludes GREATER 0 AND NOT _unityFileCount LESS _maxIncludes))
+			if (_index GREATER 0)
+				# start new unity file segment
+				math (EXPR _endIndex "${_index} - 1")
+				set (_unityFileName "${_unityFileBaseName}_${_startIndex}_${_endIndex}${_unityFileExt_${_language}}")
+				list (APPEND _unityFiles "${_baseDir}/${_unityFileName}")
+			endif()
+			set (_startIndex ${_index})
+		endif()
+		math (EXPR _index "${_index} + 1")
+	endforeach()
+	list (LENGTH _sourceFiles _numberOfSources)
+	if (_startIndex EQUAL 0)
+		# there is only a single unity file
+		cotire_make_single_unity_source_file_path(${_language} ${_target} _unityFiles)
+	elseif (_startIndex LESS _numberOfSources)
+		# end with final unity file segment
+		math (EXPR _endIndex "${_index} - 1")
+		set (_unityFileName "${_unityFileBaseName}_${_startIndex}_${_endIndex}${_unityFileExt_${_language}}")
+		list (APPEND _unityFiles "${_baseDir}/${_unityFileName}")
+	endif()
+	set (${_unityFilesVar} ${_unityFiles} PARENT_SCOPE)
+	if (COTIRE_DEBUG)
+		message(STATUS "${_unityFiles}")
+	endif()
+endfunction()
+
+function (cotire_unity_to_prefix_file_path _language _target _unityFile _prefixFileVar)
+	cotire_setup_file_extension_variables()
+	if (NOT DEFINED _unityFileExt_${_language})
+		set (${_prefixFileVar} "" PARENT_SCOPE)
+		return()
+	endif()
+	set (_unityFileBaseName "${_target}_${_language}${COTIRE_UNITY_SOURCE_FILENAME_SUFFIX}")
+	set (_prefixFileBaseName "${_target}_${_language}${COTIRE_PREFIX_HEADER_FILENAME_SUFFIX}")
+	string (REPLACE "${_unityFileBaseName}" "${_prefixFileBaseName}" _prefixFile "${_unityFile}")
+	string (REGEX REPLACE "${_unityFileExt_${_language}}$" "${_prefixFileExt_${_language}}" _prefixFile "${_prefixFile}")
+	set (${_prefixFileVar} "${_prefixFile}" PARENT_SCOPE)
+endfunction()
+
+function (cotire_make_prefix_file_name _language _target _prefixFileBaseNameVar _prefixFileNameVar)
+	cotire_setup_file_extension_variables()
+	if (NOT _language)
+		set (_prefixFileBaseName "${_target}${COTIRE_PREFIX_HEADER_FILENAME_SUFFIX}")
+		set (_prefixFileName "${_prefixFileBaseName}${_prefixFileExt_C}")
+	elseif (DEFINED _prefixFileExt_${_language})
+		set (_prefixFileBaseName "${_target}_${_language}${COTIRE_PREFIX_HEADER_FILENAME_SUFFIX}")
+		set (_prefixFileName "${_prefixFileBaseName}${_prefixFileExt_${_language}}")
+	else()
+		set (_prefixFileBaseName "")
+		set (_prefixFileName "")
+	endif()
+	set (${_prefixFileBaseNameVar} "${_prefixFileBaseName}" PARENT_SCOPE)
+	set (${_prefixFileNameVar} "${_prefixFileName}" PARENT_SCOPE)
+endfunction()
+
+function (cotire_make_prefix_file_path _language _target _prefixFileVar)
+	cotire_make_prefix_file_name("${_language}" "${_target}" _prefixFileBaseName _prefixFileName)
+	set (${_prefixFileVar} "" PARENT_SCOPE)
+	if (_prefixFileName)
+		if (NOT _language)
+			set (_language "C")
+		endif()
+		if (MSVC OR CMAKE_${_language}_COMPILER_ID MATCHES "GNU|Clang|Intel")
+			cotire_get_intermediate_dir(_baseDir)
+			set (${_prefixFileVar} "${_baseDir}/${_prefixFileName}" PARENT_SCOPE)
+		endif()
+	endif()
+endfunction()
+
+function (cotire_make_pch_file_path _language _targetSourceDir _target _pchFileVar)
+	cotire_make_prefix_file_name("${_language}" "${_target}" _prefixFileBaseName _prefixFileName)
+	set (${_pchFileVar} "" PARENT_SCOPE)
+	if (_prefixFileBaseName AND _prefixFileName)
+		cotire_check_precompiled_header_support("${_language}" "${_targetSourceDir}" "${_target}" _msg)
+		if (NOT _msg)
+			if (XCODE)
+				# For Xcode, we completely hand off the compilation of the prefix header to the IDE
+				return()
+			endif()
+			cotire_get_intermediate_dir(_baseDir)
+			if (CMAKE_${_language}_COMPILER_ID MATCHES "MSVC")
+				# MSVC uses the extension .pch added to the prefix header base name
+				set (${_pchFileVar} "${_baseDir}/${_prefixFileBaseName}.pch" PARENT_SCOPE)
+			elseif (CMAKE_${_language}_COMPILER_ID MATCHES "GNU|Clang")
+				# GCC / Clang look for a precompiled header corresponding to the prefix header with the extension .gch appended
+				set (${_pchFileVar} "${_baseDir}/${_prefixFileName}.gch" PARENT_SCOPE)
+			elseif (CMAKE_${_language}_COMPILER_ID MATCHES "Intel")
+				# Intel uses the extension .pchi added to the prefix header base name
+				set (${_pchFileVar} "${_baseDir}/${_prefixFileBaseName}.pchi" PARENT_SCOPE)
+			endif()
+		endif()
+	endif()
+endfunction()
+
+function (cotire_select_unity_source_files _unityFile _sourcesVar)
+	set (_sourceFiles ${ARGN})
+	if (_sourceFiles AND "${_unityFile}" MATCHES "${COTIRE_UNITY_SOURCE_FILENAME_SUFFIX}_([0-9]+)_([0-9]+)")
+		set (_startIndex ${CMAKE_MATCH_1})
+		set (_endIndex ${CMAKE_MATCH_2})
+		list (LENGTH _sourceFiles _numberOfSources)
+		if (NOT _startIndex LESS _numberOfSources)
+			math (EXPR _startIndex "${_numberOfSources} - 1")
+		endif()
+		if (NOT _endIndex LESS _numberOfSources)
+			math (EXPR _endIndex "${_numberOfSources} - 1")
+		endif()
+		set (_files "")
+		foreach (_index RANGE ${_startIndex} ${_endIndex})
+			list (GET _sourceFiles ${_index} _file)
+			list (APPEND _files "${_file}")
+		endforeach()
+	else()
+		set (_files ${_sourceFiles})
+	endif()
+	set (${_sourcesVar} ${_files} PARENT_SCOPE)
+endfunction()
+
+function (cotire_get_unity_source_dependencies _language _target _dependencySourcesVar)
+	set (_dependencySources "")
+	# depend on target's generated source files
+	cotire_get_objects_with_property_on(_generatedSources GENERATED SOURCE ${ARGN})
+	if (_generatedSources)
+		# but omit all generated source files that have the COTIRE_EXCLUDED property set to true
+		cotire_get_objects_with_property_on(_excludedGeneratedSources COTIRE_EXCLUDED SOURCE ${_generatedSources})
+		if (_excludedGeneratedSources)
+			list (REMOVE_ITEM _generatedSources ${_excludedGeneratedSources})
+		endif()
+		# and omit all generated source files that have the COTIRE_DEPENDENCY property set to false explicitly
+		cotire_get_objects_with_property_off(_excludedNonDependencySources COTIRE_DEPENDENCY SOURCE ${_generatedSources})
+		if (_excludedNonDependencySources)
+			list (REMOVE_ITEM _generatedSources ${_excludedNonDependencySources})
+		endif()
+		if (_generatedSources)
+			list (APPEND _dependencySources ${_generatedSources})
+		endif()
+	endif()
+	if (COTIRE_DEBUG AND _dependencySources)
+		message (STATUS "${_language} ${_target} unity source depends on ${_dependencySources}")
+	endif()
+	set (${_dependencySourcesVar} ${_dependencySources} PARENT_SCOPE)
+endfunction()
+
+function (cotire_get_prefix_header_dependencies _language _target _dependencySourcesVar)
+	# depend on target source files marked with custom COTIRE_DEPENDENCY property
+	set (_dependencySources "")
+	cotire_get_objects_with_property_on(_dependencySources COTIRE_DEPENDENCY SOURCE ${ARGN})
+	if (COTIRE_DEBUG AND _dependencySources)
+		message (STATUS "${_language} ${_target} prefix header DEPENDS ${_dependencySources}")
+	endif()
+	set (${_dependencySourcesVar} ${_dependencySources} PARENT_SCOPE)
+endfunction()
+
+function (cotire_generate_target_script _language _configurations _targetSourceDir _targetBinaryDir _target _targetScriptVar)
+	set (COTIRE_TARGET_SOURCES ${ARGN})
+	get_filename_component(_moduleName "${COTIRE_CMAKE_MODULE_FILE}" NAME)
+	set (_targetCotireScript "${CMAKE_CURRENT_BINARY_DIR}/${_target}_${_language}_${_moduleName}")
+	cotire_get_prefix_header_dependencies(${_language} ${_target} COTIRE_TARGET_PREFIX_DEPENDS ${COTIRE_TARGET_SOURCES})
+	cotire_get_unity_source_dependencies(${_language} ${_target} COTIRE_TARGET_UNITY_DEPENDS ${COTIRE_TARGET_SOURCES})
+	# set up variables to be configured
+	set (COTIRE_TARGET_LANGUAGE "${_language}")
+	cotire_determine_compiler_version("${COTIRE_TARGET_LANGUAGE}" COTIRE_${_language}_COMPILER)
+	get_target_property(COTIRE_TARGET_IGNORE_PATH ${_target} COTIRE_PREFIX_HEADER_IGNORE_PATH)
+	cotire_add_sys_root_paths(COTIRE_TARGET_IGNORE_PATH)
+	get_target_property(COTIRE_TARGET_INCLUDE_PATH ${_target} COTIRE_PREFIX_HEADER_INCLUDE_PATH)
+	cotire_add_sys_root_paths(COTIRE_TARGET_INCLUDE_PATH)
+	get_target_property(COTIRE_TARGET_PRE_UNDEFS ${_target} COTIRE_UNITY_SOURCE_PRE_UNDEFS)
+	get_target_property(COTIRE_TARGET_POST_UNDEFS ${_target} COTIRE_UNITY_SOURCE_POST_UNDEFS)
+	get_target_property(COTIRE_TARGET_MAXIMUM_NUMBER_OF_INCLUDES ${_target} COTIRE_UNITY_SOURCE_MAXIMUM_NUMBER_OF_INCLUDES)
+	cotire_get_source_files_undefs(COTIRE_UNITY_SOURCE_PRE_UNDEFS COTIRE_TARGET_SOURCES_PRE_UNDEFS ${COTIRE_TARGET_SOURCES})
+	cotire_get_source_files_undefs(COTIRE_UNITY_SOURCE_POST_UNDEFS COTIRE_TARGET_SOURCES_POST_UNDEFS ${COTIRE_TARGET_SOURCES})
+	set (COTIRE_TARGET_CONFIGURATION_TYPES "${_configurations}")
+	foreach (_config ${_configurations})
+		string (TOUPPER "${_config}" _upperConfig)
+		cotire_get_target_include_directories(
+			"${_config}" "${_language}" "${_targetSourceDir}" "${_targetBinaryDir}" "${_target}" COTIRE_TARGET_INCLUDE_DIRECTORIES_${_upperConfig})
+		cotire_get_target_compile_definitions(
+			"${_config}" "${_language}" "${_targetSourceDir}" "${_target}" COTIRE_TARGET_COMPILE_DEFINITIONS_${_upperConfig})
+		cotire_get_target_compiler_flags(
+			"${_config}" "${_language}" "${_targetSourceDir}" "${_target}" COTIRE_TARGET_COMPILE_FLAGS_${_upperConfig})
+		cotire_get_source_files_compile_definitions(
+			"${_config}" "${_language}" COTIRE_TARGET_SOURCES_COMPILE_DEFINITIONS_${_upperConfig} ${COTIRE_TARGET_SOURCES})
+	endforeach()
+	get_cmake_property(_vars VARIABLES)
+	string (REGEX MATCHALL "COTIRE_[A-Za-z0-9_]+" _matchVars "${_vars}")
+	# remove COTIRE_VERBOSE which is passed as a CMake define on command line
+	list (REMOVE_ITEM _matchVars COTIRE_VERBOSE)
+	set (_contents "")
+	foreach (_var IN LISTS _matchVars ITEMS
+		MSVC CMAKE_GENERATOR CMAKE_BUILD_TYPE CMAKE_CONFIGURATION_TYPES
+		CMAKE_${_language}_COMPILER_ID CMAKE_${_language}_COMPILER CMAKE_${_language}_COMPILER_ARG1
+		CMAKE_${_language}_SOURCE_FILE_EXTENSIONS)
+		if (DEFINED ${_var})
+			string (REPLACE "\"" "\\\"" _value "${${_var}}")
+			set (_contents "${_contents}set (${_var} \"${_value}\")\n")
+		endif()
+	endforeach()
+	cotire_write_file("CMAKE" "${_targetCotireScript}" "${_contents}" FALSE)
+	set (${_targetScriptVar} "${_targetCotireScript}" PARENT_SCOPE)
+endfunction()
+
+function (cotire_setup_pch_file_compilation _language _targetBinaryDir _targetScript _prefixFile _pchFile)
+	set (_sourceFiles ${ARGN})
+	if (CMAKE_${_language}_COMPILER_ID MATCHES "MSVC|Intel")
+		# for Visual Studio and Intel, we attach the precompiled header compilation to the first source file
+		# the remaining files include the precompiled header, see cotire_setup_prefix_file_inclusion
+		if (_sourceFiles)
+			file (TO_NATIVE_PATH "${_prefixFile}" _prefixFileNative)
+			file (TO_NATIVE_PATH "${_pchFile}" _pchFileNative)
+			list (GET _sourceFiles 0 _hostFile)
+			set (_flags "")
+			cotire_determine_compiler_version("${_language}" COTIRE_${_language}_COMPILER)
+			cotire_add_pch_compilation_flags(
+				"${_language}" "${CMAKE_${_language}_COMPILER_ID}" "${COTIRE_${_language}_COMPILER_VERSION}"
+				"${_prefixFile}" "${_pchFile}" "${_hostFile}" _flags)
+			set_property (SOURCE ${_hostFile} APPEND_STRING PROPERTY COMPILE_FLAGS " ${_flags} ")
+			set_property (SOURCE ${_hostFile} APPEND PROPERTY OBJECT_OUTPUTS "${_pchFile}")
+			# make first source file depend on prefix header
+			set_property (SOURCE ${_hostFile} APPEND PROPERTY OBJECT_DEPENDS "${_prefixFile}")
+		endif()
+	elseif ("${CMAKE_GENERATOR}" MATCHES "Makefiles|Ninja")
+		# for makefile based generator, we add a custom command to precompile the prefix header
+		if (_targetScript)
+			cotire_set_cmd_to_prologue(_cmds)
+			list (GET _sourceFiles 0 _hostFile)
+			list (APPEND _cmds -P "${COTIRE_CMAKE_MODULE_FILE}" "precompile" "${_targetScript}" "${_prefixFile}" "${_pchFile}" "${_hostFile}")
+			file (RELATIVE_PATH _pchFileRelPath "${CMAKE_BINARY_DIR}" "${_pchFile}")
+			if (COTIRE_DEBUG)
+				message (STATUS "add_custom_command: OUTPUT ${_pchFile} ${_cmds} DEPENDS ${_prefixFile} IMPLICIT_DEPENDS ${_language} ${_prefixFile}")
+			endif()
+			set_property (SOURCE "${_pchFile}" PROPERTY GENERATED TRUE)
+			add_custom_command(OUTPUT "${_pchFile}"
+				COMMAND ${_cmds}
+				DEPENDS "${_prefixFile}"
+				IMPLICIT_DEPENDS ${_language} "${_prefixFile}"
+				WORKING_DIRECTORY "${_targetSourceDir}"
+				COMMENT "Building ${_language} precompiled header ${_pchFileRelPath}" VERBATIM)
+		endif()
+	endif()
+endfunction()
+
+function (cotire_setup_prefix_file_inclusion _language _target _wholeTarget _prefixFile _pchFile)
+	set (_sourceFiles ${ARGN})
+	if (CMAKE_${_language}_COMPILER_ID MATCHES "MSVC|Intel")
+		# for Visual Studio and Intel, we include the precompiled header in all but the first source file
+		# the first source file does the precompiled header compilation, see cotire_setup_pch_file_compilation
+		list (LENGTH _sourceFiles _numberOfSourceFiles)
+		if (_numberOfSourceFiles GREATER 1)
+			# mark sources as cotired to prevent them from being used in another cotired target
+			set_source_files_properties(${_sourceFiles} PROPERTIES COTIRE_TARGET "${_target}")
+			list (REMOVE_AT _sourceFiles 0)
+			set (_flags "")
+			cotire_determine_compiler_version("${_language}" COTIRE_${_language}_COMPILER)
+			cotire_add_pch_inclusion_flags(
+				"${_language}" "${CMAKE_${_language}_COMPILER_ID}" "${COTIRE_${_language}_COMPILER_VERSION}"
+				"${_prefixFile}" "${_pchFile}" _flags)
+			set_property (SOURCE ${_sourceFiles} APPEND_STRING PROPERTY COMPILE_FLAGS " ${_flags} ")
+			# make source files depend on precompiled header
+			set_property (SOURCE ${_sourceFiles} APPEND PROPERTY OBJECT_DEPENDS "${_pchFile}")
+		endif()
+	elseif ("${CMAKE_GENERATOR}" MATCHES "Makefiles|Ninja")
+		if (NOT _wholeTarget)
+			# for makefile based generator, we force the inclusion of the prefix header for a subset
+			# of the source files, if this is a multi-language target or has excluded files
+			set (_flags "")
+			cotire_determine_compiler_version("${_language}" COTIRE_${_language}_COMPILER)
+			cotire_add_pch_inclusion_flags(
+				"${_language}" "${CMAKE_${_language}_COMPILER_ID}" "${COTIRE_${_language}_COMPILER_VERSION}"
+				"${_prefixFile}" "${_pchFile}" _flags)
+			set_property (SOURCE ${_sourceFiles} APPEND_STRING PROPERTY COMPILE_FLAGS " ${_flags} ")
+			# mark sources as cotired to prevent them from being used in another cotired target
+			set_source_files_properties(${_sourceFiles} PROPERTIES COTIRE_TARGET "${_target}")
+		endif()
+		# make source files depend on precompiled header
+		set_property (SOURCE ${_sourceFiles} APPEND PROPERTY OBJECT_DEPENDS "${_pchFile}")
+	endif()
+endfunction()
+
+function (cotire_get_first_set_property_value _propertyValueVar _type _object)
+	set (_properties ${ARGN})
+	foreach (_property ${_properties})
+		get_property(_propertyValue ${_type} "${_object}" PROPERTY ${_property})
+		if (_propertyValue)
+			set (${_propertyValueVar} ${_propertyValue} PARENT_SCOPE)
+			return()
+		endif()
+	endforeach()
+	set (${_propertyValueVar} "" PARENT_SCOPE)
+endfunction()
+
+function (cotire_setup_combine_command _language _sourceDir _targetScript _joinedFile _cmdsVar)
+	set (_files ${ARGN})
+	set (_filesPaths "")
+	foreach (_file ${_files})
+		if (IS_ABSOLUTE "${_file}")
+			set (_filePath "${_file}")
+		else()
+			get_filename_component(_filePath "${_sourceDir}/${_file}" ABSOLUTE)
+		endif()
+		file (RELATIVE_PATH _fileRelPath "${_sourceDir}" "${_filePath}")
+		if (NOT IS_ABSOLUTE "${_fileRelPath}" AND NOT "${_fileRelPath}" MATCHES "^\\.\\.")
+			list (APPEND _filesPaths "${_fileRelPath}")
+		else()
+			list (APPEND _filesPaths "${_filePath}")
+		endif()
+	endforeach()
+	cotire_set_cmd_to_prologue(_prefixCmd)
+	list (APPEND _prefixCmd -P "${COTIRE_CMAKE_MODULE_FILE}" "combine")
+	if (_targetScript)
+		list (APPEND _prefixCmd "${_targetScript}")
+	endif()
+	list (APPEND _prefixCmd "${_joinedFile}" ${_filesPaths})
+	if (COTIRE_DEBUG)
+		message (STATUS "add_custom_command: OUTPUT ${_joinedFile} COMMAND ${_prefixCmd} DEPENDS ${_files}")
+	endif()
+	set_property (SOURCE "${_joinedFile}" PROPERTY GENERATED TRUE)
+	file (RELATIVE_PATH _joinedFileRelPath "${CMAKE_BINARY_DIR}" "${_joinedFile}")
+	get_filename_component(_joinedFileName "${_joinedFileRelPath}" NAME_WE)
+	if (_language AND _joinedFileName MATCHES "${COTIRE_UNITY_SOURCE_FILENAME_SUFFIX}$")
+		set (_comment "Generating ${_language} unity source ${_joinedFileRelPath}")
+	elseif (_language AND _joinedFileName MATCHES "${COTIRE_UNITY_SOURCE_FILENAME_SUFFIX}$")
+		set (_comment "Generating ${_language} prefix header ${_joinedFileRelPath}")
+	else()
+		set (_comment "Generating ${_joinedFileRelPath}")
+	endif()
+	add_custom_command(
+		OUTPUT "${_joinedFile}"
+		COMMAND ${_prefixCmd}
+		DEPENDS ${_files}
+		COMMENT "${_comment}"
+		WORKING_DIRECTORY "${_sourceDir}" VERBATIM)
+	list (APPEND ${_cmdsVar} COMMAND ${_prefixCmd})
+	set (${_cmdsVar} ${${_cmdsVar}} PARENT_SCOPE)
+endfunction()
+
+function (cotire_setup_target_pch_usage _languages _targetSourceDir _target _wholeTarget)
+	if (XCODE)
+		# for Xcode, we attach a pre-build action to generate the unity sources and prefix headers
+		# if necessary, we also generate a single prefix header which includes all language specific prefix headers
+		set (_prefixFiles "")
+		foreach (_language ${_languages})
+			get_property(_prefixFile TARGET ${_target} PROPERTY COTIRE_${_language}_PREFIX_HEADER)
+			if (_prefixFile)
+				list (APPEND _prefixFiles "${_prefixFile}")
+			endif()
+		endforeach()
+		set (_cmds ${ARGN})
+		list (LENGTH _prefixFiles _numberOfPrefixFiles)
+		if (_numberOfPrefixFiles GREATER 1)
+			cotire_make_prefix_file_path("" ${_target} _prefixHeader)
+			cotire_setup_combine_command("" "${_targetSourceDir}" "" "${_prefixHeader}" _cmds ${_prefixFiles})
+		else()
+			set (_prefixHeader "${_prefixFiles}")
+		endif()
+		if (COTIRE_DEBUG)
+			message (STATUS "add_custom_command: TARGET ${_target} PRE_BUILD ${_cmds}")
+		endif()
+		add_custom_command(TARGET "${_target}"
+			PRE_BUILD ${_cmds}
+			WORKING_DIRECTORY "${_targetSourceDir}"
+			COMMENT "Updating target ${_target} prefix headers" VERBATIM)
+		# make Xcode precompile the generated prefix header with ProcessPCH and ProcessPCH++
+		set_target_properties(${_target} PROPERTIES XCODE_ATTRIBUTE_GCC_PRECOMPILE_PREFIX_HEADER "YES")
+		set_target_properties(${_target} PROPERTIES XCODE_ATTRIBUTE_GCC_PREFIX_HEADER "${_prefixHeader}")
+	elseif ("${CMAKE_GENERATOR}" MATCHES "Makefiles|Ninja")
+		# for makefile based generator, we force inclusion of the prefix header for all target source files
+		# if this is a single-language target without any excluded files
+		if (_wholeTarget)
+			set (_language "${_languages}")
+			# for Visual Studio and Intel, precompiled header inclusion is always done on the source file level
+			# see cotire_setup_prefix_file_inclusion
+			if (NOT CMAKE_${_language}_COMPILER_ID MATCHES "MSVC|Intel")
+				get_property(_prefixFile TARGET ${_target} PROPERTY COTIRE_${_language}_PREFIX_HEADER)
+				get_property(_pchFile TARGET ${_target} PROPERTY COTIRE_${_language}_PRECOMPILED_HEADER)
+				set (_flags "")
+				cotire_determine_compiler_version("${_language}" COTIRE_${_language}_COMPILER)
+				cotire_add_pch_inclusion_flags(
+					"${_language}" "${CMAKE_${_language}_COMPILER_ID}" "${COTIRE_${_language}_COMPILER_VERSION}"
+					"${_prefixFile}" "${_pchFile}" _flags)
+				set_property (TARGET ${_target} APPEND_STRING PROPERTY COMPILE_FLAGS " ${_flags} ")
+			endif()
+		endif()
+	endif()
+endfunction()
+
+function (cotire_setup_unity_generation_commands _language _targetSourceDir _target _targetScript _unityFiles _cmdsVar)
+	set (_dependencySources "")
+	cotire_get_unity_source_dependencies(${_language} ${_target} _dependencySources ${ARGN})
+	foreach (_unityFile ${_unityFiles})
+		file (RELATIVE_PATH _unityFileRelPath "${CMAKE_BINARY_DIR}" "${_unityFile}")
+		set_property (SOURCE "${_unityFile}" PROPERTY GENERATED TRUE)
+		# set up compiled unity source dependencies
+		# this ensures that missing source files are generated before the unity file is compiled
+		if (COTIRE_DEBUG AND _dependencySources)
+			message (STATUS "${_unityFile} OBJECT_DEPENDS ${_dependencySources}")
+		endif()
+		if (_dependencySources)
+			set_property (SOURCE "${_unityFile}" PROPERTY OBJECT_DEPENDS ${_dependencySources})
+		endif()
+		if (WIN32 AND CMAKE_${_language}_COMPILER_ID MATCHES "MSVC|Intel")
+			# unity file compilation results in potentially huge object file, thus use /bigobj by default unter MSVC and Windows Intel
+			set_property (SOURCE "${_unityFile}" APPEND_STRING PROPERTY COMPILE_FLAGS "/bigobj")
+		endif()
+		cotire_set_cmd_to_prologue(_unityCmd)
+		list (APPEND _unityCmd -P "${COTIRE_CMAKE_MODULE_FILE}" "unity" "${_targetScript}" "${_unityFile}")
+		if (COTIRE_DEBUG)
+			message (STATUS "add_custom_command: OUTPUT ${_unityFile} COMMAND ${_unityCmd} DEPENDS ${_targetScript}")
+		endif()
+		add_custom_command(
+			OUTPUT "${_unityFile}"
+			COMMAND ${_unityCmd}
+			DEPENDS "${_targetScript}"
+			COMMENT "Generating ${_language} unity source ${_unityFileRelPath}"
+			WORKING_DIRECTORY "${_targetSourceDir}" VERBATIM)
+		list (APPEND ${_cmdsVar} COMMAND ${_unityCmd})
+	endforeach()
+	list (LENGTH _unityFiles _numberOfUnityFiles)
+	if (_numberOfUnityFiles GREATER 1)
+		# create a joint unity file from all unity file segments
+		cotire_make_single_unity_source_file_path(${_language} ${_target} _unityFile)
+		cotire_setup_combine_command(${_language} "${_targetSourceDir}" "${_targetScript}" "${_unityFile}" ${_cmdsVar} ${_unityFiles})
+	endif()
+	set (${_cmdsVar} ${${_cmdsVar}} PARENT_SCOPE)
+endfunction()
+
+function (cotire_setup_single_prefix_generation_command _language _target _targetSourceDir _targetScript _prefixFile _unityFile _cmdsVar)
+	set (_sourceFiles ${ARGN})
+	set (_dependencySources "")
+	cotire_get_prefix_header_dependencies(${_language} ${_target} _dependencySources ${_sourceFiles})
+	cotire_set_cmd_to_prologue(_prefixCmd)
+	list (APPEND _prefixCmd -P "${COTIRE_CMAKE_MODULE_FILE}" "prefix" "${_targetScript}" "${_prefixFile}" "${_unityFile}")
+	set_property (SOURCE "${_prefixFile}" PROPERTY GENERATED TRUE)
+	if (COTIRE_DEBUG)
+		message (STATUS "add_custom_command: OUTPUT ${_prefixFile} COMMAND ${_prefixCmd} DEPENDS ${_targetScript} ${_unityFile} ${_dependencySources}")
+	endif()
+	file (RELATIVE_PATH _prefixFileRelPath "${CMAKE_BINARY_DIR}" "${_prefixFile}")
+	add_custom_command(
+		OUTPUT "${_prefixFile}" "${_prefixFile}.log"
+		COMMAND ${_prefixCmd}
+		DEPENDS "${_targetScript}" "${_unityFile}" ${_dependencySources}
+		COMMENT "Generating ${_language} prefix header ${_prefixFileRelPath}"
+		WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" VERBATIM)
+	list (APPEND ${_cmdsVar} COMMAND ${_prefixCmd})
+	set (${_cmdsVar} ${${_cmdsVar}} PARENT_SCOPE)
+endfunction()
+
+function (cotire_setup_multi_prefix_generation_command _language _target _targetSourceDir _targetScript _prefixFile _unityFiles _cmdsVar)
+	set (_sourceFiles ${ARGN})
+	list (LENGTH _unityFiles _numberOfUnityFiles)
+	if (_numberOfUnityFiles GREATER 1)
+		cotire_make_single_unity_source_file_path(${_language} ${_target} _unityFile)
+		cotire_setup_single_prefix_generation_command(
+			${_language} ${_target} "${_targetSourceDir}" "${_targetScript}"
+			"${_prefixFile}" "${_unityFile}" ${_cmdsVar} ${_sourceFiles})
+	else()
+		cotire_setup_single_prefix_generation_command(
+			${_language} ${_target} "${_targetSourceDir}" "${_targetScript}"
+			"${_prefixFile}" "${_unityFiles}" ${_cmdsVar} ${_sourceFiles})
+	endif()
+	set (${_cmdsVar} ${${_cmdsVar}} PARENT_SCOPE)
+endfunction()
+
+function (cotire_init_cotire_target_properties _target)
+	get_property(_isSet TARGET ${_target} PROPERTY COTIRE_ENABLE_PRECOMPILED_HEADER SET)
+	if (NOT _isSet)
+		set_property(TARGET ${_target} PROPERTY COTIRE_ENABLE_PRECOMPILED_HEADER TRUE)
+	endif()
+	get_property(_isSet TARGET ${_target} PROPERTY COTIRE_ADD_UNITY_BUILD SET)
+	if (NOT _isSet)
+		set_property(TARGET ${_target} PROPERTY COTIRE_ADD_UNITY_BUILD TRUE)
+	endif()
+	get_property(_isSet TARGET ${_target} PROPERTY COTIRE_ADD_CLEAN SET)
+	if (NOT _isSet)
+		set_property(TARGET ${_target} PROPERTY COTIRE_ADD_CLEAN FALSE)
+	endif()
+	get_property(_isSet TARGET ${_target} PROPERTY COTIRE_PREFIX_HEADER_IGNORE_PATH SET)
+	if (NOT _isSet)
+		set_property(TARGET ${_target} PROPERTY COTIRE_PREFIX_HEADER_IGNORE_PATH "${CMAKE_SOURCE_DIR}")
+		cotire_check_is_path_relative_to("${CMAKE_BINARY_DIR}" _isRelative "${CMAKE_SOURCE_DIR}")
+		if (NOT _isRelative)
+			set_property(TARGET ${_target} APPEND PROPERTY COTIRE_PREFIX_HEADER_IGNORE_PATH "${CMAKE_BINARY_DIR}")
+		endif()
+	endif()
+	get_property(_isSet TARGET ${_target} PROPERTY COTIRE_PREFIX_HEADER_INCLUDE_PATH SET)
+	if (NOT _isSet)
+		set_property(TARGET ${_target} PROPERTY COTIRE_PREFIX_HEADER_INCLUDE_PATH "")
+	endif()
+	get_property(_isSet TARGET ${_target} PROPERTY COTIRE_UNITY_SOURCE_PRE_UNDEFS SET)
+	if (NOT _isSet)
+		set_property(TARGET ${_target} PROPERTY COTIRE_UNITY_SOURCE_PRE_UNDEFS "")
+	endif()
+	get_property(_isSet TARGET ${_target} PROPERTY COTIRE_UNITY_SOURCE_POST_UNDEFS SET)
+	if (NOT _isSet)
+		set_property(TARGET ${_target} PROPERTY COTIRE_UNITY_SOURCE_POST_UNDEFS "")
+	endif()
+	get_property(_isSet TARGET ${_target} PROPERTY COTIRE_UNITY_SOURCE_MAXIMUM_NUMBER_OF_INCLUDES SET)
+	if (NOT _isSet)
+		if (COTIRE_MAXIMUM_NUMBER_OF_UNITY_INCLUDES)
+			set_property(TARGET ${_target} PROPERTY COTIRE_UNITY_SOURCE_MAXIMUM_NUMBER_OF_INCLUDES "${COTIRE_MAXIMUM_NUMBER_OF_UNITY_INCLUDES}")
+		else()
+			set_property(TARGET ${_target} PROPERTY COTIRE_UNITY_SOURCE_MAXIMUM_NUMBER_OF_INCLUDES "")
+		endif()
+	endif()
+endfunction()
+
+function (cotire_make_target_message _target _languages _disableMsg _targetMsgVar)
+	get_target_property(_targetUsePCH ${_target} COTIRE_ENABLE_PRECOMPILED_HEADER)
+	get_target_property(_targetAddSCU ${_target} COTIRE_ADD_UNITY_BUILD)
+	string (REPLACE ";" " " _languagesStr "${_languages}")
+	string (REPLACE ";" ", " _excludedStr "${ARGN}")
+	set (_targetMsg "")
+	if (NOT _languages)
+		set (_targetMsg "Target ${_target} cannot be cotired.")
+		if (_disableMsg)
+			set (_targetMsg "${_targetMsg} ${_disableMsg}")
+		endif()
+	elseif (NOT _targetUsePCH AND NOT _targetAddSCU)
+		set (_targetMsg "${_languagesStr} target ${_target} cotired without unity build and precompiled header.")
+		if (_disableMsg)
+			set (_targetMsg "${_targetMsg} ${_disableMsg}")
+		endif()
+	elseif (NOT _targetUsePCH)
+		if (_allExcludedSourceFiles)
+			set (_targetMsg "${_languagesStr} target ${_target} cotired excluding files ${_excludedStr} without precompiled header.")
+		else()
+			set (_targetMsg "${_languagesStr} target ${_target} cotired without precompiled header.")
+		endif()
+		if (_disableMsg)
+			set (_targetMsg "${_targetMsg} ${_disableMsg}")
+		endif()
+	elseif (NOT _targetAddSCU)
+		if (_allExcludedSourceFiles)
+			set (_targetMsg "${_languagesStr} target ${_target} cotired excluding files ${_excludedStr} without unity build.")
+		else()
+			set (_targetMsg "${_languagesStr} target ${_target} cotired without unity build.")
+		endif()
+	else()
+		if (_allExcludedSourceFiles)
+			set (_targetMsg "${_languagesStr} target ${_target} cotired excluding files ${_excludedStr}.")
+		else()
+			set (_targetMsg "${_languagesStr} target ${_target} cotired.")
+		endif()
+	endif()
+	set (${_targetMsgVar} "${_targetMsg}" PARENT_SCOPE)
+endfunction()
+
+function (cotire_choose_target_languages _targetSourceDir _target _targetLanguagesVar)
+	set (_languages ${ARGN})
+	set (_allSourceFiles "")
+	set (_allExcludedSourceFiles "")
+	set (_allCotiredSourceFiles "")
+	set (_targetLanguages "")
+	get_target_property(_targetType ${_target} TYPE)
+	get_target_property(_targetSourceFiles ${_target} SOURCES)
+	get_target_property(_targetUsePCH ${_target} COTIRE_ENABLE_PRECOMPILED_HEADER)
+	get_target_property(_targetAddSCU ${_target} COTIRE_ADD_UNITY_BUILD)
+	set (_disableMsg "")
+	foreach (_language ${_languages})
+		get_target_property(_prefixHeader ${_target} COTIRE_${_language}_PREFIX_HEADER)
+		get_target_property(_unityBuildFile ${_target} COTIRE_${_language}_UNITY_SOURCE)
+		if (_prefixHeader OR _unityBuildFile)
+			message (WARNING "Target ${_target} has already been cotired.")
+			set (${_targetLanguagesVar} "" PARENT_SCOPE)
+			return()
+		endif()
+		if (_targetUsePCH AND "${_language}" STREQUAL "C" OR "${_language}" STREQUAL "CXX")
+			cotire_check_precompiled_header_support("${_language}" "${_targetSourceDir}" "${_target}" _disableMsg)
+			if (_disableMsg)
+				set (_targetUsePCH FALSE)
+			endif()
+		endif()
+		set (_sourceFiles "")
+		set (_excludedSources "")
+		set (_cotiredSources "")
+		cotire_filter_language_source_files(${_language} _sourceFiles _excludedSources _cotiredSources ${_targetSourceFiles})
+		if (_sourceFiles OR _excludedSources OR _cotiredSources)
+			list (APPEND _targetLanguages ${_language})
+		endif()
+		if (_sourceFiles)
+			list (APPEND _allSourceFiles ${_sourceFiles})
+		endif()
+		if (_excludedSources)
+			list (APPEND _allExcludedSourceFiles ${_excludedSources})
+		endif()
+		if (_cotiredSources)
+			list (APPEND _allCotiredSourceFiles ${_cotiredSources})
+		endif()
+	endforeach()
+	set (_targetMsgLevel STATUS)
+	if (NOT _targetLanguages)
+		string (REPLACE ";" " or " _languagesStr "${_languages}")
+		set (_disableMsg "No ${_languagesStr} source files.")
+		set (_targetUsePCH FALSE)
+		set (_targetAddSCU FALSE)
+	endif()
+	if (_targetUsePCH)
+		list (LENGTH _allSourceFiles _numberOfSources)
+		if (_numberOfSources LESS ${COTIRE_MINIMUM_NUMBER_OF_TARGET_SOURCES})
+			set (_disableMsg "Too few applicable sources.")
+			set (_targetUsePCH FALSE)
+		elseif (_allCotiredSourceFiles)
+			cotire_get_source_file_property_values(_cotireTargets COTIRE_TARGET ${_allCotiredSourceFiles})
+			list (REMOVE_DUPLICATES _cotireTargets)
+			string (REPLACE ";" ", " _cotireTargetsStr "${_cotireTargets}")
+			set (_disableMsg "Target sources already include a precompiled header for target(s) ${_cotireTargets}.")
+			set (_disableMsg "${_disableMsg} Set target property COTIRE_ENABLE_PRECOMPILED_HEADER to FALSE for targets ${_target},")
+			set (_disableMsg "${_disableMsg} ${_cotireTargetsStr} to get a workable build system.")
+			set (_targetMsgLevel SEND_ERROR)
+			set (_targetUsePCH FALSE)
+		elseif (XCODE AND _allExcludedSourceFiles)
+			# for Xcode, we cannot apply the precompiled header to individual sources, only to the whole target
+			set (_disableMsg "Exclusion of source files not supported for generator Xcode.")
+			set (_targetUsePCH FALSE)
+		elseif (XCODE AND "${_targetType}" STREQUAL "OBJECT_LIBRARY")
+			# for Xcode, we cannot apply the required PRE_BUILD action to generate the prefix header to an OBJECT_LIBRARY target
+			set (_disableMsg "Required PRE_BUILD action not supported for OBJECT_LIBRARY targets for generator Xcode.")
+			set (_targetUsePCH FALSE)
+		endif()
+	endif()
+	set_property(TARGET ${_target} PROPERTY COTIRE_ENABLE_PRECOMPILED_HEADER ${_targetUsePCH})
+	set_property(TARGET ${_target} PROPERTY COTIRE_ADD_UNITY_BUILD ${_targetAddSCU})
+	cotire_make_target_message(${_target} "${_targetLanguages}" "${_disableMsg}" _targetMsg ${_allExcludedSourceFiles})
+	if (_targetMsg)
+		if (NOT DEFINED COTIREMSG_${_target})
+			set (COTIREMSG_${_target} "")
+		endif()
+		if (COTIRE_VERBOSE OR NOT "${_targetMsgLevel}" STREQUAL "STATUS" OR
+			NOT "${COTIREMSG_${_target}}" STREQUAL "${_targetMsg}")
+			# cache message to avoid redundant messages on re-configure
+			set (COTIREMSG_${_target} "${_targetMsg}" CACHE INTERNAL "${_target} cotire message.")
+			message (${_targetMsgLevel} "${_targetMsg}")
+		endif()
+	endif()
+	set (${_targetLanguagesVar} ${_targetLanguages} PARENT_SCOPE)
+endfunction()
+
+function (cotire_compute_unity_max_number_of_includes _target _maxIncludesVar)
+	set (_sourceFiles ${ARGN})
+	get_target_property(_maxIncludes ${_target} COTIRE_UNITY_SOURCE_MAXIMUM_NUMBER_OF_INCLUDES)
+	if (_maxIncludes MATCHES "(-j|--parallel|--jobs) ?([0-9]*)")
+		set (_numberOfThreads "${CMAKE_MATCH_2}")
+		if (NOT _numberOfThreads)
+			# use all available cores
+			ProcessorCount(_numberOfThreads)
+		endif()
+		list (LENGTH _sourceFiles _numberOfSources)
+		math (EXPR _maxIncludes "(${_numberOfSources} + ${_numberOfThreads} - 1) / ${_numberOfThreads}")
+		# a unity source segment must not contain less than COTIRE_MINIMUM_NUMBER_OF_TARGET_SOURCES files
+		if (_maxIncludes LESS ${COTIRE_MINIMUM_NUMBER_OF_TARGET_SOURCES})
+			set (_maxIncludes ${COTIRE_MINIMUM_NUMBER_OF_TARGET_SOURCES})
+		endif()
+	elseif (NOT _maxIncludes MATCHES "[0-9]+")
+		set (_maxIncludes 0)
+	endif()
+	if (COTIRE_DEBUG)
+		message (STATUS "${_target} unity source max includes = ${_maxIncludes}")
+	endif()
+	set (${_maxIncludesVar} ${_maxIncludes} PARENT_SCOPE)
+endfunction()
+
+function (cotire_process_target_language _language _configurations _targetSourceDir _targetBinaryDir _target _wholeTargetVar _cmdsVar)
+	set (${_cmdsVar} "" PARENT_SCOPE)
+	get_target_property(_targetSourceFiles ${_target} SOURCES)
+	set (_sourceFiles "")
+	set (_excludedSources "")
+	set (_cotiredSources "")
+	cotire_filter_language_source_files(${_language} _sourceFiles _excludedSources _cotiredSources ${_targetSourceFiles})
+	if (NOT _sourceFiles AND NOT _cotiredSources)
+		return()
+	endif()
+	set (_wholeTarget ${${_wholeTargetVar}})
+	set (_cmds "")
+	# check for user provided unity source file list
+	get_property(_unitySourceFiles TARGET ${_target} PROPERTY COTIRE_${_language}_UNITY_SOURCE_INIT)
+	if (NOT _unitySourceFiles)
+		set (_unitySourceFiles ${_sourceFiles} ${_cotiredSources})
+	endif()
+	cotire_generate_target_script(
+		${_language} "${_configurations}" "${_targetSourceDir}" "${_targetBinaryDir}" ${_target} _targetScript ${_unitySourceFiles})
+	cotire_compute_unity_max_number_of_includes(${_target} _maxIncludes ${_unitySourceFiles})
+	cotire_make_unity_source_file_paths(${_language} ${_target} ${_maxIncludes} _unityFiles ${_unitySourceFiles})
+	if (NOT _unityFiles)
+		return()
+	endif()
+	cotire_setup_unity_generation_commands(
+		${_language} "${_targetSourceDir}" ${_target} "${_targetScript}" "${_unityFiles}" _cmds ${_unitySourceFiles})
+	cotire_make_prefix_file_path(${_language} ${_target} _prefixFile)
+	if (_prefixFile)
+		# check for user provided prefix header files
+		get_property(_prefixHeaderFiles TARGET ${_target} PROPERTY COTIRE_${_language}_PREFIX_HEADER_INIT)
+		if (_prefixHeaderFiles)
+			cotire_setup_combine_command(${_language} "${_targetSourceDir}" "${_targetScript}" "${_prefixFile}" _cmds ${_prefixHeaderFiles})
+		else()
+			cotire_setup_multi_prefix_generation_command(
+				${_language} ${_target} "${_targetSourceDir}" "${_targetScript}" "${_prefixFile}" "${_unityFiles}" _cmds ${_unitySourceFiles})
+		endif()
+		get_target_property(_targetUsePCH ${_target} COTIRE_ENABLE_PRECOMPILED_HEADER)
+		if (_targetUsePCH)
+			cotire_make_pch_file_path(${_language} "${_targetSourceDir}" ${_target} _pchFile)
+			if (_pchFile)
+				cotire_setup_pch_file_compilation(
+					${_language} "${_targetBinaryDir}" "${_targetScript}" "${_prefixFile}" "${_pchFile}" ${_sourceFiles})
+				if (_excludedSources)
+					set (_wholeTarget FALSE)
+				endif()
+				cotire_setup_prefix_file_inclusion(
+					${_language} ${_target} ${_wholeTarget} "${_prefixFile}" "${_pchFile}" ${_sourceFiles})
+			endif()
+		endif()
+	endif()
+	# mark target as cotired for language
+	set_property(TARGET ${_target} PROPERTY COTIRE_${_language}_UNITY_SOURCE "${_unityFiles}")
+	if (_prefixFile)
+		set_property(TARGET ${_target} PROPERTY COTIRE_${_language}_PREFIX_HEADER "${_prefixFile}")
+		if (_targetUsePCH AND _pchFile)
+			set_property(TARGET ${_target} PROPERTY COTIRE_${_language}_PRECOMPILED_HEADER "${_pchFile}")
+		endif()
+	endif()
+	set (${_wholeTargetVar} ${_wholeTarget} PARENT_SCOPE)
+	set (${_cmdsVar} ${_cmds} PARENT_SCOPE)
+endfunction()
+
+function (cotire_setup_clean_target _target)
+	set (_cleanTargetName "${_target}${COTIRE_CLEAN_TARGET_SUFFIX}")
+	if (NOT TARGET "${_cleanTargetName}")
+		cotire_set_cmd_to_prologue(_cmds)
+		get_filename_component(_outputDir "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}" ABSOLUTE)
+		list (APPEND _cmds -P "${COTIRE_CMAKE_MODULE_FILE}" "cleanup" "${_outputDir}" "${COTIRE_INTDIR}" "${_target}")
+		add_custom_target(${_cleanTargetName} COMMAND ${_cmds} WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
+			COMMENT "Cleaning up target ${_target} cotire generated files" VERBATIM)
+		cotire_init_target("${_cleanTargetName}")
+	endif()
+endfunction()
+
+function (cotire_setup_pch_target _languages _configurations _target)
+	if ("${CMAKE_GENERATOR}" MATCHES "Makefiles|Ninja")
+		# for makefile based generators, we add a custom target to trigger the generation of the cotire related files
+		set (_dependsFiles "")
+		foreach (_language ${_languages})
+			set (_props COTIRE_${_language}_PREFIX_HEADER COTIRE_${_language}_UNITY_SOURCE)
+			if (NOT CMAKE_${_language}_COMPILER_ID MATCHES "MSVC|Intel")
+				# Visual Studio and Intel only create precompiled header as a side effect
+				list (INSERT _props 0 COTIRE_${_language}_PRECOMPILED_HEADER)
+			endif()
+			cotire_get_first_set_property_value(_dependsFile TARGET ${_target} ${_props})
+			if (_dependsFile)
+				list (APPEND _dependsFiles "${_dependsFile}")
+			endif()
+		endforeach()
+		if (_dependsFiles)
+			set (_pchTargetName "${_target}${COTIRE_PCH_TARGET_SUFFIX}")
+			add_custom_target("${_pchTargetName}" DEPENDS ${_dependsFiles})
+			cotire_init_target("${_pchTargetName}")
+			cotire_add_to_pch_all_target(${_pchTargetName})
+		endif()
+	else()
+		# for other generators, we add the "clean all" target to clean up the precompiled header
+		cotire_setup_clean_all_target()
+	endif()
+endfunction()
+
+function (cotire_setup_unity_build_target _languages _configurations _targetSourceDir _target)
+	get_target_property(_unityTargetName ${_target} COTIRE_UNITY_TARGET_NAME)
+	if (NOT _unityTargetName)
+		set (_unityTargetName "${_target}${COTIRE_UNITY_BUILD_TARGET_SUFFIX}")
+	endif()
+	# determine unity target sub type
+	get_target_property(_targetType ${_target} TYPE)
+	if ("${_targetType}" STREQUAL "EXECUTABLE")
+		get_target_property(_isWin32 ${_target} WIN32_EXECUTABLE)
+		get_target_property(_isMacOSX_Bundle ${_target} MACOSX_BUNDLE)
+		if (_isWin32)
+			set (_unityTargetSubType WIN32)
+		elseif (_isMacOSX_Bundle)
+			set (_unityTargetSubType MACOSX_BUNDLE)
+		else()
+			set (_unityTargetSubType "")
+		endif()
+	elseif (_targetType MATCHES "(STATIC|SHARED|MODULE|OBJECT)_LIBRARY")
+		set (_unityTargetSubType "${CMAKE_MATCH_1}")
+	else()
+		message (WARNING "Unknown target type ${_targetType}.")
+		return()
+	endif()
+	# determine unity target sources
+	get_target_property(_targetSourceFiles ${_target} SOURCES)
+	set (_unityTargetSources ${_targetSourceFiles})
+	foreach (_language ${_languages})
+		get_property(_unityFiles TARGET ${_target} PROPERTY COTIRE_${_language}_UNITY_SOURCE)
+		if (_unityFiles)
+			# remove source files that are included in the unity source
+			set (_sourceFiles "")
+			set (_excludedSources "")
+			set (_cotiredSources "")
+			cotire_filter_language_source_files(${_language} _sourceFiles _excludedSources _cotiredSources ${_targetSourceFiles})
+			if (_sourceFiles OR _cotiredSources)
+				list (REMOVE_ITEM _unityTargetSources ${_sourceFiles} ${_cotiredSources})
+			endif()
+			# if cotire is applied to a target which has not been added in the current source dir,
+			# non-existing files cannot be referenced from the unity build target (this is a CMake restriction)
+			if (NOT "${_targetSourceDir}" STREQUAL "${CMAKE_CURRENT_SOURCE_DIR}")
+				set (_nonExistingFiles "")
+				foreach (_file ${_unityTargetSources})
+					if (NOT EXISTS "${_file}")
+						list (APPEND _nonExistingFiles "${_file}")
+					endif()
+				endforeach()
+				if (_nonExistingFiles)
+					if (COTIRE_VERBOSE)
+						message (STATUS "removing non-existing ${_nonExistingFiles} from ${_unityTargetName}")
+					endif()
+					list (REMOVE_ITEM _unityTargetSources ${_nonExistingFiles})
+				endif()
+			endif()
+			# add unity source files instead
+			list (APPEND _unityTargetSources ${_unityFiles})
+		endif()
+	endforeach()
+	if (COTIRE_DEBUG)
+		message (STATUS "add ${_targetType} ${_unityTargetName} ${_unityTargetSubType} EXCLUDE_FROM_ALL ${_unityTargetSources}")
+	endif()
+	# generate unity target
+	if ("${_targetType}" STREQUAL "EXECUTABLE")
+		add_executable(${_unityTargetName} ${_unityTargetSubType} EXCLUDE_FROM_ALL ${_unityTargetSources})
+	else()
+		add_library(${_unityTargetName} ${_unityTargetSubType} EXCLUDE_FROM_ALL ${_unityTargetSources})
+	endif()
+	set (_outputDirProperties
+		ARCHIVE_OUTPUT_DIRECTORY ARCHIVE_OUTPUT_DIRECTORY_<CONFIG>
+		LIBRARY_OUTPUT_DIRECTORY LIBRARY_OUTPUT_DIRECTORY_<CONFIG>
+		RUNTIME_OUTPUT_DIRECTORY RUNTIME_OUTPUT_DIRECTORY_<CONFIG>)
+	# copy output location properties
+	if (COTIRE_UNITY_OUTPUT_DIRECTORY)
+		set (_setDefaultOutputDir TRUE)
+		if (IS_ABSOLUTE "${COTIRE_UNITY_OUTPUT_DIRECTORY}")
+			set (_outputDir "${COTIRE_UNITY_OUTPUT_DIRECTORY}")
+		else()
+			cotrie_copy_set_properites("${_configurations}" TARGET ${_target} ${_unityTargetName} ${_outputDirProperties})
+			cotrie_resolve_config_properites("${_configurations}" _properties ${_outputDirProperties})
+			foreach (_property ${_properties})
+				get_property(_outputDir TARGET ${_target} PROPERTY ${_property})
+				if (_outputDir)
+					get_filename_component(_outputDir "${_outputDir}/${COTIRE_UNITY_OUTPUT_DIRECTORY}" ABSOLUTE)
+					set_property(TARGET ${_unityTargetName} PROPERTY ${_property} "${_outputDir}")
+					set (_setDefaultOutputDir FALSE)
+				endif()
+			endforeach()
+			if (_setDefaultOutputDir)
+				get_filename_component(_outputDir "${CMAKE_CURRENT_BINARY_DIR}/${COTIRE_UNITY_OUTPUT_DIRECTORY}" ABSOLUTE)
+			endif()
+		endif()
+		if (_setDefaultOutputDir)
+			set_target_properties(${_unityTargetName} PROPERTIES
+				ARCHIVE_OUTPUT_DIRECTORY "${_outputDir}"
+				LIBRARY_OUTPUT_DIRECTORY "${_outputDir}"
+				RUNTIME_OUTPUT_DIRECTORY "${_outputDir}")
+		endif()
+	else()
+		cotrie_copy_set_properites("${_configurations}" TARGET ${_target} ${_unityTargetName} ${_outputDirProperties})
+	endif()
+	# copy output name
+	cotrie_copy_set_properites("${_configurations}" TARGET ${_target} ${_unityTargetName}
+		ARCHIVE_OUTPUT_NAME ARCHIVE_OUTPUT_NAME_<CONFIG>
+		LIBRARY_OUTPUT_NAME LIBRARY_OUTPUT_NAME_<CONFIG>
+		OUTPUT_NAME OUTPUT_NAME_<CONFIG>
+		RUNTIME_OUTPUT_NAME RUNTIME_OUTPUT_NAME_<CONFIG>
+		PREFIX <CONFIG>_POSTFIX SUFFIX)
+	# copy compile stuff
+	cotrie_copy_set_properites("${_configurations}" TARGET ${_target} ${_unityTargetName}
+		COMPILE_DEFINITIONS COMPILE_DEFINITIONS_<CONFIG>
+		COMPILE_FLAGS Fortran_FORMAT
+		INCLUDE_DIRECTORIES
+		INTERPROCEDURAL_OPTIMIZATION INTERPROCEDURAL_OPTIMIZATION_<CONFIG>
+		POSITION_INDEPENDENT_CODE)
+	# copy link stuff
+	cotrie_copy_set_properites("${_configurations}" TARGET ${_target} ${_unityTargetName}
+		BUILD_WITH_INSTALL_RPATH INSTALL_RPATH INSTALL_RPATH_USE_LINK_PATH SKIP_BUILD_RPATH
+		LINKER_LANGUAGE LINK_DEPENDS
+		LINK_FLAGS LINK_FLAGS_<CONFIG>
+		LINK_INTERFACE_LIBRARIES LINK_INTERFACE_LIBRARIES_<CONFIG>
+		LINK_INTERFACE_MULTIPLICITY LINK_INTERFACE_MULTIPLICITY_<CONFIG>
+		LINK_SEARCH_START_STATIC LINK_SEARCH_END_STATIC
+		STATIC_LIBRARY_FLAGS STATIC_LIBRARY_FLAGS_<CONFIG>
+		NO_SONAME SOVERSION VERSION)
+	# copy Qt stuff
+	cotrie_copy_set_properites("${_configurations}" TARGET ${_target} ${_unityTargetName}
+		AUTOMOC AUTOMOC_MOC_OPTIONS)
+	# copy cmake stuff
+	cotrie_copy_set_properites("${_configurations}" TARGET ${_target} ${_unityTargetName}
+		IMPLICIT_DEPENDS_INCLUDE_TRANSFORM RULE_LAUNCH_COMPILE RULE_LAUNCH_CUSTOM RULE_LAUNCH_LINK)
+	# copy platform stuff
+	if (APPLE)
+		cotrie_copy_set_properites("${_configurations}" TARGET ${_target} ${_unityTargetName}
+			BUNDLE BUNDLE_EXTENSION FRAMEWORK INSTALL_NAME_DIR MACOSX_BUNDLE_INFO_PLIST MACOSX_FRAMEWORK_INFO_PLIST
+			OSX_ARCHITECTURES OSX_ARCHITECTURES_<CONFIG> PRIVATE_HEADER PUBLIC_HEADER RESOURCE)
+	elseif (WIN32)
+		cotrie_copy_set_properites("${_configurations}" TARGET ${_target} ${_unityTargetName}
+			GNUtoMS
+			PDB_NAME PDB_NAME_<CONFIG> PDB_OUTPUT_DIRECTORY PDB_OUTPUT_DIRECTORY_<CONFIG>
+			VS_DOTNET_REFERENCES VS_GLOBAL_KEYWORD VS_GLOBAL_PROJECT_TYPES VS_KEYWORD
+			VS_SCC_AUXPATH VS_SCC_LOCALPATH VS_SCC_PROJECTNAME VS_SCC_PROVIDER
+			VS_WINRT_EXTENSIONS VS_WINRT_REFERENCES)
+	endif()
+	# use output name from original target
+	get_target_property(_targetOutputName ${_unityTargetName} OUTPUT_NAME)
+	if (NOT _targetOutputName)
+		set_property(TARGET ${_unityTargetName} PROPERTY OUTPUT_NAME "${_target}")
+	endif()
+	# use export symbol from original target
+	cotire_get_target_export_symbol("${_target}" _defineSymbol)
+	if (_defineSymbol)
+		set_property(TARGET ${_unityTargetName} PROPERTY DEFINE_SYMBOL "${_defineSymbol}")
+		if ("${_targetType}" STREQUAL "EXECUTABLE")
+			set_property(TARGET ${_unityTargetName} PROPERTY ENABLE_EXPORTS TRUE)
+		endif()
+	endif()
+	cotire_init_target(${_unityTargetName})
+	cotire_add_to_unity_all_target(${_unityTargetName})
+	set_property(TARGET ${_target} PROPERTY COTIRE_UNITY_TARGET_NAME "${_unityTargetName}")
+endfunction(cotire_setup_unity_build_target)
+
+function (cotire_target _target)
+	set(_options "")
+	set(_oneValueArgs SOURCE_DIR BINARY_DIR)
+	set(_multiValueArgs LANGUAGES CONFIGURATIONS)
+	cmake_parse_arguments(_option "${_options}" "${_oneValueArgs}" "${_multiValueArgs}" ${ARGN})
+	if (NOT _option_SOURCE_DIR)
+		set (_option_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
+	endif()
+	if (NOT _option_BINARY_DIR)
+		set (_option_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}")
+	endif()
+	if (NOT _option_LANGUAGES)
+		get_property (_option_LANGUAGES GLOBAL PROPERTY ENABLED_LANGUAGES)
+	endif()
+	if (NOT _option_CONFIGURATIONS)
+		if (CMAKE_CONFIGURATION_TYPES)
+			set (_option_CONFIGURATIONS ${CMAKE_CONFIGURATION_TYPES})
+		elseif (CMAKE_BUILD_TYPE)
+			set (_option_CONFIGURATIONS "${CMAKE_BUILD_TYPE}")
+		else()
+			set (_option_CONFIGURATIONS "None")
+		endif()
+	endif()
+	# trivial checks
+	get_target_property(_imported ${_target} IMPORTED)
+	if (_imported)
+		message (WARNING "Imported target ${_target} cannot be cotired.")
+		return()
+	endif()
+	# check if target needs to be cotired for build type
+	# when using configuration types, the test is performed at build time
+	cotire_init_cotire_target_properties(${_target})
+	if (NOT CMAKE_CONFIGURATION_TYPES)
+		if (CMAKE_BUILD_TYPE)
+			list (FIND _option_CONFIGURATIONS "${CMAKE_BUILD_TYPE}" _index)
+		else()
+			list (FIND _option_CONFIGURATIONS "None" _index)
+		endif()
+		if (_index EQUAL -1)
+			if (COTIRE_DEBUG)
+				message (STATUS "CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} not cotired (${_option_CONFIGURATIONS})")
+			endif()
+			return()
+		endif()
+	endif()
+	# choose languages that apply to the target
+	cotire_choose_target_languages("${_option_SOURCE_DIR}" "${_target}" _targetLanguages ${_option_LANGUAGES})
+	if (NOT _targetLanguages)
+		return()
+	endif()
+	list (LENGTH _targetLanguages _numberOfLanguages)
+	if (_numberOfLanguages GREATER 1)
+		set (_wholeTarget FALSE)
+	else()
+		set (_wholeTarget TRUE)
+	endif()
+	set (_cmds "")
+	foreach (_language ${_targetLanguages})
+		cotire_process_target_language("${_language}" "${_option_CONFIGURATIONS}"
+			"${_option_SOURCE_DIR}" "${_option_BINARY_DIR}" ${_target} _wholeTarget _cmd)
+		if (_cmd)
+			list (APPEND _cmds ${_cmd})
+		endif()
+	endforeach()
+	get_target_property(_targetAddSCU ${_target} COTIRE_ADD_UNITY_BUILD)
+	if (_targetAddSCU)
+		cotire_setup_unity_build_target("${_targetLanguages}" "${_option_CONFIGURATIONS}" "${_option_SOURCE_DIR}" ${_target})
+	endif()
+	get_target_property(_targetUsePCH ${_target} COTIRE_ENABLE_PRECOMPILED_HEADER)
+	if (_targetUsePCH)
+		cotire_setup_target_pch_usage("${_targetLanguages}" "${_option_SOURCE_DIR}" ${_target} ${_wholeTarget} ${_cmds})
+		cotire_setup_pch_target("${_targetLanguages}" "${_option_CONFIGURATIONS}" ${_target})
+	endif()
+	get_target_property(_targetAddCleanTarget ${_target} COTIRE_ADD_CLEAN)
+	if (_targetAddCleanTarget)
+		cotire_setup_clean_target(${_target})
+	endif()
+endfunction()
+
+function (cotire_cleanup _binaryDir _cotireIntermediateDirName _targetName)
+	if (_targetName)
+		file (GLOB_RECURSE _cotireFiles "${_binaryDir}/${_targetName}*.*")
+	else()
+		file (GLOB_RECURSE _cotireFiles "${_binaryDir}/*.*")
+	endif()
+	# filter files in intermediate directory
+	set (_filesToRemove "")
+	foreach (_file ${_cotireFiles})
+		get_filename_component(_dir "${_file}" PATH)
+		get_filename_component(_dirName "${_dir}" NAME)
+		if ("${_dirName}" STREQUAL "${_cotireIntermediateDirName}")
+			list (APPEND _filesToRemove "${_file}")
+		endif()
+	endforeach()
+	if (_filesToRemove)
+		if (COTIRE_VERBOSE)
+			message (STATUS "removing ${_filesToRemove}")
+		endif()
+		file (REMOVE ${_filesToRemove})
+	endif()
+endfunction()
+
+function (cotire_init_target _targetName)
+	if (COTIRE_TARGETS_FOLDER)
+		set_target_properties(${_targetName} PROPERTIES FOLDER "${COTIRE_TARGETS_FOLDER}")
+	endif()
+	if (MSVC_IDE)
+		set_target_properties(${_targetName} PROPERTIES EXCLUDE_FROM_DEFAULT_BUILD TRUE)
+	endif()
+endfunction()
+
+function (cotire_add_to_pch_all_target _pchTargetName)
+	set (_targetName "${COTIRE_PCH_ALL_TARGET_NAME}")
+	if (NOT TARGET "${_targetName}")
+		add_custom_target("${_targetName}" WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" VERBATIM)
+		cotire_init_target("${_targetName}")
+	endif()
+	cotire_setup_clean_all_target()
+	add_dependencies(${_targetName} ${_pchTargetName})
+endfunction()
+
+function (cotire_add_to_unity_all_target _unityTargetName)
+	set (_targetName "${COTIRE_UNITY_BUILD_ALL_TARGET_NAME}")
+	if (NOT TARGET "${_targetName}")
+		add_custom_target("${_targetName}" WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" VERBATIM)
+		cotire_init_target("${_targetName}")
+	endif()
+	cotire_setup_clean_all_target()
+	add_dependencies(${_targetName} ${_unityTargetName})
+endfunction()
+
+function (cotire_setup_clean_all_target)
+	set (_targetName "${COTIRE_CLEAN_ALL_TARGET_NAME}")
+	if (NOT TARGET "${_targetName}")
+		cotire_set_cmd_to_prologue(_cmds)
+		list (APPEND _cmds -P "${COTIRE_CMAKE_MODULE_FILE}" "cleanup" "${CMAKE_BINARY_DIR}" "${COTIRE_INTDIR}")
+		add_custom_target(${_targetName} COMMAND ${_cmds}
+			WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" COMMENT "Cleaning up all cotire generated files" VERBATIM)
+		cotire_init_target("${_targetName}")
+	endif()
+endfunction()
+
+function (cotire)
+	set(_options "")
+	set(_oneValueArgs SOURCE_DIR BINARY_DIR)
+	set(_multiValueArgs LANGUAGES CONFIGURATIONS)
+	cmake_parse_arguments(_option "${_options}" "${_oneValueArgs}" "${_multiValueArgs}" ${ARGN})
+	set (_targets ${_option_UNPARSED_ARGUMENTS})
+	if (NOT _option_SOURCE_DIR)
+		set (_option_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
+	endif()
+	if (NOT _option_BINARY_DIR)
+		set (_option_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}")
+	endif()
+	foreach (_target ${_targets})
+		if (TARGET ${_target})
+			cotire_target(${_target} LANGUAGES ${_option_LANGUAGES} CONFIGURATIONS ${_option_CONFIGURATIONS}
+				SOURCE_DIR "${_option_SOURCE_DIR}" BINARY_DIR "${_option_BINARY_DIR}")
+		else()
+			message (WARNING "${_target} is not a target")
+		endif()
+	endforeach()
+endfunction()
+
+if (CMAKE_SCRIPT_MODE_FILE)
+
+	# cotire is being run in script mode
+	# locate -P on command args
+	set (COTIRE_ARGC -1)
+	foreach (_index RANGE ${CMAKE_ARGC})
+		if (COTIRE_ARGC GREATER -1)
+			set (COTIRE_ARGV${COTIRE_ARGC} "${CMAKE_ARGV${_index}}")
+			math (EXPR COTIRE_ARGC "${COTIRE_ARGC} + 1")
+		elseif ("${CMAKE_ARGV${_index}}" STREQUAL "-P")
+			set (COTIRE_ARGC 0)
+		endif()
+	endforeach()
+
+	# include target script if available
+	if ("${COTIRE_ARGV2}" MATCHES "\\.cmake$")
+		# the included target scripts sets up additional variables relating to the target (e.g., COTIRE_TARGET_SOURCES)
+		include("${COTIRE_ARGV2}")
+	endif()
+
+	if (COTIRE_DEBUG)
+		message (STATUS "${COTIRE_ARGV0} ${COTIRE_ARGV1} ${COTIRE_ARGV2} ${COTIRE_ARGV3} ${COTIRE_ARGV4} ${COTIRE_ARGV5}")
+	endif()
+
+	if (WIN32)
+		# for MSVC, compiler IDs may not always be set correctly
+		if (MSVC)
+			set (CMAKE_C_COMPILER_ID "MSVC")
+			set (CMAKE_CXX_COMPILER_ID "MSVC")
+		endif()
+	endif()
+
+	if (NOT COTIRE_BUILD_TYPE)
+		set (COTIRE_BUILD_TYPE "None")
+	endif()
+	string (TOUPPER "${COTIRE_BUILD_TYPE}" _upperConfig)
+	set (_includeDirs ${COTIRE_TARGET_INCLUDE_DIRECTORIES_${_upperConfig}})
+	set (_compileDefinitions ${COTIRE_TARGET_COMPILE_DEFINITIONS_${_upperConfig}})
+	set (_compileFlags ${COTIRE_TARGET_COMPILE_FLAGS_${_upperConfig}})
+	# check if target has been cotired for actual build type COTIRE_BUILD_TYPE
+	list (FIND COTIRE_TARGET_CONFIGURATION_TYPES "${COTIRE_BUILD_TYPE}" _index)
+	if (_index GREATER -1)
+		set (_sources ${COTIRE_TARGET_SOURCES})
+		set (_sourcesDefinitions ${COTIRE_TARGET_SOURCES_COMPILE_DEFINITIONS_${_upperConfig}})
+	else()
+		if (COTIRE_DEBUG)
+			message (STATUS "COTIRE_BUILD_TYPE=${COTIRE_BUILD_TYPE} not cotired (${COTIRE_TARGET_CONFIGURATION_TYPES})")
+		endif()
+		set (_sources "")
+		set (_sourcesDefinitions "")
+	endif()
+	set (_targetPreUndefs ${COTIRE_TARGET_PRE_UNDEFS})
+	set (_targetPostUndefs ${COTIRE_TARGET_POST_UNDEFS})
+	set (_sourcesPreUndefs ${COTIRE_TARGET_SOURCES_PRE_UNDEFS})
+	set (_sourcesPostUndefs ${COTIRE_TARGET_SOURCES_POST_UNDEFS})
+
+	if ("${COTIRE_ARGV1}" STREQUAL "unity")
+
+		cotire_select_unity_source_files("${COTIRE_ARGV3}" _sources ${_sources})
+		cotire_generate_unity_source(
+			"${COTIRE_ARGV3}" ${_sources}
+			LANGUAGE "${COTIRE_TARGET_LANGUAGE}"
+			DEPENDS "${COTIRE_ARGV0}" "${COTIRE_ARGV2}"
+			SOURCES_COMPILE_DEFINITIONS ${_sourcesDefinitions}
+			PRE_UNDEFS ${_targetPreUndefs}
+			POST_UNDEFS ${_targetPostUndefs}
+			SOURCES_PRE_UNDEFS ${_sourcesPreUndefs}
+			SOURCES_POST_UNDEFS ${_sourcesPostUndefs})
+
+	elseif ("${COTIRE_ARGV1}" STREQUAL "prefix")
+
+		set (_files "")
+		foreach (_index RANGE 4 ${COTIRE_ARGC})
+			if (COTIRE_ARGV${_index})
+				list (APPEND _files "${COTIRE_ARGV${_index}}")
+			endif()
+		endforeach()
+
+		cotire_generate_prefix_header(
+			"${COTIRE_ARGV3}" ${_files}
+			COMPILER_EXECUTABLE "${CMAKE_${COTIRE_TARGET_LANGUAGE}_COMPILER}"
+			COMPILER_ARG1 ${CMAKE_${COTIRE_TARGET_LANGUAGE}_COMPILER_ARG1}
+			COMPILER_ID "${CMAKE_${COTIRE_TARGET_LANGUAGE}_COMPILER_ID}"
+			COMPILER_VERSION "${COTIRE_${COTIRE_TARGET_LANGUAGE}_COMPILER_VERSION}"
+			LANGUAGE "${COTIRE_TARGET_LANGUAGE}"
+			DEPENDS "${COTIRE_ARGV0}" "${COTIRE_ARGV4}" ${COTIRE_TARGET_PREFIX_DEPENDS}
+			IGNORE_PATH "${COTIRE_TARGET_IGNORE_PATH};${COTIRE_ADDITIONAL_PREFIX_HEADER_IGNORE_PATH}"
+			INCLUDE_PATH ${COTIRE_TARGET_INCLUDE_PATH}
+			IGNORE_EXTENSIONS "${CMAKE_${COTIRE_TARGET_LANGUAGE}_SOURCE_FILE_EXTENSIONS};${COTIRE_ADDITIONAL_PREFIX_HEADER_IGNORE_EXTENSIONS}"
+			INCLUDE_DIRECTORIES ${_includeDirs}
+			COMPILE_DEFINITIONS ${_compileDefinitions}
+			COMPILE_FLAGS ${_compileFlags})
+
+	elseif ("${COTIRE_ARGV1}" STREQUAL "precompile")
+
+		set (_files "")
+		foreach (_index RANGE 5 ${COTIRE_ARGC})
+			if (COTIRE_ARGV${_index})
+				list (APPEND _files "${COTIRE_ARGV${_index}}")
+			endif()
+		endforeach()
+
+		cotire_precompile_prefix_header(
+			"${COTIRE_ARGV3}" "${COTIRE_ARGV4}" "${COTIRE_ARGV5}"
+			COMPILER_EXECUTABLE "${CMAKE_${COTIRE_TARGET_LANGUAGE}_COMPILER}"
+			COMPILER_ARG1 ${CMAKE_${COTIRE_TARGET_LANGUAGE}_COMPILER_ARG1}
+			COMPILER_ID "${CMAKE_${COTIRE_TARGET_LANGUAGE}_COMPILER_ID}"
+			COMPILER_VERSION "${COTIRE_${COTIRE_TARGET_LANGUAGE}_COMPILER_VERSION}"
+			LANGUAGE "${COTIRE_TARGET_LANGUAGE}"
+			INCLUDE_DIRECTORIES ${_includeDirs}
+			COMPILE_DEFINITIONS ${_compileDefinitions}
+			COMPILE_FLAGS ${_compileFlags})
+
+	elseif ("${COTIRE_ARGV1}" STREQUAL "combine")
+
+		if (COTIRE_TARGET_LANGUAGE)
+			set (_startIndex 3)
+		else()
+			set (_startIndex 2)
+		endif()
+		set (_files "")
+		foreach (_index RANGE ${_startIndex} ${COTIRE_ARGC})
+			if (COTIRE_ARGV${_index})
+				list (APPEND _files "${COTIRE_ARGV${_index}}")
+			endif()
+		endforeach()
+		if (COTIRE_TARGET_LANGUAGE)
+			cotire_generate_unity_source(${_files} LANGUAGE "${COTIRE_TARGET_LANGUAGE}")
+		else()
+			cotire_generate_unity_source(${_files})
+		endif()
+
+	elseif ("${COTIRE_ARGV1}" STREQUAL "cleanup")
+
+		cotire_cleanup("${COTIRE_ARGV2}" "${COTIRE_ARGV3}" "${COTIRE_ARGV4}")
+
+	else()
+		message (FATAL_ERROR "Unknown cotire command \"${COTIRE_ARGV1}\".")
+	endif()
+
+else()
+
+	# cotire is being run in include mode
+	# set up all variable and property definitions
+
+	unset (COTIRE_C_COMPILER_VERSION CACHE)
+	unset (COTIRE_CXX_COMPILER_VERSION CACHE)
+
+	if (NOT DEFINED COTIRE_DEBUG_INIT)
+		if (DEFINED COTIRE_DEBUG)
+			set (COTIRE_DEBUG_INIT ${COTIRE_DEBUG})
+		else()
+			set (COTIRE_DEBUG_INIT FALSE)
+		endif()
+	endif()
+	option (COTIRE_DEBUG "Enable cotire debugging output?" ${COTIRE_DEBUG_INIT})
+
+	if (NOT DEFINED COTIRE_VERBOSE_INIT)
+		if (DEFINED COTIRE_VERBOSE)
+			set (COTIRE_VERBOSE_INIT ${COTIRE_VERBOSE})
+		else()
+			set (COTIRE_VERBOSE_INIT FALSE)
+		endif()
+	endif()
+	option (COTIRE_VERBOSE "Enable cotire verbose output?" ${COTIRE_VERBOSE_INIT})
+
+	set (COTIRE_ADDITIONAL_PREFIX_HEADER_IGNORE_EXTENSIONS "inc;inl;ipp" CACHE STRING
+		"Ignore headers with the listed file extensions from the generated prefix header.")
+
+	set (COTIRE_ADDITIONAL_PREFIX_HEADER_IGNORE_PATH "" CACHE STRING
+		"Ignore headers from these directories when generating the prefix header.")
+
+	set (COTIRE_UNITY_SOURCE_EXCLUDE_EXTENSIONS "m;mm" CACHE STRING
+		"Ignore sources with the listed file extensions from the generated unity source.")
+
+	set (COTIRE_MINIMUM_NUMBER_OF_TARGET_SOURCES "3" CACHE STRING
+		"Minimum number of sources in target required to enable use of precompiled header.")
+
+	if (NOT DEFINED COTIRE_MAXIMUM_NUMBER_OF_UNITY_INCLUDES_INIT)
+		if (DEFINED COTIRE_MAXIMUM_NUMBER_OF_UNITY_INCLUDES)
+			set (COTIRE_MAXIMUM_NUMBER_OF_UNITY_INCLUDES_INIT ${COTIRE_MAXIMUM_NUMBER_OF_UNITY_INCLUDES})
+		elseif ("${CMAKE_GENERATOR}" MATCHES "JOM|Ninja|Visual Studio")
+			# enable parallelization for generators that run multiple jobs by default
+			set (COTIRE_MAXIMUM_NUMBER_OF_UNITY_INCLUDES_INIT "-j")
+		else()
+			set (COTIRE_MAXIMUM_NUMBER_OF_UNITY_INCLUDES_INIT "0")
+		endif()
+	endif()
+	set (COTIRE_MAXIMUM_NUMBER_OF_UNITY_INCLUDES "${COTIRE_MAXIMUM_NUMBER_OF_UNITY_INCLUDES_INIT}" CACHE STRING
+		"Maximum number of source files to include in a single unity source file.")
+
+	if (NOT COTIRE_PREFIX_HEADER_FILENAME_SUFFIX)
+		set (COTIRE_PREFIX_HEADER_FILENAME_SUFFIX "_prefix")
+	endif()
+	if (NOT COTIRE_UNITY_SOURCE_FILENAME_SUFFIX)
+		set (COTIRE_UNITY_SOURCE_FILENAME_SUFFIX "_unity")
+	endif()
+	if (NOT COTIRE_INTDIR)
+		set (COTIRE_INTDIR "cotire")
+	endif()
+	if (NOT COTIRE_PCH_ALL_TARGET_NAME)
+		set (COTIRE_PCH_ALL_TARGET_NAME "all_pch")
+	endif()
+	if (NOT COTIRE_UNITY_BUILD_ALL_TARGET_NAME)
+		set (COTIRE_UNITY_BUILD_ALL_TARGET_NAME "all_unity")
+	endif()
+	if (NOT COTIRE_CLEAN_ALL_TARGET_NAME)
+		set (COTIRE_CLEAN_ALL_TARGET_NAME "clean_cotire")
+	endif()
+	if (NOT COTIRE_CLEAN_TARGET_SUFFIX)
+		set (COTIRE_CLEAN_TARGET_SUFFIX "_clean_cotire")
+	endif()
+	if (NOT COTIRE_PCH_TARGET_SUFFIX)
+		set (COTIRE_PCH_TARGET_SUFFIX "_pch")
+	endif()
+	if (NOT COTIRE_UNITY_BUILD_TARGET_SUFFIX)
+		set (COTIRE_UNITY_BUILD_TARGET_SUFFIX "_unity")
+	endif()
+	if (NOT DEFINED COTIRE_TARGETS_FOLDER)
+		set (COTIRE_TARGETS_FOLDER "cotire")
+	endif()
+	if (NOT DEFINED COTIRE_UNITY_OUTPUT_DIRECTORY)
+		if ("${CMAKE_GENERATOR}" MATCHES "Ninja")
+			# generated Ninja build files do not work if the unity target produces the same output file as the cotired target
+			set (COTIRE_UNITY_OUTPUT_DIRECTORY "unity")
+		else()
+			set (COTIRE_UNITY_OUTPUT_DIRECTORY "")
+		endif()
+	endif()
+
+	# define cotire cache variables
+
+	define_property(
+		CACHED_VARIABLE PROPERTY "COTIRE_ADDITIONAL_PREFIX_HEADER_IGNORE_PATH"
+		BRIEF_DOCS "Ignore headers from these directories when generating the prefix header."
+		FULL_DOCS
+			"The variable can be set to a semicolon separated list of include directories."
+			"If a header file is found in one of these directories or sub-directories, it will be excluded from the generated prefix header."
+			"If not defined, defaults to empty list."
+	)
+
+	define_property(
+		CACHED_VARIABLE PROPERTY "COTIRE_ADDITIONAL_PREFIX_HEADER_IGNORE_EXTENSIONS"
+		BRIEF_DOCS "Ignore includes with the listed file extensions from the generated prefix header."
+		FULL_DOCS
+			"The variable can be set to a semicolon separated list of file extensions."
+			"If a header file extension matches one in the list, it will be excluded from the generated prefix header."
+			"Includes with an extension in CMAKE_<LANG>_SOURCE_FILE_EXTENSIONS are always ignored."
+			"If not defined, defaults to inc;inl;ipp."
+	)
+
+	define_property(
+		CACHED_VARIABLE PROPERTY "COTIRE_UNITY_SOURCE_EXCLUDE_EXTENSIONS"
+		BRIEF_DOCS "Exclude sources with the listed file extensions from the generated unity source."
+		FULL_DOCS
+			"The variable can be set to a semicolon separated list of file extensions."
+			"If a source file extension matches one in the list, it will be excluded from the generated unity source file."
+			"Source files with an extension in CMAKE_<LANG>_IGNORE_EXTENSIONS are always excluded."
+			"If not defined, defaults to m;mm."
+	)
+
+	define_property(
+		CACHED_VARIABLE PROPERTY "COTIRE_MINIMUM_NUMBER_OF_TARGET_SOURCES"
+		BRIEF_DOCS "Minimum number of sources in target required to enable use of precompiled header."
+		FULL_DOCS
+			"The variable can be set to an integer > 0."
+			"If a target contains less than that number of source files, cotire will not enable the use of the precompiled header for the target."
+			"If not defined, defaults to 3."
+	)
+
+	define_property(
+		CACHED_VARIABLE PROPERTY "COTIRE_MAXIMUM_NUMBER_OF_UNITY_INCLUDES"
+		BRIEF_DOCS "Maximum number of source files to include in a single unity source file."
+		FULL_DOCS
+			"This may be set to an integer >= 0."
+			"If 0, cotire will only create a single unity source file."
+			"If a target contains more than that number of source files, cotire will create multiple unity source files for it."
+			"Can be set to \"-j\" to optimize the count of unity source files for the number of available processor cores."
+			"Can be set to \"-j jobs\" to optimize the number of unity source files for the given number of simultaneous jobs."
+			"Is used to initialize the target property COTIRE_UNITY_SOURCE_MAXIMUM_NUMBER_OF_INCLUDES."
+			"Defaults to \"-j\" for the generators Visual Studio, JOM or Ninja. Defaults to 0 otherwise."
+	)
+
+	# define cotire directory properties
+
+	define_property(
+		DIRECTORY PROPERTY "COTIRE_ENABLE_PRECOMPILED_HEADER"
+		BRIEF_DOCS "Modify build command of cotired targets added in this directory to make use of the generated precompiled header."
+		FULL_DOCS
+			"See target property COTIRE_ENABLE_PRECOMPILED_HEADER."
+	)
+
+	define_property(
+		DIRECTORY PROPERTY "COTIRE_ADD_UNITY_BUILD"
+		BRIEF_DOCS "Add a new target that performs a unity build for cotired targets added in this directory."
+		FULL_DOCS
+			"See target property COTIRE_ADD_UNITY_BUILD."
+	)
+
+	define_property(
+		DIRECTORY PROPERTY "COTIRE_ADD_CLEAN"
+		BRIEF_DOCS "Add a new target that cleans all cotire generated files for cotired targets added in this directory."
+		FULL_DOCS
+			"See target property COTIRE_ADD_CLEAN."
+	)
+
+	define_property(
+		DIRECTORY PROPERTY "COTIRE_PREFIX_HEADER_IGNORE_PATH"
+		BRIEF_DOCS "Ignore headers from these directories when generating the prefix header."
+		FULL_DOCS
+			"See target property COTIRE_PREFIX_HEADER_IGNORE_PATH."
+	)
+
+	define_property(
+		DIRECTORY PROPERTY "COTIRE_PREFIX_HEADER_INCLUDE_PATH"
+		BRIEF_DOCS "Honor headers from these directories when generating the prefix header."
+		FULL_DOCS
+			"See target property COTIRE_PREFIX_HEADER_INCLUDE_PATH."
+	)
+
+	define_property(
+		DIRECTORY PROPERTY "COTIRE_UNITY_SOURCE_PRE_UNDEFS"
+		BRIEF_DOCS "Preprocessor undefs to place in the generated unity source file before the inclusion of each source file."
+		FULL_DOCS
+			"See target property COTIRE_UNITY_SOURCE_PRE_UNDEFS."
+	)
+
+	define_property(
+		DIRECTORY PROPERTY "COTIRE_UNITY_SOURCE_POST_UNDEFS"
+		BRIEF_DOCS "Preprocessor undefs to place in the generated unity source file after the inclusion of each source file."
+		FULL_DOCS
+			"See target property COTIRE_UNITY_SOURCE_POST_UNDEFS."
+	)
+
+	define_property(
+		DIRECTORY PROPERTY "COTIRE_UNITY_SOURCE_MAXIMUM_NUMBER_OF_INCLUDES"
+		BRIEF_DOCS "Maximum number of source files to include in a single unity source file."
+		FULL_DOCS
+			"See target property COTIRE_UNITY_SOURCE_MAXIMUM_NUMBER_OF_INCLUDES."
+	)
+
+	# define cotire target properties
+
+	define_property(
+		TARGET PROPERTY "COTIRE_ENABLE_PRECOMPILED_HEADER" INHERITED
+		BRIEF_DOCS "Modify this target's build command to make use of the generated precompiled header."
+		FULL_DOCS
+			"If this property is set to TRUE, cotire will modify the build command to make use of the generated precompiled header."
+			"Irrespective of the value of this property, cotire will setup custom commands to generate the unity source and prefix header for the target."
+			"For makefile based generators cotire will also set up a custom target to manually invoke the generation of the precompiled header."
+			"The target name will be set to this target's name with the suffix _pch appended."
+			"Inherited from directory."
+			"Defaults to TRUE."
+	)
+
+	define_property(
+		TARGET PROPERTY "COTIRE_ADD_UNITY_BUILD" INHERITED
+		BRIEF_DOCS "Add a new target that performs a unity build for this target."
+		FULL_DOCS
+			"If this property is set to TRUE, cotire creates a new target of the same type that uses the generated unity source file instead of the target sources."
+			"Most of the relevant target properties will be copied from this target to the new unity build target."
+			"Target dependencies and linked libraries have to be manually set up for the new unity build target."
+			"The unity target name will be set to this target's name with the suffix _unity appended."
+			"Inherited from directory."
+			"Defaults to TRUE."
+	)
+
+	define_property(
+		TARGET PROPERTY "COTIRE_ADD_CLEAN" INHERITED
+		BRIEF_DOCS "Add a new target that cleans all cotire generated files for this target."
+		FULL_DOCS
+			"If this property is set to TRUE, cotire creates a new target that clean all files (unity source, prefix header, precompiled header)."
+			"The clean target name will be set to this target's name with the suffix _clean_cotire appended."
+			"Inherited from directory."
+			"Defaults to FALSE."
+	)
+
+	define_property(
+		TARGET PROPERTY "COTIRE_PREFIX_HEADER_IGNORE_PATH" INHERITED
+		BRIEF_DOCS "Ignore headers from these directories when generating the prefix header."
+		FULL_DOCS
+			"The property can be set to a list of directories."
+			"If a header file is found in one of these directories or sub-directories, it will be excluded from the generated prefix header."
+			"Inherited from directory."
+			"If not set, this property is initialized to \${CMAKE_SOURCE_DIR};\${CMAKE_BINARY_DIR}."
+	)
+
+	define_property(
+		TARGET PROPERTY "COTIRE_PREFIX_HEADER_INCLUDE_PATH" INHERITED
+		BRIEF_DOCS "Honor headers from these directories when generating the prefix header."
+		FULL_DOCS
+			"The property can be set to a list of directories."
+			"If a header file is found in one of these directories or sub-directories, it will be included in the generated prefix header."
+			"If a header file is both selected by COTIRE_PREFIX_HEADER_IGNORE_PATH and COTIRE_PREFIX_HEADER_INCLUDE_PATH,"
+			"the option which yields the closer relative path match wins."
+			"Inherited from directory."
+			"If not set, this property is initialized to the empty list."
+	)
+
+	define_property(
+		TARGET PROPERTY "COTIRE_UNITY_SOURCE_PRE_UNDEFS" INHERITED
+		BRIEF_DOCS "Preprocessor undefs to place in the generated unity source file before the inclusion of each target source file."
+		FULL_DOCS
+			"This may be set to a semicolon-separated list of preprocessor symbols."
+			"cotire will add corresponding #undef directives to the generated unit source file before each target source file."
+			"Inherited from directory."
+			"Defaults to empty string."
+	)
+
+	define_property(
+		TARGET PROPERTY "COTIRE_UNITY_SOURCE_POST_UNDEFS" INHERITED
+		BRIEF_DOCS "Preprocessor undefs to place in the generated unity source file after the inclusion of each target source file."
+		FULL_DOCS
+			"This may be set to a semicolon-separated list of preprocessor symbols."
+			"cotire will add corresponding #undef directives to the generated unit source file after each target source file."
+			"Inherited from directory."
+			"Defaults to empty string."
+	)
+
+	define_property(
+		TARGET PROPERTY "COTIRE_UNITY_SOURCE_MAXIMUM_NUMBER_OF_INCLUDES" INHERITED
+		BRIEF_DOCS "Maximum number of source files to include in a single unity source file."
+		FULL_DOCS
+			"This may be set to an integer > 0."
+			"If a target contains more than that number of source files, cotire will create multiple unity build files for it."
+			"If not set, cotire will only create a single unity source file."
+			"Inherited from directory."
+			"Defaults to empty."
+	)
+
+	define_property(
+		TARGET PROPERTY "COTIRE_<LANG>_UNITY_SOURCE_INIT"
+		BRIEF_DOCS "User provided unity source file to be used instead of the automatically generated one."
+		FULL_DOCS
+			"If set, cotire will only add the given file(s) to the generated unity source file."
+			"If not set, cotire will add all the target source files to the generated unity source file."
+			"The property can be set to a user provided unity source file."
+			"Defaults to empty."
+	)
+
+	define_property(
+		TARGET PROPERTY "COTIRE_<LANG>_PREFIX_HEADER_INIT"
+		BRIEF_DOCS "User provided prefix header file to be used instead of the automatically generated one."
+		FULL_DOCS
+			"If set, cotire will add the given header file(s) to the generated prefix header file."
+			"If not set, cotire will generate a prefix header by tracking the header files included by the unity source file."
+			"The property can be set to a user provided prefix header file (e.g., stdafx.h)."
+			"Defaults to empty."
+	)
+
+	define_property(
+		TARGET PROPERTY "COTIRE_<LANG>_UNITY_SOURCE"
+		BRIEF_DOCS "Read-only property. The generated <LANG> unity source file(s)."
+		FULL_DOCS
+			"cotire sets this property to the path of the generated <LANG> single computation unit source file for the target."
+			"Defaults to empty string."
+	)
+
+	define_property(
+		TARGET PROPERTY "COTIRE_<LANG>_PREFIX_HEADER"
+		BRIEF_DOCS "Read-only property. The generated <LANG> prefix header file."
+		FULL_DOCS
+			"cotire sets this property to the full path of the generated <LANG> language prefix header for the target."
+			"Defaults to empty string."
+	)
+
+	define_property(
+		TARGET PROPERTY "COTIRE_<LANG>_PRECOMPILED_HEADER"
+		BRIEF_DOCS "Read-only property. The generated <LANG> precompiled header file."
+		FULL_DOCS
+			"cotire sets this property to the full path of the generated <LANG> language precompiled header binary for the target."
+			"Defaults to empty string."
+	)
+
+	define_property(
+		TARGET PROPERTY "COTIRE_UNITY_TARGET_NAME"
+		BRIEF_DOCS "The name of the generated unity build target corresponding to this target."
+		FULL_DOCS
+			"This property can be set to the desired name of the unity target that will be created by cotire."
+			"If not set, the unity target name will be set to this target's name with the suffix _unity appended."
+			"After this target has been processed by cotire, the property is set to the actual name of the generated unity target."
+			"Defaults to empty string."
+	)
+
+	# define cotire source properties
+
+	define_property(
+		SOURCE PROPERTY "COTIRE_EXCLUDED"
+		BRIEF_DOCS "Do not modify source file's build command."
+		FULL_DOCS
+			"If this property is set to TRUE, the source file's build command will not be modified to make use of the precompiled header."
+			"The source file will also be excluded from the generated unity source file."
+			"Source files that have their COMPILE_FLAGS property set will be excluded by default."
+			"Defaults to FALSE."
+	)
+
+	define_property(
+		SOURCE PROPERTY "COTIRE_DEPENDENCY"
+		BRIEF_DOCS "Add this source file to dependencies of the automatically generated prefix header file."
+		FULL_DOCS
+			"If this property is set to TRUE, the source file is added to dependencies of the generated prefix header file."
+			"If the file is modified, cotire will re-generate the prefix header source upon build."
+			"Defaults to FALSE."
+	)
+
+	define_property(
+		SOURCE PROPERTY "COTIRE_UNITY_SOURCE_PRE_UNDEFS"
+		BRIEF_DOCS "Preprocessor undefs to place in the generated unity source file before the inclusion of this source file."
+		FULL_DOCS
+			"This may be set to a semicolon-separated list of preprocessor symbols."
+			"cotire will add corresponding #undef directives to the generated unit source file before this file is included."
+			"Defaults to empty string."
+	)
+
+	define_property(
+		SOURCE PROPERTY "COTIRE_UNITY_SOURCE_POST_UNDEFS"
+		BRIEF_DOCS "Preprocessor undefs to place in the generated unity source file after the inclusion of this source file."
+		FULL_DOCS
+			"This may be set to a semicolon-separated list of preprocessor symbols."
+			"cotire will add corresponding #undef directives to the generated unit source file after this file is included."
+			"Defaults to empty string."
+	)
+
+	define_property(
+		SOURCE PROPERTY "COTIRE_START_NEW_UNITY_SOURCE"
+		BRIEF_DOCS "Start a new unity source file which includes this source file as the first one."
+		FULL_DOCS
+			"If this property is set to TRUE, cotire will complete the current unity file and start a new one."
+			"The new unity source file will include this source file as the first one."
+			"This property essentially works as a separator for unity source files."
+			"Defaults to FALSE."
+	)
+
+	define_property(
+		SOURCE PROPERTY "COTIRE_TARGET"
+		BRIEF_DOCS "Read-only property. Mark this source file as cotired for the given target."
+		FULL_DOCS
+			"cotire sets this property to the name of target, that the source file's build command has been altered for."
+			"Defaults to empty string."
+	)
+
+	message (STATUS "cotire ${COTIRE_CMAKE_MODULE_VERSION} loaded.")
+
+endif()
diff --git a/CMake/memaccess.cmake b/CMake/memaccess.cmake
new file mode 100644
index 0000000..05aaf1a
--- /dev/null
+++ b/CMake/memaccess.cmake
@@ -0,0 +1,41 @@
+##
+## This file is part of the PulseView project.
+##
+## Copyright (C) 2014 Marcus Comstedt <marcus at mc.pp.se>
+##
+## This program is free software: you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation, either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program.  If not, see <http://www.gnu.org/licenses/>.
+##
+
+include(CheckCSourceRuns)
+
+function(memaccess_check_unaligned_le _var)
+if(NOT CMAKE_CROSSCOMPILING)
+CHECK_C_SOURCE_RUNS("
+#include <stdint.h>
+int main() {
+    int i;
+    union { uint64_t u64; uint8_t u8[16]; } d;
+    uint64_t v;
+    for (i=0; i<16; i++)
+        d.u8[i] = i;
+    v = *(uint64_t *)(d.u8+1);
+    if (v != 0x0807060504030201ULL)
+       return 1;
+    return 0;
+}" ${_var})
+endif()
+if(CMAKE_CROSSCOMPILING)
+  message(STATUS "Cross compiling - using portable code for memory access")
+endif()
+endfunction()
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..50dde49
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,373 @@
+##
+## This file is part of the PulseView project.
+##
+## Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+## Copyright (C) 2012-2013 Alexandru Gagniuc <mr.nuke.me at gmail.com>
+##
+## This program is free software: you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation, either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program.  If not, see <http://www.gnu.org/licenses/>.
+##
+
+cmake_minimum_required(VERSION 2.8.6)
+
+include(FindPkgConfig)
+include(GNUInstallDirs)
+
+set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/CMake")
+
+project(pulseview)
+
+#===============================================================================
+#= User Options
+#-------------------------------------------------------------------------------
+
+option(DISABLE_WERROR "Build without -Werror" FALSE)
+option(ENABLE_SIGNALS "Build with UNIX signals" TRUE)
+option(ENABLE_DECODE "Build with libsigrokdecode" TRUE)
+option(ENABLE_COTIRE "Enable cotire" FALSE)
+option(ENABLE_TESTS "Enable unit tests" FALSE)
+option(STATIC_PKGDEPS_LIBS "Statically link to (pkg-config) libraries" FALSE)
+
+if(WIN32)
+	# On Windows/MinGW we need to statically link to libraries.
+	# This option is user configurable, but enable it by default on win32.
+	set(STATIC_PKGDEPS_LIBS TRUE)
+
+	# For boost-thread we need two additional settings on win32:
+	set(Boost_USE_STATIC_LIBS ON)
+	add_definitions(-DBOOST_THREAD_USE_LIB)
+
+	# Windows does not support UNIX signals.
+	set(ENABLE_SIGNALS FALSE)
+endif()
+
+if(NOT CMAKE_BUILD_TYPE)
+  set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING
+      "Choose the type of build, options are: None Debug Release RelWithDebInfo MinSizeRel."
+      FORCE)
+endif()
+
+#===============================================================================
+#= Dependencies
+#-------------------------------------------------------------------------------
+
+list(APPEND PKGDEPS libsigrok>=0.3.0)
+
+if(ENABLE_DECODE)
+	list(APPEND PKGDEPS libsigrokdecode>=0.3.0)
+endif()
+
+find_package(PkgConfig)
+pkg_check_modules(PKGDEPS REQUIRED ${PKGDEPS})
+
+find_program(QT_QMAKE_EXECUTABLE NAMES qmake4 qmake-qt4 qmake-mac)
+find_package(Qt4 REQUIRED)
+
+# Find the platform's thread library (needed for boost-thread).
+# This will set ${CMAKE_THREAD_LIBS_INIT} to the correct, OS-specific value.
+find_package(Threads)
+
+if(WIN32)
+	# On Windows/MinGW we need to use 'thread_win32' instead of 'thread'.
+	# The library is named libboost_thread_win32* (not libboost_thread*).
+	find_package(Boost 1.42 COMPONENTS filesystem system thread_win32 REQUIRED)
+else()
+	find_package(Boost 1.42 COMPONENTS filesystem system thread REQUIRED)
+endif()
+
+#===============================================================================
+#= System Introspection
+#-------------------------------------------------------------------------------
+
+include(memaccess)
+memaccess_check_unaligned_le(HAVE_UNALIGNED_LITTLE_ENDIAN_ACCESS)
+
+#===============================================================================
+#= Config Header
+#-------------------------------------------------------------------------------
+
+set(PV_TITLE PulseView)
+set(PV_DESCRIPTION "A GUI for sigrok")
+
+set(PV_VERSION_MAJOR 0)
+set(PV_VERSION_MINOR 2)
+set(PV_VERSION_MICRO 0)
+set(PV_VERSION_STRING
+	${PV_VERSION_MAJOR}.${PV_VERSION_MINOR}.${PV_VERSION_MICRO}
+)
+
+configure_file (
+	${PROJECT_SOURCE_DIR}/config.h.in
+	${PROJECT_BINARY_DIR}/config.h
+)
+
+#===============================================================================
+#= Sources
+#-------------------------------------------------------------------------------
+
+set(pulseview_SOURCES
+	main.cpp
+	pv/devicemanager.cpp
+	pv/mainwindow.cpp
+	pv/sigsession.cpp
+	pv/storesession.cpp
+	pv/data/analog.cpp
+	pv/data/analogsnapshot.cpp
+	pv/data/logic.cpp
+	pv/data/logicsnapshot.cpp
+	pv/data/signaldata.cpp
+	pv/data/snapshot.cpp
+	pv/device/device.cpp
+	pv/device/file.cpp
+	pv/device/devinst.cpp
+	pv/device/inputfile.cpp
+	pv/device/sessionfile.cpp
+	pv/dialogs/about.cpp
+	pv/dialogs/connect.cpp
+	pv/dialogs/storeprogress.cpp
+	pv/popups/deviceoptions.cpp
+	pv/popups/probes.cpp
+	pv/prop/bool.cpp
+	pv/prop/double.cpp
+	pv/prop/enum.cpp
+	pv/prop/int.cpp
+	pv/prop/property.cpp
+	pv/prop/string.cpp
+	pv/prop/binding/binding.cpp
+	pv/prop/binding/deviceoptions.cpp
+	pv/toolbars/samplingbar.cpp
+	pv/view/analogsignal.cpp
+	pv/view/cursor.cpp
+	pv/view/cursorpair.cpp
+	pv/view/header.cpp
+	pv/view/marginwidget.cpp
+	pv/view/logicsignal.cpp
+	pv/view/ruler.cpp
+	pv/view/selectableitem.cpp
+	pv/view/signal.cpp
+	pv/view/timemarker.cpp
+	pv/view/trace.cpp
+	pv/view/tracepalette.cpp
+	pv/view/view.cpp
+	pv/view/viewport.cpp
+	pv/widgets/colourbutton.cpp
+	pv/widgets/colourpopup.cpp
+	pv/widgets/popup.cpp
+	pv/widgets/popuptoolbutton.cpp
+	pv/widgets/sweeptimingwidget.cpp
+	pv/widgets/wellarray.cpp
+)
+
+# This list includes only QObject derived class headers.
+set(pulseview_HEADERS
+	pv/mainwindow.h
+	pv/sigsession.h
+	pv/storesession.h
+	pv/device/devinst.h
+	pv/dialogs/about.h
+	pv/dialogs/connect.h
+	pv/dialogs/storeprogress.h
+	pv/popups/probes.h
+	pv/popups/deviceoptions.h
+	pv/prop/bool.h
+	pv/prop/double.h
+	pv/prop/enum.h
+	pv/prop/int.h
+	pv/prop/property.h
+	pv/prop/string.h
+	pv/toolbars/samplingbar.h
+	pv/view/cursor.h
+	pv/view/header.h
+	pv/view/logicsignal.h
+	pv/view/marginwidget.h
+	pv/view/ruler.h
+	pv/view/selectableitem.h
+	pv/view/signal.h
+	pv/view/timemarker.h
+	pv/view/trace.h
+	pv/view/view.h
+	pv/view/viewport.h
+	pv/widgets/colourbutton.h
+	pv/widgets/colourpopup.h
+	pv/widgets/popup.h
+	pv/widgets/popuptoolbutton.h
+	pv/widgets/sweeptimingwidget.h
+	pv/widgets/wellarray.h
+)
+
+set(pulseview_FORMS
+	pv/dialogs/about.ui
+)
+
+set(pulseview_RESOURCES
+	pulseview.qrc
+)
+
+if(ENABLE_SIGNALS)
+	list(APPEND pulseview_SOURCES signalhandler.cpp)
+	list(APPEND pulseview_HEADERS signalhandler.h)
+endif()
+
+if(ENABLE_DECODE)
+	list(APPEND pulseview_SOURCES
+		pv/data/decoderstack.cpp
+		pv/data/decode/annotation.cpp
+		pv/data/decode/decoder.cpp
+		pv/data/decode/row.cpp
+		pv/data/decode/rowdata.cpp
+		pv/prop/binding/decoderoptions.cpp
+		pv/view/decodetrace.cpp
+		pv/widgets/decodergroupbox.cpp
+		pv/widgets/decodermenu.cpp
+	)
+
+	list(APPEND pulseview_HEADERS
+		pv/data/decoderstack.h
+		pv/view/decodetrace.h
+		pv/widgets/decodergroupbox.h
+		pv/widgets/decodermenu.h
+	)
+endif()
+
+if(WIN32)
+	# Use the sigrok icon for the pulseview.exe executable.
+	set(CMAKE_RC_COMPILE_OBJECT "${CMAKE_RC_COMPILER} -O coff -I${CMAKE_CURRENT_SOURCE_DIR} <SOURCE> <OBJECT>")
+	enable_language(RC)
+	list(APPEND pulseview_SOURCES pulseviewico.rc)
+endif()
+
+qt4_wrap_cpp(pulseview_HEADERS_MOC ${pulseview_HEADERS})
+qt4_wrap_ui(pulseview_FORMS_HEADERS ${pulseview_FORMS})
+qt4_add_resources(pulseview_RESOURCES_RCC ${pulseview_RESOURCES})
+
+include(${QT_USE_FILE})
+
+#===============================================================================
+#= Global Definitions
+#-------------------------------------------------------------------------------
+
+add_definitions(${QT_DEFINITIONS})
+add_definitions(-D__STDC_LIMIT_MACROS)
+add_definitions(-Wall -Wextra)
+
+if(ENABLE_DECODE)
+	add_definitions(-DENABLE_DECODE)
+endif()
+
+if(NOT DISABLE_WERROR)
+	add_definitions(-Werror)
+endif()
+
+#===============================================================================
+#= Global Include Directories
+#-------------------------------------------------------------------------------
+
+include_directories(
+	${CMAKE_CURRENT_BINARY_DIR}
+	${CMAKE_CURRENT_SOURCE_DIR}
+	${Boost_INCLUDE_DIRS}
+)
+
+if(STATIC_PKGDEPS_LIBS)
+	include_directories(${PKGDEPS_STATIC_INCLUDE_DIRS})
+else()
+	include_directories(${PKGDEPS_INCLUDE_DIRS})
+endif()
+
+#===============================================================================
+#= Linker Configuration
+#-------------------------------------------------------------------------------
+
+link_directories(${Boost_LIBRARY_DIRS})
+
+set(PULSEVIEW_LINK_LIBS
+	${Boost_LIBRARIES}
+	${CMAKE_THREAD_LIBS_INIT}
+	${QT_LIBRARIES}
+)
+
+if(STATIC_PKGDEPS_LIBS)
+	link_directories(${PKGDEPS_STATIC_LIBRARY_DIRS})
+	list(APPEND PULSEVIEW_LINK_LIBS ${PKGDEPS_STATIC_LIBRARIES})
+if(WIN32)
+	# Workaround for a MinGW linking issue.
+	list(APPEND PULSEVIEW_LINK_LIBS "-llzma -llcms2")
+endif()
+else()
+	link_directories(${PKGDEPS_LIBRARY_DIRS})
+	list(APPEND PULSEVIEW_LINK_LIBS ${PKGDEPS_LIBRARIES})
+endif()
+
+if(WIN32)
+	# On Windows we need to statically link the libqsvg imageformat
+	# plugin (and the QtSvg component) for SVG graphics/icons to work.
+	add_definitions(-DQT_STATICPLUGIN)
+	link_directories("${QT_PLUGINS_DIR}/imageformats")
+	list(APPEND PULSEVIEW_LINK_LIBS ${QT_QTSVG_LIBRARY})
+	list(APPEND PULSEVIEW_LINK_LIBS "-lqsvg")
+endif()
+
+add_executable(${PROJECT_NAME}
+	${pulseview_SOURCES}
+	${pulseview_HEADERS_MOC}
+	${pulseview_FORMS_HEADERS}
+	${pulseview_RESOURCES_RCC}
+)
+
+target_link_libraries(${PROJECT_NAME} ${PULSEVIEW_LINK_LIBS})
+
+if(WIN32)
+	# Pass -mwindows so that no "DOS box" opens when PulseView is started.
+	set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "-mwindows")
+endif()
+
+if(ENABLE_COTIRE)
+	include(cotire)
+	cotire(${PROJECT_NAME})
+endif()
+
+#===============================================================================
+#= Installation
+#-------------------------------------------------------------------------------
+
+# Install the executable.
+install(TARGETS ${PROJECT_NAME} DESTINATION bin/)
+
+# Install the manpage.
+install(FILES doc/pulseview.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 COMPONENT doc)
+
+#===============================================================================
+#= Packaging (handled by CPack)
+#-------------------------------------------------------------------------------
+
+set(CPACK_PACKAGE_VERSION_MAJOR ${PV_VERSION_MAJOR})
+set(CPACK_PACKAGE_VERSION_MINOR ${PV_VERSION_MINOR})
+set(CPACK_PACKAGE_VERSION_PATCH ${PV_VERSION_MICRO})
+set(CPACK_PACKAGE_DESCRIPTION_FILE ${CMAKE_CURRENT_SOURCE_DIR}/README)
+set(CPACK_RESOURCE_FILE_LICENSE ${CMAKE_CURRENT_SOURCE_DIR}/COPYING)
+set(CPACK_SOURCE_IGNORE_FILES ${CMAKE_CURRENT_BINARY_DIR} ".gitignore" ".git")
+set(CPACK_SOURCE_PACKAGE_FILE_NAME
+	"${CMAKE_PROJECT_NAME}-${PV_VERSION_MAJOR}.${PV_VERSION_MINOR}.${PV_VERSION_MICRO}")
+set(CPACK_SOURCE_GENERATOR "TGZ")
+
+include(CPack)
+
+#===============================================================================
+#= Tests
+#-------------------------------------------------------------------------------
+
+if(ENABLE_TESTS)
+	add_subdirectory(test)
+	enable_testing()
+	add_test(test ${CMAKE_CURRENT_BINARY_DIR}/test/pulseview-test)
+endif()
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..94a9ed0
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/HACKING b/HACKING
new file mode 100644
index 0000000..d1edc87
--- /dev/null
+++ b/HACKING
@@ -0,0 +1,79 @@
+-------------------------------------------------------------------------------
+HACKING
+-------------------------------------------------------------------------------
+
+Coding style
+------------
+
+This project is programmed using the Linux kernel coding style, see
+http://lxr.linux.no/linux/Documentation/CodingStyle for details.
+
+Please use the same style for any code contributions, thanks!
+
+In some exceptional cases deviations from the above coding guidelines are
+OK (in order to meet Qt/C++ related guidelines, for example).
+
+
+Contributions
+-------------
+
+ - Patches should be sent to the development mailinglist at
+   sigrok-devel at lists.sourceforge.net (please subscribe to the list first).
+
+   https://lists.sourceforge.net/lists/listinfo/sigrok-devel
+
+ - Alternatively, you can also clone the git repository and let us know
+   from where to pull/review your changes. You can use gitorious.org,
+   github.com, or any other public git hosting site.
+
+
+Random notes
+------------
+
+ - Consistently use g_try_malloc() / g_try_malloc0(). Do not use standard
+   malloc()/calloc() if it can be avoided (sometimes other libs such
+   as libftdi can return malloc()'d memory, for example).
+
+ - Always properly match allocations with the proper *free() functions. If
+   glib's g_try_malloc()/g_try_malloc0() was used, use g_free() to free the
+   memory. Otherwise use standard free(). Never use the wrong function!
+
+ - Never use g_malloc() or g_malloc0(). These functions do not return NULL
+   if not enough memory is available but rather lead to an exit() or segfault
+   instead. This behaviour is not acceptable.
+   Use g_try_malloc()/g_try_malloc0() instead and check the return value.
+
+ - Use glib's gboolean / TRUE / FALSE for boolean types consistently.
+   Do not use <stdbool.h> and its true / false, and do not invent private
+   definitions for this either.
+
+ - Consistently use the same naming convention for #include guards in headers:
+   <PROJECTNAME>_<PATH_TO_FILE>_<FILE>
+   This ensures that all #include guards are always unique and consistent.
+   Example: PULSEVIEW_PV_VIEW_RULER_H
+
+ - Consistently use the same naming convention for functions, if appropriate:
+
+   Getter/setter function names should usually end with "_get" or "_set".
+   Functions creating new "objects" should end with "_new".
+   Functions destroying "objects" should end with "_destroy".
+   Functions adding or removing items (e.g. from lists) should end with
+   either "_add" or "_remove".
+   Functions operating on all items from a list (not on only one of them),
+   should end with "_all", e.g. "_remove_all", "_get_all", and so on.
+   Use "_remove_all" in favor of "_clear" for consistency.
+
+ - In Doxygen comments, put an empty line between the block of @param lines
+   and the final @return line. The @param lines themselves (if there is more
+   than one) are not separated by empty lines.
+
+
+Release engineering
+-------------------
+
+See
+
+ http://sigrok.org/wiki/Developers/Release_process
+
+for a list of items that need to be done when releasing a new tarball.
+
diff --git a/INSTALL b/INSTALL
new file mode 100644
index 0000000..4a93e08
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,53 @@
+-------------------------------------------------------------------------------
+INSTALL
+-------------------------------------------------------------------------------
+
+Requirements
+------------
+
+ - git
+ - g++
+ - make
+ - libtool
+ - pkg-config >= 0.22
+ - cmake >= 2.8.6
+ - libglib >= 2.28.0
+ - Qt >= 4.5
+ - libboost >= 1.42 (including the following libs):
+    - libboost-system
+    - libboost-thread
+    - libboost-filesystem
+    - libboost-test (optional, only needed to run the unit tests)
+ - libsigrok >= 0.3.0
+ - libsigrokdecode >= 0.3.0
+
+
+Building and installing
+-----------------------
+
+In order to get the PulseView source code and build it, run:
+
+ $ git clone git://sigrok.org/pulseview
+ $ cd pulseview
+ $ cmake .
+ $ make
+
+For installing PulseView:
+
+ $ make install
+
+See the following wiki page for more (OS-specific) instructions:
+
+ http://sigrok.org/wiki/Building
+
+
+Creating a source distribution package
+--------------------------------------
+
+In order to build a source package begin with an unconfigured source tree.
+
+ $ mkdir dist
+ $ cd dist
+ $ cmake ..
+ $ make package_source
+
diff --git a/NEWS b/NEWS
new file mode 100644
index 0000000..696f13a
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,101 @@
+0.2.0 (2014-05-06)
+------------------
+
+ * Add support for protocol decoding.
+   - A menu item Decoders->Add can add (only) low-level protocol decoders.
+   - Clicking on a decoder's arrow on the left-hand side allows stacking
+     (only) further, higher-level decoders (bug #333).
+   - Add support for multiple decoder rows per PD (bugs #161, #303).
+ * Add support for loading data from input files.
+ * Add support for saving logic analyzer data in *.sr files (bug #245).
+ * Show the filename of loaded *.sr files as "device name" in a drop-down.
+ * Add support for the new "channel groups" feature in libsigrok.
+ * Add a "Zoom to fit" and a "Zoom 1:1" icon/button.
+ * Add a "Channels" icon/button for enabling and configuring device channels.
+ * Add a "Device options" icon/button for configuring device specific settings
+   such as voltage thresholds.
+ * Double-clicking will zoom into the location the cursor points to.
+ * Add shortcut keys for Zoom-in (CTRL++) and Zoom-out (Ctrl+-) (bug #235).
+ * Add initial, basic support for analog data sources, such as oscilloscopes.
+   This has been tested on e.g. the Rigol DS1052E oscilloscope (bug #127).
+ * Almost all operations in PulseView work in an "auto-apply" style, i.e.
+   any changes in the GUI popups (decoder channel assignment, decoder option
+   changes, device channel changes, device option changes, and so on) have an
+   immediate effect (you don't have to click on any "OK" buttons).
+ * Open file dialog: Add 'All files' selection possibility.
+ * Added GUI support for the following libsigrok config options:
+   - SR_CONF_VOLTAGE_THRESHOLD
+   - SR_CONF_TRIGGER_SLOPE
+   - SR_CONF_EXTERNAL_CLOCK
+   - SR_CONF_CLOCK_EDGE
+ * Updated build requirements:
+   - cmake >= 2.8.6 (required)
+   - libsigrok >= 0.3.0 (required)
+   - libsigrokdecode >= 0.3.0 (required)
+     Note: libsigrokdecode is now always required (was optional before).
+   - libboost-filesystem >= 1.42 (required)
+   - libboost-test >= 1.42 (optional, only needed for running unit tests)
+ * Fix a build issue on NetBSD due to an incorrect #include.
+ * Fix a channel handling issue for devices with >= 16 channels.
+ * Fixed a few display issues related to UTF-8 strings in decoder annotations,
+   decoder options, and so on (bug #307, and other fixes without bug numbers).
+ * Don't decode if required PD channels were not supplied (bug #204).
+ * Fix some issues with partially disappearing PD annotations.
+ * Update the samplerate selector when the device config changes (since e.g.
+   en-/disabling logic analyzer channels can change available samplerates).
+ * Fix a samplerate selector and device config update issue (bug #296).
+ * Minor performance improvements via memory pre-allocation in some areas.
+ * Remove old PD annotations upon new captures (bugs #172, #302).
+ * Portability improvements by using portable sample pack/unpack code.
+ * Fix a segfault related to thread joining (bug #323).
+ * Fix a minor issue which lead to trigger config being unusable (bug #318).
+ * Fix an issue which lead to the user-selected samplerate in the drop-down
+   box not being selected and shown again after clicking "Run" (bug #324).
+ * Start a new decode session whenever a new frame begins.
+ * Fix an issue which arose when a session file load failed (bug #331).
+ * Fix a segfault if only exactly one libsigrok driver is available (bug #334).
+ * Fix an issue with channel names not being updated while typing (bug #338).
+ * Fix an issue by stopping the capture when a device is changed (bug #223).
+ * Fix an issue with the displayed cursor popup time (bug #229).
+ * Fix handling of different integer types in some properties (bug #203).
+ * Hide the 'Configure device' button when the popup would be empty (bug #232).
+ * Fix a decoding chunk buffer / unitsize issue (bugs #171, #225).
+ * Fix a zoom issue due to invalid samplerate, assume 1Hz if needed (bug #278).
+ * Fix a QWellArray related issue by dropping QT_NO_MENU code (bug #265).
+ * Fix an analog data channel interleaving issue (bug #279).
+ * Only show the sample count widget if needed (i.e., hide it for scopes).
+ * SweepTimingWidget: Show a 1-2-5 based list for samplerate and samplecount.
+ * Fixed a unit test issue with AnalogSnapshotTest (bug #286).
+ * Add an extra sample to edges to make the end sample visible (bug #280).
+ * Fix an issue with decoder errors being shown even after the error was fixed.
+ * Add support for SR_CONF_LIMIT_SAMPLES (device-specific limits) (bug #74).
+ * Windows:
+   - Use the sigrok logo as icon for pulseview.exe (bugs #110, #238).
+   - Fix SVG icons not being displayed on Windows (bug #239).
+   - Ship libusb0.dll in the NSIS based installer (bug #241).
+   - Additionally ship decoders, firmware files, example *.sr files, and
+     zadig.exe and zadig_xp.exe in the NSIS based installer.
+   - Fix a "working path" issue resulting in PDs not being usable.
+ * Device scan: Disable "OK" button if no devices were found (bug #237).
+ * Fix a segfault related to incorrect decoder option handling (bug #160).
+ * Fix an off-by-one issue resulting in one PD not being usable (bug #164).
+ * Fix a PD channel auto-select logic issue with optional channels (bug #310).
+ * Fix an issue when cancelling "Connect to Device" (bug #242).
+ * Avoid confusing annotation color changes (bug #311).
+ * Fix an issue that caused some decoded data to not be shown (bug #308).
+ * Don't allow disabled channels (channel arrows) to be selected (bug #313).
+ * The channel name field is now resized to always fit the contents (bug #167).
+ * Build fixes for systems with (among other versions) Qt5 installed.
+ * Fix a build issue related to missing libboost-filesystem (bug #133).
+ * Add Cotire (optional, disabled by default) support for build performance.
+ * Fix a build issue related to Cotire (bug #217).
+ * Fix an issue with missing channel names from loaded files (bug #126).
+ * Fix missing samplerate loading from session files (bug #123).
+ * Fix some build issues related to C++ namespaces (bug #196).
+ * Fix a file loading issue which triggered an assert (bug #320).
+
+0.1.0 (2013-05-04)
+------------------
+
+ * Initial release.
+
diff --git a/README b/README
new file mode 100644
index 0000000..e4aa3c1
--- /dev/null
+++ b/README
@@ -0,0 +1,69 @@
+-------------------------------------------------------------------------------
+README
+-------------------------------------------------------------------------------
+
+The sigrok project aims at creating a portable, cross-platform,
+Free/Libre/Open-Source signal analysis software suite that supports various
+device types (such as logic analyzers, oscilloscopes, multimeters, and more).
+
+PulseView is a Qt-based LA/scope/MSO GUI for sigrok.
+
+
+Status
+------
+
+PulseView is in a usable state and has had official tarball releases.
+
+However, it is still work in progress. Some basic functionality
+is available and working, but other things are still on the TODO list.
+
+
+Copyright and license
+---------------------
+
+PulseView is licensed under the terms of the GNU General Public License
+(GPL), version 3 or later.
+
+While some individual source code files are licensed under the GPLv2+, and
+some files are licensed under the GPLv3+, this doesn't change the fact that
+the program as a whole is licensed under the terms of the GPLv3+ (e.g. also
+due to the fact that it links against GPLv3+ libraries).
+
+Please see the individual source files for the full list of copyright holders.
+
+
+Copyright notices
+-----------------
+
+A copyright notice indicating a range of years, must be interpreted as having
+had copyrightable material added in each of those years.
+
+Example:
+
+ Copyright (C) 2010-2013 Contributor Name
+
+is to be interpreted as
+
+ Copyright (C) 2010,2011,2012,2013 Contributor Name
+
+
+Mailing lists
+-------------
+
+There are two mailing lists for sigrok/PulseView:
+
+ https://lists.sourceforge.net/lists/listinfo/sigrok-devel
+ https://lists.sourceforge.net/lists/listinfo/sigrok-commits
+
+
+IRC
+---
+
+You can find the sigrok developers in the #sigrok IRC channel on Freenode.
+
+
+Website
+-------
+
+ http://sigrok.org/wiki/PulseView
+
diff --git a/config.h.in b/config.h.in
new file mode 100644
index 0000000..aa6062d
--- /dev/null
+++ b/config.h.in
@@ -0,0 +1,37 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Alexandru Gagniuc <mr.nuke.me at gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef _PULSEVIEW_CONFIG_H
+#define _PULSEVIEW_CONFIG_H
+
+/* Application details */
+#define PV_TITLE "@PV_TITLE@"
+#define PV_DESCRIPTION "@PV_DESCRIPTION@"
+#define PV_BIN_NAME "@PROJECT_NAME@"
+
+/* Pulseview version information */
+#define PV_VERSION_MAJOR @PV_VERSION_MAJOR@
+#define PV_VERSION_MINOR @PV_VERSION_MINOR@
+#define PV_VERSION_MICRO @PV_VERSION_MICRO@
+#define PV_VERSION_STRING "@PV_VERSION_STRING@"
+
+/* Platform properties */
+#cmakedefine HAVE_UNALIGNED_LITTLE_ENDIAN_ACCESS
+
+#endif
diff --git a/contrib/pulseview.desktop b/contrib/pulseview.desktop
new file mode 100644
index 0000000..cbab083
--- /dev/null
+++ b/contrib/pulseview.desktop
@@ -0,0 +1,9 @@
+[Desktop Entry]
+Name=PulseView
+GenericName=Signal acquisition GUI for sigrok
+Categories=Electronics;
+Comment=Control your Logic Analyzer, Oscilloscope, or MSO
+Exec=pulseview
+Icon=sigrok-logo-notext
+Type=Application
+
diff --git a/contrib/pulseview_cross.nsi b/contrib/pulseview_cross.nsi
new file mode 100644
index 0000000..3c39351
--- /dev/null
+++ b/contrib/pulseview_cross.nsi
@@ -0,0 +1,252 @@
+##
+## This file is part of the PulseView project.
+##
+## Copyright (C) 2013-2014 Uwe Hermann <uwe at hermann-uwe.de>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+##
+
+#
+# This file is used to create the PulseView Windows installer via NSIS.
+# It is meant for use in a cross-compile setup (not for native builds).
+# See the 'sigrok-cross-mingw' script in the sigrok-util repo for details.
+#
+# NSIS documentation:
+# http://nsis.sourceforge.net/Docs/
+# http://nsis.sourceforge.net/Docs/Modern%20UI%202/Readme.html
+#
+
+# Include the "Modern UI" header, which gives us the usual Windows look-n-feel.
+!include "MUI2.nsh"
+
+
+# --- Global stuff ------------------------------------------------------------
+
+# Installer/product name.
+Name "PulseView"
+
+# Filename of the installer executable.
+OutFile "pulseview-0.2.0-installer.exe"
+
+# Where to install the application.
+InstallDir "$PROGRAMFILES\sigrok\PulseView"
+
+# Request admin privileges for Windows Vista and Windows 7.
+# http://nsis.sourceforge.net/Docs/Chapter4.html
+RequestExecutionLevel admin
+
+# Local helper definitions.
+!define REGSTR "Software\Microsoft\Windows\CurrentVersion\Uninstall\PulseView"
+
+
+# --- MUI interface configuration ---------------------------------------------
+
+# Use the following icon for the installer EXE file.
+!define MUI_ICON "../icons/sigrok-logo-notext.ico"
+
+# Show a nice image at the top of each installer page.
+!define MUI_HEADERIMAGE
+
+# Don't automatically go to the Finish page so the user can check the log.
+!define MUI_FINISHPAGE_NOAUTOCLOSE
+
+# Upon "cancel", ask the user if he really wants to abort the installer.
+!define MUI_ABORTWARNING
+
+# Don't force the user to accept the license, just show it.
+# Details: http://trac.videolan.org/vlc/ticket/3124
+!define MUI_LICENSEPAGE_BUTTON $(^NextBtn)
+!define MUI_LICENSEPAGE_TEXT_BOTTOM "Click Next to continue."
+
+# Path where the cross-compiled sigrok tools and libraries are located.
+# Change this to where-ever you installed libsigrok.a and so on.
+!define CROSS "$%HOME%/sr_mingw"
+
+# Path where the cross-compiled MXE tools and libraries are located.
+# Change this to where-ever you installed MXE (and the files it built).
+!define MXE "$%HOME%/mxe-git/usr/i686-pc-mingw32"
+
+
+# --- MUI pages ---------------------------------------------------------------
+
+# Show a nice "Welcome to the ... Setup Wizard" page.
+!insertmacro MUI_PAGE_WELCOME
+
+# Show the license of the project.
+!insertmacro MUI_PAGE_LICENSE "../COPYING"
+
+# Show a screen which allows the user to select which components to install.
+!insertmacro MUI_PAGE_COMPONENTS
+
+# Allow the user to select a different install directory.
+!insertmacro MUI_PAGE_DIRECTORY
+
+# Perform the actual installation, i.e. install the files.
+!insertmacro MUI_PAGE_INSTFILES
+
+# Show a final "We're done, click Finish to close this wizard" message.
+!insertmacro MUI_PAGE_FINISH
+
+# Pages used for the uninstaller.
+!insertmacro MUI_UNPAGE_WELCOME
+!insertmacro MUI_UNPAGE_CONFIRM
+!insertmacro MUI_UNPAGE_INSTFILES
+!insertmacro MUI_UNPAGE_FINISH
+
+
+# --- MUI language files ------------------------------------------------------
+
+# Select an installer language (required!).
+!insertmacro MUI_LANGUAGE "English"
+
+
+# --- Default section ---------------------------------------------------------
+
+Section "PulseView (required)" Section1
+
+	# This section is gray (can't be disabled) in the component list.
+	SectionIn RO
+
+	# Install the file(s) specified below into the specified directory.
+	SetOutPath "$INSTDIR"
+
+	# License file.
+	File "../COPYING"
+
+	# PulseView (statically linked, includes all libs).
+	File "${CROSS}/bin/pulseview.exe"
+
+	# libusb0.dll (needed for libusb-0.1).
+	File "${CROSS}/libusb0.dll"
+
+	# Zadig (used for installing libusb-win32 and WinUSB drivers).
+	File "${CROSS}/zadig.exe"
+	File "${CROSS}/zadig_xp.exe"
+
+	# Python
+	File "${CROSS}/python32.dll"
+	File "${CROSS}/python32.zip"
+
+	# Protocol decoders.
+	SetOutPath "$INSTDIR\decoders"
+	File /r /x "__pycache__" "${CROSS}/share/libsigrokdecode/decoders/*"
+
+	# Firmware files.
+	SetOutPath "$INSTDIR\firmware"
+	File /r "${CROSS}/share/sigrok-firmware/*"
+
+	# Example *.sr files.
+	SetOutPath "$INSTDIR\examples"
+	File /r "${CROSS}/share/sigrok-dumps/*"
+
+	# Generate the uninstaller executable.
+	WriteUninstaller "$INSTDIR\Uninstall.exe"
+
+	# Create a sub-directory in the start menu.
+	CreateDirectory "$SMPROGRAMS\sigrok"
+	CreateDirectory "$SMPROGRAMS\sigrok\PulseView"
+
+	# Create a shortcut for the PulseView application.
+	SetOutPath "$INSTDIR"
+	CreateShortCut "$SMPROGRAMS\sigrok\PulseView\PulseView.lnk" \
+		"$INSTDIR\pulseview.exe" "" "$INSTDIR\pulseview.exe" \
+		0 SW_SHOWNORMAL \
+		"" "Open-source, portable sigrok GUI"
+
+	# Create a shortcut for the uninstaller.
+	CreateShortCut "$SMPROGRAMS\sigrok\PulseView\Uninstall.lnk" \
+		"$INSTDIR\Uninstall.exe" "" "$INSTDIR\Uninstall.exe" 0 \
+		SW_SHOWNORMAL "" "Uninstall PulseView"
+
+	# Create registry keys for "Add/remove programs" in the control panel.
+	WriteRegStr HKLM "${REGSTR}" "DisplayName" "PulseView"
+	WriteRegStr HKLM "${REGSTR}" "UninstallString" \
+		"$\"$INSTDIR\Uninstall.exe$\""
+	WriteRegStr HKLM "${REGSTR}" "InstallLocation" "$\"$INSTDIR$\""
+	WriteRegStr HKLM "${REGSTR}" "DisplayIcon" \
+		"$\"$INSTDIR\sigrok-logo-notext.ico$\""
+	WriteRegStr HKLM "${REGSTR}" "Publisher" "sigrok"
+	WriteRegStr HKLM "${REGSTR}" "HelpLink" \
+		"http://sigrok.org/wiki/PulseView"
+	WriteRegStr HKLM "${REGSTR}" "URLUpdateInfo" \
+		"http://sigrok.org/wiki/Downloads"
+	WriteRegStr HKLM "${REGSTR}" "URLInfoAbout" "http://sigrok.org"
+	WriteRegStr HKLM "${REGSTR}" "DisplayVersion" "0.2.0"
+	WriteRegStr HKLM "${REGSTR}" "Contact" \
+		"sigrok-devel at lists.sourceforge.org"
+	WriteRegStr HKLM "${REGSTR}" "Comments" \
+		"This is a Qt based sigrok GUI."
+
+	# Display "Remove" instead of "Modify/Remove" in the control panel.
+	WriteRegDWORD HKLM "${REGSTR}" "NoModify" 1
+	WriteRegDWORD HKLM "${REGSTR}" "NoRepair" 1
+
+SectionEnd
+
+
+# --- Uninstaller section -----------------------------------------------------
+
+Section "Uninstall"
+
+	# Always delete the uninstaller first (yes, this really works).
+	Delete "$INSTDIR\Uninstall.exe"
+
+	# Delete the application, the application data, and related libs.
+	Delete "$INSTDIR\COPYING"
+	Delete "$INSTDIR\pulseview.exe"
+	Delete "$INSTDIR\libusb0.dll"
+	Delete "$INSTDIR\zadig.exe"
+	Delete "$INSTDIR\zadig_xp.exe"
+	Delete "$INSTDIR\python32.dll"
+	Delete "$INSTDIR\python32.zip"
+
+	# Delete all decoders and everything else in decoders/.
+	# There could be *.pyc files or __pycache__ subdirs and so on.
+	RMDir /r "$INSTDIR\decoders\*"
+
+	# Delete the firmware files.
+	RMDir /r "$INSTDIR\firmware\*"
+
+	# Delete the example *.sr files.
+	RMDir /r "$INSTDIR\examples\*"
+
+	# Delete the install directory and its sub-directories.
+	RMDir "$INSTDIR\decoders"
+	RMDir "$INSTDIR\firmware"
+	RMDir "$INSTDIR\examples"
+	RMDir "$INSTDIR"
+
+	# Delete the links from the start menu.
+	Delete "$SMPROGRAMS\sigrok\PulseView\PulseView.lnk"
+	Delete "$SMPROGRAMS\sigrok\PulseView\Uninstall.lnk"
+
+	# Delete the sub-directory in the start menu.
+	RMDir "$SMPROGRAMS\sigrok\PulseView"
+	RMDir "$SMPROGRAMS\sigrok"
+
+	# Delete the registry key(s).
+	DeleteRegKey HKLM "${REGSTR}"
+
+SectionEnd
+
+
+# --- Component selection section descriptions --------------------------------
+
+LangString DESC_Section1 ${LANG_ENGLISH} "This installs the PulseView sigrok GUI, some firmware files, the protocol decoders, some example files, and all required libraries."
+
+!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
+!insertmacro MUI_DESCRIPTION_TEXT ${Section1} $(DESC_Section1)
+!insertmacro MUI_FUNCTION_DESCRIPTION_END
+
diff --git a/doc/pulseview.1 b/doc/pulseview.1
new file mode 100644
index 0000000..b830f08
--- /dev/null
+++ b/doc/pulseview.1
@@ -0,0 +1,59 @@
+.TH PULSEVIEW 1 "May 4, 2013"
+.SH "NAME"
+PulseView \- Qt-based LA/scope/MSO GUI for sigrok
+.SH "SYNOPSIS"
+.B pulseview \fR[\fB\-lh?V\fR] [\fB\-l\fR|\fB\-\-loglevel\fR] [\fB\-h\fR|\fB\-?\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fBfile.sr\fR]
+.SH "DESCRIPTION"
+.B PulseView
+is a cross-platform Qt-based GUI for the
+.B sigrok
+software suite for test and measurement equipment such as logic analyzers,
+oscilloscopes, MSOs, and more.
+.SH "OPTIONS"
+.B PulseView
+has very few command line options, as most configuration elements are
+available from the GUI itself.
+.sp
+If the optional \fBfile.sr\fR argument is supplied, PulseView tries to open
+the specified file. It has to be in the "libsigrok session" format.
+.TP
+.B "\-l, \-\-loglevel"
+Set the libsigrok and libsigrokdecode loglevel. At the moment PulseView
+doesn't support setting the two loglevels independently. The higher the
+number, the more debug output will be printed. Valid loglevels are:
+.sp
+\fB0\fP   None
+.br
+\fB1\fP   Error
+.br
+\fB2\fP   Warnings
+.br
+\fB3\fP   Informational
+.br
+\fB4\fP   Debug
+.br
+\fB5\fP   Spew
+.TP
+.B "\-h, \-?, \-\-help"
+Show a help text and exit.
+.TP
+.B "\-V, \-\-version"
+Show version information and exit.
+.SH "EXIT STATUS"
+.B PulseView
+exits with 0 on success, 1 on most failures.
+.SH "SEE ALSO"
+\fBsigrok\-cli\fP(1)
+.SH "BUGS"
+Please report any bugs via Bugzilla
+.RB "(" http://sigrok.org/bugzilla ")"
+or on the sigrok\-devel mailing list
+.RB "(" sigrok\-devel at lists.souceforge.net ")."
+.SH "LICENSE"
+.B PulseView
+is covered by the GNU General Public License (GPL), version 3 or later.
+.SH "AUTHORS"
+Please see the individual source code files.
+.PP
+This manual page was written by Uwe Hermann <uwe at hermann\-uwe.de>.
+It is licensed under the terms of the GNU GPL (version 2 or later).
diff --git a/extdef.h b/extdef.h
new file mode 100644
index 0000000..8af615c
--- /dev/null
+++ b/extdef.h
@@ -0,0 +1,29 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_EXTDEF_H
+#define PULSEVIEW_EXTDEF_H
+
+#define countof(x) (sizeof(x)/sizeof(x[0]))
+
+#define begin_element(x) (&x[0])
+#define end_element(x) (&x[countof(x)])
+
+#endif // PULSEVIEW_EXTDEF_H
diff --git a/icons/application-exit.png b/icons/application-exit.png
new file mode 100644
index 0000000..28ac66f
Binary files /dev/null and b/icons/application-exit.png differ
diff --git a/icons/configure.png b/icons/configure.png
new file mode 100644
index 0000000..cc91d65
Binary files /dev/null and b/icons/configure.png differ
diff --git a/icons/decoder-delete.svg b/icons/decoder-delete.svg
new file mode 100644
index 0000000..b9db4ea
--- /dev/null
+++ b/icons/decoder-delete.svg
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg id="svg2989" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <metadata id="metadata2994">
+  <rdf:RDF>
+   <cc:Work rdf:about="">
+    <dc:format>image/svg+xml</dc:format>
+    <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+    <dc:title/>
+   </cc:Work>
+  </rdf:RDF>
+ </metadata>
+ <g id="layer1" stroke-linejoin="miter" style="stroke-dasharray:none;" transform="translate(0,-1036.3622)" stroke="#000" stroke-linecap="square" stroke-miterlimit="4" stroke-width="2" fill="none">
+  <path id="path3007" style="stroke-dasharray:none;" d="m11,1047.4-6-6"/>
+  <path id="path2986" style="stroke-dasharray:none;" d="m5,1047.4,6-6"/>
+ </g>
+</svg>
diff --git a/icons/decoder-hidden.svg b/icons/decoder-hidden.svg
new file mode 100644
index 0000000..d462200
--- /dev/null
+++ b/icons/decoder-hidden.svg
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   version="1.1"
+   width="22"
+   height="22"
+   id="svg2989">
+  <defs
+     id="defs2991" />
+  <metadata
+     id="metadata2994">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <path
+     d="m 19.426489,11.070664 a 11,11 0 0 1 -16.8529782,-1e-6"
+     transform="matrix(1.1867339,0,0,1.1867339,-2.0540724,-2.1379314)"
+     id="path3053"
+     style="fill:none;stroke:#000000;stroke-width:1.26397336" />
+</svg>
diff --git a/icons/decoder-shown.svg b/icons/decoder-shown.svg
new file mode 100644
index 0000000..6050bdc
--- /dev/null
+++ b/icons/decoder-shown.svg
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   version="1.1"
+   width="22"
+   height="22"
+   id="svg2989">
+  <defs
+     id="defs2991" />
+  <metadata
+     id="metadata2994">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <path
+     d="M 13.390812,-9.3707772 A 5.87888,5.87888 0 0 1 14.476134,0.74106762"
+     transform="matrix(0.89442417,0,0,0.89442417,1.1613345,14.644558)"
+     id="path2989"
+     style="fill:none;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" />
+  <path
+     d="m 12,-4 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
+     transform="matrix(1.3065319,0,0,1.3065319,-3.3718505,16.219638)"
+     id="path2995"
+     style="fill:#000000;stroke:none" />
+  <path
+     d="M 10.875,6.34375 C 7.9493854,6.36765 5.0099958,7.3787791 2.59375,9.40625 2.0116039,9.8947285 1.4884785,10.417854 1,11 1.4884785,11.582146 2.0116039,12.105271 2.59375,12.59375 8.1165975,17.227969 16.365781,16.522848 21,11 18.393252,7.8933983 14.636505,6.3130214 10.875,6.34375 z"
+     id="path3053"
+     style="fill:none;stroke:#000000;stroke-width:1.5" />
+  <path
+     d="M 7.5357022,0.74972325 A 5.87888,5.87888 0 0 1 8.1948438,-9.1664619"
+     transform="matrix(0.89442417,0,0,0.89442417,1.1613345,14.644558)"
+     id="path3057"
+     style="fill:none;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" />
+</svg>
diff --git a/icons/document-open.png b/icons/document-open.png
new file mode 100644
index 0000000..254a6b8
Binary files /dev/null and b/icons/document-open.png differ
diff --git a/icons/document-save-as.png b/icons/document-save-as.png
new file mode 100644
index 0000000..0f7b299
Binary files /dev/null and b/icons/document-save-as.png differ
diff --git a/icons/probes.svg b/icons/probes.svg
new file mode 100644
index 0000000..8e7e604
--- /dev/null
+++ b/icons/probes.svg
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   version="1.1"
+   width="22"
+   height="22"
+   id="svg2">
+  <metadata
+     id="metadata27">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs4">
+    <radialGradient
+       cx="15"
+       cy="20.5"
+       r="5.8534002"
+       id="radialGradient3934"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.0000001,4.9521507e-8,-2.6903182e-6,0.42710347,6.4579919e-5,11.744378)">
+      <stop
+         id="stop3930"
+         style="stop-color:#888a85;stop-opacity:0.5299539"
+         offset="0" />
+      <stop
+         id="stop3932"
+         style="stop-color:#888a85;stop-opacity:0"
+         offset="1" />
+    </radialGradient>
+    <radialGradient
+       cx="15"
+       cy="20.5"
+       r="5.8534002"
+       id="radialGradient3009"
+       xlink:href="#radialGradient3934"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.8792554,3.9617209e-8,-5.0557945e-6,0.34168281,-17.188707,12.995502)" />
+  </defs>
+  <path
+     d="M 21.999281,20 A 11.00022,2.0000344 0 0 1 -0.00115994,20 11.00022,2.0000344 0 1 1 21.999281,20 z"
+     id="path3926"
+     style="fill:url(#radialGradient3009);fill-rule:evenodd" />
+  <path
+     d="m 10,8 1.5,-1.5 3,3 L 13,11 z"
+     id="path3806"
+     style="fill:#cc0000;stroke:#a40000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+  <path
+     d="m 1.5,19.5 2,-2"
+     id="path3808"
+     style="fill:none;stroke:#888a85;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
+  <path
+     d="M 9.5,7.5 10,10 3.9999998,16 2,18.5 2.5,19 4.9999998,17 11,11 l 2.5,0.5 z"
+     id="path3804"
+     style="fill:#cc0000;stroke:#a40000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" />
+  <path
+     d="M 16.5,0.5 15.545182,1.4548178 15.5,2.5 15,3 14.433209,2.5667912 11,6 15,10 18.5,6.5 18,6 l 0.5,-0.5 1,0 1,-1 C 19,4 17,2 16.5,0.5 z"
+     id="path3793"
+     style="fill:#cc0000;stroke:#a40000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" />
+</svg>
diff --git a/icons/sigrok-logo-notext.ico b/icons/sigrok-logo-notext.ico
new file mode 100644
index 0000000..8f1228c
Binary files /dev/null and b/icons/sigrok-logo-notext.ico differ
diff --git a/icons/sigrok-logo-notext.png b/icons/sigrok-logo-notext.png
new file mode 100644
index 0000000..cf01c3f
Binary files /dev/null and b/icons/sigrok-logo-notext.png differ
diff --git a/icons/status-green.svg b/icons/status-green.svg
new file mode 100644
index 0000000..e9ba2e8
--- /dev/null
+++ b/icons/status-green.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg id="svg4376" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.0" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <defs id="defs3">
+  <radialGradient id="radialGradient3774" gradientUnits="userSpaceOnUse" cy="6" cx="7.5" gradientTransform="matrix(1.0799,0,0,1.0799,-2.4045,-1.3652)" r="7.5">
+   <stop id="stop3292" stop-color="#c1f093" offset="0"/>
+   <stop id="stop3294" stop-color="#8eea34" offset=".64486"/>
+   <stop id="stop3296" stop-color="#73d216" offset="1"/>
+  </radialGradient>
+ </defs>
+ <path id="path2996" stroke-linejoin="round" d="m17 7.5a8.5 8.5 0 1 1 -17 0 8.5 8.5 0 1 1 17 0z" transform="translate(2.5,3.5)" stroke="#3b7404" stroke-linecap="round" fill="url(#radialGradient3774)"/>
+ <path id="path3766" stroke-linejoin="round" d="m16 7.5a7.5 7.5 0 1 1 -15 0 7.5 7.5 0 1 1 15 0z" stroke-opacity=".64338" transform="translate(2.5,3.5)" stroke="#fff" stroke-linecap="round" fill="none"/>
+</svg>
diff --git a/icons/status-grey.svg b/icons/status-grey.svg
new file mode 100644
index 0000000..33f75d8
--- /dev/null
+++ b/icons/status-grey.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg id="svg4376" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.0" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <defs id="defs3">
+  <radialGradient id="radialGradient3774" gradientUnits="userSpaceOnUse" cy="6" cx="7.5" gradientTransform="matrix(1.0799,0,0,1.0799,-2.4045,-1.3652)" r="7.5">
+   <stop id="stop3778" stop-color="#d3d7cf" offset="0"/>
+   <stop id="stop3780" stop-color="#babdb6" offset=".64486"/>
+   <stop id="stop3782" stop-color="#888a85" offset="1"/>
+  </radialGradient>
+ </defs>
+ <path id="path2996" stroke-linejoin="round" d="m17 7.5a8.5 8.5 0 1 1 -17 0 8.5 8.5 0 1 1 17 0z" transform="translate(2.5,3.5)" stroke="#555753" stroke-linecap="round" fill="url(#radialGradient3774)"/>
+ <path id="path3766" stroke-linejoin="round" d="m16 7.5a7.5 7.5 0 1 1 -15 0 7.5 7.5 0 1 1 15 0z" stroke-opacity=".64338" transform="translate(2.5,3.5)" stroke="#fff" stroke-linecap="round" fill="none"/>
+</svg>
diff --git a/icons/status-red.svg b/icons/status-red.svg
new file mode 100644
index 0000000..593b307
--- /dev/null
+++ b/icons/status-red.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg id="svg4376" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.0" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <defs id="defs3">
+  <radialGradient id="radialGradient3774" gradientUnits="userSpaceOnUse" cy="6" cx="7.5" gradientTransform="matrix(1.0799,0,0,1.0799,-2.4045,-1.3652)" r="7.5">
+   <stop id="stop3292" stop-color="#f24e4e" offset="0"/>
+   <stop id="stop3294" stop-color="#c00" offset=".64486"/>
+   <stop id="stop3296" stop-color="#a40000" offset="1"/>
+  </radialGradient>
+ </defs>
+ <path id="path2996" stroke-linejoin="round" d="m17 7.5a8.5 8.5 0 1 1 -17 0 8.5 8.5 0 1 1 17 0z" transform="translate(2.5,3.5)" stroke="#780000" stroke-linecap="round" fill="url(#radialGradient3774)"/>
+ <path id="path3766" stroke-linejoin="round" d="m16 7.5a7.5 7.5 0 1 1 -15 0 7.5 7.5 0 1 1 15 0z" stroke-opacity=".64338" transform="translate(2.5,3.5)" stroke="#fff" stroke-linecap="round" fill="none"/>
+</svg>
diff --git a/icons/trigger-change.svg b/icons/trigger-change.svg
new file mode 100644
index 0000000..1786d4b
--- /dev/null
+++ b/icons/trigger-change.svg
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   version="1.1"
+   width="22"
+   height="22"
+   id="svg2989"
+   inkscape:version="0.48.3.1 r9886"
+   sodipodi:docname="trigger-rising.svg">
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1680"
+     inkscape:window-height="995"
+     id="namedview7"
+     showgrid="true"
+     inkscape:zoom="36.791193"
+     inkscape:cx="7.942203"
+     inkscape:cy="10.877688"
+     inkscape:window-x="1278"
+     inkscape:window-y="-3"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg2989">
+    <inkscape:grid
+       type="xygrid"
+       id="grid2984"
+       empspacing="2"
+       visible="true"
+       enabled="true"
+       snapvisiblegridlinesonly="true" />
+  </sodipodi:namedview>
+  <defs
+     id="defs2991" />
+  <metadata
+     id="metadata2994">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     transform="translate(0,-1030.3622)"
+     id="layer1">
+    <path
+       d="m 4,1048.3622 5,0 4,-14 5,0"
+       id="path3007"
+       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:square;stroke-linejoin:miter;stroke-opacity:1"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cccc" />
+    <path
+       sodipodi:nodetypes="cccc"
+       inkscape:connector-curvature="0"
+       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:square;stroke-linejoin:miter;stroke-opacity:1"
+       id="path3012"
+       d="m 4,1034.3622 5,0 4,14 5,0" />
+  </g>
+</svg>
diff --git a/icons/trigger-falling.svg b/icons/trigger-falling.svg
new file mode 100644
index 0000000..24bd4f6
--- /dev/null
+++ b/icons/trigger-falling.svg
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   version="1.1"
+   width="22"
+   height="22"
+   id="svg2989">
+  <defs
+     id="defs2991" />
+  <metadata
+     id="metadata2994">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     transform="translate(0,-1030.3622)"
+     id="layer1">
+    <path
+       d="m 18,1048.3622 -5,0 -4,-14 -5,0"
+       id="path3007"
+       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:square;stroke-linejoin:miter;stroke-opacity:1" />
+  </g>
+</svg>
diff --git a/icons/trigger-high.svg b/icons/trigger-high.svg
new file mode 100644
index 0000000..d2150d8
--- /dev/null
+++ b/icons/trigger-high.svg
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   version="1.1"
+   width="22"
+   height="22"
+   id="svg2989">
+  <defs
+     id="defs2991" />
+  <metadata
+     id="metadata2994">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     transform="translate(0,-1030.3622)"
+     id="layer1">
+    <path
+       d="m 4,1034.3622 14,0"
+       id="path3007"
+       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:square;stroke-linejoin:miter;stroke-opacity:1" />
+  </g>
+</svg>
diff --git a/icons/trigger-low.svg b/icons/trigger-low.svg
new file mode 100644
index 0000000..aa71a98
--- /dev/null
+++ b/icons/trigger-low.svg
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   version="1.1"
+   width="22"
+   height="22"
+   id="svg2989">
+  <defs
+     id="defs2991" />
+  <metadata
+     id="metadata2994">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     transform="translate(0,-1030.3622)"
+     id="layer1">
+    <path
+       d="m 4,1048.3622 14,0"
+       id="path3007"
+       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:square;stroke-linejoin:miter;stroke-opacity:1" />
+  </g>
+</svg>
diff --git a/icons/trigger-none.svg b/icons/trigger-none.svg
new file mode 100644
index 0000000..196264d
--- /dev/null
+++ b/icons/trigger-none.svg
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   version="1.1"
+   width="22"
+   height="22"
+   id="svg2989">
+  <defs
+     id="defs2991" />
+  <metadata
+     id="metadata2994">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <rect
+     width="4"
+     height="4"
+     x="9"
+     y="9"
+     id="rect3033"
+     style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none" />
+</svg>
diff --git a/icons/trigger-rising.svg b/icons/trigger-rising.svg
new file mode 100644
index 0000000..ac1550f
--- /dev/null
+++ b/icons/trigger-rising.svg
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   version="1.1"
+   width="22"
+   height="22"
+   id="svg2989">
+  <defs
+     id="defs2991" />
+  <metadata
+     id="metadata2994">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     transform="translate(0,-1030.3622)"
+     id="layer1">
+    <path
+       d="m 4,1048.3622 5,0 4,-14 5,0"
+       id="path3007"
+       style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:square;stroke-linejoin:miter;stroke-opacity:1" />
+  </g>
+</svg>
diff --git a/icons/zoom-fit.png b/icons/zoom-fit.png
new file mode 100644
index 0000000..babe581
Binary files /dev/null and b/icons/zoom-fit.png differ
diff --git a/icons/zoom-in.png b/icons/zoom-in.png
new file mode 100644
index 0000000..55f9ae1
Binary files /dev/null and b/icons/zoom-in.png differ
diff --git a/icons/zoom-original.png b/icons/zoom-original.png
new file mode 100644
index 0000000..8ae3043
Binary files /dev/null and b/icons/zoom-original.png differ
diff --git a/icons/zoom-out.png b/icons/zoom-out.png
new file mode 100644
index 0000000..9f7627e
Binary files /dev/null and b/icons/zoom-out.png differ
diff --git a/main.cpp b/main.cpp
new file mode 100644
index 0000000..2c6a59e
--- /dev/null
+++ b/main.cpp
@@ -0,0 +1,181 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifdef ENABLE_DECODE
+#include <libsigrokdecode/libsigrokdecode.h> /* First, so we avoid a _POSIX_C_SOURCE warning. */
+#endif
+
+#include <stdint.h>
+#include <libsigrok/libsigrok.h>
+
+#include <getopt.h>
+
+#include <QtGui/QApplication>
+#include <QDebug>
+
+#ifdef ENABLE_SIGNALS
+#include "signalhandler.h"
+#endif
+
+#include "pv/devicemanager.h"
+#include "pv/mainwindow.h"
+
+#include "config.h"
+
+#ifdef _WIN32
+// The static qsvg lib is required for SVG graphics/icons (on Windows).
+#include <QtPlugin>
+Q_IMPORT_PLUGIN(qsvg)
+#endif
+
+void usage()
+{
+	fprintf(stdout,
+		"Usage:\n"
+		"  %s [OPTION…] [FILE] — %s\n"
+		"\n"
+		"Help Options:\n"
+		"  -l, --loglevel                  Set libsigrok/libsigrokdecode loglevel\n"
+		"  -V, --version                   Show release version\n"
+		"  -h, -?, --help                  Show help option\n"
+		"\n", PV_BIN_NAME, PV_DESCRIPTION);
+}
+
+int main(int argc, char *argv[])
+{
+	int ret = 0;
+	struct sr_context *sr_ctx = NULL;
+	const char *open_file = NULL;
+
+	QApplication a(argc, argv);
+
+	// Set some application metadata
+	QApplication::setApplicationVersion(PV_VERSION_STRING);
+	QApplication::setApplicationName("PulseView");
+	QApplication::setOrganizationDomain("http://www.sigrok.org");
+
+	// Parse arguments
+	while (1) {
+		static const struct option long_options[] = {
+			{"loglevel", required_argument, 0, 'l'},
+			{"version", no_argument, 0, 'V'},
+			{"help", no_argument, 0, 'h'},
+			{0, 0, 0, 0}
+		};
+
+		const int c = getopt_long(argc, argv,
+			"l:Vh?", long_options, NULL);
+		if (c == -1)
+			break;
+
+		switch (c) {
+		case 'l':
+		{
+			const int loglevel = atoi(optarg);
+			sr_log_loglevel_set(loglevel);
+
+#ifdef ENABLE_DECODE
+			srd_log_loglevel_set(loglevel);
+#endif
+
+			break;
+		}
+
+		case 'V':
+			// Print version info
+			fprintf(stdout, "%s %s\n", PV_TITLE, PV_VERSION_STRING);
+			return 0;
+
+		case 'h':
+		case '?':
+			usage();
+			return 0;
+		}
+	}
+
+	if (argc - optind > 1) {
+		fprintf(stderr, "Only one file can be openened.\n");
+		return 1;
+	} else if (argc - optind == 1)
+		open_file = argv[argc - 1];
+
+	// Initialise libsigrok
+	if (sr_init(&sr_ctx) != SR_OK) {
+		qDebug() << "ERROR: libsigrok init failed.";
+		return 1;
+	}
+
+	do {
+
+#ifdef ENABLE_DECODE
+		// Initialise libsigrokdecode
+		if (srd_init(NULL) != SRD_OK) {
+			qDebug() << "ERROR: libsigrokdecode init failed.";
+			break;
+		}
+
+		// Load the protocol decoders
+		srd_decoder_load_all();
+#endif
+
+		try {
+			// Create the device manager, initialise the drivers
+			pv::DeviceManager device_manager(sr_ctx);
+
+			// Initialise the main window
+			pv::MainWindow w(device_manager, open_file);
+			w.show();
+
+#ifdef ENABLE_SIGNALS
+			if(SignalHandler::prepare_signals()) {
+				SignalHandler *const handler =
+					new SignalHandler(&w);
+				QObject::connect(handler,
+					SIGNAL(int_received()),
+					&w, SLOT(close()));
+				QObject::connect(handler,
+					SIGNAL(term_received()),
+					&w, SLOT(close()));
+    			} else {
+				qWarning() <<
+					"Could not prepare signal handler.";
+			}
+#endif
+
+			// Run the application
+			ret = a.exec();
+
+		} catch(std::exception e) {
+			qDebug() << e.what();
+		}
+
+#ifdef ENABLE_DECODE
+		// Destroy libsigrokdecode
+		srd_exit();
+#endif
+
+	} while (0);
+
+	// Destroy libsigrok
+	if (sr_ctx)
+		sr_exit(sr_ctx);
+
+	return ret;
+}
diff --git a/pulseview.qrc b/pulseview.qrc
new file mode 100644
index 0000000..98edc39
--- /dev/null
+++ b/pulseview.qrc
@@ -0,0 +1,26 @@
+<RCC>
+    <qresource prefix="/" >
+	<file>icons/application-exit.png</file>
+	<file>icons/configure.png</file>
+	<file>icons/decoder-delete.svg</file>
+	<file>icons/decoder-hidden.svg</file>
+	<file>icons/decoder-shown.svg</file>
+	<file>icons/document-open.png</file>
+	<file>icons/document-save-as.png</file>
+	<file>icons/probes.svg</file>
+	<file>icons/sigrok-logo-notext.png</file>
+	<file>icons/status-green.svg</file>
+	<file>icons/status-grey.svg</file>
+	<file>icons/status-red.svg</file>
+	<file>icons/trigger-change.svg</file>
+	<file>icons/trigger-falling.svg</file>
+	<file>icons/trigger-high.svg</file>
+	<file>icons/trigger-low.svg</file>
+	<file>icons/trigger-none.svg</file>
+	<file>icons/trigger-rising.svg</file>
+	<file>icons/zoom-fit.png</file>
+	<file>icons/zoom-in.png</file>
+	<file>icons/zoom-original.png</file>
+	<file>icons/zoom-out.png</file>
+    </qresource>
+</RCC>
diff --git a/pulseviewico.rc b/pulseviewico.rc
new file mode 100644
index 0000000..4ba6fd0
--- /dev/null
+++ b/pulseviewico.rc
@@ -0,0 +1 @@
+IDI_ICON1	ICON	DISCARDABLE	"icons/sigrok-logo-notext.ico"
diff --git a/pv/data/analog.cpp b/pv/data/analog.cpp
new file mode 100644
index 0000000..21ddb5f
--- /dev/null
+++ b/pv/data/analog.cpp
@@ -0,0 +1,64 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <boost/foreach.hpp>
+
+#include "analog.h"
+#include "analogsnapshot.h"
+
+using boost::shared_ptr;
+using std::deque;
+using std::max;
+
+namespace pv {
+namespace data {
+
+Analog::Analog() :
+	SignalData()
+{
+}
+
+void Analog::push_snapshot(shared_ptr<AnalogSnapshot> &snapshot)
+{
+	_snapshots.push_front(snapshot);
+}
+
+deque< shared_ptr<AnalogSnapshot> >& Analog::get_snapshots()
+{
+	return _snapshots;
+}
+
+void Analog::clear()
+{
+	_snapshots.clear();
+}
+
+uint64_t Analog::get_max_sample_count() const
+{
+	uint64_t l = 0;
+	BOOST_FOREACH(const boost::shared_ptr<AnalogSnapshot> s, _snapshots) {
+		assert(s);
+		l = max(l, s->get_sample_count());
+	}
+	return l;
+}
+
+} // namespace data
+} // namespace pv
diff --git a/pv/data/analog.h b/pv/data/analog.h
new file mode 100644
index 0000000..42e3167
--- /dev/null
+++ b/pv/data/analog.h
@@ -0,0 +1,56 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_DATA_ANALOG_H
+#define PULSEVIEW_PV_DATA_ANALOG_H
+
+#include "signaldata.h"
+
+#include <boost/shared_ptr.hpp>
+#include <deque>
+
+namespace pv {
+namespace data {
+
+class AnalogSnapshot;
+
+class Analog : public SignalData
+{
+public:
+	Analog();
+
+	void push_snapshot(
+		boost::shared_ptr<AnalogSnapshot> &snapshot);
+
+	std::deque< boost::shared_ptr<AnalogSnapshot> >&
+		get_snapshots();
+
+	void clear();
+
+	uint64_t get_max_sample_count() const;
+
+private:
+	std::deque< boost::shared_ptr<AnalogSnapshot> > _snapshots;
+};
+
+} // namespace data
+} // namespace pv
+
+#endif // PULSEVIEW_PV_DATA_ANALOG_H
diff --git a/pv/data/analogsnapshot.cpp b/pv/data/analogsnapshot.cpp
new file mode 100644
index 0000000..4d4f5d6
--- /dev/null
+++ b/pv/data/analogsnapshot.cpp
@@ -0,0 +1,215 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <extdef.h>
+
+#include <assert.h>
+#include <string.h>
+#include <stdlib.h>
+#include <math.h>
+
+#include <algorithm>
+
+#include <boost/foreach.hpp>
+
+#include "analogsnapshot.h"
+
+using boost::lock_guard;
+using boost::recursive_mutex;
+using std::max;
+using std::max_element;
+using std::min;
+using std::min_element;
+
+namespace pv {
+namespace data {
+
+const int AnalogSnapshot::EnvelopeScalePower = 4;
+const int AnalogSnapshot::EnvelopeScaleFactor = 1 << EnvelopeScalePower;
+const float AnalogSnapshot::LogEnvelopeScaleFactor =
+	logf(EnvelopeScaleFactor);
+const uint64_t AnalogSnapshot::EnvelopeDataUnit = 64*1024;	// bytes
+
+AnalogSnapshot::AnalogSnapshot(const uint64_t expected_num_samples) :
+	Snapshot(sizeof(float))
+{
+	set_capacity(expected_num_samples);
+
+	lock_guard<recursive_mutex> lock(_mutex);
+	memset(_envelope_levels, 0, sizeof(_envelope_levels));
+}
+
+AnalogSnapshot::~AnalogSnapshot()
+{
+	lock_guard<recursive_mutex> lock(_mutex);
+	BOOST_FOREACH(Envelope &e, _envelope_levels)
+		free(e.samples);
+}
+
+void AnalogSnapshot::append_interleaved_samples(const float *data,
+	size_t sample_count, size_t stride)
+{
+	assert(_unit_size == sizeof(float));
+
+	lock_guard<recursive_mutex> lock(_mutex);
+
+	_data = realloc(_data, (_sample_count + sample_count) * sizeof(float));
+
+	float *dst = (float*)_data + _sample_count;
+	const float *dst_end = dst + sample_count;
+	while (dst != dst_end)
+	{
+		*dst++ = *data;
+		data += stride;
+	}
+
+	_sample_count += sample_count;
+
+	// Generate the first mip-map from the data
+	append_payload_to_envelope_levels();
+}
+
+const float* AnalogSnapshot::get_samples(
+	int64_t start_sample, int64_t end_sample) const
+{
+	assert(start_sample >= 0);
+	assert(start_sample < (int64_t)_sample_count);
+	assert(end_sample >= 0);
+	assert(end_sample < (int64_t)_sample_count);
+	assert(start_sample <= end_sample);
+
+	lock_guard<recursive_mutex> lock(_mutex);
+
+	float *const data = new float[end_sample - start_sample];
+	memcpy(data, (float*)_data + start_sample, sizeof(float) *
+		(end_sample - start_sample));
+	return data;
+}
+
+void AnalogSnapshot::get_envelope_section(EnvelopeSection &s,
+	uint64_t start, uint64_t end, float min_length) const
+{
+	assert(end <= get_sample_count());
+	assert(start <= end);
+	assert(min_length > 0);
+
+	lock_guard<recursive_mutex> lock(_mutex);
+
+	const unsigned int min_level = max((int)floorf(logf(min_length) /
+		LogEnvelopeScaleFactor) - 1, 0);
+	const unsigned int scale_power = (min_level + 1) *
+		EnvelopeScalePower;
+	start >>= scale_power;
+	end >>= scale_power;
+
+	s.start = start << scale_power;
+	s.scale = 1 << scale_power;
+	s.length = end - start;
+	s.samples = new EnvelopeSample[s.length];
+	memcpy(s.samples, _envelope_levels[min_level].samples + start,
+		s.length * sizeof(EnvelopeSample));
+}
+
+void AnalogSnapshot::reallocate_envelope(Envelope &e)
+{
+	const uint64_t new_data_length = ((e.length + EnvelopeDataUnit - 1) /
+		EnvelopeDataUnit) * EnvelopeDataUnit;
+	if (new_data_length > e.data_length)
+	{
+		e.data_length = new_data_length;
+		e.samples = (EnvelopeSample*)realloc(e.samples,
+			new_data_length * sizeof(EnvelopeSample));
+	}
+}
+
+void AnalogSnapshot::append_payload_to_envelope_levels()
+{
+	Envelope &e0 = _envelope_levels[0];
+	uint64_t prev_length;
+	EnvelopeSample *dest_ptr;
+
+	// Expand the data buffer to fit the new samples
+	prev_length = e0.length;
+	e0.length = _sample_count / EnvelopeScaleFactor;
+
+	// Break off if there are no new samples to compute
+	if (e0.length == prev_length)
+		return;
+
+	reallocate_envelope(e0);
+
+	dest_ptr = e0.samples + prev_length;
+
+	// Iterate through the samples to populate the first level mipmap
+	const float *const end_src_ptr = (float*)_data +
+		e0.length * EnvelopeScaleFactor;
+	for (const float *src_ptr = (float*)_data +
+		prev_length * EnvelopeScaleFactor;
+		src_ptr < end_src_ptr; src_ptr += EnvelopeScaleFactor)
+	{
+		const EnvelopeSample sub_sample = {
+			*min_element(src_ptr, src_ptr + EnvelopeScaleFactor),
+			*max_element(src_ptr, src_ptr + EnvelopeScaleFactor),
+		};
+
+		*dest_ptr++ = sub_sample;
+	}
+
+	// Compute higher level mipmaps
+	for (unsigned int level = 1; level < ScaleStepCount; level++)
+	{
+		Envelope &e = _envelope_levels[level];
+		const Envelope &el = _envelope_levels[level-1];
+
+		// Expand the data buffer to fit the new samples
+		prev_length = e.length;
+		e.length = el.length / EnvelopeScaleFactor;
+
+		// Break off if there are no more samples to computed
+		if (e.length == prev_length)
+			break;
+
+		reallocate_envelope(e);
+
+		// Subsample the level lower level
+		const EnvelopeSample *src_ptr =
+			el.samples + prev_length * EnvelopeScaleFactor;
+		const EnvelopeSample *const end_dest_ptr = e.samples + e.length;
+		for (dest_ptr = e.samples + prev_length;
+			dest_ptr < end_dest_ptr; dest_ptr++)
+		{
+			const EnvelopeSample *const end_src_ptr =
+				src_ptr + EnvelopeScaleFactor;
+
+			EnvelopeSample sub_sample = *src_ptr++;
+			while (src_ptr < end_src_ptr)
+			{
+				sub_sample.min = min(sub_sample.min, src_ptr->min);
+				sub_sample.max = max(sub_sample.max, src_ptr->max);
+				src_ptr++;
+			}
+
+			*dest_ptr = sub_sample;
+		}
+	}
+}
+
+} // namespace data
+} // namespace pv
diff --git a/pv/data/analogsnapshot.h b/pv/data/analogsnapshot.h
new file mode 100644
index 0000000..b60c2ce
--- /dev/null
+++ b/pv/data/analogsnapshot.h
@@ -0,0 +1,96 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_DATA_ANALOGSNAPSHOT_H
+#define PULSEVIEW_PV_DATA_ANALOGSNAPSHOT_H
+
+#include "snapshot.h"
+
+#include <utility>
+#include <vector>
+
+namespace AnalogSnapshotTest {
+class Basic;
+}
+
+namespace pv {
+namespace data {
+
+class AnalogSnapshot : public Snapshot
+{
+public:
+	struct EnvelopeSample
+	{
+		float min;
+		float max;
+	};
+
+	struct EnvelopeSection
+	{
+		uint64_t start;
+		unsigned int scale;
+		uint64_t length;
+		EnvelopeSample *samples;
+	};
+
+private:
+	struct Envelope
+	{
+		uint64_t length;
+		uint64_t data_length;
+		EnvelopeSample *samples;
+	};
+
+private:
+	static const unsigned int ScaleStepCount = 10;
+	static const int EnvelopeScalePower;
+	static const int EnvelopeScaleFactor;
+	static const float LogEnvelopeScaleFactor;
+	static const uint64_t EnvelopeDataUnit;
+
+public:
+	AnalogSnapshot(uint64_t expected_num_samples = 0);
+
+	virtual ~AnalogSnapshot();
+
+	void append_interleaved_samples(const float *data,
+		size_t sample_count, size_t stride);
+
+	const float* get_samples(int64_t start_sample,
+		int64_t end_sample) const;
+
+	void get_envelope_section(EnvelopeSection &s,
+		uint64_t start, uint64_t end, float min_length) const;
+
+private:
+	void reallocate_envelope(Envelope &l);
+
+	void append_payload_to_envelope_levels();
+
+private:
+	struct Envelope _envelope_levels[ScaleStepCount];
+
+	friend class AnalogSnapshotTest::Basic;
+};
+
+} // namespace data
+} // namespace pv
+
+#endif // PULSEVIEW_PV_DATA_ANALOGSNAPSHOT_H
diff --git a/pv/data/decode/annotation.cpp b/pv/data/decode/annotation.cpp
new file mode 100644
index 0000000..b0517ae
--- /dev/null
+++ b/pv/data/decode/annotation.cpp
@@ -0,0 +1,73 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+extern "C" {
+#include <libsigrokdecode/libsigrokdecode.h>
+}
+
+#include <vector>
+
+#include "annotation.h"
+
+namespace pv {
+namespace data {
+namespace decode {
+
+Annotation::Annotation(const srd_proto_data *const pdata) :
+	_start_sample(pdata->start_sample),
+	_end_sample(pdata->end_sample)
+{
+	assert(pdata);
+	const srd_proto_data_annotation *const pda =
+		(const srd_proto_data_annotation*)pdata->data;
+	assert(pda);
+
+	_format = pda->ann_format;
+
+	const char *const *annotations = (char**)pda->ann_text;
+	while(*annotations) {
+		_annotations.push_back(QString::fromUtf8(*annotations));
+		annotations++;
+	}
+}
+
+uint64_t Annotation::start_sample() const
+{
+	return _start_sample;
+}
+
+uint64_t Annotation::end_sample() const
+{
+	return _end_sample;
+}
+
+int Annotation::format() const
+{
+	return _format;
+}
+
+const std::vector<QString>& Annotation::annotations() const
+{
+	return _annotations;
+}
+
+} // namespace decode
+} // namespace data
+} // namespace pv
diff --git a/pv/data/decode/annotation.h b/pv/data/decode/annotation.h
new file mode 100644
index 0000000..0d7fd5d
--- /dev/null
+++ b/pv/data/decode/annotation.h
@@ -0,0 +1,55 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_VIEW_DECODE_ANNOTATION_H
+#define PULSEVIEW_PV_VIEW_DECODE_ANNOTATION_H
+
+#include <stdint.h>
+
+#include <QString>
+
+struct srd_proto_data;
+
+namespace pv {
+namespace data {
+namespace decode {
+
+class Annotation
+{
+public:
+	Annotation(const srd_proto_data *const pdata);
+
+	uint64_t start_sample() const;
+	uint64_t end_sample() const;
+	int format() const;
+	const std::vector<QString>& annotations() const;
+
+private:
+	uint64_t _start_sample;
+	uint64_t _end_sample;
+	int _format;
+	std::vector<QString> _annotations; 
+};
+
+} // namespace decode
+} // namespace data
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_DECODE_ANNOTATION_H
diff --git a/pv/data/decode/decoder.cpp b/pv/data/decode/decoder.cpp
new file mode 100644
index 0000000..44e474b
--- /dev/null
+++ b/pv/data/decode/decoder.cpp
@@ -0,0 +1,159 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <libsigrok/libsigrok.h>
+#include <libsigrokdecode/libsigrokdecode.h>
+
+#include "decoder.h"
+
+#include <pv/view/logicsignal.h>
+
+using boost::shared_ptr;
+using std::set;
+using std::map;
+using std::string;
+
+namespace pv {
+namespace data {
+namespace decode {
+
+Decoder::Decoder(const srd_decoder *const dec) :
+	_decoder(dec),
+	_shown(true)
+{
+}
+
+Decoder::~Decoder()
+{
+	for (map<string, GVariant*>::const_iterator i = _options.begin();
+		i != _options.end(); i++)
+		g_variant_unref((*i).second);
+}
+
+const srd_decoder* Decoder::decoder() const
+{
+	return _decoder;
+}
+
+bool Decoder::shown() const
+{
+	return _shown;
+}
+
+void Decoder::show(bool show)
+{
+	_shown = show;
+}
+
+const map<const srd_channel*, shared_ptr<view::LogicSignal> >&
+Decoder::channels() const
+{
+	return _probes;
+}
+
+void Decoder::set_probes(std::map<const srd_channel*,
+	boost::shared_ptr<view::LogicSignal> > probes)
+{
+	_probes = probes;
+}
+
+const std::map<std::string, GVariant*>& Decoder::options() const
+{
+	return _options;
+}
+
+void Decoder::set_option(const char *id, GVariant *value)
+{
+	assert(value);
+	g_variant_ref(value);
+	_options[id] = value;
+}
+
+bool Decoder::have_required_probes() const
+{
+	for (GSList *l = _decoder->channels; l; l = l->next) {
+		const srd_channel *const pdch = (const srd_channel*)l->data;
+		assert(pdch);
+		if (_probes.find(pdch) == _probes.end())
+			return false;
+	}
+
+	return true;
+}
+
+set< shared_ptr<pv::data::Logic> > Decoder::get_data()
+{
+	set< shared_ptr<pv::data::Logic> > data;
+	for(map<const srd_channel*, shared_ptr<view::LogicSignal> >::
+		const_iterator i = _probes.begin();
+		i != _probes.end(); i++)
+	{
+		shared_ptr<view::LogicSignal> signal((*i).second);
+		assert(signal);
+		data.insert(signal->logic_data());
+	}
+
+	return data;
+}
+
+srd_decoder_inst* Decoder::create_decoder_inst(srd_session *session, int unit_size) const
+{
+	GHashTable *const opt_hash = g_hash_table_new_full(g_str_hash,
+		g_str_equal, g_free, (GDestroyNotify)g_variant_unref);
+
+	for (map<string, GVariant*>::const_iterator i = _options.begin();
+		i != _options.end(); i++)
+	{
+		GVariant *const value = (*i).second;
+		g_variant_ref(value);
+		g_hash_table_replace(opt_hash, (void*)g_strdup(
+			(*i).first.c_str()), value);
+	}
+
+	srd_decoder_inst *const decoder_inst = srd_inst_new(
+		session, _decoder->id, opt_hash);
+	g_hash_table_destroy(opt_hash);
+
+	if(!decoder_inst)
+		return NULL;
+
+	// Setup the probes
+	GHashTable *const probes = g_hash_table_new_full(g_str_hash,
+		g_str_equal, g_free, (GDestroyNotify)g_variant_unref);
+
+	for(map<const srd_channel*, shared_ptr<view::LogicSignal> >::
+		const_iterator i = _probes.begin();
+		i != _probes.end(); i++)
+	{
+		shared_ptr<view::LogicSignal> signal((*i).second);
+		GVariant *const gvar = g_variant_new_int32(
+			signal->probe()->index);
+		g_variant_ref_sink(gvar);
+		g_hash_table_insert(probes, (*i).first->id, gvar);
+	}
+
+	srd_inst_channel_set_all(decoder_inst, probes, unit_size);
+
+	return decoder_inst;
+}
+
+} // decode
+} // data
+} // pv
diff --git a/pv/data/decode/decoder.h b/pv/data/decode/decoder.h
new file mode 100644
index 0000000..dffefab
--- /dev/null
+++ b/pv/data/decode/decoder.h
@@ -0,0 +1,90 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_DATA_DECODE_DECODER_H
+#define PULSEVIEW_PV_DATA_DECODE_DECODER_H
+
+#include <map>
+#include <set>
+
+#include <boost/shared_ptr.hpp>
+
+#include <glib.h>
+
+struct srd_decoder;
+struct srd_decoder_inst;
+struct srd_channel;
+struct srd_session;
+
+namespace pv {
+
+namespace view {
+class LogicSignal;
+}
+
+namespace data {
+
+class Logic;
+
+namespace decode {
+
+class Decoder
+{
+public:
+	Decoder(const srd_decoder *const decoder);
+
+	virtual ~Decoder();
+
+	const srd_decoder* decoder() const;
+
+	bool shown() const;
+	void show(bool show = true);
+
+	const std::map<const srd_channel*,
+		boost::shared_ptr<view::LogicSignal> >& channels() const;
+	void set_probes(std::map<const srd_channel*,
+		boost::shared_ptr<view::LogicSignal> > probes);
+
+	const std::map<std::string, GVariant*>& options() const;
+
+	void set_option(const char *id, GVariant *value);
+
+	bool have_required_probes() const;
+
+	srd_decoder_inst* create_decoder_inst(
+		srd_session *session, int unit_size) const;
+
+	std::set< boost::shared_ptr<pv::data::Logic> > get_data();	
+
+private:
+	const srd_decoder *const _decoder;
+
+	bool _shown;
+
+	std::map<const srd_channel*, boost::shared_ptr<pv::view::LogicSignal> >
+		_probes;
+	std::map<std::string, GVariant*> _options;
+};
+
+} // namespace decode
+} // namespace data
+} // namespace pv
+
+#endif // PULSEVIEW_PV_DATA_DECODE_DECODER_H
diff --git a/pv/data/decode/row.cpp b/pv/data/decode/row.cpp
new file mode 100644
index 0000000..2aabf0f
--- /dev/null
+++ b/pv/data/decode/row.cpp
@@ -0,0 +1,72 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2014 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "row.h"
+
+#include <libsigrokdecode/libsigrokdecode.h>
+
+namespace pv {
+namespace data {
+namespace decode {
+
+Row::Row() :
+	_decoder(NULL),
+	_row(NULL)
+{
+}
+
+Row::Row(const srd_decoder *decoder, const srd_decoder_annotation_row *row) :
+	_decoder(decoder),
+	_row(row)
+{
+}
+
+const srd_decoder* Row::decoder() const
+{
+	return _decoder;
+}
+
+const srd_decoder_annotation_row* Row::row() const
+{
+	return _row;
+}
+
+const QString Row::title() const
+{
+	if (_decoder && _decoder->name && _row && _row->desc)
+		return QString("%1: %2")
+			.arg(QString::fromUtf8(_decoder->name))
+			.arg(QString::fromUtf8(_row->desc));
+	if (_decoder && _decoder->name)
+		return QString::fromUtf8(_decoder->name);
+	if (_row && _row->desc)
+		return QString::fromUtf8(_row->desc);
+	return QString();
+}
+
+bool Row::operator<(const Row &other) const
+{
+	return (_decoder < other._decoder) ||
+		(_decoder == other._decoder && _row < other._row);
+}
+
+} // decode
+} // data
+} // pv
diff --git a/pv/data/decode/row.h b/pv/data/decode/row.h
new file mode 100644
index 0000000..6c05ac4
--- /dev/null
+++ b/pv/data/decode/row.h
@@ -0,0 +1,59 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2014 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_DATA_DECODE_ROW_H
+#define PULSEVIEW_PV_DATA_DECODE_ROW_H
+
+#include <vector>
+
+#include "annotation.h"
+
+struct srd_decoder;
+struct srd_decoder_annotation_row;
+
+namespace pv {
+namespace data {
+namespace decode {
+
+class Row
+{
+public:
+	Row();
+
+	Row(const srd_decoder *decoder,
+		const srd_decoder_annotation_row *row = NULL);
+
+	const srd_decoder* decoder() const;
+	const srd_decoder_annotation_row* row() const;
+
+	const QString title() const;
+
+	bool operator<(const Row &other) const;
+
+private:
+	const srd_decoder *_decoder;
+	const srd_decoder_annotation_row *_row;
+};
+
+} // decode
+} // data
+} // pv
+
+#endif // PULSEVIEW_PV_DATA_DECODE_ROW_H
diff --git a/pv/data/decode/rowdata.cpp b/pv/data/decode/rowdata.cpp
new file mode 100644
index 0000000..87da27c
--- /dev/null
+++ b/pv/data/decode/rowdata.cpp
@@ -0,0 +1,58 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2014 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "rowdata.h"
+
+using std::vector;
+
+namespace pv {
+namespace data {
+namespace decode {
+
+RowData::RowData()
+{
+}
+
+uint64_t RowData::get_max_sample() const
+{
+	if (_annotations.empty())
+		return 0;
+	return _annotations.back().end_sample();
+}
+
+void RowData::get_annotation_subset(
+	vector<pv::data::decode::Annotation> &dest,
+	uint64_t start_sample, uint64_t end_sample) const
+{
+	for (vector<Annotation>::const_iterator i = _annotations.begin();
+		i != _annotations.end(); i++)
+		if ((*i).end_sample() > start_sample &&
+			(*i).start_sample() <= end_sample)
+			dest.push_back(*i);
+}
+
+void RowData::push_annotation(const Annotation &a)
+{
+	_annotations.push_back(a);
+}
+
+} // decode
+} // data
+} // pv
diff --git a/pv/data/decode/rowdata.h b/pv/data/decode/rowdata.h
new file mode 100644
index 0000000..01c65b6
--- /dev/null
+++ b/pv/data/decode/rowdata.h
@@ -0,0 +1,57 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2014 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_DATA_DECODE_ROWDATA_H
+#define PULSEVIEW_PV_DATA_DECODE_ROWDATA_H
+
+#include <vector>
+
+#include "annotation.h"
+
+namespace pv {
+namespace data {
+namespace decode {
+
+class RowData
+{
+public:
+	RowData();
+
+public:
+	uint64_t get_max_sample() const;
+
+	/**
+	 * Extracts sorted annotations between two period into a vector.
+	 */
+	void get_annotation_subset(
+		std::vector<pv::data::decode::Annotation> &dest,
+		uint64_t start_sample, uint64_t end_sample) const;
+
+	void push_annotation(const Annotation &a);
+
+private:
+	std::vector<Annotation> _annotations;
+};
+
+}
+} // data
+} // pv
+
+#endif // PULSEVIEW_PV_DATA_DECODE_ROWDATA_H
diff --git a/pv/data/decoderstack.cpp b/pv/data/decoderstack.cpp
new file mode 100644
index 0000000..c3bf26d
--- /dev/null
+++ b/pv/data/decoderstack.cpp
@@ -0,0 +1,450 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <libsigrokdecode/libsigrokdecode.h>
+
+#include <boost/foreach.hpp>
+#include <boost/thread/thread.hpp>
+
+#include <stdexcept>
+
+#include <QDebug>
+
+#include "decoderstack.h"
+
+#include <pv/data/logic.h>
+#include <pv/data/logicsnapshot.h>
+#include <pv/data/decode/decoder.h>
+#include <pv/data/decode/annotation.h>
+#include <pv/sigsession.h>
+#include <pv/view/logicsignal.h>
+
+using boost::lock_guard;
+using boost::mutex;
+using boost::optional;
+using boost::shared_ptr;
+using boost::unique_lock;
+using std::deque;
+using std::make_pair;
+using std::max;
+using std::min;
+using std::list;
+using std::map;
+using std::pair;
+using std::vector;
+
+using namespace pv::data::decode;
+
+namespace pv {
+namespace data {
+
+const double DecoderStack::DecodeMargin = 1.0;
+const double DecoderStack::DecodeThreshold = 0.2;
+const int64_t DecoderStack::DecodeChunkLength = 4096;
+const unsigned int DecoderStack::DecodeNotifyPeriod = 65536;
+
+mutex DecoderStack::_global_decode_mutex;
+
+DecoderStack::DecoderStack(pv::SigSession &session,
+	const srd_decoder *const dec) :
+	_session(session),
+	_sample_count(0),
+	_frame_complete(false),
+	_samples_decoded(0)
+{
+	connect(&_session, SIGNAL(frame_began()),
+		this, SLOT(on_new_frame()));
+	connect(&_session, SIGNAL(data_received()),
+		this, SLOT(on_data_received()));
+	connect(&_session, SIGNAL(frame_ended()),
+		this, SLOT(on_frame_ended()));
+
+	_stack.push_back(shared_ptr<decode::Decoder>(
+		new decode::Decoder(dec)));
+}
+
+DecoderStack::~DecoderStack()
+{
+	if (_decode_thread.joinable()) {
+		_decode_thread.interrupt();
+		_decode_thread.join();
+	}
+}
+
+const std::list< boost::shared_ptr<decode::Decoder> >&
+DecoderStack::stack() const
+{
+	return _stack;
+}
+
+void DecoderStack::push(boost::shared_ptr<decode::Decoder> decoder)
+{
+	assert(decoder);
+	_stack.push_back(decoder);
+}
+
+void DecoderStack::remove(int index)
+{
+	assert(index >= 0);
+	assert(index < (int)_stack.size());
+
+	// Find the decoder in the stack
+	list< shared_ptr<Decoder> >::iterator iter = _stack.begin();
+	for(int i = 0; i < index; i++, iter++)
+		assert(iter != _stack.end());
+
+	// Delete the element
+	_stack.erase(iter);
+}
+
+int64_t DecoderStack::samples_decoded() const
+{
+	lock_guard<mutex> decode_lock(_output_mutex);
+	return _samples_decoded;
+}
+
+std::vector<Row> DecoderStack::get_visible_rows() const
+{
+	lock_guard<mutex> lock(_output_mutex);
+
+	vector<Row> rows;
+
+	BOOST_FOREACH (const shared_ptr<decode::Decoder> &dec, _stack)
+	{
+		assert(dec);
+		if (!dec->shown())
+			continue;
+
+		const srd_decoder *const decc = dec->decoder();
+		assert(dec->decoder());
+
+		// Add a row for the decoder if it doesn't have a row list
+		if (!decc->annotation_rows)
+			rows.push_back(Row(decc));
+
+		// Add the decoder rows
+		for (const GSList *l = decc->annotation_rows; l; l = l->next)
+		{
+			const srd_decoder_annotation_row *const ann_row =
+				(srd_decoder_annotation_row *)l->data;
+			assert(ann_row);
+			rows.push_back(Row(decc, ann_row));
+		}
+	}
+
+	return rows;
+}
+
+void DecoderStack::get_annotation_subset(
+	std::vector<pv::data::decode::Annotation> &dest,
+	const Row &row, uint64_t start_sample,
+	uint64_t end_sample) const
+{
+	lock_guard<mutex> lock(_output_mutex);
+
+	std::map<const Row, decode::RowData>::const_iterator iter =
+		_rows.find(row);
+	if (iter != _rows.end())
+		(*iter).second.get_annotation_subset(dest,
+			start_sample, end_sample);
+}
+
+QString DecoderStack::error_message()
+{
+	lock_guard<mutex> lock(_output_mutex);
+	return _error_message;
+}
+
+void DecoderStack::clear()
+{
+	_sample_count = 0;
+	_frame_complete = false;
+	_samples_decoded = 0;
+	_error_message = QString();
+	_rows.clear();
+	_class_rows.clear();
+}
+
+void DecoderStack::begin_decode()
+{
+	shared_ptr<pv::view::LogicSignal> logic_signal;
+	shared_ptr<pv::data::Logic> data;
+
+	if (_decode_thread.joinable()) {
+		_decode_thread.interrupt();
+		_decode_thread.join();
+	}
+
+	clear();
+
+	// Check that all decoders have the required channels
+	BOOST_FOREACH(const shared_ptr<decode::Decoder> &dec, _stack)
+		if (!dec->have_required_probes()) {
+			_error_message = tr("One or more required channels "
+				"have not been specified");
+			return;
+		}
+
+	// Add classes
+	BOOST_FOREACH (const shared_ptr<decode::Decoder> &dec, _stack)
+	{
+		assert(dec);
+		const srd_decoder *const decc = dec->decoder();
+		assert(dec->decoder());
+
+		// Add a row for the decoder if it doesn't have a row list
+		if (!decc->annotation_rows)
+			_rows[Row(decc)] = decode::RowData();
+
+		// Add the decoder rows
+		for (const GSList *l = decc->annotation_rows; l; l = l->next)
+		{
+			const srd_decoder_annotation_row *const ann_row =
+				(srd_decoder_annotation_row *)l->data;
+			assert(ann_row);
+
+			const Row row(decc, ann_row);
+
+			// Add a new empty row data object
+			_rows[row] = decode::RowData();
+
+			// Map out all the classes
+			for (const GSList *ll = ann_row->ann_classes;
+				ll; ll = ll->next)
+				_class_rows[make_pair(decc,
+					GPOINTER_TO_INT(ll->data))] = row;
+		}
+	}
+
+	// We get the logic data of the first channel in the list.
+	// This works because we are currently assuming all
+	// LogicSignals have the same data/snapshot
+	BOOST_FOREACH (const shared_ptr<decode::Decoder> &dec, _stack)
+		if (dec && !dec->channels().empty() &&
+			((logic_signal = (*dec->channels().begin()).second)) &&
+			((data = logic_signal->logic_data())))
+			break;
+
+	if (!data)
+		return;
+
+	// Check we have a snapshot of data
+	const deque< shared_ptr<pv::data::LogicSnapshot> > &snapshots =
+		data->get_snapshots();
+	if (snapshots.empty())
+		return;
+	_snapshot = snapshots.front();
+
+	// Get the samplerate and start time
+	_start_time = data->get_start_time();
+	_samplerate = data->samplerate();
+	if (_samplerate == 0.0)
+		_samplerate = 1.0;
+
+	_decode_thread = boost::thread(&DecoderStack::decode_proc, this);
+}
+
+uint64_t DecoderStack::get_max_sample_count() const
+{
+	uint64_t max_sample_count = 0;
+
+	for (map<const Row, RowData>::const_iterator i = _rows.begin();
+		i != _rows.end(); i++)
+		max_sample_count = max(max_sample_count,
+			(*i).second.get_max_sample());
+
+	return max_sample_count;
+}
+
+optional<int64_t> DecoderStack::wait_for_data() const
+{
+	unique_lock<mutex> input_lock(_input_mutex);
+	while(!boost::this_thread::interruption_requested() &&
+		!_frame_complete && _samples_decoded >= _sample_count)
+		_input_cond.wait(input_lock);
+	return boost::make_optional(
+		!boost::this_thread::interruption_requested() &&
+		(_samples_decoded < _sample_count || !_frame_complete),
+		_sample_count);
+}
+
+void DecoderStack::decode_data(
+	const int64_t sample_count, const unsigned int unit_size,
+	srd_session *const session)
+{
+	uint8_t chunk[DecodeChunkLength];
+
+	const unsigned int chunk_sample_count =
+		DecodeChunkLength / _snapshot->unit_size();
+
+	for (int64_t i = 0;
+		!boost::this_thread::interruption_requested() &&
+			i < sample_count;
+		i += chunk_sample_count)
+	{
+		lock_guard<mutex> decode_lock(_global_decode_mutex);
+
+		const int64_t chunk_end = min(
+			i + chunk_sample_count, sample_count);
+		_snapshot->get_samples(chunk, i, chunk_end);
+
+		if (srd_session_send(session, i, i + sample_count, chunk,
+				(chunk_end - i) * unit_size) != SRD_OK) {
+			_error_message = tr("Decoder reported an error");
+			break;
+		}
+
+		{
+			lock_guard<mutex> lock(_output_mutex);
+			_samples_decoded = chunk_end;
+		}
+
+		if (i % DecodeNotifyPeriod == 0)
+			new_decode_data();
+	}
+
+	new_decode_data();
+}
+
+void DecoderStack::decode_proc()
+{
+	optional<int64_t> sample_count;
+	srd_session *session;
+	srd_decoder_inst *prev_di = NULL;
+
+	assert(_snapshot);
+
+	// Create the session
+	srd_session_new(&session);
+	assert(session);
+
+	// Create the decoders
+	const unsigned int unit_size = _snapshot->unit_size();
+
+	BOOST_FOREACH(const shared_ptr<decode::Decoder> &dec, _stack)
+	{
+		srd_decoder_inst *const di = dec->create_decoder_inst(session, unit_size);
+
+		if (!di)
+		{
+			_error_message = tr("Failed to create decoder instance");
+			srd_session_destroy(session);
+			return;
+		}
+
+		if (prev_di)
+			srd_inst_stack (session, prev_di, di);
+
+		prev_di = di;
+	}
+
+	// Get the intial sample count
+	{
+		unique_lock<mutex> input_lock(_input_mutex);
+		sample_count = _sample_count = _snapshot->get_sample_count();
+	}
+
+	// Start the session
+	srd_session_metadata_set(session, SRD_CONF_SAMPLERATE,
+		g_variant_new_uint64((uint64_t)_samplerate));
+
+	srd_pd_output_callback_add(session, SRD_OUTPUT_ANN,
+		DecoderStack::annotation_callback, this);
+
+	srd_session_start(session);
+
+	do {
+		decode_data(*sample_count, unit_size, session);
+	} while(_error_message.isEmpty() && (sample_count = wait_for_data()));
+
+	// Destroy the session
+	srd_session_destroy(session);
+}
+
+void DecoderStack::annotation_callback(srd_proto_data *pdata, void *decoder)
+{
+	assert(pdata);
+	assert(decoder);
+
+	DecoderStack *const d = (DecoderStack*)decoder;
+	assert(d);
+
+	lock_guard<mutex> lock(d->_output_mutex);
+
+	const Annotation a(pdata);
+
+	// Find the row
+	assert(pdata->pdo);
+	assert(pdata->pdo->di);
+	const srd_decoder *const decc = pdata->pdo->di->decoder;
+	assert(decc);
+
+	map<const Row, decode::RowData>::iterator row_iter = d->_rows.end();
+	
+	// Try looking up the sub-row of this class
+	const map<pair<const srd_decoder*, int>, Row>::const_iterator r =
+		d->_class_rows.find(make_pair(decc, a.format()));
+	if (r != d->_class_rows.end())
+		row_iter = d->_rows.find((*r).second);
+	else
+	{
+		// Failing that, use the decoder as a key
+		row_iter = d->_rows.find(Row(decc));	
+	}
+
+	assert(row_iter != d->_rows.end());
+	if (row_iter == d->_rows.end()) {
+		qDebug() << "Unexpected annotation: decoder = " << decc <<
+			", format = " << a.format();
+		assert(0);
+		return;
+	}
+
+	// Add the annotation
+	(*row_iter).second.push_annotation(a);
+}
+
+void DecoderStack::on_new_frame()
+{
+	begin_decode();
+}
+
+void DecoderStack::on_data_received()
+{
+	{
+		unique_lock<mutex> lock(_input_mutex);
+		if (_snapshot)
+			_sample_count = _snapshot->get_sample_count();
+	}
+	_input_cond.notify_one();
+}
+
+void DecoderStack::on_frame_ended()
+{
+	{
+		unique_lock<mutex> lock(_input_mutex);
+		if (_snapshot)
+			_frame_complete = true;
+	}
+	_input_cond.notify_one();
+}
+
+} // namespace data
+} // namespace pv
diff --git a/pv/data/decoderstack.h b/pv/data/decoderstack.h
new file mode 100644
index 0000000..2eeaf1c
--- /dev/null
+++ b/pv/data/decoderstack.h
@@ -0,0 +1,165 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_DATA_DECODERSTACK_H
+#define PULSEVIEW_PV_DATA_DECODERSTACK_H
+
+#include "signaldata.h"
+
+#include <list>
+
+#include <boost/optional.hpp>
+#include <boost/shared_ptr.hpp>
+#include <boost/thread.hpp>
+
+#include <QObject>
+#include <QString>
+
+#include <pv/data/decode/row.h>
+#include <pv/data/decode/rowdata.h>
+
+struct srd_decoder;
+struct srd_decoder_annotation_row;
+struct srd_channel;
+struct srd_proto_data;
+struct srd_session;
+
+namespace DecoderStackTest {
+class TwoDecoderStack;
+}
+
+namespace pv {
+
+class SigSession;
+
+namespace view {
+class LogicSignal;
+}
+
+namespace data {
+
+class LogicSnapshot;
+
+namespace decode {
+class Annotation;
+class Decoder;
+}
+
+class Logic;
+
+class DecoderStack : public QObject, public SignalData
+{
+	Q_OBJECT
+
+private:
+	static const double DecodeMargin;
+	static const double DecodeThreshold;
+	static const int64_t DecodeChunkLength;
+	static const unsigned int DecodeNotifyPeriod;
+
+public:
+	DecoderStack(pv::SigSession &_session,
+		const srd_decoder *const decoder);
+
+	virtual ~DecoderStack();
+
+	const std::list< boost::shared_ptr<decode::Decoder> >& stack() const;
+	void push(boost::shared_ptr<decode::Decoder> decoder);
+	void remove(int index);
+
+	int64_t samples_decoded() const;
+
+	std::vector<decode::Row> get_visible_rows() const;
+
+	/**
+	 * Extracts sorted annotations between two period into a vector.
+	 */
+	void get_annotation_subset(
+		std::vector<pv::data::decode::Annotation> &dest,
+		const decode::Row &row, uint64_t start_sample,
+		uint64_t end_sample) const;
+
+	QString error_message();
+
+	void clear();
+
+	uint64_t get_max_sample_count() const;
+
+	void begin_decode();
+
+private:
+	boost::optional<int64_t> wait_for_data() const;
+
+	void decode_data(const int64_t sample_count,
+		const unsigned int unit_size, srd_session *const session);
+
+	void decode_proc();
+
+	static void annotation_callback(srd_proto_data *pdata,
+		void *decoder);
+
+private slots:
+	void on_new_frame();
+
+	void on_data_received();
+
+	void on_frame_ended();
+
+signals:
+	void new_decode_data();
+
+private:
+	pv::SigSession &_session;
+
+	/**
+	 * This mutex prevents more than one decode operation occuring
+	 * concurrently.
+	 * @todo A proper solution should be implemented to allow multiple
+	 * decode operations.
+	 */
+	static boost::mutex _global_decode_mutex;
+
+	std::list< boost::shared_ptr<decode::Decoder> > _stack;
+
+	boost::shared_ptr<pv::data::LogicSnapshot> _snapshot;
+
+	mutable boost::mutex _input_mutex;
+	mutable boost::condition_variable _input_cond;
+	int64_t _sample_count;
+	bool _frame_complete;
+
+	mutable boost::mutex _output_mutex;
+	int64_t	_samples_decoded;
+
+	std::map<const decode::Row, decode::RowData> _rows;
+
+	std::map<std::pair<const srd_decoder*, int>, decode::Row> _class_rows;
+
+	QString _error_message;
+
+	boost::thread _decode_thread;
+
+	friend class DecoderStackTest::TwoDecoderStack;
+};
+
+} // namespace data
+} // namespace pv
+
+#endif // PULSEVIEW_PV_DATA_DECODERSTACK_H
diff --git a/pv/data/logic.cpp b/pv/data/logic.cpp
new file mode 100644
index 0000000..167c79b
--- /dev/null
+++ b/pv/data/logic.cpp
@@ -0,0 +1,72 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <boost/foreach.hpp>
+
+#include "logic.h"
+#include "logicsnapshot.h"
+
+using boost::shared_ptr;
+using std::deque;
+using std::max;
+
+namespace pv {
+namespace data {
+
+Logic::Logic(unsigned int num_probes) :
+	SignalData(),
+	_num_probes(num_probes)
+{
+	assert(_num_probes > 0);
+}
+
+int Logic::get_num_probes() const
+{
+	return _num_probes;
+}
+
+void Logic::push_snapshot(
+	shared_ptr<LogicSnapshot> &snapshot)
+{
+	_snapshots.push_front(snapshot);
+}
+
+deque< shared_ptr<LogicSnapshot> >& Logic::get_snapshots()
+{
+	return _snapshots;
+}
+
+void Logic::clear()
+{
+	_snapshots.clear();
+}
+
+uint64_t Logic::get_max_sample_count() const
+{
+	uint64_t l = 0;
+	BOOST_FOREACH(boost::shared_ptr<LogicSnapshot> s, _snapshots) {
+		assert(s);
+		l = max(l, s->get_sample_count());
+	}
+	return l;
+}
+
+} // namespace data
+} // namespace pv
diff --git a/pv/data/logic.h b/pv/data/logic.h
new file mode 100644
index 0000000..3d83394
--- /dev/null
+++ b/pv/data/logic.h
@@ -0,0 +1,59 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_DATA_LOGIC_H
+#define PULSEVIEW_PV_DATA_LOGIC_H
+
+#include "signaldata.h"
+
+#include <boost/shared_ptr.hpp>
+#include <deque>
+
+namespace pv {
+namespace data {
+
+class LogicSnapshot;
+
+class Logic : public SignalData
+{
+public:
+	Logic(unsigned int num_probes);
+
+	int get_num_probes() const;
+
+	void push_snapshot(
+		boost::shared_ptr<LogicSnapshot> &snapshot);
+
+	std::deque< boost::shared_ptr<LogicSnapshot> >&
+		get_snapshots();
+
+	void clear();
+
+	uint64_t get_max_sample_count() const;
+
+private:
+	const unsigned int _num_probes;
+	std::deque< boost::shared_ptr<LogicSnapshot> > _snapshots;
+};
+
+} // namespace data
+} // namespace pv
+
+#endif // PULSEVIEW_PV_DATA_LOGIC_H
diff --git a/pv/data/logicsnapshot.cpp b/pv/data/logicsnapshot.cpp
new file mode 100644
index 0000000..797a00b
--- /dev/null
+++ b/pv/data/logicsnapshot.cpp
@@ -0,0 +1,468 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <extdef.h>
+
+#include <assert.h>
+#include <string.h>
+#include <stdlib.h>
+#include <math.h>
+
+#include <boost/foreach.hpp>
+
+#include "config.h"
+#include "logicsnapshot.h"
+
+using boost::lock_guard;
+using boost::recursive_mutex;
+using std::max;
+using std::min;
+using std::pair;
+
+namespace pv {
+namespace data {
+
+const int LogicSnapshot::MipMapScalePower = 4;
+const int LogicSnapshot::MipMapScaleFactor = 1 << MipMapScalePower;
+const float LogicSnapshot::LogMipMapScaleFactor = logf(MipMapScaleFactor);
+const uint64_t LogicSnapshot::MipMapDataUnit = 64*1024;	// bytes
+
+LogicSnapshot::LogicSnapshot(const sr_datafeed_logic &logic,
+                             const uint64_t expected_num_samples) :
+	Snapshot(logic.unitsize),
+	_last_append_sample(0)
+{
+	set_capacity(expected_num_samples);
+
+	lock_guard<recursive_mutex> lock(_mutex);
+	memset(_mip_map, 0, sizeof(_mip_map));
+	append_payload(logic);
+}
+
+LogicSnapshot::~LogicSnapshot()
+{
+	lock_guard<recursive_mutex> lock(_mutex);
+	BOOST_FOREACH(MipMapLevel &l, _mip_map)
+		free(l.data);
+}
+
+uint64_t LogicSnapshot::unpack_sample(const uint8_t *ptr) const
+{
+#ifdef HAVE_UNALIGNED_LITTLE_ENDIAN_ACCESS
+	return *(uint64_t*)ptr;
+#else
+	uint64_t value = 0;
+	switch(_unit_size) {
+	default:
+		value |= ((uint64_t)ptr[7]) << 56;
+		/* FALLTHRU */
+	case 7:
+		value |= ((uint64_t)ptr[6]) << 48;
+		/* FALLTHRU */
+	case 6:
+		value |= ((uint64_t)ptr[5]) << 40;
+		/* FALLTHRU */
+	case 5:
+		value |= ((uint64_t)ptr[4]) << 32;
+		/* FALLTHRU */
+	case 4:
+		value |= ((uint32_t)ptr[3]) << 24;
+		/* FALLTHRU */
+	case 3:
+		value |= ((uint32_t)ptr[2]) << 16;
+		/* FALLTHRU */
+	case 2:
+		value |= ptr[1] << 8;
+		/* FALLTHRU */
+	case 1:
+		value |= ptr[0];
+		/* FALLTHRU */
+	case 0:
+		break;
+	}
+	return value;
+#endif
+}
+
+void LogicSnapshot::pack_sample(uint8_t *ptr, uint64_t value)
+{
+#ifdef HAVE_UNALIGNED_LITTLE_ENDIAN_ACCESS
+	*(uint64_t*)ptr = value;
+#else
+	switch(_unit_size) {
+	default:
+		ptr[7] = value >> 56;
+		/* FALLTHRU */
+	case 7:
+		ptr[6] = value >> 48;
+		/* FALLTHRU */
+	case 6:
+		ptr[5] = value >> 40;
+		/* FALLTHRU */
+	case 5:
+		ptr[4] = value >> 32;
+		/* FALLTHRU */
+	case 4:
+		ptr[3] = value >> 24;
+		/* FALLTHRU */
+	case 3:
+		ptr[2] = value >> 16;
+		/* FALLTHRU */
+	case 2:
+		ptr[1] = value >> 8;
+		/* FALLTHRU */
+	case 1:
+		ptr[0] = value;
+		/* FALLTHRU */
+	case 0:
+		break;
+	}
+#endif
+}
+
+void LogicSnapshot::append_payload(
+	const sr_datafeed_logic &logic)
+{
+	assert(_unit_size == logic.unitsize);
+	assert((logic.length % _unit_size) == 0);
+
+	lock_guard<recursive_mutex> lock(_mutex);
+
+	append_data(logic.data, logic.length / _unit_size);
+
+	// Generate the first mip-map from the data
+	append_payload_to_mipmap();
+}
+
+void LogicSnapshot::get_samples(uint8_t *const data,
+	int64_t start_sample, int64_t end_sample) const
+{
+	assert(data);
+	assert(start_sample >= 0);
+	assert(start_sample <= (int64_t)_sample_count);
+	assert(end_sample >= 0);
+	assert(end_sample <= (int64_t)_sample_count);
+	assert(start_sample <= end_sample);
+
+	lock_guard<recursive_mutex> lock(_mutex);
+
+	const size_t size = (end_sample - start_sample) * _unit_size;
+	memcpy(data, (const uint8_t*)_data + start_sample * _unit_size, size);
+}
+
+void LogicSnapshot::reallocate_mipmap_level(MipMapLevel &m)
+{
+	const uint64_t new_data_length = ((m.length + MipMapDataUnit - 1) /
+		MipMapDataUnit) * MipMapDataUnit;
+	if (new_data_length > m.data_length)
+	{
+		m.data_length = new_data_length;
+
+		// Padding is added to allow for the uint64_t write word
+		m.data = realloc(m.data, new_data_length * _unit_size +
+			sizeof(uint64_t));
+	}
+}
+
+void LogicSnapshot::append_payload_to_mipmap()
+{
+	MipMapLevel &m0 = _mip_map[0];
+	uint64_t prev_length;
+	const uint8_t *src_ptr;
+	uint8_t *dest_ptr;
+	uint64_t accumulator;
+	unsigned int diff_counter;
+
+	// Expand the data buffer to fit the new samples
+	prev_length = m0.length;
+	m0.length = _sample_count / MipMapScaleFactor;
+
+	// Break off if there are no new samples to compute
+	if (m0.length == prev_length)
+		return;
+
+	reallocate_mipmap_level(m0);
+
+	dest_ptr = (uint8_t*)m0.data + prev_length * _unit_size;
+
+	// Iterate through the samples to populate the first level mipmap
+	const uint8_t *const end_src_ptr = (uint8_t*)_data +
+		m0.length * _unit_size * MipMapScaleFactor;
+	for (src_ptr = (uint8_t*)_data +
+		prev_length * _unit_size * MipMapScaleFactor;
+		src_ptr < end_src_ptr;)
+	{
+		// Accumulate transitions which have occurred in this sample
+		accumulator = 0;
+		diff_counter = MipMapScaleFactor;
+		while (diff_counter-- > 0)
+		{
+			const uint64_t sample = unpack_sample(src_ptr);
+			accumulator |= _last_append_sample ^ sample;
+			_last_append_sample = sample;
+			src_ptr += _unit_size;
+		}
+
+		pack_sample(dest_ptr, accumulator);
+		dest_ptr += _unit_size;
+	}
+
+	// Compute higher level mipmaps
+	for (unsigned int level = 1; level < ScaleStepCount; level++)
+	{
+		MipMapLevel &m = _mip_map[level];
+		const MipMapLevel &ml = _mip_map[level-1];
+
+		// Expand the data buffer to fit the new samples
+		prev_length = m.length;
+		m.length = ml.length / MipMapScaleFactor;
+
+		// Break off if there are no more samples to computed
+		if (m.length == prev_length)
+			break;
+
+		reallocate_mipmap_level(m);
+
+		// Subsample the level lower level
+		src_ptr = (uint8_t*)ml.data +
+			_unit_size * prev_length * MipMapScaleFactor;
+		const uint8_t *const end_dest_ptr =
+			(uint8_t*)m.data + _unit_size * m.length;
+		for (dest_ptr = (uint8_t*)m.data +
+			_unit_size * prev_length;
+			dest_ptr < end_dest_ptr;
+			dest_ptr += _unit_size)
+		{
+			accumulator = 0;
+			diff_counter = MipMapScaleFactor;
+			while (diff_counter-- > 0)
+			{
+				accumulator |= unpack_sample(src_ptr);
+				src_ptr += _unit_size;
+			}
+
+			pack_sample(dest_ptr, accumulator);
+		}
+	}
+}
+
+uint64_t LogicSnapshot::get_sample(uint64_t index) const
+{
+	assert(_data);
+	assert(index < _sample_count);
+
+	return unpack_sample((uint8_t*)_data + index * _unit_size);
+}
+
+void LogicSnapshot::get_subsampled_edges(
+	std::vector<EdgePair> &edges,
+	uint64_t start, uint64_t end,
+	float min_length, int sig_index)
+{
+	uint64_t index = start;
+	unsigned int level;
+	bool last_sample;
+	bool fast_forward;
+
+	assert(end <= get_sample_count());
+	assert(start <= end);
+	assert(min_length > 0);
+	assert(sig_index >= 0);
+	assert(sig_index < 64);
+
+	lock_guard<recursive_mutex> lock(_mutex);
+
+	const uint64_t block_length = (uint64_t)max(min_length, 1.0f);
+	const unsigned int min_level = max((int)floorf(logf(min_length) /
+		LogMipMapScaleFactor) - 1, 0);
+	const uint64_t sig_mask = 1ULL << sig_index;
+
+	// Store the initial state
+	last_sample = (get_sample(start) & sig_mask) != 0;
+	edges.push_back(pair<int64_t, bool>(index++, last_sample));
+
+	while (index + block_length <= end)
+	{
+		//----- Continue to search -----//
+		level = min_level;
+
+		// We cannot fast-forward if there is no mip-map data at
+		// at the minimum level.
+		fast_forward = (_mip_map[level].data != NULL);
+
+		if (min_length < MipMapScaleFactor)
+		{
+			// Search individual samples up to the beginning of
+			// the next first level mip map block
+			const uint64_t final_index = min(end,
+				pow2_ceil(index, MipMapScalePower));
+
+			for (; index < final_index &&
+				(index & ~(~0 << MipMapScalePower)) != 0;
+				index++)
+			{
+				const bool sample =
+					(get_sample(index) & sig_mask) != 0;
+
+				// If there was a change we cannot fast forward
+				if (sample != last_sample) {
+					fast_forward = false;
+					break;
+				}
+			}
+		}
+		else
+		{
+			// If resolution is less than a mip map block,
+			// round up to the beginning of the mip-map block
+			// for this level of detail
+			const int min_level_scale_power =
+				(level + 1) * MipMapScalePower;
+			index = pow2_ceil(index, min_level_scale_power);
+			if (index >= end)
+				break;
+
+			// We can fast forward only if there was no change
+			const bool sample =
+				(get_sample(index) & sig_mask) != 0;
+			if (last_sample != sample)
+				fast_forward = false;
+		}
+
+		if (fast_forward) {
+
+			// Fast forward: This involves zooming out to higher
+			// levels of the mip map searching for changes, then
+			// zooming in on them to find the point where the edge
+			// begins.
+
+			// Slide right and zoom out at the beginnings of mip-map
+			// blocks until we encounter a change
+			while (1) {
+				const int level_scale_power =
+					(level + 1) * MipMapScalePower;
+				const uint64_t offset =
+					index >> level_scale_power;
+
+				// Check if we reached the last block at this
+				// level, or if there was a change in this block
+				if (offset >= _mip_map[level].length ||
+					(get_subsample(level, offset) &
+						sig_mask))
+					break;
+
+				if ((offset & ~(~0 << MipMapScalePower)) == 0) {
+					// If we are now at the beginning of a
+					// higher level mip-map block ascend one
+					// level
+					if (level + 1 >= ScaleStepCount ||
+						!_mip_map[level + 1].data)
+						break;
+
+					level++;
+				} else {
+					// Slide right to the beginning of the
+					// next mip map block
+					index = pow2_ceil(index + 1,
+						level_scale_power);
+				}
+			}
+
+			// Zoom in, and slide right until we encounter a change,
+			// and repeat until we reach min_level
+			while (1) {
+				assert(_mip_map[level].data);
+
+				const int level_scale_power =
+					(level + 1) * MipMapScalePower;
+				const uint64_t offset =
+					index >> level_scale_power;
+
+				// Check if we reached the last block at this
+				// level, or if there was a change in this block
+				if (offset >= _mip_map[level].length ||
+					(get_subsample(level, offset) &
+						sig_mask)) {
+					// Zoom in unless we reached the minimum
+					// zoom
+					if (level == min_level)
+						break;
+
+					level--;
+				} else {
+					// Slide right to the beginning of the
+					// next mip map block
+					index = pow2_ceil(index + 1,
+						level_scale_power);
+				}
+			}
+
+			// If individual samples within the limit of resolution,
+			// do a linear search for the next transition within the
+			// block
+			if (min_length < MipMapScaleFactor) {
+				for (; index < end; index++) {
+					const bool sample = (get_sample(index) &
+						sig_mask) != 0;
+					if (sample != last_sample)
+						break;
+				}
+			}
+		}
+
+		//----- Store the edge -----//
+
+		// Take the last sample of the quanization block
+		const int64_t final_index = index + block_length;
+		if (index + block_length > end)
+			break;
+
+		// Store the final state
+		const bool final_sample =
+			(get_sample(final_index - 1) & sig_mask) != 0;
+		edges.push_back(pair<int64_t, bool>(index, final_sample));
+
+		index = final_index;
+		last_sample = final_sample;
+	}
+
+	// Add the final state
+	const bool end_sample = get_sample(end) & sig_mask;
+	if (last_sample != end_sample)
+		edges.push_back(pair<int64_t, bool>(end, end_sample));
+	edges.push_back(pair<int64_t, bool>(end + 1, end_sample));
+}
+
+uint64_t LogicSnapshot::get_subsample(int level, uint64_t offset) const
+{
+	assert(level >= 0);
+	assert(_mip_map[level].data);
+	return unpack_sample((uint8_t*)_mip_map[level].data +
+		_unit_size * offset);
+}
+
+uint64_t LogicSnapshot::pow2_ceil(uint64_t x, unsigned int power)
+{
+	const uint64_t p = 1 << power;
+	return (x + p - 1) / p * p;
+}
+
+} // namespace data
+} // namespace pv
diff --git a/pv/data/logicsnapshot.h b/pv/data/logicsnapshot.h
new file mode 100644
index 0000000..0f6f410
--- /dev/null
+++ b/pv/data/logicsnapshot.h
@@ -0,0 +1,115 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_DATA_LOGICSNAPSHOT_H
+#define PULSEVIEW_PV_DATA_LOGICSNAPSHOT_H
+
+#include "snapshot.h"
+
+#include <utility>
+#include <vector>
+
+namespace LogicSnapshotTest {
+class Pow2;
+class Basic;
+class LargeData;
+class Pulses;
+class LongPulses;
+}
+
+namespace pv {
+namespace data {
+
+class LogicSnapshot : public Snapshot
+{
+private:
+	struct MipMapLevel
+	{
+		uint64_t length;
+		uint64_t data_length;
+		void *data;
+	};
+
+private:
+	static const unsigned int ScaleStepCount = 10;
+	static const int MipMapScalePower;
+	static const int MipMapScaleFactor;
+	static const float LogMipMapScaleFactor;
+	static const uint64_t MipMapDataUnit;
+
+public:
+	typedef std::pair<int64_t, bool> EdgePair;
+
+public:
+	LogicSnapshot(const sr_datafeed_logic &logic,
+	              uint64_t expected_num_samples = 0);
+
+	virtual ~LogicSnapshot();
+
+	void append_payload(const sr_datafeed_logic &logic);
+
+	void get_samples(uint8_t *const data,
+		int64_t start_sample, int64_t end_sample) const;
+
+private:
+	uint64_t unpack_sample(const uint8_t *ptr) const;
+	void pack_sample(uint8_t *ptr, uint64_t value);
+	
+	void reallocate_mipmap_level(MipMapLevel &m);
+
+	void append_payload_to_mipmap();
+
+	uint64_t get_sample(uint64_t index) const;
+
+public:
+	/**
+	 * Parses a logic data snapshot to generate a list of transitions
+	 * in a time interval to a given level of detail.
+	 * @param[out] edges The vector to place the edges into.
+	 * @param[in] start The start sample index.
+	 * @param[in] end The end sample index.
+	 * @param[in] min_length The minimum number of samples that
+	 * can be resolved at this level of detail.
+	 * @param[in] sig_index The index of the signal.
+	 **/
+	void get_subsampled_edges(std::vector<EdgePair> &edges,
+		uint64_t start, uint64_t end,
+		float min_length, int sig_index);
+
+private:
+	uint64_t get_subsample(int level, uint64_t offset) const;
+
+	static uint64_t pow2_ceil(uint64_t x, unsigned int power);
+
+private:
+	struct MipMapLevel _mip_map[ScaleStepCount];
+	uint64_t _last_append_sample;
+
+	friend class LogicSnapshotTest::Pow2;
+	friend class LogicSnapshotTest::Basic;
+	friend class LogicSnapshotTest::LargeData;
+	friend class LogicSnapshotTest::Pulses;
+	friend class LogicSnapshotTest::LongPulses;
+};
+
+} // namespace data
+} // namespace pv
+
+#endif // PULSEVIEW_PV_DATA_LOGICSNAPSHOT_H
diff --git a/pv/data/signaldata.cpp b/pv/data/signaldata.cpp
new file mode 100644
index 0000000..04f1d3f
--- /dev/null
+++ b/pv/data/signaldata.cpp
@@ -0,0 +1,49 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "signaldata.h"
+
+namespace pv {
+namespace data {
+
+SignalData::SignalData() :
+	_start_time(0),
+	_samplerate(0)
+{
+}
+
+double SignalData::samplerate() const
+{
+	return _samplerate;
+}
+
+void SignalData::set_samplerate(double samplerate)
+{
+	_samplerate = samplerate;
+	clear();
+}
+
+double SignalData::get_start_time() const
+{
+	return _start_time;
+}
+
+} // namespace data
+} // namespace pv
diff --git a/pv/data/signaldata.h b/pv/data/signaldata.h
new file mode 100644
index 0000000..0aa3b34
--- /dev/null
+++ b/pv/data/signaldata.h
@@ -0,0 +1,52 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_DATA_SIGNALDATA_H
+#define PULSEVIEW_PV_DATA_SIGNALDATA_H
+
+#include <stdint.h>
+
+namespace pv {
+namespace data {
+
+class SignalData
+{
+public:
+	SignalData();
+
+public:
+	double samplerate() const;
+	void set_samplerate(double samplerate);
+
+	double get_start_time() const;
+
+	virtual void clear() = 0;
+
+	virtual uint64_t get_max_sample_count() const = 0;
+
+protected:
+	double _start_time;
+	double _samplerate;
+};
+
+} // namespace data
+} // namespace pv
+
+#endif // PULSEVIEW_PV_DATA_SIGNALDATA_H
diff --git a/pv/data/snapshot.cpp b/pv/data/snapshot.cpp
new file mode 100644
index 0000000..6ba39d2
--- /dev/null
+++ b/pv/data/snapshot.cpp
@@ -0,0 +1,95 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "snapshot.h"
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+
+using boost::lock_guard;
+using boost::recursive_mutex;
+
+namespace pv {
+namespace data {
+
+Snapshot::Snapshot(int unit_size) :
+	_data(NULL),
+	_sample_count(0),
+	_capacity(0),
+	_unit_size(unit_size)
+{
+	lock_guard<recursive_mutex> lock(_mutex);
+	assert(_unit_size > 0);
+}
+
+Snapshot::~Snapshot()
+{
+	lock_guard<recursive_mutex> lock(_mutex);
+	free(_data);
+}
+
+uint64_t Snapshot::get_sample_count() const
+{
+	lock_guard<recursive_mutex> lock(_mutex);
+	return _sample_count;
+}
+
+int Snapshot::unit_size() const
+{
+	return _unit_size;
+}
+
+void Snapshot::set_capacity(const uint64_t new_capacity)
+{
+	lock_guard<recursive_mutex> lock(_mutex);
+
+	assert(_capacity >= _sample_count);
+	if (new_capacity > _capacity) {
+		_capacity = new_capacity;
+		_data = realloc(_data, (new_capacity * _unit_size) + sizeof(uint64_t));
+	}
+}
+
+uint64_t Snapshot::capacity() const
+{
+	lock_guard<recursive_mutex> lock(_mutex);
+	return _capacity;
+}
+
+void Snapshot::append_data(void *data, uint64_t samples)
+{
+	lock_guard<recursive_mutex> lock(_mutex);
+
+	assert(_capacity >= _sample_count);
+
+	// Ensure there's enough capacity to copy.
+	const uint64_t free_space = _capacity - _sample_count;
+	if (free_space < samples) {
+		set_capacity(_sample_count + samples);
+	}
+
+	memcpy((uint8_t*)_data + _sample_count * _unit_size,
+		data, samples * _unit_size);
+	_sample_count += samples;
+}
+
+} // namespace data
+} // namespace pv
diff --git a/pv/data/snapshot.h b/pv/data/snapshot.h
new file mode 100644
index 0000000..1f1ca3c
--- /dev/null
+++ b/pv/data/snapshot.h
@@ -0,0 +1,83 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_DATA_SNAPSHOT_H
+#define PULSEVIEW_PV_DATA_SNAPSHOT_H
+
+#include <libsigrok/libsigrok.h>
+
+#include <boost/thread.hpp>
+
+namespace pv {
+namespace data {
+
+class Snapshot
+{
+public:
+	Snapshot(int unit_size);
+
+	virtual ~Snapshot();
+
+	uint64_t get_sample_count() const;
+
+	int unit_size() const;
+
+	/**
+	 * @brief Increase the capacity of the snapshot.
+	 *
+	 * Increasing the capacity allows samples to be appended without needing
+	 * to reallocate memory.
+	 *
+	 * For the best efficiency @c set_capacity() should be called once before
+	 * @c append_data() is called to set up the snapshot with the expected number
+	 * of samples that will be appended in total.
+	 *
+	 * @note The capacity will automatically be increased when @c append_data()
+	 * is called if there is not enough capacity in the buffer to store the samples.
+	 *
+	 * @param[in] new_capacity The new capacity of the snapshot. If this value is
+	 * 	smaller or equal than the current capacity then the method has no effect.
+	 */
+	void set_capacity(uint64_t new_capacity);
+
+	/**
+	 * @brief Get the current capacity of the snapshot.
+	 *
+	 * The capacity can be increased by calling @c set_capacity().
+	 *
+	 * @return The current capacity of the snapshot.
+	 */
+	uint64_t capacity() const;
+
+protected:
+	void append_data(void *data, uint64_t samples);
+
+protected:
+	mutable boost::recursive_mutex _mutex;
+	void *_data;
+	uint64_t _sample_count;
+	uint64_t _capacity;
+	int _unit_size;
+};
+
+} // namespace data
+} // namespace pv
+
+#endif // PULSEVIEW_PV_DATA_SNAPSHOT_H
diff --git a/pv/device/device.cpp b/pv/device/device.cpp
new file mode 100644
index 0000000..4f9a779
--- /dev/null
+++ b/pv/device/device.cpp
@@ -0,0 +1,104 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2014 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <sstream>
+
+#include <libsigrok/libsigrok.h>
+
+#include "device.h"
+
+using std::ostringstream;
+using std::string;
+
+namespace pv {
+namespace device {
+
+Device::Device(sr_dev_inst *sdi) :
+	_sdi(sdi)
+{
+	assert(_sdi);
+}
+
+sr_dev_inst* Device::dev_inst() const
+{
+	return _sdi;
+}
+
+void Device::use(SigSession *owner) throw(QString)
+{
+	DevInst::use(owner);
+
+	sr_session_new();
+
+	assert(_sdi);
+	sr_dev_open(_sdi);
+	if (sr_session_dev_add(_sdi) != SR_OK)
+		throw QString(tr("Failed to use device."));
+}
+
+void Device::release()
+{
+	if (_owner) {
+		DevInst::release();
+		sr_session_destroy();
+	}
+
+	sr_dev_close(_sdi);
+}
+
+std::string Device::format_device_title() const
+{
+	ostringstream s;
+
+	assert(_sdi);
+
+	if (_sdi->vendor && _sdi->vendor[0]) {
+		s << _sdi->vendor;
+		if ((_sdi->model && _sdi->model[0]) ||
+			(_sdi->version && _sdi->version[0]))
+			s << ' ';
+	}
+
+	if (_sdi->model && _sdi->model[0]) {
+		s << _sdi->model;
+		if (_sdi->version && _sdi->version[0])
+			s << ' ';
+	}
+
+	if (_sdi->version && _sdi->version[0])
+		s << _sdi->version;
+
+	return s.str();
+}
+
+bool Device::is_trigger_enabled() const
+{
+	assert(_sdi);
+	for (const GSList *l = _sdi->channels; l; l = l->next) {
+		const sr_channel *const p = (const sr_channel *)l->data;
+		assert(p);
+		if (p->trigger && p->trigger[0] != '\0')
+			return true;
+	}
+	return false;
+}
+
+} // device
+} // pv
diff --git a/pv/device/device.h b/pv/device/device.h
new file mode 100644
index 0000000..b9e1729
--- /dev/null
+++ b/pv/device/device.h
@@ -0,0 +1,51 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2014 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_DEVICE_DEVICE_H
+#define PULSEVIEW_PV_DEVICE_DEVICE_H
+
+#include "devinst.h"
+
+namespace pv {
+namespace device {
+
+class Device : public DevInst
+{
+public:
+	Device(sr_dev_inst *dev_inst);
+
+	sr_dev_inst* dev_inst() const;
+
+	void use(SigSession *owner) throw(QString);
+
+	void release();
+
+	std::string format_device_title() const;
+
+	bool is_trigger_enabled() const;
+
+private:
+	sr_dev_inst *const _sdi;
+};
+
+} // device
+} // pv
+
+#endif // PULSVIEW_PV_DEVICE_DEVICE_H
diff --git a/pv/device/devinst.cpp b/pv/device/devinst.cpp
new file mode 100644
index 0000000..4543e5c
--- /dev/null
+++ b/pv/device/devinst.cpp
@@ -0,0 +1,139 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2014 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <cassert>
+
+#include <QDebug>
+
+#include <libsigrok/libsigrok.h>
+
+#include "devinst.h"
+
+#include <pv/sigsession.h>
+
+namespace pv {
+namespace device {
+
+DevInst::DevInst() :
+	_owner(NULL)
+{
+}
+
+void DevInst::use(SigSession *owner) throw(QString)
+{
+	assert(owner);
+	assert(!_owner);
+	_owner = owner;
+}
+
+void DevInst::release()
+{
+	if (_owner) {
+		_owner->release_device(this);
+		_owner = NULL;
+	}
+}
+
+SigSession* DevInst::owner() const
+{
+	return _owner;
+}
+
+GVariant* DevInst::get_config(const sr_channel_group *group, int key)
+{
+	GVariant *data = NULL;
+	assert(_owner);
+	sr_dev_inst *const sdi = dev_inst();
+	assert(sdi);
+	if (sr_config_get(sdi->driver, sdi, group, key, &data) != SR_OK)
+		return NULL;
+	return data;
+}
+
+bool DevInst::set_config(const sr_channel_group *group, int key, GVariant *data)
+{
+	assert(_owner);
+	sr_dev_inst *const sdi = dev_inst();
+	assert(sdi);
+	if(sr_config_set(sdi, group, key, data) == SR_OK) {
+		config_changed();
+		return true;
+	}
+	return false;
+}
+
+GVariant* DevInst::list_config(const sr_channel_group *group, int key)
+{
+	GVariant *data = NULL;
+	assert(_owner);
+	sr_dev_inst *const sdi = dev_inst();
+	assert(sdi);
+	if (sr_config_list(sdi->driver, sdi, group, key, &data) != SR_OK)
+		return NULL;
+	return data;
+}
+
+void DevInst::enable_probe(const sr_channel *probe, bool enable)
+{
+	assert(_owner);
+	sr_dev_inst *const sdi = dev_inst();
+	assert(sdi);
+	for (const GSList *p = sdi->channels; p; p = p->next)
+		if (probe == p->data) {
+			const_cast<sr_channel*>(probe)->enabled = enable;
+			config_changed();
+			return;
+		}
+
+	// Probe was not found in the device
+	assert(0);
+}
+
+uint64_t DevInst::get_sample_limit()
+{
+	uint64_t sample_limit;
+	GVariant* gvar = get_config(NULL, SR_CONF_LIMIT_SAMPLES);
+	if (gvar != NULL) {
+		sample_limit = g_variant_get_uint64(gvar);
+		g_variant_unref(gvar);
+	} else {
+		sample_limit = 0U;
+	}
+	return sample_limit;
+}
+
+bool DevInst::is_trigger_enabled() const
+{
+	return false;
+}
+
+void DevInst::start()
+{
+	if (sr_session_start() != SR_OK)
+		throw tr("Failed to start session.");
+}
+
+void DevInst::run()
+{
+	sr_session_run();
+}
+
+} // device
+} // pv
diff --git a/pv/device/devinst.h b/pv/device/devinst.h
new file mode 100644
index 0000000..e6a5140
--- /dev/null
+++ b/pv/device/devinst.h
@@ -0,0 +1,95 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2014 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_DEVICE_DEVINST_H
+#define PULSEVIEW_PV_DEVICE_DEVINST_H
+
+#include <string>
+
+#include <boost/shared_ptr.hpp>
+
+#include <QObject>
+
+#include <glib.h>
+
+#include <stdint.h>
+
+struct sr_dev_inst;
+struct sr_channel;
+struct sr_channel_group;
+
+namespace pv {
+
+class SigSession;
+
+namespace device {
+
+class DevInst : public QObject
+{
+	Q_OBJECT
+
+protected:
+	DevInst();
+
+public:
+	virtual sr_dev_inst* dev_inst() const = 0;
+
+	virtual void use(SigSession *owner) throw(QString);
+
+	virtual void release();
+
+	SigSession* owner() const;
+
+	virtual std::string format_device_title() const = 0;
+
+	GVariant* get_config(const sr_channel_group *group, int key);
+
+	bool set_config(const sr_channel_group *group, int key, GVariant *data);
+
+	GVariant* list_config(const sr_channel_group *group, int key);
+
+	void enable_probe(const sr_channel *probe, bool enable = true);
+
+	/**
+	 * @brief Gets the sample limit from the driver.
+	 *
+	 * @return The returned sample limit from the driver, or 0 if the
+	 * 	sample limit could not be read.
+	 */
+	uint64_t get_sample_limit();
+
+	virtual bool is_trigger_enabled() const;
+
+public:
+	virtual void start();
+
+	virtual void run();
+
+signals:
+	void config_changed();
+
+protected:
+	SigSession *_owner;
+};
+
+} // device
+} // pv
+
+#endif // PULSEVIEW_PV_DEVICE_DEVINST_H
diff --git a/pv/device/file.cpp b/pv/device/file.cpp
new file mode 100644
index 0000000..e33be85
--- /dev/null
+++ b/pv/device/file.cpp
@@ -0,0 +1,66 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2014 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "file.h"
+#include "inputfile.h"
+#include "sessionfile.h"
+
+#include <boost/filesystem.hpp>
+
+#include <libsigrok/libsigrok.h>
+
+using std::string;
+
+namespace pv {
+namespace device {
+
+File::File(const std::string path) :
+	_path(path)
+{
+}
+
+std::string File::format_device_title() const
+{
+	return boost::filesystem::path(_path).filename().string();
+}
+
+File* File::create(const string &name)
+{
+	if (sr_session_load(name.c_str()) == SR_OK) {
+		GSList *devlist = NULL;
+		sr_session_dev_list(&devlist);
+		sr_session_destroy();
+
+		if (devlist) {
+			sr_dev_inst *const sdi = (sr_dev_inst*)devlist->data;
+			g_slist_free(devlist);
+			if (sdi) {
+				sr_dev_close(sdi);
+				sr_dev_clear(sdi->driver);
+				return new SessionFile(name);
+			}
+		}
+	}
+
+	return new InputFile(name);
+}
+
+} // device
+} // pv
diff --git a/pv/device/file.h b/pv/device/file.h
new file mode 100644
index 0000000..4a45a82
--- /dev/null
+++ b/pv/device/file.h
@@ -0,0 +1,49 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2014 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_DEVICE_FILE_H
+#define PULSEVIEW_PV_DEVICE_FILE_H
+
+#include <string>
+
+#include "devinst.h"
+
+namespace pv {
+namespace device {
+
+class File : public DevInst
+{
+protected:
+	File(const std::string path);
+
+public:
+	static File* create(const std::string &name);
+
+public:
+	std::string format_device_title() const;
+
+protected:
+	const std::string _path;
+};
+
+} // device
+} // pv
+
+#endif // PULSEVIEW_PV_DEVICE_FILE_H
diff --git a/pv/device/inputfile.cpp b/pv/device/inputfile.cpp
new file mode 100644
index 0000000..55a0fa5
--- /dev/null
+++ b/pv/device/inputfile.cpp
@@ -0,0 +1,143 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2014 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <cassert>
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "inputfile.h"
+
+#include <libsigrok/libsigrok.h>
+
+using std::string;
+
+namespace pv {
+namespace device {
+
+InputFile::InputFile(const std::string &path) :
+	File(path),
+	_input(NULL)
+{
+}
+
+sr_dev_inst* InputFile::dev_inst() const
+{
+	assert(_input);
+	return _input->sdi;
+}
+
+void InputFile::use(SigSession *owner) throw(QString)
+{
+	assert(!_input);
+
+	_input = load_input_file_format(_path, NULL);
+	File::use(owner);
+
+	sr_session_new();
+
+	if (sr_session_dev_add(_input->sdi) != SR_OK)
+		throw tr("Failed to add session device.");
+}
+
+void InputFile::release()
+{
+	if (!_owner)
+		return;
+
+	assert(_input);
+	File::release();
+	sr_dev_close(_input->sdi);
+	sr_session_destroy();
+	_input = NULL;
+}
+
+sr_input_format* InputFile::determine_input_file_format(
+	const string &filename)
+{
+	int i;
+
+	/* If there are no input formats, return NULL right away. */
+	sr_input_format *const *const inputs = sr_input_list();
+	if (!inputs) {
+		g_critical("No supported input formats available.");
+		return NULL;
+	}
+
+	/* Otherwise, try to find an input module that can handle this file. */
+	for (i = 0; inputs[i]; i++) {
+		if (inputs[i]->format_match(filename.c_str()))
+			break;
+	}
+
+	/* Return NULL if no input module wanted to touch this. */
+	if (!inputs[i]) {
+		g_critical("Error: no matching input module found.");
+		return NULL;
+	}
+
+	return inputs[i];
+}
+
+sr_input* InputFile::load_input_file_format(const string &filename,
+	sr_input_format *format)
+{
+	struct stat st;
+	sr_input *in;
+
+	if (!format && !(format =
+		determine_input_file_format(filename.c_str()))) {
+		/* The exact cause was already logged. */
+		throw tr("Failed to load file");
+	}
+
+	if (stat(filename.c_str(), &st) == -1)
+		throw tr("Failed to load file");
+
+	/* Initialize the input module. */
+	if (!(in = new sr_input)) {
+		throw tr("Failed to allocate input module.");
+	}
+
+	in->format = format;
+	in->param = NULL;
+	if (in->format->init &&
+		in->format->init(in, filename.c_str()) != SR_OK) {
+		throw tr("Failed to load file");
+	}
+
+	return in;
+}
+
+void InputFile::start()
+{
+}
+
+void InputFile::run()
+{
+	assert(_input);
+	assert(_input->format);
+	assert(_input->format->loadfile);
+	_input->format->loadfile(_input, _path.c_str());
+}
+
+} // device
+} // pv
diff --git a/pv/device/inputfile.h b/pv/device/inputfile.h
new file mode 100644
index 0000000..150418d
--- /dev/null
+++ b/pv/device/inputfile.h
@@ -0,0 +1,68 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2014 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_DEVICE_INPUTFILE_H
+#define PULSEVIEW_PV_DEVICE_INPUTFILE_H
+
+#include "file.h"
+
+#include <string>
+
+struct sr_input;
+struct sr_input_format;
+
+namespace pv {
+namespace device {
+
+class InputFile : public File
+{
+public:
+	InputFile(const std::string &path);
+
+	sr_dev_inst* dev_inst() const;
+
+	virtual void use(SigSession *owner) throw(QString);
+
+	virtual void release();
+
+	virtual void start();
+
+	virtual void run();
+
+private:
+	/**
+	 * Attempts to autodetect the format. Failing that
+	 * @param filename The filename of the input file.
+	 * @return A pointer to the 'struct sr_input_format' that should be used,
+	 *         or NULL if no input format was selected or auto-detected.
+	 */
+	static sr_input_format* determine_input_file_format(
+		const std::string &filename);
+
+	static sr_input* load_input_file_format(const std::string &filename,
+		sr_input_format *format);
+private:
+	sr_input *_input;
+};
+
+} // device
+} // pv
+
+#endif // PULSEVIEW_PV_DEVICE_INPUTFILE_H
diff --git a/pv/device/sessionfile.cpp b/pv/device/sessionfile.cpp
new file mode 100644
index 0000000..ffdeb67
--- /dev/null
+++ b/pv/device/sessionfile.cpp
@@ -0,0 +1,75 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2014 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "sessionfile.h"
+
+#include <libsigrok/libsigrok.h>
+
+namespace pv {
+namespace device {
+
+SessionFile::SessionFile(const std::string &path) :
+	File(path),
+	_sdi(NULL)
+{
+}
+
+sr_dev_inst* SessionFile::dev_inst() const
+{
+	return _sdi;
+}
+
+void SessionFile::use(SigSession *owner) throw(QString)
+{
+	assert(!_sdi);
+
+	if (sr_session_load(_path.c_str()) != SR_OK)
+		throw tr("Failed to open file.\n");
+
+	GSList *devlist = NULL;
+	sr_session_dev_list(&devlist);
+
+	if (!devlist || !devlist->data) {
+		if (devlist)
+			g_slist_free(devlist);
+		throw tr("Failed to start session.");
+	}
+
+	_sdi = (sr_dev_inst*)devlist->data;
+	g_slist_free(devlist);
+
+	File::use(owner);
+}
+
+void SessionFile::release()
+{
+	if (!_owner)
+		return;
+
+	assert(_sdi);
+	File::release();
+	sr_dev_close(_sdi);
+	sr_dev_clear(_sdi->driver);
+	sr_session_destroy();
+	_sdi = NULL;
+}
+
+} // device
+} // pv
diff --git a/pv/device/sessionfile.h b/pv/device/sessionfile.h
new file mode 100644
index 0000000..c0ed958
--- /dev/null
+++ b/pv/device/sessionfile.h
@@ -0,0 +1,47 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2014 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_DEVICE_SESSIONFILE_H
+#define PULSEVIEW_PV_DEVICE_SESSIONFILE_H
+
+#include "file.h"
+
+namespace pv {
+namespace device {
+
+class SessionFile : public File
+{
+public:
+	SessionFile(const std::string &path);
+
+	sr_dev_inst* dev_inst() const;
+
+	virtual void use(SigSession *owner) throw(QString);
+
+	virtual void release();
+
+private:
+	sr_dev_inst *_sdi;
+};
+
+} // device
+} // pv
+
+#endif // PULSEVIEW_PV_DEVICE_SESSIONFILE_H
diff --git a/pv/devicemanager.cpp b/pv/devicemanager.cpp
new file mode 100644
index 0000000..fb487a8
--- /dev/null
+++ b/pv/devicemanager.cpp
@@ -0,0 +1,150 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "devicemanager.h"
+#include "device/device.h"
+#include "sigsession.h"
+
+#include <cassert>
+#include <stdexcept>
+#include <string>
+
+#include <boost/foreach.hpp>
+
+#include <libsigrok/libsigrok.h>
+
+using boost::shared_ptr;
+using std::list;
+using std::map;
+using std::ostringstream;
+using std::runtime_error;
+using std::string;
+
+namespace pv {
+
+DeviceManager::DeviceManager(struct sr_context *sr_ctx) :
+	_sr_ctx(sr_ctx)
+{
+	init_drivers();
+	scan_all_drivers();
+}
+
+DeviceManager::~DeviceManager()
+{
+	release_devices();
+}
+
+const list< shared_ptr<pv::device::Device> >& DeviceManager::devices() const
+{
+	return _devices;
+}
+
+list< shared_ptr<device::Device> > DeviceManager::driver_scan(
+	struct sr_dev_driver *const driver, GSList *const drvopts)
+{
+	list< shared_ptr<device::Device> > driver_devices;
+
+	assert(driver);
+
+	// Remove any device instances from this driver from the device
+	// list. They will not be valid after the scan.
+	list< shared_ptr<device::Device> >::iterator i = _devices.begin();
+	while (i != _devices.end()) {
+		if ((*i)->dev_inst()->driver == driver)
+			i = _devices.erase(i);
+		else
+			i++;
+	}
+
+	// Release this driver and all it's attached devices
+	release_driver(driver);
+
+	// Do the scan
+	GSList *const devices = sr_driver_scan(driver, drvopts);
+	for (GSList *l = devices; l; l = l->next)
+		driver_devices.push_back(shared_ptr<device::Device>(
+			new device::Device((sr_dev_inst*)l->data)));
+	g_slist_free(devices);
+	driver_devices.sort(compare_devices);
+
+	// Add the scanned devices to the main list
+	_devices.insert(_devices.end(), driver_devices.begin(),
+		driver_devices.end());
+	_devices.sort(compare_devices);
+
+	return driver_devices;
+}
+
+void DeviceManager::init_drivers()
+{
+	// Initialise all libsigrok drivers
+	sr_dev_driver **const drivers = sr_driver_list();
+	for (sr_dev_driver **driver = drivers; *driver; driver++) {
+		if (sr_driver_init(_sr_ctx, *driver) != SR_OK) {
+			throw runtime_error(
+				string("Failed to initialize driver ") +
+				string((*driver)->name));
+		}
+	}
+}
+
+void DeviceManager::release_devices()
+{
+	// Release all the used devices
+	BOOST_FOREACH(shared_ptr<device::Device> dev, _devices) {
+		assert(dev);
+		dev->release();
+	}
+
+	// Clear all the drivers
+	sr_dev_driver **const drivers = sr_driver_list();
+	for (sr_dev_driver **driver = drivers; *driver; driver++)
+		sr_dev_clear(*driver);
+}
+
+void DeviceManager::scan_all_drivers()
+{
+	// Scan all drivers for all devices.
+	struct sr_dev_driver **const drivers = sr_driver_list();
+	for (struct sr_dev_driver **driver = drivers; *driver; driver++)
+		driver_scan(*driver);
+}
+
+void DeviceManager::release_driver(struct sr_dev_driver *const driver)
+{
+	BOOST_FOREACH(shared_ptr<device::Device> dev, _devices) {
+		assert(dev);
+		if(dev->dev_inst()->driver == driver)
+			dev->release();
+	}
+
+	// Clear all the old device instances from this driver
+	sr_dev_clear(driver);
+}
+
+bool DeviceManager::compare_devices(shared_ptr<device::Device> a,
+	shared_ptr<device::Device> b)
+{
+	assert(a);
+	assert(b);
+	return a->format_device_title().compare(b->format_device_title()) < 0;
+}
+
+} // namespace pv
diff --git a/pv/devicemanager.h b/pv/devicemanager.h
new file mode 100644
index 0000000..acef8a4
--- /dev/null
+++ b/pv/devicemanager.h
@@ -0,0 +1,75 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_DEVICEMANAGER_H
+#define PULSEVIEW_PV_DEVICEMANAGER_H
+
+#include <glib.h>
+
+#include <list>
+#include <string>
+
+#include <boost/shared_ptr.hpp>
+
+struct sr_context;
+struct sr_dev_driver;
+
+namespace pv {
+
+class SigSession;
+
+namespace device {
+class Device;
+}
+
+class DeviceManager
+{
+public:
+	DeviceManager(struct sr_context *sr_ctx);
+
+	~DeviceManager();
+
+	const std::list< boost::shared_ptr<pv::device::Device> >&
+		devices() const;
+
+	std::list< boost::shared_ptr<pv::device::Device> > driver_scan(
+		struct sr_dev_driver *const driver,
+		GSList *const drvopts = NULL);
+
+private:
+	void init_drivers();
+
+	void release_devices();
+
+	void scan_all_drivers();
+
+	void release_driver(struct sr_dev_driver *const driver);
+
+	static bool compare_devices(boost::shared_ptr<device::Device> a,
+		boost::shared_ptr<device::Device> b);
+
+private:
+	struct sr_context *const _sr_ctx;
+	std::list< boost::shared_ptr<pv::device::Device> > _devices;
+};
+
+} // namespace pv
+
+#endif // PULSEVIEW_PV_DEVICEMANAGER_H
diff --git a/pv/dialogs/about.cpp b/pv/dialogs/about.cpp
new file mode 100644
index 0000000..fe0f16a
--- /dev/null
+++ b/pv/dialogs/about.cpp
@@ -0,0 +1,121 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifdef ENABLE_DECODE
+#include <libsigrokdecode/libsigrokdecode.h>
+#endif
+
+#include <QTextDocument>
+
+#include "about.h"
+#include <ui_about.h>
+
+/* __STDC_FORMAT_MACROS is required for PRIu64 and friends (in C++). */
+#define __STDC_FORMAT_MACROS
+#include <glib.h>
+#include <libsigrok/libsigrok.h>
+
+
+namespace pv {
+namespace dialogs {
+
+About::About(QWidget *parent) :
+	QDialog(parent),
+	ui(new Ui::About)
+{
+	struct sr_dev_driver **drivers;
+	struct sr_input_format **inputs;
+	struct sr_output_format **outputs;
+
+#ifdef ENABLE_DECODE
+	struct srd_decoder *dec;
+#endif
+
+	QString s;
+
+	ui->setupUi(this);
+
+	/* Setup the version field */
+	ui->versionInfo->setText(tr("%1 %2<br />%3<br /><a href=\"%4\">%4</a>")
+				 .arg(QApplication::applicationName())
+				 .arg(QApplication::applicationVersion())
+				 .arg(tr("GNU GPL, version 3 or later"))
+				 .arg(QApplication::organizationDomain()));
+	ui->versionInfo->setOpenExternalLinks(true);
+
+	s.append("<table>");
+
+	/* Set up the supported field */
+	s.append("<tr><td colspan=\"2\"><b>" +
+		tr("Supported hardware drivers:") +
+		"</b></td></tr>");
+	drivers = sr_driver_list();
+	for (int i = 0; drivers[i]; ++i) {
+		s.append(QString("<tr><td><i>%1</i></td><td>%2</td></tr>")
+			 .arg(QString::fromUtf8(drivers[i]->name))
+			 .arg(QString::fromUtf8(drivers[i]->longname)));
+	}
+
+	s.append("<tr><td colspan=\"2\"><b>" +
+		tr("Supported input formats:") +
+		"</b></td></tr>");
+	inputs = sr_input_list();
+	for (int i = 0; inputs[i]; ++i) {
+		s.append(QString("<tr><td><i>%1</i></td><td>%2</td></tr>")
+			 .arg(QString::fromUtf8(inputs[i]->id))
+			 .arg(QString::fromUtf8(inputs[i]->description)));
+	}
+
+	s.append("<tr><td colspan=\"2\"><b>" +
+		tr("Supported output formats:") +
+		"</b></td></tr>");
+	outputs = sr_output_list();
+	for (int i = 0; outputs[i]; ++i) {
+		s.append(QString("<tr><td><i>%1</i></td><td>%2</td></tr>")
+			.arg(QString::fromUtf8(outputs[i]->id))
+			.arg(QString::fromUtf8(outputs[i]->description)));
+	}
+
+#ifdef ENABLE_DECODE
+	s.append("<tr><td colspan=\"2\"><b>" +
+		tr("Supported protocol decoders:") +
+		"</b></td></tr>");
+	for (const GSList *l = srd_decoder_list(); l; l = l->next) {
+		dec = (struct srd_decoder *)l->data;
+		s.append(QString("<tr><td><i>%1</i></td><td>%2</td></tr>")
+			 .arg(QString::fromUtf8(dec->id))
+			 .arg(QString::fromUtf8(dec->longname)));
+	}
+#endif
+
+	s.append("</table>");
+
+	supportedDoc.reset(new QTextDocument(this));
+	supportedDoc->setHtml(s);
+	ui->supportList->setDocument(supportedDoc.get());
+}
+
+About::~About()
+{
+	delete ui;
+}
+
+} // namespace dialogs
+} // namespace pv
diff --git a/pv/dialogs/about.h b/pv/dialogs/about.h
new file mode 100644
index 0000000..cc153ad
--- /dev/null
+++ b/pv/dialogs/about.h
@@ -0,0 +1,53 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_ABOUT_H
+#define PULSEVIEW_PV_ABOUT_H
+
+#include <QDialog>
+
+#include <memory>
+
+class QTextDocument;
+
+namespace Ui {
+class About;
+}
+
+namespace pv {
+namespace dialogs {
+
+class About : public QDialog
+{
+	Q_OBJECT
+
+public:
+	explicit About(QWidget *parent = 0);
+	~About();
+
+private:
+	Ui::About *ui;
+	std::auto_ptr<QTextDocument> supportedDoc;
+};
+
+} // namespace dialogs
+} // namespace pv
+
+#endif // PULSEVIEW_PV_ABOUT_H
diff --git a/pv/dialogs/about.ui b/pv/dialogs/about.ui
new file mode 100644
index 0000000..10163f9
--- /dev/null
+++ b/pv/dialogs/about.ui
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>About</class>
+ <widget class="QDialog" name="About">
+  <property name="windowModality">
+   <enum>Qt::WindowModal</enum>
+  </property>
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>600</width>
+    <height>400</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>About</string>
+  </property>
+  <property name="whatsThis">
+   <string/>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QGridLayout" name="gridLayout">
+     <item row="0" column="0">
+      <widget class="QLabel" name="icon">
+       <property name="text">
+        <string/>
+       </property>
+       <property name="pixmap">
+        <pixmap resource="pulseview.qrc">:/icons/sigrok-logo-notext.png</pixmap>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="1">
+      <widget class="QLabel" name="versionInfo">
+       <property name="text">
+        <string/>
+       </property>
+      </widget>
+     </item>
+     <item row="1" column="1">
+      <widget class="QTextBrowser" name="supportList"/>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources>
+  <include location="pulseview.qrc"/>
+ </resources>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>About</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>About</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
diff --git a/pv/dialogs/connect.cpp b/pv/dialogs/connect.cpp
new file mode 100644
index 0000000..d5ab9c4
--- /dev/null
+++ b/pv/dialogs/connect.cpp
@@ -0,0 +1,241 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012-2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <boost/foreach.hpp>
+
+#include <libsigrok/libsigrok.h>
+
+#include "connect.h"
+
+#include "pv/devicemanager.h"
+#include "pv/device/device.h"
+
+extern "C" {
+/* __STDC_FORMAT_MACROS is required for PRIu64 and friends (in C++). */
+#define __STDC_FORMAT_MACROS
+#include <glib.h>
+#include <libsigrok/libsigrok.h>
+}
+
+using boost::shared_ptr;
+using std::list;
+using std::string;
+
+extern sr_context *sr_ctx;
+
+namespace pv {
+namespace dialogs {
+
+Connect::Connect(QWidget *parent, pv::DeviceManager &device_manager) :
+	QDialog(parent),
+	_device_manager(device_manager),
+	_layout(this),
+	_form(this),
+	_form_layout(&_form),
+	_drivers(&_form),
+	_serial_device(&_form),
+	_scan_button(tr("Scan for Devices"), this),
+	_device_list(this),
+	_button_box(QDialogButtonBox::Ok | QDialogButtonBox::Cancel,
+		Qt::Horizontal, this)
+{
+	setWindowTitle(tr("Connect to Device"));
+
+	connect(&_button_box, SIGNAL(accepted()), this, SLOT(accept()));
+	connect(&_button_box, SIGNAL(rejected()), this, SLOT(reject()));
+
+	populate_drivers();
+	connect(&_drivers, SIGNAL(activated(int)),
+		this, SLOT(device_selected(int)));
+
+	_form.setLayout(&_form_layout);
+	_form_layout.addRow(tr("Driver"), &_drivers);
+
+	_form_layout.addRow(tr("Serial Port"), &_serial_device);
+
+	unset_connection();
+
+	connect(&_scan_button, SIGNAL(pressed()),
+		this, SLOT(scan_pressed()));
+
+	setLayout(&_layout);
+	_layout.addWidget(&_form);
+	_layout.addWidget(&_scan_button);
+	_layout.addWidget(&_device_list);
+	_layout.addWidget(&_button_box);
+}
+
+shared_ptr<device::Device> Connect::get_selected_device() const
+{
+	const QListWidgetItem *const item = _device_list.currentItem();
+	if (!item)
+		return shared_ptr<device::Device>();
+
+	const sr_dev_inst *const sdi = (sr_dev_inst*)item->data(
+		Qt::UserRole).value<void*>();
+	assert(sdi);
+
+	std::map<const sr_dev_inst*, boost::shared_ptr<pv::device::Device> >::
+		const_iterator iter = _device_map.find(sdi);
+	assert(iter != _device_map.end());
+
+	return (*iter).second;
+}
+
+void Connect::populate_drivers()
+{
+	gsize num_opts = 0;
+	const int32_t *hwopts;
+	struct sr_dev_driver **drivers = sr_driver_list();
+	GVariant *gvar_opts;
+
+	for (int i = 0; drivers[i]; ++i) {
+		/**
+		 * We currently only support devices that can deliver
+		 * samples at a fixed samplerate i.e. oscilloscopes and
+		 * logic analysers.
+		 * @todo Add support for non-monotonic devices i.e. DMMs
+		 * and sensors.
+		 */
+		bool supported_device = false;
+		if ((sr_config_list(drivers[i], NULL, NULL,
+				SR_CONF_DEVICE_OPTIONS, &gvar_opts) == SR_OK)) {
+			hwopts = (const int32_t *)g_variant_get_fixed_array(gvar_opts,
+					&num_opts, sizeof(int32_t));
+			for (unsigned int j = 0; j < num_opts; j++)
+				if (hwopts[j] == SR_CONF_SAMPLERATE) {
+					supported_device = true;
+					break;
+				}
+		}
+
+		if (supported_device)
+			_drivers.addItem(QString("%1 (%2)").arg(
+				drivers[i]->longname).arg(drivers[i]->name),
+				qVariantFromValue((void*)drivers[i]));
+	}
+}
+
+void Connect::unset_connection()
+{
+	_device_list.clear();
+	_device_map.clear();
+	_serial_device.hide();
+	_form_layout.labelForField(&_serial_device)->hide();
+	_button_box.button(QDialogButtonBox::Ok)->setDisabled(true);
+}
+
+void Connect::set_serial_connection()
+{
+	_serial_device.show();
+	_form_layout.labelForField(&_serial_device)->show();
+}
+
+void Connect::scan_pressed()
+{
+	_device_list.clear();
+	_device_map.clear();
+
+	const int index = _drivers.currentIndex();
+	if (index == -1)
+		return;
+
+	sr_dev_driver *const driver = (sr_dev_driver*)_drivers.itemData(
+		index).value<void*>();
+
+	GSList *drvopts = NULL;
+
+	if (_serial_device.isVisible()) {
+		sr_config *const src = (sr_config*)g_try_malloc(sizeof(sr_config));
+		src->key = SR_CONF_CONN;
+		const QByteArray byteArray = _serial_device.text().toUtf8();
+		src->data = g_variant_new_string((const gchar*)byteArray.constData());
+		drvopts = g_slist_append(drvopts, src);
+	}
+
+	const list< shared_ptr<device::Device> > devices =
+		_device_manager.driver_scan(driver, drvopts);
+
+	g_slist_free_full(drvopts, (GDestroyNotify)free_drvopts);
+
+	BOOST_FOREACH(shared_ptr<device::Device> dev_inst, devices)
+	{
+		assert(dev_inst);
+		const sr_dev_inst *const sdi = dev_inst->dev_inst();
+		assert(sdi);
+
+		const string title = dev_inst->format_device_title();
+		QString text = QString::fromUtf8(title.c_str());
+
+		if (sdi->channels) {
+			text += QString(" with %1 channels").arg(
+				g_slist_length(sdi->channels));
+		}
+
+		QListWidgetItem *const item = new QListWidgetItem(text,
+			&_device_list);
+		item->setData(Qt::UserRole, qVariantFromValue((void*)sdi));
+		_device_list.addItem(item);
+		_device_map[sdi] = dev_inst;
+	}
+
+	_device_list.setCurrentRow(0);
+	_button_box.button(QDialogButtonBox::Ok)->setDisabled(_device_list.count() == 0);
+}
+
+void Connect::device_selected(int index)
+{
+	gsize num_opts = 0;
+	const int32_t *hwopts;
+	GVariant *gvar_list;
+	sr_dev_driver *const driver = (sr_dev_driver*)_drivers.itemData(
+		index).value<void*>();
+
+	unset_connection();
+
+	if ((sr_config_list(driver, NULL, NULL,
+				SR_CONF_SCAN_OPTIONS, &gvar_list) == SR_OK)) {
+		hwopts = (const int32_t *)g_variant_get_fixed_array(gvar_list,
+				&num_opts, sizeof(int32_t));
+
+		for (unsigned int i = 0; i < num_opts; i++) {
+			switch(hwopts[i]) {
+			case SR_CONF_SERIALCOMM:
+				set_serial_connection();
+				break;
+
+			default:
+				continue;
+			}
+
+			break;
+		}
+		g_variant_unref(gvar_list);
+	}
+}
+
+void Connect::free_drvopts(struct sr_config *src)
+{
+	g_variant_unref(src->data);
+	g_free(src);
+}
+
+} // namespace dialogs
+} // namespace pv
diff --git a/pv/dialogs/connect.h b/pv/dialogs/connect.h
new file mode 100644
index 0000000..1de0ed8
--- /dev/null
+++ b/pv/dialogs/connect.h
@@ -0,0 +1,95 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012-2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_CONNECT_H
+#define PULSEVIEW_PV_CONNECT_H
+
+#include <boost/shared_ptr.hpp>
+
+#include <QComboBox>
+#include <QDialog>
+#include <QDialogButtonBox>
+#include <QFormLayout>
+#include <QLineEdit>
+#include <QListWidget>
+#include <QPushButton>
+#include <QVBoxLayout>
+
+struct sr_config;
+struct sr_dev_inst;
+
+namespace pv {
+
+class DeviceManager;
+
+namespace device {
+class Device;
+}
+
+namespace dialogs {
+
+class Connect : public QDialog
+{
+	Q_OBJECT
+
+public:
+	Connect(QWidget *parent, pv::DeviceManager &device_manager);
+
+	boost::shared_ptr<device::Device> get_selected_device() const;
+
+private:
+	void populate_drivers();
+
+	void unset_connection();
+
+	void set_serial_connection();
+
+private slots:
+	void device_selected(int index);
+
+	void scan_pressed();
+
+private:
+	static void free_drvopts(sr_config *src);
+
+private:
+	pv::DeviceManager &_device_manager;
+
+	QVBoxLayout _layout;
+
+	QWidget _form;
+	QFormLayout _form_layout;
+
+	QComboBox _drivers;
+
+	QLineEdit _serial_device;
+
+	QPushButton _scan_button;
+	QListWidget _device_list;
+	std::map<const sr_dev_inst*, boost::shared_ptr<pv::device::Device> >
+		_device_map;
+
+	QDialogButtonBox _button_box;
+};
+
+} // namespace dialogs
+} // namespace pv
+
+#endif // PULSEVIEW_PV_CONNECT_H
diff --git a/pv/dialogs/storeprogress.cpp b/pv/dialogs/storeprogress.cpp
new file mode 100644
index 0000000..7853173
--- /dev/null
+++ b/pv/dialogs/storeprogress.cpp
@@ -0,0 +1,84 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2014 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "storeprogress.h"
+
+#include <QMessageBox>
+
+namespace pv {
+namespace dialogs {
+
+StoreProgress::StoreProgress(const QString &file_name,
+	const SigSession &session, QWidget *parent) :
+	QProgressDialog(tr("Saving..."), tr("Cancel"), 0, 0, parent),
+	_session(file_name.toStdString(), session)
+{
+	connect(&_session, SIGNAL(progress_updated()),
+		this, SLOT(on_progress_updated()));
+}
+
+StoreProgress::~StoreProgress()
+{
+	_session.wait();
+}
+
+void StoreProgress::run()
+{
+	if (_session.start())
+		show();
+	else
+		show_error();
+}
+
+void StoreProgress::show_error()
+{
+	QMessageBox msg(parentWidget());
+	msg.setText(tr("Failed to save session."));
+	msg.setInformativeText(_session.error());
+	msg.setStandardButtons(QMessageBox::Ok);
+	msg.setIcon(QMessageBox::Warning);
+	msg.exec();
+}
+
+void StoreProgress::closeEvent(QCloseEvent*)
+{
+	_session.cancel();
+}
+
+void StoreProgress::on_progress_updated()
+{
+	const std::pair<uint64_t, uint64_t> p = _session.progress();
+	assert(p.first <= p.second);
+
+	setValue(p.first);
+	setMaximum(p.second);
+
+	const QString err = _session.error();
+	if (!err.isEmpty()) {
+		show_error();
+		close();
+	}
+
+	if (p.first == p.second)
+		close();
+}
+
+} // dialogs
+} // pv
diff --git a/pv/dialogs/storeprogress.h b/pv/dialogs/storeprogress.h
new file mode 100644
index 0000000..61d08d5
--- /dev/null
+++ b/pv/dialogs/storeprogress.h
@@ -0,0 +1,65 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2014 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_DIALOGS_SAVEPROGRESS_H
+#define PULSEVIEW_PV_DIALOGS_SAVEPROGRESS_H
+
+#include <set>
+
+#include <boost/shared_ptr.hpp>
+
+#include <QProgressDialog>
+
+#include <pv/storesession.h>
+
+namespace pv {
+
+class SigSession;
+
+namespace dialogs {
+
+class StoreProgress : public QProgressDialog
+{
+	Q_OBJECT
+
+public:
+	StoreProgress(const QString &file_name, const SigSession &session,
+		QWidget *parent = 0);
+
+	virtual ~StoreProgress();
+
+	void run();
+
+private:
+	void show_error();
+
+	void closeEvent(QCloseEvent*);
+
+private slots:
+	void on_progress_updated();
+
+private:
+	pv::StoreSession _session;
+};
+
+} // dialogs
+} // pv
+
+#endif // PULSEVIEW_PV_DIALOGS_SAVEPROGRESS_H
diff --git a/pv/mainwindow.cpp b/pv/mainwindow.cpp
new file mode 100644
index 0000000..0089b11
--- /dev/null
+++ b/pv/mainwindow.cpp
@@ -0,0 +1,456 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifdef ENABLE_DECODE
+#include <libsigrokdecode/libsigrokdecode.h>
+#endif
+
+#include <boost/bind.hpp>
+#include <boost/foreach.hpp>
+
+#include <algorithm>
+#include <iterator>
+
+#include <QAction>
+#include <QApplication>
+#include <QButtonGroup>
+#include <QFileDialog>
+#include <QMessageBox>
+#include <QMenu>
+#include <QMenuBar>
+#include <QStatusBar>
+#include <QVBoxLayout>
+#include <QWidget>
+
+#include "mainwindow.h"
+
+#include "devicemanager.h"
+#include "device/device.h"
+#include "dialogs/about.h"
+#include "dialogs/connect.h"
+#include "dialogs/storeprogress.h"
+#include "toolbars/samplingbar.h"
+#include "view/logicsignal.h"
+#include "view/view.h"
+#ifdef ENABLE_DECODE
+#include "widgets/decodermenu.h"
+#endif
+
+/* __STDC_FORMAT_MACROS is required for PRIu64 and friends (in C++). */
+#define __STDC_FORMAT_MACROS
+#include <inttypes.h>
+#include <stdint.h>
+#include <stdarg.h>
+#include <glib.h>
+#include <libsigrok/libsigrok.h>
+
+using boost::shared_ptr;
+using std::list;
+
+namespace pv {
+
+namespace view {
+class SelectableItem;
+}
+
+MainWindow::MainWindow(DeviceManager &device_manager,
+	const char *open_file_name,
+	QWidget *parent) :
+	QMainWindow(parent),
+	_device_manager(device_manager),
+	_session(device_manager)
+{
+	setup_ui();
+	if (open_file_name) {
+		const QString s(QString::fromUtf8(open_file_name));
+		QMetaObject::invokeMethod(this, "load_file",
+			Qt::QueuedConnection,
+			Q_ARG(QString, s));
+	}
+}
+
+void MainWindow::setup_ui()
+{
+	setObjectName(QString::fromUtf8("MainWindow"));
+
+	resize(1024, 768);
+
+	// Set the window icon
+	QIcon icon;
+	icon.addFile(QString::fromUtf8(":/icons/sigrok-logo-notext.png"),
+		QSize(), QIcon::Normal, QIcon::Off);
+	setWindowIcon(icon);
+
+	// Setup the central widget
+	_central_widget = new QWidget(this);
+	_vertical_layout = new QVBoxLayout(_central_widget);
+	_vertical_layout->setSpacing(6);
+	_vertical_layout->setContentsMargins(0, 0, 0, 0);
+	setCentralWidget(_central_widget);
+
+	_view = new pv::view::View(_session, this);
+
+	_vertical_layout->addWidget(_view);
+
+	// Setup the menu bar
+	QMenuBar *const menu_bar = new QMenuBar(this);
+	menu_bar->setGeometry(QRect(0, 0, 400, 25));
+
+	// File Menu
+	QMenu *const menu_file = new QMenu;
+	menu_file->setTitle(QApplication::translate(
+		"MainWindow", "&File", 0, QApplication::UnicodeUTF8));
+
+	QAction *const action_open = new QAction(this);
+	action_open->setText(QApplication::translate(
+		"MainWindow", "&Open...", 0, QApplication::UnicodeUTF8));
+	action_open->setIcon(QIcon::fromTheme("document-open",
+		QIcon(":/icons/document-open.png")));
+	action_open->setObjectName(QString::fromUtf8("actionOpen"));
+	menu_file->addAction(action_open);
+
+	QAction *const action_save_as = new QAction(this);
+	action_save_as->setText(QApplication::translate(
+		"MainWindow", "&Save As...", 0, QApplication::UnicodeUTF8));
+	action_save_as->setIcon(QIcon::fromTheme("document-save-as",
+		QIcon(":/icons/document-save-as.png")));
+	action_save_as->setObjectName(QString::fromUtf8("actionSaveAs"));
+	menu_file->addAction(action_save_as);
+
+	menu_file->addSeparator();
+
+	QAction *const action_connect = new QAction(this);
+	action_connect->setText(QApplication::translate(
+		"MainWindow", "&Connect to Device...", 0,
+		QApplication::UnicodeUTF8));
+	action_connect->setObjectName(QString::fromUtf8("actionConnect"));
+	menu_file->addAction(action_connect);
+
+	menu_file->addSeparator();
+
+	QAction *action_quit = new QAction(this);
+	action_quit->setText(QApplication::translate(
+		"MainWindow", "&Quit", 0, QApplication::UnicodeUTF8));
+	action_quit->setIcon(QIcon::fromTheme("application-exit",
+		QIcon(":/icons/application-exit.png")));
+	action_quit->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q));
+	action_quit->setObjectName(QString::fromUtf8("actionQuit"));
+	menu_file->addAction(action_quit);
+
+	// View Menu
+	QMenu *menu_view = new QMenu;
+	menu_view->setTitle(QApplication::translate(
+		"MainWindow", "&View", 0, QApplication::UnicodeUTF8));
+
+	QAction *const action_view_zoom_in = new QAction(this);
+	action_view_zoom_in->setText(QApplication::translate(
+		"MainWindow", "Zoom &In", 0, QApplication::UnicodeUTF8));
+	action_view_zoom_in->setIcon(QIcon::fromTheme("zoom-in",
+		QIcon(":/icons/zoom-in.png")));
+	// simply using Qt::Key_Plus shows no + in the menu
+	action_view_zoom_in->setShortcut(QKeySequence::ZoomIn);
+	action_view_zoom_in->setObjectName(
+		QString::fromUtf8("actionViewZoomIn"));
+	menu_view->addAction(action_view_zoom_in);
+
+	QAction *const action_view_zoom_out = new QAction(this);
+	action_view_zoom_out->setText(QApplication::translate(
+		"MainWindow", "Zoom &Out", 0, QApplication::UnicodeUTF8));
+	action_view_zoom_out->setIcon(QIcon::fromTheme("zoom-out",
+		QIcon(":/icons/zoom-out.png")));
+	action_view_zoom_out->setShortcut(QKeySequence::ZoomOut);
+	action_view_zoom_out->setObjectName(
+		QString::fromUtf8("actionViewZoomOut"));
+	menu_view->addAction(action_view_zoom_out);
+
+	QAction *const action_view_zoom_fit = new QAction(this);
+	action_view_zoom_fit->setText(QApplication::translate(
+		"MainWindow", "Zoom to &Fit", 0, QApplication::UnicodeUTF8));
+	action_view_zoom_fit->setIcon(QIcon::fromTheme("zoom-fit",
+		QIcon(":/icons/zoom-fit.png")));
+	action_view_zoom_fit->setShortcut(QKeySequence(Qt::Key_F));
+	action_view_zoom_fit->setObjectName(
+		QString::fromUtf8("actionViewZoomFit"));
+	menu_view->addAction(action_view_zoom_fit);
+
+	QAction *const action_view_zoom_one_to_one = new QAction(this);
+	action_view_zoom_one_to_one->setText(QApplication::translate(
+		"MainWindow", "Zoom to &One-to-One", 0,
+			QApplication::UnicodeUTF8));
+	action_view_zoom_one_to_one->setIcon(QIcon::fromTheme("zoom-original",
+		QIcon(":/icons/zoom-original.png")));
+	action_view_zoom_one_to_one->setShortcut(QKeySequence(Qt::Key_O));
+	action_view_zoom_one_to_one->setObjectName(
+		QString::fromUtf8("actionViewZoomOneToOne"));
+	menu_view->addAction(action_view_zoom_one_to_one);
+
+	menu_view->addSeparator();
+
+	QAction *action_view_show_cursors = new QAction(this);
+	action_view_show_cursors->setCheckable(true);
+	action_view_show_cursors->setChecked(_view->cursors_shown());
+	action_view_show_cursors->setShortcut(QKeySequence(Qt::Key_C));
+	action_view_show_cursors->setObjectName(
+		QString::fromUtf8("actionViewShowCursors"));
+	action_view_show_cursors->setText(QApplication::translate(
+		"MainWindow", "Show &Cursors", 0, QApplication::UnicodeUTF8));
+	menu_view->addAction(action_view_show_cursors);
+
+	// Decoders Menu
+#ifdef ENABLE_DECODE
+	QMenu *const menu_decoders = new QMenu;
+	menu_decoders->setTitle(QApplication::translate(
+		"MainWindow", "&Decoders", 0, QApplication::UnicodeUTF8));
+
+	pv::widgets::DecoderMenu *const menu_decoders_add =
+		new pv::widgets::DecoderMenu(menu_decoders, true);
+	menu_decoders_add->setTitle(QApplication::translate(
+		"MainWindow", "&Add", 0, QApplication::UnicodeUTF8));
+	connect(menu_decoders_add, SIGNAL(decoder_selected(srd_decoder*)),
+		this, SLOT(add_decoder(srd_decoder*)));
+
+	menu_decoders->addMenu(menu_decoders_add);
+#endif
+
+	// Help Menu
+	QMenu *const menu_help = new QMenu;
+	menu_help->setTitle(QApplication::translate(
+		"MainWindow", "&Help", 0, QApplication::UnicodeUTF8));
+
+	QAction *const action_about = new QAction(this);
+	action_about->setObjectName(QString::fromUtf8("actionAbout"));
+	action_about->setText(QApplication::translate(
+		"MainWindow", "&About...", 0, QApplication::UnicodeUTF8));
+	menu_help->addAction(action_about);
+
+	menu_bar->addAction(menu_file->menuAction());
+	menu_bar->addAction(menu_view->menuAction());
+#ifdef ENABLE_DECODE
+	menu_bar->addAction(menu_decoders->menuAction());
+#endif
+	menu_bar->addAction(menu_help->menuAction());
+
+	setMenuBar(menu_bar);
+	QMetaObject::connectSlotsByName(this);
+
+	// Setup the toolbar
+	QToolBar *const toolbar = new QToolBar(tr("Main Toolbar"), this);
+	toolbar->addAction(action_open);
+	toolbar->addSeparator();
+	toolbar->addAction(action_view_zoom_in);
+	toolbar->addAction(action_view_zoom_out);
+	toolbar->addAction(action_view_zoom_fit);
+	addToolBar(toolbar);
+
+	// Setup the sampling bar
+	_sampling_bar = new toolbars::SamplingBar(_session, this);
+
+	// Populate the device list and select the initially selected device
+	update_device_list();
+
+	connect(_sampling_bar, SIGNAL(run_stop()), this,
+		SLOT(run_stop()));
+	addToolBar(_sampling_bar);
+
+	// Set the title
+	setWindowTitle(QApplication::translate("MainWindow", "PulseView", 0,
+		QApplication::UnicodeUTF8));
+
+	// Setup _session events
+	connect(&_session, SIGNAL(capture_state_changed(int)), this,
+		SLOT(capture_state_changed(int)));
+
+}
+
+void MainWindow::session_error(
+	const QString text, const QString info_text)
+{
+	QMetaObject::invokeMethod(this, "show_session_error",
+		Qt::QueuedConnection, Q_ARG(QString, text),
+		Q_ARG(QString, info_text));
+}
+
+void MainWindow::update_device_list()
+{
+	assert(_sampling_bar);
+
+	shared_ptr<pv::device::DevInst> selected_device = _session.get_device();
+	list< shared_ptr<device::DevInst> > devices;
+	std::copy(_device_manager.devices().begin(),
+		_device_manager.devices().end(), std::back_inserter(devices));
+
+	if (std::find(devices.begin(), devices.end(), selected_device) ==
+		devices.end())
+		devices.push_back(selected_device);
+	assert(selected_device);
+
+	_sampling_bar->set_device_list(devices, selected_device);
+}
+
+void MainWindow::load_file(QString file_name)
+{
+	const QString errorMessage(
+		QString("Failed to load file %1").arg(file_name));
+	const QString infoMessage;
+
+	try {
+		_session.set_file(file_name.toStdString());
+	} catch(QString e) {
+		show_session_error(tr("Failed to load ") + file_name, e);
+		_session.set_default_device();
+		update_device_list();
+		return;
+	}
+
+	update_device_list();
+
+	_session.start_capture(boost::bind(&MainWindow::session_error, this,
+		errorMessage, infoMessage));
+}
+
+void MainWindow::show_session_error(
+	const QString text, const QString info_text)
+{
+	QMessageBox msg(this);
+	msg.setText(text);
+	msg.setInformativeText(info_text);
+	msg.setStandardButtons(QMessageBox::Ok);
+	msg.setIcon(QMessageBox::Warning);
+	msg.exec();
+}
+
+void MainWindow::on_actionOpen_triggered()
+{
+	// Show the dialog
+	const QString file_name = QFileDialog::getOpenFileName(
+		this, tr("Open File"), "", tr(
+			"Sigrok Sessions (*.sr);;"
+			"All Files (*.*)"));
+	if (!file_name.isEmpty())
+		load_file(file_name);
+}
+
+void MainWindow::on_actionSaveAs_triggered()
+{
+	using pv::dialogs::StoreProgress;
+
+	// Stop any currently running capture session
+	_session.stop_capture();
+
+	// Show the dialog
+	const QString file_name = QFileDialog::getSaveFileName(
+		this, tr("Save File"), "", tr("Sigrok Sessions (*.sr)"));
+
+	if (file_name.isEmpty())
+		return;
+
+	StoreProgress *dlg = new StoreProgress(file_name, _session, this);
+	dlg->run();
+}
+
+void MainWindow::on_actionConnect_triggered()
+{
+	// Stop any currently running capture session
+	_session.stop_capture();
+
+	dialogs::Connect dlg(this, _device_manager);
+
+	// If the user selected a device, select it in the device list. Select the
+	// current device otherwise.
+	if (dlg.exec())
+		_session.set_device(dlg.get_selected_device());
+
+	update_device_list();
+}
+
+void MainWindow::on_actionQuit_triggered()
+{
+	close();
+}
+
+void MainWindow::on_actionViewZoomIn_triggered()
+{
+	_view->zoom(1);
+}
+
+void MainWindow::on_actionViewZoomOut_triggered()
+{
+	_view->zoom(-1);
+}
+
+void MainWindow::on_actionViewZoomFit_triggered()
+{
+	_view->zoom_fit();
+}
+
+void MainWindow::on_actionViewZoomOneToOne_triggered()
+{
+	_view->zoom_one_to_one();
+}
+
+void MainWindow::on_actionViewShowCursors_triggered()
+{
+	assert(_view);
+
+	const bool show = !_view->cursors_shown();
+	if(show)
+		_view->centre_cursors();
+
+	_view->show_cursors(show);
+}
+
+void MainWindow::on_actionAbout_triggered()
+{
+	dialogs::About dlg(this);
+	dlg.exec();
+}
+
+void MainWindow::add_decoder(srd_decoder *decoder)
+{
+#ifdef ENABLE_DECODE
+	assert(decoder);
+	_session.add_decoder(decoder);
+#else
+	(void)decoder;
+#endif
+}
+
+void MainWindow::run_stop()
+{
+	switch(_session.get_capture_state()) {
+	case SigSession::Stopped:
+		_session.start_capture(
+				boost::bind(&MainWindow::session_error, this,
+				QString("Capture failed"), _1));
+		break;
+
+	case SigSession::AwaitingTrigger:
+	case SigSession::Running:
+		_session.stop_capture();
+		break;
+	}
+}
+
+void MainWindow::capture_state_changed(int state)
+{
+	_sampling_bar->set_capture_state((pv::SigSession::capture_state)state);
+}
+
+} // namespace pv
diff --git a/pv/mainwindow.h b/pv/mainwindow.h
new file mode 100644
index 0000000..1f9dd52
--- /dev/null
+++ b/pv/mainwindow.h
@@ -0,0 +1,122 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_MAINWINDOW_H
+#define PULSEVIEW_PV_MAINWINDOW_H
+
+#include <list>
+
+#include <boost/weak_ptr.hpp>
+
+#include <QMainWindow>
+
+#include "sigsession.h"
+
+struct srd_decoder;
+
+class QVBoxLayout;
+
+namespace pv {
+
+class DeviceManager;
+
+namespace device {
+class DevInst;
+}
+
+namespace toolbars {
+class ContextBar;
+class SamplingBar;
+}
+
+namespace view {
+class View;
+}
+
+namespace widgets {
+class DecoderMenu;
+}
+
+class MainWindow : public QMainWindow
+{
+	Q_OBJECT
+
+public:
+	explicit MainWindow(DeviceManager &device_manager,
+		const char *open_file_name = NULL,
+		QWidget *parent = 0);
+
+private:
+	void setup_ui();
+
+	void session_error(const QString text, const QString info_text);
+
+	/**
+	 * Updates the device list in the sampling bar
+	 */
+	void update_device_list();
+
+private slots:
+	void load_file(QString file_name);
+
+
+	void show_session_error(
+		const QString text, const QString info_text);
+
+	void on_actionOpen_triggered();
+	void on_actionSaveAs_triggered();
+	void on_actionQuit_triggered();
+
+	void on_actionConnect_triggered();
+
+	void on_actionViewZoomIn_triggered();
+
+	void on_actionViewZoomOut_triggered();
+
+	void on_actionViewZoomFit_triggered();
+
+	void on_actionViewZoomOneToOne_triggered();
+
+	void on_actionViewShowCursors_triggered();
+
+	void on_actionAbout_triggered();
+
+	void add_decoder(srd_decoder *decoder);
+
+	void run_stop();
+
+	void capture_state_changed(int state);
+
+private:
+	DeviceManager &_device_manager;
+
+	SigSession _session;
+
+	pv::view::View *_view;
+
+	QWidget *_central_widget;
+	QVBoxLayout *_vertical_layout;
+
+	toolbars::SamplingBar *_sampling_bar;
+};
+
+} // namespace pv
+
+#endif // PULSEVIEW_PV_MAINWINDOW_H
diff --git a/pv/popups/deviceoptions.cpp b/pv/popups/deviceoptions.cpp
new file mode 100644
index 0000000..13055fc
--- /dev/null
+++ b/pv/popups/deviceoptions.cpp
@@ -0,0 +1,53 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "deviceoptions.h"
+
+#include <boost/foreach.hpp>
+
+#include <QFormLayout>
+#include <QListWidget>
+
+#include <pv/prop/property.h>
+
+using boost::shared_ptr;
+
+namespace pv {
+namespace popups {
+
+DeviceOptions::DeviceOptions(shared_ptr<device::DevInst> dev_inst,
+	QWidget *parent) :
+	Popup(parent),
+	_dev_inst(dev_inst),
+	_layout(this),
+	_binding(dev_inst)
+{
+	setLayout(&_layout);
+
+	_layout.addWidget(_binding.get_property_form(this, true));
+}
+
+pv::prop::binding::DeviceOptions& DeviceOptions::binding()
+{
+	return _binding;
+}
+
+} // namespace popups
+} // namespace pv
diff --git a/pv/popups/deviceoptions.h b/pv/popups/deviceoptions.h
new file mode 100644
index 0000000..945959b
--- /dev/null
+++ b/pv/popups/deviceoptions.h
@@ -0,0 +1,54 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_POPUPS_DEVICEOPTIONS_H
+#define PULSEVIEW_PV_POPUPS_DEVICEOPTIONS_H
+
+#include <QGroupBox>
+#include <QVBoxLayout>
+
+#include <pv/prop/binding/deviceoptions.h>
+#include <pv/widgets/popup.h>
+
+namespace pv {
+namespace popups {
+
+class DeviceOptions : public pv::widgets::Popup
+{
+	Q_OBJECT
+
+public:
+	DeviceOptions(boost::shared_ptr<device::DevInst> dev_inst,
+		QWidget *parent);
+
+	pv::prop::binding::DeviceOptions& binding();
+
+private:
+	boost::shared_ptr<device::DevInst> _dev_inst;
+
+	QVBoxLayout _layout;
+
+	pv::prop::binding::DeviceOptions _binding;
+};
+
+} // namespace popups
+} // namespace pv
+
+#endif // PULSEVIEW_PV_POPUPS_DEVICEOPTIONS_H
diff --git a/pv/popups/probes.cpp b/pv/popups/probes.cpp
new file mode 100644
index 0000000..ca1a2fb
--- /dev/null
+++ b/pv/popups/probes.cpp
@@ -0,0 +1,258 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <map>
+
+#include <boost/foreach.hpp>
+
+#include <QCheckBox>
+#include <QFormLayout>
+#include <QGridLayout>
+#include <QLabel>
+
+#include "probes.h"
+
+#include <pv/device/devinst.h>
+#include <pv/prop/binding/deviceoptions.h>
+#include <pv/sigsession.h>
+#include <pv/view/signal.h>
+
+using namespace Qt;
+
+using boost::shared_ptr;
+using std::map;
+using std::set;
+using std::vector;
+
+using pv::view::Signal;
+
+namespace pv {
+namespace popups {
+
+Probes::Probes(SigSession &session, QWidget *parent) :
+	Popup(parent),
+	_session(session),
+	_updating_probes(false),
+	_enable_all_probes(tr("Enable All"), this),
+	_disable_all_probes(tr("Disable All"), this),
+	_check_box_mapper(this)
+{
+	// Create the layout
+	setLayout(&_layout);
+
+	shared_ptr<device::DevInst> dev_inst = _session.get_device();
+	assert(dev_inst);
+	const sr_dev_inst *const sdi = dev_inst->dev_inst();
+	assert(sdi);
+
+	// Collect a set of signals
+	map<const sr_channel*, shared_ptr<Signal> > signal_map;
+	const vector< shared_ptr<Signal> > sigs = _session.get_signals();
+	BOOST_FOREACH(const shared_ptr<Signal> &sig, sigs)
+		signal_map[sig->probe()] = sig;
+
+	// Populate channel groups
+	for (const GSList *g = sdi->channel_groups; g; g = g->next)
+	{
+		const sr_channel_group *const group =
+			(const sr_channel_group*)g->data;
+		assert(group);
+
+		// Make a set of signals, and removed this signals from the
+		// signal map.
+		vector< shared_ptr<Signal> > group_sigs;
+		for (const GSList *p = group->channels; p; p = p->next)
+		{
+			const sr_channel *const probe = (const sr_channel*)p->data;
+			assert(probe);
+
+			const map<const sr_channel*, shared_ptr<Signal> >::
+				iterator iter = signal_map.find(probe);
+			assert(iter != signal_map.end());
+
+			group_sigs.push_back((*iter).second);
+			signal_map.erase(iter);
+		}
+
+		populate_group(group, group_sigs);
+	}
+
+	// Make a vector of the remaining probes
+	vector< shared_ptr<Signal> > global_sigs;
+	for (const GSList *p = sdi->channels; p; p = p->next)
+	{
+		const sr_channel *const probe = (const sr_channel*)p->data;
+		assert(probe);
+
+		const map<const sr_channel*, shared_ptr<Signal> >::
+			const_iterator iter = signal_map.find(probe);
+		if (iter != signal_map.end())
+			global_sigs.push_back((*iter).second);
+	}
+
+	// Create a group
+	populate_group(NULL, global_sigs);
+
+	// Create the enable/disable all buttons
+	connect(&_enable_all_probes, SIGNAL(clicked()),
+		this, SLOT(enable_all_probes()));
+	connect(&_disable_all_probes, SIGNAL(clicked()),
+		this, SLOT(disable_all_probes()));
+
+	_enable_all_probes.setFlat(true);
+	_disable_all_probes.setFlat(true);
+
+	_buttons_bar.addWidget(&_enable_all_probes);
+	_buttons_bar.addWidget(&_disable_all_probes);
+	_buttons_bar.addStretch(1);
+
+	_layout.addRow(&_buttons_bar);
+
+	// Connect the check-box signal mapper
+	connect(&_check_box_mapper, SIGNAL(mapped(QWidget*)),
+		this, SLOT(on_probe_checked(QWidget*)));
+}
+
+void Probes::set_all_probes(bool set)
+{
+	_updating_probes = true;
+
+	for (map<QCheckBox*, shared_ptr<Signal> >::const_iterator i =
+		_check_box_signal_map.begin();
+		i != _check_box_signal_map.end(); i++)
+	{
+		const shared_ptr<Signal> sig = (*i).second;
+		assert(sig);
+
+		sig->enable(set);
+		(*i).first->setChecked(set);
+	}
+
+	_updating_probes = false;
+}
+
+void Probes::populate_group(const sr_channel_group *group,
+	const vector< shared_ptr<pv::view::Signal> > sigs)
+{
+	using pv::prop::binding::DeviceOptions;
+
+	// Only bind options if this is a group. We don't do it for general
+	// options, because these properties are shown in the device config
+	// popup.
+	shared_ptr<DeviceOptions> binding;
+	if (group)
+		binding = shared_ptr<DeviceOptions>(new DeviceOptions(
+			_session.get_device(), group));
+
+	// Create a title if the group is going to have any content
+	if ((!sigs.empty() || (binding && !binding->properties().empty())) &&
+		group && group->name)
+		_layout.addRow(new QLabel(
+			QString("<h3>%1</h3>").arg(group->name)));
+
+	// Create the channel group grid
+	QGridLayout *const probe_grid =
+		create_channel_group_grid(sigs);
+	_layout.addRow(probe_grid);
+
+	// Create the channel group options
+	if (binding)
+	{
+		binding->add_properties_to_form(&_layout, true);
+		_group_bindings.push_back(binding);
+	}
+}
+
+QGridLayout* Probes::create_channel_group_grid(
+	const vector< shared_ptr<pv::view::Signal> > sigs)
+{
+	int row = 0, col = 0;
+	QGridLayout *const grid = new QGridLayout();
+
+	BOOST_FOREACH(const shared_ptr<pv::view::Signal>& sig, sigs)
+	{
+		assert(sig);
+
+		QCheckBox *const checkbox = new QCheckBox(sig->get_name());
+		_check_box_mapper.setMapping(checkbox, checkbox);
+		connect(checkbox, SIGNAL(toggled(bool)),
+			&_check_box_mapper, SLOT(map()));
+
+		grid->addWidget(checkbox, row, col);
+
+		_check_box_signal_map[checkbox] = sig;
+
+		if(++col >= 8)
+			col = 0, row++;
+	}
+
+	return grid;
+}
+
+void Probes::showEvent(QShowEvent *e)
+{
+	pv::widgets::Popup::showEvent(e);
+
+	_updating_probes = true;
+
+	for (map<QCheckBox*, shared_ptr<Signal> >::const_iterator i =
+		_check_box_signal_map.begin();
+		i != _check_box_signal_map.end(); i++)
+	{
+		const shared_ptr<Signal> sig = (*i).second;
+		assert(sig);
+
+		(*i).first->setChecked(sig->enabled());
+	}
+
+	_updating_probes = false;
+}
+
+void Probes::on_probe_checked(QWidget *widget)
+{
+	if (_updating_probes)
+		return;
+
+	QCheckBox *const check_box = (QCheckBox*)widget;
+	assert(check_box);
+
+	// Look up the signal of this check-box
+	map< QCheckBox*, shared_ptr<Signal> >::const_iterator iter =
+		_check_box_signal_map.find((QCheckBox*)check_box);
+	assert(iter != _check_box_signal_map.end());
+
+	const shared_ptr<pv::view::Signal> s = (*iter).second;
+	assert(s);
+
+	s->enable(check_box->isChecked());
+}
+
+void Probes::enable_all_probes()
+{
+	set_all_probes(true);
+}
+
+void Probes::disable_all_probes()
+{
+	set_all_probes(false);
+}
+
+} // popups
+} // pv
diff --git a/pv/popups/probes.h b/pv/popups/probes.h
new file mode 100644
index 0000000..dbf90cd
--- /dev/null
+++ b/pv/popups/probes.h
@@ -0,0 +1,104 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_POPUPS_PROBES_H
+#define PULSEVIEW_PV_POPUPS_PROBES_H
+
+#include <map>
+#include <vector>
+
+#include <boost/shared_ptr.hpp>
+
+#include <QFormLayout>
+#include <QHBoxLayout>
+#include <QPushButton>
+#include <QSignalMapper>
+
+#include <pv/widgets/popup.h>
+
+struct sr_channel_group;
+
+class QCheckBox;
+class QGridLayout;
+
+namespace pv {
+
+class SigSession;
+
+namespace prop {
+namespace binding {
+class DeviceOptions;
+}
+}
+
+namespace view {
+class Signal;
+}
+
+namespace popups {
+
+class Probes : public pv::widgets::Popup
+{
+	Q_OBJECT
+
+public:
+	Probes(SigSession &_session, QWidget *parent);
+
+private:
+	void set_all_probes(bool set);
+
+	void populate_group(const sr_channel_group *group,
+		const std::vector< boost::shared_ptr<pv::view::Signal> > sigs);
+
+	QGridLayout* create_channel_group_grid(
+		const std::vector< boost::shared_ptr<pv::view::Signal> > sigs);
+
+private:
+	void showEvent(QShowEvent *e);
+
+private slots:
+	void on_probe_checked(QWidget *widget);
+
+	void enable_all_probes();
+	void disable_all_probes();
+
+private:
+	pv::SigSession &_session;
+
+	QFormLayout _layout;
+
+	bool _updating_probes;
+
+	std::vector< boost::shared_ptr<pv::prop::binding::DeviceOptions> >
+		 _group_bindings;
+	std::map< QCheckBox*, boost::shared_ptr<pv::view::Signal> >
+		_check_box_signal_map;
+
+	QHBoxLayout _buttons_bar;
+	QPushButton _enable_all_probes;
+	QPushButton _disable_all_probes;
+
+	QSignalMapper _check_box_mapper;
+};
+
+} // popups
+} // pv
+
+#endif // PULSEVIEW_PV_POPUPS_PROBES_H
diff --git a/pv/prop/binding/binding.cpp b/pv/prop/binding/binding.cpp
new file mode 100644
index 0000000..3a6c73f
--- /dev/null
+++ b/pv/prop/binding/binding.cpp
@@ -0,0 +1,94 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <boost/foreach.hpp>
+
+#include <QFormLayout>
+
+#include <pv/prop/property.h>
+
+#include "binding.h"
+
+using boost::shared_ptr;
+
+namespace pv {
+namespace prop {
+namespace binding {
+
+const std::vector< boost::shared_ptr<Property> >& Binding::properties()
+{
+	return _properties;
+}
+
+void Binding::commit()
+{
+	BOOST_FOREACH(shared_ptr<pv::prop::Property> p, _properties) {
+		assert(p);
+		p->commit();
+	}
+}
+
+void Binding::add_properties_to_form(QFormLayout *layout,
+	bool auto_commit) const
+{
+	assert(layout);
+
+	BOOST_FOREACH(shared_ptr<pv::prop::Property> p, _properties)
+	{
+		assert(p);
+
+		QWidget *const widget = p->get_widget(layout->parentWidget(),
+			auto_commit);
+		if (p->labeled_widget())
+			layout->addRow(widget);
+		else
+			layout->addRow(p->name(), widget);
+	}
+}
+
+QWidget* Binding::get_property_form(QWidget *parent,
+	bool auto_commit) const
+{
+	QWidget *const form = new QWidget(parent);
+	QFormLayout *const layout = new QFormLayout(form);
+	form->setLayout(layout);
+	add_properties_to_form(layout, auto_commit);
+	return form;
+}
+
+QString Binding::print_gvariant(GVariant *const gvar)
+{
+	QString s;
+
+	if (g_variant_is_of_type(gvar, G_VARIANT_TYPE("s")))
+		s = QString::fromUtf8(g_variant_get_string(gvar, NULL));
+	else
+	{
+		gchar *const text = g_variant_print(gvar, FALSE);
+		s = QString::fromUtf8(text);
+		g_free(text);
+	}
+
+	return s;
+}
+
+} // binding
+} // prop
+} // pv
diff --git a/pv/prop/binding/binding.h b/pv/prop/binding/binding.h
new file mode 100644
index 0000000..89c1acb
--- /dev/null
+++ b/pv/prop/binding/binding.h
@@ -0,0 +1,64 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_PROP_BINDING_BINDING_H
+#define PULSEVIEW_PV_PROP_BINDING_BINDING_H
+
+#include <glib.h>
+
+#include <vector>
+#include <boost/shared_ptr.hpp>
+
+#include <QString>
+
+class QFormLayout;
+class QWidget;
+
+namespace pv {
+namespace prop {
+
+class Property;
+
+namespace binding {
+
+class Binding
+{
+public:
+	const std::vector< boost::shared_ptr<Property> >& properties();
+
+	void commit();
+
+	void add_properties_to_form(QFormLayout *layout,
+		bool auto_commit = false) const;
+
+	QWidget* get_property_form(QWidget *parent,
+		bool auto_commit = false) const;
+
+	static QString print_gvariant(GVariant *const gvar);
+
+protected:
+	std::vector< boost::shared_ptr<Property> > _properties;
+};
+
+} // binding
+} // prop
+} // pv
+
+#endif // PULSEVIEW_PV_PROP_BINDING_BINDING_H
diff --git a/pv/prop/binding/decoderoptions.cpp b/pv/prop/binding/decoderoptions.cpp
new file mode 100644
index 0000000..063daf8
--- /dev/null
+++ b/pv/prop/binding/decoderoptions.cpp
@@ -0,0 +1,151 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <libsigrokdecode/libsigrokdecode.h>
+
+#include "decoderoptions.h"
+
+#include <boost/bind.hpp>
+#include <boost/foreach.hpp>
+#include <boost/none_t.hpp>
+
+#include <pv/data/decoderstack.h>
+#include <pv/data/decode/decoder.h>
+#include <pv/prop/double.h>
+#include <pv/prop/enum.h>
+#include <pv/prop/int.h>
+#include <pv/prop/string.h>
+
+using boost::bind;
+using boost::none;
+using boost::shared_ptr;
+using std::make_pair;
+using std::map;
+using std::pair;
+using std::string;
+using std::vector;
+
+namespace pv {
+namespace prop {
+namespace binding {
+
+DecoderOptions::DecoderOptions(
+	shared_ptr<pv::data::DecoderStack> decoder_stack,
+	shared_ptr<data::decode::Decoder> decoder) :
+	_decoder_stack(decoder_stack),
+	_decoder(decoder)
+{
+	assert(_decoder);
+
+	const srd_decoder *const dec = _decoder->decoder();
+	assert(dec);
+
+	for (GSList *l = dec->options; l; l = l->next)
+	{
+		const srd_decoder_option *const opt =
+			(srd_decoder_option*)l->data;
+
+		const QString name = QString::fromUtf8(opt->desc);
+
+		const Property::Getter getter = bind(
+			&DecoderOptions::getter, this, opt->id);
+		const Property::Setter setter = bind(
+			&DecoderOptions::setter, this, opt->id, _1);
+
+		shared_ptr<Property> prop;
+
+		if (opt->values)
+			prop = bind_enum(name, opt, getter, setter);
+		else if (g_variant_is_of_type(opt->def, G_VARIANT_TYPE("d")))
+			prop = shared_ptr<Property>(new Double(name, 2, "",
+				none, none, getter, setter));
+		else if (g_variant_is_of_type(opt->def, G_VARIANT_TYPE("x")))
+			prop = shared_ptr<Property>(
+				new Int(name, "", none, getter, setter));
+		else if (g_variant_is_of_type(opt->def, G_VARIANT_TYPE("s")))
+			prop = shared_ptr<Property>(
+				new String(name, getter, setter));
+		else
+			continue;
+
+		_properties.push_back(prop);
+	}
+}
+
+shared_ptr<Property> DecoderOptions::bind_enum(
+	const QString &name, const srd_decoder_option *option,
+	Property::Getter getter, Property::Setter setter)
+{
+	vector< pair<GVariant*, QString> > values;
+	for (GSList *l = option->values; l; l = l->next) {
+		GVariant *const var = (GVariant*)l->data;
+		assert(var);
+		values.push_back(make_pair(var, print_gvariant(var)));
+	}
+
+	return shared_ptr<Property>(new Enum(name, values, getter, setter));
+}
+
+GVariant* DecoderOptions::getter(const char *id)
+{
+	GVariant *val = NULL;
+
+	assert(_decoder);
+
+	// Get the value from the hash table if it is already present
+	const map<string, GVariant*>& options = _decoder->options();
+	map<string, GVariant*>::const_iterator iter = options.find(id);
+
+	if (iter != options.end())
+		val = (*iter).second;
+	else
+	{
+		assert(_decoder->decoder());
+
+		// Get the default value if not
+		for (GSList *l = _decoder->decoder()->options; l; l = l->next)
+		{
+			const srd_decoder_option *const opt =
+				(srd_decoder_option*)l->data;
+			if (strcmp(opt->id, id) == 0) {
+				val = opt->def;
+				break;
+			}
+		}
+	}
+
+	if (val)
+		g_variant_ref(val);
+
+	return val;
+}
+
+void DecoderOptions::setter(const char *id, GVariant *value)
+{
+	assert(_decoder);
+	_decoder->set_option(id, value);
+
+	assert(_decoder_stack);
+	_decoder_stack->begin_decode();
+}
+
+} // binding
+} // prop
+} // pv
diff --git a/pv/prop/binding/decoderoptions.h b/pv/prop/binding/decoderoptions.h
new file mode 100644
index 0000000..6dec9c2
--- /dev/null
+++ b/pv/prop/binding/decoderoptions.h
@@ -0,0 +1,66 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_PROP_BINDING_DECODEROPTIONS_H
+#define PULSEVIEW_PV_PROP_BINDING_DECODEROPTIONS_H
+
+#include "binding.h"
+
+#include <pv/prop/property.h>
+
+struct srd_decoder_option;
+
+namespace pv {
+
+namespace data {
+class DecoderStack;
+namespace decode {
+class Decoder;
+}
+}
+
+namespace prop {
+namespace binding {
+
+class DecoderOptions : public Binding
+{
+public:
+	DecoderOptions(boost::shared_ptr<pv::data::DecoderStack> decoder_stack,
+		boost::shared_ptr<pv::data::decode::Decoder> decoder);
+
+private:
+	static boost::shared_ptr<Property> bind_enum(const QString &name,
+		const srd_decoder_option *option,
+		Property::Getter getter, Property::Setter setter);
+
+	GVariant* getter(const char *id);
+
+	void setter(const char *id, GVariant *value);
+
+private:
+	boost::shared_ptr<pv::data::DecoderStack> _decoder_stack;
+	boost::shared_ptr<pv::data::decode::Decoder> _decoder;
+};
+
+} // binding
+} // prop
+} // pv
+
+#endif // PULSEVIEW_PV_PROP_BINDING_DECODEROPTIONS_H
diff --git a/pv/prop/binding/deviceoptions.cpp b/pv/prop/binding/deviceoptions.cpp
new file mode 100644
index 0000000..2dc237a
--- /dev/null
+++ b/pv/prop/binding/deviceoptions.cpp
@@ -0,0 +1,192 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <boost/bind.hpp>
+
+#include <stdint.h>
+
+#include <QDebug>
+
+#include "deviceoptions.h"
+
+#include <pv/device/devinst.h>
+#include <pv/prop/bool.h>
+#include <pv/prop/double.h>
+#include <pv/prop/enum.h>
+#include <pv/prop/int.h>
+
+#include <libsigrok/libsigrok.h>
+
+using boost::bind;
+using boost::function;
+using boost::optional;
+using boost::shared_ptr;
+using std::make_pair;
+using std::pair;
+using std::string;
+using std::vector;
+
+namespace pv {
+namespace prop {
+namespace binding {
+
+DeviceOptions::DeviceOptions(shared_ptr<pv::device::DevInst> dev_inst,
+	const sr_channel_group *group) :
+	_dev_inst(dev_inst),
+	_group(group)
+{
+	assert(dev_inst);
+
+	GVariant *gvar_opts;
+	gsize num_opts;
+
+	if (!(gvar_opts = dev_inst->list_config(group, SR_CONF_DEVICE_OPTIONS)))
+		/* Driver supports no device instance options. */
+		return;
+
+	const int *const options = (const int32_t *)g_variant_get_fixed_array(
+		gvar_opts, &num_opts, sizeof(int32_t));
+	for (unsigned int i = 0; i < num_opts; i++) {
+		const struct sr_config_info *const info =
+			sr_config_info_get(options[i]);
+
+		if (!info)
+			continue;
+
+		const int key = info->key;
+		GVariant *const gvar_list = dev_inst->list_config(group, key);
+
+		const QString name = QString::fromUtf8(info->name);
+
+		switch(key)
+		{
+		case SR_CONF_SAMPLERATE:
+			// Sample rate values are not bound because they are shown
+			// in the SamplingBar
+			break;
+
+		case SR_CONF_CAPTURE_RATIO:
+			bind_int(name, key, "%", pair<int64_t, int64_t>(0, 100));
+			break;
+
+		case SR_CONF_PATTERN_MODE:
+		case SR_CONF_BUFFERSIZE:
+		case SR_CONF_TRIGGER_SOURCE:
+		case SR_CONF_TRIGGER_SLOPE:
+		case SR_CONF_FILTER:
+		case SR_CONF_COUPLING:
+		case SR_CONF_CLOCK_EDGE:
+			bind_enum(name, key, gvar_list);
+			break;
+
+		case SR_CONF_EXTERNAL_CLOCK:
+		case SR_CONF_RLE:
+			bind_bool(name, key);
+			break;
+
+		case SR_CONF_TIMEBASE:
+			bind_enum(name, key, gvar_list, print_timebase);
+			break;
+
+		case SR_CONF_VDIV:
+			bind_enum(name, key, gvar_list, print_vdiv);
+			break;
+
+		case SR_CONF_VOLTAGE_THRESHOLD:
+			bind_enum(name, key, gvar_list, print_voltage_threshold);
+			break;
+		}
+
+		if (gvar_list)
+			g_variant_unref(gvar_list);
+	}
+	g_variant_unref(gvar_opts);
+}
+
+void DeviceOptions::bind_bool(const QString &name, int key)
+{
+	assert(_dev_inst);
+	_properties.push_back(shared_ptr<Property>(new Bool(name,
+		bind(&device::DevInst::get_config, _dev_inst, _group, key),
+		bind(&device::DevInst::set_config, _dev_inst,
+			_group, key, _1))));
+}
+
+void DeviceOptions::bind_enum(const QString &name, int key,
+	GVariant *const gvar_list, function<QString (GVariant*)> printer)
+{
+	GVariant *gvar;
+	GVariantIter iter;
+	vector< pair<GVariant*, QString> > values;
+
+	assert(_dev_inst);
+	if (!gvar_list) {
+		qDebug() << "Config key " << key << " was listed, but no "
+			"options were given";
+		return;
+	}
+
+	g_variant_iter_init (&iter, gvar_list);
+	while ((gvar = g_variant_iter_next_value (&iter)))
+		values.push_back(make_pair(gvar, printer(gvar)));
+
+	_properties.push_back(shared_ptr<Property>(new Enum(name, values,
+		bind(&device::DevInst::get_config, _dev_inst, _group, key),
+		bind(&device::DevInst::set_config, _dev_inst,
+			_group, key, _1))));
+}
+
+void DeviceOptions::bind_int(const QString &name, int key, QString suffix,
+	optional< std::pair<int64_t, int64_t> > range)
+{
+	assert(_dev_inst);
+
+	_properties.push_back(shared_ptr<Property>(new Int(name, suffix, range,
+		bind(&device::DevInst::get_config, _dev_inst, _group, key),
+		bind(&device::DevInst::set_config, _dev_inst, _group, key, _1))));
+}
+
+QString DeviceOptions::print_timebase(GVariant *const gvar)
+{
+	uint64_t p, q;
+	g_variant_get(gvar, "(tt)", &p, &q);
+	return QString::fromUtf8(sr_period_string(p * q));
+}
+
+QString DeviceOptions::print_vdiv(GVariant *const gvar)
+{
+	uint64_t p, q;
+	g_variant_get(gvar, "(tt)", &p, &q);
+	return QString::fromUtf8(sr_voltage_string(p, q));
+}
+
+QString DeviceOptions::print_voltage_threshold(GVariant *const gvar)
+{
+	gdouble lo, hi;
+	char buf[64];
+	g_variant_get(gvar, "(dd)", &lo, &hi);
+	snprintf(buf, sizeof(buf), "L<%.1fV H>%.1fV", lo, hi);
+	return QString::fromUtf8(buf);
+}
+
+} // binding
+} // prop
+} // pv
+
diff --git a/pv/prop/binding/deviceoptions.h b/pv/prop/binding/deviceoptions.h
new file mode 100644
index 0000000..445361f
--- /dev/null
+++ b/pv/prop/binding/deviceoptions.h
@@ -0,0 +1,72 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_PROP_BINDING_DEVICEOPTIONS_H
+#define PULSEVIEW_PV_PROP_BINDING_DEVICEOPTIONS_H
+
+#include <boost/function.hpp>
+#include <boost/optional.hpp>
+
+#include <QString>
+
+#include "binding.h"
+
+#include <glib.h>
+
+struct sr_dev_inst;
+struct sr_channel_group;
+
+namespace pv {
+
+namespace device {
+class DevInst;
+}
+
+namespace prop {
+namespace binding {
+
+class DeviceOptions : public Binding
+{
+public:
+	DeviceOptions(boost::shared_ptr<pv::device::DevInst> dev_inst,
+		const sr_channel_group *group = NULL);
+
+private:
+	void bind_bool(const QString &name, int key);
+	void bind_enum(const QString &name, int key,
+		GVariant *const gvar_list,
+		boost::function<QString (GVariant*)> printer = print_gvariant);
+	void bind_int(const QString &name, int key, QString suffix,
+		boost::optional< std::pair<int64_t, int64_t> > range);
+
+	static QString print_timebase(GVariant *const gvar);
+	static QString print_vdiv(GVariant *const gvar);
+	static QString print_voltage_threshold(GVariant *const gvar);
+
+protected:
+	boost::shared_ptr<device::DevInst> _dev_inst;
+	const sr_channel_group *const _group;
+};
+
+} // binding
+} // prop
+} // pv
+
+#endif // PULSEVIEW_PV_PROP_BINDING_DEVICEOPTIONS_H
diff --git a/pv/prop/bool.cpp b/pv/prop/bool.cpp
new file mode 100644
index 0000000..e0e052f
--- /dev/null
+++ b/pv/prop/bool.cpp
@@ -0,0 +1,83 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <assert.h>
+
+#include <QCheckBox>
+
+#include "bool.h"
+
+namespace pv {
+namespace prop {
+
+Bool::Bool(QString name, Getter getter, Setter setter) :
+	Property(name, getter, setter),
+	_check_box(NULL)
+{
+}
+
+Bool::~Bool()
+{
+}
+
+QWidget* Bool::get_widget(QWidget *parent, bool auto_commit)
+{
+	if (_check_box)
+		return _check_box;
+
+	GVariant *const value = _getter ? _getter() : NULL;
+	if (!value)
+		return NULL;
+
+	_check_box = new QCheckBox(name(), parent);
+	_check_box->setCheckState(g_variant_get_boolean(value) ?
+		Qt::Checked : Qt::Unchecked);
+	g_variant_unref(value);
+
+	if (auto_commit)
+		connect(_check_box, SIGNAL(stateChanged(int)),
+			this, SLOT(on_state_changed(int)));
+
+	return _check_box;
+}
+
+bool Bool::labeled_widget() const
+{
+	return true;
+}
+
+void Bool::commit()
+{
+	assert(_setter);
+
+	if (!_check_box)
+		return;
+
+	_setter(g_variant_new_boolean(
+		_check_box->checkState() == Qt::Checked));
+}
+
+void Bool::on_state_changed(int)
+{
+	commit();
+}
+
+} // prop
+} // pv
diff --git a/pv/prop/bool.h b/pv/prop/bool.h
new file mode 100644
index 0000000..0d8e205
--- /dev/null
+++ b/pv/prop/bool.h
@@ -0,0 +1,55 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_PROP_BOOL_H
+#define PULSEVIEW_PV_PROP_BOOL_H
+
+#include "property.h"
+
+class QCheckBox;
+
+namespace pv {
+namespace prop {
+
+class Bool : public Property
+{
+	Q_OBJECT;
+
+public:
+	Bool(QString name, Getter getter, Setter setter);
+
+	virtual ~Bool();
+
+	QWidget* get_widget(QWidget *parent, bool auto_commit);
+	bool labeled_widget() const;
+
+	void commit();
+
+private slots:
+	void on_state_changed(int);
+
+private:
+	QCheckBox *_check_box;
+};
+
+} // prop
+} // pv
+
+#endif // PULSEVIEW_PV_PROP_BOOL_H
diff --git a/pv/prop/double.cpp b/pv/prop/double.cpp
new file mode 100644
index 0000000..93b45d3
--- /dev/null
+++ b/pv/prop/double.cpp
@@ -0,0 +1,96 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <assert.h>
+
+#include <QDoubleSpinBox>
+
+#include "double.h"
+
+using boost::optional;
+using std::pair;
+
+namespace pv {
+namespace prop {
+
+Double::Double(QString name,
+	int decimals,
+	QString suffix,
+	optional< pair<double, double> > range,
+	optional<double> step,
+	Getter getter,
+	Setter setter) :
+	Property(name, getter, setter),
+	_decimals(decimals),
+	_suffix(suffix),
+	_range(range),
+	_step(step),
+	_spin_box(NULL)
+{
+}
+
+Double::~Double()
+{
+}
+
+QWidget* Double::get_widget(QWidget *parent, bool auto_commit)
+{
+	if (_spin_box)
+		return _spin_box;
+
+	GVariant *const value = _getter ? _getter() : NULL;
+	if (!value)
+		return NULL;
+
+	_spin_box = new QDoubleSpinBox(parent);
+	_spin_box->setDecimals(_decimals);
+	_spin_box->setSuffix(_suffix);
+	if (_range)
+		_spin_box->setRange(_range->first, _range->second);
+	if (_step)
+		_spin_box->setSingleStep(*_step);
+
+	_spin_box->setValue(g_variant_get_double(value));
+	g_variant_unref(value);
+
+	if (auto_commit)
+		connect(_spin_box, SIGNAL(valueChanged(double)),
+			this, SLOT(on_value_changed(double)));
+
+	return _spin_box;
+}
+
+void Double::commit()
+{
+	assert(_setter);
+
+	if (!_spin_box)
+		return;
+
+	_setter(g_variant_new_double(_spin_box->value()));
+}
+
+void Double::on_value_changed(double)
+{
+	commit();
+}
+
+} // prop
+} // pv
diff --git a/pv/prop/double.h b/pv/prop/double.h
new file mode 100644
index 0000000..ba4c595
--- /dev/null
+++ b/pv/prop/double.h
@@ -0,0 +1,67 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_PROP_DOUBLE_H
+#define PULSEVIEW_PV_PROP_DOUBLE_H
+
+#include <utility>
+
+#include <boost/optional.hpp>
+
+#include "property.h"
+
+class QDoubleSpinBox;
+
+namespace pv {
+namespace prop {
+
+class Double : public Property
+{
+	Q_OBJECT
+
+public:
+	Double(QString name, int decimals, QString suffix,
+		boost::optional< std::pair<double, double> > range,
+		boost::optional<double> step,
+		Getter getter,
+		Setter setter);
+
+	virtual ~Double();
+
+	QWidget* get_widget(QWidget *parent, bool auto_commit);
+
+	void commit();
+
+private slots:
+	void on_value_changed(double);
+
+private:
+	const int _decimals;
+	const QString _suffix;
+	const boost::optional< std::pair<double, double> > _range;
+	const boost::optional<double> _step;
+
+	QDoubleSpinBox *_spin_box;
+};
+
+} // prop
+} // pv
+
+#endif // PULSEVIEW_PV_PROP_DOUBLE_H
diff --git a/pv/prop/enum.cpp b/pv/prop/enum.cpp
new file mode 100644
index 0000000..6439a72
--- /dev/null
+++ b/pv/prop/enum.cpp
@@ -0,0 +1,98 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <assert.h>
+
+#include <QComboBox>
+
+#include "enum.h"
+
+using std::pair;
+using std::vector;
+
+namespace pv {
+namespace prop {
+
+Enum::Enum(QString name,
+	vector<pair<GVariant*, QString> > values,
+	Getter getter, Setter setter) :
+	Property(name, getter, setter),
+	_values(values),
+	_selector(NULL)
+{
+	for (vector< pair<GVariant*, QString> >::const_iterator i =
+		_values.begin(); i != _values.end(); i++)
+		g_variant_ref((*i).first);
+}
+
+Enum::~Enum()
+{
+	for (vector< pair<GVariant*, QString> >::const_iterator i =
+		_values.begin(); i != _values.end(); i++)
+		g_variant_unref((*i).first);
+}
+
+QWidget* Enum::get_widget(QWidget *parent, bool auto_commit)
+{
+	if (_selector)
+		return _selector;
+
+	GVariant *const value = _getter ? _getter() : NULL;
+	if (!value)
+		return NULL;
+
+	_selector = new QComboBox(parent);
+	for (unsigned int i = 0; i < _values.size(); i++) {
+		const pair<GVariant*, QString> &v = _values[i];
+		_selector->addItem(v.second, qVariantFromValue((void*)v.first));
+		if (value && g_variant_equal(v.first, value))
+			_selector->setCurrentIndex(i);
+	}
+
+	g_variant_unref(value);
+
+	if (auto_commit)
+		connect(_selector, SIGNAL(currentIndexChanged(int)),
+			this, SLOT(on_current_item_changed(int)));
+
+	return _selector;
+}
+
+void Enum::commit()
+{
+	assert(_setter);
+
+	if (!_selector)
+		return;
+
+	const int index = _selector->currentIndex();
+	if (index < 0)
+		return;
+
+	_setter((GVariant*)_selector->itemData(index).value<void*>());
+}
+
+void Enum::on_current_item_changed(int)
+{
+	commit();
+}
+
+} // prop
+} // pv
diff --git a/pv/prop/enum.h b/pv/prop/enum.h
new file mode 100644
index 0000000..a7e6ed8
--- /dev/null
+++ b/pv/prop/enum.h
@@ -0,0 +1,60 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_PROP_ENUM_H
+#define PULSEVIEW_PV_PROP_ENUM_H
+
+#include <utility>
+#include <vector>
+
+#include "property.h"
+
+class QComboBox;
+
+namespace pv {
+namespace prop {
+
+class Enum : public Property
+{
+	Q_OBJECT;
+
+public:
+	Enum(QString name, std::vector<std::pair<GVariant*, QString> > values,
+		Getter getter, Setter setter);
+
+	virtual ~Enum();
+
+	QWidget* get_widget(QWidget *parent, bool auto_commit);
+
+	void commit();
+
+private slots:
+	void on_current_item_changed(int);
+
+private:
+	const std::vector< std::pair<GVariant*, QString> > _values;
+
+	QComboBox *_selector;
+};
+
+} // prop
+} // pv
+
+#endif // PULSEVIEW_PV_PROP_ENUM_H
diff --git a/pv/prop/int.cpp b/pv/prop/int.cpp
new file mode 100644
index 0000000..8124f9b
--- /dev/null
+++ b/pv/prop/int.cpp
@@ -0,0 +1,186 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <stdint.h>
+#include <assert.h>
+
+#include <QSpinBox>
+
+#include "int.h"
+
+using boost::optional;
+using std::max;
+using std::min;
+using std::pair;
+
+namespace pv {
+namespace prop {
+
+Int::Int(QString name,
+	QString suffix,
+	optional< pair<int64_t, int64_t> > range,
+	Getter getter,
+	Setter setter) :
+	Property(name, getter, setter),
+	_suffix(suffix),
+	_range(range),
+	_value(NULL),
+	_spin_box(NULL)
+{
+}
+
+Int::~Int()
+{
+	if (_value)
+		g_variant_unref(_value);
+}
+
+QWidget* Int::get_widget(QWidget *parent, bool auto_commit)
+{
+	int64_t int_val = 0, range_min = 0, range_max = 0;
+
+	if (_spin_box)
+		return _spin_box;
+
+	if (_value)
+		g_variant_unref(_value);
+
+	_value = _getter ? _getter() : NULL;
+	if (!_value)
+		return NULL;
+
+	_spin_box = new QSpinBox(parent);
+	_spin_box->setSuffix(_suffix);
+
+	const GVariantType *const type = g_variant_get_type(_value);
+	assert(type);
+
+	if (g_variant_type_equal(type, G_VARIANT_TYPE_BYTE))
+	{
+		int_val = g_variant_get_byte(_value);
+		range_min = 0, range_max = UINT8_MAX;
+	}
+	else if (g_variant_type_equal(type, G_VARIANT_TYPE_INT16))
+	{
+		int_val = g_variant_get_int16(_value);
+		range_min = INT16_MIN, range_max = INT16_MAX;
+	}
+	else if (g_variant_type_equal(type, G_VARIANT_TYPE_UINT16))
+	{
+		int_val = g_variant_get_uint16(_value);
+		range_min = 0, range_max = UINT16_MAX;
+	}
+	else if (g_variant_type_equal(type, G_VARIANT_TYPE_INT32))
+	{
+		int_val = g_variant_get_int32(_value);
+		range_min = INT32_MIN, range_max = INT32_MAX;
+	}
+	else if (g_variant_type_equal(type, G_VARIANT_TYPE_UINT32))
+	{
+		int_val = g_variant_get_uint32(_value);
+		range_min = 0, range_max = UINT32_MAX;
+	}
+	else if (g_variant_type_equal(type, G_VARIANT_TYPE_INT64))
+	{
+		int_val = g_variant_get_int64(_value);
+		range_min = INT64_MIN, range_max = INT64_MAX;
+	}
+	else if (g_variant_type_equal(type, G_VARIANT_TYPE_UINT64))
+	{
+		int_val = g_variant_get_uint64(_value);
+		range_min = 0, range_max = UINT64_MAX;
+	}
+	else
+	{
+		// Unexpected value type.
+		assert(0);
+	}
+
+	// @todo Sigrok supports 64-bit quantities, but Qt does not have a
+	// standard widget to allow the values to be modified over the full
+	// 64-bit range on 32-bit machines. To solve the issue we need a
+	// custom widget.
+
+	range_min = max(range_min, (int64_t)INT_MIN);
+	range_max = min(range_max, (int64_t)INT_MAX);
+
+	if (_range)
+		_spin_box->setRange((int)_range->first, (int)_range->second);
+	else
+		_spin_box->setRange((int)range_min, (int)range_max);
+
+	_spin_box->setValue((int)int_val);
+
+	if (auto_commit)
+		connect(_spin_box, SIGNAL(valueChanged(int)),
+			this, SLOT(on_value_changed(int)));
+
+	return _spin_box;
+}
+
+void Int::commit()
+{
+	assert(_setter);
+
+	if (!_spin_box)
+		return;
+
+	assert(_value);
+
+	GVariant *new_value = NULL;
+	const GVariantType *const type = g_variant_get_type(_value);
+	assert(type);
+
+	if (g_variant_type_equal(type, G_VARIANT_TYPE_BYTE))
+		new_value = g_variant_new_byte(_spin_box->value());
+	else if (g_variant_type_equal(type, G_VARIANT_TYPE_INT16))
+		new_value = g_variant_new_int16(_spin_box->value());
+	else if (g_variant_type_equal(type, G_VARIANT_TYPE_UINT16))
+		new_value = g_variant_new_uint16(_spin_box->value());
+	else if (g_variant_type_equal(type, G_VARIANT_TYPE_INT32))
+		new_value = g_variant_new_int32(_spin_box->value());
+	else if (g_variant_type_equal(type, G_VARIANT_TYPE_UINT32))
+		new_value = g_variant_new_int32(_spin_box->value());
+	else if (g_variant_type_equal(type, G_VARIANT_TYPE_INT64))
+		new_value = g_variant_new_int64(_spin_box->value());
+	else if (g_variant_type_equal(type, G_VARIANT_TYPE_UINT64))
+		new_value = g_variant_new_uint64(_spin_box->value());
+	else
+	{
+		// Unexpected value type.
+		assert(0);
+	}
+
+	assert(new_value);
+
+	g_variant_unref(_value);
+	g_variant_ref(new_value);
+	_value = new_value;
+
+	_setter(new_value);
+}
+
+void Int::on_value_changed(int)
+{
+	commit();
+}
+
+} // prop
+} // pv
diff --git a/pv/prop/int.h b/pv/prop/int.h
new file mode 100644
index 0000000..6d910cd
--- /dev/null
+++ b/pv/prop/int.h
@@ -0,0 +1,64 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_PROP_INT_H
+#define PULSEVIEW_PV_PROP_INT_H
+
+#include <utility>
+
+#include <boost/optional.hpp>
+
+#include "property.h"
+
+class QSpinBox;
+
+namespace pv {
+namespace prop {
+
+class Int : public Property
+{
+	Q_OBJECT;
+
+public:
+	Int(QString name, QString suffix,
+		boost::optional< std::pair<int64_t, int64_t> > range,
+		Getter getter, Setter setter);
+
+	virtual ~Int();
+
+	QWidget* get_widget(QWidget *parent, bool auto_commit);
+
+	void commit();
+
+private slots:
+	void on_value_changed(int);
+
+private:
+	const QString _suffix;
+	const boost::optional< std::pair<int64_t, int64_t> > _range;
+
+	GVariant *_value;
+	QSpinBox *_spin_box;
+};
+
+} // prop
+} // pv
+
+#endif // PULSEVIEW_PV_PROP_INT_H
diff --git a/pv/prop/property.cpp b/pv/prop/property.cpp
new file mode 100644
index 0000000..798b5f1
--- /dev/null
+++ b/pv/prop/property.cpp
@@ -0,0 +1,44 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "property.h"
+
+namespace pv {
+namespace prop {
+
+Property::Property(QString name, Getter getter, Setter setter) :
+	_getter(getter),
+	_setter(setter),
+	_name(name)
+{
+}
+
+const QString& Property::name() const
+{
+	return _name;
+}
+
+bool Property::labeled_widget() const
+{
+	return false;
+}
+
+} // prop
+} // pv
diff --git a/pv/prop/property.h b/pv/prop/property.h
new file mode 100644
index 0000000..0b4bc7b
--- /dev/null
+++ b/pv/prop/property.h
@@ -0,0 +1,67 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_PROP_PROPERTY_H
+#define PULSEVIEW_PV_PROP_PROPERTY_H
+
+#include <glib.h>
+
+#include <boost/function.hpp>
+
+#include <QString>
+#include <QWidget>
+
+class QWidget;
+
+namespace pv {
+namespace prop {
+
+class Property : public QObject
+{
+	Q_OBJECT;
+
+public:
+	typedef boost::function<GVariant* ()> Getter;
+	typedef boost::function<void (GVariant*)> Setter;
+
+protected:
+	Property(QString name, Getter getter, Setter setter);
+
+public:
+	const QString& name() const;
+
+	virtual QWidget* get_widget(QWidget *parent,
+		bool auto_commit = false) = 0;
+	virtual bool labeled_widget() const;
+
+	virtual void commit() = 0;
+
+protected:
+	const Getter _getter;
+	const Setter _setter;
+
+private:
+	QString _name;
+};
+
+} // prop
+} // pv
+
+#endif // PULSEVIEW_PV_PROP_PROPERTY_H
diff --git a/pv/prop/string.cpp b/pv/prop/string.cpp
new file mode 100644
index 0000000..7fcc656
--- /dev/null
+++ b/pv/prop/string.cpp
@@ -0,0 +1,77 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <assert.h>
+
+#include <QLineEdit>
+#include <QSpinBox>
+
+#include "string.h"
+
+namespace pv {
+namespace prop {
+
+String::String(QString name,
+	Getter getter,
+	Setter setter) :
+	Property(name, getter, setter),
+	_line_edit(NULL)
+{
+}
+
+QWidget* String::get_widget(QWidget *parent, bool auto_commit)
+{
+	if (_line_edit)
+		return _line_edit;
+
+	GVariant *const value = _getter ? _getter() : NULL;
+	if (!value)
+		return NULL;
+
+	_line_edit = new QLineEdit(parent);
+	_line_edit->setText(QString::fromUtf8(
+		g_variant_get_string(value, NULL)));
+	g_variant_unref(value);
+
+	if (auto_commit)
+		connect(_line_edit, SIGNAL(textEdited(const QString&)),
+			this, SLOT(on_text_edited(const QString&)));
+
+	return _line_edit;
+}
+
+void String::commit()
+{
+	assert(_setter);
+
+	if (!_line_edit)
+		return;
+
+	QByteArray ba = _line_edit->text().toLocal8Bit();
+	_setter(g_variant_new_string(ba.data()));
+}
+
+void String::on_text_edited(const QString&)
+{
+	commit();
+}
+
+} // prop
+} // pv
diff --git a/pv/prop/string.h b/pv/prop/string.h
new file mode 100644
index 0000000..d54635f
--- /dev/null
+++ b/pv/prop/string.h
@@ -0,0 +1,52 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_PROP_STRING_H
+#define PULSEVIEW_PV_PROP_STRING_H
+
+#include "property.h"
+
+class QLineEdit;
+
+namespace pv {
+namespace prop {
+
+class String : public Property
+{
+	Q_OBJECT;
+
+public:
+	String(QString name, Getter getter, Setter setter);
+
+	QWidget* get_widget(QWidget *parent, bool auto_commit);
+
+	void commit();
+
+private slots:
+	void on_text_edited(const QString&);
+
+private:
+	QLineEdit *_line_edit;
+};
+
+} // prop
+} // pv
+
+#endif // PULSEVIEW_PV_PROP_STRING_H
diff --git a/pv/sigsession.cpp b/pv/sigsession.cpp
new file mode 100644
index 0000000..b441dab
--- /dev/null
+++ b/pv/sigsession.cpp
@@ -0,0 +1,650 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012-14 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifdef ENABLE_DECODE
+#include <libsigrokdecode/libsigrokdecode.h>
+#endif
+
+#include "sigsession.h"
+
+#include "devicemanager.h"
+#include "device/device.h"
+#include "device/file.h"
+
+#include "data/analog.h"
+#include "data/analogsnapshot.h"
+#include "data/decoderstack.h"
+#include "data/logic.h"
+#include "data/logicsnapshot.h"
+#include "data/decode/decoder.h"
+
+#include "view/analogsignal.h"
+#include "view/decodetrace.h"
+#include "view/logicsignal.h"
+
+#include <assert.h>
+
+#include <stdexcept>
+
+#include <boost/foreach.hpp>
+
+#include <sys/stat.h>
+
+#include <QDebug>
+
+using boost::dynamic_pointer_cast;
+using boost::function;
+using boost::lock_guard;
+using boost::mutex;
+using boost::shared_ptr;
+using std::list;
+using std::map;
+using std::set;
+using std::string;
+using std::vector;
+
+namespace pv {
+
+// TODO: This should not be necessary
+SigSession* SigSession::_session = NULL;
+
+SigSession::SigSession(DeviceManager &device_manager) :
+	_device_manager(device_manager),
+	_capture_state(Stopped)
+{
+	// TODO: This should not be necessary
+	_session = this;
+
+	set_default_device();
+}
+
+SigSession::~SigSession()
+{
+	using pv::device::Device;
+
+	stop_capture();
+
+	if (_sampling_thread.joinable())
+		_sampling_thread.join();
+
+	_dev_inst->release();
+
+	// TODO: This should not be necessary
+	_session = NULL;
+}
+
+shared_ptr<device::DevInst> SigSession::get_device() const
+{
+	return _dev_inst;
+}
+
+void SigSession::set_device(
+	shared_ptr<device::DevInst> dev_inst) throw(QString)
+{
+	using pv::device::Device;
+
+	// Ensure we are not capturing before setting the device
+	stop_capture();
+
+	if (_dev_inst) {
+		sr_session_datafeed_callback_remove_all();
+		_dev_inst->release();
+	}
+
+	_dev_inst = dev_inst;
+	_decode_traces.clear();
+
+	if (dev_inst) {
+		dev_inst->use(this);
+		sr_session_datafeed_callback_add(data_feed_in_proc, NULL);
+		update_signals(dev_inst);
+	}
+}
+
+void SigSession::set_file(const string &name) throw(QString)
+{
+	// Deslect the old device, because file type detection in File::create
+	// destorys the old session inside libsigrok.
+	set_device(shared_ptr<device::DevInst>());
+	set_device(shared_ptr<device::DevInst>(device::File::create(name)));
+}
+
+void SigSession::set_default_device()
+{
+	shared_ptr<pv::device::DevInst> default_device;
+	const list< shared_ptr<device::Device> > &devices =
+		_device_manager.devices();
+
+	if (!devices.empty()) {
+		// Fall back to the first device in the list.
+		default_device = devices.front();
+
+		// Try and find the demo device and select that by default
+		BOOST_FOREACH (shared_ptr<pv::device::Device> dev, devices)
+			if (strcmp(dev->dev_inst()->driver->name,
+				"demo") == 0) {
+				default_device = dev;
+				break;
+			}
+	}
+
+	set_device(default_device);
+}
+
+void SigSession::release_device(device::DevInst *dev_inst)
+{
+	(void)dev_inst;
+	assert(_dev_inst.get() == dev_inst);
+
+	assert(_capture_state == Stopped);
+	_dev_inst = shared_ptr<device::DevInst>();
+}
+
+SigSession::capture_state SigSession::get_capture_state() const
+{
+	lock_guard<mutex> lock(_sampling_mutex);
+	return _capture_state;
+}
+
+void SigSession::start_capture(function<void (const QString)> error_handler)
+{
+	stop_capture();
+
+	// Check that a device instance has been selected.
+	if (!_dev_inst) {
+		qDebug() << "No device selected";
+		return;
+	}
+
+	assert(_dev_inst->dev_inst());
+
+	// Check that at least one probe is enabled
+	const GSList *l;
+	for (l = _dev_inst->dev_inst()->channels; l; l = l->next) {
+		sr_channel *const probe = (sr_channel*)l->data;
+		assert(probe);
+		if (probe->enabled)
+			break;
+	}
+
+	if (!l) {
+		error_handler(tr("No channels enabled."));
+		return;
+	}
+
+	// Begin the session
+	_sampling_thread = boost::thread(
+		&SigSession::sample_thread_proc, this, _dev_inst,
+			error_handler);
+}
+
+void SigSession::stop_capture()
+{
+	if (get_capture_state() == Stopped)
+		return;
+
+	sr_session_stop();
+
+	// Check that sampling stopped
+	if (_sampling_thread.joinable())
+		_sampling_thread.join();
+}
+
+set< shared_ptr<data::SignalData> > SigSession::get_data() const
+{
+	lock_guard<mutex> lock(_signals_mutex);
+	set< shared_ptr<data::SignalData> > data;
+	BOOST_FOREACH(const shared_ptr<view::Signal> sig, _signals) {
+		assert(sig);
+		data.insert(sig->data());
+	}
+
+	return data;
+}
+
+vector< shared_ptr<view::Signal> > SigSession::get_signals() const
+{
+	lock_guard<mutex> lock(_signals_mutex);
+	return _signals;
+}
+
+#ifdef ENABLE_DECODE
+bool SigSession::add_decoder(srd_decoder *const dec)
+{
+	map<const srd_channel*, shared_ptr<view::LogicSignal> > probes;
+	shared_ptr<data::DecoderStack> decoder_stack;
+
+	try
+	{
+		lock_guard<mutex> lock(_signals_mutex);
+
+		// Create the decoder
+		decoder_stack = shared_ptr<data::DecoderStack>(
+			new data::DecoderStack(*this, dec));
+
+		// Make a list of all the probes
+		std::vector<const srd_channel*> all_probes;
+		for(const GSList *i = dec->channels; i; i = i->next)
+			all_probes.push_back((const srd_channel*)i->data);
+		for(const GSList *i = dec->opt_channels; i; i = i->next)
+			all_probes.push_back((const srd_channel*)i->data);
+
+		// Auto select the initial probes
+		BOOST_FOREACH(const srd_channel *pdch, all_probes)
+			BOOST_FOREACH(shared_ptr<view::Signal> s, _signals)
+			{
+				shared_ptr<view::LogicSignal> l =
+					dynamic_pointer_cast<view::LogicSignal>(s);
+				if (l && QString::fromUtf8(pdch->name).
+					toLower().contains(
+					l->get_name().toLower()))
+					probes[pdch] = l;
+			}
+
+		assert(decoder_stack);
+		assert(!decoder_stack->stack().empty());
+		assert(decoder_stack->stack().front());
+		decoder_stack->stack().front()->set_probes(probes);
+
+		// Create the decode signal
+		shared_ptr<view::DecodeTrace> d(
+			new view::DecodeTrace(*this, decoder_stack,
+				_decode_traces.size()));
+		_decode_traces.push_back(d);
+	}
+	catch(std::runtime_error e)
+	{
+		return false;
+	}
+
+	signals_changed();
+
+	// Do an initial decode
+	decoder_stack->begin_decode();
+
+	return true;
+}
+
+vector< shared_ptr<view::DecodeTrace> > SigSession::get_decode_signals() const
+{
+	lock_guard<mutex> lock(_signals_mutex);
+	return _decode_traces;
+}
+
+void SigSession::remove_decode_signal(view::DecodeTrace *signal)
+{
+	for (vector< shared_ptr<view::DecodeTrace> >::iterator i =
+		_decode_traces.begin();
+		i != _decode_traces.end();
+		i++)
+		if ((*i).get() == signal)
+		{
+			_decode_traces.erase(i);
+			signals_changed();
+			return;
+		}
+}
+#endif
+
+void SigSession::set_capture_state(capture_state state)
+{
+	lock_guard<mutex> lock(_sampling_mutex);
+	const bool changed = _capture_state != state;
+	_capture_state = state;
+	if(changed)
+		capture_state_changed(state);
+}
+
+void SigSession::update_signals(shared_ptr<device::DevInst> dev_inst)
+{
+	assert(dev_inst);
+	assert(_capture_state == Stopped);
+
+	unsigned int logic_probe_count = 0;
+
+	// Clear the decode traces
+	_decode_traces.clear();
+
+	// Detect what data types we will receive
+	if(dev_inst) {
+		assert(dev_inst->dev_inst());
+		for (const GSList *l = dev_inst->dev_inst()->channels;
+			l; l = l->next) {
+			const sr_channel *const probe = (const sr_channel *)l->data;
+			if (!probe->enabled)
+				continue;
+
+			switch(probe->type) {
+			case SR_CHANNEL_LOGIC:
+				logic_probe_count++;
+				break;
+			}
+		}
+	}
+
+	// Create data containers for the logic data snapshots
+	{
+		lock_guard<mutex> data_lock(_data_mutex);
+
+		_logic_data.reset();
+		if (logic_probe_count != 0) {
+			_logic_data.reset(new data::Logic(
+				logic_probe_count));
+			assert(_logic_data);
+		}
+	}
+
+	// Make the Signals list
+	do {
+		lock_guard<mutex> lock(_signals_mutex);
+
+		_signals.clear();
+
+		if(!dev_inst)
+			break;
+
+		assert(dev_inst->dev_inst());
+		for (const GSList *l = dev_inst->dev_inst()->channels;
+			l; l = l->next) {
+			shared_ptr<view::Signal> signal;
+			sr_channel *const probe = (sr_channel *)l->data;
+			assert(probe);
+
+			switch(probe->type) {
+			case SR_CHANNEL_LOGIC:
+				signal = shared_ptr<view::Signal>(
+					new view::LogicSignal(dev_inst,
+						probe, _logic_data));
+				break;
+
+			case SR_CHANNEL_ANALOG:
+			{
+				shared_ptr<data::Analog> data(
+					new data::Analog());
+				signal = shared_ptr<view::Signal>(
+					new view::AnalogSignal(dev_inst,
+						probe, data));
+				break;
+			}
+
+			default:
+				assert(0);
+				break;
+			}
+
+			assert(signal);
+			_signals.push_back(signal);
+		}
+
+	} while(0);
+
+	signals_changed();
+}
+
+shared_ptr<view::Signal> SigSession::signal_from_probe(
+	const sr_channel *probe) const
+{
+	lock_guard<mutex> lock(_signals_mutex);
+	BOOST_FOREACH(shared_ptr<view::Signal> sig, _signals) {
+		assert(sig);
+		if (sig->probe() == probe)
+			return sig;
+	}
+	return shared_ptr<view::Signal>();
+}
+
+void SigSession::read_sample_rate(const sr_dev_inst *const sdi)
+{
+	GVariant *gvar;
+	uint64_t sample_rate = 0;
+
+	// Read out the sample rate
+	if(sdi->driver)
+	{
+		const int ret = sr_config_get(sdi->driver, sdi, NULL,
+			SR_CONF_SAMPLERATE, &gvar);
+		if (ret != SR_OK) {
+			qDebug("Failed to get samplerate\n");
+			return;
+		}
+
+		sample_rate = g_variant_get_uint64(gvar);
+		g_variant_unref(gvar);
+	}
+
+	// Set the sample rate of all data
+	const set< shared_ptr<data::SignalData> > data_set = get_data();
+	BOOST_FOREACH(shared_ptr<data::SignalData> data, data_set) {
+		assert(data);
+		data->set_samplerate(sample_rate);
+	}
+}
+
+void SigSession::sample_thread_proc(shared_ptr<device::DevInst> dev_inst,
+	function<void (const QString)> error_handler)
+{
+	assert(dev_inst);
+	assert(dev_inst->dev_inst());
+	assert(error_handler);
+
+	read_sample_rate(dev_inst->dev_inst());
+
+	try {
+		dev_inst->start();
+	} catch(const QString e) {
+		error_handler(e);
+		return;
+	}
+
+	set_capture_state(dev_inst->is_trigger_enabled() ?
+		AwaitingTrigger : Running);
+
+	dev_inst->run();
+	set_capture_state(Stopped);
+
+	// Confirm that SR_DF_END was received
+	if (_cur_logic_snapshot)
+	{
+		qDebug("SR_DF_END was not received.");
+		assert(0);
+	}
+}
+
+void SigSession::feed_in_header(const sr_dev_inst *sdi)
+{
+	read_sample_rate(sdi);
+}
+
+void SigSession::feed_in_meta(const sr_dev_inst *sdi,
+	const sr_datafeed_meta &meta)
+{
+	(void)sdi;
+
+	for (const GSList *l = meta.config; l; l = l->next) {
+		const sr_config *const src = (const sr_config*)l->data;
+		switch (src->key) {
+		case SR_CONF_SAMPLERATE:
+			/// @todo handle samplerate changes
+			/// samplerate = (uint64_t *)src->value;
+			break;
+		default:
+			// Unknown metadata is not an error.
+			break;
+		}
+	}
+
+	signals_changed();
+}
+
+void SigSession::feed_in_frame_begin()
+{
+	if (_cur_logic_snapshot || !_cur_analog_snapshots.empty())
+		frame_began();
+}
+
+void SigSession::feed_in_logic(const sr_datafeed_logic &logic)
+{
+	lock_guard<mutex> lock(_data_mutex);
+
+	if (!_logic_data)
+	{
+		qDebug() << "Unexpected logic packet";
+		return;
+	}
+
+	if (!_cur_logic_snapshot)
+	{
+		// This could be the first packet after a trigger
+		set_capture_state(Running);
+
+		// Create a new data snapshot
+		_cur_logic_snapshot = shared_ptr<data::LogicSnapshot>(
+			new data::LogicSnapshot(logic, _dev_inst->get_sample_limit()));
+		_logic_data->push_snapshot(_cur_logic_snapshot);
+
+		// @todo Putting this here means that only listeners querying
+		// for logic will be notified. Currently the only user of
+		// frame_began is DecoderStack, but in future we need to signal
+		// this after both analog and logic sweeps have begun.
+		frame_began();
+	}
+	else
+	{
+		// Append to the existing data snapshot
+		_cur_logic_snapshot->append_payload(logic);
+	}
+
+	data_received();
+}
+
+void SigSession::feed_in_analog(const sr_datafeed_analog &analog)
+{
+	lock_guard<mutex> lock(_data_mutex);
+
+	const unsigned int probe_count = g_slist_length(analog.channels);
+	const size_t sample_count = analog.num_samples / probe_count;
+	const float *data = analog.data;
+	bool sweep_beginning = false;
+
+	for (GSList *p = analog.channels; p; p = p->next)
+	{
+		shared_ptr<data::AnalogSnapshot> snapshot;
+
+		sr_channel *const probe = (sr_channel*)p->data;
+		assert(probe);
+
+		// Try to get the snapshot of the probe
+		const map< const sr_channel*, shared_ptr<data::AnalogSnapshot> >::
+			iterator iter = _cur_analog_snapshots.find(probe);
+		if (iter != _cur_analog_snapshots.end())
+			snapshot = (*iter).second;
+		else
+		{
+			// If no snapshot was found, this means we havn't
+			// created one yet. i.e. this is the first packet
+			// in the sweep containing this snapshot.
+			sweep_beginning = true;
+
+			// Create a snapshot, keep it in the maps of probes
+			snapshot = shared_ptr<data::AnalogSnapshot>(
+				new data::AnalogSnapshot(_dev_inst->get_sample_limit()));
+			_cur_analog_snapshots[probe] = snapshot;
+
+			// Find the annalog data associated with the probe
+			shared_ptr<view::AnalogSignal> sig =
+				dynamic_pointer_cast<view::AnalogSignal>(
+					signal_from_probe(probe));
+			assert(sig);
+
+			shared_ptr<data::Analog> data(sig->analog_data());
+			assert(data);
+
+			// Push the snapshot into the analog data.
+			data->push_snapshot(snapshot);
+		}
+
+		assert(snapshot);
+
+		// Append the samples in the snapshot
+		snapshot->append_interleaved_samples(data++, sample_count,
+			probe_count);
+	}
+
+	if (sweep_beginning) {
+		// This could be the first packet after a trigger
+		set_capture_state(Running);
+	}
+
+	data_received();
+}
+
+void SigSession::data_feed_in(const struct sr_dev_inst *sdi,
+	const struct sr_datafeed_packet *packet)
+{
+	assert(sdi);
+	assert(packet);
+
+	switch (packet->type) {
+	case SR_DF_HEADER:
+		feed_in_header(sdi);
+		break;
+
+	case SR_DF_META:
+		assert(packet->payload);
+		feed_in_meta(sdi,
+			*(const sr_datafeed_meta*)packet->payload);
+		break;
+
+	case SR_DF_FRAME_BEGIN:
+		feed_in_frame_begin();
+		break;
+
+	case SR_DF_LOGIC:
+		assert(packet->payload);
+		feed_in_logic(*(const sr_datafeed_logic*)packet->payload);
+		break;
+
+	case SR_DF_ANALOG:
+		assert(packet->payload);
+		feed_in_analog(*(const sr_datafeed_analog*)packet->payload);
+		break;
+
+	case SR_DF_END:
+	{
+		{
+			lock_guard<mutex> lock(_data_mutex);
+			_cur_logic_snapshot.reset();
+			_cur_analog_snapshots.clear();
+		}
+		frame_ended();
+		break;
+	}
+	}
+}
+
+void SigSession::data_feed_in_proc(const struct sr_dev_inst *sdi,
+	const struct sr_datafeed_packet *packet, void *cb_data)
+{
+	(void) cb_data;
+	assert(_session);
+	_session->data_feed_in(sdi, packet);
+}
+
+} // namespace pv
diff --git a/pv/sigsession.h b/pv/sigsession.h
new file mode 100644
index 0000000..1221750
--- /dev/null
+++ b/pv/sigsession.h
@@ -0,0 +1,204 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012-14 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_SIGSESSION_H
+#define PULSEVIEW_PV_SIGSESSION_H
+
+#include <boost/function.hpp>
+#include <boost/shared_ptr.hpp>
+#include <boost/thread.hpp>
+
+#include <map>
+#include <set>
+#include <string>
+#include <vector>
+
+#include <QObject>
+#include <QString>
+
+#include <libsigrok/libsigrok.h>
+
+struct srd_decoder;
+struct srd_channel;
+
+namespace pv {
+
+class DeviceManager;
+
+namespace data {
+class Analog;
+class AnalogSnapshot;
+class Logic;
+class LogicSnapshot;
+class SignalData;
+}
+
+namespace device {
+class DevInst;
+}
+
+namespace view {
+class DecodeTrace;
+class LogicSignal;
+class Signal;
+}
+
+class SigSession : public QObject
+{
+	Q_OBJECT
+
+public:
+	enum capture_state {
+		Stopped,
+		AwaitingTrigger,
+		Running
+	};
+
+public:
+	SigSession(DeviceManager &device_manager);
+
+	~SigSession();
+
+	boost::shared_ptr<device::DevInst> get_device() const;
+
+	/**
+	 * Sets device instance that will be used in the next capture session.
+	 */
+	void set_device(boost::shared_ptr<device::DevInst> dev_inst)
+		throw(QString);
+
+	void set_file(const std::string &name)
+		throw(QString);
+
+	void set_default_device();
+
+	void release_device(device::DevInst *dev_inst);
+
+	capture_state get_capture_state() const;
+
+	void start_capture(boost::function<void (const QString)> error_handler);
+
+	void stop_capture();
+
+	std::set< boost::shared_ptr<data::SignalData> > get_data() const;
+
+	std::vector< boost::shared_ptr<view::Signal> >
+		get_signals() const;
+
+#ifdef ENABLE_DECODE
+	bool add_decoder(srd_decoder *const dec);
+
+	std::vector< boost::shared_ptr<view::DecodeTrace> >
+		get_decode_signals() const;
+
+	void remove_decode_signal(view::DecodeTrace *signal);
+#endif
+
+private:
+	void set_capture_state(capture_state state);
+
+	void update_signals(boost::shared_ptr<device::DevInst> dev_inst);
+
+	boost::shared_ptr<view::Signal> signal_from_probe(
+		const sr_channel *probe) const;
+
+	void read_sample_rate(const sr_dev_inst *const sdi);
+
+private:
+	/**
+	 * Attempts to autodetect the format. Failing that
+	 * @param filename The filename of the input file.
+	 * @return A pointer to the 'struct sr_input_format' that should be
+	 * 	used, or NULL if no input format was selected or
+	 * 	auto-detected.
+	 */
+	static sr_input_format* determine_input_file_format(
+		const std::string &filename);
+
+	static sr_input* load_input_file_format(
+		const std::string &filename,
+		boost::function<void (const QString)> error_handler,
+		sr_input_format *format = NULL);
+
+	void sample_thread_proc(boost::shared_ptr<device::DevInst> dev_inst,
+		boost::function<void (const QString)> error_handler);
+
+	void feed_in_header(const sr_dev_inst *sdi);
+
+	void feed_in_meta(const sr_dev_inst *sdi,
+		const sr_datafeed_meta &meta);
+
+	void feed_in_frame_begin();
+
+	void feed_in_logic(const sr_datafeed_logic &logic);
+
+	void feed_in_analog(const sr_datafeed_analog &analog);
+
+	void data_feed_in(const struct sr_dev_inst *sdi,
+		const struct sr_datafeed_packet *packet);
+
+	static void data_feed_in_proc(const struct sr_dev_inst *sdi,
+		const struct sr_datafeed_packet *packet, void *cb_data);
+
+private:
+	DeviceManager &_device_manager;
+
+	/**
+	 * The device instance that will be used in the next capture session.
+	 */
+	boost::shared_ptr<device::DevInst> _dev_inst;
+
+	std::vector< boost::shared_ptr<view::DecodeTrace> > _decode_traces;
+
+	mutable boost::mutex _sampling_mutex;
+	capture_state _capture_state;
+
+	mutable boost::mutex _signals_mutex;
+	std::vector< boost::shared_ptr<view::Signal> > _signals;
+
+	mutable boost::mutex _data_mutex;
+	boost::shared_ptr<data::Logic> _logic_data;
+	boost::shared_ptr<data::LogicSnapshot> _cur_logic_snapshot;
+	std::map< const sr_channel*, boost::shared_ptr<data::AnalogSnapshot> >
+		_cur_analog_snapshots;
+
+	boost::thread _sampling_thread;
+
+signals:
+	void capture_state_changed(int state);
+
+	void signals_changed();
+
+	void frame_began();
+
+	void data_received();
+
+	void frame_ended();
+
+private:
+	// TODO: This should not be necessary. Multiple concurrent
+	// sessions should should be supported and it should be
+	// possible to associate a pointer with a sr_session.
+	static SigSession *_session;
+};
+
+} // namespace pv
+
+#endif // PULSEVIEW_PV_SIGSESSION_H
diff --git a/pv/storesession.cpp b/pv/storesession.cpp
new file mode 100644
index 0000000..3d0f12f
--- /dev/null
+++ b/pv/storesession.cpp
@@ -0,0 +1,196 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2014 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "storesession.h"
+
+#include <pv/sigsession.h>
+#include <pv/data/logic.h>
+#include <pv/data/logicsnapshot.h>
+#include <pv/view/signal.h>
+
+using boost::dynamic_pointer_cast;
+using boost::mutex;
+using boost::shared_ptr;
+using boost::thread;
+using boost::lock_guard;
+using std::deque;
+using std::make_pair;
+using std::min;
+using std::pair;
+using std::set;
+using std::string;
+using std::vector;
+
+namespace pv {
+
+const size_t StoreSession::BlockSize = 1024 * 1024;
+
+StoreSession::StoreSession(const std::string &file_name,
+	const SigSession &session) :
+	_file_name(file_name),
+	_session(session),
+	_units_stored(0),
+	_unit_count(0)
+{
+}
+
+StoreSession::~StoreSession()
+{
+	wait();
+}
+
+pair<uint64_t, uint64_t> StoreSession::progress() const
+{
+	lock_guard<mutex> lock(_mutex);
+	return make_pair(_units_stored, _unit_count);
+}
+
+const QString& StoreSession::error() const
+{
+	lock_guard<mutex> lock(_mutex);
+	return _error;
+}
+
+bool StoreSession::start()
+{
+	set< shared_ptr<data::SignalData> > data_set =
+		_session.get_data();
+	const vector< shared_ptr<view::Signal> > sigs =
+		_session.get_signals();
+
+	// Check we have logic data
+	if (data_set.empty() || sigs.empty()) {
+		_error = tr("No data to save.");
+		return false;
+	}
+
+	if (data_set.size() > 1) {
+		_error = tr("PulseView currently only has support for "
+			"storing a single data stream.");
+		return false;
+	}
+
+	// Get the logic data
+	//shared_ptr<data::SignalData
+	shared_ptr<data::Logic> data;
+	if (!(data = dynamic_pointer_cast<data::Logic>(*data_set.begin()))) {
+		_error = tr("PulseView currently only has support for "
+			"storing a logic data.");
+		return false;
+	}
+
+	// Get the snapshot
+	const deque< shared_ptr<data::LogicSnapshot> > &snapshots =
+		data->get_snapshots();
+
+	if (snapshots.empty()) {
+		_error = tr("No snapshots to save.");
+		return false;
+	}
+
+	const shared_ptr<data::LogicSnapshot> snapshot(snapshots.front());
+	assert(snapshot);
+
+	// Make a list of probes
+	char **const probes = new char*[sigs.size() + 1];
+	for (size_t i = 0; i < sigs.size(); i++) {
+		shared_ptr<view::Signal> sig(sigs[i]);
+		assert(sig);
+		probes[i] = strdup(sig->get_name().toUtf8().constData());
+	}
+	probes[sigs.size()] = NULL;
+
+	// Begin storing
+	if (sr_session_save_init(_file_name.c_str(),
+		data->samplerate(), probes) != SR_OK) {
+		_error = tr("Error while saving.");
+		return false;
+	}
+
+	// Delete the probes array
+	for (size_t i = 0; i <= sigs.size(); i++)
+		free(probes[i]);
+	delete[] probes;
+
+	_thread = boost::thread(&StoreSession::store_proc, this, snapshot);
+	return true;
+}
+
+void StoreSession::wait()
+{
+	if (_thread.joinable())
+		_thread.join();
+}
+
+void StoreSession::cancel()
+{
+	_thread.interrupt();
+}
+
+void StoreSession::store_proc(shared_ptr<data::LogicSnapshot> snapshot)
+{
+	assert(snapshot);
+
+	uint64_t start_sample = 0;
+
+	/// TODO: Wrap this in a std::unique_ptr when we transition to C++11
+	uint8_t *const data = new uint8_t[BlockSize];
+	assert(data);
+
+	const int unit_size = snapshot->unit_size();
+	assert(unit_size != 0);
+
+	{
+		lock_guard<mutex> lock(_mutex);
+		_unit_count = snapshot->get_sample_count();
+	}
+
+	const unsigned int samples_per_block = BlockSize / unit_size;
+
+	while (!boost::this_thread::interruption_requested() &&
+		start_sample < _unit_count)
+	{
+		progress_updated();
+
+		const uint64_t end_sample = min(
+			start_sample + samples_per_block, _unit_count);
+		snapshot->get_samples(data, start_sample, end_sample);
+
+		if(sr_session_append(_file_name.c_str(), data, unit_size,
+			end_sample - start_sample) != SR_OK)
+		{
+			_error = tr("Error while saving.");
+			break;
+		}
+
+		start_sample = end_sample;
+
+		{
+			lock_guard<mutex> lock(_mutex);
+			_units_stored = start_sample;
+		}
+	}
+
+	progress_updated();
+
+	delete[] data;
+}
+
+} // pv
diff --git a/pv/storesession.h b/pv/storesession.h
new file mode 100644
index 0000000..5ef92a3
--- /dev/null
+++ b/pv/storesession.h
@@ -0,0 +1,83 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2014 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_STORESESSION_H
+#define PULSEVIEW_PV_STORESESSION_H
+
+#include <stdint.h>
+
+#include <string>
+
+#include <boost/thread.hpp>
+
+#include <QObject>
+
+namespace pv {
+
+class SigSession;
+
+namespace data {
+class LogicSnapshot;
+}
+
+class StoreSession : public QObject
+{
+	Q_OBJECT
+
+private:
+	static const size_t BlockSize;
+
+public:
+	StoreSession(const std::string &file_name,
+		const SigSession &session);
+
+	~StoreSession();
+
+	std::pair<uint64_t, uint64_t> progress() const;
+
+	const QString& error() const;
+
+	bool start();
+
+	void wait();
+
+	void cancel();
+
+private:
+	void store_proc(boost::shared_ptr<pv::data::LogicSnapshot> snapshot);
+
+signals:
+	void progress_updated();
+
+private:
+	const std::string _file_name;
+	const SigSession &_session;
+
+	boost::thread _thread;
+
+	mutable boost::mutex _mutex;
+	uint64_t _units_stored;
+	uint64_t _unit_count;
+	QString _error;
+};
+
+} // pv
+
+#endif // PULSEVIEW_PV_STORESESSION_H
diff --git a/pv/toolbars/samplingbar.cpp b/pv/toolbars/samplingbar.cpp
new file mode 100644
index 0000000..91c4b07
--- /dev/null
+++ b/pv/toolbars/samplingbar.cpp
@@ -0,0 +1,450 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <extdef.h>
+
+#include <assert.h>
+
+#include <boost/foreach.hpp>
+
+#include <QAction>
+#include <QDebug>
+
+#include "samplingbar.h"
+
+#include <pv/devicemanager.h>
+#include <pv/device/devinst.h>
+#include <pv/popups/deviceoptions.h>
+#include <pv/popups/probes.h>
+
+using boost::shared_ptr;
+using std::map;
+using std::max;
+using std::min;
+using std::string;
+
+namespace pv {
+namespace toolbars {
+
+const uint64_t SamplingBar::MinSampleCount = 100ULL;
+const uint64_t SamplingBar::MaxSampleCount = 1000000000000ULL;
+const uint64_t SamplingBar::DefaultSampleCount = 1000000;
+
+SamplingBar::SamplingBar(SigSession &session, QWidget *parent) :
+	QToolBar("Sampling Bar", parent),
+	_session(session),
+	_device_selector(this),
+	_updating_device_selector(false),
+	_configure_button(this),
+	_configure_button_action(NULL),
+	_probes_button(this),
+	_sample_count(" samples", this),
+	_sample_rate("Hz", this),
+	_updating_sample_rate(false),
+	_updating_sample_count(false),
+	_sample_count_supported(false),
+	_icon_red(":/icons/status-red.svg"),
+	_icon_green(":/icons/status-green.svg"),
+	_icon_grey(":/icons/status-grey.svg"),
+	_run_stop_button(this)
+{
+	connect(&_run_stop_button, SIGNAL(clicked()),
+		this, SLOT(on_run_stop()));
+	connect(&_device_selector, SIGNAL(currentIndexChanged (int)),
+		this, SLOT(on_device_selected()));
+	connect(&_sample_count, SIGNAL(value_changed()),
+		this, SLOT(on_sample_count_changed()));
+	connect(&_sample_rate, SIGNAL(value_changed()),
+		this, SLOT(on_sample_rate_changed()));
+
+	_sample_count.show_min_max_step(0, UINT64_MAX, 1);
+
+	set_capture_state(pv::SigSession::Stopped);
+
+	_configure_button.setIcon(QIcon::fromTheme("configure",
+		QIcon(":/icons/configure.png")));
+
+	_probes_button.setIcon(QIcon::fromTheme("probes",
+		QIcon(":/icons/probes.svg")));
+
+	_run_stop_button.setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+
+	addWidget(&_device_selector);
+	_configure_button_action = addWidget(&_configure_button);
+	addWidget(&_probes_button);
+	addWidget(&_sample_count);
+	addWidget(&_sample_rate);
+
+	addWidget(&_run_stop_button);
+}
+
+void SamplingBar::set_device_list(
+	const std::list< shared_ptr<pv::device::DevInst> > &devices,
+	shared_ptr<pv::device::DevInst> selected)
+{
+	int selected_index = -1;
+
+	assert(selected);
+
+	_updating_device_selector = true;
+
+	_device_selector.clear();
+	_device_selector_map.clear();
+
+	BOOST_FOREACH (shared_ptr<pv::device::DevInst> dev_inst, devices) {
+		assert(dev_inst);
+		const string title = dev_inst->format_device_title();
+		const sr_dev_inst *sdi = dev_inst->dev_inst();
+		assert(sdi);
+
+		if (selected == dev_inst)
+			selected_index = _device_selector.count();
+
+		_device_selector_map[sdi] = dev_inst;
+		_device_selector.addItem(title.c_str(),
+			qVariantFromValue((void*)sdi));
+	}
+
+	// The selected device should have been in the list
+	assert(selected_index != -1);
+	_device_selector.setCurrentIndex(selected_index);
+
+	update_device_config_widgets();
+
+	_updating_device_selector = false;
+}
+
+shared_ptr<pv::device::DevInst> SamplingBar::get_selected_device() const
+{
+	const int index = _device_selector.currentIndex();
+	if (index < 0)
+		return shared_ptr<pv::device::DevInst>();
+
+	const sr_dev_inst *const sdi =
+		(const sr_dev_inst*)_device_selector.itemData(
+			index).value<void*>();
+	assert(sdi);
+
+	map<const sr_dev_inst*, boost::weak_ptr<device::DevInst> >::
+		const_iterator iter = _device_selector_map.find(sdi);
+	if (iter == _device_selector_map.end())
+		return shared_ptr<pv::device::DevInst>();
+
+	return shared_ptr<pv::device::DevInst>((*iter).second);
+}
+
+void SamplingBar::set_capture_state(pv::SigSession::capture_state state)
+{
+	const QIcon *icons[] = {&_icon_grey, &_icon_red, &_icon_green};
+	_run_stop_button.setIcon(*icons[state]);
+	_run_stop_button.setText((state == pv::SigSession::Stopped) ?
+		tr("Run") : tr("Stop"));
+}
+
+void SamplingBar::update_sample_rate_selector()
+{
+	GVariant *gvar_dict, *gvar_list;
+	const uint64_t *elements = NULL;
+	gsize num_elements;
+
+	if (_updating_sample_rate)
+		return;
+
+	const shared_ptr<device::DevInst> dev_inst = get_selected_device();
+	if (!dev_inst)
+		return;
+
+	assert(!_updating_sample_rate);
+	_updating_sample_rate = true;
+
+	if (!(gvar_dict = dev_inst->list_config(NULL, SR_CONF_SAMPLERATE)))
+	{
+		_sample_rate.show_none();
+		_updating_sample_rate = false;
+		return;
+	}
+
+	if ((gvar_list = g_variant_lookup_value(gvar_dict,
+			"samplerate-steps", G_VARIANT_TYPE("at"))))
+	{
+		elements = (const uint64_t *)g_variant_get_fixed_array(
+				gvar_list, &num_elements, sizeof(uint64_t));
+
+		const uint64_t min = elements[0];
+		const uint64_t max = elements[1];
+		const uint64_t step = elements[2];
+
+		g_variant_unref(gvar_list);
+
+		assert(min > 0);
+		assert(max > 0);
+		assert(max > min);
+		assert(step > 0);
+
+		if (step == 1)
+			_sample_rate.show_125_list(min, max);
+		else
+		{
+			// When the step is not 1, we cam't make a 1-2-5-10
+			// list of sample rates, because we may not be able to
+			// make round numbers. Therefore in this case, show a
+			// spin box.
+			_sample_rate.show_min_max_step(min, max, step);
+		}
+	}
+	else if ((gvar_list = g_variant_lookup_value(gvar_dict,
+			"samplerates", G_VARIANT_TYPE("at"))))
+	{
+		elements = (const uint64_t *)g_variant_get_fixed_array(
+				gvar_list, &num_elements, sizeof(uint64_t));
+		_sample_rate.show_list(elements, num_elements);
+		g_variant_unref(gvar_list);
+	}
+	_updating_sample_rate = false;
+
+	g_variant_unref(gvar_dict);
+	update_sample_rate_selector_value();
+}
+
+void SamplingBar::update_sample_rate_selector_value()
+{
+	GVariant *gvar;
+	uint64_t samplerate;
+
+	if (_updating_sample_rate)
+		return;
+
+	const shared_ptr<device::DevInst> dev_inst = get_selected_device();
+	if (!dev_inst)
+		return;
+
+	if (!(gvar = dev_inst->get_config(NULL, SR_CONF_SAMPLERATE))) {
+		qDebug() << "WARNING: Failed to get value of sample rate";
+		return;
+	}
+	samplerate = g_variant_get_uint64(gvar);
+	g_variant_unref(gvar);
+
+	assert(!_updating_sample_rate);
+	_updating_sample_rate = true;
+	_sample_rate.set_value(samplerate);
+	_updating_sample_rate = false;
+}
+
+void SamplingBar::update_sample_count_selector()
+{
+	GVariant *gvar;
+
+	if (_updating_sample_count)
+		return;
+
+	const shared_ptr<device::DevInst> dev_inst = get_selected_device();
+	if (!dev_inst)
+		return;
+
+	assert(!_updating_sample_count);
+	_updating_sample_count = true;
+
+	if (_sample_count_supported)
+	{
+		uint64_t sample_count = _sample_count.value();
+		uint64_t min_sample_count = 0;
+		uint64_t max_sample_count = MaxSampleCount;
+
+		if (sample_count == 0)
+			sample_count = DefaultSampleCount;
+
+		if ((gvar = dev_inst->list_config(NULL, SR_CONF_LIMIT_SAMPLES)))
+		{
+			g_variant_get(gvar, "(tt)",
+				&min_sample_count, &max_sample_count);
+			g_variant_unref(gvar);
+		}
+
+		min_sample_count = min(max(min_sample_count, MinSampleCount),
+			max_sample_count);
+
+		_sample_count.show_125_list(
+			min_sample_count, max_sample_count);
+
+		if ((gvar = dev_inst->get_config(NULL, SR_CONF_LIMIT_SAMPLES)))
+		{
+			sample_count = g_variant_get_uint64(gvar);
+			if (sample_count == 0)
+				sample_count = DefaultSampleCount;
+			sample_count = min(max(sample_count, MinSampleCount),
+				max_sample_count);
+
+			g_variant_unref(gvar);
+		}
+
+		_sample_count.set_value(sample_count);
+	}
+	else
+		_sample_count.show_none();
+
+	_updating_sample_count = false;
+}
+
+void SamplingBar::update_device_config_widgets()
+{
+	GVariant *gvar;
+
+	using namespace pv::popups;
+
+	const shared_ptr<device::DevInst> dev_inst = get_selected_device();
+	if (!dev_inst)
+		return;
+
+	// Update the configure popup
+	DeviceOptions *const opts = new DeviceOptions(dev_inst, this);
+	_configure_button_action->setVisible(
+		!opts->binding().properties().empty());
+	_configure_button.set_popup(opts);
+
+	// Update the probes popup
+	Probes *const probes = new Probes(_session, this);
+	_probes_button.set_popup(probes);
+
+	// Update supported options.
+	_sample_count_supported = false;
+
+	if ((gvar = dev_inst->list_config(NULL, SR_CONF_DEVICE_OPTIONS)))
+	{
+		gsize num_opts;
+		const int *const options =
+			(const int32_t *)g_variant_get_fixed_array(
+			        gvar, &num_opts, sizeof(int32_t));
+		for (unsigned int i = 0; i < num_opts; i++)
+		{
+			switch (options[i]) {
+			case SR_CONF_LIMIT_SAMPLES:
+				_sample_count_supported = true;
+				break;
+			case SR_CONF_LIMIT_FRAMES:
+				dev_inst->set_config(NULL, SR_CONF_LIMIT_FRAMES,
+					g_variant_new_uint64(1));
+				break;
+			}
+		}
+	}
+
+	// Add notification of reconfigure events
+	disconnect(this, SLOT(on_config_changed()));
+	connect(dev_inst.get(), SIGNAL(config_changed()),
+		this, SLOT(on_config_changed()));
+
+	// Update sweep timing widgets.
+	update_sample_count_selector();
+	update_sample_rate_selector();
+}
+
+void SamplingBar::commit_sample_count()
+{
+	uint64_t sample_count = 0;
+
+	if (_updating_sample_count)
+		return;
+
+	const shared_ptr<device::DevInst> dev_inst = get_selected_device();
+	if (!dev_inst)
+		return;
+
+	sample_count = _sample_count.value();
+
+	// Set the sample count
+	assert(!_updating_sample_count);
+	_updating_sample_count = true;
+	if (_sample_count_supported &&
+		!dev_inst->set_config(NULL, SR_CONF_LIMIT_SAMPLES,
+		g_variant_new_uint64(sample_count))) {
+		qDebug() << "Failed to configure sample count.";
+		return;
+	}
+	_updating_sample_count = false;
+}
+
+void SamplingBar::commit_sample_rate()
+{
+	uint64_t sample_rate = 0;
+
+	if (_updating_sample_rate)
+		return;
+
+	const shared_ptr<device::DevInst> dev_inst = get_selected_device();
+	if (!dev_inst)
+		return;
+
+	sample_rate = _sample_rate.value();
+	if (sample_rate == 0)
+		return;
+
+	// Set the samplerate
+	assert(!_updating_sample_rate);
+	_updating_sample_rate = true;
+	if (!dev_inst->set_config(NULL, SR_CONF_SAMPLERATE,
+		g_variant_new_uint64(sample_rate))) {
+		qDebug() << "Failed to configure samplerate.";
+		return;
+	}
+	_updating_sample_rate = false;
+}
+
+void SamplingBar::on_device_selected()
+{
+	if (_updating_device_selector)
+		return;
+
+	const shared_ptr<device::DevInst> dev_inst = get_selected_device();
+	if (!dev_inst)
+		return;
+
+	_session.set_device(dev_inst);
+
+	update_device_config_widgets();
+}
+
+void SamplingBar::on_sample_count_changed()
+{
+	commit_sample_count();
+}
+
+void SamplingBar::on_sample_rate_changed()
+{
+	commit_sample_rate();
+}
+
+void SamplingBar::on_run_stop()
+{
+	commit_sample_count();
+	commit_sample_rate();	
+	run_stop();
+}
+
+void SamplingBar::on_config_changed()
+{
+	commit_sample_count();
+	update_sample_count_selector();	
+	commit_sample_rate();	
+	update_sample_rate_selector();
+}
+
+} // namespace toolbars
+} // namespace pv
diff --git a/pv/toolbars/samplingbar.h b/pv/toolbars/samplingbar.h
new file mode 100644
index 0000000..e59d2f9
--- /dev/null
+++ b/pv/toolbars/samplingbar.h
@@ -0,0 +1,121 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_TOOLBARS_SAMPLINGBAR_H
+#define PULSEVIEW_PV_TOOLBARS_SAMPLINGBAR_H
+
+#include <stdint.h>
+
+#include <list>
+#include <map>
+
+#include <boost/shared_ptr.hpp>
+
+#include <QComboBox>
+#include <QDoubleSpinBox>
+#include <QToolBar>
+#include <QToolButton>
+
+#include <pv/sigsession.h>
+#include <pv/widgets/popuptoolbutton.h>
+#include <pv/widgets/sweeptimingwidget.h>
+
+class QAction;
+
+namespace pv {
+
+class SigSession;
+
+namespace device {
+class DevInst;
+}
+
+namespace toolbars {
+
+class SamplingBar : public QToolBar
+{
+	Q_OBJECT
+
+private:
+	static const uint64_t MinSampleCount;
+	static const uint64_t MaxSampleCount;
+	static const uint64_t DefaultSampleCount;
+
+public:
+	SamplingBar(SigSession &session, QWidget *parent);
+
+	void set_device_list(
+		const std::list< boost::shared_ptr<pv::device::DevInst> >
+			&devices,
+		boost::shared_ptr<pv::device::DevInst> selected);
+
+	boost::shared_ptr<pv::device::DevInst> get_selected_device() const;
+
+	void set_capture_state(pv::SigSession::capture_state state);
+
+signals:
+	void run_stop();
+
+private:
+	void update_sample_rate_selector();
+	void update_sample_rate_selector_value();
+	void update_sample_count_selector();
+	void update_device_config_widgets();
+	void commit_sample_rate();
+	void commit_sample_count();
+
+private slots:
+	void on_device_selected();
+	void on_sample_count_changed();
+	void on_sample_rate_changed();
+	void on_run_stop();
+
+	void on_config_changed();
+
+private:
+	SigSession &_session;
+
+	QComboBox _device_selector;
+	std::map<const sr_dev_inst*, boost::weak_ptr<device::DevInst> >
+		_device_selector_map;
+	bool _updating_device_selector;
+
+	pv::widgets::PopupToolButton _configure_button;
+	QAction *_configure_button_action;
+
+	pv::widgets::PopupToolButton _probes_button;
+
+	pv::widgets::SweepTimingWidget _sample_count;
+	pv::widgets::SweepTimingWidget _sample_rate;
+	bool _updating_sample_rate;
+	bool _updating_sample_count;
+
+	bool _sample_count_supported;
+
+	QIcon _icon_red;
+	QIcon _icon_green;
+	QIcon _icon_grey;
+	QToolButton _run_stop_button;
+};
+
+} // namespace toolbars
+} // namespace pv
+
+#endif // PULSEVIEW_PV_TOOLBARS_SAMPLINGBAR_H
diff --git a/pv/view/analogsignal.cpp b/pv/view/analogsignal.cpp
new file mode 100644
index 0000000..c3f2738
--- /dev/null
+++ b/pv/view/analogsignal.cpp
@@ -0,0 +1,202 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <extdef.h>
+
+#include <math.h>
+
+#include "analogsignal.h"
+#include "pv/data/analog.h"
+#include "pv/data/analogsnapshot.h"
+#include "pv/view/view.h"
+
+using boost::shared_ptr;
+using std::max;
+using std::min;
+using std::deque;
+
+namespace pv {
+namespace view {
+
+const QColor AnalogSignal::SignalColours[4] = {
+	QColor(0xC4, 0xA0, 0x00),	// Yellow
+	QColor(0x87, 0x20, 0x7A),	// Magenta
+	QColor(0x20, 0x4A, 0x87),	// Blue
+	QColor(0x4E, 0x9A, 0x06)	// Green
+};
+
+const float AnalogSignal::EnvelopeThreshold = 256.0f;
+
+AnalogSignal::AnalogSignal(shared_ptr<pv::device::DevInst> dev_inst,
+	const sr_channel *const probe, shared_ptr<data::Analog> data) :
+	Signal(dev_inst, probe),
+	_data(data),
+	_scale(1.0f)
+{
+	_colour = SignalColours[probe->index % countof(SignalColours)];
+}
+
+AnalogSignal::~AnalogSignal()
+{
+}
+
+shared_ptr<pv::data::SignalData> AnalogSignal::data() const
+{
+	return _data;
+}
+
+shared_ptr<pv::data::Analog> AnalogSignal::analog_data() const
+{
+	return _data;
+}
+
+void AnalogSignal::set_scale(float scale)
+{
+	_scale = scale;
+}
+
+void AnalogSignal::paint_back(QPainter &p, int left, int right)
+{
+	if (_probe->enabled)
+		paint_axis(p, get_y(), left, right);
+}
+
+void AnalogSignal::paint_mid(QPainter &p, int left, int right)
+{
+	assert(_data);
+	assert(right >= left);
+
+	assert(_view);
+	const int y = _v_offset - _view->v_offset();
+
+	const double scale = _view->scale();
+	assert(scale > 0);
+
+	const double offset = _view->offset();
+
+	if (!_probe->enabled)
+		return;
+
+	const deque< shared_ptr<pv::data::AnalogSnapshot> > &snapshots =
+		_data->get_snapshots();
+	if (snapshots.empty())
+		return;
+
+	const shared_ptr<pv::data::AnalogSnapshot> &snapshot =
+		snapshots.front();
+
+	const double pixels_offset = offset / scale;
+	const double samplerate = _data->samplerate();
+	const double start_time = _data->get_start_time();
+	const int64_t last_sample = snapshot->get_sample_count() - 1;
+	const double samples_per_pixel = samplerate * scale;
+	const double start = samplerate * (offset - start_time);
+	const double end = start + samples_per_pixel * (right - left);
+
+	const int64_t start_sample = min(max((int64_t)floor(start),
+		(int64_t)0), last_sample);
+	const int64_t end_sample = min(max((int64_t)ceil(end) + 1,
+		(int64_t)0), last_sample);
+
+	if (samples_per_pixel < EnvelopeThreshold)
+		paint_trace(p, snapshot, y, left,
+			start_sample, end_sample,
+			pixels_offset, samples_per_pixel);
+	else
+		paint_envelope(p, snapshot, y, left,
+			start_sample, end_sample,
+			pixels_offset, samples_per_pixel);
+}
+
+void AnalogSignal::paint_trace(QPainter &p,
+	const shared_ptr<pv::data::AnalogSnapshot> &snapshot,
+	int y, int left, const int64_t start, const int64_t end,
+	const double pixels_offset, const double samples_per_pixel)
+{
+	const int64_t sample_count = end - start;
+
+	const float *const samples = snapshot->get_samples(start, end);
+	assert(samples);
+
+	p.setPen(_colour);
+
+	QPointF *points = new QPointF[sample_count];
+	QPointF *point = points;
+
+	for (int64_t sample = start; sample != end; sample++) {
+		const float x = (sample / samples_per_pixel -
+			pixels_offset) + left;
+		*point++ = QPointF(x,
+			y - samples[sample - start] * _scale);
+	}
+
+	p.drawPolyline(points, point - points);
+
+	delete[] samples;
+	delete[] points;
+}
+
+void AnalogSignal::paint_envelope(QPainter &p,
+	const shared_ptr<pv::data::AnalogSnapshot> &snapshot,
+	int y, int left, const int64_t start, const int64_t end,
+	const double pixels_offset, const double samples_per_pixel)
+{
+	using pv::data::AnalogSnapshot;
+
+	AnalogSnapshot::EnvelopeSection e;
+	snapshot->get_envelope_section(e, start, end, samples_per_pixel);
+
+	if (e.length < 2)
+		return;
+
+	p.setPen(QPen(Qt::NoPen));
+	p.setBrush(_colour);
+
+	QRectF *const rects = new QRectF[e.length];
+	QRectF *rect = rects;
+
+	for(uint64_t sample = 0; sample < e.length-1; sample++) {
+		const float x = ((e.scale * sample + e.start) /
+			samples_per_pixel - pixels_offset) + left;
+		const AnalogSnapshot::EnvelopeSample *const s =
+			e.samples + sample;
+
+		// We overlap this sample with the next so that vertical
+		// gaps do not appear during steep rising or falling edges
+		const float b = y - max(s->max, (s+1)->min) * _scale;
+		const float t = y - min(s->min, (s+1)->max) * _scale;
+
+		float h = b - t;
+		if(h >= 0.0f && h <= 1.0f)
+			h = 1.0f;
+		if(h <= 0.0f && h >= -1.0f)
+			h = -1.0f;
+
+		*rect++ = QRectF(x, t, 1.0f, h);
+	}
+
+	p.drawRects(rects, e.length);
+
+	delete[] rects;
+	delete[] e.samples;
+}
+
+} // namespace view
+} // namespace pv
diff --git a/pv/view/analogsignal.h b/pv/view/analogsignal.h
new file mode 100644
index 0000000..f22d128
--- /dev/null
+++ b/pv/view/analogsignal.h
@@ -0,0 +1,92 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_VIEW_ANALOGSIGNAL_H
+#define PULSEVIEW_PV_VIEW_ANALOGSIGNAL_H
+
+#include "signal.h"
+
+#include <boost/shared_ptr.hpp>
+
+namespace pv {
+
+namespace data {
+class Analog;
+class AnalogSnapshot;
+}
+
+namespace view {
+
+class AnalogSignal : public Signal
+{
+private:
+	static const QColor SignalColours[4];
+
+	static const float EnvelopeThreshold;
+
+public:
+	AnalogSignal(boost::shared_ptr<pv::device::DevInst> dev_inst,
+		const sr_channel *const probe,
+		boost::shared_ptr<pv::data::Analog> data);
+
+	virtual ~AnalogSignal();
+
+	boost::shared_ptr<pv::data::SignalData> data() const;
+
+	boost::shared_ptr<pv::data::Analog> analog_data() const;
+
+	void set_scale(float scale);
+
+	/**
+	 * Paints the background layer of the signal with a QPainter
+	 * @param p the QPainter to paint into.
+	 * @param left the x-coordinate of the left edge of the signal.
+	 * @param right the x-coordinate of the right edge of the signal.
+	 **/
+	void paint_back(QPainter &p, int left, int right);
+
+	/**
+	 * Paints the mid-layer of the signal with a QPainter
+	 * @param p the QPainter to paint into.
+	 * @param left the x-coordinate of the left edge of the signal.
+	 * @param right the x-coordinate of the right edge of the signal.
+	 **/
+	void paint_mid(QPainter &p, int left, int right);
+
+private:
+	void paint_trace(QPainter &p,
+		const boost::shared_ptr<pv::data::AnalogSnapshot> &snapshot,
+		int y, int left, const int64_t start, const int64_t end,
+		const double pixels_offset, const double samples_per_pixel);
+
+	void paint_envelope(QPainter &p,
+		const boost::shared_ptr<pv::data::AnalogSnapshot> &snapshot,
+		int y, int left, const int64_t start, const int64_t end,
+		const double pixels_offset, const double samples_per_pixel);
+
+private:
+	boost::shared_ptr<pv::data::Analog> _data;
+	float _scale;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_ANALOGSIGNAL_H
diff --git a/pv/view/cursor.cpp b/pv/view/cursor.cpp
new file mode 100644
index 0000000..ca22eef
--- /dev/null
+++ b/pv/view/cursor.cpp
@@ -0,0 +1,159 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "cursor.h"
+
+#include "ruler.h"
+#include "view.h"
+
+#include <QBrush>
+#include <QPainter>
+#include <QPointF>
+#include <QRect>
+#include <QRectF>
+
+#include <stdio.h>
+
+#include <extdef.h>
+
+using boost::shared_ptr;
+
+namespace pv {
+namespace view {
+
+const QColor Cursor::LineColour(32, 74, 135);
+const QColor Cursor::FillColour(52, 101, 164);
+const QColor Cursor::HighlightColour(83, 130, 186);
+const QColor Cursor::TextColour(Qt::white);
+
+const int Cursor::Offset = 1;
+
+const int Cursor::ArrowSize = 4;
+
+Cursor::Cursor(View &view, double time) :
+	TimeMarker(view, LineColour, time)
+{
+}
+
+QRectF Cursor::get_label_rect(const QRect &rect) const
+{
+	const shared_ptr<Cursor> other(get_other_cursor());
+	assert(other);
+
+	const float x = (_time - _view.offset()) / _view.scale();
+
+	const QSizeF label_size(
+		_text_size.width() + View::LabelPadding.width() * 2,
+		_text_size.height() + View::LabelPadding.height() * 2);
+	const float top = rect.height() - label_size.height() -
+		Cursor::Offset - Cursor::ArrowSize - 0.5f;
+	const float height = label_size.height();
+
+	if (_time > other->time())
+		return QRectF(x, top, label_size.width(), height);
+	else
+		return QRectF(x - label_size.width(), top,
+			label_size.width(), height);
+}
+
+void Cursor::paint_label(QPainter &p, const QRect &rect,
+	unsigned int prefix)
+{
+	const shared_ptr<Cursor> other(get_other_cursor());
+	assert(other);
+
+	compute_text_size(p, prefix);
+	const QRectF r(get_label_rect(rect));
+
+	const QPointF left_points[] = {
+		r.topLeft(),
+		r.topRight(),
+		r.bottomRight(),
+		QPointF(r.left() + ArrowSize, r.bottom()),
+		QPointF(r.left(), rect.bottom()),
+	};
+
+	const QPointF right_points[] = {
+		r.topRight(),
+		r.topLeft(),
+		r.bottomLeft(),
+		QPointF(r.right() - ArrowSize, r.bottom()),
+		QPointF(r.right(), rect.bottom()),
+	};
+
+	const QPointF left_highlight_points[] = {
+		QPointF(r.left() + 1, r.top() + 1),
+		QPointF(r.right() - 1, r.top() + 1),
+		QPointF(r.right() - 1, r.bottom() - 1),
+		QPointF(r.left() + ArrowSize - 1, r.bottom() - 1),
+		QPointF(r.left() + 1, rect.bottom() - 1),
+	};
+
+	const QPointF right_highlight_points[] = {
+		QPointF(r.right() - 1, r.top() + 1),
+		QPointF(r.left() + 1, r.top() + 1),
+		QPointF(r.left() + 1, r.bottom() - 1),
+		QPointF(r.right() - ArrowSize + 1, r.bottom() - 1),
+		QPointF(r.right() - 1, rect.bottom() - 1),
+	};
+
+	const QPointF *const points = (_time > other->time()) ?
+		left_points : right_points;
+	const QPointF *const highlight_points = (_time > other->time()) ?
+		left_highlight_points : right_highlight_points;
+
+	if (selected()) {
+		p.setPen(highlight_pen());
+		p.setBrush(Qt::transparent);
+		p.drawPolygon(points, countof(left_points));
+	}
+
+	p.setPen(Qt::transparent);
+	p.setBrush(FillColour);
+	p.drawPolygon(points, countof(left_points));
+
+	p.setPen(HighlightColour);
+	p.setBrush(Qt::transparent);
+	p.drawPolygon(highlight_points, countof(left_highlight_points));
+
+	p.setPen(LineColour);
+	p.setBrush(Qt::transparent);
+	p.drawPolygon(points, countof(left_points));
+
+	p.setPen(TextColour);
+	p.drawText(r, Qt::AlignCenter | Qt::AlignVCenter,
+		Ruler::format_time(_time, prefix, 2));
+}
+
+void Cursor::compute_text_size(QPainter &p, unsigned int prefix)
+{
+	_text_size = p.boundingRect(QRectF(), 0,
+		Ruler::format_time(_time, prefix, 2)).size();
+}
+
+shared_ptr<Cursor> Cursor::get_other_cursor() const
+{
+	const CursorPair &cursors = _view.cursors();
+	return (cursors.first().get() == this) ?
+		cursors.second() : cursors.first();
+}
+
+} // namespace view
+} // namespace pv
diff --git a/pv/view/cursor.h b/pv/view/cursor.h
new file mode 100644
index 0000000..290365e
--- /dev/null
+++ b/pv/view/cursor.h
@@ -0,0 +1,86 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_VIEW_CURSOR_H
+#define PULSEVIEW_PV_VIEW_CURSOR_H
+
+#include "timemarker.h"
+
+#include <boost/shared_ptr.hpp>
+
+#include <QSizeF>
+
+class QPainter;
+
+namespace pv {
+namespace view {
+
+class Cursor : public TimeMarker
+{
+	Q_OBJECT
+
+public:
+	static const QColor LineColour;
+	static const QColor FillColour;
+	static const QColor HighlightColour;
+	static const QColor TextColour;
+
+	static const int Offset;
+
+	static const int ArrowSize;
+
+public:
+	/**
+	 * Constructor.
+	 * @param view A reference to the view that owns this cursor pair.
+	 * @param time The time to set the flag to.
+	 */
+	Cursor(View &view, double time);
+
+public:
+	/**
+	 * Gets the marker label rectangle.
+	 * @param rect The rectangle of the ruler client area.
+	 * @return Returns the label rectangle.
+	 */
+	QRectF get_label_rect(const QRect &rect) const;
+
+	/**
+	 * Paints the cursor's label to the ruler.
+	 * @param p The painter to draw with.
+	 * @param rect The rectangle of the ruler client area.
+	 * @param prefix The index of the SI prefix to use.
+	 */
+	void paint_label(QPainter &p, const QRect &rect,
+		unsigned int prefix);
+
+private:
+	void compute_text_size(QPainter &p, unsigned int prefix);
+
+	boost::shared_ptr<Cursor> get_other_cursor() const;
+
+private:
+	QSizeF _text_size;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_CURSOR_H
diff --git a/pv/view/cursorpair.cpp b/pv/view/cursorpair.cpp
new file mode 100644
index 0000000..ed8829d
--- /dev/null
+++ b/pv/view/cursorpair.cpp
@@ -0,0 +1,155 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "cursorpair.h"
+
+#include "ruler.h"
+#include "view.h"
+
+#include <algorithm>
+
+using boost::shared_ptr;
+using std::max;
+using std::make_pair;
+using std::min;
+using std::pair;
+
+namespace pv {
+namespace view {
+
+const int CursorPair::DeltaPadding = 8;
+
+CursorPair::CursorPair(View &view) :
+	_first(new Cursor(view, 0.0)),
+	_second(new Cursor(view, 1.0)),
+	_view(view)
+{
+}
+
+shared_ptr<Cursor> CursorPair::first() const
+{
+	return _first;
+}
+
+shared_ptr<Cursor> CursorPair::second() const
+{
+	return _second;
+}
+
+QRectF CursorPair::get_label_rect(const QRect &rect) const
+{
+	const QSizeF label_size(
+		_text_size.width() + View::LabelPadding.width() * 2,
+		_text_size.height() + View::LabelPadding.height() * 2);
+	const pair<float, float> offsets(get_cursor_offsets());
+	const pair<float, float> normal_offsets(
+		(offsets.first < offsets.second) ? offsets :
+		make_pair(offsets.second, offsets.first));
+
+	const float height = label_size.height();
+	const float left = max(normal_offsets.first + DeltaPadding, -height);
+	const float right = min(normal_offsets.second - DeltaPadding,
+		(float)rect.width() + height);
+
+	return QRectF(left, rect.height() - label_size.height() -
+		Cursor::ArrowSize - Cursor::Offset - 0.5f,
+		right - left, height);
+}
+
+void CursorPair::draw_markers(QPainter &p,
+	const QRect &rect, unsigned int prefix)
+{
+	assert(_first);
+	assert(_second);
+
+	compute_text_size(p, prefix);
+	QRectF delta_rect(get_label_rect(rect));
+
+	const int radius = delta_rect.height() / 2;
+	const QRectF text_rect(delta_rect.intersected(
+		rect).adjusted(radius, 0, -radius, 0));
+	if(text_rect.width() >= _text_size.width())
+	{
+		const int highlight_radius = delta_rect.height() / 2 - 2;
+
+		p.setBrush(Cursor::FillColour);
+		p.setPen(Cursor::LineColour);
+		p.drawRoundedRect(delta_rect, radius, radius);
+
+		delta_rect.adjust(1, 1, -1, -1);
+		p.setPen(Cursor::HighlightColour);
+		p.drawRoundedRect(delta_rect, highlight_radius, highlight_radius);
+
+		p.setPen(Cursor::TextColour);
+		p.drawText(text_rect, Qt::AlignCenter | Qt::AlignVCenter,
+			Ruler::format_time(_second->time() - _first->time(), prefix, 2));
+	}
+
+	// Paint the cursor markers
+	_first->paint_label(p, rect, prefix);
+	_second->paint_label(p, rect, prefix);
+}
+
+void CursorPair::draw_viewport_background(QPainter &p,
+	const QRect &rect)
+{
+	p.setPen(Qt::NoPen);
+	p.setBrush(QBrush(View::CursorAreaColour));
+
+	const pair<float, float> offsets(get_cursor_offsets());
+	const int l = (int)max(min(
+		offsets.first, offsets.second), 0.0f);
+	const int r = (int)min(max(
+		offsets.first, offsets.second), (float)rect.width());
+
+	p.drawRect(l, 0, r - l, rect.height());
+}
+
+void CursorPair::draw_viewport_foreground(QPainter &p,
+	const QRect &rect)
+{
+	assert(_first);
+	assert(_second);
+
+	_first->paint(p, rect);
+	_second->paint(p, rect);
+}
+
+void CursorPair::compute_text_size(QPainter &p, unsigned int prefix)
+{
+	assert(_first);
+	assert(_second);
+
+	_text_size = p.boundingRect(QRectF(), 0, Ruler::format_time(
+		_second->time() - _first->time(), prefix, 2)).size();
+}
+
+pair<float, float> CursorPair::get_cursor_offsets() const
+{
+	assert(_first);
+	assert(_second);
+
+	return pair<float, float>(
+		(_first->time() - _view.offset()) / _view.scale(),
+		(_second->time() - _view.offset()) / _view.scale());
+}
+
+} // namespace view
+} // namespace pv
diff --git a/pv/view/cursorpair.h b/pv/view/cursorpair.h
new file mode 100644
index 0000000..815276e
--- /dev/null
+++ b/pv/view/cursorpair.h
@@ -0,0 +1,81 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_VIEW_CURSORPAIR_H
+#define PULSEVIEW_PV_VIEW_CURSORPAIR_H
+
+#include "cursor.h"
+
+#include <boost/shared_ptr.hpp>
+
+#include <QPainter>
+
+class QPainter;
+
+namespace pv {
+namespace view {
+
+class CursorPair
+{
+private:
+	static const int DeltaPadding;
+
+public:
+	/**
+	 * Constructor.
+	 * @param view A reference to the view that owns this cursor pair.
+	 */
+	CursorPair(View &view);
+
+	/**
+	 * Returns a pointer to the first cursor.
+	 */
+	boost::shared_ptr<Cursor> first() const;
+
+	/**
+	 * Returns a pointer to the second cursor.
+	 */
+	boost::shared_ptr<Cursor> second() const;
+
+public:
+	QRectF get_label_rect(const QRect &rect) const;
+
+	void draw_markers(QPainter &p,
+		const QRect &rect, unsigned int prefix);
+
+	void draw_viewport_background(QPainter &p, const QRect &rect);
+
+	void draw_viewport_foreground(QPainter &p, const QRect &rect);
+
+	void compute_text_size(QPainter &p, unsigned int prefix);
+
+	std::pair<float, float> get_cursor_offsets() const;
+
+private:
+	boost::shared_ptr<Cursor> _first, _second;
+	const View &_view;
+
+	QSizeF _text_size;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_CURSORPAIR_H
diff --git a/pv/view/decodetrace.cpp b/pv/view/decodetrace.cpp
new file mode 100644
index 0000000..618de9a
--- /dev/null
+++ b/pv/view/decodetrace.cpp
@@ -0,0 +1,725 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+extern "C" {
+#include <libsigrokdecode/libsigrokdecode.h>
+}
+
+#include <extdef.h>
+
+#include <boost/foreach.hpp>
+#include <boost/functional/hash.hpp>
+
+#include <QAction>
+#include <QApplication>
+#include <QComboBox>
+#include <QFormLayout>
+#include <QLabel>
+#include <QMenu>
+#include <QPushButton>
+
+#include "decodetrace.h"
+
+#include <pv/sigsession.h>
+#include <pv/data/decoderstack.h>
+#include <pv/data/decode/decoder.h>
+#include <pv/data/logic.h>
+#include <pv/data/logicsnapshot.h>
+#include <pv/data/decode/annotation.h>
+#include <pv/view/logicsignal.h>
+#include <pv/view/view.h>
+#include <pv/widgets/decodergroupbox.h>
+#include <pv/widgets/decodermenu.h>
+
+using boost::dynamic_pointer_cast;
+using boost::shared_ptr;
+using std::list;
+using std::max;
+using std::map;
+using std::min;
+using std::vector;
+
+namespace pv {
+namespace view {
+
+const QColor DecodeTrace::DecodeColours[4] = {
+	QColor(0xEF, 0x29, 0x29),	// Red
+	QColor(0xFC, 0xE9, 0x4F),	// Yellow
+	QColor(0x8A, 0xE2, 0x34),	// Green
+	QColor(0x72, 0x9F, 0xCF)	// Blue
+};
+
+const QColor DecodeTrace::ErrorBgColour = QColor(0xEF, 0x29, 0x29);
+const QColor DecodeTrace::NoDecodeColour = QColor(0x88, 0x8A, 0x85);
+
+const int DecodeTrace::ArrowSize = 4;
+const double DecodeTrace::EndCapWidth = 5;
+const int DecodeTrace::DrawPadding = 100;
+
+const QColor DecodeTrace::Colours[16] = {
+	QColor(0xEF, 0x29, 0x29),
+	QColor(0xF6, 0x6A, 0x32),
+	QColor(0xFC, 0xAE, 0x3E),
+	QColor(0xFB, 0xCA, 0x47),
+	QColor(0xFC, 0xE9, 0x4F),
+	QColor(0xCD, 0xF0, 0x40),
+	QColor(0x8A, 0xE2, 0x34),
+	QColor(0x4E, 0xDC, 0x44),
+	QColor(0x55, 0xD7, 0x95),
+	QColor(0x64, 0xD1, 0xD2),
+	QColor(0x72, 0x9F, 0xCF),
+	QColor(0xD4, 0x76, 0xC4),
+	QColor(0x9D, 0x79, 0xB9),
+	QColor(0xAD, 0x7F, 0xA8),
+	QColor(0xC2, 0x62, 0x9B),
+	QColor(0xD7, 0x47, 0x6F)
+};
+
+const QColor DecodeTrace::OutlineColours[16] = {
+	QColor(0x77, 0x14, 0x14),
+	QColor(0x7B, 0x35, 0x19),
+	QColor(0x7E, 0x57, 0x1F),
+	QColor(0x7D, 0x65, 0x23),
+	QColor(0x7E, 0x74, 0x27),
+	QColor(0x66, 0x78, 0x20),
+	QColor(0x45, 0x71, 0x1A),
+	QColor(0x27, 0x6E, 0x22),
+	QColor(0x2A, 0x6B, 0x4A),
+	QColor(0x32, 0x68, 0x69),
+	QColor(0x39, 0x4F, 0x67),
+	QColor(0x6A, 0x3B, 0x62),
+	QColor(0x4E, 0x3C, 0x5C),
+	QColor(0x56, 0x3F, 0x54),
+	QColor(0x61, 0x31, 0x4D),
+	QColor(0x6B, 0x23, 0x37)
+};
+
+DecodeTrace::DecodeTrace(pv::SigSession &session,
+	boost::shared_ptr<pv::data::DecoderStack> decoder_stack, int index) :
+	Trace(QString::fromUtf8(
+		decoder_stack->stack().front()->decoder()->name)),
+	_session(session),
+	_decoder_stack(decoder_stack),
+	_delete_mapper(this),
+	_show_hide_mapper(this)
+{
+	assert(_decoder_stack);
+
+	_colour = DecodeColours[index % countof(DecodeColours)];
+
+	connect(_decoder_stack.get(), SIGNAL(new_decode_data()),
+		this, SLOT(on_new_decode_data()));
+	connect(&_delete_mapper, SIGNAL(mapped(int)),
+		this, SLOT(on_delete_decoder(int)));
+	connect(&_show_hide_mapper, SIGNAL(mapped(int)),
+		this, SLOT(on_show_hide_decoder(int)));
+}
+
+bool DecodeTrace::enabled() const
+{
+	return true;
+}
+
+const boost::shared_ptr<pv::data::DecoderStack>& DecodeTrace::decoder() const
+{
+	return _decoder_stack;
+}
+
+void DecodeTrace::set_view(pv::view::View *view)
+{
+	assert(view);
+	Trace::set_view(view);
+}
+
+void DecodeTrace::paint_back(QPainter &p, int left, int right)
+{
+	Trace::paint_back(p, left, right);
+	paint_axis(p, get_y(), left, right);
+}
+
+void DecodeTrace::paint_mid(QPainter &p, int left, int right)
+{
+	using namespace pv::data::decode;
+
+	const double scale = _view->scale();
+	assert(scale > 0);
+
+	double samplerate = _decoder_stack->samplerate();
+
+	_cur_row_headings.clear();
+
+	// Show sample rate as 1Hz when it is unknown
+	if (samplerate == 0.0)
+		samplerate = 1.0;
+
+	const double pixels_offset = (_view->offset() -
+		_decoder_stack->get_start_time()) / scale;
+	const double samples_per_pixel = samplerate * scale;
+
+	const uint64_t start_sample = (uint64_t)max((left + pixels_offset) *
+		samples_per_pixel, 0.0);
+	const uint64_t end_sample = (uint64_t)max((right + pixels_offset) *
+		samples_per_pixel, 0.0);
+
+	QFontMetrics m(QApplication::font());
+	const int text_height =  m.boundingRect(QRect(), 0, "Tg").height();
+	const int annotation_height = (text_height * 5) / 4;
+	const int row_height = (text_height * 6) / 4;
+
+	assert(_decoder_stack);
+	const QString err = _decoder_stack->error_message();
+	if (!err.isEmpty())
+	{
+		draw_unresolved_period(p, annotation_height, left, right,
+			samples_per_pixel, pixels_offset);
+		draw_error(p, err, left, right);
+		return;
+	}
+
+	// Iterate through the rows
+	assert(_view);
+	int y = get_y();
+
+	assert(_decoder_stack);
+
+	const vector<Row> rows(_decoder_stack->get_visible_rows());
+	for (size_t i = 0; i < rows.size(); i++)
+	{
+		const Row &row = rows[i];
+
+		size_t base_colour = 0x13579BDF;
+		boost::hash_combine(base_colour, this);
+		boost::hash_combine(base_colour, row.decoder());
+		boost::hash_combine(base_colour, row.row());
+		base_colour >>= 16;
+
+		vector<Annotation> annotations;
+		_decoder_stack->get_annotation_subset(annotations, row,
+			start_sample, end_sample);
+		if (!annotations.empty()) {
+			BOOST_FOREACH(const Annotation &a, annotations)
+				draw_annotation(a, p, get_text_colour(),
+					annotation_height, left, right,
+					samples_per_pixel, pixels_offset, y,
+					base_colour);
+			y += row_height;
+
+			_cur_row_headings.push_back(row.title());
+		}
+	}
+
+	// Draw the hatching
+	draw_unresolved_period(p, annotation_height, left, right,
+		samples_per_pixel, pixels_offset);
+}
+
+void DecodeTrace::paint_fore(QPainter &p, int left, int right)
+{
+	using namespace pv::data::decode;
+
+	(void)right;
+
+	QFontMetrics m(QApplication::font());
+	const int text_height =  m.boundingRect(QRect(), 0, "Tg").height();
+	const int row_height = (text_height * 6) / 4;
+
+	for (size_t i = 0; i < _cur_row_headings.size(); i++)
+	{
+		const int y = i * row_height + get_y();
+
+		p.setPen(QPen(Qt::NoPen));
+		p.setBrush(QApplication::palette().brush(QPalette::WindowText));
+
+		if (i != 0)
+		{
+			const QPointF points[] = {
+				QPointF(left, y - ArrowSize),
+				QPointF(left + ArrowSize, y),
+				QPointF(left, y + ArrowSize)
+			};
+			p.drawPolygon(points, countof(points));
+		}
+
+		const QRect r(left + ArrowSize * 2, y - row_height / 2,
+			right - left, row_height);
+		const QString h(_cur_row_headings[i]);
+		const int f = Qt::AlignLeft | Qt::AlignVCenter |
+			Qt::TextDontClip;
+
+		// Draw the outline
+		p.setPen(QApplication::palette().color(QPalette::Base));
+		for (int dx = -1; dx <= 1; dx++)
+			for (int dy = -1; dy <= 1; dy++)
+				if (dx != 0 && dy != 0)
+					p.drawText(r.translated(dx, dy), f, h);
+
+		// Draw the text
+		p.setPen(QApplication::palette().color(QPalette::WindowText));
+		p.drawText(r, f, h);
+	}
+}
+
+void DecodeTrace::populate_popup_form(QWidget *parent, QFormLayout *form)
+{
+	using pv::data::decode::Decoder;
+
+	assert(form);
+	assert(parent);
+	assert(_decoder_stack);
+
+	// Add the standard options
+	Trace::populate_popup_form(parent, form);
+
+	// Add the decoder options
+	_bindings.clear();
+	_probe_selectors.clear();
+	_decoder_forms.clear();
+
+	const list< shared_ptr<Decoder> >& stack = _decoder_stack->stack();
+
+	if (stack.empty())
+	{
+		QLabel *const l = new QLabel(
+			tr("<p><i>No decoders in the stack</i></p>"));
+		l->setAlignment(Qt::AlignCenter);
+		form->addRow(l);
+	}
+	else
+	{
+		list< shared_ptr<Decoder> >::const_iterator iter =
+			stack.begin();
+		for (int i = 0; i < (int)stack.size(); i++, iter++) {
+			shared_ptr<Decoder> dec(*iter);
+			create_decoder_form(i, dec, parent, form);
+		}
+
+		form->addRow(new QLabel(
+			tr("<i>* Required channels</i>"), parent));
+	}
+
+	// Add stacking button
+	pv::widgets::DecoderMenu *const decoder_menu =
+		new pv::widgets::DecoderMenu(parent);
+	connect(decoder_menu, SIGNAL(decoder_selected(srd_decoder*)),
+		this, SLOT(on_stack_decoder(srd_decoder*)));
+
+	QPushButton *const stack_button =
+		new QPushButton(tr("Stack Decoder"), parent);
+	stack_button->setMenu(decoder_menu);
+
+	QHBoxLayout *stack_button_box = new QHBoxLayout;
+	stack_button_box->addWidget(stack_button, 0, Qt::AlignRight);
+	form->addRow(stack_button_box);
+}
+
+QMenu* DecodeTrace::create_context_menu(QWidget *parent)
+{
+	QMenu *const menu = Trace::create_context_menu(parent);
+
+	menu->addSeparator();
+
+	QAction *const del = new QAction(tr("Delete"), this);
+	del->setShortcuts(QKeySequence::Delete);
+	connect(del, SIGNAL(triggered()), this, SLOT(on_delete()));
+	menu->addAction(del);
+
+	return menu;
+}
+
+void DecodeTrace::draw_annotation(const pv::data::decode::Annotation &a,
+	QPainter &p, QColor text_color, int h, int left, int right,
+	double samples_per_pixel, double pixels_offset, int y,
+	size_t base_colour) const
+{
+	const double start = a.start_sample() / samples_per_pixel -
+		pixels_offset;
+	const double end = a.end_sample() / samples_per_pixel -
+		pixels_offset;
+
+	const size_t colour = (base_colour + a.format()) % countof(Colours);
+	const QColor &fill = Colours[colour];
+	const QColor &outline = OutlineColours[colour];
+
+	if (start > right + DrawPadding || end < left - DrawPadding)
+		return;
+
+	if (a.start_sample() == a.end_sample())
+		draw_instant(a, p, fill, outline, text_color, h,
+			start, y);
+	else
+		draw_range(a, p, fill, outline, text_color, h,
+			start, end, y);
+}
+
+void DecodeTrace::draw_instant(const pv::data::decode::Annotation &a, QPainter &p,
+	QColor fill, QColor outline, QColor text_color, int h, double x, int y) const
+{
+	const QString text = a.annotations().empty() ?
+		QString() : a.annotations().back();
+	const double w = min((double)p.boundingRect(QRectF(), 0, text).width(),
+		0.0) + h;
+	const QRectF rect(x - w / 2, y - h / 2, w, h);
+
+	p.setPen(outline);
+	p.setBrush(fill);
+	p.drawRoundedRect(rect, h / 2, h / 2);
+
+	p.setPen(text_color);
+	p.drawText(rect, Qt::AlignCenter | Qt::AlignVCenter, text);
+}
+
+void DecodeTrace::draw_range(const pv::data::decode::Annotation &a, QPainter &p,
+	QColor fill, QColor outline, QColor text_color, int h, double start,
+	double end, int y) const
+{
+	const double top = y + .5 - h / 2;
+	const double bottom = y + .5 + h / 2;
+	const vector<QString> annotations = a.annotations();
+
+	p.setPen(outline);
+	p.setBrush(fill);
+
+	// If the two ends are within 1 pixel, draw a vertical line
+	if (start + 1.0 > end)
+	{
+		p.drawLine(QPointF(start, top), QPointF(start, bottom));
+		return;
+	}
+
+	const double cap_width = min((end - start) / 4, EndCapWidth);
+
+	QPointF pts[] = {
+		QPointF(start, y + .5f),
+		QPointF(start + cap_width, top),
+		QPointF(end - cap_width, top),
+		QPointF(end, y + .5f),
+		QPointF(end - cap_width, bottom),
+		QPointF(start + cap_width, bottom)
+	};
+
+	p.drawConvexPolygon(pts, countof(pts));
+
+	if (annotations.empty())
+		return;
+
+	QRectF rect(start + cap_width, y - h / 2,
+		end - start - cap_width * 2, h);
+	if (rect.width() <= 4)
+		return;
+
+	p.setPen(text_color);
+
+	// Try to find an annotation that will fit
+	QString best_annotation;
+	int best_width = 0;
+
+	BOOST_FOREACH(const QString &a, annotations) {
+		const int w = p.boundingRect(QRectF(), 0, a).width();
+		if (w <= rect.width() && w > best_width)
+			best_annotation = a, best_width = w;
+	}
+
+	if (best_annotation.isEmpty())
+		best_annotation = annotations.back();
+
+	// If not ellide the last in the list
+	p.drawText(rect, Qt::AlignCenter, p.fontMetrics().elidedText(
+		best_annotation, Qt::ElideRight, rect.width()));
+}
+
+void DecodeTrace::draw_error(QPainter &p, const QString &message,
+	int left, int right)
+{
+	const int y = get_y();
+
+	p.setPen(ErrorBgColour.darker());
+	p.setBrush(ErrorBgColour);
+
+	const QRectF bounding_rect =
+		QRectF(left, INT_MIN / 2 + y, right - left, INT_MAX);
+	const QRectF text_rect = p.boundingRect(bounding_rect,
+		Qt::AlignCenter, message);
+	const float r = text_rect.height() / 4;
+
+	p.drawRoundedRect(text_rect.adjusted(-r, -r, r, r), r, r,
+		Qt::AbsoluteSize);
+
+	p.setPen(get_text_colour());
+	p.drawText(text_rect, message);
+}
+
+void DecodeTrace::draw_unresolved_period(QPainter &p, int h, int left,
+	int right, double samples_per_pixel, double pixels_offset) 
+{
+	using namespace pv::data;
+	using pv::data::decode::Decoder;
+
+	assert(_decoder_stack);	
+
+	shared_ptr<Logic> data;
+	shared_ptr<LogicSignal> logic_signal;
+
+	const list< shared_ptr<Decoder> > &stack = _decoder_stack->stack();
+
+	// We get the logic data of the first probe in the list.
+	// This works because we are currently assuming all
+	// LogicSignals have the same data/snapshot
+	BOOST_FOREACH (const shared_ptr<Decoder> &dec, stack)
+		if (dec && !dec->channels().empty() &&
+			((logic_signal = (*dec->channels().begin()).second)) &&
+			((data = logic_signal->logic_data())))
+			break;
+
+	if (!data || data->get_snapshots().empty())
+		return;
+
+	const shared_ptr<LogicSnapshot> snapshot =
+		data->get_snapshots().front();
+	assert(snapshot);
+	const int64_t sample_count = (int64_t)snapshot->get_sample_count();
+	if (sample_count == 0)
+		return;
+
+	const int64_t samples_decoded = _decoder_stack->samples_decoded();
+	if (sample_count == samples_decoded)
+		return;
+
+	const int y = get_y();
+	const double start = max(samples_decoded /
+		samples_per_pixel - pixels_offset, left - 1.0);
+	const double end = min(sample_count / samples_per_pixel -
+		pixels_offset, right + 1.0);
+	const QRectF no_decode_rect(start, y - h/2 + 0.5, end - start, h);
+
+	p.setPen(QPen(Qt::NoPen));
+	p.setBrush(Qt::white);
+	p.drawRect(no_decode_rect);
+
+	p.setPen(NoDecodeColour);
+	p.setBrush(QBrush(NoDecodeColour, Qt::Dense6Pattern));
+	p.drawRect(no_decode_rect);
+}
+
+void DecodeTrace::create_decoder_form(int index,
+	shared_ptr<data::decode::Decoder> &dec, QWidget *parent,
+	QFormLayout *form)
+{
+	const GSList *l;
+
+	assert(dec);
+	const srd_decoder *const decoder = dec->decoder();
+	assert(decoder);
+
+	pv::widgets::DecoderGroupBox *const group =
+		new pv::widgets::DecoderGroupBox(
+			QString::fromUtf8(decoder->name));
+	group->set_decoder_visible(dec->shown());
+
+	_delete_mapper.setMapping(group, index);
+	connect(group, SIGNAL(delete_decoder()), &_delete_mapper, SLOT(map()));
+
+	_show_hide_mapper.setMapping(group, index);
+	connect(group, SIGNAL(show_hide_decoder()),
+		&_show_hide_mapper, SLOT(map()));
+
+	QFormLayout *const decoder_form = new QFormLayout;
+	group->add_layout(decoder_form);
+
+	// Add the mandatory channels
+	for(l = decoder->channels; l; l = l->next) {
+		const struct srd_channel *const pdch =
+			(struct srd_channel *)l->data;
+		QComboBox *const combo = create_probe_selector(parent, dec, pdch);
+		connect(combo, SIGNAL(currentIndexChanged(int)),
+			this, SLOT(on_probe_selected(int)));
+		decoder_form->addRow(tr("<b>%1</b> (%2) *")
+			.arg(QString::fromUtf8(pdch->name))
+			.arg(QString::fromUtf8(pdch->desc)), combo);
+
+		const ProbeSelector s = {combo, dec, pdch};
+		_probe_selectors.push_back(s);
+	}
+
+	// Add the optional channels
+	for(l = decoder->opt_channels; l; l = l->next) {
+		const struct srd_channel *const pdch =
+			(struct srd_channel *)l->data;
+		QComboBox *const combo = create_probe_selector(parent, dec, pdch);
+		connect(combo, SIGNAL(currentIndexChanged(int)),
+			this, SLOT(on_probe_selected(int)));
+		decoder_form->addRow(tr("<b>%1</b> (%2)")
+			.arg(QString::fromUtf8(pdch->name))
+			.arg(QString::fromUtf8(pdch->desc)), combo);
+
+		const ProbeSelector s = {combo, dec, pdch};
+		_probe_selectors.push_back(s);
+	}
+
+	// Add the options
+	shared_ptr<prop::binding::DecoderOptions> binding(
+		new prop::binding::DecoderOptions(_decoder_stack, dec));
+	binding->add_properties_to_form(decoder_form, true);
+
+	_bindings.push_back(binding);
+
+	form->addRow(group);
+	_decoder_forms.push_back(group);
+}
+
+QComboBox* DecodeTrace::create_probe_selector(
+	QWidget *parent, const shared_ptr<data::decode::Decoder> &dec,
+	const srd_channel *const pdch)
+{
+	assert(dec);
+
+	const vector< shared_ptr<Signal> > sigs = _session.get_signals();
+
+	assert(_decoder_stack);
+	const map<const srd_channel*,
+		shared_ptr<LogicSignal> >::const_iterator probe_iter =
+		dec->channels().find(pdch);
+
+	QComboBox *selector = new QComboBox(parent);
+
+	selector->addItem("-", qVariantFromValue((void*)NULL));
+
+	if (probe_iter == dec->channels().end())
+		selector->setCurrentIndex(0);
+
+	for(size_t i = 0; i < sigs.size(); i++) {
+		const shared_ptr<view::Signal> s(sigs[i]);
+		assert(s);
+
+		if (dynamic_pointer_cast<LogicSignal>(s) && s->enabled())
+		{
+			selector->addItem(s->get_name(),
+				qVariantFromValue((void*)s.get()));
+			if ((*probe_iter).second == s)
+				selector->setCurrentIndex(i + 1);
+		}
+	}
+
+	return selector;
+}
+
+void DecodeTrace::commit_decoder_probes(shared_ptr<data::decode::Decoder> &dec)
+{
+	assert(dec);
+
+	map<const srd_channel*, shared_ptr<LogicSignal> > probe_map;
+	const vector< shared_ptr<Signal> > sigs = _session.get_signals();
+
+	BOOST_FOREACH(const ProbeSelector &s, _probe_selectors)
+	{
+		if(s._decoder != dec)
+			break;
+
+		const LogicSignal *const selection =
+			(LogicSignal*)s._combo->itemData(
+				s._combo->currentIndex()).value<void*>();
+
+		BOOST_FOREACH(shared_ptr<Signal> sig, sigs)
+			if(sig.get() == selection) {
+				probe_map[s._pdch] =
+					dynamic_pointer_cast<LogicSignal>(sig);
+				break;
+			}
+	}
+
+	dec->set_probes(probe_map);
+}
+
+void DecodeTrace::commit_probes()
+{
+	assert(_decoder_stack);
+	BOOST_FOREACH(shared_ptr<data::decode::Decoder> dec,
+		_decoder_stack->stack())
+		commit_decoder_probes(dec);
+
+	_decoder_stack->begin_decode();
+}
+
+void DecodeTrace::on_new_decode_data()
+{
+	if (_view)
+		_view->update_viewport();
+}
+
+void DecodeTrace::delete_pressed()
+{
+	on_delete();
+}
+
+void DecodeTrace::on_delete()
+{
+	_session.remove_decode_signal(this);
+}
+
+void DecodeTrace::on_probe_selected(int)
+{
+	commit_probes();
+}
+
+void DecodeTrace::on_stack_decoder(srd_decoder *decoder)
+{
+	assert(decoder);
+	assert(_decoder_stack);
+	_decoder_stack->push(shared_ptr<data::decode::Decoder>(
+		new data::decode::Decoder(decoder)));
+	_decoder_stack->begin_decode();
+
+	create_popup_form();
+}
+
+void DecodeTrace::on_delete_decoder(int index)
+{
+	_decoder_stack->remove(index);
+
+	// Update the popup
+	create_popup_form();	
+
+	_decoder_stack->begin_decode();
+}
+
+void DecodeTrace::on_show_hide_decoder(int index)
+{
+	using pv::data::decode::Decoder;
+
+	const list< shared_ptr<Decoder> > stack(_decoder_stack->stack());
+
+	// Find the decoder in the stack
+	list< shared_ptr<Decoder> >::const_iterator iter = stack.begin();
+	for(int i = 0; i < index; i++, iter++)
+		assert(iter != stack.end());
+
+	shared_ptr<Decoder> dec = *iter;
+	assert(dec);
+
+	const bool show = !dec->shown();
+	dec->show(show);
+
+	assert(index < (int)_decoder_forms.size());
+	_decoder_forms[index]->set_decoder_visible(show);
+
+	_view->update_viewport();
+}
+
+} // namespace view
+} // namespace pv
diff --git a/pv/view/decodetrace.h b/pv/view/decodetrace.h
new file mode 100644
index 0000000..9a991fc
--- /dev/null
+++ b/pv/view/decodetrace.h
@@ -0,0 +1,191 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_VIEW_DECODETRACE_H
+#define PULSEVIEW_PV_VIEW_DECODETRACE_H
+
+#include "trace.h"
+
+#include <list>
+#include <map>
+
+#include <QSignalMapper>
+
+#include <boost/shared_ptr.hpp>
+
+#include <pv/prop/binding/decoderoptions.h>
+
+struct srd_channel;
+struct srd_decoder;
+
+class QComboBox;
+
+namespace pv {
+
+class SigSession;
+
+namespace data {
+class DecoderStack;
+
+namespace decode {
+class Annotation;
+class Decoder;
+class Row;
+}
+}
+
+namespace widgets {
+class DecoderGroupBox;
+}
+
+namespace view {
+
+class DecodeTrace : public Trace
+{
+	Q_OBJECT
+
+private:
+	struct ProbeSelector
+	{
+		const QComboBox *_combo;
+		const boost::shared_ptr<pv::data::decode::Decoder> _decoder;
+		const srd_channel *_pdch;
+	};
+
+private:
+	static const QColor DecodeColours[4];
+	static const QColor ErrorBgColour;
+	static const QColor NoDecodeColour;
+
+	static const int ArrowSize;
+	static const double EndCapWidth;
+	static const int DrawPadding;
+
+	static const QColor Colours[16];
+	static const QColor OutlineColours[16];
+
+public:
+	DecodeTrace(pv::SigSession &session,
+		boost::shared_ptr<pv::data::DecoderStack> decoder_stack,
+		int index);
+
+	bool enabled() const;
+
+	const boost::shared_ptr<pv::data::DecoderStack>& decoder() const;
+
+	void set_view(pv::view::View *view);
+
+	/**
+	 * Paints the background layer of the trace with a QPainter
+	 * @param p the QPainter to paint into.
+	 * @param left the x-coordinate of the left edge of the signal.
+	 * @param right the x-coordinate of the right edge of the signal.
+	 **/
+	void paint_back(QPainter &p, int left, int right);
+
+	/**
+	 * Paints the mid-layer of the trace with a QPainter
+	 * @param p the QPainter to paint into.
+	 * @param left the x-coordinate of the left edge of the signal
+	 * @param right the x-coordinate of the right edge of the signal
+	 **/
+	void paint_mid(QPainter &p, int left, int right);
+
+	/**
+	 * Paints the foreground layer of the trace with a QPainter
+	 * @param p the QPainter to paint into.
+	 * @param left the x-coordinate of the left edge of the signal
+	 * @param right the x-coordinate of the right edge of the signal
+	 **/
+	void paint_fore(QPainter &p, int left, int right);
+
+	void populate_popup_form(QWidget *parent, QFormLayout *form);
+
+	QMenu* create_context_menu(QWidget *parent);
+
+	void delete_pressed();
+
+private:
+	void draw_annotation(const pv::data::decode::Annotation &a, QPainter &p,
+		QColor text_colour, int text_height, int left, int right,
+		double samples_per_pixel, double pixels_offset, int y,
+		size_t base_colour) const;
+
+	void draw_instant(const pv::data::decode::Annotation &a, QPainter &p,
+		QColor fill, QColor outline, QColor text_color, int h, double x,
+		int y) const;
+
+	void draw_range(const pv::data::decode::Annotation &a, QPainter &p,
+		QColor fill, QColor outline, QColor text_color, int h, double start,
+		double end, int y) const;
+
+	void draw_error(QPainter &p, const QString &message,
+		int left, int right);
+
+	void draw_unresolved_period(QPainter &p, int h, int left,
+		int right, double samples_per_pixel, double pixels_offset);
+
+	void create_decoder_form(int index,
+		boost::shared_ptr<pv::data::decode::Decoder> &dec,
+		QWidget *parent, QFormLayout *form);
+
+	QComboBox* create_probe_selector(QWidget *parent,
+		const boost::shared_ptr<pv::data::decode::Decoder> &dec,
+		const srd_channel *const pdch);
+
+	void commit_decoder_probes(
+		boost::shared_ptr<data::decode::Decoder> &dec);
+
+	void commit_probes();
+
+private slots:
+	void on_new_decode_data();
+
+	void on_delete();
+
+	void on_probe_selected(int);
+
+	void on_stack_decoder(srd_decoder *decoder);
+
+	void on_delete_decoder(int index);
+
+	void on_show_hide_decoder(int index);
+
+private:
+	pv::SigSession &_session;
+	boost::shared_ptr<pv::data::DecoderStack> _decoder_stack;
+
+	uint64_t _decode_start, _decode_end;
+
+	std::list< boost::shared_ptr<pv::prop::binding::DecoderOptions> >
+		_bindings;
+
+	std::list<ProbeSelector> _probe_selectors;
+	std::vector<pv::widgets::DecoderGroupBox*> _decoder_forms;
+
+	std::vector<QString> _cur_row_headings;
+
+	QSignalMapper _delete_mapper, _show_hide_mapper;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_DECODETRACE_H
diff --git a/pv/view/header.cpp b/pv/view/header.cpp
new file mode 100644
index 0000000..a790252
--- /dev/null
+++ b/pv/view/header.cpp
@@ -0,0 +1,304 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "header.h"
+#include "view.h"
+
+#include "signal.h"
+#include "../sigsession.h"
+
+#include <assert.h>
+
+#include <boost/foreach.hpp>
+
+#include <QApplication>
+#include <QMenu>
+#include <QMouseEvent>
+#include <QPainter>
+#include <QRect>
+
+#include <pv/widgets/popup.h>
+
+using boost::shared_ptr;
+using std::max;
+using std::make_pair;
+using std::pair;
+using std::vector;
+
+namespace pv {
+namespace view {
+
+const int Header::Padding = 12;
+
+Header::Header(View &parent) :
+	MarginWidget(parent),
+	_dragging(false)
+{
+	setFocusPolicy(Qt::ClickFocus);
+	setMouseTracking(true);
+
+	connect(&_view.session(), SIGNAL(signals_changed()),
+		this, SLOT(on_signals_changed()));
+
+	connect(&_view, SIGNAL(signals_moved()),
+		this, SLOT(on_signals_moved()));
+
+	// Trigger the initial event manually. The default device has signals
+	// which were created before this object came into being
+	on_signals_changed();
+}
+
+QSize Header::sizeHint() const
+{
+	int max_width = 0;
+
+	const vector< shared_ptr<Trace> > traces(_view.get_traces());
+	BOOST_FOREACH(shared_ptr<Trace> t, traces) {
+		assert(t);
+		max_width = max(max_width, (int)t->get_label_rect(0).width());
+	}
+
+	return QSize(max_width + Padding, 0);
+}
+
+shared_ptr<Trace> Header::get_mouse_over_trace(const QPoint &pt)
+{
+	const int w = width();
+	const vector< shared_ptr<Trace> > traces(_view.get_traces());
+
+	BOOST_FOREACH(const shared_ptr<Trace> t, traces)
+	{
+		assert(t);
+		if (t->pt_in_label_rect(0, w, pt))
+			return t;
+	}
+
+	return shared_ptr<Trace>();
+}
+
+void Header::clear_selection()
+{
+	const vector< shared_ptr<Trace> > traces(_view.get_traces());
+	BOOST_FOREACH(const shared_ptr<Trace> t, traces) {
+		assert(t);
+		t->select(false);
+	}
+
+	update();
+}
+
+void Header::paintEvent(QPaintEvent*)
+{
+	const int w = width();
+	const vector< shared_ptr<Trace> > traces(_view.get_traces());
+
+	QPainter painter(this);
+	painter.setRenderHint(QPainter::Antialiasing);
+
+	const bool dragging = !_drag_traces.empty();
+	BOOST_FOREACH(const shared_ptr<Trace> t, traces)
+	{
+		assert(t);
+
+		const bool highlight = !dragging && t->pt_in_label_rect(
+			0, w, _mouse_point);
+		t->paint_label(painter, w, highlight);
+	}
+
+	painter.end();
+}
+
+void Header::mousePressEvent(QMouseEvent *event)
+{
+	assert(event);
+
+	const vector< shared_ptr<Trace> > traces(_view.get_traces());
+
+	if (event->button() & Qt::LeftButton) {
+		_mouse_down_point = event->pos();
+
+		// Save the offsets of any signals which will be dragged
+		BOOST_FOREACH(const shared_ptr<Trace> t, traces)
+			if (t->selected())
+				_drag_traces.push_back(
+					make_pair(t, t->get_v_offset()));
+	}
+
+	// Select the signal if it has been clicked
+	const shared_ptr<Trace> mouse_over_trace =
+		get_mouse_over_trace(event->pos());
+	if (mouse_over_trace) {
+		if (mouse_over_trace->selected())
+			mouse_over_trace->select(false);
+		else {
+			mouse_over_trace->select(true);
+
+			if (~QApplication::keyboardModifiers() &
+				Qt::ControlModifier)
+				_drag_traces.clear();
+
+			// Add the signal to the drag list
+			if (event->button() & Qt::LeftButton)
+				_drag_traces.push_back(
+					make_pair(mouse_over_trace,
+					mouse_over_trace->get_v_offset()));
+		}
+	}
+
+	if (~QApplication::keyboardModifiers() & Qt::ControlModifier) {
+		// Unselect all other signals because the Ctrl is not
+		// pressed
+		BOOST_FOREACH(const shared_ptr<Trace> t, traces)
+			if (t != mouse_over_trace)
+				t->select(false);
+	}
+
+	selection_changed();
+	update();
+}
+
+void Header::mouseReleaseEvent(QMouseEvent *event)
+{
+	using pv::widgets::Popup;
+
+	assert(event);
+	if (event->button() == Qt::LeftButton) {
+		if (_dragging)
+			_view.normalize_layout();
+		else
+		{
+			const shared_ptr<Trace> mouse_over_trace =
+				get_mouse_over_trace(event->pos());
+			if (mouse_over_trace) {
+				Popup *const p =
+					mouse_over_trace->create_popup(&_view);
+				p->set_position(mapToGlobal(QPoint(width(),
+					mouse_over_trace->get_y())),
+					Popup::Right);
+				p->show();
+			}
+		}
+
+		_dragging = false;
+		_drag_traces.clear();
+	}
+}
+
+void Header::mouseMoveEvent(QMouseEvent *event)
+{
+	assert(event);
+	_mouse_point = event->pos();
+
+	if (!(event->buttons() & Qt::LeftButton))
+		return;
+
+	if ((event->pos() - _mouse_down_point).manhattanLength() <
+		QApplication::startDragDistance())
+		return;
+
+	// Move the signals if we are dragging
+	if (!_drag_traces.empty())
+	{
+		_dragging = true;
+
+		const int delta = event->pos().y() - _mouse_down_point.y();
+
+		for (std::list<std::pair<boost::weak_ptr<Trace>,
+			int> >::iterator i = _drag_traces.begin();
+			i != _drag_traces.end(); i++) {
+			const boost::shared_ptr<Trace> trace((*i).first);
+			if (trace) {
+				const int y = (*i).second + delta;
+				const int y_snap =
+					((y + View::SignalSnapGridSize / 2) /
+						View::SignalSnapGridSize) *
+						View::SignalSnapGridSize;
+				trace->set_v_offset(y_snap);
+
+				// Ensure the trace is selected
+				trace->select();
+			}
+			
+		}
+
+		signals_moved();
+	}
+
+	update();
+}
+
+void Header::leaveEvent(QEvent*)
+{
+	_mouse_point = QPoint(-1, -1);
+	update();
+}
+
+void Header::contextMenuEvent(QContextMenuEvent *event)
+{
+	const shared_ptr<Trace> t = get_mouse_over_trace(_mouse_point);
+
+	if (t)
+		t->create_context_menu(this)->exec(event->globalPos());
+}
+
+void Header::keyPressEvent(QKeyEvent *e)
+{
+	assert(e);
+
+	switch (e->key())
+	{
+	case Qt::Key_Delete:
+	{
+		const vector< shared_ptr<Trace> > traces(_view.get_traces());
+		BOOST_FOREACH(const shared_ptr<Trace> t, traces)
+			if (t->selected())
+				t->delete_pressed();	
+		break;
+	}
+	}
+}
+
+void Header::on_signals_changed()
+{
+	const vector< shared_ptr<Trace> > traces(_view.get_traces());
+	BOOST_FOREACH(shared_ptr<Trace> t, traces) {
+		assert(t);
+		connect(t.get(), SIGNAL(visibility_changed()),
+			this, SLOT(update()));
+		connect(t.get(), SIGNAL(text_changed()),
+			this, SLOT(on_trace_text_changed()));
+		connect(t.get(), SIGNAL(colour_changed()),
+			this, SLOT(update()));
+	}
+}
+
+void Header::on_signals_moved()
+{
+	update();
+}
+
+void Header::on_trace_text_changed()
+{
+	update();
+	geometry_updated();
+}
+
+} // namespace view
+} // namespace pv
diff --git a/pv/view/header.h b/pv/view/header.h
new file mode 100644
index 0000000..5474f10
--- /dev/null
+++ b/pv/view/header.h
@@ -0,0 +1,94 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_VIEW_HEADER_H
+#define PULSEVIEW_PV_VIEW_HEADER_H
+
+#include <boost/shared_ptr.hpp>
+#include <boost/weak_ptr.hpp>
+
+#include <list>
+#include <utility>
+
+#include "marginwidget.h"
+
+namespace pv {
+namespace view {
+
+class Trace;
+class View;
+
+class Header : public MarginWidget
+{
+	Q_OBJECT
+
+private:
+	static const int Padding;
+
+public:
+	Header(View &parent);
+
+	QSize sizeHint() const;
+
+private:
+	boost::shared_ptr<pv::view::Trace> get_mouse_over_trace(
+		const QPoint &pt);
+
+	void clear_selection();
+
+private:
+	void paintEvent(QPaintEvent *event);
+
+private:
+	void mousePressEvent(QMouseEvent * event);
+
+	void mouseReleaseEvent(QMouseEvent *event);
+
+	void mouseMoveEvent(QMouseEvent *event);
+
+	void leaveEvent(QEvent *event);
+
+	void contextMenuEvent(QContextMenuEvent *event);
+
+	void keyPressEvent(QKeyEvent *e);
+
+private slots:
+	void on_signals_changed();
+
+	void on_signals_moved();
+
+	void on_trace_text_changed();
+
+signals:
+	void signals_moved();
+
+private:
+	QPoint _mouse_point;
+	QPoint _mouse_down_point;
+	bool _dragging;
+
+	std::list<std::pair<boost::weak_ptr<Trace>, int> >
+		_drag_traces;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_HEADER_H
diff --git a/pv/view/logicsignal.cpp b/pv/view/logicsignal.cpp
new file mode 100644
index 0000000..0b44264
--- /dev/null
+++ b/pv/view/logicsignal.cpp
@@ -0,0 +1,353 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <extdef.h>
+
+#include <math.h>
+
+#include <QFormLayout>
+#include <QToolBar>
+
+#include "logicsignal.h"
+#include "view.h"
+
+#include <pv/sigsession.h>
+#include <pv/device/devinst.h>
+#include <pv/data/logic.h>
+#include <pv/data/logicsnapshot.h>
+#include <pv/view/view.h>
+
+using boost::shared_ptr;
+using std::deque;
+using std::max;
+using std::min;
+using std::pair;
+using std::vector;
+
+namespace pv {
+namespace view {
+
+const float LogicSignal::Oversampling = 2.0f;
+
+const QColor LogicSignal::EdgeColour(0x80, 0x80, 0x80);
+const QColor LogicSignal::HighColour(0x00, 0xC0, 0x00);
+const QColor LogicSignal::LowColour(0xC0, 0x00, 0x00);
+
+const QColor LogicSignal::SignalColours[10] = {
+	QColor(0x16, 0x19, 0x1A),	// Black
+	QColor(0x8F, 0x52, 0x02),	// Brown
+	QColor(0xCC, 0x00, 0x00),	// Red
+	QColor(0xF5, 0x79, 0x00),	// Orange
+	QColor(0xED, 0xD4, 0x00),	// Yellow
+	QColor(0x73, 0xD2, 0x16),	// Green
+	QColor(0x34, 0x65, 0xA4),	// Blue
+	QColor(0x75, 0x50, 0x7B),	// Violet
+	QColor(0x88, 0x8A, 0x85),	// Grey
+	QColor(0xEE, 0xEE, 0xEC),	// White
+};
+
+LogicSignal::LogicSignal(shared_ptr<pv::device::DevInst> dev_inst,
+	const sr_channel *const probe, shared_ptr<data::Logic> data) :
+	Signal(dev_inst, probe),
+	_data(data),
+	_trigger_none(NULL),
+	_trigger_rising(NULL),
+	_trigger_high(NULL),
+	_trigger_falling(NULL),
+	_trigger_low(NULL),
+	_trigger_change(NULL)
+{
+	_colour = SignalColours[probe->index % countof(SignalColours)];
+}
+
+LogicSignal::~LogicSignal()
+{
+}
+
+shared_ptr<pv::data::SignalData> LogicSignal::data() const
+{
+	return _data;
+}
+
+shared_ptr<pv::data::Logic> LogicSignal::logic_data() const
+{
+	return _data;
+}
+
+void LogicSignal::paint_back(QPainter &p, int left, int right)
+{
+	if (_probe->enabled)
+		paint_axis(p, get_y(), left, right);
+}
+
+void LogicSignal::paint_mid(QPainter &p, int left, int right)
+{
+	using pv::view::View;
+
+	QLineF *line;
+
+	vector< pair<int64_t, bool> > edges;
+
+	assert(_probe);
+	assert(_data);
+	assert(right >= left);
+
+	assert(_view);
+	const int y = _v_offset - _view->v_offset();
+	
+	const double scale = _view->scale();
+	assert(scale > 0);
+	
+	const double offset = _view->offset();
+
+	if (!_probe->enabled)
+		return;
+
+	const float high_offset = y - View::SignalHeight + 0.5f;
+	const float low_offset = y + 0.5f;
+
+	const deque< shared_ptr<pv::data::LogicSnapshot> > &snapshots =
+		_data->get_snapshots();
+	if (snapshots.empty())
+		return;
+
+	const shared_ptr<pv::data::LogicSnapshot> &snapshot =
+		snapshots.front();
+
+	double samplerate = _data->samplerate();
+
+	// Show sample rate as 1Hz when it is unknown
+	if (samplerate == 0.0)
+		samplerate = 1.0;
+
+	const double pixels_offset = offset / scale;
+	const double start_time = _data->get_start_time();
+	const int64_t last_sample = snapshot->get_sample_count() - 1;
+	const double samples_per_pixel = samplerate * scale;
+	const double start = samplerate * (offset - start_time);
+	const double end = start + samples_per_pixel * (right - left);
+
+	snapshot->get_subsampled_edges(edges,
+		min(max((int64_t)floor(start), (int64_t)0), last_sample),
+		min(max((int64_t)ceil(end), (int64_t)0), last_sample),
+		samples_per_pixel / Oversampling, _probe->index);
+	assert(edges.size() >= 2);
+
+	// Paint the edges
+	const unsigned int edge_count = edges.size() - 2;
+	QLineF *const edge_lines = new QLineF[edge_count];
+	line = edge_lines;
+
+	for (vector<pv::data::LogicSnapshot::EdgePair>::const_iterator i =
+			edges.begin() + 1;
+		i != edges.end() - 1; i++) {
+		const float x = ((*i).first / samples_per_pixel -
+			pixels_offset) + left;
+		*line++ = QLineF(x, high_offset, x, low_offset);
+	}
+
+	p.setPen(EdgeColour);
+	p.drawLines(edge_lines, edge_count);
+	delete[] edge_lines;
+
+	// Paint the caps
+	const unsigned int max_cap_line_count = edges.size();
+	QLineF *const cap_lines = new QLineF[max_cap_line_count];
+
+	p.setPen(HighColour);
+	paint_caps(p, cap_lines, edges, true, samples_per_pixel,
+		pixels_offset, left, high_offset);
+	p.setPen(LowColour);
+	paint_caps(p, cap_lines, edges, false, samples_per_pixel,
+		pixels_offset, left, low_offset);
+
+	delete[] cap_lines;
+}
+
+void LogicSignal::paint_caps(QPainter &p, QLineF *const lines,
+	vector< pair<int64_t, bool> > &edges, bool level,
+	double samples_per_pixel, double pixels_offset, float x_offset,
+	float y_offset)
+{
+	QLineF *line = lines;
+
+	for (vector<pv::data::LogicSnapshot::EdgePair>::const_iterator i =
+		edges.begin(); i != (edges.end() - 1); i++)
+		if ((*i).second == level) {
+			*line++ = QLineF(
+				((*i).first / samples_per_pixel -
+					pixels_offset) + x_offset, y_offset,
+				((*(i+1)).first / samples_per_pixel -
+					pixels_offset) + x_offset, y_offset);
+		}
+
+	p.drawLines(lines, line - lines);
+}
+
+void LogicSignal::init_trigger_actions(QWidget *parent)
+{
+	_trigger_none = new QAction(QIcon(":/icons/trigger-none.svg"),
+		tr("No trigger"), parent);
+	_trigger_none->setCheckable(true);
+	connect(_trigger_none, SIGNAL(triggered()),
+		this, SLOT(on_trigger_none()));
+
+	_trigger_rising = new QAction(QIcon(":/icons/trigger-rising.svg"),
+		tr("Trigger on rising edge"), parent);
+	_trigger_rising->setCheckable(true);
+	connect(_trigger_rising, SIGNAL(triggered()),
+		this, SLOT(on_trigger_rising()));
+
+	_trigger_high = new QAction(QIcon(":/icons/trigger-high.svg"),
+		tr("Trigger on high level"), parent);
+	_trigger_high->setCheckable(true);
+	connect(_trigger_high, SIGNAL(triggered()),
+		this, SLOT(on_trigger_high()));
+
+	_trigger_falling = new QAction(QIcon(":/icons/trigger-falling.svg"),
+		tr("Trigger on falling edge"), parent);
+	_trigger_falling->setCheckable(true);
+	connect(_trigger_falling, SIGNAL(triggered()),
+		this, SLOT(on_trigger_falling()));
+
+	_trigger_low = new QAction(QIcon(":/icons/trigger-low.svg"),
+		tr("Trigger on low level"), parent);
+	_trigger_low->setCheckable(true);
+	connect(_trigger_low, SIGNAL(triggered()),
+		this, SLOT(on_trigger_low()));
+
+	_trigger_change = new QAction(QIcon(":/icons/trigger-change.svg"),
+		tr("Trigger on rising or falling edge"), parent);
+	_trigger_change->setCheckable(true);
+	connect(_trigger_change, SIGNAL(triggered()),
+		this, SLOT(on_trigger_change()));
+}
+
+void LogicSignal::populate_popup_form(QWidget *parent, QFormLayout *form)
+{
+	GVariant *gvar;
+
+	Signal::populate_popup_form(parent, form);
+
+	// Add the trigger actions
+	assert(_dev_inst);
+	if ((gvar = _dev_inst->list_config(NULL, SR_CONF_TRIGGER_TYPE)))
+	{
+		const char *const trig_types =
+			g_variant_get_string(gvar, NULL);
+
+		if (trig_types && trig_types[0] != '\0')
+		{
+			_trigger_bar = new QToolBar(parent);
+
+			init_trigger_actions(_trigger_bar);
+			_trigger_bar->addAction(_trigger_none);
+			add_trigger_action(trig_types, 'r', _trigger_rising);
+			add_trigger_action(trig_types, '1', _trigger_high);
+			add_trigger_action(trig_types, 'f', _trigger_falling);
+			add_trigger_action(trig_types, '0', _trigger_low);
+			add_trigger_action(trig_types, 'c', _trigger_change);
+		
+			update_trigger_actions();
+
+			form->addRow(tr("Trigger"), _trigger_bar);
+		}
+
+		g_variant_unref(gvar);
+	}
+}
+
+void LogicSignal::add_trigger_action(const char *trig_types, char type,
+	QAction *action)
+{
+	while(*trig_types)
+		if(*trig_types++ == type) {
+			_trigger_bar->addAction(action);
+			break;
+		}
+}
+
+void LogicSignal::update_trigger_actions()
+{
+	const char cur_trigger = _probe->trigger ?
+		_probe->trigger[0] : '\0';
+	_trigger_none->setChecked(cur_trigger == '\0');
+	_trigger_rising->setChecked(cur_trigger == 'r');
+	_trigger_high->setChecked(cur_trigger == '1');
+	_trigger_falling->setChecked(cur_trigger == 'f');
+	_trigger_low->setChecked(cur_trigger == '0');
+	_trigger_change->setChecked(cur_trigger == 'c');
+}
+
+void LogicSignal::set_trigger(char type)
+{
+	const char trigger_type_string[2] = {type, 0};
+	const char *const trigger_string =
+		(type != 0) ? trigger_type_string : NULL;
+
+	assert(_dev_inst);
+	const sr_dev_inst *const sdi = _dev_inst->dev_inst();
+	assert(sdi);
+
+	const int probe_count = g_slist_length(sdi->channels);
+	assert(probe_count > 0);
+
+	assert(_probe && _probe->index < probe_count);
+
+	for (int i = 0; i < probe_count; i++) {
+		sr_dev_trigger_set(sdi, i, (i == _probe->index) ?
+			trigger_string : NULL);
+	}
+
+	update_trigger_actions();
+}
+
+void LogicSignal::on_trigger_none()
+{
+	set_trigger('\0');	
+}
+
+void LogicSignal::on_trigger_rising()
+{
+	set_trigger('r');	
+}
+
+void LogicSignal::on_trigger_high()
+{
+	set_trigger('1');	
+}
+
+void LogicSignal::on_trigger_falling()
+{
+	set_trigger('f');	
+}
+
+void LogicSignal::on_trigger_low()
+{
+	set_trigger('0');	
+}
+
+void LogicSignal::on_trigger_change()
+{
+	set_trigger('c');	
+}
+
+} // namespace view
+} // namespace pv
diff --git a/pv/view/logicsignal.h b/pv/view/logicsignal.h
new file mode 100644
index 0000000..c3e1666
--- /dev/null
+++ b/pv/view/logicsignal.h
@@ -0,0 +1,119 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_VIEW_LOGICSIGNAL_H
+#define PULSEVIEW_PV_VIEW_LOGICSIGNAL_H
+
+#include "signal.h"
+
+#include <boost/shared_ptr.hpp>
+
+class QToolBar;
+
+namespace pv {
+
+namespace data {
+class Logic;
+}
+
+namespace view {
+
+class LogicSignal : public Signal
+{
+	Q_OBJECT
+
+private:
+	static const float Oversampling;
+
+	static const QColor EdgeColour;
+	static const QColor HighColour;
+	static const QColor LowColour;
+
+	static const QColor SignalColours[10];
+
+public:
+	LogicSignal(boost::shared_ptr<pv::device::DevInst> dev_inst,
+		const sr_channel *const probe,
+		boost::shared_ptr<pv::data::Logic> data);
+
+	virtual ~LogicSignal();
+
+	boost::shared_ptr<pv::data::SignalData> data() const;
+
+	boost::shared_ptr<pv::data::Logic> logic_data() const;
+
+	/**
+	 * Paints the background layer of the signal with a QPainter
+	 * @param p the QPainter to paint into.
+	 * @param left the x-coordinate of the left edge of the signal.
+	 * @param right the x-coordinate of the right edge of the signal.
+	 **/
+	void paint_back(QPainter &p, int left, int right);
+
+	/**
+	 * Paints the mid-layer of the signal with a QPainter
+	 * @param p the QPainter to paint into.
+	 * @param left the x-coordinate of the left edge of the signal.
+	 * @param right the x-coordinate of the right edge of the signal.
+	 **/
+	void paint_mid(QPainter &p, int left, int right);
+
+private:
+
+	void paint_caps(QPainter &p, QLineF *const lines,
+		std::vector< std::pair<int64_t, bool> > &edges,
+		bool level, double samples_per_pixel, double pixels_offset,
+		float x_offset, float y_offset);
+
+	void init_trigger_actions(QWidget *parent);
+
+	void populate_popup_form(QWidget *parent, QFormLayout *form);
+	
+	void add_trigger_action(const char *trig_types, char type,
+		QAction *action);
+
+	void update_trigger_actions();
+
+	void set_trigger(char type);
+
+private slots:
+	void on_trigger_none();
+	void on_trigger_rising();
+	void on_trigger_high();
+	void on_trigger_falling();
+	void on_trigger_low();
+	void on_trigger_change();
+
+private:
+	boost::shared_ptr<pv::data::Logic> _data;
+
+	QToolBar *_trigger_bar;
+	QAction *_trigger_none;
+	QAction *_trigger_rising;
+	QAction *_trigger_high;
+	QAction *_trigger_falling;
+	QAction *_trigger_low;
+	QAction *_trigger_change;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_LOGICSIGNAL_H
diff --git a/pv/view/marginwidget.cpp b/pv/view/marginwidget.cpp
new file mode 100644
index 0000000..539551d
--- /dev/null
+++ b/pv/view/marginwidget.cpp
@@ -0,0 +1,35 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "view.h"
+
+#include "marginwidget.h"
+
+namespace pv {
+namespace view {
+
+MarginWidget::MarginWidget(View &parent) :
+	QWidget(&parent),
+	_view(parent)
+{
+}
+
+} // namespace view
+} // namespace pv
diff --git a/pv/view/marginwidget.h b/pv/view/marginwidget.h
new file mode 100644
index 0000000..42dffa7
--- /dev/null
+++ b/pv/view/marginwidget.h
@@ -0,0 +1,53 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_MARGINWIDGET_H
+#define PULSEVIEW_PV_MARGINWIDGET_H
+
+#include <QWidget>
+
+namespace pv {
+namespace view {
+
+class View;
+
+class MarginWidget : public QWidget
+{
+	Q_OBJECT
+
+public:
+	MarginWidget(pv::view::View &parent);
+
+public slots:
+	virtual void clear_selection() = 0;
+
+signals:
+	void selection_changed();
+
+	void geometry_updated();
+
+protected:
+	pv::view::View &_view;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_MARGINWIDGET_H
diff --git a/pv/view/ruler.cpp b/pv/view/ruler.cpp
new file mode 100644
index 0000000..aec3de9
--- /dev/null
+++ b/pv/view/ruler.cpp
@@ -0,0 +1,275 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "ruler.h"
+
+#include "cursor.h"
+#include "view.h"
+#include "viewport.h"
+
+#include <extdef.h>
+
+#include <assert.h>
+#include <math.h>
+#include <limits.h>
+
+#include <QApplication>
+#include <QMouseEvent>
+#include <QPainter>
+#include <QTextStream>
+
+#include <pv/widgets/popup.h>
+
+using namespace Qt;
+using boost::shared_ptr;
+
+namespace pv {
+namespace view {
+
+const int Ruler::RulerHeight = 30;
+const int Ruler::MinorTickSubdivision = 4;
+const int Ruler::ScaleUnits[3] = {1, 2, 5};
+
+const QString Ruler::SIPrefixes[9] =
+	{"f", "p", "n", QChar(0x03BC), "m", "", "k", "M", "G"};
+const int Ruler::FirstSIPrefixPower = -15;
+
+const int Ruler::HoverArrowSize = 5;
+
+Ruler::Ruler(View &parent) :
+	MarginWidget(parent),
+	_dragging(false)
+{
+	setMouseTracking(true);
+
+	connect(&_view, SIGNAL(hover_point_changed()),
+		this, SLOT(hover_point_changed()));
+}
+
+void Ruler::clear_selection()
+{
+	CursorPair &cursors = _view.cursors();
+	cursors.first()->select(false);
+	cursors.second()->select(false);
+	update();
+}
+
+QString Ruler::format_time(double t, unsigned int prefix,
+	unsigned int precision)
+{
+	const double multiplier = pow(10.0,
+		(int)- prefix * 3 - FirstSIPrefixPower);
+
+	QString s;
+	QTextStream ts(&s);
+	ts.setRealNumberPrecision(precision);
+	ts << fixed << forcesign << (t  * multiplier) <<
+		SIPrefixes[prefix] << "s";
+	return s;
+}
+
+QSize Ruler::sizeHint() const
+{
+	return QSize(0, RulerHeight);
+}
+
+void Ruler::paintEvent(QPaintEvent*)
+{
+
+	const double SpacingIncrement = 32.0f;
+	const double MinValueSpacing = 32.0f;
+	const int ValueMargin = 3;
+
+	QPainter p(this);
+	p.setRenderHint(QPainter::Antialiasing);
+
+	double min_width = SpacingIncrement, typical_width;
+	double tick_period;
+	unsigned int prefix;
+
+	// Find tick spacing, and number formatting that does not cause
+	// value to collide.
+	do
+	{
+		const double min_period = _view.scale() * min_width;
+
+		const int order = (int)floorf(log10f(min_period));
+		const double order_decimal = pow(10.0, order);
+
+		unsigned int unit = 0;
+
+		do
+		{
+			tick_period = order_decimal * ScaleUnits[unit++];
+		} while (tick_period < min_period && unit < countof(ScaleUnits));
+
+		prefix = (order - FirstSIPrefixPower) / 3;
+		assert(prefix < countof(SIPrefixes));
+
+
+		typical_width = p.boundingRect(0, 0, INT_MAX, INT_MAX,
+			AlignLeft | AlignTop, format_time(_view.offset(),
+			prefix)).width() + MinValueSpacing;
+
+		min_width += SpacingIncrement;
+
+	} while(typical_width > tick_period / _view.scale());
+
+	const int text_height = p.boundingRect(0, 0, INT_MAX, INT_MAX,
+		AlignLeft | AlignTop, "8").height();
+
+	// Draw the tick marks
+	p.setPen(palette().color(foregroundRole()));
+
+	const double minor_tick_period = tick_period / MinorTickSubdivision;
+	const double first_major_division =
+		floor(_view.offset() / tick_period);
+	const double first_minor_division =
+		ceil(_view.offset() / minor_tick_period);
+	const double t0 = first_major_division * tick_period;
+
+	int division = (int)round(first_minor_division -
+		first_major_division * MinorTickSubdivision) - 1;
+
+	const int major_tick_y1 = text_height + ValueMargin * 2;
+	const int tick_y2 = height();
+	const int minor_tick_y1 = (major_tick_y1 + tick_y2) / 2;
+
+	double x;
+
+	do {
+		const double t = t0 + division * minor_tick_period;
+		x = (t - _view.offset()) / _view.scale();
+
+		if (division % MinorTickSubdivision == 0)
+		{
+			// Draw a major tick
+			p.drawText(x, ValueMargin, 0, text_height,
+				AlignCenter | AlignTop | TextDontClip,
+				format_time(t, prefix));
+			p.drawLine(QPointF(x, major_tick_y1),
+				QPointF(x, tick_y2));
+		}
+		else
+		{
+			// Draw a minor tick
+			p.drawLine(QPointF(x, minor_tick_y1),
+				QPointF(x, tick_y2));
+		}
+
+		division++;
+
+	} while (x < width());
+
+	// Draw the cursors
+	if (_view.cursors_shown())
+		_view.cursors().draw_markers(p, rect(), prefix);
+
+	// Draw the hover mark
+	draw_hover_mark(p);
+
+	p.end();
+}
+
+void Ruler::mouseMoveEvent(QMouseEvent *e)
+{
+	if (!(e->buttons() & Qt::LeftButton))
+		return;
+	
+	if ((e->pos() - _mouse_down_point).manhattanLength() <
+		QApplication::startDragDistance())
+		return;
+
+	_dragging = true;
+
+	if (shared_ptr<TimeMarker> m = _grabbed_marker.lock())
+		m->set_time(_view.offset() +
+			((double)e->x() + 0.5) * _view.scale());
+}
+
+void Ruler::mousePressEvent(QMouseEvent *e)
+{
+	if (e->buttons() & Qt::LeftButton)
+	{
+		_mouse_down_point = e->pos();
+
+		_grabbed_marker.reset();
+
+		clear_selection();
+
+		if (_view.cursors_shown()) {
+			CursorPair &cursors = _view.cursors();
+			if (cursors.first()->get_label_rect(
+				rect()).contains(e->pos()))
+				_grabbed_marker = cursors.first();
+			else if (cursors.second()->get_label_rect(
+				rect()).contains(e->pos()))
+				_grabbed_marker = cursors.second();
+		}
+
+		if (shared_ptr<TimeMarker> m = _grabbed_marker.lock())
+			m->select();
+
+		selection_changed();
+	}
+}
+
+void Ruler::mouseReleaseEvent(QMouseEvent *)
+{
+	using pv::widgets::Popup;
+
+	if (!_dragging)
+		if (shared_ptr<TimeMarker> m = _grabbed_marker.lock()) {
+			Popup *const p = m->create_popup(&_view);
+			p->set_position(mapToGlobal(QPoint(m->get_x(),
+				height())), Popup::Bottom);
+			p->show();
+		}
+
+	_dragging = false;
+	_grabbed_marker.reset();
+}
+
+void Ruler::draw_hover_mark(QPainter &p)
+{
+	const int x = _view.hover_point().x();
+
+	if (x == -1 || _dragging)
+		return;
+
+	p.setPen(QPen(Qt::NoPen));
+	p.setBrush(QBrush(palette().color(foregroundRole())));
+
+	const int b = height() - 1;
+	const QPointF points[] = {
+		QPointF(x, b),
+		QPointF(x - HoverArrowSize, b - HoverArrowSize),
+		QPointF(x + HoverArrowSize, b - HoverArrowSize)
+	};
+	p.drawPolygon(points, countof(points));
+}
+
+void Ruler::hover_point_changed()
+{
+	update();
+}
+
+} // namespace view
+} // namespace pv
diff --git a/pv/view/ruler.h b/pv/view/ruler.h
new file mode 100644
index 0000000..dc4e7bb
--- /dev/null
+++ b/pv/view/ruler.h
@@ -0,0 +1,84 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_VIEW_RULER_H
+#define PULSEVIEW_PV_VIEW_RULER_H
+
+#include <boost/weak_ptr.hpp>
+
+#include "marginwidget.h"
+
+namespace pv {
+namespace view {
+
+class TimeMarker;
+class View;
+
+class Ruler : public MarginWidget
+{
+	Q_OBJECT
+
+private:
+	static const int RulerHeight;
+	static const int MinorTickSubdivision;
+	static const int ScaleUnits[3];
+
+	static const QString SIPrefixes[9];
+	static const int FirstSIPrefixPower;
+
+	static const int HoverArrowSize;
+
+public:
+	Ruler(View &parent);
+
+	void clear_selection();
+
+	static QString format_time(double t, unsigned int prefix,
+		unsigned precision = 0);
+
+public:
+	QSize sizeHint() const;
+
+private:
+	void paintEvent(QPaintEvent *event);
+
+	void mouseMoveEvent(QMouseEvent *e);
+	void mousePressEvent(QMouseEvent *e);
+	void mouseReleaseEvent(QMouseEvent *);
+
+private:
+	/**
+	 * Draw a hover arrow under the cursor position.
+	 */
+	void draw_hover_mark(QPainter &p);
+
+private slots:
+	void hover_point_changed();
+
+private:
+	boost::weak_ptr<TimeMarker> _grabbed_marker;
+	QPoint _mouse_down_point;
+	bool _dragging;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_RULER_H
diff --git a/pv/view/selectableitem.cpp b/pv/view/selectableitem.cpp
new file mode 100644
index 0000000..3f4f6da
--- /dev/null
+++ b/pv/view/selectableitem.cpp
@@ -0,0 +1,66 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "selectableitem.h"
+
+#include <QApplication>
+#include <QMenu>
+#include <QPalette>
+
+namespace pv {
+namespace view {
+
+const int SelectableItem::HighlightRadius = 6;
+
+SelectableItem::SelectableItem() :
+	_context_parent(NULL),
+	_selected(false)
+{
+}
+
+bool SelectableItem::selected() const
+{
+	return _selected;
+}
+
+void SelectableItem::select(bool select)
+{
+	_selected = select;
+}
+
+QMenu* SelectableItem::create_context_menu(QWidget *parent)
+{
+	_context_parent = parent;
+	return new QMenu(parent);
+}
+
+void SelectableItem::delete_pressed()
+{
+}
+
+QPen SelectableItem::highlight_pen()
+{
+	return QPen(QApplication::palette().brush(
+		QPalette::Highlight), HighlightRadius,
+		Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin);
+}
+
+} // namespace view
+} // namespace pv
diff --git a/pv/view/selectableitem.h b/pv/view/selectableitem.h
new file mode 100644
index 0000000..d321775
--- /dev/null
+++ b/pv/view/selectableitem.h
@@ -0,0 +1,81 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_SELECTABLEITEM_H
+#define PULSEVIEW_PV_SELECTABLEITEM_H
+
+#include <list>
+
+#include <QPen>
+
+class QAction;
+class QMenu;
+class QWidget;
+
+namespace pv {
+
+namespace widgets {
+class Popup;
+}
+
+namespace view {
+
+class SelectableItem : public QObject
+{
+	Q_OBJECT
+
+private:
+	static const int HighlightRadius;
+
+public:
+	SelectableItem();
+
+public:
+	/**
+	 * Returns true if the signal has been selected by the user.
+	 */
+	bool selected() const;
+
+	/**
+	 * Selects or deselects the signal.
+	 */
+	void select(bool select = true);
+
+public:
+	virtual QMenu* create_context_menu(QWidget *parent);
+
+	virtual pv::widgets::Popup* create_popup(QWidget *parent) = 0;
+
+	virtual void delete_pressed();
+
+protected:
+	static QPen highlight_pen();
+
+protected:
+	QWidget *_context_parent;
+
+private:
+	bool _selected;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_SELECTABLEITEM_H
diff --git a/pv/view/signal.cpp b/pv/view/signal.cpp
new file mode 100644
index 0000000..3312c59
--- /dev/null
+++ b/pv/view/signal.cpp
@@ -0,0 +1,136 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <extdef.h>
+
+#include <assert.h>
+#include <math.h>
+
+#include <QApplication>
+#include <QFormLayout>
+#include <QMenu>
+
+#include <libsigrok/libsigrok.h>
+
+#include "signal.h"
+#include "view.h"
+
+#include <pv/device/devinst.h>
+
+using boost::shared_ptr;
+
+namespace pv {
+namespace view {
+
+const char *const ProbeNames[] = {
+	"CLK",
+	"DATA",
+	"IN",
+	"OUT",
+	"RST",
+	"Tx",
+	"Rx",
+	"EN",
+	"SCLK",
+	"MOSI",
+	"MISO",
+	"/SS",
+	"SDA",
+	"SCL"
+};
+
+Signal::Signal(shared_ptr<pv::device::DevInst> dev_inst,
+	const sr_channel *const probe) :
+	Trace(probe->name),
+	_dev_inst(dev_inst),
+	_probe(probe),
+	_name_widget(NULL),
+	_updating_name_widget(false)
+{
+	assert(_probe);
+}
+
+void Signal::set_name(QString name)
+{
+	Trace::set_name(name);
+	_updating_name_widget = true;
+	_name_widget->setEditText(name);
+	_updating_name_widget = false;
+}
+
+bool Signal::enabled() const
+{
+	return _probe->enabled;
+}
+
+void Signal::enable(bool enable)
+{
+	_dev_inst->enable_probe(_probe, enable);
+	visibility_changed();
+}
+
+const sr_channel* Signal::probe() const
+{
+	return _probe;
+}
+
+void Signal::populate_popup_form(QWidget *parent, QFormLayout *form)
+{
+	_name_widget = new QComboBox(parent);
+	_name_widget->setEditable(true);
+
+	for(unsigned int i = 0; i < countof(ProbeNames); i++)
+		_name_widget->insertItem(i, ProbeNames[i]);
+	_name_widget->setEditText(_probe->name);
+
+	connect(_name_widget, SIGNAL(editTextChanged(const QString&)),
+		this, SLOT(on_text_changed(const QString&)));
+
+	form->addRow(tr("Name"), _name_widget);
+
+	add_colour_option(parent, form);
+}
+
+QMenu* Signal::create_context_menu(QWidget *parent)
+{
+	QMenu *const menu = Trace::create_context_menu(parent);
+
+	menu->addSeparator();
+
+	QAction *const disable = new QAction(tr("Disable"), this);
+	disable->setShortcuts(QKeySequence::Delete);
+	connect(disable, SIGNAL(triggered()), this, SLOT(on_disable()));
+	menu->addAction(disable);
+
+	return menu;
+}
+
+void Signal::delete_pressed()
+{
+	on_disable();
+}
+
+void Signal::on_disable()
+{
+	enable(false);
+}
+
+} // namespace view
+} // namespace pv
diff --git a/pv/view/signal.h b/pv/view/signal.h
new file mode 100644
index 0000000..32d1817
--- /dev/null
+++ b/pv/view/signal.h
@@ -0,0 +1,92 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_VIEW_SIGNAL_H
+#define PULSEVIEW_PV_VIEW_SIGNAL_H
+
+#include <boost/shared_ptr.hpp>
+
+#include <QComboBox>
+#include <QWidgetAction>
+
+#include <stdint.h>
+
+#include "trace.h"
+
+struct sr_channel;
+
+namespace pv {
+
+namespace data {
+class SignalData;
+}
+
+namespace device {
+class DevInst;
+}
+
+namespace view {
+
+class Signal : public Trace
+{
+	Q_OBJECT
+
+protected:
+	Signal(boost::shared_ptr<pv::device::DevInst> dev_inst,
+		const sr_channel *const probe);
+
+public:
+	/**
+	 * Sets the name of the signal.
+	 */
+	void set_name(QString name);
+
+	virtual boost::shared_ptr<pv::data::SignalData> data() const = 0;
+
+	/**
+	 * Returns true if the trace is visible and enabled.
+	 */
+	bool enabled() const;
+
+	void enable(bool enable = true);
+
+	const sr_channel* probe() const;
+
+	virtual void populate_popup_form(QWidget *parent, QFormLayout *form);
+
+	QMenu* create_context_menu(QWidget *parent);
+
+	void delete_pressed();
+
+private slots:
+	void on_disable();
+
+protected:
+	boost::shared_ptr<pv::device::DevInst> _dev_inst;
+	const sr_channel *const _probe;
+
+	QComboBox *_name_widget;
+	bool _updating_name_widget;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_SIGNAL_H
diff --git a/pv/view/timemarker.cpp b/pv/view/timemarker.cpp
new file mode 100644
index 0000000..a5d280b
--- /dev/null
+++ b/pv/view/timemarker.cpp
@@ -0,0 +1,104 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "timemarker.h"
+
+#include "view.h"
+
+#include <QFormLayout>
+#include <QPainter>
+
+#include <pv/widgets/popup.h>
+
+namespace pv {
+namespace view {
+
+TimeMarker::TimeMarker(View &view, const QColor &colour, double time) :
+	_view(view),
+	_colour(colour),
+	_time(time),
+	_value_action(NULL),
+	_value_widget(NULL),
+	_updating_value_widget(false)
+{
+}
+
+double TimeMarker::time() const
+{
+	return _time;
+}
+
+float TimeMarker::get_x() const
+{
+	return (_time - _view.offset()) / _view.scale();
+}
+
+void TimeMarker::set_time(double time)
+{
+	_time = time;
+
+	if (_value_widget) {
+		_updating_value_widget = true;
+		_value_widget->setValue(time);
+		_updating_value_widget = false;
+	}
+
+	time_changed();
+}
+
+void TimeMarker::paint(QPainter &p, const QRect &rect)
+{
+	const float x = get_x();
+	p.setPen(_colour);
+	p.drawLine(QPointF(x, rect.top()), QPointF(x, rect.bottom()));
+}
+
+pv::widgets::Popup* TimeMarker::create_popup(QWidget *parent)
+{
+	using pv::widgets::Popup;
+
+	Popup *const popup = new Popup(parent);
+	QFormLayout *const form = new QFormLayout(popup);
+	popup->setLayout(form);
+
+	_value_widget = new QDoubleSpinBox(parent);
+	_value_widget->setDecimals(9);
+	_value_widget->setSuffix("s");
+	_value_widget->setSingleStep(1e-6);
+	_value_widget->setValue(_time);
+
+	connect(_value_widget, SIGNAL(valueChanged(double)),
+		this, SLOT(on_value_changed(double)));
+
+	form->addRow(tr("Time"), _value_widget);
+
+	return popup;
+}
+
+void TimeMarker::on_value_changed(double value)
+{
+	if (!_updating_value_widget) {
+		_time = value;
+		time_changed();
+	}
+}
+
+} // namespace view
+} // namespace pv
diff --git a/pv/view/timemarker.h b/pv/view/timemarker.h
new file mode 100644
index 0000000..fc058c2
--- /dev/null
+++ b/pv/view/timemarker.h
@@ -0,0 +1,113 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_VIEW_MARKER_H
+#define PULSEVIEW_PV_VIEW_MARKER_H
+
+#include <QColor>
+#include <QDoubleSpinBox>
+#include <QObject>
+#include <QRectF>
+#include <QWidgetAction>
+
+#include "selectableitem.h"
+
+class QPainter;
+class QRect;
+
+namespace pv {
+namespace view {
+
+class View;
+
+class TimeMarker : public SelectableItem
+{
+	Q_OBJECT
+
+protected:
+	/**
+	 * Constructor.
+	 * @param view A reference to the view that owns this marker.
+	 * @param colour A reference to the colour of this cursor.
+	 * @param time The time to set the flag to.
+	 */
+	TimeMarker(View &view, const QColor &colour, double time);
+
+public:
+	/**
+	 * Gets the time of the marker.
+	 */
+	double time() const;
+
+	/**
+	 * Sets the time of the marker.
+	 */
+	void set_time(double time);
+
+	float get_x() const;
+
+	/**
+	 * Paints the marker to the viewport.
+	 * @param p The painter to draw with.
+	 * @param rect The rectangle of the viewport client area.
+	 */
+	virtual void paint(QPainter &p, const QRect &rect);
+
+	/**
+	 * Gets the marker label rectangle.
+	 * @param rect The rectangle of the ruler client area.
+	 * @return Returns the label rectangle.
+	 */
+	virtual QRectF get_label_rect(const QRect &rect) const = 0;
+
+	/**
+	 * Paints the marker's label to the ruler.
+	 * @param p The painter to draw with.
+	 * @param rect The rectangle of the ruler client area.
+	 * @param prefix The SI prefix to paint time value with.
+	 */
+	virtual void paint_label(QPainter &p, const QRect &rect,
+		unsigned int prefix) = 0;
+
+	pv::widgets::Popup* create_popup(QWidget *parent);
+
+private slots:
+	void on_value_changed(double value);
+
+signals:
+	void time_changed();
+
+protected:
+	View &_view;
+	const QColor &_colour;
+
+	double _time;
+
+	QSizeF _text_size;
+
+	QWidgetAction *_value_action;
+	QDoubleSpinBox *_value_widget;
+	bool _updating_value_widget;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_MARKER_H
diff --git a/pv/view/trace.cpp b/pv/view/trace.cpp
new file mode 100644
index 0000000..d10448f
--- /dev/null
+++ b/pv/view/trace.cpp
@@ -0,0 +1,292 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <extdef.h>
+
+#include <assert.h>
+#include <math.h>
+
+#include <QApplication>
+#include <QFormLayout>
+#include <QLineEdit>
+
+#include "trace.h"
+#include "tracepalette.h"
+#include "view.h"
+
+#include <pv/widgets/colourbutton.h>
+#include <pv/widgets/popup.h>
+
+namespace pv {
+namespace view {
+
+const QPen Trace::AxisPen(QColor(128, 128, 128, 64));
+const int Trace::LabelHitPadding = 2;
+
+Trace::Trace(QString name) :
+	_name(name),
+	_v_offset(0),
+	_popup(NULL),
+	_popup_form(NULL)
+{
+}
+
+QString Trace::get_name() const
+{
+	return _name;
+}
+
+void Trace::set_name(QString name)
+{
+	_name = name;
+}
+
+QColor Trace::get_colour() const
+{
+	return _colour;
+}
+
+void Trace::set_colour(QColor colour)
+{
+	_colour = colour;
+}
+
+int Trace::get_v_offset() const
+{
+	return _v_offset;
+}
+
+void Trace::set_v_offset(int v_offset)
+{
+	_v_offset = v_offset;
+}
+
+void Trace::set_view(pv::view::View *view)
+{
+	assert(view);
+	_view = view;
+}
+
+void Trace::paint_back(QPainter &p, int left, int right)
+{
+	(void)p;
+	(void)left;
+	(void)right;
+}
+
+void Trace::paint_mid(QPainter &p, int left, int right)
+{
+	(void)p;
+	(void)left;
+	(void)right;
+}
+
+void Trace::paint_fore(QPainter &p, int left, int right)
+{
+	(void)p;
+	(void)left;
+	(void)right;
+}
+
+void Trace::paint_label(QPainter &p, int right, bool hover)
+{
+	assert(_view);
+	const int y = _v_offset - _view->v_offset();
+
+	p.setBrush(_colour);
+
+	if (!enabled())
+		return;
+
+	const QColor colour = get_colour();
+
+	const QRectF label_rect = get_label_rect(right);
+
+	// Paint the label
+	const QPointF points[] = {
+		label_rect.topLeft(),
+		label_rect.topRight(),
+		QPointF(right, y),
+		label_rect.bottomRight(),
+		label_rect.bottomLeft()
+	};
+
+	const QPointF highlight_points[] = {
+		QPointF(label_rect.left() + 1, label_rect.top() + 1),
+		QPointF(label_rect.right(), label_rect.top() + 1),
+		QPointF(right - 1, y),
+		QPointF(label_rect.right(), label_rect.bottom() - 1),
+		QPointF(label_rect.left() + 1, label_rect.bottom() - 1)
+	};
+
+	if (selected()) {
+		p.setPen(highlight_pen());
+		p.setBrush(Qt::transparent);
+		p.drawPolygon(points, countof(points));
+	}
+
+	p.setPen(Qt::transparent);
+	p.setBrush(hover ? colour.lighter() : colour);
+	p.drawPolygon(points, countof(points));
+
+	p.setPen(colour.lighter());
+	p.setBrush(Qt::transparent);
+	p.drawPolygon(highlight_points, countof(highlight_points));
+
+	p.setPen(colour.darker());
+	p.setBrush(Qt::transparent);
+	p.drawPolygon(points, countof(points));
+
+	// Paint the text
+	p.setPen(get_text_colour());
+	p.setFont(QApplication::font());
+	p.drawText(label_rect, Qt::AlignCenter | Qt::AlignVCenter, _name);
+}
+
+bool Trace::pt_in_label_rect(int left, int right, const QPoint &point)
+{
+	(void)left;
+
+	const QRectF label = get_label_rect(right);
+	return enabled() && QRectF(
+		QPointF(label.left() - LabelHitPadding,
+			label.top() - LabelHitPadding),
+		QPointF(right, label.bottom() + LabelHitPadding)
+			).contains(point);
+}
+
+QMenu* Trace::create_context_menu(QWidget *parent)
+{
+	QMenu *const menu = SelectableItem::create_context_menu(parent);
+
+	return menu;
+}
+
+pv::widgets::Popup* Trace::create_popup(QWidget *parent)
+{
+	using pv::widgets::Popup;
+
+	_popup = new Popup(parent);
+
+	create_popup_form();
+
+	connect(_popup, SIGNAL(closed()),
+		this, SLOT(on_popup_closed()));
+
+	return _popup;
+}
+
+int Trace::get_y() const
+{
+	return _v_offset - _view->v_offset();
+}
+
+QRectF Trace::get_label_rect(int right)
+{
+	using pv::view::View;
+
+	assert(_view);
+
+	QFontMetrics m(QApplication::font());
+	const QSize text_size(
+		m.boundingRect(QRect(), 0, _name).width(),
+		m.boundingRect(QRect(), 0, "Tg").height());
+	const QSizeF label_size(
+		text_size.width() + View::LabelPadding.width() * 2,
+		ceilf((text_size.height() + View::LabelPadding.height() * 2) / 2) * 2);
+	const float label_arrow_length = label_size.height() / 2;
+	return QRectF(
+		right - label_arrow_length - label_size.width() - 0.5,
+		get_y() + 0.5f - label_size.height() / 2,
+		label_size.width(), label_size.height());
+}
+
+QColor Trace::get_text_colour() const
+{
+	return (_colour.lightness() > 64) ? Qt::black : Qt::white;
+}
+
+void Trace::paint_axis(QPainter &p, int y, int left, int right)
+{
+	p.setPen(AxisPen);
+	p.drawLine(QPointF(left, y + 0.5f), QPointF(right, y + 0.5f));
+}
+
+void Trace::add_colour_option(QWidget *parent, QFormLayout *form)
+{
+	using pv::widgets::ColourButton;
+
+	ColourButton *const colour_button = new ColourButton(
+		TracePalette::Rows, TracePalette::Cols, parent);
+	colour_button->set_palette(TracePalette::Colours);
+	colour_button->set_colour(_colour);
+	connect(colour_button, SIGNAL(selected(const QColor&)),
+		this, SLOT(on_colour_changed(const QColor&)));
+
+	form->addRow(tr("Colour"), colour_button);
+}
+
+void Trace::create_popup_form()
+{
+	// Clear the layout
+
+	// Transfer the layout and the child widgets to a temporary widget
+	// which then goes out of scope destroying the layout and all the child
+	// widgets.
+	if (_popup_form)
+		QWidget().setLayout(_popup_form);
+
+	// Repopulate the popup
+	_popup_form = new QFormLayout(_popup);
+	_popup->setLayout(_popup_form);
+	populate_popup_form(_popup, _popup_form);
+}
+
+void Trace::populate_popup_form(QWidget *parent, QFormLayout *form)
+{
+	QLineEdit *const name_edit = new QLineEdit(parent);
+	name_edit->setText(_name);
+	connect(name_edit, SIGNAL(textChanged(const QString&)),
+		this, SLOT(on_text_changed(const QString&)));
+	form->addRow(tr("Name"), name_edit);
+
+	add_colour_option(parent, form);
+}
+
+void Trace::on_popup_closed()
+{
+	_popup = NULL;
+	_popup_form = NULL;
+}
+
+void Trace::on_text_changed(const QString &text)
+{
+	set_name(text);
+	text_changed();
+}
+
+void Trace::on_colour_changed(const QColor &colour)
+{
+	set_colour(colour);
+	colour_changed();
+}
+
+} // namespace view
+} // namespace pv
diff --git a/pv/view/trace.h b/pv/view/trace.h
new file mode 100644
index 0000000..ace93d5
--- /dev/null
+++ b/pv/view/trace.h
@@ -0,0 +1,203 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_VIEW_TRACE_H
+#define PULSEVIEW_PV_VIEW_TRACE_H
+
+#include <QColor>
+#include <QPainter>
+#include <QPen>
+#include <QRect>
+#include <QString>
+
+#include <stdint.h>
+
+#include "selectableitem.h"
+
+class QFormLayout;
+
+namespace pv {
+namespace view {
+
+class View;
+
+class Trace : public SelectableItem
+{
+	Q_OBJECT
+
+private:
+	static const QPen AxisPen;
+	static const int LabelHitPadding;
+
+protected:
+	Trace(QString name);
+
+public:
+	/**
+	 * Gets the name of this signal.
+	 */
+	QString get_name() const;
+
+	/**
+	 * Sets the name of the signal.
+	 */
+	virtual void set_name(QString name);
+
+	/**
+	 * Get the colour of the signal.
+	 */
+	QColor get_colour() const;
+
+	/**
+	 * Set the colour of the signal.
+	 */
+	void set_colour(QColor colour);
+
+	/**
+	 * Gets the vertical layout offset of this signal.
+	 */
+	int get_v_offset() const;
+
+	/**
+	 * Sets the vertical layout offset of this signal.
+	 */
+	void set_v_offset(int v_offset);
+
+	/**
+	 * Returns true if the trace is visible and enabled.
+	 */
+	virtual bool enabled() const = 0;
+
+	virtual void set_view(pv::view::View *view);
+
+	/**
+	 * Paints the background layer of the trace with a QPainter
+	 * @param p the QPainter to paint into.
+	 * @param left the x-coordinate of the left edge of the signal
+	 * @param right the x-coordinate of the right edge of the signal
+	 **/
+	virtual void paint_back(QPainter &p, int left, int right);
+
+	/**
+	 * Paints the mid-layer of the trace with a QPainter
+	 * @param p the QPainter to paint into.
+	 * @param left the x-coordinate of the left edge of the signal
+	 * @param right the x-coordinate of the right edge of the signal
+	 **/
+	virtual void paint_mid(QPainter &p, int left, int right);
+
+	/**
+	 * Paints the foreground layer of the trace with a QPainter
+	 * @param p the QPainter to paint into.
+	 * @param left the x-coordinate of the left edge of the signal
+	 * @param right the x-coordinate of the right edge of the signal
+	 **/
+	virtual void paint_fore(QPainter &p, int left, int right);
+
+	/**
+	 * Paints the signal label into a QGLWidget.
+	 * @param p the QPainter to paint into.
+	 * @param right the x-coordinate of the right edge of the header
+	 * 	area.
+	 * @param hover true if the label is being hovered over by the mouse.
+	 */
+	virtual void paint_label(QPainter &p, int right, bool hover);
+
+	/**
+	 * Determines if a point is in the header label rect.
+	 * @param left the x-coordinate of the left edge of the header
+	 * 	area.
+	 * @param right the x-coordinate of the right edge of the header
+	 * 	area.
+	 * @param point the point to test.
+	 */
+	bool pt_in_label_rect(int left, int right, const QPoint &point);
+
+	virtual QMenu* create_context_menu(QWidget *parent);
+
+	pv::widgets::Popup* create_popup(QWidget *parent);
+
+	/**
+	 * Gets the y-offset of the axis.
+	 */
+	int get_y() const;
+
+	/**
+	 * Computes the outline rectangle of a label.
+	 * @param p the QPainter to lay out text with.
+	 * @param right the x-coordinate of the right edge of the header
+	 * 	area.
+	 * @return Returns the rectangle of the signal label.
+	 */
+	QRectF get_label_rect(int right);
+
+protected:
+
+	/**
+	 * Gets the text colour.
+	 * @remarks This colour is computed by comparing the lightness
+	 * of the trace colour against a threshold to determine whether
+	 * white or black would be more visible.
+	 */
+	QColor get_text_colour() const;
+
+	/**
+	 * Paints a zero axis across the viewport.
+	 * @param p the QPainter to paint into.
+	 * @param y the y-offset of the axis.
+	 * @param left the x-coordinate of the left edge of the view.
+	 * @param right the x-coordinate of the right edge of the view.
+	 */
+	void paint_axis(QPainter &p, int y, int left, int right);
+
+	void add_colour_option(QWidget *parent, QFormLayout *form);
+
+	void create_popup_form();
+
+	virtual void populate_popup_form(QWidget *parent, QFormLayout *form);
+
+private slots:
+	void on_text_changed(const QString &text);
+
+	void on_colour_changed(const QColor &colour);
+
+	void on_popup_closed();
+
+signals:
+	void visibility_changed();
+	void text_changed();	
+	void colour_changed();
+
+protected:
+	pv::view::View *_view;
+
+	QString _name;
+	QColor _colour;
+	int _v_offset;
+
+private:
+	pv::widgets::Popup *_popup;
+	QFormLayout *_popup_form;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_TRACE_H
diff --git a/pv/view/tracepalette.cpp b/pv/view/tracepalette.cpp
new file mode 100644
index 0000000..cefb952
--- /dev/null
+++ b/pv/view/tracepalette.cpp
@@ -0,0 +1,70 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "tracepalette.h"
+
+namespace pv {
+namespace view {
+
+const QColor TracePalette::Colours[Cols * Rows] = {
+
+	// Light Colours
+	QColor(0xFC, 0xE9, 0x4F),	// Butter
+	QColor(0xFC, 0xAF, 0x3E),	// Orange
+	QColor(0xE9, 0xB9, 0x6E),	// Chocolate
+	QColor(0x8A, 0xE2, 0x34),	// Chameleon
+	QColor(0x72, 0x9F, 0xCF),	// Sky Blue
+	QColor(0xAD, 0x7F, 0xA8),	// Plum
+	QColor(0xCF, 0x72, 0xC3),	// Magenta
+	QColor(0xEF, 0x29, 0x29),	// Scarlet Red
+
+	// Mid Colours
+	QColor(0xED, 0xD4, 0x00),	// Butter
+	QColor(0xF5, 0x79, 0x00),	// Orange
+	QColor(0xC1, 0x7D, 0x11),	// Chocolate
+	QColor(0x73, 0xD2, 0x16),	// Chameleon
+	QColor(0x34, 0x65, 0xA4),	// Sky Blue
+	QColor(0x75, 0x50, 0x7B),	// Plum
+	QColor(0xA3, 0x34, 0x96),	// Magenta
+	QColor(0xCC, 0x00, 0x00),	// Scarlet Red
+
+	// Dark Colours
+	QColor(0xC4, 0xA0, 0x00),	// Butter
+	QColor(0xCE, 0x5C, 0x00),	// Orange
+	QColor(0x8F, 0x59, 0x02),	// Chocolate
+	QColor(0x4E, 0x9A, 0x06),	// Chameleon
+	QColor(0x20, 0x4A, 0x87),	// Sky Blue
+	QColor(0x5C, 0x35, 0x66),	// Plum
+	QColor(0x87, 0x20, 0x7A),	// Magenta
+	QColor(0xA4, 0x00, 0x00),	// Scarlet Red
+
+	// Greys
+	QColor(0x16, 0x19, 0x1A),	// Black
+	QColor(0x2E, 0x34, 0x36),	// Grey 1
+	QColor(0x55, 0x57, 0x53),	// Grey 2
+	QColor(0x88, 0x8A, 0x8F),	// Grey 3
+	QColor(0xBA, 0xBD, 0xB6),	// Grey 4
+	QColor(0xD3, 0xD7, 0xCF),	// Grey 5
+	QColor(0xEE, 0xEE, 0xEC),	// Grey 6
+	QColor(0xFF, 0xFF, 0xFF),	// White
+};
+
+} // view
+} // pv
diff --git a/pv/view/tracepalette.h b/pv/view/tracepalette.h
new file mode 100644
index 0000000..ae3c69a
--- /dev/null
+++ b/pv/view/tracepalette.h
@@ -0,0 +1,41 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_TRACEPALETTE_H
+#define PULSEVIEW_PV_TRACEPALETTE_H
+
+#include <QColor>
+
+namespace pv {
+namespace view {
+
+class TracePalette
+{
+public:
+	static const unsigned int Cols = 8;
+	static const unsigned int Rows = 4;
+	static const QColor Colours[Cols * Rows];
+};
+
+
+} // view
+} // pv
+
+#endif // PULSEVIEW_PV_VIEW_TRACEPALETTE_H
diff --git a/pv/view/view.cpp b/pv/view/view.cpp
new file mode 100644
index 0000000..d117489
--- /dev/null
+++ b/pv/view/view.cpp
@@ -0,0 +1,539 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifdef ENABLE_DECODE
+#include <libsigrokdecode/libsigrokdecode.h>
+#endif
+
+#include <assert.h>
+#include <limits.h>
+#include <math.h>
+
+#include <boost/foreach.hpp>
+
+#include <QEvent>
+#include <QMouseEvent>
+#include <QScrollBar>
+
+#include "decodetrace.h"
+#include "header.h"
+#include "ruler.h"
+#include "signal.h"
+#include "view.h"
+#include "viewport.h"
+
+#include "pv/sigsession.h"
+#include "pv/data/logic.h"
+#include "pv/data/logicsnapshot.h"
+
+using boost::shared_ptr;
+using boost::weak_ptr;
+using pv::data::SignalData;
+using std::deque;
+using std::list;
+using std::max;
+using std::make_pair;
+using std::min;
+using std::pair;
+using std::set;
+using std::vector;
+
+namespace pv {
+namespace view {
+
+const double View::MaxScale = 1e9;
+const double View::MinScale = 1e-15;
+
+const int View::MaxScrollValue = INT_MAX / 2;
+
+const int View::SignalHeight = 30;
+const int View::SignalMargin = 10;
+const int View::SignalSnapGridSize = 10;
+
+const QColor View::CursorAreaColour(220, 231, 243);
+
+const QSizeF View::LabelPadding(4, 0);
+
+View::View(SigSession &session, QWidget *parent) :
+	QAbstractScrollArea(parent),
+	_session(session),
+	_viewport(new Viewport(*this)),
+	_ruler(new Ruler(*this)),
+	_header(new Header(*this)),
+	_scale(1e-6),
+	_offset(0),
+	_v_offset(0),
+	_updating_scroll(false),
+	_show_cursors(false),
+	_cursors(*this),
+	_hover_point(-1, -1)
+{
+	connect(horizontalScrollBar(), SIGNAL(valueChanged(int)),
+		this, SLOT(h_scroll_value_changed(int)));
+	connect(verticalScrollBar(), SIGNAL(valueChanged(int)),
+		this, SLOT(v_scroll_value_changed(int)));
+
+	connect(&_session, SIGNAL(signals_changed()),
+		this, SLOT(signals_changed()));
+	connect(&_session, SIGNAL(data_received()),
+		this, SLOT(data_updated()));
+	connect(&_session, SIGNAL(frame_ended()),
+		this, SLOT(data_updated()));
+
+	connect(_cursors.first().get(), SIGNAL(time_changed()),
+		this, SLOT(marker_time_changed()));
+	connect(_cursors.second().get(), SIGNAL(time_changed()),
+		this, SLOT(marker_time_changed()));
+
+	connect(_header, SIGNAL(geometry_updated()),
+		this, SLOT(on_geometry_updated()));
+	connect(_header, SIGNAL(signals_moved()),
+		this, SLOT(on_signals_moved()));
+
+	connect(_header, SIGNAL(selection_changed()),
+		_ruler, SLOT(clear_selection()));
+	connect(_ruler, SIGNAL(selection_changed()),
+		_header, SLOT(clear_selection()));
+
+	connect(_header, SIGNAL(selection_changed()),
+		this, SIGNAL(selection_changed()));
+	connect(_ruler, SIGNAL(selection_changed()),
+		this, SIGNAL(selection_changed()));
+
+	setViewport(_viewport);
+
+	_viewport->installEventFilter(this);
+	_ruler->installEventFilter(this);
+	_header->installEventFilter(this);
+
+	// Trigger the initial event manually. The default device has signals
+	// which were created before this object came into being
+	signals_changed();
+}
+
+SigSession& View::session()
+{
+	return _session;
+}
+
+const SigSession& View::session() const
+{
+	return _session;
+}
+
+double View::scale() const
+{
+	return _scale;
+}
+
+double View::offset() const
+{
+	return _offset;
+}
+
+int View::v_offset() const
+{
+	return _v_offset;
+}
+
+void View::zoom(double steps)
+{
+	zoom(steps, _viewport->width() / 2);
+}
+
+void View::zoom(double steps, int offset)
+{
+	set_zoom(_scale * pow(3.0/2.0, -steps), offset);
+}
+
+void View::zoom_fit()
+{
+	const pair<double, double> extents = get_time_extents();
+	const double delta = extents.second - extents.first;
+	if (delta < 1e-12)
+		return;
+
+	assert(_viewport);
+	const int w = _viewport->width();
+	if (w <= 0)
+		return;
+
+	const double scale = max(min(delta / w, MaxScale), MinScale);
+	set_scale_offset(scale, extents.first);
+}
+
+void View::zoom_one_to_one()
+{
+	using pv::data::SignalData;
+
+	const vector< shared_ptr<Signal> > sigs(
+		session().get_signals());
+
+	// Make a set of all the visible data objects
+	set< shared_ptr<SignalData> > visible_data = get_visible_data();
+	if (visible_data.empty())
+		return;
+
+	double samplerate = 0.0;
+	BOOST_FOREACH(const shared_ptr<SignalData> d, visible_data) {
+		assert(d);
+		samplerate = max(samplerate, d->samplerate());
+	}
+
+	if (samplerate == 0.0)
+		return;
+
+	assert(_viewport);
+	const int w = _viewport->width();
+	if (w <= 0)
+		return;
+
+	set_zoom(1.0 / samplerate, w / 2);
+}
+
+void View::set_scale_offset(double scale, double offset)
+{
+	_scale = scale;
+	_offset = offset;
+
+	update_scroll();
+	_ruler->update();
+	_viewport->update();
+	scale_offset_changed();
+}
+
+vector< shared_ptr<Trace> > View::get_traces() const
+{
+	const vector< shared_ptr<Signal> > sigs(
+		session().get_signals());
+#ifdef ENABLE_DECODE
+	const vector< shared_ptr<DecodeTrace> > decode_sigs(
+		session().get_decode_signals());
+	vector< shared_ptr<Trace> > traces(
+		sigs.size() + decode_sigs.size());
+#else
+	vector< shared_ptr<Trace> > traces(sigs.size());
+#endif
+
+	vector< shared_ptr<Trace> >::iterator i = traces.begin();
+	i = copy(sigs.begin(), sigs.end(), i);
+#ifdef ENABLE_DECODE
+	i = copy(decode_sigs.begin(), decode_sigs.end(), i);
+#endif
+
+	stable_sort(traces.begin(), traces.end(), compare_trace_v_offsets);
+	return traces;
+}
+
+list<weak_ptr<SelectableItem> > View::selected_items() const
+{
+	list<weak_ptr<SelectableItem> > items;
+
+	// List the selected signals
+	const vector< shared_ptr<Trace> > traces(get_traces());
+	BOOST_FOREACH (shared_ptr<Trace> t, traces) {
+		assert(t);
+		if (t->selected())
+			items.push_back(t);
+	}
+
+	// List the selected cursors
+	if (_cursors.first()->selected())
+		items.push_back(_cursors.first());
+	if (_cursors.second()->selected())
+		items.push_back(_cursors.second());
+
+	return items;
+}
+
+set< shared_ptr<SignalData> > View::get_visible_data() const
+{
+	const vector< shared_ptr<Signal> > sigs(
+		session().get_signals());
+
+	// Make a set of all the visible data objects
+	set< shared_ptr<SignalData> > visible_data;
+	BOOST_FOREACH(const shared_ptr<Signal> sig, sigs)
+		if (sig->enabled())
+			visible_data.insert(sig->data());
+
+	return visible_data;
+}
+
+pair<double, double> View::get_time_extents() const
+{
+	const set< shared_ptr<SignalData> > visible_data = get_visible_data();
+	if (visible_data.empty())
+		return make_pair(0.0, 0.0);
+
+	double left_time = DBL_MAX, right_time = DBL_MIN;
+	BOOST_FOREACH(const shared_ptr<SignalData> d, visible_data)
+	{
+		const double start_time = d->get_start_time();
+		double samplerate = d->samplerate();
+		samplerate = (samplerate <= 0.0) ? 1.0 : samplerate;
+
+		left_time = min(left_time, start_time);
+		right_time = max(right_time, start_time +
+			d->get_max_sample_count() / samplerate);
+	}
+
+	assert(left_time < right_time);
+	return make_pair(left_time, right_time);
+}
+
+bool View::cursors_shown() const
+{
+	return _show_cursors;
+}
+
+void View::show_cursors(bool show)
+{
+	_show_cursors = show;
+	_ruler->update();
+	_viewport->update();
+}
+
+void View::centre_cursors()
+{
+	const double time_width = _scale * _viewport->width();
+	_cursors.first()->set_time(_offset + time_width * 0.4);
+	_cursors.second()->set_time(_offset + time_width * 0.6);
+	_ruler->update();
+	_viewport->update();
+}
+
+CursorPair& View::cursors()
+{
+	return _cursors;
+}
+
+const CursorPair& View::cursors() const
+{
+	return _cursors;
+}
+
+const QPoint& View::hover_point() const
+{
+	return _hover_point;
+}
+
+void View::normalize_layout()
+{
+	const vector< shared_ptr<Trace> > traces(get_traces());
+
+	int v_min = INT_MAX;
+	BOOST_FOREACH(const shared_ptr<Trace> t, traces)
+		v_min = min(t->get_v_offset(), v_min);
+
+	const int delta = -min(v_min, 0);
+	BOOST_FOREACH(shared_ptr<Trace> t, traces)
+		t->set_v_offset(t->get_v_offset() + delta);
+
+	verticalScrollBar()->setSliderPosition(_v_offset + delta);
+	v_scroll_value_changed(verticalScrollBar()->sliderPosition());
+}
+
+void View::update_viewport()
+{
+	assert(_viewport);
+	_viewport->update();
+}
+
+void View::get_scroll_layout(double &length, double &offset) const
+{
+	const pair<double, double> extents = get_time_extents();
+	length = (extents.second - extents.first) / _scale;
+	offset = _offset / _scale;
+}
+
+void View::set_zoom(double scale, int offset)
+{
+	const double cursor_offset = _offset + _scale * offset;
+	const double new_scale = max(min(scale, MaxScale), MinScale);
+	const double new_offset = cursor_offset - new_scale * offset;
+	set_scale_offset(new_scale, new_offset);
+}
+
+void View::update_scroll()
+{
+	assert(_viewport);
+
+	const QSize areaSize = _viewport->size();
+
+	// Set the horizontal scroll bar
+	double length = 0, offset = 0;
+	get_scroll_layout(length, offset);
+	length = max(length - areaSize.width(), 0.0);
+
+	horizontalScrollBar()->setPageStep(areaSize.width() / 2);
+
+	_updating_scroll = true;
+
+	if (length < MaxScrollValue) {
+		horizontalScrollBar()->setRange(0, length);
+		horizontalScrollBar()->setSliderPosition(offset);
+	} else {
+		horizontalScrollBar()->setRange(0, MaxScrollValue);
+		horizontalScrollBar()->setSliderPosition(
+			_offset * MaxScrollValue / (_scale * length));
+	}
+
+	_updating_scroll = false;
+
+	// Set the vertical scrollbar
+	verticalScrollBar()->setPageStep(areaSize.height());
+	verticalScrollBar()->setRange(0,
+		_viewport->get_total_height() + SignalMargin -
+		areaSize.height());
+}
+
+void View::update_layout()
+{
+	setViewportMargins(_header->sizeHint().width(),
+		_ruler->sizeHint().height(), 0, 0);
+	_ruler->setGeometry(_viewport->x(), 0,
+		_viewport->width(), _viewport->y());
+	_header->setGeometry(0, _viewport->y(),
+		_viewport->x(), _viewport->height());
+	update_scroll();
+}
+
+bool View::compare_trace_v_offsets(const shared_ptr<Trace> &a,
+	const shared_ptr<Trace> &b)
+{
+	assert(a);
+	assert(b);
+	return a->get_v_offset() < b->get_v_offset();
+}
+
+bool View::eventFilter(QObject *object, QEvent *event)
+{
+	const QEvent::Type type = event->type();
+	if (type == QEvent::MouseMove) {
+
+		const QMouseEvent *const mouse_event = (QMouseEvent*)event;
+		if (object == _viewport)
+			_hover_point = mouse_event->pos();
+		else if (object == _ruler)
+			_hover_point = QPoint(mouse_event->x(), 0);
+		else if (object == _header)
+			_hover_point = QPoint(0, mouse_event->y());
+		else
+			_hover_point = QPoint(-1, -1);
+
+		hover_point_changed();
+
+	} else if (type == QEvent::Leave) {
+		_hover_point = QPoint(-1, -1);
+		hover_point_changed();
+	}
+
+	return QObject::eventFilter(object, event);
+}
+
+bool View::viewportEvent(QEvent *e)
+{
+	switch(e->type()) {
+	case QEvent::Paint:
+	case QEvent::MouseButtonPress:
+	case QEvent::MouseButtonRelease:
+	case QEvent::MouseButtonDblClick:
+	case QEvent::MouseMove:
+	case QEvent::Wheel:
+		return false;
+
+	default:
+		return QAbstractScrollArea::viewportEvent(e);
+	}
+}
+
+void View::resizeEvent(QResizeEvent*)
+{
+	update_layout();
+}
+
+void View::h_scroll_value_changed(int value)
+{
+	if (_updating_scroll)
+		return;
+
+	const int range = horizontalScrollBar()->maximum();
+	if (range < MaxScrollValue)
+		_offset = _scale * value;
+	else {
+		double length = 0, offset;
+		get_scroll_layout(length, offset);
+		_offset = _scale * length * value / MaxScrollValue;
+	}
+
+	_ruler->update();
+	_viewport->update();
+}
+
+void View::v_scroll_value_changed(int value)
+{
+	_v_offset = value;
+	_header->update();
+	_viewport->update();
+}
+
+void View::signals_changed()
+{
+	int offset = SignalMargin + SignalHeight;
+	const vector< shared_ptr<Trace> > traces(get_traces());
+	BOOST_FOREACH(shared_ptr<Trace> t, traces) {
+		t->set_view(this);
+		t->set_v_offset(offset);
+		offset += SignalHeight + 2 * SignalMargin;
+	}
+
+	update_layout();
+	normalize_layout();
+}
+
+void View::data_updated()
+{
+	// Update the scroll bars
+	update_scroll();
+
+	// Repaint the view
+	_viewport->update();
+}
+
+void View::marker_time_changed()
+{
+	_ruler->update();
+	_viewport->update();
+}
+
+void View::on_signals_moved()
+{
+	update_scroll();
+	signals_moved();
+}
+
+void View::on_geometry_updated()
+{
+	update_layout();
+}
+
+} // namespace view
+} // namespace pv
diff --git a/pv/view/view.h b/pv/view/view.h
new file mode 100644
index 0000000..0661637
--- /dev/null
+++ b/pv/view/view.h
@@ -0,0 +1,214 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_VIEW_VIEW_H
+#define PULSEVIEW_PV_VIEW_VIEW_H
+
+#include <stdint.h>
+
+#include <set>
+#include <vector>
+
+#include <boost/shared_ptr.hpp>
+#include <boost/weak_ptr.hpp>
+
+#include <QAbstractScrollArea>
+#include <QSizeF>
+
+#include <pv/data/signaldata.h>
+
+#include "cursorpair.h"
+
+namespace pv {
+
+class SigSession;
+
+namespace view {
+
+class Header;
+class Ruler;
+class Trace;
+class Viewport;
+
+class View : public QAbstractScrollArea {
+	Q_OBJECT
+
+private:
+	static const double MaxScale;
+	static const double MinScale;
+
+	static const int MaxScrollValue;
+
+public:
+	static const int SignalHeight;
+	static const int SignalMargin;
+	static const int SignalSnapGridSize;
+
+	static const QColor CursorAreaColour;
+
+	static const QSizeF LabelPadding;
+
+public:
+	explicit View(SigSession &session, QWidget *parent = 0);
+
+	SigSession& session();
+	const SigSession& session() const;
+
+	/**
+	 * Returns the view time scale in seconds per pixel.
+	 */
+	double scale() const;
+
+	/**
+	 * Returns the time offset of the left edge of the view in
+	 * seconds.
+	 */
+	double offset() const;
+	int v_offset() const;
+
+	void zoom(double steps);
+	void zoom(double steps, int offset);
+
+	void zoom_fit();
+
+	void zoom_one_to_one();
+
+	/**
+	 * Sets the scale and offset.
+	 * @param scale The new view scale in seconds per pixel.
+	 * @param offset The view time offset in seconds.
+	 */
+	void set_scale_offset(double scale, double offset);
+
+	std::vector< boost::shared_ptr<Trace> > get_traces() const;
+
+	std::list<boost::weak_ptr<SelectableItem> > selected_items() const;
+
+	std::set< boost::shared_ptr<pv::data::SignalData> >
+		get_visible_data() const;
+
+	std::pair<double, double> get_time_extents() const;
+
+	/**
+	 * Returns true if cursors are displayed. false otherwise.
+	 */
+	bool cursors_shown() const;
+
+	/**
+	 * Shows or hides the cursors.
+	 */
+	void show_cursors(bool show = true);
+
+	/**
+	 * Moves the cursors to a convenient position in the view.
+	 */
+	void centre_cursors();
+
+	/**
+	 * Returns a reference to the pair of cursors.
+	 */
+	CursorPair& cursors();
+
+	/**
+	 * Returns a reference to the pair of cursors.
+	 */
+	const CursorPair& cursors() const;
+
+	const QPoint& hover_point() const;
+
+	void normalize_layout();
+
+	void update_viewport();
+
+signals:
+	void hover_point_changed();
+
+	void signals_moved();
+
+	void selection_changed();
+
+	void scale_offset_changed();
+
+private:
+	void get_scroll_layout(double &length, double &offset) const;
+
+	/**
+	 * Simultaneously sets the zoom and offset.
+	 * @param scale The scale to set the view to in seconds per pixel. This
+	 * value is clamped between MinScale and MaxScale.
+	 * @param offset The offset of the left edge of the view in seconds.
+	 */
+	void set_zoom(double scale, int offset);
+
+	void update_scroll();
+
+	void update_layout();
+
+	static bool compare_trace_v_offsets(
+		const boost::shared_ptr<pv::view::Trace> &a,
+		const boost::shared_ptr<pv::view::Trace> &b);
+
+private:
+	bool eventFilter(QObject *object, QEvent *event);
+
+	bool viewportEvent(QEvent *e);
+
+	void resizeEvent(QResizeEvent *e);
+
+private slots:
+
+	void h_scroll_value_changed(int value);
+	void v_scroll_value_changed(int value);
+
+	void signals_changed();
+	void data_updated();
+
+	void marker_time_changed();
+
+	void on_signals_moved();
+
+	void on_geometry_updated();
+
+private:
+	SigSession &_session;
+
+	Viewport *_viewport;
+	Ruler *_ruler;
+	Header *_header;
+
+	/// The view time scale in seconds per pixel.
+	double _scale;
+
+	/// The view time offset in seconds.
+	double _offset;
+
+	int _v_offset;
+	bool _updating_scroll;
+
+	bool _show_cursors;
+	CursorPair _cursors;
+
+	QPoint _hover_point;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_VIEW_H
diff --git a/pv/view/viewport.cpp b/pv/view/viewport.cpp
new file mode 100644
index 0000000..3b06cf9
--- /dev/null
+++ b/pv/view/viewport.cpp
@@ -0,0 +1,161 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "view.h"
+#include "viewport.h"
+
+#include "signal.h"
+#include "../sigsession.h"
+
+#include <QMouseEvent>
+
+#include <boost/foreach.hpp>
+
+using boost::shared_ptr;
+using std::max;
+using std::min;
+using std::vector;
+
+namespace pv {
+namespace view {
+
+Viewport::Viewport(View &parent) :
+	QWidget(&parent),
+        _view(parent)
+{
+	setMouseTracking(true);
+	setAutoFillBackground(true);
+	setBackgroundRole(QPalette::Base);
+
+	connect(&_view.session(), SIGNAL(signals_changed()),
+		this, SLOT(on_signals_changed()));
+
+	connect(&_view, SIGNAL(signals_moved()),
+		this, SLOT(on_signals_moved()));
+
+	// Trigger the initial event manually. The default device has signals
+	// which were created before this object came into being
+	on_signals_changed();
+}
+
+int Viewport::get_total_height() const
+{
+	int h = 0;
+	const vector< shared_ptr<Trace> > traces(_view.get_traces());
+	BOOST_FOREACH(const shared_ptr<Trace> t, traces) {
+		assert(t);
+		h = max(t->get_v_offset() + View::SignalHeight, h);
+	}
+
+	return h;
+}
+
+void Viewport::paintEvent(QPaintEvent*)
+{
+	const vector< shared_ptr<Trace> > traces(_view.get_traces());
+
+	QPainter p(this);
+	p.setRenderHint(QPainter::Antialiasing);
+
+	if (_view.cursors_shown())
+		_view.cursors().draw_viewport_background(p, rect());
+
+	// Plot the signal
+	BOOST_FOREACH(const shared_ptr<Trace> t, traces)
+	{
+		assert(t);
+		t->paint_back(p, 0, width());
+	}
+
+	BOOST_FOREACH(const shared_ptr<Trace> t, traces)
+		t->paint_mid(p, 0, width());
+
+	BOOST_FOREACH(const shared_ptr<Trace> t, traces)
+		t->paint_fore(p, 0, width());
+
+	if (_view.cursors_shown())
+		_view.cursors().draw_viewport_foreground(p, rect());
+
+	p.end();
+}
+
+void Viewport::mousePressEvent(QMouseEvent *event)
+{
+	assert(event);
+
+	_mouse_down_point = event->pos();
+	_mouse_down_offset = _view.offset();
+}
+
+void Viewport::mouseMoveEvent(QMouseEvent *event)
+{
+	assert(event);
+
+	if (event->buttons() & Qt::LeftButton)
+	{
+		_view.set_scale_offset(_view.scale(),
+			_mouse_down_offset +
+			(_mouse_down_point - event->pos()).x() *
+			_view.scale());
+	}
+}
+
+void Viewport::mouseDoubleClickEvent(QMouseEvent *event)
+{
+	assert(event);
+
+	if (event->buttons() & Qt::LeftButton)
+		_view.zoom(2.0, event->x());
+	else if (event->buttons() & Qt::RightButton)
+		_view.zoom(-2.0, event->x());
+}
+
+void Viewport::wheelEvent(QWheelEvent *event)
+{
+	assert(event);
+
+	if (event->orientation() == Qt::Vertical) {
+		// Vertical scrolling is interpreted as zooming in/out
+		_view.zoom(event->delta() / 120, event->x());
+	} else if (event->orientation() == Qt::Horizontal) {
+		// Horizontal scrolling is interpreted as moving left/right
+		_view.set_scale_offset(_view.scale(),
+				       event->delta() * _view.scale()
+				       + _view.offset());
+	}
+}
+
+void Viewport::on_signals_changed()
+{
+	const vector< shared_ptr<Trace> > traces(_view.get_traces());
+	BOOST_FOREACH(shared_ptr<Trace> t, traces) {
+		assert(t);
+		connect(t.get(), SIGNAL(visibility_changed()),
+			this, SLOT(update()));
+	}
+}
+
+void Viewport::on_signals_moved()
+{
+	update();
+}
+
+} // namespace view
+} // namespace pv
diff --git a/pv/view/viewport.h b/pv/view/viewport.h
new file mode 100644
index 0000000..0474b7e
--- /dev/null
+++ b/pv/view/viewport.h
@@ -0,0 +1,68 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_VIEW_VIEWPORT_H
+#define PULSEVIEW_PV_VIEW_VIEWPORT_H
+
+#include <QTimer>
+#include <QWidget>
+
+class QPainter;
+class QPaintEvent;
+class SigSession;
+
+namespace pv {
+namespace view {
+
+class View;
+
+class Viewport : public QWidget
+{
+	Q_OBJECT
+
+public:
+	explicit Viewport(View &parent);
+
+	int get_total_height() const;
+
+protected:
+	void paintEvent(QPaintEvent *event);
+
+private:
+	void mousePressEvent(QMouseEvent *event);
+	void mouseMoveEvent(QMouseEvent *event);
+	void mouseDoubleClickEvent(QMouseEvent * event);
+	void wheelEvent(QWheelEvent *event);
+
+private slots:
+	void on_signals_changed();
+	void on_signals_moved();
+
+private:
+	View &_view;
+
+	QPoint _mouse_down_point;
+	double _mouse_down_offset;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_VIEWPORT_H
diff --git a/pv/widgets/colourbutton.cpp b/pv/widgets/colourbutton.cpp
new file mode 100644
index 0000000..fd417c1
--- /dev/null
+++ b/pv/widgets/colourbutton.cpp
@@ -0,0 +1,109 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "colourbutton.h"
+
+#include <assert.h>
+
+#include <QApplication>
+#include <QPainter>
+
+namespace pv {
+namespace widgets {
+
+const int ColourButton::SwatchMargin = 7;
+
+ColourButton::ColourButton(int rows, int cols, QWidget *parent) :
+	QPushButton("", parent),
+	_popup(rows, cols, this)
+{
+	connect(this, SIGNAL(clicked(bool)), this, SLOT(on_clicked(bool)));
+	connect(&_popup, SIGNAL(selected(int, int)),
+		this, SLOT(on_selected(int, int)));
+}
+
+ColourPopup& ColourButton::popup()
+{
+	return _popup;
+}
+
+const QColor& ColourButton::colour() const
+{
+	return _cur_colour;
+}
+
+void ColourButton::set_colour(QColor colour)
+{
+	_cur_colour = colour;
+
+	const unsigned int rows = _popup.well_array().numRows();
+	const unsigned int cols = _popup.well_array().numCols();
+
+	for (unsigned int r = 0; r < rows; r++)
+		for (unsigned int c = 0; c < cols; c++)
+			if (_popup.well_array().cellBrush(r, c).color() ==
+				colour)
+			{
+				_popup.well_array().setSelected(r, c);
+				_popup.well_array().setCurrent(r, c);
+				return;
+			}	
+}
+
+void ColourButton::set_palette(const QColor *const palette)
+{
+	assert(palette);
+
+	const unsigned int rows = _popup.well_array().numRows();
+	const unsigned int cols = _popup.well_array().numCols();
+
+	for (unsigned int r = 0; r < rows; r++)
+		for (unsigned int c = 0; c < cols; c++)
+			_popup.well_array().setCellBrush(r, c,
+				QBrush(palette[r * cols + c]));
+}
+
+void ColourButton::on_clicked(bool)
+{
+	_popup.set_position(mapToGlobal(rect().center()), Popup::Bottom);
+	_popup.show();
+}
+
+void ColourButton::on_selected(int row, int col)
+{
+	_cur_colour = _popup.well_array().cellBrush(row, col).color();
+	selected(_cur_colour);
+}
+
+void ColourButton::paintEvent(QPaintEvent *e)
+{
+	QPushButton::paintEvent(e);
+
+	QPainter p(this);
+
+	const QRect r = rect().adjusted(SwatchMargin, SwatchMargin,
+		-SwatchMargin, -SwatchMargin);
+	p.setPen(QApplication::palette().color(QPalette::Dark));
+	p.setBrush(QBrush(_cur_colour));
+	p.drawRect(r);
+}
+
+} // widgets
+} // pv
diff --git a/pv/widgets/colourbutton.h b/pv/widgets/colourbutton.h
new file mode 100644
index 0000000..45a3fff
--- /dev/null
+++ b/pv/widgets/colourbutton.h
@@ -0,0 +1,68 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_WIDGETS_COLOURBUTTON_H
+#define PULSEVIEW_PV_WIDGETS_COLOURBUTTON_H
+
+#include <QPushButton>
+
+#include "colourpopup.h"
+
+namespace pv {
+namespace widgets {
+
+class ColourButton : public QPushButton
+{
+	Q_OBJECT;
+
+private:
+	static const int SwatchMargin;
+
+public:
+	ColourButton(int rows, int cols, QWidget *parent);
+
+	ColourPopup& popup();
+
+	const QColor& colour() const;
+
+	void set_colour(QColor colour);
+
+	void set_palette(const QColor *const palette);
+
+private:
+	void paintEvent(QPaintEvent *e);
+
+private slots:
+	void on_clicked(bool);
+
+	void on_selected(int row, int col);
+
+signals:
+	void selected(const QColor &colour);
+
+private:
+	ColourPopup _popup;
+	QColor _cur_colour;
+};
+
+} // widgets
+} // pv
+
+#endif // PULSEVIEW_PV_WIDGETS_COLOURBUTTON_H
diff --git a/pv/widgets/colourpopup.cpp b/pv/widgets/colourpopup.cpp
new file mode 100644
index 0000000..816a4c5
--- /dev/null
+++ b/pv/widgets/colourpopup.cpp
@@ -0,0 +1,51 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "colourpopup.h"
+
+namespace pv {
+namespace widgets {
+
+ColourPopup::ColourPopup(int rows, int cols, QWidget *parent) :
+	Popup(parent),
+	_well_array(rows, cols, this),
+	_layout(this)
+{
+	_layout.addWidget(&_well_array);
+	setLayout(&_layout);
+
+	connect(&_well_array, SIGNAL(selected(int, int)),
+		this, SIGNAL(selected(int, int)));
+	connect(&_well_array, SIGNAL(selected(int, int)),
+		this, SLOT(colour_selected(int, int)));
+}
+
+QWellArray& ColourPopup::well_array()
+{
+	return _well_array;
+}
+
+void ColourPopup::colour_selected(int, int)
+{
+	close();
+}
+
+} // widgets
+} // pv
diff --git a/pv/widgets/colourpopup.h b/pv/widgets/colourpopup.h
new file mode 100644
index 0000000..4f5c52e
--- /dev/null
+++ b/pv/widgets/colourpopup.h
@@ -0,0 +1,55 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_WIDGETS_COLOURPOPUP_H
+#define PULSEVIEW_PV_WIDGETS_COLOURPOPUP_H
+
+#include "popup.h"
+#include "wellarray.h"
+
+#include <QVBoxLayout>
+
+namespace pv {
+namespace widgets {
+
+class ColourPopup : public Popup
+{
+	Q_OBJECT
+
+public:
+	ColourPopup(int rows, int cols, QWidget *partent);
+
+	QWellArray& well_array();
+
+signals:
+	void selected(int row, int col);
+
+private slots:
+	void colour_selected(int, int);
+
+private:
+	QWellArray _well_array;
+	QVBoxLayout _layout;
+};
+
+} // widgets
+} // pv
+
+#endif // PULSEVIEW_PV_WIDGETS_COLOURPOPUP_H
diff --git a/pv/widgets/decodergroupbox.cpp b/pv/widgets/decodergroupbox.cpp
new file mode 100644
index 0000000..2cbd532
--- /dev/null
+++ b/pv/widgets/decodergroupbox.cpp
@@ -0,0 +1,77 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "decodergroupbox.h"
+
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QPushButton>
+#include <QVBoxLayout>
+
+#include <assert.h>
+
+namespace pv {
+namespace widgets {
+
+DecoderGroupBox::DecoderGroupBox(QString title, QWidget *parent) :
+	QWidget(parent),
+	_layout(new QGridLayout),
+	_show_hide_button(QIcon(":/icons/decoder-shown.svg"), QString(), this)
+{
+	_layout->setContentsMargins(0, 0, 0, 0);
+	setLayout(_layout);
+
+	_layout->addWidget(new QLabel(QString("<h3>%1</h3>").arg(title)),
+		0, 0);
+	_layout->setColumnStretch(0, 1);
+
+	QHBoxLayout *const toolbar = new QHBoxLayout;
+	_layout->addLayout(toolbar, 0, 1);
+
+	_show_hide_button.setFlat(true);
+	_show_hide_button.setIconSize(QSize(16, 16));
+	connect(&_show_hide_button, SIGNAL(clicked()),
+		this, SIGNAL(show_hide_decoder()));
+	toolbar->addWidget(&_show_hide_button);
+
+	QPushButton *const delete_button = new QPushButton(
+		QIcon(":/icons/decoder-delete.svg"), QString(), this);
+	delete_button->setFlat(true);
+	delete_button->setIconSize(QSize(16, 16));
+	connect(delete_button, SIGNAL(clicked()),
+		this, SIGNAL(delete_decoder()));
+	toolbar->addWidget(delete_button);
+}
+
+void DecoderGroupBox::add_layout(QLayout *layout)
+{
+	assert(layout);
+	_layout->addLayout(layout, 1, 0, 1, 2);
+}
+
+void DecoderGroupBox::set_decoder_visible(bool visible)
+{
+	_show_hide_button.setIcon(QIcon(visible ?
+		":/icons/decoder-shown.svg" :
+		":/icons/decoder-hidden.svg"));
+}
+
+} // widgets
+} // pv
diff --git a/pv/widgets/decodergroupbox.h b/pv/widgets/decodergroupbox.h
new file mode 100644
index 0000000..2302a97
--- /dev/null
+++ b/pv/widgets/decodergroupbox.h
@@ -0,0 +1,56 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_WIDGETS_DECODERGROUPBOX_H
+#define PULSEVIEW_PV_WIDGETS_DECODERGROUPBOX_H
+
+#include <QPushButton>
+
+class QGridLayout;
+class QToolBar;
+
+namespace pv {
+namespace widgets {
+
+class DecoderGroupBox : public QWidget
+{
+	Q_OBJECT
+
+public:
+	DecoderGroupBox(QString title, QWidget *parent = NULL);
+
+	void add_layout(QLayout *layout);
+
+	void set_decoder_visible(bool visible);
+
+signals:
+	void delete_decoder();
+
+	void show_hide_decoder();
+
+private:
+	QGridLayout *const _layout;
+	QPushButton _show_hide_button;
+};
+
+} // widgets
+} // pv
+
+#endif // PULSEVIEW_PV_WIDGETS_DECODERGROUPBOX_H
diff --git a/pv/widgets/decodermenu.cpp b/pv/widgets/decodermenu.cpp
new file mode 100644
index 0000000..2f828a2
--- /dev/null
+++ b/pv/widgets/decodermenu.cpp
@@ -0,0 +1,72 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <libsigrokdecode/libsigrokdecode.h>
+
+#include "decodermenu.h"
+
+namespace pv {
+namespace widgets {
+
+DecoderMenu::DecoderMenu(QWidget *parent, bool first_level_decoder) :
+	QMenu(parent),
+	_mapper(this)
+{
+	GSList *l = g_slist_sort(g_slist_copy(
+		(GSList*)srd_decoder_list()), decoder_name_cmp);
+	for(; l; l = l->next)
+	{
+		const srd_decoder *const d = (srd_decoder*)l->data;
+		assert(d);
+
+		const bool have_probes = (d->channels || d->opt_channels) != 0;
+		if (first_level_decoder == have_probes) {
+			QAction *const action =
+				addAction(QString::fromUtf8(d->name));
+			action->setData(qVariantFromValue(l->data));
+			_mapper.setMapping(action, action);
+			connect(action, SIGNAL(triggered()),
+				&_mapper, SLOT(map()));
+		}
+	}
+	g_slist_free(l);
+
+	connect(&_mapper, SIGNAL(mapped(QObject*)),
+		this, SLOT(on_action(QObject*)));
+}
+
+int DecoderMenu::decoder_name_cmp(const void *a, const void *b)
+{
+	return strcmp(((const srd_decoder*)a)->name,
+		((const srd_decoder*)b)->name);
+}
+
+void DecoderMenu::on_action(QObject *action)
+{
+	assert(action);
+	srd_decoder *const dec =
+		(srd_decoder*)((QAction*)action)->data().value<void*>();
+	assert(dec);
+
+	decoder_selected(dec);	
+}
+
+} // widgets
+} // pv
diff --git a/pv/widgets/decodermenu.h b/pv/widgets/decodermenu.h
new file mode 100644
index 0000000..20dd412
--- /dev/null
+++ b/pv/widgets/decodermenu.h
@@ -0,0 +1,56 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_WIDGETS_DECODERMENU_H
+#define PULSEVIEW_PV_WIDGETS_DECODERMENU_H
+
+#include <QMenu>
+#include <QSignalMapper>
+
+struct srd_decoder;
+
+namespace pv {
+namespace widgets {
+
+class DecoderMenu : public QMenu
+{
+	Q_OBJECT;
+
+public:
+	DecoderMenu(QWidget *parent, bool first_level_decoder = false);
+
+private:
+	static int decoder_name_cmp(const void *a, const void *b);
+
+
+private slots:
+	void on_action(QObject *action);
+
+signals:
+	void decoder_selected(srd_decoder *decoder);
+
+private:
+	QSignalMapper _mapper;
+};
+
+} // widgets
+} // pv
+
+#endif // PULSEVIEW_PV_WIDGETS_DECODERMENU_H
diff --git a/pv/widgets/popup.cpp b/pv/widgets/popup.cpp
new file mode 100644
index 0000000..6857358
--- /dev/null
+++ b/pv/widgets/popup.cpp
@@ -0,0 +1,269 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <algorithm>
+
+#include <assert.h>
+
+#include <QtGui>
+
+#include "popup.h"
+
+using std::max;
+using std::min;
+
+namespace pv {
+namespace widgets {
+
+const unsigned int Popup::ArrowLength = 10;
+const unsigned int Popup::ArrowOverlap = 3;
+const unsigned int Popup::MarginWidth = 6;
+
+Popup::Popup(QWidget *parent) :
+	QWidget(parent, Qt::Popup | Qt::FramelessWindowHint),
+	_point(),
+	_pos(Left)
+{
+}
+
+const QPoint& Popup::point() const
+{
+	return _point;
+}
+
+Popup::Position Popup::position() const
+{
+	return _pos;
+}
+
+void Popup::set_position(const QPoint point, Position pos)
+{
+	_point = point, _pos = pos;
+
+	setContentsMargins(
+		MarginWidth + ((pos == Right) ? ArrowLength : 0),
+		MarginWidth + ((pos == Bottom) ? ArrowLength : 0),
+		MarginWidth + ((pos == Left) ? ArrowLength : 0),
+		MarginWidth + ((pos == Top) ? ArrowLength : 0));
+
+}
+
+bool Popup::space_for_arrow() const
+{
+	// Check if there is room for the arrow
+	switch (_pos) {
+	case Right:
+		if (_point.x() > x())
+			return false;
+		return true;
+
+	case Bottom:
+		if (_point.y() > y())
+			return false;
+		return true;		
+
+	case Left:
+		if (_point.x() < (x() + width()))
+			return false;
+		return true;
+
+	case Top:
+		if (_point.y() < (y() + height()))
+			return false;
+		return true;
+	}
+
+	return true;
+}
+
+QPolygon Popup::arrow_polygon() const
+{
+	QPolygon poly;
+
+	const QPoint p = mapFromGlobal(_point);
+	const int l = ArrowLength + ArrowOverlap; 
+
+	switch (_pos)
+        {
+	case Right:
+		poly << QPoint(p.x() + l, p.y() - l);
+		break;
+
+	case Bottom:
+		poly << QPoint(p.x() - l, p.y() + l);
+		break;
+
+        case Left:
+	case Top:
+		poly << QPoint(p.x() - l, p.y() - l);
+		break;
+	}
+
+	poly << p;
+
+	switch (_pos)
+        {
+	case Right:
+	case Bottom:
+		poly << QPoint(p.x() + l, p.y() + l);
+		break;
+
+        case Left:
+		poly << QPoint(p.x() - l, p.y() + l);
+		break;
+		
+	case Top:
+		poly << QPoint(p.x() + l, p.y() - l);
+		break;
+	}
+
+	return poly;
+}
+
+QRegion Popup::arrow_region() const
+{
+	return QRegion(arrow_polygon());
+}
+
+QRect Popup::bubble_rect() const
+{
+	return QRect(
+		QPoint((_pos == Right) ? ArrowLength : 0,
+			(_pos == Bottom) ? ArrowLength : 0),
+		QSize(width() - ((_pos == Left || _pos == Right) ?
+				ArrowLength : 0),
+			height() - ((_pos == Top || _pos == Bottom) ?
+				ArrowLength : 0)));
+}
+
+QRegion Popup::bubble_region() const
+{
+	const QRect rect(bubble_rect());
+
+	const unsigned int r = MarginWidth;
+	const unsigned int d = 2 * r;
+	return QRegion(rect.adjusted(r, 0, -r, 0)).united(
+		QRegion(rect.adjusted(0, r, 0, -r))).united(
+		QRegion(rect.left(), rect.top(), d, d,
+			QRegion::Ellipse)).united(
+		QRegion(rect.right() - d, rect.top(), d, d,
+			QRegion::Ellipse)).united(
+		QRegion(rect.left(), rect.bottom() - d, d, d,
+			QRegion::Ellipse)).united(
+		QRegion(rect.right() - d, rect.bottom() - d, d, d,
+			QRegion::Ellipse));
+}
+
+QRegion Popup::popup_region() const
+{
+	if (space_for_arrow())
+		return arrow_region().united(bubble_region());
+	else
+		return bubble_region();
+}
+
+void Popup::reposition_widget()
+{
+	QPoint o;
+
+	const QRect screen_rect = QApplication::desktop()->availableGeometry(
+		QApplication::desktop()->screenNumber(_point));
+
+	if (_pos == Right || _pos == Left)
+		o.ry() = -height() / 2;
+	else
+		o.rx() = -width() / 2;
+
+	if (_pos == Left)
+		o.rx() = -width();
+	else if(_pos == Top)
+		o.ry() = -height();
+
+	o += _point;
+	move(max(min(o.x(), screen_rect.right() - width()),
+			screen_rect.left()),
+		max(min(o.y(), screen_rect.bottom() - height()),
+			screen_rect.top()));
+}
+
+void Popup::closeEvent(QCloseEvent*)
+{
+	closed();
+}
+
+void Popup::paintEvent(QPaintEvent*)
+{
+	QPainter painter(this);
+	painter.setRenderHint(QPainter::Antialiasing);
+
+	const QColor outline_color(QApplication::palette().color(
+		QPalette::Dark));
+
+	// Draw the bubble
+	const QRegion b = bubble_region();
+	const QRegion bubble_outline = QRegion(rect()).subtracted(
+		b.translated(1, 0).intersected(b.translated(0, 1).intersected(
+		b.translated(-1, 0).intersected(b.translated(0, -1)))));
+
+	painter.setPen(Qt::NoPen);
+	painter.setBrush(QApplication::palette().brush(QPalette::Window));
+	painter.drawRect(rect());
+
+	// Draw the arrow
+	if (!space_for_arrow())
+		return;
+
+	const QPoint ArrowOffsets[] = {
+		QPoint(1, 0), QPoint(0, -1), QPoint(-1, 0), QPoint(0, 1)};
+
+	const QRegion a(arrow_region());
+	const QRegion arrow_outline = a.subtracted(
+		a.translated(ArrowOffsets[_pos]));
+
+	painter.setClipRegion(bubble_outline.subtracted(a).united(
+		arrow_outline));
+	painter.setBrush(outline_color);
+	painter.drawRect(rect());
+}
+
+void Popup::resizeEvent(QResizeEvent*)
+{
+	reposition_widget();
+	setMask(popup_region());
+}
+
+void Popup::mouseReleaseEvent(QMouseEvent *e)
+{
+	assert(e);
+
+	// We need our own out-of-bounds click handler because QWidget counts
+	// the drop-shadow region as inside the widget
+	if(!bubble_rect().contains(e->pos()))
+		close();
+}
+
+void Popup::showEvent(QShowEvent*)
+{
+	reposition_widget();
+}
+
+} // namespace widgets
+} // namespace pv
+
diff --git a/pv/widgets/popup.h b/pv/widgets/popup.h
new file mode 100644
index 0000000..0bc6d0f
--- /dev/null
+++ b/pv/widgets/popup.h
@@ -0,0 +1,93 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_WIDGETS_POPUP_H
+#define PULSEVIEW_PV_WIDGETS_POPUP_H
+
+#include <QWidget>
+
+namespace pv {
+namespace widgets {
+
+class Popup : public QWidget
+{
+	Q_OBJECT
+
+public:
+	enum Position
+	{
+		Right,
+		Top,
+		Left,
+		Bottom
+	};
+
+private:
+	static const unsigned int ArrowLength;
+	static const unsigned int ArrowOverlap;
+	static const unsigned int MarginWidth;
+
+public:
+	Popup(QWidget *parent);
+
+	const QPoint& point() const;
+	Position position() const;
+
+	void set_position(const QPoint point, Position pos);
+
+private:
+	bool space_for_arrow() const;
+
+	QPolygon arrow_polygon() const;
+
+	QRegion arrow_region() const;
+
+	QRect bubble_rect() const;
+
+	QRegion bubble_region() const;
+
+	QRegion popup_region() const;
+
+	void reposition_widget();
+
+private:
+	void closeEvent(QCloseEvent*);
+
+	void paintEvent(QPaintEvent*);
+
+	void resizeEvent(QResizeEvent*);
+
+	void mouseReleaseEvent(QMouseEvent *e);
+
+protected:
+	void showEvent(QShowEvent *e);
+
+signals:
+	void closed();
+
+private:
+	QPoint _point;
+	Position _pos;
+};
+
+} // namespace widgets
+} // namespace pv
+
+#endif // PULSEVIEW_PV_WIDGETS_POPUP_H
diff --git a/pv/widgets/popuptoolbutton.cpp b/pv/widgets/popuptoolbutton.cpp
new file mode 100644
index 0000000..74743ed
--- /dev/null
+++ b/pv/widgets/popuptoolbutton.cpp
@@ -0,0 +1,58 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <assert.h>
+
+#include "popuptoolbutton.h"
+
+namespace pv {
+namespace widgets {
+
+PopupToolButton::PopupToolButton(QWidget *parent) :
+	QToolButton(parent),
+	_popup(NULL)
+{
+	connect(this, SIGNAL(clicked(bool)), this, SLOT(on_clicked(bool)));
+}
+
+Popup* PopupToolButton::popup() const
+{
+	return _popup;
+}
+
+void PopupToolButton::set_popup(Popup *popup)
+{
+	assert(popup);
+	_popup = popup;
+}
+
+void PopupToolButton::on_clicked(bool)
+{
+	if(!_popup)
+		return;
+
+	const QRect r = rect();
+	_popup->set_position(mapToGlobal(QPoint((r.left() + r.right()) / 2,
+		((r.top() + r.bottom() * 3) / 4))), Popup::Bottom);
+	_popup->show();
+}
+
+} // widgets
+} // pv
diff --git a/pv/widgets/popuptoolbutton.h b/pv/widgets/popuptoolbutton.h
new file mode 100644
index 0000000..b619b6f
--- /dev/null
+++ b/pv/widgets/popuptoolbutton.h
@@ -0,0 +1,52 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_WIDGETS_POPUPTOOLBUTTON_H
+#define PULSEVIEW_PV_WIDGETS_POPUPTOOLBUTTON_H
+
+#include "popup.h"
+
+#include <QToolButton>
+
+namespace pv {
+namespace widgets {
+
+class PopupToolButton : public QToolButton
+{
+	Q_OBJECT;
+
+public:
+	PopupToolButton(QWidget *parent);
+
+	Popup* popup() const;
+
+	void set_popup(Popup *popup);
+
+private slots:
+	void on_clicked(bool);
+
+private:
+	Popup *_popup;
+};
+
+} // widgets
+} // pv
+
+#endif // PULSEVIEW_PV_WIDGETS_POPUPTOOLBUTTON_H
diff --git a/pv/widgets/sweeptimingwidget.cpp b/pv/widgets/sweeptimingwidget.cpp
new file mode 100644
index 0000000..00ebb76
--- /dev/null
+++ b/pv/widgets/sweeptimingwidget.cpp
@@ -0,0 +1,184 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "sweeptimingwidget.h"
+
+#include <vector>
+
+#include <assert.h>
+
+#include <extdef.h>
+
+using std::vector;
+
+namespace pv {
+namespace widgets {
+
+SweepTimingWidget::SweepTimingWidget(const char *suffix,
+	QWidget *parent) :
+	QWidget(parent),
+	_suffix(suffix),
+	_layout(this),
+	_value(this),
+	_list(this),
+	_value_type(None)
+{
+	setContentsMargins(0, 0, 0, 0);
+
+	_value.setDecimals(0);
+	_value.setSuffix(QString::fromUtf8(suffix));
+
+	connect(&_list, SIGNAL(currentIndexChanged(int)),
+		this, SIGNAL(value_changed()));
+	connect(&_value, SIGNAL(editingFinished()),
+		this, SIGNAL(value_changed()));
+
+	setLayout(&_layout);
+	_layout.setMargin(0);
+	_layout.addWidget(&_list);
+	_layout.addWidget(&_value);
+
+	show_none();
+}
+
+void SweepTimingWidget::show_none()
+{
+	_value_type = None;
+	_value.hide();
+	_list.hide();
+}
+
+void SweepTimingWidget::show_min_max_step(uint64_t min, uint64_t max,
+	uint64_t step)
+{
+	assert(max > min);
+	assert(step > 0);
+
+	_value_type = MinMaxStep;
+
+	_value.setRange(min, max);
+	_value.setSingleStep(step);
+
+	_value.show();
+	_list.hide();
+}
+
+void SweepTimingWidget::show_list(const uint64_t *vals, size_t count)
+{
+	_value_type = List;
+
+	_list.clear();
+	for (size_t i = 0; i < count; i++)
+	{
+		char *const s = sr_si_string_u64(vals[i], _suffix);
+		_list.addItem(QString::fromUtf8(s),
+			qVariantFromValue(vals[i]));
+		g_free(s);
+	}
+
+	_value.hide();
+	_list.show();
+}
+
+void SweepTimingWidget::show_125_list(uint64_t min, uint64_t max)
+{
+	assert(max > min);
+
+	// Create a 1-2-5-10 list of entries.
+	const unsigned int FineScales[] = {1, 2, 5};
+	uint64_t value, decade;
+	unsigned int fine;
+	vector<uint64_t> values;
+
+	// Compute the starting decade
+	for (decade = 1; decade * 10 <= min; decade *= 10);
+
+	// Compute the first entry
+	for (fine = 0; fine < countof(FineScales); fine++)
+		if (FineScales[fine] * decade >= min)
+			break;
+
+	assert(fine < countof(FineScales));
+
+	// Add the minimum entry if it's not on the 1-2-5 progression
+	if (min != FineScales[fine] * decade)
+		values.push_back(min);
+
+	while ((value = FineScales[fine] * decade) < max) {
+		values.push_back(value);
+		if (++fine >= countof(FineScales))
+			fine = 0, decade *= 10;
+	}
+
+	// Add the max value
+	values.push_back(max);
+
+	// Make a C array, and give it to the sweep timing widget
+	uint64_t *const values_array = new uint64_t[values.size()];
+	copy(values.begin(), values.end(), values_array);
+	show_list(values_array, values.size());
+	delete[] values_array;
+}
+
+uint64_t SweepTimingWidget::value() const
+{
+	switch(_value_type)
+	{
+	case None:
+		return 0;
+
+	case MinMaxStep:
+		return (uint64_t)_value.value();
+
+	case List:
+	{
+		const int index = _list.currentIndex();
+		return (index >= 0) ? _list.itemData(
+			index).value<uint64_t>() : 0;
+	}
+
+	default:
+		// Unexpected value type
+		assert(0);
+		return 0;
+	}
+}
+
+void SweepTimingWidget::set_value(uint64_t value)
+{
+	_value.setValue(value);
+
+	int best_match = _list.count() - 1;
+	int64_t best_variance = INT64_MAX;
+
+	for (int i = 0; i < _list.count(); i++) {
+		const int64_t this_variance = abs(
+			(int64_t)value - _list.itemData(i).value<int64_t>());
+		if (this_variance < best_variance) {
+			best_variance = this_variance;
+			best_match = i;
+		}
+	}
+
+	_list.setCurrentIndex(best_match);
+}
+
+} // widgets
+} // pv
diff --git a/pv/widgets/sweeptimingwidget.h b/pv/widgets/sweeptimingwidget.h
new file mode 100644
index 0000000..b66188f
--- /dev/null
+++ b/pv/widgets/sweeptimingwidget.h
@@ -0,0 +1,75 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_WIDGETS_SWEEPTIMINGWIDGET_H
+#define PULSEVIEW_PV_WIDGETS_SWEEPTIMINGWIDGET_H
+
+#include <libsigrok/libsigrok.h>
+
+#include <QComboBox>
+#include <QDoubleSpinBox>
+#include <QHBoxLayout>
+#include <QLineEdit>
+#include <QWidget>
+
+namespace pv {
+namespace widgets {
+
+class SweepTimingWidget : public QWidget
+{
+	Q_OBJECT
+
+private:
+	enum ValueType
+	{
+		None,
+		MinMaxStep,
+		List
+	};
+
+public:
+	SweepTimingWidget(const char *suffix, QWidget *parent = NULL);
+
+	void show_none();
+	void show_min_max_step(uint64_t min, uint64_t max, uint64_t step);
+	void show_list(const uint64_t *vals, size_t count);
+	void show_125_list(uint64_t min, uint64_t max);
+
+	uint64_t value() const;
+	void set_value(uint64_t value);
+
+signals:
+	void value_changed();
+
+private:
+	const char *const _suffix;
+
+	QHBoxLayout _layout;
+
+	QDoubleSpinBox _value;
+	QComboBox _list;
+
+	ValueType _value_type;
+};
+
+} // widgets
+} // pv
+
+#endif // PULSEVIEW_PV_WIDGETS_SWEEPTIMINGWIDGET_H
diff --git a/pv/widgets/wellarray.cpp b/pv/widgets/wellarray.cpp
new file mode 100644
index 0000000..dcc3b41
--- /dev/null
+++ b/pv/widgets/wellarray.cpp
@@ -0,0 +1,290 @@
+/****************************************************************************
+**
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Contact: http://www.qt-project.org/legal
+**
+** This file is part of the QtGui module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and Digia.  For licensing terms and
+** conditions see http://qt.digia.com/licensing.  For further information
+** use the contact form at http://qt.digia.com/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 2.1 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL included in the
+** packaging of this file.  Please review the following information to
+** ensure the GNU Lesser General Public License version 2.1 requirements
+** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** In addition, as a special exception, Digia gives you certain additional
+** rights.  These rights are described in the Digia Qt LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3.0 as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL included in the
+** packaging of this file.  Please review the following information to
+** ensure the GNU General Public License version 3.0 requirements will be
+** met: http://www.gnu.org/copyleft/gpl.html.
+**
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QPainter>
+#include <QPaintEvent>
+#include <QStyle>
+#include <QStyleOptionFrame>
+
+#include "wellarray.h"
+
+void QWellArray::paintEvent(QPaintEvent *e)
+{
+    QRect r = e->rect();
+    int cx = r.x();
+    int cy = r.y();
+    int ch = r.height();
+    int cw = r.width();
+    int colfirst = columnAt(cx);
+    int collast = columnAt(cx + cw);
+    int rowfirst = rowAt(cy);
+    int rowlast = rowAt(cy + ch);
+
+    if (isRightToLeft()) {
+        int t = colfirst;
+        colfirst = collast;
+        collast = t;
+    }
+
+    QPainter painter(this);
+    QPainter *p = &painter;
+    QRect rect(0, 0, cellWidth(), cellHeight());
+
+
+    if (collast < 0 || collast >= ncols)
+        collast = ncols-1;
+    if (rowlast < 0 || rowlast >= nrows)
+        rowlast = nrows-1;
+
+    // Go through the rows
+    for (int r = rowfirst; r <= rowlast; ++r) {
+        // get row position and height
+        int rowp = rowY(r);
+
+        // Go through the columns in the row r
+        // if we know from where to where, go through [colfirst, collast],
+        // else go through all of them
+        for (int c = colfirst; c <= collast; ++c) {
+            // get position and width of column c
+            int colp = columnX(c);
+            // Translate painter and draw the cell
+            rect.translate(colp, rowp);
+            paintCell(p, r, c, rect);
+            rect.translate(-colp, -rowp);
+        }
+    }
+}
+
+struct QWellArrayData {
+    QBrush *brush;
+};
+
+QWellArray::QWellArray(int rows, int cols, QWidget *parent)
+    : QWidget(parent)
+        ,nrows(rows), ncols(cols)
+{
+    d = 0;
+    setFocusPolicy(Qt::StrongFocus);
+    cellw = 28;
+    cellh = 24;
+    curCol = 0;
+    curRow = 0;
+    selCol = -1;
+    selRow = -1;
+}
+
+QSize QWellArray::sizeHint() const
+{
+    ensurePolished();
+    return gridSize().boundedTo(QSize(640, 480));
+}
+
+
+void QWellArray::paintCell(QPainter* p, int row, int col, const QRect &rect)
+{
+    int b = 3; //margin
+
+    const QPalette & g = palette();
+    QStyleOptionFrame opt;
+    int dfw = style()->pixelMetric(QStyle::PM_DefaultFrameWidth);
+    opt.lineWidth = dfw;
+    opt.midLineWidth = 1;
+    opt.rect = rect.adjusted(b, b, -b, -b);
+    opt.palette = g;
+    opt.state = QStyle::State_Enabled | QStyle::State_Sunken;
+    style()->drawPrimitive(QStyle::PE_Frame, &opt, p, this);
+    b += dfw;
+
+    if ((row == curRow) && (col == curCol)) {
+        if (hasFocus()) {
+            QStyleOptionFocusRect opt;
+            opt.palette = g;
+            opt.rect = rect;
+            opt.state = QStyle::State_None | QStyle::State_KeyboardFocusChange;
+            style()->drawPrimitive(QStyle::PE_FrameFocusRect, &opt, p, this);
+        }
+    }
+    paintCellContents(p, row, col, opt.rect.adjusted(dfw, dfw, -dfw, -dfw));
+}
+
+/*!
+  Reimplement this function to change the contents of the well array.
+ */
+void QWellArray::paintCellContents(QPainter *p, int row, int col, const QRect &r)
+{
+    if (d) {
+        p->fillRect(r, d->brush[row*numCols()+col]);
+    } else {
+        p->fillRect(r, Qt::white);
+        p->setPen(Qt::black);
+        p->drawLine(r.topLeft(), r.bottomRight());
+        p->drawLine(r.topRight(), r.bottomLeft());
+    }
+}
+
+void QWellArray::mousePressEvent(QMouseEvent *e)
+{
+    // The current cell marker is set to the cell the mouse is pressed in
+    QPoint pos = e->pos();
+    setCurrent(rowAt(pos.y()), columnAt(pos.x()));
+}
+
+void QWellArray::mouseReleaseEvent(QMouseEvent * /* event */)
+{
+    // The current cell marker is set to the cell the mouse is clicked in
+    setSelected(curRow, curCol);
+}
+
+
+/*
+  Sets the cell currently having the focus. This is not necessarily
+  the same as the currently selected cell.
+*/
+
+void QWellArray::setCurrent(int row, int col)
+{
+    if ((curRow == row) && (curCol == col))
+        return;
+
+    if (row < 0 || col < 0)
+        row = col = -1;
+
+    int oldRow = curRow;
+    int oldCol = curCol;
+
+    curRow = row;
+    curCol = col;
+
+    updateCell(oldRow, oldCol);
+    updateCell(curRow, curCol);
+}
+
+/*
+  Sets the currently selected cell to \a row, \a column. If \a row or
+  \a column are less than zero, the current cell is unselected.
+
+  Does not set the position of the focus indicator.
+*/
+void QWellArray::setSelected(int row, int col)
+{
+    int oldRow = selRow;
+    int oldCol = selCol;
+
+    if (row < 0 || col < 0)
+        row = col = -1;
+
+    selCol = col;
+    selRow = row;
+
+    updateCell(oldRow, oldCol);
+    updateCell(selRow, selCol);
+    if (row >= 0)
+        emit selected(row, col);
+}
+
+void QWellArray::focusInEvent(QFocusEvent*)
+{
+    updateCell(curRow, curCol);
+}
+
+void QWellArray::setCellBrush(int row, int col, const QBrush &b)
+{
+    if (!d) {
+        d = new QWellArrayData;
+        int i = numRows()*numCols();
+        d->brush = new QBrush[i];
+    }
+    if (row >= 0 && row < numRows() && col >= 0 && col < numCols())
+        d->brush[row*numCols()+col] = b;
+}
+
+/*
+  Returns the brush set for the cell at \a row, \a column. If no brush is
+  set, Qt::NoBrush is returned.
+*/
+
+QBrush QWellArray::cellBrush(int row, int col)
+{
+    if (d && row >= 0 && row < numRows() && col >= 0 && col < numCols())
+        return d->brush[row*numCols()+col];
+    return Qt::NoBrush;
+}
+
+
+
+/*!\reimp
+*/
+
+void QWellArray::focusOutEvent(QFocusEvent*)
+{
+    updateCell(curRow, curCol);
+}
+
+/*\reimp
+*/
+void QWellArray::keyPressEvent(QKeyEvent* e)
+{
+    switch(e->key()) {                        // Look at the key code
+    case Qt::Key_Left:                                // If 'left arrow'-key,
+        if(curCol > 0)                        // and cr't not in leftmost col
+            setCurrent(curRow, curCol - 1);        // set cr't to next left column
+        break;
+    case Qt::Key_Right:                                // Correspondingly...
+        if(curCol < numCols()-1)
+            setCurrent(curRow, curCol + 1);
+        break;
+    case Qt::Key_Up:
+        if(curRow > 0)
+            setCurrent(curRow - 1, curCol);
+        break;
+    case Qt::Key_Down:
+        if(curRow < numRows()-1)
+            setCurrent(curRow + 1, curCol);
+        break;
+    case Qt::Key_Space:
+        setSelected(curRow, curCol);
+        break;
+    default:                                // If not an interesting key,
+        e->ignore();                        // we don't accept the event
+        return;
+    }
+
+}
diff --git a/pv/widgets/wellarray.h b/pv/widgets/wellarray.h
new file mode 100644
index 0000000..dad088c
--- /dev/null
+++ b/pv/widgets/wellarray.h
@@ -0,0 +1,133 @@
+/****************************************************************************
+**
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Contact: http://www.qt-project.org/legal
+**
+** This file is part of the QtGui module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and Digia.  For licensing terms and
+** conditions see http://qt.digia.com/licensing.  For further information
+** use the contact form at http://qt.digia.com/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 2.1 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL included in the
+** packaging of this file.  Please review the following information to
+** ensure the GNU Lesser General Public License version 2.1 requirements
+** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** In addition, as a special exception, Digia gives you certain additional
+** rights.  These rights are described in the Digia Qt LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3.0 as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL included in the
+** packaging of this file.  Please review the following information to
+** ensure the GNU General Public License version 3.0 requirements will be
+** met: http://www.gnu.org/copyleft/gpl.html.
+**
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QWidget>
+
+struct QWellArrayData;
+
+class QWellArray : public QWidget
+{
+    Q_OBJECT
+    Q_PROPERTY(int selectedColumn READ selectedColumn)
+    Q_PROPERTY(int selectedRow READ selectedRow)
+
+public:
+    QWellArray(int rows, int cols, QWidget* parent=0);
+    QString cellContent(int row, int col) const;
+
+    int selectedColumn() const { return selCol; }
+    int selectedRow() const { return selRow; }
+
+    virtual void setCurrent(int row, int col);
+    virtual void setSelected(int row, int col);
+
+    QSize sizeHint() const;
+
+    virtual void setCellBrush(int row, int col, const QBrush &);
+    QBrush cellBrush(int row, int col);
+
+    inline int cellWidth() const
+        { return cellw; }
+
+    inline int cellHeight() const
+        { return cellh; }
+
+    inline int rowAt(int y) const
+        { return y / cellh; }
+
+    inline int columnAt(int x) const
+        { if (isRightToLeft()) return ncols - (x / cellw) - 1; return x / cellw; }
+
+    inline int rowY(int row) const
+        { return cellh * row; }
+
+    inline int columnX(int column) const
+        { if (isRightToLeft()) return cellw * (ncols - column - 1); return cellw * column; }
+
+    inline int numRows() const
+        { return nrows; }
+
+    inline int numCols() const
+        {return ncols; }
+
+    inline QRect cellRect() const
+        { return QRect(0, 0, cellw, cellh); }
+
+    inline QSize gridSize() const
+        { return QSize(ncols * cellw, nrows * cellh); }
+
+    QRect cellGeometry(int row, int column)
+        {
+            QRect r;
+            if (row >= 0 && row < nrows && column >= 0 && column < ncols)
+                r.setRect(columnX(column), rowY(row), cellw, cellh);
+            return r;
+        }
+
+    inline void updateCell(int row, int column) { update(cellGeometry(row, column)); }
+
+signals:
+    void selected(int row, int col);
+
+protected:
+    virtual void paintCell(QPainter *, int row, int col, const QRect&);
+    virtual void paintCellContents(QPainter *, int row, int col, const QRect&);
+
+    void mousePressEvent(QMouseEvent*);
+    void mouseReleaseEvent(QMouseEvent*);
+    void keyPressEvent(QKeyEvent*);
+    void focusInEvent(QFocusEvent*);
+    void focusOutEvent(QFocusEvent*);
+    void paintEvent(QPaintEvent *);
+
+private:
+    Q_DISABLE_COPY(QWellArray)
+
+    int nrows;
+    int ncols;
+    int cellw;
+    int cellh;
+    int curRow;
+    int curCol;
+    int selRow;
+    int selCol;
+    QWellArrayData *d;
+};
diff --git a/signalhandler.cpp b/signalhandler.cpp
new file mode 100644
index 0000000..8c9244b
--- /dev/null
+++ b/signalhandler.cpp
@@ -0,0 +1,95 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Adam Reichold
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "signalhandler.h"
+
+#include <assert.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+#include <QDebug>
+#include <QSocketNotifier>
+
+int SignalHandler::_sockets[2];
+
+bool SignalHandler::prepare_signals()
+{
+	if(socketpair(AF_UNIX, SOCK_STREAM, 0, _sockets) != 0)
+		return false;
+
+	struct sigaction sig_action;
+
+	sig_action.sa_handler = SignalHandler::handle_signals;
+	sigemptyset(&sig_action.sa_mask);
+	sig_action.sa_flags = SA_RESTART;
+
+	if(sigaction(SIGINT, &sig_action, 0) != 0 ||
+		sigaction(SIGTERM, &sig_action, 0) != 0) {
+		close(_sockets[0]);
+		close(_sockets[1]);
+		return false;
+	}
+
+	return true;
+}
+
+SignalHandler::SignalHandler(QObject* parent) : QObject(parent),
+	_socket_notifier(0)
+{
+	_socket_notifier = new QSocketNotifier(_sockets[1],
+		QSocketNotifier::Read, this);
+	connect(_socket_notifier, SIGNAL(activated(int)),
+		SLOT(on_socket_notifier_activated()));
+}
+
+void SignalHandler::on_socket_notifier_activated()
+{
+	_socket_notifier->setEnabled(false);
+
+	int sig_number;
+	if(read(_sockets[1], &sig_number, sizeof(int)) !=
+		sizeof(int)) {
+		qDebug() << "Failed to catch signal";
+		abort();
+	}
+
+	switch(sig_number)
+	{
+	case SIGINT:
+		emit int_received();
+		break;
+	case SIGTERM:
+		emit term_received();
+		break;
+	}
+
+	_socket_notifier->setEnabled(true);
+}
+
+void SignalHandler::handle_signals(int sig_number)
+{
+	if(write(_sockets[0], &sig_number, sizeof(int)) !=
+		sizeof(int)) {
+		// Failed to handle signal
+		abort();
+	}
+}
diff --git a/signalhandler.h b/signalhandler.h
new file mode 100644
index 0000000..7ecd612
--- /dev/null
+++ b/signalhandler.h
@@ -0,0 +1,55 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Adam Reichold
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef SIGNALHANDLER_H
+#define SIGNALHANDLER_H
+
+#include <QObject>
+
+class QSocketNotifier;
+
+class SignalHandler : public QObject
+{
+	Q_OBJECT
+
+public:
+	static bool prepare_signals();
+
+public:
+	explicit SignalHandler(QObject* parent = NULL);
+
+signals:
+	void int_received();
+	void term_received();
+
+private slots:
+	void on_socket_notifier_activated();
+
+private:
+	static void handle_signals(int sig_number);
+
+private:
+	QSocketNotifier* _socket_notifier;
+
+private:
+	static int _sockets[2];
+};
+
+#endif // SIGNALHANDLER_H
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
new file mode 100644
index 0000000..0ea69f0
--- /dev/null
+++ b/test/CMakeLists.txt
@@ -0,0 +1,176 @@
+##
+## This file is part of the PulseView project.
+##
+## Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+## Copyright (C) 2012 Alexandru Gagniuc <mr.nuke.me at gmail.com>
+##
+## This program is free software: you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation, either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program.  If not, see <http://www.gnu.org/licenses/>.
+##
+
+option(ENABLE_DECODE "Build with libsigrokdecode" FALSE)
+
+list(APPEND PKGDEPS libsigrok>=0.3.0)
+
+if(ENABLE_DECODE)
+	list(APPEND PKGDEPS libsigrokdecode>=0.3.0)
+endif()
+
+find_package(PkgConfig)
+pkg_check_modules(PKGDEPS REQUIRED ${PKGDEPS})
+
+# Find the platform's thread library (needed for boost-thread).
+# This will set ${CMAKE_THREAD_LIBS_INIT} to the correct, OS-specific value.
+find_package(Threads)
+
+if(WIN32)
+	# On Windows/MinGW we need to use 'thread_win32' instead of 'thread'.
+	# The library is named libboost_thread_win32* (not libboost_thread*).
+	find_package(Boost 1.42 COMPONENTS filesystem system thread_win32 unit_test_framework REQUIRED)
+else()
+	find_package(Boost 1.42 COMPONENTS filesystem system thread unit_test_framework REQUIRED)
+endif()
+
+
+find_program(QT_QMAKE_EXECUTABLE NAMES qmake4 qmake-qt4 qmake-mac)
+find_package(Qt4 REQUIRED)
+
+set(pulseview_TEST_SOURCES
+	${PROJECT_SOURCE_DIR}/pv/devicemanager.cpp
+	${PROJECT_SOURCE_DIR}/pv/sigsession.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/cursorpair.cpp
+	${PROJECT_SOURCE_DIR}/pv/data/analog.cpp
+	${PROJECT_SOURCE_DIR}/pv/data/analogsnapshot.cpp
+	${PROJECT_SOURCE_DIR}/pv/data/logic.cpp
+	${PROJECT_SOURCE_DIR}/pv/data/logicsnapshot.cpp
+	${PROJECT_SOURCE_DIR}/pv/data/snapshot.cpp
+	${PROJECT_SOURCE_DIR}/pv/data/signaldata.cpp
+	${PROJECT_SOURCE_DIR}/pv/device/device.cpp
+	${PROJECT_SOURCE_DIR}/pv/device/devinst.cpp
+	${PROJECT_SOURCE_DIR}/pv/device/file.cpp
+	${PROJECT_SOURCE_DIR}/pv/device/inputfile.cpp
+	${PROJECT_SOURCE_DIR}/pv/device/sessionfile.cpp
+	${PROJECT_SOURCE_DIR}/pv/prop/double.cpp
+	${PROJECT_SOURCE_DIR}/pv/prop/enum.cpp
+	${PROJECT_SOURCE_DIR}/pv/prop/int.cpp
+	${PROJECT_SOURCE_DIR}/pv/prop/property.cpp
+	${PROJECT_SOURCE_DIR}/pv/prop/string.cpp
+	${PROJECT_SOURCE_DIR}/pv/prop/binding/binding.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/analogsignal.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/cursor.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/cursorpair.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/header.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/logicsignal.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/marginwidget.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/ruler.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/selectableitem.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/signal.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/timemarker.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/trace.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/tracepalette.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/view.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/viewport.cpp
+	${PROJECT_SOURCE_DIR}/pv/widgets/colourbutton.cpp
+	${PROJECT_SOURCE_DIR}/pv/widgets/colourpopup.cpp
+	${PROJECT_SOURCE_DIR}/pv/widgets/popup.cpp
+	${PROJECT_SOURCE_DIR}/pv/widgets/wellarray.cpp
+	data/analogsnapshot.cpp
+	data/logicsnapshot.cpp
+	test.cpp
+)
+
+# This list includes only QObject derived class headers.
+set(pulseview_TEST_HEADERS
+	${PROJECT_SOURCE_DIR}/pv/sigsession.h
+	${PROJECT_SOURCE_DIR}/pv/device/devinst.h
+	${PROJECT_SOURCE_DIR}/pv/prop/double.h
+	${PROJECT_SOURCE_DIR}/pv/prop/enum.h
+	${PROJECT_SOURCE_DIR}/pv/prop/int.h
+	${PROJECT_SOURCE_DIR}/pv/prop/property.h
+	${PROJECT_SOURCE_DIR}/pv/prop/string.h
+	${PROJECT_SOURCE_DIR}/pv/view/cursor.h
+	${PROJECT_SOURCE_DIR}/pv/view/header.h
+	${PROJECT_SOURCE_DIR}/pv/view/logicsignal.h
+	${PROJECT_SOURCE_DIR}/pv/view/marginwidget.h
+	${PROJECT_SOURCE_DIR}/pv/view/ruler.h
+	${PROJECT_SOURCE_DIR}/pv/view/selectableitem.h
+	${PROJECT_SOURCE_DIR}/pv/view/signal.h
+	${PROJECT_SOURCE_DIR}/pv/view/timemarker.h
+	${PROJECT_SOURCE_DIR}/pv/view/trace.h
+	${PROJECT_SOURCE_DIR}/pv/view/view.h
+	${PROJECT_SOURCE_DIR}/pv/view/viewport.h
+	${PROJECT_SOURCE_DIR}/pv/widgets/colourbutton.h
+	${PROJECT_SOURCE_DIR}/pv/widgets/colourpopup.h
+	${PROJECT_SOURCE_DIR}/pv/widgets/popup.h
+	${PROJECT_SOURCE_DIR}/pv/widgets/wellarray.h
+)
+
+if(ENABLE_DECODE)
+	list(APPEND pulseview_TEST_SOURCES
+		${PROJECT_SOURCE_DIR}/pv/data/decoderstack.cpp
+		${PROJECT_SOURCE_DIR}/pv/data/decode/annotation.cpp
+		${PROJECT_SOURCE_DIR}/pv/data/decode/decoder.cpp
+		${PROJECT_SOURCE_DIR}/pv/data/decode/row.cpp
+		${PROJECT_SOURCE_DIR}/pv/data/decode/rowdata.cpp
+		${PROJECT_SOURCE_DIR}/pv/prop/binding/decoderoptions.cpp
+		${PROJECT_SOURCE_DIR}/pv/view/decodetrace.cpp
+		${PROJECT_SOURCE_DIR}/pv/widgets/decodergroupbox.cpp
+		${PROJECT_SOURCE_DIR}/pv/widgets/decodermenu.cpp
+		data/decoderstack.cpp
+	)
+
+	list(APPEND pulseview_TEST_HEADERS
+		${PROJECT_SOURCE_DIR}/pv/data/decoderstack.h
+		${PROJECT_SOURCE_DIR}/pv/view/decodetrace.h
+		${PROJECT_SOURCE_DIR}/pv/widgets/decodergroupbox.h
+		${PROJECT_SOURCE_DIR}/pv/widgets/decodermenu.h
+	)
+endif()
+
+qt4_wrap_cpp(pulseview_TEST_HEADERS_MOC ${pulseview_TEST_HEADERS})
+
+if(ENABLE_DECODE)
+	add_definitions(-DENABLE_DECODE)
+endif()
+
+# On MinGW we need to use static linking.
+if(NOT WIN32)
+add_definitions(-DBOOST_TEST_DYN_LINK)
+endif()
+
+add_definitions(${QT_DEFINITIONS})
+
+include_directories(
+	${Boost_INCLUDE_DIRS}
+	${PKGDEPS_INCLUDE_DIRS}
+)
+
+set(PULSEVIEW_LINK_LIBS
+	${Boost_LIBRARIES}
+	${CMAKE_THREAD_LIBS_INIT}
+	${PKGDEPS_LIBRARIES}
+	${QT_LIBRARIES}
+)
+
+if(WIN32)
+	# Workaround for a MinGW linking issue.
+	list(APPEND PULSEVIEW_LINK_LIBS "-llzma -llcms2")
+endif()
+
+add_executable(pulseview-test
+	${pulseview_TEST_SOURCES}
+	${pulseview_TEST_HEADERS_MOC}
+)
+
+target_link_libraries(pulseview-test ${PULSEVIEW_LINK_LIBS})
+
diff --git a/test/data/analogsnapshot.cpp b/test/data/analogsnapshot.cpp
new file mode 100644
index 0000000..200d262
--- /dev/null
+++ b/test/data/analogsnapshot.cpp
@@ -0,0 +1,114 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <extdef.h>
+
+#include <stdint.h>
+
+#include <boost/test/unit_test.hpp>
+
+#include "../../pv/data/analogsnapshot.h"
+
+using pv::data::AnalogSnapshot;
+
+BOOST_AUTO_TEST_SUITE(AnalogSnapshotTest)
+
+void push_analog(AnalogSnapshot &s, unsigned int num_samples,
+	float value)
+{
+	float *const data = new float[num_samples];
+	for (unsigned int i = 0; i < num_samples; i++)
+		data[i] = value;
+
+	s.append_interleaved_samples(data, num_samples, 1);
+	delete[] data;
+}
+
+BOOST_AUTO_TEST_CASE(Basic)
+{
+	// Create an empty AnalogSnapshot object
+	AnalogSnapshot s;
+
+	//----- Test AnalogSnapshot::push_analog -----//
+
+	BOOST_CHECK(s.get_sample_count() == 0);
+	for (unsigned int i = 0; i < AnalogSnapshot::ScaleStepCount; i++)
+	{
+		const AnalogSnapshot::Envelope &m = s._envelope_levels[i];
+		BOOST_CHECK_EQUAL(m.length, 0);
+		BOOST_CHECK_EQUAL(m.data_length, 0);
+		BOOST_CHECK(m.samples == NULL);
+	}
+
+	// Push 8 samples of all zeros
+	push_analog(s, 8, 0.0f);
+
+	BOOST_CHECK(s.get_sample_count() == 8);
+
+	// There should not be enough samples to have a single mip map sample
+	for (unsigned int i = 0; i < AnalogSnapshot::ScaleStepCount; i++)
+	{
+		const AnalogSnapshot::Envelope &m = s._envelope_levels[i];
+		BOOST_CHECK_EQUAL(m.length, 0);
+		BOOST_CHECK_EQUAL(m.data_length, 0);
+		BOOST_CHECK(m.samples == NULL);
+	}
+
+	// Push 8 samples of 1.0s to bring the total up to 16
+	push_analog(s, 8, 1.0f);
+
+	// There should now be enough data for exactly one sample
+	// in mip map level 0, and that sample should be 0
+	const AnalogSnapshot::Envelope &e0 = s._envelope_levels[0];
+	BOOST_CHECK_EQUAL(e0.length, 1);
+	BOOST_CHECK_EQUAL(e0.data_length, AnalogSnapshot::EnvelopeDataUnit);
+	BOOST_REQUIRE(e0.samples != NULL);
+	BOOST_CHECK_EQUAL(e0.samples[0].min, 0.0f);
+	BOOST_CHECK_EQUAL(e0.samples[0].max, 1.0f);
+
+	// The higher levels should still be empty
+	for (unsigned int i = 1; i < AnalogSnapshot::ScaleStepCount; i++)
+	{
+		const AnalogSnapshot::Envelope &m = s._envelope_levels[i];
+		BOOST_CHECK_EQUAL(m.length, 0);
+		BOOST_CHECK_EQUAL(m.data_length, 0);
+		BOOST_CHECK(m.samples == NULL);
+	}
+
+	// Push 240 samples of all zeros to bring the total up to 256
+	push_analog(s, 240, -1.0f);
+
+	BOOST_CHECK_EQUAL(e0.length, 16);
+	BOOST_CHECK_EQUAL(e0.data_length, AnalogSnapshot::EnvelopeDataUnit);
+
+	for (unsigned int i = 1; i < e0.length; i++) {
+		BOOST_CHECK_EQUAL(e0.samples[i].min, -1.0f);
+		BOOST_CHECK_EQUAL(e0.samples[i].max, -1.0f);
+	}
+
+	const AnalogSnapshot::Envelope &e1 = s._envelope_levels[1];
+	BOOST_CHECK_EQUAL(e1.length, 1);
+	BOOST_CHECK_EQUAL(e1.data_length, AnalogSnapshot::EnvelopeDataUnit);
+	BOOST_REQUIRE(e1.samples != NULL);
+	BOOST_CHECK_EQUAL(e1.samples[0].min, -1.0f);
+	BOOST_CHECK_EQUAL(e1.samples[0].max, 1.0f);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/data/decoderstack.cpp b/test/data/decoderstack.cpp
new file mode 100644
index 0000000..d6e4f75
--- /dev/null
+++ b/test/data/decoderstack.cpp
@@ -0,0 +1,86 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <libsigrokdecode/libsigrokdecode.h> /* First, so we avoid a _POSIX_C_SOURCE warning. */
+#include <boost/test/unit_test.hpp>
+
+#include <libsigrok/libsigrok.h>
+
+#include "../../pv/data/decoderstack.h"
+#include "../../pv/devicemanager.h"
+#include "../../pv/sigsession.h"
+#include "../../pv/view/decodetrace.h"
+
+using boost::shared_ptr;
+using pv::data::DecoderStack;
+using pv::data::decode::Decoder;
+using pv::view::DecodeTrace;
+using std::vector;
+
+BOOST_AUTO_TEST_SUITE(DecoderStackTest)
+
+BOOST_AUTO_TEST_CASE(TwoDecoderStack)
+{
+	sr_context *ctx = NULL;
+
+	BOOST_REQUIRE(sr_init(&ctx) == SR_OK);
+	BOOST_REQUIRE(ctx);
+
+	BOOST_REQUIRE(srd_init(NULL) == SRD_OK);
+
+	srd_decoder_load_all();
+
+	{
+		pv::DeviceManager dm(ctx);
+		pv::SigSession ss(dm);
+
+		const GSList *l = srd_decoder_list();
+		BOOST_REQUIRE(l);
+		srd_decoder *const dec = (struct srd_decoder*)l->data;
+		BOOST_REQUIRE(dec);
+
+		ss.add_decoder(dec);
+		ss.add_decoder(dec);
+
+		// Check the signals were created
+		const vector< shared_ptr<DecodeTrace> > sigs =
+			ss.get_decode_signals();
+
+		shared_ptr<DecoderStack> dec0 = sigs[0]->decoder();
+		BOOST_REQUIRE(dec0);
+
+		shared_ptr<DecoderStack> dec1 = sigs[0]->decoder();
+		BOOST_REQUIRE(dec1);
+
+		// Wait for the decode threads to complete
+		dec0->_decode_thread.join();
+		dec1->_decode_thread.join();
+
+		// Check there were no errors
+		BOOST_CHECK_EQUAL(dec0->error_message().isEmpty(), true);
+		BOOST_CHECK_EQUAL(dec1->error_message().isEmpty(), true);
+	}
+
+
+	srd_exit();
+	sr_exit(ctx);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/data/logicsnapshot.cpp b/test/data/logicsnapshot.cpp
new file mode 100644
index 0000000..5fc1721
--- /dev/null
+++ b/test/data/logicsnapshot.cpp
@@ -0,0 +1,534 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <extdef.h>
+
+#include <stdint.h>
+
+#include <boost/test/unit_test.hpp>
+
+#include "../../pv/data/logicsnapshot.h"
+
+using pv::data::LogicSnapshot;
+using std::vector;
+
+BOOST_AUTO_TEST_SUITE(LogicSnapshotTest)
+
+void push_logic(LogicSnapshot &s, unsigned int length, uint8_t value)
+{
+	sr_datafeed_logic logic;
+	logic.unitsize = 1;
+	logic.length = length;
+	logic.data = new uint8_t[length];
+	memset(logic.data, value, length * logic.unitsize);
+	s.append_payload(logic);
+	delete[] (uint8_t*)logic.data;
+}
+
+BOOST_AUTO_TEST_CASE(Pow2)
+{
+	BOOST_CHECK_EQUAL(LogicSnapshot::pow2_ceil(0, 0), 0);
+	BOOST_CHECK_EQUAL(LogicSnapshot::pow2_ceil(1, 0), 1);
+	BOOST_CHECK_EQUAL(LogicSnapshot::pow2_ceil(2, 0), 2);
+
+	BOOST_CHECK_EQUAL(
+		LogicSnapshot::pow2_ceil(INT64_MIN, 0), INT64_MIN);
+	BOOST_CHECK_EQUAL(
+		LogicSnapshot::pow2_ceil(INT64_MAX, 0), INT64_MAX);
+
+	BOOST_CHECK_EQUAL(LogicSnapshot::pow2_ceil(0, 1), 0);
+	BOOST_CHECK_EQUAL(LogicSnapshot::pow2_ceil(1, 1), 2);
+	BOOST_CHECK_EQUAL(LogicSnapshot::pow2_ceil(2, 1), 2);
+	BOOST_CHECK_EQUAL(LogicSnapshot::pow2_ceil(3, 1), 4);
+}
+
+BOOST_AUTO_TEST_CASE(Basic)
+{
+	// Create an empty LogicSnapshot object
+	sr_datafeed_logic logic;
+	logic.length = 0;
+	logic.unitsize = 1;
+	logic.data = NULL;
+
+	LogicSnapshot s(logic);
+
+	//----- Test LogicSnapshot::push_logic -----//
+
+	BOOST_CHECK(s.get_sample_count() == 0);
+	for (unsigned int i = 0; i < LogicSnapshot::ScaleStepCount; i++)
+	{
+		const LogicSnapshot::MipMapLevel &m = s._mip_map[i];
+		BOOST_CHECK_EQUAL(m.length, 0);
+		BOOST_CHECK_EQUAL(m.data_length, 0);
+		BOOST_CHECK(m.data == NULL);
+	}
+
+	// Push 8 samples of all zeros
+	push_logic(s, 8, 0);
+
+	BOOST_CHECK(s.get_sample_count() == 8);
+
+	// There should not be enough samples to have a single mip map sample
+	for (unsigned int i = 0; i < LogicSnapshot::ScaleStepCount; i++)
+	{
+		const LogicSnapshot::MipMapLevel &m = s._mip_map[i];
+		BOOST_CHECK_EQUAL(m.length, 0);
+		BOOST_CHECK_EQUAL(m.data_length, 0);
+		BOOST_CHECK(m.data == NULL);
+	}
+
+	// Push 8 samples of 0x11s to bring the total up to 16
+	push_logic(s, 8, 0x11);
+
+	// There should now be enough data for exactly one sample
+	// in mip map level 0, and that sample should be 0
+	const LogicSnapshot::MipMapLevel &m0 = s._mip_map[0];
+	BOOST_CHECK_EQUAL(m0.length, 1);
+	BOOST_CHECK_EQUAL(m0.data_length, LogicSnapshot::MipMapDataUnit);
+	BOOST_REQUIRE(m0.data != NULL);
+	BOOST_CHECK_EQUAL(((uint8_t*)m0.data)[0], 0x11);
+
+	// The higher levels should still be empty
+	for (unsigned int i = 1; i < LogicSnapshot::ScaleStepCount; i++)
+	{
+		const LogicSnapshot::MipMapLevel &m = s._mip_map[i];
+		BOOST_CHECK_EQUAL(m.length, 0);
+		BOOST_CHECK_EQUAL(m.data_length, 0);
+		BOOST_CHECK(m.data == NULL);
+	}
+
+	// Push 240 samples of all zeros to bring the total up to 256
+	push_logic(s, 240, 0);
+
+	BOOST_CHECK_EQUAL(m0.length, 16);
+	BOOST_CHECK_EQUAL(m0.data_length, LogicSnapshot::MipMapDataUnit);
+
+	BOOST_CHECK_EQUAL(((uint8_t*)m0.data)[1], 0x11);
+	for (unsigned int i = 2; i < m0.length; i++)
+		BOOST_CHECK_EQUAL(((uint8_t*)m0.data)[i], 0);
+
+	const LogicSnapshot::MipMapLevel &m1 = s._mip_map[1];
+	BOOST_CHECK_EQUAL(m1.length, 1);
+	BOOST_CHECK_EQUAL(m1.data_length, LogicSnapshot::MipMapDataUnit);
+	BOOST_REQUIRE(m1.data != NULL);
+	BOOST_CHECK_EQUAL(((uint8_t*)m1.data)[0], 0x11);
+
+	//----- Test LogicSnapshot::get_subsampled_edges -----//
+
+	// Test a full view at full zoom.
+	vector<LogicSnapshot::EdgePair> edges;
+	s.get_subsampled_edges(edges, 0, 255, 1, 0);
+	BOOST_REQUIRE_EQUAL(edges.size(), 4);
+
+	BOOST_CHECK_EQUAL(edges[0].first, 0);
+	BOOST_CHECK_EQUAL(edges[1].first, 8);
+	BOOST_CHECK_EQUAL(edges[2].first, 16);
+	BOOST_CHECK_EQUAL(edges[3].first, 256);
+
+	// Test a subset at high zoom
+	edges.clear();
+	s.get_subsampled_edges(edges, 6, 17, 0.05f, 0);
+	BOOST_REQUIRE_EQUAL(edges.size(), 4);
+
+	BOOST_CHECK_EQUAL(edges[0].first, 6);
+	BOOST_CHECK_EQUAL(edges[1].first, 8);
+	BOOST_CHECK_EQUAL(edges[2].first, 16);
+	BOOST_CHECK_EQUAL(edges[3].first, 18);
+}
+
+BOOST_AUTO_TEST_CASE(LargeData)
+{
+	uint8_t prev_sample;
+	const unsigned int Length = 1000000;
+
+	sr_datafeed_logic logic;
+	logic.unitsize = 1;
+	logic.length = Length;
+	logic.data = new uint8_t[Length];
+	uint8_t *data = (uint8_t*)logic.data;
+
+	for (unsigned int i = 0; i < Length; i++)
+		*data++ = (uint8_t)(i >> 8);
+
+	LogicSnapshot s(logic);
+	delete[] (uint8_t*)logic.data;
+
+	BOOST_CHECK(s.get_sample_count() == Length);
+
+	// Check mip map level 0
+	BOOST_CHECK_EQUAL(s._mip_map[0].length, 62500);
+	BOOST_CHECK_EQUAL(s._mip_map[0].data_length,
+		LogicSnapshot::MipMapDataUnit);
+	BOOST_REQUIRE(s._mip_map[0].data != NULL);
+
+	prev_sample = 0;
+	for (unsigned int i = 0; i < s._mip_map[0].length;)
+	{
+		BOOST_TEST_MESSAGE("Testing mip_map[0].data[" << i << "]");
+
+		const uint8_t sample = (uint8_t)((i*16) >> 8);
+		BOOST_CHECK_EQUAL(s.get_subsample(0, i++) & 0xFF,
+			prev_sample ^ sample);
+		prev_sample = sample;
+
+		for (int j = 1; i < s._mip_map[0].length && j < 16; j++)
+		{
+			BOOST_TEST_MESSAGE("Testing mip_map[0].data[" << i << "]");
+			BOOST_CHECK_EQUAL(s.get_subsample(0, i++) & 0xFF, 0);
+		}
+	}
+
+	// Check mip map level 1
+	BOOST_CHECK_EQUAL(s._mip_map[1].length, 3906);
+	BOOST_CHECK_EQUAL(s._mip_map[1].data_length,
+		LogicSnapshot::MipMapDataUnit);
+	BOOST_REQUIRE(s._mip_map[1].data != NULL);
+
+	prev_sample = 0;
+	for (unsigned int i = 0; i < s._mip_map[1].length; i++)
+	{
+		BOOST_TEST_MESSAGE("Testing mip_map[1].data[" << i << "]");
+
+		const uint8_t sample = i;
+		const uint8_t expected = sample ^ prev_sample;
+		prev_sample = i;
+
+		BOOST_CHECK_EQUAL(s.get_subsample(1, i) & 0xFF, expected);
+	}
+
+	// Check mip map level 2
+	BOOST_CHECK_EQUAL(s._mip_map[2].length, 244);
+	BOOST_CHECK_EQUAL(s._mip_map[2].data_length,
+		LogicSnapshot::MipMapDataUnit);
+	BOOST_REQUIRE(s._mip_map[2].data != NULL);
+
+	prev_sample = 0;
+	for (unsigned int i = 0; i < s._mip_map[2].length; i++)
+	{
+		BOOST_TEST_MESSAGE("Testing mip_map[2].data[" << i << "]");
+
+		const uint8_t sample = i << 4;
+		const uint8_t expected = (sample ^ prev_sample) | 0x0F;
+		prev_sample = sample;
+
+		BOOST_CHECK_EQUAL(s.get_subsample(2, i) & 0xFF, expected);
+	}
+
+	// Check mip map level 3
+	BOOST_CHECK_EQUAL(s._mip_map[3].length, 15);
+	BOOST_CHECK_EQUAL(s._mip_map[3].data_length,
+		LogicSnapshot::MipMapDataUnit);
+	BOOST_REQUIRE(s._mip_map[3].data != NULL);
+
+	for (unsigned int i = 0; i < s._mip_map[3].length; i++)
+		BOOST_CHECK_EQUAL(*((uint8_t*)s._mip_map[3].data + i),
+			0xFF);
+
+	// Check the higher levels
+	for (unsigned int i = 4; i < LogicSnapshot::ScaleStepCount; i++)
+	{
+		const LogicSnapshot::MipMapLevel &m = s._mip_map[i];
+		BOOST_CHECK_EQUAL(m.length, 0);
+		BOOST_CHECK_EQUAL(m.data_length, 0);
+		BOOST_CHECK(m.data == NULL);
+	}
+
+	//----- Test LogicSnapshot::get_subsampled_edges -----//
+	// Check in normal case
+	vector<LogicSnapshot::EdgePair> edges;
+	s.get_subsampled_edges(edges, 0, Length-1, 1, 7);
+
+	BOOST_CHECK_EQUAL(edges.size(), 32);
+
+	for (unsigned int i = 0; i < edges.size() - 1; i++)
+	{
+		BOOST_CHECK_EQUAL(edges[i].first, i * 32768);
+		BOOST_CHECK_EQUAL(edges[i].second, i & 1);
+	}
+
+	BOOST_CHECK_EQUAL(edges[31].first, 1000000);
+
+	// Check in very low zoom case
+	edges.clear();
+	s.get_subsampled_edges(edges, 0, Length-1, 50e6f, 7);
+
+	BOOST_CHECK_EQUAL(edges.size(), 2);
+}
+
+BOOST_AUTO_TEST_CASE(Pulses)
+{
+	const int Cycles = 3;
+	const int Period = 64;
+	const int Length = Cycles * Period;
+
+	vector<LogicSnapshot::EdgePair> edges;
+
+	//----- Create a LogicSnapshot -----//
+	sr_datafeed_logic logic;
+	logic.unitsize = 1;
+	logic.length = Length;
+	logic.data = (uint64_t*)new uint8_t[Length];
+	uint8_t *p = (uint8_t*)logic.data;
+
+	for (int i = 0; i < Cycles; i++) {
+		*p++ = 0xFF;
+		for (int j = 1; j < Period; j++)
+			*p++ = 0x00;
+	}
+
+	LogicSnapshot s(logic);
+	delete[] (uint8_t*)logic.data;
+
+	//----- Check the mip-map -----//
+	// Check mip map level 0
+	BOOST_CHECK_EQUAL(s._mip_map[0].length, 12);
+	BOOST_CHECK_EQUAL(s._mip_map[0].data_length,
+		LogicSnapshot::MipMapDataUnit);
+	BOOST_REQUIRE(s._mip_map[0].data != NULL);
+
+	for (unsigned int i = 0; i < s._mip_map[0].length;) {
+		BOOST_TEST_MESSAGE("Testing mip_map[0].data[" << i << "]");
+		BOOST_CHECK_EQUAL(s.get_subsample(0, i++) & 0xFF, 0xFF);
+
+		for (int j = 1;
+			i < s._mip_map[0].length &&
+			j < Period/LogicSnapshot::MipMapScaleFactor; j++) {
+			BOOST_TEST_MESSAGE(
+				"Testing mip_map[0].data[" << i << "]");
+			BOOST_CHECK_EQUAL(s.get_subsample(0, i++) & 0xFF, 0x00);
+		}
+	}
+
+	// Check the higher levels are all inactive
+	for (unsigned int i = 1; i < LogicSnapshot::ScaleStepCount; i++) {
+		const LogicSnapshot::MipMapLevel &m = s._mip_map[i];
+		BOOST_CHECK_EQUAL(m.length, 0);
+		BOOST_CHECK_EQUAL(m.data_length, 0);
+		BOOST_CHECK(m.data == NULL);
+	}
+
+	//----- Test get_subsampled_edges at reduced scale -----//
+	s.get_subsampled_edges(edges, 0, Length-1, 16.0f, 2);
+	BOOST_REQUIRE_EQUAL(edges.size(), Cycles + 2);
+
+	BOOST_CHECK_EQUAL(0, false);
+	for (unsigned int i = 1; i < edges.size(); i++)
+		BOOST_CHECK_EQUAL(edges[i].second, false);
+}
+
+BOOST_AUTO_TEST_CASE(LongPulses)
+{
+	const int Cycles = 3;
+	const int Period = 64;
+	const int PulseWidth = 16;
+	const int Length = Cycles * Period;
+
+	int j;
+	vector<LogicSnapshot::EdgePair> edges;
+
+	//----- Create a LogicSnapshot -----//
+	sr_datafeed_logic logic;
+	logic.unitsize = 8;
+	logic.length = Length * 8;
+	logic.data = (uint64_t*)new uint64_t[Length];
+	uint64_t *p = (uint64_t*)logic.data;
+
+	for (int i = 0; i < Cycles; i++) {
+		for (j = 0; j < PulseWidth; j++)
+			*p++ = ~0;
+		for (; j < Period; j++)
+			*p++ = 0;
+	}
+
+	LogicSnapshot s(logic);
+	delete[] (uint64_t*)logic.data;
+
+	//----- Check the mip-map -----//
+	// Check mip map level 0
+	BOOST_CHECK_EQUAL(s._mip_map[0].length, 12);
+	BOOST_CHECK_EQUAL(s._mip_map[0].data_length,
+		LogicSnapshot::MipMapDataUnit);
+	BOOST_REQUIRE(s._mip_map[0].data != NULL);
+
+	for (unsigned int i = 0; i < s._mip_map[0].length;) {
+		for (j = 0; i < s._mip_map[0].length && j < 2; j++) {
+			BOOST_TEST_MESSAGE(
+				"Testing mip_map[0].data[" << i << "]");
+			BOOST_CHECK_EQUAL(s.get_subsample(0, i++), ~0);
+		}
+
+		for (; i < s._mip_map[0].length &&
+			j < Period/LogicSnapshot::MipMapScaleFactor; j++) {
+			BOOST_TEST_MESSAGE(
+				"Testing mip_map[0].data[" << i << "]");
+			BOOST_CHECK_EQUAL(s.get_subsample(0, i++), 0);
+		}
+	}
+
+	// Check the higher levels are all inactive
+	for (unsigned int i = 1; i < LogicSnapshot::ScaleStepCount; i++) {
+		const LogicSnapshot::MipMapLevel &m = s._mip_map[i];
+		BOOST_CHECK_EQUAL(m.length, 0);
+		BOOST_CHECK_EQUAL(m.data_length, 0);
+		BOOST_CHECK(m.data == NULL);
+	}
+
+	//----- Test get_subsampled_edges at a full scale -----//
+	s.get_subsampled_edges(edges, 0, Length-1, 16.0f, 2);
+	BOOST_REQUIRE_EQUAL(edges.size(), Cycles * 2 + 1);
+
+	for (int i = 0; i < Cycles; i++) {
+		BOOST_CHECK_EQUAL(edges[i*2].first, i * Period);
+		BOOST_CHECK_EQUAL(edges[i*2].second, true);
+		BOOST_CHECK_EQUAL(edges[i*2+1].first, i * Period + PulseWidth);
+		BOOST_CHECK_EQUAL(edges[i*2+1].second, false);
+	}
+
+	BOOST_CHECK_EQUAL(edges.back().first, Length);
+	BOOST_CHECK_EQUAL(edges.back().second, false);
+
+	//----- Test get_subsampled_edges at a simplified scale -----//
+	edges.clear();
+	s.get_subsampled_edges(edges, 0, Length-1, 17.0f, 2);
+
+	BOOST_CHECK_EQUAL(edges[0].first, 0);
+	BOOST_CHECK_EQUAL(edges[0].second, true);
+	BOOST_CHECK_EQUAL(edges[1].first, 16);
+	BOOST_CHECK_EQUAL(edges[1].second, false);
+	
+	for (int i = 1; i < Cycles; i++) {
+		BOOST_CHECK_EQUAL(edges[i+1].first, i * Period);
+		BOOST_CHECK_EQUAL(edges[i+1].second, false);
+	}
+
+	BOOST_CHECK_EQUAL(edges.back().first, Length);
+	BOOST_CHECK_EQUAL(edges.back().second, false);
+}
+
+BOOST_AUTO_TEST_CASE(LisaMUsbHid)
+{
+	/* This test was created from the beginning of the USB_DM signal in
+	 * sigrok-dumps-usb/lisa_m_usbhid/lisa_m_usbhid.sr
+	 */
+
+	const int Edges[] = {
+		7028, 7033, 7036, 7041, 7044, 7049, 7053, 7066, 7073, 7079,
+		7086, 7095, 7103, 7108, 7111, 7116, 7119, 7124, 7136, 7141,
+		7148, 7162, 7500
+	};
+	const int Length = Edges[countof(Edges) - 1];
+
+	bool state = false;
+	int lastEdgePos = 0;
+
+	//----- Create a LogicSnapshot -----//
+	sr_datafeed_logic logic;
+	logic.unitsize = 1;
+	logic.length = Length;
+	logic.data = new uint8_t[Length];
+	uint8_t *data = (uint8_t*)logic.data;
+
+	for (unsigned int i = 0; i < countof(Edges); i++) {
+		const int edgePos = Edges[i];
+		memset(&data[lastEdgePos], state ? 0x02 : 0,
+			edgePos - lastEdgePos - 1);
+
+		lastEdgePos = edgePos;
+		state = !state;
+	}
+
+	LogicSnapshot s(logic);
+	delete[] (uint64_t*)logic.data;
+
+	vector<LogicSnapshot::EdgePair> edges;
+
+
+	/* The trailing edge of the pulse train is falling in the source data.
+	 * Check this is always true at different scales
+	 */
+
+	edges.clear();
+	s.get_subsampled_edges(edges, 0, Length-1, 33.333332f, 1);
+	BOOST_CHECK_EQUAL(edges[edges.size() - 2].second, false);
+}
+
+/*
+ * This test checks the rendering of wide data (more than 8 probes)
+ * Probe signals are either all-high, or all-low, but are interleaved such that
+ * they would toggle during every sample if treated like 8 probes.
+ * The packet contains a large number of samples, so the mipmap generation kicks
+ * in.
+ *
+ * The signals should not toggle (have exactly two edges: the start and end)
+ */
+BOOST_AUTO_TEST_CASE(WideData)
+{
+	const int Length = 512<<10;
+	uint16_t *data = new uint16_t[Length];
+
+	sr_datafeed_logic logic;
+	logic.unitsize = sizeof(data[0]);
+	logic.length = Length * sizeof(data[0]);
+	logic.data = data;
+
+	for (int i = 0; i < Length; i++)
+		data[i] = 0x0FF0;
+
+	LogicSnapshot s(logic);
+
+	vector<LogicSnapshot::EdgePair> edges;
+
+	edges.clear();
+	s.get_subsampled_edges(edges, 0, Length-1, 1, 0);
+	BOOST_CHECK_EQUAL(edges.size(), 2);
+
+	edges.clear();
+	s.get_subsampled_edges(edges, 0, Length-1, 1, 8);
+	BOOST_CHECK_EQUAL(edges.size(), 2);
+
+	// Cleanup
+	delete [] data;
+}
+
+/*
+ * This test is a replica of sixteen.sr attached to Bug #33.
+ */
+BOOST_AUTO_TEST_CASE(Sixteen)
+{
+	const int Length = 8;
+	uint16_t data[Length];
+
+	sr_datafeed_logic logic;
+	logic.unitsize = sizeof(data[0]);
+	logic.length = Length * sizeof(data[0]);
+	logic.data = data;
+
+	for (int i = 0; i < Length; i++)
+		data[i] = 0xFFFE;
+
+	LogicSnapshot s(logic);
+
+	vector<LogicSnapshot::EdgePair> edges;
+	s.get_subsampled_edges(edges, 0, 2, 0.0004, 1);
+
+	BOOST_CHECK_EQUAL(edges.size(), 2);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/test.cpp b/test/test.cpp
new file mode 100644
index 0000000..04c9981
--- /dev/null
+++ b/test/test.cpp
@@ -0,0 +1,22 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#define BOOST_TEST_MAIN
+#include <boost/test/unit_test.hpp>

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-science/packages/pulseview.git



More information about the debian-science-commits mailing list