changeset 1216:dc2af6616c83

Merge from branch 3.0-integration
author Chris Cannam
date Fri, 13 Jan 2017 10:29:50 +0000
parents e8102ff5573b (current diff) c603aba61702 (diff)
children 51b6381fc413
files
diffstat 100 files changed, 7390 insertions(+), 5217 deletions(-) [+]
line wrap: on
line diff
--- a/configure	Fri Mar 04 12:23:31 2016 +0000
+++ b/configure	Fri Jan 13 10:29:50 2017 +0000
@@ -646,16 +646,12 @@
 libpulse_CFLAGS
 JACK_LIBS
 JACK_CFLAGS
-portaudio_2_0_LIBS
-portaudio_2_0_CFLAGS
+portaudio_LIBS
+portaudio_CFLAGS
 liblo_LIBS
 liblo_CFLAGS
 rubberband_LIBS
 rubberband_CFLAGS
-vamphostsdk_LIBS
-vamphostsdk_CFLAGS
-vamp_LIBS
-vamp_CFLAGS
 samplerate_LIBS
 samplerate_CFLAGS
 sndfile_LIBS
@@ -756,16 +752,12 @@
 sndfile_LIBS
 samplerate_CFLAGS
 samplerate_LIBS
-vamp_CFLAGS
-vamp_LIBS
-vamphostsdk_CFLAGS
-vamphostsdk_LIBS
 rubberband_CFLAGS
 rubberband_LIBS
 liblo_CFLAGS
 liblo_LIBS
-portaudio_2_0_CFLAGS
-portaudio_2_0_LIBS
+portaudio_CFLAGS
+portaudio_LIBS
 JACK_CFLAGS
 JACK_LIBS
 libpulse_CFLAGS
@@ -1423,12 +1415,6 @@
               C compiler flags for samplerate, overriding pkg-config
   samplerate_LIBS
               linker flags for samplerate, overriding pkg-config
-  vamp_CFLAGS C compiler flags for vamp, overriding pkg-config
-  vamp_LIBS   linker flags for vamp, overriding pkg-config
-  vamphostsdk_CFLAGS
-              C compiler flags for vamphostsdk, overriding pkg-config
-  vamphostsdk_LIBS
-              linker flags for vamphostsdk, overriding pkg-config
   rubberband_CFLAGS
               C compiler flags for rubberband, overriding pkg-config
   rubberband_LIBS
@@ -1436,10 +1422,10 @@
   liblo_CFLAGS
               C compiler flags for liblo, overriding pkg-config
   liblo_LIBS  linker flags for liblo, overriding pkg-config
-  portaudio_2_0_CFLAGS
-              C compiler flags for portaudio_2_0, overriding pkg-config
-  portaudio_2_0_LIBS
-              linker flags for portaudio_2_0, overriding pkg-config
+  portaudio_CFLAGS
+              C compiler flags for portaudio, overriding pkg-config
+  portaudio_LIBS
+              linker flags for portaudio, overriding pkg-config
   JACK_CFLAGS C compiler flags for JACK, overriding pkg-config
   JACK_LIBS   linker flags for JACK, overriding pkg-config
   libpulse_CFLAGS
@@ -5226,18 +5212,18 @@
 fi
 
 
-SV_MODULE_MODULE=vamp
-SV_MODULE_VERSION_TEST="vamp >= 2.1"
-SV_MODULE_HEADER=vamp/vamp.h
-SV_MODULE_LIB=
-SV_MODULE_FUNC=
-SV_MODULE_HAVE=HAVE_$(echo vamp | tr 'a-z' 'A-Z')
+SV_MODULE_MODULE=rubberband
+SV_MODULE_VERSION_TEST="rubberband"
+SV_MODULE_HEADER=rubberband/RubberBandStretcher.h
+SV_MODULE_LIB=rubberband
+SV_MODULE_FUNC=rubberband_new
+SV_MODULE_HAVE=HAVE_$(echo rubberband | tr 'a-z' 'A-Z')
 SV_MODULE_FAILED=1
-if test -n "$vamp_LIBS" ; then
+if test -n "$rubberband_LIBS" ; then
    { $as_echo "$as_me:${as_lineno-$LINENO}: User set ${SV_MODULE_MODULE}_LIBS explicitly, skipping test for $SV_MODULE_MODULE" >&5
 $as_echo "$as_me: User set ${SV_MODULE_MODULE}_LIBS explicitly, skipping test for $SV_MODULE_MODULE" >&6;}
-   CXXFLAGS="$CXXFLAGS $vamp_CFLAGS"
-   LIBS="$LIBS $vamp_LIBS"
+   CXXFLAGS="$CXXFLAGS $rubberband_CFLAGS"
+   LIBS="$LIBS $rubberband_LIBS"
    SV_MODULE_FAILED=""
 fi
 if test -z "$SV_MODULE_VERSION_TEST" ; then
@@ -5246,11 +5232,11 @@
 if test -n "$SV_MODULE_FAILED" && test -n "$PKG_CONFIG"; then
 
 pkg_failed=no
-{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for vamp" >&5
-$as_echo_n "checking for vamp... " >&6; }
-
-if test -n "$vamp_CFLAGS"; then
-    pkg_cv_vamp_CFLAGS="$vamp_CFLAGS"
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for rubberband" >&5
+$as_echo_n "checking for rubberband... " >&6; }
+
+if test -n "$rubberband_CFLAGS"; then
+    pkg_cv_rubberband_CFLAGS="$rubberband_CFLAGS"
  elif test -n "$PKG_CONFIG"; then
     if test -n "$PKG_CONFIG" && \
     { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"\$SV_MODULE_VERSION_TEST\""; } >&5
@@ -5258,7 +5244,7 @@
   ac_status=$?
   $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
   test $ac_status = 0; }; then
-  pkg_cv_vamp_CFLAGS=`$PKG_CONFIG --cflags "$SV_MODULE_VERSION_TEST" 2>/dev/null`
+  pkg_cv_rubberband_CFLAGS=`$PKG_CONFIG --cflags "$SV_MODULE_VERSION_TEST" 2>/dev/null`
 		      test "x$?" != "x0" && pkg_failed=yes
 else
   pkg_failed=yes
@@ -5266,8 +5252,8 @@
  else
     pkg_failed=untried
 fi
-if test -n "$vamp_LIBS"; then
-    pkg_cv_vamp_LIBS="$vamp_LIBS"
+if test -n "$rubberband_LIBS"; then
+    pkg_cv_rubberband_LIBS="$rubberband_LIBS"
  elif test -n "$PKG_CONFIG"; then
     if test -n "$PKG_CONFIG" && \
     { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"\$SV_MODULE_VERSION_TEST\""; } >&5
@@ -5275,7 +5261,7 @@
   ac_status=$?
   $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
   test $ac_status = 0; }; then
-  pkg_cv_vamp_LIBS=`$PKG_CONFIG --libs "$SV_MODULE_VERSION_TEST" 2>/dev/null`
+  pkg_cv_rubberband_LIBS=`$PKG_CONFIG --libs "$SV_MODULE_VERSION_TEST" 2>/dev/null`
 		      test "x$?" != "x0" && pkg_failed=yes
 else
   pkg_failed=yes
@@ -5296,12 +5282,12 @@
         _pkg_short_errors_supported=no
 fi
         if test $_pkg_short_errors_supported = yes; then
-	        vamp_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
+	        rubberband_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
         else
-	        vamp_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
+	        rubberband_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
         fi
 	# Put the nasty error message in config.log where it belongs
-	echo "$vamp_PKG_ERRORS" >&5
+	echo "$rubberband_PKG_ERRORS" >&5
 
 	{ $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find required module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&5
 $as_echo "$as_me: Failed to find required module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&6;}
@@ -5311,11 +5297,11 @@
 	{ $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find required module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&5
 $as_echo "$as_me: Failed to find required module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&6;}
 else
-	vamp_CFLAGS=$pkg_cv_vamp_CFLAGS
-	vamp_LIBS=$pkg_cv_vamp_LIBS
+	rubberband_CFLAGS=$pkg_cv_rubberband_CFLAGS
+	rubberband_LIBS=$pkg_cv_rubberband_LIBS
         { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5
 $as_echo "yes" >&6; }
-	HAVES="$HAVES $SV_MODULE_HAVE";CXXFLAGS="$CXXFLAGS $vamp_CFLAGS";LIBS="$LIBS $vamp_LIBS";SV_MODULE_FAILED=""
+	HAVES="$HAVES $SV_MODULE_HAVE";CXXFLAGS="$CXXFLAGS $rubberband_CFLAGS";LIBS="$LIBS $rubberband_LIBS";SV_MODULE_FAILED=""
 fi
 fi
 if test -n "$SV_MODULE_FAILED"; then
@@ -5377,18 +5363,19 @@
 fi
 
 
-SV_MODULE_MODULE=vamphostsdk
-SV_MODULE_VERSION_TEST="vamp-hostsdk >= 2.5"
-SV_MODULE_HEADER=vamp-hostsdk/PluginLoader.h
-SV_MODULE_LIB=vamp-hostsdk
-SV_MODULE_FUNC=libvamphostsdk_v_2_5_present
-SV_MODULE_HAVE=HAVE_$(echo vamphostsdk | tr 'a-z' 'A-Z')
+
+SV_MODULE_MODULE=liblo
+SV_MODULE_VERSION_TEST=""
+SV_MODULE_HEADER=lo/lo.h
+SV_MODULE_LIB=lo
+SV_MODULE_FUNC=lo_address_new
+SV_MODULE_HAVE=HAVE_$(echo liblo | tr 'a-z' 'A-Z')
 SV_MODULE_FAILED=1
-if test -n "$vamphostsdk_LIBS" ; then
+if test -n "$liblo_LIBS" ; then
    { $as_echo "$as_me:${as_lineno-$LINENO}: User set ${SV_MODULE_MODULE}_LIBS explicitly, skipping test for $SV_MODULE_MODULE" >&5
 $as_echo "$as_me: User set ${SV_MODULE_MODULE}_LIBS explicitly, skipping test for $SV_MODULE_MODULE" >&6;}
-   CXXFLAGS="$CXXFLAGS $vamphostsdk_CFLAGS"
-   LIBS="$LIBS $vamphostsdk_LIBS"
+   CXXFLAGS="$CXXFLAGS $liblo_CFLAGS"
+   LIBS="$LIBS $liblo_LIBS"
    SV_MODULE_FAILED=""
 fi
 if test -z "$SV_MODULE_VERSION_TEST" ; then
@@ -5397,11 +5384,11 @@
 if test -n "$SV_MODULE_FAILED" && test -n "$PKG_CONFIG"; then
 
 pkg_failed=no
-{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for vamphostsdk" >&5
-$as_echo_n "checking for vamphostsdk... " >&6; }
-
-if test -n "$vamphostsdk_CFLAGS"; then
-    pkg_cv_vamphostsdk_CFLAGS="$vamphostsdk_CFLAGS"
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for liblo" >&5
+$as_echo_n "checking for liblo... " >&6; }
+
+if test -n "$liblo_CFLAGS"; then
+    pkg_cv_liblo_CFLAGS="$liblo_CFLAGS"
  elif test -n "$PKG_CONFIG"; then
     if test -n "$PKG_CONFIG" && \
     { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"\$SV_MODULE_VERSION_TEST\""; } >&5
@@ -5409,7 +5396,7 @@
   ac_status=$?
   $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
   test $ac_status = 0; }; then
-  pkg_cv_vamphostsdk_CFLAGS=`$PKG_CONFIG --cflags "$SV_MODULE_VERSION_TEST" 2>/dev/null`
+  pkg_cv_liblo_CFLAGS=`$PKG_CONFIG --cflags "$SV_MODULE_VERSION_TEST" 2>/dev/null`
 		      test "x$?" != "x0" && pkg_failed=yes
 else
   pkg_failed=yes
@@ -5417,8 +5404,8 @@
  else
     pkg_failed=untried
 fi
-if test -n "$vamphostsdk_LIBS"; then
-    pkg_cv_vamphostsdk_LIBS="$vamphostsdk_LIBS"
+if test -n "$liblo_LIBS"; then
+    pkg_cv_liblo_LIBS="$liblo_LIBS"
  elif test -n "$PKG_CONFIG"; then
     if test -n "$PKG_CONFIG" && \
     { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"\$SV_MODULE_VERSION_TEST\""; } >&5
@@ -5426,7 +5413,7 @@
   ac_status=$?
   $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
   test $ac_status = 0; }; then
-  pkg_cv_vamphostsdk_LIBS=`$PKG_CONFIG --libs "$SV_MODULE_VERSION_TEST" 2>/dev/null`
+  pkg_cv_liblo_LIBS=`$PKG_CONFIG --libs "$SV_MODULE_VERSION_TEST" 2>/dev/null`
 		      test "x$?" != "x0" && pkg_failed=yes
 else
   pkg_failed=yes
@@ -5447,40 +5434,42 @@
         _pkg_short_errors_supported=no
 fi
         if test $_pkg_short_errors_supported = yes; then
-	        vamphostsdk_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
+	        liblo_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
         else
-	        vamphostsdk_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
+	        liblo_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
         fi
 	# Put the nasty error message in config.log where it belongs
-	echo "$vamphostsdk_PKG_ERRORS" >&5
-
-	{ $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find required module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&5
-$as_echo "$as_me: Failed to find required module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&6;}
+	echo "$liblo_PKG_ERRORS" >&5
+
+	{ $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find optional module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&5
+$as_echo "$as_me: Failed to find optional module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&6;}
 elif test $pkg_failed = untried; then
      	{ $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
 $as_echo "no" >&6; }
-	{ $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find required module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&5
-$as_echo "$as_me: Failed to find required module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&6;}
-else
-	vamphostsdk_CFLAGS=$pkg_cv_vamphostsdk_CFLAGS
-	vamphostsdk_LIBS=$pkg_cv_vamphostsdk_LIBS
+	{ $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find optional module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&5
+$as_echo "$as_me: Failed to find optional module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&6;}
+else
+	liblo_CFLAGS=$pkg_cv_liblo_CFLAGS
+	liblo_LIBS=$pkg_cv_liblo_LIBS
         { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5
 $as_echo "yes" >&6; }
-	HAVES="$HAVES $SV_MODULE_HAVE";CXXFLAGS="$CXXFLAGS $vamphostsdk_CFLAGS";LIBS="$LIBS $vamphostsdk_LIBS";SV_MODULE_FAILED=""
+	HAVES="$HAVES $SV_MODULE_HAVE";CXXFLAGS="$CXXFLAGS $liblo_CFLAGS";LIBS="$LIBS $liblo_LIBS";SV_MODULE_FAILED=""
 fi
 fi
 if test -n "$SV_MODULE_FAILED"; then
    as_ac_Header=`$as_echo "ac_cv_header_$SV_MODULE_HEADER" | $as_tr_sh`
 ac_fn_cxx_check_header_mongrel "$LINENO" "$SV_MODULE_HEADER" "$as_ac_Header" "$ac_includes_default"
 if eval test \"x\$"$as_ac_Header"\" = x"yes"; then :
-  HAVES="$HAVES $SV_MODULE_HAVE"
-else
-  as_fn_error $? "Failed to find header $SV_MODULE_HEADER for required module $SV_MODULE_MODULE" "$LINENO" 5
-fi
-
-
-   if test -n "$SV_MODULE_LIB"; then
-     as_ac_Lib=`$as_echo "ac_cv_lib_$SV_MODULE_LIB''_$SV_MODULE_FUNC" | $as_tr_sh`
+  HAVES="$HAVES $SV_MODULE_HAVE";SV_MODULE_FAILED=""
+else
+  { $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find header $SV_MODULE_HEADER for optional module $SV_MODULE_MODULE" >&5
+$as_echo "$as_me: Failed to find header $SV_MODULE_HEADER for optional module $SV_MODULE_MODULE" >&6;}
+fi
+
+
+   if test -z "$SV_MODULE_FAILED"; then
+      if test -n "$SV_MODULE_LIB"; then
+           as_ac_Lib=`$as_echo "ac_cv_lib_$SV_MODULE_LIB''_$SV_MODULE_FUNC" | $as_tr_sh`
 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $SV_MODULE_FUNC in -l$SV_MODULE_LIB" >&5
 $as_echo_n "checking for $SV_MODULE_FUNC in -l$SV_MODULE_LIB... " >&6; }
 if eval \${$as_ac_Lib+:} false; then :
@@ -5521,25 +5510,27 @@
 if eval test \"x\$"$as_ac_Lib"\" = x"yes"; then :
   LIBS="$LIBS -l$SV_MODULE_LIB"
 else
-  as_fn_error $? "Failed to find library $SV_MODULE_LIB for required module $SV_MODULE_MODULE" "$LINENO" 5
-fi
-
+  { $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find library $SV_MODULE_LIB for optional module $SV_MODULE_MODULE" >&5
+$as_echo "$as_me: Failed to find library $SV_MODULE_LIB for optional module $SV_MODULE_MODULE" >&6;}
+fi
+
+      fi
    fi
 fi
 
 
-SV_MODULE_MODULE=rubberband
-SV_MODULE_VERSION_TEST="rubberband"
-SV_MODULE_HEADER=rubberband/RubberBandStretcher.h
-SV_MODULE_LIB=rubberband
-SV_MODULE_FUNC=rubberband_new
-SV_MODULE_HAVE=HAVE_$(echo rubberband | tr 'a-z' 'A-Z')
+SV_MODULE_MODULE=portaudio
+SV_MODULE_VERSION_TEST="portaudio-2.0 >= 19"
+SV_MODULE_HEADER=portaudio.h
+SV_MODULE_LIB=portaudio
+SV_MODULE_FUNC=Pa_IsFormatSupported
+SV_MODULE_HAVE=HAVE_$(echo portaudio | tr 'a-z' 'A-Z')
 SV_MODULE_FAILED=1
-if test -n "$rubberband_LIBS" ; then
+if test -n "$portaudio_LIBS" ; then
    { $as_echo "$as_me:${as_lineno-$LINENO}: User set ${SV_MODULE_MODULE}_LIBS explicitly, skipping test for $SV_MODULE_MODULE" >&5
 $as_echo "$as_me: User set ${SV_MODULE_MODULE}_LIBS explicitly, skipping test for $SV_MODULE_MODULE" >&6;}
-   CXXFLAGS="$CXXFLAGS $rubberband_CFLAGS"
-   LIBS="$LIBS $rubberband_LIBS"
+   CXXFLAGS="$CXXFLAGS $portaudio_CFLAGS"
+   LIBS="$LIBS $portaudio_LIBS"
    SV_MODULE_FAILED=""
 fi
 if test -z "$SV_MODULE_VERSION_TEST" ; then
@@ -5548,11 +5539,11 @@
 if test -n "$SV_MODULE_FAILED" && test -n "$PKG_CONFIG"; then
 
 pkg_failed=no
-{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for rubberband" >&5
-$as_echo_n "checking for rubberband... " >&6; }
-
-if test -n "$rubberband_CFLAGS"; then
-    pkg_cv_rubberband_CFLAGS="$rubberband_CFLAGS"
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for portaudio" >&5
+$as_echo_n "checking for portaudio... " >&6; }
+
+if test -n "$portaudio_CFLAGS"; then
+    pkg_cv_portaudio_CFLAGS="$portaudio_CFLAGS"
  elif test -n "$PKG_CONFIG"; then
     if test -n "$PKG_CONFIG" && \
     { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"\$SV_MODULE_VERSION_TEST\""; } >&5
@@ -5560,7 +5551,7 @@
   ac_status=$?
   $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
   test $ac_status = 0; }; then
-  pkg_cv_rubberband_CFLAGS=`$PKG_CONFIG --cflags "$SV_MODULE_VERSION_TEST" 2>/dev/null`
+  pkg_cv_portaudio_CFLAGS=`$PKG_CONFIG --cflags "$SV_MODULE_VERSION_TEST" 2>/dev/null`
 		      test "x$?" != "x0" && pkg_failed=yes
 else
   pkg_failed=yes
@@ -5568,8 +5559,8 @@
  else
     pkg_failed=untried
 fi
-if test -n "$rubberband_LIBS"; then
-    pkg_cv_rubberband_LIBS="$rubberband_LIBS"
+if test -n "$portaudio_LIBS"; then
+    pkg_cv_portaudio_LIBS="$portaudio_LIBS"
  elif test -n "$PKG_CONFIG"; then
     if test -n "$PKG_CONFIG" && \
     { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"\$SV_MODULE_VERSION_TEST\""; } >&5
@@ -5577,7 +5568,7 @@
   ac_status=$?
   $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
   test $ac_status = 0; }; then
-  pkg_cv_rubberband_LIBS=`$PKG_CONFIG --libs "$SV_MODULE_VERSION_TEST" 2>/dev/null`
+  pkg_cv_portaudio_LIBS=`$PKG_CONFIG --libs "$SV_MODULE_VERSION_TEST" 2>/dev/null`
 		      test "x$?" != "x0" && pkg_failed=yes
 else
   pkg_failed=yes
@@ -5598,164 +5589,12 @@
         _pkg_short_errors_supported=no
 fi
         if test $_pkg_short_errors_supported = yes; then
-	        rubberband_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
+	        portaudio_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
         else
-	        rubberband_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
+	        portaudio_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
         fi
 	# Put the nasty error message in config.log where it belongs
-	echo "$rubberband_PKG_ERRORS" >&5
-
-	{ $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find required module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&5
-$as_echo "$as_me: Failed to find required module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&6;}
-elif test $pkg_failed = untried; then
-     	{ $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
-$as_echo "no" >&6; }
-	{ $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find required module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&5
-$as_echo "$as_me: Failed to find required module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&6;}
-else
-	rubberband_CFLAGS=$pkg_cv_rubberband_CFLAGS
-	rubberband_LIBS=$pkg_cv_rubberband_LIBS
-        { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5
-$as_echo "yes" >&6; }
-	HAVES="$HAVES $SV_MODULE_HAVE";CXXFLAGS="$CXXFLAGS $rubberband_CFLAGS";LIBS="$LIBS $rubberband_LIBS";SV_MODULE_FAILED=""
-fi
-fi
-if test -n "$SV_MODULE_FAILED"; then
-   as_ac_Header=`$as_echo "ac_cv_header_$SV_MODULE_HEADER" | $as_tr_sh`
-ac_fn_cxx_check_header_mongrel "$LINENO" "$SV_MODULE_HEADER" "$as_ac_Header" "$ac_includes_default"
-if eval test \"x\$"$as_ac_Header"\" = x"yes"; then :
-  HAVES="$HAVES $SV_MODULE_HAVE"
-else
-  as_fn_error $? "Failed to find header $SV_MODULE_HEADER for required module $SV_MODULE_MODULE" "$LINENO" 5
-fi
-
-
-   if test -n "$SV_MODULE_LIB"; then
-     as_ac_Lib=`$as_echo "ac_cv_lib_$SV_MODULE_LIB''_$SV_MODULE_FUNC" | $as_tr_sh`
-{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $SV_MODULE_FUNC in -l$SV_MODULE_LIB" >&5
-$as_echo_n "checking for $SV_MODULE_FUNC in -l$SV_MODULE_LIB... " >&6; }
-if eval \${$as_ac_Lib+:} false; then :
-  $as_echo_n "(cached) " >&6
-else
-  ac_check_lib_save_LIBS=$LIBS
-LIBS="-l$SV_MODULE_LIB  $LIBS"
-cat confdefs.h - <<_ACEOF >conftest.$ac_ext
-/* end confdefs.h.  */
-
-/* Override any GCC internal prototype to avoid an error.
-   Use char because int might match the return type of a GCC
-   builtin and then its argument prototype would still apply.  */
-#ifdef __cplusplus
-extern "C"
-#endif
-char $SV_MODULE_FUNC ();
-int
-main ()
-{
-return $SV_MODULE_FUNC ();
-  ;
-  return 0;
-}
-_ACEOF
-if ac_fn_cxx_try_link "$LINENO"; then :
-  eval "$as_ac_Lib=yes"
-else
-  eval "$as_ac_Lib=no"
-fi
-rm -f core conftest.err conftest.$ac_objext \
-    conftest$ac_exeext conftest.$ac_ext
-LIBS=$ac_check_lib_save_LIBS
-fi
-eval ac_res=\$$as_ac_Lib
-	       { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5
-$as_echo "$ac_res" >&6; }
-if eval test \"x\$"$as_ac_Lib"\" = x"yes"; then :
-  LIBS="$LIBS -l$SV_MODULE_LIB"
-else
-  as_fn_error $? "Failed to find library $SV_MODULE_LIB for required module $SV_MODULE_MODULE" "$LINENO" 5
-fi
-
-   fi
-fi
-
-
-
-SV_MODULE_MODULE=liblo
-SV_MODULE_VERSION_TEST=""
-SV_MODULE_HEADER=lo/lo.h
-SV_MODULE_LIB=lo
-SV_MODULE_FUNC=lo_address_new
-SV_MODULE_HAVE=HAVE_$(echo liblo | tr 'a-z' 'A-Z')
-SV_MODULE_FAILED=1
-if test -n "$liblo_LIBS" ; then
-   { $as_echo "$as_me:${as_lineno-$LINENO}: User set ${SV_MODULE_MODULE}_LIBS explicitly, skipping test for $SV_MODULE_MODULE" >&5
-$as_echo "$as_me: User set ${SV_MODULE_MODULE}_LIBS explicitly, skipping test for $SV_MODULE_MODULE" >&6;}
-   CXXFLAGS="$CXXFLAGS $liblo_CFLAGS"
-   LIBS="$LIBS $liblo_LIBS"
-   SV_MODULE_FAILED=""
-fi
-if test -z "$SV_MODULE_VERSION_TEST" ; then
-   SV_MODULE_VERSION_TEST=$SV_MODULE_MODULE
-fi
-if test -n "$SV_MODULE_FAILED" && test -n "$PKG_CONFIG"; then
-
-pkg_failed=no
-{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for liblo" >&5
-$as_echo_n "checking for liblo... " >&6; }
-
-if test -n "$liblo_CFLAGS"; then
-    pkg_cv_liblo_CFLAGS="$liblo_CFLAGS"
- elif test -n "$PKG_CONFIG"; then
-    if test -n "$PKG_CONFIG" && \
-    { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"\$SV_MODULE_VERSION_TEST\""; } >&5
-  ($PKG_CONFIG --exists --print-errors "$SV_MODULE_VERSION_TEST") 2>&5
-  ac_status=$?
-  $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
-  test $ac_status = 0; }; then
-  pkg_cv_liblo_CFLAGS=`$PKG_CONFIG --cflags "$SV_MODULE_VERSION_TEST" 2>/dev/null`
-		      test "x$?" != "x0" && pkg_failed=yes
-else
-  pkg_failed=yes
-fi
- else
-    pkg_failed=untried
-fi
-if test -n "$liblo_LIBS"; then
-    pkg_cv_liblo_LIBS="$liblo_LIBS"
- elif test -n "$PKG_CONFIG"; then
-    if test -n "$PKG_CONFIG" && \
-    { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"\$SV_MODULE_VERSION_TEST\""; } >&5
-  ($PKG_CONFIG --exists --print-errors "$SV_MODULE_VERSION_TEST") 2>&5
-  ac_status=$?
-  $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
-  test $ac_status = 0; }; then
-  pkg_cv_liblo_LIBS=`$PKG_CONFIG --libs "$SV_MODULE_VERSION_TEST" 2>/dev/null`
-		      test "x$?" != "x0" && pkg_failed=yes
-else
-  pkg_failed=yes
-fi
- else
-    pkg_failed=untried
-fi
-
-
-
-if test $pkg_failed = yes; then
-   	{ $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
-$as_echo "no" >&6; }
-
-if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then
-        _pkg_short_errors_supported=yes
-else
-        _pkg_short_errors_supported=no
-fi
-        if test $_pkg_short_errors_supported = yes; then
-	        liblo_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
-        else
-	        liblo_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
-        fi
-	# Put the nasty error message in config.log where it belongs
-	echo "$liblo_PKG_ERRORS" >&5
+	echo "$portaudio_PKG_ERRORS" >&5
 
 	{ $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find optional module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&5
 $as_echo "$as_me: Failed to find optional module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&6;}
@@ -5765,166 +5604,11 @@
 	{ $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find optional module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&5
 $as_echo "$as_me: Failed to find optional module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&6;}
 else
-	liblo_CFLAGS=$pkg_cv_liblo_CFLAGS
-	liblo_LIBS=$pkg_cv_liblo_LIBS
+	portaudio_CFLAGS=$pkg_cv_portaudio_CFLAGS
+	portaudio_LIBS=$pkg_cv_portaudio_LIBS
         { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5
 $as_echo "yes" >&6; }
-	HAVES="$HAVES $SV_MODULE_HAVE";CXXFLAGS="$CXXFLAGS $liblo_CFLAGS";LIBS="$LIBS $liblo_LIBS";SV_MODULE_FAILED=""
-fi
-fi
-if test -n "$SV_MODULE_FAILED"; then
-   as_ac_Header=`$as_echo "ac_cv_header_$SV_MODULE_HEADER" | $as_tr_sh`
-ac_fn_cxx_check_header_mongrel "$LINENO" "$SV_MODULE_HEADER" "$as_ac_Header" "$ac_includes_default"
-if eval test \"x\$"$as_ac_Header"\" = x"yes"; then :
-  HAVES="$HAVES $SV_MODULE_HAVE";SV_MODULE_FAILED=""
-else
-  { $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find header $SV_MODULE_HEADER for optional module $SV_MODULE_MODULE" >&5
-$as_echo "$as_me: Failed to find header $SV_MODULE_HEADER for optional module $SV_MODULE_MODULE" >&6;}
-fi
-
-
-   if test -z "$SV_MODULE_FAILED"; then
-      if test -n "$SV_MODULE_LIB"; then
-           as_ac_Lib=`$as_echo "ac_cv_lib_$SV_MODULE_LIB''_$SV_MODULE_FUNC" | $as_tr_sh`
-{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $SV_MODULE_FUNC in -l$SV_MODULE_LIB" >&5
-$as_echo_n "checking for $SV_MODULE_FUNC in -l$SV_MODULE_LIB... " >&6; }
-if eval \${$as_ac_Lib+:} false; then :
-  $as_echo_n "(cached) " >&6
-else
-  ac_check_lib_save_LIBS=$LIBS
-LIBS="-l$SV_MODULE_LIB  $LIBS"
-cat confdefs.h - <<_ACEOF >conftest.$ac_ext
-/* end confdefs.h.  */
-
-/* Override any GCC internal prototype to avoid an error.
-   Use char because int might match the return type of a GCC
-   builtin and then its argument prototype would still apply.  */
-#ifdef __cplusplus
-extern "C"
-#endif
-char $SV_MODULE_FUNC ();
-int
-main ()
-{
-return $SV_MODULE_FUNC ();
-  ;
-  return 0;
-}
-_ACEOF
-if ac_fn_cxx_try_link "$LINENO"; then :
-  eval "$as_ac_Lib=yes"
-else
-  eval "$as_ac_Lib=no"
-fi
-rm -f core conftest.err conftest.$ac_objext \
-    conftest$ac_exeext conftest.$ac_ext
-LIBS=$ac_check_lib_save_LIBS
-fi
-eval ac_res=\$$as_ac_Lib
-	       { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5
-$as_echo "$ac_res" >&6; }
-if eval test \"x\$"$as_ac_Lib"\" = x"yes"; then :
-  LIBS="$LIBS -l$SV_MODULE_LIB"
-else
-  { $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find library $SV_MODULE_LIB for optional module $SV_MODULE_MODULE" >&5
-$as_echo "$as_me: Failed to find library $SV_MODULE_LIB for optional module $SV_MODULE_MODULE" >&6;}
-fi
-
-      fi
-   fi
-fi
-
-
-SV_MODULE_MODULE=portaudio_2_0
-SV_MODULE_VERSION_TEST="portaudio-2.0 >= 19"
-SV_MODULE_HEADER=portaudio.h
-SV_MODULE_LIB=portaudio
-SV_MODULE_FUNC=Pa_IsFormatSupported
-SV_MODULE_HAVE=HAVE_$(echo portaudio_2_0 | tr 'a-z' 'A-Z')
-SV_MODULE_FAILED=1
-if test -n "$portaudio_2_0_LIBS" ; then
-   { $as_echo "$as_me:${as_lineno-$LINENO}: User set ${SV_MODULE_MODULE}_LIBS explicitly, skipping test for $SV_MODULE_MODULE" >&5
-$as_echo "$as_me: User set ${SV_MODULE_MODULE}_LIBS explicitly, skipping test for $SV_MODULE_MODULE" >&6;}
-   CXXFLAGS="$CXXFLAGS $portaudio_2_0_CFLAGS"
-   LIBS="$LIBS $portaudio_2_0_LIBS"
-   SV_MODULE_FAILED=""
-fi
-if test -z "$SV_MODULE_VERSION_TEST" ; then
-   SV_MODULE_VERSION_TEST=$SV_MODULE_MODULE
-fi
-if test -n "$SV_MODULE_FAILED" && test -n "$PKG_CONFIG"; then
-
-pkg_failed=no
-{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for portaudio_2_0" >&5
-$as_echo_n "checking for portaudio_2_0... " >&6; }
-
-if test -n "$portaudio_2_0_CFLAGS"; then
-    pkg_cv_portaudio_2_0_CFLAGS="$portaudio_2_0_CFLAGS"
- elif test -n "$PKG_CONFIG"; then
-    if test -n "$PKG_CONFIG" && \
-    { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"\$SV_MODULE_VERSION_TEST\""; } >&5
-  ($PKG_CONFIG --exists --print-errors "$SV_MODULE_VERSION_TEST") 2>&5
-  ac_status=$?
-  $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
-  test $ac_status = 0; }; then
-  pkg_cv_portaudio_2_0_CFLAGS=`$PKG_CONFIG --cflags "$SV_MODULE_VERSION_TEST" 2>/dev/null`
-		      test "x$?" != "x0" && pkg_failed=yes
-else
-  pkg_failed=yes
-fi
- else
-    pkg_failed=untried
-fi
-if test -n "$portaudio_2_0_LIBS"; then
-    pkg_cv_portaudio_2_0_LIBS="$portaudio_2_0_LIBS"
- elif test -n "$PKG_CONFIG"; then
-    if test -n "$PKG_CONFIG" && \
-    { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"\$SV_MODULE_VERSION_TEST\""; } >&5
-  ($PKG_CONFIG --exists --print-errors "$SV_MODULE_VERSION_TEST") 2>&5
-  ac_status=$?
-  $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
-  test $ac_status = 0; }; then
-  pkg_cv_portaudio_2_0_LIBS=`$PKG_CONFIG --libs "$SV_MODULE_VERSION_TEST" 2>/dev/null`
-		      test "x$?" != "x0" && pkg_failed=yes
-else
-  pkg_failed=yes
-fi
- else
-    pkg_failed=untried
-fi
-
-
-
-if test $pkg_failed = yes; then
-   	{ $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
-$as_echo "no" >&6; }
-
-if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then
-        _pkg_short_errors_supported=yes
-else
-        _pkg_short_errors_supported=no
-fi
-        if test $_pkg_short_errors_supported = yes; then
-	        portaudio_2_0_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
-        else
-	        portaudio_2_0_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
-        fi
-	# Put the nasty error message in config.log where it belongs
-	echo "$portaudio_2_0_PKG_ERRORS" >&5
-
-	{ $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find optional module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&5
-$as_echo "$as_me: Failed to find optional module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&6;}
-elif test $pkg_failed = untried; then
-     	{ $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
-$as_echo "no" >&6; }
-	{ $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find optional module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&5
-$as_echo "$as_me: Failed to find optional module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&6;}
-else
-	portaudio_2_0_CFLAGS=$pkg_cv_portaudio_2_0_CFLAGS
-	portaudio_2_0_LIBS=$pkg_cv_portaudio_2_0_LIBS
-        { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5
-$as_echo "yes" >&6; }
-	HAVES="$HAVES $SV_MODULE_HAVE";CXXFLAGS="$CXXFLAGS $portaudio_2_0_CFLAGS";LIBS="$LIBS $portaudio_2_0_LIBS";SV_MODULE_FAILED=""
+	HAVES="$HAVES $SV_MODULE_HAVE";CXXFLAGS="$CXXFLAGS $portaudio_CFLAGS";LIBS="$LIBS $portaudio_LIBS";SV_MODULE_FAILED=""
 fi
 fi
 if test -n "$SV_MODULE_FAILED"; then
--- a/configure.ac	Fri Mar 04 12:23:31 2016 +0000
+++ b/configure.ac	Fri Jan 13 10:29:50 2017 +0000
@@ -83,12 +83,10 @@
 SV_MODULE_REQUIRED([fftw3f],[fftw3f >= 3.0.0],[fftw3.h],[fftw3f],[fftwf_execute])
 SV_MODULE_REQUIRED([sndfile],[sndfile >= 1.0.16],[sndfile.h],[sndfile],[sf_open])
 SV_MODULE_REQUIRED([samplerate],[samplerate >= 0.1.2],[samplerate.h],[samplerate],[src_new])
-SV_MODULE_REQUIRED([vamp],[vamp >= 2.1],[vamp/vamp.h],[],[])
-SV_MODULE_REQUIRED([vamphostsdk],[vamp-hostsdk >= 2.5],[vamp-hostsdk/PluginLoader.h],[vamp-hostsdk],[libvamphostsdk_v_2_5_present])
 SV_MODULE_REQUIRED([rubberband],[rubberband],[rubberband/RubberBandStretcher.h],[rubberband],[rubberband_new])
 
 SV_MODULE_OPTIONAL([liblo],[],[lo/lo.h],[lo],[lo_address_new])
-SV_MODULE_OPTIONAL([portaudio_2_0],[portaudio-2.0 >= 19],[portaudio.h],[portaudio],[Pa_IsFormatSupported])
+SV_MODULE_OPTIONAL([portaudio],[portaudio-2.0 >= 19],[portaudio.h],[portaudio],[Pa_IsFormatSupported])
 SV_MODULE_OPTIONAL([JACK],[jack >= 0.100],[jack/jack.h],[jack],[jack_client_open])
 SV_MODULE_OPTIONAL([libpulse],[libpulse >= 0.9],[pulse/pulseaudio.h],[pulse],[pa_stream_new])
 SV_MODULE_OPTIONAL([lrdf],[lrdf >= 0.2],[lrdf.h],[lrdf],[lrdf_init])
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/files.pri	Fri Jan 13 10:29:50 2017 +0000
@@ -0,0 +1,168 @@
+
+SVGUI_HEADERS += \
+           layer/Colour3DPlotLayer.h \
+	   layer/Colour3DPlotRenderer.h \
+	   layer/ColourDatabase.h \
+	   layer/ColourMapper.h \
+           layer/ColourScale.h \
+           layer/ColourScaleLayer.h \
+           layer/FlexiNoteLayer.h \
+           layer/ImageLayer.h \
+           layer/ImageRegionFinder.h \
+           layer/Layer.h \
+           layer/LayerFactory.h \
+           layer/LayerGeometryProvider.h \
+           layer/LinearNumericalScale.h \
+           layer/LogNumericalScale.h \
+           layer/LinearColourScale.h \
+           layer/LogColourScale.h \
+           layer/NoteLayer.h \
+           layer/PaintAssistant.h \
+           layer/PianoScale.h \
+           layer/RegionLayer.h \
+           layer/RenderTimer.h \
+           layer/ScrollableImageCache.h \
+           layer/ScrollableMagRangeCache.h \
+           layer/SingleColourLayer.h \
+           layer/SliceableLayer.h \
+           layer/SliceLayer.h \
+           layer/SpectrogramLayer.h \
+           layer/SpectrumLayer.h \
+           layer/TextLayer.h \
+           layer/TimeInstantLayer.h \
+           layer/TimeRulerLayer.h \
+           layer/TimeValueLayer.h \
+           layer/VerticalScaleLayer.h \
+           layer/WaveformLayer.h \
+	   view/AlignmentView.h \
+           view/Overview.h \
+           view/Pane.h \
+           view/PaneStack.h \
+           view/View.h \
+           view/ViewManager.h \
+           view/ViewProxy.h \
+	   widgets/ActivityLog.h \
+           widgets/AudioDial.h \
+           widgets/ClickableLabel.h \
+           widgets/ColourComboBox.h \
+           widgets/ColourMapComboBox.h \
+           widgets/ColourNameDialog.h \
+           widgets/CommandHistory.h \
+           widgets/CSVFormatDialog.h \
+           widgets/Fader.h \
+           widgets/InteractiveFileFinder.h \
+           widgets/IconLoader.h \
+           widgets/ImageDialog.h \
+           widgets/ItemEditDialog.h \
+           widgets/KeyReference.h \
+           widgets/LabelCounterInputDialog.h \
+           widgets/LayerTree.h \
+           widgets/LayerTreeDialog.h \
+           widgets/LEDButton.h \
+           widgets/LevelPanToolButton.h \
+           widgets/LevelPanWidget.h \
+           widgets/ListInputDialog.h \
+           widgets/MIDIFileImportDialog.h \
+           widgets/ModelDataTableDialog.h \
+           widgets/NotifyingCheckBox.h \
+           widgets/NotifyingComboBox.h \
+           widgets/NotifyingPushButton.h \
+           widgets/NotifyingTabBar.h \
+           widgets/NotifyingToolButton.h \
+           widgets/Panner.h \
+           widgets/PluginParameterBox.h \
+           widgets/PluginParameterDialog.h \
+           widgets/ProgressDialog.h \
+           widgets/PropertyBox.h \
+           widgets/PropertyStack.h \
+           widgets/RangeInputDialog.h \
+           widgets/SelectableLabel.h \
+           widgets/SubdividingMenu.h \
+           widgets/TextAbbrev.h \
+           widgets/Thumbwheel.h \
+           widgets/TipDialog.h \
+           widgets/TransformFinder.h \
+           widgets/UnitConverter.h \
+           widgets/WidgetScale.h \
+           widgets/WindowShapePreview.h \
+           widgets/WindowTypeSelector.h
+
+SVGUI_SOURCES += \
+           layer/Colour3DPlotLayer.cpp \
+	   layer/Colour3DPlotRenderer.cpp \
+	   layer/ColourDatabase.cpp \
+	   layer/ColourMapper.cpp \
+	   layer/ColourScale.cpp \
+           layer/FlexiNoteLayer.cpp \
+           layer/ImageLayer.cpp \
+           layer/ImageRegionFinder.cpp \
+           layer/Layer.cpp \
+           layer/LayerFactory.cpp \
+           layer/LinearNumericalScale.cpp \
+           layer/LogNumericalScale.cpp \
+           layer/LinearColourScale.cpp \
+           layer/LogColourScale.cpp \
+           layer/NoteLayer.cpp \
+           layer/PaintAssistant.cpp \
+           layer/PianoScale.cpp \
+           layer/RegionLayer.cpp \
+           layer/ScrollableImageCache.cpp \
+           layer/ScrollableMagRangeCache.cpp \
+           layer/SingleColourLayer.cpp \
+           layer/SliceLayer.cpp \
+           layer/SpectrogramLayer.cpp \
+           layer/SpectrumLayer.cpp \
+           layer/TextLayer.cpp \
+           layer/TimeInstantLayer.cpp \
+           layer/TimeRulerLayer.cpp \
+           layer/TimeValueLayer.cpp \
+           layer/WaveformLayer.cpp \
+	   view/AlignmentView.cpp \
+           view/Overview.cpp \
+           view/Pane.cpp \
+           view/PaneStack.cpp \
+           view/View.cpp \
+           view/ViewManager.cpp \
+	   widgets/ActivityLog.cpp \
+           widgets/AudioDial.cpp \
+           widgets/ColourComboBox.cpp \
+           widgets/ColourMapComboBox.cpp \
+           widgets/ColourNameDialog.cpp \
+           widgets/CommandHistory.cpp \
+           widgets/CSVFormatDialog.cpp \
+           widgets/Fader.cpp \
+           widgets/InteractiveFileFinder.cpp \
+           widgets/IconLoader.cpp \
+           widgets/ImageDialog.cpp \
+           widgets/ItemEditDialog.cpp \
+           widgets/KeyReference.cpp \
+           widgets/LabelCounterInputDialog.cpp \
+           widgets/LayerTree.cpp \
+           widgets/LayerTreeDialog.cpp \
+           widgets/LEDButton.cpp \
+           widgets/LevelPanToolButton.cpp \
+           widgets/LevelPanWidget.cpp \
+           widgets/ListInputDialog.cpp \
+           widgets/MIDIFileImportDialog.cpp \
+           widgets/ModelDataTableDialog.cpp \
+           widgets/NotifyingCheckBox.cpp \
+           widgets/NotifyingComboBox.cpp \
+           widgets/NotifyingPushButton.cpp \
+           widgets/NotifyingTabBar.cpp \
+           widgets/NotifyingToolButton.cpp \
+           widgets/Panner.cpp \
+           widgets/PluginParameterBox.cpp \
+           widgets/PluginParameterDialog.cpp \
+           widgets/ProgressDialog.cpp \
+           widgets/PropertyBox.cpp \
+           widgets/PropertyStack.cpp \
+           widgets/RangeInputDialog.cpp \
+           widgets/SelectableLabel.cpp \
+           widgets/SubdividingMenu.cpp \
+           widgets/TextAbbrev.cpp \
+           widgets/Thumbwheel.cpp \
+           widgets/TipDialog.cpp \
+           widgets/TransformFinder.cpp \
+           widgets/UnitConverter.cpp \
+           widgets/WindowShapePreview.cpp \
+           widgets/WindowTypeSelector.cpp
--- a/layer/Colour3DPlotLayer.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/Colour3DPlotLayer.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -15,25 +15,28 @@
 
 #include "Colour3DPlotLayer.h"
 
-#include "view/View.h"
 #include "base/Profiler.h"
 #include "base/LogRange.h"
 #include "base/RangeMapper.h"
+
 #include "ColourMapper.h"
+#include "LayerGeometryProvider.h"
+#include "PaintAssistant.h"
+
+#include "data/model/Dense3DModelPeakCache.h"
+
+#include "view/ViewManager.h"
 
 #include <QPainter>
 #include <QImage>
 #include <QRect>
 #include <QTextStream>
+#include <QSettings>
 
 #include <iostream>
 
 #include <cassert>
 
-#ifndef __GNUC__
-#include <alloca.h>
-#endif
-
 using std::vector;
 
 //#define DEBUG_COLOUR_3D_PLOT_LAYER_PAINT 1
@@ -41,32 +44,92 @@
 
 Colour3DPlotLayer::Colour3DPlotLayer() :
     m_model(0),
-    m_cache(0),
-    m_peaksCache(0),
-    m_cacheValidStart(0),
-    m_cacheValidEnd(0),
-    m_colourScale(LinearScale),
+    m_colourScale(ColourScaleType::Linear),
     m_colourScaleSet(false),
     m_colourMap(0),
     m_gain(1.0),
-    m_binScale(LinearBinScale),
-    m_normalizeColumns(false),
+    m_binScale(BinScale::Linear),
+    m_normalization(ColumnNormalization::None),
     m_normalizeVisibleArea(false),
-    m_normalizeHybrid(false),
     m_invertVertical(false),
     m_opaque(false),
     m_smooth(false),
     m_peakResolution(256),
     m_miny(0),
-    m_maxy(0)
+    m_maxy(0),
+    m_synchronous(false),
+    m_peakCache(0),
+    m_peakCacheDivisor(8)
 {
-    
+    QSettings settings;
+    settings.beginGroup("Preferences");
+    setColourMap(settings.value("colour-3d-plot-colour", ColourMapper::Green).toInt());
+    settings.endGroup();
 }
 
 Colour3DPlotLayer::~Colour3DPlotLayer()
 {
-    delete m_cache;
-    delete m_peaksCache;
+    invalidateRenderers();
+    delete m_peakCache;
+}
+
+ColourScaleType
+Colour3DPlotLayer::convertToColourScale(int value)
+{
+    switch (value) {
+    default:
+    case 0: return ColourScaleType::Linear;
+    case 1: return ColourScaleType::Log;
+    case 2: return ColourScaleType::PlusMinusOne;
+    case 3: return ColourScaleType::Absolute;
+    }
+}
+
+int
+Colour3DPlotLayer::convertFromColourScale(ColourScaleType scale)
+{
+    switch (scale) {
+    case ColourScaleType::Linear: return 0;
+    case ColourScaleType::Log: return 1;
+    case ColourScaleType::PlusMinusOne: return 2;
+    case ColourScaleType::Absolute: return 3;
+
+    case ColourScaleType::Meter:
+    case ColourScaleType::Phase:
+    default: return 0;
+    }
+}
+
+std::pair<ColumnNormalization, bool>
+Colour3DPlotLayer::convertToColumnNorm(int value)
+{
+    switch (value) {
+    default:
+    case 0: return { ColumnNormalization::None, false };
+    case 1: return { ColumnNormalization::Max1, false };
+    case 2: return { ColumnNormalization::None, true }; // visible area
+    case 3: return { ColumnNormalization::Hybrid, false };
+    }
+}
+
+int
+Colour3DPlotLayer::convertFromColumnNorm(ColumnNormalization norm, bool visible)
+{
+    if (visible) return 2;
+    switch (norm) {
+    case ColumnNormalization::None: return 0;
+    case ColumnNormalization::Max1: return 1;
+    case ColumnNormalization::Hybrid: return 3;
+
+    case ColumnNormalization::Sum1:
+    default: return 0;
+    }
+}
+
+void
+Colour3DPlotLayer::setSynchronousPainting(bool synchronous)
+{
+    m_synchronous = synchronous;
 }
 
 void
@@ -91,7 +154,11 @@
     } else if (model->getResolution() > 2) {
         m_peakResolution = 128;
     }
-    cacheInvalid();
+
+    delete m_peakCache;
+    m_peakCache = 0;
+
+    invalidateRenderers();
 
     emit modelReplaced();
     emit sliceableModelReplaced(oldModel, model);
@@ -100,34 +167,46 @@
 void
 Colour3DPlotLayer::cacheInvalid()
 {
-    delete m_cache;
-    delete m_peaksCache;
-    m_cache = 0;
-    m_peaksCache = 0;
-    m_cacheValidStart = 0;
-    m_cacheValidEnd = 0;
+    invalidateRenderers();
 }
 
 void
-Colour3DPlotLayer::cacheInvalid(sv_frame_t startFrame, sv_frame_t endFrame)
+Colour3DPlotLayer::cacheInvalid(sv_frame_t /* startFrame */,
+                                sv_frame_t /* endFrame */)
 {
-    if (!m_cache || !m_model) return;
+    //!!! should do this only if the range is visible
+    delete m_peakCache;
+    m_peakCache = 0;
+    
+    invalidateRenderers();
+}
 
-    int modelResolution = m_model->getResolution();
-    int start = int(startFrame / modelResolution);
-    int end = int(endFrame / modelResolution + 1);
-    if (m_cacheValidStart < end) m_cacheValidStart = end;
-    if (m_cacheValidEnd > start) m_cacheValidEnd = start;
-    if (m_cacheValidStart > m_cacheValidEnd) m_cacheValidEnd = m_cacheValidStart;
+void
+Colour3DPlotLayer::invalidateRenderers()
+{
+    for (ViewRendererMap::iterator i = m_renderers.begin();
+         i != m_renderers.end(); ++i) {
+        delete i->second;
+    }
+    m_renderers.clear();
+}
+
+Dense3DModelPeakCache *
+Colour3DPlotLayer::getPeakCache() const
+{
+    if (!m_peakCache) {
+        m_peakCache = new Dense3DModelPeakCache(m_model, m_peakCacheDivisor);
+    }
+    return m_peakCache;
 }
 
 void
 Colour3DPlotLayer::modelChanged()
 {
-    if (!m_colourScaleSet && m_colourScale == LinearScale) {
+    if (!m_colourScaleSet && m_colourScale == ColourScaleType::Linear) {
         if (m_model) {
             if (m_model->shouldUseLogValueScale()) {
-                setColourScale(LogScale);
+                setColourScale(ColourScaleType::Log);
             } else {
                 m_colourScaleSet = true;
             }
@@ -139,10 +218,10 @@
 void
 Colour3DPlotLayer::modelChangedWithin(sv_frame_t startFrame, sv_frame_t endFrame)
 {
-    if (!m_colourScaleSet && m_colourScale == LinearScale) {
+    if (!m_colourScaleSet && m_colourScale == ColourScaleType::Linear) {
         if (m_model && m_model->getWidth() > 50) {
             if (m_model->shouldUseLogValueScale()) {
-                setColourScale(LogScale);
+                setColourScale(ColourScaleType::Log);
             } else {
                 m_colourScaleSet = true;
             }
@@ -157,8 +236,7 @@
     PropertyList list;
     list.push_back("Colour");
     list.push_back("Colour Scale");
-    list.push_back("Normalize Columns");
-    list.push_back("Normalize Visible Area");
+    list.push_back("Normalization");
     list.push_back("Gain");
     list.push_back("Bin Scale");
     list.push_back("Invert Vertical Scale");
@@ -172,8 +250,7 @@
 {
     if (name == "Colour") return tr("Colour");
     if (name == "Colour Scale") return tr("Scale");
-    if (name == "Normalize Columns") return tr("Normalize Columns");
-    if (name == "Normalize Visible Area") return tr("Normalize Visible Area");
+    if (name == "Normalization") return tr("Normalization");
     if (name == "Invert Vertical Scale") return tr("Invert Vertical Scale");
     if (name == "Gain") return tr("Gain");
     if (name == "Opaque") return tr("Always Opaque");
@@ -185,8 +262,6 @@
 QString
 Colour3DPlotLayer::getPropertyIconName(const PropertyName &name) const
 {
-    if (name == "Normalize Columns") return "normalise-columns";
-    if (name == "Normalize Visible Area") return "normalise";
     if (name == "Invert Vertical Scale") return "invert-vertical";
     if (name == "Opaque") return "opaque";
     if (name == "Smooth") return "smooth";
@@ -197,20 +272,18 @@
 Colour3DPlotLayer::getPropertyType(const PropertyName &name) const
 {
     if (name == "Gain") return RangeProperty;
-    if (name == "Normalize Columns") return ToggleProperty;
-    if (name == "Normalize Visible Area") return ToggleProperty;
     if (name == "Invert Vertical Scale") return ToggleProperty;
     if (name == "Opaque") return ToggleProperty;
     if (name == "Smooth") return ToggleProperty;
+    if (name == "Colour") return ColourMapProperty;
     return ValueProperty;
 }
 
 QString
 Colour3DPlotLayer::getPropertyGroupName(const PropertyName &name) const
 {
-    if (name == "Normalize Columns" ||
-        name == "Normalize Visible Area" ||
-	name == "Colour Scale" ||
+    if (name == "Normalization" ||
+        name == "Colour Scale" ||
         name == "Gain") return tr("Scale");
     if (name == "Bin Scale" ||
         name == "Invert Vertical Scale") return tr("Bins");
@@ -246,11 +319,12 @@
 
     } else if (name == "Colour Scale") {
 
+        // linear, log, +/-1, abs
 	*min = 0;
 	*max = 3;
-        *deflt = (int)LinearScale;
+        *deflt = 0;
 
-	val = (int)m_colourScale;
+	val = convertFromColourScale(m_colourScale);
 
     } else if (name == "Colour") {
 
@@ -260,15 +334,13 @@
 
 	val = m_colourMap;
 
-    } else if (name == "Normalize Columns") {
+    } else if (name == "Normalization") {
 	
+        *min = 0;
+        *max = 3;
         *deflt = 0;
-	val = (m_normalizeColumns ? 1 : 0);
 
-    } else if (name == "Normalize Visible Area") {
-	
-        *deflt = 0;
-	val = (m_normalizeVisibleArea ? 1 : 0);
+        val = convertFromColumnNorm(m_normalization, m_normalizeVisibleArea);
 
     } else if (name == "Invert Vertical Scale") {
 	
@@ -279,7 +351,7 @@
 
 	*min = 0;
 	*max = 1;
-        *deflt = int(LinearBinScale);
+        *deflt = int(BinScale::Linear);
 	val = (int)m_binScale;
 
     } else if (name == "Opaque") {
@@ -315,6 +387,16 @@
 	case 3: return tr("Absolute");
 	}
     }
+    if (name == "Normalization") {
+        switch(value) {
+        default:
+        case 0: return tr("None");
+        case 1: return tr("Col");
+        case 2: return tr("View");
+        case 3: return tr("Hybrid");
+        }
+//        return ""; // icon only
+    }
     if (name == "Bin Scale") {
 	switch (value) {
 	default:
@@ -325,6 +407,22 @@
     return tr("<unknown>");
 }
 
+QString
+Colour3DPlotLayer::getPropertyValueIconName(const PropertyName &name,
+                                            int value) const
+{
+    if (name == "Normalization") {
+        switch(value) {
+        default:
+        case 0: return "normalise-none";
+        case 1: return "normalise-columns";
+        case 2: return "normalise";
+        case 3: return "normalise-hybrid";
+        }
+    }
+    return "";
+}
+
 RangeMapper *
 Colour3DPlotLayer::getNewPropertyRangeMapper(const PropertyName &name) const
 {
@@ -340,19 +438,9 @@
     if (name == "Gain") {
 	setGain(float(pow(10, value/20.0)));
     } else if (name == "Colour Scale") {
-	switch (value) {
-	default:
-	case 0: setColourScale(LinearScale); break;
-	case 1: setColourScale(LogScale); break;
-	case 2: setColourScale(PlusMinusOneScale); break;
-	case 3: setColourScale(AbsoluteScale); break;
-	}
+        setColourScale(convertToColourScale(value));
     } else if (name == "Colour") {
         setColourMap(value);
-    } else if (name == "Normalize Columns") {
-	setNormalizeColumns(value ? true : false);
-    } else if (name == "Normalize Visible Area") {
-	setNormalizeVisibleArea(value ? true : false);
     } else if (name == "Invert Vertical Scale") {
 	setInvertVertical(value ? true : false);
     } else if (name == "Opaque") {
@@ -362,19 +450,23 @@
     } else if (name == "Bin Scale") {
 	switch (value) {
 	default:
-	case 0: setBinScale(LinearBinScale); break;
-	case 1: setBinScale(LogBinScale); break;
+	case 0: setBinScale(BinScale::Linear); break;
+	case 1: setBinScale(BinScale::Log); break;
 	}
+    } else if (name == "Normalization") {
+        auto n = convertToColumnNorm(value);
+        setNormalization(n.first);
+        setNormalizeVisibleArea(n.second);
     }
 }
 
 void
-Colour3DPlotLayer::setColourScale(ColourScale scale)
+Colour3DPlotLayer::setColourScale(ColourScaleType scale)
 {
     if (m_colourScale == scale) return;
     m_colourScale = scale;
     m_colourScaleSet = true;
-    cacheInvalid();
+    invalidateRenderers();
     emit layerParametersChanged();
 }
 
@@ -383,7 +475,7 @@
 {
     if (m_colourMap == map) return;
     m_colourMap = map;
-    cacheInvalid();
+    invalidateRenderers();
     emit layerParametersChanged();
 }
 
@@ -392,7 +484,7 @@
 {
     if (m_gain == gain) return;
     m_gain = gain;
-    cacheInvalid();
+    invalidateRenderers();
     emit layerParametersChanged();
 }
 
@@ -407,52 +499,41 @@
 {
     if (m_binScale == binScale) return;
     m_binScale = binScale;
-    cacheInvalid();
+    invalidateRenderers();
     emit layerParametersChanged();
 }
 
-Colour3DPlotLayer::BinScale
+BinScale
 Colour3DPlotLayer::getBinScale() const
 {
     return m_binScale;
 }
 
 void
-Colour3DPlotLayer::setNormalizeColumns(bool n)
+Colour3DPlotLayer::setNormalization(ColumnNormalization n)
 {
-    if (m_normalizeColumns == n) return;
-    m_normalizeColumns = n;
-    cacheInvalid();
+    if (m_normalization == n) return;
+
+    m_normalization = n;
+    invalidateRenderers();
+    
     emit layerParametersChanged();
 }
 
-bool
-Colour3DPlotLayer::getNormalizeColumns() const
+ColumnNormalization
+Colour3DPlotLayer::getNormalization() const
 {
-    return m_normalizeColumns;
-}
-
-void
-Colour3DPlotLayer::setNormalizeHybrid(bool n)
-{
-    if (m_normalizeHybrid == n) return;
-    m_normalizeHybrid = n;
-    cacheInvalid();
-    emit layerParametersChanged();
-}
-
-bool
-Colour3DPlotLayer::getNormalizeHybrid() const
-{
-    return m_normalizeHybrid;
+    return m_normalization;
 }
 
 void
 Colour3DPlotLayer::setNormalizeVisibleArea(bool n)
 {
     if (m_normalizeVisibleArea == n) return;
+
     m_normalizeVisibleArea = n;
-    cacheInvalid();
+    invalidateRenderers();
+    
     emit layerParametersChanged();
 }
 
@@ -467,7 +548,7 @@
 {
     if (m_invertVertical == n) return;
     m_invertVertical = n;
-    cacheInvalid();
+    invalidateRenderers();
     emit layerParametersChanged();
 }
 
@@ -476,6 +557,7 @@
 {
     if (m_opaque == n) return;
     m_opaque = n;
+    invalidateRenderers();
     emit layerParametersChanged();
 }
 
@@ -484,6 +566,7 @@
 {
     if (m_smooth == n) return;
     m_smooth = n;
+    invalidateRenderers();
     emit layerParametersChanged();
 }
 
@@ -506,7 +589,7 @@
 }
 
 void
-Colour3DPlotLayer::setLayerDormant(const View *v, bool dormant)
+Colour3DPlotLayer::setLayerDormant(const LayerGeometryProvider *v, bool dormant)
 {
     if (dormant) {
 
@@ -530,16 +613,19 @@
 }
 
 bool
-Colour3DPlotLayer::isLayerScrollable(const View *v) const
+Colour3DPlotLayer::isLayerScrollable(const LayerGeometryProvider * /* v */) const
 {
     if (m_normalizeVisibleArea) {
         return false;
     }
-    if (shouldPaintDenseIn(v)) {
-        return true;
-    }
-    QPoint discard;
-    return !v->shouldIlluminateLocalFeatures(this, discard);
+    //!!! ah hang on, if we're potentially rendering incrementally
+    //!!! they we can't be scrollable
+    return false;
+//    if (getRenderer(v)->willRenderOpaque(v)) {
+//        return true;
+//    }
+//    QPoint discard;
+//    return !v->shouldIlluminateLocalFeatures(this, discard);
 }
 
 bool
@@ -584,12 +670,14 @@
     m_miny = int(lrint(min));
     m_maxy = int(lrint(max));
     
+    invalidateRenderers();
+    
     emit layerParametersChanged();
     return true;
 }
 
 bool
-Colour3DPlotLayer::getYScaleValue(const View *, int,
+Colour3DPlotLayer::getYScaleValue(const LayerGeometryProvider *, int,
                                   double &, QString &) const
 {
     return false;//!!!
@@ -630,6 +718,8 @@
     m_maxy = m_miny + dist;
     if (m_maxy > m_model->getHeight()) m_maxy = m_model->getHeight();
 
+    invalidateRenderers();
+    
 //    SVDEBUG << "Colour3DPlotLayer::setVerticalZoomStep(" <<step <<"):  after: miny = " << m_miny << ", maxy = " << m_maxy << endl;
     
     emit layerParametersChanged();
@@ -645,14 +735,14 @@
 }
 
 double
-Colour3DPlotLayer::getYForBin(View *v, double bin) const
+Colour3DPlotLayer::getYForBin(const LayerGeometryProvider *v, double bin) const
 {
     double y = bin;
     if (!m_model) return y;
     double mn = 0, mx = m_model->getHeight();
     getDisplayExtents(mn, mx);
-    double h = v->height();
-    if (m_binScale == LinearBinScale) {
+    double h = v->getPaintHeight();
+    if (m_binScale == BinScale::Linear) {
         y = h - (((bin - mn) * h) / (mx - mn));
     } else {
         double logmin = mn + 1, logmax = mx + 1;
@@ -662,21 +752,15 @@
     return y;
 }
 
-int
-Colour3DPlotLayer::getIYForBin(View *v, int bin) const
-{
-    return int(round(getYForBin(v, bin)));
-}
-
 double
-Colour3DPlotLayer::getBinForY(View *v, double y) const
+Colour3DPlotLayer::getBinForY(const LayerGeometryProvider *v, double y) const
 {
     double bin = y;
     if (!m_model) return bin;
     double mn = 0, mx = m_model->getHeight();
     getDisplayExtents(mn, mx);
-    double h = v->height();
-    if (m_binScale == LinearBinScale) {
+    double h = v->getPaintHeight();
+    if (m_binScale == BinScale::Linear) {
         bin = mn + ((h - y) * (mx - mn)) / h;
     } else {
         double logmin = mn + 1, logmax = mx + 1;
@@ -686,14 +770,8 @@
     return bin;
 }
 
-int
-Colour3DPlotLayer::getIBinForY(View *v, int y) const
-{
-    return int(floor(getBinForY(v, y)));
-}
-
 QString
-Colour3DPlotLayer::getFeatureDescription(View *v, QPoint &pos) const
+Colour3DPlotLayer::getFeatureDescription(LayerGeometryProvider *v, QPoint &pos) const
 {
     if (!m_model) return "";
 
@@ -724,8 +802,8 @@
     if (symin < 0) symin = 0;
     if (symax > sh) symax = sh;
 
- //    double binHeight = double(v->height()) / (symax - symin);
-//    int sy = int((v->height() - y) / binHeight) + symin;
+ //    double binHeight = double(v->getPaintHeight()) / (symax - symin);
+//    int sy = int((v->getPaintHeight() - y) / binHeight) + symin;
 
     int sy = getIBinForY(v, y);
 
@@ -755,14 +833,15 @@
 }
 
 int
-Colour3DPlotLayer::getColourScaleWidth(QPainter &) const
+Colour3DPlotLayer::getColourScaleWidth(QPainter &p) const
 {
-    int cw = 20;
+    // Font is rotated
+    int cw = p.fontMetrics().height();
     return cw;
 }
 
 int
-Colour3DPlotLayer::getVerticalScaleWidth(View *, bool, QPainter &paint) const
+Colour3DPlotLayer::getVerticalScaleWidth(LayerGeometryProvider *, bool, QPainter &paint) const
 {
     if (!m_model) return 0;
 
@@ -784,7 +863,7 @@
 }
 
 void
-Colour3DPlotLayer::paintVerticalScale(View *v, bool, QPainter &paint, QRect rect) const
+Colour3DPlotLayer::paintVerticalScale(LayerGeometryProvider *v, bool, QPainter &paint, QRect rect) const
 {
     if (!m_model) return;
 
@@ -793,49 +872,20 @@
     int cw = getColourScaleWidth(paint);
     
     int ch = h - 20;
-    if (ch > 20 && m_cache) {
+    if (ch > 20) {
 
-        double min = m_model->getMinimumLevel();
-        double max = m_model->getMaximumLevel();
+        double min = m_viewMags[v->getId()].getMin();
+        double max = m_viewMags[v->getId()].getMax();
 
-        double mmin = min;
-        double mmax = max;
+        if (max <= min) max = min + 0.1;
 
-        if (m_colourScale == LogScale) {
-            LogRange::mapRange(mmin, mmax);
-        } else if (m_colourScale == PlusMinusOneScale) {
-            mmin = -1.f;
-            mmax = 1.f;
-        } else if (m_colourScale == AbsoluteScale) {
-            if (mmin < 0) {
-                if (fabs(mmin) > fabs(mmax)) mmax = fabs(mmin);
-                else mmax = fabs(mmax);
-                mmin = 0;
-            } else {
-                mmin = fabs(mmin);
-                mmax = fabs(mmax);
-            }
-        }
-    
-        if (max == min) max = min + 1.f;
-        if (mmax == mmin) mmax = mmin + 1.f;
-    
         paint.setPen(v->getForeground());
         paint.drawRect(4, 10, cw - 8, ch+1);
 
         for (int y = 0; y < ch; ++y) {
             double value = ((max - min) * (double(ch-y) - 1.0)) / double(ch) + min;
-            if (m_colourScale == LogScale) {
-                value = LogRange::map(value);
-            }
-            int pixel = int(((value - mmin) * 256) / (mmax - mmin));
-            if (pixel >= 0 && pixel < 256) {
-                QRgb c = m_cache->color(pixel);
-                paint.setPen(QColor(qRed(c), qGreen(c), qBlue(c)));
-                paint.drawLine(5, 11 + y, cw - 5, 11 + y);
-            } else {
-                cerr << "WARNING: Colour3DPlotLayer::paintVerticalScale: value " << value << ", mmin " << mmin << ", mmax " << mmax << " leads to invalid pixel " << pixel << endl;
-            }
+            paint.setPen(getRenderer(v)->getColour(value));
+            paint.drawLine(5, 11 + y, cw - 5, 11 + y);
         }
 
         QString minstr = QString("%1").arg(min);
@@ -844,8 +894,12 @@
         paint.save();
 
         QFont font = paint.font();
-        font.setPixelSize(10);
-        paint.setFont(font);
+        if (font.pixelSize() > 0) {
+            int newSize = int(font.pixelSize() * 0.65);
+            if (newSize < 6) newSize = 6;
+            font.setPixelSize(newSize);
+            paint.setFont(font);
+        }
 
         int msw = paint.fontMetrics().width(maxstr);
 
@@ -855,12 +909,12 @@
 
         paint.setWorldMatrix(m);
 
-        v->drawVisibleText(paint, 2, 0, minstr, View::OutlinedText);
+        PaintAssistant::drawVisibleText(v, paint, 2, 0, minstr, PaintAssistant::OutlinedText);
 
         m.translate(ch - msw - 2, 0);
         paint.setWorldMatrix(m);
 
-        v->drawVisibleText(paint, 0, 0, maxstr, View::OutlinedText);
+        PaintAssistant::drawVisibleText(v, paint, 0, 0, maxstr, PaintAssistant::OutlinedText);
 
         paint.restore();
     }
@@ -882,6 +936,8 @@
 
     int py = h;
 
+    int defaultFontHeight = paint.fontMetrics().height();
+    
     for (int i = symin; i <= symax; ++i) {
 
         int y0;
@@ -891,9 +947,9 @@
 
         if (i > symin) {
             if (paint.fontMetrics().height() >= h) {
-                if (h >= 8) {
+                if (h >= defaultFontHeight * 0.8) {
                     QFont tf = paint.font();
-                    tf.setPixelSize(h-2);
+                    tf.setPixelSize(int(h * 0.8));
                     paint.setFont(tf);
                 } else {
                     continue;
@@ -931,22 +987,27 @@
     Profiler profiler("Colour3DPlotLayer::getColumn");
 
     DenseThreeDimensionalModel::Column values = m_model->getColumn(col);
-    while (values.size() < m_model->getHeight()) values.push_back(0.f);
-    if (!m_normalizeColumns && !m_normalizeHybrid) return values;
+    values.resize(m_model->getHeight(), 0.f);
+    if (m_normalization != ColumnNormalization::Max1 &&
+        m_normalization != ColumnNormalization::Hybrid) {
+        return values;
+    }
 
     double colMax = 0.f, colMin = 0.f;
     double min = 0.f, max = 0.f;
 
+    int nv = int(values.size());
+    
     min = m_model->getMinimumLevel();
     max = m_model->getMaximumLevel();
 
-    for (int y = 0; y < values.size(); ++y) {
+    for (int y = 0; y < nv; ++y) {
         if (y == 0 || values.at(y) > colMax) colMax = values.at(y);
         if (y == 0 || values.at(y) < colMin) colMin = values.at(y);
     }
     if (colMin == colMax) colMax = colMin + 1;
     
-    for (int y = 0; y < values.size(); ++y) {
+    for (int y = 0; y < nv; ++y) {
     
         double value = values.at(y);
         double norm = (value - colMin) / (colMax - colMin);
@@ -955,317 +1016,106 @@
         if (value != newvalue) values[y] = float(newvalue);
     }
 
-    if (m_normalizeHybrid && (colMax > 0.0)) {
+    if (m_normalization == ColumnNormalization::Hybrid
+        && (colMax > 0.0)) {
         double logmax = log10(colMax);
-        for (int y = 0; y < values.size(); ++y) {
+        for (int y = 0; y < nv; ++y) {
             values[y] = float(values[y] * logmax);
         }
     }
 
     return values;
 }
-    
-void
-Colour3DPlotLayer::fillCache(int firstColumn, int lastColumn) const
+
+Colour3DPlotRenderer *
+Colour3DPlotLayer::getRenderer(const LayerGeometryProvider *v) const
 {
-    // This call requests a (perhaps partial) fill of the cache
-    // between model columns firstColumn and lastColumn inclusive.
-    // The cache itself always has size sufficient to contain the
-    // whole model, but its validity may be less, depending on which
-    // regions have been requested via calls to this function.  Note
-    // that firstColumn and lastColumn are *model* column numbers. If
-    // the model starts at a frame > 0, a firstColumn of zero still
-    // corresponds to the first column in the model, not the first
-    // column on the resulting rendered layer.
+    if (m_renderers.find(v->getId()) == m_renderers.end()) {
 
-    Profiler profiler("Colour3DPlotLayer::fillCache", true);
+        Colour3DPlotRenderer::Sources sources;
+        sources.verticalBinLayer = this;
+        sources.fft = 0;
+        sources.source = m_model;
+        sources.peakCaches.push_back(getPeakCache());
 
-    int cacheWidth = m_model->getWidth();
-    int cacheHeight = m_model->getHeight();
+        ColourScale::Parameters cparams;
+        cparams.colourMap = m_colourMap;
+        cparams.scaleType = m_colourScale;
+        cparams.gain = m_gain;
 
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-    cerr << "Colour3DPlotLayer::fillCache: range " << firstColumn << " -> " << lastColumn << " (cache size will be " << cacheWidth << " x " << cacheHeight << ")" << endl;
-#endif
+        if (m_normalization == ColumnNormalization::None) {
+            cparams.minValue = m_model->getMinimumLevel();
+            cparams.maxValue = m_model->getMaximumLevel();
+        } else if (m_normalization == ColumnNormalization::Hybrid) {
+            cparams.minValue = 0;
+            cparams.maxValue = log10(m_model->getMaximumLevel() + 1.0);
+        }
 
-    if (m_cache && m_cache->height() != cacheHeight) {
-        // height has changed: delete everything rather than resizing
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-        cerr << "Colour3DPlotLayer::fillCache: Cache height has changed, recreating" << endl;
-#endif
-        delete m_cache;
-        delete m_peaksCache;
-        m_cache = 0;
-        m_peaksCache = 0;
-    } 
+        if (cparams.maxValue <= cparams.minValue) {
+            cparams.maxValue = cparams.minValue + 0.1;
+        }
+        
+        Colour3DPlotRenderer::Parameters params;
+        params.colourScale = ColourScale(cparams);
+        params.normalization = m_normalization;
+        params.binScale = m_binScale;
+        params.alwaysOpaque = m_opaque;
+        params.invertVertical = m_invertVertical;
+        params.interpolate = m_smooth;
 
-    if (m_cache && m_cache->width() != cacheWidth) {
-        // width has changed and we have an existing cache: resize it
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-        cerr << "Colour3DPlotLayer::fillCache: Cache width has changed, resizing existing cache" << endl;
-#endif
-        QImage *newCache =
-            new QImage(m_cache->copy(0, 0, cacheWidth, cacheHeight));
-        delete m_cache;
-        m_cache = newCache;
-        if (m_peaksCache) {
-            QImage *newPeaksCache =
-                new QImage(m_peaksCache->copy
-                           (0, 0, cacheWidth / m_peakResolution + 1, cacheHeight));
-            delete m_peaksCache;
-            m_peaksCache = newPeaksCache;
-        }
+        m_renderers[v->getId()] = new Colour3DPlotRenderer(sources, params);
     }
 
-    if (!m_cache) {
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-        cerr << "Colour3DPlotLayer::fillCache: Have no cache, making one" << endl;
-#endif
-        m_cache = new QImage(cacheWidth, cacheHeight, QImage::Format_Indexed8);
-        m_cache->setColorCount(256);
-        m_cache->fill(0);
-        if (!m_normalizeVisibleArea) {
-            m_peaksCache = new QImage
-                (cacheWidth / m_peakResolution + 1, cacheHeight,
-                 QImage::Format_Indexed8);
-            m_peaksCache->setColorCount(256);
-            m_peaksCache->fill(0);
-        } else if (m_peaksCache) {
-            delete m_peaksCache;
-            m_peaksCache = 0;
-        }
-        m_cacheValidStart = 0;
-        m_cacheValidEnd = 0;
-    }
+    return m_renderers[v->getId()];
+}
 
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-    cerr << "cache size = " << m_cache->width() << "x" << m_cache->height()
-         << " peaks cache size = " << m_peaksCache->width() << "x" << m_peaksCache->height() << endl;
-#endif
+void
+Colour3DPlotLayer::paintWithRenderer(LayerGeometryProvider *v,
+                                     QPainter &paint, QRect rect) const
+{
+    Colour3DPlotRenderer *renderer = getRenderer(v);
 
-    if (m_cacheValidStart <= firstColumn && m_cacheValidEnd >= lastColumn) {
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-        cerr << "Cache is valid in this region already" << endl;
-#endif
-        return;
+    Colour3DPlotRenderer::RenderResult result;
+    MagnitudeRange magRange;
+    int viewId = v->getId();
+
+    if (!renderer->geometryChanged(v)) {
+        magRange = m_viewMags[viewId];
     }
     
-    int fillStart = firstColumn;
-    int fillEnd = lastColumn;
+    if (m_synchronous) {
 
-    if (fillStart >= cacheWidth) fillStart = cacheWidth-1;
-    if (fillStart < 0) fillStart = 0;
-    if (fillEnd >= cacheWidth) fillEnd = cacheWidth-1;
-    if (fillEnd < 0) fillEnd = 0;
-    if (fillEnd < fillStart) fillEnd = fillStart;
-
-    bool normalizeVisible = (m_normalizeVisibleArea && !m_normalizeColumns);
-
-    if (!normalizeVisible && (m_cacheValidStart < m_cacheValidEnd)) {
-        
-        if (m_cacheValidEnd < fillStart) {
-            fillStart = m_cacheValidEnd + 1;
-        }
-        if (m_cacheValidStart > fillEnd) {
-            fillEnd = m_cacheValidStart - 1;
-        }
-        
-        m_cacheValidStart = std::min(fillStart, m_cacheValidStart);
-        m_cacheValidEnd = std::max(fillEnd, m_cacheValidEnd);
+        result = renderer->render(v, paint, rect);
 
     } else {
 
-        // when normalising the visible area, the only valid area,
-        // ever, is the currently visible one
+        result = renderer->renderTimeConstrained(v, paint, rect);
 
-        m_cacheValidStart = fillStart;
-        m_cacheValidEnd = fillEnd;
-    }
-
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-    cerr << "Cache size " << cacheWidth << "x" << cacheHeight << " will be valid from " << m_cacheValidStart << " to " << m_cacheValidEnd << " (fillStart = " << fillStart << ", fillEnd = " << fillEnd << ")" << endl;
-#endif
-
-    DenseThreeDimensionalModel::Column values;
-
-    double min = m_model->getMinimumLevel();
-    double max = m_model->getMaximumLevel();
-
-    if (m_colourScale == LogScale) {
-        LogRange::mapRange(min, max);
-    } else if (m_colourScale == PlusMinusOneScale) {
-        min = -1.f;
-        max = 1.f;
-    } else if (m_colourScale == AbsoluteScale) {
-        if (min < 0) {
-            if (fabs(min) > fabs(max)) max = fabs(min);
-            else max = fabs(max);
-            min = 0;
-        } else {
-            min = fabs(min);
-            max = fabs(max);
+        QRect uncached = renderer->getLargestUncachedRect(v);
+        if (uncached.width() > 0) {
+            v->updatePaintRect(uncached);
         }
     }
     
-    if (max == min) max = min + 1.f;
-    
-    ColourMapper mapper(m_colourMap, 0.f, 255.f);
-    
-    for (int index = 0; index < 256; ++index) {
-        QColor colour = mapper.map(index);
-        m_cache->setColor
-            (index, qRgb(colour.red(), colour.green(), colour.blue()));
-        if (m_peaksCache) {
-            m_peaksCache->setColor
-                (index, qRgb(colour.red(), colour.green(), colour.blue()));
+    magRange.sample(result.range);
+
+    if (magRange.isSet()) {
+        if (!(m_viewMags[viewId] == magRange)) {
+            m_viewMags[viewId] = magRange;
+    //!!! now need to do the normalise-visible thing
         }
     }
     
-    double visibleMax = 0.f, visibleMin = 0.f;
-
-    if (normalizeVisible) {
+    cerr << "mag range in this view: "
+         << m_viewMags[v->getId()].getMin()
+         << " -> "
+         << m_viewMags[v->getId()].getMax()
+         << endl;
         
-        for (int c = fillStart; c <= fillEnd; ++c) {
-	
-            values = getColumn(c);
-
-            double colMax = 0.f, colMin = 0.f;
-
-            for (int y = 0; y < cacheHeight; ++y) {
-                if (y >= values.size()) break;
-                if (y == 0 || values[y] > colMax) colMax = values[y];
-                if (y == 0 || values[y] < colMin) colMin = values[y];
-            }
-
-            if (c == fillStart || colMax > visibleMax) visibleMax = colMax;
-            if (c == fillStart || colMin < visibleMin) visibleMin = colMin;
-        }
-
-        if (m_colourScale == LogScale) {
-            visibleMin = LogRange::map(visibleMin);
-            visibleMax = LogRange::map(visibleMax);
-            if (visibleMin > visibleMax) std::swap(visibleMin, visibleMax);
-        } else if (m_colourScale == AbsoluteScale) {
-            if (visibleMin < 0) {
-                if (fabs(visibleMin) > fabs(visibleMax)) visibleMax = fabs(visibleMin);
-                else visibleMax = fabs(visibleMax);
-                visibleMin = 0;
-            } else {
-                visibleMin = fabs(visibleMin);
-                visibleMax = fabs(visibleMax);
-            }
-        }
-    }
-    
-    if (visibleMin == visibleMax) visibleMax = visibleMin + 1;
-
-    int *peaks = 0;
-    if (m_peaksCache) {
-        peaks = new int[cacheHeight];
-        for (int y = 0; y < cacheHeight; ++y) {
-            peaks[y] = 0;
-        }
-    }
-
-    Profiler profiler2("Colour3DPlotLayer::fillCache: filling", true);
-
-    for (int c = fillStart; c <= fillEnd; ++c) {
-	
-        values = getColumn(c);
-
-        if (c >= m_cache->width()) {
-            cerr << "ERROR: column " << c << " >= cache width "
-                 << m_cache->width() << endl;
-            continue;
-        }
-
-        for (int y = 0; y < cacheHeight; ++y) {
-
-            double value = min;
-            if (y < values.size()) {
-                value = values.at(y);
-            }
-
-            value = value * m_gain;
-
-            if (m_colourScale == LogScale) {
-                value = LogRange::map(value);
-            } else if (m_colourScale == AbsoluteScale) {
-                value = fabs(value);
-            }
-            
-            if (normalizeVisible) {
-                double norm = (value - visibleMin) / (visibleMax - visibleMin);
-                value = min + (max - min) * norm;
-            }
-
-            int pixel = int(((value - min) * 256) / (max - min));
-            if (pixel < 0) pixel = 0;
-            if (pixel > 255) pixel = 255;
-            if (peaks && (pixel > peaks[y])) peaks[y] = pixel;
-
-            if (m_invertVertical) {
-                m_cache->setPixel(c, cacheHeight - y - 1, pixel);
-            } else {
-                if (y >= m_cache->height()) {
-                    cerr << "ERROR: row " << y << " >= cache height " << m_cache->height() << endl;
-                } else {
-                    m_cache->setPixel(c, y, pixel);
-                }
-            }
-        }
-
-        if (peaks) {
-            int notch = (c % m_peakResolution);
-            if (notch == m_peakResolution-1 || c == fillEnd) {
-                int pc = c / m_peakResolution;
-                if (pc >= m_peaksCache->width()) {
-                    cerr << "ERROR: peak column " << pc
-                         << " (from col " << c << ") >= peaks cache width "
-                         << m_peaksCache->width() << endl;
-                    continue;
-                }
-                for (int y = 0; y < cacheHeight; ++y) {
-                    if (m_invertVertical) {
-                        m_peaksCache->setPixel(pc, cacheHeight - y - 1, peaks[y]);
-                    } else {
-                        if (y >= m_peaksCache->height()) {
-                            cerr << "ERROR: row " << y
-                                 << " >= peaks cache height "
-                                 << m_peaksCache->height() << endl;
-                        } else {
-                            m_peaksCache->setPixel(pc, y, peaks[y]);
-                        }
-                    }
-                }
-                for (int y = 0; y < cacheHeight; ++y) {
-                    peaks[y] = 0;
-                }
-            }
-        }
-    }
-
-    delete[] peaks;
-}
-
-bool
-Colour3DPlotLayer::shouldPaintDenseIn(const View *v) const
-{
-    if (!m_model || !v || !(v->getViewManager())) {
-        return false;
-    }
-    double srRatio =
-        v->getViewManager()->getMainModelSampleRate() / m_model->getSampleRate();
-    if (m_opaque || 
-        m_smooth ||
-        m_model->getHeight() >= v->height() ||
-        ((m_model->getResolution() * srRatio) / v->getZoomLevel()) < 2) {
-        return true;
-    }
-    return false;
 }
 
 void
-Colour3DPlotLayer::paint(View *v, QPainter &paint, QRect rect) const
+Colour3DPlotLayer::paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const
 {
 /*
     if (m_model) {
@@ -1280,387 +1130,35 @@
     int completion = 0;
     if (!m_model || !m_model->isOK() || !m_model->isReady(&completion)) {
 	if (completion > 0) {
-	    paint.fillRect(0, 10, v->width() * completion / 100,
+	    paint.fillRect(0, 10, v->getPaintWidth() * completion / 100,
 			   10, QColor(120, 120, 120));
 	}
 	return;
     }
 
-    if (m_normalizeVisibleArea && !m_normalizeColumns) rect = v->rect();
-
-    sv_frame_t modelStart = m_model->getStartFrame();
-    sv_frame_t modelEnd = m_model->getEndFrame();
-    int modelResolution = m_model->getResolution();
-
-    // The cache is from the model's start frame to the model's end
-    // frame at the model's window increment frames per pixel.  We
-    // want to draw from our start frame + x0 * zoomLevel to our start
-    // frame + x1 * zoomLevel at zoomLevel frames per pixel.
-
-    //  We have quite different paint mechanisms for rendering "large"
-    //  bins (more than one bin per pixel in both directions) and
-    //  "small".  This is "large"; see paintDense below for "small".
-
-    int x0 = rect.left();
-    int x1 = rect.right() + 1;
-
-    int h = v->height();
-
-    double srRatio =
-        v->getViewManager()->getMainModelSampleRate() / m_model->getSampleRate();
-
-    // the s-prefix values are source, i.e. model, column and bin numbers
-    int sx0 = int((double(v->getFrameForX(x0)) / srRatio - double(modelStart))
-                  / modelResolution);
-    int sx1 = int((double(v->getFrameForX(x1)) / srRatio - double(modelStart))
-                  / modelResolution);
-    int sh = m_model->getHeight();
-
-    int symin = m_miny;
-    int symax = m_maxy;
-    if (symax <= symin) {
-        symin = 0;
-        symax = sh;
-    }
-    if (symin < 0) symin = 0;
-    if (symax > sh) symax = sh;
-
-    if (sx0 > 0) --sx0;
-    fillCache(sx0 < 0 ? 0 : sx0,
-              sx1 < 0 ? 0 : sx1);
-
+    if (m_model->getWidth() == 0) {
 #ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-    cerr << "Colour3DPlotLayer::paint: height = "<< m_model->getHeight() << ", modelStart = " << modelStart << ", resolution = " << modelResolution << ", model rate = " << m_model->getSampleRate() << " (zoom level = " << v->getZoomLevel() << ", srRatio = " << srRatio << ")" << endl;
+        cerr << "Colour3DPlotLayer::paint(): model width == 0, "
+             << "nothing to paint (yet)" << endl;
 #endif
-
-    if (shouldPaintDenseIn(v)) {
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-        cerr << "calling paintDense" << endl;
-#endif
-        paintDense(v, paint, rect);
         return;
     }
 
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-    cerr << "Colour3DPlotLayer::paint: w " << x1-x0 << ", h " << h << ", sx0 " << sx0 << ", sx1 " << sx1 << ", sw " << sx1-sx0 << ", sh " << sh << endl;
-    cerr << "Colour3DPlotLayer: sample rate is " << m_model->getSampleRate() << ", resolution " << m_model->getResolution() << endl;
-#endif
-
-    QPoint illuminatePos;
-    bool illuminate = v->shouldIlluminateLocalFeatures(this, illuminatePos);
+    //!!!???
     
-    const int buflen = 40;
-    char labelbuf[buflen];
-
-    for (int sx = sx0; sx <= sx1; ++sx) {
-
-	sv_frame_t fx = sx * modelResolution + modelStart;
-
-	if (fx + modelResolution <= modelStart || fx > modelEnd) continue;
-
-        int rx0 = v->getXForFrame(int(double(fx) * srRatio));
-	int rx1 = v->getXForFrame(int(double(fx + modelResolution + 1) * srRatio));
-
-	int rw = rx1 - rx0;
-	if (rw < 1) rw = 1;
-
-	bool showLabel = (rw > 10 &&
-			  paint.fontMetrics().width("0.000000") < rw - 3 &&
-			  paint.fontMetrics().height() < (h / sh));
-        
-	for (int sy = symin; sy < symax; ++sy) {
-
-            int ry0 = getIYForBin(v, sy);
-            int ry1 = getIYForBin(v, sy + 1);
-            QRect r(rx0, ry1, rw, ry0 - ry1);
-
-	    QRgb pixel = qRgb(255, 255, 255);
-	    if (sx >= 0 && sx < m_cache->width() &&
-		sy >= 0 && sy < m_cache->height()) {
-		pixel = m_cache->pixel(sx, sy);
-	    }
-
-            if (rw == 1) {
-                paint.setPen(pixel);
-                paint.setBrush(Qt::NoBrush);
-                paint.drawLine(r.x(), r.y(), r.x(), r.y() + r.height() - 1);
-                continue;
-            }
-
-	    QColor pen(255, 255, 255, 80);
-	    QColor brush(pixel);
-
-            if (rw > 3 && r.height() > 3) {
-                brush.setAlpha(160);
-            }
-
-	    paint.setPen(Qt::NoPen);
-	    paint.setBrush(brush);
-
-	    if (illuminate) {
-		if (r.contains(illuminatePos)) {
-		    paint.setPen(v->getForeground());
-		}
-	    }
-            
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-//            cerr << "rect " << r.x() << "," << r.y() << " "
-//                      << r.width() << "x" << r.height() << endl;
-#endif
-
-	    paint.drawRect(r);
-
-	    if (showLabel) {
-		if (sx >= 0 && sx < m_cache->width() &&
-		    sy >= 0 && sy < m_cache->height()) {
-		    double value = m_model->getValueAt(sx, sy);
-		    snprintf(labelbuf, buflen, "%06f", value);
-		    QString text(labelbuf);
-		    v->drawVisibleText
-                        (paint,
-                         rx0 + 2,
-                         ry0 - h / sh - 1 + 2 + paint.fontMetrics().ascent(),
-                         text,
-                         View::OutlinedText);
-		}
-	    }
-	}
-    }
-}
-
-void
-Colour3DPlotLayer::paintDense(View *v, QPainter &paint, QRect rect) const
-{
-    Profiler profiler("Colour3DPlotLayer::paintDense", true);
-    if (!m_cache) return;
-
-    double modelStart = double(m_model->getStartFrame());
-    double modelResolution = double(m_model->getResolution());
-
-    sv_samplerate_t mmsr = v->getViewManager()->getMainModelSampleRate();
-    sv_samplerate_t msr = m_model->getSampleRate();
-    double srRatio = mmsr / msr;
-
-    int x0 = rect.left();
-    int x1 = rect.right() + 1;
-
-    const int w = x1 - x0; // const so it can be used as array size below
-    int h = v->height(); // we always paint full height
-    int sh = m_model->getHeight();
-
-    int symin = m_miny;
-    int symax = m_maxy;
-    if (symax <= symin) {
-        symin = 0;
-        symax = sh;
-    }
-    if (symin < 0) symin = 0;
-    if (symax > sh) symax = sh;
-
-    QImage img(w, h, QImage::Format_Indexed8);
-    img.setColorTable(m_cache->colorTable());
-
-    uchar *peaks = new uchar[w];
-    memset(peaks, 0, w);
-
-    int zoomLevel = v->getZoomLevel();
-    
-    QImage *source = m_cache;
-
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT    
-    cerr << "modelResolution " << modelResolution << ", srRatio "
-         << srRatio << ", m_peakResolution " << m_peakResolution
-         << ", zoomLevel " << zoomLevel << ", result "
-         << ((modelResolution * srRatio * m_peakResolution) / zoomLevel)
-         << endl;
-#endif
-
-    if (m_peaksCache) {
-        if (((modelResolution * srRatio * m_peakResolution) / zoomLevel) < 1) {
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT    
-            cerr << "using peaks cache" << endl;
-#endif
-            source = m_peaksCache;
-            modelResolution *= m_peakResolution;
-        } else {
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT    
-            cerr << "not using peaks cache" << endl;
-#endif
-        }
-    } else {
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT    
-        cerr << "have no peaks cache" << endl;
-#endif
+    if (m_normalizeVisibleArea) {
+        rect = v->getPaintRect();
     }
 
-    int sw = source->width();
-    
-    sv_frame_t xf = -1;
-    sv_frame_t nxf = v->getFrameForX(x0);
+    //!!! why is the setLayerDormant(false) found here in
+    //!!! SpectrogramLayer not present in Colour3DPlotLayer?
+    //!!! unnecessary? vestigial? forgotten?
 
-    double epsilon = 0.000001;
-
-    vector<double> sxa(w*2);
-    
-    for (int x = 0; x < w; ++x) {
-
-        xf = nxf;
-        nxf = xf + zoomLevel;
-
-        double sx0 = (double(xf) / srRatio - modelStart) / modelResolution;
-        double sx1 = (double(nxf) / srRatio - modelStart) / modelResolution;
-
-        sxa[x*2] = sx0;
-        sxa[x*2 + 1] = sx1;
-    }
-
-    double logmin = symin+1, logmax = symax+1;
-    LogRange::mapRange(logmin, logmax);
-
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-    cerr << "m_smooth = " << m_smooth << ", w = " << w << ", h = " << h << endl;
-#endif
-
-    if (m_smooth) {
-        
-        for (int y = 0; y < h; ++y) {
-
-            double sy = getBinForY(v, y) - 0.5;
-            int syi = int(sy + epsilon);
-            if (syi < 0 || syi >= source->height()) continue;
-
-            uchar *targetLine = img.scanLine(y);
-            uchar *sourceLine = source->scanLine(syi);
-            uchar *nextSource;
-            if (syi + 1 < source->height()) {
-                nextSource = source->scanLine(syi + 1);
-            } else {
-                nextSource = sourceLine;
-            }
-
-            for (int x = 0; x < w; ++x) {
-
-                targetLine[x] = 0;
-
-                double sx0 = sxa[x*2];
-                if (sx0 < 0) continue;
-                int sx0i = int(sx0 + epsilon);
-                if (sx0i >= sw) break;
-
-                double a = sourceLine[sx0i];
-                double b = a;
-                double value;
-
-                double sx1 = sxa[x*2+1];
-                if (sx1 > sx0 + 1.f) {
-                    int sx1i = int(sx1);
-                    bool have = false;
-                    for (int sx = sx0i; sx <= sx1i; ++sx) {
-                        if (sx < 0 || sx >= sw) continue;
-                        if (!have) {
-                            a = sourceLine[sx];
-                            b = nextSource[sx];
-                            have = true;
-                        } else {
-                            a = std::max(a, double(sourceLine[sx]));
-                            b = std::max(b, double(nextSource[sx]));
-                        }
-                    }
-                    double yprop = sy - syi;
-                    value = (a * (1.f - yprop) + b * yprop);
-                } else {
-                    a = sourceLine[sx0i];
-                    b = nextSource[sx0i];
-                    double yprop = sy - syi;
-                    value = (a * (1.f - yprop) + b * yprop);
-                    int oi = sx0i + 1;
-                    double xprop = sx0 - sx0i;
-                    xprop -= 0.5;
-                    if (xprop < 0) {
-                        oi = sx0i - 1;
-                        xprop = -xprop;
-                    }
-                    if (oi < 0 || oi >= sw) oi = sx0i;
-                    a = sourceLine[oi];
-                    b = nextSource[oi];
-                    value = (value * (1.f - xprop) +
-                             (a * (1.f - yprop) + b * yprop) * xprop);
-                }
-                
-                int vi = int(lrint(value));
-                if (vi > 255) vi = 255;
-                if (vi < 0) vi = 0;
-                targetLine[x] = uchar(vi);
-            }
-        }
-    } else {
-
-        double sy0 = getBinForY(v, 0);
-
-        int psy0i = -1, psy1i = -1;
-
-        for (int y = 0; y < h; ++y) {
-
-            double sy1 = sy0;
-            sy0 = getBinForY(v, double(y + 1));
-
-            int sy0i = int(sy0 + epsilon);
-            int sy1i = int(sy1);
-
-            uchar *targetLine = img.scanLine(y);
-
-            if (sy0i == psy0i && sy1i == psy1i) {
-                // same source scan line as just computed
-                goto copy;
-            }
-
-            psy0i = sy0i;
-            psy1i = sy1i;
-
-            for (int x = 0; x < w; ++x) {
-                peaks[x] = 0;
-            }
-        
-            for (int sy = sy0i; sy <= sy1i; ++sy) {
-
-                if (sy < 0 || sy >= source->height()) continue;
-
-                uchar *sourceLine = source->scanLine(sy);
-            
-                for (int x = 0; x < w; ++x) {
-
-                    double sx1 = sxa[x*2 + 1];
-                    if (sx1 < 0) continue;
-                    int sx1i = int(sx1);
-
-                    double sx0 = sxa[x*2];
-                    if (sx0 < 0) continue;
-                    int sx0i = int(sx0 + epsilon);
-                    if (sx0i >= sw) break;
-
-                    uchar peak = 0;
-                    for (int sx = sx0i; sx <= sx1i; ++sx) {
-                        if (sx < 0 || sx >= sw) continue;
-                        if (sourceLine[sx] > peak) peak = sourceLine[sx];
-                    }
-                    peaks[x] = peak;
-                }
-            }
-        
-        copy:
-            for (int x = 0; x < w; ++x) {
-                targetLine[x] = peaks[x];
-            }
-        }
-    }
-
-    delete[] peaks;
-
-    paint.drawImage(x0, 0, img);
+    paintWithRenderer(v, paint, rect);
 }
 
 bool
-Colour3DPlotLayer::snapToFeatureFrame(View *v, sv_frame_t &frame,
+Colour3DPlotLayer::snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
 				      int &resolution,
 				      SnapType snap) const
 {
@@ -1691,25 +1189,40 @@
 {
     QString s = QString("scale=\"%1\" "
                         "colourScheme=\"%2\" "
-                        "normalizeColumns=\"%3\" "
-                        "normalizeVisibleArea=\"%4\" "
-                        "minY=\"%5\" "
-                        "maxY=\"%6\" "
-                        "invertVertical=\"%7\" "
-                        "opaque=\"%8\" %9")
-	.arg((int)m_colourScale)
+                        "minY=\"%3\" "
+                        "maxY=\"%4\" "
+                        "invertVertical=\"%5\" "
+                        "opaque=\"%6\" %7")
+	.arg(convertFromColourScale(m_colourScale))
         .arg(m_colourMap)
-        .arg(m_normalizeColumns ? "true" : "false")
-        .arg(m_normalizeVisibleArea ? "true" : "false")
         .arg(m_miny)
         .arg(m_maxy)
         .arg(m_invertVertical ? "true" : "false")
         .arg(m_opaque ? "true" : "false")
         .arg(QString("binScale=\"%1\" smooth=\"%2\" gain=\"%3\" ")
-             .arg((int)m_binScale)
+             .arg(int(m_binScale))
              .arg(m_smooth ? "true" : "false")
              .arg(m_gain));
     
+    // New-style normalization attributes, allowing for more types of
+    // normalization in future: write out the column normalization
+    // type separately, and then whether we are normalizing visible
+    // area as well afterwards
+    
+    s += QString("columnNormalization=\"%1\" ")
+        .arg(m_normalization == ColumnNormalization::Max1 ? "peak" :
+             m_normalization == ColumnNormalization::Hybrid ? "hybrid" : "none");
+
+    // Old-style normalization attribute, for backward compatibility
+    
+    s += QString("normalizeColumns=\"%1\" ")
+	.arg(m_normalization == ColumnNormalization::Max1 ? "true" : "false");
+
+    // And this applies to both old- and new-style attributes
+    
+    s += QString("normalizeVisibleArea=\"%1\" ")
+        .arg(m_normalizeVisibleArea ? "true" : "false");
+    
     Layer::toXml(stream, indent, extraAttributes + " " + s);
 }
 
@@ -1718,22 +1231,16 @@
 {
     bool ok = false, alsoOk = false;
 
-    ColourScale scale = (ColourScale)attributes.value("scale").toInt(&ok);
-    if (ok) setColourScale(scale);
+    ColourScaleType colourScale = convertToColourScale
+        (attributes.value("colourScale").toInt(&ok));
+    if (ok) setColourScale(colourScale);
 
     int colourMap = attributes.value("colourScheme").toInt(&ok);
     if (ok) setColourMap(colourMap);
 
-    BinScale binscale = (BinScale)attributes.value("binScale").toInt(&ok);
-    if (ok) setBinScale(binscale);
-
-    bool normalizeColumns =
-        (attributes.value("normalizeColumns").trimmed() == "true");
-    setNormalizeColumns(normalizeColumns);
-
-    bool normalizeVisibleArea =
-        (attributes.value("normalizeVisibleArea").trimmed() == "true");
-    setNormalizeVisibleArea(normalizeVisibleArea);
+    BinScale binScale = (BinScale)
+	attributes.value("binScale").toInt(&ok);
+    if (ok) setBinScale(binScale);
 
     bool invertVertical =
         (attributes.value("invertVertical").trimmed() == "true");
@@ -1753,5 +1260,50 @@
     float min = attributes.value("minY").toFloat(&ok);
     float max = attributes.value("maxY").toFloat(&alsoOk);
     if (ok && alsoOk) setDisplayExtents(min, max);
+
+    bool haveNewStyleNormalization = false;
+    
+    QString columnNormalization = attributes.value("columnNormalization");
+
+    if (columnNormalization != "") {
+
+        haveNewStyleNormalization = true;
+
+        if (columnNormalization == "peak") {
+            setNormalization(ColumnNormalization::Max1);
+        } else if (columnNormalization == "hybrid") {
+            setNormalization(ColumnNormalization::Hybrid);
+        } else if (columnNormalization == "none") {
+            setNormalization(ColumnNormalization::None);
+        } else {
+            cerr << "NOTE: Unknown or unsupported columnNormalization attribute \""
+                 << columnNormalization << "\"" << endl;
+        }
+    }
+
+    if (!haveNewStyleNormalization) {
+
+        setNormalization(ColumnNormalization::None);
+
+        bool normalizeColumns =
+            (attributes.value("normalizeColumns").trimmed() == "true");
+        if (normalizeColumns) {
+            setNormalization(ColumnNormalization::Max1);
+        }
+
+        bool normalizeHybrid =
+            (attributes.value("normalizeHybrid").trimmed() == "true");
+        if (normalizeHybrid) {
+            setNormalization(ColumnNormalization::Hybrid);
+        }
+    }
+    
+    bool normalizeVisibleArea =
+        (attributes.value("normalizeVisibleArea").trimmed() == "true");
+    setNormalizeVisibleArea(normalizeVisibleArea);
+
+    //!!! todo: check save/reload scaling, compare with
+    //!!! SpectrogramLayer, compare with prior SV versions, compare
+    //!!! with Tony v1 and v2 and their save files
 }
 
--- a/layer/Colour3DPlotLayer.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/Colour3DPlotLayer.h	Fri Jan 13 10:29:50 2017 +0000
@@ -13,10 +13,14 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _COLOUR_3D_PLOT_H_
-#define _COLOUR_3D_PLOT_H_
+#ifndef COLOUR_3D_PLOT_LAYER_H
+#define COLOUR_3D_PLOT_LAYER_H
 
 #include "SliceableLayer.h"
+#include "VerticalBinLayer.h"
+
+#include "ColourScale.h"
+#include "Colour3DPlotRenderer.h"
 
 #include "data/model/DenseThreeDimensionalModel.h"
 
@@ -30,14 +34,11 @@
  * colour range.  Its source is a DenseThreeDimensionalModel.
  *
  * This was the original implementation for the spectrogram view, but
- * it was replaced with a more efficient implementation that derived
- * the spectrogram itself from a DenseTimeValueModel instead of using
- * a three-dimensional model.  This class is retained in case it
- * becomes useful, but it will probably need some cleaning up if it's
- * ever actually used.
+ * it was replaced for that purpose with a more efficient
+ * implementation that derived the spectrogram itself from a
+ * DenseTimeValueModel instead of using a three-dimensional model.
  */
-
-class Colour3DPlotLayer : public SliceableLayer
+class Colour3DPlotLayer : public VerticalBinLayer
 {
     Q_OBJECT
 
@@ -49,20 +50,21 @@
         return m_model ? m_model->getZoomConstraint() : 0;
     }
     virtual const Model *getModel() const { return m_model; }
-    virtual void paint(View *v, QPainter &paint, QRect rect) const;
+    virtual void paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
+    virtual void setSynchronousPainting(bool synchronous);
 
-    virtual int getVerticalScaleWidth(View *v, bool, QPainter &) const;
-    virtual void paintVerticalScale(View *v, bool, QPainter &paint, QRect rect) const;
+    virtual int getVerticalScaleWidth(LayerGeometryProvider *v, bool, QPainter &) const;
+    virtual void paintVerticalScale(LayerGeometryProvider *v, bool, QPainter &paint, QRect rect) const;
 
-    virtual QString getFeatureDescription(View *v, QPoint &) const;
+    virtual QString getFeatureDescription(LayerGeometryProvider *v, QPoint &) const;
 
-    virtual bool snapToFeatureFrame(View *v, sv_frame_t &frame, 
+    virtual bool snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame, 
 				    int &resolution,
 				    SnapType snap) const;
 
-    virtual void setLayerDormant(const View *v, bool dormant);
+    virtual void setLayerDormant(const LayerGeometryProvider *v, bool dormant);
 
-    virtual bool isLayerScrollable(const View *v) const;
+    virtual bool isLayerScrollable(const LayerGeometryProvider *v) const;
 
     virtual ColourSignificance getLayerColourSignificance() const {
         return ColourHasMeaningfulValue;
@@ -70,7 +72,7 @@
 
     void setModel(const DenseThreeDimensionalModel *model);
 
-    virtual int getCompletion(View *) const { return m_model->getCompletion(); }
+    virtual int getCompletion(LayerGeometryProvider *) const { return m_model->getCompletion(); }
 
     virtual PropertyList getProperties() const;
     virtual PropertyType getPropertyType(const PropertyName &) const;
@@ -81,19 +83,14 @@
                                          int *min, int *max, int *deflt) const;
     virtual QString getPropertyValueLabel(const PropertyName &,
 					  int value) const;
+    virtual QString getPropertyValueIconName(const PropertyName &,
+                                             int value) const;
     virtual RangeMapper *getNewPropertyRangeMapper(const PropertyName &) const;
     virtual void setProperty(const PropertyName &, int value);
     virtual void setProperties(const QXmlAttributes &);
     
-    enum ColourScale {
-        LinearScale,
-        LogScale,
-        PlusMinusOneScale,
-        AbsoluteScale
-    };
-
-    void setColourScale(ColourScale);
-    ColourScale getColourScale() const { return m_colourScale; }
+    void setColourScale(ColourScaleType);
+    ColourScaleType getColourScale() const { return m_colourScale; }
 
     void setColourMap(int map);
     int getColourMap() const;
@@ -104,11 +101,6 @@
      */
     void setGain(float gain);
     float getGain() const;
-
-    enum BinScale {
-	LinearBinScale,
-	LogBinScale
-    };
     
     /**
      * Specify the scale for the y axis.
@@ -117,25 +109,17 @@
     BinScale getBinScale() const;
 
     /**
-     * Normalize each column to its maximum value, independent of its
-     * neighbours.
+     * Specify the normalization mode for individual columns.
      */
-    void setNormalizeColumns(bool n);
-    bool getNormalizeColumns() const;
+    void setNormalization(ColumnNormalization);
+    ColumnNormalization getNormalization() const;
 
     /**
-     * Normalize each value against the maximum in the visible region.
+     * Specify whether to normalize the visible area.
      */
-    void setNormalizeVisibleArea(bool n);
+    void setNormalizeVisibleArea(bool);
     bool getNormalizeVisibleArea() const;
 
-    /**
-     * Normalize each column to its maximum value, and then scale by
-     * the log of the (absolute) maximum value.
-     */
-    void setNormalizeHybrid(bool n);
-    bool getNormalizeHybrid() const;
-
     void setInvertVertical(bool i);
     bool getInvertVertical() const;
 
@@ -151,7 +135,7 @@
     virtual bool getDisplayExtents(double &min, double &max) const;
     virtual bool setDisplayExtents(double min, double max);
 
-    virtual bool getYScaleValue(const View *, int /* y */,
+    virtual bool getYScaleValue(const LayerGeometryProvider *, int /* y */,
                                 double &/* value */, QString &/* unit */) const;
 
     virtual int getVerticalZoomSteps(int &defaultStep) const;
@@ -173,41 +157,50 @@
 protected:
     const DenseThreeDimensionalModel *m_model; // I do not own this
     
-    mutable QImage *m_cache;
-    mutable QImage *m_peaksCache;
-    mutable int m_cacheValidStart;
-    mutable int m_cacheValidEnd;
-
-    ColourScale m_colourScale;
-    bool        m_colourScaleSet;
-    int         m_colourMap;
-    float       m_gain;
-    BinScale    m_binScale;
-    bool        m_normalizeColumns;
-    bool        m_normalizeVisibleArea;
-    bool        m_normalizeHybrid;
-    bool        m_invertVertical;
-    bool        m_opaque;
-    bool        m_smooth;
-    int         m_peakResolution;
+    ColourScaleType m_colourScale;
+    bool m_colourScaleSet;
+    int m_colourMap;
+    float m_gain;
+    BinScale m_binScale;
+    ColumnNormalization m_normalization; // of individual columns
+    bool m_normalizeVisibleArea;
+    bool m_invertVertical;
+    bool m_opaque;
+    bool m_smooth;
+    int m_peakResolution;
 
     // Minimum and maximum bin numbers visible within the view. We
     // always snap to whole bins at view edges.
-    int         m_miny;
-    int         m_maxy;
+    int m_miny;
+    int m_maxy;
 
+    bool m_synchronous;
+
+    static ColourScaleType convertToColourScale(int value);
+    static int convertFromColourScale(ColourScaleType);
+    static std::pair<ColumnNormalization, bool> convertToColumnNorm(int value);
+    static int convertFromColumnNorm(ColumnNormalization norm, bool visible);
+
+    mutable Dense3DModelPeakCache *m_peakCache;
+    const int m_peakCacheDivisor;
+    Dense3DModelPeakCache *getPeakCache() const;
+
+    typedef std::map<int, MagnitudeRange> ViewMagMap; // key is view id
+    mutable ViewMagMap m_viewMags;
+
+    typedef std::map<int, Colour3DPlotRenderer *> ViewRendererMap; // key is view id
+    mutable ViewRendererMap m_renderers;
+    
+    Colour3DPlotRenderer *getRenderer(const LayerGeometryProvider *) const;
+    void invalidateRenderers();
+        
     /**
      * Return the y coordinate at which the given bin "starts"
      * (i.e. at the bottom of the bin, if the given bin is an integer
      * and the vertical scale is the usual way up). Bin number may be
      * fractional, to obtain a position part-way through a bin.
      */
-    double getYForBin(View *, double bin) const;
-
-    /**
-     * As getYForBin, but rounding to integer values.
-     */
-    int getIYForBin(View *, int bin) const;
+    double getYForBin(const LayerGeometryProvider *, double bin) const;
     
     /**
      * Return the bin number, possibly fractional, at the given y
@@ -215,25 +208,13 @@
      * at which the bins "start" (i.e. the bottom of the visible bin,
      * if the vertical scale is the usual way up).
      */
-    double getBinForY(View *, double y) const;
-
-    /**
-     * As getBinForY, but rounding to integer values.
-     */
-    int getIBinForY(View *, int y) const;
+    double getBinForY(const LayerGeometryProvider *, double y) const;
     
     DenseThreeDimensionalModel::Column getColumn(int col) const;
 
-    /**
-     * True if we have the opaque or smooth flag set, or if the cells
-     * are so small you can't see their borders. False for big,
-     * translucent cells.
-     */
-    bool shouldPaintDenseIn(const View *) const; 
+    int getColourScaleWidth(QPainter &) const;
 
-    int getColourScaleWidth(QPainter &) const;
-    void fillCache(int firstBin, int lastBin) const;
-    void paintDense(View *v, QPainter &paint, QRect rect) const;
+    void paintWithRenderer(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
 };
 
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/Colour3DPlotRenderer.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -0,0 +1,1164 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2016 Chris Cannam and QMUL.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "Colour3DPlotRenderer.h"
+#include "RenderTimer.h"
+
+#include "base/Profiler.h"
+#include "base/HitCount.h"
+
+#include "data/model/DenseThreeDimensionalModel.h"
+#include "data/model/Dense3DModelPeakCache.h"
+#include "data/model/FFTModel.h"
+
+#include "LayerGeometryProvider.h"
+#include "VerticalBinLayer.h"
+#include "PaintAssistant.h"
+#include "ImageRegionFinder.h"
+
+#include "view/ViewManager.h" // for main model sample rate. Pity
+
+#include <vector>
+
+//#define DEBUG_COLOUR_PLOT_REPAINT 1
+
+using namespace std;
+
+Colour3DPlotRenderer::RenderResult
+Colour3DPlotRenderer::render(const LayerGeometryProvider *v, QPainter &paint, QRect rect)
+{
+    return render(v, paint, rect, false);
+}
+
+Colour3DPlotRenderer::RenderResult
+Colour3DPlotRenderer::renderTimeConstrained(const LayerGeometryProvider *v,
+                                            QPainter &paint, QRect rect)
+{
+    return render(v, paint, rect, true);
+}
+
+QRect
+Colour3DPlotRenderer::getLargestUncachedRect(const LayerGeometryProvider *v)
+{
+    RenderType renderType = decideRenderType(v);
+
+    if (renderType == DirectTranslucent) {
+        return QRect(); // never cached
+    }
+
+    int h = m_cache.getSize().height();
+
+    QRect areaLeft(0, 0, m_cache.getValidLeft(), h);
+    QRect areaRight(m_cache.getValidRight(), 0,
+                    m_cache.getSize().width() - m_cache.getValidRight(), h);
+
+    if (areaRight.width() > areaLeft.width()) {
+        return areaRight;
+    } else {
+        return areaLeft;
+    }
+}
+
+bool
+Colour3DPlotRenderer::geometryChanged(const LayerGeometryProvider *v)
+{
+    RenderType renderType = decideRenderType(v);
+
+    if (renderType == DirectTranslucent) {
+        return true; // never cached
+    }
+
+    if (m_cache.getSize() == v->getPaintSize() &&
+        m_cache.getZoomLevel() == v->getZoomLevel() &&
+        m_cache.getStartFrame() == v->getStartFrame()) {
+        return false;
+    } else {
+        return true;
+    }
+}
+
+Colour3DPlotRenderer::RenderResult
+Colour3DPlotRenderer::render(const LayerGeometryProvider *v,
+                             QPainter &paint, QRect rect, bool timeConstrained)
+{
+    RenderType renderType = decideRenderType(v);
+
+    if (renderType != DrawBufferPixelResolution) {
+        // Rendering should be fast in bin-resolution and direct draw
+        // cases because we are quite well zoomed-in, and the sums are
+        // easier this way. Calculating boundaries later will be
+        // fiddly for partial paints otherwise.
+        timeConstrained = false;
+    }
+
+    int x0 = v->getXForViewX(rect.x());
+    int x1 = v->getXForViewX(rect.x() + rect.width());
+    if (x0 < 0) x0 = 0;
+    if (x1 > v->getPaintWidth()) x1 = v->getPaintWidth();
+
+    sv_frame_t startFrame = v->getStartFrame();
+    
+    m_cache.resize(v->getPaintSize());
+    m_cache.setZoomLevel(v->getZoomLevel());
+
+    m_magCache.resize(v->getPaintSize().width());
+    m_magCache.setZoomLevel(v->getZoomLevel());
+    
+    if (renderType == DirectTranslucent) {
+        MagnitudeRange range = renderDirectTranslucent(v, paint, rect);
+        return { rect, range };
+    }
+    
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    SVDEBUG << "cache start " << m_cache.getStartFrame()
+         << " valid left " << m_cache.getValidLeft()
+         << " valid right " << m_cache.getValidRight()
+         << endl;
+    SVDEBUG << " view start " << startFrame
+         << " x0 " << x0
+         << " x1 " << x1
+         << endl;
+#endif
+
+    static HitCount count("Colour3DPlotRenderer: image cache");
+    
+    if (m_cache.isValid()) { // some part of the cache is valid
+
+        if (v->getXForFrame(m_cache.getStartFrame()) ==
+            v->getXForFrame(startFrame) &&
+            m_cache.getValidLeft() <= x0 &&
+            m_cache.getValidRight() >= x1) {
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+            SVDEBUG << "cache hit" << endl;
+#endif
+            count.hit();
+            
+            // cache is valid for the complete requested area
+            paint.drawImage(rect, m_cache.getImage(), rect);
+
+            MagnitudeRange range = m_magCache.getRange(x0, x1 - x0);
+
+            return { rect, range };
+
+        } else {
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+            SVDEBUG << "cache partial hit" << endl;
+#endif
+            count.partial();
+            
+            // cache doesn't begin at the right frame or doesn't
+            // contain the complete view, but might be scrollable or
+            // partially usable
+            m_cache.scrollTo(v, startFrame);
+            m_magCache.scrollTo(v, startFrame);
+
+            // if we are not time-constrained, then we want to paint
+            // the whole area in one go; we don't return a partial
+            // paint. To avoid providing the more complex logic to
+            // handle painting discontiguous areas, if the only valid
+            // part of cache is in the middle, just make the whole
+            // thing invalid and start again.
+            if (!timeConstrained) {
+                if (m_cache.getValidLeft() > x0 &&
+                    m_cache.getValidRight() < x1) {
+                    m_cache.invalidate();
+                }
+            }
+        }
+    } else {
+        // cache is completely invalid
+        count.miss();
+        m_cache.setStartFrame(startFrame);
+        m_magCache.setStartFrame(startFrame);
+    }
+
+    bool rightToLeft = false;
+
+    int reqx0 = x0;
+    int reqx1 = x1;
+    
+    if (!m_cache.isValid() && timeConstrained) {
+        // When rendering the whole area, in a context where we might
+        // not be able to complete the work, start from somewhere near
+        // the middle so that the region of interest appears
+        // first. But only if we aren't using a peak cache, as
+        // rendering from peak cache is usually (not always) quick and
+        // looks odd if we make a habit of jumping back after reaching
+        // the end.
+        if (x0 == 0 && x1 == v->getPaintWidth()) {
+            int peakCacheIndex = -1, binsPerPeak = -1;
+            getPreferredPeakCache(v, peakCacheIndex, binsPerPeak);
+            if (peakCacheIndex == -1) { // no peak cache
+                x0 = int(x1 * 0.3);
+            }
+        }
+    }
+
+    if (m_cache.isValid()) {
+            
+        // When rendering only a part of the cache, we need to make
+        // sure that the part we're rendering is adjacent to (or
+        // overlapping) a valid area of cache, if we have one. The
+        // alternative is to ditch the valid area of cache and render
+        // only the requested area, but that's risky because this can
+        // happen when just waving the pointer over a small part of
+        // the view -- if we lose the partly-built cache every time
+        // the user does that, we'll never finish building it.
+        int left = x0;
+        int width = x1 - x0;
+        bool isLeftOfValidArea = false;
+        m_cache.adjustToTouchValidArea(left, width, isLeftOfValidArea);
+        x0 = left;
+        x1 = x0 + width;
+
+        // That call also told us whether we should be painting
+        // sub-regions of our target region in right-to-left order in
+        // order to ensure contiguity
+        rightToLeft = isLeftOfValidArea;
+    }
+    
+    // Note, we always paint the full height to cache. We want to
+    // ensure the cache is coherent without having to worry about
+    // vertical matching of required and valid areas as well as
+    // horizontal.
+
+    if (renderType == DrawBufferBinResolution) {
+
+        renderToCacheBinResolution(v, x0, x1 - x0);
+
+    } else { // must be DrawBufferPixelResolution, handled DirectTranslucent earlier
+
+        renderToCachePixelResolution(v, x0, x1 - x0, rightToLeft, timeConstrained);
+    }
+
+    QRect pr = rect & m_cache.getValidArea();
+    paint.drawImage(pr.x(), pr.y(), m_cache.getImage(),
+                    pr.x(), pr.y(), pr.width(), pr.height());
+
+    if (!timeConstrained && (pr != rect)) {
+        cerr << "WARNING: failed to render entire requested rect "
+             << "even when not time-constrained" << endl;
+    }
+
+    MagnitudeRange range = m_magCache.getRange(reqx0, reqx1 - reqx0);
+    
+    return { pr, range };
+}
+
+Colour3DPlotRenderer::RenderType
+Colour3DPlotRenderer::decideRenderType(const LayerGeometryProvider *v) const
+{
+    const DenseThreeDimensionalModel *model = m_sources.source;
+    if (!model || !v || !(v->getViewManager())) {
+        return DrawBufferPixelResolution; // or anything
+    }
+
+    int binResolution = model->getResolution();
+    int zoomLevel = v->getZoomLevel();
+    sv_samplerate_t modelRate = model->getSampleRate();
+
+    double rateRatio = v->getViewManager()->getMainModelSampleRate() / modelRate;
+    double relativeBinResolution = binResolution * rateRatio;
+
+    if (m_params.binDisplay == BinDisplay::PeakFrequencies) {
+        // no alternative works here
+        return DrawBufferPixelResolution;
+    }
+
+    if (!m_params.alwaysOpaque && !m_params.interpolate) {
+
+        // consider translucent option -- only if not smoothing & not
+        // explicitly requested opaque & sufficiently zoomed-in
+        
+        if (model->getHeight() * 3 < v->getPaintHeight() &&
+            relativeBinResolution >= 3 * zoomLevel) {
+            return DirectTranslucent;
+        }
+    }
+
+    if (relativeBinResolution > zoomLevel) {
+        return DrawBufferBinResolution;
+    } else {
+        return DrawBufferPixelResolution;
+    }
+}
+
+ColumnOp::Column
+Colour3DPlotRenderer::getColumn(int sx, int minbin, int nbins,
+                                int peakCacheIndex) const
+{
+    Profiler profiler("Colour3DPlotRenderer::getColumn");
+    
+    // order:
+    // get column -> scale -> normalise -> record extents ->
+    // peak pick -> distribute/interpolate -> apply display gain
+
+    // we do the first bit here:
+    // get column -> scale -> normalise
+
+    ColumnOp::Column column;
+                
+    if (m_params.colourScale.getScale() == ColourScaleType::Phase &&
+        m_sources.fft) {
+
+        ColumnOp::Column fullColumn = m_sources.fft->getPhases(sx);
+
+        column = vector<float>(fullColumn.data() + minbin,
+                               fullColumn.data() + minbin + nbins);
+
+    } else {
+
+        ColumnOp::Column fullColumn =
+            (peakCacheIndex >= 0 ?
+             m_sources.peakCaches[peakCacheIndex] :
+             m_sources.source)
+            ->getColumn(sx);
+                
+        column = vector<float>(fullColumn.data() + minbin,
+                               fullColumn.data() + minbin + nbins);
+
+        column = ColumnOp::applyGain(column, m_params.scaleFactor);
+
+        column = ColumnOp::normalize(column, m_params.normalization);
+    }
+
+    return column;
+}
+
+MagnitudeRange
+Colour3DPlotRenderer::renderDirectTranslucent(const LayerGeometryProvider *v,
+                                              QPainter &paint,
+                                              QRect rect)
+{
+    Profiler profiler("Colour3DPlotRenderer::renderDirectTranslucent");
+
+    MagnitudeRange magRange;
+    
+    QPoint illuminatePos;
+    bool illuminate = v->shouldIlluminateLocalFeatures
+        (m_sources.verticalBinLayer, illuminatePos);
+
+    const DenseThreeDimensionalModel *model = m_sources.source;
+    
+    int x0 = rect.left();
+    int x1 = rect.right() + 1;
+
+    int h = v->getPaintHeight();
+
+    sv_frame_t modelStart = model->getStartFrame();
+    sv_frame_t modelEnd = model->getEndFrame();
+    int modelResolution = model->getResolution();
+
+    double rateRatio =
+        v->getViewManager()->getMainModelSampleRate() / model->getSampleRate();
+
+    // the s-prefix values are source, i.e. model, column and bin numbers
+    int sx0 = int((double(v->getFrameForX(x0)) / rateRatio - double(modelStart))
+                  / modelResolution);
+    int sx1 = int((double(v->getFrameForX(x1)) / rateRatio - double(modelStart))
+                  / modelResolution);
+
+    int sh = model->getHeight();
+
+    const int buflen = 40;
+    char labelbuf[buflen];
+
+    int minbin = m_sources.verticalBinLayer->getIBinForY(v, h);
+    if (minbin >= sh) minbin = sh - 1;
+    if (minbin < 0) minbin = 0;
+    
+    int nbins  = m_sources.verticalBinLayer->getIBinForY(v, 0) - minbin + 1;
+    if (minbin + nbins > sh) nbins = sh - minbin;
+
+    int psx = -1;
+
+    vector<float> preparedColumn;
+
+    int modelWidth = model->getWidth();
+
+    for (int sx = sx0; sx <= sx1; ++sx) {
+
+        if (sx < 0 || sx >= modelWidth) {
+            continue;
+        }
+
+        if (sx != psx) {
+
+            // order:
+            // get column -> scale -> normalise -> record extents ->
+            // peak pick -> distribute/interpolate -> apply display gain
+
+            // this does the first three:
+            preparedColumn = getColumn(sx, minbin, nbins, false);
+            
+            magRange.sample(preparedColumn);
+
+            if (m_params.binDisplay == BinDisplay::PeakBins) {
+                preparedColumn = ColumnOp::peakPick(preparedColumn);
+            }
+
+            // Display gain belongs to the colour scale and is
+            // applied by the colour scale object when mapping it
+
+            psx = sx;
+        }
+
+	sv_frame_t fx = sx * modelResolution + modelStart;
+
+	if (fx + modelResolution <= modelStart || fx > modelEnd) continue;
+
+        int rx0 = v->getXForFrame(int(double(fx) * rateRatio));
+	int rx1 = v->getXForFrame(int(double(fx + modelResolution + 1) * rateRatio));
+
+	int rw = rx1 - rx0;
+	if (rw < 1) rw = 1;
+
+	bool showLabel = (rw > 10 &&
+			  paint.fontMetrics().width("0.000000") < rw - 3 &&
+			  paint.fontMetrics().height() < (h / sh));
+        
+	for (int sy = minbin; sy < minbin + nbins; ++sy) {
+
+            int ry0 = m_sources.verticalBinLayer->getIYForBin(v, sy);
+            int ry1 = m_sources.verticalBinLayer->getIYForBin(v, sy + 1);
+
+            if (m_params.invertVertical) {
+                ry0 = h - ry0 - 1;
+                ry1 = h - ry1 - 1;
+            }
+                    
+            QRect r(rx0, ry1, rw, ry0 - ry1);
+
+            float value = preparedColumn[sy - minbin];
+            QColor colour = m_params.colourScale.getColour(value,
+                                                           m_params.colourRotation);
+
+            if (rw == 1) {
+                paint.setPen(colour);
+                paint.setBrush(Qt::NoBrush);
+                paint.drawLine(r.x(), r.y(), r.x(), r.y() + r.height() - 1);
+                continue;
+            }
+
+	    QColor pen(255, 255, 255, 80);
+	    QColor brush(colour);
+
+            if (rw > 3 && r.height() > 3) {
+                brush.setAlpha(160);
+            }
+
+	    paint.setPen(Qt::NoPen);
+	    paint.setBrush(brush);
+
+	    if (illuminate) {
+		if (r.contains(illuminatePos)) {
+		    paint.setPen(v->getForeground());
+		}
+	    }
+            
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+//            SVDEBUG << "rect " << r.x() << "," << r.y() << " "
+//                      << r.width() << "x" << r.height() << endl;
+#endif
+
+	    paint.drawRect(r);
+
+	    if (showLabel) {
+                double value = model->getValueAt(sx, sy);
+                snprintf(labelbuf, buflen, "%06f", value);
+                QString text(labelbuf);
+                PaintAssistant::drawVisibleText
+                    (v,
+                     paint,
+                     rx0 + 2,
+                     ry0 - h / sh - 1 + 2 + paint.fontMetrics().ascent(),
+                     text,
+                     PaintAssistant::OutlinedText);
+	    }
+	}
+    }
+
+    return magRange;
+}
+
+void
+Colour3DPlotRenderer::getPreferredPeakCache(const LayerGeometryProvider *v,
+                                            int &peakCacheIndex,
+                                            int &binsPerPeak) const
+{
+    peakCacheIndex = -1;
+    binsPerPeak = -1;
+
+    const DenseThreeDimensionalModel *model = m_sources.source;
+    if (!model) return;
+    
+    int zoomLevel = v->getZoomLevel();
+    int binResolution = model->getResolution();
+    
+    for (int ix = 0; in_range_for(m_sources.peakCaches, ix); ++ix) {
+        int bpp = m_sources.peakCaches[ix]->getColumnsPerPeak();
+        int equivZoom = binResolution * bpp;
+        if (zoomLevel >= equivZoom) {
+            // this peak cache would work, though it might not be best
+            if (bpp > binsPerPeak) {
+                // ok, it's better than the best one we've found so far
+                peakCacheIndex = ix;
+                binsPerPeak = bpp;
+            }
+        }
+    }
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    SVDEBUG << "getPreferredPeakCache: zoomLevel = " << zoomLevel
+            << ", binResolution " << binResolution 
+            << ", binsPerPeak " << binsPerPeak
+            << ", peakCacheIndex " << peakCacheIndex
+            << ", peakCaches " << m_sources.peakCaches.size()
+            << endl;
+#endif
+}
+
+void
+Colour3DPlotRenderer::renderToCachePixelResolution(const LayerGeometryProvider *v,
+                                                   int x0, int repaintWidth,
+                                                   bool rightToLeft,
+                                                   bool timeConstrained)
+{
+    Profiler profiler("Colour3DPlotRenderer::renderToCachePixelResolution");
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    SVDEBUG << "renderToCachePixelResolution" << endl;
+#endif
+    
+    // Draw to the draw buffer, and then copy from there. The draw
+    // buffer is at the same resolution as the target in the cache, so
+    // no extra scaling needed.
+
+    const DenseThreeDimensionalModel *model = m_sources.source;
+    if (!model || !model->isOK() || !model->isReady()) {
+	throw std::logic_error("no source model provided, or model not ready");
+    }
+
+    int h = v->getPaintHeight();
+
+    clearDrawBuffer(repaintWidth, h);
+
+    vector<int> binforx(repaintWidth);
+    vector<double> binfory(h);
+    
+    int binResolution = model->getResolution();
+
+    for (int x = 0; x < repaintWidth; ++x) {
+        sv_frame_t f0 = v->getFrameForX(x0 + x);
+        double s0 = double(f0 - model->getStartFrame()) / binResolution;
+        binforx[x] = int(s0 + 0.0001);
+    }
+
+    int peakCacheIndex = -1;
+    int binsPerPeak = -1;
+
+    if (m_params.colourScale.getScale() != ColourScaleType::Phase) {
+        getPreferredPeakCache(v, peakCacheIndex, binsPerPeak);
+    }
+    
+    for (int y = 0; y < h; ++y) {
+        binfory[y] = m_sources.verticalBinLayer->getBinForY(v, h - y - 1);
+    }
+
+    int attainedWidth;
+
+    if (m_params.binDisplay == BinDisplay::PeakFrequencies) {
+        attainedWidth = renderDrawBufferPeakFrequencies(v,
+                                                        repaintWidth,
+                                                        h,
+                                                        binforx,
+                                                        binfory,
+                                                        rightToLeft,
+                                                        timeConstrained);
+
+    } else {
+        attainedWidth = renderDrawBuffer(repaintWidth,
+                                         h,
+                                         binforx,
+                                         binfory,
+                                         peakCacheIndex,
+                                         rightToLeft,
+                                         timeConstrained);
+    }
+
+    if (attainedWidth == 0) return;
+
+    // draw buffer is pixel resolution, no scaling factors or padding involved
+    
+    int paintedLeft = x0;
+    if (rightToLeft) {
+        paintedLeft += (repaintWidth - attainedWidth);
+    }
+
+    m_cache.drawImage(paintedLeft, attainedWidth,
+                      m_drawBuffer,
+                      paintedLeft - x0, attainedWidth);
+
+    for (int i = 0; in_range_for(m_magRanges, i); ++i) {
+        m_magCache.sampleColumn(i, m_magRanges.at(i));
+    }
+}
+
+QImage
+Colour3DPlotRenderer::scaleDrawBufferImage(QImage image,
+                                           int targetWidth,
+                                           int targetHeight) const
+{
+    int sourceWidth = image.width();
+    int sourceHeight = image.height();
+
+    // We can only do this if we're making the image larger --
+    // otherwise peaks may be lost. So this should be called only when
+    // rendering in DrawBufferBinResolution mode. Whenever the bin
+    // size is smaller than the pixel size, in either x or y axis, we
+    // should be using DrawBufferPixelResolution mode instead
+    
+    if (targetWidth < sourceWidth || targetHeight < sourceHeight) {
+        throw std::logic_error("Colour3DPlotRenderer::scaleDrawBufferImage: Can only use this function when making the image larger; should be rendering DrawBufferPixelResolution instead");
+    }
+
+    if (sourceWidth <= 0 || sourceHeight <= 0) {
+        throw std::logic_error("Colour3DPlotRenderer::scaleDrawBufferImage: Source image is empty");
+    }
+
+    if (targetWidth <= 0 || targetHeight <= 0) {
+        throw std::logic_error("Colour3DPlotRenderer::scaleDrawBufferImage: Target image is empty");
+    }        
+
+    // This function exists because of some unpredictable behaviour
+    // from Qt when scaling images with FastTransformation mode. We
+    // continue to use Qt's scaler for SmoothTransformation but let's
+    // bring the non-interpolated version "in-house" so we know what
+    // it's really doing.
+    
+    if (m_params.interpolate) {
+        return image.scaled(targetWidth, targetHeight,
+                            Qt::IgnoreAspectRatio,
+                            Qt::SmoothTransformation);
+    }
+    
+    // Same format as the target cache
+    QImage target(targetWidth, targetHeight, QImage::Format_ARGB32_Premultiplied);
+
+    for (int y = 0; y < targetHeight; ++y) {
+
+        QRgb *targetLine = reinterpret_cast<QRgb *>(target.scanLine(y));
+        
+        int sy = int((uint64_t(y) * sourceHeight) / targetHeight);
+        if (sy == sourceHeight) --sy;
+
+        for (int x = 0; x < targetWidth; ++x) {
+
+            int sx = int((uint64_t(x) * sourceWidth) / targetWidth);
+            if (sx == sourceWidth) --sx;
+            
+            targetLine[x] = image.pixel(sx, sy);
+        }
+    }
+
+    return target;
+}
+
+void
+Colour3DPlotRenderer::renderToCacheBinResolution(const LayerGeometryProvider *v,
+                                                 int x0, int repaintWidth)
+{
+    Profiler profiler("Colour3DPlotRenderer::renderToCacheBinResolution");
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    SVDEBUG << "renderToCacheBinResolution" << endl;
+#endif
+    
+    // Draw to the draw buffer, and then scale-copy from there. Draw
+    // buffer is at bin resolution, i.e. buffer x == source column
+    // number. We use toolkit smooth scaling for interpolation.
+
+    const DenseThreeDimensionalModel *model = m_sources.source;
+    if (!model || !model->isOK() || !model->isReady()) {
+	throw std::logic_error("no source model provided, or model not ready");
+    }
+
+    // The draw buffer will contain a fragment at bin resolution. We
+    // need to ensure that it starts and ends at points where a
+    // time-bin boundary occurs at an exact pixel boundary, and with a
+    // certain amount of overlap across existing pixels so that we can
+    // scale and draw from it without smoothing errors at the edges.
+
+    // If (getFrameForX(x) / increment) * increment ==
+    // getFrameForX(x), then x is a time-bin boundary.  We want two
+    // such boundaries at either side of the draw buffer -- one which
+    // we draw up to, and one which we subsequently crop at.
+
+    sv_frame_t leftBoundaryFrame = -1, leftCropFrame = -1;
+    sv_frame_t rightBoundaryFrame = -1, rightCropFrame = -1;
+
+    int drawBufferWidth;
+    int binResolution = model->getResolution();
+
+    for (int x = x0; ; --x) {
+        sv_frame_t f = v->getFrameForX(x);
+        if ((f / binResolution) * binResolution == f) {
+            if (leftCropFrame == -1) leftCropFrame = f;
+            else if (x < x0 - 2) {
+                leftBoundaryFrame = f;
+                break;
+            }
+        }
+    }
+    for (int x = x0 + repaintWidth; ; ++x) {
+        sv_frame_t f = v->getFrameForX(x);
+        if ((f / binResolution) * binResolution == f) {
+            if (rightCropFrame == -1) rightCropFrame = f;
+            else if (x > x0 + repaintWidth + 2) {
+                rightBoundaryFrame = f;
+                break;
+            }
+        }
+    }
+    drawBufferWidth = int
+        ((rightBoundaryFrame - leftBoundaryFrame) / binResolution);
+    
+    int h = v->getPaintHeight();
+
+    // For our purposes here, the draw buffer needs to be exactly our
+    // target size (so we recreate always rather than just clear it)
+    
+    recreateDrawBuffer(drawBufferWidth, h);
+
+    vector<int> binforx(drawBufferWidth);
+    vector<double> binfory(h);
+    
+    for (int x = 0; x < drawBufferWidth; ++x) {
+        binforx[x] = int(leftBoundaryFrame / binResolution) + x;
+    }
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    SVDEBUG << "[BIN] binResolution " << binResolution << endl;
+#endif
+    
+    for (int y = 0; y < h; ++y) {
+        binfory[y] = m_sources.verticalBinLayer->getBinForY(v, h - y - 1);
+    }
+
+    int attainedWidth = renderDrawBuffer(drawBufferWidth,
+                                         h,
+                                         binforx,
+                                         binfory,
+                                         -1,
+                                         false,
+                                         false);
+
+    if (attainedWidth == 0) return;
+
+    int scaledLeft = v->getXForFrame(leftBoundaryFrame);
+    int scaledRight = v->getXForFrame(rightBoundaryFrame);
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    SVDEBUG << "scaling draw buffer from width " << m_drawBuffer.width()
+            << " to " << (scaledRight - scaledLeft) << " (nb drawBufferWidth = "
+            << drawBufferWidth << ")" << endl;
+#endif
+
+    QImage scaled = scaleDrawBufferImage
+        (m_drawBuffer, scaledRight - scaledLeft, h);
+            
+    int scaledLeftCrop = v->getXForFrame(leftCropFrame);
+    int scaledRightCrop = v->getXForFrame(rightCropFrame);
+    
+    int targetLeft = scaledLeftCrop;
+    if (targetLeft < 0) {
+        targetLeft = 0;
+    }
+    
+    int targetWidth = scaledRightCrop - targetLeft;
+    if (targetLeft + targetWidth > m_cache.getSize().width()) {
+        targetWidth = m_cache.getSize().width() - targetLeft;
+    }
+    
+    int sourceLeft = targetLeft - scaledLeft;
+    if (sourceLeft < 0) {
+        sourceLeft = 0;
+    }
+    
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    SVDEBUG << "repaintWidth = " << repaintWidth
+            << ", targetWidth = " << targetWidth << endl;
+#endif
+    
+    if (targetWidth > 0) {
+        // we are copying from an image that has already been scaled,
+        // hence using the same width in both geometries
+        m_cache.drawImage(targetLeft, targetWidth,
+                          scaled,
+                          sourceLeft, targetWidth);
+    }
+    
+    for (int i = 0; i < targetWidth; ++i) {
+        // but the mag range vector has not been scaled
+        int sourceIx = int((double(i + sourceLeft) / scaled.width())
+                           * int(m_magRanges.size()));
+        if (in_range_for(m_magRanges, sourceIx)) {
+            m_magCache.sampleColumn(i, m_magRanges.at(sourceIx));
+        }
+    }
+}
+
+int
+Colour3DPlotRenderer::renderDrawBuffer(int w, int h,
+                                       const vector<int> &binforx,
+                                       const vector<double> &binfory,
+                                       int peakCacheIndex,
+                                       bool rightToLeft,
+                                       bool timeConstrained)
+{
+    // Callers must have checked that the appropriate subset of
+    // Sources data members are set for the supplied flags (e.g. that
+    // peakCache corresponding to peakCacheIndex exists)
+    
+    RenderTimer timer(timeConstrained ?
+                      RenderTimer::FastRender :
+                      RenderTimer::NoTimeout);
+
+    Profiler profiler("Colour3DPlotRenderer::renderDrawBuffer");
+    
+    int divisor = 1;
+    const DenseThreeDimensionalModel *sourceModel = m_sources.source;
+    if (peakCacheIndex >= 0) {
+        divisor = m_sources.peakCaches[peakCacheIndex]->getColumnsPerPeak();
+        sourceModel = m_sources.peakCaches[peakCacheIndex];
+    }
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    SVDEBUG << "renderDrawBuffer: w = " << w << ", h = " << h
+            << ", peakCacheIndex = " << peakCacheIndex << " (divisor = "
+            << divisor << "), rightToLeft = " << rightToLeft
+            << ", timeConstrained = " << timeConstrained << endl;
+    SVDEBUG << "renderDrawBuffer: normalization = " << int(m_params.normalization)
+            << ", binDisplay = " << int(m_params.binDisplay)
+            << ", binScale = " << int(m_params.binScale)
+            << ", alwaysOpaque = " << m_params.alwaysOpaque
+            << ", interpolate = " << m_params.interpolate << endl;
+#endif
+    
+    int sh = sourceModel->getHeight();
+    
+    int minbin = int(binfory[0] + 0.0001);
+    if (minbin >= sh) minbin = sh - 1;
+    if (minbin < 0) minbin = 0;
+
+    int nbins  = int(binfory[h-1] + 0.0001) - minbin + 1;
+    if (minbin + nbins > sh) nbins = sh - minbin;
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    SVDEBUG << "minbin = " << minbin << ", nbins = " << nbins << ", last binfory = "
+         << binfory[h-1] << " (rounds to " << int(binfory[h-1]) << ") (model height " << sh << ")" << endl;
+#endif
+    
+    int psx = -1;
+
+    int start = 0;
+    int finish = w;
+    int step = 1;
+
+    if (rightToLeft) {
+        start = w-1;
+        finish = -1;
+        step = -1;
+    }
+
+    int columnCount = 0;
+    
+    vector<float> preparedColumn;
+
+    int modelWidth = sourceModel->getWidth();
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    SVDEBUG << "modelWidth " << modelWidth << ", divisor " << divisor << endl;
+#endif
+    
+    for (int x = start; x != finish; x += step) {
+
+        // x is the on-canvas pixel coord; sx (later) will be the
+        // source column index
+        
+        ++columnCount;
+        
+        if (binforx[x] < 0) continue;
+
+        int sx0 = binforx[x] / divisor;
+        int sx1 = sx0;
+        if (x+1 < w) sx1 = binforx[x+1] / divisor;
+        if (sx0 < 0) sx0 = sx1 - 1;
+        if (sx0 < 0) continue;
+        if (sx1 <= sx0) sx1 = sx0 + 1;
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+//        SVDEBUG << "x = " << x << ", binforx[x] = " << binforx[x] << ", sx range " << sx0 << " -> " << sx1 << endl;
+#endif
+
+        vector<float> pixelPeakColumn;
+        MagnitudeRange magRange;
+        
+        for (int sx = sx0; sx < sx1; ++sx) {
+
+            if (sx < 0 || sx >= modelWidth) {
+                continue;
+            }
+
+            if (sx != psx) {
+                
+                // order:
+                // get column -> scale -> normalise -> record extents ->
+                // peak pick -> distribute/interpolate -> apply display gain
+
+                // this does the first three:
+                ColumnOp::Column column = getColumn(sx, minbin, nbins,
+                                                    peakCacheIndex);
+
+                magRange.sample(column);
+
+                if (m_params.binDisplay == BinDisplay::PeakBins) {
+                    column = ColumnOp::peakPick(column);
+                }
+
+                preparedColumn =
+                    ColumnOp::distribute(column,
+                                         h,
+                                         binfory,
+                                         minbin,
+                                         m_params.interpolate);
+
+                // Display gain belongs to the colour scale and is
+                // applied by the colour scale object when mapping it
+                
+                psx = sx;
+            }
+
+            if (sx == sx0) {
+                pixelPeakColumn = preparedColumn;
+            } else {
+                for (int i = 0; in_range_for(pixelPeakColumn, i); ++i) {
+                    pixelPeakColumn[i] = std::max(pixelPeakColumn[i],
+                                                  preparedColumn[i]);
+                }
+            }
+        }
+
+        if (!pixelPeakColumn.empty()) {
+
+            for (int y = 0; y < h; ++y) {
+                int py;
+                if (m_params.invertVertical) {
+                    py = y;
+                } else {
+                    py = h - y - 1;
+                }
+                m_drawBuffer.setPixel
+                    (x,
+                     py,
+                     m_params.colourScale.getPixel(pixelPeakColumn[y]));
+            }
+            
+            m_magRanges.push_back(magRange);
+        }
+
+        double fractionComplete = double(columnCount) / double(w);
+        if (timer.outOfTime(fractionComplete)) {
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+            SVDEBUG << "out of time" << endl;
+#endif
+            return columnCount;
+        }
+    }
+
+    return columnCount;
+}
+
+int
+Colour3DPlotRenderer::renderDrawBufferPeakFrequencies(const LayerGeometryProvider *v,
+                                                      int w, int h,
+                                                      const vector<int> &binforx,
+                                                      const vector<double> &binfory,
+                                                      bool rightToLeft,
+                                                      bool timeConstrained)
+{
+    // Callers must have checked that the appropriate subset of
+    // Sources data members are set for the supplied flags (e.g. that
+    // fft model exists)
+    
+    RenderTimer timer(timeConstrained ?
+                      RenderTimer::FastRender :
+                      RenderTimer::NoTimeout);
+
+    const FFTModel *fft = m_sources.fft;
+
+    int sh = fft->getHeight();
+    
+    int minbin = int(binfory[0] + 0.0001);
+    if (minbin >= sh) minbin = sh - 1;
+    if (minbin < 0) minbin = 0;
+
+    int nbins  = int(binfory[h-1]) - minbin + 1;
+    if (minbin + nbins > sh) nbins = sh - minbin;
+
+    FFTModel::PeakSet peakfreqs;
+
+    int psx = -1;
+    
+    int start = 0;
+    int finish = w;
+    int step = 1;
+
+    if (rightToLeft) {
+        start = w-1;
+        finish = -1;
+        step = -1;
+    }
+    
+    int columnCount = 0;
+    
+    vector<float> preparedColumn;
+
+    int modelWidth = fft->getWidth();
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    SVDEBUG << "modelWidth " << modelWidth << endl;
+#endif
+    
+    double minFreq =
+        (double(minbin) * fft->getSampleRate()) / fft->getFFTSize();
+    double maxFreq =
+        (double(minbin + nbins - 1) * fft->getSampleRate()) / fft->getFFTSize();
+
+    bool logarithmic = (m_params.binScale == BinScale::Log);
+    
+    for (int x = start; x != finish; x += step) {
+        
+        // x is the on-canvas pixel coord; sx (later) will be the
+        // source column index
+        
+        ++columnCount;
+        
+        if (binforx[x] < 0) continue;
+
+        int sx0 = binforx[x];
+        int sx1 = sx0;
+        if (x+1 < w) sx1 = binforx[x+1];
+        if (sx0 < 0) sx0 = sx1 - 1;
+        if (sx0 < 0) continue;
+        if (sx1 <= sx0) sx1 = sx0 + 1;
+
+        vector<float> pixelPeakColumn;
+        MagnitudeRange magRange;
+        
+        for (int sx = sx0; sx < sx1; ++sx) {
+
+            if (sx < 0 || sx >= modelWidth) {
+                continue;
+            }
+
+            if (sx != psx) {
+                preparedColumn = getColumn(sx, minbin, nbins, false);
+                magRange.sample(preparedColumn);
+                psx = sx;
+            }
+
+            if (sx == sx0) {
+                pixelPeakColumn = preparedColumn;
+                peakfreqs = fft->getPeakFrequencies(FFTModel::AllPeaks, sx,
+                                                    minbin, minbin + nbins - 1);
+            } else {
+                for (int i = 0; in_range_for(pixelPeakColumn, i); ++i) {
+                    pixelPeakColumn[i] = std::max(pixelPeakColumn[i],
+                                                  preparedColumn[i]);
+                }
+            }
+        }
+
+        if (!pixelPeakColumn.empty()) {
+
+            for (FFTModel::PeakSet::const_iterator pi = peakfreqs.begin();
+                 pi != peakfreqs.end(); ++pi) {
+
+                int bin = pi->first;
+                double freq = pi->second;
+
+                if (bin < minbin) continue;
+                if (bin >= minbin + nbins) break;
+            
+                double value = pixelPeakColumn[bin - minbin];
+            
+                double y = v->getYForFrequency
+                    (freq, minFreq, maxFreq, logarithmic);
+            
+                int iy = int(y + 0.5);
+                if (iy < 0 || iy >= h) continue;
+
+                m_drawBuffer.setPixel
+                    (x,
+                     iy,
+                     m_params.colourScale.getPixel(value));
+            }
+
+            m_magRanges.push_back(magRange);
+        }
+
+        double fractionComplete = double(columnCount) / double(w);
+        if (timer.outOfTime(fractionComplete)) {
+            return columnCount;
+        }
+    }
+
+    return columnCount;
+}
+
+void
+Colour3DPlotRenderer::recreateDrawBuffer(int w, int h)
+{
+    m_drawBuffer = QImage(w, h, QImage::Format_Indexed8);
+
+    for (int pixel = 0; pixel < 256; ++pixel) {
+        m_drawBuffer.setColor
+            ((unsigned char)pixel,
+             m_params.colourScale.getColourForPixel
+             (pixel, m_params.colourRotation).rgb());
+    }
+
+    m_drawBuffer.fill(0);
+    m_magRanges.clear();
+}
+
+void
+Colour3DPlotRenderer::clearDrawBuffer(int w, int h)
+{
+    if (m_drawBuffer.width() < w || m_drawBuffer.height() != h) {
+        recreateDrawBuffer(w, h);
+    } else {
+        m_drawBuffer.fill(0);
+        m_magRanges.clear();
+    }
+}
+
+QRect
+Colour3DPlotRenderer::findSimilarRegionExtents(QPoint p) const
+{
+    QImage image = m_cache.getImage();
+    ImageRegionFinder finder;
+    QRect rect = finder.findRegionExtents(&image, p);
+    return rect;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/Colour3DPlotRenderer.h	Fri Jan 13 10:29:50 2017 +0000
@@ -0,0 +1,316 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2016 Chris Cannam and QMUL.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef COLOUR_3D_PLOT_RENDERER_H
+#define COLOUR_3D_PLOT_RENDERER_H
+
+#include "ColourScale.h"
+#include "ScrollableImageCache.h"
+#include "ScrollableMagRangeCache.h"
+
+#include "base/ColumnOp.h"
+#include "base/MagnitudeRange.h"
+
+#include <QRect>
+#include <QPainter>
+#include <QImage>
+
+class LayerGeometryProvider;
+class VerticalBinLayer;
+class DenseThreeDimensionalModel;
+class Dense3DModelPeakCache;
+class FFTModel;
+
+enum class BinDisplay {
+    AllBins,
+    PeakBins,
+    PeakFrequencies
+};
+
+enum class BinScale {
+    Linear,
+    Log
+};
+
+class Colour3DPlotRenderer
+{
+public:
+    struct Sources {
+        Sources() : verticalBinLayer(0), source(0), fft(0) { }
+        
+        // These must all outlive this class
+        const VerticalBinLayer *verticalBinLayer;  // always
+	const DenseThreeDimensionalModel *source;  // always
+	const FFTModel *fft;                       // optionally
+	std::vector<Dense3DModelPeakCache *> peakCaches; // zero or more
+    };        
+
+    struct Parameters {
+	Parameters() :
+	    colourScale(ColourScale::Parameters()),
+	    normalization(ColumnNormalization::None),
+	    binDisplay(BinDisplay::AllBins),
+            binScale(BinScale::Linear),
+	    alwaysOpaque(false),
+            interpolate(false),
+            invertVertical(false),
+            scaleFactor(1.0),
+            colourRotation(0) { }
+
+        /** A complete ColourScale object by value, used for colour
+         *  map conversion. Note that the final display gain setting is
+         *  also encapsulated here. */
+	ColourScale colourScale;
+
+        /** Type of column normalization. */
+	ColumnNormalization normalization;
+
+        /** Selection of bins to display. */
+	BinDisplay binDisplay;
+
+        /** Scale for vertical bin spacing (linear or logarithmic). */
+	BinScale binScale;
+
+        /** Whether cells should always be opaque. If false, then
+         *  large cells (when zoomed in a long way) will be rendered
+         *  translucent in order not to obscure anything in a layer
+         *  beneath. */
+	bool alwaysOpaque;
+
+        /** Whether to apply smoothing when rendering cells at more
+         *  than one pixel per cell.  !!! todo: decide about separating
+         *  out x-interpolate and y-interpolate as the spectrogram
+         *  actually does (or used to)
+         */
+	bool interpolate;
+
+        /** Whether to render the whole caboodle upside-down. */
+	bool invertVertical;
+
+        /** Initial scale factor (e.g. for FFT scaling). This factor
+         *  is applied to all values read from the underlying model
+         *  *before* magnitude ranges are calculated, in contrast to
+         *  the display gain found in the ColourScale parameter. */
+        double scaleFactor;
+
+        /** Colourmap rotation, in the range 0-255. */
+        int colourRotation;
+    };
+    
+    Colour3DPlotRenderer(Sources sources, Parameters parameters) :
+        m_sources(sources),
+	m_params(parameters)
+    { }
+
+    struct RenderResult {
+        /**
+         * The rect that was actually rendered. May be equal to the
+         * rect that was requested to render, or may be smaller if
+         * time ran out and the complete flag was not set.
+         */
+        QRect rendered;
+
+        /**
+         * The magnitude range of the data in the rendered area.
+         */
+        MagnitudeRange range;
+    };
+
+    /**
+     * Render the requested area using the given painter, obtaining
+     * geometry (e.g. start frame) from the given
+     * LayerGeometryProvider.
+     *
+     * The whole of the supplied rect will be rendered and the
+     * returned QRect will be equal to the supplied QRect. (See
+     * renderTimeConstrained for an alternative that may render only
+     * part of the rect in cases where obtaining source data is slow
+     * and retaining responsiveness is important.)
+     *
+     * Note that Colour3DPlotRenderer retains internal cache state
+     * related to the size and position of the supplied
+     * LayerGeometryProvider. Although it is valid to call render()
+     * successively on the same Colour3DPlotRenderer with different
+     * LayerGeometryProviders, it will be much faster to use a
+     * dedicated Colour3DPlotRenderer for each LayerGeometryProvider.
+     *
+     * If the model to render from is not ready, this will throw a
+     * std::logic_error exception. The model must be ready and the
+     * layer requesting the render must not be dormant in its view, so
+     * that the LayerGeometryProvider returns valid results; it is the
+     * caller's responsibility to ensure these.
+     */
+    RenderResult render(const LayerGeometryProvider *v,
+                        QPainter &paint, QRect rect);
+    
+    /**
+     * Render the requested area using the given painter, obtaining
+     * geometry (e.g. start frame) from the stored
+     * LayerGeometryProvider.
+     *
+     * As much of the rect will be rendered as can be managed given
+     * internal time constraints (using a RenderTimer object
+     * internally). The returned QRect (the rendered field in the
+     * RenderResult struct) will contain the area that was
+     * rendered. Note that we always render the full requested height,
+     * it's only width that is time-constrained.
+     *
+     * Note that Colour3DPlotRenderer retains internal cache state
+     * related to the size and position of the supplied
+     * LayerGeometryProvider. Although it is valid to call render()
+     * successively on the same Colour3DPlotRenderer with different
+     * LayerGeometryProviders, it will be much faster to use a
+     * dedicated Colour3DPlotRenderer for each LayerGeometryProvider.
+     *
+     * If the model to render from is not ready, this will throw a
+     * std::logic_error exception. The model must be ready and the
+     * layer requesting the render must not be dormant in its view, so
+     * that the LayerGeometryProvider returns valid results; it is the
+     * caller's responsibility to ensure these.
+     */
+    RenderResult renderTimeConstrained(const LayerGeometryProvider *v,
+                                       QPainter &paint, QRect rect);
+
+    /**
+     * Return the area of the largest rectangle within the entire area
+     * of the cache that is unavailable in the cache. This is only
+     * valid in relation to a preceding render() call which is
+     * presumed to have set the area, start frame, and zoom level for
+     * the cache. It could be used to establish a suitable region for
+     * a subsequent paint request (because if an area is not in the
+     * cache, it cannot have been rendered since the cache was
+     * cleared).
+     *
+     * Returns an empty QRect if the cache is entirely valid.
+     */
+    QRect getLargestUncachedRect(const LayerGeometryProvider *v);
+
+    /**
+     * Return true if the provider's geometry differs from the cache,
+     * or if we are not using a cache. i.e. if the cache will be
+     * regenerated for the next render, or the next render performed
+     * from scratch.
+     */
+    bool geometryChanged(const LayerGeometryProvider *v);
+    
+    /**
+     * Return true if the rendering will be opaque. This may be used
+     * by the calling layer to determine whether it can scroll
+     * directly without regard to any other layers beneath.
+     */
+    bool willRenderOpaque(const LayerGeometryProvider *v) {
+        return decideRenderType(v) != DirectTranslucent;
+    }
+    
+    /**
+     * Return the colour corresponding to the given value.
+     * \see ColourScale::getPixel
+     * \see ColourScale::getColour
+     */
+    QColor getColour(double value) const {
+        return m_params.colourScale.getColour(value, m_params.colourRotation);
+    }
+
+    /**
+     * Return the enclosing rectangle for the region of similar colour
+     * to the given point within the cache. Return an empty QRect if
+     * this is not possible. \see ImageRegionFinder
+     */
+    QRect findSimilarRegionExtents(QPoint point) const;
+    
+private:
+    Sources m_sources;
+    Parameters m_params;
+
+    // Draw buffer is the target of each partial repaint. It is always
+    // at view height (not model height) and is cleared and repainted
+    // on each fragment render. The only reason it's stored as a data
+    // member is to avoid reallocation.
+    QImage m_drawBuffer;
+
+    // A temporary store of magnitude ranges per-column, used when
+    // rendering to the draw buffer. This always has the same length
+    // as the width of the draw buffer, and the x coordinates of the
+    // two containers are equivalent.
+    std::vector<MagnitudeRange> m_magRanges;
+    
+    // The image cache is our persistent record of the visible
+    // area. It is always the same size as the view (i.e. the paint
+    // size reported by the LayerGeometryProvider) and is scrolled and
+    // partially repainted internally as appropriate. A render request
+    // is carried out by repainting to cache (via the draw buffer) any
+    // area that is being requested but is not valid in the cache, and
+    // then repainting from cache to the requested painter.
+    ScrollableImageCache m_cache;
+
+    // The mag range cache is our record of the column magnitude
+    // ranges for each of the columns in the cache. It always has the
+    // same start frame and width as the image cache, and the column
+    // indices match up across both. Our cache update mechanism
+    // guarantees that every valid column in the image cache has a
+    // valid range in the magnitude cache, but not necessarily vice
+    // versa (as the image cache is limited to contiguous ranges).
+    ScrollableMagRangeCache m_magCache;
+    
+    RenderResult render(const LayerGeometryProvider *v,
+                        QPainter &paint, QRect rect, bool timeConstrained);
+
+    MagnitudeRange renderDirectTranslucent(const LayerGeometryProvider *v,
+                                           QPainter &paint, QRect rect);
+    
+    void renderToCachePixelResolution(const LayerGeometryProvider *v, int x0,
+                                      int repaintWidth, bool rightToLeft,
+                                      bool timeConstrained);
+
+    void renderToCacheBinResolution(const LayerGeometryProvider *v, int x0,
+                                    int repaintWidth);
+
+    int renderDrawBuffer(int w, int h,
+                         const std::vector<int> &binforx,
+                         const std::vector<double> &binfory,
+                         int peakCacheIndex, // -1 => don't use a peak cache
+                         bool rightToLeft,
+                         bool timeConstrained);
+
+    int renderDrawBufferPeakFrequencies(const LayerGeometryProvider *v,
+                                        int w, int h,
+                                        const std::vector<int> &binforx,
+                                        const std::vector<double> &binfory,
+                                        bool rightToLeft,
+                                        bool timeConstrained);
+    
+    void recreateDrawBuffer(int w, int h);
+    void clearDrawBuffer(int w, int h);
+
+    enum RenderType {
+        DrawBufferPixelResolution,
+        DrawBufferBinResolution,
+        DirectTranslucent
+    };
+
+    RenderType decideRenderType(const LayerGeometryProvider *) const;
+
+    QImage scaleDrawBufferImage(QImage source, int targetWidth, int targetHeight)
+        const;
+    
+    ColumnOp::Column getColumn(int sx, int minbin, int nbins,
+                               int peakCacheIndex) const; // -1 => don't use cache
+
+    void getPreferredPeakCache(const LayerGeometryProvider *,
+                               int &peakCacheIndex, int &binsPerPeak) const;
+};
+
+#endif
+
--- a/layer/ColourMapper.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/ColourMapper.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -4,7 +4,7 @@
     Sonic Visualiser
     An audio file viewer and annotation editor.
     Centre for Digital Music, Queen Mary, University of London.
-    This file copyright 2006-2007 Chris Cannam and QMUL.
+    This file copyright 2006-2016 Chris Cannam and QMUL.
     
     This program is free software; you can redistribute it and/or
     modify it under the terms of the GNU General Public License as
@@ -21,8 +21,47 @@
 
 #include "base/Debug.h"
 
+#include <vector>
+
+#include <QPainter>
+
+using namespace std;
+
+static vector<QColor> convertStrings(const vector<QString> &strs)
+{
+    vector<QColor> converted;
+    for (const auto &s: strs) converted.push_back(QColor(s));
+    reverse(converted.begin(), converted.end());
+    return converted;
+}
+
+static vector<QColor> ice = convertStrings({
+        // Based on ColorBrewer ylGnBu
+        "#ffffff", "#ffff00", "#f7fcf0", "#e0f3db", "#ccebc5", "#a8ddb5",
+        "#7bccc4", "#4eb3d3", "#2b8cbe", "#0868ac", "#084081", "#042040"
+        });
+
+static vector<QColor> cherry = convertStrings({
+        "#f7f7f7", "#fddbc7", "#f4a582", "#d6604d", "#b2182b", "#dd3497",
+        "#ae017e", "#7a0177", "#49006a"
+        });
+    
+static void
+mapDiscrete(double norm, vector<QColor> &colours, double &r, double &g, double &b)
+{
+    int n = int(colours.size());
+    double m = norm * (n-1);
+    if (m >= n-1) { colours[n-1].getRgbF(&r, &g, &b, 0); return; }
+    if (m <= 0) { colours[0].getRgbF(&r, &g, &b, 0); return; }
+    int base(int(floor(m)));
+    double prop0 = (base + 1.0) - m, prop1 = m - base;
+    QColor c0(colours[base]), c1(colours[base+1]);
+    r = c0.redF() * prop0 + c1.redF() * prop1;
+    g = c0.greenF() * prop0 + c1.greenF() * prop1;
+    b = c0.blueF() * prop0 + c1.blueF() * prop1;
+}
+
 ColourMapper::ColourMapper(int map, double min, double max) :
-    QObject(),
     m_map(map),
     m_min(min),
     m_max(max)
@@ -47,25 +86,25 @@
 QString
 ColourMapper::getColourMapName(int n)
 {
-    if (n >= getColourMapCount()) return tr("<unknown>");
+    if (n >= getColourMapCount()) return QObject::tr("<unknown>");
     StandardMap map = (StandardMap)n;
 
     switch (map) {
-    case DefaultColours:   return tr("Default");
-    case WhiteOnBlack:     return tr("White on Black");
-    case BlackOnWhite:     return tr("Black on White");
-    case RedOnBlue:        return tr("Red on Blue");
-    case YellowOnBlack:    return tr("Yellow on Black");
-    case BlueOnBlack:      return tr("Blue on Black");
-    case Sunset:           return tr("Sunset");
-    case FruitSalad:       return tr("Fruit Salad");
-    case Banded:           return tr("Banded");
-    case Highlight:        return tr("Highlight");
-    case Printer:          return tr("Printer");
-    case HighGain:         return tr("High Gain");
+    case Green:            return QObject::tr("Green");
+    case WhiteOnBlack:     return QObject::tr("White on Black");
+    case BlackOnWhite:     return QObject::tr("Black on White");
+    case Cherry:           return QObject::tr("Cherry");
+    case Wasp:             return QObject::tr("Wasp");
+    case Ice:              return QObject::tr("Ice");
+    case Sunset:           return QObject::tr("Sunset");
+    case FruitSalad:       return QObject::tr("Fruit Salad");
+    case Banded:           return QObject::tr("Banded");
+    case Highlight:        return QObject::tr("Highlight");
+    case Printer:          return QObject::tr("Printer");
+    case HighGain:         return QObject::tr("High Gain");
     }
 
-    return tr("<unknown>");
+    return QObject::tr("<unknown>");
 }
 
 QColor
@@ -85,7 +124,7 @@
 
     switch (map) {
 
-    case DefaultColours:
+    case Green:
         h = blue - norm * 2.0 * pieslice;
         s = 0.5f + norm/2.0;
         v = norm;
@@ -101,30 +140,17 @@
         hsv = false;
         break;
 
-    case RedOnBlue:
-        h = blue - pieslice/4.0 + norm * (pieslice + pieslice/4.0);
-        s = 1.0;
-        v = norm;
+    case Cherry:
+        hsv = false;
+        mapDiscrete(norm, cherry, r, g, b);
         break;
 
-    case YellowOnBlack:
+    case Wasp:
         h = 0.15;
         s = 1.0;
         v = norm;
         break;
 
-    case BlueOnBlack:
-        h = blue;
-        s = 1.0;
-        v = norm * 2.0;
-        if (v > 1.0) {
-            v = 1.0;
-            s = 1.0 - (sqrt(norm) - 0.707) * 3.413;
-            if (s < 0.0) s = 0.0;
-            if (s > 1.0) s = 1.0;
-        }
-        break;
-
     case Sunset:
         r = (norm - 0.24) * 2.38;
         if (r > 1.0) r = 1.0;
@@ -207,6 +233,10 @@
         hsv = false;
 */
         break;
+
+    case Ice:
+        hsv = false;
+        mapDiscrete(norm, ice, r, g, b);
     }
 
     if (hsv) {
@@ -224,7 +254,7 @@
 
     switch (map) {
 
-    case DefaultColours:
+    case Green:
         return QColor(255, 150, 50);
 
     case WhiteOnBlack:
@@ -233,13 +263,13 @@
     case BlackOnWhite:
         return Qt::darkGreen;
 
-    case RedOnBlue:
+    case Cherry:
         return Qt::green;
 
-    case YellowOnBlack:
+    case Wasp:
         return QColor::fromHsv(240, 255, 255);
 
-    case BlueOnBlack:
+    case Ice:
         return Qt::red;
 
     case Sunset:
@@ -277,12 +307,12 @@
     case HighGain:
         return true;
 
-    case DefaultColours:
+    case Green:
     case Sunset:
     case WhiteOnBlack:
-    case RedOnBlue:
-    case YellowOnBlack:
-    case BlueOnBlack:
+    case Cherry:
+    case Wasp:
+    case Ice:
     case FruitSalad:
     case Banded:
     case Highlight:
@@ -292,4 +322,29 @@
     }
 }
 
+QPixmap
+ColourMapper::getExamplePixmap(QSize size) const
+{
+    QPixmap pmap(size);
+    pmap.fill(Qt::white);
+    QPainter paint(&pmap);
 
+    int w = size.width(), h = size.height();
+    
+    int margin = 2;
+    if (w < 4 || h < 4) margin = 0;
+    else if (w < 8 || h < 8) margin = 1;
+
+    int n = w - margin*2;
+    
+    for (int x = 0; x < n; ++x) {
+        double value = m_min + ((m_max - m_min) * x) / (n-1);
+        QColor colour(map(value));
+        paint.setPen(colour);
+        paint.drawLine(x + margin, margin, x + margin, h - margin);
+    }
+    
+    return pmap;
+}
+
+
--- a/layer/ColourMapper.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/ColourMapper.h	Fri Jan 13 10:29:50 2017 +0000
@@ -13,33 +13,34 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _COLOUR_MAPPER_H_
-#define _COLOUR_MAPPER_H_
+#ifndef SV_COLOUR_MAPPER_H
+#define SV_COLOUR_MAPPER_H
 
 #include <QObject>
 #include <QColor>
 #include <QString>
+#include <QPixmap>
 
 /**
  * A class for mapping intensity values onto various colour maps.
  */
-
-class ColourMapper : public QObject
+class ColourMapper
 {
-    Q_OBJECT
-
 public:
     ColourMapper(int map, double minValue, double maxValue);
-    virtual ~ColourMapper();
+    ~ColourMapper();
+
+    ColourMapper(const ColourMapper &) = default;
+    ColourMapper &operator=(const ColourMapper &) = default;
 
     enum StandardMap {
-        DefaultColours,
+        Green,
         Sunset,
         WhiteOnBlack,
         BlackOnWhite,
-        RedOnBlue,
-        YellowOnBlack,
-        BlueOnBlack,
+        Cherry,
+        Wasp,
+        Ice,
         FruitSalad,
         Banded,
         Highlight,
@@ -59,6 +60,8 @@
     QColor getContrastingColour() const; // for cursors etc
     bool hasLightBackground() const;
 
+    QPixmap getExamplePixmap(QSize size) const;
+    
 protected:
     int m_map;
     double m_min;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/ColourScale.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -0,0 +1,160 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2016 Chris Cannam and QMUL.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "ColourScale.h"
+
+#include "base/AudioLevel.h"
+#include "base/LogRange.h"
+
+#include <cmath>
+#include <iostream>
+
+using namespace std;
+
+int ColourScale::m_maxPixel = 255;
+
+ColourScale::ColourScale(Parameters parameters) :
+    m_params(parameters),
+    m_mapper(m_params.colourMap, 1.f, double(m_maxPixel))
+{
+    if (m_params.minValue >= m_params.maxValue) {
+        cerr << "ERROR: ColourScale::ColourScale: minValue = "
+             << m_params.minValue << ", maxValue = " << m_params.maxValue << endl;
+	throw std::logic_error("maxValue must be greater than minValue");
+    }
+
+    m_mappedMin = m_params.minValue;
+    m_mappedMax = m_params.maxValue;
+
+    if (m_mappedMin < m_params.threshold) {
+        m_mappedMin = m_params.threshold;
+    }
+    
+    if (m_params.scaleType == ColourScaleType::Log) {
+
+	LogRange::mapRange(m_mappedMin, m_mappedMax);
+	
+    } else if (m_params.scaleType == ColourScaleType::PlusMinusOne) {
+	
+	m_mappedMin = -1.0;
+	m_mappedMax =  1.0;
+
+    } else if (m_params.scaleType == ColourScaleType::Absolute) {
+
+	m_mappedMin = fabs(m_mappedMin);
+	m_mappedMax = fabs(m_mappedMax);
+	if (m_mappedMin >= m_mappedMax) {
+	    std::swap(m_mappedMin, m_mappedMax);
+	}
+    }
+
+    if (m_mappedMin >= m_mappedMax) {
+        cerr << "ERROR: ColourScale::ColourScale: minValue = " << m_params.minValue
+             << ", maxValue = " << m_params.maxValue
+             << ", threshold = " << m_params.threshold
+             << ", scale = " << int(m_params.scaleType)
+             << " resulting in mapped minValue = " << m_mappedMin
+             << ", mapped maxValue = " << m_mappedMax << endl;
+	throw std::logic_error("maxValue must be greater than minValue [after mapping]");
+    }
+}
+
+ColourScale::~ColourScale()
+{
+}
+
+ColourScaleType
+ColourScale::getScale() const
+{
+    return m_params.scaleType;
+}
+
+int
+ColourScale::getPixel(double value) const
+{
+    double maxPixF = m_maxPixel;
+
+    if (m_params.scaleType == ColourScaleType::Phase) {
+	double half = (maxPixF - 1.f) / 2.f;
+        int pixel = 1 + int((value * half) / M_PI + half);
+//        cerr << "phase = " << value << " pixel = " << pixel << endl;
+        return pixel;
+    }
+    
+    value *= m_params.gain;
+    
+    if (value < m_params.threshold) return 0;
+
+    double mapped = value;
+
+    if (m_params.scaleType == ColourScaleType::Log) {
+	mapped = LogRange::map(value);
+    } else if (m_params.scaleType == ColourScaleType::PlusMinusOne) {
+	if (mapped < -1.f) mapped = -1.f;
+	if (mapped > 1.f) mapped = 1.f;
+    } else if (m_params.scaleType == ColourScaleType::Absolute) {
+	if (mapped < 0.f) mapped = -mapped;
+    }
+
+    mapped *= m_params.multiple;
+    
+    if (mapped < m_mappedMin) {
+	mapped = m_mappedMin;
+    }
+    if (mapped > m_mappedMax) {
+	mapped = m_mappedMax;
+    }
+
+    double proportion = (mapped - m_mappedMin) / (m_mappedMax - m_mappedMin);
+
+    int pixel = 0;
+
+    if (m_params.scaleType == ColourScaleType::Meter) {
+	pixel = AudioLevel::multiplier_to_preview(proportion, m_maxPixel-1) + 1;
+    } else {
+	pixel = int(proportion * maxPixF) + 1;
+    }
+
+    if (pixel < 0) {
+	pixel = 0;
+    }
+    if (pixel > m_maxPixel) {
+	pixel = m_maxPixel;
+    }
+    return pixel;
+}
+
+QColor
+ColourScale::getColourForPixel(int pixel, int rotation) const
+{
+    if (pixel < 0) {
+	pixel = 0;
+    }
+    if (pixel > m_maxPixel) {
+	pixel = m_maxPixel;
+    }
+    if (pixel == 0) {
+	if (m_mapper.hasLightBackground()) {
+	    return Qt::white;
+	} else {
+	    return Qt::black;
+	}
+    } else {
+	int target = int(pixel) + rotation;
+	while (target < 1) target += m_maxPixel;
+	while (target > m_maxPixel) target -= m_maxPixel;
+	return m_mapper.map(double(target));
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/ColourScale.h	Fri Jan 13 10:29:50 2017 +0000
@@ -0,0 +1,120 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2016 Chris Cannam and QMUL.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef COLOUR_SCALE_H
+#define COLOUR_SCALE_H
+
+#include "ColourMapper.h"
+
+enum class ColourScaleType {
+    Linear,
+    Meter,
+    Log,
+    Phase,
+    PlusMinusOne,
+    Absolute
+};
+
+/**
+ * Map values within a range onto a set of colours, with a given
+ * distribution (linear, log etc) and optional colourmap rotation.
+ */
+class ColourScale
+{
+public:
+    struct Parameters {
+	Parameters() : colourMap(0), scaleType(ColourScaleType::Linear),
+		       minValue(0.0), maxValue(1.0),
+		       threshold(0.0), gain(1.0), multiple(1.0) { }
+
+	/** A colour map index as used by ColourMapper */
+	int colourMap;
+	
+	/** Distribution for the scale */
+	ColourScaleType scaleType;
+	
+	/** Minimum value in source range */
+	double minValue;
+	
+	/** Maximum value in source range. Must be > minValue */
+	double maxValue;
+
+	/** Threshold below which every value is mapped to background
+	    pixel 0 */
+	double threshold;
+
+	/** Gain to apply before thresholding, mapping, and clamping */
+	double gain;
+
+        /** Multiple to apply after thresholding and mapping. In most
+         *  cases the gain parameter is the one you want instead of
+         *  this, but this can be used for example with Log scale to
+         *  produce the log of some power of the original value,
+         *  e.g. multiple = 2 gives log(x^2). */
+        double multiple;
+    };
+    
+    /**
+     * Create a ColourScale with the given parameters.
+     *
+     * Note that some parameters may be ignored for some scale
+     * distribution settings. For example, min and max are ignored for
+     * PlusMinusOneScale and PhaseColourScale and threshold and gain
+     * are ignored for PhaseColourScale.
+     */
+    ColourScale(Parameters parameters);
+    ~ColourScale();
+
+    ColourScale(const ColourScale &) = default;
+    ColourScale &operator=(const ColourScale &) = default;
+
+    /**
+     * Return the general type of scale this is.
+     */
+    ColourScaleType getScale() const;
+    
+    /**
+     * Return a pixel number (in the range 0-255 inclusive)
+     * corresponding to the given value.  The pixel 0 is used only for
+     * values below the threshold supplied in the constructor. All
+     * other values are mapped onto the range 1-255.
+     */
+    int getPixel(double value) const;
+
+    /**
+     * Return the colour for the given pixel number (which must be in
+     * the range 0-255). The pixel 0 is always the background
+     * colour. Other pixels are mapped taking into account the given
+     * colourmap rotation (which is also a value in the range 0-255).
+     */
+    QColor getColourForPixel(int pixel, int rotation) const;
+    
+    /**
+     * Return the colour corresponding to the given value. This is
+     * equivalent to getColourForPixel(getPixel(value), rotation).
+     */
+    QColor getColour(double value, int rotation) const {
+	return getColourForPixel(getPixel(value), rotation);
+    }
+
+private:
+    Parameters m_params;
+    ColourMapper m_mapper;
+    double m_mappedMin;
+    double m_mappedMax;
+    static int m_maxPixel;
+};
+
+#endif
--- a/layer/ColourScaleLayer.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/ColourScaleLayer.h	Fri Jan 13 10:29:50 2017 +0000
@@ -19,11 +19,20 @@
 #include <QString>
 #include <QColor>
 
+class LayerGeometryProvider;
+
+/**
+ * Interface for layers in which a colour scale represents (or can
+ * sometimes represent, depending on the display mode) the sample
+ * value. For example, TimeValueLayer uses colour scale when in
+ * segment mode and so provides this interface for use by the
+ * LogColourScale or LinearColourScale scale renderers.
+ */
 class ColourScaleLayer
 {
 public:
     virtual QString getScaleUnits() const = 0;
-    virtual QColor getColourForValue(View *v, double value) const = 0;
+    virtual QColor getColourForValue(LayerGeometryProvider *v, double value) const = 0;
 };
 
 #endif
--- a/layer/FlexiNoteLayer.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/FlexiNoteLayer.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -22,15 +22,18 @@
 #include "base/Pitch.h"
 #include "base/LogRange.h"
 #include "base/RangeMapper.h"
+
 #include "ColourDatabase.h"
-#include "view/View.h"
-
+#include "LayerGeometryProvider.h"
 #include "PianoScale.h"
 #include "LinearNumericalScale.h"
 #include "LogNumericalScale.h"
+#include "PaintAssistant.h"
 
 #include "data/model/FlexiNoteModel.h"
 
+#include "view/View.h"
+
 #include "widgets/ItemEditDialog.h"
 #include "widgets/TextAbbrev.h"
 
@@ -204,7 +207,7 @@
 }
 
 bool
-FlexiNoteLayer::isLayerScrollable(const View *v) const
+FlexiNoteLayer::isLayerScrollable(const LayerGeometryProvider *v) const
 {
     QPoint discard;
     return !v->shouldIlluminateLocalFeatures(this, discard);
@@ -405,7 +408,7 @@
 }
 
 FlexiNoteModel::PointList
-FlexiNoteLayer::getLocalPoints(View *v, int x) const
+FlexiNoteLayer::getLocalPoints(LayerGeometryProvider *v, int x) const
 {
     if (!m_model) return FlexiNoteModel::PointList();
 
@@ -448,7 +451,7 @@
 }
 
 bool
-FlexiNoteLayer::getPointToDrag(View *v, int x, int y, FlexiNoteModel::Point &p) const
+FlexiNoteLayer::getPointToDrag(LayerGeometryProvider *v, int x, int y, FlexiNoteModel::Point &p) const
 {
     if (!m_model) return false;
 
@@ -476,7 +479,7 @@
 }
 
 bool
-FlexiNoteLayer::getNoteToEdit(View *v, int x, int y, FlexiNoteModel::Point &p) const
+FlexiNoteLayer::getNoteToEdit(LayerGeometryProvider *v, int x, int y, FlexiNoteModel::Point &p) const
 {
     // GF: find the note that is closest to the cursor
     if (!m_model) return false;
@@ -505,7 +508,7 @@
 }
 
 QString
-FlexiNoteLayer::getFeatureDescription(View *v, QPoint &pos) const
+FlexiNoteLayer::getFeatureDescription(LayerGeometryProvider *v, QPoint &pos) const
 {
     int x = pos.x();
 
@@ -593,7 +596,7 @@
 }
 
 bool
-FlexiNoteLayer::snapToFeatureFrame(View *v, sv_frame_t &frame,
+FlexiNoteLayer::snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
                                    int &resolution,
                                    SnapType snap) const
 {
@@ -673,7 +676,7 @@
 }
 
 void
-FlexiNoteLayer::getScaleExtents(View *v, double &min, double &max, bool &log) const
+FlexiNoteLayer::getScaleExtents(LayerGeometryProvider *v, double &min, double &max, bool &log) const
 {
     min = 0.0;
     max = 0.0;
@@ -730,11 +733,11 @@
 }
 
 int
-FlexiNoteLayer::getYForValue(View *v, double val) const
+FlexiNoteLayer::getYForValue(LayerGeometryProvider *v, double val) const
 {
     double min = 0.0, max = 0.0;
     bool logarithmic = false;
-    int h = v->height();
+    int h = v->getPaintHeight();
 
     getScaleExtents(v, min, max, logarithmic);
 
@@ -765,11 +768,11 @@
 }
 
 double
-FlexiNoteLayer::getValueForY(View *v, int y) const
+FlexiNoteLayer::getValueForY(LayerGeometryProvider *v, int y) const
 {
     double min = 0.0, max = 0.0;
     bool logarithmic = false;
-    int h = v->height();
+    int h = v->getPaintHeight();
 
     getScaleExtents(v, min, max, logarithmic);
 
@@ -794,7 +797,7 @@
 }
 
 void
-FlexiNoteLayer::paint(View *v, QPainter &paint, QRect rect) const
+FlexiNoteLayer::paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const
 {
     if (!m_model || !m_model->isOK()) return;
 
@@ -860,41 +863,41 @@
                 !FlexiNoteModel::Point::Comparator()(illuminatePoint, p) &&
                 !FlexiNoteModel::Point::Comparator()(p, illuminatePoint)) {
 
-                paint.drawLine(x, -1, x, v->height() + 1);
-                paint.drawLine(x+w, -1, x+w, v->height() + 1);
+                paint.drawLine(x, -1, x, v->getPaintHeight() + 1);
+                paint.drawLine(x+w, -1, x+w, v->getPaintHeight() + 1);
         
                 paint.setPen(v->getForeground());
                 // paint.setBrush(v->getForeground());
         
                 QString vlabel = QString("freq: %1%2").arg(p.value).arg(m_model->getScaleUnits());
-                // v->drawVisibleText(paint, 
+                // PaintAssistant::drawVisibleText(v, paint, 
                 //                    x - paint.fontMetrics().width(vlabel) - 2,
                 //                    y + paint.fontMetrics().height()/2
                 //                      - paint.fontMetrics().descent(), 
-                //                    vlabel, View::OutlinedText);
-                v->drawVisibleText(paint, 
+                //                    vlabel, PaintAssistant::OutlinedText);
+                PaintAssistant::drawVisibleText(v, paint, 
                                    x,
                                    y - h/2 - 2 - paint.fontMetrics().height()
                                      - paint.fontMetrics().descent(), 
-                                   vlabel, View::OutlinedText);
+                                   vlabel, PaintAssistant::OutlinedText);
 
                 QString hlabel = "dur: " + QString(RealTime::frame2RealTime
                     (p.duration, m_model->getSampleRate()).toText(true).c_str());
-                v->drawVisibleText(paint, 
+                PaintAssistant::drawVisibleText(v, paint, 
                                    x,
                                    y - h/2 - paint.fontMetrics().descent() - 2,
-                                   hlabel, View::OutlinedText);
+                                   hlabel, PaintAssistant::OutlinedText);
 
                 QString llabel = QString("%1").arg(p.label);
-                v->drawVisibleText(paint, 
+                PaintAssistant::drawVisibleText(v, paint, 
                                    x,
                                    y + h + 2 + paint.fontMetrics().descent(),
-                                   llabel, View::OutlinedText);
+                                   llabel, PaintAssistant::OutlinedText);
                 QString nlabel = QString("%1").arg(noteNumber);
-                v->drawVisibleText(paint, 
+                PaintAssistant::drawVisibleText(v, paint, 
                                    x + paint.fontMetrics().averageCharWidth() / 2,
                                    y + h/2 - paint.fontMetrics().descent(),
-                                   nlabel, View::OutlinedText);
+                                   nlabel, PaintAssistant::OutlinedText);
         }
     
         paint.drawRect(x, y - h/2, w, h);
@@ -904,7 +907,7 @@
 }
 
 int
-FlexiNoteLayer::getVerticalScaleWidth(View *v, bool, QPainter &paint) const
+FlexiNoteLayer::getVerticalScaleWidth(LayerGeometryProvider *v, bool, QPainter &paint) const
 {
     if (!m_model || shouldAutoAlign()) {
         return 0;
@@ -918,7 +921,7 @@
 }
 
 void
-FlexiNoteLayer::paintVerticalScale(View *v, bool, QPainter &paint, QRect) const
+FlexiNoteLayer::paintVerticalScale(LayerGeometryProvider *v, bool, QPainter &paint, QRect) const
 {
     if (!m_model || m_model->getPoints().empty()) return;
 
@@ -927,7 +930,7 @@
     bool logarithmic;
 
     int w = getVerticalScaleWidth(v, false, paint);
-    int h = v->height();
+    int h = v->getPaintHeight();
 
     getScaleExtents(v, min, max, logarithmic);
 
@@ -956,7 +959,7 @@
 }
 
 void
-FlexiNoteLayer::drawStart(View *v, QMouseEvent *e)
+FlexiNoteLayer::drawStart(LayerGeometryProvider *v, QMouseEvent *e)
 {
 //    SVDEBUG << "FlexiNoteLayer::drawStart(" << e->x() << "," << e->y() << ")" << endl;
 
@@ -980,7 +983,7 @@
 }
 
 void
-FlexiNoteLayer::drawDrag(View *v, QMouseEvent *e)
+FlexiNoteLayer::drawDrag(LayerGeometryProvider *v, QMouseEvent *e)
 {
 //    SVDEBUG << "FlexiNoteLayer::drawDrag(" << e->x() << "," << e->y() << ")" << endl;
 
@@ -1009,7 +1012,7 @@
 }
 
 void
-FlexiNoteLayer::drawEnd(View *, QMouseEvent *)
+FlexiNoteLayer::drawEnd(LayerGeometryProvider *, QMouseEvent *)
 {
 //    SVDEBUG << "FlexiNoteLayer::drawEnd(" << e->x() << "," << e->y() << ")" << endl;
     if (!m_model || !m_editing) return;
@@ -1019,7 +1022,7 @@
 }
 
 void
-FlexiNoteLayer::eraseStart(View *v, QMouseEvent *e)
+FlexiNoteLayer::eraseStart(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_model) return;
 
@@ -1034,12 +1037,12 @@
 }
 
 void
-FlexiNoteLayer::eraseDrag(View *, QMouseEvent *)
+FlexiNoteLayer::eraseDrag(LayerGeometryProvider *, QMouseEvent *)
 {
 }
 
 void
-FlexiNoteLayer::eraseEnd(View *v, QMouseEvent *e)
+FlexiNoteLayer::eraseEnd(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_model || !m_editing) return;
 
@@ -1059,7 +1062,7 @@
 }
 
 void
-FlexiNoteLayer::editStart(View *v, QMouseEvent *e)
+FlexiNoteLayer::editStart(LayerGeometryProvider *v, QMouseEvent *e)
 {
 //    SVDEBUG << "FlexiNoteLayer::editStart(" << e->x() << "," << e->y() << ")" << endl;
     std::cerr << "FlexiNoteLayer::editStart(" << e->x() << "," << e->y() << ")" << std::endl;
@@ -1110,7 +1113,7 @@
 }
 
 void
-FlexiNoteLayer::editDrag(View *v, QMouseEvent *e)
+FlexiNoteLayer::editDrag(LayerGeometryProvider *v, QMouseEvent *e)
 {
 //    SVDEBUG << "FlexiNoteLayer::editDrag(" << e->x() << "," << e->y() << ")" << endl;
     std::cerr << "FlexiNoteLayer::editDrag(" << e->x() << "," << e->y() << ")" << std::endl;
@@ -1190,7 +1193,7 @@
 }
 
 void
-FlexiNoteLayer::editEnd(View *v, QMouseEvent *e)
+FlexiNoteLayer::editEnd(LayerGeometryProvider *v, QMouseEvent *e)
 {
 //    SVDEBUG << "FlexiNoteLayer::editEnd(" << e->x() << "," << e->y() << ")" << endl;
     std::cerr << "FlexiNoteLayer::editEnd(" << e->x() << "," << e->y() << ")" << std::endl;
@@ -1229,7 +1232,7 @@
 }
 
 void
-FlexiNoteLayer::splitStart(View *v, QMouseEvent *e)
+FlexiNoteLayer::splitStart(LayerGeometryProvider *v, QMouseEvent *e)
 {
     // GF: note splitting starts (!! remove printing soon)
     std::cerr << "splitStart (n.b. editStart will be called later, if the user drags the mouse)" << std::endl;
@@ -1252,7 +1255,7 @@
 }
 
 void
-FlexiNoteLayer::splitEnd(View *v, QMouseEvent *e)
+FlexiNoteLayer::splitEnd(LayerGeometryProvider *v, QMouseEvent *e)
 {
     // GF: note splitting ends. (!! remove printing soon)
     std::cerr << "splitEnd" << std::endl;
@@ -1271,13 +1274,13 @@
 }
 
 void
-FlexiNoteLayer::splitNotesAt(View *v, sv_frame_t frame)
+FlexiNoteLayer::splitNotesAt(LayerGeometryProvider *v, sv_frame_t frame)
 {
     splitNotesAt(v, frame, 0);
 }
 
 void
-FlexiNoteLayer::splitNotesAt(View *v, sv_frame_t frame, QMouseEvent *e)
+FlexiNoteLayer::splitNotesAt(LayerGeometryProvider *v, sv_frame_t frame, QMouseEvent *e)
 {
     FlexiNoteModel::PointList onPoints = m_model->getPoints(frame);
     if (onPoints.empty()) return;
@@ -1317,7 +1320,7 @@
 }
 
 void
-FlexiNoteLayer::addNote(View *v, QMouseEvent *e)
+FlexiNoteLayer::addNote(LayerGeometryProvider *v, QMouseEvent *e)
 {
     std::cerr << "addNote" << std::endl;
     if (!m_model) return;
@@ -1356,14 +1359,14 @@
 }
 
 SparseTimeValueModel *
-FlexiNoteLayer::getAssociatedPitchModel(View *v) const
+FlexiNoteLayer::getAssociatedPitchModel(LayerGeometryProvider *v) const
 {
     // Better than we used to do, but still not very satisfactory
 
 //    cerr << "FlexiNoteLayer::getAssociatedPitchModel()" << endl;
 
-    for (int i = 0; i < v->getLayerCount(); ++i) {
-        Layer *layer = v->getLayer(i);
+    for (int i = 0; i < v->getView()->getLayerCount(); ++i) {
+        Layer *layer = v->getView()->getLayer(i);
         if (layer &&
             layer->getLayerPresentationName() != "candidate") {
 //            cerr << "FlexiNoteLayer::getAssociatedPitchModel: looks like our layer is " << layer << endl;
@@ -1381,7 +1384,7 @@
 }
 
 void
-FlexiNoteLayer::snapSelectedNotesToPitchTrack(View *v, Selection s)
+FlexiNoteLayer::snapSelectedNotesToPitchTrack(LayerGeometryProvider *v, Selection s)
 {
     if (!m_model) return;
 
@@ -1419,7 +1422,7 @@
 }
 
 void
-FlexiNoteLayer::mergeNotes(View *v, Selection s, bool inclusive)
+FlexiNoteLayer::mergeNotes(LayerGeometryProvider *v, Selection s, bool inclusive)
 {
     FlexiNoteModel::PointList points =
         m_model->getPoints(s.getStartFrame(), s.getEndFrame());
@@ -1462,8 +1465,7 @@
 }
 
 bool
-FlexiNoteLayer::updateNoteValueFromPitchCurve(View *v,
-                                              FlexiNoteModel::Point &note) const
+FlexiNoteLayer::updateNoteValueFromPitchCurve(LayerGeometryProvider *v, FlexiNoteModel::Point &note) const
 {
     SparseTimeValueModel *model = getAssociatedPitchModel(v);
     if (!model) return false;
@@ -1507,13 +1509,13 @@
 }
 
 void 
-FlexiNoteLayer::mouseMoveEvent(View *v, QMouseEvent *e)
+FlexiNoteLayer::mouseMoveEvent(LayerGeometryProvider *v, QMouseEvent *e)
 {
     // GF: context sensitive cursors
-    // v->setCursor(Qt::ArrowCursor);
+    // v->getView()->setCursor(Qt::ArrowCursor);
     FlexiNoteModel::Point note(0);
     if (!getNoteToEdit(v, e->x(), e->y(), note)) { 
-        // v->setCursor(Qt::UpArrowCursor);
+        // v->getView()->setCursor(Qt::UpArrowCursor);
         return; 
     }
 
@@ -1524,28 +1526,28 @@
                              closeToTop, closeToBottom);
     
     if (closeToLeft) {
-        v->setCursor(Qt::SizeHorCursor);
+        v->getView()->setCursor(Qt::SizeHorCursor);
         m_editMode = LeftBoundary;
         cerr << "edit mode -> LeftBoundary" << endl;
     } else if (closeToRight) {
-        v->setCursor(Qt::SizeHorCursor);
+        v->getView()->setCursor(Qt::SizeHorCursor);
         m_editMode = RightBoundary;
         cerr << "edit mode -> RightBoundary" << endl;
     } else if (closeToTop) {
-        v->setCursor(Qt::CrossCursor);
+        v->getView()->setCursor(Qt::CrossCursor);
         m_editMode = DragNote;
         cerr << "edit mode -> DragNote" << endl;
     } else if (closeToBottom) {
-        v->setCursor(Qt::UpArrowCursor);
+        v->getView()->setCursor(Qt::UpArrowCursor);
         m_editMode = SplitNote;
         cerr << "edit mode -> SplitNote" << endl;
     } else {
-        v->setCursor(Qt::ArrowCursor);
+        v->getView()->setCursor(Qt::ArrowCursor);
     }
 }
 
 void
-FlexiNoteLayer::getRelativeMousePosition(View *v, FlexiNoteModel::Point &note, int x, int y, bool &closeToLeft, bool &closeToRight, bool &closeToTop, bool &closeToBottom) const
+FlexiNoteLayer::getRelativeMousePosition(LayerGeometryProvider *v, FlexiNoteModel::Point &note, int x, int y, bool &closeToLeft, bool &closeToRight, bool &closeToTop, bool &closeToBottom) const
 {
     // GF: TODO: consoloidate the tolerance values
     if (!m_model) return;
@@ -1574,7 +1576,7 @@
 
 
 bool
-FlexiNoteLayer::editOpen(View *v, QMouseEvent *e)
+FlexiNoteLayer::editOpen(LayerGeometryProvider *v, QMouseEvent *e)
 {
     std::cerr << "Opening note editor dialog" << std::endl;
     if (!m_model) return false;
@@ -1728,7 +1730,7 @@
 }
 
 void
-FlexiNoteLayer::copy(View *v, Selection s, Clipboard &to)
+FlexiNoteLayer::copy(LayerGeometryProvider *v, Selection s, Clipboard &to)
 {
     if (!m_model) return;
 
@@ -1746,7 +1748,7 @@
 }
 
 bool
-FlexiNoteLayer::paste(View *v, const Clipboard &from, sv_frame_t /*frameOffset */, bool /* interactive */)
+FlexiNoteLayer::paste(LayerGeometryProvider *v, const Clipboard &from, sv_frame_t /*frameOffset */, bool /* interactive */)
 {
     if (!m_model) return false;
 
@@ -1757,7 +1759,7 @@
     if (clipboardHasDifferentAlignment(v, from)) {
 
         QMessageBox::StandardButton button =
-            QMessageBox::question(v, tr("Re-align pasted items?"),
+            QMessageBox::question(v->getView(), tr("Re-align pasted items?"),
                                   tr("The items you are pasting came from a layer with different source material from this one.  Do you want to re-align them in time, to match the source material for this layer?"),
                                   QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel,
                                   QMessageBox::Yes);
@@ -1894,7 +1896,7 @@
 }
 
 void
-FlexiNoteLayer::setVerticalRangeToNoteRange(View *v)
+FlexiNoteLayer::setVerticalRangeToNoteRange(LayerGeometryProvider *v)
 {
     double minf = std::numeric_limits<double>::max();
     double maxf = 0;
@@ -1910,7 +1912,7 @@
     std::cerr << "min frequency:" << minf << ", max frequency: " << maxf << std::endl;
     
     if (hasNotes) {
-        v->getLayer(1)->setDisplayExtents(minf*0.66,maxf*1.5); 
+        v->getView()->getLayer(1)->setDisplayExtents(minf*0.66,maxf*1.5); 
         // MM: this is a hack because we rely on 
         // * this layer being automatically aligned to layer 1
         // * layer one is a log frequency layer.
--- a/layer/FlexiNoteLayer.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/FlexiNoteLayer.h	Fri Jan 13 10:29:50 2017 +0000
@@ -38,50 +38,50 @@
 public:
     FlexiNoteLayer();
 
-    virtual void paint(View *v, QPainter &paint, QRect rect) const;
+    virtual void paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
 
-    virtual int getVerticalScaleWidth(View *v, bool, QPainter &) const;
-    virtual void paintVerticalScale(View *v, bool, QPainter &paint, QRect rect) const;
+    virtual int getVerticalScaleWidth(LayerGeometryProvider *v, bool, QPainter &) const;
+    virtual void paintVerticalScale(LayerGeometryProvider *v, bool, QPainter &paint, QRect rect) const;
 
-    virtual QString getFeatureDescription(View *v, QPoint &) const;
+    virtual QString getFeatureDescription(LayerGeometryProvider *v, QPoint &) const;
 
-    virtual bool snapToFeatureFrame(View *v, sv_frame_t &frame,
+    virtual bool snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
                     int &resolution,
                     SnapType snap) const;
 
-    virtual void drawStart(View *v, QMouseEvent *);
-    virtual void drawDrag(View *v, QMouseEvent *);
-    virtual void drawEnd(View *v, QMouseEvent *);
+    virtual void drawStart(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void drawDrag(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void drawEnd(LayerGeometryProvider *v, QMouseEvent *);
 
-    virtual void eraseStart(View *v, QMouseEvent *);
-    virtual void eraseDrag(View *v, QMouseEvent *);
-    virtual void eraseEnd(View *v, QMouseEvent *);
+    virtual void eraseStart(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void eraseDrag(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void eraseEnd(LayerGeometryProvider *v, QMouseEvent *);
 
-    virtual void editStart(View *v, QMouseEvent *);
-    virtual void editDrag(View *v, QMouseEvent *);
-    virtual void editEnd(View *v, QMouseEvent *);
+    virtual void editStart(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void editDrag(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void editEnd(LayerGeometryProvider *v, QMouseEvent *);
 
-    virtual void splitStart(View *v, QMouseEvent *);
-    virtual void splitEnd(View *v, QMouseEvent *);
+    virtual void splitStart(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void splitEnd(LayerGeometryProvider *v, QMouseEvent *);
     
-    virtual void addNote(View *v, QMouseEvent *e);
+    virtual void addNote(LayerGeometryProvider *v, QMouseEvent *e);
 
-    virtual void mouseMoveEvent(View *v, QMouseEvent *);
+    virtual void mouseMoveEvent(LayerGeometryProvider *v, QMouseEvent *);
 
-    virtual bool editOpen(View *v, QMouseEvent *);
+    virtual bool editOpen(LayerGeometryProvider *v, QMouseEvent *);
 
     virtual void moveSelection(Selection s, sv_frame_t newStartFrame);
     virtual void resizeSelection(Selection s, Selection newSize);
     virtual void deleteSelection(Selection s);
     virtual void deleteSelectionInclusive(Selection s);
 
-    virtual void copy(View *v, Selection s, Clipboard &to);
-    virtual bool paste(View *v, const Clipboard &from, sv_frame_t frameOffset,
+    virtual void copy(LayerGeometryProvider *v, Selection s, Clipboard &to);
+    virtual bool paste(LayerGeometryProvider *v, const Clipboard &from, sv_frame_t frameOffset,
                        bool interactive);
 
-    void splitNotesAt(View *v, sv_frame_t frame);
-    void snapSelectedNotesToPitchTrack(View *v, Selection s);
-    void mergeNotes(View *v, Selection s, bool inclusive);
+    void splitNotesAt(LayerGeometryProvider *v, sv_frame_t frame);
+    void snapSelectedNotesToPitchTrack(LayerGeometryProvider *v, Selection s);
+    void mergeNotes(LayerGeometryProvider *v, Selection s, bool inclusive);
 
     virtual const Model *getModel() const { return m_model; }
     void setModel(FlexiNoteModel *model);
@@ -116,11 +116,11 @@
     void setVerticalScale(VerticalScale scale);
     VerticalScale getVerticalScale() const { return m_verticalScale; }
 
-    virtual bool isLayerScrollable(const View *v) const;
+    virtual bool isLayerScrollable(const LayerGeometryProvider *v) const;
 
     virtual bool isLayerEditable() const { return true; }
 
-    virtual int getCompletion(View *) const { return m_model->getCompletion(); }
+    virtual int getCompletion(LayerGeometryProvider *) const { return m_model->getCompletion(); }
 
     virtual bool getValueExtents(double &min, double &max,
                                  bool &log, QString &unit) const;
@@ -156,11 +156,11 @@
 
     void setProperties(const QXmlAttributes &attributes);
     
-    void setVerticalRangeToNoteRange(View *v);
+    void setVerticalRangeToNoteRange(LayerGeometryProvider *v);
 
     /// VerticalScaleLayer methods
-    virtual int getYForValue(View *v, double value) const;
-    virtual double getValueForY(View *v, int y) const;
+    virtual int getYForValue(LayerGeometryProvider *v, double value) const;
+    virtual double getValueForY(LayerGeometryProvider *v, int y) const;
     virtual QString getScaleUnits() const;
 
 signals:
@@ -168,23 +168,19 @@
     void materialiseReAnalysis();
     
 protected:
-    void getScaleExtents(View *, double &min, double &max, bool &log) const;
+    void getScaleExtents(LayerGeometryProvider *, double &min, double &max, bool &log) const;
     bool shouldConvertMIDIToHz() const;
 
     virtual int getDefaultColourHint(bool dark, bool &impose);
 
-    FlexiNoteModel::PointList getLocalPoints(View *v, int) const;
+    FlexiNoteModel::PointList getLocalPoints(LayerGeometryProvider *v, int) const;
 
-    bool getPointToDrag(View *v, int x, int y, FlexiNoteModel::Point &) const;
-    bool getNoteToEdit(View *v, int x, int y, FlexiNoteModel::Point &) const;
-    void getRelativeMousePosition(View *v, FlexiNoteModel::Point &note,
-                                  int x, int y,
-                                  bool &closeToLeft, bool &closeToRight,
-                                  bool &closeToTop, bool &closeToBottom) const;
-    SparseTimeValueModel *getAssociatedPitchModel(View *v) const;
-    bool updateNoteValueFromPitchCurve(View *v,
-                                       FlexiNoteModel::Point &note) const;
-    void splitNotesAt(View *v, sv_frame_t frame, QMouseEvent *e);
+    bool getPointToDrag(LayerGeometryProvider *v, int x, int y, FlexiNoteModel::Point &) const;
+    bool getNoteToEdit(LayerGeometryProvider *v, int x, int y, FlexiNoteModel::Point &) const;
+    void getRelativeMousePosition(LayerGeometryProvider *v, FlexiNoteModel::Point &note, int x, int y, bool &closeToLeft, bool &closeToRight, bool &closeToTop, bool &closeToBottom) const;
+    SparseTimeValueModel *getAssociatedPitchModel(LayerGeometryProvider *v) const;
+    bool updateNoteValueFromPitchCurve(LayerGeometryProvider *v, FlexiNoteModel::Point &note) const;
+    void splitNotesAt(LayerGeometryProvider *v, sv_frame_t frame, QMouseEvent *e);
 
     FlexiNoteModel *m_model;
     bool m_editing;
--- a/layer/ImageLayer.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/ImageLayer.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -116,14 +116,14 @@
 }
 
 bool
-ImageLayer::isLayerScrollable(const View *) const
+ImageLayer::isLayerScrollable(const LayerGeometryProvider *) const
 {
     return true;
 }
 
 
 ImageModel::PointList
-ImageLayer::getLocalPoints(View *v, int x, int ) const
+ImageLayer::getLocalPoints(LayerGeometryProvider *v, int x, int ) const
 {
     if (!m_model) return ImageModel::PointList();
 
@@ -169,7 +169,7 @@
 }
 
 QString
-ImageLayer::getFeatureDescription(View *v, QPoint &pos) const
+ImageLayer::getFeatureDescription(LayerGeometryProvider *v, QPoint &pos) const
 {
     int x = pos.x();
 
@@ -208,7 +208,7 @@
 //!!! too much overlap with TimeValueLayer/TimeInstantLayer/TextLayer
 
 bool
-ImageLayer::snapToFeatureFrame(View *v, sv_frame_t &frame,
+ImageLayer::snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
 			      int &resolution,
 			      SnapType snap) const
 {
@@ -280,7 +280,7 @@
 }
 
 void
-ImageLayer::paint(View *v, QPainter &paint, QRect rect) const
+ImageLayer::paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const
 {
     if (!m_model || !m_model->isOK()) return;
 
@@ -290,7 +290,7 @@
 //    Profiler profiler("ImageLayer::paint", true);
 
 //    int x0 = rect.left(), x1 = rect.right();
-    int x0 = 0, x1 = v->width();
+    int x0 = 0, x1 = v->getPaintWidth();
 
     sv_frame_t frame0 = v->getFrameForX(x0);
     sv_frame_t frame1 = v->getFrameForX(x1);
@@ -299,7 +299,7 @@
     if (points.empty()) return;
 
     paint.save();
-    paint.setClipRect(rect.x(), 0, rect.width(), v->height());
+    paint.setClipRect(rect.x(), 0, rect.width(), v->getPaintHeight());
 
     QColor penColour;
     penColour = v->getForeground();
@@ -338,7 +338,7 @@
 }
 
 void
-ImageLayer::drawImage(View *v, QPainter &paint, const ImageModel::Point &p,
+ImageLayer::drawImage(LayerGeometryProvider *v, QPainter &paint, const ImageModel::Point &p,
                       int x, int nx) const
 {
     QString label = p.label;
@@ -358,12 +358,12 @@
     int bottomMargin = 10;
     int spacing = 5;
 
-    if (v->height() < 100) {
+    if (v->getPaintHeight() < 100) {
         topMargin = 5;
         bottomMargin = 5;
     }
 
-    int maxBoxHeight = v->height() - topMargin - bottomMargin;
+    int maxBoxHeight = v->getPaintHeight() - topMargin - bottomMargin;
 
     int availableWidth = nx - x - 3;
     if (availableWidth < 20) availableWidth = 20;
@@ -372,7 +372,7 @@
 
     if (label != "") {
 
-        int likelyHeight = v->height() / 4;
+        int likelyHeight = v->getPaintHeight() / 4;
 
         int likelyWidth = // available height times image aspect
             ((maxBoxHeight - likelyHeight) * imageSize.width())
@@ -435,10 +435,10 @@
         division += paint.fontMetrics().height();
     }                
 
-    bottomMargin = v->height() - topMargin - boxHeight;
-    if (bottomMargin > topMargin + v->height()/7) {
-        topMargin += v->height()/8;
-        bottomMargin -= v->height()/8;
+    bottomMargin = v->getPaintHeight() - topMargin - boxHeight;
+    if (bottomMargin > topMargin + v->getPaintHeight()/7) {
+        topMargin += v->getPaintHeight()/8;
+        bottomMargin -= v->getPaintHeight()/8;
     }
 
     paint.drawRect(x - 1,
@@ -480,7 +480,7 @@
 }
 
 void
-ImageLayer::setLayerDormant(const View *v, bool dormant)
+ImageLayer::setLayerDormant(const LayerGeometryProvider *v, bool dormant)
 {
     if (dormant) {
         // Delete the images named in the view's scaled map from the
@@ -517,7 +517,7 @@
 }
 
 QImage 
-ImageLayer::getImage(View *v, QString name, QSize maxSize) const
+ImageLayer::getImage(LayerGeometryProvider *v, QString name, QSize maxSize) const
 {
 //    SVDEBUG << "ImageLayer::getImage(" << v << ", " << name << ", ("
 //              << maxSize.width() << "x" << maxSize.height() << "))" << endl;
@@ -554,7 +554,7 @@
 }
 
 void
-ImageLayer::drawStart(View *v, QMouseEvent *e)
+ImageLayer::drawStart(LayerGeometryProvider *v, QMouseEvent *e)
 {
 //    SVDEBUG << "ImageLayer::drawStart(" << e->x() << "," << e->y() << ")" << endl;
 
@@ -578,7 +578,7 @@
 }
 
 void
-ImageLayer::drawDrag(View *v, QMouseEvent *e)
+ImageLayer::drawDrag(LayerGeometryProvider *v, QMouseEvent *e)
 {
 //    SVDEBUG << "ImageLayer::drawDrag(" << e->x() << "," << e->y() << ")" << endl;
 
@@ -594,7 +594,7 @@
 }
 
 void
-ImageLayer::drawEnd(View *, QMouseEvent *)
+ImageLayer::drawEnd(LayerGeometryProvider *, QMouseEvent *)
 {
 //    SVDEBUG << "ImageLayer::drawEnd(" << e->x() << "," << e->y() << ")" << endl;
     if (!m_model || !m_editing) return;
@@ -638,7 +638,7 @@
 }
 
 void
-ImageLayer::editStart(View *v, QMouseEvent *e)
+ImageLayer::editStart(LayerGeometryProvider *v, QMouseEvent *e)
 {
 //    SVDEBUG << "ImageLayer::editStart(" << e->x() << "," << e->y() << ")" << endl;
 
@@ -660,7 +660,7 @@
 }
 
 void
-ImageLayer::editDrag(View *v, QMouseEvent *e)
+ImageLayer::editDrag(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_model || !m_editing) return;
 
@@ -680,7 +680,7 @@
 }
 
 void
-ImageLayer::editEnd(View *, QMouseEvent *)
+ImageLayer::editEnd(LayerGeometryProvider *, QMouseEvent *)
 {
 //    SVDEBUG << "ImageLayer::editEnd(" << e->x() << "," << e->y() << ")" << endl;
     if (!m_model || !m_editing) return;
@@ -694,7 +694,7 @@
 }
 
 bool
-ImageLayer::editOpen(View *v, QMouseEvent *e)
+ImageLayer::editOpen(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_model) return false;
 
@@ -801,7 +801,7 @@
 }
 
 void
-ImageLayer::copy(View *v, Selection s, Clipboard &to)
+ImageLayer::copy(LayerGeometryProvider *v, Selection s, Clipboard &to)
 {
     if (!m_model) return;
 
@@ -819,7 +819,7 @@
 }
 
 bool
-ImageLayer::paste(View *v, const Clipboard &from, sv_frame_t /* frameOffset */, bool /* interactive */)
+ImageLayer::paste(LayerGeometryProvider *v, const Clipboard &from, sv_frame_t /* frameOffset */, bool /* interactive */)
 {
     if (!m_model) return false;
 
@@ -830,7 +830,7 @@
     if (clipboardHasDifferentAlignment(v, from)) {
 
         QMessageBox::StandardButton button =
-            QMessageBox::question(v, tr("Re-align pasted items?"),
+            QMessageBox::question(v->getView(), tr("Re-align pasted items?"),
                                   tr("The items you are pasting came from a layer with different source material from this one.  Do you want to re-align them in time, to match the source material for this layer?"),
                                   QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel,
                                   QMessageBox::Yes);
--- a/layer/ImageLayer.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/ImageLayer.h	Fri Jan 13 10:29:50 2017 +0000
@@ -38,31 +38,31 @@
     ImageLayer();
     virtual ~ImageLayer();
 
-    virtual void paint(View *v, QPainter &paint, QRect rect) const;
+    virtual void paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
 
-    virtual QString getFeatureDescription(View *v, QPoint &) const;
+    virtual QString getFeatureDescription(LayerGeometryProvider *v, QPoint &) const;
 
-    virtual bool snapToFeatureFrame(View *v, sv_frame_t &frame,
+    virtual bool snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
 				    int &resolution,
 				    SnapType snap) const;
 
-    virtual void drawStart(View *v, QMouseEvent *);
-    virtual void drawDrag(View *v, QMouseEvent *);
-    virtual void drawEnd(View *v, QMouseEvent *);
+    virtual void drawStart(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void drawDrag(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void drawEnd(LayerGeometryProvider *v, QMouseEvent *);
 
-    virtual void editStart(View *v, QMouseEvent *);
-    virtual void editDrag(View *v, QMouseEvent *);
-    virtual void editEnd(View *v, QMouseEvent *);
+    virtual void editStart(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void editDrag(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void editEnd(LayerGeometryProvider *v, QMouseEvent *);
 
     virtual void moveSelection(Selection s, sv_frame_t newStartFrame);
     virtual void resizeSelection(Selection s, Selection newSize);
     virtual void deleteSelection(Selection s);
 
-    virtual void copy(View *v, Selection s, Clipboard &to);
-    virtual bool paste(View *v, const Clipboard &from, sv_frame_t frameOffset,
+    virtual void copy(LayerGeometryProvider *v, Selection s, Clipboard &to);
+    virtual bool paste(LayerGeometryProvider *v, const Clipboard &from, sv_frame_t frameOffset,
                        bool interactive);
 
-    virtual bool editOpen(View *, QMouseEvent *); // on double-click
+    virtual bool editOpen(LayerGeometryProvider *, QMouseEvent *); // on double-click
 
     virtual const Model *getModel() const { return m_model; }
     void setModel(ImageModel *model);
@@ -80,11 +80,11 @@
         return ColourAbsent;
     }
 
-    virtual bool isLayerScrollable(const View *v) const;
+    virtual bool isLayerScrollable(const LayerGeometryProvider *v) const;
 
     virtual bool isLayerEditable() const { return true; }
 
-    virtual int getCompletion(View *) const { return m_model->getCompletion(); }
+    virtual int getCompletion(LayerGeometryProvider *) const { return m_model->getCompletion(); }
 
     virtual bool getValueExtents(double &min, double &max,
                                  bool &logarithmic, QString &unit) const;
@@ -92,9 +92,9 @@
     virtual void toXml(QTextStream &stream, QString indent = "",
                        QString extraAttributes = "") const;
 
-    virtual int getVerticalScaleWidth(View *, bool, QPainter &) const { return 0; }
+    virtual int getVerticalScaleWidth(LayerGeometryProvider *, bool, QPainter &) const { return 0; }
 
-    virtual void setLayerDormant(const View *v, bool dormant);
+    virtual void setLayerDormant(const LayerGeometryProvider *v, bool dormant);
 
     void setProperties(const QXmlAttributes &attributes);
 
@@ -105,18 +105,18 @@
     void fileSourceReady();
 
 protected:
-    ImageModel::PointList getLocalPoints(View *v, int x, int y) const;
+    ImageModel::PointList getLocalPoints(LayerGeometryProvider *v, int x, int y) const;
 
     bool getImageOriginalSize(QString name, QSize &size) const;
-    QImage getImage(View *v, QString name, QSize maxSize) const;
+    QImage getImage(LayerGeometryProvider *v, QString name, QSize maxSize) const;
 
-    void drawImage(View *v, QPainter &paint, const ImageModel::Point &p,
+    void drawImage(LayerGeometryProvider *v, QPainter &paint, const ImageModel::Point &p,
                    int x, int nx) const;
 
     //!!! how to reap no-longer-used images?
 
     typedef std::map<QString, QImage> ImageMap;
-    typedef std::map<const View *, ImageMap> ViewImageMap;
+    typedef std::map<const LayerGeometryProvider *, ImageMap> ViewImageMap;
     typedef std::map<QString, FileSource *> FileSourceMap;
 
     static ImageMap m_images;
--- a/layer/Layer.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/Layer.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -115,7 +115,7 @@
 }
 
 void
-Layer::setLayerDormant(const View *v, bool dormant)
+Layer::setLayerDormant(const LayerGeometryProvider *v, bool dormant)
 {
     const void *vv = (const void *)v;
     QMutexLocker locker(&m_dormancyMutex);
@@ -123,7 +123,7 @@
 }
 
 bool
-Layer::isLayerDormant(const View *v) const
+Layer::isLayerDormant(const LayerGeometryProvider *v) const
 {
     const void *vv = (const void *)v;
     QMutexLocker locker(&m_dormancyMutex);
@@ -132,14 +132,14 @@
 }
 
 void
-Layer::showLayer(View *view, bool show)
+Layer::showLayer(LayerGeometryProvider *view, bool show)
 {
     setLayerDormant(view, !show);
     emit layerParametersChanged();
 }
 
 bool
-Layer::getXScaleValue(const View *v, int x, double &value, QString &unit) const
+Layer::getXScaleValue(const LayerGeometryProvider *v, int x, double &value, QString &unit) const
 {
     if (!hasTimeXAxis()) return false;
 
@@ -152,7 +152,7 @@
 }
 
 bool
-Layer::getYScaleDifference(const View *v, int y0, int y1,
+Layer::getYScaleDifference(const LayerGeometryProvider *v, int y0, int y1,
                            double &diff, QString &unit) const
 {
     double v0, v1;
@@ -166,31 +166,31 @@
 }
 
 sv_frame_t
-Layer::alignToReference(View *v, sv_frame_t frame) const
+Layer::alignToReference(LayerGeometryProvider *v, sv_frame_t frame) const
 {
     const Model *m = getModel();
     SVDEBUG << "Layer::alignToReference(" << frame << "): model = " << m << ", alignment reference = " << (m ? m->getAlignmentReference() : 0) << endl;
     if (m && m->getAlignmentReference()) {
         return m->alignToReference(frame);
     } else {
-        return v->alignToReference(frame);
+        return v->getView()->alignToReference(frame);
     }
 }
 
 sv_frame_t
-Layer::alignFromReference(View *v, sv_frame_t frame) const
+Layer::alignFromReference(LayerGeometryProvider *v, sv_frame_t frame) const
 {
     const Model *m = getModel();
     SVDEBUG << "Layer::alignFromReference(" << frame << "): model = " << m << ", alignment reference = " << (m ? m->getAlignmentReference() : 0) << endl;
     if (m && m->getAlignmentReference()) {
         return m->alignFromReference(frame);
     } else {
-        return v->alignFromReference(frame);
+        return v->getView()->alignFromReference(frame);
     }
 }
 
 bool
-Layer::clipboardHasDifferentAlignment(View *v, const Clipboard &clip) const
+Layer::clipboardHasDifferentAlignment(LayerGeometryProvider *v, const Clipboard &clip) const
 {
     // Notes on pasting to an aligned layer:
     // 
@@ -371,7 +371,7 @@
 }
 
 void
-Layer::measureStart(View *v, QMouseEvent *e)
+Layer::measureStart(LayerGeometryProvider *v, QMouseEvent *e)
 {
     setMeasureRectFromPixrect(v, m_draggingRect,
                               QRect(e->x(), e->y(), 0, 0));
@@ -379,7 +379,7 @@
 }
 
 void
-Layer::measureDrag(View *v, QMouseEvent *e)
+Layer::measureDrag(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_haveDraggingRect) return;
 
@@ -391,7 +391,7 @@
 }
 
 void
-Layer::measureEnd(View *v, QMouseEvent *e)
+Layer::measureEnd(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_haveDraggingRect) return;
     measureDrag(v, e);
@@ -405,7 +405,7 @@
 }
 
 void
-Layer::measureDoubleClick(View *, QMouseEvent *)
+Layer::measureDoubleClick(LayerGeometryProvider *, QMouseEvent *)
 {
     // nothing, in the base class
 }
@@ -425,7 +425,7 @@
 }
 
 void
-Layer::paintMeasurementRects(View *v, QPainter &paint,
+Layer::paintMeasurementRects(LayerGeometryProvider *v, QPainter &paint,
                              bool showFocus, QPoint focusPoint) const
 {
     updateMeasurePixrects(v);
@@ -457,7 +457,7 @@
 }
 
 bool
-Layer::nearestMeasurementRectChanged(View *v, QPoint prev, QPoint now) const
+Layer::nearestMeasurementRectChanged(LayerGeometryProvider *v, QPoint prev, QPoint now) const
 {
     updateMeasurePixrects(v);
     
@@ -468,7 +468,7 @@
 }
 
 void
-Layer::updateMeasurePixrects(View *v) const
+Layer::updateMeasurePixrects(LayerGeometryProvider *v) const
 {
     sv_frame_t sf = v->getStartFrame();
     sv_frame_t ef = v->getEndFrame();
@@ -507,26 +507,26 @@
 }
 
 void
-Layer::updateMeasureRectYCoords(View *v, const MeasureRect &r) const
+Layer::updateMeasureRectYCoords(LayerGeometryProvider *v, const MeasureRect &r) const
 {
-    int y0 = int(lrint(r.startY * v->height()));
-    int y1 = int(lrint(r.endY * v->height()));
+    int y0 = int(lrint(r.startY * v->getPaintHeight()));
+    int y1 = int(lrint(r.endY * v->getPaintHeight()));
     r.pixrect = QRect(r.pixrect.x(), y0, r.pixrect.width(), y1 - y0);
 }
 
 void
-Layer::setMeasureRectYCoord(View *v, MeasureRect &r, bool start, int y) const
+Layer::setMeasureRectYCoord(LayerGeometryProvider *v, MeasureRect &r, bool start, int y) const
 {
     if (start) {
-        r.startY = double(y) / double(v->height());
+        r.startY = double(y) / double(v->getPaintHeight());
         r.endY = r.startY;
     } else {
-        r.endY = double(y) / double(v->height());
+        r.endY = double(y) / double(v->getPaintHeight());
     }
 }
 
 void
-Layer::setMeasureRectFromPixrect(View *v, MeasureRect &r, QRect pixrect) const
+Layer::setMeasureRectFromPixrect(LayerGeometryProvider *v, MeasureRect &r, QRect pixrect) const
 {
     r.pixrect = pixrect;
     r.haveFrames = hasTimeXAxis();
@@ -566,13 +566,13 @@
 }
 
 void
-Layer::paintMeasurementRect(View *v, QPainter &paint,
+Layer::paintMeasurementRect(LayerGeometryProvider *v, QPainter &paint,
                             const MeasureRect &r, bool focus) const
 {
     if (r.haveFrames) {
         
         int x0 = -1;
-        int x1 = v->width() + 1;
+        int x1 = v->getPaintWidth() + 1;
         
         if (r.startFrame >= v->getStartFrame()) {
             x0 = v->getXForFrame(r.startFrame);
--- a/layer/Layer.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/Layer.h	Fri Jan 13 10:29:50 2017 +0000
@@ -39,6 +39,7 @@
 class Model;
 class QPainter;
 class View;
+class LayerGeometryProvider;
 class QMouseEvent;
 class Clipboard;
 class RangeMapper;
@@ -83,12 +84,13 @@
     /**
      * Paint the given rectangle of this layer onto the given view
      * using the given painter, superimposing it on top of any
-     * existing material in that view.  The view is provided here
-     * because it is possible for one layer to exist in more than one
-     * view, so the dimensions of the view may vary from one paint
-     * call to another (without any view having been resized).
+     * existing material in that view.  The LayerGeometryProvider (an
+     * interface implemented by View) is provided here because it is
+     * possible for one layer to exist in more than one view, so the
+     * dimensions of the view may vary from one paint call to another
+     * (without any view having been resized).
      */
-    virtual void paint(View *, QPainter &, QRect) const = 0;   
+    virtual void paint(LayerGeometryProvider *, QPainter &, QRect) const = 0;   
 
     /**
      * Enable or disable synchronous painting.  If synchronous
@@ -128,25 +130,25 @@
     virtual QString getLayerPresentationName() const;
     virtual QPixmap getLayerPresentationPixmap(QSize) const { return QPixmap(); }
 
-    virtual int getVerticalScaleWidth(View *, bool detailed,
+    virtual int getVerticalScaleWidth(LayerGeometryProvider *, bool detailed,
                                       QPainter &) const = 0;
 
-    virtual void paintVerticalScale(View *, bool /* detailed */,
+    virtual void paintVerticalScale(LayerGeometryProvider *, bool /* detailed */,
                                     QPainter &, QRect) const { }
 
-    virtual bool getCrosshairExtents(View *, QPainter &, QPoint /* cursorPos */,
+    virtual bool getCrosshairExtents(LayerGeometryProvider *, QPainter &, QPoint /* cursorPos */,
                                      std::vector<QRect> &) const {
         return false;
     }
-    virtual void paintCrosshairs(View *, QPainter &, QPoint) const { }
+    virtual void paintCrosshairs(LayerGeometryProvider *, QPainter &, QPoint) const { }
 
-    virtual void paintMeasurementRects(View *, QPainter &,
+    virtual void paintMeasurementRects(LayerGeometryProvider *, QPainter &,
                                        bool showFocus, QPoint focusPoint) const;
 
-    virtual bool nearestMeasurementRectChanged(View *, QPoint prev,
+    virtual bool nearestMeasurementRectChanged(LayerGeometryProvider *, QPoint prev,
                                                QPoint now) const;
 
-    virtual QString getFeatureDescription(View *, QPoint &) const {
+    virtual QString getFeatureDescription(LayerGeometryProvider *, QPoint &) const {
 	return "";
     }
 
@@ -180,7 +182,7 @@
      * (and leave frame unmodified).  If returning true, also return
      * the resolution of the model in this layer in sample frames.
      */
-    virtual bool snapToFeatureFrame(View * /* v */,
+    virtual bool snapToFeatureFrame(LayerGeometryProvider * /* v */,
 				    sv_frame_t & /* frame */,
 				    int &resolution,
 				    SnapType /* snap */) const {
@@ -204,7 +206,7 @@
      * (and leave frame unmodified).  If returning true, also return
      * the resolution of the model in this layer in sample frames.
      */
-    virtual bool snapToSimilarFeature(View * /* v */,
+    virtual bool snapToSimilarFeature(LayerGeometryProvider * /* v */,
                                       sv_frame_t & /* source frame */,
                                       int &resolution,
                                       SnapType /* snap */) const {
@@ -217,30 +219,30 @@
     // Layer needs to get actual mouse events, I guess.  Draw mode is
     // probably the easier.
 
-    virtual void drawStart(View *, QMouseEvent *) { }
-    virtual void drawDrag(View *, QMouseEvent *) { }
-    virtual void drawEnd(View *, QMouseEvent *) { }
+    virtual void drawStart(LayerGeometryProvider *, QMouseEvent *) { }
+    virtual void drawDrag(LayerGeometryProvider *, QMouseEvent *) { }
+    virtual void drawEnd(LayerGeometryProvider *, QMouseEvent *) { }
 
-    virtual void eraseStart(View *, QMouseEvent *) { }
-    virtual void eraseDrag(View *, QMouseEvent *) { }
-    virtual void eraseEnd(View *, QMouseEvent *) { }
+    virtual void eraseStart(LayerGeometryProvider *, QMouseEvent *) { }
+    virtual void eraseDrag(LayerGeometryProvider *, QMouseEvent *) { }
+    virtual void eraseEnd(LayerGeometryProvider *, QMouseEvent *) { }
 
-    virtual void editStart(View *, QMouseEvent *) { }
-    virtual void editDrag(View *, QMouseEvent *) { }
-    virtual void editEnd(View *, QMouseEvent *) { }
+    virtual void editStart(LayerGeometryProvider *, QMouseEvent *) { }
+    virtual void editDrag(LayerGeometryProvider *, QMouseEvent *) { }
+    virtual void editEnd(LayerGeometryProvider *, QMouseEvent *) { }
 
-    virtual void splitStart(View *, QMouseEvent *) { }
-    virtual void splitEnd(View *, QMouseEvent *) { }
-    virtual void addNote(View *, QMouseEvent *) { };
+    virtual void splitStart(LayerGeometryProvider *, QMouseEvent *) { }
+    virtual void splitEnd(LayerGeometryProvider *, QMouseEvent *) { }
+    virtual void addNote(LayerGeometryProvider *, QMouseEvent *) { };
 
     // Measurement rectangle (or equivalent).  Unlike draw and edit,
     // the base Layer class can provide working implementations of
     // these for most situations.
     //
-    virtual void measureStart(View *, QMouseEvent *);
-    virtual void measureDrag(View *, QMouseEvent *);
-    virtual void measureEnd(View *, QMouseEvent *);
-    virtual void measureDoubleClick(View *, QMouseEvent *);
+    virtual void measureStart(LayerGeometryProvider *, QMouseEvent *);
+    virtual void measureDrag(LayerGeometryProvider *, QMouseEvent *);
+    virtual void measureEnd(LayerGeometryProvider *, QMouseEvent *);
+    virtual void measureDoubleClick(LayerGeometryProvider *, QMouseEvent *);
 
     virtual bool haveCurrentMeasureRect() const {
         return m_haveCurrentMeasureRect;
@@ -252,13 +254,13 @@
      * double-click).  If there is no item or editing is not
      * supported, return false.
      */
-    virtual bool editOpen(View *, QMouseEvent *) { return false; }
+    virtual bool editOpen(LayerGeometryProvider *, QMouseEvent *) { return false; }
 
     virtual void moveSelection(Selection, sv_frame_t /* newStartFrame */) { }
     virtual void resizeSelection(Selection, Selection /* newSize */) { }
     virtual void deleteSelection(Selection) { }
 
-    virtual void copy(View *, Selection, Clipboard & /* to */) { }
+    virtual void copy(LayerGeometryProvider *, Selection, Clipboard & /* to */) { }
 
     /**
      * Paste from the given clipboard onto the layer at the given
@@ -267,7 +269,7 @@
      * return false if the user cancelled the paste operation.  This
      * function should return true if a paste actually occurred.
      */
-    virtual bool paste(View *,
+    virtual bool paste(LayerGeometryProvider *,
                        const Clipboard & /* from */,
                        sv_frame_t /* frameOffset */,
                        bool /* interactive */) { return false; }
@@ -289,7 +291,7 @@
      * scrolling better if it is known that individual views can be
      * scrolled safely in this way.
      */
-    virtual bool isLayerScrollable(const View *) const { return true; }
+    virtual bool isLayerScrollable(const LayerGeometryProvider *) const { return true; }
 
     /**
      * This should return true if the layer completely obscures any
@@ -344,14 +346,14 @@
      * isReady(int *) call.  The view may choose to show a progress
      * meter if it finds that this returns < 100 at any given moment.
      */
-    virtual int getCompletion(View *) const { return 100; }
+    virtual int getCompletion(LayerGeometryProvider *) const { return 100; }
 
     /**
      * Return an error string if any errors have occurred while
      * loading or processing data for the given view.  Return the
      * empty string if no error has occurred.
      */
-    virtual QString getError(View *) const { return ""; }
+    virtual QString getError(LayerGeometryProvider *) const { return ""; }
 
     virtual void setObjectName(const QString &name);
 
@@ -400,13 +402,13 @@
      * A layer class that overrides this function must also call this
      * class's implementation.
      */
-    virtual void setLayerDormant(const View *v, bool dormant);
+    virtual void setLayerDormant(const LayerGeometryProvider *v, bool dormant);
 
     /**
      * Return whether the layer is dormant (i.e. hidden) in the given
      * view.
      */
-    virtual bool isLayerDormant(const View *v) const;
+    virtual bool isLayerDormant(const LayerGeometryProvider *v) const;
 
     virtual PlayParameters *getPlayParameters();
 
@@ -457,14 +459,14 @@
      * measurement tool.  The default implementation works correctly
      * if the layer hasTimeXAxis().
      */
-    virtual bool getXScaleValue(const View *v, int x,
+    virtual bool getXScaleValue(const LayerGeometryProvider *v, int x,
                                 double &value, QString &unit) const;
 
     /** 
      * Return the value and unit at the given y coordinate in the
      * given view.
      */
-    virtual bool getYScaleValue(const View *, int /* y */,
+    virtual bool getYScaleValue(const LayerGeometryProvider *, int /* y */,
                                 double &/* value */, QString &/* unit */) const {
         return false;
     }
@@ -475,7 +477,7 @@
      * The default implementation just calls getYScaleValue twice and
      * returns the difference, with the same unit.
      */
-    virtual bool getYScaleDifference(const View *v, int y0, int y1,
+    virtual bool getYScaleDifference(const LayerGeometryProvider *v, int y0, int y1,
                                      double &diff, QString &unit) const;
         
     /**
@@ -526,7 +528,7 @@
     virtual bool canExistWithoutModel() const { return false; }
 
 public slots:
-    void showLayer(View *, bool show);
+    void showLayer(LayerGeometryProvider *, bool show);
 
 signals:
     void modelChanged();
@@ -545,9 +547,9 @@
 protected:
     void connectSignals(const Model *);
 
-    virtual sv_frame_t alignToReference(View *v, sv_frame_t frame) const;
-    virtual sv_frame_t alignFromReference(View *v, sv_frame_t frame) const;
-    bool clipboardHasDifferentAlignment(View *v, const Clipboard &clip) const;
+    virtual sv_frame_t alignToReference(LayerGeometryProvider *v, sv_frame_t frame) const;
+    virtual sv_frame_t alignFromReference(LayerGeometryProvider *v, sv_frame_t frame) const;
+    bool clipboardHasDifferentAlignment(LayerGeometryProvider *v, const Clipboard &clip) const;
 
     struct MeasureRect {
 
@@ -612,16 +614,16 @@
     // Note that pixrects are only correct for a single view.
     // So we should update them at the start of the paint procedure
     // (painting is single threaded) and only use them after that.
-    void updateMeasurePixrects(View *v) const;
+    void updateMeasurePixrects(LayerGeometryProvider *v) const;
 
-    virtual void updateMeasureRectYCoords(View *v, const MeasureRect &r) const;
-    virtual void setMeasureRectYCoord(View *v, MeasureRect &r, bool start, int y) const;
-    virtual void setMeasureRectFromPixrect(View *v, MeasureRect &r, QRect pixrect) const;
+    virtual void updateMeasureRectYCoords(LayerGeometryProvider *v, const MeasureRect &r) const;
+    virtual void setMeasureRectYCoord(LayerGeometryProvider *v, MeasureRect &r, bool start, int y) const;
+    virtual void setMeasureRectFromPixrect(LayerGeometryProvider *v, MeasureRect &r, QRect pixrect) const;
 
     // This assumes updateMeasurementPixrects has been called
     MeasureRectSet::const_iterator findFocusedMeasureRect(QPoint) const;
 
-    void paintMeasurementRect(View *v, QPainter &paint,
+    void paintMeasurementRect(LayerGeometryProvider *v, QPainter &paint,
                               const MeasureRect &r, bool focus) const;
 
     QString m_presentationName;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/LayerGeometryProvider.h	Fri Jan 13 10:29:50 2017 +0000
@@ -0,0 +1,184 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef LAYER_GEOMETRY_PROVIDER_H
+#define LAYER_GEOMETRY_PROVIDER_H
+
+#include "base/BaseTypes.h"
+
+#include <QMutex>
+#include <QMutexLocker>
+#include <QPainter>
+
+class ViewManager;
+class View;
+class Layer;
+
+/**
+ * Interface for classes that provide geometry information (such as
+ * size, start frame, and a large number of other properties) about
+ * the disposition of a layer. The main implementor of this interface
+ * is the View class, but other implementations may be used in
+ * different circumstances, e.g. as a proxy to handle hi-dpi
+ * coordinate mapping.
+ *
+ * Note it is expected that some implementations of this may be
+ * disposable, created on-the-fly for a single use. Code that receives
+ * a LayerGeometryProvider pointer as an argument to something should
+ * not, in general, store that pointer as it may be invalidated before
+ * the next use. Use getId() to instead obtain a persistent identifier
+ * for a LayerGeometryProvider, for example to establish whether the
+ * same one is being provided in two separate calls.
+ */
+class LayerGeometryProvider
+{
+protected:
+    static int getNextId() {
+        static QMutex idMutex;
+        static int nextId = 1;
+        static int maxId = INT_MAX;
+        QMutexLocker locker(&idMutex);
+        int id = nextId;
+        if (nextId == maxId) {
+            // we don't expect this to happen in the lifetime of a
+            // process, but it would be undefined behaviour if it did
+            // since we're using a signed int, so we should really
+            // guard for it...
+            nextId = 1;
+        } else {
+            nextId++;
+        }
+        return id;
+    }            
+    
+public:
+    LayerGeometryProvider() { }
+    
+    /**
+     * Retrieve the id of this object.
+     */
+    virtual int getId() const = 0;
+
+    /**
+     * Retrieve the first visible sample frame on the widget.
+     * This is a calculated value based on the centre-frame, widget
+     * width and zoom level.  The result may be negative.
+     */
+    virtual sv_frame_t getStartFrame() const = 0;
+
+    /**
+     * Return the centre frame of the visible widget.  This is an
+     * exact value that does not depend on the zoom block size.  Other
+     * frame values (start, end) are calculated from this based on the
+     * zoom and other factors.
+     */
+    virtual sv_frame_t getCentreFrame() const = 0;
+
+    /**
+     * Retrieve the last visible sample frame on the widget.
+     * This is a calculated value based on the centre-frame, widget
+     * width and zoom level.
+     */
+    virtual sv_frame_t getEndFrame() const = 0;
+
+    /**
+     * Return the pixel x-coordinate corresponding to a given sample
+     * frame (which may be negative).
+     */
+    virtual int getXForFrame(sv_frame_t frame) const = 0;
+
+    /**
+     * Return the closest frame to the given pixel x-coordinate.
+     */
+    virtual sv_frame_t getFrameForX(int x) const = 0;
+
+    virtual sv_frame_t getModelsStartFrame() const = 0;
+    virtual sv_frame_t getModelsEndFrame() const = 0;
+
+    /**
+     * Return the closest pixel x-coordinate corresponding to a given
+     * view x-coordinate.
+     */
+    virtual int getXForViewX(int viewx) const = 0;
+    
+    /**
+     * Return the closest view x-coordinate corresponding to a given
+     * pixel x-coordinate.
+     */
+    virtual int getViewXForX(int x) const = 0;
+    
+    /**
+     * Return the (maybe fractional) pixel y-coordinate corresponding
+     * to a given frequency, if the frequency range is as specified.
+     * This does not imply any policy about layer frequency ranges,
+     * but it might be useful for layers to match theirs up if
+     * desired.
+     *
+     * Not thread-safe in logarithmic mode.  Call only from GUI thread.
+     */
+    virtual double getYForFrequency(double frequency,
+                                    double minFreq, double maxFreq, 
+                                    bool logarithmic) const = 0;
+
+    /**
+     * Return the closest frequency to the given (maybe fractional)
+     * pixel y-coordinate, if the frequency range is as specified.
+     *
+     * Not thread-safe in logarithmic mode.  Call only from GUI thread.
+     */
+    virtual double getFrequencyForY(double y,
+                                    double minFreq, double maxFreq,
+                                    bool logarithmic) const = 0;
+
+    virtual int getTextLabelHeight(const Layer *layer, QPainter &) const = 0;
+
+    virtual bool getValueExtents(QString unit, double &min, double &max,
+                                 bool &log) const = 0;
+
+    /**
+     * Return the zoom level, i.e. the number of frames per pixel
+     */
+    virtual int getZoomLevel() const = 0;
+
+    /**
+     * To be called from a layer, to obtain the extent of the surface
+     * that the layer is currently painting to. This may be the extent
+     * of the view (if 1x display scaling is in effect) or of a larger
+     * cached pixmap (if greater display scaling is in effect).
+     */
+    virtual QRect getPaintRect() const = 0;
+
+    virtual QSize getPaintSize() const { return getPaintRect().size(); }
+    virtual int getPaintWidth() const { return getPaintRect().width(); }
+    virtual int getPaintHeight() const { return getPaintRect().height(); }
+
+    virtual bool hasLightBackground() const = 0;
+    virtual QColor getForeground() const = 0;
+    virtual QColor getBackground() const = 0;
+
+    virtual ViewManager *getViewManager() const = 0;
+
+    virtual bool shouldIlluminateLocalFeatures(const Layer *, QPoint &) const = 0;
+    virtual bool shouldShowFeatureLabels() const = 0;
+
+    virtual void drawMeasurementRect(QPainter &p, const Layer *,
+                                     QRect rect, bool focus) const = 0;
+
+    virtual void updatePaintRect(QRect r) = 0;
+    
+    virtual View *getView() = 0;
+    virtual const View *getView() const = 0;
+};
+
+#endif
--- a/layer/LinearColourScale.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/LinearColourScale.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -20,24 +20,24 @@
 
 #include <cmath>
 
-#include "view/View.h"
+#include "LayerGeometryProvider.h"
 
 int
-LinearColourScale::getWidth(View *,
+LinearColourScale::getWidth(LayerGeometryProvider *,
 			    QPainter &paint)
 {
     return paint.fontMetrics().width("-000.00") + 15;
 }
 
 void
-LinearColourScale::paintVertical(View *v,
+LinearColourScale::paintVertical(LayerGeometryProvider *v,
 				 const ColourScaleLayer *layer,
 				 QPainter &paint,
 				 int /* x0 */,
 				 double min,
 				 double max)
 {
-    int h = v->height();
+    int h = v->getPaintHeight();
 
     int n = 10;
 
--- a/layer/LinearColourScale.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/LinearColourScale.h	Fri Jan 13 10:29:50 2017 +0000
@@ -19,16 +19,16 @@
 #include <QRect>
 
 class QPainter;
-class View;
+class LayerGeometryProvider;
 class ColourScaleLayer;
 
 class LinearColourScale
 {
 public:
-    int getWidth(View *v, QPainter &paint);
+    int getWidth(LayerGeometryProvider *v, QPainter &paint);
 
     void paintVertical
-    (View *v, const ColourScaleLayer *layer, QPainter &paint, int x0,
+    (LayerGeometryProvider *v, const ColourScaleLayer *layer, QPainter &paint, int x0,
      double minf, double maxf);
 };
 
--- a/layer/LinearNumericalScale.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/LinearNumericalScale.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -20,17 +20,17 @@
 
 #include <cmath>
 
-#include "view/View.h"
+#include "LayerGeometryProvider.h"
 
 int
-LinearNumericalScale::getWidth(View *,
+LinearNumericalScale::getWidth(LayerGeometryProvider *,
 			       QPainter &paint)
 {
     return paint.fontMetrics().width("-000.00") + 10;
 }
 
 void
-LinearNumericalScale::paintVertical(View *v,
+LinearNumericalScale::paintVertical(LayerGeometryProvider *v,
 				    const VerticalScaleLayer *layer,
 				    QPainter &paint,
 				    int x0,
@@ -69,7 +69,7 @@
         double dispval = val;
 
 	if (i == n-1 &&
-	    v->height() < paint.fontMetrics().height() * (n*2)) {
+	    v->getPaintHeight() < paint.fontMetrics().height() * (n*2)) {
 	    if (layer->getScaleUnits() != "") drawText = false;
 	}
 	dispval = int(rint(val / round) * round);
--- a/layer/LinearNumericalScale.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/LinearNumericalScale.h	Fri Jan 13 10:29:50 2017 +0000
@@ -19,17 +19,17 @@
 #include <QRect>
 
 class QPainter;
-class View;
+class LayerGeometryProvider;
 class VerticalScaleLayer;
 
 class LinearNumericalScale
 {
 public:
-    int getWidth(View *v, QPainter &paint);
+    int getWidth(LayerGeometryProvider *v, QPainter &paint);
 
     void paintVertical
-    (View *v, const VerticalScaleLayer *layer, QPainter &paint, int x0,
-     double minf, double maxf);
+    (LayerGeometryProvider *v, const VerticalScaleLayer *layer,
+     QPainter &paint, int x0, double minf, double maxf);
 };
 
 #endif
--- a/layer/LogColourScale.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/LogColourScale.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -22,24 +22,24 @@
 
 #include <cmath>
 
-#include "view/View.h"
+#include "LayerGeometryProvider.h"
 
 int
-LogColourScale::getWidth(View *,
+LogColourScale::getWidth(LayerGeometryProvider *,
 			    QPainter &paint)
 {
     return paint.fontMetrics().width("-000.00") + 15;
 }
 
 void
-LogColourScale::paintVertical(View *v,
+LogColourScale::paintVertical(LayerGeometryProvider *v,
 			      const ColourScaleLayer *layer,
 			      QPainter &paint,
 			      int /* x0 */,
 			      double minlog,
 			      double maxlog)
 {
-    int h = v->height();
+    int h = v->getPaintHeight();
 
     int n = 10;
 
--- a/layer/LogColourScale.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/LogColourScale.h	Fri Jan 13 10:29:50 2017 +0000
@@ -19,16 +19,16 @@
 #include <QRect>
 
 class QPainter;
-class View;
+class LayerGeometryProvider;
 class ColourScaleLayer;
 
 class LogColourScale
 {
 public:
-    int getWidth(View *v, QPainter &paint);
+    int getWidth(LayerGeometryProvider *v, QPainter &paint);
 
     void paintVertical
-    (View *v, const ColourScaleLayer *layer, QPainter &paint, int x0,
+    (LayerGeometryProvider *v, const ColourScaleLayer *layer, QPainter &paint, int x0,
      double minf, double maxf);
 };
 
--- a/layer/LogNumericalScale.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/LogNumericalScale.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -22,19 +22,19 @@
 
 #include <cmath>
 
-#include "view/View.h"
+#include "LayerGeometryProvider.h"
 
 //#define DEBUG_TIME_VALUE_LAYER 1
 
 int
-LogNumericalScale::getWidth(View *,
+LogNumericalScale::getWidth(LayerGeometryProvider *,
 			    QPainter &paint)
 {
     return paint.fontMetrics().width("-000.00") + 10;
 }
 
 void
-LogNumericalScale::paintVertical(View *v,
+LogNumericalScale::paintVertical(LayerGeometryProvider *v,
 				 const VerticalScaleLayer *layer,
 				 QPainter &paint,
 				 int x0,
@@ -79,7 +79,7 @@
         bool drawText = true;
 
 	if (i == n-1 &&
-	    v->height() < paint.fontMetrics().height() * (n*2)) {
+	    v->getPaintHeight() < paint.fontMetrics().height() * (n*2)) {
 	    if (layer->getScaleUnits() != "") drawText = false;
 	}
 
--- a/layer/LogNumericalScale.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/LogNumericalScale.h	Fri Jan 13 10:29:50 2017 +0000
@@ -19,16 +19,16 @@
 #include <QRect>
 
 class QPainter;
-class View;
+class LayerGeometryProvider;
 class VerticalScaleLayer;
 
 class LogNumericalScale
 {
 public:
-    int getWidth(View *v, QPainter &paint);
+    int getWidth(LayerGeometryProvider *v, QPainter &paint);
 
     void paintVertical
-    (View *v, const VerticalScaleLayer *layer, QPainter &paint, int x0,
+    (LayerGeometryProvider *v, const VerticalScaleLayer *layer, QPainter &paint, int x0,
      double minlog, double maxlog);
 };
 
--- a/layer/NoteLayer.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/NoteLayer.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -21,12 +21,13 @@
 #include "base/Pitch.h"
 #include "base/LogRange.h"
 #include "base/RangeMapper.h"
-#include "ColourDatabase.h"
 #include "view/View.h"
 
+#include "ColourDatabase.h"
 #include "PianoScale.h"
 #include "LinearNumericalScale.h"
 #include "LogNumericalScale.h"
+#include "PaintAssistant.h"
 
 #include "data/model/NoteModel.h"
 
@@ -191,7 +192,7 @@
 }
 
 bool
-NoteLayer::isLayerScrollable(const View *v) const
+NoteLayer::isLayerScrollable(const LayerGeometryProvider *v) const
 {
     QPoint discard;
     return !v->shouldIlluminateLocalFeatures(this, discard);
@@ -389,7 +390,7 @@
 }
 
 NoteModel::PointList
-NoteLayer::getLocalPoints(View *v, int x) const
+NoteLayer::getLocalPoints(LayerGeometryProvider *v, int x) const
 {
     if (!m_model) return NoteModel::PointList();
 
@@ -432,7 +433,7 @@
 }
 
 bool
-NoteLayer::getPointToDrag(View *v, int x, int y, NoteModel::Point &p) const
+NoteLayer::getPointToDrag(LayerGeometryProvider *v, int x, int y, NoteModel::Point &p) const
 {
     if (!m_model) return false;
 
@@ -460,7 +461,7 @@
 }
 
 QString
-NoteLayer::getFeatureDescription(View *v, QPoint &pos) const
+NoteLayer::getFeatureDescription(LayerGeometryProvider *v, QPoint &pos) const
 {
     int x = pos.x();
 
@@ -547,7 +548,7 @@
 }
 
 bool
-NoteLayer::snapToFeatureFrame(View *v, sv_frame_t &frame,
+NoteLayer::snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
 			      int &resolution,
 			      SnapType snap) const
 {
@@ -619,7 +620,7 @@
 }
 
 void
-NoteLayer::getScaleExtents(View *v, double &min, double &max, bool &log) const
+NoteLayer::getScaleExtents(LayerGeometryProvider *v, double &min, double &max, bool &log) const
 {
     min = 0.0;
     max = 0.0;
@@ -677,11 +678,11 @@
 }
 
 int
-NoteLayer::getYForValue(View *v, double val) const
+NoteLayer::getYForValue(LayerGeometryProvider *v, double val) const
 {
     double min = 0.0, max = 0.0;
     bool logarithmic = false;
-    int h = v->height();
+    int h = v->getPaintHeight();
 
     getScaleExtents(v, min, max, logarithmic);
 
@@ -712,11 +713,11 @@
 }
 
 double
-NoteLayer::getValueForY(View *v, int y) const
+NoteLayer::getValueForY(LayerGeometryProvider *v, int y) const
 {
     double min = 0.0, max = 0.0;
     bool logarithmic = false;
-    int h = v->height();
+    int h = v->getPaintHeight();
 
     getScaleExtents(v, min, max, logarithmic);
 
@@ -741,7 +742,7 @@
 }
 
 void
-NoteLayer::paint(View *v, QPainter &paint, QRect rect) const
+NoteLayer::paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const
 {
     if (!m_model || !m_model->isOK()) return;
 
@@ -809,18 +810,18 @@
             paint.setBrush(v->getForeground());
 
             QString vlabel = QString("%1%2").arg(p.value).arg(getScaleUnits());
-            v->drawVisibleText(paint, 
+            PaintAssistant::drawVisibleText(v, paint, 
                                x - paint.fontMetrics().width(vlabel) - 2,
                                y + paint.fontMetrics().height()/2
                                  - paint.fontMetrics().descent(), 
-                               vlabel, View::OutlinedText);
+                               vlabel, PaintAssistant::OutlinedText);
 
             QString hlabel = RealTime::frame2RealTime
                 (p.frame, m_model->getSampleRate()).toText(true).c_str();
-            v->drawVisibleText(paint, 
+            PaintAssistant::drawVisibleText(v, paint, 
                                x,
                                y - h/2 - paint.fontMetrics().descent() - 2,
-                               hlabel, View::OutlinedText);
+                               hlabel, PaintAssistant::OutlinedText);
 	}
 	
 	paint.drawRect(x, y - h/2, w, h);
@@ -830,7 +831,7 @@
 }
 
 int
-NoteLayer::getVerticalScaleWidth(View *v, bool, QPainter &paint) const
+NoteLayer::getVerticalScaleWidth(LayerGeometryProvider *v, bool, QPainter &paint) const
 {
     if (!m_model || shouldAutoAlign()) {
         return 0;
@@ -844,7 +845,7 @@
 }
 
 void
-NoteLayer::paintVerticalScale(View *v, bool, QPainter &paint, QRect) const
+NoteLayer::paintVerticalScale(LayerGeometryProvider *v, bool, QPainter &paint, QRect) const
 {
     if (!m_model || m_model->getPoints().empty()) return;
 
@@ -853,7 +854,7 @@
     bool logarithmic;
 
     int w = getVerticalScaleWidth(v, false, paint);
-    int h = v->height();
+    int h = v->getPaintHeight();
 
     getScaleExtents(v, min, max, logarithmic);
 
@@ -882,7 +883,7 @@
 }
 
 void
-NoteLayer::drawStart(View *v, QMouseEvent *e)
+NoteLayer::drawStart(LayerGeometryProvider *v, QMouseEvent *e)
 {
 //    SVDEBUG << "NoteLayer::drawStart(" << e->x() << "," << e->y() << ")" << endl;
 
@@ -906,7 +907,7 @@
 }
 
 void
-NoteLayer::drawDrag(View *v, QMouseEvent *e)
+NoteLayer::drawDrag(LayerGeometryProvider *v, QMouseEvent *e)
 {
 //    SVDEBUG << "NoteLayer::drawDrag(" << e->x() << "," << e->y() << ")" << endl;
 
@@ -935,7 +936,7 @@
 }
 
 void
-NoteLayer::drawEnd(View *, QMouseEvent *)
+NoteLayer::drawEnd(LayerGeometryProvider *, QMouseEvent *)
 {
 //    SVDEBUG << "NoteLayer::drawEnd(" << e->x() << "," << e->y() << ")" << endl;
     if (!m_model || !m_editing) return;
@@ -945,7 +946,7 @@
 }
 
 void
-NoteLayer::eraseStart(View *v, QMouseEvent *e)
+NoteLayer::eraseStart(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_model) return;
 
@@ -960,12 +961,12 @@
 }
 
 void
-NoteLayer::eraseDrag(View *, QMouseEvent *)
+NoteLayer::eraseDrag(LayerGeometryProvider *, QMouseEvent *)
 {
 }
 
 void
-NoteLayer::eraseEnd(View *v, QMouseEvent *e)
+NoteLayer::eraseEnd(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_model || !m_editing) return;
 
@@ -985,7 +986,7 @@
 }
 
 void
-NoteLayer::editStart(View *v, QMouseEvent *e)
+NoteLayer::editStart(LayerGeometryProvider *v, QMouseEvent *e)
 {
 //    SVDEBUG << "NoteLayer::editStart(" << e->x() << "," << e->y() << ")" << endl;
 
@@ -1008,7 +1009,7 @@
 }
 
 void
-NoteLayer::editDrag(View *v, QMouseEvent *e)
+NoteLayer::editDrag(LayerGeometryProvider *v, QMouseEvent *e)
 {
 //    SVDEBUG << "NoteLayer::editDrag(" << e->x() << "," << e->y() << ")" << endl;
 
@@ -1037,7 +1038,7 @@
 }
 
 void
-NoteLayer::editEnd(View *, QMouseEvent *)
+NoteLayer::editEnd(LayerGeometryProvider *, QMouseEvent *)
 {
 //    SVDEBUG << "NoteLayer::editEnd(" << e->x() << "," << e->y() << ")" << endl;
     if (!m_model || !m_editing) return;
@@ -1065,7 +1066,7 @@
 }
 
 bool
-NoteLayer::editOpen(View *v, QMouseEvent *e)
+NoteLayer::editOpen(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_model) return false;
 
@@ -1193,7 +1194,7 @@
 }    
 
 void
-NoteLayer::copy(View *v, Selection s, Clipboard &to)
+NoteLayer::copy(LayerGeometryProvider *v, Selection s, Clipboard &to)
 {
     if (!m_model) return;
 
@@ -1211,7 +1212,7 @@
 }
 
 bool
-NoteLayer::paste(View *v, const Clipboard &from, sv_frame_t /* frameOffset */, bool /* interactive */)
+NoteLayer::paste(LayerGeometryProvider *v, const Clipboard &from, sv_frame_t /* frameOffset */, bool /* interactive */)
 {
     if (!m_model) return false;
 
@@ -1222,7 +1223,7 @@
     if (clipboardHasDifferentAlignment(v, from)) {
 
         QMessageBox::StandardButton button =
-            QMessageBox::question(v, tr("Re-align pasted items?"),
+            QMessageBox::question(v->getView(), tr("Re-align pasted items?"),
                                   tr("The items you are pasting came from a layer with different source material from this one.  Do you want to re-align them in time, to match the source material for this layer?"),
                                   QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel,
                                   QMessageBox::Yes);
--- a/layer/NoteLayer.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/NoteLayer.h	Fri Jan 13 10:29:50 2017 +0000
@@ -35,37 +35,37 @@
 public:
     NoteLayer();
 
-    virtual void paint(View *v, QPainter &paint, QRect rect) const;
+    virtual void paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
 
-    virtual int getVerticalScaleWidth(View *v, bool, QPainter &) const;
-    virtual void paintVerticalScale(View *v, bool, QPainter &paint, QRect rect) const;
+    virtual int getVerticalScaleWidth(LayerGeometryProvider *v, bool, QPainter &) const;
+    virtual void paintVerticalScale(LayerGeometryProvider *v, bool, QPainter &paint, QRect rect) const;
 
-    virtual QString getFeatureDescription(View *v, QPoint &) const;
+    virtual QString getFeatureDescription(LayerGeometryProvider *v, QPoint &) const;
 
-    virtual bool snapToFeatureFrame(View *v, sv_frame_t &frame,
+    virtual bool snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
 				    int &resolution,
 				    SnapType snap) const;
 
-    virtual void drawStart(View *v, QMouseEvent *);
-    virtual void drawDrag(View *v, QMouseEvent *);
-    virtual void drawEnd(View *v, QMouseEvent *);
+    virtual void drawStart(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void drawDrag(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void drawEnd(LayerGeometryProvider *v, QMouseEvent *);
 
-    virtual void eraseStart(View *v, QMouseEvent *);
-    virtual void eraseDrag(View *v, QMouseEvent *);
-    virtual void eraseEnd(View *v, QMouseEvent *);
+    virtual void eraseStart(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void eraseDrag(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void eraseEnd(LayerGeometryProvider *v, QMouseEvent *);
 
-    virtual void editStart(View *v, QMouseEvent *);
-    virtual void editDrag(View *v, QMouseEvent *);
-    virtual void editEnd(View *v, QMouseEvent *);
+    virtual void editStart(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void editDrag(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void editEnd(LayerGeometryProvider *v, QMouseEvent *);
 
-    virtual bool editOpen(View *v, QMouseEvent *);
+    virtual bool editOpen(LayerGeometryProvider *v, QMouseEvent *);
 
     virtual void moveSelection(Selection s, sv_frame_t newStartFrame);
     virtual void resizeSelection(Selection s, Selection newSize);
     virtual void deleteSelection(Selection s);
 
-    virtual void copy(View *v, Selection s, Clipboard &to);
-    virtual bool paste(View *v, const Clipboard &from, sv_frame_t frameOffset,
+    virtual void copy(LayerGeometryProvider *v, Selection s, Clipboard &to);
+    virtual bool paste(LayerGeometryProvider *v, const Clipboard &from, sv_frame_t frameOffset,
                        bool interactive);
 
     virtual const Model *getModel() const { return m_model; }
@@ -91,11 +91,11 @@
     void setVerticalScale(VerticalScale scale);
     VerticalScale getVerticalScale() const { return m_verticalScale; }
 
-    virtual bool isLayerScrollable(const View *v) const;
+    virtual bool isLayerScrollable(const LayerGeometryProvider *v) const;
 
     virtual bool isLayerEditable() const { return true; }
 
-    virtual int getCompletion(View *) const { return m_model->getCompletion(); }
+    virtual int getCompletion(LayerGeometryProvider *) const { return m_model->getCompletion(); }
 
     virtual bool getValueExtents(double &min, double &max,
                                  bool &log, QString &unit) const;
@@ -132,19 +132,19 @@
     void setProperties(const QXmlAttributes &attributes);
 
     /// VerticalScaleLayer methods
-    virtual int getYForValue(View *v, double value) const;
-    virtual double getValueForY(View *v, int y) const;
+    virtual int getYForValue(LayerGeometryProvider *v, double value) const;
+    virtual double getValueForY(LayerGeometryProvider *v, int y) const;
     virtual QString getScaleUnits() const;
 
 protected:
-    void getScaleExtents(View *, double &min, double &max, bool &log) const;
+    void getScaleExtents(LayerGeometryProvider *, double &min, double &max, bool &log) const;
     bool shouldConvertMIDIToHz() const;
 
     virtual int getDefaultColourHint(bool dark, bool &impose);
 
-    NoteModel::PointList getLocalPoints(View *v, int) const;
+    NoteModel::PointList getLocalPoints(LayerGeometryProvider *v, int) const;
 
-    bool getPointToDrag(View *v, int x, int y, NoteModel::Point &) const;
+    bool getPointToDrag(LayerGeometryProvider *v, int x, int y, NoteModel::Point &) const;
 
     NoteModel *m_model;
     bool m_editing;
--- a/layer/PaintAssistant.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/PaintAssistant.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -15,7 +15,10 @@
 
 #include "PaintAssistant.h"
 
+#include "LayerGeometryProvider.h"
+
 #include "base/AudioLevel.h"
+#include "base/Strings.h"
 
 #include <QPaintDevice>
 #include <QPainter>
@@ -79,7 +82,7 @@
             text = QString("%1").arg(meterdbs[i]);
             if (i == n) text = "0dB";
             if (i == 0) {
-                text = "-Inf";
+                text = Strings::minus_infinity;
                 val = 0.0;
             }
             break;
@@ -89,7 +92,7 @@
             text = QString("%1").arg(-(10*n) + i * 10);
             if (i == n) text = "0dB";
             if (i == 0) {
-                text = "-Inf";
+                text = Strings::minus_infinity;
                 val = 0.0;
             }
             break;
@@ -207,3 +210,55 @@
 
     return vy;
 }
+
+void
+PaintAssistant::drawVisibleText(const LayerGeometryProvider *v,
+                                QPainter &paint, int x, int y,
+                                QString text, TextStyle style)
+{
+    if (style == OutlinedText || style == OutlinedItalicText) {
+
+        paint.save();
+
+        if (style == OutlinedItalicText) {
+            QFont f(paint.font());
+            f.setItalic(true);
+            paint.setFont(f);
+        }
+
+        QColor penColour, surroundColour, boxColour;
+
+        penColour = v->getForeground();
+        surroundColour = v->getBackground();
+        boxColour = surroundColour;
+        boxColour.setAlpha(127);
+
+        paint.setPen(Qt::NoPen);
+        paint.setBrush(boxColour);
+        
+        QRect r = paint.fontMetrics().boundingRect(text);
+        r.translate(QPoint(x, y));
+//        cerr << "drawVisibleText: r = " << r.x() << "," <<r.y() << " " << r.width() << "x" << r.height() << endl;
+        paint.drawRect(r);
+        paint.setBrush(Qt::NoBrush);
+
+	paint.setPen(surroundColour);
+
+	for (int dx = -1; dx <= 1; ++dx) {
+	    for (int dy = -1; dy <= 1; ++dy) {
+		if (!(dx || dy)) continue;
+		paint.drawText(x + dx, y + dy, text);
+	    }
+	}
+
+	paint.setPen(penColour);
+
+	paint.drawText(x, y, text);
+
+        paint.restore();
+
+    } else {
+
+        std::cerr << "ERROR: PaintAssistant::drawVisibleText: Boxed style not yet implemented!" << std::endl;
+    }
+}
--- a/layer/PaintAssistant.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/PaintAssistant.h	Fri Jan 13 10:29:50 2017 +0000
@@ -13,13 +13,15 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _PAINT_ASSISTANT_H_
-#define _PAINT_ASSISTANT_H_
+#ifndef PAINT_ASSISTANT_H
+#define PAINT_ASSISTANT_H
 
 #include <QRect>
 #include <vector>
 
 class QPainter;
+class Layer;
+class LayerGeometryProvider;
 
 class PaintAssistant
 {
@@ -34,6 +36,16 @@
     static int getYForValue(Scale scale, double value,
                             double minVal, double maxVal,
                             int minY, int height);
+
+    enum TextStyle {
+	BoxedText,
+	OutlinedText,
+        OutlinedItalicText
+    };
+
+    static void drawVisibleText(const LayerGeometryProvider *,
+                                QPainter &p, int x, int y,
+                                QString text, TextStyle style);
 };
 
 #endif
--- a/layer/PianoScale.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/PianoScale.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -21,10 +21,10 @@
 
 #include "base/Pitch.h"
 
-#include "view/View.h"
+#include "LayerGeometryProvider.h"
 
 void
-PianoScale::paintPianoVertical(View *v,
+PianoScale::paintPianoVertical(LayerGeometryProvider *v,
 			       QPainter &paint,
 			       QRect r,
 			       double minf,
--- a/layer/PianoScale.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/PianoScale.h	Fri Jan 13 10:29:50 2017 +0000
@@ -19,13 +19,13 @@
 #include <QRect>
 
 class QPainter;
-class View;
+class LayerGeometryProvider;
 
 class PianoScale
 {
 public:
     void paintPianoVertical
-    (View *v, QPainter &paint, QRect rect, double minf, double maxf);
+    (LayerGeometryProvider *v, QPainter &paint, QRect rect, double minf, double maxf);
 };
 
 #endif
--- a/layer/RegionLayer.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/RegionLayer.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -19,13 +19,14 @@
 #include "base/RealTime.h"
 #include "base/Profiler.h"
 #include "base/LogRange.h"
+
 #include "ColourDatabase.h"
-
 #include "ColourMapper.h"
 #include "LinearNumericalScale.h"
 #include "LogNumericalScale.h"
 #include "LinearColourScale.h"
 #include "LogColourScale.h"
+#include "PaintAssistant.h"
 
 #include "view/View.h"
 
@@ -244,7 +245,7 @@
 }
 
 bool
-RegionLayer::isLayerScrollable(const View *v) const
+RegionLayer::isLayerScrollable(const LayerGeometryProvider *v) const
 {
     QPoint discard;
     return !v->shouldIlluminateLocalFeatures(this, discard);
@@ -302,7 +303,7 @@
 }
 
 RegionModel::PointList
-RegionLayer::getLocalPoints(View *v, int x) const
+RegionLayer::getLocalPoints(LayerGeometryProvider *v, int x) const
 {
     if (!m_model) return RegionModel::PointList();
 
@@ -345,7 +346,7 @@
 }
 
 bool
-RegionLayer::getPointToDrag(View *v, int x, int y, RegionModel::Point &p) const
+RegionLayer::getPointToDrag(LayerGeometryProvider *v, int x, int y, RegionModel::Point &p) const
 {
     if (!m_model) return false;
 
@@ -383,7 +384,7 @@
 }
 
 QString
-RegionLayer::getFeatureDescription(View *v, QPoint &pos) const
+RegionLayer::getFeatureDescription(LayerGeometryProvider *v, QPoint &pos) const
 {
     int x = pos.x();
 
@@ -453,7 +454,7 @@
 }
 
 bool
-RegionLayer::snapToFeatureFrame(View *v, sv_frame_t &frame,
+RegionLayer::snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
                                 int &resolution,
                                 SnapType snap) const
 {
@@ -536,7 +537,7 @@
 }
 
 bool
-RegionLayer::snapToSimilarFeature(View *v, sv_frame_t &frame,
+RegionLayer::snapToSimilarFeature(LayerGeometryProvider *v, sv_frame_t &frame,
                                   int &resolution,
                                   SnapType snap) const
 {
@@ -624,7 +625,7 @@
 }
 
 void
-RegionLayer::getScaleExtents(View *v, double &min, double &max, bool &log) const
+RegionLayer::getScaleExtents(LayerGeometryProvider *v, double &min, double &max, bool &log) const
 {
     min = 0.0;
     max = 0.0;
@@ -676,9 +677,9 @@
 }
 
 int
-RegionLayer::spacingIndexToY(View *v, int i) const
+RegionLayer::spacingIndexToY(LayerGeometryProvider *v, int i) const
 {
-    int h = v->height();
+    int h = v->getPaintHeight();
     int n = int(m_spacingMap.size());
     // this maps from i (spacing of the value from the spacing
     // map) and n (number of region types) to y
@@ -687,10 +688,10 @@
 }
 
 double
-RegionLayer::yToSpacingIndex(View *v, int y) const
+RegionLayer::yToSpacingIndex(LayerGeometryProvider *v, int y) const
 {
     // we return an inexact result here (double rather than int)
-    int h = v->height();
+    int h = v->getPaintHeight();
     int n = int(m_spacingMap.size());
     // from y = h - ((h * i) / n) + (h / (2 * n)) as above (vh taking place of i)
     double vh = double(2*h*n - h - 2*n*y) / double(2*h);
@@ -698,11 +699,11 @@
 }
 
 int
-RegionLayer::getYForValue(View *v, double val) const
+RegionLayer::getYForValue(LayerGeometryProvider *v, double val) const
 {
     double min = 0.0, max = 0.0;
     bool logarithmic = false;
-    int h = v->height();
+    int h = v->getPaintHeight();
 
     if (m_verticalScale == EqualSpaced) {
 
@@ -733,17 +734,17 @@
 }
 
 double
-RegionLayer::getValueForY(View *v, int y) const
+RegionLayer::getValueForY(LayerGeometryProvider *v, int y) const
 {
     return getValueForY(v, y, -1);
 }
 
 double
-RegionLayer::getValueForY(View *v, int y, int avoid) const
+RegionLayer::getValueForY(LayerGeometryProvider *v, int y, int avoid) const
 {
     double min = 0.0, max = 0.0;
     bool logarithmic = false;
-    int h = v->height();
+    int h = v->getPaintHeight();
 
     if (m_verticalScale == EqualSpaced) {
 
@@ -836,7 +837,7 @@
 }
 
 QColor
-RegionLayer::getColourForValue(View *v, double val) const
+RegionLayer::getColourForValue(LayerGeometryProvider *v, double val) const
 {
     double min, max;
     bool log;
@@ -866,7 +867,7 @@
 }
 
 void
-RegionLayer::paint(View *v, QPainter &paint, QRect rect) const
+RegionLayer::paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const
 {
     if (!m_model || !m_model->isOK()) return;
 
@@ -942,7 +943,7 @@
 	if (w < 1) w = 1;
 
 	if (m_plotStyle == PlotSegmentation) {
-            paint.setPen(getForegroundQColor(v));
+            paint.setPen(getForegroundQColor(v->getView()));
             paint.setBrush(getColourForValue(v, p.value));
         } else {
             paint.setPen(getBaseQColor());
@@ -958,15 +959,15 @@
                 RegionModel::Point::Comparator()(illuminatePoint, p) ||
                 RegionModel::Point::Comparator()(p, illuminatePoint)) {
 
-                paint.setPen(QPen(getForegroundQColor(v), 1));
-                paint.drawLine(x, 0, x, v->height());
+                paint.setPen(QPen(getForegroundQColor(v->getView()), 1));
+                paint.drawLine(x, 0, x, v->getPaintHeight());
                 paint.setPen(Qt::NoPen);
 
             } else {
-                paint.setPen(QPen(getForegroundQColor(v), 2));
+                paint.setPen(QPen(getForegroundQColor(v->getView()), 2));
             }
 
-	    paint.drawRect(x, -1, ex - x, v->height() + 2);
+	    paint.drawRect(x, -1, ex - x, v->getPaintHeight() + 2);
 
 	} else {
 
@@ -979,18 +980,18 @@
                 paint.setBrush(v->getForeground());
 
                 QString vlabel = QString("%1%2").arg(p.value).arg(getScaleUnits());
-                v->drawVisibleText(paint, 
+                PaintAssistant::drawVisibleText(v, paint, 
                                    x - paint.fontMetrics().width(vlabel) - 2,
                                    y + paint.fontMetrics().height()/2
                                    - paint.fontMetrics().descent(), 
-                                   vlabel, View::OutlinedText);
+                                   vlabel, PaintAssistant::OutlinedText);
                 
                 QString hlabel = RealTime::frame2RealTime
                     (p.frame, m_model->getSampleRate()).toText(true).c_str();
-                v->drawVisibleText(paint, 
+                PaintAssistant::drawVisibleText(v, paint, 
                                    x,
                                    y - h/2 - paint.fontMetrics().descent() - 2,
-                                   hlabel, View::OutlinedText);
+                                   hlabel, PaintAssistant::OutlinedText);
             }
             
             paint.drawLine(x, y-1, x + w, y-1);
@@ -1040,7 +1041,7 @@
                 labelX = x + 5;
                 labelY = v->getTextLabelHeight(this, paint);
                 if (labelX < nextLabelMinX) {
-                    if (lastLabelY < v->height()/2) {
+                    if (lastLabelY < v->getPaintHeight()/2) {
                         labelY = lastLabelY + fontHeight;
                     }
                 }
@@ -1048,7 +1049,7 @@
                 nextLabelMinX = labelX + paint.fontMetrics().width(label);
             }
 
-            v->drawVisibleText(paint, labelX, labelY, label, View::OutlinedText);
+            PaintAssistant::drawVisibleText(v, paint, labelX, labelY, label, PaintAssistant::OutlinedText);
         }
     }
 
@@ -1056,7 +1057,7 @@
 }
 
 int
-RegionLayer::getVerticalScaleWidth(View *v, bool, QPainter &paint) const
+RegionLayer::getVerticalScaleWidth(LayerGeometryProvider *v, bool, QPainter &paint) const
 {
     if (!m_model || 
         m_verticalScale == AutoAlignScale || 
@@ -1078,7 +1079,7 @@
 }
 
 void
-RegionLayer::paintVerticalScale(View *v, bool, QPainter &paint, QRect) const
+RegionLayer::paintVerticalScale(LayerGeometryProvider *v, bool, QPainter &paint, QRect) const
 {
     if (!m_model || m_model->getPoints().empty()) return;
 
@@ -1121,7 +1122,7 @@
 }
 
 void
-RegionLayer::drawStart(View *v, QMouseEvent *e)
+RegionLayer::drawStart(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_model) return;
 
@@ -1145,7 +1146,7 @@
 }
 
 void
-RegionLayer::drawDrag(View *v, QMouseEvent *e)
+RegionLayer::drawDrag(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_model || !m_editing) return;
 
@@ -1175,7 +1176,7 @@
 }
 
 void
-RegionLayer::drawEnd(View *, QMouseEvent *)
+RegionLayer::drawEnd(LayerGeometryProvider *, QMouseEvent *)
 {
     if (!m_model || !m_editing) return;
     finish(m_editingCommand);
@@ -1186,7 +1187,7 @@
 }
 
 void
-RegionLayer::eraseStart(View *v, QMouseEvent *e)
+RegionLayer::eraseStart(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_model) return;
 
@@ -1202,12 +1203,12 @@
 }
 
 void
-RegionLayer::eraseDrag(View *, QMouseEvent *)
+RegionLayer::eraseDrag(LayerGeometryProvider *, QMouseEvent *)
 {
 }
 
 void
-RegionLayer::eraseEnd(View *v, QMouseEvent *e)
+RegionLayer::eraseEnd(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_model || !m_editing) return;
 
@@ -1229,7 +1230,7 @@
 }
 
 void
-RegionLayer::editStart(View *v, QMouseEvent *e)
+RegionLayer::editStart(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_model) return;
 
@@ -1254,7 +1255,7 @@
 }
 
 void
-RegionLayer::editDrag(View *v, QMouseEvent *e)
+RegionLayer::editDrag(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_model || !m_editing) return;
 
@@ -1289,7 +1290,7 @@
 }
 
 void
-RegionLayer::editEnd(View *, QMouseEvent *)
+RegionLayer::editEnd(LayerGeometryProvider *, QMouseEvent *)
 {
     if (!m_model || !m_editing) return;
 
@@ -1317,7 +1318,7 @@
 }
 
 bool
-RegionLayer::editOpen(View *v, QMouseEvent *e)
+RegionLayer::editOpen(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_model) return false;
 
@@ -1447,7 +1448,7 @@
 }    
 
 void
-RegionLayer::copy(View *v, Selection s, Clipboard &to)
+RegionLayer::copy(LayerGeometryProvider *v, Selection s, Clipboard &to)
 {
     if (!m_model) return;
 
@@ -1465,7 +1466,7 @@
 }
 
 bool
-RegionLayer::paste(View *v, const Clipboard &from, sv_frame_t /* frameOffset */, bool /* interactive */)
+RegionLayer::paste(LayerGeometryProvider *v, const Clipboard &from, sv_frame_t /* frameOffset */, bool /* interactive */)
 {
     if (!m_model) return false;
 
@@ -1476,7 +1477,7 @@
     if (clipboardHasDifferentAlignment(v, from)) {
 
         QMessageBox::StandardButton button =
-            QMessageBox::question(v, tr("Re-align pasted items?"),
+            QMessageBox::question(v->getView(), tr("Re-align pasted items?"),
                                   tr("The items you are pasting came from a layer with different source material from this one.  Do you want to re-align them in time, to match the source material for this layer?"),
                                   QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel,
                                   QMessageBox::Yes);
--- a/layer/RegionLayer.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/RegionLayer.h	Fri Jan 13 10:29:50 2017 +0000
@@ -39,41 +39,41 @@
 public:
     RegionLayer();
 
-    virtual void paint(View *v, QPainter &paint, QRect rect) const;
+    virtual void paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
 
-    virtual int getVerticalScaleWidth(View *v, bool, QPainter &) const;
-    virtual void paintVerticalScale(View *v, bool, QPainter &paint, QRect rect) const;
+    virtual int getVerticalScaleWidth(LayerGeometryProvider *v, bool, QPainter &) const;
+    virtual void paintVerticalScale(LayerGeometryProvider *v, bool, QPainter &paint, QRect rect) const;
 
-    virtual QString getFeatureDescription(View *v, QPoint &) const;
+    virtual QString getFeatureDescription(LayerGeometryProvider *v, QPoint &) const;
     virtual QString getLabelPreceding(sv_frame_t) const;
 
-    virtual bool snapToFeatureFrame(View *v, sv_frame_t &frame,
+    virtual bool snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
 				    int &resolution,
 				    SnapType snap) const;
-    virtual bool snapToSimilarFeature(View *v, sv_frame_t &frame,
+    virtual bool snapToSimilarFeature(LayerGeometryProvider *v, sv_frame_t &frame,
                                       int &resolution,
                                       SnapType snap) const;
 
-    virtual void drawStart(View *v, QMouseEvent *);
-    virtual void drawDrag(View *v, QMouseEvent *);
-    virtual void drawEnd(View *v, QMouseEvent *);
+    virtual void drawStart(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void drawDrag(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void drawEnd(LayerGeometryProvider *v, QMouseEvent *);
 
-    virtual void eraseStart(View *v, QMouseEvent *);
-    virtual void eraseDrag(View *v, QMouseEvent *);
-    virtual void eraseEnd(View *v, QMouseEvent *);
+    virtual void eraseStart(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void eraseDrag(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void eraseEnd(LayerGeometryProvider *v, QMouseEvent *);
 
-    virtual void editStart(View *v, QMouseEvent *);
-    virtual void editDrag(View *v, QMouseEvent *);
-    virtual void editEnd(View *v, QMouseEvent *);
+    virtual void editStart(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void editDrag(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void editEnd(LayerGeometryProvider *v, QMouseEvent *);
 
-    virtual bool editOpen(View *v, QMouseEvent *);
+    virtual bool editOpen(LayerGeometryProvider *v, QMouseEvent *);
 
     virtual void moveSelection(Selection s, sv_frame_t newStartFrame);
     virtual void resizeSelection(Selection s, Selection newSize);
     virtual void deleteSelection(Selection s);
 
-    virtual void copy(View *v, Selection s, Clipboard &to);
-    virtual bool paste(View *v, const Clipboard &from, sv_frame_t frameOffset,
+    virtual void copy(LayerGeometryProvider *v, Selection s, Clipboard &to);
+    virtual bool paste(LayerGeometryProvider *v, const Clipboard &from, sv_frame_t frameOffset,
                        bool interactive);
 
     virtual const Model *getModel() const { return m_model; }
@@ -110,11 +110,11 @@
     void setPlotStyle(PlotStyle style);
     PlotStyle getPlotStyle() const { return m_plotStyle; }
 
-    virtual bool isLayerScrollable(const View *v) const;
+    virtual bool isLayerScrollable(const LayerGeometryProvider *v) const;
 
     virtual bool isLayerEditable() const { return true; }
 
-    virtual int getCompletion(View *) const { return m_model->getCompletion(); }
+    virtual int getCompletion(LayerGeometryProvider *) const { return m_model->getCompletion(); }
 
     virtual bool getValueExtents(double &min, double &max,
                                  bool &log, QString &unit) const;
@@ -127,23 +127,23 @@
     void setProperties(const QXmlAttributes &attributes);
 
     /// VerticalScaleLayer and ColourScaleLayer methods
-    int getYForValue(View *v, double value) const;
-    double getValueForY(View *v, int y) const;
+    int getYForValue(LayerGeometryProvider *v, double value) const;
+    double getValueForY(LayerGeometryProvider *v, int y) const;
     virtual QString getScaleUnits() const;
-    QColor getColourForValue(View *v, double value) const;
+    QColor getColourForValue(LayerGeometryProvider *v, double value) const;
 
 protected slots:
     void recalcSpacing();
 
 protected:
-    double getValueForY(View *v, int y, int avoid) const;
-    void getScaleExtents(View *, double &min, double &max, bool &log) const;
+    double getValueForY(LayerGeometryProvider *v, int y, int avoid) const;
+    void getScaleExtents(LayerGeometryProvider *, double &min, double &max, bool &log) const;
 
     virtual int getDefaultColourHint(bool dark, bool &impose);
 
-    RegionModel::PointList getLocalPoints(View *v, int x) const;
+    RegionModel::PointList getLocalPoints(LayerGeometryProvider *v, int x) const;
 
-    bool getPointToDrag(View *v, int x, int y, RegionModel::Point &) const;
+    bool getPointToDrag(LayerGeometryProvider *v, int x, int y, RegionModel::Point &) const;
 
     RegionModel *m_model;
     bool m_editing;
@@ -166,8 +166,8 @@
     // region value -> number of regions with this value
     SpacingMap m_distributionMap;
 
-    int spacingIndexToY(View *v, int i) const;
-    double yToSpacingIndex(View *v, int y) const;
+    int spacingIndexToY(LayerGeometryProvider *v, int i) const;
+    double yToSpacingIndex(LayerGeometryProvider *v, int y) const;
 
     void finish(RegionModel::EditCommand *command) {
         Command *c = command->finish();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/RenderTimer.h	Fri Jan 13 10:29:50 2017 +0000
@@ -0,0 +1,102 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef RENDER_TIMER_H
+#define RENDER_TIMER_H
+
+#include <chrono>
+
+class RenderTimer
+{
+public:
+    enum Type {
+	/// A normal rendering operation with normal responsiveness demands
+	FastRender,
+
+	/// An operation that the user might accept being slower
+	SlowRender,
+
+	/// An operation that should always complete, i.e. as if there
+	/// were no RenderTimer in use, but without having to change
+	/// client code structurally
+	NoTimeout
+    };
+    
+    /**
+     * Create a new RenderTimer and start timing. Make one of these
+     * before rendering, and then call outOfTime() regularly during
+     * rendering. If outOfTime() returns true, abandon rendering!  and
+     * schedule the rest for after some user responsiveness has
+     * happened.
+     */
+    RenderTimer(Type t) :
+	m_start(std::chrono::steady_clock::now()),
+	m_haveLimits(true),
+	m_minFraction(0.1),
+	m_softLimit(0.1),
+	m_hardLimit(0.2),
+	m_softLimitOverridden(false) {
+
+	if (t == NoTimeout) {
+	    m_haveLimits = false;
+	} else if (t == SlowRender) {
+	    m_softLimit = 0.2;
+	    m_hardLimit = 0.4;
+	}
+    }
+
+
+    /**
+     * Return true if we have run out of time and should suspend
+     * rendering and handle user events instead. Call this regularly
+     * during rendering work: fractionComplete should be an estimate
+     * of how much of the work has been done as of this call, as a
+     * number between 0.0 (none of it) and 1.0 (all of it).
+     */
+    bool outOfTime(double fractionComplete) {
+
+	if (!m_haveLimits || fractionComplete < m_minFraction) {
+	    return false;
+	}
+	
+	auto t = std::chrono::steady_clock::now();
+	double elapsed = std::chrono::duration<double>(t - m_start).count();
+	
+	if (elapsed > m_hardLimit) {
+	    return true;
+	} else if (!m_softLimitOverridden && elapsed > m_softLimit) {
+	    if (fractionComplete > 0.6) {
+		// If we're significantly more than half way by the
+		// time we reach the soft limit, ignore it (though
+		// always respect the hard limit, above). Otherwise
+		// respect the soft limit and report out of time now.
+		m_softLimitOverridden = true;
+	    } else {
+		return true;
+	    }
+	}
+
+	return false;
+    }
+
+private:
+    std::chrono::time_point<std::chrono::steady_clock> m_start;
+    bool m_haveLimits;
+    double m_minFraction;
+    double m_softLimit;
+    double m_hardLimit;
+    bool m_softLimitOverridden;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/ScrollableImageCache.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -0,0 +1,220 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "ScrollableImageCache.h"
+
+#include "base/HitCount.h"
+
+#include <iostream>
+using namespace std;
+
+//#define DEBUG_SCROLLABLE_IMAGE_CACHE 1
+
+void
+ScrollableImageCache::scrollTo(const LayerGeometryProvider *v,
+                               sv_frame_t newStartFrame)
+{
+    static HitCount count("ScrollableImageCache: scrolling");
+    
+    int dx = (v->getXForFrame(m_startFrame) -
+	      v->getXForFrame(newStartFrame));
+    
+#ifdef DEBUG_SCROLLABLE_IMAGE_CACHE
+    cerr << "ScrollableImageCache::scrollTo: start frame " << m_startFrame
+	 << " -> " << newStartFrame << ", dx = " << dx << endl;
+#endif
+
+    if (m_startFrame == newStartFrame) {
+	// haven't moved
+        count.hit();
+        return;
+    }
+	
+    m_startFrame = newStartFrame;
+	
+    if (!isValid()) {
+        count.miss();
+	return;
+    }
+
+    int w = m_image.width();
+
+    if (dx == 0) {
+	// haven't moved visibly (even though start frame may have changed)
+        count.hit();
+	return;
+    }
+
+    if (dx <= -w || dx >= w) {
+	// scrolled entirely off
+	invalidate();
+        count.miss();
+	return;
+    }
+
+    count.partial();
+	
+    // dx is in range, cache is scrollable
+
+    int dxp = dx;
+    if (dxp < 0) dxp = -dxp;
+
+    int copylen = (w - dxp) * int(sizeof(QRgb));
+    for (int y = 0; y < m_image.height(); ++y) {
+	QRgb *line = (QRgb *)m_image.scanLine(y);
+	if (dx < 0) {
+	    memmove(line, line + dxp, copylen);
+	} else {
+	    memmove(line + dxp, line, copylen);
+	}
+    }
+	
+    // update valid area
+        
+    int px = m_validLeft;
+    int pw = m_validWidth;
+	
+    px += dx;
+	
+    if (dx < 0) {
+	// we scrolled left
+	if (px < 0) {
+	    pw += px;
+	    px = 0;
+	    if (pw < 0) {
+		pw = 0;
+	    }
+	}
+    } else {
+	// we scrolled right
+	if (px + pw > w) {
+	    pw = w - px;
+	    if (pw < 0) {
+		pw = 0;
+	    }
+	}
+    }
+
+    m_validLeft = px;
+    m_validWidth = pw;
+}
+
+void
+ScrollableImageCache::adjustToTouchValidArea(int &left, int &width,
+					     bool &isLeftOfValidArea) const
+{
+#ifdef DEBUG_SCROLLABLE_IMAGE_CACHE
+    cerr << "ScrollableImageCache::adjustToTouchValidArea: left " << left
+         << ", width " << width << endl;
+    cerr << "ScrollableImageCache: my left " << m_validLeft
+         << ", width " << m_validWidth << " so right " << (m_validLeft + m_validWidth) << endl;
+#endif
+    if (left < m_validLeft) {
+	isLeftOfValidArea = true;
+	if (left + width <= m_validLeft + m_validWidth) {
+	    width = m_validLeft - left;
+	}
+#ifdef DEBUG_SCROLLABLE_IMAGE_CACHE
+        cerr << "ScrollableImageCache: we're left of valid area, adjusted width to " << width << endl;
+#endif
+    } else {
+	isLeftOfValidArea = false;
+	width = left + width - (m_validLeft + m_validWidth);
+	left = m_validLeft + m_validWidth;
+	if (width < 0) width = 0;
+#ifdef DEBUG_SCROLLABLE_IMAGE_CACHE
+        cerr << "ScrollableImageCache: we're right of valid area, adjusted left to " << left << ", width to " << width << endl;
+#endif
+    }
+}
+    
+void
+ScrollableImageCache::drawImage(int left,
+				int width,
+				QImage image,
+				int imageLeft,
+				int imageWidth)
+{
+    if (image.height() != m_image.height()) {
+	cerr << "ScrollableImageCache::drawImage: ERROR: Supplied image height "
+	     << image.height() << " does not match cache height "
+	     << m_image.height() << endl;
+	throw std::logic_error("Image height must match cache height in ScrollableImageCache::drawImage");
+    }
+    if (left < 0 || width < 0 || left + width > m_image.width()) {
+	cerr << "ScrollableImageCache::drawImage: ERROR: Target area (left = "
+	     << left << ", width = " << width << ", so right = " << left + width
+             << ") out of bounds for cache of width " << m_image.width() << endl;
+	throw std::logic_error("Target area out of bounds in ScrollableImageCache::drawImage");
+    }
+    if (imageLeft < 0 || imageWidth < 0 ||
+	imageLeft + imageWidth > image.width()) {
+	cerr << "ScrollableImageCache::drawImage: ERROR: Source area (left = "
+	     << imageLeft << ", width = " << imageWidth << ", so right = "
+             << imageLeft + imageWidth << ") out of bounds for image of "
+	     << "width " << image.width() << endl;
+	throw std::logic_error("Source area out of bounds in ScrollableImageCache::drawImage");
+    }
+	
+    QPainter painter(&m_image);
+    painter.drawImage(QRect(left, 0, width, m_image.height()),
+		      image,
+		      QRect(imageLeft, 0, imageWidth, image.height()));
+    painter.end();
+
+    if (!isValid()) {
+	m_validLeft = left;
+	m_validWidth = width;
+	return;
+    }
+	
+    if (left < m_validLeft) {
+	if (left + width > m_validLeft + m_validWidth) {
+	    // new image completely contains the old valid area --
+	    // use the new area as is
+	    m_validLeft = left;
+	    m_validWidth = width;
+	} else if (left + width < m_validLeft) {
+	    // new image completely off left of old valid area --
+	    // we can't extend the valid area because the bit in
+	    // between is not valid, so must use the new area only
+	    m_validLeft = left;
+	    m_validWidth = width;
+	} else {
+	    // new image overlaps old valid area on left side --
+	    // use new left edge, and extend width to existing
+	    // right edge
+	    m_validWidth = (m_validLeft + m_validWidth) - left;
+	    m_validLeft = left;
+	}
+    } else {
+	if (left > m_validLeft + m_validWidth) {
+	    // new image completely off right of old valid area --
+	    // we can't extend the valid area because the bit in
+	    // between is not valid, so must use the new area only
+	    m_validLeft = left;
+	    m_validWidth = width;
+	} else if (left + width > m_validLeft + m_validWidth) {
+	    // new image overlaps old valid area on right side --
+	    // use existing left edge, and extend width to new
+	    // right edge
+	    m_validWidth = (left + width) - m_validLeft;
+	    // (m_validLeft unchanged)
+	} else {
+	    // new image completely contained within old valid
+	    // area -- leave the old area unchanged
+	}
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/ScrollableImageCache.h	Fri Jan 13 10:29:50 2017 +0000
@@ -0,0 +1,163 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef SCROLLABLE_IMAGE_CACHE_H
+#define SCROLLABLE_IMAGE_CACHE_H
+
+#include "base/BaseTypes.h"
+
+#include "LayerGeometryProvider.h"
+
+#include <QImage>
+#include <QRect>
+#include <QPainter>
+
+/**
+ * A cached image for a view that scrolls horizontally, such as a
+ * spectrogram. The cache object holds an image, reports the size of
+ * the image (likely the same as the underlying view, but it's the
+ * caller's responsibility to set the size appropriately), can scroll
+ * the image, and can report and update which contiguous horizontal
+ * range of the image is valid.
+ *
+ * The only way to *update* the valid area in a cache is to draw to it
+ * using the drawImage call.
+ */
+class ScrollableImageCache
+{
+public:
+    ScrollableImageCache() :
+	m_validLeft(0),
+	m_validWidth(0),
+	m_startFrame(0),
+	m_zoomLevel(0)
+    {}
+
+    void invalidate() {
+	m_validWidth = 0;
+    }
+    
+    bool isValid() const {
+	return m_validWidth > 0;
+    }
+
+    QSize getSize() const {
+	return m_image.size();
+    }
+
+    /**
+     * Set the size of the cache. If the new size differs from the
+     * current size, the cache is invalidated.
+     */
+    void resize(QSize newSize) {
+        if (getSize() != newSize) {
+            m_image = QImage(newSize, QImage::Format_ARGB32_Premultiplied);
+            invalidate();
+        }
+    }
+	
+    int getValidLeft() const {
+	return m_validLeft;
+    }
+    
+    int getValidWidth() const {
+	return m_validWidth;
+    }
+
+    int getValidRight() const {
+	return m_validLeft + m_validWidth;
+    }
+
+    QRect getValidArea() const {
+	return QRect(m_validLeft, 0, m_validWidth, m_image.height());
+    }
+    
+    int getZoomLevel() const {
+	return m_zoomLevel;
+    }
+
+    /**
+     * Set the zoom level. If the new zoom level differs from the
+     * current one, the cache is invalidated. (Determining whether to
+     * invalidate the cache here is the only thing the zoom level is
+     * used for.)
+     */
+    void setZoomLevel(int zoom) {
+        if (m_zoomLevel != zoom) {
+            m_zoomLevel = zoom;
+            invalidate();
+        }
+    }
+
+    sv_frame_t getStartFrame() const {
+	return m_startFrame;
+    }
+
+    /**
+     * Set the start frame. If the new start frame differs from the
+     * current one, the cache is invalidated. To scroll, i.e. to set
+     * the start frame while retaining cache validity where possible,
+     * use scrollTo() instead.
+     */
+    void setStartFrame(sv_frame_t frame) {
+        if (m_startFrame != frame) {
+            m_startFrame = frame;
+            invalidate();
+        }
+    }
+    
+    const QImage &getImage() const {
+	return m_image;
+    }
+
+    /**
+     * Set the new start frame for the cache, according to the
+     * geometry of the supplied LayerGeometryProvider, if possible
+     * also moving along any existing valid data within the cache so
+     * that it continues to be valid for the new start frame.
+     */
+    void scrollTo(const LayerGeometryProvider *v, sv_frame_t newStartFrame);
+
+    /**
+     * Take a left coordinate and width describing a region, and
+     * adjust them so that they are contiguous with the cache valid
+     * region and so that the union of the adjusted region with the
+     * cache valid region contains the supplied region.  Does not
+     * modify anything about the cache, only about the arguments.
+     */
+    void adjustToTouchValidArea(int &left, int &width,
+				bool &isLeftOfValidArea) const;
+    
+    /**
+     * Draw from an image onto the cache. The supplied image must have
+     * the same height as the cache and the full height is always
+     * drawn. The left and width parameters determine the target
+     * region of the cache, the imageLeft and imageWidth parameters
+     * the source region of the image.
+     */
+    void drawImage(int left,
+		   int width,
+		   QImage image,
+		   int imageLeft,
+		   int imageWidth);
+    
+private:
+    QImage m_image;
+    int m_validLeft;
+    int m_validWidth;
+    sv_frame_t m_startFrame;
+    int m_zoomLevel;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/ScrollableMagRangeCache.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -0,0 +1,122 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "ScrollableMagRangeCache.h"
+
+#include "base/HitCount.h"
+
+#include <iostream>
+using namespace std;
+
+//#define DEBUG_SCROLLABLE_MAG_RANGE_CACHE 1
+
+void
+ScrollableMagRangeCache::scrollTo(const LayerGeometryProvider *v,
+				  sv_frame_t newStartFrame)
+{	
+    static HitCount count("ScrollableMagRangeCache: scrolling");
+    
+    int dx = (v->getXForFrame(m_startFrame) -
+	      v->getXForFrame(newStartFrame));
+
+#ifdef DEBUG_SCROLLABLE_MAG_RANGE_CACHE
+    cerr << "ScrollableMagRangeCache::scrollTo: start frame " << m_startFrame
+	 << " -> " << newStartFrame << ", dx = " << dx << endl;
+#endif
+
+    if (m_startFrame == newStartFrame) {
+	// haven't moved
+        count.hit();
+        return;
+    }
+    
+    m_startFrame = newStartFrame;
+
+    if (dx == 0) {
+	// haven't moved visibly (even though start frame may have changed)
+        count.hit();
+	return;
+    }
+	
+    int w = int(m_ranges.size());
+
+    if (dx <= -w || dx >= w) {
+	// scrolled entirely off
+	invalidate();
+        count.miss();
+	return;
+    }
+
+    count.partial();
+	
+    // dx is in range, cache is scrollable
+
+    if (dx < 0) {
+	// The new start frame is to the left of the old start
+	// frame. We need to add some empty ranges at the left (start)
+	// end and clip the right end. Assemble -dx new values, then
+	// w+dx old values starting at index 0.
+
+	auto newRanges = vector<MagnitudeRange>(-dx);
+	newRanges.insert(newRanges.end(),
+			 m_ranges.begin(), m_ranges.begin() + (w + dx));
+	m_ranges = newRanges;
+	
+    } else {
+	// The new start frame is to the right of the old start
+	// frame. We want to clip the left (start) end and add some
+	// empty ranges at the right end. Assemble w-dx old values
+	// starting at index dx, then dx new values.
+
+	auto newRanges = vector<MagnitudeRange>(dx);
+	newRanges.insert(newRanges.begin(),
+			 m_ranges.begin() + dx, m_ranges.end());
+	m_ranges = newRanges;
+    }
+
+#ifdef DEBUG_SCROLLABLE_MAG_RANGE_CACHE
+    cerr << "maxes (" << m_ranges.size() << ") now: ";
+    for (int i = 0; in_range_for(m_ranges, i); ++i) {
+	cerr << m_ranges[i].getMax() << " ";
+    }
+    cerr << endl;
+#endif
+}
+
+MagnitudeRange
+ScrollableMagRangeCache::getRange(int x, int count) const
+{
+    MagnitudeRange r;
+#ifdef DEBUG_SCROLLABLE_MAG_RANGE_CACHE
+    cerr << "ScrollableMagRangeCache::getRange(" << x << ", " << count << ")" << endl;
+#endif
+    for (int i = 0; i < count; ++i) {
+	r.sample(m_ranges.at(x + i));
+    }
+    return r;
+}
+
+void
+ScrollableMagRangeCache::sampleColumn(int column, const MagnitudeRange &r)
+{
+    if (!in_range_for(m_ranges, column)) {
+	cerr << "ERROR: ScrollableMagRangeCache::sampleColumn: column " << column
+	     << " is out of range for cache of width " << m_ranges.size()
+	     << " (with start frame " << m_startFrame << ")" << endl;
+	throw logic_error("column out of range");
+    } else {
+	m_ranges[column].sample(r);
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/ScrollableMagRangeCache.h	Fri Jan 13 10:29:50 2017 +0000
@@ -0,0 +1,139 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef SCROLLABLE_MAG_RANGE_CACHE_H
+#define SCROLLABLE_MAG_RANGE_CACHE_H
+
+#include "base/BaseTypes.h"
+#include "base/MagnitudeRange.h"
+
+#include "LayerGeometryProvider.h"
+
+/**
+ * A cached set of magnitude range records for a view that scrolls
+ * horizontally, such as a spectrogram. The cache object holds a
+ * magnitude range per column of the view, can report width (likely
+ * the same as the underlying view, but it's the caller's
+ * responsibility to set the size appropriately), can scroll the set
+ * of ranges, and can report and update which columns have had a range
+ * specified.
+ *
+ * The only way to *update* the valid area in a cache is to update the
+ * magnitude range for a column using the sampleColumn call.
+ */
+class ScrollableMagRangeCache
+{
+public:
+    ScrollableMagRangeCache() :
+	m_startFrame(0),
+	m_zoomLevel(0)
+    {}
+
+    void invalidate() {
+	m_ranges = std::vector<MagnitudeRange>(m_ranges.size());
+    }
+    
+    int getWidth() const {
+	return int(m_ranges.size());
+    }
+
+    /**
+     * Set the width of the cache in columns. If the new size differs
+     * from the current size, the cache is invalidated.
+     */
+    void resize(int newWidth) {
+        if (getWidth() != newWidth) {
+	    m_ranges = std::vector<MagnitudeRange>(newWidth);
+        }
+    }
+	
+    int getZoomLevel() const {
+	return m_zoomLevel;
+    }
+
+    /**
+     * Set the zoom level. If the new zoom level differs from the
+     * current one, the cache is invalidated. (Determining whether to
+     * invalidate the cache here is the only thing the zoom level is
+     * used for.)
+     */
+    void setZoomLevel(int zoom) {
+        if (m_zoomLevel != zoom) {
+            m_zoomLevel = zoom;
+            invalidate();
+        }
+    }
+
+    sv_frame_t getStartFrame() const {
+	return m_startFrame;
+    }
+
+    /**
+     * Set the start frame. If the new start frame differs from the
+     * current one, the cache is invalidated. To scroll, i.e. to set
+     * the start frame while retaining cache validity where possible,
+     * use scrollTo() instead.
+     */
+    void setStartFrame(sv_frame_t frame) {
+        if (m_startFrame != frame) {
+            m_startFrame = frame;
+            invalidate();
+        }
+    }
+
+    bool isColumnSet(int column) const {
+	return in_range_for(m_ranges, column) && m_ranges.at(column).isSet();
+    }
+
+    bool areColumnsSet(int x, int count) const {
+	for (int i = 0; i < count; ++i) {
+	    if (!isColumnSet(x + i)) return false;
+	}
+	return true;
+    }
+    
+    /**
+     * Get the magnitude range for a single column.
+     */
+    MagnitudeRange getRange(int column) const {
+	return m_ranges.at(column);
+    }
+
+    /**
+     * Get the magnitude range for a range of columns.
+     */
+    MagnitudeRange getRange(int x, int count) const;
+    
+    /**
+     * Set the new start frame for the cache, according to the
+     * geometry of the supplied LayerGeometryProvider, if possible
+     * also moving along any existing valid data within the cache so
+     * that it continues to be valid for the new start frame.
+     */
+    void scrollTo(const LayerGeometryProvider *v, sv_frame_t newStartFrame);
+    
+    /**
+     * Update a column in the cache, by column index. (Column zero is
+     * the first column in the cache, it has nothing to do with any
+     * underlying model that the cache may be used with.)
+     */
+    void sampleColumn(int column, const MagnitudeRange &r);
+    
+private:
+    std::vector<MagnitudeRange> m_ranges;
+    sv_frame_t m_startFrame;
+    int m_zoomLevel;
+};
+
+#endif
--- a/layer/SingleColourLayer.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/SingleColourLayer.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -137,7 +137,7 @@
 }
 
 void
-SingleColourLayer::setDefaultColourFor(View *v)
+SingleColourLayer::setDefaultColourFor(LayerGeometryProvider *v)
 {
 #ifdef DEBUG_COLOUR_SELECTION
     SVDEBUG << "SingleColourLayer::setDefaultColourFor: m_colourExplicitlySet = " << m_colourExplicitlySet << ", m_defaultColourSet " << m_defaultColourSet << endl;
@@ -244,19 +244,19 @@
 }
 
 QColor
-SingleColourLayer::getBackgroundQColor(View *v) const
+SingleColourLayer::getBackgroundQColor(LayerGeometryProvider *v) const
 {
     return v->getBackground();
 }
 
 QColor
-SingleColourLayer::getForegroundQColor(View *v) const
+SingleColourLayer::getForegroundQColor(LayerGeometryProvider *v) const
 {
     return v->getForeground();
 }
 
 std::vector<QColor>
-SingleColourLayer::getPartialShades(View *v) const
+SingleColourLayer::getPartialShades(LayerGeometryProvider *v) const
 {
     std::vector<QColor> s;
     QColor base = getBaseQColor();
--- a/layer/SingleColourLayer.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/SingleColourLayer.h	Fri Jan 13 10:29:50 2017 +0000
@@ -71,16 +71,16 @@
 
     virtual void setProperties(const QXmlAttributes &attributes);
 
-    virtual void setDefaultColourFor(View *v);
+    virtual void setDefaultColourFor(LayerGeometryProvider *v);
 
 protected:
     SingleColourLayer();
     virtual ~SingleColourLayer();
 
     virtual QColor getBaseQColor() const;
-    virtual QColor getBackgroundQColor(View *v) const;
-    virtual QColor getForegroundQColor(View *v) const;
-    std::vector<QColor> getPartialShades(View *v) const;
+    virtual QColor getBackgroundQColor(LayerGeometryProvider *v) const;
+    virtual QColor getForegroundQColor(LayerGeometryProvider *v) const;
+    std::vector<QColor> getPartialShades(LayerGeometryProvider *v) const;
 
     virtual void flagBaseColourChanged() { }
     virtual int getDefaultColourHint(bool /* darkBackground */,
--- a/layer/SliceLayer.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/SliceLayer.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -92,14 +92,14 @@
 }
 
 QString
-SliceLayer::getFeatureDescription(View *v, QPoint &p) const
+SliceLayer::getFeatureDescription(LayerGeometryProvider *v, QPoint &p) const
 {
     int minbin, maxbin, range;
     return getFeatureDescriptionAux(v, p, true, minbin, maxbin, range);
 }
 
 QString
-SliceLayer::getFeatureDescriptionAux(View *v, QPoint &p,
+SliceLayer::getFeatureDescriptionAux(LayerGeometryProvider *v, QPoint &p,
                                      bool includeBinDescription,
                                      int &minbin, int &maxbin, int &range) const
 {
@@ -108,7 +108,7 @@
     if (!m_sliceableModel) return "";
 
     int xorigin = m_xorigins[v];
-    int w = v->width() - xorigin - 1;
+    int w = v->getPaintWidth() - xorigin - 1;
     
     int mh = m_sliceableModel->getHeight();
     minbin = getBinForX(p.x() - xorigin, mh, w);
@@ -226,7 +226,7 @@
 }
 
 double
-SliceLayer::getYForValue(double value, const View *v, double &norm) const
+SliceLayer::getYForValue(double value, const LayerGeometryProvider *v, double &norm) const
 {
     norm = 0.0;
 
@@ -276,7 +276,7 @@
 }
 
 double
-SliceLayer::getValueForY(double y, const View *v) const
+SliceLayer::getValueForY(double y, const LayerGeometryProvider *v) const
 {
     double value = 0.0;
 
@@ -313,7 +313,7 @@
 }
 
 void
-SliceLayer::paint(View *v, QPainter &paint, QRect rect) const
+SliceLayer::paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const
 {
     if (!m_sliceableModel || !m_sliceableModel->isOK() ||
         !m_sliceableModel->isReady()) return;
@@ -334,11 +334,11 @@
     paint.setPen(getBaseQColor());
 
     int xorigin = getVerticalScaleWidth(v, true, paint) + 1;
-    int w = v->width() - xorigin - 1;
+    int w = v->getPaintWidth() - xorigin - 1;
 
     m_xorigins[v] = xorigin; // for use in getFeatureDescription
     
-    int yorigin = v->height() - 20 - paint.fontMetrics().height() - 7;
+    int yorigin = v->getPaintHeight() - 20 - paint.fontMetrics().height() - 7;
     int h = yorigin - paint.fontMetrics().height() - 8;
 
     m_yorigins[v] = yorigin; // for getYForValue etc
@@ -485,23 +485,23 @@
         v->drawVisibleText
             (paint, xorigin + 5,
              paint.fontMetrics().ascent() + 5,
-             startText, View::OutlinedText);
+             startText, PaintAssistant::OutlinedText);
         
         v->drawVisibleText
             (paint, xorigin + 5,
              paint.fontMetrics().ascent() + paint.fontMetrics().height() + 10,
-             endText, View::OutlinedText);
+             endText, PaintAssistant::OutlinedText);
         
         v->drawVisibleText
             (paint, xorigin + 5,
              paint.fontMetrics().ascent() + 2*paint.fontMetrics().height() + 15,
-             durationText, View::OutlinedText);
+             durationText, PaintAssistant::OutlinedText);
     }
 */
 }
 
 int
-SliceLayer::getVerticalScaleWidth(View *, bool, QPainter &paint) const
+SliceLayer::getVerticalScaleWidth(LayerGeometryProvider *, bool, QPainter &paint) const
 {
     if (m_energyScale == LinearScale || m_energyScale == AbsoluteScale) {
 	return std::max(paint.fontMetrics().width("0.0") + 13,
@@ -513,7 +513,7 @@
 }
 
 void
-SliceLayer::paintVerticalScale(View *v, bool, QPainter &paint, QRect rect) const
+SliceLayer::paintVerticalScale(LayerGeometryProvider *v, bool, QPainter &paint, QRect rect) const
 {
     double thresh = m_threshold;
     if (m_energyScale != LinearScale && m_energyScale != AbsoluteScale) {
@@ -523,7 +523,7 @@
 //    int h = (rect.height() * 3) / 4;
 //    int y = (rect.height() / 2) - (h / 2);
     
-    int yorigin = v->height() - 20 - paint.fontMetrics().height() - 6;
+    int yorigin = v->getPaintHeight() - 20 - paint.fontMetrics().height() - 6;
     int h = yorigin - paint.fontMetrics().height() - 8;
     if (h < 0) return;
 
--- a/layer/SliceLayer.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/SliceLayer.h	Fri Jan 13 10:29:50 2017 +0000
@@ -38,12 +38,12 @@
 
     void setSliceableModel(const Model *model);    
 
-    virtual void paint(View *v, QPainter &paint, QRect rect) const;
+    virtual void paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
 
-    virtual QString getFeatureDescription(View *v, QPoint &) const;
+    virtual QString getFeatureDescription(LayerGeometryProvider *v, QPoint &) const;
 
-    virtual int getVerticalScaleWidth(View *v, bool, QPainter &) const;
-    virtual void paintVerticalScale(View *v, bool, QPainter &paint, QRect rect) const;
+    virtual int getVerticalScaleWidth(LayerGeometryProvider *v, bool, QPainter &) const;
+    virtual void paintVerticalScale(LayerGeometryProvider *v, bool, QPainter &paint, QRect rect) const;
 
     virtual ColourSignificance getLayerColourSignificance() const {
         return ColourAndBackgroundSignificant;
@@ -67,7 +67,7 @@
 
     virtual bool hasTimeXAxis() const { return false; }
 
-    virtual bool isLayerScrollable(const View *) const { return false; }
+    virtual bool isLayerScrollable(const LayerGeometryProvider *) const { return false; }
 
     enum EnergyScale { LinearScale, MeterScale, dBScale, AbsoluteScale };
 
@@ -112,10 +112,10 @@
     virtual double getXForBin(int bin, int totalBins, double w) const;
     virtual int getBinForX(double x, int totalBins, double w) const;
 
-    virtual double getYForValue(double value, const View *v, double &norm) const;
-    virtual double getValueForY(double y, const View *v) const;
+    virtual double getYForValue(double value, const LayerGeometryProvider *v, double &norm) const;
+    virtual double getValueForY(double y, const LayerGeometryProvider *v) const;
     
-    virtual QString getFeatureDescriptionAux(View *v, QPoint &,
+    virtual QString getFeatureDescriptionAux(LayerGeometryProvider *v, QPoint &,
                                              bool includeBinDescription,
                                              int &minbin, int &maxbin,
                                              int &range) const;
@@ -141,9 +141,9 @@
     float                             m_initialThreshold;
     float                             m_gain;
     mutable std::vector<int>          m_scalePoints;
-    mutable std::map<const View *, int> m_xorigins;
-    mutable std::map<const View *, int> m_yorigins;
-    mutable std::map<const View *, int> m_heights;
+    mutable std::map<const LayerGeometryProvider *, int> m_xorigins;
+    mutable std::map<const LayerGeometryProvider *, int> m_yorigins;
+    mutable std::map<const LayerGeometryProvider *, int> m_heights;
     mutable sv_frame_t                m_currentf0;
     mutable sv_frame_t                m_currentf1;
     mutable std::vector<float>        m_values;
--- a/layer/SpectrogramLayer.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/SpectrogramLayer.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -23,34 +23,37 @@
 #include "base/Preferences.h"
 #include "base/RangeMapper.h"
 #include "base/LogRange.h"
+#include "base/ColumnOp.h"
+#include "base/Strings.h"
+#include "base/StorageAdviser.h"
+#include "base/Exceptions.h"
 #include "widgets/CommandHistory.h"
+#include "data/model/Dense3DModelPeakCache.h"
+
 #include "ColourMapper.h"
-#include "ImageRegionFinder.h"
-#include "data/model/Dense3DModelPeakCache.h"
 #include "PianoScale.h"
+#include "PaintAssistant.h"
+#include "Colour3DPlotRenderer.h"
 
 #include <QPainter>
 #include <QImage>
 #include <QPixmap>
 #include <QRect>
-#include <QTimer>
 #include <QApplication>
 #include <QMessageBox>
 #include <QMouseEvent>
 #include <QTextStream>
+#include <QSettings>
 
 #include <iostream>
 
 #include <cassert>
 #include <cmath>
 
-#ifndef __GNUC__
-#include <alloca.h>
-#endif
-
+//#define DEBUG_SPECTROGRAM 1
 //#define DEBUG_SPECTROGRAM_REPAINT 1
 
-using std::vector;
+using namespace std;
 
 SpectrogramLayer::SpectrogramLayer(Configuration config) :
     m_model(0),
@@ -58,33 +61,34 @@
     m_windowSize(1024),
     m_windowType(HanningWindow),
     m_windowHopLevel(2),
-    m_zeroPadLevel(0),
-    m_fftSize(1024),
     m_gain(1.0),
     m_initialGain(1.0),
-    m_threshold(0.0),
-    m_initialThreshold(0.0),
+    m_threshold(1.0e-8f),
+    m_initialThreshold(1.0e-8f),
     m_colourRotation(0),
     m_initialRotation(0),
     m_minFrequency(10),
     m_maxFrequency(8000),
     m_initialMaxFrequency(8000),
-    m_colourScale(dBColourScale),
+    m_colourScale(ColourScaleType::Log),
+    m_colourScaleMultiple(1.0),
     m_colourMap(0),
-    m_frequencyScale(LinearFrequencyScale),
-    m_binDisplay(AllBins),
-    m_normalizeColumns(false),
+    m_binScale(BinScale::Linear),
+    m_binDisplay(BinDisplay::AllBins),
+    m_normalization(ColumnNormalization::None),
     m_normalizeVisibleArea(false),
-    m_normalizeHybrid(false),
     m_lastEmittedZoomStep(-1),
     m_synchronous(false),
     m_haveDetailedScale(false),
-    m_lastPaintBlockWidth(0),
-    m_updateTimer(0),
-    m_candidateFillStartFrame(0),
     m_exiting(false),
-    m_sliceableModel(0)
+    m_fftModel(0),
+    m_wholeCache(0),
+    m_peakCache(0),
+    m_peakCacheDivisor(8)
 {
+    QString colourConfigName = "spectrogram-colour";
+    int colourConfigDefault = int(ColourMapper::Green);
+    
     if (config == FullRangeDb) {
         m_initialMaxFrequency = 0;
         setMaxFrequency(0);
@@ -94,9 +98,11 @@
         m_initialMaxFrequency = 1500;
 	setMaxFrequency(1500);
         setMinFrequency(40);
-	setColourScale(LinearColourScale);
+	setColourScale(ColourScaleType::Linear);
         setColourMap(ColourMapper::Sunset);
-        setFrequencyScale(LogFrequencyScale);
+        setBinScale(BinScale::Log);
+        colourConfigName = "spectrogram-melodic-colour";
+        colourConfigDefault = int(ColourMapper::Sunset);
 //        setGain(20);
     } else if (config == MelodicPeaks) {
 	setWindowSize(4096);
@@ -104,26 +110,85 @@
         m_initialMaxFrequency = 2000;
 	setMaxFrequency(2000);
 	setMinFrequency(40);
-	setFrequencyScale(LogFrequencyScale);
-	setColourScale(LinearColourScale);
-	setBinDisplay(PeakFrequencies);
-	setNormalizeColumns(true);
+	setBinScale(BinScale::Log);
+	setColourScale(ColourScaleType::Linear);
+	setBinDisplay(BinDisplay::PeakFrequencies);
+        setNormalization(ColumnNormalization::Max1);
+        colourConfigName = "spectrogram-melodic-colour";
+        colourConfigDefault = int(ColourMapper::Sunset);
     }
 
+    QSettings settings;
+    settings.beginGroup("Preferences");
+    setColourMap(settings.value(colourConfigName, colourConfigDefault).toInt());
+    settings.endGroup();
+    
     Preferences *prefs = Preferences::getInstance();
     connect(prefs, SIGNAL(propertyChanged(PropertyContainer::PropertyName)),
             this, SLOT(preferenceChanged(PropertyContainer::PropertyName)));
     setWindowType(prefs->getWindowType());
-
-    initialisePalette();
 }
 
 SpectrogramLayer::~SpectrogramLayer()
 {
-    delete m_updateTimer;
-    m_updateTimer = 0;
-    
-    invalidateFFTModels();
+    invalidateRenderers();
+
+    delete m_fftModel;
+    delete m_peakCache;
+    delete m_wholeCache;
+}
+
+pair<ColourScaleType, double>
+SpectrogramLayer::convertToColourScale(int value)
+{
+    switch (value) {
+    case 0: return { ColourScaleType::Linear, 1.0 };
+    case 1: return { ColourScaleType::Meter, 1.0 };
+    case 2: return { ColourScaleType::Log, 2.0 }; // dB^2 (i.e. log of power)
+    case 3: return { ColourScaleType::Log, 1.0 }; // dB   (of magnitude)
+    case 4: return { ColourScaleType::Phase, 1.0 };
+    default: return { ColourScaleType::Linear, 1.0 };
+    }
+}
+
+int
+SpectrogramLayer::convertFromColourScale(ColourScaleType scale, double multiple)
+{
+    switch (scale) {
+    case ColourScaleType::Linear: return 0;
+    case ColourScaleType::Meter: return 1;
+    case ColourScaleType::Log: return (multiple > 1.5 ? 2 : 3);
+    case ColourScaleType::Phase: return 4;
+    case ColourScaleType::PlusMinusOne:
+    case ColourScaleType::Absolute:
+    default: return 0;
+    }
+}
+
+std::pair<ColumnNormalization, bool>
+SpectrogramLayer::convertToColumnNorm(int value)
+{
+    switch (value) {
+    default:
+    case 0: return { ColumnNormalization::None, false };
+    case 1: return { ColumnNormalization::Max1, false };
+    case 2: return { ColumnNormalization::None, true }; // visible area
+    case 3: return { ColumnNormalization::Hybrid, false };
+    }
+}
+
+int
+SpectrogramLayer::convertFromColumnNorm(ColumnNormalization norm, bool visible)
+{
+    if (visible) return 2;
+    switch (norm) {
+    case ColumnNormalization::None: return 0;
+    case ColumnNormalization::Max1: return 1;
+    case ColumnNormalization::Hybrid: return 3;
+
+    case ColumnNormalization::Sum1:
+    default: return 0;
+    }
 }
 
 void
@@ -134,7 +199,8 @@
     if (model == m_model) return;
 
     m_model = model;
-    invalidateFFTModels();
+
+    recreateFFTModel();
 
     if (!m_model || !m_model->isOK()) return;
 
@@ -155,8 +221,7 @@
     list.push_back("Colour Scale");
     list.push_back("Window Size");
     list.push_back("Window Increment");
-    list.push_back("Normalize Columns");
-    list.push_back("Normalize Visible Area");
+    list.push_back("Normalization");
     list.push_back("Bin Display");
     list.push_back("Threshold");
     list.push_back("Gain");
@@ -164,7 +229,6 @@
 //    list.push_back("Min Frequency");
 //    list.push_back("Max Frequency");
     list.push_back("Frequency Scale");
-////    list.push_back("Zero Padding");
     return list;
 }
 
@@ -175,8 +239,7 @@
     if (name == "Colour Scale") return tr("Colour Scale");
     if (name == "Window Size") return tr("Window Size");
     if (name == "Window Increment") return tr("Window Overlap");
-    if (name == "Normalize Columns") return tr("Normalize Columns");
-    if (name == "Normalize Visible Area") return tr("Normalize Visible Area");
+    if (name == "Normalization") return tr("Normalization");
     if (name == "Bin Display") return tr("Bin Display");
     if (name == "Threshold") return tr("Threshold");
     if (name == "Gain") return tr("Gain");
@@ -184,15 +247,12 @@
     if (name == "Min Frequency") return tr("Min Frequency");
     if (name == "Max Frequency") return tr("Max Frequency");
     if (name == "Frequency Scale") return tr("Frequency Scale");
-    if (name == "Zero Padding") return tr("Smoothing");
     return "";
 }
 
 QString
-SpectrogramLayer::getPropertyIconName(const PropertyName &name) const
+SpectrogramLayer::getPropertyIconName(const PropertyName &) const
 {
-    if (name == "Normalize Columns") return "normalise-columns";
-    if (name == "Normalize Visible Area") return "normalise";
     return "";
 }
 
@@ -201,10 +261,8 @@
 {
     if (name == "Gain") return RangeProperty;
     if (name == "Colour Rotation") return RangeProperty;
-    if (name == "Normalize Columns") return ToggleProperty;
-    if (name == "Normalize Visible Area") return ToggleProperty;
     if (name == "Threshold") return RangeProperty;
-    if (name == "Zero Padding") return ToggleProperty;
+    if (name == "Colour") return ColourMapProperty;
     return ValueProperty;
 }
 
@@ -214,13 +272,11 @@
     if (name == "Bin Display" ||
         name == "Frequency Scale") return tr("Bins");
     if (name == "Window Size" ||
-	name == "Window Increment" ||
-        name == "Zero Padding") return tr("Window");
+	name == "Window Increment") return tr("Window");
     if (name == "Colour" ||
 	name == "Threshold" ||
 	name == "Colour Rotation") return tr("Colour");
-    if (name == "Normalize Columns" ||
-        name == "Normalize Visible Area" ||
+    if (name == "Normalization" ||
         name == "Gain" ||
 	name == "Colour Scale") return tr("Scale");
     return QString();
@@ -252,8 +308,8 @@
 
     } else if (name == "Threshold") {
 
-	*min = -50;
-	*max = 0;
+	*min = -81;
+	*max = -1;
 
         *deflt = int(lrint(AudioLevel::multiplier_to_dB(m_initialThreshold)));
 	if (*deflt < *min) *deflt = *min;
@@ -273,11 +329,12 @@
 
     } else if (name == "Colour Scale") {
 
+        // linear, meter, db^2, db, phase
 	*min = 0;
 	*max = 4;
-        *deflt = int(dBColourScale);
-
-	val = (int)m_colourScale;
+        *deflt = 2;
+
+	val = convertFromColourScale(m_colourScale, m_colourScaleMultiple);
 
     } else if (name == "Colour") {
 
@@ -305,14 +362,6 @@
 
         val = m_windowHopLevel;
     
-    } else if (name == "Zero Padding") {
-	
-	*min = 0;
-	*max = 1;
-        *deflt = 0;
-	
-        val = m_zeroPadLevel > 0 ? 1 : 0;
-    
     } else if (name == "Min Frequency") {
 
 	*min = 0;
@@ -355,25 +404,23 @@
 
 	*min = 0;
 	*max = 1;
-        *deflt = int(LinearFrequencyScale);
-	val = (int)m_frequencyScale;
+        *deflt = int(BinScale::Linear);
+	val = (int)m_binScale;
 
     } else if (name == "Bin Display") {
 
 	*min = 0;
 	*max = 2;
-        *deflt = int(AllBins);
+        *deflt = int(BinDisplay::AllBins);
 	val = (int)m_binDisplay;
 
-    } else if (name == "Normalize Columns") {
+    } else if (name == "Normalization") {
 	
+        *min = 0;
+        *max = 3;
         *deflt = 0;
-	val = (m_normalizeColumns ? 1 : 0);
-
-    } else if (name == "Normalize Visible Area") {
-	
-        *deflt = 0;
-	val = (m_normalizeVisibleArea ? 1 : 0);
+        
+        val = convertFromColumnNorm(m_normalization, m_normalizeVisibleArea);
 
     } else {
 	val = Layer::getPropertyRangeAndValue(name, min, max, deflt);
@@ -399,6 +446,16 @@
 	case 4: return tr("Phase");
 	}
     }
+    if (name == "Normalization") {
+        switch(value) {
+        default:
+        case 0: return tr("None");
+        case 1: return tr("Col");
+        case 2: return tr("View");
+        case 3: return tr("Hybrid");
+        }
+//        return ""; // icon only
+    }
     if (name == "Window Size") {
 	return QString("%1").arg(32 << value);
     }
@@ -413,10 +470,6 @@
 	case 5: return tr("93.75 %");
 	}
     }
-    if (name == "Zero Padding") {
-        if (value == 0) return tr("None");
-        return QString("%1x").arg(value + 1);
-    }
     if (name == "Min Frequency") {
 	switch (value) {
 	default:
@@ -465,6 +518,22 @@
     return tr("<unknown>");
 }
 
+QString
+SpectrogramLayer::getPropertyValueIconName(const PropertyName &name,
+                                           int value) const
+{
+    if (name == "Normalization") {
+        switch(value) {
+        default:
+        case 0: return "normalise-none";
+        case 1: return "normalise-columns";
+        case 2: return "normalise";
+        case 3: return "normalise-hybrid";
+        }
+    }
+    return "";
+}
+
 RangeMapper *
 SpectrogramLayer::getNewPropertyRangeMapper(const PropertyName &name) const
 {
@@ -472,7 +541,8 @@
         return new LinearRangeMapper(-50, 50, -25, 25, tr("dB"));
     }
     if (name == "Threshold") {
-        return new LinearRangeMapper(-50, 0, -50, 0, tr("dB"));
+        return new LinearRangeMapper(-81, -1, -81, -1, tr("dB"), false,
+                                     { { -81, Strings::minus_infinity } });
     }
     return 0;
 }
@@ -483,7 +553,7 @@
     if (name == "Gain") {
 	setGain(float(pow(10, float(value)/20.0)));
     } else if (name == "Threshold") {
-	if (value == -50) setThreshold(0.0);
+	if (value == -81) setThreshold(0.0);
 	else setThreshold(float(AudioLevel::dB_to_multiplier(value)));
     } else if (name == "Colour Rotation") {
 	setColourRotation(value);
@@ -493,8 +563,6 @@
 	setWindowSize(32 << value);
     } else if (name == "Window Increment") {
         setWindowHopLevel(value);
-    } else if (name == "Zero Padding") {
-        setZeroPadLevel(value > 0.1 ? 3 : 0);
     } else if (name == "Min Frequency") {
 	switch (value) {
 	default:
@@ -534,108 +602,50 @@
             m_lastEmittedZoomStep = vs;
         }
     } else if (name == "Colour Scale") {
+        setColourScaleMultiple(1.0);
 	switch (value) {
 	default:
-	case 0: setColourScale(LinearColourScale); break;
-	case 1: setColourScale(MeterColourScale); break;
-	case 2: setColourScale(dBSquaredColourScale); break;
-	case 3: setColourScale(dBColourScale); break;
-	case 4: setColourScale(PhaseColourScale); break;
+	case 0: setColourScale(ColourScaleType::Linear); break;
+	case 1: setColourScale(ColourScaleType::Meter); break;
+	case 2:
+            setColourScale(ColourScaleType::Log);
+            setColourScaleMultiple(2.0);
+            break;
+	case 3: setColourScale(ColourScaleType::Log); break;
+	case 4: setColourScale(ColourScaleType::Phase); break;
 	}
     } else if (name == "Frequency Scale") {
 	switch (value) {
 	default:
-	case 0: setFrequencyScale(LinearFrequencyScale); break;
-	case 1: setFrequencyScale(LogFrequencyScale); break;
+	case 0: setBinScale(BinScale::Linear); break;
+	case 1: setBinScale(BinScale::Log); break;
 	}
     } else if (name == "Bin Display") {
 	switch (value) {
 	default:
-	case 0: setBinDisplay(AllBins); break;
-	case 1: setBinDisplay(PeakBins); break;
-	case 2: setBinDisplay(PeakFrequencies); break;
+	case 0: setBinDisplay(BinDisplay::AllBins); break;
+	case 1: setBinDisplay(BinDisplay::PeakBins); break;
+	case 2: setBinDisplay(BinDisplay::PeakFrequencies); break;
 	}
-    } else if (name == "Normalize Columns") {
-	setNormalizeColumns(value ? true : false);
-    } else if (name == "Normalize Visible Area") {
-	setNormalizeVisibleArea(value ? true : false);
+    } else if (name == "Normalization") {
+        auto n = convertToColumnNorm(value);
+        setNormalization(n.first);
+        setNormalizeVisibleArea(n.second);
     }
 }
 
 void
-SpectrogramLayer::invalidateImageCaches()
+SpectrogramLayer::invalidateRenderers()
 {
-    for (ViewImageCache::iterator i = m_imageCaches.begin();
-         i != m_imageCaches.end(); ++i) {
-        i->second.validArea = QRect();
+#ifdef DEBUG_SPECTROGRAM
+    cerr << "SpectrogramLayer::invalidateRenderers called" << endl;
+#endif
+
+    for (ViewRendererMap::iterator i = m_renderers.begin();
+         i != m_renderers.end(); ++i) {
+        delete i->second;
     }
-}
-
-void
-SpectrogramLayer::invalidateImageCaches(sv_frame_t startFrame, sv_frame_t endFrame)
-{
-    for (ViewImageCache::iterator i = m_imageCaches.begin();
-         i != m_imageCaches.end(); ++i) {
-
-        //!!! when are views removed from the map? on setLayerDormant?
-        const View *v = i->first;
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        SVDEBUG << "SpectrogramLayer::invalidateImageCaches(" 
-                  << startFrame << ", " << endFrame << "): view range is "
-                  << v->getStartFrame() << ", " << v->getEndFrame()
-                  << endl;
-
-        cerr << "Valid area was: " << i->second.validArea.x() << ", "
-                  << i->second.validArea.y() << " "
-                  << i->second.validArea.width() << "x"
-                  << i->second.validArea.height() << endl;
-#endif
-
-        if (int(startFrame) > v->getStartFrame()) {
-            if (startFrame >= v->getEndFrame()) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                cerr << "Modified start frame is off right of view" << endl;
-#endif
-                return;
-            }
-            int x = v->getXForFrame(startFrame);
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            SVDEBUG << "clipping from 0 to " << x-1 << endl;
-#endif
-            if (x > 1) {
-                i->second.validArea &=
-                    QRect(0, 0, x-1, v->height());
-            } else {
-                i->second.validArea = QRect();
-            }
-        } else {
-            if (int(endFrame) < v->getStartFrame()) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                cerr << "Modified end frame is off left of view" << endl;
-#endif
-                return;
-            }
-            int x = v->getXForFrame(endFrame);
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            SVDEBUG << "clipping from " << x+1 << " to " << v->width()
-                      << endl;
-#endif
-            if (x < v->width()) {
-                i->second.validArea &=
-                    QRect(x+1, 0, v->width()-(x+1), v->height());
-            } else {
-                i->second.validArea = QRect();
-            }
-        }
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "Valid area is now: " << i->second.validArea.x() << ", "
-                  << i->second.validArea.y() << " "
-                  << i->second.validArea.width() << "x"
-                  << i->second.validArea.height() << endl;
-#endif
-    }
+    m_renderers.clear();
 }
 
 void
@@ -648,12 +658,13 @@
         return;
     }
     if (name == "Spectrogram Y Smoothing") {
-        invalidateImageCaches();
+        setWindowSize(m_windowSize);
+        invalidateRenderers();
         invalidateMagnitudes();
         emit layerParametersChanged();
     }
     if (name == "Spectrogram X Smoothing") {
-        invalidateImageCaches();
+        invalidateRenderers();
         invalidateMagnitudes();
         emit layerParametersChanged();
     }
@@ -667,9 +678,9 @@
 {
     if (m_channel == ch) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     m_channel = ch;
-    invalidateFFTModels();
+    recreateFFTModel();
 
     emit layerParametersChanged();
 }
@@ -680,17 +691,40 @@
     return m_channel;
 }
 
+int
+SpectrogramLayer::getFFTOversampling() const
+{
+    if (m_binDisplay != BinDisplay::AllBins) {
+        return 1;
+    }
+
+    Preferences::SpectrogramSmoothing smoothing = 
+        Preferences::getInstance()->getSpectrogramSmoothing();
+    
+    if (smoothing == Preferences::NoSpectrogramSmoothing ||
+        smoothing == Preferences::SpectrogramInterpolated) {
+        return 1;
+    }
+
+    return 4;
+}
+
+int
+SpectrogramLayer::getFFTSize() const
+{
+    return m_windowSize * getFFTOversampling();
+}
+
 void
 SpectrogramLayer::setWindowSize(int ws)
 {
     if (m_windowSize == ws) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     
     m_windowSize = ws;
-    m_fftSize = ws * (m_zeroPadLevel + 1);
     
-    invalidateFFTModels();
+    recreateFFTModel();
 
     emit layerParametersChanged();
 }
@@ -706,15 +740,13 @@
 {
     if (m_windowHopLevel == v) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     
     m_windowHopLevel = v;
     
-    invalidateFFTModels();
+    recreateFFTModel();
 
     emit layerParametersChanged();
-
-//    fillCache();
 }
 
 int
@@ -724,36 +756,15 @@
 }
 
 void
-SpectrogramLayer::setZeroPadLevel(int v)
-{
-    if (m_zeroPadLevel == v) return;
-
-    invalidateImageCaches();
-    
-    m_zeroPadLevel = v;
-    m_fftSize = m_windowSize * (v + 1);
-
-    invalidateFFTModels();
-
-    emit layerParametersChanged();
-}
-
-int
-SpectrogramLayer::getZeroPadLevel() const
-{
-    return m_zeroPadLevel;
-}
-
-void
 SpectrogramLayer::setWindowType(WindowType w)
 {
     if (m_windowType == w) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     
     m_windowType = w;
 
-    invalidateFFTModels();
+    recreateFFTModel();
 
     emit layerParametersChanged();
 }
@@ -772,7 +783,7 @@
 
     if (m_gain == gain) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     
     m_gain = gain;
     
@@ -790,7 +801,7 @@
 {
     if (m_threshold == threshold) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     
     m_threshold = threshold;
 
@@ -810,7 +821,7 @@
 
 //    SVDEBUG << "SpectrogramLayer::setMinFrequency: " << mf << endl;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     invalidateMagnitudes();
     
     m_minFrequency = mf;
@@ -831,7 +842,7 @@
 
 //    SVDEBUG << "SpectrogramLayer::setMaxFrequency: " << mf << endl;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     invalidateMagnitudes();
     
     m_maxFrequency = mf;
@@ -848,47 +859,67 @@
 void
 SpectrogramLayer::setColourRotation(int r)
 {
-    invalidateImageCaches();
-
     if (r < 0) r = 0;
     if (r > 256) r = 256;
     int distance = r - m_colourRotation;
 
     if (distance != 0) {
-	rotatePalette(-distance);
 	m_colourRotation = r;
     }
+
+    // Initially the idea with colour rotation was that we would just
+    // rotate the palette of an already-generated cache. That's not
+    // really practical now that cacheing is handled in a separate
+    // class in which the main cache no longer has a palette.
+    invalidateRenderers();
     
     emit layerParametersChanged();
 }
 
 void
-SpectrogramLayer::setColourScale(ColourScale colourScale)
+SpectrogramLayer::setColourScale(ColourScaleType colourScale)
 {
     if (m_colourScale == colourScale) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     
     m_colourScale = colourScale;
     
     emit layerParametersChanged();
 }
 
-SpectrogramLayer::ColourScale
+ColourScaleType
 SpectrogramLayer::getColourScale() const
 {
     return m_colourScale;
 }
 
 void
+SpectrogramLayer::setColourScaleMultiple(double multiple)
+{
+    if (m_colourScaleMultiple == multiple) return;
+
+    invalidateRenderers();
+    
+    m_colourScaleMultiple = multiple;
+    
+    emit layerParametersChanged();
+}
+
+double
+SpectrogramLayer::getColourScaleMultiple() const
+{
+    return m_colourScaleMultiple;
+}
+
+void
 SpectrogramLayer::setColourMap(int map)
 {
     if (m_colourMap == map) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     
     m_colourMap = map;
-    initialisePalette();
 
     emit layerParametersChanged();
 }
@@ -900,20 +931,20 @@
 }
 
 void
-SpectrogramLayer::setFrequencyScale(FrequencyScale frequencyScale)
+SpectrogramLayer::setBinScale(BinScale binScale)
 {
-    if (m_frequencyScale == frequencyScale) return;
-
-    invalidateImageCaches();
-    m_frequencyScale = frequencyScale;
+    if (m_binScale == binScale) return;
+
+    invalidateRenderers();
+    m_binScale = binScale;
 
     emit layerParametersChanged();
 }
 
-SpectrogramLayer::FrequencyScale
-SpectrogramLayer::getFrequencyScale() const
+BinScale
+SpectrogramLayer::getBinScale() const
 {
-    return m_frequencyScale;
+    return m_binScale;
 }
 
 void
@@ -921,66 +952,45 @@
 {
     if (m_binDisplay == binDisplay) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     m_binDisplay = binDisplay;
 
     emit layerParametersChanged();
 }
 
-SpectrogramLayer::BinDisplay
+BinDisplay
 SpectrogramLayer::getBinDisplay() const
 {
     return m_binDisplay;
 }
 
 void
-SpectrogramLayer::setNormalizeColumns(bool n)
+SpectrogramLayer::setNormalization(ColumnNormalization n)
 {
-    if (m_normalizeColumns == n) return;
-
-    invalidateImageCaches();
+    if (m_normalization == n) return;
+
+    invalidateRenderers();
     invalidateMagnitudes();
-    m_normalizeColumns = n;
+    m_normalization = n;
 
     emit layerParametersChanged();
 }
 
-bool
-SpectrogramLayer::getNormalizeColumns() const
+ColumnNormalization
+SpectrogramLayer::getNormalization() const
 {
-    return m_normalizeColumns;
-}
-
-void
-SpectrogramLayer::setNormalizeHybrid(bool n)
-{
-    if (m_normalizeHybrid == n) return;
-
-    invalidateImageCaches();
-    invalidateMagnitudes();
-    m_normalizeHybrid = n;
-
-    emit layerParametersChanged();
-}
-
-bool
-SpectrogramLayer::getNormalizeHybrid() const
-{
-    return m_normalizeHybrid;
+    return m_normalization;
 }
 
 void
 SpectrogramLayer::setNormalizeVisibleArea(bool n)
 {
-    SVDEBUG << "SpectrogramLayer::setNormalizeVisibleArea(" << n
-              << ") (from " << m_normalizeVisibleArea << ")" << endl;
-
     if (m_normalizeVisibleArea == n) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     invalidateMagnitudes();
     m_normalizeVisibleArea = n;
-
+    
     emit layerParametersChanged();
 }
 
@@ -991,12 +1001,12 @@
 }
 
 void
-SpectrogramLayer::setLayerDormant(const View *v, bool dormant)
+SpectrogramLayer::setLayerDormant(const LayerGeometryProvider *v, bool dormant)
 {
     if (dormant) {
 
 #ifdef DEBUG_SPECTROGRAM_REPAINT
-        SVDEBUG << "SpectrogramLayer::setLayerDormant(" << dormant << ")"
+        cerr << "SpectrogramLayer::setLayerDormant(" << dormant << ")"
                   << endl;
 #endif
 
@@ -1006,30 +1016,7 @@
 
         Layer::setLayerDormant(v, true);
 
-	invalidateImageCaches();
-        m_imageCaches.erase(v);
-
-        if (m_fftModels.find(v) != m_fftModels.end()) {
-
-            if (m_sliceableModel == m_fftModels[v].first) {
-                bool replaced = false;
-                for (ViewFFTMap::iterator i = m_fftModels.begin();
-                     i != m_fftModels.end(); ++i) {
-                    if (i->second.first != m_sliceableModel) {
-                        emit sliceableModelReplaced(m_sliceableModel, i->second.first);
-                        replaced = true;
-                        break;
-                    }
-                }
-                if (!replaced) emit sliceableModelReplaced(m_sliceableModel, 0);
-            }
-
-            delete m_fftModels[v].first;
-            m_fftModels.erase(v);
-
-            delete m_peakCaches[v];
-            m_peakCaches.erase(v);
-        }
+	invalidateRenderers();
 	
     } else {
 
@@ -1041,238 +1028,52 @@
 SpectrogramLayer::cacheInvalid()
 {
 #ifdef DEBUG_SPECTROGRAM_REPAINT
-    SVDEBUG << "SpectrogramLayer::cacheInvalid()" << endl;
+    cerr << "SpectrogramLayer::cacheInvalid()" << endl;
 #endif
 
-    invalidateImageCaches();
+    invalidateRenderers();
     invalidateMagnitudes();
 }
 
 void
-SpectrogramLayer::cacheInvalid(sv_frame_t from, sv_frame_t to)
+SpectrogramLayer::cacheInvalid(
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+    sv_frame_t from, sv_frame_t to
+#else 
+    sv_frame_t     , sv_frame_t
+#endif
+    )
 {
 #ifdef DEBUG_SPECTROGRAM_REPAINT
-    SVDEBUG << "SpectrogramLayer::cacheInvalid(" << from << ", " << to << ")" << endl;
+    cerr << "SpectrogramLayer::cacheInvalid(" << from << ", " << to << ")" << endl;
 #endif
 
-    invalidateImageCaches(from, to);
+    // We used to call invalidateMagnitudes(from, to) to invalidate
+    // only those caches whose views contained some of the (from, to)
+    // range. That's the right thing to do; it has been lost in
+    // pulling out the image cache code, but it might not matter very
+    // much, since the underlying models for spectrogram layers don't
+    // change very often. Let's see.
+    invalidateRenderers();
     invalidateMagnitudes();
 }
 
-void
-SpectrogramLayer::fillTimerTimedOut()
-{
-    if (!m_model) return;
-
-    bool allDone = true;
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    SVDEBUG << "SpectrogramLayer::fillTimerTimedOut: have " << m_fftModels.size() << " FFT models associated with views" << endl;
-#endif
-
-    for (ViewFFTMap::iterator i = m_fftModels.begin();
-         i != m_fftModels.end(); ++i) {
-
-        const FFTModel *model = i->second.first;
-        sv_frame_t lastFill = i->second.second;
-
-        if (model) {
-
-            sv_frame_t fill = model->getFillExtent();
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            SVDEBUG << "SpectrogramLayer::fillTimerTimedOut: extent for " << model << ": " << fill << ", last " << lastFill << ", total " << m_model->getEndFrame() << endl;
-#endif
-
-            if (fill >= lastFill) {
-                if (fill >= m_model->getEndFrame() && lastFill > 0) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                    cerr << "complete!" << endl;
-#endif
-                    invalidateImageCaches();
-                    i->second.second = -1;
-                    emit modelChanged();
-
-                } else if (fill > lastFill) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                    cerr << "SpectrogramLayer: emitting modelChanged("
-                              << lastFill << "," << fill << ")" << endl;
-#endif
-                    invalidateImageCaches(lastFill, fill);
-                    i->second.second = fill;
-                    emit modelChangedWithin(lastFill, fill);
-                }
-            } else {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                cerr << "SpectrogramLayer: going backwards, emitting modelChanged("
-                          << m_model->getStartFrame() << "," << m_model->getEndFrame() << ")" << endl;
-#endif
-                invalidateImageCaches();
-                i->second.second = fill;
-                emit modelChangedWithin(m_model->getStartFrame(), m_model->getEndFrame());
-            }
-
-            if (i->second.second >= 0) {
-                allDone = false;
-            }
-        }
-    }
-
-    if (allDone) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "SpectrogramLayer: all complete!" << endl;
-#endif
-        delete m_updateTimer;
-        m_updateTimer = 0;
-    }
-}
-
 bool
 SpectrogramLayer::hasLightBackground() const 
 {
     return ColourMapper(m_colourMap, 1.f, 255.f).hasLightBackground();
 }
 
-void
-SpectrogramLayer::initialisePalette()
-{
-    int formerRotation = m_colourRotation;
-
-    if (m_colourMap == (int)ColourMapper::BlackOnWhite) {
-	m_palette.setColour(NO_VALUE, Qt::white);
-    } else {
-	m_palette.setColour(NO_VALUE, Qt::black);
-    }
-
-    ColourMapper mapper(m_colourMap, 1.f, 255.f);
-    
-    for (int pixel = 1; pixel < 256; ++pixel) {
-        m_palette.setColour((unsigned char)pixel, mapper.map(pixel));
-    }
-
-    m_crosshairColour = mapper.getContrastingColour();
-
-    m_colourRotation = 0;
-    rotatePalette(m_colourRotation - formerRotation);
-    m_colourRotation = formerRotation;
-
-    m_drawBuffer = QImage();
-}
-
-void
-SpectrogramLayer::rotatePalette(int distance)
-{
-    QColor newPixels[256];
-
-    newPixels[NO_VALUE] = m_palette.getColour(NO_VALUE);
-
-    for (int pixel = 1; pixel < 256; ++pixel) {
-	int target = pixel + distance;
-	while (target < 1) target += 255;
-	while (target > 255) target -= 255;
-	newPixels[target] = m_palette.getColour((unsigned char)pixel);
-    }
-
-    for (int pixel = 0; pixel < 256; ++pixel) {
-	m_palette.setColour((unsigned char)pixel, newPixels[pixel]);
-    }
-
-    m_drawBuffer = QImage();
-}
-
-unsigned char
-SpectrogramLayer::getDisplayValue(View *v, double input) const
-{
-    int value;
-
-    double min = 0.0;
-    double max = 1.0;
-
-    if (m_normalizeVisibleArea) {
-        min = m_viewMags[v].getMin();
-        max = m_viewMags[v].getMax();
-    } else if (!m_normalizeColumns) {
-        if (m_colourScale == LinearColourScale //||
-//            m_colourScale == MeterColourScale) {
-            ) {
-            max = 0.1;
-        }
-    }
-
-    double thresh = -80.0;
-
-    if (max == 0.0) max = 1.0;
-    if (max == min) min = max - 0.0001;
-
-    switch (m_colourScale) {
-	
-    default:
-    case LinearColourScale:
-        value = int(((input - min) / (max - min)) * 255.0) + 1;
-	break;
-	
-    case MeterColourScale:
-        value = AudioLevel::multiplier_to_preview
-            ((input - min) / (max - min), 254) + 1;
-	break;
-
-    case dBSquaredColourScale:
-        input = ((input - min) * (input - min)) / ((max - min) * (max - min));
-        if (input > 0.0) {
-            input = 10.0 * log10(input);
-        } else {
-            input = thresh;
-        }
-        if (min > 0.0) {
-            thresh = 10.0 * log10(min * min);
-            if (thresh < -80.0) thresh = -80.0;
-        }
-	input = (input - thresh) / (-thresh);
-	if (input < 0.0) input = 0.0;
-	if (input > 1.0) input = 1.0;
-	value = int(input * 255.0) + 1;
-	break;
-	
-    case dBColourScale:
-        //!!! experiment with normalizing the visible area this way.
-        //In any case, we need to have some indication of what the dB
-        //scale is relative to.
-        input = (input - min) / (max - min);
-        if (input > 0.0) {
-            input = 10.0 * log10(input);
-        } else {
-            input = thresh;
-        }
-        if (min > 0.0) {
-            thresh = 10.0 * log10(min);
-            if (thresh < -80.0) thresh = -80.0;
-        }
-	input = (input - thresh) / (-thresh);
-	if (input < 0.0) input = 0.0;
-	if (input > 1.0) input = 1.0;
-	value = int(input * 255.0) + 1;
-	break;
-	
-    case PhaseColourScale:
-	value = int((input * 127.0 / M_PI) + 128);
-	break;
-    }
-
-    if (value > UCHAR_MAX) value = UCHAR_MAX;
-    if (value < 0) value = 0;
-    return (unsigned char)value;
-}
-
 double
 SpectrogramLayer::getEffectiveMinFrequency() const
 {
     sv_samplerate_t sr = m_model->getSampleRate();
-    double minf = double(sr) / m_fftSize;
+    double minf = double(sr) / getFFTSize();
 
     if (m_minFrequency > 0.0) {
-	int minbin = int((double(m_minFrequency) * m_fftSize) / sr + 0.01);
+	int minbin = int((double(m_minFrequency) * getFFTSize()) / sr + 0.01);
 	if (minbin < 1) minbin = 1;
-	minf = minbin * sr / m_fftSize;
+	minf = minbin * sr / getFFTSize();
     }
 
     return minf;
@@ -1285,68 +1086,59 @@
     double maxf = double(sr) / 2;
 
     if (m_maxFrequency > 0.0) {
-	int maxbin = int((double(m_maxFrequency) * m_fftSize) / sr + 0.1);
-	if (maxbin > m_fftSize / 2) maxbin = m_fftSize / 2;
-	maxf = maxbin * sr / m_fftSize;
+	int maxbin = int((double(m_maxFrequency) * getFFTSize()) / sr + 0.1);
+	if (maxbin > getFFTSize() / 2) maxbin = getFFTSize() / 2;
+	maxf = maxbin * sr / getFFTSize();
     }
 
     return maxf;
 }
 
 bool
-SpectrogramLayer::getYBinRange(View *v, int y, double &q0, double &q1) const
+SpectrogramLayer::getYBinRange(LayerGeometryProvider *v, int y, double &q0, double &q1) const
 {
     Profiler profiler("SpectrogramLayer::getYBinRange");
+    int h = v->getPaintHeight();
+    if (y < 0 || y >= h) return false;
+    q0 = getBinForY(v, y);
+    q1 = getBinForY(v, y-1);
+    return true;
+}
+
+double
+SpectrogramLayer::getYForBin(const LayerGeometryProvider *v, double bin) const
+{
+    double minf = getEffectiveMinFrequency();
+    double maxf = getEffectiveMaxFrequency();
+    bool logarithmic = (m_binScale == BinScale::Log);
+    sv_samplerate_t sr = m_model->getSampleRate();
+
+    double freq = (bin * sr) / getFFTSize();
     
-    int h = v->height();
-    if (y < 0 || y >= h) return false;
-
+    double y = v->getYForFrequency(freq, minf, maxf, logarithmic);
+    
+    return y;
+}
+
+double
+SpectrogramLayer::getBinForY(const LayerGeometryProvider *v, double y) const
+{
     sv_samplerate_t sr = m_model->getSampleRate();
     double minf = getEffectiveMinFrequency();
     double maxf = getEffectiveMaxFrequency();
 
-    bool logarithmic = (m_frequencyScale == LogFrequencyScale);
-
-    q0 = v->getFrequencyForY(y, minf, maxf, logarithmic);
-    q1 = v->getFrequencyForY(y - 1, minf, maxf, logarithmic);
-
-    // Now map these on to ("proportions of") actual bins, using raw
-    // FFT size (unsmoothed)
-
-    q0 = (q0 * m_fftSize) / sr;
-    q1 = (q1 * m_fftSize) / sr;
-
-    return true;
+    bool logarithmic = (m_binScale == BinScale::Log);
+
+    double freq = v->getFrequencyForY(y, minf, maxf, logarithmic);
+
+    // Now map on to ("proportion of") actual bins
+    double bin = (freq * getFFTSize()) / sr;
+
+    return bin;
 }
 
 bool
-SpectrogramLayer::getSmoothedYBinRange(View *v, int y, double &q0, double &q1) const
-{
-    Profiler profiler("SpectrogramLayer::getSmoothedYBinRange");
-
-    int h = v->height();
-    if (y < 0 || y >= h) return false;
-
-    sv_samplerate_t sr = m_model->getSampleRate();
-    double minf = getEffectiveMinFrequency();
-    double maxf = getEffectiveMaxFrequency();
-
-    bool logarithmic = (m_frequencyScale == LogFrequencyScale);
-
-    q0 = v->getFrequencyForY(y, minf, maxf, logarithmic);
-    q1 = v->getFrequencyForY(y - 1, minf, maxf, logarithmic);
-
-    // Now map these on to ("proportions of") actual bins, using raw
-    // FFT size (unsmoothed)
-
-    q0 = (q0 * getFFTSize(v)) / sr;
-    q1 = (q1 * getFFTSize(v)) / sr;
-
-    return true;
-}
-    
-bool
-SpectrogramLayer::getXBinRange(View *v, int x, double &s0, double &s1) const
+SpectrogramLayer::getXBinRange(LayerGeometryProvider *v, int x, double &s0, double &s1) const
 {
     sv_frame_t modelStart = m_model->getStartFrame();
     sv_frame_t modelEnd = m_model->getEndFrame();
@@ -1370,7 +1162,7 @@
 }
  
 bool
-SpectrogramLayer::getXBinSourceRange(View *v, int x, RealTime &min, RealTime &max) const
+SpectrogramLayer::getXBinSourceRange(LayerGeometryProvider *v, int x, RealTime &min, RealTime &max) const
 {
     double s0 = 0, s1 = 0;
     if (!getXBinRange(v, x, s0, s1)) return false;
@@ -1389,7 +1181,7 @@
 }
 
 bool
-SpectrogramLayer::getYBinSourceRange(View *v, int y, double &freqMin, double &freqMax)
+SpectrogramLayer::getYBinSourceRange(LayerGeometryProvider *v, int y, double &freqMin, double &freqMax)
 const
 {
     double q0 = 0, q1 = 0;
@@ -1401,14 +1193,14 @@
     sv_samplerate_t sr = m_model->getSampleRate();
 
     for (int q = q0i; q <= q1i; ++q) {
-	if (q == q0i) freqMin = (sr * q) / m_fftSize;
-	if (q == q1i) freqMax = (sr * (q+1)) / m_fftSize;
+	if (q == q0i) freqMin = (sr * q) / getFFTSize();
+	if (q == q1i) freqMax = (sr * (q+1)) / getFFTSize();
     }
     return true;
 }
 
 bool
-SpectrogramLayer::getAdjustedYBinSourceRange(View *v, int x, int y,
+SpectrogramLayer::getAdjustedYBinSourceRange(LayerGeometryProvider *v, int x, int y,
 					     double &freqMin, double &freqMax,
 					     double &adjFreqMin, double &adjFreqMax)
 const
@@ -1417,7 +1209,7 @@
 	return false;
     }
 
-    FFTModel *fft = getFFTModel(v);
+    FFTModel *fft = getFFTModel();
     if (!fft) return false;
 
     double s0 = 0, s1 = 0;
@@ -1436,22 +1228,23 @@
 
     bool haveAdj = false;
 
-    bool peaksOnly = (m_binDisplay == PeakBins ||
-		      m_binDisplay == PeakFrequencies);
+    bool peaksOnly = (m_binDisplay == BinDisplay::PeakBins ||
+		      m_binDisplay == BinDisplay::PeakFrequencies);
 
     for (int q = q0i; q <= q1i; ++q) {
 
 	for (int s = s0i; s <= s1i; ++s) {
 
-            if (!fft->isColumnAvailable(s)) continue;
-
 	    double binfreq = (double(sr) * q) / m_windowSize;
 	    if (q == q0i) freqMin = binfreq;
 	    if (q == q1i) freqMax = binfreq;
 
 	    if (peaksOnly && !fft->isLocalPeak(s, q)) continue;
 
-	    if (!fft->isOverThreshold(s, q, float(m_threshold * double(m_fftSize)/2.0))) continue;
+	    if (!fft->isOverThreshold
+                (s, q, float(m_threshold * double(getFFTSize())/2.0))) {
+                continue;
+            }
 
             double freq = binfreq;
 	    
@@ -1475,7 +1268,7 @@
 }
     
 bool
-SpectrogramLayer::getXYBinSourceRange(View *v, int x, int y,
+SpectrogramLayer::getXYBinSourceRange(LayerGeometryProvider *v, int x, int y,
 				      double &min, double &max,
 				      double &phaseMin, double &phaseMax) const
 {
@@ -1497,11 +1290,7 @@
 
     bool rv = false;
 
-    int zp = getZeroPadLevel(v);
-    q0i *= zp + 1;
-    q1i *= zp + 1;
-
-    FFTModel *fft = getFFTModel(v);
+    FFTModel *fft = getFFTModel();
 
     if (fft) {
 
@@ -1518,15 +1307,13 @@
             for (int s = s0i; s <= s1i; ++s) {
                 if (s >= 0 && q >= 0 && s < cw && q < ch) {
 
-                    if (!fft->isColumnAvailable(s)) continue;
-                    
                     double value;
 
                     value = fft->getPhaseAt(s, q);
                     if (!have || value < phaseMin) { phaseMin = value; }
                     if (!have || value > phaseMax) { phaseMax = value; }
 
-                    value = fft->getMagnitudeAt(s, q) / (m_fftSize/2.0);
+                    value = fft->getMagnitudeAt(s, q) / (getFFTSize()/2.0);
                     if (!have || value < min) { min = value; }
                     if (!have || value > max) { max = value; }
                     
@@ -1542,222 +1329,109 @@
 
     return rv;
 }
-   
-int
-SpectrogramLayer::getZeroPadLevel(const View *v) const
+	
+void
+SpectrogramLayer::recreateFFTModel()
 {
-    //!!! tidy all this stuff
-
-    if (m_binDisplay != AllBins) return 0;
-
-    Preferences::SpectrogramSmoothing smoothing = 
-        Preferences::getInstance()->getSpectrogramSmoothing();
+#ifdef DEBUG_SPECTROGRAM
+    cerr << "SpectrogramLayer::recreateFFTModel called" << endl;
+#endif
+
+    if (!m_model || !m_model->isOK()) {
+        emit sliceableModelReplaced(m_fftModel, 0);
+        delete m_fftModel;
+        delete m_peakCache;
+        delete m_wholeCache;
+        m_fftModel = 0;
+        m_peakCache = 0;
+        m_wholeCache = 0;
+        return;
+    }
+
+    FFTModel *oldModel = m_fftModel;
+
+    m_fftModel = new FFTModel(m_model,
+                              m_channel,
+                              m_windowType,
+                              m_windowSize,
+                              getWindowIncrement(),
+                              getFFTSize());
+
+    delete m_peakCache;
+    m_peakCache = 0;
+
+    delete m_wholeCache;
+    m_wholeCache = 0;
     
-    if (smoothing == Preferences::NoSpectrogramSmoothing ||
-        smoothing == Preferences::SpectrogramInterpolated) return 0;
-
-    if (m_frequencyScale == LogFrequencyScale) return 3;
-
-    sv_samplerate_t sr = m_model->getSampleRate();
-    
-    int maxbin = m_fftSize / 2;
-    if (m_maxFrequency > 0) {
-	maxbin = int((double(m_maxFrequency) * m_fftSize) / sr + 0.1);
-	if (maxbin > m_fftSize / 2) maxbin = m_fftSize / 2;
+    if (!m_fftModel->isOK()) {
+        QMessageBox::critical
+            (0, tr("FFT cache failed"),
+             tr("Failed to create the FFT model for this spectrogram.\n"
+                "There may be insufficient memory or disc space to continue."));
+        delete m_fftModel;
+        m_fftModel = 0;
+        return;
     }
 
-    int minbin = 1;
-    if (m_minFrequency > 0) {
-	minbin = int((double(m_minFrequency) * m_fftSize) / sr + 0.1);
-	if (minbin < 1) minbin = 1;
-	if (minbin >= maxbin) minbin = maxbin - 1;
+    if (canStoreWholeCache()) { // i.e. if enough memory
+        m_wholeCache = new Dense3DModelPeakCache(m_fftModel, 1);
+        m_peakCache = new Dense3DModelPeakCache(m_wholeCache, m_peakCacheDivisor);
+    } else {
+        m_peakCache = new Dense3DModelPeakCache(m_fftModel, m_peakCacheDivisor);
     }
 
-    double perPixel =
-        double(v->height()) /
-        double((maxbin - minbin) / (m_zeroPadLevel + 1));
-
-    if (perPixel > 2.8) {
-        return 3; // 4x oversampling
-    } else if (perPixel > 1.5) {
-        return 1; // 2x
-    } else {
-        return 0; // 1x
+    emit sliceableModelReplaced(oldModel, m_fftModel);
+
+    delete oldModel;
+}
+
+bool
+SpectrogramLayer::canStoreWholeCache() const
+{
+    if (!m_fftModel) {
+        return false; // or true, doesn't really matter
     }
-}
-
-int
-SpectrogramLayer::getFFTSize(const View *v) const
-{
-    return m_fftSize * (getZeroPadLevel(v) + 1);
-}
-	
-FFTModel *
-SpectrogramLayer::getFFTModel(const View *v) const
-{
-    if (!m_model) return 0;
-
-    int fftSize = getFFTSize(v);
-
-    if (m_fftModels.find(v) != m_fftModels.end()) {
-        if (m_fftModels[v].first == 0) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            SVDEBUG << "SpectrogramLayer::getFFTModel(" << v << "): Found null model" << endl;
-#endif
-            return 0;
+
+    size_t sz =
+        size_t(m_fftModel->getWidth()) *
+        size_t(m_fftModel->getHeight()) *
+        sizeof(float);
+
+    try {
+        SVDEBUG << "Requesting advice from StorageAdviser on whether to create whole-model cache" << endl;
+        StorageAdviser::Recommendation recommendation =
+            StorageAdviser::recommend
+            (StorageAdviser::Criteria(StorageAdviser::SpeedCritical |
+                                      StorageAdviser::PrecisionCritical |
+                                      StorageAdviser::FrequentLookupLikely),
+             sz / 1024, sz / 1024);
+        if ((recommendation & StorageAdviser::UseDisc) ||
+            (recommendation & StorageAdviser::ConserveSpace)) {
+            SVDEBUG << "Seems inadvisable to create whole-model cache" << endl;
+            return false;
+        } else {
+            SVDEBUG << "Seems fine to create whole-model cache" << endl;
+            return true;
         }
-        if (m_fftModels[v].first->getHeight() != fftSize / 2 + 1) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            SVDEBUG << "SpectrogramLayer::getFFTModel(" << v << "): Found a model with the wrong height (" << m_fftModels[v].first->getHeight() << ", wanted " << (fftSize / 2 + 1) << ")" << endl;
-#endif
-            delete m_fftModels[v].first;
-            m_fftModels.erase(v);
-            delete m_peakCaches[v];
-            m_peakCaches.erase(v);
-        } else {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            SVDEBUG << "SpectrogramLayer::getFFTModel(" << v << "): Found a good model of height " << m_fftModels[v].first->getHeight() << endl;
-#endif
-            return m_fftModels[v].first;
-        }
+    } catch (const InsufficientDiscSpace &) {
+        SVDEBUG << "Seems like a terrible idea to create whole-model cache" << endl;
+        return false;
     }
-
-    if (m_fftModels.find(v) == m_fftModels.end()) {
-
-        FFTModel *model = new FFTModel(m_model,
-                                       m_channel,
-                                       m_windowType,
-                                       m_windowSize,
-                                       getWindowIncrement(),
-                                       fftSize,
-                                       true, // polar
-                                       StorageAdviser::SpeedCritical,
-                                       m_candidateFillStartFrame);
-
-        if (!model->isOK()) {
-            QMessageBox::critical
-                (0, tr("FFT cache failed"),
-                 tr("Failed to create the FFT model for this spectrogram.\n"
-                    "There may be insufficient memory or disc space to continue."));
-            delete model;
-            m_fftModels[v] = FFTFillPair(0, 0);
-            return 0;
-        }
-
-        if (!m_sliceableModel) {
-#ifdef DEBUG_SPECTROGRAM
-            cerr << "SpectrogramLayer: emitting sliceableModelReplaced(0, " << model << ")" << endl;
-#endif
-            ((SpectrogramLayer *)this)->sliceableModelReplaced(0, model);
-            m_sliceableModel = model;
-        }
-
-        m_fftModels[v] = FFTFillPair(model, 0);
-
-        model->resume();
-        
-        delete m_updateTimer;
-        m_updateTimer = new QTimer((SpectrogramLayer *)this);
-        connect(m_updateTimer, SIGNAL(timeout()),
-                this, SLOT(fillTimerTimedOut()));
-        m_updateTimer->start(200);
-    }
-
-    return m_fftModels[v].first;
-}
-
-Dense3DModelPeakCache *
-SpectrogramLayer::getPeakCache(const View *v) const
-{
-    if (!m_peakCaches[v]) {
-        FFTModel *f = getFFTModel(v);
-        if (!f) return 0;
-        m_peakCaches[v] = new Dense3DModelPeakCache(f, 8);
-    }
-    return m_peakCaches[v];
 }
 
 const Model *
 SpectrogramLayer::getSliceableModel() const
 {
-    if (m_sliceableModel) return m_sliceableModel;
-    if (m_fftModels.empty()) return 0;
-    m_sliceableModel = m_fftModels.begin()->second.first;
-    return m_sliceableModel;
-}
-
-void
-SpectrogramLayer::invalidateFFTModels()
-{
-    for (ViewFFTMap::iterator i = m_fftModels.begin();
-         i != m_fftModels.end(); ++i) {
-        delete i->second.first;
-    }
-    for (PeakCacheMap::iterator i = m_peakCaches.begin();
-         i != m_peakCaches.end(); ++i) {
-        delete i->second;
-    }
-    
-    m_fftModels.clear();
-    m_peakCaches.clear();
-
-    if (m_sliceableModel) {
-        cerr << "SpectrogramLayer: emitting sliceableModelReplaced(" << m_sliceableModel << ", 0)" << endl;
-        emit sliceableModelReplaced(m_sliceableModel, 0);
-        m_sliceableModel = 0;
-    }
+    return m_fftModel;
 }
 
 void
 SpectrogramLayer::invalidateMagnitudes()
 {
+#ifdef DEBUG_SPECTROGRAM
+    cerr << "SpectrogramLayer::invalidateMagnitudes called" << endl;
+#endif
     m_viewMags.clear();
-    for (std::vector<MagnitudeRange>::iterator i = m_columnMags.begin();
-         i != m_columnMags.end(); ++i) {
-        *i = MagnitudeRange();
-    }
-}
-
-bool
-SpectrogramLayer::updateViewMagnitudes(View *v) const
-{
-    MagnitudeRange mag;
-
-    int x0 = 0, x1 = v->width();
-    double s00 = 0, s01 = 0, s10 = 0, s11 = 0;
-    
-    if (!getXBinRange(v, x0, s00, s01)) {
-        s00 = s01 = double(m_model->getStartFrame()) / getWindowIncrement();
-    }
-
-    if (!getXBinRange(v, x1, s10, s11)) {
-        s10 = s11 = double(m_model->getEndFrame()) / getWindowIncrement();
-    }
-
-    int s0 = int(std::min(s00, s10) + 0.0001);
-    int s1 = int(std::max(s01, s11) + 0.0001);
-
-//    SVDEBUG << "SpectrogramLayer::updateViewMagnitudes: x0 = " << x0 << ", x1 = " << x1 << ", s00 = " << s00 << ", s11 = " << s11 << " s0 = " << s0 << ", s1 = " << s1 << endl;
-
-    if (int(m_columnMags.size()) <= s1) {
-        m_columnMags.resize(s1 + 1);
-    }
-
-    for (int s = s0; s <= s1; ++s) {
-        if (m_columnMags[s].isSet()) {
-            mag.sample(m_columnMags[s]);
-        }
-    }
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    SVDEBUG << "SpectrogramLayer::updateViewMagnitudes returning from cols "
-              << s0 << " -> " << s1 << " inclusive" << endl;
-#endif
-
-    if (!mag.isSet()) return false;
-    if (mag == m_viewMags[v]) return false;
-    m_viewMags[v] = mag;
-    return true;
 }
 
 void
@@ -1766,24 +1440,150 @@
     m_synchronous = synchronous;
 }
 
+Colour3DPlotRenderer *
+SpectrogramLayer::getRenderer(LayerGeometryProvider *v) const
+{
+    int viewId = v->getId();
+    
+    if (m_renderers.find(viewId) == m_renderers.end()) {
+
+        Colour3DPlotRenderer::Sources sources;
+        sources.verticalBinLayer = this;
+        sources.fft = getFFTModel();
+        sources.source = sources.fft;
+        if (m_peakCache) sources.peakCaches.push_back(m_peakCache);
+        if (m_wholeCache) sources.peakCaches.push_back(m_wholeCache);
+
+        ColourScale::Parameters cparams;
+        cparams.colourMap = m_colourMap;
+        cparams.scaleType = m_colourScale;
+        cparams.multiple = m_colourScaleMultiple;
+
+        if (m_colourScale != ColourScaleType::Phase) {
+            cparams.gain = m_gain;
+            cparams.threshold = m_threshold;
+        }
+
+        float minValue = 0.0f;
+        float maxValue = 1.0f;
+        
+        if (m_normalizeVisibleArea && m_viewMags[viewId].isSet()) {
+            minValue = m_viewMags[viewId].getMin();
+            maxValue = m_viewMags[viewId].getMax();
+        } else if (m_colourScale == ColourScaleType::Linear &&
+                   m_normalization == ColumnNormalization::None) {
+            maxValue = 0.1f;
+        }
+
+        if (maxValue <= minValue) {
+            maxValue = minValue + 0.1f;
+        }
+        if (maxValue <= m_threshold) {
+            maxValue = m_threshold + 0.1f;
+        }
+
+        cparams.minValue = minValue;
+        cparams.maxValue = maxValue;
+
+        m_lastRenderedMags[viewId] = MagnitudeRange(minValue, maxValue);
+
+        Colour3DPlotRenderer::Parameters params;
+        params.colourScale = ColourScale(cparams);
+        params.normalization = m_normalization;
+        params.binDisplay = m_binDisplay;
+        params.binScale = m_binScale;
+        params.alwaysOpaque = true;
+        params.invertVertical = false;
+        params.scaleFactor = 1.0;
+        params.colourRotation = m_colourRotation;
+
+        if (m_colourScale != ColourScaleType::Phase &&
+            m_normalization != ColumnNormalization::Hybrid) {
+            params.scaleFactor *= 2.f / float(getFFTSize());
+        }
+
+        Preferences::SpectrogramSmoothing smoothing = 
+            Preferences::getInstance()->getSpectrogramSmoothing();
+        params.interpolate = 
+            (smoothing == Preferences::SpectrogramInterpolated ||
+             smoothing == Preferences::SpectrogramZeroPaddedAndInterpolated);
+
+        m_renderers[v->getId()] = new Colour3DPlotRenderer(sources, params);
+    }
+
+    return m_renderers[v->getId()];
+}
+
 void
-SpectrogramLayer::paint(View *v, QPainter &paint, QRect rect) const
+SpectrogramLayer::paintWithRenderer(LayerGeometryProvider *v, QPainter &paint, QRect rect) const
 {
-    // What a lovely, old-fashioned function this is.
-    // It's practically FORTRAN 77 in its clarity and linearity.
-
+    Colour3DPlotRenderer *renderer = getRenderer(v);
+
+    Colour3DPlotRenderer::RenderResult result;
+    MagnitudeRange magRange;
+    int viewId = v->getId();
+
+    bool continuingPaint = !renderer->geometryChanged(v);
+    
+    if (continuingPaint) {
+        magRange = m_viewMags[viewId];
+    }
+    
+    if (m_synchronous) {
+
+        result = renderer->render(v, paint, rect);
+
+    } else {
+
+        result = renderer->renderTimeConstrained(v, paint, rect);
+
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+        cerr << "rect width from this paint: " << result.rendered.width()
+             << ", mag range in this paint: " << result.range.getMin() << " -> "
+             << result.range.getMax() << endl;
+#endif
+        
+        QRect uncached = renderer->getLargestUncachedRect(v);
+        if (uncached.width() > 0) {
+            v->updatePaintRect(uncached);
+        }
+    }
+
+    magRange.sample(result.range);
+
+    if (magRange.isSet()) {
+        if (m_viewMags[viewId] != magRange) {
+            m_viewMags[viewId] = magRange;
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+            cerr << "mag range in this view has changed: "
+                 << magRange.getMin() << " -> " << magRange.getMax() << endl;
+#endif
+        }
+    }
+
+    if (!continuingPaint && m_normalizeVisibleArea &&
+        m_viewMags[viewId] != m_lastRenderedMags[viewId]) {
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+        cerr << "mag range has changed from last rendered range: re-rendering"
+             << endl;
+#endif
+        delete m_renderers[viewId];
+        m_renderers.erase(viewId);
+        v->updatePaintRect(v->getPaintRect());
+    }
+}
+
+void
+SpectrogramLayer::paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const
+{
     Profiler profiler("SpectrogramLayer::paint", false);
 
 #ifdef DEBUG_SPECTROGRAM_REPAINT
-    SVDEBUG << "SpectrogramLayer::paint(): m_model is " << m_model << ", zoom level is " << v->getZoomLevel() << ", m_updateTimer " << m_updateTimer << endl;
+    cerr << "SpectrogramLayer::paint() entering: m_model is " << m_model << ", zoom level is " << v->getZoomLevel() << endl;
     
-    cerr << "rect is " << rect.x() << "," << rect.y() << " " << rect.width() << "x" << rect.height() << endl;
+    cerr << "SpectrogramLayer::paint(): rect is " << rect.x() << "," << rect.y() << " " << rect.width() << "x" << rect.height() << endl;
 #endif
 
-    sv_frame_t startFrame = v->getStartFrame();
-    if (startFrame < 0) m_candidateFillStartFrame = 0;
-    else m_candidateFillStartFrame = startFrame;
-
     if (!m_model || !m_model->isOK() || !m_model->isReady()) {
 	return;
     }
@@ -1792,1034 +1592,13 @@
 	SVDEBUG << "SpectrogramLayer::paint(): Layer is dormant, making it undormant again" << endl;
     }
 
-    // Need to do this even if !isLayerDormant, as that could mean v
-    // is not in the dormancy map at all -- we need it to be present
-    // and accountable for when determining whether we need the cache
-    // in the cache-fill thread above.
-    //!!! no inter use cache-fill thread
-    const_cast<SpectrogramLayer *>(this)->Layer::setLayerDormant(v, false);
-
-    int fftSize = getFFTSize(v);
-/*
-    FFTModel *fft = getFFTModel(v);
-    if (!fft) {
-	cerr << "ERROR: SpectrogramLayer::paint(): No FFT model, returning" << endl;
-	return;
-    }
-*/
-    ImageCache &cache = m_imageCaches[v];
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    SVDEBUG << "SpectrogramLayer::paint(): image cache valid area " << cache.
-
-validArea.x() << ", " << cache.validArea.y() << ", " << cache.validArea.width() << "x" << cache.validArea.height() << endl;
-#endif
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    bool stillCacheing = (m_updateTimer != 0);
-    SVDEBUG << "SpectrogramLayer::paint(): Still cacheing = " << stillCacheing << endl;
-#endif
-
-    int zoomLevel = v->getZoomLevel();
-
-    int x0 = 0;
-    int x1 = v->width();
-
-    bool recreateWholeImageCache = true;
-
-    x0 = rect.left();
-    x1 = rect.right() + 1;
-/*
-    double xPixelRatio = double(fft->getResolution()) / double(zoomLevel);
-    cerr << "xPixelRatio = " << xPixelRatio << endl;
-    if (xPixelRatio < 1.f) xPixelRatio = 1.f;
-*/
-    if (cache.validArea.width() > 0) {
-
-        int cw = cache.image.width();
-        int ch = cache.image.height();
-        
-	if (int(cache.zoomLevel) == zoomLevel &&
-	    cw == v->width() &&
-	    ch == v->height()) {
-
-	    if (v->getXForFrame(cache.startFrame) ==
-		v->getXForFrame(startFrame) &&
-                cache.validArea.x() <= x0 &&
-                cache.validArea.x() + cache.validArea.width() >= x1) {
-	    
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-		cerr << "SpectrogramLayer: image cache good" << endl;
-#endif
-
-		paint.drawImage(rect, cache.image, rect);
-                //!!!
-//                paint.drawImage(v->rect(), cache.image,
-//                                QRect(QPoint(0, 0), cache.image.size()));
-
-                illuminateLocalFeatures(v, paint);
-		return;
-
-	    } else {
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-		cerr << "SpectrogramLayer: image cache partially OK" << endl;
-#endif
-
-		recreateWholeImageCache = false;
-
-		int dx = v->getXForFrame(cache.startFrame) -
-		         v->getXForFrame(startFrame);
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-		cerr << "SpectrogramLayer: dx = " << dx << " (image cache " << cw << "x" << ch << ")" << endl;
-#endif
-
-		if (dx != 0 &&
-                    dx > -cw &&
-                    dx <  cw) {
-                    
-                    int dxp = dx;
-                    if (dxp < 0) dxp = -dxp;
-                    size_t copy = (cw - dxp) * sizeof(QRgb);
-                    for (int y = 0; y < ch; ++y) {
-                        QRgb *line = (QRgb *)cache.image.scanLine(y);
-                        if (dx < 0) {
-                            memmove(line, line + dxp, copy);
-                        } else {
-                            memmove(line + dxp, line, copy);
-                        }
-                    }
-
-                    int px = cache.validArea.x();
-                    int pw = cache.validArea.width();
-
-		    if (dx < 0) {
-			x0 = cw + dx;
-			x1 = cw;
-                        px += dx;
-                        if (px < 0) {
-                            pw += px;
-                            px = 0;
-                            if (pw < 0) pw = 0;
-                        }
-		    } else {
-			x0 = 0;
-			x1 = dx;
-                        px += dx;
-                        if (px + pw > cw) {
-                            pw = int(cw) - px;
-                            if (pw < 0) pw = 0;
-                        }
-		    }
-                    
-                    cache.validArea =
-                        QRect(px, cache.validArea.y(),
-                              pw, cache.validArea.height());
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                    cerr << "valid area now "
-                              << px << "," << cache.validArea.y()
-                              << " " << pw << "x" << cache.validArea.height()
-                              << endl;
-#endif
-/*
-		    paint.drawImage(rect & cache.validArea,
-                                     cache.image,
-                                     rect & cache.validArea);
-*/
-                } else if (dx != 0) {
-
-                    // we scrolled too far to be of use
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                    cerr << "dx == " << dx << ": scrolled too far for cache to be useful" << endl;
-#endif
-
-                    cache.validArea = QRect();
-                    recreateWholeImageCache = true;
-                }
-	    }
-	} else {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-	    cerr << "SpectrogramLayer: image cache useless" << endl;
-            if (int(cache.zoomLevel) != zoomLevel) {
-                cerr << "(cache zoomLevel " << cache.zoomLevel
-                          << " != " << zoomLevel << ")" << endl;
-            }
-            if (cw != v->width()) {
-                cerr << "(cache width " << cw
-                          << " != " << v->width();
-            }
-            if (ch != v->height()) {
-                cerr << "(cache height " << ch
-                          << " != " << v->height();
-            }
-#endif
-            cache.validArea = QRect();
-//            recreateWholeImageCache = true;
-	}
-    }
-
-    if (updateViewMagnitudes(v)) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "SpectrogramLayer: magnitude range changed to [" << m_viewMags[v].getMin() << "->" << m_viewMags[v].getMax() << "]" << endl;
-#endif
-        if (m_normalizeVisibleArea) {
-            cache.validArea = QRect();
-            recreateWholeImageCache = true;
-        }
-    } else {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "No change in magnitude range [" << m_viewMags[v].getMin() << "->" << m_viewMags[v].getMax() << "]" << endl;
-#endif
-    }
-
-    if (recreateWholeImageCache) {
-        x0 = 0;
-        x1 = v->width();
-    }
-
-    struct timeval tv;
-    (void)gettimeofday(&tv, 0);
-    RealTime mainPaintStart = RealTime::fromTimeval(tv);
-
-    int paintBlockWidth = m_lastPaintBlockWidth;
-
-    if (m_synchronous) {
-        if (paintBlockWidth < x1 - x0) {
-            // always paint full width
-            paintBlockWidth = x1 - x0;
-        }
-    } else {
-        if (paintBlockWidth == 0) {
-            paintBlockWidth = (300000 / zoomLevel);
-        } else {
-            RealTime lastTime = m_lastPaintTime;
-            while (lastTime > RealTime::fromMilliseconds(200) &&
-                   paintBlockWidth > 50) {
-                paintBlockWidth /= 2;
-                lastTime = lastTime / 2;
-            }
-            while (lastTime < RealTime::fromMilliseconds(90) &&
-                   paintBlockWidth < 1500) {
-                paintBlockWidth *= 2;
-                lastTime = lastTime * 2;
-            }
-        }
-        
-        if (paintBlockWidth < 20) paintBlockWidth = 20;
-    }
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "[" << this << "]: last paint width: " << m_lastPaintBlockWidth << ", last paint time: " << m_lastPaintTime << ", new paint width: " << paintBlockWidth << endl;
-#endif
-
-    // We always paint the full height when refreshing the cache.
-    // Smaller heights can be used when painting direct from cache
-    // (further up in this function), but we want to ensure the cache
-    // is coherent without having to worry about vertical matching of
-    // required and valid areas as well as horizontal.
-
-    int h = v->height();
-
-    if (cache.validArea.width() > 0) {
-
-        // If part of the cache is known to be valid, select a strip
-        // immediately to left or right of the valid part
-
-        //!!! this really needs to be coordinated with the selection
-        //!!! of m_drawBuffer boundaries in the bufferBinResolution
-        //!!! case below
-
-        int vx0 = 0, vx1 = 0;
-        vx0 = cache.validArea.x();
-        vx1 = cache.validArea.x() + cache.validArea.width();
-        
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "x0 " << x0 << ", x1 " << x1 << ", vx0 " << vx0 << ", vx1 " << vx1 << ", paintBlockWidth " << paintBlockWidth << endl;
-#endif         
-        if (x0 < vx0) {
-            if (x0 + paintBlockWidth < vx0) {
-                x0 = vx0 - paintBlockWidth;
-            }
-            x1 = vx0;
-        } else if (x0 >= vx1) {
-            x0 = vx1;
-            if (x1 > x0 + paintBlockWidth) {
-                x1 = x0 + paintBlockWidth;
-            }
-        } else {
-            // x0 is within the valid area
-            if (x1 > vx1) {
-                x0 = vx1;
-                if (x0 + paintBlockWidth < x1) {
-                    x1 = x0 + paintBlockWidth;
-                }
-            } else {
-                x1 = x0; // it's all valid, paint nothing
-            }
-        }
-         
-        cache.validArea = QRect
-            (std::min(vx0, x0), cache.validArea.y(),
-             std::max(vx1 - std::min(vx0, x0),
-                       x1 - std::min(vx0, x0)),
-             cache.validArea.height());
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "Valid area becomes " << cache.validArea.x()
-                  << ", " << cache.validArea.y() << ", "
-                  << cache.validArea.width() << "x"
-                  << cache.validArea.height() << endl;
-#endif
-            
-    } else {
-        if (x1 > x0 + paintBlockWidth) {
-            int sfx = x1;
-            if (startFrame < 0) sfx = v->getXForFrame(0);
-            if (sfx >= x0 && sfx + paintBlockWidth <= x1) {
-                x0 = sfx;
-                x1 = x0 + paintBlockWidth;
-            } else {
-                int mid = (x1 + x0) / 2;
-                x0 = mid - paintBlockWidth/2;
-                x1 = x0 + paintBlockWidth;
-            }
-        }
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "Valid area becomes " << x0 << ", 0, " << (x1-x0)
-                  << "x" << h << endl;
-#endif
-        cache.validArea = QRect(x0, 0, x1 - x0, h);
-    }
-
-/*
-    if (xPixelRatio != 1.f) {
-        x0 = int((int(x0 / xPixelRatio) - 4) * xPixelRatio + 0.0001);
-        x1 = int((int(x1 / xPixelRatio) + 4) * xPixelRatio + 0.0001);
-    }
-*/
-    int w = x1 - x0;
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "x0 " << x0 << ", x1 " << x1 << ", w " << w << ", h " << h << endl;
-#endif
-
-    sv_samplerate_t sr = m_model->getSampleRate();
-
-    // Set minFreq and maxFreq to the frequency extents of the possibly
-    // zero-padded visible bin range, and displayMinFreq and displayMaxFreq
-    // to the actual scale frequency extents (presumably not zero padded).
-
-    // If we are zero padding, we want to use the zero-padded
-    // equivalents of the bins that we would be using if not zero
-    // padded, to avoid spaces at the top and bottom of the display.
-
-    // Note fftSize is the actual zero-padded fft size, m_fftSize the
-    // nominal fft size.
-    
-    int maxbin = m_fftSize / 2;
-    if (m_maxFrequency > 0) {
-	maxbin = int((double(m_maxFrequency) * m_fftSize) / sr + 0.001);
-	if (maxbin > m_fftSize / 2) maxbin = m_fftSize / 2;
-    }
-
-    int minbin = 1;
-    if (m_minFrequency > 0) {
-	minbin = int((double(m_minFrequency) * m_fftSize) / sr + 0.001);
-//        cerr << "m_minFrequency = " << m_minFrequency << " -> minbin = " << minbin << endl;
-	if (minbin < 1) minbin = 1;
-	if (minbin >= maxbin) minbin = maxbin - 1;
-    }
-
-    int zpl = getZeroPadLevel(v) + 1;
-    minbin = minbin * zpl;
-    maxbin = (maxbin + 1) * zpl - 1;
-
-    double minFreq = (double(minbin) * sr) / fftSize;
-    double maxFreq = (double(maxbin) * sr) / fftSize;
-
-    double displayMinFreq = minFreq;
-    double displayMaxFreq = maxFreq;
-
-    if (fftSize != m_fftSize) {
-        displayMinFreq = getEffectiveMinFrequency();
-        displayMaxFreq = getEffectiveMaxFrequency();
-    }
-
-//    cerr << "(giving actual minFreq " << minFreq << " and display minFreq " << displayMinFreq << ")" << endl;
-
-    int increment = getWindowIncrement();
-    
-    bool logarithmic = (m_frequencyScale == LogFrequencyScale);
-/*
-    double yforbin[maxbin - minbin + 1];
-
-    for (int q = minbin; q <= maxbin; ++q) {
-        double f0 = (double(q) * sr) / fftSize;
-        yforbin[q - minbin] =
-            v->getYForFrequency(f0, displayMinFreq, displayMaxFreq,
-                                logarithmic);
-    }
-*/
-    MagnitudeRange overallMag = m_viewMags[v];
-    bool overallMagChanged = false;
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << ((double(v->getFrameForX(1) - v->getFrameForX(0))) / increment) << " bin(s) per pixel" << endl;
-#endif
-
-    if (w == 0) {
-        SVDEBUG << "*** NOTE: w == 0" << endl;
-    }
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    int pixels = 0;
-#endif
-
-    Profiler outerprof("SpectrogramLayer::paint: all cols");
-
-    // The draw buffer contains a fragment at either our pixel
-    // resolution (if there is more than one time-bin per pixel) or
-    // time-bin resolution (if a time-bin spans more than one pixel).
-    // We need to ensure that it starts and ends at points where a
-    // time-bin boundary occurs at an exact pixel boundary, and with a
-    // certain amount of overlap across existing pixels so that we can
-    // scale and draw from it without smoothing errors at the edges.
-
-    // If (getFrameForX(x) / increment) * increment ==
-    // getFrameForX(x), then x is a time-bin boundary.  We want two
-    // such boundaries at either side of the draw buffer -- one which
-    // we draw up to, and one which we subsequently crop at.
-
-    bool bufferBinResolution = false;
-    if (increment > zoomLevel) bufferBinResolution = true;
-
-    sv_frame_t leftBoundaryFrame = -1, leftCropFrame = -1;
-    sv_frame_t rightBoundaryFrame = -1, rightCropFrame = -1;
-
-    int bufwid;
-
-    if (bufferBinResolution) {
-
-        for (int x = x0; ; --x) {
-            sv_frame_t f = v->getFrameForX(x);
-            if ((f / increment) * increment == f) {
-                if (leftCropFrame == -1) leftCropFrame = f;
-                else if (x < x0 - 2) { leftBoundaryFrame = f; break; }
-            }
-        }
-        for (int x = x0 + w; ; ++x) {
-            sv_frame_t f = v->getFrameForX(x);
-            if ((f / increment) * increment == f) {
-                if (rightCropFrame == -1) rightCropFrame = f;
-                else if (x > x0 + w + 2) { rightBoundaryFrame = f; break; }
-            }
-        }
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "Left: crop: " << leftCropFrame << " (bin " << leftCropFrame/increment << "); boundary: " << leftBoundaryFrame << " (bin " << leftBoundaryFrame/increment << ")" << endl;
-        cerr << "Right: crop: " << rightCropFrame << " (bin " << rightCropFrame/increment << "); boundary: " << rightBoundaryFrame << " (bin " << rightBoundaryFrame/increment << ")" << endl;
-#endif
-
-        bufwid = int((rightBoundaryFrame - leftBoundaryFrame) / increment);
-
-    } else {
-        
-        bufwid = w;
-    }
-
-    vector<int> binforx(bufwid);
-    vector<double> binfory(h);
-    
-    bool usePeaksCache = false;
-
-    if (bufferBinResolution) {
-        for (int x = 0; x < bufwid; ++x) {
-            binforx[x] = int(leftBoundaryFrame / increment) + x;
-//            cerr << "binforx[" << x << "] = " << binforx[x] << endl;
-        }
-        m_drawBuffer = QImage(bufwid, h, QImage::Format_Indexed8);
-    } else {
-        for (int x = 0; x < bufwid; ++x) {
-            double s0 = 0, s1 = 0;
-            if (getXBinRange(v, x + x0, s0, s1)) {
-                binforx[x] = int(s0 + 0.0001);
-            } else {
-                binforx[x] = -1; //???
-            }
-        }
-        if (m_drawBuffer.width() < bufwid || m_drawBuffer.height() < h) {
-            m_drawBuffer = QImage(bufwid, h, QImage::Format_Indexed8);
-        }
-        usePeaksCache = (increment * 8) < zoomLevel;
-        if (m_colourScale == PhaseColourScale) usePeaksCache = false;
-    }
-
-// No longer exists in Qt5:    m_drawBuffer.setNumColors(256);
-    for (int pixel = 0; pixel < 256; ++pixel) {
-        m_drawBuffer.setColor((unsigned char)pixel,
-                              m_palette.getColour((unsigned char)pixel).rgb());
-    }
-
-    m_drawBuffer.fill(0);
-    
-    if (m_binDisplay != PeakFrequencies) {
-
-        for (int y = 0; y < h; ++y) {
-            double q0 = 0, q1 = 0;
-            if (!getSmoothedYBinRange(v, h-y-1, q0, q1)) {
-                binfory[y] = -1;
-            } else {
-                binfory[y] = q0;
-//                cerr << "binfory[" << y << "] = " << binfory[y] << endl;
-            }
-        }
-
-        paintDrawBuffer(v, bufwid, h, binforx, binfory, usePeaksCache,
-                        overallMag, overallMagChanged);
-
-    } else {
-
-        paintDrawBufferPeakFrequencies(v, bufwid, h, binforx,
-                                       minbin, maxbin,
-                                       displayMinFreq, displayMaxFreq,
-                                       logarithmic,
-                                       overallMag, overallMagChanged);
-    }
-
-/*
-    for (int x = 0; x < w / xPixelRatio; ++x) {
-
-        Profiler innerprof("SpectrogramLayer::paint: 1 pixel column");
-
-        runOutOfData = !paintColumnValues(v, fft, x0, x,
-                                          minbin, maxbin,
-                                          displayMinFreq, displayMaxFreq,
-                                          xPixelRatio,
-                                          h, yforbin);
-
-        if (runOutOfData) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            cerr << "Run out of data -- dropping out of loop" << endl;
-#endif
-            break;
-        }
-    }
-*/
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-//    cerr << pixels << " pixels drawn" << endl;
-#endif
-
-    if (overallMagChanged) {
-        m_viewMags[v] = overallMag;
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "Overall mag is now [" << m_viewMags[v].getMin() << "->" << m_viewMags[v].getMax() << "] - will be updating" << endl;
-#endif
-    } else {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "Overall mag unchanged at [" << m_viewMags[v].getMin() << "->" << m_viewMags[v].getMax() << "]" << endl;
-#endif
-    }
-
-    outerprof.end();
-
-    Profiler profiler2("SpectrogramLayer::paint: draw image");
-
-    if (recreateWholeImageCache) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        SVDEBUG << "Recreating image cache: width = " << v->width()
-                  << ", height = " << h << endl;
-#endif
-	cache.image = QImage(v->width(), h, QImage::Format_ARGB32_Premultiplied);
-    }
-
-    if (w > 0) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        SVDEBUG << "Painting " << w << "x" << h
-                  << " from draw buffer at " << 0 << "," << 0
-                  << " to " << w << "x" << h << " on cache at "
-                  << x0 << "," << 0 << endl;
-#endif
-
-        QPainter cachePainter(&cache.image);
-
-        if (bufferBinResolution) {
-            int scaledLeft = v->getXForFrame(leftBoundaryFrame);
-            int scaledRight = v->getXForFrame(rightBoundaryFrame);
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            SVDEBUG << "Rescaling image from " << bufwid
-                 << "x" << h << " to "
-                 << scaledRight-scaledLeft << "x" << h << endl;
-#endif
-            Preferences::SpectrogramXSmoothing xsmoothing = 
-                Preferences::getInstance()->getSpectrogramXSmoothing();
-//            SVDEBUG << "xsmoothing == " << xsmoothing << endl;
-            QImage scaled = m_drawBuffer.scaled
-                (scaledRight - scaledLeft, h,
-                 Qt::IgnoreAspectRatio,
-                 ((xsmoothing == Preferences::SpectrogramXInterpolated) ?
-                  Qt::SmoothTransformation : Qt::FastTransformation));
-            int scaledLeftCrop = v->getXForFrame(leftCropFrame);
-            int scaledRightCrop = v->getXForFrame(rightCropFrame);
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            SVDEBUG << "Drawing image region of width " << scaledRightCrop - scaledLeftCrop << " to "
-                 << scaledLeftCrop << " from " << scaledLeftCrop - scaledLeft << endl;
-#endif
-            cachePainter.drawImage
-                (QRect(scaledLeftCrop, 0,
-                       scaledRightCrop - scaledLeftCrop, h),
-                 scaled,
-                 QRect(scaledLeftCrop - scaledLeft, 0,
-                       scaledRightCrop - scaledLeftCrop, h));
-        } else {
-            cachePainter.drawImage(QRect(x0, 0, w, h),
-                                   m_drawBuffer,
-                                   QRect(0, 0, w, h));
-        }
-
-        cachePainter.end();
-    }
-
-    QRect pr = rect & cache.validArea;
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    SVDEBUG << "Painting " << pr.width() << "x" << pr.height()
-              << " from cache at " << pr.x() << "," << pr.y()
-              << " to window" << endl;
-#endif
-
-    paint.drawImage(pr.x(), pr.y(), cache.image,
-                    pr.x(), pr.y(), pr.width(), pr.height());
-    //!!!
-//    paint.drawImage(v->rect(), cache.image,
-//                    QRect(QPoint(0, 0), cache.image.size()));
-
-    cache.startFrame = startFrame;
-    cache.zoomLevel = zoomLevel;
-
-    if (!m_synchronous) {
-
-        if (!m_normalizeVisibleArea || !overallMagChanged) {
-    
-            if (cache.validArea.x() > 0) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                SVDEBUG << "SpectrogramLayer::paint() updating left (0, "
-                          << cache.validArea.x() << ")" << endl;
-#endif
-                v->update(0, 0, cache.validArea.x(), h);
-            }
-            
-            if (cache.validArea.x() + cache.validArea.width() <
-                cache.image.width()) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                SVDEBUG << "SpectrogramLayer::paint() updating right ("
-                          << cache.validArea.x() + cache.validArea.width()
-                          << ", "
-                          << cache.image.width() - (cache.validArea.x() +
-                                                     cache.validArea.width())
-                          << ")" << endl;
-#endif
-                v->update(cache.validArea.x() + cache.validArea.width(),
-                          0,
-                          cache.image.width() - (cache.validArea.x() +
-                                                  cache.validArea.width()),
-                          h);
-            }
-        } else {
-            // overallMagChanged
-            cerr << "\noverallMagChanged - updating all\n" << endl;
-            cache.validArea = QRect();
-            v->update();
-        }
-    }
+    paintWithRenderer(v, paint, rect);
 
     illuminateLocalFeatures(v, paint);
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    SVDEBUG << "SpectrogramLayer::paint() returning" << endl;
-#endif
-
-    if (!m_synchronous) {
-        m_lastPaintBlockWidth = paintBlockWidth;
-        (void)gettimeofday(&tv, 0);
-        m_lastPaintTime = RealTime::fromTimeval(tv) - mainPaintStart;
-    }
-
-//!!!    if (fftSuspended) fft->resume();
 }
 
-bool
-SpectrogramLayer::paintDrawBufferPeakFrequencies(View *v,
-                                                 int w,
-                                                 int h,
-                                                 const vector<int> &binforx,
-                                                 int minbin,
-                                                 int maxbin,
-                                                 double displayMinFreq,
-                                                 double displayMaxFreq,
-                                                 bool logarithmic,
-                                                 MagnitudeRange &overallMag,
-                                                 bool &overallMagChanged) const
-{
-    Profiler profiler("SpectrogramLayer::paintDrawBufferPeakFrequencies");
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "minbin " << minbin << ", maxbin " << maxbin << "; w " << w << ", h " << h << endl;
-#endif
-    if (minbin < 0) minbin = 0;
-    if (maxbin < 0) maxbin = minbin+1;
-
-    FFTModel *fft = getFFTModel(v);
-    if (!fft) return false;
-
-    FFTModel::PeakSet peakfreqs;
-
-    int psx = -1;
-
-#ifdef __GNUC__
-    float values[maxbin - minbin + 1];
-#else
-    float *values = (float *)alloca((maxbin - minbin + 1) * sizeof(float));
-#endif
-
-    for (int x = 0; x < w; ++x) {
-        
-        if (binforx[x] < 0) continue;
-
-        int sx0 = binforx[x];
-        int sx1 = sx0;
-        if (x+1 < w) sx1 = binforx[x+1];
-        if (sx0 < 0) sx0 = sx1 - 1;
-        if (sx0 < 0) continue;
-        if (sx1 <= sx0) sx1 = sx0 + 1;
-
-        for (int sx = sx0; sx < sx1; ++sx) {
-
-            if (sx < 0 || sx >= int(fft->getWidth())) continue;
-
-            if (!m_synchronous) {
-                if (!fft->isColumnAvailable(sx)) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                    cerr << "Met unavailable column at col " << sx << endl;
-#endif
-                    return false;
-                }
-            }
-
-            MagnitudeRange mag;
-
-            if (sx != psx) {
-                peakfreqs = fft->getPeakFrequencies(FFTModel::AllPeaks, sx,
-                                                    minbin, maxbin - 1);
-                if (m_colourScale == PhaseColourScale) {
-                    fft->getPhasesAt(sx, values, minbin, maxbin - minbin + 1);
-                } else if (m_normalizeColumns) {
-                    fft->getNormalizedMagnitudesAt(sx, values, minbin, maxbin - minbin + 1);
-                } else if (m_normalizeHybrid) {
-                    fft->getNormalizedMagnitudesAt(sx, values, minbin, maxbin - minbin + 1);
-                    double max = fft->getMaximumMagnitudeAt(sx);
-                    if (max > 0.f) {
-                        for (int i = minbin; i <= maxbin; ++i) {
-                            values[i - minbin] = float(values[i - minbin] * log10(max));
-                        }
-                    }
-                } else {
-                    fft->getMagnitudesAt(sx, values, minbin, maxbin - minbin + 1);
-                }
-                psx = sx;
-            }
-
-            for (FFTModel::PeakSet::const_iterator pi = peakfreqs.begin();
-                 pi != peakfreqs.end(); ++pi) {
-
-                int bin = pi->first;
-                double freq = pi->second;
-
-                if (bin < minbin) continue;
-                if (bin > maxbin) break;
-
-                double value = values[bin - minbin];
-
-                if (m_colourScale != PhaseColourScale) {
-                    if (!m_normalizeColumns && !m_normalizeHybrid) {
-                        value /= (m_fftSize/2.0);
-                    }
-                    mag.sample(float(value));
-                    value *= m_gain;
-                }
-
-                double y = v->getYForFrequency
-                    (freq, displayMinFreq, displayMaxFreq, logarithmic);
-
-                int iy = int(y + 0.5);
-                if (iy < 0 || iy >= h) continue;
-
-                m_drawBuffer.setPixel(x, iy, getDisplayValue(v, value));
-            }
-
-            if (mag.isSet()) {
-                if (sx >= int(m_columnMags.size())) {
-#ifdef DEBUG_SPECTROGRAM
-                    cerr << "INTERNAL ERROR: " << sx << " >= "
-                              << m_columnMags.size()
-                              << " at SpectrogramLayer.cpp::paintDrawBuffer"
-                              << endl;
-#endif
-                } else {
-                    m_columnMags[sx].sample(mag);
-                    if (overallMag.sample(mag)) overallMagChanged = true;
-                }
-            }
-        }
-    }
-
-    return true;
-}
-
-bool
-SpectrogramLayer::paintDrawBuffer(View *v,
-                                  int w,
-                                  int h,
-                                  const vector<int> &binforx,
-                                  const vector<double> &binfory,
-                                  bool usePeaksCache,
-                                  MagnitudeRange &overallMag,
-                                  bool &overallMagChanged) const
-{
-    Profiler profiler("SpectrogramLayer::paintDrawBuffer");
-
-    int minbin = int(binfory[0] + 0.0001);
-    int maxbin = int(binfory[h-1]);
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "minbin " << minbin << ", maxbin " << maxbin << "; w " << w << ", h " << h << endl;
-#endif
-    if (minbin < 0) minbin = 0;
-    if (maxbin < 0) maxbin = minbin+1;
-
-    DenseThreeDimensionalModel *sourceModel = 0;
-    FFTModel *fft = 0;
-    int divisor = 1;
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "Note: bin display = " << m_binDisplay << ", w = " << w << ", binforx[" << w-1 << "] = " << binforx[w-1] << ", binforx[0] = " << binforx[0] << endl;
-#endif
-    if (usePeaksCache) { //!!!
-        sourceModel = getPeakCache(v);
-        divisor = 8;//!!!
-        minbin = 0;
-        maxbin = sourceModel->getHeight();
-    } else {
-        sourceModel = fft = getFFTModel(v);
-    }
-
-    if (!sourceModel) return false;
-
-    bool interpolate = false;
-    Preferences::SpectrogramSmoothing smoothing = 
-        Preferences::getInstance()->getSpectrogramSmoothing();
-    if (smoothing == Preferences::SpectrogramInterpolated ||
-        smoothing == Preferences::SpectrogramZeroPaddedAndInterpolated) {
-        if (m_binDisplay != PeakBins &&
-            m_binDisplay != PeakFrequencies) {
-            interpolate = true;
-        }
-    }
-
-    int psx = -1;
-
-#ifdef __GNUC__
-    float autoarray[maxbin - minbin + 1];
-    float peaks[h];
-#else
-    float *autoarray = (float *)alloca((maxbin - minbin + 1) * sizeof(float));
-    float *peaks = (float *)alloca(h * sizeof(float));
-#endif
-
-    const float *values = autoarray;
-    DenseThreeDimensionalModel::Column c;
-
-    for (int x = 0; x < w; ++x) {
-        
-        if (binforx[x] < 0) continue;
-
-//        float columnGain = m_gain;
-        float columnMax = 0.f;
-
-        int sx0 = binforx[x] / divisor;
-        int sx1 = sx0;
-        if (x+1 < w) sx1 = binforx[x+1] / divisor;
-        if (sx0 < 0) sx0 = sx1 - 1;
-        if (sx0 < 0) continue;
-        if (sx1 <= sx0) sx1 = sx0 + 1;
-
-        for (int y = 0; y < h; ++y) peaks[y] = 0.f;
-            
-        for (int sx = sx0; sx < sx1; ++sx) {
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-//            cerr << "sx = " << sx << endl;
-#endif
-
-            if (sx < 0 || sx >= int(sourceModel->getWidth())) continue;
-
-            if (!m_synchronous) {
-                if (!sourceModel->isColumnAvailable(sx)) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                    cerr << "Met unavailable column at col " << sx << endl;
-#endif
-                    return false;
-                }
-            }
-
-            MagnitudeRange mag;
-
-            if (sx != psx) {
-                if (fft) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                    SVDEBUG << "Retrieving column " << sx << " from fft directly" << endl;
-#endif
-                    if (m_colourScale == PhaseColourScale) {
-                        fft->getPhasesAt(sx, autoarray, minbin, maxbin - minbin + 1);
-                    } else if (m_normalizeColumns) {
-                        fft->getNormalizedMagnitudesAt(sx, autoarray, minbin, maxbin - minbin + 1);
-                    } else if (m_normalizeHybrid) {
-                        fft->getNormalizedMagnitudesAt(sx, autoarray, minbin, maxbin - minbin + 1);
-                        double max = fft->getMaximumMagnitudeAt(sx);
-                        for (int i = minbin; i <= maxbin; ++i) {
-                            if (max > 0.0) {
-                                autoarray[i - minbin] = float(autoarray[i - minbin] * log10(max));
-                            }
-                        }
-                    } else {
-                        fft->getMagnitudesAt(sx, autoarray, minbin, maxbin - minbin + 1);
-                    }
-                } else {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                    SVDEBUG << "Retrieving column " << sx << " from peaks cache" << endl;
-#endif
-                    c = sourceModel->getColumn(sx);
-                    if (m_normalizeColumns || m_normalizeHybrid) {
-                        for (int y = 0; y < h; ++y) {
-                            if (c[y] > columnMax) columnMax = c[y];
-                        }
-                    }
-                    values = c.constData() + minbin;
-                }
-                psx = sx;
-            }
-
-            for (int y = 0; y < h; ++y) {
-
-                double sy0 = binfory[y];
-                double sy1 = sy0 + 1;
-                if (y+1 < h) sy1 = binfory[y+1];
-
-                double value = 0.0;
-
-                if (interpolate && fabs(sy1 - sy0) < 1.0) {
-
-                    double centre = (sy0 + sy1) / 2;
-                    double dist = (centre - 0.5) - rint(centre - 0.5);
-                    int bin = int(centre);
-                    int other = (dist < 0 ? (bin-1) : (bin+1));
-                    if (bin < minbin) bin = minbin;
-                    if (bin > maxbin) bin = maxbin;
-                    if (other < minbin || other > maxbin) other = bin;
-                    double prop = 1.0 - fabs(dist);
-
-                    double v0 = values[bin - minbin];
-                    double v1 = values[other - minbin];
-                    if (m_binDisplay == PeakBins) {
-                        if (bin == minbin || bin == maxbin ||
-                            v0 < values[bin-minbin-1] ||
-                            v0 < values[bin-minbin+1]) v0 = 0.0;
-                        if (other == minbin || other == maxbin ||
-                            v1 < values[other-minbin-1] ||
-                            v1 < values[other-minbin+1]) v1 = 0.0;
-                    }
-                    if (v0 == 0.0 && v1 == 0.0) continue;
-                    value = prop * v0 + (1.0 - prop) * v1;
-
-                    if (m_colourScale != PhaseColourScale) {
-                        if (!m_normalizeColumns) {
-                            value /= (m_fftSize/2.0);
-                        }
-                        mag.sample(float(value));
-                        value *= m_gain;
-                    }
-
-                    peaks[y] = float(value);
-
-                } else {                    
-
-                    int by0 = int(sy0 + 0.0001);
-                    int by1 = int(sy1 + 0.0001);
-                    if (by1 < by0 + 1) by1 = by0 + 1;
-
-                    for (int bin = by0; bin < by1; ++bin) {
-
-                        value = values[bin - minbin];
-                        if (m_binDisplay == PeakBins) {
-                            if (bin == minbin || bin == maxbin ||
-                                value < values[bin-minbin-1] ||
-                                value < values[bin-minbin+1]) continue;
-                        }
-
-                        if (m_colourScale != PhaseColourScale) {
-                            if (!m_normalizeColumns) {
-                                value /= (m_fftSize/2.0);
-                            }
-                            mag.sample(float(value));
-                            value *= m_gain;
-                        }
-
-                        if (value > peaks[y]) {
-                            peaks[y] = float(value); //!!! not right for phase!
-                        }
-                    }
-                }
-            }
-
-            if (mag.isSet()) {
-                if (sx >= int(m_columnMags.size())) {
-#ifdef DEBUG_SPECTROGRAM
-                    cerr << "INTERNAL ERROR: " << sx << " >= "
-                              << m_columnMags.size()
-                              << " at SpectrogramLayer.cpp::paintDrawBuffer"
-                              << endl;
-#endif
-                } else {
-                    m_columnMags[sx].sample(mag);
-                    if (overallMag.sample(mag)) overallMagChanged = true;
-                }
-            }
-        }
-
-        for (int y = 0; y < h; ++y) {
-
-            double peak = peaks[y];
-            
-            if (m_colourScale != PhaseColourScale &&
-                (m_normalizeColumns || m_normalizeHybrid) && 
-                columnMax > 0.f) {
-                peak /= columnMax;
-                if (m_normalizeHybrid) {
-                    peak *= log10(columnMax);
-                }
-            }
-            
-            unsigned char peakpix = getDisplayValue(v, peak);
-
-            m_drawBuffer.setPixel(x, h-y-1, peakpix);
-        }
-    }
-
-    return true;
-}
-
 void
-SpectrogramLayer::illuminateLocalFeatures(View *v, QPainter &paint) const
+SpectrogramLayer::illuminateLocalFeatures(LayerGeometryProvider *v, QPainter &paint) const
 {
     Profiler profiler("SpectrogramLayer::illuminateLocalFeatures");
 
@@ -2828,8 +1607,10 @@
         return;
     }
 
-//    cerr << "SpectrogramLayer: illuminateLocalFeatures("
-//              << localPos.x() << "," << localPos.y() << ")" << endl;
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+    cerr << "SpectrogramLayer: illuminateLocalFeatures("
+              << localPos.x() << "," << localPos.y() << ")" << endl;
+#endif
 
     double s0, s1;
     double f0, f1;
@@ -2846,8 +1627,10 @@
         int y1 = int(getYForFrequency(v, f1));
         int y0 = int(getYForFrequency(v, f0));
         
-//        cerr << "SpectrogramLayer: illuminate "
-//                  << x0 << "," << y1 << " -> " << x1 << "," << y0 << endl;
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+        cerr << "SpectrogramLayer: illuminate "
+                  << x0 << "," << y1 << " -> " << x1 << "," << y0 << endl;
+#endif
         
         paint.setPen(v->getForeground());
 
@@ -2858,41 +1641,39 @@
 }
 
 double
-SpectrogramLayer::getYForFrequency(const View *v, double frequency) const
+SpectrogramLayer::getYForFrequency(const LayerGeometryProvider *v, double frequency) const
 {
     return v->getYForFrequency(frequency,
 			       getEffectiveMinFrequency(),
 			       getEffectiveMaxFrequency(),
-			       m_frequencyScale == LogFrequencyScale);
+			       m_binScale == BinScale::Log);
 }
 
 double
-SpectrogramLayer::getFrequencyForY(const View *v, int y) const
+SpectrogramLayer::getFrequencyForY(const LayerGeometryProvider *v, int y) const
 {
     return v->getFrequencyForY(y,
 			       getEffectiveMinFrequency(),
 			       getEffectiveMaxFrequency(),
-			       m_frequencyScale == LogFrequencyScale);
+			       m_binScale == BinScale::Log);
 }
 
 int
-SpectrogramLayer::getCompletion(View *v) const
+SpectrogramLayer::getCompletion(LayerGeometryProvider *) const
 {
-    if (m_updateTimer == 0) return 100;
-    if (m_fftModels.find(v) == m_fftModels.end()) return 100;
-
-    int completion = m_fftModels[v].first->getCompletion();
+    if (!m_fftModel) return 100;
+    int completion = m_fftModel->getCompletion();
 #ifdef DEBUG_SPECTROGRAM_REPAINT
-    SVDEBUG << "SpectrogramLayer::getCompletion: completion = " << completion << endl;
+    cerr << "SpectrogramLayer::getCompletion: completion = " << completion << endl;
 #endif
     return completion;
 }
 
 QString
-SpectrogramLayer::getError(View *v) const
+SpectrogramLayer::getError(LayerGeometryProvider *) const
 {
-    if (m_fftModels.find(v) == m_fftModels.end()) return "";
-    return m_fftModels[v].first->getError();
+    if (!m_fftModel) return "";
+    return m_fftModel->getError();
 }
 
 bool
@@ -2902,10 +1683,10 @@
     if (!m_model) return false;
 
     sv_samplerate_t sr = m_model->getSampleRate();
-    min = double(sr) / m_fftSize;
+    min = double(sr) / getFFTSize();
     max = double(sr) / 2;
     
-    logarithmic = (m_frequencyScale == LogFrequencyScale);
+    logarithmic = (m_binScale == BinScale::Log);
     unit = "Hz";
     return true;
 }
@@ -2935,7 +1716,7 @@
 
     if (m_minFrequency == minf && m_maxFrequency == maxf) return true;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     invalidateMagnitudes();
 
     m_minFrequency = minf;
@@ -2953,7 +1734,7 @@
 }
 
 bool
-SpectrogramLayer::getYScaleValue(const View *v, int y,
+SpectrogramLayer::getYScaleValue(const LayerGeometryProvider *v, int y,
                                  double &value, QString &unit) const
 {
     value = getFrequencyForY(v, y);
@@ -2962,7 +1743,7 @@
 }
 
 bool
-SpectrogramLayer::snapToFeatureFrame(View *,
+SpectrogramLayer::snapToFeatureFrame(LayerGeometryProvider *,
                                      sv_frame_t &frame,
 				     int &resolution,
 				     SnapType snap) const
@@ -2985,17 +1766,12 @@
 } 
 
 void
-SpectrogramLayer::measureDoubleClick(View *v, QMouseEvent *e)
+SpectrogramLayer::measureDoubleClick(LayerGeometryProvider *v, QMouseEvent *e)
 {
-    ImageCache &cache = m_imageCaches[v];
-
-    cerr << "cache width: " << cache.image.width() << ", height: "
-              << cache.image.height() << endl;
-
-    QImage image = cache.image;
-
-    ImageRegionFinder finder;
-    QRect rect = finder.findRegionExtents(&image, e->pos());
+    const Colour3DPlotRenderer *renderer = getRenderer(v);
+    if (!renderer) return;
+
+    QRect rect = renderer->findSimilarRegionExtents(e->pos());
     if (rect.isValid()) {
         MeasureRect mr;
         setMeasureRectFromPixrect(v, mr, rect);
@@ -3005,11 +1781,11 @@
 }
 
 bool
-SpectrogramLayer::getCrosshairExtents(View *v, QPainter &paint,
+SpectrogramLayer::getCrosshairExtents(LayerGeometryProvider *v, QPainter &paint,
                                       QPoint cursorPos,
-                                      std::vector<QRect> &extents) const
+                                      vector<QRect> &extents) const
 {
-    QRect vertical(cursorPos.x() - 12, 0, 12, v->height());
+    QRect vertical(cursorPos.x() - 12, 0, 12, v->getPaintHeight());
     extents.push_back(vertical);
 
     QRect horizontal(0, cursorPos.y(), cursorPos.x(), 1);
@@ -3028,14 +1804,14 @@
     extents.push_back(pitch);
 
     QRect rt(cursorPos.x(),
-             v->height() - paint.fontMetrics().height() - 2,
+             v->getPaintHeight() - paint.fontMetrics().height() - 2,
              paint.fontMetrics().width("1234.567 s"),
              paint.fontMetrics().height());
     extents.push_back(rt);
 
     int w(paint.fontMetrics().width("1234567890") + 2);
     QRect frame(cursorPos.x() - w - 2,
-                v->height() - paint.fontMetrics().height() - 2,
+                v->getPaintHeight() - paint.fontMetrics().height() - 2,
                 w,
                 paint.fontMetrics().height());
     extents.push_back(frame);
@@ -3044,7 +1820,7 @@
 }
 
 void
-SpectrogramLayer::paintCrosshairs(View *v, QPainter &paint,
+SpectrogramLayer::paintCrosshairs(LayerGeometryProvider *v, QPainter &paint,
                                   QPoint cursorPos) const
 {
     paint.save();
@@ -3059,46 +1835,46 @@
     paint.setPen(m_crosshairColour);
 
     paint.drawLine(0, cursorPos.y(), cursorPos.x() - 1, cursorPos.y());
-    paint.drawLine(cursorPos.x(), 0, cursorPos.x(), v->height());
+    paint.drawLine(cursorPos.x(), 0, cursorPos.x(), v->getPaintHeight());
     
     double fundamental = getFrequencyForY(v, cursorPos.y());
 
-    v->drawVisibleText(paint,
+    PaintAssistant::drawVisibleText(v, paint,
                        sw + 2,
                        cursorPos.y() - 2,
                        QString("%1 Hz").arg(fundamental),
-                       View::OutlinedText);
+                       PaintAssistant::OutlinedText);
 
     if (Pitch::isFrequencyInMidiRange(fundamental)) {
         QString pitchLabel = Pitch::getPitchLabelForFrequency(fundamental);
-        v->drawVisibleText(paint,
+        PaintAssistant::drawVisibleText(v, paint,
                            sw + 2,
                            cursorPos.y() + paint.fontMetrics().ascent() + 2,
                            pitchLabel,
-                           View::OutlinedText);
+                           PaintAssistant::OutlinedText);
     }
 
     sv_frame_t frame = v->getFrameForX(cursorPos.x());
     RealTime rt = RealTime::frame2RealTime(frame, m_model->getSampleRate());
     QString rtLabel = QString("%1 s").arg(rt.toText(true).c_str());
     QString frameLabel = QString("%1").arg(frame);
-    v->drawVisibleText(paint,
+    PaintAssistant::drawVisibleText(v, paint,
                        cursorPos.x() - paint.fontMetrics().width(frameLabel) - 2,
-                       v->height() - 2,
+                       v->getPaintHeight() - 2,
                        frameLabel,
-                       View::OutlinedText);
-    v->drawVisibleText(paint,
+                       PaintAssistant::OutlinedText);
+    PaintAssistant::drawVisibleText(v, paint,
                        cursorPos.x() + 2,
-                       v->height() - 2,
+                       v->getPaintHeight() - 2,
                        rtLabel,
-                       View::OutlinedText);
+                       PaintAssistant::OutlinedText);
 
     int harmonic = 2;
 
     while (harmonic < 100) {
 
         int hy = int(lrint(getYForFrequency(v, fundamental * harmonic)));
-        if (hy < 0 || hy > v->height()) break;
+        if (hy < 0 || hy > v->getPaintHeight()) break;
         
         int len = 7;
 
@@ -3122,7 +1898,7 @@
 }
 
 QString
-SpectrogramLayer::getFeatureDescription(View *v, QPoint &pos) const
+SpectrogramLayer::getFeatureDescription(LayerGeometryProvider *v, QPoint &pos) const
 {
     int x = pos.x();
     int y = pos.y();
@@ -3147,7 +1923,7 @@
 
     QString adjFreqText = "", adjPitchText = "";
 
-    if (m_binDisplay == PeakFrequencies) {
+    if (m_binDisplay == BinDisplay::PeakFrequencies) {
 
 	if (!getAdjustedYBinSourceRange(v, x, y, freqMin, freqMax,
 					adjFreqMin, adjFreqMax)) {
@@ -3209,12 +1985,12 @@
 	QString dbMinString;
 	QString dbMaxString;
 	if (dbMin == AudioLevel::DB_FLOOR) {
-	    dbMinString = tr("-Inf");
+	    dbMinString = Strings::minus_infinity;
 	} else {
 	    dbMinString = QString("%1").arg(lrint(dbMin));
 	}
 	if (dbMax == AudioLevel::DB_FLOOR) {
-	    dbMaxString = tr("-Inf");
+	    dbMaxString = Strings::minus_infinity;
 	} else {
 	    dbMaxString = QString("%1").arg(lrint(dbMax));
 	}
@@ -3244,7 +2020,7 @@
 }
 
 int
-SpectrogramLayer::getVerticalScaleWidth(View *, bool detailed, QPainter &paint) const
+SpectrogramLayer::getVerticalScaleWidth(LayerGeometryProvider *, bool detailed, QPainter &paint) const
 {
     if (!m_model || !m_model->isOK()) return 0;
 
@@ -3259,13 +2035,14 @@
     int fw = paint.fontMetrics().width(tr("43Hz"));
     if (tw < fw) tw = fw;
 
-    int tickw = (m_frequencyScale == LogFrequencyScale ? 10 : 4);
+    int tickw = (m_binScale == BinScale::Log ? 10 : 4);
     
     return cw + tickw + tw + 13;
 }
 
 void
-SpectrogramLayer::paintVerticalScale(View *v, bool detailed, QPainter &paint, QRect rect) const
+SpectrogramLayer::paintVerticalScale(LayerGeometryProvider *v, bool detailed,
+                                     QPainter &paint, QRect rect) const
 {
     if (!m_model || !m_model->isOK()) {
 	return;
@@ -3274,115 +2051,40 @@
     Profiler profiler("SpectrogramLayer::paintVerticalScale");
 
     //!!! cache this?
-
+    
     int h = rect.height(), w = rect.width();
-
-    int tickw = (m_frequencyScale == LogFrequencyScale ? 10 : 4);
-    int pkw = (m_frequencyScale == LogFrequencyScale ? 10 : 0);
-
-    int bins = m_fftSize / 2;
+    int textHeight = paint.fontMetrics().height();
+
+    if (detailed && (h > textHeight * 3 + 10)) {
+        paintDetailedScale(v, paint, rect);
+    }
+    m_haveDetailedScale = detailed;
+
+    int tickw = (m_binScale == BinScale::Log ? 10 : 4);
+    int pkw = (m_binScale == BinScale::Log ? 10 : 0);
+
+    int bins = getFFTSize() / 2;
     sv_samplerate_t sr = m_model->getSampleRate();
 
     if (m_maxFrequency > 0) {
-	bins = int((double(m_maxFrequency) * m_fftSize) / sr + 0.1);
-	if (bins > m_fftSize / 2) bins = m_fftSize / 2;
+	bins = int((double(m_maxFrequency) * getFFTSize()) / sr + 0.1);
+	if (bins > getFFTSize() / 2) bins = getFFTSize() / 2;
     }
 
     int cw = 0;
-
     if (detailed) cw = getColourScaleWidth(paint);
-    int cbw = paint.fontMetrics().width("dB");
 
     int py = -1;
-    int textHeight = paint.fontMetrics().height();
     int toff = -textHeight + paint.fontMetrics().ascent() + 2;
 
-    if (detailed && (h > textHeight * 3 + 10)) {
-
-        int topLines = 2;
-        if (m_colourScale == PhaseColourScale) topLines = 1;
-
-	int ch = h - textHeight * (topLines + 1) - 8;
-//	paint.drawRect(4, textHeight + 4, cw - 1, ch + 1);
-	paint.drawRect(4 + cw - cbw, textHeight * topLines + 4, cbw - 1, ch + 1);
-
-	QString top, bottom;
-        double min = m_viewMags[v].getMin();
-        double max = m_viewMags[v].getMax();
-
-        double dBmin = AudioLevel::multiplier_to_dB(min);
-        double dBmax = AudioLevel::multiplier_to_dB(max);
-
-        if (dBmax < -60.f) dBmax = -60.f;
-        else top = QString("%1").arg(lrint(dBmax));
-
-        if (dBmin < dBmax - 60.f) dBmin = dBmax - 60.f;
-        bottom = QString("%1").arg(lrint(dBmin));
-
-        //!!! & phase etc
-
-        if (m_colourScale != PhaseColourScale) {
-            paint.drawText((cw + 6 - paint.fontMetrics().width("dBFS")) / 2,
-                           2 + textHeight + toff, "dBFS");
-        }
-
-//	paint.drawText((cw + 6 - paint.fontMetrics().width(top)) / 2,
-	paint.drawText(3 + cw - cbw - paint.fontMetrics().width(top),
-		       2 + textHeight * topLines + toff + textHeight/2, top);
-
-	paint.drawText(3 + cw - cbw - paint.fontMetrics().width(bottom),
-		       h + toff - 3 - textHeight/2, bottom);
-
-	paint.save();
-	paint.setBrush(Qt::NoBrush);
-
-        int lasty = 0;
-        int lastdb = 0;
-
-	for (int i = 0; i < ch; ++i) {
-
-            double dBval = dBmin + (((dBmax - dBmin) * i) / (ch - 1));
-            int idb = int(dBval);
-
-            double value = AudioLevel::dB_to_multiplier(dBval);
-            int colour = getDisplayValue(v, value * m_gain);
-
-	    paint.setPen(m_palette.getColour((unsigned char)colour));
-
-            int y = textHeight * topLines + 4 + ch - i;
-
-            paint.drawLine(5 + cw - cbw, y, cw + 2, y);
-
-            if (i == 0) {
-                lasty = y;
-                lastdb = idb;
-            } else if (i < ch - paint.fontMetrics().ascent() &&
-                       idb != lastdb &&
-                       ((abs(y - lasty) > textHeight && 
-                         idb % 10 == 0) ||
-                        (abs(y - lasty) > paint.fontMetrics().ascent() && 
-                         idb % 5 == 0))) {
-                paint.setPen(v->getBackground());
-                QString text = QString("%1").arg(idb);
-                paint.drawText(3 + cw - cbw - paint.fontMetrics().width(text),
-                               y + toff + textHeight/2, text);
-                paint.setPen(v->getForeground());
-                paint.drawLine(5 + cw - cbw, y, 8 + cw - cbw, y);
-                lasty = y;
-                lastdb = idb;
-            }
-	}
-	paint.restore();
-    }
-
     paint.drawLine(cw + 7, 0, cw + 7, h);
 
     int bin = -1;
 
-    for (int y = 0; y < v->height(); ++y) {
+    for (int y = 0; y < v->getPaintHeight(); ++y) {
 
 	double q0, q1;
-	if (!getYBinRange(v, v->height() - y, q0, q1)) continue;
+	if (!getYBinRange(v, v->getPaintHeight() - y, q0, q1)) continue;
 
 	int vy;
 
@@ -3393,10 +2095,10 @@
 	    continue;
 	}
 
-	int freq = int((sr * bin) / m_fftSize);
+	int freq = int((sr * bin) / getFFTSize());
 
 	if (py >= 0 && (vy - py) < textHeight - 1) {
-	    if (m_frequencyScale == LinearFrequencyScale) {
+	    if (m_binScale == BinScale::Linear) {
 		paint.drawLine(w - tickw, h - vy, w, h - vy);
 	    }
 	    continue;
@@ -3407,14 +2109,14 @@
 	paint.drawLine(cw + 7, h - vy, w - pkw - 1, h - vy);
 
 	if (h - vy - textHeight >= -2) {
-	    int tx = w - 3 - paint.fontMetrics().width(text) - std::max(tickw, pkw);
+	    int tx = w - 3 - paint.fontMetrics().width(text) - max(tickw, pkw);
 	    paint.drawText(tx, h - vy + toff, text);
 	}
 
 	py = vy;
     }
 
-    if (m_frequencyScale == LogFrequencyScale) {
+    if (m_binScale == BinScale::Log) {
 
         // piano keyboard
 
@@ -3426,6 +2128,152 @@
     m_haveDetailedScale = detailed;
 }
 
+void
+SpectrogramLayer::paintDetailedScale(LayerGeometryProvider *v,
+                                     QPainter &paint, QRect rect) const
+{
+    // The colour scale
+
+    if (m_colourScale == ColourScaleType::Phase) {
+        paintDetailedScalePhase(v, paint, rect);
+        return;
+    }
+    
+    int h = rect.height();
+    int textHeight = paint.fontMetrics().height();
+    int toff = -textHeight + paint.fontMetrics().ascent() + 2;
+
+    int cw = getColourScaleWidth(paint);
+    int cbw = paint.fontMetrics().width("dB");
+
+    int topLines = 2;
+
+    int ch = h - textHeight * (topLines + 1) - 8;
+//	paint.drawRect(4, textHeight + 4, cw - 1, ch + 1);
+    paint.drawRect(4 + cw - cbw, textHeight * topLines + 4, cbw - 1, ch + 1);
+
+    QString top, bottom;
+    double min = m_viewMags[v->getId()].getMin();
+    double max = m_viewMags[v->getId()].getMax();
+
+    if (min < m_threshold) min = m_threshold;
+    if (max <= min) max = min + 0.1;
+        
+    double dBmin = AudioLevel::multiplier_to_dB(min);
+    double dBmax = AudioLevel::multiplier_to_dB(max);
+
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+    cerr << "paintVerticalScale: for view id " << v->getId()
+         << ": min = " << min << ", max = " << max
+         << ", dBmin = " << dBmin << ", dBmax = " << dBmax << endl;
+#endif
+        
+    if (dBmax < -60.f) dBmax = -60.f;
+    else top = QString("%1").arg(lrint(dBmax));
+
+    if (dBmin < dBmax - 60.f) dBmin = dBmax - 60.f;
+    bottom = QString("%1").arg(lrint(dBmin));
+
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+    cerr << "adjusted dB range to min = " << dBmin << ", max = " << dBmax
+         << endl;
+#endif
+        
+    paint.drawText((cw + 6 - paint.fontMetrics().width("dBFS")) / 2,
+                   2 + textHeight + toff, "dBFS");
+
+    paint.drawText(3 + cw - cbw - paint.fontMetrics().width(top),
+                   2 + textHeight * topLines + toff + textHeight/2, top);
+
+    paint.drawText(3 + cw - cbw - paint.fontMetrics().width(bottom),
+                   h + toff - 3 - textHeight/2, bottom);
+
+    paint.save();
+    paint.setBrush(Qt::NoBrush);
+
+    int lasty = 0;
+    int lastdb = 0;
+
+    for (int i = 0; i < ch; ++i) {
+
+        double dBval = dBmin + (((dBmax - dBmin) * i) / (ch - 1));
+        int idb = int(dBval);
+
+        double value = AudioLevel::dB_to_multiplier(dBval);
+        paint.setPen(getRenderer(v)->getColour(value));
+
+        int y = textHeight * topLines + 4 + ch - i;
+
+        paint.drawLine(5 + cw - cbw, y, cw + 2, y);
+        
+        if (i == 0) {
+            lasty = y;
+            lastdb = idb;
+        } else if (i < ch - paint.fontMetrics().ascent() &&
+                   idb != lastdb &&
+                   ((abs(y - lasty) > textHeight && 
+                     idb % 10 == 0) ||
+                    (abs(y - lasty) > paint.fontMetrics().ascent() && 
+                     idb % 5 == 0))) {
+            paint.setPen(v->getForeground());
+            QString text = QString("%1").arg(idb);
+            paint.drawText(3 + cw - cbw - paint.fontMetrics().width(text),
+                           y + toff + textHeight/2, text);
+            paint.drawLine(5 + cw - cbw, y, 8 + cw - cbw, y);
+            lasty = y;
+            lastdb = idb;
+        }
+    }
+    paint.restore();
+}
+
+void
+SpectrogramLayer::paintDetailedScalePhase(LayerGeometryProvider *v,
+                                          QPainter &paint, QRect rect) const
+{
+    // The colour scale in phase mode
+    
+    int h = rect.height();
+    int textHeight = paint.fontMetrics().height();
+    int toff = -textHeight + paint.fontMetrics().ascent() + 2;
+
+    int cw = getColourScaleWidth(paint);
+
+    // Phase is not measured in dB of course, but this places the
+    // scale at the same position as in the magnitude spectrogram
+    int cbw = paint.fontMetrics().width("dB");
+
+    int topLines = 1;
+
+    int ch = h - textHeight * (topLines + 1) - 8;
+    paint.drawRect(4 + cw - cbw, textHeight * topLines + 4, cbw - 1, ch + 1);
+
+    QString top = Strings::pi, bottom = Strings::minus_pi, middle = "0";
+    
+    double min = -M_PI;
+    double max =  M_PI;
+
+    paint.drawText(3 + cw - cbw - paint.fontMetrics().width(top),
+                   2 + textHeight * topLines + toff + textHeight/2, top);
+
+    paint.drawText(3 + cw - cbw - paint.fontMetrics().width(middle),
+                   2 + textHeight * topLines + ch/2 + toff + textHeight/2, middle);
+
+    paint.drawText(3 + cw - cbw - paint.fontMetrics().width(bottom),
+                   h + toff - 3 - textHeight/2, bottom);
+
+    paint.save();
+    paint.setBrush(Qt::NoBrush);
+
+    for (int i = 0; i < ch; ++i) {
+        double val = min + (((max - min) * i) / (ch - 1));
+        paint.setPen(getRenderer(v)->getColour(val));
+        int y = textHeight * topLines + 4 + ch - i;
+        paint.drawLine(5 + cw - cbw, y, cw + 2, y);
+    }
+    paint.restore();
+}
+
 class SpectrogramRangeMapper : public RangeMapper
 {
 public:
@@ -3490,9 +2338,9 @@
 
     sv_samplerate_t sr = m_model->getSampleRate();
 
-    SpectrogramRangeMapper mapper(sr, m_fftSize);
-
-//    int maxStep = mapper.getPositionForValue((double(sr) / m_fftSize) + 0.001);
+    SpectrogramRangeMapper mapper(sr, getFFTSize());
+
+//    int maxStep = mapper.getPositionForValue((double(sr) / getFFTSize()) + 0.001);
     int maxStep = mapper.getPositionForValue(0);
     int minStep = mapper.getPositionForValue(double(sr) / 2);
 
@@ -3514,7 +2362,7 @@
     double dmin, dmax;
     getDisplayExtents(dmin, dmax);
     
-    SpectrogramRangeMapper mapper(m_model->getSampleRate(), m_fftSize);
+    SpectrogramRangeMapper mapper(m_model->getSampleRate(), getFFTSize());
     int n = mapper.getPositionForValue(dmax - dmin);
 //    SVDEBUG << "SpectrogramLayer::getCurrentVerticalZoomStep: " << n << endl;
     return n;
@@ -3531,12 +2379,12 @@
 //    cerr << "current range " << dmin << " -> " << dmax << ", range " << dmax-dmin << ", mid " << (dmax + dmin)/2 << endl;
     
     sv_samplerate_t sr = m_model->getSampleRate();
-    SpectrogramRangeMapper mapper(sr, m_fftSize);
+    SpectrogramRangeMapper mapper(sr, getFFTSize());
     double newdist = mapper.getValueForPosition(step);
 
     double newmin, newmax;
 
-    if (m_frequencyScale == LogFrequencyScale) {
+    if (m_binScale == BinScale::Log) {
 
         // need to pick newmin and newmax such that
         //
@@ -3592,11 +2440,11 @@
 SpectrogramLayer::getNewVerticalZoomRangeMapper() const
 {
     if (!m_model) return 0;
-    return new SpectrogramRangeMapper(m_model->getSampleRate(), m_fftSize);
+    return new SpectrogramRangeMapper(m_model->getSampleRate(), getFFTSize());
 }
 
 void
-SpectrogramLayer::updateMeasureRectYCoords(View *v, const MeasureRect &r) const
+SpectrogramLayer::updateMeasureRectYCoords(LayerGeometryProvider *v, const MeasureRect &r) const
 {
     int y0 = 0;
     if (r.startY > 0.0) y0 = int(getYForFrequency(v, r.startY));
@@ -3610,7 +2458,7 @@
 }
 
 void
-SpectrogramLayer::setMeasureRectYCoord(View *v, MeasureRect &r, bool start, int y) const
+SpectrogramLayer::setMeasureRectYCoord(LayerGeometryProvider *v, MeasureRect &r, bool start, int y) const
 {
     if (start) {
         r.startY = getFrequencyForY(v, y);
@@ -3648,19 +2496,35 @@
 		 "binDisplay=\"%7\" ")
 	.arg(m_minFrequency)
 	.arg(m_maxFrequency)
-	.arg(m_colourScale)
+	.arg(convertFromColourScale(m_colourScale, m_colourScaleMultiple))
 	.arg(m_colourMap)
 	.arg(m_colourRotation)
-	.arg(m_frequencyScale)
-	.arg(m_binDisplay);
-
-    s += QString("normalizeColumns=\"%1\" "
-                 "normalizeVisibleArea=\"%2\" "
-                 "normalizeHybrid=\"%3\" ")
-	.arg(m_normalizeColumns ? "true" : "false")
-        .arg(m_normalizeVisibleArea ? "true" : "false")
-        .arg(m_normalizeHybrid ? "true" : "false");
-
+	.arg(int(m_binScale))
+	.arg(int(m_binDisplay));
+
+    // New-style normalization attributes, allowing for more types of
+    // normalization in future: write out the column normalization
+    // type separately, and then whether we are normalizing visible
+    // area as well afterwards
+    
+    s += QString("columnNormalization=\"%1\" ")
+        .arg(m_normalization == ColumnNormalization::Max1 ? "peak" :
+             m_normalization == ColumnNormalization::Hybrid ? "hybrid" : "none");
+
+    // Old-style normalization attribute. We *don't* write out
+    // normalizeHybrid here because the only release that would accept
+    // it (Tony v1.0) has a totally different scale factor for
+    // it. We'll just have to accept that session files from Tony
+    // v2.0+ will look odd in Tony v1.0
+    
+    s += QString("normalizeColumns=\"%1\" ")
+	.arg(m_normalization == ColumnNormalization::Max1 ? "true" : "false");
+
+    // And this applies to both old- and new-style attributes
+    
+    s += QString("normalizeVisibleArea=\"%1\" ")
+        .arg(m_normalizeVisibleArea ? "true" : "false");
+    
     Layer::toXml(stream, indent, extraAttributes + " " + s);
 }
 
@@ -3707,9 +2571,12 @@
         setMaxFrequency(maxFrequency);
     }
 
-    ColourScale colourScale = (ColourScale)
-	attributes.value("colourScale").toInt(&ok);
-    if (ok) setColourScale(colourScale);
+    auto colourScale = convertToColourScale
+        (attributes.value("colourScale").toInt(&ok));
+    if (ok) {
+        setColourScale(colourScale.first);
+        setColourScaleMultiple(colourScale.second);
+    }
 
     int colourMap = attributes.value("colourScheme").toInt(&ok);
     if (ok) setColourMap(colourMap);
@@ -3717,24 +2584,60 @@
     int colourRotation = attributes.value("colourRotation").toInt(&ok);
     if (ok) setColourRotation(colourRotation);
 
-    FrequencyScale frequencyScale = (FrequencyScale)
+    BinScale binScale = (BinScale)
 	attributes.value("frequencyScale").toInt(&ok);
-    if (ok) setFrequencyScale(frequencyScale);
+    if (ok) setBinScale(binScale);
 
     BinDisplay binDisplay = (BinDisplay)
 	attributes.value("binDisplay").toInt(&ok);
     if (ok) setBinDisplay(binDisplay);
 
-    bool normalizeColumns =
-	(attributes.value("normalizeColumns").trimmed() == "true");
-    setNormalizeColumns(normalizeColumns);
+    bool haveNewStyleNormalization = false;
+    
+    QString columnNormalization = attributes.value("columnNormalization");
+
+    if (columnNormalization != "") {
+
+        haveNewStyleNormalization = true;
+
+        if (columnNormalization == "peak") {
+            setNormalization(ColumnNormalization::Max1);
+        } else if (columnNormalization == "hybrid") {
+            setNormalization(ColumnNormalization::Hybrid);
+        } else if (columnNormalization == "none") {
+            setNormalization(ColumnNormalization::None);
+        } else {
+            cerr << "NOTE: Unknown or unsupported columnNormalization attribute \""
+                 << columnNormalization << "\"" << endl;
+        }
+    }
+
+    if (!haveNewStyleNormalization) {
+
+        bool normalizeColumns =
+            (attributes.value("normalizeColumns").trimmed() == "true");
+        if (normalizeColumns) {
+            setNormalization(ColumnNormalization::Max1);
+        }
+
+        bool normalizeHybrid =
+            (attributes.value("normalizeHybrid").trimmed() == "true");
+        if (normalizeHybrid) {
+            setNormalization(ColumnNormalization::Hybrid);
+        }
+    }
 
     bool normalizeVisibleArea =
-	(attributes.value("normalizeVisibleArea").trimmed() == "true");
+        (attributes.value("normalizeVisibleArea").trimmed() == "true");
     setNormalizeVisibleArea(normalizeVisibleArea);
 
-    bool normalizeHybrid =
-	(attributes.value("normalizeHybrid").trimmed() == "true");
-    setNormalizeHybrid(normalizeHybrid);
+    if (!haveNewStyleNormalization && m_normalization == ColumnNormalization::Hybrid) {
+        // Tony v1.0 is (and hopefully will remain!) the only released
+        // SV-a-like to use old-style attributes when saving sessions
+        // that ask for hybrid normalization. It saves them with the
+        // wrong gain factor, so hack in a fix for that here -- this
+        // gives us backward but not forward compatibility.
+        setGain(m_gain / float(getFFTSize() / 2));
+    }
 }
     
--- a/layer/SpectrogramLayer.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/SpectrogramLayer.h	Fri Jan 13 10:29:50 2017 +0000
@@ -13,11 +13,12 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _SPECTROGRAM_LAYER_H_
-#define _SPECTROGRAM_LAYER_H_
+#ifndef SPECTROGRAM_LAYER_H
+#define SPECTROGRAM_LAYER_H
 
 #include "SliceableLayer.h"
 #include "base/Window.h"
+#include "base/MagnitudeRange.h"
 #include "base/RealTime.h"
 #include "base/Thread.h"
 #include "base/PropertyContainer.h"
@@ -25,6 +26,10 @@
 #include "data/model/DenseTimeValueModel.h"
 #include "data/model/FFTModel.h"
 
+#include "VerticalBinLayer.h"
+#include "ColourScale.h"
+#include "Colour3DPlotRenderer.h"
+
 #include <QMutex>
 #include <QWaitCondition>
 #include <QImage>
@@ -38,13 +43,12 @@
 class FFTModel;
 class Dense3DModelPeakCache;
 
-
 /**
  * SpectrogramLayer represents waveform data (obtained from a
  * DenseTimeValueModel) in spectrogram form.
  */
 
-class SpectrogramLayer : public SliceableLayer,
+class SpectrogramLayer : public VerticalBinLayer,
 			 public PowerOfSqrtTwoZoomConstraint
 {
     Q_OBJECT
@@ -57,23 +61,23 @@
 
     virtual const ZoomConstraint *getZoomConstraint() const { return this; }
     virtual const Model *getModel() const { return m_model; }
-    virtual void paint(View *v, QPainter &paint, QRect rect) const;
+    virtual void paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
     virtual void setSynchronousPainting(bool synchronous);
 
-    virtual int getVerticalScaleWidth(View *v, bool detailed, QPainter &) const;
-    virtual void paintVerticalScale(View *v, bool detailed, QPainter &paint, QRect rect) const;
+    virtual int getVerticalScaleWidth(LayerGeometryProvider *v, bool detailed, QPainter &) const;
+    virtual void paintVerticalScale(LayerGeometryProvider *v, bool detailed, QPainter &paint, QRect rect) const;
 
-    virtual bool getCrosshairExtents(View *, QPainter &, QPoint cursorPos,
+    virtual bool getCrosshairExtents(LayerGeometryProvider *, QPainter &, QPoint cursorPos,
                                      std::vector<QRect> &extents) const;
-    virtual void paintCrosshairs(View *, QPainter &, QPoint) const;
+    virtual void paintCrosshairs(LayerGeometryProvider *, QPainter &, QPoint) const;
 
-    virtual QString getFeatureDescription(View *v, QPoint &) const;
+    virtual QString getFeatureDescription(LayerGeometryProvider *v, QPoint &) const;
 
-    virtual bool snapToFeatureFrame(View *v, sv_frame_t &frame,
+    virtual bool snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
 				    int &resolution,
 				    SnapType snap) const;
 
-    virtual void measureDoubleClick(View *, QMouseEvent *);
+    virtual void measureDoubleClick(LayerGeometryProvider *, QMouseEvent *);
 
     virtual bool hasLightBackground() const;
 
@@ -88,6 +92,8 @@
                                          int *min, int *max, int *deflt) const;
     virtual QString getPropertyValueLabel(const PropertyName &,
 					  int value) const;
+    virtual QString getPropertyValueIconName(const PropertyName &,
+                                             int value) const;
     virtual RangeMapper *getNewPropertyRangeMapper(const PropertyName &) const;
     virtual void setProperty(const PropertyName &, int value);
 
@@ -108,9 +114,6 @@
     void setWindowType(WindowType type);
     WindowType getWindowType() const;
 
-    void setZeroPadLevel(int level);
-    int getZeroPadLevel() const;
-
     /**
      * Set the gain multiplier for sample values in this view.
      * The default is 1.0.
@@ -122,7 +125,7 @@
      * Set the threshold for sample values to qualify for being shown
      * in the FFT, in voltage units.
      *
-     * The default is 0.0.
+     * The default is 10^-8 (-80dB).
      */
     void setThreshold(float threshold);
     float getThreshold() const;
@@ -133,64 +136,49 @@
     void setMaxFrequency(int); // 0 -> no maximum
     int getMaxFrequency() const;
 
-    enum ColourScale {
-	LinearColourScale,
-	MeterColourScale,
-        dBSquaredColourScale,
-	dBColourScale,
-	PhaseColourScale
-    };
+    /**
+     * Specify the scale for sample levels.  See ColourScale and
+     * WaveformLayer for comparison and details of meter and dB
+     * scaling.  The default is LogColourScale.
+     */
+    void setColourScale(ColourScaleType);
+    ColourScaleType getColourScale() const;
 
     /**
-     * Specify the scale for sample levels.  See WaveformLayer for
-     * details of meter and dB scaling.  The default is dBColourScale.
+     * Specify multiple factor for colour scale. This is 2.0 for
+     * log-power spectrogram and 1.0 otherwise.
      */
-    void setColourScale(ColourScale);
-    ColourScale getColourScale() const;
-
-    enum FrequencyScale {
-	LinearFrequencyScale,
-	LogFrequencyScale
-    };
+    void setColourScaleMultiple(double);
+    double getColourScaleMultiple() const;
     
     /**
      * Specify the scale for the y axis.
      */
-    void setFrequencyScale(FrequencyScale);
-    FrequencyScale getFrequencyScale() const;
+    void setBinScale(BinScale);
+    BinScale getBinScale() const;
 
-    enum BinDisplay {
-	AllBins,
-	PeakBins,
-	PeakFrequencies
-    };
-    
     /**
      * Specify the processing of frequency bins for the y axis.
      */
     void setBinDisplay(BinDisplay);
     BinDisplay getBinDisplay() const;
- 
-    /**
-     * Normalize each column to its maximum value, independent of its
-     * neighbours.
-     */
-    void setNormalizeColumns(bool n);
-    bool getNormalizeColumns() const;
 
     /**
-     * Normalize each value against the maximum in the visible region.
+     * Specify the normalization mode for individual columns.
      */
-    void setNormalizeVisibleArea(bool n);
+    void setNormalization(ColumnNormalization);
+    ColumnNormalization getNormalization() const;
+
+    /**
+     * Specify whether to normalize the visible area.
+     */
+    void setNormalizeVisibleArea(bool);
     bool getNormalizeVisibleArea() const;
 
     /**
-     * Normalize each column to its maximum value, and then scale by
-     * the log of the (absolute) maximum value.
+     * Specify the colour map. See ColourMapper for the colour map
+     * values.
      */
-    void setNormalizeHybrid(bool n);
-    bool getNormalizeHybrid() const;
-    
     void setColourMap(int map);
     int getColourMap() const;
 
@@ -210,11 +198,15 @@
         return ColourHasMeaningfulValue;
     }
 
-    double getYForFrequency(const View *v, double frequency) const;
-    double getFrequencyForY(const View *v, int y) const;
+    double getYForFrequency(const LayerGeometryProvider *v, double frequency) const;
+    double getFrequencyForY(const LayerGeometryProvider *v, int y) const;
 
-    virtual int getCompletion(View *v) const;
-    virtual QString getError(View *v) const;
+    //!!! VerticalBinLayer methods. Note overlap with get*BinRange()
+    double getYForBin(const LayerGeometryProvider *, double bin) const;
+    double getBinForY(const LayerGeometryProvider *, double y) const;
+    
+    virtual int getCompletion(LayerGeometryProvider *v) const;
+    virtual QString getError(LayerGeometryProvider *v) const;
 
     virtual bool getValueExtents(double &min, double &max,
                                  bool &logarithmic, QString &unit) const;
@@ -223,16 +215,16 @@
 
     virtual bool setDisplayExtents(double min, double max);
 
-    virtual bool getYScaleValue(const View *, int, double &, QString &) const;
+    virtual bool getYScaleValue(const LayerGeometryProvider *, int, double &, QString &) const;
 
     virtual void toXml(QTextStream &stream, QString indent = "",
                        QString extraAttributes = "") const;
 
     void setProperties(const QXmlAttributes &attributes);
 
-    virtual void setLayerDormant(const View *v, bool dormant);
+    virtual void setLayerDormant(const LayerGeometryProvider *v, bool dormant);
 
-    virtual bool isLayerScrollable(const View *) const { return false; }
+    virtual bool isLayerScrollable(const LayerGeometryProvider *) const { return false; }
 
     virtual int getVerticalZoomSteps(int &defaultStep) const;
     virtual int getCurrentVerticalZoomStep() const;
@@ -247,118 +239,58 @@
     
     void preferenceChanged(PropertyContainer::PropertyName name);
 
-    void fillTimerTimedOut();
-
 protected:
     const DenseTimeValueModel *m_model; // I do not own this
 
     int                 m_channel;
-    int              m_windowSize;
+    int                 m_windowSize;
     WindowType          m_windowType;
-    int              m_windowHopLevel;
-    int              m_zeroPadLevel;
-    int              m_fftSize;
+    int                 m_windowHopLevel;
     float               m_gain;
     float               m_initialGain;
     float               m_threshold;
     float               m_initialThreshold;
     int                 m_colourRotation;
     int                 m_initialRotation;
-    int              m_minFrequency;
-    int              m_maxFrequency;
-    int              m_initialMaxFrequency;
-    ColourScale         m_colourScale;
+    int                 m_minFrequency;
+    int                 m_maxFrequency;
+    int                 m_initialMaxFrequency;
+    ColourScaleType     m_colourScale;
+    double              m_colourScaleMultiple;
     int                 m_colourMap;
     QColor              m_crosshairColour;
-    FrequencyScale      m_frequencyScale;
+    BinScale            m_binScale;
     BinDisplay          m_binDisplay;
-    bool                m_normalizeColumns;
+    ColumnNormalization m_normalization; // of individual columns
     bool                m_normalizeVisibleArea;
-    bool                m_normalizeHybrid;
     int                 m_lastEmittedZoomStep;
     bool                m_synchronous;
 
     mutable bool        m_haveDetailedScale;
-    mutable int         m_lastPaintBlockWidth;
-    mutable RealTime    m_lastPaintTime;
 
-    enum { NO_VALUE = 0 }; // colour index for unused pixels
+    static std::pair<ColourScaleType, double> convertToColourScale(int value);
+    static int convertFromColourScale(ColourScaleType type, double multiple);
+    static std::pair<ColumnNormalization, bool> convertToColumnNorm(int value);
+    static int convertFromColumnNorm(ColumnNormalization norm, bool visible);
 
-    class Palette
-    {
-    public:
-        QColor getColour(unsigned char index) const {
-            return m_colours[index];
-        }
-    
-        void setColour(unsigned char index, QColor colour) {
-            m_colours[index] = colour;
-        }
-
-    private:
-        QColor m_colours[256];
-    };
-
-    Palette m_palette;
-
-    /**
-     * ImageCache covers the area of the view, at view resolution.
-     * Not all of it is necessarily valid at once (it is refreshed
-     * in parts when scrolling, for example).
-     */
-    struct ImageCache
-    {
-        QImage image;
-        QRect validArea;
-        sv_frame_t startFrame;
-        int zoomLevel;
-    };
-    typedef std::map<const View *, ImageCache> ViewImageCache;
-    void invalidateImageCaches();
-    void invalidateImageCaches(sv_frame_t startFrame, sv_frame_t endFrame);
-    mutable ViewImageCache m_imageCaches;
-
-    /**
-     * When painting, we draw directly onto the draw buffer and then
-     * copy this to the part of the image cache that needed refreshing
-     * before copying the image cache onto the window.  (Remind me why
-     * we don't draw directly onto the cache?)
-     */
-    mutable QImage m_drawBuffer;
-
-    mutable QTimer *m_updateTimer;
-
-    mutable sv_frame_t m_candidateFillStartFrame;
     bool m_exiting;
 
-    void initialisePalette();
-    void rotatePalette(int distance);
-
-    unsigned char getDisplayValue(View *v, double input) const;
-
     int getColourScaleWidth(QPainter &) const;
 
-    void illuminateLocalFeatures(View *v, QPainter &painter) const;
+    void illuminateLocalFeatures(LayerGeometryProvider *v, QPainter &painter) const;
 
     double getEffectiveMinFrequency() const;
     double getEffectiveMaxFrequency() const;
 
-    // Note that the getYBin... methods return the nominal bin in the
-    // un-smoothed spectrogram.  This is not necessarily the same bin
-    // as is pulled from the spectrogram and drawn at the given
-    // position, if the spectrogram has oversampling smoothing.  Use
-    // getSmoothedYBinRange to obtain that.
+    bool getXBinRange(LayerGeometryProvider *v, int x, double &windowMin, double &windowMax) const;
+    bool getYBinRange(LayerGeometryProvider *v, int y, double &freqBinMin, double &freqBinMax) const;
 
-    bool getXBinRange(View *v, int x, double &windowMin, double &windowMax) const;
-    bool getYBinRange(View *v, int y, double &freqBinMin, double &freqBinMax) const;
-    bool getSmoothedYBinRange(View *v, int y, double &freqBinMin, double &freqBinMax) const;
-
-    bool getYBinSourceRange(View *v, int y, double &freqMin, double &freqMax) const;
-    bool getAdjustedYBinSourceRange(View *v, int x, int y,
+    bool getYBinSourceRange(LayerGeometryProvider *v, int y, double &freqMin, double &freqMax) const;
+    bool getAdjustedYBinSourceRange(LayerGeometryProvider *v, int x, int y,
 				    double &freqMin, double &freqMax,
 				    double &adjFreqMin, double &adjFreqMax) const;
-    bool getXBinSourceRange(View *v, int x, RealTime &timeMin, RealTime &timeMax) const;
-    bool getXYBinSourceRange(View *v, int x, int y, double &min, double &max,
+    bool getXBinSourceRange(LayerGeometryProvider *v, int x, RealTime &timeMin, RealTime &timeMax) const;
+    bool getXYBinSourceRange(LayerGeometryProvider *v, int x, int y, double &min, double &max,
 			     double &phaseMin, double &phaseMax) const;
 
     int getWindowIncrement() const {
@@ -367,84 +299,39 @@
         else return m_windowSize / (1 << (m_windowHopLevel - 1));
     }
 
-    int getZeroPadLevel(const View *v) const;
-    int getFFTSize(const View *v) const;
-    FFTModel *getFFTModel(const View *v) const;
-    Dense3DModelPeakCache *getPeakCache(const View *v) const;
-    void invalidateFFTModels();
+    int getFFTOversampling() const;
+    int getFFTSize() const; // m_windowSize * getFFTOversampling()
 
-    typedef std::pair<FFTModel *, sv_frame_t> FFTFillPair; // model, last fill
-    typedef std::map<const View *, FFTFillPair> ViewFFTMap;
-    typedef std::map<const View *, Dense3DModelPeakCache *> PeakCacheMap;
-    mutable ViewFFTMap m_fftModels;
-    mutable PeakCacheMap m_peakCaches;
-    mutable Model *m_sliceableModel;
+    FFTModel *m_fftModel;
+    FFTModel *getFFTModel() const { return m_fftModel; }
+    Dense3DModelPeakCache *m_wholeCache;
+    Dense3DModelPeakCache *m_peakCache;
+    Dense3DModelPeakCache *getPeakCache() const { return m_peakCache; }
+    const int m_peakCacheDivisor;
+    bool canStoreWholeCache() const;
+    void recreateFFTModel();
 
-    class MagnitudeRange {
-    public:
-        MagnitudeRange() : m_min(0), m_max(0) { }
-        bool operator==(const MagnitudeRange &r) {
-            return r.m_min == m_min && r.m_max == m_max;
-        }
-        bool isSet() const { return (m_min != 0.f || m_max != 0.f); }
-        void set(float min, float max) {
-            m_min = min;
-            m_max = max;
-            if (m_max < m_min) m_max = m_min;
-        }
-        bool sample(float f) {
-            bool changed = false;
-            if (isSet()) {
-                if (f < m_min) { m_min = f; changed = true; }
-                if (f > m_max) { m_max = f; changed = true; }
-            } else {
-                m_max = m_min = f;
-                changed = true;
-            }
-            return changed;
-        }            
-        bool sample(const MagnitudeRange &r) {
-            bool changed = false;
-            if (isSet()) {
-                if (r.m_min < m_min) { m_min = r.m_min; changed = true; }
-                if (r.m_max > m_max) { m_max = r.m_max; changed = true; }
-            } else {
-                m_min = r.m_min;
-                m_max = r.m_max;
-                changed = true;
-            }
-            return changed;
-        }            
-        float getMin() const { return m_min; }
-        float getMax() const { return m_max; }
-    private:
-        float m_min;
-        float m_max;
-    };
+    typedef std::map<int, MagnitudeRange> ViewMagMap; // key is view id
+    mutable ViewMagMap m_viewMags;
+    mutable ViewMagMap m_lastRenderedMags; // when in normalizeVisibleArea mode
+    void invalidateMagnitudes();
 
-    typedef std::map<const View *, MagnitudeRange> ViewMagMap;
-    mutable ViewMagMap m_viewMags;
-    mutable std::vector<MagnitudeRange> m_columnMags;
-    void invalidateMagnitudes();
-    bool updateViewMagnitudes(View *v) const;
-    bool paintDrawBuffer(View *v, int w, int h,
-                         const std::vector<int> &binforx,
-                         const std::vector<double> &binfory,
-                         bool usePeaksCache,
-                         MagnitudeRange &overallMag,
-                         bool &overallMagChanged) const;
-    bool paintDrawBufferPeakFrequencies(View *v, int w, int h,
-                                        const std::vector<int> &binforx,
-                                        int minbin,
-                                        int maxbin,
-                                        double displayMinFreq,
-                                        double displayMaxFreq,
-                                        bool logarithmic,
-                                        MagnitudeRange &overallMag,
-                                        bool &overallMagChanged) const;
+    typedef std::map<int, Colour3DPlotRenderer *> ViewRendererMap; // key is view id
+    mutable ViewRendererMap m_renderers;
+    Colour3DPlotRenderer *getRenderer(LayerGeometryProvider *) const;
+    void invalidateRenderers();
+    
+    void paintWithRenderer(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
 
-    virtual void updateMeasureRectYCoords(View *v, const MeasureRect &r) const;
-    virtual void setMeasureRectYCoord(View *v, MeasureRect &r, bool start, int y) const;
+    void paintDetailedScale(LayerGeometryProvider *v,
+                            QPainter &paint, QRect rect) const;
+    void paintDetailedScalePhase(LayerGeometryProvider *v,
+                                 QPainter &paint, QRect rect) const;
+    
+    virtual void updateMeasureRectYCoords(LayerGeometryProvider *v,
+                                          const MeasureRect &r) const;
+    virtual void setMeasureRectYCoord(LayerGeometryProvider *v,
+                                      MeasureRect &r, bool start, int y) const;
 };
 
 #endif
--- a/layer/SpectrumLayer.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/SpectrumLayer.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -21,7 +21,10 @@
 #include "base/Preferences.h"
 #include "base/RangeMapper.h"
 #include "base/Pitch.h"
+#include "base/Strings.h"
+
 #include "ColourMapper.h"
+#include "PaintAssistant.h"
 
 #include <QPainter>
 #include <QTextStream>
@@ -112,11 +115,7 @@
                                     m_windowType,
                                     m_windowSize,
                                     getWindowIncrement(),
-                                    m_windowSize,
-                                    false,
-                                    StorageAdviser::Criteria
-                                    (StorageAdviser::SpeedCritical |
-                                     StorageAdviser::FrequentLookupLikely));
+                                    m_windowSize);
 
     setSliceableModel(newFFT);
 
@@ -125,8 +124,6 @@
         m_biasCurve.push_back(1.f / (float(m_windowSize)/2.f));
     }
 
-    newFFT->resume();
-
     m_newFFTNeeded = false;
 }
 
@@ -386,18 +383,18 @@
 }
 
 bool
-SpectrumLayer::getXScaleValue(const View *v, int x, 
+SpectrumLayer::getXScaleValue(const LayerGeometryProvider *v, int x, 
                               double &value, QString &unit) const
 {
     if (m_xorigins.find(v) == m_xorigins.end()) return false;
     int xorigin = m_xorigins.find(v)->second;
-    value = getFrequencyForX(x - xorigin, v->width() - xorigin - 1);
+    value = getFrequencyForX(x - xorigin, v->getPaintWidth() - xorigin - 1);
     unit = "Hz";
     return true;
 }
 
 bool
-SpectrumLayer::getYScaleValue(const View *v, int y,
+SpectrumLayer::getYScaleValue(const LayerGeometryProvider *v, int y,
                               double &value, QString &unit) const
 {
     value = getValueForY(y, v);
@@ -419,7 +416,7 @@
 }
 
 bool
-SpectrumLayer::getYScaleDifference(const View *v, int y0, int y1,
+SpectrumLayer::getYScaleDifference(const LayerGeometryProvider *v, int y0, int y1,
                                    double &diff, QString &unit) const
 {
     bool rv = SliceLayer::getYScaleDifference(v, y0, y1, diff, unit);
@@ -429,14 +426,14 @@
 
 
 bool
-SpectrumLayer::getCrosshairExtents(View *v, QPainter &paint,
+SpectrumLayer::getCrosshairExtents(LayerGeometryProvider *v, QPainter &paint,
                                    QPoint cursorPos,
                                    std::vector<QRect> &extents) const
 {
-    QRect vertical(cursorPos.x(), cursorPos.y(), 1, v->height() - cursorPos.y());
+    QRect vertical(cursorPos.x(), cursorPos.y(), 1, v->getPaintHeight() - cursorPos.y());
     extents.push_back(vertical);
 
-    QRect horizontal(0, cursorPos.y(), v->width(), 12);
+    QRect horizontal(0, cursorPos.y(), v->getPaintWidth(), 12);
     extents.push_back(horizontal);
 
     int hoffset = 2;
@@ -455,14 +452,14 @@
     extents.push_back(log);
 
     QRect freq(cursorPos.x(),
-               v->height() - paint.fontMetrics().height() - hoffset,
+               v->getPaintHeight() - paint.fontMetrics().height() - hoffset,
                paint.fontMetrics().width("123456 Hz") + 2,
                paint.fontMetrics().height());
     extents.push_back(freq);
 
     int w(paint.fontMetrics().width("C#10+50c") + 2);
     QRect pitch(cursorPos.x() - w,
-                v->height() - paint.fontMetrics().height() - hoffset,
+                v->getPaintHeight() - paint.fontMetrics().height() - hoffset,
                 w,
                 paint.fontMetrics().height());
     extents.push_back(pitch);
@@ -471,7 +468,7 @@
 }
 
 void
-SpectrumLayer::paintCrosshairs(View *v, QPainter &paint,
+SpectrumLayer::paintCrosshairs(LayerGeometryProvider *v, QPainter &paint,
                                QPoint cursorPos) const
 {
     if (!m_sliceableModel) return;
@@ -487,29 +484,29 @@
     paint.setPen(mapper.getContrastingColour());
 
     int xorigin = m_xorigins[v];
-    int w = v->width() - xorigin - 1;
+    int w = v->getPaintWidth() - xorigin - 1;
     
-    paint.drawLine(xorigin, cursorPos.y(), v->width(), cursorPos.y());
-    paint.drawLine(cursorPos.x(), cursorPos.y(), cursorPos.x(), v->height());
+    paint.drawLine(xorigin, cursorPos.y(), v->getPaintWidth(), cursorPos.y());
+    paint.drawLine(cursorPos.x(), cursorPos.y(), cursorPos.x(), v->getPaintHeight());
     
     double fundamental = getFrequencyForX(cursorPos.x() - xorigin, w);
 
     int hoffset = 2;
     if (m_binScale == LogBins) hoffset = 13;
 
-    v->drawVisibleText(paint,
+    PaintAssistant::drawVisibleText(v, paint,
                        cursorPos.x() + 2,
-                       v->height() - 2 - hoffset,
+                       v->getPaintHeight() - 2 - hoffset,
                        QString("%1 Hz").arg(fundamental),
-                       View::OutlinedText);
+                       PaintAssistant::OutlinedText);
 
     if (Pitch::isFrequencyInMidiRange(fundamental)) {
         QString pitchLabel = Pitch::getPitchLabelForFrequency(fundamental);
-        v->drawVisibleText(paint,
+        PaintAssistant::drawVisibleText(v, paint,
                            cursorPos.x() - paint.fontMetrics().width(pitchLabel) - 2,
-                           v->height() - 2 - hoffset,
+                           v->getPaintHeight() - 2 - hoffset,
                            pitchLabel,
-                           View::OutlinedText);
+                           PaintAssistant::OutlinedText);
     }
 
     double value = getValueForY(cursorPos.y(), v);
@@ -518,17 +515,17 @@
     if (value > 0.0) db = 10.0 * log10(value);
     if (db < thresh) db = thresh;
 
-    v->drawVisibleText(paint,
+    PaintAssistant::drawVisibleText(v, paint,
                        xorigin + 2,
                        cursorPos.y() - 2,
                        QString("%1 V").arg(value),
-                       View::OutlinedText);
+                       PaintAssistant::OutlinedText);
 
-    v->drawVisibleText(paint,
+    PaintAssistant::drawVisibleText(v, paint,
                        xorigin + 2,
                        cursorPos.y() + 2 + paint.fontMetrics().ascent(),
                        QString("%1 dBV").arg(db),
-                       View::OutlinedText);
+                       PaintAssistant::OutlinedText);
     
     int harmonic = 2;
 
@@ -537,7 +534,7 @@
         int hx = int(lrint(getXForFrequency(fundamental * harmonic, w)));
         hx += xorigin;
 
-        if (hx < xorigin || hx > v->width()) break;
+        if (hx < xorigin || hx > v->getPaintWidth()) break;
         
         int len = 7;
 
@@ -561,7 +558,7 @@
 }
 
 QString
-SpectrumLayer::getFeatureDescription(View *v, QPoint &p) const
+SpectrumLayer::getFeatureDescription(LayerGeometryProvider *v, QPoint &p) const
 {
     if (!m_sliceableModel) return "";
 
@@ -611,12 +608,12 @@
     QString mindbstr;
     QString maxdbstr;
     if (mindb == AudioLevel::DB_FLOOR) {
-        mindbstr = tr("-Inf");
+        mindbstr = Strings::minus_infinity;
     } else {
         mindbstr = QString("%1").arg(lrint(mindb));
     }
     if (maxdb == AudioLevel::DB_FLOOR) {
-        maxdbstr = tr("-Inf");
+        maxdbstr = Strings::minus_infinity;
     } else {
         maxdbstr = QString("%1").arg(lrint(maxdb));
     }
@@ -650,7 +647,7 @@
 }
 
 void
-SpectrumLayer::paint(View *v, QPainter &paint, QRect rect) const
+SpectrumLayer::paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const
 {
     if (!m_originModel || !m_originModel->isOK() ||
         !m_originModel->isReady()) {
@@ -669,7 +666,7 @@
     double thresh = (pow(10, -6) / m_gain) * (m_windowSize / 2.0); // -60dB adj
 
     int xorigin = getVerticalScaleWidth(v, false, paint) + 1;
-    int w = v->width() - xorigin - 1;
+    int w = v->getPaintWidth() - xorigin - 1;
 
     int pkh = 0;
 //!!!    if (m_binScale == LogBins) {
@@ -729,7 +726,7 @@
             (void)getYForValue(values[bin], v, norm); // don't need return value, need norm
 
             paint.setPen(mapper.map(norm));
-            paint.drawLine(xorigin + x, 0, xorigin + x, v->height() - pkh - 1);
+            paint.drawLine(xorigin + x, 0, xorigin + x, v->getPaintHeight() - pkh - 1);
         }
 
         paint.restore();
@@ -749,7 +746,7 @@
 //    if (m_binScale == LogBins) {
 
 //        int pkh = 10;
-        int h = v->height();
+        int h = v->getPaintHeight();
 
         // piano keyboard
         //!!! should be in a new paintHorizontalScale()?
--- a/layer/SpectrumLayer.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/SpectrumLayer.h	Fri Jan 13 10:29:50 2017 +0000
@@ -39,13 +39,13 @@
     void setModel(DenseTimeValueModel *model);
     virtual const Model *getModel() const { return m_originModel; }
 
-    virtual bool getCrosshairExtents(View *, QPainter &, QPoint cursorPos,
+    virtual bool getCrosshairExtents(LayerGeometryProvider *, QPainter &, QPoint cursorPos,
                                      std::vector<QRect> &extents) const;
-    virtual void paintCrosshairs(View *, QPainter &, QPoint) const;
+    virtual void paintCrosshairs(LayerGeometryProvider *, QPainter &, QPoint) const;
 
-    virtual QString getFeatureDescription(View *v, QPoint &) const;
+    virtual QString getFeatureDescription(LayerGeometryProvider *v, QPoint &) const;
 
-    virtual void paint(View *v, QPainter &paint, QRect rect) const;
+    virtual void paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
 
     virtual VerticalPosition getPreferredFrameCountPosition() const {
 	return PositionTop;
@@ -67,16 +67,16 @@
     virtual bool getValueExtents(double &min, double &max,
                                  bool &logarithmic, QString &unit) const;
 
-    virtual bool getXScaleValue(const View *v, int x,
+    virtual bool getXScaleValue(const LayerGeometryProvider *v, int x,
                                 double &value, QString &unit) const;
 
-    virtual bool getYScaleValue(const View *, int y,
+    virtual bool getYScaleValue(const LayerGeometryProvider *, int y,
                                 double &value, QString &unit) const;
 
-    virtual bool getYScaleDifference(const View *, int y0, int y1,
+    virtual bool getYScaleDifference(const LayerGeometryProvider *, int y0, int y1,
                                      double &diff, QString &unit) const;
 
-    virtual bool isLayerScrollable(const View *) const { return false; }
+    virtual bool isLayerScrollable(const LayerGeometryProvider *) const { return false; }
 
     void setChannel(int);
     int getChannel() const { return m_channel; }
@@ -93,7 +93,7 @@
     void setShowPeaks(bool);
     bool getShowPeaks() const { return m_showPeaks; }
 
-    virtual int getVerticalScaleWidth(View *, bool, QPainter &) const { return 0; }
+    virtual int getVerticalScaleWidth(LayerGeometryProvider *, bool, QPainter &) const { return 0; }
 
     virtual void toXml(QTextStream &stream, QString indent = "",
                        QString extraAttributes = "") const;
--- a/layer/TextLayer.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/TextLayer.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -102,7 +102,7 @@
 }
 
 bool
-TextLayer::isLayerScrollable(const View *v) const
+TextLayer::isLayerScrollable(const LayerGeometryProvider *v) const
 {
     QPoint discard;
     return !v->shouldIlluminateLocalFeatures(this, discard);
@@ -110,12 +110,12 @@
 
 
 TextModel::PointList
-TextLayer::getLocalPoints(View *v, int x, int y) const
+TextLayer::getLocalPoints(LayerGeometryProvider *v, int x, int y) const
 {
     if (!m_model) return TextModel::PointList();
 
     sv_frame_t frame0 = v->getFrameForX(-150);
-    sv_frame_t frame1 = v->getFrameForX(v->width() + 150);
+    sv_frame_t frame1 = v->getFrameForX(v->getPaintWidth() + 150);
     
     TextModel::PointList points(m_model->getPoints(frame0, frame1));
 
@@ -139,9 +139,9 @@
 	    (QRect(0, 0, 150, 200),
 	     Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, label);
 
-	if (py + rect.height() > v->height()) {
-	    if (rect.height() > v->height()) py = 0;
-	    else py = v->height() - rect.height() - 1;
+	if (py + rect.height() > v->getPaintHeight()) {
+	    if (rect.height() > v->getPaintHeight()) py = 0;
+	    else py = v->getPaintHeight() - rect.height() - 1;
 	}
 
 	if (x >= px && x < px + rect.width() &&
@@ -154,7 +154,7 @@
 }
 
 bool
-TextLayer::getPointToDrag(View *v, int x, int y, TextModel::Point &p) const
+TextLayer::getPointToDrag(LayerGeometryProvider *v, int x, int y, TextModel::Point &p) const
 {
     if (!m_model) return false;
 
@@ -182,7 +182,7 @@
 }
 
 QString
-TextLayer::getFeatureDescription(View *v, QPoint &pos) const
+TextLayer::getFeatureDescription(LayerGeometryProvider *v, QPoint &pos) const
 {
     int x = pos.x();
 
@@ -220,7 +220,7 @@
 //!!! too much overlap with TimeValueLayer/TimeInstantLayer
 
 bool
-TextLayer::snapToFeatureFrame(View *v, sv_frame_t &frame,
+TextLayer::snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
 			      int &resolution,
 			      SnapType snap) const
 {
@@ -292,21 +292,21 @@
 }
 
 int
-TextLayer::getYForHeight(View *v, double height) const
+TextLayer::getYForHeight(LayerGeometryProvider *v, double height) const
 {
-    int h = v->height();
+    int h = v->getPaintHeight();
     return h - int(height * h);
 }
 
 double
-TextLayer::getHeightForY(View *v, int y) const
+TextLayer::getHeightForY(LayerGeometryProvider *v, int y) const
 {
-    int h = v->height();
+    int h = v->getPaintHeight();
     return double(h - y) / h;
 }
 
 void
-TextLayer::paint(View *v, QPainter &paint, QRect rect) const
+TextLayer::paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const
 {
     if (!m_model || !m_model->isOK()) return;
 
@@ -347,7 +347,7 @@
     int boxMaxHeight = 200;
 
     paint.save();
-    paint.setClipRect(rect.x(), 0, rect.width() + boxMaxWidth, v->height());
+    paint.setClipRect(rect.x(), 0, rect.width() + boxMaxWidth, v->getPaintHeight());
     
     for (TextModel::PointList::const_iterator i = points.begin();
 	 i != points.end(); ++i) {
@@ -380,9 +380,9 @@
 	QRect textRect = QRect(3, 2, boxRect.width(), boxRect.height());
 	boxRect = QRect(0, 0, boxRect.width() + 6, boxRect.height() + 2);
 
-	if (y + boxRect.height() > v->height()) {
-	    if (boxRect.height() > v->height()) y = 0;
-	    else y = v->height() - boxRect.height() - 1;
+	if (y + boxRect.height() > v->getPaintHeight()) {
+	    if (boxRect.height() > v->getPaintHeight()) y = 0;
+	    else y = v->getPaintHeight() - boxRect.height() - 1;
 	}
 
 	boxRect = QRect(x, y, boxRect.width(), boxRect.height());
@@ -411,7 +411,7 @@
 }
 
 void
-TextLayer::drawStart(View *v, QMouseEvent *e)
+TextLayer::drawStart(LayerGeometryProvider *v, QMouseEvent *e)
 {
 //    SVDEBUG << "TextLayer::drawStart(" << e->x() << "," << e->y() << ")" << endl;
 
@@ -437,7 +437,7 @@
 }
 
 void
-TextLayer::drawDrag(View *v, QMouseEvent *e)
+TextLayer::drawDrag(LayerGeometryProvider *v, QMouseEvent *e)
 {
 //    SVDEBUG << "TextLayer::drawDrag(" << e->x() << "," << e->y() << ")" << endl;
 
@@ -456,13 +456,13 @@
 }
 
 void
-TextLayer::drawEnd(View *v, QMouseEvent *)
+TextLayer::drawEnd(LayerGeometryProvider *v, QMouseEvent *)
 {
 //    SVDEBUG << "TextLayer::drawEnd(" << e->x() << "," << e->y() << ")" << endl;
     if (!m_model || !m_editing) return;
 
     bool ok = false;
-    QString label = QInputDialog::getText(v, tr("Enter label"),
+    QString label = QInputDialog::getText(v->getView(), tr("Enter label"),
 					  tr("Please enter a new label:"),
 					  QLineEdit::Normal, "", &ok);
 
@@ -480,7 +480,7 @@
 }
 
 void
-TextLayer::eraseStart(View *v, QMouseEvent *e)
+TextLayer::eraseStart(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_model) return;
 
@@ -495,12 +495,12 @@
 }
 
 void
-TextLayer::eraseDrag(View *, QMouseEvent *)
+TextLayer::eraseDrag(LayerGeometryProvider *, QMouseEvent *)
 {
 }
 
 void
-TextLayer::eraseEnd(View *v, QMouseEvent *e)
+TextLayer::eraseEnd(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_model || !m_editing) return;
 
@@ -521,7 +521,7 @@
 }
 
 void
-TextLayer::editStart(View *v, QMouseEvent *e)
+TextLayer::editStart(LayerGeometryProvider *v, QMouseEvent *e)
 {
 //    SVDEBUG << "TextLayer::editStart(" << e->x() << "," << e->y() << ")" << endl;
 
@@ -543,7 +543,7 @@
 }
 
 void
-TextLayer::editDrag(View *v, QMouseEvent *e)
+TextLayer::editDrag(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_model || !m_editing) return;
 
@@ -570,7 +570,7 @@
 }
 
 void
-TextLayer::editEnd(View *, QMouseEvent *)
+TextLayer::editEnd(LayerGeometryProvider *, QMouseEvent *)
 {
 //    SVDEBUG << "TextLayer::editEnd(" << e->x() << "," << e->y() << ")" << endl;
     if (!m_model || !m_editing) return;
@@ -598,7 +598,7 @@
 }
 
 bool
-TextLayer::editOpen(View *v, QMouseEvent *e)
+TextLayer::editOpen(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_model) return false;
 
@@ -608,7 +608,7 @@
     QString label = text.label;
 
     bool ok = false;
-    label = QInputDialog::getText(v, tr("Enter label"),
+    label = QInputDialog::getText(v->getView(), tr("Enter label"),
 				  tr("Please enter a new label:"),
 				  QLineEdit::Normal, label, &ok);
     if (ok && label != text.label) {
@@ -699,7 +699,7 @@
 }
 
 void
-TextLayer::copy(View *v, Selection s, Clipboard &to)
+TextLayer::copy(LayerGeometryProvider *v, Selection s, Clipboard &to)
 {
     if (!m_model) return;
 
@@ -717,7 +717,7 @@
 }
 
 bool
-TextLayer::paste(View *v, const Clipboard &from, sv_frame_t /* frameOffset */, bool /* interactive */)
+TextLayer::paste(LayerGeometryProvider *v, const Clipboard &from, sv_frame_t /* frameOffset */, bool /* interactive */)
 {
     if (!m_model) return false;
 
@@ -728,7 +728,7 @@
     if (clipboardHasDifferentAlignment(v, from)) {
 
         QMessageBox::StandardButton button =
-            QMessageBox::question(v, tr("Re-align pasted items?"),
+            QMessageBox::question(v->getView(), tr("Re-align pasted items?"),
                                   tr("The items you are pasting came from a layer with different source material from this one.  Do you want to re-align them in time, to match the source material for this layer?"),
                                   QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel,
                                   QMessageBox::Yes);
--- a/layer/TextLayer.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/TextLayer.h	Fri Jan 13 10:29:50 2017 +0000
@@ -32,35 +32,35 @@
 public:
     TextLayer();
 
-    virtual void paint(View *v, QPainter &paint, QRect rect) const;
+    virtual void paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
 
-    virtual QString getFeatureDescription(View *v, QPoint &) const;
+    virtual QString getFeatureDescription(LayerGeometryProvider *v, QPoint &) const;
 
-    virtual bool snapToFeatureFrame(View *v, sv_frame_t &frame,
+    virtual bool snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
 				    int &resolution,
 				    SnapType snap) const;
 
-    virtual void drawStart(View *v, QMouseEvent *);
-    virtual void drawDrag(View *v, QMouseEvent *);
-    virtual void drawEnd(View *v, QMouseEvent *);
+    virtual void drawStart(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void drawDrag(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void drawEnd(LayerGeometryProvider *v, QMouseEvent *);
 
-    virtual void eraseStart(View *v, QMouseEvent *);
-    virtual void eraseDrag(View *v, QMouseEvent *);
-    virtual void eraseEnd(View *v, QMouseEvent *);
+    virtual void eraseStart(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void eraseDrag(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void eraseEnd(LayerGeometryProvider *v, QMouseEvent *);
 
-    virtual void editStart(View *v, QMouseEvent *);
-    virtual void editDrag(View *v, QMouseEvent *);
-    virtual void editEnd(View *v, QMouseEvent *);
+    virtual void editStart(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void editDrag(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void editEnd(LayerGeometryProvider *v, QMouseEvent *);
 
     virtual void moveSelection(Selection s, sv_frame_t newStartFrame);
     virtual void resizeSelection(Selection s, Selection newSize);
     virtual void deleteSelection(Selection s);
 
-    virtual void copy(View *v, Selection s, Clipboard &to);
-    virtual bool paste(View *v, const Clipboard &from, sv_frame_t frameOffset,
+    virtual void copy(LayerGeometryProvider *v, Selection s, Clipboard &to);
+    virtual bool paste(LayerGeometryProvider *v, const Clipboard &from, sv_frame_t frameOffset,
                        bool interactive);
 
-    virtual bool editOpen(View *, QMouseEvent *); // on double-click
+    virtual bool editOpen(LayerGeometryProvider *, QMouseEvent *); // on double-click
 
     virtual const Model *getModel() const { return m_model; }
     void setModel(TextModel *model);
@@ -74,16 +74,16 @@
 					  int value) const;
     virtual void setProperty(const PropertyName &, int value);
 
-    virtual bool isLayerScrollable(const View *v) const;
+    virtual bool isLayerScrollable(const LayerGeometryProvider *v) const;
 
     virtual bool isLayerEditable() const { return true; }
 
-    virtual int getCompletion(View *) const { return m_model->getCompletion(); }
+    virtual int getCompletion(LayerGeometryProvider *) const { return m_model->getCompletion(); }
 
     virtual bool getValueExtents(double &min, double &max,
                                  bool &logarithmic, QString &unit) const;
 
-    virtual int getVerticalScaleWidth(View *, bool, QPainter &) const { return 0; }
+    virtual int getVerticalScaleWidth(LayerGeometryProvider *, bool, QPainter &) const { return 0; }
 
     virtual void toXml(QTextStream &stream, QString indent = "",
                        QString extraAttributes = "") const;
@@ -91,14 +91,14 @@
     void setProperties(const QXmlAttributes &attributes);
 
 protected:
-    int getYForHeight(View *v, double height) const;
-    double getHeightForY(View *v, int y) const;
+    int getYForHeight(LayerGeometryProvider *v, double height) const;
+    double getHeightForY(LayerGeometryProvider *v, int y) const;
 
     virtual int getDefaultColourHint(bool dark, bool &impose);
 
-    TextModel::PointList getLocalPoints(View *v, int x, int y) const;
+    TextModel::PointList getLocalPoints(LayerGeometryProvider *v, int x, int y) const;
 
-    bool getPointToDrag(View *v, int x, int y, TextModel::Point &) const;
+    bool getPointToDrag(LayerGeometryProvider *v, int x, int y, TextModel::Point &) const;
 
     TextModel *m_model;
     bool m_editing;
--- a/layer/TimeInstantLayer.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/TimeInstantLayer.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -20,7 +20,9 @@
 #include "view/View.h"
 #include "base/Profiler.h"
 #include "base/Clipboard.h"
+
 #include "ColourDatabase.h"
+#include "PaintAssistant.h"
 
 #include "data/model/SparseOneDimensionalModel.h"
 
@@ -147,14 +149,14 @@
 }
 
 bool
-TimeInstantLayer::isLayerScrollable(const View *v) const
+TimeInstantLayer::isLayerScrollable(const LayerGeometryProvider *v) const
 {
     QPoint discard;
     return !v->shouldIlluminateLocalFeatures(this, discard);
 }
 
 SparseOneDimensionalModel::PointList
-TimeInstantLayer::getLocalPoints(View *v, int x) const
+TimeInstantLayer::getLocalPoints(LayerGeometryProvider *v, int x) const
 {
     // Return a set of points that all have the same frame number, the
     // nearest to the given x coordinate, and that are within a
@@ -213,7 +215,7 @@
 }
 
 QString
-TimeInstantLayer::getFeatureDescription(View *v, QPoint &pos) const
+TimeInstantLayer::getFeatureDescription(LayerGeometryProvider *v, QPoint &pos) const
 {
     int x = pos.x();
 
@@ -249,7 +251,7 @@
 }
 
 bool
-TimeInstantLayer::snapToFeatureFrame(View *v, sv_frame_t &frame,
+TimeInstantLayer::snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
 				     int &resolution,
 				     SnapType snap) const
 {
@@ -321,7 +323,7 @@
 }
 
 void
-TimeInstantLayer::paint(View *v, QPainter &paint, QRect rect) const
+TimeInstantLayer::paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const
 {
     if (!m_model || !m_model->isOK()) return;
 
@@ -403,16 +405,16 @@
 	}
 		
 	if (p.frame == illuminateFrame) {
-	    paint.setPen(getForegroundQColor(v));
+	    paint.setPen(getForegroundQColor(v->getView()));
 	} else {
 	    paint.setPen(brushColour);
 	}
 
 	if (m_plotStyle == PlotInstants) {
 	    if (iw > 1) {
-		paint.drawRect(x, 0, iw - 1, v->height() - 1);
+		paint.drawRect(x, 0, iw - 1, v->getPaintHeight() - 1);
 	    } else {
-		paint.drawLine(x, 0, x, v->height() - 1);
+		paint.drawLine(x, 0, x, v->getPaintHeight() - 1);
 	    }
 	} else {
 
@@ -431,11 +433,11 @@
 	    if (nx >= x) {
 		
 		if (illuminateFrame != p.frame &&
-		    (nx < x + 5 || x >= v->width() - 1)) {
+		    (nx < x + 5 || x >= v->getPaintWidth() - 1)) {
 		    paint.setPen(Qt::NoPen);
 		}
 
-                paint.drawRect(x, -1, nx - x, v->height() + 1);
+                paint.drawRect(x, -1, nx - x, v->getPaintHeight() + 1);
 	    }
 
 	    odd = !odd;
@@ -456,7 +458,7 @@
 	    }
 
 	    if (good) {
-                v->drawVisibleText(paint, x + iw + 2, textY, p.label, View::OutlinedText);
+                PaintAssistant::drawVisibleText(v, paint, x + iw + 2, textY, p.label, PaintAssistant::OutlinedText);
 //		paint.drawText(x + iw + 2, textY, p.label);
 	    }
 	}
@@ -466,7 +468,7 @@
 }
 
 void
-TimeInstantLayer::drawStart(View *v, QMouseEvent *e)
+TimeInstantLayer::drawStart(LayerGeometryProvider *v, QMouseEvent *e)
 {
 #ifdef DEBUG_TIME_INSTANT_LAYER
     cerr << "TimeInstantLayer::drawStart(" << e->x() << ")" << endl;
@@ -489,7 +491,7 @@
 }
 
 void
-TimeInstantLayer::drawDrag(View *v, QMouseEvent *e)
+TimeInstantLayer::drawDrag(LayerGeometryProvider *v, QMouseEvent *e)
 {
 #ifdef DEBUG_TIME_INSTANT_LAYER
     cerr << "TimeInstantLayer::drawDrag(" << e->x() << ")" << endl;
@@ -506,7 +508,7 @@
 }
 
 void
-TimeInstantLayer::drawEnd(View *, QMouseEvent *)
+TimeInstantLayer::drawEnd(LayerGeometryProvider *, QMouseEvent *)
 {
 #ifdef DEBUG_TIME_INSTANT_LAYER
     cerr << "TimeInstantLayer::drawEnd(" << e->x() << ")" << endl;
@@ -523,7 +525,7 @@
 }
 
 void
-TimeInstantLayer::eraseStart(View *v, QMouseEvent *e)
+TimeInstantLayer::eraseStart(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_model) return;
 
@@ -541,12 +543,12 @@
 }
 
 void
-TimeInstantLayer::eraseDrag(View *, QMouseEvent *)
+TimeInstantLayer::eraseDrag(LayerGeometryProvider *, QMouseEvent *)
 {
 }
 
 void
-TimeInstantLayer::eraseEnd(View *v, QMouseEvent *e)
+TimeInstantLayer::eraseEnd(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_model || !m_editing) return;
 
@@ -567,7 +569,7 @@
 }
 
 void
-TimeInstantLayer::editStart(View *v, QMouseEvent *e)
+TimeInstantLayer::editStart(LayerGeometryProvider *v, QMouseEvent *e)
 {
 #ifdef DEBUG_TIME_INSTANT_LAYER
     cerr << "TimeInstantLayer::editStart(" << e->x() << ")" << endl;
@@ -589,7 +591,7 @@
 }
 
 void
-TimeInstantLayer::editDrag(View *v, QMouseEvent *e)
+TimeInstantLayer::editDrag(LayerGeometryProvider *v, QMouseEvent *e)
 {
 #ifdef DEBUG_TIME_INSTANT_LAYER
     cerr << "TimeInstantLayer::editDrag(" << e->x() << ")" << endl;
@@ -612,7 +614,7 @@
 }
 
 void
-TimeInstantLayer::editEnd(View *, QMouseEvent *)
+TimeInstantLayer::editEnd(LayerGeometryProvider *, QMouseEvent *)
 {
 #ifdef DEBUG_TIME_INSTANT_LAYER
     cerr << "TimeInstantLayer::editEnd(" << e->x() << ")" << endl;
@@ -631,7 +633,7 @@
 }
 
 bool
-TimeInstantLayer::editOpen(View *v, QMouseEvent *e)
+TimeInstantLayer::editOpen(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_model) return false;
 
@@ -747,7 +749,7 @@
 }
 
 void
-TimeInstantLayer::copy(View *v, Selection s, Clipboard &to)
+TimeInstantLayer::copy(LayerGeometryProvider *v, Selection s, Clipboard &to)
 {
     if (!m_model) return;
 
@@ -765,7 +767,7 @@
 }
 
 bool
-TimeInstantLayer::paste(View *v, const Clipboard &from, sv_frame_t frameOffset, bool)
+TimeInstantLayer::paste(LayerGeometryProvider *v, const Clipboard &from, sv_frame_t frameOffset, bool)
 {
     if (!m_model) return false;
 
@@ -776,7 +778,7 @@
     if (clipboardHasDifferentAlignment(v, from)) {
 
         QMessageBox::StandardButton button =
-            QMessageBox::question(v, tr("Re-align pasted instants?"),
+            QMessageBox::question(v->getView(), tr("Re-align pasted instants?"),
                                   tr("The instants you are pasting came from a layer with different source material from this one.  Do you want to re-align them in time, to match the source material for this layer?"),
                                   QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel,
                                   QMessageBox::Yes);
--- a/layer/TimeInstantLayer.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/TimeInstantLayer.h	Fri Jan 13 10:29:50 2017 +0000
@@ -33,35 +33,35 @@
     TimeInstantLayer();
     virtual ~TimeInstantLayer();
 
-    virtual void paint(View *v, QPainter &paint, QRect rect) const;
+    virtual void paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
 
     virtual QString getLabelPreceding(sv_frame_t) const;
-    virtual QString getFeatureDescription(View *v, QPoint &) const;
+    virtual QString getFeatureDescription(LayerGeometryProvider *v, QPoint &) const;
 
-    virtual bool snapToFeatureFrame(View *v, sv_frame_t &frame,
+    virtual bool snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
 				    int &resolution,
 				    SnapType snap) const;
 
-    virtual void drawStart(View *v, QMouseEvent *);
-    virtual void drawDrag(View *v, QMouseEvent *);
-    virtual void drawEnd(View *v, QMouseEvent *);
+    virtual void drawStart(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void drawDrag(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void drawEnd(LayerGeometryProvider *v, QMouseEvent *);
 
-    virtual void eraseStart(View *v, QMouseEvent *);
-    virtual void eraseDrag(View *v, QMouseEvent *);
-    virtual void eraseEnd(View *v, QMouseEvent *);
+    virtual void eraseStart(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void eraseDrag(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void eraseEnd(LayerGeometryProvider *v, QMouseEvent *);
 
-    virtual void editStart(View *v, QMouseEvent *);
-    virtual void editDrag(View *v, QMouseEvent *);
-    virtual void editEnd(View *v, QMouseEvent *);
+    virtual void editStart(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void editDrag(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void editEnd(LayerGeometryProvider *v, QMouseEvent *);
 
-    virtual bool editOpen(View *, QMouseEvent *);
+    virtual bool editOpen(LayerGeometryProvider *, QMouseEvent *);
 
     virtual void moveSelection(Selection s, sv_frame_t newStartFrame);
     virtual void resizeSelection(Selection s, Selection newSize);
     virtual void deleteSelection(Selection s);
 
-    virtual void copy(View *v, Selection s, Clipboard &to);
-    virtual bool paste(View *v, const Clipboard &from, sv_frame_t frameOffset,
+    virtual void copy(LayerGeometryProvider *v, Selection s, Clipboard &to);
+    virtual bool paste(LayerGeometryProvider *v, const Clipboard &from, sv_frame_t frameOffset,
                        bool interactive);
 
     virtual const Model *getModel() const { return m_model; }
@@ -84,11 +84,11 @@
     void setPlotStyle(PlotStyle style);
     PlotStyle getPlotStyle() const { return m_plotStyle; }
 
-    virtual bool isLayerScrollable(const View *v) const;
+    virtual bool isLayerScrollable(const LayerGeometryProvider *v) const;
 
     virtual bool isLayerEditable() const { return true; }
 
-    virtual int getCompletion(View *) const { return m_model->getCompletion(); }
+    virtual int getCompletion(LayerGeometryProvider *) const { return m_model->getCompletion(); }
 
     virtual bool needsTextLabelHeight() const { return m_model->hasTextLabels(); }
 
@@ -109,14 +109,14 @@
         }
     }
 
-    virtual int getVerticalScaleWidth(View *, bool, QPainter &) const { return 0; }
+    virtual int getVerticalScaleWidth(LayerGeometryProvider *, bool, QPainter &) const { return 0; }
 
 protected:
-    SparseOneDimensionalModel::PointList getLocalPoints(View *v, int) const;
+    SparseOneDimensionalModel::PointList getLocalPoints(LayerGeometryProvider *v, int) const;
 
     virtual int getDefaultColourHint(bool dark, bool &impose);
 
-    bool clipboardAlignmentDiffers(View *v, const Clipboard &) const;
+    bool clipboardAlignmentDiffers(LayerGeometryProvider *v, const Clipboard &) const;
 
     SparseOneDimensionalModel *m_model;
     bool m_editing;
--- a/layer/TimeRulerLayer.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/TimeRulerLayer.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -19,19 +19,20 @@
 
 #include "data/model/Model.h"
 #include "base/RealTime.h"
+#include "view/View.h"
+
 #include "ColourDatabase.h"
-#include "view/View.h"
+#include "PaintAssistant.h"
 
 #include <QPainter>
 
 #include <iostream>
 #include <cmath>
+#include <stdexcept>
 
 //#define DEBUG_TIME_RULER_LAYER 1
 
 
-
-
 TimeRulerLayer::TimeRulerLayer() :
     SingleColourLayer(),
     m_model(0),
@@ -49,7 +50,7 @@
 }
 
 bool
-TimeRulerLayer::snapToFeatureFrame(View *v, sv_frame_t &frame,
+TimeRulerLayer::snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
                                    int &resolution, SnapType snap) const
 {
     if (!m_model) {
@@ -141,7 +142,7 @@
 }
 
 int
-TimeRulerLayer::getMajorTickSpacing(View *v, bool &quarterTicks) const
+TimeRulerLayer::getMajorTickSpacing(LayerGeometryProvider *v, bool &quarterTicks) const
 {
     // return value is in milliseconds
 
@@ -158,7 +159,7 @@
     RealTime rtStart = RealTime::frame2RealTime(startFrame, sampleRate);
     RealTime rtEnd = RealTime::frame2RealTime(endFrame, sampleRate);
 
-    int count = v->width() / minPixelSpacing;
+    int count = v->getPaintWidth() / minPixelSpacing;
     if (count < 1) count = 1;
     RealTime rtGap = (rtEnd - rtStart) / count;
 
@@ -182,6 +183,8 @@
     } else {
 	incms = 1;
 	int ms = rtGap.msec();
+//        cerr << "rtGap.msec = " << ms << ", rtGap = " << rtGap << ", count = " << count << endl;
+//        cerr << "startFrame = " << startFrame << ", endFrame = " << endFrame << " rtStart = " << rtStart << ", rtEnd = " << rtEnd << endl;
 	if (ms > 0) { incms *= 10; ms /= 10; }
 	if (ms > 0) { incms *= 10; ms /= 10; }
 	if (ms > 0) { incms *= 5; ms /= 5; }
@@ -192,7 +195,7 @@
 }
 
 void
-TimeRulerLayer::paint(View *v, QPainter &paint, QRect rect) const
+TimeRulerLayer::paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const
 {
 #ifdef DEBUG_TIME_RULER_LAYER
     SVDEBUG << "TimeRulerLayer::paint (" << rect.x() << "," << rect.y()
@@ -241,6 +244,9 @@
     // time < 0 which would cut it in half
     int minlabel = 1; // ms
 
+    // used for a sanity check
+    sv_frame_t prevframe = 0;
+    
     while (1) {
 
         // frame is used to determine where to draw the lines, so it
@@ -253,10 +259,17 @@
         frame /= v->getZoomLevel();
         frame *= v->getZoomLevel(); // so frame corresponds to an exact pixel
 
+        if (frame == prevframe && prevframe != 0) {
+            cerr << "ERROR: frame == prevframe (== " << frame
+                 << ") in TimeRulerLayer::paint" << endl;
+            throw std::logic_error("frame == prevframe in TimeRulerLayer::paint");
+        }
+        prevframe = frame;
+        
         int x = v->getXForFrame(frame);
 
 #ifdef DEBUG_TIME_RULER_LAYER
-        SVDEBUG << "Considering frame = " << frame << ", x = " << x << endl;
+        cerr << "Considering frame = " << frame << ", x = " << x << endl;
 #endif
 
         if (x >= rect.x() + rect.width() + 50) {
@@ -287,11 +300,11 @@
             }
 
             paint.setPen(greyColour);
-            paint.drawLine(x, 0, x, v->height());
+            paint.drawLine(x, 0, x, v->getPaintHeight());
 
             paint.setPen(getBaseQColor());
             paint.drawLine(x, 0, x, 5);
-            paint.drawLine(x, v->height() - 6, x, v->height() - 1);
+            paint.drawLine(x, v->getPaintHeight() - 6, x, v->getPaintHeight() - 1);
 
             int y;
             switch (m_labelHeight) {
@@ -300,20 +313,20 @@
                 y = 6 + metrics.ascent();
                 break;
             case LabelMiddle:
-                y = v->height() / 2 - metrics.height() / 2 + metrics.ascent();
+                y = v->getPaintHeight() / 2 - metrics.height() / 2 + metrics.ascent();
                 break;
             case LabelBottom:
-                y = v->height() - metrics.height() + metrics.ascent() - 6;
+                y = v->getPaintHeight() - metrics.height() + metrics.ascent() - 6;
             }
 
             if (v->getViewManager() && v->getViewManager()->getOverlayMode() !=
                 ViewManager::NoOverlays) {
 
-                if (v->getLayer(0) == this) {
+                if (v->getView()->getLayer(0) == this) {
                     // backmost layer, don't worry about outlining the text
                     paint.drawText(x+2 - tw/2, y, text);
                 } else {
-                    v->drawVisibleText(paint, x+2 - tw/2, y, text, View::OutlinedText);
+                    PaintAssistant::drawVisibleText(v, paint, x+2 - tw/2, y, text, PaintAssistant::OutlinedText);
                 }
             }
         }
@@ -344,14 +357,14 @@
 	    if (ticks == 10) {
 		if ((i % 2) == 1) {
 		    if (i == 5) {
-			paint.drawLine(x, 0, x, v->height());
+			paint.drawLine(x, 0, x, v->getPaintHeight());
 		    } else sz = 3;
 		} else {
 		    sz = 7;
 		}
 	    }
 	    paint.drawLine(x, 0, x, sz);
-	    paint.drawLine(x, v->height() - sz - 1, x, v->height() - 1);
+	    paint.drawLine(x, v->getPaintHeight() - sz - 1, x, v->getPaintHeight() - 1);
 	}
 
 	ms += incms;
--- a/layer/TimeRulerLayer.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/TimeRulerLayer.h	Fri Jan 13 10:29:50 2017 +0000
@@ -32,7 +32,7 @@
 public:
     TimeRulerLayer();
 
-    virtual void paint(View *v, QPainter &paint, QRect rect) const;
+    virtual void paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
 
     void setModel(Model *);
     virtual const Model *getModel() const { return m_model; }
@@ -41,7 +41,7 @@
     void setLabelHeight(LabelHeight h) { m_labelHeight = h; }
     LabelHeight getLabelHeight() const { return m_labelHeight; }
 
-    virtual bool snapToFeatureFrame(View *, sv_frame_t &, int &, SnapType) const;
+    virtual bool snapToFeatureFrame(LayerGeometryProvider *, sv_frame_t &, int &, SnapType) const;
 
     virtual ColourSignificance getLayerColourSignificance() const {
         return ColourIrrelevant;
@@ -53,7 +53,7 @@
 
     virtual QString getLayerPresentationName() const;
 
-    virtual int getVerticalScaleWidth(View *, bool, QPainter &) const { return 0; }
+    virtual int getVerticalScaleWidth(LayerGeometryProvider *, bool, QPainter &) const { return 0; }
 
     virtual void toXml(QTextStream &stream, QString indent = "",
                        QString extraAttributes = "") const;
@@ -68,7 +68,7 @@
 
     virtual int getDefaultColourHint(bool dark, bool &impose);
 
-    int getMajorTickSpacing(View *, bool &quarterTicks) const;
+    int getMajorTickSpacing(LayerGeometryProvider *, bool &quarterTicks) const;
 };
 
 #endif
--- a/layer/TimeValueLayer.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/TimeValueLayer.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -21,7 +21,6 @@
 #include "base/LogRange.h"
 #include "base/RangeMapper.h"
 #include "base/Pitch.h"
-#include "ColourDatabase.h"
 #include "view/View.h"
 
 #include "data/model/SparseTimeValueModel.h"
@@ -31,12 +30,14 @@
 #include "widgets/ListInputDialog.h"
 #include "widgets/TextAbbrev.h"
 
+#include "ColourDatabase.h"
 #include "ColourMapper.h"
 #include "PianoScale.h"
 #include "LinearNumericalScale.h"
 #include "LogNumericalScale.h"
 #include "LinearColourScale.h"
 #include "LogColourScale.h"
+#include "PaintAssistant.h"
 
 #include <QPainter>
 #include <QPainterPath>
@@ -131,7 +132,7 @@
     if (name == "Plot Type") return ValueProperty;
     if (name == "Vertical Scale") return ValueProperty;
     if (name == "Scale Units") return UnitsProperty;
-    if (name == "Colour" && m_plotStyle == PlotSegmentation) return ValueProperty;
+    if (name == "Colour" && m_plotStyle == PlotSegmentation) return ColourMapProperty;
     if (name == "Draw Segment Division Lines") return ToggleProperty;
     if (name == "Show Derivative") return ToggleProperty;
     return SingleColourLayer::getPropertyType(name);
@@ -316,7 +317,7 @@
 }
 
 bool
-TimeValueLayer::isLayerScrollable(const View *v) const
+TimeValueLayer::isLayerScrollable(const LayerGeometryProvider *v) const
 {
     // We don't illuminate sections in the line or curve modes, so
     // they're always scrollable
@@ -530,7 +531,7 @@
 }
 
 SparseTimeValueModel::PointList
-TimeValueLayer::getLocalPoints(View *v, int x) const
+TimeValueLayer::getLocalPoints(LayerGeometryProvider *v, int x) const
 {
     if (!m_model) return SparseTimeValueModel::PointList();
 
@@ -587,7 +588,7 @@
 }
 
 QString
-TimeValueLayer::getFeatureDescription(View *v, QPoint &pos) const
+TimeValueLayer::getFeatureDescription(LayerGeometryProvider *v, QPoint &pos) const
 {
     int x = pos.x();
 
@@ -641,7 +642,7 @@
 }
 
 bool
-TimeValueLayer::snapToFeatureFrame(View *v, sv_frame_t &frame,
+TimeValueLayer::snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
 				   int &resolution,
 				   SnapType snap) const
 {
@@ -713,7 +714,7 @@
 }
 
 bool
-TimeValueLayer::snapToSimilarFeature(View *v, sv_frame_t &frame,
+TimeValueLayer::snapToSimilarFeature(LayerGeometryProvider *v, sv_frame_t &frame,
                                      int &resolution,
                                      SnapType snap) const
 {
@@ -794,7 +795,7 @@
 }
 
 void
-TimeValueLayer::getScaleExtents(View *v, double &min, double &max, bool &log) const
+TimeValueLayer::getScaleExtents(LayerGeometryProvider *v, double &min, double &max, bool &log) const
 {
     min = 0.0;
     max = 0.0;
@@ -830,11 +831,11 @@
 }
 
 int
-TimeValueLayer::getYForValue(View *v, double val) const
+TimeValueLayer::getYForValue(LayerGeometryProvider *v, double val) const
 {
     double min = 0.0, max = 0.0;
     bool logarithmic = false;
-    int h = v->height();
+    int h = v->getPaintHeight();
 
     getScaleExtents(v, min, max, logarithmic);
 
@@ -851,11 +852,11 @@
 }
 
 double
-TimeValueLayer::getValueForY(View *v, int y) const
+TimeValueLayer::getValueForY(LayerGeometryProvider *v, int y) const
 {
     double min = 0.0, max = 0.0;
     bool logarithmic = false;
-    int h = v->height();
+    int h = v->getPaintHeight();
 
     getScaleExtents(v, min, max, logarithmic);
 
@@ -877,7 +878,7 @@
 }
 
 QColor
-TimeValueLayer::getColourForValue(View *v, double val) const
+TimeValueLayer::getColourForValue(LayerGeometryProvider *v, double val) const
 {
     double min, max;
     bool log;
@@ -908,7 +909,7 @@
 }
 
 void
-TimeValueLayer::paint(View *v, QPainter &paint, QRect rect) const
+TimeValueLayer::paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const
 {
     if (!m_model || !m_model->isOK()) return;
 
@@ -943,8 +944,8 @@
     double max = m_model->getValueMaximum();
     if (max == min) max = min + 1.0;
 
-    int origin = int(nearbyint(v->height() -
-			       (-min * v->height()) / (max - min)));
+    int origin = int(nearbyint(v->getPaintHeight() -
+			       (-min * v->getPaintHeight()) / (max - min)));
 
     QPoint localPos;
     sv_frame_t illuminateFrame = -1;
@@ -978,7 +979,7 @@
         textY = v->getTextLabelHeight(this, paint);
     } else {
         int originY = getYForValue(v, 0.f);
-        if (originY > 0 && originY < v->height()) {
+        if (originY > 0 && originY < v->getPaintHeight()) {
             paint.save();
             paint.setPen(getPartialShades(v)[1]);
             paint.drawLine(x0, originY, x1, originY);
@@ -1183,12 +1184,12 @@
             if (!illuminate) {
                 if (!m_drawSegmentDivisions ||
                     nx < x + 5 ||
-                    x >= v->width() - 1) {
+                    x >= v->getPaintWidth() - 1) {
                     paint.setPen(Qt::NoPen);
                 }
 	    }
 
-	    paint.drawRect(x, -1, nx - x, v->height() + 1);
+	    paint.drawRect(x, -1, nx - x, v->getPaintHeight() + 1);
 	}
 
         if (v->shouldShowFeatureLabels()) {
@@ -1214,10 +1215,10 @@
                 if (haveRoom ||
                     (!haveNext &&
                      (pointCount == 0 || !italic))) {
-                    v->drawVisibleText(paint, x + 5, textY, label,
+                    PaintAssistant::drawVisibleText(v, paint, x + 5, textY, label,
                                        italic ?
-                                       View::OutlinedItalicText :
-                                       View::OutlinedText);
+                                       PaintAssistant::OutlinedItalicText :
+                                       PaintAssistant::OutlinedText);
                 }
             }
         }
@@ -1231,7 +1232,7 @@
 	paint.drawPath(path);
     } else if ((m_plotStyle == PlotCurve || m_plotStyle == PlotLines)
                && !path.isEmpty()) {
-	paint.setRenderHint(QPainter::Antialiasing, pointCount <= v->width());
+	paint.setRenderHint(QPainter::Antialiasing, pointCount <= v->getPaintWidth());
 	paint.drawPath(path);
     }
 
@@ -1242,7 +1243,7 @@
 }
 
 int
-TimeValueLayer::getVerticalScaleWidth(View *v, bool, QPainter &paint) const
+TimeValueLayer::getVerticalScaleWidth(LayerGeometryProvider *v, bool, QPainter &paint) const
 {
     if (!m_model || shouldAutoAlign()) {
         return 0;
@@ -1262,7 +1263,7 @@
 }
 
 void
-TimeValueLayer::paintVerticalScale(View *v, bool, QPainter &paint, QRect) const
+TimeValueLayer::paintVerticalScale(LayerGeometryProvider *v, bool, QPainter &paint, QRect) const
 {
     if (!m_model || m_model->getPoints().empty()) return;
 
@@ -1271,7 +1272,7 @@
     bool logarithmic;
 
     int w = getVerticalScaleWidth(v, false, paint);
-    int h = v->height();
+    int h = v->getPaintHeight();
 
     if (m_plotStyle == PlotSegmentation) {
 
@@ -1314,7 +1315,7 @@
 }
 
 void
-TimeValueLayer::drawStart(View *v, QMouseEvent *e)
+TimeValueLayer::drawStart(LayerGeometryProvider *v, QMouseEvent *e)
 {
 #ifdef DEBUG_TIME_VALUE_LAYER
     cerr << "TimeValueLayer::drawStart(" << e->x() << "," << e->y() << ")" << endl;
@@ -1364,7 +1365,7 @@
 }
 
 void
-TimeValueLayer::drawDrag(View *v, QMouseEvent *e)
+TimeValueLayer::drawDrag(LayerGeometryProvider *v, QMouseEvent *e)
 {
 #ifdef DEBUG_TIME_VALUE_LAYER
     cerr << "TimeValueLayer::drawDrag(" << e->x() << "," << e->y() << ")" << endl;
@@ -1426,7 +1427,7 @@
 }
 
 void
-TimeValueLayer::drawEnd(View *, QMouseEvent *)
+TimeValueLayer::drawEnd(LayerGeometryProvider *, QMouseEvent *)
 {
 #ifdef DEBUG_TIME_VALUE_LAYER
     cerr << "TimeValueLayer::drawEnd" << endl;
@@ -1438,7 +1439,7 @@
 }
 
 void
-TimeValueLayer::eraseStart(View *v, QMouseEvent *e)
+TimeValueLayer::eraseStart(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_model) return;
 
@@ -1456,12 +1457,12 @@
 }
 
 void
-TimeValueLayer::eraseDrag(View *, QMouseEvent *)
+TimeValueLayer::eraseDrag(LayerGeometryProvider *, QMouseEvent *)
 {
 }
 
 void
-TimeValueLayer::eraseEnd(View *v, QMouseEvent *e)
+TimeValueLayer::eraseEnd(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_model || !m_editing) return;
 
@@ -1483,7 +1484,7 @@
 }
 
 void
-TimeValueLayer::editStart(View *v, QMouseEvent *e)
+TimeValueLayer::editStart(LayerGeometryProvider *v, QMouseEvent *e)
 {
 #ifdef DEBUG_TIME_VALUE_LAYER
     cerr << "TimeValueLayer::editStart(" << e->x() << "," << e->y() << ")" << endl;
@@ -1506,7 +1507,7 @@
 }
 
 void
-TimeValueLayer::editDrag(View *v, QMouseEvent *e)
+TimeValueLayer::editDrag(LayerGeometryProvider *v, QMouseEvent *e)
 {
 #ifdef DEBUG_TIME_VALUE_LAYER
     cerr << "TimeValueLayer::editDrag(" << e->x() << "," << e->y() << ")" << endl;
@@ -1532,7 +1533,7 @@
 }
 
 void
-TimeValueLayer::editEnd(View *, QMouseEvent *)
+TimeValueLayer::editEnd(LayerGeometryProvider *, QMouseEvent *)
 {
 #ifdef DEBUG_TIME_VALUE_LAYER
     cerr << "TimeValueLayer::editEnd" << endl;
@@ -1562,7 +1563,7 @@
 }
 
 bool
-TimeValueLayer::editOpen(View *v, QMouseEvent *e)
+TimeValueLayer::editOpen(LayerGeometryProvider *v, QMouseEvent *e)
 {
     if (!m_model) return false;
 
@@ -1685,7 +1686,7 @@
 }    
 
 void
-TimeValueLayer::copy(View *v, Selection s, Clipboard &to)
+TimeValueLayer::copy(LayerGeometryProvider *v, Selection s, Clipboard &to)
 {
     if (!m_model) return;
 
@@ -1703,7 +1704,7 @@
 }
 
 bool
-TimeValueLayer::paste(View *v, const Clipboard &from, sv_frame_t /* frameOffset */,
+TimeValueLayer::paste(LayerGeometryProvider *v, const Clipboard &from, sv_frame_t /* frameOffset */,
                       bool interactive)
 {
     if (!m_model) return false;
@@ -1715,7 +1716,7 @@
     if (clipboardHasDifferentAlignment(v, from)) {
 
         QMessageBox::StandardButton button =
-            QMessageBox::question(v, tr("Re-align pasted items?"),
+            QMessageBox::question(v->getView(), tr("Re-align pasted items?"),
                                   tr("The items you are pasting came from a layer with different source material from this one.  Do you want to re-align them in time, to match the source material for this layer?"),
                                   QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel,
                                   QMessageBox::Yes);
--- a/layer/TimeValueLayer.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/TimeValueLayer.h	Fri Jan 13 10:29:50 2017 +0000
@@ -37,41 +37,41 @@
 public:
     TimeValueLayer();
 
-    virtual void paint(View *v, QPainter &paint, QRect rect) const;
+    virtual void paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
 
-    virtual int getVerticalScaleWidth(View *v, bool, QPainter &) const;
-    virtual void paintVerticalScale(View *v, bool, QPainter &paint, QRect rect) const;
+    virtual int getVerticalScaleWidth(LayerGeometryProvider *v, bool, QPainter &) const;
+    virtual void paintVerticalScale(LayerGeometryProvider *v, bool, QPainter &paint, QRect rect) const;
 
-    virtual QString getFeatureDescription(View *v, QPoint &) const;
+    virtual QString getFeatureDescription(LayerGeometryProvider *v, QPoint &) const;
     virtual QString getLabelPreceding(sv_frame_t) const;
 
-    virtual bool snapToFeatureFrame(View *v, sv_frame_t &frame,
+    virtual bool snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
 				    int &resolution,
 				    SnapType snap) const;
-    virtual bool snapToSimilarFeature(View *v, sv_frame_t &frame,
+    virtual bool snapToSimilarFeature(LayerGeometryProvider *v, sv_frame_t &frame,
                                       int &resolution,
                                       SnapType snap) const;
 
-    virtual void drawStart(View *v, QMouseEvent *);
-    virtual void drawDrag(View *v, QMouseEvent *);
-    virtual void drawEnd(View *v, QMouseEvent *);
+    virtual void drawStart(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void drawDrag(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void drawEnd(LayerGeometryProvider *v, QMouseEvent *);
 
-    virtual void eraseStart(View *v, QMouseEvent *);
-    virtual void eraseDrag(View *v, QMouseEvent *);
-    virtual void eraseEnd(View *v, QMouseEvent *);
+    virtual void eraseStart(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void eraseDrag(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void eraseEnd(LayerGeometryProvider *v, QMouseEvent *);
 
-    virtual void editStart(View *v, QMouseEvent *);
-    virtual void editDrag(View *v, QMouseEvent *);
-    virtual void editEnd(View *v, QMouseEvent *);
+    virtual void editStart(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void editDrag(LayerGeometryProvider *v, QMouseEvent *);
+    virtual void editEnd(LayerGeometryProvider *v, QMouseEvent *);
 
-    virtual bool editOpen(View *v, QMouseEvent *);
+    virtual bool editOpen(LayerGeometryProvider *v, QMouseEvent *);
 
     virtual void moveSelection(Selection s, sv_frame_t newStartFrame);
     virtual void resizeSelection(Selection s, Selection newSize);
     virtual void deleteSelection(Selection s);
 
-    virtual void copy(View *v, Selection s, Clipboard &to);
-    virtual bool paste(View *v, const Clipboard &from, sv_frame_t frameOffset,
+    virtual void copy(LayerGeometryProvider *v, Selection s, Clipboard &to);
+    virtual bool paste(LayerGeometryProvider *v, const Clipboard &from, sv_frame_t frameOffset,
                        bool interactive);
 
     virtual const Model *getModel() const { return m_model; }
@@ -120,11 +120,11 @@
     void setShowDerivative(bool);
     bool getShowDerivative() const { return m_derivative; }
 
-    virtual bool isLayerScrollable(const View *v) const;
+    virtual bool isLayerScrollable(const LayerGeometryProvider *v) const;
 
     virtual bool isLayerEditable() const { return true; }
 
-    virtual int getCompletion(View *) const { return m_model->getCompletion(); }
+    virtual int getCompletion(LayerGeometryProvider *) const { return m_model->getCompletion(); }
 
     virtual bool needsTextLabelHeight() const {
         return m_plotStyle == PlotSegmentation && m_model->hasTextLabels();
@@ -146,6 +146,7 @@
 
     void setProperties(const QXmlAttributes &attributes);
 
+    /// Override from SingleColourLayer
     virtual ColourSignificance getLayerColourSignificance() const {
         if (m_plotStyle == PlotSegmentation) {
             return ColourHasMeaningfulValue;
@@ -154,17 +155,26 @@
         }
     }
 
+    /// Override from SingleColourLayer
+    virtual bool hasLightBackground() const {
+        if (m_plotStyle == PlotSegmentation) {
+            return true;
+        } else {
+            return SingleColourLayer::hasLightBackground();
+        }
+    }
+
     /// VerticalScaleLayer and ColourScaleLayer methods
-    virtual int getYForValue(View *, double value) const;
-    virtual double getValueForY(View *, int y) const;
+    virtual int getYForValue(LayerGeometryProvider *, double value) const;
+    virtual double getValueForY(LayerGeometryProvider *, int y) const;
     virtual QString getScaleUnits() const;
-    virtual QColor getColourForValue(View *v, double value) const;
+    virtual QColor getColourForValue(LayerGeometryProvider *v, double value) const;
 
 protected:
-    void getScaleExtents(View *, double &min, double &max, bool &log) const;
+    void getScaleExtents(LayerGeometryProvider *, double &min, double &max, bool &log) const;
     bool shouldAutoAlign() const;
 
-    SparseTimeValueModel::PointList getLocalPoints(View *v, int) const;
+    SparseTimeValueModel::PointList getLocalPoints(LayerGeometryProvider *v, int) const;
 
     virtual int getDefaultColourHint(bool dark, bool &impose);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/VerticalBinLayer.h	Fri Jan 13 10:29:50 2017 +0000
@@ -0,0 +1,62 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2016 Chris Cannam and QMUL.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef VERTICAL_BIN_LAYER_H
+#define VERTICAL_BIN_LAYER_H
+
+#include "SliceableLayer.h"
+
+/**
+ * Interface for layers in which the Y axis corresponds to bin number
+ * rather than scale value. Colour3DPlotLayer is the obvious example.
+ * Conceptually these are always SliceableLayers as well, and this
+ * subclasses from SliceableLayer to avoid a big inheritance mess.
+ */
+class VerticalBinLayer : public SliceableLayer
+{
+public:
+    /**
+     * Return the y coordinate at which the given bin "starts"
+     * (i.e. at the bottom of the bin, if the given bin is an integer
+     * and the vertical scale is the usual way up). Bin number may be
+     * fractional, to obtain a position part-way through a bin.
+     */
+    virtual double getYForBin(const LayerGeometryProvider *, double bin) const = 0;
+
+    /**
+     * As getYForBin, but rounding to integer values.
+     */
+    virtual int getIYForBin(const LayerGeometryProvider *v, int bin) const {
+        return int(round(getYForBin(v, bin)));
+    }
+    
+    /**
+     * Return the bin number, possibly fractional, at the given y
+     * coordinate. Note that the whole numbers occur at the positions
+     * at which the bins "start" (i.e. the bottom of the visible bin,
+     * if the vertical scale is the usual way up).
+     */
+    virtual double getBinForY(const LayerGeometryProvider *, double y) const = 0;
+
+    /**
+     * As getBinForY, but rounding to integer values.
+     */
+    virtual int getIBinForY(const LayerGeometryProvider *v, int y) const {
+        return int(floor(getBinForY(v, y)));
+    }
+};
+
+#endif
+
--- a/layer/VerticalScaleLayer.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/VerticalScaleLayer.h	Fri Jan 13 10:29:50 2017 +0000
@@ -16,11 +16,17 @@
 #ifndef VERTICAL_SCALE_LAYER_H
 #define VERTICAL_SCALE_LAYER_H
 
+/**
+ * Interface for layers in which the Y axis represents (or can
+ * sometimes represent, depending on the display mode) the sample
+ * value. For example, TimeValueLayer uses vertical scale when in
+ * point mode and so provides this interface.
+ */
 class VerticalScaleLayer
 {
 public:
-    virtual int getYForValue(View *, double value) const = 0;
-    virtual double getValueForY(View *, int y) const = 0;
+    virtual int getYForValue(LayerGeometryProvider *, double value) const = 0;
+    virtual double getValueForY(LayerGeometryProvider *, int y) const = 0;
     virtual QString getScaleUnits() const = 0;
 };
 
--- a/layer/WaveformLayer.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/WaveformLayer.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -19,7 +19,10 @@
 #include "view/View.h"
 #include "base/Profiler.h"
 #include "base/RangeMapper.h"
+#include "base/Strings.h"
+
 #include "ColourDatabase.h"
+#include "PaintAssistant.h"
 
 #include <QPainter>
 #include <QPixmap>
@@ -326,7 +329,7 @@
 }
 
 int
-WaveformLayer::getCompletion(View *) const
+WaveformLayer::getCompletion(LayerGeometryProvider *) const
 {
     int completion = 100;
     if (!m_model || !m_model->isOK()) return completion;
@@ -399,7 +402,7 @@
 }    
 
 bool
-WaveformLayer::isLayerScrollable(const View *) const
+WaveformLayer::isLayerScrollable(const LayerGeometryProvider *) const
 {
     return !m_autoNormalize;
 }
@@ -408,7 +411,7 @@
                             -5, -3, -2, -1, -0.5, 0 };
 
 bool
-WaveformLayer::getSourceFramesForX(View *v, int x, int modelZoomLevel,
+WaveformLayer::getSourceFramesForX(LayerGeometryProvider *v, int x, int modelZoomLevel,
                                    sv_frame_t &f0, sv_frame_t &f1) const
 {
     sv_frame_t viewFrame = v->getFrameForX(x);
@@ -433,7 +436,7 @@
 }
 
 float
-WaveformLayer::getNormalizeGain(View *v, int channel) const
+WaveformLayer::getNormalizeGain(LayerGeometryProvider *v, int channel) const
 {
     sv_frame_t startFrame = v->getStartFrame();
     sv_frame_t endFrame = v->getEndFrame();
@@ -473,7 +476,7 @@
 }
 
 void
-WaveformLayer::paint(View *v, QPainter &viewPainter, QRect rect) const
+WaveformLayer::paint(LayerGeometryProvider *v, QPainter &viewPainter, QRect rect) const
 {
     if (!m_model || !m_model->isOK()) {
 	return;
@@ -494,8 +497,8 @@
                                      mergingChannels, mixingChannels);
     if (channels == 0) return;
 
-    int w = v->width();
-    int h = v->height();
+    int w = v->getPaintWidth();
+    int h = v->getPaintHeight();
 
     bool ready = m_model->isReady();
     QPainter *paint;
@@ -559,7 +562,7 @@
     y1 = rect.bottom();
 
     if (x0 > 0) --x0;
-    if (x1 < v->width()) ++x1;
+    if (x1 < w) ++x1;
 
     // Our zoom level may differ from that at which the underlying
     // model has its blocks.
@@ -945,7 +948,7 @@
 
     if (m_aggressive) {
 
-	if (ready && rect == v->rect()) {
+	if (ready && rect == v->getPaintRect()) {
 	    m_cacheValid = true;
 	    m_cacheZoomLevel = zoomLevel;
 	}
@@ -959,7 +962,7 @@
 }
 
 QString
-WaveformLayer::getFeatureDescription(View *v, QPoint &pos) const
+WaveformLayer::getFeatureDescription(LayerGeometryProvider *v, QPoint &pos) const
 {
     int x = pos.x();
 
@@ -1042,7 +1045,7 @@
 }
 
 int
-WaveformLayer::getYForValue(const View *v, double value, int channel) const
+WaveformLayer::getYForValue(const LayerGeometryProvider *v, double value, int channel) const
 {
     int channels = 0, minChannel = 0, maxChannel = 0;
     bool mergingChannels = false, mixingChannels = false;
@@ -1052,7 +1055,7 @@
     if (channels == 0) return 0;
     if (maxChannel < minChannel || channel < minChannel) return 0;
 
-    int h = v->height();
+    int h = v->getPaintHeight();
     int m = (h / channels) / 2;
 	
     if ((m_scale == dBScale || m_scale == MeterScale) &&
@@ -1085,7 +1088,7 @@
 }
 
 double
-WaveformLayer::getValueForY(const View *v, int y, int &channel) const
+WaveformLayer::getValueForY(const LayerGeometryProvider *v, int y, int &channel) const
 {
     int channels = 0, minChannel = 0, maxChannel = 0;
     bool mergingChannels = false, mixingChannels = false;
@@ -1095,7 +1098,7 @@
     if (channels == 0) return 0;
     if (maxChannel < minChannel) return 0;
 
-    int h = v->height();
+    int h = v->getPaintHeight();
     int m = (h / channels) / 2;
 
     if ((m_scale == dBScale || m_scale == MeterScale) &&
@@ -1131,7 +1134,7 @@
 }
 
 bool
-WaveformLayer::getYScaleValue(const View *v, int y,
+WaveformLayer::getYScaleValue(const LayerGeometryProvider *v, int y,
                               double &value, QString &unit) const
 {
     int channel;
@@ -1157,7 +1160,7 @@
 }
 
 bool
-WaveformLayer::getYScaleDifference(const View *v, int y0, int y1,
+WaveformLayer::getYScaleDifference(const LayerGeometryProvider *v, int y0, int y1,
                                    double &diff, QString &unit) const
 {
     int c0, c1;
@@ -1195,18 +1198,18 @@
 }
 
 int
-WaveformLayer::getVerticalScaleWidth(View *, bool, QPainter &paint) const
+WaveformLayer::getVerticalScaleWidth(LayerGeometryProvider *, bool, QPainter &paint) const
 {
     if (m_scale == LinearScale) {
 	return paint.fontMetrics().width("0.0") + 13;
     } else {
 	return std::max(paint.fontMetrics().width(tr("0dB")),
-			paint.fontMetrics().width(tr("-Inf"))) + 13;
+			paint.fontMetrics().width(Strings::minus_infinity)) + 13;
     }
 }
 
 void
-WaveformLayer::paintVerticalScale(View *v, bool, QPainter &paint, QRect rect) const
+WaveformLayer::paintVerticalScale(LayerGeometryProvider *v, bool, QPainter &paint, QRect rect) const
 {
     if (!m_model || !m_model->isOK()) {
 	return;
@@ -1255,7 +1258,7 @@
 		text = QString("%1").arg(meterdbs[i]);
 		if (i == n) text = tr("0dB");
 		if (i == 0) {
-                    text = tr("-Inf");
+                    text = Strings::minus_infinity;
                     val = 0.0;
 		}
                 break;
@@ -1265,7 +1268,7 @@
 		text = QString("%1").arg(-(10*n) + i * 10);
 		if (i == n) text = tr("0dB");
 		if (i == 0) {
-                    text = tr("-Inf");
+                    text = Strings::minus_infinity;
                     val = 0.0;
 		}
                 break;
--- a/layer/WaveformLayer.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/layer/WaveformLayer.h	Fri Jan 13 10:29:50 2017 +0000
@@ -38,16 +38,16 @@
         return m_model ? m_model->getZoomConstraint() : 0;
     }
     virtual const Model *getModel() const { return m_model; }
-    virtual void paint(View *v, QPainter &paint, QRect rect) const;
+    virtual void paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
 
-    virtual QString getFeatureDescription(View *v, QPoint &) const;
+    virtual QString getFeatureDescription(LayerGeometryProvider *v, QPoint &) const;
 
     virtual ColourSignificance getLayerColourSignificance() const {
         return ColourAndBackgroundSignificant;
     }
 
-    virtual int getVerticalScaleWidth(View *v, bool detailed, QPainter &) const;
-    virtual void paintVerticalScale(View *v, bool detailed, QPainter &paint, QRect rect) const;
+    virtual int getVerticalScaleWidth(LayerGeometryProvider *v, bool detailed, QPainter &) const;
+    virtual void paintVerticalScale(LayerGeometryProvider *v, bool detailed, QPainter &paint, QRect rect) const;
 
     void setModel(const RangeSummarisableTimeValueModel *model);
 
@@ -180,17 +180,17 @@
     void setAggressiveCacheing(bool);
     bool getAggressiveCacheing() const { return m_aggressive; }
 
-    virtual bool isLayerScrollable(const View *) const;
+    virtual bool isLayerScrollable(const LayerGeometryProvider *) const;
 
-    virtual int getCompletion(View *) const;
+    virtual int getCompletion(LayerGeometryProvider *) const;
 
     virtual bool getValueExtents(double &min, double &max,
                                  bool &log, QString &unit) const;
 
-    virtual bool getYScaleValue(const View *v, int y,
+    virtual bool getYScaleValue(const LayerGeometryProvider *v, int y,
                                 double &value, QString &unit) const;
     
-    virtual bool getYScaleDifference(const View *v, int y0, int y1,
+    virtual bool getYScaleDifference(const LayerGeometryProvider *v, int y0, int y1,
                                      double &diff, QString &unit) const;
 
     virtual void toXml(QTextStream &stream, QString indent = "",
@@ -213,14 +213,14 @@
     int getChannelArrangement(int &min, int &max,
                                  bool &merging, bool &mixing) const;
 
-    int getYForValue(const View *v, double value, int channel) const;
+    int getYForValue(const LayerGeometryProvider *v, double value, int channel) const;
 
-    double getValueForY(const View *v, int y, int &channel) const;
+    double getValueForY(const LayerGeometryProvider *v, int y, int &channel) const;
 
-    bool getSourceFramesForX(View *v, int x, int modelZoomLevel,
+    bool getSourceFramesForX(LayerGeometryProvider *v, int x, int modelZoomLevel,
                              sv_frame_t &f0, sv_frame_t &f1) const;
 
-    float getNormalizeGain(View *v, int channel) const;
+    float getNormalizeGain(LayerGeometryProvider *v, int channel) const;
 
     virtual void flagBaseColourChanged() { m_cacheValid = false; }
 
--- a/svgui.pro	Fri Mar 04 12:23:31 2016 +0000
+++ b/svgui.pro	Fri Jan 13 10:29:50 2017 +0000
@@ -1,32 +1,14 @@
 
 TEMPLATE = lib
 
+INCLUDEPATH += ../vamp-plugin-sdk
+
 exists(config.pri) {
     include(config.pri)
 }
-!exists(config.pri) {
-
-    CONFIG += release
-    DEFINES += NDEBUG BUILD_RELEASE NO_TIMING
-
-    win32-g++ {
-        INCLUDEPATH += ../sv-dependency-builds/win32-mingw/include
-        LIBS += -L../sv-dependency-builds/win32-mingw/lib
-    }
-    win32-msvc* {
-        INCLUDEPATH += ../sv-dependency-builds/win32-msvc/include
-        LIBS += -L../sv-dependency-builds/win32-msvc/lib
-    }
-    macx* {
-        INCLUDEPATH += ../sv-dependency-builds/osx/include
-        LIBS += -L../sv-dependency-builds/osx/lib
-    }
-
-    DEFINES += HAVE_BZ2 HAVE_FFTW3 HAVE_FFTW3F HAVE_SNDFILE HAVE_SAMPLERATE HAVE_VAMP HAVE_VAMPHOSTSDK HAVE_RUBBERBAND HAVE_LIBLO HAVE_MAD HAVE_ID3TAG
-}
 
 CONFIG += staticlib qt thread warn_on stl rtti exceptions c++11
-QT += network xml gui widgets
+QT += network xml gui widgets svg
 
 TARGET = svgui
 
@@ -35,149 +17,7 @@
 OBJECTS_DIR = o
 MOC_DIR = o
 
-HEADERS += layer/Colour3DPlotLayer.h \
-	   layer/ColourDatabase.h \
-	   layer/ColourMapper.h \
-           layer/ColourScaleLayer.h \
-           layer/FlexiNoteLayer.h \
-           layer/ImageLayer.h \
-           layer/ImageRegionFinder.h \
-           layer/Layer.h \
-           layer/LayerFactory.h \
-           layer/LinearNumericalScale.h \
-           layer/LogNumericalScale.h \
-           layer/LinearColourScale.h \
-           layer/LogColourScale.h \
-           layer/NoteLayer.h \
-           layer/PaintAssistant.h \
-           layer/PianoScale.h \
-           layer/RegionLayer.h \
-           layer/SingleColourLayer.h \
-           layer/SliceableLayer.h \
-           layer/SliceLayer.h \
-           layer/SpectrogramLayer.h \
-           layer/SpectrumLayer.h \
-           layer/TextLayer.h \
-           layer/TimeInstantLayer.h \
-           layer/TimeRulerLayer.h \
-           layer/TimeValueLayer.h \
-           layer/VerticalScaleLayer.h \
-           layer/WaveformLayer.h
-SOURCES += layer/Colour3DPlotLayer.cpp \
-	   layer/ColourDatabase.cpp \
-	   layer/ColourMapper.cpp \
-           layer/FlexiNoteLayer.cpp \
-           layer/ImageLayer.cpp \
-           layer/ImageRegionFinder.cpp \
-           layer/Layer.cpp \
-           layer/LayerFactory.cpp \
-           layer/LinearNumericalScale.cpp \
-           layer/LogNumericalScale.cpp \
-           layer/LinearColourScale.cpp \
-           layer/LogColourScale.cpp \
-           layer/NoteLayer.cpp \
-           layer/PaintAssistant.cpp \
-           layer/PianoScale.cpp \
-           layer/RegionLayer.cpp \
-           layer/SingleColourLayer.cpp \
-           layer/SliceLayer.cpp \
-           layer/SpectrogramLayer.cpp \
-           layer/SpectrumLayer.cpp \
-           layer/TextLayer.cpp \
-           layer/TimeInstantLayer.cpp \
-           layer/TimeRulerLayer.cpp \
-           layer/TimeValueLayer.cpp \
-           layer/WaveformLayer.cpp
+include(files.pri)
 
-HEADERS += view/Overview.h \
-           view/Pane.h \
-           view/PaneStack.h \
-           view/View.h \
-           view/ViewManager.h
-SOURCES += view/Overview.cpp \
-           view/Pane.cpp \
-           view/PaneStack.cpp \
-           view/View.cpp \
-           view/ViewManager.cpp
-
-HEADERS += widgets/ActivityLog.h \
-           widgets/AudioDial.h \
-           widgets/ClickableLabel.h \
-           widgets/ColourNameDialog.h \
-           widgets/CommandHistory.h \
-           widgets/CSVFormatDialog.h \
-           widgets/Fader.h \
-           widgets/InteractiveFileFinder.h \
-           widgets/IconLoader.h \
-           widgets/ImageDialog.h \
-           widgets/ItemEditDialog.h \
-           widgets/KeyReference.h \
-           widgets/LabelCounterInputDialog.h \
-           widgets/LayerTree.h \
-           widgets/LayerTreeDialog.h \
-           widgets/LEDButton.h \
-           widgets/LevelPanToolButton.h \
-           widgets/LevelPanWidget.h \
-           widgets/ListInputDialog.h \
-           widgets/MIDIFileImportDialog.h \
-           widgets/ModelDataTableDialog.h \
-           widgets/NotifyingCheckBox.h \
-           widgets/NotifyingComboBox.h \
-           widgets/NotifyingPushButton.h \
-           widgets/NotifyingTabBar.h \
-           widgets/Panner.h \
-           widgets/PluginParameterBox.h \
-           widgets/PluginParameterDialog.h \
-           widgets/ProgressDialog.h \
-           widgets/PropertyBox.h \
-           widgets/PropertyStack.h \
-           widgets/RangeInputDialog.h \
-           widgets/SelectableLabel.h \
-           widgets/SubdividingMenu.h \
-           widgets/TextAbbrev.h \
-           widgets/Thumbwheel.h \
-           widgets/TipDialog.h \
-           widgets/TransformFinder.h \
-           widgets/UnitConverter.h \
-           widgets/WindowShapePreview.h \
-           widgets/WindowTypeSelector.h
-SOURCES += widgets/ActivityLog.cpp \
-           widgets/AudioDial.cpp \
-           widgets/ColourNameDialog.cpp \
-           widgets/CommandHistory.cpp \
-           widgets/CSVFormatDialog.cpp \
-           widgets/Fader.cpp \
-           widgets/InteractiveFileFinder.cpp \
-           widgets/IconLoader.cpp \
-           widgets/ImageDialog.cpp \
-           widgets/ItemEditDialog.cpp \
-           widgets/KeyReference.cpp \
-           widgets/LabelCounterInputDialog.cpp \
-           widgets/LayerTree.cpp \
-           widgets/LayerTreeDialog.cpp \
-           widgets/LEDButton.cpp \
-           widgets/LevelPanToolButton.cpp \
-           widgets/LevelPanWidget.cpp \
-           widgets/ListInputDialog.cpp \
-           widgets/MIDIFileImportDialog.cpp \
-           widgets/ModelDataTableDialog.cpp \
-           widgets/NotifyingCheckBox.cpp \
-           widgets/NotifyingComboBox.cpp \
-           widgets/NotifyingPushButton.cpp \
-           widgets/NotifyingTabBar.cpp \
-           widgets/Panner.cpp \
-           widgets/PluginParameterBox.cpp \
-           widgets/PluginParameterDialog.cpp \
-           widgets/ProgressDialog.cpp \
-           widgets/PropertyBox.cpp \
-           widgets/PropertyStack.cpp \
-           widgets/RangeInputDialog.cpp \
-           widgets/SelectableLabel.cpp \
-           widgets/SubdividingMenu.cpp \
-           widgets/TextAbbrev.cpp \
-           widgets/Thumbwheel.cpp \
-           widgets/TipDialog.cpp \
-           widgets/TransformFinder.cpp \
-           widgets/UnitConverter.cpp \
-           widgets/WindowShapePreview.cpp \
-           widgets/WindowTypeSelector.cpp
+HEADERS = $$(SVGUI_HEADERS)
+SOURCES = $$(SVGUI_SOURCES)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/view/AlignmentView.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -0,0 +1,200 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2014 Chris Cannam and QMUL.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "AlignmentView.h"
+
+#include <QPainter>
+
+#include "data/model/SparseOneDimensionalModel.h"
+
+#include "layer/TimeInstantLayer.h"
+
+using std::vector;
+
+AlignmentView::AlignmentView(QWidget *w) :
+    View(w, false),
+    m_above(0),
+    m_below(0)
+{
+    setObjectName(tr("AlignmentView"));
+}
+
+void
+AlignmentView::globalCentreFrameChanged(sv_frame_t f)
+{
+    View::globalCentreFrameChanged(f);
+    update();
+}
+
+void
+AlignmentView::viewCentreFrameChanged(View *v, sv_frame_t f)
+{
+    View::viewCentreFrameChanged(v, f);
+    if (v == m_above) {
+	m_centreFrame = f;
+	update();
+    } else if (v == m_below) {
+	update();
+    }
+}
+
+void
+AlignmentView::viewManagerPlaybackFrameChanged(sv_frame_t)
+{
+    update();
+}
+
+void
+AlignmentView::viewAboveZoomLevelChanged(int level, bool)
+{
+    m_zoomLevel = level;
+    update();
+}
+
+void
+AlignmentView::viewBelowZoomLevelChanged(int, bool)
+{
+    update();
+}
+
+void
+AlignmentView::setViewAbove(View *v)
+{
+    if (m_above) {
+	disconnect(m_above, 0, this, 0);
+    }
+
+    m_above = v;
+
+    if (m_above) {
+	connect(m_above,
+		SIGNAL(zoomLevelChanged(int, bool)),
+		this, 
+		SLOT(viewAboveZoomLevelChanged(int, bool)));
+    }
+}
+
+void
+AlignmentView::setViewBelow(View *v)
+{
+    if (m_below) {
+	disconnect(m_below, 0, this, 0);
+    }
+
+    m_below = v;
+
+    if (m_below) {
+	connect(m_below,
+		SIGNAL(zoomLevelChanged(int, bool)),
+		this, 
+		SLOT(viewBelowZoomLevelChanged(int, bool)));
+    }
+}
+
+void
+AlignmentView::paintEvent(QPaintEvent *)
+{
+    if (m_above == 0 || m_below == 0 || !m_manager) return;
+
+    bool darkPalette = false;
+    if (m_manager) darkPalette = m_manager->getGlobalDarkBackground();
+
+    QColor fg, bg;
+    if (darkPalette) {
+        fg = Qt::gray;
+        bg = Qt::black;
+    } else {
+        fg = Qt::black;
+        bg = Qt::gray;
+    }
+
+    QPainter paint(this);
+    paint.setPen(QPen(fg, 2));
+    paint.setBrush(Qt::NoBrush);
+    paint.setRenderHint(QPainter::Antialiasing, true);
+
+    paint.fillRect(rect(), bg);
+
+    vector<sv_frame_t> keyFrames = getKeyFrames();
+
+    foreach (sv_frame_t f, keyFrames) {
+	int ax = m_above->getXForFrame(f);
+	sv_frame_t rf = m_above->alignToReference(f);
+	sv_frame_t bf = m_below->alignFromReference(rf);
+	int bx = m_below->getXForFrame(bf);
+	paint.drawLine(ax, 0, bx, height());
+    }
+
+    paint.end();
+}
+
+vector<sv_frame_t>
+AlignmentView::getKeyFrames()
+{
+    if (!m_above) {
+	return getDefaultKeyFrames();
+    }
+
+    SparseOneDimensionalModel *m = 0;
+
+    // get the topmost such
+    for (int i = 0; i < m_above->getLayerCount(); ++i) {
+	if (qobject_cast<TimeInstantLayer *>(m_above->getLayer(i))) {
+	    SparseOneDimensionalModel *mm = 
+		qobject_cast<SparseOneDimensionalModel *>
+		(m_above->getLayer(i)->getModel());
+	    if (mm) m = mm;
+	}
+    }
+
+    if (!m) {
+	return getDefaultKeyFrames();
+    }
+
+    vector<sv_frame_t> keyFrames;
+
+    const SparseOneDimensionalModel::PointList pp = m->getPoints();
+    for (SparseOneDimensionalModel::PointList::const_iterator pi = pp.begin();
+	 pi != pp.end(); ++pi) {
+	keyFrames.push_back(pi->frame);
+    }
+
+    return keyFrames;
+}
+
+vector<sv_frame_t>
+AlignmentView::getDefaultKeyFrames()
+{
+    vector<sv_frame_t> keyFrames;
+
+    if (!m_above || !m_manager) return keyFrames;
+
+    sv_samplerate_t rate = m_manager->getMainModelSampleRate();
+    if (rate == 0) return keyFrames;
+
+    for (sv_frame_t f = m_above->getModelsStartFrame(); 
+	 f <= m_above->getModelsEndFrame(); 
+	 f += sv_frame_t(rate * 5 + 0.5)) {
+	keyFrames.push_back(f);
+    }
+    
+    return keyFrames;
+}
+
+
+
+
+
+    
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/view/AlignmentView.h	Fri Jan 13 10:29:50 2017 +0000
@@ -0,0 +1,50 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2014 Chris Cannam and QMUL.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef ALIGNMENT_VIEW_H
+#define ALIGNMENT_VIEW_H
+
+#include "View.h"
+
+class AlignmentView : public View
+{
+    Q_OBJECT
+
+public:
+    AlignmentView(QWidget *parent = 0);
+    virtual QString getPropertyContainerIconName() const { return "alignment"; }
+    
+    void setViewAbove(View *view);
+    void setViewBelow(View *view);
+
+public slots:
+    virtual void globalCentreFrameChanged(sv_frame_t);
+    virtual void viewCentreFrameChanged(View *, sv_frame_t);
+    virtual void viewAboveZoomLevelChanged(int, bool);
+    virtual void viewBelowZoomLevelChanged(int, bool);
+    virtual void viewManagerPlaybackFrameChanged(sv_frame_t);
+
+protected:
+    virtual void paintEvent(QPaintEvent *e);
+    virtual bool shouldLabelSelections() const { return false; }
+
+    std::vector<sv_frame_t> getKeyFrames();
+    std::vector<sv_frame_t> getDefaultKeyFrames();
+
+    View *m_above;
+    View *m_below;
+};
+
+#endif
--- a/view/Overview.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/view/Overview.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -211,8 +211,6 @@
     paint.setClipRegion(e->region());
     paint.setRenderHints(QPainter::Antialiasing);
     
-    QRect r(rect());
-
     // We paint a rounded rect for each distinct set of view extents,
     // and we colour in the inside and outside of the rect that
     // corresponds to the current view. (One small caveat -- we don't
@@ -245,7 +243,6 @@
 	int x0 = getXForFrame(f0);
 	int x1 = getXForFrame(f1);
 
-
 	if (x1 <= x0) x1 = x0 + 1;
 
         std::pair<int, int> extent(x0, x1);
--- a/view/Pane.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/view/Pane.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -22,9 +22,11 @@
 #include "ViewManager.h"
 #include "widgets/CommandHistory.h"
 #include "widgets/TextAbbrev.h"
+#include "widgets/IconLoader.h"
 #include "base/Preferences.h"
 #include "layer/WaveformLayer.h"
 #include "layer/TimeRulerLayer.h"
+#include "layer/PaintAssistant.h"
 
 // GF: added so we can propagate the mouse move event to the note layer for context handling.
 #include "layer/LayerFactory.h"
@@ -141,8 +143,8 @@
         m_hthumb->setObjectName(tr("Horizontal Zoom"));
         m_hthumb->setCursor(Qt::ArrowCursor);
         layout->addWidget(m_hthumb, 1, 0, 1, 2);
-        m_hthumb->setFixedWidth(70);
-        m_hthumb->setFixedHeight(16);
+        m_hthumb->setFixedWidth(m_manager->scalePixelSize(70));
+        m_hthumb->setFixedHeight(m_manager->scalePixelSize(16));
         m_hthumb->setDefaultValue(0);
         m_hthumb->setSpeed(0.6f);
         connect(m_hthumb, SIGNAL(valueChanged(int)), this, 
@@ -153,8 +155,8 @@
         m_vpan = new Panner;
         m_vpan->setCursor(Qt::ArrowCursor);
         layout->addWidget(m_vpan, 0, 1);
-        m_vpan->setFixedWidth(12);
-        m_vpan->setFixedHeight(70);
+        m_vpan->setFixedWidth(m_manager->scalePixelSize(12));
+        m_vpan->setFixedHeight(m_manager->scalePixelSize(70));
         m_vpan->setAlpha(80, 130);
         connect(m_vpan, SIGNAL(rectExtentsChanged(float, float, float, float)),
                 this, SLOT(verticalPannerMoved(float, float, float, float)));
@@ -167,8 +169,8 @@
         m_vthumb->setObjectName(tr("Vertical Zoom"));
         m_vthumb->setCursor(Qt::ArrowCursor);
         layout->addWidget(m_vthumb, 0, 2);
-        m_vthumb->setFixedWidth(16);
-        m_vthumb->setFixedHeight(70);
+        m_vthumb->setFixedWidth(m_manager->scalePixelSize(16));
+        m_vthumb->setFixedHeight(m_manager->scalePixelSize(70));
         connect(m_vthumb, SIGNAL(valueChanged(int)), this, 
                 SLOT(verticalThumbwheelMoved(int)));
         connect(m_vthumb, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget()));
@@ -182,9 +184,9 @@
         m_reset = new NotifyingPushButton;
         m_reset->setFlat(true);
         m_reset->setCursor(Qt::ArrowCursor);
-        m_reset->setFixedHeight(16);
-        m_reset->setFixedWidth(16);
-        m_reset->setIcon(QPixmap(":/icons/zoom-reset.png"));
+        m_reset->setFixedHeight(m_manager->scalePixelSize(16));
+        m_reset->setFixedWidth(m_manager->scalePixelSize(16));
+        m_reset->setIcon(IconLoader().load("zoom-reset"));
         m_reset->setToolTip(tr("Reset zoom to default"));
         layout->addWidget(m_reset, 1, 2);
         
@@ -283,16 +285,19 @@
     updateVerticalPanner();
 
     if (m_manager && m_manager->getZoomWheelsEnabled() &&
-        width() > 120 && height() > 100) {
+        width() > m_manager->scalePixelSize(120) &&
+        height() > m_manager->scalePixelSize(100)) {
         if (!m_headsUpDisplay->isVisible()) {
             m_headsUpDisplay->show();
         }
+        int shift = m_manager->scalePixelSize(86);
         if (haveVThumb) {
             m_headsUpDisplay->setFixedHeight(m_vthumb->height() + m_hthumb->height());
-            m_headsUpDisplay->move(width() - 86, height() - 86);
+            m_headsUpDisplay->move(width() - shift, height() - shift);
         } else {
             m_headsUpDisplay->setFixedHeight(m_hthumb->height());
-            m_headsUpDisplay->move(width() - 86, height() - 16);
+            m_headsUpDisplay->move(width() - shift,
+                                   height() - m_manager->scalePixelSize(16));
         }
     } else {
         m_headsUpDisplay->hide();
@@ -717,6 +722,10 @@
 void
 Pane::drawCentreLine(sv_samplerate_t sampleRate, QPainter &paint, bool omitLine)
 {
+    if (omitLine && m_manager->getMainModelSampleRate() == 0) {
+        return;
+    }
+    
     int fontHeight = paint.fontMetrics().height();
     int fontAscent = paint.fontMetrics().ascent();
 
@@ -773,14 +782,14 @@
             int tw = paint.fontMetrics().width(text);
             int x = width()/2 - 4 - tw;
             
-            drawVisibleText(paint, x, y, text, OutlinedText);
+            PaintAssistant::drawVisibleText(this, paint, x, y, text, PaintAssistant::OutlinedText);
         }
         
         QString text = QString("%1").arg(m_centreFrame);
         
         int x = width()/2 + 4;
         
-        drawVisibleText(paint, x, y, text, OutlinedText);
+        PaintAssistant::drawVisibleText(this, paint, x, y, text, PaintAssistant::OutlinedText);
     }
 }
 
@@ -862,8 +871,8 @@
         return;
     }
     
-    drawVisibleText(paint, m_scaleWidth + 5,
-                    paint.fontMetrics().ascent() + y, text, OutlinedText);
+    PaintAssistant::drawVisibleText(this, paint, m_scaleWidth + 5,
+                    paint.fontMetrics().ascent() + y, text, PaintAssistant::OutlinedText);
 
     paint.restore();
 }
@@ -901,8 +910,8 @@
         return;
     }
     
-    drawVisibleText(paint, m_scaleWidth + 5,
-                    paint.fontMetrics().ascent() + y, text, OutlinedText);
+    PaintAssistant::drawVisibleText(this, paint, m_scaleWidth + 5,
+                    paint.fontMetrics().ascent() + y, text, PaintAssistant::OutlinedText);
 
     paint.restore();
 }
@@ -915,7 +924,7 @@
 
     int lly = height() - 6;
     if (m_manager->getZoomWheelsEnabled()) {
-        lly -= 20;
+        lly -= m_manager->scalePixelSize(20);
     }
 
     if (r.y() + r.height() < lly - int(m_layerStack.size()) * fontHeight) {
@@ -937,7 +946,7 @@
 
     int llx = width() - maxTextWidth - 5;
     if (m_manager->getZoomWheelsEnabled()) {
-        llx -= 36;
+        llx -= m_manager->scalePixelSize(36);
     }
     
     if (r.x() + r.width() >= llx - fontAscent - 3) {
@@ -950,9 +959,9 @@
                 paint.setPen(getForeground());
             }
             
-            drawVisibleText(paint, llx,
+            PaintAssistant::drawVisibleText(this, paint, llx,
                             lly - fontHeight + fontAscent,
-                            texts[i], OutlinedText);
+                            texts[i], PaintAssistant::OutlinedText);
 
             if (!pixmaps[i].isNull()) {
                 paint.drawPixmap(llx - fontAscent - 3,
@@ -1014,10 +1023,10 @@
             offsetText = tr("+%1").arg(offsetText);
         }
     }
-    drawVisibleText(paint, p0 + 2, fontAscent + fontHeight + 4, startText, OutlinedText);
-    drawVisibleText(paint, p1 + 2, fontAscent + fontHeight + 4, endText, OutlinedText);
-    drawVisibleText(paint, p0 + 2, fontAscent + fontHeight*2 + 4, offsetText, OutlinedText);
-    drawVisibleText(paint, p1 + 2, fontAscent + fontHeight*2 + 4, offsetText, OutlinedText);
+    PaintAssistant::drawVisibleText(this, paint, p0 + 2, fontAscent + fontHeight + 4, startText, PaintAssistant::OutlinedText);
+    PaintAssistant::drawVisibleText(this, paint, p1 + 2, fontAscent + fontHeight + 4, endText, PaintAssistant::OutlinedText);
+    PaintAssistant::drawVisibleText(this, paint, p0 + 2, fontAscent + fontHeight*2 + 4, offsetText, PaintAssistant::OutlinedText);
+    PaintAssistant::drawVisibleText(this, paint, p1 + 2, fontAscent + fontHeight*2 + 4, offsetText, PaintAssistant::OutlinedText);
     
     //!!! duplicating display policy with View::drawSelections
     
@@ -1048,23 +1057,19 @@
     sv_samplerate_t modelRate = waveformModel->getSampleRate();
     sv_samplerate_t nativeRate = waveformModel->getNativeRate();
     sv_samplerate_t playbackRate = m_manager->getPlaybackSampleRate();
-    sv_samplerate_t outputRate = m_manager->getOutputSampleRate();
         
     QString srNote = "";
 
-    // Show (R) for waveform models that have been resampled or will
-    // be resampled on playback, and (X) for waveform models that will
-    // be played at the wrong rate because their rate differs from the
-    // current playback rate (which is not necessarily that of the
-    // main model).
-
-    if (playbackRate != 0) {
-        if (modelRate == playbackRate) {
-            if (modelRate != outputRate || modelRate != nativeRate) {
-                srNote = " " + tr("(R)");
-            }
-        } else {
+    // Show (R) for waveform models that have been resampled during
+    // load, and (X) for waveform models that will be played at the
+    // wrong rate because their rate differs from the current playback
+    // rate (which is not necessarily that of the main model).
+
+    if (modelRate != nativeRate) {
+        if (playbackRate != 0 && modelRate != playbackRate) {
             srNote = " " + tr("(X)");
+        } else {            
+            srNote = " " + tr("(R)");
         }
     }
 
@@ -1080,9 +1085,9 @@
     if (x < pbw + 5) x = pbw + 5;
 
     if (r.x() < x + paint.fontMetrics().width(desc)) {
-        drawVisibleText(paint, x,
+        PaintAssistant::drawVisibleText(this, paint, x,
                         height() - fontHeight + fontAscent - 6,
-                        desc, OutlinedText);
+                        desc, PaintAssistant::OutlinedText);
     }
 }
 
@@ -1118,7 +1123,7 @@
 }
 
 QImage *
-Pane::toNewImage(sv_frame_t f0, sv_frame_t f1)
+Pane::renderPartToNewImage(sv_frame_t f0, sv_frame_t f1)
 {
     int x0 = int(f0 / getZoomLevel());
     int x1 = int(f1 / getZoomLevel());
@@ -1157,9 +1162,9 @@
 }
 
 QSize
-Pane::getImageSize(sv_frame_t f0, sv_frame_t f1)
+Pane::getRenderedPartImageSize(sv_frame_t f0, sv_frame_t f1)
 {
-    QSize s = View::getImageSize(f0, f1);
+    QSize s = View::getRenderedPartImageSize(f0, f1);
     QImage *image = new QImage(100, 100, QImage::Format_RGB32);
     QPainter paint(image);
 
@@ -1943,7 +1948,7 @@
          true, // can move horiz
          canTopLayerMoveVertical(), // can move vert
          canTopLayerMoveVertical() || (m_manager && m_manager->isPlaying()), // resist horiz
-         !(m_manager && m_manager->isPlaying())); // resist vert
+         true); // resist vert
 
     if (m_dragMode == HorizontalDrag ||
         m_dragMode == FreeDrag) {
@@ -2264,8 +2269,10 @@
 void
 Pane::wheelEvent(QWheelEvent *e)
 {
-    cerr << "wheelEvent, delta " << e->delta() << ", angleDelta " << e->angleDelta().x() << "," << e->angleDelta().y() << ", pixelDelta " << e->pixelDelta().x() << "," << e->pixelDelta().y() << ", modifiers " << e->modifiers() << endl;
-
+//    cerr << "wheelEvent, delta " << e->delta() << ", angleDelta " << e->angleDelta().x() << "," << e->angleDelta().y() << ", pixelDelta " << e->pixelDelta().x() << "," << e->pixelDelta().y() << ", modifiers " << e->modifiers() << endl;
+
+    e->accept(); // we never want wheel events on the pane to be propagated
+    
     int dx = e->angleDelta().x();
     int dy = e->angleDelta().y();
 
@@ -2285,7 +2292,7 @@
     }
 
     if (e->phase() == Qt::ScrollBegin ||
-        fabs(d) >= 120 ||
+        std::abs(d) >= 120 ||
         (d > 0 && m_pendingWheelAngle < 0) ||
         (d < 0 && m_pendingWheelAngle > 0)) {
         m_pendingWheelAngle = d;
@@ -2312,7 +2319,7 @@
             m_pendingWheelAngle = 0;
             return;
         }
-        
+
         while (abs(m_pendingWheelAngle) >= 120) {
 
             int sign = (m_pendingWheelAngle < 0 ? -1 : 1);
@@ -2331,7 +2338,7 @@
 void
 Pane::wheelVertical(int sign, Qt::KeyboardModifiers mods)
 {
-    cerr << "wheelVertical: sign = " << sign << endl;
+//    cerr << "wheelVertical: sign = " << sign << endl;
 
     if (mods & Qt::ShiftModifier) {
 
@@ -2378,7 +2385,7 @@
 void
 Pane::wheelHorizontal(int sign, Qt::KeyboardModifiers mods)
 {
-    cerr << "wheelHorizontal: sign = " << sign << endl;
+//    cerr << "wheelHorizontal: sign = " << sign << endl;
 
     // Scroll left or right, rapidly
 
@@ -2388,7 +2395,7 @@
 void
 Pane::wheelHorizontalFine(int pixels, Qt::KeyboardModifiers)
 {
-    cerr << "wheelHorizontalFine: pixels = " << pixels << endl;
+//    cerr << "wheelHorizontalFine: pixels = " << pixels << endl;
 
     // Scroll left or right by a fixed number of pixels
 
--- a/view/Pane.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/view/Pane.h	Fri Jan 13 10:29:50 2017 +0000
@@ -37,28 +37,35 @@
 
 public:
     Pane(QWidget *parent = 0);
-    virtual QString getPropertyContainerIconName() const { return "pane"; }
+    virtual QString getPropertyContainerIconName() const override { return "pane"; }
 
     virtual bool shouldIlluminateLocalFeatures(const Layer *layer,
-					       QPoint &pos) const;
+					       QPoint &pos) const override;
     virtual bool shouldIlluminateLocalSelection(QPoint &pos,
 						bool &closeToLeft,
-						bool &closeToRight) const;
+						bool &closeToRight) const override;
 
     void setCentreLineVisible(bool visible);
     bool getCentreLineVisible() const { return m_centreLineVisible; }
 
-    virtual sv_frame_t getFirstVisibleFrame() const;
+    virtual sv_frame_t getFirstVisibleFrame() const override;
 
-    virtual int getVerticalScaleWidth() const;
+    int getVerticalScaleWidth() const;
 
-    virtual QImage *toNewImage(sv_frame_t f0, sv_frame_t f1);
-    virtual QImage *toNewImage() { return View::toNewImage(); }
-    virtual QSize getImageSize(sv_frame_t f0, sv_frame_t f1);
-    virtual QSize getImageSize() { return View::getImageSize(); }
+    virtual QImage *renderToNewImage() override {
+        return View::renderToNewImage();
+    }
+    
+    virtual QImage *renderPartToNewImage(sv_frame_t f0, sv_frame_t f1) override;
+
+    virtual QSize getRenderedImageSize() override {
+        return View::getRenderedImageSize();
+    }
+    
+    virtual QSize getRenderedPartImageSize(sv_frame_t f0, sv_frame_t f1) override;
 
     virtual void toXml(QTextStream &stream, QString indent = "",
-                       QString extraAttributes = "") const;
+                       QString extraAttributes = "") const override;
 
     static void registerShortcuts(KeyReference &kr);
 
@@ -77,20 +84,22 @@
     void regionOutlined(QRect rect);
 
 public slots:
-    virtual void toolModeChanged();
-    virtual void zoomWheelsEnabledChanged();
-    virtual void viewZoomLevelChanged(View *v, int z, bool locked);
-    virtual void modelAlignmentCompletionChanged();
+    // view slots
+    virtual void toolModeChanged() override;
+    virtual void zoomWheelsEnabledChanged() override;
+    virtual void viewZoomLevelChanged(View *v, int z, bool locked) override;
+    virtual void modelAlignmentCompletionChanged() override;
 
+    // local slots, not overrides
     virtual void horizontalThumbwheelMoved(int value);
     virtual void verticalThumbwheelMoved(int value);
     virtual void verticalZoomChanged();
     virtual void verticalPannerMoved(float x, float y, float w, float h);
     virtual void editVerticalPannerExtents();
 
-    virtual void layerParametersChanged();
+    virtual void layerParametersChanged() override;
 
-    virtual void propertyContainerSelected(View *, PropertyContainer *pc);
+    virtual void propertyContainerSelected(View *, PropertyContainer *pc) override;
 
     void zoomToRegion(QRect r);
 
@@ -101,17 +110,17 @@
     void playbackScheduleTimerElapsed();
 
 protected:
-    virtual void paintEvent(QPaintEvent *e);
-    virtual void mousePressEvent(QMouseEvent *e);
-    virtual void mouseReleaseEvent(QMouseEvent *e);
-    virtual void mouseMoveEvent(QMouseEvent *e);
-    virtual void mouseDoubleClickEvent(QMouseEvent *e);
-    virtual void enterEvent(QEvent *e);
-    virtual void leaveEvent(QEvent *e);
-    virtual void wheelEvent(QWheelEvent *e);
-    virtual void resizeEvent(QResizeEvent *e);
-    virtual void dragEnterEvent(QDragEnterEvent *e);
-    virtual void dropEvent(QDropEvent *e);
+    virtual void paintEvent(QPaintEvent *e) override;
+    virtual void mousePressEvent(QMouseEvent *e) override;
+    virtual void mouseReleaseEvent(QMouseEvent *e) override;
+    virtual void mouseMoveEvent(QMouseEvent *e) override;
+    virtual void mouseDoubleClickEvent(QMouseEvent *e) override;
+    virtual void enterEvent(QEvent *e) override;
+    virtual void leaveEvent(QEvent *e) override;
+    virtual void wheelEvent(QWheelEvent *e) override;
+    virtual void resizeEvent(QResizeEvent *e) override;
+    virtual void dragEnterEvent(QDragEnterEvent *e) override;
+    virtual void dropEvent(QDropEvent *e) override;
 
     void wheelVertical(int sign, Qt::KeyboardModifiers);
     void wheelHorizontal(int sign, Qt::KeyboardModifiers);
@@ -127,7 +136,7 @@
     void drawEditingSelection(QPainter &);
     void drawAlignmentStatus(QRect, QPainter &, const Model *, bool down);
 
-    virtual bool render(QPainter &paint, int x0, sv_frame_t f0, sv_frame_t f1);
+    virtual bool render(QPainter &paint, int x0, sv_frame_t f0, sv_frame_t f1) override;
 
     Selection getSelectionAt(int x, bool &closeToLeft, bool &closeToRight) const;
 
--- a/view/PaneStack.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/view/PaneStack.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -21,6 +21,7 @@
 #include "widgets/ClickableLabel.h"
 #include "layer/Layer.h"
 #include "ViewManager.h"
+#include "AlignmentView.h"
 
 #include <QApplication>
 #include <QHBoxLayout>
@@ -40,6 +41,7 @@
     QFrame(parent),
     m_currentPane(0),
     m_showAccessories(true),
+    m_showAlignmentViews(false),
     m_splitter(new QSplitter),
     m_propertyStackStack(new QStackedWidget),
     m_viewManager(viewManager),
@@ -67,6 +69,15 @@
     m_showAccessories = show;
 }
 
+void
+PaneStack::setShowAlignmentViews(bool show)
+{
+    m_showAlignmentViews = show;
+    foreach (const PaneRec &r, m_panes) {
+        r.alignmentView->setVisible(m_showAlignmentViews);
+    }
+}
+
 Pane *
 PaneStack::addPane(bool suppressPropertyBox)
 {
@@ -112,6 +123,12 @@
     layout->addWidget(pane, 0, 1, 2, 1);
     layout->setColumnStretch(1, 20);
 
+    AlignmentView *av = new AlignmentView(frame);
+    av->setFixedHeight(40);//!!!
+    av->setVisible(m_showAlignmentViews);
+    av->setViewManager(m_viewManager);
+    layout->addWidget(av, 2, 1);
+
     QWidget *properties = 0;
     if (suppressPropertyBox) {
 	properties = new QFrame();
@@ -139,6 +156,7 @@
     rec.currentIndicator = currentIndicator;
     rec.frame = frame;
     rec.layout = layout;
+    rec.alignmentView = av;
     m_panes.push_back(rec);
 
     frame->setLayout(layout);
@@ -167,11 +185,34 @@
     }
 
     showOrHidePaneAccessories();
+    relinkAlignmentViews();
 
     return pane;
 }
 
 void
+PaneStack::relinkAlignmentViews()
+{
+    for (int i = 0; i < (int)m_panes.size(); ++i) {
+        m_panes[i].alignmentView->setViewAbove(m_panes[i].pane);
+        if (i + 1 < (int)m_panes.size()) {
+            m_panes[i].alignmentView->setViewBelow(m_panes[i+1].pane);
+        } else {
+            m_panes[i].alignmentView->setViewBelow(0);
+        }
+    }
+}
+
+void
+PaneStack::unlinkAlignmentViews()
+{
+    for (int i = 0; i < (int)m_panes.size(); ++i) {
+        m_panes[i].alignmentView->setViewAbove(0);
+        m_panes[i].alignmentView->setViewBelow(0);
+    }
+}
+
+void
 PaneStack::setPropertyStackMinWidth(int mw)
 {
     for (std::vector<PaneRec>::iterator i = m_panes.begin();
@@ -279,6 +320,7 @@
     }
 
     emit paneAboutToBeDeleted(pane);
+    unlinkAlignmentViews();
 
     cerr << "PaneStack::deletePane: about to delete parent " << pane->parent() << " of pane " << pane << endl;
 
@@ -303,6 +345,7 @@
     }
 
     showOrHidePaneAccessories();
+    relinkAlignmentViews();
 
     emit paneDeleted();
 }
@@ -362,6 +405,8 @@
 	++i;
     }
 
+    relinkAlignmentViews();
+
     cerr << "WARNING: PaneStack::hidePane(" << pane << "): Pane not found in visible panes" << endl;
 }
 
@@ -386,6 +431,8 @@
 	++i;
     }
 
+    relinkAlignmentViews();
+
     cerr << "WARNING: PaneStack::showPane(" << pane << "): Pane not found in hidden panes" << endl;
 }
 
--- a/view/PaneStack.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/view/PaneStack.h	Fri Jan 13 10:29:50 2017 +0000
@@ -34,6 +34,7 @@
 class ViewManager;
 class PropertyContainer;
 class PropertyStack;
+class AlignmentView;
 
 class PaneStack : public QFrame
 {
@@ -73,6 +74,8 @@
     
     void setShowPaneAccessories(bool show); // current indicator, close button
 
+    void setShowAlignmentViews(bool show);
+
     void sizePanesEqually();
 
 signals:
@@ -114,18 +117,20 @@
 
     struct PaneRec
     {
-	Pane        *pane;
-	QWidget     *propertyStack;
-        QPushButton *xButton;
-	QLabel      *currentIndicator;
-        QFrame      *frame;
-        QGridLayout *layout;
+	Pane          *pane;
+	QWidget       *propertyStack;
+        QPushButton   *xButton;
+	QLabel        *currentIndicator;
+        QFrame        *frame;
+        QGridLayout   *layout;
+        AlignmentView *alignmentView;
     };
 
     std::vector<PaneRec> m_panes;
     std::vector<PaneRec> m_hiddenPanes;
 
     bool m_showAccessories;
+    bool m_showAlignmentViews;
 
     QSplitter *m_splitter;
     QStackedWidget *m_propertyStackStack;
@@ -136,6 +141,9 @@
 
     void showOrHidePaneAccessories();
 
+    void unlinkAlignmentViews();
+    void relinkAlignmentViews();
+
     LayoutStyle m_layoutStyle;
 };
 
--- a/view/View.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/view/View.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -20,9 +20,12 @@
 #include "base/Profiler.h"
 #include "base/Pitch.h"
 #include "base/Preferences.h"
+#include "ViewProxy.h"
 
 #include "layer/TimeRulerLayer.h"
 #include "layer/SingleColourLayer.h"
+#include "layer/PaintAssistant.h"
+
 #include "data/model/PowerOfSqrtTwoZoomConstraint.h"
 #include "data/model/RangeSummarisableTimeValueModel.h"
 
@@ -37,19 +40,19 @@
 #include <QFont>
 #include <QMessageBox>
 #include <QPushButton>
+#include <QSettings>
+#include <QSvgGenerator>
 
 #include <iostream>
 #include <cassert>
 #include <cmath>
 
-#include <unistd.h>
-
 //#define DEBUG_VIEW 1
 //#define DEBUG_VIEW_WIDGET_PAINT 1
 
-
 View::View(QWidget *w, bool showProgress) :
     QFrame(w),
+    m_id(getNextId()),
     m_centreFrame(0),
     m_zoomLevel(1024),
     m_followPan(true),
@@ -59,6 +62,7 @@
     m_playPointerFrame(0),
     m_showProgress(showProgress),
     m_cache(0),
+    m_buffer(0),
     m_cacheCentreFrame(0),
     m_cacheZoomLevel(1024),
     m_selectionCached(false),
@@ -76,6 +80,8 @@
 
     m_deleting = true;
     delete m_propertyContainer;
+    delete m_cache;
+    delete m_buffer;
 }
 
 PropertyContainer::PropertyList
@@ -362,15 +368,17 @@
 sv_frame_t
 View::getFrameForX(int x) const
 {
-    int z = m_zoomLevel;
+    sv_frame_t z = m_zoomLevel; // nb not just int, or multiplication may overflow
     sv_frame_t frame = m_centreFrame - (width()/2) * z;
 
+    frame = (frame / z) * z; // this is start frame
+    frame = frame + x * z;
+
 #ifdef DEBUG_VIEW_WIDGET_PAINT
-    SVDEBUG << "View::getFrameForX(" << x << "): z = " << z << ", m_centreFrame = " << m_centreFrame << ", width() = " << width() << ", frame = " << frame << endl;
+    cerr << "View::getFrameForX(" << x << "): z = " << z << ", m_centreFrame = " << m_centreFrame << ", width() = " << width() << ", frame = " << frame << endl;
 #endif
 
-    frame = (frame / z) * z; // this is start frame
-    return frame + x * z;
+    return frame;
 }
 
 double
@@ -408,12 +416,12 @@
 }
 
 double
-View::getFrequencyForY(int y,
+View::getFrequencyForY(double y,
 		       double minf,
 		       double maxf,
 		       bool logarithmic) const
 {
-    int h = height();
+    double h = height();
 
     if (logarithmic) {
 
@@ -448,9 +456,30 @@
     return m_zoomLevel;
 }
 
+int
+View::effectiveDevicePixelRatio() const
+{
+#ifdef Q_OS_MAC
+    int dpratio = devicePixelRatio();
+    if (dpratio > 1) {
+        QSettings settings;
+        settings.beginGroup("Preferences");
+        if (!settings.value("scaledHiDpi", true).toBool()) {
+            dpratio = 1;
+        }
+        settings.endGroup();
+    }
+    return dpratio;
+#else
+    return 1;
+#endif
+}
+
 void
 View::setZoomLevel(int z)
 {
+    int dpratio = effectiveDevicePixelRatio();
+    if (z < dpratio) return;
     if (z < 1) z = 1;
     if (m_zoomLevel != int(z)) {
 	m_zoomLevel = z;
@@ -784,56 +813,6 @@
 }
 
 void
-View::drawVisibleText(QPainter &paint, int x, int y, QString text, TextStyle style) const
-{
-    if (style == OutlinedText || style == OutlinedItalicText) {
-
-        paint.save();
-
-        if (style == OutlinedItalicText) {
-            QFont f(paint.font());
-            f.setItalic(true);
-            paint.setFont(f);
-        }
-
-        QColor penColour, surroundColour, boxColour;
-
-        penColour = getForeground();
-        surroundColour = getBackground();
-        boxColour = surroundColour;
-        boxColour.setAlpha(127);
-
-        paint.setPen(Qt::NoPen);
-        paint.setBrush(boxColour);
-        
-        QRect r = paint.fontMetrics().boundingRect(text);
-        r.translate(QPoint(x, y));
-//        cerr << "drawVisibleText: r = " << r.x() << "," <<r.y() << " " << r.width() << "x" << r.height() << endl;
-        paint.drawRect(r);
-        paint.setBrush(Qt::NoBrush);
-
-	paint.setPen(surroundColour);
-
-	for (int dx = -1; dx <= 1; ++dx) {
-	    for (int dy = -1; dy <= 1; ++dy) {
-		if (!(dx || dy)) continue;
-		paint.drawText(x + dx, y + dy, text);
-	    }
-	}
-
-	paint.setPen(penColour);
-
-	paint.drawText(x, y, text);
-
-        paint.restore();
-
-    } else {
-
-	cerr << "ERROR: View::drawVisibleText: Boxed style not yet implemented!" << endl;
-    }
-}
-
-void
 View::setPlaybackFollow(PlaybackFollowMode m)
 {
     m_followPlay = m;
@@ -1027,7 +1006,7 @@
     f = getAlignedPlaybackFrame();
 
 #ifdef DEBUG_VIEW
-    cerr << " -> aligned frame = " << af << endl;
+    cerr << " -> aligned frame = " << f << endl;
 #endif
 
     movePlayPointer(f);
@@ -1653,11 +1632,27 @@
 void
 View::setPaintFont(QPainter &paint)
 {
+    int scaleFactor = 1;
+    int dpratio = effectiveDevicePixelRatio();
+    if (dpratio > 1) {
+        QPaintDevice *dev = paint.device();
+        if (dynamic_cast<QPixmap *>(dev) || dynamic_cast<QImage *>(dev)) {
+            scaleFactor = dpratio;
+        }
+    }
+
     QFont font(paint.font());
-    font.setPointSize(Preferences::getInstance()->getViewFontSize());
+    font.setPointSize(Preferences::getInstance()->getViewFontSize()
+                      * scaleFactor);
     paint.setFont(font);
 }
 
+QRect
+View::getPaintRect() const
+{
+    return rect();
+}
+
 void
 View::paintEvent(QPaintEvent *e)
 {
@@ -1694,6 +1689,8 @@
 
     QRect nonCacheRect(cacheRect);
 
+    int dpratio = effectiveDevicePixelRatio();
+
     // If not all layers are scrollable, but some of the back layers
     // are, we should store only those in the cache.
 
@@ -1705,25 +1702,34 @@
 
     // If all the non-scrollable layers are non-opaque, then we draw
     // the selection rectangle behind them and cache it.  If any are
-    // opaque, however, we can't cache.
+    // opaque, however, or if our device-pixel ratio is not 1 (so we
+    // need to paint direct to the widget), then we can't cache.
     //
-    if (!selectionCacheable) {
-	selectionCacheable = true;
-	for (LayerList::const_iterator i = nonScrollables.begin();
-	     i != nonScrollables.end(); ++i) {
-	    if ((*i)->isLayerOpaque()) {
-		selectionCacheable = false;
-		break;
-	    }
-	}
-    }
-
-    if (selectionCacheable) {
-	QPoint localPos;
-	bool closeToLeft, closeToRight;
-	if (shouldIlluminateLocalSelection(localPos, closeToLeft, closeToRight)) {
-	    selectionCacheable = false;
-	}
+    if (dpratio == 1) {
+
+        if (!selectionCacheable) {
+            selectionCacheable = true;
+            for (LayerList::const_iterator i = nonScrollables.begin();
+                 i != nonScrollables.end(); ++i) {
+                if ((*i)->isLayerOpaque()) {
+                    selectionCacheable = false;
+                    break;
+                }
+            }
+        }
+
+        if (selectionCacheable) {
+            QPoint localPos;
+            bool closeToLeft, closeToRight;
+            if (shouldIlluminateLocalSelection
+                (localPos, closeToLeft, closeToRight)) {
+                selectionCacheable = false;
+            }
+        }
+
+    } else {
+
+        selectionCacheable = false;
     }
 
 #ifdef DEBUG_VIEW_WIDGET_PAINT
@@ -1741,6 +1747,14 @@
 	m_selectionCached = false;
     }
 
+    QSize scaledCacheSize(scaledSize(size(), dpratio));
+    QRect scaledCacheRect(scaledRect(cacheRect, dpratio));
+
+    if (!m_buffer || scaledCacheSize != m_buffer->size()) {
+        delete m_buffer;
+        m_buffer = new QPixmap(scaledCacheSize);
+    }
+    
     if (!scrollables.empty()) {
 
 #ifdef DEBUG_VIEW_WIDGET_PAINT
@@ -1750,8 +1764,7 @@
 
 	if (!m_cache ||
 	    m_cacheZoomLevel != m_zoomLevel ||
-	    width() != m_cache->width() ||
-	    height() != m_cache->height()) {
+            scaledCacheSize != m_cache->size()) {
 
 	    // cache is not valid
 
@@ -1763,7 +1776,7 @@
 #endif
 	    } else {
 		delete m_cache;
-		m_cache = new QPixmap(width(), height());
+		m_cache = new QPixmap(scaledCacheSize);
 #ifdef DEBUG_VIEW_WIDGET_PAINT
 		cerr << "View(" << this << ")::paintEvent: recreated cache" << endl;
 #endif
@@ -1778,24 +1791,10 @@
 		getXForFrame(m_centreFrame);
 
 	    if (dx > -width() && dx < width()) {
-#ifdef PIXMAP_COPY_TO_SELF
-                // This is not normally defined. Copying a pixmap to
-		// itself doesn't work properly on Windows, Mac, or
-		// X11 with the raster backend (it only works when
-		// moving in one direction and then presumably only by
-		// accident).  It does actually seem to be fine on X11
-		// with the native backend, but we prefer not to use
-		// that anyway
-		paint.begin(m_cache);
-		paint.drawPixmap(dx, 0, *m_cache);
-		paint.end();
-#else
 		static QPixmap *tmpPixmap = 0;
-		if (!tmpPixmap ||
-		    tmpPixmap->width() != width() ||
-		    tmpPixmap->height() != height()) {
+		if (!tmpPixmap || tmpPixmap->size() != scaledCacheSize) {
 		    delete tmpPixmap;
-		    tmpPixmap = new QPixmap(width(), height());
+		    tmpPixmap = new QPixmap(scaledCacheSize);
 		}
 		paint.begin(tmpPixmap);
 		paint.drawPixmap(0, 0, *m_cache);
@@ -1803,7 +1802,6 @@
 		paint.begin(m_cache);
 		paint.drawPixmap(dx, 0, *tmpPixmap);
 		paint.end();
-#endif
 		if (dx < 0) {
 		    cacheRect = QRect(width() + dx, 0, -dx, height());
 		} else {
@@ -1824,8 +1822,8 @@
 #ifdef DEBUG_VIEW_WIDGET_PAINT
 	    cerr << "View(" << this << ")::paintEvent: cache is good" << endl;
 #endif
-	    paint.begin(this);
-	    paint.drawPixmap(cacheRect, *m_cache, cacheRect);
+	    paint.begin(m_buffer);
+	    paint.drawPixmap(scaledCacheRect, *m_cache, scaledCacheRect);
 	    paint.end();
 	    QFrame::paintEvent(e);
 	    paintedCacheRect = true;
@@ -1841,16 +1839,26 @@
 
     // Scrollable (cacheable) items first
 
+    ViewProxy proxy(this, dpratio);
+    
     if (!paintedCacheRect) {
 
-	if (repaintCache) paint.begin(m_cache);
-	else paint.begin(this);
+        QRect rectToPaint;
+
+	if (repaintCache) {
+            paint.begin(m_cache);
+            rectToPaint = scaledCacheRect;
+        } else {
+            paint.begin(m_buffer);
+            rectToPaint = scaledCacheRect;
+        }
+
         setPaintFont(paint);
-	paint.setClipRect(cacheRect);
+	paint.setClipRect(rectToPaint);
 
         paint.setPen(getBackground());
         paint.setBrush(getBackground());
-	paint.drawRect(cacheRect);
+	paint.drawRect(rectToPaint);
 
 	paint.setPen(getForeground());
 	paint.setBrush(Qt::NoBrush);
@@ -1858,7 +1866,10 @@
 	for (LayerList::iterator i = scrollables.begin(); i != scrollables.end(); ++i) {
 	    paint.setRenderHint(QPainter::Antialiasing, false);
 	    paint.save();
-	    (*i)->paint(this, paint, cacheRect);
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+            cerr << "Painting scrollable layer " << *i << " using proxy with repaintCache = " << repaintCache << ", dpratio = " << dpratio << ", rectToPaint = " << rectToPaint.x() << "," << rectToPaint.y() << " " << rectToPaint.width() << "x" << rectToPaint.height() << endl;
+#endif
+            (*i)->paint(&proxy, paint, rectToPaint);
 	    paint.restore();
 	}
 
@@ -1871,8 +1882,9 @@
 
 	if (repaintCache) {
 	    cacheRect |= (e ? e->rect() : rect());
-	    paint.begin(this);
-	    paint.drawPixmap(cacheRect, *m_cache, cacheRect);
+            scaledCacheRect = scaledRect(cacheRect, dpratio);
+	    paint.begin(m_buffer);
+	    paint.drawPixmap(scaledCacheRect, *m_cache, scaledCacheRect);
 	    paint.end();
 	}
     }
@@ -1883,13 +1895,15 @@
 
     nonCacheRect |= cacheRect;
 
-    paint.begin(this);
-    paint.setClipRect(nonCacheRect);
+    QRect scaledNonCacheRect = scaledRect(nonCacheRect, dpratio);
+    
+    paint.begin(m_buffer);
+    paint.setClipRect(scaledNonCacheRect);
     setPaintFont(paint);
     if (scrollables.empty()) {
         paint.setPen(getBackground());
         paint.setBrush(getBackground());
-	paint.drawRect(nonCacheRect);
+	paint.drawRect(scaledNonCacheRect);
     }
 	
     paint.setPen(getForeground());
@@ -1897,10 +1911,18 @@
 	
     for (LayerList::iterator i = nonScrollables.begin(); i != nonScrollables.end(); ++i) {
 //        Profiler profiler2("View::paintEvent non-cacheable");
-	(*i)->paint(this, paint, nonCacheRect);
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+        cerr << "Painting non-scrollable layer " << *i << " without proxy with repaintCache = " << repaintCache << ", dpratio = " << dpratio << ", rectToPaint = " << nonCacheRect.x() << "," << nonCacheRect.y() << " " << nonCacheRect.width() << "x" << nonCacheRect.height() << endl;
+#endif
+	(*i)->paint(&proxy, paint, scaledNonCacheRect);
     }
 	
     paint.end();
+    
+    paint.begin(this);
+    QRect finalPaintRect = e ? e->rect() : rect();
+    paint.drawPixmap(finalPaintRect, *m_buffer, scaledRect(finalPaintRect, dpratio));
+    paint.end();
 
     paint.begin(this);
     setPaintFont(paint);
@@ -1925,7 +1947,7 @@
             showPlayPointer = false;
         }
     }
-
+    
     if (showPlayPointer) {
 
 	paint.begin(this);
@@ -2081,11 +2103,15 @@
 		dx = p1 - 2 - dw;
 	    }
 
-	    paint.drawText(sx, sy, startText);
-	    paint.drawText(ex, ey, endText);
-	    paint.drawText(dx, dy, durationText);
+            PaintAssistant::drawVisibleText(this, paint, sx, sy, startText,
+                                            PaintAssistant::OutlinedText);
+            PaintAssistant::drawVisibleText(this, paint, ex, ey, endText,
+                                            PaintAssistant::OutlinedText);
+            PaintAssistant::drawVisibleText(this, paint, dx, dy, durationText,
+                                            PaintAssistant::OutlinedText);
             if (durationBothEnds) {
-                paint.drawText(sx, dy, durationText);
+                PaintAssistant::drawVisibleText(this, paint, sx, dy, durationText,
+                                                PaintAssistant::OutlinedText);
             }
 	}
     }
@@ -2300,32 +2326,32 @@
     }
     
     if (axs != "") {
-        drawVisibleText(paint, axx, axy, axs, OutlinedText);
+        PaintAssistant::drawVisibleText(this, paint, axx, axy, axs, PaintAssistant::OutlinedText);
         axy += fontHeight;
     }
     
     if (ays != "") {
-        drawVisibleText(paint, axx, axy, ays, OutlinedText);
+        PaintAssistant::drawVisibleText(this, paint, axx, axy, ays, PaintAssistant::OutlinedText);
         axy += fontHeight;
     }
 
     if (bxs != "") {
-        drawVisibleText(paint, bxx, bxy, bxs, OutlinedText);
+        PaintAssistant::drawVisibleText(this, paint, bxx, bxy, bxs, PaintAssistant::OutlinedText);
         bxy += fontHeight;
     }
 
     if (bys != "") {
-        drawVisibleText(paint, bxx, bxy, bys, OutlinedText);
+        PaintAssistant::drawVisibleText(this, paint, bxx, bxy, bys, PaintAssistant::OutlinedText);
         bxy += fontHeight;
     }
 
     if (dxs != "") {
-        drawVisibleText(paint, dxx, dxy, dxs, OutlinedText);
+        PaintAssistant::drawVisibleText(this, paint, dxx, dxy, dxs, PaintAssistant::OutlinedText);
         dxy += fontHeight;
     }
 
     if (dys != "") {
-        drawVisibleText(paint, dxx, dxy, dys, OutlinedText);
+        PaintAssistant::drawVisibleText(this, paint, dxx, dxy, dys, PaintAssistant::OutlinedText);
         dxy += fontHeight;
     }
 
@@ -2412,23 +2438,23 @@
 
 	for (LayerList::iterator i = m_layerStack.begin();
              i != m_layerStack.end(); ++i) {
-		if(!((*i)->isLayerDormant(this))){
-
-		    paint.setRenderHint(QPainter::Antialiasing, false);
-
-		    paint.save();
-	            paint.translate(xorigin + x, 0);
-
-	            cerr << "Centre frame now: " << m_centreFrame << " drawing to " << chunk.x() + x + xorigin << ", " << chunk.width() << endl;
-
-	            (*i)->setSynchronousPainting(true);
-
-		    (*i)->paint(this, paint, chunk);
-
-	            (*i)->setSynchronousPainting(false);
-
-		    paint.restore();
-		}
+            if (!((*i)->isLayerDormant(this))){
+
+                paint.setRenderHint(QPainter::Antialiasing, false);
+
+                paint.save();
+                paint.translate(xorigin + x, 0);
+
+                cerr << "Centre frame now: " << m_centreFrame << " drawing to " << chunk.x() + x + xorigin << ", " << chunk.width() << endl;
+
+                (*i)->setSynchronousPainting(true);
+
+                (*i)->paint(this, paint, chunk);
+
+                (*i)->setSynchronousPainting(false);
+
+                paint.restore();
+            }
 	}
     }
 
@@ -2438,16 +2464,16 @@
 }
 
 QImage *
-View::toNewImage()
+View::renderToNewImage()
 {
     sv_frame_t f0 = getModelsStartFrame();
     sv_frame_t f1 = getModelsEndFrame();
 
-    return toNewImage(f0, f1);
+    return renderPartToNewImage(f0, f1);
 }
 
 QImage *
-View::toNewImage(sv_frame_t f0, sv_frame_t f1)
+View::renderPartToNewImage(sv_frame_t f0, sv_frame_t f1)
 {
     int x0 = int(f0 / getZoomLevel());
     int x1 = int(f1 / getZoomLevel());
@@ -2466,16 +2492,16 @@
 }
 
 QSize
-View::getImageSize()
+View::getRenderedImageSize()
 {
     sv_frame_t f0 = getModelsStartFrame();
     sv_frame_t f1 = getModelsEndFrame();
 
-    return getImageSize(f0, f1);
+    return getRenderedPartImageSize(f0, f1);
 }
     
 QSize
-View::getImageSize(sv_frame_t f0, sv_frame_t f1)
+View::getRenderedPartImageSize(sv_frame_t f0, sv_frame_t f1)
 {
     int x0 = int(f0 / getZoomLevel());
     int x1 = int(f1 / getZoomLevel());
@@ -2483,6 +2509,35 @@
     return QSize(x1 - x0, height());
 }
 
+bool
+View::renderToSvgFile(QString filename)
+{
+    sv_frame_t f0 = getModelsStartFrame();
+    sv_frame_t f1 = getModelsEndFrame();
+
+    return renderPartToSvgFile(filename, f0, f1);
+}
+
+bool
+View::renderPartToSvgFile(QString filename, sv_frame_t f0, sv_frame_t f1)
+{
+    int x0 = int(f0 / getZoomLevel());
+    int x1 = int(f1 / getZoomLevel());
+
+    QSvgGenerator generator;
+    generator.setFileName(filename);
+    generator.setSize(QSize(x1 - x0, height()));
+    generator.setViewBox(QRect(0, 0, x1 - x0, height()));
+    generator.setTitle(tr("Exported image from %1")
+                       .arg(QApplication::applicationName()));
+    
+    QPainter paint;
+    paint.begin(&generator);
+    bool result = render(paint, 0, f0, f1);
+    paint.end();
+    return result;
+}
+
 void
 View::toXml(QTextStream &stream,
             QString indent, QString extraAttributes) const
--- a/view/View.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/view/View.h	Fri Jan 13 10:29:50 2017 +0000
@@ -19,6 +19,8 @@
 #include <QFrame>
 #include <QProgressBar>
 
+#include "layer/LayerGeometryProvider.h"
+
 #include "base/ZoomConstraint.h"
 #include "base/PropertyContainer.h"
 #include "ViewManager.h"
@@ -49,7 +51,8 @@
  */
 
 class View : public QFrame,
-	     public XmlExportable
+	     public XmlExportable,
+             public LayerGeometryProvider
 {
     Q_OBJECT
 
@@ -61,6 +64,12 @@
     virtual ~View();
 
     /**
+     * Retrieve the id of this object. Views have their own unique
+     * ids, but ViewProxy objects share the id of their View.
+     */
+    int getId() const { return m_id; }
+    
+    /**
      * Retrieve the first visible sample frame on the widget.
      * This is a calculated value based on the centre-frame, widget
      * width and zoom level.  The result may be negative.
@@ -105,6 +114,20 @@
     sv_frame_t getFrameForX(int x) const;
 
     /**
+     * Return the closest pixel x-coordinate corresponding to a given
+     * view x-coordinate. Default is no scaling, ViewProxy handles
+     * scaling case.
+     */
+    int getXForViewX(int viewx) const { return viewx; }
+
+    /**
+     * Return the closest view x-coordinate corresponding to a given
+     * pixel x-coordinate. Default is no scaling, ViewProxy handles
+     * scaling case.
+     */
+    int getViewXForX(int x) const { return x; }
+
+    /**
      * Return the pixel y-coordinate corresponding to a given
      * frequency, if the frequency range is as specified.  This does
      * not imply any policy about layer frequency ranges, but it might
@@ -121,8 +144,8 @@
      *
      * Not thread-safe in logarithmic mode.  Call only from GUI thread.
      */
-    double getFrequencyForY(int y, double minFreq, double maxFreq,
-			   bool logarithmic) const;
+    double getFrequencyForY(double y, double minFreq, double maxFreq,
+                            bool logarithmic) const;
 
     /**
      * Return the zoom level, i.e. the number of frames per pixel
@@ -243,15 +266,6 @@
     virtual QColor getForeground() const;
     virtual QColor getBackground() const;
 
-    enum TextStyle {
-	BoxedText,
-	OutlinedText,
-        OutlinedItalicText
-    };
-
-    virtual void drawVisibleText(QPainter &p, int x, int y,
-				 QString text, TextStyle style) const;
-
     virtual void drawMeasurementRect(QPainter &p, const Layer *,
                                      QRect rect, bool focus) const;
 
@@ -294,12 +308,42 @@
     virtual const PropertyContainer *getPropertyContainer(int i) const;
     virtual PropertyContainer *getPropertyContainer(int i);
 
-    // Render the contents on a wide canvas
-    virtual QImage *toNewImage(sv_frame_t f0, sv_frame_t f1);
-    virtual QImage *toNewImage();
-    virtual QSize getImageSize(sv_frame_t f0, sv_frame_t f1);
-    virtual QSize getImageSize();
+    /** 
+     * Render the view contents to a new QImage (which may be wider
+     * than the visible View).
+     */
+    virtual QImage *renderToNewImage();
 
+    /** 
+     * Render the view contents between the given frame extents to a
+     * new QImage (which may be wider than the visible View).
+     */
+    virtual QImage *renderPartToNewImage(sv_frame_t f0, sv_frame_t f1);
+
+    /**
+     * Calculate and return the size of image that will be generated
+     * by renderToNewImage().
+     */
+    virtual QSize getRenderedImageSize();
+
+    /**
+     * Calculate and return the size of image that will be generated
+     * by renderPartToNewImage(f0, f1).
+     */
+    virtual QSize getRenderedPartImageSize(sv_frame_t f0, sv_frame_t f1);
+
+    /**
+     * Render the view contents to a new SVG file.
+     */
+    virtual bool renderToSvgFile(QString filename);
+
+    /**
+     * Render the view contents between the given frame extents to a
+     * new SVG file.
+     */
+    virtual bool renderPartToSvgFile(QString filename,
+                                     sv_frame_t f0, sv_frame_t f1);
+    
     virtual int getTextLabelHeight(const Layer *layer, QPainter &) const;
 
     virtual bool getValueExtents(QString unit, double &min, double &max,
@@ -315,6 +359,18 @@
     sv_frame_t getModelsStartFrame() const;
     sv_frame_t getModelsEndFrame() const;
 
+    /**
+     * To be called from a layer, to obtain the extent of the surface
+     * that the layer is currently painting to. This may be the extent
+     * of the view (if 1x display scaling is in effect) or of a larger
+     * cached pixmap (if greater display scaling is in effect).
+     */
+    QRect getPaintRect() const;
+
+    QSize getPaintSize() const { return getPaintRect().size(); }
+    int getPaintWidth() const { return getPaintRect().width(); }
+    int getPaintHeight() const { return getPaintRect().height(); }
+
     typedef std::set<Model *> ModelSet;
     ModelSet getModels();
 
@@ -324,6 +380,11 @@
     sv_frame_t alignToReference(sv_frame_t) const;
     sv_frame_t getAlignedPlaybackFrame() const;
 
+    void updatePaintRect(QRect r) { update(r); }
+    
+    View *getView() { return this; } 
+    const View *getView() const { return this; } 
+    
 signals:
     void propertyContainerAdded(PropertyContainer *pc);
     void propertyContainerRemoved(PropertyContainer *pc);
@@ -339,7 +400,7 @@
                             bool globalScroll,
                             PlaybackFollowMode followMode);
 
-    void zoomLevelChanged(int, bool);
+    void zoomLevelChanged(int level, bool locked);
 
     void contextHelpChanged(const QString &);
 
@@ -372,11 +433,22 @@
 
 protected:
     View(QWidget *, bool showProgress);
+
+    int m_id;
+    
     virtual void paintEvent(QPaintEvent *e);
     virtual void drawSelections(QPainter &);
     virtual bool shouldLabelSelections() const { return true; }
     virtual bool render(QPainter &paint, int x0, sv_frame_t f0, sv_frame_t f1);
     virtual void setPaintFont(QPainter &paint);
+
+    QSize scaledSize(const QSize &s, int factor) {
+        return QSize(s.width() * factor, s.height() * factor);
+    }
+    QRect scaledRect(const QRect &r, int factor) {
+        return QRect(r.x() * factor, r.y() * factor,
+                     r.width() * factor, r.height() * factor);
+    }
     
     typedef std::vector<Layer *> LayerList;
 
@@ -406,6 +478,8 @@
     void checkProgress(void *object);
     int getProgressBarWidth() const; // if visible
 
+    int effectiveDevicePixelRatio() const;
+
     sv_frame_t          m_centreFrame;
     int                 m_zoomLevel;
     bool                m_followPan;
@@ -416,7 +490,8 @@
     bool                m_lightBackground;
     bool                m_showProgress;
 
-    QPixmap            *m_cache;
+    QPixmap            *m_cache;  // I own this
+    QPixmap            *m_buffer; // I own this
     sv_frame_t          m_cacheCentreFrame;
     int                 m_cacheZoomLevel;
     bool                m_selectionCached;
--- a/view/ViewManager.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/view/ViewManager.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -15,6 +15,7 @@
 
 #include "ViewManager.h"
 #include "base/AudioPlaySource.h"
+#include "base/AudioRecordTarget.h"
 #include "base/RealTime.h"
 #include "data/model/Model.h"
 #include "widgets/CommandHistory.h"
@@ -30,6 +31,7 @@
 
 ViewManager::ViewManager() :
     m_playSource(0),
+    m_recordTarget(0),
     m_globalCentreFrame(0),
     m_globalZoom(1024),
     m_playbackFrame(0),
@@ -157,8 +159,20 @@
 sv_frame_t
 ViewManager::getPlaybackFrame() const
 {
-    if (m_playSource && m_playSource->isPlaying()) {
+    if (isRecording()) {
+        m_playbackFrame = m_recordTarget->getRecordDuration();
+#ifdef DEBUG_VIEW_MANAGER
+        cout << "ViewManager::getPlaybackFrame(recording) -> " << m_playbackFrame << endl;
+#endif
+    } else if (isPlaying()) {
 	m_playbackFrame = m_playSource->getCurrentPlayingFrame();
+#ifdef DEBUG_VIEW_MANAGER
+        cout << "ViewManager::getPlaybackFrame(playing) -> " << m_playbackFrame << endl;
+#endif
+    } else {
+#ifdef DEBUG_VIEW_MANAGER
+        cout << "ViewManager::getPlaybackFrame(not playing) -> " << m_playbackFrame << endl;
+#endif
     }
     return m_playbackFrame;
 }
@@ -166,11 +180,14 @@
 void
 ViewManager::setPlaybackFrame(sv_frame_t f)
 {
+#ifdef DEBUG_VIEW_MANAGER
+    cerr << "ViewManager::setPlaybackFrame(" << f << ")" << endl;
+#endif
     if (f < 0) f = 0;
     if (m_playbackFrame != f) {
 	m_playbackFrame = f;
 	emit playbackFrameChanged(f);
-	if (m_playSource && m_playSource->isPlaying()) {
+	if (isPlaying()) {
 	    m_playSource->play(f);
 	}
     }
@@ -489,10 +506,10 @@
 }
 
 sv_samplerate_t
-ViewManager::getOutputSampleRate() const
+ViewManager::getDeviceSampleRate() const
 {
     if (m_playSource) {
-	return m_playSource->getTargetSampleRate();
+	return m_playSource->getDeviceSampleRate();
     }
     return 0;
 }
@@ -507,6 +524,15 @@
 }
 
 void
+ViewManager::setAudioRecordTarget(AudioRecordTarget *target)
+{
+    if (!m_recordTarget) {
+	QTimer::singleShot(100, this, SLOT(checkPlayStatus()));
+    }
+    m_recordTarget = target;
+}
+
+void
 ViewManager::playStatusChanged(bool /* playing */)
 {
 #ifdef DEBUG_VIEW_MANAGER
@@ -516,14 +542,44 @@
 }
 
 void
+ViewManager::recordStatusChanged(bool /* recording */)
+{
+#ifdef DEBUG_VIEW_MANAGER
+    cerr << "ViewManager::recordStatusChanged" << endl;
+#endif
+    checkPlayStatus();
+}
+
+void
 ViewManager::checkPlayStatus()
 {
-    if (m_playSource && m_playSource->isPlaying()) {
+    if (isRecording()) {
+
+	float left = 0, right = 0;
+	if (m_recordTarget->getInputLevels(left, right)) {
+	    if (left != m_lastLeft || right != m_lastRight) {
+		emit monitoringLevelsChanged(left, right);
+		m_lastLeft = left;
+		m_lastRight = right;
+	    }
+	}
+
+	m_playbackFrame = m_recordTarget->getRecordDuration();
+
+#ifdef DEBUG_VIEW_MANAGER
+	cerr << "ViewManager::checkPlayStatus: Recording, frame " << m_playbackFrame << ", levels " << m_lastLeft << "," << m_lastRight << endl;
+#endif
+
+	emit playbackFrameChanged(m_playbackFrame);
+
+	QTimer::singleShot(500, this, SLOT(checkPlayStatus()));
+
+    } else if (isPlaying()) {
 
 	float left = 0, right = 0;
 	if (m_playSource->getOutputLevels(left, right)) {
 	    if (left != m_lastLeft || right != m_lastRight) {
-		emit outputLevelsChanged(left, right);
+		emit monitoringLevelsChanged(left, right);
 		m_lastLeft = left;
 		m_lastRight = right;
 	    }
@@ -542,13 +598,13 @@
     } else {
 
 	if (m_lastLeft != 0.0 || m_lastRight != 0.0) {
-	    emit outputLevelsChanged(0.0, 0.0);
+	    emit monitoringLevelsChanged(0.0, 0.0);
 	    m_lastLeft = 0.0;
 	    m_lastRight = 0.0;
 	}
 
 #ifdef DEBUG_VIEW_MANAGER
-	cerr << "ViewManager::checkPlayStatus: Not playing" << endl;
+	cerr << "ViewManager::checkPlayStatus: Not playing or recording" << endl;
 #endif
     }
 }
@@ -559,6 +615,12 @@
     return m_playSource && m_playSource->isPlaying();
 }
 
+bool
+ViewManager::isRecording() const
+{
+    return m_recordTarget && m_recordTarget->isRecording();
+}
+
 void
 ViewManager::viewCentreFrameChanged(sv_frame_t f, bool locked,
                                     PlaybackFollowMode mode)
@@ -597,14 +659,22 @@
     cerr << "ViewManager::seek(" << f << ")" << endl;
 #endif
 
-    if (m_playSource && m_playSource->isPlaying()) {
+    if (isRecording()) {
+        // ignore
+#ifdef DEBUG_VIEW_MANAGER
+        cerr << "ViewManager::seek: Ignoring during recording" << endl;
+#endif
+        return;
+    }
+    
+    if (isPlaying()) {
 	sv_frame_t playFrame = m_playSource->getCurrentPlayingFrame();
 	sv_frame_t diff = std::max(f, playFrame) - std::min(f, playFrame);
 	if (diff > 20000) {
 	    m_playbackFrame = f;
 	    m_playSource->play(f);
 #ifdef DEBUG_VIEW_MANAGER 
-	    cerr << "ViewManager::considerSeek: reseeking from " << playFrame << " to " << f << endl;
+	    cerr << "ViewManager::seek: reseeking from " << playFrame << " to " << f << endl;
 #endif
             emit playbackFrameChanged(f);
 	}
@@ -741,3 +811,4 @@
     if (pixels != 0 && scaled == 0) scaled = 1;
     return scaled;
 }
+
--- a/view/ViewManager.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/view/ViewManager.h	Fri Jan 13 10:29:50 2017 +0000
@@ -29,6 +29,7 @@
 #include "base/BaseTypes.h"
 
 class AudioPlaySource;
+class AudioRecordTarget;
 class Model;
 
 enum PlaybackFollowMode {
@@ -80,8 +81,10 @@
     virtual ~ViewManager();
 
     void setAudioPlaySource(AudioPlaySource *source);
+    void setAudioRecordTarget(AudioRecordTarget *target);
 
     bool isPlaying() const;
+    bool isRecording() const;
 
     sv_frame_t getGlobalCentreFrame() const; // the set method is a slot
     int getGlobalZoom() const;
@@ -172,9 +175,9 @@
     /**
      * The sample rate of the audio output device.  If the playback
      * sample rate differs from this, everything will be resampled at
-     * the output stage.
+     * the output stage (but not before).
      */
-    sv_samplerate_t getOutputSampleRate() const;
+    sv_samplerate_t getDeviceSampleRate() const;
 
     /**
      * The sample rate of the current main model.  This may in theory
@@ -254,8 +257,8 @@
     /** Emitted when the playback frame changes. */
     void playbackFrameChanged(sv_frame_t frame);
 
-    /** Emitted when the output levels change. Values in range 0.0 -> 1.0. */
-    void outputLevelsChanged(float left, float right);
+    /** Emitted when the output or record levels change. Values in range 0.0 -> 1.0. */
+    void monitoringLevelsChanged(float left, float right);
 
     /** Emitted whenever the selection has changed. */
     void selectionChanged();
@@ -305,6 +308,7 @@
     void setGlobalCentreFrame(sv_frame_t);
     void setPlaybackFrame(sv_frame_t);
     void playStatusChanged(bool playing);
+    void recordStatusChanged(bool recording);
 
 protected slots:
     void checkPlayStatus();
@@ -313,6 +317,8 @@
 
 protected:
     AudioPlaySource *m_playSource;
+    AudioRecordTarget *m_recordTarget;
+    
     sv_frame_t m_globalCentreFrame;
     int m_globalZoom;
     mutable sv_frame_t m_playbackFrame;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/view/ViewProxy.h	Fri Jan 13 10:29:50 2017 +0000
@@ -0,0 +1,147 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef VIEW_PROXY_H
+#define VIEW_PROXY_H
+
+#include "layer/LayerGeometryProvider.h"
+
+class ViewProxy : public LayerGeometryProvider
+{
+public:
+    ViewProxy(View *view, int scaleFactor) :
+	m_view(view), m_scaleFactor(scaleFactor) { }
+
+    virtual int getId() const {
+        return m_view->getId();
+    }
+    virtual sv_frame_t getStartFrame() const {
+	return m_view->getStartFrame();
+    }
+    virtual sv_frame_t getCentreFrame() const {
+	return m_view->getCentreFrame();
+    }
+    virtual sv_frame_t getEndFrame() const {
+	return m_view->getEndFrame();
+    }
+    virtual int getXForFrame(sv_frame_t frame) const {
+        //!!! not actually correct, if frame lies between view's pixels
+	return m_scaleFactor * m_view->getXForFrame(frame);
+    }
+    virtual sv_frame_t getFrameForX(int x) const {
+        sv_frame_t f0 = m_view->getFrameForX(x / m_scaleFactor);
+        if (m_scaleFactor == 1) return f0;
+        sv_frame_t f1 = m_view->getFrameForX((x / m_scaleFactor) + 1);
+        return f0 + ((f1 - f0) * (x % m_scaleFactor)) / m_scaleFactor;
+    }
+    virtual int getXForViewX(int viewx) const {
+        return viewx * m_scaleFactor;
+    }
+    virtual int getViewXForX(int x) const {
+        return x / m_scaleFactor;
+    }
+    virtual sv_frame_t getModelsStartFrame() const {
+	return m_view->getModelsStartFrame();
+    }
+    virtual sv_frame_t getModelsEndFrame() const {
+	return m_view->getModelsEndFrame();
+    }
+    virtual double getYForFrequency(double frequency,
+				    double minFreq, double maxFreq, 
+                                    bool logarithmic) const {
+	return m_scaleFactor *
+	    m_view->getYForFrequency(frequency, minFreq, maxFreq, logarithmic);
+    }
+    virtual double getFrequencyForY(double y, double minFreq, double maxFreq,
+				    bool logarithmic) const {
+        return m_view->getFrequencyForY
+            (y / m_scaleFactor, minFreq, maxFreq, logarithmic);
+    }
+    virtual int getTextLabelHeight(const Layer *layer, QPainter &paint) const {
+	return m_scaleFactor * m_view->getTextLabelHeight(layer, paint);
+    }
+    virtual bool getValueExtents(QString unit, double &min, double &max,
+                                 bool &log) const {
+	return m_view->getValueExtents(unit, min, max, log);
+    }
+    virtual int getZoomLevel() const {
+	int z = m_view->getZoomLevel();
+//	cerr << "getZoomLevel: from " << z << " to ";
+	z = z / m_scaleFactor;
+//	cerr << z << endl;
+        if (z < 1) z = 1;
+	return z;
+    }
+    virtual QRect getPaintRect() const {
+	QRect r = m_view->getPaintRect();
+	return QRect(r.x() * m_scaleFactor,
+		     r.y() * m_scaleFactor,
+		     r.width() * m_scaleFactor,
+		     r.height() * m_scaleFactor);
+    }
+    virtual QSize getPaintSize() const {
+        return getPaintRect().size();
+    }
+    virtual int getPaintWidth() const { 
+        return getPaintRect().width();
+    }
+    virtual int getPaintHeight() const { 
+        return getPaintRect().height();
+    }
+    virtual bool hasLightBackground() const {
+	return m_view->hasLightBackground();
+    }
+    virtual QColor getForeground() const {
+	return m_view->getForeground();
+    }
+    virtual QColor getBackground() const {
+	return m_view->getBackground();
+    }
+    virtual ViewManager *getViewManager() const {
+	return m_view->getViewManager();
+    }
+	
+    virtual bool shouldIlluminateLocalFeatures(const Layer *layer,
+                                               QPoint &point) const {
+        QPoint p;
+	bool should = m_view->shouldIlluminateLocalFeatures(layer, p);
+        point = QPoint(p.x() * m_scaleFactor, p.y() * m_scaleFactor);
+        return should;
+    }
+
+    virtual bool shouldShowFeatureLabels() const {
+	return m_view->shouldShowFeatureLabels();
+    }
+
+    virtual void drawMeasurementRect(QPainter &p, const Layer *layer,
+                                     QRect rect, bool focus) const {
+	m_view->drawMeasurementRect(p, layer, rect, focus);
+    }
+
+    virtual void updatePaintRect(QRect r) {
+        m_view->update(r.x() / m_scaleFactor,
+                       r.y() / m_scaleFactor,
+                       r.width() / m_scaleFactor,
+                       r.height() / m_scaleFactor);
+    }
+    
+    virtual View *getView() { return m_view; }
+    virtual const View *getView() const { return m_view; }
+
+private:
+    View *m_view;
+    int m_scaleFactor;
+};
+
+#endif
--- a/widgets/AudioDial.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/widgets/AudioDial.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -409,13 +409,27 @@
 
     if (m_showTooltip) {
         QString name = objectName();
-        QString unit = "";
+        QString label;
+        if (m_rangeMapper) {
+            label = m_rangeMapper->getLabel(value);
+        }
         QString text;
-        if (m_rangeMapper) unit = m_rangeMapper->getUnit();
-        if (name != "") {
-            text = tr("%1: %2%3").arg(name).arg(m_mappedValue).arg(unit);
+        if (label != "") {
+            if (name != "") {
+                text = tr("%1: %2").arg(name).arg(label);
+            } else {
+                text = label;
+            }
         } else {
-            text = tr("%2%3").arg(m_mappedValue).arg(unit);
+            QString unit = "";
+            if (m_rangeMapper) {
+                unit = m_rangeMapper->getUnit();
+            }
+            if (name != "") {
+                text = tr("%1: %2%3").arg(name).arg(m_mappedValue).arg(unit);
+            } else {
+                text = tr("%2%3").arg(m_mappedValue).arg(unit);
+            }
         }
         setToolTip(text);
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/ColourComboBox.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -0,0 +1,101 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2007-2016 QMUL.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "ColourComboBox.h"
+
+#include "ColourNameDialog.h"
+
+#include "layer/ColourDatabase.h"
+
+#include "base/Debug.h"
+
+#include <QFontMetrics>
+#include <QColorDialog>
+
+#include <iostream>
+
+using namespace std;
+
+ColourComboBox::ColourComboBox(bool withAddNewColourEntry, QWidget *parent) :
+    NotifyingComboBox(parent),
+    m_withAddNewColourEntry(withAddNewColourEntry)
+{
+    setEditable(false);
+    rebuild();
+
+    connect(this, SIGNAL(activated(int)), this, SLOT(comboActivated(int)));
+    connect(ColourDatabase::getInstance(), SIGNAL(colourDatabaseChanged()),
+            this, SLOT(rebuild()));
+
+    if (count() < 20 && count() > maxVisibleItems()) {
+	setMaxVisibleItems(count());
+    }
+}
+
+void
+ColourComboBox::comboActivated(int index)
+{
+    if (!m_withAddNewColourEntry ||
+	index < int(ColourDatabase::getInstance()->getColourCount())) {
+	emit colourChanged(index);
+	return;
+    }
+    
+    QColor newColour = QColorDialog::getColor();
+    if (!newColour.isValid()) return;
+
+    ColourNameDialog dialog(tr("Name New Colour"),
+                            tr("Enter a name for the new colour:"),
+                            newColour, newColour.name(), this);
+    dialog.showDarkBackgroundCheckbox(tr("Prefer black background for this colour"));
+    if (dialog.exec() == QDialog::Accepted) {
+        //!!! command
+        ColourDatabase *db = ColourDatabase::getInstance();
+        int index = db->addColour(newColour, dialog.getColourName());
+        db->setUseDarkBackground(index, dialog.isDarkBackgroundChecked());
+	// addColour will have called back on rebuild(), and the new
+	// colour will be at the index previously occupied by Add New
+	// Colour, which is our current index
+	emit colourChanged(currentIndex());
+    }
+}
+
+void
+ColourComboBox::rebuild()
+{
+    blockSignals(true);
+
+    int ix = currentIndex();
+    
+    clear();
+
+    int size = (QFontMetrics(QFont()).height() * 2) / 3;
+    if (size < 12) size = 12;
+    
+    ColourDatabase *db = ColourDatabase::getInstance();
+    for (int i = 0; i < db->getColourCount(); ++i) {
+	QString name = db->getColourName(i);
+	addItem(db->getExamplePixmap(i, QSize(size, size)), name);
+    }
+
+    if (m_withAddNewColourEntry) {
+	addItem(tr("Add New Colour..."));
+    }
+
+    setCurrentIndex(ix);
+    
+    blockSignals(false);
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/ColourComboBox.h	Fri Jan 13 10:29:50 2017 +0000
@@ -0,0 +1,44 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2007-2016 QMUL.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef SV_COLOUR_COMBO_BOX_H
+#define SV_COLOUR_COMBO_BOX_H
+
+#include "NotifyingComboBox.h"
+
+/**
+ * Colour-picker combo box with swatches, optionally including "Add
+ * New Colour..." entry to invoke a QColorDialog/ColourNameDialog
+ */
+class ColourComboBox : public NotifyingComboBox
+{
+    Q_OBJECT
+
+public:
+    ColourComboBox(bool withAddNewColourEntry, QWidget *parent = 0);
+
+signals:
+    void colourChanged(int colourIndex);
+
+private slots:
+    void rebuild();
+    void comboActivated(int);
+    
+private:
+    bool m_withAddNewColourEntry;
+};
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/ColourMapComboBox.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -0,0 +1,74 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2007-2016 QMUL.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "ColourMapComboBox.h"
+
+#include "layer/ColourMapper.h"
+
+#include "base/Debug.h"
+
+#include <QFontMetrics>
+
+#include <iostream>
+
+using namespace std;
+
+ColourMapComboBox::ColourMapComboBox(bool includeSwatches, QWidget *parent) :
+    NotifyingComboBox(parent),
+    m_includeSwatches(includeSwatches)
+{
+    setEditable(false);
+    rebuild();
+
+    connect(this, SIGNAL(activated(int)), this, SLOT(comboActivated(int)));
+
+    if (count() < 20 && count() > maxVisibleItems()) {
+	setMaxVisibleItems(count());
+    }
+}
+
+void
+ColourMapComboBox::comboActivated(int index)
+{
+    emit colourMapChanged(index);
+}
+
+void
+ColourMapComboBox::rebuild()
+{
+    blockSignals(true);
+
+    int ix = currentIndex();
+    
+    clear();
+
+    int size = (QFontMetrics(QFont()).height() * 2) / 3;
+    if (size < 12) size = 12;
+
+    for (int i = 0; i < ColourMapper::getColourMapCount(); ++i) {
+        QString name = ColourMapper::getColourMapName(i);
+        if (m_includeSwatches) {
+            ColourMapper mapper(i, 0.0, 1.0);
+            addItem(mapper.getExamplePixmap(QSize(size * 2, size)), name);
+        } else {
+            addItem(name);
+        }
+    }
+
+    setCurrentIndex(ix);
+    
+    blockSignals(false);
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/ColourMapComboBox.h	Fri Jan 13 10:29:50 2017 +0000
@@ -0,0 +1,43 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2007-2016 QMUL.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef SV_COLOURMAP_COMBO_BOX_H
+#define SV_COLOURMAP_COMBO_BOX_H
+
+#include "NotifyingComboBox.h"
+
+/**
+ * Colour map picker combo box with optional swatches
+ */
+class ColourMapComboBox : public NotifyingComboBox
+{
+    Q_OBJECT
+
+public:
+    ColourMapComboBox(bool includeSwatches, QWidget *parent = 0);
+
+signals:
+    void colourMapChanged(int index);
+
+private slots:
+    void rebuild();
+    void comboActivated(int);
+
+private:
+    bool m_includeSwatches;
+};
+
+#endif
+
--- a/widgets/CommandHistory.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/widgets/CommandHistory.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -26,6 +26,8 @@
 
 #include "base/Command.h"
 
+#include "IconLoader.h"
+
 #include <QRegExp>
 #include <QMenu>
 #include <QToolBar>
@@ -53,12 +55,16 @@
     m_bundleTimer(0),
     m_bundleTimeout(3000)
 {
-    m_undoAction = new QAction(QIcon(":/icons/undo.png"), tr("&Undo"), this);
+    IconLoader loader;
+    QIcon undoIcon(loader.load("undo"));
+    QIcon redoIcon(loader.load("redo"));
+    
+    m_undoAction = new QAction(undoIcon, ("&Undo"), this);
     m_undoAction->setShortcut(tr("Ctrl+Z"));
     m_undoAction->setStatusTip(tr("Undo the last editing operation"));
     connect(m_undoAction, SIGNAL(triggered()), this, SLOT(undo()));
     
-    m_undoMenuAction = new QAction(QIcon(":/icons/undo.png"), tr("&Undo"), this);
+    m_undoMenuAction = new QAction(undoIcon, tr("&Undo"), this);
     connect(m_undoMenuAction, SIGNAL(triggered()), this, SLOT(undo()));
     
     m_undoMenu = new QMenu(tr("&Undo"));
@@ -66,12 +72,12 @@
     connect(m_undoMenu, SIGNAL(triggered(QAction *)),
 	    this, SLOT(undoActivated(QAction*)));
 
-    m_redoAction = new QAction(QIcon(":/icons/redo.png"), tr("Re&do"), this);
+    m_redoAction = new QAction(redoIcon, tr("Re&do"), this);
     m_redoAction->setShortcut(tr("Ctrl+Shift+Z"));
     m_redoAction->setStatusTip(tr("Redo the last operation that was undone"));
     connect(m_redoAction, SIGNAL(triggered()), this, SLOT(redo()));
     
-    m_redoMenuAction = new QAction(QIcon(":/icons/redo.png"), tr("Re&do"), this);
+    m_redoMenuAction = new QAction(redoIcon, tr("Re&do"), this);
     connect(m_redoMenuAction, SIGNAL(triggered()), this, SLOT(redo()));
 
     m_redoMenu = new QMenu(tr("Re&do"));
--- a/widgets/IconLoader.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/widgets/IconLoader.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -19,8 +19,17 @@
 #include <QApplication>
 #include <QPainter>
 #include <QPalette>
+#include <QFile>
+#include <QSvgRenderer>
 
-static const char *autoInvertExceptions[] = {
+#include <vector>
+#include <set>
+
+#include "base/Debug.h"
+
+using namespace std;
+
+static set<QString> autoInvertExceptions {
     // These are the icons that look OK in their default colours, even
     // in a colour scheme with a black background.  (They may also be
     // icons that would look worse if we tried to auto-invert them.)
@@ -29,16 +38,13 @@
     // supply inverted versions -- the loader will load xx_inverse.png
     // in preference to xx.png if a dark background is found.)
     "fileclose",
-    "filenew-22",
     "filenew",
-    "fileopen-22",
     "fileopen",
     "fileopenaudio",
     "fileopensession",
-    "filesave-22",
     "filesave",
-    "filesaveas-22",
     "filesaveas",
+    "filesaveas-sv",
     "help",
     "editcut",
     "editcopy",
@@ -51,42 +57,121 @@
     "zoom"
 };
 
+static vector<int> sizes { 0, 16, 22, 24, 32, 48, 64, 128 };
+
 QIcon
 IconLoader::load(QString name)
 {
-    QPixmap pmap(loadPixmap(name));
-    if (pmap.isNull()) return QIcon();
-    else return QIcon(pmap);
+    QIcon icon;
+    for (int sz: sizes) {
+        QPixmap pmap(loadPixmap(name, sz));
+        if (!pmap.isNull()) icon.addPixmap(pmap);
+    }
+    return icon;
+}
+
+bool
+IconLoader::shouldInvert() const
+{
+    QColor bg = QApplication::palette().window().color();
+    bool darkBackground = (bg.red() + bg.green() + bg.blue() <= 384);
+    return darkBackground;
+}
+
+bool
+IconLoader::shouldAutoInvert(QString name) const
+{
+    if (shouldInvert()) {
+        return (autoInvertExceptions.find(name) == autoInvertExceptions.end());
+    } else {
+        return false;
+    }
 }
 
 QPixmap
-IconLoader::loadPixmap(QString name)
+IconLoader::loadPixmap(QString name, int size)
 {
-    QColor bg = QApplication::palette().window().color();
-    if (bg.red() + bg.green() + bg.blue() > 384) { // light background
-        QPixmap pmap(QString(":icons/%1").arg(name));
-        if (pmap.isNull()) {
-            pmap = QPixmap(QString(":icons/%1.png").arg(name));
-        }
-        return pmap;
+    bool invert = shouldInvert();
+
+    QString scalableName, nonScalableName;
+    QPixmap pmap;
+
+    nonScalableName = makeNonScalableFilename(name, size, invert);
+    pmap = QPixmap(nonScalableName);
+    if (!pmap.isNull()) return pmap;
+
+    if (size > 0) {
+        scalableName = makeScalableFilename(name, invert);
+        pmap = loadScalable(scalableName, size);
+        if (!pmap.isNull()) return pmap;
     }
 
-    QPixmap pmap(QString(":icons/%1").arg(name));
-    if (pmap.isNull()) {
-        pmap = QPixmap(QString(":icons/%1_inverse.png").arg(name));
-        if (pmap.isNull()) {
-            pmap = QPixmap(QString(":icons/%1.png").arg(name));
-        }
-    }
-    if (pmap.isNull()) return pmap;
+    if (invert && shouldAutoInvert(name)) {
 
-    for (int i = 0; i < int(sizeof(autoInvertExceptions)/
-                            sizeof(autoInvertExceptions[0])); ++i) {
-        if (autoInvertExceptions[i] == name) {
-            return pmap;
+        nonScalableName = makeNonScalableFilename(name, size, false);
+        pmap = QPixmap(nonScalableName);
+        if (!pmap.isNull()) return invertPixmap(pmap);
+
+        if (size > 0) {
+            scalableName = makeScalableFilename(name, false);
+            pmap = loadScalable(scalableName, size);
+            if (!pmap.isNull()) return invertPixmap(pmap);
         }
     }
 
+    return QPixmap();
+}
+
+QPixmap
+IconLoader::loadScalable(QString name, int size)
+{
+    if (!QFile(name).exists()) {
+//        cerr << "loadScalable: no such file as: \"" << name << "\"" << endl;
+        return QPixmap();
+    }
+    QPixmap pmap(size, size);
+    pmap.fill(Qt::transparent);
+    QSvgRenderer renderer(name);
+    QPainter painter;
+    painter.begin(&pmap);
+//    cerr << "calling renderer for " << name << " at size " << size << "..." << endl;
+    renderer.render(&painter);
+//    cerr << "renderer completed" << endl;
+    painter.end();
+    return pmap;
+}
+
+QString
+IconLoader::makeNonScalableFilename(QString name, int size, bool invert)
+{
+    if (invert) {
+        if (size == 0) {
+            return QString(":icons/%1_inverse.png").arg(name);
+        } else {
+            return QString(":icons/%1-%2_inverse.png").arg(name).arg(size);
+        }
+    } else {
+        if (size == 0) {
+            return QString(":icons/%1.png").arg(name);
+        } else {
+            return QString(":icons/%1-%2.png").arg(name).arg(size);
+        }
+    }
+}
+
+QString
+IconLoader::makeScalableFilename(QString name, bool invert)
+{
+    if (invert) {
+        return QString(":icons/scalable/%1_inverse.svg").arg(name);
+    } else {
+        return QString(":icons/scalable/%1.svg").arg(name);
+    }
+}
+
+QPixmap
+IconLoader::invertPixmap(QPixmap pmap)
+{
     // No suitable inverted icon found for black background; try to
     // auto-invert the default one
 
--- a/widgets/IconLoader.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/widgets/IconLoader.h	Fri Jan 13 10:29:50 2017 +0000
@@ -25,7 +25,15 @@
     virtual ~IconLoader() { }
 
     QIcon load(QString name);
-    QPixmap loadPixmap(QString name);
+
+private:
+    bool shouldInvert() const;
+    bool shouldAutoInvert(QString) const;
+    QPixmap loadPixmap(QString, int);
+    QPixmap loadScalable(QString, int);
+    QPixmap invertPixmap(QPixmap);
+    QString makeScalableFilename(QString, bool);
+    QString makeNonScalableFilename(QString, int, bool);
 };
 
 #endif
--- a/widgets/InteractiveFileFinder.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/widgets/InteractiveFileFinder.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -37,7 +37,6 @@
     m_lastLocatedLocation(""),
     m_parent(0)
 {
-    SVDEBUG << "Registering interactive file finder" << endl;
     FileFinder::registerFileFinder(this);
 }
 
@@ -135,6 +134,11 @@
         }
         break;
 
+    case SVGFile:
+        settingsKeyStub = "svg";
+        filter = tr("Scalable Vector Graphics files (*.svg)\nAll files (*.*)");
+        break;
+
     case CSVFile:
         settingsKeyStub = "layer";
         filter = tr("Comma-separated data files (*.csv)\nSpace-separated .lab files (*.lab)\nText files (*.txt)\nAll files (*.*)");
@@ -283,6 +287,12 @@
         filter = tr("Portable Network Graphics files (*.png)\nAll files (*.*)");
         break;
 
+    case SVGFile:
+        settingsKeyStub = "savesvg";
+        title = tr("Select a file to export to");
+        filter = tr("Scalable Vector Graphics files (*.svg)\nAll files (*.*)");
+        break;
+
     case CSVFile:
         settingsKeyStub = "savelayer";
         title = tr("Select a file to export to");
@@ -331,6 +341,8 @@
         defaultSuffix = "wav";
     } else if (type == ImageFile) {
         defaultSuffix = "png";
+    } else if (type == SVGFile) {
+        defaultSuffix = "svg";
     } else if (type == CSVFile) {
         defaultSuffix = "csv";
     }
@@ -451,6 +463,10 @@
         settingsKeyStub = "image";
         break;
 
+    case SVGFile:
+        settingsKeyStub = "svg";
+        break;
+
     case CSVFile:
         settingsKeyStub = "layer";
         break;
--- a/widgets/LEDButton.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/widgets/LEDButton.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -23,6 +23,7 @@
 
 
 #include "LEDButton.h"
+#include "WidgetScale.h"
 
 #include <QPainter>
 #include <QImage>
@@ -38,8 +39,6 @@
 
     int dark_factor;
     QColor offcolor;
-    QPixmap *off_map;
-    QPixmap *on_map;
 };
 
 
@@ -51,8 +50,6 @@
     d = new LEDButton::LEDButtonPrivate;
     d->dark_factor = 300;
     d->offcolor = col.dark(300);
-    d->off_map = 0;
-    d->on_map = 0;
     
     setColor(col);
 }
@@ -65,8 +62,6 @@
     d = new LEDButton::LEDButtonPrivate;
     d->dark_factor = 300;
     d->offcolor = col.dark(300);
-    d->off_map = 0;
-    d->on_map = 0;
 
     setColor(col);
 }
@@ -78,16 +73,12 @@
     d = new LEDButton::LEDButtonPrivate;
     d->dark_factor = 300;
     d->offcolor = col.dark(300);
-    d->off_map = 0;
-    d->on_map = 0;
 
     setColor(col);
 }
 
 LEDButton::~LEDButton()
 {
-    delete d->off_map;
-    delete d->on_map;
     delete d;
 }
 
@@ -135,40 +126,7 @@
     if (width < 0) 
 	width = 0;
 
-    QPixmap *tmpMap = 0;
-
-    if (led_state) {
-	if (d->on_map) {
-            if (d->on_map->size() == size()) {
-                paint.begin(this);
-                paint.drawPixmap(0, 0, *d->on_map);
-                paint.end();
-                return;
-            } else {
-                delete d->on_map;
-                d->on_map = 0;
-            }
-	}
-    } else {
-	if (d->off_map) {
-            if (d->off_map->size() == size()) {
-                paint.begin(this);
-                paint.drawPixmap(0, 0, *d->off_map);
-                paint.end();
-                return;
-            } else {
-                delete d->off_map;
-                d->off_map = 0;
-            }
-	}
-    }
-
-    int scale = 1;
-    width *= scale;
-
-    tmpMap = new QPixmap(width, width);
-    tmpMap->fill(palette().background().color());
-    paint.begin(tmpMap);
+    paint.begin(this);
 
     paint.setRenderHint(QPainter::Antialiasing, true);
 
@@ -182,14 +140,14 @@
     paint.setBrush(brush);
 
     // Draws a "flat" LED with the given color:
-    paint.drawEllipse( scale, scale, width - scale*2, width - scale*2 );
+    paint.drawEllipse( 1, 1, width - 2, width - 2 );
 
     // Draw the bright light spot of the LED now, using modified "old"
     // painter routine taken from KDEUI´s LEDButton widget:
 
     // Setting the new width of the pen is essential to avoid "pixelized"
     // shadow like it can be observed with the old LED code
-    pen.setWidth( 2 * scale );
+    pen.setWidth( 2 );
 
     // shrink the light on the LED to a size about 2/3 of the complete LED
     int pos = width/5 + 1;
@@ -217,12 +175,13 @@
 	pos++; light_width--;
     }
 
+    paint.drawPoint(pos, pos);
+
     // Drawing of bright spot finished, now draw a thin border
     // around the LED which resembles a shadow with light coming
     // from the upper left.
 
-//    pen.setWidth( 2 * scale + 1 ); // ### shouldn't this value be smaller for smaller LEDs?
-    pen.setWidth(2 * scale);
+    pen.setWidth(2);
     brush.setStyle(Qt::NoBrush);
     paint.setBrush(brush); // This avoids filling of the ellipse
 
@@ -235,36 +194,13 @@
     for (int arc = 120; arc < 2880; arc += 240) {
 	pen.setColor(color);
 	paint.setPen(pen);
-	int w = width - pen.width()/2 - scale + 1;
+	int w = width - pen.width()/2;
 	paint.drawArc(pen.width()/2 + 1, pen.width()/2 + 1, w - 2, w - 2, angle + arc, 240);
 	paint.drawArc(pen.width()/2 + 1, pen.width()/2 + 1, w - 2, w - 2, angle - arc, 240);
 	color = color.dark(110); //FIXME: this should somehow use the contrast value
     }	// end for ( angle = 720; angle < 6480; angle += 160 )
 
     paint.end();
-    //
-    // painting done
-
-    QPixmap *&dest = led_state ? d->on_map : d->off_map;
-
-    if (scale > 1) {
-
-	QImage i = tmpMap->toImage();
-	width /= scale;
-	delete tmpMap;
-	dest = new QPixmap(QPixmap::fromImage
-			   (i.scaled(width, width, 
-				     Qt::KeepAspectRatio,
-				     Qt::SmoothTransformation)));
-
-    } else {
-
-	dest = tmpMap;
-    }
-
-    paint.begin(this);
-    paint.drawPixmap(0, 0, *dest);
-    paint.end();
 }
 
 bool
@@ -303,10 +239,6 @@
     if(led_color!=col) {
 	led_color = col;
 	d->offcolor = col.dark(d->dark_factor);
-	delete d->on_map;
-	d->on_map = 0;
-	delete d->off_map;
-	d->off_map = 0;
 	update();
     }
 }
@@ -348,12 +280,12 @@
 QSize
 LEDButton::sizeHint() const
 {
-    return QSize(17, 17);
+    return WidgetScale::scaleQSize(QSize(17, 17));
 }
 
 QSize
 LEDButton::minimumSizeHint() const
 {
-    return QSize(17, 17);
+    return WidgetScale::scaleQSize(QSize(17, 17));
 }
 
--- a/widgets/LEDButton.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/widgets/LEDButton.h	Fri Jan 13 10:29:50 2017 +0000
@@ -25,8 +25,8 @@
     sunken variant.  This version also implements a simple button API.
 */
 
-#ifndef _LED_BUTTON_H_
-#define _LED_BUTTON_H_
+#ifndef SV_LED_BUTTON_H
+#define SV_LED_BUTTON_H
 
 #include <QWidget>
 #include "base/Debug.h"
--- a/widgets/LevelPanToolButton.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/widgets/LevelPanToolButton.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -49,6 +49,7 @@
 
     setPopupMode(InstantPopup);
     setMenu(menu);
+    setToolTip(tr("Click to adjust level and pan"));
 
     setImageSize(m_pixels);
     setBigImageSize(m_pixelsBig);
@@ -58,6 +59,12 @@
 {
 }
 
+void
+LevelPanToolButton::wheelEvent(QWheelEvent *e)
+{
+    m_lpw->wheelEvent(e);
+}
+
 float
 LevelPanToolButton::getLevel() const
 {
@@ -110,6 +117,13 @@
 }
 
 void
+LevelPanToolButton::setMonitoringLevels(float left, float right)
+{
+    m_lpw->setMonitoringLevels(left, right);
+    update();
+}
+
+void
 LevelPanToolButton::setIncludeMute(bool include)
 {
     m_lpw->setIncludeMute(include);
@@ -170,4 +184,18 @@
     m_lpw->renderTo(this, QRectF(margin, margin, m_pixels, m_pixels), false);
 }
 
+void
+LevelPanToolButton::enterEvent(QEvent *e)
+{
+    QToolButton::enterEvent(e);
+    emit mouseEntered();
+}
 
+void
+LevelPanToolButton::leaveEvent(QEvent *e)
+{
+    QToolButton::enterEvent(e);
+    emit mouseLeft();
+}
+
+
--- a/widgets/LevelPanToolButton.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/widgets/LevelPanToolButton.h	Fri Jan 13 10:29:50 2017 +0000
@@ -47,6 +47,9 @@
     /// Set pan in the range [-1,1] -- will be rounded
     void setPan(float);
 
+    /// Set left and right peak monitoring levels in the range [0,1]
+    void setMonitoringLevels(float, float);
+    
     /// Specify whether the level range should include muting or not
     void setIncludeMute(bool);
 
@@ -56,12 +59,18 @@
     void levelChanged(float);
     void panChanged(float);
 
+    void mouseEntered();
+    void mouseLeft();
+
 private slots:
     void selfLevelChanged(float);
     void selfClicked();
     
 protected:
-    void paintEvent(QPaintEvent *);
+    virtual void paintEvent(QPaintEvent *);
+    virtual void enterEvent(QEvent *);
+    virtual void leaveEvent(QEvent *);
+    virtual void wheelEvent(QWheelEvent *e);
     
     LevelPanWidget *m_lpw;
     int m_pixels;
--- a/widgets/LevelPanWidget.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/widgets/LevelPanWidget.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -21,6 +21,8 @@
 #include "layer/ColourMapper.h"
 #include "base/AudioLevel.h"
 
+#include "WidgetScale.h"
+
 #include <iostream>
 #include <cmath>
 #include <cassert>
@@ -35,9 +37,14 @@
     QWidget(parent),
     m_level(maxLevel),
     m_pan(0),
+    m_monitorLeft(-1),
+    m_monitorRight(-1),
     m_editable(true),
     m_includeMute(true)
 {
+    setToolTip(tr("Drag vertically to adjust level, horizontally to adjust pan"));
+    setLevel(1.0);
+    setPan(0.0);
 }
 
 LevelPanWidget::~LevelPanWidget()
@@ -47,22 +54,7 @@
 QSize
 LevelPanWidget::sizeHint() const
 {
-    static double ratio = 0.0;
-    if (ratio == 0.0) {
-        double baseEm;
-#ifdef Q_OS_MAC
-        baseEm = 17.0;
-#else
-        baseEm = 15.0;
-#endif
-        double em = QFontMetrics(QFont()).height();
-        ratio = em / baseEm;
-    }
-
-    int pixels = 40;
-    int scaled = int(pixels * ratio + 0.5);
-    if (pixels != 0 && scaled == 0) scaled = 1;
-    return QSize(scaled, scaled);
+    return WidgetScale::scaleQSize(QSize(40, 40));
 }
 
 static int
@@ -91,18 +83,36 @@
     else return -20.;
 }
 
+int
+LevelPanWidget::audioLevelToLevel(float audioLevel, bool withMute)
+{
+    int level;
+    if (withMute) {
+        level = AudioLevel::multiplier_to_fader
+            (audioLevel, maxLevel, AudioLevel::ShortFader);
+    } else {
+        level = db_to_level(AudioLevel::multiplier_to_dB(audioLevel));
+    }
+    if (level < 0) level = 0;
+    if (level > maxLevel) level = maxLevel;
+    return level;
+}
+
+float
+LevelPanWidget::levelToAudioLevel(int level, bool withMute)
+{
+    if (withMute) {
+        return float(AudioLevel::fader_to_multiplier
+                     (level, maxLevel, AudioLevel::ShortFader));
+    } else {
+        return float(AudioLevel::dB_to_multiplier(level_to_db(level)));
+    }
+}
+
 void
 LevelPanWidget::setLevel(float flevel)
 {
-    int level;
-    if (m_includeMute) {
-        level = AudioLevel::multiplier_to_fader
-            (flevel, maxLevel, AudioLevel::ShortFader);
-    } else {
-        level = db_to_level(AudioLevel::multiplier_to_dB(flevel));
-    }
-    if (level < 0) level = 0;
-    if (level > maxLevel) level = maxLevel;
+    int level = audioLevelToLevel(flevel, m_includeMute);
     if (level != m_level) {
 	m_level = level;
 	float convertsTo = getLevel();
@@ -116,20 +126,46 @@
 float
 LevelPanWidget::getLevel() const
 {
-    if (m_includeMute) {
-        return float(AudioLevel::fader_to_multiplier
-                     (m_level, maxLevel, AudioLevel::ShortFader));
-    } else {
-        return float(AudioLevel::dB_to_multiplier(level_to_db(m_level)));
+    float flevel = levelToAudioLevel(m_level, m_includeMute);
+    return flevel;
+}
+
+int
+LevelPanWidget::audioPanToPan(float audioPan)
+{
+    int pan = int(round(audioPan * maxPan));
+    if (pan < -maxPan) pan = -maxPan;
+    if (pan > maxPan) pan = maxPan;
+    return pan;
+}
+
+float
+LevelPanWidget::panToAudioPan(int pan)
+{
+    return float(pan) / float(maxPan);
+}
+
+void
+LevelPanWidget::setPan(float fpan)
+{
+    int pan = audioPanToPan(fpan);
+    if (pan != m_pan) {
+        m_pan = pan;
+        update();
     }
 }
 
+float
+LevelPanWidget::getPan() const
+{
+    return panToAudioPan(m_pan);
+}
+
 void
-LevelPanWidget::setPan(float pan)
+LevelPanWidget::setMonitoringLevels(float left, float right)
 {
-    m_pan = int(round(pan * maxPan));
-    if (m_pan < -maxPan) m_pan = -maxPan;
-    if (m_pan > maxPan) m_pan = maxPan;
+    m_monitorLeft = left;
+    m_monitorRight = right;
     update();
 }
 
@@ -160,23 +196,15 @@
     update();
 }
 
-float
-LevelPanWidget::getPan() const
-{
-    return float(m_pan) / float(maxPan);
-}
-
 void
 LevelPanWidget::emitLevelChanged()
 {
-    cerr << "emitting levelChanged(" << getLevel() << ")" << endl;
     emit levelChanged(getLevel());
 }
 
 void
 LevelPanWidget::emitPanChanged()
 {
-    cerr << "emitting panChanged(" << getPan() << ")" << endl;
     emit panChanged(getPan());
 }
 
@@ -338,11 +366,33 @@
     pen.setWidthF(cellLightSize(rect).width() + thin);
     pen.setCapStyle(Qt::RoundCap);
     paint.setPen(pen);
+    paint.setBrush(Qt::NoBrush);
 
     for (int pan = -maxPan; pan <= maxPan; ++pan) {
 	paint.drawLine(cellCentre(rect, 0, pan), cellCentre(rect, maxLevel, pan));
     }
 
+    if (m_monitorLeft > 0.f || m_monitorRight > 0.f) {
+        paint.setPen(Qt::NoPen);
+        for (int pan = -maxPan; pan <= maxPan; ++pan) {
+            float audioPan = panToAudioPan(pan);
+            float audioLevel;
+            if (audioPan < 0.f) {
+                audioLevel = m_monitorLeft + m_monitorRight * (1.f + audioPan);
+            } else {
+                audioLevel = m_monitorRight + m_monitorLeft * (1.f - audioPan);
+            }
+            int levelHere = audioLevelToLevel(audioLevel, false);
+            for (int level = 0; level <= levelHere; ++level) {
+                paint.setBrush(level_to_colour(level));
+                QRectF clr = cellLightRect(rect, level, pan);
+                paint.drawEllipse(clr);
+            }
+        }
+        paint.setPen(pen);
+        paint.setBrush(Qt::NoBrush);
+    }
+    
     if (isEnabled()) {
 	pen.setColor(Qt::black);
     } else {
@@ -384,5 +434,17 @@
     renderTo(this, rect(), m_editable);
 }
 
+void
+LevelPanWidget::enterEvent(QEvent *e)
+{
+    QWidget::enterEvent(e);
+    emit mouseEntered();
+}
 
+void
+LevelPanWidget::leaveEvent(QEvent *e)
+{
+    QWidget::enterEvent(e);
+    emit mouseLeft();
+}
 
--- a/widgets/LevelPanWidget.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/widgets/LevelPanWidget.h	Fri Jan 13 10:29:50 2017 +0000
@@ -29,7 +29,8 @@
     LevelPanWidget(QWidget *parent = 0);
     ~LevelPanWidget();
     
-    /// Return level as a gain value in the range [0,1]
+    /// Return level as a gain value. The basic level range is [0,1] but the
+    /// gain scale may go up to 4.0
     float getLevel() const; 
     
     /// Return pan as a value in the range [-1,1]
@@ -47,37 +48,56 @@
     QSize sizeHint() const;
                                                
 public slots:
-    /// Set level in the range [0,1] -- will be rounded
+    /// Set level. The basic level range is [0,1] but the scale may go
+    /// higher. The value will be rounded.
     void setLevel(float);
 
-    /// Set pan in the range [-1,1] -- will be rounded
+    /// Set pan in the range [-1,1]. The value will be rounded
     void setPan(float);
 
+    /// Set left and right peak monitoring levels in the range [0,1]
+    void setMonitoringLevels(float, float);
+    
     /// Specify whether the widget is editable or read-only (default editable)
     void setEditable(bool);
 
     /// Specify whether the level range should include muting or not
     void setIncludeMute(bool);
     
+    // public so it can be called from LevelPanToolButton (ew)
+    virtual void wheelEvent(QWheelEvent *ev);
+    
 signals:
-    void levelChanged(float);
-    void panChanged(float);
+    void levelChanged(float); // range [0,1]
+    void panChanged(float); // range [-1,1]
 
+    void mouseEntered();
+    void mouseLeft();
+    
 protected:
     virtual void mousePressEvent(QMouseEvent *ev);
     virtual void mouseMoveEvent(QMouseEvent *ev);
     virtual void mouseReleaseEvent(QMouseEvent *ev);
-    virtual void wheelEvent(QWheelEvent *ev);
     virtual void paintEvent(QPaintEvent *ev);
+    virtual void enterEvent(QEvent *);
+    virtual void leaveEvent(QEvent *);
 
     void emitLevelChanged();
     void emitPanChanged();
     
     int m_level;
     int m_pan;
+    float m_monitorLeft;
+    float m_monitorRight;
     bool m_editable;
     bool m_includeMute;
 
+    static int audioLevelToLevel(float audioLevel, bool withMute);
+    static float levelToAudioLevel(int level, bool withMute);
+
+    static int audioPanToPan(float audioPan);
+    static float panToAudioPan(int pan);
+    
     QSizeF cellSize(QRectF) const;
     QPointF cellCentre(QRectF, int level, int pan) const;
     QSizeF cellLightSize(QRectF) const;
--- a/widgets/ModelDataTableDialog.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/widgets/ModelDataTableDialog.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -61,7 +61,7 @@
 
     toolbar = addToolBar(tr("Edit Toolbar"));
 
-    action = new QAction(il.load("datainsert"), tr("Insert New Item"), this);
+    action = new QAction(il.load("draw"), tr("Insert New Item"), this);
     action->setShortcut(tr("Insert"));
     action->setStatusTip(tr("Insert a new item"));
     connect(action, SIGNAL(triggered()), this, SLOT(insertRow()));
--- a/widgets/NotifyingComboBox.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/widgets/NotifyingComboBox.h	Fri Jan 13 10:29:50 2017 +0000
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _NOTIFYING_COMBO_BOX_H_
-#define _NOTIFYING_COMBO_BOX_H_
+#ifndef SV_NOTIFYING_COMBO_BOX_H
+#define SV_NOTIFYING_COMBO_BOX_H
 
 #include <QComboBox>
 
@@ -26,8 +26,8 @@
 class NotifyingComboBox : public QComboBox
 {
     Q_OBJECT
+
 public:
-
     NotifyingComboBox(QWidget *parent = 0) :
         QComboBox(parent) { }
 
--- a/widgets/NotifyingPushButton.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/widgets/NotifyingPushButton.h	Fri Jan 13 10:29:50 2017 +0000
@@ -13,21 +13,22 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _NOTIFYING_PUSH_BUTTON_H_
-#define _NOTIFYING_PUSH_BUTTON_H_
+#ifndef SV_NOTIFYING_PUSH_BUTTON_H
+#define SV_NOTIFYING_PUSH_BUTTON_H
 
 #include <QPushButton>
 
 /**
- * Very trivial enhancement to QPushButton to make it emit signals when
- * the mouse enters and leaves (for context help).
+ * Very trivial enhancement to QPushButton to make it emit signals
+ * when the mouse enters and leaves (for context help). See also
+ * NotifyingToolButton
  */
 
 class NotifyingPushButton : public QPushButton
 {
     Q_OBJECT
+
 public:
-
     NotifyingPushButton(QWidget *parent = 0) :
         QPushButton(parent) { }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/NotifyingToolButton.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -0,0 +1,35 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2007 QMUL.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "NotifyingToolButton.h"
+
+NotifyingToolButton::~NotifyingToolButton()
+{
+}
+
+void
+NotifyingToolButton::enterEvent(QEvent *e)
+{
+    QToolButton::enterEvent(e);
+    emit mouseEntered();
+}
+
+void
+NotifyingToolButton::leaveEvent(QEvent *e)
+{
+    QToolButton::enterEvent(e);
+    emit mouseLeft();
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/NotifyingToolButton.h	Fri Jan 13 10:29:50 2017 +0000
@@ -0,0 +1,47 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2007 QMUL.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef SV_NOTIFYING_TOOL_BUTTON_H
+#define SV_NOTIFYING_TOOL_BUTTON_H
+
+#include <QToolButton>
+
+/**
+ * Very trivial enhancement to QToolButton to make it emit signals
+ * when the mouse enters and leaves (for context help). See also
+ * NotifyingPushButton
+ */
+
+class NotifyingToolButton : public QToolButton
+{
+    Q_OBJECT
+
+public:
+    NotifyingToolButton(QWidget *parent = 0) :
+        QToolButton(parent) { }
+
+    virtual ~NotifyingToolButton();
+
+signals:
+    void mouseEntered();
+    void mouseLeft();
+
+protected:
+    virtual void enterEvent(QEvent *);
+    virtual void leaveEvent(QEvent *);
+};
+
+#endif
+
--- a/widgets/PropertyBox.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/widgets/PropertyBox.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -20,23 +20,28 @@
 #include "base/PlayParameters.h"
 #include "base/PlayParameterRepository.h"
 #include "layer/Layer.h"
-#include "layer/ColourDatabase.h"
 #include "base/UnitDatabase.h"
 #include "base/RangeMapper.h"
 
 #include "AudioDial.h"
 #include "LEDButton.h"
 #include "IconLoader.h"
+#include "LevelPanWidget.h"
+#include "LevelPanToolButton.h"
+#include "WidgetScale.h"
 
 #include "NotifyingCheckBox.h"
 #include "NotifyingComboBox.h"
 #include "NotifyingPushButton.h"
-#include "ColourNameDialog.h"
+#include "NotifyingToolButton.h"
+#include "ColourComboBox.h"
+#include "ColourMapComboBox.h"
 
 #include <QGridLayout>
 #include <QHBoxLayout>
 #include <QVBoxLayout>
 #include <QPushButton>
+#include <QToolButton>
 #include <QLabel>
 #include <QFrame>
 #include <QApplication>
@@ -63,6 +68,12 @@
     m_mainBox = new QVBoxLayout;
     setLayout(m_mainBox);
 
+#ifdef Q_OS_MAC
+    QMargins mm = m_mainBox->contentsMargins();
+    QMargins mmhalf(mm.left()/2, mm.top()/3, mm.right()/2, mm.bottom()/3);
+    m_mainBox->setContentsMargins(mmhalf);
+#endif
+
 //    m_nameWidget = new QLabel;
 //    m_mainBox->addWidget(m_nameWidget);
 //    m_nameWidget->setText(container->objectName());
@@ -97,9 +108,6 @@
     connect(UnitDatabase::getInstance(), SIGNAL(unitDatabaseChanged()),
             this, SLOT(unitDatabaseChanged()));
 
-    connect(ColourDatabase::getInstance(), SIGNAL(colourDatabaseChanged()),
-            this, SLOT(colourDatabaseChanged()));
-
 #ifdef DEBUG_PROPERTY_BOX
     cerr << "PropertyBox[" << this << "]::PropertyBox returning" << endl;
 #endif
@@ -141,7 +149,7 @@
     m_viewPlayFrame->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken);
     m_mainBox->addWidget(m_viewPlayFrame);
 
-    QHBoxLayout *layout = new QHBoxLayout;
+    QGridLayout *layout = new QGridLayout;
     m_viewPlayFrame->setLayout(layout);
 
     layout->setMargin(layout->margin() / 2);
@@ -150,32 +158,18 @@
     SVDEBUG << "PropertyBox::populateViewPlayFrame: container " << m_container << " (name " << m_container->getPropertyContainerName() << ") params " << params << endl;
 #endif
 
-    if (layer) {
-	QLabel *showLabel = new QLabel(tr("Show"));
-	layout->addWidget(showLabel);
-	layout->setAlignment(showLabel, Qt::AlignVCenter);
+    QSize buttonSize = WidgetScale::scaleQSize(QSize(26, 26));
+    int col = 0;
 
-	m_showButton = new LEDButton(Qt::blue);
-	layout->addWidget(m_showButton);
-	connect(m_showButton, SIGNAL(stateChanged(bool)),
-		this, SIGNAL(showLayer(bool)));
-        connect(m_showButton, SIGNAL(mouseEntered()),
-                this, SLOT(mouseEnteredWidget()));
-        connect(m_showButton, SIGNAL(mouseLeft()),
-                this, SLOT(mouseLeftWidget()));
-	layout->setAlignment(m_showButton, Qt::AlignVCenter);
-    }
-    
     if (params) {
-
-	QLabel *playLabel = new QLabel(tr("Play"));
-	layout->addWidget(playLabel);
-	layout->setAlignment(playLabel, Qt::AlignVCenter);
-
-	m_playButton = new LEDButton(Qt::darkGreen);
-        m_playButton->setState(!params->isPlayMuted());
-	layout->addWidget(m_playButton);
-	connect(m_playButton, SIGNAL(stateChanged(bool)),
+        
+        m_playButton = new NotifyingToolButton;
+        m_playButton->setCheckable(true);
+        m_playButton->setIcon(IconLoader().load("speaker"));
+        m_playButton->setToolTip(tr("Click to toggle playback"));
+        m_playButton->setChecked(!params->isPlayMuted());
+        m_playButton->setFixedSize(buttonSize);
+	connect(m_playButton, SIGNAL(toggled(bool)),
 		this, SLOT(playAudibleButtonChanged(bool)));
         connect(m_playButton, SIGNAL(mouseEntered()),
                 this, SLOT(mouseEnteredWidget()));
@@ -183,76 +177,56 @@
                 this, SLOT(mouseLeftWidget()));
 	connect(params, SIGNAL(playAudibleChanged(bool)),
 		this, SLOT(playAudibleChanged(bool)));
-	layout->setAlignment(m_playButton, Qt::AlignVCenter);
 
-	layout->insertStretch(-1, 10);
+        LevelPanToolButton *levelPan = new LevelPanToolButton;
+        levelPan->setFixedSize(buttonSize);
+        levelPan->setImageSize((buttonSize.height() * 3) / 4);
+        layout->addWidget(levelPan, 0, col++, Qt::AlignCenter);
+        connect(levelPan, SIGNAL(levelChanged(float)),
+                this, SLOT(playGainControlChanged(float)));
+        connect(levelPan, SIGNAL(panChanged(float)),
+                this, SLOT(playPanControlChanged(float)));
+        connect(params, SIGNAL(playGainChanged(float)),
+                levelPan, SLOT(setLevel(float)));
+        connect(params, SIGNAL(playPanChanged(float)),
+                levelPan, SLOT(setPan(float)));
+        connect(levelPan, SIGNAL(mouseEntered()),
+                this, SLOT(mouseEnteredWidget()));
+        connect(levelPan, SIGNAL(mouseLeft()),
+                this, SLOT(mouseLeftWidget()));
+
+	layout->addWidget(m_playButton, 0, col++, Qt::AlignCenter);
 
         if (params->getPlayClipId() != "") {
-            QPushButton *playParamButton =
-                new QPushButton(QIcon(":icons/faders.png"), "");
-            playParamButton->setFixedWidth(24);
-            playParamButton->setFixedHeight(24);
-            layout->addWidget(playParamButton);
+            QToolButton *playParamButton = new QToolButton;
+            playParamButton->setObjectName("playParamButton");
+            playParamButton->setIcon(IconLoader().load("faders"));
+            playParamButton->setFixedSize(buttonSize);
+            layout->addWidget(playParamButton, 0, col++, Qt::AlignCenter);
             connect(playParamButton, SIGNAL(clicked()),
                     this, SLOT(editPlayParameters()));
+            connect(playParamButton, SIGNAL(mouseEntered()),
+                    this, SLOT(mouseEnteredWidget()));
+            connect(playParamButton, SIGNAL(mouseLeft()),
+                    this, SLOT(mouseLeftWidget()));
         }
+    }
 
-	AudioDial *gainDial = new AudioDial;
-	layout->addWidget(gainDial);
-	gainDial->setMeterColor(Qt::darkRed);
-	gainDial->setMinimum(-50);
-	gainDial->setMaximum(50);
-	gainDial->setPageStep(1);
-	gainDial->setFixedWidth(24);
-	gainDial->setFixedHeight(24);
-	gainDial->setNotchesVisible(false);
-	gainDial->setDefaultValue(0);
-        gainDial->setObjectName(tr("Playback Gain"));
-        gainDial->setRangeMapper(new LinearRangeMapper
-                                 (-50, 50, -25, 25, tr("dB")));
-        gainDial->setShowToolTip(true);
-	connect(gainDial, SIGNAL(valueChanged(int)),
-		this, SLOT(playGainDialChanged(int)));
-	connect(params, SIGNAL(playGainChanged(float)),
-		this, SLOT(playGainChanged(float)));
-	connect(this, SIGNAL(changePlayGainDial(int)),
-		gainDial, SLOT(setValue(int)));
-        connect(gainDial, SIGNAL(mouseEntered()),
+    layout->setColumnStretch(col++, 10);
+
+    if (layer) {
+
+	QLabel *showLabel = new QLabel(tr("Show"));
+	layout->addWidget(showLabel, 0, col++, Qt::AlignVCenter | Qt::AlignRight);
+
+	m_showButton = new LEDButton(palette().highlight().color());
+	layout->addWidget(m_showButton, 0, col++, Qt::AlignVCenter | Qt::AlignLeft);
+	connect(m_showButton, SIGNAL(stateChanged(bool)),
+		this, SIGNAL(showLayer(bool)));
+        connect(m_showButton, SIGNAL(mouseEntered()),
                 this, SLOT(mouseEnteredWidget()));
-        connect(gainDial, SIGNAL(mouseLeft()),
+        connect(m_showButton, SIGNAL(mouseLeft()),
                 this, SLOT(mouseLeftWidget()));
-        playGainChanged(params->getPlayGain());
-	layout->setAlignment(gainDial, Qt::AlignVCenter);
-
-	AudioDial *panDial = new AudioDial;
-	layout->addWidget(panDial);
-	panDial->setMeterColor(Qt::darkGreen);
-	panDial->setMinimum(-50);
-	panDial->setMaximum(50);
-	panDial->setPageStep(1);
-	panDial->setFixedWidth(24);
-	panDial->setFixedHeight(24);
-	panDial->setNotchesVisible(false);
-	panDial->setToolTip(tr("Playback Pan / Balance"));
-	panDial->setDefaultValue(0);
-        panDial->setObjectName(tr("Playback Pan / Balance"));
-        panDial->setShowToolTip(true);
-	connect(panDial, SIGNAL(valueChanged(int)),
-		this, SLOT(playPanDialChanged(int)));
-	connect(params, SIGNAL(playPanChanged(float)),
-		this, SLOT(playPanChanged(float)));
-	connect(this, SIGNAL(changePlayPanDial(int)),
-		panDial, SLOT(setValue(int)));
-        connect(panDial, SIGNAL(mouseEntered()),
-                this, SLOT(mouseEnteredWidget()));
-        connect(panDial, SIGNAL(mouseLeft()),
-                this, SLOT(mouseLeftWidget()));
-        playPanChanged(params->getPlayPan());
-	layout->setAlignment(panDial, Qt::AlignVCenter);
-
-    } else {
-
-	layout->insertStretch(-1, 10);
     }
 }
 
@@ -282,39 +256,51 @@
 	      << groupName << "\"" << endl;
 #endif
 
-    bool inGroup = (groupName != QString());
-
+    QString groupLabel = groupName;
+    if (groupName == QString()) {
+        groupName = "ungrouped: " + name; // not tr(), this is internal id
+        groupLabel = propertyLabel;
+    }
+    
     if (!have) {
-	if (inGroup) {
-	    if (m_groupLayouts.find(groupName) == m_groupLayouts.end()) {
-#ifdef DEBUG_PROPERTY_BOX
-		cerr << "PropertyBox: adding label \"" << groupName << "\" and frame for group for \"" << name << "\"" << endl;
+        if (m_groupLayouts.find(groupName) == m_groupLayouts.end()) {
+            QWidget *labelWidget = new QLabel(groupLabel, m_mainWidget);
+            m_layout->addWidget(labelWidget, row, 0);
+            QWidget *frame = new QWidget(m_mainWidget);
+            frame->setMinimumSize(WidgetScale::scaleQSize(QSize(1, 24)));
+            m_groupLayouts[groupName] = new QGridLayout;
+#ifdef Q_OS_MAC
+            // Seems to be plenty of whitespace already
+            m_groupLayouts[groupName]->setContentsMargins(0, 0, 0, 0);
+#else
+            // Need a bit of padding on the left
+            m_groupLayouts[groupName]->setContentsMargins
+                (WidgetScale::scalePixelSize(10), 0, 0, 0);
 #endif
-		m_layout->addWidget(new QLabel(groupName, m_mainWidget), row, 0);
-		QFrame *frame = new QFrame(m_mainWidget);
-		m_layout->addWidget(frame, row, 1, 1, 2);
-		m_groupLayouts[groupName] = new QGridLayout;
-		m_groupLayouts[groupName]->setMargin(0);
-		frame->setLayout(m_groupLayouts[groupName]);
-	    }
-	} else {
-#ifdef DEBUG_PROPERTY_BOX 
-	    cerr << "PropertyBox: adding label \"" << propertyLabel << "\"" << endl;
-#endif
-	    m_layout->addWidget(new QLabel(propertyLabel, m_mainWidget), row, 0);
-	}
+            frame->setLayout(m_groupLayouts[groupName]);
+            m_layout->addWidget(frame, row, 1, 1, 2);
+            m_layout->setColumnStretch(1, 10);
+        }
     }
 
+    QGridLayout *groupLayout = m_groupLayouts[groupName];
+
+#ifdef DEBUG_PROPERTY_BOX
+    cerr << "groupName becomes \"" << groupName << "\", groupLabel = \""
+         << groupLabel << "\", groupLayout = " << groupLayout << endl;
+#endif
+    
+    assert(groupLayout);
+
+    QWidget *existing = m_propertyControllers[name];
+    
     switch (type) {
 
     case PropertyContainer::ToggleProperty:
     {
-        QAbstractButton *button = 0;
+        QAbstractButton *button;
 
-	if (have) {
-            button = dynamic_cast<QAbstractButton *>(m_propertyControllers[name]);
-            assert(button);
-	} else {
+	if (!(button = qobject_cast<QAbstractButton *>(existing))) {
 #ifdef DEBUG_PROPERTY_BOX 
 	    cerr << "PropertyBox: creating new checkbox" << endl;
 #endif
@@ -324,7 +310,7 @@
                 QIcon icon(IconLoader().load(iconName));
                 button->setIcon(icon);
                 button->setObjectName(name);
-                button->setFixedSize(QSize(18, 18));
+                button->setFixedSize(WidgetScale::scaleQSize(QSize(18, 18)));
             } else {
                 button = new NotifyingCheckBox();
                 button->setObjectName(name);
@@ -335,13 +321,15 @@
                     this, SLOT(mouseEnteredWidget()));
             connect(button, SIGNAL(mouseLeft()),
                     this, SLOT(mouseLeftWidget()));
-	    if (inGroup) {
-		button->setToolTip(propertyLabel);
-		m_groupLayouts[groupName]->addWidget
-                    (button, 0, m_groupLayouts[groupName]->columnCount());
-	    } else {
-		m_layout->addWidget(button, row, 1, 1, 2);
-	    }
+            button->setToolTip(propertyLabel);
+
+            if (existing) {
+                groupLayout->replaceWidget(existing, button);
+                delete existing;
+            } else {
+                groupLayout->addWidget(button, 0, groupLayout->columnCount());
+            }
+
 	    m_propertyControllers[name] = button;
 	}
 
@@ -357,9 +345,7 @@
     {
 	AudioDial *dial;
 
-	if (have) {
-	    dial = dynamic_cast<AudioDial *>(m_propertyControllers[name]);
-	    assert(dial);
+	if ((dial = qobject_cast<AudioDial *>(existing))) {
             if (rangeChanged) {
                 dial->blockSignals(true);
                 dial->setMinimum(min);
@@ -367,8 +353,7 @@
                 dial->setRangeMapper(m_container->getNewPropertyRangeMapper(name));
                 dial->blockSignals(false);
             }
-                
-	} else {
+        } else {
 #ifdef DEBUG_PROPERTY_BOX 
 	    cerr << "PropertyBox: creating new dial" << endl;
 #endif
@@ -378,8 +363,10 @@
 	    dial->setMaximum(max);
 	    dial->setPageStep(1);
 	    dial->setNotchesVisible((max - min) <= 12);
+            // important to set the range mapper before the default,
+            // because the range mapper is used to map the default
+            dial->setRangeMapper(m_container->getNewPropertyRangeMapper(name));
 	    dial->setDefaultValue(deflt);
-            dial->setRangeMapper(m_container->getNewPropertyRangeMapper(name));
             dial->setShowToolTip(true);
 	    connect(dial, SIGNAL(valueChanged(int)),
 		    this, SLOT(propertyControllerChanged(int)));
@@ -388,21 +375,15 @@
             connect(dial, SIGNAL(mouseLeft()),
                     this, SLOT(mouseLeftWidget()));
 
-	    if (inGroup) {
-		dial->setFixedWidth(24);
-		dial->setFixedHeight(24);
-		m_groupLayouts[groupName]->addWidget
-                    (dial, 0, m_groupLayouts[groupName]->columnCount());
-	    } else {
-		dial->setFixedWidth(32);
-		dial->setFixedHeight(32);
-		m_layout->addWidget(dial, row, 1);
-		QLabel *label = new QLabel(m_mainWidget);
-		connect(dial, SIGNAL(valueChanged(int)),
-			label, SLOT(setNum(int)));
-		label->setNum(value);
-		m_layout->addWidget(label, row, 2);
-	    }
+            dial->setFixedWidth(WidgetScale::scalePixelSize(24));
+            dial->setFixedHeight(WidgetScale::scalePixelSize(24));
+
+            if (existing) {
+                groupLayout->replaceWidget(existing, dial);
+                delete existing;
+            } else {
+                groupLayout->addWidget(dial, 0, groupLayout->columnCount());
+            }
 
 	    m_propertyControllers[name] = dial;
 	}
@@ -415,20 +396,94 @@
 	break;
     }
 
+    case PropertyContainer::ColourProperty:
+    {
+        ColourComboBox *cb;
+        
+	if (!(cb = qobject_cast<ColourComboBox *>(existing))) {
+
+#ifdef DEBUG_PROPERTY_BOX 
+	    cerr << "PropertyBox: creating new colour combobox" << endl;
+#endif
+            cb = new ColourComboBox(true);
+            cb->setObjectName(name);
+
+	    connect(cb, SIGNAL(colourChanged(int)),
+		    this, SLOT(propertyControllerChanged(int)));
+            connect(cb, SIGNAL(mouseEntered()),
+                    this, SLOT(mouseEnteredWidget()));
+            connect(cb, SIGNAL(mouseLeft()),
+                    this, SLOT(mouseLeftWidget()));
+
+            cb->setToolTip(propertyLabel);
+
+            if (existing) {
+                groupLayout->replaceWidget(existing, cb);
+                delete existing;
+            } else {
+                groupLayout->addWidget(cb, 0, groupLayout->columnCount());
+            }
+            
+	    m_propertyControllers[name] = cb;
+	}
+
+        if (cb->currentIndex() != value) {
+            cb->blockSignals(true);
+            cb->setCurrentIndex(value);
+            cb->blockSignals(false);
+        }
+
+        break;
+    }        
+
+    case PropertyContainer::ColourMapProperty:
+    {
+        ColourMapComboBox *cb;
+
+        if (!(cb = qobject_cast<ColourMapComboBox *>(existing))) {
+#ifdef DEBUG_PROPERTY_BOX 
+	    cerr << "PropertyBox: creating new colourmap combobox" << endl;
+#endif
+            cb = new ColourMapComboBox(false);
+            cb->setObjectName(name);
+
+	    connect(cb, SIGNAL(colourMapChanged(int)),
+		    this, SLOT(propertyControllerChanged(int)));
+            connect(cb, SIGNAL(mouseEntered()),
+                    this, SLOT(mouseEnteredWidget()));
+            connect(cb, SIGNAL(mouseLeft()),
+                    this, SLOT(mouseLeftWidget()));
+            
+            cb->setToolTip(propertyLabel);
+
+            if (existing) {
+                groupLayout->replaceWidget(existing, cb);
+                delete existing;
+            } else {
+                groupLayout->addWidget(cb, 0, groupLayout->columnCount());
+            }
+            
+	    m_propertyControllers[name] = cb;
+	}
+
+        if (cb->currentIndex() != value) {
+            cb->blockSignals(true);
+            cb->setCurrentIndex(value);
+            cb->blockSignals(false);
+        }
+
+        break;
+    }        
+
     case PropertyContainer::ValueProperty:
     case PropertyContainer::UnitsProperty:
-    case PropertyContainer::ColourProperty:
     {
 	NotifyingComboBox *cb;
 
-	if (have) {
-	    cb = dynamic_cast<NotifyingComboBox *>(m_propertyControllers[name]);
-	    assert(cb);
-	} else {
+	if (!(cb = qobject_cast<NotifyingComboBox *>(existing))) {
 #ifdef DEBUG_PROPERTY_BOX 
 	    cerr << "PropertyBox: creating new combobox" << endl;
 #endif
-
 	    cb = new NotifyingComboBox();
 	    cb->setObjectName(name);
             cb->setDuplicatesEnabled(false);
@@ -443,10 +498,19 @@
             if (type == PropertyContainer::ValueProperty) {
 
                 for (int i = min; i <= max; ++i) {
-                    cb->addItem(m_container->getPropertyValueLabel(name, i));
+
+                    QString label = m_container->getPropertyValueLabel(name, i);
+                    QString iname = m_container->getPropertyValueIconName(name, i);
+
+                    if (iname != "") {
+                        QIcon icon(IconLoader().load(iname));
+                        cb->addItem(icon, label);
+                    } else {
+                        cb->addItem(label);
+                    }
                 }
 
-            } else if (type == PropertyContainer::UnitsProperty) {
+            } else { // PropertyContainer::UnitsProperty
 
                 QStringList units = UnitDatabase::getInstance()->getKnownUnits();
                 for (int i = 0; i < units.size(); ++i) {
@@ -454,23 +518,6 @@
                 }
 
                 cb->setEditable(true);
-
-            } else { // ColourProperty
-
-                //!!! should be a proper colour combobox class that
-                // manages its own Add New Colour entry...
-                
-                ColourDatabase *db = ColourDatabase::getInstance();
-                for (int i = 0; i < db->getColourCount(); ++i) {
-                    QString name = db->getColourName(i);
-                    cb->addItem(db->getExamplePixmap(i, QSize(12, 12)), name);
-                }
-                cb->addItem(tr("Add New Colour..."));
-            }                
-                
-            cb->blockSignals(false);
-            if (cb->count() < 20 && cb->count() > cb->maxVisibleItems()) {
-                cb->setMaxVisibleItems(cb->count());
             }
         }
 
@@ -482,19 +529,16 @@
             connect(cb, SIGNAL(mouseLeft()),
                     this, SLOT(mouseLeftWidget()));
 
-	    if (inGroup) {
-		cb->setToolTip(propertyLabel);
-		m_groupLayouts[groupName]->addWidget
-                    (cb, 0, m_groupLayouts[groupName]->columnCount());
-	    } else {
-		m_layout->addWidget(cb, row, 1, 1, 2);
-	    }
+            cb->setToolTip(propertyLabel);
+            groupLayout->addWidget(cb, 0, groupLayout->columnCount());
 	    m_propertyControllers[name] = cb;
-	}
+	} else if (existing != cb) {
+            groupLayout->replaceWidget(existing, cb);
+            delete existing;
+        }
 
         cb->blockSignals(true);
-        if (type == PropertyContainer::ValueProperty ||
-            type == PropertyContainer::ColourProperty) {
+        if (type == PropertyContainer::ValueProperty) {
             if (cb->currentIndex() != value) {
                 cb->setCurrentIndex(value);
             }
@@ -511,11 +555,6 @@
         }
         cb->blockSignals(false);
 
-#ifdef Q_OS_MAC
-	// Crashes on startup without this, for some reason
-	cb->setMinimumSize(QSize(10, 10));
-#endif
-
 	break;
     }
 
@@ -583,22 +622,6 @@
 }    
 
 void
-PropertyBox::colourDatabaseChanged()
-{
-    blockSignals(true);
-
-    PropertyContainer::PropertyList properties = m_container->getProperties();
-    for (size_t i = 0; i < properties.size(); ++i) {
-        if (m_container->getPropertyType(properties[i]) ==
-            PropertyContainer::ColourProperty) {
-            updatePropertyEditor(properties[i], true);
-        }
-    }
-
-    blockSignals(false);
-}    
-
-void
 PropertyBox::propertyControllerChanged(bool on)
 {
     propertyControllerChanged(on ? 1 : 0);
@@ -620,24 +643,13 @@
 
     if (type == PropertyContainer::UnitsProperty) {
 
-        NotifyingComboBox *cb = dynamic_cast<NotifyingComboBox *>(obj);
+        NotifyingComboBox *cb = qobject_cast<NotifyingComboBox *>(obj);
         if (cb) {
             QString unit = cb->currentText();
             c = m_container->getSetPropertyCommand
                 (name, UnitDatabase::getInstance()->getUnitId(unit));
         }
 
-    } else if (type == PropertyContainer::ColourProperty) {
-
-        if (value == int(ColourDatabase::getInstance()->getColourCount())) {
-            addNewColour();
-            if (value == int(ColourDatabase::getInstance()->getColourCount())) {
-                propertyContainerPropertyChanged(m_container);
-                return;
-            }
-        }
-        c = m_container->getSetPropertyCommand(name, value);
-
     } else if (type != PropertyContainer::InvalidProperty) {
 
 	c = m_container->getSetPropertyCommand(name, value);
@@ -649,27 +661,9 @@
 }
 
 void
-PropertyBox::addNewColour()
-{
-    QColor newColour = QColorDialog::getColor();
-    if (!newColour.isValid()) return;
-
-    ColourNameDialog dialog(tr("Name New Colour"),
-                            tr("Enter a name for the new colour:"),
-                            newColour, newColour.name(), this);
-    dialog.showDarkBackgroundCheckbox(tr("Prefer black background for this colour"));
-    if (dialog.exec() == QDialog::Accepted) {
-        //!!! command
-        ColourDatabase *db = ColourDatabase::getInstance();
-        int index = db->addColour(newColour, dialog.getColourName());
-        db->setUseDarkBackground(index, dialog.isDarkBackgroundChecked());
-    }
-}
-
-void
 PropertyBox::playAudibleChanged(bool audible)
 {
-    m_playButton->setState(audible);
+    m_playButton->setChecked(audible);
 }
 
 void
@@ -685,26 +679,15 @@
         CommandHistory::getInstance()->addCommand(command, true, true);
     }
 }
-    
-void
-PropertyBox::playGainChanged(float gain)
-{
-    int dialValue = int(lrint(log10(gain) * 20.0));
-    if (dialValue < -50) dialValue = -50;
-    if (dialValue >  50) dialValue =  50;
-    emit changePlayGainDial(dialValue);
-}
 
 void
-PropertyBox::playGainDialChanged(int dialValue)
+PropertyBox::playGainControlChanged(float gain)
 {
     QObject *obj = sender();
 
     PlayParameters *params = m_container->getPlayParameters();
     if (!params) return;
 
-    float gain = float(pow(10, float(dialValue) / 20.0));
-
     if (params->getPlayGain() != gain) {
         PlayParameterRepository::EditCommand *command =
             new PlayParameterRepository::EditCommand(params);
@@ -714,28 +697,15 @@
 
     updateContextHelp(obj);
 }
-    
-void
-PropertyBox::playPanChanged(float pan)
-{
-    int dialValue = int(lrint(pan * 50.0));
-    if (dialValue < -50) dialValue = -50;
-    if (dialValue >  50) dialValue =  50;
-    emit changePlayPanDial(dialValue);
-}
 
 void
-PropertyBox::playPanDialChanged(int dialValue)
+PropertyBox::playPanControlChanged(float pan)
 {
     QObject *obj = sender();
 
     PlayParameters *params = m_container->getPlayParameters();
     if (!params) return;
 
-    float pan = float(dialValue) / 50.f;
-    if (pan < -1.f) pan = -1.f;
-    if (pan >  1.f) pan =  1.f;
-
     if (params->getPlayPan() != pan) {
         PlayParameterRepository::EditCommand *command =
             new PlayParameterRepository::EditCommand(params);
@@ -820,17 +790,34 @@
 void
 PropertyBox::updateContextHelp(QObject *o)
 {
-    QWidget *w = dynamic_cast<QWidget *>(o);
+    QWidget *w = qobject_cast<QWidget *>(o);
     if (!w) return;
 
     if (!m_container) return;
     QString cname = m_container->getPropertyContainerName();
     if (cname == "") return;
 
+    LevelPanToolButton *lp = qobject_cast<LevelPanToolButton *>(w);
+    if (lp) {
+        emit contextHelpChanged(tr("Adjust playback level and pan of %1").arg(cname));
+        return;
+    }
+
     QString wname = w->objectName();
 
+    if (wname == "playParamButton") {
+        PlayParameters *params = m_container->getPlayParameters();
+        if (params) {
+            emit contextHelpChanged
+                (tr("Change sound used for playback (currently \"%1\")")
+                 .arg(params->getPlayClipId()));
+            return;
+        }
+    }
+    
     QString extraText;
-    AudioDial *dial = dynamic_cast<AudioDial *>(w);
+    
+    AudioDial *dial = qobject_cast<AudioDial *>(w);
     if (dial) {
         double mv = dial->mappedValue();
         QString unit = "";
@@ -848,7 +835,7 @@
         emit contextHelpChanged(tr("Toggle Playback of %1").arg(cname));
     } else if (wname == "") {
         return;
-    } else if (dynamic_cast<QAbstractButton *>(w)) {
+    } else if (qobject_cast<QAbstractButton *>(w)) {
         emit contextHelpChanged(tr("Toggle %1 property of %2")
                                 .arg(wname).arg(cname));
     } else {
--- a/widgets/PropertyBox.h	Fri Mar 04 12:23:31 2016 +0000
+++ b/widgets/PropertyBox.h	Fri Jan 13 10:29:50 2017 +0000
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _PROPERTY_BOX_H_
-#define _PROPERTY_BOX_H_
+#ifndef SV_PROPERTY_BOX_H
+#define SV_PROPERTY_BOX_H
 
 #include "base/PropertyContainer.h"
 
@@ -27,6 +27,8 @@
 class QVBoxLayout;
 class QLabel;
 class LEDButton;
+class QToolButton;
+class NotifyingPushButton;
 
 class PropertyBox : public QFrame
 {
@@ -39,8 +41,6 @@
     PropertyContainer *getContainer() { return m_container; }
 
 signals:
-    void changePlayGainDial(int);
-    void changePlayPanDial(int);
     void showLayer(bool);
     void contextHelpChanged(const QString &);
 
@@ -56,15 +56,12 @@
 
     void playAudibleChanged(bool);
     void playAudibleButtonChanged(bool);
-    void playGainChanged(float);
-    void playGainDialChanged(int);
-    void playPanChanged(float);
-    void playPanDialChanged(int);
+    void playGainControlChanged(float);
+    void playPanControlChanged(float);
 
     void populateViewPlayFrame();
 
     void unitDatabaseChanged();
-    void colourDatabaseChanged();
 
     void editPlayParameters();
 
@@ -75,7 +72,6 @@
     void updatePropertyEditor(PropertyContainer::PropertyName,
                               bool rangeChanged = false);
     void updateContextHelp(QObject *o);
-    void addNewColour();
 
     QLabel *m_nameWidget;
     QWidget *m_mainWidget;
@@ -84,7 +80,7 @@
     QFrame *m_viewPlayFrame;
     QVBoxLayout *m_mainBox;
     LEDButton *m_showButton;
-    LEDButton *m_playButton;
+    QToolButton *m_playButton;
     std::map<QString, QGridLayout *> m_groupLayouts;
     std::map<QString, QWidget *> m_propertyControllers;
 };
--- a/widgets/PropertyStack.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/widgets/PropertyStack.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -25,6 +25,8 @@
 #include "widgets/CommandHistory.h"
 #include "layer/ShowLayerCommand.h"
 
+#include "WidgetScale.h"
+
 #include <QIcon>
 #include <QTabWidget>
 
@@ -45,12 +47,10 @@
 
     setTabBar(bar);
 
-#if (QT_VERSION >= 0x0402)
     setElideMode(Qt::ElideNone); 
     tabBar()->setUsesScrollButtons(true); 
-    tabBar()->setIconSize(QSize(16, 16));
-#endif
-    
+    tabBar()->setIconSize(WidgetScale::scaleQSize(QSize(16, 16)));
+
     repopulate();
 
     connect(this, SIGNAL(currentChanged(int)),
--- a/widgets/Thumbwheel.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/widgets/Thumbwheel.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -445,35 +445,36 @@
 
     if (!m_cache.isNull()) {
         QPainter paint(this);
-        paint.drawImage(0, 0, m_cache);
+        paint.drawImage(rect(), m_cache, m_cache.rect());
         return;
     }
 
     Profiler profiler2("Thumbwheel::paintEvent (no cache)");
 
-    m_cache = QImage(size(), QImage::Format_ARGB32);
+    QSize imageSize = size() * devicePixelRatio();
+    m_cache = QImage(imageSize, QImage::Format_ARGB32);
     m_cache.fill(Qt::transparent);
 
-    int bw = 3;
+    int w = m_cache.width();
+    int h = m_cache.height();
+    int bw = 3; // border width
 
     QRect subclip;
     if (m_orientation == Qt::Horizontal) {
-        subclip = QRect(bw, bw+1, width() - bw*2, height() - bw*2 - 2);
+        subclip = QRect(bw, bw+1, w - bw*2, h - bw*2 - 2);
     } else {
-        subclip = QRect(bw+1, bw, width() - bw*2 - 2, height() - bw*2);
+        subclip = QRect(bw+1, bw, w - bw*2 - 2, h - bw*2);
     }
 
     QPainter paint(&m_cache);
-    paint.setClipRect(rect());
+    paint.setClipRect(m_cache.rect());
     paint.fillRect(subclip, palette().background().color());
 
     paint.setRenderHint(QPainter::Antialiasing, true);
 
-    double w  = width();
     double w0 = 0.5;
     double w1 = w - 0.5;
 
-    double h  = height();
     double h0 = 0.5;
     double h1 = h - 0.5;
 
@@ -508,13 +509,13 @@
 
 //    cerr << "value = " << m_value << ", min = " << m_min << ", max = " << m_max << ", rotation = " << rotation << endl;
 
-    w = (m_orientation == Qt::Horizontal ? width() : height()) - bw*2;
+    int ww = (m_orientation == Qt::Horizontal ? w : h) - bw*2; // wheel width
 
     // total number of notches on the entire wheel
     int notches = 25;
     
     // radius of the wheel including invisible part
-    int radius = int(w / 2 + 2);
+    int radius = int(ww / 2 + 2);
 
     for (int i = 0; i < notches; ++i) {
 
@@ -525,13 +526,13 @@
         double depth = cos((a0 + a2) / 2);
         if (depth < 0) continue;
 
-        double x0 = radius * sin(a0) + w/2;
-        double x1 = radius * sin(a1) + w/2;
-        double x2 = radius * sin(a2) + w/2;
-        if (x2 < 0 || x0 > w) continue;
+        double x0 = radius * sin(a0) + ww/2;
+        double x1 = radius * sin(a1) + ww/2;
+        double x2 = radius * sin(a2) + ww/2;
+        if (x2 < 0 || x0 > ww) continue;
 
         if (x0 < 0) x0 = 0;
-        if (x2 > w) x2 = w;
+        if (x2 > ww) x2 = ww;
 
         x0 += bw;
         x1 += bw;
@@ -557,10 +558,10 @@
             }
             
             if (m_orientation == Qt::Horizontal) {
-                paint.drawRect(QRectF(x1, height() - (height() - bw*2) * prop - bw,
-                                      x2 - x1, height() * prop));
+                paint.drawRect(QRectF(x1, h - (h - bw*2) * prop - bw,
+                                      x2 - x1, h * prop));
             } else {
-                paint.drawRect(QRectF(bw, x1, (width() - bw*2) * prop, x2 - x1));
+                paint.drawRect(QRectF(bw, x1, (w - bw*2) * prop, x2 - x1));
             }
         }
 
@@ -568,14 +569,14 @@
         paint.setBrush(palette().background().color());
 
         if (m_orientation == Qt::Horizontal) {
-            paint.drawRect(QRectF(x0, bw, x1 - x0, height() - bw*2));
+            paint.drawRect(QRectF(x0, bw, x1 - x0, h - bw*2));
         } else {
-            paint.drawRect(QRectF(bw, x0, width() - bw*2, x1 - x0));
+            paint.drawRect(QRectF(bw, x0, w - bw*2, x1 - x0));
         }
     }
 
     QPainter paint2(this);
-    paint2.drawImage(0, 0, m_cache);
+    paint2.drawImage(rect(), m_cache, m_cache.rect());
 }
 
 QSize
--- a/widgets/UnitConverter.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/widgets/UnitConverter.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -325,7 +325,7 @@
     int note, octave;
     Pitch::getNoteAndOctaveForPitch(pitch, note, octave);
 
-    cerr << "pitch " << pitch << " note " << note << " octave " << octave << " cents " << cents << endl;
+//    cerr << "pitch " << pitch << " note " << note << " octave " << octave << " cents " << cents << endl;
 
     setTo(m_midi, pitch);
     setTo(m_cents, cents);
@@ -387,7 +387,7 @@
     double samples = m_samples->value();
     double rate = getSampleRate();
 
-    cerr << samples << " samples at rate " << rate << endl;
+//    cerr << samples << " samples at rate " << rate << endl;
 
     double sec = samples / rate;
     double hz = rate / samples;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/WidgetScale.h	Fri Jan 13 10:29:50 2017 +0000
@@ -0,0 +1,59 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    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.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef SV_WIDGET_SCALE_H
+#define SV_WIDGET_SCALE_H
+
+#include <QFont>
+#include <QFontMetrics>
+
+#include "base/Debug.h"
+
+class WidgetScale
+{
+public:   
+    /**
+     * Take a "design pixel" size and scale it for the actual
+     * display. This is relevant to hi-dpi systems that do not do
+     * pixel doubling (i.e. Windows and Linux rather than OS/X).
+     */
+    static int scalePixelSize(int pixels) {
+
+        static double ratio = 0.0;
+        if (ratio == 0.0) {
+            double baseEm;
+#ifdef Q_OS_MAC
+            baseEm = 17.0;
+#else
+            baseEm = 15.0;
+#endif
+            double em = QFontMetrics(QFont()).height();
+            ratio = em / baseEm;
+            SVDEBUG << "WidgetScale::scalePixelSize: baseEm = " << baseEm
+                    << ", platform default font height = " << em
+                    << ", resulting scale factor = " << ratio << endl;
+        }
+
+        int scaled = int(pixels * ratio + 0.5);
+        if (pixels != 0 && scaled == 0) scaled = 1;
+        return scaled;
+    }
+
+    static QSize scaleQSize(QSize size) {
+        return QSize(scalePixelSize(size.width()),
+                     scalePixelSize(size.height()));
+    }
+};
+
+#endif
--- a/widgets/WindowShapePreview.cpp	Fri Mar 04 12:23:31 2016 +0000
+++ b/widgets/WindowShapePreview.cpp	Fri Jan 13 10:29:50 2017 +0000
@@ -22,13 +22,14 @@
 #include <QFont>
 #include <QString>
 
-#include "data/fft/FFTapi.h"
+#include <bqfft/FFT.h>
 
+#include <vector>
+#include <complex>
 #include <iostream>
 
-#ifndef __GNUC__
-#include <alloca.h>
-#endif
+using namespace std;
+
 
 WindowShapePreview::WindowShapePreview(QWidget *parent) :
     QFrame(parent),
@@ -50,12 +51,17 @@
 void
 WindowShapePreview::updateLabels()
 {
-    int step = 24;
-    float peak = 48;
-    int w = step * 4, h = 64;
+    float scaleRatio = float(QFontMetrics(font()).height()) / 14.f;
+    if (scaleRatio < 1.f) scaleRatio = 1.f;
+
+    int step = int(24 * scaleRatio);
+    float peak = float(48 * scaleRatio);
+
+    int w = step * 4, h = int((peak * 4) / 3);
+
     WindowType type = m_windowType;
     Window<float> windower = Window<float>(type, step * 2);
-
+    
     QPixmap timeLabel(w, h + 1);
     timeLabel.fill(Qt::white);
     QPainter timePainter(&timeLabel);
@@ -71,12 +77,7 @@
     
     path = QPainterPath();
 
-#ifdef __GNUC__
-    float acc[w];
-#else
-    float *acc = (float *)alloca(w * sizeof(float));
-#endif
-
+    float *acc = new float[w];
     for (int i = 0; i < w; ++i) acc[i] = 0.f;
     for (int j = 0; j < 3; ++j) {
         for (int i = 0; i < step * 2; ++i) {
@@ -88,6 +89,7 @@
         if (i == 0) path.moveTo(i, y);
         else path.lineTo(i, y);
     }
+    delete[] acc;
 
     timePainter.drawPath(path);
     timePainter.setRenderHint(QPainter::Antialiasing, false);
@@ -112,7 +114,7 @@
     timePainter.drawPath(path);
 
     QFont font;
-    font.setPixelSize(10);
+    font.setPixelSize(int(10 * scaleRatio));
     font.setItalic(true);
     timePainter.setFont(font);
     QString label = tr("V / time");
@@ -121,33 +123,32 @@
 
     m_windowTimeExampleLabel->setPixmap(timeLabel);
     
-    int fw = 100;
-
-    QPixmap freqLabel(fw, h + 1);
+    QPixmap freqLabel(w, h + 1);
     freqLabel.fill(Qt::white);
     QPainter freqPainter(&freqLabel);
     path = QPainterPath();
 
     int fftsize = 512;
 
-    float *input = (float *)fftf_malloc(fftsize * sizeof(float));
-    fftf_complex *output =
-        (fftf_complex *)fftf_malloc(fftsize * sizeof(fftf_complex));
-    fftf_plan plan = fftf_plan_dft_r2c_1d(fftsize, input, output,
-                                            FFTW_ESTIMATE);
+    breakfastquay::FFT fft(fftsize);
+
+    vector<float> input(fftsize);
+    vector<complex<float>> output(fftsize/2 + 1);
+    
     for (int i = 0; i < fftsize; ++i) input[i] = 0.f;
     for (int i = 0; i < step * 2; ++i) {
         input[fftsize/2 - step + i] = windower.getValue(i);
     }
-    
-    fftf_execute(plan);
-    fftf_destroy_plan(plan);
+
+    fft.forwardInterleaved(input.data(), reinterpret_cast<float *>(output.data()));
 
     float maxdb = 0.f;
     float mindb = 0.f;
     bool first = true;
     for (int i = 0; i < fftsize/2; ++i) {
-        float power = output[i][0] * output[i][0] + output[i][1] * output[i][1];
+        float power =
+            output[i].real() * output[i].real() +
+            output[i].imag() * output[i].imag();
         float db = mindb;
         if (power > 0) {
             db = 20.f * log10f(power);
@@ -168,7 +169,7 @@
 //    float ly = h - ((-80.f + -mindb) / maxval) * peak + 1;
 
     path.moveTo(0, float(h) - peak + 1);
-    path.lineTo(fw, float(h) - peak + 1);
+    path.lineTo(w, float(h) - peak + 1);
 
     freqPainter.setPen(Qt::gray);
     freqPainter.setRenderHint(QPainter::Antialiasing, true);
@@ -180,27 +181,26 @@
 //    cerr << "maxdb = " << maxdb << ", mindb = " << mindb << ", maxval = " <<maxval << endl;
 
     for (int i = 0; i < fftsize/2; ++i) {
-        float power = output[i][0] * output[i][0] + output[i][1] * output[i][1];
+        float power =
+            output[i].real() * output[i].real() +
+            output[i].imag() * output[i].imag();
         float db = 20.f * log10f(power);
         float val = db + -mindb;
         if (val < 0) val = 0;
         float norm = val / maxval;
-        float x = (float(fw) / float(fftsize/2)) * float(i);
+        float x = (float(w) / float(fftsize/2)) * float(i);
         float y = float(h) - norm * peak + 1;
         if (i == 0) path.moveTo(x, y);
         else path.lineTo(x, y);
     }
 
     freqPainter.setRenderHint(QPainter::Antialiasing, true);
-    path.addRect(0, 0, fw, h + 1);
+    path.addRect(0, 0, w, h + 1);
     freqPainter.drawPath(path);
 
-    fftf_free(input);
-    fftf_free(output);
-
     freqPainter.setFont(font);
     label = tr("dB / freq");
-    freqPainter.drawText(fw - freqPainter.fontMetrics().width(label) - 4,
+    freqPainter.drawText(w - freqPainter.fontMetrics().width(label) - 4,
                          freqPainter.fontMetrics().ascent() + 1, label);
 
     m_windowFreqExampleLabel->setPixmap(freqLabel);