[pulseview] 03/06: New upstream version 0.3.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 2d67d35fbb14ee3551208775f1e978fcca53a4c8
Author: Zoltan Gyarmati <mr.zoltan.gyarmati at gmail.com>
Date:   Wed Mar 1 02:14:46 2017 +0100

    New upstream version 0.3.0
---
 CMake/GetGitRevisionDescription.cmake              |  130 +
 CMake/GetGitRevisionDescription.cmake.in           |   41 +
 CMake/cotire.cmake                                 | 3185 --------------------
 CMakeLists.txt                                     |  288 +-
 Doxyfile                                           |  234 ++
 HACKING                                            |    2 +-
 INSTALL                                            |   22 +-
 NEWS                                               |  157 +
 README                                             |   10 +-
 android/AndroidManifest.xml                        |   68 +
 android/assetreader.cpp                            |   82 +
 android/assetreader.hpp                            |   41 +
 android/bundled_libs.xml.in                        |   24 +
 android/custom_rules.xml                           |  123 +
 android/loghandler.cpp                             |  103 +
 android/loghandler.hpp                             |   39 +
 android/res/layout/splash.xml                      |   20 +
 android/res/values/libs.xml                        |   49 +
 android/res/values/strings-pv.xml                  |   22 +
 .../org/sigrok/pulseview/PulseViewActivity.java    |   51 +
 .../org/sigrok/pulseview/PulseViewApplication.java |   38 +
 config.h.in                                        |    1 +
 ...{pulseview_cross.nsi => pulseview_cross.nsi.in} |   51 +-
 doc/pulseview.1                                    |   63 +-
 extdef.h                                           |    2 +-
 icons/add-decoder.svg                              |    7 +
 icons/channels.svg                                 |   26 +
 icons/decoder-hidden.svg                           |   41 +-
 icons/decoder-shown.svg                            |   60 +-
 icons/menu.svg                                     |   21 +
 icons/probes.svg                                   |   72 -
 icons/show-cursors.svg                             |    7 +
 icons/sigrok-logo-notext.ico                       |  Bin 9662 -> 9662 bytes
 icons/sigrok-logo-notext.svg                       |  234 ++
 icons/status-green.svg                             |   20 +-
 icons/status-grey.svg                              |   20 +-
 icons/status-red.svg                               |   20 +-
 icons/trigger-change.svg                           |   87 +-
 icons/trigger-falling.svg                          |   46 +-
 icons/trigger-high.svg                             |   46 +-
 icons/trigger-low.svg                              |   46 +-
 ...rigger-rising.svg => trigger-marker-change.svg} |   12 +-
 ...trigger-none.svg => trigger-marker-falling.svg} |   15 +-
 .../{trigger-none.svg => trigger-marker-high.svg}  |   15 +-
 .../{trigger-rising.svg => trigger-marker-low.svg} |   11 +-
 ...rigger-rising.svg => trigger-marker-rising.svg} |    8 +-
 icons/trigger-none.svg                             |   43 +-
 icons/trigger-rising.svg                           |   46 +-
 main.cpp                                           |  105 +-
 pulseview.qrc                                      |   12 +-
 pv/{data/signaldata.cpp => application.cpp}        |   41 +-
 pv/{view/marginwidget.cpp => application.hpp}      |   23 +-
 pv/{prop => }/binding/binding.cpp                  |   35 +-
 pv/{prop/binding/binding.h => binding/binding.hpp} |   22 +-
 .../decoderoptions.cpp => binding/decoder.cpp}     |   99 +-
 .../decoderoptions.h => binding/decoder.hpp}       |   30 +-
 pv/binding/device.cpp                              |  196 ++
 pv/binding/device.hpp                              |   72 +
 pv/binding/inputoutput.cpp                         |  119 +
 pv/binding/inputoutput.hpp                         |   81 +
 pv/data/analog.cpp                                 |   29 +-
 pv/data/{analog.h => analog.hpp}                   |   26 +-
 pv/data/{analogsnapshot.cpp => analogsegment.cpp}  |   98 +-
 pv/data/{analogsnapshot.h => analogsegment.hpp}    |   22 +-
 pv/data/decode/annotation.cpp                      |   21 +-
 pv/data/decode/{annotation.h => annotation.hpp}    |   14 +-
 pv/data/decode/decoder.cpp                         |   71 +-
 pv/data/decode/{decoder.h => decoder.hpp}          |   31 +-
 pv/data/decode/row.cpp                             |   32 +-
 pv/data/decode/{row.h => row.hpp}                  |   14 +-
 pv/data/decode/rowdata.cpp                         |   11 +-
 pv/data/decode/{rowdata.h => rowdata.hpp}          |   10 +-
 pv/data/decoderstack.cpp                           |  272 +-
 pv/data/{decoderstack.h => decoderstack.hpp}       |   78 +-
 pv/data/logic.cpp                                  |   41 +-
 pv/data/{logic.h => logic.hpp}                     |   31 +-
 pv/data/{logicsnapshot.cpp => logicsegment.cpp}    |  180 +-
 pv/data/{logicsnapshot.h => logicsegment.hpp}      |   54 +-
 pv/data/segment.cpp                                |  110 +
 pv/data/{snapshot.h => segment.hpp}                |   47 +-
 pv/data/signaldata.cpp                             |   22 +-
 pv/data/{signaldata.h => signaldata.hpp}           |   24 +-
 pv/data/snapshot.cpp                               |   95 -
 pv/device/device.cpp                               |  104 -
 pv/device/devinst.cpp                              |  139 -
 pv/device/devinst.h                                |   95 -
 pv/device/file.cpp                                 |   66 -
 pv/device/inputfile.cpp                            |  143 -
 pv/device/inputfile.h                              |   68 -
 pv/device/sessionfile.cpp                          |   75 -
 pv/device/sessionfile.h                            |   47 -
 pv/devicemanager.cpp                               |  205 +-
 pv/devicemanager.h                                 |   75 -
 pv/devicemanager.hpp                               |   83 +
 pv/devices/device.cpp                              |   99 +
 pv/devices/device.hpp                              |   85 +
 pv/{data/signaldata.cpp => devices/file.cpp}       |   29 +-
 pv/{device/file.h => devices/file.hpp}             |   33 +-
 pv/devices/hardwaredevice.cpp                      |  133 +
 pv/devices/hardwaredevice.hpp                      |   68 +
 pv/devices/inputfile.cpp                           |  106 +
 pv/{view/viewport.h => devices/inputfile.hpp}      |   57 +-
 .../signaldata.cpp => devices/sessionfile.cpp}     |   36 +-
 pv/{dialogs/about.h => devices/sessionfile.hpp}    |   37 +-
 pv/dialogs/about.cpp                               |   40 +-
 pv/dialogs/{about.h => about.hpp}                  |   14 +-
 pv/dialogs/about.ui                                |    2 +-
 pv/dialogs/connect.cpp                             |  242 +-
 pv/dialogs/{connect.h => connect.hpp}              |   57 +-
 pv/dialogs/{about.h => inputoutputoptions.cpp}     |   54 +-
 pv/dialogs/inputoutputoptions.hpp                  |   71 +
 pv/dialogs/storeprogress.cpp                       |   45 +-
 pv/dialogs/{storeprogress.h => storeprogress.hpp}  |   23 +-
 pv/mainwindow.cpp                                  |  825 +++--
 pv/mainwindow.h                                    |  122 -
 pv/mainwindow.hpp                                  |  212 ++
 pv/popups/channels.cpp                             |  260 ++
 pv/popups/{probes.h => channels.hpp}               |   65 +-
 pv/popups/deviceoptions.cpp                        |   29 +-
 pv/popups/{deviceoptions.h => deviceoptions.hpp}   |   24 +-
 pv/popups/probes.cpp                               |  258 --
 pv/prop/binding/deviceoptions.cpp                  |  192 --
 pv/prop/binding/deviceoptions.h                    |   72 -
 pv/prop/bool.cpp                                   |   38 +-
 pv/prop/{bool.h => bool.hpp}                       |   12 +-
 pv/prop/double.cpp                                 |   55 +-
 pv/prop/{double.h => double.hpp}                   |   20 +-
 pv/prop/enum.cpp                                   |   57 +-
 pv/prop/{enum.h => enum.hpp}                       |   20 +-
 pv/prop/int.cpp                                    |  125 +-
 pv/prop/{int.h => int.hpp}                         |   18 +-
 pv/prop/property.cpp                               |   10 +-
 pv/prop/{property.h => property.hpp}               |   23 +-
 pv/prop/string.cpp                                 |   42 +-
 pv/prop/{string.h => string.hpp}                   |   12 +-
 pv/session.cpp                                     |  682 +++++
 pv/session.hpp                                     |  200 ++
 pv/sigsession.cpp                                  |  650 ----
 pv/sigsession.h                                    |  204 --
 pv/storesession.cpp                                |  224 +-
 pv/{storesession.h => storesession.hpp}            |   58 +-
 pv/toolbars/mainbar.cpp                            |  565 ++++
 pv/toolbars/{samplingbar.h => mainbar.hpp}         |   89 +-
 pv/toolbars/samplingbar.cpp                        |  450 ---
 pv/util.cpp                                        |  239 ++
 pv/util.hpp                                        |  130 +
 pv/view/analogsignal.cpp                           |  158 +-
 pv/view/{analogsignal.h => analogsignal.hpp}       |   74 +-
 pv/view/cursor.cpp                                 |  142 +-
 pv/view/{cursor.h => cursor.hpp}                   |   43 +-
 pv/view/cursorpair.cpp                             |  143 +-
 pv/view/cursorpair.hpp                             |  111 +
 pv/view/decodetrace.cpp                            |  626 ++--
 pv/view/decodetrace.h                              |  191 --
 pv/view/decodetrace.hpp                            |  217 ++
 pv/view/flag.cpp                                   |  112 +
 pv/view/{cursorpair.h => flag.hpp}                 |   60 +-
 pv/view/header.cpp                                 |  342 +--
 pv/view/{header.h => header.hpp}                   |   70 +-
 pv/view/logicsignal.cpp                            |  465 ++-
 pv/view/logicsignal.h                              |  119 -
 pv/view/logicsignal.hpp                            |  155 +
 pv/view/marginwidget.cpp                           |   51 +-
 pv/view/{selectableitem.h => marginwidget.hpp}     |   61 +-
 pv/view/{marginwidget.cpp => rowitem.cpp}          |   10 +-
 pv/view/{marginwidget.h => rowitem.hpp}            |   25 +-
 pv/view/ruler.cpp                                  |  354 +--
 pv/view/ruler.h                                    |   84 -
 pv/view/ruler.hpp                                  |  180 ++
 pv/view/selectableitem.cpp                         |   66 -
 pv/view/signal.cpp                                 |   94 +-
 pv/view/{signal.h => signal.hpp}                   |   67 +-
 pv/view/signalscalehandle.cpp                      |  107 +
 pv/view/signalscalehandle.hpp                      |   93 +
 pv/view/{marginwidget.cpp => timeitem.cpp}         |   17 +-
 pv/view/{marginwidget.h => timeitem.hpp}           |   39 +-
 pv/view/timemarker.cpp                             |  163 +-
 pv/view/timemarker.h                               |  113 -
 pv/view/timemarker.hpp                             |  133 +
 pv/view/trace.cpp                                  |  213 +-
 pv/view/trace.h                                    |  203 --
 pv/view/trace.hpp                                  |  145 +
 pv/view/tracegroup.cpp                             |  226 ++
 pv/view/tracegroup.hpp                             |  133 +
 pv/view/tracepalette.cpp                           |    2 +-
 pv/view/{tracepalette.h => tracepalette.hpp}       |    6 +-
 pv/view/tracetreeitem.cpp                          |  147 +
 pv/view/tracetreeitem.hpp                          |  142 +
 pv/view/tracetreeitemowner.cpp                     |  130 +
 pv/view/tracetreeitemowner.hpp                     |  117 +
 pv/view/triggermarker.cpp                          |   82 +
 pv/view/triggermarker.hpp                          |   86 +
 pv/view/view.cpp                                   | 1102 +++++--
 pv/view/view.h                                     |  214 --
 pv/view/view.hpp                                   |  404 +++
 pv/view/viewitem.cpp                               |  139 +
 pv/view/viewitem.hpp                               |  178 ++
 pv/view/viewitemiterator.hpp                       |  129 +
 .../decode/rowdata.cpp => view/viewitemowner.cpp}  |   44 +-
 pv/view/viewitemowner.hpp                          |   96 +
 .../device.h => view/viewitempaintparams.cpp}      |   46 +-
 pv/view/viewitempaintparams.hpp                    |   92 +
 pv/view/viewport.cpp                               |  236 +-
 pv/view/viewport.hpp                               |  110 +
 pv/view/viewwidget.cpp                             |  301 ++
 pv/view/viewwidget.hpp                             |  152 +
 pv/widgets/colourbutton.cpp                        |   40 +-
 pv/widgets/{colourbutton.h => colourbutton.hpp}    |   16 +-
 pv/widgets/colourpopup.cpp                         |   18 +-
 pv/widgets/{colourpopup.h => colourpopup.hpp}      |   20 +-
 pv/widgets/decodergroupbox.cpp                     |   46 +-
 .../{decodergroupbox.h => decodergroupbox.hpp}     |   15 +-
 pv/widgets/decodermenu.cpp                         |   19 +-
 pv/widgets/{decodermenu.h => decodermenu.hpp}      |   12 +-
 pv/widgets/devicetoolbutton.cpp                    |  153 +
 pv/widgets/devicetoolbutton.hpp                    |  105 +
 pv/widgets/exportmenu.cpp                          |   99 +
 pv/widgets/{decodermenu.h => exportmenu.hpp}       |   33 +-
 .../signaldata.cpp => widgets/hidingmenubar.cpp}   |   35 +-
 pv/widgets/{colourpopup.h => hidingmenubar.hpp}    |   49 +-
 pv/widgets/importmenu.cpp                          |   89 +
 pv/widgets/{decodermenu.h => importmenu.hpp}       |   33 +-
 pv/widgets/popup.cpp                               |  101 +-
 pv/widgets/{popup.h => popup.hpp}                  |   16 +-
 pv/widgets/popuptoolbutton.cpp                     |   14 +-
 .../{popuptoolbutton.h => popuptoolbutton.hpp}     |   12 +-
 pv/widgets/sweeptimingwidget.cpp                   |   81 +-
 .../{sweeptimingwidget.h => sweeptimingwidget.hpp} |   20 +-
 pv/widgets/timestampspinbox.cpp                    |  121 +
 pv/widgets/timestampspinbox.hpp                    |   93 +
 pv/widgets/wellarray.cpp                           |   50 +-
 pv/widgets/{wellarray.h => wellarray.hpp}          |   18 +-
 signalhandler.cpp                                  |   32 +-
 signalhandler.h => signalhandler.hpp               |   16 +-
 test/CMakeLists.txt                                |  180 +-
 .../data/{analogsnapshot.cpp => analogsegment.cpp} |   48 +-
 test/data/decoderstack.cpp                         |   24 +-
 test/data/{logicsnapshot.cpp => logicsegment.cpp}  |  208 +-
 test/test.cpp                                      |    6 +
 test/{test.cpp => test.hpp}                        |   12 +-
 test/util.cpp                                      |  240 ++
 test/view/ruler.cpp                                |  172 ++
 242 files changed, 16354 insertions(+), 11682 deletions(-)

diff --git a/CMake/GetGitRevisionDescription.cmake b/CMake/GetGitRevisionDescription.cmake
new file mode 100644
index 0000000..c8d27f2
--- /dev/null
+++ b/CMake/GetGitRevisionDescription.cmake
@@ -0,0 +1,130 @@
+# - Returns a version string from Git
+#
+# These functions force a re-configure on each git commit so that you can
+# trust the values of the variables in your build system.
+#
+#  get_git_head_revision(<refspecvar> <hashvar> [<additional arguments to git describe> ...])
+#
+# Returns the refspec and sha hash of the current head revision
+#
+#  git_describe(<var> [<additional arguments to git describe> ...])
+#
+# Returns the results of git describe on the source tree, and adjusting
+# the output so that it tests false if an error occurs.
+#
+#  git_get_exact_tag(<var> [<additional arguments to git describe> ...])
+#
+# Returns the results of git describe --exact-match on the source tree,
+# and adjusting the output so that it tests false if there was no exact
+# matching tag.
+#
+# Requires CMake 2.6 or newer (uses the 'function' command)
+#
+# Original Author:
+# 2009-2010 Ryan Pavlik <rpavlik at iastate.edu> <abiryan at ryand.net>
+# http://academic.cleardefinition.com
+# Iowa State University HCI Graduate Program/VRAC
+#
+# Copyright Iowa State University 2009-2010.
+# Distributed under the Boost Software License, Version 1.0.
+# (See accompanying file LICENSE_1_0.txt or copy at
+# http://www.boost.org/LICENSE_1_0.txt)
+
+if(__get_git_revision_description)
+	return()
+endif()
+set(__get_git_revision_description YES)
+
+# We must run the following at "include" time, not at function call time,
+# to find the path to this module rather than the path to a calling list file
+get_filename_component(_gitdescmoddir ${CMAKE_CURRENT_LIST_FILE} PATH)
+
+function(get_git_head_revision _refspecvar _hashvar)
+	set(GIT_PARENT_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
+	set(GIT_DIR "${GIT_PARENT_DIR}/.git")
+	while(NOT EXISTS "${GIT_DIR}")	# .git dir not found, search parent directories
+		set(GIT_PREVIOUS_PARENT "${GIT_PARENT_DIR}")
+		get_filename_component(GIT_PARENT_DIR ${GIT_PARENT_DIR} PATH)
+		if(GIT_PARENT_DIR STREQUAL GIT_PREVIOUS_PARENT)
+			# We have reached the root directory, we are not in git
+			set(${_refspecvar} "GITDIR-NOTFOUND" PARENT_SCOPE)
+			set(${_hashvar} "GITDIR-NOTFOUND" PARENT_SCOPE)
+			return()
+		endif()
+		set(GIT_DIR "${GIT_PARENT_DIR}/.git")
+	endwhile()
+	# check if this is a submodule
+	if(NOT IS_DIRECTORY ${GIT_DIR})
+		file(READ ${GIT_DIR} submodule)
+		string(REGEX REPLACE "gitdir: (.*)\n$" "\\1" GIT_DIR_RELATIVE ${submodule})
+		get_filename_component(SUBMODULE_DIR ${GIT_DIR} PATH)
+		get_filename_component(GIT_DIR ${SUBMODULE_DIR}/${GIT_DIR_RELATIVE} ABSOLUTE)
+	endif()
+	set(GIT_DATA "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/git-data")
+	if(NOT EXISTS "${GIT_DATA}")
+		file(MAKE_DIRECTORY "${GIT_DATA}")
+	endif()
+
+	if(NOT EXISTS "${GIT_DIR}/HEAD")
+		return()
+	endif()
+	set(HEAD_FILE "${GIT_DATA}/HEAD")
+	configure_file("${GIT_DIR}/HEAD" "${HEAD_FILE}" COPYONLY)
+
+	configure_file("${_gitdescmoddir}/GetGitRevisionDescription.cmake.in"
+		"${GIT_DATA}/grabRef.cmake"
+		@ONLY)
+	include("${GIT_DATA}/grabRef.cmake")
+
+	set(${_refspecvar} "${HEAD_REF}" PARENT_SCOPE)
+	set(${_hashvar} "${HEAD_HASH}" PARENT_SCOPE)
+endfunction()
+
+function(git_describe _var)
+	if(NOT GIT_FOUND)
+		find_package(Git QUIET)
+	endif()
+	get_git_head_revision(refspec hash)
+	if(NOT GIT_FOUND)
+		set(${_var} "GIT-NOTFOUND" PARENT_SCOPE)
+		return()
+	endif()
+	if(NOT hash)
+		set(${_var} "HEAD-HASH-NOTFOUND" PARENT_SCOPE)
+		return()
+	endif()
+
+	# TODO sanitize
+	#if((${ARGN}" MATCHES "&&") OR
+	#	(ARGN MATCHES "||") OR
+	#	(ARGN MATCHES "\\;"))
+	#	message("Please report the following error to the project!")
+	#	message(FATAL_ERROR "Looks like someone's doing something nefarious with git_describe! Passed arguments ${ARGN}")
+	#endif()
+
+	#message(STATUS "Arguments to execute_process: ${ARGN}")
+
+	execute_process(COMMAND
+		"${GIT_EXECUTABLE}"
+		describe
+		${hash}
+		${ARGN}
+		WORKING_DIRECTORY
+		"${CMAKE_SOURCE_DIR}"
+		RESULT_VARIABLE
+		res
+		OUTPUT_VARIABLE
+		out
+		ERROR_QUIET
+		OUTPUT_STRIP_TRAILING_WHITESPACE)
+	if(NOT res EQUAL 0)
+		set(out "${out}-${res}-NOTFOUND")
+	endif()
+
+	set(${_var} "${out}" PARENT_SCOPE)
+endfunction()
+
+function(git_get_exact_tag _var)
+	git_describe(out --exact-match ${ARGN})
+	set(${_var} "${out}" PARENT_SCOPE)
+endfunction()
diff --git a/CMake/GetGitRevisionDescription.cmake.in b/CMake/GetGitRevisionDescription.cmake.in
new file mode 100644
index 0000000..6d8b708
--- /dev/null
+++ b/CMake/GetGitRevisionDescription.cmake.in
@@ -0,0 +1,41 @@
+#
+# Internal file for GetGitRevisionDescription.cmake
+#
+# Requires CMake 2.6 or newer (uses the 'function' command)
+#
+# Original Author:
+# 2009-2010 Ryan Pavlik <rpavlik at iastate.edu> <abiryan at ryand.net>
+# http://academic.cleardefinition.com
+# Iowa State University HCI Graduate Program/VRAC
+#
+# Copyright Iowa State University 2009-2010.
+# Distributed under the Boost Software License, Version 1.0.
+# (See accompanying file LICENSE_1_0.txt or copy at
+# http://www.boost.org/LICENSE_1_0.txt)
+
+set(HEAD_HASH)
+
+file(READ "@HEAD_FILE@" HEAD_CONTENTS LIMIT 1024)
+
+string(STRIP "${HEAD_CONTENTS}" HEAD_CONTENTS)
+if(HEAD_CONTENTS MATCHES "ref")
+	# named branch
+	string(REPLACE "ref: " "" HEAD_REF "${HEAD_CONTENTS}")
+	if(EXISTS "@GIT_DIR@/${HEAD_REF}")
+		configure_file("@GIT_DIR@/${HEAD_REF}" "@GIT_DATA@/head-ref" COPYONLY)
+	else()
+		configure_file("@GIT_DIR@/packed-refs" "@GIT_DATA@/packed-refs" COPYONLY)
+		file(READ "@GIT_DATA@/packed-refs" PACKED_REFS)
+		if(${PACKED_REFS} MATCHES "([0-9a-z]*) ${HEAD_REF}")
+			set(HEAD_HASH "${CMAKE_MATCH_1}")
+		endif()
+	endif()
+else()
+	# detached HEAD
+	configure_file("@GIT_DIR@/HEAD" "@GIT_DATA@/head-ref" COPYONLY)
+endif()
+
+if(NOT HEAD_HASH)
+	file(READ "@GIT_DATA@/head-ref" HEAD_HASH LIMIT 1024)
+	string(STRIP "${HEAD_HASH}" HEAD_HASH)
+endif()
diff --git a/CMake/cotire.cmake b/CMake/cotire.cmake
deleted file mode 100644
index a6e3141..0000000
--- a/CMake/cotire.cmake
+++ /dev/null
@@ -1,3185 +0,0 @@
-# - 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/CMakeLists.txt b/CMakeLists.txt
index 50dde49..e21ace6 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -20,13 +20,12 @@
 
 cmake_minimum_required(VERSION 2.8.6)
 
-include(FindPkgConfig)
 include(GNUInstallDirs)
 
-set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/CMake")
-
 project(pulseview)
 
+list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/CMake")
+
 #===============================================================================
 #= User Options
 #-------------------------------------------------------------------------------
@@ -34,9 +33,9 @@ project(pulseview)
 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(ENABLE_TESTS "Enable unit tests" TRUE)
 option(STATIC_PKGDEPS_LIBS "Statically link to (pkg-config) libraries" FALSE)
+option(FORCE_QT4 "Force use of Qt4 even if Qt5 is available" FALSE)
 
 if(WIN32)
 	# On Windows/MinGW we need to statically link to libraries.
@@ -47,44 +46,67 @@ if(WIN32)
 	set(Boost_USE_STATIC_LIBS ON)
 	add_definitions(-DBOOST_THREAD_USE_LIB)
 
+	# On Windows/MinGW we need to use 'thread_win32' instead of 'thread'.
+	# The library is named libboost_thread_win32* (not libboost_thread*).
+	set(Boost_THREADAPI win32)
+
 	# 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)
+	set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING
+	"Choose the type of build (None, Debug, Release, RelWithDebInfo, MinSizeRel)."
+	FORCE)
 endif()
 
 #===============================================================================
 #= Dependencies
 #-------------------------------------------------------------------------------
 
-list(APPEND PKGDEPS libsigrok>=0.3.0)
+list(APPEND PKGDEPS libsigrokcxx>=0.4.0)
 
 if(ENABLE_DECODE)
-	list(APPEND PKGDEPS libsigrokdecode>=0.3.0)
+	list(APPEND PKGDEPS libsigrokdecode>=0.4.0)
+endif()
+
+if(ANDROID)
+	list(APPEND PKGDEPS libsigrokandroidutils>=0.1.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)
+if(FORCE_QT4)
+	set(Qt5Core_FOUND FALSE)
+else()
+	find_package(Qt5Core QUIET)
+endif()
 
-# 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(Qt5Core_FOUND)
+	message("-- Using Qt5")
+	find_package(Qt5Widgets REQUIRED)
+	find_package(Qt5Gui REQUIRED)
+	find_package(Qt5Svg REQUIRED)
+	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${Qt5Widgets_EXECUTABLE_COMPILE_FLAGS}")
+	set(QT_INCLUDE_DIRS ${Qt5Gui_INCLUDE_DIRS} ${Qt5Widgets_INCLUDE_DIRS})
+	set(QT_LIBRARIES Qt5::Gui Qt5::Widgets Qt5::Svg)
+	add_definitions(${Qt5Gui_DEFINITIONS} ${Qt5Widgets_DEFINITIONS})
+else()
+	find_program(QT_QMAKE_EXECUTABLE NAMES qmake4 qmake-qt4 qmake-mac)
+	find_package(Qt4 REQUIRED QtCore QtGui QtSvg)
+endif()
 
-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)
+if(ENABLE_TESTS)
+	find_package(Boost 1.53 COMPONENTS filesystem system thread unit_test_framework REQUIRED)
 else()
-	find_package(Boost 1.42 COMPONENTS filesystem system thread REQUIRED)
+	find_package(Boost 1.53 COMPONENTS filesystem system thread REQUIRED)
 endif()
 
+# Find the platform's thread library (needed for C++11 threads).
+# This will set ${CMAKE_THREAD_LIBS_INIT} to the correct, OS-specific value.
+find_package(Threads REQUIRED)
+
 #===============================================================================
 #= System Introspection
 #-------------------------------------------------------------------------------
@@ -98,13 +120,28 @@ memaccess_check_unaligned_le(HAVE_UNALIGNED_LITTLE_ENDIAN_ACCESS)
 
 set(PV_TITLE PulseView)
 set(PV_DESCRIPTION "A GUI for sigrok")
+set(PV_VERSION_STRING "0.3.0")
+
+include(GetGitRevisionDescription)
+
+# Append the revision hash unless we are exactly on a tagged release.
+git_describe(PV_TAG_VERSION_STRING --match "pulseview-${PV_VERSION_STRING}" --exact-match)
+if(NOT PV_TAG_VERSION_STRING)
+	get_git_head_revision(PV_REVSPEC PV_HASH)
+	if(PV_HASH)
+		string(SUBSTRING "${PV_HASH}" 0 7 PV_SHORTHASH)
+		set(PV_VERSION_STRING "${PV_VERSION_STRING}-git-${PV_SHORTHASH}")
+	endif()
+endif()
 
-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}
-)
+if(PV_VERSION_STRING MATCHES "^([0-9]+)\\.([0-9]+)\\.([0-9]+)(-[-0-9a-z]*)?$")
+	set(PV_VERSION_MAJOR ${CMAKE_MATCH_1})
+	set(PV_VERSION_MINOR ${CMAKE_MATCH_2})
+	set(PV_VERSION_MICRO ${CMAKE_MATCH_3})
+	set(PV_VERSION_SUFFIX ${CMAKE_MATCH_4})
+endif()
+
+message("-- ${PV_TITLE} version: ${PV_VERSION_STRING}")
 
 configure_file (
 	${PROJECT_SOURCE_DIR}/config.h.in
@@ -117,92 +154,126 @@ configure_file (
 
 set(pulseview_SOURCES
 	main.cpp
+	pv/application.cpp
 	pv/devicemanager.cpp
 	pv/mainwindow.cpp
-	pv/sigsession.cpp
+	pv/session.cpp
 	pv/storesession.cpp
+	pv/util.cpp
+	pv/binding/binding.cpp
+	pv/binding/inputoutput.cpp
+	pv/binding/device.cpp
 	pv/data/analog.cpp
-	pv/data/analogsnapshot.cpp
+	pv/data/analogsegment.cpp
 	pv/data/logic.cpp
-	pv/data/logicsnapshot.cpp
+	pv/data/logicsegment.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/data/segment.cpp
+	pv/devices/device.cpp
+	pv/devices/file.cpp
+	pv/devices/hardwaredevice.cpp
+	pv/devices/inputfile.cpp
+	pv/devices/sessionfile.cpp
 	pv/dialogs/about.cpp
 	pv/dialogs/connect.cpp
+	pv/dialogs/inputoutputoptions.cpp
 	pv/dialogs/storeprogress.cpp
 	pv/popups/deviceoptions.cpp
-	pv/popups/probes.cpp
+	pv/popups/channels.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/toolbars/mainbar.cpp
 	pv/view/analogsignal.cpp
 	pv/view/cursor.cpp
 	pv/view/cursorpair.cpp
+	pv/view/flag.cpp
 	pv/view/header.cpp
 	pv/view/marginwidget.cpp
 	pv/view/logicsignal.cpp
+	pv/view/rowitem.cpp
 	pv/view/ruler.cpp
-	pv/view/selectableitem.cpp
 	pv/view/signal.cpp
+	pv/view/signalscalehandle.cpp
+	pv/view/timeitem.cpp
 	pv/view/timemarker.cpp
 	pv/view/trace.cpp
+	pv/view/tracegroup.cpp
 	pv/view/tracepalette.cpp
+	pv/view/tracetreeitem.cpp
+	pv/view/tracetreeitemowner.cpp
+	pv/view/triggermarker.cpp
 	pv/view/view.cpp
+	pv/view/viewitem.cpp
+	pv/view/viewitemowner.cpp
+	pv/view/viewitempaintparams.cpp
 	pv/view/viewport.cpp
+	pv/view/viewwidget.cpp
 	pv/widgets/colourbutton.cpp
 	pv/widgets/colourpopup.cpp
+	pv/widgets/devicetoolbutton.cpp
+	pv/widgets/exportmenu.cpp
+	pv/widgets/hidingmenubar.cpp
+	pv/widgets/importmenu.cpp
 	pv/widgets/popup.cpp
 	pv/widgets/popuptoolbutton.cpp
 	pv/widgets/sweeptimingwidget.cpp
+	pv/widgets/timestampspinbox.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
+	pv/mainwindow.hpp
+	pv/session.hpp
+	pv/storesession.hpp
+	pv/binding/device.hpp
+	pv/dialogs/about.hpp
+	pv/dialogs/connect.hpp
+	pv/dialogs/inputoutputoptions.hpp
+	pv/dialogs/storeprogress.hpp
+	pv/popups/channels.hpp
+	pv/popups/deviceoptions.hpp
+	pv/prop/bool.hpp
+	pv/prop/double.hpp
+	pv/prop/enum.hpp
+	pv/prop/int.hpp
+	pv/prop/property.hpp
+	pv/prop/string.hpp
+	pv/toolbars/mainbar.hpp
+	pv/view/cursor.hpp
+	pv/view/flag.hpp
+	pv/view/header.hpp
+	pv/view/logicsignal.hpp
+	pv/view/marginwidget.hpp
+	pv/view/rowitem.hpp
+	pv/view/ruler.hpp
+	pv/view/signal.hpp
+	pv/view/signalscalehandle.hpp
+	pv/view/timeitem.hpp
+	pv/view/timemarker.hpp
+	pv/view/trace.hpp
+	pv/view/tracegroup.hpp
+	pv/view/tracetreeitem.hpp
+	pv/view/triggermarker.hpp
+	pv/view/view.hpp
+	pv/view/viewitem.hpp
+	pv/view/viewport.hpp
+	pv/view/viewwidget.hpp
+	pv/widgets/colourbutton.hpp
+	pv/widgets/colourpopup.hpp
+	pv/widgets/devicetoolbutton.hpp
+	pv/widgets/exportmenu.hpp
+	pv/widgets/hidingmenubar.hpp
+	pv/widgets/importmenu.hpp
+	pv/widgets/popup.hpp
+	pv/widgets/popuptoolbutton.hpp
+	pv/widgets/sweeptimingwidget.hpp
+	pv/widgets/timestampspinbox.hpp
+	pv/widgets/wellarray.hpp
 )
 
 set(pulseview_FORMS
@@ -215,27 +286,27 @@ set(pulseview_RESOURCES
 
 if(ENABLE_SIGNALS)
 	list(APPEND pulseview_SOURCES signalhandler.cpp)
-	list(APPEND pulseview_HEADERS signalhandler.h)
+	list(APPEND pulseview_HEADERS signalhandler.hpp)
 endif()
 
 if(ENABLE_DECODE)
 	list(APPEND pulseview_SOURCES
+		pv/binding/decoder.cpp
 		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
+		pv/data/decoderstack.hpp
+		pv/view/decodetrace.hpp
+		pv/widgets/decodergroupbox.hpp
+		pv/widgets/decodermenu.hpp
 	)
 endif()
 
@@ -246,19 +317,35 @@ if(WIN32)
 	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})
+if(ANDROID)
+	list(APPEND pulseview_SOURCES
+		android/assetreader.cpp
+		android/loghandler.cpp
+	)
+endif()
 
-include(${QT_USE_FILE})
+if(Qt5Core_FOUND)
+	qt5_wrap_cpp(pulseview_HEADERS_MOC ${pulseview_HEADERS})
+	qt5_wrap_ui(pulseview_FORMS_HEADERS ${pulseview_FORMS})
+	qt5_add_resources(pulseview_RESOURCES_RCC ${pulseview_RESOURCES})
+else()
+	# Workaround for QTBUG-22829: -DBOOST_NEXT_PRIOR_HPP_INCLUDED.
+	# https://bugreports.qt.io/browse/QTBUG-22829
+	qt4_wrap_cpp(pulseview_HEADERS_MOC ${pulseview_HEADERS}
+		OPTIONS -DBOOST_NEXT_PRIOR_HPP_INCLUDED)
+	qt4_wrap_ui(pulseview_FORMS_HEADERS ${pulseview_FORMS})
+	qt4_add_resources(pulseview_RESOURCES_RCC ${pulseview_RESOURCES})
+	include(${QT_USE_FILE})
+endif()
 
 #===============================================================================
 #= Global Definitions
 #-------------------------------------------------------------------------------
 
-add_definitions(${QT_DEFINITIONS})
+add_definitions(${QT_DEFINITIONS} -DQT_NO_KEYWORDS)
 add_definitions(-D__STDC_LIMIT_MACROS)
 add_definitions(-Wall -Wextra)
+add_definitions(-std=c++11)
 
 if(ENABLE_DECODE)
 	add_definitions(-DENABLE_DECODE)
@@ -268,6 +355,10 @@ if(NOT DISABLE_WERROR)
 	add_definitions(-Werror)
 endif()
 
+if(ENABLE_SIGNALS)
+	add_definitions(-DENABLE_SIGNALS)
+endif()
+
 #===============================================================================
 #= Global Include Directories
 #-------------------------------------------------------------------------------
@@ -276,6 +367,7 @@ include_directories(
 	${CMAKE_CURRENT_BINARY_DIR}
 	${CMAKE_CURRENT_SOURCE_DIR}
 	${Boost_INCLUDE_DIRS}
+	${QT_INCLUDE_DIRS}
 )
 
 if(STATIC_PKGDEPS_LIBS)
@@ -292,8 +384,8 @@ link_directories(${Boost_LIBRARY_DIRS})
 
 set(PULSEVIEW_LINK_LIBS
 	${Boost_LIBRARIES}
-	${CMAKE_THREAD_LIBS_INIT}
 	${QT_LIBRARIES}
+	${CMAKE_THREAD_LIBS_INIT}
 )
 
 if(STATIC_PKGDEPS_LIBS)
@@ -313,16 +405,29 @@ if(WIN32)
 	# 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")
+	list(APPEND PULSEVIEW_LINK_LIBS ${QT_QTSVG_LIBRARY})
 endif()
 
+if(ANDROID)
+	list(APPEND PULSEVIEW_LINK_LIBS "-llog")
+endif()
+
+if(ANDROID)
+add_library(${PROJECT_NAME} SHARED
+	${pulseview_SOURCES}
+	${pulseview_HEADERS_MOC}
+	${pulseview_FORMS_HEADERS}
+	${pulseview_RESOURCES_RCC}
+)
+else()
 add_executable(${PROJECT_NAME}
 	${pulseview_SOURCES}
 	${pulseview_HEADERS_MOC}
 	${pulseview_FORMS_HEADERS}
 	${pulseview_RESOURCES_RCC}
 )
+endif()
 
 target_link_libraries(${PROJECT_NAME} ${PULSEVIEW_LINK_LIBS})
 
@@ -331,11 +436,6 @@ if(WIN32)
 	set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "-mwindows")
 endif()
 
-if(ENABLE_COTIRE)
-	include(cotire)
-	cotire(${PROJECT_NAME})
-endif()
-
 #===============================================================================
 #= Installation
 #-------------------------------------------------------------------------------
@@ -346,6 +446,9 @@ install(TARGETS ${PROJECT_NAME} DESTINATION bin/)
 # Install the manpage.
 install(FILES doc/pulseview.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 COMPONENT doc)
 
+# Generate Windows installer script.
+configure_file(contrib/pulseview_cross.nsi.in contrib/pulseview_cross.nsi @ONLY)
+
 #===============================================================================
 #= Packaging (handled by CPack)
 #-------------------------------------------------------------------------------
@@ -356,8 +459,7 @@ 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_PACKAGE_FILE_NAME "${CMAKE_PROJECT_NAME}-${PV_VERSION_STRING}")
 set(CPACK_SOURCE_GENERATOR "TGZ")
 
 include(CPack)
diff --git a/Doxyfile b/Doxyfile
new file mode 100644
index 0000000..a7cb281
--- /dev/null
+++ b/Doxyfile
@@ -0,0 +1,234 @@
+# Doxyfile 1.8.6
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+DOXYFILE_ENCODING      = UTF-8
+PROJECT_NAME           = "PulseView"
+PROJECT_NUMBER         = "unreleased development snapshot"
+PROJECT_BRIEF          = "A Qt-based sigrok GUI"
+PROJECT_LOGO           = icons/sigrok-logo-notext.png
+OUTPUT_DIRECTORY       = doxy
+CREATE_SUBDIRS         = NO
+OUTPUT_LANGUAGE        = English
+BRIEF_MEMBER_DESC      = YES
+REPEAT_BRIEF           = YES
+ABBREVIATE_BRIEF       =
+ALWAYS_DETAILED_SEC    = NO
+INLINE_INHERITED_MEMB  = NO
+FULL_PATH_NAMES        = YES
+STRIP_FROM_PATH        =
+STRIP_FROM_INC_PATH    =
+SHORT_NAMES            = NO
+JAVADOC_AUTOBRIEF      = NO
+QT_AUTOBRIEF           = NO
+MULTILINE_CPP_IS_BRIEF = NO
+INHERIT_DOCS           = YES
+SEPARATE_MEMBER_PAGES  = NO
+TAB_SIZE               = 4
+ALIASES                =
+MARKDOWN_SUPPORT       = YES
+AUTOLINK_SUPPORT       = YES
+BUILTIN_STL_SUPPORT    = YES
+SUBGROUPING            = YES
+INLINE_GROUPED_CLASSES = NO
+INLINE_SIMPLE_STRUCTS  = YES
+TYPEDEF_HIDES_STRUCT   = NO
+LOOKUP_CACHE_SIZE      = 0
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+EXTRACT_ALL            = YES
+EXTRACT_PRIVATE        = YES
+EXTRACT_PACKAGE        = NO
+EXTRACT_STATIC         = YES
+EXTRACT_LOCAL_CLASSES  = YES
+HIDE_UNDOC_MEMBERS     = NO
+HIDE_UNDOC_CLASSES     = NO
+HIDE_FRIEND_COMPOUNDS  = NO
+HIDE_IN_BODY_DOCS      = NO
+INTERNAL_DOCS          = YES
+CASE_SENSE_NAMES       = YES
+HIDE_SCOPE_NAMES       = NO
+SHOW_INCLUDE_FILES     = YES
+SHOW_GROUPED_MEMB_INC  = NO
+FORCE_LOCAL_INCLUDES   = NO
+INLINE_INFO            = YES
+SORT_MEMBER_DOCS       = YES
+SORT_BRIEF_DOCS        = NO
+SORT_MEMBERS_CTORS_1ST = YES
+SORT_GROUP_NAMES       = NO
+SORT_BY_SCOPE_NAME     = NO
+STRICT_PROTO_MATCHING  = NO
+GENERATE_TODOLIST      = NO
+GENERATE_TESTLIST      = YES
+GENERATE_BUGLIST       = NO
+GENERATE_DEPRECATEDLIST= NO
+ENABLED_SECTIONS       =
+MAX_INITIALIZER_LINES  = 30
+SHOW_USED_FILES        = YES
+SHOW_FILES             = YES
+SHOW_NAMESPACES        = YES
+FILE_VERSION_FILTER    =
+LAYOUT_FILE            =
+
+#---------------------------------------------------------------------------
+# Configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+QUIET                  = YES
+WARNINGS               = YES
+WARN_IF_UNDOCUMENTED   = YES
+WARN_IF_DOC_ERROR      = YES
+WARN_NO_PARAMDOC       = NO
+WARN_FORMAT            = "$file:$line: $text"
+WARN_LOGFILE           =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the input files
+#---------------------------------------------------------------------------
+
+INPUT                  = .
+INPUT_ENCODING         = UTF-8
+FILE_PATTERNS          =
+RECURSIVE              = YES
+EXCLUDE                =
+EXCLUDE_SYMLINKS       = NO
+EXCLUDE_PATTERNS       = moc_*.cxx*
+EXCLUDE_SYMBOLS        =
+EXAMPLE_PATH           =
+EXAMPLE_PATTERNS       =
+EXAMPLE_RECURSIVE      = NO
+IMAGE_PATH             =
+INPUT_FILTER           =
+FILTER_PATTERNS        =
+FILTER_SOURCE_FILES    = NO
+FILTER_SOURCE_PATTERNS =
+USE_MDFILE_AS_MAINPAGE =
+
+#---------------------------------------------------------------------------
+# Configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+SOURCE_BROWSER         = YES
+INLINE_SOURCES         = NO
+STRIP_CODE_COMMENTS    = YES
+REFERENCED_BY_RELATION = NO
+REFERENCES_RELATION    = NO
+REFERENCES_LINK_SOURCE = YES
+SOURCE_TOOLTIPS        = YES
+USE_HTAGS              = NO
+VERBATIM_HEADERS       = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+ALPHABETICAL_INDEX     = YES
+COLS_IN_ALPHA_INDEX    = 5
+IGNORE_PREFIX          =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+GENERATE_HTML          = YES
+HTML_OUTPUT            = html-api
+HTML_FILE_EXTENSION    = .html
+HTML_HEADER            =
+HTML_FOOTER            =
+HTML_STYLESHEET        =
+HTML_EXTRA_STYLESHEET  =
+HTML_EXTRA_FILES       =
+HTML_COLORSTYLE_HUE    = 220
+HTML_COLORSTYLE_SAT    = 100
+HTML_COLORSTYLE_GAMMA  = 80
+HTML_TIMESTAMP         = YES
+HTML_DYNAMIC_SECTIONS  = NO
+HTML_INDEX_NUM_ENTRIES = 100
+DISABLE_INDEX          = NO
+GENERATE_TREEVIEW      = YES
+ENUM_VALUES_PER_LINE   = 4
+TREEVIEW_WIDTH         = 250
+EXT_LINKS_IN_WINDOW    = NO
+FORMULA_FONTSIZE       = 10
+FORMULA_TRANSPARENT    = YES
+SEARCHENGINE           = YES
+SERVER_BASED_SEARCH    = NO
+EXTERNAL_SEARCH        = NO
+SEARCHENGINE_URL       =
+SEARCHDATA_FILE        = searchdata.xml
+EXTERNAL_SEARCH_ID     =
+EXTRA_SEARCH_MAPPINGS  =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the Latex output
+#---------------------------------------------------------------------------
+
+GENERATE_LATEX         = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+
+ENABLE_PREPROCESSING   = YES
+MACRO_EXPANSION        = NO
+EXPAND_ONLY_PREDEF     = NO
+SEARCH_INCLUDES        = YES
+INCLUDE_PATH           =
+INCLUDE_FILE_PATTERNS  =
+PREDEFINED             =
+EXPAND_AS_DEFINED      =
+SKIP_FUNCTION_MACROS   = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to external references
+#---------------------------------------------------------------------------
+
+TAGFILES               =
+GENERATE_TAGFILE       =
+ALLEXTERNALS           = NO
+EXTERNAL_GROUPS        = YES
+EXTERNAL_PAGES         = YES
+PERL_PATH              = /usr/bin/perl
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+
+CLASS_DIAGRAMS         = YES
+MSCGEN_PATH            =
+DIA_PATH               =
+HIDE_UNDOC_RELATIONS   = NO
+HAVE_DOT               = YES
+DOT_NUM_THREADS        = 0
+DOT_FONTNAME           = Helvetica
+DOT_FONTSIZE           = 10
+DOT_FONTPATH           =
+CLASS_GRAPH            = YES
+COLLABORATION_GRAPH    = YES
+GROUP_GRAPHS           = YES
+UML_LOOK               = YES
+UML_LIMIT_NUM_FIELDS   = 10
+TEMPLATE_RELATIONS     = NO
+INCLUDE_GRAPH          = YES
+INCLUDED_BY_GRAPH      = YES
+CALL_GRAPH             = YES
+CALLER_GRAPH           = YES
+GRAPHICAL_HIERARCHY    = YES
+DIRECTORY_GRAPH        = YES
+DOT_IMAGE_FORMAT       = svg
+INTERACTIVE_SVG        = YES
+DOT_PATH               =
+DOTFILE_DIRS           =
+MSCFILE_DIRS           =
+DIAFILE_DIRS           =
+DOT_GRAPH_MAX_NODES    = 50
+MAX_DOT_GRAPH_DEPTH    = 0
+DOT_TRANSPARENT        = YES
+DOT_MULTI_TARGETS      = YES
+GENERATE_LEGEND        = YES
+DOT_CLEANUP            = YES
diff --git a/HACKING b/HACKING
index d1edc87..17183ed 100644
--- a/HACKING
+++ b/HACKING
@@ -38,7 +38,7 @@ Random notes
    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
+ - Never use g_malloc() or g_malloc0(). These functions do not return nullptr
    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.
diff --git a/INSTALL b/INSTALL
index 4a93e08..b532339 100644
--- a/INSTALL
+++ b/INSTALL
@@ -5,21 +5,27 @@ INSTALL
 Requirements
 ------------
 
- - git
- - g++
+ - git (only needed when building from git)
+ - A C++ compiler with C++11 support (-std=c++11 option), e.g.
+   - g++ (>= 4.7)
+   - clang++ (>= 3.1)
  - make
- - libtool
+ - libtool (only needed when building from git)
  - pkg-config >= 0.22
  - cmake >= 2.8.6
  - libglib >= 2.28.0
- - Qt >= 4.5
- - libboost >= 1.42 (including the following libs):
+ - glibmm-2.4 (>= 2.28.0)
+ - Qt4 >= 4.5 or Qt5 (including the following components):
+    - Qt4: QtCore, QtGui, QtSvg
+    - Qt5: Qt5Core, Qt5Gui, Qt5Widgets, Qt5Svg
+ - libboost >= 1.53 (including the following libs):
     - libboost-system
-    - libboost-thread
     - libboost-filesystem
+    - libboost-thread
     - libboost-test (optional, only needed to run the unit tests)
- - libsigrok >= 0.3.0
- - libsigrokdecode >= 0.3.0
+ - libsigrokcxx >= 0.4.0 (libsigrok C++ bindings)
+ - libsigrokdecode >= 0.4.0
+ - libsigrokandroidutils >= 0.1.0 (optional, only needed on Android)
 
 
 Building and installing
diff --git a/NEWS b/NEWS
index 696f13a..5ffbfe8 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,160 @@
+0.3.0 (2016-01-29)
+------------------
+
+ * PulseView now uses libsigrokcxx (the libsigrok C++ bindings library).
+ * Update to use the new APIs of libsigrokcxx 0.4.x and libsigrokdecode 0.4.x.
+ * Add support for vertical scaling of analog and logic traces.
+ * Add a "Save selection range as..." feature. This allows the user to select
+   a subset of the data (using the cursors) and save that into a file in the
+   libsigrok session format (*.sr).
+ * Remember the directory of the last file that was opened/saved.
+ * Automatically save and restore (upon PulseView shutdown/startup):
+    - the window state (size, position)
+    - the most recently used device
+ * Implement pinch-zoom support (useful e.g. on tablets).
+ * Implement an "always zoom-to-fit" feature.
+ * Implement a "sticky scrolling" feature, using hotkey 's' (bug #121).
+ * Show icons for active triggers on the right-hand side of the window.
+ * Cursors: Also show the frequency in addition to distance/time (bug #502).
+ * List available serial ports (for serial-based devices) in a drop-down.
+ * Add support for trace groups. An arbitrary number of traces can be grouped
+   together, which allows for dragging them around as a group and re-ordering
+   traces within the group. Hotkeys: group (CTRL+g), ungroup (CTRL+u).
+ * Use either alternating gray values as trace background color, or the
+   trace's own color (slightly modified). This behaviour is configurable
+   via the menu or the 'b' hotkey.
+ * Show a vertical marker at the trigger point (bug #685).
+ * Sampling bar: Show the total sampling time in a tooltip.
+ * Trace/Decoder popups: Close the popup when the ENTER/RETURN key is pressed.
+ * Improve horizontal arrow-key scrolling step size.
+ * The viewport can now be dragged vertically as well.
+ * Add support for placing arbitrary markers (double-click at the desired
+   position in the ruler area) with arbitrary name/content. Markers can be
+   removed via the delete key (or the right-click context menu).
+ * Show full device names in the device drop-down.
+ * The device selector combobox is now a split button. Clicking on a device
+   name will open the "Connect to Device" dialog which allows for manual
+   device selection. Clicking on the small arrow will open a drop-down with
+   all autodetected devices.
+ * The main menu is now hidden by default, pressing ALT will make it appear,
+   pressing ALT or ESC will hide it again.
+ * Add an Export menu item, which can export the data in various formats.
+ * Add an Import menu item, which allows data import of various formats.
+ * Support specifying input files + formats on the command-line (-i and -I).
+ * Update the possible samplerates/samplecounts widgets when needed.
+ * Various UI fixes and improvements (colors, font sizes, clipping, etc).
+ * Add support for the SR_CONF_{POWER_OFF,PROBE_FACTOR} config keys.
+ * Include the device / filename in the window title (bug #244).
+ * Keyboard shortcuts:
+   - Add space as a shortcut to start/stop an acquisition.
+   - Add shortcuts for the open/save menu items (CTRL-o, CTRL-s).
+   - Add trace group shortcuts: group (CTRL+g), ungroup (CTRL+u).
+   - Add 's' as shortcut for enabling/disabling sticky scrolling.
+   - Add 'b' as shortcut for colored / alternating-gray trace backgrounds.
+ * Add a few missing toolbar buttons.
+ * Provide tooltips for decoder annotations (which show the "full" annotation
+   text regardless of zoom-level).
+ * Improvements to work (better) with Qt5 (Qt4 remains supported as well).
+ * The whole code-base has been converted to C++11 (dropping the use
+   of various Boost functionality in favor of std:: equivalents).
+ * Build system:
+   - Always build with -std=c++11.
+   - Auto-detect Qt4 or Qt5. If both are available, Qt5 will be selected
+     unless the cmake option FORCE_QT4 is set to TRUE.
+   - Don't use Qt-defined keywords (can cause issues with other headers).
+   - Fix an issue with QtSvg linking (bug #369).
+   - Fix a build issue related to Qt4 MOC.
+   - Add "-git-<hash>" suffix to development version numbers (bug #609).
+   - Fix the build for older glibmm versions (bug #548).
+ * Updated build requirements:
+   - A C++ compiler with C++11 support (g++ >= 4.7 or clang++ >= 3.1)
+   - glibmm-2.4 (>= 2.28.0)
+   - Qt4 >= 4.5 or Qt5 (including the following components):
+    - Qt4: QtCore, QtGui, QtSvg
+    - Qt5: Qt5Core, Qt5Gui, Qt5Widgets, Qt5Svg
+   - Boost >= 1.53 (bugs #722, #593).
+   - libsigrokcxx >= 0.4.0 (libsigrok C++ bindings)
+   - libsigrokdecode >= 0.4.0
+   - libsigrokandroidutils >= 0.1.0 (optional, only needed on Android)
+ * Dropped build requirements:
+   - libsigrok (PulseView now uses libsigrokcxx instead).
+ * manpage:
+   - Various fixes and updates.
+   - Document all keyboard shortcuts.
+ * Android:
+   - Add basic Android support and support for building a PulseView APK.
+   - Install logging callbacks for Android.
+   - Fix a rendering issue by disabling the system background.
+   - Fix an issue due to a missing libintl.so in the APK (bug #575).
+   - Add asset reader functionality, e.g. for firmware files.
+   - Include firmware files (from sigrok-firmware) in the APK (bug #400).
+ * Windows:
+   - Fix an issue related to CMAKE_MODULE_PATH usage.
+   - Fix multiple Boost- and thread-related issues.
+   - Fix a build issue due to windows.h namespace pollution (bug #517).
+   - Fix an issue when saving .sr files (bug #615).
+ * Mac OS X:
+   - Fix an issue related to Glib::Variant types.
+   - Fix a build failure on Mac OS X 10.10 (bug #621).
+ * NSIS:
+   - Drop libusb0.dll, we use libusb-1.0 everywhere now.
+   - Add start menu entries for Zadig (bug #542).
+   - Support out-of-tree builds.
+   - Don't hardcode the MXE install location.
+ * README: Drop reference to obsolete sigrok-commits mailing list.
+ * Add a Doxygen file for auto-generated code documentation.
+ * Populate signal popup combo box with signal names (not probe names).
+ * Fix various thread related issues.
+ * Fix various compiler warnings and compiler portability issues.
+ * Adapt PulseView to use the new libsigrokcxx trigger API (bugs #448, #452).
+ * Reimplement file save using the "srzip" output module (bug #451).
+ * Check whether config keys are available before use (bug #487).
+ * Markers: Fix display of negative values in popups (bug #460).
+ * Properly handle device selection failure (bug #455).
+ * Fix a progressbar issue when saving files (bug #451).
+ * Fix incorrect channel names in .sr files (bug #490).
+ * Save and load signal names as UTF-8 strings (bug #498).
+ * Add a workaround for QTBUG-22829 (bug #532).
+ * Allow vertical scrolling via CTRL + mouse-wheel (bug #497).
+ * Fix an isnan() related compiler error (bug #531).
+ * Gracefully handle the case of a failing SAMPLERATE query (bug #529).
+ * Show less device info in the device dropdown (bug #285).
+ * Always show full device info in device selection tooltips (bug #489).
+ * Fix a segfault due to a missing Capability::LIST check (bug #453).
+ * Allow PulseView to be killed via CTRL-C from a terminal (bug #368).
+ * Support having no selected device (bug #488, #392).
+ * Add menu actions to the main window too (bug #590).
+ * Avoid confusing autocompletion in the channel name popups (bug #501).
+ * Fix an issue with channel ordering being reset incorrectly (bug #536).
+ * Fix an issue related to a disappearing samplerate dropdown (bug #594).
+ * Fix an issue with incorrect samplenumbers when running decoders.
+ * Suppress warnings from glibmm about deprecated auto_ptr (bug #654).
+ * Add a workaround for a corrupted timescale issue (bug #627).
+ * Fix a libzip-related issue resulting in errors writing .sr files (bug #570).
+ * Fix an issue that caused incorrect .sr files being saved (bug #599).
+ * When clicking a channel name, place the cursor in the text field (bug #298).
+ * Fix a double-free issue/segfault when trying to open an .sr file (bug #405).
+ * Fix an issue with incorrect placement of PD annotation tooltips (bug #477).
+ * Allow drag-moving both cursors (left + right) at the same time (bug #514).
+ * Improve the step-size of the vertical scroll bar click-movement (bug #513).
+ * Fix a too small display area for devices with many channels (bug #515).
+ * Fix a Qt5 QWellArray related static linking issue (bug #525).
+ * Fix broken session saving for devices with more than one channel (bug #404).
+ * Fix incorrect ruler units of kilo-/mega-/giga-seconds (bug #371).
+ * Fix an issue with segment sizes wasting huge amounts of memory (bug #622).
+ * Avoid a crash when running out of memory, show an error instead (bug #626).
+ * Improve behaviour when the last PD is removed from the stack (bug #510).
+ * Disable device options GUI elements after an acquisition start (bug #597).
+ * Fix an issue with the samplerate input format option (bug #595).
+ * Fix a segfault when switching from a file to a device and back (bug #596).
+ * Fix a segfault when loading very large .sr files (bug #592).
+ * Fix a segfault when selecting a device twice (bug #605).
+ * Fix an incorrect decode trace background color (bug #718).
+ * Fix incorrect annotation row background color behaviour (bug #719).
+ * Fix an issue with lingering decoder traces (bug #687).
+ * Fix non-intuitive channel name editing behaviour (bug #717).
+ * Speed up annotation drawing by quite a bit (bug #325).
+
 0.2.0 (2014-05-06)
 ------------------
 
diff --git a/README b/README
index e4aa3c1..ccc7d05 100644
--- a/README
+++ b/README
@@ -14,9 +14,6 @@ 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
 ---------------------
@@ -47,13 +44,10 @@ is to be interpreted as
  Copyright (C) 2010,2011,2012,2013 Contributor Name
 
 
-Mailing lists
--------------
-
-There are two mailing lists for sigrok/PulseView:
+Mailing list
+------------
 
  https://lists.sourceforge.net/lists/listinfo/sigrok-devel
- https://lists.sourceforge.net/lists/listinfo/sigrok-commits
 
 
 IRC
diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml
new file mode 100644
index 0000000..eacc002
--- /dev/null
+++ b/android/AndroidManifest.xml
@@ -0,0 +1,68 @@
+<?xml version='1.0' encoding='utf-8'?>
+<!--
+   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 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/>.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+		package="org.sigrok.pulseview"
+		android:versionName="0.2.0"
+		android:versionCode="20"
+		android:installLocation="auto">
+	<application android:hardwareAccelerated="true"
+		 android:name="org.sigrok.pulseview.PulseViewApplication"
+		 android:label="@string/pv_app_name"
+		 android:icon="@drawable/logo">
+		<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|locale|fontScale|keyboard|keyboardHidden|navigation"
+				android:name="org.sigrok.pulseview.PulseViewActivity"
+				android:label="@string/pv_app_name"
+				android:screenOrientation="unspecified"
+				android:launchMode="singleTop">
+			<intent-filter>
+				<action android:name="android.intent.action.MAIN"/>
+				<category android:name="android.intent.category.LAUNCHER"/>
+			</intent-filter>
+			<meta-data android:name="android.app.lib_name" android:value="pulseview"/>
+			<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
+			<meta-data android:name="android.app.repository" android:value="default"/>
+			<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
+			<meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/>
+			<!-- Deploy Qt libs as part of package -->
+			<meta-data android:name="android.app.bundle_local_qt_libs" android:value="1"/>
+			<meta-data android:name="android.app.bundled_in_lib_resource_id" android:resource="@array/bundled_in_lib"/>
+			<meta-data android:name="android.app.bundled_in_assets_resource_id" android:resource="@array/bundled_in_assets"/>
+			<!-- Run with local libs -->
+			<meta-data android:name="android.app.use_local_qt_libs" android:value="1"/>
+			<meta-data android:name="android.app.libs_prefix" android:value="/data/local/tmp/qt/"/>
+			<meta-data android:name="android.app.load_local_libs" android:value="plugins/platforms/android/libqtforandroid.so"/>
+			<meta-data android:name="android.app.load_local_jars" android:value="jar/QtAndroid.jar:jar/QtAndroidAccessibility.jar:jar/QtAndroid-bundled.jar:jar/QtAndroidAccessibility-bundled.jar"/>
+			<meta-data android:name="android.app.static_init_classes" android:value=""/>
+			<!-- Messages maps -->
+			<meta-data android:value="@string/ministro_not_found_msg" android:name="android.app.ministro_not_found_msg"/>
+			<meta-data android:value="@string/ministro_needed_msg" android:name="android.app.ministro_needed_msg"/>
+			<meta-data android:value="@string/fatal_error_msg" android:name="android.app.fatal_error_msg"/>
+			<!-- Messages maps -->
+			<!-- Splash screen -->
+			<!--
+			<meta-data android:name="android.app.splash_screen_drawable" android:resource="@drawable/logo"/>
+			-->
+			<!-- Splash screen -->
+		</activity>
+	</application>
+	<uses-sdk android:minSdkVersion="12" android:targetSdkVersion="14"/>
+	<supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
+	<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+</manifest>
diff --git a/android/assetreader.cpp b/android/assetreader.cpp
new file mode 100644
index 0000000..f14e7b6
--- /dev/null
+++ b/android/assetreader.cpp
@@ -0,0 +1,82 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2015 Daniel Elstner <daniel.kitta 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 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/>.
+ */
+
+#include "assetreader.hpp"
+#include <libsigrok/libsigrok.h>
+#include <memory>
+#include <QtCore/QDebug>
+#include <QtCore/QFile>
+#include <QtCore/QStandardPaths>
+
+using namespace pv;
+
+AndroidAssetReader::~AndroidAssetReader()
+{}
+
+void AndroidAssetReader::open(struct sr_resource *res, std::string name)
+{
+	if (res->type == SR_RESOURCE_FIRMWARE) {
+		auto path = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
+						   QString::fromStdString("sigrok-firmware/" + name));
+		if (path.isEmpty())
+			path = QString::fromStdString("assets:/sigrok-firmware/" + name);
+
+		std::unique_ptr<QFile> file {new QFile{path}};
+
+		if (!file->open(QIODevice::ReadOnly))
+			throw sigrok::Error{SR_ERR};
+
+		const auto size = file->size();
+		if (size < 0)
+			throw sigrok::Error{SR_ERR};
+
+		res->size = size;
+		res->handle = file.release();
+	} else {
+		qWarning() << "AndroidAssetReader: Unknown resource type" << res->type;
+		throw sigrok::Error{SR_ERR};
+	}
+}
+
+void AndroidAssetReader::close(struct sr_resource *res)
+{
+	if (!res->handle) {
+		qCritical("AndroidAssetReader: Invalid handle");
+		throw sigrok::Error{SR_ERR_ARG};
+	}
+	const std::unique_ptr<QFile> file {static_cast<QFile*>(res->handle)};
+	res->handle = nullptr;
+
+	file->close();
+}
+
+size_t AndroidAssetReader::read(const struct sr_resource *res, void *buf, size_t count)
+{
+	if (!res->handle) {
+		qCritical("AndroidAssetReader: Invalid handle");
+		throw sigrok::Error{SR_ERR_ARG};
+	}
+	auto *const file = static_cast<QFile*>(res->handle);
+
+	const auto n_read = file->read(static_cast<char*>(buf), count);
+	if (n_read < 0)
+		throw sigrok::Error{SR_ERR};
+
+	return n_read;
+}
diff --git a/android/assetreader.hpp b/android/assetreader.hpp
new file mode 100644
index 0000000..74d4999
--- /dev/null
+++ b/android/assetreader.hpp
@@ -0,0 +1,41 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2015 Daniel Elstner <daniel.kitta 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 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/>.
+ */
+
+#ifndef PULSEVIEW_ANDROID_ASSETREADER_HPP
+#define PULSEVIEW_ANDROID_ASSETREADER_HPP
+
+#include <libsigrokcxx/libsigrokcxx.hpp>
+
+namespace pv {
+
+class AndroidAssetReader : public sigrok::ResourceReader
+{
+public:
+	AndroidAssetReader() {}
+	virtual ~AndroidAssetReader();
+
+private:
+	void open(struct sr_resource *res, std::string name) override;
+	void close(struct sr_resource *res) override;
+	size_t read(const struct sr_resource *res, void *buf, size_t count) override;
+};
+
+} // namespace pv
+
+#endif // !PULSEVIEW_ANDROID_ASSETREADER_HPP
diff --git a/android/bundled_libs.xml.in b/android/bundled_libs.xml.in
new file mode 100644
index 0000000..d308390
--- /dev/null
+++ b/android/bundled_libs.xml.in
@@ -0,0 +1,24 @@
+<?xml version='1.0' encoding='utf-8'?>
+<!--
+   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 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/>.
+-->
+<resources>
+	<array name="bundled_libs">
+		@bundled_libs@
+	</array>
+</resources>
diff --git a/android/custom_rules.xml b/android/custom_rules.xml
new file mode 100644
index 0000000..0800575
--- /dev/null
+++ b/android/custom_rules.xml
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   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 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/>.
+-->
+<project>
+	<property name="pulseview-source-path" value="${basedir}/.."/>
+	<property name="pulseview-build-path" value="${pulseview-source-path}"/>
+	<property name="cmake-cache" value="${pulseview-build-path}/CMakeCache.txt"/>
+	<target name="-get-prefix">
+		<loadproperties srcFile="${cmake-cache}" prefix="cmake">
+			<filterchain>
+				<replaceregex pattern=":[A-Z]*=" replace="="/>
+			</filterchain>
+		</loadproperties>
+		<property name="prefix" value="${cmake.CMAKE_INSTALL_PREFIX}"/>
+		<property name="android.abi" value="${cmake.ANDROID_ABI}"/>
+	</target>
+	<target name="-declare-anttasks" depends="-get-prefix">
+		<taskdef resource="anttasks.properties"
+			classpath="${prefix}/jar/sigrok-androidutils-anttasks.jar"/>
+	</target>
+	<target name="-pre-build" depends="-get-prefix, -declare-anttasks">
+		<copylibs todir="${native.libs.absolute.dir}" property="bundled_libs">
+			<fileset dir="${pulseview-build-path}/libs"/>
+			<include name="libpulseview.so"/>
+			<exclude name="libQt5*.so"/>
+			<exclude name="libc.so"/>
+			<exclude name="libm.so"/>
+			<exclude name="libdl.so"/>
+			<exclude name="liblog.so"/>
+			<exclude name="libstdc++.so"/>
+			<exclude name="libz.so"/>
+		</copylibs>
+		<copy todir="${native.libs.absolute.dir}">
+			<fileset dir="${prefix}/jar">
+				<include name="QtAndroid-bundled.jar" />
+				<include name="QtAndroidAccessibility-bundled.jar" />
+			</fileset>
+		</copy>
+		<copy todir="${native.libs.absolute.dir}/${android.abi}">
+			<fileset dir="${prefix}/lib">
+				<include name="libQt5Core.so" />
+				<include name="libQt5Gui.so" />
+				<include name="libQt5Widgets.so" />
+				<include name="libQt5Svg.so" />
+			</fileset>
+		</copy>
+		<copy todir="${native.libs.absolute.dir}/${android.abi}">
+			<fileset dir="${prefix}/plugins">
+				<include name="platforms/android/libqtforandroid.so"/>
+				<include name="platforms/libqeglfs.so"/>
+				<include name="platforms/libqminimal.so"/>
+				<include name="platforms/libqminimalegl.so"/>
+				<include name="platforms/libqoffscreen.so"/>
+				<include name="generic/libqevdevkeyboardplugin.so"/>
+				<include name="generic/libqevdevmouseplugin.so"/>
+				<include name="generic/libqevdevtabletplugin.so"/>
+				<include name="generic/libqevdevtouchplugin.so"/>
+				<include name="imageformats/libqsvg.so"/>
+				<include name="iconengines/libqsvgicon.so"/>
+			</fileset>
+			<filtermapper>
+				<replacestring from="/" to="_" />
+				<prefixlines prefix="libplugins_" />
+			</filtermapper>
+		</copy>
+		<copy file="${prefix}/jar/sigrok-androidutils.jar"
+			tofile="${jar.libs.absolute.dir}/sigrok-androidutils.jar"/>
+		<copy file="${prefix}/share/sigrok-androidutils/device_filter.xml"
+			tofile="${resource.absolute.dir}/xml/device_filter.xml"/>
+		<copy file="bundled_libs.xml.in"
+			tofile="${resource.absolute.dir}/values/bundled_libs.xml">
+			<filterset>
+				<filter token="bundled_libs" value="${bundled_libs}"/>
+			</filterset>
+		</copy>
+		<copy file="${pulseview-source-path}/icons/sigrok-logo-notext.png"
+			tofile="${resource.absolute.dir}/drawable/logo.png"/>
+		<copy todir="${source.absolute.dir}">
+			<fileset dir="${prefix}/src/android/java/src">
+				<include name="org/qtproject/qt5/android/bindings/**"/>
+				<include name="org/kde/necessitas/ministro/**"/>
+			</fileset>
+		</copy>
+		<copy todir="${resource.absolute.dir}">
+			<fileset dir="${prefix}/src/android/java/res">
+				<include name="**/strings.xml"/>
+			</fileset>
+		</copy>
+		<copy todir="${asset.absolute.dir}/libsigrokdecode">
+			<fileset dir="${prefix}/share/libsigrokdecode"/>
+		</copy>
+		<copy todir="${asset.absolute.dir}/python3.3">
+			<fileset dir="${prefix}/lib/python3.3">
+				<include name="**/*.py"/>
+				<exclude name="**/test/**"/>
+				<exclude name="**/tests/**"/>
+				<exclude name="**/tkinter/**"/>
+				<exclude name="**/turtledemo/**"/>
+				<exclude name="**/turtle.py"/>
+				<exclude name="**/idlelib/**"/>
+			</fileset>
+		</copy>
+		<copy todir="${asset.absolute.dir}/sigrok-firmware">
+			<fileset dir="${prefix}/share/sigrok-firmware"/>
+		</copy>
+	</target>
+</project>
diff --git a/android/loghandler.cpp b/android/loghandler.cpp
new file mode 100644
index 0000000..2c6674c
--- /dev/null
+++ b/android/loghandler.cpp
@@ -0,0 +1,103 @@
+/*
+ * 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 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/>.
+ */
+
+#ifdef ENABLE_DECODE
+#include <libsigrokdecode/libsigrokdecode.h> /* First, so we avoid a _POSIX_C_SOURCE warning. */
+#endif
+
+#include <android/log.h>
+
+#include <stdint.h>
+#include <libsigrok/libsigrok.h>
+
+#include "android/loghandler.hpp"
+
+namespace pv {
+
+int AndroidLogHandler::sr_callback(void *cb_data, int loglevel, const char *format, va_list args)
+{
+	static const int prio[] = {
+		[SR_LOG_NONE] = ANDROID_LOG_SILENT,
+		[SR_LOG_ERR] = ANDROID_LOG_ERROR,
+		[SR_LOG_WARN] = ANDROID_LOG_WARN,
+		[SR_LOG_INFO] = ANDROID_LOG_INFO,
+		[SR_LOG_DBG] = ANDROID_LOG_DEBUG,
+		[SR_LOG_SPEW] = ANDROID_LOG_VERBOSE,
+	};
+	int ret;
+
+	/* This specific log callback doesn't need the void pointer data. */
+	(void)cb_data;
+
+	/* Only output messages of at least the selected loglevel(s). */
+	if (loglevel > sr_log_loglevel_get())
+		return SR_OK;
+
+	if (loglevel < SR_LOG_NONE)
+		loglevel = SR_LOG_NONE;
+	else if (loglevel > SR_LOG_SPEW)
+		loglevel = SR_LOG_SPEW;
+
+	ret = __android_log_vprint(prio[loglevel], "sr", format, args);
+
+	return ret;
+}
+
+int AndroidLogHandler::srd_callback(void *cb_data, int loglevel, const char *format, va_list args)
+{
+#ifdef ENABLE_DECODE
+	static const int prio[] = {
+		[SRD_LOG_NONE] = ANDROID_LOG_SILENT,
+		[SRD_LOG_ERR] = ANDROID_LOG_ERROR,
+		[SRD_LOG_WARN] = ANDROID_LOG_WARN,
+		[SRD_LOG_INFO] = ANDROID_LOG_INFO,
+		[SRD_LOG_DBG] = ANDROID_LOG_DEBUG,
+		[SRD_LOG_SPEW] = ANDROID_LOG_VERBOSE,
+	};
+	int ret;
+
+	/* This specific log callback doesn't need the void pointer data. */
+	(void)cb_data;
+
+	/* Only output messages of at least the selected loglevel(s). */
+	if (loglevel > srd_log_loglevel_get())
+		return SRD_OK;
+
+	if (loglevel < SRD_LOG_NONE)
+		loglevel = SRD_LOG_NONE;
+	else if (loglevel > SRD_LOG_SPEW)
+		loglevel = SRD_LOG_SPEW;
+
+	ret = __android_log_vprint(prio[loglevel], "srd", format, args);
+
+	return ret;
+#else
+	return 0;
+#endif
+}
+
+void AndroidLogHandler::install_callbacks()
+{
+	sr_log_callback_set(sr_callback, nullptr);
+#ifdef ENABLE_DECODE
+	srd_log_callback_set(srd_callback, nullptr);
+#endif
+}
+
+} // namespace pv
diff --git a/android/loghandler.hpp b/android/loghandler.hpp
new file mode 100644
index 0000000..b12a36e
--- /dev/null
+++ b/android/loghandler.hpp
@@ -0,0 +1,39 @@
+/*
+ * 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 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/>.
+ */
+
+#ifndef PULSEVIEW_ANDROID_LOGHANDLER_HPP
+#define PULSEVIEW_ANDROID_LOGHANDLER_HPP
+
+#include <stdarg.h>
+
+namespace pv {
+
+class AndroidLogHandler
+{
+private:
+	static int sr_callback(void *cb_data, int loglevel, const char *format, va_list args);
+	static int srd_callback(void *cb_data, int loglevel, const char *format, va_list args);
+
+public:
+	static void install_callbacks();
+};
+
+} // namespace pv
+
+#endif // PULSEVIEW_ANDROID_LOGHANDLER_HPP
diff --git a/android/res/layout/splash.xml b/android/res/layout/splash.xml
new file mode 100644
index 0000000..eb93afe
--- /dev/null
+++ b/android/res/layout/splash.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+   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 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/>.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"/>
diff --git a/android/res/values/libs.xml b/android/res/values/libs.xml
new file mode 100644
index 0000000..4447e9f
--- /dev/null
+++ b/android/res/values/libs.xml
@@ -0,0 +1,49 @@
+<?xml version='1.0' encoding='utf-8'?>
+<!--
+   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 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/>.
+-->
+<resources>
+	<array name="qt_sources">
+		<item>https://download.qt-project.org/ministro/android/qt5/qt-5.3</item>
+	</array>
+	<array name="qt_libs">
+		<!-- %%INSERT_QT_LIBS%% -->
+		<item>gnustl_shared</item>
+		<item>Qt5Core</item>
+		<item>Qt5Gui</item>
+		<item>Qt5Widgets</item>
+		<item>Qt5Svg</item>
+	</array>
+	<array name="bundled_in_lib">
+		<!-- %%INSERT_BUNDLED_IN_LIB%% -->
+		<item>libplugins_platforms_android_libqtforandroid.so:plugins/platforms/android/libqtforandroid.so</item>
+		<item>libplugins_platforms_libqeglfs.so:plugins/platforms/libqeglfs.so</item>
+		<item>libplugins_platforms_libqminimal.so:plugins/platforms/libqminimal.so</item>
+		<item>libplugins_platforms_libqminimalegl.so:plugins/platforms/libqminimalegl.so</item>
+		<item>libplugins_platforms_libqoffscreen.so:plugins/platforms/libqoffscreen.so</item>
+		<item>libplugins_generic_libqevdevkeyboardplugin.so:plugins/generic/libqevdevkeyboardplugin.so</item>
+		<item>libplugins_generic_libqevdevmouseplugin.so:plugins/generic/libqevdevmouseplugin.so</item>
+		<item>libplugins_generic_libqevdevtabletplugin.so:plugins/generic/libqevdevtabletplugin.so</item>
+		<item>libplugins_generic_libqevdevtouchplugin.so:plugins/generic/libqevdevtouchplugin.so</item>
+		<item>libplugins_imageformats_libqsvg.so:plugins/imageformats/libqsvg.so</item>
+		<item>libplugins_iconengines_libqsvgicon.so:plugins/iconengines/libqsvgicon.so</item>
+	</array>
+	<array name="bundled_in_assets">
+		<!-- %%INSERT_BUNDLED_IN_ASSETS%% -->
+	</array>
+</resources>
diff --git a/android/res/values/strings-pv.xml b/android/res/values/strings-pv.xml
new file mode 100644
index 0000000..4606c21
--- /dev/null
+++ b/android/res/values/strings-pv.xml
@@ -0,0 +1,22 @@
+<?xml version='1.0' encoding='utf-8'?>
+<!--
+   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 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/>.
+-->
+<resources>
+	<string name="pv_app_name">PulseView</string>
+</resources>
diff --git a/android/src/org/sigrok/pulseview/PulseViewActivity.java b/android/src/org/sigrok/pulseview/PulseViewActivity.java
new file mode 100644
index 0000000..cdd9149
--- /dev/null
+++ b/android/src/org/sigrok/pulseview/PulseViewActivity.java
@@ -0,0 +1,51 @@
+/*
+ * 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 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/>.
+ */
+
+package org.sigrok.pulseview;
+
+import org.qtproject.qt5.android.bindings.QtActivity;
+import org.sigrok.androidutils.UsbSupplicant;
+
+import android.os.Bundle;
+
+public class PulseViewActivity extends QtActivity
+{
+	private UsbSupplicant supplicant;
+
+	@Override
+	public void onCreate(Bundle savedInstanceState)
+	{
+		super.onCreate(savedInstanceState);
+		supplicant = new UsbSupplicant(getApplicationContext(), R.xml.device_filter);
+	}
+
+	@Override
+	protected void onStart()
+	{
+		super.onStart();
+		supplicant.start();
+	}
+
+	@Override
+	protected void onStop()
+	{
+		supplicant.stop();
+		super.onStop();
+	}
+}
diff --git a/android/src/org/sigrok/pulseview/PulseViewApplication.java b/android/src/org/sigrok/pulseview/PulseViewApplication.java
new file mode 100644
index 0000000..55d00d9
--- /dev/null
+++ b/android/src/org/sigrok/pulseview/PulseViewApplication.java
@@ -0,0 +1,38 @@
+/*
+ * 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 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/>.
+ */
+
+package org.sigrok.pulseview;
+
+import org.qtproject.qt5.android.bindings.QtApplication;
+import org.sigrok.androidutils.Environment;
+import org.sigrok.androidutils.UsbHelper;
+
+import java.io.File;
+import java.io.IOException;
+
+public class PulseViewApplication extends QtApplication
+{
+	@Override
+	public void onCreate()
+	{
+		Environment.initEnvironment(getApplicationInfo().sourceDir);
+		UsbHelper.setContext(getApplicationContext());
+		super.onCreate();
+	}
+}
diff --git a/config.h.in b/config.h.in
index aa6062d..8da6daf 100644
--- a/config.h.in
+++ b/config.h.in
@@ -29,6 +29,7 @@
 #define PV_VERSION_MAJOR @PV_VERSION_MAJOR@
 #define PV_VERSION_MINOR @PV_VERSION_MINOR@
 #define PV_VERSION_MICRO @PV_VERSION_MICRO@
+#define PV_VERSION_SUFFIX @PV_VERSION_SUFFIX@
 #define PV_VERSION_STRING "@PV_VERSION_STRING@"
 
 /* Platform properties */
diff --git a/contrib/pulseview_cross.nsi b/contrib/pulseview_cross.nsi.in
similarity index 85%
rename from contrib/pulseview_cross.nsi
rename to contrib/pulseview_cross.nsi.in
index 3c39351..6c0475c 100644
--- a/contrib/pulseview_cross.nsi
+++ b/contrib/pulseview_cross.nsi.in
@@ -38,7 +38,7 @@
 Name "PulseView"
 
 # Filename of the installer executable.
-OutFile "pulseview-0.2.0-installer.exe"
+OutFile "pulseview- at PV_VERSION_STRING@-installer.exe"
 
 # Where to install the application.
 InstallDir "$PROGRAMFILES\sigrok\PulseView"
@@ -54,7 +54,7 @@ RequestExecutionLevel admin
 # --- MUI interface configuration ---------------------------------------------
 
 # Use the following icon for the installer EXE file.
-!define MUI_ICON "../icons/sigrok-logo-notext.ico"
+!define MUI_ICON "@PROJECT_SOURCE_DIR@/icons/sigrok-logo-notext.ico"
 
 # Show a nice image at the top of each installer page.
 !define MUI_HEADERIMAGE
@@ -72,11 +72,7 @@ RequestExecutionLevel admin
 
 # 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"
+!define CROSS "@CMAKE_INSTALL_PREFIX@"
 
 
 # --- MUI pages ---------------------------------------------------------------
@@ -85,7 +81,7 @@ RequestExecutionLevel admin
 !insertmacro MUI_PAGE_WELCOME
 
 # Show the license of the project.
-!insertmacro MUI_PAGE_LICENSE "../COPYING"
+!insertmacro MUI_PAGE_LICENSE "@PROJECT_SOURCE_DIR@/COPYING"
 
 # Show a screen which allows the user to select which components to install.
 !insertmacro MUI_PAGE_COMPONENTS
@@ -123,15 +119,12 @@ Section "PulseView (required)" Section1
 	SetOutPath "$INSTDIR"
 
 	# License file.
-	File "../COPYING"
+	File "@PROJECT_SOURCE_DIR@/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).
+	# Zadig (used for installing WinUSB drivers).
 	File "${CROSS}/zadig.exe"
 	File "${CROSS}/zadig_xp.exe"
 
@@ -139,13 +132,13 @@ Section "PulseView (required)" Section1
 	File "${CROSS}/python32.dll"
 	File "${CROSS}/python32.zip"
 
+	SetOutPath "$INSTDIR\share"
+
 	# Protocol decoders.
-	SetOutPath "$INSTDIR\decoders"
-	File /r /x "__pycache__" "${CROSS}/share/libsigrokdecode/decoders/*"
+	File /r /x "__pycache__" /x "*.pyc" "${CROSS}/share/libsigrokdecode"
 
 	# Firmware files.
-	SetOutPath "$INSTDIR\firmware"
-	File /r "${CROSS}/share/sigrok-firmware/*"
+	File /r "${CROSS}/share/sigrok-firmware"
 
 	# Example *.sr files.
 	SetOutPath "$INSTDIR\examples"
@@ -170,6 +163,16 @@ Section "PulseView (required)" Section1
 		"$INSTDIR\Uninstall.exe" "" "$INSTDIR\Uninstall.exe" 0 \
 		SW_SHOWNORMAL "" "Uninstall PulseView"
 
+	# Create a shortcut for the Zadig executable.
+	CreateShortCut "$SMPROGRAMS\sigrok\PulseView\Zadig.lnk" \
+		"$INSTDIR\zadig.exe" "" "$INSTDIR\zadig.exe" 0 \
+		SW_SHOWNORMAL "" "Zadig"
+
+	# Create a shortcut for the Zadig executable (for Win XP).
+	CreateShortCut "$SMPROGRAMS\sigrok\PulseView\Zadig (Win XP).lnk" \
+		"$INSTDIR\zadig_xp.exe" "" "$INSTDIR\zadig_xp.exe" 0 \
+		SW_SHOWNORMAL "" "Zadig (Win XP)"
+
 	# Create registry keys for "Add/remove programs" in the control panel.
 	WriteRegStr HKLM "${REGSTR}" "DisplayName" "PulseView"
 	WriteRegStr HKLM "${REGSTR}" "UninstallString" \
@@ -183,7 +186,7 @@ Section "PulseView (required)" Section1
 	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}" "DisplayVersion" "@PV_VERSION_STRING@"
 	WriteRegStr HKLM "${REGSTR}" "Contact" \
 		"sigrok-devel at lists.sourceforge.org"
 	WriteRegStr HKLM "${REGSTR}" "Comments" \
@@ -206,31 +209,31 @@ Section "Uninstall"
 	# 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/.
+	# Delete all decoders and everything else in libsigrokdecode/.
 	# There could be *.pyc files or __pycache__ subdirs and so on.
-	RMDir /r "$INSTDIR\decoders\*"
+	RMDir /r "$INSTDIR\share\libsigrokdecode"
 
 	# Delete the firmware files.
-	RMDir /r "$INSTDIR\firmware\*"
+	RMDir /r "$INSTDIR\share\sigrok-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\share"
 	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 "$SMPROGRAMS\sigrok\PulseView\Zadig.lnk"
+	Delete "$SMPROGRAMS\sigrok\PulseView\Zadig (Win XP).lnk"
 
 	# Delete the sub-directory in the start menu.
 	RMDir "$SMPROGRAMS\sigrok\PulseView"
diff --git a/doc/pulseview.1 b/doc/pulseview.1
index b830f08..eb294e1 100644
--- a/doc/pulseview.1
+++ b/doc/pulseview.1
@@ -1,8 +1,8 @@
-.TH PULSEVIEW 1 "May 4, 2013"
+.TH PULSEVIEW 1 "December 16, 2015"
 .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]
+.B pulseview \fR[\fBOPTIONS\fR] [\fBfile.sr\fR]
 .SH "DESCRIPTION"
 .B PulseView
 is a cross-platform Qt-based GUI for the
@@ -39,6 +39,65 @@ Show a help text and exit.
 .TP
 .B "\-V, \-\-version"
 Show version information and exit.
+.TP
+.BR "\-i, \-\-input\-file " <filename>
+Load input from a file. If the
+.B \-\-input\-format
+option is not supplied, PulseView attempts to load the file as a sigrok session
+file.
+.TP
+.BR "\-I, \-\-input\-format " <format>
+Specifies the format of the input file to be loaded.
+.SH "KEYBOARD SHORTCUTS"
+.TP
+.B "f"
+Zoom-to-fit.
+.TP
+.B "o"
+Zoom 1:1.
+.TP
+.B "s"
+Enable / disable sticky scrolling.
+.TP
+.B "c"
+Show / hide cursors.
+.TP
+.B "b"
+Toggle between coloured trace backgrounds and alternating light/dark
+gray trace backgrounds.
+.TP
+.B "SPACE"
+Start / stop an acquisition.
+.TP
+.B "ALT"
+Show / hide the menu.
+.TP
+.B "Arrow keys"
+Scroll up/down/left/right.
+.TP
+.B "CTRL+o"
+Open file.
+.TP
+.B "CTRL+s"
+Save as...
+.TP
+.B "CTRL+r"
+Save selected range as...
+.TP
+.B "CTRL+g"
+Group all currently selected traces into a trace group.
+.TP
+.B "CTRL+u"
+Ungroup the traces in the currently selected trace group.
+.TP
+.B "CTRL++"
+Zoom in.
+.TP
+.B "CTRL+-"
+Zoom out.
+.TP
+.B "CTRL+q"
+Quit, i.e. shutdown PulseView.
 .SH "EXIT STATUS"
 .B PulseView
 exits with 0 on success, 1 on most failures.
diff --git a/extdef.h b/extdef.h
index 8af615c..0e92865 100644
--- a/extdef.h
+++ b/extdef.h
@@ -21,7 +21,7 @@
 #ifndef PULSEVIEW_EXTDEF_H
 #define PULSEVIEW_EXTDEF_H
 
-#define countof(x) (sizeof(x)/sizeof(x[0]))
+#define countof(x) (sizeof(x) / sizeof(x[0]))
 
 #define begin_element(x) (&x[0])
 #define end_element(x) (&x[countof(x)])
diff --git a/icons/add-decoder.svg b/icons/add-decoder.svg
new file mode 100644
index 0000000..58167ba
--- /dev/null
+++ b/icons/add-decoder.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg id="svg2" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="24" width="24" version="1.0" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path id="path2989" d="m1.5 12.5h21" stroke="#888a85" stroke-linecap="square" stroke-width="1px" fill="none"/>
+ <path id="path2991" d="m3.5 12.5 2-5h2l2 5-2 5h-2z" stroke="#967b00" stroke-width="1px" fill="#edd400"/>
+ <path id="path3012" d="m9.5 12.5 2-5 7 4e-7 2 5-2 5-7-0.000005z" stroke="#4e9a06" stroke-width="1px" fill="#73d216"/>
+</svg>
diff --git a/icons/channels.svg b/icons/channels.svg
new file mode 100644
index 0000000..42e3cca
--- /dev/null
+++ b/icons/channels.svg
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg id="svg2" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="24" width="24" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <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/>
+   </cc:Work>
+  </rdf:RDF>
+ </metadata>
+ <defs id="defs4">
+  <radialGradient id="radialGradient3098" gradientUnits="userSpaceOnUse" cy="20.5" cx="15" gradientTransform="matrix(1.8793 3.9617e-8 -.0000050558 .34168 -17.189 14.996)" r="5.8534">
+   <stop id="stop3930" stop-color="#888a85" stop-opacity=".52995" offset="0"/>
+   <stop id="stop3932" stop-color="#888a85" stop-opacity="0" offset="1"/>
+  </radialGradient>
+ </defs>
+ <g id="g3091" transform="translate(1.0007 -1)">
+  <path id="path3926" fill-rule="evenodd" fill="url(#radialGradient3098)" d="m21.999 22a11 2 0 0 1 -22 0 11 2 0 1 1 22 0z"/>
+  <path id="path3806" d="m10 10 1.5-1.5 3 3l-1.5 1.5z" stroke="#a40000" stroke-width="1px" fill="#c00"/>
+  <path id="path3808" d="m1.5 21.5 2-2" stroke="#888a85" stroke-linecap="round" stroke-width="1px" fill="none"/>
+  <path id="path3804" stroke-linejoin="round" d="m9.5 9.5 0.5 2.5-6 6-2 2.5 0.5 0.5 2.5-2 6-6l2.5 0.5z" stroke="#a40000" stroke-width="1px" fill="#c00"/>
+  <path id="path3793" stroke-linejoin="round" d="m16.5 2.5-0.955 0.9548-0.045 1.0452-0.5 0.5-0.567-0.4332-3.433 3.4332 4 4 3.5-3.5-0.5-0.5l0.5-0.5h1l1-1c-1.5-0.5-3.5-2.5-4-4z" stroke="#a40000" stroke-width="1px" fill="#c00"/>
+ </g>
+</svg>
diff --git a/icons/decoder-hidden.svg b/icons/decoder-hidden.svg
index d462200..43678af 100644
--- a/icons/decoder-hidden.svg
+++ b/icons/decoder-hidden.svg
@@ -1,33 +1,14 @@
 <?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 id="svg2989" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="24" width="24" 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>
+ <path id="path3053" d="m22 12c-2.4408 2.9089-6.2027 4.6631-10 4.6631s-7.5592-1.7542-10-4.6631" stroke="#000" stroke-width="1.5" fill="none"/>
 </svg>
diff --git a/icons/decoder-shown.svg b/icons/decoder-shown.svg
index 6050bdc..a634cf4 100644
--- a/icons/decoder-shown.svg
+++ b/icons/decoder-shown.svg
@@ -1,47 +1,19 @@
 <?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 id="svg2989" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="24" width="24" 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="g3321" transform="translate(1 -.99889)">
+  <path id="path2989" stroke-linejoin="round" d="m13.138 8.2631a5.2582 5.2582 0 0 1 0.97074 9.0443" stroke="#000" stroke-linecap="round" stroke-width=".44721" fill="none"/>
+  <path id="path2995" d="m12.307 12.994a1.3065 1.3065 0 0 1 -2.6131 0 1.3065 1.3065 0 1 1 2.6131 0z"/>
+  <path id="path3053" d="m10.875 8.3438c-2.9256 0.0238-5.865 1.035-8.2812 3.0622-0.5822 0.489-1.1053 1.012-1.5938 1.594 0.4885 0.582 1.0116 1.105 1.5938 1.594 5.5228 4.634 13.772 3.929 18.406-1.594-2.607-3.1066-6.363-4.687-10.125-4.6562z" stroke="#000" stroke-width="1.5" fill="none"/>
+  <path id="path3057" stroke-linejoin="round" d="m7.9014 17.315a5.2582 5.2582 0 0 1 0.5896 -8.8691" stroke="#000" stroke-linecap="round" stroke-width=".44721" fill="none"/>
+ </g>
 </svg>
diff --git a/icons/menu.svg b/icons/menu.svg
new file mode 100644
index 0000000..2d1492d
--- /dev/null
+++ b/icons/menu.svg
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg id="svg2991" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="24" width="24" 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="defs3001">
+  <linearGradient id="linearGradient2999" y2="8.5" gradientUnits="userSpaceOnUse" x2="12.5" y1="7" x1="12.5">
+   <stop id="stop2995" stop-color="#2e3436" offset="0"/>
+   <stop id="stop2997" stop-color="#888a85" offset="1"/>
+  </linearGradient>
+ </defs>
+ <g id="g3001-4" fill-rule="evenodd" transform="translate(0,5)">
+  <rect id="rect2993-6" rx="1.5" ry="1.5" height="3" width="18" y="6" x="3" fill="#2e3436"/>
+  <rect id="rect2991-0" rx="1" ry="1" height="2" width="16" y="6.5" x="4" fill="url(#linearGradient2999)"/>
+ </g>
+ <g id="g3001-8" fill-rule="evenodd" transform="translate(0,10)">
+  <rect id="rect2993-9" rx="1.5" ry="1.5" height="3" width="18" y="6" x="3" fill="#2e3436"/>
+  <rect id="rect2991-8" rx="1" ry="1" height="2" width="16" y="6.5" x="4" fill="url(#linearGradient2999)"/>
+ </g>
+ <g id="g3001" fill-rule="evenodd">
+  <rect id="rect2993" rx="1.5" ry="1.5" height="3" width="18" y="6" x="3" fill="#2e3436"/>
+  <rect id="rect2991" rx="1" ry="1" height="2" width="16" y="6.5" x="4" fill="url(#linearGradient2999)"/>
+ </g>
+</svg>
diff --git a/icons/probes.svg b/icons/probes.svg
deleted file mode 100644
index 8e7e604..0000000
--- a/icons/probes.svg
+++ /dev/null
@@ -1,72 +0,0 @@
-<?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/show-cursors.svg b/icons/show-cursors.svg
new file mode 100644
index 0000000..1cbf440
--- /dev/null
+++ b/icons/show-cursors.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg id="svg2" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="24" width="24" version="1.0" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path id="path2993" d="m1.5 3.5v5h4l3 3v-8z" stroke="#204a87" stroke-width="1px" fill="#3465a4"/>
+ <path id="path2995" d="m8.5 20.5v-16" stroke="#204a87" stroke-linecap="square" stroke-width="1px" fill="none"/>
+ <path id="path2993-2" d="m22.5 3.5v5h-4l-3 3v-8z" stroke="#204a87" stroke-width="1px" fill="#3465a4"/>
+ <path id="path2995-2" d="m15.5 20.5v-16" stroke="#204a87" stroke-linecap="square" stroke-width="1px" fill="none"/>
+</svg>
diff --git a/icons/sigrok-logo-notext.ico b/icons/sigrok-logo-notext.ico
index 8f1228c..d8b50c6 100644
Binary files a/icons/sigrok-logo-notext.ico and b/icons/sigrok-logo-notext.ico differ
diff --git a/icons/sigrok-logo-notext.svg b/icons/sigrok-logo-notext.svg
new file mode 100644
index 0000000..620028d
--- /dev/null
+++ b/icons/sigrok-logo-notext.svg
@@ -0,0 +1,234 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg id="svg2" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="48" width="48" version="1.1" 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="defs4">
+  <filter id="filter5288" color-interpolation-filters="sRGB">
+   <feGaussianBlur id="feGaussianBlur5290" stdDeviation="1.2304688"/>
+  </filter>
+  <filter id="filter5288-0" color-interpolation-filters="sRGB">
+   <feGaussianBlur id="feGaussianBlur5290-1" stdDeviation="1.2304688"/>
+  </filter>
+  <filter id="filter5354" color-interpolation-filters="sRGB">
+   <feGaussianBlur id="feGaussianBlur5356" stdDeviation="1.2575"/>
+  </filter>
+  <filter id="filter5354-5" color-interpolation-filters="sRGB">
+   <feGaussianBlur id="feGaussianBlur5356-2" stdDeviation="1.2575"/>
+  </filter>
+  <filter id="filter5412" color-interpolation-filters="sRGB">
+   <feGaussianBlur id="feGaussianBlur5414" stdDeviation="1.311875"/>
+  </filter>
+  <filter id="filter5412-5" color-interpolation-filters="sRGB">
+   <feGaussianBlur id="feGaussianBlur5414-6" stdDeviation="1.311875"/>
+  </filter>
+  <filter id="filter5470" height="1.0266" width="1.2422" color-interpolation-filters="sRGB" y="-.013320" x="-.12110">
+   <feGaussianBlur id="feGaussianBlur5472" stdDeviation="1.30875"/>
+  </filter>
+  <filter id="filter5470-6" height="1.0266" width="1.2422" color-interpolation-filters="sRGB" y="-.013320" x="-.12110">
+   <feGaussianBlur id="feGaussianBlur5472-1" stdDeviation="1.30875"/>
+  </filter>
+  <filter id="filter5536" height="1.0246" width="1.9962" color-interpolation-filters="sRGB" y="-.012296" x="-.49812">
+   <feGaussianBlur id="feGaussianBlur5538" stdDeviation="1.2453125"/>
+  </filter>
+  <filter id="filter5703" height="1" width="1" color-interpolation-filters="sRGB" y="0" x="0">
+   <feGaussianBlur id="feGaussianBlur5705" stdDeviation="1.709984544049459" result="result8"/>
+   <feTurbulence id="feTurbulence5707" baseFrequency="0.0080370942812983005 0.027820710973724884" seed="56" result="result7" numOctaves="2" type="turbulence"/>
+   <feComposite id="feComposite5709" operator="in" result="result6" in2="result8" in="SourceGraphic"/>
+   <feComposite id="feComposite5711" in="result6" in2="result7" k3="1" k2="0" k1="0" result="result2" k4="0" operator="arithmetic"/>
+   <feComposite id="feComposite5713" operator="in" result="fbSourceGraphic" in2="result6" in="result2"/>
+   <feComposite id="feComposite5715" in="fbSourceGraphic" in2="fbSourceGraphic" k3="0" k2="2.5" k1="0" result="fbSourceGraphic" k4="0" operator="arithmetic"/>
+   <feColorMatrix id="feColorMatrix5805" result="fbSourceGraphicAlpha" values="0" type="saturate" in="fbSourceGraphic"/>
+   <feColorMatrix id="feColorMatrix5807" values="1" type="saturate" result="result2" in="fbSourceGraphic"/>
+   <feFlood id="feFlood5809" flood-color="rgb(113,79,56)" result="result1"/>
+   <feBlend id="feBlend5811" result="result3" mode="multiply" in2="result2" in="result1"/>
+   <feComposite id="feComposite5813" operator="in" result="result4" in2="fbSourceGraphic"/>
+  </filter>
+  <filter id="filter4766-3" color-interpolation-filters="sRGB">
+   <feGaussianBlur id="feGaussianBlur4768-8" stdDeviation="0.25453125"/>
+  </filter>
+  <filter id="filter4654-6" height="1.0252" width="1.5121" color-interpolation-filters="sRGB" y="-.012590" x="-.25603">
+   <feGaussianBlur id="feGaussianBlur4656-4" stdDeviation="0.4267185"/>
+  </filter>
+  <filter id="filter4596-6" height="1.2771" width="1.0125" color-interpolation-filters="sRGB" y="-.13854" x="-.0062716">
+   <feGaussianBlur id="feGaussianBlur4598-4" stdDeviation="0.48343725"/>
+  </filter>
+  <filter id="filter4536-1" height="1.0263" width="1.2736" color-interpolation-filters="sRGB" y="-.013154" x="-.13680">
+   <feGaussianBlur id="feGaussianBlur4538-3" stdDeviation="0.45059085"/>
+  </filter>
+  <linearGradient id="linearGradient4358-7">
+   <stop id="stop4360-7" stop-color="#aca592" offset="0"/>
+   <stop id="stop4362-1" stop-color="#aca592" stop-opacity="0" offset="1"/>
+  </linearGradient>
+  <filter id="filter6293" height="1.4486" width="1.1034" color-interpolation-filters="sRGB" y="-.22428" x="-.051678">
+   <feGaussianBlur id="feGaussianBlur6295" stdDeviation="4.5525501"/>
+  </filter>
+  <filter id="filter6293-3" height="1.4486" width="1.1034" color-interpolation-filters="sRGB" y="-.22428" x="-.051678">
+   <feGaussianBlur id="feGaussianBlur6295-5" stdDeviation="4.5525501"/>
+  </filter>
+  <filter id="filter7222" color-interpolation-filters="sRGB">
+   <feGaussianBlur id="feGaussianBlur7224" stdDeviation="0.93122891"/>
+  </filter>
+  <filter id="filter7410" color-interpolation-filters="sRGB">
+   <feGaussianBlur id="feGaussianBlur7412" stdDeviation="0.28402344"/>
+  </filter>
+  <filter id="filter7414" color-interpolation-filters="sRGB">
+   <feGaussianBlur id="feGaussianBlur7416" stdDeviation="0.28402344"/>
+  </filter>
+  <linearGradient id="linearGradient4024" y2="837.09" gradientUnits="userSpaceOnUse" x2="112.48" gradientTransform="translate(279.37)" y1="900.59" x1="-60.266">
+   <stop id="stop4324-0" stop-color="#6c6753" stop-opacity=".50862" offset="0"/>
+   <stop id="stop4332-7" stop-color="#a39e88" stop-opacity="0" offset=".45474"/>
+   <stop id="stop4330-01" stop-color="#a39e88" stop-opacity="0" offset=".77666"/>
+   <stop id="stop4326-8" stop-color="#6c6753" stop-opacity=".70690" offset="1"/>
+  </linearGradient>
+  <linearGradient id="linearGradient4026" y2="805.75" xlink:href="#linearGradient4358-7" gradientUnits="userSpaceOnUse" x2="1.7663" gradientTransform="translate(279.37)" y1="787.42" x1="1.1134"/>
+  <linearGradient id="linearGradient4028" y2="716.49" gradientUnits="userSpaceOnUse" x2="-133.64" gradientTransform="translate(279.37)" y1="715.43" x1="-134">
+   <stop id="stop4408-14" offset="0"/>
+   <stop id="stop4410-2" stop-opacity="0" offset="1"/>
+  </linearGradient>
+  <linearGradient id="linearGradient4030" y2="806.65" gradientUnits="userSpaceOnUse" x2="-69.701" gradientTransform="translate(279.37)" y1="839.88" x1="-69.701">
+   <stop id="stop4542-9" stop-color="#989078" offset="0"/>
+   <stop id="stop4544-5" stop-color="#aca592" stop-opacity="0" offset="1"/>
+  </linearGradient>
+  <linearGradient id="linearGradient4032" y2="820.96" xlink:href="#linearGradient4358-7" gradientUnits="userSpaceOnUse" x2="-8.9303" gradientTransform="translate(279.37)" y1="829.56" x1="-8.9303"/>
+  <linearGradient id="linearGradient4034" y2="801.74" gradientUnits="userSpaceOnUse" x2="25.605" gradientTransform="translate(279.37)" y1="911.18" x1="25.605">
+   <stop id="stop4704-1" stop-color="#6c6753" stop-opacity=".50862" offset="0"/>
+   <stop id="stop4706-4" stop-color="#a39e88" stop-opacity="0" offset=".60980"/>
+   <stop id="stop4708-5" stop-color="#a39e88" stop-opacity="0" offset=".77666"/>
+   <stop id="stop4710-4" stop-color="#6c6753" stop-opacity=".70690" offset="1"/>
+  </linearGradient>
+  <radialGradient id="radialGradient4036" gradientUnits="userSpaceOnUse" cy="356.19" cx="300" gradientTransform="matrix(1.3455 -.0000010547 2.0946e-7 .10026 -103.64 317.01)" r="55.264">
+   <stop id="stop6329" offset="0"/>
+   <stop id="stop6333" stop-color="#464646" offset=".60223"/>
+   <stop id="stop6331" stop-color="#aca592" stop-opacity="0" offset="1"/>
+  </radialGradient>
+  <radialGradient id="radialGradient4038" gradientUnits="userSpaceOnUse" cy="38.204" cx="301.23" gradientTransform="matrix(1 0 0 13.333 0 -471.18)" r="2.1213">
+   <stop id="stop4661" stop-color="#fff" stop-opacity=".33621" offset="0"/>
+   <stop id="stop4663" stop-color="#fff" stop-opacity="0" offset="1"/>
+  </radialGradient>
+  <radialGradient id="radialGradient4040" gradientUnits="userSpaceOnUse" cy="67.522" cx="302.84" gradientTransform="matrix(.74160 0 0 3.0648 78.253 -107.79)" r="7.0144">
+   <stop id="stop4691" stop-color="#fff" stop-opacity=".23276" offset="0"/>
+   <stop id="stop4693" stop-color="#fff" stop-opacity="0" offset="1"/>
+  </radialGradient>
+  <linearGradient id="linearGradient4074" y2="333.77" gradientUnits="userSpaceOnUse" x2="291.22" gradientTransform="matrix(1.0518 0 0 1.0574 -15.542 -15.976)" y1="330.77" x1="291.22">
+   <stop id="stop5572" stop-color="#fff" stop-opacity=".36207" offset="0"/>
+   <stop id="stop5574" stop-opacity="0" offset="1"/>
+  </linearGradient>
+  <linearGradient id="linearGradient4076" y2="339.48" gradientUnits="userSpaceOnUse" x2="337.09" gradientTransform="matrix(1.0417 0 0 1.2372 -12.207 -75.58)" y1="339.48" x1="343.43">
+   <stop id="stop5572-3" stop-color="#fff" stop-opacity=".36207" offset="0"/>
+   <stop id="stop5574-2" stop-opacity="0" offset="1"/>
+  </linearGradient>
+  <linearGradient id="linearGradient4078" y2="304.68" gradientUnits="userSpaceOnUse" x2="300.31" gradientTransform="translate(0,4)" y1="331.2" x1="300.31">
+   <stop id="stop5656" stop-opacity=".30172" offset="0"/>
+   <stop id="stop5658" stop-opacity="0" offset="1"/>
+  </linearGradient>
+ </defs>
+ <metadata id="metadata7">
+  <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" transform="translate(-93.213 -870.88)">
+  <path id="path1648" d="m1319.8 222.81c105.62-29.815 164.49-18.697 304.84-6.9484 82.527 6.9082 180.31-11.422 237.14-8.5433 95.114 4.8165 189.98 28.25 321.13 20.545" display="none" stroke="#fff" stroke-width="1px" fill="none"/>
+ </g>
+ <g id="layer2" transform="translate(-93.213 -418.52)">
+  <g id="g3938" transform="matrix(.10491 0 0 .10491 85.726 417.58)">
+   <path id="rect10305-9-6-8-1-4-6" style="color:#000000" d="m403.34 894.39 1.1978 14.787-3.5969 6.1085-5.8543 2.6695-6.7717 1.1769-12.988-1.6568-14.183 0.0522-9.733 0.82833-9.6449 1.6044-9.7661-1.2906-9.033-0.36618-15.992 0.46239-12.627 1.1944-11.95-3.3136-8.446 0.80213-11.891 1.6831-16.155-1.6219-12.442 1.6743-9.5288 0.11012-7.6552-3.0431-11.662-3.9917-4.2875-7.8841-4.7091-13.569-2.5128-14.622 5.9409-4.9408 199.41 1.7589 4.5302 6.1849 0.35355 15.203z" fill-opacity=".27273" transform="m [...]
+   <path id="rect10305-9-6-8-1-4-6-9" style="color:#000000" d="m403.34 894.39 1.1978 14.787-3.5969 6.1085-5.8543 2.6695-6.7717 1.1769-12.988-1.6568-14.183 0.0522-9.733 0.82833-9.6449 1.6044-9.7661-1.2906-9.033-0.36618-15.992 0.46239-12.627 1.1944-11.95-3.3136-8.446 0.80213-11.891 1.6831-16.155-1.6219-12.442 1.6743-9.5288 0.11012-7.6552-3.0431-11.662-3.9917-4.2875-7.8841-4.7091-13.569-2.5128-14.622 5.9409-4.9408 199.41 1.7589 4.5302 6.1849 0.35355 15.203z" fill-opacity=".60766" transform= [...]
+   <path id="rect10305-9-6-8-1-4" style="color:#000000" d="m399.81 876.33 0.13139 28.312-3.4465 5.2698-5.6095 2.303-6.4886 1.0153-12.445-1.4293-13.59 0.045-9.3261 0.7146-9.2417 1.3841-9.3578-1.1134-8.6554-0.3159-15.323 0.3989-12.099 1.0304-11.451-2.8586-8.0929 0.692-11.394 1.452-15.48-1.3992-11.922 1.4444-9.1305 0.095-7.3352-2.6253-11.175-3.4437-4.1083-6.8016-4.5122-11.706-2.4078-12.614 202.46 0.15063z" fill-opacity=".41627" transform="matrix(.97921 0 0 .79971 6.2101 -277.03)" filter="ur [...]
+   <g id="g6187" transform="translate(0 -456.36)">
+    <path id="rect10307-2-2" style="color:#000000" d="m302 787.43-10.188 1.625-7.4375-0.6875-4.1875 1.875-10.969-0.65625-15.906-0.4375-12.375-0.71875-8.7188 3.375-9.125 0.3125-3.1562 2.7812-5.5312-0.46875-5.125 3.625-0.0312 0.0312v0.0312l-5.1562 6.4062-2.875 14.812-2.4375 16.688 0.0312 14.625-0.65625 24.844 2.9062 11.156 2.4688 4.75 1.5625 4.125 2.5312 3.3438v0.0312l1.625 2.7812 3.7812 2.8125v-0.0625l7.4375 4.5938 10.156-0.125 9.0312 0.0937 11.812 1.4375 15.312-1.375 11.281 1.4375 8 0.68 [...]
+    <path id="rect10307-2-93" style="color:#000000" d="m302 787.42-10.188 1.625-7.4375-0.6875-4.1875 1.875-10.969-0.65625-15.906-0.4375-12.375-0.71875-8.7188 3.375-9.125 0.3125-3.1562 2.7812-5.5312-0.46875-5.125 3.625-0.0312 0.0312v0.0312l-5.1562 6.4062-2.875 14.812-2.4375 16.688 0.0312 14.625-0.65625 24.844 2.9062 11.156 2.4688 4.75 1.5625 4.125 2.5312 3.3438v0.031l1.625 2.7812 3.7812 2.8125v-0.062l7.4375 4.5938 10.156-0.125 9.0312 0.094 11.812 1.4375 15.312-1.375 11.281 1.4375 8 0.6875 [...]
+    <path id="rect10307-2-40" opacity=".85446" style="color:#000000" d="m240.93 788.44-8.722 3.3744-9.1332 0.29289-3.159 2.7813-5.513-0.4688-5.6193 4.9375-1.552 5.9778-0.28856 7.9063 5.6345 0.1875 2.8704 4.625 3.4762-0.65782 5.4177 1.2203 3.1183 0.18775 2.5158-1.0469 5.7268 1.7656 4.223-1.0781 4.6232 0.43755 6.4071 4.1406 6.4091-0.3437 2.0368-0.39063 13.819-0.45317 11.132-0.5937 5.4025-0.66994 5.913 1.2891 9.9355 0.18187 5.9864-2.5133 14.125 1.1596 19.16-1.0722 5.2521 1.7751 5.1057-0.337 [...]
+    <path id="path4520-4-5" opacity=".85446" style="color:#000000" d="m295.67 821.86-5.9301-1.3438-5.388 0.7188-11.132 0.5937-13.732 0.46483 20.46 0.78517 5.103 3.0313 4.5562 0.4062 5.7864-1 5.8775 1.5h6.485l3.3108-2.9375 7.943-0.5 22.887-1.0312 4.8725-0.67192 4.3831 1.1485 4.1244 2.1796 2.3844-0.1875 9.5528-1.9375 2.8859 0.8125 2.9916-0.7812 3.3564 0.75-7.1606-2.9493-14.056 0.1368-5.0615 0.33764-5.2963-1.7751-19.513 0.7187-13.74-0.875-5.9984 2.5625-9.9516-0.1562zm-55.761-3.4688-4.1917 1 [...]
+    <path id="rect10305-9-5" opacity=".85446" style="color:#000000" d="m215.44 818 2.6312 4.8729 7.0328-2.6372 15.87 4.8498 3.7519-0.7846 5.6678 1.7952 6.0397-2.0213 2.9938-1.7933 20.527 0.7826 5.1115 3.032 4.548 0.42551 5.7911-1.0106 5.878 1.516h6.487l3.3018-2.9522 7.9493-0.5053 22.891-1.0107 4.872-0.68284 4.3742 1.143 4.1299 2.1786 2.3796-0.1734 9.55-1.9601 2.893 0.82619 2.9862-0.77539 10.993 2.4759 10.526-2.5267 5.7016 11.871-0.31026 10.481 1.8466 14.246-1.231 24.242-1.1703 16.219-2.4 [...]
+    <path id="rect10309-8-0" opacity=".85446" style="color:#000000" d="m208.41 896.27-0.005-52.06-1.4772-19.14-1.524-9.5383-1.5111-3.6499 0.26834-4.6763 5.1214-9.1629-5.202 6.4739-2.8718 14.825-2.4333 16.683 0.0321 14.616-0.6476 24.862 2.9029 11.135 2.4543 4.7714 1.5812 4.11 2.5721 3.4418 0.73955-2.6911h0.00002z" fill="#aca793"/>
+    <path id="path4517-1-0" opacity=".85446" style="color:#000000" d="m214.44 794.41-5.2056 3.6638-5.066 9.134-0.27706 4.7026 1.5072 3.61 1.5339 9.5625 1.4732 19.125v41.156l4.3284-54.281 2.6578-12.812 0.0607-0.2185-2.8704-4.625-5.6345-0.1875 0.28856-7.9063 1.5868-5.9587 5.6169-4.9643z" fill="#e3e2db"/>
+    <path id="path4522-1-19" opacity=".85446" d="m208.31 885.27-0.18225 3.4688 0.27338-3.375-0.0911-0.094zm-0.19743 3.9375-0.50119 9.6875 1.6402 2.7813 3.7665 2.8125v-0.062l-0.18225-0.125-2.5211-5.625-2.2022-9.4688z" fill-rule="evenodd" fill="#797463"/>
+    <path id="rect10305-9-6-1" style="color:#000000" d="m215.44 818 2.6312 4.8729 7.0328-2.6372 15.87 4.8498 3.7519-0.7846 5.6678 1.7952 6.0397-2.0213 2.9938-1.7933 20.527 0.7826 5.1115 3.032 4.548 0.42551 5.7911-1.0106 5.878 1.516h6.487l3.3018-2.9522 7.9493-0.5053 22.891-1.0107 4.872-0.68284 4.3742 1.143 4.1299 2.1786 2.3796-0.1734 9.55-1.9601 2.893 0.82619 2.9862-0.77539 10.993 2.4759 10.526-2.5267 5.7016 11.871-0.31026 10.481 1.8466 14.246-1.231 24.242-1.1703 16.219-2.412 7.7302-4.068 [...]
+    <path id="rect10307-2-9-5" style="color:#000000" d="m240.93 788.44-8.722 3.3744-9.1332 0.29289-3.159 2.7813-5.513-0.4688-5.6193 4.9375-1.552 5.9778-0.28856 7.9063 5.6345 0.1875 2.8704 4.625 3.4762-0.65782 5.4177 1.2203 3.1496 0.12525 2.4689-0.96875 5.7893 1.6875 4.1605-1.0938 4.5763 0.4688 6.4696 4.1874 6.4091-0.3437 2.0368-0.39063 13.819-0.45317 11.132-0.5937 5.388-0.75005 5.8364 1.3438 10.077 0.12495 5.9672-2.4688 14.094 1.1973 19.16-1.0722 5.2575 1.7343 5.1003-0.29682 14.045-0.156 [...]
+    <path id="rect10309-8-4-6" style="color:#000000" d="m208.41 896.27-0.005-52.06-1.4772-19.14-1.524-9.5383-1.5111-3.6499 0.26834-4.6763 5.1214-9.1629-5.202 6.4739-2.8718 14.825-2.4333 16.683 0.0321 14.616-0.6476 24.862 2.9029 11.135 2.4543 4.7714 1.5812 4.11 2.5721 3.4418 0.73955-2.6911h0.00002z" fill="url(#linearGradient4028)"/>
+    <path id="path4517-1-6-7" style="color:#000000" d="m214.44 794.41-5.2056 3.6638-5.066 9.134-0.27706 4.7026 1.5072 3.61 1.5339 9.5625 1.4732 19.125v41.156l4.3284-54.281 2.6578-12.812 0.0607-0.2185-2.8704-4.625-5.6345-0.1875 0.28856-7.9063 1.5868-5.9587 5.6169-4.9643z" fill="url(#linearGradient4030)"/>
+    <path id="path4520-4-2-3" style="color:#000000" d="m295.7 821.86-5.9926-1.3438-5.3568 0.7188-11.132 0.5937-13.732 0.46483 20.46 0.78517 5.103 3.0313 4.5562 0.4062 5.7864-1 5.8775 1.5h6.485l3.3108-2.9375 7.943-0.5 22.887-1.0312 4.8725-0.67192 4.3831 1.1485 4.1244 2.1796 2.3844-0.1875 9.5528-1.9375 2.8859 0.8125 2.9916-0.7812 3.3564 0.75-7.1245-2.9375-14.092 0.125-5.0066 0.31245-5.3512-1.75-19.513 0.7187-13.803-0.875-5.8734 2.5312-9.9829-0.12495zm-55.824-3.4688-4.1605 1-5.7268-1.75-2.5 [...]
+    <path id="rect10305-9-6-2-0" style="color:#000000" d="m213.3 828.62-2.5971 22.13-3.1409 39.489 3.8661 12.473-2.0177-12.023 1.0763-18.998 1.913-21.72 0.90036-21.35z" filter="url(#filter4536-1)" fill="#7f7a66"/>
+    <path id="path4520-4-2-2-6" style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m397.96 818.25-3.875 0.1875h-0.0625-0.0625l-16.719 5.1562 0.28125 0.96875 16.625-5.125h0.0312l4.0625-0.1875h0.0312v-0.5l0.375 0.28125 0.75-1.9375-1.4374 1.1562zm-179.06-1.375-0.125 0.0312-3.4375 0.65625 0.1875 0.96875 3.3438-0.625 5.375 1.1875h0.0312 0.0625l3.0938 0.125h0.125l0.0937-0.0625 2.3438-0.96875 5.5938 1.6875 0.125 0.0312 0.125-0.0312 4-0.96875h0.0312l4.4375 0.46875 6. [...]
+    <path id="rect10309-8-4-2-0" style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m203.83 811.86 1.0771 3.7672 1.5312 9.4688v0.0625l1.4688 19.062v0.0312l-0.35355 51.943 0.32322 2.2981 0.32322-2.9168 0.70711-51.355v-0.0312-0.0312l-1.4688-19.125v-0.0312l-1.5312-9.5625-2.0771-3.5798z" filter="url(#filter4654-6)" fill="#888470"/>
+    <path id="rect10305-9-6-8-1" style="color:#000000" d="m215.44 818 2.6312 4.8729 7.0328-2.6372 15.87 4.8498 3.7519-0.7846 5.6678 1.7952 6.0397-2.0213 2.9938-1.7933 20.527 0.7826 5.1115 3.032 4.548 0.42551 5.7911-1.0106 5.878 1.516h6.487l3.3018-2.9522 7.9493-0.5053 22.891-1.0107 4.872-0.68284 4.3742 1.143 4.1299 2.1786 2.3796-0.1734 9.55-1.9601 2.893 0.82619 2.9862-0.77539 10.993 2.4759 10.526-2.5267 5.7016 11.871-0.31026 10.481 1.8466 14.246-1.231 24.242-1.1703 16.219-2.412 7.7302-4.0 [...]
+    <path id="path4517-1-6-8-9" style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m209.01 798.41-0.125 0.0937-0.0312 0.15625-1.8589 6.0259v0.0625 0.0625l-0.3125 7.9062v0.5l0.5 0.0312 5.2424 0.36428 2.7263 4.0107 0.52275 0.49686-2.429-5.4656-0.125-0.21875h-0.28125l-5.125-0.15625 0.28125-7.4375 1.5312-5.6875 4.7487-4.3005-5.2648 3.5559z" filter="url(#filter4766-3)" fill="#e3e2db"/>
+   </g>
+   <path id="path7353" style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m240.5 829.86v61h-22.719v5h27.719v-61h59v61h30v-61h16v61h28.5v-5h-23.5v-61h-26v61h-20v-61h-69z" fill-opacity=".40670" transform="translate(0 -452.36)" fill="#f2f1ef"/>
+   <path id="path7353-5" style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m240.5 377.5v61h-22.719v5h1v-3h22.719v-61h68v-2h-69zm89 0v61h-19v2h20v-61h25v-2h-26zm-25 7v59h1v-59h-1zm46 0v59h1v-59h-1zm6 54v2h22.5v-2h-22.5z" filter="url(#filter7410)" fill="#4b473c"/>
+   <path id="path7353-8" style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m308.5 829.86v60h1v-60h-1zm46 0v60h1v-60h-1zm-110 4v61h-26.719v1h27.719v-61h58v-1h-59zm89 0v61h-29v1h30v-61h15v-1h-16zm44.5 57v4h-27.5v1h28.5v-5h-1z" transform="translate(0 -452.36)" filter="url(#filter7414)" fill="#e2e0da"/>
+   <path id="path4332" style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m295 109.34v151.53 91.531h10v-91.531-151.53h-10z" fill="#d4aa00"/>
+   <path id="path4332-4" style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m297 109.34v243.06h6v-243.06h-6z" fill-opacity=".54440" filter="url(#filter5536)" fill="#fcff4c"/>
+   <path id="path4332-3" style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m274.94 116.59-9.875 1.5c7.92 51.61 19.94 109.69 19.94 162.79v71.531h10v-71.531c0-54.598-12.202-113.07-20.062-164.28z" fill="#d45500"/>
+   <path id="path4332-3-9" style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m272.94 116.59-5.875 1.5c7.92 51.61 19.94 109.69 19.94 162.79v71.531h6v-71.531c0-54.598-12.202-113.07-20.062-164.28z" fill-opacity=".40927" filter="url(#filter5470)" fill="#fca"/>
+   <path id="path4332-3-5" style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m244.75 133.78-9.5 3.125c19.28 58.02 39.75 96.67 39.75 183.98v31.531h10v-31.531c0-88.882-21.224-129.84-40.25-187.09z" fill="#a00"/>
+   <path id="path4332-3-5-8" style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m242.75 133.78-3.5 3.125c19.277 58.01 38.25 96.664 38.25 183.97v31.531h5.5v-31.531c0-88.882-21.224-129.84-40.25-187.09z" fill-opacity=".47490" filter="url(#filter5412)" fill="#faa"/>
+   <path id="path4332-3-5-7" style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m214.19 156.62-8.375 5.4688c28.45 43.57 59.18 77.47 59.18 158.79v31.531h10v-31.531c0-83.882-32.86-121.45-60.812-164.25z" fill="#784421"/>
+   <path id="path4332-3-5-7-0" style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m215.19 162.62-4.375-0.53125c28.45 43.57 57.68 77.47 57.68 158.79v31.531h4v-31.531c0-83.882-29.36-115.45-57.312-158.25z" fill-opacity=".37838" filter="url(#filter5354)" fill="#deaa87"/>
+   <path id="path4332-3-5-7-3" style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m183.16 185.47-6.3125 7.75c28.98 23.48 78.15 58.28 78.15 127.66v31.531h10v-31.531c0-74.139-53.403-112.37-81.844-135.41z" fill="#1a1a1a"/>
+   <path id="path4332-3-5-7-3-15" style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m183.16 189.47-4.3125 1.75c28.98 23.48 79.65 53.27 79.65 129.66v31.531h4v-31.531c0-71.639-50.903-108.37-79.344-131.41z" fill-opacity=".31274" filter="url(#filter5288)" fill="#fff"/>
+   <path id="path4332-3-0" style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m325.06 116.59c-7.86 51.21-20.06 109.69-20.06 164.29v71.531h10v-71.531c0-53.099 12.017-111.18 19.938-162.78l-9.875-1.5z" fill="#008000"/>
+   <path id="path4332-3-9-6" style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m272.94 112.59-5.875 1.5c7.92 51.61 19.94 109.69 19.94 162.79v71.531h6v-71.531c0-54.598-12.202-113.07-20.062-164.28z" fill-opacity=".40927" transform="matrix(-1 0 0 1 600.06 4)" filter="url(#filter5470-6)" fill="#cfa"/>
+   <path id="path4332-3-5-91" style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m355.25 133.78c-19.03 57.26-40.25 98.21-40.25 187.1v31.531h10v-31.531c0-87.304 20.473-125.96 39.75-183.97l-9.5-3.125z" fill="#04a"/>
+   <path id="path4332-3-5-8-6" style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m242.75 129.78-3.5 3.125c19.277 58.01 38.25 96.664 38.25 183.97v31.531h5.5v-31.531c0-88.882-21.224-129.84-40.25-187.09z" fill-opacity=".47490" transform="matrix(-1,0,0,1,600.25,4)" filter="url(#filter5412-5)" fill="#acf"/>
+   <path id="path4332-3-5-7-1" style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m385.81 156.62c-27.95 42.81-60.81 80.37-60.81 164.26v31.531h10v-31.531c0-81.315 30.735-115.21 59.188-158.78l-8.375-5.4688z" fill="#aa00d4"/>
+   <path id="path4332-3-5-7-0-9" style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m275.19 158.62-4.375-0.53125c28.45 43.57 57.68 77.47 57.68 158.79v31.531h4v-31.531c0-83.882-29.36-115.45-57.312-158.25z" fill-opacity=".48263" transform="matrix(-1 0 0 1 660.81 4)" filter="url(#filter5354-5)" fill="#eaf"/>
+   <path id="path4332-3-5-7-3-0" style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m416.84 185.47c-28.44 23.03-81.84 61.27-81.84 135.41v31.531h10v-31.531c0-69.374 49.168-104.18 78.156-127.66l-6.3125-7.75z" fill="#6f6f91"/>
+   <path id="path4332-3-5-7-3-15-1" style="block-progression:tb;text-indent:0;color:#000000;text-transform:none" d="m183.16 185.47-4.3125 1.75c28.98 23.48 78.15 55.27 78.15 129.65v31.531h5v-31.531c0-69.13-50.4-108.37-78.84-131.4z" fill-opacity=".42857" transform="matrix(-1 0 0 1 598.84 4)" filter="url(#filter5288-0)" fill="#dbdbe3"/>
+   <path id="rect4622-0" style="color:#000000" d="m225.64 343.5h148.71v16.777c-38.37 1.6959-81.343 2.6948-148.71 0v-16.777z" fill="url(#radialGradient4036)"/>
+   <path id="rect4622" style="color:#000000" d="m253.93 333h92.143v20.357c-23.774 2.0577-50.401 3.2698-92.143 0v-20.357z" fill="#1a1a1a"/>
+   <g id="g4720" transform="translate(0,8)">
+    <path id="rect4625" style="color:#000000" d="m300.03 0.9989c-1.1099 0.0045-2.2085 0.96259-2.3617 2.8125-3.4868 42.092-1.1379 61.943-1.3171 72.469-0.1512 8.8808-4.6309 14.242-4.3146 14.969 1.5637 3.5938 1.7792 8.6526 1.7792 11.905v17.126h12.532v-17.225c0-3.4204 0.19044-8.2459 1.6756-11.806 0.41457-0.9939-4.3147-6.0874-4.3147-14.969 0-10.271 2.7656-30.213-1.2717-72.469-0.17845-1.8678-1.2972-2.817-2.4071-2.8125l-0.00015-0.0001z" fill="#1a1a1a"/>
+    <path id="path4657" style="color:#000000" d="m303.35 38.204a2.1213 28.284 0 1 1 -4.2426 0 2.1213 28.284 0 1 1 4.2426 0z" transform="translate(-2.1213 8.4853)" fill="url(#radialGradient4038)"/>
+    <path id="rect4625-9" style="color:#000000" d="m298.41 7.3114c-3.4868 42.092 0.11212 56.693-0.0671 67.219-0.1512 8.8808-4.6309 14.242-4.3146 14.969 1.5637 3.5938 1.7792 8.6526 1.7792 11.905v17.126l10.532 1.75v-17.225c0-3.4204 0.19044-8.2459 1.6756-11.806 0.41457-0.9939-4.3147-6.0874-4.3147-14.969 0-10.271 2.7656-30.213-1.2717-72.469-0.67872-3.8375-3.0216 0.39801-4.019 3.4999z" fill="url(#radialGradient4040)"/>
+   </g>
+   <g id="g4720-2" transform="matrix(.98787 -.15526 .15526 .98787 -42.271 63.417)">
+    <path id="rect4625-0" style="color:#000000" d="m300.03 0.9989c-1.1099 0.0045-2.2085 0.96259-2.3617 2.8125-3.4868 42.092-1.1379 61.943-1.3171 72.469-0.1512 8.8808-4.6309 14.242-4.3146 14.969 1.5637 3.5938 1.7792 8.6526 1.7792 11.905v17.126h12.532v-17.225c0-3.4204 0.19044-8.2459 1.6756-11.806 0.41457-0.9939-4.3147-6.0874-4.3147-14.969 0-10.271 2.7656-30.213-1.2717-72.469-0.17845-1.8678-1.2972-2.817-2.4071-2.8125l-0.00015-0.0001z" fill="#1a1a1a"/>
+    <path id="path4657-9" style="color:#000000" d="m303.35 38.204a2.1213 28.284 0 1 1 -4.2426 0 2.1213 28.284 0 1 1 4.2426 0z" transform="translate(-2.1213 8.4853)" fill="url(#radialGradient4038)"/>
+    <path id="rect4625-9-0" style="color:#000000" d="m298.41 7.3114c-3.4868 42.092 0.11212 56.693-0.0671 67.219-0.1512 8.8808-4.6309 14.242-4.3146 14.969 1.5637 3.5938 1.7792 8.6526 1.7792 11.905v17.126l10.532 1.75v-17.225c0-3.4204 0.19044-8.2459 1.6756-11.806 0.41457-0.9939-4.3147-6.0874-4.3147-14.969 0-10.271 2.7656-30.213-1.2717-72.469-0.67872-3.8375-3.0216 0.39801-4.019 3.4999z" fill="url(#radialGradient4040)"/>
+   </g>
+   <g id="g4720-6" transform="matrix(.94451 -.32848 .32848 .94451 -79.478 131.23)">
+    <path id="rect4625-93" style="color:#000000" d="m300.03 0.9989c-1.1099 0.0045-2.2085 0.96259-2.3617 2.8125-3.4868 42.092-1.1379 61.943-1.3171 72.469-0.1512 8.8808-4.6309 14.242-4.3146 14.969 1.5637 3.5938 1.7792 8.6526 1.7792 11.905v17.126h12.532v-17.225c0-3.4204 0.19044-8.2459 1.6756-11.806 0.41457-0.9939-4.3147-6.0874-4.3147-14.969 0-10.271 2.7656-30.213-1.2717-72.469-0.17845-1.8678-1.2972-2.817-2.4071-2.8125l-0.00015-0.0001z" fill="#1a1a1a"/>
+    <path id="path4657-5" style="color:#000000" d="m303.35 38.204a2.1213 28.284 0 1 1 -4.2426 0 2.1213 28.284 0 1 1 4.2426 0z" transform="translate(-2.1213 8.4853)" fill="url(#radialGradient4038)"/>
+    <path id="rect4625-9-6" style="color:#000000" d="m298.41 7.3114c-3.4868 42.092 0.11212 56.693-0.0671 67.219-0.1512 8.8808-4.6309 14.242-4.3146 14.969 1.5637 3.5938 1.7792 8.6526 1.7792 11.905v17.126l10.532 1.75v-17.225c0-3.4204 0.19044-8.2459 1.6756-11.806 0.41457-0.9939-4.3147-6.0874-4.3147-14.969 0-10.271 2.7656-30.213-1.2717-72.469-0.67872-3.8375-3.0216 0.39801-4.019 3.4999z" fill="url(#radialGradient4040)"/>
+   </g>
+   <g id="g4720-0" transform="matrix(.84411 -.53617 .53617 .84411 -103.8 224.17)">
+    <path id="rect4625-6" style="color:#000000" d="m300.03 0.9989c-1.1099 0.0045-2.2085 0.96259-2.3617 2.8125-3.4868 42.092-1.1379 61.943-1.3171 72.469-0.1512 8.8808-4.6309 14.242-4.3146 14.969 1.5637 3.5938 1.7792 8.6526 1.7792 11.905v17.126h12.532v-17.225c0-3.4204 0.19044-8.2459 1.6756-11.806 0.41457-0.9939-4.3147-6.0874-4.3147-14.969 0-10.271 2.7656-30.213-1.2717-72.469-0.17845-1.8678-1.2972-2.817-2.4071-2.8125l-0.00015-0.0001z" fill="#1a1a1a"/>
+    <path id="path4657-2" style="color:#000000" d="m303.35 38.204a2.1213 28.284 0 1 1 -4.2426 0 2.1213 28.284 0 1 1 4.2426 0z" transform="translate(-2.1213 8.4853)" fill="url(#radialGradient4038)"/>
+    <path id="rect4625-9-1" style="color:#000000" d="m298.41 7.3114c-3.4868 42.092 0.11212 56.693-0.0671 67.219-0.1512 8.8808-4.6309 14.242-4.3146 14.969 1.5637 3.5938 1.7792 8.6526 1.7792 11.905v17.126l10.532 1.75v-17.225c0-3.4204 0.19044-8.2459 1.6756-11.806 0.41457-0.9939-4.3147-6.0874-4.3147-14.969 0-10.271 2.7656-30.213-1.2717-72.469-0.67872-3.8375-3.0216 0.39801-4.019 3.4999z" fill="url(#radialGradient4040)"/>
+   </g>
+   <g id="g4720-4" transform="matrix(.63008 -.77653 .77653 .63008 -96.161 351.53)">
+    <path id="rect4625-05" style="color:#000000" d="m300.03 0.9989c-1.1099 0.0045-2.2085 0.96259-2.3617 2.8125-3.4868 42.092-1.1379 61.943-1.3171 72.469-0.1512 8.8808-4.6309 14.242-4.3146 14.969 1.5637 3.5938 1.7792 8.6526 1.7792 11.905v17.126h12.532v-17.225c0-3.4204 0.19044-8.2459 1.6756-11.806 0.41457-0.9939-4.3147-6.0874-4.3147-14.969 0-10.271 2.7656-30.213-1.2717-72.469-0.17845-1.8678-1.2972-2.817-2.4071-2.8125l-0.00015-0.0001z" fill="#1a1a1a"/>
+    <path id="path4657-0" style="color:#000000" d="m303.35 38.204a2.1213 28.284 0 1 1 -4.2426 0 2.1213 28.284 0 1 1 4.2426 0z" transform="translate(-2.1213 8.4853)" fill="url(#radialGradient4038)"/>
+    <path id="rect4625-9-2" style="color:#000000" d="m298.41 7.3114c-3.4868 42.092 0.11212 56.693-0.0671 67.219-0.1512 8.8808-4.6309 14.242-4.3146 14.969 1.5637 3.5938 1.7792 8.6526 1.7792 11.905v17.126l10.532 1.75v-17.225c0-3.4204 0.19044-8.2459 1.6756-11.806 0.41457-0.9939-4.3147-6.0874-4.3147-14.969 0-10.271 2.7656-30.213-1.2717-72.469-0.67872-3.8375-3.0216 0.39801-4.019 3.4999z" fill="url(#radialGradient4040)"/>
+   </g>
+   <g id="g4720-2-1" transform="matrix(-.98787 -.15526 -.15526 .98787 642.52 63.417)">
+    <path id="rect4625-0-8" style="color:#000000" d="m300.03 0.9989c-1.1099 0.0045-2.2085 0.96259-2.3617 2.8125-3.4868 42.092-1.1379 61.943-1.3171 72.469-0.1512 8.8808-4.6309 14.242-4.3146 14.969 1.5637 3.5938 1.7792 8.6526 1.7792 11.905v17.126h12.532v-17.225c0-3.4204 0.19044-8.2459 1.6756-11.806 0.41457-0.9939-4.3147-6.0874-4.3147-14.969 0-10.271 2.7656-30.213-1.2717-72.469-0.17845-1.8678-1.2972-2.817-2.4071-2.8125l-0.00015-0.0001z" fill="#1a1a1a"/>
+    <path id="path4657-9-4" style="color:#000000" d="m303.35 38.204a2.1213 28.284 0 1 1 -4.2426 0 2.1213 28.284 0 1 1 4.2426 0z" transform="translate(-2.1213 8.4853)" fill="url(#radialGradient4038)"/>
+    <path id="rect4625-9-0-8" style="color:#000000" d="m298.41 7.3114c-3.4868 42.092 0.11212 56.693-0.0671 67.219-0.1512 8.8808-4.6309 14.242-4.3146 14.969 1.5637 3.5938 1.7792 8.6526 1.7792 11.905v17.126l10.532 1.75v-17.225c0-3.4204 0.19044-8.2459 1.6756-11.806 0.41457-0.9939-4.3147-6.0874-4.3147-14.969 0-10.271 2.7656-30.213-1.2717-72.469-0.67872-3.8375-3.0216 0.39801-4.019 3.4999z" fill="url(#radialGradient4040)"/>
+   </g>
+   <g id="g4720-6-1" transform="matrix(-.94451 -.32848 -.32848 .94451 679.72 131.23)">
+    <path id="rect4625-93-8" style="color:#000000" d="m300.03 0.9989c-1.1099 0.0045-2.2085 0.96259-2.3617 2.8125-3.4868 42.092-1.1379 61.943-1.3171 72.469-0.1512 8.8808-4.6309 14.242-4.3146 14.969 1.5637 3.5938 1.7792 8.6526 1.7792 11.905v17.126h12.532v-17.225c0-3.4204 0.19044-8.2459 1.6756-11.806 0.41457-0.9939-4.3147-6.0874-4.3147-14.969 0-10.271 2.7656-30.213-1.2717-72.469-0.17845-1.8678-1.2972-2.817-2.4071-2.8125l-0.00015-0.0001z" fill="#1a1a1a"/>
+    <path id="path4657-5-1" style="color:#000000" d="m303.35 38.204a2.1213 28.284 0 1 1 -4.2426 0 2.1213 28.284 0 1 1 4.2426 0z" transform="translate(-2.1213 8.4853)" fill="url(#radialGradient4038)"/>
+    <path id="rect4625-9-6-7" style="color:#000000" d="m298.41 7.3114c-3.4868 42.092 0.11212 56.693-0.0671 67.219-0.1512 8.8808-4.6309 14.242-4.3146 14.969 1.5637 3.5938 1.7792 8.6526 1.7792 11.905v17.126l10.532 1.75v-17.225c0-3.4204 0.19044-8.2459 1.6756-11.806 0.41457-0.9939-4.3147-6.0874-4.3147-14.969 0-10.271 2.7656-30.213-1.2717-72.469-0.67872-3.8375-3.0216 0.39801-4.019 3.4999z" fill="url(#radialGradient4040)"/>
+   </g>
+   <g id="g4720-0-8" transform="matrix(-.84411 -.53617 -.53617 .84411 704.05 224.17)">
+    <path id="rect4625-6-8" style="color:#000000" d="m300.03 0.9989c-1.1099 0.0045-2.2085 0.96259-2.3617 2.8125-3.4868 42.092-1.1379 61.943-1.3171 72.469-0.1512 8.8808-4.6309 14.242-4.3146 14.969 1.5637 3.5938 1.7792 8.6526 1.7792 11.905v17.126h12.532v-17.225c0-3.4204 0.19044-8.2459 1.6756-11.806 0.41457-0.9939-4.3147-6.0874-4.3147-14.969 0-10.271 2.7656-30.213-1.2717-72.469-0.17845-1.8678-1.2972-2.817-2.4071-2.8125l-0.00015-0.0001z" fill="#1a1a1a"/>
+    <path id="path4657-2-3" style="color:#000000" d="m303.35 38.204a2.1213 28.284 0 1 1 -4.2426 0 2.1213 28.284 0 1 1 4.2426 0z" transform="translate(-2.1213 8.4853)" fill="url(#radialGradient4038)"/>
+    <path id="rect4625-9-1-5" style="color:#000000" d="m298.41 7.3114c-3.4868 42.092 0.11212 56.693-0.0671 67.219-0.1512 8.8808-4.6309 14.242-4.3146 14.969 1.5637 3.5938 1.7792 8.6526 1.7792 11.905v17.126l10.532 1.75v-17.225c0-3.4204 0.19044-8.2459 1.6756-11.806 0.41457-0.9939-4.3147-6.0874-4.3147-14.969 0-10.271 2.7656-30.213-1.2717-72.469-0.67872-3.8375-3.0216 0.39801-4.019 3.4999z" fill="url(#radialGradient4040)"/>
+   </g>
+   <g id="g4720-4-5" transform="matrix(-.63008 -.77653 -.77653 .63008 696.41 351.53)">
+    <path id="rect4625-05-6" style="color:#000000" d="m300.03 0.9989c-1.1099 0.0045-2.2085 0.96259-2.3617 2.8125-3.4868 42.092-1.1379 61.943-1.3171 72.469-0.1512 8.8808-4.6309 14.242-4.3146 14.969 1.5637 3.5938 1.7792 8.6526 1.7792 11.905v17.126h12.532v-17.225c0-3.4204 0.19044-8.2459 1.6756-11.806 0.41457-0.9939-4.3147-6.0874-4.3147-14.969 0-10.271 2.7656-30.213-1.2717-72.469-0.17845-1.8678-1.2972-2.817-2.4071-2.8125l-0.00015-0.0001z" fill="#1a1a1a"/>
+    <path id="path4657-0-4" style="color:#000000" d="m303.35 38.204a2.1213 28.284 0 1 1 -4.2426 0 2.1213 28.284 0 1 1 4.2426 0z" transform="translate(-2.1213 8.4853)" fill="url(#radialGradient4038)"/>
+    <path id="rect4625-9-2-6" style="color:#000000" d="m298.41 7.3114c-3.4868 42.092 0.11212 56.693-0.0671 67.219-0.1512 8.8808-4.6309 14.242-4.3146 14.969 1.5637 3.5938 1.7792 8.6526 1.7792 11.905v17.126l10.532 1.75v-17.225c0-3.4204 0.19044-8.2459 1.6756-11.806 0.41457-0.9939-4.3147-6.0874-4.3147-14.969 0-10.271 2.7656-30.213-1.2717-72.469-0.67872-3.8375-3.0216 0.39801-4.019 3.4999z" fill="url(#radialGradient4040)"/>
+   </g>
+   <path id="rect4622-8" style="color:#000000" d="m254.32 333.77h91.362v2.5144c-23.573 1.6946-49.974 2.6928-91.362 0v-2.5144z" fill="url(#linearGradient4074)"/>
+   <path id="rect4622-8-0" style="color:#000000" d="m338.82 333.63h6.7367v19.615c-3.3472-0.0173-4.745 0.15065-6.7367 0v-19.615z" fill="url(#linearGradient4076)"/>
+   <path id="rect4622-8-0-4" style="color:#000000" d="m255.07 308.68h90.487v24.565c-23.347 1.9827-49.495 3.1506-90.487 0v-24.565z" fill="url(#linearGradient4078)"/>
+  </g>
+ </g>
+</svg>
diff --git a/icons/status-green.svg b/icons/status-green.svg
index e9ba2e8..9c76cd8 100644
--- a/icons/status-green.svg
+++ b/icons/status-green.svg
@@ -1,12 +1,24 @@
 <?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/">
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg id="svg4376" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="24" width="24" 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/">
+ <metadata id="metadata3015">
+  <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>
  <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">
+  <radialGradient id="radialGradient3051" gradientUnits="userSpaceOnUse" cy="6" cx="7.5" gradientTransform="matrix(1.0799 0 0 1.0799 .0955 4.1348)" 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"/>
+ <g id="g3047" stroke-linejoin="round" stroke-linecap="round" transform="translate(1,-1)">
+  <path id="path2996" d="m19.5 13a8.5 8.5 0 0 1 -17 0 8.5 8.5 0 1 1 17 0z" stroke="#3b7404" fill="url(#radialGradient3051)"/>
+  <path id="path3766" d="m18.5 13a7.5 7.5 0 0 1 -15 0 7.5 7.5 0 1 1 15 0z" stroke-opacity=".64338" stroke="#fff" fill="none"/>
+ </g>
 </svg>
diff --git a/icons/status-grey.svg b/icons/status-grey.svg
index 33f75d8..516c7b6 100644
--- a/icons/status-grey.svg
+++ b/icons/status-grey.svg
@@ -1,12 +1,24 @@
 <?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/">
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg id="svg4376" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="24" width="24" 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/">
+ <metadata id="metadata3027">
+  <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>
  <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">
+  <radialGradient id="radialGradient3125" gradientUnits="userSpaceOnUse" cy="6" cx="7.5" gradientTransform="matrix(1.0799 0 0 1.0799 .0955 4.1348)" 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"/>
+ <g id="g3121" stroke-linejoin="round" stroke-linecap="round" transform="translate(1,-1)">
+  <path id="path2996" d="m19.5 13a8.5 8.5 0 0 1 -17 0 8.5 8.5 0 1 1 17 0z" stroke="#555753" fill="url(#radialGradient3125)"/>
+  <path id="path3766" d="m18.5 13a7.5 7.5 0 0 1 -15 0 7.5 7.5 0 1 1 15 0z" stroke-opacity=".64338" stroke="#fff" fill="none"/>
+ </g>
 </svg>
diff --git a/icons/status-red.svg b/icons/status-red.svg
index 593b307..9a48fd5 100644
--- a/icons/status-red.svg
+++ b/icons/status-red.svg
@@ -1,12 +1,24 @@
 <?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/">
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg id="svg4376" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="24" width="24" 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/">
+ <metadata id="metadata3039">
+  <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>
  <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">
+  <radialGradient id="radialGradient3045" gradientUnits="userSpaceOnUse" cy="6" cx="7.5" gradientTransform="matrix(1.0799 0 0 1.0799 .0955 4.1348)" 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"/>
+ <g id="g3041" stroke-linejoin="round" stroke-linecap="round" transform="translate(1,-1)">
+  <path id="path2996" d="m19.5 13a8.5 8.5 0 0 1 -17 0 8.5 8.5 0 1 1 17 0z" stroke="#780000" fill="url(#radialGradient3045)"/>
+  <path id="path3766" d="m18.5 13a7.5 7.5 0 0 1 -15 0 7.5 7.5 0 1 1 15 0z" stroke-opacity=".64338" stroke="#fff" fill="none"/>
+ </g>
 </svg>
diff --git a/icons/trigger-change.svg b/icons/trigger-change.svg
index 1786d4b..be20eb4 100644
--- a/icons/trigger-change.svg
+++ b/icons/trigger-change.svg
@@ -1,76 +1,17 @@
 <?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 id="svg2989" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="24" width="24" 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" transform="translate(1 -1029.4)" stroke="#000" stroke-linecap="square" stroke-width="2" fill="none">
+  <path id="path3007" d="m4 1048.4h5l4-14h5"/>
+  <path id="path3012" d="m4 1034.4h5l4 14h5"/>
+ </g>
 </svg>
diff --git a/icons/trigger-falling.svg b/icons/trigger-falling.svg
index 24bd4f6..ed72f2c 100644
--- a/icons/trigger-falling.svg
+++ b/icons/trigger-falling.svg
@@ -1,36 +1,16 @@
 <?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 id="svg2989" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="24" width="24" 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" transform="translate(1 -1029.4)">
+  <path id="path3007" d="m18 1048.4h-5l-4-14h-5" stroke="#000" stroke-linecap="square" stroke-width="2" fill="none"/>
+ </g>
 </svg>
diff --git a/icons/trigger-high.svg b/icons/trigger-high.svg
index d2150d8..c9ef3c4 100644
--- a/icons/trigger-high.svg
+++ b/icons/trigger-high.svg
@@ -1,36 +1,16 @@
 <?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 id="svg2989" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="24" width="24" 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" transform="translate(1 -1028.4)">
+  <path id="path3007" d="m4 1033.4h14" stroke="#000" stroke-linecap="square" stroke-width="2" fill="none"/>
+ </g>
 </svg>
diff --git a/icons/trigger-low.svg b/icons/trigger-low.svg
index aa71a98..aec158e 100644
--- a/icons/trigger-low.svg
+++ b/icons/trigger-low.svg
@@ -1,36 +1,16 @@
 <?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 id="svg2989" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="24" width="24" 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" transform="translate(1.1398 -1029.2)">
+  <path id="path3007" d="m3.8602 1048.2h14" stroke="#000" stroke-linecap="square" stroke-width="2" fill="none"/>
+ </g>
 </svg>
diff --git a/icons/trigger-rising.svg b/icons/trigger-marker-change.svg
similarity index 75%
copy from icons/trigger-rising.svg
copy to icons/trigger-marker-change.svg
index ac1550f..b454c91 100644
--- a/icons/trigger-rising.svg
+++ b/icons/trigger-marker-change.svg
@@ -8,8 +8,8 @@
    xmlns:svg="http://www.w3.org/2000/svg"
    xmlns="http://www.w3.org/2000/svg"
    version="1.1"
-   width="22"
-   height="22"
+   width="16"
+   height="16"
    id="svg2989">
   <defs
      id="defs2991" />
@@ -26,11 +26,15 @@
     </rdf:RDF>
   </metadata>
   <g
-     transform="translate(0,-1030.3622)"
+     transform="translate(0,-1036.3622)"
      id="layer1">
     <path
-       d="m 4,1048.3622 5,0 4,-14 5,0"
+       d="m 2,1049.3622 4,0 4,-10 4,0"
        id="path3007"
        style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:square;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       d="m 2,1039.3622 4,0 4,10 4,0"
+       id="path3012"
+       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-marker-falling.svg
similarity index 78%
copy from icons/trigger-none.svg
copy to icons/trigger-marker-falling.svg
index 196264d..ec92877 100644
--- a/icons/trigger-none.svg
+++ b/icons/trigger-marker-falling.svg
@@ -8,8 +8,8 @@
    xmlns:svg="http://www.w3.org/2000/svg"
    xmlns="http://www.w3.org/2000/svg"
    version="1.1"
-   width="22"
-   height="22"
+   width="16"
+   height="16"
    id="svg2989">
   <defs
      id="defs2991" />
@@ -25,11 +25,8 @@
       </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" />
+  <path
+     d="m 2,3 4,0 4,10 4,0"
+     id="path3012"
+     style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:square;stroke-linejoin:miter;stroke-opacity:1" />
 </svg>
diff --git a/icons/trigger-none.svg b/icons/trigger-marker-high.svg
similarity index 78%
copy from icons/trigger-none.svg
copy to icons/trigger-marker-high.svg
index 196264d..b7a8825 100644
--- a/icons/trigger-none.svg
+++ b/icons/trigger-marker-high.svg
@@ -8,8 +8,8 @@
    xmlns:svg="http://www.w3.org/2000/svg"
    xmlns="http://www.w3.org/2000/svg"
    version="1.1"
-   width="22"
-   height="22"
+   width="16"
+   height="16"
    id="svg2989">
   <defs
      id="defs2991" />
@@ -25,11 +25,8 @@
       </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" />
+  <path
+     d="M 2,3 14,3"
+     id="path3066"
+     style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:square;stroke-linejoin:miter;stroke-opacity:1" />
 </svg>
diff --git a/icons/trigger-rising.svg b/icons/trigger-marker-low.svg
similarity index 84%
copy from icons/trigger-rising.svg
copy to icons/trigger-marker-low.svg
index ac1550f..8513561 100644
--- a/icons/trigger-rising.svg
+++ b/icons/trigger-marker-low.svg
@@ -8,8 +8,8 @@
    xmlns:svg="http://www.w3.org/2000/svg"
    xmlns="http://www.w3.org/2000/svg"
    version="1.1"
-   width="22"
-   height="22"
+   width="16"
+   height="16"
    id="svg2989">
   <defs
      id="defs2991" />
@@ -26,11 +26,12 @@
     </rdf:RDF>
   </metadata>
   <g
-     transform="translate(0,-1030.3622)"
+     transform="translate(0,-1036.3622)"
      id="layer1">
     <path
-       d="m 4,1048.3622 5,0 4,-14 5,0"
-       id="path3007"
+       d="m 2,13 12,0"
+       transform="translate(0,1036.3622)"
+       id="path3906"
        style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:square;stroke-linejoin:miter;stroke-opacity:1" />
   </g>
 </svg>
diff --git a/icons/trigger-rising.svg b/icons/trigger-marker-rising.svg
similarity index 89%
copy from icons/trigger-rising.svg
copy to icons/trigger-marker-rising.svg
index ac1550f..ddebe8d 100644
--- a/icons/trigger-rising.svg
+++ b/icons/trigger-marker-rising.svg
@@ -8,8 +8,8 @@
    xmlns:svg="http://www.w3.org/2000/svg"
    xmlns="http://www.w3.org/2000/svg"
    version="1.1"
-   width="22"
-   height="22"
+   width="16"
+   height="16"
    id="svg2989">
   <defs
      id="defs2991" />
@@ -26,10 +26,10 @@
     </rdf:RDF>
   </metadata>
   <g
-     transform="translate(0,-1030.3622)"
+     transform="translate(0,-1036.3622)"
      id="layer1">
     <path
-       d="m 4,1048.3622 5,0 4,-14 5,0"
+       d="m 2,1049.3622 4,0 4,-10 4,0"
        id="path3007"
        style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:square;stroke-linejoin:miter;stroke-opacity:1" />
   </g>
diff --git a/icons/trigger-none.svg b/icons/trigger-none.svg
index 196264d..631b681 100644
--- a/icons/trigger-none.svg
+++ b/icons/trigger-none.svg
@@ -1,35 +1,14 @@
 <?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 id="svg2989" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="24" width="24" 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>
+ <rect id="rect3033" fill-rule="evenodd" height="4" width="4" y="10" x="10"/>
 </svg>
diff --git a/icons/trigger-rising.svg b/icons/trigger-rising.svg
index ac1550f..730aa72 100644
--- a/icons/trigger-rising.svg
+++ b/icons/trigger-rising.svg
@@ -1,36 +1,16 @@
 <?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 id="svg2989" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="24" width="24" 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" transform="translate(1 -1029.4)">
+  <path id="path3007" d="m4 1048.4h5l4-14h5" stroke="#000" stroke-linecap="square" stroke-width="2" fill="none"/>
+ </g>
 </svg>
diff --git a/main.cpp b/main.cpp
index 2c6a59e..3e00479 100644
--- a/main.cpp
+++ b/main.cpp
@@ -23,19 +23,24 @@
 #endif
 
 #include <stdint.h>
-#include <libsigrok/libsigrok.h>
+#include <libsigrokcxx/libsigrokcxx.hpp>
 
 #include <getopt.h>
 
-#include <QtGui/QApplication>
 #include <QDebug>
 
 #ifdef ENABLE_SIGNALS
-#include "signalhandler.h"
+#include "signalhandler.hpp"
 #endif
 
-#include "pv/devicemanager.h"
-#include "pv/mainwindow.h"
+#include "pv/application.hpp"
+#include "pv/devicemanager.hpp"
+#include "pv/mainwindow.hpp"
+#ifdef ANDROID
+#include <libsigrokandroidutils/libsigrokandroidutils.h>
+#include "android/assetreader.hpp"
+#include "android/loghandler.hpp"
+#endif
 
 #include "config.h"
 
@@ -49,47 +54,64 @@ void usage()
 {
 	fprintf(stdout,
 		"Usage:\n"
-		"  %s [OPTION…] [FILE] — %s\n"
+		"  %s [OPTION…] — %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"
+		"Application Options:\n"
+		"  -V, --version                   Show release version\n"
+		"  -l, --loglevel                  Set libsigrok/libsigrokdecode loglevel\n"
+		"  -i, --input-file                Load input from file\n"
+		"  -I, --input-format              Input format\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;
+	std::shared_ptr<sigrok::Context> context;
+	std::string open_file, open_file_format;
 
-	QApplication a(argc, argv);
+	Application a(argc, argv);
 
-	// Set some application metadata
-	QApplication::setApplicationVersion(PV_VERSION_STRING);
-	QApplication::setApplicationName("PulseView");
-	QApplication::setOrganizationDomain("http://www.sigrok.org");
+#ifdef ANDROID
+	srau_init_environment();
+	pv::AndroidLogHandler::install_callbacks();
+	pv::AndroidAssetReader asset_reader;
+#endif
 
 	// 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'},
+			{"version", no_argument, 0, 'V'},
+			{"loglevel", required_argument, 0, 'l'},
+			{"input-file", required_argument, 0, 'i'},
+			{"input-format", required_argument, 0, 'I'},
 			{0, 0, 0, 0}
 		};
 
 		const int c = getopt_long(argc, argv,
-			"l:Vh?", long_options, NULL);
+			"l:Vh?i:I:", long_options, nullptr);
 		if (c == -1)
 			break;
 
 		switch (c) {
+		case 'h':
+		case '?':
+			usage();
+			return 0;
+
+		case 'V':
+			// Print version info
+			fprintf(stdout, "%s %s\n", PV_TITLE, PV_VERSION_STRING);
+			return 0;
+
 		case 'l':
 		{
 			const int loglevel = atoi(optarg);
-			sr_log_loglevel_set(loglevel);
+			context->set_log_level(sigrok::LogLevel::get(loglevel));
 
 #ifdef ENABLE_DECODE
 			srd_log_loglevel_set(loglevel);
@@ -98,35 +120,33 @@ int main(int argc, char *argv[])
 			break;
 		}
 
-		case 'V':
-			// Print version info
-			fprintf(stdout, "%s %s\n", PV_TITLE, PV_VERSION_STRING);
-			return 0;
+		case 'i':
+			open_file = optarg;
+			break;
 
-		case 'h':
-		case '?':
-			usage();
-			return 0;
+		case 'I':
+			open_file_format = optarg;
+			break;
 		}
 	}
 
 	if (argc - optind > 1) {
-		fprintf(stderr, "Only one file can be openened.\n");
+		fprintf(stderr, "Only one file can be opened.\n");
 		return 1;
-	} else if (argc - optind == 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;
 	}
 
+	// Initialise libsigrok
+	context = sigrok::Context::create();
+#ifdef ANDROID
+	context->set_resource_reader(&asset_reader);
+#endif
 	do {
 
 #ifdef ENABLE_DECODE
 		// Initialise libsigrokdecode
-		if (srd_init(NULL) != SRD_OK) {
+		if (srd_init(nullptr) != SRD_OK) {
 			qDebug() << "ERROR: libsigrokdecode init failed.";
 			break;
 		}
@@ -137,14 +157,15 @@ int main(int argc, char *argv[])
 
 		try {
 			// Create the device manager, initialise the drivers
-			pv::DeviceManager device_manager(sr_ctx);
+			pv::DeviceManager device_manager(context);
 
 			// Initialise the main window
-			pv::MainWindow w(device_manager, open_file);
+			pv::MainWindow w(device_manager,
+				open_file, open_file_format);
 			w.show();
 
 #ifdef ENABLE_SIGNALS
-			if(SignalHandler::prepare_signals()) {
+			if (SignalHandler::prepare_signals()) {
 				SignalHandler *const handler =
 					new SignalHandler(&w);
 				QObject::connect(handler,
@@ -153,7 +174,7 @@ int main(int argc, char *argv[])
 				QObject::connect(handler,
 					SIGNAL(term_received()),
 					&w, SLOT(close()));
-    			} else {
+			} else {
 				qWarning() <<
 					"Could not prepare signal handler.";
 			}
@@ -162,7 +183,7 @@ int main(int argc, char *argv[])
 			// Run the application
 			ret = a.exec();
 
-		} catch(std::exception e) {
+		} catch (std::exception e) {
 			qDebug() << e.what();
 		}
 
@@ -173,9 +194,5 @@ int main(int argc, char *argv[])
 
 	} while (0);
 
-	// Destroy libsigrok
-	if (sr_ctx)
-		sr_exit(sr_ctx);
-
 	return ret;
 }
diff --git a/pulseview.qrc b/pulseview.qrc
index 98edc39..05cf878 100644
--- a/pulseview.qrc
+++ b/pulseview.qrc
@@ -1,5 +1,6 @@
 <RCC>
     <qresource prefix="/" >
+	<file>icons/add-decoder.svg</file>
 	<file>icons/application-exit.png</file>
 	<file>icons/configure.png</file>
 	<file>icons/decoder-delete.svg</file>
@@ -7,15 +8,22 @@
 	<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/channels.svg</file>
+	<file>icons/menu.svg</file>
+	<file>icons/sigrok-logo-notext.svg</file>
 	<file>icons/status-green.svg</file>
 	<file>icons/status-grey.svg</file>
 	<file>icons/status-red.svg</file>
+	<file>icons/show-cursors.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-marker-change.svg</file>
+	<file>icons/trigger-marker-falling.svg</file>
+	<file>icons/trigger-marker-high.svg</file>
+	<file>icons/trigger-marker-low.svg</file>
+	<file>icons/trigger-marker-rising.svg</file>
 	<file>icons/trigger-none.svg</file>
 	<file>icons/trigger-rising.svg</file>
 	<file>icons/zoom-fit.png</file>
diff --git a/pv/data/signaldata.cpp b/pv/application.cpp
similarity index 57%
copy from pv/data/signaldata.cpp
copy to pv/application.cpp
index 04f1d3f..41f00f8 100644
--- a/pv/data/signaldata.cpp
+++ b/pv/application.cpp
@@ -1,7 +1,7 @@
 /*
  * This file is part of the PulseView project.
  *
- * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ * Copyright (C) 2014 Martin Ling <martin-sigrok at earth.li>
  *
  * 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
@@ -18,32 +18,27 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "signaldata.h"
+#include "application.hpp"
+#include "config.h"
 
-namespace pv {
-namespace data {
+#include <iostream>
 
-SignalData::SignalData() :
-	_start_time(0),
-	_samplerate(0)
+Application::Application(int &argc, char* argv[]) :
+	QApplication(argc, argv)
 {
+	setApplicationVersion(PV_VERSION_STRING);
+	setApplicationName("PulseView");
+	setOrganizationName("sigrok");
+	setOrganizationDomain("sigrok.org");
 }
 
-double SignalData::samplerate() const
+bool Application::notify(QObject *receiver, QEvent *event)
 {
-	return _samplerate;
+	try {
+		return QApplication::notify(receiver, event);
+	} catch (std::exception& e) {
+		std::cerr << "Caught exception: " << e.what() << std::endl;
+		exit(1);
+		return false;
+	}
 }
-
-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/view/marginwidget.cpp b/pv/application.hpp
similarity index 69%
copy from pv/view/marginwidget.cpp
copy to pv/application.hpp
index 539551d..8d0ebd4 100644
--- a/pv/view/marginwidget.cpp
+++ b/pv/application.hpp
@@ -1,7 +1,7 @@
 /*
  * This file is part of the PulseView project.
  *
- * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ * Copyright (C) 2014 Martin Ling <martin-sigrok at earth.li>
  *
  * 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
@@ -18,18 +18,17 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "view.h"
+#ifndef PULSEVIEW_PV_APPLICATION_HPP
+#define PULSEVIEW_PV_APPLICATION_HPP
 
-#include "marginwidget.h"
+#include <QApplication>
 
-namespace pv {
-namespace view {
-
-MarginWidget::MarginWidget(View &parent) :
-	QWidget(&parent),
-	_view(parent)
+class Application : public QApplication
 {
-}
+public:
+	Application(int &argc, char* argv[]);
+private:
+	bool notify(QObject *receiver, QEvent *event);
+};
 
-} // namespace view
-} // namespace pv
+#endif // PULSEVIEW_PV_APPLICATION_HPP
diff --git a/pv/prop/binding/binding.cpp b/pv/binding/binding.cpp
similarity index 71%
rename from pv/prop/binding/binding.cpp
rename to pv/binding/binding.cpp
index 3a6c73f..f2a28d3 100644
--- a/pv/prop/binding/binding.cpp
+++ b/pv/binding/binding.cpp
@@ -18,28 +18,27 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include <boost/foreach.hpp>
+#include <cassert>
 
 #include <QFormLayout>
 
-#include <pv/prop/property.h>
+#include <pv/prop/property.hpp>
 
-#include "binding.h"
+#include "binding.hpp"
 
-using boost::shared_ptr;
+using std::shared_ptr;
 
 namespace pv {
-namespace prop {
 namespace binding {
 
-const std::vector< boost::shared_ptr<Property> >& Binding::properties()
+const std::vector< std::shared_ptr<prop::Property> >& Binding::properties()
 {
-	return _properties;
+	return properties_;
 }
 
 void Binding::commit()
 {
-	BOOST_FOREACH(shared_ptr<pv::prop::Property> p, _properties) {
+	for (shared_ptr<pv::prop::Property> p : properties_) {
 		assert(p);
 		p->commit();
 	}
@@ -50,8 +49,7 @@ void Binding::add_properties_to_form(QFormLayout *layout,
 {
 	assert(layout);
 
-	BOOST_FOREACH(shared_ptr<pv::prop::Property> p, _properties)
-	{
+	for (shared_ptr<pv::prop::Property> p : properties_) {
 		assert(p);
 
 		QWidget *const widget = p->get_widget(layout->parentWidget(),
@@ -73,22 +71,21 @@ QWidget* Binding::get_property_form(QWidget *parent,
 	return form;
 }
 
-QString Binding::print_gvariant(GVariant *const gvar)
+QString Binding::print_gvariant(Glib::VariantBase gvar)
 {
 	QString s;
 
-	if (g_variant_is_of_type(gvar, G_VARIANT_TYPE("s")))
-		s = QString::fromUtf8(g_variant_get_string(gvar, NULL));
+	if (!gvar.gobj())
+		s = QString::fromStdString("(null)");
+	else if (gvar.is_of_type(Glib::VariantType("s")))
+		s = QString::fromStdString(
+			Glib::VariantBase::cast_dynamic<Glib::Variant<std::string>>(
+				gvar).get());
 	else
-	{
-		gchar *const text = g_variant_print(gvar, FALSE);
-		s = QString::fromUtf8(text);
-		g_free(text);
-	}
+		s = QString::fromStdString(gvar.print());
 
 	return s;
 }
 
 } // binding
-} // prop
 } // pv
diff --git a/pv/prop/binding/binding.h b/pv/binding/binding.hpp
similarity index 71%
rename from pv/prop/binding/binding.h
rename to pv/binding/binding.hpp
index 89c1acb..4854bd6 100644
--- a/pv/prop/binding/binding.h
+++ b/pv/binding/binding.hpp
@@ -18,13 +18,17 @@
  * 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
+#ifndef PULSEVIEW_PV_BINDING_BINDING_HPP
+#define PULSEVIEW_PV_BINDING_BINDING_HPP
 
 #include <glib.h>
+// Suppress warnings due to use of deprecated std::auto_ptr<> by glibmm.
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+#include <glibmm.h>
+G_GNUC_END_IGNORE_DEPRECATIONS
 
 #include <vector>
-#include <boost/shared_ptr.hpp>
+#include <memory>
 
 #include <QString>
 
@@ -32,16 +36,17 @@ class QFormLayout;
 class QWidget;
 
 namespace pv {
-namespace prop {
 
+namespace prop {
 class Property;
+}
 
 namespace binding {
 
 class Binding
 {
 public:
-	const std::vector< boost::shared_ptr<Property> >& properties();
+	const std::vector< std::shared_ptr<prop::Property> >& properties();
 
 	void commit();
 
@@ -51,14 +56,13 @@ public:
 	QWidget* get_property_form(QWidget *parent,
 		bool auto_commit = false) const;
 
-	static QString print_gvariant(GVariant *const gvar);
+	static QString print_gvariant(Glib::VariantBase gvar);
 
 protected:
-	std::vector< boost::shared_ptr<Property> > _properties;
+	std::vector< std::shared_ptr<prop::Property> > properties_;
 };
 
 } // binding
-} // prop
 } // pv
 
-#endif // PULSEVIEW_PV_PROP_BINDING_BINDING_H
+#endif // PULSEVIEW_PV_BINDING_BINDING_HPP
diff --git a/pv/prop/binding/decoderoptions.cpp b/pv/binding/decoder.cpp
similarity index 59%
rename from pv/prop/binding/decoderoptions.cpp
rename to pv/binding/decoder.cpp
index 063daf8..7d29e2f 100644
--- a/pv/prop/binding/decoderoptions.cpp
+++ b/pv/binding/decoder.cpp
@@ -20,108 +20,106 @@
 
 #include <libsigrokdecode/libsigrokdecode.h>
 
-#include "decoderoptions.h"
+#include "decoder.hpp"
 
-#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>
+#include <pv/data/decoderstack.hpp>
+#include <pv/data/decode/decoder.hpp>
+#include <pv/prop/double.hpp>
+#include <pv/prop/enum.hpp>
+#include <pv/prop/int.hpp>
+#include <pv/prop/string.hpp>
 
-using boost::bind;
 using boost::none;
-using boost::shared_ptr;
 using std::make_pair;
 using std::map;
 using std::pair;
+using std::shared_ptr;
 using std::string;
 using std::vector;
 
+using pv::prop::Double;
+using pv::prop::Enum;
+using pv::prop::Int;
+using pv::prop::Property;
+using pv::prop::String;
+
 namespace pv {
-namespace prop {
 namespace binding {
 
-DecoderOptions::DecoderOptions(
+Decoder::Decoder(
 	shared_ptr<pv::data::DecoderStack> decoder_stack,
 	shared_ptr<data::decode::Decoder> decoder) :
-	_decoder_stack(decoder_stack),
-	_decoder(decoder)
+	decoder_stack_(decoder_stack),
+	decoder_(decoder)
 {
-	assert(_decoder);
+	assert(decoder_);
 
-	const srd_decoder *const dec = _decoder->decoder();
+	const srd_decoder *const dec = decoder_->decoder();
 	assert(dec);
 
-	for (GSList *l = dec->options; l; l = l->next)
-	{
+	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);
+		const Property::Getter get = [&, opt]() {
+			return getter(opt->id); };
+		const Property::Setter set = [&, opt](Glib::VariantBase value) {
+			setter(opt->id, value); };
 
 		shared_ptr<Property> prop;
 
 		if (opt->values)
-			prop = bind_enum(name, opt, getter, setter);
+			prop = bind_enum(name, opt, get, set);
 		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));
+				none, none, get, set));
 		else if (g_variant_is_of_type(opt->def, G_VARIANT_TYPE("x")))
 			prop = shared_ptr<Property>(
-				new Int(name, "", none, getter, setter));
+				new Int(name, "", none, get, set));
 		else if (g_variant_is_of_type(opt->def, G_VARIANT_TYPE("s")))
 			prop = shared_ptr<Property>(
-				new String(name, getter, setter));
+				new String(name, get, set));
 		else
 			continue;
 
-		_properties.push_back(prop);
+		properties_.push_back(prop);
 	}
 }
 
-shared_ptr<Property> DecoderOptions::bind_enum(
+shared_ptr<Property> Decoder::bind_enum(
 	const QString &name, const srd_decoder_option *option,
 	Property::Getter getter, Property::Setter setter)
 {
-	vector< pair<GVariant*, QString> > values;
+	vector< pair<Glib::VariantBase, QString> > values;
 	for (GSList *l = option->values; l; l = l->next) {
-		GVariant *const var = (GVariant*)l->data;
-		assert(var);
+		Glib::VariantBase var = Glib::VariantBase((GVariant*)l->data, true);
 		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)
+Glib::VariantBase Decoder::getter(const char *id)
 {
-	GVariant *val = NULL;
+	GVariant *val = nullptr;
 
-	assert(_decoder);
+	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);
+	const map<string, GVariant*>& options = decoder_->options();
+	const auto iter = options.find(id);
 
 	if (iter != options.end())
 		val = (*iter).second;
-	else
-	{
-		assert(_decoder->decoder());
+	else {
+		assert(decoder_->decoder());
 
 		// Get the default value if not
-		for (GSList *l = _decoder->decoder()->options; l; l = l->next)
-		{
+		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) {
@@ -132,20 +130,19 @@ GVariant* DecoderOptions::getter(const char *id)
 	}
 
 	if (val)
-		g_variant_ref(val);
-
-	return val;
+		return Glib::VariantBase(val, true);
+	else
+		return Glib::VariantBase();
 }
 
-void DecoderOptions::setter(const char *id, GVariant *value)
+void Decoder::setter(const char *id, Glib::VariantBase value)
 {
-	assert(_decoder);
-	_decoder->set_option(id, value);
+	assert(decoder_);
+	decoder_->set_option(id, value.gobj());
 
-	assert(_decoder_stack);
-	_decoder_stack->begin_decode();
+	assert(decoder_stack_);
+	decoder_stack_->begin_decode();
 }
 
 } // binding
-} // prop
 } // pv
diff --git a/pv/prop/binding/decoderoptions.h b/pv/binding/decoder.hpp
similarity index 60%
rename from pv/prop/binding/decoderoptions.h
rename to pv/binding/decoder.hpp
index 6dec9c2..107ac15 100644
--- a/pv/prop/binding/decoderoptions.h
+++ b/pv/binding/decoder.hpp
@@ -18,12 +18,12 @@
  * 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
+#ifndef PULSEVIEW_PV_BINDING_DECODER_HPP
+#define PULSEVIEW_PV_BINDING_DECODER_HPP
 
-#include "binding.h"
+#include "binding.hpp"
 
-#include <pv/prop/property.h>
+#include <pv/prop/property.hpp>
 
 struct srd_decoder_option;
 
@@ -36,31 +36,29 @@ class Decoder;
 }
 }
 
-namespace prop {
 namespace binding {
 
-class DecoderOptions : public Binding
+class Decoder : public Binding
 {
 public:
-	DecoderOptions(boost::shared_ptr<pv::data::DecoderStack> decoder_stack,
-		boost::shared_ptr<pv::data::decode::Decoder> decoder);
+	Decoder(std::shared_ptr<pv::data::DecoderStack> decoder_stack,
+		std::shared_ptr<pv::data::decode::Decoder> decoder);
 
 private:
-	static boost::shared_ptr<Property> bind_enum(const QString &name,
+	static std::shared_ptr<prop::Property> bind_enum(const QString &name,
 		const srd_decoder_option *option,
-		Property::Getter getter, Property::Setter setter);
+		prop::Property::Getter getter, prop::Property::Setter setter);
 
-	GVariant* getter(const char *id);
+	Glib::VariantBase getter(const char *id);
 
-	void setter(const char *id, GVariant *value);
+	void setter(const char *id, Glib::VariantBase value);
 
 private:
-	boost::shared_ptr<pv::data::DecoderStack> _decoder_stack;
-	boost::shared_ptr<pv::data::decode::Decoder> _decoder;
+	std::shared_ptr<pv::data::DecoderStack> decoder_stack_;
+	std::shared_ptr<pv::data::decode::Decoder> decoder_;
 };
 
 } // binding
-} // prop
 } // pv
 
-#endif // PULSEVIEW_PV_PROP_BINDING_DECODEROPTIONS_H
+#endif // PULSEVIEW_PV_BINDING_DECODER_HPP
diff --git a/pv/binding/device.cpp b/pv/binding/device.cpp
new file mode 100644
index 0000000..026ca16
--- /dev/null
+++ b/pv/binding/device.cpp
@@ -0,0 +1,196 @@
+/*
+ * 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 <stdint.h>
+
+#include <QDebug>
+
+#include "device.hpp"
+
+#include <pv/prop/bool.hpp>
+#include <pv/prop/double.hpp>
+#include <pv/prop/enum.hpp>
+#include <pv/prop/int.hpp>
+
+#include <libsigrokcxx/libsigrokcxx.hpp>
+
+using boost::optional;
+using std::function;
+using std::make_pair;
+using std::pair;
+using std::shared_ptr;
+using std::string;
+using std::vector;
+
+using sigrok::Capability;
+using sigrok::Configurable;
+using sigrok::ConfigKey;
+using sigrok::Error;
+
+using pv::prop::Bool;
+using pv::prop::Double;
+using pv::prop::Enum;
+using pv::prop::Int;
+using pv::prop::Property;
+
+namespace pv {
+namespace binding {
+
+Device::Device(shared_ptr<sigrok::Configurable> configurable) :
+	configurable_(configurable)
+{
+
+	auto keys = configurable->config_keys();
+
+	for (auto key : keys) {
+
+		auto capabilities = configurable->config_capabilities(key);
+
+		if (!capabilities.count(Capability::GET) ||
+			!capabilities.count(Capability::SET))
+			continue;
+
+		string name_str;
+		try {
+			name_str = key->description();
+		} catch (Error e) {
+			name_str = key->name();
+		}
+
+		const QString name = QString::fromStdString(name_str);
+
+		const Property::Getter get = [&, key]() {
+			return configurable_->config_get(key); };
+		const Property::Setter set = [&, key](Glib::VariantBase value) {
+			configurable_->config_set(key, value);
+			config_changed();
+		};
+
+		switch (key->id()) {
+		case SR_CONF_SAMPLERATE:
+			// Sample rate values are not bound because they are shown
+			// in the MainBar
+			break;
+
+		case SR_CONF_CAPTURE_RATIO:
+			bind_int(name, "%", pair<int64_t, int64_t>(0, 100),
+				get, set);
+			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, capabilities, get, set);
+			break;
+
+		case SR_CONF_EXTERNAL_CLOCK:
+		case SR_CONF_RLE:
+		case SR_CONF_POWER_OFF:
+			bind_bool(name, get, set);
+			break;
+
+		case SR_CONF_TIMEBASE:
+			bind_enum(name, key, capabilities, get, set, print_timebase);
+			break;
+
+		case SR_CONF_VDIV:
+			bind_enum(name, key, capabilities, get, set, print_vdiv);
+			break;
+
+		case SR_CONF_VOLTAGE_THRESHOLD:
+			bind_enum(name, key, capabilities, get, set, print_voltage_threshold);
+			break;
+
+		case SR_CONF_PROBE_FACTOR:
+			bind_int(name, "", pair<int64_t, int64_t>(1, 500), get, set);
+			break;
+
+		default:
+			break;
+		}
+	}
+}
+
+void Device::bind_bool(const QString &name,
+	Property::Getter getter, Property::Setter setter)
+{
+	assert(configurable_);
+	properties_.push_back(shared_ptr<Property>(new Bool(
+		name, getter, setter)));
+}
+
+void Device::bind_enum(const QString &name,
+	const ConfigKey *key, std::set<const Capability *> capabilities,
+	Property::Getter getter,
+	Property::Setter setter, function<QString (Glib::VariantBase)> printer)
+{
+	Glib::VariantBase gvar;
+	vector< pair<Glib::VariantBase, QString> > values;
+
+	assert(configurable_);
+
+	if (!capabilities.count(Capability::LIST))
+		return;
+
+	Glib::VariantIter iter(configurable_->config_list(key));
+	while ((iter.next_value(gvar)))
+		values.push_back(make_pair(gvar, printer(gvar)));
+
+	properties_.push_back(shared_ptr<Property>(new Enum(name, values,
+		getter, setter)));
+}
+
+void Device::bind_int(const QString &name, QString suffix,
+	optional< std::pair<int64_t, int64_t> > range,
+	Property::Getter getter, Property::Setter setter)
+{
+	assert(configurable_);
+
+	properties_.push_back(shared_ptr<Property>(new Int(name, suffix, range,
+		getter, setter)));
+}
+
+QString Device::print_timebase(Glib::VariantBase gvar)
+{
+	uint64_t p, q;
+	g_variant_get(gvar.gobj(), "(tt)", &p, &q);
+	return QString::fromUtf8(sr_period_string(p * q));
+}
+
+QString Device::print_vdiv(Glib::VariantBase gvar)
+{
+	uint64_t p, q;
+	g_variant_get(gvar.gobj(), "(tt)", &p, &q);
+	return QString::fromUtf8(sr_voltage_string(p, q));
+}
+
+QString Device::print_voltage_threshold(Glib::VariantBase gvar)
+{
+	gdouble lo, hi;
+	g_variant_get(gvar.gobj(), "(dd)", &lo, &hi);
+	return QString("L<%1V H>%2V").arg(lo, 0, 'f', 1).arg(hi, 0, 'f', 1);
+}
+
+} // binding
+} // pv
diff --git a/pv/binding/device.hpp b/pv/binding/device.hpp
new file mode 100644
index 0000000..563132a
--- /dev/null
+++ b/pv/binding/device.hpp
@@ -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_BINDING_DEVICE_HPP
+#define PULSEVIEW_PV_BINDING_DEVICE_HPP
+
+#include <boost/optional.hpp>
+
+#include <QObject>
+#include <QString>
+
+#include "binding.hpp"
+
+#include <pv/prop/property.hpp>
+
+#include <libsigrokcxx/libsigrokcxx.hpp>
+
+namespace pv {
+
+namespace binding {
+
+class Device : public QObject, public Binding
+{
+	Q_OBJECT
+
+public:
+	Device(std::shared_ptr<sigrok::Configurable> configurable);
+
+Q_SIGNALS:
+	void config_changed();
+
+private:
+	void bind_bool(const QString &name,
+		prop::Property::Getter getter, prop::Property::Setter setter);
+	void bind_enum(const QString &name,
+		const sigrok::ConfigKey *key,
+		std::set<const sigrok::Capability *> capabilities,
+		prop::Property::Getter getter, prop::Property::Setter setter,
+		std::function<QString (Glib::VariantBase)> printer = print_gvariant);
+	void bind_int(const QString &name, QString suffix,
+		boost::optional< std::pair<int64_t, int64_t> > range,
+		prop::Property::Getter getter, prop::Property::Setter setter);
+
+	static QString print_timebase(Glib::VariantBase gvar);
+	static QString print_vdiv(Glib::VariantBase gvar);
+	static QString print_voltage_threshold(Glib::VariantBase gvar);
+
+protected:
+	std::shared_ptr<sigrok::Configurable> configurable_;
+};
+
+} // binding
+} // pv
+
+#endif // PULSEVIEW_PV_BINDING_DEVICE_HPP
diff --git a/pv/binding/inputoutput.cpp b/pv/binding/inputoutput.cpp
new file mode 100644
index 0000000..30548a8
--- /dev/null
+++ b/pv/binding/inputoutput.cpp
@@ -0,0 +1,119 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2015 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 <iostream>
+#include <utility>
+
+#include <boost/none_t.hpp>
+
+#include <libsigrokcxx/libsigrokcxx.hpp>
+
+#include <pv/prop/bool.hpp>
+#include <pv/prop/double.hpp>
+#include <pv/prop/enum.hpp>
+#include <pv/prop/int.hpp>
+#include <pv/prop/string.hpp>
+
+#include "inputoutput.hpp"
+
+using boost::none;
+
+using std::make_pair;
+using std::map;
+using std::pair;
+using std::shared_ptr;
+using std::string;
+using std::vector;
+
+using Glib::VariantBase;
+using Glib::VariantType;
+
+using sigrok::Option;
+
+using pv::prop::Bool;
+using pv::prop::Double;
+using pv::prop::Enum;
+using pv::prop::Int;
+using pv::prop::Property;
+using pv::prop::String;
+
+namespace pv {
+namespace binding {
+
+InputOutput::InputOutput(
+	const map<string, shared_ptr<Option>> &options)
+{
+	for (pair<string, shared_ptr<Option>> o : options) {
+		const shared_ptr<Option> &opt = o.second;
+		assert(opt);
+
+		const QString name = QString::fromStdString(opt->name());
+		const VariantBase def_val = opt->default_value();
+		const vector<VariantBase> values = opt->values();
+
+		options_[opt->id()] = def_val;
+ 
+		const Property::Getter get = [&, opt]() {
+			return options_[opt->id()]; };
+		const Property::Setter set = [&, opt](VariantBase value) {
+			options_[opt->id()] = value; };
+
+		shared_ptr<Property> prop;
+
+		if (!opt->values().empty())
+			prop = bind_enum(name, values, get, set);
+		else if (def_val.is_of_type(VariantType("b")))
+			prop = shared_ptr<Property>(new Bool(name, get, set));
+		else if (def_val.is_of_type(VariantType("d")))
+			prop = shared_ptr<Property>(new Double(name, 2, "",
+				none, none, get, set));
+		else if (def_val.is_of_type(VariantType("i")) ||
+			def_val.is_of_type(VariantType("t")) ||
+			def_val.is_of_type(VariantType("u")))
+			prop = shared_ptr<Property>(
+				new Int(name, "", none, get, set));
+		else if (def_val.is_of_type(VariantType("s")))
+			prop = shared_ptr<Property>(
+				new String(name, get, set));
+		else
+			continue;
+
+		properties_.push_back(prop);
+	}
+}
+
+const map<string, VariantBase>& InputOutput::options() const
+{
+	return options_;
+}
+
+shared_ptr<Property> InputOutput::bind_enum(
+	const QString &name, const vector<VariantBase> &values,
+	Property::Getter getter, Property::Setter setter)
+{
+	vector< pair<VariantBase, QString> > enum_vals;
+	for (VariantBase var : values)
+		enum_vals.push_back(make_pair(var, print_gvariant(var)));
+	return shared_ptr<Property>(new Enum(name, enum_vals, getter, setter));
+}
+
+} // namespace binding
+} // namespace pv
diff --git a/pv/binding/inputoutput.hpp b/pv/binding/inputoutput.hpp
new file mode 100644
index 0000000..628b1c4
--- /dev/null
+++ b/pv/binding/inputoutput.hpp
@@ -0,0 +1,81 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2015 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_BINDING_INPUTOUTPUT_HPP
+#define PULSEVIEW_PV_BINDING_INPUTOUTPUT_HPP
+
+#include <map>
+#include <memory>
+#include <string>
+
+#include "binding.hpp"
+
+#include <pv/prop/property.hpp>
+
+namespace sigrok {
+class Option;
+}
+
+namespace pv {
+namespace binding {
+
+/**
+ * A binding of glibmm variants for sigrok input and output options.
+ */
+class InputOutput : public Binding
+{
+public:
+	/**
+	 * Constructs a new @c InputOutput binding.
+	 * @param options the map of options to use as a template.
+	 */
+	InputOutput(
+		const std::map<std::string, std::shared_ptr<sigrok::Option>>
+			&options);
+
+	/**
+	 * Gets the map of selected options.
+	 * @return the options.
+	 */
+	const std::map<std::string, Glib::VariantBase>& options() const;
+
+private:
+	/**
+	 * A helper function to bind an option list to and enum property.
+	 * @param name the name of the property.
+	 * @param values the list of values.
+	 * @param getter the getter that will read the values out of the map.
+	 * @param setter the setter that will set the values into the map.
+	 */
+	std::shared_ptr<prop::Property> bind_enum(const QString &name,
+		const std::vector<Glib::VariantBase> &values,
+		prop::Property::Getter getter, prop::Property::Setter setter);
+
+private:
+	/**
+	 * The current map of options.
+	 */
+	std::map<std::string, Glib::VariantBase> options_;
+};
+
+} // binding
+} // pv
+
+#endif // PULSEVIEW_PV_BINDING_INPUTOUTPUT_H
diff --git a/pv/data/analog.cpp b/pv/data/analog.cpp
index 21ddb5f..ca2659b 100644
--- a/pv/data/analog.cpp
+++ b/pv/data/analog.cpp
@@ -18,14 +18,15 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include <boost/foreach.hpp>
+#include <cassert>
 
-#include "analog.h"
-#include "analogsnapshot.h"
+#include "analog.hpp"
+#include "analogsegment.hpp"
 
-using boost::shared_ptr;
 using std::deque;
 using std::max;
+using std::shared_ptr;
+using std::vector;
 
 namespace pv {
 namespace data {
@@ -35,25 +36,31 @@ Analog::Analog() :
 {
 }
 
-void Analog::push_snapshot(shared_ptr<AnalogSnapshot> &snapshot)
+void Analog::push_segment(shared_ptr<AnalogSegment> &segment)
 {
-	_snapshots.push_front(snapshot);
+	segments_.push_front(segment);
 }
 
-deque< shared_ptr<AnalogSnapshot> >& Analog::get_snapshots()
+const deque< shared_ptr<AnalogSegment> >& Analog::analog_segments() const
 {
-	return _snapshots;
+	return segments_;
+}
+
+vector< shared_ptr<Segment> > Analog::segments() const
+{
+	return vector< shared_ptr<Segment> >(
+		segments_.begin(), segments_.end());
 }
 
 void Analog::clear()
 {
-	_snapshots.clear();
+	segments_.clear();
 }
 
-uint64_t Analog::get_max_sample_count() const
+uint64_t Analog::max_sample_count() const
 {
 	uint64_t l = 0;
-	BOOST_FOREACH(const boost::shared_ptr<AnalogSnapshot> s, _snapshots) {
+	for (const std::shared_ptr<AnalogSegment> s : segments_) {
 		assert(s);
 		l = max(l, s->get_sample_count());
 	}
diff --git a/pv/data/analog.h b/pv/data/analog.hpp
similarity index 68%
rename from pv/data/analog.h
rename to pv/data/analog.hpp
index 42e3167..58f5f3f 100644
--- a/pv/data/analog.h
+++ b/pv/data/analog.hpp
@@ -18,39 +18,41 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_DATA_ANALOG_H
-#define PULSEVIEW_PV_DATA_ANALOG_H
+#ifndef PULSEVIEW_PV_DATA_ANALOG_HPP
+#define PULSEVIEW_PV_DATA_ANALOG_HPP
 
-#include "signaldata.h"
+#include "signaldata.hpp"
 
-#include <boost/shared_ptr.hpp>
 #include <deque>
+#include <memory>
 
 namespace pv {
 namespace data {
 
-class AnalogSnapshot;
+class AnalogSegment;
 
 class Analog : public SignalData
 {
 public:
 	Analog();
 
-	void push_snapshot(
-		boost::shared_ptr<AnalogSnapshot> &snapshot);
+	void push_segment(
+		std::shared_ptr<AnalogSegment> &segment);
 
-	std::deque< boost::shared_ptr<AnalogSnapshot> >&
-		get_snapshots();
+	const std::deque< std::shared_ptr<AnalogSegment> >&
+		analog_segments() const;
+
+	std::vector< std::shared_ptr<Segment> > segments() const;
 
 	void clear();
 
-	uint64_t get_max_sample_count() const;
+	uint64_t max_sample_count() const;
 
 private:
-	std::deque< boost::shared_ptr<AnalogSnapshot> > _snapshots;
+	std::deque< std::shared_ptr<AnalogSegment> > segments_;
 };
 
 } // namespace data
 } // namespace pv
 
-#endif // PULSEVIEW_PV_DATA_ANALOG_H
+#endif // PULSEVIEW_PV_DATA_ANALOG_HPP
diff --git a/pv/data/analogsnapshot.cpp b/pv/data/analogsegment.cpp
similarity index 64%
rename from pv/data/analogsnapshot.cpp
rename to pv/data/analogsegment.cpp
index 4d4f5d6..cd4384d 100644
--- a/pv/data/analogsnapshot.cpp
+++ b/pv/data/analogsegment.cpp
@@ -23,16 +23,14 @@
 #include <assert.h>
 #include <string.h>
 #include <stdlib.h>
-#include <math.h>
+#include <cmath>
 
 #include <algorithm>
 
-#include <boost/foreach.hpp>
+#include "analogsegment.hpp"
 
-#include "analogsnapshot.h"
-
-using boost::lock_guard;
-using boost::recursive_mutex;
+using std::lock_guard;
+using std::recursive_mutex;
 using std::max;
 using std::max_element;
 using std::min;
@@ -41,76 +39,77 @@ using std::min_element;
 namespace pv {
 namespace data {
 
-const int AnalogSnapshot::EnvelopeScalePower = 4;
-const int AnalogSnapshot::EnvelopeScaleFactor = 1 << EnvelopeScalePower;
-const float AnalogSnapshot::LogEnvelopeScaleFactor =
+const int AnalogSegment::EnvelopeScalePower = 4;
+const int AnalogSegment::EnvelopeScaleFactor = 1 << EnvelopeScalePower;
+const float AnalogSegment::LogEnvelopeScaleFactor =
 	logf(EnvelopeScaleFactor);
-const uint64_t AnalogSnapshot::EnvelopeDataUnit = 64*1024;	// bytes
+const uint64_t AnalogSegment::EnvelopeDataUnit = 64*1024;	// bytes
 
-AnalogSnapshot::AnalogSnapshot(const uint64_t expected_num_samples) :
-	Snapshot(sizeof(float))
+AnalogSegment::AnalogSegment(
+	uint64_t samplerate, const uint64_t expected_num_samples) :
+	Segment(samplerate, sizeof(float))
 {
 	set_capacity(expected_num_samples);
 
-	lock_guard<recursive_mutex> lock(_mutex);
-	memset(_envelope_levels, 0, sizeof(_envelope_levels));
+	lock_guard<recursive_mutex> lock(mutex_);
+	memset(envelope_levels_, 0, sizeof(envelope_levels_));
 }
 
-AnalogSnapshot::~AnalogSnapshot()
+AnalogSegment::~AnalogSegment()
 {
-	lock_guard<recursive_mutex> lock(_mutex);
-	BOOST_FOREACH(Envelope &e, _envelope_levels)
+	lock_guard<recursive_mutex> lock(mutex_);
+	for (Envelope &e : envelope_levels_)
 		free(e.samples);
 }
 
-void AnalogSnapshot::append_interleaved_samples(const float *data,
+void AnalogSegment::append_interleaved_samples(const float *data,
 	size_t sample_count, size_t stride)
 {
-	assert(_unit_size == sizeof(float));
+	assert(unit_size_ == sizeof(float));
 
-	lock_guard<recursive_mutex> lock(_mutex);
+	lock_guard<recursive_mutex> lock(mutex_);
 
-	_data = realloc(_data, (_sample_count + sample_count) * sizeof(float));
+	// If we're out of memory, this will throw std::bad_alloc
+	data_.resize((sample_count_ + sample_count) * sizeof(float));
 
-	float *dst = (float*)_data + _sample_count;
+	float *dst = (float*)data_.data() + sample_count_;
 	const float *dst_end = dst + sample_count;
-	while (dst != dst_end)
-	{
+	while (dst != dst_end) {
 		*dst++ = *data;
 		data += stride;
 	}
 
-	_sample_count += sample_count;
+	sample_count_ += sample_count;
 
 	// Generate the first mip-map from the data
 	append_payload_to_envelope_levels();
 }
 
-const float* AnalogSnapshot::get_samples(
+const float* AnalogSegment::get_samples(
 	int64_t start_sample, int64_t end_sample) const
 {
 	assert(start_sample >= 0);
-	assert(start_sample < (int64_t)_sample_count);
+	assert(start_sample < (int64_t)sample_count_);
 	assert(end_sample >= 0);
-	assert(end_sample < (int64_t)_sample_count);
+	assert(end_sample < (int64_t)sample_count_);
 	assert(start_sample <= end_sample);
 
-	lock_guard<recursive_mutex> lock(_mutex);
+	lock_guard<recursive_mutex> lock(mutex_);
 
 	float *const data = new float[end_sample - start_sample];
-	memcpy(data, (float*)_data + start_sample, sizeof(float) *
+	memcpy(data, (float*)data_.data() + start_sample, sizeof(float) *
 		(end_sample - start_sample));
 	return data;
 }
 
-void AnalogSnapshot::get_envelope_section(EnvelopeSection &s,
+void AnalogSegment::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);
+	lock_guard<recursive_mutex> lock(mutex_);
 
 	const unsigned int min_level = max((int)floorf(logf(min_length) /
 		LogEnvelopeScaleFactor) - 1, 0);
@@ -123,31 +122,30 @@ void AnalogSnapshot::get_envelope_section(EnvelopeSection &s,
 	s.scale = 1 << scale_power;
 	s.length = end - start;
 	s.samples = new EnvelopeSample[s.length];
-	memcpy(s.samples, _envelope_levels[min_level].samples + start,
+	memcpy(s.samples, envelope_levels_[min_level].samples + start,
 		s.length * sizeof(EnvelopeSample));
 }
 
-void AnalogSnapshot::reallocate_envelope(Envelope &e)
+void AnalogSegment::reallocate_envelope(Envelope &e)
 {
 	const uint64_t new_data_length = ((e.length + EnvelopeDataUnit - 1) /
 		EnvelopeDataUnit) * EnvelopeDataUnit;
-	if (new_data_length > e.data_length)
-	{
+	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()
+void AnalogSegment::append_payload_to_envelope_levels()
 {
-	Envelope &e0 = _envelope_levels[0];
+	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;
+	e0.length = sample_count_ / EnvelopeScaleFactor;
 
 	// Break off if there are no new samples to compute
 	if (e0.length == prev_length)
@@ -158,12 +156,11 @@ void AnalogSnapshot::append_payload_to_envelope_levels()
 	dest_ptr = e0.samples + prev_length;
 
 	// Iterate through the samples to populate the first level mipmap
-	const float *const end_src_ptr = (float*)_data +
+	const float *const end_src_ptr = (float*)data_.data() +
 		e0.length * EnvelopeScaleFactor;
-	for (const float *src_ptr = (float*)_data +
-		prev_length * EnvelopeScaleFactor;
-		src_ptr < end_src_ptr; src_ptr += EnvelopeScaleFactor)
-	{
+	for (const float *src_ptr = (float*)data_.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),
@@ -173,10 +170,9 @@ void AnalogSnapshot::append_payload_to_envelope_levels()
 	}
 
 	// Compute higher level mipmaps
-	for (unsigned int level = 1; level < ScaleStepCount; level++)
-	{
-		Envelope &e = _envelope_levels[level];
-		const Envelope &el = _envelope_levels[level-1];
+	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;
@@ -193,14 +189,12 @@ void AnalogSnapshot::append_payload_to_envelope_levels()
 			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++)
-		{
+				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)
-			{
+			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++;
diff --git a/pv/data/analogsnapshot.h b/pv/data/analogsegment.hpp
similarity index 81%
rename from pv/data/analogsnapshot.h
rename to pv/data/analogsegment.hpp
index b60c2ce..1a91b8b 100644
--- a/pv/data/analogsnapshot.h
+++ b/pv/data/analogsegment.hpp
@@ -18,22 +18,22 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_DATA_ANALOGSNAPSHOT_H
-#define PULSEVIEW_PV_DATA_ANALOGSNAPSHOT_H
+#ifndef PULSEVIEW_PV_DATA_ANALOGSEGMENT_HPP
+#define PULSEVIEW_PV_DATA_ANALOGSEGMENT_HPP
 
-#include "snapshot.h"
+#include "segment.hpp"
 
 #include <utility>
 #include <vector>
 
-namespace AnalogSnapshotTest {
-class Basic;
+namespace AnalogSegmentTest {
+struct Basic;
 }
 
 namespace pv {
 namespace data {
 
-class AnalogSnapshot : public Snapshot
+class AnalogSegment : public Segment
 {
 public:
 	struct EnvelopeSample
@@ -66,9 +66,9 @@ private:
 	static const uint64_t EnvelopeDataUnit;
 
 public:
-	AnalogSnapshot(uint64_t expected_num_samples = 0);
+	AnalogSegment(uint64_t samplerate, uint64_t expected_num_samples = 0);
 
-	virtual ~AnalogSnapshot();
+	virtual ~AnalogSegment();
 
 	void append_interleaved_samples(const float *data,
 		size_t sample_count, size_t stride);
@@ -85,12 +85,12 @@ private:
 	void append_payload_to_envelope_levels();
 
 private:
-	struct Envelope _envelope_levels[ScaleStepCount];
+	struct Envelope envelope_levels_[ScaleStepCount];
 
-	friend class AnalogSnapshotTest::Basic;
+	friend struct AnalogSegmentTest::Basic;
 };
 
 } // namespace data
 } // namespace pv
 
-#endif // PULSEVIEW_PV_DATA_ANALOGSNAPSHOT_H
+#endif // PULSEVIEW_PV_DATA_ANALOGSEGMENT_HPP
diff --git a/pv/data/decode/annotation.cpp b/pv/data/decode/annotation.cpp
index b0517ae..9a16580 100644
--- a/pv/data/decode/annotation.cpp
+++ b/pv/data/decode/annotation.cpp
@@ -22,50 +22,51 @@ extern "C" {
 #include <libsigrokdecode/libsigrokdecode.h>
 }
 
+#include <cassert>
 #include <vector>
 
-#include "annotation.h"
+#include "annotation.hpp"
 
 namespace pv {
 namespace data {
 namespace decode {
 
 Annotation::Annotation(const srd_proto_data *const pdata) :
-	_start_sample(pdata->start_sample),
-	_end_sample(pdata->end_sample)
+	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;
+	format_ = pda->ann_class;
 
 	const char *const *annotations = (char**)pda->ann_text;
-	while(*annotations) {
-		_annotations.push_back(QString::fromUtf8(*annotations));
+	while (*annotations) {
+		annotations_.push_back(QString::fromUtf8(*annotations));
 		annotations++;
 	}
 }
 
 uint64_t Annotation::start_sample() const
 {
-	return _start_sample;
+	return start_sample_;
 }
 
 uint64_t Annotation::end_sample() const
 {
-	return _end_sample;
+	return end_sample_;
 }
 
 int Annotation::format() const
 {
-	return _format;
+	return format_;
 }
 
 const std::vector<QString>& Annotation::annotations() const
 {
-	return _annotations;
+	return annotations_;
 }
 
 } // namespace decode
diff --git a/pv/data/decode/annotation.h b/pv/data/decode/annotation.hpp
similarity index 83%
rename from pv/data/decode/annotation.h
rename to pv/data/decode/annotation.hpp
index 0d7fd5d..071f292 100644
--- a/pv/data/decode/annotation.h
+++ b/pv/data/decode/annotation.hpp
@@ -18,8 +18,8 @@
  * 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
+#ifndef PULSEVIEW_PV_VIEW_DECODE_ANNOTATION_HPP
+#define PULSEVIEW_PV_VIEW_DECODE_ANNOTATION_HPP
 
 #include <stdint.h>
 
@@ -42,14 +42,14 @@ public:
 	const std::vector<QString>& annotations() const;
 
 private:
-	uint64_t _start_sample;
-	uint64_t _end_sample;
-	int _format;
-	std::vector<QString> _annotations; 
+	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
+#endif // PULSEVIEW_PV_VIEW_DECODE_ANNOTATION_HPP
diff --git a/pv/data/decode/decoder.cpp b/pv/data/decode/decoder.cpp
index 44e474b..03f4f0e 100644
--- a/pv/data/decode/decoder.cpp
+++ b/pv/data/decode/decoder.cpp
@@ -18,16 +18,18 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include <libsigrok/libsigrok.h>
+#include <cassert>
+
+#include <libsigrokcxx/libsigrokcxx.hpp>
 #include <libsigrokdecode/libsigrokdecode.h>
 
-#include "decoder.h"
+#include "decoder.hpp"
 
-#include <pv/view/logicsignal.h>
+#include <pv/view/logicsignal.hpp>
 
-using boost::shared_ptr;
 using std::set;
 using std::map;
+using std::shared_ptr;
 using std::string;
 
 namespace pv {
@@ -35,63 +37,62 @@ namespace data {
 namespace decode {
 
 Decoder::Decoder(const srd_decoder *const dec) :
-	_decoder(dec),
-	_shown(true)
+	decoder_(dec),
+	shown_(true)
 {
 }
 
 Decoder::~Decoder()
 {
-	for (map<string, GVariant*>::const_iterator i = _options.begin();
-		i != _options.end(); i++)
+	for (auto i = options_.begin(); i != options_.end(); i++)
 		g_variant_unref((*i).second);
 }
 
 const srd_decoder* Decoder::decoder() const
 {
-	return _decoder;
+	return decoder_;
 }
 
 bool Decoder::shown() const
 {
-	return _shown;
+	return shown_;
 }
 
 void Decoder::show(bool show)
 {
-	_shown = show;
+	shown_ = show;
 }
 
 const map<const srd_channel*, shared_ptr<view::LogicSignal> >&
 Decoder::channels() const
 {
-	return _probes;
+	return channels_;
 }
 
-void Decoder::set_probes(std::map<const srd_channel*,
-	boost::shared_ptr<view::LogicSignal> > probes)
+void Decoder::set_channels(std::map<const srd_channel*,
+	std::shared_ptr<view::LogicSignal> > channels)
 {
-	_probes = probes;
+	channels_ = channels;
 }
 
 const std::map<std::string, GVariant*>& Decoder::options() const
 {
-	return _options;
+	return options_;
 }
 
 void Decoder::set_option(const char *id, GVariant *value)
 {
 	assert(value);
 	g_variant_ref(value);
-	_options[id] = value;
+	options_[id] = value;
 }
 
-bool Decoder::have_required_probes() const
+bool Decoder::have_required_channels() const
 {
-	for (GSList *l = _decoder->channels; l; l = l->next) {
+	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())
+		if (channels_.find(pdch) == channels_.end())
 			return false;
 	}
 
@@ -101,10 +102,7 @@ bool Decoder::have_required_probes() const
 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++)
-	{
+	for (auto i = channels_.cbegin(); i != channels_.cend(); i++) {
 		shared_ptr<view::LogicSignal> signal((*i).second);
 		assert(signal);
 		data.insert(signal->logic_data());
@@ -113,13 +111,12 @@ set< shared_ptr<pv::data::Logic> > Decoder::get_data()
 	return data;
 }
 
-srd_decoder_inst* Decoder::create_decoder_inst(srd_session *session, int unit_size) const
+srd_decoder_inst* Decoder::create_decoder_inst(srd_session *session) 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++)
+	for (auto i = options_.cbegin(); i != options_.cend(); i++)
 	{
 		GVariant *const value = (*i).second;
 		g_variant_ref(value);
@@ -128,28 +125,26 @@ srd_decoder_inst* Decoder::create_decoder_inst(srd_session *session, int unit_si
 	}
 
 	srd_decoder_inst *const decoder_inst = srd_inst_new(
-		session, _decoder->id, opt_hash);
+		session, decoder_->id, opt_hash);
 	g_hash_table_destroy(opt_hash);
 
-	if(!decoder_inst)
-		return NULL;
+	if (!decoder_inst)
+		return nullptr;
 
-	// Setup the probes
-	GHashTable *const probes = g_hash_table_new_full(g_str_hash,
+	// Setup the channels
+	GHashTable *const channels = 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++)
+	for (auto i = channels_.cbegin(); i != channels_.cend(); i++)
 	{
 		shared_ptr<view::LogicSignal> signal((*i).second);
 		GVariant *const gvar = g_variant_new_int32(
-			signal->probe()->index);
+			signal->channel()->index());
 		g_variant_ref_sink(gvar);
-		g_hash_table_insert(probes, (*i).first->id, gvar);
+		g_hash_table_insert(channels, (*i).first->id, gvar);
 	}
 
-	srd_inst_channel_set_all(decoder_inst, probes, unit_size);
+	srd_inst_channel_set_all(decoder_inst, channels);
 
 	return decoder_inst;
 }
diff --git a/pv/data/decode/decoder.h b/pv/data/decode/decoder.hpp
similarity index 70%
rename from pv/data/decode/decoder.h
rename to pv/data/decode/decoder.hpp
index dffefab..121286f 100644
--- a/pv/data/decode/decoder.h
+++ b/pv/data/decode/decoder.hpp
@@ -18,14 +18,13 @@
  * 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
+#ifndef PULSEVIEW_PV_DATA_DECODE_DECODER_HPP
+#define PULSEVIEW_PV_DATA_DECODE_DECODER_HPP
 
 #include <map>
+#include <memory>
 #include <set>
 
-#include <boost/shared_ptr.hpp>
-
 #include <glib.h>
 
 struct srd_decoder;
@@ -58,33 +57,33 @@ public:
 	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);
+		std::shared_ptr<view::LogicSignal> >& channels() const;
+	void set_channels(std::map<const srd_channel*,
+		std::shared_ptr<view::LogicSignal> > channels);
 
 	const std::map<std::string, GVariant*>& options() const;
 
 	void set_option(const char *id, GVariant *value);
 
-	bool have_required_probes() const;
+	bool have_required_channels() const;
 
 	srd_decoder_inst* create_decoder_inst(
-		srd_session *session, int unit_size) const;
+		srd_session *session) const;
 
-	std::set< boost::shared_ptr<pv::data::Logic> > get_data();	
+	std::set< std::shared_ptr<pv::data::Logic> > get_data();
 
 private:
-	const srd_decoder *const _decoder;
+	const srd_decoder *const decoder_;
 
-	bool _shown;
+	bool shown_;
 
-	std::map<const srd_channel*, boost::shared_ptr<pv::view::LogicSignal> >
-		_probes;
-	std::map<std::string, GVariant*> _options;
+	std::map<const srd_channel*, std::shared_ptr<pv::view::LogicSignal> >
+		channels_;
+	std::map<std::string, GVariant*> options_;
 };
 
 } // namespace decode
 } // namespace data
 } // namespace pv
 
-#endif // PULSEVIEW_PV_DATA_DECODE_DECODER_H
+#endif // PULSEVIEW_PV_DATA_DECODE_DECODER_HPP
diff --git a/pv/data/decode/row.cpp b/pv/data/decode/row.cpp
index 2aabf0f..70a0609 100644
--- a/pv/data/decode/row.cpp
+++ b/pv/data/decode/row.cpp
@@ -18,7 +18,7 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "row.h"
+#include "row.hpp"
 
 #include <libsigrokdecode/libsigrokdecode.h>
 
@@ -27,44 +27,44 @@ namespace data {
 namespace decode {
 
 Row::Row() :
-	_decoder(NULL),
-	_row(NULL)
+	decoder_(nullptr),
+	row_(nullptr)
 {
 }
 
 Row::Row(const srd_decoder *decoder, const srd_decoder_annotation_row *row) :
-	_decoder(decoder),
-	_row(row)
+	decoder_(decoder),
+	row_(row)
 {
 }
 
 const srd_decoder* Row::decoder() const
 {
-	return _decoder;
+	return decoder_;
 }
 
 const srd_decoder_annotation_row* Row::row() const
 {
-	return _row;
+	return row_;
 }
 
 const QString Row::title() const
 {
-	if (_decoder && _decoder->name && _row && _row->desc)
+	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);
+			.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);
+	return (decoder_ < other.decoder_) ||
+		(decoder_ == other.decoder_ && row_ < other.row_);
 }
 
 } // decode
diff --git a/pv/data/decode/row.h b/pv/data/decode/row.hpp
similarity index 81%
rename from pv/data/decode/row.h
rename to pv/data/decode/row.hpp
index 6c05ac4..9c81b87 100644
--- a/pv/data/decode/row.h
+++ b/pv/data/decode/row.hpp
@@ -18,12 +18,12 @@
  * 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
+#ifndef PULSEVIEW_PV_DATA_DECODE_ROW_HPP
+#define PULSEVIEW_PV_DATA_DECODE_ROW_HPP
 
 #include <vector>
 
-#include "annotation.h"
+#include "annotation.hpp"
 
 struct srd_decoder;
 struct srd_decoder_annotation_row;
@@ -38,7 +38,7 @@ public:
 	Row();
 
 	Row(const srd_decoder *decoder,
-		const srd_decoder_annotation_row *row = NULL);
+		const srd_decoder_annotation_row *row = nullptr);
 
 	const srd_decoder* decoder() const;
 	const srd_decoder_annotation_row* row() const;
@@ -48,12 +48,12 @@ public:
 	bool operator<(const Row &other) const;
 
 private:
-	const srd_decoder *_decoder;
-	const srd_decoder_annotation_row *_row;
+	const srd_decoder *decoder_;
+	const srd_decoder_annotation_row *row_;
 };
 
 } // decode
 } // data
 } // pv
 
-#endif // PULSEVIEW_PV_DATA_DECODE_ROW_H
+#endif // PULSEVIEW_PV_DATA_DECODE_ROW_HPP
diff --git a/pv/data/decode/rowdata.cpp b/pv/data/decode/rowdata.cpp
index 87da27c..9402e70 100644
--- a/pv/data/decode/rowdata.cpp
+++ b/pv/data/decode/rowdata.cpp
@@ -18,7 +18,7 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "rowdata.h"
+#include "rowdata.hpp"
 
 using std::vector;
 
@@ -32,17 +32,16 @@ RowData::RowData()
 
 uint64_t RowData::get_max_sample() const
 {
-	if (_annotations.empty())
+	if (annotations_.empty())
 		return 0;
-	return _annotations.back().end_sample();
+	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++)
+	for (auto i = annotations_.cbegin(); i != annotations_.cend(); i++)
 		if ((*i).end_sample() > start_sample &&
 			(*i).start_sample() <= end_sample)
 			dest.push_back(*i);
@@ -50,7 +49,7 @@ void RowData::get_annotation_subset(
 
 void RowData::push_annotation(const Annotation &a)
 {
-	_annotations.push_back(a);
+	annotations_.push_back(a);
 }
 
 } // decode
diff --git a/pv/data/decode/rowdata.h b/pv/data/decode/rowdata.hpp
similarity index 86%
rename from pv/data/decode/rowdata.h
rename to pv/data/decode/rowdata.hpp
index 01c65b6..820959d 100644
--- a/pv/data/decode/rowdata.h
+++ b/pv/data/decode/rowdata.hpp
@@ -18,12 +18,12 @@
  * 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
+#ifndef PULSEVIEW_PV_DATA_DECODE_ROWDATA_HPP
+#define PULSEVIEW_PV_DATA_DECODE_ROWDATA_HPP
 
 #include <vector>
 
-#include "annotation.h"
+#include "annotation.hpp"
 
 namespace pv {
 namespace data {
@@ -47,11 +47,11 @@ public:
 	void push_annotation(const Annotation &a);
 
 private:
-	std::vector<Annotation> _annotations;
+	std::vector<Annotation> annotations_;
 };
 
 }
 } // data
 } // pv
 
-#endif // PULSEVIEW_PV_DATA_DECODE_ROWDATA_H
+#endif // PULSEVIEW_PV_DATA_DECODE_ROWDATA_HPP
diff --git a/pv/data/decoderstack.cpp b/pv/data/decoderstack.cpp
index c3bf26d..2668df2 100644
--- a/pv/data/decoderstack.cpp
+++ b/pv/data/decoderstack.cpp
@@ -20,27 +20,23 @@
 
 #include <libsigrokdecode/libsigrokdecode.h>
 
-#include <boost/foreach.hpp>
-#include <boost/thread/thread.hpp>
-
 #include <stdexcept>
 
 #include <QDebug>
 
-#include "decoderstack.h"
+#include "decoderstack.hpp"
 
-#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>
+#include <pv/data/logic.hpp>
+#include <pv/data/logicsegment.hpp>
+#include <pv/data/decode/decoder.hpp>
+#include <pv/data/decode/annotation.hpp>
+#include <pv/session.hpp>
+#include <pv/view/logicsignal.hpp>
 
-using boost::lock_guard;
-using boost::mutex;
+using std::lock_guard;
+using std::mutex;
 using boost::optional;
-using boost::shared_ptr;
-using boost::unique_lock;
+using std::unique_lock;
 using std::deque;
 using std::make_pair;
 using std::max;
@@ -48,6 +44,7 @@ using std::min;
 using std::list;
 using std::map;
 using std::pair;
+using std::shared_ptr;
 using std::vector;
 
 using namespace pv::data::decode;
@@ -60,74 +57,86 @@ const double DecoderStack::DecodeThreshold = 0.2;
 const int64_t DecoderStack::DecodeChunkLength = 4096;
 const unsigned int DecoderStack::DecodeNotifyPeriod = 65536;
 
-mutex DecoderStack::_global_decode_mutex;
+mutex DecoderStack::global_decode_mutex_;
 
-DecoderStack::DecoderStack(pv::SigSession &session,
+DecoderStack::DecoderStack(pv::Session &session,
 	const srd_decoder *const dec) :
-	_session(session),
-	_sample_count(0),
-	_frame_complete(false),
-	_samples_decoded(0)
+	session_(session),
+	start_time_(0),
+	samplerate_(0),
+	sample_count_(0),
+	frame_complete_(false),
+	samples_decoded_(0)
 {
-	connect(&_session, SIGNAL(frame_began()),
+	connect(&session_, SIGNAL(frame_began()),
 		this, SLOT(on_new_frame()));
-	connect(&_session, SIGNAL(data_received()),
+	connect(&session_, SIGNAL(data_received()),
 		this, SLOT(on_data_received()));
-	connect(&_session, SIGNAL(frame_ended()),
+	connect(&session_, SIGNAL(frame_ended()),
 		this, SLOT(on_frame_ended()));
 
-	_stack.push_back(shared_ptr<decode::Decoder>(
+	stack_.push_back(shared_ptr<decode::Decoder>(
 		new decode::Decoder(dec)));
 }
 
 DecoderStack::~DecoderStack()
 {
-	if (_decode_thread.joinable()) {
-		_decode_thread.interrupt();
-		_decode_thread.join();
+	if (decode_thread_.joinable()) {
+		interrupt_ = true;
+		input_cond_.notify_one();
+		decode_thread_.join();
 	}
 }
 
-const std::list< boost::shared_ptr<decode::Decoder> >&
+const std::list< std::shared_ptr<decode::Decoder> >&
 DecoderStack::stack() const
 {
-	return _stack;
+	return stack_;
 }
 
-void DecoderStack::push(boost::shared_ptr<decode::Decoder> decoder)
+void DecoderStack::push(std::shared_ptr<decode::Decoder> decoder)
 {
 	assert(decoder);
-	_stack.push_back(decoder);
+	stack_.push_back(decoder);
 }
 
 void DecoderStack::remove(int index)
 {
 	assert(index >= 0);
-	assert(index < (int)_stack.size());
+	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());
+	auto iter = stack_.begin();
+	for (int i = 0; i < index; i++, iter++)
+		assert(iter != stack_.end());
 
 	// Delete the element
-	_stack.erase(iter);
+	stack_.erase(iter);
+}
+
+double DecoderStack::samplerate() const
+{
+	return samplerate_;
+}
+
+const pv::util::Timestamp& DecoderStack::start_time() const
+{
+	return start_time_;
 }
 
 int64_t DecoderStack::samples_decoded() const
 {
-	lock_guard<mutex> decode_lock(_output_mutex);
-	return _samples_decoded;
+	lock_guard<mutex> decode_lock(output_mutex_);
+	return samples_decoded_;
 }
 
 std::vector<Row> DecoderStack::get_visible_rows() const
 {
-	lock_guard<mutex> lock(_output_mutex);
+	lock_guard<mutex> lock(output_mutex_);
 
 	vector<Row> rows;
 
-	BOOST_FOREACH (const shared_ptr<decode::Decoder> &dec, _stack)
-	{
+	for (const shared_ptr<decode::Decoder> &dec : stack_) {
 		assert(dec);
 		if (!dec->shown())
 			continue;
@@ -140,8 +149,7 @@ std::vector<Row> DecoderStack::get_visible_rows() const
 			rows.push_back(Row(decc));
 
 		// Add the decoder rows
-		for (const GSList *l = decc->annotation_rows; l; l = l->next)
-		{
+		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);
@@ -157,29 +165,28 @@ void DecoderStack::get_annotation_subset(
 	const Row &row, uint64_t start_sample,
 	uint64_t end_sample) const
 {
-	lock_guard<mutex> lock(_output_mutex);
+	lock_guard<mutex> lock(output_mutex_);
 
-	std::map<const Row, decode::RowData>::const_iterator iter =
-		_rows.find(row);
-	if (iter != _rows.end())
+	const auto 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;
+	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();
+	sample_count_ = 0;
+	frame_complete_ = false;
+	samples_decoded_ = 0;
+	error_message_ = QString();
+	rows_.clear();
+	class_rows_.clear();
 }
 
 void DecoderStack::begin_decode()
@@ -187,35 +194,34 @@ 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();
+	if (decode_thread_.joinable()) {
+		interrupt_ = true;
+		input_cond_.notify_one();
+		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 "
+	for (const shared_ptr<decode::Decoder> &dec : stack_)
+		if (!dec->have_required_channels()) {
+			error_message_ = tr("One or more required channels "
 				"have not been specified");
 			return;
 		}
 
 	// Add classes
-	BOOST_FOREACH (const shared_ptr<decode::Decoder> &dec, _stack)
-	{
+	for (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();
+			rows_[Row(decc)] = decode::RowData();
 
 		// Add the decoder rows
-		for (const GSList *l = decc->annotation_rows; l; l = l->next)
-		{
+		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);
@@ -223,20 +229,20 @@ void DecoderStack::begin_decode()
 			const Row row(decc, ann_row);
 
 			// Add a new empty row data object
-			_rows[row] = decode::RowData();
+			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,
+				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)
+	// LogicSignals have the same data/segment
+	for (const shared_ptr<decode::Decoder> &dec : stack_)
 		if (dec && !dec->channels().empty() &&
 			((logic_signal = (*dec->channels().begin()).second)) &&
 			((data = logic_signal->logic_data())))
@@ -245,28 +251,28 @@ void DecoderStack::begin_decode()
 	if (!data)
 		return;
 
-	// Check we have a snapshot of data
-	const deque< shared_ptr<pv::data::LogicSnapshot> > &snapshots =
-		data->get_snapshots();
-	if (snapshots.empty())
+	// Check we have a segment of data
+	const deque< shared_ptr<pv::data::LogicSegment> > &segments =
+		data->logic_segments();
+	if (segments.empty())
 		return;
-	_snapshot = snapshots.front();
+	segment_ = segments.front();
 
 	// Get the samplerate and start time
-	_start_time = data->get_start_time();
-	_samplerate = data->samplerate();
-	if (_samplerate == 0.0)
-		_samplerate = 1.0;
+	start_time_ = segment_->start_time();
+	samplerate_ = segment_->samplerate();
+	if (samplerate_ == 0.0)
+		samplerate_ = 1.0;
 
-	_decode_thread = boost::thread(&DecoderStack::decode_proc, this);
+	interrupt_ = false;
+	decode_thread_ = std::thread(&DecoderStack::decode_proc, this);
 }
 
-uint64_t DecoderStack::get_max_sample_count() const
+uint64_t DecoderStack::max_sample_count() const
 {
 	uint64_t max_sample_count = 0;
 
-	for (map<const Row, RowData>::const_iterator i = _rows.begin();
-		i != _rows.end(); i++)
+	for (auto i = rows_.cbegin(); i != rows_.end(); i++)
 		max_sample_count = max(max_sample_count,
 			(*i).second.get_max_sample());
 
@@ -275,14 +281,13 @@ uint64_t DecoderStack::get_max_sample_count() const
 
 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);
+	unique_lock<mutex> input_lock(input_mutex_);
+	while (!interrupt_ && !frame_complete_ &&
+		samples_decoded_ >= sample_count_)
+		input_cond_.wait(input_lock);
+	return boost::make_optional(!interrupt_ &&
+		(samples_decoded_ < sample_count_ || !frame_complete_),
+		sample_count_);
 }
 
 void DecoderStack::decode_data(
@@ -292,28 +297,25 @@ void DecoderStack::decode_data(
 	uint8_t chunk[DecodeChunkLength];
 
 	const unsigned int chunk_sample_count =
-		DecodeChunkLength / _snapshot->unit_size();
+		DecodeChunkLength / segment_->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);
+	for (int64_t i = 0; !interrupt_ && 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);
+		segment_->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");
+		if (srd_session_send(session, i, chunk_end, chunk,
+				(chunk_end - i) * unit_size, unit_size) != SRD_OK) {
+			error_message_ = tr("Decoder reported an error");
 			break;
 		}
 
 		{
-			lock_guard<mutex> lock(_output_mutex);
-			_samples_decoded = chunk_end;
+			lock_guard<mutex> lock(output_mutex_);
+			samples_decoded_ = chunk_end;
 		}
 
 		if (i % DecodeNotifyPeriod == 0)
@@ -327,24 +329,22 @@ void DecoderStack::decode_proc()
 {
 	optional<int64_t> sample_count;
 	srd_session *session;
-	srd_decoder_inst *prev_di = NULL;
+	srd_decoder_inst *prev_di = nullptr;
 
-	assert(_snapshot);
+	assert(segment_);
 
 	// Create the session
 	srd_session_new(&session);
 	assert(session);
 
 	// Create the decoders
-	const unsigned int unit_size = _snapshot->unit_size();
+	const unsigned int unit_size = segment_->unit_size();
 
-	BOOST_FOREACH(const shared_ptr<decode::Decoder> &dec, _stack)
-	{
-		srd_decoder_inst *const di = dec->create_decoder_inst(session, unit_size);
+	for (const shared_ptr<decode::Decoder> &dec : stack_) {
+		srd_decoder_inst *const di = dec->create_decoder_inst(session);
 
-		if (!di)
-		{
-			_error_message = tr("Failed to create decoder instance");
+		if (!di) {
+			error_message_ = tr("Failed to create decoder instance");
 			srd_session_destroy(session);
 			return;
 		}
@@ -357,13 +357,13 @@ void DecoderStack::decode_proc()
 
 	// Get the intial sample count
 	{
-		unique_lock<mutex> input_lock(_input_mutex);
-		sample_count = _sample_count = _snapshot->get_sample_count();
+		unique_lock<mutex> input_lock(input_mutex_);
+		sample_count = sample_count_ = segment_->get_sample_count();
 	}
 
 	// Start the session
 	srd_session_metadata_set(session, SRD_CONF_SAMPLERATE,
-		g_variant_new_uint64((uint64_t)_samplerate));
+		g_variant_new_uint64((uint64_t)samplerate_));
 
 	srd_pd_output_callback_add(session, SRD_OUTPUT_ANN,
 		DecoderStack::annotation_callback, this);
@@ -372,7 +372,7 @@ void DecoderStack::decode_proc()
 
 	do {
 		decode_data(*sample_count, unit_size, session);
-	} while(_error_message.isEmpty() && (sample_count = wait_for_data()));
+	} while (error_message_.isEmpty() && (sample_count = wait_for_data()));
 
 	// Destroy the session
 	srd_session_destroy(session);
@@ -386,7 +386,7 @@ void DecoderStack::annotation_callback(srd_proto_data *pdata, void *decoder)
 	DecoderStack *const d = (DecoderStack*)decoder;
 	assert(d);
 
-	lock_guard<mutex> lock(d->_output_mutex);
+	lock_guard<mutex> lock(d->output_mutex_);
 
 	const Annotation a(pdata);
 
@@ -396,21 +396,19 @@ void DecoderStack::annotation_callback(srd_proto_data *pdata, void *decoder)
 	const srd_decoder *const decc = pdata->pdo->di->decoder;
 	assert(decc);
 
-	map<const Row, decode::RowData>::iterator row_iter = d->_rows.end();
-	
+	auto 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
-	{
+	const auto 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));	
+		row_iter = d->rows_.find(Row(decc));	
 	}
 
-	assert(row_iter != d->_rows.end());
-	if (row_iter == d->_rows.end()) {
+	assert(row_iter != d->rows_.end());
+	if (row_iter == d->rows_.end()) {
 		qDebug() << "Unexpected annotation: decoder = " << decc <<
 			", format = " << a.format();
 		assert(0);
@@ -429,21 +427,21 @@ void DecoderStack::on_new_frame()
 void DecoderStack::on_data_received()
 {
 	{
-		unique_lock<mutex> lock(_input_mutex);
-		if (_snapshot)
-			_sample_count = _snapshot->get_sample_count();
+		unique_lock<mutex> lock(input_mutex_);
+		if (segment_)
+			sample_count_ = segment_->get_sample_count();
 	}
-	_input_cond.notify_one();
+	input_cond_.notify_one();
 }
 
 void DecoderStack::on_frame_ended()
 {
 	{
-		unique_lock<mutex> lock(_input_mutex);
-		if (_snapshot)
-			_frame_complete = true;
+		unique_lock<mutex> lock(input_mutex_);
+		if (segment_)
+			frame_complete_ = true;
 	}
-	_input_cond.notify_one();
+	input_cond_.notify_one();
 }
 
 } // namespace data
diff --git a/pv/data/decoderstack.h b/pv/data/decoderstack.hpp
similarity index 63%
rename from pv/data/decoderstack.h
rename to pv/data/decoderstack.hpp
index 2eeaf1c..64ce13b 100644
--- a/pv/data/decoderstack.h
+++ b/pv/data/decoderstack.hpp
@@ -18,22 +18,26 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_DATA_DECODERSTACK_H
-#define PULSEVIEW_PV_DATA_DECODERSTACK_H
+#ifndef PULSEVIEW_PV_DATA_DECODERSTACK_HPP
+#define PULSEVIEW_PV_DATA_DECODERSTACK_HPP
 
-#include "signaldata.h"
+#include "signaldata.hpp"
 
+#include <atomic>
+#include <condition_variable>
 #include <list>
+#include <map>
+#include <memory>
+#include <thread>
 
 #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>
+#include <pv/data/decode/row.hpp>
+#include <pv/data/decode/rowdata.hpp>
+#include <pv/util.hpp>
 
 struct srd_decoder;
 struct srd_decoder_annotation_row;
@@ -42,12 +46,12 @@ struct srd_proto_data;
 struct srd_session;
 
 namespace DecoderStackTest {
-class TwoDecoderStack;
+struct TwoDecoderStack;
 }
 
 namespace pv {
 
-class SigSession;
+class Session;
 
 namespace view {
 class LogicSignal;
@@ -55,7 +59,7 @@ class LogicSignal;
 
 namespace data {
 
-class LogicSnapshot;
+class LogicSegment;
 
 namespace decode {
 class Annotation;
@@ -64,7 +68,7 @@ class Decoder;
 
 class Logic;
 
-class DecoderStack : public QObject, public SignalData
+class DecoderStack : public QObject
 {
 	Q_OBJECT
 
@@ -75,15 +79,19 @@ private:
 	static const unsigned int DecodeNotifyPeriod;
 
 public:
-	DecoderStack(pv::SigSession &_session,
+	DecoderStack(pv::Session &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);
+	const std::list< std::shared_ptr<decode::Decoder> >& stack() const;
+	void push(std::shared_ptr<decode::Decoder> decoder);
 	void remove(int index);
 
+	double samplerate() const;
+
+	const pv::util::Timestamp& start_time() const;
+
 	int64_t samples_decoded() const;
 
 	std::vector<decode::Row> get_visible_rows() const;
@@ -100,7 +108,7 @@ public:
 
 	void clear();
 
-	uint64_t get_max_sample_count() const;
+	uint64_t max_sample_count() const;
 
 	void begin_decode();
 
@@ -115,18 +123,21 @@ private:
 	static void annotation_callback(srd_proto_data *pdata,
 		void *decoder);
 
-private slots:
+private Q_SLOTS:
 	void on_new_frame();
 
 	void on_data_received();
 
 	void on_frame_ended();
 
-signals:
+Q_SIGNALS:
 	void new_decode_data();
 
 private:
-	pv::SigSession &_session;
+	pv::Session &session_;
+
+	pv::util::Timestamp start_time_;
+	double samplerate_;
 
 	/**
 	 * This mutex prevents more than one decode operation occuring
@@ -134,32 +145,33 @@ private:
 	 * @todo A proper solution should be implemented to allow multiple
 	 * decode operations.
 	 */
-	static boost::mutex _global_decode_mutex;
+	static std::mutex global_decode_mutex_;
 
-	std::list< boost::shared_ptr<decode::Decoder> > _stack;
+	std::list< std::shared_ptr<decode::Decoder> > stack_;
 
-	boost::shared_ptr<pv::data::LogicSnapshot> _snapshot;
+	std::shared_ptr<pv::data::LogicSegment> segment_;
 
-	mutable boost::mutex _input_mutex;
-	mutable boost::condition_variable _input_cond;
-	int64_t _sample_count;
-	bool _frame_complete;
+	mutable std::mutex input_mutex_;
+	mutable std::condition_variable input_cond_;
+	int64_t sample_count_;
+	bool frame_complete_;
 
-	mutable boost::mutex _output_mutex;
-	int64_t	_samples_decoded;
+	mutable std::mutex output_mutex_;
+	int64_t	samples_decoded_;
 
-	std::map<const decode::Row, decode::RowData> _rows;
+	std::map<const decode::Row, decode::RowData> rows_;
 
-	std::map<std::pair<const srd_decoder*, int>, decode::Row> _class_rows;
+	std::map<std::pair<const srd_decoder*, int>, decode::Row> class_rows_;
 
-	QString _error_message;
+	QString error_message_;
 
-	boost::thread _decode_thread;
+	std::thread decode_thread_;
+	std::atomic<bool> interrupt_;
 
-	friend class DecoderStackTest::TwoDecoderStack;
+	friend struct DecoderStackTest::TwoDecoderStack;
 };
 
 } // namespace data
 } // namespace pv
 
-#endif // PULSEVIEW_PV_DATA_DECODERSTACK_H
+#endif // PULSEVIEW_PV_DATA_DECODERSTACK_HPP
diff --git a/pv/data/logic.cpp b/pv/data/logic.cpp
index 167c79b..b346353 100644
--- a/pv/data/logic.cpp
+++ b/pv/data/logic.cpp
@@ -18,50 +18,57 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include <boost/foreach.hpp>
+#include <cassert>
 
-#include "logic.h"
-#include "logicsnapshot.h"
+#include "logic.hpp"
+#include "logicsegment.hpp"
 
-using boost::shared_ptr;
 using std::deque;
 using std::max;
+using std::shared_ptr;
+using std::vector;
 
 namespace pv {
 namespace data {
 
-Logic::Logic(unsigned int num_probes) :
+Logic::Logic(unsigned int num_channels) :
 	SignalData(),
-	_num_probes(num_probes)
+	num_channels_(num_channels)
 {
-	assert(_num_probes > 0);
+	assert(num_channels_ > 0);
 }
 
-int Logic::get_num_probes() const
+unsigned int Logic::num_channels() const
 {
-	return _num_probes;
+	return num_channels_;
 }
 
-void Logic::push_snapshot(
-	shared_ptr<LogicSnapshot> &snapshot)
+void Logic::push_segment(
+	shared_ptr<LogicSegment> &segment)
 {
-	_snapshots.push_front(snapshot);
+	segments_.push_front(segment);
 }
 
-deque< shared_ptr<LogicSnapshot> >& Logic::get_snapshots()
+const deque< shared_ptr<LogicSegment> >& Logic::logic_segments() const
 {
-	return _snapshots;
+	return segments_;
+}
+
+vector< shared_ptr<Segment> > Logic::segments() const
+{
+	return vector< shared_ptr<Segment> >(
+		segments_.begin(), segments_.end());
 }
 
 void Logic::clear()
 {
-	_snapshots.clear();
+	segments_.clear();
 }
 
-uint64_t Logic::get_max_sample_count() const
+uint64_t Logic::max_sample_count() const
 {
 	uint64_t l = 0;
-	BOOST_FOREACH(boost::shared_ptr<LogicSnapshot> s, _snapshots) {
+	for (std::shared_ptr<LogicSegment> s : segments_) {
 		assert(s);
 		l = max(l, s->get_sample_count());
 	}
diff --git a/pv/data/logic.h b/pv/data/logic.hpp
similarity index 64%
rename from pv/data/logic.h
rename to pv/data/logic.hpp
index 3d83394..a597027 100644
--- a/pv/data/logic.h
+++ b/pv/data/logic.hpp
@@ -18,42 +18,43 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_DATA_LOGIC_H
-#define PULSEVIEW_PV_DATA_LOGIC_H
+#ifndef PULSEVIEW_PV_DATA_LOGIC_HPP
+#define PULSEVIEW_PV_DATA_LOGIC_HPP
 
-#include "signaldata.h"
+#include "signaldata.hpp"
 
-#include <boost/shared_ptr.hpp>
 #include <deque>
 
 namespace pv {
 namespace data {
 
-class LogicSnapshot;
+class LogicSegment;
 
 class Logic : public SignalData
 {
 public:
-	Logic(unsigned int num_probes);
+	Logic(unsigned int num_channels);
 
-	int get_num_probes() const;
+	unsigned int num_channels() const;
 
-	void push_snapshot(
-		boost::shared_ptr<LogicSnapshot> &snapshot);
+	void push_segment(
+		std::shared_ptr<LogicSegment> &segment);
 
-	std::deque< boost::shared_ptr<LogicSnapshot> >&
-		get_snapshots();
+	const std::deque< std::shared_ptr<LogicSegment> >&
+		logic_segments() const;
+
+	std::vector< std::shared_ptr<Segment> > segments() const;
 
 	void clear();
 
-	uint64_t get_max_sample_count() const;
+	uint64_t max_sample_count() const;
 
 private:
-	const unsigned int _num_probes;
-	std::deque< boost::shared_ptr<LogicSnapshot> > _snapshots;
+	const unsigned int num_channels_;
+	std::deque< std::shared_ptr<LogicSegment> > segments_;
 };
 
 } // namespace data
 } // namespace pv
 
-#endif // PULSEVIEW_PV_DATA_LOGIC_H
+#endif // PULSEVIEW_PV_DATA_LOGIC_HPP
diff --git a/pv/data/logicsnapshot.cpp b/pv/data/logicsegment.cpp
similarity index 69%
rename from pv/data/logicsnapshot.cpp
rename to pv/data/logicsegment.cpp
index 797a00b..3e18b85 100644
--- a/pv/data/logicsnapshot.cpp
+++ b/pv/data/logicsegment.cpp
@@ -23,53 +23,55 @@
 #include <assert.h>
 #include <string.h>
 #include <stdlib.h>
-#include <math.h>
+#include <cmath>
 
-#include <boost/foreach.hpp>
+#include "logicsegment.hpp"
 
-#include "config.h"
-#include "logicsnapshot.h"
+#include <libsigrokcxx/libsigrokcxx.hpp>
 
-using boost::lock_guard;
-using boost::recursive_mutex;
+using std::lock_guard;
+using std::recursive_mutex;
 using std::max;
 using std::min;
 using std::pair;
+using std::shared_ptr;
+
+using sigrok::Logic;
 
 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
+const int LogicSegment::MipMapScalePower = 4;
+const int LogicSegment::MipMapScaleFactor = 1 << MipMapScalePower;
+const float LogicSegment::LogMipMapScaleFactor = logf(MipMapScaleFactor);
+const uint64_t LogicSegment::MipMapDataUnit = 64*1024;	// bytes
 
-LogicSnapshot::LogicSnapshot(const sr_datafeed_logic &logic,
-                             const uint64_t expected_num_samples) :
-	Snapshot(logic.unitsize),
-	_last_append_sample(0)
+LogicSegment::LogicSegment(shared_ptr<Logic> logic, uint64_t samplerate,
+				const uint64_t expected_num_samples) :
+	Segment(samplerate, logic->unit_size()),
+	last_append_sample_(0)
 {
 	set_capacity(expected_num_samples);
 
-	lock_guard<recursive_mutex> lock(_mutex);
-	memset(_mip_map, 0, sizeof(_mip_map));
+	lock_guard<recursive_mutex> lock(mutex_);
+	memset(mip_map_, 0, sizeof(mip_map_));
 	append_payload(logic);
 }
 
-LogicSnapshot::~LogicSnapshot()
+LogicSegment::~LogicSegment()
 {
-	lock_guard<recursive_mutex> lock(_mutex);
-	BOOST_FOREACH(MipMapLevel &l, _mip_map)
+	lock_guard<recursive_mutex> lock(mutex_);
+	for (MipMapLevel &l : mip_map_)
 		free(l.data);
 }
 
-uint64_t LogicSnapshot::unpack_sample(const uint8_t *ptr) const
+uint64_t LogicSegment::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) {
+	switch (unit_size_) {
 	default:
 		value |= ((uint64_t)ptr[7]) << 56;
 		/* FALLTHRU */
@@ -101,12 +103,12 @@ uint64_t LogicSnapshot::unpack_sample(const uint8_t *ptr) const
 #endif
 }
 
-void LogicSnapshot::pack_sample(uint8_t *ptr, uint64_t value)
+void LogicSegment::pack_sample(uint8_t *ptr, uint64_t value)
 {
 #ifdef HAVE_UNALIGNED_LITTLE_ENDIAN_ACCESS
 	*(uint64_t*)ptr = value;
 #else
-	switch(_unit_size) {
+	switch (unit_size_) {
 	default:
 		ptr[7] = value >> 56;
 		/* FALLTHRU */
@@ -137,53 +139,52 @@ void LogicSnapshot::pack_sample(uint8_t *ptr, uint64_t value)
 #endif
 }
 
-void LogicSnapshot::append_payload(
-	const sr_datafeed_logic &logic)
+void LogicSegment::append_payload(shared_ptr<Logic> logic)
 {
-	assert(_unit_size == logic.unitsize);
-	assert((logic.length % _unit_size) == 0);
+	assert(unit_size_ == logic->unit_size());
+	assert((logic->data_length() % unit_size_) == 0);
 
-	lock_guard<recursive_mutex> lock(_mutex);
+	lock_guard<recursive_mutex> lock(mutex_);
 
-	append_data(logic.data, logic.length / _unit_size);
+	append_data(logic->data_pointer(),
+		logic->data_length() / unit_size_);
 
 	// Generate the first mip-map from the data
 	append_payload_to_mipmap();
 }
 
-void LogicSnapshot::get_samples(uint8_t *const data,
+void LogicSegment::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(start_sample <= (int64_t)sample_count_);
 	assert(end_sample >= 0);
-	assert(end_sample <= (int64_t)_sample_count);
+	assert(end_sample <= (int64_t)sample_count_);
 	assert(start_sample <= end_sample);
 
-	lock_guard<recursive_mutex> lock(_mutex);
+	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);
+	const size_t size = (end_sample - start_sample) * unit_size_;
+	memcpy(data, (const uint8_t*)data_.data() + start_sample * unit_size_, size);
 }
 
-void LogicSnapshot::reallocate_mipmap_level(MipMapLevel &m)
+void LogicSegment::reallocate_mipmap_level(MipMapLevel &m)
 {
 	const uint64_t new_data_length = ((m.length + MipMapDataUnit - 1) /
 		MipMapDataUnit) * MipMapDataUnit;
-	if (new_data_length > m.data_length)
-	{
+	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 +
+		m.data = realloc(m.data, new_data_length * unit_size_ +
 			sizeof(uint64_t));
 	}
 }
 
-void LogicSnapshot::append_payload_to_mipmap()
+void LogicSegment::append_payload_to_mipmap()
 {
-	MipMapLevel &m0 = _mip_map[0];
+	MipMapLevel &m0 = mip_map_[0];
 	uint64_t prev_length;
 	const uint8_t *src_ptr;
 	uint8_t *dest_ptr;
@@ -192,7 +193,7 @@ void LogicSnapshot::append_payload_to_mipmap()
 
 	// Expand the data buffer to fit the new samples
 	prev_length = m0.length;
-	m0.length = _sample_count / MipMapScaleFactor;
+	m0.length = sample_count_ / MipMapScaleFactor;
 
 	// Break off if there are no new samples to compute
 	if (m0.length == prev_length)
@@ -200,35 +201,32 @@ void LogicSnapshot::append_payload_to_mipmap()
 
 	reallocate_mipmap_level(m0);
 
-	dest_ptr = (uint8_t*)m0.data + prev_length * _unit_size;
+	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;)
-	{
+	const uint8_t *const end_src_ptr = (uint8_t*)data_.data() +
+		m0.length * unit_size_ * MipMapScaleFactor;
+	for (src_ptr = (uint8_t*)data_.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)
-		{
+		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;
+			accumulator |= last_append_sample_ ^ sample;
+			last_append_sample_ = sample;
+			src_ptr += unit_size_;
 		}
 
 		pack_sample(dest_ptr, accumulator);
-		dest_ptr += _unit_size;
+		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];
+	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;
@@ -242,20 +240,18 @@ void LogicSnapshot::append_payload_to_mipmap()
 
 		// Subsample the level lower level
 		src_ptr = (uint8_t*)ml.data +
-			_unit_size * prev_length * MipMapScaleFactor;
+			unit_size_ * prev_length * MipMapScaleFactor;
 		const uint8_t *const end_dest_ptr =
-			(uint8_t*)m.data + _unit_size * m.length;
+			(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)
-		{
+				unit_size_ * prev_length;
+				dest_ptr < end_dest_ptr;
+				dest_ptr += unit_size_) {
 			accumulator = 0;
 			diff_counter = MipMapScaleFactor;
-			while (diff_counter-- > 0)
-			{
+			while (diff_counter-- > 0) {
 				accumulator |= unpack_sample(src_ptr);
-				src_ptr += _unit_size;
+				src_ptr += unit_size_;
 			}
 
 			pack_sample(dest_ptr, accumulator);
@@ -263,15 +259,14 @@ void LogicSnapshot::append_payload_to_mipmap()
 	}
 }
 
-uint64_t LogicSnapshot::get_sample(uint64_t index) const
+uint64_t LogicSegment::get_sample(uint64_t index) const
 {
-	assert(_data);
-	assert(index < _sample_count);
+	assert(index < sample_count_);
 
-	return unpack_sample((uint8_t*)_data + index * _unit_size);
+	return unpack_sample((uint8_t*)data_.data() + index * unit_size_);
 }
 
-void LogicSnapshot::get_subsampled_edges(
+void LogicSegment::get_subsampled_edges(
 	std::vector<EdgePair> &edges,
 	uint64_t start, uint64_t end,
 	float min_length, int sig_index)
@@ -287,7 +282,7 @@ void LogicSnapshot::get_subsampled_edges(
 	assert(sig_index >= 0);
 	assert(sig_index < 64);
 
-	lock_guard<recursive_mutex> lock(_mutex);
+	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) /
@@ -298,26 +293,23 @@ void LogicSnapshot::get_subsampled_edges(
 	last_sample = (get_sample(start) & sig_mask) != 0;
 	edges.push_back(pair<int64_t, bool>(index++, last_sample));
 
-	while (index + block_length <= end)
-	{
+	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);
+		fast_forward = (mip_map_[level].data != nullptr);
 
-		if (min_length < MipMapScaleFactor)
-		{
+		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++)
-			{
+					(index & ~(~0 << MipMapScalePower)) != 0;
+					index++) {
 				const bool sample =
 					(get_sample(index) & sig_mask) != 0;
 
@@ -327,9 +319,7 @@ void LogicSnapshot::get_subsampled_edges(
 					break;
 				}
 			}
-		}
-		else
-		{
+		} 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
@@ -363,7 +353,7 @@ void LogicSnapshot::get_subsampled_edges(
 
 				// 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 ||
+				if (offset >= mip_map_[level].length ||
 					(get_subsample(level, offset) &
 						sig_mask))
 					break;
@@ -373,7 +363,7 @@ void LogicSnapshot::get_subsampled_edges(
 					// higher level mip-map block ascend one
 					// level
 					if (level + 1 >= ScaleStepCount ||
-						!_mip_map[level + 1].data)
+						!mip_map_[level + 1].data)
 						break;
 
 					level++;
@@ -388,7 +378,7 @@ void LogicSnapshot::get_subsampled_edges(
 			// Zoom in, and slide right until we encounter a change,
 			// and repeat until we reach min_level
 			while (1) {
-				assert(_mip_map[level].data);
+				assert(mip_map_[level].data);
 
 				const int level_scale_power =
 					(level + 1) * MipMapScalePower;
@@ -397,8 +387,8 @@ void LogicSnapshot::get_subsampled_edges(
 
 				// 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) &
+				if (offset >= mip_map_[level].length ||
+						(get_subsample(level, offset) &
 						sig_mask)) {
 					// Zoom in unless we reached the minimum
 					// zoom
@@ -450,15 +440,15 @@ void LogicSnapshot::get_subsampled_edges(
 	edges.push_back(pair<int64_t, bool>(end + 1, end_sample));
 }
 
-uint64_t LogicSnapshot::get_subsample(int level, uint64_t offset) const
+uint64_t LogicSegment::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);
+	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)
+uint64_t LogicSegment::pow2_ceil(uint64_t x, unsigned int power)
 {
 	const uint64_t p = 1 << power;
 	return (x + p - 1) / p * p;
diff --git a/pv/data/logicsnapshot.h b/pv/data/logicsegment.hpp
similarity index 72%
rename from pv/data/logicsnapshot.h
rename to pv/data/logicsegment.hpp
index 0f6f410..c9e8559 100644
--- a/pv/data/logicsnapshot.h
+++ b/pv/data/logicsegment.hpp
@@ -18,26 +18,30 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_DATA_LOGICSNAPSHOT_H
-#define PULSEVIEW_PV_DATA_LOGICSNAPSHOT_H
+#ifndef PULSEVIEW_PV_DATA_LOGICSEGMENT_HPP
+#define PULSEVIEW_PV_DATA_LOGICSEGMENT_HPP
 
-#include "snapshot.h"
+#include "segment.hpp"
 
 #include <utility>
 #include <vector>
 
-namespace LogicSnapshotTest {
-class Pow2;
-class Basic;
-class LargeData;
-class Pulses;
-class LongPulses;
+namespace sigrok {
+	class Logic;
+}
+
+namespace LogicSegmentTest {
+struct Pow2;
+struct Basic;
+struct LargeData;
+struct Pulses;
+struct LongPulses;
 }
 
 namespace pv {
 namespace data {
 
-class LogicSnapshot : public Snapshot
+class LogicSegment : public Segment
 {
 private:
 	struct MipMapLevel
@@ -58,12 +62,12 @@ public:
 	typedef std::pair<int64_t, bool> EdgePair;
 
 public:
-	LogicSnapshot(const sr_datafeed_logic &logic,
-	              uint64_t expected_num_samples = 0);
+	LogicSegment(std::shared_ptr<sigrok::Logic> logic,
+		uint64_t samplerate, uint64_t expected_num_samples = 0);
 
-	virtual ~LogicSnapshot();
+	virtual ~LogicSegment();
 
-	void append_payload(const sr_datafeed_logic &logic);
+	void append_payload(std::shared_ptr<sigrok::Logic> logic);
 
 	void get_samples(uint8_t *const data,
 		int64_t start_sample, int64_t end_sample) const;
@@ -80,7 +84,7 @@ private:
 
 public:
 	/**
-	 * Parses a logic data snapshot to generate a list of transitions
+	 * Parses a logic data segment 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.
@@ -88,7 +92,7 @@ public:
 	 * @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);
@@ -99,17 +103,17 @@ private:
 	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;
+	struct MipMapLevel mip_map_[ScaleStepCount];
+	uint64_t last_append_sample_;
+
+	friend struct LogicSegmentTest::Pow2;
+	friend struct LogicSegmentTest::Basic;
+	friend struct LogicSegmentTest::LargeData;
+	friend struct LogicSegmentTest::Pulses;
+	friend struct LogicSegmentTest::LongPulses;
 };
 
 } // namespace data
 } // namespace pv
 
-#endif // PULSEVIEW_PV_DATA_LOGICSNAPSHOT_H
+#endif // PULSEVIEW_PV_DATA_LOGICSEGMENT_HPP
diff --git a/pv/data/segment.cpp b/pv/data/segment.cpp
new file mode 100644
index 0000000..754e300
--- /dev/null
+++ b/pv/data/segment.cpp
@@ -0,0 +1,110 @@
+/*
+ * 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 "segment.hpp"
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+
+using std::lock_guard;
+using std::recursive_mutex;
+
+namespace pv {
+namespace data {
+
+Segment::Segment(uint64_t samplerate, unsigned int unit_size) :
+	sample_count_(0),
+	start_time_(0),
+	samplerate_(samplerate),
+	capacity_(0),
+	unit_size_(unit_size)
+{
+	lock_guard<recursive_mutex> lock(mutex_);
+	assert(unit_size_ > 0);
+}
+
+Segment::~Segment()
+{
+	lock_guard<recursive_mutex> lock(mutex_);
+}
+
+uint64_t Segment::get_sample_count() const
+{
+	lock_guard<recursive_mutex> lock(mutex_);
+	return sample_count_;
+}
+
+const pv::util::Timestamp& Segment::start_time() const
+{
+	return start_time_;
+}
+
+double Segment::samplerate() const
+{
+	return samplerate_;
+}
+
+void Segment::set_samplerate(double samplerate)
+{
+	samplerate_ = samplerate;
+}
+
+unsigned int Segment::unit_size() const
+{
+	return unit_size_;
+}
+
+void Segment::set_capacity(const uint64_t new_capacity)
+{
+	lock_guard<recursive_mutex> lock(mutex_);
+
+	assert(capacity_ >= sample_count_);
+	if (new_capacity > capacity_) {
+		// If we're out of memory, this will throw std::bad_alloc
+		data_.resize((new_capacity * unit_size_) + sizeof(uint64_t));
+		capacity_ = new_capacity;
+	}
+}
+
+uint64_t Segment::capacity() const
+{
+	lock_guard<recursive_mutex> lock(mutex_);
+	return data_.size();
+}
+
+void Segment::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_.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/segment.hpp
similarity index 64%
rename from pv/data/snapshot.h
rename to pv/data/segment.hpp
index 1f1ca3c..5d0a242 100644
--- a/pv/data/snapshot.h
+++ b/pv/data/segment.hpp
@@ -18,51 +18,58 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_DATA_SNAPSHOT_H
-#define PULSEVIEW_PV_DATA_SNAPSHOT_H
+#ifndef PULSEVIEW_PV_DATA_SEGMENT_HPP
+#define PULSEVIEW_PV_DATA_SEGMENT_HPP
 
-#include <libsigrok/libsigrok.h>
+#include "pv/util.hpp"
 
-#include <boost/thread.hpp>
+#include <thread>
+#include <mutex>
+#include <vector>
 
 namespace pv {
 namespace data {
 
-class Snapshot
+class Segment
 {
 public:
-	Snapshot(int unit_size);
+	Segment(uint64_t samplerate, unsigned int unit_size);
 
-	virtual ~Snapshot();
+	virtual ~Segment();
 
 	uint64_t get_sample_count() const;
 
-	int unit_size() const;
+	const pv::util::Timestamp& start_time() const;
+
+	double samplerate() const;
+	void set_samplerate(double samplerate);
+
+	unsigned int unit_size() const;
 
 	/**
-	 * @brief Increase the capacity of the snapshot.
+	 * @brief Increase the capacity of the segment.
 	 *
 	 * 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
+	 * @c append_data() is called to set up the segment 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
+	 * @param[in] new_capacity The new capacity of the segment. 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.
+	 * @brief Get the current capacity of the segment.
 	 *
 	 * The capacity can be increased by calling @c set_capacity().
 	 *
-	 * @return The current capacity of the snapshot.
+	 * @return The current capacity of the segment.
 	 */
 	uint64_t capacity() const;
 
@@ -70,14 +77,16 @@ 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;
+	mutable std::recursive_mutex mutex_;
+	std::vector<uint8_t> data_;
+	uint64_t sample_count_;
+	pv::util::Timestamp start_time_;
+	double samplerate_;
+	uint64_t capacity_;
+	unsigned int unit_size_;
 };
 
 } // namespace data
 } // namespace pv
 
-#endif // PULSEVIEW_PV_DATA_SNAPSHOT_H
+#endif // PULSEVIEW_PV_DATA_SEGMENT_HPP
diff --git a/pv/data/signaldata.cpp b/pv/data/signaldata.cpp
index 04f1d3f..6a4c6d9 100644
--- a/pv/data/signaldata.cpp
+++ b/pv/data/signaldata.cpp
@@ -18,32 +18,14 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "signaldata.h"
+#include "signaldata.hpp"
 
 namespace pv {
 namespace data {
 
-SignalData::SignalData() :
-	_start_time(0),
-	_samplerate(0)
+SignalData::SignalData()
 {
 }
 
-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.hpp
similarity index 74%
rename from pv/data/signaldata.h
rename to pv/data/signaldata.hpp
index 0aa3b34..ff30cf1 100644
--- a/pv/data/signaldata.h
+++ b/pv/data/signaldata.hpp
@@ -18,35 +18,33 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_DATA_SIGNALDATA_H
-#define PULSEVIEW_PV_DATA_SIGNALDATA_H
+#ifndef PULSEVIEW_PV_DATA_SIGNALDATA_HPP
+#define PULSEVIEW_PV_DATA_SIGNALDATA_HPP
 
-#include <stdint.h>
+#include <cstdint>
+#include <memory>
+#include <vector>
 
 namespace pv {
 namespace data {
 
+class Segment;
+
 class SignalData
 {
 public:
 	SignalData();
+	virtual ~SignalData() {}
 
 public:
-	double samplerate() const;
-	void set_samplerate(double samplerate);
-
-	double get_start_time() const;
+	virtual std::vector< std::shared_ptr<Segment> > segments() const = 0;
 
 	virtual void clear() = 0;
 
-	virtual uint64_t get_max_sample_count() const = 0;
-
-protected:
-	double _start_time;
-	double _samplerate;
+	virtual uint64_t max_sample_count() const = 0;
 };
 
 } // namespace data
 } // namespace pv
 
-#endif // PULSEVIEW_PV_DATA_SIGNALDATA_H
+#endif // PULSEVIEW_PV_DATA_SIGNALDATA_HPP
diff --git a/pv/data/snapshot.cpp b/pv/data/snapshot.cpp
deleted file mode 100644
index 6ba39d2..0000000
--- a/pv/data/snapshot.cpp
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * 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/device/device.cpp b/pv/device/device.cpp
deleted file mode 100644
index 4f9a779..0000000
--- a/pv/device/device.cpp
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * 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/devinst.cpp b/pv/device/devinst.cpp
deleted file mode 100644
index 4543e5c..0000000
--- a/pv/device/devinst.cpp
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * 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
deleted file mode 100644
index e6a5140..0000000
--- a/pv/device/devinst.h
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * 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
deleted file mode 100644
index e33be85..0000000
--- a/pv/device/file.cpp
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * 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/inputfile.cpp b/pv/device/inputfile.cpp
deleted file mode 100644
index 55a0fa5..0000000
--- a/pv/device/inputfile.cpp
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 150418d..0000000
--- a/pv/device/inputfile.h
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * 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
deleted file mode 100644
index ffdeb67..0000000
--- a/pv/device/sessionfile.cpp
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * 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
deleted file mode 100644
index c0ed958..0000000
--- a/pv/device/sessionfile.h
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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
index fb487a8..b11e60c 100644
--- a/pv/devicemanager.cpp
+++ b/pv/devicemanager.cpp
@@ -18,133 +18,184 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "devicemanager.h"
-#include "device/device.h"
-#include "sigsession.h"
+#include "devicemanager.hpp"
+#include "session.hpp"
 
 #include <cassert>
+#include <functional>
 #include <stdexcept>
+#include <sstream>
 #include <string>
+#include <vector>
 
-#include <boost/foreach.hpp>
+#include <libsigrokcxx/libsigrokcxx.hpp>
 
-#include <libsigrok/libsigrok.h>
+#include <boost/algorithm/string/join.hpp>
+#include <boost/filesystem.hpp>
 
-using boost::shared_ptr;
+#include <pv/devices/hardwaredevice.hpp>
+
+using boost::algorithm::join;
+
+using std::bind;
+using std::dynamic_pointer_cast;
 using std::list;
 using std::map;
-using std::ostringstream;
+using std::placeholders::_1;
+using std::placeholders::_2;
+using std::remove_if;
 using std::runtime_error;
+using std::shared_ptr;
 using std::string;
+using std::vector;
+
+using Glib::VariantBase;
+
+using sigrok::ConfigKey;
+using sigrok::Context;
+using sigrok::Driver;
+using sigrok::SessionDevice;
 
 namespace pv {
 
-DeviceManager::DeviceManager(struct sr_context *sr_ctx) :
-	_sr_ctx(sr_ctx)
+DeviceManager::DeviceManager(shared_ptr<Context> context) :
+	context_(context)
 {
-	init_drivers();
-	scan_all_drivers();
+	for (auto entry : context->drivers())
+		driver_scan(entry.second, map<const ConfigKey *, VariantBase>());
 }
 
 DeviceManager::~DeviceManager()
 {
-	release_devices();
 }
 
-const list< shared_ptr<pv::device::Device> >& DeviceManager::devices() const
+const std::shared_ptr<sigrok::Context>& DeviceManager::context() const
+{
+	return context_;
+}
+
+shared_ptr<Context> DeviceManager::context()
+{
+	return context_;
+}
+
+const list< shared_ptr<devices::HardwareDevice> >&
+DeviceManager::devices() const
 {
-	return _devices;
+	return devices_;
 }
 
-list< shared_ptr<device::Device> > DeviceManager::driver_scan(
-	struct sr_dev_driver *const driver, GSList *const drvopts)
+list< shared_ptr<devices::HardwareDevice> >
+DeviceManager::driver_scan(
+	shared_ptr<Driver> driver, map<const ConfigKey *, VariantBase> drvopts)
 {
-	list< shared_ptr<device::Device> > driver_devices;
+	list< shared_ptr<devices::HardwareDevice> > 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);
+	devices_.remove_if([&](shared_ptr<devices::HardwareDevice> device) {
+		return device->hardware_device()->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;
-}
+	auto devices = driver->scan(drvopts);
 
-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));
-		}
+	// Add the scanned devices to the main list, set display names and sort.
+	for (shared_ptr<sigrok::HardwareDevice> device : devices) {
+		const shared_ptr<devices::HardwareDevice> d(
+			new devices::HardwareDevice(context_, device));
+		driver_devices.push_back(d);
 	}
-}
 
-void DeviceManager::release_devices()
-{
-	// Release all the used devices
-	BOOST_FOREACH(shared_ptr<device::Device> dev, _devices) {
-		assert(dev);
-		dev->release();
-	}
+	devices_.insert(devices_.end(), driver_devices.begin(),
+		driver_devices.end());
+	devices_.sort(bind(&DeviceManager::compare_devices, this, _1, _2));
+	driver_devices.sort(bind(
+		&DeviceManager::compare_devices, this, _1, _2));
 
-	// Clear all the drivers
-	sr_dev_driver **const drivers = sr_driver_list();
-	for (sr_dev_driver **driver = drivers; *driver; driver++)
-		sr_dev_clear(*driver);
+	return driver_devices;
 }
 
-void DeviceManager::scan_all_drivers()
+const map<string, string> DeviceManager::get_device_info(
+	shared_ptr<devices::Device> device)
 {
-	// 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);
+	map<string, string> result;
+
+	assert(device);
+
+	const shared_ptr<sigrok::Device> sr_dev = device->device();
+	if (sr_dev->vendor().length() > 0)
+		result["vendor"] = sr_dev->vendor();
+	if (sr_dev->model().length() > 0)
+		result["model"] = sr_dev->model();
+	if (sr_dev->version().length() > 0)
+		result["version"] = sr_dev->version();
+	if (sr_dev->serial_number().length() > 0)
+		result["serial_num"] = sr_dev->serial_number();
+	if (sr_dev->connection_id().length() > 0)
+		result["connection_id"] = sr_dev->connection_id();
+
+	return result;
 }
 
-void DeviceManager::release_driver(struct sr_dev_driver *const driver)
+const shared_ptr<devices::HardwareDevice> DeviceManager::find_device_from_info(
+	const map<string, string> search_info)
 {
-	BOOST_FOREACH(shared_ptr<device::Device> dev, _devices) {
+	shared_ptr<devices::HardwareDevice> last_resort_dev;
+	map<string, string> dev_info;
+
+	for (shared_ptr<devices::HardwareDevice> dev : devices_) {
 		assert(dev);
-		if(dev->dev_inst()->driver == driver)
-			dev->release();
+		dev_info = get_device_info(dev);
+
+		// If present, vendor and model always have to match.
+		if (dev_info.count("vendor") > 0 && search_info.count("vendor") > 0)
+			if (dev_info.at("vendor") != search_info.at("vendor"))
+				continue;
+
+		if (dev_info.count("model") > 0 && search_info.count("model") > 0)
+			if (dev_info.at("model") != search_info.at("model"))
+				continue;
+
+		// Most unique match: vendor/model/serial_num (but don't match a S/N of 0)
+		if ((dev_info.count("serial_num") > 0) && (dev_info.at("serial_num") != "0")
+				&& search_info.count("serial_num") > 0)
+			if (dev_info.at("serial_num") == search_info.at("serial_num") &&
+					dev_info.at("serial_num") != "0")
+				return dev;
+
+		// Second best match: vendor/model/connection_id
+		if (dev_info.count("connection_id") > 0 &&
+			search_info.count("connection_id") > 0)
+			if (dev_info.at("connection_id") == search_info.at("connection_id"))
+				return dev;
+
+		// Last resort: vendor/model/version
+		if (dev_info.count("version") > 0 &&
+			search_info.count("version") > 0)
+			if (dev_info.at("version") == search_info.at("version") &&
+					dev_info.at("version") != "0")
+				return dev;
+
+		// For this device, we merely have a vendor/model match.
+		last_resort_dev = dev;
 	}
 
-	// Clear all the old device instances from this driver
-	sr_dev_clear(driver);
+	// If there wasn't even a vendor/model/version match, we end up here.
+	// This is usually the case for devices with only vendor/model data.
+	// The selected device may be wrong with multiple such devices attached
+	// but it is the best we can do at this point. After all, there may be
+	// only one such device and we do want to select it in this case.
+	return last_resort_dev;
 }
 
-bool DeviceManager::compare_devices(shared_ptr<device::Device> a,
-	shared_ptr<device::Device> b)
+bool DeviceManager::compare_devices(shared_ptr<devices::Device> a,
+	shared_ptr<devices::Device> b)
 {
 	assert(a);
 	assert(b);
-	return a->format_device_title().compare(b->format_device_title()) < 0;
+	return a->display_name(*this).compare(b->display_name(*this)) < 0;
 }
 
 } // namespace pv
diff --git a/pv/devicemanager.h b/pv/devicemanager.h
deleted file mode 100644
index acef8a4..0000000
--- a/pv/devicemanager.h
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * 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/devicemanager.hpp b/pv/devicemanager.hpp
new file mode 100644
index 0000000..33e784d
--- /dev/null
+++ b/pv/devicemanager.hpp
@@ -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
+ */
+
+#ifndef PULSEVIEW_PV_DEVICEMANAGER_HPP
+#define PULSEVIEW_PV_DEVICEMANAGER_HPP
+
+#include <list>
+#include <map>
+#include <memory>
+#include <string>
+
+namespace Glib {
+class VariantBase;
+}
+
+namespace sigrok {
+class ConfigKey;
+class Context;
+class Driver;
+}
+
+namespace pv {
+
+namespace devices {
+class Device;
+class HardwareDevice;
+}
+
+class Session;
+
+class DeviceManager
+{
+public:
+	DeviceManager(std::shared_ptr<sigrok::Context> context);
+
+	~DeviceManager();
+
+	const std::shared_ptr<sigrok::Context>& context() const;
+
+	std::shared_ptr<sigrok::Context> context();
+
+	const std::list< std::shared_ptr<devices::HardwareDevice> >&
+		devices() const;
+
+	std::list< std::shared_ptr<devices::HardwareDevice> > driver_scan(
+		std::shared_ptr<sigrok::Driver> driver,
+		std::map<const sigrok::ConfigKey *, Glib::VariantBase> drvopts);
+
+	const std::map<std::string, std::string> get_device_info(
+		const std::shared_ptr<devices::Device> device);
+
+	const std::shared_ptr<devices::HardwareDevice> find_device_from_info(
+		const std::map<std::string, std::string> search_info);
+
+private:
+	bool compare_devices(std::shared_ptr<devices::Device> a,
+		std::shared_ptr<devices::Device> b);
+
+protected:
+	std::shared_ptr<sigrok::Context> context_;
+	std::list< std::shared_ptr<devices::HardwareDevice> > devices_;
+};
+
+} // namespace pv
+
+#endif // PULSEVIEW_PV_DEVICEMANAGER_HPP
diff --git a/pv/devices/device.cpp b/pv/devices/device.cpp
new file mode 100644
index 0000000..403acc4
--- /dev/null
+++ b/pv/devices/device.cpp
@@ -0,0 +1,99 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2015 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 <libsigrokcxx/libsigrokcxx.hpp>
+
+#include "device.hpp"
+
+using std::map;
+using std::set;
+
+using sigrok::ConfigKey;
+using sigrok::Capability;
+using sigrok::Error;
+
+using Glib::VariantBase;
+using Glib::Variant;
+
+namespace pv {
+namespace devices {
+
+Device::Device()
+{
+}
+
+Device::~Device()
+{
+	if (session_)
+		session_->remove_datafeed_callbacks();
+}
+
+std::shared_ptr<sigrok::Session> Device::session() const
+{
+	return session_;
+}
+
+std::shared_ptr<sigrok::Device> Device::device() const
+{
+	return device_;
+}
+
+template
+uint64_t Device::read_config(const sigrok::ConfigKey*,
+	const uint64_t);
+
+template<typename T>
+T Device::read_config(const ConfigKey *key, const T default_value)
+{
+	assert(key);
+
+	if (!device_)
+		return default_value;
+
+	if (!device_->config_check(key, Capability::GET))
+		return default_value;
+
+	return VariantBase::cast_dynamic<Glib::Variant<guint64>>(
+		device_->config_get(ConfigKey::SAMPLERATE)).get();
+}
+
+void Device::start()
+{
+	assert(session_);
+	session_->start();
+}
+
+void Device::run()
+{
+	assert(device_);
+	assert(session_);
+	session_->run();
+}
+
+void Device::stop()
+{
+	assert(session_);
+	session_->stop();
+}
+
+} // namespace devices
+} // namespace pv
diff --git a/pv/devices/device.hpp b/pv/devices/device.hpp
new file mode 100644
index 0000000..acc772e
--- /dev/null
+++ b/pv/devices/device.hpp
@@ -0,0 +1,85 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2015 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_DEVICES_DEVICE_HPP
+#define PULSEVIEW_PV_DEVICES_DEVICE_HPP
+
+#include <memory>
+#include <string>
+
+namespace sigrok {
+class ConfigKey;
+class Device;
+class Session;
+} // namespace sigrok
+
+namespace pv {
+
+class DeviceManager;
+
+namespace devices {
+
+class Device
+{
+protected:
+	Device();
+
+public:
+	virtual ~Device();
+
+	std::shared_ptr<sigrok::Session> session() const;
+
+	std::shared_ptr<sigrok::Device> device() const;
+
+	template<typename T>
+	T read_config(const sigrok::ConfigKey *key, const T default_value = 0);
+
+	/**
+	 * Builds the full name. It only contains all the fields.
+	 */
+	virtual std::string full_name() const = 0;
+
+	/**
+	 * Builds the display name. It only contains fields as required.
+	 * @param device_manager a reference to the device manager is needed
+	 * so that other similarly titled devices can be detected.
+	 */
+	virtual std::string display_name(
+		const DeviceManager &device_manager) const = 0;
+
+	virtual void open() = 0;
+
+	virtual void close() = 0;
+
+	virtual void start();
+
+	virtual void run();
+
+	virtual void stop();
+
+protected:
+	std::shared_ptr<sigrok::Session> session_;
+	std::shared_ptr<sigrok::Device> device_;
+};
+
+} // namespace devices
+} // namespace pv
+
+#endif // PULSEVIEW_PV_DEVICES_DEVICE_HPP
diff --git a/pv/data/signaldata.cpp b/pv/devices/file.cpp
similarity index 66%
copy from pv/data/signaldata.cpp
copy to pv/devices/file.cpp
index 04f1d3f..5e1cb85 100644
--- a/pv/data/signaldata.cpp
+++ b/pv/devices/file.cpp
@@ -1,7 +1,7 @@
 /*
  * This file is part of the PulseView project.
  *
- * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ * Copyright (C) 2015 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
@@ -18,32 +18,27 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "signaldata.h"
+#include <boost/filesystem.hpp>
 
-namespace pv {
-namespace data {
+#include "file.hpp"
 
-SignalData::SignalData() :
-	_start_time(0),
-	_samplerate(0)
-{
-}
+namespace pv {
+namespace devices {
 
-double SignalData::samplerate() const
+File::File(const std::string &file_name) :
+	file_name_(file_name)
 {
-	return _samplerate;
 }
 
-void SignalData::set_samplerate(double samplerate)
+std::string File::full_name() const
 {
-	_samplerate = samplerate;
-	clear();
+	return boost::filesystem::path(file_name_).filename().string();
 }
 
-double SignalData::get_start_time() const
+std::string File::display_name(const DeviceManager&) const
 {
-	return _start_time;
+	return File::full_name();
 }
 
-} // namespace data
+} // namespace devices
 } // namespace pv
diff --git a/pv/device/file.h b/pv/devices/file.hpp
similarity index 59%
rename from pv/device/file.h
rename to pv/devices/file.hpp
index 4a45a82..8a8c870 100644
--- a/pv/device/file.h
+++ b/pv/devices/file.hpp
@@ -1,7 +1,7 @@
 /*
  * This file is part of the PulseView project.
  *
- * Copyright (C) 2014 Joel Holdsworth <joel at airwebreathe.org.uk>
+ * Copyright (C) 2015 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
@@ -18,32 +18,37 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_DEVICE_FILE_H
-#define PULSEVIEW_PV_DEVICE_FILE_H
+#ifndef PULSEVIEW_PV_DEVICES_FILE_HPP
+#define PULSEVIEW_PV_DEVICES_FILE_HPP
 
 #include <string>
 
-#include "devinst.h"
+#include "device.hpp"
 
 namespace pv {
-namespace device {
+namespace devices {
 
-class File : public DevInst
+class File : public Device
 {
 protected:
-	File(const std::string path);
+	File(const std::string &file_name);
 
 public:
-	static File* create(const std::string &name);
+	/**
+	 * Builds the full name. It only contains all the fields.
+	 */
+	std::string full_name() const;
 
-public:
-	std::string format_device_title() const;
+	/**
+	 * Builds the display name. It only contains fields as required.
+	 */
+	std::string display_name(const DeviceManager&) const;
 
 protected:
-	const std::string _path;
+	const std::string file_name_;
 };
 
-} // device
-} // pv
+} // namespace devices
+} // namespace pv
 
-#endif // PULSEVIEW_PV_DEVICE_FILE_H
+#endif // PULSEVIEW_PV_DEVICES_FILE_HPP
diff --git a/pv/devices/hardwaredevice.cpp b/pv/devices/hardwaredevice.cpp
new file mode 100644
index 0000000..318ce1c
--- /dev/null
+++ b/pv/devices/hardwaredevice.cpp
@@ -0,0 +1,133 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2015 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/algorithm/string/join.hpp>
+
+#include <QString>
+
+#include <libsigrokcxx/libsigrokcxx.hpp>
+
+#include <pv/devicemanager.hpp>
+
+#include "hardwaredevice.hpp"
+
+using std::dynamic_pointer_cast;
+using std::shared_ptr;
+using std::static_pointer_cast;
+using std::string;
+using std::vector;
+
+using boost::algorithm::join;
+
+using sigrok::HardwareDevice;
+
+namespace pv {
+namespace devices {
+
+HardwareDevice::HardwareDevice(const std::shared_ptr<sigrok::Context> &context,
+	std::shared_ptr<sigrok::HardwareDevice> device) :
+	context_(context),
+	device_open_(false)
+{
+	device_ = device;
+}
+
+HardwareDevice::~HardwareDevice()
+{
+	close();
+}
+
+string HardwareDevice::full_name() const
+{
+	vector<string> parts = {device_->vendor(), device_->model(),
+		device_->version(), device_->serial_number()};
+	if (device_->connection_id().length() > 0)
+		parts.push_back("(" + device_->connection_id() + ")");
+	return join(parts, " ");
+}
+
+shared_ptr<sigrok::HardwareDevice> HardwareDevice::hardware_device() const
+{
+	return static_pointer_cast<sigrok::HardwareDevice>(device_);
+}
+
+string HardwareDevice::display_name(
+	const DeviceManager &device_manager) const
+{
+	const auto hw_dev = hardware_device();
+
+	// If we can find another device with the same model/vendor then
+	// we have at least two such devices and need to distinguish them.
+	const auto &devices = device_manager.devices();
+	const bool multiple_dev = hw_dev && any_of(
+		devices.begin(), devices.end(),
+		[&](shared_ptr<devices::HardwareDevice> dev) {
+			return dev->hardware_device()->vendor() ==
+					hw_dev->vendor() &&
+				dev->hardware_device()->model() ==
+					hw_dev->model() &&
+				dev->device_ != device_;
+		});
+
+	vector<string> parts = {device_->vendor(), device_->model()};
+
+	if (multiple_dev) {
+		parts.push_back(device_->version());
+		parts.push_back(device_->serial_number());
+
+		if ((device_->serial_number().length() == 0) &&
+			(device_->connection_id().length() > 0))
+			parts.push_back("(" + device_->connection_id() + ")");
+	}
+
+	return join(parts, " ");
+}
+
+void HardwareDevice::open()
+{
+	if (device_open_)
+		close();
+
+	try {
+		device_->open();
+	} catch (const sigrok::Error &e) {
+		throw QString(e.what());
+	}
+
+	device_open_ = true;
+
+	// Set up the session
+	session_ = context_->create_session();
+	session_->add_device(device_);
+}
+
+void HardwareDevice::close()
+{
+	if (device_open_)
+		device_->close();
+
+	if (session_)
+		session_->remove_devices();
+
+	device_open_ = false;
+}
+
+} // namespace devices
+} // namespace pv
diff --git a/pv/devices/hardwaredevice.hpp b/pv/devices/hardwaredevice.hpp
new file mode 100644
index 0000000..bc8e47a
--- /dev/null
+++ b/pv/devices/hardwaredevice.hpp
@@ -0,0 +1,68 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2015 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_DEVICES_HARDWAREDEVICE_HPP
+#define PULSEVIEW_PV_DEVICES_HARDWAREDEVICE_HPP
+
+#include "device.hpp"
+
+namespace sigrok {
+class Context;
+class HardwareDevice;
+} // sigrok
+
+namespace pv {
+namespace devices {
+
+class HardwareDevice final : public Device
+{
+public:
+	HardwareDevice(const std::shared_ptr<sigrok::Context> &context,
+		std::shared_ptr<sigrok::HardwareDevice> device);
+
+	~HardwareDevice();
+
+	std::shared_ptr<sigrok::HardwareDevice> hardware_device() const;
+
+	/**
+	 * Builds the full name. It only contains all the fields.
+	 */
+	std::string full_name() const;
+
+	/**
+	 * Builds the display name. It only contains fields as required.
+	 * @param device_manager a reference to the device manager is needed
+	 * so that other similarly titled devices can be detected.
+	 */
+	std::string display_name(const DeviceManager &device_manager) const;
+
+	void open();
+
+	void close();
+
+private:
+	const std::shared_ptr<sigrok::Context> context_;
+	bool device_open_;
+};
+
+} // namespace devices
+} // namespace pv
+
+#endif // PULSEVIEW_PV_DEVICES_HARDWAREDEVICE_HPP
diff --git a/pv/devices/inputfile.cpp b/pv/devices/inputfile.cpp
new file mode 100644
index 0000000..3ddc8ae
--- /dev/null
+++ b/pv/devices/inputfile.cpp
@@ -0,0 +1,106 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2015 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 <fstream>
+
+#include <QString>
+
+#include "inputfile.hpp"
+
+namespace pv {
+namespace devices {
+
+const std::streamsize InputFile::BufferSize = 16384;
+
+InputFile::InputFile(const std::shared_ptr<sigrok::Context> &context,
+	const std::string &file_name,
+	std::shared_ptr<sigrok::InputFormat> format,
+	const std::map<std::string, Glib::VariantBase> &options) :
+	File(file_name),
+	context_(context),
+	input_(format->create_input(options)),
+	interrupt_(false)
+{
+	if (!input_)
+		throw QString("Failed to create input");
+}
+
+void InputFile::open()
+{
+	if (session_)
+		close();
+
+	session_ = context_->create_session();
+}
+
+void InputFile::close()
+{
+	if (session_)
+		session_->remove_devices();
+}
+
+void InputFile::start()
+{
+}
+
+void InputFile::run()
+{
+	char buffer[BufferSize];
+	bool need_device = true;
+
+	assert(session_);
+	assert(input_);
+
+	interrupt_ = false;
+	std::ifstream f(file_name_);
+	while (!interrupt_ && f) {
+		f.read(buffer, BufferSize);
+		const std::streamsize size = f.gcount();
+		if (size == 0)
+			break;
+
+		input_->send(buffer, size);
+
+		if (need_device) {
+			try {
+				device_ = input_->device();
+			} catch (sigrok::Error) {
+				break;
+			}
+
+			session_->add_device(device_);
+			need_device = false;
+		}
+
+		if (size != BufferSize)
+			break;
+	}
+
+	input_->end();
+}
+
+void InputFile::stop()
+{
+	interrupt_ = true;
+}
+
+} // namespace devices
+} // namespace pv
diff --git a/pv/view/viewport.h b/pv/devices/inputfile.hpp
similarity index 51%
rename from pv/view/viewport.h
rename to pv/devices/inputfile.hpp
index 0474b7e..9b9aee8 100644
--- a/pv/view/viewport.h
+++ b/pv/devices/inputfile.hpp
@@ -1,7 +1,7 @@
 /*
  * This file is part of the PulseView project.
  *
- * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ * Copyright (C) 2015 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
@@ -18,51 +18,48 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_VIEW_VIEWPORT_H
-#define PULSEVIEW_PV_VIEW_VIEWPORT_H
+#ifndef PULSEVIEW_PV_DEVICE_INPUTFILE_HPP
+#define PULSEVIEW_PV_DEVICE_INPUTFILE_HPP
 
-#include <QTimer>
-#include <QWidget>
+#include <atomic>
 
-class QPainter;
-class QPaintEvent;
-class SigSession;
+#include <libsigrokcxx/libsigrokcxx.hpp>
 
-namespace pv {
-namespace view {
+#include "file.hpp"
 
-class View;
+namespace pv {
+namespace devices {
 
-class Viewport : public QWidget
+class InputFile final : public File
 {
-	Q_OBJECT
+private:
+	static const std::streamsize BufferSize;
 
 public:
-	explicit Viewport(View &parent);
+	InputFile(const std::shared_ptr<sigrok::Context> &context,
+		const std::string &file_name,
+		std::shared_ptr<sigrok::InputFormat> format,
+		const std::map<std::string, Glib::VariantBase> &options);
 
-	int get_total_height() const;
+	void open();
 
-protected:
-	void paintEvent(QPaintEvent *event);
+	void close();
 
-private:
-	void mousePressEvent(QMouseEvent *event);
-	void mouseMoveEvent(QMouseEvent *event);
-	void mouseDoubleClickEvent(QMouseEvent * event);
-	void wheelEvent(QWheelEvent *event);
+	void start();
+
+	void run();
 
-private slots:
-	void on_signals_changed();
-	void on_signals_moved();
+	void stop();
 
 private:
-	View &_view;
+	const std::shared_ptr<sigrok::Context> context_;
+	const std::shared_ptr<sigrok::Input> input_;
 
-	QPoint _mouse_down_point;
-	double _mouse_down_offset;
+	std::atomic<bool> interrupt_;
 };
 
-} // namespace view
+} // namespace devices
 } // namespace pv
 
-#endif // PULSEVIEW_PV_VIEW_VIEWPORT_H
+#endif // PULSEVIEW_PV_SESSIONS_INPUTFILE_HPP
+
diff --git a/pv/data/signaldata.cpp b/pv/devices/sessionfile.cpp
similarity index 59%
copy from pv/data/signaldata.cpp
copy to pv/devices/sessionfile.cpp
index 04f1d3f..8a583ee 100644
--- a/pv/data/signaldata.cpp
+++ b/pv/devices/sessionfile.cpp
@@ -1,7 +1,7 @@
 /*
  * This file is part of the PulseView project.
  *
- * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ * Copyright (C) 2015 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
@@ -18,32 +18,36 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "signaldata.h"
+#include <libsigrokcxx/libsigrokcxx.hpp>
+
+#include <boost/filesystem.hpp>
+
+#include "sessionfile.hpp"
 
 namespace pv {
-namespace data {
+namespace devices {
 
-SignalData::SignalData() :
-	_start_time(0),
-	_samplerate(0)
+SessionFile::SessionFile(const std::shared_ptr<sigrok::Context> context,
+	const std::string &file_name) :
+	File(file_name),
+	context_(context)
 {
 }
 
-double SignalData::samplerate() const
+void SessionFile::open()
 {
-	return _samplerate;
-}
+	if (session_)
+		close();
 
-void SignalData::set_samplerate(double samplerate)
-{
-	_samplerate = samplerate;
-	clear();
+	session_ = context_->load_session(file_name_);
+	device_ = session_->devices()[0];
 }
 
-double SignalData::get_start_time() const
+void SessionFile::close()
 {
-	return _start_time;
+	if (session_)
+		session_->remove_devices();
 }
 
-} // namespace data
+} // namespace devices
 } // namespace pv
diff --git a/pv/dialogs/about.h b/pv/devices/sessionfile.hpp
similarity index 62%
copy from pv/dialogs/about.h
copy to pv/devices/sessionfile.hpp
index cc153ad..9494571 100644
--- a/pv/dialogs/about.h
+++ b/pv/devices/sessionfile.hpp
@@ -1,7 +1,7 @@
 /*
  * This file is part of the PulseView project.
  *
- * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ * Copyright (C) 2015 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
@@ -18,36 +18,35 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_ABOUT_H
-#define PULSEVIEW_PV_ABOUT_H
-
-#include <QDialog>
+#ifndef PULSEVIEW_PV_DEVICES_SESSIONFILE_HPP
+#define PULSEVIEW_PV_DEVICES_SESSIONFILE_HPP
 
 #include <memory>
 
-class QTextDocument;
+#include "file.hpp"
 
-namespace Ui {
-class About;
-}
+namespace sigrok {
+class Context;
+} // sigrok
 
 namespace pv {
-namespace dialogs {
+namespace devices {
 
-class About : public QDialog
+class SessionFile final : public File
 {
-	Q_OBJECT
-
 public:
-	explicit About(QWidget *parent = 0);
-	~About();
+	SessionFile(const std::shared_ptr<sigrok::Context> context,
+		const std::string &file_name);
+
+	void open();
+
+	void close();
 
 private:
-	Ui::About *ui;
-	std::auto_ptr<QTextDocument> supportedDoc;
+	const std::shared_ptr<sigrok::Context> context_;
 };
 
-} // namespace dialogs
+} // namespace devices
 } // namespace pv
 
-#endif // PULSEVIEW_PV_ABOUT_H
+#endif // PULSEVIEW_PV_DEVICES_SESSIONFILE_HPP
diff --git a/pv/dialogs/about.cpp b/pv/dialogs/about.cpp
index fe0f16a..20a0299 100644
--- a/pv/dialogs/about.cpp
+++ b/pv/dialogs/about.cpp
@@ -24,26 +24,21 @@
 
 #include <QTextDocument>
 
-#include "about.h"
+#include "about.hpp"
 #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>
+#include <libsigrokcxx/libsigrokcxx.hpp>
 
+using std::shared_ptr;
+using sigrok::Context;
 
 namespace pv {
 namespace dialogs {
 
-About::About(QWidget *parent) :
+About::About(shared_ptr<Context> context, 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
@@ -66,31 +61,28 @@ About::About(QWidget *parent) :
 	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) {
+	for (auto entry : context->drivers()) {
 		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)));
+			 .arg(QString::fromUtf8(entry.first.c_str()))
+			 .arg(QString::fromUtf8(entry.second->long_name().c_str())));
 	}
 
 	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) {
+	for (auto entry : context->input_formats()) {
 		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)));
+			 .arg(QString::fromUtf8(entry.first.c_str()))
+			 .arg(QString::fromUtf8(entry.second->description().c_str())));
 	}
 
 	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) {
+	for (auto entry : context->output_formats()) {
 		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)));
+			 .arg(QString::fromUtf8(entry.first.c_str()))
+			 .arg(QString::fromUtf8(entry.second->description().c_str())));
 	}
 
 #ifdef ENABLE_DECODE
@@ -107,9 +99,9 @@ About::About(QWidget *parent) :
 
 	s.append("</table>");
 
-	supportedDoc.reset(new QTextDocument(this));
+	supportedDoc = new QTextDocument(this);
 	supportedDoc->setHtml(s);
-	ui->supportList->setDocument(supportedDoc.get());
+	ui->supportList->setDocument(supportedDoc);
 }
 
 About::~About()
diff --git a/pv/dialogs/about.h b/pv/dialogs/about.hpp
similarity index 81%
copy from pv/dialogs/about.h
copy to pv/dialogs/about.hpp
index cc153ad..1baa603 100644
--- a/pv/dialogs/about.h
+++ b/pv/dialogs/about.hpp
@@ -18,8 +18,8 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_ABOUT_H
-#define PULSEVIEW_PV_ABOUT_H
+#ifndef PULSEVIEW_PV_ABOUT_HPP
+#define PULSEVIEW_PV_ABOUT_HPP
 
 #include <QDialog>
 
@@ -27,6 +27,10 @@
 
 class QTextDocument;
 
+namespace sigrok {
+	class Context;
+}
+
 namespace Ui {
 class About;
 }
@@ -39,15 +43,15 @@ class About : public QDialog
 	Q_OBJECT
 
 public:
-	explicit About(QWidget *parent = 0);
+	explicit About(std::shared_ptr<sigrok::Context> context, QWidget *parent = 0);
 	~About();
 
 private:
 	Ui::About *ui;
-	std::auto_ptr<QTextDocument> supportedDoc;
+	QTextDocument *supportedDoc;
 };
 
 } // namespace dialogs
 } // namespace pv
 
-#endif // PULSEVIEW_PV_ABOUT_H
+#endif // PULSEVIEW_PV_ABOUT_HPP
diff --git a/pv/dialogs/about.ui b/pv/dialogs/about.ui
index 10163f9..11450f7 100644
--- a/pv/dialogs/about.ui
+++ b/pv/dialogs/about.ui
@@ -28,7 +28,7 @@
         <string/>
        </property>
        <property name="pixmap">
-        <pixmap resource="pulseview.qrc">:/icons/sigrok-logo-notext.png</pixmap>
+        <pixmap resource="pulseview.qrc">:/icons/sigrok-logo-notext.svg</pixmap>
        </property>
       </widget>
      </item>
diff --git a/pv/dialogs/connect.cpp b/pv/dialogs/connect.cpp
index d5ab9c4..4d33cc9 100644
--- a/pv/dialogs/connect.cpp
+++ b/pv/dialogs/connect.cpp
@@ -18,95 +18,87 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include <boost/foreach.hpp>
+#include <cassert>
 
-#include <libsigrok/libsigrok.h>
+#include <libsigrokcxx/libsigrokcxx.hpp>
 
-#include "connect.h"
+#include "connect.hpp"
 
-#include "pv/devicemanager.h"
-#include "pv/device/device.h"
+#include <pv/devicemanager.hpp>
+#include <pv/devices/hardwaredevice.hpp>
 
-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::map;
+using std::shared_ptr;
 using std::string;
 
-extern sr_context *sr_ctx;
+using Glib::ustring;
+using Glib::Variant;
+using Glib::VariantBase;
+
+using sigrok::ConfigKey;
+using sigrok::Driver;
+using sigrok::Error;
+
+using pv::devices::HardwareDevice;
 
 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,
+	device_manager_(device_manager),
+	layout_(this),
+	form_(this),
+	form_layout_(&form_),
+	drivers_(&form_),
+	serial_devices_(&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()));
+	connect(&button_box_, SIGNAL(accepted()), this, SLOT(accept()));
+	connect(&button_box_, SIGNAL(rejected()), this, SLOT(reject()));
 
 	populate_drivers();
-	connect(&_drivers, SIGNAL(activated(int)),
+	connect(&drivers_, SIGNAL(activated(int)),
 		this, SLOT(device_selected(int)));
 
-	_form.setLayout(&_form_layout);
-	_form_layout.addRow(tr("Driver"), &_drivers);
+	form_.setLayout(&form_layout_);
+	form_layout_.addRow(tr("&Driver"), &drivers_);
 
-	_form_layout.addRow(tr("Serial Port"), &_serial_device);
+	form_layout_.addRow(tr("Serial &Port"), &serial_devices_);
+	serial_devices_.setEditable(true);
 
 	unset_connection();
 
-	connect(&_scan_button, SIGNAL(pressed()),
+	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);
+	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
+shared_ptr<HardwareDevice> Connect::get_selected_device() const
 {
-	const QListWidgetItem *const item = _device_list.currentItem();
+	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 shared_ptr<HardwareDevice>();
 
-	return (*iter).second;
+	return item->data(Qt::UserRole).value<shared_ptr<HardwareDevice>>();
 }
 
 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) {
+	for (auto entry : device_manager_.context()->drivers()) {
+		auto name = entry.first;
+		auto driver = entry.second;
 		/**
 		 * We currently only support devices that can deliver
 		 * samples at a fixed samplerate i.e. oscilloscopes and
@@ -114,127 +106,99 @@ void Connect::populate_drivers()
 		 * @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;
-				}
-		}
+		const auto keys = driver->config_keys();
+
+		bool supported_device = keys.count(ConfigKey::LOGIC_ANALYZER) |
+			keys.count(ConfigKey::OSCILLOSCOPE);
 
 		if (supported_device)
-			_drivers.addItem(QString("%1 (%2)").arg(
-				drivers[i]->longname).arg(drivers[i]->name),
-				qVariantFromValue((void*)drivers[i]));
+			drivers_.addItem(QString("%1 (%2)").arg(
+				driver->long_name().c_str()).arg(name.c_str()),
+				qVariantFromValue(driver));
 	}
 }
 
+void Connect::populate_serials(shared_ptr<Driver> driver)
+{
+	serial_devices_.clear();
+	for (auto serial : device_manager_.context()->serials(driver))
+		serial_devices_.addItem(QString("%1 (%2)").arg(
+			serial.first.c_str()).arg(serial.second.c_str()),
+			QString::fromStdString(serial.first));
+}
+
 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);
+	device_list_.clear();
+	serial_devices_.hide();
+	form_layout_.labelForField(&serial_devices_)->hide();
+	button_box_.button(QDialogButtonBox::Ok)->setDisabled(true);
 }
 
-void Connect::set_serial_connection()
+void Connect::set_serial_connection(shared_ptr<Driver> driver)
 {
-	_serial_device.show();
-	_form_layout.labelForField(&_serial_device)->show();
+	populate_serials(driver);
+	serial_devices_.show();
+	form_layout_.labelForField(&serial_devices_)->show();
 }
 
 void Connect::scan_pressed()
 {
-	_device_list.clear();
-	_device_map.clear();
+	device_list_.clear();
 
-	const int index = _drivers.currentIndex();
+	const int index = drivers_.currentIndex();
 	if (index == -1)
 		return;
 
-	sr_dev_driver *const driver = (sr_dev_driver*)_drivers.itemData(
-		index).value<void*>();
+	shared_ptr<Driver> driver =
+		drivers_.itemData(index).value<shared_ptr<Driver>>();
 
-	GSList *drvopts = NULL;
+	assert(driver);
 
-	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);
+	map<const ConfigKey *, VariantBase> drvopts;
 
-	g_slist_free_full(drvopts, (GDestroyNotify)free_drvopts);
+	if (serial_devices_.isVisible()) {
+		QString serial;
+		const int index = serial_devices_.currentIndex();
+		if (index >= 0 && index < serial_devices_.count() &&
+		    serial_devices_.currentText() == serial_devices_.itemText(index))
+			serial = serial_devices_.itemData(index).value<QString>();
+		else
+			serial = serial_devices_.currentText();
+		drvopts[ConfigKey::CONN] = Variant<ustring>::create(
+			serial.toUtf8().constData());
+	}
 
-	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 list< shared_ptr<HardwareDevice> > devices =
+		device_manager_.driver_scan(driver, drvopts);
 
-		const string title = dev_inst->format_device_title();
-		QString text = QString::fromUtf8(title.c_str());
+	for (shared_ptr<HardwareDevice> device : devices) {
+		assert(device);
 
-		if (sdi->channels) {
-			text += QString(" with %1 channels").arg(
-				g_slist_length(sdi->channels));
-		}
+		QString text = QString::fromStdString(
+			device->display_name(device_manager_));
+		text += QString(" with %1 channels").arg(
+			device->device()->channels().size());
 
 		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_);
+		item->setData(Qt::UserRole, qVariantFromValue(device));
+		device_list_.addItem(item);
 	}
 
-	_device_list.setCurrentRow(0);
-	_button_box.button(QDialogButtonBox::Ok)->setDisabled(_device_list.count() == 0);
+	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*>();
+	shared_ptr<Driver> driver =
+		drivers_.itemData(index).value<shared_ptr<Driver>>();
 
 	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);
+	if (driver->scan_options().count(ConfigKey::SERIALCOMM))
+		set_serial_connection(driver);
 }
 
 } // namespace dialogs
diff --git a/pv/dialogs/connect.h b/pv/dialogs/connect.hpp
similarity index 63%
rename from pv/dialogs/connect.h
rename to pv/dialogs/connect.hpp
index 1de0ed8..a8f793a 100644
--- a/pv/dialogs/connect.h
+++ b/pv/dialogs/connect.hpp
@@ -18,10 +18,10 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_CONNECT_H
-#define PULSEVIEW_PV_CONNECT_H
+#ifndef PULSEVIEW_PV_CONNECT_HPP
+#define PULSEVIEW_PV_CONNECT_HPP
 
-#include <boost/shared_ptr.hpp>
+#include <memory>
 
 #include <QComboBox>
 #include <QDialog>
@@ -32,16 +32,22 @@
 #include <QPushButton>
 #include <QVBoxLayout>
 
-struct sr_config;
-struct sr_dev_inst;
+namespace sigrok {
+class Driver;
+}
 
 namespace pv {
+namespace devices {
+class HardwareDevice;
+}
+}
 
-class DeviceManager;
+Q_DECLARE_METATYPE(std::shared_ptr<sigrok::Driver>);
+Q_DECLARE_METATYPE(std::shared_ptr<pv::devices::HardwareDevice>);
 
-namespace device {
-class Device;
-}
+namespace pv {
+
+class DeviceManager;
 
 namespace dialogs {
 
@@ -52,44 +58,41 @@ class Connect : public QDialog
 public:
 	Connect(QWidget *parent, pv::DeviceManager &device_manager);
 
-	boost::shared_ptr<device::Device> get_selected_device() const;
+	std::shared_ptr<devices::HardwareDevice> get_selected_device() const;
 
 private:
 	void populate_drivers();
 
+	void populate_serials(std::shared_ptr<sigrok::Driver> driver);
+
 	void unset_connection();
 
-	void set_serial_connection();
+	void set_serial_connection(std::shared_ptr<sigrok::Driver> driver);
 
-private slots:
+private Q_SLOTS:
 	void device_selected(int index);
 
 	void scan_pressed();
 
 private:
-	static void free_drvopts(sr_config *src);
-
-private:
-	pv::DeviceManager &_device_manager;
+	pv::DeviceManager &device_manager_;
 
-	QVBoxLayout _layout;
+	QVBoxLayout layout_;
 
-	QWidget _form;
-	QFormLayout _form_layout;
+	QWidget form_;
+	QFormLayout form_layout_;
 
-	QComboBox _drivers;
+	QComboBox drivers_;
 
-	QLineEdit _serial_device;
+	QComboBox serial_devices_;
 
-	QPushButton _scan_button;
-	QListWidget _device_list;
-	std::map<const sr_dev_inst*, boost::shared_ptr<pv::device::Device> >
-		_device_map;
+	QPushButton scan_button_;
+	QListWidget device_list_;
 
-	QDialogButtonBox _button_box;
+	QDialogButtonBox button_box_;
 };
 
 } // namespace dialogs
 } // namespace pv
 
-#endif // PULSEVIEW_PV_CONNECT_H
+#endif // PULSEVIEW_PV_CONNECT_HPP
diff --git a/pv/dialogs/about.h b/pv/dialogs/inputoutputoptions.cpp
similarity index 50%
rename from pv/dialogs/about.h
rename to pv/dialogs/inputoutputoptions.cpp
index cc153ad..8f013a2 100644
--- a/pv/dialogs/about.h
+++ b/pv/dialogs/inputoutputoptions.cpp
@@ -18,36 +18,52 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_ABOUT_H
-#define PULSEVIEW_PV_ABOUT_H
+#include "inputoutputoptions.hpp"
 
-#include <QDialog>
+#include <pv/prop/property.hpp>
 
-#include <memory>
+using std::map;
+using std::shared_ptr;
+using std::string;
 
-class QTextDocument;
+using Glib::VariantBase;
 
-namespace Ui {
-class About;
-}
+using sigrok::Option;
 
 namespace pv {
 namespace dialogs {
 
-class About : public QDialog
+InputOutputOptions::InputOutputOptions(const QString &title,
+	const map<string, shared_ptr<Option>> &options, QWidget *parent) :
+	QDialog(parent),
+	layout_(this),
+	button_box_(QDialogButtonBox::Ok | QDialogButtonBox::Cancel,
+		Qt::Horizontal, this),
+	binding_(options)
 {
-	Q_OBJECT
+	setWindowTitle(title);
+
+	connect(&button_box_, SIGNAL(accepted()), this, SLOT(accept()));
+	connect(&button_box_, SIGNAL(rejected()), this, SLOT(reject()));
+
+	setLayout(&layout_);
 
-public:
-	explicit About(QWidget *parent = 0);
-	~About();
+	layout_.addWidget(binding_.get_property_form(this));
+	layout_.addWidget(&button_box_);
+}
 
-private:
-	Ui::About *ui;
-	std::auto_ptr<QTextDocument> supportedDoc;
-};
+const map<string, VariantBase>& InputOutputOptions::options() const
+{
+	return binding_.options();
+}
+
+void InputOutputOptions::accept()
+{
+	QDialog::accept();
+
+	// Commit the properties
+	binding_.commit();
+}
 
 } // namespace dialogs
 } // namespace pv
-
-#endif // PULSEVIEW_PV_ABOUT_H
diff --git a/pv/dialogs/inputoutputoptions.hpp b/pv/dialogs/inputoutputoptions.hpp
new file mode 100644
index 0000000..cde778c
--- /dev/null
+++ b/pv/dialogs/inputoutputoptions.hpp
@@ -0,0 +1,71 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2015 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_INPUTOUTPUTOPTIONS_HPP
+#define PULSEVIEW_PV_DIALOGS_INPUTOUTPUTOPTIONS_HPP
+
+#include <QDialog>
+#include <QDialogButtonBox>
+#include <QGroupBox>
+#include <QVBoxLayout>
+
+#include <pv/binding/inputoutput.hpp>
+
+namespace pv {
+namespace dialogs {
+
+/**
+ * Presents the selection of inpout/output options to the user.
+ */
+class InputOutputOptions : public QDialog
+{
+	Q_OBJECT
+
+public:
+	/**
+	 * Constructor.
+	 * @param title the title of the dialog.
+	 * @param options the map of options to use as a template.
+	 * @param parent the parent widget of the dialog.
+	 */ 
+	InputOutputOptions(const QString &title,
+		const std::map<std::string, std::shared_ptr<sigrok::Option>>
+			&options,
+		QWidget *parent);
+
+	/**
+	 * Gets the map of selected options.
+	 * @return the options.
+	 */
+	const std::map<std::string, Glib::VariantBase>& options() const;
+
+protected:
+	void accept();
+
+private:
+	QVBoxLayout layout_;
+	QDialogButtonBox button_box_;
+	pv::binding::InputOutput binding_;
+};
+
+} // namespace dialogs
+} // namespace pv
+
+#endif // PULSEVIEW_PV_INPUTOUTPUTOPTIONS_HPP
diff --git a/pv/dialogs/storeprogress.cpp b/pv/dialogs/storeprogress.cpp
index 7853173..5aeafd1 100644
--- a/pv/dialogs/storeprogress.cpp
+++ b/pv/dialogs/storeprogress.cpp
@@ -18,30 +18,41 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "storeprogress.h"
+#include <cassert>
 
 #include <QMessageBox>
 
+#include "storeprogress.hpp"
+
+using std::map;
+using std::string;
+
+using Glib::VariantBase;
+
 namespace pv {
 namespace dialogs {
 
 StoreProgress::StoreProgress(const QString &file_name,
-	const SigSession &session, QWidget *parent) :
+	const std::shared_ptr<sigrok::OutputFormat> output_format,
+	const map<string, VariantBase> &options,
+	const std::pair<uint64_t, uint64_t> sample_range,
+	const Session &session, QWidget *parent) :
 	QProgressDialog(tr("Saving..."), tr("Cancel"), 0, 0, parent),
-	_session(file_name.toStdString(), session)
+	session_(file_name.toStdString(), output_format, options, sample_range,
+		session)
 {
-	connect(&_session, SIGNAL(progress_updated()),
+	connect(&session_, SIGNAL(progress_updated()),
 		this, SLOT(on_progress_updated()));
 }
 
 StoreProgress::~StoreProgress()
 {
-	_session.wait();
+	session_.wait();
 }
 
 void StoreProgress::run()
 {
-	if (_session.start())
+	if (session_.start())
 		show();
 	else
 		show_error();
@@ -51,7 +62,7 @@ void StoreProgress::show_error()
 {
 	QMessageBox msg(parentWidget());
 	msg.setText(tr("Failed to save session."));
-	msg.setInformativeText(_session.error());
+	msg.setInformativeText(session_.error());
 	msg.setStandardButtons(QMessageBox::Ok);
 	msg.setIcon(QMessageBox::Warning);
 	msg.exec();
@@ -59,25 +70,23 @@ void StoreProgress::show_error()
 
 void StoreProgress::closeEvent(QCloseEvent*)
 {
-	_session.cancel();
+	session_.cancel();
 }
 
 void StoreProgress::on_progress_updated()
 {
-	const std::pair<uint64_t, uint64_t> p = _session.progress();
+	const std::pair<int, int> p = session_.progress();
 	assert(p.first <= p.second);
 
-	setValue(p.first);
-	setMaximum(p.second);
-
-	const QString err = _session.error();
-	if (!err.isEmpty()) {
-		show_error();
+	if (p.second) {
+		setValue(p.first);
+		setMaximum(p.second);
+	} else {
+		const QString err = session_.error();
+		if (!err.isEmpty())
+			show_error();
 		close();
 	}
-
-	if (p.first == p.second)
-		close();
 }
 
 } // dialogs
diff --git a/pv/dialogs/storeprogress.h b/pv/dialogs/storeprogress.hpp
similarity index 70%
rename from pv/dialogs/storeprogress.h
rename to pv/dialogs/storeprogress.hpp
index 61d08d5..3791642 100644
--- a/pv/dialogs/storeprogress.h
+++ b/pv/dialogs/storeprogress.hpp
@@ -18,20 +18,19 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_DIALOGS_SAVEPROGRESS_H
-#define PULSEVIEW_PV_DIALOGS_SAVEPROGRESS_H
+#ifndef PULSEVIEW_PV_DIALOGS_SAVEPROGRESS_HPP
+#define PULSEVIEW_PV_DIALOGS_SAVEPROGRESS_HPP
 
+#include <memory>
 #include <set>
 
-#include <boost/shared_ptr.hpp>
-
 #include <QProgressDialog>
 
-#include <pv/storesession.h>
+#include <pv/storesession.hpp>
 
 namespace pv {
 
-class SigSession;
+class Session;
 
 namespace dialogs {
 
@@ -40,7 +39,11 @@ class StoreProgress : public QProgressDialog
 	Q_OBJECT
 
 public:
-	StoreProgress(const QString &file_name, const SigSession &session,
+	StoreProgress(const QString &file_name,
+		const std::shared_ptr<sigrok::OutputFormat> output_format,
+		const std::map<std::string, Glib::VariantBase> &options,
+		const std::pair<uint64_t, uint64_t> sample_range,
+		const Session &session,
 		QWidget *parent = 0);
 
 	virtual ~StoreProgress();
@@ -52,14 +55,14 @@ private:
 
 	void closeEvent(QCloseEvent*);
 
-private slots:
+private Q_SLOTS:
 	void on_progress_updated();
 
 private:
-	pv::StoreSession _session;
+	pv::StoreSession session_;
 };
 
 } // dialogs
 } // pv
 
-#endif // PULSEVIEW_PV_DIALOGS_SAVEPROGRESS_H
+#endif // PULSEVIEW_PV_DIALOGS_SAVEPROGRESS_HPP
diff --git a/pv/mainwindow.cpp b/pv/mainwindow.cpp
index 0089b11..912ed60 100644
--- a/pv/mainwindow.cpp
+++ b/pv/mainwindow.cpp
@@ -18,227 +18,508 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
+#include <cassert>
+
 #ifdef ENABLE_DECODE
 #include <libsigrokdecode/libsigrokdecode.h>
 #endif
 
-#include <boost/bind.hpp>
-#include <boost/foreach.hpp>
-
 #include <algorithm>
 #include <iterator>
 
+#include <boost/algorithm/string/join.hpp>
+
 #include <QAction>
 #include <QApplication>
 #include <QButtonGroup>
+#include <QCloseEvent>
 #include <QFileDialog>
 #include <QMessageBox>
 #include <QMenu>
 #include <QMenuBar>
+#include <QSettings>
 #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"
+#include "mainwindow.hpp"
+
+#include "devicemanager.hpp"
+#include "util.hpp"
+#include "data/segment.hpp"
+#include "devices/hardwaredevice.hpp"
+#include "devices/inputfile.hpp"
+#include "devices/sessionfile.hpp"
+#include "dialogs/about.hpp"
+#include "dialogs/connect.hpp"
+#include "dialogs/inputoutputoptions.hpp"
+#include "dialogs/storeprogress.hpp"
+#include "toolbars/mainbar.hpp"
+#include "view/logicsignal.hpp"
+#include "view/view.hpp"
+#include "widgets/exportmenu.hpp"
+#include "widgets/importmenu.hpp"
 #ifdef ENABLE_DECODE
-#include "widgets/decodermenu.h"
+#include "widgets/decodermenu.hpp"
 #endif
+#include "widgets/hidingmenubar.hpp"
 
-/* __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>
+#include <libsigrokcxx/libsigrokcxx.hpp>
 
-using boost::shared_ptr;
+using std::cerr;
+using std::endl;
 using std::list;
+using std::map;
+using std::pair;
+using std::shared_ptr;
+using std::string;
+using std::vector;
+
+using boost::algorithm::join;
+
+using sigrok::Error;
+using sigrok::OutputFormat;
+using sigrok::InputFormat;
 
 namespace pv {
 
 namespace view {
-class SelectableItem;
+class ViewItem;
 }
 
+const char *MainWindow::SettingOpenDirectory = "MainWindow/OpenDirectory";
+const char *MainWindow::SettingSaveDirectory = "MainWindow/SaveDirectory";
+
 MainWindow::MainWindow(DeviceManager &device_manager,
-	const char *open_file_name,
+	string open_file_name, string open_file_format,
 	QWidget *parent) :
 	QMainWindow(parent),
-	_device_manager(device_manager),
-	_session(device_manager)
+	device_manager_(device_manager),
+	session_(device_manager),
+	action_open_(new QAction(this)),
+	action_save_as_(new QAction(this)),
+	action_save_selection_as_(new QAction(this)),
+	action_connect_(new QAction(this)),
+	action_quit_(new QAction(this)),
+	action_view_zoom_in_(new QAction(this)),
+	action_view_zoom_out_(new QAction(this)),
+	action_view_zoom_fit_(new QAction(this)),
+	action_view_zoom_one_to_one_(new QAction(this)),
+	action_view_sticky_scrolling_(new QAction(this)),
+	action_view_coloured_bg_(new QAction(this)),
+	action_view_show_cursors_(new QAction(this)),
+	action_about_(new QAction(this))
+#ifdef ENABLE_DECODE
+	, menu_decoders_add_(new pv::widgets::DecoderMenu(this, true))
+#endif
 {
+	qRegisterMetaType<util::Timestamp>("util::Timestamp");
+
 	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));
+	restore_ui_settings();
+	if (open_file_name.empty())
+		select_init_device();
+	else
+		load_init_file(open_file_name, open_file_format);
+}
+
+QAction* MainWindow::action_open() const
+{
+	return action_open_;
+}
+
+QAction* MainWindow::action_save_as() const
+{
+	return action_save_as_;
+}
+
+QAction* MainWindow::action_save_selection_as() const
+{
+	return action_save_selection_as_;
+}
+
+QAction* MainWindow::action_connect() const
+{
+	return action_connect_;
+}
+
+QAction* MainWindow::action_quit() const
+{
+	return action_quit_;
+}
+
+QAction* MainWindow::action_view_zoom_in() const
+{
+	return action_view_zoom_in_;
+}
+
+QAction* MainWindow::action_view_zoom_out() const
+{
+	return action_view_zoom_out_;
+}
+
+QAction* MainWindow::action_view_zoom_fit() const
+{
+	return action_view_zoom_fit_;
+}
+
+QAction* MainWindow::action_view_zoom_one_to_one() const
+{
+	return action_view_zoom_one_to_one_;
+}
+
+QAction* MainWindow::action_view_sticky_scrolling() const
+{
+	return action_view_sticky_scrolling_;
+}
+
+QAction* MainWindow::action_view_coloured_bg() const
+{
+	return action_view_coloured_bg_;
+}
+
+QAction* MainWindow::action_view_show_cursors() const
+{
+	return action_view_show_cursors_;
+}
+
+QAction* MainWindow::action_about() const
+{
+	return action_about_;
+}
+
+#ifdef ENABLE_DECODE
+QMenu* MainWindow::menu_decoder_add() const
+{
+	return menu_decoders_add_;
+}
+#endif
+
+void MainWindow::run_stop()
+{
+	switch (session_.get_capture_state()) {
+	case Session::Stopped:
+		session_.start_capture([&](QString message) {
+			session_error("Capture failed", message); });
+		break;
+	case Session::AwaitingTrigger:
+	case Session::Running:
+		session_.stop_capture();
+		break;
+	}
+}
+
+void MainWindow::select_device(shared_ptr<devices::Device> device)
+{
+	try {
+		if (device)
+			session_.set_device(device);
+		else
+			session_.set_default_device();
+	} catch (const QString &e) {
+		QMessageBox msg(this);
+		msg.setText(e);
+		msg.setInformativeText(tr("Failed to Select Device"));
+		msg.setStandardButtons(QMessageBox::Ok);
+		msg.setIcon(QMessageBox::Warning);
+		msg.exec();
+	}
+}
+
+void MainWindow::export_file(shared_ptr<OutputFormat> format,
+	bool selection_only)
+{
+	using pv::dialogs::StoreProgress;
+
+	// Stop any currently running capture session
+	session_.stop_capture();
+
+	QSettings settings;
+	const QString dir = settings.value(SettingSaveDirectory).toString();
+
+	std::pair<uint64_t, uint64_t> sample_range;
+
+	// Selection only? Verify that the cursors are active and fetch their values
+	if (selection_only) {
+		if (!view_->cursors()->enabled()) {
+			show_session_error(tr("Missing Cursors"), tr("You need to set the " \
+					"cursors before you can save the data enclosed by them " \
+					"to a session file (e.g. using ALT-V - Show Cursors)."));
+			return;
+		}
+
+		const double samplerate = session_.get_samplerate();
+
+		const pv::util::Timestamp& start_time = view_->cursors()->first()->time();
+		const pv::util::Timestamp& end_time = view_->cursors()->second()->time();
+
+		const uint64_t start_sample = start_time.convert_to<double>() * samplerate;
+		const uint64_t end_sample = end_time.convert_to<double>() * samplerate;
+
+		sample_range = std::make_pair(start_sample, end_sample);
+	} else {
+		sample_range = std::make_pair(0, 0);
+	}
+
+	// Construct the filter
+	const vector<string> exts = format->extensions();
+	QString filter = tr("%1 files ").arg(
+		QString::fromStdString(format->description()));
+
+	if (exts.empty())
+		filter += "(*.*)";
+	else
+		filter += QString("(*.%1);;%2 (*.*)").arg(
+			QString::fromStdString(join(exts, ", *."))).arg(
+			tr("All Files"));
+
+	// Show the file dialog
+	const QString file_name = QFileDialog::getSaveFileName(
+		this, tr("Save File"), dir, filter);
+
+	if (file_name.isEmpty())
+		return;
+
+	const QString abs_path = QFileInfo(file_name).absolutePath();
+	settings.setValue(SettingSaveDirectory, abs_path);
+
+	// Show the options dialog
+	map<string, Glib::VariantBase> options;
+	if (!format->options().empty()) {
+		dialogs::InputOutputOptions dlg(
+			tr("Export %1").arg(QString::fromStdString(
+				format->description())),
+			format->options(), this);
+		if (!dlg.exec())
+			return;
+		options = dlg.options();
+	}
+
+	StoreProgress *dlg = new StoreProgress(file_name, format, options,
+		sample_range, session_, this);
+	dlg->run();
+}
+
+void MainWindow::import_file(shared_ptr<InputFormat> format)
+{
+	assert(format);
+
+	QSettings settings;
+	const QString dir = settings.value(SettingOpenDirectory).toString();
+
+	// Construct the filter
+	const vector<string> exts = format->extensions();
+	const QString filter = exts.empty() ? "" :
+		tr("%1 files (*.%2)").arg(
+			QString::fromStdString(format->description())).arg(
+			QString::fromStdString(join(exts, ", *.")));
+
+	// Show the file dialog
+	const QString file_name = QFileDialog::getOpenFileName(
+		this, tr("Import File"), dir, tr(
+			"%1 files (*.*);;All Files (*.*)").arg(
+			QString::fromStdString(format->description())));
+
+	if (file_name.isEmpty())
+		return;
+
+	// Show the options dialog
+	map<string, Glib::VariantBase> options;
+	if (!format->options().empty()) {
+		dialogs::InputOutputOptions dlg(
+			tr("Import %1").arg(QString::fromStdString(
+				format->description())),
+			format->options(), this);
+		if (!dlg.exec())
+			return;
+		options = dlg.options();
 	}
+
+	load_file(file_name, format, options);
+
+	const QString abs_path = QFileInfo(file_name).absolutePath();
+	settings.setValue(SettingOpenDirectory, abs_path);
 }
 
 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);
+	icon.addFile(QString(":/icons/sigrok-logo-notext.svg"));
 	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);
+	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);
+	view_ = new pv::view::View(session_, this);
 
-	_vertical_layout->addWidget(_view);
+	vertical_layout_->addWidget(view_);
 
 	// Setup the menu bar
-	QMenuBar *const menu_bar = new QMenuBar(this);
-	menu_bar->setGeometry(QRect(0, 0, 400, 25));
+	pv::widgets::HidingMenuBar *const menu_bar =
+		new pv::widgets::HidingMenuBar(this);
 
 	// File Menu
 	QMenu *const menu_file = new QMenu;
-	menu_file->setTitle(QApplication::translate(
-		"MainWindow", "&File", 0, QApplication::UnicodeUTF8));
+	menu_file->setTitle(tr("&File"));
 
-	QAction *const action_open = new QAction(this);
-	action_open->setText(QApplication::translate(
-		"MainWindow", "&Open...", 0, QApplication::UnicodeUTF8));
-	action_open->setIcon(QIcon::fromTheme("document-open",
+	action_open_->setText(tr("&Open..."));
+	action_open_->setIcon(QIcon::fromTheme("document-open",
 		QIcon(":/icons/document-open.png")));
-	action_open->setObjectName(QString::fromUtf8("actionOpen"));
-	menu_file->addAction(action_open);
+	action_open_->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_O));
+	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",
+	action_save_as_->setText(tr("&Save As..."));
+	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);
+	action_save_as_->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_S));
+	action_save_as_->setObjectName(QString::fromUtf8("actionSaveAs"));
+	menu_file->addAction(action_save_as_);
+
+	action_save_selection_as_->setText(tr("Save Selected &Range As..."));
+	action_save_selection_as_->setIcon(QIcon::fromTheme("document-save-as",
+		QIcon(":/icons/document-save-as.png")));
+	action_save_selection_as_->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_R));
+	action_save_selection_as_->setObjectName(QString::fromUtf8("actionSaveSelectionAs"));
+	menu_file->addAction(action_save_selection_as_);
+
+	menu_file->addSeparator();
+
+	widgets::ExportMenu *menu_file_export = new widgets::ExportMenu(this,
+		device_manager_.context());
+	menu_file_export->setTitle(tr("&Export"));
+	connect(menu_file_export,
+		SIGNAL(format_selected(std::shared_ptr<sigrok::OutputFormat>)),
+		this, SLOT(export_file(std::shared_ptr<sigrok::OutputFormat>)));
+	menu_file->addAction(menu_file_export->menuAction());
+
+	widgets::ImportMenu *menu_file_import = new widgets::ImportMenu(this,
+		device_manager_.context());
+	menu_file_import->setTitle(tr("&Import"));
+	connect(menu_file_import,
+		SIGNAL(format_selected(std::shared_ptr<sigrok::InputFormat>)),
+		this, SLOT(import_file(std::shared_ptr<sigrok::InputFormat>)));
+	menu_file->addAction(menu_file_import->menuAction());
 
 	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);
+	action_connect_->setText(tr("&Connect to Device..."));
+	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",
+	action_quit_->setText(tr("&Quit"));
+	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);
+	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));
+	menu_view->setTitle(tr("&View"));
 
-	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",
+	action_view_zoom_in_->setText(tr("Zoom &In"));
+	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(
+	action_view_zoom_in_->setShortcut(QKeySequence::ZoomIn);
+	action_view_zoom_in_->setObjectName(
 		QString::fromUtf8("actionViewZoomIn"));
-	menu_view->addAction(action_view_zoom_in);
+	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",
+	action_view_zoom_out_->setText(tr("Zoom &Out"));
+	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(
+	action_view_zoom_out_->setShortcut(QKeySequence::ZoomOut);
+	action_view_zoom_out_->setObjectName(
 		QString::fromUtf8("actionViewZoomOut"));
-	menu_view->addAction(action_view_zoom_out);
+	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",
+	action_view_zoom_fit_->setCheckable(true);
+	action_view_zoom_fit_->setText(tr("Zoom to &Fit"));
+	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(
+	action_view_zoom_fit_->setShortcut(QKeySequence(Qt::Key_F));
+	action_view_zoom_fit_->setObjectName(
 		QString::fromUtf8("actionViewZoomFit"));
-	menu_view->addAction(action_view_zoom_fit);
+	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",
+	action_view_zoom_one_to_one_->setText(tr("Zoom to O&ne-to-One"));
+	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(
+	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->addAction(action_view_zoom_one_to_one_);
+
+	menu_view->addSeparator();
+
+	action_view_sticky_scrolling_->setCheckable(true);
+	action_view_sticky_scrolling_->setChecked(true);
+	action_view_sticky_scrolling_->setShortcut(QKeySequence(Qt::Key_S));
+	action_view_sticky_scrolling_->setObjectName(
+		QString::fromUtf8("actionViewStickyScrolling"));
+	action_view_sticky_scrolling_->setText(tr("&Sticky Scrolling"));
+	menu_view->addAction(action_view_sticky_scrolling_);
+
+	view_->enable_sticky_scrolling(action_view_sticky_scrolling_->isChecked());
 
 	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(
+	action_view_coloured_bg_->setCheckable(true);
+	action_view_coloured_bg_->setChecked(true);
+	action_view_coloured_bg_->setShortcut(QKeySequence(Qt::Key_B));
+	action_view_coloured_bg_->setObjectName(
+		QString::fromUtf8("actionViewColouredBg"));
+	action_view_coloured_bg_->setText(tr("Use &coloured backgrounds"));
+	menu_view->addAction(action_view_coloured_bg_);
+
+	view_->enable_coloured_bg(action_view_coloured_bg_->isChecked());
+
+	menu_view->addSeparator();
+
+	action_view_show_cursors_->setCheckable(true);
+	action_view_show_cursors_->setChecked(view_->cursors_shown());
+	action_view_show_cursors_->setIcon(QIcon::fromTheme("show-cursors",
+		QIcon(":/icons/show-cursors.svg")));
+	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);
+	action_view_show_cursors_->setText(tr("Show &Cursors"));
+	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*)),
+	menu_decoders->setTitle(tr("&Decoders"));
+
+	menu_decoders_add_->setTitle(tr("&Add"));
+	connect(menu_decoders_add_, SIGNAL(decoder_selected(srd_decoder*)),
 		this, SLOT(add_decoder(srd_decoder*)));
 
-	menu_decoders->addMenu(menu_decoders_add);
+	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));
+	menu_help->setTitle(tr("&Help"));
 
-	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);
+	action_about_->setObjectName(QString::fromUtf8("actionAbout"));
+	action_about_->setText(tr("&About..."));
+	menu_help->addAction(action_about_);
 
 	menu_bar->addAction(menu_file->menuAction());
 	menu_bar->addAction(menu_view->menuAction());
@@ -250,33 +531,140 @@ void MainWindow::setup_ui()
 	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);
+	// Also add all actions to the main window for always-enabled hotkeys
+	for (QAction* action : menu_bar->actions())
+		this->addAction(action);
 
-	// Setup the sampling bar
-	_sampling_bar = new toolbars::SamplingBar(_session, this);
+	// Setup the toolbar
+	main_bar_ = new toolbars::MainBar(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);
+	addToolBar(main_bar_);
 
 	// Set the title
-	setWindowTitle(QApplication::translate("MainWindow", "PulseView", 0,
-		QApplication::UnicodeUTF8));
+	setWindowTitle(tr("PulseView"));
 
-	// Setup _session events
-	connect(&_session, SIGNAL(capture_state_changed(int)), this,
+	// Setup session_ events
+	connect(&session_, SIGNAL(capture_state_changed(int)), this,
 		SLOT(capture_state_changed(int)));
+	connect(&session_, SIGNAL(device_selected()), this,
+		SLOT(device_selected()));
+	connect(&session_, SIGNAL(trigger_event(util::Timestamp)), view_,
+		SLOT(trigger_event(util::Timestamp)));
+
+	// Setup view_ events
+	connect(view_, SIGNAL(sticky_scrolling_changed(bool)), this,
+		SLOT(sticky_scrolling_changed(bool)));
+	connect(view_, SIGNAL(always_zoom_to_fit_changed(bool)), this,
+		SLOT(always_zoom_to_fit_changed(bool)));
+
+}
+
+void MainWindow::select_init_device()
+{
+	QSettings settings;
+	map<string, string> dev_info;
+	list<string> key_list;
+
+	// Re-select last used device if possible.
+	settings.beginGroup("Device");
+	key_list.push_back("vendor");
+	key_list.push_back("model");
+	key_list.push_back("version");
+	key_list.push_back("serial_num");
+	key_list.push_back("connection_id");
+
+	for (string key : key_list) {
+		const QString k = QString::fromStdString(key);
+		if (!settings.contains(k))
+			continue;
+
+		const string value = settings.value(k).toString().toStdString();
+		if (!value.empty())
+			dev_info.insert(std::make_pair(key, value));
+	}
+
+	const shared_ptr<devices::HardwareDevice> device =
+		device_manager_.find_device_from_info(dev_info);
+	select_device(device);
+	update_device_list();
+
+	settings.endGroup();
+}
+
+void MainWindow::load_init_file(const std::string &file_name,
+	const std::string &format)
+{
+	shared_ptr<InputFormat> input_format;
+
+	if (!format.empty()) {
+		const map<string, shared_ptr<InputFormat> > formats =
+			device_manager_.context()->input_formats();
+		const auto iter = find_if(formats.begin(), formats.end(),
+			[&](const pair<string, shared_ptr<InputFormat> > f) {
+				return f.first == format; });
+		if (iter == formats.end()) {
+			cerr << "Unexpected input format: " << format << endl;
+			return;
+		}
+
+		input_format = (*iter).second;
+	}
+
+	load_file(QString::fromStdString(file_name), input_format);
+}
+
+
+void MainWindow::save_ui_settings()
+{
+	QSettings settings;
+
+	map<string, string> dev_info;
+	list<string> key_list;
+
+	settings.beginGroup("MainWindow");
+	settings.setValue("state", saveState());
+	settings.setValue("geometry", saveGeometry());
+	settings.endGroup();
+
+	if (session_.device()) {
+		settings.beginGroup("Device");
+		key_list.push_back("vendor");
+		key_list.push_back("model");
+		key_list.push_back("version");
+		key_list.push_back("serial_num");
+		key_list.push_back("connection_id");
+
+		dev_info = device_manager_.get_device_info(
+			session_.device());
+
+		for (string key : key_list) {
+			if (dev_info.count(key))
+				settings.setValue(QString::fromUtf8(key.c_str()),
+						QString::fromUtf8(dev_info.at(key).c_str()));
+			else
+				settings.remove(QString::fromUtf8(key.c_str()));
+		}
+
+		settings.endGroup();
+	}
+}
+
+void MainWindow::restore_ui_settings()
+{
+	QSettings settings;
+
+	settings.beginGroup("MainWindow");
 
+	if (settings.contains("geometry")) {
+		restoreGeometry(settings.value("geometry").toByteArray());
+		restoreState(settings.value("state").toByteArray());
+	} else
+		resize(1000, 720);
+
+	settings.endGroup();
 }
 
 void MainWindow::session_error(
@@ -289,40 +677,54 @@ void MainWindow::session_error(
 
 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);
+	main_bar_->update_device_list();
 }
 
-void MainWindow::load_file(QString file_name)
+void MainWindow::load_file(QString file_name,
+	std::shared_ptr<sigrok::InputFormat> format,
+	const std::map<std::string, Glib::VariantBase> &options)
 {
 	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();
+		if (format)
+			session_.set_device(shared_ptr<devices::Device>(
+				new devices::InputFile(
+					device_manager_.context(),
+					file_name.toStdString(),
+					format, options)));
+		else
+			session_.set_device(shared_ptr<devices::Device>(
+				new devices::SessionFile(
+					device_manager_.context(),
+					file_name.toStdString())));
+	} catch (Error e) {
+		show_session_error(tr("Failed to load ") + file_name, e.what());
+		session_.set_default_device();
 		update_device_list();
 		return;
 	}
 
 	update_device_list();
 
-	_session.start_capture(boost::bind(&MainWindow::session_error, this,
-		errorMessage, infoMessage));
+	session_.start_capture([&, errorMessage](QString infoMessage) {
+		session_error(errorMessage, infoMessage); });
+}
+
+void MainWindow::closeEvent(QCloseEvent *event)
+{
+	save_ui_settings();
+	event->accept();
+}
+
+void MainWindow::keyReleaseEvent(QKeyEvent *event)
+{
+	if (event->key() == Qt::Key_Alt) {
+		menuBar()->setHidden(!menuBar()->isHidden());
+		menuBar()->setFocus();
+	}
+	QMainWindow::keyReleaseEvent(event);
 }
 
 void MainWindow::show_session_error(
@@ -338,44 +740,44 @@ void MainWindow::show_session_error(
 
 void MainWindow::on_actionOpen_triggered()
 {
+	QSettings settings;
+	const QString dir = settings.value(SettingOpenDirectory).toString();
+
 	// Show the dialog
 	const QString file_name = QFileDialog::getOpenFileName(
-		this, tr("Open File"), "", tr(
+		this, tr("Open File"), dir, tr(
 			"Sigrok Sessions (*.sr);;"
 			"All Files (*.*)"));
-	if (!file_name.isEmpty())
+
+	if (!file_name.isEmpty()) {
 		load_file(file_name);
+
+		const QString abs_path = QFileInfo(file_name).absolutePath();
+		settings.setValue(SettingOpenDirectory, abs_path);
+	}
 }
 
 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;
+	export_file(device_manager_.context()->output_formats()["srzip"]);
+}
 
-	StoreProgress *dlg = new StoreProgress(file_name, _session, this);
-	dlg->run();
+void MainWindow::on_actionSaveSelectionAs_triggered()
+{
+	export_file(device_manager_.context()->output_formats()["srzip"], true);
 }
 
 void MainWindow::on_actionConnect_triggered()
 {
 	// Stop any currently running capture session
-	_session.stop_capture();
+	session_.stop_capture();
 
-	dialogs::Connect dlg(this, _device_manager);
+	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());
+		select_device(dlg.get_selected_device());
 
 	update_device_list();
 }
@@ -387,70 +789,85 @@ void MainWindow::on_actionQuit_triggered()
 
 void MainWindow::on_actionViewZoomIn_triggered()
 {
-	_view->zoom(1);
+	view_->zoom(1);
 }
 
 void MainWindow::on_actionViewZoomOut_triggered()
 {
-	_view->zoom(-1);
+	view_->zoom(-1);
 }
 
 void MainWindow::on_actionViewZoomFit_triggered()
 {
-	_view->zoom_fit();
+	view_->zoom_fit(action_view_zoom_fit_->isChecked());
 }
 
 void MainWindow::on_actionViewZoomOneToOne_triggered()
 {
-	_view->zoom_one_to_one();
+	view_->zoom_one_to_one();
+}
+
+void MainWindow::on_actionViewStickyScrolling_triggered()
+{
+	view_->enable_sticky_scrolling(action_view_sticky_scrolling_->isChecked());
+}
+
+void MainWindow::on_actionViewColouredBg_triggered()
+{
+	view_->enable_coloured_bg(action_view_coloured_bg_->isChecked());
 }
 
 void MainWindow::on_actionViewShowCursors_triggered()
 {
-	assert(_view);
+	assert(view_);
 
-	const bool show = !_view->cursors_shown();
-	if(show)
-		_view->centre_cursors();
+	const bool show = !view_->cursors_shown();
+	if (show)
+		view_->centre_cursors();
 
-	_view->show_cursors(show);
+	view_->show_cursors(show);
 }
 
 void MainWindow::on_actionAbout_triggered()
 {
-	dialogs::About dlg(this);
+	dialogs::About dlg(device_manager_.context(), this);
 	dlg.exec();
 }
 
+void MainWindow::sticky_scrolling_changed(bool state)
+{
+	action_view_sticky_scrolling_->setChecked(state);
+}
+
+void MainWindow::always_zoom_to_fit_changed(bool state)
+{
+	action_view_zoom_fit_->setChecked(state);
+}
+
 void MainWindow::add_decoder(srd_decoder *decoder)
 {
 #ifdef ENABLE_DECODE
 	assert(decoder);
-	_session.add_decoder(decoder);
+	session_.add_decoder(decoder);
 #else
 	(void)decoder;
 #endif
 }
 
-void MainWindow::run_stop()
+void MainWindow::capture_state_changed(int state)
 {
-	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;
-	}
+	main_bar_->set_capture_state((pv::Session::capture_state)state);
 }
 
-void MainWindow::capture_state_changed(int state)
+void MainWindow::device_selected()
 {
-	_sampling_bar->set_capture_state((pv::SigSession::capture_state)state);
+	// Set the title to include the device/file name
+	const shared_ptr<devices::Device> device = session_.device();
+	if (!device)
+		return;
+
+	const string display_name = device->display_name(device_manager_);
+	setWindowTitle(tr("%1 - PulseView").arg(display_name.c_str()));
 }
 
 } // namespace pv
diff --git a/pv/mainwindow.h b/pv/mainwindow.h
deleted file mode 100644
index 1f9dd52..0000000
--- a/pv/mainwindow.h
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * 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/mainwindow.hpp b/pv/mainwindow.hpp
new file mode 100644
index 0000000..208cbe4
--- /dev/null
+++ b/pv/mainwindow.hpp
@@ -0,0 +1,212 @@
+/*
+ * 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_HPP
+#define PULSEVIEW_PV_MAINWINDOW_HPP
+
+#include <list>
+#include <map>
+#include <memory>
+
+#include <glibmm/variant.h>
+
+#include <QMainWindow>
+
+#include "session.hpp"
+
+struct srd_decoder;
+
+class QVBoxLayout;
+
+namespace sigrok {
+class InputFormat;
+class OutputFormat;
+}
+
+namespace pv {
+
+class DeviceManager;
+
+namespace toolbars {
+class ContextBar;
+class MainBar;
+}
+
+namespace view {
+class View;
+}
+
+namespace widgets {
+#ifdef ENABLE_DECODE
+class DecoderMenu;
+#endif
+}
+
+class MainWindow : public QMainWindow
+{
+	Q_OBJECT
+
+private:
+	/**
+	 * Name of the setting used to remember the directory
+	 * containing the last file that was opened.
+	 */
+	static const char *SettingOpenDirectory;
+
+	/**
+	 * Name of the setting used to remember the directory
+	 * containing the last file that was saved.
+	 */
+	static const char *SettingSaveDirectory;
+
+public:
+	explicit MainWindow(DeviceManager &device_manager,
+		std::string open_file_name = std::string(),
+		std::string open_file_format = std::string(),
+		QWidget *parent = 0);
+
+	QAction* action_open() const;
+	QAction* action_save_as() const;
+	QAction* action_save_selection_as() const;
+	QAction* action_connect() const;
+	QAction* action_quit() const;
+	QAction* action_view_zoom_in() const;
+	QAction* action_view_zoom_out() const;
+	QAction* action_view_zoom_fit() const;
+	QAction* action_view_zoom_one_to_one() const;
+	QAction* action_view_sticky_scrolling() const;
+	QAction* action_view_coloured_bg() const;
+	QAction* action_view_show_cursors() const;
+	QAction* action_about() const;
+
+#ifdef ENABLE_DECODE
+	QMenu* menu_decoder_add() const;
+#endif
+
+	void run_stop();
+
+	void select_device(std::shared_ptr<devices::Device> device);
+
+public Q_SLOTS:
+	void export_file(std::shared_ptr<sigrok::OutputFormat> format,
+		bool selection_only = false);
+	void import_file(std::shared_ptr<sigrok::InputFormat> format);
+
+private:
+	void setup_ui();
+
+	void select_init_device();
+
+	void load_init_file(const std::string &file_name,
+		const std::string &format);
+
+	void save_ui_settings();
+
+	void restore_ui_settings();
+
+	void session_error(const QString text, const QString info_text);
+
+	/**
+	 * Updates the device list in the toolbar
+	 */
+	void update_device_list();	
+
+	void load_file(QString file_name,
+		std::shared_ptr<sigrok::InputFormat> format = nullptr,
+		const std::map<std::string, Glib::VariantBase> &options =
+			std::map<std::string, Glib::VariantBase>());
+
+	void save_selection_to_file();
+
+private:
+	void closeEvent(QCloseEvent *event);
+
+	void keyReleaseEvent(QKeyEvent *event);
+
+private Q_SLOTS:
+	void show_session_error(
+		const QString text, const QString info_text);
+
+	void on_actionOpen_triggered();
+	void on_actionSaveAs_triggered();
+	void on_actionSaveSelectionAs_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_actionViewStickyScrolling_triggered();
+
+	void on_actionViewColouredBg_triggered();
+
+	void on_actionViewShowCursors_triggered();
+
+	void on_actionAbout_triggered();
+
+	void add_decoder(srd_decoder *decoder);
+
+	void capture_state_changed(int state);
+	void device_selected();
+
+	void sticky_scrolling_changed(bool state);
+
+	void always_zoom_to_fit_changed(bool state);
+
+private:
+	DeviceManager &device_manager_;
+
+	Session session_;
+
+	pv::view::View *view_;
+
+	QWidget *central_widget_;
+	QVBoxLayout *vertical_layout_;
+
+	toolbars::MainBar *main_bar_;
+
+	QAction *const action_open_;
+	QAction *const action_save_as_;
+	QAction *const action_save_selection_as_;
+	QAction *const action_connect_;
+	QAction *const action_quit_;
+	QAction *const action_view_zoom_in_;
+	QAction *const action_view_zoom_out_;
+	QAction *const action_view_zoom_fit_;
+	QAction *const action_view_zoom_one_to_one_;
+	QAction *const action_view_sticky_scrolling_;
+	QAction *const action_view_coloured_bg_;
+	QAction *const action_view_show_cursors_;
+	QAction *const action_about_;
+
+#ifdef ENABLE_DECODE
+	QMenu *const menu_decoders_add_;
+#endif
+};
+
+} // namespace pv
+
+#endif // PULSEVIEW_PV_MAINWINDOW_HPP
diff --git a/pv/popups/channels.cpp b/pv/popups/channels.cpp
new file mode 100644
index 0000000..1a042da
--- /dev/null
+++ b/pv/popups/channels.cpp
@@ -0,0 +1,260 @@
+/*
+ * 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>
+
+#ifdef _WIN32
+// Windows: Avoid boost/thread namespace pollution (which includes windows.h).
+#define NOGDI
+#define NORESOURCE
+#endif
+#include <boost/thread/locks.hpp>
+#include <boost/thread/shared_mutex.hpp>
+
+#include <QCheckBox>
+#include <QFormLayout>
+#include <QGridLayout>
+#include <QLabel>
+
+#include "channels.hpp"
+
+#include <pv/binding/device.hpp>
+#include <pv/devices/device.hpp>
+#include <pv/session.hpp>
+#include <pv/view/signal.hpp>
+
+#include <libsigrokcxx/libsigrokcxx.hpp>
+
+using namespace Qt;
+
+using boost::shared_lock;
+using boost::shared_mutex;
+using std::lock_guard;
+using std::map;
+using std::mutex;
+using std::set;
+using std::shared_ptr;
+using std::unordered_set;
+using std::vector;
+
+using sigrok::Channel;
+using sigrok::ChannelGroup;
+using sigrok::Device;
+
+using pv::view::Signal;
+
+namespace pv {
+namespace popups {
+
+Channels::Channels(Session &session, QWidget *parent) :
+	Popup(parent),
+	session_(session),
+	updating_channels_(false),
+	enable_all_channels_(tr("Enable All"), this),
+	disable_all_channels_(tr("Disable All"), this),
+	check_box_mapper_(this)
+{
+	// Create the layout
+	setLayout(&layout_);
+
+	const shared_ptr<sigrok::Device> device = session_.device()->device();
+	assert(device);
+
+	// Collect a set of signals
+	map<shared_ptr<Channel>, shared_ptr<Signal> > signal_map;
+
+	const unordered_set< shared_ptr<Signal> > sigs(session_.signals());
+
+	for (const shared_ptr<Signal> &sig : sigs)
+		signal_map[sig->channel()] = sig;
+
+	// Populate channel groups
+	for (auto entry : device->channel_groups()) {
+		shared_ptr<ChannelGroup> group = entry.second;
+		// Make a set of signals, and removed this signals from the
+		// signal map.
+		vector< shared_ptr<Signal> > group_sigs;
+		for (auto channel : group->channels()) {
+			const auto iter = signal_map.find(channel);
+
+			if (iter == signal_map.end())
+				break;
+
+			group_sigs.push_back((*iter).second);
+			signal_map.erase(iter);
+		}
+
+		populate_group(group, group_sigs);
+	}
+
+	// Make a vector of the remaining channels
+	vector< shared_ptr<Signal> > global_sigs;
+	for (auto channel : device->channels()) {
+		const map<shared_ptr<Channel>, shared_ptr<Signal> >::
+			const_iterator iter = signal_map.find(channel);
+		if (iter != signal_map.end())
+			global_sigs.push_back((*iter).second);
+	}
+
+	// Create a group
+	populate_group(nullptr, global_sigs);
+
+	// Create the enable/disable all buttons
+	connect(&enable_all_channels_, SIGNAL(clicked()),
+		this, SLOT(enable_all_channels()));
+	connect(&disable_all_channels_, SIGNAL(clicked()),
+		this, SLOT(disable_all_channels()));
+
+	enable_all_channels_.setFlat(true);
+	disable_all_channels_.setFlat(true);
+
+	buttons_bar_.addWidget(&enable_all_channels_);
+	buttons_bar_.addWidget(&disable_all_channels_);
+	buttons_bar_.addStretch(1);
+
+	layout_.addRow(&buttons_bar_);
+
+	// Connect the check-box signal mapper
+	connect(&check_box_mapper_, SIGNAL(mapped(QWidget*)),
+		this, SLOT(on_channel_checked(QWidget*)));
+}
+
+void Channels::set_all_channels(bool set)
+{
+	updating_channels_ = 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_channels_ = false;
+}
+
+void Channels::populate_group(shared_ptr<ChannelGroup> group,
+	const vector< shared_ptr<pv::view::Signal> > sigs)
+{
+	using pv::binding::Device;
+
+	// 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<Device> binding;
+	if (group)
+		binding = shared_ptr<Device>(new Device(group));
+
+	// Create a title if the group is going to have any content
+	if ((!sigs.empty() || (binding && !binding->properties().empty())) &&
+		group)
+		layout_.addRow(new QLabel(
+			QString("<h3>%1</h3>").arg(group->name().c_str())));
+
+	// Create the channel group grid
+	QGridLayout *const channel_grid =
+		create_channel_group_grid(sigs);
+	layout_.addRow(channel_grid);
+
+	// Create the channel group options
+	if (binding)
+	{
+		binding->add_properties_to_form(&layout_, true);
+		group_bindings_.push_back(binding);
+	}
+}
+
+QGridLayout* Channels::create_channel_group_grid(
+	const vector< shared_ptr<pv::view::Signal> > sigs)
+{
+	int row = 0, col = 0;
+	QGridLayout *const grid = new QGridLayout();
+
+	for (const shared_ptr<pv::view::Signal>& sig : sigs) {
+		assert(sig);
+
+		QCheckBox *const checkbox = new QCheckBox(sig->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 Channels::showEvent(QShowEvent *e)
+{
+	pv::widgets::Popup::showEvent(e);
+
+	updating_channels_ = 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_channels_ = false;
+}
+
+void Channels::on_channel_checked(QWidget *widget)
+{
+	if (updating_channels_)
+		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 Channels::enable_all_channels()
+{
+	set_all_channels(true);
+}
+
+void Channels::disable_all_channels()
+{
+	set_all_channels(false);
+}
+
+} // popups
+} // pv
diff --git a/pv/popups/probes.h b/pv/popups/channels.hpp
similarity index 54%
rename from pv/popups/probes.h
rename to pv/popups/channels.hpp
index dbf90cd..6ba8646 100644
--- a/pv/popups/probes.h
+++ b/pv/popups/channels.hpp
@@ -18,34 +18,33 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_POPUPS_PROBES_H
-#define PULSEVIEW_PV_POPUPS_PROBES_H
+#ifndef PULSEVIEW_PV_POPUPS_CHANNELS_HPP
+#define PULSEVIEW_PV_POPUPS_CHANNELS_HPP
 
 #include <map>
+#include <memory>
 #include <vector>
 
-#include <boost/shared_ptr.hpp>
-
 #include <QFormLayout>
 #include <QHBoxLayout>
 #include <QPushButton>
 #include <QSignalMapper>
 
-#include <pv/widgets/popup.h>
-
-struct sr_channel_group;
+#include <pv/widgets/popup.hpp>
 
 class QCheckBox;
 class QGridLayout;
 
+namespace sigrok {
+	class ChannelGroup;
+}
+
 namespace pv {
 
-class SigSession;
+class Session;
 
-namespace prop {
 namespace binding {
-class DeviceOptions;
-}
+class Device;
 }
 
 namespace view {
@@ -54,51 +53,51 @@ class Signal;
 
 namespace popups {
 
-class Probes : public pv::widgets::Popup
+class Channels : public pv::widgets::Popup
 {
 	Q_OBJECT
 
 public:
-	Probes(SigSession &_session, QWidget *parent);
+	Channels(Session &session_, QWidget *parent);
 
 private:
-	void set_all_probes(bool set);
+	void set_all_channels(bool set);
 
-	void populate_group(const sr_channel_group *group,
-		const std::vector< boost::shared_ptr<pv::view::Signal> > sigs);
+	void populate_group(std::shared_ptr<sigrok::ChannelGroup> group,
+		const std::vector< std::shared_ptr<pv::view::Signal> > sigs);
 
 	QGridLayout* create_channel_group_grid(
-		const std::vector< boost::shared_ptr<pv::view::Signal> > sigs);
+		const std::vector< std::shared_ptr<pv::view::Signal> > sigs);
 
 private:
 	void showEvent(QShowEvent *e);
 
-private slots:
-	void on_probe_checked(QWidget *widget);
+private Q_SLOTS:
+	void on_channel_checked(QWidget *widget);
 
-	void enable_all_probes();
-	void disable_all_probes();
+	void enable_all_channels();
+	void disable_all_channels();
 
 private:
-	pv::SigSession &_session;
+	pv::Session &session_;
 
-	QFormLayout _layout;
+	QFormLayout layout_;
 
-	bool _updating_probes;
+	bool updating_channels_;
 
-	std::vector< boost::shared_ptr<pv::prop::binding::DeviceOptions> >
-		 _group_bindings;
-	std::map< QCheckBox*, boost::shared_ptr<pv::view::Signal> >
-		_check_box_signal_map;
+	std::vector< std::shared_ptr<pv::binding::Device> >
+		 group_bindings_;
+	std::map< QCheckBox*, std::shared_ptr<pv::view::Signal> >
+		check_box_signal_map_;
 
-	QHBoxLayout _buttons_bar;
-	QPushButton _enable_all_probes;
-	QPushButton _disable_all_probes;
+	QHBoxLayout buttons_bar_;
+	QPushButton enable_all_channels_;
+	QPushButton disable_all_channels_;
 
-	QSignalMapper _check_box_mapper;
+	QSignalMapper check_box_mapper_;
 };
 
 } // popups
 } // pv
 
-#endif // PULSEVIEW_PV_POPUPS_PROBES_H
+#endif // PULSEVIEW_PV_POPUPS_CHANNELS_HPP
diff --git a/pv/popups/deviceoptions.cpp b/pv/popups/deviceoptions.cpp
index 13055fc..fe79551 100644
--- a/pv/popups/deviceoptions.cpp
+++ b/pv/popups/deviceoptions.cpp
@@ -18,35 +18,36 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "deviceoptions.h"
-
-#include <boost/foreach.hpp>
+#include "deviceoptions.hpp"
 
 #include <QFormLayout>
 #include <QListWidget>
 
-#include <pv/prop/property.h>
+#include <pv/prop/property.hpp>
+
+#include <libsigrokcxx/libsigrokcxx.hpp>
+
+using std::shared_ptr;
 
-using boost::shared_ptr;
+using sigrok::Device;
 
 namespace pv {
 namespace popups {
 
-DeviceOptions::DeviceOptions(shared_ptr<device::DevInst> dev_inst,
-	QWidget *parent) :
+DeviceOptions::DeviceOptions(shared_ptr<Device> device, QWidget *parent) :
 	Popup(parent),
-	_dev_inst(dev_inst),
-	_layout(this),
-	_binding(dev_inst)
+	device_(device),
+	layout_(this),
+	binding_(device)
 {
-	setLayout(&_layout);
+	setLayout(&layout_);
 
-	_layout.addWidget(_binding.get_property_form(this, true));
+	layout_.addWidget(binding_.get_property_form(this, true));
 }
 
-pv::prop::binding::DeviceOptions& DeviceOptions::binding()
+pv::binding::Device& DeviceOptions::binding()
 {
-	return _binding;
+	return binding_;
 }
 
 } // namespace popups
diff --git a/pv/popups/deviceoptions.h b/pv/popups/deviceoptions.hpp
similarity index 71%
rename from pv/popups/deviceoptions.h
rename to pv/popups/deviceoptions.hpp
index 945959b..e358932 100644
--- a/pv/popups/deviceoptions.h
+++ b/pv/popups/deviceoptions.hpp
@@ -18,14 +18,18 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_POPUPS_DEVICEOPTIONS_H
-#define PULSEVIEW_PV_POPUPS_DEVICEOPTIONS_H
+#ifndef PULSEVIEW_PV_POPUPS_DEVICEOPTIONS_HPP
+#define PULSEVIEW_PV_POPUPS_DEVICEOPTIONS_HPP
 
 #include <QGroupBox>
 #include <QVBoxLayout>
 
-#include <pv/prop/binding/deviceoptions.h>
-#include <pv/widgets/popup.h>
+#include <pv/binding/device.hpp>
+#include <pv/widgets/popup.hpp>
+
+namespace sigrok {
+	class Device;
+}
 
 namespace pv {
 namespace popups {
@@ -35,20 +39,20 @@ class DeviceOptions : public pv::widgets::Popup
 	Q_OBJECT
 
 public:
-	DeviceOptions(boost::shared_ptr<device::DevInst> dev_inst,
+	DeviceOptions(std::shared_ptr<sigrok::Device> device,
 		QWidget *parent);
 
-	pv::prop::binding::DeviceOptions& binding();
+	pv::binding::Device& binding();
 
 private:
-	boost::shared_ptr<device::DevInst> _dev_inst;
+	std::shared_ptr<sigrok::Device> device_;
 
-	QVBoxLayout _layout;
+	QVBoxLayout layout_;
 
-	pv::prop::binding::DeviceOptions _binding;
+	pv::binding::Device binding_;
 };
 
 } // namespace popups
 } // namespace pv
 
-#endif // PULSEVIEW_PV_POPUPS_DEVICEOPTIONS_H
+#endif // PULSEVIEW_PV_POPUPS_DEVICEOPTIONS_HPP
diff --git a/pv/popups/probes.cpp b/pv/popups/probes.cpp
deleted file mode 100644
index ca1a2fb..0000000
--- a/pv/popups/probes.cpp
+++ /dev/null
@@ -1,258 +0,0 @@
-/*
- * 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/prop/binding/deviceoptions.cpp b/pv/prop/binding/deviceoptions.cpp
deleted file mode 100644
index 2dc237a..0000000
--- a/pv/prop/binding/deviceoptions.cpp
+++ /dev/null
@@ -1,192 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 445361f..0000000
--- a/pv/prop/binding/deviceoptions.h
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * 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
index e0e052f..3e201cc 100644
--- a/pv/prop/bool.cpp
+++ b/pv/prop/bool.cpp
@@ -22,14 +22,14 @@
 
 #include <QCheckBox>
 
-#include "bool.h"
+#include "bool.hpp"
 
 namespace pv {
 namespace prop {
 
 Bool::Bool(QString name, Getter getter, Setter setter) :
 	Property(name, getter, setter),
-	_check_box(NULL)
+	check_box_(nullptr)
 {
 }
 
@@ -39,23 +39,27 @@ Bool::~Bool()
 
 QWidget* Bool::get_widget(QWidget *parent, bool auto_commit)
 {
-	if (_check_box)
-		return _check_box;
+	if (check_box_)
+		return check_box_;
 
-	GVariant *const value = _getter ? _getter() : NULL;
-	if (!value)
-		return NULL;
+	if (!getter_)
+		return nullptr;
 
-	_check_box = new QCheckBox(name(), parent);
-	_check_box->setCheckState(g_variant_get_boolean(value) ?
-		Qt::Checked : Qt::Unchecked);
-	g_variant_unref(value);
+	Glib::VariantBase variant = getter_();
+	if (!variant.gobj())
+		return nullptr;
+
+	bool value = Glib::VariantBase::cast_dynamic<Glib::Variant<bool>>(
+		variant).get();
+
+	check_box_ = new QCheckBox(name(), parent);
+	check_box_->setCheckState(value ? Qt::Checked : Qt::Unchecked);
 
 	if (auto_commit)
-		connect(_check_box, SIGNAL(stateChanged(int)),
+		connect(check_box_, SIGNAL(stateChanged(int)),
 			this, SLOT(on_state_changed(int)));
 
-	return _check_box;
+	return check_box_;
 }
 
 bool Bool::labeled_widget() const
@@ -65,13 +69,13 @@ bool Bool::labeled_widget() const
 
 void Bool::commit()
 {
-	assert(_setter);
+	assert(setter_);
 
-	if (!_check_box)
+	if (!check_box_)
 		return;
 
-	_setter(g_variant_new_boolean(
-		_check_box->checkState() == Qt::Checked));
+	setter_(Glib::Variant<bool>::create(
+		check_box_->checkState() == Qt::Checked));
 }
 
 void Bool::on_state_changed(int)
diff --git a/pv/prop/bool.h b/pv/prop/bool.hpp
similarity index 87%
rename from pv/prop/bool.h
rename to pv/prop/bool.hpp
index 0d8e205..6d32da7 100644
--- a/pv/prop/bool.h
+++ b/pv/prop/bool.hpp
@@ -18,10 +18,10 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_PROP_BOOL_H
-#define PULSEVIEW_PV_PROP_BOOL_H
+#ifndef PULSEVIEW_PV_PROP_BOOL_HPP
+#define PULSEVIEW_PV_PROP_BOOL_HPP
 
-#include "property.h"
+#include "property.hpp"
 
 class QCheckBox;
 
@@ -42,14 +42,14 @@ public:
 
 	void commit();
 
-private slots:
+private Q_SLOTS:
 	void on_state_changed(int);
 
 private:
-	QCheckBox *_check_box;
+	QCheckBox *check_box_;
 };
 
 } // prop
 } // pv
 
-#endif // PULSEVIEW_PV_PROP_BOOL_H
+#endif // PULSEVIEW_PV_PROP_BOOL_HPP
diff --git a/pv/prop/double.cpp b/pv/prop/double.cpp
index 93b45d3..c5ab1db 100644
--- a/pv/prop/double.cpp
+++ b/pv/prop/double.cpp
@@ -22,7 +22,7 @@
 
 #include <QDoubleSpinBox>
 
-#include "double.h"
+#include "double.hpp"
 
 using boost::optional;
 using std::pair;
@@ -38,11 +38,11 @@ Double::Double(QString name,
 	Getter getter,
 	Setter setter) :
 	Property(name, getter, setter),
-	_decimals(decimals),
-	_suffix(suffix),
-	_range(range),
-	_step(step),
-	_spin_box(NULL)
+	decimals_(decimals),
+	suffix_(suffix),
+	range_(range),
+	step_(step),
+	spin_box_(nullptr)
 {
 }
 
@@ -52,39 +52,44 @@ Double::~Double()
 
 QWidget* Double::get_widget(QWidget *parent, bool auto_commit)
 {
-	if (_spin_box)
-		return _spin_box;
+	if (spin_box_)
+		return spin_box_;
 
-	GVariant *const value = _getter ? _getter() : NULL;
-	if (!value)
-		return NULL;
+	if (!getter_)
+		return nullptr;
 
-	_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);
+	Glib::VariantBase variant = getter_();
+	if (!variant.gobj())
+		return nullptr;
 
-	_spin_box->setValue(g_variant_get_double(value));
-	g_variant_unref(value);
+	double value = Glib::VariantBase::cast_dynamic<Glib::Variant<double>>(
+		variant).get();
+
+	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(value);
 
 	if (auto_commit)
-		connect(_spin_box, SIGNAL(valueChanged(double)),
+		connect(spin_box_, SIGNAL(valueChanged(double)),
 			this, SLOT(on_value_changed(double)));
 
-	return _spin_box;
+	return spin_box_;
 }
 
 void Double::commit()
 {
-	assert(_setter);
+	assert(setter_);
 
-	if (!_spin_box)
+	if (!spin_box_)
 		return;
 
-	_setter(g_variant_new_double(_spin_box->value()));
+	setter_(Glib::Variant<double>::create(spin_box_->value()));
 }
 
 void Double::on_value_changed(double)
diff --git a/pv/prop/double.h b/pv/prop/double.hpp
similarity index 80%
rename from pv/prop/double.h
rename to pv/prop/double.hpp
index ba4c595..b62d1fa 100644
--- a/pv/prop/double.h
+++ b/pv/prop/double.hpp
@@ -18,14 +18,14 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_PROP_DOUBLE_H
-#define PULSEVIEW_PV_PROP_DOUBLE_H
+#ifndef PULSEVIEW_PV_PROP_DOUBLE_HPP
+#define PULSEVIEW_PV_PROP_DOUBLE_HPP
 
 #include <utility>
 
 #include <boost/optional.hpp>
 
-#include "property.h"
+#include "property.hpp"
 
 class QDoubleSpinBox;
 
@@ -49,19 +49,19 @@ public:
 
 	void commit();
 
-private slots:
+private Q_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;
+	const int decimals_;
+	const QString suffix_;
+	const boost::optional< std::pair<double, double> > range_;
+	const boost::optional<double> step_;
 
-	QDoubleSpinBox *_spin_box;
+	QDoubleSpinBox *spin_box_;
 };
 
 } // prop
 } // pv
 
-#endif // PULSEVIEW_PV_PROP_DOUBLE_H
+#endif // PULSEVIEW_PV_PROP_DOUBLE_HPP
diff --git a/pv/prop/enum.cpp b/pv/prop/enum.cpp
index 6439a72..06e4d76 100644
--- a/pv/prop/enum.cpp
+++ b/pv/prop/enum.cpp
@@ -22,7 +22,7 @@
 
 #include <QComboBox>
 
-#include "enum.h"
+#include "enum.hpp"
 
 using std::pair;
 using std::vector;
@@ -31,62 +31,57 @@ namespace pv {
 namespace prop {
 
 Enum::Enum(QString name,
-	vector<pair<GVariant*, QString> > values,
+	vector<pair<Glib::VariantBase, QString> > values,
 	Getter getter, Setter setter) :
 	Property(name, getter, setter),
-	_values(values),
-	_selector(NULL)
+	values_(values),
+	selector_(nullptr)
 {
-	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);
+	if (selector_)
+		return selector_;
+
+	if (!getter_)
+		return nullptr;
+
+	Glib::VariantBase variant = getter_();
+	if (!variant.gobj())
+		return nullptr;
+
+	selector_ = new QComboBox(parent);
+	for (unsigned int i = 0; i < values_.size(); i++) {
+		const pair<Glib::VariantBase, QString> &v = values_[i];
+		selector_->addItem(v.second, qVariantFromValue(v.first));
+		if (v.first.equal(variant))
+			selector_->setCurrentIndex(i);
 	}
 
-	g_variant_unref(value);
-
 	if (auto_commit)
-		connect(_selector, SIGNAL(currentIndexChanged(int)),
+		connect(selector_, SIGNAL(currentIndexChanged(int)),
 			this, SLOT(on_current_item_changed(int)));
 
-	return _selector;
+	return selector_;
 }
 
 void Enum::commit()
 {
-	assert(_setter);
+	assert(setter_);
 
-	if (!_selector)
+	if (!selector_)
 		return;
 
-	const int index = _selector->currentIndex();
+	const int index = selector_->currentIndex();
 	if (index < 0)
 		return;
 
-	_setter((GVariant*)_selector->itemData(index).value<void*>());
+	setter_(selector_->itemData(index).value<Glib::VariantBase>());
 }
 
 void Enum::on_current_item_changed(int)
diff --git a/pv/prop/enum.h b/pv/prop/enum.hpp
similarity index 75%
rename from pv/prop/enum.h
rename to pv/prop/enum.hpp
index a7e6ed8..63f78af 100644
--- a/pv/prop/enum.h
+++ b/pv/prop/enum.hpp
@@ -18,13 +18,17 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_PROP_ENUM_H
-#define PULSEVIEW_PV_PROP_ENUM_H
+#ifndef PULSEVIEW_PV_PROP_ENUM_HPP
+#define PULSEVIEW_PV_PROP_ENUM_HPP
 
 #include <utility>
 #include <vector>
 
-#include "property.h"
+#include "property.hpp"
+
+#include <QMetaType>
+
+Q_DECLARE_METATYPE(Glib::VariantBase);
 
 class QComboBox;
 
@@ -36,7 +40,7 @@ class Enum : public Property
 	Q_OBJECT;
 
 public:
-	Enum(QString name, std::vector<std::pair<GVariant*, QString> > values,
+	Enum(QString name, std::vector<std::pair<Glib::VariantBase, QString> > values,
 		Getter getter, Setter setter);
 
 	virtual ~Enum();
@@ -45,16 +49,16 @@ public:
 
 	void commit();
 
-private slots:
+private Q_SLOTS:
 	void on_current_item_changed(int);
 
 private:
-	const std::vector< std::pair<GVariant*, QString> > _values;
+	const std::vector< std::pair<Glib::VariantBase, QString> > values_;
 
-	QComboBox *_selector;
+	QComboBox *selector_;
 };
 
 } // prop
 } // pv
 
-#endif // PULSEVIEW_PV_PROP_ENUM_H
+#endif // PULSEVIEW_PV_PROP_ENUM_HPP
diff --git a/pv/prop/int.cpp b/pv/prop/int.cpp
index 8124f9b..e750e9f 100644
--- a/pv/prop/int.cpp
+++ b/pv/prop/int.cpp
@@ -23,7 +23,7 @@
 
 #include <QSpinBox>
 
-#include "int.h"
+#include "int.hpp"
 
 using boost::optional;
 using std::max;
@@ -39,76 +39,61 @@ Int::Int(QString name,
 	Getter getter,
 	Setter setter) :
 	Property(name, getter, setter),
-	_suffix(suffix),
-	_range(range),
-	_value(NULL),
-	_spin_box(NULL)
+	suffix_(suffix),
+	range_(range),
+	spin_box_(nullptr)
 {
 }
 
 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;
+	int64_t int_val = 0, range_min = 0;
+	uint64_t range_max = 0;
 
-	if (_spin_box)
-		return _spin_box;
+	if (spin_box_)
+		return spin_box_;
 
-	if (_value)
-		g_variant_unref(_value);
+	if (!getter_)
+		return nullptr;
 
-	_value = _getter ? _getter() : NULL;
-	if (!_value)
-		return NULL;
+	value_ = getter_();
 
-	_spin_box = new QSpinBox(parent);
-	_spin_box->setSuffix(_suffix);
+	GVariant *value = value_.gobj();
+	if (!value)
+		return nullptr;
 
-	const GVariantType *const type = g_variant_get_type(_value);
+	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);
+	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);
+	} 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);
+	} 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);
+	} 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);
+	} 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);
+	} 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);
+	} 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
-	{
+	} else {
 		// Unexpected value type.
 		assert(0);
 	}
@@ -119,49 +104,47 @@ QWidget* Int::get_widget(QWidget *parent, bool auto_commit)
 	// custom widget.
 
 	range_min = max(range_min, (int64_t)INT_MIN);
-	range_max = min(range_max, (int64_t)INT_MAX);
+	range_max = min(range_max, (uint64_t)INT_MAX);
 
-	if (_range)
-		_spin_box->setRange((int)_range->first, (int)_range->second);
+	if (range_)
+		spin_box_->setRange((int)range_->first, (int)range_->second);
 	else
-		_spin_box->setRange((int)range_min, (int)range_max);
+		spin_box_->setRange((int)range_min, (int)range_max);
 
-	_spin_box->setValue((int)int_val);
+	spin_box_->setValue((int)int_val);
 
 	if (auto_commit)
-		connect(_spin_box, SIGNAL(valueChanged(int)),
+		connect(spin_box_, SIGNAL(valueChanged(int)),
 			this, SLOT(on_value_changed(int)));
 
-	return _spin_box;
+	return spin_box_;
 }
 
 void Int::commit()
 {
-	assert(_setter);
+	assert(setter_);
 
-	if (!_spin_box)
+	if (!spin_box_)
 		return;
 
-	assert(_value);
-
-	GVariant *new_value = NULL;
-	const GVariantType *const type = g_variant_get_type(_value);
+	GVariant *new_value = nullptr;
+	const GVariantType *const type = g_variant_get_type(value_.gobj());
 	assert(type);
 
 	if (g_variant_type_equal(type, G_VARIANT_TYPE_BYTE))
-		new_value = g_variant_new_byte(_spin_box->value());
+		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());
+		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());
+		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());
+		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());
+		new_value = g_variant_new_uint32(spin_box_->value());
 	else if (g_variant_type_equal(type, G_VARIANT_TYPE_INT64))
-		new_value = g_variant_new_int64(_spin_box->value());
+		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());
+		new_value = g_variant_new_uint64(spin_box_->value());
 	else
 	{
 		// Unexpected value type.
@@ -170,11 +153,9 @@ void Int::commit()
 
 	assert(new_value);
 
-	g_variant_unref(_value);
-	g_variant_ref(new_value);
-	_value = new_value;
+	value_ = Glib::VariantBase(new_value);
 
-	_setter(new_value);
+	setter_(value_);
 }
 
 void Int::on_value_changed(int)
diff --git a/pv/prop/int.h b/pv/prop/int.hpp
similarity index 81%
rename from pv/prop/int.h
rename to pv/prop/int.hpp
index 6d910cd..60c9d51 100644
--- a/pv/prop/int.h
+++ b/pv/prop/int.hpp
@@ -18,14 +18,14 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_PROP_INT_H
-#define PULSEVIEW_PV_PROP_INT_H
+#ifndef PULSEVIEW_PV_PROP_INT_HPP
+#define PULSEVIEW_PV_PROP_INT_HPP
 
 #include <utility>
 
 #include <boost/optional.hpp>
 
-#include "property.h"
+#include "property.hpp"
 
 class QSpinBox;
 
@@ -47,18 +47,18 @@ public:
 
 	void commit();
 
-private slots:
+private Q_SLOTS:
 	void on_value_changed(int);
 
 private:
-	const QString _suffix;
-	const boost::optional< std::pair<int64_t, int64_t> > _range;
+	const QString suffix_;
+	const boost::optional< std::pair<int64_t, int64_t> > range_;
 
-	GVariant *_value;
-	QSpinBox *_spin_box;
+	Glib::VariantBase value_;
+	QSpinBox *spin_box_;
 };
 
 } // prop
 } // pv
 
-#endif // PULSEVIEW_PV_PROP_INT_H
+#endif // PULSEVIEW_PV_PROP_INT_HPP
diff --git a/pv/prop/property.cpp b/pv/prop/property.cpp
index 798b5f1..c513d51 100644
--- a/pv/prop/property.cpp
+++ b/pv/prop/property.cpp
@@ -18,21 +18,21 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "property.h"
+#include "property.hpp"
 
 namespace pv {
 namespace prop {
 
 Property::Property(QString name, Getter getter, Setter setter) :
-	_getter(getter),
-	_setter(setter),
-	_name(name)
+	getter_(getter),
+	setter_(setter),
+	name_(name)
 {
 }
 
 const QString& Property::name() const
 {
-	return _name;
+	return name_;
 }
 
 bool Property::labeled_widget() const
diff --git a/pv/prop/property.h b/pv/prop/property.hpp
similarity index 73%
rename from pv/prop/property.h
rename to pv/prop/property.hpp
index 0b4bc7b..77eb9a6 100644
--- a/pv/prop/property.h
+++ b/pv/prop/property.hpp
@@ -18,13 +18,16 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_PROP_PROPERTY_H
-#define PULSEVIEW_PV_PROP_PROPERTY_H
+#ifndef PULSEVIEW_PV_PROP_PROPERTY_HPP
+#define PULSEVIEW_PV_PROP_PROPERTY_HPP
 
 #include <glib.h>
+// Suppress warnings due to use of deprecated std::auto_ptr<> by glibmm.
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+#include <glibmm.h>
+G_GNUC_END_IGNORE_DEPRECATIONS
 
-#include <boost/function.hpp>
-
+#include <functional>
 #include <QString>
 #include <QWidget>
 
@@ -38,8 +41,8 @@ class Property : public QObject
 	Q_OBJECT;
 
 public:
-	typedef boost::function<GVariant* ()> Getter;
-	typedef boost::function<void (GVariant*)> Setter;
+	typedef std::function<Glib::VariantBase ()> Getter;
+	typedef std::function<void (Glib::VariantBase)> Setter;
 
 protected:
 	Property(QString name, Getter getter, Setter setter);
@@ -54,14 +57,14 @@ public:
 	virtual void commit() = 0;
 
 protected:
-	const Getter _getter;
-	const Setter _setter;
+	const Getter getter_;
+	const Setter setter_;
 
 private:
-	QString _name;
+	QString name_;
 };
 
 } // prop
 } // pv
 
-#endif // PULSEVIEW_PV_PROP_PROPERTY_H
+#endif // PULSEVIEW_PV_PROP_PROPERTY_HPP
diff --git a/pv/prop/string.cpp b/pv/prop/string.cpp
index 7fcc656..e7af459 100644
--- a/pv/prop/string.cpp
+++ b/pv/prop/string.cpp
@@ -23,7 +23,11 @@
 #include <QLineEdit>
 #include <QSpinBox>
 
-#include "string.h"
+#include "string.hpp"
+
+using std::string;
+
+using Glib::ustring;
 
 namespace pv {
 namespace prop {
@@ -32,40 +36,44 @@ String::String(QString name,
 	Getter getter,
 	Setter setter) :
 	Property(name, getter, setter),
-	_line_edit(NULL)
+	line_edit_(nullptr)
 {
 }
 
 QWidget* String::get_widget(QWidget *parent, bool auto_commit)
 {
-	if (_line_edit)
-		return _line_edit;
+	if (line_edit_)
+		return line_edit_;
+
+	if (!getter_)
+		return nullptr;
+
+	Glib::VariantBase variant = getter_();
+	if (!variant.gobj())
+		return nullptr;
 
-	GVariant *const value = _getter ? _getter() : NULL;
-	if (!value)
-		return NULL;
+	string value = Glib::VariantBase::cast_dynamic<Glib::Variant<ustring>>(
+		variant).get();
 
-	_line_edit = new QLineEdit(parent);
-	_line_edit->setText(QString::fromUtf8(
-		g_variant_get_string(value, NULL)));
-	g_variant_unref(value);
+	line_edit_ = new QLineEdit(parent);
+	line_edit_->setText(QString::fromStdString(value));
 
 	if (auto_commit)
-		connect(_line_edit, SIGNAL(textEdited(const QString&)),
+		connect(line_edit_, SIGNAL(textEdited(const QString&)),
 			this, SLOT(on_text_edited(const QString&)));
 
-	return _line_edit;
+	return line_edit_;
 }
 
 void String::commit()
 {
-	assert(_setter);
+	assert(setter_);
 
-	if (!_line_edit)
+	if (!line_edit_)
 		return;
 
-	QByteArray ba = _line_edit->text().toLocal8Bit();
-	_setter(g_variant_new_string(ba.data()));
+	QByteArray ba = line_edit_->text().toLocal8Bit();
+	setter_(Glib::Variant<ustring>::create(ba.data()));
 }
 
 void String::on_text_edited(const QString&)
diff --git a/pv/prop/string.h b/pv/prop/string.hpp
similarity index 86%
rename from pv/prop/string.h
rename to pv/prop/string.hpp
index d54635f..9cd9278 100644
--- a/pv/prop/string.h
+++ b/pv/prop/string.hpp
@@ -18,10 +18,10 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_PROP_STRING_H
-#define PULSEVIEW_PV_PROP_STRING_H
+#ifndef PULSEVIEW_PV_PROP_STRING_HPP
+#define PULSEVIEW_PV_PROP_STRING_HPP
 
-#include "property.h"
+#include "property.hpp"
 
 class QLineEdit;
 
@@ -39,14 +39,14 @@ public:
 
 	void commit();
 
-private slots:
+private Q_SLOTS:
 	void on_text_edited(const QString&);
 
 private:
-	QLineEdit *_line_edit;
+	QLineEdit *line_edit_;
 };
 
 } // prop
 } // pv
 
-#endif // PULSEVIEW_PV_PROP_STRING_H
+#endif // PULSEVIEW_PV_PROP_STRING_HPP
diff --git a/pv/session.cpp b/pv/session.cpp
new file mode 100644
index 0000000..9fd8fb3
--- /dev/null
+++ b/pv/session.cpp
@@ -0,0 +1,682 @@
+/*
+ * 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 _WIN32
+// Windows: Avoid boost/thread namespace pollution (which includes windows.h).
+#define NOGDI
+#define NORESOURCE
+#endif
+#include <boost/thread/locks.hpp>
+#include <boost/thread/shared_mutex.hpp>
+
+#ifdef ENABLE_DECODE
+#include <libsigrokdecode/libsigrokdecode.h>
+#endif
+
+#include "session.hpp"
+
+#include "devicemanager.hpp"
+
+#include "data/analog.hpp"
+#include "data/analogsegment.hpp"
+#include "data/decoderstack.hpp"
+#include "data/logic.hpp"
+#include "data/logicsegment.hpp"
+#include "data/decode/decoder.hpp"
+
+#include "devices/hardwaredevice.hpp"
+#include "devices/sessionfile.hpp"
+
+#include "view/analogsignal.hpp"
+#include "view/decodetrace.hpp"
+#include "view/logicsignal.hpp"
+
+#include <cassert>
+#include <mutex>
+#include <stdexcept>
+
+#include <sys/stat.h>
+
+#include <QDebug>
+
+#include <libsigrokcxx/libsigrokcxx.hpp>
+
+using boost::shared_lock;
+using boost::shared_mutex;
+using boost::unique_lock;
+
+using std::dynamic_pointer_cast;
+using std::function;
+using std::lock_guard;
+using std::list;
+using std::map;
+using std::mutex;
+using std::recursive_mutex;
+using std::set;
+using std::shared_ptr;
+using std::string;
+using std::unordered_set;
+using std::vector;
+
+using sigrok::Analog;
+using sigrok::Channel;
+using sigrok::ChannelType;
+using sigrok::ConfigKey;
+using sigrok::DatafeedCallbackFunction;
+using sigrok::Error;
+using sigrok::Header;
+using sigrok::Logic;
+using sigrok::Meta;
+using sigrok::Packet;
+using sigrok::PacketPayload;
+using sigrok::Session;
+using sigrok::SessionDevice;
+
+using Glib::VariantBase;
+using Glib::Variant;
+
+namespace pv {
+Session::Session(DeviceManager &device_manager) :
+	device_manager_(device_manager),
+	capture_state_(Stopped),
+	cur_samplerate_(0)
+{
+}
+
+Session::~Session()
+{
+	// Stop and join to the thread
+	stop_capture();
+}
+
+DeviceManager& Session::device_manager()
+{
+	return device_manager_;
+}
+
+const DeviceManager& Session::device_manager() const
+{
+	return device_manager_;
+}
+
+shared_ptr<sigrok::Session> Session::session() const
+{
+	if (!device_)
+		return shared_ptr<sigrok::Session>();
+	return device_->session();
+}
+
+shared_ptr<devices::Device> Session::device() const
+{
+	return device_;
+}
+
+void Session::set_device(shared_ptr<devices::Device> device)
+{
+	assert(device);
+
+	// Ensure we are not capturing before setting the device
+	stop_capture();
+
+	if (device_)
+		device_->close();
+
+	device_ = std::move(device);
+	device_->open();
+	device_->session()->add_datafeed_callback([=]
+		(shared_ptr<sigrok::Device> device, shared_ptr<Packet> packet) {
+			data_feed_in(device, packet);
+		});
+
+	decode_traces_.clear();
+
+	update_signals();
+	device_selected();
+}
+
+void Session::set_default_device()
+{
+	const list< shared_ptr<devices::HardwareDevice> > &devices =
+		device_manager_.devices();
+
+	if (devices.empty())
+		return;
+
+	// Try and find the demo device and select that by default
+	const auto iter = std::find_if(devices.begin(), devices.end(),
+		[] (const shared_ptr<devices::HardwareDevice> &d) {
+			return d->hardware_device()->driver()->name() ==
+			"demo";	});
+	set_device((iter == devices.end()) ? devices.front() : *iter);
+}
+
+Session::capture_state Session::get_capture_state() const
+{
+	lock_guard<mutex> lock(sampling_mutex_);
+	return capture_state_;
+}
+
+void Session::start_capture(function<void (const QString)> error_handler)
+{
+	stop_capture();
+
+	// Check that at least one channel is enabled
+	assert(device_);
+	const shared_ptr<sigrok::Device> sr_dev = device_->device();
+	if (sr_dev) {
+		const auto channels = sr_dev->channels();
+		if (!std::any_of(channels.begin(), channels.end(),
+			[](shared_ptr<Channel> channel) {
+				return channel->enabled(); })) {
+			error_handler(tr("No channels enabled."));
+			return;
+		}
+	}
+
+	// Clear signal data
+	for (const shared_ptr<data::SignalData> d : get_data())
+		d->clear();
+
+	// Begin the session
+	sampling_thread_ = std::thread(
+		&Session::sample_thread_proc, this, device_,
+			error_handler);
+}
+
+void Session::stop_capture()
+{
+	if (get_capture_state() != Stopped)
+		device_->stop();
+
+	// Check that sampling stopped
+	if (sampling_thread_.joinable())
+		sampling_thread_.join();
+}
+
+set< shared_ptr<data::SignalData> > Session::get_data() const
+{
+	shared_lock<shared_mutex> lock(signals_mutex_);
+	set< shared_ptr<data::SignalData> > data;
+	for (const shared_ptr<view::Signal> sig : signals_) {
+		assert(sig);
+		data.insert(sig->data());
+	}
+
+	return data;
+}
+
+double Session::get_samplerate() const
+{
+	double samplerate = 0.0;
+
+	for (const shared_ptr<pv::data::SignalData> d : get_data()) {
+		assert(d);
+		const vector< shared_ptr<pv::data::Segment> > segments =
+			d->segments();
+		for (const shared_ptr<pv::data::Segment> &s : segments)
+			samplerate = std::max(samplerate, s->samplerate());
+	}
+
+	// If there is no sample rate given we use samples as unit
+	if (samplerate == 0.0)
+		samplerate = 1.0;
+
+	return samplerate;
+}
+
+const unordered_set< shared_ptr<view::Signal> > Session::signals() const
+{
+	shared_lock<shared_mutex> lock(signals_mutex_);
+	return signals_;
+}
+
+#ifdef ENABLE_DECODE
+bool Session::add_decoder(srd_decoder *const dec)
+{
+	map<const srd_channel*, shared_ptr<view::LogicSignal> > channels;
+	shared_ptr<data::DecoderStack> decoder_stack;
+
+	try {
+		lock_guard<boost::shared_mutex> lock(signals_mutex_);
+
+		// Create the decoder
+		decoder_stack = shared_ptr<data::DecoderStack>(
+			new data::DecoderStack(*this, dec));
+
+		// Make a list of all the channels
+		std::vector<const srd_channel*> all_channels;
+		for (const GSList *i = dec->channels; i; i = i->next)
+			all_channels.push_back((const srd_channel*)i->data);
+		for (const GSList *i = dec->opt_channels; i; i = i->next)
+			all_channels.push_back((const srd_channel*)i->data);
+
+		// Auto select the initial channels
+		for (const srd_channel *pdch : all_channels)
+			for (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->name().toLower()))
+					channels[pdch] = l;
+			}
+
+		assert(decoder_stack);
+		assert(!decoder_stack->stack().empty());
+		assert(decoder_stack->stack().front());
+		decoder_stack->stack().front()->set_channels(channels);
+
+		// 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> > Session::get_decode_signals() const
+{
+	shared_lock<shared_mutex> lock(signals_mutex_);
+	return decode_traces_;
+}
+
+void Session::remove_decode_signal(view::DecodeTrace *signal)
+{
+	for (auto i = decode_traces_.begin(); i != decode_traces_.end(); i++)
+		if ((*i).get() == signal) {
+			decode_traces_.erase(i);
+			signals_changed();
+			return;
+		}
+}
+#endif
+
+void Session::set_capture_state(capture_state state)
+{
+	bool changed;
+
+	{
+		lock_guard<mutex> lock(sampling_mutex_);
+		changed = capture_state_ != state;
+		capture_state_ = state;
+	}
+
+	if (changed)
+		capture_state_changed(state);
+}
+
+void Session::update_signals()
+{
+	assert(device_);
+
+	lock_guard<recursive_mutex> lock(data_mutex_);
+
+	const shared_ptr<sigrok::Device> sr_dev = device_->device();
+	if (!sr_dev) {
+		signals_.clear();
+		logic_data_.reset();
+		return;
+	}
+
+	// Detect what data types we will receive
+	auto channels = sr_dev->channels();
+	unsigned int logic_channel_count = std::count_if(
+		channels.begin(), channels.end(),
+		[] (shared_ptr<Channel> channel) {
+			return channel->type() == ChannelType::LOGIC; });
+
+	// Create data containers for the logic data segments
+	{
+		lock_guard<recursive_mutex> data_lock(data_mutex_);
+
+		if (logic_channel_count == 0) {
+			logic_data_.reset();
+		} else if (!logic_data_ ||
+			logic_data_->num_channels() != logic_channel_count) {
+			logic_data_.reset(new data::Logic(
+				logic_channel_count));
+			assert(logic_data_);
+		}
+	}
+
+	// Make the Signals list
+	{
+		unique_lock<shared_mutex> lock(signals_mutex_);
+
+		unordered_set< shared_ptr<view::Signal> > prev_sigs(signals_);
+		signals_.clear();
+
+		for (auto channel : sr_dev->channels()) {
+			shared_ptr<view::Signal> signal;
+
+			// Find the channel in the old signals
+			const auto iter = std::find_if(
+				prev_sigs.cbegin(), prev_sigs.cend(),
+				[&](const shared_ptr<view::Signal> &s) {
+					return s->channel() == channel;
+				});
+			if (iter != prev_sigs.end()) {
+				// Copy the signal from the old set to the new
+				signal = *iter;
+				auto logic_signal = dynamic_pointer_cast<
+					view::LogicSignal>(signal);
+				if (logic_signal)
+					logic_signal->set_logic_data(
+						logic_data_);
+			} else {
+				// Create a new signal
+				switch(channel->type()->id()) {
+				case SR_CHANNEL_LOGIC:
+					signal = shared_ptr<view::Signal>(
+						new view::LogicSignal(*this,
+							device_, channel,
+							logic_data_));
+					break;
+
+				case SR_CHANNEL_ANALOG:
+				{
+					shared_ptr<data::Analog> data(
+						new data::Analog());
+					signal = shared_ptr<view::Signal>(
+						new view::AnalogSignal(
+							*this, channel, data));
+					break;
+				}
+
+				default:
+					assert(0);
+					break;
+				}
+			}
+
+			assert(signal);
+			signals_.insert(signal);
+		}
+	}
+
+	signals_changed();
+}
+
+shared_ptr<view::Signal> Session::signal_from_channel(
+	shared_ptr<Channel> channel) const
+{
+	lock_guard<boost::shared_mutex> lock(signals_mutex_);
+	for (shared_ptr<view::Signal> sig : signals_) {
+		assert(sig);
+		if (sig->channel() == channel)
+			return sig;
+	}
+	return shared_ptr<view::Signal>();
+}
+
+void Session::sample_thread_proc(shared_ptr<devices::Device> device,
+	function<void (const QString)> error_handler)
+{
+	assert(device);
+	assert(error_handler);
+
+	(void)device;
+
+	cur_samplerate_ = device_->read_config<uint64_t>(ConfigKey::SAMPLERATE);
+
+	out_of_memory_ = false;
+
+	try {
+		device_->start();
+	} catch (Error e) {
+		error_handler(e.what());
+		return;
+	}
+
+	set_capture_state(device_->session()->trigger() ?
+		AwaitingTrigger : Running);
+
+	device_->run();
+	set_capture_state(Stopped);
+
+	// Confirm that SR_DF_END was received
+	if (cur_logic_segment_) {
+		qDebug("SR_DF_END was not received.");
+		assert(0);
+	}
+
+	if (out_of_memory_)
+		error_handler(tr("Out of memory, acquisition stopped."));
+}
+
+void Session::feed_in_header()
+{
+	cur_samplerate_ = device_->read_config<uint64_t>(ConfigKey::SAMPLERATE);
+}
+
+void Session::feed_in_meta(shared_ptr<Meta> meta)
+{
+	for (auto entry : meta->config()) {
+		switch (entry.first->id()) {
+		case SR_CONF_SAMPLERATE:
+			// We can't rely on the header to always contain the sample rate,
+			// so in case it's supplied via a meta packet, we use it.
+			if (!cur_samplerate_)
+				cur_samplerate_ = g_variant_get_uint64(entry.second.gobj());
+
+			/// @todo handle samplerate changes
+			break;
+		default:
+			// Unknown metadata is not an error.
+			break;
+		}
+	}
+
+	signals_changed();
+}
+
+void Session::feed_in_trigger()
+{
+	// The channel containing most samples should be most accurate
+	uint64_t sample_count = 0;
+
+	for (const shared_ptr<pv::data::SignalData> d : get_data()) {
+		assert(d);
+		uint64_t temp_count = 0;
+
+		const vector< shared_ptr<pv::data::Segment> > segments =
+			d->segments();
+		for (const shared_ptr<pv::data::Segment> &s : segments)
+			temp_count += s->get_sample_count();
+
+		if (temp_count > sample_count)
+			sample_count = temp_count;
+	}
+
+	trigger_event(sample_count / get_samplerate());
+}
+
+void Session::feed_in_frame_begin()
+{
+	if (cur_logic_segment_ || !cur_analog_segments_.empty())
+		frame_began();
+}
+
+void Session::feed_in_logic(shared_ptr<Logic> logic)
+{
+	lock_guard<recursive_mutex> lock(data_mutex_);
+
+	const size_t sample_count = logic->data_length() / logic->unit_size();
+
+	if (!logic_data_) {
+		// The only reason logic_data_ would not have been created is
+		// if it was not possible to determine the signals when the
+		// device was created.
+		update_signals();
+	}
+
+	if (!cur_logic_segment_) {
+		// This could be the first packet after a trigger
+		set_capture_state(Running);
+
+		// Create a new data segment
+		cur_logic_segment_ = shared_ptr<data::LogicSegment>(
+			new data::LogicSegment(
+				logic, cur_samplerate_, sample_count));
+		logic_data_->push_segment(cur_logic_segment_);
+
+		// @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 segment
+		cur_logic_segment_->append_payload(logic);
+	}
+
+	data_received();
+}
+
+void Session::feed_in_analog(shared_ptr<Analog> analog)
+{
+	lock_guard<recursive_mutex> lock(data_mutex_);
+
+	const vector<shared_ptr<Channel>> channels = analog->channels();
+	const unsigned int channel_count = channels.size();
+	const size_t sample_count = analog->num_samples() / channel_count;
+	const float *data = static_cast<const float *>(analog->data_pointer());
+	bool sweep_beginning = false;
+
+	if (signals_.empty())
+		update_signals();
+
+	for (auto channel : channels) {
+		shared_ptr<data::AnalogSegment> segment;
+
+		// Try to get the segment of the channel
+		const map< shared_ptr<Channel>, shared_ptr<data::AnalogSegment> >::
+			iterator iter = cur_analog_segments_.find(channel);
+		if (iter != cur_analog_segments_.end())
+			segment = (*iter).second;
+		else {
+			// If no segment was found, this means we haven't
+			// created one yet. i.e. this is the first packet
+			// in the sweep containing this segment.
+			sweep_beginning = true;
+
+			// Create a segment, keep it in the maps of channels
+			segment = shared_ptr<data::AnalogSegment>(
+				new data::AnalogSegment(
+					cur_samplerate_, sample_count));
+			cur_analog_segments_[channel] = segment;
+
+			// Find the analog data associated with the channel
+			shared_ptr<view::AnalogSignal> sig =
+				dynamic_pointer_cast<view::AnalogSignal>(
+					signal_from_channel(channel));
+			assert(sig);
+
+			shared_ptr<data::Analog> data(sig->analog_data());
+			assert(data);
+
+			// Push the segment into the analog data.
+			data->push_segment(segment);
+		}
+
+		assert(segment);
+
+		// Append the samples in the segment
+		segment->append_interleaved_samples(data++, sample_count,
+			channel_count);
+	}
+
+	if (sweep_beginning) {
+		// This could be the first packet after a trigger
+		set_capture_state(Running);
+	}
+
+	data_received();
+}
+
+void Session::data_feed_in(shared_ptr<sigrok::Device> device,
+	shared_ptr<Packet> packet)
+{
+	(void)device;
+
+	assert(device);
+	assert(device == device_->device());
+	assert(packet);
+
+	switch (packet->type()->id()) {
+	case SR_DF_HEADER:
+		feed_in_header();
+		break;
+
+	case SR_DF_META:
+		feed_in_meta(dynamic_pointer_cast<Meta>(packet->payload()));
+		break;
+
+	case SR_DF_TRIGGER:
+		feed_in_trigger();
+		break;
+
+	case SR_DF_FRAME_BEGIN:
+		feed_in_frame_begin();
+		break;
+
+	case SR_DF_LOGIC:
+		try {
+			feed_in_logic(dynamic_pointer_cast<Logic>(packet->payload()));
+		} catch (std::bad_alloc) {
+			out_of_memory_ = true;
+			device_->stop();
+		}
+		break;
+
+	case SR_DF_ANALOG:
+		try {
+			feed_in_analog(dynamic_pointer_cast<Analog>(packet->payload()));
+		} catch (std::bad_alloc) {
+			out_of_memory_ = true;
+			device_->stop();
+		}
+		break;
+
+	case SR_DF_END:
+	{
+		{
+			lock_guard<recursive_mutex> lock(data_mutex_);
+			cur_logic_segment_.reset();
+			cur_analog_segments_.clear();
+		}
+		frame_ended();
+		break;
+	}
+	default:
+		break;
+	}
+}
+
+} // namespace pv
diff --git a/pv/session.hpp b/pv/session.hpp
new file mode 100644
index 0000000..ac3d654
--- /dev/null
+++ b/pv/session.hpp
@@ -0,0 +1,200 @@
+/*
+ * 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_SESSION_HPP
+#define PULSEVIEW_PV_SESSION_HPP
+
+#include <map>
+#include <memory>
+#include <mutex>
+#include <set>
+#include <string>
+#include <thread>
+#include <unordered_set>
+#include <vector>
+
+#ifdef _WIN32
+// Windows: Avoid boost/thread namespace pollution (which includes windows.h).
+#define NOGDI
+#define NORESOURCE
+#endif
+#include <boost/thread/shared_mutex.hpp>
+
+#include <QObject>
+#include <QString>
+
+#include "util.hpp"
+
+struct srd_decoder;
+struct srd_channel;
+
+namespace sigrok {
+class Analog;
+class Channel;
+class Device;
+class Logic;
+class Meta;
+class Packet;
+class Session;
+}
+
+namespace pv {
+
+class DeviceManager;
+
+namespace data {
+class Analog;
+class AnalogSegment;
+class Logic;
+class LogicSegment;
+class SignalData;
+}
+
+namespace devices {
+class Device;
+}
+
+namespace view {
+class DecodeTrace;
+class LogicSignal;
+class Signal;
+}
+
+class Session : public QObject
+{
+	Q_OBJECT
+
+public:
+	enum capture_state {
+		Stopped,
+		AwaitingTrigger,
+		Running
+	};
+
+public:
+	Session(DeviceManager &device_manager);
+
+	~Session();
+
+	DeviceManager& device_manager();
+
+	const DeviceManager& device_manager() const;
+
+	std::shared_ptr<sigrok::Session> session() const;
+
+	std::shared_ptr<devices::Device> device() const;
+
+	/**
+	 * Sets device instance that will be used in the next capture session.
+	 */
+	void set_device(std::shared_ptr<devices::Device> device);
+
+	void set_default_device();
+
+	capture_state get_capture_state() const;
+
+	void start_capture(std::function<void (const QString)> error_handler);
+
+	void stop_capture();
+
+	std::set< std::shared_ptr<data::SignalData> > get_data() const;
+
+	double get_samplerate() const;
+
+	const std::unordered_set< std::shared_ptr<view::Signal> >
+		signals() const;
+
+#ifdef ENABLE_DECODE
+	bool add_decoder(srd_decoder *const dec);
+
+	std::vector< std::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();
+
+	std::shared_ptr<view::Signal> signal_from_channel(
+		std::shared_ptr<sigrok::Channel> channel) const;
+
+private:
+	void sample_thread_proc(std::shared_ptr<devices::Device> device,
+		std::function<void (const QString)> error_handler);
+
+	void feed_in_header();
+
+	void feed_in_meta(std::shared_ptr<sigrok::Meta> meta);
+
+	void feed_in_trigger();
+
+	void feed_in_frame_begin();
+
+	void feed_in_logic(std::shared_ptr<sigrok::Logic> logic);
+
+	void feed_in_analog(std::shared_ptr<sigrok::Analog> analog);
+
+	void data_feed_in(std::shared_ptr<sigrok::Device> device,
+		std::shared_ptr<sigrok::Packet> packet);
+
+private:
+	DeviceManager &device_manager_;
+	std::shared_ptr<devices::Device> device_;
+
+	std::vector< std::shared_ptr<view::DecodeTrace> > decode_traces_;
+
+	mutable std::mutex sampling_mutex_; //!< Protects access to capture_state_.
+	capture_state capture_state_;
+
+	mutable boost::shared_mutex signals_mutex_;
+	std::unordered_set< std::shared_ptr<view::Signal> > signals_;
+
+	mutable std::recursive_mutex data_mutex_;
+	std::shared_ptr<data::Logic> logic_data_;
+	uint64_t cur_samplerate_;
+	std::shared_ptr<data::LogicSegment> cur_logic_segment_;
+	std::map< std::shared_ptr<sigrok::Channel>, std::shared_ptr<data::AnalogSegment> >
+		cur_analog_segments_;
+
+	std::thread sampling_thread_;
+
+	bool out_of_memory_;
+
+Q_SIGNALS:
+	void capture_state_changed(int state);
+	void device_selected();
+
+	void signals_changed();
+
+	void trigger_event(util::Timestamp location);
+
+	void frame_began();
+
+	void data_received();
+
+	void frame_ended();
+};
+
+} // namespace pv
+
+#endif // PULSEVIEW_PV_SESSION_HPP
diff --git a/pv/sigsession.cpp b/pv/sigsession.cpp
deleted file mode 100644
index b441dab..0000000
--- a/pv/sigsession.cpp
+++ /dev/null
@@ -1,650 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 1221750..0000000
--- a/pv/sigsession.h
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- * 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
index 3d0f12f..4642ed6 100644
--- a/pv/storesession.cpp
+++ b/pv/storesession.cpp
@@ -18,36 +18,70 @@
  * 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;
+#include <cassert>
+
+#ifdef _WIN32
+// Windows: Avoid boost/thread namespace pollution (which includes windows.h).
+#define NOGDI
+#define NORESOURCE
+#endif
+#include <boost/thread/locks.hpp>
+#include <boost/thread/shared_mutex.hpp>
+
+#include "storesession.hpp"
+
+#include <pv/devicemanager.hpp>
+#include <pv/session.hpp>
+#include <pv/data/logic.hpp>
+#include <pv/data/logicsegment.hpp>
+#include <pv/devices/device.hpp>
+#include <pv/view/signal.hpp>
+
+#include <libsigrokcxx/libsigrokcxx.hpp>
+
+using boost::shared_lock;
+using boost::shared_mutex;
+
 using std::deque;
+using std::dynamic_pointer_cast;
+using std::ios_base;
+using std::lock_guard;
 using std::make_pair;
+using std::map;
 using std::min;
+using std::mutex;
 using std::pair;
 using std::set;
+using std::shared_ptr;
 using std::string;
+using std::thread;
+using std::unordered_set;
 using std::vector;
 
+using Glib::VariantBase;
+
+using sigrok::ConfigKey;
+using sigrok::Error;
+using sigrok::OutputFormat;
+using sigrok::OutputFlag;
+
 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)
+	const shared_ptr<OutputFormat> &output_format,
+	const map<string, VariantBase> &options,
+	const std::pair<uint64_t, uint64_t> sample_range,
+	const Session &session) :
+	file_name_(file_name),
+	output_format_(output_format),
+	options_(options),
+	sample_range_(sample_range),
+	session_(session),
+	interrupt_(false),
+	units_stored_(0),
+	unit_count_(0)
 {
 }
 
@@ -56,140 +90,166 @@ StoreSession::~StoreSession()
 	wait();
 }
 
-pair<uint64_t, uint64_t> StoreSession::progress() const
+pair<int, int> StoreSession::progress() const
 {
-	lock_guard<mutex> lock(_mutex);
-	return make_pair(_units_stored, _unit_count);
+	return make_pair(units_stored_.load(), unit_count_.load());
 }
 
 const QString& StoreSession::error() const
 {
-	lock_guard<mutex> lock(_mutex);
-	return _error;
+	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();
+	const unordered_set< shared_ptr<view::Signal> > sigs(session_.signals());
+
+	// Add enabled channels to the data set
+	set< shared_ptr<data::SignalData> > data_set;
+
+	for (shared_ptr<view::Signal> signal : sigs)
+		if (signal->enabled())
+			data_set.insert(signal->data());
 
 	// Check we have logic data
 	if (data_set.empty() || sigs.empty()) {
-		_error = tr("No data to save.");
+		error_ = tr("No data to save.");
 		return false;
 	}
 
 	if (data_set.size() > 1) {
-		_error = tr("PulseView currently only has support for "
+		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.");
+		error_ = tr("PulseView currently only has support for "
+			"storing logic data.");
 		return false;
 	}
 
-	// Get the snapshot
-	const deque< shared_ptr<data::LogicSnapshot> > &snapshots =
-		data->get_snapshots();
+	// Get the segment
+	const deque< shared_ptr<data::LogicSegment> > &segments =
+		data->logic_segments();
 
-	if (snapshots.empty()) {
-		_error = tr("No snapshots to save.");
+	if (segments.empty()) {
+		error_ = tr("No segments 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());
+	const shared_ptr<data::LogicSegment> segment(segments.front());
+	assert(segment);
+
+	// Check whether the user wants to export a certain sample range
+	if (sample_range_.first == sample_range_.second) {
+		start_sample_ = 0;
+		sample_count_ = segment->get_sample_count();
+	} else {
+		if (sample_range_.first > sample_range_.second) {
+			start_sample_ = sample_range_.second;
+			sample_count_ = sample_range_.first - sample_range_.second;
+		} else {
+			start_sample_ = sample_range_.first;
+			sample_count_ = sample_range_.second - sample_range_.first;
+		}
 	}
-	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.");
+	try {
+		const auto context = session_.device_manager().context();
+		auto device = session_.device()->device();
+
+		map<string, Glib::VariantBase> options = options_;
+
+		if (!output_format_->test_flag(OutputFlag::INTERNAL_IO_HANDLING))
+			output_stream_.open(file_name_, ios_base::binary |
+					ios_base::trunc | ios_base::out);
+
+		output_ = output_format_->create_output(file_name_, device, options);
+		auto meta = context->create_meta_packet(
+			{{ConfigKey::SAMPLERATE, Glib::Variant<guint64>::create(
+				segment->samplerate())}});
+		output_->receive(meta);
+	} catch (Error error) {
+		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);
+	thread_ = std::thread(&StoreSession::store_proc, this, segment);
 	return true;
 }
 
 void StoreSession::wait()
 {
-	if (_thread.joinable())
-		_thread.join();
+	if (thread_.joinable())
+		thread_.join();
 }
 
 void StoreSession::cancel()
 {
-	_thread.interrupt();
+	interrupt_ = true;
 }
 
-void StoreSession::store_proc(shared_ptr<data::LogicSnapshot> snapshot)
+void StoreSession::store_proc(shared_ptr<data::LogicSegment> segment)
 {
-	assert(snapshot);
+	assert(segment);
 
-	uint64_t start_sample = 0;
+	unsigned progress_scale = 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();
+	const int unit_size = segment->unit_size();
 	assert(unit_size != 0);
 
-	{
-		lock_guard<mutex> lock(_mutex);
-		_unit_count = snapshot->get_sample_count();
-	}
+	// Qt needs the progress values to fit inside an int. If they would
+	// not, scale the current and max values down until they do.
+	while ((sample_count_ >> progress_scale) > INT_MAX)
+		progress_scale ++;
+
+	unit_count_ = sample_count_ >> progress_scale;
 
 	const unsigned int samples_per_block = BlockSize / unit_size;
 
-	while (!boost::this_thread::interruption_requested() &&
-		start_sample < _unit_count)
-	{
+	while (!interrupt_ && sample_count_) {
 		progress_updated();
 
-		const uint64_t end_sample = min(
-			start_sample + samples_per_block, _unit_count);
-		snapshot->get_samples(data, start_sample, end_sample);
+		const uint64_t packet_len =
+			std::min((uint64_t)samples_per_block, sample_count_);
 
-		if(sr_session_append(_file_name.c_str(), data, unit_size,
-			end_sample - start_sample) != SR_OK)
-		{
-			_error = tr("Error while saving.");
-			break;
-		}
+		segment->get_samples(data, start_sample_, start_sample_ + packet_len);
 
-		start_sample = end_sample;
+		size_t length = packet_len * unit_size;
 
-		{
-			lock_guard<mutex> lock(_mutex);
-			_units_stored = start_sample;
+		try {
+			const auto context = session_.device_manager().context();
+			auto logic = context->create_logic_packet(data, length, unit_size);
+			const string data = output_->receive(logic);
+			if (output_stream_.is_open())
+				output_stream_ << data;
+		} catch (Error error) {
+			error_ = tr("Error while saving.");
+			break;
 		}
+
+		sample_count_ -= packet_len;
+		start_sample_ += packet_len;
+		units_stored_ = unit_count_ - (sample_count_ >> progress_scale);
 	}
 
+	// Zeroing the progress variables indicates completion
+	units_stored_ = unit_count_ = 0;
+
 	progress_updated();
 
+	output_.reset();
+	output_stream_.close();
+
 	delete[] data;
 }
 
diff --git a/pv/storesession.h b/pv/storesession.hpp
similarity index 52%
rename from pv/storesession.h
rename to pv/storesession.hpp
index 5ef92a3..6a08180 100644
--- a/pv/storesession.h
+++ b/pv/storesession.hpp
@@ -18,23 +18,34 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_STORESESSION_H
-#define PULSEVIEW_PV_STORESESSION_H
+#ifndef PULSEVIEW_PV_STORESESSION_HPP
+#define PULSEVIEW_PV_STORESESSION_HPP
 
 #include <stdint.h>
 
+#include <atomic>
+#include <fstream>
+#include <map>
+#include <memory>
+#include <mutex>
 #include <string>
+#include <thread>
 
-#include <boost/thread.hpp>
+#include <glibmm/variant.h>
 
 #include <QObject>
 
+namespace sigrok {
+class Output;
+class OutputFormat;
+}
+
 namespace pv {
 
-class SigSession;
+class Session;
 
 namespace data {
-class LogicSnapshot;
+class LogicSegment;
 }
 
 class StoreSession : public QObject
@@ -46,11 +57,14 @@ private:
 
 public:
 	StoreSession(const std::string &file_name,
-		const SigSession &session);
+		const std::shared_ptr<sigrok::OutputFormat> &output_format,
+		const std::map<std::string, Glib::VariantBase> &options,
+		const std::pair<uint64_t, uint64_t> sample_range,
+		const Session &session);
 
 	~StoreSession();
 
-	std::pair<uint64_t, uint64_t> progress() const;
+	std::pair<int, int> progress() const;
 
 	const QString& error() const;
 
@@ -61,23 +75,33 @@ public:
 	void cancel();
 
 private:
-	void store_proc(boost::shared_ptr<pv::data::LogicSnapshot> snapshot);
+	void store_proc(std::shared_ptr<pv::data::LogicSegment> segment);
 
-signals:
+Q_SIGNALS:
 	void progress_updated();
 
 private:
-	const std::string _file_name;
-	const SigSession &_session;
+	const std::string file_name_;
+	const std::shared_ptr<sigrok::OutputFormat> output_format_;
+	const std::map<std::string, Glib::VariantBase> options_;
+	const std::pair<uint64_t, uint64_t> sample_range_;
+	const Session &session_;
+
+	std::shared_ptr<sigrok::Output> output_;
+	std::ofstream output_stream_;
+
+	std::thread thread_;
+
+	std::atomic<bool> interrupt_;
+
+	std::atomic<int> units_stored_, unit_count_;
 
-	boost::thread _thread;
+	mutable std::mutex mutex_;
+	QString error_;
 
-	mutable boost::mutex _mutex;
-	uint64_t _units_stored;
-	uint64_t _unit_count;
-	QString _error;
+	uint64_t start_sample_, sample_count_;
 };
 
 } // pv
 
-#endif // PULSEVIEW_PV_STORESESSION_H
+#endif // PULSEVIEW_PV_STORESESSION_HPP
diff --git a/pv/toolbars/mainbar.cpp b/pv/toolbars/mainbar.cpp
new file mode 100644
index 0000000..8c0b063
--- /dev/null
+++ b/pv/toolbars/mainbar.cpp
@@ -0,0 +1,565 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2012-2015 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 <algorithm>
+#include <cassert>
+
+#include <QAction>
+#include <QDebug>
+#include <QHelpEvent>
+#include <QMenu>
+#include <QToolTip>
+
+#include "mainbar.hpp"
+
+#include <pv/devicemanager.hpp>
+#include <pv/devices/hardwaredevice.hpp>
+#include <pv/mainwindow.hpp>
+#include <pv/popups/deviceoptions.hpp>
+#include <pv/popups/channels.hpp>
+#include <pv/util.hpp>
+#include <pv/widgets/exportmenu.hpp>
+#include <pv/widgets/importmenu.hpp>
+
+#include <libsigrokcxx/libsigrokcxx.hpp>
+
+using std::back_inserter;
+using std::copy;
+using std::list;
+using std::map;
+using std::max;
+using std::min;
+using std::shared_ptr;
+using std::string;
+using std::vector;
+
+using sigrok::Capability;
+using sigrok::ConfigKey;
+using sigrok::Error;
+using sigrok::InputFormat;
+
+namespace pv {
+namespace toolbars {
+
+const uint64_t MainBar::MinSampleCount = 100ULL;
+const uint64_t MainBar::MaxSampleCount = 1000000000000ULL;
+const uint64_t MainBar::DefaultSampleCount = 1000000;
+
+MainBar::MainBar(Session &session, MainWindow &main_window) :
+	QToolBar("Sampling Bar", &main_window),
+	session_(session),
+	main_window_(main_window),
+	device_selector_(this, session.device_manager(),
+		main_window.action_connect()),
+	configure_button_(this),
+	configure_button_action_(nullptr),
+	channels_button_(this),
+	channels_button_action_(nullptr),
+	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),
+	run_stop_button_action_(nullptr),
+	menu_button_(this)
+{
+	setObjectName(QString::fromUtf8("MainBar"));
+
+	setMovable(false);
+	setFloatable(false);
+	setContextMenuPolicy(Qt::PreventContextMenu);
+
+	// Open button
+	QToolButton *const open_button = new QToolButton(this);
+
+	widgets::ImportMenu *import_menu = new widgets::ImportMenu(this,
+		session.device_manager().context(),
+		main_window.action_open());
+	connect(import_menu,
+		SIGNAL(format_selected(std::shared_ptr<sigrok::InputFormat>)),
+		&main_window_,
+		SLOT(import_file(std::shared_ptr<sigrok::InputFormat>)));
+
+	open_button->setMenu(import_menu);
+	open_button->setDefaultAction(main_window.action_open());
+	open_button->setPopupMode(QToolButton::MenuButtonPopup);
+
+	// Save button
+	QToolButton *const save_button = new QToolButton(this);
+
+	vector<QAction *> open_actions;
+	open_actions.push_back(main_window.action_save_as());
+	open_actions.push_back(main_window.action_save_selection_as());
+
+	widgets::ExportMenu *export_menu = new widgets::ExportMenu(this,
+		session.device_manager().context(),
+		open_actions);
+	connect(export_menu,
+		SIGNAL(format_selected(std::shared_ptr<sigrok::OutputFormat>)),
+		&main_window_,
+		SLOT(export_file(std::shared_ptr<sigrok::OutputFormat>)));
+
+	save_button->setMenu(export_menu);
+	save_button->setDefaultAction(main_window.action_save_as());
+	save_button->setPopupMode(QToolButton::MenuButtonPopup);
+
+	// Device selector menu
+	connect(&device_selector_, SIGNAL(device_selected()),
+		this, SLOT(on_device_selected()));
+
+	// Setup the decoder button
+#ifdef ENABLE_DECODE
+	QToolButton *add_decoder_button = new QToolButton(this);
+	add_decoder_button->setIcon(QIcon::fromTheme("add-decoder",
+		QIcon(":/icons/add-decoder.svg")));
+	add_decoder_button->setPopupMode(QToolButton::InstantPopup);
+	add_decoder_button->setMenu(main_window_.menu_decoder_add());
+#endif
+
+	// Setup the burger menu
+	QMenu *const menu = new QMenu(this);
+
+	QMenu *const menu_view = new QMenu;
+	menu_view->setTitle(tr("&View"));
+	menu_view->addAction(main_window.action_view_sticky_scrolling());
+	menu_view->addSeparator();
+	menu_view->addAction(main_window.action_view_coloured_bg());
+
+	QMenu *const menu_help = new QMenu;
+	menu_help->setTitle(tr("&Help"));
+	menu_help->addAction(main_window.action_about());
+
+	menu->addAction(menu_view->menuAction());
+	menu->addSeparator();
+	menu->addAction(menu_help->menuAction());
+	menu->addSeparator();
+	menu->addAction(main_window.action_quit());
+
+	menu_button_.setMenu(menu);
+	menu_button_.setPopupMode(QToolButton::InstantPopup);
+	menu_button_.setIcon(QIcon::fromTheme("menu",
+		QIcon(":/icons/menu.svg")));
+
+	// Setup the toolbar
+	addWidget(open_button);
+	addWidget(save_button);
+	addSeparator();
+	addAction(main_window.action_view_zoom_in());
+	addAction(main_window.action_view_zoom_out());
+	addAction(main_window.action_view_zoom_fit());
+	addAction(main_window.action_view_zoom_one_to_one());
+	addSeparator();
+	addAction(main_window.action_view_show_cursors());
+	addSeparator();
+
+	connect(&run_stop_button_, SIGNAL(clicked()),
+		this, SLOT(on_run_stop()));
+	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::Session::Stopped);
+
+	configure_button_.setIcon(QIcon::fromTheme("configure",
+		QIcon(":/icons/configure.png")));
+
+	channels_button_.setIcon(QIcon::fromTheme("channels",
+		QIcon(":/icons/channels.svg")));
+
+	run_stop_button_.setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+
+	addWidget(&device_selector_);
+	configure_button_action_ = addWidget(&configure_button_);
+	channels_button_action_ = addWidget(&channels_button_);
+	addWidget(&sample_count_);
+	addWidget(&sample_rate_);
+	run_stop_button_action_ = addWidget(&run_stop_button_);
+#ifdef ENABLE_DECODE
+	addSeparator();
+	addWidget(add_decoder_button);
+#endif
+
+	QWidget *const spacer = new QWidget();
+	spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+	addWidget(spacer);
+
+	addWidget(&menu_button_);
+
+	sample_count_.installEventFilter(this);
+	sample_rate_.installEventFilter(this);
+}
+
+void MainBar::update_device_list()
+{
+	DeviceManager &mgr = session_.device_manager();
+	shared_ptr<devices::Device> selected_device = session_.device();
+	list< shared_ptr<devices::Device> > devs;
+
+	copy(mgr.devices().begin(), mgr.devices().end(), back_inserter(devs));
+
+	if (std::find(devs.begin(), devs.end(), selected_device) == devs.end())
+		devs.push_back(selected_device);
+
+	device_selector_.set_device_list(devs, selected_device);
+	update_device_config_widgets();
+}
+
+
+void MainBar::set_capture_state(pv::Session::capture_state state)
+{
+	const QIcon *icons[] = {&icon_grey_, &icon_red_, &icon_green_};
+	run_stop_button_.setIcon(*icons[state]);
+	run_stop_button_.setText((state == pv::Session::Stopped) ?
+		tr("Run") : tr("Stop"));
+	run_stop_button_.setShortcut(QKeySequence(Qt::Key_Space));
+
+	bool ui_enabled = (state == pv::Session::Stopped) ? true : false;
+
+	device_selector_.setEnabled(ui_enabled);
+	configure_button_.setEnabled(ui_enabled);
+	channels_button_.setEnabled(ui_enabled);
+	sample_count_.setEnabled(ui_enabled);
+	sample_rate_.setEnabled(ui_enabled);
+}
+
+void MainBar::update_sample_rate_selector()
+{
+	Glib::VariantContainerBase gvar_dict;
+	GVariant *gvar_list;
+	const uint64_t *elements = nullptr;
+	gsize num_elements;
+	map< const ConfigKey*, std::set<Capability> > keys;
+
+	if (updating_sample_rate_) {
+		sample_rate_.show_none();
+		return;
+	}
+
+	const shared_ptr<devices::Device> device =
+		device_selector_.selected_device();
+	if (!device)
+		return;
+
+	assert(!updating_sample_rate_);
+	updating_sample_rate_ = true;
+
+	const shared_ptr<sigrok::Device> sr_dev = device->device();
+
+	if (sr_dev->config_check(ConfigKey::SAMPLERATE, Capability::LIST)) {
+		gvar_dict = sr_dev->config_list(ConfigKey::SAMPLERATE);
+	} else {
+		sample_rate_.show_none();
+		updating_sample_rate_ = false;
+		return;
+	}
+
+	if ((gvar_list = g_variant_lookup_value(gvar_dict.gobj(),
+			"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.gobj(),
+			"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;
+
+	update_sample_rate_selector_value();
+}
+
+void MainBar::update_sample_rate_selector_value()
+{
+	if (updating_sample_rate_)
+		return;
+
+	const shared_ptr<devices::Device> device =
+		device_selector_.selected_device();
+	if (!device)
+		return;
+
+	try {
+		auto gvar = device->device()->config_get(ConfigKey::SAMPLERATE);
+		uint64_t samplerate =
+			Glib::VariantBase::cast_dynamic<Glib::Variant<guint64>>(gvar).get();
+		assert(!updating_sample_rate_);
+		updating_sample_rate_ = true;
+		sample_rate_.set_value(samplerate);
+		updating_sample_rate_ = false;
+	} catch (Error error) {
+		qDebug() << "WARNING: Failed to get value of sample rate";
+		return;
+	}
+}
+
+void MainBar::update_sample_count_selector()
+{
+	if (updating_sample_count_)
+		return;
+
+	const shared_ptr<devices::Device> device =
+		device_selector_.selected_device();
+	if (!device)
+		return;
+
+	const shared_ptr<sigrok::Device> sr_dev = device->device();
+
+	assert(!updating_sample_count_);
+	updating_sample_count_ = true;
+
+	if (!sample_count_supported_) {
+		sample_count_.show_none();
+		updating_sample_count_ = false;
+		return;
+	}
+
+	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 (sr_dev->config_check(ConfigKey::LIMIT_SAMPLES, Capability::LIST)) {
+		auto gvar = sr_dev->config_list(ConfigKey::LIMIT_SAMPLES);
+		if (gvar.gobj())
+			g_variant_get(gvar.gobj(), "(tt)",
+				&min_sample_count, &max_sample_count);
+	}
+
+	min_sample_count = min(max(min_sample_count, MinSampleCount),
+		max_sample_count);
+
+	sample_count_.show_125_list(
+		min_sample_count, max_sample_count);
+
+	if (sr_dev->config_check(ConfigKey::LIMIT_SAMPLES, Capability::GET)) {
+		auto gvar = sr_dev->config_get(ConfigKey::LIMIT_SAMPLES);
+		sample_count = g_variant_get_uint64(gvar.gobj());
+		if (sample_count == 0)
+			sample_count = DefaultSampleCount;
+		sample_count = min(max(sample_count, MinSampleCount),
+			max_sample_count);
+	}
+
+	sample_count_.set_value(sample_count);
+
+	updating_sample_count_ = false;
+}
+
+void MainBar::update_device_config_widgets()
+{
+	using namespace pv::popups;
+
+	const shared_ptr<devices::Device> device =
+		device_selector_.selected_device();
+
+	// Hide the widgets if no device is selected
+	channels_button_action_->setVisible(!!device);
+	run_stop_button_action_->setVisible(!!device);
+	if (!device) {
+		configure_button_action_->setVisible(false);
+		sample_count_.show_none();
+		sample_rate_.show_none();
+		return;
+	}
+
+	const shared_ptr<sigrok::Device> sr_dev = device->device();
+	if (!sr_dev)
+		return;
+
+	// Update the configure popup
+	DeviceOptions *const opts = new DeviceOptions(sr_dev, this);
+	configure_button_action_->setVisible(
+		!opts->binding().properties().empty());
+	configure_button_.set_popup(opts);
+
+	// Update the channels popup
+	Channels *const channels = new Channels(session_, this);
+	channels_button_.set_popup(channels);
+
+	// Update supported options.
+	sample_count_supported_ = false;
+
+	if (sr_dev->config_check(ConfigKey::LIMIT_SAMPLES, Capability::SET))
+		sample_count_supported_ = true;
+
+	if (sr_dev->config_check(ConfigKey::LIMIT_FRAMES, Capability::SET)) {
+		sr_dev->config_set(ConfigKey::LIMIT_FRAMES,
+			Glib::Variant<guint64>::create(1));
+			on_config_changed();
+	}
+
+	// Add notification of reconfigure events
+	disconnect(this, SLOT(on_config_changed()));
+	connect(&opts->binding(), SIGNAL(config_changed()),
+		this, SLOT(on_config_changed()));
+
+	// Update sweep timing widgets.
+	update_sample_count_selector();
+	update_sample_rate_selector();
+}
+
+void MainBar::commit_sample_count()
+{
+	uint64_t sample_count = 0;
+
+	const shared_ptr<devices::Device> device =
+		device_selector_.selected_device();
+	if (!device)
+		return;
+
+	const shared_ptr<sigrok::Device> sr_dev = device->device();
+
+	sample_count = sample_count_.value();
+	if (sample_count_supported_) {
+		try {
+			sr_dev->config_set(ConfigKey::LIMIT_SAMPLES,
+				Glib::Variant<guint64>::create(sample_count));
+			update_sample_count_selector();
+		} catch (Error error) {
+			qDebug() << "Failed to configure sample count.";
+			return;
+		}
+	}
+
+	// Devices with built-in memory might impose limits on certain
+	// configurations, so let's check what sample rate the driver
+	// lets us use now.
+	update_sample_rate_selector();
+}
+
+void MainBar::commit_sample_rate()
+{
+	uint64_t sample_rate = 0;
+
+	const shared_ptr<devices::Device> device =
+		device_selector_.selected_device();
+	if (!device)
+		return;
+
+	const shared_ptr<sigrok::Device> sr_dev = device->device();
+
+	sample_rate = sample_rate_.value();
+	if (sample_rate == 0)
+		return;
+
+	try {
+		sr_dev->config_set(ConfigKey::SAMPLERATE,
+			Glib::Variant<guint64>::create(sample_rate));
+		update_sample_rate_selector();
+	} catch (Error error) {
+		qDebug() << "Failed to configure samplerate.";
+		return;
+	}
+
+	// Devices with built-in memory might impose limits on certain
+	// configurations, so let's check what sample count the driver
+	// lets us use now.
+	update_sample_count_selector();
+}
+
+void MainBar::on_device_selected()
+{
+	shared_ptr<devices::Device> device = device_selector_.selected_device();
+	if (!device)
+		return;
+
+	main_window_.select_device(device);
+
+	update_device_config_widgets();
+}
+
+void MainBar::on_sample_count_changed()
+{
+	if (!updating_sample_count_)
+		commit_sample_count();
+}
+
+void MainBar::on_sample_rate_changed()
+{
+	if (!updating_sample_rate_)
+		commit_sample_rate();
+}
+
+void MainBar::on_run_stop()
+{
+	commit_sample_count();
+	commit_sample_rate();	
+	main_window_.run_stop();
+}
+
+void MainBar::on_config_changed()
+{
+	commit_sample_count();
+	commit_sample_rate();	
+}
+
+bool MainBar::eventFilter(QObject *watched, QEvent *event)
+{
+	if (sample_count_supported_ && (watched == &sample_count_ ||
+			watched == &sample_rate_) &&
+			(event->type() == QEvent::ToolTip)) {
+		auto sec = pv::util::Timestamp(sample_count_.value()) / sample_rate_.value();
+		QHelpEvent *help_event = static_cast<QHelpEvent*>(event);
+
+		QString str = tr("Total sampling time: %1").arg(
+			pv::util::format_time_si(sec, pv::util::SIPrefix::unspecified, 0, "s", false));
+		QToolTip::showText(help_event->globalPos(), str);
+
+		return true;
+	}
+
+	return false;
+}
+
+} // namespace toolbars
+} // namespace pv
diff --git a/pv/toolbars/samplingbar.h b/pv/toolbars/mainbar.hpp
similarity index 54%
rename from pv/toolbars/samplingbar.h
rename to pv/toolbars/mainbar.hpp
index e59d2f9..6e2a706 100644
--- a/pv/toolbars/samplingbar.h
+++ b/pv/toolbars/mainbar.hpp
@@ -18,38 +18,42 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_TOOLBARS_SAMPLINGBAR_H
-#define PULSEVIEW_PV_TOOLBARS_SAMPLINGBAR_H
+#ifndef PULSEVIEW_PV_TOOLBARS_MAINBAR_HPP
+#define PULSEVIEW_PV_TOOLBARS_MAINBAR_HPP
 
 #include <stdint.h>
 
 #include <list>
-#include <map>
-
-#include <boost/shared_ptr.hpp>
+#include <memory>
 
 #include <QComboBox>
 #include <QDoubleSpinBox>
+#include <QMenu>
 #include <QToolBar>
 #include <QToolButton>
 
-#include <pv/sigsession.h>
-#include <pv/widgets/popuptoolbutton.h>
-#include <pv/widgets/sweeptimingwidget.h>
+#include <pv/session.hpp>
+#include <pv/widgets/devicetoolbutton.hpp>
+#include <pv/widgets/popuptoolbutton.hpp>
+#include <pv/widgets/sweeptimingwidget.hpp>
+
+namespace sigrok {
+class Device;
+class InputFormat;
+}
+
+Q_DECLARE_METATYPE(std::shared_ptr<sigrok::Device>)
 
 class QAction;
 
 namespace pv {
 
-class SigSession;
-
-namespace device {
-class DevInst;
-}
+class MainWindow;
+class Session;
 
 namespace toolbars {
 
-class SamplingBar : public QToolBar
+class MainBar : public QToolBar
 {
 	Q_OBJECT
 
@@ -59,19 +63,11 @@ private:
 	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;
+	MainBar(Session &session, pv::MainWindow &main_window);
 
-	void set_capture_state(pv::SigSession::capture_state state);
+	void update_device_list();
 
-signals:
-	void run_stop();
+	void set_capture_state(pv::Session::capture_state state);
 
 private:
 	void update_sample_rate_selector();
@@ -81,7 +77,7 @@ private:
 	void commit_sample_rate();
 	void commit_sample_count();
 
-private slots:
+private Q_SLOTS:
 	void on_device_selected();
 	void on_sample_count_changed();
 	void on_sample_rate_changed();
@@ -89,33 +85,38 @@ private slots:
 
 	void on_config_changed();
 
+protected:
+	bool eventFilter(QObject *watched, QEvent *event);
+
 private:
-	SigSession &_session;
+	Session &session_;
+	MainWindow &main_window_;
+
+	pv::widgets::DeviceToolButton device_selector_;
 
-	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 _configure_button;
-	QAction *_configure_button_action;
+	pv::widgets::PopupToolButton channels_button_;
+	QAction *channels_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_;
 
-	pv::widgets::SweepTimingWidget _sample_count;
-	pv::widgets::SweepTimingWidget _sample_rate;
-	bool _updating_sample_rate;
-	bool _updating_sample_count;
+	bool sample_count_supported_;
 
-	bool _sample_count_supported;
+	QIcon icon_red_;
+	QIcon icon_green_;
+	QIcon icon_grey_;
+	QToolButton run_stop_button_;
+	QAction *run_stop_button_action_;
 
-	QIcon _icon_red;
-	QIcon _icon_green;
-	QIcon _icon_grey;
-	QToolButton _run_stop_button;
+	QToolButton menu_button_;
 };
 
 } // namespace toolbars
 } // namespace pv
 
-#endif // PULSEVIEW_PV_TOOLBARS_SAMPLINGBAR_H
+#endif // PULSEVIEW_PV_TOOLBARS_MAINBAR_HPP
diff --git a/pv/toolbars/samplingbar.cpp b/pv/toolbars/samplingbar.cpp
deleted file mode 100644
index 91c4b07..0000000
--- a/pv/toolbars/samplingbar.cpp
+++ /dev/null
@@ -1,450 +0,0 @@
-/*
- * 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/util.cpp b/pv/util.cpp
new file mode 100644
index 0000000..f7ee772
--- /dev/null
+++ b/pv/util.cpp
@@ -0,0 +1,239 @@
+/*
+ * 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 "util.hpp"
+
+#include <extdef.h>
+
+#include <assert.h>
+
+#include <algorithm>
+#include <sstream>
+
+#include <QTextStream>
+#include <QDebug>
+
+using namespace Qt;
+
+namespace pv {
+namespace util {
+
+static QTextStream& operator<<(QTextStream& stream, SIPrefix prefix)
+{
+	switch (prefix) {
+	case SIPrefix::yocto: return stream << 'y';
+	case SIPrefix::zepto: return stream << 'z';
+	case SIPrefix::atto:  return stream << 'a';
+	case SIPrefix::femto: return stream << 'f';
+	case SIPrefix::pico:  return stream << 'p';
+	case SIPrefix::nano:  return stream << 'n';
+	case SIPrefix::micro: return stream << QChar(0x03BC);
+	case SIPrefix::milli: return stream << 'm';
+	case SIPrefix::kilo:  return stream << 'k';
+	case SIPrefix::mega:  return stream << 'M';
+	case SIPrefix::giga:  return stream << 'G';
+	case SIPrefix::tera:  return stream << 'T';
+	case SIPrefix::peta:  return stream << 'P';
+	case SIPrefix::exa:   return stream << 'E';
+	case SIPrefix::zetta: return stream << 'Z';
+	case SIPrefix::yotta: return stream << 'Y';
+
+	default: return stream;
+	}
+}
+
+int exponent(SIPrefix prefix)
+{
+	return 3 * (static_cast<int>(prefix) - static_cast<int>(SIPrefix::none));
+}
+
+static SIPrefix successor(SIPrefix prefix)
+{
+	assert(prefix != SIPrefix::yotta);
+	return static_cast<SIPrefix>(static_cast<int>(prefix) + 1);
+}
+
+// Insert the timestamp value into the stream in fixed-point notation
+// (and honor the precision)
+static QTextStream& operator<<(QTextStream& stream, const Timestamp& t)
+{
+	// The multiprecision types already have a function and a stream insertion
+	// operator to convert them to a string, however these functions abuse a
+	// precision value of zero to print all available decimal places instead of
+	// none, and the boost authors refuse to fix this because they don't want
+	// to break buggy code that relies on this bug.
+	// (https://svn.boost.org/trac/boost/ticket/10103)
+	// Therefore we have to work around the case where precision is zero.
+
+	int precision = stream.realNumberPrecision();
+
+	std::ostringstream ss;
+	ss << std::fixed;
+
+	if (stream.numberFlags() & QTextStream::ForceSign)
+		ss << std::showpos;
+
+	if (0 == precision)
+		ss << std::setprecision(1) << round(t);
+	else
+		ss << std::setprecision(precision) << t;
+
+	std::string str(ss.str());
+	if (0 == precision) {
+		// remove the separator and the unwanted decimal place
+		str.resize(str.size() - 2);
+	}
+
+	return stream << QString::fromStdString(str);
+}
+
+QString format_time_si(
+	const Timestamp& v,
+	SIPrefix prefix,
+	unsigned int precision,
+	QString unit,
+	bool sign)
+{
+	if (prefix == SIPrefix::unspecified) {
+		// No prefix given, calculate it
+
+		if (v.is_zero()) {
+			prefix = SIPrefix::none;
+		} else {
+			int exp = exponent(SIPrefix::yotta);
+			prefix = SIPrefix::yocto;
+			while ((fabs(v) * pow(Timestamp(10), exp)) > 999 &&
+					prefix < SIPrefix::yotta) {
+				prefix = successor(prefix);
+				exp -= 3;
+			}
+		}
+	}
+
+	assert(prefix >= SIPrefix::yocto);
+	assert(prefix <= SIPrefix::yotta);
+
+	const Timestamp multiplier = pow(Timestamp(10), -exponent(prefix));
+
+	QString s;
+	QTextStream ts(&s);
+	if (sign && !v.is_zero())
+		ts << forcesign;
+	ts
+		<< qSetRealNumberPrecision(precision)
+		<< (v * multiplier)
+		<< ' '
+		<< prefix
+		<< unit;
+
+	return s;
+}
+
+QString format_time_si_adjusted(
+	const Timestamp& t,
+	SIPrefix prefix,
+	unsigned precision,
+	QString unit,
+	bool sign)
+{
+	// The precision is always given without taking the prefix into account
+	// so we need to deduct the number of decimals the prefix might imply
+	const int prefix_order = -exponent(prefix);
+
+	const unsigned int relative_prec =
+		(prefix >= SIPrefix::none) ? precision :
+		std::max((int)(precision - prefix_order), 0);
+
+	return format_time_si(t, prefix, relative_prec, unit, sign);
+}
+
+
+// Helper for 'format_time_minutes()'.
+static QString pad_number(unsigned int number, int length)
+{
+	return QString("%1").arg(number, length, 10, QChar('0'));
+}
+
+QString format_time_minutes(const Timestamp& t, signed precision, bool sign)
+{
+	const Timestamp whole_seconds = floor(abs(t));
+	const Timestamp days = floor(whole_seconds / (60 * 60 * 24));
+	const unsigned int hours = fmod(whole_seconds / (60 * 60), 24).convert_to<uint>();
+	const unsigned int minutes = fmod(whole_seconds / 60, 60).convert_to<uint>();
+	const unsigned int seconds = fmod(whole_seconds, 60).convert_to<uint>();
+
+	QString s;
+	QTextStream ts(&s);
+
+	if (t < 0)
+		ts << "-";
+	else if (sign)
+		ts << "+";
+
+	bool use_padding = false;
+
+	// DD
+	if (days) {
+		ts << days.str().c_str() << ":";
+		use_padding = true;
+	}
+
+	// HH
+	if (hours || days) {
+		ts << pad_number(hours, use_padding ? 2 : 0) << ":";
+		use_padding = true;
+	}
+
+	// MM
+	ts << pad_number(minutes, use_padding ? 2 : 0);
+
+	ts << ":";
+
+	// SS
+	ts << pad_number(seconds, 2);
+
+	if (precision) {
+		ts << ".";
+
+		const Timestamp fraction = fabs(t) - whole_seconds;
+
+		std::ostringstream ss;
+		ss
+			<< std::fixed
+			<< std::setprecision(precision)
+			<< std::setfill('0')
+			<< fraction;
+		std::string fs = ss.str();
+
+		// Copy all digits, inserting spaces as unit separators
+		for (int i = 1; i <= precision; i++) {
+			// Start at index 2 to skip the "0." at the beginning
+			ts << fs.at(1 + i);
+
+			if ((i > 0) && (i % 3 == 0) && (i != precision))
+				ts << " ";
+		}
+	}
+
+	return s;
+}
+
+} // namespace util
+} // namespace pv
diff --git a/pv/util.hpp b/pv/util.hpp
new file mode 100644
index 0000000..71177cc
--- /dev/null
+++ b/pv/util.hpp
@@ -0,0 +1,130 @@
+/*
+ * 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_UTIL_HPP
+#define PULSEVIEW_UTIL_HPP
+
+#include <cmath>
+
+#ifndef Q_MOC_RUN
+#include <boost/multiprecision/cpp_dec_float.hpp>
+#endif
+
+#include <QMetaType>
+#include <QString>
+
+namespace pv {
+namespace util {
+
+enum class TimeUnit {
+	Time = 1,
+	Samples = 2
+};
+
+enum class SIPrefix {
+	unspecified = -1,
+	yocto, zepto,
+	atto, femto, pico,
+	nano, micro, milli,
+	none,
+	kilo, mega, giga,
+	tera, peta, exa,
+	zetta, yotta
+};
+
+/// Returns the exponent that corresponds to a given prefix.
+int exponent(SIPrefix prefix);
+
+/// Timestamp type providing yoctosecond resolution.
+typedef boost::multiprecision::number<
+	boost::multiprecision::cpp_dec_float<24>,
+	boost::multiprecision::et_off> Timestamp;
+
+/**
+ * Formats a given timestamp with the specified SI prefix.
+ *
+ * If 'prefix' is left 'unspecified', the function chooses a prefix so that
+ * the value in front of the decimal point is between 1 and 999.
+ *
+ * The default value "s" for the unit argument makes the most sense when
+ * formatting time values, but a different value can be given if the function
+ * is reused to format a value of another quantity.
+ *
+ * @param t The value to format.
+ * @param prefix The SI prefix to use.
+ * @param precision The number of digits after the decimal separator.
+ * @param unit The unit of quantity.
+ * @param sign Whether or not to add a sign also for positive numbers.
+ *
+ * @return The formatted value.
+ */
+QString format_time_si(
+	const Timestamp& t,
+	SIPrefix prefix = SIPrefix::unspecified,
+	unsigned precision = 0,
+	QString unit = "s",
+	bool sign = true);
+
+/**
+ * Wrapper around 'format_time_si()' that interprets the given 'precision'
+ * value as the number of decimal places if the timestamp would be formatted
+ * without a SI prefix (using 'SIPrefix::none') and adjusts the precision to
+ * match the given 'prefix'
+ *
+ * @param t The value to format.
+ * @param prefix The SI prefix to use.
+ * @param precision The number of digits after the decimal separator if the
+ *        'prefix' would be 'SIPrefix::none', see above for more information.
+ * @param unit The unit of quantity.
+ * @param sign Whether or not to add a sign also for positive numbers.
+ *
+ * @return The formatted value.
+ */
+QString format_time_si_adjusted(
+	const Timestamp& t,
+	SIPrefix prefix,
+	unsigned precision = 0,
+	QString unit = "s",
+	bool sign = true);
+
+/**
+ * Formats the given timestamp using "[+-]DD:HH:MM:SS.mmm uuu nnn ppp..." format.
+ *
+ * "DD" and "HH" are left out if they would be "00" (but if "DD" is generated,
+ * "HH" is also always generated. The "MM:SS" part is always produced, the
+ * number of subsecond digits can be influenced using the 'precision' parameter.
+ *
+ * @param t The value to format.
+ * @param precision The number of digits after the decimal separator.
+ * @param sign Whether or not to add a sign also for positive numbers.
+ *
+ * @return The formatted value.
+ */
+QString format_time_minutes(
+	const Timestamp& t,
+	signed precision = 0,
+	bool sign = true);
+
+} // namespace util
+} // namespace pv
+
+Q_DECLARE_METATYPE(pv::util::Timestamp)
+
+#endif // PULSEVIEW_UTIL_HPP
diff --git a/pv/view/analogsignal.cpp b/pv/view/analogsignal.cpp
index c3f2738..2597a8f 100644
--- a/pv/view/analogsignal.cpp
+++ b/pv/view/analogsignal.cpp
@@ -20,21 +20,31 @@
 
 #include <extdef.h>
 
-#include <math.h>
+#include <cassert>
+#include <cmath>
+#include <cstdlib>
+#include <limits>
 
-#include "analogsignal.h"
-#include "pv/data/analog.h"
-#include "pv/data/analogsnapshot.h"
-#include "pv/view/view.h"
+#include "analogsignal.hpp"
+#include "pv/data/analog.hpp"
+#include "pv/data/analogsegment.hpp"
+#include "pv/view/view.hpp"
+
+#include <libsigrokcxx/libsigrokcxx.hpp>
 
-using boost::shared_ptr;
 using std::max;
+using std::make_pair;
 using std::min;
+using std::shared_ptr;
 using std::deque;
 
+using sigrok::Channel;
+
 namespace pv {
 namespace view {
 
+const int AnalogSignal::NominalHeight = 80;
+
 const QColor AnalogSignal::SignalColours[4] = {
 	QColor(0xC4, 0xA0, 0x00),	// Yellow
 	QColor(0x87, 0x20, 0x7A),	// Magenta
@@ -44,13 +54,16 @@ const QColor AnalogSignal::SignalColours[4] = {
 
 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)
+AnalogSignal::AnalogSignal(
+	pv::Session &session,
+	shared_ptr<Channel> channel,
+	shared_ptr<data::Analog> data) :
+	Signal(session, channel),
+	data_(data),
+	scale_index_(0),
+	scale_index_drag_offset_(0)
 {
-	_colour = SignalColours[probe->index % countof(SignalColours)];
+	set_colour(SignalColours[channel_->index() % countof(SignalColours)]);
 }
 
 AnalogSignal::~AnalogSignal()
@@ -59,83 +72,98 @@ AnalogSignal::~AnalogSignal()
 
 shared_ptr<pv::data::SignalData> AnalogSignal::data() const
 {
-	return _data;
+	return data_;
 }
 
 shared_ptr<pv::data::Analog> AnalogSignal::analog_data() const
 {
-	return _data;
+	return data_;
 }
 
-void AnalogSignal::set_scale(float scale)
+std::pair<int, int> AnalogSignal::v_extents() const
 {
-	_scale = scale;
+	const int h = NominalHeight / 2;
+	return make_pair(-h, h);
 }
 
-void AnalogSignal::paint_back(QPainter &p, int left, int right)
+int AnalogSignal::scale_handle_offset() const
 {
-	if (_probe->enabled)
-		paint_axis(p, get_y(), left, right);
+	return ((scale_index_drag_offset_ - scale_index_) *
+		NominalHeight / 4) - NominalHeight / 2;
 }
 
-void AnalogSignal::paint_mid(QPainter &p, int left, int right)
+void AnalogSignal::scale_handle_dragged(int offset)
 {
-	assert(_data);
-	assert(right >= left);
+	scale_index_ = scale_index_drag_offset_ -
+		(offset + NominalHeight / 2) / (NominalHeight / 4);
+}
 
-	assert(_view);
-	const int y = _v_offset - _view->v_offset();
+void AnalogSignal::scale_handle_drag_release()
+{
+	scale_index_drag_offset_ = scale_index_;
+}
 
-	const double scale = _view->scale();
-	assert(scale > 0);
+void AnalogSignal::paint_back(QPainter &p, const ViewItemPaintParams &pp)
+{
+	if (channel_->enabled()) {
+		Trace::paint_back(p, pp);
+		paint_axis(p, pp, get_visual_y());
+	}
+}
+
+void AnalogSignal::paint_mid(QPainter &p, const ViewItemPaintParams &pp)
+{
+	assert(data_);
+	assert(owner_);
 
-	const double offset = _view->offset();
+	const int y = get_visual_y();
 
-	if (!_probe->enabled)
+	if (!channel_->enabled())
 		return;
 
-	const deque< shared_ptr<pv::data::AnalogSnapshot> > &snapshots =
-		_data->get_snapshots();
-	if (snapshots.empty())
+	const deque< shared_ptr<pv::data::AnalogSegment> > &segments =
+		data_->analog_segments();
+	if (segments.empty())
 		return;
 
-	const shared_ptr<pv::data::AnalogSnapshot> &snapshot =
-		snapshots.front();
+	const shared_ptr<pv::data::AnalogSegment> &segment =
+		segments.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 double pixels_offset = pp.pixels_offset();
+	const double samplerate = max(1.0, segment->samplerate());
+	const pv::util::Timestamp& start_time = segment->start_time();
+	const int64_t last_sample = segment->get_sample_count() - 1;
+	const double samples_per_pixel = samplerate * pp.scale();
+	const pv::util::Timestamp start = samplerate * (pp.offset() - start_time);
+	const pv::util::Timestamp end = start + samples_per_pixel * pp.width();
 
-	const int64_t start_sample = min(max((int64_t)floor(start),
+	const int64_t start_sample = min(max(floor(start).convert_to<int64_t>(),
 		(int64_t)0), last_sample);
-	const int64_t end_sample = min(max((int64_t)ceil(end) + 1,
+	const int64_t end_sample = min(max((ceil(end) + 1).convert_to<int64_t>(),
 		(int64_t)0), last_sample);
 
 	if (samples_per_pixel < EnvelopeThreshold)
-		paint_trace(p, snapshot, y, left,
+		paint_trace(p, segment, y, pp.left(),
 			start_sample, end_sample,
 			pixels_offset, samples_per_pixel);
 	else
-		paint_envelope(p, snapshot, y, left,
+		paint_envelope(p, segment, y, pp.left(),
 			start_sample, end_sample,
 			pixels_offset, samples_per_pixel);
 }
 
 void AnalogSignal::paint_trace(QPainter &p,
-	const shared_ptr<pv::data::AnalogSnapshot> &snapshot,
+	const shared_ptr<pv::data::AnalogSegment> &segment,
 	int y, int left, const int64_t start, const int64_t end,
 	const double pixels_offset, const double samples_per_pixel)
 {
+	const float scale = this->scale();
 	const int64_t sample_count = end - start;
 
-	const float *const samples = snapshot->get_samples(start, end);
+	const float *const samples = segment->get_samples(start, end);
 	assert(samples);
 
-	p.setPen(_colour);
+	p.setPen(colour_);
 
 	QPointF *points = new QPointF[sample_count];
 	QPointF *point = points;
@@ -144,7 +172,7 @@ void AnalogSignal::paint_trace(QPainter &p,
 		const float x = (sample / samples_per_pixel -
 			pixels_offset) + left;
 		*point++ = QPointF(x,
-			y - samples[sample - start] * _scale);
+			y - samples[sample - start] * scale);
 	}
 
 	p.drawPolyline(points, point - points);
@@ -154,39 +182,41 @@ void AnalogSignal::paint_trace(QPainter &p,
 }
 
 void AnalogSignal::paint_envelope(QPainter &p,
-	const shared_ptr<pv::data::AnalogSnapshot> &snapshot,
+	const shared_ptr<pv::data::AnalogSegment> &segment,
 	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;
+	using pv::data::AnalogSegment;
+
+	const float scale = this->scale();
 
-	AnalogSnapshot::EnvelopeSection e;
-	snapshot->get_envelope_section(e, start, end, samples_per_pixel);
+	AnalogSegment::EnvelopeSection e;
+	segment->get_envelope_section(e, start, end, samples_per_pixel);
 
 	if (e.length < 2)
 		return;
 
 	p.setPen(QPen(Qt::NoPen));
-	p.setBrush(_colour);
+	p.setBrush(colour_);
 
 	QRectF *const rects = new QRectF[e.length];
 	QRectF *rect = rects;
 
-	for(uint64_t sample = 0; sample < e.length-1; sample++) {
+	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 =
+		const AnalogSegment::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;
+		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)
+		if (h >= 0.0f && h <= 1.0f)
 			h = 1.0f;
-		if(h <= 0.0f && h >= -1.0f)
+		if (h <= 0.0f && h >= -1.0f)
 			h = -1.0f;
 
 		*rect++ = QRectF(x, t, 1.0f, h);
@@ -198,5 +228,15 @@ void AnalogSignal::paint_envelope(QPainter &p,
 	delete[] e.samples;
 }
 
+float AnalogSignal::scale() const
+{
+	const float seq[] = {1.0f, 2.0f, 5.0f};
+	const int offset = std::numeric_limits<int>::max() / (2 * countof(seq));
+	const std::div_t d = std::div(
+		(int)(scale_index_ + countof(seq) * offset),
+		countof(seq));
+	return powf(10.0f, d.quot - offset) * seq[d.rem];
+}
+
 } // namespace view
 } // namespace pv
diff --git a/pv/view/analogsignal.h b/pv/view/analogsignal.hpp
similarity index 51%
rename from pv/view/analogsignal.h
rename to pv/view/analogsignal.hpp
index f22d128..938d92d 100644
--- a/pv/view/analogsignal.h
+++ b/pv/view/analogsignal.hpp
@@ -18,18 +18,18 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_VIEW_ANALOGSIGNAL_H
-#define PULSEVIEW_PV_VIEW_ANALOGSIGNAL_H
+#ifndef PULSEVIEW_PV_VIEW_ANALOGSIGNAL_HPP
+#define PULSEVIEW_PV_VIEW_ANALOGSIGNAL_HPP
 
-#include "signal.h"
+#include "signal.hpp"
 
-#include <boost/shared_ptr.hpp>
+#include <memory>
 
 namespace pv {
 
 namespace data {
 class Analog;
-class AnalogSnapshot;
+class AnalogSegment;
 }
 
 namespace view {
@@ -37,56 +37,82 @@ namespace view {
 class AnalogSignal : public Signal
 {
 private:
+	static const int NominalHeight;
 	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);
+	AnalogSignal(pv::Session &session,
+		std::shared_ptr<sigrok::Channel> channel,
+		std::shared_ptr<pv::data::Analog> data);
 
 	virtual ~AnalogSignal();
 
-	boost::shared_ptr<pv::data::SignalData> data() const;
+	std::shared_ptr<pv::data::SignalData> data() const;
 
-	boost::shared_ptr<pv::data::Analog> analog_data() const;
+	std::shared_ptr<pv::data::Analog> analog_data() const;
 
-	void set_scale(float scale);
+	/**
+	 * Computes the vertical extents of the contents of this row item.
+	 * @return A pair containing the minimum and maximum y-values.
+	 */
+	std::pair<int, int> v_extents() const;
+
+	/**
+	 * Returns the offset to show the drag handle.
+	 */
+	int scale_handle_offset() const;
+
+	/**
+	 * Handles the scale handle being dragged to an offset.
+	 * @param offset the offset the scale handle was dragged to.
+	 */
+	void scale_handle_dragged(int offset);
+
+	/**
+	 * @copydoc pv::view::Signal::signal_scale_handle_drag_release()
+	 */
+	void scale_handle_drag_release();
 
 	/**
 	 * 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);
+	 * @param pp the painting parameters object to paint with..
+	 */
+	void paint_back(QPainter &p, const ViewItemPaintParams &pp);
 
 	/**
 	 * 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);
+	 * @param pp the painting parameters object to paint with..
+	 */
+	void paint_mid(QPainter &p, const ViewItemPaintParams &pp);
 
 private:
 	void paint_trace(QPainter &p,
-		const boost::shared_ptr<pv::data::AnalogSnapshot> &snapshot,
+		const std::shared_ptr<pv::data::AnalogSegment> &segment,
 		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,
+		const std::shared_ptr<pv::data::AnalogSegment> &segment,
 		int y, int left, const int64_t start, const int64_t end,
 		const double pixels_offset, const double samples_per_pixel);
 
+	/**
+	 * Computes the scale factor from the scale index.
+	 */
+	float scale() const;
+
 private:
-	boost::shared_ptr<pv::data::Analog> _data;
-	float _scale;
+	std::shared_ptr<pv::data::Analog> data_;
+
+	int scale_index_;
+	int scale_index_drag_offset_;
 };
 
 } // namespace view
 } // namespace pv
 
-#endif // PULSEVIEW_PV_VIEW_ANALOGSIGNAL_H
+#endif // PULSEVIEW_PV_VIEW_ANALOGSIGNAL_HPP
diff --git a/pv/view/cursor.cpp b/pv/view/cursor.cpp
index ca22eef..7432e30 100644
--- a/pv/view/cursor.cpp
+++ b/pv/view/cursor.cpp
@@ -18,141 +18,83 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "cursor.h"
+#include "cursor.hpp"
 
-#include "ruler.h"
-#include "view.h"
+#include "ruler.hpp"
+#include "view.hpp"
+#include "pv/util.hpp"
 
+#include <QApplication>
 #include <QBrush>
 #include <QPainter>
 #include <QPointF>
 #include <QRect>
 #include <QRectF>
 
-#include <stdio.h>
+#include <cassert>
+#include <cstdio>
+#include <limits>
 
-#include <extdef.h>
-
-using boost::shared_ptr;
+using std::abs;
+using std::shared_ptr;
+using std::numeric_limits;
 
 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;
+Cursor::Cursor(View &view, double time) :
+	TimeMarker(view, FillColour, time)
+{
+}
 
-const int Cursor::ArrowSize = 4;
+bool Cursor::enabled() const
+{
+	return view_.cursors_shown();
+}
 
-Cursor::Cursor(View &view, double time) :
-	TimeMarker(view, LineColour, time)
+QString Cursor::get_text() const
 {
+	const shared_ptr<Cursor> other = get_other_cursor();
+	const pv::util::Timestamp& diff = abs(time_ - other->time_);
+
+	return Ruler::format_time_with_distance(
+		diff, time_, view_.tick_prefix(), view_.time_unit(), view_.tick_precision());
 }
 
-QRectF Cursor::get_label_rect(const QRect &rect) const
+QRectF Cursor::label_rect(const QRectF &rect) const
 {
 	const shared_ptr<Cursor> other(get_other_cursor());
 	assert(other);
 
-	const float x = (_time - _view.offset()) / _view.scale();
+	const float x = ((time_ - view_.offset())/ view_.scale()).convert_to<float>();
+
+	QFontMetrics m(QApplication::font());
+	QSize text_size = m.boundingRect(get_text()).size();
 
 	const QSizeF label_size(
-		_text_size.width() + View::LabelPadding.width() * 2,
-		_text_size.height() + View::LabelPadding.height() * 2);
+		text_size.width() + LabelPadding.width() * 2,
+		text_size.height() + LabelPadding.height() * 2);
 	const float top = rect.height() - label_size.height() -
-		Cursor::Offset - Cursor::ArrowSize - 0.5f;
+		TimeMarker::ArrowSize - 0.5f;
 	const float height = label_size.height();
 
-	if (_time > other->time())
+	const pv::util::Timestamp& other_time = other->time();
+
+	if (time_ > other_time ||
+		(abs(time_ - other_time).is_zero() && this > other.get()))
 		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();
+		return QRectF(x - label_size.width(), top, label_size.width(), height);
 }
 
 shared_ptr<Cursor> Cursor::get_other_cursor() const
 {
-	const CursorPair &cursors = _view.cursors();
-	return (cursors.first().get() == this) ?
-		cursors.second() : cursors.first();
+	const shared_ptr<CursorPair> cursors(view_.cursors());
+	assert(cursors);
+	return (cursors->first().get() == this) ?
+		cursors->second() : cursors->first();
 }
 
 } // namespace view
diff --git a/pv/view/cursor.h b/pv/view/cursor.hpp
similarity index 64%
rename from pv/view/cursor.h
rename to pv/view/cursor.hpp
index 290365e..e64beaa 100644
--- a/pv/view/cursor.h
+++ b/pv/view/cursor.hpp
@@ -18,12 +18,12 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_VIEW_CURSOR_H
-#define PULSEVIEW_PV_VIEW_CURSOR_H
+#ifndef PULSEVIEW_PV_VIEW_CURSOR_HPP
+#define PULSEVIEW_PV_VIEW_CURSOR_HPP
 
-#include "timemarker.h"
+#include "timemarker.hpp"
 
-#include <boost/shared_ptr.hpp>
+#include <memory>
 
 #include <QSizeF>
 
@@ -37,14 +37,7 @@ 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:
 	/**
@@ -56,31 +49,27 @@ public:
 
 public:
 	/**
-	 * Gets the marker label rectangle.
-	 * @param rect The rectangle of the ruler client area.
-	 * @return Returns the label rectangle.
+	 * Returns true if the item is visible and enabled.
 	 */
-	QRectF get_label_rect(const QRect &rect) const;
+	bool enabled() 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.
+	 * Gets the text to show in the marker.
 	 */
-	void paint_label(QPainter &p, const QRect &rect,
-		unsigned int prefix);
+	QString get_text() const;
 
-private:
-	void compute_text_size(QPainter &p, unsigned int prefix);
-
-	boost::shared_ptr<Cursor> get_other_cursor() const;
+	/**
+	 * Gets the marker label rectangle.
+	 * @param rect The rectangle of the ruler client area.
+	 * @return Returns the label rectangle.
+	 */
+	QRectF label_rect(const QRectF &rect) const;
 
 private:
-	QSizeF _text_size;
+	std::shared_ptr<Cursor> get_other_cursor() const;
 };
 
 } // namespace view
 } // namespace pv
 
-#endif // PULSEVIEW_PV_VIEW_CURSOR_H
+#endif // PULSEVIEW_PV_VIEW_CURSOR_HPP
diff --git a/pv/view/cursorpair.cpp b/pv/view/cursorpair.cpp
index ed8829d..f9d3e53 100644
--- a/pv/view/cursorpair.cpp
+++ b/pv/view/cursorpair.cpp
@@ -18,46 +18,75 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "cursorpair.h"
+#include "cursorpair.hpp"
 
-#include "ruler.h"
-#include "view.h"
+#include "ruler.hpp"
+#include "view.hpp"
+#include "pv/util.hpp"
 
+#include <cassert>
 #include <algorithm>
 
-using boost::shared_ptr;
 using std::max;
 using std::make_pair;
 using std::min;
+using std::shared_ptr;
 using std::pair;
 
 namespace pv {
 namespace view {
 
 const int CursorPair::DeltaPadding = 8;
+const QColor CursorPair::ViewportFillColour(220, 231, 243);
 
 CursorPair::CursorPair(View &view) :
-	_first(new Cursor(view, 0.0)),
-	_second(new Cursor(view, 1.0)),
-	_view(view)
+	TimeItem(view),
+	first_(new Cursor(view, 0.0)),
+	second_(new Cursor(view, 1.0))
 {
 }
 
+bool CursorPair::enabled() const
+{
+	return view_.cursors_shown();
+}
+
 shared_ptr<Cursor> CursorPair::first() const
 {
-	return _first;
+	return first_;
 }
 
 shared_ptr<Cursor> CursorPair::second() const
 {
-	return _second;
+	return second_;
+}
+
+void CursorPair::set_time(const pv::util::Timestamp& time)
+{
+	const pv::util::Timestamp delta = second_->time() - first_->time();
+	first_->set_time(time);
+	second_->set_time(time + delta);
 }
 
-QRectF CursorPair::get_label_rect(const QRect &rect) const
+float CursorPair::get_x() const
 {
-	const QSizeF label_size(
-		_text_size.width() + View::LabelPadding.width() * 2,
-		_text_size.height() + View::LabelPadding.height() * 2);
+	return (first_->get_x() + second_->get_x()) / 2.0f;
+}
+
+QPoint CursorPair::point(const QRect &rect) const
+{
+	return first_->point(rect);
+}
+
+pv::widgets::Popup* CursorPair::create_popup(QWidget *parent)
+{
+	(void)parent;
+	return nullptr;
+}
+
+QRectF CursorPair::label_rect(const QRectF &rect) const
+{
+	const QSizeF label_size(text_size_ + LabelPadding * 2);
 	const pair<float, float> offsets(get_cursor_offsets());
 	const pair<float, float> normal_offsets(
 		(offsets.first < offsets.second) ? offsets :
@@ -69,86 +98,98 @@ QRectF CursorPair::get_label_rect(const QRect &rect) const
 		(float)rect.width() + height);
 
 	return QRectF(left, rect.height() - label_size.height() -
-		Cursor::ArrowSize - Cursor::Offset - 0.5f,
+		TimeMarker::ArrowSize - 0.5f,
 		right - left, height);
 }
 
-void CursorPair::draw_markers(QPainter &p,
-	const QRect &rect, unsigned int prefix)
+void CursorPair::paint_label(QPainter &p, const QRect &rect, bool hover)
 {
-	assert(_first);
-	assert(_second);
+	assert(first_);
+	assert(second_);
+
+	if (!enabled())
+		return;
 
-	compute_text_size(p, prefix);
-	QRectF delta_rect(get_label_rect(rect));
+	const QColor text_colour =
+		ViewItem::select_text_colour(Cursor::FillColour);
+
+	p.setPen(text_colour);
+	compute_text_size(p);
+	QRectF delta_rect(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())
-	{
+	if (text_rect.width() >= text_size_.width()) {
 		const int highlight_radius = delta_rect.height() / 2 - 2;
 
-		p.setBrush(Cursor::FillColour);
-		p.setPen(Cursor::LineColour);
+		if (selected()) {
+			p.setBrush(Qt::transparent);
+			p.setPen(highlight_pen());
+			p.drawRoundedRect(delta_rect, radius, radius);
+		}
+
+		p.setBrush(hover ? Cursor::FillColour.lighter() :
+			Cursor::FillColour);
+		p.setPen(Cursor::FillColour.darker());
 		p.drawRoundedRect(delta_rect, radius, radius);
 
 		delta_rect.adjust(1, 1, -1, -1);
-		p.setPen(Cursor::HighlightColour);
+		p.setPen(Cursor::FillColour.lighter());
 		p.drawRoundedRect(delta_rect, highlight_radius, highlight_radius);
 
-		p.setPen(Cursor::TextColour);
+		p.setPen(text_colour);
 		p.drawText(text_rect, Qt::AlignCenter | Qt::AlignVCenter,
-			Ruler::format_time(_second->time() - _first->time(), prefix, 2));
+			format_string());
 	}
-
-	// 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)
+void CursorPair::paint_back(QPainter &p, const ViewItemPaintParams &pp)
 {
+	if (!enabled())
+		return;
+
 	p.setPen(Qt::NoPen);
-	p.setBrush(QBrush(View::CursorAreaColour));
+	p.setBrush(QBrush(ViewportFillColour));
 
 	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());
+		offsets.first, offsets.second), (float)pp.width());
 
-	p.drawRect(l, 0, r - l, rect.height());
+	p.drawRect(l, pp.top(), r - l, pp.height());
 }
 
-void CursorPair::draw_viewport_foreground(QPainter &p,
-	const QRect &rect)
+QString CursorPair::format_string()
 {
-	assert(_first);
-	assert(_second);
+	const pv::util::SIPrefix prefix = view_.tick_prefix();
+	const pv::util::Timestamp diff = abs(second_->time() - first_->time());
+
+	const QString s1 = Ruler::format_time_with_distance(
+		diff, diff, prefix, view_.time_unit(), view_.tick_precision(), false);
+	const QString s2 = util::format_time_si(
+		1 / diff, pv::util::SIPrefix::unspecified, 4, "Hz", false);
 
-	_first->paint(p, rect);
-	_second->paint(p, rect);
+	return QString("%1 / %2").arg(s1).arg(s2);
 }
 
-void CursorPair::compute_text_size(QPainter &p, unsigned int prefix)
+void CursorPair::compute_text_size(QPainter &p)
 {
-	assert(_first);
-	assert(_second);
+	assert(first_);
+	assert(second_);
 
-	_text_size = p.boundingRect(QRectF(), 0, Ruler::format_time(
-		_second->time() - _first->time(), prefix, 2)).size();
+	text_size_ = p.boundingRect(QRectF(), 0, format_string()).size();
 }
 
 pair<float, float> CursorPair::get_cursor_offsets() const
 {
-	assert(_first);
-	assert(_second);
+	assert(first_);
+	assert(second_);
 
 	return pair<float, float>(
-		(_first->time() - _view.offset()) / _view.scale(),
-		(_second->time() - _view.offset()) / _view.scale());
+		((first_->time() - view_.offset()) / view_.scale()).convert_to<float>(),
+		((second_->time() - view_.offset()) / view_.scale()).convert_to<float>());
 }
 
 } // namespace view
diff --git a/pv/view/cursorpair.hpp b/pv/view/cursorpair.hpp
new file mode 100644
index 0000000..345700f
--- /dev/null
+++ b/pv/view/cursorpair.hpp
@@ -0,0 +1,111 @@
+/*
+ * 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_HPP
+#define PULSEVIEW_PV_VIEW_CURSORPAIR_HPP
+
+#include "cursor.hpp"
+
+#include <memory>
+
+#include <QPainter>
+
+class QPainter;
+
+namespace pv {
+namespace view {
+
+class CursorPair : public TimeItem
+{
+private:
+	static const int DeltaPadding;
+	static const QColor ViewportFillColour;
+
+public:
+	/**
+	 * Constructor.
+	 * @param view A reference to the view that owns this cursor pair.
+	 */
+	CursorPair(View &view);
+
+public:
+	/**
+	 * Returns true if the item is visible and enabled.
+	 */
+	bool enabled() const;
+
+	/**
+	 * Returns a pointer to the first cursor.
+	 */
+	std::shared_ptr<Cursor> first() const;
+
+	/**
+	 * Returns a pointer to the second cursor.
+	 */
+	std::shared_ptr<Cursor> second() const;
+
+	/**
+	 * Sets the time of the marker.
+	 */
+	void set_time(const pv::util::Timestamp& time) override;
+
+	float get_x() const;
+
+	QPoint point(const QRect &rect) const;
+
+	pv::widgets::Popup* create_popup(QWidget *parent);
+
+public:
+	QRectF label_rect(const QRectF &rect) const;
+
+	/**
+	 * 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 hover true if the label is being hovered over by the mouse.
+	 */
+	void paint_label(QPainter &p, const QRect &rect, bool hover);
+
+	/**
+	 * Paints the background layer of the item with a QPainter
+	 * @param p the QPainter to paint into.
+	 * @param pp the painting parameters object to paint with.
+	 */
+	void paint_back(QPainter &p, const ViewItemPaintParams &pp);
+
+	/**
+	 * Constructs the string to display.
+	 */
+	QString format_string();
+
+	void compute_text_size(QPainter &p);
+
+	std::pair<float, float> get_cursor_offsets() const;
+
+private:
+	std::shared_ptr<Cursor> first_, second_;
+
+	QSizeF text_size_;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_CURSORPAIR_HPP
diff --git a/pv/view/decodetrace.cpp b/pv/view/decodetrace.cpp
index 618de9a..e0f3a0f 100644
--- a/pv/view/decodetrace.cpp
+++ b/pv/view/decodetrace.cpp
@@ -22,10 +22,15 @@ extern "C" {
 #include <libsigrokdecode/libsigrokdecode.h>
 }
 
+#include <mutex>
+
 #include <extdef.h>
 
-#include <boost/foreach.hpp>
+#include <tuple>
+
 #include <boost/functional/hash.hpp>
+#include <boost/thread/locks.hpp>
+#include <boost/thread/shared_mutex.hpp>
 
 #include <QAction>
 #include <QApplication>
@@ -34,26 +39,36 @@ extern "C" {
 #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;
+#include <QToolTip>
+
+#include "decodetrace.hpp"
+
+#include <pv/session.hpp>
+#include <pv/data/decoderstack.hpp>
+#include <pv/data/decode/decoder.hpp>
+#include <pv/data/logic.hpp>
+#include <pv/data/logicsegment.hpp>
+#include <pv/data/decode/annotation.hpp>
+#include <pv/view/logicsignal.hpp>
+#include <pv/view/view.hpp>
+#include <pv/view/viewport.hpp>
+#include <pv/widgets/decodergroupbox.hpp>
+#include <pv/widgets/decodermenu.hpp>
+
+using boost::shared_lock;
+using boost::shared_mutex;
+using std::dynamic_pointer_cast;
 using std::list;
+using std::lock_guard;
+using std::make_pair;
 using std::max;
+using std::make_pair;
 using std::map;
 using std::min;
+using std::pair;
+using std::shared_ptr;
+using std::tie;
+using std::unordered_set;
 using std::vector;
 
 namespace pv {
@@ -111,24 +126,26 @@ const QColor DecodeTrace::OutlineColours[16] = {
 	QColor(0x6B, 0x23, 0x37)
 };
 
-DecodeTrace::DecodeTrace(pv::SigSession &session,
-	boost::shared_ptr<pv::data::DecoderStack> decoder_stack, int index) :
+DecodeTrace::DecodeTrace(pv::Session &session,
+	std::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)
+	session_(session),
+	decoder_stack_(decoder_stack),
+	row_height_(0),
+	max_visible_rows_(0),
+	delete_mapper_(this),
+	show_hide_mapper_(this)
 {
-	assert(_decoder_stack);
+	assert(decoder_stack_);
 
-	_colour = DecodeColours[index % countof(DecodeColours)];
+	set_colour(DecodeColours[index % countof(DecodeColours)]);
 
-	connect(_decoder_stack.get(), SIGNAL(new_decode_data()),
+	connect(decoder_stack_.get(), SIGNAL(new_decode_data()),
 		this, SLOT(on_new_decode_data()));
-	connect(&_delete_mapper, SIGNAL(mapped(int)),
+	connect(&delete_mapper_, SIGNAL(mapped(int)),
 		this, SLOT(on_delete_decoder(int)));
-	connect(&_show_hide_mapper, SIGNAL(mapped(int)),
+	connect(&show_hide_mapper_, SIGNAL(mapped(int)),
 		this, SLOT(on_show_hide_decoder(int)));
 }
 
@@ -137,71 +154,51 @@ bool DecodeTrace::enabled() const
 	return true;
 }
 
-const boost::shared_ptr<pv::data::DecoderStack>& DecodeTrace::decoder() const
+const std::shared_ptr<pv::data::DecoderStack>& DecodeTrace::decoder() const
 {
-	return _decoder_stack;
+	return decoder_stack_;
 }
 
-void DecodeTrace::set_view(pv::view::View *view)
+pair<int, int> DecodeTrace::v_extents() const
 {
-	assert(view);
-	Trace::set_view(view);
+	const int row_height = (ViewItemPaintParams::text_height() * 6) / 4;
+
+	return make_pair(-row_height, row_height * max_visible_rows_);
 }
 
-void DecodeTrace::paint_back(QPainter &p, int left, int right)
+void DecodeTrace::paint_back(QPainter &p, const ViewItemPaintParams &pp)
 {
-	Trace::paint_back(p, left, right);
-	paint_axis(p, get_y(), left, right);
+	Trace::paint_back(p, pp);
+	paint_axis(p, pp, get_visual_y());
 }
 
-void DecodeTrace::paint_mid(QPainter &p, int left, int right)
+void DecodeTrace::paint_mid(QPainter &p, const ViewItemPaintParams &pp)
 {
 	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 text_height = ViewItemPaintParams::text_height();
+	row_height_ = (text_height * 6) / 4;
 	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);
+
+	assert(decoder_stack_);
+	const QString err = decoder_stack_->error_message();
+	if (!err.isEmpty()) {
+		draw_unresolved_period(
+			p, annotation_height, pp.left(), pp.right());
+		draw_error(p, err, pp);
 		return;
 	}
 
 	// Iterate through the rows
-	assert(_view);
-	int y = get_y();
+	int y = get_visual_y();
+	pair<uint64_t, uint64_t> sample_range = get_sample_range(
+		pp.left(), pp.right());
 
-	assert(_decoder_stack);
+	assert(decoder_stack_);
+	const vector<Row> rows(decoder_stack_->get_visible_rows());
 
-	const vector<Row> rows(_decoder_stack->get_visible_rows());
-	for (size_t i = 0; i < rows.size(); i++)
-	{
+	visible_rows_.clear();
+	for (size_t i = 0; i < rows.size(); i++) {
 		const Row &row = rows[i];
 
 		size_t base_colour = 0x13579BDF;
@@ -211,55 +208,49 @@ void DecodeTrace::paint_mid(QPainter &p, int left, int right)
 		base_colour >>= 16;
 
 		vector<Annotation> annotations;
-		_decoder_stack->get_annotation_subset(annotations, row,
-			start_sample, end_sample);
+		decoder_stack_->get_annotation_subset(annotations, row,
+			sample_range.first, sample_range.second);
 		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_annotations(annotations, p, annotation_height, pp, y,
+				base_colour);
+
+			y += row_height_;
+
+			visible_rows_.push_back(rows[i]);
 		}
 	}
 
 	// Draw the hatching
-	draw_unresolved_period(p, annotation_height, left, right,
-		samples_per_pixel, pixels_offset);
+	draw_unresolved_period(p, annotation_height, pp.left(), pp.right());
+
+	// Update the maximum row count if needed
+	max_visible_rows_ = std::max(max_visible_rows_, (int)visible_rows_.size());
 }
 
-void DecodeTrace::paint_fore(QPainter &p, int left, int right)
+void DecodeTrace::paint_fore(QPainter &p, const ViewItemPaintParams &pp)
 {
 	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;
+	assert(row_height_);
 
-	for (size_t i = 0; i < _cur_row_headings.size(); i++)
-	{
-		const int y = i * row_height + get_y();
+	for (size_t i = 0; i < visible_rows_.size(); i++) {
+		const int y = i * row_height_ + get_visual_y();
 
 		p.setPen(QPen(Qt::NoPen));
 		p.setBrush(QApplication::palette().brush(QPalette::WindowText));
 
-		if (i != 0)
-		{
+		if (i != 0) {
 			const QPointF points[] = {
-				QPointF(left, y - ArrowSize),
-				QPointF(left + ArrowSize, y),
-				QPointF(left, y + ArrowSize)
+				QPointF(pp.left(), y - ArrowSize),
+				QPointF(pp.left() + ArrowSize, y),
+				QPointF(pp.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 QRect r(pp.left() + ArrowSize * 2, y - row_height_ / 2,
+			pp.right() - pp.left(), row_height_);
+		const QString h(visible_rows_[i].title());
 		const int f = Qt::AlignLeft | Qt::AlignVCenter |
 			Qt::TextDontClip;
 
@@ -282,29 +273,25 @@ void DecodeTrace::populate_popup_form(QWidget *parent, QFormLayout *form)
 
 	assert(form);
 	assert(parent);
-	assert(_decoder_stack);
+	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();
+	bindings_.clear();
+	channel_selectors_.clear();
+	decoder_forms_.clear();
 
-	const list< shared_ptr<Decoder> >& stack = _decoder_stack->stack();
+	const list< shared_ptr<Decoder> >& stack = decoder_stack_->stack();
 
-	if (stack.empty())
-	{
+	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();
+	} else {
+		auto iter = stack.cbegin();
 		for (int i = 0; i < (int)stack.size(); i++, iter++) {
 			shared_ptr<Decoder> dec(*iter);
 			create_decoder_form(i, dec, parent, form);
@@ -343,11 +330,56 @@ QMenu* DecodeTrace::create_context_menu(QWidget *parent)
 	return menu;
 }
 
+void DecodeTrace::draw_annotations(vector<pv::data::decode::Annotation> annotations,
+		QPainter &p, int h, const ViewItemPaintParams &pp, int y,
+		size_t base_colour)
+{
+	using namespace pv::data::decode;
+
+	vector<Annotation> a_block;
+	int prev_ann_pos = INT_MIN;
+
+	double samples_per_pixel, pixels_offset;
+	tie(pixels_offset, samples_per_pixel) =
+		get_pixels_offset_samples_per_pixel();
+
+	// Gather all annotations that form a visual "block" and draw them as such
+	for (const Annotation &a : annotations) {
+
+		const int end = a.end_sample() / samples_per_pixel - pixels_offset;
+		const int delta = end - prev_ann_pos;
+
+		// Some annotations are in reverse order, so we cannot
+		// simply check for delta > 1
+		if (abs(delta) > 1) {
+			// Block was broken, draw it
+			if (a_block.size() == 1)
+				draw_annotation(a_block.front(), p, h, pp, y, base_colour);
+			else
+				if (a_block.size() > 0)
+					draw_annotation_block(a_block, p, h, y, base_colour);
+
+			a_block.clear();
+		}
+
+		a_block.push_back(a);
+		prev_ann_pos = end;
+	}
+
+	if (a_block.size() == 1)
+		draw_annotation(a_block.front(), p, h, pp, y, base_colour);
+	else
+		draw_annotation_block(a_block, p, h, y, base_colour);
+}
+
 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,
+	QPainter &p, int h, const ViewItemPaintParams &pp, int y,
 	size_t base_colour) const
 {
+	double samples_per_pixel, pixels_offset;
+	tie(pixels_offset, samples_per_pixel) =
+		get_pixels_offset_samples_per_pixel();
+
 	const double start = a.start_sample() / samples_per_pixel -
 		pixels_offset;
 	const double end = a.end_sample() / samples_per_pixel -
@@ -357,19 +389,52 @@ void DecodeTrace::draw_annotation(const pv::data::decode::Annotation &a,
 	const QColor &fill = Colours[colour];
 	const QColor &outline = OutlineColours[colour];
 
-	if (start > right + DrawPadding || end < left - DrawPadding)
+	if (start > pp.right() + DrawPadding || end < pp.left() - DrawPadding)
 		return;
 
 	if (a.start_sample() == a.end_sample())
-		draw_instant(a, p, fill, outline, text_color, h,
-			start, y);
+		draw_instant(a, p, fill, outline, h, start, y);
 	else
-		draw_range(a, p, fill, outline, text_color, h,
-			start, end, y);
+		draw_range(a, p, fill, outline, h, start, end, y);
+}
+
+void DecodeTrace::draw_annotation_block(
+	vector<pv::data::decode::Annotation> annotations, QPainter &p, int h,
+	int y, size_t base_colour) const
+{
+	using namespace pv::data::decode;
+
+	double samples_per_pixel, pixels_offset;
+	tie(pixels_offset, samples_per_pixel) =
+		get_pixels_offset_samples_per_pixel();
+
+	const double start = annotations.front().start_sample() /
+		samples_per_pixel - pixels_offset;
+	const double end = annotations.back().end_sample() /
+		samples_per_pixel - pixels_offset;
+
+	const double top = y + .5 - h / 2;
+	const double bottom = y + .5 + h / 2;
+
+	const size_t colour = (base_colour + annotations.front().format()) %
+		countof(Colours);
+
+	// Check if all annotations are of the same type (i.e. we can use one color)
+	// or if we should use a neutral color (i.e. gray)
+	const int format = annotations.front().format();
+	const bool single_format = std::all_of(
+		annotations.begin(), annotations.end(),
+		[&](const Annotation &a) { return a.format() == format; });
+
+	p.setPen((single_format ? OutlineColours[colour] : Qt::gray));
+	p.setBrush(QBrush((single_format ? Colours[colour] : Qt::gray),
+		Qt::Dense4Pattern));
+	p.drawRoundedRect(
+		QRectF(start, top, end - start, bottom - top), h/4, h/4);
 }
 
 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
+	QColor fill, QColor outline, int h, double x, int y) const
 {
 	const QString text = a.annotations().empty() ?
 		QString() : a.annotations().back();
@@ -381,12 +446,12 @@ void DecodeTrace::draw_instant(const pv::data::decode::Annotation &a, QPainter &
 	p.setBrush(fill);
 	p.drawRoundedRect(rect, h / 2, h / 2);
 
-	p.setPen(text_color);
+	p.setPen(Qt::black);
 	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,
+	QColor fill, QColor outline, int h, double start,
 	double end, int y) const
 {
 	const double top = y + .5 - h / 2;
@@ -397,8 +462,7 @@ void DecodeTrace::draw_range(const pv::data::decode::Annotation &a, QPainter &p,
 	p.setBrush(fill);
 
 	// If the two ends are within 1 pixel, draw a vertical line
-	if (start + 1.0 > end)
-	{
+	if (start + 1.0 > end) {
 		p.drawLine(QPointF(start, top), QPointF(start, bottom));
 		return;
 	}
@@ -424,13 +488,13 @@ void DecodeTrace::draw_range(const pv::data::decode::Annotation &a, QPainter &p,
 	if (rect.width() <= 4)
 		return;
 
-	p.setPen(text_color);
+	p.setPen(Qt::black);
 
 	// Try to find an annotation that will fit
 	QString best_annotation;
 	int best_width = 0;
 
-	BOOST_FOREACH(const QString &a, annotations) {
+	for (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;
@@ -445,15 +509,15 @@ void DecodeTrace::draw_range(const pv::data::decode::Annotation &a, QPainter &p,
 }
 
 void DecodeTrace::draw_error(QPainter &p, const QString &message,
-	int left, int right)
+	const ViewItemPaintParams &pp)
 {
-	const int y = get_y();
+	const int y = get_visual_y();
 
 	p.setPen(ErrorBgColour.darker());
 	p.setBrush(ErrorBgColour);
 
 	const QRectF bounding_rect =
-		QRectF(left, INT_MIN / 2 + y, right - left, INT_MAX);
+		QRectF(pp.width(), INT_MIN / 2 + y, pp.width(), INT_MAX);
 	const QRectF text_rect = p.boundingRect(bounding_rect,
 		Qt::AlignCenter, message);
 	const float r = text_rect.height() / 4;
@@ -461,47 +525,53 @@ void DecodeTrace::draw_error(QPainter &p, const QString &message,
 	p.drawRoundedRect(text_rect.adjusted(-r, -r, r, r), r, r,
 		Qt::AbsoluteSize);
 
-	p.setPen(get_text_colour());
+	p.setPen(Qt::black);
 	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) 
+	int right) const
 {
 	using namespace pv::data;
 	using pv::data::decode::Decoder;
 
-	assert(_decoder_stack);	
+	double samples_per_pixel, pixels_offset;
+
+	assert(decoder_stack_);	
 
 	shared_ptr<Logic> data;
 	shared_ptr<LogicSignal> logic_signal;
 
-	const list< shared_ptr<Decoder> > &stack = _decoder_stack->stack();
+	const list< shared_ptr<Decoder> > &stack = decoder_stack_->stack();
 
-	// We get the logic data of the first probe in the list.
+	// 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<Decoder> &dec, stack)
+	// LogicSignals have the same data/segment
+	for (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())
+	if (!data || data->logic_segments().empty())
 		return;
 
-	const shared_ptr<LogicSnapshot> snapshot =
-		data->get_snapshots().front();
-	assert(snapshot);
-	const int64_t sample_count = (int64_t)snapshot->get_sample_count();
+	const shared_ptr<LogicSegment> segment =
+		data->logic_segments().front();
+	assert(segment);
+	const int64_t sample_count = (int64_t)segment->get_sample_count();
 	if (sample_count == 0)
 		return;
 
-	const int64_t samples_decoded = _decoder_stack->samples_decoded();
+	const int64_t samples_decoded = decoder_stack_->samples_decoded();
 	if (sample_count == samples_decoded)
 		return;
 
-	const int y = get_y();
+	const int y = get_visual_y();
+
+	tie(pixels_offset, samples_per_pixel) =
+		get_pixels_offset_samples_per_pixel();
+
 	const double start = max(samples_decoded /
 		samples_per_pixel - pixels_offset, left - 1.0);
 	const double end = min(sample_count / samples_per_pixel -
@@ -517,6 +587,125 @@ void DecodeTrace::draw_unresolved_period(QPainter &p, int h, int left,
 	p.drawRect(no_decode_rect);
 }
 
+pair<double, double> DecodeTrace::get_pixels_offset_samples_per_pixel() const
+{
+	assert(owner_);
+	assert(decoder_stack_);
+
+	const View *view = owner_->view();
+	assert(view);
+
+	const double scale = view->scale();
+	assert(scale > 0);
+
+	const double pixels_offset =
+		((view->offset() - decoder_stack_->start_time()) / scale).convert_to<double>();
+
+	double samplerate = decoder_stack_->samplerate();
+
+	// Show sample rate as 1Hz when it is unknown
+	if (samplerate == 0.0)
+		samplerate = 1.0;
+
+	return make_pair(pixels_offset, samplerate * scale);
+}
+
+pair<uint64_t, uint64_t> DecodeTrace::get_sample_range(
+	int x_start, int x_end) const
+{
+	double samples_per_pixel, pixels_offset;
+	tie(pixels_offset, samples_per_pixel) =
+		get_pixels_offset_samples_per_pixel();
+
+	const uint64_t start = (uint64_t)max(
+		(x_start + pixels_offset) * samples_per_pixel, 0.0);
+	const uint64_t end = (uint64_t)max(
+		(x_end + pixels_offset) * samples_per_pixel, 0.0);
+
+	return make_pair(start, end);
+}
+
+int DecodeTrace::get_row_at_point(const QPoint &point)
+{
+	if (!row_height_)
+		return -1;
+
+	const int y = (point.y() - get_visual_y() + row_height_ / 2);
+
+	/* Integer divison of (x-1)/x would yield 0, so we check for this. */
+	if (y < 0)
+		return -1;
+
+	const int row = y / row_height_;
+
+	if (row >= (int)visible_rows_.size())
+		return -1;
+
+	return row;
+}
+
+const QString DecodeTrace::get_annotation_at_point(const QPoint &point)
+{
+	using namespace pv::data::decode;
+
+	if (!enabled())
+		return QString();
+
+	const pair<uint64_t, uint64_t> sample_range =
+		get_sample_range(point.x(), point.x() + 1);
+	const int row = get_row_at_point(point);
+	if (row < 0)
+		return QString();
+
+	vector<pv::data::decode::Annotation> annotations;
+
+	assert(decoder_stack_);
+	decoder_stack_->get_annotation_subset(annotations, visible_rows_[row],
+		sample_range.first, sample_range.second);
+
+	return (annotations.empty()) ?
+		QString() : annotations[0].annotations().front();
+}
+
+void DecodeTrace::hover_point_changed()
+{
+	assert(owner_);
+
+	const View *const view = owner_->view();
+	assert(view);
+
+	QPoint hp = view->hover_point();
+	QString ann = get_annotation_at_point(hp);
+
+	assert(view);
+
+	if (!row_height_ || ann.isEmpty()) {
+		QToolTip::hideText();
+		return;
+	}
+
+	const int hover_row = get_row_at_point(hp);
+
+	QFontMetrics m(QToolTip::font());
+	const QRect text_size = m.boundingRect(QRect(), 0, ann);
+
+	// This is OS-specific and unfortunately we can't query it, so
+	// use an approximation to at least try to minimize the error.
+	const int padding = 8;
+
+	// Make sure the tool tip doesn't overlap with the mouse cursor.
+	// If it did, the tool tip would constantly hide and re-appear.
+	// We also push it up by one row so that it appears above the
+	// decode trace, not below.
+	hp.setX(hp.x() - (text_size.width() / 2) - padding);
+
+	hp.setY(get_visual_y() - (row_height_ / 2) +
+		(hover_row * row_height_) -
+		row_height_ - text_size.height() - padding);
+
+	QToolTip::showText(view->viewport()->mapToGlobal(hp), ann);
+}
+
 void DecodeTrace::create_decoder_form(int index,
 	shared_ptr<data::decode::Decoder> &dec, QWidget *parent,
 	QFormLayout *form)
@@ -527,139 +716,145 @@ void DecodeTrace::create_decoder_form(int index,
 	const srd_decoder *const decoder = dec->decoder();
 	assert(decoder);
 
+	const bool decoder_deletable = index > 0;
+
 	pv::widgets::DecoderGroupBox *const group =
 		new pv::widgets::DecoderGroupBox(
-			QString::fromUtf8(decoder->name));
+			QString::fromUtf8(decoder->name), nullptr, decoder_deletable);
 	group->set_decoder_visible(dec->shown());
 
-	_delete_mapper.setMapping(group, index);
-	connect(group, SIGNAL(delete_decoder()), &_delete_mapper, SLOT(map()));
+	if (decoder_deletable) {
+		delete_mapper_.setMapping(group, index);
+		connect(group, SIGNAL(delete_decoder()), &delete_mapper_, SLOT(map()));
+	}
 
-	_show_hide_mapper.setMapping(group, index);
+	show_hide_mapper_.setMapping(group, index);
 	connect(group, SIGNAL(show_hide_decoder()),
-		&_show_hide_mapper, SLOT(map()));
+		&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) {
+	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);
+		QComboBox *const combo = create_channel_selector(parent, dec, pdch);
 		connect(combo, SIGNAL(currentIndexChanged(int)),
-			this, SLOT(on_probe_selected(int)));
+			this, SLOT(on_channel_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);
+		const ChannelSelector s = {combo, dec, pdch};
+		channel_selectors_.push_back(s);
 	}
 
 	// Add the optional channels
-	for(l = decoder->opt_channels; l; l = l->next) {
+	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);
+		QComboBox *const combo = create_channel_selector(parent, dec, pdch);
 		connect(combo, SIGNAL(currentIndexChanged(int)),
-			this, SLOT(on_probe_selected(int)));
+			this, SLOT(on_channel_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);
+		const ChannelSelector s = {combo, dec, pdch};
+		channel_selectors_.push_back(s);
 	}
 
 	// Add the options
-	shared_ptr<prop::binding::DecoderOptions> binding(
-		new prop::binding::DecoderOptions(_decoder_stack, dec));
+	shared_ptr<binding::Decoder> binding(
+		new binding::Decoder(decoder_stack_, dec));
 	binding->add_properties_to_form(decoder_form, true);
 
-	_bindings.push_back(binding);
+	bindings_.push_back(binding);
 
 	form->addRow(group);
-	_decoder_forms.push_back(group);
+	decoder_forms_.push_back(group);
 }
 
-QComboBox* DecodeTrace::create_probe_selector(
+QComboBox* DecodeTrace::create_channel_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();
+	const auto sigs(session_.signals());
+
+	vector< shared_ptr<Signal> > sig_list(sigs.begin(), sigs.end());
+	std::sort(sig_list.begin(), sig_list.end(),
+		[](const shared_ptr<Signal> &a, const shared_ptr<Signal> b) {
+			return a->name().compare(b->name()) < 0; });
 
-	assert(_decoder_stack);
-	const map<const srd_channel*,
-		shared_ptr<LogicSignal> >::const_iterator probe_iter =
-		dec->channels().find(pdch);
+	assert(decoder_stack_);
+	const auto channel_iter = dec->channels().find(pdch);
 
 	QComboBox *selector = new QComboBox(parent);
 
-	selector->addItem("-", qVariantFromValue((void*)NULL));
+	selector->addItem("-", qVariantFromValue((void*)nullptr));
 
-	if (probe_iter == dec->channels().end())
+	if (channel_iter == dec->channels().end())
 		selector->setCurrentIndex(0);
 
-	for(size_t i = 0; i < sigs.size(); i++) {
-		const shared_ptr<view::Signal> s(sigs[i]);
+	for (const shared_ptr<view::Signal> &s : sig_list) {
 		assert(s);
-
-		if (dynamic_pointer_cast<LogicSignal>(s) && s->enabled())
-		{
-			selector->addItem(s->get_name(),
+		if (dynamic_pointer_cast<LogicSignal>(s) && s->enabled()) {
+			selector->addItem(s->name(),
 				qVariantFromValue((void*)s.get()));
-			if ((*probe_iter).second == s)
-				selector->setCurrentIndex(i + 1);
+
+			if (channel_iter != dec->channels().end() &&
+				(*channel_iter).second == s)
+				selector->setCurrentIndex(
+					selector->count() - 1);
 		}
 	}
 
 	return selector;
 }
 
-void DecodeTrace::commit_decoder_probes(shared_ptr<data::decode::Decoder> &dec)
+void DecodeTrace::commit_decoder_channels(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();
+	map<const srd_channel*, shared_ptr<LogicSignal> > channel_map;
+
+	const unordered_set< shared_ptr<Signal> > sigs(session_.signals());
 
-	BOOST_FOREACH(const ProbeSelector &s, _probe_selectors)
-	{
-		if(s._decoder != dec)
+	for (const ChannelSelector &s : channel_selectors_) {
+		if (s.decoder_ != dec)
 			break;
 
 		const LogicSignal *const selection =
-			(LogicSignal*)s._combo->itemData(
-				s._combo->currentIndex()).value<void*>();
+			(LogicSignal*)s.combo_->itemData(
+				s.combo_->currentIndex()).value<void*>();
 
-		BOOST_FOREACH(shared_ptr<Signal> sig, sigs)
-			if(sig.get() == selection) {
-				probe_map[s._pdch] =
+		for (shared_ptr<Signal> sig : sigs)
+			if (sig.get() == selection) {
+				channel_map[s.pdch_] =
 					dynamic_pointer_cast<LogicSignal>(sig);
 				break;
 			}
 	}
 
-	dec->set_probes(probe_map);
+	dec->set_channels(channel_map);
 }
 
-void DecodeTrace::commit_probes()
+void DecodeTrace::commit_channels()
 {
-	assert(_decoder_stack);
-	BOOST_FOREACH(shared_ptr<data::decode::Decoder> dec,
-		_decoder_stack->stack())
-		commit_decoder_probes(dec);
+	assert(decoder_stack_);
+	for (shared_ptr<data::decode::Decoder> dec : decoder_stack_->stack())
+		commit_decoder_channels(dec);
 
-	_decoder_stack->begin_decode();
+	decoder_stack_->begin_decode();
 }
 
 void DecodeTrace::on_new_decode_data()
 {
-	if (_view)
-		_view->update_viewport();
+	if (owner_)
+		owner_->row_item_appearance_changed(false, true);
 }
 
 void DecodeTrace::delete_pressed()
@@ -669,44 +864,44 @@ void DecodeTrace::delete_pressed()
 
 void DecodeTrace::on_delete()
 {
-	_session.remove_decode_signal(this);
+	session_.remove_decode_signal(this);
 }
 
-void DecodeTrace::on_probe_selected(int)
+void DecodeTrace::on_channel_selected(int)
 {
-	commit_probes();
+	commit_channels();
 }
 
 void DecodeTrace::on_stack_decoder(srd_decoder *decoder)
 {
 	assert(decoder);
-	assert(_decoder_stack);
-	_decoder_stack->push(shared_ptr<data::decode::Decoder>(
+	assert(decoder_stack_);
+	decoder_stack_->push(shared_ptr<data::decode::Decoder>(
 		new data::decode::Decoder(decoder)));
-	_decoder_stack->begin_decode();
+	decoder_stack_->begin_decode();
 
 	create_popup_form();
 }
 
 void DecodeTrace::on_delete_decoder(int index)
 {
-	_decoder_stack->remove(index);
+	decoder_stack_->remove(index);
 
 	// Update the popup
 	create_popup_form();	
 
-	_decoder_stack->begin_decode();
+	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());
+	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++)
+	auto iter = stack.cbegin();
+	for (int i = 0; i < index; i++, iter++)
 		assert(iter != stack.end());
 
 	shared_ptr<Decoder> dec = *iter;
@@ -715,10 +910,11 @@ void DecodeTrace::on_show_hide_decoder(int index)
 	const bool show = !dec->shown();
 	dec->show(show);
 
-	assert(index < (int)_decoder_forms.size());
-	_decoder_forms[index]->set_decoder_visible(show);
+	assert(index < (int)decoder_forms_.size());
+	decoder_forms_[index]->set_decoder_visible(show);
 
-	_view->update_viewport();
+	if (owner_)
+		owner_->row_item_appearance_changed(false, true);
 }
 
 } // namespace view
diff --git a/pv/view/decodetrace.h b/pv/view/decodetrace.h
deleted file mode 100644
index 9a991fc..0000000
--- a/pv/view/decodetrace.h
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * 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/decodetrace.hpp b/pv/view/decodetrace.hpp
new file mode 100644
index 0000000..4964a95
--- /dev/null
+++ b/pv/view/decodetrace.hpp
@@ -0,0 +1,217 @@
+/*
+ * 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_HPP
+#define PULSEVIEW_PV_VIEW_DECODETRACE_HPP
+
+#include "trace.hpp"
+
+#include <list>
+#include <map>
+#include <memory>
+#include <vector>
+
+#include <QSignalMapper>
+
+#include <pv/binding/decoder.hpp>
+#include <pv/data/decode/row.hpp>
+
+struct srd_channel;
+struct srd_decoder;
+
+class QComboBox;
+
+namespace pv {
+
+class Session;
+
+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 ChannelSelector
+	{
+		const QComboBox *combo_;
+		const std::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::Session &session,
+		std::shared_ptr<pv::data::DecoderStack> decoder_stack,
+		int index);
+
+	bool enabled() const;
+
+	const std::shared_ptr<pv::data::DecoderStack>& decoder() const;
+
+	/**
+	 * Computes the vertical extents of the contents of this row item.
+	 * @return A pair containing the minimum and maximum y-values.
+	 */
+	std::pair<int, int> v_extents() const;
+
+	/**
+	 * Paints the background layer of the trace with a QPainter
+	 * @param p the QPainter to paint into.
+	 * @param pp the painting parameters object to paint with..
+	 */
+	void paint_back(QPainter &p, const ViewItemPaintParams &pp);
+
+	/**
+	 * Paints the mid-layer of the trace with a QPainter
+	 * @param p the QPainter to paint into.
+	 * @param pp the painting parameters object to paint with.
+	 */
+	void paint_mid(QPainter &p, const ViewItemPaintParams &pp);
+
+	/**
+	 * Paints the foreground layer of the trace with a QPainter
+	 * @param p the QPainter to paint into.
+	 * @param pp the painting parameters object to paint with.
+	 */
+	void paint_fore(QPainter &p, const ViewItemPaintParams &pp);
+
+	void populate_popup_form(QWidget *parent, QFormLayout *form);
+
+	QMenu* create_context_menu(QWidget *parent);
+
+	void delete_pressed();
+
+private:
+	void draw_annotations(std::vector<pv::data::decode::Annotation> annotations,
+		QPainter &p, int h, const ViewItemPaintParams &pp, int y,
+		size_t base_colour);
+
+	void draw_annotation(const pv::data::decode::Annotation &a, QPainter &p,
+		int text_height, const ViewItemPaintParams &pp, int y,
+		size_t base_colour) const;
+
+	void draw_annotation_block(std::vector<pv::data::decode::Annotation> a,
+		QPainter &p, int h, int y, size_t base_colour) const;
+
+	void draw_instant(const pv::data::decode::Annotation &a, QPainter &p,
+		QColor fill, QColor outline, int h, double x, int y) const;
+
+	void draw_range(const pv::data::decode::Annotation &a, QPainter &p,
+		QColor fill, QColor outline, int h, double start,
+		double end, int y) const;
+
+	void draw_error(QPainter &p, const QString &message,
+		const ViewItemPaintParams &pp);
+
+	void draw_unresolved_period(QPainter &p, int h, int left,
+		int right) const;
+
+	std::pair<double, double> get_pixels_offset_samples_per_pixel() const;
+
+	/**
+	 * Determines the start and end sample for a given pixel range.
+	 * @param x_start the X coordinate of the start sample in the view
+	 * @param x_end the X coordinate of the end sample in the view
+	 * @return Returns a pair containing the start sample and the end
+	 * 	sample that correspond to the start and end coordinates.
+	 */
+	std::pair<uint64_t, uint64_t> get_sample_range(int x_start, int x_end) const;
+
+	int get_row_at_point(const QPoint &point);
+
+	const QString get_annotation_at_point(const QPoint &point);
+
+	void create_decoder_form(int index,
+		std::shared_ptr<pv::data::decode::Decoder> &dec,
+		QWidget *parent, QFormLayout *form);
+
+	QComboBox* create_channel_selector(QWidget *parent,
+		const std::shared_ptr<pv::data::decode::Decoder> &dec,
+		const srd_channel *const pdch);
+
+	void commit_decoder_channels(
+		std::shared_ptr<data::decode::Decoder> &dec);
+
+	void commit_channels();
+
+public:
+	void hover_point_changed();
+
+private Q_SLOTS:
+	void on_new_decode_data();
+
+	void on_delete();
+
+	void on_channel_selected(int);
+
+	void on_stack_decoder(srd_decoder *decoder);
+
+	void on_delete_decoder(int index);
+
+	void on_show_hide_decoder(int index);
+
+private:
+	pv::Session &session_;
+	std::shared_ptr<pv::data::DecoderStack> decoder_stack_;
+
+	uint64_t decode_start_, decode_end_;
+
+	std::list< std::shared_ptr<pv::binding::Decoder> >
+		bindings_;
+
+	std::list<ChannelSelector> channel_selectors_;
+	std::vector<pv::widgets::DecoderGroupBox*> decoder_forms_;
+
+	std::vector<data::decode::Row> visible_rows_;
+	int row_height_, max_visible_rows_;
+
+	QSignalMapper delete_mapper_, show_hide_mapper_;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_DECODETRACE_HPP
diff --git a/pv/view/flag.cpp b/pv/view/flag.cpp
new file mode 100644
index 0000000..779ea2e
--- /dev/null
+++ b/pv/view/flag.cpp
@@ -0,0 +1,112 @@
+/*
+ * 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 "timemarker.hpp"
+#include "view.hpp"
+
+#include <QColor>
+#include <QFormLayout>
+#include <QLineEdit>
+#include <QMenu>
+
+#include <libsigrokcxx/libsigrokcxx.hpp>
+
+#include <pv/widgets/popup.hpp>
+
+using std::shared_ptr;
+
+namespace pv {
+namespace view {
+
+const QColor Flag::FillColour(0x73, 0xD2, 0x16);
+
+Flag::Flag(View &view, const pv::util::Timestamp& time, const QString &text) :
+	TimeMarker(view, FillColour, time),
+	text_(text)
+{
+}
+
+Flag::Flag(const Flag &flag) :
+	TimeMarker(flag.view_, FillColour, flag.time_),
+	std::enable_shared_from_this<pv::view::Flag>(flag)
+{
+}
+
+bool Flag::enabled() const
+{
+	return true;
+}
+
+QString Flag::get_text() const
+{
+	return text_;
+}
+
+pv::widgets::Popup* Flag::create_popup(QWidget *parent)
+{
+	using pv::widgets::Popup;
+
+	Popup *const popup = TimeMarker::create_popup(parent);
+	popup->set_position(parent->mapToGlobal(
+		point(parent->rect())), Popup::Bottom);
+
+	QFormLayout *const form = (QFormLayout*)popup->layout();
+
+	QLineEdit *const text_edit = new QLineEdit(popup);
+	text_edit->setText(text_);
+
+	connect(text_edit, SIGNAL(textChanged(const QString&)),
+		this, SLOT(on_text_changed(const QString&)));
+
+	form->insertRow(0, tr("Text"), text_edit);
+
+	return popup;
+}
+
+QMenu* Flag::create_context_menu(QWidget *parent)
+{
+	QMenu *const menu = new QMenu(parent);
+
+	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 Flag::delete_pressed()
+{
+	on_delete();
+}
+
+void Flag::on_delete()
+{
+	view_.remove_flag(shared_ptr<Flag>(shared_from_this()));
+}
+
+void Flag::on_text_changed(const QString &text)
+{
+	text_ = text;
+	view_.time_item_appearance_changed(true, false);
+}
+
+} // namespace view
+} // namespace pv
diff --git a/pv/view/cursorpair.h b/pv/view/flag.hpp
similarity index 50%
rename from pv/view/cursorpair.h
rename to pv/view/flag.hpp
index 815276e..bc82934 100644
--- a/pv/view/cursorpair.h
+++ b/pv/view/flag.hpp
@@ -1,7 +1,7 @@
 /*
  * This file is part of the PulseView project.
  *
- * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ * 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
@@ -18,64 +18,66 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_VIEW_CURSORPAIR_H
-#define PULSEVIEW_PV_VIEW_CURSORPAIR_H
+#ifndef PULSEVIEW_PV_VIEW_FLAG_HPP
+#define PULSEVIEW_PV_VIEW_FLAG_HPP
 
-#include "cursor.h"
+#include <memory>
 
-#include <boost/shared_ptr.hpp>
+#include "timemarker.hpp"
 
-#include <QPainter>
-
-class QPainter;
+class QMenu;
 
 namespace pv {
 namespace view {
 
-class CursorPair
+class Flag : public TimeMarker,
+	public std::enable_shared_from_this<pv::view::Flag>
 {
-private:
-	static const int DeltaPadding;
+	Q_OBJECT
+
+public:
+	static const QColor FillColour;
 
 public:
 	/**
 	 * Constructor.
 	 * @param view A reference to the view that owns this cursor pair.
+	 * @param time The time to set the flag to.
+	 * @param text The text of the marker.
 	 */
-	CursorPair(View &view);
+	Flag(View &view, const pv::util::Timestamp& time, const QString &text);
 
 	/**
-	 * Returns a pointer to the first cursor.
+	 * Copy constructor.
 	 */
-	boost::shared_ptr<Cursor> first() const;
+	Flag(const Flag &flag);
 
 	/**
-	 * Returns a pointer to the second cursor.
+	 * Returns true if the item is visible and enabled.
 	 */
-	boost::shared_ptr<Cursor> second() const;
+	bool enabled() const;
 
-public:
-	QRectF get_label_rect(const QRect &rect) const;
+	/**
+	 * Gets the text to show in the marker.
+	 */
+	QString get_text() const;
 
-	void draw_markers(QPainter &p,
-		const QRect &rect, unsigned int prefix);
+	pv::widgets::Popup* create_popup(QWidget *parent);
 
-	void draw_viewport_background(QPainter &p, const QRect &rect);
+	QMenu* create_context_menu(QWidget *parent);
 
-	void draw_viewport_foreground(QPainter &p, const QRect &rect);
+	void delete_pressed();
 
-	void compute_text_size(QPainter &p, unsigned int prefix);
+private Q_SLOTS:
+	void on_delete();
 
-	std::pair<float, float> get_cursor_offsets() const;
+	void on_text_changed(const QString &text);
 
 private:
-	boost::shared_ptr<Cursor> _first, _second;
-	const View &_view;
-
-	QSizeF _text_size;
+	QString text_;
 };
 
 } // namespace view
 } // namespace pv
 
-#endif // PULSEVIEW_PV_VIEW_CURSORPAIR_H
+#endif // PULSEVIEW_PV_VIEW_FLAG_HPP
diff --git a/pv/view/header.cpp b/pv/view/header.cpp
index a790252..1977d05 100644
--- a/pv/view/header.cpp
+++ b/pv/view/header.cpp
@@ -18,15 +18,16 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "header.h"
-#include "view.h"
+#include "header.hpp"
+#include "view.hpp"
 
-#include "signal.h"
-#include "../sigsession.h"
+#include "signal.hpp"
+#include "tracegroup.hpp"
 
-#include <assert.h>
+#include <cassert>
+#include <algorithm>
 
-#include <boost/foreach.hpp>
+#include <boost/iterator/filter_iterator.hpp>
 
 #include <QApplication>
 #include <QMenu>
@@ -34,270 +35,187 @@
 #include <QPainter>
 #include <QRect>
 
-#include <pv/widgets/popup.h>
+#include <pv/session.hpp>
+#include <pv/widgets/popup.hpp>
 
-using boost::shared_ptr;
+using boost::make_filter_iterator;
+using std::dynamic_pointer_cast;
 using std::max;
 using std::make_pair;
+using std::min;
 using std::pair;
+using std::shared_ptr;
+using std::stable_sort;
 using std::vector;
 
 namespace pv {
 namespace view {
 
 const int Header::Padding = 12;
+const int Header::BaselineOffset = 5;
 
-Header::Header(View &parent) :
-	MarginWidget(parent),
-	_dragging(false)
+static bool item_selected(shared_ptr<TraceTreeItem> r)
 {
-	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()));
+	return r->selected();
+}
 
-	// Trigger the initial event manually. The default device has signals
-	// which were created before this object came into being
-	on_signals_changed();
+Header::Header(View &parent) :
+	MarginWidget(parent)
+{
 }
 
 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);
+	QRectF max_rect(-Padding, 0, Padding, 0);
+	const vector<shared_ptr<TraceTreeItem>> items(
+		view_.list_by_type<TraceTreeItem>());
+	for (auto &i : items)
+		if (i->enabled())
+			max_rect = max_rect.united(i->label_rect(QRect()));
+	return QSize(max_rect.width() + Padding + BaselineOffset, 0);
 }
 
-shared_ptr<Trace> Header::get_mouse_over_trace(const QPoint &pt)
+QSize Header::extended_size_hint() const
 {
-	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>();
+	return sizeHint() + QSize(ViewItem::HighlightRadius, 0);
 }
 
-void Header::clear_selection()
+vector< shared_ptr<ViewItem> > Header::items()
 {
-	const vector< shared_ptr<Trace> > traces(_view.get_traces());
-	BOOST_FOREACH(const shared_ptr<Trace> t, traces) {
-		assert(t);
-		t->select(false);
-	}
-
-	update();
+	const vector<shared_ptr<TraceTreeItem>> items(
+		view_.list_by_type<TraceTreeItem>());
+	return vector< shared_ptr<ViewItem> >(items.begin(), items.end());
 }
 
-void Header::paintEvent(QPaintEvent*)
+shared_ptr<ViewItem> Header::get_mouse_over_item(const QPoint &pt)
 {
-	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();
+	const QRect r(0, 0, width() - BaselineOffset, height());
+	const vector<shared_ptr<TraceTreeItem>> items(
+		view_.list_by_type<TraceTreeItem>());
+	for (auto i = items.rbegin(); i != items.rend(); i++)
+		if ((*i)->enabled() && (*i)->label_rect(r).contains(pt))
+			return *i;
+	return shared_ptr<TraceTreeItem>();
 }
 
-void Header::mousePressEvent(QMouseEvent *event)
+void Header::paintEvent(QPaintEvent*)
 {
-	assert(event);
+	// The trace labels are not drawn with the arrows exactly on the
+	// left edge of the widget, because then the selection shadow
+	// would be clipped away.
+	const QRect rect(0, 0, width() - BaselineOffset, height());
 
-	const vector< shared_ptr<Trace> > traces(_view.get_traces());
+	vector< shared_ptr<RowItem> > items(
+		view_.list_by_type<RowItem>());
 
-	if (event->button() & Qt::LeftButton) {
-		_mouse_down_point = event->pos();
+	stable_sort(items.begin(), items.end(),
+		[](const shared_ptr<RowItem> &a, const shared_ptr<RowItem> &b) {
+			return a->point(QRect()).y() < b->point(QRect()).y(); });
 
-		// 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()));
-	}
+	QPainter painter(this);
+	painter.setRenderHint(QPainter::Antialiasing);
 
-	// 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()));
-		}
-	}
+	for (const shared_ptr<RowItem> r : items) {
+		assert(r);
 
-	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);
+		const bool highlight = !item_dragging_ &&
+			r->label_rect(rect).contains(mouse_point_);
+		r->paint_label(painter, rect, highlight);
 	}
 
-	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();
-	}
+	painter.end();
 }
 
-void Header::mouseMoveEvent(QMouseEvent *event)
+void Header::contextMenuEvent(QContextMenuEvent *event)
 {
-	assert(event);
-	_mouse_point = event->pos();
-
-	if (!(event->buttons() & Qt::LeftButton))
+	const shared_ptr<ViewItem> r = get_mouse_over_item(mouse_point_);
+	if (!r)
 		return;
 
-	if ((event->pos() - _mouse_down_point).manhattanLength() <
-		QApplication::startDragDistance())
-		return;
+	QMenu *menu = r->create_context_menu(this);
+	if (!menu)
+		menu = new QMenu(this);
 
-	// Move the signals if we are dragging
-	if (!_drag_traces.empty())
+	const vector< shared_ptr<TraceTreeItem> > items(
+		view_.list_by_type<TraceTreeItem>());
+	if (std::count_if(items.begin(), items.end(), item_selected) > 1)
 	{
-		_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();
+		menu->addSeparator();
+
+		QAction *const group = new QAction(tr("Group"), this);
+		QList<QKeySequence> shortcuts;
+		shortcuts.append(QKeySequence(Qt::ControlModifier | Qt::Key_G));
+		group->setShortcuts(shortcuts);
+		connect(group, SIGNAL(triggered()), this, SLOT(on_group()));
+		menu->addAction(group);
 	}
 
-	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());
+	menu->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;
-	}
-	}
-}
+	MarginWidget::keyPressEvent(e);
 
-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()));
-	}
+	if (e->key() == Qt::Key_G && e->modifiers() == Qt::ControlModifier)
+		on_group();
+	else if (e->key() == Qt::Key_U && e->modifiers() == Qt::ControlModifier)
+		on_ungroup();
 }
 
-void Header::on_signals_moved()
+void Header::on_group()
 {
-	update();
+	const vector< shared_ptr<TraceTreeItem> > items(
+		view_.list_by_type<TraceTreeItem>());
+	vector< shared_ptr<TraceTreeItem> > selected_items(
+		make_filter_iterator(item_selected, items.begin(), items.end()),
+		make_filter_iterator(item_selected, items.end(), items.end()));
+	stable_sort(selected_items.begin(), selected_items.end(),
+		[](const shared_ptr<TraceTreeItem> &a, const shared_ptr<TraceTreeItem> &b) {
+			return a->visual_v_offset() < b->visual_v_offset(); });
+
+	shared_ptr<TraceGroup> group(new TraceGroup());
+	shared_ptr<TraceTreeItem> mouse_down_item(
+		std::dynamic_pointer_cast<TraceTreeItem>(mouse_down_item_));
+	shared_ptr<TraceTreeItem> focus_item(
+		mouse_down_item ? mouse_down_item : selected_items.front());
+
+	assert(focus_item);
+	assert(focus_item->owner());
+	focus_item->owner()->add_child_item(group);
+
+	// Set the group v_offset here before reparenting
+	group->force_to_v_offset(focus_item->layout_v_offset() +
+		focus_item->v_extents().first);
+
+	for (size_t i = 0; i < selected_items.size(); i++) {
+		const shared_ptr<TraceTreeItem> &r = selected_items[i];
+		assert(r->owner());
+		r->owner()->remove_child_item(r);
+		group->add_child_item(r);
+
+		// Put the items at 1-pixel offsets, so that restack will
+		// stack them in the right order
+		r->set_layout_v_offset(i);
+	}
 }
 
-void Header::on_trace_text_changed()
+void Header::on_ungroup()
 {
-	update();
-	geometry_updated();
+	bool restart;
+	do {
+		restart = false;
+		const vector< shared_ptr<TraceGroup> > groups(
+			view_.list_by_type<TraceGroup>());
+		for (const shared_ptr<TraceGroup> tg : groups)
+			if (tg->selected()) {
+				tg->ungroup();
+				restart = true;
+				break;
+			}
+	} while (restart);
 }
 
 } // namespace view
diff --git a/pv/view/header.h b/pv/view/header.hpp
similarity index 56%
rename from pv/view/header.h
rename to pv/view/header.hpp
index 5474f10..ade6d33 100644
--- a/pv/view/header.h
+++ b/pv/view/header.hpp
@@ -18,22 +18,21 @@
  * 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>
+#ifndef PULSEVIEW_PV_VIEW_HEADER_HPP
+#define PULSEVIEW_PV_VIEW_HEADER_HPP
 
 #include <list>
+#include <memory>
 #include <utility>
 
-#include "marginwidget.h"
+#include "marginwidget.hpp"
 
 namespace pv {
 namespace view {
 
-class Trace;
+class TraceTreeItem;
 class View;
+class ViewItem;
 
 class Header : public MarginWidget
 {
@@ -47,48 +46,49 @@ public:
 
 	QSize sizeHint() const;
 
+	/**
+	 * The extended area that the header widget would like to be sized to.
+	 * @remarks This area is the area specified by sizeHint, extended by
+	 * the area to overlap the viewport.
+	 */
+	QSize extended_size_hint() const;
+
+	/**
+	 * The horizontal offset, relative to the left edge of the widget,
+	 * where the arrows of the trace labels end.
+	 */
+	static const int BaselineOffset;
+
 private:
-	boost::shared_ptr<pv::view::Trace> get_mouse_over_trace(
+	/**
+	 * Gets the row items.
+	 */
+	std::vector< std::shared_ptr<pv::view::ViewItem> > items();
+
+	/**
+	 * Gets the first view item which has a label that contains @c pt .
+	 * @param pt the point to search with.
+	 * @return the view item that has been found, or and empty
+	 *   @c shared_ptr if no item was found.
+	 */
+	std::shared_ptr<pv::view::ViewItem> get_mouse_over_item(
 		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;
+private Q_SLOTS:
+	void on_group();
 
-	std::list<std::pair<boost::weak_ptr<Trace>, int> >
-		_drag_traces;
+	void on_ungroup();
 };
 
 } // namespace view
 } // namespace pv
 
-#endif // PULSEVIEW_PV_VIEW_HEADER_H
+#endif // PULSEVIEW_PV_VIEW_HEADER_HPP
diff --git a/pv/view/logicsignal.cpp b/pv/view/logicsignal.cpp
index 0b44264..78bfdec 100644
--- a/pv/view/logicsignal.cpp
+++ b/pv/view/logicsignal.cpp
@@ -20,27 +20,44 @@
 
 #include <extdef.h>
 
-#include <math.h>
+#include <cassert>
+#include <cmath>
 
+#include <algorithm>
+
+#include <QApplication>
 #include <QFormLayout>
 #include <QToolBar>
 
-#include "logicsignal.h"
-#include "view.h"
+#include "logicsignal.hpp"
+#include "view.hpp"
+
+#include <pv/session.hpp>
+#include <pv/devicemanager.hpp>
+#include <pv/devices/device.hpp>
+#include <pv/data/logic.hpp>
+#include <pv/data/logicsegment.hpp>
+#include <pv/view/view.hpp>
 
-#include <pv/sigsession.h>
-#include <pv/device/devinst.h>
-#include <pv/data/logic.h>
-#include <pv/data/logicsnapshot.h>
-#include <pv/view/view.h>
+#include <libsigrokcxx/libsigrokcxx.hpp>
 
-using boost::shared_ptr;
 using std::deque;
 using std::max;
+using std::make_pair;
 using std::min;
 using std::pair;
+using std::shared_ptr;
 using std::vector;
 
+using sigrok::Channel;
+using sigrok::ConfigKey;
+using sigrok::Capability;
+using sigrok::Error;
+using sigrok::Trigger;
+using sigrok::TriggerStage;
+using sigrok::TriggerMatch;
+using sigrok::TriggerMatchType;
+
 namespace pv {
 namespace view {
 
@@ -63,18 +80,50 @@ const QColor LogicSignal::SignalColours[10] = {
 	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)
+QColor LogicSignal::TriggerMarkerBackgroundColour = QColor(0xED, 0xD4, 0x00);
+const int LogicSignal::TriggerMarkerPadding = 2;
+const char* LogicSignal::TriggerMarkerIcons[8] = {
+	nullptr,
+	":/icons/trigger-marker-low.svg",
+	":/icons/trigger-marker-high.svg",
+	":/icons/trigger-marker-rising.svg",
+	":/icons/trigger-marker-falling.svg",
+	":/icons/trigger-marker-change.svg",
+	nullptr,
+	nullptr
+};
+
+QCache<QString, const QIcon> LogicSignal::icon_cache_;
+QCache<QString, const QPixmap> LogicSignal::pixmap_cache_;
+
+LogicSignal::LogicSignal(
+	pv::Session &session,
+	shared_ptr<devices::Device> device,
+	shared_ptr<Channel> channel,
+	shared_ptr<data::Logic> data) :
+	Signal(session, channel),
+	signal_height_(QFontMetrics(QApplication::font()).height() * 2),
+	device_(device),
+	data_(data),
+	trigger_none_(nullptr),
+	trigger_rising_(nullptr),
+	trigger_high_(nullptr),
+	trigger_falling_(nullptr),
+	trigger_low_(nullptr),
+	trigger_change_(nullptr)
 {
-	_colour = SignalColours[probe->index % countof(SignalColours)];
+	shared_ptr<Trigger> trigger;
+
+	set_colour(SignalColours[channel->index() % countof(SignalColours)]);
+
+	/* Populate this channel's trigger setting with whatever we
+	 * find in the current session trigger, if anything. */
+	trigger_match_ = nullptr;
+	if ((trigger = session_.session()->trigger()))
+		for (auto stage : trigger->stages())
+			for (auto match : stage->matches())
+				if (match->channel() == channel_)
+					trigger_match_ = match->type();
 }
 
 LogicSignal::~LogicSignal()
@@ -83,71 +132,84 @@ LogicSignal::~LogicSignal()
 
 shared_ptr<pv::data::SignalData> LogicSignal::data() const
 {
-	return _data;
+	return data_;
 }
 
 shared_ptr<pv::data::Logic> LogicSignal::logic_data() const
 {
-	return _data;
+	return data_;
+}
+
+void LogicSignal::set_logic_data(std::shared_ptr<pv::data::Logic> data)
+{
+	data_ = data;
 }
 
-void LogicSignal::paint_back(QPainter &p, int left, int right)
+std::pair<int, int> LogicSignal::v_extents() const
 {
-	if (_probe->enabled)
-		paint_axis(p, get_y(), left, right);
+	const int signal_margin =
+		QFontMetrics(QApplication::font()).height() / 2;
+	return make_pair(-signal_height_ - signal_margin, signal_margin);
 }
 
-void LogicSignal::paint_mid(QPainter &p, int left, int right)
+int LogicSignal::scale_handle_offset() const
 {
-	using pv::view::View;
+	return -signal_height_;
+}
 
+void LogicSignal::scale_handle_dragged(int offset)
+{
+	const int font_height = QFontMetrics(QApplication::font()).height();
+	const int units = (-offset / font_height);
+	signal_height_ = ((units < 1) ? 1 : units) * font_height;
+}
+
+void LogicSignal::paint_mid(QPainter &p, const ViewItemPaintParams &pp)
+{
 	QLineF *line;
 
 	vector< pair<int64_t, bool> > edges;
 
-	assert(_probe);
-	assert(_data);
-	assert(right >= left);
+	assert(channel_);
+	assert(data_);
+	assert(owner_);
 
-	assert(_view);
-	const int y = _v_offset - _view->v_offset();
-	
-	const double scale = _view->scale();
-	assert(scale > 0);
-	
-	const double offset = _view->offset();
+	const int y = get_visual_y();
 
-	if (!_probe->enabled)
+	if (!channel_->enabled())
 		return;
 
-	const float high_offset = y - View::SignalHeight + 0.5f;
+	const float high_offset = y - signal_height_ + 0.5f;
 	const float low_offset = y + 0.5f;
 
-	const deque< shared_ptr<pv::data::LogicSnapshot> > &snapshots =
-		_data->get_snapshots();
-	if (snapshots.empty())
+	const deque< shared_ptr<pv::data::LogicSegment> > &segments =
+		data_->logic_segments();
+	if (segments.empty())
 		return;
 
-	const shared_ptr<pv::data::LogicSnapshot> &snapshot =
-		snapshots.front();
+	const shared_ptr<pv::data::LogicSegment> &segment =
+		segments.front();
 
-	double samplerate = _data->samplerate();
+	double samplerate = segment->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);
+	const double pixels_offset = pp.pixels_offset();
+	const pv::util::Timestamp& start_time = segment->start_time();
+	const int64_t last_sample = segment->get_sample_count() - 1;
+	const double samples_per_pixel = samplerate * pp.scale();
+	const pv::util::Timestamp start = samplerate * (pp.offset() - start_time);
+	const pv::util::Timestamp end = start + samples_per_pixel * pp.width();
+
+	const int64_t start_sample = min(max(floor(start).convert_to<int64_t>(),
+		(int64_t)0), last_sample);
+	const uint64_t end_sample = min(max(ceil(end).convert_to<int64_t>(),
+		(int64_t)0), last_sample);
+
+	segment->get_subsampled_edges(edges, start_sample, end_sample,
+		samples_per_pixel / Oversampling, channel_->index());
 	assert(edges.size() >= 2);
 
 	// Paint the edges
@@ -155,11 +217,9 @@ void LogicSignal::paint_mid(QPainter &p, int left, int right)
 	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++) {
+	for (auto i = edges.cbegin() + 1; i != edges.cend() - 1; i++) {
 		const float x = ((*i).first / samples_per_pixel -
-			pixels_offset) + left;
+			pixels_offset) + pp.left();
 		*line++ = QLineF(x, high_offset, x, low_offset);
 	}
 
@@ -173,14 +233,51 @@ void LogicSignal::paint_mid(QPainter &p, int left, int right)
 
 	p.setPen(HighColour);
 	paint_caps(p, cap_lines, edges, true, samples_per_pixel,
-		pixels_offset, left, high_offset);
+		pixels_offset, pp.left(), high_offset);
 	p.setPen(LowColour);
 	paint_caps(p, cap_lines, edges, false, samples_per_pixel,
-		pixels_offset, left, low_offset);
+		pixels_offset, pp.left(), low_offset);
 
 	delete[] cap_lines;
 }
 
+void LogicSignal::paint_fore(QPainter &p, const ViewItemPaintParams &pp)
+{
+	// Draw the trigger marker
+	if (!trigger_match_ || !channel_->enabled())
+		return;
+
+	const int y = get_visual_y();
+	const vector<int32_t> trig_types = get_trigger_types();
+	for (int32_t type_id : trig_types) {
+		const TriggerMatchType *const type =
+			TriggerMatchType::get(type_id);
+		if (trigger_match_ != type || type_id < 0 ||
+			(size_t)type_id >= countof(TriggerMarkerIcons) ||
+			!TriggerMarkerIcons[type_id])
+			continue;
+
+		const QPixmap *const pixmap = get_pixmap(
+			TriggerMarkerIcons[type_id]);
+		if (!pixmap)
+			continue;
+
+		const float pad = TriggerMarkerPadding - 0.5f;
+		const QSize size = pixmap->size();
+		const QPoint point(
+			pp.right() - size.width() - pad * 2,
+			y - (signal_height_ + size.height()) / 2);
+
+		p.setPen(QPen(TriggerMarkerBackgroundColour.darker()));
+		p.setBrush(TriggerMarkerBackgroundColour);
+		p.drawRoundedRect(QRectF(point, size).adjusted(
+			-pad, -pad, pad, pad), pad, pad);
+		p.drawPixmap(point, *pixmap);
+
+		break;
+	}
+}
+
 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,
@@ -188,8 +285,7 @@ void LogicSignal::paint_caps(QPainter &p, QLineF *const lines,
 {
 	QLineF *line = lines;
 
-	for (vector<pv::data::LogicSnapshot::EdgePair>::const_iterator i =
-		edges.begin(); i != (edges.end() - 1); i++)
+	for (auto i = edges.begin(); i != (edges.end() - 1); i++)
 		if ((*i).second == level) {
 			*line++ = QLineF(
 				((*i).first / samples_per_pixel -
@@ -203,150 +299,191 @@ void LogicSignal::paint_caps(QPainter &p, QLineF *const lines,
 
 void LogicSignal::init_trigger_actions(QWidget *parent)
 {
-	_trigger_none = new QAction(QIcon(":/icons/trigger-none.svg"),
+	trigger_none_ = new QAction(*get_icon(":/icons/trigger-none.svg"),
 		tr("No trigger"), parent);
-	_trigger_none->setCheckable(true);
-	connect(_trigger_none, SIGNAL(triggered()),
-		this, SLOT(on_trigger_none()));
+	trigger_none_->setCheckable(true);
+	connect(trigger_none_, SIGNAL(triggered()), this, SLOT(on_trigger()));
 
-	_trigger_rising = new QAction(QIcon(":/icons/trigger-rising.svg"),
+	trigger_rising_ = new QAction(*get_icon(":/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_rising_->setCheckable(true);
+	connect(trigger_rising_, SIGNAL(triggered()), this, SLOT(on_trigger()));
 
-	_trigger_high = new QAction(QIcon(":/icons/trigger-high.svg"),
+	trigger_high_ = new QAction(*get_icon(":/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_high_->setCheckable(true);
+	connect(trigger_high_, SIGNAL(triggered()), this, SLOT(on_trigger()));
 
-	_trigger_falling = new QAction(QIcon(":/icons/trigger-falling.svg"),
+	trigger_falling_ = new QAction(*get_icon(":/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_falling_->setCheckable(true);
+	connect(trigger_falling_, SIGNAL(triggered()), this, SLOT(on_trigger()));
 
-	_trigger_low = new QAction(QIcon(":/icons/trigger-low.svg"),
+	trigger_low_ = new QAction(*get_icon(":/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_low_->setCheckable(true);
+	connect(trigger_low_, SIGNAL(triggered()), this, SLOT(on_trigger()));
 
-	_trigger_change = new QAction(QIcon(":/icons/trigger-change.svg"),
+	trigger_change_ = new QAction(*get_icon(":/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()));
+	trigger_change_->setCheckable(true);
+	connect(trigger_change_, SIGNAL(triggered()), this, SLOT(on_trigger()));
 }
 
-void LogicSignal::populate_popup_form(QWidget *parent, QFormLayout *form)
+const vector<int32_t> LogicSignal::get_trigger_types() const
 {
-	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);
+	const auto sr_dev = device_->device();
+	if (sr_dev->config_check(ConfigKey::TRIGGER_MATCH, Capability::LIST)) {
+		const Glib::VariantContainerBase gvar =
+			sr_dev->config_list(ConfigKey::TRIGGER_MATCH);
+		return Glib::VariantBase::cast_dynamic<
+			Glib::Variant<vector<int32_t>>>(gvar).get();
+	} else {
+		return vector<int32_t>();
 	}
 }
 
-void LogicSignal::add_trigger_action(const char *trig_types, char type,
-	QAction *action)
+QAction* LogicSignal::action_from_trigger_type(const TriggerMatchType *type)
 {
-	while(*trig_types)
-		if(*trig_types++ == type) {
-			_trigger_bar->addAction(action);
+	QAction *action;
+
+	action = trigger_none_;
+	if (type) {
+		switch (type->id()) {
+		case SR_TRIGGER_ZERO:
+			action = trigger_low_;
 			break;
+		case SR_TRIGGER_ONE:
+			action = trigger_high_;
+			break;
+		case SR_TRIGGER_RISING:
+			action = trigger_rising_;
+			break;
+		case SR_TRIGGER_FALLING:
+			action = trigger_falling_;
+			break;
+		case SR_TRIGGER_EDGE:
+			action = trigger_change_;
+			break;
+		default:
+			assert(0);
 		}
+	}
+
+	return action;
 }
 
-void LogicSignal::update_trigger_actions()
+const TriggerMatchType *LogicSignal::trigger_type_from_action(QAction *action)
 {
-	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');
+	if (action == trigger_low_)
+		return TriggerMatchType::ZERO;
+	else if (action == trigger_high_)
+		return TriggerMatchType::ONE;
+	else if (action == trigger_rising_)
+		return TriggerMatchType::RISING;
+	else if (action == trigger_falling_)
+		return TriggerMatchType::FALLING;
+	else if (action == trigger_change_)
+		return TriggerMatchType::EDGE;
+	else
+		return nullptr;
 }
 
-void LogicSignal::set_trigger(char type)
+void LogicSignal::populate_popup_form(QWidget *parent, QFormLayout *form)
 {
-	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);
+	Signal::populate_popup_form(parent, form);
 
-	const int probe_count = g_slist_length(sdi->channels);
-	assert(probe_count > 0);
+	const vector<int32_t> trig_types = get_trigger_types();
 
-	assert(_probe && _probe->index < probe_count);
+	if (!trig_types.empty()) {
+		trigger_bar_ = new QToolBar(parent);
+		init_trigger_actions(trigger_bar_);
+		trigger_bar_->addAction(trigger_none_);
+		trigger_none_->setChecked(!trigger_match_);
 
-	for (int i = 0; i < probe_count; i++) {
-		sr_dev_trigger_set(sdi, i, (i == _probe->index) ?
-			trigger_string : NULL);
+		for (auto type_id : trig_types) {
+			const TriggerMatchType *const type =
+				TriggerMatchType::get(type_id);
+			QAction *const action = action_from_trigger_type(type);
+			trigger_bar_->addAction(action);
+			action->setChecked(trigger_match_ == type);
+		}
+		form->addRow(tr("Trigger"), trigger_bar_);
 	}
-
-	update_trigger_actions();
 }
 
-void LogicSignal::on_trigger_none()
+void LogicSignal::modify_trigger()
 {
-	set_trigger('\0');	
-}
+	auto trigger = session_.session()->trigger();
+	auto new_trigger = session_.device_manager().context()->create_trigger("pulseview");
+
+	if (trigger) {
+		for (auto stage : trigger->stages()) {
+			const auto &matches = stage->matches();
+			if (std::none_of(matches.begin(), matches.end(),
+			    [&](shared_ptr<TriggerMatch> match) {
+					return match->channel() != channel_; }))
+				continue;
+
+			auto new_stage = new_trigger->add_stage();
+			for (auto match : stage->matches()) {
+				if (match->channel() == channel_)
+					continue;
+				new_stage->add_match(match->channel(), match->type());
+			}
+		}
+	}
 
-void LogicSignal::on_trigger_rising()
-{
-	set_trigger('r');	
-}
+	if (trigger_match_) {
+		// Until we can let the user decide how to group trigger matches
+		// into stages, put all of the matches into a single stage --
+		// most devices only support a single trigger stage.
+		if (new_trigger->stages().empty())
+			new_trigger->add_stage();
 
-void LogicSignal::on_trigger_high()
-{
-	set_trigger('1');	
+		new_trigger->stages().back()->add_match(channel_, trigger_match_);
+	}
+
+	session_.session()->set_trigger(
+		new_trigger->stages().empty() ? nullptr : new_trigger);
+
+	if (owner_)
+		owner_->row_item_appearance_changed(false, true);
 }
 
-void LogicSignal::on_trigger_falling()
+const QIcon* LogicSignal::get_icon(const char *path)
 {
-	set_trigger('f');	
+	const QIcon *icon = icon_cache_.take(path);
+	if (!icon) {
+		icon = new QIcon(path);
+		icon_cache_.insert(path, icon);
+	}
+
+	return icon;
 }
 
-void LogicSignal::on_trigger_low()
+const QPixmap* LogicSignal::get_pixmap(const char *path)
 {
-	set_trigger('0');	
+	const QPixmap *pixmap = pixmap_cache_.take(path);
+	if (!pixmap) {
+		pixmap = new QPixmap(path);
+		pixmap_cache_.insert(path, pixmap);
+	}
+
+	return pixmap;
 }
 
-void LogicSignal::on_trigger_change()
+void LogicSignal::on_trigger()
 {
-	set_trigger('c');	
+	QAction *action;
+
+	action_from_trigger_type(trigger_match_)->setChecked(false);
+
+	action = (QAction *)sender();
+	action->setChecked(true);
+	trigger_match_ = trigger_type_from_action(action);
+
+	modify_trigger();
 }
 
 } // namespace view
diff --git a/pv/view/logicsignal.h b/pv/view/logicsignal.h
deleted file mode 100644
index c3e1666..0000000
--- a/pv/view/logicsignal.h
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * 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/logicsignal.hpp b/pv/view/logicsignal.hpp
new file mode 100644
index 0000000..8c3525e
--- /dev/null
+++ b/pv/view/logicsignal.hpp
@@ -0,0 +1,155 @@
+/*
+ * 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_HPP
+#define PULSEVIEW_PV_VIEW_LOGICSIGNAL_HPP
+
+#include <QCache>
+
+#include "signal.hpp"
+
+#include <memory>
+
+class QIcon;
+class QToolBar;
+
+namespace sigrok {
+class TriggerMatchType;
+}
+
+namespace pv {
+
+namespace devices {
+class Device;
+}
+
+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];
+
+	static QColor TriggerMarkerBackgroundColour;
+	static const int TriggerMarkerPadding;
+	static const char* TriggerMarkerIcons[8];
+
+public:
+	LogicSignal(pv::Session &session,
+		std::shared_ptr<devices::Device> device,
+		std::shared_ptr<sigrok::Channel> channel,
+		std::shared_ptr<pv::data::Logic> data);
+
+	virtual ~LogicSignal();
+
+	std::shared_ptr<pv::data::SignalData> data() const;
+
+	std::shared_ptr<pv::data::Logic> logic_data() const;
+
+	void set_logic_data(std::shared_ptr<pv::data::Logic> data);
+
+	/**
+	 * Computes the vertical extents of the contents of this row item.
+	 * @return A pair containing the minimum and maximum y-values.
+	 */
+	std::pair<int, int> v_extents() const;
+
+	/**
+	 * Returns the offset to show the drag handle.
+	 */
+	int scale_handle_offset() const;
+
+	/**
+	 * Handles the scale handle being dragged to an offset.
+	 * @param offset the offset the scale handle was dragged to.
+	 */
+	void scale_handle_dragged(int offset);
+
+	/**
+	 * Paints the mid-layer of the signal with a QPainter
+	 * @param p the QPainter to paint into.
+	 * @param pp the painting parameters object to paint with..
+	 */
+	void paint_mid(QPainter &p, const ViewItemPaintParams &pp);
+
+	/**
+	 * Paints the foreground layer of the signal with a QPainter
+	 * @param p the QPainter to paint into.
+	 * @param pp the painting parameters object to paint with.
+	 */
+	virtual void paint_fore(QPainter &p, const ViewItemPaintParams &pp);
+
+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);
+
+	const std::vector<int32_t> get_trigger_types() const;
+	QAction* action_from_trigger_type(
+		const sigrok::TriggerMatchType *match);
+	const sigrok::TriggerMatchType* trigger_type_from_action(
+		QAction *action);
+	void populate_popup_form(QWidget *parent, QFormLayout *form);
+	void modify_trigger();
+
+	static const QIcon* get_icon(const char *path);
+	static const QPixmap* get_pixmap(const char *path);
+
+private Q_SLOTS:
+	void on_trigger();
+
+private:
+	int signal_height_;
+
+	std::shared_ptr<pv::devices::Device> device_;
+	std::shared_ptr<pv::data::Logic> data_;
+
+	const sigrok::TriggerMatchType *trigger_match_;
+	QToolBar *trigger_bar_;
+	QAction *trigger_none_;
+	QAction *trigger_rising_;
+	QAction *trigger_high_;
+	QAction *trigger_falling_;
+	QAction *trigger_low_;
+	QAction *trigger_change_;
+
+	static QCache<QString, const QIcon> icon_cache_;
+	static QCache<QString, const QPixmap> pixmap_cache_;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_LOGICSIGNAL_HPP
diff --git a/pv/view/marginwidget.cpp b/pv/view/marginwidget.cpp
index 539551d..0d65761 100644
--- a/pv/view/marginwidget.cpp
+++ b/pv/view/marginwidget.cpp
@@ -18,17 +18,60 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "view.h"
+#include <QMenu>
+#include <QMouseEvent>
 
-#include "marginwidget.h"
+#include "view.hpp"
+
+#include "marginwidget.hpp"
+
+#include <pv/widgets/popup.hpp>
+
+using std::shared_ptr;
 
 namespace pv {
 namespace view {
 
 MarginWidget::MarginWidget(View &parent) :
-	QWidget(&parent),
-	_view(parent)
+	ViewWidget(parent)
+{
+	setAttribute(Qt::WA_NoSystemBackground, true);
+}
+
+void MarginWidget::item_clicked(const shared_ptr<ViewItem> &item)
+{
+	if (item && item->enabled())
+		show_popup(item);
+}
+
+void MarginWidget::show_popup(const shared_ptr<ViewItem> &item)
 {
+	pv::widgets::Popup *const p = item->create_popup(this);
+	if (p)
+		p->show();
+}
+
+void MarginWidget::contextMenuEvent(QContextMenuEvent *event)
+{
+	const shared_ptr<ViewItem> r = get_mouse_over_item(mouse_point_);
+	if (!r)
+		return;
+
+	QMenu *menu = r->create_context_menu(this);
+	if (menu)
+		menu->exec(event->globalPos());
+}
+
+void MarginWidget::keyPressEvent(QKeyEvent *e)
+{
+	assert(e);
+
+	if (e->key() == Qt::Key_Delete) {
+		const auto items = this->items();
+		for (auto &i : items)
+			if (i->selected())
+				i->delete_pressed();
+	}
 }
 
 } // namespace view
diff --git a/pv/view/selectableitem.h b/pv/view/marginwidget.hpp
similarity index 50%
rename from pv/view/selectableitem.h
rename to pv/view/marginwidget.hpp
index d321775..963d717 100644
--- a/pv/view/selectableitem.h
+++ b/pv/view/marginwidget.hpp
@@ -18,64 +18,55 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_SELECTABLEITEM_H
-#define PULSEVIEW_PV_SELECTABLEITEM_H
+#ifndef PULSEVIEW_PV_MARGINWIDGET_HPP
+#define PULSEVIEW_PV_MARGINWIDGET_HPP
 
-#include <list>
+#include <memory>
 
-#include <QPen>
+#include <QPoint>
 
-class QAction;
-class QMenu;
-class QWidget;
+#include "viewwidget.hpp"
 
 namespace pv {
-
-namespace widgets {
-class Popup;
-}
-
 namespace view {
 
-class SelectableItem : public QObject
+class ViewItem;
+
+class MarginWidget : public ViewWidget
 {
 	Q_OBJECT
 
-private:
-	static const int HighlightRadius;
-
 public:
-	SelectableItem();
+	MarginWidget(pv::view::View &parent);
 
-public:
 	/**
-	 * Returns true if the signal has been selected by the user.
+	 * The extended area that the margin widget would like to be sized to.
+	 * @remarks This area is the area specified by sizeHint, extended by
+	 * the area to overlap the viewport.
 	 */
-	bool selected() const;
+	virtual QSize extended_size_hint() const = 0;
 
+protected:
 	/**
-	 * Selects or deselects the signal.
+	 * Indicates the event an a view item has been clicked.
+	 * @param item the view item that has been clicked.
 	 */
-	void select(bool select = true);
+	virtual void item_clicked(
+		const std::shared_ptr<pv::view::ViewItem> &item);
 
-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();
+	/**
+	 * Shows the popup of a the specified @c ViewItem .
+	 * @param item The item to show the popup for.
+	 */
+	void show_popup(const std::shared_ptr<ViewItem> &item);
 
 protected:
-	QWidget *_context_parent;
+	virtual void contextMenuEvent(QContextMenuEvent *event);
 
-private:
-	bool _selected;
+	virtual void keyPressEvent(QKeyEvent *e);
 };
 
 } // namespace view
 } // namespace pv
 
-#endif // PULSEVIEW_PV_SELECTABLEITEM_H
+#endif // PULSEVIEW_PV_MARGINWIDGET_HPP
diff --git a/pv/view/marginwidget.cpp b/pv/view/rowitem.cpp
similarity index 82%
copy from pv/view/marginwidget.cpp
copy to pv/view/rowitem.cpp
index 539551d..4ad8b59 100644
--- a/pv/view/marginwidget.cpp
+++ b/pv/view/rowitem.cpp
@@ -1,7 +1,7 @@
 /*
  * This file is part of the PulseView project.
  *
- * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ * Copyright (C) 2015 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
@@ -18,16 +18,12 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "view.h"
-
-#include "marginwidget.h"
+#include "rowitem.hpp"
 
 namespace pv {
 namespace view {
 
-MarginWidget::MarginWidget(View &parent) :
-	QWidget(&parent),
-	_view(parent)
+void RowItem::hover_point_changed()
 {
 }
 
diff --git a/pv/view/marginwidget.h b/pv/view/rowitem.hpp
similarity index 71%
copy from pv/view/marginwidget.h
copy to pv/view/rowitem.hpp
index 42dffa7..1c48786 100644
--- a/pv/view/marginwidget.h
+++ b/pv/view/rowitem.hpp
@@ -18,36 +18,23 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_MARGINWIDGET_H
-#define PULSEVIEW_PV_MARGINWIDGET_H
+#ifndef PULSEVIEW_PV_VIEW_ROWITEM_HPP
+#define PULSEVIEW_PV_VIEW_ROWITEM_HPP
 
-#include <QWidget>
+#include "viewitem.hpp"
 
 namespace pv {
 namespace view {
 
-class View;
-
-class MarginWidget : public QWidget
+class RowItem : public ViewItem
 {
 	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;
+	virtual void hover_point_changed();
 };
 
 } // namespace view
 } // namespace pv
 
-#endif // PULSEVIEW_PV_MARGINWIDGET_H
+#endif // PULSEVIEW_PV_VIEW_ROWITEM_HPP
diff --git a/pv/view/ruler.cpp b/pv/view/ruler.cpp
index aec3de9..31ec121 100644
--- a/pv/view/ruler.cpp
+++ b/pv/view/ruler.cpp
@@ -18,258 +18,258 @@
  * 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 <QFontMetrics>
 #include <QMouseEvent>
-#include <QPainter>
-#include <QTextStream>
 
-#include <pv/widgets/popup.h>
+#include "ruler.hpp"
+#include "view.hpp"
 
 using namespace Qt;
-using boost::shared_ptr;
+
+using std::shared_ptr;
+using std::vector;
 
 namespace pv {
 namespace view {
 
-const int Ruler::RulerHeight = 30;
+const float Ruler::RulerHeight = 2.5f; // x Text Height
 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;
+const float Ruler::HoverArrowSize = 0.5f; // x Text Height
 
 Ruler::Ruler(View &parent) :
-	MarginWidget(parent),
-	_dragging(false)
+	MarginWidget(parent)
 {
 	setMouseTracking(true);
 
-	connect(&_view, SIGNAL(hover_point_changed()),
+	connect(&view_, SIGNAL(hover_point_changed()),
 		this, SLOT(hover_point_changed()));
+	connect(&view_, SIGNAL(offset_changed()),
+		this, SLOT(invalidate_tick_position_cache()));
+	connect(&view_, SIGNAL(scale_changed()),
+		this, SLOT(invalidate_tick_position_cache()));
+	connect(&view_, SIGNAL(tick_prefix_changed()),
+		this, SLOT(invalidate_tick_position_cache()));
+	connect(&view_, SIGNAL(tick_precision_changed()),
+		this, SLOT(invalidate_tick_position_cache()));
+	connect(&view_, SIGNAL(tick_period_changed()),
+		this, SLOT(invalidate_tick_position_cache()));
+	connect(&view_, SIGNAL(time_unit_changed()),
+		this, SLOT(invalidate_tick_position_cache()));
 }
 
-void Ruler::clear_selection()
+QSize Ruler::sizeHint() const
 {
-	CursorPair &cursors = _view.cursors();
-	cursors.first()->select(false);
-	cursors.second()->select(false);
-	update();
+	const int text_height = calculate_text_height();
+	return QSize(0, RulerHeight * text_height);
 }
 
-QString Ruler::format_time(double t, unsigned int prefix,
-	unsigned int precision)
+QSize Ruler::extended_size_hint() const
 {
-	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;
+	QRectF max_rect;
+	std::vector< std::shared_ptr<TimeItem> > items(view_.time_items());
+	for (auto &i : items)
+		max_rect = max_rect.united(i->label_rect(QRect()));
+	return QSize(0, sizeHint().height() - max_rect.top() / 2 +
+		ViewItem::HighlightRadius);
 }
 
-QSize Ruler::sizeHint() const
+QString Ruler::format_time_with_distance(
+	const pv::util::Timestamp& distance,
+	const pv::util::Timestamp& t,
+	pv::util::SIPrefix prefix,
+	pv::util::TimeUnit unit,
+	unsigned precision,
+	bool sign)
 {
-	return QSize(0, RulerHeight);
+	const unsigned limit = 60;
+
+	if (t.is_zero())
+		return "0";
+
+	// If we have to use samples then we have no alternative formats
+	if (unit == pv::util::TimeUnit::Samples)
+		return pv::util::format_time_si_adjusted(t, prefix, precision, "sa", sign);
+
+	// View zoomed way out -> low precision (0), big distance (>=60s)
+	// -> DD:HH:MM
+	if ((precision == 0) && (distance >= limit))
+		return pv::util::format_time_minutes(t, 0, sign);
+
+	// View in "normal" range -> medium precision, medium step size
+	// -> HH:MM:SS.mmm... or xxxx (si unit) if less than limit seconds
+	// View zoomed way in -> high precision (>3), low step size (<1s)
+	// -> HH:MM:SS.mmm... or xxxx (si unit) if less than limit seconds
+	if (abs(t) < limit)
+		return pv::util::format_time_si_adjusted(t, prefix, precision, "s", sign);
+	else
+		return pv::util::format_time_minutes(t, precision, sign);
 }
 
-void Ruler::paintEvent(QPaintEvent*)
+vector< shared_ptr<ViewItem> > Ruler::items()
 {
+	const vector< shared_ptr<TimeItem> > time_items(view_.time_items());
+	return vector< shared_ptr<ViewItem> >(
+		time_items.begin(), time_items.end());
+}
 
-	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));
-
+shared_ptr<ViewItem> Ruler::get_mouse_over_item(const QPoint &pt)
+{
+	const vector< shared_ptr<TimeItem> > items(view_.time_items());
+	for (auto i = items.rbegin(); i != items.rend(); i++)
+		if ((*i)->enabled() && (*i)->label_rect(rect()).contains(pt))
+			return *i;
+	return nullptr;
+}
 
-		typical_width = p.boundingRect(0, 0, INT_MAX, INT_MAX,
-			AlignLeft | AlignTop, format_time(_view.offset(),
-			prefix)).width() + MinValueSpacing;
+void Ruler::paintEvent(QPaintEvent*)
+{
+	if (!tick_position_cache_) {
+		auto ffunc = [this](const pv::util::Timestamp& t) {
+			return format_time_with_distance(
+				this->view_.tick_period(),
+				t,
+				this->view_.tick_prefix(),
+				this->view_.time_unit(),
+				this->view_.tick_precision());
+		};
+
+		tick_position_cache_ = calculate_tick_positions(
+			view_.tick_period(),
+			view_.offset(),
+			view_.scale(),
+			width(),
+			ffunc);
+	}
 
-		min_width += SpacingIncrement;
+	const int ValueMargin = 3;
 
-	} while(typical_width > tick_period / _view.scale());
+	const int text_height = calculate_text_height();
+	const int ruler_height = RulerHeight * text_height;
+	const int major_tick_y1 = text_height + ValueMargin * 2;
+	const int minor_tick_y1 = (major_tick_y1 + ruler_height) / 2;
 
-	const int text_height = p.boundingRect(0, 0, INT_MAX, INT_MAX,
-		AlignLeft | AlignTop, "8").height();
+	QPainter p(this);
 
 	// 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());
+	for (const auto& tick: tick_position_cache_->major) {
+		p.drawText(tick.first, ValueMargin, 0, text_height,
+				AlignCenter | AlignTop | TextDontClip, tick.second);
+		p.drawLine(QPointF(tick.first, major_tick_y1),
+			QPointF(tick.first, ruler_height));
+	}
 
-	// Draw the cursors
-	if (_view.cursors_shown())
-		_view.cursors().draw_markers(p, rect(), prefix);
+	for (const auto& tick: tick_position_cache_->minor) {
+		p.drawLine(QPointF(tick, minor_tick_y1),
+				QPointF(tick, ruler_height));
+	}
 
 	// Draw the hover mark
-	draw_hover_mark(p);
+	draw_hover_mark(p, text_height);
+
+	p.setRenderHint(QPainter::Antialiasing);
 
-	p.end();
+	// The cursor labels are not drawn with the arrows exactly on the
+	// bottom line of the widget, because then the selection shadow
+	// would be clipped away.
+	const QRect r = rect().adjusted(0, 0, 0, -ViewItem::HighlightRadius);
+
+	// Draw the items
+	const vector< shared_ptr<TimeItem> > items(view_.time_items());
+	for (auto &i : items) {
+		const bool highlight = !item_dragging_ &&
+			i->label_rect(r).contains(mouse_point_);
+		i->paint_label(p, r, highlight);
+	}
 }
 
-void Ruler::mouseMoveEvent(QMouseEvent *e)
+Ruler::TickPositions Ruler::calculate_tick_positions(
+	const pv::util::Timestamp& major_period,
+	const pv::util::Timestamp& offset,
+	const double scale,
+	const int width,
+	std::function<QString(const pv::util::Timestamp&)> format_function)
 {
-	if (!(e->buttons() & Qt::LeftButton))
-		return;
-	
-	if ((e->pos() - _mouse_down_point).manhattanLength() <
-		QApplication::startDragDistance())
-		return;
+	TickPositions tp;
 
-	_dragging = true;
+	const pv::util::Timestamp minor_period = major_period / MinorTickSubdivision;
+	const pv::util::Timestamp first_major_division = floor(offset / major_period);
+	const pv::util::Timestamp first_minor_division = ceil(offset / minor_period);
+	const pv::util::Timestamp t0 = first_major_division * major_period;
 
-	if (shared_ptr<TimeMarker> m = _grabbed_marker.lock())
-		m->set_time(_view.offset() +
-			((double)e->x() + 0.5) * _view.scale());
-}
+	int division = (round(first_minor_division -
+		first_major_division * MinorTickSubdivision)).convert_to<int>() - 1;
 
-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();
+	double x;
+
+	do {
+		pv::util::Timestamp t = t0 + division * minor_period;
+		x = ((t - offset) / scale).convert_to<double>();
+
+		if (division % MinorTickSubdivision == 0) {
+			// Recalculate 't' without using 'minor_period' which is a fraction
+			t = t0 + division / MinorTickSubdivision * major_period;
+			tp.major.emplace_back(x, format_function(t));
+		} else {
+			tp.minor.emplace_back(x);
 		}
 
-		if (shared_ptr<TimeMarker> m = _grabbed_marker.lock())
-			m->select();
+		division++;
+	} while (x < width);
 
-		selection_changed();
-	}
+	return tp;
 }
 
-void Ruler::mouseReleaseEvent(QMouseEvent *)
+void Ruler::mouseDoubleClickEvent(QMouseEvent *e)
 {
-	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();
+	view_.add_flag(view_.offset() + ((double)e->x() + 0.5) * view_.scale());
 }
 
-void Ruler::draw_hover_mark(QPainter &p)
+void Ruler::draw_hover_mark(QPainter &p, int text_height)
 {
-	const int x = _view.hover_point().x();
+	const int x = view_.hover_point().x();
 
-	if (x == -1 || _dragging)
+	if (x == -1)
 		return;
 
 	p.setPen(QPen(Qt::NoPen));
 	p.setBrush(QBrush(palette().color(foregroundRole())));
 
-	const int b = height() - 1;
+	const int b = RulerHeight * text_height;
+	const float hover_arrow_size = HoverArrowSize * text_height;
 	const QPointF points[] = {
 		QPointF(x, b),
-		QPointF(x - HoverArrowSize, b - HoverArrowSize),
-		QPointF(x + HoverArrowSize, b - HoverArrowSize)
+		QPointF(x - hover_arrow_size, b - hover_arrow_size),
+		QPointF(x + hover_arrow_size, b - hover_arrow_size)
 	};
 	p.drawPolygon(points, countof(points));
 }
 
+int Ruler::calculate_text_height() const
+{
+	return QFontMetrics(font()).ascent();
+}
+
 void Ruler::hover_point_changed()
 {
 	update();
 }
 
+void Ruler::invalidate_tick_position_cache()
+{
+	tick_position_cache_ = boost::none;
+}
+
+void Ruler::resizeEvent(QResizeEvent*)
+{
+	// the tick calculation depends on the width of this widget
+	invalidate_tick_position_cache();
+}
+
 } // namespace view
 } // namespace pv
diff --git a/pv/view/ruler.h b/pv/view/ruler.h
deleted file mode 100644
index dc4e7bb..0000000
--- a/pv/view/ruler.h
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * 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/ruler.hpp b/pv/view/ruler.hpp
new file mode 100644
index 0000000..cf63eaf
--- /dev/null
+++ b/pv/view/ruler.hpp
@@ -0,0 +1,180 @@
+/*
+ * 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_HPP
+#define PULSEVIEW_PV_VIEW_RULER_HPP
+
+#include <functional>
+#include <memory>
+
+#include <boost/optional.hpp>
+
+#include "marginwidget.hpp"
+#include <pv/util.hpp>
+
+namespace RulerTest {
+class tick_position_test_0;
+class tick_position_test_1;
+class tick_position_test_2;
+}
+
+namespace pv {
+namespace view {
+
+class TimeItem;
+class ViewItem;
+
+class Ruler : public MarginWidget
+{
+	Q_OBJECT
+
+	friend class RulerTest::tick_position_test_0;
+	friend class RulerTest::tick_position_test_1;
+	friend class RulerTest::tick_position_test_2;
+
+private:
+
+	/// Height of the ruler in multipes of the text height
+	static const float RulerHeight;
+
+	static const int MinorTickSubdivision;
+
+	/// Height of the hover arrow in multiples of the text height
+	static const float HoverArrowSize;
+
+public:
+	Ruler(View &parent);
+
+public:
+	QSize sizeHint() const;
+
+	/**
+	 * The extended area that the header widget would like to be sized to.
+	 * @remarks This area is the area specified by sizeHint, extended by
+	 * the area to overlap the viewport.
+	 */
+	QSize extended_size_hint() const;
+
+	/**
+	 * Formats a timestamp depending on its distance to another timestamp.
+	 *
+	 * Heuristic function, useful when multiple timestamps should be put side by
+	 * side. The function procedes in the following order:
+	 *   - If 't' is zero, "0" is returned.
+	 *   - If 'unit' is 'TimeUnit::Samples', 'pv::util::format_time_si_adjusted()'
+	 *     is used to format 't'.
+	 *   - If a zoomed out view is detected (determined by 'precision' and
+	 *     'distance'), 'pv::util::format_time_minutes() is used.
+	 *   - For timestamps "near the origin" (determined by 'distance'),
+	 *    'pv::util::format_time_si_adjusted()' is used.
+	 *   - If none of the previous was true, 'pv::util::format_time_minutes()'
+	 *     is used again.
+	 *
+	 * @param distance The distance between the timestamp to format and
+	 *        an adjacent one.
+	 * @param t The value to format
+	 * @param prefix The SI prefix to use.
+	 * @param unit The representation of the timestamp value.
+	 * @param precision The number of digits after the decimal separator.
+	 * @param sign Whether or not to add a sign also for positive numbers.
+	 *
+	 * @return The formated value.
+	 */
+	static QString format_time_with_distance(
+		const pv::util::Timestamp& distance,
+		const pv::util::Timestamp& t,
+		pv::util::SIPrefix prefix = pv::util::SIPrefix::unspecified,
+		pv::util::TimeUnit unit = pv::util::TimeUnit::Time,
+		unsigned precision = 0,
+		bool sign = true);
+
+private:
+	/**
+	 * Gets the time items.
+	 */
+	std::vector< std::shared_ptr<pv::view::ViewItem> > items();
+
+	/**
+	 * Gets the first view item which has a label that contains @c pt .
+	 * @param pt the point to search with.
+	 * @return the view item that has been found, or and empty
+	 *   @c shared_ptr if no item was found.
+	 */
+	std::shared_ptr<pv::view::ViewItem> get_mouse_over_item(
+		const QPoint &pt);
+
+	void paintEvent(QPaintEvent *event);
+
+	void mouseDoubleClickEvent(QMouseEvent *e);
+
+	/**
+	 * Draw a hover arrow under the cursor position.
+	 * @param p The painter to draw into.
+	 * @param text_height The height of a single text ascent.
+	 */
+	void draw_hover_mark(QPainter &p, int text_height);
+
+	int calculate_text_height() const;
+
+	struct TickPositions
+	{
+		std::vector<std::pair<double, QString>> major;
+		std::vector<double> minor;
+	};
+
+	/**
+	 * Holds the tick positions so that they don't have to be recalculated on
+	 * every redraw. Set by 'paintEvent()' when needed.
+	 */
+	boost::optional<TickPositions> tick_position_cache_;
+
+	/**
+	 * Calculates the major and minor tick positions.
+	 *
+	 * @param major_period The period between the major ticks.
+	 * @param offset The time at the left border of the ruler.
+	 * @param scale The scale in seconds per pixel.
+	 * @param width the Width of the ruler.
+	 * @param format_function A function used to format the major tick times.
+	 * @return An object of type 'TickPositions' that contains the major tick
+	 *         positions together with the labels at that ticks, and the minor
+	 *         tick positions.
+	 */
+	static TickPositions calculate_tick_positions(
+		const pv::util::Timestamp& major_period,
+		const pv::util::Timestamp& offset,
+		const double scale,
+		const int width,
+		std::function<QString(const pv::util::Timestamp&)> format_function);
+
+protected:
+	void resizeEvent(QResizeEvent*) override;
+
+private Q_SLOTS:
+	void hover_point_changed();
+
+	// Resets the 'tick_position_cache_'.
+	void invalidate_tick_position_cache();
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_RULER_HPP
diff --git a/pv/view/selectableitem.cpp b/pv/view/selectableitem.cpp
deleted file mode 100644
index 3f4f6da..0000000
--- a/pv/view/selectableitem.cpp
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * 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/signal.cpp b/pv/view/signal.cpp
index 3312c59..67f8dde 100644
--- a/pv/view/signal.cpp
+++ b/pv/view/signal.cpp
@@ -21,32 +21,35 @@
 #include <extdef.h>
 
 #include <assert.h>
-#include <math.h>
+#include <cmath>
 
 #include <QApplication>
 #include <QFormLayout>
+#include <QKeyEvent>
+#include <QLineEdit>
 #include <QMenu>
 
-#include <libsigrok/libsigrok.h>
+#include <libsigrokcxx/libsigrokcxx.hpp>
 
-#include "signal.h"
-#include "view.h"
+#include "signal.hpp"
+#include "view.hpp"
 
-#include <pv/device/devinst.h>
+using std::shared_ptr;
+using std::make_shared;
 
-using boost::shared_ptr;
+using sigrok::Channel;
 
 namespace pv {
 namespace view {
 
-const char *const ProbeNames[] = {
+const char *const ChannelNames[] = {
 	"CLK",
 	"DATA",
 	"IN",
 	"OUT",
 	"RST",
-	"Tx",
-	"Rx",
+	"TX",
+	"RX",
 	"EN",
 	"SCLK",
 	"MOSI",
@@ -56,54 +59,81 @@ const char *const ProbeNames[] = {
 	"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)
+Signal::Signal(pv::Session &session,
+	std::shared_ptr<sigrok::Channel> channel) :
+	Trace(QString::fromUtf8(channel->name().c_str())),
+	session_(session),
+	channel_(channel),
+	scale_handle_(make_shared<SignalScaleHandle>(*this)),
+	items_({scale_handle_}),
+	name_widget_(nullptr)
 {
-	assert(_probe);
+	assert(channel_);
 }
 
 void Signal::set_name(QString name)
 {
 	Trace::set_name(name);
-	_updating_name_widget = true;
-	_name_widget->setEditText(name);
-	_updating_name_widget = false;
+
+	if (name != name_widget_->currentText())
+		name_widget_->setEditText(name);
+
+	// Store the channel name in sigrok::Channel so that it
+	// will end up in the .sr file upon save.
+	channel_->set_name(name.toUtf8().constData());
 }
 
 bool Signal::enabled() const
 {
-	return _probe->enabled;
+	return channel_->enabled();
 }
 
 void Signal::enable(bool enable)
 {
-	_dev_inst->enable_probe(_probe, enable);
-	visibility_changed();
+	channel_->set_enabled(enable);
+
+	if (owner_)
+		owner_->extents_changed(true, true);
+}
+
+shared_ptr<Channel> Signal::channel() const
+{
+	return channel_;
 }
 
-const sr_channel* Signal::probe() const
+const ViewItemOwner::item_list& Signal::child_items() const
 {
-	return _probe;
+	return items_;
+}
+
+void Signal::paint_back(QPainter &p, const ViewItemPaintParams &pp)
+{
+	if (channel_->enabled())
+		Trace::paint_back(p, pp);
 }
 
 void Signal::populate_popup_form(QWidget *parent, QFormLayout *form)
 {
-	_name_widget = new QComboBox(parent);
-	_name_widget->setEditable(true);
+	name_widget_ = new QComboBox(parent);
+	name_widget_->setEditable(true);
+	name_widget_->setCompleter(0);
+
+	for (unsigned int i = 0; i < countof(ChannelNames); i++)
+		name_widget_->insertItem(i, ChannelNames[i]);
+
+	const int index = name_widget_->findText(name_, Qt::MatchExactly);
 
-	for(unsigned int i = 0; i < countof(ProbeNames); i++)
-		_name_widget->insertItem(i, ProbeNames[i]);
-	_name_widget->setEditText(_probe->name);
+	if (index == -1) {
+		name_widget_->insertItem(0, name_);
+		name_widget_->setCurrentIndex(0);
+	} else {
+		name_widget_->setCurrentIndex(index);
+	}
 
-	connect(_name_widget, SIGNAL(editTextChanged(const QString&)),
+	connect(name_widget_, SIGNAL(editTextChanged(const QString&)),
 		this, SLOT(on_text_changed(const QString&)));
 
-	form->addRow(tr("Name"), _name_widget);
+	form->addRow(tr("Name"), name_widget_);
 
 	add_colour_option(parent, form);
 }
diff --git a/pv/view/signal.h b/pv/view/signal.hpp
similarity index 53%
rename from pv/view/signal.h
rename to pv/view/signal.hpp
index 32d1817..517bbf4 100644
--- a/pv/view/signal.h
+++ b/pv/view/signal.hpp
@@ -18,39 +18,41 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_VIEW_SIGNAL_H
-#define PULSEVIEW_PV_VIEW_SIGNAL_H
+#ifndef PULSEVIEW_PV_VIEW_SIGNAL_HPP
+#define PULSEVIEW_PV_VIEW_SIGNAL_HPP
 
-#include <boost/shared_ptr.hpp>
+#include <memory>
 
 #include <QComboBox>
 #include <QWidgetAction>
 
 #include <stdint.h>
 
-#include "trace.h"
+#include "signalscalehandle.hpp"
+#include "trace.hpp"
+#include "viewitemowner.hpp"
 
-struct sr_channel;
+namespace sigrok {
+	class Channel;
+}
 
 namespace pv {
 
+class Session;
+
 namespace data {
 class SignalData;
 }
 
-namespace device {
-class DevInst;
-}
-
 namespace view {
 
-class Signal : public Trace
+class Signal : public Trace, public ViewItemOwner
 {
 	Q_OBJECT
 
 protected:
-	Signal(boost::shared_ptr<pv::device::DevInst> dev_inst,
-		const sr_channel *const probe);
+	Signal(pv::Session &session,
+		std::shared_ptr<sigrok::Channel> channel);
 
 public:
 	/**
@@ -58,7 +60,7 @@ public:
 	 */
 	void set_name(QString name);
 
-	virtual boost::shared_ptr<pv::data::SignalData> data() const = 0;
+	virtual std::shared_ptr<pv::data::SignalData> data() const = 0;
 
 	/**
 	 * Returns true if the trace is visible and enabled.
@@ -67,7 +69,14 @@ public:
 
 	void enable(bool enable = true);
 
-	const sr_channel* probe() const;
+	std::shared_ptr<sigrok::Channel> channel() const;
+
+	/**
+	 * Returns a list of row items owned by this object.
+	 */
+	const item_list& child_items() const;
+
+	void paint_back(QPainter &p, const ViewItemPaintParams &pp);
 
 	virtual void populate_popup_form(QWidget *parent, QFormLayout *form);
 
@@ -75,18 +84,36 @@ public:
 
 	void delete_pressed();
 
-private slots:
+	/**
+	 * Returns the offset to show the drag handle.
+	 */
+	virtual int scale_handle_offset() const = 0;
+
+	/**
+	 * Handles the scale handle being dragged to an offset.
+	 * @param offset the offset the scale handle was dragged to.
+	 */
+	virtual void scale_handle_dragged(int offset) = 0;
+
+	/**
+	 * Handles the scale handle being being released.
+	 */
+	virtual void scale_handle_released() {};
+
+private Q_SLOTS:
 	void on_disable();
 
 protected:
-	boost::shared_ptr<pv::device::DevInst> _dev_inst;
-	const sr_channel *const _probe;
+	pv::Session &session_;
+	std::shared_ptr<sigrok::Channel> channel_;
+
+	const std::shared_ptr<SignalScaleHandle> scale_handle_;
+	const item_list items_;
 
-	QComboBox *_name_widget;
-	bool _updating_name_widget;
+	QComboBox *name_widget_;
 };
 
 } // namespace view
 } // namespace pv
 
-#endif // PULSEVIEW_PV_VIEW_SIGNAL_H
+#endif // PULSEVIEW_PV_VIEW_SIGNAL_HPP
diff --git a/pv/view/signalscalehandle.cpp b/pv/view/signalscalehandle.cpp
new file mode 100644
index 0000000..a4c2b41
--- /dev/null
+++ b/pv/view/signalscalehandle.cpp
@@ -0,0 +1,107 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2015 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 <QRadialGradient>
+
+#include "signal.hpp"
+#include "signalscalehandle.hpp"
+#include "tracetreeitemowner.hpp"
+
+using std::max;
+using std::min;
+
+namespace pv {
+namespace view {
+
+SignalScaleHandle::SignalScaleHandle(Signal &owner) :
+	owner_(owner)
+{
+}
+
+bool SignalScaleHandle::enabled() const
+{
+	return selected() || owner_.selected();
+}
+
+void SignalScaleHandle::select(bool select)
+{
+	ViewItem::select(select);
+	owner_.owner()->row_item_appearance_changed(true, true);
+}
+
+void SignalScaleHandle::drag_release()
+{
+	RowItem::drag_release();
+	owner_.scale_handle_released();
+	owner_.owner()->row_item_appearance_changed(true, true);
+}
+
+void SignalScaleHandle::drag_by(const QPoint &delta)
+{
+	owner_.scale_handle_dragged(
+		drag_point_.y() + delta.y() - owner_.get_visual_y());
+	owner_.owner()->row_item_appearance_changed(true, true);
+}
+
+QPoint SignalScaleHandle::point(const QRect &rect) const
+{
+	return owner_.point(rect) + QPoint(0, owner_.scale_handle_offset());
+}
+
+QRectF SignalScaleHandle::hit_box_rect(const ViewItemPaintParams &pp) const
+{
+	const int text_height = ViewItemPaintParams::text_height();
+	const double x = -pp.pixels_offset() - text_height / 2;
+	const double min_x = pp.left() + text_height;
+	const double max_x = pp.right() - text_height * 2;
+	return QRectF(min(max(x, min_x), max_x),
+		owner_.get_visual_y() + owner_.scale_handle_offset() -
+			text_height / 2,
+		text_height, text_height);
+}
+
+void SignalScaleHandle::paint_fore(QPainter &p, const ViewItemPaintParams &pp)
+{
+	if (!enabled())
+		return;
+
+	const QRectF r(hit_box_rect(pp));
+	const QPointF c = (r.topLeft() + 2 * r.center()) / 3;
+	QRadialGradient gradient(c, r.width(), c);
+
+	if (selected()) {
+		gradient.setColorAt(0.0, QColor(255, 255, 255));
+		gradient.setColorAt(0.75, QColor(192, 192, 192));
+		gradient.setColorAt(1.0, QColor(128, 128, 128));
+	} else {
+		gradient.setColorAt(0.0, QColor(192, 192, 192));
+		gradient.setColorAt(0.75, QColor(128, 128, 128));
+		gradient.setColorAt(1.0, QColor(128, 128, 128));
+	}
+
+	p.setBrush(QBrush(gradient));
+	p.setPen(QColor(128, 128, 128));
+	p.drawEllipse(r);
+}
+
+} // view
+} // pv
diff --git a/pv/view/signalscalehandle.hpp b/pv/view/signalscalehandle.hpp
new file mode 100644
index 0000000..9d1ae93
--- /dev/null
+++ b/pv/view/signalscalehandle.hpp
@@ -0,0 +1,93 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2015 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_SIGNALSCALEHANDLE_HPP
+#define PULSEVIEW_PV_VIEW_SIGNALSCALEHANDLE_HPP
+
+#include "rowitem.hpp"
+
+namespace pv {
+namespace view {
+
+class Signal;
+
+/**
+ * A row item owned by a @c Signal that implements the v-scale adjustment grab
+ * handle.
+ */
+class SignalScaleHandle : public RowItem
+{
+	Q_OBJECT
+public:
+	/**
+	 * Constructor
+	 */
+	explicit SignalScaleHandle(Signal &owner);
+
+public:
+	/**
+	 * Returns true if the parent item is enabled.
+	 */
+	bool enabled() const;
+
+	/**
+	 * Selects or deselects the signal.
+	 */
+	void select(bool select = true);
+
+	/**
+	 * Sets this item into the un-dragged state.
+	 */
+	void drag_release();
+
+	/**
+	 * Drags the item to a delta relative to the drag point.
+	 * @param delta the offset from the drag point.
+	 */
+	void drag_by(const QPoint &delta);
+
+	/**
+	 * Get the drag point.
+	 * @param rect the rectangle of the widget area.
+	 */
+	QPoint point(const QRect &rect) const;
+
+	/**
+	 * Computes the outline rectangle of the viewport hit-box.
+	 * @param rect the rectangle of the viewport area.
+	 * @return Returns the rectangle of the hit-box.
+	 */
+	QRectF hit_box_rect(const ViewItemPaintParams &pp) const;
+
+	/**
+	 * Paints the foreground layer of the item with a QPainter
+	 * @param p the QPainter to paint into.
+	 * @param pp the painting parameters object to paint with.
+	 */
+	void paint_fore(QPainter &p, const ViewItemPaintParams &pp);
+
+private:
+	Signal &owner_;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_SIGNALSCALEHANDLE_HPP
diff --git a/pv/view/marginwidget.cpp b/pv/view/timeitem.cpp
similarity index 74%
copy from pv/view/marginwidget.cpp
copy to pv/view/timeitem.cpp
index 539551d..ec8dd1a 100644
--- a/pv/view/marginwidget.cpp
+++ b/pv/view/timeitem.cpp
@@ -1,7 +1,7 @@
 /*
  * This file is part of the PulseView project.
  *
- * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ * 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
@@ -18,17 +18,20 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "view.h"
-
-#include "marginwidget.h"
+#include "timeitem.hpp"
+#include "view.hpp"
 
 namespace pv {
 namespace view {
 
-MarginWidget::MarginWidget(View &parent) :
-	QWidget(&parent),
-	_view(parent)
+TimeItem::TimeItem(View &view) :
+	view_(view) {
+}
+
+void TimeItem::drag_by(const QPoint &delta)
 {
+	set_time(view_.offset() + (drag_point_.x() + delta.x() - 0.5) *
+		view_.scale());
 }
 
 } // namespace view
diff --git a/pv/view/marginwidget.h b/pv/view/timeitem.hpp
similarity index 57%
rename from pv/view/marginwidget.h
rename to pv/view/timeitem.hpp
index 42dffa7..cd6f5f2 100644
--- a/pv/view/marginwidget.h
+++ b/pv/view/timeitem.hpp
@@ -1,7 +1,7 @@
 /*
  * This file is part of the PulseView project.
  *
- * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ * 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
@@ -18,36 +18,47 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_MARGINWIDGET_H
-#define PULSEVIEW_PV_MARGINWIDGET_H
+#ifndef PULSEVIEW_PV_VIEW_TIMEITEM_HPP
+#define PULSEVIEW_PV_VIEW_TIMEITEM_HPP
 
-#include <QWidget>
+#include "viewitem.hpp"
 
 namespace pv {
 namespace view {
 
 class View;
 
-class MarginWidget : public QWidget
+class TimeItem : public ViewItem
+
 {
 	Q_OBJECT
 
-public:
-	MarginWidget(pv::view::View &parent);
+protected:
+	/**
+	 * Constructor.
+	 * @param view A reference to the view that owns this marker.
+	 */
+	TimeItem(View &view);
 
-public slots:
-	virtual void clear_selection() = 0;
+public:
+	/**
+	 * Sets the time of the marker.
+	 */
+	virtual void set_time(const pv::util::Timestamp& time) = 0;
 
-signals:
-	void selection_changed();
+	virtual float get_x() const = 0;
 
-	void geometry_updated();
+	/**
+	 * Drags the item to a delta relative to the drag point.
+	 * @param delta the offset from the drag point.
+	 */
+	void drag_by(const QPoint &delta);
 
 protected:
-	pv::view::View &_view;
+	View &view_;
 };
 
 } // namespace view
 } // namespace pv
 
-#endif // PULSEVIEW_PV_MARGINWIDGET_H
+#endif // PULSEVIEW_PV_VIEW_TIMEITEM_HPP
diff --git a/pv/view/timemarker.cpp b/pv/view/timemarker.cpp
index a5d280b..b5662e4 100644
--- a/pv/view/timemarker.cpp
+++ b/pv/view/timemarker.cpp
@@ -18,56 +18,149 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "timemarker.h"
+#include <algorithm>
 
-#include "view.h"
+#include <extdef.h>
 
+#include "timemarker.hpp"
+
+#include "view.hpp"
+#include "pv/widgets/timestampspinbox.hpp"
+
+#include <QApplication>
 #include <QFormLayout>
+#include <QFontMetrics>
 #include <QPainter>
 
-#include <pv/widgets/popup.h>
+#include <pv/widgets/popup.hpp>
+
+using std::max;
+using std::min;
 
 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)
+const int TimeMarker::ArrowSize = 4;
+
+TimeMarker::TimeMarker(
+	View &view, const QColor &colour, const pv::util::Timestamp& time) :
+	TimeItem(view),
+	colour_(colour),
+	time_(time),
+	value_action_(nullptr),
+	value_widget_(nullptr),
+	updating_value_widget_(false)
+{
+}
+
+const pv::util::Timestamp& TimeMarker::time() const
 {
+	return time_;
 }
 
-double TimeMarker::time() const
+void TimeMarker::set_time(const pv::util::Timestamp& time)
 {
-	return _time;
+	time_ = time;
+
+	if (value_widget_) {
+		updating_value_widget_ = true;
+		value_widget_->setValue(time);
+		updating_value_widget_ = false;
+	}
+
+	view_.time_item_appearance_changed(true, true);
 }
 
 float TimeMarker::get_x() const
 {
-	return (_time - _view.offset()) / _view.scale();
+	return ((time_ - view_.offset()) / view_.scale()).convert_to<float>();
 }
 
-void TimeMarker::set_time(double time)
+QPoint TimeMarker::point(const QRect &rect) const
 {
-	_time = time;
+	return QPoint(get_x(), rect.bottom());
+}
+
+QRectF TimeMarker::label_rect(const QRectF &rect) const
+{
+	QFontMetrics m(QApplication::font());
+	const QSizeF text_size(
+		max(m.boundingRect(get_text()).size().width(), ArrowSize),
+		m.height());
+	const QSizeF label_size(text_size + LabelPadding * 2);
+	const float top = rect.height() - label_size.height() -
+		TimeMarker::ArrowSize - 0.5f;
+	const float x = get_x();
 
-	if (_value_widget) {
-		_updating_value_widget = true;
-		_value_widget->setValue(time);
-		_updating_value_widget = false;
+	return QRectF(QPointF(x - label_size.width() / 2, top), label_size);
+}
+
+QRectF TimeMarker::hit_box_rect(const ViewItemPaintParams &pp) const
+{
+	const float x = get_x();
+	const float h = QFontMetrics(QApplication::font()).height();
+	return QRectF(x - h / 2.0f, pp.top(), h, pp.height());
+}
+
+void TimeMarker::paint_label(QPainter &p, const QRect &rect, bool hover)
+{
+	if (!enabled())
+		return;
+
+	const qreal x = ((time_ - view_.offset()) / view_.scale()).convert_to<qreal>();
+	const QRectF r(label_rect(rect));
+
+	const QPointF points[] = {
+		r.topLeft(),
+		r.bottomLeft(),
+		QPointF(max(r.left(), x - ArrowSize), r.bottom()),
+		QPointF(x, rect.bottom()),
+		QPointF(min(r.right(), x + ArrowSize), r.bottom()),
+		r.bottomRight(),
+		r.topRight()
+	};
+
+	const QPointF highlight_points[] = {
+		QPointF(r.left() + 1, r.top() + 1),
+		QPointF(r.left() + 1, r.bottom() - 1),
+		QPointF(max(r.left() + 1, x - ArrowSize), r.bottom() - 1),
+		QPointF(min(max(r.left() + 1, x), r.right() - 1),
+			rect.bottom() - 1),
+		QPointF(min(r.right() - 1, x + ArrowSize), r.bottom() - 1),
+		QPointF(r.right() - 1, r.bottom() - 1),
+		QPointF(r.right() - 1, r.top() + 1),
+	};
+
+	if (selected()) {
+		p.setPen(highlight_pen());
+		p.setBrush(Qt::transparent);
+		p.drawPolygon(points, countof(points));
 	}
 
-	time_changed();
+	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));
+
+	p.setPen(select_text_colour(colour_));
+	p.drawText(r, Qt::AlignCenter | Qt::AlignVCenter, get_text());
 }
 
-void TimeMarker::paint(QPainter &p, const QRect &rect)
+void TimeMarker::paint_fore(QPainter &p, const ViewItemPaintParams &pp)
 {
+	if (!enabled())
+		return;
+
 	const float x = get_x();
-	p.setPen(_colour);
-	p.drawLine(QPointF(x, rect.top()), QPointF(x, rect.bottom()));
+	p.setPen(colour_.darker());
+	p.drawLine(QPointF(x, pp.top()), QPointF(x, pp.bottom()));
 }
 
 pv::widgets::Popup* TimeMarker::create_popup(QWidget *parent)
@@ -75,29 +168,27 @@ pv::widgets::Popup* TimeMarker::create_popup(QWidget *parent)
 	using pv::widgets::Popup;
 
 	Popup *const popup = new Popup(parent);
+	popup->set_position(parent->mapToGlobal(
+		point(parent->rect())), Popup::Bottom);
+
 	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);
+	value_widget_ = new pv::widgets::TimestampSpinBox(parent);
+	value_widget_->setValue(time_);
 
-	connect(_value_widget, SIGNAL(valueChanged(double)),
-		this, SLOT(on_value_changed(double)));
+	connect(value_widget_, SIGNAL(valueChanged(const pv::util::Timestamp&)),
+		this, SLOT(on_value_changed(const pv::util::Timestamp&)));
 
-	form->addRow(tr("Time"), _value_widget);
+	form->addRow(tr("Time"), value_widget_);
 
 	return popup;
 }
 
-void TimeMarker::on_value_changed(double value)
+void TimeMarker::on_value_changed(const pv::util::Timestamp& value)
 {
-	if (!_updating_value_widget) {
-		_time = value;
-		time_changed();
-	}
+	if (!updating_value_widget_)
+		set_time(value);
 }
 
 } // namespace view
diff --git a/pv/view/timemarker.h b/pv/view/timemarker.h
deleted file mode 100644
index fc058c2..0000000
--- a/pv/view/timemarker.h
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * 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/timemarker.hpp b/pv/view/timemarker.hpp
new file mode 100644
index 0000000..f16fea0
--- /dev/null
+++ b/pv/view/timemarker.hpp
@@ -0,0 +1,133 @@
+/*
+ * 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_HPP
+#define PULSEVIEW_PV_VIEW_MARKER_HPP
+
+#include <QColor>
+#include <QDoubleSpinBox>
+#include <QObject>
+#include <QRectF>
+#include <QWidgetAction>
+
+#include "timeitem.hpp"
+
+class QPainter;
+class QRect;
+
+namespace pv {
+namespace widgets {
+	class TimestampSpinBox;
+}
+
+namespace view {
+
+class View;
+
+class TimeMarker : public TimeItem
+{
+	Q_OBJECT
+
+public:
+	static const int ArrowSize;
+
+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, const pv::util::Timestamp& time);
+
+public:
+	/**
+	 * Gets the time of the marker.
+	 */
+	const pv::util::Timestamp& time() const;
+
+	/**
+	 * Sets the time of the marker.
+	 */
+	void set_time(const pv::util::Timestamp& time) override;
+
+	float get_x() const;
+
+	/**
+	 * Gets the arrow-tip point of the time marker.
+	 * @param rect the rectangle of the ruler area.
+	 */
+	QPoint point(const QRect &rect) const;
+
+	/**
+	 * Computes the outline rectangle of a label.
+	 * @param rect the rectangle of the header area.
+	 * @return Returns the rectangle of the signal label.
+	 */
+	QRectF label_rect(const QRectF &rect) const;
+
+	/**
+	 * Computes the outline rectangle of the viewport hit-box.
+	 * @param rect the rectangle of the viewport area.
+	 * @return Returns the rectangle of the hit-box.
+	 */
+	QRectF hit_box_rect(const ViewItemPaintParams &pp) const;
+
+	/**
+	 * Gets the text to show in the marker.
+	 */
+	virtual QString get_text() 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 hover true if the label is being hovered over by the mouse.
+	 */
+	void paint_label(QPainter &p, const QRect &rect, bool hover);
+
+	/**
+	 * Paints the foreground layer of the item with a QPainter
+	 * @param p the QPainter to paint into.
+	 * @param pp the painting parameters object to paint with.
+	 */
+	void paint_fore(QPainter &p, const ViewItemPaintParams &pp);
+
+	virtual pv::widgets::Popup* create_popup(QWidget *parent);
+
+private Q_SLOTS:
+	void on_value_changed(const pv::util::Timestamp& value);
+
+protected:
+	const QColor &colour_;
+
+	pv::util::Timestamp time_;
+
+	QSizeF text_size_;
+
+	QWidgetAction *value_action_;
+	pv::widgets::TimestampSpinBox *value_widget_;
+	bool updating_value_widget_;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_MARKER_HPP
diff --git a/pv/view/trace.cpp b/pv/view/trace.cpp
index d10448f..264541c 100644
--- a/pv/view/trace.cpp
+++ b/pv/view/trace.cpp
@@ -21,18 +21,19 @@
 #include <extdef.h>
 
 #include <assert.h>
-#include <math.h>
+#include <cmath>
 
 #include <QApplication>
 #include <QFormLayout>
+#include <QKeyEvent>
 #include <QLineEdit>
 
-#include "trace.h"
-#include "tracepalette.h"
-#include "view.h"
+#include "trace.hpp"
+#include "tracepalette.hpp"
+#include "view.hpp"
 
-#include <pv/widgets/colourbutton.h>
-#include <pv/widgets/popup.h>
+#include <pv/widgets/colourbutton.hpp>
+#include <pv/widgets/popup.hpp>
 
 namespace pv {
 namespace view {
@@ -40,100 +41,71 @@ namespace view {
 const QPen Trace::AxisPen(QColor(128, 128, 128, 64));
 const int Trace::LabelHitPadding = 2;
 
+const QColor Trace::DarkBGColour(235, 235, 235);    // Quite light grey
+const QColor Trace::BrightBGColour(245, 245, 245);  // Very light grey
+
 Trace::Trace(QString name) :
-	_name(name),
-	_v_offset(0),
-	_popup(NULL),
-	_popup_form(NULL)
+	name_(name),
+	coloured_bg_(true), // Default setting is set in MainWindow::setup_ui()
+	popup_(nullptr),
+	popup_form_(nullptr)
 {
 }
 
-QString Trace::get_name() const
+QString Trace::name() const
 {
-	return _name;
+	return name_;
 }
 
 void Trace::set_name(QString name)
 {
-	_name = name;
+	name_ = name;
 }
 
-QColor Trace::get_colour() const
+QColor Trace::colour() const
 {
-	return _colour;
+	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;
-}
+	colour_ = colour;
 
-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;
+	bgcolour_ = colour;
+	bgcolour_.setAlpha(20);
 }
 
-void Trace::paint_fore(QPainter &p, int left, int right)
+void Trace::set_coloured_bg(bool state)
 {
-	(void)p;
-	(void)left;
-	(void)right;
+	coloured_bg_ = state;
 }
 
-void Trace::paint_label(QPainter &p, int right, bool hover)
+void Trace::paint_label(QPainter &p, const QRect &rect, bool hover)
 {
-	assert(_view);
-	const int y = _v_offset - _view->v_offset();
+	const int y = get_visual_y();
 
-	p.setBrush(_colour);
+	p.setBrush(colour_);
 
 	if (!enabled())
 		return;
 
-	const QColor colour = get_colour();
-
-	const QRectF label_rect = get_label_rect(right);
+	const QRectF r = label_rect(rect);
 
 	// Paint the label
+	const float label_arrow_length = r.height() / 2;
 	const QPointF points[] = {
-		label_rect.topLeft(),
-		label_rect.topRight(),
-		QPointF(right, y),
-		label_rect.bottomRight(),
-		label_rect.bottomLeft()
+		r.topLeft(),
+		QPointF(r.right() - label_arrow_length, r.top()),
+		QPointF(r.right(), y),
+		QPointF(r.right() - label_arrow_length, r.bottom()),
+		r.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)
+		QPointF(r.left() + 1, r.top() + 1),
+		QPointF(r.right() - label_arrow_length, r.top() + 1),
+		QPointF(r.right() - 1, y),
+		QPointF(r.right() - label_arrow_length, r.bottom() - 1),
+		QPointF(r.left() + 1, r.bottom() - 1)
 	};
 
 	if (selected()) {
@@ -143,38 +115,28 @@ void Trace::paint_label(QPainter &p, int right, bool hover)
 	}
 
 	p.setPen(Qt::transparent);
-	p.setBrush(hover ? colour.lighter() : colour);
+	p.setBrush(hover ? colour_.lighter() : colour_);
 	p.drawPolygon(points, countof(points));
 
-	p.setPen(colour.lighter());
+	p.setPen(colour_.lighter());
 	p.setBrush(Qt::transparent);
 	p.drawPolygon(highlight_points, countof(highlight_points));
 
-	p.setPen(colour.darker());
+	p.setPen(colour_.darker());
 	p.setBrush(Qt::transparent);
 	p.drawPolygon(points, countof(points));
 
 	// Paint the text
-	p.setPen(get_text_colour());
+	p.setPen(select_text_colour(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);
+	p.drawText(QRectF(r.x(), r.y(),
+		r.width() - label_arrow_length, r.height()),
+		Qt::AlignCenter | Qt::AlignVCenter, name_);
 }
 
 QMenu* Trace::create_context_menu(QWidget *parent)
 {
-	QMenu *const menu = SelectableItem::create_context_menu(parent);
+	QMenu *const menu = ViewItem::create_context_menu(parent);
 
 	return menu;
 }
@@ -183,50 +145,59 @@ pv::widgets::Popup* Trace::create_popup(QWidget *parent)
 {
 	using pv::widgets::Popup;
 
-	_popup = new Popup(parent);
+	popup_ = new Popup(parent);
+	popup_->set_position(parent->mapToGlobal(
+		point(parent->rect())), Popup::Right);
 
 	create_popup_form();
 
-	connect(_popup, SIGNAL(closed()),
+	connect(popup_, SIGNAL(closed()),
 		this, SLOT(on_popup_closed()));
 
-	return _popup;
-}
-
-int Trace::get_y() const
-{
-	return _v_offset - _view->v_offset();
+	return popup_;
 }
 
-QRectF Trace::get_label_rect(int right)
+QRectF Trace::label_rect(const QRectF &rect) const
 {
 	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());
+		m.boundingRect(QRect(), 0, name_).width(), m.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;
+		text_size.width() + LabelPadding.width() * 2,
+		ceilf((text_size.height() + LabelPadding.height() * 2) / 2) * 2);
+	const float half_height = 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());
+		rect.right() - half_height - label_size.width() - 0.5,
+		get_visual_y() + 0.5f - half_height,
+		label_size.width() + half_height,
+		label_size.height());
 }
 
-QColor Trace::get_text_colour() const
+void Trace::paint_back(QPainter &p, const ViewItemPaintParams &pp)
 {
-	return (_colour.lightness() > 64) ? Qt::black : Qt::white;
+	if (coloured_bg_)
+		p.setBrush(bgcolour_);
+	else
+		p.setBrush(bgcolour_state_ ? BrightBGColour : DarkBGColour);
+
+	p.setPen(QPen(Qt::NoPen));
+
+	const std::pair<int, int> extents = v_extents();
+
+	const int x = 0;
+	const int y = get_visual_y() + extents.first;
+	const int w = pp.right() - pp.left();
+	const int h = extents.second - extents.first;
+
+	p.drawRect(x, y, w, h);
 }
 
-void Trace::paint_axis(QPainter &p, int y, int left, int right)
+void Trace::paint_axis(QPainter &p, const ViewItemPaintParams &pp, int y)
 {
 	p.setPen(AxisPen);
-	p.drawLine(QPointF(left, y + 0.5f), QPointF(right, y + 0.5f));
+	p.drawLine(QPointF(pp.left(), y + 0.5f), QPointF(pp.right(), y + 0.5f));
 }
 
 void Trace::add_colour_option(QWidget *parent, QFormLayout *form)
@@ -236,7 +207,7 @@ void Trace::add_colour_option(QWidget *parent, QFormLayout *form)
 	ColourButton *const colour_button = new ColourButton(
 		TracePalette::Rows, TracePalette::Cols, parent);
 	colour_button->set_palette(TracePalette::Colours);
-	colour_button->set_colour(_colour);
+	colour_button->set_colour(colour_);
 	connect(colour_button, SIGNAL(selected(const QColor&)),
 		this, SLOT(on_colour_changed(const QColor&)));
 
@@ -250,19 +221,19 @@ void Trace::create_popup_form()
 	// 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);
+	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);
+	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);
+	name_edit->setText(name_);
 	connect(name_edit, SIGNAL(textChanged(const QString&)),
 		this, SLOT(on_text_changed(const QString&)));
 	form->addRow(tr("Name"), name_edit);
@@ -272,20 +243,24 @@ void Trace::populate_popup_form(QWidget *parent, QFormLayout *form)
 
 void Trace::on_popup_closed()
 {
-	_popup = NULL;
-	_popup_form = NULL;
+	popup_ = nullptr;
+	popup_form_ = nullptr;
 }
 
 void Trace::on_text_changed(const QString &text)
 {
 	set_name(text);
-	text_changed();
+
+	if (owner_)
+		owner_->extents_changed(true, false);
 }
 
 void Trace::on_colour_changed(const QColor &colour)
 {
 	set_colour(colour);
-	colour_changed();
+
+	if (owner_)
+		owner_->row_item_appearance_changed(true, true);
 }
 
 } // namespace view
diff --git a/pv/view/trace.h b/pv/view/trace.h
deleted file mode 100644
index ace93d5..0000000
--- a/pv/view/trace.h
+++ /dev/null
@@ -1,203 +0,0 @@
-/*
- * 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/trace.hpp b/pv/view/trace.hpp
new file mode 100644
index 0000000..cb5ac15
--- /dev/null
+++ b/pv/view/trace.hpp
@@ -0,0 +1,145 @@
+/*
+ * 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_HPP
+#define PULSEVIEW_PV_VIEW_TRACE_HPP
+
+#include <QColor>
+#include <QPainter>
+#include <QPen>
+#include <QRect>
+#include <QString>
+
+#include <stdint.h>
+
+#include "tracetreeitem.hpp"
+
+class QFormLayout;
+
+namespace pv {
+
+namespace widgets {
+class Popup;
+}
+
+namespace view {
+
+class Trace : public TraceTreeItem
+{
+	Q_OBJECT
+
+private:
+	static const QPen AxisPen;
+	static const int LabelHitPadding;
+
+	static const QColor BrightBGColour;
+	static const QColor DarkBGColour;
+
+protected:
+	Trace(QString name);
+
+public:
+	/**
+	 * Gets the name of this signal.
+	 */
+	QString name() const;
+
+	/**
+	 * Sets the name of the signal.
+	 */
+	virtual void set_name(QString name);
+
+	/**
+	 * Get the colour of the signal.
+	 */
+	QColor colour() const;
+
+	/**
+	 * Set the colour of the signal.
+	 */
+	void set_colour(QColor colour);
+
+	/**
+	 * Enables or disables the coloured background for this trace.
+	 */
+	void set_coloured_bg(bool state);
+
+	/**
+	 * Paints the signal label.
+	 * @param p the QPainter to paint into.
+	 * @param rect the rectangle of the header area.
+	 * @param hover true if the label is being hovered over by the mouse.
+	 */
+	virtual void paint_label(QPainter &p, const QRect &rect, bool hover);
+
+	virtual QMenu* create_context_menu(QWidget *parent);
+
+	pv::widgets::Popup* create_popup(QWidget *parent);
+
+	/**
+	 * Computes the outline rectangle of a label.
+	 * @param rect the rectangle of the header area.
+	 * @return Returns the rectangle of the signal label.
+	 */
+	QRectF label_rect(const QRectF &rect) const;
+
+protected:
+	/**
+	 * Paints the background layer of the signal with a QPainter.
+	 * @param p The QPainter to paint into.
+	 * @param pp The painting parameters object to paint with.
+	 */
+	virtual void paint_back(QPainter &p, const ViewItemPaintParams &pp);
+
+	/**
+	 * Paints a zero axis across the viewport.
+	 * @param p the QPainter to paint into.
+	 * @param pp the painting parameters object to paint with.
+	 * @param y the y-offset of the axis.
+	 */
+	void paint_axis(QPainter &p, const ViewItemPaintParams &pp, int y);
+
+	void add_colour_option(QWidget *parent, QFormLayout *form);
+
+	void create_popup_form();
+
+	virtual void populate_popup_form(QWidget *parent, QFormLayout *form);
+
+private Q_SLOTS:
+	void on_text_changed(const QString &text);
+
+	void on_colour_changed(const QColor &colour);
+
+	void on_popup_closed();
+
+protected:
+	QString name_;
+	QColor colour_, bgcolour_;
+	bool coloured_bg_, coloured_bg_state_;
+
+private:
+	pv::widgets::Popup *popup_;
+	QFormLayout *popup_form_;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_TRACE_HPP
diff --git a/pv/view/tracegroup.cpp b/pv/view/tracegroup.cpp
new file mode 100644
index 0000000..9c1d49f
--- /dev/null
+++ b/pv/view/tracegroup.cpp
@@ -0,0 +1,226 @@
+/*
+ * 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 <algorithm>
+
+#include <QMenu>
+#include <QPainter>
+
+#include "tracegroup.hpp"
+
+using std::pair;
+using std::shared_ptr;
+using std::vector;
+
+namespace pv {
+namespace view {
+
+const int TraceGroup::Padding = 8;
+const int TraceGroup::Width = 12;
+const int TraceGroup::LineThickness = 5;
+const QColor TraceGroup::LineColour(QColor(0x55, 0x57, 0x53));
+
+TraceGroup::~TraceGroup()
+{
+	owner_ = nullptr;
+	clear_child_items();
+}
+
+bool TraceGroup::enabled() const
+{
+	return std::any_of(child_items().begin(), child_items().end(),
+		[](const shared_ptr<ViewItem> &r) { return r->enabled(); });
+}
+
+pv::Session& TraceGroup::session()
+{
+	assert(owner_);
+	return owner_->session();
+}
+
+const pv::Session& TraceGroup::session() const
+{
+	assert(owner_);
+	return owner_->session();
+}
+
+pv::view::View* TraceGroup::view()
+{
+	assert(owner_);
+	return owner_->view();
+}
+
+const pv::view::View* TraceGroup::view() const
+{
+	assert(owner_);
+	return owner_->view();
+}
+
+pair<int, int> TraceGroup::v_extents() const
+{
+	return TraceTreeItemOwner::v_extents();
+}
+
+void TraceGroup::paint_label(QPainter &p, const QRect &rect, bool hover)
+{
+	const QRectF r = label_rect(rect).adjusted(
+		LineThickness / 2, LineThickness / 2,
+		-LineThickness / 2, -LineThickness / 2);
+
+	// Paint the label
+	const QPointF points[] = {
+		r.topRight(),
+		r.topLeft(),
+		r.bottomLeft(),
+		r.bottomRight()
+	};
+
+	if (selected()) {
+		const QPen pen(highlight_pen());
+		p.setPen(QPen(pen.brush(), pen.width() + LineThickness,
+			Qt::SolidLine, Qt::SquareCap, Qt::RoundJoin));
+		p.setBrush(Qt::transparent);
+		p.drawPolyline(points, countof(points));
+	}
+
+	p.setPen(QPen(QBrush(LineColour.darker()), LineThickness,
+		Qt::SolidLine, Qt::SquareCap, Qt::RoundJoin));
+	p.drawPolyline(points, countof(points));
+	p.setPen(QPen(QBrush(hover ? LineColour.lighter() : LineColour),
+		LineThickness - 2, Qt::SolidLine, Qt::SquareCap,
+		Qt::RoundJoin));
+	p.drawPolyline(points, countof(points));
+}
+
+QRectF TraceGroup::label_rect(const QRectF &rect) const
+{
+	QRectF child_rect;
+	for (const shared_ptr<ViewItem> r : child_items())
+		if (r && r->enabled())
+			child_rect = child_rect.united(r->label_rect(rect));
+
+	return QRectF(child_rect.x() - Width - Padding, child_rect.y(),
+		Width, child_rect.height());
+}
+
+bool TraceGroup::pt_in_label_rect(int left, int right, const QPoint &point)
+{
+	(void)left;
+	(void)right;
+	(void)point;
+
+	return false;
+}
+
+QMenu* TraceGroup::create_context_menu(QWidget *parent)
+{
+	QMenu *const menu = new QMenu(parent);
+
+	QAction *const ungroup = new QAction(tr("Ungroup"), this);
+	ungroup->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_U));
+	connect(ungroup, SIGNAL(triggered()), this, SLOT(on_ungroup()));
+	menu->addAction(ungroup);
+
+	return menu;
+}
+
+pv::widgets::Popup* TraceGroup::create_popup(QWidget *parent)
+{
+	(void)parent;
+	return nullptr;
+}
+
+int TraceGroup::owner_visual_v_offset() const
+{
+	return owner_ ? visual_v_offset() + owner_->owner_visual_v_offset() : 0;
+}
+
+void TraceGroup::restack_items()
+{
+	vector<shared_ptr<TraceTreeItem>> items(trace_tree_child_items());
+
+	// Sort by the centre line of the extents
+	stable_sort(items.begin(), items.end(),
+		[](const shared_ptr<TraceTreeItem> &a, const shared_ptr<TraceTreeItem> &b) {
+			const auto aext = a->v_extents();
+			const auto bext = b->v_extents();
+			return a->layout_v_offset() +
+					(aext.first + aext.second) / 2 <
+				b->layout_v_offset() +
+					(bext.first + bext.second) / 2;
+		});
+
+	int total_offset = 0;
+	for (shared_ptr<TraceTreeItem> r : items) {
+		const pair<int, int> extents = r->v_extents();
+		if (extents.first == 0 && extents.second == 0)
+			continue;
+
+		// We position disabled traces, so that they are close to the
+		// animation target positon should they be re-enabled
+		if (r->enabled())
+			total_offset += -extents.first;
+
+		if (!r->dragging())
+			r->set_layout_v_offset(total_offset);
+
+		if (r->enabled())
+			total_offset += extents.second;
+	}
+}
+
+unsigned int TraceGroup::depth() const
+{
+	return owner_ ? owner_->depth() + 1 : 0;
+}
+
+void TraceGroup::ungroup()
+{
+	const vector<shared_ptr<TraceTreeItem>> items(trace_tree_child_items());
+	clear_child_items();
+
+	for (shared_ptr<TraceTreeItem> r : items)
+		owner_->add_child_item(r);
+
+	owner_->remove_child_item(shared_from_this());
+}
+
+void TraceGroup::on_ungroup()
+{
+	ungroup();
+}
+
+void TraceGroup::row_item_appearance_changed(bool label, bool content)
+{
+	if (owner_)
+		owner_->row_item_appearance_changed(label, content);
+}
+
+void TraceGroup::extents_changed(bool horz, bool vert)
+{
+	if (owner_)
+		owner_->extents_changed(horz, vert);
+}
+
+} // namespace view
+} // namespace pv
diff --git a/pv/view/tracegroup.hpp b/pv/view/tracegroup.hpp
new file mode 100644
index 0000000..6407f1b
--- /dev/null
+++ b/pv/view/tracegroup.hpp
@@ -0,0 +1,133 @@
+/*
+ * 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_TRACEGROUP_HPP
+#define PULSEVIEW_PV_VIEW_TRACEGROUP_HPP
+
+#include "tracetreeitem.hpp"
+#include "tracetreeitemowner.hpp"
+
+namespace pv {
+namespace view {
+
+class TraceGroup : public TraceTreeItem, public TraceTreeItemOwner
+{
+	Q_OBJECT
+
+private:
+	static const int Padding;
+	static const int Width;
+	static const int LineThickness;
+	static const QColor LineColour;
+
+public:
+	/**
+	 * Virtual destructor
+	 */
+	virtual ~TraceGroup();
+
+	/**
+	 * Returns true if the item is visible and enabled.
+	 */
+	bool enabled() const;
+
+	/**
+	 * Returns the session of the onwer.
+	 */
+	pv::Session& session();
+
+	/**
+	 * Returns the session of the onwer.
+	 */
+	const pv::Session& session() const;
+
+	/**
+	 * Returns the view of the owner.
+	 */
+	virtual pv::view::View* view();
+
+	/**
+	 * Returns the view of the owner.
+	 */
+	virtual const pv::view::View* view() const;
+
+	/**
+	 * Computes the vertical extents of the contents of this row item.
+	 * @return A pair containing the minimum and maximum y-values.
+	 */
+	std::pair<int, int> v_extents() const;
+
+	/**
+	 * Paints the signal label.
+	 * @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.
+	 */
+	void paint_label(QPainter &p, const QRect &rect, bool hover);
+
+	/**
+	 * Computes the outline rectangle of a label.
+	 * @param rect the rectangle of the header area.
+	 * @return Returns the rectangle of the signal label.
+	 */
+	QRectF label_rect(const QRectF &rect) const;
+
+	/**
+	 * 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);
+
+	QMenu* create_context_menu(QWidget *parent);
+
+	pv::widgets::Popup* create_popup(QWidget *parent);
+
+	/**
+	 * Returns the total vertical offset of this trace and all it's owners
+	 */
+	int owner_visual_v_offset() const;
+
+	void restack_items();
+
+	/**
+	 * Returns the number of nested parents that this row item owner has.
+	 */
+	unsigned int depth() const;
+
+	void ungroup();
+
+public:
+	void row_item_appearance_changed(bool label, bool content);
+
+	void extents_changed(bool horz, bool vert);
+
+private Q_SLOTS:
+	void on_ungroup();
+};
+
+} // view
+} // pv
+
+#endif // PULSEVIEW_PV_VIEW_TRACEGROUP_HPP
diff --git a/pv/view/tracepalette.cpp b/pv/view/tracepalette.cpp
index cefb952..3dcb63d 100644
--- a/pv/view/tracepalette.cpp
+++ b/pv/view/tracepalette.cpp
@@ -18,7 +18,7 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "tracepalette.h"
+#include "tracepalette.hpp"
 
 namespace pv {
 namespace view {
diff --git a/pv/view/tracepalette.h b/pv/view/tracepalette.hpp
similarity index 89%
rename from pv/view/tracepalette.h
rename to pv/view/tracepalette.hpp
index ae3c69a..e878a22 100644
--- a/pv/view/tracepalette.h
+++ b/pv/view/tracepalette.hpp
@@ -18,8 +18,8 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_TRACEPALETTE_H
-#define PULSEVIEW_PV_TRACEPALETTE_H
+#ifndef PULSEVIEW_PV_TRACEPALETTE_HPP
+#define PULSEVIEW_PV_TRACEPALETTE_HPP
 
 #include <QColor>
 
@@ -38,4 +38,4 @@ public:
 } // view
 } // pv
 
-#endif // PULSEVIEW_PV_VIEW_TRACEPALETTE_H
+#endif // PULSEVIEW_PV_VIEW_TRACEPALETTE_HPP
diff --git a/pv/view/tracetreeitem.cpp b/pv/view/tracetreeitem.cpp
new file mode 100644
index 0000000..44c5d4b
--- /dev/null
+++ b/pv/view/tracetreeitem.cpp
@@ -0,0 +1,147 @@
+/*
+ * 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 "view.hpp"
+
+#include "tracetreeitem.hpp"
+
+namespace pv {
+namespace view {
+
+TraceTreeItem::TraceTreeItem() :
+	owner_(nullptr),
+	layout_v_offset_(0),
+	visual_v_offset_(0),
+	v_offset_animation_(this, "visual_v_offset")
+{
+}
+
+void TraceTreeItem::select(bool select)
+{
+	ViewItem::select(select);
+	owner_->row_item_appearance_changed(true, true);
+}
+
+int TraceTreeItem::layout_v_offset() const
+{
+	return layout_v_offset_;
+}
+
+void TraceTreeItem::set_layout_v_offset(int v_offset)
+{
+	if (layout_v_offset_ == v_offset)
+		return;
+
+	layout_v_offset_ = v_offset;
+
+	if (owner_)
+		owner_->extents_changed(false, true);
+}
+
+int TraceTreeItem::visual_v_offset() const
+{
+	return visual_v_offset_;
+}
+
+void TraceTreeItem::set_visual_v_offset(int v_offset)
+{
+	visual_v_offset_ = v_offset;
+
+	if (owner_)
+		owner_->row_item_appearance_changed(true, true);
+}
+
+void TraceTreeItem::force_to_v_offset(int v_offset)
+{
+	v_offset_animation_.stop();
+	layout_v_offset_ = visual_v_offset_ = v_offset;
+
+	if (owner_) {
+		owner_->row_item_appearance_changed(true, true);
+		owner_->extents_changed(false, true);
+	}
+}
+
+void TraceTreeItem::animate_to_layout_v_offset()
+{
+	if (visual_v_offset_ == layout_v_offset_ ||
+		(v_offset_animation_.endValue() == layout_v_offset_ &&
+		v_offset_animation_.state() == QAbstractAnimation::Running))
+		return;
+
+	v_offset_animation_.setDuration(100);
+	v_offset_animation_.setStartValue(visual_v_offset_);
+	v_offset_animation_.setEndValue(layout_v_offset_);
+	v_offset_animation_.setEasingCurve(QEasingCurve::OutQuad);
+	v_offset_animation_.start();
+}
+
+TraceTreeItemOwner* TraceTreeItem::owner() const
+{
+	return owner_;
+}
+
+void TraceTreeItem::set_owner(TraceTreeItemOwner *owner)
+{
+	assert(owner_ || owner);
+	v_offset_animation_.stop();
+
+	if (owner_) {
+		const int owner_offset = owner_->owner_visual_v_offset();
+		layout_v_offset_ += owner_offset;
+		visual_v_offset_ += owner_offset;
+	}
+
+	owner_ = owner;
+
+	if (owner_) {
+		const int owner_offset = owner_->owner_visual_v_offset();
+		layout_v_offset_ -= owner_offset;
+		visual_v_offset_ -= owner_offset;
+	}
+}
+
+int TraceTreeItem::get_visual_y() const
+{
+	assert(owner_);
+	return visual_v_offset_ + owner_->owner_visual_v_offset();
+}
+
+void TraceTreeItem::drag_by(const QPoint &delta)
+{
+	assert(owner_);
+	force_to_v_offset(drag_point_.y() + delta.y() -
+		owner_->owner_visual_v_offset());
+}
+
+QPoint TraceTreeItem::point(const QRect &rect) const
+{
+	return QPoint(rect.right(), get_visual_y());
+}
+
+void TraceTreeItem::set_bgcolour_state(bool state)
+{
+       bgcolour_state_ = state;
+}
+
+} // namespace view
+} // namespace pv
diff --git a/pv/view/tracetreeitem.hpp b/pv/view/tracetreeitem.hpp
new file mode 100644
index 0000000..ed0e7e8
--- /dev/null
+++ b/pv/view/tracetreeitem.hpp
@@ -0,0 +1,142 @@
+/*
+ * 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_TRACETREEITEM_HPP
+#define PULSEVIEW_PV_VIEW_TRACETREEITEM_HPP
+
+#include <memory>
+
+#include <QPropertyAnimation>
+
+#include "rowitem.hpp"
+
+namespace pv {
+namespace view {
+
+class TraceTreeItemOwner;
+
+class TraceTreeItem : public RowItem,
+	public std::enable_shared_from_this<pv::view::TraceTreeItem>
+{
+	Q_OBJECT
+	Q_PROPERTY(int visual_v_offset
+		READ visual_v_offset
+		WRITE set_visual_v_offset)
+
+public:
+	/**
+	 * Constructor.
+	 */
+	TraceTreeItem();
+
+	/**
+	 * Gets the owner this item in the view item hierachy.
+	 */
+	TraceTreeItemOwner* owner() const;
+
+	/**
+	 * Selects or deselects the signal.
+	 */
+	void select(bool select = true);
+
+	/**
+	 * Gets the vertical layout offset of this signal.
+	 */
+	int layout_v_offset() const;
+
+	/**
+	 * Sets the vertical layout offset of this signal.
+	 */
+	void set_layout_v_offset(int v_offset);
+
+	/**
+	 * Gets the vertical visual offset of this signal.
+	 */
+	int visual_v_offset() const;
+
+	/**
+	 * Sets the vertical visual offset of this signal.
+	 */
+	void set_visual_v_offset(int v_offset);
+
+	/**
+	 * Sets the visual and layout offset of this signal.
+	 */
+	void force_to_v_offset(int v_offset);
+
+	/**
+	 * Begins an animation that will animate the visual offset toward
+	 * the layout offset.
+	 */
+	void animate_to_layout_v_offset();
+
+	/**
+	 * Sets the owner this trace in the view trace hierachy.
+	 * @param The new owner of the trace.
+	 */
+	void set_owner(pv::view::TraceTreeItemOwner *owner);
+
+	/**
+	 * Gets the visual y-offset of the axis.
+	 */
+	int get_visual_y() const;
+
+	/**
+	 * Drags the item to a delta relative to the drag point.
+	 * @param delta the offset from the drag point.
+	 */
+	void drag_by(const QPoint &delta);
+
+	/**
+	 * Gets the arrow-tip point of the row item marker.
+	 * @param rect the rectangle of the header area.
+	 */
+	QPoint point(const QRect &rect) const;
+
+	/**
+	 * Sets the new background colour state: false = dark, true = bright.
+	 * This is to allow for alternating backgrounds but has no effect
+	 * when coloured background colours are used.
+	 * @param state New bg color state to use.
+	 */
+	void set_bgcolour_state(bool state);
+
+	/**
+	 * Computes the vertical extents of the contents of this row item.
+	 * @return A pair containing the minimum and maximum y-values.
+	 */
+	virtual std::pair<int, int> v_extents() const = 0;
+
+protected:
+	TraceTreeItemOwner *owner_;
+
+	int layout_v_offset_;
+	int visual_v_offset_;
+
+	bool bgcolour_state_;
+
+private:
+	QPropertyAnimation v_offset_animation_;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_TRACETREEITEM_HPP
diff --git a/pv/view/tracetreeitemowner.cpp b/pv/view/tracetreeitemowner.cpp
new file mode 100644
index 0000000..2417479
--- /dev/null
+++ b/pv/view/tracetreeitemowner.cpp
@@ -0,0 +1,130 @@
+/*
+ * 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 "tracetreeitem.hpp"
+#include "tracetreeitemowner.hpp"
+#include "trace.hpp"
+
+using std::dynamic_pointer_cast;
+using std::max;
+using std::make_pair;
+using std::min;
+using std::pair;
+using std::set;
+using std::shared_ptr;
+using std::static_pointer_cast;
+using std::vector;
+
+namespace pv {
+namespace view {
+
+const ViewItemOwner::item_list& TraceTreeItemOwner::child_items() const
+{
+	return items_;
+}
+
+vector< std::shared_ptr<TraceTreeItem> >
+TraceTreeItemOwner::trace_tree_child_items() const
+{
+	vector< shared_ptr<TraceTreeItem> > items;
+	for (auto &i : items_) {
+		assert(dynamic_pointer_cast<TraceTreeItem>(i));
+		const shared_ptr<TraceTreeItem> t(
+			static_pointer_cast<TraceTreeItem>(i));
+		items.push_back(t);
+	}
+
+	return items;
+}
+
+void TraceTreeItemOwner::clear_child_items()
+{
+	for (auto &t : trace_tree_child_items()) {
+		assert(t->owner() == this);
+		t->set_owner(nullptr);
+	}
+	items_.clear();
+}
+
+void TraceTreeItemOwner::add_child_item(std::shared_ptr<TraceTreeItem> item)
+{
+	assert(!item->owner());
+	item->set_owner(this);
+	items_.push_back(item);
+
+	extents_changed(true, true);
+}
+
+void TraceTreeItemOwner::remove_child_item(std::shared_ptr<TraceTreeItem> item)
+{
+	assert(item->owner() == this);
+	item->set_owner(nullptr);
+	auto iter = std::find(items_.begin(), items_.end(), item);
+	assert(iter != items_.end());
+	items_.erase(iter);
+
+	extents_changed(true, true);
+}
+
+pair<int, int> TraceTreeItemOwner::v_extents() const
+{
+	pair<int, int> extents(INT_MAX, INT_MIN);
+
+	for (const shared_ptr<TraceTreeItem> t : trace_tree_child_items()) {
+		assert(t);
+		if (!t->enabled())
+			continue;
+
+		const int child_offset = t->layout_v_offset();
+		const pair<int, int> child_extents = t->v_extents();
+		extents.first = min(child_extents.first + child_offset,
+			extents.first);
+		extents.second = max(child_extents.second + child_offset,
+			extents.second);
+	}
+
+	return extents;
+}
+
+bool TraceTreeItemOwner::reassign_bgcolour_states(bool next_bgcolour_state)
+{
+	vector< shared_ptr<TraceTreeItem> > items = trace_tree_child_items();
+
+	// Sort items according to vertical position
+	sort(items.begin(), items.end(),
+		[](const shared_ptr<TraceTreeItem> a, const shared_ptr<TraceTreeItem> b) {
+		return a->layout_v_offset() > b->layout_v_offset(); });
+
+	for (const shared_ptr<TraceTreeItem> item : items) {
+		item->set_bgcolour_state(next_bgcolour_state);
+		next_bgcolour_state = !next_bgcolour_state;
+	}
+
+	return next_bgcolour_state;
+}
+
+void TraceTreeItemOwner::restack_items()
+{
+}
+
+} // view
+} // pv
diff --git a/pv/view/tracetreeitemowner.hpp b/pv/view/tracetreeitemowner.hpp
new file mode 100644
index 0000000..6d4b6d1
--- /dev/null
+++ b/pv/view/tracetreeitemowner.hpp
@@ -0,0 +1,117 @@
+/*
+ * 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_VIEW_TRACETREEITEMOWNER_HPP
+#define PULSEVIEW_PV_VIEW_TRACETREEITEMOWNER_HPP
+
+#include "viewitemowner.hpp"
+#include "tracetreeitem.hpp"
+
+namespace pv {
+
+class Session;
+
+namespace view {
+
+class TraceTreeItem;
+class View;
+
+class TraceTreeItemOwner : public ViewItemOwner
+{
+public:
+	/**
+	 * Returns the view of the owner.
+	 */
+	virtual pv::view::View* view() = 0;
+
+	/**
+	 * Returns the view of the owner.
+	 */
+	virtual const pv::view::View* view() const = 0;
+
+	virtual int owner_visual_v_offset() const = 0;
+
+	/**
+	 * Returns the session of the onwer.
+	 */
+	virtual pv::Session& session() = 0;
+
+	/**
+	 * Returns the session of the owner.
+	 */
+	virtual const pv::Session& session() const = 0;
+
+	/**
+	 * Returns the number of nested parents that this row item owner has.
+	 */
+	virtual unsigned int depth() const = 0;
+
+	/**
+	 * Returns a list of row items owned by this object.
+	 */
+	virtual const item_list& child_items() const;
+
+	/**
+	 * Returns a list of row items owned by this object.
+	 */
+	std::vector< std::shared_ptr<TraceTreeItem> >
+	trace_tree_child_items() const;
+
+	/**
+	 * Clears the list of child items.
+	 */
+	void clear_child_items();
+
+	/**
+	 * Adds a child item to this object.
+	 */
+	void add_child_item(std::shared_ptr<TraceTreeItem> item);
+
+	/**
+	 * Removes a child item from this object.
+	 */
+	void remove_child_item(std::shared_ptr<TraceTreeItem> item);
+
+	virtual void restack_items();
+
+	/**
+	 * Computes the vertical extents of the contents of this row item owner.
+	 * @return A pair containing the minimum and maximum y-values.
+	 */
+	std::pair<int, int> v_extents() const;
+
+	/*
+	 * Reassigns background color states to all its children, thereby
+	 * providing them with alternating backgrounds.
+	 * @param next_bgcolour_state First brightness state to use.
+	 * @return The next brightness state to use.
+	 */
+	bool reassign_bgcolour_states(bool next_bgcolour_state);
+
+public:
+	virtual void row_item_appearance_changed(bool label, bool content) = 0;
+
+	virtual void extents_changed(bool horz, bool vert) = 0;
+};
+
+} // view
+} // pv
+
+#endif // PULSEVIEW_PV_VIEW_TRACETREEITEMOWNER_HPP
diff --git a/pv/view/triggermarker.cpp b/pv/view/triggermarker.cpp
new file mode 100644
index 0000000..f15672b
--- /dev/null
+++ b/pv/view/triggermarker.cpp
@@ -0,0 +1,82 @@
+/*
+ * 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 "triggermarker.hpp"
+#include "view.hpp"
+
+namespace pv {
+namespace view {
+
+const QColor TriggerMarker::Colour(0x00, 0x00, 0xB0);
+
+TriggerMarker::TriggerMarker(View &view, const pv::util::Timestamp& time) :
+	TimeItem(view),
+	time_(time)
+{
+}
+
+TriggerMarker::TriggerMarker(const TriggerMarker &marker) :
+	TimeItem(marker.view_),
+	time_(marker.time_)
+{
+}
+
+bool TriggerMarker::enabled() const
+{
+	return true;
+}
+
+bool TriggerMarker::is_draggable() const
+{
+	return false;
+}
+
+void TriggerMarker::set_time(const pv::util::Timestamp& time)
+{
+	time_ = time;
+
+	view_.time_item_appearance_changed(true, true);
+}
+
+float TriggerMarker::get_x() const
+{
+	return ((time_ - view_.offset()) / view_.scale()).convert_to<float>();
+}
+
+QPoint TriggerMarker::point(const QRect &rect) const
+{
+	return QPoint(get_x(), rect.bottom());
+}
+
+void TriggerMarker::paint_fore(QPainter &p, const ViewItemPaintParams &pp)
+{
+	if (!enabled())
+		return;
+
+	QPen pen(Colour);
+	pen.setStyle(Qt::DashLine);
+
+	const float x = get_x();
+	p.setPen(pen);
+	p.drawLine(QPointF(x, pp.top()), QPointF(x, pp.bottom()));
+}
+
+} // namespace view
+} // namespace pv
diff --git a/pv/view/triggermarker.hpp b/pv/view/triggermarker.hpp
new file mode 100644
index 0000000..cbbcb36
--- /dev/null
+++ b/pv/view/triggermarker.hpp
@@ -0,0 +1,86 @@
+/*
+ * 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_VIEW_TRIGGER_MARKER_HPP
+#define PULSEVIEW_PV_VIEW_TRIGGER_MARKER_HPP
+
+#include "timeitem.hpp"
+
+namespace pv {
+namespace view {
+
+class TriggerMarker : public TimeItem
+{
+	Q_OBJECT
+
+public:
+	static const QColor Colour;
+
+public:
+	/**
+	 * Constructor.
+	 * @param view A reference to the view that owns this marker.
+	 * @param time The time to set the marker to.
+	 */
+	TriggerMarker(View &view, const pv::util::Timestamp& time);
+
+	/**
+	 * Copy constructor.
+	 */
+	TriggerMarker(const TriggerMarker &marker);
+
+	/**
+	 * Returns true if the item is visible and enabled.
+	 */
+	bool enabled() const override;
+
+	/**
+	  Returns true if the item may be dragged/moved.
+	 */
+	bool is_draggable() const override;
+
+	/**
+	 * Sets the time of the marker.
+	 */
+	void set_time(const pv::util::Timestamp& time) override;
+
+	float get_x() const override;
+
+	/**
+	 * Gets the arrow-tip point of the time marker.
+	 * @param rect the rectangle of the ruler area.
+	 */
+	QPoint point(const QRect &rect) const override;
+
+	/**
+	 * Paints the foreground layer of the item with a QPainter
+	 * @param p the QPainter to paint into.
+	 * @param pp the painting parameters object to paint with.
+	 */
+	void paint_fore(QPainter &p, const ViewItemPaintParams &pp) override;
+
+private:
+	pv::util::Timestamp time_;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_TRIGGER_MARKER_HPP
diff --git a/pv/view/view.cpp b/pv/view/view.cpp
index d117489..c9b08bf 100644
--- a/pv/view/view.cpp
+++ b/pv/view/view.cpp
@@ -22,406 +22,778 @@
 #include <libsigrokdecode/libsigrokdecode.h>
 #endif
 
-#include <assert.h>
-#include <limits.h>
-#include <math.h>
+#include <extdef.h>
 
-#include <boost/foreach.hpp>
+#include <algorithm>
+#include <cassert>
+#include <climits>
+#include <cmath>
+#include <iterator>
+#include <mutex>
+#include <unordered_set>
 
+#include <boost/thread/locks.hpp>
+
+#include <QApplication>
 #include <QEvent>
+#include <QFontMetrics>
 #include <QMouseEvent>
 #include <QScrollBar>
 
-#include "decodetrace.h"
-#include "header.h"
-#include "ruler.h"
-#include "signal.h"
-#include "view.h"
-#include "viewport.h"
+#include <libsigrokcxx/libsigrokcxx.hpp>
+
+#include "analogsignal.hpp"
+#include "decodetrace.hpp"
+#include "header.hpp"
+#include "logicsignal.hpp"
+#include "ruler.hpp"
+#include "signal.hpp"
+#include "tracegroup.hpp"
+#include "triggermarker.hpp"
+#include "view.hpp"
+#include "viewport.hpp"
 
-#include "pv/sigsession.h"
-#include "pv/data/logic.h"
-#include "pv/data/logicsnapshot.h"
+#include "pv/session.hpp"
+#include "pv/devices/device.hpp"
+#include "pv/data/logic.hpp"
+#include "pv/data/logicsegment.hpp"
+#include "pv/util.hpp"
+
+using boost::shared_lock;
+using boost::shared_mutex;
 
-using boost::shared_ptr;
-using boost::weak_ptr;
 using pv::data::SignalData;
+using pv::data::Segment;
+using pv::util::TimeUnit;
+using pv::util::Timestamp;
+
+using std::back_inserter;
+using std::copy_if;
 using std::deque;
+using std::dynamic_pointer_cast;
+using std::inserter;
 using std::list;
+using std::lock_guard;
 using std::max;
 using std::make_pair;
+using std::make_shared;
 using std::min;
 using std::pair;
 using std::set;
+using std::set_difference;
+using std::shared_ptr;
+using std::unordered_map;
+using std::unordered_set;
 using std::vector;
+using std::weak_ptr;
 
 namespace pv {
 namespace view {
 
-const double View::MaxScale = 1e9;
-const double View::MinScale = 1e-15;
+const Timestamp View::MaxScale("1e9");
+const Timestamp View::MinScale("1e-12");
 
 const int View::MaxScrollValue = INT_MAX / 2;
+const int View::MaxViewAutoUpdateRate = 25; // No more than 25 Hz with sticky scrolling
 
-const int View::SignalHeight = 30;
-const int View::SignalMargin = 10;
-const int View::SignalSnapGridSize = 10;
+const int View::ScaleUnits[3] = {1, 2, 5};
 
-const QColor View::CursorAreaColour(220, 231, 243);
-
-const QSizeF View::LabelPadding(4, 0);
-
-View::View(SigSession &session, QWidget *parent) :
+View::View(Session &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)
+	session_(session),
+	viewport_(new Viewport(*this)),
+	ruler_(new Ruler(*this)),
+	header_(new Header(*this)),
+	scale_(1e-3),
+	offset_(0),
+	updating_scroll_(false),
+	sticky_scrolling_(false), // Default setting is set in MainWindow::setup_ui()
+	always_zoom_to_fit_(false),
+	tick_period_(0),
+	tick_prefix_(pv::util::SIPrefix::yocto),
+	tick_precision_(0),
+	time_unit_(util::TimeUnit::Time),
+	show_cursors_(false),
+	cursors_(new CursorPair(*this)),
+	next_flag_text_('A'),
+	trigger_markers_(),
+	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)));
+		this, SLOT(v_scroll_value_changed()));
 
-	connect(&_session, SIGNAL(signals_changed()),
+	connect(&session_, SIGNAL(signals_changed()),
 		this, SLOT(signals_changed()));
-	connect(&_session, SIGNAL(data_received()),
+	connect(&session_, SIGNAL(capture_state_changed(int)),
+		this, SLOT(capture_state_updated(int)));
+	connect(&session_, SIGNAL(data_received()),
 		this, SLOT(data_updated()));
-	connect(&_session, SIGNAL(frame_ended()),
+	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()),
-		_ruler, SLOT(clear_selection()));
-	connect(_ruler, SIGNAL(selection_changed()),
-		_header, SLOT(clear_selection()));
-
-	connect(_header, SIGNAL(selection_changed()),
+	connect(header_, SIGNAL(selection_changed()),
 		this, SIGNAL(selection_changed()));
-	connect(_ruler, SIGNAL(selection_changed()),
+	connect(ruler_, SIGNAL(selection_changed()),
 		this, SIGNAL(selection_changed()));
 
-	setViewport(_viewport);
+	connect(this, SIGNAL(hover_point_changed()),
+		this, SLOT(on_hover_point_changed()));
+
+	connect(&lazy_event_handler_, SIGNAL(timeout()),
+		this, SLOT(process_sticky_events()));
+	lazy_event_handler_.setSingleShot(true);
+
+	connect(&delayed_view_updater_, SIGNAL(timeout()),
+		this, SLOT(perform_delayed_view_update()));
+	delayed_view_updater_.setSingleShot(true);
+	delayed_view_updater_.setInterval(1000 / MaxViewAutoUpdateRate);
+
+	setViewport(viewport_);
 
-	_viewport->installEventFilter(this);
-	_ruler->installEventFilter(this);
-	_header->installEventFilter(this);
+	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();
+
+	// make sure the transparent widgets are on the top
+	ruler_->raise();
+	header_->raise();
+
+	// Update the zoom state
+	calculate_tick_spacing();
+}
+
+Session& View::session()
+{
+	return session_;
+}
+
+const Session& View::session() const
+{
+	return session_;
+}
+
+View* View::view()
+{
+	return this;
+}
+
+const View* View::view() const
+{
+	return this;
+}
+
+Viewport* View::viewport()
+{
+	return viewport_;
 }
 
-SigSession& View::session()
+const Viewport* View::viewport() const
 {
-	return _session;
+	return viewport_;
 }
 
-const SigSession& View::session() const
+vector< shared_ptr<TimeItem> > View::time_items() const
 {
-	return _session;
+	const vector<shared_ptr<Flag>> f(flags());
+	vector<shared_ptr<TimeItem>> items(f.begin(), f.end());
+	items.push_back(cursors_);
+	items.push_back(cursors_->first());
+	items.push_back(cursors_->second());
+
+	for (auto trigger_marker : trigger_markers_)
+		items.push_back(trigger_marker);
+
+	return items;
 }
 
 double View::scale() const
 {
-	return _scale;
+	return scale_;
+}
+
+void View::set_scale(double scale)
+{
+	if (scale_ != scale) {
+		scale_ = scale;
+		Q_EMIT scale_changed();
+	}
+}
+
+const Timestamp& View::offset() const
+{
+	return offset_;
+}
+
+void View::set_offset(const pv::util::Timestamp& offset)
+{
+	if (offset_ != offset) {
+		offset_ = offset;
+		Q_EMIT offset_changed();
+	}
+}
+
+int View::owner_visual_v_offset() const
+{
+	return -verticalScrollBar()->sliderPosition();
+}
+
+void View::set_v_offset(int offset)
+{
+	verticalScrollBar()->setSliderPosition(offset);
+	header_->update();
+	viewport_->update();
+}
+
+unsigned int View::depth() const
+{
+	return 0;
 }
 
-double View::offset() const
+pv::util::SIPrefix View::tick_prefix() const
 {
-	return _offset;
+	return tick_prefix_;
 }
 
-int View::v_offset() const
+void View::set_tick_prefix(pv::util::SIPrefix tick_prefix)
 {
-	return _v_offset;
+	if (tick_prefix_ != tick_prefix) {
+		tick_prefix_ = tick_prefix;
+		Q_EMIT tick_prefix_changed();
+	}
+}
+
+unsigned int View::tick_precision() const
+{
+	return tick_precision_;
+}
+
+void View::set_tick_precision(unsigned tick_precision)
+{
+	if (tick_precision_ != tick_precision) {
+		tick_precision_ = tick_precision;
+		Q_EMIT tick_precision_changed();
+	}
+}
+
+const pv::util::Timestamp& View::tick_period() const
+{
+	return tick_period_;
+}
+
+void View::set_tick_period(const pv::util::Timestamp& tick_period)
+{
+	if (tick_period_ != tick_period) {
+		tick_period_ = tick_period;
+		Q_EMIT tick_period_changed();
+	}
+}
+
+TimeUnit View::time_unit() const
+{
+	return time_unit_;
+}
+
+void View::set_time_unit(pv::util::TimeUnit time_unit)
+{
+	if (time_unit_ != time_unit) {
+		time_unit_ = time_unit;
+		Q_EMIT time_unit_changed();
+	}
 }
 
 void View::zoom(double steps)
 {
-	zoom(steps, _viewport->width() / 2);
+	zoom(steps, viewport_->width() / 2);
 }
 
 void View::zoom(double steps, int offset)
 {
-	set_zoom(_scale * pow(3.0/2.0, -steps), offset);
+	set_zoom(scale_ * pow(3.0/2.0, -steps), offset);
 }
 
-void View::zoom_fit()
+void View::zoom_fit(bool gui_state)
 {
-	const pair<double, double> extents = get_time_extents();
-	const double delta = extents.second - extents.first;
-	if (delta < 1e-12)
+	// Act as one-shot when stopped, toggle along with the GUI otherwise
+	if (session_.get_capture_state() == Session::Stopped) {
+		always_zoom_to_fit_ = false;
+		always_zoom_to_fit_changed(false);
+	} else {
+		always_zoom_to_fit_ = gui_state;
+		always_zoom_to_fit_changed(gui_state);
+	}
+
+	const pair<Timestamp, Timestamp> extents = get_time_extents();
+	const Timestamp delta = extents.second - extents.first;
+	if (delta < Timestamp("1e-12"))
 		return;
 
-	assert(_viewport);
-	const int w = _viewport->width();
+	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);
+	const Timestamp scale = max(min(delta / w, MaxScale), MinScale);
+	set_scale_offset(scale.convert_to<double>(), 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();
+	assert(viewport_);
+	const int w = viewport_->width();
 	if (w <= 0)
 		return;
 
-	set_zoom(1.0 / samplerate, w / 2);
+	set_zoom(1.0 / session_.get_samplerate(), w / 2);
 }
 
-void View::set_scale_offset(double scale, double offset)
+void View::set_scale_offset(double scale, const Timestamp& offset)
 {
-	_scale = scale;
-	_offset = offset;
+	// Disable sticky scrolling / always zoom to fit when acquisition runs
+	// and user drags the viewport
+	if ((scale_ == scale) && (offset_ != offset) &&
+			(session_.get_capture_state() == Session::Running)) {
+
+		if (sticky_scrolling_) {
+			sticky_scrolling_ = false;
+			sticky_scrolling_changed(false);
+		}
+
+		if (always_zoom_to_fit_) {
+			always_zoom_to_fit_ = false;
+			always_zoom_to_fit_changed(false);
+		}
+	}
+
+	set_scale(scale);
+	set_offset(offset);
+
+	calculate_tick_spacing();
 
 	update_scroll();
-	_ruler->update();
-	_viewport->update();
-	scale_offset_changed();
+	ruler_->update();
+	viewport_->update();
 }
 
-vector< shared_ptr<Trace> > View::get_traces() const
+set< shared_ptr<SignalData> > View::get_visible_data() 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
+	const unordered_set< shared_ptr<Signal> > sigs(session().signals());
 
-	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
+	// Make a set of all the visible data objects
+	set< shared_ptr<SignalData> > visible_data;
+	for (const shared_ptr<Signal> sig : sigs)
+		if (sig->enabled())
+			visible_data.insert(sig->data());
 
-	stable_sort(traces.begin(), traces.end(), compare_trace_v_offsets);
-	return traces;
+	return visible_data;
 }
 
-list<weak_ptr<SelectableItem> > View::selected_items() const
+pair<Timestamp, Timestamp> View::get_time_extents() 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);
+	boost::optional<Timestamp> left_time, right_time;
+	const set< shared_ptr<SignalData> > visible_data = get_visible_data();
+	for (const shared_ptr<SignalData> d : visible_data) {
+		const vector< shared_ptr<Segment> > segments =
+			d->segments();
+		for (const shared_ptr<Segment> &s : segments) {
+			double samplerate = s->samplerate();
+			samplerate = (samplerate <= 0.0) ? 1.0 : samplerate;
+
+			const Timestamp start_time = s->start_time();
+			left_time = left_time ?
+				min(*left_time, start_time) :
+				                start_time;
+			right_time = right_time ?
+				max(*right_time, start_time + d->max_sample_count() / samplerate) :
+				                 start_time + d->max_sample_count() / samplerate;
+		}
 	}
 
-	// List the selected cursors
-	if (_cursors.first()->selected())
-		items.push_back(_cursors.first());
-	if (_cursors.second()->selected())
-		items.push_back(_cursors.second());
+	if (!left_time || !right_time)
+		return make_pair(0, 0);
 
-	return items;
+	assert(*left_time < *right_time);
+	return make_pair(*left_time, *right_time);
 }
 
-set< shared_ptr<SignalData> > View::get_visible_data() const
+void View::enable_sticky_scrolling(bool state)
 {
-	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;
+	sticky_scrolling_ = state;
 }
 
-pair<double, double> View::get_time_extents() const
+void View::enable_coloured_bg(bool state)
 {
-	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);
+	const vector<shared_ptr<TraceTreeItem>> items(
+		list_by_type<TraceTreeItem>());
+
+	for (shared_ptr<TraceTreeItem> i : items) {
+		// Can't cast to Trace because it's abstract, so we need to
+		// check for any derived classes individually
+
+		shared_ptr<AnalogSignal> a = dynamic_pointer_cast<AnalogSignal>(i);
+		if (a)
+			a->set_coloured_bg(state);
+
+		shared_ptr<LogicSignal> l = dynamic_pointer_cast<LogicSignal>(i);
+		if (l)
+			l->set_coloured_bg(state);
+
+		shared_ptr<DecodeTrace> d = dynamic_pointer_cast<DecodeTrace>(i);
+		if (d)
+			d->set_coloured_bg(state);
 	}
 
-	assert(left_time < right_time);
-	return make_pair(left_time, right_time);
+	viewport_->update();
 }
 
 bool View::cursors_shown() const
 {
-	return _show_cursors;
+	return show_cursors_;
 }
 
 void View::show_cursors(bool show)
 {
-	_show_cursors = show;
-	_ruler->update();
-	_viewport->update();
+	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();
+	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()
+std::shared_ptr<CursorPair> View::cursors() const
 {
-	return _cursors;
+	return cursors_;
 }
 
-const CursorPair& View::cursors() const
+void View::add_flag(const Timestamp& time)
 {
-	return _cursors;
+	flags_.push_back(shared_ptr<Flag>(new Flag(*this, time,
+		QString("%1").arg(next_flag_text_))));
+
+	next_flag_text_ = (next_flag_text_ >= 'Z') ? 'A' :
+		(next_flag_text_ + 1);
+
+	time_item_appearance_changed(true, true);
 }
 
-const QPoint& View::hover_point() const
+void View::remove_flag(std::shared_ptr<Flag> flag)
 {
-	return _hover_point;
+	flags_.remove(flag);
+	time_item_appearance_changed(true, true);
 }
 
-void View::normalize_layout()
+vector< std::shared_ptr<Flag> > View::flags() const
 {
-	const vector< shared_ptr<Trace> > traces(get_traces());
+	vector< std::shared_ptr<Flag> > flags(flags_.begin(), flags_.end());
+	stable_sort(flags.begin(), flags.end(),
+		[](const shared_ptr<Flag> &a, const shared_ptr<Flag> &b) {
+			return a->time() < b->time();
+		});
 
-	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);
+	return flags;
+}
 
-	verticalScrollBar()->setSliderPosition(_v_offset + delta);
-	v_scroll_value_changed(verticalScrollBar()->sliderPosition());
+const QPoint& View::hover_point() const
+{
+	return hover_point_;
 }
 
 void View::update_viewport()
 {
-	assert(_viewport);
-	_viewport->update();
+	assert(viewport_);
+	viewport_->update();
+	header_->update();
+}
+
+void View::restack_all_trace_tree_items()
+{
+	// Make a list of owners that is sorted from deepest first
+	const vector<shared_ptr<TraceTreeItem>> items(
+		list_by_type<TraceTreeItem>());
+	set< TraceTreeItemOwner* > owners;
+	for (const auto &r : items)
+		owners.insert(r->owner());
+	vector< TraceTreeItemOwner* > sorted_owners(owners.begin(), owners.end());
+	sort(sorted_owners.begin(), sorted_owners.end(),
+		[](const TraceTreeItemOwner* a, const TraceTreeItemOwner *b) {
+			return a->depth() > b->depth(); });
+
+	// Restack the items recursively
+	for (auto &o : sorted_owners)
+		o->restack_items();
+
+	// Re-assign background colors
+	bool next_bgcolour_state = 0;
+
+	for (auto &o : sorted_owners)
+		next_bgcolour_state = o->reassign_bgcolour_states(next_bgcolour_state);
+
+	// Animate the items to their destination
+	for (const auto &i : items)
+		i->animate_to_layout_v_offset();
+}
+
+void View::trigger_event(util::Timestamp location)
+{
+	trigger_markers_.push_back(shared_ptr<TriggerMarker>(
+		new TriggerMarker(*this, location)));
 }
 
-void View::get_scroll_layout(double &length, double &offset) const
+void View::get_scroll_layout(double &length, Timestamp &offset) const
 {
-	const pair<double, double> extents = get_time_extents();
-	length = (extents.second - extents.first) / _scale;
-	offset = _offset / _scale;
+	const pair<Timestamp, Timestamp> extents = get_time_extents();
+	length = ((extents.second - extents.first) / scale_).convert_to<double>();
+	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);
+	// Reset the "always zoom to fit" feature as the user changed the zoom
+	always_zoom_to_fit_ = false;
+	always_zoom_to_fit_changed(false);
+
+	const Timestamp cursor_offset = offset_ + scale_ * offset;
+	const Timestamp new_scale = max(min(Timestamp(scale), MaxScale), MinScale);
+	const Timestamp new_offset = cursor_offset - new_scale * offset;
+	set_scale_offset(new_scale.convert_to<double>(), new_offset);
+}
+
+void View::calculate_tick_spacing()
+{
+	const double SpacingIncrement = 10.0f;
+	const double MinValueSpacing = 40.0f;
+
+	// Figure out the highest numeric value visible on a label
+	const QSize areaSize = viewport_->size();
+	const Timestamp max_time = max(fabs(offset_),
+		fabs(offset_ + scale_ * areaSize.width()));
+
+	double min_width = SpacingIncrement;
+	double label_width, tick_period_width;
+
+	QFontMetrics m(QApplication::font());
+
+	// Copies of the member variables with the same name, used in the calculation
+	// and written back afterwards, so that we don't emit signals all the time
+	// during the calculation.
+	pv::util::Timestamp tick_period = tick_period_;
+	pv::util::SIPrefix tick_prefix = tick_prefix_;
+	unsigned tick_precision = tick_precision_;
+
+	do {
+		const double min_period = scale_ * min_width;
+
+		const int order = (int)floorf(log10f(min_period));
+		const pv::util::Timestamp order_decimal =
+			pow(pv::util::Timestamp(10), order);
+
+		// Allow for a margin of error so that a scale unit of 1 can be used.
+		// Otherwise, for a SU of 1 the tick period will almost always be below
+		// the min_period by a small amount - and thus skipped in favor of 2.
+		// Note: margin assumes that SU[0] and SU[1] contain the smallest values
+		double tp_margin = (ScaleUnits[0] + ScaleUnits[1]) / 2.0;
+		double tp_with_margin;
+		unsigned int unit = 0;
+
+		do {
+			tp_with_margin = order_decimal.convert_to<double>() *
+				(ScaleUnits[unit++] + tp_margin);
+		} while (tp_with_margin < min_period && unit < countof(ScaleUnits));
+
+		tick_period = order_decimal * ScaleUnits[unit - 1];
+		tick_prefix = static_cast<pv::util::SIPrefix>(
+			(order - pv::util::exponent(pv::util::SIPrefix::yocto)) / 3);
+
+		// Precision is the number of fractional digits required, not
+		// taking the prefix into account (and it must never be negative)
+		tick_precision = std::max(ceil(log10(1 / tick_period)).convert_to<int>(), 0);
+
+		tick_period_width = (tick_period / scale_).convert_to<double>();
+
+		const QString label_text = Ruler::format_time_with_distance(
+			tick_period, max_time, tick_prefix, time_unit_, tick_precision);
+
+		label_width = m.boundingRect(0, 0, INT_MAX, INT_MAX,
+			Qt::AlignLeft | Qt::AlignTop, label_text).width() +
+				MinValueSpacing;
+
+		min_width += SpacingIncrement;
+	} while (tick_period_width < label_width);
+
+	set_tick_period(tick_period);
+	set_tick_prefix(tick_prefix);
+	set_tick_precision(tick_precision);
 }
 
 void View::update_scroll()
 {
-	assert(_viewport);
+	assert(viewport_);
 
-	const QSize areaSize = _viewport->size();
+	const QSize areaSize = viewport_->size();
 
 	// Set the horizontal scroll bar
-	double length = 0, offset = 0;
+	double length = 0;
+	Timestamp offset;
 	get_scroll_layout(length, offset);
 	length = max(length - areaSize.width(), 0.0);
 
+	int major_tick_distance = (tick_period_ / scale_).convert_to<int>();
+
 	horizontalScrollBar()->setPageStep(areaSize.width() / 2);
+	horizontalScrollBar()->setSingleStep(major_tick_distance);
 
-	_updating_scroll = true;
+	updating_scroll_ = true;
 
 	if (length < MaxScrollValue) {
 		horizontalScrollBar()->setRange(0, length);
-		horizontalScrollBar()->setSliderPosition(offset);
+		horizontalScrollBar()->setSliderPosition(offset.convert_to<double>());
 	} else {
 		horizontalScrollBar()->setRange(0, MaxScrollValue);
 		horizontalScrollBar()->setSliderPosition(
-			_offset * MaxScrollValue / (_scale * length));
+			(offset_ * MaxScrollValue / (scale_ * length)).convert_to<double>());
 	}
 
-	_updating_scroll = false;
+	updating_scroll_ = false;
 
 	// Set the vertical scrollbar
 	verticalScrollBar()->setPageStep(areaSize.height());
-	verticalScrollBar()->setRange(0,
-		_viewport->get_total_height() + SignalMargin -
-		areaSize.height());
+	verticalScrollBar()->setSingleStep(areaSize.height() / 8);
+
+	const pair<int, int> extents = v_extents();
+	verticalScrollBar()->setRange(extents.first - (areaSize.height() / 2),
+		extents.second - (areaSize.height() / 2));
 }
 
 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());
+	setViewportMargins(
+		header_->sizeHint().width() - pv::view::Header::BaselineOffset,
+		ruler_->sizeHint().height(), 0, 0);
+	ruler_->setGeometry(viewport_->x(), 0,
+		viewport_->width(), ruler_->extended_size_hint().height());
+	header_->setGeometry(0, viewport_->y(),
+		header_->extended_size_hint().width(), viewport_->height());
 	update_scroll();
 }
 
-bool View::compare_trace_v_offsets(const shared_ptr<Trace> &a,
-	const shared_ptr<Trace> &b)
+void View::paint_label(QPainter &p, const QRect &rect, bool hover)
+{
+	(void)p;
+	(void)rect;
+	(void)hover;
+}
+
+QRectF View::label_rect(const QRectF &rect)
+{
+	(void)rect;
+	return QRectF();
+}
+
+TraceTreeItemOwner* View::find_prevalent_trace_group(
+	const shared_ptr<sigrok::ChannelGroup> &group,
+	const unordered_map<shared_ptr<sigrok::Channel>, shared_ptr<Signal> >
+		&signal_map)
+{
+	assert(group);
+
+	unordered_set<TraceTreeItemOwner*> owners;
+	vector<TraceTreeItemOwner*> owner_list;
+
+	// Make a set and a list of all the owners
+	for (const auto &channel : group->channels()) {
+		const auto iter = signal_map.find(channel);
+		if (iter == signal_map.end())
+			continue;
+
+		TraceTreeItemOwner *const o = (*iter).second->owner();
+		owner_list.push_back(o);
+		owners.insert(o);
+	}
+
+	// Iterate through the list of owners, and find the most prevalent
+	size_t max_prevalence = 0;
+	TraceTreeItemOwner *prevalent_owner = nullptr;
+	for (TraceTreeItemOwner *owner : owners) {
+		const size_t prevalence = std::count_if(
+			owner_list.begin(), owner_list.end(),
+			[&](TraceTreeItemOwner *o) { return o == owner; });
+		if (prevalence > max_prevalence) {
+			max_prevalence = prevalence;
+			prevalent_owner = owner;
+		}
+	}
+
+	return prevalent_owner;
+}
+
+vector< shared_ptr<Trace> > View::extract_new_traces_for_channels(
+	const vector< shared_ptr<sigrok::Channel> > &channels,
+	const unordered_map<shared_ptr<sigrok::Channel>, shared_ptr<Signal> >
+		&signal_map,
+	set< shared_ptr<Trace> > &add_list)
+{
+	vector< shared_ptr<Trace> > filtered_traces;
+
+	for (const auto &channel : channels) {
+		const auto map_iter = signal_map.find(channel);
+		if (map_iter == signal_map.end())
+			continue;
+
+		shared_ptr<Trace> trace = (*map_iter).second;
+		const auto list_iter = add_list.find(trace);
+		if (list_iter == add_list.end())
+			continue;
+
+		filtered_traces.push_back(trace);
+		add_list.erase(list_iter);
+	}
+
+	return filtered_traces;
+}
+
+void View::determine_time_unit()
 {
-	assert(a);
-	assert(b);
-	return a->get_v_offset() < b->get_v_offset();
+	// Check whether we know the sample rate and hence can use time as the unit
+	if (time_unit_ == util::TimeUnit::Samples) {
+		const unordered_set< shared_ptr<Signal> > sigs(session().signals());
+
+		// Check all signals but...
+		for (const shared_ptr<Signal> signal : sigs) {
+			const shared_ptr<SignalData> data = signal->data();
+
+			// ...only check first segment of each
+			const vector< shared_ptr<Segment> > segments = data->segments();
+			if (!segments.empty())
+				if (segments[0]->samplerate()) {
+					set_time_unit(util::TimeUnit::Time);
+					break;
+				}
+		}
+	}
 }
 
 bool View::eventFilter(QObject *object, QEvent *event)
@@ -430,19 +802,19 @@ bool View::eventFilter(QObject *object, QEvent *event)
 	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());
+		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_ = QPoint(-1, -1);
 
 		hover_point_changed();
 
 	} else if (type == QEvent::Leave) {
-		_hover_point = QPoint(-1, -1);
+		hover_point_ = QPoint(-1, -1);
 		hover_point_changed();
 	}
 
@@ -451,15 +823,17 @@ bool View::eventFilter(QObject *object, QEvent *event)
 
 bool View::viewportEvent(QEvent *e)
 {
-	switch(e->type()) {
+	switch (e->type()) {
 	case QEvent::Paint:
 	case QEvent::MouseButtonPress:
 	case QEvent::MouseButtonRelease:
 	case QEvent::MouseButtonDblClick:
 	case QEvent::MouseMove:
 	case QEvent::Wheel:
+	case QEvent::TouchBegin:
+	case QEvent::TouchUpdate:
+	case QEvent::TouchEnd:
 		return false;
-
 	default:
 		return QAbstractScrollArea::viewportEvent(e);
 	}
@@ -470,69 +844,283 @@ void View::resizeEvent(QResizeEvent*)
 	update_layout();
 }
 
+void View::row_item_appearance_changed(bool label, bool content)
+{
+	if (label)
+		header_->update();
+	if (content)
+		viewport_->update();
+}
+
+void View::time_item_appearance_changed(bool label, bool content)
+{
+	if (label)
+		ruler_->update();
+	if (content)
+		viewport_->update();
+}
+
+void View::extents_changed(bool horz, bool vert)
+{
+	sticky_events_ |=
+		(horz ? TraceTreeItemHExtentsChanged : 0) |
+		(vert ? TraceTreeItemVExtentsChanged : 0);
+	lazy_event_handler_.start();
+}
+
 void View::h_scroll_value_changed(int value)
 {
-	if (_updating_scroll)
+	if (updating_scroll_)
 		return;
 
+	// Disable sticky scrolling when user moves the horizontal scroll bar
+	// during a running acquisition
+	if (sticky_scrolling_ && (session_.get_capture_state() == Session::Running)) {
+		sticky_scrolling_ = false;
+		sticky_scrolling_changed(false);
+	}
+
 	const int range = horizontalScrollBar()->maximum();
 	if (range < MaxScrollValue)
-		_offset = _scale * value;
+		set_offset(scale_ * value);
 	else {
-		double length = 0, offset;
+		double length = 0;
+		Timestamp offset;
 		get_scroll_layout(length, offset);
-		_offset = _scale * length * value / MaxScrollValue;
+		set_offset(scale_ * length * value / MaxScrollValue);
 	}
 
-	_ruler->update();
-	_viewport->update();
+	ruler_->update();
+	viewport_->update();
 }
 
-void View::v_scroll_value_changed(int value)
+void View::v_scroll_value_changed()
 {
-	_v_offset = value;
-	_header->update();
-	_viewport->update();
+	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;
+	using sigrok::Channel;
+
+	vector< shared_ptr<TraceTreeItem> > new_top_level_items;
+
+	const auto device = session_.device();
+	if (!device)
+		return;
+
+	shared_ptr<sigrok::Device> sr_dev = device->device();
+	assert(sr_dev);
+
+	const vector< shared_ptr<Channel> > channels(
+		sr_dev->channels());
+
+	// Make a list of traces that are being added, and a list of traces
+	// that are being removed
+	const vector<shared_ptr<Trace>> prev_trace_list = list_by_type<Trace>();
+	const set<shared_ptr<Trace>> prev_traces(
+		prev_trace_list.begin(), prev_trace_list.end());
+
+	const unordered_set< shared_ptr<Signal> > sigs(session_.signals());
+
+	set< shared_ptr<Trace> > traces(sigs.begin(), sigs.end());
+
+#ifdef ENABLE_DECODE
+	const vector< shared_ptr<DecodeTrace> > decode_traces(
+		session().get_decode_signals());
+	traces.insert(decode_traces.begin(), decode_traces.end());
+#endif
+
+	set< shared_ptr<Trace> > add_traces;
+	set_difference(traces.begin(), traces.end(),
+		prev_traces.begin(), prev_traces.end(),
+		inserter(add_traces, add_traces.begin()));
+
+	set< shared_ptr<Trace> > remove_traces;
+	set_difference(prev_traces.begin(), prev_traces.end(),
+		traces.begin(), traces.end(),
+		inserter(remove_traces, remove_traces.begin()));
+
+	// Make a look-up table of sigrok Channels to pulseview Signals
+	unordered_map<shared_ptr<sigrok::Channel>, shared_ptr<Signal> >
+		signal_map;
+	for (const shared_ptr<Signal> &sig : sigs)
+		signal_map[sig->channel()] = sig;
+
+	// Populate channel groups
+	for (auto entry : sr_dev->channel_groups()) {
+		const shared_ptr<sigrok::ChannelGroup> &group = entry.second;
+
+		if (group->channels().size() <= 1)
+			continue;
+
+		// Find best trace group to add to
+		TraceTreeItemOwner *owner = find_prevalent_trace_group(
+			group, signal_map);
+
+		// If there is no trace group, create one
+		shared_ptr<TraceGroup> new_trace_group;
+		if (!owner) {
+			new_trace_group.reset(new TraceGroup());
+			owner = new_trace_group.get();
+		}
+
+		// Extract traces for the trace group, removing them from
+		// the add list
+		const vector< shared_ptr<Trace> > new_traces_in_group =
+			extract_new_traces_for_channels(group->channels(),
+				signal_map, add_traces);
+
+		// Add the traces to the group
+		const pair<int, int> prev_v_extents = owner->v_extents();
+		int offset = prev_v_extents.second - prev_v_extents.first;
+		for (shared_ptr<Trace> trace : new_traces_in_group) {
+			assert(trace);
+			owner->add_child_item(trace);
+
+			const pair<int, int> extents = trace->v_extents();
+			if (trace->enabled())
+				offset += -extents.first;
+			trace->force_to_v_offset(offset);
+			if (trace->enabled())
+				offset += extents.second;
+		}
+
+		// If this is a new group, enqueue it in the new top level
+		// items list
+		if (!new_traces_in_group.empty() && new_trace_group)
+			new_top_level_items.push_back(new_trace_group);
+	}
+
+	// Enqueue the remaining logic channels in a group
+	vector< shared_ptr<Channel> > logic_channels;
+	copy_if(channels.begin(), channels.end(), back_inserter(logic_channels),
+		[](const shared_ptr<Channel>& c) {
+			return c->type() == sigrok::ChannelType::LOGIC; });
+	const vector< shared_ptr<Trace> > non_grouped_logic_signals =
+		extract_new_traces_for_channels(logic_channels,
+			signal_map, add_traces);
+	const shared_ptr<TraceGroup> non_grouped_trace_group(
+		make_shared<TraceGroup>());
+	for (shared_ptr<Trace> trace : non_grouped_logic_signals)
+		non_grouped_trace_group->add_child_item(trace);
+	new_top_level_items.push_back(non_grouped_trace_group);
+
+	// Enqueue the remaining channels as free ungrouped traces
+	const vector< shared_ptr<Trace> > new_top_level_signals =
+		extract_new_traces_for_channels(channels,
+			signal_map, add_traces);
+	new_top_level_items.insert(new_top_level_items.end(),
+		new_top_level_signals.begin(), new_top_level_signals.end());
+
+	// Enqueue any remaining traces i.e. decode traces
+	new_top_level_items.insert(new_top_level_items.end(),
+		add_traces.begin(), add_traces.end());
+
+	// Remove any removed traces
+	for (shared_ptr<Trace> trace : remove_traces) {
+		TraceTreeItemOwner *const owner = trace->owner();
+		assert(owner);
+		owner->remove_child_item(trace);
+	}
+
+	// Add and position the pending top levels items
+	for (auto item : new_top_level_items) {
+		add_child_item(item);
+
+		// Position the item after the last present item
+		int offset = v_extents().second;
+		const pair<int, int> extents = item->v_extents();
+		if (item->enabled())
+			offset += -extents.first;
+		item->force_to_v_offset(offset);
+		if (item->enabled())
+			offset += extents.second;
 	}
 
 	update_layout();
-	normalize_layout();
+
+	header_->update();
+	viewport_->update();
 }
 
-void View::data_updated()
+void View::capture_state_updated(int state)
 {
-	// Update the scroll bars
-	update_scroll();
+	if (state == Session::Running) {
+		set_time_unit(util::TimeUnit::Samples);
+
+		trigger_markers_.clear();
+	}
+
+	if (state == Session::Stopped) {
+		// After acquisition has stopped we need to re-calculate the ticks once
+		// as it's otherwise done when the user pans or zooms, which is too late
+		calculate_tick_spacing();
 
-	// Repaint the view
-	_viewport->update();
+		// Reset "always zoom to fit", the acquisition has stopped
+		if (always_zoom_to_fit_) {
+			always_zoom_to_fit_ = false;
+			always_zoom_to_fit_changed(false);
+		}
+	}
 }
 
-void View::marker_time_changed()
+void View::data_updated()
 {
-	_ruler->update();
-	_viewport->update();
+	if (always_zoom_to_fit_ || sticky_scrolling_) {
+		if (!delayed_view_updater_.isActive())
+			delayed_view_updater_.start();
+	} else {
+		determine_time_unit();
+		update_scroll();
+		ruler_->update();
+		viewport_->update();
+	}
 }
 
-void View::on_signals_moved()
+void View::perform_delayed_view_update()
 {
+	if (always_zoom_to_fit_)
+		zoom_fit(true);
+
+	if (sticky_scrolling_) {
+		// Make right side of the view sticky
+		double length = 0;
+		Timestamp offset;
+		get_scroll_layout(length, offset);
+
+		const QSize areaSize = viewport_->size();
+		length = max(length - areaSize.width(), 0.0);
+
+		set_offset(scale_ * length);
+	}
+
+	determine_time_unit();
 	update_scroll();
-	signals_moved();
+	ruler_->update();
+	viewport_->update();
 }
 
-void View::on_geometry_updated()
+void View::process_sticky_events()
 {
-	update_layout();
+	if (sticky_events_ & TraceTreeItemHExtentsChanged)
+		update_layout();
+	if (sticky_events_ & TraceTreeItemVExtentsChanged) {
+		restack_all_trace_tree_items();
+		update_scroll();
+	}
+
+	// Clear the sticky events
+	sticky_events_ = 0;
+}
+
+void View::on_hover_point_changed()
+{
+	const vector<shared_ptr<TraceTreeItem>> trace_tree_items(
+		list_by_type<TraceTreeItem>());
+	for (shared_ptr<TraceTreeItem> r : trace_tree_items)
+		r->hover_point_changed();
 }
 
 } // namespace view
diff --git a/pv/view/view.h b/pv/view/view.h
deleted file mode 100644
index 0661637..0000000
--- a/pv/view/view.h
+++ /dev/null
@@ -1,214 +0,0 @@
-/*
- * 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/view.hpp b/pv/view/view.hpp
new file mode 100644
index 0000000..635273d
--- /dev/null
+++ b/pv/view/view.hpp
@@ -0,0 +1,404 @@
+/*
+ * 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_HPP
+#define PULSEVIEW_PV_VIEW_VIEW_HPP
+
+#include <stdint.h>
+
+#include <list>
+#include <memory>
+#include <set>
+#include <unordered_map>
+#include <vector>
+
+#include <QAbstractScrollArea>
+#include <QSizeF>
+#include <QTimer>
+
+#include <pv/data/signaldata.hpp>
+#include <pv/util.hpp>
+
+#include "cursorpair.hpp"
+#include "flag.hpp"
+#include "tracetreeitemowner.hpp"
+
+namespace sigrok {
+class ChannelGroup;
+}
+
+namespace pv {
+
+class Session;
+
+namespace view {
+
+class CursorHeader;
+class Header;
+class Ruler;
+class Trace;
+class Viewport;
+class TriggerMarker;
+
+class View : public QAbstractScrollArea, public TraceTreeItemOwner {
+	Q_OBJECT
+
+private:
+	enum StickyEvents {
+		TraceTreeItemHExtentsChanged = 1,
+		TraceTreeItemVExtentsChanged = 2
+	};
+
+private:
+	static const pv::util::Timestamp MaxScale;
+	static const pv::util::Timestamp MinScale;
+
+	static const int MaxScrollValue;
+	static const int MaxViewAutoUpdateRate;
+
+	static const int ScaleUnits[3];
+
+public:
+	explicit View(Session &session, QWidget *parent = 0);
+
+	Session& session();
+	const Session& session() const;
+
+	/**
+	 * Returns the view of the owner.
+	 */
+	virtual pv::view::View* view();
+
+	/**
+	 * Returns the view of the owner.
+	 */
+	virtual const pv::view::View* view() const;
+
+	Viewport* viewport();
+
+	const Viewport* viewport() const;
+
+	/**
+	 * Gets a list of time markers.
+	 */
+	std::vector< std::shared_ptr<TimeItem> > time_items() 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.
+	 */
+	const pv::util::Timestamp& offset() const;
+
+	/**
+	 * Returns the vertical scroll offset.
+	 */
+	int owner_visual_v_offset() const;
+
+	/**
+	 * Sets the visual v-offset.
+	 */
+	void set_v_offset(int offset);
+
+	/**
+	 * Returns the SI prefix to apply to the graticule time markings.
+	 */
+	pv::util::SIPrefix tick_prefix() const;
+
+	/**
+	 * Returns the number of fractional digits shown for the time markings.
+	 */
+	unsigned int tick_precision() const;
+
+	/**
+	 * Returns period of the graticule time markings.
+	 */
+	const pv::util::Timestamp& tick_period() const;
+
+	/**
+	 * Returns the unit of time currently used.
+	 */
+	util::TimeUnit time_unit() const;
+
+	/**
+	 * Returns the number of nested parents that this row item owner has.
+	 */
+	unsigned int depth() const;
+
+	void zoom(double steps);
+	void zoom(double steps, int offset);
+
+	void zoom_fit(bool gui_state);
+
+	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, const pv::util::Timestamp& offset);
+
+	std::set< std::shared_ptr<pv::data::SignalData> >
+		get_visible_data() const;
+
+	std::pair<pv::util::Timestamp, pv::util::Timestamp> get_time_extents() const;
+
+	/**
+	 * Enables or disables sticky scrolling, i.e. the view always shows
+	 * the most recent samples when capturing data.
+	 */
+	void enable_sticky_scrolling(bool state);
+
+	/**
+	 * Enables or disables coloured trace backgrounds. If they're not
+	 * coloured then they will use alternating colors.
+	 */
+	void enable_coloured_bg(bool state);
+
+	/**
+	 * 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.
+	 */
+	std::shared_ptr<CursorPair> cursors() const;
+
+	/**
+	 * Adds a new flag at a specified time.
+	 */
+	void add_flag(const pv::util::Timestamp& time);
+
+	/**
+	 * Removes a flag from the list.
+	 */
+	void remove_flag(std::shared_ptr<Flag> flag);
+
+	/**
+	 * Gets the list of flags.
+	 */
+	std::vector< std::shared_ptr<Flag> > flags() const;
+
+	const QPoint& hover_point() const;
+
+	void update_viewport();
+
+	void restack_all_trace_tree_items();
+
+Q_SIGNALS:
+	void hover_point_changed();
+
+	void selection_changed();
+
+	/// Emitted when the offset changed.
+	void offset_changed();
+
+	/// Emitted when the scale changed.
+	void scale_changed();
+
+	void sticky_scrolling_changed(bool state);
+
+	void always_zoom_to_fit_changed(bool state);
+
+	/// Emitted when the tick_prefix changed.
+	void tick_prefix_changed();
+
+	/// Emitted when the tick_precision changed.
+	void tick_precision_changed();
+
+	/// Emitted when the tick_period changed.
+	void tick_period_changed();
+
+	/// Emitted when the time_unit changed.
+	void time_unit_changed();
+
+public Q_SLOTS:
+	void trigger_event(util::Timestamp location);
+
+private:
+	void get_scroll_layout(double &length, pv::util::Timestamp &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);
+
+	/**
+	 * Find a tick spacing and number formatting that does not cause
+	 * the values to collide.
+	 */
+	void calculate_tick_spacing();
+
+	void update_scroll();
+
+	void update_layout();
+
+	/**
+	 * Satisifies TraceTreeItem functionality.
+	 * @param p the QPainter to paint into.
+	 * @param rect the rectangle of the header area.
+	 * @param hover true if the label is being hovered over by the mouse.
+	 */
+	void paint_label(QPainter &p, const QRect &rect, bool hover);
+
+	/**
+	 * Computes the outline rectangle of a label.
+	 * @param rect the rectangle of the header area.
+	 * @return Returns the rectangle of the signal label.
+	 */
+	QRectF label_rect(const QRectF &rect);
+
+	TraceTreeItemOwner* find_prevalent_trace_group(
+		const std::shared_ptr<sigrok::ChannelGroup> &group,
+		const std::unordered_map<std::shared_ptr<sigrok::Channel>,
+			std::shared_ptr<Signal> > &signal_map);
+
+	static std::vector< std::shared_ptr<Trace> >
+		extract_new_traces_for_channels(
+		const std::vector< std::shared_ptr<sigrok::Channel> > &channels,
+		const std::unordered_map<std::shared_ptr<sigrok::Channel>,
+			std::shared_ptr<Signal> > &signal_map,
+		std::set< std::shared_ptr<Trace> > &add_list);
+
+	void determine_time_unit();
+
+	bool eventFilter(QObject *object, QEvent *event);
+
+	bool viewportEvent(QEvent *e);
+
+	void resizeEvent(QResizeEvent *e);
+
+public:
+	void row_item_appearance_changed(bool label, bool content);
+	void time_item_appearance_changed(bool label, bool content);
+
+	void extents_changed(bool horz, bool vert);
+
+private Q_SLOTS:
+
+	void h_scroll_value_changed(int value);
+	void v_scroll_value_changed();
+
+	void signals_changed();
+	void capture_state_updated(int state);
+	void data_updated();
+
+	void perform_delayed_view_update();
+
+	void process_sticky_events();
+
+	void on_hover_point_changed();
+
+	/**
+	 * Sets the 'offset_' member and emits the 'offset_changed'
+	 * signal if needed.
+	 */
+	void set_offset(const pv::util::Timestamp& offset);
+
+	/**
+	 * Sets the 'scale_' member and emits the 'scale_changed'
+	 * signal if needed.
+	 */
+	void set_scale(double scale);
+
+	/**
+	 * Sets the 'tick_prefix_' member and emits the 'tick_prefix_changed'
+	 * signal if needed.
+	 */
+	void set_tick_prefix(pv::util::SIPrefix tick_prefix);
+
+	/**
+	 * Sets the 'tick_precision_' member and emits the 'tick_precision_changed'
+	 * signal if needed.
+	 */
+	void set_tick_precision(unsigned tick_precision);
+
+	/**
+	 * Sets the 'tick_period_' member and emits the 'tick_period_changed'
+	 * signal if needed.
+	 */
+	void set_tick_period(const pv::util::Timestamp& tick_period);
+
+	/**
+	 * Sets the 'time_unit' member and emits the 'time_unit_changed'
+	 * signal if needed.
+	 */
+	void set_time_unit(pv::util::TimeUnit time_unit);
+
+private:
+	Session &session_;
+
+	Viewport *viewport_;
+	Ruler *ruler_;
+	Header *header_;
+
+	/// The view time scale in seconds per pixel.
+	double scale_;
+
+	/// The view time offset in seconds.
+	pv::util::Timestamp offset_;
+
+	bool updating_scroll_;
+	bool sticky_scrolling_;
+	bool always_zoom_to_fit_;
+	QTimer delayed_view_updater_;
+
+	pv::util::Timestamp tick_period_;
+	pv::util::SIPrefix tick_prefix_;
+	unsigned int tick_precision_;
+	util::TimeUnit time_unit_;
+
+	bool show_cursors_;
+	std::shared_ptr<CursorPair> cursors_;
+
+	std::list< std::shared_ptr<Flag> > flags_;
+	char next_flag_text_;
+
+	std::vector< std::shared_ptr<TriggerMarker> > trigger_markers_;
+
+	QPoint hover_point_;
+
+	unsigned int sticky_events_;
+	QTimer lazy_event_handler_;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_VIEW_HPP
diff --git a/pv/view/viewitem.cpp b/pv/view/viewitem.cpp
new file mode 100644
index 0000000..e9602f8
--- /dev/null
+++ b/pv/view/viewitem.cpp
@@ -0,0 +1,139 @@
+/*
+ * 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 "viewitem.hpp"
+
+#include <climits>
+
+#include <QApplication>
+#include <QMenu>
+#include <QPalette>
+
+namespace pv {
+namespace view {
+
+const QSizeF ViewItem::LabelPadding(4, 0);
+const int ViewItem::HighlightRadius = 3;
+
+ViewItem::ViewItem() :
+	context_parent_(nullptr),
+	drag_point_(INT_MIN, INT_MIN),
+	selected_(false)
+{
+}
+
+bool ViewItem::selected() const
+{
+	return selected_;
+}
+
+void ViewItem::select(bool select)
+{
+	selected_ = select;
+}
+
+bool ViewItem::is_draggable() const
+{
+	return true;
+}
+
+bool ViewItem::dragging() const
+{
+	return drag_point_.x() != INT_MIN && drag_point_.y() != INT_MIN;
+}
+
+void ViewItem::drag()
+{
+	if (is_draggable())
+		drag_point_ = point(QRect());
+}
+
+void ViewItem::drag_release()
+{
+	drag_point_ = QPoint(INT_MIN, INT_MIN);
+}
+
+QRectF ViewItem::label_rect(const QRectF &rect) const
+{
+	(void)rect;
+	return QRectF();
+}
+
+QRectF ViewItem::hit_box_rect(const ViewItemPaintParams &pp) const
+{
+	(void)pp;
+	return QRectF();
+}
+
+QMenu* ViewItem::create_context_menu(QWidget *parent)
+{
+	context_parent_ = parent;
+	return new QMenu(parent);
+}
+
+widgets::Popup* ViewItem::create_popup(QWidget *parent)
+{
+	(void)parent;
+	return nullptr;
+}
+
+void ViewItem::delete_pressed()
+{
+}
+
+QPen ViewItem::highlight_pen()
+{
+	return QPen(QApplication::palette().brush(
+		QPalette::Highlight), HighlightRadius * 2,
+		Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin);
+}
+
+void ViewItem::paint_label(QPainter &p, const QRect &rect, bool hover)
+{
+	(void)p;
+	(void)rect;
+	(void)hover;
+}
+
+void ViewItem::paint_back(QPainter &p, const ViewItemPaintParams &pp)
+{
+	(void)p;
+	(void)pp;
+}
+
+void ViewItem::paint_mid(QPainter &p, const ViewItemPaintParams &pp)
+{
+	(void)p;
+	(void)pp;
+}
+
+void ViewItem::paint_fore(QPainter &p, const ViewItemPaintParams &pp)
+{
+	(void)p;
+	(void)pp;
+}
+
+QColor ViewItem::select_text_colour(QColor background)
+{
+	return (background.lightness() > 110) ? Qt::black : Qt::white;
+}
+
+} // namespace view
+} // namespace pv
diff --git a/pv/view/viewitem.hpp b/pv/view/viewitem.hpp
new file mode 100644
index 0000000..a4eb6ae
--- /dev/null
+++ b/pv/view/viewitem.hpp
@@ -0,0 +1,178 @@
+/*
+ * 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_VIEWITEM_HPP
+#define PULSEVIEW_PV_VIEWITEM_HPP
+
+#include <list>
+
+#include <QPen>
+
+#include "viewitempaintparams.hpp"
+
+class QAction;
+class QMenu;
+class QWidget;
+
+namespace pv {
+
+namespace widgets {
+class Popup;
+}
+
+namespace view {
+
+class ViewItemOwner;
+
+class ViewItem : public QObject
+{
+	Q_OBJECT
+
+public:
+	static const QSizeF LabelPadding;
+	static const int HighlightRadius;
+
+public:
+	ViewItem();
+
+public:
+	/**
+	 * Returns true if the item is visible and enabled.
+	 */
+	virtual bool enabled() const = 0;
+
+	/**
+	 * Returns true if the item has been selected by the user.
+	 */
+	bool selected() const;
+
+	/**
+	 * Selects or deselects the signal.
+	 */
+	virtual void select(bool select = true);
+
+	/**
+	 * Returns true if the item may be dragged/moved.
+	 */
+	virtual bool is_draggable() const;
+
+	/**
+	 * Returns true if the item is being dragged.
+	 */
+	bool dragging() const;
+
+	/**
+	 * Sets this item into the dragged state.
+	 */
+	void drag();
+
+	/**
+	 * Sets this item into the un-dragged state.
+	 */
+	virtual void drag_release();
+
+	/**
+	 * Drags the item to a delta relative to the drag point.
+	 * @param delta the offset from the drag point.
+	 */
+	virtual void drag_by(const QPoint &delta) = 0;
+
+	/**
+	 * Get the drag point.
+	 * @param rect the rectangle of the widget area.
+	 */
+	virtual QPoint point(const QRect &rect) const = 0;
+
+	/**
+	 * Computes the outline rectangle of a label.
+	 * @param rect the rectangle of the header area.
+	 * @return Returns the rectangle of the signal label.
+	 * @remarks The default implementation returns an empty rectangle.
+	 */
+	virtual QRectF label_rect(const QRectF &rect) const;
+
+	/**
+	 * Computes the outline rectangle of the viewport hit-box.
+	 * @param rect the rectangle of the viewport area.
+	 * @return Returns the rectangle of the hit-box.
+	 * @remarks The default implementation returns an empty hit-box.
+	 */
+	virtual QRectF hit_box_rect(const ViewItemPaintParams &pp) const;
+
+	/**
+	 * Paints the signal label.
+	 * @param p the QPainter to paint into.
+	 * @param rect the rectangle of the header area.
+	 * @param hover true if the label is being hovered over by the mouse.
+	 */
+	virtual void paint_label(QPainter &p, const QRect &rect, bool hover);
+
+	/**
+	 * Paints the background layer of the item with a QPainter
+	 * @param p the QPainter to paint into.
+	 * @param pp the painting parameters object to paint with.
+	 */
+	virtual void paint_back(QPainter &p, const ViewItemPaintParams &pp);
+
+	/**
+	 * Paints the mid-layer of the item with a QPainter
+	 * @param p the QPainter to paint into.
+	 * @param pp the painting parameters object to paint with.
+	 */
+	virtual void paint_mid(QPainter &p, const ViewItemPaintParams &pp);
+
+	/**
+	 * Paints the foreground layer of the item with a QPainter
+	 * @param p the QPainter to paint into.
+	 * @param pp the painting parameters object to paint with.
+	 */
+	virtual void paint_fore(QPainter &p, const ViewItemPaintParams &pp);
+
+public:
+	/**
+	 * 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.
+	 */
+	static QColor select_text_colour(QColor background);
+
+public:
+	virtual QMenu* create_context_menu(QWidget *parent);
+
+	virtual pv::widgets::Popup* create_popup(QWidget *parent);
+
+	virtual void delete_pressed();
+
+protected:
+	static QPen highlight_pen();
+
+protected:
+	QWidget *context_parent_;
+	QPoint drag_point_;
+
+private:
+	bool selected_;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEWITEM_HPP
diff --git a/pv/view/viewitemiterator.hpp b/pv/view/viewitemiterator.hpp
new file mode 100644
index 0000000..eed67a9
--- /dev/null
+++ b/pv/view/viewitemiterator.hpp
@@ -0,0 +1,129 @@
+/*
+ * 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_VIEW_VIEWITEMITERATOR_HPP
+#define PULSEVIEW_PV_VIEW_VIEWITEMITERATOR_HPP
+
+#include <algorithm>
+#include <cassert>
+#include <iterator>
+#include <memory>
+#include <stack>
+#include <type_traits>
+#include <vector>
+
+#include <pv/session.hpp>
+
+namespace pv {
+namespace view {
+
+template<class Owner, class Item> class ViewItemIterator
+{
+public:
+	typedef typename Owner::item_list::const_iterator child_iterator;
+	typedef std::shared_ptr<Item> value_type;
+	typedef ptrdiff_t difference_type;
+	typedef value_type pointer;
+	typedef const value_type& reference;
+	typedef std::forward_iterator_tag iterator_category;
+
+public:
+	ViewItemIterator(Owner *owner) :
+		owner_stack_({owner}) {}
+
+	ViewItemIterator(Owner *owner, child_iterator iter) :
+		owner_stack_({owner}) {
+		assert(owner);
+		if (iter != owner->child_items().end())
+			iter_stack_.push(iter);
+	}
+
+	ViewItemIterator(const ViewItemIterator<Owner, Item> &o) :
+		owner_stack_(o.owner_stack_),
+		iter_stack_(o.iter_stack_) {}
+
+	reference operator*() const {
+		return *iter_stack_.top();
+	}
+
+	reference operator->() const {
+		return *this;
+	}
+
+	ViewItemIterator<Owner, Item>& operator++() {
+		using std::dynamic_pointer_cast;
+		using std::shared_ptr;
+
+		assert(!owner_stack_.empty());
+		assert(!iter_stack_.empty());
+
+		shared_ptr<Owner> owner(dynamic_pointer_cast<Owner>(
+			*iter_stack_.top()));
+		if (owner && !owner->child_items().empty()) {
+			owner_stack_.push(owner.get());
+			iter_stack_.push(owner->child_items().begin());
+		} else {
+			while (!iter_stack_.empty() && (++iter_stack_.top()) ==
+				owner_stack_.top()->child_items().end()) {
+				owner_stack_.pop();
+				iter_stack_.pop();
+			}
+		}
+
+		return *this;
+	}
+
+	ViewItemIterator<Owner, Item> operator++(int) {
+		ViewItemIterator<Owner, Item> pre = *this;
+		++*this;
+		return pre;
+	}
+
+	bool operator==(const ViewItemIterator &o) const {
+		return (iter_stack_.empty() && o.iter_stack_.empty()) || (
+			iter_stack_.size() == o.iter_stack_.size() &&
+			owner_stack_.top() == o.owner_stack_.top() &&
+			iter_stack_.top() == o.iter_stack_.top());
+	}
+
+	bool operator!=(const ViewItemIterator &o) const {
+		return !((const ViewItemIterator&)*this == o);
+	}
+
+	void swap(ViewItemIterator<Owner, Item>& other) {
+		swap(owner_stack_, other.owner_stack_);
+		swap(iter_stack_, other.iter_stack_);
+	}
+
+private:
+	std::stack<Owner*> owner_stack_;
+	std::stack<child_iterator> iter_stack_;
+};
+
+template<class Owner, class Item>
+void swap(ViewItemIterator<Owner, Item>& a, ViewItemIterator<Owner, Item>& b)
+{
+	a.swap(b);
+}
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_VIEWITEMITERATOR_HPP
diff --git a/pv/data/decode/rowdata.cpp b/pv/view/viewitemowner.cpp
similarity index 58%
copy from pv/data/decode/rowdata.cpp
copy to pv/view/viewitemowner.cpp
index 87da27c..8a70478 100644
--- a/pv/data/decode/rowdata.cpp
+++ b/pv/view/viewitemowner.cpp
@@ -18,41 +18,43 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "rowdata.h"
-
+#include <cassert>
+
+#include "tracetreeitem.hpp"
+#include "tracetreeitemowner.hpp"
+#include "trace.hpp"
+
+using std::dynamic_pointer_cast;
+using std::max;
+using std::make_pair;
+using std::min;
+using std::pair;
+using std::set;
+using std::shared_ptr;
 using std::vector;
 
 namespace pv {
-namespace data {
-namespace decode {
+namespace view {
 
-RowData::RowData()
+ViewItemOwner::iterator ViewItemOwner::begin()
 {
+	return iterator(this, items_.begin());
 }
 
-uint64_t RowData::get_max_sample() const
+ViewItemOwner::iterator ViewItemOwner::end()
 {
-	if (_annotations.empty())
-		return 0;
-	return _annotations.back().end_sample();
+	return iterator(this);
 }
 
-void RowData::get_annotation_subset(
-	vector<pv::data::decode::Annotation> &dest,
-	uint64_t start_sample, uint64_t end_sample) const
+ViewItemOwner::const_iterator ViewItemOwner::begin() 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);
+	return const_iterator(this, items_.cbegin());
 }
 
-void RowData::push_annotation(const Annotation &a)
+ViewItemOwner::const_iterator ViewItemOwner::end() const
 {
-	_annotations.push_back(a);
+	return const_iterator(this);
 }
 
-} // decode
-} // data
+} // view
 } // pv
diff --git a/pv/view/viewitemowner.hpp b/pv/view/viewitemowner.hpp
new file mode 100644
index 0000000..8d34059
--- /dev/null
+++ b/pv/view/viewitemowner.hpp
@@ -0,0 +1,96 @@
+/*
+ * 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_VIEW_VIEWITEMOWNER_HPP
+#define PULSEVIEW_PV_VIEW_VIEWITEMOWNER_HPP
+
+#include <memory>
+#include <vector>
+
+#include "viewitemiterator.hpp"
+
+namespace pv {
+
+class Session;
+
+namespace view {
+
+class ViewItem;
+class View;
+
+class ViewItemOwner
+{
+public:
+	typedef std::vector< std::shared_ptr<ViewItem> > item_list;
+	typedef ViewItemIterator<ViewItemOwner, ViewItem> iterator;
+	typedef ViewItemIterator<const ViewItemOwner, ViewItem> const_iterator;
+
+public:
+	/**
+	 * Returns a list of row items owned by this object.
+	 */
+	virtual const item_list& child_items() const = 0;
+
+	/**
+	 * Returns a depth-first iterator at the beginning of the child ViewItem
+	 * tree.
+	 */
+	iterator begin();
+
+	/**
+	 * Returns a depth-first iterator at the end of the child ViewItem tree.
+	 */
+	iterator end();
+
+	/**
+	 * Returns a constant depth-first iterator at the beginning of the
+	 * child ViewItem tree.
+	 */
+	const_iterator begin() const;
+
+	/**
+	 * Returns a constant depth-first iterator at the end of the child
+	 * ViewItem tree.
+	 */
+	const_iterator end() const;
+
+	/**
+	 * Creates a list of decendant signals filtered by type.
+	 */
+	template<class T>
+	std::vector< std::shared_ptr<T> > list_by_type() {
+		std::vector< std::shared_ptr<T> > items;
+		for (const auto &r : *this) {
+			std::shared_ptr<T> p = std::dynamic_pointer_cast<T>(r);
+			if (p)
+				items.push_back(p);
+		}
+
+		return items;
+	}
+
+protected:
+	item_list items_;
+};
+
+} // view
+} // pv
+
+#endif // PULSEVIEW_PV_VIEW_VIEWITEMOWNER_HPP
diff --git a/pv/device/device.h b/pv/view/viewitempaintparams.cpp
similarity index 63%
rename from pv/device/device.h
rename to pv/view/viewitempaintparams.cpp
index b9e1729..998045c 100644
--- a/pv/device/device.h
+++ b/pv/view/viewitempaintparams.cpp
@@ -18,34 +18,34 @@
  * 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 <cassert>
 
-#include "devinst.h"
+#include <QApplication>
+#include <QFontMetrics>
+
+#include "viewitempaintparams.hpp"
 
 namespace pv {
-namespace device {
+namespace view {
 
-class Device : public DevInst
+ViewItemPaintParams::ViewItemPaintParams(
+	const QRect &rect, double scale, const pv::util::Timestamp& offset) :
+	rect_(rect),
+	scale_(scale),
+	offset_(offset)
 {
-public:
-	Device(sr_dev_inst *dev_inst);
-
-	sr_dev_inst* dev_inst() const;
-
-	void use(SigSession *owner) throw(QString);
-
-	void release();
+	assert(scale > 0.0);
+}
 
-	std::string format_device_title() const;
-
-	bool is_trigger_enabled() const;
-
-private:
-	sr_dev_inst *const _sdi;
-};
+QFont ViewItemPaintParams::font()
+{
+	return QApplication::font();
+}
 
-} // device
-} // pv
+int ViewItemPaintParams::text_height()
+{
+	return QFontMetrics(font()).height();
+}
 
-#endif // PULSVIEW_PV_DEVICE_DEVICE_H
+} // namespace view
+} // namespace pv
diff --git a/pv/view/viewitempaintparams.hpp b/pv/view/viewitempaintparams.hpp
new file mode 100644
index 0000000..bba0d70
--- /dev/null
+++ b/pv/view/viewitempaintparams.hpp
@@ -0,0 +1,92 @@
+/*
+ * 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_VIEW_VIEWITEMPAINTPARAMS_HPP
+#define PULSEVIEW_PV_VIEW_VIEWITEMPAINTPARAMS_HPP
+
+#include "pv/util.hpp"
+
+#include <QFont>
+#include <QRect>
+
+namespace pv {
+namespace view {
+
+class ViewItemPaintParams
+{
+public:
+	ViewItemPaintParams(
+		const QRect &rect, double scale, const pv::util::Timestamp& offset);
+
+	QRect rect() const {
+		return rect_;
+	}
+
+	double scale() const {
+		return scale_;
+	}
+
+	const pv::util::Timestamp& offset() const {
+		return offset_;
+	}
+
+	int left() const {
+		return rect_.left();
+	}
+
+	int right() const {
+		return rect_.right();
+	}
+
+	int top() const {
+		return rect_.top();
+	}
+
+	int bottom() const {
+		return rect_.bottom();
+	}
+
+	int width() const {
+		return rect_.width();
+	}
+
+	int height() const {
+		return rect_.height();
+	}
+
+	double pixels_offset() const {
+		return (offset_ / scale_).convert_to<double>();
+	}
+
+public:
+	static QFont font();
+
+	static int text_height();
+
+private:
+	QRect rect_;
+	double scale_;
+	pv::util::Timestamp offset_;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_VIEWITEMPAINTPARAMS_HPP
diff --git a/pv/view/viewport.cpp b/pv/view/viewport.cpp
index 3b06cf9..9e13666 100644
--- a/pv/view/viewport.cpp
+++ b/pv/view/viewport.cpp
@@ -18,103 +18,178 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "view.h"
-#include "viewport.h"
+#include <cassert>
+#include <cmath>
+#include <algorithm>
+#include <limits>
 
-#include "signal.h"
-#include "../sigsession.h"
+#include "signal.hpp"
+#include "view.hpp"
+#include "viewitempaintparams.hpp"
+#include "viewport.hpp"
 
-#include <QMouseEvent>
+#include <pv/session.hpp>
 
-#include <boost/foreach.hpp>
+#include <QMouseEvent>
 
-using boost::shared_ptr;
+using std::abs;
+using std::back_inserter;
+using std::copy;
+using std::dynamic_pointer_cast;
 using std::max;
 using std::min;
+using std::none_of;
+using std::numeric_limits;
+using std::shared_ptr;
+using std::stable_sort;
 using std::vector;
 
 namespace pv {
 namespace view {
 
 Viewport::Viewport(View &parent) :
-	QWidget(&parent),
-        _view(parent)
+	ViewWidget(parent),
+	pinch_zoom_active_(false)
 {
-	setMouseTracking(true);
 	setAutoFillBackground(true);
 	setBackgroundRole(QPalette::Base);
+}
 
-	connect(&_view.session(), SIGNAL(signals_changed()),
-		this, SLOT(on_signals_changed()));
+shared_ptr<ViewItem> Viewport::get_mouse_over_item(const QPoint &pt)
+{
+	const ViewItemPaintParams pp(rect(), view_.scale(), view_.offset());
+	const vector< shared_ptr<ViewItem> > items(this->items());
+	for (auto i = items.rbegin(); i != items.rend(); i++)
+		if ((*i)->enabled() &&
+			(*i)->hit_box_rect(pp).contains(pt))
+			return *i;
+	return nullptr;
+}
 
-	connect(&_view, SIGNAL(signals_moved()),
-		this, SLOT(on_signals_moved()));
+void Viewport::item_hover(const shared_ptr<ViewItem> &item)
+{
+	if (item && item->is_draggable())
+		setCursor(dynamic_pointer_cast<RowItem>(item) ?
+			Qt::SizeVerCursor : Qt::SizeHorCursor);
+	else
+		unsetCursor();
+}
 
-	// Trigger the initial event manually. The default device has signals
-	// which were created before this object came into being
-	on_signals_changed();
+void Viewport::drag()
+{
+	drag_offset_ = view_.offset();
+	drag_v_offset_ = view_.owner_visual_v_offset();
 }
 
-int Viewport::get_total_height() const
+void Viewport::drag_by(const QPoint &delta)
 {
-	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);
-	}
+	if (drag_offset_ == boost::none)
+		return;
 
-	return h;
+	view_.set_scale_offset(view_.scale(),
+		(*drag_offset_ - delta.x() * view_.scale()));
+
+	view_.set_v_offset(-drag_v_offset_ - delta.y());
 }
 
-void Viewport::paintEvent(QPaintEvent*)
+void Viewport::drag_release()
 {
-	const vector< shared_ptr<Trace> > traces(_view.get_traces());
+	drag_offset_ = boost::none;
+}
 
-	QPainter p(this);
-	p.setRenderHint(QPainter::Antialiasing);
+vector< shared_ptr<ViewItem> > Viewport::items()
+{
+	vector< shared_ptr<ViewItem> > items;
+	const std::vector< shared_ptr<ViewItem> > view_items(
+		view_.list_by_type<ViewItem>());
+	copy(view_items.begin(), view_items.end(), back_inserter(items));
+	const vector< shared_ptr<TimeItem> > time_items(view_.time_items());
+	copy(time_items.begin(), time_items.end(), back_inserter(items));
+	return items;
+}
 
-	if (_view.cursors_shown())
-		_view.cursors().draw_viewport_background(p, rect());
+bool Viewport::touch_event(QTouchEvent *event)
+{
+	QList<QTouchEvent::TouchPoint> touchPoints = event->touchPoints();
 
-	// Plot the signal
-	BOOST_FOREACH(const shared_ptr<Trace> t, traces)
-	{
-		assert(t);
-		t->paint_back(p, 0, width());
+	if (touchPoints.count() != 2) {
+		pinch_zoom_active_ = false;
+		return false;
 	}
 
-	BOOST_FOREACH(const shared_ptr<Trace> t, traces)
-		t->paint_mid(p, 0, width());
+	const QTouchEvent::TouchPoint &touchPoint0 = touchPoints.first();
+	const QTouchEvent::TouchPoint &touchPoint1 = touchPoints.last();
+
+	if (!pinch_zoom_active_ ||
+	    (event->touchPointStates() & Qt::TouchPointPressed)) {
+		pinch_offset0_ = (view_.offset() + view_.scale() * touchPoint0.pos().x()).convert_to<double>();
+		pinch_offset1_ = (view_.offset() + view_.scale() * touchPoint1.pos().x()).convert_to<double>();
+		pinch_zoom_active_ = true;
+	}
 
-	BOOST_FOREACH(const shared_ptr<Trace> t, traces)
-		t->paint_fore(p, 0, width());
+	double w = touchPoint1.pos().x() - touchPoint0.pos().x();
+	if (abs(w) >= 1.0) {
+		const double scale =
+			fabs((pinch_offset1_ - pinch_offset0_) / w);
+		double offset = pinch_offset0_ - touchPoint0.pos().x() * scale;
+		if (scale > 0)
+			view_.set_scale_offset(scale, offset);
+	}
 
-	if (_view.cursors_shown())
-		_view.cursors().draw_viewport_foreground(p, rect());
+	if (event->touchPointStates() & Qt::TouchPointReleased) {
+		pinch_zoom_active_ = false;
+
+		if (touchPoint0.state() & Qt::TouchPointReleased) {
+			// Primary touch released
+			drag_release();
+		} else {
+			// Update the mouse down fields so that continued
+			// dragging with the primary touch will work correctly
+			mouse_down_point_ = touchPoint0.pos().toPoint();
+			drag();
+		}
+	}
 
-	p.end();
+	return true;
 }
 
-void Viewport::mousePressEvent(QMouseEvent *event)
+void Viewport::paintEvent(QPaintEvent*)
 {
-	assert(event);
+	vector< shared_ptr<RowItem> > row_items(view_.list_by_type<RowItem>());
+	assert(none_of(row_items.begin(), row_items.end(),
+		[](const shared_ptr<RowItem> &r) { return !r; }));
 
-	_mouse_down_point = event->pos();
-	_mouse_down_offset = _view.offset();
-}
+	stable_sort(row_items.begin(), row_items.end(),
+		[](const shared_ptr<RowItem> &a, const shared_ptr<RowItem> &b) {
+			return a->point(QRect()).y() < b->point(QRect()).y(); });
 
-void Viewport::mouseMoveEvent(QMouseEvent *event)
-{
-	assert(event);
+	const vector< shared_ptr<TimeItem> > time_items(view_.time_items());
+	assert(none_of(time_items.begin(), time_items.end(),
+		[](const shared_ptr<TimeItem> &t) { return !t; }));
 
-	if (event->buttons() & Qt::LeftButton)
-	{
-		_view.set_scale_offset(_view.scale(),
-			_mouse_down_offset +
-			(_mouse_down_point - event->pos()).x() *
-			_view.scale());
-	}
+	QPainter p(this);
+	p.setRenderHint(QPainter::Antialiasing);
+
+	const ViewItemPaintParams pp(rect(), view_.scale(), view_.offset());
+
+	for (const shared_ptr<TimeItem> t : time_items)
+		t->paint_back(p, pp);
+	for (const shared_ptr<RowItem> r : row_items)
+		r->paint_back(p, pp);
+
+	for (const shared_ptr<TimeItem> t : time_items)
+		t->paint_mid(p, pp);
+	for (const shared_ptr<RowItem> r : row_items)
+		r->paint_mid(p, pp);
+
+	for (const shared_ptr<RowItem> r : row_items)
+		r->paint_fore(p, pp);
+
+	p.setRenderHint(QPainter::Antialiasing, false);
+	for (const shared_ptr<TimeItem> t : time_items)
+		t->paint_fore(p, pp);
+
+	p.end();
 }
 
 void Viewport::mouseDoubleClickEvent(QMouseEvent *event)
@@ -122,40 +197,31 @@ void Viewport::mouseDoubleClickEvent(QMouseEvent *event)
 	assert(event);
 
 	if (event->buttons() & Qt::LeftButton)
-		_view.zoom(2.0, event->x());
+		view_.zoom(2.0, event->x());
 	else if (event->buttons() & Qt::RightButton)
-		_view.zoom(-2.0, event->x());
+		view_.zoom(-2.0, event->x());
 }
 
-void Viewport::wheelEvent(QWheelEvent *event)
+void Viewport::wheelEvent(QWheelEvent *e)
 {
-	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) {
+	assert(e);
+
+	if (e->orientation() == Qt::Vertical) {
+		if (e->modifiers() & Qt::ControlModifier) {
+			// Vertical scrolling with the control key pressed
+			// is intrepretted as vertical scrolling
+			view_.set_v_offset(-view_.owner_visual_v_offset() -
+				(e->delta() * height()) / (8 * 120));
+		} else {
+			// Vertical scrolling is interpreted as zooming in/out
+			view_.zoom(e->delta() / 120, e->x());
+		}
+	} else if (e->orientation() == Qt::Horizontal) {
 		// Horizontal scrolling is interpreted as moving left/right
-		_view.set_scale_offset(_view.scale(),
-				       event->delta() * _view.scale()
-				       + _view.offset());
+		view_.set_scale_offset(view_.scale(),
+			e->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.hpp b/pv/view/viewport.hpp
new file mode 100644
index 0000000..704e731
--- /dev/null
+++ b/pv/view/viewport.hpp
@@ -0,0 +1,110 @@
+/*
+ * 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_HPP
+#define PULSEVIEW_PV_VIEW_VIEWPORT_HPP
+
+#include <boost/optional.hpp>
+
+#include <QTimer>
+#include <QTouchEvent>
+
+#include "pv/util.hpp"
+#include "viewwidget.hpp"
+
+class QPainter;
+class QPaintEvent;
+class Session;
+
+namespace pv {
+namespace view {
+
+class View;
+
+class Viewport : public ViewWidget
+{
+	Q_OBJECT
+
+public:
+	explicit Viewport(View &parent);
+
+private:
+	/**
+	 * Indicates when a view item is being hovered over.
+	 * @param item The item that is being hovered over, or @c nullptr
+	 * if no view item is being hovered over.
+	 */
+	void item_hover(const std::shared_ptr<pv::view::ViewItem> &item);
+
+	/**
+	 * Gets the first view item which has a hit-box that contains @c pt .
+	 * @param pt the point to search with.
+	 * @return the view item that has been found, or and empty
+	 *   @c shared_ptr if no item was found.
+	 */
+	std::shared_ptr<pv::view::ViewItem> get_mouse_over_item(
+		const QPoint &pt);
+
+	/**
+	 * Sets this item into the dragged state.
+	 */
+	void drag();
+
+	/**
+	 * Drag the background by the delta offset.
+	 * @param delta the drag offset in pixels.
+	 */
+	void drag_by(const QPoint &delta);
+
+	/**
+	 * Sets this item into the un-dragged state.
+	 */
+	void drag_release();
+
+	/**
+	 * Gets the items in the view widget.
+	 */
+	std::vector< std::shared_ptr<pv::view::ViewItem> > items();
+
+	/**
+	 * Handles touch begin update and end events.
+	 * @param e the event that triggered this handler.
+	 */
+	bool touch_event(QTouchEvent *e);
+
+private:
+	void paintEvent(QPaintEvent *event);
+
+	void mouseDoubleClickEvent(QMouseEvent * event);
+	void wheelEvent(QWheelEvent *event);
+
+private:
+	boost::optional<pv::util::Timestamp> drag_offset_;
+	int drag_v_offset_;
+
+	double pinch_offset0_;
+	double pinch_offset1_;
+	bool pinch_zoom_active_;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEW_VIEWPORT_HPP
diff --git a/pv/view/viewwidget.cpp b/pv/view/viewwidget.cpp
new file mode 100644
index 0000000..30d96eb
--- /dev/null
+++ b/pv/view/viewwidget.cpp
@@ -0,0 +1,301 @@
+/*
+ * 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 <QApplication>
+#include <QMouseEvent>
+#include <QTouchEvent>
+
+#include "tracetreeitem.hpp"
+#include "view.hpp"
+#include "viewwidget.hpp"
+
+using std::any_of;
+using std::shared_ptr;
+using std::vector;
+
+namespace pv {
+namespace view {
+
+ViewWidget::ViewWidget(View &parent) :
+	QWidget(&parent),
+	view_(parent),
+	item_dragging_(false)
+{
+	setFocusPolicy(Qt::ClickFocus);
+	setAttribute(Qt::WA_AcceptTouchEvents, true);
+	setMouseTracking(true);
+}
+
+void ViewWidget::clear_selection()
+{
+	const auto items = this->items();
+	for (auto &i : items)
+		i->select(false);
+}
+
+void ViewWidget::item_hover(const shared_ptr<ViewItem> &item)
+{
+	(void)item;
+}
+
+void ViewWidget::item_clicked(const shared_ptr<ViewItem> &item)
+{
+	(void)item;
+}
+
+bool ViewWidget::accept_drag() const
+{
+	const vector< shared_ptr<TimeItem> > items(view_.time_items());
+	const vector< shared_ptr<TraceTreeItem> > trace_tree_items(
+		view_.list_by_type<TraceTreeItem>());
+
+	const bool any_row_items_selected = any_of(
+		trace_tree_items.begin(), trace_tree_items.end(),
+		[](const shared_ptr<TraceTreeItem> &r) { return r->selected(); });
+
+	const bool any_time_items_selected = any_of(items.begin(), items.end(),
+		[](const shared_ptr<TimeItem> &i) { return i->selected(); });
+
+	if (any_row_items_selected && !any_time_items_selected) {
+		// Check all the drag items share a common owner
+		TraceTreeItemOwner *item_owner = nullptr;
+		for (shared_ptr<TraceTreeItem> r : trace_tree_items)
+			if (r->dragging()) {
+				if (!item_owner)
+					item_owner = r->owner();
+				else if (item_owner != r->owner())
+					return false;
+			}
+
+		return true;
+	} else if (any_time_items_selected && !any_row_items_selected) {
+		return true;
+	}
+
+	// A background drag is beginning
+	return true;
+}
+
+bool ViewWidget::mouse_down() const
+{
+	return mouse_down_point_.x() != INT_MIN &&
+		mouse_down_point_.y() != INT_MIN;
+}
+
+void ViewWidget::drag_items(const QPoint &delta)
+{
+	bool item_dragged = false;
+
+	// Drag the row items
+	const vector< shared_ptr<RowItem> > row_items(
+		view_.list_by_type<RowItem>());
+	for (shared_ptr<RowItem> r : row_items)
+		if (r->dragging()) {
+			r->drag_by(delta);
+
+			// Ensure the trace is selected
+			r->select();
+
+			item_dragged = true;
+		}
+
+	// If an item is being dragged, update the stacking
+	TraceTreeItemOwner *item_owner = nullptr;
+	const vector< shared_ptr<TraceTreeItem> > trace_tree_items(
+		view_.list_by_type<TraceTreeItem>());
+	for (shared_ptr<TraceTreeItem> i : trace_tree_items)
+		if (i->dragging())
+			item_owner = i->owner();
+
+	if (item_owner) {
+		item_owner->restack_items();
+		for (shared_ptr<TraceTreeItem> i : trace_tree_items)
+			i->animate_to_layout_v_offset();
+	}
+
+	// Drag the time items
+	const vector< shared_ptr<TimeItem> > items(view_.time_items());
+	for (auto &i : items)
+		if (i->dragging()) {
+			i->drag_by(delta);
+			item_dragged = true;
+		}
+
+	// Do the background drag
+	if (!item_dragged)
+		drag_by(delta);
+}
+
+void ViewWidget::drag()
+{
+}
+
+void ViewWidget::drag_by(const QPoint &delta)
+{
+	(void)delta;
+}
+
+void ViewWidget::drag_release()
+{
+}
+
+void ViewWidget::mouse_left_press_event(QMouseEvent *event)
+{
+	(void)event;
+
+	const bool ctrl_pressed =
+		QApplication::keyboardModifiers() & Qt::ControlModifier;
+
+	// Clear selection if control is not pressed and this item is unselected
+	if ((!mouse_down_item_ || !mouse_down_item_->selected()) &&
+		!ctrl_pressed)
+		clear_selection();
+
+	// Set the signal selection state if the item has been clicked
+	if (mouse_down_item_) {
+		if (ctrl_pressed)
+			mouse_down_item_->select(!mouse_down_item_->selected());
+		else
+			mouse_down_item_->select(true);
+	}
+
+	// Save the offsets of any signals which will be dragged
+	bool item_dragged = false;
+	const auto items = this->items();
+	for (auto &i : items)
+		if (i->selected()) {
+			item_dragged = true;
+			i->drag();
+		}
+
+	// Do the background drag
+	if (!item_dragged)
+		drag();
+
+	selection_changed();
+}
+
+void ViewWidget::mouse_left_release_event(QMouseEvent *event)
+{
+	assert(event);
+
+	auto items = this->items();
+	const bool ctrl_pressed =
+		QApplication::keyboardModifiers() & Qt::ControlModifier;
+
+	// Unselect everything if control is not pressed
+	const shared_ptr<ViewItem> mouse_over =
+		get_mouse_over_item(event->pos());
+
+	for (auto &i : items)
+		i->drag_release();
+
+	if (item_dragging_)
+		view_.restack_all_trace_tree_items();
+	else {
+		if (!ctrl_pressed) {
+			for (shared_ptr<ViewItem> i : items)
+				if (mouse_down_item_ != i)
+					i->select(false);
+
+			if (mouse_down_item_)
+				item_clicked(mouse_down_item_);
+		}
+	}
+
+	item_dragging_ = false;
+}
+
+bool ViewWidget::touch_event(QTouchEvent *e)
+{
+	(void)e;
+	return false;
+}
+
+bool ViewWidget::event(QEvent *event)
+{
+	switch (event->type()) {
+	case QEvent::TouchBegin:
+	case QEvent::TouchUpdate:
+	case QEvent::TouchEnd:
+		if (touch_event(static_cast<QTouchEvent *>(event)))
+			return true;
+		break;
+
+	default:
+		break;
+	}
+
+	return QWidget::event(event);
+}
+
+void ViewWidget::mousePressEvent(QMouseEvent *event)
+{
+	assert(event);
+
+	mouse_down_point_ = event->pos();
+	mouse_down_item_ = get_mouse_over_item(event->pos());
+
+	if (event->button() & Qt::LeftButton)
+		mouse_left_press_event(event);
+}
+
+void ViewWidget::mouseReleaseEvent(QMouseEvent *event)
+{
+	assert(event);
+	if (event->button() & Qt::LeftButton)
+		mouse_left_release_event(event);
+
+	mouse_down_point_ = QPoint(INT_MIN, INT_MIN);
+	mouse_down_item_ = nullptr;
+}
+
+void ViewWidget::mouseMoveEvent(QMouseEvent *e)
+{
+	assert(e);
+	mouse_point_ = e->pos();
+
+	if (!e->buttons())
+		item_hover(get_mouse_over_item(e->pos()));
+	else if (e->buttons() & Qt::LeftButton) {
+		if (!item_dragging_) {
+			if ((e->pos() - mouse_down_point_).manhattanLength() <
+				QApplication::startDragDistance())
+				return;
+
+			if (!accept_drag())
+				return;
+
+			item_dragging_ = true;
+		}
+
+		// Do the drag
+		drag_items(e->pos() - mouse_down_point_);
+	}
+}
+
+void ViewWidget::leaveEvent(QEvent*)
+{
+	mouse_point_ = QPoint(-1, -1);
+	update();
+}
+
+} // namespace view
+} // namespace pv
diff --git a/pv/view/viewwidget.hpp b/pv/view/viewwidget.hpp
new file mode 100644
index 0000000..e2c04f2
--- /dev/null
+++ b/pv/view/viewwidget.hpp
@@ -0,0 +1,152 @@
+/*
+ * 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_VIEWWIDGET_HPP
+#define PULSEVIEW_PV_VIEWWIDGET_HPP
+
+#include <memory>
+
+#include <QWidget>
+
+class QTouchEvent;
+
+namespace pv {
+namespace view {
+
+class View;
+class ViewItem;
+
+class ViewWidget : public QWidget
+{
+	Q_OBJECT
+
+protected:
+	ViewWidget(View &parent);
+
+	/**
+	 * Indicates when a view item is being hovered over.
+	 * @param item The item that is being hovered over, or @c nullptr
+	 * if no view item is being hovered over.
+	 * @remarks the default implementation does nothing.
+	 */
+	virtual void item_hover(
+		const std::shared_ptr<pv::view::ViewItem> &item);
+
+	/**
+	 * Indicates the event an a view item has been clicked.
+	 * @param item the view item that has been clicked.
+	 * @remarks the default implementation does nothing.
+	 */
+	virtual void item_clicked(
+		const std::shared_ptr<pv::view::ViewItem> &item);
+
+	/**
+	 * Returns true if the selection of row items allows dragging.
+	 * @return Returns true if the drag is acceptable.
+	 */
+	bool accept_drag() const;
+
+	/**
+	 * Returns true if the mouse button is down.
+	 */
+	bool mouse_down() const;
+
+	/**
+	 * Drag the dragging items by the delta offset.
+	 * @param delta the drag offset in pixels.
+	 */
+	void drag_items(const QPoint &delta);
+
+	/**
+	 * Sets this item into the dragged state.
+	 */
+	virtual void drag();
+
+	/**
+	 * Drag the background by the delta offset.
+	 * @param delta the drag offset in pixels.
+	 * @remarks The default implementation does nothing.
+	 */
+	virtual void drag_by(const QPoint &delta);
+
+	/**
+	 * Sets this item into the un-dragged state.
+	 */
+	virtual void drag_release();
+
+	/**
+	 * Gets the items in the view widget.
+	 */
+	virtual std::vector< std::shared_ptr<pv::view::ViewItem> > items() = 0;
+
+	/**
+	 * Gets the first view item which has a hit-box that contains @c pt .
+	 * @param pt the point to search with.
+	 * @return the view item that has been found, or and empty
+	 *   @c shared_ptr if no item was found.
+	 */
+	virtual std::shared_ptr<pv::view::ViewItem> get_mouse_over_item(
+		const QPoint &pt) = 0;
+
+	/**
+	 * Handles left mouse button press events.
+	 * @param event the mouse event that triggered this handler.
+	 */
+	void mouse_left_press_event(QMouseEvent *event);
+
+	/**
+	 * Handles left mouse button release events.
+	 * @param event the mouse event that triggered this handler.
+	 */
+	void mouse_left_release_event(QMouseEvent *event);
+
+	/**
+	 * Handles touch begin update and end events.
+	 * @param e the event that triggered this handler.
+	 */
+	virtual bool touch_event(QTouchEvent *e);
+
+protected:
+	bool event(QEvent *event);
+
+	void mousePressEvent(QMouseEvent * event);
+	void mouseReleaseEvent(QMouseEvent *event);
+	void mouseMoveEvent(QMouseEvent *event);
+
+	void leaveEvent(QEvent *event);
+
+public Q_SLOTS:
+	void clear_selection();
+
+Q_SIGNALS:
+	void selection_changed();
+
+protected:
+	pv::view::View &view_;
+	QPoint mouse_point_;
+	QPoint mouse_down_point_;
+	std::shared_ptr<ViewItem> mouse_down_item_;
+	bool item_dragging_;
+};
+
+} // namespace view
+} // namespace pv
+
+#endif // PULSEVIEW_PV_VIEWWIDGET_HPP
diff --git a/pv/widgets/colourbutton.cpp b/pv/widgets/colourbutton.cpp
index fd417c1..13b9673 100644
--- a/pv/widgets/colourbutton.cpp
+++ b/pv/widgets/colourbutton.cpp
@@ -18,7 +18,7 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "colourbutton.h"
+#include "colourbutton.hpp"
 
 #include <assert.h>
 
@@ -32,37 +32,35 @@ const int ColourButton::SwatchMargin = 7;
 
 ColourButton::ColourButton(int rows, int cols, QWidget *parent) :
 	QPushButton("", parent),
-	_popup(rows, cols, this)
+	popup_(rows, cols, this)
 {
 	connect(this, SIGNAL(clicked(bool)), this, SLOT(on_clicked(bool)));
-	connect(&_popup, SIGNAL(selected(int, int)),
+	connect(&popup_, SIGNAL(selected(int, int)),
 		this, SLOT(on_selected(int, int)));
 }
 
 ColourPopup& ColourButton::popup()
 {
-	return _popup;
+	return popup_;
 }
 
 const QColor& ColourButton::colour() const
 {
-	return _cur_colour;
+	return cur_colour_;
 }
 
 void ColourButton::set_colour(QColor colour)
 {
-	_cur_colour = colour;
+	cur_colour_ = colour;
 
-	const unsigned int rows = _popup.well_array().numRows();
-	const unsigned int cols = _popup.well_array().numCols();
+	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);
+			if (popup_.well_array().cellBrush(r, c).color() == colour) {
+				popup_.well_array().setSelected(r, c);
+				popup_.well_array().setCurrent(r, c);
 				return;
 			}	
 }
@@ -71,25 +69,25 @@ 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();
+	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,
+			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();
+	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);
+	cur_colour_ = popup_.well_array().cellBrush(row, col).color();
+	selected(cur_colour_);
 }
 
 void ColourButton::paintEvent(QPaintEvent *e)
@@ -101,7 +99,7 @@ void ColourButton::paintEvent(QPaintEvent *e)
 	const QRect r = rect().adjusted(SwatchMargin, SwatchMargin,
 		-SwatchMargin, -SwatchMargin);
 	p.setPen(QApplication::palette().color(QPalette::Dark));
-	p.setBrush(QBrush(_cur_colour));
+	p.setBrush(QBrush(cur_colour_));
 	p.drawRect(r);
 }
 
diff --git a/pv/widgets/colourbutton.h b/pv/widgets/colourbutton.hpp
similarity index 85%
rename from pv/widgets/colourbutton.h
rename to pv/widgets/colourbutton.hpp
index 45a3fff..86e8e98 100644
--- a/pv/widgets/colourbutton.h
+++ b/pv/widgets/colourbutton.hpp
@@ -18,12 +18,12 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_WIDGETS_COLOURBUTTON_H
-#define PULSEVIEW_PV_WIDGETS_COLOURBUTTON_H
+#ifndef PULSEVIEW_PV_WIDGETS_COLOURBUTTON_HPP
+#define PULSEVIEW_PV_WIDGETS_COLOURBUTTON_HPP
 
 #include <QPushButton>
 
-#include "colourpopup.h"
+#include "colourpopup.hpp"
 
 namespace pv {
 namespace widgets {
@@ -49,20 +49,20 @@ public:
 private:
 	void paintEvent(QPaintEvent *e);
 
-private slots:
+private Q_SLOTS:
 	void on_clicked(bool);
 
 	void on_selected(int row, int col);
 
-signals:
+Q_SIGNALS:
 	void selected(const QColor &colour);
 
 private:
-	ColourPopup _popup;
-	QColor _cur_colour;
+	ColourPopup popup_;
+	QColor cur_colour_;
 };
 
 } // widgets
 } // pv
 
-#endif // PULSEVIEW_PV_WIDGETS_COLOURBUTTON_H
+#endif // PULSEVIEW_PV_WIDGETS_COLOURBUTTON_HPP
diff --git a/pv/widgets/colourpopup.cpp b/pv/widgets/colourpopup.cpp
index 816a4c5..8c8b138 100644
--- a/pv/widgets/colourpopup.cpp
+++ b/pv/widgets/colourpopup.cpp
@@ -18,28 +18,28 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "colourpopup.h"
+#include "colourpopup.hpp"
 
 namespace pv {
 namespace widgets {
 
 ColourPopup::ColourPopup(int rows, int cols, QWidget *parent) :
 	Popup(parent),
-	_well_array(rows, cols, this),
-	_layout(this)
+	well_array_(rows, cols, this),
+	layout_(this)
 {
-	_layout.addWidget(&_well_array);
-	setLayout(&_layout);
+	layout_.addWidget(&well_array_);
+	setLayout(&layout_);
 
-	connect(&_well_array, SIGNAL(selected(int, int)),
+	connect(&well_array_, SIGNAL(selected(int, int)),
 		this, SIGNAL(selected(int, int)));
-	connect(&_well_array, SIGNAL(selected(int, int)),
+	connect(&well_array_, SIGNAL(selected(int, int)),
 		this, SLOT(colour_selected(int, int)));
 }
 
-QWellArray& ColourPopup::well_array()
+WellArray& ColourPopup::well_array()
 {
-	return _well_array;
+	return well_array_;
 }
 
 void ColourPopup::colour_selected(int, int)
diff --git a/pv/widgets/colourpopup.h b/pv/widgets/colourpopup.hpp
similarity index 79%
copy from pv/widgets/colourpopup.h
copy to pv/widgets/colourpopup.hpp
index 4f5c52e..2abf1b7 100644
--- a/pv/widgets/colourpopup.h
+++ b/pv/widgets/colourpopup.hpp
@@ -18,11 +18,11 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_WIDGETS_COLOURPOPUP_H
-#define PULSEVIEW_PV_WIDGETS_COLOURPOPUP_H
+#ifndef PULSEVIEW_PV_WIDGETS_COLOURPOPUP_HPP
+#define PULSEVIEW_PV_WIDGETS_COLOURPOPUP_HPP
 
-#include "popup.h"
-#include "wellarray.h"
+#include "popup.hpp"
+#include "wellarray.hpp"
 
 #include <QVBoxLayout>
 
@@ -36,20 +36,20 @@ class ColourPopup : public Popup
 public:
 	ColourPopup(int rows, int cols, QWidget *partent);
 
-	QWellArray& well_array();
+	WellArray& well_array();
 
-signals:
+Q_SIGNALS:
 	void selected(int row, int col);
 
-private slots:
+private Q_SLOTS:
 	void colour_selected(int, int);
 
 private:
-	QWellArray _well_array;
-	QVBoxLayout _layout;
+	WellArray well_array_;
+	QVBoxLayout layout_;
 };
 
 } // widgets
 } // pv
 
-#endif // PULSEVIEW_PV_WIDGETS_COLOURPOPUP_H
+#endif // PULSEVIEW_PV_WIDGETS_COLOURPOPUP_HPP
diff --git a/pv/widgets/decodergroupbox.cpp b/pv/widgets/decodergroupbox.cpp
index 2cbd532..81e5e90 100644
--- a/pv/widgets/decodergroupbox.cpp
+++ b/pv/widgets/decodergroupbox.cpp
@@ -18,7 +18,7 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "decodergroupbox.h"
+#include "decodergroupbox.hpp"
 
 #include <QHBoxLayout>
 #include <QLabel>
@@ -30,45 +30,47 @@
 namespace pv {
 namespace widgets {
 
-DecoderGroupBox::DecoderGroupBox(QString title, QWidget *parent) :
+DecoderGroupBox::DecoderGroupBox(QString title, QWidget *parent, bool isDeletable) :
 	QWidget(parent),
-	_layout(new QGridLayout),
-	_show_hide_button(QIcon(":/icons/decoder-shown.svg"), QString(), this)
+	layout_(new QGridLayout),
+	show_hide_button_(QIcon(":/icons/decoder-shown.svg"), QString(), this)
 {
-	_layout->setContentsMargins(0, 0, 0, 0);
-	setLayout(_layout);
+	layout_->setContentsMargins(0, 0, 0, 0);
+	setLayout(layout_);
 
-	_layout->addWidget(new QLabel(QString("<h3>%1</h3>").arg(title)),
+	layout_->addWidget(new QLabel(QString("<h3>%1</h3>").arg(title)),
 		0, 0);
-	_layout->setColumnStretch(0, 1);
+	layout_->setColumnStretch(0, 1);
 
 	QHBoxLayout *const toolbar = new QHBoxLayout;
-	_layout->addLayout(toolbar, 0, 1);
+	layout_->addLayout(toolbar, 0, 1);
 
-	_show_hide_button.setFlat(true);
-	_show_hide_button.setIconSize(QSize(16, 16));
-	connect(&_show_hide_button, SIGNAL(clicked()),
+	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);
+	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);
+	if (isDeletable) {
+		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);
+	layout_->addLayout(layout, 1, 0, 1, 2);
 }
 
 void DecoderGroupBox::set_decoder_visible(bool visible)
 {
-	_show_hide_button.setIcon(QIcon(visible ?
+	show_hide_button_.setIcon(QIcon(visible ?
 		":/icons/decoder-shown.svg" :
 		":/icons/decoder-hidden.svg"));
 }
diff --git a/pv/widgets/decodergroupbox.h b/pv/widgets/decodergroupbox.hpp
similarity index 79%
rename from pv/widgets/decodergroupbox.h
rename to pv/widgets/decodergroupbox.hpp
index 2302a97..7554431 100644
--- a/pv/widgets/decodergroupbox.h
+++ b/pv/widgets/decodergroupbox.hpp
@@ -18,8 +18,8 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_WIDGETS_DECODERGROUPBOX_H
-#define PULSEVIEW_PV_WIDGETS_DECODERGROUPBOX_H
+#ifndef PULSEVIEW_PV_WIDGETS_DECODERGROUPBOX_HPP
+#define PULSEVIEW_PV_WIDGETS_DECODERGROUPBOX_HPP
 
 #include <QPushButton>
 
@@ -34,23 +34,24 @@ class DecoderGroupBox : public QWidget
 	Q_OBJECT
 
 public:
-	DecoderGroupBox(QString title, QWidget *parent = NULL);
+	DecoderGroupBox(QString title, QWidget *parent = nullptr,
+		bool isDeletable = true);
 
 	void add_layout(QLayout *layout);
 
 	void set_decoder_visible(bool visible);
 
-signals:
+Q_SIGNALS:
 	void delete_decoder();
 
 	void show_hide_decoder();
 
 private:
-	QGridLayout *const _layout;
-	QPushButton _show_hide_button;
+	QGridLayout *const layout_;
+	QPushButton show_hide_button_;
 };
 
 } // widgets
 } // pv
 
-#endif // PULSEVIEW_PV_WIDGETS_DECODERGROUPBOX_H
+#endif // PULSEVIEW_PV_WIDGETS_DECODERGROUPBOX_HPP
diff --git a/pv/widgets/decodermenu.cpp b/pv/widgets/decodermenu.cpp
index 2f828a2..7da77bd 100644
--- a/pv/widgets/decodermenu.cpp
+++ b/pv/widgets/decodermenu.cpp
@@ -18,37 +18,38 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
+#include <cassert>
+
 #include <libsigrokdecode/libsigrokdecode.h>
 
-#include "decodermenu.h"
+#include "decodermenu.hpp"
 
 namespace pv {
 namespace widgets {
 
 DecoderMenu::DecoderMenu(QWidget *parent, bool first_level_decoder) :
 	QMenu(parent),
-	_mapper(this)
+	mapper_(this)
 {
 	GSList *l = g_slist_sort(g_slist_copy(
 		(GSList*)srd_decoder_list()), decoder_name_cmp);
-	for(; l; l = l->next)
-	{
+	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) {
+		const bool have_channels = (d->channels || d->opt_channels) != 0;
+		if (first_level_decoder == have_channels) {
 			QAction *const action =
 				addAction(QString::fromUtf8(d->name));
 			action->setData(qVariantFromValue(l->data));
-			_mapper.setMapping(action, action);
+			mapper_.setMapping(action, action);
 			connect(action, SIGNAL(triggered()),
-				&_mapper, SLOT(map()));
+				&mapper_, SLOT(map()));
 		}
 	}
 	g_slist_free(l);
 
-	connect(&_mapper, SIGNAL(mapped(QObject*)),
+	connect(&mapper_, SIGNAL(mapped(QObject*)),
 		this, SLOT(on_action(QObject*)));
 }
 
diff --git a/pv/widgets/decodermenu.h b/pv/widgets/decodermenu.hpp
similarity index 86%
copy from pv/widgets/decodermenu.h
copy to pv/widgets/decodermenu.hpp
index 20dd412..e154cd2 100644
--- a/pv/widgets/decodermenu.h
+++ b/pv/widgets/decodermenu.hpp
@@ -18,8 +18,8 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_WIDGETS_DECODERMENU_H
-#define PULSEVIEW_PV_WIDGETS_DECODERMENU_H
+#ifndef PULSEVIEW_PV_WIDGETS_DECODERMENU_HPP
+#define PULSEVIEW_PV_WIDGETS_DECODERMENU_HPP
 
 #include <QMenu>
 #include <QSignalMapper>
@@ -40,17 +40,17 @@ private:
 	static int decoder_name_cmp(const void *a, const void *b);
 
 
-private slots:
+private Q_SLOTS:
 	void on_action(QObject *action);
 
-signals:
+Q_SIGNALS:
 	void decoder_selected(srd_decoder *decoder);
 
 private:
-	QSignalMapper _mapper;
+	QSignalMapper mapper_;
 };
 
 } // widgets
 } // pv
 
-#endif // PULSEVIEW_PV_WIDGETS_DECODERMENU_H
+#endif // PULSEVIEW_PV_WIDGETS_DECODERMENU_HPP
diff --git a/pv/widgets/devicetoolbutton.cpp b/pv/widgets/devicetoolbutton.cpp
new file mode 100644
index 0000000..a0fc10e
--- /dev/null
+++ b/pv/widgets/devicetoolbutton.cpp
@@ -0,0 +1,153 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2015 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 <QTimer>
+#include <QToolTip>
+
+#include <libsigrokcxx/libsigrokcxx.hpp>
+
+#include <pv/devicemanager.hpp>
+#include <pv/devices/device.hpp>
+
+#include "devicetoolbutton.hpp"
+
+using std::list;
+using std::shared_ptr;
+using std::string;
+using std::weak_ptr;
+using std::vector;
+
+using pv::devices::Device;
+
+namespace pv {
+namespace widgets {
+
+DeviceToolButton::DeviceToolButton(QWidget *parent,
+	DeviceManager &device_manager,
+	QAction *connect_action) :
+	QToolButton(parent),
+	device_manager_(device_manager),
+	connect_action_(connect_action),
+	menu_(this),
+	mapper_(this),
+	devices_()
+{
+	setPopupMode(QToolButton::MenuButtonPopup);
+	setMenu(&menu_);
+	setDefaultAction(connect_action_);
+	setMinimumWidth(QFontMetrics(font()).averageCharWidth() * 24);
+
+	connect(&mapper_, SIGNAL(mapped(QObject*)),
+		this, SLOT(on_action(QObject*)));
+
+	connect(&menu_, SIGNAL(hovered(QAction*)),
+		this, SLOT(on_menu_hovered(QAction*)));
+}
+
+shared_ptr<Device> DeviceToolButton::selected_device()
+{
+	return selected_device_;
+}
+
+void DeviceToolButton::set_device_list(
+	const list< shared_ptr<Device> > &devices, shared_ptr<Device> selected)
+{
+	selected_device_ = selected;
+	setText(selected ? QString::fromStdString(
+		selected->display_name(device_manager_)) : "<No Device>");
+	devices_ = vector< weak_ptr<Device> >(devices.begin(), devices.end());
+	update_device_list();
+}
+
+void DeviceToolButton::update_device_list()
+{
+	menu_.clear();
+	menu_.addAction(connect_action_);
+	menu_.setDefaultAction(connect_action_);
+	menu_.addSeparator();
+
+	for (weak_ptr<Device> dev_weak_ptr : devices_) {
+		shared_ptr<Device> dev(dev_weak_ptr.lock());
+		if (!dev)
+			continue;
+
+		QAction *const a = new QAction(QString::fromStdString(
+			dev->display_name(device_manager_)), this);
+		a->setCheckable(true);
+		a->setChecked(selected_device_ == dev);
+		a->setData(qVariantFromValue((void*)dev.get()));
+		a->setToolTip(QString::fromStdString(dev->full_name()));
+		mapper_.setMapping(a, a);
+
+		connect(a, SIGNAL(triggered()), &mapper_, SLOT(map()));
+
+		menu_.addAction(a);
+	}
+}
+
+void DeviceToolButton::on_action(QObject *action)
+{
+	assert(action);
+
+	Device *const dev = (Device*)((QAction*)action)->data().value<void*>();
+	for (weak_ptr<Device> dev_weak_ptr : devices_) {
+		shared_ptr<Device> dev_ptr(dev_weak_ptr);
+		if (dev_ptr.get() == dev) {
+			selected_device_ = shared_ptr<Device>(dev_ptr);
+			break;
+		}
+	}
+
+	update_device_list();
+	setText(QString::fromStdString(
+		selected_device_->display_name(device_manager_)));
+
+	device_selected();
+}
+
+void DeviceToolButton::on_menu_hovered(QAction *action)
+{
+	assert(action);
+
+	// Only show the tooltip for device entries (they hold
+	// device pointers in their data field)
+	if (!action->data().isValid())
+		return;
+
+	device_tooltip_ = action->toolTip();
+
+	if (QToolTip::isVisible())
+		on_menu_hover_timeout();
+	else
+		QTimer::singleShot(1000, this, SLOT(on_menu_hover_timeout()));
+}
+
+void DeviceToolButton::on_menu_hover_timeout()
+{
+	if (device_tooltip_.isEmpty())
+		return;
+
+	QToolTip::showText(QCursor::pos(), device_tooltip_);
+}
+
+} // widgets
+} // pv
diff --git a/pv/widgets/devicetoolbutton.hpp b/pv/widgets/devicetoolbutton.hpp
new file mode 100644
index 0000000..ed69a4b
--- /dev/null
+++ b/pv/widgets/devicetoolbutton.hpp
@@ -0,0 +1,105 @@
+/*
+ * 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_WIDGETS_DEVICETOOLBUTTON_HPP
+#define PULSEVIEW_PV_WIDGETS_DEVICETOOLBUTTON_HPP
+
+#include <list>
+#include <memory>
+#include <vector>
+
+#include <QAction>
+#include <QMenu>
+#include <QSignalMapper>
+#include <QToolButton>
+
+struct srd_decoder;
+
+namespace pv {
+
+class DeviceManager;
+
+namespace devices {
+class Device;
+}
+
+namespace widgets {
+
+class DeviceToolButton : public QToolButton
+{
+	Q_OBJECT;
+
+public:
+	/**
+	 * Constructor
+	 * @param parent the parent widget.
+	 * @param device_manager the device manager.
+	 * @param connect_action the connect-to-device action.
+	 */
+	DeviceToolButton(QWidget *parent, DeviceManager &device_manager,
+		QAction *connect_action);
+
+	/**
+	 * Returns a reference to the selected device.
+	 */
+	std::shared_ptr<devices::Device> selected_device();
+
+	/**
+	 * Sets the current list of devices.
+	 * @param device the list of devices.
+	 * @param selected_device the currently active device.
+	 */
+	void set_device_list(
+		const std::list< std::shared_ptr<devices::Device> > &devices,
+		std::shared_ptr<devices::Device> selected);
+
+private:
+	/**
+	 * Repopulates the menu from the device list.
+	 */
+	void update_device_list();
+
+private Q_SLOTS:
+	void on_action(QObject *action);
+
+	void on_menu_hovered(QAction *action);
+
+	void on_menu_hover_timeout();
+
+Q_SIGNALS:
+	void device_selected();
+
+private:
+	DeviceManager &device_manager_;
+	QAction *const connect_action_;
+
+	QMenu menu_;
+	QSignalMapper mapper_;
+
+	std::shared_ptr<devices::Device> selected_device_;
+	std::vector< std::weak_ptr<devices::Device> > devices_;
+
+	QString device_tooltip_;
+};
+
+} // widgets
+} // pv
+
+#endif // PULSEVIEW_PV_WIDGETS_DEVICETOOLBUTTON_HPP
diff --git a/pv/widgets/exportmenu.cpp b/pv/widgets/exportmenu.cpp
new file mode 100644
index 0000000..e9fb415
--- /dev/null
+++ b/pv/widgets/exportmenu.cpp
@@ -0,0 +1,99 @@
+/*
+ * 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 <cassert>
+#include <string>
+#include <utility>
+
+#include <libsigrokcxx/libsigrokcxx.hpp>
+
+#include "exportmenu.hpp"
+
+using std::find_if;
+using std::map;
+using std::pair;
+using std::string;
+using std::shared_ptr;
+
+using sigrok::Context;
+using sigrok::OutputFormat;
+
+namespace pv {
+namespace widgets {
+
+ExportMenu::ExportMenu(QWidget *parent, shared_ptr<Context> context,
+	std::vector<QAction *>open_actions) :
+	QMenu(parent),
+	context_(context),
+	mapper_(this)
+{
+	assert(context);
+
+	if (!open_actions.empty()) {
+		bool first_action = true;
+		for (auto open_action : open_actions) {
+			addAction(open_action);
+
+			if (first_action) {
+				first_action = false;
+				setDefaultAction(open_action);
+			}
+		}
+		addSeparator();
+	}
+
+	const map<string, shared_ptr<OutputFormat> > formats =
+		context->output_formats();
+
+	for (const pair<string, shared_ptr<OutputFormat> > &f : formats) {
+		if (f.first == "srzip")
+			continue;
+
+		assert(f.second);
+		QAction *const action =	addAction(tr("Export %1...")
+			.arg(QString::fromStdString(f.second->description())));
+		action->setData(qVariantFromValue((void*)f.second.get()));
+		mapper_.setMapping(action, action);
+		connect(action, SIGNAL(triggered()), &mapper_, SLOT(map()));
+	}
+
+	connect(&mapper_, SIGNAL(mapped(QObject*)),
+		this, SLOT(on_action(QObject*)));
+}
+
+void ExportMenu::on_action(QObject *action)
+{
+	assert(action);
+
+	const map<string, shared_ptr<OutputFormat> > formats =
+		context_->output_formats();
+	const auto iter = find_if(formats.cbegin(), formats.cend(),
+		[&](const pair<string, shared_ptr<OutputFormat> > &f) {
+			return f.second.get() ==
+				((QAction*)action)->data().value<void*>(); });
+	if (iter == formats.cend())
+		return;
+
+	format_selected((*iter).second);
+}
+
+} // widgets
+} // pv
diff --git a/pv/widgets/decodermenu.h b/pv/widgets/exportmenu.hpp
similarity index 61%
copy from pv/widgets/decodermenu.h
copy to pv/widgets/exportmenu.hpp
index 20dd412..e3b1f23 100644
--- a/pv/widgets/decodermenu.h
+++ b/pv/widgets/exportmenu.hpp
@@ -1,7 +1,7 @@
 /*
  * This file is part of the PulseView project.
  *
- * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ * Copyright (C) 2015 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
@@ -18,39 +18,42 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_WIDGETS_DECODERMENU_H
-#define PULSEVIEW_PV_WIDGETS_DECODERMENU_H
+#ifndef PULSEVIEW_PV_WIDGETS_EXPORTMENU_HPP
+#define PULSEVIEW_PV_WIDGETS_EXPORTMENU_HPP
+
+#include <memory>
 
 #include <QMenu>
 #include <QSignalMapper>
 
-struct srd_decoder;
+namespace sigrok {
+class Context;
+class OutputFormat;
+}
 
 namespace pv {
 namespace widgets {
 
-class DecoderMenu : public QMenu
+class ExportMenu : 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);
-
+	ExportMenu(QWidget *parent, std::shared_ptr<sigrok::Context> context,
+		std::vector<QAction *>open_actions = std::vector<QAction *>());
 
-private slots:
+private Q_SLOTS:
 	void on_action(QObject *action);
 
-signals:
-	void decoder_selected(srd_decoder *decoder);
+Q_SIGNALS:
+	void format_selected(std::shared_ptr<sigrok::OutputFormat> format);
 
 private:
-	QSignalMapper _mapper;
+	std::shared_ptr<sigrok::Context> context_;
+	QSignalMapper mapper_;
 };
 
 } // widgets
 } // pv
 
-#endif // PULSEVIEW_PV_WIDGETS_DECODERMENU_H
+#endif // PULSEVIEW_PV_WIDGETS_EXPORTMENU_HPP
diff --git a/pv/data/signaldata.cpp b/pv/widgets/hidingmenubar.cpp
similarity index 56%
copy from pv/data/signaldata.cpp
copy to pv/widgets/hidingmenubar.cpp
index 04f1d3f..0da364b 100644
--- a/pv/data/signaldata.cpp
+++ b/pv/widgets/hidingmenubar.cpp
@@ -1,7 +1,7 @@
 /*
  * This file is part of the PulseView project.
  *
- * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ * Copyright (C) 2015 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
@@ -18,32 +18,39 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "signaldata.h"
+#include <QKeyEvent>
+
+#include "hidingmenubar.hpp"
 
 namespace pv {
-namespace data {
+namespace widgets {
 
-SignalData::SignalData() :
-	_start_time(0),
-	_samplerate(0)
+HidingMenuBar::HidingMenuBar(QWidget *parent) :
+	QMenuBar(parent)
 {
+	setHidden(true);
+	connect(this, SIGNAL(triggered(QAction*)),
+		this, SLOT(item_triggered()));
 }
 
-double SignalData::samplerate() const
+void HidingMenuBar::focusOutEvent(QFocusEvent *e)
 {
-	return _samplerate;
+	if (e->reason() != Qt::PopupFocusReason)
+		setHidden(true);
+	QMenuBar::focusOutEvent(e);
 }
 
-void SignalData::set_samplerate(double samplerate)
+void HidingMenuBar::keyPressEvent(QKeyEvent *e)
 {
-	_samplerate = samplerate;
-	clear();
+	if (e->key() == Qt::Key_Escape)
+		setHidden(true);
+	QMenuBar::keyPressEvent(e);
 }
 
-double SignalData::get_start_time() const
+void HidingMenuBar::item_triggered()
 {
-	return _start_time;
+	setHidden(true);
 }
 
-} // namespace data
+} // namespace widgets
 } // namespace pv
diff --git a/pv/widgets/colourpopup.h b/pv/widgets/hidingmenubar.hpp
similarity index 50%
rename from pv/widgets/colourpopup.h
rename to pv/widgets/hidingmenubar.hpp
index 4f5c52e..4cf5d16 100644
--- a/pv/widgets/colourpopup.h
+++ b/pv/widgets/hidingmenubar.hpp
@@ -1,7 +1,7 @@
 /*
  * This file is part of the PulseView project.
  *
- * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ * Copyright (C) 2015 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
@@ -18,38 +18,49 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_WIDGETS_COLOURPOPUP_H
-#define PULSEVIEW_PV_WIDGETS_COLOURPOPUP_H
+#ifndef PULSEVIEW_PV_WIDGETS_HIDINGMENUBAR_HPP
+#define PULSEVIEW_PV_WIDGETS_HIDINGMENUBAR_HPP
 
-#include "popup.h"
-#include "wellarray.h"
-
-#include <QVBoxLayout>
+#include <QMenuBar>
 
 namespace pv {
 namespace widgets {
 
-class ColourPopup : public Popup
+/**
+ * A menu bar widget that only remains visible while it retains focus.
+ */
+class HidingMenuBar : public QMenuBar
 {
 	Q_OBJECT
 
 public:
-	ColourPopup(int rows, int cols, QWidget *partent);
-
-	QWellArray& well_array();
+	/**
+	 * Constructor
+	 * @param parent The parent widget.
+	 */
+	HidingMenuBar(QWidget *parent = nullptr);
 
-signals:
-	void selected(int row, int col);
+private:
+	/**
+	 * Handles the event that the widget loses keyboard focus.
+	 * @param e the representation of the event details.
+	 */
+	void focusOutEvent(QFocusEvent *e);
 
-private slots:
-	void colour_selected(int, int);
+	/**
+	 * Handles the event that a key is depressed.
+	 * @param e the representation of the event details.
+	 */
+	void keyPressEvent(QKeyEvent *e);
 
-private:
-	QWellArray _well_array;
-	QVBoxLayout _layout;
+private Q_SLOTS:
+	/**
+	 * Handles a menu items being triggered.
+	 */
+	void item_triggered();
 };
 
 } // widgets
 } // pv
 
-#endif // PULSEVIEW_PV_WIDGETS_COLOURPOPUP_H
+#endif // PULSEVIEW_PV_WIDGETS_HIDINGMENUBAR_HPP
diff --git a/pv/widgets/importmenu.cpp b/pv/widgets/importmenu.cpp
new file mode 100644
index 0000000..a56bff7
--- /dev/null
+++ b/pv/widgets/importmenu.cpp
@@ -0,0 +1,89 @@
+/*
+ * 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 <cassert>
+#include <string>
+#include <utility>
+
+#include <libsigrokcxx/libsigrokcxx.hpp>
+
+#include "importmenu.hpp"
+
+using std::find_if;
+using std::map;
+using std::pair;
+using std::string;
+using std::shared_ptr;
+
+using sigrok::Context;
+using sigrok::InputFormat;
+
+namespace pv {
+namespace widgets {
+
+ImportMenu::ImportMenu(QWidget *parent, shared_ptr<Context> context,
+	QAction *open_action) :
+	QMenu(parent),
+	context_(context),
+	mapper_(this)
+{
+	assert(context);
+
+	if (open_action) {
+		addAction(open_action);
+		setDefaultAction(open_action);
+		addSeparator();
+	}
+
+	const map<string, shared_ptr<InputFormat> > formats =
+		context->input_formats();
+
+	for (const pair<string, shared_ptr<InputFormat> > &f : formats) {
+		assert(f.second);
+		QAction *const action =	addAction(tr("Import %1...")
+			.arg(QString::fromStdString(f.second->description())));
+		action->setData(qVariantFromValue((void*)f.second.get()));
+		mapper_.setMapping(action, action);
+		connect(action, SIGNAL(triggered()), &mapper_, SLOT(map()));
+	}
+
+	connect(&mapper_, SIGNAL(mapped(QObject*)),
+		this, SLOT(on_action(QObject*)));
+}
+
+void ImportMenu::on_action(QObject *action)
+{
+	assert(action);
+
+	const map<string, shared_ptr<InputFormat> > formats =
+		context_->input_formats();
+	const auto iter = find_if(formats.cbegin(), formats.cend(),
+		[&](const pair<string, shared_ptr<InputFormat> > &f) {
+			return f.second.get() ==
+				((QAction*)action)->data().value<void*>(); });
+	if (iter == formats.cend())
+		return;
+
+	format_selected((*iter).second);
+}
+
+} // widgets
+} // pv
diff --git a/pv/widgets/decodermenu.h b/pv/widgets/importmenu.hpp
similarity index 62%
rename from pv/widgets/decodermenu.h
rename to pv/widgets/importmenu.hpp
index 20dd412..b56631e 100644
--- a/pv/widgets/decodermenu.h
+++ b/pv/widgets/importmenu.hpp
@@ -1,7 +1,7 @@
 /*
  * This file is part of the PulseView project.
  *
- * Copyright (C) 2013 Joel Holdsworth <joel at airwebreathe.org.uk>
+ * Copyright (C) 2015 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
@@ -18,39 +18,42 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_WIDGETS_DECODERMENU_H
-#define PULSEVIEW_PV_WIDGETS_DECODERMENU_H
+#ifndef PULSEVIEW_PV_WIDGETS_IMPORTMENU_HPP
+#define PULSEVIEW_PV_WIDGETS_IMPORTMENU_HPP
+
+#include <memory>
 
 #include <QMenu>
 #include <QSignalMapper>
 
-struct srd_decoder;
+namespace sigrok {
+class Context;
+class InputFormat;
+}
 
 namespace pv {
 namespace widgets {
 
-class DecoderMenu : public QMenu
+class ImportMenu : 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);
-
+	ImportMenu(QWidget *parent, std::shared_ptr<sigrok::Context> context,
+		QAction *open_action = nullptr);
 
-private slots:
+private Q_SLOTS:
 	void on_action(QObject *action);
 
-signals:
-	void decoder_selected(srd_decoder *decoder);
+Q_SIGNALS:
+	void format_selected(std::shared_ptr<sigrok::InputFormat> format);
 
 private:
-	QSignalMapper _mapper;
+	std::shared_ptr<sigrok::Context> context_;
+	QSignalMapper mapper_;
 };
 
 } // widgets
 } // pv
 
-#endif // PULSEVIEW_PV_WIDGETS_DECODERMENU_H
+#endif // PULSEVIEW_PV_WIDGETS_IMPORTMENU_HPP
diff --git a/pv/widgets/popup.cpp b/pv/widgets/popup.cpp
index 6857358..17bc719 100644
--- a/pv/widgets/popup.cpp
+++ b/pv/widgets/popup.cpp
@@ -23,8 +23,11 @@
 #include <assert.h>
 
 #include <QtGui>
+#include <QApplication>
+#include <QDesktopWidget>
+#include <QLineEdit>
 
-#include "popup.h"
+#include "popup.hpp"
 
 using std::max;
 using std::min;
@@ -38,54 +41,95 @@ const unsigned int Popup::MarginWidth = 6;
 
 Popup::Popup(QWidget *parent) :
 	QWidget(parent, Qt::Popup | Qt::FramelessWindowHint),
-	_point(),
-	_pos(Left)
+	point_(),
+	pos_(Left)
 {
 }
 
 const QPoint& Popup::point() const
 {
-	return _point;
+	return point_;
 }
 
 Popup::Position Popup::position() const
 {
-	return _pos;
+	return pos_;
 }
 
 void Popup::set_position(const QPoint point, Position pos)
 {
-	_point = point, _pos = 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::eventFilter(QObject *obj, QEvent *evt)
+{
+	QKeyEvent *keyEvent;
 
+	(void)obj;
+
+	if (evt->type() == QEvent::KeyPress) {
+		keyEvent = static_cast<QKeyEvent*>(evt);
+		if (keyEvent->key() == Qt::Key_Enter ||
+		    keyEvent->key() == Qt::Key_Return) {
+			close();
+			return true;
+		}
+	}
+
+	return false;
+}
+
+void Popup::show()
+{
+	QLineEdit* le;
+
+	QWidget::show();
+
+	// We want to close the popup when the Enter key was
+	// pressed and the first editable widget had focus.
+	if ((le = this->findChild<QLineEdit*>())) {
+
+		// For combo boxes we need to hook into the parent of
+		// the line edit (i.e. the QComboBox). For edit boxes
+		// we hook into the widget directly.
+		if (le->parent()->metaObject()->className() ==
+				this->metaObject()->className())
+			le->installEventFilter(this);
+		else
+			le->parent()->installEventFilter(this);
+
+		le->selectAll();
+		le->setFocus();
+	}
 }
 
 bool Popup::space_for_arrow() const
 {
 	// Check if there is room for the arrow
-	switch (_pos) {
+	switch (pos_) {
 	case Right:
-		if (_point.x() > x())
+		if (point_.x() > x())
 			return false;
 		return true;
 
 	case Bottom:
-		if (_point.y() > y())
+		if (point_.y() > y())
 			return false;
 		return true;		
 
 	case Left:
-		if (_point.x() < (x() + width()))
+		if (point_.x() < (x() + width()))
 			return false;
 		return true;
 
 	case Top:
-		if (_point.y() < (y() + height()))
+		if (point_.y() < (y() + height()))
 			return false;
 		return true;
 	}
@@ -97,11 +141,10 @@ QPolygon Popup::arrow_polygon() const
 {
 	QPolygon poly;
 
-	const QPoint p = mapFromGlobal(_point);
+	const QPoint p = mapFromGlobal(point_);
 	const int l = ArrowLength + ArrowOverlap; 
 
-	switch (_pos)
-        {
+	switch (pos_) {
 	case Right:
 		poly << QPoint(p.x() + l, p.y() - l);
 		break;
@@ -110,7 +153,7 @@ QPolygon Popup::arrow_polygon() const
 		poly << QPoint(p.x() - l, p.y() + l);
 		break;
 
-        case Left:
+	case Left:
 	case Top:
 		poly << QPoint(p.x() - l, p.y() - l);
 		break;
@@ -118,14 +161,13 @@ QPolygon Popup::arrow_polygon() const
 
 	poly << p;
 
-	switch (_pos)
-        {
+	switch (pos_) {
 	case Right:
 	case Bottom:
 		poly << QPoint(p.x() + l, p.y() + l);
 		break;
 
-        case Left:
+	case Left:
 		poly << QPoint(p.x() - l, p.y() + l);
 		break;
 		
@@ -145,11 +187,11 @@ QRegion Popup::arrow_region() const
 QRect Popup::bubble_rect() const
 {
 	return QRect(
-		QPoint((_pos == Right) ? ArrowLength : 0,
-			(_pos == Bottom) ? ArrowLength : 0),
-		QSize(width() - ((_pos == Left || _pos == Right) ?
+		QPoint((pos_ == Right) ? ArrowLength : 0,
+			(pos_ == Bottom) ? ArrowLength : 0),
+		QSize(width() - ((pos_ == Left || pos_ == Right) ?
 				ArrowLength : 0),
-			height() - ((_pos == Top || _pos == Bottom) ?
+			height() - ((pos_ == Top || pos_ == Bottom) ?
 				ArrowLength : 0)));
 }
 
@@ -184,19 +226,19 @@ void Popup::reposition_widget()
 	QPoint o;
 
 	const QRect screen_rect = QApplication::desktop()->availableGeometry(
-		QApplication::desktop()->screenNumber(_point));
+		QApplication::desktop()->screenNumber(point_));
 
-	if (_pos == Right || _pos == Left)
+	if (pos_ == Right || pos_ == Left)
 		o.ry() = -height() / 2;
 	else
 		o.rx() = -width() / 2;
 
-	if (_pos == Left)
+	if (pos_ == Left)
 		o.rx() = -width();
-	else if(_pos == Top)
+	else if (pos_ == Top)
 		o.ry() = -height();
 
-	o += _point;
+	o += point_;
 	move(max(min(o.x(), screen_rect.right() - width()),
 			screen_rect.left()),
 		max(min(o.y(), screen_rect.bottom() - height()),
@@ -235,7 +277,7 @@ void Popup::paintEvent(QPaintEvent*)
 
 	const QRegion a(arrow_region());
 	const QRegion arrow_outline = a.subtracted(
-		a.translated(ArrowOffsets[_pos]));
+		a.translated(ArrowOffsets[pos_]));
 
 	painter.setClipRegion(bubble_outline.subtracted(a).united(
 		arrow_outline));
@@ -255,7 +297,7 @@ void Popup::mouseReleaseEvent(QMouseEvent *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()))
+	if (!bubble_rect().contains(e->pos()))
 		close();
 }
 
@@ -266,4 +308,3 @@ void Popup::showEvent(QShowEvent*)
 
 } // namespace widgets
 } // namespace pv
-
diff --git a/pv/widgets/popup.h b/pv/widgets/popup.hpp
similarity index 88%
rename from pv/widgets/popup.h
rename to pv/widgets/popup.hpp
index 0bc6d0f..c6f931f 100644
--- a/pv/widgets/popup.h
+++ b/pv/widgets/popup.hpp
@@ -18,8 +18,8 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_WIDGETS_POPUP_H
-#define PULSEVIEW_PV_WIDGETS_POPUP_H
+#ifndef PULSEVIEW_PV_WIDGETS_POPUP_HPP
+#define PULSEVIEW_PV_WIDGETS_POPUP_HPP
 
 #include <QWidget>
 
@@ -52,6 +52,10 @@ public:
 
 	void set_position(const QPoint point, Position pos);
 
+	bool eventFilter(QObject *obj, QEvent *evt);
+
+	void show();
+
 private:
 	bool space_for_arrow() const;
 
@@ -79,15 +83,15 @@ private:
 protected:
 	void showEvent(QShowEvent *e);
 
-signals:
+Q_SIGNALS:
 	void closed();
 
 private:
-	QPoint _point;
-	Position _pos;
+	QPoint point_;
+	Position pos_;
 };
 
 } // namespace widgets
 } // namespace pv
 
-#endif // PULSEVIEW_PV_WIDGETS_POPUP_H
+#endif // PULSEVIEW_PV_WIDGETS_POPUP_HPP
diff --git a/pv/widgets/popuptoolbutton.cpp b/pv/widgets/popuptoolbutton.cpp
index 74743ed..ff335a9 100644
--- a/pv/widgets/popuptoolbutton.cpp
+++ b/pv/widgets/popuptoolbutton.cpp
@@ -20,38 +20,38 @@
 
 #include <assert.h>
 
-#include "popuptoolbutton.h"
+#include "popuptoolbutton.hpp"
 
 namespace pv {
 namespace widgets {
 
 PopupToolButton::PopupToolButton(QWidget *parent) :
 	QToolButton(parent),
-	_popup(NULL)
+	popup_(nullptr)
 {
 	connect(this, SIGNAL(clicked(bool)), this, SLOT(on_clicked(bool)));
 }
 
 Popup* PopupToolButton::popup() const
 {
-	return _popup;
+	return popup_;
 }
 
 void PopupToolButton::set_popup(Popup *popup)
 {
 	assert(popup);
-	_popup = popup;
+	popup_ = popup;
 }
 
 void PopupToolButton::on_clicked(bool)
 {
-	if(!_popup)
+	if (!popup_)
 		return;
 
 	const QRect r = rect();
-	_popup->set_position(mapToGlobal(QPoint((r.left() + r.right()) / 2,
+	popup_->set_position(mapToGlobal(QPoint((r.left() + r.right()) / 2,
 		((r.top() + r.bottom() * 3) / 4))), Popup::Bottom);
-	_popup->show();
+	popup_->show();
 }
 
 } // widgets
diff --git a/pv/widgets/popuptoolbutton.h b/pv/widgets/popuptoolbutton.hpp
similarity index 84%
rename from pv/widgets/popuptoolbutton.h
rename to pv/widgets/popuptoolbutton.hpp
index b619b6f..3586aaf 100644
--- a/pv/widgets/popuptoolbutton.h
+++ b/pv/widgets/popuptoolbutton.hpp
@@ -18,10 +18,10 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_WIDGETS_POPUPTOOLBUTTON_H
-#define PULSEVIEW_PV_WIDGETS_POPUPTOOLBUTTON_H
+#ifndef PULSEVIEW_PV_WIDGETS_POPUPTOOLBUTTON_HPP
+#define PULSEVIEW_PV_WIDGETS_POPUPTOOLBUTTON_HPP
 
-#include "popup.h"
+#include "popup.hpp"
 
 #include <QToolButton>
 
@@ -39,14 +39,14 @@ public:
 
 	void set_popup(Popup *popup);
 
-private slots:
+private Q_SLOTS:
 	void on_clicked(bool);
 
 private:
-	Popup *_popup;
+	Popup *popup_;
 };
 
 } // widgets
 } // pv
 
-#endif // PULSEVIEW_PV_WIDGETS_POPUPTOOLBUTTON_H
+#endif // PULSEVIEW_PV_WIDGETS_POPUPTOOLBUTTON_HPP
diff --git a/pv/widgets/sweeptimingwidget.cpp b/pv/widgets/sweeptimingwidget.cpp
index 00ebb76..ec404c3 100644
--- a/pv/widgets/sweeptimingwidget.cpp
+++ b/pv/widgets/sweeptimingwidget.cpp
@@ -18,7 +18,9 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "sweeptimingwidget.h"
+#include "sweeptimingwidget.hpp"
+
+#include <cstdlib>
 
 #include <vector>
 
@@ -26,6 +28,7 @@
 
 #include <extdef.h>
 
+using std::abs;
 using std::vector;
 
 namespace pv {
@@ -34,35 +37,35 @@ namespace widgets {
 SweepTimingWidget::SweepTimingWidget(const char *suffix,
 	QWidget *parent) :
 	QWidget(parent),
-	_suffix(suffix),
-	_layout(this),
-	_value(this),
-	_list(this),
-	_value_type(None)
+	suffix_(suffix),
+	layout_(this),
+	value_(this),
+	list_(this),
+	value_type_(None)
 {
 	setContentsMargins(0, 0, 0, 0);
 
-	_value.setDecimals(0);
-	_value.setSuffix(QString::fromUtf8(suffix));
+	value_.setDecimals(0);
+	value_.setSuffix(QString::fromUtf8(suffix));
 
-	connect(&_list, SIGNAL(currentIndexChanged(int)),
+	connect(&list_, SIGNAL(currentIndexChanged(int)),
 		this, SIGNAL(value_changed()));
-	connect(&_value, SIGNAL(editingFinished()),
+	connect(&value_, SIGNAL(editingFinished()),
 		this, SIGNAL(value_changed()));
 
-	setLayout(&_layout);
-	_layout.setMargin(0);
-	_layout.addWidget(&_list);
-	_layout.addWidget(&_value);
+	setLayout(&layout_);
+	layout_.setMargin(0);
+	layout_.addWidget(&list_);
+	layout_.addWidget(&value_);
 
 	show_none();
 }
 
 void SweepTimingWidget::show_none()
 {
-	_value_type = None;
-	_value.hide();
-	_list.hide();
+	value_type_ = None;
+	value_.hide();
+	list_.hide();
 }
 
 void SweepTimingWidget::show_min_max_step(uint64_t min, uint64_t max,
@@ -71,30 +74,29 @@ void SweepTimingWidget::show_min_max_step(uint64_t min, uint64_t max,
 	assert(max > min);
 	assert(step > 0);
 
-	_value_type = MinMaxStep;
+	value_type_ = MinMaxStep;
 
-	_value.setRange(min, max);
-	_value.setSingleStep(step);
+	value_.setRange(min, max);
+	value_.setSingleStep(step);
 
-	_value.show();
-	_list.hide();
+	value_.show();
+	list_.hide();
 }
 
 void SweepTimingWidget::show_list(const uint64_t *vals, size_t count)
 {
-	_value_type = List;
+	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),
+	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();
+	value_.hide();
+	list_.show();
 }
 
 void SweepTimingWidget::show_125_list(uint64_t min, uint64_t max)
@@ -139,18 +141,17 @@ void SweepTimingWidget::show_125_list(uint64_t min, uint64_t max)
 
 uint64_t SweepTimingWidget::value() const
 {
-	switch(_value_type)
-	{
+	switch(value_type_) {
 	case None:
 		return 0;
 
 	case MinMaxStep:
-		return (uint64_t)_value.value();
+		return (uint64_t)value_.value();
 
 	case List:
 	{
-		const int index = _list.currentIndex();
-		return (index >= 0) ? _list.itemData(
+		const int index = list_.currentIndex();
+		return (index >= 0) ? list_.itemData(
 			index).value<uint64_t>() : 0;
 	}
 
@@ -163,21 +164,21 @@ uint64_t SweepTimingWidget::value() const
 
 void SweepTimingWidget::set_value(uint64_t value)
 {
-	_value.setValue(value);
+	value_.setValue(value);
 
-	int best_match = _list.count() - 1;
+	int best_match = list_.count() - 1;
 	int64_t best_variance = INT64_MAX;
 
-	for (int i = 0; i < _list.count(); i++) {
+	for (int i = 0; i < list_.count(); i++) {
 		const int64_t this_variance = abs(
-			(int64_t)value - _list.itemData(i).value<int64_t>());
+			(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);
+	list_.setCurrentIndex(best_match);
 }
 
 } // widgets
diff --git a/pv/widgets/sweeptimingwidget.h b/pv/widgets/sweeptimingwidget.hpp
similarity index 80%
rename from pv/widgets/sweeptimingwidget.h
rename to pv/widgets/sweeptimingwidget.hpp
index b66188f..e9e4dcf 100644
--- a/pv/widgets/sweeptimingwidget.h
+++ b/pv/widgets/sweeptimingwidget.hpp
@@ -18,8 +18,8 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef PULSEVIEW_PV_WIDGETS_SWEEPTIMINGWIDGET_H
-#define PULSEVIEW_PV_WIDGETS_SWEEPTIMINGWIDGET_H
+#ifndef PULSEVIEW_PV_WIDGETS_SWEEPTIMINGWIDGET_HPP
+#define PULSEVIEW_PV_WIDGETS_SWEEPTIMINGWIDGET_HPP
 
 #include <libsigrok/libsigrok.h>
 
@@ -45,7 +45,7 @@ private:
 	};
 
 public:
-	SweepTimingWidget(const char *suffix, QWidget *parent = NULL);
+	SweepTimingWidget(const char *suffix, QWidget *parent = nullptr);
 
 	void show_none();
 	void show_min_max_step(uint64_t min, uint64_t max, uint64_t step);
@@ -55,21 +55,21 @@ public:
 	uint64_t value() const;
 	void set_value(uint64_t value);
 
-signals:
+Q_SIGNALS:
 	void value_changed();
 
 private:
-	const char *const _suffix;
+	const char *const suffix_;
 
-	QHBoxLayout _layout;
+	QHBoxLayout layout_;
 
-	QDoubleSpinBox _value;
-	QComboBox _list;
+	QDoubleSpinBox value_;
+	QComboBox list_;
 
-	ValueType _value_type;
+	ValueType value_type_;
 };
 
 } // widgets
 } // pv
 
-#endif // PULSEVIEW_PV_WIDGETS_SWEEPTIMINGWIDGET_H
+#endif // PULSEVIEW_PV_WIDGETS_SWEEPTIMINGWIDGET_HPP
diff --git a/pv/widgets/timestampspinbox.cpp b/pv/widgets/timestampspinbox.cpp
new file mode 100644
index 0000000..463a846
--- /dev/null
+++ b/pv/widgets/timestampspinbox.cpp
@@ -0,0 +1,121 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2015 Jens Steinhauser <jens.steinhauser 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include "timestampspinbox.hpp"
+
+#include <QLineEdit>
+#include <QRegExp>
+
+namespace pv {
+namespace widgets {
+
+TimestampSpinBox::TimestampSpinBox(QWidget* parent)
+	: QAbstractSpinBox(parent)
+	, precision_(9)
+	, stepsize_("1e-6")
+{
+	connect(this, SIGNAL(editingFinished()), this, SLOT(on_editingFinished()));
+
+	updateEdit();
+}
+
+void TimestampSpinBox::stepBy(int steps)
+{
+	setValue(value_ + steps * stepsize_);
+}
+
+QAbstractSpinBox::StepEnabled TimestampSpinBox::stepEnabled() const
+{
+	return QAbstractSpinBox::StepUpEnabled | QAbstractSpinBox::StepDownEnabled;
+}
+
+unsigned TimestampSpinBox::precision() const
+{
+	return precision_;
+}
+
+void TimestampSpinBox::setPrecision(unsigned precision)
+{
+	precision_ = precision;
+	updateEdit();
+}
+
+const pv::util::Timestamp& TimestampSpinBox::singleStep() const
+{
+	return stepsize_;
+}
+
+void TimestampSpinBox::setSingleStep(const pv::util::Timestamp& step)
+{
+	stepsize_ = step;
+}
+
+const pv::util::Timestamp& TimestampSpinBox::value() const
+{
+	return value_;
+}
+
+QSize TimestampSpinBox::minimumSizeHint() const
+{
+	const QFontMetrics fm(fontMetrics());
+	const int l = round(value_).str().size() + precision_ + 10;
+	const int w = fm.width(QString(l, '0'));
+	const int h = lineEdit()->minimumSizeHint().height();
+	return QSize(w, h);
+}
+
+void TimestampSpinBox::setValue(const pv::util::Timestamp& val)
+{
+	if (val == value_)
+		return;
+
+	value_ = val;
+	updateEdit();
+	Q_EMIT valueChanged(value_);
+}
+
+void TimestampSpinBox::on_editingFinished()
+{
+	if (!lineEdit()->isModified())
+		return;
+	lineEdit()->setModified(false);
+
+	QRegExp re(R"(\s*([-+]?)\s*([0-9]+\.?[0-9]*).*)");
+
+	if (re.exactMatch(text())) {
+		QStringList captures = re.capturedTexts();
+		captures.removeFirst(); // remove entire match
+		QString str = captures.join("");
+		setValue(pv::util::Timestamp(str.toStdString()));
+	} else {
+		// replace the malformed entered string with the old value
+		updateEdit();
+	}
+}
+
+void TimestampSpinBox::updateEdit()
+{
+	QString newtext = pv::util::format_time_si(
+		value_, pv::util::SIPrefix::none, precision_);
+	lineEdit()->setText(newtext);
+}
+
+} // widgets
+} // pv
diff --git a/pv/widgets/timestampspinbox.hpp b/pv/widgets/timestampspinbox.hpp
new file mode 100644
index 0000000..bac7cac
--- /dev/null
+++ b/pv/widgets/timestampspinbox.hpp
@@ -0,0 +1,93 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2015 Jens Steinhauser <jens.steinhauser 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef PULSEVIEW_PV_WIDGETS_TIMESTAMPSPINBOX_HPP
+#define PULSEVIEW_PV_WIDGETS_TIMESTAMPSPINBOX_HPP
+
+#include "pv/util.hpp"
+
+#include <QAbstractSpinBox>
+
+namespace pv {
+namespace widgets {
+
+class TimestampSpinBox : public QAbstractSpinBox
+{
+	Q_OBJECT
+
+	Q_PROPERTY(unsigned precision
+		READ precision
+		WRITE setPrecision)
+
+	// Needed because of some strange behaviour of the Qt4 MOC that would add
+	// a reference to a 'staticMetaObject' member of 'pv::util' (the namespace)
+	// if pv::util::Timestamp is used directly in the Q_PROPERTY macros below.
+	// Didn't happen with the Qt5 MOC in this case, however others have had
+	// similar problems with Qt5: https://bugreports.qt.io/browse/QTBUG-37519
+	typedef pv::util::Timestamp Timestamp;
+
+	Q_PROPERTY(Timestamp singleStep
+		READ singleStep
+		WRITE setSingleStep)
+
+	Q_PROPERTY(Timestamp value
+		READ value
+		WRITE setValue
+		NOTIFY valueChanged
+		USER true)
+
+public:
+	TimestampSpinBox(QWidget* parent = nullptr);
+
+	void stepBy(int steps) override;
+
+	StepEnabled stepEnabled() const override;
+
+	unsigned precision() const;
+	void setPrecision(unsigned precision);
+
+	const pv::util::Timestamp& singleStep() const;
+	void setSingleStep(const pv::util::Timestamp& step);
+
+	const pv::util::Timestamp& value() const;
+
+	QSize minimumSizeHint() const override;
+
+public Q_SLOTS:
+	void setValue(const pv::util::Timestamp& val);
+
+Q_SIGNALS:
+	void valueChanged(const pv::util::Timestamp&);
+
+private Q_SLOTS:
+	void on_editingFinished();
+
+private:
+	unsigned precision_;
+	pv::util::Timestamp stepsize_;
+	pv::util::Timestamp value_;
+
+	void updateEdit();
+};
+
+} // widgets
+} // pv
+
+#endif
diff --git a/pv/widgets/wellarray.cpp b/pv/widgets/wellarray.cpp
index dcc3b41..1f3bf69 100644
--- a/pv/widgets/wellarray.cpp
+++ b/pv/widgets/wellarray.cpp
@@ -44,9 +44,12 @@
 #include <QStyle>
 #include <QStyleOptionFrame>
 
-#include "wellarray.h"
+#include "wellarray.hpp"
 
-void QWellArray::paintEvent(QPaintEvent *e)
+namespace pv {
+namespace widgets {
+
+void WellArray::paintEvent(QPaintEvent *e)
 {
     QRect r = e->rect();
     int cx = r.x();
@@ -93,11 +96,11 @@ void QWellArray::paintEvent(QPaintEvent *e)
     }
 }
 
-struct QWellArrayData {
+struct WellArrayData {
     QBrush *brush;
 };
 
-QWellArray::QWellArray(int rows, int cols, QWidget *parent)
+WellArray::WellArray(int rows, int cols, QWidget *parent)
     : QWidget(parent)
         ,nrows(rows), ncols(cols)
 {
@@ -111,14 +114,14 @@ QWellArray::QWellArray(int rows, int cols, QWidget *parent)
     selRow = -1;
 }
 
-QSize QWellArray::sizeHint() const
+QSize WellArray::sizeHint() const
 {
     ensurePolished();
     return gridSize().boundedTo(QSize(640, 480));
 }
 
 
-void QWellArray::paintCell(QPainter* p, int row, int col, const QRect &rect)
+void WellArray::paintCell(QPainter* p, int row, int col, const QRect &rect)
 {
     int b = 3; //margin
 
@@ -148,7 +151,7 @@ void QWellArray::paintCell(QPainter* p, int row, int col, const QRect &rect)
 /*!
   Reimplement this function to change the contents of the well array.
  */
-void QWellArray::paintCellContents(QPainter *p, int row, int col, const QRect &r)
+void WellArray::paintCellContents(QPainter *p, int row, int col, const QRect &r)
 {
     if (d) {
         p->fillRect(r, d->brush[row*numCols()+col]);
@@ -160,14 +163,14 @@ void QWellArray::paintCellContents(QPainter *p, int row, int col, const QRect &r
     }
 }
 
-void QWellArray::mousePressEvent(QMouseEvent *e)
+void WellArray::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 */)
+void WellArray::mouseReleaseEvent(QMouseEvent * /* event */)
 {
     // The current cell marker is set to the cell the mouse is clicked in
     setSelected(curRow, curCol);
@@ -179,7 +182,7 @@ void QWellArray::mouseReleaseEvent(QMouseEvent * /* event */)
   the same as the currently selected cell.
 */
 
-void QWellArray::setCurrent(int row, int col)
+void WellArray::setCurrent(int row, int col)
 {
     if ((curRow == row) && (curCol == col))
         return;
@@ -203,7 +206,7 @@ void QWellArray::setCurrent(int row, int col)
 
   Does not set the position of the focus indicator.
 */
-void QWellArray::setSelected(int row, int col)
+void WellArray::setSelected(int row, int col)
 {
     int oldRow = selRow;
     int oldCol = selCol;
@@ -217,18 +220,18 @@ void QWellArray::setSelected(int row, int col)
     updateCell(oldRow, oldCol);
     updateCell(selRow, selCol);
     if (row >= 0)
-        emit selected(row, col);
+        Q_EMIT selected(row, col);
 }
 
-void QWellArray::focusInEvent(QFocusEvent*)
+void WellArray::focusInEvent(QFocusEvent*)
 {
     updateCell(curRow, curCol);
 }
 
-void QWellArray::setCellBrush(int row, int col, const QBrush &b)
+void WellArray::setCellBrush(int row, int col, const QBrush &b)
 {
     if (!d) {
-        d = new QWellArrayData;
+        d = new WellArrayData;
         int i = numRows()*numCols();
         d->brush = new QBrush[i];
     }
@@ -241,7 +244,7 @@ void QWellArray::setCellBrush(int row, int col, const QBrush &b)
   set, Qt::NoBrush is returned.
 */
 
-QBrush QWellArray::cellBrush(int row, int col)
+QBrush WellArray::cellBrush(int row, int col)
 {
     if (d && row >= 0 && row < numRows() && col >= 0 && col < numCols())
         return d->brush[row*numCols()+col];
@@ -253,30 +256,30 @@ QBrush QWellArray::cellBrush(int row, int col)
 /*!\reimp
 */
 
-void QWellArray::focusOutEvent(QFocusEvent*)
+void WellArray::focusOutEvent(QFocusEvent*)
 {
     updateCell(curRow, curCol);
 }
 
 /*\reimp
 */
-void QWellArray::keyPressEvent(QKeyEvent* e)
+void WellArray::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
+        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)
+        if (curCol < numCols()-1)
             setCurrent(curRow, curCol + 1);
         break;
     case Qt::Key_Up:
-        if(curRow > 0)
+        if (curRow > 0)
             setCurrent(curRow - 1, curCol);
         break;
     case Qt::Key_Down:
-        if(curRow < numRows()-1)
+        if (curRow < numRows()-1)
             setCurrent(curRow + 1, curCol);
         break;
     case Qt::Key_Space:
@@ -288,3 +291,6 @@ void QWellArray::keyPressEvent(QKeyEvent* e)
     }
 
 }
+
+} // namespace wellarray
+} // namespace pv
diff --git a/pv/widgets/wellarray.h b/pv/widgets/wellarray.hpp
similarity index 94%
rename from pv/widgets/wellarray.h
rename to pv/widgets/wellarray.hpp
index dad088c..8132eb0 100644
--- a/pv/widgets/wellarray.h
+++ b/pv/widgets/wellarray.hpp
@@ -41,16 +41,19 @@
 
 #include <QWidget>
 
-struct QWellArrayData;
+namespace pv {
+namespace widgets {
 
-class QWellArray : public QWidget
+struct WellArrayData;
+
+class WellArray : 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);
+    WellArray(int rows, int cols, QWidget* parent=0);
     QString cellContent(int row, int col) const;
 
     int selectedColumn() const { return selCol; }
@@ -104,7 +107,7 @@ public:
 
     inline void updateCell(int row, int column) { update(cellGeometry(row, column)); }
 
-signals:
+Q_SIGNALS:
     void selected(int row, int col);
 
 protected:
@@ -119,7 +122,7 @@ protected:
     void paintEvent(QPaintEvent *);
 
 private:
-    Q_DISABLE_COPY(QWellArray)
+    Q_DISABLE_COPY(WellArray)
 
     int nrows;
     int ncols;
@@ -129,5 +132,8 @@ private:
     int curCol;
     int selRow;
     int selCol;
-    QWellArrayData *d;
+    WellArrayData *d;
 };
+
+} // namespace wellarray
+} // namespace pv
diff --git a/signalhandler.cpp b/signalhandler.cpp
index 8c9244b..fbbcf22 100644
--- a/signalhandler.cpp
+++ b/signalhandler.cpp
@@ -18,7 +18,7 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#include "signalhandler.h"
+#include "signalhandler.hpp"
 
 #include <assert.h>
 #include <signal.h>
@@ -29,11 +29,11 @@
 #include <QDebug>
 #include <QSocketNotifier>
 
-int SignalHandler::_sockets[2];
+int SignalHandler::sockets_[2];
 
 bool SignalHandler::prepare_signals()
 {
-	if(socketpair(AF_UNIX, SOCK_STREAM, 0, _sockets) != 0)
+	if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockets_) != 0)
 		return false;
 
 	struct sigaction sig_action;
@@ -42,10 +42,10 @@ bool SignalHandler::prepare_signals()
 	sigemptyset(&sig_action.sa_mask);
 	sig_action.sa_flags = SA_RESTART;
 
-	if(sigaction(SIGINT, &sig_action, 0) != 0 ||
+	if (sigaction(SIGINT, &sig_action, 0) != 0 ||
 		sigaction(SIGTERM, &sig_action, 0) != 0) {
-		close(_sockets[0]);
-		close(_sockets[1]);
+		close(sockets_[0]);
+		close(sockets_[1]);
 		return false;
 	}
 
@@ -53,21 +53,20 @@ bool SignalHandler::prepare_signals()
 }
 
 SignalHandler::SignalHandler(QObject* parent) : QObject(parent),
-	_socket_notifier(0)
+	socket_notifier_(0)
 {
-	_socket_notifier = new QSocketNotifier(_sockets[1],
+	socket_notifier_ = new QSocketNotifier(sockets_[1],
 		QSocketNotifier::Read, this);
-	connect(_socket_notifier, SIGNAL(activated(int)),
+	connect(socket_notifier_, SIGNAL(activated(int)),
 		SLOT(on_socket_notifier_activated()));
 }
 
 void SignalHandler::on_socket_notifier_activated()
 {
-	_socket_notifier->setEnabled(false);
+	socket_notifier_->setEnabled(false);
 
 	int sig_number;
-	if(read(_sockets[1], &sig_number, sizeof(int)) !=
-		sizeof(int)) {
+	if (read(sockets_[1], &sig_number, sizeof(int)) != sizeof(int)) {
 		qDebug() << "Failed to catch signal";
 		abort();
 	}
@@ -75,20 +74,19 @@ void SignalHandler::on_socket_notifier_activated()
 	switch(sig_number)
 	{
 	case SIGINT:
-		emit int_received();
+		Q_EMIT int_received();
 		break;
 	case SIGTERM:
-		emit term_received();
+		Q_EMIT term_received();
 		break;
 	}
 
-	_socket_notifier->setEnabled(true);
+	socket_notifier_->setEnabled(true);
 }
 
 void SignalHandler::handle_signals(int sig_number)
 {
-	if(write(_sockets[0], &sig_number, sizeof(int)) !=
-		sizeof(int)) {
+	if (write(sockets_[0], &sig_number, sizeof(int)) != sizeof(int)) {
 		// Failed to handle signal
 		abort();
 	}
diff --git a/signalhandler.h b/signalhandler.hpp
similarity index 83%
rename from signalhandler.h
rename to signalhandler.hpp
index 7ecd612..a8ddccb 100644
--- a/signalhandler.h
+++ b/signalhandler.hpp
@@ -18,8 +18,8 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#ifndef SIGNALHANDLER_H
-#define SIGNALHANDLER_H
+#ifndef SIGNALHANDLER_HPP
+#define SIGNALHANDLER_HPP
 
 #include <QObject>
 
@@ -33,23 +33,23 @@ public:
 	static bool prepare_signals();
 
 public:
-	explicit SignalHandler(QObject* parent = NULL);
+	explicit SignalHandler(QObject* parent = nullptr);
 
-signals:
+Q_SIGNALS:
 	void int_received();
 	void term_received();
 
-private slots:
+private Q_SLOTS:
 	void on_socket_notifier_activated();
 
 private:
 	static void handle_signals(int sig_number);
 
 private:
-	QSocketNotifier* _socket_notifier;
+	QSocketNotifier* socket_notifier_;
 
 private:
-	static int _sockets[2];
+	static int sockets_[2];
 };
 
-#endif // SIGNALHANDLER_H
+#endif // SIGNALHANDLER_HPP
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 0ea69f0..ef98a9c 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -18,111 +18,120 @@
 ## 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/session.cpp
+	${PROJECT_SOURCE_DIR}/pv/storesession.cpp
+	${PROJECT_SOURCE_DIR}/pv/util.cpp
+	${PROJECT_SOURCE_DIR}/pv/binding/binding.cpp
+	${PROJECT_SOURCE_DIR}/pv/binding/device.cpp
 	${PROJECT_SOURCE_DIR}/pv/data/analog.cpp
-	${PROJECT_SOURCE_DIR}/pv/data/analogsnapshot.cpp
+	${PROJECT_SOURCE_DIR}/pv/data/analogsegment.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/logicsegment.cpp
+	${PROJECT_SOURCE_DIR}/pv/data/segment.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/devices/device.cpp
+	${PROJECT_SOURCE_DIR}/pv/devices/file.cpp
+	${PROJECT_SOURCE_DIR}/pv/devices/hardwaredevice.cpp
+	${PROJECT_SOURCE_DIR}/pv/devices/inputfile.cpp
+	${PROJECT_SOURCE_DIR}/pv/devices/sessionfile.cpp
+	${PROJECT_SOURCE_DIR}/pv/prop/bool.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/popups/channels.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/flag.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/logicsignal.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/rowitem.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/signalscalehandle.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/timeitem.cpp
 	${PROJECT_SOURCE_DIR}/pv/view/timemarker.cpp
 	${PROJECT_SOURCE_DIR}/pv/view/trace.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/tracegroup.cpp
 	${PROJECT_SOURCE_DIR}/pv/view/tracepalette.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/tracetreeitem.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/tracetreeitemowner.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/triggermarker.cpp
 	${PROJECT_SOURCE_DIR}/pv/view/view.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/viewitem.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/viewitemowner.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/viewitempaintparams.cpp
 	${PROJECT_SOURCE_DIR}/pv/view/viewport.cpp
+	${PROJECT_SOURCE_DIR}/pv/view/viewwidget.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/popuptoolbutton.cpp
+	${PROJECT_SOURCE_DIR}/pv/widgets/sweeptimingwidget.cpp
+	${PROJECT_SOURCE_DIR}/pv/widgets/timestampspinbox.cpp
 	${PROJECT_SOURCE_DIR}/pv/widgets/wellarray.cpp
-	data/analogsnapshot.cpp
-	data/logicsnapshot.cpp
+	data/analogsegment.cpp
+	data/logicsegment.cpp
+	view/ruler.cpp
 	test.cpp
+	util.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
+	${PROJECT_SOURCE_DIR}/pv/session.hpp
+	${PROJECT_SOURCE_DIR}/pv/storesession.hpp
+	${PROJECT_SOURCE_DIR}/pv/binding/device.hpp
+	${PROJECT_SOURCE_DIR}/pv/devices/device.hpp
+	${PROJECT_SOURCE_DIR}/pv/popups/channels.hpp
+	${PROJECT_SOURCE_DIR}/pv/popups/deviceoptions.hpp
+	${PROJECT_SOURCE_DIR}/pv/prop/bool.hpp
+	${PROJECT_SOURCE_DIR}/pv/prop/double.hpp
+	${PROJECT_SOURCE_DIR}/pv/prop/enum.hpp
+	${PROJECT_SOURCE_DIR}/pv/prop/int.hpp
+	${PROJECT_SOURCE_DIR}/pv/prop/property.hpp
+	${PROJECT_SOURCE_DIR}/pv/prop/string.hpp
+	${PROJECT_SOURCE_DIR}/pv/view/cursor.hpp
+	${PROJECT_SOURCE_DIR}/pv/view/flag.hpp
+	${PROJECT_SOURCE_DIR}/pv/view/header.hpp
+	${PROJECT_SOURCE_DIR}/pv/view/logicsignal.hpp
+	${PROJECT_SOURCE_DIR}/pv/view/marginwidget.hpp
+	${PROJECT_SOURCE_DIR}/pv/view/rowitem.hpp
+	${PROJECT_SOURCE_DIR}/pv/view/ruler.hpp
+	${PROJECT_SOURCE_DIR}/pv/view/signal.hpp
+	${PROJECT_SOURCE_DIR}/pv/view/signalscalehandle.hpp
+	${PROJECT_SOURCE_DIR}/pv/view/timeitem.hpp
+	${PROJECT_SOURCE_DIR}/pv/view/timemarker.hpp
+	${PROJECT_SOURCE_DIR}/pv/view/trace.hpp
+	${PROJECT_SOURCE_DIR}/pv/view/tracegroup.hpp
+	${PROJECT_SOURCE_DIR}/pv/view/tracetreeitem.hpp
+	${PROJECT_SOURCE_DIR}/pv/view/triggermarker.hpp
+	${PROJECT_SOURCE_DIR}/pv/view/view.hpp
+	${PROJECT_SOURCE_DIR}/pv/view/viewitem.hpp
+	${PROJECT_SOURCE_DIR}/pv/view/viewport.hpp
+	${PROJECT_SOURCE_DIR}/pv/view/viewwidget.hpp
+	${PROJECT_SOURCE_DIR}/pv/widgets/colourbutton.hpp
+	${PROJECT_SOURCE_DIR}/pv/widgets/colourpopup.hpp
+	${PROJECT_SOURCE_DIR}/pv/widgets/popup.hpp
+	${PROJECT_SOURCE_DIR}/pv/widgets/popuptoolbutton.hpp
+	${PROJECT_SOURCE_DIR}/pv/widgets/sweeptimingwidget.hpp
+	${PROJECT_SOURCE_DIR}/pv/widgets/timestampspinbox.hpp
+	${PROJECT_SOURCE_DIR}/pv/widgets/wellarray.hpp
 )
 
 if(ENABLE_DECODE)
 	list(APPEND pulseview_TEST_SOURCES
+		${PROJECT_SOURCE_DIR}/pv/binding/decoder.cpp
 		${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
@@ -130,41 +139,26 @@ if(ENABLE_DECODE)
 	)
 
 	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
+		${PROJECT_SOURCE_DIR}/pv/data/decoderstack.hpp
+		${PROJECT_SOURCE_DIR}/pv/view/decodetrace.hpp
+		${PROJECT_SOURCE_DIR}/pv/widgets/decodergroupbox.hpp
+		${PROJECT_SOURCE_DIR}/pv/widgets/decodermenu.hpp
 	)
 endif()
 
-qt4_wrap_cpp(pulseview_TEST_HEADERS_MOC ${pulseview_TEST_HEADERS})
-
-if(ENABLE_DECODE)
-	add_definitions(-DENABLE_DECODE)
+if(Qt5Core_FOUND)
+	qt5_wrap_cpp(pulseview_TEST_HEADERS_MOC ${pulseview_TEST_HEADERS})
+else()
+	# Workaround for QTBUG-22829: -DBOOST_NEXT_PRIOR_HPP_INCLUDED.
+	# https://bugreports.qt.io/browse/QTBUG-22829
+	qt4_wrap_cpp(pulseview_TEST_HEADERS_MOC ${pulseview_TEST_HEADERS}
+		OPTIONS -DBOOST_NEXT_PRIOR_HPP_INCLUDED)
+	include(${QT_USE_FILE})
 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")
+	add_definitions(-DBOOST_TEST_DYN_LINK)
 endif()
 
 add_executable(pulseview-test
diff --git a/test/data/analogsnapshot.cpp b/test/data/analogsegment.cpp
similarity index 66%
rename from test/data/analogsnapshot.cpp
rename to test/data/analogsegment.cpp
index 200d262..2593bf6 100644
--- a/test/data/analogsnapshot.cpp
+++ b/test/data/analogsegment.cpp
@@ -24,13 +24,14 @@
 
 #include <boost/test/unit_test.hpp>
 
-#include "../../pv/data/analogsnapshot.h"
+#include <pv/data/analogsegment.hpp>
 
-using pv::data::AnalogSnapshot;
+using pv::data::AnalogSegment;
 
-BOOST_AUTO_TEST_SUITE(AnalogSnapshotTest)
+#if 0
+BOOST_AUTO_TEST_SUITE(AnalogSegmentTest)
 
-void push_analog(AnalogSnapshot &s, unsigned int num_samples,
+void push_analog(AnalogSegment &s, unsigned int num_samples,
 	float value)
 {
 	float *const data = new float[num_samples];
@@ -43,18 +44,18 @@ void push_analog(AnalogSnapshot &s, unsigned int num_samples,
 
 BOOST_AUTO_TEST_CASE(Basic)
 {
-	// Create an empty AnalogSnapshot object
-	AnalogSnapshot s;
+	// Create an empty AnalogSegment object
+	AnalogSegment s;
 
-	//----- Test AnalogSnapshot::push_analog -----//
+	//----- Test AnalogSegment::push_analog -----//
 
 	BOOST_CHECK(s.get_sample_count() == 0);
-	for (unsigned int i = 0; i < AnalogSnapshot::ScaleStepCount; i++)
+	for (unsigned int i = 0; i < AnalogSegment::ScaleStepCount; i++)
 	{
-		const AnalogSnapshot::Envelope &m = s._envelope_levels[i];
+		const AnalogSegment::Envelope &m = s.envelope_levels_[i];
 		BOOST_CHECK_EQUAL(m.length, 0);
 		BOOST_CHECK_EQUAL(m.data_length, 0);
-		BOOST_CHECK(m.samples == NULL);
+		BOOST_CHECK(m.samples == nullptr);
 	}
 
 	// Push 8 samples of all zeros
@@ -63,12 +64,12 @@ BOOST_AUTO_TEST_CASE(Basic)
 	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++)
+	for (unsigned int i = 0; i < AnalogSegment::ScaleStepCount; i++)
 	{
-		const AnalogSnapshot::Envelope &m = s._envelope_levels[i];
+		const AnalogSegment::Envelope &m = s.envelope_levels_[i];
 		BOOST_CHECK_EQUAL(m.length, 0);
 		BOOST_CHECK_EQUAL(m.data_length, 0);
-		BOOST_CHECK(m.samples == NULL);
+		BOOST_CHECK(m.samples == nullptr);
 	}
 
 	// Push 8 samples of 1.0s to bring the total up to 16
@@ -76,39 +77,40 @@ BOOST_AUTO_TEST_CASE(Basic)
 
 	// 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];
+	const AnalogSegment::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.data_length, AnalogSegment::EnvelopeDataUnit);
+	BOOST_REQUIRE(e0.samples != nullptr);
 	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++)
+	for (unsigned int i = 1; i < AnalogSegment::ScaleStepCount; i++)
 	{
-		const AnalogSnapshot::Envelope &m = s._envelope_levels[i];
+		const AnalogSegment::Envelope &m = s.envelope_levels_[i];
 		BOOST_CHECK_EQUAL(m.length, 0);
 		BOOST_CHECK_EQUAL(m.data_length, 0);
-		BOOST_CHECK(m.samples == NULL);
+		BOOST_CHECK(m.samples == nullptr);
 	}
 
 	// 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);
+	BOOST_CHECK_EQUAL(e0.data_length, AnalogSegment::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];
+	const AnalogSegment::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.data_length, AnalogSegment::EnvelopeDataUnit);
+	BOOST_REQUIRE(e1.samples != nullptr);
 	BOOST_CHECK_EQUAL(e1.samples[0].min, -1.0f);
 	BOOST_CHECK_EQUAL(e1.samples[0].max, 1.0f);
 }
 
 BOOST_AUTO_TEST_SUITE_END()
+#endif
diff --git a/test/data/decoderstack.cpp b/test/data/decoderstack.cpp
index d6e4f75..70ed7e0 100644
--- a/test/data/decoderstack.cpp
+++ b/test/data/decoderstack.cpp
@@ -21,35 +21,36 @@
 #include <libsigrokdecode/libsigrokdecode.h> /* First, so we avoid a _POSIX_C_SOURCE warning. */
 #include <boost/test/unit_test.hpp>
 
-#include <libsigrok/libsigrok.h>
+#include <libsigrokcxx/libsigrokcxx.hpp>
 
-#include "../../pv/data/decoderstack.h"
-#include "../../pv/devicemanager.h"
-#include "../../pv/sigsession.h"
-#include "../../pv/view/decodetrace.h"
+#include "../../pv/data/decoderstack.hpp"
+#include "../../pv/devicemanager.hpp"
+#include "../../pv/session.hpp"
+#include "../../pv/view/decodetrace.hpp"
 
-using boost::shared_ptr;
 using pv::data::DecoderStack;
 using pv::data::decode::Decoder;
 using pv::view::DecodeTrace;
+using std::shared_ptr;
 using std::vector;
 
+#if 0
 BOOST_AUTO_TEST_SUITE(DecoderStackTest)
 
 BOOST_AUTO_TEST_CASE(TwoDecoderStack)
 {
-	sr_context *ctx = NULL;
+	sr_context *ctx = nullptr;
 
 	BOOST_REQUIRE(sr_init(&ctx) == SR_OK);
 	BOOST_REQUIRE(ctx);
 
-	BOOST_REQUIRE(srd_init(NULL) == SRD_OK);
+	BOOST_REQUIRE(srd_init(nullptr) == SRD_OK);
 
 	srd_decoder_load_all();
 
 	{
 		pv::DeviceManager dm(ctx);
-		pv::SigSession ss(dm);
+		pv::Session ss(dm);
 
 		const GSList *l = srd_decoder_list();
 		BOOST_REQUIRE(l);
@@ -70,8 +71,8 @@ BOOST_AUTO_TEST_CASE(TwoDecoderStack)
 		BOOST_REQUIRE(dec1);
 
 		// Wait for the decode threads to complete
-		dec0->_decode_thread.join();
-		dec1->_decode_thread.join();
+		dec0->decode_thread_.join();
+		dec1->decode_thread_.join();
 
 		// Check there were no errors
 		BOOST_CHECK_EQUAL(dec0->error_message().isEmpty(), true);
@@ -84,3 +85,4 @@ BOOST_AUTO_TEST_CASE(TwoDecoderStack)
 }
 
 BOOST_AUTO_TEST_SUITE_END()
+#endif
diff --git a/test/data/logicsnapshot.cpp b/test/data/logicsegment.cpp
similarity index 68%
rename from test/data/logicsnapshot.cpp
rename to test/data/logicsegment.cpp
index 5fc1721..fabcbd4 100644
--- a/test/data/logicsnapshot.cpp
+++ b/test/data/logicsegment.cpp
@@ -24,14 +24,23 @@
 
 #include <boost/test/unit_test.hpp>
 
-#include "../../pv/data/logicsnapshot.h"
+#include <pv/data/logicsegment.hpp>
 
-using pv::data::LogicSnapshot;
+using pv::data::LogicSegment;
 using std::vector;
 
-BOOST_AUTO_TEST_SUITE(LogicSnapshotTest)
+// Dummy, remove again when unit tests are fixed.
+BOOST_AUTO_TEST_SUITE(DummyTestSuite)
+BOOST_AUTO_TEST_CASE(DummyTestCase)
+{
+	BOOST_CHECK_EQUAL(1, 1);
+}
+BOOST_AUTO_TEST_SUITE_END()
+
+#if 0
+BOOST_AUTO_TEST_SUITE(LogicSegmentTest)
 
-void push_logic(LogicSnapshot &s, unsigned int length, uint8_t value)
+void push_logic(LogicSegment &s, unsigned int length, uint8_t value)
 {
 	sr_datafeed_logic logic;
 	logic.unitsize = 1;
@@ -44,40 +53,40 @@ void push_logic(LogicSnapshot &s, unsigned int length, uint8_t value)
 
 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(LogicSegment::pow2_ceil(0, 0), 0);
+	BOOST_CHECK_EQUAL(LogicSegment::pow2_ceil(1, 0), 1);
+	BOOST_CHECK_EQUAL(LogicSegment::pow2_ceil(2, 0), 2);
 
 	BOOST_CHECK_EQUAL(
-		LogicSnapshot::pow2_ceil(INT64_MIN, 0), INT64_MIN);
+		LogicSegment::pow2_ceil(INT64_MIN, 0), INT64_MIN);
 	BOOST_CHECK_EQUAL(
-		LogicSnapshot::pow2_ceil(INT64_MAX, 0), INT64_MAX);
+		LogicSegment::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_CHECK_EQUAL(LogicSegment::pow2_ceil(0, 1), 0);
+	BOOST_CHECK_EQUAL(LogicSegment::pow2_ceil(1, 1), 2);
+	BOOST_CHECK_EQUAL(LogicSegment::pow2_ceil(2, 1), 2);
+	BOOST_CHECK_EQUAL(LogicSegment::pow2_ceil(3, 1), 4);
 }
 
 BOOST_AUTO_TEST_CASE(Basic)
 {
-	// Create an empty LogicSnapshot object
+	// Create an empty LogicSegment object
 	sr_datafeed_logic logic;
 	logic.length = 0;
 	logic.unitsize = 1;
-	logic.data = NULL;
+	logic.data = nullptr;
 
-	LogicSnapshot s(logic);
+	LogicSegment s(logic);
 
-	//----- Test LogicSnapshot::push_logic -----//
+	//----- Test LogicSegment::push_logic -----//
 
 	BOOST_CHECK(s.get_sample_count() == 0);
-	for (unsigned int i = 0; i < LogicSnapshot::ScaleStepCount; i++)
+	for (unsigned int i = 0; i < LogicSegment::ScaleStepCount; i++)
 	{
-		const LogicSnapshot::MipMapLevel &m = s._mip_map[i];
+		const LogicSegment::MipMapLevel &m = s.mip_map_[i];
 		BOOST_CHECK_EQUAL(m.length, 0);
 		BOOST_CHECK_EQUAL(m.data_length, 0);
-		BOOST_CHECK(m.data == NULL);
+		BOOST_CHECK(m.data == nullptr);
 	}
 
 	// Push 8 samples of all zeros
@@ -86,12 +95,12 @@ BOOST_AUTO_TEST_CASE(Basic)
 	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++)
+	for (unsigned int i = 0; i < LogicSegment::ScaleStepCount; i++)
 	{
-		const LogicSnapshot::MipMapLevel &m = s._mip_map[i];
+		const LogicSegment::MipMapLevel &m = s.mip_map_[i];
 		BOOST_CHECK_EQUAL(m.length, 0);
 		BOOST_CHECK_EQUAL(m.data_length, 0);
-		BOOST_CHECK(m.data == NULL);
+		BOOST_CHECK(m.data == nullptr);
 	}
 
 	// Push 8 samples of 0x11s to bring the total up to 16
@@ -99,41 +108,41 @@ BOOST_AUTO_TEST_CASE(Basic)
 
 	// 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];
+	const LogicSegment::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(m0.data_length, LogicSegment::MipMapDataUnit);
+	BOOST_REQUIRE(m0.data != nullptr);
 	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++)
+	for (unsigned int i = 1; i < LogicSegment::ScaleStepCount; i++)
 	{
-		const LogicSnapshot::MipMapLevel &m = s._mip_map[i];
+		const LogicSegment::MipMapLevel &m = s.mip_map_[i];
 		BOOST_CHECK_EQUAL(m.length, 0);
 		BOOST_CHECK_EQUAL(m.data_length, 0);
-		BOOST_CHECK(m.data == NULL);
+		BOOST_CHECK(m.data == nullptr);
 	}
 
 	// 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(m0.data_length, LogicSegment::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];
+	const LogicSegment::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(m1.data_length, LogicSegment::MipMapDataUnit);
+	BOOST_REQUIRE(m1.data != nullptr);
 	BOOST_CHECK_EQUAL(((uint8_t*)m1.data)[0], 0x11);
 
-	//----- Test LogicSnapshot::get_subsampled_edges -----//
+	//----- Test LogicSegment::get_subsampled_edges -----//
 
 	// Test a full view at full zoom.
-	vector<LogicSnapshot::EdgePair> edges;
+	vector<LogicSegment::EdgePair> edges;
 	s.get_subsampled_edges(edges, 0, 255, 1, 0);
 	BOOST_REQUIRE_EQUAL(edges.size(), 4);
 
@@ -167,19 +176,19 @@ BOOST_AUTO_TEST_CASE(LargeData)
 	for (unsigned int i = 0; i < Length; i++)
 		*data++ = (uint8_t)(i >> 8);
 
-	LogicSnapshot s(logic);
+	LogicSegment 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);
+	BOOST_CHECK_EQUAL(s.mip_map_[0].length, 62500);
+	BOOST_CHECK_EQUAL(s.mip_map_[0].data_length,
+		LogicSegment::MipMapDataUnit);
+	BOOST_REQUIRE(s.mip_map_[0].data != nullptr);
 
 	prev_sample = 0;
-	for (unsigned int i = 0; i < s._mip_map[0].length;)
+	for (unsigned int i = 0; i < s.mip_map_[0].length;)
 	{
 		BOOST_TEST_MESSAGE("Testing mip_map[0].data[" << i << "]");
 
@@ -188,7 +197,7 @@ BOOST_AUTO_TEST_CASE(LargeData)
 			prev_sample ^ sample);
 		prev_sample = sample;
 
-		for (int j = 1; i < s._mip_map[0].length && j < 16; j++)
+		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);
@@ -196,13 +205,13 @@ BOOST_AUTO_TEST_CASE(LargeData)
 	}
 
 	// 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);
+	BOOST_CHECK_EQUAL(s.mip_map_[1].length, 3906);
+	BOOST_CHECK_EQUAL(s.mip_map_[1].data_length,
+		LogicSegment::MipMapDataUnit);
+	BOOST_REQUIRE(s.mip_map_[1].data != nullptr);
 
 	prev_sample = 0;
-	for (unsigned int i = 0; i < s._mip_map[1].length; i++)
+	for (unsigned int i = 0; i < s.mip_map_[1].length; i++)
 	{
 		BOOST_TEST_MESSAGE("Testing mip_map[1].data[" << i << "]");
 
@@ -214,13 +223,13 @@ BOOST_AUTO_TEST_CASE(LargeData)
 	}
 
 	// 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);
+	BOOST_CHECK_EQUAL(s.mip_map_[2].length, 244);
+	BOOST_CHECK_EQUAL(s.mip_map_[2].data_length,
+		LogicSegment::MipMapDataUnit);
+	BOOST_REQUIRE(s.mip_map_[2].data != nullptr);
 
 	prev_sample = 0;
-	for (unsigned int i = 0; i < s._mip_map[2].length; i++)
+	for (unsigned int i = 0; i < s.mip_map_[2].length; i++)
 	{
 		BOOST_TEST_MESSAGE("Testing mip_map[2].data[" << i << "]");
 
@@ -232,27 +241,27 @@ BOOST_AUTO_TEST_CASE(LargeData)
 	}
 
 	// 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);
+	BOOST_CHECK_EQUAL(s.mip_map_[3].length, 15);
+	BOOST_CHECK_EQUAL(s.mip_map_[3].data_length,
+		LogicSegment::MipMapDataUnit);
+	BOOST_REQUIRE(s.mip_map_[3].data != nullptr);
 
-	for (unsigned int i = 0; i < s._mip_map[3].length; i++)
-		BOOST_CHECK_EQUAL(*((uint8_t*)s._mip_map[3].data + i),
+	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++)
+	for (unsigned int i = 4; i < LogicSegment::ScaleStepCount; i++)
 	{
-		const LogicSnapshot::MipMapLevel &m = s._mip_map[i];
+		const LogicSegment::MipMapLevel &m = s.mip_map_[i];
 		BOOST_CHECK_EQUAL(m.length, 0);
 		BOOST_CHECK_EQUAL(m.data_length, 0);
-		BOOST_CHECK(m.data == NULL);
+		BOOST_CHECK(m.data == nullptr);
 	}
 
-	//----- Test LogicSnapshot::get_subsampled_edges -----//
+	//----- Test LogicSegment::get_subsampled_edges -----//
 	// Check in normal case
-	vector<LogicSnapshot::EdgePair> edges;
+	vector<LogicSegment::EdgePair> edges;
 	s.get_subsampled_edges(edges, 0, Length-1, 1, 7);
 
 	BOOST_CHECK_EQUAL(edges.size(), 32);
@@ -278,9 +287,9 @@ BOOST_AUTO_TEST_CASE(Pulses)
 	const int Period = 64;
 	const int Length = Cycles * Period;
 
-	vector<LogicSnapshot::EdgePair> edges;
+	vector<LogicSegment::EdgePair> edges;
 
-	//----- Create a LogicSnapshot -----//
+	//----- Create a LogicSegment -----//
 	sr_datafeed_logic logic;
 	logic.unitsize = 1;
 	logic.length = Length;
@@ -293,23 +302,23 @@ BOOST_AUTO_TEST_CASE(Pulses)
 			*p++ = 0x00;
 	}
 
-	LogicSnapshot s(logic);
+	LogicSegment 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);
+	BOOST_CHECK_EQUAL(s.mip_map_[0].length, 12);
+	BOOST_CHECK_EQUAL(s.mip_map_[0].data_length,
+		LogicSegment::MipMapDataUnit);
+	BOOST_REQUIRE(s.mip_map_[0].data != nullptr);
 
-	for (unsigned int i = 0; i < s._mip_map[0].length;) {
+	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++) {
+			i < s.mip_map_[0].length &&
+			j < Period/LogicSegment::MipMapScaleFactor; j++) {
 			BOOST_TEST_MESSAGE(
 				"Testing mip_map[0].data[" << i << "]");
 			BOOST_CHECK_EQUAL(s.get_subsample(0, i++) & 0xFF, 0x00);
@@ -317,11 +326,11 @@ BOOST_AUTO_TEST_CASE(Pulses)
 	}
 
 	// Check the higher levels are all inactive
-	for (unsigned int i = 1; i < LogicSnapshot::ScaleStepCount; i++) {
-		const LogicSnapshot::MipMapLevel &m = s._mip_map[i];
+	for (unsigned int i = 1; i < LogicSegment::ScaleStepCount; i++) {
+		const LogicSegment::MipMapLevel &m = s.mip_map_[i];
 		BOOST_CHECK_EQUAL(m.length, 0);
 		BOOST_CHECK_EQUAL(m.data_length, 0);
-		BOOST_CHECK(m.data == NULL);
+		BOOST_CHECK(m.data == nullptr);
 	}
 
 	//----- Test get_subsampled_edges at reduced scale -----//
@@ -341,9 +350,9 @@ BOOST_AUTO_TEST_CASE(LongPulses)
 	const int Length = Cycles * Period;
 
 	int j;
-	vector<LogicSnapshot::EdgePair> edges;
+	vector<LogicSegment::EdgePair> edges;
 
-	//----- Create a LogicSnapshot -----//
+	//----- Create a LogicSegment -----//
 	sr_datafeed_logic logic;
 	logic.unitsize = 8;
 	logic.length = Length * 8;
@@ -357,25 +366,25 @@ BOOST_AUTO_TEST_CASE(LongPulses)
 			*p++ = 0;
 	}
 
-	LogicSnapshot s(logic);
+	LogicSegment 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);
+	BOOST_CHECK_EQUAL(s.mip_map_[0].length, 12);
+	BOOST_CHECK_EQUAL(s.mip_map_[0].data_length,
+		LogicSegment::MipMapDataUnit);
+	BOOST_REQUIRE(s.mip_map_[0].data != nullptr);
 
-	for (unsigned int i = 0; i < s._mip_map[0].length;) {
-		for (j = 0; i < s._mip_map[0].length && j < 2; j++) {
+	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++) {
+		for (; i < s.mip_map_[0].length &&
+			j < Period/LogicSegment::MipMapScaleFactor; j++) {
 			BOOST_TEST_MESSAGE(
 				"Testing mip_map[0].data[" << i << "]");
 			BOOST_CHECK_EQUAL(s.get_subsample(0, i++), 0);
@@ -383,11 +392,11 @@ BOOST_AUTO_TEST_CASE(LongPulses)
 	}
 
 	// Check the higher levels are all inactive
-	for (unsigned int i = 1; i < LogicSnapshot::ScaleStepCount; i++) {
-		const LogicSnapshot::MipMapLevel &m = s._mip_map[i];
+	for (unsigned int i = 1; i < LogicSegment::ScaleStepCount; i++) {
+		const LogicSegment::MipMapLevel &m = s.mip_map_[i];
 		BOOST_CHECK_EQUAL(m.length, 0);
 		BOOST_CHECK_EQUAL(m.data_length, 0);
-		BOOST_CHECK(m.data == NULL);
+		BOOST_CHECK(m.data == nullptr);
 	}
 
 	//----- Test get_subsampled_edges at a full scale -----//
@@ -438,7 +447,7 @@ BOOST_AUTO_TEST_CASE(LisaMUsbHid)
 	bool state = false;
 	int lastEdgePos = 0;
 
-	//----- Create a LogicSnapshot -----//
+	//----- Create a LogicSegment -----//
 	sr_datafeed_logic logic;
 	logic.unitsize = 1;
 	logic.length = Length;
@@ -454,10 +463,10 @@ BOOST_AUTO_TEST_CASE(LisaMUsbHid)
 		state = !state;
 	}
 
-	LogicSnapshot s(logic);
+	LogicSegment s(logic);
 	delete[] (uint64_t*)logic.data;
 
-	vector<LogicSnapshot::EdgePair> edges;
+	vector<LogicSegment::EdgePair> edges;
 
 
 	/* The trailing edge of the pulse train is falling in the source data.
@@ -470,9 +479,9 @@ BOOST_AUTO_TEST_CASE(LisaMUsbHid)
 }
 
 /*
- * This test checks the rendering of wide data (more than 8 probes)
+ * This test checks the rendering of wide data (more than 8 channels)
  * 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.
+ * they would toggle during every sample if treated like 8 channels.
  * The packet contains a large number of samples, so the mipmap generation kicks
  * in.
  *
@@ -491,9 +500,9 @@ BOOST_AUTO_TEST_CASE(WideData)
 	for (int i = 0; i < Length; i++)
 		data[i] = 0x0FF0;
 
-	LogicSnapshot s(logic);
+	LogicSegment s(logic);
 
-	vector<LogicSnapshot::EdgePair> edges;
+	vector<LogicSegment::EdgePair> edges;
 
 	edges.clear();
 	s.get_subsampled_edges(edges, 0, Length-1, 1, 0);
@@ -523,12 +532,13 @@ BOOST_AUTO_TEST_CASE(Sixteen)
 	for (int i = 0; i < Length; i++)
 		data[i] = 0xFFFE;
 
-	LogicSnapshot s(logic);
+	LogicSegment s(logic);
 
-	vector<LogicSnapshot::EdgePair> edges;
+	vector<LogicSegment::EdgePair> edges;
 	s.get_subsampled_edges(edges, 0, 2, 0.0004, 1);
 
 	BOOST_CHECK_EQUAL(edges.size(), 2);
 }
 
 BOOST_AUTO_TEST_SUITE_END()
+#endif
diff --git a/test/test.cpp b/test/test.cpp
index 04c9981..2d67f29 100644
--- a/test/test.cpp
+++ b/test/test.cpp
@@ -20,3 +20,9 @@
 
 #define BOOST_TEST_MAIN
 #include <boost/test/unit_test.hpp>
+#include "test/test.hpp"
+
+std::ostream& operator<<(std::ostream& stream, const QString& str)
+{
+	return stream << str.toUtf8().data();
+}
diff --git a/test/test.cpp b/test/test.hpp
similarity index 77%
copy from test/test.cpp
copy to test/test.hpp
index 04c9981..1400370 100644
--- a/test/test.cpp
+++ b/test/test.hpp
@@ -1,7 +1,7 @@
 /*
  * This file is part of the PulseView project.
  *
- * Copyright (C) 2012 Joel Holdsworth <joel at airwebreathe.org.uk>
+ * Copyright (C) 2015 Jens Steinhauser <jens.steinhauser 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
@@ -18,5 +18,11 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  */
 
-#define BOOST_TEST_MAIN
-#include <boost/test/unit_test.hpp>
+#ifndef PULSEVIEW_TEST_TEST_HPP
+#define PULSEVIEW_TEST_TEST_HPP
+
+#include <QString>
+
+std::ostream& operator<<(std::ostream& stream, const QString& str);
+
+#endif
diff --git a/test/util.cpp b/test/util.cpp
new file mode 100644
index 0000000..21a32c5
--- /dev/null
+++ b/test/util.cpp
@@ -0,0 +1,240 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2015 Jens Steinhauser <jens.steinhauser 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <boost/test/unit_test.hpp>
+
+#include "pv/util.hpp"
+#include "test/test.hpp"
+
+using namespace pv::util;
+using ts = pv::util::Timestamp;
+
+namespace {
+	QChar mu = QChar(0x03BC);
+
+	pv::util::SIPrefix unspecified = pv::util::SIPrefix::unspecified;
+	pv::util::SIPrefix yocto       = pv::util::SIPrefix::yocto;
+	pv::util::SIPrefix nano        = pv::util::SIPrefix::nano;
+	pv::util::SIPrefix micro       = pv::util::SIPrefix::micro;
+	pv::util::SIPrefix milli       = pv::util::SIPrefix::milli;
+	pv::util::SIPrefix none        = pv::util::SIPrefix::none;
+	pv::util::SIPrefix kilo        = pv::util::SIPrefix::kilo;
+	pv::util::SIPrefix yotta       = pv::util::SIPrefix::yotta;
+
+	pv::util::TimeUnit Time = pv::util::TimeUnit::Time;
+}
+
+BOOST_AUTO_TEST_SUITE(UtilTest)
+
+BOOST_AUTO_TEST_CASE(exponent_test)
+{
+	BOOST_CHECK_EQUAL(exponent(SIPrefix::yocto), -24);
+	BOOST_CHECK_EQUAL(exponent(SIPrefix::zepto), -21);
+	BOOST_CHECK_EQUAL(exponent(SIPrefix::atto),  -18);
+	BOOST_CHECK_EQUAL(exponent(SIPrefix::femto), -15);
+	BOOST_CHECK_EQUAL(exponent(SIPrefix::pico),  -12);
+	BOOST_CHECK_EQUAL(exponent(SIPrefix::nano),   -9);
+	BOOST_CHECK_EQUAL(exponent(SIPrefix::micro),  -6);
+	BOOST_CHECK_EQUAL(exponent(SIPrefix::milli),  -3);
+	BOOST_CHECK_EQUAL(exponent(SIPrefix::none),    0);
+	BOOST_CHECK_EQUAL(exponent(SIPrefix::kilo),    3);
+	BOOST_CHECK_EQUAL(exponent(SIPrefix::mega),    6);
+	BOOST_CHECK_EQUAL(exponent(SIPrefix::giga),    9);
+	BOOST_CHECK_EQUAL(exponent(SIPrefix::tera),   12);
+	BOOST_CHECK_EQUAL(exponent(SIPrefix::peta),   15);
+	BOOST_CHECK_EQUAL(exponent(SIPrefix::exa),    18);
+	BOOST_CHECK_EQUAL(exponent(SIPrefix::zetta),  21);
+	BOOST_CHECK_EQUAL(exponent(SIPrefix::yotta),  24);
+}
+
+BOOST_AUTO_TEST_CASE(format_time_si_test)
+{
+	// check prefix calculation
+
+	BOOST_CHECK_EQUAL(format_time_si(ts("0")), "0 s");
+
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-24")),    "+1 ys");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-23")),   "+10 ys");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-22")),  "+100 ys");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-21")),    "+1 zs");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-20")),   "+10 zs");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-19")),  "+100 zs");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-18")),    "+1 as");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-17")),   "+10 as");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-16")),  "+100 as");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-15")),    "+1 fs");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-14")),   "+10 fs");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-13")),  "+100 fs");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-12")),    "+1 ps");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-11")),   "+10 ps");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-10")),  "+100 ps");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-9")),     "+1 ns");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-8")),    "+10 ns");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-7")),   "+100 ns");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-6")),    QString("+1 ") + mu + "s");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-5")),   QString("+10 ") + mu + "s");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-4")),  QString("+100 ") + mu + "s");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-3")),     "+1 ms");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-2")),    "+10 ms");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-1")),   "+100 ms");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e0")),       "+1 s");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e1")),      "+10 s");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e2")),     "+100 s");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e3")),      "+1 ks");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e4")),     "+10 ks");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e5")),    "+100 ks");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e6")),      "+1 Ms");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e7")),     "+10 Ms");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e8")),    "+100 Ms");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e9")),      "+1 Gs");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e10")),    "+10 Gs");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e11")),   "+100 Gs");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e12")),     "+1 Ts");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e13")),    "+10 Ts");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e14")),   "+100 Ts");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e15")),     "+1 Ps");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e16")),    "+10 Ps");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e17")),   "+100 Ps");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e18")),     "+1 Es");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e19")),    "+10 Es");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e20")),   "+100 Es");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e21")),     "+1 Zs");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e22")),    "+10 Zs");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e23")),   "+100 Zs");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e24")),     "+1 Ys");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e25")),    "+10 Ys");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e26")),   "+100 Ys");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e27")),  "+1000 Ys");
+
+	BOOST_CHECK_EQUAL(format_time_si(ts("1234")),              "+1 ks");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1234"), kilo, 3), "+1.234 ks");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1234.5678")),         "+1 ks");
+
+	// check prefix
+
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-24"), yocto),    "+1 ys");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-21"), yocto), "+1000 ys");
+	BOOST_CHECK_EQUAL(format_time_si(ts("0"), yocto),         "0 ys");
+
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-4"), milli),         "+0 ms");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-4"), milli, 1),     "+0.1 ms");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1000"), milli),    "+1000000 ms");
+	BOOST_CHECK_EQUAL(format_time_si(ts("0"), milli),              "0 ms");
+
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-1"), none),       "+0 s");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-1"), none, 1),  "+0.1 s");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e-1"), none, 2), "+0.10 s");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1"), none),          "+1 s");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e1"), none),       "+10 s");
+
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e23"), yotta),       "+0 Ys");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e23"), yotta, 1),  "+0.1 Ys");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1e27"), yotta),    "+1000 Ys");
+	BOOST_CHECK_EQUAL(format_time_si(ts("0"), yotta),           "0 Ys");
+
+	// check precision, rounding
+
+	BOOST_CHECK_EQUAL(format_time_si(ts("1.2345678")),                         "+1 s");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1.4")),                               "+1 s");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1.5")),                               "+2 s");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1.9")),                               "+2 s");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1.2345678"), unspecified, 2),      "+1.23 s");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1.2345678"), unspecified, 3),     "+1.235 s");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1.2345678"), milli, 3),       "+1234.568 ms");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1.2345678"), milli, 0),           "+1235 ms");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1.2"), unspecified, 3),           "+1.200 s");
+
+	// check unit and sign
+
+	BOOST_CHECK_EQUAL(format_time_si(ts("-1"), none, 0, "V", true),  "-1 V");
+	BOOST_CHECK_EQUAL(format_time_si(ts("-1"), none, 0, "V", false), "-1 V");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1"), none, 0, "V", true),   "+1 V");
+	BOOST_CHECK_EQUAL(format_time_si(ts("1"), none, 0, "V", false),   "1 V");
+}
+
+BOOST_AUTO_TEST_CASE(format_time_si_adjusted_test)
+{
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts("-1.5"), milli), "-1500 ms");
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts("-1.0"), milli), "-1000 ms");
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts("-0.2"), milli),  "-200 ms");
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts("-0.1"), milli),  "-100 ms");
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts( "0.0"), milli),     "0 ms");
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts( "0.1"), milli),  "+100 ms");
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts( "0.2"), milli),  "+200 ms");
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts( "0.3"), milli),  "+300 ms");
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts( "0.4"), milli),  "+400 ms");
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts( "0.5"), milli),  "+500 ms");
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts( "0.6"), milli),  "+600 ms");
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts( "0.7"), milli),  "+700 ms");
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts( "0.8"), milli),  "+800 ms");
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts( "0.9"), milli),  "+900 ms");
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts( "1.0"), milli), "+1000 ms");
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts( "1.1"), milli), "+1100 ms");
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts( "1.2"), milli), "+1200 ms");
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts( "1.3"), milli), "+1300 ms");
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts( "1.4"), milli), "+1400 ms");
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts( "1.5"), milli), "+1500 ms");
+
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts( "1.5"), milli, 6), "+1500.000 ms");
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts( "1.5"), nano,  6), "+1500000000 ns");
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts( "1.5"), nano,  8), "+1500000000 ns");
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts( "1.5"), nano,  9), "+1500000000 ns");
+	BOOST_CHECK_EQUAL(format_time_si_adjusted(ts( "1.5"), nano, 10), "+1500000000.0 ns");
+}
+
+BOOST_AUTO_TEST_CASE(format_time_minutes_test)
+{
+	using namespace std::placeholders;
+
+	auto fmt = std::bind(format_time_minutes, _1, _2, true);
+
+	BOOST_CHECK_EQUAL(fmt(ts(    0), 0),    "+0:00");
+	BOOST_CHECK_EQUAL(fmt(ts(    1), 0),    "+0:01");
+	BOOST_CHECK_EQUAL(fmt(ts(   59), 0),    "+0:59");
+	BOOST_CHECK_EQUAL(fmt(ts(   60), 0),    "+1:00");
+	BOOST_CHECK_EQUAL(fmt(ts(   -1), 0),    "-0:01");
+	BOOST_CHECK_EQUAL(fmt(ts(  -59), 0),    "-0:59");
+	BOOST_CHECK_EQUAL(fmt(ts(  -60), 0),    "-1:00");
+	BOOST_CHECK_EQUAL(fmt(ts(  100), 0),    "+1:40");
+	BOOST_CHECK_EQUAL(fmt(ts( -100), 0),    "-1:40");
+	BOOST_CHECK_EQUAL(fmt(ts( 4000), 0), "+1:06:40");
+	BOOST_CHECK_EQUAL(fmt(ts(-4000), 0), "-1:06:40");
+	BOOST_CHECK_EQUAL(fmt(ts(12000), 0), "+3:20:00");
+	BOOST_CHECK_EQUAL(fmt(ts(15000), 0), "+4:10:00");
+	BOOST_CHECK_EQUAL(fmt(ts(20000), 0), "+5:33:20");
+	BOOST_CHECK_EQUAL(fmt(ts(25000), 0), "+6:56:40");
+
+	BOOST_CHECK_EQUAL(fmt(ts("10641906.007008009"), 0), "+123:04:05:06");
+	BOOST_CHECK_EQUAL(fmt(ts("10641906.007008009"), 1), "+123:04:05:06.0");
+	BOOST_CHECK_EQUAL(fmt(ts("10641906.007008009"), 2), "+123:04:05:06.01");
+	BOOST_CHECK_EQUAL(fmt(ts("10641906.007008009"), 3), "+123:04:05:06.007");
+	BOOST_CHECK_EQUAL(fmt(ts("10641906.007008009"), 4), "+123:04:05:06.007 0");
+	BOOST_CHECK_EQUAL(fmt(ts("10641906.007008009"), 5), "+123:04:05:06.007 01");
+	BOOST_CHECK_EQUAL(fmt(ts("10641906.007008009"), 6), "+123:04:05:06.007 008");
+	BOOST_CHECK_EQUAL(fmt(ts("10641906.007008009"), 7), "+123:04:05:06.007 008 0");
+	BOOST_CHECK_EQUAL(fmt(ts("10641906.007008009"), 8), "+123:04:05:06.007 008 01");
+	BOOST_CHECK_EQUAL(fmt(ts("10641906.007008009"), 9), "+123:04:05:06.007 008 009");
+
+	BOOST_CHECK_EQUAL(format_time_minutes(ts(   0), 0, false),  "0:00");
+	BOOST_CHECK_EQUAL(format_time_minutes(ts( 100), 0, false),  "1:40");
+	BOOST_CHECK_EQUAL(format_time_minutes(ts(-100), 0, false), "-1:40");
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/view/ruler.cpp b/test/view/ruler.cpp
new file mode 100644
index 0000000..a6dd1c9
--- /dev/null
+++ b/test/view/ruler.cpp
@@ -0,0 +1,172 @@
+/*
+ * This file is part of the PulseView project.
+ *
+ * Copyright (C) 2015 Jens Steinhauser <jens.steinhauser 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <boost/test/unit_test.hpp>
+#include <boost/test/floating_point_comparison.hpp>
+
+#include "pv/view/ruler.hpp"
+#include "test/test.hpp"
+
+using namespace pv::view;
+
+namespace {
+	QString format(const pv::util::Timestamp& t)
+	{
+		return pv::util::format_time_si(t, pv::util::SIPrefix::none, 6);
+	}
+
+	const double e = 0.0001;
+};
+
+BOOST_AUTO_TEST_SUITE(RulerTest)
+
+BOOST_AUTO_TEST_CASE(tick_position_test_0)
+{
+	const pv::util::Timestamp major_period("0.1");
+	const pv::util::Timestamp offset("0");
+	const double scale(0.001);
+	const int width(500);
+
+	const Ruler::TickPositions ts = Ruler::calculate_tick_positions(
+		major_period, offset, scale, width, format);
+
+	BOOST_REQUIRE_EQUAL(ts.major.size(), 6);
+
+	BOOST_CHECK_CLOSE(ts.major[0].first,   0, e);
+	BOOST_CHECK_CLOSE(ts.major[1].first, 100, e);
+	BOOST_CHECK_CLOSE(ts.major[2].first, 200, e);
+	BOOST_CHECK_CLOSE(ts.major[3].first, 300, e);
+	BOOST_CHECK_CLOSE(ts.major[4].first, 400, e);
+	BOOST_CHECK_CLOSE(ts.major[5].first, 500, e);
+
+	BOOST_CHECK_EQUAL(ts.major[0].second,  "0.000000 s");
+	BOOST_CHECK_EQUAL(ts.major[1].second, "+0.100000 s");
+	BOOST_CHECK_EQUAL(ts.major[2].second, "+0.200000 s");
+	BOOST_CHECK_EQUAL(ts.major[3].second, "+0.300000 s");
+	BOOST_CHECK_EQUAL(ts.major[4].second, "+0.400000 s");
+	BOOST_CHECK_EQUAL(ts.major[5].second, "+0.500000 s");
+
+	BOOST_REQUIRE_EQUAL(ts.minor.size(), 16);
+
+	BOOST_CHECK_CLOSE(ts.minor[ 0], -25, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 1],  25, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 2],  50, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 3],  75, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 4], 125, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 5], 150, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 6], 175, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 7], 225, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 8], 250, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 9], 275, e);
+	BOOST_CHECK_CLOSE(ts.minor[10], 325, e);
+	BOOST_CHECK_CLOSE(ts.minor[11], 350, e);
+	BOOST_CHECK_CLOSE(ts.minor[12], 375, e);
+	BOOST_CHECK_CLOSE(ts.minor[13], 425, e);
+	BOOST_CHECK_CLOSE(ts.minor[14], 450, e);
+	BOOST_CHECK_CLOSE(ts.minor[15], 475, e);
+}
+
+BOOST_AUTO_TEST_CASE(tick_position_test_1)
+{
+	const pv::util::Timestamp major_period("0.1");
+	const pv::util::Timestamp offset("-0.463");
+	const double scale(0.001);
+	const int width(500);
+
+	const Ruler::TickPositions ts = Ruler::calculate_tick_positions(
+		major_period, offset, scale, width, format);
+
+	BOOST_REQUIRE_EQUAL(ts.major.size(), 5);
+
+	BOOST_CHECK_CLOSE(ts.major[0].first,   63, e);
+	BOOST_CHECK_CLOSE(ts.major[1].first,  163, e);
+	BOOST_CHECK_CLOSE(ts.major[2].first,  263, e);
+	BOOST_CHECK_CLOSE(ts.major[3].first,  363, e);
+	BOOST_CHECK_CLOSE(ts.major[4].first,  463, e);
+
+	BOOST_CHECK_EQUAL(ts.major[0].second, "-0.400000 s");
+	BOOST_CHECK_EQUAL(ts.major[1].second, "-0.300000 s");
+	BOOST_CHECK_EQUAL(ts.major[2].second, "-0.200000 s");
+	BOOST_CHECK_EQUAL(ts.major[3].second, "-0.100000 s");
+	BOOST_CHECK_EQUAL(ts.major[4].second,  "0.000000 s");
+
+	BOOST_REQUIRE_EQUAL(ts.minor.size(), 17);
+	BOOST_CHECK_CLOSE(ts.minor[ 0], -12, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 1],  13, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 2],  38, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 3],  88, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 4], 113, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 5], 138, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 6], 188, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 7], 213, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 8], 238, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 9], 288, e);
+	BOOST_CHECK_CLOSE(ts.minor[10], 313, e);
+	BOOST_CHECK_CLOSE(ts.minor[11], 338, e);
+	BOOST_CHECK_CLOSE(ts.minor[12], 388, e);
+	BOOST_CHECK_CLOSE(ts.minor[13], 413, e);
+	BOOST_CHECK_CLOSE(ts.minor[14], 438, e);
+	BOOST_CHECK_CLOSE(ts.minor[15], 488, e);
+	BOOST_CHECK_CLOSE(ts.minor[16], 513, e);
+}
+
+BOOST_AUTO_TEST_CASE(tick_position_test_2)
+{
+	const pv::util::Timestamp major_period("20");
+	const pv::util::Timestamp offset("8");
+	const double scale(0.129746);
+	const int width(580);
+
+	const Ruler::TickPositions ts = Ruler::calculate_tick_positions(
+		major_period, offset, scale, width, format);
+
+	const double mp = 5;
+	const int off = 8;
+
+	BOOST_REQUIRE_EQUAL(ts.major.size(), 4);
+
+	BOOST_CHECK_CLOSE(ts.major[0].first, ( 4 * mp - off) / scale, e);
+	BOOST_CHECK_CLOSE(ts.major[1].first, ( 8 * mp - off) / scale, e);
+	BOOST_CHECK_CLOSE(ts.major[2].first, (12 * mp - off) / scale, e);
+	BOOST_CHECK_CLOSE(ts.major[3].first, (16 * mp - off) / scale, e);
+
+	BOOST_CHECK_EQUAL(ts.major[0].second, "+20.000000 s");
+	BOOST_CHECK_EQUAL(ts.major[1].second, "+40.000000 s");
+	BOOST_CHECK_EQUAL(ts.major[2].second, "+60.000000 s");
+	BOOST_CHECK_EQUAL(ts.major[3].second, "+80.000000 s");
+
+	BOOST_REQUIRE_EQUAL(ts.minor.size(), 13);
+
+	BOOST_CHECK_CLOSE(ts.minor[ 0], ( 1 * mp - off) / scale, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 1], ( 2 * mp - off) / scale, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 2], ( 3 * mp - off) / scale, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 3], ( 5 * mp - off) / scale, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 4], ( 6 * mp - off) / scale, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 5], ( 7 * mp - off) / scale, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 6], ( 9 * mp - off) / scale, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 7], (10 * mp - off) / scale, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 8], (11 * mp - off) / scale, e);
+	BOOST_CHECK_CLOSE(ts.minor[ 9], (13 * mp - off) / scale, e);
+	BOOST_CHECK_CLOSE(ts.minor[10], (14 * mp - off) / scale, e);
+	BOOST_CHECK_CLOSE(ts.minor[11], (15 * mp - off) / scale, e);
+	BOOST_CHECK_CLOSE(ts.minor[12], (17 * mp - off) / scale, e);
+}
+
+BOOST_AUTO_TEST_SUITE_END()

-- 
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